From 1383f00b1cf6aa44d893a9e40f5c9b1c8437bd1c Mon Sep 17 00:00:00 2001 From: Thor Harald Johansen Date: Thu, 1 Jun 2023 13:17:28 +0200 Subject: [PATCH] Initial commit --- .gitignore | 1 + CMakeLists.txt | 31 ++++++ include/cc.h | 31 ++++++ include/channel.h | 83 ++++++++++++++ include/dsp/adsr.h | 85 +++++++++++++++ include/dsp/delayline.h | 74 +++++++++++++ include/dsp/filter.h | 142 ++++++++++++++++++++++++ include/dsp/fm.h | 107 ++++++++++++++++++ include/dsp/frame.h | 141 ++++++++++++++++++++++++ include/dsp/oscillator.h | 124 +++++++++++++++++++++ include/dsp/phys.h | 34 ++++++ include/dsp/reverb.h | 53 +++++++++ include/dsp/stereodelayline.h | 45 ++++++++ include/dsp/stereosap.h | 28 +++++ include/globals.h | 9 ++ include/luts.h | 26 +++++ include/perf.h | 37 +++++++ include/preset.h | 47 ++++++++ include/synth.h | 73 +++++++++++++ include/util.h | 69 ++++++++++++ include/voice.h | 135 +++++++++++++++++++++++ include/voicemanager.h | 34 ++++++ src/channel.cpp | 198 ++++++++++++++++++++++++++++++++++ src/dsp/adsr.cpp | 22 ++++ src/dsp/filter.cpp | 7 ++ src/dsp/oscillator.cpp | 5 + src/dsp/reverb.cpp | 16 +++ src/luts.cpp | 19 ++++ src/perf.cpp | 3 + src/preset.cpp | 42 ++++++++ src/synth.cpp | 18 ++++ src/voice.cpp | 33 ++++++ src/voicemanager.cpp | 71 ++++++++++++ 33 files changed, 1843 insertions(+) create mode 100644 .gitignore create mode 100644 CMakeLists.txt create mode 100644 include/cc.h create mode 100644 include/channel.h create mode 100644 include/dsp/adsr.h create mode 100644 include/dsp/delayline.h create mode 100644 include/dsp/filter.h create mode 100644 include/dsp/fm.h create mode 100644 include/dsp/frame.h create mode 100644 include/dsp/oscillator.h create mode 100644 include/dsp/phys.h create mode 100644 include/dsp/reverb.h create mode 100644 include/dsp/stereodelayline.h create mode 100644 include/dsp/stereosap.h create mode 100644 include/globals.h create mode 100644 include/luts.h create mode 100644 include/perf.h create mode 100644 include/preset.h create mode 100644 include/synth.h create mode 100644 include/util.h create mode 100644 include/voice.h create mode 100644 include/voicemanager.h create mode 100644 src/channel.cpp create mode 100644 src/dsp/adsr.cpp create mode 100644 src/dsp/filter.cpp create mode 100644 src/dsp/oscillator.cpp create mode 100644 src/dsp/reverb.cpp create mode 100644 src/luts.cpp create mode 100644 src/perf.cpp create mode 100644 src/preset.cpp create mode 100644 src/synth.cpp create mode 100644 src/voice.cpp create mode 100644 src/voicemanager.cpp diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..796b96d --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +/build diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..9dc7efb --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,31 @@ +cmake_minimum_required(VERSION 3.26) +project(synth) + +set(CMAKE_CXX_STANDARD 20) +if(MSVC) + set(CMAKE_CXX_FLAGS "/W4") + set(CMAKE_CXX_FLAGS_DEBUG "/DEBUG:FASTLINK /fp:fast") + set(CMAKE_CXX_FLAGS_RELEASE "/O2 /fp:fast /DNDEBUG") +else() + set(CMAKE_CXX_FLAGS "-Wall -Wextra -Wdouble-promotion -Rpass-analysis=loop-vectorize") + set(CMAKE_CXX_FLAGS_DEBUG "-g -ffast-math") + set(CMAKE_CXX_FLAGS_RELEASE "-Ofast -DNDEBUG") +endif() + +if(MSVC) + add_compile_definitions(_USE_MATH_DEFINES) +endif() + +add_library(synth + src/luts.cpp + src/synth.cpp + src/voicemanager.cpp + src/channel.cpp + src/preset.cpp + src/voice.cpp + src/dsp/oscillator.cpp + src/dsp/filter.cpp + src/dsp/adsr.cpp + src/dsp/reverb.cpp + src/perf.cpp) +target_include_directories(synth PUBLIC include) \ No newline at end of file diff --git a/include/cc.h b/include/cc.h new file mode 100644 index 0000000..fdff9d0 --- /dev/null +++ b/include/cc.h @@ -0,0 +1,31 @@ +#ifndef __CC_H__ +#define __CC_H__ + +#define CC_VOLUME 7 // Volume (Standard MIDI) +#define CC_FLT_ATK 16 // Filter Attack Time +#define CC_FLT_DEC 17 // Filter Decay Time +#define CC_FLT_SUS 18 // Filter Sustain +#define CC_FLT_REL 19 // Filter Release Time +#define CC_FLT_Q 71 // Timbre / Harmonic Content (Standard MIDI) +#define CC_AMP_REL 72 // Release Time (Standard MIDI) +#define CC_AMP_ATK 73 // Attack Time (Standard MIDI) +#define CC_FLT_FRQ 74 // Brightness (Standard MIDI) +#define CC_AMP_DEC 75 // Decay Time +#define CC_AMP_SUS 76 // Sustain +#define CC_OSC_DET 94 // Detune (Standard MIDI) +#define CC_MOD_FLT 20 // Mod Envelope Filter Gain +#define CC_KEY_TRK 21 // Filter Key Tracking +#define CC_OSC2PIT 22 // Oscillator 2 Pitch +#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 +#define CC_RVB_SND 102 // Reverb Send +#define CC_NOI_MIX 103 // Noise Mix + +#endif \ No newline at end of file diff --git a/include/channel.h b/include/channel.h new file mode 100644 index 0000000..f40be88 --- /dev/null +++ b/include/channel.h @@ -0,0 +1,83 @@ +#ifndef __PART_H__ +#define __PART_H__ + +#include +#include + +#include "globals.h" +#include "voicemanager.h" +#include "voice.h" +#include "preset.h" +#include "dsp/frame.h" +#include "luts.h" +#include "perf.h" + +class Channel { +public: + VoiceManager::channel_t number; + VoiceManager* voiceManager; + Voice::Settings settings; + + Channel() : volume(1), pitchBend(0), modulation(0), lfoPhase(randFrac()) {} + + void loadPreset(const Preset* preset); + + void noteOn(int note, int vel); + void noteOff(int note); + void control(int cc, int val); + + bool tick(float *out, const size_t bufferSize) { + nano_t started = nanos(); + + Voice** voices = voiceManager->getChannelVoices(number); + if(*voices == NULL) { + nano_t finished = nanos(); + perfNanos.channel += finished - started; + return false; + } + + const size_t numFrames = bufferSize / 2; + + float lfo[numFrames]; + for(size_t i = 0; i < numFrames; ++i) { + lfo[i] = fastSin(lfoPhase); + + lfoPhase += settings.lfoStep; + lfoPhase -= floorf(lfoPhase); + } + + float pitchMod[numFrames]; + for(size_t i = 0; i < numFrames; ++i) { + pitchMod[i] = settings.lfoPitchMod * lfo[i]; + } + + float fltMod[numFrames]; + for(size_t i = 0; i < numFrames; ++i) { + fltMod[i] = settings.lfoFltMod * lfo[i]; + } + + std::fill(out, out + bufferSize, 0.f); + for(Voice** voice = voices; *voice != NULL; ++voice) { + float voiceOut[bufferSize]; + (*voice)->tick(voiceOut, bufferSize, pitchMod, fltMod); + + for(size_t i = 0; i < bufferSize; ++i) { + out[i] += voiceOut[i]; + } + } + + nano_t finished = nanos(); + perfNanos.channel += finished - started; + + return true; + } + +private: + float volume; + float pitchBend; + float modulation; + + float lfoPhase; +}; + +#endif \ No newline at end of file diff --git a/include/dsp/adsr.h b/include/dsp/adsr.h new file mode 100644 index 0000000..70740ec --- /dev/null +++ b/include/dsp/adsr.h @@ -0,0 +1,85 @@ +#ifndef __ADSR_H__ +#define __ADSR_H__ + +#include + +#include "../util.h" + +class ADSR { +public: + typedef struct { + float attackStep; + float decayStep; + float sustain; + float releaseStep; + } Envelope; + + Envelope const * env; + + ADSR() : state(IDLE), t(0), last(0) {}; + void reset(); + void assign(Envelope const * env); + void noteOn(); + void noteOff(); + bool isIdle() { + return state == IDLE; + } + + inline float tick() { + switch(state) { + case IDLE: + return 0; + + case ATTACK: + if(t < 1) { + t += env->attackStep; + last = t*t; + } else { + state = DECAY; + t = 1; + last = 1; + } + break; + + case DECAY: + if(t > 0) { + t -= env->decayStep; + //last = env->sustain + (1 - env->sustain) * curve(t); + last = env->sustain + (1 - env->sustain) * t*t; + } else { + state = SUSTAIN; + t = 1; + last = env->sustain; + } + break; + + case SUSTAIN: + last = env->sustain; + break; + + case RELEASE: + if(t > 0) { + t -= env->releaseStep; + return last * t*t; + } else { + state = IDLE; + t = 0; + last = 0; + } + break; + } + + return last; + } + +private: + enum { IDLE, ATTACK, DECAY, SUSTAIN, RELEASE } state; + float t; + float last; + + float curve(float gain) { + return 0.5f - 0.5f * cosf(clamp(gain * (float) M_PI, 0.f, M_PI)); + } +}; + +#endif \ No newline at end of file diff --git a/include/dsp/delayline.h b/include/dsp/delayline.h new file mode 100644 index 0000000..f7a5544 --- /dev/null +++ b/include/dsp/delayline.h @@ -0,0 +1,74 @@ +#ifndef __DELAYLINE_H__ +#define __DELAYLINE_H__ + +#include + +template +class DelayLine { +private: + float samples[CAPACITY] = {0}; + size_t cursor; + +public: + size_t size; + + DelayLine(size_t size = CAPACITY) : size(size) {} + + inline float get() { + while(cursor >= size) { + cursor -= size; + } + return samples[cursor]; + } + + inline float tick() { + ++cursor; + while(cursor >= size) { + cursor -= size; + } + return samples[cursor]; + } + + inline float tick(float in) { + while(cursor >= size) { + cursor -= size; + } + samples[cursor++] = in; + if(cursor == size) { + cursor = 0; + } + return samples[cursor]; + } + + inline void inject(float in, size_t pos) { + size_t index = cursor + pos; + while(index >= size) { + index -= size; + } + samples[index] = in; + } + + inline void inject(float* in, size_t num, size_t pos = 0) { + size_t index = cursor + pos; + while(index >= size) { + index -= size; + } + for(size_t i = 0; i < num; ++i) { + samples[index++] = *(in++); + if(index == size) { + index = 0; + } + } + } + + inline float 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/dsp/filter.h b/include/dsp/filter.h new file mode 100644 index 0000000..9bdfe32 --- /dev/null +++ b/include/dsp/filter.h @@ -0,0 +1,142 @@ +#ifndef __SVF_H__ +#define __SVF_H__ + +/* CEM3320/Oberheim/Phrophet-style filter as described here: https://arxiv.org/pdf/2111.05592.pdf */ + +#include +#include +#include +#include +#include + +#include "../globals.h" +#include "../util.h" +#include "../luts.h" +#include "../perf.h" + +#define FILTER_K_SCALE (2 * CV_FREQ_MIN / SAMPLE_RATE) + +inline float cvToK(float cv) { + return tanf((float) M_PI_2 * std::min(0.5f, (float) FILTER_K_SCALE * exp2f(cv))); +} + +class SVF12Stereo { +protected: + float hp[2], bp[2], lp[2]; + float as1[2], as2[2]; + +public: + SVF12Stereo() : hp{0, 0}, bp{0, 0}, lp{0, 0}, as1{0, 0}, as2{0, 0} {} + + inline void tick(float *in, float *hpo, float *bpo, float *lpo, size_t bufferSize, float *kK, float *kQ) { + size_t frameCount = bufferSize / 2; + + float kQ_1[frameCount]; + for(size_t i = 0; i < frameCount; ++i) { + kQ_1[i] = 1.f / kQ[i]; + } + float kMul[frameCount]; + for(size_t i = 0; i < frameCount; ++i) { + kMul[i] = 1.f / (1.f + kK[i]*kQ_1[i] + kK[i]*kK[i]); + } + for(size_t i = 0, j = 0; i < bufferSize; ++i) { + size_t ch = i & 1; + + hp[ch] = (in[i] - (kQ_1[j] + kK[j]) * as1[ch] - as2[ch]) * kMul[j]; + float au = hp[ch] * kK[j]; + bp[ch] = au + as1[ch]; + as1[ch] = au + bp[ch]; + au = bp[ch] * kK[j]; + lp[ch] = au + as2[ch]; + as2[ch] = au + lp[ch]; + + hpo && (hpo[i] = hp[ch]); + bpo && (bpo[i] = bp[ch]); + lpo && (lpo[i] = lp[ch]); + + j += ch; + } + } +}; + +// 12 and 24 dB/oct +class FilterStereo { +public: + enum Type { + TYPE_NONE = 0, + TYPE_LP, + TYPE_BP, + TYPE_HP + }; + + enum Slope { + SLOPE_12 = 0, + SLOPE_24, + }; + + typedef struct { + float freq; + float res; + Type type; + Slope slope; + } Settings; + +private: + Settings const * settings; + SVF12Stereo fltA, fltB; + float freq, res; + +public: + void assign(Settings const * settings); + + inline void tick(float *in, float *out, size_t bufferSize, float *freqMod) { + nano_t started = nanos(); + + size_t frameCount = bufferSize / 2; + + float dt = 1.f / (float) frameCount; + + float kK[frameCount]; + float dFreq = dt * (settings->freq - freq); + for(size_t i = 0; i < frameCount; i++) { + kK[i] = fastCVtoK(freq + freqMod[i]); + freq += dFreq; + } + float kQ[frameCount]; + float dRes = dt * (settings->res - res); + for(size_t i = 0; i < frameCount; i++) { + kQ[i] = (float) M_SQRT1_2 + res; + res += dRes; + } + + switch(settings->type) { + case TYPE_NONE: + std::copy(in, in + bufferSize, out); + break; + + case TYPE_LP: + fltA.tick(in, NULL, NULL, out, bufferSize, kK, kQ); + if(settings->slope == SLOPE_24) { + fltB.tick(out, NULL, NULL, out, bufferSize, kK, kQ); + } + break; + case TYPE_BP: + fltA.tick(in, NULL, out, NULL, bufferSize, kK, kQ); + if(settings->slope == SLOPE_24) { + fltB.tick(out, NULL, out, NULL, bufferSize, kK, kQ); + } + break; + case TYPE_HP: + fltA.tick(in, out, NULL, NULL, bufferSize, kK, kQ); + if(settings->slope == SLOPE_24) { + fltB.tick(out, out, NULL, NULL, bufferSize, kK, kQ); + } + break; + } + + nano_t finished = nanos(); + perfNanos.filter += finished - started; + } +}; + +#endif \ No newline at end of file diff --git a/include/dsp/fm.h b/include/dsp/fm.h new file mode 100644 index 0000000..19ea385 --- /dev/null +++ b/include/dsp/fm.h @@ -0,0 +1,107 @@ +#ifndef __FM_H__ +#define __FM_H__ + +#include + +#include "../util.h" + +class FM { +private: + float phase1 = randPhase(), phase2 = randPhase(), phase3 = randPhase(), phase4 = randPhase(), phase5 = randPhase(), phase6 = randPhase(); + float fb1 = 0.f, fb2 = 0.f; + +public: + struct Settings { + struct { + float pitch; + float level; + } ops[6]; + }; + Settings settings; + + float tick(float pitch, float fmEnv) { + float step3 = cvToStep(pitch); + float step2 = cvToStep(pitch + 3.807354922057604f); // Frequency Coarse = 14 + float step1 = cvToStep(pitch + 0.007195501404204f); // Detune = +7 + float step6 = cvToStep(pitch); + float step5 = cvToStep(pitch - 0.007231569231076f); // Detune = -7 + float step4 = cvToStep(pitch); + + float osc3 = fmEnv * 0.333f * sinf(phase3); + float osc2 = fmEnv * 0.333f * sinf(phase2 + (float) M_PI * osc3); + float osc1 = sinf(phase1 + (float) M_PI * osc2); + + float osc6 = fmEnv * 1.0f * sinf(phase6 + 0.0f * (float) M_PI * (fb1 + fb2)); + float osc5 = fmEnv * 1.0f * sinf(phase5 + (float) M_PI * osc6); + float osc4 = sinf(phase4 + (float) M_PI * osc5); + fb2 = fb1; + fb1 = osc4; + + phase1 += step1; + if(phase1 >= (float) PIx2) { + phase1 -= PIx2; + } + phase2 += step2; + if(phase2 >= (float) PIx2) { + phase2 -= PIx2; + } + phase3 += step3; + if(phase3 >= (float) PIx2) { + phase3 -= PIx2; + } + phase4 += step4; + if(phase4 >= (float) PIx2) { + phase4 -= PIx2; + } + phase5 += step5; + if(phase5 >= (float) PIx2) { + phase5 -= PIx2; + } + phase6 += step6; + if(phase6 >= (float) PIx2) { + phase6 -= PIx2; + } + + return osc1 + osc4; + } +}; + +/* +op6.detune 14 +op6.output_level 79 +op6.frequency_coarse 1 + +op5.detune 0 +op5.output_level 99 +op5.frequency_coarse 1 + +op4.detune 7 +op4.output_level 89 +op4.frequency_coarse 1 + +op3.detune 7 +op3.output_level 99 +op3.frequency_coarse 1 + +op2.detune 7 +op2.output_level 58 +op2.frequency_coarse 14 + +op1.detune 10 +op1.output_level 99 +op1.frequency_coarse 1 + +algorithm 4 +feedback 6 [0..7] + +RANGES + +opx.output_level [0, 99] +opX.frequency_coarse [0, 31] 0.50 - 31.00 X +op6.frequency_fine [0, 99] 1.00 - 1.99 X +opX.detune [0, 14] [-7, +7] +feedback [0, 7] + +*/ + +#endif \ No newline at end of file diff --git a/include/dsp/frame.h b/include/dsp/frame.h new file mode 100644 index 0000000..0259c2b --- /dev/null +++ b/include/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/dsp/oscillator.h b/include/dsp/oscillator.h new file mode 100644 index 0000000..cfb9af3 --- /dev/null +++ b/include/dsp/oscillator.h @@ -0,0 +1,124 @@ +#ifndef __OSCILLATOR_H__ +#define __OSCILLATOR_H__ + +/* PolyBLEP oscillator inspired by: https://www.metafunction.co.uk/post/all-about-digital-oscillators-part-2-blits-bleps + * Further info about BLEP at: https://pbat.ch/sndkit/blep/ + */ + +#include +#include + +#include "../util.h" +#include "../luts.h" +#include "../perf.h" + +class Oscillator { +public: + enum Mode { MODE_SINE = 0, MODE_SAW, MODE_SQUARE }; + + Mode const * mode; + float phase; // current waveform phase angle (radians) + float value; // current amplitude value + float driftAmount; + float driftValue; + + Oscillator() : phase(randFrac()), value(0), driftAmount(0.001f), driftValue(0) {} + + void assign(Mode const * mode); + + // Generate next output sample and advance the phase angle + inline void tick(float* out, size_t bufferSize, float *pitch, float* gain) { + nano_t started = nanos(); + + size_t frameCount = bufferSize / 2; + + float driftBuf[frameCount]; + for(size_t i = 0; i < frameCount; ++i) { + driftValue += 0.005f * whiteNoise() - 0.00001f * driftValue; + driftBuf[i] = 1.0f + driftAmount * driftValue; + } + float phaseStepBuf[frameCount]; + float step = cvToStep(pitch[0]); + float stepEnd = cvToStep(pitch[1]); + float stepStep = (stepEnd - step) / frameCount; + for(size_t i = 0; i < frameCount; ++i) { + phaseStepBuf[i] = step * driftBuf[i]; + step += stepStep; + } + + float phaseBuf[frameCount]; + for(size_t i = 0; i < frameCount; ++i) { + phaseBuf[i] = phase; + phase += phaseStepBuf[i]; + if(unlikely(phase >= 1.f)) { + phase -= 1.f; + } + } + + if(*mode == MODE_SINE) { + for(size_t i = 0, j = 0; i < frameCount; ++i, j += 2) { + float value = fastSin(phaseBuf[i]); + + out[j] += gain[0] * value; + out[j + 1] += gain[1] * value; + } + } else if(*mode == MODE_SAW) { + for(size_t i = 0, j = 0; i < frameCount; ++i, j += 2) { + float value = (2.0f * phaseBuf[i]) - 1.0f; // Render naive waveshape + + value -= polyBlep(phaseBuf[i], phaseStepBuf[i]); // Layer output of Poly BLEP on top + + out[j] += gain[0] * value; + out[j + 1] += gain[1] * value; + } + } else if (*mode == MODE_SQUARE) { + float antiphaseBuf[frameCount]; + for(size_t i = 0; i < frameCount; ++i) { + float antiphase = phaseBuf[i] + 0.5f; + if(unlikely(antiphase >= 1.f)) { + antiphase -= 1; + } + antiphaseBuf[i] = antiphase; + } + + for(size_t i = 0, j = 0; i < frameCount; ++i, j += 2) { + float value = phaseBuf[i] < 0.5f ? 1.f : -1.f; // Render naive waveshape + + value += polyBlep(phaseBuf[i], phaseStepBuf[i]); // Layer output of Poly BLEP on top (flip) + value -= polyBlep(antiphaseBuf[i], phaseStepBuf[i]); // Layer output of Poly BLEP on top (flop) + + out[j] += gain[0] * value; + out[j + 1] += gain[1] * value; + } + } + + nano_t finished = nanos(); + perfNanos.oscillator += finished - started; + } + + inline float polyBlep(float t, float dt) { + // t-t^2/2 +1/2 + // 0 < t <= 1 + // discontinuities between 0 & 1 + if(unlikely(t < dt)) { + t /= dt; + return t + t - t * t - 1.0f; + } + + // t^2/2 +t +1/2 + // -1 <= t <= 0 + // discontinuities between -1 & 0 + else if(unlikely(t > 1.0f - dt)) { + t = (t - 1.0f) / dt; + return t * t + t + t + 1.0f; + } + + // no discontinuities + // 0 otherwise + else { + return 0.0; + } + } +}; + +#endif \ No newline at end of file diff --git a/include/dsp/phys.h b/include/dsp/phys.h new file mode 100644 index 0000000..b30e2ea --- /dev/null +++ b/include/dsp/phys.h @@ -0,0 +1,34 @@ +#ifndef __PHYS_H__ +#define __PHYS_H__ + +#include "../util.h" +#include "delayline.h" + +class Physical { +private: + DelayLine dl; + float lp = 0, lp2 = 0; + +public: + inline float tick(float pitch) { + int period = roundf(cvToPeriod(pitch)); + dl.size = period; + float out = dl.get(); + lp *= 0.99f; + lp += 0.90f * (out - lp); + dl.inject(lp, 0); + dl.tick(); + lp2 += 0.01f * (out - lp2); + return lp2; + } + + inline void pluck() { + float tmp[512]; + for(int i = 0; i < 512; ++i) { + tmp[i] = 8.f * (whiteNoise() + whiteNoise() + whiteNoise() + whiteNoise()); + } + dl.inject(tmp, 512); + } +}; + +#endif \ No newline at end of file diff --git a/include/dsp/reverb.h b/include/dsp/reverb.h new file mode 100644 index 0000000..372a284 --- /dev/null +++ b/include/dsp/reverb.h @@ -0,0 +1,53 @@ +#ifndef __REVERB_H__ +#define __REVERB_H__ + +// Velvet noise reverb ("Late Reverberation Synthesis Using Filtered Velvet Noise") + +#include + +#include "globals.h" +#include "frame.h" +#include "stereodelayline.h" + +#define REVERB_FRAMES (3 * SAMPLE_RATE) +#define REVERB_TAPS 256 + +class Reverb { +private: + struct Tap { + size_t lp, rp; + float lg, rg; + }; + + StereoDelayLine dl; + Tap taps[REVERB_TAPS]; + float lpfL = 0.f, lpfR = 0.f; + float hpfL = 0.f, hpfR = 0.f; + +public: + Reverb(); + + frame tick(frame in) { + dl.tick(in); + + frame out{0, 0}; + + for(const 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 out; + } +}; + +#endif \ No newline at end of file diff --git a/include/dsp/stereodelayline.h b/include/dsp/stereodelayline.h new file mode 100644 index 0000000..9d255ff --- /dev/null +++ b/include/dsp/stereodelayline.h @@ -0,0 +1,45 @@ +#ifndef __STEREODELAYLINE_H__ +#define __STEREODELAYLINE_H__ + +#include + +#include "frame.h" + +template +class StereoDelayLine { +private: + frame samples[CAPACITY] = {{0, 0}}; + size_t cursor; + +public: + size_t size; + + StereoDelayLine(size_t size = CAPACITY) : size(size) {} + + inline frame tick() { + ++cursor; + if(cursor == size) { + cursor = 0; + } + return samples[cursor]; + } + + 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/dsp/stereosap.h b/include/dsp/stereosap.h new file mode 100644 index 0000000..ba05e6f --- /dev/null +++ b/include/dsp/stereosap.h @@ -0,0 +1,28 @@ +#ifndef __SAP_H__ +#define __SAP_H__ + +// Schroeder All-Pass (delay w/feedback) + +#include + +#include "stereodelayline.h" + +template +class StereoSAP { +private: + const size_t size; + StereoDelayLine dl; + +public: + float gain = 0.7; + + StereoSAP(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/globals.h b/include/globals.h new file mode 100644 index 0000000..ff96702 --- /dev/null +++ b/include/globals.h @@ -0,0 +1,9 @@ +#ifndef __GLOBALS_H__ +#define __GLOBALS_H__ + +#define SAMPLE_RATE 96000 +#define SAMPLE_RATE_INV (1.0 / SAMPLE_RATE) + +#define FILTER_CV_MAX 9.25 // MIDI note "135" (19912.126958213178287 Hz) + +#endif \ No newline at end of file diff --git a/include/luts.h b/include/luts.h new file mode 100644 index 0000000..4720520 --- /dev/null +++ b/include/luts.h @@ -0,0 +1,26 @@ +#ifndef __LUTS_H__ +#define __LUTS_H__ + +#include + +#include "globals.h" +#include "util.h" + +#define SIN_LUT_SIZE 256 +#define K_LUT_SIZE 256 + +extern float sinLUT[SIN_LUT_SIZE]; +extern float kLUT[K_LUT_SIZE]; + +void genLUTs(); + +inline float fastSin(float t) { + t += randFrac() * (1.f / SIN_LUT_SIZE); + return sinLUT[(size_t) (t * SIN_LUT_SIZE) & (SIN_LUT_SIZE - 1)]; +} + +inline float fastCVtoK(float cv) { + return kLUT[std::min(K_LUT_SIZE - (size_t) 1, (size_t) (cv * (K_LUT_SIZE - 1) * (1.f / (float) FILTER_CV_MAX)))]; +} + +#endif \ No newline at end of file diff --git a/include/perf.h b/include/perf.h new file mode 100644 index 0000000..cd3ed9c --- /dev/null +++ b/include/perf.h @@ -0,0 +1,37 @@ +#ifndef __PERF_H__ +#define __PERF_H__ + +#include +#include + +#if defined linux || __APPLE__ || _WIN32 +typedef std::chrono::nanoseconds::rep nano_t; +#else +typedef uint32_t nano_t; +#endif + +typedef struct { + nano_t total; + nano_t channel; + nano_t voice; + nano_t oscillator; + nano_t filter; +} PerfNanos; + +extern PerfNanos perfNanos; + +// Get timestamp in nanoseconds +#if defined linux || __APPLE__ || _WIN32 +inline nano_t nanos() { + return std::chrono::duration_cast( + std::chrono::high_resolution_clock::now().time_since_epoch()) + .count(); +} +#else +inline nano_t nanos() { + // SysTick * 100 / 55 + return 0; +} +#endif + +#endif \ No newline at end of file diff --git a/include/preset.h b/include/preset.h new file mode 100644 index 0000000..d843621 --- /dev/null +++ b/include/preset.h @@ -0,0 +1,47 @@ +#ifndef __PRESET_H__ +#define __PRESET_H__ + +#include "dsp/oscillator.h" +#include "dsp/filter.h" +#include "dsp/adsr.h" + +struct Preset { + typedef struct { + uint8_t attack; + uint8_t decay; + uint8_t sustain; + uint8_t release; + } Envelope; + + uint8_t unison; + + uint8_t osc1Mode; + uint8_t osc2Mode; + uint8_t oscMix; + uint8_t oscDetune; + uint8_t osc2Pitch; + + uint8_t noiseMix; + + struct { + uint8_t type; + uint8_t slope; + uint8_t freq; + uint8_t Q; + } filter; + + Envelope ampEnv; + Envelope modEnv; + + uint8_t modEnvFltGain; + uint8_t keyTrack; + uint8_t lfoFreq; + uint8_t lfoPitchMod; + uint8_t lfoFltMod; + + uint8_t reverb; +}; + +extern const Preset DEFAULT_PRESET; + +#endif \ No newline at end of file diff --git a/include/synth.h b/include/synth.h new file mode 100644 index 0000000..a569925 --- /dev/null +++ b/include/synth.h @@ -0,0 +1,73 @@ +#ifndef __SYNTH_H__ +#define __SYNTH_H__ + +#include "voicemanager.h" +#include "channel.h" +#include "dsp/frame.h" +#include "dsp/reverb.h" +#include "luts.h" +#include "perf.h" + +class Synth { +public: + Synth() { + genLUTs(); + + for(VoiceManager::channel_t i = 0; i < 16; ++i) { + channels[i].number = i; + channels[i].voiceManager = &voiceManager; + channels[i].loadPreset(&DEFAULT_PRESET); + } + } + + void noteOn(int ch, int note, int vel); + void noteOff(int ch, int note); + void control(int ch, int cc, int val); + + inline void tick(float* out, const size_t bufferSize) { + nano_t started = nanos(); + + std::fill(out, out + bufferSize, 0.f); + + float reverbBus[bufferSize]; + std::fill(reverbBus, reverbBus + bufferSize, 0.f); + + for(auto& channel : channels) { + float chOut[bufferSize]; + if(!channel.tick(chOut, bufferSize)) { + continue; + } + + for(size_t i = 0; i < bufferSize; ++i) { + out[i] += chOut[i]; + } + + for(size_t i = 0; i < bufferSize; ++i) { + reverbBus[i] += channel.settings.reverb * chOut[i]; + } + } + + /* + for(size_t i = 0; i < bufferSize; i += 2) { + frame f = {reverbBus[i], reverbBus[i + 1]}; + f = reverb.tick(f); + out[i] += f.l; + out[i + 1] += f.r; + } + */ + + for(size_t i = 0; i < bufferSize; ++i) { + out[i] *= 0.125f; + } + + nano_t finished = nanos(); + perfNanos.total += finished - started; + } + +private: + VoiceManager voiceManager{}; + Channel channels[16]; + Reverb reverb; +}; + +#endif \ No newline at end of file diff --git a/include/util.h b/include/util.h new file mode 100644 index 0000000..7c16ae0 --- /dev/null +++ b/include/util.h @@ -0,0 +1,69 @@ +#ifndef __UTIL_H__ +#define __UTIL_H__ + +#include +#include + +#include "globals.h" + +#define unlikely(cond) __builtin_expect((cond), 0) +#define likely(cond) __builtin_expect((cond), 1) + +#define PIx2 (2 * M_PI) +#define PIx2_INV (1.0 / PIx2) + +#define CV_FREQ_MIN 32.703195662574829 // MIDI note 24 (C1) +#define MAX_SAMPLE_PERIOD ((int) (SAMPLE_RATE / CV_FREQ_MIN + 0.5)) + +inline float fakeSin(float x) { + x = 4.f * (x - 0.5f); + return x * (2.f - fabsf(x)); +} + +inline float clamp(float x, float a, float b) { + return fminf(b, fmaxf(a, x)); +} + +inline float whiteNoise() { + static int32_t x1 = 0x70F4F854; + static int32_t x2 = 0xE1E9F0A7; + + x1 ^= x2; + float value = x2 * (2.0f / 0xFFFFFFFF); + x2 += x1; + + return value; +} + +inline float randFrac() { + static uint32_t x1 = 0x70F4F854; + static uint32_t x2 = 0xE1E9F0A7; + + x1 ^= x2; + float value = x2 * (1.f / 0x100000000); + x2 += x1; + + return value; +} + +inline float triangle(float phase) { + if(phase < 0.5f) { + return 2.f * phase - 1; + } else { + return 1 - 2.f * (phase - 0.5f); + } +} + +inline float cvToStep(float cv) { + return (float) CV_FREQ_MIN * exp2f(cv) * (float) SAMPLE_RATE_INV; +} + +inline float noteToCV(float note) { + return note / 12.f; +} + +inline float cvToPeriod(float cv) { + return (float) SAMPLE_RATE / ((float) CV_FREQ_MIN * exp2f(cv)); +} + +#endif \ No newline at end of file diff --git a/include/voice.h b/include/voice.h new file mode 100644 index 0000000..b934888 --- /dev/null +++ b/include/voice.h @@ -0,0 +1,135 @@ +#ifndef __VOICE_H__ +#define __VOICE_H__ + +#include "globals.h" +#include "preset.h" + +#include "dsp/oscillator.h" +#include "dsp/filter.h" +#include "dsp/adsr.h" +#include "perf.h" + +#define MAX_OSCS 8 + +class Voice { +public: + typedef struct { + size_t unison; + + Oscillator::Mode osc1Mode; + Oscillator::Mode osc2Mode; + float oscMix; + float oscDetune; + float osc2Pitch; + + float noiseMix; + + FilterStereo::Settings filter; + + ADSR::Envelope ampEnv; + ADSR::Envelope modEnv; + + float modEnvFltGain; + float keyTrack; + float lfoStep; + float lfoPitchMod; + float lfoFltMod; + + float reverb; + } Settings; + +private: + Settings const * settings; + + ADSR adsrAmp; + ADSR adsrMod; + Oscillator osc[MAX_OSCS]; + FilterStereo filter; + +public: + int note; + float velocity; + float gain; + + void reset(); + void assign(Settings const * settings); + void noteOn(int note, float velocity = 1, float gain = 1.0f); + void noteOff(); + inline bool isIdle() { return adsrAmp.isIdle(); } + + inline void tick(float *out, const size_t bufferSize, float *pitchMod, float *fltMod) { + nano_t started = nanos(); + + const size_t frameCount = bufferSize / 2; + + float basePitchBuf[2]; + basePitchBuf[0] = note + pitchMod[0]; + basePitchBuf[1] = note + pitchMod[frameCount - 1]; + + float oscPitchBuf[MAX_OSCS][2]; + float detune = -settings->oscDetune * 0.5f * (settings->unison - 1); + for(size_t i = 0; i < settings->unison; ++i) { + float transpose = (i & 1) ? settings->osc2Pitch : 0; + oscPitchBuf[i][0] = noteToCV(basePitchBuf[0] + detune + transpose); + oscPitchBuf[i][1] = noteToCV(basePitchBuf[1] + detune + transpose); + detune += settings->oscDetune; + } + + float pan, panStep; + if(settings->unison > 2) { + pan = 0.f; + panStep = 2.f / (settings->unison - 1); + } else { + pan = 0.5f; + panStep = 0.f; + } + + float oscBuf[bufferSize]; + std::fill(oscBuf, oscBuf + bufferSize, 0.f); + for(size_t i = 0; i < settings->unison; ++i) { + float baseGain = settings->unison > 1 ? ((i & 1) ? settings->oscMix : (1 - settings->oscMix)) : 1.f; + float gain[2]; + gain[0] = baseGain * sinf((1.f - pan) * (float) M_PI_2); + gain[1] = baseGain * sinf(pan * (float) M_PI_2); + pan += (i & 1) * panStep; + osc[i].tick(oscBuf, bufferSize, oscPitchBuf[i], gain); + } + + float noiseBuf[bufferSize]; + if(settings->unison > 2) { // Stereo noise + for(size_t i = 0; i < bufferSize; ++i) { + noiseBuf[i] = (1.f / 3.f) * (whiteNoise() + whiteNoise() + whiteNoise()); + } + } else { // Mono noise + for(size_t i = 0; i < bufferSize; i += 2 ) { + float noise = (1.f / 3.f) * (whiteNoise() + whiteNoise() + whiteNoise()); + noiseBuf[i] = noise; + noiseBuf[i + 1] = noise; + } + } + + for(size_t i = 0; i < bufferSize; ++i) { + oscBuf[i] += settings->noiseMix * (noiseBuf[i] - oscBuf[i]); + } + + float keyTrack = settings->keyTrack * (basePitchBuf[0] - 24) * (1.f / 12.f); + float keyTrackEnd = settings->keyTrack * (basePitchBuf[1] - 24) * (1.f / 12.f); + float keyTrackDelta = (keyTrackEnd - keyTrack) / frameCount; + + float fltModBuf[frameCount]; + for(size_t i = 0; i < frameCount; ++i) { + fltModBuf[i] = settings->modEnvFltGain * adsrMod.tick() + keyTrack + fltMod[i]; + keyTrack += keyTrackDelta; + } + + filter.tick(oscBuf, out, bufferSize, fltModBuf); + for(size_t i = 0; i < bufferSize; ++i) { + out[i] *= gain * adsrAmp.tick(); + } + + nano_t finished = nanos(); + perfNanos.voice += finished - started; + } +}; + +#endif \ No newline at end of file diff --git a/include/voicemanager.h b/include/voicemanager.h new file mode 100644 index 0000000..a3a113e --- /dev/null +++ b/include/voicemanager.h @@ -0,0 +1,34 @@ +#ifndef __VOICEMANAGER_H__ +#define __VOICEMANAGER_H__ + +#include + +#include "voice.h" + +#define NUM_VOICES 16 + +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; + Voice voice; + }; + + unsigned int serial; + VoiceData voices[NUM_VOICES]; + + unsigned int scoreVoice(VoiceData& voiceData); +}; + +#endif \ No newline at end of file diff --git a/src/channel.cpp b/src/channel.cpp new file mode 100644 index 0000000..37c15ca --- /dev/null +++ b/src/channel.cpp @@ -0,0 +1,198 @@ +#include + +#include "globals.h" +#include "cc.h" + +#include "channel.h" + +inline int midiToNote(int note) { + return note - 24; +} + +inline float timeToStep(float t) { + return (float) SAMPLE_RATE_INV / t; +} + +inline float ccToA(int x) { + return 5.0 * exp(9.2 * (x / 127.0) - 9.2); +} + +inline float ccToDR(int x) { + return 5.0 * exp(7.0 * (x / 127.0) - 7.0); +} + +inline float ccToLFOStep(int x) { + return (float) 0.1f * expf((1 + x) * 6.907755278982137f / 128.0f) * (float) SAMPLE_RATE_INV; +} + +inline float ccToLFOPitchMod(int x) { + return 6.f * x * x / 16129.f; +} + +inline float ccToLFOFltMod(int x) { + return (float) FILTER_CV_MAX * (x - 64) / 126.f; +} + +void Channel::loadPreset(const Preset * preset) { + settings.unison = 2 + 2 * floorf(preset->unison * 4.f / 128.f); + + settings.osc1Mode = Oscillator::Mode(preset->osc1Mode); + settings.osc2Mode = Oscillator::Mode(preset->osc2Mode); + settings.oscDetune = preset->oscDetune / 254.f; + settings.osc2Pitch = floorf(preset->osc2Pitch * 13.f / 128.f); + settings.oscMix = preset->oscMix / 127.f; + settings.noiseMix = preset->noiseMix / 127.f; + + settings.filter.type = FilterStereo::Type(floorf(4.f * preset->filter.type / 128.f)); + settings.filter.slope = FilterStereo::Slope(floorf(2.f * preset->filter.slope / 128.f)); + 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.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.f; + settings.modEnv.releaseStep = timeToStep(ccToDR(preset->modEnv.release)); + + 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 = ccToLFOFltMod(preset->lfoFltMod); + + settings.reverb = preset->reverb / 127.f; +} + +void Channel::noteOn(int note, int velocity) { + //printf("on: midi=%d note=%d\n", note, midiToNote(note)); + Voice* const voice = voiceManager->get(number); + voice->assign(&settings); + voice->noteOn(midiToNote(note), velocity / 127.f, 1.f); +} + +void Channel::noteOff(int note) { + Voice** voices = voiceManager->getChannelVoices(number); + for(Voice** voice = voices; *voice != NULL; ++voice) { + if((*voice)->note == midiToNote(note)) { + //printf("off: midi=%d note=%d\n", note, midiToNote(note)); + (*voice)->noteOff(); + } + } +} + +void Channel::control(int code, int value) { + switch(code) { + case CC_VOLUME: // Volume (Standard MIDI) + volume = value / 127.f; + break; + + case CC_FLT_ATK: // Filter Attack Time + settings.modEnv.attackStep = timeToStep(ccToA(value)); + break; + + case CC_FLT_DEC: // Filter Decay Time + settings.modEnv.decayStep = timeToStep(ccToDR(value)); + break; + + case CC_FLT_SUS: // Filter Sustain + settings.modEnv.sustain = value / 127.f; + break; + + case CC_FLT_REL: // Filter Release Time + settings.modEnv.releaseStep = timeToStep(ccToDR(value)); + break; + + case CC_FLT_Q: // Timbre / Harmonic Content (Standard MIDI) + settings.filter.res = value / 31.75f; + break; + + case CC_AMP_REL: // Release Time (Standard MIDI) + settings.ampEnv.releaseStep = timeToStep(ccToDR(value)); + break; + + case CC_AMP_ATK: // Attack Time (Standard MIDI) + settings.ampEnv.attackStep = timeToStep(ccToA(value)); + break; + + case CC_FLT_FRQ: // Brightness (Standard MIDI) + settings.filter.freq = (float) FILTER_CV_MAX * value / 127.f; + break; + + case CC_AMP_DEC: // Decay Time + settings.ampEnv.decayStep = timeToStep(ccToDR(value)); + break; + + case CC_AMP_SUS: // Sustain + settings.ampEnv.sustain = value / 127.f; + break; + + case CC_OSC_DET: // Detune (Standard MIDI) + settings.oscDetune = value / 254.f; + break; + + case CC_OSC2PIT: // Oscillator 2 Pitch + settings.osc2Pitch = floorf(value * (13.f / 128.f)); + break; + + case CC_MOD_FLT: // Mod Envelope Filter Gain + settings.modEnvFltGain = (float) FILTER_CV_MAX * value / 127.f; + break; + + case CC_KEY_TRK: // Filter Key Tracking + settings.keyTrack = value / 127.f; + break; + + case CC_LFO_FRQ: // LFO Frequency + settings.lfoStep = ccToLFOStep(value); + break; + + case CC_LFO_PIT: // LFO Pitch Modulation + settings.lfoPitchMod = ccToLFOPitchMod(value); + break; + + case CC_LFO_FLT: // LFO Filter Modulation + settings.lfoFltMod = ccToLFOFltMod(value); + break; + + case CC_UNISON: // Unison Amount + settings.unison = 2 + 2 * 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 = FilterStereo::Type(floorf(4.f * value / 128.f)); + break; + + case CC_FLT_SLP: // Filter Slope + settings.filter.slope = FilterStereo::Slope(floorf(2.f * value / 128.f)); + break; + + case CC_RVB_SND: + settings.reverb = value / 127.f; + break; + + case CC_NOI_MIX: + settings.noiseMix = value / 127.f; + break; + + // LFO delay + // Reverb Time + // Reverb Brightness + // Reverb Density + } +} \ No newline at end of file diff --git a/src/dsp/adsr.cpp b/src/dsp/adsr.cpp new file mode 100644 index 0000000..38109a4 --- /dev/null +++ b/src/dsp/adsr.cpp @@ -0,0 +1,22 @@ +#include "dsp/adsr.h" + +void ADSR::reset() { + state = IDLE; + t = 0; +} + +void ADSR::assign(Envelope const * env) { + this->env = env; +} + +void ADSR::noteOn() { + state = ATTACK; + t = 0; +} + +void ADSR::noteOff() { + if(state != RELEASE) { + state = RELEASE; + t = 1; + } +} \ No newline at end of file diff --git a/src/dsp/filter.cpp b/src/dsp/filter.cpp new file mode 100644 index 0000000..6d7aa41 --- /dev/null +++ b/src/dsp/filter.cpp @@ -0,0 +1,7 @@ +#include "dsp/filter.h" + +void FilterStereo::assign(Settings const * settings) { + this->settings = settings; + freq = settings->freq; + res = settings->res; +} \ No newline at end of file diff --git a/src/dsp/oscillator.cpp b/src/dsp/oscillator.cpp new file mode 100644 index 0000000..c58930c --- /dev/null +++ b/src/dsp/oscillator.cpp @@ -0,0 +1,5 @@ +#include "dsp/oscillator.h" + +void Oscillator::assign(Mode const * mode) { + this->mode = mode; +} \ No newline at end of file diff --git a/src/dsp/reverb.cpp b/src/dsp/reverb.cpp new file mode 100644 index 0000000..a3de5ba --- /dev/null +++ b/src/dsp/reverb.cpp @@ -0,0 +1,16 @@ +#include + +#include "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/luts.cpp b/src/luts.cpp new file mode 100644 index 0000000..c43c962 --- /dev/null +++ b/src/luts.cpp @@ -0,0 +1,19 @@ +#include +//#include + +#include "luts.h" + +#include "dsp/filter.h" + +float sinLUT[SIN_LUT_SIZE]; +float kLUT[K_LUT_SIZE]; + +void genLUTs() { + for(size_t i = 0; i < SIN_LUT_SIZE; ++i) { + sinLUT[i] = sin((i * 2 * M_PI) / SIN_LUT_SIZE); + } + + for(size_t i = 0; i < K_LUT_SIZE; ++i) { + kLUT[i] = cvToK(i * FILTER_CV_MAX / (K_LUT_SIZE - 1)); + } +} \ No newline at end of file diff --git a/src/perf.cpp b/src/perf.cpp new file mode 100644 index 0000000..2e38be7 --- /dev/null +++ b/src/perf.cpp @@ -0,0 +1,3 @@ +#include "perf.h" + +PerfNanos perfNanos; \ No newline at end of file diff --git a/src/preset.cpp b/src/preset.cpp new file mode 100644 index 0000000..c9059ba --- /dev/null +++ b/src/preset.cpp @@ -0,0 +1,42 @@ +#include "preset.h" + +const Preset DEFAULT_PRESET = { + .unison = 1, + + .osc1Mode = Oscillator::MODE_SAW, + .osc2Mode = Oscillator::MODE_SAW, + .oscMix = 64, + .oscDetune = 6, + .osc2Pitch = 0, + + .noiseMix = 0, + + .filter = { + .type = 0, + .slope = 22, + .freq = 32, + .Q = 25, + }, + + .ampEnv = { + .attack = 0, + .decay = 56, + .sustain = 64, + .release = 56 + }, + + .modEnv = { + .attack = 0, + .decay = 106, + .sustain = 0, + .release = 106 + }, + + .modEnvFltGain = 56, + .keyTrack = 127, + .lfoFreq = 64, + .lfoPitchMod = 0, + .lfoFltMod = 64, + + .reverb = 16 +}; \ No newline at end of file diff --git a/src/synth.cpp b/src/synth.cpp new file mode 100644 index 0000000..3aa2510 --- /dev/null +++ b/src/synth.cpp @@ -0,0 +1,18 @@ +//#include + +#include "synth.h" + +void Synth::noteOn(int ch, int note, int vel) { + //printf("Note On: ch=%d note=%d vel=%d\n", ch, note, vel); + channels[ch].noteOn(note, vel); +} + +void Synth::noteOff(int ch, int note) { + //printf("Note Off: ch=%d note=%d\n", ch, note); + channels[ch].noteOff(note); +} + +void Synth::control(int ch, int cc, int val) { + //printf("Controller: ch=%d cc=%d val=%d\n", ch, cc, val); + channels[ch].control(cc, val); +} \ No newline at end of file diff --git a/src/voice.cpp b/src/voice.cpp new file mode 100644 index 0000000..94100d0 --- /dev/null +++ b/src/voice.cpp @@ -0,0 +1,33 @@ +#include "voice.h" + +void Voice::reset() { + adsrAmp.reset(); + adsrMod.reset(); +} + +void Voice::assign(Settings const * settings) { + this->settings = settings; + adsrAmp.assign(&settings->ampEnv); + adsrMod.assign(&settings->modEnv); + for(size_t i = 0; i < MAX_OSCS; ++i) { + if(i & 1) { + osc[i].assign(&settings->osc2Mode); + } else { + osc[i].assign(&settings->osc1Mode); + } + } + filter.assign(&settings->filter); +} + +void Voice::noteOn(int note, float velocity, float gain) { + this->note = note; + this->velocity = velocity; + this->gain = gain; + adsrAmp.noteOn(); + adsrMod.noteOn(); +} + +void Voice::noteOff() { + adsrAmp.noteOff(); + adsrMod.noteOff(); +} \ No newline at end of file diff --git a/src/voicemanager.cpp b/src/voicemanager.cpp new file mode 100644 index 0000000..f71ab88 --- /dev/null +++ b/src/voicemanager.cpp @@ -0,0 +1,71 @@ +//#include +#include + +#include "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; + vd.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), [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]; + if(!voiceData.voice.isIdle()) { + //printf("Out of voices\n"); + } + voiceData.channel = channel; + voiceData.serial = serial++; + voiceData.voice.reset(); + + 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.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; +}