How to play 2 audio streams back to back seamless on runtime?

Godot Version

godot 4.3

Question

On runtime, I want to play 2 audio streams*, one after the other, without even a frame of delay between them. These streams are not decided beforehand.

  • These audio streams are actually 2 halves of a music track, so the transition must be perfect or it will produce a noticeable audio “click”.

Possible solutions that don’t seem to work:

- Using the finished signal of the audio player:

func _play_music():
	audio_player.stream = audio_stream_1
	audio_player.play()
	audio_player.finished.connect(_on_audio_player_finished, 4)

func _on_audio_player_finished():
	audio_player.stream = audio_stream_2
	audio_player.play()

This doesn’t make a seamless transition. There’s a small gap between the end of one stream and the start of the next.

- Using a playlist or interactable:

Unless I’m missing something, these don’t allow the streams they contain to be set at runtime, so I can’t use them since the streams must be chosen at runtime.

I actually managed a solution that so far seems to work, but seems extremely clunky for something that should be pretty basic.

The solution is to import the first audiostream with loop mode forward, then manually check each frame if the first audio stream is finished or about to finish, and trigger the change:

func _process(delta: float) -> void:
	if music_player.get_playback_position() + AudioServer.get_time_since_last_mix() + delta  >= music_player.stream.get_length() || music_player.get_playback_position() < 0.1:
		music_player.stream = audio_stream_2
		music_player.play()

Isn’t there a better way? Am I missing something basic here?

There seems to be a bug with AudioStreamPlaylist where the AudioStreamPlaylist.stream_count is not updated when adding one at runtime. You can manually set it to the number of streams and it works.

You can set AudioStreamPlaylist.fade_time to 0 avoid the fade.

Here’s an example:

extends AudioStreamPlayer


func _ready() -> void:
	var playlist = AudioStreamPlaylist.new()
	playlist.set_list_stream(0, preload("res://part1.mp3"))
	playlist.set_list_stream(1, preload("res://part2.mp3"))
	playlist.stream_count = 2
	playlist.fade_time = 0
	stream = playlist
	play()

Thanks mrcdk. That almost worked, except for my use case I need to be able to update the playlist on the fly (adding the second stream while the first one is playing), but seems that calling set_list_stream makes the audio player stop.

I tried getting the current playback position before updating the playlist, then seeking to that point right after, but that produces a noticeable jump.

Any ideas?

No, sorry, not in Godot 4.3.

In Godot 4.4 you may be able to use an AudioStreamGenerator and use the AudioStreamPlayback.mix_audio() of other audio streams to fill its buffer. Here’s a simple example:

extends AudioStreamPlayer

var streams:Array[AudioStream] = [
	preload("res://assets/part1.ogg"),
	preload("res://assets/part2.ogg"),
	preload("res://assets/part3.ogg"),
]

var current_stream = 0
var playback:AudioStreamGeneratorPlayback
var playbacks:Array[AudioStreamPlayback] = []



func _ready() -> void:
	var generator = AudioStreamGenerator.new()
	generator.mix_rate_mode = AudioStreamGenerator.MIX_RATE_OUTPUT
	stream = generator
	play()
	playback = get_stream_playback() as AudioStreamGeneratorPlayback
	playbacks.push_back(streams[0].instantiate_playback())
	await get_tree().create_timer(0.32).timeout
	playbacks.push_back(streams[1].instantiate_playback())
	await get_tree().create_timer(0.63).timeout
	playbacks.push_back(streams[2].instantiate_playback())


func _process(delta: float) -> void:
	mix()

	
func mix() -> void:
	var available = playback.get_frames_available()
	while available > 0:
		var stream_playback = playbacks[current_stream]
		if not stream_playback.is_playing():
			stream_playback.start()

		var to_mix = stream_playback.mix_audio(1.0, available)
		var mixed = to_mix.size()
		if mixed > 0:
			playback.push_buffer(to_mix)

			if mixed < available:
				current_stream = wrap(current_stream + 1, 0, playbacks.size())

		available -= mixed

There’s a small noticeable pop when mixing the streams and I’m not sure why, though. Maybe because I just cut the original audio track at random points? :person_shrugging: dunno.