2004.04_Grajek, czyli dekoder plików OGG_[Programowanie].pdf

(834 KB) Pobierz
439033900 UNPDF
dla programistów
Zobacz w:
Grajek, czyli dekoder plików
OGG
jak przedstawia się kod źródłowy programu
XMMS. Zadają sobie również pytanie, jak we
własnym programie zrealizować odtwarzanie
plików audio. Właściwie można to sprawdzić samemu, ścią-
gając odpowiedni pakiet i przeglądając źródła. Wydawać się
może, że program XMMS nie jest skomplikowaną aplika-
cją, ale każdy, kto spróbuje przejrzeć źródła tego programu
bądź wielu podobnych, będzie zmuszony zmienić zdanie.
Kod źródłowy, choć nie jest specjalnie duży, to rozwiązu-
je wiele problemów, które zaciemniają sam proces dekodo-
wania dźwięku.
W tym artykule chciałbym przedstawić przykład nie-
wielkiej aplikacji dekodującej pliki w formacie OGG, zdo-
bywającym coraz większą popularność. Wszystkie progra-
my przeznaczone do odtwarzania plików audio pozwala-
ją na odtwarzanie plików w formacie OGG, więc tworze-
nie własnego programu może wydawać się niepotrzeb-
ne. Nasza aplikacja posiada jednak walory edukacyjne. Ze
względu na jej niewielkie rozmiary, będzie przydatna dla
osób chcących poznać główne zasady dekodowania plików
audio.
(„high-level API”) to zaledwie kilka funkcji przeznaczonych
do sterowania procesem dekodowania.
Skoro tworzymy program w KDE, to powinniśmy sko-
rzystać z systemu dźwięku aRts , ale wybierzemy system
dźwięku ALSA , który stosunkowo niedawno doczekał się
wersji 1.0.
Funkcje do obsługi systemu ALSA
Pisanie programu odtwarzającego pliki w formacie OGG
rozpoczniemy od przygotowania kilku funkcji związanych
z obsługą dźwięku. Przeznaczenie tych funkcji jest dość
oczywiste: init_alsa oraz init_mixer dokonują inicjaliza-
cji systemu ALSA oraz miksera. Odwrotne w działaniu są
następujące funkcje: done_alsa oraz done_mixer . Nasz przy-
szły „player” pozwala sterować głośnością w mikserze, więc
potrzebna jest funkcja do odczytu głośności get_volume
i ustalania głośności set_volume . Kod tych funkcji jest
zawarty w pliku alsa_func.c i każdy, kto go przejrzy, zoba-
czy, że znajduje się tam definicja dodatkowych pięciu funk-
cji. Spis i znaczenie wszystkich funkcji zawiera Tabela 1.
Może dziwić brak definicji funkcji do odtwarzania danych.
Specjalna funkcja w istocie nie jest nam potrzebna, gdyż
skorzystamy z funkcji bibliotecznej ALSA snd_pcm_writei .
Przykład funkcji dokonującej inicjalizacji systemu
ALSA przedstawia Listing 1. Właściwą inicjalizację wykona
wywołanie snd_pcm_open . Dodatkowa funkcja alsa_error_
fnc obsługuje ewentualne błędy -- polega to tylko na
wyświetlaniu komunikatów tekstowych na konsoli. Następ-
Założenia początkowe
Naszą aplikację napiszemy w oparciu o program KDeve-
lop w wersji 3.0 (Rysunek 1). Zastosowanie tego narzędzia
oznacza, że program będzie przeznaczony dla środowiska
KDE – chociaż KDevelop aspiruje do miana uniwersalnego
IDE, np. pozwala pisać programy dla środowiska GNOME,
to jednak najlepiej wspiera system KDE oraz bibliotekę QT.
W przypadku dekodowania plików w formacie OGG,
nie mamy wyboru -- jest tylko jedna implementacja tego
standardu, czyli biblioteka OGG/Vorbis . Obiektywnie trzeba
jednak przyznać, że API, które oferuje, jest bardzo wygod-
ne w stosowaniu. Istnieją dwa zestawy funkcji. Pierwszy
to „low-level API”, czyli interfejs niskiego poziomu, gdzie
samodzielnie troszczymy się o odbiór danych, ich deko-
dowanie i zajmujemy się wieloma innymi detalami. Drugi
O autorze:
Autor zajmuje się tworzeniem oprogramowania dla
WIN32 i Linuksa. Zainteresowania: teoria języków
programowania oraz dobra literatura. Kontakt z autorem:
autorzy@linux.com.pl
Rysunek 1. KDevelop i tworzona przez nas aplikacja
58
kwiecień 2004
Marek Sawerwain
Z pewnością wielu Czytelników jest ciekawych,
439033900.015.png 439033900.016.png 439033900.017.png
dekoder plików ogg
Listing 1. Treść funkcji init_alsa
nia funkcji snd_pcm_writei . Przykład jej zastosowania jest
trywialny:
int init_alsa ()
{
if ( alsa_error_fnc ( snd_pcm_open (& handle, pcm_name,
stream, open_mode ))==- 1 )
printf ( „Błąd w snd_pcm_open \ );
return - 1 ;
}
sample_buffer = malloc ( sample_buffer_size );
snd_pcm_hw_params_alloca (& hwparams );
if ( set_hw_params ()< 0 )
{
printf ( „Błąd w set_hw_params \ );
return - 1 ;
}
}
err = snd_pcm_writei(handle, ptr, cptr);
Zmienna handle to uchwyt do urządzenia PCM, uzyskany
funkcją snd_pcm_open . Następny parametr ptr to wskaźnik
na bufor, a cptr to ilość ramek w buforze. Funkcja ta prze-
syła podane próbki bezpośrednio do karty. Naszym zada-
niem jest cykliczne dostarczanie danych do karty dźwię-
kowej, więc wywołanie snd_pcm_writei następuje w pętli
while , która w większości programów pisanych w systemie
ALSA (np. wtyczka ALSA dla XMMS) wygląda mniej więcej
tak, jak na Listingu 2.
Przekazanie zdekodowanych danych do karty to
zadanie wymienionej wcześniej funkcji snd_pcm_writei .
Koniecznie trzeba wprowadzić jedną poprawkę związa-
ną z ilością danych. Funkcja systemu ALSA mierzy ilość
danych w ramkach (ramka to pojedyncza próbka dźwię-
kowa; w naszym przypadku mamy dwa kanały: lewy
i prawy; dla każdego z tych kanałów dźwięk jest opisywa-
ny za pomocą dwóch bajtów, czyli ramka składa się z czte-
rech bajtów), natomiast w wyniku działania funkcji ov_read
(dekoduje plik w formacie OGG), zmienna tt zawiera ilość
danych, ale w bajtach. Przed wejściem do pętli przygotowu-
jemy zatem nową wartość, określającą ilość danych. Jest to
zmienna cptr , w której umieszczamy wartość zmiennej tt
podzieloną przez cztery.
Po operacji przygotowania zmiennych cptr i ptr ,
wchodzimy w pętlę. Jest ona konieczna, gdyż ALSA może
nie przyjąć w całości przekazywanych danych. W takim
przypadku zmienna err będzie zawierać ilość ramek
wysłanych do karty. Sprawdzamy także, czy zmienna err
nie zawiera informacji o potencjalnym błędzie. Może ona
przyjąć wartość -EAGAIN , co jest sygnałem, aby ponownie
ną istotną czynnością jest przydzielenie pamięci dla bufora,
w którym będą znajdować się rozkodowane dane przezna-
czone do odtworzenia. Wielkość buforu koniecznie musi
być podzielna przez cztery.
Ostateczne ustalenie parametrów to zadanie dla funk-
cji set_hw_params . Ustalamy w niej tryb dostępu. W naszej
aplikacji będzie to przeplatany tryb zapisu i odczytu, okre-
ślony przez wartość: SND_PCM_ACCESS_RW_INTERLEAVED .
Równie istotne jest określenie ilości kanałów i częstotliwo-
ści, z którą będzie próbkowany dźwięk:
err = snd_pcm_hw_params_set_channels(handle, hwparams,
channels);
err = snd_pcm_hw_params_set_rate_near(handle, hwparams,
&rrate, 0);
Powyższe dwie linijki kodu ustalają tryb stereo oraz często-
tliwość równą 44100hz. Ostatnim bardzo istotnym parame-
trem jest format danych, a będą to liczby szesnastobitowe
ze znakiem. Fragment kodu z pliku alsa_func.c jest nastę-
pujący:
Kompilacja potrzebnych pakietów
Zanim przystąpimy do kompilacji pakietów OGG/Vorbis, warto
sprawdzić, czy używana dystrybucja przypadkiem nie zawie-
ra tych bibliotek. W większości przypadków biblioteki powinny
być obecne. Gdy jednak chcemy przeprowadzić kompilację,
to w pierwszej kolejności kompilujemy pakiet libogg, a po nim
libvorbis. Pozostałe pakiety nie będą nam potrzebne. Ponie-
waż do kompilacji zastosowano system Autoconf i Automa-
ke, to kompilacja nie powinna sprawić kłopotów. Jak zwykle
w przypadku tandemu Autoconf i Automake, wydajemy trzy
polecenia:
static snd_pcm_format_t format=SND_PCM_FORMAT_S16;
...
err = snd_pcm_hw_params_set_format(handle, hwparams,
format);
Ponieważ na łamach Linux+ system ALSA był już omawia-
ny, więc pozwolę sobie Czytelnika odesłać do artykułu
z numeru 08/2003 (został on zamieszczony na płycie CDA/
DVDA w katalogu ALSA).
./configure –prefix=/usr
make
make install
Odtwarzanie próbki dźwiękowej
ALSA oferuje kilka trybów przesyłania danych do karty
dźwiękowej. Nasza aplikacja wykorzystuje z powodze-
niem najprostszą metodę, opartą o bezpośrednie wywoła-
W przypadku, gdy nie mamy programu KDevelop w wersji 3.0,
czeka nasz samodzielna jego kompilacja. KDevelop wymaga,
aby KDE było przynajmniej w wersji 3.0.2, ale większość dys-
trybucji w ostatnich wydaniach zawiera już wersję z serii 3.1.x.
www.linux.com.pl
59
439033900.018.png
dla programistów
Tabela 1. Spis funkcji omawianych w artykule
Dekodowanie pliku w formacie ogg
Znamy już dokładną metodę przesyłania zdekodowa-
nych danych do karty dźwiękowej. Teraz trzeba poznać
sposób, w jaki dane dekodujemy. Funkcje realizują-
ce proces dekodowania z formatu OGG w przypadku
API wysokiego poziomu nie są trudne do opanowania.
Co więcej, funkcje, które za chwilę przedstawię, są podob-
ne w użyciu do typowych funkcji bibliotecznych, takich
jak open , read , close czy seek . Każdy, kto popraw-
nie posługuje się funkcjami do obsługi plików, nie
będzie miał problemów z obsługą plików w formacie
OGG.
Zanim rozpoczniemy dekodowanie pliku, musimy go
otworzyć. Wykonujemy to funkcją fopen . Uzyskane odnie-
sienie do pliku przekazujemy funkcji ov_open w pierw-
szym parametrze, a w drugim podajemy adres zmiennej
reprezentującej właściwy uchwyt. W dalszych wywoła-
niach posługujemy się tym uchwytem (typ OggVorbis_File )
zamiast zmienną plikową FILE * . Czynności przygotowaw-
cze wyglądają następująco:
Plik alsa_func.c
int alsa_error_fnc(int i); wyświetla komunikat
o błędzie
int set_hw_params(); ustalenie parametrów
sprzętowych
funkcja po wystąpieniu błędu
próbuje doprowadzić do
„porządku” bufor
urządzenia PCM
void ind_obj_pcm(); odszukanie obiektu
miksera odpowiedzialnego
za sterowanie głośnią
urządzenia pcm
void set_volume(double v); ustalenie głośności
void get_volume(double *v); odczytanie aktualnej
głośności
int alsa_reset();
reset urządzenia PCM
int init_alsa();
inicjalizacja systemu
int done_alsa();
zamknięcie systemu
FILE *file_handle;
OggVorbis_File vf;
file_handle=fopen(”~/file.ogg”, „r”);
ov_open(file_handle, &vf, NULL, 0);
int init_mixer();
inicjalizacja miksera
int done_mixer();
zamknięcie miksera
Plik oggplay.c
int ogg_prepare_to_play
(const char *fn);
wykonanie wstępnych
czynności
int ogg_do_play_full(); odtworzenie całego pliku
int ogg_do_play_frag-
ment();
Następnie musimy poznać pewne podstawowe infor-
macje, takie jak całkowita długość utworu (ilość próbek
PCM albo całkowity czas trwania). Odczytanie długo-
ści utworu dokonujemy w naszej aplikacji w następują-
cy sposób:
odtworzenie pojedynczego
fragmentu pliku
long ogg_tell_pcm_pos(); odczytanie aktualnej pozycji
w utworze
long ogg_full_pcm(); całkowita długość utworu
void ogg_set_new_pos
(int i);
pcm_length=ov_pcm_total(&vf,-1);
total_time=ov_time_total(&vf, -1);
ustawienie nowej pozycji
w utworze (zero początek,
sto koniec)
Istotna informacja, którą wykorzystamy w dalszej części
programu, to aktualna pozycja w odtwarzanym utwo-
rze. Odczytujemy ją albo w postaci próbek PCM albo
czasu trwania, a służą do tego funkcje: ov_pcm_tell oraz
ov_time_tell .
Oczywiście można uzyskać jeszcze kilka dodatko-
wych informacji dzięki strukturze vorbis_info . Dostęp
do tych danych uzyskujemy w następujący sposób:
vorbis_info *vi=ov_info(&vf,-1); . Uzyskanie ilości kana-
łów dźwiękowych, które zawiera określony plik, sprowa-
dza się do skorzystania z informacji zawartych w poszcze-
gólnych polach, np. channels=vi->channels; .
W każdym odtwarzaczu ważną funkcją jest przeszu-
kiwanie utworu. Może się jednak zdarzyć, że określony
plik OGG nie pozwala na ustalenie, od którego fragmen-
tu ma być odtwarzany. Do sprawdzenia, czy plik pozwa-
la na szybkie przeszukiwanie, wykorzystujemy funkcję
ov_seekable :
ustawienie nowej pozycji
w utworze
int ogg_close_play(); zamknięcie pliku
const char *ogg_get_title(); odczytanie tytułu utworu
void ogg_get_time(char
*time_str);
pobranie czasu trwania
utworu
void ogg_get_remain-
der_time(char *tstr);
pobranie czasu pozostałego
do końca utworu
spróbować wysłać próbki do karty dźwiękowej. W przy-
padku wartości ujemnej, jak widać na Listingu 2, prze-
rywamy działanie pętli, a dodatkowo, jeśli nie udało się
zastosować funkcji xrun_recovery , przerywamy działanie
programu. Gdy nie wystąpiła ujemna wartość err , to otrzy-
maliśmy ilość odtworzonych ramek. Wobec tego zwięk-
szamy wskaźnik ptr oraz zmniejszamy zmienną cptr o już
odtworzone ramki.
if(ov_seekable(&vf)){
}
60
kwiecień 2004
int xrun_recovery(snd_
pcm_t *h, int err);
void ogg_set_new_pcm_
pos(long i);
439033900.001.png
dekoder plików ogg
Listing 2. Pętla odtwarzająca pojedynczą próbkę dźwięku
Zajmijmy się parametrami ov_read . Pierwszy para-
metr to uchwyt typu OggVorbis_File . W drugim podaje-
my wskaźnik naszego buforu. Po nim określamy w bajtach
ilość danych, które chcemy zdekodować. Wartość 4096 jest
najlepsza, choć można poeksperymentować z innymi licz-
bami. Pamiętajmy, że muszą to być potęgi liczby dwa. Jest to
dość istotne dla wartości wynikowej, ponieważ dzielimy ją
przez cztery, a ilość danych musi koniecznie być parzysta.
Tajemniczo wyglądają trzy następne cyfry: 0, 2, 1.
Zero oznacza, że zdekodowane dane będą w formacie
little endian” (pierwszy bajt mniej znaczący). Podanie
zamiast zera jedynki oznacza ułożenie danych w formacie
big endian” (pierwszy bajt bardziej znaczący). Następna
liczba (2) oznacza wielkość pojedynczego słowa, a dokład-
niej oznacza słowo szesnastobitowe. Ostatnia liczba doty-
czy znaku. Podanie jedynki oznacza, że dekodowane dane
będą posiadały znak (zero oznacza wartości bez znaku).
Ostatni argument to tzw. numer logicznego strumienia,
ale nas ta wartość nie interesuje, choć w wywołaniu trzeba
ją oczywiście podać. Kod źródłowy funkcji przeznaczonych
do odtwarzania plików w formacie ogg zawiera plik ogg-
play.c – ich spis znajduje się w Tabeli 1.
int tt = 0 , err = 0 , cptr = 0 , current_section = 0 ;
short int ptr ;
/* dekodowanie fragmentu pliku */
tt = ov_read (& vf, sample_buffer, 4096, 0, 2, 1, & current_
section );
cptr = tt / 4 ;
ptr =( short int *) sample_buffer ;
while ( cptr > 0 ) {
err = snd_pcm_writei ( handle, ptr, cptr );
if ( err == - EAGAIN ) continue ;
if ( err < 0 ) {
if ( xrun_recovery ( handle, err ) < 0 ) {
printf ( „Błąd zapisu: %s \ ”, snd_strerror ( err ));
exit ( EXIT_FAILURE );
}
break ;
}
ptr += err * channels ;
cptr -= err ;
}
Czas aktualny i czas pozostały do
końca
Listing 3 zawiera funkcję realizującą odczyt czasu pod-
czas odtwarzania utworu ( ogg_get_time ) oraz wyznacza-
jąca pozostały czas ( ogg_get_remainder_time ) do końca
utworu.
Zajmijmy się pierwszą z tych funkcji. Na początku
wykonujemy odczytanie czasu trwania funkcją ov_time_
tell . Otrzymujemy wartość rzeczywistą, z której trzeba „wy-
ciągnąć” minuty oraz sekundy. Wyznaczenie liczby minut
jest następujące: long min = (long) curr_time / (long) 60;
– dzielimy czas przez wartość 60. Naszym zamiarem jest
uzyskanie całkowitej wartości zamiast np. 4,823. O ile cyfra
4 to z pewnością liczba minut, to pozostała część po prze-
cinku istotnie reprezentuje sekundy i jest to w przybliżeniu
82% minuty. Z tego powodu wykorzystujemy konwersję do
Po sprawdzeniu, czy możemy szybko się przenieść w inną
część utworu, sam proces przeniesienia jest zaskakująco
prosty i dla ramek PCM realizuje to poniższa funkcja:
void ogg_set_new_pos(int i){
double new_pos=(i/100.0) * pcm_length;
ov_pcm_seek(&vf, (long)new_pos);
}
Ustalenie pozycji to wywołanie funkcji ov_pcm_seek , jednak
wcześniejsze obliczenie nowej pozycji to także proste zada-
nie. Zakładamy, że nową pozycją będzie liczba od zera
do stu. Całkowita długość utworu jest zawarta w zmien-
nej pcm_length , więc mnożąc ją przez ułamek i/100.0 prze-
mieszczamy się proporcjonalnie po całym utworze – dla
100 powędrujemy na sam koniec, dla 50 mniej więcej na
środek, a dla i równego zero (0/100) na początek.
Jeśli ktoś przypuszcza, że proces dekodowania dźwięku
z poziomu API jest skomplikowany, to z pewnością ucieszy
się, gdy powiem, że sprowadza się do jednej funkcji -- całą
robotę „odwala” funkcja ov_read .
Dekodowanie pojedynczego fragmentu przedstawia
poniższa linia kodu:
Listing 3. Funkcje odczytujące czas
void ogg_get_time ( char * time_str ) {
double curr_time = ov_time_tell (& vf );
long min = ( long ) curr_time / ( long ) 60 ;
double sec = curr_time - 60.0f * min ;
sprintf ( time_str, „%02li:%05.2f”, min, sec );
}
void ogg_get_remainder_time ( char * time_str ) {
double curr_time = ov_time_tell (& vf );
curr_time = total_time – curr_time ;
long min = ( long ) curr_time / ( long ) 60 ;
double sec = curr_time - 60.0f * min ;
sprintf ( time_str, „%02li:%05.2f”, min, sec );
}
tt=ov_read(&vf, sample_buffer, 4096, 0, 2, 1, &current_
section);
W zmiennej tt otrzymamy ilość bajtów, która została zde-
kodowana. Wykorzystujemy ją także do wykrycia końca
utworu – w takim przypadku zmienna będzie zawierać
wartość 0.
www.linux.com.pl
61
439033900.002.png
dla programistów
Rysunek 2. Schemat budowy odtwarzacza plików ogg
typu całkowitego long , otrzymując wartość całkowitą. Pozo-
stałe sekundy wyznaczamy w następujący sposób: double
sec = curr_time - 60.0f * min; – od czasu odejmujemy
całkowitą ilość minut. Pozostała cześć to właśnie brakujące
sekundy. Oznacza to, że funkcja ogg_get_time zwraca war-
tości w sekundach. Ostatnim krokiem do uzyskania czasu
w postaci odpowiedniego ciągu znaków jest zastosowanie
funkcji sprintf .
Następna funkcja, która pozwoli naszej prostej aplika-
cji nadać bardziej „rasowy” wygląd, to get_remainder_time .
Wyznacza ona czas, który pozostał do końca utworu. Od
poprzedniej różni się tylko jedną linią kodu: curr_time-
=total_time – curr_time; . Zmienna total_time reprezentu-
je całkowitą długość utworu. Jeśli od tej wielkości odejmie-
my aktualny czas, czyli pozycję w odtwarzanym utworze,
to otrzymamy pozostały czas do końca utworu. Z otrzyma-
62
kwiecień 2004
439033900.003.png 439033900.004.png 439033900.005.png 439033900.006.png 439033900.007.png 439033900.008.png 439033900.009.png 439033900.010.png 439033900.011.png 439033900.012.png 439033900.013.png 439033900.014.png
Zgłoś jeśli naruszono regulamin