Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Smooth retrigger #239

Open
wants to merge 5 commits into
base: main
Choose a base branch
from
Open
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
20 changes: 15 additions & 5 deletions src/amy.c
Original file line number Diff line number Diff line change
Expand Up @@ -652,7 +652,11 @@ void reset_osc(uint16_t i ) {
}
synth[i].eg_type[j] = ENVELOPE_NORMAL;
}
for(uint8_t j=0;j<MAX_BREAKPOINT_SETS;j++) { synth[i].last_scale[j] = 0; }
for(uint8_t j=0;j<MAX_BREAKPOINT_SETS;j++) {
synth[i].last_scale[j] = 0;
synth[i].seg_start_val[j] = 0;
AMY_UNSET(synth[i].current_seg[j]);
}
synth[i].last_two[0] = 0;
synth[i].last_two[1] = 0;
synth[i].lut = NULL;
Expand Down Expand Up @@ -1037,9 +1041,9 @@ void play_event(struct delta d) {
synth[d.osc].note_on_clock = total_samples; //esp_timer_get_time() / 1000;

// if there was a filter active for this voice, reset it
if(synth[d.osc].filter_type != FILTER_NONE) reset_filter(d.osc);
//if(synth[d.osc].filter_type != FILTER_NONE) reset_filter(d.osc);
// For repeatability, start at zero phase.
synth[d.osc].phase = 0;
//synth[d.osc].phase = 0;

// restart the waveforms
// Guess at the initial frequency depending only on const & note. Envelopes not "developed" yet.
Expand Down Expand Up @@ -1102,8 +1106,11 @@ void play_event(struct delta d) {
// For now, note_off_clock signals note off BUT ONLY IF IT'S NOT KS, ALGO, PARTIAL, PARTIALS, PCM, or CUSTOM.
// I'm not crazy about this, but if we apply it in those cases, the default bp0 amp envelope immediately zeros-out
// those waves on note-off.
AMY_UNSET(synth[d.osc].note_on_clock);
synth[d.osc].note_off_clock = total_samples;
if (AMY_IS_SET(synth[d.osc].note_on_clock)) {
// Only if the note-on clock is running, i.e. ignore repeated note-offs (which could restart release).
AMY_UNSET(synth[d.osc].note_on_clock);
synth[d.osc].note_off_clock = total_samples;
}
}
}
// Now maybe propagate the velocity event to the chained osc.
Expand Down Expand Up @@ -1298,6 +1305,9 @@ SAMPLE render_osc_wave(uint16_t osc, uint8_t core, SAMPLE* buf) {
if ( (total_samples - synth[osc].zero_amp_clock) >= MIN_ZERO_AMP_TIME_SAMPS) {
//printf("h&m: time %f osc %d OFF\n", total_samples/(float)AMY_SAMPLE_RATE, osc);
synth[osc].status = SYNTH_AUDIBLE_SUSPENDED; // It *could* come back...
// .. but reset osc and filter just in case.
synth[osc].phase = 0;
if(synth[osc].filter_type != FILTER_NONE) reset_filter(osc);
}
}
} else if (max_val == 0) {
Expand Down
4 changes: 3 additions & 1 deletion src/amy.h
Original file line number Diff line number Diff line change
Expand Up @@ -347,7 +347,9 @@ struct synthinfo {
float breakpoint_values[MAX_BREAKPOINT_SETS][MAX_BREAKPOINTS];
uint8_t eg_type[MAX_BREAKPOINT_SETS]; // one of the ENVELOPE_ values
SAMPLE last_scale[MAX_BREAKPOINT_SETS]; // remembers current envelope level, to use as start point in release.

float seg_start_val[MAX_BREAKPOINT_SETS]; // remembers starting point of current envelope segment.
uint8_t current_seg[MAX_BREAKPOINT_SETS]; // which envelope seg we are currently in.

// State variable for the dc-removal filter.
SAMPLE hpf_state[2];
// Constant offset to add to sawtooth before integrating.
Expand Down
33 changes: 14 additions & 19 deletions src/envelope.c
Original file line number Diff line number Diff line change
Expand Up @@ -60,10 +60,10 @@ SAMPLE compute_breakpoint_scale(uint16_t osc, uint8_t bp_set, uint16_t sample_of
// We have to aim to overshoot to the desired gap so that we hit the target by exponential_rate time.
const SAMPLE exponential_rate_overshoot_factor = F2S(1.0f / (1.0f - exp2f(EXP_RATE_VAL)));
uint32_t elapsed = 0;
SAMPLE scale = F2S(1.0f);
int eg_type = synth[osc].eg_type[bp_set];
uint32_t bp_end_times[MAX_BREAKPOINTS];
uint32_t cumulated_time = 0;
SAMPLE scale = F2S(1.0f);

// Scan breakpoints to find which one is release (the last one)
bp_r = -1;
Expand All @@ -78,9 +78,7 @@ SAMPLE compute_breakpoint_scale(uint16_t osc, uint8_t bp_set, uint16_t sample_of
if(bp_r < 0) {
// no breakpoints, return key gate.
if(AMY_IS_SET(synth[osc].note_off_clock)) scale = 0;
synth[osc].last_scale[bp_set] = scale;
//return scale;
goto return_label;
goto return_scale;
}
// Fix up bp_end_times for release segment to be relative to note-off time.
bp_end_times[bp_r] = synth[osc].breakpoint_times[bp_set][bp_r];
Expand All @@ -99,19 +97,15 @@ SAMPLE compute_breakpoint_scale(uint16_t osc, uint8_t bp_set, uint16_t sample_of
// We didn't find anything, so we are in sustain.
found = bp_r - 1; // segment before release defines sustain
scale = F2S(synth[osc].breakpoint_values[bp_set][found]);
synth[osc].last_scale[bp_set] = scale;
//printf("env: time %lld bpset %d seg %d SUSTAIN %f\n", total_samples, bp_set, found, S2F(scale));
//return scale;
goto return_label;
goto return_scale;
}
} else if(AMY_IS_SET(synth[osc].note_off_clock)) {
release = 1;
elapsed = (total_samples - synth[osc].note_off_clock + sample_offset) + 1;
// Get the last t/v pair , for release
found = bp_r;
t0 = 0; // start the elapsed clock again
// Release starts from wherever we got to
v0 = synth[osc].last_scale[bp_set];
if(elapsed > synth[osc].breakpoint_times[bp_set][bp_r]) {
// OK. partials (et al) need a frame to fade out to avoid clicks. This is in conflict with the breakpoint release,
// which will set it to the bp end value before the fade out, often 0 so the fadeout never gets to hit.
Expand All @@ -120,24 +114,25 @@ SAMPLE compute_breakpoint_scale(uint16_t osc, uint8_t bp_set, uint16_t sample_of
// to fully respect the actual envelope, else it pops up to full amplitude after the release.
if(synth[osc].wave==PARTIAL && synth[osc].patch >= 0) {
scale = F2S(1.0f);
synth[osc].last_scale[bp_set] = scale;
//return scale;
goto return_label;
goto return_scale;
}
//printf("cbp: time %f osc %d amp %f OFF\n", total_samples / (float)AMY_SAMPLE_RATE, osc, msynth[osc].amp);
// Synth is now turned off in hold_and_modify, which tracks when the amplitude goes to zero (and waits a bit).
//synth[osc].status=SYNTH_OFF;
//AMY_UNSET(synth[osc].note_off_clock);
scale = F2S(synth[osc].breakpoint_values[bp_set][bp_r]);
synth[osc].last_scale[bp_set] = scale;
//return scale;
goto return_label;
goto return_scale;
}
}
if (found != synth[osc].current_seg[bp_set]) {
// This is the first time we've been in this segment.
synth[osc].current_seg[bp_set] = found;
// Initialize the segment value from wherever we have gotten to so far.
synth[osc].seg_start_val[bp_set] = synth[osc].last_scale[bp_set];
}

if(found<0) return scale;

t1 = bp_end_times[found];
v0 = synth[osc].seg_start_val[bp_set];
v1 = F2S(synth[osc].breakpoint_values[bp_set][found]);
if(found>0 && bp_r != found && !release) {
t0 = bp_end_times[found-1];
Expand Down Expand Up @@ -211,8 +206,8 @@ SAMPLE compute_breakpoint_scale(uint16_t osc, uint8_t bp_set, uint16_t sample_of
scale = -scale;
}
// Keep track of the most-recently returned non-release scale.
return_label:
if (!release) synth[osc].last_scale[bp_set] = scale;
return_scale:
synth[osc].last_scale[bp_set] = scale;
//if (osc < AMY_OSCS && found != -1)
// fprintf(stderr, "env: time %f osc %d bpset %d seg %d type %d t0 %d t1 %d elapsed %d v0 %f v1 %f scale %f\n", total_samples / (float)AMY_SAMPLE_RATE, osc, bp_set, found, eg_type, t0, t1, elapsed, S2F(v0), S2F(v1), S2F(scale));
AMY_PROFILE_STOP(COMPUTE_BREAKPOINT_SCALE)
Expand Down
28 changes: 27 additions & 1 deletion test.py
Original file line number Diff line number Diff line change
Expand Up @@ -222,6 +222,32 @@ def run(self):
amy.send(time=100, vel=1)
amy.send(time=500, vel=0)

class TestEnvRetrig(AmyTest):

def run(self):
# Retriggering an envelope that hasn't fully decayed should restart from current value, not zero.
amy.send(time=0, osc=0, wave=amy.SINE, freq=1000)
amy.send(time=0, osc=0, amp='0,0,0.85,1,0,0', bp0='50,1,200,0.1,100,0')
amy.send(time=100, vel=8)
amy.send(time=200, vel=8) # Retrigger during Decay
amy.send(time=300, vel=0)
amy.send(time=350, vel=8) # Retrigger during Release
amy.send(time=500, vel=0)
amy.send(time=600, vel=0) # Repeated note-off

class TestFiltRetrig(AmyTest):

def run(self):
# Retriggering with filter state.
amy.send(time=0, osc=0, wave=amy.SINE, freq=1000)
amy.send(time=0, osc=0, amp='0,0,0.85,1,0,0', bp0='50,1,200,0.1,100,0',
filter_type=amy.FILTER_LPF, filter_freq=500, resonance=8)
amy.send(time=100, vel=8)
amy.send(time=200, vel=8) # Retrigger during Decay
amy.send(time=300, vel=0)
amy.send(time=350, vel=8) # Retrigger during Release
amy.send(time=500, vel=0)
amy.send(time=600, vel=0) # Repeated note-off

class TestAlgo(AmyTest):

Expand Down Expand Up @@ -578,7 +604,6 @@ def main(argv):
#TestBleep().test()
#TestBrass().test()
#TestBrass2().test()
#TestSineEnv().test()
#TestSawDownOsc().test()
#TestGuitar().test()
#TestFilter().test()
Expand All @@ -589,6 +614,7 @@ def main(argv):
#TestJunoTrumpetPatch().test()
#TestPcmLoop().test()
TestBYOPNoteOff().test()
TestSineEnv().test()

amy.send(debug=0)
print("tests done.")
Expand Down
Binary file modified tests/ref/TestBuildYourOwnPartials.wav
Binary file not shown.
Binary file modified tests/ref/TestChainedOsc.wav
Binary file not shown.
Binary file modified tests/ref/TestChorus.wav
Binary file not shown.
Binary file added tests/ref/TestEnvRetrig.wav
Binary file not shown.
Binary file added tests/ref/TestFiltRetrig.wav
Binary file not shown.
Binary file modified tests/ref/TestFilter.wav
Binary file not shown.
Binary file modified tests/ref/TestFilter24.wav
Binary file not shown.
Binary file modified tests/ref/TestJunoCheapTrumpetPatch.wav
Binary file not shown.
Binary file modified tests/ref/TestJunoClip.wav
Binary file not shown.
Binary file modified tests/ref/TestJunoTrumpetPatch.wav
Binary file not shown.
Binary file modified tests/ref/TestLowerVcf.wav
Binary file not shown.
Binary file modified tests/ref/TestOscBD.wav
Binary file not shown.
Binary file modified tests/ref/TestOverload.wav
Binary file not shown.