Hangkártyák programozása retro 23.

9. Mesterséges hangzás kialakítása a gyakorlatban

  1. A kialakítandó hangzásnak megfelelő terem, illetve hangtér kiválasztása. Ilyenkor ügyelni kell a hangforrás (egy vagy több hangszóró) és a mikrofon, illetve mikrofonok, vagy műfej elhelyezésére. Ugyanis a kialakítandó hangzás attól is függ nagymértékben, hogy hova képzeljük a hallgató személyt, illetve pl. a zenekart.
  2. A programok és hangminták telepítése a lemezmellékletről történik, alapesetben a Program Files\Spiller\Diplomamunka könyvtárba. Ezen belül az ASF könyvtárban található maga a mesterséges hangzást kialakító program, melynek az Artificial Sound Field (Mesterséges hangtér) fantázianevet adtam. A Program Files\Spiller\Diplomamunka\CppDLL alkönyvtárban az ASF program által használt Fourier transzformációs és konvolúciós függvényeket tartalmazó DLL C++ kódja található. A Program Files\Spiller\Diplomamunka\Test könyvtár tartalmazza mindazon programokat, amelyekkel a hangkártya programozása során foglalkoztunk, illetve itt található a legfontosabb program: Rec.exe, amely az impulzusválasz tesztet végrehajtja. A Program Files\Spiller\Diplomamunka\Samples könyvtárba történik a hangminták telepítése. Ezen belül négy alkönyvtár található: Others: egyéb vizsgálódásra alkalmas hangminták, Responses: különböző hangterek impulzusválaszai, Studio: stúdió minőségű, vagy rendkívül kicsi „teremhangot” tartalmazó felvételek, Results: a stúdiófelvételek és az impulzusválaszok konvolúciójaként létrehozott, az adott hangtérnek megfelelő hangzással rendelkező hangminta eredmények.
  3. Ha mindent megfelelően elrendeztünk, jöhet a teszt. Miután megtettük a programbeli szükséges beállításokat is, a dolgunk csupán annyi, hogy a DOS parancssorába begépeljük a felvevő program nevét és paraméterét, amely nem lesz más, mint a felvétel eredményének mentési útvonala. Például Rec C:\dokumnetumok\test.sam.
  4. Ellenőrzés képen vissza is játszhatjuk a felvétel eredményét, hasonló módon, mint ahogy a felvétel történt: Play C:\dokumnetumok\test.sam. A Scroll.pas nevű Pascal programmal meg is tekinthetjük a felvétel időfüggvényét. Ez azonban nem paraméteresen működik, így futtatásához szükségünk van valamilyen keretrendszerre.
  5. Ha minden rendben, akkor elindíthatjuk az ASF programot, s nekiláthatunk függvényeink elemzésének, illetve a mesterséges hangzás létrehozásának.
  6. Miután elmentettük a konvolúció eredményét, azt visszahallgathatjuk a 3. pontban bemutatott módon.

Az ASF program használatának ismertetése:

  1. A File menüpont Open Impulse Response parancsával nyithatjuk meg az impulzusválasz teszt eredmény-függvényét. A megjelenő párbeszédablakból kiválaszthatjuk a keresett hangmintát. Természetesen akármilyen hangmintát kiválaszthatunk vizsgálat céljából, de ha értelmes eredményt kívánunk elérni a konvolúcióval, célszerű itt az impulzusválaszt megnyitni. Megnyitást követően a függvény grafikusan is megjelenik az ablakban, amelynek időfüggvénye a gördítősáv segítségével vizsgálható.
  2. A függvényen önmagában is végezhetünk transzformációkat, de ügyeljünk arra, hogy a konvolúció előtt mindig eredeti állapotba állítsuk vissza. A Tools menüpont Fast Fourier Transform almenüjéből két pont választható: Normal és Inverse. Közvetlenül a hangminta megnyitását követően az Inverse lesz kipipálva, ami azt jelzi, nincs transzformált állapotban a függvényünk. Ha rákattintunk a Normal-ra, megkapjuk a függvény gyors Fourier transzformáltját, amely grafikusan is látható lesz, s amely az időfüggvényhez hasonlatosan görgetéssel vizsgálható. Természetesen kattintáskor a Normal kipipált állapotba kerül, jelezve, hogy a függvényünk transzformált állapotban van. Ha az Inverse pontra kattintunk, megtörténik a visszatranszformálás.
  3. A második függvényünk esetében is ugyanúgy kell eljárni, mint az első esetében, csupán annyi a különbség, hogy azt a File Open Sample menüpontjával nyithatjuk meg. A két beolvasott függvény, illetve a majdani eredmény között az ablak alsó részén elhelyezkedő gombok segítségével lehet váltani.
  4. Az Options Log of NFFT paranccsal adhatjuk meg, hogy hány alappontra kívánjuk elvégezni a transzformációt. Ha például 10-et választunk, akkor az alappontok száma: 1024, mivel a listából az alpontok számának kettesalapú logaritmusát kell kiválasztani.
  5. A legfontosabb parancs a Tools menü Convolution with the help of FFT pontja, amely magát a konvolúciót végzi. Elvégzése után három függvényt vizsgálhatunk. Természetesen az eredmény spektrumát is megnézhetjük, majd az eredeti állapotába visszaállítva elmenthetjük a File\Save Result As menüponttal.

