Uwagi programistyczne OpenSL ES

OSTRZEŻENIE: standard OpenSL ES został wycofany. Deweloperzy powinni używać biblioteki open source Oboe, która jest dostępna na GitHubie. Oboe to kod w języku C++ zapewniający interfejs API podobny do AAudio. Oboe wywołuje AAudio, gdy jest ono dostępne, a gdy nie jest, używa OpenSL ES.

Uwagi w tej sekcji uzupełniają specyfikację OpenSL ES 1.0.1.

Inicjowanie obiektów i interfejsu

Dwa aspekty modelu programowania OpenSL ES, które mogą być nieznane nowym deweloperom, to rozróżnianie obiektów i interfejsów oraz sekwencja inicjalizacji.

Obiekt OpenSL ES jest podobny do koncepcji obiektu w językach programowania, takich jak Java czy C++. Jedyna różnica jest taka, że obiekt OpenSL ES jest widoczny tylko przez powiązane z nim interfejsy. Obejmuje to początkowy interfejs wszystkich obiektów, czyli SLObjectItf. Nie ma uchwytu dla obiektu, tylko uchwyt dla interfejsu SLObjectItf obiektu.

Najpierw tworzony jest obiekt OpenSL ES, który zwraca SLObjectItf, a następnie realizowany. Jest to podobne do typowego wzorca programowania, w którym najpierw tworzony jest obiekt (który nigdy nie powinien się nie powieść z innego powodu niż brak pamięci lub nieprawidłowe parametry), a następnie przeprowadzana jest inicjalizacja (która może się nie powieść z powodu braku zasobów). Etap realizacji zapewnia implementacji logiczne miejsce na przydzielenie dodatkowych zasobów w razie potrzeby.

W ramach interfejsu API do tworzenia obiektów aplikacja określa tablicę interfejsów, które zamierza nabyć w przyszłości. Pamiętaj, że ta tablica nie nabywa automatycznie interfejsów. Wskazuje jedynie na zamiar ich nabycia w przyszłości. Interfejsy są rozróżniane jako domyślne lub wyraźne. Jeśli chcesz pozyskać później interfejs, musi on być podany w tablicy. Implikatywny interfejs nie musi być wymieniony w tablicy tworzenia obiektu , ale nic nie stoi na przeszkodzie, aby go tam umieścić. OpenSL ES ma jeszcze jeden rodzaj interfejsu o nazwie dynamiczny, którego nie trzeba określać w tablicy tworzenia obiektu. Można go dodać później, po utworzeniu obiektu. Implementacja na Androida zawiera funkcję ułatwiającą unikanie tej złożoności. Opisana jest ona w artykule Dynamiczne interfejsy podczas tworzenia obiektów.

Po utworzeniu i zrealizowaniu obiektu aplikacja powinna pobrać interfejsy dla każdej potrzebnej funkcji za pomocą GetInterface na początkowym elemencie SLObjectItf.

W końcu obiekt jest dostępny do użycia za pomocą interfejsów, ale pamiętaj, że niektóre obiekty wymagają dodatkowej konfiguracji. W szczególności odtwarzacz audio z źródłem danych URI wymaga nieco więcej przygotowań, aby wykrywać błędy połączenia. Szczegółowe informacje znajdziesz w sekcji Wstępna wczytywanie odtwarzacza audio.

Gdy aplikacja nie będzie już potrzebować obiektu, powinna go wyraźnie zniszczyć. Zapoznaj się z sekcją Zniszczenie poniżej.

Pobieranie z wyprzedzeniem odtwarzacza audio

W przypadku odtwarzacza audio z źródłem danych URI funkcja Object::Realize przydziela zasoby, ale nie łączy się ze źródłem danych (przygotowanie) ani nie rozpoczyna wstępnego pobierania danych. Występują one, gdy stan odtwarzacza ma wartość SL_PLAYSTATE_PAUSED lub SL_PLAYSTATE_PLAYING.

Niektóre informacje mogą być znane dopiero stosunkowo późno. W szczególności początkowo funkcja Player::GetDuration zwraca wartość SL_TIME_UNKNOWN, a funkcja MuteSolo::GetChannelCount zwraca albo wartość 0, która oznacza, że nie ma kanałów, albo kod błędu SL_RESULT_PRECONDITIONS_VIOLATED. Te interfejsy API zwracają odpowiednie wartości, gdy są znane.

