Skip to content

Commit 4af7bec

Browse files
PizzaLovers007fire
andcommitted
Implement audio absolute time and scheduled play
The absolute time is calculated based on the total mixed frames and the mix rate. This means it only updates when a mix step happens. Specific play_scheduled behaviors: - If a sound is playing, play_scheduled() will stop that sound. This matches the behavior of play(). - If a sound is scheduled, then paused, then resumed before the schedule happens, the sound still plays at the correct scheduled time. - If a playing sound is paused, then play_scheduled() is called, the sound will restart from the beginning. This matches the behavior of play(). - With a higher max_polyphony, multiple sounds can be scheduled. - play_scheduled is unaffected by pitch scale - play_scheduled does not support samples Scheduled stop is not implemented due to limited use cases. Co-authored-by: K. S. Ernest (iFire) Lee <ernest.lee@chibifire.com>
1 parent 09ea7bc commit 4af7bec

14 files changed

+167
-24
lines changed

doc/classes/AudioServer.xml

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,26 @@
3535
Generates an [AudioBusLayout] using the available buses and effects.
3636
</description>
3737
</method>
38+
<method name="get_absolute_time" qualifiers="const" experimental="">
39+
<return type="float" />
40+
<description>
41+
Returns the absolute time in seconds of the [AudioServer]'s timeline, based on the number of audio frames mixed. Used to schedule sounds to be played with high precision timing, such as with [method AudioStreamPlayer.play_scheduled].
42+
[b]Note:[/b] This value only updates each time an audio chunk is mixed and should not be relied on as an accurate "current" time of the [AudioServer].
43+
[b]Example:[/b] Schedule two sounds to be played at the same time, roughly 1 second in the future:
44+
[codeblocks]
45+
[gdscript]
46+
var future_time = AudioServer.get_absolute_time() + 1
47+
player1.play_scheduled(future_time)
48+
player2.play_scheduled(future_time)
49+
[/gdscript]
50+
[csharp]
51+
double futureTime = AudioServer.GetAbsoluteTime() + 1;
52+
player1.PlayScheduled(futureTime);
53+
player2.PlayScheduled(futureTime);
54+
[/csharp]
55+
[/codeblocks]
56+
</description>
57+
</method>
3858
<method name="get_bus_channels" qualifiers="const">
3959
<return type="int" />
4060
<param index="0" name="bus_idx" type="int" />

doc/classes/AudioStreamPlayer.xml

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@
2828
<method name="get_stream_playback">
2929
<return type="AudioStreamPlayback" />
3030
<description>
31-
Returns the latest [AudioStreamPlayback] of this node, usually the most recently created by [method play]. If no sounds are playing, this method fails and returns an empty playback.
31+
Returns the latest [AudioStreamPlayback] of this node, usually the most recently created by [method play] or [method play_scheduled]. If no sounds are playing, this method fails and returns an empty playback.
3232
</description>
3333
</method>
3434
<method name="has_stream_playback">
@@ -44,6 +44,17 @@
4444
Plays a sound from the beginning, or the given [param from_position] in seconds.
4545
</description>
4646
</method>
47+
<method name="play_scheduled" experimental="">
48+
<return type="void" />
49+
<param index="0" name="absolute_time" type="float" />
50+
<param index="1" name="from_position" type="float" default="0.0" />
51+
<description>
52+
Schedules a sound to be played on the [AudioServer]'s timeline at [param absolute_time] in seconds. If the sound is scheduled to play earlier than the value returned by [method AudioServer.get_absolute_time], it will be played immediately. The sound starts from the given [param from_position] in seconds.
53+
Use this method for high precision playbacks, such as a metronome or other rhythm-based sounds.
54+
[b]Note:[/b] Calling this method after [member max_polyphony] is reached will cut off the oldest sound playing on this node.
55+
[b]Note:[/b] On the Web platform, [member playback_type] must be set to [constant AudioServer.PLAYBACK_TYPE_STREAM]. Otherwise, this method will behave like [method play].
56+
</description>
57+
</method>
4758
<method name="seek">
4859
<return type="void" />
4960
<param index="0" name="to_position" type="float" />
@@ -67,7 +78,7 @@
6778
[b]Note:[/b] At runtime, if no bus with the given name exists, all sounds will fall back on [code]"Master"[/code]. See also [method AudioServer.get_bus_name].
6879
</member>
6980
<member name="max_polyphony" type="int" setter="set_max_polyphony" getter="get_max_polyphony" default="1">
70-
The maximum number of sounds this node can play at the same time. Calling [method play] after this value is reached will cut off the oldest sounds.
81+
The maximum number of sounds this node can play and schedule at the same time. Calling [method play] or [method play_scheduled] after this value is reached will cut off the oldest sounds.
7182
</member>
7283
<member name="mix_target" type="int" setter="set_mix_target" getter="get_mix_target" enum="AudioStreamPlayer.MixTarget" default="0">
7384
The mix target channels, as one of the [enum MixTarget] constants. Has no effect when two speakers or less are detected (see [enum AudioServer.SpeakerMode]).