Érdemes néhány vizsgálatot is elvégezni a telepített hangminták segítségével. A telepítést követően a Program Files\Spiller\Diplomamunka\Samples\Studio könyvtár hangmintái között található egy stúdió felvétel 2cd.sam néven, amely egy részlet a The Music of Ireland című CD válogatásról. Ezt a hangmintát fogjuk szoftveresen más és más hangzással ellátni.

Első lépésben nyissuk meg az impulzusválaszok közül a dist68.sam nevű hangmintát. Ez egy viszonylag kicsi, 2,2m x 3m x 2,2m-es, néhány bútordarabbal berendezett szobában készült. A hangforrás és a mikrofon egymással szemben helyezkedett el, 68 cm távolságra. Ha ezt konvolúciós viszonyba hozzuk a stúdió felvételünkkel és elmentjük annak eredményét, akkor hangmintát kapunk, amely úgy szól, mintha az eredeti felvételt a szobában a mikrofontól 68 cm távolságban vettük volna fel. Az eredmény függvény egyébként megtalálható a Program Files\Spiller\Diplomamunka\Samples\Results könyvtárban 2cd68.sam néven, amelyet összehasonlítva az eredetivel lényeges különbséget fogunk tapasztalni. További vizsgálatokat érdemes elvégezni a dist102.sam, dist136.sam és a dist200.sam impulzusválaszokkal, amelyek az említett szobában készültek különböző távolságokban.

Eddig csak olyan eseteket vizsgáltunk, amikor az első hanghullámok közvetlenül érték a mikrofont. Most hasonlítsuk össze a 2cd200.sam és a 2cd220.sam hangmintákat. Ugyanabban a szobában történt a felvétel különböző távolságban, azonban a második esetben a hangforrást és a mikrofont egymásnak ellenkező irányba fordítottuk, úgy hogy az utóbbi egy sarokban helyezkedett el. A két hangmintát meghallgatva meglepődötten tapasztalhatjuk, hogy a nagyobb távolságban végrehajtott teszt „erősebb” hangzást produkál. Erre a hangforrás és a mikrofon elhelyezése ad magyarázatot: a szoba sarka felerősítette a nem közvetlenül érkező hanghullámokat.

Miután megvizsgáltuk, hogy a távolság illetve a hangforrás és a mikrofon helyiségen belüli pozíciója miként befolyásolja a hangzást, nézzük meg, hogy különböző akusztikai terek milyen különbségeket okoznak összességében. Az eredmény függvényeink között található 2cdb2.sam, 2cdr2.sam és 2cdg.sam, amelyek közül az első egy fürdőszoba, a második egy nagyobb szoba és a harmadik egy garázs hangzását adja vissza A legmarkánsabb az összes közül talán a fürdőszoba, amely igen nagy utózengési idővel rendelkezik a rendkívül nagy csempe és tükörfelületnek köszönhetően.

Végül nézzük meg a mesterséges visszhang kialakításának lehetőségeit. Itt ahelyett, hogy egy valódi visszhangot produkáló hangteret kerestem volna, csaláshoz folyamodtam. Ugyanúgy jártam el az impulzusválasz tesztnél, mint bármilyen más terem esetében, csupán annyi volt a különbség, hogy több impulzust generáltam egymás után, attól függően, hogy milyen időkéséssel akartam létrehozni a visszhangot, illetve egyszeres, vagy többszörös visszhangot akartam-e. Ha például a tihanyi visszhangot szeretném modellezni, akkor egymás után két impulzust kell generálni, mivel egyszeres visszhangról van szó. Természetesen az első nem lehet olyan „magas”, mint a másik, mivel számolnunk kell bizonyos energiaveszteséggel. A hangnak kb. 1000 m-t kell megtennie, ami 340 m/s-os sebesség mellet 2,94 s-ot jelent, esetünkben hangmintára átszámítva: 129654. Azonban a rendszer is rendelkezik bizonyos időkéséssel, így 129654-nél kisseb kell, hogy legyen a két impulzus távolsága. Az eredményt visszahallgatva egyértelmű, hogy az nem tökéletes, mert rendkívüli módon zeng. Ennek az oka az, hogy nem szabadtérben hajtottuk végre az impulzusválasz tesztet, hanem egy teremben, így annak az utózengése megmaradt a hangmintán. Ez azonban jó példa lehet arra, hogy miként lehet különböző termek hangzásait „összekeverni”. A gyakorlatban is sokszor találkozunk olyan esettel, amikor van egy visszhangosított termünk, amelynek nem megfelelő az utózengése, ugyanakkor szeretnénk a termenek a visszhang tulajdonságát megőrizni. Ilyenkor nincs mit tennünk, mint a megfelelő utózengésű teremben mesterségesen létrehozni a visszhangot és az így keletkező válaszfüggvénnyel konvolúciós szűrést végezni a stúdió felvételen. A 3.4. pontban említett, nem kívánatos teremvisszhangok kiküszöbölése is lehetséges hasonló módszerrel.

Egyszeres visszhanggal rendelkező termet modellez 2cdecho2.sam. Szintén egyszeres visszhangra mutat példát vecho3.sam, a különbség annyi, hogy itt egy rövid mondat hangzik el. A vfecho.sam  a csörgővisszhangot igyekszik bemutatni, amely a többszörös visszhang egy speciális fajtája. Végül pedig a tihanyi visszhang időkésését utánzó hangállományt érdemes megvizsgálni: vtihany.sam.

