added simple keyboard control

This commit is contained in:
2025-12-24 20:26:37 -06:00
parent ae85e7ad13
commit c5bf66b31c
12 changed files with 202 additions and 19 deletions

View File

@@ -0,0 +1,51 @@
#include "KeyboardController.h"
#include <iostream>
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
});
}

View File

@@ -0,0 +1,26 @@
#pragma once
#include <QKeyEvent>
#include <unordered_map>
#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<int32_t, uint8_t> keymap_;
};

View File

@@ -1,12 +1,19 @@
#include "MainWindow.h"
#include <QTimer>
#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();

View File

@@ -2,6 +2,7 @@
#pragma once
#include <QMainWindow>
#include <QKeyEvent>
#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_;
};

View File

@@ -1,5 +1,6 @@
#include <QApplication>
#include "MainWindow.h"
#include <iostream>
@@ -13,5 +14,7 @@ int main(int argc, char *argv[]) {
MainWindow window;
window.show();
return app.exec();
int status = app.exec();
return status;
}

View File

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

View File

@@ -6,6 +6,7 @@
#include <atomic>
#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 &params_; }
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

View File

@@ -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<size_t>(id)].current;
}
inline float Synth::noteToFrequency(uint8_t note) {
return SYNTH_PITCH_STANDARD * pow(2.0f, static_cast<float>(note - SYNTH_MIDI_HOME) / static_cast<float>(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<float>(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<float>(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;

View File

@@ -2,7 +2,9 @@
#pragma once
#include "../ParameterStore.h"
#include "../NoteQueue.h"
#include <vector>
#include <atomic>
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<SmoothedParam, PARAM_COUNT> 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<uint8_t> heldNotes_;
};