While I think this should probably be addressed in the Playdate API, here's a custom SoundEffect
that implements pop-free volume changes, for whoever wants to use it. I tried to write it efficiently, though it will almost certainly use more CPU cycles than just setting a channel's volume. The following code has both the implementation for the effect, plus an updated version of my example code above that shows how to use it.
typedef struct SmoothVolumeEffect_ {
SoundEffect *effect;
_Atomic float volume;
int32_t lastVolumeFixed;
int32_t fadeSampleCount;
} SmoothVolumeEffect;
static inline int32_t multiplyQ8_24(int32_t a, int32_t b) {
return (int32_t)(((int64_t)a * (int64_t)b) >> 24);
}
static inline float floatToQ8_24(float f) {
if (f > 127.999999f) {
return INT32_MAX;
}
if (f < -128.0f) {
return INT32_MIN;
}
return (int32_t)roundf(f * (1 << 24));
}
static inline int32_t lerpQ8_24(int32_t a, int32_t b, int32_t x, int32_t y) {
int64_t aa = (int64_t)a * (y - x);
int64_t bb = (int64_t)b * x;
return (int32_t)((aa + bb) / y);
}
int SmoothVolumeEffectCallback(SoundEffect *effect, int32_t *left, int32_t *right, int nsamples, int bufactive)
{
if (bufactive == 0) {
return 0;
}
void *userData = pd->sound->effect->getUserdata(effect);
if (!userData) {
return 0;
}
SmoothVolumeEffect *data = (SmoothVolumeEffect *)userData;
int32_t volumeFixed = floatToQ8_24(data->volume);
int transitionSamples;
if (volumeFixed == data->lastVolumeFixed) {
transitionSamples = 0;
} else {
transitionSamples = (nsamples >= data->fadeSampleCount) ? data->fadeSampleCount : nsamples;
}
for(int i = 0; i < transitionSamples; ++i) {
int32_t vol = lerpQ8_24(data->lastVolumeFixed, volumeFixed, i, transitionSamples);
if (left) {
left[i] = multiplyQ8_24(left[i], vol);
}
if (right) {
right[i] = multiplyQ8_24(left[i], vol);
}
}
if (left) {
for(int i = transitionSamples; i < nsamples; ++i) {
left[i] = multiplyQ8_24(left[i], volumeFixed);
}
}
if (right) {
for(int i = transitionSamples; i < nsamples; ++i) {
right[i] = multiplyQ8_24(left[i], volumeFixed);
}
}
data->lastVolumeFixed = volumeFixed;
return 1;
}
SmoothVolumeEffect * newSmoothVolumeEffect(void)
{
SmoothVolumeEffect *result = pd->system->realloc(NULL, sizeof(SmoothVolumeEffect));
if (!result) {
return NULL;
}
result->effect = pd->sound->effect->newEffect(SmoothVolumeEffectCallback, result);
result->fadeSampleCount = 64;
result->volume = 1.0f;
result->lastVolumeFixed = floatToQ8_24(result->volume);
return result;
}
void freeSmoothVolumeEffect(SmoothVolumeEffect *smoothVolumeEffect)
{
pd->sound->effect->freeEffect(smoothVolumeEffect->effect);
pd->system->realloc(smoothVolumeEffect, 0);
}
int update(void* ud)
{
static uint32_t lastTime = 0;
static SoundChannel *channel = NULL;
static SmoothVolumeEffect *smoothVolumeEffect;
uint32_t currentTime = pd->sound->getCurrentTime();
if (!channel) {
channel = pd->sound->channel->newChannel();
PDSynth *synth = pd->sound->synth->newSynth();
pd->sound->synth->setWaveform(synth, kWaveformSine);
pd->sound->channel->addSource(channel, (SoundSource *)synth);
pd->sound->synth->playMIDINote(synth, NOTE_C4, 0.5, -1.0, 0);
smoothVolumeEffect = newSmoothVolumeEffect();
pd->sound->channel->addEffect(channel, smoothVolumeEffect->effect);
pd->sound->effect->setMix(smoothVolumeEffect->effect, 1.0f);
}
if ((currentTime / 44100) != (lastTime / 44100)) {
smoothVolumeEffect->volume = (smoothVolumeEffect->volume == 1.0f) ? 0.0f : 1.0f;
}
lastTime = currentTime;
return 1;
}