تأخیر صوتی

تأخیر زمانی است که طول می کشد تا یک سیگنال از یک سیستم عبور کند. انواع متداول تأخیر مربوط به برنامه های صوتی عبارتند از:

  • تأخیر خروجی صدا ، زمان بین تولید نمونه صوتی توسط یک برنامه و پخش نمونه از طریق جک هدفون یا بلندگوی داخلی است.
  • تأخیر ورودی صوتی ، زمان بین دریافت سیگنال صوتی توسط ورودی صوتی دستگاه، مانند میکروفون، و در دسترس بودن همان داده های صوتی برای یک برنامه است.
  • تأخیر رفت و برگشت مجموع تأخیر ورودی، زمان پردازش برنامه و تأخیر خروجی است.

  • تأخیر لمس زمان بین لمس صفحه توسط کاربر و دریافت آن رویداد لمسی توسط یک برنامه است.
  • تأخیر Warmup زمانی است که برای راه‌اندازی خط لوله صوتی در اولین باری که داده‌ها در یک بافر قرار می‌گیرند، طول می‌کشد.

این صفحه نحوه توسعه برنامه صوتی خود را با ورودی و خروجی کم تأخیر و نحوه جلوگیری از تأخیر گرم کردن توضیح می‌دهد.

تأخیر را اندازه گیری کنید

اندازه‌گیری تأخیر ورودی و خروجی صدا به صورت مجزا دشوار است زیرا نیاز به دانستن دقیق زمان ارسال اولین نمونه به مسیر صوتی دارد (اگرچه این کار را می‌توان با استفاده از یک مدار تست نور و یک اسیلوسکوپ انجام داد). اگر زمان تأخیر صوتی رفت و برگشت را می دانید، می توانید از قانون کلی استفاده کنید: تأخیر ورودی (و خروجی) صدا نصف تأخیر صوتی رفت و برگشت در مسیرهای بدون پردازش سیگنال است .

تأخیر صوتی رفت و برگشت بسته به مدل دستگاه و ساخت اندروید بسیار متفاوت است. با خواندن اندازه‌گیری‌های منتشر شده ، می‌توانید تصوری تقریبی از تأخیر رفت‌وآمد دستگاه‌های Nexus داشته باشید.

می توانید با ایجاد برنامه ای که سیگنال صوتی تولید می کند، به سیگنال گوش می دهد و زمان بین ارسال و دریافت آن را اندازه گیری می کند، تأخیر صوتی رفت و برگشت را اندازه گیری کنید.

از آنجایی که کمترین تأخیر در مسیرهای صوتی با حداقل پردازش سیگنال به دست می‌آید، ممکن است بخواهید از Audio Loopback Dongle نیز استفاده کنید که اجازه می‌دهد آزمایش از طریق اتصال هدست انجام شود.

بهترین روش ها برای به حداقل رساندن تاخیر

اعتبارسنجی عملکرد صوتی

سند تعریف سازگاری اندروید (CDD) الزامات سخت افزاری و نرم افزاری یک دستگاه Android سازگار را برمی شمرد. برای اطلاعات بیشتر در مورد برنامه سازگاری کلی به سازگاری Android و برای سند CDD واقعی به CDD مراجعه کنید.

در CDD، تأخیر رفت و برگشت 20 میلی‌ثانیه یا کمتر مشخص شده است (حتی اگر نوازندگان عموماً به 10 میلی‌ثانیه نیاز دارند). این به این دلیل است که موارد استفاده مهمی وجود دارد که با 20 میلی ثانیه فعال می شوند.

در حال حاضر هیچ API برای تعیین تأخیر صدا در هر مسیری در دستگاه Android در زمان اجرا وجود ندارد. با این حال، می‌توانید از پرچم‌های ویژگی سخت‌افزار زیر استفاده کنید تا متوجه شوید که آیا دستگاه تضمینی برای تأخیر دارد یا خیر:

معیارهای گزارش این پرچم ها در CDD در بخش های 5.6 تأخیر صوتی و 5.10 صدای حرفه ای تعریف شده است.

در اینجا نحوه بررسی این ویژگی ها در جاوا آمده است:

کاتلین

val hasLowLatencyFeature: Boolean =
        packageManager.hasSystemFeature(PackageManager.FEATURE_AUDIO_LOW_LATENCY)

val hasProFeature: Boolean =
        packageManager.hasSystemFeature(PackageManager.FEATURE_AUDIO_PRO)

جاوا

boolean hasLowLatencyFeature =
    getPackageManager().hasSystemFeature(PackageManager.FEATURE_AUDIO_LOW_LATENCY);

boolean hasProFeature =
    getPackageManager().hasSystemFeature(PackageManager.FEATURE_AUDIO_PRO);