Látható tehát, hogy ahelyett, hogy az adott akusztikai térben vettük volna fel a teljes hanganyagot elég volt csupán az impulzusválasz tesztet elvégezni, majd annak eredményét tárolni, ezáltal időt spórolva és lehetővé téve az újboli felhasználáhatóságot. Magyarán nem kell mindig az adott hangversenytermbe, vagy templomba rohanni, hogy a teljes zeneművet ott vegyük fel az adott hangzás elérése érdekében, ha ezt megtehetjük otthon, vagy a stúdióban egy számítógép segítségével.

Végül eljutottunk odáig, hogy magát a tesztet sem a valóságos helyen hajtottuk végre, hanem modelleztük annak valamilyen tulajdonságát.

Irodalomjegyzék

[1]  Tarnóczy Tamás: Hangnyomás, hangosság, zajosság, Akadémia Kiadó, Budapest, 1984

[2]  Tarnóczy Tamás: Teremakusztika I-II. kötet, Akadémia Kiadó, Budapest, 1986

[3]  Peter M. Ridge – David M. Golden – Ivan Luk – Scott E. Sindorf: Sound Blaster: The Official Book, Panem – McGraw-Hill, Berkeley, California, 1994

[4]  László József: Hangkártya programozása Pascal és Assembly nyelven, Computer Books, Budapest, 1999

[5]  Bajusz – Bors – Csibra – Horváth: A PC-k hangja, COM-WARE, Budapest, 1995

[6]  Székely Vladimír: Képkorrekció, hanganalízis, térszámítás PC-n (Gyors Fourier transzformációs módszerek), Computer Books, Budapest, 1994

[7]  Gyimesi László: Digitális jelfeldolgozás, SZIF-UNIVERSITAS Kft., 1999

[8]  Giczy Gergő: Hanghatár, CHIP magazin, XII. évf., 1. szám, 92-96. oldal

[9]  Köhler Zsolt: Hangoskodás, Computer Panoráma, XI. évf., 6. szám, 40-47. oldal

[10]        Szittya Ottó: Digitális és analóg technika, LSI Oktató Központ, Budapest 1999

[11]        Markó Imre: PC-k konfigurálása és installálása (A hardver), LSI Oktató Központ, Budapest 1999

Hangkártyák programozása retro 22.

8.3. A Fourier térben végezhető konvolúció C függvénye

A következő struktúra definiálása szükséges:

typedef struct { int n;         // A függvény elemek száma

int n0;        // Az első elem valódi indexe

double *fv;    // Mutató a függvény elemek tömbjére

int ndft;      // A dft alappontok száma, vagy 0

complex *dft;  // Mutató a dft értékekre, vagy NULL

} fvtype;

A függvény az fvtype struktúrákban a Fourier transzformáltat is tárolja, azt csak akkor számolja újból, ha szükség van rá.

extern “C” __declspec(dllexport)

int WINAPI convol_by_fft(fvtype *fvta,fvtype *fvtb,fvtype *fvtres)

// A bemenő függvényeket és az eredmény függvényt kapja paraméternek

{ long int i,na,nb,nres,ndft,logndft;

double s,*dbuff,*dp1,*dp2;

complex *cp1,*cp2,*cp3;

//Előkészítések

na=fvta->n;

nb=fvtb->n;

nres=na+nb-1; //Az eredmény vektor hossza

ndft=1; //A DFT hossza

logndft=0;

while(ndft<nres) { ndft<<=1; logndft+=1; }

refft_prepare(logndft); //Az FFT előkészítése

//Operandusok transzformálása

if(dft_fvtype(fvta,ndft)) goto alloc_err;

if(dft_fvtype(fvtb,ndft)) goto alloc_err;

//Konvolúció

free_fvtype(fvtres);

fvtres->dft=(complex*)farcalloc(ndft,sizeof(double));

if(fvtres->dft==NULL) goto alloc_err;

cp1=fvta->dft;

cp2=fvtb->dft;

cp3=fvtres->dft;

cp3->re=cp1->re*cp2->re;

cp3->im=cp1->im*cp2->im;

cp1++; cp2++; cp3++;

for(i=1;i<ndft/2;i++) cx_mul(cp1++,cp2++,cp3++);

fvtres->ndft=ndft;

//Visszatranszformálás

dbuff=(double*)farcalloc(ndft,sizeof(double));

if(dbuff==NULL) goto alloc_err;

dp1=(double *)fvtres->dft;

dp2=dbuff;

s=(double)ndft;

for(i=0;i<ndft;i++) *(dp2++)=*(dp1++)*s;

reifft(dbuff);

//Az eredmény elhelyezése az eredményvektorban

fvtres->fv=(double*)farcalloc(nres,sizeof(double));

if(fvtres->fv==NULL) goto alloc_err2;

dp1=dbuff;

dp2=fvtres->fv;

for(i=0;i<nres;i++) *(dp2++)=*(dp1++);

fvtres->n=nres;

fvtres->n0=fvta->n0+fvtb->n0; //Kezdőpont offset

//Kilépések, hibajelzések

farfree(dbuff);

refft_close();

return(0);

alloc_err2:

farfree(dbuff);

alloc_err:

refft_close();

return(1);

}