doc/classes/AudioStreamPlayer2D.xml

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,17 @@
3838
Queues the audio to play on the next physics frame, from the given position [param from_position], in seconds.
3939
</description>
4040
</method>
41+
<method name="play_scheduled" experimental="">
42+
<return type="void" />
43+
<param index="0" name="absolute_time" type="float" />
44+
<param index="1" name="from_position" type="float" default="0.0" />
45+
<description>
46+
Schedules a sound to be played on the [AudioServer]'s timeline at [param absolute_time] in seconds. If the sound is scheduled to play earlier than the value returned by [method AudioServer.get_absolute_time], it will be played immediately. The sound starts from the given [param from_position] in seconds.
47+
Use this method for high precision playbacks, such as a metronome or other rhythm-based sounds.
48+
[b]Note:[/b] Calling this method after [member max_polyphony] is reached will cut off the oldest sound playing on this node.
49+
[b]Note:[/b] On the Web platform, [member playback_type] must be set to [constant AudioServer.PLAYBACK_TYPE_STREAM]. Otherwise, this method will behave like [method play].
50+
</description>
51+
</method>
4152
<method name="seek">
4253
<return type="void" />
4354
<param index="0" name="to_position" type="float" />
@@ -70,7 +81,7 @@
7081
Maximum distance from which audio is still hearable.
7182
</member>
7283
<member name="max_polyphony" type="int" setter="set_max_polyphony" getter="get_max_polyphony" default="1">
73-
The maximum number of sounds this node can play at the same time. Playing additional sounds after this value is reached will cut off the oldest sounds.
84+
The maximum number of sounds this node can play and schedule at the same time. Calling [method play] or [method play_scheduled] after this value is reached will cut off the oldest sounds.
7485
</member>
7586
<member name="panning_strength" type="float" setter="set_panning_strength" getter="get_panning_strength" default="1.0">
7687
Scales the panning strength for this node by multiplying the base [member ProjectSettings.audio/general/2d_panning_strength] with this factor. Higher values will pan audio from left to right more dramatically than lower values.

doc/classes/AudioStreamPlayer3D.xml

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,17 @@
3838
Queues the audio to play on the next physics frame, from the given position [param from_position], in seconds.
3939
</description>
4040
</method>
41+
<method name="play_scheduled" experimental="">
42+
<return type="void" />
43+
<param index="0" name="absolute_time" type="float" />
44+
<param index="1" name="from_position" type="float" default="0.0" />
45+
<description>
46+
Schedules a sound to be played on the [AudioServer]'s timeline at [param absolute_time] in seconds. If the sound is scheduled to play earlier than the value returned by [method AudioServer.get_absolute_time], it will be played immediately. The sound starts from the given [param from_position] in seconds.
47+
Use this method for high precision playbacks, such as a metronome or other rhythm-based sounds.
48+
[b]Note:[/b] Calling this method after [member max_polyphony] is reached will cut off the oldest sound playing on this node.
49+
[b]Note:[/b] On the Web platform, [member playback_type] must be set to [constant AudioServer.PLAYBACK_TYPE_STREAM]. Otherwise, this method will behave like [method play].
50+
</description>
51+
</method>
4152
<method name="seek">
4253
<return type="void" />
4354
<param index="0" name="to_position" type="float" />
@@ -91,7 +102,7 @@
91102
The distance past which the sound can no longer be heard at all. Only has an effect if set to a value greater than [code]0.0[/code]. [member max_distance] works in tandem with [member unit_size]. However, unlike [member unit_size] whose behavior depends on the [member attenuation_model], [member max_distance] always works in a linear fashion. This can be used to prevent the [AudioStreamPlayer3D] from requiring audio mixing when the listener is far away, which saves CPU resources.
92103
</member>
93104
<member name="max_polyphony" type="int" setter="set_max_polyphony" getter="get_max_polyphony" default="1">
94-
The maximum number of sounds this node can play at the same time. Playing additional sounds after this value is reached will cut off the oldest sounds.
105+
The maximum number of sounds this node can play and schedule at the same time. Calling [method play] or [method play_scheduled] after this value is reached will cut off the oldest sounds.
95106
</member>
96107
<member name="panning_strength" type="float" setter="set_panning_strength" getter="get_panning_strength" default="1.0">
97108
Scales the panning strength for this node by multiplying the base [member ProjectSettings.audio/general/3d_panning_strength] by this factor. If the product is [code]0.0[/code] then stereo panning is disabled and the volume is the same for all channels. If the product is [code]1.0[/code] then one of the channels will be muted when the sound is located exactly to the left (or right) of the listener.

