diff --git a/CMakeLists.txt b/CMakeLists.txt index 5089f36..226c9b5 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -38,7 +38,8 @@ add_executable(main src/synth/dsp/oscillator.cpp src/synth/dsp/filter.cpp src/synth/dsp/filterlut.cpp - src/synth/dsp/adsr.cpp) + src/synth/dsp/adsr.cpp + src/synth/dsp/reverb.cpp) target_include_directories(main PRIVATE include) add_executable(genluts src/genluts.cpp) diff --git a/include/synth/cc.h b/include/synth/cc.h index ed4adde..c5020c9 100644 --- a/include/synth/cc.h +++ b/include/synth/cc.h @@ -19,5 +19,10 @@ #define CC_LFO_FRQ 23 // LFO Frequency #define CC_LFO_PIT 24 // LFO Pitch Modulation #define CC_LFO_FLT 25 // LFO Filter Modulation - +#define CC_UNISON 26 // Unison Amount +#define CC_OSC_MIX 27 // Oscillator 1/2 Mix +#define CC_OSC1MDE 28 // Oscillator 1 Mode +#define CC_OSC2MDE 29 // Oscillator 2 Mode +#define CC_FLT_TYP 30 // Filter Type +#define CC_FLT_SLP 31 // Filter Slope #endif \ No newline at end of file diff --git a/include/synth/channel.h b/include/synth/channel.h index 10cfac1..8760385 100644 --- a/include/synth/channel.h +++ b/include/synth/channel.h @@ -8,11 +8,11 @@ #include "voicemanager.h" #include "voice.h" #include "preset.h" -#include "frame.h" +#include "dsp/frame.h" class Channel { public: - channel_t number; + VoiceManager::channel_t number; VoiceManager* voiceManager; Voice::Settings settings; @@ -29,7 +29,9 @@ public: float lfo = sin(lfoPhase); Voice** voices = voiceManager->getChannelVoices(number); for(Voice** voice = voices; *voice != NULL; ++voice) { - out += (*voice)->tick(pitchBend + settings.lfoPitchMod * lfo, settings.lfoFltMod * lfo); + float voiceOut = (*voice)->tick(pitchBend + settings.lfoPitchMod * lfo, settings.lfoFltMod * lfo); + out.l += sinf((1 - (*voice)->pan) * (float) M_PI_2) * voiceOut; + out.r += sinf((*voice)->pan * (float) M_PI_2) * voiceOut; } out *= volume; diff --git a/include/synth/dsp/delayline.h b/include/synth/dsp/delayline.h new file mode 100644 index 0000000..1d36924 --- /dev/null +++ b/include/synth/dsp/delayline.h @@ -0,0 +1,36 @@ +#ifndef __DELAYLINE_H__ +#define __DELAYLINE_H__ + +#include + +#include "frame.h" + +template +class DelayLine { +private: + const size_t size; + frame samples[CAPACITY] = {{0, 0}}; + size_t cursor; + +public: + DelayLine(size_t size = CAPACITY) : size(size) {} + + inline frame tick(frame in) { + samples[cursor++] = in; + if(cursor == size) { + cursor = 0; + } + return samples[cursor]; + } + + inline frame tap(size_t n) { + size_t index = cursor + n; + if(index < size) { + return samples[index]; + } else { + return samples[index - size]; + } + } +}; + +#endif \ No newline at end of file diff --git a/include/synth/dsp/filter.h b/include/synth/dsp/filter.h index f993beb..bc75527 100644 --- a/include/synth/dsp/filter.h +++ b/include/synth/dsp/filter.h @@ -6,40 +6,42 @@ #include #include #include +#include +#include #include "../globals.h" #include "util.h" #define FILTER_OVERSAMPLE 4 -#define FILTER_K_MIN_FREQ 20.0f -#define FILTER_K_MAX_FREQ 15000.0f -#define FILTER_K_BASE logf(FILTER_K_MAX_FREQ / FILTER_K_MIN_FREQ) -#define FILTER_K_SCALE (2.f * FILTER_K_MIN_FREQ / (FILTER_OVERSAMPLE * SAMPLE_RATE)) +#define FILTER_K_SCALE (2 * CV_FREQ_MIN / (FILTER_OVERSAMPLE * SAMPLE_RATE)) +#define FILTER_CV_MAX 9.25 // MIDI note "135" (19912.126958213178287 Hz) +//#define USE_LUTS #define K_LUT_SIZE 64 #ifdef USE_LUTS - extern float kLUT[K_LUT_SIZE]; - inline float freqToK(float x) { - float intPart, fracPart = modf(x * (K_LUT_SIZE - 1), &intPart); + inline float cvToK(float cv) { + float intPart, fracPart = modf(cv * (K_LUT_SIZE - 1) / FILTER_K_LUT_MAX, &intPart); int index = (int) intPart; - return fracPart < FLT_EPSILON ? kLUT[index] : (1 - fracPart) * kLUT[index] + fracPart * kLUT[index + 1]; + if(index < 0) { + return kLUT[0]; + } else if(index >= K_LUT_SIZE - 1) { + return kLUT[K_LUT_SIZE - 1]; + } else { + return (1 - fracPart) * kLUT[index] + fracPart * kLUT[index + 1]; + } } #else - inline float freqToK(float x) { - return tanf((float) M_PI_2 * std::min(0.5f, FILTER_K_SCALE * expf(x * FILTER_K_BASE))); + inline float cvToK(float cv) { + return tanf((float) M_PI_2 * std::min(0.5f, std::min(1.f / FILTER_OVERSAMPLE, (float) FILTER_K_SCALE * exp2f(cv)))); } #endif -inline float noteToFltFreq(float note) { - return ((note - 69) / 12.0f) / (FILTER_K_BASE * (float) M_LOG2E); -} - class SVF12 { public: typedef struct { @@ -81,10 +83,15 @@ public: if(times < 2) { return process(in); } else { - float out = aa2.tick(aa1.tick(times * process(in), kK, kQ).lp, kK, kQ).lp; +// float out = aa2.tick(aa1.tick(times * process(in), kK, kQ).lp, kK, kQ).lp; +// for(int i = 1; i < times; ++i) { +// aa2.tick(aa1.tick(process(0), kK, kQ).lp, kK, kQ); +// } + float out = process(aa2.tick(aa1.tick(times * in, kK, kQ).lp, kK, kQ).lp); for(int i = 1; i < times; ++i) { - aa2.tick(aa1.tick(process(0), kK, kQ).lp, kK, kQ); + process(aa2.tick(aa1.tick(0, kK, kQ).lp, kK, kQ).lp); } + return out; } } @@ -95,13 +102,13 @@ class Filter { public: enum Type { TYPE_LP = 0, - TYPE_BP = 42, - TYPE_HP = 84 + TYPE_BP, + TYPE_HP }; enum Slope { SLOPE_24 = 0, - SLOPE_12 = 64, + SLOPE_12, }; typedef struct { @@ -119,9 +126,8 @@ private: public: void assign(Settings const * settings); - inline float tick(float in, float freqAdd) { - float freq = clamp(settings->freq + freqAdd, 0.f, 1.f); - float kK = freqToK(freq); + inline float tick(float in, float freqMod) { + float kK = cvToK(settings->freq + freqMod); float kQ = (float) M_SQRT1_2 + settings->res; return os.tick(in, [this, kK, kQ](float oin) { diff --git a/include/synth/dsp/frame.h b/include/synth/dsp/frame.h new file mode 100644 index 0000000..0259c2b --- /dev/null +++ b/include/synth/dsp/frame.h @@ -0,0 +1,141 @@ +#ifndef __FRAME_H__ +#define __FRAME_H__ + +struct frame { + float l; + float r; + + inline frame operator+(const frame& rhs) { + return { + .l = l + rhs.l, + .r = r + rhs.r + }; + } + + inline frame operator+(float rhs) { + return { + .l = l + rhs, + .r = r + rhs + }; + } + + inline frame operator-(const frame& rhs) { + return { + .l = l - rhs.l, + .r = r - rhs.r + }; + } + + inline frame operator-(float rhs) { + return { + .l = l - rhs, + .r = r - rhs + }; + } + + inline frame operator*(const frame& rhs) { + return { + .l = l * rhs.l, + .r = r * rhs.r + }; + } + + inline frame operator*(float rhs) { + return { + .l = l * rhs, + .r = r * rhs + }; + } + + inline frame operator/(const frame& rhs) { + return { + .l = l / rhs.l, + .r = r / rhs.r + }; + } + + inline frame operator/(float rhs) { + return { + .l = l / rhs, + .r = r / rhs + }; + } + + inline frame operator+=(const frame& rhs) { + this->l += rhs.l; + this->r += rhs.r; + return *this; + } + + inline frame operator+=(float rhs) { + this->l += rhs; + this->r += rhs; + return *this; + } + + inline frame operator-=(const frame& rhs) { + this->l -= rhs.l; + this->r -= rhs.r; + return *this; + } + + inline frame operator-=(float rhs) { + this->l -= rhs; + this->r -= rhs; + return *this; + } + + inline frame operator*=(const frame& rhs) { + this->l *= rhs.l; + this->r *= rhs.r; + return *this; + } + + inline frame operator*=(float rhs) { + this->l *= rhs; + this->r *= rhs; + return *this; + } + + inline frame operator/=(const frame& rhs) { + this->l /= rhs.l; + this->r /= rhs.r; + return *this; + } + + inline frame operator/=(float rhs) { + this->l /= rhs; + this->r /= rhs; + return *this; + } +}; + +inline frame operator+(float lhs, const frame& rhs) { + return { + lhs + rhs.l, + lhs + rhs.r + }; +} + +inline frame operator-(float lhs, const frame& rhs) { + return { + lhs + rhs.l, + lhs + rhs.r + }; +} + +inline frame operator*(float lhs, const frame& rhs) { + return { + lhs * rhs.l, + lhs * rhs.r + }; +} + +inline frame operator/(float lhs, const frame& rhs) { + return { + lhs / rhs.l, + lhs / rhs.r + }; +} + +#endif \ No newline at end of file diff --git a/include/synth/dsp/oscillator.h b/include/synth/dsp/oscillator.h index cd4b3cf..54d6f91 100644 --- a/include/synth/dsp/oscillator.h +++ b/include/synth/dsp/oscillator.h @@ -12,7 +12,7 @@ class Oscillator { public: - enum Mode { MODE_SINE, MODE_SAW, MODE_SQUARE }; + enum Mode { MODE_SINE = 0, MODE_SAW, MODE_SQUARE }; Mode const * mode; float phase; // current waveform phase angle (radians) @@ -25,9 +25,11 @@ public: void assign(Mode const * mode); // Generate next output sample and advance the phase angle - inline float tick(float phaseStep) { - float t = phase / PIx2; // Define half phase - float dt = phaseStep / PIx2; + inline float tick(float cv) { + const float phaseStep = (float) (2 * CV_FREQ_MIN) * exp2f(cv) / SAMPLE_RATE; + + const float t = phase / PIx2; // Define half phase + const float dt = phaseStep / PIx2; if (*mode == MODE_SINE) { value = sinf(phase); // No harmonics in sine so no aliasing!! No Poly BLEPs needed! diff --git a/include/synth/dsp/reverb.h b/include/synth/dsp/reverb.h new file mode 100644 index 0000000..12a6a0d --- /dev/null +++ b/include/synth/dsp/reverb.h @@ -0,0 +1,55 @@ +#ifndef __REVERB_H__ +#define __REVERB_H__ + +// Velvet noise reverb ("Late Reverberation Synthesis Using Filtered Velvet Noise") + +#include + +#include "frame.h" +#include "delayline.h" +#include "sap.h" + +#define REVERB_FRAMES 131072 +#define REVERB_TAPS 256 + +class Reverb { +private: + struct Tap { + size_t lp, rp; + float lg, rg; + }; + + DelayLine dl; + Tap taps[REVERB_TAPS]; + float lpfL = 0.f, lpfR = 0.f; + float hpfL = 0.f, hpfR = 0.f; + +public: + float mix = 0.25f; + + Reverb(); + + frame tick(frame in) { + dl.tick(in); + + frame out{0, 0}; + + for(Tap& tap : taps) { + out.l += tap.lg * dl.tap(tap.lp).l; + out.r += tap.rg * dl.tap(tap.rp).r; + } + + lpfL += 0.1f * (out.l - lpfL); + lpfR += 0.1f * (out.r - lpfR); + + hpfL += 0.05f * (lpfL - hpfL); + hpfR += 0.05f * (lpfR - hpfR); + + out.l = lpfL - hpfL; + out.r = lpfR - hpfR; + + return mix * out + (1 - mix) * in; + } +}; + +#endif \ No newline at end of file diff --git a/include/synth/dsp/sap.h b/include/synth/dsp/sap.h new file mode 100644 index 0000000..0f1aee1 --- /dev/null +++ b/include/synth/dsp/sap.h @@ -0,0 +1,28 @@ +#ifndef __SAP_H__ +#define __SAP_H__ + +// Schroeder All-Pass (delay w/feedback) + +#include + +#include "delayline.h" + +template +class SAP { +private: + const size_t size; + DelayLine dl; + +public: + float gain = 0.7; + + SAP(size_t size = CAPACITY) : size(size) {} + + inline frame tick(frame in) { + frame fb = in + dl.tap(0) * gain; + frame out = dl.tick(fb) + fb * -gain; + return out; + } +}; + +#endif \ No newline at end of file diff --git a/include/synth/dsp/util.h b/include/synth/dsp/util.h index ab3d1e1..38344cf 100644 --- a/include/synth/dsp/util.h +++ b/include/synth/dsp/util.h @@ -8,6 +8,8 @@ #define PIx2 ((float )(2 * M_PI)) +#define CV_FREQ_MIN 32.703195662574829 // MIDI note 24 (C1) + //#define USE_NOTE_LUTS #define NOTE_LUT_SIZE 128 #define CENT_LUT_SIZE 128 @@ -24,8 +26,8 @@ #else - inline float noteToFreq(float note) { - return 440 * powf(2.f, (note - 69) / 12.0f); + inline float noteToCV(float note) { + return (note - 24) / 12.f; } #endif diff --git a/include/synth/frame.h b/include/synth/frame.h deleted file mode 100644 index 80af2fd..0000000 --- a/include/synth/frame.h +++ /dev/null @@ -1,41 +0,0 @@ -#ifndef __FRAME_H__ -#define __FRAME_H__ - -struct frame { - float left; - float right; - - inline frame operator+(const frame& rhs) { - return { - .left = left + rhs.left, - .right = right + rhs.right - }; - } - - inline frame operator+=(const frame& rhs) { - this->left += rhs.left; - this->right += rhs.right; - return *this; - } - - inline frame operator*(float rhs) { - return { - .left = left * rhs, - .right = right * rhs - }; - } - - inline frame operator+=(float rhs) { - this->left += rhs; - this->right += rhs; - return *this; - } - - inline frame operator*=(float rhs) { - this->left *= rhs; - this->right *= rhs; - return *this; - } -}; - -#endif \ No newline at end of file diff --git a/include/synth/preset.h b/include/synth/preset.h index 1f17abd..dc85590 100644 --- a/include/synth/preset.h +++ b/include/synth/preset.h @@ -13,6 +13,8 @@ struct Preset { uint8_t release; } Envelope; + uint8_t unison; + uint8_t osc1Mode; uint8_t osc2Mode; uint8_t oscMix; diff --git a/include/synth/synth.h b/include/synth/synth.h index e9d48c0..ff7ab29 100644 --- a/include/synth/synth.h +++ b/include/synth/synth.h @@ -3,12 +3,13 @@ #include "voicemanager.h" #include "channel.h" -#include "frame.h" +#include "dsp/frame.h" +#include "dsp/reverb.h" class Synth { public: Synth() { - for(channel_t i = 0; i < 16; ++i) { + for(VoiceManager::channel_t i = 0; i < 16; ++i) { channels[i].number = i; channels[i].voiceManager = &voiceManager; channels[i].loadPreset(&DEFAULT_PRESET); @@ -24,13 +25,15 @@ public: for(auto& channel : channels) { out += channel.tick(); } - out *= 0.100f; + out = reverb.tick(out); + out *= 0.125f; return out; } private: VoiceManager voiceManager{}; Channel channels[16]; + Reverb reverb; }; #endif \ No newline at end of file diff --git a/include/synth/voice.h b/include/synth/voice.h index 89999cc..f41f85b 100644 --- a/include/synth/voice.h +++ b/include/synth/voice.h @@ -11,6 +11,8 @@ class Voice { public: typedef struct { + float unison; + Oscillator::Mode osc1Mode; Oscillator::Mode osc2Mode; float oscMix; @@ -40,23 +42,24 @@ private: public: int note; - float unisonPosition; + float pan; + float detune; void reset(); void assign(Settings const * settings); - void noteOn(int note, float unisonPosition = 0); + void noteOn(int note, float detune = 0, float pan = 0.5); void noteOff(); inline bool isIdle() { return adsrAmp.isIdle(); } - inline float tick(float pitchAdd, float fltFreqAdd) { - const float basePitch = note + pitchAdd + unisonPosition * 4 * settings->oscDetune; - const float osc1PhaseStep = noteToFreq(basePitch - settings->oscDetune) * PIx2 / SAMPLE_RATE; - const float osc2PhaseStep = noteToFreq(basePitch + settings->oscDetune + settings->osc2Pitch) * PIx2 / SAMPLE_RATE; + inline float tick(float pitchMod, float fltMod) { + const float basePitch = note + pitchMod + detune; + const float osc1CV = noteToCV(basePitch - settings->oscDetune); + const float osc2CV = noteToCV(basePitch + settings->oscDetune + settings->osc2Pitch); float out = 0; - out += (1 - settings->oscMix) * osc1.tick(osc1PhaseStep); - out += settings->oscMix * osc2.tick(osc2PhaseStep); - const float keyTrackVal = settings->keyTrack * noteToFltFreq(note); - out = filter.tick(out, settings->modEnvFltGain * adsrMod.tick() + keyTrackVal + fltFreqAdd); + out += (1 - settings->oscMix) * osc1.tick(osc1CV); + out += settings->oscMix * osc2.tick(osc2CV); + const float keyTrackVal = settings->keyTrack * (note - 72) / 12.f; + out = filter.tick(out, settings->modEnvFltGain * adsrMod.tick() + keyTrackVal + fltMod); out *= adsrAmp.tick(); return out; } diff --git a/include/synth/voicemanager.h b/include/synth/voicemanager.h index f37e57f..431361d 100644 --- a/include/synth/voicemanager.h +++ b/include/synth/voicemanager.h @@ -7,16 +7,18 @@ #define NUM_VOICES 32 -typedef unsigned int channel_t; -typedef unsigned int serial_t; - class VoiceManager { public: + typedef unsigned int channel_t; + VoiceManager(); Voice* get(channel_t channel); Voice** getChannelVoices(channel_t channel); private: + typedef unsigned int serial_t; + typedef unsigned int score_t; + struct VoiceData { channel_t channel; serial_t serial; @@ -25,6 +27,8 @@ private: unsigned int serial; VoiceData voices[NUM_VOICES]; + + unsigned int scoreVoice(VoiceData& voiceData); }; #endif \ No newline at end of file diff --git a/include/synthframe.h b/include/synthframe.h index b23194b..cde1dbd 100644 --- a/include/synthframe.h +++ b/include/synthframe.h @@ -13,9 +13,17 @@ public: private: enum { - OSC_DET_SLIDER = wxID_TOP, + UNISON_SLIDER = wxID_TOP, + + OSC1_MODE_SLIDER, + OSC2_MODE_SLIDER, + + OSC_DET_SLIDER, OSC2_PIT_SLIDER, + OSC_MIX_SLIDER, + FLT_TYPE_SLIDER, + FLT_SLOPE_SLIDER, FLT_FREQ_SLIDER, FLT_Q_SLIDER, @@ -43,9 +51,16 @@ private: void OnExit(wxCommandEvent& event); void OnAbout(wxCommandEvent& event); + void OnUnisonScroll(wxScrollEvent& event); + + void OnOsc1ModeScroll(wxScrollEvent& event); + void OnOsc2ModeScroll(wxScrollEvent& event); void OnOscDetScroll(wxScrollEvent& event); void OnOsc2PitScroll(wxScrollEvent& event); + void OnOscMixScroll(wxScrollEvent& event); + void OnFltTypeScroll(wxScrollEvent& event); + void OnFltSlopeScroll(wxScrollEvent& event); void OnFltFreqScroll(wxScrollEvent& event); void OnFltQScroll(wxScrollEvent& event); diff --git a/src/genluts.cpp b/src/genluts.cpp index 2cff62d..0dbf903 100644 --- a/src/genluts.cpp +++ b/src/genluts.cpp @@ -26,7 +26,7 @@ void dumpLUT(const char * filename, const char * identifier, float *lut, size_t int main(int argc, char** argv) { float kLUT[K_LUT_SIZE]; for(int i = 0; i < K_LUT_SIZE; ++i) { - kLUT[i] = tanf((float) M_PI_4 * FILTER_K_SCALE * expf(i / (K_LUT_SIZE - 1.0f) * FILTER_K_BASE)); + kLUT[i] = tan(std::max(M_PI_4, M_PI_2 * FILTER_K_SCALE * exp2(FILTER_CV_MAX * i / (K_LUT_SIZE - 1.0)))); } dumpLUT("src/synth/dsp/filterlut.cpp", "kLUT", kLUT, K_LUT_SIZE); diff --git a/src/synth/channel.cpp b/src/synth/channel.cpp index 143ee65..b2d689c 100644 --- a/src/synth/channel.cpp +++ b/src/synth/channel.cpp @@ -5,6 +5,10 @@ #include "synth/channel.h" +float midiToNote(int note) { + return note + 24; +} + float timeToStep(float t) { return (1.0f / SAMPLE_RATE) / t; } @@ -22,50 +26,57 @@ float ccToLFOStep(int x) { } float ccToLFOPitchMod(int x) { - return pow(x / 127.0, 2) * 6; + return 6.f * x * x / 16129.f; +} + +float ccToLFOFltMod(int x) { + return (float) FILTER_CV_MAX * (x - 64) / 126.f; } void Channel::loadPreset(const Preset * preset) { + settings.unison = 1 + floorf(preset->unison * 4.f / 128.f); + settings.osc1Mode = Oscillator::Mode(preset->osc1Mode); settings.osc2Mode = Oscillator::Mode(preset->osc2Mode); - settings.oscMix = preset->oscMix / 127.0; - settings.oscDetune = preset->oscDetune / 254.0; - settings.osc2Pitch = floorf(preset->osc2Pitch * (13.0 / 128.0)); + settings.oscDetune = preset->oscDetune / 254.f; + settings.osc2Pitch = floorf(preset->osc2Pitch * 13.f / 128.f); + settings.oscMix = preset->oscMix / 127.f; settings.filter.type = Filter::Type(preset->filter.type); settings.filter.slope = Filter::Slope(preset->filter.slope); - settings.filter.freq = preset->filter.freq / 127.0; - settings.filter.res = preset->filter.Q / 31.75; + settings.filter.freq = (float) FILTER_CV_MAX * preset->filter.freq / 127.f; + settings.filter.res = preset->filter.Q / 31.75f; settings.ampEnv.attackStep = timeToStep(ccToA(preset->ampEnv.attack)); settings.ampEnv.decayStep = timeToStep(ccToDR(preset->ampEnv.decay)); - settings.ampEnv.sustain = preset->ampEnv.sustain / 127.0; + settings.ampEnv.sustain = preset->ampEnv.sustain / 127.f; settings.ampEnv.releaseStep = timeToStep(ccToDR(preset->ampEnv.release)); settings.modEnv.attackStep = timeToStep(ccToA(preset->modEnv.attack)); settings.modEnv.decayStep = timeToStep(ccToDR(preset->modEnv.decay)); - settings.modEnv.sustain = preset->modEnv.sustain / 127.0; + settings.modEnv.sustain = preset->modEnv.sustain / 127.f; settings.modEnv.releaseStep = timeToStep(ccToDR(preset->modEnv.release)); - settings.modEnvFltGain = preset->modEnvFltGain / 127.0; - settings.keyTrack = preset->keyTrack / 127.0; + settings.modEnvFltGain = (float) FILTER_CV_MAX * preset->modEnvFltGain / 127.f; + settings.keyTrack = preset->keyTrack / 127.f; settings.lfoStep = ccToLFOStep(preset->lfoFreq); settings.lfoPitchMod = ccToLFOPitchMod(preset->lfoPitchMod); - settings.lfoFltMod = (preset->lfoFltMod - 64) / 126.0; + settings.lfoFltMod = ccToLFOFltMod(preset->lfoFltMod); } void Channel::noteOn(int note, int velocity) { - for(int i = -1; i <= 1; ++i) { + int range = 2 * (settings.unison - 1); + for(int i = -range; i <= range; i += 4) { Voice* const voice = voiceManager->get(number); voice->assign(&settings); - voice->noteOn(note, i); + voice->noteOn(midiToNote(note), i * settings.oscDetune, range ? 0.5f + 0.5f * i / range : 0.5f); } } void Channel::noteOff(int note) { Voice** voices = voiceManager->getChannelVoices(number); for(Voice** voice = voices; *voice != NULL; ++voice) { - if((*voice)->note == note) { + if((*voice)->note == midiToNote(note)) { (*voice)->noteOff(); } } @@ -74,7 +85,7 @@ void Channel::noteOff(int note) { void Channel::control(int code, int value) { switch(code) { case CC_VOLUME: // Volume (Standard MIDI) - volume = value / 127.0; + volume = value / 127.f; break; case CC_FLT_ATK: // Filter Attack Time @@ -86,7 +97,7 @@ void Channel::control(int code, int value) { break; case CC_FLT_SUS: // Filter Sustain - settings.modEnv.sustain = value / 127.0; + settings.modEnv.sustain = value / 127.f; break; case CC_FLT_REL: // Filter Release Time @@ -94,7 +105,7 @@ void Channel::control(int code, int value) { break; case CC_FLT_Q: // Timbre / Harmonic Content (Standard MIDI) - settings.filter.res = value / 31.75; + settings.filter.res = value / 31.75f; printf("Q=%f\n", M_SQRT1_2 + (double) settings.filter.res); break; @@ -107,7 +118,7 @@ void Channel::control(int code, int value) { break; case CC_FLT_FRQ: // Brightness (Standard MIDI) - settings.filter.freq = value / 127.0; + settings.filter.freq = (float) FILTER_CV_MAX * value / 127.f; break; case CC_AMP_DEC: // Decay Time @@ -115,23 +126,23 @@ void Channel::control(int code, int value) { break; case CC_AMP_SUS: // Sustain - settings.ampEnv.sustain = value / 127.0; + settings.ampEnv.sustain = value / 127.f; break; case CC_OSC_DET: // Detune (Standard MIDI) - settings.oscDetune = value / 254.0; + settings.oscDetune = value / 254.f; break; case CC_OSC2PIT: // Oscillator 2 Pitch - settings.osc2Pitch = floorf(value * (13.0 / 128.0)); + settings.osc2Pitch = floorf(value * (13.f / 128.f)); break; case CC_MOD_FLT: // Mod Envelope Filter Gain - settings.modEnvFltGain = value / 127.0; + settings.modEnvFltGain = (float) FILTER_CV_MAX * value / 127.f; break; case CC_KEY_TRK: // Filter Key Tracking - settings.keyTrack = value / 127.0; + settings.keyTrack = value / 127.f; break; case CC_LFO_FRQ: // LFO Frequency @@ -143,7 +154,36 @@ void Channel::control(int code, int value) { break; case CC_LFO_FLT: // LFO Filter Modulation - settings.lfoFltMod = (value - 64) / 126.0; + settings.lfoFltMod = ccToLFOFltMod(value); + break; + + case CC_UNISON: // Unison Amount + settings.unison = 1 + floorf(value * 4.f / 128.f); + break; + + case CC_OSC_MIX: // Oscillator 1/2 Mix + settings.oscMix = value / 127.f; break; + + case CC_OSC1MDE: // Oscillator 1 Mode + settings.osc1Mode = Oscillator::Mode(floorf(3.f * value / 128.f)); + break; + + case CC_OSC2MDE: // Oscillator 2 Mode + settings.osc2Mode = Oscillator::Mode(floorf(3.f * value / 128.f)); + break; + + case CC_FLT_TYP: // Filter Type + settings.filter.type = Filter::Type(floorf(3.f * value / 128.f)); + break; + + case CC_FLT_SLP: // Filter Slope + settings.filter.slope = Filter::Slope(floorf(2.f * value / 128.f)); + break; + + // LFO delay + // Reverb Time + // Reverb Brightness + // Reverb Density } } \ No newline at end of file diff --git a/src/synth/dsp/reverb.cpp b/src/synth/dsp/reverb.cpp new file mode 100644 index 0000000..67425b5 --- /dev/null +++ b/src/synth/dsp/reverb.cpp @@ -0,0 +1,16 @@ +#include + +#include "synth/dsp/reverb.h" + +Reverb::Reverb() { + size_t interval = REVERB_FRAMES / REVERB_TAPS - 4096 / REVERB_TAPS; + for(size_t tap = 0; tap < REVERB_TAPS; ++tap) { + float gain = exp2f(-8.f + 8.f * tap / (REVERB_TAPS - 1)); + taps[tap] = { + .lp = tap * interval + (rand() % interval), + .rp = tap * interval + (rand() % interval), + .lg = (rand() > RAND_MAX / 2 ? 1 : -1) * gain, + .rg = (rand() > RAND_MAX / 2 ? 1 : -1) * gain + }; + } +} diff --git a/src/synth/preset.cpp b/src/synth/preset.cpp index 206ceac..9a7f8c4 100644 --- a/src/synth/preset.cpp +++ b/src/synth/preset.cpp @@ -1,6 +1,8 @@ #include "synth/preset.h" const Preset DEFAULT_PRESET = { + .unison = 1, + .osc1Mode = Oscillator::MODE_SAW, .osc2Mode = Oscillator::MODE_SAW, .oscMix = 64, @@ -8,17 +10,17 @@ const Preset DEFAULT_PRESET = { .osc2Pitch = 0, .filter = { - .type = Filter::TYPE_LP, - .slope = Filter::SLOPE_24, - .freq = 64, - .Q = 50, + .type = 0, + .slope = 22, + .freq = 32, + .Q = 25, }, .ampEnv = { .attack = 0, .decay = 80, .sustain = 64, - .release = 100 + .release = 0 }, .modEnv = { diff --git a/src/synth/voice.cpp b/src/synth/voice.cpp index e394be4..d5c323d 100644 --- a/src/synth/voice.cpp +++ b/src/synth/voice.cpp @@ -14,9 +14,10 @@ void Voice::assign(Settings const * settings) { filter.assign(&settings->filter); } -void Voice::noteOn(int note, float unisonPosition) { +void Voice::noteOn(int note, float detune, float pan) { this->note = note; - this->unisonPosition = unisonPosition; + this->detune = detune; + this->pan = pan; adsrAmp.noteOn(); adsrMod.noteOn(); } diff --git a/src/synth/voicemanager.cpp b/src/synth/voicemanager.cpp index 7378d83..3a5ce98 100644 --- a/src/synth/voicemanager.cpp +++ b/src/synth/voicemanager.cpp @@ -3,6 +3,12 @@ #include "synth/voicemanager.h" +#define VOICE_SCORE_MSB (8 * sizeof(score_t) - 1) + +#define VOICE_SCORE_ACTIVE ( 1 << (VOICE_SCORE_MSB - 0)) +#define VOICE_SCORE_CHANNEL(channel) ((score_t) (channel) << (VOICE_SCORE_MSB - 4)) +#define VOICE_SCORE_SERIAL(serial) ((score_t) serial & ((1 << (VOICE_SCORE_MSB - 4)) - 1)) + VoiceManager::VoiceManager() : serial(0) { for(VoiceData& vd : voices) { vd.channel = 0; @@ -10,34 +16,53 @@ VoiceManager::VoiceManager() : serial(0) { } } +VoiceManager::score_t VoiceManager::scoreVoice(VoiceData& voiceData) { + return + (voiceData.voice.isIdle() ? 0 : VOICE_SCORE_ACTIVE) | + VOICE_SCORE_CHANNEL(voiceData.channel) | + VOICE_SCORE_SERIAL(voiceData.serial); +} + Voice * VoiceManager::get(channel_t channel) { // Sort idle voices first, then by highest channel and lowest serial - std::sort(std::begin(voices), std::end(voices), [](VoiceData& a, VoiceData& b) { - return (a.voice.isIdle() && !b.voice.isIdle()) || a.channel > b.channel || a.serial < b.serial; + std::sort(std::begin(voices), std::end(voices), [this](VoiceData& a, VoiceData& b) { + return scoreVoice(a) < scoreVoice(b); }); +/* + putchar('\n'); + for(VoiceData& vd : voices) { + printf("channel=%d serial=%d idle=%d note=%d score=%x\n", vd.channel, vd.serial, vd.voice.isIdle(), vd.voice.note, scoreVoice(vd)); + } + putchar('\n'); +*/ + VoiceData& voiceData = voices[0]; voiceData.channel = channel; voiceData.serial = serial++; voiceData.voice.reset(); - //putchar('\n'); - //for(VoiceData& vd : voices) { - // printf("channel=%d serial=%d idle=%d note=%d\n", vd.channel, vd.serial, vd.voice.isIdle(), vd.voice.note); - //} - //putchar('\n'); - return &voiceData.voice; } Voice** VoiceManager::getChannelVoices(channel_t channel) { static Voice* result[NUM_VOICES + 1]; Voice** voice = result; + bool empty = true; for(VoiceData& voiceData : voices) { - if(voiceData.channel == channel && !voiceData.voice.isIdle()) { - *voice++ = &voiceData.voice; + if(!voiceData.voice.isIdle()) { + empty = false; + if(voiceData.channel == channel) { + *voice++ = &voiceData.voice; + } } } *voice = NULL; + + // Reset serial if voice table is empty + if(empty) { + serial = 0; + } + return result; } diff --git a/src/synthapp.cpp b/src/synthapp.cpp index 6b5d89c..e4e9448 100644 --- a/src/synthapp.cpp +++ b/src/synthapp.cpp @@ -52,8 +52,8 @@ static int paCallback( for(unsigned long i = 0; i < framesPerBuffer; i++) { frame f = app->synth.tick(); - *out++ = f.left; - *out++ = f.right; + *out++ = f.l; + *out++ = f.r; } return 0; diff --git a/src/synthframe.cpp b/src/synthframe.cpp index 6d76161..98be0db 100644 --- a/src/synthframe.cpp +++ b/src/synthframe.cpp @@ -31,9 +31,16 @@ SynthFrame::SynthFrame() : wxFrame(NULL, wxID_ANY, "Hello World") { wxBoxSizer* sizer = new wxBoxSizer(wxHORIZONTAL); panel->SetSizer(sizer); sizer->AddStretchSpacer(); + sizer->Add(new CCSlider(panel, UNISON_SLIDER, DEFAULT_PRESET.unison, "Uni"), 1, wxEXPAND); + sizer->AddStretchSpacer(); + sizer->Add(new CCSlider(panel, OSC1_MODE_SLIDER, DEFAULT_PRESET.osc1Mode, "O1M"), 1, wxEXPAND); + sizer->Add(new CCSlider(panel, OSC2_MODE_SLIDER, DEFAULT_PRESET.osc2Mode, "O2M"), 1, wxEXPAND); sizer->Add(new CCSlider(panel, OSC_DET_SLIDER, DEFAULT_PRESET.oscDetune, "Det"), 1, wxEXPAND); sizer->Add(new CCSlider(panel, OSC2_PIT_SLIDER, DEFAULT_PRESET.osc2Pitch, "2nd"), 1, wxEXPAND); + sizer->Add(new CCSlider(panel, OSC_MIX_SLIDER, DEFAULT_PRESET.oscMix, "Mix"), 1, wxEXPAND); sizer->AddStretchSpacer(); + sizer->Add(new CCSlider(panel, FLT_TYPE_SLIDER, DEFAULT_PRESET.filter.type, "FTy"), 1, wxEXPAND); + sizer->Add(new CCSlider(panel, FLT_SLOPE_SLIDER, DEFAULT_PRESET.filter.slope, "FSl"), 1, wxEXPAND); sizer->Add(new CCSlider(panel, FLT_FREQ_SLIDER, DEFAULT_PRESET.filter.freq, "Frq"), 1, wxEXPAND); sizer->Add(new CCSlider(panel, FLT_Q_SLIDER, DEFAULT_PRESET.filter.Q, "Res"), 1, wxEXPAND); sizer->AddStretchSpacer(); @@ -71,6 +78,21 @@ void SynthFrame::OnAbout(wxCommandEvent& event) { "About Hello World", wxOK | wxICON_INFORMATION); } +void SynthFrame::OnUnisonScroll(wxScrollEvent& event) { + SynthApp& app = wxGetApp(); + app.synth.control(0, CC_UNISON, event.GetPosition()); +} + +void SynthFrame::OnOsc1ModeScroll(wxScrollEvent& event) { + SynthApp& app = wxGetApp(); + app.synth.control(0, CC_OSC1MDE, event.GetPosition()); +} + +void SynthFrame::OnOsc2ModeScroll(wxScrollEvent& event) { + SynthApp& app = wxGetApp(); + app.synth.control(0, CC_OSC2MDE, event.GetPosition()); +} + void SynthFrame::OnOscDetScroll(wxScrollEvent& event) { SynthApp& app = wxGetApp(); app.synth.control(0, CC_OSC_DET, event.GetPosition()); @@ -81,6 +103,21 @@ void SynthFrame::OnOsc2PitScroll(wxScrollEvent& event) { app.synth.control(0, CC_OSC2PIT, event.GetPosition()); } +void SynthFrame::OnOscMixScroll(wxScrollEvent& event) { + SynthApp& app = wxGetApp(); + app.synth.control(0, CC_OSC_MIX, event.GetPosition()); +} + +void SynthFrame::OnFltTypeScroll(wxScrollEvent& event) { + SynthApp& app = wxGetApp(); + app.synth.control(0, CC_FLT_TYP, event.GetPosition()); +} + +void SynthFrame::OnFltSlopeScroll(wxScrollEvent& event) { + SynthApp& app = wxGetApp(); + app.synth.control(0, CC_FLT_SLP, event.GetPosition()); +} + void SynthFrame::OnFltFreqScroll(wxScrollEvent& event) { SynthApp& app = wxGetApp(); app.synth.control(0, CC_FLT_FRQ, event.GetPosition()); @@ -158,9 +195,20 @@ void SynthFrame::OnLFOFilterScroll(wxScrollEvent& event) { wxBEGIN_EVENT_TABLE(SynthFrame, wxFrame) +EVT_COMMAND_SCROLL(SynthFrame::UNISON_SLIDER, SynthFrame::OnUnisonScroll) + + +//#define CC_OSC1MDE 28 // Oscillator 1 Mode +//#define CC_OSC2MDE 29 // Oscillator 2 Mode + +EVT_COMMAND_SCROLL(SynthFrame::OSC1_MODE_SLIDER, SynthFrame::OnOsc1ModeScroll) +EVT_COMMAND_SCROLL(SynthFrame::OSC2_MODE_SLIDER, SynthFrame::OnOsc2ModeScroll) EVT_COMMAND_SCROLL(SynthFrame::OSC_DET_SLIDER, SynthFrame::OnOscDetScroll) EVT_COMMAND_SCROLL(SynthFrame::OSC2_PIT_SLIDER, SynthFrame::OnOsc2PitScroll) +EVT_COMMAND_SCROLL(SynthFrame::OSC_MIX_SLIDER, SynthFrame::OnOscMixScroll) +EVT_COMMAND_SCROLL(SynthFrame::FLT_TYPE_SLIDER, SynthFrame::OnFltTypeScroll) +EVT_COMMAND_SCROLL(SynthFrame::FLT_SLOPE_SLIDER, SynthFrame::OnFltSlopeScroll) EVT_COMMAND_SCROLL(SynthFrame::FLT_FREQ_SLIDER, SynthFrame::OnFltFreqScroll) EVT_COMMAND_SCROLL(SynthFrame::FLT_Q_SLIDER, SynthFrame::OnFltQScroll)