Độ trễ âm thanh

Độ trễ là thời gian cần thiết để một tín hiệu đi qua một hệ thống. Sau đây là các loại độ trễ phổ biến liên quan đến ứng dụng âm thanh:

  • Độ trễ đầu ra âm thanh (audio output latency) là khoảng thời gian tính từ lúc một mẫu âm thanh được một ứng dụng tạo ra đến lúc mẫu đó được phát thông qua giắc tai nghe hoặc loa tích hợp.
  • Độ trễ đầu vào âm thanh (audio input latency) là khoảng thời gian tính từ khi thiết bị nhận được tín hiệu âm thanh qua thiết bị đầu vào (chẳng hạn như micrô) cho đến lúc dữ liệu âm thanh đó sẵn sàng dùng được trên một ứng dụng.
  • Độ trễ trọn vòng (round-trip latency)là tổng của độ trễ đầu vào, thời gian xử lý của ứng dụng và độ trễ đầu ra.

  • Độ trễ chạm (touch latency) là thời gian từ lúc người dùng nhấn vào màn hình cho đến khi ứng dụng nhận được sự kiện chạm.
  • Độ trễ khởi động (warmup latency) là thời gian để khởi động quy trình âm thanh trong lần đầu dữ liệu được đưa vào hàng đợi trong bộ đệm.

Trang này mô tả cách phát triển ứng dụng âm thanh có độ trễ đầu vào và đầu ra thấp cũng như cách tránh độ trễ khởi động.

Đo độ trễ

Rất khó để đo lường riêng độ trễ đầu vào và đầu ra cho âm thanh vì việc này đòi hỏi bạn phải biết chính xác thời điểm mẫu đầu tiên được gửi đến đường dẫn âm thanh (mặc dù bạn có thể thực hiện bằng cách sử dụng bộ mạch kiểm tra ánh sáng và một dao động ký). Nếu biết độ trễ âm thanh trọn vòng, bạn có thể sử dụng quy tắc chung là: độ trễ đầu vào (và đầu ra) âm thanh là một nửa độ trễ âm thanh trọn vòng trên các đường dẫn mà không tính đến việc xử lý tín hiệu.

Độ trễ âm thanh trọn vòng thay đổi đáng kể tuỳ thuộc vào mẫu thiết bị và bản dựng Android. Bạn có nắm sơ bộ khái niệm độ trễ trọn vòng đối với thiết bị Nexus bằng cách đọc số liệu đo lường đã xuất bản.

Bạn có thể đo độ trễ âm thanh trọn vòng bằng cách xây dựng một ứng dụng tạo ra tín hiệu âm thanh, nghe tín hiệu đó rồi đo thời gian từ khi gửi đến lúc nhận tín hiệu. Ngoài ra, bạn có thể cài đặt ứng dụng kiểm tra độ trễ này. Ứng dụng này thực hiện việc kiểm tra độ trễ khứ hồi bằng cách sử dụng bài kiểm tra Larsen. Bạn cũng có thể xem mã nguồn của ứng dụng kiểm tra độ trễ này.

Vì độ trễ thấp nhất đạt được trên đường dẫn âm thanh có mức xử lý tín hiệu tối thiểu, nên bạn cũng nên sử dụng Audio Loopback Dongle (thiết bị kiểm tra vòng lặp âm thanh). Thiết bị này cho phép thực hiện bài kiểm tra trên bộ kết nối tai nghe.

Các phương pháp hay nhất để giảm thiểu độ trễ

Xác thực hiệu năng âm thanh

Tài liệu định nghĩa về khả năng tương thích (Compatibility Definition Document – CDD) cho Android có liệt kê các yêu cầu về phần cứng và phần mềm đối với thiết bị Android tương thích. Xem nội dung Khả năng tương thích với Android để biết thêm thông tin về chương trình tương thích tổng thể và truy cập CDD để xem tài liệu CDD thực tế.

