added simple keyboard control
This commit is contained in:
@@ -8,6 +8,7 @@ set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
|||||||
find_package(Qt6 REQUIRED COMPONENTS Widgets)
|
find_package(Qt6 REQUIRED COMPONENTS Widgets)
|
||||||
|
|
||||||
if (WIN32) # windows 11 x86_64
|
if (WIN32) # windows 11 x86_64
|
||||||
|
|
||||||
# Header-only target (real target, no ::)
|
# Header-only target (real target, no ::)
|
||||||
add_library(rtaudio_headers INTERFACE)
|
add_library(rtaudio_headers INTERFACE)
|
||||||
target_include_directories(rtaudio_headers INTERFACE
|
target_include_directories(rtaudio_headers INTERFACE
|
||||||
@@ -45,6 +46,10 @@ qt_add_executable(metabolus
|
|||||||
src/MainWindow.h
|
src/MainWindow.h
|
||||||
src/MainWindow.ui
|
src/MainWindow.ui
|
||||||
src/ParameterStore.h
|
src/ParameterStore.h
|
||||||
|
src/KeyboardController.cpp
|
||||||
|
src/KeyboardController.h
|
||||||
|
src/NoteQueue.cpp
|
||||||
|
src/NoteQueue.h
|
||||||
src/synth/AudioEngine.cpp
|
src/synth/AudioEngine.cpp
|
||||||
src/synth/AudioEngine.h
|
src/synth/AudioEngine.h
|
||||||
src/synth/Synth.cpp
|
src/synth/Synth.cpp
|
||||||
@@ -53,7 +58,11 @@ qt_add_executable(metabolus
|
|||||||
)
|
)
|
||||||
|
|
||||||
if (WIN32)
|
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
|
add_custom_command(TARGET metabolus POST_BUILD
|
||||||
COMMAND ${CMAKE_COMMAND} -E copy_if_different
|
COMMAND ${CMAKE_COMMAND} -E copy_if_different
|
||||||
|
|||||||
@@ -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] QTWidgets hello-world program: basic increment/reset counter
|
||||||
- [x] RtAudio hello-world: basic sine output
|
- [x] RtAudio hello-world: basic sine output
|
||||||
- [x] Connect UI control to sound output, add a slider for frequency control
|
- [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
|
start and stop tone generation
|
||||||
- [ ] Create a widget for this smart-slider to clean up the ui code
|
- [ ] 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,
|
- [ ] 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 ?
|
- [ ] Add noise ?
|
||||||
- [ ] planning gets sparse this far out because its how far I got with the ESP32 synth
|
- [ ] 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
|
## setup
|
||||||
TODO: instructions on build setup
|
TODO: instructions on build setup
|
||||||
|
|||||||
@@ -30,7 +30,6 @@ if not exist %BUILD_DIR% (
|
|||||||
cmake -S . -B %BUILD_DIR% ^
|
cmake -S . -B %BUILD_DIR% ^
|
||||||
-G Ninja ^
|
-G Ninja ^
|
||||||
-DCMAKE_BUILD_TYPE=%CONFIG% ^
|
-DCMAKE_BUILD_TYPE=%CONFIG% ^
|
||||||
-DCMAKE_PREFIX_PATH=%QT_ROOT% ^
|
|
||||||
-DRTAUDIO_ROOT=%RTAUDIO_ROOT%
|
-DRTAUDIO_ROOT=%RTAUDIO_ROOT%
|
||||||
|
|
||||||
if errorlevel 1 goto error
|
if errorlevel 1 goto error
|
||||||
|
|||||||
@@ -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
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -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_;
|
||||||
|
|
||||||
|
};
|
||||||
|
|||||||
@@ -1,12 +1,19 @@
|
|||||||
|
|
||||||
#include "MainWindow.h"
|
#include "MainWindow.h"
|
||||||
|
|
||||||
|
#include <QTimer>
|
||||||
#include "ui_MainWindow.h"
|
#include "ui_MainWindow.h"
|
||||||
|
|
||||||
#include "ParameterStore.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);
|
ui->setupUi(this);
|
||||||
|
setFocusPolicy(Qt::StrongFocus);
|
||||||
|
|
||||||
// Initialize UI
|
// Initialize UI
|
||||||
updateCounterLabel();
|
updateCounterLabel();
|
||||||
@@ -24,7 +31,6 @@ MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent), ui(new Ui::MainWi
|
|||||||
connect(ui->inputValue, &QLineEdit::editingFinished, this, &MainWindow::onValueChanged);
|
connect(ui->inputValue, &QLineEdit::editingFinished, this, &MainWindow::onValueChanged);
|
||||||
|
|
||||||
// synth business
|
// synth business
|
||||||
audio_ = new AudioEngine();
|
|
||||||
audio_->start();
|
audio_->start();
|
||||||
|
|
||||||
// init defaults
|
// init defaults
|
||||||
@@ -41,6 +47,14 @@ MainWindow::~MainWindow() {
|
|||||||
delete ui;
|
delete ui;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void MainWindow::keyPressEvent(QKeyEvent* event) {
|
||||||
|
keyboard_.handleKeyPress(event);
|
||||||
|
}
|
||||||
|
|
||||||
|
void MainWindow::keyReleaseEvent(QKeyEvent* event) {
|
||||||
|
keyboard_.handleKeyRelease(event);
|
||||||
|
}
|
||||||
|
|
||||||
void MainWindow::onIncrementClicked() {
|
void MainWindow::onIncrementClicked() {
|
||||||
counter_++;
|
counter_++;
|
||||||
updateCounterLabel();
|
updateCounterLabel();
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include <QMainWindow>
|
#include <QMainWindow>
|
||||||
|
#include <QKeyEvent>
|
||||||
|
|
||||||
#include "synth/AudioEngine.h"
|
#include "synth/AudioEngine.h"
|
||||||
|
|
||||||
@@ -19,6 +20,10 @@ public:
|
|||||||
explicit MainWindow(QWidget *parent = nullptr);
|
explicit MainWindow(QWidget *parent = nullptr);
|
||||||
~MainWindow();
|
~MainWindow();
|
||||||
|
|
||||||
|
protected:
|
||||||
|
void keyPressEvent(QKeyEvent* event) override;
|
||||||
|
void keyReleaseEvent(QKeyEvent* event) override;
|
||||||
|
|
||||||
private slots:
|
private slots:
|
||||||
void onIncrementClicked();
|
void onIncrementClicked();
|
||||||
void onResetClicked();
|
void onResetClicked();
|
||||||
@@ -42,4 +47,6 @@ private:
|
|||||||
void syncValueToUi(int value);
|
void syncValueToUi(int value);
|
||||||
|
|
||||||
AudioEngine* audio_ = nullptr;
|
AudioEngine* audio_ = nullptr;
|
||||||
|
KeyboardController keyboard_;
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
|
|
||||||
#include <QApplication>
|
#include <QApplication>
|
||||||
|
|
||||||
#include "MainWindow.h"
|
#include "MainWindow.h"
|
||||||
|
|
||||||
#include <iostream>
|
#include <iostream>
|
||||||
@@ -13,5 +14,7 @@ int main(int argc, char *argv[]) {
|
|||||||
MainWindow window;
|
MainWindow window;
|
||||||
window.show();
|
window.show();
|
||||||
|
|
||||||
return app.exec();
|
int status = app.exec();
|
||||||
|
|
||||||
|
return status;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ AudioEngine::AudioEngine() : synth_(params_) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// TODO: get audio configurations
|
// TODO: get audio configurations
|
||||||
|
|
||||||
synth_.setSampleRate(sampleRate_);
|
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) {
|
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_);
|
synth_.process(out, nFrames, sampleRate_);
|
||||||
|
|
||||||
return 0;
|
return 0;
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
#include <atomic>
|
#include <atomic>
|
||||||
|
|
||||||
#include "Synth.h"
|
#include "Synth.h"
|
||||||
|
#include "../KeyboardController.h"
|
||||||
|
|
||||||
class AudioEngine {
|
class AudioEngine {
|
||||||
|
|
||||||
@@ -19,8 +20,9 @@ public:
|
|||||||
// stops the audio stream.
|
// stops the audio stream.
|
||||||
void stop();
|
void stop();
|
||||||
|
|
||||||
// params getter
|
// getters
|
||||||
ParameterStore* parameters() { return ¶ms_; }
|
ParameterStore* parameters() { return ¶ms_; }
|
||||||
|
NoteQueue& noteQueue() { return noteQueue_; }
|
||||||
|
|
||||||
private:
|
private:
|
||||||
|
|
||||||
@@ -31,6 +33,7 @@ private:
|
|||||||
int32_t process(float* out, uint32_t nFrames);
|
int32_t process(float* out, uint32_t nFrames);
|
||||||
|
|
||||||
ParameterStore params_;
|
ParameterStore params_;
|
||||||
|
NoteQueue noteQueue_;
|
||||||
Synth synth_;
|
Synth synth_;
|
||||||
|
|
||||||
// TODO: id like a yml config file or something for these
|
// TODO: id like a yml config file or something for these
|
||||||
|
|||||||
@@ -8,6 +8,10 @@
|
|||||||
#define M_PI 3.14159265358979323846
|
#define M_PI 3.14159265358979323846
|
||||||
#endif
|
#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) {
|
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;
|
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) {
|
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
|
// yeah really only need to update this once per buffer if its ~6ms latency
|
||||||
updateParams();
|
updateParams();
|
||||||
|
|
||||||
|
float sampleOut = 0.0f;
|
||||||
|
|
||||||
for (uint32_t i = 0; i < nFrames; i++) {
|
for (uint32_t i = 0; i < nFrames; i++) {
|
||||||
|
|
||||||
|
// updates internal buffered parameters for smoothing
|
||||||
for(auto& p : params_) p.update(); // TODO: profile this
|
for(auto& p : params_) p.update(); // TODO: profile this
|
||||||
|
|
||||||
// based on oscillator frequency
|
// skip if no note is being played
|
||||||
float frequency = getParam(ParamId::Osc1Frequency); // this will come from a midi controller
|
// TODO: this will be handled by an idle envelope eventually
|
||||||
float phaseInc = 2.0f * M_PI * frequency / static_cast<float>(sampleRate);
|
// 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
|
// sample generation
|
||||||
float gain = getParam(ParamId::Osc1Gain);
|
float gain = getParam(ParamId::Osc1Gain);
|
||||||
float sample = std::sin(phase_) * gain;
|
sampleOut = std::sin(phase_) * gain;
|
||||||
|
|
||||||
// write to buffer
|
// write to buffer
|
||||||
out[2*i] = sample; // left
|
out[2*i] = sampleOut; // left
|
||||||
out[2*i+1] = sample; // right
|
out[2*i+1] = sampleOut; // right
|
||||||
|
|
||||||
// sampling business
|
// sampling business
|
||||||
phase_ += phaseInc;
|
phase_ += phaseInc;
|
||||||
|
|||||||
@@ -2,7 +2,9 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include "../ParameterStore.h"
|
#include "../ParameterStore.h"
|
||||||
|
#include "../NoteQueue.h"
|
||||||
|
|
||||||
|
#include <vector>
|
||||||
#include <atomic>
|
#include <atomic>
|
||||||
|
|
||||||
struct SmoothedParam {
|
struct SmoothedParam {
|
||||||
@@ -10,9 +12,7 @@ struct SmoothedParam {
|
|||||||
float target = 0.0f;
|
float target = 0.0f;
|
||||||
float gain = 0.001f;
|
float gain = 0.001f;
|
||||||
|
|
||||||
inline void update() {
|
inline void update() { current += gain * (target - current); }
|
||||||
current += gain * (target - current);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
class Synth {
|
class Synth {
|
||||||
@@ -24,6 +24,9 @@ public:
|
|||||||
// generates a buffer of audio samples nFrames long
|
// generates a buffer of audio samples nFrames long
|
||||||
void process(float* out, uint32_t nFrames, uint32_t sampleRate);
|
void process(float* out, uint32_t nFrames, uint32_t sampleRate);
|
||||||
|
|
||||||
|
// handles note events
|
||||||
|
void handleNoteEvent(const NoteEvent& event);
|
||||||
|
|
||||||
// sample rate setter
|
// sample rate setter
|
||||||
void setSampleRate(uint32_t sampleRate) { sampleRate_ = sampleRate; }
|
void setSampleRate(uint32_t sampleRate) { sampleRate_ = sampleRate; }
|
||||||
|
|
||||||
@@ -35,10 +38,24 @@ private:
|
|||||||
// small getter that abstracts all the static casts and such
|
// small getter that abstracts all the static casts and such
|
||||||
inline float getParam(ParamId);
|
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_;
|
const ParameterStore& paramStore_;
|
||||||
// smoothed params creates a buffer in case the thread controlling paramStore gets blocked
|
// smoothed params creates a buffer in case the thread controlling paramStore gets blocked
|
||||||
std::array<SmoothedParam, PARAM_COUNT> params_;
|
std::array<SmoothedParam, PARAM_COUNT> params_;
|
||||||
uint32_t sampleRate_;
|
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;
|
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_;
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user