În vederea implementarii acestei aplicatii am optat pentru folosirea mediului Visual C++ si a librariei MFC. Aceasta optiune e explicabila prin viteza de lucru oferita, usurinta lucrului precum si stabilitatea într-o utilizare îndelungata a aplicatiei.
Pentru înregistrarea respectiv redarea semnalului audio de la/la un dispozitiv periferic s-au folosit uneletele puse la dispozitie de tehnologia DirectX. Acest aspect integreaza aplicatia perfect în mediul Windows, dat fiind faptul ca o mare parte a operatiilor cu semnalul vocal sunte efectuate direct de DirectX. În aceasta categorie intra aspecte ca: selectarea device-ului de input la întregistrare, selectarea device-ului de output la redare, volumul înregistrarii / redarii si lista poate continua.
Dupa cum afirmam si în capitolele anterioare ideile de baza care au stat la structurarea aplicatiei au fost: utilizarea unor unelete parametrizabile, posibilitatea definirii unor liste de operatii ce se vor executa serial, usurinta ridicata în vederea extinderii sistemului de unelte utilizator. Toate aceste principii au influentat definitiv structura interna a aplicatiei si aici ma refer la structura claselor si la relatiile între acestea.
În continuare voi prezenta structura claselor principale urmarind aparteneta acestora la modulele enumerate anterior.
Stocarea semnalului audio în cadrul aplicatiei se face folosind o structura specifica ce se aseamana cu formatul PCM al fisierelor wav. De fapt acest format a si constituit sursa de plecare pentru structura clasei.
În câteva cuvinte structura unui fisier wav ar fi urmatoarea: fisierul începe cu un RIFF Header , care printre altele contine si identificatorul string “RIFF” pentru acest format. În continuare urmeaza specificarea tipului de RIFF. În cazul nostru aplicatia va citi fisierele cu RIFF Type-ul “WAVE”. Structura urmatoare reprezinta o componenta comuna pentru formatul Microsoft WAVE PCM , componenta în care se specifica elemente ca numarul de esantioane pe secunda, numarul de bytes pe secunda etc. Dupa aceste caracteristici urmeaza esantioanele propri-zise care în functie de caracteristicile semnalului vocal sunt memorate pe 8 sau 16 biti. Aplicatia WaveIO lucreaza cu semnale vocale memorate pe 8 biti, ceea ce este de altfel suficient pentru scopurile propuse ale aplicatiei.
Stocarea interna în cadrul aplicatiei a unui semnal vocal respecta în mare parte structura interna a fisierelor în format Microsoft WAVE PCM.
Toate informatiile legate de un semnal vocal, fie înregistrat cu prezenta aplicatie, fie deschis dintr-un fisier de pe disc sunt stocate într-o membra a clasei CWaveUnit . Inainte de prezentarea pe larg a clasei cu metodele si atributele sale este utila prezentarea unei diagrame de clase. Vezi Fig. 4.3