Trong CDD, độ trễ trọn vòng được chỉ định ở mức 20 mili giây trở xuống (mặc dù các nhạc sĩ thường yêu cầu 10 mili giây). Lý do là có một số trường hợp sử dụng quan trọng được bật ở mức 20 mili giây.

Hiện không có API nào để xác định độ trễ âm thanh ở bất kỳ đường dẫn nào trên thiết bị Android trong thời gian chạy. Tuy nhiên, bạn có thể sử dụng các cờ tính năng phần cứng sau đây để tìm hiểu xem thiết bị có đảm bảo về độ trễ hay không:

Tiêu chí báo cáo các cờ này được xác định trong CDD ở các mục 5.6 Audio Latency (5.6 Độ trễ âm thanh) và 5.10 Professional Audio (5.10 Âm thanh chuyên nghiệp).

Sau đây là cách kiểm tra các tính năng này trong Java:

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);

Về mối quan hệ của các tính năng âm thanh, tính năng android.hardware.audio.low_latency là điều kiện tiên quyết để sử dụng android.hardware.audio.pro. Một thiết bị có thể triển khai android.hardware.audio.low_latency và không triển khai android.hardware.audio.pro, nhưng không thể thực hiện ngược lại.

Đừng giả định về hiệu năng âm thanh

Hãy thận trọng trước những giả định sau đây để giúp tránh các vấn đề về độ trễ:

  • Đừng giả định rằng loa và micrô dùng trong thiết bị di động thường có độ vang âm tốt. Do loa và micrô dùng trong thiết bị di động có kích thước nhỏ, độ vang âm thường kém, cho nên tính năng xử lý tín hiệu được thêm vào để cải thiện chất lượng âm thanh. Quá trình xử lý tín hiệu này gây ra độ trễ.
  • Đừng giả định rằng các lệnh gọi lại đầu vào và đầu ra của bạn được đồng bộ hoá. Đối với đầu vào và đầu ra đồng thời, mỗi bên dùng một trình xử lý hoàn thành hàng đợi bộ đệm riêng biệt. Không thể đảm bảo thứ tự tương đối của các lệnh gọi lại này hoặc tính năng đồng bộ hoá các xung nhịp âm thanh, ngay cả khi cả hai bên sử dụng cùng một tốc độ lấy mẫu. Ứng dụng của bạn nên lưu dữ liệu vào bộ đệm bằng tính năng đồng bộ hoá bộ đệm thích hợp.
  • Đừng giả định rằng tốc độ lấy mẫu thực tế khớp chính xác với tốc độ lấy mẫu danh nghĩa. Ví dụ: nếu tốc độ lấy mẫu danh nghĩa là 48.000 Hz, việc xung nhịp âm thanh sẽ chuyển sang tốc độ hơi khác so với CLOCK_MONOTONIC của hệ điều hành là điều bình thường. Lý do là xung nhịp âm thanh và xung nhịp hệ thống có thể bắt nguồn từ các tinh thể khác biệt.
  • Đừng giả định rằng tốc độ lấy mẫu phát lại thực tế khớp chính xác với tốc độ lấy mẫu thu thập thực tế, đặc biệt là khi các điểm cuối nằm trên các đường dẫn riêng biệt. Ví dụ: nếu bạn đang thu âm qua micrô trên thiết bị ở tốc độ lấy mẫu danh nghĩa 48.000 Hz và đang phát trên kênh âm thanh USB ở tốc độ lấy mẫu danh nghĩa 48.000 Hz, thì tốc độ lấy mẫu thực tế ở mỗi bên có thể sẽ hơi khác nhau.

