2006.01_Koder plików w formacie OGG_[Programowanie].pdf

(722 KB) Pobierz
439161176 UNPDF
dla programistów
Koder plików
w formacie OGG
Marek Sawerwain
już wiele razy udowod-
niliśmy, że niewielkim
nakładem pracy można
napisać interesujący program, który będzie
przydatny w codziennej pracy. Zrobimy to
jeszcze raz, pisząc program, który będzie
kompresował pliki audio wav do formatu
ogg . Będzie on rozmiarów nie większych
niż 8 kB tekstu źródłowego, ale pomimo
tego będzie bardzo skuteczny – sam go
używam do tworzenia oggów z nieskom-
presowanych wavów (działa wyśmieni-
cie).
przypadku ABR określamy wartość bitra-
te'u: w przedziale od wartości 64 do 320
kbit/s.
Obsługa naszego programu będzie
dość prosta, aby nie powiedzieć, trywial-
na. Pozwolimy na podanie nazwy pliku
źródłowego, a program samodzielnie za-
proponuje nazwą wynikową, co będzie
polegać na dopisaniu do nazwy dodat-
kowego rozszerzenia .ogg . Następnie
możemy rozpocząć proces kompresji.
Postęp w tym procesie będzie wyświe-
tlany za pomocą paska postępu – widget
o typie GtkProgressBar . Oczywiście,
przed przystąpieniem do kompresji mo-
żemy zmienić parametry związane z tym
zadaniem.
Centralnym elementem w naszym
programie, jak łatwo zgadnąć, będzie pro-
cedura kodowania, czyli kompresja da-
nych audio do formatu ogg . Ogólnie, pro-
ces ten sprowadza się do trzech kroków.
W pierwszej kolejności odczytujemy dane
z pliku, np. w formacie wav . Po odczy-
taniu fragmentu danych z pliku audio
dokonujemy ich konwersji na typ loat ,
gdyż biblioteka Vorbis posługuje się licz-
bami loat do reprezentacji danych audio.
Gdy dokonamy konwersji, przystępuje-
my do kompresji i zapisu danych do pliku
docelowego.
Najważniejsze zdarzenia w progra-
mie, które przed chwilą wymieniłem,
warto przedstawić w postaci schematu
– Rysunek 1.
Nasz program będzie pracował w śro-
dowisku GNOME, co oznacza, że napi-
szemy go przy wykorzystaniu progra-
mu GLADE oraz biblioteki GTK+ (bę-
dziemy korzystać z ostatnich wersji sta-
bilnych, czyli GTK+ w wersji 2.8.X bądź
2.6.X). Będziemy jeszcze potrzebowali
odpowiednich narzędzi do obsługi plików
audio. Do realizacji odczytu plików znako-
micie nadaje się biblioteka Audioile , nato-
miast do samego kodowania niezbędny
jest zestaw bibliotek Vorbis .
Założenia do programu
Zakładamy, że będziemy kompresować
pliki zawierające dwa kanały audio (stereo)
o próbkach 16-bitowych. Niewątpliwie,
jest to najbardziej rozpowszechniony stan-
dard zapisu sygnału audio. Ponadto, użyt-
kownik będzie miał do wyboru dwa pod-
stawowe tryby kompresji: VBR (zmien-
ny bitrate) oraz ABR (uśredniony bitra-
te). Oprócz tych dwóch podstawowych
rodzajów pracy, w przypadku trybu VBR
możemy określić jakość kompresji: od zera
(najgorsza) do dziewięciu (najlepsza), a w
Na płycie CD/DVD
Na płycie CD/DVD znajdują się
wykorzystywane biblioteki oraz
kod źródłowy programu.
Pierwsze czynności
w programie
Proces projektowania interfejsu odbywa
się za pomocą myszki, więc nie warto go
66 styczeń 2006
W dziale Dla Programistów
439161176.047.png 439161176.052.png 439161176.053.png
 
439161176.001.png 439161176.002.png 439161176.003.png 439161176.004.png 439161176.005.png 439161176.006.png 439161176.007.png
 
