I want to create a loop that produces a sort of “stacking effect”, by repeating the same sound in a loop before the end of its duration.
Heres an example of this exact sample and how it would sound like in sonic-pi:
This is how its done in Roblox:
local SoundService = game:GetService("SoundService")
local Samples = SoundService.Samples
local expectedInterval = 0.5
local lastTime = tick()
while true do
local currentTime = tick()
local elapsedTime = currentTime - lastTime
lastTime = currentTime
local waitTime = expectedInterval - elapsedTime
if waitTime < 0 then waitTime = 0 end
lastElapsed = task.wait(waitTime)
local c = Samples.loop_mika:Clone()
c.Parent = SoundService
c:Destroy()
end
You might’ve noticed, that I have task.wait() compensation, which would prevent inaccuracies in task.wait() from affecting the sound. Good solution, right?
No. No, it’s not even close to a solution.
Running the bellow code in-game makes the culprit apparent, as the audio occasionally tends to very slightly clip into the next beat, which as you’ve seen here above, snowballs very quickly into an uncontrollable mess.
And so, I am asking, if there is any more precise method of pausing the loop, that isn’t running task.wait(), as that would be the solution to this problem.
The task.wait(0) case was for any negative values (and to be honest, that piece of code in itself is kind of rushed, though any kind of compensation seems to only provide a very, very short baind-aid fix).
There is no reason for me to set the TimePosition of the sample, as I am destroying a PlayOnRemove = true sound for intentional clipping, intending to play it at TimePosition 0, with the entire sound itself being unchanged, only having more of itself layered on over time.
task.wait will not wait the exact amount you put. Instead, it decides to stop waiting after it sees it has taken at least that much time. To do this, the function sets what time the task should be continued, and it will check this every resumption cycle. A cycle happens every frame.
Sounds are also unreliable when it comes to exactly when they play and loop, so that could also be causing an issue.
If you’re talking about my sonic-pi code, then yes. It doesn’t have to compensate for time, because sonic-pi’s sleep implementation is very accurate in time.
If you’re talking about the command-line code at the bottom, that simply plays the sample for a second, then stops, which can be run multiple times, which reveals the occasional clipping/general time inaccuracies.
The sound is cloned, then destroyed, to allow for it to play without having copies of the same sample spawn over and over, preventing any reading of its variables, though, I could try to see how it would work under that implementation.
I’d change the way it’s implemented and not delete the sound, while getting the current TimePosition of the sound to accurately tell where in the audio length it’s being emitted yk
I modified your piece of code and seems to sync up fine.
--!strict
local SoundService = game:GetService("SoundService")
local sound: Sound = script.Sound
local expectedInterval: number = 0.5
local lastTime: number = os.clock()
while true do
local currentTime: number = os.clock()
local elapsedTime: number = currentTime - lastTime
local waitTime: number = expectedInterval - elapsedTime
if waitTime > 0 then
local actualTime: number = task.wait(waitTime)
lastTime += actualTime
end
local sample = sound:Clone()
sample.Parent = SoundService
sample:Destroy()
end
I’ve tried your code, and the same issue takes place (I would’ve recorded the difference, but it sounds basically identical to what I recorded in the top post).
Maybe it’s an issue with the sound itself, though I’d doubt it. It seems fine; by itself it loops perfectly with Looped = true.
you don’t need to get it every time it changes, all you need is to check the value once it’s playing to get the position the sound is at and use that to figure out when the next sound is supposed to start playing yk?
local SoundService = game:GetService("SoundService")
local Samples = SoundService.Samples
local expectedInterval = 0.5
local lastTime = tick()
local function createSound()
local c = Samples.loop_mika:Clone()
c.Parent = Samples
c:Play()
local d = c.Changed:Connect(function()
print(c.TimePosition)
if c.TimePosition >= 1 then d:Disconnect() createSound() end
end)
end
createSound()
I may be a bit off here but, from working with many languages there is a unwritten rule more or less not to call 0 time waits … wait(), wait(0) or the task.wait version. Roblox uses this in some templates so I assume your 0 time calls are not truly 0 time. If I need to go low, I will always use 0.33 or even 0.033. these are old assembler tricks on faking one FPS wait. The fact you are finding a 0 wait not consistent isn’t shocking. Many languages would just lock up with a call like that.
Edit: It would be interesting to see your test using this technique. I got a feeling it will pass with flying colors with a task.wait …
Like this? The wording was a little confusing.
Either way, the issue persists.
local SoundService = game:GetService("SoundService")
local Samples = SoundService.Samples
local offset
while true do
local c = Samples.loop_mika:Clone()
c.Parent = Samples
c:Play() -- doesnt seem to make a difference if its before or after
if not c.IsLoaded then c.Loaded:Wait() end
local nextOffset = c.TimePosition
print(nextOffset) -- still returns 0
if offset then c.TimePosition += offset end
task.wait(math.max(0.5 - nextOffset, 0))
offset = nextOffset
end