Vítám všechny programátorské nadšence v kurzu jehož tématem a účelem je dát do hromady několik těch řádků
kódu v C++ jež nám zajistí možnost přehrávání zvukových souborů ogg.
Nebudu se tady zabývat historií
ogg formátu ani nebudu vysvětlovat jak funguje komprese v něm použitá. Za prvé proto, že to není pro naše účely vůbec potřeba, a za druhé proto,
že jsem to sám nenastudoval :)Abychom mohli vůbec na ogg pomýšlet, budeme potřebovat Ogg SDK, které najdete na internetu nebo na CHIP Cd.
Konkrétně budeme používat knihovnu vorbisfile, která poskytuje funkce pro snadnou práci s ogg soubory. SDK obsahuje (můžeme také říct že součástí ogg kodeku jsou) ještě knihovny vorbisenc (slouží pro kódování do ogg)
,ogg a vorbis. Poslední dvě nebudeme probírat. Jednak proto, že obsahují funkce pro práci s ogg soubory na nejnižší úrovni a knihovny
vorbisfile a vorbisenc je používají a nabízejí nám funkce podstatně jednodušší. Dále také proto že programování s pomocí těchto knihoven nemám také zrovna v malíčku :).
To mě přivádí na poznámku, že jakékoliv poznatky, zkušenosti a připomínky jsou vítány. Naučíme se tedy dekódovat ogg soubor pomocí vorbisfile, od toho jsme
jen krůček k převodu na wav soubor. Podporu pro přehrávání ogg souborů budeme implementovat jako třídu, později jako rozhraní,
a uděláme též jakýsi manažer ogg souborů. Co se týká tohoto kurzu, uzpúsobím implementaci třídy a větší části projektu tak, aby bylo možné ji zahrnout do enginu který
je probírán v seriálu o DirectX. Ostatně C++ jsem se prakticky naučil díky Chipu.
Máte oproti mě tu výhodu, že vám tu vysvětlím, jak bude náš projekt pracovat a na vás už je jen si opsat uveřejněný kód, nebo
napsat vlastní. Nevím jak ostatním, ale mě se pro učení nových věcí nejvíc osvědčila metoda opisování kódu (například při učení DirectX). Následuje editace kódu.
Tak také většinou zjistím co se mi na implementaci nelíbí, uzpůsobím si kód k obrazu svému či dodělám věci co mi chybí. Jakmile danou problematiku aspoň z části zvládnu, pustím se
do dostupné dokumentace a odpoutávám se od seriálu, přesněji od ukázkového kódu a píšu vlastní kód. Tím se učím nejvíc.
Samozřejmě to nejde u každého tématu. Při hledání způsobu jak přehrávat ogg jsem také opisoval. Dokumentace k SDK, aspoň tedy mě, se zdá
nepříliš podrobná a pro člověka který o problému nic neví také moc vstřícná není. Teď když do ní nakouknu, je mnohem přátelštější a nechápu co mi předtím dělalo takové problémy.
Každpopádně jsem stáhl z internetu program, který přehrával ogg, docela jednoduchý, dobrý k učení, opsal jsem ho a pochopil jak funguje. Fungoval bez chyby, reproduktory vyluzovaly tóny přesně podle očekávání, ale vadila mi na něm jedna maličkost.
Jelikož knihovny kodeku Ogg neposkytují žádnou funkci typu Hraj, musíte sami zajistit samotné přehrávání, nejlépe pomocí DirectSound. To byl případ ukázkového programu a je to i případ náš. Program
inicializoval tuto součást DirectX, vytvořil zvukový buffer (sound buffer) pro ogg soubor, celý soubor do něj dekódoval a spustil přehrávání vytvořeného zvukového bufferu.
Když ale použijete soubor o délce třeba 3 minuty, vzorkováním 44100Hz, Stereo, 16bitů na vzorek, bude se muset vaše pamět připravit na cca (44100Hz = 44100 vzorků za sekundu, jeden vzorek má 2 bajty (tj 16bitů))
44100 vzorků/sekundu * 180 sekund * 2 kanály * 2 bajty = 31752000 bajtů = 31007,9kB = 30,3MB. Pokud bychom použili zmíněný postup třeba ve hře a potřebovali přehrávat nějaký
seznam hudebních souborů (playlist), chtělo by to asi načíst více souborů najednou. Za to by vás operační pamět asi ráda neměla. Nehledě na to že dekódování celého souboru do takového zvukového bufferu není záležitostí
jednoho cyklu procesoru a navíc je to velmi nefektivní,což to by naše svědomí programátora neustálo. Což ale nic nemění na tom, že zmíněný příklad je výborným odrazovým můstkem a já jsem jeho autorovi za něj vděčný. Bez něj bych se možná ani nedostal dál.
Hledal jsem tedy dál na internetu zda najdu nějaký elegantněji vyřešený příklad, ze kterého bych mohl čerpat, ale marně, nebo špatně. Říkáte, že by bylo nejlepší
dekódovat vždy jen nějakou část souboru a tu potom přehrát? Správně, to bude lepší řešení, naší paměti se bude daleko víc zamlouvat. A když si projdete správné příklady a části dokumentace z
SDK k DirectX týkající se DirectSound, přijdete jistě také jako já na způsob, jak tento plán realizovat.
Klíčovým slovem dne se pro nás tedy stává streamovaný zvukový buffer (streaming sound buffer). Ono vlastně DirectSound nic takového neposkytuje, jde jen o techniku používání zvukového bufferu, kterému se pak říká
že je streamovaný. Samotné DirectSound nám nabídne jen zvukový buffer.
Vytoříme si pro hudební soubor zvukový buffer do kterého se nám bude vejít jen krátký úsek záznamu, řekněme například pro 3s, 5s, to je jedno. Hodnota by ale neměla být příliš malá ani příliš vysoká. Pak spustíme přehrávání tohoto bufferu přičemž budeme tak nároční, že
mu přikážeme, aby pokaždé, když se přehraje do konce, se vrátíl zpět na začátek a hrál znovu a znovu a pořád dokud ho nezastavíme. Jeho obsah budeme tedy moci teoreticky poslouchat donekonečna, prakticky pak o něco kratší dobu (nebo snad už někdo má nápoj nesmrtelnosti?).
Já jsem zvolil 3s. Vy co jste si v těch příkladech k DirectSound z SDK DirectX všimli, že tam použili také 3s, mlčte! Následně musíme nahrát, respektive dekódovat do zvukového bufferu co se do
něj vejde, 3s. Co potom? Buffer je přehráván pořád dokola. Abychom neposlouchali pořád to samé, budeme muset data v bufferu měnit, tj stará data co už byla přehrána nahradit novými.
Pokud to není zcela jasné, udělám v tom větší zmatek ještě zavedením pojmu hrací a zapisovací (play and write) kursor (cursor).
Pomocí hracího kurzoru můžeme zjistit, jaký bajt bufferu je právě přehráván, tj. kolik bajtů od začátku bufferu už bylo přehráno. Zapisovací kurzor udává pozici na kterou můžeme zapsat nová data.
Přehrát celý buffer novými daty vždy když hrací kurzor dorazí na konec bufferu je také nevhodné, jednak by to program nestihl a na začátku by určitě vznikla nějaká díra, tj část zvuku bychom nikdy neslyšeli a také by mohlo dojít
k překřížení hracího a zapisovacího kurzoru. Tyto kurzory nikdy nesmí vzájemně ukazovat na stejné místo. To znamená, že pokud bude pozice hracího kurzoru n, můžeme zapisovat data před pozici n do n-1 a od
n+1 (což je taky dost o vyjímku) do konce bufferu, ale nikdy ne na n. Přesně řečeno, on zapisovací kurzor může ukazovat na stejné místo jako hrací kurzor, ale nikdy nesmíte na pozici n začít zapisovat v okamžiku kdy tam ukazuje i hrací kurzor, tj. buffer právě přehrává daný bajt! Rozdělíme si tedy zvukový buffer na několik částí, lépe řečeno, definujeme si po kolika bajtech budeme buffer obnovovat, říkejme tomu délka. Tu si stanovíme například tak, aby se vešla do bufferu například 16x.
Pokud se ve světě počítačů pohybujete delší dobu, jistě víte že je jakýmsi způsobem zaměřený spíše na sudá čísla, konkrétně na mocniny 2. Pokud se nepohybujete, víte to teď i bez toho běhání :)
Během přehrávání bufferu budeme hlídat vzdálenost mezi hracím kurzorem a zapisovacím kurzorem (poslední pozice kam jsme zapsali data) a vždy když překročí naši definovanou délku, nahradíme data v bufferu která začínají na pozici
zapisovacího kurzoru a končí na pozici hracího kurzoru - 1.
Jak se dozvíme že vzdálenost kurzorů už je dostatečně velká a že můžeme bezpečně obnovit data v bufferu bez nebezpečí překřížení kurzorů? To můžeme uděla dvěma způsoby. Jednak se můžeme
bufferu ptát v každém cyklu aplikace kde se nachází hrací kurzor a na základě toho se rozhodnout zda obnovit data či ne, nebo si můžeme bufferu říct, ať nás upozorní pomocí zprávy
windows že byla přehrána určitá délka bufferu, určitý počet bajtů. Vybral jsem si první způsob, který se podle mě více hodí pro použití v herním enginu, kde potřebujeme přehrávat více zvuků najednou,
namísto druhého způsobu, který se hodí více pro přehrávač, který přehrává jeden zvuk v jeden čas a nemusí se starat, který buffer o sobě dal vlastně vědět.
Celý postup demonstruje následující animace (k posuvu používejte šipku vpravo dole):
Jedna z posledních věcí kterou je snad třeba zmínit je, jak se budeme chovat u konce bufferu. Situace: Zapisovací kurzor je 2 délky před koncem bufferu. Řekněme že CPU počítače se zrovna někde zdrží a až se dostane k testu na délku vzdálenosti mezi kurzory, bude vzdálenost
překročena třeba 1.5 krát. My ale nahráváme data od poslední pozice zapisovacího kurzoru do pozice hracího kurzoru - 1. Nastavíme novou pozici zapisovacího kurzoru na pozici hracího kurzoru. Teď už nám před koncem bufferu zbyla jen půlka definované délky. Nevadí, budeme počítat tuto polovinu.
Buffer se vrátí na začátek a pokračuje v přehrávání. My počítáme bajty dál a přičítáme k tomu co zbylo na konci. Až bude vzdálenost dostatečná, nahrajeme polovinu dat na konec bufferu, přesně do mezery která nám tam zbyla a zbytek na začátek bufferu. Protože buffer by se měl obnovovat 16x (závisí na definici délky) za jeden průchod, nemůže
se stát že bychom nestačili nahrát na konec bufferu data včas než tam dorazí hrací kurzor a že bychom slyšeli ještě tóny které tam byly při minulém průchodu.
K vlastnímu přehrávání zvukových dat použijeme zvukový buffer (sound buffer), který je součástí DirectX.
Definujeme si délku, která rozdělí buffer na několik menších částí
Budeme hlídat vzdálenost mezi hracím a zapisovacím kurzorem a pokud překročí definovanou délku, nahrajeme nová data
do bufferu od pozice zapisovacího kurzoru do pozice hracího kurzoru - 1;
Pokud nám na konci bufferu nezbyde místo velikosti délka kam bychom nahráli data, budeme počítat vzdálenost ještě při
přehrávání bufferu znovu od začátku a až bude vzdálenost překročena nahrajeme data na konec bufferu a zbytek na začátek bufferu
opět do pozice hracího kurzoru - 1
Teď ještě něco na způsob vývojového diagramu (i když ho tak nenávidím :)):
Máte pravdu pokud si říkáte že program na vývojovém diagramu nikdy neskončí, šlo mi jen o nakreslení způsobu obnovy zvukového bufferu. O rozpoznávání
konce ogg souboru, přehrání celého souboru se budeme podrobně věnovat až později.
Doufám že se mi podařilo srozumitelně vysvětlit jak bude program pracovat, v části 1 začneme s implementací zvukového bufferu do kterého budeme později dekódovat ogg soubor.
K opravdové práci s funkcemi ogg se dostaneme ve druhé části, ale většinou to tak bývá, že člověk musí udělat spoustu nezáživné práce než dospěje do stádia kdy dělá něco nového a
zajímavého. V případě problémů, nedorozumění, námitků, postřehů a podobných dodatků mě kontaktujte na e-mailu či na icq.