Fig. 4.3 Diagrama clasei CWaveUnit
Atributele membre ale clasei CWaveUnit, mai precis acele atribute care memoreaza caracteristicile si informatia propriu-zisa a semnalului sonor, sunt instante ale unor Type-uri.
Atributul riffHeader este definit de tipul de data RIFFHeader, ce va stoca identificatorul pentru tipul formatului, în cazul nostru RIFF precum si un atribut ce va contine informatii despre dimensiunea fisierului ce contine semnalul curent. Atributul riffType , asa dupa cum ii specifica si numele va contine numele tipului de RIFF. Aplicatia WaveIO opereaza doar cu WAVE RIFF.
O structura mai complexa are tag-ul WAVECommon . Acesta va contine informatiile necesare redarii folosind functiile puse la dispozitie de DirectX. În aceasta categorie intra:
wFormatTag continand categoria formatului, valoarea pentru WAVE FORMAT este 000x1;
wChannels: numarul de canale pentru fisierul wave deschis. Valoarea sa poate fi 1 sau 2, dupa cum semnalul este mono sau stereo. În prezent aplicatia WaveIO suporta doar formatul mono.
dwSamplesPerSec: numarul esantioane pe secunda, sau frecventa de esantionare. Aplicatia WaveIO suporta pentru acest atribut valorile de : 8000, 11050 si 22025, valori care sunt cel mai frecvent folosite în prelucrarea semnalului vocal.
dwAvgBytesPerSecond: numarul mediu de bytes pe secunda la care unda sonora ar trebui transferata. Software-ul de redare poate estima dimensiunea buffer-ului folosind aceasta valoare.
wBlockAlign: aliniamentul în bytes al blocurilor semnalului. Software-ul de redare are nevoie sa proceseze un multiplu de wBlockAlign bytes la un moment dat. Prin urmare valoarea lui wBlockAlign poate fi folosita la aliniamentul bufferelor.
În fine atributul care contine esantioanele propriu-zise se numeste wData fiind o instanta a WAVEData . Pe langa esantioane, pentru care exista un pointer ce va indica zona în care acestea sunt memorate, exista si o structura de header, ce memoreaza informatii suplimentare numita WAVEDataHeader . Tot în cadrul WAVEData vor fi stocate informatii cu referire la numarul de milisecunde reprezentând durata semnalului sonor, numarul de esantioane si respectiv limitele între care se încadreaza esantioanele.
Pentru a asigura o cat mai ridicata independenta a clasei CWaveUnit aceasta detine metode ce stiu sa citeasca dintr-un fisier wav continutul si respectiv sa salveze propriul continut într-un fisier de pe disc. Respectivele operatii sunt asigurate de metodele publice: ReadRiffHeader, ReadRiffType, ReadWaveFormat, ReadData pentru citirea datelor din fisier, respectiv Save pentru salvarea informatiei în fisier.
Alte metode importante ale clasei sunt GetData, care va returna esantioanele într-un vector de double dat ca parametru, respectiv metoda SetDataMono, care va efectua operatia inversa, anume va trece valorile dintr-un vector de double în vectorul de sample-uri. Utilitatea acestor metode apare în momentul în care se folosesc functiile DirectX pentru înregistrare respectiv redare, functii care primesc ca parametrii vectori de valori în care sunt memorate esantioanele.
Spuneam în partea teoretica a lucrarii ca majoritatea operatiilor efectuate asupra unui semnal sonor, nu se efectueaza asupra întregului semnal, ci mai întâi de împarte semnalul în segmente care sunt analizate sau prelucrate în parte si apoi rezultatele obtinute sunt combinate corespunzator.
Aceasta mentiune împreuna cu necesitatea de care vorbeam, ca utilizatorul sa poata defini o lista de opratii pe care sa le poata aplica serial au determinat necesitatea unei structuri noi de memorare a informatiei.
Pentru aceasta am contruit doua noi clase: CFrame si CFrameArray . Daca ar fi sa prezint aceste doua clase în câteva cuvinte, as putea zice, desi ar fi o informatie incompleta ca CFrameArray corespunde întregului semnal în timp ce CFrame reprezinta unui segment din cele în care este impartit semnalul.

