Is it possible to perfectly chain sounds?

I have a background sound loop consisting of four pieces, and I want to play them back to back over and over again in a random order. Is there any way to get the transitions to seamlessly match up reliably?

(It’s cut at calm points in the track, so they don’t have to be sample-perfect transitions, but they do have to be good enough that there’s no noticeable gap)

6 Likes

check sound.TimePosition (with wait or I guess .Changed) and start to play the next random sound like 0.1 seconds before it ends

I think .wav (edit: ogg) files don’t have that weird .mp3 gap when you start a new sound though. check em out

start to play the next random sound like 0.1 seconds before it ends

Well, that was my first intuition, but that’s what I’m hoping to avoid.

And the file type actually matters? I figured that they would all be converted to the same format internally anyways. I uploaded the parts as .ogg, know anything about how that acts? Is there any guide somewhere on that gotcha?

As audio plays at a much higher frequency than Lua code is scheduled, you cannot perfectly chain them (e.g. the Ended event does not trigger immediately after the last sample is played, to my knowledge, but a bit later). This causes tiny gaps of “no audio”, which is very noticeable.

Besides that, make sure the separate parts start and end with a sample near the zero-crossing, otherwise you’ll get audible and very annoying popping.

Ogg files are best used for these things, they do not have that period of silence mp3 enforces for some reason.

I’d use a small crossfade to work around the first issue, as there is no api to handle this nicely…

16 Likes

I think I confused ogg with wav

I mean, when you’re talking about an event, a lot of events aren’t “scheduled”, they’re fired and run synchronously from the code that triggered them. Though I’m not sure what I was hoping for, since even if the event were fired synchronously from in the “request more samples” part of the audio code the Play() call on the next sound wouldn’t synchronously be able to get samples back to that call.

I think I confused ogg with wav

Yeah, there’s not even an option to upload wavs. It’s mp3 or ogg.

Looks like starting the next sound an arbitrary delay before the end is what I’ll have to do.

I’ve always sort of assumed that the events emitted from the audio thread were queued or so, nothing ever seems to block execution of that thread.
Play not actually “extending” the audio stream seems like a more plausible explanation though :stuck_out_tongue: .

You’re probably right since blocking the audio thread to call into Lua is a Bad Idea™, I’m just pointing out that it could be synchronous since some events that you might expect to be asynchronous actually aren’t. Most likely both the event and the Play() call are asynchronous / queued.

1 Like

Try tweening the volume of the first song to 0 while tweening the volume of the other sound a few seconds before the first sound ends. A lot of music players have this option so transitioning from song to song is smooth.
Also, use ogg files so you don’t get that mp3 delay

1 Like

I have put OGG files together and chained them using the events. Never could audibly notice the switch.

1 Like

I’ll try it again. I definitely used OGG files and definitely was having noticeable drops.

Maybe I accidentally left some delay in the OGG files and the drops are my fault.

If you have a known amount of silence at the beginning of each track you should be able to play sound #2 early and use the TimePosition property to sync both up.

Something like this
local function syncSounds(sound1, sound2, silenceSeconds) --> success: bool
	-- if sound 1 is not playing (or does not exist), we start sound2 asap
	if not sound1 or not sound1.IsPlaying then
		sound2.TimePosition = silenceSeconds
		sound2:Play()
		return true
	end
	-- get how much time is remaining before sound1 ends
	local remaining = sound1.TimeLength - sound1.TimePosition
	if remaining > silenceSeconds then  -- if it's more than silenceSeconds, it's impossible to sync up
		return false
	end
	-- sync up sound2 to start as soon as sound1 ends
	sound2.TimePosition = silenceSeconds - remaining
	sound2:Play()
	return true
end

local function waitForSyncReady(sound1, silenceSeconds)
	wait(sound1.TimeLength - sound1.TimePosition - silenceSeconds)
	-- always waits for *at least* this amount of time, so it should never wait too little.
end

---

local silence = 1  -- every sound has 1 second of silence in the beginning

local sound = getRandomSound()  -- assume this is defined somewhere. Returns a new sound on each call.
syncSounds(nil, sound, silence)
while true do
	waitForSyncReady(sound)
	local nextSound = getRandomSound()
	if syncSounds(sound, nextSound, silence) then
		sound = nextSound
	end
end

I feel like syncing up TimePositions will be more accurate than playing sounds whenever Lua gets to it. Both solutions are probably close enough that players won’t notice, anyway.

1 Like

Even if you can’t get perfect sync, it may be possible to do a short crossfade over the seam if your audio is something like background music/ambiance and you upload clips that have a bit of redundancy at either end (i.e. they are engineered to overlap, not splice end to end)

1 Like