diff --git a/CMakeLists.txt b/CMakeLists.txt index 034c287..54d8134 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -40,8 +40,13 @@ find_package(Qt6 REQUIRED COMPONENTS qt_standard_project_setup() add_library(sonobulus_core STATIC - src/synth/AudioEngine.cpp + src/ConfigService.cpp + src/LoggerService.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 Qt6::Core @@ -50,10 +55,10 @@ target_link_libraries(sonobulus_core PRIVATE rtmidi ) -message(STATUS "Looking for compiler dependencies: ${rtaudio_SOURCE_DIR}...") - target_include_directories(sonobulus_core PRIVATE + ${CMAKE_SOURCE_DIR}/src/ ${rtaudio_SOURCE_DIR} + ${rtmidi_SOURCE_DIR} ) qt_add_executable(sonobulus @@ -68,6 +73,7 @@ qt_add_qml_module(sonobulus target_include_directories(sonobulus PRIVATE ${rtaudio_SOURCE_DIR} + ${rtmidi_SOURCE_DIR} ) target_link_libraries(sonobulus PRIVATE diff --git a/src/ConfigService.cpp b/src/ConfigService.cpp new file mode 100644 index 0000000..e69de29 diff --git a/src/ConfigService.hpp b/src/ConfigService.hpp new file mode 100644 index 0000000..9f248c1 --- /dev/null +++ b/src/ConfigService.hpp @@ -0,0 +1,6 @@ + +#pragma once + +class ConfigService { + +}; diff --git a/src/LoggerService.cpp b/src/LoggerService.cpp new file mode 100644 index 0000000..e69de29 diff --git a/src/LoggerService.hpp b/src/LoggerService.hpp new file mode 100644 index 0000000..e69de29 diff --git a/src/synth/KeyboardController.cpp b/src/synth/KeyboardController.cpp index 87aa5c0..687b521 100644 --- a/src/synth/KeyboardController.cpp +++ b/src/synth/KeyboardController.cpp @@ -1,2 +1,67 @@ -#include "KeyboardController.hpp" \ No newline at end of file +#include "KeyboardController.hpp" + +#include +// #include +#include + +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 noteString = entry.second.as(); + + // // match the strings to ints + // uint8_t noteValue = notesNode[noteString].as(); + // uint32_t keyValue = keysNode[keyString].as(); + + // // 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() + }); +} \ No newline at end of file diff --git a/src/synth/KeyboardController.hpp b/src/synth/KeyboardController.hpp index 1fd8ce1..3103328 100644 --- a/src/synth/KeyboardController.hpp +++ b/src/synth/KeyboardController.hpp @@ -1,2 +1,29 @@ // the keyboard controller acts as an instrument input device for creating note events from a computer keyboard +#pragma once + +#include +#include + +#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 keymap_; + +}; diff --git a/src/synth/MidiController.cpp b/src/synth/MidiController.cpp index a71fd44..9a36575 100644 --- a/src/synth/MidiController.cpp +++ b/src/synth/MidiController.cpp @@ -1,2 +1,137 @@ #include "MidiController.hpp" + +#include +#include + +MidiController::MidiController(NoteQueue& queue) : noteQueue_(queue) { + try { + midiIn_ = std::make_unique(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* message, void* userData) { + auto* self = static_cast(userData); + if (!message || message->empty()) return; + self->handleMessage(*message); // pass to parsing function if valid +} + +void MidiController::handleMessage(const std::vector& 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(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(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(); + } +} \ No newline at end of file diff --git a/src/synth/MidiController.hpp b/src/synth/MidiController.hpp index 747ba7c..646fdfb 100644 --- a/src/synth/MidiController.hpp +++ b/src/synth/MidiController.hpp @@ -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 +#pragma once + +#include +#include +#include "NoteQueue.hpp" +#include + +class MidiController { +public: + + MidiController(NoteQueue& queue); + ~MidiController(); + + bool openDefaultPort(); + bool openPort(unsigned int index); + void close(); + +private: + + static void midiCallback(double deltaTime, std::vector* message, void* userData); + + void handleMessage(const std::vector& msg); + void handleSustain(bool down); + void noteOn(uint8_t note, uint8_t vel); + void noteOff(uint8_t note); + + std::unique_ptr midiIn_; + NoteQueue& noteQueue_; + + bool sustainDown_ = false; + std::unordered_set sustainedNotes_; + +}; diff --git a/src/synth/NoteQueue.cpp b/src/synth/NoteQueue.cpp index b58814c..d01d701 100644 --- a/src/synth/NoteQueue.cpp +++ b/src/synth/NoteQueue.cpp @@ -1,2 +1,28 @@ #include "NoteQueue.hpp" +#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) % 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; +} diff --git a/src/synth/NoteQueue.hpp b/src/synth/NoteQueue.hpp index bec4d74..80c9ba4 100644 --- a/src/synth/NoteQueue.hpp +++ b/src/synth/NoteQueue.hpp @@ -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 +#pragma once + +#include +#include +#include +#include + +#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 timestamp; +}; + +class NoteQueue { + +public: + NoteQueue() = default; + ~NoteQueue() = default; + + bool push(const NoteEvent& event); + bool pop(NoteEvent& event); + +private: + + std::array buffer_; + std::atomic head_{ 0 }; + std::atomic tail_{ 0 }; + +};