Compare commits
8 Commits
dbc1db37e1
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| dd9bd5511e | |||
| c7432320fe | |||
| 5bb1bbe56c | |||
| 4643c681f3 | |||
| f4d855b36b | |||
| cfc8cb2b51 | |||
| 397e1fe7dc | |||
| e9fff9691b |
@@ -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>
|
||||
)
|
||||
|
||||
@@ -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.
|
||||
@@ -10,7 +10,7 @@ Logger = (
|
||||
"Error"
|
||||
);
|
||||
|
||||
ShowTime = true;
|
||||
ShowTime = false;
|
||||
ShowSourceTrace = false;
|
||||
CoutEnabled = true;
|
||||
|
||||
|
||||
36
scripts/first_order.py
Normal file
36
scripts/first_order.py
Normal 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
168
scripts/string_model_de.py
Normal 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()
|
||||
82
scripts/string_model_ss.py
Normal file
82
scripts/string_model_ss.py
Normal 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()
|
||||
16
src/main.cpp
16
src/main.cpp
@@ -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[]) {
|
||||
|
||||
@@ -21,29 +22,28 @@ int main(int argc, char* argv[]) {
|
||||
// create app objects
|
||||
ConfigService config = ConfigService("config/sonobulus.cfg");
|
||||
LoggerService logger = LoggerService(&config, "Engine");
|
||||
NoteQueue queue = NoteQueue();
|
||||
ScopeBuffer scopeBuffer = ScopeBuffer(512);
|
||||
NoteQueue queue = NoteQueue(&config, &logger);
|
||||
ScopeBuffer scopeBuffer = ScopeBuffer(&config, &logger, 2048);
|
||||
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
|
||||
AudioEngine audioEngine = AudioEngine(&config, &logger, &synth);
|
||||
audioEngine.start();
|
||||
|
||||
// attach backend gui components
|
||||
//Scope scope; //scope.setBuffer(&scopeBuffer);
|
||||
qmlRegisterType<TimerComponent>("AppDemo", 1, 0, "TimerComponent");
|
||||
//qmlRegisterSingletonInstance("AppDemo", 1, 0, "Scope", &scope);
|
||||
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()) {
|
||||
std::cout << "engine is empty" << std::endl;
|
||||
logger.log("Main", LogFlag::Error, "Engine is empty.");
|
||||
return -1;
|
||||
}
|
||||
|
||||
|
||||
@@ -9,11 +9,8 @@
|
||||
AudioEngine::AudioEngine(ConfigService* config, LoggerService* logger, Synth* synth) : config_(config), logger_(logger), synth_(synth) {
|
||||
|
||||
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() {
|
||||
@@ -33,13 +30,13 @@ bool AudioEngine::start() {
|
||||
|
||||
RtAudioErrorType status = audioDevice_.openStream(¶ms, nullptr, RTAUDIO_FLOAT32, sampleRate_, &bufferFrames_, &AudioEngine::audioCallback, this, &options);
|
||||
if(status != RTAUDIO_NO_ERROR) {
|
||||
std::cout << "Error opening RtAudio stream" << std::endl;
|
||||
logger_->log("Audio", LogFlag::Error, "Error opening RtAudio stream.");
|
||||
return false;
|
||||
}
|
||||
|
||||
status = audioDevice_.startStream();
|
||||
if(status != RTAUDIO_NO_ERROR) {
|
||||
std::cout << "Error starting RtAudio stream" << std::endl;
|
||||
logger_->log("Audio", LogFlag::Error, "Error starting RtAudio stream.");
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
@@ -28,12 +28,21 @@ bool Instrument::isActive() {
|
||||
float Instrument::process(bool& scopeTrigger) {
|
||||
|
||||
if(active_ && envelope_ < 1.0f) envelope_ += 0.01f;
|
||||
if(!active_ && envelope_ > 0.0f) envelope_ -= 0.01f;
|
||||
|
||||
phase_ += phaseIncrement_;
|
||||
if(phase_ > 2.0f * pi) phase_ -= 2.0f * pi;
|
||||
if(!active_ && envelope_ > 0.0f) envelope_ -= 0.0004f;
|
||||
|
||||
if(!isActive()) return 0.0f;
|
||||
return sin(phase_) * envelope_;
|
||||
|
||||
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_;
|
||||
|
||||
}
|
||||
|
||||
@@ -34,4 +34,8 @@ private:
|
||||
float phaseIncrement_ = 0.0f;
|
||||
float envelope_ = 0.0f;
|
||||
|
||||
float targetSample = 0.0f;
|
||||
float currentSample = 0.0f;
|
||||
static constexpr float responsiveness_ = 0.1f;
|
||||
|
||||
};
|
||||
|
||||
@@ -4,14 +4,21 @@
|
||||
#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;
|
||||
std::string msg = "RtMidi init failed: " + e.getMessage();
|
||||
logger_->log("MIDI", LogFlag::Warning, msg);
|
||||
}
|
||||
// TODO: this still doesnt work on windows
|
||||
|
||||
openDefaultPort();
|
||||
}
|
||||
|
||||
MidiController::~MidiController() {
|
||||
@@ -19,18 +26,21 @@ MidiController::~MidiController() {
|
||||
}
|
||||
|
||||
// open the first for thats successful
|
||||
// we could also add an option in the config for a preferred port name
|
||||
bool MidiController::openDefaultPort() {
|
||||
if (!midiIn_) return false;
|
||||
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;
|
||||
}
|
||||
|
||||
// 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();
|
||||
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++) {
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -43,11 +53,13 @@ bool MidiController::openPort(unsigned int index) {
|
||||
try {
|
||||
midiIn_->openPort(index);
|
||||
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;
|
||||
} catch (RtMidiError& e) {
|
||||
std::cout << "Midi Port error" << std::endl;
|
||||
std::cerr << e.getMessage() << std::endl;
|
||||
std::string msg = "Midi Port error: " + e.getMessage();
|
||||
logger_->log("MIDI", LogFlag::Error, msg);
|
||||
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 == 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
|
||||
if(status == 0xB0 && data1 == 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) {
|
||||
sustainedNotes_.erase(note);
|
||||
|
||||
noteQueue_.push({
|
||||
noteQueue_->push({
|
||||
NoteEventType::NoteOn,
|
||||
static_cast<uint8_t>(note),
|
||||
vel / 127.0f,
|
||||
@@ -114,7 +138,7 @@ void MidiController::noteOff(uint8_t note) {
|
||||
sustainedNotes_.insert(note);
|
||||
return;
|
||||
}
|
||||
noteQueue_.push({
|
||||
noteQueue_->push({
|
||||
NoteEventType::NoteOff,
|
||||
static_cast<uint8_t>(note),
|
||||
0.0f,
|
||||
|
||||
@@ -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_;
|
||||
|
||||
@@ -2,6 +2,15 @@
|
||||
#include "NoteQueue.hpp"
|
||||
#include <iostream>
|
||||
|
||||
NoteQueue::NoteQueue() {
|
||||
|
||||
}
|
||||
|
||||
NoteQueue::NoteQueue(ConfigService* config, LoggerService* logger) :
|
||||
config_(config), logger_(logger) {
|
||||
|
||||
}
|
||||
|
||||
// add event to noteQueue, called by MidiController or keyboardController
|
||||
bool NoteQueue::push(const NoteEvent& event) {
|
||||
size_t head = head_.load(std::memory_order_relaxed);
|
||||
|
||||
@@ -7,6 +7,9 @@
|
||||
#include <cstdint>
|
||||
#include <chrono>
|
||||
|
||||
#include "ConfigService.hpp"
|
||||
#include "LoggerService.hpp"
|
||||
|
||||
enum NoteEventType {
|
||||
NoteOn = 0,
|
||||
NoteOff
|
||||
@@ -22,7 +25,8 @@ struct NoteEvent {
|
||||
class NoteQueue {
|
||||
|
||||
public:
|
||||
NoteQueue() = default;
|
||||
NoteQueue();
|
||||
NoteQueue(ConfigService* config, LoggerService* logger);
|
||||
~NoteQueue() = default;
|
||||
|
||||
bool push(const NoteEvent& event);
|
||||
@@ -30,7 +34,10 @@ public:
|
||||
|
||||
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::atomic<size_t> head_{ 0 };
|
||||
|
||||
@@ -3,7 +3,12 @@
|
||||
|
||||
#include <iostream>
|
||||
|
||||
ScopeBuffer::ScopeBuffer(size_t size) : buffer_(size) {
|
||||
ScopeBuffer::ScopeBuffer(QObject *parent) : QObject(parent) {
|
||||
|
||||
}
|
||||
|
||||
ScopeBuffer::ScopeBuffer(ConfigService* config, LoggerService* logger, size_t size) :
|
||||
config_(config), logger_(logger), buffer_(size) {
|
||||
|
||||
}
|
||||
|
||||
@@ -27,21 +32,45 @@ 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) {
|
||||
|
||||
//std::cout << "onPaint" << std::endl;
|
||||
while(scopeBuffer_->spinlock()) {
|
||||
// wait
|
||||
}
|
||||
|
||||
static float phase = 0.0f;
|
||||
phase += 0.1f;
|
||||
float sample = sin(phase);
|
||||
if(scopeBuffer_ != nullptr) scopeBuffer_->read(buffer_);
|
||||
|
||||
QPen pen(Qt::blue, 4);
|
||||
// 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);
|
||||
|
||||
painter->drawRect(10, 10, width() - 20*sample, height() - 20*sample);
|
||||
|
||||
painter->setPen(QPen(Qt::red, 2));
|
||||
painter->drawLine(0, 0, width(), height());
|
||||
|
||||
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
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,11 +8,17 @@
|
||||
#include <vector>
|
||||
#include <atomic>
|
||||
|
||||
class ScopeBuffer {
|
||||
#include "ConfigService.hpp"
|
||||
#include "LoggerService.hpp"
|
||||
|
||||
class ScopeBuffer : public QObject {
|
||||
|
||||
Q_OBJECT // needed to attach to a qml component
|
||||
|
||||
public:
|
||||
|
||||
ScopeBuffer(size_t size);
|
||||
explicit ScopeBuffer(QObject* parent = nullptr);
|
||||
ScopeBuffer(ConfigService* config, LoggerService* logger, size_t size);
|
||||
~ScopeBuffer() = default;
|
||||
|
||||
void push(float sample);
|
||||
@@ -22,31 +28,36 @@ public:
|
||||
void setWavelength(size_t wavelength) { wavelength_ = wavelength; }
|
||||
size_t trigger() { return trigger_; }
|
||||
size_t wavelength() { return wavelength_; }
|
||||
void spinlock(bool lock) { spinlock_ = lock; }
|
||||
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:
|
||||
|
||||
ConfigService* config_;
|
||||
LoggerService* logger_;
|
||||
|
||||
std::vector<float> buffer_;
|
||||
std::atomic<size_t> writeIndex_{0};
|
||||
|
||||
size_t trigger_ = 0;
|
||||
size_t wavelength_ = 400;
|
||||
|
||||
bool spinlock_ = false;
|
||||
std::atomic<bool> spinlock_ = false;
|
||||
|
||||
};
|
||||
|
||||
class Scope : public QQuickPaintedItem {
|
||||
|
||||
Q_OBJECT
|
||||
QML_ELEMENT
|
||||
Q_PROPERTY(ScopeBuffer* buffer WRITE setBuffer)
|
||||
|
||||
public:
|
||||
explicit Scope(QQuickItem* parent = nullptr);
|
||||
|
||||
void paint(QPainter* painter) override;
|
||||
void setBuffer(ScopeBuffer* scopeBuffer) { scopeBuffer_ = scopeBuffer; }
|
||||
|
||||
void setBuffer(ScopeBuffer* scopeBuffer);
|
||||
|
||||
std::vector<float> buffer_;
|
||||
ScopeBuffer* scopeBuffer_;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -28,18 +35,22 @@ void Synth::process(float* out, size_t nFrames) {
|
||||
handleNoteEvent(noteEvent);
|
||||
}
|
||||
|
||||
// size_t lowestVoice = 0;
|
||||
// float lowestFreq = 100000.0f;
|
||||
// for(size_t i = 0; i < voices_.size(); i++) {
|
||||
// if(!voices_[i].isActive()) continue;
|
||||
// float currentFreq = voices_[i].frequency();
|
||||
// if(currentFreq < lowestFreq) {
|
||||
// lowestVoice = i;
|
||||
// lowestFreq = currentFreq;
|
||||
// }
|
||||
// }
|
||||
size_t lowestVoice = 0;
|
||||
float lowestFreq = 100000.0f;
|
||||
for(size_t i = 0; i < voices_.size(); i++) {
|
||||
if(!voices_[i].isActive()) continue;
|
||||
float currentFreq = voices_[i].frequency();
|
||||
if(currentFreq < lowestFreq) {
|
||||
lowestVoice = i;
|
||||
lowestFreq = currentFreq;
|
||||
}
|
||||
}
|
||||
|
||||
scope_->spinlock(true);
|
||||
|
||||
float sampleOut = 0.0f;
|
||||
bool triggered = false;
|
||||
bool once = false;
|
||||
for(size_t i = 0; i < nFrames; i++) {
|
||||
|
||||
float mix = 0.0f;
|
||||
@@ -47,7 +58,7 @@ void Synth::process(float* out, size_t nFrames) {
|
||||
bool temp = false;
|
||||
//if(!voices_[j].isActive()) continue;
|
||||
mix += voices_[j].process(temp);
|
||||
// if(j == lowestVoice) triggered = temp;
|
||||
if(j == lowestVoice) triggered = temp;
|
||||
}
|
||||
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+1] = sampleOut;
|
||||
|
||||
// if(scope_) scope_->push(sampleOut);
|
||||
// if(triggered && !once) {
|
||||
// scope_->setTrigger(i);
|
||||
// once = true;
|
||||
// }
|
||||
|
||||
if(scope_) scope_->push(sampleOut);
|
||||
if(triggered && !once) {
|
||||
scope_->setTrigger(i);
|
||||
once = true;
|
||||
}
|
||||
}
|
||||
|
||||
scope_->spinlock(false);
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -32,7 +32,7 @@ private:
|
||||
|
||||
ConfigService* config_;
|
||||
LoggerService* logger_;
|
||||
ScopeBuffer* scope_ = nullptr;
|
||||
ScopeBuffer* scope_;;
|
||||
NoteQueue* noteQueue_;
|
||||
|
||||
};
|
||||
|
||||
@@ -21,6 +21,7 @@ public:
|
||||
|
||||
float process(bool& scopeTrigger);
|
||||
uint8_t note() { return note_; }
|
||||
float frequency() { return noteToFrequency(note_); }
|
||||
|
||||
private:
|
||||
|
||||
|
||||
@@ -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 {
|
||||
@@ -63,6 +66,8 @@ ApplicationWindow {
|
||||
repeat: true
|
||||
onTriggered: scope.update()
|
||||
}
|
||||
|
||||
buffer: scopeBuffer
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user