A fenti függvény szintén megtalálható a F.27-ben.

A Delphi-ben közvetlenül alkalmazott C függvények deklarálása, illetve rövid leírása:

function ReFftPrepare(log_n:longint):integer;

stdcall; external ‘prjconvoldll.dll’ name ‘refft_prepare’;

A fenti függvény feladata, hogy előkészítse a bemenő valós vektor transzformációjához szükséges munkaterületeket és az exponenciális függvény táblázatos kezelését. A transzformáció elvégzése előtt mindig ezt a függvényt kell meghívni, s itt kell megadni, hogy hány alappontú transzformációt szeretnénk végezni.

procedure ReFftClose;

stdcall; external ‘prjconvoldll.dll’ name ‘refft_close’;

A ReFfftClose függvény, illetve eljárás a ReFftPrepare által lefoglalt munkaterületek felszabadítását végzi, ha már nem kívánunk további transzformációkat végezni.

procedure ReFft(poi:pdouble);

stdcall; external ‘prjconvoldll.dll’ name ‘refft’;

Ez az eljárás végzi a valós bemenő vektor gyors Fourier transzformációt, miután megtörtént a munkaterületek előkészítése és az exponenciális függvény táblázatos megadása. Algoritmusát és az azt megvalósító program kódját részletesen tárgyaltuk az előző fejezetekben.

procedure ReiFft(poi:pdouble);

stdcall; external ‘prjconvoldll.dll’ name ‘reifft’;

A ReiFft eljárás végzi az inverz gyors Fourier transzformációt, amely szintén a szükség előkészítések után végezhető el eredményesen. Paraméterként a transzformálandó vektor kezdetére mutató pointert vár.

procedure InitFvtype(fvt:pfvtype);

stdcall; external ‘prjconvoldll.dll’ name ‘init_fvtype’;

A fenti eljárás a kezdeti, üres állapotnak megfelelően kitölti az fvt struktúrát, illetve rekordot. Deklarációt követően használata kötelező, de a tapasztalat azt mutatja, hogy a Delphi eleve az üres állapotnak megfelelően tölti ki a struktúrát, vagyis elvégzi helyettünk az inicializálást. Delphi-ben használata inkább ajánlatos, mint kötelező érvényű.

function FillFvtype(n:longint;n0:longint;data:pdouble;fvt:pfvtype):integer;stdcal;external ‘prjconvoldll.dll’ name ‘fill_fvtype’;

A megfelelő adatsor alapján a FillFvtype függvény kitölti az adott struktúrát, illetve rekordot.

function Convolution(fvta:pfvtype;fvtb:pfvtype;fvtres:pfvtype):integer;

stdcall; external ‘prjconvoldll.dll’ name ‘convol_by_fft’;

A tulajdonképpeni konvolúciót végző függvény, amelynek mindhárom paramétere egy fvtype típusú rekord. A rutin meghatározza a konvolúció eredményének hosszát és az FFT alappontjainak minimálisan szükséges hosszúságát, majd előállítja a két operandus transzformáltját. Kiszámítja az operandusok DFT-inek szorzatát, és ezt az eredmény DFT mezőjére helyezi. Végül elvégzi az eredmény visszatranszformálását.

A következő metódus magának, a konvolúciónak a teljes menetét mutatja be:

procedure TForm1.Convolution1Click(Sender: TObject);

var i:longint;

begin

//A kurzorunk jelzi, hogy dolgozunk

screen.cursor:=crHourglass;

try

//Függvények inicializálása

InitFvtype(@fv1);

InitFvtype(@fv2);

InitFvtype(@fv3);

//Az operandus függvények előkészítése, hiba esetén jelzés, majd kilépés

if(FillFvtype(high(p1^)+1,0,@p1^,@fv1))<>0 then

begin

ShowMessage(‘Allocation error while FillFvtype!’);

exit;

end;

if (FillFvtype(high(p2^)+1,0,@p2^,@fv2))<>0 then

begin

ShowMessage(‘Allocation error while FillFvtype!’);

exit;

end;

//Konvolúció elvégzése, hiba esetén jelzés, majd kilépés

if (Convolution(@fv1,@fv2,@fv3))<>0 then

begin

ShowMessage(‘Allocation error while Convolution!’);

exit;

end;

//A konvolúció eredményének kiolvasása

for i:=low(pres^) to high(pres^)-1 do

begin

pres^[i]:=fv3.fv^;

inc(fv3.fv);

end;

//Ha végeztünk üzenet, illetve a kurzor visszaállítása

ShowMessage(‘Convolution ready!’);

finally

screen.cursor:=crDefault;

end;

button:=3;

//Az eredmény függvény grafikus megjelenítése

DrawWaveRes(pres^);

end;

Egy, az FFT elvégzésére alkalmas metódust is bemutatok:

procedure TForm1.normal1Click(Sender: TObject);

begin

//Jelölések tájékoztatásul: most normal FFT-t végzünk

normal1.checked:=true;

inverse1.checked:=false;

screen.cursor:=crHourGlass;

try

//A transzformálandó függvény kiválasztása

case button of

1:

begin

// Munkaterület, exp.tábla előkészítése,

// transzformáció alappontjainak száma

if ReFftPrepare(lognfft)<> 0 then

begin

ShowMessage(‘Allocation error while FFT!’);

exit;

end;//if

