Uwagi programistyczne OpenSL ES

Uwagi w tej sekcji stanowią uzupełnienie specyfikacji OpenSL ES 1.0.1.

Obiekty i inicjowanie interfejsu

Dwa aspekty modelu programowania OpenSL ES, których nowi programiści mogą nie znać, to różnica między obiektami i interfejsami oraz sekwencja inicjowania.

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

Obiekt OpenSL ES jest najpierw tworzony, który zwraca wartość SLObjectItf, a następnie realizowany. Działa to podobnie do popularnego wzorca programowania, które polega na pierwszym konstruowaniu obiektu (którego działanie powinno się zakończyć tylko z powodu braku pamięci lub nieprawidłowych parametrów), a potem do zakończenia inicjowania (co może się nie udać z powodu braku zasobów). Etap realizacji sprawia, że wdrożenie stanowi logiczne miejsce na przydzielenie w razie potrzeby dodatkowych zasobów.

W ramach interfejsu API tworzącego obiekt aplikacja określa tablicę interfejsów, które planuje uzyskać później. Pamiętaj, że tablica ta nie pozyskuje automatycznie interfejsów. Wskazuje jedynie na przyszły zamiar ich pozyskania. Interfejsy są rozróżniane jako niejawne i jawne. Jeśli zostanie uzyskany później, interfejs musi być wymieniony w tablicy. Interfejs niejawny nie musi być wymieniony w tablicy tworzenia obiektów, ale nic nie stoi na przeszkodzie, aby go tam umieścić. OpenSL ES ma jeszcze jeden rodzaj interfejsu – dynamic. Nie trzeba go podawać w tablicy tworzenia obiektów. Można go dodać później po utworzeniu obiektu. Implementacja na Androida zapewnia wygodne funkcje, które pozwalają uniknąć tego złożoności. Opisaliśmy je w artykule Dynamiczne interfejsy podczas tworzenia obiektu.

Po utworzeniu i uruchomieniu obiektu aplikacja powinna pozyskać interfejsy dla każdej potrzebnej funkcji, korzystając z metody GetInterface w początkowej konfiguracji SLObjectItf.

Obiekt jest dostępny do użycia przez jego interfejsy, ale niektóre obiekty wymagają dalszej konfiguracji. W szczególności odtwarzacz audio ze źródłem danych URI wymaga więcej przygotowania, by wykryć błędy połączenia. Więcej informacji znajdziesz w sekcji Wstępne pobieranie odtwarzacza audio.

Gdy aplikacja korzysta z obiektu, musisz ją jawnie zniszczyć. Więcej informacji znajdziesz w sekcji Zniszczenie poniżej.

Pobieranie z wyprzedzeniem odtwarzacza audio

W przypadku odtwarzacza audio ze źródłem danych URI Object::Realize przydziela zasoby, ale nie łączy się ze źródłem danych (przygotowuje) ani nie rozpoczyna pobierania z wyprzedzeniem. Pojawiają się one, gdy stan odtwarzacza jest ustawiony na SL_PLAYSTATE_PAUSED lub SL_PLAYSTATE_PLAYING.

Niektóre informacje mogą być wciąż nieznane aż do stosunkowo późnego momentu w sekwencji. Szczególnie na początku funkcja Player::GetDuration zwraca wartość SL_TIME_UNKNOWN i MuteSolo::GetChannelCount zwraca wartość z liczbą kanałów równą 0 lub z wynikiem błędu SL_RESULT_PRECONDITIONS_VIOLATED. Te interfejsy API zwracają prawidłowe wartości, gdy są znane.

Inne właściwości, które są początkowo nieznane, to między innymi częstotliwość próbkowania i rzeczywisty typ treści multimedialnych określany na podstawie przeanalizowania nagłówka treści (w odróżnieniu od typu MIME i typu kontenera określonego przez aplikację). Są one również ustalane później podczas przygotowywania/pobierania z wyprzedzeniem, ale nie ma interfejsów API, które można by je pobrać.