با توجه به ارتباط ویژگی‌های صوتی، ویژگی android.hardware.audio.low_latency یک پیش نیاز برای android.hardware.audio.pro است. یک دستگاه می‌تواند android.hardware.audio.low_latency را پیاده‌سازی کند و android.hardware.audio.pro اجرا نمی‌کند، اما نه برعکس.

هیچ فرضی در مورد عملکرد صوتی نداشته باشید

مراقب مفروضات زیر برای کمک به جلوگیری از مشکلات تاخیر باشید:

  • تصور نکنید که بلندگوها و میکروفون های مورد استفاده در دستگاه های تلفن همراه عموما آکوستیک خوبی دارند. به دلیل اندازه کوچک آنها، آکوستیک عموما ضعیف است، بنابراین پردازش سیگنال برای بهبود کیفیت صدا اضافه می شود. این پردازش سیگنال تاخیر را معرفی می کند.
  • تصور نکنید که تماس های ورودی و خروجی شما هماهنگ هستند. برای ورودی و خروجی همزمان، کنترل کننده های تکمیل صف بافر جداگانه برای هر طرف استفاده می شود. هیچ تضمینی برای ترتیب نسبی این تماس‌ها یا همگام‌سازی ساعت‌های صوتی وجود ندارد، حتی زمانی که هر دو طرف از یک نرخ نمونه استفاده می‌کنند. برنامه شما باید داده ها را با همگام سازی بافر مناسب بافر کند.
  • فرض نکنید که نرخ نمونه واقعی دقیقاً با نرخ نمونه اسمی مطابقت دارد. به عنوان مثال، اگر نرخ نمونه اسمی 48000 هرتز باشد، طبیعی است که ساعت صوتی با سرعت کمی متفاوت از سیستم عامل CLOCK_MONOTONIC پیش برود. این به این دلیل است که ساعت‌های صوتی و سیستمی ممکن است از کریستال‌های مختلفی مشتق شوند.
  • فرض نکنید که نرخ نمونه پخش واقعی دقیقاً با نرخ نمونه برداری واقعی مطابقت دارد، به خصوص اگر نقاط پایانی در مسیرهای جداگانه باشند. برای مثال، اگر از میکروفون روی دستگاه با نرخ نمونه اسمی 48000 هرتز عکس می گیرید و صدای USB را با نرخ نمونه اسمی 48000 هرتز پخش می کنید، احتمالاً نرخ نمونه واقعی کمی با یکدیگر متفاوت است.

پیامد ساعت‌های صوتی بالقوه مستقل، نیاز به تبدیل نرخ نمونه ناهمزمان است. یک تکنیک ساده (اگرچه برای کیفیت صدا ایده‌آل نیست) برای تبدیل نرخ نمونه ناهمزمان، کپی کردن یا رها کردن نمونه‌ها در صورت نیاز در نزدیکی نقطه صفر است. تبدیل های پیچیده تر امکان پذیر است.

تأخیر ورودی را به حداقل برسانید

این بخش پیشنهاداتی برای کمک به کاهش تأخیر ورودی صدا در هنگام ضبط با میکروفون داخلی یا میکروفون هدست خارجی ارائه می دهد.

  • اگر برنامه شما ورودی را زیر نظر دارد، به کاربران خود پیشنهاد دهید که از هدست استفاده کنند (مثلاً با نمایش صفحه بهترین با هدفون در اولین اجرا). توجه داشته باشید که استفاده از هدست کمترین تاخیر ممکن را تضمین نمی کند. ممکن است لازم باشد مراحل دیگری را برای حذف هرگونه پردازش سیگنال ناخواسته از مسیر صوتی انجام دهید، مانند استفاده از پیش تنظیم VOICE_RECOGNITION هنگام ضبط.
  • برای کنترل نرخ نمونه اسمی 44100 و 48000 هرتز همانطور که توسط getProperty(String) برای PROPERTY_OUTPUT_SAMPLE_RATE گزارش شده است آماده باشید. سایر نرخ های نمونه ممکن است، اما نادر است.
  • برای رسیدگی به اندازه بافر گزارش شده توسط getProperty(String) برای PROPERTY_OUTPUT_FRAMES_PER_BUFFER آماده باشید. اندازه های بافر معمولی شامل 96، 128، 160، 192، 240، 256 یا 512 فریم است، اما مقادیر دیگری نیز ممکن است.

تأخیر خروجی را به حداقل برسانید

هنگام ایجاد پخش کننده صوتی خود از نرخ نمونه بهینه استفاده کنید

