Digitális hangminták
Gaga
Digitális hangfájlok

Bevezetés

Egyszerű 10-15-soros C programokat írunk, melyek hangtechnikai effekteket valósítanak meg. Az effekteket csővezetékbe kapcsoljuk, mint a gitáros az effektpedálokat. A bináris fájlokon (és bináris standard input/outputon) kívül a hangfájlok rendszertechnikáját ismerjük meg.

Digitális hangminták

A hang a fület érő hangnyomás időbeli váltakozása: \(p(t)\), ahol \(t\) az idő, \(p\) pedig a hangnyomás.

Megjegyzés
A hangnyomás a légnyomás kisjelű ingadozása. A légnyomás statikus (állandó) értéke 101 kPa. Az ehhez hozzáadódó kis ingadozás a hangnyomás, melynek effektív értéke ritkán haladja meg az 1 Pa-t. A fülünkkel a kis ingadozást érzékeljük. A pillanatnyi hangnyomás lehet pozitív és negatív is.

A digitális hangminta a hangnyomás értékének rögzítése \(\Delta t\) időközönként egy tömbben \(p[k] = p(k\Delta t)\) . Ezt a digitális tárolási technikát Pulse Code Modulation (PCM) néven szoktuk emlegetni. Az alábbi ábra egy sztereó hangmintát ábrázol. A vízszintes tengely az idő, a függőleges a normalizált hangnyomás.

Hangjel Hangjel

Az egyes hangminta-adatokat különböző módon tárolhatjuk.

  • lebegőpontos tárolás esetén értéküket \(\pm 1\) között ábrázoljuk (normalizált hangnyomás).
  • egész típusú tárolás esetén a hangnyomást leképezzük az egész típus ábrázolási tartományára.

A mintavételezést sokszor inkább a \(\Delta t\) időlépés reciprokával, a mintavételi frekvenciával (sample rate) jellemezzük.

Megjegyzés
Az audió CD-k PCM rendszere pl. 16 bites signed int típussal tárol. Ekkor a hangminta adatait a \(-2^{15}\) és \(2^{15}-1\) közötti egész értékekkel reprezentáljuk, vagyis a legkisebb hangnyomásérték a -32768 és a legnagyobb a +32767. Az audió CD-k mintavételi frekvenciája 44100 Hz. Egy CD-ről leszedett 10 másodperces egycsatornás (monaurális, mono) hangminta egy 441 000 elemű tömbben tárolható.

Egy sztereó hangminta külön tartalmazza a bal és a jobb fület érő hangjeleket. Kezelhetjük akár 2D tömbként is, melynek egyik dimenziója 2 méretű, de tipikusan összefésülve (interlace), 1D tömbként ábrázoljuk. Sztereó minta esetén az összefésült tömb az alábbi formátumú:

+--------+---------+--------+---------+--------+---------+----
| 0. bal | 0. jobb | 1. bal | 1. jobb | 2. bal | 2. jobb | ...
+--------+---------+--------+---------+--------+---------+----

Hangfájlok

A hangfájlok digitális hangminták tárolására valóak. Rengeteg különböző hangfájl-formátum létezik.

  • A legegyszerűbb hangfájl a nyers (RAW) formátum, mely a PCM hangmintát (tömböt) mindenféle további információ nélkül tárolja. Alkalmazhatóságát korlátozza, hogy mivel nem tárolja a PCM adatfolyam formátumának leírását, a felhasználónak kell megjegyeznie, hogy melyik fájlban milyen formátum szerint, milyen mintavételi frekvenciával, hány csatorna hangmintáját tárolja (tipikusan beírja a fájlnévbe, pl. lady_gaga_telephone_s16le_44100_2ch.raw).
  • Az egyszerűbb WAV fájlok a PCM hangmintát tárolják, de azokat egy WAV header (fejléc) előzi meg, mely leírja a PCM ábrázolási formátumát (egész vagy lebegőpontos, hány biten tárol), a fájlban tárolt minta méretét, a csatornaszámot és mintavételi frekvenciát.
  • A tárolt adatmennyiség csökkentése érdekében sok formátum tömöríti a hangmintát. Hatékony veszteségmentes tömörített formátum pl. a FLAC.
  • A nagyarányú tömörítés érdekében sokszor veszteséges kódolókat használunk. A legelterjedtebb veszteséges tömörítési mód az MPEG-I/II Layer3 (MP3) formátum.

