From f80daed2220197dcf24475d96efb08afabd3b97f Mon Sep 17 00:00:00 2001 From: Dejvino Date: Fri, 27 Feb 2026 22:23:59 +0100 Subject: [PATCH] ADSR and filters --- main.cpp | 98 ++++++++++++++++++++++++++++++++++++++++------ synth_engine.cpp | 100 +++++++++++++++++++++++++++++++++++++++-------- synth_engine.h | 28 +++++++++++++ 3 files changed, 199 insertions(+), 27 deletions(-) diff --git a/main.cpp b/main.cpp index 9e286fb..c1058e7 100644 --- a/main.cpp +++ b/main.cpp @@ -28,6 +28,10 @@ float knob_vol_val = 0.5f; SynthEngine::Waveform current_waveform = SynthEngine::SAWTOOTH; const char* waveform_names[] = {"Saw", "Square", "Sine"}; +// ADSR (A, D, R in seconds, S in 0-1) +float adsr_vals[4] = {0.05f, 0.2f, 0.6f, 0.5f}; +float filter_vals[2] = {1.0f, 0.0f}; // LP (1.0=Open), HP (0.0=Open) + // --- MIDI / Keyboard Input State --- std::map key_to_note_map; int current_key_scancode = 0; // 0 for none @@ -174,6 +178,21 @@ void drawToggle(SDL_Renderer* renderer, int x, int y, int size, bool active) { SDL_RenderDrawLine(renderer, m_x + m_w, m_y, m_x + m_w, m_y + m_h); // Right leg } +void drawSlider(SDL_Renderer* renderer, int x, int y, int w, int h, float val, const char* label) { + // Track + SDL_SetRenderDrawColor(renderer, 80, 80, 80, 255); + SDL_Rect track = {x + w/2 - 2, y, 4, h}; + SDL_RenderFillRect(renderer, &track); + + // Handle + int handleH = 10; + int handleY = y + h - (int)(val * h) - handleH/2; + SDL_SetRenderDrawColor(renderer, 200, 200, 200, 255); + SDL_Rect handle = {x, handleY, w, handleH}; + SDL_RenderFillRect(renderer, &handle); + SDL_RenderDrawRect(renderer, &handle); +} + int main(int argc, char* argv[]) { (void)argc; (void)argv; @@ -232,6 +251,10 @@ int main(int argc, char* argv[]) { engine.setVolume(knob_vol_val); engine.setGate(false); // Start with silence + + // Init engine with default UI values + engine.setADSR(adsr_vals[0]*2.0f, adsr_vals[1]*2.0f, adsr_vals[2], adsr_vals[3]*2.0f); + engine.setFilter(20.0f + filter_vals[0]*19980.0f, 20.0f + filter_vals[1]*19980.0f); // --- Main Loop --- bool quit = false; @@ -285,9 +308,9 @@ int main(int argc, char* argv[]) { SDL_GetMouseState(&mouseX, &mouseY); // Check Toggle Click - int toggleX = WINDOW_WIDTH / 2; - int toggleY = WINDOW_HEIGHT * 3 / 4; - int toggleSize = 40; + int toggleX = 580; + int toggleY = 450; + int toggleSize = 30; if (mouseX >= toggleX - toggleSize/2 && mouseX <= toggleX + toggleSize/2 && mouseY >= toggleY - toggleSize/2 && mouseY <= toggleY + toggleSize/2) { @@ -298,10 +321,52 @@ int main(int argc, char* argv[]) { if (auto_melody_enabled) { auto_melody_next_event_time = SDL_GetTicks(); // Start immediately } - } else if (e.button.button == SDL_BUTTON_LEFT && mouseX < WINDOW_WIDTH / 2) { - // Left knob click emulates encoder switch: cycle waveform - current_waveform = (SynthEngine::Waveform)(((int)current_waveform + 1) % 3); - engine.setWaveform(current_waveform); + } else if (e.button.button == SDL_BUTTON_LEFT) { + // Check Left Knob (Octave) or Waveform Icon + // Knob: 100, 450, r=40. Waveform: 75, 500, 50x20 + bool clickedKnob = (pow(mouseX - 100, 2) + pow(mouseY - 450, 2)) <= pow(40, 2); + bool clickedWave = (mouseX >= 75 && mouseX <= 125 && mouseY >= 500 && mouseY <= 520); + if (clickedKnob || clickedWave) { + // Left knob click emulates encoder switch: cycle waveform + current_waveform = (SynthEngine::Waveform)(((int)current_waveform + 1) % 3); + engine.setWaveform(current_waveform); + } + } + } else if (e.type == SDL_MOUSEMOTION) { + if (e.motion.state & SDL_BUTTON_LMASK) { + int mouseX = e.motion.x; + int mouseY = e.motion.y; + + // Handle Sliders + // ADSR: x=200, 250, 300, 350. y=380, h=150 + // Filters: x=450, 500. + int sliderY = 380; + int sliderH = 150; + int sliderW = 30; + + auto checkSlider = [&](int idx, int sx, float* val) { + if (mouseX >= sx && mouseX <= sx + sliderW && mouseY >= sliderY - 20 && mouseY <= sliderY + sliderH + 20) { + *val = 1.0f - (float)(mouseY - sliderY) / (float)sliderH; + if (*val < 0.0f) *val = 0.0f; + if (*val > 1.0f) *val = 1.0f; + return true; + } + return false; + }; + + bool changed = false; + if (checkSlider(0, 200, &adsr_vals[0])) changed = true; + if (checkSlider(1, 250, &adsr_vals[1])) changed = true; + if (checkSlider(2, 300, &adsr_vals[2])) changed = true; + if (checkSlider(3, 350, &adsr_vals[3])) changed = true; + if (changed) engine.setADSR(adsr_vals[0]*2.0f, adsr_vals[1]*2.0f, adsr_vals[2], adsr_vals[3]*2.0f); + + if (checkSlider(0, 450, &filter_vals[0]) || checkSlider(1, 500, &filter_vals[1])) { + // Map 0-1 to 20Hz-20kHz + float lpFreq = 20.0f + pow(filter_vals[0], 2.0f) * 19980.0f; // Exponential feel + float hpFreq = 20.0f + pow(filter_vals[1], 2.0f) * 19980.0f; + engine.setFilter(lpFreq, hpFreq); + } } } else if (e.type == SDL_KEYDOWN) { if (e.key.repeat == 0) { // Ignore key repeats @@ -390,11 +455,22 @@ int main(int argc, char* argv[]) { // --- Draw Controls --- // Draw in the bottom half of the window + // Knobs moved to edges float normalized_octave = (float)current_octave / 8.0f; // Max octave 8 - drawKnob(renderer, WINDOW_WIDTH / 4, WINDOW_HEIGHT * 3 / 4, 50, normalized_octave); - drawWaveformIcon(renderer, WINDOW_WIDTH / 4 - 25, WINDOW_HEIGHT * 3 / 4 + 60, 50, 20, current_waveform); - drawToggle(renderer, WINDOW_WIDTH / 2, WINDOW_HEIGHT * 3 / 4, 40, auto_melody_enabled); - drawKnob(renderer, WINDOW_WIDTH * 3 / 4, WINDOW_HEIGHT * 3 / 4, 50, knob_vol_val); + drawKnob(renderer, 100, 450, 40, normalized_octave); + drawWaveformIcon(renderer, 100 - 25, 450 + 50, 50, 20, current_waveform); + + drawKnob(renderer, 700, 450, 40, knob_vol_val); + + drawToggle(renderer, 580, 450, 30, auto_melody_enabled); + + drawSlider(renderer, 200, 380, 30, 150, adsr_vals[0], "A"); + drawSlider(renderer, 250, 380, 30, 150, adsr_vals[1], "D"); + drawSlider(renderer, 300, 380, 30, 150, adsr_vals[2], "S"); + drawSlider(renderer, 350, 380, 30, 150, adsr_vals[3], "R"); + + drawSlider(renderer, 450, 380, 30, 150, filter_vals[0], "LP"); + drawSlider(renderer, 500, 380, 30, 150, filter_vals[1], "HP"); SDL_RenderPresent(renderer); } diff --git a/synth_engine.cpp b/synth_engine.cpp index 9a79332..899cdc1 100644 --- a/synth_engine.cpp +++ b/synth_engine.cpp @@ -24,11 +24,20 @@ SynthEngine::SynthEngine(uint32_t sampleRate) _increment(0), _volume(0.5f), _waveform(SAWTOOTH), - _isGateOpen(false) + _isGateOpen(false), + _envState(ENV_IDLE), + _envLevel(0.0f), + _attackInc(0.0f), + _decayDec(0.0f), + _sustainLevel(1.0f), + _releaseDec(0.0f), + _lpAlpha(1.0f), _hpAlpha(0.0f), + _lpVal(0.0f), _hpVal(0.0f) { fill_sine_table(); // Initialize with a default frequency setFrequency(440.0f); + setADSR(0.05f, 0.1f, 0.7f, 0.2f); // Default envelope } void SynthEngine::setFrequency(float freq) { @@ -52,6 +61,31 @@ void SynthEngine::setWaveform(Waveform form) { void SynthEngine::setGate(bool isOpen) { _isGateOpen = isOpen; + if (isOpen) { + _envState = ENV_ATTACK; + } else { + _envState = ENV_RELEASE; + } +} + +void SynthEngine::setADSR(float attack, float decay, float sustain, float release) { + // Calculate increments per sample based on time in seconds + // Avoid division by zero + _attackInc = (attack > 0.001f) ? (1.0f / (attack * _sampleRate)) : 1.0f; + _decayDec = (decay > 0.001f) ? (1.0f / (decay * _sampleRate)) : 1.0f; + _sustainLevel = sustain; + _releaseDec = (release > 0.001f) ? (1.0f / (release * _sampleRate)) : 1.0f; +} + +void SynthEngine::setFilter(float lpCutoff, float hpCutoff) { + // Simple one-pole filter coefficient calculation: alpha = 2*PI*fc/fs + _lpAlpha = 2.0f * M_PI * lpCutoff / _sampleRate; + if (_lpAlpha > 1.0f) _lpAlpha = 1.0f; + if (_lpAlpha < 0.0f) _lpAlpha = 0.0f; + + _hpAlpha = 2.0f * M_PI * hpCutoff / _sampleRate; + if (_hpAlpha > 1.0f) _hpAlpha = 1.0f; + if (_hpAlpha < 0.0f) _hpAlpha = 0.0f; } float SynthEngine::getFrequency() const { @@ -63,22 +97,56 @@ void SynthEngine::process(int16_t* buffer, uint32_t numFrames) { _phase += _increment; int16_t sample = 0; - if (_isGateOpen) { - switch (_waveform) { - case SAWTOOTH: - sample = static_cast(_phase >> 16); - break; - case SQUARE: - sample = (_phase < 0x80000000) ? 32767 : -32768; - break; - case SINE: - // Use top 8 bits of phase as index into sine table - sample = sine_table[(_phase >> 24) & 0xFF]; - break; - } + + // Oscillator Generation + switch (_waveform) { + case SAWTOOTH: + sample = static_cast(_phase >> 16); + break; + case SQUARE: + sample = (_phase < 0x80000000) ? 32767 : -32768; + break; + case SINE: + sample = sine_table[(_phase >> 24) & 0xFF]; + break; } - // Apply volume and write to buffer - buffer[i] = static_cast(sample * _volume); + float sampleF = static_cast(sample); + + // Apply Filters (One-pole) + // Low Pass + _lpVal += _lpAlpha * (sampleF - _lpVal); + sampleF = _lpVal; + + // High Pass (implemented as Input - LowPass(hp_cutoff)) + _hpVal += _hpAlpha * (sampleF - _hpVal); + sampleF = sampleF - _hpVal; + + // Apply ADSR Envelope + switch (_envState) { + case ENV_ATTACK: + _envLevel += _attackInc; + if (_envLevel >= 1.0f) { _envLevel = 1.0f; _envState = ENV_DECAY; } + break; + case ENV_DECAY: + _envLevel -= _decayDec; + if (_envLevel <= _sustainLevel) { _envLevel = _sustainLevel; _envState = ENV_SUSTAIN; } + break; + case ENV_SUSTAIN: + _envLevel = _sustainLevel; + break; + case ENV_RELEASE: + _envLevel -= _releaseDec; + if (_envLevel <= 0.0f) { _envLevel = 0.0f; _envState = ENV_IDLE; } + break; + case ENV_IDLE: + _envLevel = 0.0f; + break; + } + + sampleF *= _envLevel; + + // Apply Master Volume and write to buffer + buffer[i] = static_cast(sampleF * _volume); } } \ No newline at end of file diff --git a/synth_engine.h b/synth_engine.h index 0cf49da..24eefa4 100644 --- a/synth_engine.h +++ b/synth_engine.h @@ -66,6 +66,22 @@ public: */ float getFrequency() const; + /** + * @brief Configures the ADSR envelope. + * @param attack Attack time in seconds. + * @param decay Decay time in seconds. + * @param sustain Sustain level (0.0 to 1.0). + * @param release Release time in seconds. + */ + void setADSR(float attack, float decay, float sustain, float release); + + /** + * @brief Configures the filters. + * @param lpCutoff Low-pass cutoff frequency in Hz. + * @param hpCutoff High-pass cutoff frequency in Hz. + */ + void setFilter(float lpCutoff, float hpCutoff); + private: uint32_t _sampleRate; uint32_t _phase; // Phase accumulator for the oscillator. @@ -73,6 +89,18 @@ private: float _volume; Waveform _waveform; bool _isGateOpen; + + // ADSR State + enum EnvState { ENV_IDLE, ENV_ATTACK, ENV_DECAY, ENV_SUSTAIN, ENV_RELEASE }; + EnvState _envState; + float _envLevel; + float _attackInc, _decayDec, _sustainLevel, _releaseDec; + + // Filter State + float _lpAlpha; + float _hpAlpha; + float _lpVal; + float _hpVal; }; #endif // SYNTH_ENGINE_H \ No newline at end of file