Inne właściwości, które są początkowo nieznane, to częstotliwość próbkowania i rzeczywisty typ treści multimedialnych na podstawie nagłówka treści (w przeciwieństwie do typu MIME określonego przez aplikację i typu kontenera). Są one również określane później podczas przygotowywania/pobierania w poprzednim terminie, ale nie ma interfejsów API do ich pobierania.

Interfejs stanu wstępnego jest przydatny do wykrywania, kiedy wszystkie informacje są dostępne, lub kiedy aplikacja może okresowo przeprowadzać ankiety. Pamiętaj, że niektórych informacji, np. czasu trwania strumieniowego przesyłania plików MP3, nigdy nie można poznać.

Interfejs stanu pobierania z wyprzedzeniem przydaje się też do wykrywania błędów. Zarejestruj wywołanie zwrotne i włącz co najmniej zdarzenia SL_PREFETCHEVENT_FILLLEVELCHANGESL_PREFETCHEVENT_STATUSCHANGE. Jeśli oba te zdarzenia są przesyłane jednocześnie, a PrefetchStatus::GetFillLevel zwraca wartość zerową, a PrefetchStatus::GetPrefetchStatus – wartość SL_PREFETCHSTATUS_UNDERFLOW, oznacza to, że w źródle danych wystąpiła nieodwracalna nieprawidłowość. Obejmuje to brak możliwości połączenia ze źródłem danych, ponieważ lokalna nazwa pliku nie istnieje lub identyfikator URI sieci jest nieprawidłowy.

W kolejnych wersjach OpenSL ES ma zostać dodana bardziej wyraźna obsługa błędów w źródle danych. Jednak ze względu na przyszłą zgodność plików binarnych zamierzamy nadal obsługiwać bieżącą metodę zgłaszania nieodwracalnych błędów.

Podsumowując, zalecana kolejność kodu to:

  1. Engine::CreateAudioPlayer
  2. Object:Realize
  3. Object::GetInterface za SL_IID_PREFETCHSTATUS
  4. PrefetchStatus::SetCallbackEventsMask
  5. PrefetchStatus::SetFillUpdatePeriod
  6. PrefetchStatus::RegisterCallback
  7. Object::GetInterface za SL_IID_PLAY
  8. Play::SetPlayState to SL_PLAYSTATE_PAUSED lub SL_PLAYSTATE_PLAYING

Uwaga: w tym momencie odbywa się przygotowanie i pobieranie wstępne. W tym czasie do Twojego telefonu jest wysyłane wywołanie zwrotne z okresowymi aktualizacjami stanu.

Zniszcz

Pamiętaj, aby przed zamknięciem aplikacji usunąć wszystkie obiekty. Obiekty powinny być usuwane w odwrotnej kolejności ich utworzenia, ponieważ usuwanie obiektu, który ma obiekty zależne, jest niebezpieczne. Na przykład w tej kolejności: odtwarzacze i rejestratory dźwięku, miks wyjściowy, a na końcu silnik.

OpenSL ES nie obsługuje automatycznego usuwania elementów nieużywanych, ani liczenia odwołań interfejsów. Po wywołaniu funkcji Object::Destroy wszystkie istniejące interfejsy wyprowadzone z powiązanego obiektu stają się niezdefiniowane.

Implementacja OpenSL ES na Androidzie nie wykrywa nieprawidłowego użycia takich interfejsów. Dalsze używanie takich interfejsów po zniszczeniu obiektu może spowodować awarię aplikacji lub jej nieprzewidziane działanie.

Zalecamy, aby w ramach sekwencji niszczenia obiektów jawnie ustawić zarówno interfejs obiektu podstawowego, jak i wszystkie powiązane interfejsy na wartość NULL. Zapobiegnie to przypadkowemu użyciu nieaktualnego identyfikatora interfejsu.

Panning stereo

