-
-
Notifications
You must be signed in to change notification settings - Fork 22.8k
Implement audio absolute time (DSP time) and scheduled play #105510
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
39034ac
to
ac855dd
Compare
This comment was marked as outdated.
This comment was marked as outdated.
This comment was marked as outdated.
This comment was marked as outdated.
ac855dd
to
70052c0
Compare
70052c0
to
7771d07
Compare
7771d07
to
f1e6dc6
Compare
I removed breaks compat because we decided to only change the new parameters to double. Edited: At least that's the approach we want to take. It may still be breaking compat, but that's not by design and is a bug. |
This comment was marked as outdated.
This comment was marked as outdated.
f1e6dc6
to
0f1f501
Compare
Added, thanks for the pioneer work and review! |
Two reasons to change this: * At a default mix rate of 44100, the playback position in seconds can experience rounding errors with a 32-bit type if the value is high enough. 44100 requires 15 free bits in the mantissa to be within 1/2 an audio frame, so the cutoff is 512 seconds before rounding issues occur (512=2^9, and 9+15 > 23 mantissa bits in 32-bit float). * Internally, AudioStreamPlayback and AudioStream use a 64-bit type for playback position. See this discussion: godotengine#105510 (comment)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Part of what originally confused up on the purpose of this PR is that there's no documented reason for this method to exist.
What I mean is, at a glance, most users will look at both play
and play_scheduled
. They will see that the latter has a longer name, longer description, and takes longer to set up. As such, they may stick to _process()
callbacks, Timer nodes, etc. for accurate audio purposes. There's barely any mention, or any examples as to why this should be used over play()
.
I have no suggestions about that, but I would heavily recommend to at least provide one.
ed81d23
to
67c01ab
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm generally questioning an implementation detail. I have not looked much at the prior conversation, so I'm not sure if this has ever been brought up.
If AudioServer.get_absolute_time
is so integral to AudioStreamPlayer.play_scheduled
, why should it be fetched every time in user code? Why should play_scheduled
require an absolute time, instead of relative time?
Essentially my suggestion is as follows:
# Before:
var future_time = AudioServer.get_absolute_time() + 1
player1.play_scheduled(future_time)
# After:
player1.play_scheduled(1)
Regarding the precision of these methods, I wonder if #81873 would be of any inspiration, or if it would even supersede AudioServer.get_absolute_time
in some situations?
- See #81873
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 <[email protected]>
67c01ab
to
4af7bec
Compare
Here's a trimmed down version of the metronome in my demo project: var _start_absolute_time: float
var _scheduled_time: float
func _process(_delta: float) -> void:
# Once the currently scheduled tick has started, begin scheduling the next
# one.
var curr_time := AudioServer.get_absolute_time()
if curr_time > _scheduled_time:
var next_tick := ceili((curr_time - _start_absolute_time) / beat_length)
_scheduled_time = _start_absolute_time + next_tick * beat_length
play_scheduled(_scheduled_time)
func start(start_absolute_time: float) -> void:
_start_absolute_time = start_absolute_time
_scheduled_time = start_absolute_time
play_scheduled(_scheduled_time) If we were to change the API to be relative, reimplementing it would look something like: var _delay: float
func _process(delta: float) -> void:
_delay -= delta
if _delay > 0:
return
# Now that the currently scheduled tick has started, begin scheduling the
# next one.
var curr_time := song_player.get_playback_position()
var next_tick := ceili(curr_time / beat_length)
var relative_delay := curr_time - next_tick * beat_length
play_scheduled(curr_time)
_delay = relative_delay
func start(delay: float) -> void:
_delay = delay
play_scheduled(_delay) For scheduling sounds at the very start, using relative seems a lot easier since you only need to care about the initial delay. However once the game logic advances past that first frame, you need some sort of "current time" in both cases in order to know when to schedule sounds (in the above example, the next metronome tick). Using
To me, the monotonic and audio thread synced nature of
IMO this follows best engine practices with this low-level API exposed to users that need it so they can build their own solutions. |
(I'm so sorry for the review delay. 🙇) I will summarize here the thread I made on the Godot Developers Chat. First and foremost, thank you @PizzaLovers007 for your great PR. You did put a lot of work on it, and it shows! So. The need for a new PRI think a new PR is needed, as a little twist is needed on how to implement the functionality. I think nobody here is doubting of the necessity to add such feature to Godot. And @PizzaLovers007 you should totally let this one intact (just to keep the history of the code and such), and start a new one if you're still interested! The problem with the current implementation is that it relies heavily on the We need to ponder about the fact that
I think we should inspire ourselves from the Web Audio API An And the API is so small: there's nothing really you can do once it started. All you can do is to disconnect the node from it's audio destination. An alternative way to handle scheduled playWhat about We can keep using // When `p_start == -1.0`, it would be the equivalent of "now"
// I initially thought about `AudioStreamPlayer.schedule_play()`, but I think the new name states better what it does... and especially what it returns.
Ref<AudioStreamPlaybackScheduled> AudioStreamPlayer::start_scheduled_stream_playback(double p_start = -1.0, double p_stream_offset = 0.0);
// No default here because it's an optional call. Thanks @PizzaLovers007 for the suggestion.
void AudioStreamPlaybackScheduled::set_scheduled_stop(double p_end); This has the added bonus to give the control to the user. The user can summon any number of It can be useful to summon multiple streams in advance too. Currently, in this PR, the metronome plays on-time when the process is at 1FPS, but fails a lot of time to actually play, because the main code didn't have the opportunity to queue up new ticks. As a metronome is predictable, you could easily whip up something like this: var player_in_range: = true
var playback_instances: Array[AudioStreamPlaybackScheduled] = []
const MAX_PLAYBACK_INSTANCES: = 10
func _on_playback_end(playback: AudioStreamPlaybackScheduled) -> void:
playback_instances.erase(playback)
func _process() -> void:
if player_in_range:
while playback_instances.size() < MAX_PLAYBACK_INSTANCES:
var playback: = $player.start_scheduled_stream_playback(AudioServer.get_current_time() + playback_instances.size())
playback.connect("playback_ended", _on_playback_end.bind(playback), CONNECT_ONE_SHOT)
playback_instances.push_back(playback)
else:
for playback_instance in playback_instances:
AudioServer.stop_playback_stream(playback_instance)
|
This is a rewrite of godotengine#105510 that pulls the silence frames logic into a separate AudioStreamPlaybackScheduled class. The rewrite allows both the AudioServer and the player (mostly) to treat it as if it were a generic playback. Main differences: - play_scheduled returns an AudioStreamPlaybackScheduled instance, which is tied to the player that created it. - The start time can be changed after scheduling. - You can now set an end time for the playback. - The scheduled playback can be cancelled separately from other playbacks on the player. Co-authored-by: K. S. Ernest (iFire) Lee <[email protected]>
This is a rewrite of godotengine#105510 that moves the silence frames logic into a separate AudioStreamPlaybackScheduled class. The rewrite allows both the AudioServer and the player (mostly) to treat it as if it were a generic playback. It also simplifies the addition of new features. Main differences: - play_scheduled returns an AudioStreamPlaybackScheduled instance, which is tied to the player that created it. - The start time can be changed after scheduling. - You can now set an end time for the playback. - The scheduled playback can be cancelled separately from other playbacks on the player. Co-authored-by: K. S. Ernest (iFire) Lee <[email protected]>
Thanks for the review! I've given this some more thought after trying to implement it (WIP) as well as seeing some of the post-rework discussions in the chat. I can agree that the scheduling logic should be moved to its own playback. However, I think my main worry about dissociating the playback from the player is that there are some settings like volume and panning that as soon as you schedule the sound, you'd need to interact with Additionally from your example:
If we want to expand on the playback idea but keep it tied to the player, I came up with few alternatives: Alternative 1: Add the playback to the playervoid AudioStreamPlayer::add_playback(AudioStreamPlayback playback); Benefits of this would be that the user is free to create any kind of playback from any stream but still reap the benefits of the player frontends. The drawback here would be that the playback itself may have nothing to do with the attached Alternative 2:
|
This is a rewrite of godotengine#105510 that moves the silence frames logic into a separate AudioStreamPlaybackScheduled class. The rewrite allows both the AudioServer and the player (mostly) to treat it as if it were a generic playback. It also simplifies the addition of new features. Main differences: - play_scheduled returns an AudioStreamPlaybackScheduled instance, which is tied to the player that created it. - The start time can be changed after scheduling. - You can now set an end time for the playback. - The scheduled playback can be cancelled separately from other playbacks on the player. Co-authored-by: K. S. Ernest (iFire) Lee <[email protected]>
This is a rewrite of godotengine#105510 that moves the silence frames logic into a separate AudioStreamPlaybackScheduled class. The rewrite allows both the AudioServer and the player (mostly) to treat it as if it were a generic playback. It also simplifies the addition of new features. Main differences: - play_scheduled returns an AudioStreamPlaybackScheduled instance, which is tied to the player that created it. - The start time can be changed after scheduling. - You can now set an end time for the playback. - The scheduled playback can be cancelled separately from other playbacks on the player. Co-authored-by: K. S. Ernest (iFire) Lee <[email protected]>
@PizzaLovers007 We discussed your proposal during the last audio meeting and I had an idea of a counter proposal. I just didn't have the time yet to do so. I'll try to do it today or in the next days. |
Just wanted to ping this thread! I did read the meeting notes, but I'm not sure if "forked synced" node refers to Godot nodes or Web API nodes like |
Until this is merged, here's how I've been working around it; accurate within a millisecond. var silence = position - music_player.get_playback_position()
sfx_player.play(clamp(1.0 - silence, 0.0, 1.0)) Oh yeah using an AudioStreamPlaylist to add silence before your sample doesn't work; perhaps you could do it with an AudioStreamGenerator, but for my use case it was easier to just modify the sample itself. |
@PizzaLovers007 So my idea would be to create These nodes are destined to be derived from existing related func _ready() -> void:
var scheduled_player = $AudioStreamPlayer.create_scheduled()
add_child(scheduled_player) How does it sound? |
I may be missing some pieces of discussion, but why new nodes and not new AudioStreamPlayback types (still created from the existing nodes)? |
See @PizzaLovers007's comment:
(emphasis is mine) |
Making new nodes sounds good, though I'm not sure about the If we want to keep this behavior, I think a "copy" constructor would work better since that would break the circular dependency from the inheritance. So something like Is there a stronger reason to not make |
This is a rewrite of godotengine#105510 that moves the silence frames logic into a separate AudioStreamPlaybackScheduled class and introduces new ScheduledAudioStreamPlayer/2D/3D nodes. The rewrite allows both the AudioServer and the player (mostly) to treat it as if it were a generic playback. Main differences: - ScheduledAudioStreamPlayer/2D/3D are new nodes inheriting from their AudioStreamPlayer/2D/3D counterparts. These nodes only contain one new method: play_scheduled. - play_scheduled returns an AudioStreamPlaybackScheduled instance, which is tied to the player that created it. - The start time can be changed after scheduling. - You can now set an end time for the playback. - The scheduled playback can be cancelled separately from other playbacks on the player. Co-authored-by: K. S. Ernest (iFire) Lee <[email protected]>
This is a rewrite of godotengine#105510 that moves the silence frames logic into a separate AudioStreamPlaybackScheduled class and introduces new ScheduledAudioStreamPlayer/2D/3D nodes. The rewrite allows both the AudioServer and the player (mostly) to treat it as if it were a generic playback. Main differences: - ScheduledAudioStreamPlayer/2D/3D are new nodes inheriting from their AudioStreamPlayer/2D/3D counterparts. These nodes only contain one new method: play_scheduled. - play_scheduled returns an AudioStreamPlaybackScheduled instance, which is tied to the player that created it. - The start time can be changed after scheduling. - You can now set an end time for the playback. - The scheduled playback can be cancelled separately from other playbacks on the player. Co-authored-by: K. S. Ernest (iFire) Lee <[email protected]>
This is a rewrite of godotengine#105510 that moves the silence frames logic into a separate AudioStreamPlaybackScheduled class and introduces new ScheduledAudioStreamPlayer/2D/3D nodes. The rewrite allows both the AudioServer and the player (mostly) to treat it as if it were a generic playback. Main differences: - ScheduledAudioStreamPlayer/2D/3D are new nodes inheriting from their AudioStreamPlayer/2D/3D counterparts. These nodes only contain one new method: play_scheduled. - play_scheduled returns an AudioStreamPlaybackScheduled instance, which is tied to the player that created it. - The start time can be changed after scheduling. - You can now set an end time for the playback. - The scheduled playback can be cancelled separately from other playbacks on the player. Co-authored-by: K. S. Ernest (iFire) Lee <[email protected]>
I've rewritten the PR based on the discussions here, and will close this in favor of #107226. |
Note: This PR is superseded by #107226.
Implementation is based off of @fire's notes 4 years ago: https://gist.github.com/fire/b9ed7853e7be24ab1d5355ef01f46bf1
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:play_scheduled()
will stop that sound (with single polyphony). This matches the behavior of play().max_polyphony
, multiple sounds can be scheduled, and playing sounds can continue playing.play_scheduled
is unaffected by pitch scale.play_scheduled
does not support samples. The "Stream" default playback type is required for Web builds (ideally with threads enabled).Scheduled stop is not implemented due to limited use cases.
Fixes godotengine/godot-proposals#1151.