Egy jó hanglejátszó szoftver számos hangformátumot ismer. A szoftver feladata a különböző tömörített formátumok felismerése, kitömörítése (nyers PCM adatfolyammá alakítása), és az adatfolyam hangkártyára küldése:

Blokkdiagram

Az igazán jól használható hanglejátszó szoftver ezen kívül

  • ingyenes, nyílt forráskódú,
  • parancssorból futtatható,
  • nemcsak fájlból, hanem standard inputról érkező vagy internetről streamelt adatokat is lejátszik,
  • nemcsak hangkártyára, hanem fájlba, esetleg standard outputra is tudja küldeni a nyers PCM adatfolyamot.

Vannak ilyen programok. A két legismertebb és legszélesebb körben használt talán az mplayer és a vlc media player

A vlc media player

Az alábbiakban a vlc media player használatát ismertetjük. Minden itt leírt funkció megvalósítható az mplayerrel is.

Az alábbi paranccsal a vlc megnyílik, és lejátssza a guns.mp3 fájlt:

vlc guns.mp3

Ha azt akarjuk, hogy a lejátszás után a vlc automatikusan bezáruljon,

vlc guns.mp3 vlc://quit

Igazából teljesen fölösleges a lejátszáshoz megnyitni a player GUI-ját (graphical user interface):

cvlc guns.mp3 vlc://quit

Itt a cvlc a console vlc-re utal.

Megjegyzés
Windows alatt nagy valószínűséggel nem megy a cvlc. Helyette írhatjuk azt, hogy
vlc -I dummy
ahol a -I dummy a (korántsem) buta parancssoros interfészt jelenti.

Ha azt akarjuk, hogy a vlc a hang kimenetet ne a hangkártyára, hanem fájlba küldje, akkor fel kell paramétereznünk a stream outputot (sout)

cvlc guns.mp3 --sout "#transcode{acodec=fl32,channels=2,samplerate=44100}:std{access=file,mux=raw,dst=guns.pcm}"  vlc://quit

Itt a --sout kapcsolót követően egy sztringet adtunk meg, mely a transzkóder (mp3-ból nyers PCM) paramétereit adja meg:

  • acodec=fl32 nyers PCM mintákat tárolunk 32 bites floating point (fl32) adatként (float).
  • channels=2 sztereó
  • samplerate=44100 a mintavételi frekvencia
  • access=file a kimenet egy fájl lesz
  • mux=raw nem írunk semmiféle fejlécet, csak a nyers adatokat
  • dst=guns.pcm a kimenő (destination) fájl neve

Ekkor létrejött a guns.pcm, melyben a guns.mp3 kitömörített PCM adatfolyama van a megadott paraméterekkel kódolva.

Már csak egyetlen dolog van hátra. Ha a vlc-vel nyers PCM adatot akarunk lejátszani, akkor tudatnunk kell vele annak formátumát:

cvlc guns.pcm --demux=rawaud --rawaud-fourcc=fl32 --rawaud-channels=2 --rawaud-samplerate=44100 vlc://quit

Itt a fourcc (four character code) paraméter adja meg a mintaadatok formátumát, a többi paraméter magáért beszél.

Összefoglalva képesek vagyunk

  • egy tetszőleges audió fájlban tárolt zenét nyers PCM adatfolyamként fájlba küldeni
  • nyers PCM fájlt lejátszani

Kihasználva továbbá, hogy a VLC a kitölörített PCM-et nemcsak fájlba, hanem a standard outputra is tudja küldeni, továbbá a VLC nemcsak fájlból, hanem a standard inputról is képes olvasni, az alábbi blokkdiagram megvalósítására is képesek vagyunk:

