Helitöötlus ja -süntees
Peatükk annab ülevaate heli olemusest ning vahenditest, millega saab Pythonis heliga ümber käia. Kõigepealt tutvustatakse seda, kuidas heli matemaatilisel kujul väljendada ja kuidas seda digitaalsel kujul arvutis salvestatakse. Seejärel näidatakse, kuidas Pythoni teekide abil on võimalik heli sünteesida, esitada, lindistada, töödelda ja salvestada.
Ettevalmistus
Õpikust peab olema läbitud järjendite peatükk. Soovitatav on tutvuda objektorienteeritud programmeerimise mõistetega, aga suur osa materjalist peaks olema ilma selletagi mõistetav. Helilainete kuvamiseks kasutame teeki matplotlib
. Heli esitamiseks peab paigaldama teegi simpleaudio
, reaalajas lindistamiseks ja esitamiseks teegi sounddevice
ning töötlemiseks teegi pydub
. Kõik saab näiteks käsurealt paigaldada ühe käsuga pip install matplotlib sounddevice simpleaudio pydub
. Vajadusel loe läbi õpikus moodulite paigaldamise juhised.
Helilained ja -sagedused
Heli all mõistetakse seda, kui õhus edasikanduvad võnked jõuavad kõrva. Helilained on mudeldatavad siinusfunktsiooni abil, sest see kirjeldab üsna hästi millegi võnkumist. Laine amplituud A määrab sealjuures heli valjuse ning sagedus f helikõrguse. Kuna just võnkumise protsess tekitab helikogemuse, siis vaatame ühe ajahetke asemel ajavahemikku. Selle jaoks on t valemis ajavektor, mille pikkus määrab ka heli kestvuse.
{$ y = A \cdot \sin(2 \pi f \cdot t) $}
Kujutada saame seda Pythonis mooduli matplotlib
abil. Kui teha joonis täppidega, siis on paremini näha, et tegemist on siiski digitaalse signaali, mitte pideva analoogsignaaliga. Samas eristub siiski laine kuju.
from math import * import matplotlib.pyplot as plt A = 1.0 f = 432 samplerate = 44100 t = [i/samplerate for i in range(samplerate)] # ajatelg y = [A*sin(2*pi*f * punkt) for punkt in t] # siinusfunktsiooni telg plt.plot(t[:700], y[:700], ".") # Teeb täppidega teljestiku plt.show()
Katseta erinevate A, f ja t väärtustega ning uuri, kuidas saadud graafik muutub.
Helifailid ja parameetrid
Analooghelisignaali digitaliseerimine on sisuliselt pideva helisignaali salvestamine diskreetsete väärtuste jadana. Diskreetimissagedus (ingl sample rate) määrab väärtuste või mõõtmiste (öeldakse ka diskreetide või sämplite) arvu, mis salvestatakse ühe sekundi jooksul. Vahel (eriti videote puhul) kasutatakse ka mõistet kaadrisagedus (ingl frame rate). Mida rohkem väärtusi ajaühikus salvestada, seda täpsem on heli digitaalne esitus. Standardiks loetakse 44100 väärtust sekundis ehk 44100Hz (44.1kHz).
Salvestatud heli täpsust määrab ka bitisügavus (ingl bit depth) ehk mitut bitti kasutatakse ühe mõõtmise kohta info salvestamiseks. Vahel kasutatakse ka mõistet sämpli laius (ingl sample width), mida tavaliselt väljendatakse baitides (1 bait = 8 bitti).
Helikanalite arv (ingl channels) näitab helifailide puhul, mitme kanali kaudu heli edastada: 1 kanal = monoheli, 2 kanalit = stereo, 3+ kanalit = ruumiline heli.
Failivormingud
Heli loomise või töötlemise ajal soovitatakse kasutada vähemalt 24-bitise bitisügavuse ja 48kHz diskreetimissagedusega pakkimata helifaile, et säiliks võimalikult palju infot. Selleks otstarbeks kasutatakse tihti WAV-faile - neid on ka lihtne lugeda ja töödelda, sest andmeid hoitakse failis võrdse pikkusega lõikudena. Apple'i arvutitel kasutatakse ka AIFF-vormingut.
Heli lõplikuks salvestamiseks sobib vorming, mis kasutab kadudega pakkimist, sest see võtab vähem ruumi. Üldjuhul eemaldab kadudega vorming need helid, mida keskmine inimene niikuinii ei eristaks. Sellised vormingud on näiteks OGG
, MP3
ja selle edasiarendus AAC
.
Kadudeta pakkimist kasutavad vormingud FLAC
ja ALAC
(Apple Lossless).
MIDI-vorming
MIDI (Musical Instrument Digital Interface) on protokoll, mis kirjeldab, kuidas arvutid, süntesaatorid jm digitaalsed muusikariistad saavad omavahel suhelda. Kui eelnevalt mainitud vormingud salvestasid heli üht tüüpi väärtuste jadana ja kindla diskreetimissagedusega, siis MIDI-vormingus failid salvestavad instruktsioone. Need ütlevad, millist nooti, millise tugevusega, kui kaua ning läbi millise kanali mängida. Igale kanalile vastab üks virtuaalne instrument, nii et korraga saab mängida näiteks klaverile ja kitarrile iseloomulikke helisid.
Failide avamine ja salvestamine
Siinsetes näidetes kasutame WAV-faile, aga tasub teada, kuidas Pythoniga muudes vormingutes faile avada. Pydub
suudab avada erinevaid vorminguid (v.a. MIDI), aga selle jaoks tuleb arvutisse paigaldada ka FFmpeg raamistik: FFmpegi kodulehelt allalaetud ZIP-fail tuleb lahti pakkida ning seejärel tõsta kaustast bin
kõik failid oma programmi kausta. Teine võimalus on määrata bin
kaust oma süsteemi keskkonnamuutujate hulka.
Kui kõik vajalik on olemas, saab teegiga pydub
faile näiteks ühest vormingust teise ümber salvestada:
from pydub import AudioSegment sound = AudioSegment.from_file('fail.mp3', format='mp3') sound.export('fail.wav', format='wav')
Kui on vaja ainult heli taasesitada, siis piisab teegist simpleaudio
või pygame
. Viimane suudab muuhulgas avada ja esitada ka MIDI-faile:
import pygame pygame.init() pygame.mixer.music.load("fail.mid") pygame.mixer.music.play()
MIDI-failide töötlemiseks on muud teegid, näiteks mido
.
Lainevormid
Lisaks sagedusele ja amplituudile mõjutab heli kõla ka selle lainevorm. Peale siinuslaine eristatakse ka teisi lainevorme, kuigi tegelikult on needki väljendatavad erinevate siinuslainete summana. Teisisõnu, kõiki lainevorme peale siinuslaine võib põhimõtteliselt nimetada akordideks. Tabelis on toodud tuntuimad lainevormid:
Lainevorm | Valem | Kuju | Kõla (220Hz) Allikas: Wikimedia Commons |
---|---|---|---|
Siinuslaine | {$ f(x) = \sin(2 \pi*x) $} | ∿ | |
Kandiline ehk impulsslaine | {$ f(x) = 2*(2*\lfloor x \rfloor - \lfloor{2*x}\rfloor) + 1 $} | ⎍⎍⎍ | |
Kolmnurklaine | {$ f(x) = 2*|(2x - 0.5) \bmod 2 - 1| - 1 $} | /\/\/ | |
Saehammaslaine | {$ f(x) = -(2x \bmod 2) + 1 $} | või |
Näiteks varajaste arvutimängude helides kasutati palju kandilisi laineid. Nimelt kettaruumi oli vähe, aga sellise laine ühe punkti salvestamiseks piisab ainult ühest bitist, mis ütleb, kummas amplituudväärtuses laine parajasti on.
Tasub mainida, et väga kõrgetel sagedustel on kõik lainevormid sarnase kõla ja välimusega, sest üleminekud võngetes on iga vormi puhul väga järsud. Samas võib kuulates märgata, et näiteks impulsslained on palju valjemad kui sama amplituudiga siinuslained. Valjus oleneb sellest, kui kaua laine igal sammul oma amplituudväärtuste läheduses viibib. Siinuslained on kõige vähem aega amplituudväärtuse lähedal, aga impulsslained on pidevalt amplituudväärtuses.
Lainevormidega heli sünteesimine
Eelneva põhjal on üsna lihtne heli digitaalselt luua. Tuleb teha järjend, kus iga element on ühele ajahetkele vastav lainefunktsiooni väärtus. Esimeses koodinäites uurisime helilainet sagedusega 432Hz. Paneme nüüd näiteks kokku ühe oktavi pikkuse heliredeli.
Selleks peame teadma iga noodi võnkesagedust. Vanasti polnud ühist instrumentide häälestamise standardit ja iga helilooja otsustas ise, millisele võnkesagedusele ta oma pillid häälestas. Aja jooksul ja piirkonniti on muutunud ka noodiskaalad. Tänapäeval on läänemaailmas standardiks 12 tooniga oktavite süsteem. Põhitooni ehk viienda oktavi noodi A võnkesageduseks on valitud 440Hz. Teades põhitooni sagedust on näiteks klaveril võimalik järgmise valemiga leida ülejäänud klahvide sagedused (põhitoon on tavaliselt 49. klahv):
{$ \Large f(klahv) = 440 * 2 ^{klahv-49\over{12}} $}
Järgnev kood mängib helilõigu, kus iga klahv (49-60) kõlab pool sekundit. Klaveri klahvide tekitatud lainevormid on küll keerulisemad kui puhas siinustoon, aga mängime siin lihtsamalt:
from math import * import struct from pydub import AudioSegment from pydub.playback import play def listToBytes(wave): # Teeb helilaine baidijadaks waveMax = max(1, max(wave), abs(min(wave))) wave = [int((i/waveMax) * (2**15 - 1)) for i in wave] return struct.pack(f'<{len(wave)}h', *wave) seconds = 0.5 samplerate = 44100 A = 1.0 # Heliamplituud (0.0 - 1.0) t = [i/samplerate for i in range(int(samplerate*seconds))] notes = range(49, 49+12) notefrequencies = [440 * 2 ** ((note-49)/12) for note in notes] sound = AudioSegment.empty() for freq in notefrequencies: # Teeme iga noodi kohta 0.5 sek pikkuse helisignaali wave = [A*sin(2*pi*freq * x) for x in t] waveBytes = listToBytes(wave) notesound = AudioSegment( waveBytes, frame_rate=samplerate, sample_width=2, channels=1 ) fade = min(len(sound), 5) sound = sound.append(notesound, crossfade=fade) play(sound)
Selgitav video:
Peatüki esimese osa lõpuks toome paar näidet sellest, kuidas lainevorme on võimalik keerulisemaks muuta (kasutatud väärtused: A=1.0, f=444, seconds=5):
wave = [A*sin(2*pi*f * 2*sqrt(x)) for x in t]
wave = [A*sin(2*pi*f * (sqrt(x)*(x-seconds) if x<2 else sqrt(2*x))) for x in t]
wave = [(5**(x+1.5)) % 2 for x in t]
Enesekontrolliküsimused
Heli lindistamine
Lindistamiseks tuleb kasutada teeki sounddevice
. Võimalik on seda teha kahel viisil:
import sounddevice as sd samplerate = 44100 seconds = 2.5 ### Lihtsam variant (sd.rec tagastab NumPy massiivi): data = sd.rec(int(seconds * samplerate), samplerate=samplerate, channels=1, dtype="int16") print("Lindistamine...") sd.wait() print("Valmis.") sd.play(data, samplerate) data = data.tobytes() ### Keerukam variant (toimib paralleelselt ülejäänud programmi tööga, kasutab baidimassiivi): data = bytearray() def callback(indata, outdata, frames, time, status): if status: # Kui midagi on valesti print(status) data.extend(indata) print("Lindistamine...") with sd.RawStream(channels=1, samplerate=samplerate, callback=callback, dtype="int16"): # Lindistatakse kuni programm viibib siin plokkis sd.sleep(int(seconds * 1000)) print("Valmis.")
Sounddevice
salvestab heli "toorete" baitidena või numpy
massiivina. Et seda mugavalt esitada või faili salvestada, teisendame selle pydub
jaoks sobivasse vormingusse:
from pydub import AudioSegment from pydub.playback import play # Teisendus: sound = AudioSegment( data, # data peab olema baidijada, NumPy massiivi puhul kasuta data.tobytes() frame_rate=samplerate, sample_width=2, channels=1 ) # Esitamine play(sound) # Faili salvestamine sound.export('fail.wav', format='wav')
Heli töötlemine
Baidijadasid või massiive saab ise "käsitsi" töödelda. See on tegelikult päris mõistlik, kui olemasolevad teegid kõiki mugavusi ei paku. Eriti hea on massiive töödelda teegi numpy
abil ning kasutada vajaduse korral näiteks heliefektide teeke, mis numpy
massiive, järjendeid või baidijadasid sisendi ja väljundina kasutavad. Ühe pikema nimekirja Pythoni audioteekidest leiab siit lehelt: https://wiki.python.org/moin/PythonInMusic.
Siin peatükis kasutame üht üldisemat teeki nimega pydub
, mille dokumentatsiooni leiab siit: https://github.com/jiaaro/pydub/blob/master/API.markdown. Allpool toome välja mõned selle võimalused.
AudioSegment
Teegi pydub
keskmes on klass AudioSegment
, mis väljendab mingit helilõiku. Üht konkreetset helilõiku kirjeldavat isendit saab luua erinevate funktsioonide abil:
AudioSegment.from_file
- Loob helifailist vastava helilõigu objekti.AudioSegment.silent
- Loob etteantud pikkuse ja diskreetimissagedusega vaikusega täidetud helilõigu.AudioSegment.from_mono_audiosegments
- Loob mitmest monoheliga helilõigust ühe mitme helikanaliga helilõigu.AudioSegment
(Klassi vaikekonstruktor) - Loob baidijadast vastava helilõigu.
Ühel helilõigul on järgnevad isendimuutujad:dBFS
- Helilõigu keskmine helitugevus detsibellides võrreldes maksimaalse helitugevusega (dBFS ehk decibels relative to full scale).channels
- Helilõigu helikanalite arv.sample_width
- Baitide arv, mis on ühes kanalis ühe ühiku heli salvestamiseks kasutatud. Bitisügavus = 8*sample_width.frame_rate
- Diskreetimissagedus hertsides.max
- Kõrgeim heliamplituud helilõigus.max_dBFS
- Kõrgeim heliamplituud helilõigus detsibellides võrreldes maksimaalse helitugevusega.duration_seconds
- Helilõigu pikkus sekundites. len(helilõik)
annab pikkuse millisekundites.raw_data
- Helilõiku kirjeldav toorete baitide jada.
Helilõikudega saab teha järgmisi tehteid:helilõik + arv
- Annab helilõigu, mille helitugevus on mingi arvu detsibellide võrra suurem.helilõik - arv
- Annab helilõigu, mille helitugevus on mingi arvu detsibellide võrra väiksem.helilõik * arv
- Toimib nagu sõnede korrutamine, annab mingi arv kordi dubleeritud helilõigu.helilõik + helilõik
- Toimib nagu sõnede liitmine, annab ühendatud helilõikudest koosneva pikema helilõigu.helilõik * helilõik
- Helilõigud liidetakse nii, et nad mängivad samaaegselt. Teine helilõik "laotatakse" esimese peale. Kui teine on lühem, siis teda korratakse esimese helilõigu lõpuni. Kui teine on pikem, siis pikem osa lõigatakse ära.
Ühel AudioSegment
isendil (ehk siis ühel konkreetsel helilõigul) on järgmised meetodid. NB! Kuna AudioSegment
isendid pole muudetavad, siis kõik helilõiku muutvad meetodid tagastavad uue isendi ega muuda algset helilõiku:
Meetod | Kirjeldus |
---|---|
append | Pikendab üht helilõiku teisega. Parameetriga crossfade saab juhtida ülemineku sujuvust ühest lõigust teise. |
overlay | Laotab helilõigu peale mingi teise helilõigu, nii et nad mängivad samaaegselt. Parameetritega saab määrata, mis hetkest peaks pealelaotatav lõik mängima ja mitu korda korduma (kui ta on lühem). |
fade | Muudab sujuvalt helilõigu valjust mingis ajavahemikus. |
fade_in | Alustab helilõiku seda sujuvalt valjemaks muutes. |
fade_out | Muudab helilõiku lõpus sujuvalt vaiksemaks. |
pan | Suurendab helitugevust ühes helikanalis ja vähendab samavõrra tugevust teises kanalis. |
invert_phase | Tagastab helilõigu, mille lained on esimesega vastandfaasis. Saadud helilõiku saab kasutada nt. algse heli nullimiseks. |
Vaikuse eemaldamine
Silence
on teegi pydub
moodul, kust leiab mõned vahendid helilõigust vaikuse tuvastamiseks:
Funktsioon | Kirjeldus |
---|---|
detect_silence | Tagastab järjendi kõikide vahemikega, kus helilõigus tuvastati vaikus. |
detect_nonsilent | Tagastab järjendi kõikide vahemikega, kus helilõigus ei tuvastatud vaikust. |
split_on_silence | Tagastab helilõikude järjendi, mis koosneb ainult nendest algse heli osadest, kus vaikust ei tuvastatud. |
Näide
Allolev video selgitab, kuidas lindistada juttu ja seejärel sellele taustaheli lisada või vaikust eemaldada:
Kasutatud taustaheli: Attach:äikesevihm.wav
from pydub import AudioSegment, silence from pydub.playback import play import sounddevice as sd # Jutu lindistamine samplerate = 16000 seconds = 7 print("Lindistamine...") data = sd.rec(int(seconds * samplerate), samplerate=samplerate, channels=1, dtype='int16') sd.wait() print("Valmis.") data = data.tobytes() # Teeb baitidest helilõigu ja muudab 30db võrra valjemaks jutt = AudioSegment( data, frame_rate=samplerate, sample_width=2, channels=1 ) + 30 play(jutt) # Taustaheliga jutt # allikas: https://soundbible.com/1907-Thunder.html taust = AudioSegment.from_file("äikesevihm.wav") jutu_pikkus = len(jutt) sujuv_lõpp = taust[jutu_pikkus : jutu_pikkus+1000].fade_out(duration=1000) taustaga_jutt = jutt.overlay(taust) + sujuv_lõpp play(taustaga_jutt) # Ilma pausideta jutt jutulõigud = silence.split_on_silence(jutt, min_silence_len=100, silence_thresh=-18, keep_silence=75) pausideta_jutt = AudioSegment.empty() for lõik in jutulõigud: pausideta_jutt = pausideta_jutt + lõik play(pausideta_jutt) print("Pausideta jutt tuli", jutu_pikkus-len(pausideta_jutt), "ms lühem!")
Fourier' teisendus
Kui on soovi helisignaalist müra või teatud signaale eemaldada, siis on võimalik uurida levinud tehnikat, mida nimetatakse Fourier' teisenduseks (ingl Fourier transform). Meetod eraldab helisignaalist kõik seal esinevad erineva sagedusega signaalid. Müra (sisuliselt juhuväärtuste jada) koosneb tavaliselt nõrgematest signaalidest, mida on siis võimalik välja filtreerida.
Reaalajas heli esitamine ja peatamine
Mõnikord on vaja mängida ainult osa mingist helist või mängida heli muu programmitöö ajal. Siis peaks funktsiooni play
asemel kasutama _play_with_simpleaudio
:
from pydub.playback import _play_with_simpleaudio player = _play_with_simpleaudio(helilõik) ... if player.is_playing(): if oota_lõpuni == True: player.wait_done() else: player.stop()
Vahel on aga vaja toota heli hoopis reaalajas sissetuleva sisendi põhjal ning siis tuleks kasutada veidi keerulisemat lahendust. Teegiga sounddevice
saab reaalajas heli lindistada või esitada. Esitamine kestab kuni programm viibib allolevas näites toodud with
-plokis. Funktsioon RawOutputStream
loob väljundvoo, mis püsib avatuna ploki lõpuni (nagu tekstifail!). Sellele tuleb anda parameetrid umbes samamoodi nagu tegime ühe helilõigu loomiseks (dtype="int16"
on sisuliselt sama, mis sample_width=2
). Parameeter callback
peab olema nelja parameetriga funktsioon, mida hakatakse ploki vältel mitu korda sekundis välja kutsuma.
Funktsiooni callback
neljast parameetrist on praegu olulised kaks esimest:
- Väljundandmete puhver, mis söödetakse kõlarile.
- Sämplite arv puhvris
Kui ehitame funktsiooni callback
järgneval viisil, saame nt. tuttaval viisil mingi helisignaali konstrueerida ja selle puhvrisse kirjutada. Baitideks teisendamiseks võiks taas kasutada abifunktsiooni listToBytes
:
def callback(outdata, frames, time, status): t = [i/frames for i in range(frames)] wave = [... for x in t] outdata[:] = listToBytes(wave) with sd.RawOutputStream(channels=1, samplerate=44100, callback=callback, dtype="int16"): sd.sleep(1000*seconds) ... # Muud tegevused
Enesekontrolliküsimused
Ülesanded
1. Kirjuta programm, mis paneb mõningatest fikseeritud helimustritest või juhuslikest noodikombinatsioonidest iga kord kokku pikema unikaalse meloodia ja mängib seda. Kasuta erinevaid lainevorme!
2. Koosta programm, mis võtab sisendiks mõne inimkõnet sisaldava helifaili (nt. salvestatud loengu) ja teeb selle lühemaks. Selleks võib programm heli kiirendada või sellest tühje lõike välja lõigata.
Vihje: Et heli kaks korda kiiremaks teha, lõika välja iga teine diskreet või kasuta funktsiooni pydub.effects.speedup
.
3. Tee graafilise või käsureapõhise kasutajaliidesega programm, kus saab ekraanil või klaviatuuril klahvidele vajutades mingeid noote mängida.
4. Konstrueeri virtuaalne teremin, mida saab mängida hiirt ekraanil ringi liigutades. Peamine tingimus on, et mängitud sagedus peaks sõltuma hiire koordinaatidest reaalajas. Siit võid saada inspiratsiooni: https://theremin.app/.