Compare commits

..

10 Commits

Author SHA1 Message Date
dd9bd5511e model checkpoint 2026-06-16 22:42:49 -05:00
c7432320fe math 2026-06-16 20:13:08 -05:00
5bb1bbe56c basic python piano string state space system 2026-06-14 23:38:30 -05:00
4643c681f3 fix midi bug that caused the synth to continue to play notes after an interface stops output 2026-06-14 19:09:59 -05:00
f4d855b36b sync 2026-06-14 15:34:51 -05:00
cfc8cb2b51 tweaks 2026-06-14 12:48:42 -05:00
397e1fe7dc midi checkpoint 2026-06-13 22:04:27 -05:00
e9fff9691b add visual scope 2026-06-13 19:31:46 -05:00
dbc1db37e1 scope checkpoint 2026-06-13 16:36:33 -05:00
fcc24c5e3e add simple envelope to stop popping 2026-06-13 12:26:57 -05:00
20 changed files with 573 additions and 69 deletions

View File

@@ -27,6 +27,11 @@ FetchContent_Declare(
GIT_REPOSITORY https://github.com/hyperrealm/libconfig.git GIT_REPOSITORY https://github.com/hyperrealm/libconfig.git
GIT_TAG v1.8.2 GIT_TAG v1.8.2
) )
FetchContent_Declare(
eigen
GIT_REPOSITORY git@gitlab.com:libeigen/eigen.git
GIT_TAG 5.0
)
FetchContent_MakeAvailable(rtaudio) FetchContent_MakeAvailable(rtaudio)
FetchContent_MakeAvailable(rtmidi) FetchContent_MakeAvailable(rtmidi)
FetchContent_MakeAvailable(libconfig) FetchContent_MakeAvailable(libconfig)
@@ -106,6 +111,7 @@ if (WIN32)
TARGET sonobulus POST_BUILD TARGET sonobulus POST_BUILD
COMMAND ${CMAKE_COMMAND} -E copy_if_different COMMAND ${CMAKE_COMMAND} -E copy_if_different
$<TARGET_FILE:rtaudio> $<TARGET_FILE:rtaudio>
$<TARGET_FILE:rtmidi>
$<TARGET_FILE:libconfig++> $<TARGET_FILE:libconfig++>
$<TARGET_FILE_DIR:sonobulus> $<TARGET_FILE_DIR:sonobulus>
) )

View File

@@ -29,7 +29,7 @@ to produce somewhat well-sounding instruments and music performance.
to external midi controllers/apps (like musescore) to external midi controllers/apps (like musescore)
- [ ] Create a UI scope to visualize the synthesized composite waveform - [ ] Create a UI scope to visualize the synthesized composite waveform
- [ ] Check cross-platform combatibility for Windows & Linux, especially MIDI interfacing - [ ] Check cross-platform combatibility for Windows & Linux, especially MIDI interfacing
- [ ] Checkpoint at a rudimentary keyboard instrument producing a basic sine output - [x] Checkpoint at a rudimentary keyboard instrument producing a basic sine output
- [ ] Will flesh out future goals when I do the math on how complicated implementing - [ ] Will flesh out future goals when I do the math on how complicated implementing
state-space modelling in c++ is state-space modelling in c++ is
@@ -67,9 +67,12 @@ $ ./build/sonobulus
``` ```
## Configurations ## 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 ## 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. 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 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. 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.

View File

@@ -10,7 +10,7 @@ Logger = (
"Error" "Error"
); );
ShowTime = true; ShowTime = false;
ShowSourceTrace = false; ShowSourceTrace = false;
CoutEnabled = true; CoutEnabled = true;

36
scripts/first_order.py Normal file
View File

@@ -0,0 +1,36 @@
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()

168
scripts/string_model_de.py Normal file
View File