Interfejs stanu pobierania z wyprzedzeniem przydaje się do wykrywania, czy wszystkie informacje są dostępne, a aplikacja może okresowo przeprowadzać ankiety. Pamiętaj, że niektóre informacje, np. czas trwania przesyłanego strumieniowo pliku MP3, mogą nigdy być znane.

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_FILLLEVELCHANGE i SL_PREFETCHEVENT_STATUSCHANGE. Jeśli oba te zdarzenia są wywoływane jednocześnie, a PrefetchStatus::GetFillLevel w raportach na poziomie 0, a PrefetchStatus::GetPrefetchStatusSL_PREFETCHSTATUS_UNDERFLOW, wskazuje to na nieodwracalny błąd w źródle danych. Obejmuje to brak możliwości połączenia ze źródłem danych, ponieważ lokalna nazwa pliku nie istnieje lub sieciowy identyfikator URI jest nieprawidłowy.

Kolejna wersja OpenSL ES powinna dodać bardziej wyraźną obsługę błędów w źródle danych. Aby jednak zapewnić w przyszłości zgodność plików binarnych, będziemy nadal obsługiwać obecną metodę zgłaszania nieodwracalnych błędów.

Podsumowując, zalecana sekwencja kodu to:

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

Uwaga: tutaj odbywa się przygotowanie i pobieranie z wyprzedzeniem. W tym czasie wywołanie zwrotne jest wywoływane z okresowymi aktualizacjami stanu.

Zniszcz

Pamiętaj, aby zniszczyć wszystkie obiekty przy wychodzeniu z aplikacji. Obiekty należy niszczyć w odwrotnej kolejności po ich utworzeniu, ponieważ nie jest bezpieczne niszczenie obiektów zawierających obiekty zależne. Na przykład zniszcz odtwarzacze i dyktafony w tej kolejności, mikser wyjściowy, a na koniec silnik.

OpenSL ES nie obsługuje automatycznego czyszczenia pamięci ani zliczania plików referencyjnych interfejsów. Po wywołaniu Object::Destroy wszystkie istniejące interfejsy, które pochodzą z powiązanego obiektu, staną się niezdefiniowane.

Implementacja OpenSL ES Androida nie wykrywa nieprawidłowego użycia takich interfejsów. Dalsze korzystanie z tych interfejsów po zniszczeniu obiektu może spowodować awarię aplikacji lub nieprzewidywalne zachowanie aplikacji.

Zalecamy jawne ustawienie interfejsu głównego obiektu i wszystkich powiązanych interfejsów na NULL w ramach sekwencji niszczenia obiektów. Zapobiega to przypadkowemu niewłaściwemu użyciu nieaktualnego uchwytu interfejsu.

Panorama w stereo

Gdy używasz funkcji Volume::EnableStereoPosition do włączania efektu stereo ze źródła mono, następuje zmniejszenie całkowitego poziomu mocy dźwięku o 3 dB. Jest to konieczne, by łączna moc dźwięku pozostawała na stałym poziomie podczas przenoszenia źródła z jednego kanału do drugiego. Dlatego pozycjonowanie stereo należy włączać tylko wtedy, gdy jest potrzebne. Więcej informacji znajdziesz w artykule o przesuwaniu dźwięku w Wikipedii.

Wywołania zwrotne i wątki

Moduły obsługi wywołań zwrotnych są zwykle wywoływane synchronicznie, gdy implementacja wykryje zdarzenie. Ten punkt jest asynchroniczny w odniesieniu do aplikacji, dlatego należy użyć nieblokującego mechanizmu synchronizacji, aby kontrolować dostęp do wszystkich zmiennych współdzielonych przez aplikację i moduł obsługi wywołania zwrotnego. W przykładowym kodzie, jak w przypadku kolejek bufora, pominęliśmy tę synchronizację lub zastosowaliśmy synchronizację blokującą, aby zwiększyć wygodę. Jednak w przypadku każdego kodu produkcyjnego właściwa, nieblokująca synchronizacja ma kluczowe znaczenie.

Moduły obsługi wywołań zwrotnych są wywoływane z wewnętrznych wątków niebędących aplikacjami, które nie są dołączone do środowiska wykonawczego Androida, więc nie kwalifikują się do korzystania z JNI. Te wątki wewnętrzne mają kluczowe znaczenie dla integralności implementacji OpenSL ES, dlatego moduł obsługi wywołania zwrotnego również nie powinien blokować ani wykonywać nadmiernej pracy.

