Synth using samples with short loops become distorted when playing high notes

,

Platform: macOS 13
SDK version: 2.5
(Running on Playdate Simulator)

I'm writing a music engine that uses looping samples for its instruments, and some of those samples have very short loops that only encompass a few hundred samples. I'm finding that when they're playing high notes, the output becomes distorted and garbled.

Here's a sample that you can use to demonstrate the issue: 7001_lead-c4.wav.zip (3.2 KB)
and some accompanying code:

The following plays the sample at its normal speed and sounds fine:

AudioSample *sample = pd->sound->sample->load("audio/7001_lead-c4");
PDSynth *synth = pd->sound->synth->newSynth();
pd->sound->synth->setSample(synth, sample, 520, 1368);
pd->sound->channel->addSource(pd->sound->getDefaultChannel(), (SoundSource *)synth);
pd->sound->synth->playMIDINote(synth, NOTE_C4, 0.5, 0.5, 0);

If however you play a C6, i.e.:

pd->sound->synth->playMIDINote(synth, NOTE_C4 + 24, 0.5, 0.5, 0);

...it sounds awful! It buzzes as though the loop start and end points have drifted. (My own speculation as to what could be going wrong.)

P.S. Hopefully the fact that I'm opening up individual threads in a short span of time for the bugs I'm finding isn't an issue. Writing a music engine is really putting the Playdate API through its paces, though!

...and it so happens that in the course of writing that sample code for the above bug report, I found another bug. :sweat: I'll go ahead and post it here:

It seems PDSynth ignores the release portion of its ADSR envelope. The following uses the same sample from above, and you can hear the attack, decay, and sustain phases. But when the note ends, it just stops abruptly rather than fading out for one second:

AudioSample *sample = pd->sound->sample->load("audio/7001_lead-c4");
PDSynth *synth = pd->sound->synth->newSynth();
pd->sound->synth->setSample(synth, sample, 520, 1368);
pd->sound->synth->setAttackTime(synth, 0.2f);
pd->sound->synth->setDecayTime(synth, 0.4f);
pd->sound->synth->setSustainLevel(synth, 0.5f);
pd->sound->synth->setReleaseTime(synth, 1.0);
pd->sound->channel->addSource(pd->sound->getDefaultChannel(), (SoundSource *)synth);
pd->sound->synth->playMIDINote(synth, NOTE_C4, 0.5, 2.0, 0);

This very well may be the same bug as this report I made earlier.

So many great bugs! Thank you so much for finding these and for your excellent reports. This part of the API doesn't get as much use as the more mainstream features, so unfortunately the bugs have a longer shelf life. But anyway..

The weird distortion was because the code wasn't expecting to have to loop more than once in a (256-sample) render cycle, leaving the rest of the data in the update frame blank. Not sure what I was thinking there.. This means with your sustain range of 848 samples it will start glitching above 848/256=3.3125x playback rate, or A5 (if I'm doing the math right). That was an easy fix, but the envelope release problem is a little trickier because it's not immediately obvious what the intended behavior is in different circumstances. Here's what I came up with:

  • If the synth has a sustain range then it's looped while the note is active (same as current, though with the above bug fixed)
  • If the sustain range goes all the way to the end of the sample then it continues to loop that while the release phase of the envelope plays out
  • But if the sample has data after the sustain range then I assume you want to play that instead, so when you release the note we let it play through to the end
  • One last complication here: If the sample has data after the sustain range and you've changed the envelope settings then we apply that--notably, if your release time is zero then the synth stops playing as soon as the note is released and you don't hear the end of the sample. If you don't touch the envelope it will play to the end. (I realize now that we need a "clearEnvelope" function so you can go back to that behavior after you've set an envelope. I'll add that, too..)

Let me know if that makes sense, or if you have any suggestions!

Glad to hear you've managed to fix the sample looping issue! :partying_face:

Regarding the sustain range, I see your point. Since I've been inundating myself in old school tracker music formats like MOD and S3M, I sort of just assumed that an instrument should keep looping during its release phase. But that's largely because those old formats don't support playing out a sample past its looping range anyway!

What occurs to me is that we're dealing with essentially two different but overlapping behaviors: whether or not to exit the sample's loop on release (regardless of its ADSR envelope), and then the ADSR envelope itself which could be configured to have any duration of release phase or no release phase at all. I can see use cases for the four possible combinations of configuring those two behaviors. (That is, 1. loop and stop on release / no release phase, 2. keep looping and fade out, 3. exit the loop and don't fade out, and 4. exit the loop and fade out.)

With your idea and the case of there being sample data after the sustain / loop range, I'm not sure whether you were thinking it would fade out according to the release time, or just play through to the end without fading. But I think we'd want both to be possible.

One idea: there could be an additional function that simply sets what the looping behavior is on release: either keep looping, or play through to the end of the sample. That has the advantage of making everything explicit, and then the user can set up whatever ADSR envelope they want.

Or, going back to your idea, perhaps there could be a setting for the release time that indicates an indefinite release, in which case a note would either keep looping if there is no sample data after the sustain range, or if there is, it plays to the end of the sample without fading.

(Hopefully I explained that all clearly enough!)

this release behaviour you've described, where release basically becomes a fadeout envelope when the sound being played is longer than than the sustain time, is probably more common in DAWs or effect pedal modelling, but not very common in synths.

A more frequently seen behaviour is to use a very shallow reverb and delay effect to create the release based on the last few samples once the note is cutoff.