Hệ quả của các xung nhịp âm thanh độc lập tiềm ẩn là cần phải chuyển đổi tốc độ lấy mẫu không đồng bộ. Kỹ thuật đơn giản (mặc dù không lý tưởng cho chất lượng âm thanh) để chuyển đổi tốc độ lấy mẫu không đồng bộ là sao chép (hoặc bỏ các mẫu khi cần thiết) gần một điểm về 0 (zero-crossing point). Bạn cũng có thể áp dụng các kỹ thuật chuyển đổi phức tạp hơn.

Giảm thiểu độ trễ đầu vào

Phần này đưa ra các đề xuất để giúp bạn giảm độ trễ đầu vào âm thanh khi ghi âm bằng micrô tích hợp hoặc micrô của tai nghe bên ngoài.

  • Nếu ứng dụng của bạn đang theo dõi đầu vào, hãy đề xuất người dùng sử dụng một tai nghe (ví dụ: bằng cách hiện màn hình Tốt nhất nên dùng với tai nghe trong lần chạy đầu tiên). Xin lưu ý rằng việc chỉ sử dụng tai nghe không đảm bảo độ trễ thấp nhất có thể. Có thể bạn phải thực hiện các bước khác để loại bỏ mọi quá trình xử lý tín hiệu không mong muốn khỏi đường dẫn âm thanh, chẳng hạn như bằng cách sử dụng giá trị đặt trước VOICE_RECOGNITION khi ghi âm.
  • Chuẩn bị để xử lý tốc độ lấy mẫu danh nghĩa là 44.100 và 48.000 Hz theo báo cáo của getProperty(String) cho PROPERTY_OUTPUT_SAMPLE_RATE. Cũng có trường hợp có tốc độ lấy mẫu khác nhưng khá hiếm.
  • Chuẩn bị để xử lý kích thước bộ nhớ đệm mà getProperty(String) báo cáo cho PROPERTY_OUTPUT_FRAMES_PER_BUFFER. Thường thì kích thước bộ nhớ đệm là 96, 128, 160, 192, 240, 256, hoặc 512 khung, nhưng có thể có các giá trị khác.

Giảm thiểu độ trễ đầu ra

Sử dụng tốc độ lấy mẫu tối ưu khi bạn tạo trình phát âm thanh

Để có được độ trễ thấp nhất, bạn phải cung cấp dữ liệu âm thanh khớp với tốc độ lấy mẫu và kích thước bộ đệm tối ưu của thiết bị. Để biết thêm thông tin, hãy xem nội dung Thiết kế để giảm độ trễ.

Trong Java, bạn có thể có được tốc độ lấy mẫu tối ưu qua AudioManager như trình bày trong đoạn mã ví dụ sau:

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

Khi đã biết tốc độ lấy mẫu tối ưu, bạn có thể cung cấp tốc độ đó khi tạo trình phát. Ví dụ này sử dụng 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;
   ...
}

Lưu ý: samplesPerSectốc độ lấy mẫu mỗi kênh (tính bằng mili giây) (1 Hz = 1000 mHz).

Sử dụng kích thước bộ nhớ đệm tối ưu để thêm dữ liệu âm thanh vào hàng đợi

Bạn có thể lấy kích thước bộ đệm tối ưu theo cách tương tự như tốc độ mẫu tối ưu, bằng cách sử dụng 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

Thuộc tính PROPERTY_OUTPUT_FRAMES_PER_BUFFER cho biết số khung âm thanh mà bộ đệm HAL (Hardware Abstraction Layer – Lớp trừu tượng phần cứng) có thể chứa. Bạn nên xây dựng các bộ đệm âm thanh sao cho có thể chứa bội số chính xác của con số này. Nếu bạn sử dụng số lượng khung âm thanh chính xác, thì các lệnh gọi lại của bạn sẽ diễn ra định kỳ, giúp làm giảm hiện tượng dao động.

Quan trọng là bạn phải sử dụng API để xác định kích thước bộ đệm thay vì dùng giá trị mã hoá cứng, vì kích thước bộ đệm HAL còn thay đổi tuỳ theo thiết bị và bản dựng Android.

