Синхронізація гри зі звуком та музикою

Вступ

У будь-якому додатку, або грі, відтворення звуку і музики матиме невелику затримку. Для ігор ця затримка часто настільки мала, що немає значення. Звукові ефекти з'являться через кілька мілісекунд після виклику будь-якої функції play(). Для музики це не має значення, оскільки в більшості ігор вона не взаємодіє з ігровим процесом.

Тим не менш, для деяких ігор (в основному, ритм-ігор) може знадобитися синхронізація дій гравця з чимось, що відбувається в пісні (зазвичай синхронізовано з BPM). Для цього корисно мати точнішу інформацію про час позиції відтворення.

Досягти дуже малої різниці часу важко. Це пов'язано з тим, що під час відтворення звуку відтворюється ще багато факторів:

  • Звук змішується в шматки (не безперервно), в залежності від розміру використовуваних аудіо буферів (перевірте затримки в параметрах проекту).

  • Змішані фрагменти звуку відтворюються не відразу.

  • Графічні API відображаються із запізненням на два-три кадри.

  • При відтворенні на телевізорах може бути додана деяка затримка через обробку зображень.

Найпоширенішим способом зменшення затримки є зменшення звукових буферів (знову ж таки, шляхом редагування параметра затримки в налаштуваннях проекту). Проблема в тому, що коли затримка занадто мала, змішування звуку зажадає значно потужнішого процесора. Це збільшує ризик пропуску (тріщини в звучанні, тому що зворотний виклик міксу був втрачений).

Це загальний компроміс, тому Godot поставляється з розумними дефолтами, які не треба міняти.

Проблема, врешті-решт, не в цій невеликій затримці, а в синхронізації графіки та звуку в іграх, які цього вимагають. Починаючи з Godot 3.2, були додані деякі помічники, для отримування точнішого часу відтворення.

Використання системного годинника для синхронізації

Як згадувалося раніше, Якщо ви викликаєте AudioStreamPlayer.play(), звук почнеться не відразу, а коли звуковий потік обробить наступний фрагмент.

Цієї затримки не уникнути, але її можна вирахувати викликавши AudioServer.get_time_to_next_mix().

Затримку виводу (що відбувається після змішування) також можна вирахувати викликавши AudioServer.get_output_latency().

Додайте ці дві функції, і зможете майже точно вгадати, коли звук, або музика, почнуть відтворюватися в динаміках під час виконання _process():

var time_begin
var time_delay


func _ready():
    time_begin = OS.get_ticks_usec()
    time_delay = AudioServer.get_time_to_next_mix() + AudioServer.get_output_latency()
    $Player.play()


func _process(delta):
    # Obtain from ticks.
    var time = (OS.get_ticks_usec() - time_begin) / 1000000.0
    # Compensate for latency.
    time -= time_delay
    # May be below 0 (did not begin yet).
    time = max(0, time)
    print("Time is: ", time)

У довгостроковій перспективі, однак, оскільки звуковий апаратний годинник ніколи точно не синхронізується з системним годинником, інформація про хронометраж повільно відхиляється.

Для ритм-гри, де пісня починається і закінчується через кілька хвилин, цей підхід прекрасний (і це рекомендований підхід). Для гри, де відтворення може тривати набагато довше, гра в кінцевому підсумку вийде з синхронізації, тому потрібен інший підхід.

Використання звукового апаратного годинника для синхронізації

Використання AudioStreamPlayer.get_playback_position() для отримання поточної позиції відтворення пісні здається ідеальним, але це не зовсім так. Це значення буде збільшуватися в фрагментах (кожен раз, при обробці нового блоку звуку), тому багато викликів можуть повертати одне і те ж значення. Окрім цього, значення також буде не синхронізоване з динаміками через раніше згадані причини.

Компенсувати "фрагментований" вивід може допомогти функція: AudioServer.get_time_since_last_mix().

Додавання повернутого значення з цієї функції до get_playback_position() збільшує точність:

var time = $Player.get_playback_position() + AudioServer.get_time_since_last_mix()

Щоб збільшити точність, відніміть інформацію про затримку (скільки потрібно для того, щоб звук був почутий після його змішування):

var time = $Player.get_playback_position() + AudioServer.get_time_since_last_mix() - AudioServer.get_output_latency()

Результат може бути трохи тремтячим через те, як працюють кілька потоків. Просто переконайтеся, що значення не менше, ніж у попередньому кадрі (відкиньте його, якщо так). Це також менш точний підхід, ніж раніше, але він буде працювати для пісень будь-якої довжини, або синхронізації чого-небудь (звукові ефекти, як приклад) з музикою.

Ось той же код, що і до використання такого підходу:

func _ready():
    $Player.play()


func _process(delta):
    var time = $Player.get_playback_position() + AudioServer.get_time_since_last_mix()
    # Compensate for output latency.
    time -= AudioServer.get_output_latency()
    print("Time is: ", time)