Compare commits

..

2 Commits

Author SHA1 Message Date
cfc8cb2b51 tweaks 2026-06-14 12:48:42 -05:00
397e1fe7dc midi checkpoint 2026-06-13 22:04:27 -05:00
9 changed files with 80 additions and 15 deletions

View File

@@ -111,6 +111,7 @@ if (WIN32)
TARGET sonobulus POST_BUILD
COMMAND ${CMAKE_COMMAND} -E copy_if_different
$<TARGET_FILE:rtaudio>
$<TARGET_FILE:rtmidi>
$<TARGET_FILE:libconfig++>
$<TARGET_FILE_DIR:sonobulus>
)

View File

@@ -67,9 +67,12 @@ $ ./build/sonobulus
```
## Configurations
There is a plan eventually to use config files so app behavior can be tweaked without needing to recompile. Will flesh out more once more of the app's structure takes shape.
Configuration files are located in the config directory. They offer options to change the program's settings without recompiling. Eventually I might make a user's guide for the configurations, but there's not too much there right now.
## Instrument Profiles
Later into the app's development I have this vision: the state-space model of an instrument can be saved to a file. The app can allow parameter tweaking and re-saving of an instruments matrices.
Far far into the future there may be a dynamic process for constructing your own instrument models and a way for the app to load and play those instruments. At the end of the day, a state-space
model is just a set of 2 n-dimensional matrix equations and a profile would only need to be able to represent each element in the matrices.
## Note for Windows MIDI
If you intend to hook this instrument to a DAW or some kind of application that outputs midi (I use musescore), Windows takes an extra step (Linux and Mac works fine AFAIK). You need a program that creates a virtual MIDI port, like loopMIDI in order to hook the interfaces between apps. Installation is easy and just requires having the program open with one virtual midi port available. Then, attach the midi output in your DAW to the loopMIDI port and the synthesizer should pick it up on startup.

35
scripts/string_model.py Normal file
View File

@@ -0,0 +1,35 @@
import scipy.signal as sig
import matplotlib.pyplot as plt
import numpy as np
# simple first order step response simulation
# www.halvorsen.blog/documents/programming/python/resources/powerpoints/State Space Models with Python.pdf
# simulation Parameters
x0 = [0, 0]
start = 0
stop = 30
step = 1
t = np.arange(start,stop,step)
K = 3
T = 4
# state-space Model
A = [[-1/T, 0], [0, 0]]
B = [[K/T], [0]]
C = [[1, 0]]
D = 0
sys = sig.StateSpace(A, B, C, D)
# step Response
t, y = sig.step(sys, x0, t)
# plotting
plt.plot(t, y)
plt.title("Step Response")
plt.xlabel("t")
plt.ylabel("y")
plt.grid()
plt.show()

View File

@@ -11,6 +11,7 @@
#include "synth/AudioEngine.hpp"
#include "synth/KeyboardController.hpp"
#include "synth/Scope.hpp"
#include "synth/MidiController.hpp"
int main(int argc, char* argv[]) {
@@ -24,6 +25,7 @@ int main(int argc, char* argv[]) {
NoteQueue queue = NoteQueue();
ScopeBuffer scopeBuffer = ScopeBuffer(512);
KeyboardController keyboard(&config, &logger, &queue);
MidiController midi(&config, &logger, &queue);
Synth synth(&config, &logger, &scopeBuffer, &queue);
// audio synthesizer doohickey
@@ -33,12 +35,11 @@ int main(int argc, char* argv[]) {
// attach backend gui components
qmlRegisterType<TimerComponent>("AppDemo", 1, 0, "TimerComponent");
qmlRegisterType<Scope>("AppDemo", 1, 0, "Scope");
// attach backend services to qml
engine.rootContext()->setContextProperty("keyboardController", &keyboard);
engine.rootContext()->setContextProperty("scopeBuffer", &scopeBuffer);
// load qml
//engine.loadFromModule("sonobulus", "Main");
//engine.load(QUrl("qrc:/Main.qml"));
engine.load(QUrl::fromLocalFile("ui/Main.qml")); // ugh
if(engine.rootObjects().isEmpty()) {

View File

@@ -28,7 +28,7 @@ bool Instrument::isActive() {
float Instrument::process(bool& scopeTrigger) {
if(active_ && envelope_ < 1.0f) envelope_ += 0.01f;
if(!active_ && envelope_ > 0.0f) envelope_ -= 0.01f;
if(!active_ && envelope_ > 0.0f) envelope_ -= 0.0004f;
phase_ += phaseIncrement_;
if(phase_ > 2.0f * pi) {
@@ -37,6 +37,10 @@ float Instrument::process(bool& scopeTrigger) {
}
if(!isActive()) return 0.0f;
return sin(phase_) * envelope_;
// float sample = sin(phase_);
float sample = phase_ / pi - 1.0f; // saw
return sample * envelope_;
}

View File

@@ -4,14 +4,20 @@
#include <iostream>
#include <chrono>
MidiController::MidiController(NoteQueue& queue) : noteQueue_(queue) {
MidiController::MidiController(ConfigService* config, LoggerService* logger, NoteQueue* queue) :
config_(config), logger_(logger), noteQueue_(queue) {
try {
#ifdef WIN32
midiIn_ = std::make_unique<RtMidiIn>(RtMidi::WINDOWS_MM);
#else
midiIn_ = std::make_unique<RtMidiIn>(RtMidi::LINUX_ALSA);
#endif
midiIn_->ignoreTypes(false, false, false);
} catch (RtMidiError& e) {
std::cout << "RtMidi init failed: " << e.getMessage() << std::endl;
}
// TODO: this still doesnt work on windows
openDefaultPort();
}
MidiController::~MidiController() {
@@ -100,7 +106,7 @@ void MidiController::handleMessage(const std::vector<unsigned char>& msg) {
void MidiController::noteOn(uint8_t note, uint8_t vel) {
sustainedNotes_.erase(note);
noteQueue_.push({
noteQueue_->push({
NoteEventType::NoteOn,
static_cast<uint8_t>(note),
vel / 127.0f,
@@ -114,7 +120,7 @@ void MidiController::noteOff(uint8_t note) {
sustainedNotes_.insert(note);
return;
}
noteQueue_.push({
noteQueue_->push({
NoteEventType::NoteOff,
static_cast<uint8_t>(note),
0.0f,

View File

@@ -4,13 +4,16 @@
#include <RtMidi.h>
#include <memory>
#include "NoteQueue.hpp"
#include <unordered_set>
#include "NoteQueue.hpp"
#include "ConfigService.hpp"
#include "LoggerService.hpp"
class MidiController {
public:
MidiController(NoteQueue& queue);
MidiController(ConfigService* config, LoggerService* logger, NoteQueue* queue);
~MidiController();
bool openDefaultPort();
@@ -27,7 +30,9 @@ private:
void noteOff(uint8_t note);
std::unique_ptr<RtMidiIn> midiIn_;
NoteQueue& noteQueue_;
ConfigService* config_;
LoggerService* logger_;
NoteQueue* noteQueue_;
bool sustainDown_ = false;
std::unordered_set<uint8_t> sustainedNotes_;

View File

@@ -10,12 +10,19 @@ Synth::Synth(ConfigService* config, LoggerService* logger, ScopeBuffer* scope, N
void Synth::handleNoteEvent(const NoteEvent& event) {
Voice* v = findVoiceByNote(event.note);
if(v != nullptr) v->noteOff();
// Voice* v = findVoiceByNote(event.note);
// if(v != nullptr) v->noteOff();
// stop all voices that are currently playing this note
for(Voice& v : voices_) {
if(v.isActive() && v.note() == event.note) {
v.noteOff();
}
}
if(event.type == NoteEventType::NoteOn) {
v = findFreeVoice();
Voice* v = findFreeVoice();
if(v != nullptr) v->noteOn(event.note, event.velocity);
}

View File

@@ -54,6 +54,9 @@ ApplicationWindow {
width: 600
height: 300
anchors.horizontalCenter: parent.horizontalCenter
anchors.verticalCenter: parent.verticalCenter
anchors.verticalCenterOffset: -200
// this timer triggers a constant update on the scope's canvas, calls its draw() each time
Timer {