Đừng thêm các giao diện đầu ra liên quan đến hoạt động xử lý tín hiệu

Bộ trộn nhanh chỉ hỗ trợ các giao diện này:

  • SL_IID_ANDROIDSIMPLEBUFFERQUEUE
  • SL_IID_VOLUME
  • SL_IID_MUTESOLO

Các giao diện sau đây thì không được cho phép vì có liên quan đến hoạt động xử lý tín hiệu và sẽ khiến yêu cầu triển khai nhanh của bạn bị từ chối:

  • SL_IID_BASSBOOST
  • SL_IID_EFFECTSEND
  • SL_IID_ENVIRONMENTALREVERB
  • SL_IID_EQUALIZER
  • SL_IID_PLAYBACKRATE
  • SL_IID_PRESETREVERB
  • SL_IID_VIRTUALIZER
  • SL_IID_ANDROIDEFFECT
  • SL_IID_ANDROIDEFFECTSEND

Khi bạn tạo trình phát, hãy nhớ chỉ thêm các giao diện nhanh, như trong ví dụ sau:

const SLInterfaceID interface_ids[2] = { SL_IID_ANDROIDSIMPLEBUFFERQUEUE, SL_IID_VOLUME };

Xác minh rằng bạn đang sử dụng bản nhạc có độ trễ thấp

Hãy hoàn tất các bước sau để xác minh rằng bạn đã có được một bản nhạc có độ trễ thấp:

  1. Chạy ứng dụng của bạn rồi chạy lệnh sau:
  2. adb shell ps | grep your_app_name
    
  3. Ghi lại mã quy trình của ứng dụng.
  4. Bây giờ, hãy phát một số âm thanh qua ứng dụng của bạn. Bạn có khoảng 3 giây để chạy lệnh sau từ thiết bị đầu cuối:
  5. adb shell dumpsys media.audio_flinger
    
  6. Quét tìm mã nhận dạng quy trình. Nếu bạn thấy một chữ F trong cột Name (Tên) thì quy trình đó là trên một bản nhạc có độ trễ thấp (F là viết tắt của fast track – nhanh).

Giảm thiểu độ trễ khởi động

Trong lần đầu bạn đưa dữ liệu âm thanh vào hàng đợi, có thể sẽ mất một chút thời gian (nhưng vẫn đáng kể) để mạch âm thanh trên thiết bị khởi động. Để tránh độ trễ khởi động này, bạn có thể đưa các bộ đệm chứa dữ liệu âm thanh có khoảng lặng vào hàng đợi, như thể hiện trong đoạn mã ví dụ sau:

#define CHANNELS 1
static short* silenceBuffer;
int numSamples = frames * CHANNELS;
silenceBuffer = malloc(sizeof(*silenceBuffer) * numSamples);
    for (i = 0; i<numSamples; i++) {
        silenceBuffer[i] = 0;
    }

Tại thời điểm cần tạo âm thanh, bạn có thể chuyển sang xếp hàng các bộ đệm chứa dữ liệu âm thanh thực.

Lưu ý: Việc liên tục nhập âm thanh sẽ làm tiêu hao pin một cách đáng kể. Hãy đảm bảo bạn dừng tín hiệu đầu ra trong phương thức onpause(). Bạn cũng nên cân nhắc việc tạm dừng tín hiệu đầu ra im lặng sau một khoảng thời gian người dùng không hoạt động.

Mã nguồn mẫu khác

Để tải một ứng dụng mẫu cho thấy độ trễ âm thanh, hãy xem nội dung Các mẫu NDK.

Thông tin khác

  1. Thông tin về độ trễ âm thanh dành cho nhà phát triển ứng dụng
  2. Các yếu tố tạo nên độ trễ âm thanh
  3. Đo lường độ trễ âm thanh
  4. Khởi động âm thanh
  5. Độ trễ (âm thanh)
  6. Thời gian trễ trọn vòng