Using 3.1.1
Audio that is set to loop does not send a "finished" signal when it restarts.
To get around this, I disabled loop, and loop through code (on the finished signal, play)
That is almost good enough, but I have found:
Manually looping like this causes the audio to have a slight delay before it restarts, so the looping audio is not seamless.
When set to automatically loop, there is no (observable) delay. However as I said, with automatic loop set I can't get the finished signal.
They should totally emit a signal, but I think it should be a different one to avoid any compatibility break, something like signal looped
For fun, I decided to guess on how to implement this feature
Background: Godot implements scripting and audio on different threads. To avoid avoiding mixing on the main thread, Audioplayer::play() only sets flags in the Audioplayer itself and calls AudioStreamPlayback::play() which holds the state instance for static AudioStream resources. When the Audioplayer ends, Scenetree calls Audioplayer::_notification(int) to emit signals.
The Problem: AudioStreamPlayer::_mix_internal(bool) is managed by the audio thread and should not be emitting signals. Although AudioStreamPlayer mix creates AudioFrame to be generated into the audio buffer, the Class store mostly user settings rather than player state. AudioStreamPlayback::get_loop_count() can only provide count information but cannot discern when the playback happened.
Proposal: Implement play() flag setting in reverse.
Add a signal looped to Audioplayer
https://github.com/godotengine/godot/blob/17732fe698b835c29f77c84f329b2ed6cab215ce/scene/audio/audio_stream_player.cpp#L422 .
Call AudioStreamPlayback::get_loop_count() in the beginning of _mix_internal() and call it again to the end to see if the player looped and set a flag for later processing
In the NOTIFICATION_INTERNAL_PROCESS state, check the flag and `emit_signal("looped")
https://github.com/godotengine/godot/blob/17732fe698b835c29f77c84f329b2ed6cab215ce/scene/audio/audio_stream_player.cpp#L150
Like play, this loop will only emit signals in between frames like the rest of the engine.
Somewhat related discussion: #33579.
Most helpful comment
They should totally emit a signal, but I think it should be a different one to avoid any compatibility break, something like
signal looped