Jeśli moduł obsługi wywołania zwrotnego musi używać JNI lub wykonywać zadania, które nie są proporcjonalne do wywołania zwrotnego, powinien on opublikować zdarzenie do przetworzenia w innym wątku. Przykładami akceptowanych zadań wywołania zwrotnego są renderowanie i dodawanie do kolejki następnego bufora wyjściowego (dla odtwarzacza audio), przetwarzanie właśnie wypełnionego bufora wejściowego i dodawanie do kolejki kolejnego pustego bufora (dla Rejestratora dźwięku) lub proste interfejsy API, takie jak większość rodziny Get. Informacje o zadaniu znajdziesz poniżej w sekcji Wydajność.

Zauważ, że odwrotność jest bezpieczna: wątek aplikacji na Androida, do którego dodano JNI, może bezpośrednio wywoływać interfejsy OpenSL ES API, w tym te, które blokują. Jednak blokowanie połączeń nie jest zalecane z wątku głównego, ponieważ może to spowodować błąd Aplikacja nie odpowiada (ANR).

Decyzja o tym, który wątek wywołuje moduł obsługi wywołania zwrotnego, należy w dużej mierze do implementacji. Ma to na celu umożliwienie przyszłych optymalizacji, zwłaszcza na urządzeniach wielordzeniowych.

Wątek, w którym działa moduł obsługi wywołania zwrotnego, nie musi mieć tej samej tożsamości w różnych wywołaniach. Dlatego nie polegaj na tym, aby pthread_t zwracany przez pthread_self() ani pid_t zwracany przez gettid() był spójny w wywołaniach. Z tego samego powodu nie używaj w wywołaniach zwrotnych interfejsów API TLS, takich jak pthread_setspecific() i pthread_getspecific().

Implementacja gwarantuje, że równoczesne wywołania zwrotne tego samego rodzaju nie będą występować dla tego samego obiektu. Jednak w przypadku różnych wątków możliwe są równoczesne wywołania zwrotne różnego rodzaju dla tego samego obiektu.

Wyniki

OpenSL ES to natywny interfejs API w języku C, więc wątki aplikacji innych niż środowisko wykonawcze, które wywołują OpenSL ES, nie mają dodatkowych kosztów związanych ze środowiskiem wykonawczym, takich jak wstrzymania odśmiecania. Z jednym wyjątkiem opisanym poniżej korzystanie z OpenSL ES nie zapewnia żadnych dodatkowych korzyści w zakresie wydajności. W szczególności korzystanie z OpenSL ES nie gwarantuje ulepszeń, takich jak mniejsze opóźnienie dźwięku i wyższy priorytet harmonogramu w porównaniu z innymi rozwiązaniami dostępnymi na platformie. Z drugiej strony – platforma Androida i implementacje konkretnych urządzeń wciąż ewoluują, dlatego aplikacja OpenSL ES może w przyszłości przynieść korzyści związane z poprawą wydajności systemu.

Jedną z takich zmian jest obsługa krótszego opóźnienia wyjścia audio. Zalety skrócenia czasu oczekiwania wyjściowego zostały najpierw uwzględnione w Androidzie 4.1 (poziom API 16), a dalsze postępy można było zaobserwować w Androidzie 4.2 (poziom API 17). Te ulepszenia są dostępne w OpenSL ES w przypadku implementacji urządzeń, które mają dostęp do funkcji android.hardware.audio.low_latency. Jeśli urządzenie nie deklaruje 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óźnienie może być większe. Ścieżka z mniejszym opóźnieniem wyjściowym jest używana tylko wtedy, gdy aplikacja żąda rozmiaru bufora i częstotliwości próbkowania, które są zgodne z natywną konfiguracją danych wyjściowych urządzenia. Te parametry są związane z konkretnym urządzeniem i należy je uzyskać w sposób opisany poniżej.

Począwszy od Androida 4.2 (poziom interfejsu API 17) aplikacja może wysyłać zapytania o natywną lub optymalną częstotliwość próbkowania wyjściowego na platformie oraz rozmiar bufora w przypadku podstawowego strumienia wyjściowego urządzenia. W połączeniu z wspomnianym przed chwilą testem funkcji aplikacja może się teraz odpowiednio skonfigurować pod kątem mniejszego opóźnienia na urządzeniach, które zgłaszają prośby o obsługę.

W przypadku Androida 4.2 (poziom interfejsu API 17) i starszych wersji wymagana jest liczba buforów wynosząca co najmniej 2, aby zmniejszyć opóźnienie. Począwszy od Androida 4.3 (poziom interfejsu API 18) liczba buforów wynosząca 1 wystarczy, aby zmniejszyć czas oczekiwania.

