copy core from metabalus
This commit is contained in:
@@ -40,8 +40,13 @@ find_package(Qt6 REQUIRED COMPONENTS
|
|||||||
qt_standard_project_setup()
|
qt_standard_project_setup()
|
||||||
|
|
||||||
add_library(sonobulus_core STATIC
|
add_library(sonobulus_core STATIC
|
||||||
src/synth/AudioEngine.cpp
|
src/ConfigService.cpp
|
||||||
|
src/LoggerService.cpp
|
||||||
src/TimerComponent.cpp
|
src/TimerComponent.cpp
|
||||||
|
src/synth/AudioEngine.cpp
|
||||||
|
src/synth/KeyboardController.cpp
|
||||||
|
src/synth/MidiController.cpp
|
||||||
|
src/synth/NoteQueue.cpp
|
||||||
)
|
)
|
||||||
target_link_libraries(sonobulus_core PRIVATE
|
target_link_libraries(sonobulus_core PRIVATE
|
||||||
Qt6::Core
|
Qt6::Core
|
||||||
@@ -50,10 +55,10 @@ target_link_libraries(sonobulus_core PRIVATE
|
|||||||
rtmidi
|
rtmidi
|
||||||
)
|
)
|
||||||
|
|
||||||
message(STATUS "Looking for compiler dependencies: ${rtaudio_SOURCE_DIR}...")
|
|
||||||
|
|
||||||
target_include_directories(sonobulus_core PRIVATE
|
target_include_directories(sonobulus_core PRIVATE
|
||||||
|
${CMAKE_SOURCE_DIR}/src/
|
||||||
${rtaudio_SOURCE_DIR}
|
${rtaudio_SOURCE_DIR}
|
||||||
|
${rtmidi_SOURCE_DIR}
|
||||||
)
|
)
|
||||||
|
|
||||||
qt_add_executable(sonobulus
|
qt_add_executable(sonobulus
|
||||||
@@ -68,6 +73,7 @@ qt_add_qml_module(sonobulus
|
|||||||
|
|
||||||
target_include_directories(sonobulus PRIVATE
|
target_include_directories(sonobulus PRIVATE
|
||||||
${rtaudio_SOURCE_DIR}
|
${rtaudio_SOURCE_DIR}
|
||||||
|
${rtmidi_SOURCE_DIR}
|
||||||
)
|
)
|
||||||
|
|
||||||
target_link_libraries(sonobulus PRIVATE
|
target_link_libraries(sonobulus PRIVATE
|
||||||
|
|||||||
0
src/ConfigService.cpp
Normal file
0
src/ConfigService.cpp
Normal file
6
src/ConfigService.hpp
Normal file
6
src/ConfigService.hpp
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
class ConfigService {
|
||||||
|
|
||||||
|
};
|
||||||
0
src/LoggerService.cpp
Normal file
0
src/LoggerService.cpp
Normal file
0
src/LoggerService.hpp
Normal file
0
src/LoggerService.hpp
Normal file
@@ -1,2 +1,67 @@
|
|||||||
|
|
||||||
#include "KeyboardController.hpp"
|
#include "KeyboardController.hpp"
|
||||||
|
|
||||||
|
#include <iostream>
|
||||||
|
// #include <yaml-cpp/yaml.h>
|
||||||
|
#include <filesystem>
|
||||||
|
|
||||||
|
KeyboardController::KeyboardController(NoteQueue& queue, ConfigService* config) : queue_(queue), config_(config) {
|
||||||
|
|
||||||
|
// load keymap from config file
|
||||||
|
std::string filepath = "config/keymap.yaml";
|
||||||
|
filepath = std::filesystem::absolute(filepath).string();
|
||||||
|
// YAML::Node file;
|
||||||
|
try {
|
||||||
|
// file = YAML::LoadFile(filepath);
|
||||||
|
} catch(const std::exception& e) {
|
||||||
|
std::cerr << e.what() << std::endl;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// YAML::Node keymapNode = file["keymap"]; // node for string to string mappings
|
||||||
|
// YAML::Node notesNode = file["notes"]; // string to midi int mappings
|
||||||
|
// YAML::Node keysNode = file["keys"]; // string to qt key id mappings
|
||||||
|
|
||||||
|
// for each element in the keymap
|
||||||
|
// for (const auto& entry : keymapNode) {
|
||||||
|
|
||||||
|
// std::string keyString = entry.first.as<std::string>();
|
||||||
|
// std::string noteString = entry.second.as<std::string>();
|
||||||
|
|
||||||
|
// // match the strings to ints
|
||||||
|
// uint8_t noteValue = notesNode[noteString].as<uint8_t>();
|
||||||
|
// uint32_t keyValue = keysNode[keyString].as<uint32_t>();
|
||||||
|
|
||||||
|
// // insert into map
|
||||||
|
// keymap_.emplace(keyValue, noteValue);
|
||||||
|
// }
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
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,
|
||||||
|
std::chrono::high_resolution_clock::now()
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
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,
|
||||||
|
std::chrono::high_resolution_clock::now()
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -1,2 +1,29 @@
|
|||||||
|
|
||||||
// the keyboard controller acts as an instrument input device for creating note events from a computer keyboard
|
// the keyboard controller acts as an instrument input device for creating note events from a computer keyboard
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <QKeyEvent>
|
||||||
|
#include <unordered_map>
|
||||||
|
|
||||||
|
#include "NoteQueue.hpp"
|
||||||
|
#include "ConfigService.hpp"
|
||||||
|
|
||||||
|
// The keyboardcontroller handles user inputs from a keyboard and maps them to note events
|
||||||
|
class KeyboardController {
|
||||||
|
|
||||||
|
public:
|
||||||
|
explicit KeyboardController(NoteQueue& queue, ConfigService* config);
|
||||||
|
~KeyboardController() = default;
|
||||||
|
|
||||||
|
void handleKeyPress(QKeyEvent* e);
|
||||||
|
void handleKeyRelease(QKeyEvent* e);
|
||||||
|
|
||||||
|
private:
|
||||||
|
|
||||||
|
NoteQueue& queue_;
|
||||||
|
ConfigService* config_;
|
||||||
|
|
||||||
|
// keymap is key -> midi note id
|
||||||
|
std::unordered_map<int32_t, uint8_t> keymap_;
|
||||||
|
|
||||||
|
};
|
||||||
|
|||||||
@@ -1,2 +1,137 @@
|
|||||||
|
|
||||||
#include "MidiController.hpp"
|
#include "MidiController.hpp"
|
||||||
|
|
||||||
|
#include <iostream>
|
||||||
|
#include <chrono>
|
||||||
|
|
||||||
|
MidiController::MidiController(NoteQueue& queue) : noteQueue_(queue) {
|
||||||
|
try {
|
||||||
|
midiIn_ = std::make_unique<RtMidiIn>(RtMidi::LINUX_ALSA);
|
||||||
|
midiIn_->ignoreTypes(false, false, false);
|
||||||
|
} catch (RtMidiError& e) {
|
||||||
|
std::cout << "RtMidi init failed: " << e.getMessage() << std::endl;
|
||||||
|
}
|
||||||
|
// TODO: this still doesnt work on windows
|
||||||
|
}
|
||||||
|
|
||||||
|
MidiController::~MidiController() {
|
||||||
|
close();
|
||||||
|
}
|
||||||
|
|
||||||
|
// open the first for thats successful
|
||||||
|
bool MidiController::openDefaultPort() {
|
||||||
|
if (!midiIn_) return false;
|
||||||
|
if (midiIn_->getPortCount() == 0) {
|
||||||
|
std::cout << "No MIDI input ports available" << std::endl;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint32_t portCount = midiIn_->getPortCount();
|
||||||
|
std::cout << "Available MidiIn ports: " << portCount << std::endl;
|
||||||
|
for (int i = 0; i < portCount; i++) {
|
||||||
|
std::cout << "#" << i << " : " << midiIn_->getPortName(i) << std::endl;
|
||||||
|
|
||||||
|
if(openPort(i)) return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool MidiController::openPort(unsigned int index) {
|
||||||
|
if (!midiIn_) return false;
|
||||||
|
|
||||||
|
try {
|
||||||
|
midiIn_->openPort(index);
|
||||||
|
midiIn_->setCallback(&MidiController::midiCallback, this);
|
||||||
|
std::cout << "Opened MIDI port: " << midiIn_->getPortName(index) << std::endl;
|
||||||
|
return true;
|
||||||
|
} catch (RtMidiError& e) {
|
||||||
|
std::cout << "Midi Port error" << std::endl;
|
||||||
|
std::cerr << e.getMessage() << std::endl;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void MidiController::close() {
|
||||||
|
if (midiIn_ && midiIn_->isPortOpen()) {
|
||||||
|
midiIn_->closePort();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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<unsigned char>* message, void* userData) {
|
||||||
|
auto* self = static_cast<MidiController*>(userData);
|
||||||
|
if (!message || message->empty()) return;
|
||||||
|
self->handleMessage(*message); // pass to parsing function if valid
|
||||||
|
}
|
||||||
|
|
||||||
|
void MidiController::handleMessage(const std::vector<unsigned char>& msg) {
|
||||||
|
|
||||||
|
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; // "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; // note number
|
||||||
|
unsigned char vel = msg.size() > 2 ? msg[2] : 0; // velocity
|
||||||
|
|
||||||
|
// note on (velocity > 0)
|
||||||
|
if (status == 0x90 && vel > 0) {
|
||||||
|
noteOn(note, vel);
|
||||||
|
}
|
||||||
|
// 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);
|
||||||
|
|
||||||
|
noteQueue_.push({
|
||||||
|
NoteEventType::NoteOn,
|
||||||
|
static_cast<uint8_t>(note),
|
||||||
|
vel / 127.0f,
|
||||||
|
std::chrono::high_resolution_clock::now()
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// add note off event to noteQueue if no sustain active
|
||||||
|
void MidiController::noteOff(uint8_t note) {
|
||||||
|
if(sustainDown_) {
|
||||||
|
sustainedNotes_.insert(note);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
noteQueue_.push({
|
||||||
|
NoteEventType::NoteOff,
|
||||||
|
static_cast<uint8_t>(note),
|
||||||
|
0.0f,
|
||||||
|
std::chrono::high_resolution_clock::now()
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// if sustain goes from on->off, then noteOff all the active ntoes
|
||||||
|
void MidiController::handleSustain(bool down) {
|
||||||
|
if(down == sustainDown_) return;
|
||||||
|
|
||||||
|
sustainDown_ = down;
|
||||||
|
|
||||||
|
if(!sustainDown_) {
|
||||||
|
for(uint8_t note : sustainedNotes_) {
|
||||||
|
noteOff(note);
|
||||||
|
}
|
||||||
|
sustainedNotes_.clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,2 +1,35 @@
|
|||||||
|
|
||||||
// the midi controller handles interfacing a stream from a midi input device and processing them into note events for the synthesizer
|
// the midi controller handles interfacing a stream from a midi input device and processing them into note events for the synthesizer
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <RtMidi.h>
|
||||||
|
#include <memory>
|
||||||
|
#include "NoteQueue.hpp"
|
||||||
|
#include <unordered_set>
|
||||||
|
|
||||||
|
class MidiController {
|
||||||
|
public:
|
||||||
|
|
||||||
|
MidiController(NoteQueue& queue);
|
||||||
|
~MidiController();
|
||||||
|
|
||||||
|
bool openDefaultPort();
|
||||||
|
bool openPort(unsigned int index);
|
||||||
|
void close();
|
||||||
|
|
||||||
|
private:
|
||||||
|
|
||||||
|
static void midiCallback(double deltaTime, std::vector<unsigned char>* message, void* userData);
|
||||||
|
|
||||||
|
void handleMessage(const std::vector<unsigned char>& msg);
|
||||||
|
void handleSustain(bool down);
|
||||||
|
void noteOn(uint8_t note, uint8_t vel);
|
||||||
|
void noteOff(uint8_t note);
|
||||||
|
|
||||||
|
std::unique_ptr<RtMidiIn> midiIn_;
|
||||||
|
NoteQueue& noteQueue_;
|
||||||
|
|
||||||
|
bool sustainDown_ = false;
|
||||||
|
std::unordered_set<uint8_t> sustainedNotes_;
|
||||||
|
|
||||||
|
};
|
||||||
|
|||||||
@@ -1,2 +1,28 @@
|
|||||||
|
|
||||||
#include "NoteQueue.hpp"
|
#include "NoteQueue.hpp"
|
||||||
|
#include <iostream>
|
||||||
|
|
||||||
|
// 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) % SYNTH_NOTE_QUEUE_SIZE;
|
||||||
|
|
||||||
|
if(next == tail_.load(std::memory_order_relaxed)) return false; // full
|
||||||
|
|
||||||
|
buffer_[head] = event;
|
||||||
|
head_.store(next, std::memory_order_relaxed);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// take event from noteQueue, called by synth
|
||||||
|
bool NoteQueue::pop(NoteEvent& event) {
|
||||||
|
size_t tail = tail_.load(std::memory_order_relaxed);
|
||||||
|
|
||||||
|
if(tail == head_.load(std::memory_order_acquire)) return false; // empty
|
||||||
|
|
||||||
|
event = buffer_[tail];
|
||||||
|
tail_.store((tail + 1) % SYNTH_NOTE_QUEUE_SIZE, std::memory_order_release);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,2 +1,39 @@
|
|||||||
|
|
||||||
// the note queue is a wrapper for a FIFO array to which the midi/keyboard controller enters note events into and the synthesizer consumes from
|
// the note queue is a wrapper for a FIFO array to which the midi/keyboard controller enters note events into and the synthesizer consumes from
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <array>
|
||||||
|
#include <atomic>
|
||||||
|
#include <cstdint>
|
||||||
|
#include <chrono>
|
||||||
|
|
||||||
|
#define SYNTH_NOTE_QUEUE_SIZE 128
|
||||||
|
|
||||||
|
enum class NoteEventType {
|
||||||
|
NoteOn,
|
||||||
|
NoteOff
|
||||||
|
};
|
||||||
|
|
||||||
|
struct NoteEvent {
|
||||||
|
NoteEventType type; // noteOn or noteOff
|
||||||
|
uint8_t note; // 0-128, a keyboard goes 0-87
|
||||||
|
float velocity; // 0-1, from a midi instrument its 0-127 though
|
||||||
|
std::chrono::time_point<std::chrono::high_resolution_clock> timestamp;
|
||||||
|
};
|
||||||
|
|
||||||
|
class NoteQueue {
|
||||||
|
|
||||||
|
public:
|
||||||
|
NoteQueue() = default;
|
||||||
|
~NoteQueue() = default;
|
||||||
|
|
||||||
|
bool push(const NoteEvent& event);
|
||||||
|
bool pop(NoteEvent& event);
|
||||||
|
|
||||||
|
private:
|
||||||
|
|
||||||
|
std::array<NoteEvent, SYNTH_NOTE_QUEUE_SIZE> buffer_;
|
||||||
|
std::atomic<size_t> head_{ 0 };
|
||||||
|
std::atomic<size_t> tail_{ 0 };
|
||||||
|
|
||||||
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user