#pragma once #include "audio_buffer.h" #include "audio_math.h" #include "voice_allocator.h" #include #include namespace trnr { ////////////// // SINE OSC // ////////////// struct tx_sineosc { bool phase_reset; double samplerate; float phase_resolution; float phase; float history; }; inline void tx_randomize_phase(float& phase) { std::random_device random; std::mt19937 gen(random()); std::uniform_real_distribution<> dis(0.0, 1.0); phase = dis(gen); } inline void tx_sineosc_init(tx_sineosc& s, double samplerate) { s.phase_reset = false; s.samplerate = samplerate; s.phase_resolution = 16.f; s.phase = 0.f; s.history = 0.f; tx_randomize_phase(s.phase); } inline float tx_wrap(float& phase) { while (phase < 0.) phase += 1.; while (phase >= 1.) phase -= 1.; return phase; } inline float tx_sineosc_process_sample(tx_sineosc& s, bool trigger, float frequency, float phase_modulation = 0.f) { if (trigger) { if (s.phase_reset) s.phase = 0.f; else tx_randomize_phase(s.phase); } float lookup_phase = s.phase + phase_modulation; tx_wrap(lookup_phase); s.phase += frequency / s.samplerate; tx_wrap(s.phase); // redux s.phase = static_cast(s.phase * s.phase_resolution) / s.phase_resolution; // x is scaled 0<=x<4096 float output; float x = lookup_phase; const float a = -0.40319426317E-08; const float b = 0.21683205691E+03; const float c = 0.28463350538E-04; const float d = -0.30774648337E-02; float y; bool negate = false; if (x > 2048) { negate = true; x -= 2048; } if (x > 1024) x = 2048 - x; y = (a + x) / (b + c * x * x) + d * x; if (negate) output = (-y); else output = y; // filter output = 0.5 * (output + s.history); s.history = output; return output; } ////////////// // ENVELOPE // ////////////// enum tx_env_state { idle = 0, attack1, attack2, hold, decay1, decay2, sustain, release1, release2 }; struct tx_envelope { tx_env_state state = idle; float attack1_rate = 0; float attack1_level = 0; float attack2_rate = 0; float hold_rate = 0; float decay1_rate = 0; float decay1_level = 0; float decay2_rate = 0; float sustain_level = 0; float release1_rate = 0; float release1_level = 0; float release2_rate = 0; bool skip_sustain = false; double samplerate = 44100.; size_t phase = 0; float level = 0.f; float start_level = 0.f; float h1 = 0.f; float h2 = 0.f; float h3 = 0.f; bool retrigger; }; inline void tx_envelope_init(tx_envelope& e, double samplerate, bool retrigger = false) { e.samplerate = samplerate; e.retrigger = retrigger; } inline size_t tx_mtos(float ms, double samplerate) { return static_cast(ms * samplerate / 1000.f); } inline float tx_lerp(float x1, float y1, float x2, float y2, float x) { return y1 + (((x - x1) * (y2 - y1)) / (x2 - x1)); } inline float tx_envelope_process_sample(tx_envelope& e, bool gate, bool trigger, float _attack_mod = 0, float _decay_mod = 0) { size_t attack_mid_x1 = tx_mtos(e.attack1_rate + (float)_attack_mod, e.samplerate); size_t attack_mid_x2 = tx_mtos(e.attack2_rate + (float)_attack_mod, e.samplerate); size_t hold_samp = tx_mtos(e.hold_rate, e.samplerate); size_t decay_mid_x1 = tx_mtos(e.decay1_rate + (float)_decay_mod, e.samplerate); size_t decay_mid_x2 = tx_mtos(e.decay2_rate + (float)_decay_mod, e.samplerate); size_t release_mid_x1 = tx_mtos(e.release1_rate + (float)_decay_mod, e.samplerate); size_t release_mid_x2 = tx_mtos(e.release2_rate + (float)_decay_mod, e.samplerate); // if note on is triggered, transition to attack phase if (trigger) { if (e.retrigger) e.start_level = 0.f; else e.start_level = e.level; e.phase = 0; e.state = attack1; } // attack 1st half if (e.state == attack1) { // while in attack phase if (e.phase < attack_mid_x1) { e.level = tx_lerp(0, e.start_level, (float)attack_mid_x1, e.attack1_level, (float)e.phase); e.phase += 1; } // reset phase if parameter was changed if (e.phase > attack_mid_x1) { e.phase = attack_mid_x1; } // if attack phase is done, transition to decay phase if (e.phase == attack_mid_x1) { e.state = attack2; e.phase = 0; } } // attack 2nd half if (e.state == attack2) { // while in attack phase if (e.phase < attack_mid_x2) { e.level = tx_lerp(0, e.attack1_level, (float)attack_mid_x2, 1, (float)e.phase); e.phase += 1; } // reset phase if parameter was changed if (e.phase > attack_mid_x2) { e.phase = attack_mid_x2; } // if attack phase is done, transition to decay phase if (e.phase == attack_mid_x2) { e.state = hold; e.phase = 0; } } // hold if (e.state == hold) { if (e.phase < hold_samp) { e.level = 1.0; e.phase += 1; } if (e.phase > hold_samp) { e.phase = hold_samp; } if (e.phase == hold_samp) { e.state = decay1; e.phase = 0; } } // decay 1st half if (e.state == decay1) { // while in decay phase if (e.phase < decay_mid_x1) { e.level = tx_lerp(0, 1, (float)decay_mid_x1, e.decay1_level, (float)e.phase); e.phase += 1; } // reset phase if parameter was changed if (e.phase > decay_mid_x1) { e.phase = decay_mid_x1; } // if decay phase is done, transition to sustain phase if (e.phase == decay_mid_x1) { e.state = decay2; e.phase = 0; } } // decay 2nd half if (e.state == decay2) { // while in decay phase if (e.phase < decay_mid_x2) { e.level = tx_lerp(0, e.decay1_level, (float)decay_mid_x2, e.sustain_level, (float)e.phase); e.phase += 1; } // reset phase if parameter was changed if (e.phase > decay_mid_x2) { e.phase = decay_mid_x2; } // if decay phase is done, transition to sustain phase if (e.phase == decay_mid_x2) { e.state = sustain; e.phase = 0; e.level = e.sustain_level; } } // while sustain phase: if note off is triggered, transition to release phase if (e.state == sustain && (!gate || e.skip_sustain)) { e.state = release1; e.level = e.sustain_level; } // release 1st half if (e.state == release1) { // while in release phase if (e.phase < release_mid_x1) { e.level = tx_lerp(0, e.sustain_level, (float)release_mid_x1, e.release1_level, (float)e.phase); e.phase += 1; } // reset phase if parameter was changed if (e.phase > release_mid_x1) { e.phase = release_mid_x1; } // transition to 2nd release half if (e.phase == release_mid_x1) { e.phase = 0; e.state = release2; } } // release 2nd half if (e.state == release2) { // while in release phase if (e.phase < release_mid_x2) { e.level = tx_lerp(0, e.release1_level, (float)release_mid_x2, 0, (float)e.phase); e.phase += 1; } // reset phase if parameter was changed if (e.phase > release_mid_x2) { e.phase = release_mid_x2; } // reset if (e.phase == release_mid_x2) { e.phase = 0; e.state = idle; e.level = 0; } } // smooth output e.h3 = e.h2; e.h2 = e.h1; e.h1 = e.level; return (e.h1 + e.h2 + e.h3) / 3.f; } ////////////// // OPERATOR // ////////////// struct tx_operator { tx_envelope envelope; tx_sineosc oscillator; float ratio = 1.f; float amplitude = 1.f; }; inline void tx_operator_init(tx_operator& op, double samplerate) { tx_envelope_init(op.envelope, samplerate); tx_sineosc_init(op.oscillator, samplerate); } inline float tx_operator_process_sample(tx_operator& op, bool gate, bool trigger, float frequency, float velocity, float pm = 0.f) { float env = tx_envelope_process_sample(op.envelope, gate, trigger); // drifts and sounds better! if (op.envelope.state != idle) { float osc = tx_sineosc_process_sample(op.oscillator, trigger, frequency, pm); return osc * env * velocity; } else { return 0.f; } } //////////// // VOICE // //////////// constexpr float MOD_INDEX_COEFF = 4.f; struct tx_state { float additional_pitch_mod = 0.f; // modulates pitch in frequency int algorithm = 0; float pitch_env_amt = 0.f; float pitch_mod = 0.f; float feedback_amt = 0.f; float bit_resolution = 12.f; tx_sineosc feedback_osc; tx_envelope pitch_env; tx_operator op1; tx_operator op2; tx_operator op3; }; inline void tx_voice_init(tx_state& s, double samplerate) { tx_sineosc_init(s.feedback_osc, samplerate); tx_envelope_init(s.pitch_env, samplerate); tx_operator_init(s.op1, samplerate); tx_operator_init(s.op2, samplerate); tx_operator_init(s.op3, samplerate); } inline void tx_voice_process_block(tx_state& t, voice_state& s, float** audio, size_t num_frames, const vector>& mods = {}) { float frequency = midi_to_frequency(s.midi_note + t.pitch_mod + t.additional_pitch_mod); for (int i = 0; i < num_frames; i++) { voice_process_event_for_frame(s, i); float pitch_env_signal = tx_envelope_process_sample(t.pitch_env, s.gate, s.trigger) * t.pitch_env_amt; float pitched_freq = frequency + pitch_env_signal; float output = 0.f; // mix operator signals according to selected algorithm if (t.algorithm == 0) { float fb_freq = frequency * t.op3.ratio; float fb_mod_index = (t.feedback_amt * MOD_INDEX_COEFF); float fb_signal = tx_sineosc_process_sample(t.feedback_osc, s.trigger, fb_freq) * fb_mod_index; float op3_Freq = frequency * t.op3.ratio; float op3_mod_index = (t.op3.amplitude * MOD_INDEX_COEFF); float op3_signal = tx_operator_process_sample(t.op3, s.gate, s.trigger, op3_Freq, s.velocity, fb_signal) * op3_mod_index; float op2_freq = frequency * t.op2.ratio; float op2_mod_index = (t.op2.amplitude * MOD_INDEX_COEFF); float op2_signal = tx_operator_process_sample(t.op2, s.gate, s.trigger, op2_freq, s.velocity, op3_signal) * op2_mod_index; float op1_freq = frequency * t.op1.ratio; output = tx_operator_process_sample(t.op1, s.gate, s.trigger, op1_freq, s.velocity, op2_signal) * t.op1.amplitude; } else if (t.algorithm == 1) { float fb_freq = frequency * t.op3.ratio; float fb_mod_index = (t.feedback_amt * MOD_INDEX_COEFF); float fb_signal = tx_sineosc_process_sample(t.feedback_osc, s.trigger, fb_freq) * fb_mod_index; float op3_freq = frequency * t.op3.ratio; float op3_signal = tx_operator_process_sample(t.op3, s.gate, s.trigger, op3_freq, s.velocity, fb_signal) * t.op3.amplitude; float op2_freq = frequency * t.op2.ratio; float op2_mod_index = (t.op2.amplitude * MOD_INDEX_COEFF); float op2_signal = tx_operator_process_sample(t.op2, s.gate, s.trigger, op2_freq, s.velocity) * op2_mod_index; float op1_freq = frequency * t.op1.ratio; float op1_signal = tx_operator_process_sample(t.op1, s.gate, s.trigger, op1_freq, s.velocity, op2_signal) * t.op1.amplitude; output = op1_signal + op3_signal; } else if (t.algorithm == 2) { float fb_freq = frequency * t.op3.ratio; float fb_mod_index = (t.feedback_amt * MOD_INDEX_COEFF); float fb_signal = tx_sineosc_process_sample(t.feedback_osc, s.trigger, fb_freq) * fb_mod_index; float op3_freq = frequency * t.op3.ratio; float op3_signal = tx_operator_process_sample(t.op3, s.gate, s.trigger, op3_freq, s.velocity, fb_signal) * t.op3.amplitude; float op2_freq = frequency * t.op2.ratio; float op2_signal = tx_operator_process_sample(t.op2, s.gate, s.trigger, op2_freq, s.velocity) * t.op2.amplitude; float op1_freq = frequency * t.op1.ratio; float op1_signal = tx_operator_process_sample(t.op1, s.gate, s.trigger, op1_freq, s.velocity) * t.op1.amplitude; output = op1_signal + op2_signal + op3_signal; } else if (t.algorithm == 3) { float fb_freq = frequency * t.op3.ratio; float fb_mod_index = (t.feedback_amt * MOD_INDEX_COEFF); float fb_signal = tx_sineosc_process_sample(t.feedback_osc, s.trigger, fb_freq) * fb_mod_index; float op3_freq = frequency * t.op3.ratio; float op3_mod_index = (t.op3.amplitude * MOD_INDEX_COEFF); float op3_signal = tx_operator_process_sample(t.op3, s.gate, s.trigger, op3_freq, s.velocity, fb_signal) * op3_mod_index; float op2_freq = frequency * t.op2.ratio; float op2_mod_index = (t.op2.amplitude * MOD_INDEX_COEFF); float op2_signal = tx_operator_process_sample(t.op2, s.gate, s.trigger, op2_freq, s.velocity) * op2_mod_index; float op1_freq = frequency * t.op1.ratio; output = tx_operator_process_sample(t.op1, s.gate, s.trigger, op1_freq, s.velocity, op2_signal + op3_signal) * t.op1.amplitude; } // reset trigger s.trigger = false; float res = powf(2, t.bit_resolution); output = roundf(output * res) / res; audio[0][i] += output / 3.; audio[1][i] = audio[0][i]; } } enum tx_parameter { BIT_RESOLUTION = 0, FEEDBACKOSC_PHASE_RESOLUTION, FEEDBACK, ALGORITHM, PITCH_ENVELOPE_AMOUNT, PITCH_ENVELOPE_SKIP_SUSTAIN, PITCH_ENVELOPE_ATTACK1_RATE, PITCH_ENVELOPE_ATTACK1_LEVEL, PITCH_ENVELOPE_ATTACK2_RATE, PITCH_ENVELOPE_HOLD_RATE, PITCH_ENVELOPE_DECAY1_RATE, PITCH_ENVELOPE_DECAY1_LEVEL, PITCH_ENVELOPE_DECAY2_RATE, PITCH_ENVELOPE_SUSTAIN_LEVEL, PITCH_ENVELOPE_RELEASE1_RATE, PITCH_ENVELOPE_RELEASE1_LEVEL, PITCH_ENVELOPE_RELEASE2_RATE, OP1_RATIO, OP1_AMPLITUDE, OP1_PHASE_RESOLUTION, OP1_ENVELOPE_SKIP_SUSTAIN, OP1_ENVELOPE_ATTACK1_RATE, OP1_ENVELOPE_ATTACK1_LEVEL, OP1_ENVELOPE_ATTACK2_RATE, OP1_ENVELOPE_HOLD_RATE, OP1_ENVELOPE_DECAY1_RATE, OP1_ENVELOPE_DECAY1_LEVEL, OP1_ENVELOPE_DECAY2_RATE, OP1_ENVELOPE_SUSTAIN_LEVEL, OP1_ENVELOPE_RELEASE1_RATE, OP1_ENVELOPE_RELEASE1_LEVEL, OP1_ENVELOPE_RELEASE2_RATE, OP2_RATIO, OP2_AMPLITUDE, OP2_PHASE_RESOLUTION, OP2_ENVELOPE_SKIP_SUSTAIN, OP2_ENVELOPE_ATTACK1_RATE, OP2_ENVELOPE_ATTACK1_LEVEL, OP2_ENVELOPE_ATTACK2_RATE, OP2_ENVELOPE_HOLD_RATE, OP2_ENVELOPE_DECAY1_RATE, OP2_ENVELOPE_DECAY1_LEVEL, OP2_ENVELOPE_DECAY2_RATE, OP2_ENVELOPE_SUSTAIN_LEVEL, OP2_ENVELOPE_RELEASE1_RATE, OP2_ENVELOPE_RELEASE1_LEVEL, OP2_ENVELOPE_RELEASE2_RATE, OP3_RATIO, OP3_AMPLITUDE, OP3_PHASE_RESOLUTION, OP3_ENVELOPE_SKIP_SUSTAIN, OP3_ENVELOPE_ATTACK1_RATE, OP3_ENVELOPE_ATTACK1_LEVEL, OP3_ENVELOPE_ATTACK2_RATE, OP3_ENVELOPE_HOLD_RATE, OP3_ENVELOPE_DECAY1_RATE, OP3_ENVELOPE_DECAY1_LEVEL, OP3_ENVELOPE_DECAY2_RATE, OP3_ENVELOPE_SUSTAIN_LEVEL, OP3_ENVELOPE_RELEASE1_RATE, OP3_ENVELOPE_RELEASE1_LEVEL, OP3_ENVELOPE_RELEASE2_RATE, }; struct tx_parameter_mapping { float range_min; float range_max; float exponent; tx_parameter parameter; float apply(float _input) const { if (range_min == range_max && exponent == 1.f) return _input; return powf(_input, exponent) * (range_max - range_min) + range_min; } }; struct tx_synth { voice_allocator allocator; array voices; }; inline void tx_synth_init(tx_synth& s, double samplerate) { voice_allocator_init(s.allocator); for (int i = 0; i < MAX_VOICES; i++) { tx_voice_init(s.voices[i], samplerate); } } inline void tx_synth_process_block(tx_synth& s, float** audio, size_t num_frames, const vector& midi_events, const vector>& mods = {}) { for (int i = 0; i < num_frames; i++) { audio[0][i] = audio[1][i] = 0.f; } // clear audio buffers voice_allocator_process_block(s.allocator, midi_events); for (int i = 0; i < s.allocator.active_voice_count; i++) { tx_voice_process_block(s.voices[i], s.allocator.voices[i], audio, num_frames, mods); } } inline void tx_apply_parameter_mapping(array& v, tx_parameter_mapping& m, float value) { if (m.range_min != m.range_max || m.exponent != 1.f) value = powf(value, m.exponent) * (m.range_max - m.range_min) + m.range_min; for (int i = 0; i < MAX_VOICES; i++) { tx_state& s = v[i]; switch (m.parameter) { case tx_parameter::BIT_RESOLUTION: s.bit_resolution = value; break; case tx_parameter::FEEDBACKOSC_PHASE_RESOLUTION: s.feedback_osc.phase_resolution = value; break; case tx_parameter::FEEDBACK: s.feedback_amt = value; break; case tx_parameter::ALGORITHM: s.algorithm = value; break; case tx_parameter::PITCH_ENVELOPE_AMOUNT: s.pitch_env_amt = value; break; case tx_parameter::PITCH_ENVELOPE_SKIP_SUSTAIN: s.pitch_env.skip_sustain = value; break; case tx_parameter::PITCH_ENVELOPE_ATTACK1_RATE: s.pitch_env.attack1_rate = value; break; case tx_parameter::PITCH_ENVELOPE_ATTACK1_LEVEL: s.pitch_env.attack1_level = value; break; case tx_parameter::PITCH_ENVELOPE_ATTACK2_RATE: s.pitch_env.attack2_rate = value; break; case tx_parameter::PITCH_ENVELOPE_HOLD_RATE: s.pitch_env.hold_rate = value; break; case tx_parameter::PITCH_ENVELOPE_DECAY1_RATE: s.pitch_env.decay1_rate = value; break; case tx_parameter::PITCH_ENVELOPE_DECAY1_LEVEL: s.pitch_env.decay1_level = value; break; case tx_parameter::PITCH_ENVELOPE_DECAY2_RATE: s.pitch_env.decay2_rate = value; break; case tx_parameter::PITCH_ENVELOPE_SUSTAIN_LEVEL: s.pitch_env.sustain_level = value; break; case tx_parameter::PITCH_ENVELOPE_RELEASE1_RATE: s.pitch_env.release1_rate = value; break; case tx_parameter::PITCH_ENVELOPE_RELEASE1_LEVEL: s.pitch_env.release1_level = value; break; case tx_parameter::PITCH_ENVELOPE_RELEASE2_RATE: s.pitch_env.release2_rate = value; break; case tx_parameter::OP1_RATIO: s.op1.ratio = value; break; case tx_parameter::OP1_AMPLITUDE: s.op1.amplitude = value; break; case tx_parameter::OP1_PHASE_RESOLUTION: s.op1.oscillator.phase_resolution = value; break; case tx_parameter::OP1_ENVELOPE_SKIP_SUSTAIN: s.op1.envelope.skip_sustain = value; break; case tx_parameter::OP1_ENVELOPE_ATTACK1_RATE: s.op1.envelope.attack1_rate = value; break; case tx_parameter::OP1_ENVELOPE_ATTACK1_LEVEL: s.op1.envelope.attack1_level = value; break; case tx_parameter::OP1_ENVELOPE_ATTACK2_RATE: s.op1.envelope.attack2_rate = value; break; case tx_parameter::OP1_ENVELOPE_HOLD_RATE: s.op1.envelope.hold_rate = value; break; case tx_parameter::OP1_ENVELOPE_DECAY1_RATE: s.op1.envelope.decay1_rate = value; break; case tx_parameter::OP1_ENVELOPE_DECAY1_LEVEL: s.op1.envelope.decay1_level = value; break; case tx_parameter::OP1_ENVELOPE_DECAY2_RATE: s.op1.envelope.decay2_rate = value; break; case tx_parameter::OP1_ENVELOPE_SUSTAIN_LEVEL: s.op1.envelope.sustain_level = value; break; case tx_parameter::OP1_ENVELOPE_RELEASE1_RATE: s.op1.envelope.release1_rate = value; break; case tx_parameter::OP1_ENVELOPE_RELEASE1_LEVEL: s.op1.envelope.release1_level = value; break; case tx_parameter::OP1_ENVELOPE_RELEASE2_RATE: s.op1.envelope.release2_rate = value; break; case tx_parameter::OP2_RATIO: s.op2.ratio = value; break; case tx_parameter::OP2_AMPLITUDE: s.op2.amplitude = value; break; case tx_parameter::OP2_PHASE_RESOLUTION: s.op2.oscillator.phase_resolution = value; break; case tx_parameter::OP2_ENVELOPE_SKIP_SUSTAIN: s.op2.envelope.skip_sustain = value; break; case tx_parameter::OP2_ENVELOPE_ATTACK1_RATE: s.op2.envelope.attack1_rate = value; break; case tx_parameter::OP2_ENVELOPE_ATTACK1_LEVEL: s.op2.envelope.attack1_level = value; break; case tx_parameter::OP2_ENVELOPE_ATTACK2_RATE: s.op2.envelope.attack2_rate = value; break; case tx_parameter::OP2_ENVELOPE_HOLD_RATE: s.op2.envelope.hold_rate = value; break; case tx_parameter::OP2_ENVELOPE_DECAY1_RATE: s.op2.envelope.decay1_rate = value; break; case tx_parameter::OP2_ENVELOPE_DECAY1_LEVEL: s.op2.envelope.decay1_level = value; break; case tx_parameter::OP2_ENVELOPE_DECAY2_RATE: s.op2.envelope.decay2_rate = value; break; case tx_parameter::OP2_ENVELOPE_SUSTAIN_LEVEL: s.op2.envelope.sustain_level = value; break; case tx_parameter::OP2_ENVELOPE_RELEASE1_RATE: s.op2.envelope.release1_rate = value; break; case tx_parameter::OP2_ENVELOPE_RELEASE1_LEVEL: s.op2.envelope.release1_level = value; break; case tx_parameter::OP2_ENVELOPE_RELEASE2_RATE: s.op2.envelope.release2_rate = value; break; case tx_parameter::OP3_RATIO: s.op3.ratio = value; break; case tx_parameter::OP3_AMPLITUDE: s.op3.amplitude = value; break; case tx_parameter::OP3_PHASE_RESOLUTION: s.op3.oscillator.phase_resolution = value; break; case tx_parameter::OP3_ENVELOPE_SKIP_SUSTAIN: s.op3.envelope.skip_sustain = value; break; case tx_parameter::OP3_ENVELOPE_ATTACK1_RATE: s.op3.envelope.attack1_rate = value; break; case tx_parameter::OP3_ENVELOPE_ATTACK1_LEVEL: s.op3.envelope.attack1_level = value; break; case tx_parameter::OP3_ENVELOPE_ATTACK2_RATE: s.op3.envelope.attack2_rate = value; break; case tx_parameter::OP3_ENVELOPE_HOLD_RATE: s.op3.envelope.hold_rate = value; break; case tx_parameter::OP3_ENVELOPE_DECAY1_RATE: s.op3.envelope.decay1_rate = value; break; case tx_parameter::OP3_ENVELOPE_DECAY1_LEVEL: s.op3.envelope.decay1_level = value; break; case tx_parameter::OP3_ENVELOPE_DECAY2_RATE: s.op3.envelope.decay2_rate = value; break; case tx_parameter::OP3_ENVELOPE_SUSTAIN_LEVEL: s.op3.envelope.sustain_level = value; break; case tx_parameter::OP3_ENVELOPE_RELEASE1_RATE: s.op3.envelope.release1_rate = value; break; case tx_parameter::OP3_ENVELOPE_RELEASE1_LEVEL: s.op3.envelope.release1_level = value; break; case tx_parameter::OP3_ENVELOPE_RELEASE2_RATE: s.op3.envelope.release2_rate = value; break; } } } inline void tx_apply_parameter_mappings(array& v, std::vector& m, float value) { for (int i = 0; i < m.size(); i++) { tx_apply_parameter_mapping(v, m[i], value); } } } // namespace trnr