Wszystkie interfejsy OpenSL ES do efektów wyjściowych wykluczają krótszy czas oczekiwania.

Zalecana sekwencja:

  1. Sprawdź, czy masz interfejs API na poziomie 9 lub wyższym, by potwierdzić korzystanie z OpenSL ES.
  2. Poszukaj funkcji android.hardware.audio.low_latency, używając następującego kodu:

    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 używasz interfejsu API na poziomie 17 lub wyższym, aby potwierdzić korzystanie z: android.media.AudioManager.getProperty().
  4. Pobierz natywną lub optymalną częstotliwość próbkowania oraz rozmiar bufora dla podstawowego strumienia wyjściowego tego urządzenia, korzystając z takiego kodu:

    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);
    
    Pamiętaj, że sampleRate i framesPerBuffer to ciągi tekstowe. Najpierw sprawdź wartość null, a następnie przekonwertuj na int za pomocą funkcji Integer.parseInt().
  5. Użyj OpenSL ES, aby utworzyć odtwarzacz audio z lokalizatorem danych kolejki bufora PCM.

Uwaga: do określenia rozmiaru bufora natywnego i częstotliwości próbkowania dla aplikacji audio OpenSL ES na urządzeniu audio możesz użyć aplikacji testowej Rozmiar bufora dźwięku. Możesz też odwiedzić GitHuba, by zobaczyć przykłady audio-buffer-size.

Liczba odtwarzaczy audio z mniejszym opóźnieniem jest ograniczona. Jeśli aplikacja wymaga więcej niż kilku źródeł dźwięku, rozważ zmiksowanie dźwięku na poziomie aplikacji. Pamiętaj, aby w momencie wstrzymania działania niszczyć odtwarzacze audio, ponieważ są one globalnym zasobem udostępnianym innym aplikacjom.

Aby uniknąć zakłóceń dźwiękowych, moduł obsługi wywołania zwrotnego kolejki bufora musi być wykonywany w krótkim, przewidywalnym przedziale czasu. Zwykle oznacza to brak nieograniczonego blokowania muteksów, warunków czy operacji wejścia-wyjścia. Zamiast tego rozważ zastosowanie blokad, blokad i oczekiwania z limitami czasu oraz algorytmów nieblokujących.

Przetwarzanie wymagane do wyrenderowania następnego bufora (w przypadku AudioPlayera) lub wykorzystania poprzedniego bufora (w przypadku AudioRecord) powinno zająć mniej więcej taki sam czas przy każdym wywołaniu zwrotnym. Unikaj algorytmów, które działają w niedeterministycznym czasie lub są przerywane w obliczeniach. Obliczenie wywołania zwrotnego jest przerywane, jeśli czas pracy procesora poświęcony w danym wywołaniu zwrotnym jest znacznie dłuższy od średniej. Podsumowując, najlepiej byłoby, gdyby wariancja czasu wykonywania procesora przez moduł obsługi była bliska 0 i aby moduł obsługi nie blokował się przez czas nieograniczony.

Dźwięk z mniejszym opóźnieniem jest możliwy tylko w przypadku tych wyjść:

  • Głośniki na urządzeniu.
  • Słuchawki przewodowe.
  • Przewodowe zestawy słuchawkowe.
  • Wyrównaj.
  • Dźwięk cyfrowy USB.

Na niektórych urządzeniach opóźnienie głośnika jest większe niż na innych ścieżkach. Jest to spowodowane przetwarzaniem sygnału cyfrowego, które zapewnia korekcję i ochronę głośnika.

Od Androida 5.0 (poziom interfejsu API 21) wybrane urządzenia obsługują wejście audio z mniejszym opóźnieniem. Aby korzystać z tej funkcji, najpierw sprawdź, czy dane wyjściowe są dostępne z mniejszym czasem oczekiwania, jak opisano powyżej. Możliwość uzyskiwania danych wyjściowych z mniejszym czasem oczekiwania jest wymogiem wstępnym korzystania z funkcji wprowadzania o mniejszych opóźnieniach. Następnie utwórz Rejestrator dźwięku z taką samą częstotliwością próbkowania i rozmiarem bufora, jaki będzie używany w przypadku danych wyjściowych. Interfejsy OpenSL ES do obsługi efektów wejściowych wykluczają ścieżkę z mniejszym opóźnieniem. Aby zmniejszyć opóźnienie, należy używać gotowych ustawień SL_ANDROID_RECORDING_PRESET_VOICE_RECOGNITION. Wyłącza ono przetwarzanie sygnału cyfrowego na danym urządzeniu, które może zwiększyć opóźnienie na ścieżce wejściowej. Więcej informacji o gotowych ustawieniach nagrywania znajdziesz w sekcji Interfejs konfiguracyjny Androida powyżej.