Gdy Volume::EnableStereoPosition jest używany do włączenia panoramowania stereo w przypadku źródła mono, następuje zmniejszenie łącznego poziomu mocy dźwięku o 3 dB. Jest to konieczne, aby całkowity poziom mocy dźwięku pozostał stały, gdy źródło jest przesuwane z jednego kanału na drugi. Dlatego włącz pozycjonowanie stereo tylko wtedy, gdy jest to konieczne. Więcej informacji znajdziesz w artykule na temat panowania dźwięku w Wikipedii.

Wywołania zwrotne i wątki

Obsługi wywołania są zwykle wywoływane synchronicznie, gdy implementacja wykryje zdarzenie. Ten punkt jest asynchroniczny w stosunku do aplikacji, dlatego należy użyć mechanizmu synchronizacji bez blokowania, aby kontrolować dostęp do wszystkich zmiennych udostępnianych przez aplikację i obsługę wywołania zwrotnego. W przykładowym kodzie, np. w przypadku kolejek buforowych, pominęliśmy tę synchronizację lub użyliśmy blokującej synchronizacji ze względu na prostotę. Jednak prawidłowa synchronizacja bez blokowania jest kluczowa dla każdego kodu produkcyjnego.

Obsługi wywołań zwrotnych są wywoływane z wewnętrznych wątków, które nie są związane z aplikacją i nie są przypisane do środowiska wykonawczego Androida, więc nie mogą korzystać z JNI. Ponieważ te wątki wewnętrzne są kluczowe dla integralności implementacji OpenSL ES, w obsługach wywołania zwrotnego nie należy blokować ani wykonywać nadmiernej pracy.

Jeśli Twój handler wywołania zwrotnego musi używać JNI lub wykonywać zadania, które nie są proporcjonalne do wywołania zwrotnego, zamiast tego powinien opublikować zdarzenie, które ma przetworzyć inny wątek. Przykłady akceptowalnych obciążeń wywołania to renderowanie i dodawanie do kolejki kolejnego bufora wyjściowego (w przypadku AudioPlayer), przetwarzanie właśnie wypełnionego bufora wejściowego i dodawanie do kolejki kolejnego pustego bufora (w przypadku AudioRecorder) lub proste interfejsy API, takie jak większość interfejsów z rodziny Get. Informacje o zadaniu znajdziesz w sekcji Wydajność poniżej.

Należy pamiętać, że odwrotna kolejność jest bezpieczna: wątek aplikacji na Androida, który wejdzie do JNI, może bezpośrednio wywoływać interfejsy OpenSL ES, w tym te, które blokują. Nie zalecamy jednak wywołań blokujących z wątku głównego, ponieważ mogą one spowodować błąd Aplikacja nie odpowiada (ANR).

Decyzja o tym, który wątek wywołuje moduł obsługi wywołania zwrotnego, w większości należy do wdrożenia. Ta elastyczność umożliwia przyszłe optymalizacje, zwłaszcza na urządzeniach wielordzeniowych.

Nie ma gwarancji, że wątek, w którym działa obiekt obsługi wywołania zwrotnego, będzie miał tę samą tożsamość w różnych wywołaniach. Dlatego nie należy polegać na wartości pthread_t zwracanej przez funkcję pthread_self() ani na wartości pid_t zwracanej przez funkcję gettid(), ponieważ mogą się one różnić w różnych wywołaniach. Z tego samego powodu nie używaj interfejsów API pamięci lokalnej wątku (TLS), takich jak pthread_setspecific()pthread_getspecific(), w wywołaniu zwrotnym.

Implementacja gwarantuje, że nie występują równoległe wywołania tego samego typu dla tego samego obiektu. W różnych wątkach możliwe są jednak równoczesne wywołania zwrotne różnego rodzaju dla tego samego obiektu.

Wydajność

Ponieważ OpenSL ES jest natywnym interfejsem API w języku C, wątki aplikacji, które nie są związane z czasem wykonywania i wywołują OpenSL ES, nie mają narzutu związanego z czasem wykonywania, takiego jak przerwy w zbieraniu elementów. Z jednym wyjątkiem opisanym poniżej, nie ma żadnych dodatkowych korzyści z użycia OpenSL ES. W szczególności użycie OpenSL ES nie gwarantuje ulepszeń takich jak mniejsza opóźnienie dźwięku czy wyższy priorytet przy przydzielaniu czasu procesora niż ten, który zapewnia platforma. Z drugiej strony, wraz z rozwojem platformy Android i specjalnych implementacji na urządzeniach aplikacja OpenSL ES może korzystać z ewentualnych przyszłych ulepszeń wydajności systemu.