Fig. 4.4 Diagrama de clase pentru CFrameArray si CFrame
Desi oarecum prea simplista comparatia între CFrame si CFrameArray pe de o parte , si semnalul si segmentele componente, pe de alta parte, totusi ea este utila pentru a servi ca punct de plecare în analiza celor doua clase.
Pentru început ma voi opri asupra clasei CFrameArray si mai exact asupra modului în care are loc construirea unei instante a acestei clase.
Initializarea si contruirea propriu-zisa a obiectelor, instante ale clasei CFrameArray se realizeaza prin apelul metodei Initialize . Antetul acestei metode este:
void CFrameArray::Initialize(CWaveUnit *extWaveUnit, unsigned int uiStartSample, unsigned uExtFrameSize, unsigned uExtOveralpping, int ewindow)
Orice obiect CFrameArray va fi legat direct de un obiect CWaveUnit . În cadrul metodei Initialize, sample-urile din CWaveUnit vor fi copiate în cadrul obiectului CFrameArray , spre zona respectiva “pointând” dInfo. De asemenea atributul waveUnit va fi setat la parametrul metodei: extWaveUnit . Ceilalti parametrii au urmatoarea semnificatie:
uiStartSample: esantionul de start de la care se începe copierea în CFrameArray
uExtFrameSize: dimensiune în sample-uri pentru fiecare segment/ frame.
uExtOverlapping : suprapunerea în procente a doua frame-uri consecutive
ewindow: fereastra care se aplica fiecarui frame în parte.
Ultimii trei parametrii sunt folositi la contruirea vectorului frames membru al clasei CFrameArray ale carui elemente sunt instante ale clasei CFrame .
Pe lânga faptul ca stocheaza sample-urile semnalului sonor, CFrameArray functioneaza si ca un manager al clasei CFrame . La construirea obiectului instanta a CFrameArray, se va initializa vectorul de obiecte CFrame, care fiecare în parte va stoca un anumit segment din esantioanele memorate. O instanta a clasei CFrame nu va stoca sample-urile propriu-zis ci va memora doar pozitia primului sample si numarul de sample-uri din intervalul alocat, accesul la informatia propriu-zisa fiind facut cu ajutorul unui pointer spre clasa CFrameArray “parinte”. Aceasta optimizare nu doar va economisi memoria folosita de aplicatie ci va evita posibilitatea aparitiei inconsistentelor ce puteau aparea daca esantioanele erau memorate atât în CFrameArray cât si în CFrame.
Asa dupa cum spune si teoria majoritatea operatiilor nu se efectueaza asupra întregului vector de esantioane, în cazul nostru asupra clasei CFrameArray ci asupra fiecarui frame în parte, în cazul nostru asupra clasei CFrame. Desi niciuna din operatii nu se aplica din exterior direct asupra CFrame ci numai prin intermediul CFrameArray. De aici apare situatia în care numele majoritatii metodelor din CFrameArray apar si în CFrame.
Daca ar fi sa prezentam o metoda generica din clasa CFrameArray, metoda ce efectueaza o anume operatie asupra semnalului vocal, aceasta ar arata în felul urmator:
return_type CFrameArray::OperatiaX( parametrii_x )
{
Operatii Pregatitoare;
for (idx = idx_start; idx <= idx_end; idx++)
{
frames[idx].OperatiaX(parametrii_y);
}
Combinare rezultate;
Operatii finale;
}
Desigur ca în functie de situatie unul sau mai multi din pasii metodei poate lipsi sau unii sa apara de mai multe ori.
În continuare ma voi opri pe larg asupra atributelor si metodelor celor doua clase.
Metoda de initializare a unui obiect CFrameArray este metoda Initialze , ai carei parametrii i-am explicat anterior. În cadrul acestei metode se va initializa vectorul de obiecte CFrame tinânadu-se cont de dimensiunea blocului precum si de factorul de suprapunere. Obiectele CFrame se initializeaza prin apelul metodei Create. În cadrul acestei metode se seteaza indicele primului sample precum si numarul de sample-uri si de asemenea se cheama metodele de calculare a numarului de treceri prin zero si de calculare a energiei semnalului în segmentul ocupat de frame-ul curent.
Majoritatea metodelor clasei CFrameArray reprezinta operatii aplicabile asupra semnalului vocal. Cea mai simpla astfel de operatie este cea de aplicare a unei ferestre pe segmentul alocat. Metoda corespunzatoare operatiei este SetWindow . Singurul lucru ce îl voi mai spune despre aceasta operatie este acela ca pentru aplicarea ferestrelor se foloseste de clasa CWindows , mai multe despre aceasta clase fiind prezentate mai târziu.
Metoda DetectSpeech este aplicata pentru detectia limitelor semnalului vocal în semnalul sonor memorat în obiect. Începutul si respectiv sfârsitul semnalului vocal este tinut în doua atribute numite speechStart si speechEnd.
Aceasta detectie se bazeaza pe caracteristicile diferite ale semnalului vocal de cele ale zgomotului. Astfel vom folosi doua criterii, si anume: energia (mica pentru zgomot, mare pentru semnal vocal) si numarul de terceri prin zero – NTZ – (mare pentru zgomot si mic pentru voce), prezentat sintetic în tabelul urmator
SEMNAL |
ENERGIE |
NTZ |
s. sonor (vocalizat) |
Mare |
Mic |
s. nesonor (nevoc.) |
Medie/mica |
Mare |
Zgomot |
Mica |
mare |
Liniste |
~0 |
~0 |
S-au definit doua praguri pentru energie, unul inferior EnMin si unul superior EnMax si doua pentru NTZ: NTZMin si NTZMax. Fie L numarul total de cadre de câte N esantioane din semnal. Algoritmul prezentat mai jos, care este si cel folosit în cadrul aplicatiei WaveIO este valabil atât pentru detectia începutului cât si a sfârsitului (caz în care prelucrarea se face de la sfârsit la început).
set k = 1, StartFrame = 1;
if En(k) >EnMax(k)
andif En(k)>EnMax
StartFrame = k;
Break;
if (NTZ(k) > NTZMin) & NTZ(k) < NTZMax
StartFrame = k;
Break;
endif;
endif;
3. end.
Algoritmul prezentat mai sus este implementat în cadrul metodei DetectSpeech din CFrame. În functie de rezultatul returnat de aceasta functie, în cadrul metodei DetectSpeech din CFrameArray , se va cauta primul si repectiv ultimul frame ce reprezinta semnal vocal marcându-se astfel începutul si sfârsitul vorbirii în semnalul audio curent.
Energia si numarul de treceri prin zero necesare detectiei liniste /vorbire se calculeaza aplicând formulele prezentate în modulul teoretic al lucrarii.
Urmatoarele atribute asupra carora ma voi opri în continuare sunt matricile de coeficienti cepstrali si LPC. Matricile sunt implementate în cadrul clasei CMatrix pe larg prezentata la un sub-punct urmator.Cele doua atribute ce stocheaza coeficientii respectivi sunt mtxLPCCoefs si mtxCepsCoefs , atribute ce apar atât în clasa CFrameArray cât si în CFrame . Calcularea coeficientilor are loc în metodele Cepstral si LPC din CFrame, în metodele omonime din CFrameArray având loc doar o combinare a rezultatelor. Aceste metode ofera si posibilitatea salvarii coeficientilor în fisiere. Calcularea coeficientilor LPC si cepstrali se face folosind algoritmii prezentati în cadrul suportului teoretic al lucrarii. Toate aceste metode vor primi ca parametru numarul de coeficienti care trebuie calculati, acest parametru venind din interfata grafica.
O metoda asemanatoare ca structura si functionalitate este metoda de determinare a spectrului de frecventa prin aplicarea Transformatei Fourier Rapide, DoFFT.
Aplicarea transformatei FFT asupra unui semnal da un rezultat continuu, dar se poate aplica aceasta functie si discret în n puncte fara o pierdere de informatie. În acest caz transformata ar avea urmatoarea forma:

