Programowanie Gier -> Wykład: Dźwięk 3D - OpenAL
PomocSpisTreści
Główna strona OpenALa ze specyfikacjami, linkami do skompilowanych bibliotek, i wieloma innymi informacjami.
1. Ogólnie
Biblioteka do łatwego odtwarzania dźwięków w przestrzeni 3D. My mówimy na jakiej pozycji 3D gra jaki dźwięk, a OpenAL sam robi resztę — oblicza jak głośny jest dźwięk (dźwięki dalsze są cichsze), "renderuje" (czyli gra) dźwięk na głośnikach tak żeby wykorzystać stereo (albo i lepsze ustawienia głośników) do symulowania np. tego że dźwięk dobiega z lewej strony. I robi różne inne efekty po drodze.
Otwarta specyfikacja (PDF), open-source'owe implementacje dostępne pod wszystkie systemy operacyjne. API zainspirowane OpenGLem (wszystkie polecenia zaczynają się od al, mamy stan i podobne pojęcia).
Krótka historia:
Zapoczątkowana przez Loki Software (wspaniałą firmę która zaczęła także SDLa i niestety już nie istnieje). Loki stworzyła pierwszą implementację OpenALa, software'ową (mogła renderować dźwięki poprzez OSS, ALSA, kilka innych, także cross-platform SDL, a nawet do pliku WAV).
Kiedy Loki upadło, oficjalną pieczę nad openal.org przejęło Creative, udostępniając też własną implementację OpenAL tylko pod Windows. Apple miał też własną implementację pod Mac OS (potem Mac OS X). Przez długi czas istniały trzy zupełnie niezależne (chociaż hostowane na tym samym serwerze) popularne implementacje OpenAL (Creative, Loki, i Apple). Creative przez jakiś czas nie robiło zbyt wiele IMHO, także zespół ochotników pracujących nad implementacją Loki leniuchował. Implementacja Loki, choć teoretycznie cross-platform, w praktyce była używana tylko pod Unixami (Linux, FreeBSD etc.), ponieważ 1. pod Windowsem implementacja Creative dobierała się do dźwięku bez (niepotrzebnego) pośrednictwa SDLa, a 2. pod Mac OS (X) Apple dostarczał swoją niezłą implementację która miała kilka specyficznych rozszerzeń pozwalających wykorzystać hardware i inne biblioteki na komputerach Apple.
Po małym okresie stagnacji nastąpił OpenAL 1.1, który trochę odświeżył i załatał wiele niepewności istniejących w starej specyfikacji.
Potem powstała implementacja OpenAL Soft, która jest aktywnie rozwijana (implementacja Loki zakurzyła się tak strasznie przez lata że prościej było ją porzucić) i moim zdaniem trochę odświeżyła status OpenALa, także prowokując pozostałe implementacje do bardziej aktywnego rozwoju. OpenAL Soft jest obecnie popularnie używany na Unixach (chociaż jest cross-platform, zazwyczaj pod Windowsem/Mac OS X są ciągle używane implementacje Creative/Apple). Oryginalna implementacja Loki w zasadzie jest deprecated, nie wygląda na to aby był ktoś zainteresowany poprawieniem jej (np. żeby nie używała ALSA'y w trybie blokującym) ani rozwinięciem (np. żeby obsługiwała pełną specyfikację 1.1 albo nowe rozszerzenia).
2. Podstawowe pojęcia OpenALa
Kontekst — urządzenie, karta dźwiękowa, a czasem także konkretna implementacja.
Np. w przypadku standardowej implementacji OpenAL pod Windowsem przez Creative, jeżeli nie mamy żadnej specjalnej karty dźwiękowej, mamy dwa urządzenia: Generic Hardware oznacza że używana jest implementacja poprzez DirectSound3D (czyli możemy mieć wsparcie hardware'owe jeżeli DirectSound3D jest wspierany hardware'owo) a Generic Software używa DirectSound albo MMSystem (proste systemy produkujące audio stereo pod Windowsem, całe spatialization musi być robione przez OpenAL czyli w softwarze na CPU). Pod Linuxem mamy m.in. ALSA (chyba najpopularniejszy obecnie sposób "dobrania" się do karty dźwiękowej), OSS (stary sposób), PulseAudio, i inne.
W typowej grze po prostu tworzymy kontekst na początku programu, i zamykamy go na końcu. Polecenia OpenALa alcXxx służą do zarządzania kontekstami (alc = OpenAL Context API). Polecenia alXxx działają zawsze w ramach aktywnego kontekstu. Czyli podobnie jak z kontekstami OpenGLa — tylko że tutaj API do tworzenia kontekstów (alcXxx) też jest cross-platform i zawarte w specyfikacji.
Trywialne tworzenie / zamykanie kontekstu:
alutInit(NULL, 0); ... alutExit();
ALUT to biblioteka implementująca trochę pomocniczych funkcji do OpenALa, używająca na spodzie OpenAL (funckji alXxx i alcXxx). Daje także proste ładowanie dźwięków WAV. Kiedyś była zawarta w OpenALu, i generalnie była małym hackiem o którego nikt się specjalnie nie troszczył. Ale obecnie jest osobną biblioteką o którą ktoś aktywnie dba i ma swoją własną elegancką specyfikację (PDF).
Można też zrobić to bardziej ręcznie, bez aluta, nie jest to zbyt trudne:
device := alcOpenDevice(nil); context := alcCreateContext(device, nil); alcMakeContextCurrent(audio_context); .... alcMakeContextCurrent(nil); { note: don't do this for old Loki implementation (alGetString(AL_VENDOR) = "J. Valenzuela"), may hang for a couple of minutes } alcDestroyContext(context); alcCloseDevice(device);
Można pobrać od OpenALa listę dostępnych devices poprzez alcGetString(nil, ALC_DEVICE_SPECIFIER), patrz specyfikacja ALC_ENUMERATION_EXT. W OpenAL >= 1.1 obecność ALC_ENUMERATION_EXT jest gwarantowana. Uzyskaną tak nazwę urządzenia możemy przekazać jako parametr do alutInit albo alcOpenDevice, wartość nil (=NULL) oznacza że pozwalamy OpenALowi wybrać najlepsze, jego zdaniem, urządzenie.
Listener — to my, tzn. nasz gracz w świecie 3D którego uszami słuchamy. Jest zawsze jeden i dokładnie jeden listener w naszym świecie 3D.
Listener ma kilka atrybutów, przede wszystkim pozycję i orientację w świecie 3D (kluczowe żeby robić należyte spatialization). Listener ma też atrybut GAIN, który odpowiada za globalną "głośność" dźwięku w grze (skaluje głośność wszystkich dźwięków).
Operacje na listenerze zapisujemy trywialnie:
ALfloat position[3]; alListenerfv(AL_POSITION, &position); alListenerf(AL_GAIN, 0.5); { default 1, normalne wartości są pomiędzy 0..1, chociaż można też podawać większe wartości. Implementacja może na końcu zrobić clamp obliczonej ostatecznej głośności do <0, 1> (zupełnie jak komponenty kolorów podawane jako floaty dla OpenGLa). }
Source — źródło dźwięku. W teorii, samo API pozwala na stworzenie dowolnie wielu źródeł dźwięku. W praktyce, implementacja OpenALa (zwłaszcza hardware'owa) może ograniczać ilość źródeł dźwięku (dlatego powiemy sobie później kilka słów o "menedżerze źródeł dźwięków").
Proste użycie źródła dźwięku:
ALuint Source; alGenSources(1, &Source); alSourcei (Source, AL_BUFFER, Buffer ); { wskazuje faktyczną zawartość dźwięku, za chwilę } alSourcef (Source, AL_GAIN, 0.5 ); { każdy dźwiek ma swoją indywidualną głośność } alSourcefv(Source, AL_POSITION, &position); alSourcei (Source, AL_LOOPING, AL_TRUE ); alSourcePlay(Source); alSourcePause(Source); alSourcePlay(Source); alSourceStop(Source); alDeleteSources(1, &Source);
Buffer — zawiera faktyczne dane dźwięku. W typowej sytuacji po prostu tworzymy 1 bufor dla każdego pliku dźwiękowego jaki mamy. Możemy tworzyć dowolnie wiele buforów, jedyne ograniczenie tutaj to ilość pamięci — duże dźwięki zajmują naturalnie dużo pamięci, zwłaszcza po rozpakowaniu (dlatego opowiemy sobie później o streaming dźwięków).
Proste użycie bufora dźwięku:
ALuint Buffer; alGenBuffers(1, &Buffer); ALenum format; ALvoid* data; ALsizei size; ALsizei freq; alutLoadWAVFile("sample.wav", &format, &data, &size, &freq, &loop); alBufferData(Buffer, format, data, size, freq); alutUnloadWAV(format, data, size, freq); { bufor kopiuje dane z data, możemy już zwolnić data } { żeby bufor miał sens, musimy "podczepić" go do jakiegoś source, jak wyżej } alSourcei (Source, AL_BUFFER, Buffer); alDeleteBuffers(1, &Buffer); { dla testów: Buffer = alutCreateBufferHelloWorld(); }
Żeby uzyskać maksymalną jakość dźwięku, generalnie dane dźwięku w buforze powinny wykorzystywać maksymalną rozdzielczość używanych liczb (typowo 1 albo 2 bajty na sample (typowo 44100 sampli na sekundę), chociaż można też we floatach mając odpowiednie rozszerzenia OpenALa). Mówiąc po ludzku, oznacza to że nie należy ściszać dźwięków poprzez ściszanie plików. Plik wav z odgłosem skradającej się myszki powinien być tak samo głośny jak plik z odgłosem startu rakiety. Faktyczne ściszanie/podgłaśnianie dźwięków należy robić poprzez GAIN na source OpenALa.
Przykładowy trywialny program odtwarzający dźwięk używając OpenAL.
Demo: tremulous (na bazie ioquake3), może moje castle? Wszystko pod Linuxem z dźwiękiem 3D używa OpenALa!
3. Efekt Dopplera
Kiedy mija nas jadący szybko samochód, wysokość dźwięku (warkotu silnika) rośnie w miarę jak samochód jest coraz bliżej, i maleje kiedy samochód się oddala ("wziuuuum" :) ). To właśnie efekt Dopplera, stosunkowo oczywisty kiedy uświadomimy sobie że dźwięk to fala rozchodząca się w powietrzu. Kiedy źródło dźwięku porusza się względem słuchacza, fale odbierane są z większą lub mniejszą częstotliwością. OpenAL może to symulować.
Komendy OpenALa są tutaj naprawdę trywialne:
Ustaw prędkość ruchu listenera i źródła dźwięku (AL_VELOCITY, vector 3f którego kierunek i długość określają kierunek i szybkość ruchu). OpenAL nigdy nie ustawia sam AL_VELOCITY (np. OpenAL nie oblicza sam velocity na podstawie zmian AL_POSITION; jeżeli w konkretnym zastosowaniu ma to sens, musimy zrobić to sami).
Możemy ustawić także alDopplerFactor (zwykłe skalowanie efektu, aby uczynić go bardziej dramatycznym/subtelnym. Domyślnie jest 1. Ustawienie na 0 wyłącza efekt Dopplera, implementacja OpenALa może to optymalizować), alSpeedOfSound (chyba self-explanatory; domyślna wartość 343.3, = ~faktyczna prędkość dźwięku w powietrzu przy temperaturze około 20 stopni). W starych wersjach OpenAL (przed 1.1) zamiast alSpeedOfSound było alDopplerVelocity, które nie miało zbyt precyzyjnej specyfikacji (było inicjowane też na inne wartości przez różne implementacje), generalnie nie używajcie.
Demo: doppler_demo.
Omówienie na devmaster.net
4. Inne efekty: EFX, EAX
Krótka historia: (Michalis nie jest fanem wykładów o historii czegokolwiek :), ale kilka słów tutaj pomoże nam zorientować się w terminologii)
EAX powstał dawno temu jako zestaw efektów jaki można było przypisać dźwiękom w kartach Creative. Ten "zestaw efektów" był specyficzny dla kart Creative. (Chociaż mógł być później emulowany przez OpenALa Creative pod Windowsem, na dowolnych kartach dźwiękowych, kosztem CPU; chociaż kod źródłowy tej emulacji chyba nigdy nie został opublikowany.) Oryginalnie, Creative dodało kilka rozszerzeń do DirectSound3D aby EAX było dostępne. Później, dodano analogiczne rozszerzenia do OpenALa, tyle że API tych rozszerzeń wyglądało dość koszmarnie — np. nazwy stałych zostały przeniesione żywcem z DirectSound3D. W miarę jak DirectSound3D umiera a OpenAL rośnie, zrozumiano że API EAX trzeba wyrzucić do kosza. Zamiast niego powstało EFX: ładne, czyste API. OpenAL Soft implementuje je, więc jest to też API które działa cross-platform. Czyli, w skróćie: EAX is dead, long live EFX. Piszę o EAX tylko dlatego że możecie jeszcze znaleźć w sieci mnóstwo informacji i wzmianek o EAX.
EFX to eleganckie rozszerzenie alc (ALC_EXT_EFX). Dokumentacja znajduje się w OpenAL SDK dla Windowsa (szczęśliwie instalowalne pod Linuxem przed wine), w Effects Extension Guide.pdf (w docs/). Sama dokumentacja tego rozszerzenia jest dłuższa od specyfikacji OpenALa (144 strony vs 61), więc z konieczności zrobimy sobie tylko krótkie streszczenie poniżej.
EFX dodaje nowe pojęcia do OpenALa:
Auxiliary Effect Slots - bramki które faktycznie wykonują dany efekt, miksując i przetwarzając (wiele) wejściowych dźwięków, i wysyłając wynik do odtwarzania. Do Auxiliary Effect Slots "podczepiamy" wiele wejściowych dźwięków, i jeden efekt.
Effect Object - to po prostu zestaw ustawień efektu, który "doczepiamy" do zadanego Auxiliary Effect Slot. "Effect Object" definiuje zachowanie efektu, ale nie wie jakie dźwięki są pod niego podczepione. Dzięki temu że mamy osobne pojęcia na "Effect Object" i "Auxiliary Effect Slot" możemy stworzyć typowe konfiguracje efektów (np. dla różnie rezonujących typów pomieszczeń — hangar, łazienka, etc.), a następnie swobodnie podczepiać je pod różne Auxiliary Effect Slots (w miarę jak gracz porusza się w świecie 3D pomiędzy odpowiednimi pomieszczeniami).
Filtr - zwyczajny filtr, tzn. przetwarza pojedynczy dźwięk. Normalne dźwięki OpenALa przechodzą przez taki filtr (po drodze do ostatecznego wyniku (dry path) albo po drodze do Auxiliary Effect Slot (wet path)).
Dema: z SDK: EFXEnumerateWin32.exe, EFXReverbWin32.exe, moje proste efx_demo.
Notka: OpenAL Soft obsługuje obecnie tylko najbardziej bazowe funkcje: tylko efekt reverb, tylko filtr low-pass, tylko 1 aux slot. OpenAL w wersji Creative który działa na "Generic Sofware" umie bardzo niewiele więcej. Generalnie, trzeba przygotować się że efekty EFX będą naprawdę piękne tylko jeżeli gracz posiada faktycznie dobrą kartę dźwiękową, optymalnie — jeżeli ma implementację OpenAL dostarczoną przez producentów kart zoptymalizowaną pod dany hardware. To nie jest norma na obecnych komputerach, ale możemy mieć nadzieję że za kilka lat lepsze karty dźwiękowe będą powszechne, a razem z nimi efekty EFX staną się szerzej dostępne.
5. Menedżer dźwięków na priorytetach
O ile możemy stworzyć bardzo dużo buforów (dopóki dźwięki są krótkie i nie zajmują zbyt wiele pamięci), nie jest dobrze tworzyć zbyt wielu dźwięków (source). Miksowanie wielu dźwięków zajmuje dużo czasu, a użytkownik przecież nie rozróżni i tak 20 dźwięków na raz. Z tego powodu niektóre implementacje OpenALa (np. Generic Hardware pod Windowsem) mogą nawet odmawiać stworzenia zbyt wielu źródeł dźwięku. Implementacja OpenALa nie wybiera sama dźwięków, nie ma żadnego mechanizmu który automatyczne odrzucałby zbyt słabe (o małym GAIN, albo daleko od słuchacza) dźwięki. Nie ma nawet mechanizmu który gwarantowałby eliminowanie wyłączonych/paused dźwięków (bo przecież dźwięk paused może być w następnej sekundzie odtworzony). Idea jest taka że musimy zrobić to sami w naszym programie, znając zależności pomiędzy dźwiękami.
Proste demo dynamicznego przydzielania buforów do dźwięków.
W ogólnej sytuacji, typowe rozwiązanie to menedżer dźwięków który zarządza "zasobem" dźwięków OpenALa:
Na początku programu inicjujemy naszą pulę dźwięków, tworząc kilka (rozsądne wartości są pomiędzy 4 a 16) początkowych dźwięków.
Menedżer dźwięków śledzi jak każdy dźwięk OpenAL jest wykorzystany. Wszystkie żądania odtworzenia dźwięku muszą przechodzić przez nasz menedżer. Menedżer musi także śledzić kiedy dany dźwięk skończył odtwarzanie (alGetSource1i(ALSource, AL_SOURCE_STATE) = AL_STOPPED), czyli nie jest już używany.
Kiedy chcemy odegrać dźwięk, menedżer dźwięku patrzy na obecnę zajętość dźwięków. Kiedy ma aktulanie niewykorzystany dźwięk, to super, używa go. Kiedy nie — wtedy mamy wiele możliwości.
- Możemy spróbować zaalokować dodatkowy dźwięk jeżeli jeszcze nie przekroczyliśmy "rozsądnego" limitu (np. mamy zaalokowane 4 dźwięki, ale pozwalamy sobie w razie potrzeby utworzyć do 16 dźwięków). Po (a także przed, dla bezpieczeństwa) alCreateSources(1, ...) należy sprawdzić alGetError(), alCreateSources ustawia błąd AL_INVALID_VALUE kiedy nie może utworzyć więcej sources.
- Możemy spróbować znaleźć dźwięk o niższym priorytecie — wtedy bezczelnie go wyrzucamy na korzyść dźwięku o wyższym priorytecie. Np. dźwięk kiedy gracz ekwipuje broń, albo trafia/chybia potworka są bardzo ważne dla gracza; natomiast dźwięk "środowiska" (ćwierkanie ptaków, szum wiatru) nie jest aż tak ważny — możemy go (przynajmniej na chwilę) wyłączyć, i tak jest cichy i pewnie byłby zagłuszony przez ważniejsze dźwięki. Musimy więc znać priorytet każdego dźwięku (osobiście, dla mnie działa nawet prosta skala z kilkoma wartościami: "mało ważny dźwięk środowiska", "dźwięk potworka", "dźwięk akcji gracza", "bardzo ważny dźwięk środowiska").
- Jeżeli wyczerpaliśmy wszystkie możliwości, to trudno — odrzucamy nowy dźwięk, nie będziemy go odtwarzać.
W zależności od dynamiki naszych dźwięków, inne strategie mogą mieć sens. Np. może być sens bardziej agresywnie usuwać stare dźwięki, albo na odwrót — starać się nie przerywać starych dźwięków (w przełożeniu na kod, oznaczałoby to że skalujemy priorytet nowego dźwięku na porzeby porównania np. razy 2 albo razy 0.5). Może być sens uznawać głośniejsze dźwięki za bardziej ważne (tutaj przyda nam się znajomość "głośności" dźwięku którą podajemy jako GAIN dla source OpenALa, patrz notki powyżej o głośności dźwięku w buforze).
Przykłady: np. sound menedżer OpenALa w OGRE. Możecie też zerknąć na mój TALSourceAllocator, powiązany z TGameSoundEngine.
Demo: test_al_source_allocator.
6. Strumieniowanie
Kiedy chcemy załadować duże pliki, może nie być rozsądnie marnować kilkadziesiąt MB pamięci na ścieżki dźwiękowe naszego levelu. Stąd pomysł na strumieniowanie: odkompresowujemy i "podajemy" OpenALowi zawartość dźwięków w miarę jak są potrzebne. Możemy po prostu czytać i dekompresować plik w locie, a możemy też wczytać do pamięci skompresowany plik z muzyką, i tylko odkompresowywać go na żądanie (wada: marnujemy te kilka MB, zaleta: nie "szuramy po dysku" w trakcie działania gry — to jest dobre, generalnie odczyt/zapis z dysku potrafi uszkodzić szybkość działania gry, OpenGLa etc.).
Sam odczyt (i ew. dekompresja) dźwięku — patrz inne biblioteki. Tak jak OpenGL nie ma funkcji do odczytu tekstur z pliku, tak samo OpenAL nie ma funkcji do odczytu dźwięków z pliku. Chociaż ALUT ma funkcję do odczytu prostych WAV, a niektóre implementacje OpenAL mają rozszerzenie AL_EXT_vorbis a nawet AL_EXT_mp3; ale rozszerzenia AL_EXT_vorbis/mp3 nie przyjęły się (bo developerzy twierdzą że jest zbyt łatwo zaimplementować to samemu :) ).
Popularny format to OggVorbis ze świetną i trywialną w obsłudze biblioteką vorbisfile, można znaleźć mnóstwo przykładów w Internecie (także w powiązaniu z OpenALem). Są też biblioteki radzące sobie z większą ilością formatów, jak libsndfile. Jako programiści gier, powinniśmy mieć przynajmniej dwa formaty dźwięków w swoim arsenale: WAV (prosty, nieskompresowany, idealna jakość dźwięku, dobry dla krótkich dźwięków, można zaimplementować samemu w kilkanaście linijek kodu) i OggVorbis lub MP3 (format mocno skompresowany, do dłuższych ścieżek dźwiękowych; Michalis mocno poleca OggVorbis — bez patentów, otwarta specyfikacja, otwarta i dobrze udokumentowana implementacja).
Szczegóły streaming w OpenAL:
W zasadzie, to trywialne: funkcja alSourceQueueBuffers(ALuint sourceName, ALsizei numBuffers, ALuint *bufferNames); nakazuje na danym source odtwarzać listę buforów.
Musimy jeszcze wiedzieć kiedy nasze bufory się kończą, i powinniśmy załadować następne. Tak naprawdę, powinniśmy załadować następny bufor zanim jeszcze ostatni bufor się skończy, żeby nie było nieprzyjemnego "kliku" w trakcie grania (kiedy jeden bufor się kończy i przez ułamek sekundy nasz dźwięk jest wyłączony). Ponadto, chcemy usuwać stare bufory z kolejki, i wypełniać je nowymi danymi (inaczej po pewnym czasie mielibyśmy bardzo dużo buforów które w sumie zawierałyby cały nasz dźwięk zdekompresowany, a przecież chcieliśmy tego uniknąć). W praktyce, najprościej jest utrzymywać pewną ilość (co najmniej 2) buforów queued na naszym source.
Komenda alGetSourcei(source, AL_BUFFERS_PROCESSED, &processed;); mówi nam ile buforów z kolejki zostało już przetworzonych. Możemy śledzić sami ile buforów w ogóle jest w kolejce (albo zapytać OpenALa o AL_BUFFERS_QUEUED), w rezultacie wiemy kiedy jesteśmy blisko ostatniego bufora i powinniśmy dodać do kolejki następny bufor. Komenda alSourceUnqueueBuffers(ALuint sourceName, ALsizei numEntries, ALuint *bufferNames) usuwa przetworzone bufory z kolejki, zwracając ich identyfikatory (można usunąc maksymalnie AL_BUFFERS_PROCESSED buforów, nie więcej). Więc prosta metoda która "utrzymuje" 2 bufory w kolejce (i w rezultacie implementuje dobre streaming, przy założeniu że czas załadowania 1 bufora jest krótszy niż czas odtworzenia 1 bufora) to:
/* Na początku załaduj pierwsze dwa bufory, wrzuć je do kolejki i zacznij grać. stream(xxx) ładuje z pliku następny kawałek do bufora xxx. */ alGenBuffers(2, buffers); stream(buffers[0]); stream(buffers[1]); alSourceQueueBuffers(source, 2, buffers); alSourcePlay(source); ..... /* Co jakiś czas w trakcie gry upewnij się że masz 2 bufory w kolejce: */ alGetSourcei(source, AL_BUFFERS_PROCESSED, &processed;); while(processed--) { ALuint buffer; /* pobierz nieużywany bufor */ alSourceUnqueueBuffers(source, 1, &buffer;); /* wypełnij bufor kolejną porcją danych *. stream(buffer); /* dodaj bufor do kolejki */ alSourceQueueBuffers(source, 1, &buffer;); } /* Powyżej jest uproszczony kod z artykułu na devmaster.net, link poniżej */
Taki mechanizm pozwala na różne inne ciekawe sztuczki, nie tylko proste odtwarzanie dźwieku z ładowanych danych. Np. można komponować ścieżkę dźwiękową w trakcie gry, np. gramy w kółko bufory A, B, C ze spokojną muzyką. Jeżeli w momencie ładowania buforów stwierdzimy że gracz aktualnie walczy (np. otrzymał obrażenia i/lub jest w pobliżu wrogiego potworka) to ładujemy następnie bufory D, E, F. W ten sposób możemy skomponować dwie ścieżki dźwiękowe (na okazje spokojnej eksploracji i walki), zagwarantować że sklejają się jak należy (normalne loop, czyli C->A oraz F->D oraz przejścia: C->D oraz F->A). Dynamiczna zmiana muzyki robi całkiem niezłe wrażenie (chyba pierwszy raz widziałem to w bardzo bardzo starej grze "Witchcraft").
Trzeba pamiętać o pewnych ograniczeniach: wszystkie bufory queued na jednym source muszą mieć taki sam format (jeżeli rozpakowujemy je z jednego pliku, czyli robimy najprostsze strumieniowanie, to zazwyczaj już jesteśmy Ok, mamy to zagwarantowane). Nie można zainicjować strumieniowania na source do którego przydzieliliśmy pojedynczy bufor (należy najpierw zresetować source poprzez przypisanie mu pustego bufora, alSourcei(sid, AL_BUFFER, NULL)), trzeba na to uważać kiedy mamy menedżer dźwieków.
OpenAL Lesson 8: OggVorbis Streaming Using The Source Queue (z devmaster.net).
Play Compressed Formats With OpenAL, przykładowe odtwarzanie plików ze strumieniami poprzez ffmpeg (przez autora OpenAL Soft, więc pewnie wie o czym pisze :) ).
Programming Linux Games — rozdział 5 poświecony jest programowaniu dźwięku. Zawiera omówienie podstaw i przykłady użycia OpenAL, libsndfile, vorbisfile, i wielu innych rzeczy. Mimo że książka ma swoje lata (omawia np. kilka przestarzałych rozszerzeń OpenALa z implementacji Loki), polecam!
Końcowe uwagi: pamiętajcie sprawdzać alGetError(), alcGetError(device). Mechanizm jest podobny jak w przypadku OpenGLa, tzn. funkcja która spowodowała błąd generalnie jest ignorowana i ustawia wewnętrzną flagę. Sprawdzenie alGetError() zwraca tą flagę i czyści ją. Poprawny program nigdy nie powinien generować błędów OpenALa (no, powiedzmy że AL_OUT_OF_MEMORY jest nie do uniknięcia w teorii).