Some looped audios make a repeating popping noise.

Some audios containing single cycle waveforms, that are looped using AudioPlayer.LoopRegion, seem to produce a repeating ‘pop’ or ‘click’ sound, though it can sometimes take half a second to start happening.

This is reproducible by creating an AudioPlayer with the audio: rbxassetid://102825916505480, setting Looping to true, and setting. its loop region to NumberRange.new(25 / 22050, 58 / 22050).
Place1.rbxl (56.8 KB)

The frequency of the popping noise seems to vary (but not correspond to) the PlaybackSpeed. After recording and analyzing the audio output from Roblox, the popping seems to be caused by the time position getting reset too early.

Expected behavior

The audio should loop cleanly, without any popping noises.

6 Likes

I should also preface that when you set any Sound object to a specific TimePosition to create a looping effect without the use of the newer Audio APIs, there is also a noticeable pop or skip occurring as well with Looped enabled.

Not sure if this is specifically related to FMOD, but it’s quite annoying. Tracks have to be designed to loop back to their initial starting point when programmed TimePosition loops should technically be supported too. These issues make that impossible.

1 Like

Hey @5luau – I’m going to take a look to figure out exactly why these clicks are happening, but off the top of my head some things that might be contributing

  1. The engine uses an internal samplerate of 48kHz, meaning sample-values based on 22050hz can get smudged due to resampling
  2. NumberRanges store 2 32-bit floating point numbers, and neither 25 / 22050 nor 58 / 22050 are exactly-representable, which could contribute to rounding errors

we tend to expose units in seconds/milliseconds in our APIs, but we might need to add an alternative API that takes units in samples to make cases like this easier to work with.

@Vyntrick W.r.t.

Tracks have to be designed to loop back to their initial starting point when programmed TimePosition loops should technically be supported too

The trouble with programmatically seeking is that scripts run on a different thread than where audio is being processed – your scripts only get a chance to set TimePosition every frame, but audio is flowing constantly in the background.

If you tried to create a loop between 3 & 5 seconds with a script like this

audio.TimePosition = 3
audio:Play()

while audio.IsPlaying do
    if shouldLoop and audio.TimePosition > 5 then
        audio.TimePosition = 3
    end
    task.wait() -- *
end

the problem is that task.wait() yields until the next frame, at which point TimePosition may have progressed past the desired loop-end.

1 Like
  1. The engine uses an internal samplerate of 48kHz, meaning sample-values based on 22050hz can get smudged due to resampling

I’m already aware of this, however the clicking doesn’t happen every wave cycle, but rather roughly every second (but sometimes closer to 10 times per second on certain playback speeds), so I think something else is going on here.

  1. NumberRanges store 2 32-bit floating point numbers, and neither 25 / 22050 nor 58 / 22050 are exactly-representable, which could contribute to rounding errors

I’ve already done some digging into this because I initially was having trouble getting the loops to sound right at all. It is correct that rounding errors show up (for example, printing NumberRange.new(0, 1 / 44100).Max * 44100 results in 0.9999999…) And from my testing, it seems as if the audio engine floors instead of rounds these values back up to sample positions. My current workaround is to just add 0.5 to the sample position, however this wasnt necessary for the reproduction steps as both the min and max of the NumberRange I gave give the correct sample positions after multiplying and flooring.

I actually am using a lot of other 22050hz samples in my game, and most of them don’t have the issue either. I think it’s only occurring with extremely short samples

Hey @5luau – I’m able to reproduce the issue and can confirm it’s an engine bug, not rounding or resampling errors. We’ll get it fixed; thanks for reporting!

Is there is feasible way to get this result? The approach I remember using was I watched for changes in the TimePosition of the Sound object and when it hit a specific value I’d jump the TimePosition back to a point in the track where the music would sound like it’s a continuous track with no seams in how it sounds. Obviously due to this engine limitation though, there was an audible skip.

If audio is processed independent of frames, and scripts are entirely frame-bound, is there some sort of math that could be done to compensate for the sounds typical flow, or do you prefer that we do design tracks to depend entirely on Looped (Tracks end and the beginning of the track has the same frequency as the end of the track, causing no audible skip?)

Thank you for the explanation. That clears things up for sure.

If audio is processed independent of frames, and scripts are entirely frame-bound, is there some sort of math that could be done to compensate for the sounds typical flow

Not really – the higher your framerate, the more time-accurate your scripts can be, but 48000 samples-per-second is several orders of magnitude finer-grained than a typical 30, 60, or 120 frames-per-second

