Provide a way to set a channel's volume without creating popping artifacts

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;
}