Jednym z takich ulepszeń jest obsługa mniejszego opóźnienia wyjścia dźwięku. Krótszy czas oczekiwania na wygenerowanie danych został uwzględniony najpierw w Androidzie 4.1 (poziom interfejsu API 16), a kolejne postępy pojawiły się w Androidzie 4.2 (poziom API 17). Te ulepszenia są dostępne w OpenSL ES na urządzeniach, które obsługują funkcję android.hardware.audio.low_latency. Jeśli urządzenie nie zgłasza praw do tej funkcji, ale obsługuje Androida 2.3 (poziom interfejsu API 9) lub nowszego, nadal możesz używać interfejsów OpenSL ES API, ale opóźnienia mogą być większe. Ścieżka o krótszym czasie oczekiwania na wyjście jest używana tylko wtedy, gdy aplikacja żąda rozmiaru bufora i próbkowania zgodnych z natywną konfiguracją wyjścia urządzenia. Te parametry są specyficzne dla urządzenia i należy je uzyskać w sposób podany poniżej.

Począwszy od Androida w wersji 4.2 (poziom interfejsu API 17) aplikacja może wysyłać zapytania dotyczące natywnej lub optymalnej częstotliwości próbkowania wyjściowego i rozmiaru bufora głównego strumienia wyjściowego urządzenia. W połączeniu z wspomnianym testem funkcji aplikacja może teraz odpowiednio skonfigurować się pod kątem niższej latencji na urządzeniach, które obsługują tę funkcję.

W przypadku Androida 4.2 (poziom API 17) i starszych wersja interfejsu API liczba buforów musi wynosić co najmniej 2, aby zmniejszyć opóźnienie. Począwszy od Androida 4.3 (interfejs API na poziomie 18) wystarczy jeden bufor, aby zmniejszyć opóźnienie.

Wszystkie interfejsy OpenSL ES do efektów wyjściowych wykluczają ścieżkę o mniejszym opóźnieniu.

Zalecana kolejność to:

  1. Aby potwierdzić korzystanie z OpenSL ES, sprawdź, czy używasz interfejsu API na poziomie 9 lub wyższym.
  2. Sprawdź, czy funkcja android.hardware.audio.low_latency jest dostępna, używając kodu takiego jak ten:

    Kotlin

    import android.content.pm.PackageManager
    ...
    val pm: PackageManager = context.packageManager
    val claimsFeature: Boolean = pm.hasSystemFeature(PackageManager.FEATURE_AUDIO_LOW_LATENCY)

    Java

    import android.content.pm.PackageManager;
    ...
    PackageManager pm = getContext().getPackageManager();
    boolean claimsFeature = pm.hasSystemFeature(PackageManager.FEATURE_AUDIO_LOW_LATENCY);
  3. Sprawdź, czy interfejs API jest na poziomie 17 lub wyższym, aby potwierdzić użycie interfejsu android.media.AudioManager.getProperty().
  4. Uzyskaj natywny lub optymalny współczynnik próbkowania wyjścia i rozmiar bufora dla głównego strumienia wyjściowego tego urządzenia, używając kodu podobnego do tego:

    Kotlin

    import android.media.AudioManager
    ...
    val am = getSystemService(Context.AUDIO_SERVICE) as AudioManager
    val sampleRate: String = am.getProperty(AudioManager.PROPERTY_OUTPUT_SAMPLE_RATE)
    val framesPerBuffer: String = am.getProperty(AudioManager.PROPERTY_OUTPUT_FRAMES_PER_BUFFER)

    Java

    import android.media.AudioManager;
    ...
    AudioManager am = (AudioManager) getSystemService(Context.AUDIO_SERVICE);
    String sampleRate = am.getProperty(AudioManager.PROPERTY_OUTPUT_SAMPLE_RATE);
    String framesPerBuffer = am.getProperty(AudioManager.PROPERTY_OUTPUT_FRAMES_PER_BUFFER);
    sampleRateframesPerBuffer to ciągi znaków. Najpierw sprawdź, czy zmienna jest null, a potem przekonwertuj ją na int za pomocą funkcji Integer.parseInt().
  5. Teraz użyj OpenSL ES, aby utworzyć odtwarzacz audio z lokalizatorem danych kolejki bufora PCM.