Blokkdiagram

Itt két VLC példányt indítunk el párhuzamosan, melyek egyszerre futnak. Az első kitömöríti a hangfájlt, és a nyers PCM-et kiküldi a standard outputra. A második a nyers PCM-et standard inputján kapja meg, és lejátssza.

Ehhez a megfelelő VLC-hívás az alábbi:

cvlc guns.mp3 --sout "#transcode{acodec=fl32,channels=2,samplerate=44100}:std{access=file,mux=raw,dst=-}" | cvlc - --demux=rawaud --rawaud-channels=2 --rawaud-fourcc=fl32 --rawaud-samplerate=44100

A két programot a pipe operátorral kapcsoltuk össze (|). Az első parancsban a dst=- felelős a fájl helyett standard otputra küldésért, a másodikban a bemenő fájl helyén a - paraméter utal a standard inputról való lejátszásra.

Effektprogramok

Ezek után már csak a két nyers PCM fájl közé kell C programokat (effekteket) írnunk. A kibővített blokkdiagramunk tehát

Blokkdiagram

Sztereó drót

Egy megfelelő (és első körben semmit nem csináló) effektprogram a sztereó drót, mely a wire.c fájlban van megvalósítva. Ez a program kettesével olvas hangmintákat a bemenetről, amíg csak jönnek, és kiírja őket a kimenetre. Ahhoz, hogy jól lefusson, fontos, hogy a standard streameket (stdin és stdout) binárisra állítsuk. A standard inputról és outputról a 7. előadáson azt tanultuk, hogy "szöveges fájlok", melyek automatikusan megnyílnak a program indulásakor. A binary_streams függvény újranyitja őket bináris módban, mert a PCM adatok nem szövegesen, hanem float értékek formájában érkeznek.

Erősítő

Nagyon egyszerű az erősítő effekt, melyet az amplify.c valósít meg. Ez a sztereó drót minimális módosításával megvalósítható. A bejövő hangmintákat egy konstanssal szorozzuk, mely a program parancssori paramétereként adható meg. Ezek értelmében a program lehetséges használata

amplify 10.0 < input.pcm > output.pcm

Torzító

A legegyszerűbb torzító úgy működik, hogy a bemenő \(x\) jelet felerősítjük \(d\)-szeresre (ez a drive), majd \(\pm 1\) szinten levágjuk (limiter). A distort.c fájlban megtalálható torzító algoritmusa az erősítő minmális továbbfejlesztésével kódolható. Hogy ne legyen túl hangos a kimenet, a torzító mögé kacsolhatunk egy erősítőt, mellyel csillapítunk az alábbi módon:

distort 20 < input.pcm | amplify .1 > output.pcm

Az effekt nyers bemenete: queen. az effekt kimenete: queen_dist

Delay (zengető)

A delay.c fájlban megvalósított zengető egyszerű szabályos visszaverődésekből álló végtelen visszhangot valósít meg. Megvalósítása végtelenített (cirkuláris) tárolóval történik. A cirkuláris tároló egyszerű tömb, melyet 0-tól N-1-ig indexelünk, de az N-1. elem után ismét a nulladikat írjuk felül. A tároló kezdetben üres, mi pedig feltöltjük a beérkező mintaelemekkel. Mikor már létező elemét írnánk felül, annak értékét megszorozzuk az \(\alpha < 1\) csillapítással (ez a visszhang csillapítása), majd hozzáadjuk az éppen beérkező mintaelemet. A visszhang késleltetését a tároló hossza határozza meg.

Az aktuális kimenet az éppen módosított tárolóelem lesz.

A cirkuláris puffer sztereó minta esetén egy 2D tömb, ahol az N érték természetesen konstans.

sample_t circ_buffer[N][2] = {0};

A feldolgozási fázis pedig az alábbi módon algoritmizálható (a k számláló értéke kezdetben 0):

