Initial architecture for parts, presets and voices

main
Thor 1 year ago
parent 5f2086e829
commit 6cd5e452f5
  1. 22
      .vscode/c_cpp_properties.json
  2. 3
      CMakeLists.txt
  3. 26
      include/synth/dsp/adsr.h
  4. 88
      include/synth/dsp/filter.h
  5. 31
      include/synth/dsp/oscillator.h
  6. 117
      include/synth/dsp/svf.h
  7. 6
      include/synth/globals.h
  8. 33
      include/synth/part.h
  9. 51
      include/synth/preset.h
  10. 22
      include/synth/synth.h
  11. 37
      include/synth/voice.h
  12. 10
      include/synth/voicemanager.h
  13. 19
      src/synth/dsp/adsr.cpp
  14. 44
      src/synth/part.cpp
  15. 6
      src/synth/synth.cpp
  16. 2
      src/synthapp.cpp
  17. 26
      src/synthframe.cpp

@ -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
}

@ -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)

@ -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) {

@ -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 <math.h>
#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

@ -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;

@ -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 <math.h>
#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

@ -0,0 +1,6 @@
#ifndef __GLOBALS_H__
#define __GLOBALS_H__
#define SAMPLE_RATE 48000
#endif

@ -0,0 +1,33 @@
#ifndef __PART_H__
#define __PART_H__
#include <map>
#include <unordered_set>
#include "voicemanager.h"
#include "voice.h"
#include "preset.h"
class Part {
public:
VoiceManager* voiceManager;
Voice::Settings settings;
float pitchBend;
float modulation;
std::map<uint8_t, Voice*> 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

@ -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

@ -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

@ -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

@ -0,0 +1,10 @@
#ifndef __VOICEMANAGER_H__
#define __VOICEMANAGER_H__
class VoiceManager {
public:
private:
};
#endif

@ -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;
}
}

@ -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) {
}

@ -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) {
}

@ -4,6 +4,8 @@
#include <porttime.h>
#include "synth/globals.h"
#include "synthapp.h"
#include "synthframe.h"

@ -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)

Loading…
Cancel
Save