Sample based synth sustain region produces clicks

If you create a synth using a sample and set the sustain frames there's an audible click when the sustain region loops:

local path = "Samples/pcm/sandman_pad", 
self.sample = playdate.sound.sample.new(path)
self.sampleSynth = playdate.sound.synth.new(self.sample, 40804, 125326)

Then play a long note.

To test I've exageratted the sustain boundary - fading in from 40804, and out to 125326 - so it should be looping at a silent point back to a silent point.

Looping this selection in Audacity is smooth, and the frame/sample values come from those loop points, but Playdate produces an audible click at most notes - some lower notes sometimes sustain smoothly (and sound great!).

I may be missig something here, but I think this is an issue in the synth implementation.

Cheers,
Orllewin

2 Likes

Actually, it turns out any sample based synth produces clicks when playing notes... there's something very broken here.

By "any sample based synth" do you mean regardless of looping? Where is the clicking happening?

Regarding the looping, is the file encoded with ADPCM, by chance? For some reason I don't remember now I commented out the code that sets the position inside of the ADPCM block when it loops, so it's always jumps to the beginning of the block instead. I'll take a look at that now, see if I can figure out if that was intentional or not and get it working correctly again.

No, I was sure to use PCM for these. You can hear the clicks and pops in this video: https://www.youtube.com/watch?v=KvH9JL0IWqw

It doesn't always happen, but if you retrigger a lot you get lots of pops. This is all manual so it's repeat calls to playMidiNote. The underlying audio implementation is really simple, just your basic Lua API wrapped in a fancy UI!

So the pops are either happening at the start of playing a new note, or when terminating a currently playing note.

The sustain region too, same deal when it loops (I've turned that off for now)

I've managed to mitigate this (in a quite expensive manner) by fading out the playing note before triggering the new note:

self.releaseTimer = playdate.timer.new(100, 1.0, 0.0)
self.releaseTimer.updateCallback = function(timer)
	self.sampleSynth:setVolume(timer.value)
end
self.releaseTimer.timerEndedCallback = function(timer)
	self.sampleSynth:setVolume(1)
	self.sampleSynth:playMIDINote(math.floor(event:getValue()), 1)
end	

So the pops and clicks are happening when terminating the currently playing note.

(This is all unrelated to the sustain region issue of course!)

I'm not totally clear on how this is put together, but could you use the envelope to fade it out instead?

self.sampleSynth:setRelease(0.1)
self.sampleSynth:setFinishCallback(function(s) s:playMIDINote(math.floor(event:getValue()) end)

About the clicking: I've got a fix for the looping, wasn't chopping the frame up correctly. This looks like another place where I roughed in the code and forgot to go back and finish it. :confused: Explains the lack of docs, too. The click when triggering a note while it's already playing is a bit trickier, since it jumps immediately back to the start of the data.

Screen Shot 2023-10-24 at 4.38.49 PM

A crossfade would be ideal but a bit of work. The simplest workaround I can think of is to use two synths there and ping-pong between them so you can play the note for the next event while letting the previous finish on its own.

1 Like

Aaah brilliant, thank you for looking into it, glad it wasn't me doing something silly. I did notice some improvement with sustain when I turned off dithering when exporting hits from Ableton but it still had noticeable artefacts.

I'll find the lightest way of working around things when retriggering a note, this timer solution is far too heavy (sounds great though: https://www.youtube.com/watch?v=KKrzh5pRDTo ), having a release with callback would be better if I can get it to work - only one synth per 'synth' then... but two synths better than spawning a timer every note!

if self.sampleSynthA:isPlaying() then
	self.sampleSynthA:noteOff()
	self.sampleSynthB:playMIDINote(math.floor(event:getValue()))
elseif self.sampleSynthB:isPlaying() then
	self.sampleSynthB:noteOff()
	self.sampleSynthA:playMIDINote(math.floor(event:getValue()))
else
	self.sampleSynthA:playMIDINote(math.floor(event:getValue()))
end

This works beautifully.

2 Likes

Hey @Orllewin, was this working for you before you started this thread? Since updating to 2.1.1 I noticed many of my sample-based synths are now playing distorted sounds or clicking and popping all over the place, I'm trying to figure out if this is related.

And @dave, you mentioned you had a fix, and given 2.1.0 came out a day after this thread, is it safe to say the next OS release should have that fix?

Not checked lately - if you have distorted sound, including the system sounds when you hit the menu button, you'll need to restart the Playdate - something's leaking somewhere in the audio engine.

Nah, I wish it was that simple, this also happens in the simulator and only for certain sample-based synths that I create and manage, which were working before and haven't changed since. I'll continue to investigate and open a separate bug if needed.