diff --git a/README.md b/README.md index 25ccaef..c61312f 100644 --- a/README.md +++ b/README.md @@ -204,7 +204,7 @@ Here's the full list: | `l` | `vel` | float 0-1+ | Velocity: > 0 to trigger note on, 0 to trigger note off | | `L` | `mod_source` | 0 to OSCS-1 | Which oscillator is used as an modulation/LFO source for this oscillator. Source oscillator will be silent. | | `m` | `portamento` | uint | Time constant (in ms) for pitch changes when note is changed without intervening note-off. default 0 (immediate), 100 is good. | -| `M` | `echo` | float[,int,int,float,float] | Echo parameters -- level, delay_ms, max_delay_ms, feedback, 1st order IIR zero location. | +| `M` | `echo` | float[,int,int,float,float] | Echo parameters -- level, delay_ms, max_delay_ms, feedback, filter_coef (-1 is HPF, 0 is flat, +1 is LPF). | | `N` | `note` | uint 0-127 | Midi note, sets frequency | | `N` | `latency_ms` | uint | Sets latency in ms. default 0 (see LATENCY) | | `o` | `algorithm` | uint 1-32 | DX7 FM algorithm to use for ALGO type | diff --git a/src/amy.c b/src/amy.c index f2e28d1..1366bee 100644 --- a/src/amy.c +++ b/src/amy.c @@ -181,17 +181,20 @@ void config_echo(float level, float delay_ms, float max_delay_ms, float feedback echo.max_delay_samples = max_delay_samples; //fprintf(stderr, "config_echo: max_delay_samples=%d\n", max_delay_samples); } - // Apply delay. - if (delay_samples > echo.max_delay_samples) delay_samples = echo.max_delay_samples; + // Apply delay. We have to stay 1 sample less than delay line length for FIR EQ delay. + if (delay_samples > echo.max_delay_samples - 1) delay_samples = echo.max_delay_samples - 1; for (int c = 0; c < AMY_NCHANS; ++c) { echo_delay_lines[c]->fixed_delay = delay_samples; } } echo.level = F2S(level); echo.delay_samples = delay_samples; - echo.feedback = F2S(feedback); - // FIR filter is [1], [1, filter_coef] but scaled to have peak gain of 1.0. + // Filter is IIR [1, filter_coef] normalized for filter_coef > 0 (LPF), or FIR [1, filter_coef] normalized for filter_coef < 0 (HPF). + if (filter_coef > 0.99) filter_coef = 0.99; // Avoid unstable filters. echo.filter_coef = F2S(filter_coef); + // FIR filter potentially has gain > 1 for high frequencies, so discount the loop feedback to stop things exploding. + if (filter_coef < 0) feedback /= 1.f - filter_coef; + echo.feedback = F2S(feedback); //fprintf(stderr, "config_echo: delay_samples=%d level=%.3f feedback=%.3f filter_coef=%.3f fc0=%.3f\n", delay_samples, level, feedback, filter_coef, S2F(echo.filter_coef)); } diff --git a/src/amy_config.h b/src/amy_config.h index 8e3aa68..bfdadc5 100644 --- a/src/amy_config.h +++ b/src/amy_config.h @@ -98,7 +98,8 @@ extern const uint16_t pcm_samples; // echo setup, Levels etc are SAMPLE (fxpoint), delays are in samples. #define ECHO_DEFAULT_LEVEL 0 #define ECHO_DEFAULT_DELAY_MS 500.f -#define ECHO_DEFAULT_MAX_DELAY_MS 1000.f +// Delay line allocates in 2^n samples at 44k; 743ms is just under 32768 samples. +#define ECHO_DEFAULT_MAX_DELAY_MS 743.f #define ECHO_DEFAULT_FEEDBACK 0 #define ECHO_DEFAULT_FILTER_COEF 0 diff --git a/src/delay.c b/src/delay.c index 070c765..d5a3c35 100644 --- a/src/delay.c +++ b/src/delay.c @@ -85,9 +85,9 @@ void delay_line_in_out(SAMPLE *in, SAMPLE *out, int n_samples, SAMPLE* mod_in, S delay_line->next_in = index_in; } -static inline SAMPLE DEL_OUT(delay_line_t *delay_line, int offset) { +static inline SAMPLE DEL_OUT(delay_line_t *delay_line, int extra_delay) { int out_index = - (delay_line->next_in - (delay_line->fixed_delay - offset)) & (delay_line->len - 1); + (delay_line->next_in - (delay_line->fixed_delay + extra_delay)) & (delay_line->len - 1); return delay_line->samples[out_index]; } @@ -108,17 +108,36 @@ void delay_line_in_out_fixed_delay(SAMPLE *in, SAMPLE *out, int n_samples, int d // Read and write the next n_samples from/to the delay line. // Simplified version of delay_line_in_out() that uses a fixed integer delay // for the whole block. - while(n_samples-- > 0) { - SAMPLE next_in = *in++; - SAMPLE last_out = delay_line->samples[(delay_line->next_in - 1) & (delay_line->len - 1)]; - //SAMPLE sample_1 = DEL_OUT(delay_line, 1); - SAMPLE sample = DEL_OUT(delay_line, 0); - if (filter_coef > 0) - sample += SMULR6(filter_coef, last_out - sample); - else if (filter_coef < 0) - sample += SMULR6(filter_coef, last_out + sample); - DEL_IN(delay_line, next_in + SMULR6(feedback_level, sample)); - *out++ += MUL8_SS(mix_level, sample); // mix delayed + original. + if (filter_coef == 0) { + while(n_samples-- > 0) { + SAMPLE delay_out = DEL_OUT(delay_line, 0); + SAMPLE next_in = *in++ + SMULR6(feedback_level, delay_out); + DEL_IN(delay_line, next_in); + *out++ += MUL8_SS(mix_level, delay_out); + } + } else if (filter_coef > 0) { + // Positive filter coef is a pole on the positive real line, to get low-pass effect. + // We apply the filter on the way *in* to the delay line. + while(n_samples-- > 0) { + SAMPLE delay_out = DEL_OUT(delay_line, 0); + SAMPLE next_in = *in++ + SMULR6(feedback_level, delay_out); + // Peek at the last value we wrote to the delay line to get the most recent filter output. + SAMPLE last_filter_result = delay_line->samples[(delay_line->next_in - 1) & (delay_line->len - 1)]; + SAMPLE filter_result = next_in + SMULR6(filter_coef, last_filter_result - next_in); + DEL_IN(delay_line, filter_result); + *out++ += MUL8_SS(mix_level, delay_out); + } + } else { + // Negative filter coef is a zero on the positive real axis to get high-pass. + // We apply the FIR zero on the way *out* of the delay line. + while(n_samples-- > 0) { + SAMPLE delay_out = DEL_OUT(delay_line, 0); + SAMPLE prev_delay_out = DEL_OUT(delay_line, 1); + SAMPLE output = delay_out + SMULR6(filter_coef, prev_delay_out); + SAMPLE next_in = *in++ + SMULR6(feedback_level, output); + DEL_IN(delay_line, next_in); + *out++ += MUL8_SS(mix_level, output); + } } } diff --git a/test.py b/test.py index 25cf9ad..f963680 100644 --- a/test.py +++ b/test.py @@ -467,7 +467,25 @@ def run(self): amy.send(time=0, osc=0, bp0="0,1,200,0,0,0") amy.send(time=100, osc=0, note=48, vel=1) - + + +class TestEchoLPF(AmyTest): + + def run(self): + amy.echo(level=0.5, delay_ms=200, feedback=0.7, filter_coef=0.9) + amy.send(time=0, osc=0, wave=amy.SAW_DOWN, bp0="0,1,200,0,0,0") + + amy.send(time=100, osc=0, note=48, vel=1) + + +class TestEchoHPF(AmyTest): + + def run(self): + amy.echo(level=0.5, delay_ms=200, feedback=0.7, filter_coef=-0.9) + amy.send(time=0, osc=0, wave=amy.SAW_DOWN, bp0="0,1,200,0,0,0") + + amy.send(time=100, osc=0, note=48, vel=1) + def main(argv): if len(argv) > 1: diff --git a/tests/ref/TestEcho.wav b/tests/ref/TestEcho.wav index e8d2984..f192afb 100644 Binary files a/tests/ref/TestEcho.wav and b/tests/ref/TestEcho.wav differ diff --git a/tests/ref/TestEchoHPF.wav b/tests/ref/TestEchoHPF.wav new file mode 100644 index 0000000..81cf3f4 Binary files /dev/null and b/tests/ref/TestEchoHPF.wav differ diff --git a/tests/ref/TestEchoLPF.wav b/tests/ref/TestEchoLPF.wav new file mode 100644 index 0000000..0a137a9 Binary files /dev/null and b/tests/ref/TestEchoLPF.wav differ