Szenvedés a hangokkal

Egy eszközt szeretnék készíteni, ami a felhasználóra reagálva generál hangot. Rájöttem, jogy ha a hullámformát kitalálom és megcsinálpm numpy-ban, a felhasználót a pygame-en keresztül tudom figyelni, a pygame.sndarray és a pygame.mixer modulok pedig előállítják és lejátszák a hangot. Nézzük az első felét.

Találtam egy jó példát arról, hogyan lehet szinusz-hullámot csinálni numpy-jal, ebből indultam ki. Definiáltam egy frekvenciát és egy mintaelemszámot, és megpróbáltam egy fűrészfogat készíteni, ami valahogy így néz ki:
[0, 0.25, 0.5, 0.75, 1, 0, 0.25, 0.5, 0.75, 1, ...]

A numply linspace függvényét használtam a fűrész egyetlen fogához, ami olyan hosszú, mint a minták számának egy frekvenciányi része:
fs = 44100
freq=440.
np.linspace(-1,1,fs/freq)

Ovasgatván a pygame documetációt arra is rájöttem, hogy mindennek a legelején a mixer modult kell inicializálni, és megnézni, milyen csatornát hozott létre:
import pygame
import pygame.mixer as mixer
import pygame.snarray as snd
#
mixer.init()
mixer.get_init()
# (22050, -16, 2)

Tehát két oszlopban, 16 bites számokból kell létrehoznom a huullámot.

Mivel egy fűrészhegy már megvan, kettőt kell belőle csinálnom. Erre a tile és a reshape függvény tökéletesnek tűnik: az egyetlen fogat megismétlem, majd a teljes tömböt félbevégom.
import numpy as np
fs = mixer.get_init()[0]
chs = mixer.get_init()[2]
tst_sound = np.tile(np.linspace(-1,1,fs/freq), chs).reshape(fs/freq, chs)

Már van egy nagyon rövid hangom, már csak meg kell ismételni freq-szer hogy egy másodpercig tartson. Bökkenő, hogy a tile függvény az oszlopok számát sokszorozza meg, ami most probléma, mégha érthető is. Ennek orvoslására találtam meg a transpose függvényt. Felcseréltem a sorokat és az oszlopokat, az új oszlopokat megsokszoroztam, majd visszacseréltem őket.
tst_second = np.transpose(np.tile(np.transpose(tst_sound), freq))
tst_snd = snd.make_sound(tst_second)

Ha a hang alakja megfelelő, a make_sound függvény elvileg nem ad hibát, nekem legalábbis amint megoldottam az oszlop-problémát, működött, s csak le kellett játszanom a moxer modullal:
tst_snd.play()

Fúúj. Nem ezt a hangot akartam. Nem véletlen mondja meg a mixer.get_init(), milyen típusú számokat vár. nekem ilyen típusúra kell konvertálnom a számokat:
new_type = "int"+str(abs(mixer.get_init()[1]))

A konvertáláshoz pedig kétezik egy astype metódus:
tst_snd = tst_snd.astype(new_type)

Javítás

Még mélyebbre ásva magamat a dokumentációban megkáttam, hogy a tile függvény több, mint egy dimenziót is elfogad, tehát sokkal egyszerűbben így néz ki a kód:
tst_sound = np.tile(np.linspace(-1.,1.,fs/frequency), (chs, 1))

Ráadásul transzponálás helyett használhatom az axis argumentumát a repeat() függvénynek további egyszerűsítésként:
tst_second = np.repeat(tst_sound, frequency, 1)

Ezt midn össze tudjuk fűzni egy függvénnyé, ami a mintagyakoriságot és a csatornák számát közvetlenül a mixer modultót szerzi:
def create_snd(frequency=440., duration=.3):
    # mixer init props:
    fs = mixer.get_init()[0]
    new_type = "int"+str(abs(mixer.get_init()[1]))
    chs = mixer.get_init()[2]
    # one wave
    tst_sound = np.tile(np.linspace(-1.,1.,fs/frequency), (chs, 1))
    # make in one second
    tst_second = np.repeat(tst_sound, frequency*duration, 1)
    # convert it to a sound and return it
    return(snd.make_sound(tst_second.astype(new_type)))

Ehhez azonban már rendesen be kell tölteni a pygame modult (nem tudom miért, mondjuk ha elolvasnám a leírását valószínűleg kiderülne :D ). A teljes folyamat az alábbi kódban található, az eredeti példából vett szinusz-hullám-gyártó függvénnyel:

def create_sin(frequency=440., duration=.3, amplitude=100):
    # mixer init props
    fs = mixer.get_init()[0]
    chs = mixer.get_init()[2]
    snd_type = "int"+str(abs(mixer.get_init()[1]))
    # time
    t = np.arange(0, duration, 1./fs)
    freq = np.array((frequency, frequency))
    sig = np.sin(2 * np.pi * freq * t.reshape(-1, 1))*amplitude
    return(snd.make_sound(sig.astype(snd_type)))
 
# program for changing notes
def int_snd_test():
    # program for changing notes
    #
    pg.init()
    pg.display.set_mode([50,50])
    mixer.pre_init(frequency=44100)
    mixer.init(frequency=44100, channels=1)
    #
    freq=440.
    done = False
    print("press w to increase, s to decrease frequency")
    #
    while not done:
        e = pg.event.wait()
        print("Waiting for user event")
        # create sound and play it:
        create_sin(frequency=freq, duration=1, amplitude=250).play()
        print("sound began to play...")
        # check user's events:
        if e.type == pg.KEYDOWN:
            if e.key == pg.K_ESCAPE:
                done = True
                pg.display.quit()
                break
            elif e.key == pg.K_w:
                freq += 100
                print("frequency up; ", freq)
            elif e.key == pg.K_s:
                freq -= 100
                print("frequency down;", freq)

Hmmm... minden szép és jó, de bizonyos frekvenciák esetén változik a kiadott hang magassága. Van bárkinek bármi ötlete? Nem találtam ilyet a Stack Overflow-n.

Ráadásul beleraktam egy időtartam és egy amplitúdó argumentumot, mert az eredeti hang halk.

Hozzászólások