If it weren’t for the bug that @5luau reported here, Looped/Looping and LoopRegions would work, because they tell the audio engine in advance when you want the loop to wrap. With that information provided ahead-of-time, the audio engine has time to plan the loop so that it’s sample-accurate on the audio thread.

By contrast, the while loop snippet above is more “reactive” – constantly checking if it’s time to loop now – but “now” may already be too late.

1 Like

Thanks for looking into this btw.

Not really – the higher your framerate, the more time-accurate your scripts can be, but 48000 samples-per-second is several orders of magnitude finer-grained than a typical 30, 60, or 120 frames-per-second

While we’re on this topic, if a call :Play on two different audios consecutively, do they begin playback as soon as the method is called (meaning one will be slightly offset) or are they scheduled to begin at the exact same time? I may end up needing to support a system where two seperate audios are used for the left and right channels, but I’m not sure if its possible to keep them synchronized to the sample.

While we’re on this topic, if a call :Play on two different audios consecutively, do they begin playback as soon as the method is called (meaning one will be slightly offset) or are they scheduled to begin at the exact same time?

Without taking any special steps, they begin playing on the next mix – so not immediately. While the audio engine uses a samplerate of 48kHz, it chunks that up into larger buffers, typically 512 samples at a time. Every few milliseconds (512/48000 → 10.6 ms) there will be a mixer step that produces a chunk of audio.

Note, however, mixer steps don’t usually align with your framerate, and the mixer is running at the same time as your code.

So even though they don’t begin immediately, it’s still possible for a script like this

audio1:Play()
audio2:Play()

to get unlucky, and have a mixer update occur in-between those two lines of code – that would put your two playbacks several milliseconds apart from one another.

I may end up needing to support a system where two seperate audios are used for the left and right channels, but I’m not sure if its possible to keep them synchronized to the sample.

We recently added sample-accurate pre-planning support to AudioPlayer’s Play/Stop methods, so you can achieve this by reworking the above code:

local SoundService = game:GetService("SoundService")
local now = SoundService:GetMixerTime()

--[[
You can probably make this preplanning time smaller,
it only needs to be big enough to guarantee 
that the audio engine sees the whole plan,
before it needs to start playing any of it.
--]]
local preplanningTime = 0.1

local startTime = now + preplanningTime

assert(SoundService:GetMixerTime() < startTime) -- diagnostic, can be removed
audio1:Play(startTime)

assert(SoundService:GetMixerTime() < startTime) -- diagnostic, can be removed
audio2:Play(startTime)

We recently added sample-accurate pre-planning support to AudioPlayer’s Play/Stop methods, so you can achieve this by reworking the above code:

Oh cool, thanks!

Hey @5luau a fix is currently in-review for this

1 Like

Cool, thanks. Let me know when it’s been implemented.

I’m going to slip this information because I have a feeling that this is also related to the engine bug after Roblox Windows client has been updated to the latest 0.731.0.7310943 (version-9affbe66b2624d20). Anyways, our community have started reporting inconsistent popping and clicking sounds in both our game’s MIDI system and Roblox’s voice chat on our Dan’s Karaoke game 2 days ago in our Discord server.

Take note that the game’s MIDI system was upgraded to the new AudioAPI on July 10 but the game hasn’t been updated 13* days ago and there was no ‘popping’ 'til the game client has been updated where the issues started to arise.
and to some people that are not aware, Dan’s Karaoke is also using audio loops for playing instruments for the MIDI system.

Based on our analysis, we are, too, also seeing some weird discontinuity in the waveform on the audio in this game audio recording with Audacity.

The clicking is faint but noticeable.

While we cannot reproduce nor confirm the issue on the voice chat, it seems that the voice chat is being affected if its near any audio source where popping noise is occuring.

Hey @JakeDoesNotG – I think that’s actually related to this other bug report; we have fixes in review for both.

This bug only affects looping sounds themselves (other audio is unaffected), but the linked issue affects the entire audio mixer’s ability to hit its deadlines, which can cause pops/crackles in unrelated audio streams

1 Like

A fix flag has been merged into version 734 – we were hoping to get it landed into 733, but we’ll flip the flag as soon as it’s available on most platforms.

I can confirm enabling FFlagFMODPlaybackChannelDeferLoopWrapToFmod fixes the issue in studio. I will mark this as fixed when it’s live.

Awesome! Unfortunately most platforms are up-to-date, but some are lagging behind (mobile, mainly) so the flag isn’t universally available yet