Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions amy/test.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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)
Expand Down
142 changes: 93 additions & 49 deletions src/amy.c
Original file line number Diff line number Diff line change
Expand Up @@ -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;j<MAX_ALGO_OPS;j++) AMY_UNSET(psynth->algo_source[j]);
psynth->terminate_on_silence = 1; // This is what we do, *except* for PCM.
for(uint8_t j=0;j<MAX_BREAKPOINT_SETS;j++) {
// max_num_breakpoints describes the alloc for this synthinfo, and is *not* reset.
for(uint8_t k=0;k<psynth->max_num_breakpoints[j];k++) {
Expand All @@ -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;j<MAX_BREAKPOINT_SETS;j++) { psynth->last_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 ) {
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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)

// <none> 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) {
Expand All @@ -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);
Expand Down
44 changes: 20 additions & 24 deletions src/amy.h
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
33 changes: 33 additions & 0 deletions src/filters.c
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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;
}

Expand Down
Binary file modified tests/ref/TestBrass.wav
Binary file not shown.
Binary file modified tests/ref/TestGuitar.wav
Binary file not shown.
Binary file modified tests/ref/TestOwBassClick.wav
Binary file not shown.
Loading