Compare commits
28 Commits
dbc1db37e1
...
feature/wa
| Author | SHA1 | Date | |
|---|---|---|---|
| 5d86018d06 | |||
| 54996469d1 | |||
| 2436946a3c | |||
| b00e7a053c | |||
| fdb55bb78d | |||
| 8de4081a50 | |||
| eb84c93b7b | |||
| 295a869839 | |||
| 9365e12bb4 | |||
| 269648e021 | |||
| 41f7499c23 | |||
| b3d227dc71 | |||
| 6913736476 | |||
| 5bd347de83 | |||
| e94d4c980e | |||
| 3e94d64f4a | |||
| b4df8657dd | |||
| a5af4f6283 | |||
| a2df345216 | |||
| 2bad7d66d8 | |||
| dd9bd5511e | |||
| c7432320fe | |||
| 5bb1bbe56c | |||
| 4643c681f3 | |||
| f4d855b36b | |||
| cfc8cb2b51 | |||
| 397e1fe7dc | |||
| e9fff9691b |
2
.gitignore
vendored
2
.gitignore
vendored
@@ -1,2 +1,4 @@
|
||||
|
||||
build/*
|
||||
|
||||
*/__pycache__/*
|
||||
@@ -1,9 +1,10 @@
|
||||
|
||||
cmake_minimum_required(VERSION 4.0)
|
||||
cmake_minimum_required(VERSION 3.21)
|
||||
project(sonobulus LANGUAGES CXX)
|
||||
|
||||
set(CMAKE_CXX_STANDARD 20)
|
||||
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
||||
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=c++20")
|
||||
|
||||
# get dependencies
|
||||
include(FetchContent)
|
||||
@@ -45,7 +46,7 @@ find_package(Qt6 REQUIRED COMPONENTS
|
||||
qt_standard_project_setup()
|
||||
|
||||
add_library(sonobulus_core STATIC
|
||||
src/ConfigService.cpp
|
||||
src/config/ConfigService.cpp
|
||||
src/LoggerService.cpp
|
||||
src/TimerComponent.cpp
|
||||
src/synth/AudioEngine.cpp
|
||||
@@ -54,15 +55,28 @@ add_library(sonobulus_core STATIC
|
||||
src/synth/NoteQueue.cpp
|
||||
src/synth/Scope.cpp
|
||||
src/synth/Synth.cpp
|
||||
src/synth/Voice.cpp
|
||||
src/synth/Instrument.cpp
|
||||
#src/synth/Voice.cpp
|
||||
src/synth/Filter.cpp
|
||||
src/synth/WavetableLoader.cpp
|
||||
src/synth/Envelope.cpp
|
||||
src/synth/InstrumentFactory.cpp
|
||||
src/synth/instruments/Instrument.cpp
|
||||
src/synth/instruments/PianoString.cpp
|
||||
src/synth/instruments/WavetableOscillator.cpp
|
||||
)
|
||||
|
||||
if (WIN32)
|
||||
set(LIBCONFIG_LIB libconfig++)
|
||||
else()
|
||||
set(LIBCONFIG_LIB config++)
|
||||
endif()
|
||||
|
||||
target_link_libraries(sonobulus_core PRIVATE
|
||||
Qt6::Core
|
||||
Qt6::Quick
|
||||
rtaudio
|
||||
rtmidi
|
||||
libconfig++
|
||||
${LIBCONFIG_LIB}
|
||||
)
|
||||
|
||||
target_include_directories(sonobulus_core PRIVATE
|
||||
@@ -80,6 +94,8 @@ qt_add_executable(sonobulus
|
||||
src/main.cpp
|
||||
)
|
||||
|
||||
set_target_properties(sonobulus PROPERTIES OUTPUT_NAME "sonobulus.out")
|
||||
|
||||
qt_add_qml_module(sonobulus
|
||||
URI sonobulus
|
||||
VERSION 1.0
|
||||
@@ -87,6 +103,7 @@ qt_add_qml_module(sonobulus
|
||||
)
|
||||
|
||||
target_include_directories(sonobulus PRIVATE
|
||||
${CMAKE_SOURCE_DIR}/src/
|
||||
${rtaudio_SOURCE_DIR}
|
||||
${rtmidi_SOURCE_DIR}
|
||||
${libconfig_SOURCE_DIR}/lib
|
||||
@@ -111,6 +128,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.
|
||||
@@ -8,9 +8,9 @@ Logger = (
|
||||
"Info",
|
||||
"Warning",
|
||||
"Error"
|
||||
);
|
||||
); # TODO: console flags vs file flags
|
||||
|
||||
ShowTime = true;
|
||||
ShowTime = false;
|
||||
ShowSourceTrace = false;
|
||||
CoutEnabled = true;
|
||||
|
||||
@@ -19,6 +19,23 @@ Logger = (
|
||||
}
|
||||
);
|
||||
|
||||
AudioEngine = (
|
||||
{
|
||||
Id = "Main";
|
||||
|
||||
SampleRate = 44100;
|
||||
Channels = 2;
|
||||
StereoMode = 1;
|
||||
BufferSize = 512;
|
||||
PitchStandard = 440.0;
|
||||
MidiHome = 69;
|
||||
NotesPerOctave = 12.0;
|
||||
OctaveConstant = 2.0;
|
||||
MaxVoices = 32;
|
||||
}
|
||||
);
|
||||
|
||||
# TODO: I don't like this being in the main config, maybe import it in (mainly the notes and keycodes)
|
||||
KeyboardController = (
|
||||
{
|
||||
Id = "Main";
|
||||
|
||||
BIN
config/wavetables/saw.wt
Normal file
BIN
config/wavetables/saw.wt
Normal file
Binary file not shown.
BIN
config/wavetables/shark_fin.wt
Normal file
BIN
config/wavetables/shark_fin.wt
Normal file
Binary file not shown.
BIN
config/wavetables/sigmoid.wt
Normal file
BIN
config/wavetables/sigmoid.wt
Normal file
Binary file not shown.
BIN
config/wavetables/sine.wt
Normal file
BIN
config/wavetables/sine.wt
Normal file
Binary file not shown.
BIN
config/wavetables/square.wt
Normal file
BIN
config/wavetables/square.wt
Normal file
Binary file not shown.
BIN
config/wavetables/triangle.wt
Normal file
BIN
config/wavetables/triangle.wt
Normal file
Binary file not shown.
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()
|
||||
256
scripts/string_model_de.py
Normal file
256
scripts/string_model_de.py
Normal file
@@ -0,0 +1,256 @@
|
||||
|
||||
import matplotlib.pyplot as plt
|
||||
import numpy as np
|
||||
import math
|
||||
import sounddevice as sd
|
||||
|
||||
# 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)
|
||||
|
||||
# simulation parameters
|
||||
f1 = 50 # fundamental frequency
|
||||
f_e = 44100 # sampling frequency
|
||||
N = 200 # number of string segments
|
||||
delta_t = 1/f_e # time step
|
||||
L = 0.5 # string length, meters
|
||||
delta_x = L/N # spatial step
|
||||
H = int(f_e * 2) # length of simulation in time
|
||||
|
||||
# string parameters
|
||||
E = 100 * 10**9 # youngs modulus, steel = 200GPa
|
||||
rho = 8000 # density, steel, kg/m^3
|
||||
radius = 0.005 # meters
|
||||
kappa = radius/2 # radius of gyration, r/2 for a circular string
|
||||
S = math.pi*radius**2 # string cross sectional area, assuming circular
|
||||
mu = S*rho # linear mass density
|
||||
M_S = mu*L # string mass
|
||||
T = 500 # string tension, N
|
||||
c = math.sqrt(T/mu) # transverse wave velocity
|
||||
I = math.pi*radius**4 / 4
|
||||
k = math.sqrt(E*I/(rho*S))
|
||||
# stiffness = k**2 * delta_t**2 / delta_x**4 # string stiffness parameter
|
||||
stiffness = 0
|
||||
sigma = 1 # decay rate
|
||||
tau = 1/sigma # decay time
|
||||
omega = 1/f1 # angular frequency
|
||||
|
||||
# hammer parameters
|
||||
M_H = 0.5 # hammer mass, kg
|
||||
HSMR = M_H/M_S # hammer-mass string ratio
|
||||
V_H_0 = 10000000000 # initial hammer velocity at t=0, m/s
|
||||
x_0 = L/2 # distance of hammer from agraffe
|
||||
alpha = x_0 / L # relative hammer striking position
|
||||
|
||||
# empirical constants
|
||||
b_1 = 0 # some constant
|
||||
b_3 = 0 # 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_3/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
|
||||
|
||||
# string
|
||||
x = [0] * N # current string position
|
||||
x_initial = [0] * N # assuming string at rest
|
||||
last_x1 = [0] * N # string position from last timestep
|
||||
last_x2 = [0] * N # string position from two timesteps ago
|
||||
x_sample = int(0.8*N) # location where we sample the string position for signal
|
||||
|
||||
# hammer
|
||||
F_H_current = 0 # current force exterted by hammer
|
||||
eta_current = 0 # current hammer position
|
||||
eta_last = 0 # last hammer position
|
||||
i_H = 16 # hammer width
|
||||
x_hammer = int(alpha * N) # location where hammer strikes
|
||||
eta_0 = 0 # initial hammer displacement
|
||||
|
||||
x_out = np.zeros(H) # taking this as the sound output at some arbitrary point along the string
|
||||
t = np.arange(0, H/f_e, delta_t)
|
||||
|
||||
def plot_current():
|
||||
plt.plot(np.arange(0, N, 1), x)
|
||||
plt.title("Response")
|
||||
plt.xlabel("t")
|
||||
plt.ylabel("x(t, x_sample)")
|
||||
plt.grid()
|
||||
plt.show()
|
||||
|
||||
def spatial_window(i):
|
||||
if(i < x_hammer+i_H/2 and i > x_hammer-i_H/2):
|
||||
return abs(i-x_hammer-i_H/2)
|
||||
else:
|
||||
return 0
|
||||
|
||||
# first iteration, n = 1
|
||||
last_x1 = x_initial.copy()
|
||||
eta_current = V_H_0 * delta_t + eta_0
|
||||
|
||||
for i in range(1, N-1):
|
||||
x[i] = (last_x1[i+1] + last_x1[i-1])/2 # irrelavant if string is at rest at hammer strike, but relevant if a repeat hit
|
||||
last_x1 = x.copy()
|
||||
F_H_current = K * abs(eta_current - x[x_hammer])**p
|
||||
|
||||
# second iteration, n = 2
|
||||
eta_next = 2*eta_current - eta_0 - (delta_t**2*N*F_H_current)/M_S
|
||||
eta_last = eta_current
|
||||
eta_current = eta_next
|
||||
|
||||
for i in range(1, N-1):
|
||||
x[i] = last_x1[i+1] + last_x1[i-1] - x_initial[i] + ((delta_t**2)*N*F_H_current*spatial_window(i))/M_S
|
||||
|
||||
last_x2 = last_x1.copy()
|
||||
last_x1 = x.copy()
|
||||
F_H_current = K * abs(eta_current - x[x_hammer])**p
|
||||
|
||||
# rest of the iterations
|
||||
for n in range(H): # time
|
||||
|
||||
if(n <= 2):
|
||||
continue
|
||||
|
||||
eta_next = 2*eta_current - eta_last - (delta_t**2*N*F_H_current)/M_S
|
||||
eta_last = eta_current
|
||||
eta_current = eta_next
|
||||
|
||||
push = eta_current - x[x_hammer]
|
||||
if(push > 0):
|
||||
F_H_current = K * abs(push)**p
|
||||
else:
|
||||
F_H_current = 0
|
||||
F_H_current = 0
|
||||
|
||||
next_x = [0] * N
|
||||
|
||||
for i in range(2, N-2): # space
|
||||
|
||||
# term_1 = a_1*x[i] + a_2*last_x1[i]
|
||||
# term_2 = a_3*(x[i+1] + x[i-1]) + a_4*(x[i+2] + x[i-2])
|
||||
# term_3 = a_5*(last_x1[i+1] + last_x1[i-1] + last_x2[i])
|
||||
# term_4 = (delta_t**2 * N*F_H_current * spatial_window(i))/M_S
|
||||
|
||||
# next_x[i] = term_1 + term_2 + term_3 + term_4
|
||||
next_x[i] = 2*(1-r**2)*x[i] + r**2 * (x[i+1] + x[i-1]) - last_x1[i]
|
||||
|
||||
x[0] = 0
|
||||
x[1] = 0
|
||||
x[N-2] = 0
|
||||
x[N-1] = 0
|
||||
|
||||
last_x2 = last_x1.copy()
|
||||
last_x1 = x.copy()
|
||||
x = next_x.copy()
|
||||
|
||||
x_out[n] = x[x_sample]
|
||||
|
||||
# plotting
|
||||
plt.plot(t, x_out)
|
||||
plt.title("Response")
|
||||
plt.xlabel("t")
|
||||
plt.ylabel("x(t, x_sample)")
|
||||
plt.grid()
|
||||
plt.show()
|
||||
|
||||
sd.play(x_out, f_e)
|
||||
sd.wait()
|
||||
120
scripts/string_model_fdm.py
Normal file
120
scripts/string_model_fdm.py
Normal file
@@ -0,0 +1,120 @@
|
||||
|
||||
import math
|
||||
import numpy as np
|
||||
import matplotlib.pyplot as plt
|
||||
import sounddevice as sd
|
||||
import time
|
||||
|
||||
sample_rate = 44100
|
||||
seconds = 10
|
||||
|
||||
N = 100 # number of string segments
|
||||
I = int(sample_rate * seconds) # number of samples to simulate
|
||||
|
||||
# string parameters
|
||||
rho = 8000 # density, steel, kg/m^3
|
||||
radius = 0.001 # meters
|
||||
S = math.pi*radius**2 # string cross sectional area, assuming circular
|
||||
mu = S*rho # linear mass density
|
||||
T = 1200 # string tension, N
|
||||
c = math.sqrt(T/mu) # transverse wave velocity
|
||||
kappa = 0.001 # stiffness coefficient
|
||||
sigma = 0.5 # damping coefficient
|
||||
L = 1.0 # length of string
|
||||
strike_position = 0.2 # x of impulse location
|
||||
impulse_width = 0.02 # x of impulse width
|
||||
impulse_velocity = 1000.0 # x/t of impulse magnitude
|
||||
sample_position = 0.1 # percentage along L of sampling for audio
|
||||
|
||||
f_0 = c / (2*L) # fundamental frequency of a non-stiff string
|
||||
f_1 = f_0 * math.sqrt(1 + kappa) # fundamental frequency of the stiff string
|
||||
print("fundamental frequency =", f_1)
|
||||
|
||||
dx = L / N # delta x
|
||||
dt = 1/sample_rate
|
||||
#dt = 0.2 * dx / c
|
||||
if(dx**2 < (c*dt)**2 + 4*(kappa*dt/(dx**2))**2):
|
||||
print("warning: possibly unstable, increase sample rate or decrease string segments")
|
||||
|
||||
# derived constants
|
||||
r1 = c * dt/dx
|
||||
r2 = (c * dt/dx) ** 2
|
||||
s1 = kappa * dt/dx**2
|
||||
s2 = (kappa * dt/dx**2) ** 2
|
||||
a1 = 2 - 2*sigma*dt
|
||||
a2 = 2*sigma*dt - 1
|
||||
|
||||
# string grid
|
||||
x = np.linspace(0, L, N + 1) # linspace my beloved
|
||||
|
||||
# state vectors
|
||||
y_last = np.zeros(N + 1)
|
||||
y_current = np.zeros(N + 1)
|
||||
y_next = np.zeros(N + 1)
|
||||
# output
|
||||
y_sample = np.zeros(I)
|
||||
|
||||
# vector of initial velocity across the string as a result of the impulse
|
||||
v0 = impulse_velocity * np.exp(-((x - strike_position) ** 2)/(2 * impulse_width ** 2))
|
||||
|
||||
# initial conditions:
|
||||
# y(x, 0) = 0 and dy/dt(x, 0) = v0(x)
|
||||
# boundary conditions: y(0, t) = y(L, t) = 0 as well as all dy/dts thereafter
|
||||
|
||||
def show_plot():
|
||||
plt.plot(np.arange(0, N+1, 1), y_current)
|
||||
plt.grid()
|
||||
plt.show()
|
||||
|
||||
start_time = time.perf_counter()
|
||||
|
||||
def applyImpulse():
|
||||
for n in range(2, N-2):
|
||||
y_current[n] = y_current[n] + dt*v0[n]
|
||||
|
||||
# first iteration
|
||||
for n in range(2, N-2):
|
||||
y_current[n] = 0.5 * r1**2 * (y_last[n-1] - 2*y_last[n] + y_last[n+1])
|
||||
applyImpulse()
|
||||
|
||||
n_sample = int(sample_position * L * N)
|
||||
y_sample[0] = y_last[n_sample]
|
||||
y_sample[1] = y_current[n_sample]
|
||||
|
||||
# rest of the iterations
|
||||
for i in range(2, I):
|
||||
|
||||
for n in range(2, N-2):
|
||||
# stiff wave equation
|
||||
y_xx = y_current[n+1] - 2*y_current[n] + y_current[n-1]
|
||||
y_xxxx = y_current[n-2] - 4*y_current[n-1] + 6*y_current[n] - 4*y_current[n+1] + y_current[n+2]
|
||||
|
||||
y_next[n] = a1 * y_current[n] + a2 * y_last[n] + r2 * y_xx - s2 * y_xxxx
|
||||
|
||||
y_next[0] = 0
|
||||
y_next[1] = 0
|
||||
y_next[N-1] = 0
|
||||
y_next[N-2] = 0
|
||||
|
||||
# y_sample[i] = math.tanh(y_next[n_sample])
|
||||
y_sample[i] = y_next[n_sample]
|
||||
|
||||
y_last = y_current.copy()
|
||||
y_current = y_next.copy()
|
||||
|
||||
if(i == 120000):
|
||||
applyImpulse()
|
||||
|
||||
if(i % 10000*seconds == 0):
|
||||
print(f"{i/I * 100:4.4}% complete")
|
||||
|
||||
end_time = time.perf_counter()
|
||||
elapsed = end_time - start_time
|
||||
print(f"Executed in {elapsed:.3f} seconds. {elapsed/seconds*100:.2f}% overshoot")
|
||||
|
||||
plt.plot(np.arange(0, I, 1), y_sample)
|
||||
plt.grid()
|
||||
plt.show()
|
||||
|
||||
sd.play(y_sample, sample_rate)
|
||||
sd.wait()
|
||||
95
scripts/string_model_ss.py
Normal file
95
scripts/string_model_ss.py
Normal file
@@ -0,0 +1,95 @@
|
||||
|
||||
import scipy.signal as sig
|
||||
import matplotlib.pyplot as plt
|
||||
import numpy as np
|
||||
import sounddevice as sd
|
||||
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 = 100 # fundamental frequency
|
||||
def f_n(n):
|
||||
return f_1 * n
|
||||
omega_0 = 2*math.pi*f_1
|
||||
b = 1.5
|
||||
c = 2
|
||||
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
|
||||
return c*n*omega_0*math.sqrt(1+b*(n-1)**2)
|
||||
|
||||
# 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.001 # i guessed
|
||||
zeta_2 = 1.5 * zeta_1
|
||||
zeta_3 = 2 * 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.002
|
||||
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)
|
||||
|
||||
for i in range(t.size):
|
||||
y[i] = math.tanh(y[i])/2
|
||||
if(y[i] > 1):
|
||||
print("what", y[i])
|
||||
|
||||
# plotting
|
||||
plt.plot(t, y)
|
||||
plt.title("Step Response")
|
||||
plt.xlabel("t")
|
||||
plt.ylabel("y")
|
||||
plt.grid()
|
||||
plt.show()
|
||||
|
||||
sd.play(y, 44100)
|
||||
sd.wait()
|
||||
52
scripts/wavetables/example_wavetable.py
Normal file
52
scripts/wavetables/example_wavetable.py
Normal file
@@ -0,0 +1,52 @@
|
||||
|
||||
import math
|
||||
from generate_wavetable import generateWavetable
|
||||
|
||||
WAVETABLE_FILE_NAME = "config/wavetables/shark_fin.wt"
|
||||
|
||||
# process expects a waveform from x=[0, 2pi) centered around f(x)=0
|
||||
# normalization is handled by the wavetable generator
|
||||
|
||||
def sine(phase):
|
||||
return math.sin(phase)
|
||||
|
||||
def square(phase):
|
||||
sample = 1.0
|
||||
if(phase <= math.pi):
|
||||
sample = -1
|
||||
return sample
|
||||
|
||||
def saw(phase):
|
||||
return (phase / math.pi) - 1.0
|
||||
|
||||
def triangle(phase):
|
||||
sample = 0.0
|
||||
if(phase <= math.pi/2.0):
|
||||
sample = phase * 2.0/math.pi
|
||||
elif(phase <= 3.0*math.pi/2.0):
|
||||
sample = phase * (-2.0/math.pi) + 2.0
|
||||
else:
|
||||
sample = phase * 2.0/math.pi - 4.0
|
||||
return sample
|
||||
|
||||
def sharkFin(phase):
|
||||
k = 10.0
|
||||
sample = 0.0
|
||||
if(phase <= math.pi):
|
||||
sample = 2.0 * (phase/math.pi) ** k - 1.0
|
||||
else:
|
||||
sample = -2.0 * (phase/math.pi - 1) ** k + 1.0
|
||||
return sample
|
||||
|
||||
def sigmoid(phase):
|
||||
k = 4.0
|
||||
a = -k * (phase - math.pi)
|
||||
sample = 2.0 / (1.0 + math.exp(a)) - 1.0
|
||||
return sample
|
||||
|
||||
# pass in the name of your wavetable file and the process function
|
||||
def main():
|
||||
generateWavetable(WAVETABLE_FILE_NAME, sharkFin)
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
67
scripts/wavetables/generate_wavetable.py
Normal file
67
scripts/wavetables/generate_wavetable.py
Normal file
@@ -0,0 +1,67 @@
|
||||
|
||||
# this is a script for generating a wavetable file
|
||||
# a wavetable file consists of a one-dimensional array of samples representing one period of a waveform
|
||||
# metadata includes:
|
||||
# - file version (for program compatibilty)
|
||||
# - binary format (float, double, int32, etc.)
|
||||
# - domain (eg phase is represented from [0, 2pi) )
|
||||
# - range (determined by datatype, e.g. float=[-1,1], int32=[-2^15, 2^15-1])
|
||||
# - waveform RMS (for loudness normalization)
|
||||
# - sample count
|
||||
# the synth program uses the filename, not any metadata
|
||||
|
||||
# this script uses the input function to calculate samples
|
||||
# if you want a custom wavetable, copt/edit/modify the example function (i used desmos for brainstorming)
|
||||
# import this script and call generateWavetable(processFunction) to generate a custom wavetable
|
||||
|
||||
from array import array
|
||||
import math
|
||||
|
||||
wavetableLength = 2048
|
||||
|
||||
def createFile(name):
|
||||
filename = name
|
||||
print("Creating file " + filename)
|
||||
file = open(filename, "wb")
|
||||
return file
|
||||
|
||||
def writeMetadata(file):
|
||||
print(">> im writing metadata")
|
||||
|
||||
def writeData(file, processFunction):
|
||||
print(">> im generating the wavetable")
|
||||
|
||||
# init variables
|
||||
data_list = [None] * wavetableLength
|
||||
phaseInc = 2*math.pi / wavetableLength
|
||||
x = 0
|
||||
accumulator = 0
|
||||
|
||||
# generate each discrete sample
|
||||
for i in range(wavetableLength):
|
||||
sample = processFunction(x)
|
||||
accumulator += sample * sample
|
||||
x += phaseInc
|
||||
data_list[i] = sample
|
||||
|
||||
# normalize by rms
|
||||
rms = math.sqrt(accumulator/wavetableLength)
|
||||
print(">> wavetable RMS: ", rms)
|
||||
for i in range(wavetableLength):
|
||||
data_list[i] /= rms
|
||||
|
||||
# write to file
|
||||
binary_data = array("f", data_list)
|
||||
file.write(binary_data)
|
||||
|
||||
def closeFile(file):
|
||||
print(">> finishing up")
|
||||
file.close()
|
||||
|
||||
def generateWavetable(name, processFunction):
|
||||
print("Hello main")
|
||||
file = createFile(name)
|
||||
writeMetadata(file)
|
||||
writeData(file, processFunction)
|
||||
closeFile(file)
|
||||
print("Done")
|
||||
@@ -1,128 +0,0 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <libconfig.h++>
|
||||
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <iostream>
|
||||
#include <unordered_map>
|
||||
|
||||
struct LoggerConfig {
|
||||
std::string id;
|
||||
std::vector<std::string> flagsEnabled;
|
||||
bool showTime = false;
|
||||
bool showSourceTrace = false;
|
||||
bool coutEnabled = false;
|
||||
bool fileEnabled = false;
|
||||
std::string filePath;
|
||||
|
||||
bool parseConfig(const libconfig::Setting& setting) {
|
||||
|
||||
if (!setting.lookupValue("Id", id)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
const libconfig::Setting& flags = setting.lookup("FlagsEnabled");
|
||||
for (int index = 0; index < flags.getLength(); ++index) {
|
||||
flagsEnabled.push_back(static_cast<const char*>(flags[index]));
|
||||
}
|
||||
} catch (const libconfig::SettingException&) {
|
||||
flagsEnabled.clear();
|
||||
}
|
||||
|
||||
setting.lookupValue("ShowTime", showTime);
|
||||
setting.lookupValue("ShowSourceTrace", showSourceTrace);
|
||||
setting.lookupValue("CoutEnabled", coutEnabled);
|
||||
setting.lookupValue("FileEnabled", fileEnabled);
|
||||
setting.lookupValue("FilePath", filePath);
|
||||
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
struct AudioConfig {
|
||||
uint32_t sampleRate;
|
||||
uint32_t channels;
|
||||
uint32_t stereoMode;
|
||||
uint32_t bufferSize;
|
||||
float pitchStandard;
|
||||
int32_t midiHome;
|
||||
int32_t notesPerOctave;
|
||||
};
|
||||
|
||||
struct KeymapConfig {
|
||||
std::string id;
|
||||
std::unordered_map<int32_t, uint8_t> keymap;
|
||||
|
||||
bool parseConfig(const libconfig::Setting& setting) {
|
||||
|
||||
if (!setting.lookupValue("Id", id)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
const libconfig::Setting& keymapGroup = setting.lookup("Keymap");
|
||||
const libconfig::Setting& notesGroup = setting.lookup("Notes");
|
||||
const libconfig::Setting& keysGroup = setting.lookup("Keys");
|
||||
|
||||
for(size_t i = 0; i < keymapGroup.getLength(); i++) {
|
||||
libconfig::Setting& item = keymapGroup[i];
|
||||
std::string key = item.getName();
|
||||
std::string note = "";
|
||||
if(item.getType() == libconfig::Setting::TypeString) note = std::string(item.c_str());
|
||||
|
||||
int8_t noteId = static_cast<int>(notesGroup[note]) % INT8_MAX;
|
||||
int32_t keyId = static_cast<int>(keysGroup[key]) % INT32_MAX;
|
||||
|
||||
keymap.emplace(keyId, noteId);
|
||||
}
|
||||
|
||||
} catch (const libconfig::SettingException&) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
class ConfigService {
|
||||
|
||||
public:
|
||||
|
||||
ConfigService(const std::string& filePath);
|
||||
~ConfigService() = default;
|
||||
|
||||
bool loadFromFile(const std::string& filePath);
|
||||
|
||||
template<typename T>
|
||||
bool getConfig(const std::string& type, const std::string& id, T* config) const {
|
||||
try {
|
||||
const libconfig::Setting& configs = config_.lookup(type);
|
||||
for (int index = 0; index < configs.getLength(); ++index) {
|
||||
const libconfig::Setting& configSetting = configs[index];
|
||||
|
||||
std::string configId;
|
||||
if (!configSetting.lookupValue("Id", configId) || configId != id) {
|
||||
continue;
|
||||
}
|
||||
|
||||
return config->parseConfig(configSetting);
|
||||
}
|
||||
} catch (const libconfig::SettingException& ex) {
|
||||
std::cout << "libconfig setting exception: " << ex.what() << std::endl;
|
||||
return false;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
const std::string& lastError() const;
|
||||
|
||||
private:
|
||||
|
||||
libconfig::Config config_;
|
||||
std::string lastError_;
|
||||
};
|
||||
@@ -7,22 +7,17 @@
|
||||
#include <iomanip>
|
||||
#include <fstream>
|
||||
#include <filesystem>
|
||||
#include <algorithm>
|
||||
|
||||
namespace fs = std::filesystem;
|
||||
|
||||
LoggerService::LoggerService(ConfigService* config, const std::string& loggerId) {
|
||||
|
||||
if(!(config->getConfig<LoggerConfig>("Logger", loggerId, &configuration_))) {
|
||||
std::cout << "Failed to get logger configuration fom config service" << std::endl;
|
||||
if(!(config->getConfig<LoggerParams, LoggerConfig>("Logger", loggerId, &configuration_))) {
|
||||
std::cout << "Failed to get logger configuration from config service" << std::endl;
|
||||
return;
|
||||
}
|
||||
|
||||
standardOutputEnabled_ = configuration_.coutEnabled;
|
||||
fileOutputEnabled_ = configuration_.fileEnabled;
|
||||
additionaldetailsEnabled_ = configuration_.showSourceTrace;
|
||||
timeEnabled_ = configuration_.showTime;
|
||||
id_ = configuration_.id;
|
||||
|
||||
for(std::string& flag : configuration_.flagsEnabled) {
|
||||
bool found = false;
|
||||
for(const char* validFlag : LogFlagStrings) {
|
||||
@@ -54,7 +49,7 @@ LoggerService::LoggerService(ConfigService* config, const std::string& loggerId)
|
||||
fs::create_directories(std::string(BINARY_DIR) + "/" + finaltime);
|
||||
std::string logPath = std::string(BINARY_DIR) + "/" + finaltime + "/" + configuration_.filePath;
|
||||
|
||||
if(fileOutputEnabled_) outfile_.open(logPath);
|
||||
if(configuration_.fileEnabled) outfile_.open(logPath);
|
||||
|
||||
log("Logger", LogFlag::Info, "Logger initialized.");
|
||||
}
|
||||
@@ -77,11 +72,11 @@ void LoggerService::log(std::string component, LogFlag flag, std::string message
|
||||
|
||||
std::string finalmessage = "";
|
||||
|
||||
if(timeEnabled_) {
|
||||
if(configuration_.showTime) {
|
||||
finalmessage = finalmessage + "[" + "Not Implemented" + "] ";
|
||||
}
|
||||
|
||||
std::string componentTrace = "[" + id_ + ": " + component + "] ";
|
||||
std::string componentTrace = "[" + configuration_.id + ": " + component + "] ";
|
||||
finalmessage += componentTrace; // component is the section of the program (For example Mesh or Engine) that is calling the logger
|
||||
|
||||
std::string level = "";
|
||||
@@ -93,15 +88,15 @@ void LoggerService::log(std::string component, LogFlag flag, std::string message
|
||||
finalmessage = finalmessage + "[" + level + "] ";
|
||||
finalmessage = finalmessage + message + " ";
|
||||
|
||||
if (additionaldetailsEnabled_) {
|
||||
if (configuration_.showSourceTrace) {
|
||||
finalmessage = finalmessage + "[Function: " + Source.function_name() + "]" + " " + "[Line: " + std::to_string(Source.line()) + "]" + " " + "[File: " + Source.file_name() + "]";
|
||||
}
|
||||
|
||||
if(standardOutputEnabled_) {
|
||||
if(configuration_.coutEnabled) {
|
||||
std::cout << finalmessage << std::endl;
|
||||
}
|
||||
|
||||
if(fileOutputEnabled_) {
|
||||
if(configuration_.fileEnabled) {
|
||||
outfile_ << finalmessage << std::endl;
|
||||
}
|
||||
return;
|
||||
|
||||
@@ -5,7 +5,8 @@
|
||||
#include <fstream>
|
||||
#include <source_location>
|
||||
|
||||
#include "ConfigService.hpp"
|
||||
#include "config/ConfigService.hpp"
|
||||
#include "config/LoggerConfig.hpp"
|
||||
|
||||
enum LogFlag {
|
||||
Debug,
|
||||
@@ -33,14 +34,8 @@ public:
|
||||
|
||||
private:
|
||||
|
||||
bool standardOutputEnabled_;
|
||||
bool fileOutputEnabled_;
|
||||
bool additionaldetailsEnabled_;
|
||||
bool timeEnabled_;
|
||||
std::ofstream outfile_;
|
||||
std::string id_;
|
||||
|
||||
std::vector<LogFlag> activeFlags_;
|
||||
|
||||
LoggerConfig configuration_;
|
||||
LoggerParams configuration_;
|
||||
};
|
||||
18
src/ParameterStore.cpp
Normal file
18
src/ParameterStore.cpp
Normal file
@@ -0,0 +1,18 @@
|
||||
|
||||
#include "ParameterStore.hpp"
|
||||
|
||||
ParameterStore::ParameterStore() {
|
||||
|
||||
}
|
||||
|
||||
// set parameter value
|
||||
void ParameterStore::set(ParamId id, float value) {
|
||||
Param oldParam = values_[static_cast<size_t>(id)].load(std::memory_order_relaxed);
|
||||
Param updatedParam = { value, oldParam.min, oldParam.max };
|
||||
values_[static_cast<size_t>(id)].store(updatedParam, std::memory_order_relaxed);
|
||||
}
|
||||
|
||||
// get a single parameter
|
||||
float ParameterStore::get(ParamId id) const {
|
||||
return values_[static_cast<size_t>(id)].load(std::memory_order_relaxed).val;
|
||||
}
|
||||
37
src/ParameterStore.hpp
Normal file
37
src/ParameterStore.hpp
Normal file
@@ -0,0 +1,37 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <vector>
|
||||
#include <cstdint>
|
||||
|
||||
enum class ParamId : uint8_t {
|
||||
MasterGain,
|
||||
Count
|
||||
};
|
||||
|
||||
struct Param {
|
||||
float val;
|
||||
float min;
|
||||
float max;
|
||||
};
|
||||
|
||||
constexpr size_t PARAM_COUNT = static_cast<size_t>(ParamId::Count);
|
||||
|
||||
class ParameterStore {
|
||||
|
||||
public:
|
||||
|
||||
ParameterStore();
|
||||
~ParameterStore() = default;
|
||||
|
||||
void set(ParamId id, float value);
|
||||
void set(ParamId id, int32_t value) { set(id, static_cast<float>(value)); }
|
||||
void set(EnvelopeId id, float depth, float a, float d, float s, float r);
|
||||
float get(ParamId id) const;
|
||||
int32_t getInt(ParamId id) const { return static_cast<int32_t>(get(id)); }
|
||||
|
||||
private:
|
||||
|
||||
std::array<std::atomic<Param>, PARAM_COUNT> values_;
|
||||
|
||||
};
|
||||
45
src/config/AudioConfig.hpp
Normal file
45
src/config/AudioConfig.hpp
Normal file
@@ -0,0 +1,45 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "IConfig.hpp"
|
||||
|
||||
struct AudioParams : IParams {
|
||||
std::string id;
|
||||
uint32_t sampleRate;
|
||||
uint32_t channels;
|
||||
uint32_t stereoMode;
|
||||
uint32_t bufferSize;
|
||||
float pitchStandard;
|
||||
int32_t midiHome;
|
||||
float notesPerOctave;
|
||||
float octaveConstant;
|
||||
size_t maxVoices;
|
||||
};
|
||||
|
||||
|
||||
class AudioConfig : public IConfig<AudioParams> {
|
||||
|
||||
public:
|
||||
|
||||
using IConfig<AudioParams>::IConfig;
|
||||
|
||||
bool parseConfig(const libconfig::Setting& setting) override {
|
||||
|
||||
if (!setting.lookupValue("Id", params_->id)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
setting.lookupValue("SampleRate", params_->sampleRate);
|
||||
setting.lookupValue("StereoMode", params_->stereoMode);
|
||||
setting.lookupValue("BufferSize", params_->bufferSize);
|
||||
setting.lookupValue("Channels", params_->channels);
|
||||
setting.lookupValue("PitchStandard", params_->pitchStandard);
|
||||
setting.lookupValue("MidiHome", params_->midiHome);
|
||||
setting.lookupValue("NotesPerOctave", params_->notesPerOctave);
|
||||
setting.lookupValue("OctaveConstant", params_->octaveConstant);
|
||||
setting.lookupValue("MaxVoices", params_->maxVoices);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
};
|
||||
56
src/config/ConfigService.hpp
Normal file
56
src/config/ConfigService.hpp
Normal file
@@ -0,0 +1,56 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <libconfig.h++>
|
||||
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <iostream>
|
||||
#include <unordered_map>
|
||||
|
||||
#include "config/IConfig.hpp"
|
||||
|
||||
// TODO: would be cool for the config file to include other config files so theyre a bit more compartmentalized
|
||||
// then the main file will be like a hub and would be more like each component having its own config file
|
||||
class ConfigService {
|
||||
|
||||
public:
|
||||
|
||||
ConfigService(const std::string& filePath);
|
||||
~ConfigService() = default;
|
||||
|
||||
bool loadFromFile(const std::string& filePath);
|
||||
|
||||
template<typename T1, typename T2>
|
||||
bool getConfig(const std::string& type, const std::string& id, T1* params) const {
|
||||
|
||||
T2 config { params };
|
||||
|
||||
try {
|
||||
const libconfig::Setting& configs = config_.lookup(type);
|
||||
for (int index = 0; index < configs.getLength(); ++index) {
|
||||
const libconfig::Setting& configSetting = configs[index];
|
||||
|
||||
std::string configId;
|
||||
if (!configSetting.lookupValue("Id", configId) || configId != id) {
|
||||
continue;
|
||||
}
|
||||
|
||||
return config.parseConfig(configSetting);
|
||||
}
|
||||
} catch (const libconfig::SettingException& ex) {
|
||||
std::cout << "libconfig setting exception: " << ex.what() << std::endl;
|
||||
return false;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
const std::string& lastError() const;
|
||||
|
||||
private:
|
||||
|
||||
libconfig::Config config_;
|
||||
std::string lastError_;
|
||||
};
|
||||
24
src/config/IConfig.hpp
Normal file
24
src/config/IConfig.hpp
Normal file
@@ -0,0 +1,24 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <libconfig.h++>
|
||||
|
||||
struct IParams {
|
||||
|
||||
};
|
||||
|
||||
template <typename T>
|
||||
class IConfig {
|
||||
|
||||
public:
|
||||
|
||||
IConfig(T* params) : params_(params) { }
|
||||
~IConfig() = default;
|
||||
|
||||
virtual bool parseConfig(const libconfig::Setting& setting) = 0;
|
||||
IParams* params() { return params_; }
|
||||
|
||||
protected:
|
||||
T* params_;
|
||||
|
||||
};
|
||||
47
src/config/KeymapConfig.hpp
Normal file
47
src/config/KeymapConfig.hpp
Normal file
@@ -0,0 +1,47 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "IConfig.hpp"
|
||||
|
||||
struct KeymapParams : IParams {
|
||||
std::string id;
|
||||
std::unordered_map<int32_t, uint8_t> keymap;
|
||||
};
|
||||
|
||||
class KeymapConfig : public IConfig<KeymapParams> {
|
||||
|
||||
public:
|
||||
|
||||
using IConfig<KeymapParams>::IConfig;
|
||||
|
||||
bool parseConfig(const libconfig::Setting& setting) override {
|
||||
|
||||
if (!setting.lookupValue("Id", params_->id)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
const libconfig::Setting& keymapGroup = setting.lookup("Keymap");
|
||||
const libconfig::Setting& notesGroup = setting.lookup("Notes");
|
||||
const libconfig::Setting& keysGroup = setting.lookup("Keys");
|
||||
|
||||
for(size_t i = 0; i < keymapGroup.getLength(); i++) {
|
||||
libconfig::Setting& item = keymapGroup[i];
|
||||
std::string key = item.getName();
|
||||
std::string note = "";
|
||||
if(item.getType() == libconfig::Setting::TypeString) note = std::string(item.c_str());
|
||||
|
||||
int8_t noteId = static_cast<int>(notesGroup[note]) % INT8_MAX;
|
||||
int32_t keyId = static_cast<int>(keysGroup[key]) % INT32_MAX;
|
||||
|
||||
params_->keymap.emplace(keyId, noteId);
|
||||
}
|
||||
|
||||
} catch (const libconfig::SettingException&) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
};
|
||||
47
src/config/LoggerConfig.hpp
Normal file
47
src/config/LoggerConfig.hpp
Normal file
@@ -0,0 +1,47 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "IConfig.hpp"
|
||||
|
||||
struct LoggerParams : IParams {
|
||||
std::string id;
|
||||
std::vector<std::string> flagsEnabled;
|
||||
bool showTime = false;
|
||||
bool showSourceTrace = false;
|
||||
bool coutEnabled = false;
|
||||
bool fileEnabled = false;
|
||||
std::string filePath;
|
||||
};
|
||||
|
||||
class LoggerConfig : public IConfig<LoggerParams> {
|
||||
|
||||
public:
|
||||
|
||||
using IConfig<LoggerParams>::IConfig;
|
||||
|
||||
bool parseConfig(const libconfig::Setting& setting) override {
|
||||
|
||||
if (!setting.lookupValue("Id", params_->id)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
const libconfig::Setting& flags = setting.lookup("FlagsEnabled");
|
||||
for (int index = 0; index < flags.getLength(); ++index) {
|
||||
params_->flagsEnabled.push_back(static_cast<const char*>(flags[index]));
|
||||
}
|
||||
} catch (const libconfig::SettingException&) {
|
||||
params_->flagsEnabled.clear();
|
||||
return false;
|
||||
}
|
||||
|
||||
setting.lookupValue("ShowTime", params_->showTime);
|
||||
setting.lookupValue("ShowSourceTrace", params_->showSourceTrace);
|
||||
setting.lookupValue("CoutEnabled", params_->coutEnabled);
|
||||
setting.lookupValue("FileEnabled", params_->fileEnabled);
|
||||
setting.lookupValue("FilePath", params_->filePath);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
};
|
||||
18
src/main.cpp
18
src/main.cpp
@@ -7,10 +7,11 @@
|
||||
|
||||
#include "TimerComponent.hpp"
|
||||
#include "LoggerService.hpp"
|
||||
#include "ConfigService.hpp"
|
||||
#include "config/ConfigService.hpp"
|
||||
#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;
|
||||
}
|
||||
|
||||
|
||||
@@ -8,12 +8,15 @@
|
||||
|
||||
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;
|
||||
// load audio settings from config service
|
||||
if(!(config->getConfig<AudioParams, AudioConfig>("AudioEngine", "Main", &configuration_))) {
|
||||
logger_->log("Audio", LogFlag::Error, "Failed to get logger configuration from config service");
|
||||
return;
|
||||
}
|
||||
|
||||
if(logger_ == nullptr) std::cout << "err: logger nullptr" << std::endl;
|
||||
|
||||
if(audioDevice_.getDeviceCount() < 1) {
|
||||
logger_->log("Audio", LogFlag::Error, "No audio devices found.");
|
||||
}
|
||||
}
|
||||
|
||||
AudioEngine::~AudioEngine() {
|
||||
@@ -25,26 +28,35 @@ bool AudioEngine::start() {
|
||||
// initialize the audio engine
|
||||
RtAudio::StreamParameters params;
|
||||
params.deviceId = audioDevice_.getDefaultOutputDevice();
|
||||
params.nChannels = channels_; // we're doing two duplicate channels for pseudo-mono
|
||||
params.nChannels = configuration_.channels; // we're doing two duplicate channels for pseudo-mono
|
||||
params.firstChannel = 0;
|
||||
|
||||
RtAudio::StreamOptions options;
|
||||
options.flags = RTAUDIO_MINIMIZE_LATENCY;
|
||||
|
||||
RtAudioErrorType status = audioDevice_.openStream(¶ms, nullptr, RTAUDIO_FLOAT32, sampleRate_, &bufferFrames_, &AudioEngine::audioCallback, this, &options);
|
||||
RtAudioErrorType status = audioDevice_.openStream(
|
||||
¶ms,
|
||||
nullptr,
|
||||
RTAUDIO_FLOAT32,
|
||||
configuration_.sampleRate,
|
||||
&configuration_.bufferSize,
|
||||
&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;
|
||||
}
|
||||
|
||||
// sanity check
|
||||
std::string msg = "sample rate: " + std::to_string(sampleRate_) + ", buffer frames: " + std::to_string(bufferFrames_);
|
||||
std::string msg = "sample rate: " + std::to_string(configuration_.sampleRate) + ", buffer frames: " + std::to_string(configuration_.bufferSize);
|
||||
logger_->log("AudioEngine", LogFlag::Info, msg);
|
||||
return true;
|
||||
|
||||
|
||||
@@ -6,7 +6,8 @@
|
||||
#include <RtAudio.h>
|
||||
|
||||
#include "LoggerService.hpp"
|
||||
#include "ConfigService.hpp"
|
||||
#include "config/ConfigService.hpp"
|
||||
#include "config/AudioConfig.hpp"
|
||||
#include "Synth.hpp"
|
||||
#include "NoteQueue.hpp"
|
||||
|
||||
@@ -35,13 +36,10 @@ private:
|
||||
|
||||
RtAudio audioDevice_ { AUDIO_API };
|
||||
|
||||
// TODO: make these configurable
|
||||
uint32_t sampleRate_ = 44100;
|
||||
uint32_t bufferFrames_ = 512;
|
||||
uint32_t channels_ = 2;
|
||||
|
||||
LoggerService* logger_;
|
||||
ConfigService* config_;
|
||||
AudioParams configuration_;
|
||||
|
||||
Synth* synth_;
|
||||
|
||||
};
|
||||
|
||||
24
src/synth/Envelope.cpp
Normal file
24
src/synth/Envelope.cpp
Normal file
@@ -0,0 +1,24 @@
|
||||
|
||||
#include "Envelope.hpp"
|
||||
|
||||
Envelope::Envelope() {
|
||||
|
||||
}
|
||||
|
||||
Envelope::Envelope(float sampleRate) {
|
||||
|
||||
}
|
||||
|
||||
void Envelope::noteOn() {
|
||||
|
||||
}
|
||||
|
||||
void Envelope::noteOff() {
|
||||
|
||||
}
|
||||
|
||||
float Envelope::process() {
|
||||
|
||||
return 0.0f;
|
||||
|
||||
}
|
||||
55
src/synth/Envelope.hpp
Normal file
55
src/synth/Envelope.hpp
Normal file
@@ -0,0 +1,55 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#define ENVELOPE_MIN_TIME 0.001f // 1 ms
|
||||
|
||||
#include <cstdint>
|
||||
#include <algorithm>
|
||||
|
||||
class Envelope {
|
||||
|
||||
enum class State {
|
||||
Idle,
|
||||
Attack,
|
||||
Decay,
|
||||
Sustain,
|
||||
Release
|
||||
};
|
||||
|
||||
public:
|
||||
|
||||
Envelope(); // we shouldn't need services for this
|
||||
Envelope(float sampleRate);
|
||||
~Envelope() = default;
|
||||
|
||||
void setSampleRate(float sampleRate) { sampleRate_ = sampleRate; }
|
||||
void setAttack(float seconds) { attack_ = std::max(seconds, ENVELOPE_MIN_TIME); }
|
||||
void setDecay(float seconds) { decay_ = std::max(seconds, ENVELOPE_MIN_TIME); }
|
||||
void setSustain(float level) { sustain_ = level; }
|
||||
void setRelease(float seconds) { release_ = std::max(seconds, ENVELOPE_MIN_TIME); }
|
||||
// values close to zero cause some popping sounds on noteOns/noteOffs
|
||||
|
||||
State state() { return state_; }
|
||||
|
||||
// note events
|
||||
void noteOn(); // transitions from idle state to attack
|
||||
void noteOff(); // transitions from sustain to release
|
||||
|
||||
float process();
|
||||
|
||||
bool isActive() { return state_ != State::Idle; }
|
||||
|
||||
private:
|
||||
|
||||
State state_ = State::Idle;
|
||||
|
||||
float sampleRate_ = 44100.0f;
|
||||
|
||||
float attack_ = 0.0f;
|
||||
float decay_ = 0.0f;
|
||||
float sustain_ = 0.0f;
|
||||
float release_ = 0.0f;
|
||||
|
||||
float value_ = 0.0f;
|
||||
|
||||
};
|
||||
86
src/synth/Filter.cpp
Normal file
86
src/synth/Filter.cpp
Normal file
@@ -0,0 +1,86 @@
|
||||
|
||||
#include "Filter.hpp"
|
||||
|
||||
#include <cmath>
|
||||
#include <algorithm>
|
||||
|
||||
void Filter::setSampleRate(float sampleRate) {
|
||||
sampleRate_ = sampleRate;
|
||||
calculateCoefficients();
|
||||
}
|
||||
|
||||
// recalculate filter based on params
|
||||
void Filter::setParams(Type type, float frequency, float q) {
|
||||
type_ = type;
|
||||
frequency_ = std::min(frequency, sampleRate_ / 2.0f * 0.999f);
|
||||
q_ = q;
|
||||
calculateCoefficients();
|
||||
}
|
||||
|
||||
// update current state and output filtered value
|
||||
float Filter::biquadProcess(float in) {
|
||||
|
||||
// calculate filtered sample
|
||||
float out = b0_ * in + z1_;
|
||||
|
||||
// update states
|
||||
z1_ = b1_ * in - a1_ * out + z2_;
|
||||
z2_ = b2_ * in - a2_ * out;
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
// internal control system emulation
|
||||
void Filter::calculateCoefficients() {
|
||||
|
||||
if(q_ < 0.001f) q_ = 0.001f;
|
||||
|
||||
float omega = 2.0f * pi * frequency_ / sampleRate_;
|
||||
float sinOmega = std::sin(omega);
|
||||
float cosOmega = std::cos(omega);
|
||||
float alpha = sinOmega / (2.0f * q_);
|
||||
|
||||
float b0, b1, b2, a0, a1, a2;
|
||||
|
||||
switch (type_) {
|
||||
case Type::BiquadLowpass:
|
||||
b0 = (1.0f - cosOmega) * 0.5f;
|
||||
b1 = 1.0f - cosOmega;
|
||||
b2 = (1.0f - cosOmega) * 0.5f;
|
||||
a0 = 1.0f + alpha;
|
||||
a1 = -2.0f * cosOmega;
|
||||
a2 = 1.0f - alpha;
|
||||
break;
|
||||
|
||||
case Type::BiquadHighpass:
|
||||
b0 = (1.0f + cosOmega) * 0.5f;
|
||||
b1 = -(1.0f + cosOmega);
|
||||
b2 = (1.0f + cosOmega) * 0.5f;
|
||||
a0 = 1.0f + alpha;
|
||||
a1 = -2.0f * cosOmega;
|
||||
a2 = 1.0f - alpha;
|
||||
break;
|
||||
|
||||
case Type::BiquadBandpass:
|
||||
b0 = sinOmega * 0.5f;
|
||||
b1 = 0.0f;
|
||||
b2 = -sinOmega * 0.5f;
|
||||
a0 = 1.0f + alpha;
|
||||
a1 = -2.0f * cosOmega;
|
||||
a2 = 1.0f - alpha;
|
||||
break;
|
||||
}
|
||||
|
||||
// values need to be normalized
|
||||
b0_ = b0 / a0;
|
||||
b1_ = b1 / a0;
|
||||
b2_ = b2 / a0;
|
||||
a1_ = a1 / a0;
|
||||
a2_ = a2 / a0;
|
||||
|
||||
}
|
||||
|
||||
void Filter::resetSate() {
|
||||
z1_ = 0.0f;
|
||||
z2_ = 0.0f;
|
||||
}
|
||||
46
src/synth/Filter.hpp
Normal file
46
src/synth/Filter.hpp
Normal file
@@ -0,0 +1,46 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
class Filter {
|
||||
public:
|
||||
|
||||
enum class Type : uint16_t {
|
||||
BiquadLowpass,
|
||||
BiquadNotch,
|
||||
BiquadBandpass,
|
||||
BiquadHighpass
|
||||
};
|
||||
|
||||
Filter() = default;
|
||||
~Filter() = default;
|
||||
|
||||
void setSampleRate(float sampleRate);
|
||||
void setParams(Type type, float frequency, float q);
|
||||
float biquadProcess(float in);
|
||||
void resetSate();
|
||||
|
||||
// TODO: add more filter types here
|
||||
// high pass
|
||||
// band pass
|
||||
// notch
|
||||
// https://webaudio.github.io/Audio-EQ-Cookbook/audio-eq-cookbook.html
|
||||
// instead of making different calculate functions, have an enum which specifies the filter type
|
||||
// one public calculate which the enum is passed through and a private calculate for the specific types, switch statement to choose
|
||||
|
||||
private:
|
||||
|
||||
void calculateCoefficients();
|
||||
|
||||
Type type_ = Type::BiquadLowpass;
|
||||
float sampleRate_ = 1.0f;
|
||||
|
||||
float frequency_ = 6000.0f;
|
||||
float q_ = 0.707f;
|
||||
static constexpr float pi = 3.14159265358979323846;
|
||||
|
||||
// biquad filter structure
|
||||
float a1_, a2_, b0_, b1_, b2_;
|
||||
float z1_, z2_;
|
||||
|
||||
};
|
||||
@@ -1,39 +0,0 @@
|
||||
|
||||
#include "Instrument.hpp"
|
||||
|
||||
#include "string"
|
||||
|
||||
Instrument::Instrument(ConfigService* config, LoggerService* logger) :
|
||||
config_(config), logger_(logger) {
|
||||
|
||||
}
|
||||
|
||||
void Instrument::noteOn(float frequency, float velocity) {
|
||||
|
||||
phaseIncrement_ = 2.0f * pi * frequency / sampleRate_;
|
||||
|
||||
envelope_ += 0.01f; // so it triggers as active
|
||||
|
||||
active_ = true;
|
||||
}
|
||||
|
||||
void Instrument::noteOff() {
|
||||
active_ = false;
|
||||
}
|
||||
|
||||
bool Instrument::isActive() {
|
||||
return (envelope_ > 0.0f);
|
||||
}
|
||||
|
||||
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(!isActive()) return 0.0f;
|
||||
return sin(phase_) * envelope_;
|
||||
|
||||
}
|
||||
@@ -1,37 +0,0 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "ConfigService.hpp"
|
||||
#include "LoggerService.hpp"
|
||||
|
||||
// an instrument is our state space model. it calculates discrete samples as a function of time, the current states, and the inputs
|
||||
// an instrumeent is owned by a voice
|
||||
class Instrument {
|
||||
|
||||
public:
|
||||
|
||||
Instrument() = default;
|
||||
Instrument(ConfigService* config, LoggerService* logger);
|
||||
~Instrument() = default;
|
||||
|
||||
void noteOn(float frequency, float velocity);
|
||||
void noteOff();
|
||||
|
||||
bool isActive();
|
||||
|
||||
float process(bool& scopeTrigger);
|
||||
|
||||
private:
|
||||
|
||||
float sampleRate_ = 44100.0f;
|
||||
bool active_ = false;
|
||||
|
||||
ConfigService* config_;
|
||||
LoggerService* logger_;
|
||||
|
||||
static constexpr float pi = 3.14159265358979323846f;
|
||||
float phase_ = 0.0f;
|
||||
float phaseIncrement_ = 0.0f;
|
||||
float envelope_ = 0.0f;
|
||||
|
||||
};
|
||||
38
src/synth/InstrumentFactory.cpp
Normal file
38
src/synth/InstrumentFactory.cpp
Normal file
@@ -0,0 +1,38 @@
|
||||
|
||||
#include "InstrumentFactory.hpp"
|
||||
|
||||
#include "Instruments/PianoString.hpp"
|
||||
#include "Instruments/WavetableOscillator.hpp"
|
||||
|
||||
InstrumentFactory::InstrumentFactory(ConfigService* config, LoggerService* logger) :
|
||||
config_(config), logger_(logger) {
|
||||
|
||||
}
|
||||
|
||||
Instrument* InstrumentFactory::createInstrument(InstrumentType type, float sampleRate, AudioParams* params) {
|
||||
|
||||
switch(type) {
|
||||
case InstrumentType::Default:
|
||||
return new Instrument(config_, logger_, sampleRate, params);
|
||||
case InstrumentType::PianoString:
|
||||
return new PianoString(config_, logger_, sampleRate, params);
|
||||
case InstrumentType::WavetableOscillator:
|
||||
return new WavetableOscillator(config_, logger_, wavetableLoader_, sampleRate, params);
|
||||
default:
|
||||
return nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
int32_t InstrumentFactory::createInstruments(std::vector<Instrument*>* instruments, size_t voiceCount, float sampleRate, AudioParams* params) {
|
||||
|
||||
// parse the config file
|
||||
// if there's wavetables then we need a wavetable loader
|
||||
// initialize wavetableLoader_ here
|
||||
|
||||
for(size_t i = 0; i < params->maxVoices; i++) {
|
||||
instruments->push_back(createInstrument(InstrumentType::PianoString, static_cast<float>(params->sampleRate), params));
|
||||
}
|
||||
|
||||
return params->maxVoices;
|
||||
|
||||
}
|
||||
35
src/synth/InstrumentFactory.hpp
Normal file
35
src/synth/InstrumentFactory.hpp
Normal file
@@ -0,0 +1,35 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "config/ConfigService.hpp"
|
||||
#include "config/AudioConfig.hpp"
|
||||
#include "LoggerService.hpp"
|
||||
#include "Instruments/Instrument.hpp"
|
||||
#include "WavetableLoader.hpp"
|
||||
|
||||
class InstrumentFactory {
|
||||
|
||||
public:
|
||||
|
||||
InstrumentFactory(ConfigService* config, LoggerService* logger);
|
||||
~InstrumentFactory() = default;
|
||||
|
||||
int32_t createInstruments(std::vector<Instrument*>* instruments, size_t voiceCount, float sampleRate, AudioParams* params);
|
||||
|
||||
enum class InstrumentType {
|
||||
Default,
|
||||
PianoString,
|
||||
WavetableOscillator
|
||||
};
|
||||
|
||||
private:
|
||||
|
||||
Instrument* createInstrument(InstrumentType type, float sampleRate, AudioParams* params);
|
||||
|
||||
ConfigService* config_;
|
||||
LoggerService* logger_;
|
||||
|
||||
// optional creators
|
||||
WavetableLoader* wavetableLoader_ = nullptr;
|
||||
|
||||
};
|
||||
56
src/synth/Instruments/Instrument.cpp
Normal file
56
src/synth/Instruments/Instrument.cpp
Normal file
@@ -0,0 +1,56 @@
|
||||
|
||||
#include "Instrument.hpp"
|
||||
|
||||
#include "string"
|
||||
|
||||
Instrument::Instrument(ConfigService* config, LoggerService* logger, float sampleRate, AudioParams* params) :
|
||||
config_(config), logger_(logger), sampleRate_(sampleRate), params_(params) {
|
||||
|
||||
}
|
||||
|
||||
void Instrument::startNote(uint8_t midiNote, float velocity) {
|
||||
|
||||
note_ = midiNote;
|
||||
|
||||
noteOn(noteToFrequency(midiNote), velocity);
|
||||
|
||||
}
|
||||
|
||||
void Instrument::noteOn(float frequency, float velocity) {
|
||||
|
||||
phaseIncrement_ = 2.0f * pi * frequency / sampleRate_;
|
||||
|
||||
envelope_ += 0.01f; // so it triggers as active
|
||||
|
||||
active_ = true;
|
||||
}
|
||||
|
||||
void Instrument::noteOff() {
|
||||
active_ = false;
|
||||
}
|
||||
|
||||
bool Instrument::isActive() {
|
||||
return (envelope_ > 0.0f);
|
||||
}
|
||||
|
||||
float Instrument::process(bool& scopeTrigger) {
|
||||
|
||||
if(active_ && envelope_ < 1.0f) envelope_ += 0.01f;
|
||||
if(!active_ && envelope_ > 0.0f) envelope_ -= 0.0004f;
|
||||
|
||||
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_;
|
||||
|
||||
}
|
||||
53
src/synth/Instruments/Instrument.hpp
Normal file
53
src/synth/Instruments/Instrument.hpp
Normal file
@@ -0,0 +1,53 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "config/ConfigService.hpp"
|
||||
#include "LoggerService.hpp"
|
||||
#include "config/AudioConfig.hpp"
|
||||
|
||||
// an instrument is our state space model. it calculates discrete samples as a function of time, the current states, and the inputs
|
||||
// an instrument is owned by a voice
|
||||
class Instrument {
|
||||
|
||||
public:
|
||||
|
||||
Instrument() = default;
|
||||
Instrument(ConfigService* config, LoggerService* logger, float sampleRate, AudioParams* params);
|
||||
~Instrument() = default;
|
||||
|
||||
void startNote(uint8_t midiNote, float velocity);
|
||||
virtual void noteOn(float frequency, float velocity);
|
||||
virtual void noteOff();
|
||||
|
||||
virtual bool isActive();
|
||||
|
||||
virtual float process(bool& scopeTrigger);
|
||||
|
||||
uint8_t note() { return note_; }
|
||||
float frequency() { return noteToFrequency(note_); }
|
||||
|
||||
protected:
|
||||
|
||||
inline float noteToFrequency(uint8_t note) {
|
||||
return params_->pitchStandard * pow(params_->octaveConstant, static_cast<float>(note - params_->midiHome) / params_->notesPerOctave);
|
||||
}
|
||||
|
||||
float sampleRate_ = 1.0f;
|
||||
bool active_ = false;
|
||||
|
||||
ConfigService* config_;
|
||||
LoggerService* logger_;
|
||||
AudioParams* params_;
|
||||
|
||||
uint8_t note_;
|
||||
|
||||
static constexpr float pi = 3.14159265358979323846f;
|
||||
float phase_ = 0.0f;
|
||||
float phaseIncrement_ = 0.0f;
|
||||
float envelope_ = 0.0f;
|
||||
|
||||
float targetSample = 0.0f;
|
||||
float currentSample = 0.0f;
|
||||
static constexpr float responsiveness_ = 0.1f;
|
||||
|
||||
};
|
||||
133
src/synth/Instruments/PianoString.cpp
Normal file
133
src/synth/Instruments/PianoString.cpp
Normal file
@@ -0,0 +1,133 @@
|
||||
|
||||
#include "PianoString.hpp"
|
||||
|
||||
PianoString::PianoString(ConfigService* config, LoggerService* logger, float sampleRate, AudioParams* params) : Instrument(config, logger, sampleRate, params) {
|
||||
|
||||
stringY_current_.resize(segmentCount_ + 1);
|
||||
stringY_previous_.resize(segmentCount_ + 1);
|
||||
stringY_next_.resize(segmentCount_ + 1);
|
||||
stringX_.resize(segmentCount_ + 1);
|
||||
|
||||
}
|
||||
|
||||
void PianoString::noteOn(float frequency, float velocity) {
|
||||
|
||||
active_ = true;
|
||||
frequency_ = frequency / 1.03746174945f; // empirical adjustment (original was 83 cents sharp)
|
||||
|
||||
damping_ = 0.5f;
|
||||
rms_ = 5.0f;
|
||||
recalculateConstants();
|
||||
|
||||
// resize the state vectors so that they are stable at the specified frequency
|
||||
stringY_current_.resize(segmentCount_ + 1);
|
||||
stringY_previous_.resize(segmentCount_ + 1);
|
||||
stringY_next_.resize(segmentCount_ + 1);
|
||||
stringX_.resize(segmentCount_ + 1);
|
||||
|
||||
// because stringGrid is resized, reevaluate
|
||||
dx_ = stringLength_ / static_cast<float>(segmentCount_);
|
||||
for(size_t i = 0; i < segmentCount_ + 1; i++) {
|
||||
stringX_[i] = i * dx_;
|
||||
}
|
||||
|
||||
// first iteration
|
||||
for(size_t i = 2; i < segmentCount_ - 2; i++) {
|
||||
stringY_current_[i] = 0.5f * r1_*r1_ * (stringY_previous_[i-1] - 2.0f*stringY_previous_[i] + stringY_previous_[i+1]);
|
||||
}
|
||||
// apply the velocity impulse
|
||||
for(size_t i = 0; i < segmentCount_ + 1; i++) {
|
||||
float v0 = velocity * impulseVelocity_ * std::exp(-1.0f * (stringX_[i] - strikePosition_*L_)*(stringX_[i] - strikePosition_*L_) / ((2.0f * impulseWidth_)*(2.0f * impulseWidth_)));
|
||||
stringY_current_[i] = stringY_current_[i] + dt_ * v0;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
void PianoString::noteOff() {
|
||||
damping_ = 8.0f;
|
||||
recalculateConstants();
|
||||
}
|
||||
|
||||
bool PianoString::isActive() {
|
||||
if(active_ && (std::abs(rms_) < 0.0000001f)) {
|
||||
active_ = false;
|
||||
|
||||
// reset state to stationary
|
||||
for(size_t i = 0; i < segmentCount_ + 1; i++) {
|
||||
stringY_current_[i] = 0.0f;
|
||||
stringY_previous_[i] = 0.0f;
|
||||
stringY_next_[i] = 0.0f;
|
||||
}
|
||||
|
||||
}
|
||||
return active_;
|
||||
}
|
||||
|
||||
float PianoString::process(bool& scopeTrigger) {
|
||||
|
||||
// supersample
|
||||
for(uint32_t n = 0; n < samplingSteps_; n++) {
|
||||
|
||||
// simulate over string
|
||||
for(size_t i = 2; i < segmentCount_ - 2; i++) {
|
||||
float y_xx = stringY_current_[i-1] - 2.0f*stringY_current_[i] + stringY_current_[i+1];
|
||||
float y_xxxx = stringY_current_[i-2] - 4.0f*stringY_current_[i-1] + 6.0f*stringY_current_[i] - 4.0f*stringY_current_[i+1] + stringY_current_[i+2];
|
||||
stringY_next_[i] = a1_ * stringY_current_[i] + a2_ * stringY_previous_[i] + r2_ * y_xx - s2_ * y_xxxx;
|
||||
}
|
||||
stringY_next_[0] = 0.0f;
|
||||
stringY_next_[1] = 0.0f;
|
||||
stringY_next_[segmentCount_-1] = 0.0f;
|
||||
stringY_next_[segmentCount_-2] = 0.0f;
|
||||
|
||||
stringY_previous_ = stringY_current_;
|
||||
stringY_current_ = stringY_next_;
|
||||
}
|
||||
|
||||
phaseTracker_ += frequency_ / sampleRate_;
|
||||
if(phaseTracker_ > 2.0f) {
|
||||
phaseTracker_ -= 2.0f;
|
||||
if(!scopeTrigger) scopeTrigger = true;
|
||||
}
|
||||
|
||||
float sampleOut = stringY_next_[static_cast<size_t>(samplePosition_*segmentCount_)];
|
||||
|
||||
rms_ = 0.99f * rms_ + 0.01f * sampleOut*sampleOut;
|
||||
|
||||
return sampleOut * gain_;
|
||||
|
||||
}
|
||||
|
||||
void PianoString::recalculateConstants() {
|
||||
|
||||
L_ = stringLength_ * std::pow(1.001f, -frequency_ + 20.0f) + 0.5f;
|
||||
|
||||
waveVelocity_ = 2.0f * frequency_ * L_;
|
||||
|
||||
// calculate number of super-sampling steps needed to maintain string stability
|
||||
samplingSteps_ = 0;
|
||||
do {
|
||||
samplingSteps_++;
|
||||
float effectiveSampleRate_ = sampleRate_ * static_cast<float>(samplingSteps_);
|
||||
dt_ = 1.0f / effectiveSampleRate_;
|
||||
|
||||
float a = (waveVelocity_*dt_/L_)*(waveVelocity_*dt_/L_);
|
||||
float b = 4.0f*(stiffness_*dt_/(L_*L_))*(stiffness_*dt_/(L_*L_));
|
||||
if(stiffness_ < 0.00001f) { // avoid divide by zero
|
||||
segmentCount_ = static_cast<size_t>((L_ * effectiveSampleRate_)/waveVelocity_) - 1;
|
||||
} else {
|
||||
segmentCount_ = static_cast<size_t>(std::sqrt((-a+std::sqrt(a*a+4.0*b))/(2.0*b))) - 1;
|
||||
}
|
||||
|
||||
} while (segmentCount_ < 80);
|
||||
// cap segment count at 80 if above
|
||||
segmentCount_ = std::min(segmentCount_, static_cast<size_t>(80));
|
||||
|
||||
dx_ = L_ / static_cast<float>(segmentCount_);
|
||||
|
||||
r1_ = waveVelocity_ * dt_/dx_;
|
||||
r2_ = (waveVelocity_ * dt_/dx_)*(waveVelocity_ * dt_/dx_);
|
||||
s1_ = stiffness_ * dt_/(dx_*dx_);
|
||||
s2_ = (stiffness_ * dt_/(dx_*dx_))*(stiffness_ * dt_/(dx_*dx_));
|
||||
a1_ = 2.0f - 2.0f * damping_ * dt_;
|
||||
a2_ = 2.0f * damping_ * dt_ - 1.0f;
|
||||
}
|
||||
75
src/synth/Instruments/PianoString.hpp
Normal file
75
src/synth/Instruments/PianoString.hpp
Normal file
@@ -0,0 +1,75 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <cmath>
|
||||
|
||||
#include "synth/instruments/Instrument.hpp"
|
||||
|
||||
class PianoString : public Instrument {
|
||||
|
||||
public:
|
||||
|
||||
PianoString() = default;
|
||||
PianoString(ConfigService* config, LoggerService* logger, float sampleRate, AudioParams* params);
|
||||
~PianoString() = default;
|
||||
|
||||
void noteOn(float frequency, float velocity) override;
|
||||
void noteOff() override;
|
||||
|
||||
bool isActive() override;
|
||||
|
||||
float process(bool& scopeTrigger) override;
|
||||
|
||||
private:
|
||||
|
||||
void recalculateConstants();
|
||||
|
||||
// states
|
||||
float frequency_ = 0.0f;
|
||||
std::vector<float> stringY_current_;
|
||||
std::vector<float> stringY_previous_;
|
||||
std::vector<float> stringY_next_;
|
||||
std::vector<float> stringX_;
|
||||
bool active_ = false;
|
||||
|
||||
// constants
|
||||
|
||||
// string parameters
|
||||
size_t segmentCount_ = 80;
|
||||
static constexpr float rho_ = 8000.0f; // density, steel, kg/m^3
|
||||
static constexpr float radius_ = 0.001f; // meters
|
||||
static constexpr float stringTension_ = 1200.0f; // string tension, N
|
||||
static constexpr float stiffness_ = 0.0f; // stiffness coefficient
|
||||
float damping_ = 0.5f; // damping coefficient
|
||||
static constexpr float stringLength_ = 4.0f; // length of string at lowest frequency (20 hz)
|
||||
static constexpr float strikePosition_ = 0.8f; // x of impulse location
|
||||
static constexpr float impulseWidth_ = 0.1f; // x% of impulse width
|
||||
static constexpr float impulseVelocity_ = 10000.0f; // x/t of impulse magnitude
|
||||
static constexpr float samplePosition_ = 0.2f; // percentage along L of sampling for audio
|
||||
float crossSectionalArea_ = pi * std::pow(radius_, 2.0f); // string cross sectional area, assuming circular
|
||||
float mu_ = crossSectionalArea_ * rho_; // linear mass density
|
||||
float waveVelocity_ = std::sqrt(stringTension_ / mu_); // transverse wave velocity
|
||||
float L_ = 1.0f; // length of string at a given frequency
|
||||
float gain_ = 0.5f;
|
||||
|
||||
// f0_ = waveVelocity_ / (2.0f * stringLength_); // fundamental frequency of a non-stiff string
|
||||
// f1_ = f0_ * std::sqrt(1.0f + stiffness_); // fundamental frequency of the stiff string
|
||||
|
||||
uint32_t samplingSteps_ = 1;
|
||||
float phaseTracker_ = 0.0f;
|
||||
|
||||
float dx_ = L_ / static_cast<float>(segmentCount_);
|
||||
float dt_ = 1.0f / sampleRate_;
|
||||
|
||||
// derived constants
|
||||
float r1_ = waveVelocity_ * dt_/dx_;
|
||||
float r2_ = std::pow(waveVelocity_ * dt_/dx_, 2.0f);
|
||||
float s1_ = stiffness_ * dt_/std::pow(dx_, 2.0f);
|
||||
float s2_ = std::pow(stiffness_ * dt_/std::pow(dx_, 2.0f), 2.0f);
|
||||
float a1_ = 2.0f - 2.0f * damping_ * dt_;
|
||||
float a2_ = 2.0f * damping_ * dt_ - 1.0f;
|
||||
|
||||
// keeping track of the string's activeness
|
||||
float rms_ = 0.0f;
|
||||
|
||||
};
|
||||
41
src/synth/Instruments/WavetableOscillator.cpp
Normal file
41
src/synth/Instruments/WavetableOscillator.cpp
Normal file
@@ -0,0 +1,41 @@
|
||||
|
||||
#include "WavetableOscillator.hpp"
|
||||
|
||||
|
||||
WavetableOscillator::WavetableOscillator(ConfigService* config, LoggerService* logger, WavetableLoader* wavetabler, float sampleRate, AudioParams* params)
|
||||
: Instrument(config, logger, sampleRate, params), wavetableLoader_(wavetabler) {
|
||||
|
||||
}
|
||||
|
||||
void WavetableOscillator::noteOn(float frequency, float velocity) {
|
||||
envelope_.noteOn(); // hmm we can scale this by velocity
|
||||
|
||||
frequency_ = frequency;
|
||||
phaseIncrement_ = 2.0f * kPi_ * frequency_ / sampleRate_;
|
||||
|
||||
}
|
||||
|
||||
void WavetableOscillator::noteOff() {
|
||||
envelope_.noteOff();
|
||||
}
|
||||
|
||||
bool WavetableOscillator::isActive() {
|
||||
return envelope_.isActive();
|
||||
}
|
||||
|
||||
float WavetableOscillator::process(bool& scopeTrigger) {
|
||||
|
||||
if(wavetableLoader_ == nullptr) {
|
||||
return 0.0f;
|
||||
}
|
||||
|
||||
float sampleOut = wavetableLoader_->sample(wavetableId_, phase_);
|
||||
|
||||
phase_ += phaseIncrement_;
|
||||
if(phase_ > 2.0f * kPi_) {
|
||||
phase_ -= 2.0f * kPi_;
|
||||
scopeTrigger = true;
|
||||
}
|
||||
|
||||
return sampleOut;
|
||||
}
|
||||
40
src/synth/Instruments/WavetableOscillator.hpp
Normal file
40
src/synth/Instruments/WavetableOscillator.hpp
Normal file
@@ -0,0 +1,40 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <cmath>
|
||||
|
||||
#include "synth/instruments/Instrument.hpp"
|
||||
#include "synth/Envelope.hpp"
|
||||
#include "synth/WavetableLoader.hpp"
|
||||
|
||||
class WavetableOscillator : public Instrument {
|
||||
|
||||
public:
|
||||
|
||||
WavetableOscillator() = default;
|
||||
WavetableOscillator(ConfigService* config, LoggerService* logger, WavetableLoader* wavetabler, float sampleRate, AudioParams* params);
|
||||
~WavetableOscillator() = default;
|
||||
|
||||
void noteOn(float frequency, float velocity) override;
|
||||
void noteOff() override;
|
||||
|
||||
bool isActive() override;
|
||||
|
||||
float process(bool& scopeTrigger) override;
|
||||
|
||||
bool setWavetableId(size_t id) { wavetableId_ = id; }
|
||||
|
||||
private:
|
||||
|
||||
WavetableLoader* wavetableLoader_;
|
||||
size_t wavetableId_ = 0;
|
||||
|
||||
Envelope envelope_;
|
||||
|
||||
// float phase_ = 0.0f; inherited from parent class
|
||||
float frequency_ = 0.0f;
|
||||
float phaseIncrement = 0.0f;
|
||||
|
||||
static constexpr float kPi_ = 3.14159265358979323846;
|
||||
|
||||
};
|
||||
@@ -12,8 +12,8 @@ KeyboardController::KeyboardController(QObject* parent) : QObject(parent) {
|
||||
KeyboardController::KeyboardController(ConfigService* config, LoggerService* logger, NoteQueue* queue) : config_(config), logger_(logger), queue_(queue) {
|
||||
|
||||
// load keymap from config service
|
||||
if(!(config->getConfig<KeymapConfig>("KeyboardController", "Main", &configuration_))) {
|
||||
logger_->log("Keyboard", LogFlag::Error, "Failed to get logger configuration fom config service");
|
||||
if(!(config->getConfig<KeymapParams, KeymapConfig>("KeyboardController", "Main", &configuration_))) {
|
||||
logger_->log("Keyboard", LogFlag::Error, "Failed to get logger configuration from config service");
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -7,7 +7,8 @@
|
||||
#include <unordered_map>
|
||||
|
||||
#include "NoteQueue.hpp"
|
||||
#include "ConfigService.hpp"
|
||||
#include "config/ConfigService.hpp"
|
||||
#include "config/KeymapConfig.hpp"
|
||||
#include "LoggerService.hpp"
|
||||
|
||||
// The keyboardcontroller handles user inputs from a keyboard and maps them to note events
|
||||
@@ -32,7 +33,7 @@ private:
|
||||
LoggerService* logger_;
|
||||
|
||||
// keymap is key -> midi note id
|
||||
KeymapConfig configuration_;
|
||||
KeymapParams configuration_;
|
||||
|
||||
static constexpr float defaultVelocity_ = 0.8f;
|
||||
|
||||
|
||||
@@ -4,14 +4,25 @@
|
||||
#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
|
||||
|
||||
#ifdef WIN32
|
||||
openDefaultPort();
|
||||
#else
|
||||
openPort(1);
|
||||
#endif
|
||||
}
|
||||
|
||||
MidiController::~MidiController() {
|
||||
@@ -19,18 +30,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 +57,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 +92,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 +128,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 +142,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 "config/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 "config/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) {
|
||||
// timer is set in qml
|
||||
}
|
||||
|
||||
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 ? probably make it configurable if you want to make scope options
|
||||
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 "config/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_;
|
||||
|
||||
@@ -2,21 +2,43 @@
|
||||
#include "Synth.hpp"
|
||||
|
||||
Synth::Synth(ConfigService* config, LoggerService* logger, ScopeBuffer* scope, NoteQueue* queue) :
|
||||
config_(config), logger_(logger), scope_(scope), noteQueue_(queue) {
|
||||
config_(config), logger_(logger), scope_(scope), noteQueue_(queue), instrumentFactory_(config_, logger_) {
|
||||
|
||||
voices_.fill(Voice(config_, logger_));
|
||||
// load audio settings from config service
|
||||
if(!(config->getConfig<AudioParams, AudioConfig>("AudioEngine", "Main", &engineConfiguration_))) {
|
||||
logger_->log("Audio", LogFlag::Error, "Failed to get logger configuration from config service");
|
||||
return;
|
||||
}
|
||||
// note: this is the audio engine's configuration so we can grab sample rate
|
||||
// it includes some synth params as well, so when synth gets its own config node they all need to be moved
|
||||
|
||||
int32_t instrumentsCreated = instrumentFactory_.createInstruments(&instruments_, engineConfiguration_.maxVoices, static_cast<float>(engineConfiguration_.sampleRate), &engineConfiguration_);
|
||||
if(instrumentsCreated == 0) {
|
||||
logger_->log("Audio", LogFlag::Error, "Failed to create any voices from factory.");
|
||||
} else if(instrumentsCreated > engineConfiguration_.maxVoices) {
|
||||
logger_->log("Audio", LogFlag::Warning, "Created too many voices, possible truncation or overflow.");
|
||||
} else {
|
||||
logger_->log("Audio", LogFlag::Info, "Created voices from factory.");
|
||||
}
|
||||
|
||||
filter_.setSampleRate(engineConfiguration_.sampleRate);
|
||||
filter_.setParams(Filter::Type::BiquadLowpass, 4000.0f, 0.707f);
|
||||
|
||||
}
|
||||
|
||||
void Synth::handleNoteEvent(const NoteEvent& event) {
|
||||
|
||||
Voice* v = findVoiceByNote(event.note);
|
||||
if(v != nullptr) v->noteOff();
|
||||
// stop all voices that are currently playing this note
|
||||
for(Instrument* i : instruments_) {
|
||||
if(i->isActive() && i->note() == event.note) {
|
||||
i->noteOff();
|
||||
}
|
||||
}
|
||||
|
||||
// call note-on for an idle voice for the note-event
|
||||
if(event.type == NoteEventType::NoteOn) {
|
||||
|
||||
v = findFreeVoice();
|
||||
if(v != nullptr) v->noteOn(event.note, event.velocity);
|
||||
Instrument* i = findFreeVoice();
|
||||
if(i != nullptr) i->startNote(event.note, event.velocity);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -28,59 +50,67 @@ 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;
|
||||
// }
|
||||
// }
|
||||
// find lowest frequency voice for scope triggering
|
||||
size_t lowestVoice = 0;
|
||||
float lowestFreq = FLT_MAX;
|
||||
for(size_t i = 0; i < instruments_.size(); i++) {
|
||||
if(!instruments_[i]->isActive()) continue;
|
||||
float currentFreq = instruments_[i]->frequency();
|
||||
if(currentFreq < lowestFreq) {
|
||||
lowestVoice = i;
|
||||
lowestFreq = currentFreq;
|
||||
}
|
||||
}
|
||||
|
||||
scope_->spinlock(true);
|
||||
|
||||
float sampleOut = 0.0f;
|
||||
bool triggered = false;
|
||||
bool once = false;
|
||||
|
||||
// generate n samples for the output buffer
|
||||
for(size_t i = 0; i < nFrames; i++) {
|
||||
|
||||
// process all active voices and mix their produced samples
|
||||
float mix = 0.0f;
|
||||
for(size_t j = 0; j < voices_.size(); j++) {
|
||||
for(size_t j = 0; j < instruments_.size(); j++) {
|
||||
bool temp = false;
|
||||
//if(!voices_[j].isActive()) continue;
|
||||
mix += voices_[j].process(temp);
|
||||
// if(j == lowestVoice) triggered = temp;
|
||||
if(!instruments_[j]->isActive()) continue;
|
||||
mix += instruments_[j]->process(temp);
|
||||
if(j == lowestVoice) triggered = temp;
|
||||
}
|
||||
mix = tanh(mix/4.0f); // prevent clipping
|
||||
mix = tanh(mix); // prevent clipping
|
||||
|
||||
sampleOut = mix;
|
||||
sampleOut = filter_.biquadProcess(mix);
|
||||
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);
|
||||
|
||||
}
|
||||
|
||||
Instrument* Synth::findFreeVoice() {
|
||||
|
||||
}
|
||||
|
||||
Voice* Synth::findFreeVoice() {
|
||||
|
||||
for(Voice& v : voices_) {
|
||||
if(!v.isActive()) {
|
||||
return &v;
|
||||
for(Instrument* i : instruments_) {
|
||||
if(!i->isActive()) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
Voice* Synth::findVoiceByNote(uint8_t note) {
|
||||
Instrument* Synth::findVoiceByNote(uint8_t note) {
|
||||
|
||||
for(Voice& v : voices_) {
|
||||
if(v.isActive() && v.note() == note) {
|
||||
return &v;
|
||||
for(Instrument* i : instruments_) {
|
||||
if(i->isActive() && i->note() == note) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
return nullptr;
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "ConfigService.hpp"
|
||||
#include "config/ConfigService.hpp"
|
||||
#include "config/AudioConfig.hpp"
|
||||
#include "LoggerService.hpp"
|
||||
#include "NoteQueue.hpp"
|
||||
#include "Voice.hpp"
|
||||
#include "Scope.hpp"
|
||||
#include "Filter.hpp"
|
||||
#include "Instruments/Instrument.hpp"
|
||||
#include "InstrumentFactory.hpp"
|
||||
|
||||
#include <vector>
|
||||
|
||||
@@ -21,18 +24,21 @@ public:
|
||||
|
||||
private:
|
||||
|
||||
Voice* findFreeVoice();
|
||||
Voice* findVoiceByNote(uint8_t note);
|
||||
Instrument* findFreeVoice();
|
||||
Instrument* findVoiceByNote(uint8_t note);
|
||||
|
||||
std::vector<uint8_t> sustainedNotes_;
|
||||
Filter filter_;
|
||||
|
||||
// voices
|
||||
static constexpr size_t MAX_VOICES = 32;
|
||||
std::array<Voice, MAX_VOICES> voices_;
|
||||
size_t maxVoices;
|
||||
std::vector<Instrument*> instruments_;
|
||||
|
||||
ConfigService* config_;
|
||||
AudioParams engineConfiguration_;
|
||||
LoggerService* logger_;
|
||||
ScopeBuffer* scope_ = nullptr;
|
||||
ScopeBuffer* scope_;
|
||||
NoteQueue* noteQueue_;
|
||||
InstrumentFactory instrumentFactory_;
|
||||
|
||||
};
|
||||
|
||||
@@ -1,34 +0,0 @@
|
||||
|
||||
#include "Voice.hpp"
|
||||
|
||||
Voice::Voice(ConfigService* config, LoggerService* logger) :
|
||||
config_(config), logger_(logger) {
|
||||
|
||||
instrument_ = Instrument(config_, logger_);
|
||||
|
||||
}
|
||||
|
||||
void Voice::noteOn(uint8_t midiNote, float velocity) {
|
||||
|
||||
note_ = midiNote;
|
||||
instrument_.noteOn(noteToFrequency(midiNote), velocity);
|
||||
|
||||
}
|
||||
|
||||
void Voice::noteOff() {
|
||||
|
||||
instrument_.noteOff();
|
||||
|
||||
}
|
||||
|
||||
bool Voice::isActive() {
|
||||
|
||||
return instrument_.isActive();
|
||||
|
||||
}
|
||||
|
||||
float Voice::process(bool& scopeTrigger) {
|
||||
|
||||
return instrument_.process(scopeTrigger);
|
||||
|
||||
}
|
||||
@@ -1,41 +0,0 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
#include "Instrument.hpp"
|
||||
|
||||
// a voice is a tone generator that the synth uses for polyphony
|
||||
// the synth mixes multiple voices together into a polyphonic audio. calculations for samples are handled in the instrument
|
||||
class Voice {
|
||||
|
||||
public:
|
||||
|
||||
Voice() = default;
|
||||
Voice(ConfigService* config, LoggerService* logger);
|
||||
~Voice() = default;
|
||||
|
||||
void noteOn(uint8_t midiNote, float velocity);
|
||||
void noteOff();
|
||||
bool isActive();
|
||||
|
||||
float process(bool& scopeTrigger);
|
||||
uint8_t note() { return note_; }
|
||||
|
||||
private:
|
||||
|
||||
float sampleRate_ = 44100.0f;
|
||||
|
||||
inline float noteToFrequency(uint8_t note) {
|
||||
return 440.0f * pow(2.0f, static_cast<float>(note - 69) / static_cast<float>(12));
|
||||
}
|
||||
|
||||
uint8_t note_ = 0;
|
||||
float velocity_ = 1.0f;
|
||||
bool active_ = false;
|
||||
|
||||
ConfigService* config_;
|
||||
LoggerService* logger_;
|
||||
Instrument instrument_;
|
||||
|
||||
};
|
||||
58
src/synth/WavetableLoader.cpp
Normal file
58
src/synth/WavetableLoader.cpp
Normal file
@@ -0,0 +1,58 @@
|
||||
|
||||
#include "WavetableLoader.hpp"
|
||||
|
||||
WavetableLoader::WavetableLoader(ConfigService* config, LoggerService* logger) : config_(config), logger_(logger) {
|
||||
|
||||
}
|
||||
|
||||
int32_t WavetableLoader::loadWavetables() {
|
||||
|
||||
// find wavetable files
|
||||
std::vector<std::filesystem::path> wavetableFiles;
|
||||
for(std::filesystem::directory_entry entry : std::filesystem::directory_iterator(wavetableFilepath_)) {
|
||||
if(std::filesystem::is_regular_file(entry.status())) {
|
||||
wavetableFiles.push_back(entry.path()); // TODO: check this file to see if it is valid
|
||||
}
|
||||
}
|
||||
wavetableCount_ = wavetableFiles.size();
|
||||
wavetables_.resize(wavetableCount_);
|
||||
|
||||
// load from file to vector
|
||||
for(size_t i = 0; i < wavetableCount_; i++) {
|
||||
// logger->log(loading wavetable file...)
|
||||
std::ifstream inputFile(wavetableFiles[i], std::ios::in | std::ios::binary);
|
||||
if(!inputFile) // logger->log("error oppening file...")
|
||||
inputFile.read(reinterpret_cast<char*>(wavetables_[i].data()), 2048 * sizeof(float));
|
||||
}
|
||||
|
||||
// TODO: wavetable header should contain info like number of samples
|
||||
|
||||
return wavetableCount_;
|
||||
|
||||
}
|
||||
|
||||
float WavetableLoader::sample(size_t wavetableIndex, float phase) {
|
||||
|
||||
float sampleValue = 0.0f;
|
||||
|
||||
if(phase >= 0.0f) {
|
||||
while(phase >= 2.0f*kPi_) {
|
||||
phase -= 2.0f*kPi_;
|
||||
}
|
||||
} else {
|
||||
while(phase <= 0.0f) {
|
||||
phase += 2.0f*kPi_;
|
||||
}
|
||||
}
|
||||
|
||||
float scaledPhase = phase * static_cast<float>(2048) / (2.0f*kPi_);
|
||||
// TODO: I'd like the default domain of wavetables to be [0, 1) but it should also be loaded in from the wavetable file metadata
|
||||
size_t index = static_cast<size_t>(round(scaledPhase));
|
||||
if(index >= 2048) index = 2048 - 1;
|
||||
|
||||
if(wavetableIndex >= wavetableCount_) {
|
||||
wavetableIndex = wavetableCount_ - 1;
|
||||
}
|
||||
|
||||
return wavetables_[wavetableIndex][index];
|
||||
}
|
||||
42
src/synth/WavetableLoader.hpp
Normal file
42
src/synth/WavetableLoader.hpp
Normal file
@@ -0,0 +1,42 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#pragma once
|
||||
|
||||
// placeholder to handle loading wavetables from files
|
||||
// so that wavetables can be shared to each voice/instrument
|
||||
// only need to need to load once for x number of voices
|
||||
|
||||
#include <vector>
|
||||
#include <cstdint>
|
||||
#include <filesystem>
|
||||
|
||||
#include "config/ConfigService.hpp"
|
||||
#include "LoggerService.hpp"
|
||||
|
||||
class WavetableLoader {
|
||||
|
||||
public:
|
||||
|
||||
WavetableLoader(ConfigService* config, LoggerService* logger);
|
||||
~WavetableLoader() = default;
|
||||
|
||||
float sample(size_t wavetableIndex, float phase);
|
||||
|
||||
private:
|
||||
|
||||
int32_t loadWavetables();
|
||||
|
||||
size_t wavetableCount_ = 0;
|
||||
std::vector<std::vector<float>> wavetables_;
|
||||
|
||||
// TODO: this will be configurable
|
||||
|
||||
const std::filesystem::path wavetableFilepath_ = "./config/wavetables";
|
||||
|
||||
LoggerService* logger_;
|
||||
ConfigService* config_;
|
||||
|
||||
static constexpr float kPi_ = 3.14159265358979323846;
|
||||
|
||||
};
|
||||
@@ -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