shorepine / amy

AMY - A high-performance fixed-point Music synthesizer librarY for microcontrollers
https://shorepine.github.io/amy/
MIT License
184 stars 11 forks source link

Filter glitch on rapid release #126

Closed dpwe closed 2 months ago

dpwe commented 2 months ago

I'm hearing occasional, inconsistent note-end glitches on some Juno patches, which I suspect are failures of the filter24 block floating point scaling calculation.

The filter attempts to set the scale of the fixed-point calculations so that the 16 bit dynamic range of the multiply inputs are fully utilized (to get good resolution) without overflowing the result. It does this by looking at the magnitude of the inputs as well as the state from the previous block (256 samples), but that's not a guarantee that the state won't grow a lot during the current block and overflow. I think for most filter regimes we're OK, but it's possible that the very abrupt change in filter characteristics during a rapid envelope change may be derailing. (This could just be the mismatched state, not necessarily the fixed-point calculation I suppose, but I think the problem goes away under true floating-point calculation).

Steps to reproduce: With Juno-6, enable just the Saw oscillator. Set VCF freq to around 50 and resonance to 0. Set VCF env follow to 64 and Kybd follow to 127. Set A, S, and R to 0, and D to 127. Now, the ADSR is a gate - the envelope drops to zero the moment a key is released. Since the filter is following the env, that means the filter freq suddenly steps down. Play a note, and about half the time you'll get a huge "clunk" at the end, as of an unstable filter. It's very short, but much louder than the note itself. It varies I think depending on the exact timing and level of the note, it certainly seems just kinda random.

I'll attempt to add a test to Amy test.py to reproduce this. The solution may be clamping the filter frequency slew rate - set the release (R) to even just 1 and it's mostly fixed.

dpwe commented 2 months ago

I was able to add a simple repro to test.py:

    amy.send(time=0, osc=0, wave=amy.SAW_DOWN, filter_type=amy.FILTER_LPF24, filter_freq='100,0,0,6')
    amy.send(time=100, note=64, vel=1)
    amy.send(time=500, vel=0)

To my surprise, this causes a massive glitch even using float-point math (undefine AMY_USE_FIXEDPOINT). So state propagation across abrupt filter param changes is the remaining suspect. I'll capture the actual filter param changes.

dpwe commented 2 months ago

Yes, the filter cutoff frequency goes from 6400 Hz to 100 Hz in a single step. The feed-forward filter coefficients go from ~0.25 to ~0.0001 (2500x smaller), which I think means the state scale needs to change by a similar factor.

Flpf t=0.499229 f=6400.000000 q=0.700000 alpha 0.564739 b0 0.123890 b1 0.247781 b2 0.123890 a1 -0.782607 a2 0.278168 r -99.000000 theta 0.911846
Slpf t=0.499229 f=6400.000000 q=0.700000 b0 -0.123890 b1 -0.247781 b2 -0.123890 a1 -0.782607 a2 0.278168
Flpf t=0.505034 f=100.000000 q=0.700000 alpha 0.010177 b0 0.000050 b1 0.000100 b2 0.000050 a1 -1.979651 a2 0.979852 r -99.000000 theta 0.014248
Slpf t=0.505034 f=100.000000 q=0.700000 b0 -0.000050 b1 -0.000100 b2 -0.000050 a1 -1.979651 a2 0.979852