Uwaga: aby określić rozmiar natywnego bufora i częstotliwość próbkowania dla aplikacji audio OpenSL ES na urządzeniu audio, możesz użyć aplikacji testowej Audio Buffer Size. Możesz też odwiedzić GitHuba, aby zobaczyć przykłady rozmiaru bufora audio.

Liczba odtwarzaczy dźwięku o krótszym opóźnieniu jest ograniczona. Jeśli Twoja aplikacja wymaga więcej niż kilku źródeł dźwięku, rozważ zmiksowanie dźwięku na poziomie aplikacji. Pamiętaj, aby usunąć odtwarzacze audio, gdy Twoja aktywność jest wstrzymana, ponieważ są one zasobem globalnym udostępnianym innym aplikacjom.

Aby uniknąć zakłóceń dźwiękowych, wywołanie obsługi kolejki buforowej musi być wykonywane w ramach małego i przewidywalnego okna czasowego. Zwykle oznacza to brak nieograniczonego blokowania na semaforach, warunkach lub operacjach wejścia-wyjścia. Zamiast tego rozważ użycie blokad próbnych, blokad z czasem oczekiwania i  nieblokujących algorytmów.

Obliczenia wymagane do renderowania następnego bufora (w przypadku AudioPlayer) lub wykorzystania poprzedniego bufora (w przypadku AudioRecord) powinny zajmować mniej więcej tyle samo czasu w przypadku każdego wywołania zwrotnego. Unikaj algorytmów, które działają w niedeterministycznym czasie lub są wybuchowe w swoich obliczeniach. Obliczenia wywołania zwrotnego są niestabilne, jeśli czas procesora poświęcony na dowolne wywołanie zwrotne jest znacznie dłuższy niż średnia. Podsumowując, idealnie byłoby, gdyby czas wykonywania modułu obsługi przez procesor był zbliżony do 0, a moduł nie blokował się przez nieograniczony czas.

Mniejsze opóźnienie dźwięku jest możliwe tylko w przypadku tych urządzeń wyjściowych:

Na niektórych urządzeniach opóźnienie głośnika jest dłuższe niż w przypadku innych ścieżek ze względu na cyfrowe przetwarzanie sygnału w celu korekty i ochrony głośnika.

Od Androida 5.0 (poziom interfejsu API 21) wejściowe wejście audio z mniejszym opóźnieniem jest obsługiwane na wybranych urządzeniach. Aby skorzystać z tej funkcji, najpierw sprawdź, czy wyjście o niższym opóźnieniu jest dostępne zgodnie z opisem powyżej. Wyjście o krótszym opóźnieniu jest warunkiem wstępnym dla funkcji wejścia o krótszym opóźnieniu. Następnie utwórz Rejestrator audio z taką samą częstotliwością próbkowania i rozmiarem bufora jak w przypadku danych wyjściowych. Interfejsy OpenSL ES do efektów wejściowych wyłączają ścieżkę z mniejszym czasem oczekiwania. Aby uzyskać mniejsze opóźnienie, należy użyć wstępnie ustawionego zapisu SL_ANDROID_RECORDING_PRESET_VOICE_RECOGNITION. Wyłącza on przetwarzanie sygnału cyfrowego na poziomie urządzenia, które może zwiększać opóźnienie na ścieżce wejścia. Więcej informacji o gotowych ustawieniach rejestrowania znajdziesz w sekcji Interfejs konfiguracji Androida powyżej.

W przypadku jednoczesnego przesyłania i odbierania danych po każdej stronie używane są oddzielne procedury obsługi zakończenia kolejki buforowej. Nie ma gwarancji względnej kolejności tych wywołań ani synchronizacji zegarów audio, nawet gdy obie strony używają tego samego współczynnika próbkowania. Aplikacja powinna buforować dane, zapewniając prawidłową synchronizację bufora.