scene/2d/audio_stream_player_2d.cpp

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ void AudioStreamPlayer2D::_notification(int p_what) {
6464

6565
if (setplayback.is_valid() && setplay.get() >= 0) {
6666
internal->active.set();
67-
AudioServer::get_singleton()->start_playback_stream(setplayback, _get_actual_bus(), volume_vector, setplay.get(), internal->pitch_scale);
67+
AudioServer::get_singleton()->start_playback_stream(setplayback, _get_actual_bus(), volume_vector, setplay.get(), internal->scheduled_time, internal->pitch_scale);
6868
setplayback.unref();
6969
setplay.set(-1);
7070
}
@@ -231,7 +231,7 @@ float AudioStreamPlayer2D::get_pitch_scale() const {
231231
return internal->pitch_scale;
232232
}
233233

234-
void AudioStreamPlayer2D::play(float p_from_pos) {
234+
void AudioStreamPlayer2D::_play_internal(double p_from_pos) {
235235
Ref<AudioStreamPlayback> stream_playback = internal->play_basic();
236236
if (stream_playback.is_null()) {
237237
return;
@@ -241,6 +241,10 @@ void AudioStreamPlayer2D::play(float p_from_pos) {
241241

242242
// Sample handling.
243243
if (stream_playback->get_is_sample() && stream_playback->get_sample_playback().is_valid()) {
244+
if (internal->scheduled_time > 0) {
245+
WARN_PRINT_ED("play_scheduled() does not support samples. Playing immediately.");
246+
}
247+
244248
Ref<AudioSamplePlayback> sample_playback = stream_playback->get_sample_playback();
245249
sample_playback->offset = p_from_pos;
246250
sample_playback->bus = _get_actual_bus();
@@ -249,6 +253,16 @@ void AudioStreamPlayer2D::play(float p_from_pos) {
249253
}
250254
}
251255

256+
void AudioStreamPlayer2D::play(float p_from_pos) {
257+
internal->scheduled_time = 0;
258+
_play_internal(p_from_pos);
259+
}
260+
261+
void AudioStreamPlayer2D::play_scheduled(double p_abs_time, double p_from_pos) {
262+
internal->scheduled_time = p_abs_time;
263+
_play_internal(p_from_pos);
264+
}
265+
252266
void AudioStreamPlayer2D::seek(float p_seconds) {
253267
internal->seek(p_seconds);
254268
}
@@ -388,6 +402,7 @@ void AudioStreamPlayer2D::_bind_methods() {
388402
ClassDB::bind_method(D_METHOD("get_pitch_scale"), &AudioStreamPlayer2D::get_pitch_scale);
389403

390404
ClassDB::bind_method(D_METHOD("play", "from_position"), &AudioStreamPlayer2D::play, DEFVAL(0.0));
405+
ClassDB::bind_method(D_METHOD("play_scheduled", "absolute_time", "from_position"), &AudioStreamPlayer2D::play_scheduled, DEFVAL(0.0));
391406
ClassDB::bind_method(D_METHOD("seek", "to_position"), &AudioStreamPlayer2D::seek);
392407
ClassDB::bind_method(D_METHOD("stop"), &AudioStreamPlayer2D::stop);
393408

scene/2d/audio_stream_player_2d.h

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,8 @@ class AudioStreamPlayer2D : public Node2D {
7272
StringName _get_actual_bus();
7373
void _update_panning();
7474

75+
void _play_internal(double p_from_pos = 0.0);
76+
7577
static void _listener_changed_cb(void *self) { reinterpret_cast<AudioStreamPlayer2D *>(self)->force_update_panning = true; }
7678

7779
uint32_t area_mask = 1;
@@ -110,6 +112,7 @@ class AudioStreamPlayer2D : public Node2D {
110112
float get_pitch_scale() const;
111113

112114
void play(float p_from_pos = 0.0);
115+
void play_scheduled(double p_abs_time, double p_from_pos = 0.0);
113116
void seek(float p_seconds);
114117
void stop();
115118
bool is_playing() const;

scene/3d/audio_stream_player_3d.cpp

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -289,7 +289,7 @@ void AudioStreamPlayer3D::_notification(int p_what) {
289289
internal->active.set();
290290
HashMap<StringName, Vector<AudioFrame>> bus_map;
291291
bus_map[_get_actual_bus()] = volume_vector;
292-
AudioServer::get_singleton()->start_playback_stream(setplayback, bus_map, setplay.get(), actual_pitch_scale, linear_attenuation, attenuation_filter_cutoff_hz);
292+
AudioServer::get_singleton()->start_playback_stream(setplayback, bus_map, setplay.get(), internal->scheduled_time, actual_pitch_scale, linear_attenuation, attenuation_filter_cutoff_hz);
293293
setplayback.unref();
294294
setplay.set(-1);
295295
}
@@ -591,7 +591,7 @@ float AudioStreamPlayer3D::get_pitch_scale() const {
591591
return internal->pitch_scale;
592592
}
593593

594-
void AudioStreamPlayer3D::play(float p_from_pos) {
594+
void AudioStreamPlayer3D::_play_internal(double p_from_pos) {
595595
Ref<AudioStreamPlayback> stream_playback = internal->play_basic();
596596
if (stream_playback.is_null()) {
597597
return;
@@ -601,6 +601,10 @@ void AudioStreamPlayer3D::play(float p_from_pos) {
601601

602602
// Sample handling.
603603
if (stream_playback->get_is_sample() && stream_playback->get_sample_playback().is_valid()) {
604+
if (internal->scheduled_time > 0) {
605+
WARN_PRINT_ED("play_scheduled() does not support samples. Playing immediately.");
606+
}
607+
604608
Ref<AudioSamplePlayback> sample_playback = stream_playback->get_sample_playback();
605609
sample_playback->offset = p_from_pos;
606610
sample_playback->bus = _get_actual_bus();
@@ -609,6 +613,16 @@ void AudioStreamPlayer3D::play(float p_from_pos) {
609613
}
610614
}
611615

616+
void AudioStreamPlayer3D::play(float p_from_pos) {
617+
internal->scheduled_time = 0;
618+
_play_internal(p_from_pos);
619+
}
620+
621+
void AudioStreamPlayer3D::play_scheduled(double p_abs_time, double p_from_pos) {
622+
internal->scheduled_time = p_abs_time;
623+
_play_internal(p_from_pos);
624+
}
625+
612626
void AudioStreamPlayer3D::seek(float p_seconds) {
613627
internal->seek(p_seconds);
614628
}
@@ -822,6 +836,7 @@ void AudioStreamPlayer3D::_bind_methods() {
822836
ClassDB::bind_method(D_METHOD("get_pitch_scale"), &AudioStreamPlayer3D::get_pitch_scale);
823837

824838
ClassDB::bind_method(D_METHOD("play", "from_position"), &AudioStreamPlayer3D::play, DEFVAL(0.0));
839+
ClassDB::bind_method(D_METHOD("play_scheduled", "absolute_time", "from_position"), &AudioStreamPlayer3D::play_scheduled, DEFVAL(0.0));
825840
ClassDB::bind_method(D_METHOD("seek", "to_position"), &AudioStreamPlayer3D::seek);
826841
ClassDB::bind_method(D_METHOD("stop"), &AudioStreamPlayer3D::stop);
827842

scene/3d/audio_stream_player_3d.h

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,8 @@ class AudioStreamPlayer3D : public Node3D {
9797
#endif // PHYSICS_3D_DISABLED
9898
Vector<AudioFrame> _update_panning();
9999

100+
void _play_internal(double p_from_pos = 0.0);
101+
100102
uint32_t area_mask = 1;
101103

102104
AudioServer::PlaybackType playback_type = AudioServer::PlaybackType::PLAYBACK_TYPE_DEFAULT;
@@ -155,6 +157,7 @@ class AudioStreamPlayer3D : public Node3D {
155157
float get_pitch_scale() const;
156158

157159
void play(float p_from_pos = 0.0);
160+
void play_scheduled(double p_abs_time, double p_from_pos = 0.0);
158161
void seek(float p_seconds);
159162
void stop();
160163
bool is_playing() const;

scene/audio/audio_stream_player.cpp

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -103,16 +103,20 @@ int AudioStreamPlayer::get_max_polyphony() const {
103103
return internal->max_polyphony;
104104
}
105105

106-
void AudioStreamPlayer::play(float p_from_pos) {
106+
void AudioStreamPlayer::_play_internal(double p_from_pos) {
107107
Ref<AudioStreamPlayback> stream_playback = internal->play_basic();
108108
if (stream_playback.is_null()) {
109109
return;
110110
}
111-
AudioServer::get_singleton()->start_playback_stream(stream_playback, internal->bus, _get_volume_vector(), p_from_pos, internal->pitch_scale);
111+
AudioServer::get_singleton()->start_playback_stream(stream_playback, internal->bus, _get_volume_vector(), p_from_pos, internal->scheduled_time, internal->pitch_scale);
112112
internal->ensure_playback_limit();
113113

114114
// Sample handling.
115115
if (stream_playback->get_is_sample() && stream_playback->get_sample_playback().is_valid()) {
116+
if (internal->scheduled_time > 0) {
117+
WARN_PRINT_ED("play_scheduled() does not support samples. Playing immediately.");
118+
}
119+
116120
Ref<AudioSamplePlayback> sample_playback = stream_playback->get_sample_playback();
117121
sample_playback->offset = p_from_pos;
118122
sample_playback->volume_vector = _get_volume_vector();
@@ -122,6 +126,16 @@ void AudioStreamPlayer::play(float p_from_pos) {
122126
}
123127
}
124128

129+
void AudioStreamPlayer::play(float p_from_pos) {
130+
internal->scheduled_time = 0;
131+
_play_internal(p_from_pos);
132+
}
133+
134+
void AudioStreamPlayer::play_scheduled(double p_abs_time, double p_from_pos) {
135+
internal->scheduled_time = p_abs_time;
136+
_play_internal(p_from_pos);
137+
}
138+
125139
void AudioStreamPlayer::seek(float p_seconds) {
126140
internal->seek(p_seconds);
127141
}
@@ -248,6 +262,7 @@ void AudioStreamPlayer::_bind_methods() {
248262
ClassDB::bind_method(D_METHOD("get_pitch_scale"), &AudioStreamPlayer::get_pitch_scale);
249263

250264
ClassDB::bind_method(D_METHOD("play", "from_position"), &AudioStreamPlayer::play, DEFVAL(0.0));
265+
ClassDB::bind_method(D_METHOD("play_scheduled", "absolute_time", "from_position"), &AudioStreamPlayer::play_scheduled, DEFVAL(0.0));
251266
ClassDB::bind_method(D_METHOD("seek", "to_position"), &AudioStreamPlayer::seek);
252267
ClassDB::bind_method(D_METHOD("stop"), &AudioStreamPlayer::stop);
253268

scene/audio/audio_stream_player.h

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,8 @@ class AudioStreamPlayer : public Node {
5858

5959
Vector<AudioFrame> _get_volume_vector();
6060

61+
void _play_internal(double p_from_pos = 0.0);
62+
6163
protected:
6264
void _validate_property(PropertyInfo &p_property) const;
6365
void _notification(int p_what);
@@ -89,6 +91,7 @@ class AudioStreamPlayer : public Node {
8991
int get_max_polyphony() const;
9092

9193
void play(float p_from_pos = 0.0);
94+
void play_scheduled(double p_abs_time, double p_from_pos = 0.0);
9295
void seek(float p_seconds);
9396
void stop();
9497
bool is_playing() const;

0 commit comments

Comments
 (0)