while (fread(samples, sizeof(sample_t), 2, stdin) == 2) { /* bemenet beolvasása */
int c;
for (c = 0; c < 2; ++c) {
circ_buffer[k][c] *= alpha; /* múlt csillapítása */
circ_buffer[k][c] += samples[c]; /* jelen hozzáadása */
}
fwrite(circ_buffer[k], sizeof(sample_t), 2, stdout); /* kimenet kiadása */
if (++k == N) /* cirkuláris léptetés */
k = 0;
}

Az effekt alkalmazása:

delay .2 .3 < input.pcm > output.pcm

ahol az első paraméter a 0.2 másodperces visszahngkésleltetésre, a második pedig a 0.3 értékű csillapításra utal.

A nyers hangbemenete: living. Az effektezett kimenete: living_delay

Vibrátó

A vibrátó a hangmagasság periodikus, gyors (pár hertzes) változtatását jelenti. A vibrátó algoritmusának megértéséhez először egy egyszerű késleltetőt valósítsunk meg:

A késleltető egy tömb, melyet cirkulárisan feltöltünk a beérkező elemekkel. A cirkuláris feltöltés ugyanúgy történik, mint az előző példában, csak itt a korábbi elemeket egyszerűen felülírjuk. Ezáltal a tároló mindig az N megelőző mintaelemet tartalmazza. A késleletetés megvalósításához az aktuális kimenetet a tárolóból a beírás ütemével szinkronban, de lemaradva olvassuk ki:

int write = 0, read = N/2;
while (fread(circ_buffer[write], sizeof(sample_t), 2, stdin) == 2) {
fwrite(circ_buffer[read], sizeof(sample_t), 2, stdout);
if (++write == N) /* írófej cirkulárisan lép */
write = 0;
if (++read == N) /* olvasófej cirkulárisan lép */
read = 0;
}

A fenti példában az algoritmus N/2 elemnyi késleltetéssel dolgozik.

A vibrátó azt jelenti, hogy az olvasófej pozícióját periodikusan elmozdítjuk. Az alábbi példában az olvasófej kitérése

\( d \cdot \sin(2\pi f_{vib} k \Delta t)\)

vagyis maximum \( d \) mintaelemnyivel tér ki a szinkron pozícióhoz képest, és ezt \(f_{vib}\) frekvencián teszi.

long long int k = 0; /* időszámláló */
int write = 0, read = N/2; /* író és olvasópozíciók */
while (fread(circ_buffer[write], sizeof(sample_t), 2, stdin) == 2) {
int r = read + sin(2.0*M_PI*f_vib/SAMPLE_RATE*k) * depth; /* kitérített olvasó */
if (r < 0)
r += N;
fwrite(circ_buffer[r % N], sizeof(sample_t), 2, stdout); /* olvasás */
if (++write == N)
write = 0;
if (++read == N)
read = 0;
k++;
}

A nyers hangbemenet: billie. Az effektezett kimenet: billie_vib

A kórus (chorus)

A kórus effekt legegyszerűbb verziója egy vibrátóval megeffektezett hangjel, melyhez hozzákeverjük (hozzáadjuk) az eredeti nyers hangot is. A vibrátó ismeretében igen könnyen leprogramozható, érdekes hangzása van:

A nyers hangbemenet: guns. Az effektezett kimenet: guns_chorus

A programok fordítása és használata

Az összes program innen tölthető le. Egyszerűen fordulnak. VS alatt

cl amplify.c
cl distort.c
cl delay.c
cl vibrato.c
cl chorus.c

gcc-vel

gcc amplify.c -o amplify
gcc distort.c -o distort
gcc delay.c -o delay
gcc vibrato.c -o vibrato -lm
gcc chorus.c -o chorus -lm

ahol a -lm a math.h beillesztése miatt kell.

sample_t
float sample_t
type of a single sample
Definition: amplify.c:11
N
@ N
Definition: chorus.c:17
SAMPLE_RATE
@ SAMPLE_RATE
Definition: chorus.c:16