@@ -0,0 +1,168 @@
import matplotlib.pyplot as plt
import numpy as np
import math
# https://people.cs.uchicago.edu/~ridg/stabil/pianostring.pdf
# a differential equations model for a piano string
# eq 1: governing wave equation
# d2y/dt2 = c^2*d2y/dx2 - stiffness*c^2*L^2*d4y/dx4 - 2*b_1*dy/dt + 2*b_3*d3y/dt3 + f(x, x_0, t)
# where y = string's transverse displacement, b_1, b_3 = damping coefficients, f = force density
# c = sqrt(T/mu), transverse wave velocity , T = string tension, mu = string linear mass density)
# eq 2: string stiffness
# stiffness = K^2*(E*S/(T*L^2))
# where K = string's radius of gyration (r/2 for a circular string), E = string's Young's modulus,
# S = cross sectional string area, T = string tension, L = string length
# eq 3: decay
# sigma = 1/tau = b_1 + b_3*omega^2
# where sigma = decay rate, tau = decay time, omega = angular frequency
# eq 4: excitation
# f(x, x_0, t) = f_H(t) * g(x, x_0)
# where f_H(t) = hammer force, g(x, x_0) = hammer dimensional effect
# eq 5: hammer time history
# read the paper if you care, useful only for derivation
# eq 6: power law
# F_H(t) = K*|eta(t) - y(x_0, t)|^p
# where eta(t) is the transverse displacement of the hammer head
# and p = stiffness nonlinear exponent
# eq 7: hammer displacement
# M_H*d2eta/dt2 = -F_H(t)
# where M_H is the mass of the hamemr head
# eq 8: boundary conditions
# y(0, t) = y(L, t) = 0, fixed ends dont move
# d2y/dx2(0, t) = d2y/dx2(L, t) = 0, displacement along the string approaching the ends is continuous
# discrete time implementation
# eq 9: continuous to discrete
# y(x, t) -> y(x_i, t_n) -> y(i, n)
# divide the string into i segments and iterate over n timesteps
# where x_i = delta_x * i and t_n = delta_t * n
# eq 10: recurrence derivation
# y(i, n+1) = a_1*y(i, n) + a_2*y(i, n-1) + a_3*[y(i+1, n) + y(i-1, n)] + a_4*[y(i+2,n) + y(i-2,n)]
# + a_5*[y(i+1, n-1) + y(i-1, n-1) + y(i, n-2)]
# + [delta_t^2 * N*F_H(n) * g(i, i_0)]/M_S
# where a_1 through a_5 are defined as follows:
# a_1 = [2 - 2*r^2 + b_1/delta_t - 6*stiffness*N^2*r^2]/D
# a_2 = [-1 + b_1*delta_t + 2*b_3/delta_t]/D
# a_3 = [r^2*(1 + 4*stiffness*N^2)]/D
# a_4 = [b_3/delta_t - stiffness*N^2*r^2]/D
# a_5 = [-b_3/delta_t]/D
# where D = 1 + b_1*delta_t + 2*b_3/delta_t
# and r = c*delta_t/delta_x
# eq 11: stability condition
# N_max = sqrt{[-1 + sqrt(1+16*stiffness*gamma^2)]/(8*stiffness)}
# where
# eq 12: idk what gamma represents
# gamma = f_e/(2*f_1), f_e = sampling frequency and f_1 = fundamental frequency
# or
# eq 13: if neglecting stiffness
# N_max = gamma
# eq 14: rest condition
# y(i, 0) = 0
# eq 15: discrete hammer displacement
# at t = delta_t (n = 1)
# eta(1) = V_H_0 * delta_t
# eq 16: discrete truncated taylor series
# y(i, 1) = [y(i + 1, 0) + y(i - 1, 0)]/2
# eq 17: hammer force exertion
# F_H(1) = K*|eta(1) - y(i_0, 1)|^p
# eq 18: string displacement iteration
# y(i, 2) = y(i-1, 1)] + y(i + 1, 1) - y(i, 0) + [delta_t^2 * N*F_H(1) * g(i, i_0)]/M_S
# eq 19: hammer displacement iteration
# eta(2) = 2*eta(1) - eta(0) - [delta_t^2 * F_H(1)]/M_H
# eq 20: hammer force iteration
# F_H(2) = K*|eta(2) - y(i_0, 2)|^p
# eq 21: hammer rest condition
# eta(n + 1) < y(i_0, n + 1)
# eq 22: spacial boundary conditions
# y(0, n) = y(N, n) = 0
# eq 23: temporal boundary conditions
# y(-1, n) = -y(1, n)
# y(N + 1, n) = -y(N - 1, n)
# string parameters
E = 1 # youngs modulus
mu = 1 # linear mass density
kappa = 1 # radius of gyration
L = 1 # string length
M_S = mu*L # string mass
S = 1 # string cross sectional area
T = 1 # string tension
c = math.sqrt(T/mu) # transverse wave velocity
stiffness = 1 # string stiffness parameter
sigma = 1 # decay rate
tau = 1/sigma # decay time
omega = 1 # angular frequency
# hammer parameters
M_H = 1 # hammer mass
HSMR = M_H/M_S # hammer-mass string ratio
V_H_0 = 1 # initial hammer velocity at t=0
x_0 = 1 # distance of hammer from agraffe
alpha = x_0 / L # relative hammer striking position
# simulation parameters
f1 = 440 # fundamental frequency
f_e = 44100 # sampling frequency
N = 100 # number of string segments
delta_t = 1/f_e # time step
delta_x = L/N # spatial step
H = f_e * 10 # length of simulation in time
# empirical constants
b_1 = 1 # some constant
b_3 = 1 # some constant
K = 1 # hammer stiffness
p = 1 # stiffness nonlinear exponent
# derived components
D = 1 + b_1*delta_t + 2*b_3/delta_t
r = c*delta_t/delta_x
a_1 = (2 - 2*r**2 + b_1/delta_t - 6*stiffness*N**2*r**2)/D
a_2 = (-1 + b_1*delta_t + 2*b_3/delta_t)/D
a_3 = (r**2*(1 + 4*stiffness*N**2))/D
a_4 = (b_3/delta_t - stiffness*N**2*r**2)/D
a_5 = (-b_3/delta_t)/D
x = [0] * N # current string position
last_x1 = [0] * N # string position from last timestep
last_x2 = [0] * N # string position from two timesteps ago
x_next = [0] * N # buffer for next string position
x_out = np.zeros(H) # taking this as the sound output at some artibraty point along the string
t = np.arange(0, H/f_e, delta_t)
for n in range(H): # time
for i in range(N): # space
x_out[n] = math.sin(n/10000)
# plotting
plt.plot(t, x_out)
plt.title("Step Response")
plt.xlabel("t")
plt.ylabel("y")
plt.grid()
plt.show()

