diff --git a/synth/midi_event.h b/synth/midi_event.h deleted file mode 100644 index 44da90c..0000000 --- a/synth/midi_event.h +++ /dev/null @@ -1,48 +0,0 @@ -#pragma once - -namespace trnr { - -enum midi_event_type { - note_on = 0, - note_off, - pitch_wheel, - mod_wheel -}; - -class midi_event { -public: - midi_event_type type; - int offset = 0; - int midi_note = 0; - float velocity = 1.f; - double data = 0; - - void make_note_on(int _midi_note, float _velocity, int _offset = 0) - { - type = midi_event_type::note_on; - midi_note = _midi_note; - velocity = _velocity; - offset = _offset; - } - - void make_note_off(int _midi_note, float _velocity, int _offset = 0) - { - type = midi_event_type::note_off; - midi_note = _midi_note; - velocity = _velocity; - offset = _offset; - } - - void make_pitch_wheel(double _pitch, int _offset = 0) - { - type = midi_event_type::pitch_wheel; - data = _pitch; - } - - void make_mod_wheel(double _mod, int _offset = 0) - { - type = midi_event_type::pitch_wheel; - data = _mod; - } -}; -} // namespace trnr diff --git a/synth/midi_synth.h b/synth/midi_synth.h deleted file mode 100644 index a8a1829..0000000 --- a/synth/midi_synth.h +++ /dev/null @@ -1,91 +0,0 @@ -#pragma once -#include "audio_buffer.h" -#include "midi_event.h" -#include "voice_allocator.h" -#include -#include -#include - -namespace trnr { - -// a generic midi synth base class with sample accurate event handling. -// the templated type t_voice must derive from ivoice -template -class midi_synth : public voice_allocator { -public: - std::vector m_event_queue; - int m_block_size; - bool m_voices_active; - - midi_synth(size_t num_voices = 1) - : m_voices_active {false} - , voice_allocator(num_voices) - { - } - - void set_samplerate_blocksize(double _samplerate, int _block_size) - { - m_block_size = _block_size; - voice_allocator::set_samplerate(_samplerate); - } - - void process_block(t_sample** _outputs, int _n_frames, std::vector> _modulators = {}) - { - // clear outputs - for (auto i = 0; i < 2; i++) { memset(_outputs[i], 0, _n_frames * sizeof(t_sample)); } - - // sample accurate event handling based on the iPlug2 synth by Oli Larkin - if (m_voices_active || !m_event_queue.empty()) { - int block_size = m_block_size; - int samples_remaining = _n_frames; - int start_index = 0; - - while (samples_remaining > 0) { - - if (samples_remaining < block_size) block_size = samples_remaining; - - while (!m_event_queue.empty()) { - midi_event event = m_event_queue.front(); - - // we assume the messages are in chronological order. If we find one later than the current block we - // are done. - if (event.offset > start_index + block_size) break; - - // send performance messages to the voice allocator - // message offset is relative to the start of this process_samples() block - event.offset -= start_index; - voice_allocator::add_event(event); - - m_event_queue.erase(m_event_queue.begin()); - } - - voice_allocator::process_samples(_outputs, start_index, block_size, _modulators); - - samples_remaining -= block_size; - start_index += block_size; - } - - m_voices_active = voice_allocator::voices_active(); - - flush_event_queue(_n_frames); - } else { - for (int s = 0; s < _n_frames; s++) { - _outputs[0][s] = 0.; - _outputs[1][s] = 0.; - } - } - } - - void add_event(midi_event event) - { - if (event.type == midi_event_type::note_on) m_voices_active = true; - - m_event_queue.push_back(event); - } - - void flush_event_queue(int frames) - { - for (int i = 0; i < m_event_queue.size(); i++) { m_event_queue.at(i).offset -= frames; } - } -}; -} // namespace trnr diff --git a/synth/synth.h b/synth/synth.h deleted file mode 100644 index 9aff873..0000000 --- a/synth/synth.h +++ /dev/null @@ -1,116 +0,0 @@ -#pragma once - -#include "audio_buffer.h" -#include -#include -#include - -using namespace std; - -namespace trnr { - -enum midi_event_type { - note_on = 0, - note_off, - pitch_wheel, - mod_wheel -}; - -struct midi_event { - midi_event_type type; - int offset; - int midi_note; - float velocity; - double data; -}; - -constexpr size_t MAX_VOICES = 8; -constexpr size_t MAX_EVENTS_PER_VOICE = 32; - -template -void voice_process_block(t_voice& v, t_sample** frames, size_t num_frames, midi_event* events, size_t num_events, - const vector>& mods = {}); - -template -struct synth { - array voices; - array, MAX_VOICES> voice_events; - array counts; - int active_voice_count = 1; - size_t index_to_steal = 0; -}; - -template -void synth_init(synth& s, double samplerate) -{ - for (size_t i = 0; i < MAX_VOICES; ++i) { - s.voices[i] = t_voice(); - s.voices[i].voice_init(samplerate); - s.counts[i] = 0; - } -} - -template -void synth_process_block(synth& s, t_sample** frames, int num_frames, const vector& midi_events, - const vector>& mods = {}) -{ - // reset voice events and counts - for (int i = 0; i < MAX_VOICES; i++) { - s.voice_events[i].fill(midi_event {}); - s.counts[i] = 0; - } - - for (const auto& ev : midi_events) { - switch (ev.type) { - case note_on: { - bool found = false; - // attempt to find a free voice - for (size_t i = 0; i < s.active_voice_count; ++i) { - if (!s.voices[i].is_busy) { - s.voice_events[i][s.counts[i]++] = ev; - found = true; - break; - } - } - - if (found) break; - - // try to find a voice that is not gated - for (size_t i = 0; i < s.active_voice_count; ++i) { - if (!s.voices[i].gate) { - s.voice_events[i][s.counts[i]++] = ev; - found = true; - break; - } - } - - if (found) break; - - // if all voices are gated, steal one round-robin - s.voice_events[s.index_to_steal][s.counts[s.index_to_steal]++] = ev; - s.index_to_steal++; - if (s.index_to_steal >= s.active_voice_count) s.index_to_steal = 0; - break; - } - case note_off: { - for (size_t i = 0; i < s.active_voice_count; ++i) { - if (s.voices[i].midi_note == ev.midi_note) s.voice_events[i][s.counts[i]++] = ev; - } - break; - } - case pitch_wheel: - case mod_wheel: { - for (size_t i = 0; i < s.active_voice_count; ++i) { s.voice_events[i][s.counts[i]++] = ev; } - break; - } - } - } - - for (size_t i = 0; i < s.active_voice_count; ++i) { - auto& v = s.voices[i]; - auto& events = s.voice_events[i].data(); - size_t num_events = s.counts[i]; - voice_process_block(v, frames, num_frames, events, num_events, mods); - } -} -} // namespace trnr diff --git a/synth/triplex.h b/synth/triplex.h index 5f74762..2975006 100644 --- a/synth/triplex.h +++ b/synth/triplex.h @@ -1,6 +1,7 @@ #pragma once +#include "audio_buffer.h" #include "audio_math.h" -#include "synth.h" +#include "voice_allocator.h" #include #include @@ -312,15 +313,11 @@ inline float tx_operator_process_sample(tx_operator& op, bool gate, bool trigger constexpr float MOD_INDEX_COEFF = 4.f; -struct tx_voice { - bool gate = false; - bool trigger = false; - int midi_note = 0; - float velocity = 1.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; @@ -328,130 +325,404 @@ struct tx_voice { tx_operator op1; tx_operator op2; tx_operator op3; - - float pitch_mod = 0.f; }; -inline float calc_algo1(tx_voice& v, const float frequency) +inline void tx_voice_process_block(tx_state& t, voice_state& s, float** audio, size_t num_frames, + const vector>& mods) { - float fb_freq = frequency * v.op3.ratio; - float fb_mod_index = (v.feedback_amt * MOD_INDEX_COEFF); - float fb_signal = tx_sineosc_process_sample(v.feedback_osc, v.trigger, fb_freq) * fb_mod_index; + float frequency = midi_to_frequency(s.midi_note + t.pitch_mod + t.additional_pitch_mod); - float op3_Freq = frequency * v.op3.ratio; - float op3_mod_index = (v.op3.amplitude * MOD_INDEX_COEFF); - float op3_signal = - tx_operator_process_sample(v.op3, v.gate, v.trigger, op3_Freq, v.velocity, fb_signal) * op3_mod_index; + for (int i = 0; i < num_frames; i++) { - float op2_freq = frequency * v.op2.ratio; - float op2_mod_index = (v.op2.amplitude * MOD_INDEX_COEFF); - float op2_signal = - tx_operator_process_sample(v.op2, v.gate, v.trigger, op2_freq, v.velocity, op3_signal) * op2_mod_index; + voice_process_event_for_frame(s, i); - float op1_freq = frequency * v.op1.ratio; - return tx_operator_process_sample(v.op1, v.gate, v.trigger, op1_freq, v.velocity, op2_signal) * v.op1.amplitude; -} - -inline float calc_algo2(tx_voice& v, const float frequency) -{ - float fb_freq = frequency * v.op3.ratio; - float fb_mod_index = (v.feedback_amt * MOD_INDEX_COEFF); - float fb_signal = tx_sineosc_process_sample(v.feedback_osc, v.trigger, fb_freq) * fb_mod_index; - - float op3_freq = frequency * v.op3.ratio; - float op3_signal = - tx_operator_process_sample(v.op3, v.gate, v.trigger, op3_freq, v.velocity, fb_signal) * v.op3.amplitude; - - float op2_freq = frequency * v.op2.ratio; - float op2_mod_index = (v.op2.amplitude * MOD_INDEX_COEFF); - float op2_signal = tx_operator_process_sample(v.op2, v.gate, v.trigger, op2_freq, v.velocity) * op2_mod_index; - - float op1_freq = frequency * v.op1.ratio; - float op1_signal = - tx_operator_process_sample(v.op1, v.gate, v.trigger, op1_freq, v.velocity, op2_signal) * v.op1.amplitude; - - return op1_signal + op3_signal; -} - -inline float calc_algo3(tx_voice& v, const float frequency) -{ - float fb_freq = frequency * v.op3.ratio; - float fb_mod_index = (v.feedback_amt * MOD_INDEX_COEFF); - float fb_signal = tx_sineosc_process_sample(v.feedback_osc, v.trigger, fb_freq) * fb_mod_index; - - float op3_freq = frequency * v.op3.ratio; - float op3_signal = - tx_operator_process_sample(v.op3, v.gate, v.trigger, op3_freq, v.velocity, fb_signal) * v.op3.amplitude; - - float op2_freq = frequency * v.op2.ratio; - float op2_signal = tx_operator_process_sample(v.op2, v.gate, v.trigger, op2_freq, v.velocity) * v.op2.amplitude; - - float op1_freq = frequency * v.op1.ratio; - float op1_signal = tx_operator_process_sample(v.op1, v.gate, v.trigger, op1_freq, v.velocity) * v.op1.amplitude; - - return op1_signal + op2_signal + op3_signal; -} - -inline float calc_algo4(tx_voice& v, const float frequency) -{ - float fb_freq = frequency * v.op3.ratio; - float fb_mod_index = (v.feedback_amt * MOD_INDEX_COEFF); - float fb_signal = tx_sineosc_process_sample(v.feedback_osc, v.trigger, fb_freq) * fb_mod_index; - - float op3_freq = frequency * v.op3.ratio; - float op3_mod_index = (v.op3.amplitude * MOD_INDEX_COEFF); - float op3_signal = - tx_operator_process_sample(v.op3, v.gate, v.trigger, op3_freq, v.velocity, fb_signal) * op3_mod_index; - - float op2_freq = frequency * v.op2.ratio; - float op2_mod_index = (v.op2.amplitude * MOD_INDEX_COEFF); - float op2_signal = tx_operator_process_sample(v.op2, v.gate, v.trigger, op2_freq, v.velocity) * op2_mod_index; - - float op1_freq = frequency * v.op1.ratio; - return tx_operator_process_sample(v.op1, v.gate, v.trigger, op1_freq, v.velocity, op2_signal + op3_signal) * - v.op1.amplitude; -} - -template <> -inline void voice_process_block(tx_voice& v, float** frames, size_t num_frames, midi_event* events, - size_t num_events, const vector>& mods) -{ - float frequency = midi_to_frequency(v.midi_note + v.pitch_mod + v.additional_pitch_mod); - - for (int s = 0; s < num_frames; s++) { - - float pitch_env_signal = tx_envelope_process_sample(v.pitch_env, v.gate, v.trigger) * v.pitch_env_amt; + 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 - switch (v.algorithm) { - case 0: - output = calc_algo1(v, pitched_freq); - break; - case 1: - output = calc_algo2(v, pitched_freq); - break; - case 2: - output = calc_algo3(v, pitched_freq); - break; - case 3: - output = calc_algo4(v, pitched_freq); - break; - default: - output = calc_algo1(v, pitched_freq); - break; + 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 - v.trigger = false; + s.trigger = false; - float res = powf(2, v.bit_resolution); + float res = powf(2, t.bit_resolution); output = roundf(output * res) / res; - frames[0][s] += output / 3.; - frames[1][s] = frames[0][s]; + 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_process_block(tx_synth& s, float** audio, size_t num_frames, const vector& midi_events, + const vector>& mods) +{ + voice_allocator_process_block(s.allocator, midi_events); + + for (int i = 0; i < MAX_VOICES; 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; + } } } diff --git a/synth/voice_allocator.h b/synth/voice_allocator.h index 6bb20df..91c42f5 100644 --- a/synth/voice_allocator.h +++ b/synth/voice_allocator.h @@ -1,154 +1,138 @@ #pragma once -#include "audio_buffer.h" -#include "midi_event.h" -#include -#include -#include -#include +#include +#include #include +using namespace std; + namespace trnr { -template -class voice_allocator { -public: - std::vector> voice_ptrs; - std::vector input_queue; - int index_to_steal = 0; - const int internal_block_size = 16; - size_t active_voice_count; - bool steal_non_gated = true; +enum midi_event_type { + note_on = 0, + note_off, + pitch_wheel, + mod_wheel +}; - voice_allocator(size_t num_voices = 1) - { - assert(num_voices > 0 && "number of voices must be greater than 0"); - init_voice_ptrs(num_voices); +struct midi_event { + midi_event_type type; + int offset; + int midi_note; + float velocity; + double data; +}; + +#ifndef MAX_VOICES +#define MAX_VOICES 16 +#endif + +#ifndef MAX_EVENTS_PER_VOICE +#define MAX_EVENTS_PER_VOICE 32 +#endif + +struct voice_state { + int midi_note; + bool is_busy; + bool gate; + bool trigger; + float velocity; + array events; +}; + +struct voice_allocator { + array voices; + array counts; + int active_voice_count = 1; + size_t index_to_steal = 0; +}; + +inline void voice_allocator_init(voice_allocator& va) +{ + for (size_t i = 0; i < MAX_VOICES; ++i) { va.counts[i] = 0; } +} + +inline void voice_allocator_process_block(voice_allocator& va, const vector& midi_events) +{ + // reset voice events and counts + for (int i = 0; i < MAX_VOICES; i++) { + for (int j = 0; j < MAX_EVENTS_PER_VOICE; j++) { va.voices[i].events[j] = midi_event {}; } } - void set_voice_count(const int& voice_count) - { - active_voice_count = std::min(voice_count, voice_ptrs.size()); - } - - void note_on(const midi_event& event) - { - auto voice = get_free_voice(); - - if (!voice) { voice = steal_voice(); } - - if (voice) { voice->note_on(event.midi_note, event.velocity); } - } - - void note_off(const midi_event& event) - { - for (const auto& v : voice_ptrs) { - if (v->midi_note == event.midi_note) v->note_off(); - } - } - - void access(std::function f) - { - std::for_each(voice_ptrs.begin(), voice_ptrs.end(), [&](std::shared_ptr ptr) { - if (ptr) { - f(ptr.get()); // Call the function with the raw pointer - } - }); - } - - void process_samples(t_sample** _outputs, int _start_index, int _block_size, - std::vector> _modulators = {}) - { - for (int b = _start_index; b < _start_index + _block_size; b += internal_block_size) { - - // process all events in the block (introduces potential inaccuracy of up to 16 samples) - process_events(b, internal_block_size); - - for (size_t i = 0; i < active_voice_count; ++i) { - voice_ptrs[i]->process_samples(_outputs, b, internal_block_size, _modulators); - } - } - } - - void add_event(midi_event event) { input_queue.push_back(event); } - - bool voices_active() - { - bool voices_active = false; - - for (const auto& v : voice_ptrs) { - bool busy = v->is_busy(); - voices_active |= busy; - } - - return voices_active; - } - - void set_samplerate(double _samplerate) - { - for (const auto& v : voice_ptrs) { v->set_samplerate(_samplerate); } - } - - void init_voice_ptrs(size_t num_voices) - { - voice_ptrs.reserve(num_voices); - - for (size_t i = 0; i < num_voices; ++i) { voice_ptrs.emplace_back(std::make_shared()); } - } - - std::shared_ptr get_free_voice() - { - for (size_t i = 0; i < active_voice_count; ++i) { - if (!voice_ptrs[i]->is_busy()) { return voice_ptrs[i]; } - } - - return nullptr; - } - - std::shared_ptr steal_voice() - { - // Try to find a voice that is not gated (not playing a note) - if (steal_non_gated) - for (size_t i = 0; i < active_voice_count; ++i) { - if (!voice_ptrs[i]->gate) { return voice_ptrs[i]; } - } - - // If all voices are gated, steal one round-robin - auto voice = voice_ptrs[index_to_steal]; - index_to_steal++; - if (index_to_steal >= active_voice_count) index_to_steal = 0; - return voice; - } - - void process_events(int _start_index, int _block_size) - { - for (int s = _start_index; s < _start_index + _block_size; s++) { - auto iterator = input_queue.begin(); - while (iterator != input_queue.end()) { - - midi_event& event = *iterator; - if (event.offset == s) { - - switch (event.type) { - case midi_event_type::note_on: - note_on(event); - break; - case midi_event_type::note_off: - note_off(event); - break; - case midi_event_type::pitch_wheel: - access([&event](t_voice* voice) { voice->modulate_pitch(event.data); }); - break; - default: - break; - } - - iterator = input_queue.erase(iterator); - } else { - iterator++; + for (const auto& ev : midi_events) { + switch (ev.type) { + case note_on: { + bool found = false; + // attempt to find a free voice + for (size_t i = 0; i < va.active_voice_count; ++i) { + if (!va.voices[i].is_busy) { + va.voices[i].events[va.counts[i]++] = ev; + found = true; + break; } } + + if (found) break; + + // try to find a voice that is not gated + for (size_t i = 0; i < va.active_voice_count; ++i) { + if (!va.voices[i].gate) { + va.voices[i].events[va.counts[i]++] = ev; + found = true; + break; + } + } + + if (found) break; + + // if all voices are gated, steal one round-robin + va.voices[va.index_to_steal].events[va.counts[va.index_to_steal]++] = ev; + va.index_to_steal++; + if (va.index_to_steal >= va.active_voice_count) va.index_to_steal = 0; + break; + } + case note_off: { + for (size_t i = 0; i < va.active_voice_count; ++i) { + if (va.voices[i].midi_note == ev.midi_note) va.voices[i].events[va.counts[i]++] = ev; + } + break; + } + case pitch_wheel: + case mod_wheel: { + for (size_t i = 0; i < va.active_voice_count; ++i) { va.voices[i].events[va.counts[i]++] = ev; } + break; + } } } -}; +} + +inline void voice_process_event_for_frame(voice_state& v, size_t frame) +{ + const midi_event* best_event = nullptr; + + for (int i = 0; i < v.events.size(); i++) { + const midi_event& ev = v.events[i]; + if (ev.offset == frame) { + best_event = &ev; + if (ev.type == note_on) break; + } + } + + if (best_event) switch (best_event->type) { + case note_on: + v.midi_note = best_event->midi_note; + v.velocity = best_event->velocity; + v.is_busy = true; + v.gate = true; + v.trigger = true; + break; + case note_off: + v.gate = false; + break; + // TODO: handle pitch wheel and mod wheel events + case pitch_wheel: + case mod_wheel: + break; + } +} } // namespace trnr