//Gyors Fourier transzformáció elvégzése

Refft(@p1^);

//Lefoglalt munkaterületek felszabadítása

ReFftClose;

//A transzformált grafikus megjelenítése

DrawWaveSam(p1^);

end;//1

2:

begin

if ReFftPrepare(lognfft)<> 0 then

begin

ShowMessage(‘Allocation error while FFT!’);

exit;

end;//if

Refft(@p2^);

ReFftClose;

DrawWaveSam(p2^);

end;//2

3:

begin

if ReFftPrepare(lognfft+1)<> 0 then

begin

ShowMessage(‘Allocation error while FFT!’);

exit;

end;//if

Refft(@pres^);

ReFftClose;

DrawWaveRes(pres^);

end;//3

end;//case

finally

screen.cursor:=crDefault;

end;

end;

Lejjebb, F.24-ben példát láthatunk a konvolúció eredményére.

A konvolúciót, illetve az FFT-t végző teljes Delphi forrás pedig a F.28-ban tanulmányozható.

F.24.a

Impulzusválasz függvénye

F.24.b

Tetszőleges gerjesztés függvénye

F.24.c

Konvolúció eredménye

F.24. Mesterséges hangzás megvalósítása konvolúcióval

Hangkártyák programozása retro 20.

8. Konvolúció a Fourier térben

Miután megismerkedtünk a diszkrét Fourier transzformációval és az azt megvalósító FFT-vel, a mesterséges hangzás létrehozásának elvét tárgyaljuk. Ahogy előzőleg említettem, erre a konvolúciót fogjuk használni. Nézzük meg először az elvét, illetve algoritmusát alapján és a megvalósítást végző program kódját [6] alapján.

8.1. Folytonos függvények konvolúciója

A konvolúció fogalmát egy egyszerű elektromos hálózat példáján keresztül fogjuk levezetni. Legyen adott egy áramkör, amely ellenállásokból és kondenzátorokból áll. A kondenzátorok miatt a hálózat bementére adott jel késéssel és némileg torzult formában jut el a kimenetre. Vezessünk be egy egyezményes mérőjelet, amelyet a hálózat bementére juttatunk és mérjük kimenetét, vagyis a bemenetre adott válaszát. E mérőjel legyen egy elég keskeny Dt szélességű feszültség impulzus. Az impulzus integrálja legyen egységnyi. Következésképp 1/Dt magas lesz. A Dt szélességű mérőimpulzusra adott rDt(t) válasz független Dt tényleges értékétől. Ez annál inkább tapasztalható, minél kisebb a Dt idő a válasz-impulzus lefutásának teljes tartamához képest. Ebből következik, hogy a Dt időt a hálózat tulajdonságait figyelembe véve kell megválasztani, mindig úgy, hogy Dt igen kicsi legyen a hálózat tranziens időihez képest. (Lásd F.21.)

F.21. Elektromos hálózat impulzusválasza

Természetesen a mérőjelet függetlenné tehetjük az adott hálózattól, mégpedig a Dirac-d gerjesztés bevezetésével. Ez esetben Dt kicsi bármely hálózat tranziens időihez képest, tehát Dt->0, vagyis mérőjelünk végtelenül keskeny, de ugyanakkor egységnyi területű impulzus, tehát végtelen magas is egyben. A hálózatnak a Dirac-d bemenetre adott válasz-függvénye a hálózat súlyfüggvénye. Ha tehát Dt tart zérushoz, a Dt szélességű impulzusra adott válasz tart a súlyfüggvényhez:

(8.1)

Dirac-d tulajdonságai a követezőek:

  • Értéke minden t értéknél zérus, kivéve a t=0 pillanatot, amikor végtelen,
  • Integrálja egységnyi.

Látható, hogy válasz csak a gerjesztés követően jön létre, vagyis míg a bemeneti Dirac-d a t=0 pillanatban jelentkezik, addig a súlyfüggvény csak a t>=0 pillanatokban lehet zérustól eltérő.

Ahhoz, hogy megértsük a hálózat tetszőleges gerjesztésére adott válasz számolását, tekintsük a következő példát. A bemenő függvényünket bontsuk fel Dt szélességű szakaszokra és közelítsük a függvényt a szakasz elején felvett értékével. Eredményként a gerjesztő függvényt, mint impulzus sorozatot kapjuk meg. Egyetlen Dt szélességű impulzusra ismerjük a hálózat válaszát: rDt(t). Jelen esetben pedig több, különböző magasságú, különböző időpontban jelentkező bemenő impulzusunk van. (Lásd F.22.)

F.22. Tetszőleges gerjesztő függvény felbontása Dt szélességű impulzusokra

Az i-edik impulzus a t=0 időponthoz képest iDt-vel később jelentkezik és magassága nem 1/Dt, hanem g(iDt). Tehát a válasz több rDt(t) formájú függvény összege lesz, amelyek a gerjesztő impulzus időbeni eltolásának és változó magasságának megfelelően eltolva és konstanssal szorozva jelentkeznek:

(8.2)

Ne felejtsük el, hogy lineáris hálózatról van szó, így több gerjesztés együttes hatása egyenlő  az egyenkénti gerjesztések hatásának összegével.

