diff --git a/.vscode/c_cpp_properties.json b/.vscode/c_cpp_properties.json new file mode 100644 index 0000000..f1ebb45 --- /dev/null +++ b/.vscode/c_cpp_properties.json @@ -0,0 +1,22 @@ +{ + "configurations": [ + { + "name": "Mac", + "includePath": [ + "${workspaceFolder}/**" + ], + "defines": [ + "__LITTLE_ENDIAN__" + ], + "macFrameworkPath": [ + "/Library/Developer/CommandLineTools/SDKs/MacOSX.sdk/System/Library/Frameworks" + ], + "compilerPath": "/usr/bin/clang", + "cStandard": "c17", + "cppStandard": "c++17", + "intelliSenseMode": "macos-clang-x64", + "configurationProvider": "ms-vscode.cmake-tools" + } + ], + "version": 4 +} \ No newline at end of file diff --git a/CMakeLists.txt b/CMakeLists.txt index e5e41bd..0fd87c4 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -3,13 +3,14 @@ project(synth) set(CMAKE_CXX_STANDARD 11) set(BUILD_SHARED_LIBS OFF) +add_definitions(-D__LITTLE_ENDIAN__) set(PA_USE_ASIO ON CACHE BOOL "Enable support for ASIO") add_subdirectory(lib/portaudio) add_subdirectory(lib/portmidi) add_subdirectory(lib/wxWidgets) -add_executable(main src/synthapp.cpp src/synthframe.cpp src/synth/synth.cpp src/synth/dsp/adsr.cpp) +add_executable(main src/synthapp.cpp src/synthframe.cpp src/synth/synth.cpp src/synth/part.cpp src/synth/dsp/adsr.cpp) target_include_directories(main PRIVATE include) target_link_libraries(main PRIVATE PortAudio) diff --git a/include/synth/dsp/adsr.h b/include/synth/dsp/adsr.h index 8bb152f..55e5784 100644 --- a/include/synth/dsp/adsr.h +++ b/include/synth/dsp/adsr.h @@ -7,19 +7,21 @@ class ADSR { public: - struct Envelope { + typedef struct { float attackStep; float decayStep; float sustain; float releaseStep; - }; + } Envelope; - ADSR(); - void setEnvelope(const Envelope* envelope); + const Envelope* env; + + ADSR() = delete; + ADSR(const Envelope *env); void noteOn(); void noteOff(); - float tick() { + inline float tick() { float out = curve(gain); switch(state) { @@ -28,7 +30,7 @@ public: case ATTACK: if(gain < 1) { - gain += envelope->attackStep; + gain += env->attackStep; } else { state = DECAY; gain = 1; @@ -36,11 +38,11 @@ public: break; case DECAY: - if(gain > envelope->sustain) { - gain -= envelope->decayStep; + if(gain > env->sustain) { + gain -= env->decayStep; } else { state = SUSTAIN; - gain = envelope->sustain; + gain = env->sustain; } break; @@ -49,7 +51,7 @@ public: case RELEASE: if(gain > 0) { - gain -= envelope->releaseStep; + gain -= env->releaseStep; } else { state = IDLE; gain = 0; @@ -62,10 +64,6 @@ public: private: enum { IDLE, ATTACK, DECAY, SUSTAIN, RELEASE } state; - - static const Envelope DEFAULT_ENVELOPE; - - const Envelope* envelope; float gain; float curve(float gain) { diff --git a/include/synth/dsp/filter.h b/include/synth/dsp/filter.h new file mode 100644 index 0000000..afd090f --- /dev/null +++ b/include/synth/dsp/filter.h @@ -0,0 +1,88 @@ +#ifndef __SVF_H__ +#define __SVF_H__ + +/* CEM3320/Oberheim/Phrophet-style filter as described here: https://arxiv.org/pdf/2111.05592.pdf */ + +#include + +#include "util.h" + +class SVF12 { +public: + float frequency; + float resonance; + + typedef struct { + float lp; // low-pass + float bp; // band-pass + float hp; // high-pass + } Output; + +protected: + float as1, as2; + +public: + SVF12() : as1(0), as2(0) {} + + Output tick(float as, float freq, float Q) { + Output out; + float kK = tan(M_PI_4 * clamp(freq, 0, 1)); + float kQ = fmaxf(0, Q); + + float kdiv = 1 + kK/kQ + kK*kK; + out.hp = (as - (1/kQ + kK) * as1 - as2) / kdiv; + float au = out.hp * kK; + out.bp = au + as1; + as1 = au + out.bp; + au = out.bp * kK; + out.lp = au + as2; + as2 = au + out.lp; + + return out; + } +}; + +// 12 and 24 dB/oct +class Filter { +public: + enum Type { + TYPE_LP = 0, + TYPE_BP = 42, + TYPE_HP = 84 + }; + + enum Slope { + SLOPE_24 = 0, + SLOPE_12 = 64, + }; + + typedef struct { + float freq; + float Q; + Type type; + Slope slope; + } Settings; + +private: + const Settings* settings; + SVF12 first, second; + +public: + Filter() = delete; + Filter(const Settings *settings): settings(settings) {} + + inline float tick(float as, float freqMod) { + SVF12::Output in = first.tick(as, settings->freq + freqMod, settings->Q); + + switch(settings->type) { + case TYPE_LP: + return settings->slope == SLOPE_24 ? second.tick(in.lp, settings->freq + freqMod, settings->Q).lp : in.lp; + case TYPE_BP: + return settings->slope == SLOPE_24 ? second.tick(in.bp, settings->freq + freqMod, settings->Q).bp : in.bp; + case TYPE_HP: + return settings->slope == SLOPE_24 ? second.tick(in.hp, settings->freq + freqMod, settings->Q).hp : in.hp; + } + } +}; + +#endif \ No newline at end of file diff --git a/include/synth/dsp/oscillator.h b/include/synth/dsp/oscillator.h index cf08d2e..ac41d2f 100644 --- a/include/synth/dsp/oscillator.h +++ b/include/synth/dsp/oscillator.h @@ -13,25 +13,15 @@ class Oscillator { public: enum Mode { MODE_SINE, MODE_SAW, MODE_SQUARE }; - Mode mode; + const Mode *mode; float phase; // current waveform phase angle (radians) - float phaseStep; // phase angle step per sample float value; // current amplitude value float driftAmount; float driftValue; - Oscillator() { - mode = MODE_SAW; - phase = 0; - phaseStep = (440 * 2 * M_PI) / 44100; - value = 0; - driftAmount = 0.001; - driftValue = 0; - } - - float polyBlep(float t) { - float dt = phaseStep / PIx2; + Oscillator(const Mode *mode) : mode(mode), phase(0), value(0), driftAmount(0.001), driftValue(0) {} + inline float polyBlep(float t, float dt) { // t-t^2/2 +1/2 // 0 < t <= 1 // discontinuities between 0 & 1 @@ -54,22 +44,23 @@ public: } // Generate next output sample and advance the phase angle - float tick() { + inline float tick(float phaseStep) { float t = phase / PIx2; // Define half phase + float dt = phaseStep / PIx2; - if (mode == MODE_SINE) { + if (*mode == MODE_SINE) { value = sinf(phase); // No harmonics in sine so no aliasing!! No Poly BLEPs needed! - } else if (mode == MODE_SAW) { + } else if (*mode == MODE_SAW) { value = (2.0 * phase / PIx2) - 1.0; // Render naive waveshape - value -= polyBlep(t); // Layer output of Poly BLEP on top - } else if (mode == MODE_SQUARE) { + value -= polyBlep(t, dt); // Layer output of Poly BLEP on top + } else if (*mode == MODE_SQUARE) { if (phase < M_PI) { value = 1.0; // Flip } else { value = -1.0; // Flop } - value += polyBlep(t); // Layer output of Poly BLEP on top (flip) - value -= polyBlep(fmodf(t + 0.5, 1.0)); // Layer output of Poly BLEP on top (flop) + value += polyBlep(t, dt); // Layer output of Poly BLEP on top (flip) + value -= polyBlep(fmodf(t + 0.5, 1.0), dt); // Layer output of Poly BLEP on top (flop) } driftValue += 0.01 * ((float) rand() / RAND_MAX - 0.5) - 0.00001 * driftValue; diff --git a/include/synth/dsp/svf.h b/include/synth/dsp/svf.h deleted file mode 100644 index 7cac4f8..0000000 --- a/include/synth/dsp/svf.h +++ /dev/null @@ -1,117 +0,0 @@ -#ifndef __SVF_H__ -#define __SVF_H__ - -/* CEM3320/Oberheim/Phrophet-style filter as described here: https://arxiv.org/pdf/2111.05592.pdf */ - -#include - -#include "util.h" - -class SVF12 { -protected: - float as1, as2; - float kdiv; - float kK, kQ; - - inline void updateCoeffs() { - kdiv = 1 + kK/kQ + kK*kK; - } - -public: - typedef struct { - float hp; // high-pass - float bp; // band-pass - float lp; // low-pass - } Output; - - SVF12() { - as1 = 0; - as2 = 0; - kK = tan(M_PI_4); - kQ = M_SQRT1_2; - updateCoeffs(); - } - - void setFrequency(float f) { - kK = tan(M_PI_4 * clamp(f, 0, 1)); - updateCoeffs(); - } - - void setQ(float q) { - kQ = q; - updateCoeffs(); - } - - Output tick(float as) { - Output out; - out.hp = (as - (1/kQ + kK) * as1 - as2) / kdiv; - float au = out.hp * kK; - out.bp = au + as1; - as1 = au + out.bp; - au = out.bp * kK; - out.lp = au + as2; - as2 = au + out.lp; - return out; - } -}; - -// 12 and 24 dB/oct -class SVF { -public: - enum Mode { - MODE_HP_12, - MODE_HP_24, - MODE_BP_12, - MODE_BP_24, - MODE_LP_12, - MODE_LP_24, - }; - -private: - Mode mode; - SVF12 first, second; - -public: - SVF() { - this->mode = MODE_LP_24; - } - - void setMode(Mode mode) { - this->mode = mode; - } - - void setFrequency(float f) { - first.setFrequency(f); - second.setFrequency(f); - } - - void setQ(float q) { - first.setQ(q); - second.setQ(q); - } - - float tick(float as) { - SVF12::Output in = first.tick(as); - switch(mode) { - case MODE_HP_12: - return in.hp; - - case MODE_HP_24: - return second.tick(in.hp).hp; - - case MODE_BP_12: - return in.bp; - - case MODE_BP_24: - return second.tick(in.bp).bp; - - case MODE_LP_12: - return in.lp; - - case MODE_LP_24: - return second.tick(in.lp).lp; - } - } -}; - -#endif \ No newline at end of file diff --git a/include/synth/globals.h b/include/synth/globals.h new file mode 100644 index 0000000..44aba82 --- /dev/null +++ b/include/synth/globals.h @@ -0,0 +1,6 @@ +#ifndef __GLOBALS_H__ +#define __GLOBALS_H__ + +#define SAMPLE_RATE 48000 + +#endif \ No newline at end of file diff --git a/include/synth/part.h b/include/synth/part.h new file mode 100644 index 0000000..167da48 --- /dev/null +++ b/include/synth/part.h @@ -0,0 +1,33 @@ +#ifndef __PART_H__ +#define __PART_H__ + +#include +#include + +#include "voicemanager.h" +#include "voice.h" +#include "preset.h" + +class Part { +public: + VoiceManager* voiceManager; + + Voice::Settings settings; + + float pitchBend; + float modulation; + + std::map notes; + + void loadPreset(Preset* preset); + + void noteOn(int note, int vel); + void noteOff(int note); + void control(int cc, int val); + + float tick() { + return 0; + } +}; + +#endif \ No newline at end of file diff --git a/include/synth/preset.h b/include/synth/preset.h index affdd68..a124513 100644 --- a/include/synth/preset.h +++ b/include/synth/preset.h @@ -1,13 +1,58 @@ #ifndef __PRESET_H__ #define __PRESET_H__ +#include "dsp/oscillator.h" +#include "dsp/filter.h" #include "dsp/adsr.h" struct Preset { - float cutoff; - float resonance; + typedef struct { + uint8_t attack; + uint8_t decay; + uint8_t sustain; + uint8_t release; + } Envelope; - ADSR::Envelope ampEnv, fltEnv; + uint8_t osc1Mode; + uint8_t osc2Mode; + uint8_t oscMix; + + struct { + uint8_t type; + uint8_t slope; + uint8_t freq; + uint8_t Q; + } filter; + + Envelope ampEnv; + Envelope fltEnv; +}; + +static const Preset DEFAULT_PRESET = { + .osc1Mode = Oscillator::MODE_SAW, + .osc2Mode = Oscillator::MODE_SAW, + .oscMix = 0, + + .filter = { + .type = Filter::TYPE_LP, + .slope = Filter::SLOPE_24, + .freq = 127, + .Q = 0, + }, + + .ampEnv = { + .attack = 0, + .decay = 0, + .sustain = 127, + .release = 0 + }, + + .fltEnv = { + .attack = 0, + .decay = 0, + .sustain = 127, + .release = 0 + } }; #endif \ No newline at end of file diff --git a/include/synth/synth.h b/include/synth/synth.h index 70ee4eb..39b27b0 100644 --- a/include/synth/synth.h +++ b/include/synth/synth.h @@ -1,19 +1,23 @@ #ifndef __SYNTH_H__ #define __SYNTH_H__ -#include "voice.h" - -const int SAMPLE_RATE = 48000; -const int NUM_VOICES = 72; +#include "voicemanager.h" +#include "part.h" class Synth { -public: - void noteOn(int note, int velocity); - void noteOff(int note); - void control(int code, int value); +public: + Synth() { + for(int i = 0; i < 16; ++i) { + parts[i].voiceManager = &voiceManager; + } + } + void noteOn(int ch, int note, int vel); + void noteOff(int ch, int note); + void control(int ch, int cc, int val); private: - Voice voices[NUM_VOICES]; + VoiceManager voiceManager{}; + Part parts[16]; }; #endif \ No newline at end of file diff --git a/include/synth/voice.h b/include/synth/voice.h index e306475..561da6e 100644 --- a/include/synth/voice.h +++ b/include/synth/voice.h @@ -4,20 +4,43 @@ #include "preset.h" #include "dsp/oscillator.h" -#include "dsp/svf.h" +#include "dsp/filter.h" #include "dsp/adsr.h" class Voice { public: - static const int NUM_OSCS = 2; + typedef struct { + Filter::Settings filter; - void usePreset(Preset* preset); + Oscillator::Mode osc1Mode; + Oscillator::Mode osc2Mode; + float oscMix; + + ADSR::Envelope ampEnv; + ADSR::Envelope fltEnv; + } Settings; private: - ADSR adsrAmp; - ADSR adsrFlt; - Oscillator oscs[NUM_OSCS]; - SVF filter; + const Settings* settings; + + ADSR adsrAmp{&settings->ampEnv}; + ADSR adsrFlt{&settings->fltEnv}; + Oscillator osc1{&settings->osc1Mode}; + Oscillator osc2{&settings->osc2Mode}; + Filter filter{&settings->filter}; + +public: + Voice() = delete; + Voice(const Settings* settings) : settings(settings) {} + + inline float tick(float osc1PhaseStep, float osc2PhaseStep) { + float out = 0; + out += (1 - settings->oscMix) * osc1.tick(osc1PhaseStep); + out += settings->oscMix * osc2.tick(osc2PhaseStep); + out = filter.tick(out, adsrFlt.tick()); + out *= adsrAmp.tick(); + return out; + } }; #endif \ No newline at end of file diff --git a/include/synth/voicemanager.h b/include/synth/voicemanager.h new file mode 100644 index 0000000..1fb7ca4 --- /dev/null +++ b/include/synth/voicemanager.h @@ -0,0 +1,10 @@ +#ifndef __VOICEMANAGER_H__ +#define __VOICEMANAGER_H__ + +class VoiceManager { +public: +private: + +}; + +#endif \ No newline at end of file diff --git a/src/synth/dsp/adsr.cpp b/src/synth/dsp/adsr.cpp index 4a779af..c7a15a0 100644 --- a/src/synth/dsp/adsr.cpp +++ b/src/synth/dsp/adsr.cpp @@ -1,20 +1,9 @@ #include "synth/dsp/adsr.h" -const ADSR::Envelope ADSR::DEFAULT_ENVELOPE = { - .attackStep = 1, - .decayStep = 1, - .sustain = 1, - .releaseStep = 1 -}; - -ADSR::ADSR() { - this->envelope = &DEFAULT_ENVELOPE; +ADSR::ADSR(const Envelope *env) { + this->env = env; this->state = IDLE; - this->gain = 0; -} - -void ADSR::setEnvelope(const Envelope* envelope) { - this->envelope = envelope; + this->gain = 0; } void ADSR::noteOn() { @@ -23,4 +12,4 @@ void ADSR::noteOn() { void ADSR::noteOff() { state = RELEASE; -} +} \ No newline at end of file diff --git a/src/synth/part.cpp b/src/synth/part.cpp new file mode 100644 index 0000000..a99df58 --- /dev/null +++ b/src/synth/part.cpp @@ -0,0 +1,44 @@ +#include "synth/globals.h" + +#include "synth/part.h" + +float ccToA(int x) { + return 5.0 * exp(9.2 * (x / 127.0) - 9.2); +} + +float ccToDR(int x) { + return 5.0 * exp(7.0 * (x / 127.0) - 7.0); +} + +void Part::loadPreset(Preset* preset) { + settings.osc1Mode = Oscillator::Mode(preset->osc1Mode); + settings.osc2Mode = Oscillator::Mode(preset->osc2Mode); + settings.oscMix = preset->oscMix / 127.0; + + 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.Q = preset->filter.Q / 127.0; + + settings.ampEnv.attackStep = (1.0 / SAMPLE_RATE) / ccToA(preset->ampEnv.attack);; + settings.ampEnv.decayStep = (1.0 / SAMPLE_RATE) / ccToDR(preset->ampEnv.decay); + settings.ampEnv.sustain = preset->ampEnv.sustain / 127.0; + settings.ampEnv.releaseStep = (1.0 / SAMPLE_RATE) / ccToDR(preset->ampEnv.release); + + settings.fltEnv.attackStep = (1.0 / SAMPLE_RATE) / ccToA(preset->fltEnv.attack);; + settings.fltEnv.decayStep = (1.0 / SAMPLE_RATE) / ccToDR(preset->fltEnv.decay); + settings.fltEnv.sustain = preset->fltEnv.sustain / 127.0; + settings.fltEnv.releaseStep = (1.0 / SAMPLE_RATE) / ccToDR(preset->fltEnv.release); +} + +void Part::noteOn(int note, int velocity) { + +} + +void Part::noteOff(int note) { + +} + +void Part::control(int code, int value) { + +} \ No newline at end of file diff --git a/src/synth/synth.cpp b/src/synth/synth.cpp index 923e6c6..bad9330 100644 --- a/src/synth/synth.cpp +++ b/src/synth/synth.cpp @@ -1,13 +1,13 @@ #include "synth/synth.h" -void Synth::noteOn(int note, int velocity) { +void Synth::noteOn(int ch, int note, int vel) { } -void Synth::noteOff(int note) { +void Synth::noteOff(int ch, int note) { } -void Synth::control(int code, int value) { +void Synth::control(int ch, int cc, int val) { } \ No newline at end of file diff --git a/src/synthapp.cpp b/src/synthapp.cpp index 8db7665..2010ced 100644 --- a/src/synthapp.cpp +++ b/src/synthapp.cpp @@ -4,6 +4,8 @@ #include +#include "synth/globals.h" + #include "synthapp.h" #include "synthframe.h" diff --git a/src/synthframe.cpp b/src/synthframe.cpp index 0d0c06b..b37a762 100644 --- a/src/synthframe.cpp +++ b/src/synthframe.cpp @@ -5,14 +5,6 @@ wxDECLARE_APP(SynthApp); -float ccToA(int x) { - return 5.0 * exp(9.2 * (x / 127.0) - 9.2); -} - -float ccToDR(int x) { - return 5.0 * exp(7.0 * (x / 127.0) - 7.0); -} - SynthFrame::SynthFrame() : wxFrame(NULL, wxID_ANY, "Hello World") { wxMenu *menuFile = new wxMenu; menuFile->Append(wxID_EXIT); @@ -59,51 +51,35 @@ void SynthFrame::OnAbout(wxCommandEvent& event) { void SynthFrame::OnAmpAttackScroll(wxScrollEvent& event) { SynthApp& app = wxGetApp(); - float seconds = ccToA(event.GetPosition()); - //app.state.adsrAmp.attackStep = (1.0 / 48000.0) / seconds; + // event.GetPosition() } void SynthFrame::OnAmpDecayScroll(wxScrollEvent& event) { SynthApp& app = wxGetApp(); - float seconds = ccToDR(event.GetPosition()); - //app.state.adsrAmp.decayStep = (1.0 / 48000.0) / seconds; } void SynthFrame::OnAmpSustainScroll(wxScrollEvent& event) { SynthApp& app = wxGetApp(); - float gain = event.GetPosition() / 127.0; - std::cout << "Gain: " << gain << std::endl; - //app.state.adsrAmp.sustain = gain; } void SynthFrame::OnAmpReleaseScroll(wxScrollEvent& event) { SynthApp& app = wxGetApp(); - float seconds = ccToDR(event.GetPosition()); - //app.state.adsrAmp.releaseStep = (1.0 / 48000.0) / seconds; } void SynthFrame::OnFltAttackScroll(wxScrollEvent& event) { SynthApp& app = wxGetApp(); - float seconds = ccToA(event.GetPosition()); - //app.state.adsrFlt.attackStep = (1.0 / 48000.0) / seconds; } void SynthFrame::OnFltDecayScroll(wxScrollEvent& event) { SynthApp& app = wxGetApp(); - float seconds = ccToDR(event.GetPosition()); - //app.state.adsrFlt.decayStep = (1.0 / 48000.0) / seconds; } void SynthFrame::OnFltSustainScroll(wxScrollEvent& event) { SynthApp& app = wxGetApp(); - float gain = event.GetPosition() / 127.0; - //app.state.adsrFlt.sustain = gain; } void SynthFrame::OnFltReleaseScroll(wxScrollEvent& event) { SynthApp& app = wxGetApp(); - float seconds = ccToDR(event.GetPosition()); - //app.state.adsrFlt.releaseStep = (1.0 / 48000.0) / seconds; } wxBEGIN_EVENT_TABLE(SynthFrame, wxFrame)