diff --git a/amy/test.py b/amy/test.py index 2e45626..c0e66a8 100644 --- a/amy/test.py +++ b/amy/test.py @@ -722,7 +722,7 @@ def run(self): amy.send(time=800, synth=0, vel=0) class TestOwBassClick(AmyTest): - """Hearing clicks on OwBass??.""" + """Hearing clicks on OwBass??. See https://github.com/shorepine/amy/issues/629. """ def run(self): amy.send(time=0, synth=0, num_voices=1, oscs_per_voice=3) @@ -1177,8 +1177,8 @@ def main(argv): #TestRestartFileSample().test() #TestDiskSample().test() #print(TestFileTransfer().test()[1]) - #TestVoiceStealClick().test() - TestOwBassClick().test() + #print(TestVoiceStealClick().test()[1]) + print(TestOwBassClick().test()[1]) if not quiet: amy.send(debug=0) diff --git a/src/amy.c b/src/amy.c index 7b3e843..a3e61e9 100644 --- a/src/amy.c +++ b/src/amy.c @@ -634,53 +634,51 @@ void amy_event_to_deltas_queue(amy_event *e, uint16_t base_osc, struct delta **q } -void reset_osc_by_pointer(struct synthinfo *psynth, struct mod_synthinfo *pmsynth) { - // set synth state to defaults given a pointer +void reset_modosc(struct mod_synthinfo *pmsynth) { + if (pmsynth != NULL) { + pmsynth->last_duty = 0.5f; + pmsynth->amp = 0; // This matters for wave=PARTIAL, where msynth amp is effectively 1-frame delayed. + pmsynth->last_amp = 0; + pmsynth->logfreq = 0; + pmsynth->filter_logfreq = 0; + AMY_UNSET(pmsynth->last_filter_logfreq); + pmsynth->duty = 0.5f; + pmsynth->pan = 0.5f; + pmsynth->feedback = F2S(0); //.996; todo ks feedback is v different from fm feedback + pmsynth->resonance = 0.7f; + } +} + +void reset_osc_params(struct synthinfo *psynth) { + // osc params are the things set through the amy_event API + // Event-derived config psynth->wave = SINE; AMY_UNSET(psynth->preset); + AMY_UNSET(psynth->note_source); AMY_UNSET(psynth->midi_note); - for (int j = 0; j < NUM_COMBO_COEFS; ++j) - psynth->amp_coefs[j] = 0; - psynth->amp_coefs[COEF_CONST] = 1.0f; // Mostly a no-op, but partials_note_on used to want this? + psynth->velocity = 0; + for (int j = 0; j < NUM_COMBO_COEFS; ++j) psynth->amp_coefs[j] = 0; + psynth->amp_coefs[COEF_CONST] = 1.0f; // Partials_note_on used to want this? psynth->amp_coefs[COEF_VEL] = 1.0f; psynth->amp_coefs[COEF_EG0] = 1.0f; - for (int j = 0; j < NUM_COMBO_COEFS; ++j) - psynth->logfreq_coefs[j] = 0; + for (int j = 0; j < NUM_COMBO_COEFS; ++j) psynth->logfreq_coefs[j] = 0; psynth->logfreq_coefs[COEF_NOTE] = 1.0; psynth->logfreq_coefs[COEF_BEND] = 1.0; - for (int j = 0; j < NUM_COMBO_COEFS; ++j) - psynth->filter_logfreq_coefs[j] = 0; - for (int j = 0; j < NUM_COMBO_COEFS; ++j) - psynth->duty_coefs[j] = 0; + for (int j = 0; j < NUM_COMBO_COEFS; ++j) psynth->filter_logfreq_coefs[j] = 0; + for (int j = 0; j < NUM_COMBO_COEFS; ++j) psynth->duty_coefs[j] = 0; psynth->duty_coefs[COEF_CONST] = 0.5f; - for (int j = 0; j < NUM_COMBO_COEFS; ++j) - psynth->pan_coefs[j] = 0; + for (int j = 0; j < NUM_COMBO_COEFS; ++j) psynth->pan_coefs[j] = 0; psynth->pan_coefs[COEF_CONST] = 0.5f; psynth->feedback = F2S(0); //.996; todo ks feedback is v different from fm feedback - psynth->phase = F2P(0); AMY_UNSET(psynth->trigger_phase); AMY_UNSET(psynth->logratio); - psynth->resonance = 0.7f; psynth->portamento_alpha = 0; - psynth->velocity = 0; - psynth->step = 0; - AMY_UNSET(psynth->note_source); - psynth->mod_value = F2S(0); - psynth->substep = 0; - psynth->status = SYNTH_OFF; + psynth->resonance = 0.7f; + psynth->filter_type = FILTER_NONE; AMY_UNSET(psynth->chained_osc); AMY_UNSET(psynth->mod_source); - AMY_UNSET(psynth->render_clock); - AMY_UNSET(psynth->note_on_clock); - psynth->note_off_clock = 0; // Used to check that last event seen by note was off. - AMY_UNSET(psynth->zero_amp_clock); - AMY_UNSET(psynth->mod_value_clock); - psynth->filter_type = FILTER_NONE; - for(int j = 0; j < 2 * FILT_NUM_DELAYS; ++j) psynth->filter_delay[j] = 0; - psynth->last_filt_norm_bits = 0; psynth->algorithm = 0; for(uint8_t j=0;jalgo_source[j]); - psynth->terminate_on_silence = 1; // This is what we do, *except* for PCM. for(uint8_t j=0;jmax_num_breakpoints[j];k++) { @@ -689,22 +687,36 @@ void reset_osc_by_pointer(struct synthinfo *psynth, struct mod_synthinfo *pmsynt } psynth->eg_type[j] = ENVELOPE_NORMAL; } + // Not part of event-driven config, but still stable through the evolution of a note. + psynth->terminate_on_silence = 1; // This is what we do, *except* for PCM. + psynth->lut = NULL; +} + +void reset_osc_state(struct synthinfo *psynth) { + // osc state are the internal values that keep track of the osc evolution in time. + psynth->status = SYNTH_OFF; + psynth->phase = F2P(0); + psynth->step = 0; + psynth->substep = 0; + AMY_UNSET(psynth->render_clock); + AMY_UNSET(psynth->note_on_clock); + psynth->note_off_clock = 0; // Used to check that last event seen by note was off. + AMY_UNSET(psynth->zero_amp_clock); + AMY_UNSET(psynth->mod_value_clock); + psynth->mod_value = F2S(0); for(uint8_t j=0;jlast_scale[j] = 0; } psynth->last_two[0] = 0; psynth->last_two[1] = 0; - psynth->lut = NULL; - if (pmsynth != NULL) { - pmsynth->last_duty = 0.5f; - pmsynth->amp = 0; // This matters for wave=PARTIAL, where msynth amp is effectively 1-frame delayed. - pmsynth->last_amp = 0; - pmsynth->logfreq = 0; - pmsynth->filter_logfreq = 0; - AMY_UNSET(pmsynth->last_filter_logfreq); - pmsynth->duty = 0.5f; - pmsynth->pan = 0.5f; - pmsynth->feedback = F2S(0); //.996; todo ks feedback is v different from fm feedback - pmsynth->resonance = 0.7f; - } + for(int j = 0; j < 2 * FILT_NUM_DELAYS; ++j) psynth->filter_delay[j] = 0; + psynth->last_filt_norm_bits = 0; +} + +void reset_osc_by_pointer(struct synthinfo *psynth, struct mod_synthinfo *pmsynth) { + // set synth state to defaults given a pointer + // Current state + reset_osc_params(psynth); + reset_osc_state(psynth); + reset_modosc(pmsynth); } void reset_osc(uint16_t i ) { @@ -1391,11 +1403,12 @@ void hold_and_modify(uint16_t osc) { float last_logfreq = msynth[osc]->last_filter_logfreq; if (filter_logfreq < last_logfreq - MAX_DELTA_FILTER_LOGFREQ_DOWN) { // Filter cutoff downward slew-rate limit. + // See https://github.com/shorepine/amy/issues/126 filter_logfreq = last_logfreq - MAX_DELTA_FILTER_LOGFREQ_DOWN; } } - msynth[osc]->filter_logfreq = filter_logfreq; msynth[osc]->last_filter_logfreq = filter_logfreq; + msynth[osc]->filter_logfreq = filter_logfreq; msynth[osc]->duty = combine_controls(ctrl_inputs, synth[osc]->duty_coefs); msynth[osc]->pan = combine_controls(ctrl_inputs, synth[osc]->pan_coefs); // amp is a special case - coeffs apply in log domain. @@ -1538,10 +1551,34 @@ SAMPLE render_osc_wave(uint16_t osc, uint8_t core, SAMPLE* buf) { synth[osc]->status = SYNTH_AUDIBLE_SUSPENDED; // It *could* come back... // .. but force it to start at zero phase next time. synth[osc]->phase = 0; - //reset_filter(osc); // 2026-03-22: It's necessary to reset these two fields in msynth to get OwBass to restart without click... - msynth[osc]->filter_logfreq = 0; - msynth[osc]->resonance = 0.7f; + msynth[osc]->filter_logfreq = 0; // (a) + msynth[osc]->resonance = 0.7f; // (b) + //reset_filter(osc); // (c) + //AMY_UNSET(msynth[osc]->last_filter_logfreq); // (d) + + // err=-33.6 dB + // (d) err=-33.6 dB + // (e) err=-35.6 dB + // (a) + (b) + (e) err=-35.6 dB + // (a) err=-41.1 dB + // (b) err=-37.7 dB + // (a) + (b) err=-100.0 dB + // (a) + (b) + (d) err=-100.0 dB + // (c) err=-50.5 dB + // (c) + (e) err=-50.5 dB + // (a) + (c) err=-50.5 dB + // (a) + (c) + (e) err=-50.5 dB + // (a) + (b) + (c) err=-50.5 dB + // (a) + (b) + (c) + (e) err=-50.5 dB + // (a) + (b) + (c) + (d) + (e) err=-50.5 dB + // + // Preserve the open-filter ring-out, then clear the filter state: + // (a) + (b) + (f) err=-87.8 dB just as good as -100 + // (f) err=-35.8 dB cutoff delayed, same prob + // + // (c) always gives -50.5 .. so emptying the filter state negates the impact of a,b,e + // (d) never makes any difference .. so it's not slew rate? } } } else if (max_val == 0) { @@ -1565,12 +1602,19 @@ void amy_render(uint16_t start, uint16_t end, uint8_t core) { SAMPLE max_val = render_osc_wave(osc, core, per_osc_fb[core]); // check it's not off, just in case. todo, why do i care? // apply filter to osc if set - if(synth[osc]->filter_type != FILTER_NONE) { + if(//synth[osc]->status == SYNTH_AUDIBLE && // (e) + synth[osc]->filter_type != FILTER_NONE) { //fprintf(stderr, "time %.3f osc %d filter_type %d\n", // (float)amy_global.total_blocks*AMY_BLOCK_SIZE / AMY_SAMPLE_RATE, // osc, synth[osc]->filter_type); max_val = filter_process(per_osc_fb[core], osc, max_val); - } + // Maybe clear filter state here if we've finshed this osc. + if (synth[osc]->status != SYNTH_AUDIBLE) { + reset_filter(osc); // (f) + reset_modosc(msynth[osc]); // (g) This makes a difference, but not clicks + reset_osc_state(synth[osc]); + } + } uint8_t handled = 0; if(amy_global.config.amy_external_render_hook != NULL) { handled = amy_global.config.amy_external_render_hook(osc, per_osc_fb[core], AMY_BLOCK_SIZE); diff --git a/src/amy.h b/src/amy.h index 05aea82..3295e58 100644 --- a/src/amy.h +++ b/src/amy.h @@ -522,55 +522,51 @@ typedef struct amy_event { // This is the state of each oscillator, set by the sequencer from deltas struct synthinfo { uint16_t osc; // self-reference + // Configuration (can be fixed during oscillation) uint16_t wave; int16_t preset; // Negative preset is voice count for build-your-own PARTIALS + uint8_t note_source; // Was the most recent note on/off received e.g. from MIDI? float midi_note; + float velocity; float amp_coefs[NUM_COMBO_COEFS]; float logfreq_coefs[NUM_COMBO_COEFS]; float filter_logfreq_coefs[NUM_COMBO_COEFS]; float duty_coefs[NUM_COMBO_COEFS]; float pan_coefs[NUM_COMBO_COEFS]; float feedback; - uint8_t status; // not in event - float velocity; float trigger_phase; - PHASOR phase; // not in event - float step; // not in event - float substep; // not in event - SAMPLE mod_value; // last value returned by this oscillator when acting as a MOD_SOURCE, not in event float logratio; - float resonance; float portamento_alpha; + float resonance; + uint8_t filter_type; uint16_t chained_osc; uint16_t mod_source; uint8_t algorithm; - uint8_t filter_type; - // algo_source remains int16 because users can add -1 to indicate no osc - int16_t algo_source[MAX_ALGO_OPS]; - uint8_t terminate_on_silence; // Do we enable the auto-termination of silent oscs? Usually yes, not for PCM. not in event. - // Rum-time state, not in event + int16_t algo_source[MAX_ALGO_OPS]; // int16 not uint because -1 specified to indicate no osc + uint8_t eg_type[MAX_BREAKPOINT_SETS]; // one of the ENVELOPE_ values + uint8_t max_num_breakpoints[MAX_BREAKPOINT_SETS]; // alloc'd length of breakpoint_times/vals + uint32_t *breakpoint_times[MAX_BREAKPOINT_SETS]; // (in samples) dynamically sized. + float *breakpoint_values[MAX_BREAKPOINT_SETS]; // dynamically sized. + // Per-note state (set on initialization, does not change during note) + uint8_t terminate_on_silence; // Usually yes, not for PCM. not in event. + const LUT *lut; // Selected lookup table and size. + // Per-block state (changes with time) + uint8_t status; // not in event + PHASOR phase; // not in event + float step; // not in event + float substep; // not in event uint32_t render_clock; uint32_t note_on_clock; uint32_t note_off_clock; uint32_t zero_amp_clock; // Time amplitude hits zero. uint32_t mod_value_clock; // Only calculate mod_value once per frame (for mod_source). - // Back to params - uint32_t *breakpoint_times[MAX_BREAKPOINT_SETS]; // (in samples) was [MAX_BREAKPOINTS] now dynamically sized. - float *breakpoint_values[MAX_BREAKPOINT_SETS]; // was [MAX_BREAKPOINTS] now dynamically sized. - uint8_t eg_type[MAX_BREAKPOINT_SETS]; // one of the ENVELOPE_ values + SAMPLE mod_value; // last value returned by this oscillator when acting as a MOD_SOURCE, not in event SAMPLE last_scale[MAX_BREAKPOINT_SETS]; // remembers current envelope level, to use as start point in release. - uint8_t max_num_breakpoints[MAX_BREAKPOINT_SETS]; // actual length of breakpoint_times/breakpoint values - - // Selected lookup table and size. - const LUT *lut; - // For ALGO feedback ops - SAMPLE last_two[2]; + SAMPLE last_two[2]; // For ALGO feedback ops // For filters. Need 2x because LPF24 uses two instances of filter. SAMPLE filter_delay[2 * FILT_NUM_DELAYS]; // The block-floating-point shift of the filter delay values. int last_filt_norm_bits; - // Was the most recent note on/off received e.g. from MIDI? - uint8_t note_source; }; // synthinfo, but only the things that mods/env can change. one per osc diff --git a/src/filters.c b/src/filters.c index b8ca42c..e75160a 100644 --- a/src/filters.c +++ b/src/filters.c @@ -758,6 +758,23 @@ SAMPLE filter_process(SAMPLE * block, uint16_t osc, SAMPLE max_val) { } AMY_PROFILE_STOP(FILTER_PROCESS_STAGE0) +#ifdef NOTDEF + printf("FlPr t=%.3f f=%.3f q=%.3f %.3f %.3f %.3f %.3f %.3f ST %.6f %.6f %.6f %.6f %.6f %.6f %.6f %.6f B %.6f %.6f %.6f %.6f\n", + amy_global.total_blocks*AMY_BLOCK_SIZE / (float)AMY_SAMPLE_RATE, + ratio * AMY_SAMPLE_RATE, msynth[osc]->resonance, + S2F(coeffs[0]), S2F(coeffs[1]), S2F(coeffs[2]), S2F(coeffs[3]), S2F(coeffs[4]), + S2F(synth[osc]->filter_delay[0]), + S2F(synth[osc]->filter_delay[1]), + S2F(synth[osc]->filter_delay[2]), + S2F(synth[osc]->filter_delay[3]), + S2F(synth[osc]->filter_delay[4]), + S2F(synth[osc]->filter_delay[5]), + S2F(synth[osc]->filter_delay[6]), + S2F(synth[osc]->filter_delay[7]), + S2F(block[0]), S2F(block[1]), S2F(block[2]), S2F(block[3]) + ); +#endif + AMY_PROFILE_START(FILTER_PROCESS_STAGE1) //SAMPLE max_val = scan_max(block, AMY_BLOCK_SIZE); // Also have to consider the filter state. @@ -791,6 +808,22 @@ SAMPLE filter_process(SAMPLE * block, uint16_t osc, SAMPLE max_val) { // Final high-pass to remove residual DC offset from sub-fundamental LPF. (Not needed now source waveforms are zero-mean). AMY_PROFILE_STOP(FILTER_PROCESS_STAGE1) AMY_PROFILE_STOP(FILTER_PROCESS) +#ifdef NOTDEF + printf("FlP2 t=%.3f f=%.3f q=%.3f %.3f %.3f %.3f %.3f %.3f ST %.6f %.6f %.6f %.6f %.6f %.6f %.6f %.6f B %.6f %.6f %.6f %.6f\n", + amy_global.total_blocks*AMY_BLOCK_SIZE / (float)AMY_SAMPLE_RATE, + ratio * AMY_SAMPLE_RATE, msynth[osc]->resonance, + S2F(coeffs[0]), S2F(coeffs[1]), S2F(coeffs[2]), S2F(coeffs[3]), S2F(coeffs[4]), + S2F(synth[osc]->filter_delay[0]), + S2F(synth[osc]->filter_delay[1]), + S2F(synth[osc]->filter_delay[2]), + S2F(synth[osc]->filter_delay[3]), + S2F(synth[osc]->filter_delay[4]), + S2F(synth[osc]->filter_delay[5]), + S2F(synth[osc]->filter_delay[6]), + S2F(synth[osc]->filter_delay[7]), + S2F(block[0]), S2F(block[1]), S2F(block[2]), S2F(block[3]) + ); +#endif return max_val; } diff --git a/tests/ref/TestBrass.wav b/tests/ref/TestBrass.wav index 67930e6..4bfaa15 100644 Binary files a/tests/ref/TestBrass.wav and b/tests/ref/TestBrass.wav differ diff --git a/tests/ref/TestGuitar.wav b/tests/ref/TestGuitar.wav index 100ea11..c948395 100644 Binary files a/tests/ref/TestGuitar.wav and b/tests/ref/TestGuitar.wav differ diff --git a/tests/ref/TestOwBassClick.wav b/tests/ref/TestOwBassClick.wav index 7f54881..cfef9db 100644 Binary files a/tests/ref/TestOwBassClick.wav and b/tests/ref/TestOwBassClick.wav differ