Finomítsuk most végtelenül a gerjesztő függvény impulzusokra bontását, azaz Dt tartson zérushoz. Ekkor (8.2) summája alatt az rDt(t) válaszfüggvény az s(t) súlyfüggvényhez tart. Maga a summa egy integrál közelítő összege, ami Dt->0 határátmenettel a t=iDt szerinti integrálba megy át a következő módon:

(8.3)

A fenti képlet a hálózatelméletből ismeretes, lineáris rendszerekre vonatkozó Duhamel tétel alapján vezethető le. A képlet maga pedig az úgynevezett konvolúciós integrál.

Az integrálási határok abból adódnak, hogy az integrálásnak csak azon t tartományra kell kiterjednie, amelyen az integrandus nullától eltérő lehet. Mivel feltételezzük, hogy

s(t)=0 ha t<0,

g(t)=0 ha t<0                            (8.4)

a t<0 esetben g(t) biztosan zérus, a t>t esetben s(t-t) biztosan zérus.

A v(t) válaszfüggvény tehát a g(t) gerjesztési- és az s(t) súlyfüggvény konvolúciója szerint számolható:

v(t)=g(t) X s(t)                            (8.5)

A gyakorlatban a konvolúciós integrál értelmezése igen érdekes: a g(t) függvényre rárajzoljuk az s(t)-t, a negatív t értékek felé áttükrözve és az origóját t pontba eltolva. Majd a közös szakaszon szorzatukat integráljuk, vagyis nem teszünk mást, mint vesszük g(t) függvénynek az s(t) súlyfüggvénnyel súlyozott integrálját. A v(t) függvény t paraméterét változtatva az s(t) függvény kezdőpontja eltolódik, s az s(t) szinte végigcsúszik a g(t)-n, miközben mindig más szakaszon képződik a súlyozott átlag.

Meg kell említeni, hogy a konvolúciós szűrés átalában információveszteséggel jár. Ennek vizsgálatához ismét egy egyszerű elektromos hálózatot választunk. (Lásd F.23.)

F.23. Integráló tag és súlyfüggvénye

Az integráló tag súlyfüggvénye a következőképpen alakul:

s(t)=(1/t)e-t/t  ha t<=0

s(t)=0 ha t<0                            (8.6)

Az ezzel való konvolúció durván t nagyságú időtartományra való átlagolásnak felel meg. Ha t igen nagy a vizsgált jel tartamához képest, akkor ez a konvolúció közelítőleg a jel integrálját adja. Az átlagolás miatt a magasabb frekvencia összetevők egyre kisebb amplitúdóval jutnak át a rendszeren. Ha a szűrés hatását semlegesíteni szeretnénk, megpróbálhatjuk alkalmazni a konvolúció inverz műveletét, a dekonvolúciót, azonban számolnunk kell azzal, hogy a kimeneten véletlenszerű feszültségingadozás, vagyis zaj jelenik meg. Ha a jel magas frekvenciájú összetevői annyira legyengülnek, hogy a zajszint alá esnek, akkor a kiegyenlítő visszaerősítés már nem az eredeti komponenseket, hanem hozza vissza, hanem a zajt.

Hangkártyák programozása retro 19.

7.1.8. FFT valós vektor esetén

A (3.34) szerinti általános rész-transzformáltra áll, hogy

(7.40)


Ebből az fi valós esetben egyenesen következik, hogy

(7.41)


Ahol * a konjugált képzést jelenti. Más szóval:

(7.42)

számsorozat redundáns, hiszen az r=N/2p fölötti rész konjugálással képezhető a kisebb indexűekből.


Nézzük, mennyi a transzformált nem redundáns része. Ez a következőkből áll:

valós érték (7.34) szerint egyértelmű,

valós érték,

komplex értékek.

Ha összeszámláljuk, kiderül, hogy ez éppen N darab adatot jelent (a komplex számot itt két adatnak vettük). Ez megfelel a várakozásnak, hiszen a transzformáció N darab adatból indul.

A valós bemenő vektorra egyszerűsített algoritmusnál tehát nem tároljuk a teljes rész-transzformáltat, csak a nem redundáns részt. Ennek megfelelően át kell rendeznünk a (7.39) szerinti rekurziós formulát. Ez a rendezés az alábbi eredményt adja, amelyet nem részletezek:

                                        (7.43)

 

(7.44)

 

(7.45)

Egy N/2 elemű komplex vektorban a 0 indexű elem nem tényleges komplex szám, hanem a valós illetőleg a képzetes rész helyén a transzformált két tisztán valós elemét, D0 és DN/2 –t tároljuk. A transzformáció eredményét ezen rend szerint kapjuk. Ez a tárolási séma a rész-transzformáltakra is vonatkozik.

A transzformációt végző C függvény [6] alapján a következő:

//A megfelelő paraméterátadási konvenció beállítása: WINAPI,

//és a függvény láthatóvá tétele a külvilág számára: __declspec(dllexport)

extern “C” __declspec(dllexport)

void WINAPI refft(double *poi) //transzformálandó vektor, mint paraméter