View File

@@ -0,0 +1,82 @@
import scipy.signal as sig
import matplotlib.pyplot as plt
import numpy as np
import math
# simple first order step response simulation
# www.halvorsen.blog/documents/programming/python/resources/powerpoints/State Space Models with Python.pdf
# simulation Parameters
# string pde (wave equation)
# rho*A*d2y(x,t)/dt2 + c*dy(x,t)/dt - T*d2y(x,t)/dx2 = f(x,t)
# where rho*A = 1 dimensional string density, c = string damping, T = string tension, y(x,t) = displacement, f(x,t) = exitation force
# assume fixed ends: y(0, t) = y(L, t) = 0
# displacement y(x,t) can be represented as the sum from n=1 to N of qn(t)*sin(n*pi*x/L) where q_n(t) is the modal coordinate of mode n (displacement is the sum of all resonating modes)
# therefore:
# q..n + 2*zeta_n*omega_n*q.n + omega_n2*q_n = b_n*u(t)
# where omega_n = n*pi/L * sqrt(T/(rho*A)) for an ideal string
# and b_n = sin(n*pi*x_h/L) but assuming the hammer strikes at midpoint, x_h = L/2 ( b_n = sin(n*pi/2) )
# state space representation:
# each mode:
#「 q_n_dot | =「 0 1 | 「 q_n | + 「 0 |* u
# L q_n_dot_dot 」= L -omega_n^2 -2*zeta_n*omega_n 」 L q_ndot 」 L b_n 」
# and x_dot = Ax + Bu
# where A = diagonal matrix of A_n and B = [ 0 b_1 0 b_2 ... b_n]^T
# lets start with 3 nodes where the string is tuned to 440hz (we'll get to arbitrary modes evantually)
f_1 = 440 # fundamental frequency
def f_n(n):
return f_1 * n
def omega_n(n):
return 2*math.pi*f_n(n) # a cooler option would be omega_n = c*n*omega_1*sqrt(1+B*n^2) to factor in string stiffness to its vibration mode
# x = [ q1, q1dot, q2, q2dot, q3, q3dot ]^T < --state vector
omega_1 = omega_n(1)
omega_2 = omega_n(2)
omega_3 = omega_n(3)
zeta_1 = 0.0001 # i guessed
zeta_2 = 2 * zeta_1
zeta_3 = 3 * zeta_1
A = [
[ 0, 1, 0, 0, 0, 0],
[-omega_1**2, -2*zeta_1*omega_1, 0, 0, 0, 0],
[ 0, 0, 0, 1, 0, 0],
[ 0, 0, -omega_2**2, -2*zeta_2*omega_2, 0, 0],
[ 0, 0, 0, 0, 0, 1],
[ 0, 0, 0, 0, -omega_3**2, -2*zeta_3*omega_3]
] # isnt this formatting gorgeous
B = [ [0], [0.707], [0], [0], [0], [-0.707] ]
c_1 = 0.001
c_2 = c_1 / 2
c_3 = c_1 / 3
C = [[-c_1*omega_1**2, -2*c_1*zeta_1*omega_1, -c_2*omega_2**2, -2*c_2*zeta_2*omega_2, -c_3*omega_3**2, -2*c_3*zeta_3*omega_3]]
D = c_1 * 0.707 + c_2 * 0 + c_3 * (-0.707)
# input
#v_h = 100 # hammer velocity
#u = v_h * impulse(t)
x0 = [[0], [0], [0], [0], [0], [0]]
start = 0
stop = 10
step = 1/44100
t = np.arange(start,stop,step)
sys = sig.StateSpace(A, B, C, D)
# step Response
t, y = sig.impulse(sys, T=t)
# plotting
plt.plot(t, y)
plt.title("Step Response")
plt.xlabel("t")
plt.ylabel("y")
plt.grid()
plt.show()

