From 21fc76f42ce629a0c6b2f7f3ee5fb14288f04b96 Mon Sep 17 00:00:00 2001 From: kolten Date: Wed, 8 Jan 2025 10:44:45 -0700 Subject: [PATCH] add support for syncing oscillator waveforms --- src/amy.c | 7 +++- src/amy.h | 3 ++ src/oscillators.c | 97 ++++++++++++++++++++++++++++++++++++++++++----- 3 files changed, 96 insertions(+), 11 deletions(-) diff --git a/src/amy.c b/src/amy.c index 01da61cf..bb4936e6 100644 --- a/src/amy.c +++ b/src/amy.c @@ -387,6 +387,7 @@ struct event amy_default_event() { AMY_UNSET(e.filter_type); AMY_UNSET(e.chained_osc); AMY_UNSET(e.mod_source); + AMY_UNSET(e.sync_source); AMY_UNSET(e.eq_l); AMY_UNSET(e.eq_m); AMY_UNSET(e.eq_h); @@ -528,6 +529,7 @@ void amy_add_event_internal(struct event e, uint16_t base_osc) { if(AMY_IS_SET(e.chained_osc)) { e.chained_osc += base_osc; d.param=CHAINED_OSC; d.data = *(uint32_t *)&e.chained_osc; add_delta_to_queue(d); } if(AMY_IS_SET(e.reset_osc)) { e.reset_osc += base_osc; d.param=RESET_OSC; d.data = *(uint32_t *)&e.reset_osc; add_delta_to_queue(d); } if(AMY_IS_SET(e.mod_source)) { e.mod_source += base_osc; d.param=MOD_SOURCE; d.data = *(uint32_t *)&e.mod_source; add_delta_to_queue(d); } + if(AMY_IS_SET(e.sync_source)) { e.sync_source += base_osc; d.param=SYNC_SOURCE; d.data = *(uint32_t *)&e.sync_source; add_delta_to_queue(d); } if(AMY_IS_SET(e.filter_type)) { d.param=FILTER_TYPE; d.data = *(uint32_t *)&e.filter_type; add_delta_to_queue(d); } if(AMY_IS_SET(e.algorithm)) { d.param=ALGORITHM; d.data = *(uint32_t *)&e.algorithm; add_delta_to_queue(d); } if(AMY_IS_SET(e.eq_l)) { d.param=EQ_L; d.data = *(uint32_t *)&e.eq_l; add_delta_to_queue(d); } @@ -637,6 +639,7 @@ void reset_osc(uint16_t i ) { synth[i].status = SYNTH_OFF; AMY_UNSET(synth[i].chained_osc); AMY_UNSET(synth[i].mod_source); + AMY_UNSET(synth[i].sync_source); AMY_UNSET(synth[i].render_clock); AMY_UNSET(synth[i].note_on_clock); synth[i].note_off_clock = 0; // Used to check that last event seen by note was off. @@ -1003,6 +1006,7 @@ void play_event(struct delta d) { // Remove default amplitude dependence on velocity when an oscillator is made a modulator. synth[mod_osc].amp_coefs[COEF_VEL] = 0; } + if(d.param == SYNC_SOURCE) synth[d.osc].sync_source = *(uint16_t*)&d.data; if(d.param == RATIO) synth[d.osc].logratio = *(float *)&d.data; @@ -1878,6 +1882,7 @@ struct event amy_parse_message(char * message) { e.eq_h = eq[2]; } break; + case 'y': e.sync_source=atoi(message + start); break; case 'z': { uint32_t sm[6]; // patch, length, SR, midinote, loopstart, loopend parse_list_uint32_t(message+start, sm, 6, 0); @@ -1889,7 +1894,7 @@ struct event amy_parse_message(char * message) { } break; } - /* Y,y available */ + /* Y available */ /* Z used for end of message */ default: break; diff --git a/src/amy.h b/src/amy.h index b3911b13..83a21a94 100644 --- a/src/amy.h +++ b/src/amy.h @@ -165,6 +165,7 @@ enum params{ MOD_SOURCE, FILTER_TYPE, // 48, 49 EQ_L, EQ_M, EQ_H, // 50, 51, 52 ALGORITHM, LATENCY, TEMPO, // 53, 54, 55 + SYNC_SOURCE, // 56 ALGO_SOURCE_START=100, // 100..105 ALGO_SOURCE_END=100+MAX_ALGO_OPS, // 106 BP_START=ALGO_SOURCE_END + 1, // 107..138 @@ -295,6 +296,7 @@ struct event { uint16_t portamento_ms; uint16_t chained_osc; uint16_t mod_source; + uint16_t sync_source; uint8_t algorithm; uint8_t filter_type; float eq_l; @@ -337,6 +339,7 @@ struct synthinfo { float portamento_alpha; uint16_t chained_osc; uint16_t mod_source; + uint16_t sync_source; uint8_t algorithm; uint8_t filter_type; // algo_source remains int16 because users can add -1 to indicate no osc diff --git a/src/oscillators.c b/src/oscillators.c index 212395e3..5a16fac6 100644 --- a/src/oscillators.c +++ b/src/oscillators.c @@ -186,7 +186,7 @@ PHASOR render_lut_fm(SAMPLE* buf, } PHASOR render_lut(SAMPLE* buf, - PHASOR phase, + PHASOR phase, PHASOR step, SAMPLE incoming_amp, SAMPLE ending_amp, const LUT* lut, @@ -205,6 +205,7 @@ PHASOR render_lut(SAMPLE* buf, return phase; } + PHASOR render_lut_cub(SAMPLE* buf, PHASOR phase, PHASOR step, @@ -225,6 +226,54 @@ PHASOR render_lut_cub(SAMPLE* buf, return phase; } +//accepting some repetition to not add more branching to the normal render loop +PHASOR render_synced_lut(SAMPLE* buf, + PHASOR phase, PHASOR step, + PHASOR sync, PHASOR sync_step, + SAMPLE incoming_amp, SAMPLE ending_amp, + const LUT* lut, + SAMPLE* pmax_value) { + AMY_PROFILE_START(RENDER_LUT) + RENDER_LUT_PREAMBLE + for(uint16_t i = 0; i < AMY_BLOCK_SIZE; i++) { + PHASOR total_phase = phase; + + RENDER_LUT_GUTS(NOTHING, NOTHING, INTERP_LINEAR) + + RENDER_LUT_LOOP_END + + if (sync + sync_step > 1.0f) phase = 0; + sync = P_WRAPPED_SUM(sync, sync_step); + + } + *pmax_value = max_value; + AMY_PROFILE_STOP(RENDER_LUT) + return phase; +} + +PHASOR render_synced_lut_cub(SAMPLE* buf, + PHASOR phase, PHASOR step, + PHASOR sync, PHASOR sync_step, PHASOR sync_start, + SAMPLE incoming_amp, SAMPLE ending_amp, + const LUT* lut, + SAMPLE *pmax_value) { + AMY_PROFILE_START(RENDER_LUT_CUB) + RENDER_LUT_PREAMBLE + for(uint16_t i = 0; i < AMY_BLOCK_SIZE; i++) { + PHASOR total_phase = phase; + + RENDER_LUT_GUTS(NOTHING, NOTHING, INTERP_CUBIC) + + RENDER_LUT_LOOP_END + + if (sync + sync_step > 1.0f) phase = sync_start; + sync = P_WRAPPED_SUM(sync, sync_step); + } + *pmax_value = max_value; + AMY_PROFILE_STOP(RENDER_LUT_CUB) + return phase; +} + /* Pulse wave */ void pulse_note_on(uint16_t osc, float freq) { //printf("pulse_note_on: time %lld osc %d logfreq %f amp %f last_amp %f\n", total_samples, osc, synth[osc].logfreq, msynth[osc].amp, msynth[osc].last_amp); @@ -241,15 +290,29 @@ SAMPLE render_lpf_lut(SAMPLE* buf, uint16_t osc, int8_t is_square, int8_t direct SAMPLE last_amp = direction * F2S(msynth[osc].last_amp); PHASOR pwm_phase = synth[osc].phase; SAMPLE max_value; - synth[osc].phase = render_lut_cub(buf, synth[osc].phase, step, last_amp, amp, synth[osc].lut, &max_value); + int8_t is_synced = AMY_IS_SET(synth[osc].sync_source) && synth[osc].sync_source != osc; + PHASOR sync_phase, sync_step; + if (is_synced) { + sync_step = F2P(freq_of_logfreq(msynth[synth[osc].sync_source].logfreq) / (float) AMY_SAMPLE_RATE); + sync_phase = synth[synth[osc].sync_source].phase; + synth[osc].phase = render_synced_lut_cub(buf, synth[osc].phase, step, sync_phase, sync_step, F2P(0), last_amp, amp, synth[osc].lut, &max_value); + } else + synth[osc].phase = render_lut_cub(buf, synth[osc].phase, step, last_amp, amp, synth[osc].lut, &max_value); + if (is_square) { // For pulse only, add a second delayed negative LUT wave. float duty = msynth[osc].duty; - if (duty < 0.01f) duty = 0.01f; - if (duty > 0.99f) duty = 0.99f; - pwm_phase = P_WRAPPED_SUM(pwm_phase, F2P(msynth[osc].last_duty)); - // Second pulse is given some blockwise-constant FM to maintain phase continuity across blocks. - PHASOR delta_phase_per_sample = F2P((duty - msynth[osc].last_duty) / AMY_BLOCK_SIZE); - render_lut_cub(buf, pwm_phase, step + delta_phase_per_sample, -last_amp, -amp, synth[osc].lut, &max_value); + if (is_synced) { + //we aren't as worried about phase continuity when syncing + pwm_phase = P_WRAPPED_SUM(pwm_phase, F2P(duty)); + render_synced_lut_cub(buf, pwm_phase, step, sync_phase, sync_step, F2P(duty), -last_amp, -amp, synth[osc].lut, &max_value); + } else { + // Second pulse is given some blockwise-constant FM to maintain phase continuity across blocks. + if (duty < 0.01f) duty = 0.01f; + if (duty > 0.99f) duty = 0.99f; + pwm_phase = P_WRAPPED_SUM(pwm_phase, F2P(msynth[osc].last_duty)); + PHASOR delta_phase_per_sample = F2P((duty - msynth[osc].last_duty) / AMY_BLOCK_SIZE); + render_lut_cub(buf, pwm_phase, step + delta_phase_per_sample, -last_amp, -amp, synth[osc].lut, &max_value); + } msynth[osc].last_duty = duty; } // Remember last_amp. @@ -361,7 +424,13 @@ SAMPLE render_triangle(SAMPLE* buf, uint16_t osc) { SAMPLE amp = F2S(msynth[osc].amp); SAMPLE last_amp = F2S(msynth[osc].last_amp); SAMPLE max_value; - synth[osc].phase = render_lut(buf, synth[osc].phase, step, last_amp, amp, synth[osc].lut, &max_value); + + if (AMY_IS_SET(synth[osc].sync_source) && synth[osc].sync_source != osc) { + PHASOR sync_step = F2P(freq_of_logfreq(msynth[synth[osc].sync_source].logfreq) / (float) AMY_SAMPLE_RATE); + PHASOR sync_phase = synth[synth[osc].sync_source].phase; + synth[osc].phase = render_synced_lut(buf, synth[osc].phase, step, sync_phase, sync_step, last_amp, amp, synth[osc].lut, &max_value); + } else + synth[osc].phase = render_lut(buf, synth[osc].phase, step, last_amp, amp, synth[osc].lut, &max_value); msynth[osc].last_amp = msynth[osc].amp; return max_value; } @@ -447,7 +516,13 @@ SAMPLE render_sine(SAMPLE* buf, uint16_t osc) { SAMPLE last_amp = F2S(msynth[osc].last_amp); //fprintf(stderr, "render_sine: time %f osc %d freq %f last_amp %f amp %f\n", total_samples / (float)AMY_SAMPLE_RATE, osc, AMY_SAMPLE_RATE * P2F(step), S2F(last_amp), S2F(amp)); SAMPLE max_value; - synth[osc].phase = render_lut(buf, synth[osc].phase, step, last_amp, amp, synth[osc].lut, &max_value); + //check if we are syncing + if (AMY_IS_SET(synth[osc].sync_source) && synth[osc].sync_source != osc) { + PHASOR sync_step = F2P(freq_of_logfreq(msynth[synth[osc].sync_source].logfreq) / (float) AMY_SAMPLE_RATE); + PHASOR sync_phase = synth[synth[osc].sync_source].phase; + synth[osc].phase = render_synced_lut(buf, synth[osc].phase, step, sync_phase, sync_step, last_amp, amp, synth[osc].lut, &max_value); + } else + synth[osc].phase = render_lut(buf, synth[osc].phase, step, last_amp, amp, synth[osc].lut, &max_value); msynth[osc].last_amp = msynth[osc].amp; return max_value; } @@ -633,3 +708,5 @@ void ks_deinit(void) { for(int i=0;i