iar transformata inversa:
![]()
În aplicatiea WaveIO, calculul transformatei Fourier se face utilizând algoritmul butterfly de calcul al FFT. Caracteristica principala a acestui algoritm este ca lucreaza cu operatii pe biti ceea ce ii asigura o viteza ridicata dar si îi determina câteva limitari. Printre acestea cele mai importante ar fi faptul ca numarul de frecvente determinate trebuie sa fie de forma “2 la puterea x”, iar domeniul în care sunt calculate frecventele nu este cel real. Oricum aceste doua limitari nu reprezinta niste inconveniente în cadrul aplicatiei noastre, primul deoarece oricum numai primele frecvente prezinta interes iar al doilea pentru ca acest spectru este calculat în vedea comparatiei cu spectre de acelasi fel, în acest caz contând diferenta între doua spectre si nu valorile absolute ale frecventelor.
Determinarea spectrului se face în fiecare cadru de semnal în metoda DoFFT din CFrame , pentru ca mai apoi aceste spectre sa fie combinate în cadrul metodei DoFFT din CFrameArray . Combinarea spectrelor consta de fapt în adunarea valorilor de pe indici corespunzatori din vectorii ce contin spectrul frecventelor în fiecare frame.
Spectrul de frecvente este memorat tot în cadrul unei matrici numita mtxFFTFreqs .
În fine, ultima metoda prezentata apartinând clasei CFrameArray este de fapt un exemplu de implementare a unei operatii de catre un utilizator ce doreste integrarea propriilor operatii în produs. Metoda corespunzatoare este UD_DetectSpeechFFT scopul ei fiind izolarea vorbirii folosindu-se de spectrul de frecvente.
Mai multe despre implementarea acestei metode vor fi prezentate în sectiunea “Implementarea uneltelor utilizator”.
Pentru asigurarea unei omogenitati în lucrul cu diferitele tipuri de coeficienti s-a ales implementarea unei singure clase care sa fie fie folosita indiferent de tipul de coeficienti. Acceasi structura este folosita si pentru lucrul cu spectrele de frecvente.
Daca e sa vorbim despre coeficientii LPC sau cepstrali se stie ca pentru fiecare cadru dintr-un semnal se determina câte un set de coeficienti. Asfel pentru o mai usoara abordare a problemei s-a ales o reprezentare asemanatoare celei de la CFrameArray si CFrame. În mod similar aici vom avea o clasa cu setul de coeficienti pentru un frame, numita CCoefs, si o clasa care va reuni toate aceste seturi de coeficienti, numita CCoefsArray.

