Opóźnienie dźwięku

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:

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:

  1. Uruchom aplikację, a następnie uruchom to polecenie:
  2. adb shell ps | grep your_app_name
    
  3. Zanotuj identyfikator procesu aplikacji.
  4. Teraz odtwórz dźwięk z aplikacji. Masz około 3 sekundy na uruchomienie w terminalu tego polecenia:
  5. adb shell dumpsys media.audio_flinger
    
  6. 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ą).

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

  1. Opóźnienie dźwięku dla deweloperów aplikacji
  2. Współtwórcy dźwięku z opóźnieniami
  3. Pomiar opóźnienia dźwięku
  4. Przygotowanie dźwięku
  5. Opóźnienie (dźwięk)
  6. Czas opóźnienia w obie strony