View File

@@ -10,7 +10,8 @@
#include "ConfigService.hpp" #include "ConfigService.hpp"
#include "synth/AudioEngine.hpp" #include "synth/AudioEngine.hpp"
#include "synth/KeyboardController.hpp" #include "synth/KeyboardController.hpp"
//#include "synth/ScopeBuffer.hpp" #include "synth/Scope.hpp"
#include "synth/MidiController.hpp"
int main(int argc, char* argv[]) { int main(int argc, char* argv[]) {
@@ -21,10 +22,11 @@ int main(int argc, char* argv[]) {
// create app objects // create app objects
ConfigService config = ConfigService("config/sonobulus.cfg"); ConfigService config = ConfigService("config/sonobulus.cfg");
LoggerService logger = LoggerService(&config, "Engine"); LoggerService logger = LoggerService(&config, "Engine");
NoteQueue queue = NoteQueue(); NoteQueue queue = NoteQueue(&config, &logger);
//ScopeBuffer scope = ScopeBuffer(); ScopeBuffer scopeBuffer = ScopeBuffer(&config, &logger, 2048);
KeyboardController keyboard(&config, &logger, &queue); KeyboardController keyboard(&config, &logger, &queue);
Synth synth(&config, &logger, nullptr, &queue); MidiController midi(&config, &logger, &queue);
Synth synth(&config, &logger, &scopeBuffer, &queue);
// audio synthesizer doohickey // audio synthesizer doohickey
AudioEngine audioEngine = AudioEngine(&config, &logger, &synth); AudioEngine audioEngine = AudioEngine(&config, &logger, &synth);
@@ -32,16 +34,16 @@ int main(int argc, char* argv[]) {
// attach backend gui components // attach backend gui components
qmlRegisterType<TimerComponent>("AppDemo", 1, 0, "TimerComponent"); 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("keyboardController", &keyboard);
// adds the TimerComponent type (exposed in qml as "TimerComponent") to the module named"AppDemo" (numbers mean version 1.0) engine.rootContext()->setContextProperty("scopeBuffer", &scopeBuffer);
// load qml // load qml
//engine.loadFromModule("sonobulus", "Main");
//engine.load(QUrl("qrc:/Main.qml"));
engine.load(QUrl::fromLocalFile("ui/Main.qml")); // ugh engine.load(QUrl::fromLocalFile("ui/Main.qml")); // ugh
if(engine.rootObjects().isEmpty()) { if(engine.rootObjects().isEmpty()) {
std::cout << "engine is empty" << std::endl; logger.log("Main", LogFlag::Error, "Engine is empty.");
return -1; return -1;
} }

View File

@@ -9,11 +9,8 @@
AudioEngine::AudioEngine(ConfigService* config, LoggerService* logger, Synth* synth) : config_(config), logger_(logger), synth_(synth) { AudioEngine::AudioEngine(ConfigService* config, LoggerService* logger, Synth* synth) : config_(config), logger_(logger), synth_(synth) {
if(audioDevice_.getDeviceCount() < 1) { if(audioDevice_.getDeviceCount() < 1) {
std::cout << "No audio devices found" << std::endl; logger_->log("Audio", LogFlag::Error, "No audio devices found.");
} }
if(logger_ == nullptr) std::cout << "err: logger nullptr" << std::endl;
} }
AudioEngine::~AudioEngine() { AudioEngine::~AudioEngine() {
@@ -33,13 +30,13 @@ bool AudioEngine::start() {
RtAudioErrorType status = audioDevice_.openStream(&params, nullptr, RTAUDIO_FLOAT32, sampleRate_, &bufferFrames_, &AudioEngine::audioCallback, this, &options); RtAudioErrorType status = audioDevice_.openStream(&params, nullptr, RTAUDIO_FLOAT32, sampleRate_, &bufferFrames_, &AudioEngine::audioCallback, this, &options);
if(status != RTAUDIO_NO_ERROR) { if(status != RTAUDIO_NO_ERROR) {
std::cout << "Error opening RtAudio stream" << std::endl; logger_->log("Audio", LogFlag::Error, "Error opening RtAudio stream.");
return false; return false;
} }
status = audioDevice_.startStream(); status = audioDevice_.startStream();
if(status != RTAUDIO_NO_ERROR) { if(status != RTAUDIO_NO_ERROR) {
std::cout << "Error starting RtAudio stream" << std::endl; logger_->log("Audio", LogFlag::Error, "Error starting RtAudio stream.");
return false; return false;
} }

View File

@@ -10,11 +10,10 @@ Instrument::Instrument(ConfigService* config, LoggerService* logger) :
void Instrument::noteOn(float frequency, float velocity) { void Instrument::noteOn(float frequency, float velocity) {
// std::string msg = "NoteOn Frequency = " + std::to_string(frequency);
// if(logger_ != nullptr) logger_->log("Instrument", LogFlag::Debug, msg);
phaseIncrement_ = 2.0f * pi * frequency / sampleRate_; phaseIncrement_ = 2.0f * pi * frequency / sampleRate_;
envelope_ += 0.01f; // so it triggers as active
active_ = true; active_ = true;
} }
@@ -23,14 +22,27 @@ void Instrument::noteOff() {
} }
bool Instrument::isActive() { bool Instrument::isActive() {
return active_; return (envelope_ > 0.0f);
} }
float Instrument::process(bool& scopeTrigger) { float Instrument::process(bool& scopeTrigger) {
phase_ += phaseIncrement_; if(active_ && envelope_ < 1.0f) envelope_ += 0.01f;
if(phase_ > 2.0f * pi) phase_ -= 2.0f * pi; if(!active_ && envelope_ > 0.0f) envelope_ -= 0.0004f;
return sin(phase_); if(!isActive()) return 0.0f;
phase_ += phaseIncrement_;
if(phase_ > 2.0f * pi) {
phase_ -= 2.0f * pi;
scopeTrigger = true;
}
// float sample = sin(phase_);
targetSample = phase_ / pi - 1.0f; // saw
currentSample = (1.0f - responsiveness_) * currentSample + responsiveness_ * targetSample;
return currentSample * envelope_;
} }

View File

@@ -32,5 +32,10 @@ private:
static constexpr float pi = 3.14159265358979323846f; static constexpr float pi = 3.14159265358979323846f;
float phase_ = 0.0f; float phase_ = 0.0f;
float phaseIncrement_ = 0.0f; float phaseIncrement_ = 0.0f;
float envelope_ = 0.0f;
float targetSample = 0.0f;
float currentSample = 0.0f;
static constexpr float responsiveness_ = 0.1f;
}; };

View File

@@ -4,14 +4,21 @@
#include <iostream> #include <iostream>
#include <chrono> #include <chrono>
MidiController::MidiController(NoteQueue& queue) : noteQueue_(queue) { MidiController::MidiController(ConfigService* config, LoggerService* logger, NoteQueue* queue) :
config_(config), logger_(logger), noteQueue_(queue) {
try { try {
#ifdef WIN32
midiIn_ = std::make_unique<RtMidiIn>(RtMidi::WINDOWS_MM);
#else
midiIn_ = std::make_unique<RtMidiIn>(RtMidi::LINUX_ALSA); midiIn_ = std::make_unique<RtMidiIn>(RtMidi::LINUX_ALSA);
#endif
midiIn_->ignoreTypes(false, false, false); midiIn_->ignoreTypes(false, false, false);
} catch (RtMidiError& e) { } catch (RtMidiError& e) {
std::cout << "RtMidi init failed: " << e.getMessage() << std::endl; std::string msg = "RtMidi init failed: " + e.getMessage();
logger_->log("MIDI", LogFlag::Warning, msg);
} }
// TODO: this still doesnt work on windows
openDefaultPort();
} }
MidiController::~MidiController() { MidiController::~MidiController() {
@@ -19,18 +26,21 @@ MidiController::~MidiController() {
} }
// open the first for thats successful // open the first for thats successful
// we could also add an option in the config for a preferred port name
bool MidiController::openDefaultPort() { bool MidiController::openDefaultPort() {
if (!midiIn_) return false; if (!midiIn_) return false;
if (midiIn_->getPortCount() == 0) { if (midiIn_->getPortCount() == 0) {
std::cout << "No MIDI input ports available" << std::endl; logger_->log("MIDI", LogFlag::Warning, "No MIDI input ports available.");
return false; return false;
} }
// TODO: the ui will eventually need this class to expose the available midi ports so they can be chosen dynamically
uint32_t portCount = midiIn_->getPortCount(); uint32_t portCount = midiIn_->getPortCount();
std::cout << "Available MidiIn ports: " << portCount << std::endl; std::string msg = "Available MidiIn ports: " + std::to_string(portCount);
logger_->log("MIDI", LogFlag::Info, msg);
for (int i = 0; i < portCount; i++) { for (int i = 0; i < portCount; i++) {
std::cout << "#" << i << " : " << midiIn_->getPortName(i) << std::endl; msg = "\t#" + std::to_string(i) + " : " + midiIn_->getPortName(i);
logger_->log("MIDI", LogFlag::Info, msg);
if(openPort(i)) return true; if(openPort(i)) return true;
} }
@@ -43,11 +53,13 @@ bool MidiController::openPort(unsigned int index) {
try { try {
midiIn_->openPort(index); midiIn_->openPort(index);
midiIn_->setCallback(&MidiController::midiCallback, this); midiIn_->setCallback(&MidiController::midiCallback, this);
std::cout << "Opened MIDI port: " << midiIn_->getPortName(index) << std::endl;
std::string msg = "Opened MIDI port: " + midiIn_->getPortName(index);
logger_->log("MIDI", LogFlag::Info, msg);
return true; return true;
} catch (RtMidiError& e) { } catch (RtMidiError& e) {
std::cout << "Midi Port error" << std::endl; std::string msg = "Midi Port error: " + e.getMessage();
std::cerr << e.getMessage() << std::endl; logger_->log("MIDI", LogFlag::Error, msg);
return false; return false;
} }
} }
@@ -76,6 +88,18 @@ void MidiController::handleMessage(const std::vector<unsigned char>& msg) {
if(status == 0xFE) return; // "Active Sensing" -> 300ms heartbeat. could be useful to sense if this is missing for device failure detection 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 if(status == 0xF8) return; // "Timing Clock" -> 24 pulses per quarter note, for steady rhythm. not useful for this instrument
if(status == 0xB0) { // channel mode change
if((data1 & 0xF0) == 0x70) { // all notes off for this channel
for(uint8_t i = 0; i < UINT8_MAX; i++) {
noteOff(i);
}
return;
}
// TODO: msg[0] contains the channel to turn all notes off for
// since the current implementation is channel agnostic, we just turn all notes off
// eventually we might have noteOff(channelLookup(msg[0]), i);
}
// sustain pedal message event // sustain pedal message event
if(status == 0xB0 && data1 == 64) { if(status == 0xB0 && data1 == 64) {
handleSustain(data2 >= 64); handleSustain(data2 >= 64);
@@ -100,7 +124,7 @@ void MidiController::handleMessage(const std::vector<unsigned char>& msg) {
void MidiController::noteOn(uint8_t note, uint8_t vel) { void MidiController::noteOn(uint8_t note, uint8_t vel) {
sustainedNotes_.erase(note); sustainedNotes_.erase(note);
noteQueue_.push({ noteQueue_->push({
NoteEventType::NoteOn, NoteEventType::NoteOn,
static_cast<uint8_t>(note), static_cast<uint8_t>(note),
vel / 127.0f, vel / 127.0f,
@@ -114,7 +138,7 @@ void MidiController::noteOff(uint8_t note) {
sustainedNotes_.insert(note); sustainedNotes_.insert(note);
return; return;
} }
noteQueue_.push({ noteQueue_->push({
NoteEventType::NoteOff, NoteEventType::NoteOff,
static_cast<uint8_t>(note), static_cast<uint8_t>(note),
0.0f, 0.0f,

View File

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

View File

@@ -2,6 +2,15 @@
#include "NoteQueue.hpp" #include "NoteQueue.hpp"
#include <iostream> #include <iostream>
NoteQueue::NoteQueue() {
}
NoteQueue::NoteQueue(ConfigService* config, LoggerService* logger) :
config_(config), logger_(logger) {
}
// add event to noteQueue, called by MidiController or keyboardController // add event to noteQueue, called by MidiController or keyboardController
bool NoteQueue::push(const NoteEvent& event) { bool NoteQueue::push(const NoteEvent& event) {
size_t head = head_.load(std::memory_order_relaxed); size_t head = head_.load(std::memory_order_relaxed);

View File

@@ -7,6 +7,9 @@
#include <cstdint> #include <cstdint>
#include <chrono> #include <chrono>
#include "ConfigService.hpp"
#include "LoggerService.hpp"
enum NoteEventType { enum NoteEventType {
NoteOn = 0, NoteOn = 0,
NoteOff NoteOff
@@ -22,7 +25,8 @@ struct NoteEvent {
class NoteQueue { class NoteQueue {
public: public:
NoteQueue() = default; NoteQueue();
NoteQueue(ConfigService* config, LoggerService* logger);
~NoteQueue() = default; ~NoteQueue() = default;
bool push(const NoteEvent& event); bool push(const NoteEvent& event);
@@ -30,7 +34,10 @@ public:
private: private:
static constexpr size_t SYNTH_NOTE_QUEUE_SIZE = 128; ConfigService* config_;
LoggerService* logger_;
static constexpr size_t SYNTH_NOTE_QUEUE_SIZE = 128; // TODO: config
std::array<NoteEvent, SYNTH_NOTE_QUEUE_SIZE> buffer_; std::array<NoteEvent, SYNTH_NOTE_QUEUE_SIZE> buffer_;
std::atomic<size_t> head_{ 0 }; std::atomic<size_t> head_{ 0 };

View File

@@ -0,0 +1,76 @@
#include "Scope.hpp"
#include <iostream>
ScopeBuffer::ScopeBuffer(QObject *parent) : QObject(parent) {
}
ScopeBuffer::ScopeBuffer(ConfigService* config, LoggerService* logger, size_t size) :
config_(config), logger_(logger), buffer_(size) {
}
void ScopeBuffer::push(float sample) {
size_t w = writeIndex_.fetch_add(1, std::memory_order_relaxed);
buffer_[w % buffer_.size()] = sample;
}
void ScopeBuffer::read(std::vector<float>& out) {
size_t w = writeIndex_.load(std::memory_order_relaxed);
for(size_t i = 0; i < buffer_.size(); i++) {
size_t idx = (w + trigger_ + i * wavelength_ / out.size()) % buffer_.size();
out[i] = buffer_[idx];
}
}
Scope::Scope(QQuickItem *parent) : QQuickPaintedItem(parent) {
setAntialiasing(true);
}
void Scope::setBuffer(ScopeBuffer* scopeBuffer) {
scopeBuffer_ = scopeBuffer;
buffer_ = std::vector<float>(scopeBuffer_->size());
}
void Scope::paint(QPainter *painter) {
while(scopeBuffer_->spinlock()) {
// wait
}
if(scopeBuffer_ != nullptr) scopeBuffer_->read(buffer_);
// TODO: scale to max amplitude ?
float maxAmp = 1.0f;
/*
for(float s : buffer_) {
maxAmp = std::max(maxAmp, std::abs(s));
}
*/
float scaleY = (height() * 0.45f) / maxAmp;
float midY = height() / 2.0f;
painter->fillRect(0, 0, width(), height(), QColor(20, 20, 20));
QPen pen;
pen.setWidthF(3.0f);
QColor green(50, 255, 70);
pen.setColor(green);
painter->setPen(pen);
painter->setRenderHint(QPainter::Antialiasing);
for(size_t i = 1; i < buffer_.size(); i++) {
painter->drawLine(
i * width() / buffer_.size(),
midY + buffer_[i-1] * scaleY,
i * width() / buffer_.size(),
midY + buffer_[i] * scaleY
);
}
}

View File

@@ -1,27 +1,65 @@
#pragma once #pragma once
#include <QObject>
#include <QtQuick/QQuickPaintedItem>
#include <QtGui/QPainter>
#include <vector> #include <vector>
#include <atomic> #include <atomic>
class ScopeBuffer { #include "ConfigService.hpp"
#include "LoggerService.hpp"
class ScopeBuffer : public QObject {
Q_OBJECT // needed to attach to a qml component
public: public:
ScopeBuffer(size_t size); explicit ScopeBuffer(QObject* parent = nullptr);
ScopeBuffer(ConfigService* config, LoggerService* logger, size_t size);
~ScopeBuffer() = default; ~ScopeBuffer() = default;
void push(float sample); void push(float sample);
void read(std::vector<float>& out); void read(std::vector<float>& out);
void setTrigger(size_t trigger) { trigger_ = trigger; }
void setWavelength(size_t wavelength) { wavelength_ = wavelength; }
size_t trigger() { return trigger_; }
size_t wavelength() { return wavelength_; }
void spinlock(bool lock) { spinlock_.store(lock, std::memory_order_relaxed); }
bool spinlock() { return spinlock_.load(std::memory_order_relaxed); }
size_t size() { return buffer_.size(); }
private: private:
ConfigService* config_;
LoggerService* logger_;
std::vector<float> buffer_; std::vector<float> buffer_;
std::atomic<size_t> writeIndex_{0}; std::atomic<size_t> writeIndex_{0};
size_t trigger_ = 0; size_t trigger_ = 0;
uint32_t wavelgnth = 400; size_t wavelength_ = 400;
bool spinLock_ = false; std::atomic<bool> spinlock_ = false;
};
class Scope : public QQuickPaintedItem {
Q_OBJECT
Q_PROPERTY(ScopeBuffer* buffer WRITE setBuffer)
public:
explicit Scope(QQuickItem* parent = nullptr);
void paint(QPainter* painter) override;
void setBuffer(ScopeBuffer* scopeBuffer);
std::vector<float> buffer_;
ScopeBuffer* scopeBuffer_;
}; };

View File

@@ -10,12 +10,19 @@ Synth::Synth(ConfigService* config, LoggerService* logger, ScopeBuffer* scope, N
void Synth::handleNoteEvent(const NoteEvent& event) { void Synth::handleNoteEvent(const NoteEvent& event) {
Voice* v = findVoiceByNote(event.note); // Voice* v = findVoiceByNote(event.note);
if(v != nullptr) v->noteOff(); // 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) { if(event.type == NoteEventType::NoteOn) {
v = findFreeVoice(); Voice* v = findFreeVoice();
if(v != nullptr) v->noteOn(event.note, event.velocity); if(v != nullptr) v->noteOn(event.note, event.velocity);
} }
@@ -28,26 +35,30 @@ void Synth::process(float* out, size_t nFrames) {
handleNoteEvent(noteEvent); handleNoteEvent(noteEvent);
} }
// size_t lowestVoice = 0; size_t lowestVoice = 0;
// float lowestFreq = 100000.0f; float lowestFreq = 100000.0f;
// for(size_t i = 0; i < voices_.size(); i++) { for(size_t i = 0; i < voices_.size(); i++) {
// if(!voices_[i].isActive()) continue; if(!voices_[i].isActive()) continue;
// float currentFreq = voices_[i].frequency(); float currentFreq = voices_[i].frequency();
// if(currentFreq < lowestFreq) { if(currentFreq < lowestFreq) {
// lowestVoice = i; lowestVoice = i;
// lowestFreq = currentFreq; lowestFreq = currentFreq;
// } }
// } }
scope_->spinlock(true);
float sampleOut = 0.0f; float sampleOut = 0.0f;
bool triggered = false;
bool once = false;
for(size_t i = 0; i < nFrames; i++) { for(size_t i = 0; i < nFrames; i++) {
float mix = 0.0f; float mix = 0.0f;
for(size_t j = 0; j < voices_.size(); j++) { for(size_t j = 0; j < voices_.size(); j++) {
bool temp = false; bool temp = false;
if(!voices_[j].isActive()) continue; //if(!voices_[j].isActive()) continue;
mix += voices_[j].process(temp); mix += voices_[j].process(temp);
// if(j == lowestVoice) triggered = temp; if(j == lowestVoice) triggered = temp;
} }
mix = tanh(mix/4.0f); // prevent clipping mix = tanh(mix/4.0f); // prevent clipping
@@ -55,14 +66,15 @@ void Synth::process(float* out, size_t nFrames) {
out[2*i] = sampleOut; out[2*i] = sampleOut;
out[2*i+1] = sampleOut; out[2*i+1] = sampleOut;
// if(scope_) scope_->push(sampleOut); if(scope_) scope_->push(sampleOut);
// if(triggered && !once) { if(triggered && !once) {
// scope_->setTrigger(i); scope_->setTrigger(i);
// once = true; once = true;
// } }
} }
scope_->spinlock(false);
} }

View File

@@ -32,7 +32,7 @@ private:
ConfigService* config_; ConfigService* config_;
LoggerService* logger_; LoggerService* logger_;
ScopeBuffer* scope_ = nullptr; ScopeBuffer* scope_;;
NoteQueue* noteQueue_; NoteQueue* noteQueue_;
}; };

View File

@@ -21,6 +21,7 @@ public:
float process(bool& scopeTrigger); float process(bool& scopeTrigger);
uint8_t note() { return note_; } uint8_t note() { return note_; }
float frequency() { return noteToFrequency(note_); }
private: private:

View File

@@ -6,8 +6,8 @@ import AppDemo
ApplicationWindow { ApplicationWindow {
visible: true visible: true
width: 600 width: 1200
height: 400 height: 800
title: "sonobulus" title: "sonobulus"
TimerComponent { TimerComponent {
@@ -49,4 +49,25 @@ ApplicationWindow {
} }
} }
Scope {
id: scope
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 {
id: updateTimer
interval: 16 // ~60 fps
running: true
repeat: true
onTriggered: scope.update()
}
buffer: scopeBuffer
}
} }