Fig. 4.5 Structura claselor de coeficienti
Chiar daca nu este o asemanare între aceste multimi de seturi de coeficienti si un spectru de frecvente reprezentat de un vector uni-dimensional, pentru consistenta s-a ales solutia stocarii acestui vector folosind aceleasi clase sub urmatoarea forma: fiecare element de frecventa este stocat într-un obiect CCoefs în atributul dFreq .
Coeficientii cepstrali si LPC sunt memorati în cadrul matricei mtxCoefs . Totusi în cazul coeficientilor LPC, pe langa acestia în cadrul obiectului CCoefs se memoreaza si matricea R (vezi prezentarea metodei de determinare a coeficientilor LPC prin metoda autocorelatiei), într-o structura CMatrix, numita mtxR.
Pentru a se putea face diferenta între atâtea tipuri de date stocate în cadrul acestor structuri, clasa CCoefsArray contine un atribut numit uElementsType care în functie de valoarea sa va stabili tipul de date stocate: coeficienti LPC, coeficienti cepstrali, spectru de frecvente.
Legatura între CCoefs si CCoefsArray este realizata prin intermediul atributului ccElements care reprezinta un pointer din CCoefsArray spre CCoefs.
Incarcarea continutului obiectelor CCoefs se face din fisiere într-un format specific ce în prealabil au fost salvate pe disc dupa prelucrari cu unelte standard din aplicatia WaveIO. Dat fiind acest fapt clasa CCoefsArray dispune de o interfata de lucru cu fisierele WaveIO bine pusa la punct.
Prin intermediul metodelor acestei interfete se vor citi diferitele tipuri de coeficienti. Metodele sunt: ReadCoefs (metoda “parinte” din care se cheama celalalte în functie de tipul fisierului deschis), ReadCEPCoefs, ReadLPCCoefs, ReadFreqs .
În cazul în care clasa CCoefs este folosita pentru coeficienti LPC, pe lânga coeficientii propriu-zisi, mai sunt stocate si câstigul filtrului, în atributul d_Gap si eroarea medie patratica în dEMP .
În fiecare obiect CCoefs va exista un index ce memoreaza pozitia în CCoefsArray.
Motivatia pentru care s-a ales memorarea coeficientilor în cadrul acestor structuri este în calculul distantei între doua seturi de coeficienti sau respectiv doua spectre de frecvente. Aceste operatii pentru calculul diferentei sunt implementate la nivelul claselor prin metode specifice fiecarui tip de coeficienti. La fel ca în cazul claselor CFrame si CFrameArray si aici avem o corespondenta între metodele-operatii din CCoefs si CCoefsArray. Diferenta semnificativa fiind ca în acest din urma caz operatiile sunt implementate în metode friend.
Algoritmii teoretici de implementare ai diferentei între doua seturi de coeficienti cepstrali si respectiv LPC sunt cei prezentati în partea teoretica a lucrarii. Le fel, în cazul compararii a doua spectre s-au folosit conceptele prezentate în partea teoretica dar algoritmul propriu-zis este unul original al autorului acestei lucrari si este prezentat pe larg în cadrul capitolului Implementarea uneltelor comparative . Tot în acelasi capitol poate fi gasit si algoritmul generic al unei "metode-operatie comparativa” apartinând clasei CCoefsArray.