Jednym z efektów potencjalnie niezależnych zegarów audio jest konieczność asynchronicznej konwersji częstotliwości próbkowania. Prostą (choć nieidealną pod względem jakości dźwięku) techniką asynchronicznej konwersji częstotliwości próbkowania jest powielanie lub pomijanie próbek w pobliżu punktu przecięcia z osią zero. Możliwe są bardziej złożone konwersje.

Tryby wydajności

Począwszy od Androida w wersji 7.1 (API Level 25) w OpenSL ES wprowadziliśmy sposób określania trybu wydajności dla ścieżki audio. Dostępne opcje:

  • SL_ANDROID_PERFORMANCE_NONE: brak wymagań dotyczących wydajności. Umożliwia stosowanie efektów sprzętowych i programowych.
  • SL_ANDROID_PERFORMANCE_LATENCY: priorytet jest uwzględniany w czasie oczekiwania. Bez efektów sprzętowych ani programowych. Jest to tryb domyślny.
  • SL_ANDROID_PERFORMANCE_LATENCY_EFFECTS: priorytet ma opóźnienie, ale nadal są dozwolone efekty sprzętowe i programowe.
  • SL_ANDROID_PERFORMANCE_POWER_SAVING: priorytetem jest oszczędzanie energii. Umożliwia stosowanie efektów sprzętowych i programowych.

Uwaga: jeśli nie potrzebujesz ścieżki o niskiej latencji i chcesz korzystać z wbudowanych efektów dźwiękowych urządzenia (np. aby poprawić jakość dźwięku podczas odtwarzania filmu), musisz wyraźnie ustawić tryb działania na SL_ANDROID_PERFORMANCE_NONE.

Aby ustawić tryb wydajności, musisz wywołać SetConfiguration za pomocą interfejsu konfiguracji Androida, jak pokazano poniżej:

  // Obtain the Android configuration interface using a previously configured SLObjectItf.
  SLAndroidConfigurationItf configItf = nullptr;
  (*objItf)->GetInterface(objItf, SL_IID_ANDROIDCONFIGURATION, &configItf);

  // Set the performance mode.
  SLuint32 performanceMode = SL_ANDROID_PERFORMANCE_NONE;
    result = (*configItf)->SetConfiguration(configItf, SL_ANDROID_KEY_PERFORMANCE_MODE,
                                                     &performanceMode, sizeof(performanceMode));

Zabezpieczenia i uprawnienia

Kto może co robić, zabezpieczenia w Androidzie są wykonywane na poziomie procesu. Kod w języku programowania Java nie może zrobić niczego więcej niż kod natywny, a kod natywny nie może zrobić niczego więcej niż kod w języku programowania Java. Różnią się one tylko dostępnymi interfejsami API.

Aplikacje korzystające z OpenSL ES muszą prosić o uprawnienia, których potrzebują do działania podobnych nienatywnych interfejsów API. Jeśli na przykład aplikacja nagrywa dźwięk, potrzebuje uprawnienia android.permission.RECORD_AUDIO. Aplikacje korzystające z efektów dźwiękowych muszą mieć android.permission.MODIFY_AUDIO_SETTINGS. Aplikacje, które odtwarzają zasoby sieciowe z identyfikatorem URI, wymagają android.permission.NETWORK. Więcej informacji znajdziesz w artykule Praca z uprawnieniami systemowymi.

W zależności od wersji i implementacji platformy parsery treści multimedialnych oraz kodeki oprogramowania mogą działać w kontekście aplikacji na Androida, która wywołuje OpenSL ES (kodeki sprzętowe są abstrakcyjne, ale zależą od urządzenia). Nieprawidłowo sformułowane treści, które mają na celu wykorzystanie luk w zabezpieczeniach parsowania i kodowania, to znany wektor ataku. Zalecamy odtwarzanie multimediów tylko z zaufanych źródeł lub podzielenie aplikacji na partycje tak, aby kod obsługujący multimedia z niepewnych źródeł działał w względnie odizolowanym środowisku. Możesz na przykład przetworzyć multimedia z niezaufanych źródeł w osobnym procesie. Oba procesy będą nadal działać pod tym samym identyfikatorem UID, ale ich rozdzielenie utrudnia przeprowadzenie ataku.