From b160c8ae6960a50ad416d8e2bc33ec9e817ede5a Mon Sep 17 00:00:00 2001 From: Bliblank Date: Wed, 14 Jan 2026 23:06:56 -0600 Subject: [PATCH] comments --- README.md | 4 +++- src/KeyboardController.cpp | 1 - src/MidiController.cpp | 25 +++++++++++++++---------- src/NoteQueue.cpp | 2 ++ src/ParameterStore.cpp | 5 +++++ src/main.cpp | 6 +++--- src/synth/AudioEngine.cpp | 6 ++++-- src/synth/Envelope.cpp | 1 + src/synth/Filter.cpp | 5 ++++- src/synth/Oscillator.cpp | 2 ++ src/synth/ScopeBuffer.cpp | 2 ++ src/synth/Synth.cpp | 6 ++++-- src/synth/Voice.cpp | 7 +++++-- 13 files changed, 50 insertions(+), 22 deletions(-) diff --git a/README.md b/README.md index 16717e2..dc858fe 100644 --- a/README.md +++ b/README.md @@ -21,10 +21,11 @@ This synthesizer isn't very good, but it's neat :3 oscillators increase the sound complexity considerably - [x] Create a UI scope to visualize the synthesized composite waveform - [ ] Create wavetables for more complex tone generation. Needs to be selectable from ui +- [ ] Wavetable file loading - [x] Create digital filters, prob biquad. Controllable from ui obv (cutoff + resonance) - [x] Add polyphony somewhere. Probably involves a voice class. If processing power allows it, tie a voice to each midi note -- [ ] polyphony is lacking sustain pedal rn +- [x] polyphony is lacking sustain pedal rn - [ ] planning gets sparse this far out because its how far I got with the ESP32 synth - [ ] Filter upgrades including some more complex solving techniques (State Variable Filter), better key tracking, more natural envelope curves, filter drive, etc. @@ -34,6 +35,7 @@ This synthesizer isn't very good, but it's neat :3 - [ ] Profile saving and loading, also includes loading configurations like keymaps, audio engine config, etc. from a yaml instead of being hardcoded - [ ] Noise +- [ ] LFO modulation ## setup TODO: instructions on build setup diff --git a/src/KeyboardController.cpp b/src/KeyboardController.cpp index d9edb1f..4b60687 100644 --- a/src/KeyboardController.cpp +++ b/src/KeyboardController.cpp @@ -50,7 +50,6 @@ void KeyboardController::handleKeyPress(QKeyEvent* e) { }); } -// TODO: something like a sustain pedal will suspend note-off events. probably control that in the midi controller void KeyboardController::handleKeyRelease(QKeyEvent* e) { if (e->isAutoRepeat()) return; diff --git a/src/MidiController.cpp b/src/MidiController.cpp index ade4e40..f1422e8 100644 --- a/src/MidiController.cpp +++ b/src/MidiController.cpp @@ -18,6 +18,7 @@ MidiController::~MidiController() { close(); } +// this dont work too well but whatever bool MidiController::openDefaultPort() { if (!midiIn_) return false; if (midiIn_->getPortCount() == 0) { @@ -33,8 +34,7 @@ bool MidiController::openPort(unsigned int index) { try { midiIn_->openPort(index); midiIn_->setCallback(&MidiController::midiCallback, this); - std::cout << "Opened MIDI port: " - << midiIn_->getPortName(index) << "\n"; + std::cout << "Opened MIDI port: " << midiIn_->getPortName(index) << "\n"; return true; } catch (RtMidiError& e) { std::cerr << e.getMessage() << std::endl; @@ -48,42 +48,45 @@ void MidiController::close() { } } +// called by RtMidi on receive of a midi message. deltaTime is time since last midi message, not useful atm void MidiController::midiCallback(double /*deltaTime*/, std::vector* message, void* userData) { auto* self = static_cast(userData); if (!message || message->empty()) return; - self->handleMessage(*message); + self->handleMessage(*message); // pass to parsing function if valid } void MidiController::handleMessage(const std::vector& msg) { - if(msg.size() <= 1) return; + if(msg.size() <= 1) return; // msg doesn't contain useful note info uint8_t status = msg[0] & 0xF0; uint8_t data1 = msg[1]; uint8_t data2 = msg[2]; - if(status == 0xFE) return; - if(status == 0xF8) return; + if(status == 0xFE) return; // "Active Sensing" -> 300ms heartbeat. could be useful to sense if this is missing for device failure detection + if(status == 0xF8) return; // "Timing Clock" -> 24 pulses per quarter note, for steady rhythm. not useful for this instrument + // sustain pedal message event if(status == 0xB0 && data1 == 64) { handleSustain(data2 >= 64); return; } - unsigned char note = msg.size() > 1 ? msg[1] : 0; - unsigned char vel = msg.size() > 2 ? msg[2] : 0; + unsigned char note = msg.size() > 1 ? msg[1] : 0; // note number + unsigned char vel = msg.size() > 2 ? msg[2] : 0; // velocity - // Note On (velocity > 0) + // note on (velocity > 0) if (status == 0x90 && vel > 0) { noteOn(note, vel); } - // Note Off (or Note On with velocity 0) + // note off (or note on with 0 velocity) else if (status == 0x80 || (status == 0x90 && vel == 0)) { noteOff(note); } } +// construct note on event and add to noteQueue void MidiController::noteOn(uint8_t note, uint8_t vel) { sustainedNotes_.erase(note); @@ -95,6 +98,7 @@ void MidiController::noteOn(uint8_t note, uint8_t vel) { }); } +// add note off event to noteQueue if no sustain active void MidiController::noteOff(uint8_t note) { if(sustainDown_) { sustainedNotes_.insert(note); @@ -108,6 +112,7 @@ void MidiController::noteOff(uint8_t note) { }); } +// if sustain goes from on->off, then noteOff all the active ntoes void MidiController::handleSustain(bool down) { if(down == sustainDown_) return; diff --git a/src/NoteQueue.cpp b/src/NoteQueue.cpp index e86617f..85df585 100644 --- a/src/NoteQueue.cpp +++ b/src/NoteQueue.cpp @@ -2,6 +2,7 @@ #include "NoteQueue.h" #include +// add event to noteQueue, called by MidiController or keyboardController bool NoteQueue::push(const NoteEvent& event) { size_t head = head_.load(std::memory_order_relaxed); size_t next = (head + 1) % SIZE; @@ -14,6 +15,7 @@ bool NoteQueue::push(const NoteEvent& event) { return true; } +// take event from noteQueue, called by synth bool NoteQueue::pop(NoteEvent& event) { size_t tail = tail_.load(std::memory_order_relaxed); diff --git a/src/ParameterStore.cpp b/src/ParameterStore.cpp index b06c428..49f1d94 100644 --- a/src/ParameterStore.cpp +++ b/src/ParameterStore.cpp @@ -5,10 +5,12 @@ ParameterStore::ParameterStore() { resetToDefaults(); } +// set parameter value void ParameterStore::set(ParamId id, float value) { values_[static_cast(id)].store(value, std::memory_order_relaxed); } +// set a whole envelope of parameters void ParameterStore::set(EnvelopeId id, float depth, float a, float d, float s, float r) { EnvelopeParam params = ENV_PARAMS[static_cast(id)]; @@ -19,6 +21,7 @@ void ParameterStore::set(EnvelopeId id, float depth, float a, float d, float s, set(params.r, r); } +// get a single parameter float ParameterStore::get(ParamId id) const { return values_[static_cast(id)].load(std::memory_order_relaxed); } @@ -28,3 +31,5 @@ void ParameterStore::resetToDefaults() { values_[i].store(PARAM_DEFS[i].def, std::memory_order_relaxed); } } + +// TODO: applying parameter profiles will work similarly to above function diff --git a/src/main.cpp b/src/main.cpp index dea8c3a..92ca2a1 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -11,10 +11,10 @@ int main(int argc, char *argv[]) { QApplication app(argc, argv); - MainWindow window; + MainWindow window; // entry point goes to MainWindow::MainWindow() window.show(); - - int status = app.exec(); + + int status = app.exec(); // assembles ui return status; } diff --git a/src/synth/AudioEngine.cpp b/src/synth/AudioEngine.cpp index 2a284a3..fa1673b 100644 --- a/src/synth/AudioEngine.cpp +++ b/src/synth/AudioEngine.cpp @@ -21,9 +21,10 @@ AudioEngine::~AudioEngine() { bool AudioEngine::start() { + // initialize the audio engine RtAudio::StreamParameters params; params.deviceId = audio_.getDefaultOutputDevice(); - params.nChannels = channels_; + params.nChannels = channels_; // we're doing two duplicate channels for pseudo-mono params.firstChannel = 0; RtAudio::StreamOptions options; @@ -47,7 +48,8 @@ void AudioEngine::stop() { int32_t AudioEngine::audioCallback( void* outputBuffer, void*, uint32_t nFrames, double, RtAudioStreamStatus status, void* userData) { - if (status) std::cerr << "Stream underflow!" << std::endl; + // error if process is too slow for the callback. If this is consistent, then need to optimize synth.process() or whatever cascades from it + if (status) std::cerr << "Stream underflow" << std::endl; return static_cast(userData)->process(static_cast(outputBuffer), nFrames); } diff --git a/src/synth/Envelope.cpp b/src/synth/Envelope.cpp index 617e60a..5d0f883 100644 --- a/src/synth/Envelope.cpp +++ b/src/synth/Envelope.cpp @@ -14,6 +14,7 @@ void Envelope::noteOff() { if(state_ != State::Idle) state_ = State::Release; } +// returns current value based on state and steps forward one sample float Envelope::process() { switch (state_) { diff --git a/src/synth/Filter.cpp b/src/synth/Filter.cpp index 7e27c5e..4334f9d 100644 --- a/src/synth/Filter.cpp +++ b/src/synth/Filter.cpp @@ -13,6 +13,7 @@ void Filter::setSampleRate(float sampleRate) { calculateCoefficients(); } +// recalculate filter based on params void Filter::setParams(Type type, float frequency, float q) { type_ = type; frequency_ = std::min(frequency, sampleRate_ / 2.0f * 0.999f); @@ -20,6 +21,7 @@ void Filter::setParams(Type type, float frequency, float q) { calculateCoefficients(); } +// update current state and output filtered value float Filter::biquadProcess(float in) { // calculate filtered sample @@ -32,6 +34,7 @@ float Filter::biquadProcess(float in) { return out; } +// internal control system emulation void Filter::calculateCoefficients() { if(q_ < 0.001f) q_ = 0.001f; @@ -72,7 +75,7 @@ void Filter::calculateCoefficients() { break; } - // Normalize + // values need to be normalized b0_ = b0 / a0; b1_ = b1 / a0; b2_ = b2 / a0; diff --git a/src/synth/Oscillator.cpp b/src/synth/Oscillator.cpp index 93dd432..bc8c857 100644 --- a/src/synth/Oscillator.cpp +++ b/src/synth/Oscillator.cpp @@ -2,3 +2,5 @@ #include "Oscillator.h" // placeholder + +// will eventually hold wavetable sampling, store phase state, and all that silliness diff --git a/src/synth/ScopeBuffer.cpp b/src/synth/ScopeBuffer.cpp index 96ff31f..e418cdb 100644 --- a/src/synth/ScopeBuffer.cpp +++ b/src/synth/ScopeBuffer.cpp @@ -5,11 +5,13 @@ ScopeBuffer::ScopeBuffer(size_t size) : buffer_(size) { } +// add a single sample to the scope buffer, called by synth void ScopeBuffer::push(float sample) { size_t w = writeIndex_.fetch_add(1, std::memory_order_relaxed); buffer_[w % buffer_.size()] = sample; } +// outputs value from the scope buffer, called by the scope widget void ScopeBuffer::read(std::vector& out) const { size_t w = writeIndex_.load(std::memory_order_relaxed); for (size_t i = 0; i < out.size(); i++) { diff --git a/src/synth/Synth.cpp b/src/synth/Synth.cpp index 2315e80..52790d0 100644 --- a/src/synth/Synth.cpp +++ b/src/synth/Synth.cpp @@ -30,6 +30,8 @@ inline float Synth::getParam(ParamId id) { return params_[static_cast(id)].current; } +// called by audio engine before process (because it has the reference to the noteEvent store) +// this function consumes the noteEvents in the noteEvent store (whether produced by MIDI or keyboard) void Synth::handleNoteEvent(const NoteEvent& event) { lastTime = event.timestamp; @@ -45,7 +47,6 @@ void Synth::handleNoteEvent(const NoteEvent& event) { } } - // TODO: find quietest voice and assign a note to it instead of just the first inactive one // find inactive voice and start it with the given note for(Voice& v : voices_) { if(!v.isActive()) { @@ -85,7 +86,7 @@ void Synth::process(float* out, uint32_t nFrames, uint32_t sampleRate) { float params[PARAM_COUNT] = {0.0f}; for(int i = 0; i < PARAM_COUNT; i++) { params[i] = params_[i].current; - } + } // maybe take this outside the loop if performance is an issue // foreach voice, process... float mix = 0.0f; @@ -94,6 +95,7 @@ void Synth::process(float* out, uint32_t nFrames, uint32_t sampleRate) { } mix /= 4.0f; // for number of voices to prevent clipping mix = tanh(mix); // really prevents clipping + // TODO: these saturation function work kinda like magic, use them elsewhere sampleOut = mix; diff --git a/src/synth/Voice.cpp b/src/synth/Voice.cpp index 8f3b4ed..69c326a 100644 --- a/src/synth/Voice.cpp +++ b/src/synth/Voice.cpp @@ -7,6 +7,7 @@ Voice::Voice(SmoothedParam* params) : params_(params) { } +// cascade sample rate to all descendant objects void Voice::setSampleRate(float sampleRate) { sampleRate_ = sampleRate; @@ -23,6 +24,7 @@ void Voice::setSampleRate(float sampleRate) { //osc1_.setSampleRate(sampleRate); } +// calculates oscillator frequency based on midi note inline float Voice::noteToFrequency(uint8_t note) { return SYNTH_PITCH_STANDARD * pow(2.0f, static_cast(note - SYNTH_MIDI_HOME) / static_cast(SYNTH_NOTES_PER_OCTAVE)); } @@ -55,6 +57,7 @@ bool Voice::isActive() { return active_; } +// generates a single sample, called from synth.process() float Voice::process(float* params, bool& scopeTrigger) { // process all envelopes @@ -69,11 +72,10 @@ float Voice::process(float* params, bool& scopeTrigger) { return 0.0f; } + // process all envelopes float gainEnv = gainEnvelope_.process(); float cutoffEnv = cutoffEnvelope_.process(); float resonanceEnv = resonanceEnvelope_.process(); - // TODO: envelope is shared between all notes so this sequence involves a note change but only one envelope attack: - // NOTE_A_ON > NOTE_B_ON > NOTE_A_OFF and note B starts playing part-way through note A's envelope // TODO: make pitchOffset variable for each oscillator (maybe three values like octave, semitone offset, and pitch offset in cents) float pitchOffset = 1.0f; @@ -120,6 +122,7 @@ float Voice::process(float* params, bool& scopeTrigger) { sampleOut = filter1_.biquadProcess(sampleOut); sampleOut = filter2_.biquadProcess(sampleOut); + // state tracking, may keep this here even if oscillators store their own phase because it might help with scope triggering phase_ += phaseInc; if (phase_ > 2.0f * M_PI) { phase_ -= 2.0f * M_PI;