برای به دست آوردن کمترین تأخیر، باید داده های صوتی را ارائه دهید که با نرخ نمونه بهینه دستگاه و اندازه بافر مطابقت داشته باشد. برای اطلاعات بیشتر، به طراحی برای کاهش تاخیر مراجعه کنید.

در جاوا، همانطور که در مثال کد زیر نشان داده شده است، می توانید نرخ نمونه بهینه را از AudioManager بدست آورید:

کاتلین

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

جاوا

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

هنگامی که نرخ نمونه بهینه را می دانید، می توانید هنگام ایجاد پخش کننده خود، آن را تهیه کنید. این مثال از 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;
   ...
}

توجه: samplesPerSec به نرخ نمونه در هر کانال بر حسب میلی هرتز (1 هرتز = 1000 مگاهرتز) اشاره دارد.

از اندازه بافر بهینه برای ردیف کردن داده های صوتی استفاده کنید

با استفاده از AudioManager API می توانید اندازه بافر بهینه را به روشی مشابه با نرخ نمونه بهینه بدست آورید:

کاتلین

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

جاوا

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

ویژگی PROPERTY_OUTPUT_FRAMES_PER_BUFFER تعداد فریم های صوتی را نشان می دهد که بافر HAL (لایه انتزاعی سخت افزار) می تواند نگه دارد. شما باید بافرهای صوتی خود را طوری بسازید که مضرب دقیقی از این عدد داشته باشند. اگر از تعداد صحیح فریم های صوتی استفاده کنید، تماس های شما در فواصل زمانی معین اتفاق می افتد که باعث کاهش لرزش می شود.

استفاده از API برای تعیین اندازه بافر به جای استفاده از یک مقدار کدگذاری شده مهم است، زیرا اندازه بافر HAL در دستگاه‌ها و در ساخت‌های Android متفاوت است.

رابط های خروجی که شامل پردازش سیگنال هستند اضافه نکنید

فقط این رابط ها توسط میکسر سریع پشتیبانی می شوند:

  • SL_IID_ANDROIDSIMPLEBUFFERQUEUE
  • SL_IID_VOLUME
  • SL_IID_MUTESOLO

این رابط‌ها مجاز نیستند زیرا شامل پردازش سیگنال هستند و باعث می‌شوند درخواست شما برای یک مسیر سریع رد شود:

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

هنگامی که پخش کننده خود را ایجاد می کنید، مطمئن شوید که فقط رابط های سریع اضافه می کنید، همانطور که در مثال زیر نشان داده شده است:

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

بررسی کنید که از مسیری با تأخیر کم استفاده می‌کنید

این مراحل را تکمیل کنید تا مطمئن شوید که یک آهنگ با تاخیر کم را با موفقیت به دست آورده اید:

  1. برنامه خود را اجرا کنید و سپس دستور زیر را اجرا کنید:
  2. adb shell ps | grep your_app_name
    
  3. شناسه فرآیند برنامه خود را یادداشت کنید.
  4. اکنون، مقداری صدا از برنامه خود پخش کنید. شما تقریباً سه ثانیه فرصت دارید تا دستور زیر را از ترمینال اجرا کنید:
  5. adb shell dumpsys media.audio_flinger
    
  6. شناسه فرآیند خود را اسکن کنید. اگر F را در ستون Name مشاهده کردید، در یک مسیر کم تأخیر قرار دارد (F مخفف مسیر سریع است).

تأخیر گرم کردن را به حداقل برسانید

وقتی برای اولین بار داده های صوتی را در نوبت قرار می دهید، مدت زمان کمی، اما همچنان قابل توجهی طول می کشد تا مدار صوتی دستگاه گرم شود. برای جلوگیری از این تأخیر گرم کردن، می‌توانید بافرهای داده‌های صوتی حاوی سکوت را در صف قرار دهید، همانطور که در مثال کد زیر نشان داده شده است:

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

در نقطه ای که باید صدا تولید شود، می توانید به بافرهای صف حاوی داده های صوتی واقعی بروید.

توجه: خروجی مداوم صدا مصرف انرژی قابل توجهی دارد. مطمئن شوید که خروجی را در متد onPause() متوقف کرده اید. همچنین توقف خروجی بی صدا را پس از مدتی عدم فعالیت کاربر در نظر بگیرید.

کد نمونه اضافی

برای دانلود یک برنامه نمونه که تأخیر صوتی را نشان می‌دهد، به نمونه‌های NDK مراجعه کنید.

برای اطلاعات بیشتر

  1. تأخیر صوتی برای توسعه دهندگان برنامه
  2. مشارکت کنندگان در تأخیر صوتی
  3. اندازه گیری تاخیر صدا
  4. گرم کردن صدا
  5. تأخیر (صوتی)
  6. زمان تاخیر رفت و برگشت