Opóźnienie to czas potrzebny na przesłanie sygnału przez system. Oto typowe opóźnienia w aplikacjach audio:
- Opóźnienie wyjścia audio to czas upływający między wygenerowaniem próbki audio przez aplikację a odtworzeniem próbki przez gniazdo słuchawek lub wbudowany głośnik.
- Opóźnienie wejścia audio to czas upływający między sygnałem dźwiękowym odebranym przez wejście audio urządzenia (np. mikrofonem) a dostępem do tych samych danych audio aplikacji.
Czas oczekiwania w obie strony to suma czasu oczekiwania na dane wejściowe, czasu przetwarzania aplikacji i opóźnień wyjściowych.
- Opóźnienie dotknięcia to czas upływający między dotknięciem ekranu przez użytkownika a odebraniem przez aplikację tego zdarzenia.
- Czas oczekiwania na przygotowanie to czas potrzebny do uruchomienia potoku audio po pierwszym umieszczeniu danych w kolejce w buforze.
Z tego artykułu dowiesz się, jak utworzyć aplikację audio z małym opóźnieniem wejściowym i wyjściowym oraz jak uniknąć opóźnień na rozgrzewce.
Zmierz opóźnienie
Trudno osobno mierzyć opóźnienie wejścia i wyjścia dźwięku, ponieważ wymaga to sprawdzenia, kiedy pierwsza próbka jest wysyłana do ścieżki audio (chociaż można to zrobić za pomocą obwodu do testowania światła i oscyloskopu). Jeśli znasz opóźnienie dźwięku w obie strony, możesz skorzystać z ogólnej zasady: opóźnienie w przypadku danych wejściowych (i wyjściowych) jest o połowę mniejsze w przypadku ścieżek bez przetwarzania sygnału.
Opóźnienie dźwięku w obie strony różni się znacznie w zależności od modelu urządzenia i kompilacji Androida. Informacje o czasie oczekiwania w obie strony w przypadku urządzeń Nexus znajdziesz w opublikowanych pomiarach.
Możesz zmierzyć opóźnienie dźwięku w obie strony, tworząc aplikację, która generuje sygnał audio, go nasłuchuje i mierzy czas upływający między jego wysłaniem a odebraniem.
Najniższe opóźnienie osiąga najniższe opóźnienie w przypadku ścieżek audio przy minimalnym przetwarzaniu sygnału, dlatego warto też użyć klucza sprzężenia zwrotnego audio, który pozwala na przeprowadzanie testu przez złącze zestawu słuchawkowego.
Sprawdzone metody minimalizacji opóźnienia
Sprawdź wydajność dźwięku
Dokument CDD (Android Compatibility Definition Document) zawiera wymagania dotyczące sprzętu i oprogramowania zgodnego urządzenia z Androidem. Więcej informacji na temat ogólnego programu zgodności znajdziesz w artykule na temat zgodności z Androidem, a dokumentu dotyczącego dokumentu CDD znajdziesz na stronie CDD.
W dokumencie CDD czas oczekiwania w obie strony jest określony na poziomie 20 ms lub krótszym (mimo że muzycy zwykle wymagają 10 ms). Dzieje się tak, ponieważ istnieją ważne przypadki użycia, które można włączyć do 20 ms.
Obecnie nie ma interfejsu API, który określałby opóźnienie dźwięku w dowolnej ścieżce na urządzeniu z Androidem w czasie działania. Możesz jednak użyć poniższych flag funkcji sprzętowych, aby dowiedzieć się, czy urządzenie gwarantuje opóźnienia:
-
android.hardware.audio.low_latency
oznacza ciągłe opóźnienie wyjściowe wynoszące maksymalnie 45 ms. -
android.hardware.audio.pro
oznacza ciągłe opóźnienie w obie strony nie większe niż 20 ms.
Kryteria zgłaszania tych zgłoszeń są określone w dokumencie CDD w sekcjach Opóźnienie dźwięku 5.6 i 5.10 Profesjonalny dźwięk.
Aby sprawdzić dostępność tych funkcji w Javie:
Kotlin
val hasLowLatencyFeature: Boolean = packageManager.hasSystemFeature(PackageManager.FEATURE_AUDIO_LOW_LATENCY) val hasProFeature: Boolean = packageManager.hasSystemFeature(PackageManager.FEATURE_AUDIO_PRO)
Java
boolean hasLowLatencyFeature = getPackageManager().hasSystemFeature(PackageManager.FEATURE_AUDIO_LOW_LATENCY); boolean hasProFeature = getPackageManager().hasSystemFeature(PackageManager.FEATURE_AUDIO_PRO);
Jeśli chodzi o związek między funkcjami audio, warunkiem wstępnym android.hardware.audio.pro
jest użycie funkcji android.hardware.audio.low_latency
. Urządzenie może zaimplementować android.hardware.audio.low_latency
, a nie android.hardware.audio.pro
, ale nie odwrotnie.
Nie zakładaj żadnych zasad dotyczących jakości dźwięku
Aby uniknąć problemów z opóźnieniami, przestrzegaj tych zasad:
- Nie zakładaj, że głośniki i mikrofony używane w urządzeniach mobilnych zwykle mają dobrą akustykę. Ze względu na nieduży rozmiar akustyka jest zwykle słaba, dlatego w celu poprawy jakości dźwięku dodajemy przetwarzanie sygnału. Przetwarzanie sygnału wiąże się z opóźnieniem.
- Nie zakładaj, że wejściowe i wyjściowe wywołania zwrotne są synchronizowane. 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ą tej samej częstotliwości próbkowania. Aplikacja powinna buforować dane za pomocą odpowiedniej synchronizacji bufora.
- Nie zakładaj, że rzeczywista częstotliwość próbkowania dokładnie odpowiada nominalnej częstotliwości próbkowania. Na przykład, jeśli nominalna częstotliwość próbkowania wynosi 48 000 Hz,to normalne, że zegar audio przyspiesza w nieco inny sposób niż w systemie operacyjnym
CLOCK_MONOTONIC
. Wynika to z faktu, że dźwięk i zegary systemowe mogą pochodzić z różnych kryształów. - Nie zakładaj, że rzeczywista częstotliwość próbkowania odtwarzania dokładnie odpowiada rzeczywistej częstotliwości próbkowania przechwytywania, zwłaszcza jeśli punkty końcowe znajdują się na osobnych ścieżkach. Jeśli np. nagrywasz z mikrofonu na urządzeniu z nominalną częstotliwością próbkowania 48 000 Hz i grasz na dysku USB z nominalną częstotliwością próbkowania 48 000 Hz, rzeczywiste współczynniki próbkowania mogą się nieco różnić od siebie.
W efekcie potencjalnie niezależnych zegarów dźwiękowych konieczne jest stosowanie 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. Bardziej zaawansowane konwersje są możliwe.
Minimalizuj opóźnienie sygnału wejściowego
Ta sekcja zawiera sugestie, które pomogą Ci zmniejszyć opóźnienie wejścia audio podczas nagrywania przy użyciu wbudowanego mikrofonu lub zewnętrznego mikrofonu w zestawie słuchawkowym.
- Jeśli aplikacja monitoruje dane wejściowe, zasugeruj użytkownikom korzystanie z zestawu słuchawkowego (np. przez wyświetlanie ekranu Najlepsze ze słuchawkami przy pierwszym uruchomieniu). Pamiętaj, że samo używanie zestawu słuchawkowego nie gwarantuje najniższego możliwego opóźnienia. Być może trzeba będzie wykonać inne czynności, aby usunąć niepożądane przetwarzanie sygnału ze ścieżki audio, np. użyć gotowego ustawienia
VOICE_RECOGNITION
podczas nagrywania. - Przygotuj się na obsługę nominalnych częstotliwości próbkowania 44 100 i 48 000 Hz zgodnie z raportem getProperty(String) dotyczącym PROPERTY_OUTPUT_SAMPLE_RATE. Inne współczynniki próbkowania są możliwe, ale rzadkie.
- Przygotuj się na obsługę rozmiaru bufora zgłoszonego przez getProperty(String) dla PROPERTY_OUTPUT_FRAMES_PER_BUFFER. Typowe rozmiary bufora to 96, 128, 160, 192, 240, 256 i 512 klatek, ale możliwe są też inne wartości.
Minimalizuj opóźnienie wyjścia
Używaj optymalnej częstotliwości próbkowania podczas tworzenia odtwarzacza audio
Aby uzyskać najniższy czas oczekiwania, musisz dostarczyć dane audio, które odpowiadają optymalnej częstotliwości próbkowania na urządzeniu i rozmiarowi bufora. Więcej informacji znajdziesz w artykule Projektowanie z myślą o krótszym czasie oczekiwania.
Optymalną częstotliwość próbkowania w Javie możesz uzyskać z usługi AudioManager, jak pokazano w tym przykładowym kodzie:
Kotlin
val am = getSystemService(Context.AUDIO_SERVICE) as AudioManager val sampleRateStr: String? = am.getProperty(AudioManager.PROPERTY_OUTPUT_SAMPLE_RATE) var sampleRate: Int = sampleRateStr?.let { str -> Integer.parseInt(str).takeUnless { it == 0 } } ?: 44100 // Use a default value if property not found
Java
AudioManager am = (AudioManager) getSystemService(Context.AUDIO_SERVICE); String sampleRateStr = am.getProperty(AudioManager.PROPERTY_OUTPUT_SAMPLE_RATE); int sampleRate = Integer.parseInt(sampleRateStr); if (sampleRate == 0) sampleRate = 44100; // Use a default value if property not found
Znając optymalną częstotliwość próbkowania, możesz podać ją podczas tworzenia odtwarzacza. W tym przykładzie użyto OpenSL ES:
// create buffer queue audio player void Java_com_example_audio_generatetone_MainActivity_createBufferQueueAudioPlayer (JNIEnv* env, jclass clazz, jint sampleRate, jint framesPerBuffer) { ... // specify the audio source format SLDataFormat_PCM format_pcm; format_pcm.numChannels = 2; format_pcm.samplesPerSec = (SLuint32) sampleRate * 1000; ... }
Uwaga: samplesPerSec
odnosi się do częstotliwości próbkowania na kanał w milihercach (1 Hz = 1000 mHz).
Użyj optymalnego rozmiaru bufora, aby umieścić dane audio w kolejce
Optymalny rozmiar bufora można uzyskać w sposób podobny do optymalnej częstotliwości próbkowania za pomocą interfejsu AudioManager API:
Kotlin
val am = getSystemService(Context.AUDIO_SERVICE) as AudioManager val framesPerBuffer: String? = am.getProperty(AudioManager.PROPERTY_OUTPUT_FRAMES_PER_BUFFER) var framesPerBufferInt: Int = framesPerBuffer?.let { str -> Integer.parseInt(str).takeUnless { it == 0 } } ?: 256 // Use default
Java
AudioManager am = (AudioManager) getSystemService(Context.AUDIO_SERVICE); String framesPerBuffer = am.getProperty(AudioManager.PROPERTY_OUTPUT_FRAMES_PER_BUFFER); int framesPerBufferInt = Integer.parseInt(framesPerBuffer); if (framesPerBufferInt == 0) framesPerBufferInt = 256; // Use default
Właściwość
PROPERTY_OUTPUT_FRAMES_PER_BUFFER
wskazuje liczbę klatek audio, które może zapisać bufor HAL (Hardware Abstraction Layer). Bufory dźwięku należy utworzyć tak,
aby zawierały dokładną wielokrotność tej liczby. Jeśli użyjesz prawidłowej liczby klatek audio, wywołania zwrotne będą wykonywane w regularnych odstępach czasu, co zmniejsza zakłócenia.
Ważne jest, aby do określania rozmiaru bufora używać interfejsu API, a nie wartości zakodowanej na stałe, ponieważ rozmiar bufora HAL różni się w zależności od urządzenia i kompilacji Androida.
Nie dodawaj interfejsów wyjściowych, które obejmują przetwarzanie sygnałów
Szybki mikser obsługuje tylko te interfejsy:
- SL_IID_ANDROIDSIMPLEBUFFERQUEUE
- SL_IID_VOLUME
- SL_IID_MUTESOLO
Te interfejsy są niedozwolone, ponieważ obejmują przetwarzanie sygnałów i spowodują odrzucenie prośby o szybką ścieżkę:
- SL_IID_BASSBOOST
- SL_IID_EFFECTSEND
- SL_IID_ENVIRONMENTALREVERB
- SL_IID_EQUALIZER
- SL_IID_PLAYBACKRATE
- SL_IID_PRESETREVERB
- SL_IID_VIRTUALIZER
- SL_IID_ANDROIDIZE
- SL_IID_ANDROIDIZESEND
Pamiętaj, by przy tworzeniu odtwarzacza dodawać tylko interfejsy szybkie, jak w tym przykładzie:
const SLInterfaceID interface_ids[2] = { SL_IID_ANDROIDSIMPLEBUFFERQUEUE, SL_IID_VOLUME };
Sprawdzanie, czy używasz ścieżki z małym opóźnieniem
Wykonaj te czynności, aby sprawdzić, czy udało się uzyskać ścieżkę z niewielkimi opóźnieniami:
- Uruchom aplikację, a następnie uruchom to polecenie:
- Zanotuj identyfikator procesu aplikacji.
- Teraz odtwórz dźwięk z aplikacji. Masz około 3 sekundy na uruchomienie w terminalu tego polecenia:
- Zeskanuj go, aby znaleźć identyfikator procesu. Jeśli w kolumnie Nazwa widzisz literę F, oznacza to, że ścieżka działa z krótkim czasem oczekiwania (litera F oznacza ścieżkę szybką).
adb shell ps | grep your_app_name
adb shell dumpsys media.audio_flinger
Minimalizuj czas oczekiwania na rozgrzewkę
Gdy umieścisz dane w kolejce po raz pierwszy, nagrzewanie się obwodu audio urządzenia wymaga niewielkiego, ale wciąż znaczącego czasu. Aby uniknąć tego opóźnienia na rozgrzewkę, możesz umieszczać w kolejce bufory danych dźwiękowych zawierające ciszę, jak w tym przykładzie kodu:
#define CHANNELS 1 static short* silenceBuffer; int numSamples = frames * CHANNELS; silenceBuffer = malloc(sizeof(*silenceBuffer) * numSamples); for (i = 0; i<numSamples; i++) { silenceBuffer[i] = 0; }
W momencie, gdy chcesz utworzyć dźwięk, możesz przełączyć się na buforowanie buforowania zawierającego rzeczywiste dane audio.
Uwaga: ciągłe odtwarzanie dźwięku skutkuje znacznym zużyciem energii. Pamiętaj o zatrzymaniu danych wyjściowych w metodzie onPause(). Możesz też wstrzymać ciche urządzenie wyjściowe po pewnym czasie braku aktywności użytkownika.
Dodatkowy przykładowy kod
Przykładową aplikację z opóźnieniem dźwięku znajdziesz tutaj: NDK Sample.
Więcej informacji
- Opóźnienie dźwięku dla deweloperów aplikacji
- Współtwórcy dźwięku z opóźnieniami
- Pomiar opóźnienia dźwięku
- Przygotowanie dźwięku
- Opóźnienie (dźwięk)
- Czas opóźnienia w obie strony