C/C++/Vorbis
dla programistów
Dodatkowe biblioteki
i kompilacja programu
Ponieważ środowisko GNOME, podob-
nie jak KDE, stało się standardowym ele-
mentem wielu dystrybucji Linuksa, więc
z pewnością w systemie używanym przez
Czytelniczkę/Czytelnika, GNOME jest
dostępne. Podobnie jest w przypadku pa-
kietu Vorbis. Biblioteka ta jest dostęp-
na w większości dystrybucji, przy czym
pakiet Vorbis to dwie zasadnicze biblio-
teki: libogg (zarządzanie strumieniem)
oraz libvorbis (właściwa biblioteka zaj-
mująca się kompresją). Z tego powodu
wystarczy upewnić się, czy odpowiednie
pakiety binarne oraz pakiety dla dewelo-
perów zostały zainstalowane w systemie
– nie musimy instalować oraz kompilować
samodzielnie dodatkowych bibliotek.
Proces kompilacji naszego progra-
mu nie jest skomplikowany, gdyż wystar-
czy utworzyć prosty plik makeile o nastę-
pującej postaci:
S
src `pkg-conig --clags gtk+-2.0
S
S
src `pkg-conig --clags gtk+-2.0
libgnomeui-2.0` -I./include
S
src `pkg-conig --clags S
gtk+-2.0 libgnomeui-2.0
S
audioile vorbis vorbisenc`
S
S
conig --libs gthread-2.0 S
gtk+-2.0 libgnomeui-2.0
S
audioile vorbis vorbisenc`
Rysunek 1. Schemat najważniejszych zdarzeń występujących w programie
dokładnie omawiać. Utworzone okno dia-
logowe możemy załadować do progra-
mu na dwa sposoby. Pierwszy polega na
tym, iż interfejs jest zapisywany do pliku
glade , a do programu jest on wczytywa-
ny dynamicznie. Drugi sposób to wyge-
nerowanie plików źródłowych, co spowo-
duje, że interfejs jest tworzony statycznie
(każda jego zmiana wymaga rekompilacji
programu), ale z drugiej strony, program
nie potrzebuje dodatkowych plików do
poprawnego funkcjonowania. My wybie-
rzemy tę drugą metodę.
Wygenerowanie kodu nastąpi, gdy
w GLADE wybierzemy przycisk Zbuduj
( Build w wersji angielskiej), przy czym
nas interesują tylko pliki interface.c i inter-
face.h (w nich znajduje się kod tworzą-
cy interfejs) oraz pliki support.c i support.h
(zawierają one pomocnicze funkcje, takie
jak np. bardzo przydatną lookup_widget ).
Pozostałe pliki możemy zignorować.
Chociaż mogą przydać się pliki callback.h
i callback.c , to obsługę poszczególnych
funkcji odpowiadających na sygnały umie-
ścimy bezpośrednio w pliku, w którym
znajduje się funkcja main .
Pierwsza czynność w funkcji main to
inicjalizacja biblioteki GTK+: gtk_init
(&argc, &argv); , natomiast bezpośrednio
po tym następuje utworzenie okna głów-
nego: MainWin=create_MainWin(); .
Funkcja tworząca okno została wyge-
nerowana przez GLADE, a jej implemen-
Pierwsze dwie linie są odpowiedzialne
za kompilację interfejsu i dodatkowych
funkcji pomocniczych. Właściwy program
jest kompilowany za pomocą trzeciej
linii. Budowa pliku binarnego odbywa się
w ostatniej linii skryptu.
tacja (osobom, które zaczynają poznawać
GTK+, warto polecić analizę tej funkcji,
aby zobaczyli, w jaki sposób jest budo-
wany interfejs użytkownika) znajduje się
w pliku interface.c . Ponieważ w kodzie
programu będziemy odwoływać się bez-
pośrednio do niektórych kontrolek, to
za pomocą funkcji lookup_widget uzy-
skamy odniesienia do potrzebnych nam
widgetów:
www.lpmagazine.org
67
all:
gcc -c src/interface.c -I./
libgnomeui-2.0` -I./include
gcc -c src/support.c -I./
S
gcc -c ogg_encoder.c -I./
-I./include
gcc -o ogg_encoder ogg_encoder.o
interface.o support.o `pkg-
439161176.008.png 439161176.009.png 439161176.010.png 439161176.011.png 439161176.012.png 439161176.013.png 439161176.014.png 439161176.015.png 439161176.016.png 439161176.017.png 439161176.018.png 439161176.019.png 439161176.020.png 439161176.021.png
dla programistów
Listing 1. Wybór pliku, który ma zostać poddany kompresji
krótko zaprezentować najważniejsze zda-
rzenia, które pojawiają się w kontekście
obsługi interfejsu. Na Rysunku 2 widzi-
my nasz program. Znajduje się tam przy-
cisk z trzema kropkami. To za jego pomocą
wybieramy plik, który ma zostać poddany
kompresji. Gdy użytkownik kliknie na
ten przycisk, zostanie wywołana funkcja
o nazwie on_FileSelBTN_clicked . Jej pełna
treść została przedstawiona na Listingu 1.
Wykonujemy w niej dwie czynności – two-
rzymy okno dialogowe za pomocą funk-
cji gtk_ile_chooser_dialog_new , a następ-
nie nakazujemy wyświetlenie tego okna
funkcją gtk_dialog_run . Jeśli użytkownik
wybrał plik i potwierdził jego wybór, np.
klikając przycisk OK , to wynikiem działa-
nia funkcji gtk_dialog_run będzie wartość
GTK_RESPONSE_ACCEPT , czyli zostanie wyko-
nana treść instrukcji warunkowej. Nazwę
pliku, który wybrał użytkownik, odczy-
tujemy za pomocą funkcji gtk_ile_cho-
oser_get_ilename . Wpisujemy ją do pola
tekstowego o nazwie FileToEncodeEntry .
Do drugiego pola, o nieco długiej nazwie
FinalNameEncodedFileEntry , wpisuje-
my nazwę pliku wynikowego. Wcześniej,
za pomocą kilku instrukcji strcpy oraz
strcat , dopisujemy do oryginalnej nazwy
rozszerzenie .ogg . W ten sposób w obydwu
polach tekstowych mamy poprawnie wpi-
sane nazwy plików.
To nie wszystkie czynności związane
z poprawną obsługą interfejsu. Gdy użyt-
kownik naciśnie przycisk OK , to zamknięta
void on_FileSelBTN_clicked ( GtkButton * button , gpointer user_data ) {
GtkWidget * dialog ;
dialog = gtk_ile_chooser_dialog_new ( "Open File" ,
MainWin ,
GTK_FILE_CHOOSER_ACTION_OPEN ,
GTK_STOCK_CANCEL , GTK_RESPONSE_CANCEL ,
GTK_STOCK_OPEN , GTK_RESPONSE_ACCEPT ,
NULL );
if ( gtk_dialog_run ( GTK_DIALOG ( dialog )) == GTK_RESPONSE_ACCEPT ) {
char * ilename , newile [ 1024 ];
ilename = gtk_ile_chooser_get_ilename ( GTK_FILE_CHOOSER ( dialog ));
gtk_entry_set_text ( GTK_ENTRY ( FileToEncodeEntry ) , ilename );
strcpy (& ile_in [ 0 ] , ilename );
strcpy (& newile [ 0 ] , ilename );
strcat (& newile [ 0 ] , ".ogg" );
strcpy (& ile_out [ 0 ] , newile );
gtk_entry_set_text ( GTK_ENTRY ( FinalNameEncodedFileEntry ) , & newile [ 0 ]);
g_free ( ilename );
}
gtk_widget_destroy ( dialog );
}
S
(MainWin, "EndWorkBTN");
S
"delete_event", G_CALLBACK
S
(gtk_main_quit), NULL);
Po odczytaniu widgetów, wystarczy tylko
wykonać trzy linijki kodu, który prezentu-
ją się następująco:
Obsługa interfejsu
Zanim przejdziemy do opisu, w jaki
sposób kompresujemy dane, warto jeszcze
S
(GTK_PROGRESS_BAR(ProgressBar),
S
".: OGG Mini Encoder V1.0 :.");
gtk_widget_show_all(MainWin);
gtk_main();
W pierwszej linii kodu w pasku postę-
pu umieszczamy komunikat. Druga li-
nia kodu spowoduje, że w oknie zostaną
wyświetlone wszystkie widgety. W ostat-
niej linii uruchamiamy główną pętlę
biblioteki GTK+, co powoduje, że nasza
aplikacja zacznie działać.
Na końcu warto jeszcze wspomnieć
o podłączeniu obsługi sygnału delete_
event . Jest on generowany podczas klik-
nięcia na przycisk zamykający okno, przy
czym nie jest to przycisk OK , ale przycisk
menedżera okien. W takim przypad-
ku generowane jest wspomniane zda-
rzenie. Najłatwiej jest podłączyć wy-
wołanie funkcji gtk_main_quit , która
spowoduje natychmiastowe zamknięcie
okna:
Rysunek 2. Program OGG Encoder V1.0
68 styczeń 2006
EndWorkBTN=lookup_widget
g_signal_connect (MainWin,
gtk_progress_bar_set_text
439161176.022.png 439161176.023.png 439161176.024.png
 
439161176.025.png 439161176.026.png 439161176.027.png 439161176.028.png 439161176.029.png 439161176.030.png 439161176.031.png
 
C/C++/Vorbis
dla programistów
zostanie cała aplikacja. W tej chwili inte-
resuje nas obsługa czterech widgetów
związanych z parametrami kompresji.
Pierwszym ważnym elementem jest
wybór trybu kompresji: ABR bądź VBR.
Odpowiedzialne są za to dwa przełączni-
ki. Wybranie opcji ABR powoduje wywo-
łanie funkcji on_ABR_RadioBTN_toggled ,
która wykonuje dwa trywialne przypi-
sania: vbr_sel=0, abr_sel=1; . Podobne
przypisania są wykonywane, gdy użyt-
kownik wybierze tryb VBR: vbr_sel=1,
abr_sel=0; . Za pomocą następnego wid-
getu, BitrateComboBox , wybieramy szero-
kość strumienia dla trybu ABR. Jak widać
na Rysunku 2, wartością domyślną jest
wartość 192 kbit/s. W przypadku, gdy
zdecydujemy się na tryb VBR, możemy
za pomocą suwaka, który reprezentuje
widget QualityWidget , ustalić jakość kom-
presji. Wartość zero wskazuje najniższą
możliwą jakość, natomiast wartość dzie-
więć – najwyższą. W obydwu przypad-
kach nie wykorzystujemy zdarzeń, gdyż
po wybraniu przycisku Encode , funk-
cja, którą za chwilę szerzej przedstawię,
odczytuje potrzebne dane bezpośrednio ze
wspomnianych widgetów.
Listing 2. Sprawdzanie, czy plik ma odpowiedni format
if ( channelCount != 2 && sampleWidth != 16 && sampleFormat != AF_SAMPFMT_TWOSCOMP )
{
afCloseFile ( ile );
dialog = gtk_message_dialog_new ( NULL ,
GTK_DIALOG_MODAL ,
GTK_MESSAGE_ERROR ,
GTK_BUTTONS_CLOSE ,
"Sorry, the selected ile hasn't proper format! Only two channels and
16 bits sample iles are allowed." );
gtk_dialog_run ( GTK_DIALOG ( dialog ));
gtk_widget_destroy ( dialog );
return ;
}
unt ) czy częstotliwość próbkowania ( rate ).
Wymienione dwie przykładowe wielkości
otrzymamy w następujący sposób:
16-bitowej rozdzielczości. Listing 2 przed-
stawia fragment programu, w którym za
pomocą instrukcji warunkowej spraw-
dzamy, czy otwarty plik spełnia nasze
wcześniejsze założenia. Jeśli tak nie jest,
to zamykamy go funkcją afCloseFile .
Następnie wyświetlamy okno dialogo-
we z odpowiednim komunikatem, infor-
mując użytkownika, że plik zawiera dane
w nieobsługiwanym formacie. Zwróćmy
też uwagę na to, iż wykorzystując biblio-
tekę Audioile , nie musimy nawet wiedzieć,
czy ten plik był w formacie wav, czy w do-
wolnym innym – wystarczy, aby był
to plik, w którym dane audio zapisa-
no w obsługiwanym przez nas formacie
danych.
Ostatnią ważną czynnością jest utwo-
rzenie pliku oraz przydzielenie pamięci
dla bufora:
S
Channels(ile, AF_DEFAULT_TRACK);
rate=afGetRate(ile, AF_DEFAULT_TRACK);
Otrzymane w ten sposób informacje są
dla nas bardzo ważne. Ponieważ biblio-
teka OGG wymaga dostarczenia danych
w postaci liczb zmiennoprzecinkowych
typu loat , to musimy uzyskać informacje,
z jakim typem danych mamy do czynie-
nia, aby poprawnie dokonać odpowiedniej
konwersji danych.
W naszej dość elementarnej aplika-
cji dla uproszczenia będziemy zajmo-
wać się tylko plikami o dwóch kanałach,
w których znajdują się liczby całkowite o
Przygotowania
do kompresji
Przed właściwą kompresją danych należy
otworzyć plik, z którego będziemy odczy-
tywać dane. Ponieważ stosujemy biblio-
tekę Audioile , to operacja otworzenia pli-
ku jest wykonywana za pomocą funkcji
afOpenFile :
ile = afOpenFile(&ile_in[0],
"r", NULL);
Listing 3. Utworzenie nagłówków pliku OGG
W programie sprawdzamy również, czy
istotnie udało się otworzyć plik, spraw-
dzając, czy zmienna ile posiada wartość
równą AF_NULL_FILEHANDLE . Następnie,
jeśli bez kłopotów otworzyliśmy plik,
odczytujemy podstawowe informacje,
które są niezbędne w trakcie kompresji.
Istotną informacją dla nas jest ilość
ramek w pliku:
ogg_packet header ;
ogg_packet header_comm ;
ogg_packet header_code ;
srand ( time ( NULL ));
ogg_stream_init (& os , rand ());
vorbis_analysis_headerout (& vd , & vc , & header , & header_comm , & header_code );
ogg_stream_packetin (& os , & header );
ogg_stream_packetin (& os , & header_comm );
ogg_stream_packetin (& os , & header_code );
frameCount = afGetFrameCount
S
(ile, AF_DEFAULT_TRACK);
while (! eos ) {
result = ogg_stream_lush (& os , & og );
if ( result == 0 ) break ;
fwrite ( og . header , 1 , og . header_len , f_out );
fwrite ( og . body , 1 , og . body_len , f_out );
}
Jest nam ona potrzebna, aby podczas wła-
ściwej kompresji pasek statusu wskazywał
poprawny stan zaawansowania tego pro-
cesu. Potrzebne są nam także inne informa-
cje, takie jak np. ilość kanałów ( channelCo-
www.lpmagazine.org
69
channelCount = afGetVirtual
439161176.032.png 439161176.033.png 439161176.034.png 439161176.035.png 439161176.036.png 439161176.037.png 439161176.038.png 439161176.039.png 439161176.040.png
dla programistów
Listing 4. Pętla, która dokonuje kompresji danych audio do formatu OGG
S
(&vi, 2, 44100, -1, 128000, -1);
while (! eos ) {
framesRead = afReadFrames ( ile , AF_DEFAULT_TRACK , buffer , BUFFER_FRAMES );
frames += framesRead ;
if ( framesRead == 0 ) {
vorbis_analysis_wrote (& vd , 0 );
} else {
vorbis_buffer = vorbis_analysis_buffer (& vd , BUFFER_FRAMES );
for ( i = 0 ; i < framesRead ; i ++) {
vorbis_buffer [ 0 ][ i ]=((*( buffer + i * 4 + 1 )<< 8 ) |
Dla trybu VBR stosujemy inną funkcję:
S
vbr(&vi, 2, 44100, 0.5);
S
w której ostatni parametr o wartości 0.5
oznacza jakość kompresji. Oczywiście,
tak jak wcześniej wspomniałem, wielkość
bitrate'u i jakość kompresji odczytujemy
z odpowiednich kontrolek.
Warto do pliku inalnego dodać ko-
mentarz, w którym umieścimy nazwę na-
szego programu, odpowiedzialnego za
utworzenie pliku OGG:
( 0x00ff &( int )*( buffer + i * 4 ))) / 32768.f ;
vorbis_buffer [ 1 ][ i ]=((*( buffer + i * 4 + 3 )<< 8 ) |
S
( 0x00ff &( int )*( buffer + i * 4 + 2 ))) / 32768.f ;
}
vorbis_analysis_wrote (& vd , framesRead );
}
while ( vorbis_analysis_blockout (& vd , & vb )== 1 ) {
vorbis_analysis (& vb , NULL );
vorbis_bitrate_addblock (& vb );
while ( vorbis_bitrate_lushpacket (& vd , & op ))
{
ogg_stream_packetin (& os , & op );
while (! eos ) {
int result = ogg_stream_pageout (& os , & og );
if ( result == 0 ) break ;
fwrite ( og . header , 1 , og . header_len , f_out );
fwrite ( og . body , 1 , og . body_len , f_out );
if ( ogg_page_eos (& og )) eos = 1 ;
}
}
}
S
"ENCODER","Mini-encoder V1.0");
Następne elementy, które musimy opro-
gramować, stanowią bezpośredni wstęp
do właściwej kompresji. Tworzymy zmien-
ne, które są odpowiedzialne za analizę
danych oraz za bufor danych:
vorbis_analysis_init(&vd, &vi);
vorbis_block_init(&vd, &vb);
Po poprawnej inicjalizacji tych zmien-
nych wykonywany jest kod przedstawio-
ny na Listing 3. Jego zadaniem jest umie-
szczenie w pliku wyjściowym f_out
trzech nagłówków. W pierwszym (zmien-
na header) znajdują się podstawowe infor-
macje o parametrach kompresji. Drugi
(zmienna header_comm ) zawiera dodatkowe
informacje tekstowe, takie jak np. komen-
tarz, a trzeci poprzedza właściwy strumień
danych poddanych kompresji.
Dane do pliku są zapisywane za po-
mocą funkcji fwrite , ale właściwe dane
są przygotowywane przed pętlą while .
W samej pętli za pomocą funkcji ogg_
stream_lush staramy się przepisywać dane
do zmiennej og . Pętla while , jak widać
na Listingu 3, będzie działać tak długo,
aż funkcja ogg_stream_lush zwróci war-
tość zero.
gtk_progress_bar_update ( GTK_PROGRESS_BAR ( ProgressBar ) ,
S
( loat ) frames / frameCount );
while ( gtk_events_pending ()) gtk_main_iteration ();
}
S
(BUFFER_FRAMES * frameSize);
padek kliknąć na któryś z przycisków,
a proces kompresji byłby aktywny.
Krok pierwszy
Przed wejściem do głównej pętli, w której
odbywa się kompresja, należy wykonać
kilka czynności wstępnych. Przede wszys-
tkim, należy zainicjalizować podstawową
zmienną o krótkiej nazwie vi w nastę-
pujący sposób: vorbis_info_init(&vi); .
Pozostałe zmienne zawsze wykorzystu-
ją zmienną vi , bezpośrednio bądź też
w pośredni sposób.
Drugim krokiem jest określenie para-
metrów kompresji, tzn. czy będziemy kodo-
wać plik OGG w trybie VBR, czy też w try-
bie ABR. Jeśli użytkownik wskazał ten dru-
gi tryb, to przykładowe wywołanie funkcji
narzucającej ten tryb kompresji dla bitrate'u
równego 128 kbit/s jest następujące:
Zanim ostatecznie zajmiemy się kom-
presją, to przyciski Encode i OK oraz
przycisk, którym wybieramy plik, war-
to uczynić nieaktywnymi. Zrobimy to
za pomocą trzech następujących linii
kodu:
S
(GTK_WIDGET(EndWorkBTN), FALSE);
gtk_widget_set_sensitive
S
(GTK_WIDGET(EncodeBTN), FALSE);
gtk_widget_set_sensitive
Pętla główna kompresji
Dotarliśmy do momentu, w którym jeste-
śmy gotowi do przeprowadzenia procesu
kompresji. Za to zadanie jest odpowiedzial-
na pętla z Listingu 4. W kodzie możemy
wyróżnić cztery podstawowe kroki: wczy-
tanie do zmiennej buffer odpowiedniej ilo-
S
(GTK_WIDGET(FileSelBTN), FALSE);
W ten sposób eliminujemy sytuację,
w której użytkownik mógłby przez przy-
70 styczeń 2006
ret = vorbis_encode_init
ret = vorbis_encode_init_
vorbis_comment_init(&vc);
vorbis_comment_add_tag(&vc,
f_out=fopen(&ile_out[0],"w");
buffer = (signed char*)malloc
gtk_widget_set_sensitive
439161176.041.png 439161176.042.png 439161176.043.png
 
439161176.044.png 439161176.045.png 439161176.046.png 439161176.048.png 439161176.049.png 439161176.050.png 439161176.051.png
 
Zgłoś jeśli naruszono regulamin