diff --git a/CMakeLists.txt b/CMakeLists.txt index 4c8eee5..001b6d2 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -8,6 +8,7 @@ set(CMAKE_CXX_STANDARD_REQUIRED ON) find_package(Qt6 REQUIRED COMPONENTS Widgets) if (WIN32) # windows 11 x86_64 + # Header-only target (real target, no ::) add_library(rtaudio_headers INTERFACE) target_include_directories(rtaudio_headers INTERFACE @@ -45,6 +46,10 @@ qt_add_executable(metabolus src/MainWindow.h src/MainWindow.ui src/ParameterStore.h + src/KeyboardController.cpp + src/KeyboardController.h + src/NoteQueue.cpp + src/NoteQueue.h src/synth/AudioEngine.cpp src/synth/AudioEngine.h src/synth/Synth.cpp @@ -53,7 +58,11 @@ qt_add_executable(metabolus ) if (WIN32) - target_link_libraries(metabolus PRIVATE Qt6::Widgets RtAudio::RtAudio) + target_link_libraries(metabolus + PRIVATE + Qt6::Widgets + RtAudio::RtAudio + ) add_custom_command(TARGET metabolus POST_BUILD COMMAND ${CMAKE_COMMAND} -E copy_if_different diff --git a/README.md b/README.md index 0f0f171..28df2ed 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ This synthesizer isn't very good, but it's neat :3 - [x] QTWidgets hello-world program: basic increment/reset counter - [x] RtAudio hello-world: basic sine output - [x] Connect UI control to sound output, add a slider for frequency control -- [ ] Add note control via either Midi or a keyboard. Coordinate on-off events to +- [+] Add note control via either Midi or a keyboard. Coordinate on-off events to start and stop tone generation - [ ] Create a widget for this smart-slider to clean up the ui code - [ ] Add envelope generation, attach to global volume for now. ADSR and such, @@ -27,5 +27,7 @@ This synthesizer isn't very good, but it's neat :3 - [ ] Add noise ? - [ ] planning gets sparse this far out because its how far I got with the ESP32 synth +x = complete, + = implemented, in progress, o = working on it + ## setup TODO: instructions on build setup diff --git a/scripts/build.bat b/scripts/build.bat index 4742dd7..31080d8 100644 --- a/scripts/build.bat +++ b/scripts/build.bat @@ -30,8 +30,7 @@ if not exist %BUILD_DIR% ( cmake -S . -B %BUILD_DIR% ^ -G Ninja ^ -DCMAKE_BUILD_TYPE=%CONFIG% ^ - -DCMAKE_PREFIX_PATH=%QT_ROOT% ^ - -DRTAUDIO_ROOT=%RTAUDIO_ROOT% + -DRTAUDIO_ROOT=%RTAUDIO_ROOT% if errorlevel 1 goto error diff --git a/src/KeyboardController.cpp b/src/KeyboardController.cpp index e69de29..e831213 100644 --- a/src/KeyboardController.cpp +++ b/src/KeyboardController.cpp @@ -0,0 +1,51 @@ + +#include "KeyboardController.h" + +#include + +KeyboardController::KeyboardController(NoteQueue& queue) : queue_(queue) { + + // TODO: also configurable via a yml + keymap_ = { + { Qt::Key_Z, 60-12 }, // C + { Qt::Key_S, 61-12 }, // C# + { Qt::Key_X, 62-12 }, // D + { Qt::Key_D, 63-12 }, // D# + { Qt::Key_C, 64-12 }, // E + { Qt::Key_V, 65-12 }, // F + { Qt::Key_G, 66-12 }, // F# + { Qt::Key_B, 67-12 }, // G + { Qt::Key_H, 68-12 }, // G# + { Qt::Key_N, 69-12 }, // A + { Qt::Key_J, 70-12 }, // A# + { Qt::Key_M, 71-12 }, // B + { Qt::Key_Q, 72-12 } // C (octave up) + }; +} + +void KeyboardController::handleKeyPress(QKeyEvent* e) { + if (e->isAutoRepeat()) return; + + auto it = keymap_.find(e->key()); + if (it == keymap_.end()) return; + + queue_.push({ + NoteEventType::NoteOn, + it->second, + 0.8f + }); +} + +// 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; + + auto it = keymap_.find(e->key()); + if (it == keymap_.end()) return; + + queue_.push({ + NoteEventType::NoteOff, + it->second, + 0.8f + }); +} \ No newline at end of file diff --git a/src/KeyboardController.h b/src/KeyboardController.h index e69de29..a793ac9 100644 --- a/src/KeyboardController.h +++ b/src/KeyboardController.h @@ -0,0 +1,26 @@ + +#pragma once + +#include +#include + +#include "NoteQueue.h" + +// The keyboardcontroller handles user inputs from a keyboard and maps them to note events +class KeyboardController { + +public: + explicit KeyboardController(NoteQueue& queue); + ~KeyboardController() = default; + + void handleKeyPress(QKeyEvent* e); + void handleKeyRelease(QKeyEvent* e); + +private: + + NoteQueue& queue_; + + // keymap is key -> midi note id + std::unordered_map keymap_; + +}; diff --git a/src/MainWindow.cpp b/src/MainWindow.cpp index 7b1ec74..21e0914 100644 --- a/src/MainWindow.cpp +++ b/src/MainWindow.cpp @@ -1,12 +1,19 @@ #include "MainWindow.h" +#include #include "ui_MainWindow.h" + #include "ParameterStore.h" -MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent), ui(new Ui::MainWindow) { +MainWindow::MainWindow(QWidget *parent) : + QMainWindow(parent), + ui(new Ui::MainWindow), + audio_(new AudioEngine()), + keyboard_(audio_->noteQueue()) { ui->setupUi(this); + setFocusPolicy(Qt::StrongFocus); // Initialize UI updateCounterLabel(); @@ -24,7 +31,6 @@ MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent), ui(new Ui::MainWi connect(ui->inputValue, &QLineEdit::editingFinished, this, &MainWindow::onValueChanged); // synth business - audio_ = new AudioEngine(); audio_->start(); // init defaults @@ -41,6 +47,14 @@ MainWindow::~MainWindow() { delete ui; } +void MainWindow::keyPressEvent(QKeyEvent* event) { + keyboard_.handleKeyPress(event); +} + +void MainWindow::keyReleaseEvent(QKeyEvent* event) { + keyboard_.handleKeyRelease(event); +} + void MainWindow::onIncrementClicked() { counter_++; updateCounterLabel(); diff --git a/src/MainWindow.h b/src/MainWindow.h index 2f54513..e1450b3 100644 --- a/src/MainWindow.h +++ b/src/MainWindow.h @@ -2,6 +2,7 @@ #pragma once #include +#include #include "synth/AudioEngine.h" @@ -19,6 +20,10 @@ public: explicit MainWindow(QWidget *parent = nullptr); ~MainWindow(); +protected: + void keyPressEvent(QKeyEvent* event) override; + void keyReleaseEvent(QKeyEvent* event) override; + private slots: void onIncrementClicked(); void onResetClicked(); @@ -42,4 +47,6 @@ private: void syncValueToUi(int value); AudioEngine* audio_ = nullptr; + KeyboardController keyboard_; + }; diff --git a/src/main.cpp b/src/main.cpp index 4fc7f97..a93a0a3 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -1,5 +1,6 @@ #include + #include "MainWindow.h" #include @@ -13,5 +14,7 @@ int main(int argc, char *argv[]) { MainWindow window; window.show(); - return app.exec(); + int status = app.exec(); + + return status; } diff --git a/src/synth/AudioEngine.cpp b/src/synth/AudioEngine.cpp index d518c66..f3dfd87 100644 --- a/src/synth/AudioEngine.cpp +++ b/src/synth/AudioEngine.cpp @@ -9,7 +9,6 @@ AudioEngine::AudioEngine() : synth_(params_) { } // TODO: get audio configurations - synth_.setSampleRate(sampleRate_); } @@ -53,7 +52,13 @@ int32_t AudioEngine::audioCallback( void* outputBuffer, void*, uint32_t nFrames, int32_t AudioEngine::process(float* out, uint32_t nFrames) { - // pass to synth + // pass note handling to synth + NoteEvent event; + while(noteQueue_.pop(event)) { + synth_.handleNoteEvent(event); + } + + // pass sample generation to synth synth_.process(out, nFrames, sampleRate_); return 0; diff --git a/src/synth/AudioEngine.h b/src/synth/AudioEngine.h index c39f18a..5bbd987 100644 --- a/src/synth/AudioEngine.h +++ b/src/synth/AudioEngine.h @@ -6,6 +6,7 @@ #include #include "Synth.h" +#include "../KeyboardController.h" class AudioEngine { @@ -19,8 +20,9 @@ public: // stops the audio stream. void stop(); - // params getter + // getters ParameterStore* parameters() { return ¶ms_; } + NoteQueue& noteQueue() { return noteQueue_; } private: @@ -31,6 +33,7 @@ private: int32_t process(float* out, uint32_t nFrames); ParameterStore params_; + NoteQueue noteQueue_; Synth synth_; // TODO: id like a yml config file or something for these diff --git a/src/synth/Synth.cpp b/src/synth/Synth.cpp index 5d72d54..b4a819f 100644 --- a/src/synth/Synth.cpp +++ b/src/synth/Synth.cpp @@ -8,6 +8,10 @@ #define M_PI 3.14159265358979323846 #endif +#define SYNTH_PITCH_STANDARD 432.0f // frequency of home pitch +#define SYNTH_MIDI_HOME 69 // midi note index of home pitch +#define SYNTH_NOTES_PER_OCTAVE 12 + Synth::Synth(const ParameterStore& params) : paramStore_(params) { } @@ -22,26 +26,69 @@ inline float Synth::getParam(ParamId id) { return params_[static_cast(id)].current; } +inline float Synth::noteToFrequency(uint8_t note) { + return SYNTH_PITCH_STANDARD * pow(2.0f, static_cast(note - SYNTH_MIDI_HOME) / static_cast(SYNTH_NOTES_PER_OCTAVE)); +} + +// TODO: stop popping on note-offs +void Synth::handleNoteEvent(const NoteEvent& event) { + if(event.type == NoteEventType::NoteOn) { + // add note to activeNotes list + if (std::find(heldNotes_.begin(), heldNotes_.end(), event.note) == heldNotes_.end()) { + heldNotes_.push_back(event.note); + } + } else { + // remove note from activeNotes list + auto it = std::find(heldNotes_.begin(), heldNotes_.end(), event.note); + if (it != heldNotes_.end()) { + heldNotes_.erase(it); + } + } + + updateCurrentNote(); +} + +void Synth::updateCurrentNote() { + if(heldNotes_.empty()) { + noteActive_ = false; + return; + } + + uint8_t note = heldNotes_.back(); + frequency_ = noteToFrequency(note); + noteActive_ = true; +} + void Synth::process(float* out, uint32_t nFrames, uint32_t sampleRate) { // yeah really only need to update this once per buffer if its ~6ms latency updateParams(); + float sampleOut = 0.0f; + for (uint32_t i = 0; i < nFrames; i++) { + // updates internal buffered parameters for smoothing for(auto& p : params_) p.update(); // TODO: profile this - // based on oscillator frequency - float frequency = getParam(ParamId::Osc1Frequency); // this will come from a midi controller - float phaseInc = 2.0f * M_PI * frequency / static_cast(sampleRate); + // skip if no note is being played + // TODO: this will be handled by an idle envelope eventually + // could also say gain = 0.0f; but w/e, this saves computing + if(!noteActive_) { + out[2*i] = 0.0f; + out[2*i+1] = 0.0f; + continue; + } + + float phaseInc = 2.0f * M_PI * frequency_ / static_cast(sampleRate); // sample generation float gain = getParam(ParamId::Osc1Gain); - float sample = std::sin(phase_) * gain; + sampleOut = std::sin(phase_) * gain; // write to buffer - out[2*i] = sample; // left - out[2*i+1] = sample; // right + out[2*i] = sampleOut; // left + out[2*i+1] = sampleOut; // right // sampling business phase_ += phaseInc; diff --git a/src/synth/Synth.h b/src/synth/Synth.h index 2c77054..76fc118 100644 --- a/src/synth/Synth.h +++ b/src/synth/Synth.h @@ -2,7 +2,9 @@ #pragma once #include "../ParameterStore.h" +#include "../NoteQueue.h" +#include #include struct SmoothedParam { @@ -10,9 +12,7 @@ struct SmoothedParam { float target = 0.0f; float gain = 0.001f; - inline void update() { - current += gain * (target - current); - } + inline void update() { current += gain * (target - current); } }; class Synth { @@ -24,6 +24,9 @@ public: // generates a buffer of audio samples nFrames long void process(float* out, uint32_t nFrames, uint32_t sampleRate); + // handles note events + void handleNoteEvent(const NoteEvent& event); + // sample rate setter void setSampleRate(uint32_t sampleRate) { sampleRate_ = sampleRate; } @@ -35,10 +38,24 @@ private: // small getter that abstracts all the static casts and such inline float getParam(ParamId); + // for calculating frequency based on midi note id + inline float noteToFrequency(uint8_t note); + + // finds the active voice + void updateCurrentNote(); + const ParameterStore& paramStore_; // smoothed params creates a buffer in case the thread controlling paramStore gets blocked std::array params_; uint32_t sampleRate_; + // here's where the actual sound generation happens + // TODO: put this in an oscillator class + bool noteActive_ = false; + float frequency_ = 220.0f; float phase_ = 0.0f; + + // TODO: might make this a fixed array where index=midi-note and the value=velocity + // so non-zero elements are the ones currently being played + std::vector heldNotes_; };