sync_with_audio.rst 6.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150
  1. :article_outdated: True
  2. .. _doc_sync_with_audio:
  3. Sync the gameplay with audio and music
  4. =======================================
  5. Introduction
  6. ------------
  7. In any application or game, sound and music playback will have a slight delay. For games, this delay is often so small that it is negligible. Sound effects will come out a few milliseconds after any play() function is called. For music this does not matter as in most games it does not interact with the gameplay.
  8. Still, for some games (mainly, rhythm games), it may be required to synchronize player actions with something happening in a song (usually in sync with the BPM). For this, having more precise timing information for an exact playback position is useful.
  9. Achieving very low playback timing precision is difficult. This is because many factors are at play during audio playback:
  10. * Audio is mixed in chunks (not continuously), depending on the size of audio buffers used (check latency in project settings).
  11. * Mixed chunks of audio are not played immediately.
  12. * Graphics APIs display two or three frames late.
  13. * When playing on TVs, some delay may be added due to image processing.
  14. The most common way to reduce latency is to shrink the audio buffers (again, by editing the latency setting in the project settings). The problem is that when latency is too small, sound mixing will require considerably more CPU. This increases the risk of skipping (a crack in sound because a mix callback was lost).
  15. This is a common tradeoff, so Godot ships with sensible defaults that should not need to be altered.
  16. The problem, in the end, is not this slight delay but synchronizing graphics and
  17. audio for games that require it. Some helpers are available to obtain more
  18. precise playback timing.
  19. Using the system clock to sync
  20. ------------------------------
  21. As mentioned before, If you call :ref:`AudioStreamPlayer.play()<class_AudioStreamPlayer_method_play>`, sound will not begin immediately, but when the audio thread processes the next chunk.
  22. This delay can't be avoided but it can be estimated by calling :ref:`AudioServer.get_time_to_next_mix()<class_AudioServer_method_get_time_to_next_mix>`.
  23. The output latency (what happens after the mix) can also be estimated by calling :ref:`AudioServer.get_output_latency()<class_AudioServer_method_get_output_latency>`.
  24. Add these two and it's possible to guess almost exactly when sound or music will begin playing in the speakers during *_process()*:
  25. .. tabs::
  26. .. code-tab:: gdscript GDScript
  27. var time_begin
  28. var time_delay
  29. func _ready():
  30. time_begin = Time.get_ticks_usec()
  31. time_delay = AudioServer.get_time_to_next_mix() + AudioServer.get_output_latency()
  32. $Player.play()
  33. func _process(delta):
  34. # Obtain from ticks.
  35. var time = (Time.get_ticks_usec() - time_begin) / 1000000.0
  36. # Compensate for latency.
  37. time -= time_delay
  38. # May be below 0 (did not begin yet).
  39. time = max(0, time)
  40. print("Time is: ", time)
  41. .. code-tab:: csharp
  42. private double _timeBegin;
  43. private double _timeDelay;
  44. public override void _Ready()
  45. {
  46. _timeBegin = Time.GetTicksUsec();
  47. _timeDelay = AudioServer.GetTimeToNextMix() + AudioServer.GetOutputLatency();
  48. GetNode<AudioStreamPlayer>("Player").Play();
  49. }
  50. public override void _Process(float _delta)
  51. {
  52. double time = (Time.GetTicksUsec() - _timeBegin) / 1000000.0d;
  53. time = Math.Max(0.0d, time - _timeDelay);
  54. GD.Print(string.Format("Time is: {0}", time));
  55. }
  56. In the long run, though, as the sound hardware clock is never exactly in sync with the system clock, the timing information will slowly drift away.
  57. For a rhythm game where a song begins and ends after a few minutes, this approach is fine (and it's the recommended approach). For a game where playback can last a much longer time, the game will eventually go out of sync and a different approach is needed.
  58. Using the sound hardware clock to sync
  59. --------------------------------------
  60. Using :ref:`AudioStreamPlayer.get_playback_position()<class_AudioStreamPlayer_method_get_playback_position>` to obtain the current position for the song sounds ideal, but it's not that useful as-is. This value will increment in chunks (every time the audio callback mixed a block of sound), so many calls can return the same value. Added to this, the value will be out of sync with the speakers too because of the previously mentioned reasons.
  61. To compensate for the "chunked" output, there is a function that can help: :ref:`AudioServer.get_time_since_last_mix()<class_AudioServer_method_get_time_since_last_mix>`.
  62. Adding the return value from this function to *get_playback_position()* increases precision:
  63. .. tabs::
  64. .. code-tab:: gdscript GDScript
  65. var time = $Player.get_playback_position() + AudioServer.get_time_since_last_mix()
  66. .. code-tab:: csharp
  67. double time = GetNode<AudioStreamPlayer>("Player").GetPlaybackPosition() + AudioServer.GetTimeSinceLastMix();
  68. To increase precision, subtract the latency information (how much it takes for the audio to be heard after it was mixed):
  69. .. tabs::
  70. .. code-tab:: gdscript GDScript
  71. var time = $Player.get_playback_position() + AudioServer.get_time_since_last_mix() - AudioServer.get_output_latency()
  72. .. code-tab:: csharp
  73. double time = GetNode<AudioStreamPlayer>("Player").GetPlaybackPosition() + AudioServer.GetTimeSinceLastMix() - AudioServer.GetOutputLatency();
  74. The result may be a bit jittery due how multiple threads work. Just check that the value is not less than in the previous frame (discard it if so). This is also a less precise approach than the one before, but it will work for songs of any length, or synchronizing anything (sound effects, as an example) to music.
  75. Here is the same code as before using this approach:
  76. .. tabs::
  77. .. code-tab:: gdscript GDScript
  78. func _ready():
  79. $Player.play()
  80. func _process(delta):
  81. var time = $Player.get_playback_position() + AudioServer.get_time_since_last_mix()
  82. # Compensate for output latency.
  83. time -= AudioServer.get_output_latency()
  84. print("Time is: ", time)
  85. .. code-tab:: csharp
  86. public override void _Ready()
  87. {
  88. GetNode<AudioStreamPlayer>("Player").Play();
  89. }
  90. public override void _Process(float _delta)
  91. {
  92. double time = GetNode<AudioStreamPlayer>("Player").GetPlaybackPosition() + AudioServer.GetTimeSinceLastMix();
  93. // Compensate for output latency.
  94. time -= AudioServer.GetOutputLatency();
  95. GD.Print(string.Format("Time is: {0}", time));
  96. }