{ unsigned long int p,p2,q,r,i,nper2p,nper4p;

double s,*poi1,*poi2;

complex *p_exp;

complex *p_from,*p_to,*p1_from,*p1_to,*p2_from,*p2_to;

complex cx,cxexp;

//Első rekurziós lépés megvalósítása

p=n/2;

poi1=poi; //Munkaterületek kezdőcímeinek kijelölése

poi2=poi+p;

p1_to=fftbuff;

for(q=0;q<p;q++)

{ p1_to->re=*poi1+(*poi2);

p1_to->im=*poi1-(*poi2);

poi1++;

poi2++;

p1_to++;

}

//A további rekurziós lépések ciklusa

p_from=fftbuff;

p_to=(complex *)poi;

for(i=1;i<logn;i++)

{ p2=p;

p>>=1;

nper2p=n/p2;

//Az r=0 és r=n/2p eset

p2_from=p+(p1_from=p_from);

p1_to=p_to;

for(q=0;q<p;q++) { p1_to->re=p1_from->re+p2_from->re;

p1_to->im=p1_from->re-p2_from->re;

p1_from++;

p2_from++;

p1_to++;

}

p1_from+=p;

p2_from+=p;

p2_to=p_to+n/2-p;

//Általános eset

if(n>=4*p)

{ p_exp=exp_tabl+p-1;

nper4p=nper2p/2;

for(r=1;r<nper4p;r++)

{ cx_copy(p_exp,&cxexp);

for(q=0;q<p;q++)

{ cx_mul(p2_from,&cxexp,&cx);

cx_add(p1_from,&cx,p1_to);

cx_sub(p1_from,&cx,p2_to);

p2_to->im=-p2_to->im;

p1_from++;

p2_from++;

p1_to++;

p2_to++;

}

p_exp+=p;

p1_from+=p;

p2_from+=p;

p2_to-=p2;

}

}

//Az r=n/4p eset

p2_from=p+(p1_from=p_from);

for(q=0;q<p;q++)

{ p1_to->re=p1_from->im;

p1_to->im=-p2_from->im;

p1_from++;

p2_from++;

p1_to++;

}

p1_from=p_from;

p_from=p_to;

p_to=p1_from;

}

if(logn%2) //Visszamásolás a poi által mutatott adaterületre

{ poi1=(double *)fftbuff;

poi2=poi;

for(i=0;i<n;i++)

*(poi2++)=*(poi1++);

}

s=1.0/(double)n; //Az eredmény végigosztása n-nel

for(i=0;i<n;i++) *(poi++)*=s;

return;

}

Az inverz transzformáció hasonló meggondolások szerint kódolható, listája megtalálható a [6] alapján készült F.27-ben.

A fenti függvényt Delphi-ben a következőképpen kell deklarálni:

procedure ReFft(poi:pdouble); //pdouble=^double

stdcall; external ‘prjconvoldll.dll’ name ‘refft’;

ahol prjconvoldll.dll az állomány, illetve a dinamikusan szerkesztett könyvtár, ahonnét a kérdéses függvényt behívjuk, refft pedig a C függvény eredeti neve.

 

[6]  Székely Vladimír: Képkorrekció, hanganalízis, térszámítás PC-n (Gyors Fourier transzformációs módszerek), Computer Books, Budapest, 1994


Hangkártyák programozása retro 18.

7.1.6. A gyors Fourier transzformáció (FFT)

Az FFT eljárás algoritmusát Cooley és Tukey dolgozták ki, majd 1965-ben publikálták. Ez az eljárás mondhatni forradalmasította a digitális jelfeldolgozást, mivel az addigi módszer rendkívül számításigényes volt, s így természetesen sok időt is vett igénybe.

Egy nyolc alappontú transzformáció lépései a következők:

  1. nyolc alappont,
  2. ezekből négy darab két alappontos transzformált előállítása,
  3. ezekből két darab négy alappontos transzformált előállítása,
  4. ezekből a nyolc alappontos transzformált, vagyis a végeredmény előállítása.
A 0-dik alappont a transzformációban részt vevő két alappont összege, míg az N/2-dik az előbbi két alappont különbsége, és így tovább. (Lásd F.19.)

F.19.

7.1.7. Az FFT algoritmus általános megfogalmazása és megvalósítása

Az algoritmust N alappontra adjuk meg, amely kettő hatványa. A transzformálandó függvény fi elemei komplex értékűek lehetnek. Az FFT algoritmus alapgondolata, hogy az N alappontú transzformáltat N/2 alappontú résztranszformáltból kell előállítani, utóbbiakat N/4 alappontra támaszkodó résztranszformáltakból, stb. Fogadjuk el a résztranszformált definíciójának a következőt:

(7.34)

Ahol r a résztranszformált elemeinek futó indexe, a felülre írt indexek pedig:

  • q: a rész-transzformált alappont sorozata az fq alapponton kezdődik,
  • p: a rész-transzformált alappont sorozata fi egymástól p-re lévő elemeiből áll (p mindig kettő hatványa).

F.20. alapján:

(7.35)

Mivel fi-nek N eleme van, a rész-transzformált N/p alappontra támaszkodik, ezért a transzformált ugyanennyi elemből áll:

(7.36)

Állítások:

Ha p=N, N darab rész-transzformáltunk van, amelyek egyenként megegyeznek az fi transzformálandó minta-sorozat egymás utáni elemeivel. Igazolás a (7.36) alapján Dr futó indexe csak r=0 lehet.  (7.34) kifejezésben ezért az exponenciális tényező értéke 1. Maga a szumma az egyetlen k=0 értékre terjed ki. Ezzel:

(7.37)