Na potrzeby jednoczesnych danych wejściowych i wyjściowych dla każdej strony używane są osobne moduły obsługi zakończenia kolejki bufora. Nie ma gwarancji względnej kolejności wywołań zwrotnych ani synchronizacji zegarów audio, nawet jeśli obie strony używają takiej samej częstotliwości próbkowania. Aplikacja powinna buforować dane, stosując odpowiednią synchronizację bufora.

Jedną z potencjalnie niezależnych zegarów dla dźwięku jest potrzeba asynchronicznej konwersji częstotliwości próbkowania. Prosta (choć nie jest idealna w przypadku jakości dźwięku) metoda asynchronicznej konwersji częstotliwości próbkowania polega na duplikowaniu lub usuwaniu próbek w pobliżu punktu zerowego przejścia. Możliwe są bardziej złożone konwersje.

Tryby wydajności

Od Androida 7.1 (poziom interfejsu API 25) w OpenSL ES wprowadzono sposób określania trybu wydajności dla ścieżki audio. Dostępne są te opcje:

  • SL_ANDROID_PERFORMANCE_NONE: brak konkretnych wymagań dotyczących wydajności. Umożliwia stosowanie efektów sprzętowych i oprogramowanych.
  • SL_ANDROID_PERFORMANCE_LATENCY: priorytet jest traktowane jako priorytet czasu oczekiwania. Bez efektów sprzętowych i programowych. Jest to tryb domyślny.
  • SL_ANDROID_PERFORMANCE_LATENCY_EFFECTS: priorytet jest traktowane jako priorytet czasu oczekiwania, przy jednoczesnym umożliwieniu efektów sprzętowych i programowych.
  • SL_ANDROID_PERFORMANCE_POWER_SAVING: priorytet otrzymuje oszczędzanie energii. Umożliwia stosowanie efektów sprzętowych i oprogramowanych.

Uwaga: jeśli nie potrzebujesz ścieżki z małym opóźnieniem i chcesz korzystać z wbudowanych efektów dźwiękowych urządzenia (na przykład w celu poprawy jakości akustycznej odtwarzania filmu), musisz samodzielnie ustawić tryb wydajności na SL_ANDROID_PERFORMANCE_NONE.

Aby ustawić tryb wydajności, musisz wywołać SetConfiguration w interfejsie 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 i co może zrobić, bezpieczeństwo w Androidzie odbywa się na poziomie całego procesu. Kod języka programowania Java nie może robić nic więcej niż kod natywny, a kod natywny nie może robić nic więcej niż kod języka programowania Java. Jedyne różnice między nimi to dostępne interfejsy API.

Aplikacje korzystające z OpenSL ES muszą żądać uprawnień, których potrzebowaliby dla podobnych, nienatywnych interfejsów API. Jeśli na przykład aplikacja nagrywa dźwięk, potrzebuje uprawnienia android.permission.RECORD_AUDIO. Aplikacje używające efektów dźwiękowych wymagają android.permission.MODIFY_AUDIO_SETTINGS. Aplikacje odtwarzające zasoby identyfikatora URI sieci potrzebują elementu android.permission.NETWORK. Więcej informacji znajdziesz w artykule o korzystaniu z uprawnień systemowych.

W zależności od wersji platformy i implementacji parsery treści multimedialnych i kodeki programowe 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). Zniekształcone treści, które wykorzystują luki w zabezpieczeniach parserów i kodeków, to znany wektor ataku. Zalecamy odtwarzanie multimediów tylko z zaufanych źródeł lub partycjonowanie aplikacji w taki sposób, aby kod obsługujący multimedia z niezaufanych źródeł działał w środowisku piaskownicy. Możesz na przykład przetwarzać multimedia z niezaufanych źródeł w ramach osobnego procesu. Choć oba procesy będą nadal korzystać z tego samego identyfikatora UID, taka separacja utrudnia atak.