From 814002f0d912d2bdd97ae049dd9aff2b44612c7d Mon Sep 17 00:00:00 2001 From: Blitblank Date: Sat, 27 Dec 2025 14:30:49 -0600 Subject: [PATCH] added waveform selectors --- src/ParameterStore.h | 35 +-- src/synth/Envelope.cpp | 3 +- src/synth/ScopeBuffer.h | 3 + src/synth/Synth.cpp | 22 +- src/ui/MainWindow.cpp | 33 ++- src/ui/MainWindow.ui | 209 +++++++++++++++++- .../EnvelopeGenerator/EnvelopeGenerator.ui | 68 +++--- src/ui/widgets/Scope/Scope.cpp | 2 +- src/ui/widgets/SmartSlider/SmartSlider.ui | 58 +++-- 9 files changed, 354 insertions(+), 79 deletions(-) diff --git a/src/ParameterStore.h b/src/ParameterStore.h index ac90caa..c04308e 100644 --- a/src/ParameterStore.h +++ b/src/ParameterStore.h @@ -20,6 +20,8 @@ enum class ParamId : uint16_t { FilterResonanceEnvD, FilterResonanceEnvS, FilterResonanceEnvR, + Osc1WaveSelector1, + Osc1WaveSelector2, // ... and so on // this list could be like 200 long if I really wanted to Count // for sizing @@ -47,7 +49,6 @@ constexpr std::array(EnvelopeId::Count)> ENV_ { ParamId::Osc1VolumeEnvA, ParamId::Osc1VolumeEnvD, ParamId::Osc1VolumeEnvS, ParamId::Osc1VolumeEnvR }, // Osc3Volume (not implemented) { ParamId::FilterCutoffEnvA, ParamId::FilterCutoffEnvR, ParamId::FilterCutoffEnvS, ParamId::FilterCutoffEnvR }, // FilterCutoff { ParamId::FilterResonanceEnvA, ParamId::FilterResonanceEnvR, ParamId::FilterResonanceEnvS, ParamId::FilterResonanceEnvR }, // FilterResonance - }}; struct ParamDefault { @@ -59,20 +60,22 @@ struct ParamDefault { // TODO: make these configurable via yml file too // later TODO: and then when I have full on profile saving there will be a default profile to load from constexpr std::array(ParamId::Count)> PARAM_DEFS {{ - { 100.0f, 20.0f, 600.0f}, // Osc1Freq - { 0.8f, 0.0f, 1.0f}, // Osc1Gain - { 0.05f, 0.0f, 2.0f}, // Osc1VolumeEnvA, - { 0.2f, 0.0f, 2.0f}, // Osc1VolumeEnvD, - { 0.7f, 0.0f, 1.0f}, // Osc1VolumeEnvS, - { 0.2f, 0.0f, 2.0f}, // Osc1VolumeEnvR, - { 10.0f, 0.0f, 1000.0f}, // FilterCutoffEnvA, - { 10.0f, 0.0f, 1000.0f}, // FilterCutoffEnvD, - { 10.0f, 0.0f, 1000.0f}, // FilterCutoffEnvS, - { 10.0f, 0.0f, 1000.0f}, // FilterCutoffEnvR, - { 10.0f, 0.0f, 1000.0f}, // FilterResonanceEnvA, - { 10.0f, 0.0f, 1000.0f}, // FilterResonanceEnvD, - { 10.0f, 0.0f, 1000.0f}, // FilterResonanceEnvS, - { 10.0f, 0.0f, 1000.0f}, // FilterResonanceEnvR, + { 100.0f, 20.0f, 600.0f}, // Osc1Freq + { 0.8f, 0.0f, 1.0f}, // Osc1Gain + { 0.05f, 0.0f, 2.0f}, // Osc1VolumeEnvA + { 0.2f, 0.0f, 2.0f}, // Osc1VolumeEnvD + { 0.7f, 0.0f, 1.0f}, // Osc1VolumeEnvS + { 0.2f, 0.0f, 2.0f}, // Osc1VolumeEnvR + { 10.0f, 0.0f, 1000.0f}, // FilterCutoffEnvA + { 10.0f, 0.0f, 1000.0f}, // FilterCutoffEnvD + { 10.0f, 0.0f, 1000.0f}, // FilterCutoffEnvS + { 10.0f, 0.0f, 1000.0f}, // FilterCutoffEnvR + { 10.0f, 0.0f, 1000.0f}, // FilterResonanceEnvA + { 10.0f, 0.0f, 1000.0f}, // FilterResonanceEnvD + { 10.0f, 0.0f, 1000.0f}, // FilterResonanceEnvS + { 10.0f, 0.0f, 1000.0f}, // FilterResonanceEnvR + { 1.0f, 0.0f, 0.0f}, // Osc1WaveSelector1 + { 1.0f, 0.0f, 0.0f}, // Osc1WaveSelector2 }}; constexpr size_t PARAM_COUNT = static_cast(ParamId::Count); @@ -85,8 +88,10 @@ public: ~ParameterStore() = default; void set(ParamId id, float value); + void set(ParamId id, int32_t value) { set(id, static_cast(value)); } void set(EnvelopeId, float a, float d, float s, float r); float get(ParamId id) const; + int32_t getInt(ParamId id) const { return static_cast(get(id)); } void resetToDefaults(); private: diff --git a/src/synth/Envelope.cpp b/src/synth/Envelope.cpp index 0d9d2c7..5db79b0 100644 --- a/src/synth/Envelope.cpp +++ b/src/synth/Envelope.cpp @@ -7,6 +7,7 @@ Envelope::Envelope() { void Envelope::noteOn() { state_ = State::Attack; + // there's interntionally no envelope value_ reset here because slurs } void Envelope::noteOff() { @@ -26,7 +27,7 @@ float Envelope::process() { state_ = State::Decay; } break; - case State::Decay: // TODO: if noteOff occurs during decay, release doesn't trigger until decay finishes + case State::Decay: value_ -= (1.0f - sustain_) / (decay_ * sampleRate_); if(value_ <= sustain_) { value_ = sustain_; diff --git a/src/synth/ScopeBuffer.h b/src/synth/ScopeBuffer.h index 988ebb5..571b9f5 100644 --- a/src/synth/ScopeBuffer.h +++ b/src/synth/ScopeBuffer.h @@ -4,6 +4,8 @@ #include #include +// the scope buffer is used by the ui to visualize the audio waveform +// the ui thread shouldn't read directly from memory being read from/written to so it copies the data into this class class ScopeBuffer { public: @@ -22,6 +24,7 @@ public: // NOTE: there are limits to the wavelengths that the scope can show cleanly due to the size of the audio buffer // at a buffer size of 256 at 44100hz the min visible steady frequency is ~172hz + // the min visible steady frequency can be lowered by increasing buffer size (increases latency) or decreasing sample rate (decreases audio fidelity) private: std::vector buffer_; diff --git a/src/synth/Synth.cpp b/src/synth/Synth.cpp index 2dc8d69..09a6b94 100644 --- a/src/synth/Synth.cpp +++ b/src/synth/Synth.cpp @@ -94,8 +94,25 @@ void Synth::process(float* out, uint32_t nFrames, uint32_t sampleRate) { // TODO: wavetables should be scaled by their RMS for equal loudness (prelim standard = 0.707) float sineSample = std::sin(phase_); float squareSample = (phase_ >= M_PI) ? 0.707f : -0.707f; - float sawSample = ((phase_ / M_PI) - 1.0f) / 0.577f * 0.707f; - sampleOut = squareSample * gain; + float sawSample = ((phase_ / M_PI) - 1.0f) / 0.577f * 0.707f; + // switch statement will be replaced with an array index for our array of wavetables + switch (static_cast(std::round(getParam(ParamId::Osc1WaveSelector1)))) { + case 0: + sampleOut = sineSample * gain; + break; + case 1: + sampleOut = squareSample * gain; + break; + case 2: + sampleOut = sawSample * gain; + break; + case 3: + // TODO: no triable wave yet :( + sampleOut = sineSample * gain; + break; + default: // unreachable + break; + } // write to buffer out[2*i] = sampleOut; // left @@ -113,6 +130,7 @@ void Synth::process(float* out, uint32_t nFrames, uint32_t sampleRate) { if(!triggered) { scope_->setTrigger(i); // this is where we consider the start of a waveform triggered = true; + // TODO: investigate triggering accross buffers when a single wave period transcends a single audio buffer } } } diff --git a/src/ui/MainWindow.cpp b/src/ui/MainWindow.cpp index 2b0c407..0ee8678 100644 --- a/src/ui/MainWindow.cpp +++ b/src/ui/MainWindow.cpp @@ -20,14 +20,32 @@ MainWindow::MainWindow(QWidget *parent) : // Connect buttons to slots connect(ui_->buttonReset, &QPushButton::clicked, this, &MainWindow::onResetClicked); + onResetClicked(); // manually reset - // connect envelopeGenerator - ui_->envelopeOsc1Volume->init(EnvelopeId::Osc1Volume); + // connect envelopeGenerators connect(ui_->envelopeOsc1Volume, &EnvelopeGenerator::envelopeChanged, this, [this](float a, float d, float s, float r) { audio_->parameters()->set(EnvelopeId::Osc1Volume, a, d, s, r); }); - // this should be easy enough to put into a for each envelopeGenerator loop + connect(ui_->envelopeFilterCutoff, &EnvelopeGenerator::envelopeChanged, + this, [this](float a, float d, float s, float r) { + audio_->parameters()->set(EnvelopeId::FilterCutoff, a, d, s, r); + }); + connect(ui_->envelopeFilterResonance, &EnvelopeGenerator::envelopeChanged, + this, [this](float a, float d, float s, float r) { + audio_->parameters()->set(EnvelopeId::FilterResonance, a, d, s, r); + }); + // this should be easy enough to put into a for each envelopeGenerator loop, each ui element just needs an EnvelopeId specifiers + + // other ui elements + connect(ui_->comboOsc1WaveSelector1, QOverload::of(&QComboBox::currentIndexChanged), + this, [this](int index) { + audio_->parameters()->set(ParamId::Osc1WaveSelector1, index); + }); + connect(ui_->comboOsc1WaveSelector2, QOverload::of(&QComboBox::currentIndexChanged), + this, [this](int index) { + audio_->parameters()->set(ParamId::Osc1WaveSelector2, index); + }); // synth business audio_->start(); @@ -49,5 +67,14 @@ void MainWindow::keyReleaseEvent(QKeyEvent* event) { void MainWindow::onResetClicked() { // initialize to defaults + + // envelopeGenerators ui_->envelopeOsc1Volume->init(EnvelopeId::Osc1Volume); + ui_->envelopeFilterCutoff->init(EnvelopeId::FilterCutoff); + ui_->envelopeFilterResonance->init(EnvelopeId::FilterResonance); + + // comboBoxes + ui_->comboOsc1WaveSelector1->setCurrentIndex(static_cast(PARAM_DEFS[static_cast(ParamId::Osc1WaveSelector1)].def)); + ui_->comboOsc1WaveSelector2->setCurrentIndex(static_cast(PARAM_DEFS[static_cast(ParamId::Osc1WaveSelector2)].def)); + } diff --git a/src/ui/MainWindow.ui b/src/ui/MainWindow.ui index 4952e45..287cd30 100644 --- a/src/ui/MainWindow.ui +++ b/src/ui/MainWindow.ui @@ -6,7 +6,7 @@ 0 0 - 800 + 940 600 @@ -38,10 +38,10 @@ - 150 - 230 - 500 - 360 + 20 + 270 + 300 + 300 @@ -54,7 +54,7 @@ - 200 + 270 20 400 200 @@ -64,6 +64,203 @@ true + + + + 320 + 270 + 300 + 300 + + + + false + + + true + + + + + + 620 + 270 + 300 + 300 + + + + false + + + true + + + + + + 110 + 240 + 101 + 16 + + + + + 12 + + + + Osc1Volume + + + Qt::AlignmentFlag::AlignCenter + + + + + + 420 + 240 + 101 + 16 + + + + + 12 + + + + FilterCutoff + + + Qt::AlignmentFlag::AlignCenter + + + + + + 710 + 240 + 121 + 16 + + + + + 12 + + + + FilterResonance + + + Qt::AlignmentFlag::AlignCenter + + + + + + 120 + 90 + 86 + 26 + + + + + Sine + + + + + Square + + + + + Saw + + + + + Triangle + + + + + + + 10 + 90 + 101 + 21 + + + + + 8 + + + + Osc1WaveSelector1 + + + Qt::AlignmentFlag::AlignRight|Qt::AlignmentFlag::AlignTrailing|Qt::AlignmentFlag::AlignVCenter + + + + + + 120 + 120 + 86 + 26 + + + + + Sine + + + + + Square + + + + + Saw + + + + + Triangle + + + + + + + 10 + 120 + 101 + 21 + + + + + 8 + + + + Osc1WaveSelector2 + + + Qt::AlignmentFlag::AlignRight|Qt::AlignmentFlag::AlignTrailing|Qt::AlignmentFlag::AlignVCenter + + diff --git a/src/ui/widgets/EnvelopeGenerator/EnvelopeGenerator.ui b/src/ui/widgets/EnvelopeGenerator/EnvelopeGenerator.ui index 6511e09..794d3a4 100644 --- a/src/ui/widgets/EnvelopeGenerator/EnvelopeGenerator.ui +++ b/src/ui/widgets/EnvelopeGenerator/EnvelopeGenerator.ui @@ -6,8 +6,8 @@ 0 0 - 500 - 360 + 300 + 300 @@ -22,10 +22,10 @@ - 130 - 50 - 120 - 300 + 80 + 20 + 65 + 280 @@ -35,10 +35,10 @@ - 370 - 50 - 120 - 300 + 220 + 20 + 65 + 280 @@ -48,10 +48,10 @@ - 250 - 50 - 120 - 300 + 150 + 20 + 65 + 280 @@ -62,9 +62,9 @@ 10 - 50 - 120 - 300 + 20 + 65 + 280 @@ -75,8 +75,8 @@ 0 - 340 - 491 + 290 + 291 20 @@ -88,8 +88,8 @@ 0 - 30 - 491 + 10 + 291 20 @@ -100,15 +100,15 @@ - 50 - 0 + 30 + -10 21 31 - 20 + 12 @@ -121,15 +121,15 @@ - 170 - 0 - 30 + 100 + -10 + 21 31 - 20 + 12 @@ -142,15 +142,15 @@ - 300 - 0 + 170 + -10 20 31 - 20 + 12 @@ -163,15 +163,15 @@ - 420 - 0 + 240 + -10 20 31 - 20 + 12 diff --git a/src/ui/widgets/Scope/Scope.cpp b/src/ui/widgets/Scope/Scope.cpp index dce472f..3e2cff3 100644 --- a/src/ui/widgets/Scope/Scope.cpp +++ b/src/ui/widgets/Scope/Scope.cpp @@ -53,7 +53,7 @@ void Scope::paintEvent(QPaintEvent*) { for (int32_t i = 1; i < samples_.size(); i++) { p.drawLine( - (i - 1) * width() / samples_.size(), + (i) * width() / samples_.size(), midY - samples_[i - 1] * scaleY, i * width() / samples_.size(), midY - samples_[i] * scaleY diff --git a/src/ui/widgets/SmartSlider/SmartSlider.ui b/src/ui/widgets/SmartSlider/SmartSlider.ui index 32eb06b..1d0122b 100644 --- a/src/ui/widgets/SmartSlider/SmartSlider.ui +++ b/src/ui/widgets/SmartSlider/SmartSlider.ui @@ -6,8 +6,8 @@ 0 0 - 120 - 300 + 65 + 280 @@ -22,8 +22,8 @@ - 50 - 90 + 23 + 80 16 160 @@ -44,9 +44,9 @@ - 20 - 260 - 82 + 6 + 250 + 51 24 @@ -56,13 +56,19 @@ 0 + + Qt::AlignmentFlag::AlignCenter + + + QAbstractSpinBox::ButtonSymbols::NoButtons + - 20 - 60 - 82 + 6 + 50 + 51 24 @@ -72,13 +78,22 @@ 0 + + Qt::AlignmentFlag::AlignCenter + + + QAbstractSpinBox::ButtonSymbols::NoButtons + + + 40000.000000000000000 + - 20 - 20 - 82 + 6 + 10 + 51 31 @@ -93,14 +108,23 @@ 12 + + Qt::LayoutDirection::LeftToRight + + + Qt::AlignmentFlag::AlignCenter + + + QAbstractSpinBox::ButtonSymbols::NoButtons + - 0 + -7 10 20 - 271 + 261 @@ -116,10 +140,10 @@ - 100 + 50 10 20 - 271 + 261