Ha p=1, egy darab transzformáltunk van, ami azonos a kiszámítandó diszkrét Fourier transzformálttal. Igazolás: ha p=1, (7.35) szerint q csak 0 értékű lehet. Ezt felhasználva a (7.34) képletben, az alábbi egyenletre jutunk:

(7.38)

Ami megegyezik a diszkrét Fourier transzformált definíciójával.

A p=2P eset rész-transzformáltjainak birtokában a p=P eset összes rész-transzformáltja kiszámítható az alábbi rekurzív formula segítségével:

(7.39)

Igazolását nem vezetjük le.

A fenti három állítás alapján megadható az FFT algoritmus:

  1. Az első állítás alapján a kiinduló fi adatsor egyben a p=N eset rész-transzformáltjainak sora.
  2. A harmadik állítás log2N-szeri egymást követő ismétlésével megkapjuk a p=N/2, p=N/4, stb., végül a p=1-hez tartozó rész-transzformáltakat.
  3. A második állítás szerint utóbbi azonos a keresett transzformálttal.

Hangkártyák programozása retro 16.

7.1.2. A Fourier sor exponenciális alakja

Ha felhasználjuk, hogy a trigonometrikus függvények a komplex exponenciális függvénnyel kifejezhetők, a következőkhöz jutunk:

Sin(x)=(1/2j)(ejx-e-jx)

(7.5)

Cos(x)=(1/2)(ejx+e-jx)

(7.6)

Ezek után az (7.1) egyenlet a következő alakban is felírható:

(7.7)

Ha átcsoportosítjuk a szumma alatti tagokat, akkor pedig így:

(7.8)

Ha bevezetjük a harmonikus összetevőkre a

Cn=(an-jbn)/2

(7.9)

komplex amplitúdót, a szumma alatti rész egyszerűbb alakját kapjuk:

(7.10)

ahol a

C-n=Cn* és C0=c0

(7.11)

jelöléseket is használtuk. (* a komplex konjugáltképzés jele).

Ha (7.9)-be behelyettesítjük a (7.3)-(7.4) integrálokat, közvetlenül kiszámolhatjuk a bevezetett komplex amplitúdót:

(7.12)

A fenti egyenlet valós f(t) függvény esetében eleget tesz a Cn=C-n* feltételnek, sőt az n=0 esetben (7.2)-t is kiadja.

Az L hosszúságú periodikus, valós f(x) függvényre a Fourier sor együtthatói:

(7.13)

Az együtthatókból a függvényt így állítjuk vissza:

(7.14)

Hangkártyák programozása retro 15.

7. Mesterséges hangzás megvalósítása

Elérkeztünk e munka legérdekesebb, ám legnehezebb részéhez. Azt akarjuk, hogy a stúdióban, „steril” körülmények között rögzített hanganyag úgy szóljon, mintha az a kívánt teremben történne. De vajon mit kell mindehhez tennünk?

Először is, ismerjük a termünknek az egyetlen impulzusra adott válaszát, ez lesz a súlyfüggvényünk. Másodszor, ismerjük a stúdióban felvett hanganyagot, ez lesz a gerjesztési függvényünk. A kívánt eredmény elérése érdekében elő kell állítanunk mindkét függvény Fourier transzformáltját, majd az eredményül kapott függvényeket elemenként össze kell szorozni, s ha ez megtörtént, a szorzás eredményének inverz Fourier transzformálásával meg is kapjuk a kívánt eredményt. A fenti folyamat nem más, mint két függvény konvolúciója.

Így igen egyszerűnek tűnik a dolog, de tisztában kell lennünk azzal, hogy a megvalósításhoz komoly matematikai ismeretek szükségesek, amelyeket a következő fejezetekben ismertetetünk [6], illetve [7] alapján.

7.1.1. A Fourier sor

Egy f(t) periodikus időfüggvény felbontható egy konstans tag, valamint végtelen sok szinuszos, koszinuszos időfüggésű összetevő összegére. A periodikus jel T periódusidejének reciproka adja az alapharmonikus frekvenciáját, a további összetevők frekvenciája ezen alapfrekvencia egész többszöröse. Matematikai formában ez a következőképpen írható fel:

(7.1)

Ahol c0 stacionárius egyenáramú összetevőt így kapjuk:

(7.2)

A koszinuszos, illetve szinuszos összetevők amplitúdóját pedig így számolhatjuk:

(7.3)

(7.4)

Egy függvény akkor fejthető Fourier sorba, ha a következő feltételeket teljesíti:

  • f(t) függvény legyen T tartományon belül korlátos,
  • legyen integrálható,
  • legyen legalább szakaszonként differenciálható.

Ha a fenti feltételek teljesülnek, a (7.1) sor az f(t) függvény folytonos pontjain magához a függvényhez, szakadási helyein a bal- és a jobboldali határértékek számtani közepéhez konvergál. A hangfeldolgozásban előforduló függvények mindig megfelelnek az említett feltételeknek. (7.3 ) és (7.4) tanulmányozásával megállapíthatjuk, hogy páratlan f(t) függvény esetén minden an együttható, páros függvényesetén minden bn együttható zérus.

[6]   Székely Vladimír: Képkorrekció, hanganalízis, térszámítás PC-n (Gyors Fourier transzformációs módszerek), Computer Books, Budapest, 1994

[7]   Gyimesi László: Digitális jelfeldolgozás, SZIF-UNIVERSITAS Kft., 1999