Compare commits
11 Commits
b3d227dc71
...
feature/wa
| Author | SHA1 | Date | |
|---|---|---|---|
| 5d86018d06 | |||
| 54996469d1 | |||
| 2436946a3c | |||
| b00e7a053c | |||
| fdb55bb78d | |||
| 8de4081a50 | |||
| eb84c93b7b | |||
| 295a869839 | |||
| 9365e12bb4 | |||
| 269648e021 | |||
| 41f7499c23 |
2
.gitignore
vendored
2
.gitignore
vendored
@@ -1,2 +1,4 @@
|
|||||||
|
|
||||||
build/*
|
build/*
|
||||||
|
|
||||||
|
*/__pycache__/*
|
||||||
@@ -46,7 +46,7 @@ find_package(Qt6 REQUIRED COMPONENTS
|
|||||||
qt_standard_project_setup()
|
qt_standard_project_setup()
|
||||||
|
|
||||||
add_library(sonobulus_core STATIC
|
add_library(sonobulus_core STATIC
|
||||||
src/ConfigService.cpp
|
src/config/ConfigService.cpp
|
||||||
src/LoggerService.cpp
|
src/LoggerService.cpp
|
||||||
src/TimerComponent.cpp
|
src/TimerComponent.cpp
|
||||||
src/synth/AudioEngine.cpp
|
src/synth/AudioEngine.cpp
|
||||||
@@ -55,10 +55,14 @@ add_library(sonobulus_core STATIC
|
|||||||
src/synth/NoteQueue.cpp
|
src/synth/NoteQueue.cpp
|
||||||
src/synth/Scope.cpp
|
src/synth/Scope.cpp
|
||||||
src/synth/Synth.cpp
|
src/synth/Synth.cpp
|
||||||
src/synth/Voice.cpp
|
#src/synth/Voice.cpp
|
||||||
src/synth/Filter.cpp
|
src/synth/Filter.cpp
|
||||||
src/synth/Instrument.cpp
|
src/synth/WavetableLoader.cpp
|
||||||
src/synth/Instruments/PianoString.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)
|
if (WIN32)
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ Logger = (
|
|||||||
"Info",
|
"Info",
|
||||||
"Warning",
|
"Warning",
|
||||||
"Error"
|
"Error"
|
||||||
);
|
); # TODO: console flags vs file flags
|
||||||
|
|
||||||
ShowTime = false;
|
ShowTime = false;
|
||||||
ShowSourceTrace = false;
|
ShowSourceTrace = false;
|
||||||
@@ -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 = (
|
KeyboardController = (
|
||||||
{
|
{
|
||||||
Id = "Main";
|
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.
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_;
|
|
||||||
};
|
|
||||||
@@ -13,17 +13,11 @@ namespace fs = std::filesystem;
|
|||||||
|
|
||||||
LoggerService::LoggerService(ConfigService* config, const std::string& loggerId) {
|
LoggerService::LoggerService(ConfigService* config, const std::string& loggerId) {
|
||||||
|
|
||||||
if(!(config->getConfig<LoggerConfig>("Logger", loggerId, &configuration_))) {
|
if(!(config->getConfig<LoggerParams, LoggerConfig>("Logger", loggerId, &configuration_))) {
|
||||||
std::cout << "Failed to get logger configuration fom config service" << std::endl;
|
std::cout << "Failed to get logger configuration from config service" << std::endl;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
standardOutputEnabled_ = configuration_.coutEnabled;
|
|
||||||
fileOutputEnabled_ = configuration_.fileEnabled;
|
|
||||||
additionaldetailsEnabled_ = configuration_.showSourceTrace;
|
|
||||||
timeEnabled_ = configuration_.showTime;
|
|
||||||
id_ = configuration_.id;
|
|
||||||
|
|
||||||
for(std::string& flag : configuration_.flagsEnabled) {
|
for(std::string& flag : configuration_.flagsEnabled) {
|
||||||
bool found = false;
|
bool found = false;
|
||||||
for(const char* validFlag : LogFlagStrings) {
|
for(const char* validFlag : LogFlagStrings) {
|
||||||
@@ -55,7 +49,7 @@ LoggerService::LoggerService(ConfigService* config, const std::string& loggerId)
|
|||||||
fs::create_directories(std::string(BINARY_DIR) + "/" + finaltime);
|
fs::create_directories(std::string(BINARY_DIR) + "/" + finaltime);
|
||||||
std::string logPath = std::string(BINARY_DIR) + "/" + finaltime + "/" + configuration_.filePath;
|
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.");
|
log("Logger", LogFlag::Info, "Logger initialized.");
|
||||||
}
|
}
|
||||||
@@ -78,11 +72,11 @@ void LoggerService::log(std::string component, LogFlag flag, std::string message
|
|||||||
|
|
||||||
std::string finalmessage = "";
|
std::string finalmessage = "";
|
||||||
|
|
||||||
if(timeEnabled_) {
|
if(configuration_.showTime) {
|
||||||
finalmessage = finalmessage + "[" + "Not Implemented" + "] ";
|
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
|
finalmessage += componentTrace; // component is the section of the program (For example Mesh or Engine) that is calling the logger
|
||||||
|
|
||||||
std::string level = "";
|
std::string level = "";
|
||||||
@@ -94,15 +88,15 @@ void LoggerService::log(std::string component, LogFlag flag, std::string message
|
|||||||
finalmessage = finalmessage + "[" + level + "] ";
|
finalmessage = finalmessage + "[" + level + "] ";
|
||||||
finalmessage = finalmessage + message + " ";
|
finalmessage = finalmessage + message + " ";
|
||||||
|
|
||||||
if (additionaldetailsEnabled_) {
|
if (configuration_.showSourceTrace) {
|
||||||
finalmessage = finalmessage + "[Function: " + Source.function_name() + "]" + " " + "[Line: " + std::to_string(Source.line()) + "]" + " " + "[File: " + Source.file_name() + "]";
|
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;
|
std::cout << finalmessage << std::endl;
|
||||||
}
|
}
|
||||||
|
|
||||||
if(fileOutputEnabled_) {
|
if(configuration_.fileEnabled) {
|
||||||
outfile_ << finalmessage << std::endl;
|
outfile_ << finalmessage << std::endl;
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -5,7 +5,8 @@
|
|||||||
#include <fstream>
|
#include <fstream>
|
||||||
#include <source_location>
|
#include <source_location>
|
||||||
|
|
||||||
#include "ConfigService.hpp"
|
#include "config/ConfigService.hpp"
|
||||||
|
#include "config/LoggerConfig.hpp"
|
||||||
|
|
||||||
enum LogFlag {
|
enum LogFlag {
|
||||||
Debug,
|
Debug,
|
||||||
@@ -33,14 +34,8 @@ public:
|
|||||||
|
|
||||||
private:
|
private:
|
||||||
|
|
||||||
bool standardOutputEnabled_;
|
|
||||||
bool fileOutputEnabled_;
|
|
||||||
bool additionaldetailsEnabled_;
|
|
||||||
bool timeEnabled_;
|
|
||||||
std::ofstream outfile_;
|
std::ofstream outfile_;
|
||||||
std::string id_;
|
|
||||||
|
|
||||||
std::vector<LogFlag> activeFlags_;
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
};
|
||||||
@@ -7,7 +7,7 @@
|
|||||||
|
|
||||||
#include "TimerComponent.hpp"
|
#include "TimerComponent.hpp"
|
||||||
#include "LoggerService.hpp"
|
#include "LoggerService.hpp"
|
||||||
#include "ConfigService.hpp"
|
#include "config/ConfigService.hpp"
|
||||||
#include "synth/AudioEngine.hpp"
|
#include "synth/AudioEngine.hpp"
|
||||||
#include "synth/KeyboardController.hpp"
|
#include "synth/KeyboardController.hpp"
|
||||||
#include "synth/Scope.hpp"
|
#include "synth/Scope.hpp"
|
||||||
|
|||||||
@@ -8,6 +8,12 @@
|
|||||||
|
|
||||||
AudioEngine::AudioEngine(ConfigService* config, LoggerService* logger, Synth* synth) : config_(config), logger_(logger), synth_(synth) {
|
AudioEngine::AudioEngine(ConfigService* config, LoggerService* logger, Synth* synth) : config_(config), logger_(logger), synth_(synth) {
|
||||||
|
|
||||||
|
// 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(audioDevice_.getDeviceCount() < 1) {
|
if(audioDevice_.getDeviceCount() < 1) {
|
||||||
logger_->log("Audio", LogFlag::Error, "No audio devices found.");
|
logger_->log("Audio", LogFlag::Error, "No audio devices found.");
|
||||||
}
|
}
|
||||||
@@ -22,13 +28,22 @@ bool AudioEngine::start() {
|
|||||||
// initialize the audio engine
|
// initialize the audio engine
|
||||||
RtAudio::StreamParameters params;
|
RtAudio::StreamParameters params;
|
||||||
params.deviceId = audioDevice_.getDefaultOutputDevice();
|
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;
|
params.firstChannel = 0;
|
||||||
|
|
||||||
RtAudio::StreamOptions options;
|
RtAudio::StreamOptions options;
|
||||||
options.flags = RTAUDIO_MINIMIZE_LATENCY;
|
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) {
|
if(status != RTAUDIO_NO_ERROR) {
|
||||||
logger_->log("Audio", LogFlag::Error, "Error opening RtAudio stream.");
|
logger_->log("Audio", LogFlag::Error, "Error opening RtAudio stream.");
|
||||||
return false;
|
return false;
|
||||||
@@ -41,7 +56,7 @@ bool AudioEngine::start() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// sanity check
|
// 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);
|
logger_->log("AudioEngine", LogFlag::Info, msg);
|
||||||
return true;
|
return true;
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,8 @@
|
|||||||
#include <RtAudio.h>
|
#include <RtAudio.h>
|
||||||
|
|
||||||
#include "LoggerService.hpp"
|
#include "LoggerService.hpp"
|
||||||
#include "ConfigService.hpp"
|
#include "config/ConfigService.hpp"
|
||||||
|
#include "config/AudioConfig.hpp"
|
||||||
#include "Synth.hpp"
|
#include "Synth.hpp"
|
||||||
#include "NoteQueue.hpp"
|
#include "NoteQueue.hpp"
|
||||||
|
|
||||||
@@ -35,13 +36,10 @@ private:
|
|||||||
|
|
||||||
RtAudio audioDevice_ { AUDIO_API };
|
RtAudio audioDevice_ { AUDIO_API };
|
||||||
|
|
||||||
// TODO: make these configurable
|
|
||||||
uint32_t sampleRate_ = 44100;
|
|
||||||
uint32_t bufferFrames_ = 512;
|
|
||||||
uint32_t channels_ = 2;
|
|
||||||
|
|
||||||
LoggerService* logger_;
|
LoggerService* logger_;
|
||||||
ConfigService* config_;
|
ConfigService* config_;
|
||||||
|
AudioParams configuration_;
|
||||||
|
|
||||||
Synth* synth_;
|
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;
|
||||||
|
|
||||||
|
};
|
||||||
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;
|
||||||
|
|
||||||
|
};
|
||||||
@@ -3,8 +3,16 @@
|
|||||||
|
|
||||||
#include "string"
|
#include "string"
|
||||||
|
|
||||||
Instrument::Instrument(ConfigService* config, LoggerService* logger) :
|
Instrument::Instrument(ConfigService* config, LoggerService* logger, float sampleRate, AudioParams* params) :
|
||||||
config_(config), logger_(logger) {
|
config_(config), logger_(logger), sampleRate_(sampleRate), params_(params) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
void Instrument::startNote(uint8_t midiNote, float velocity) {
|
||||||
|
|
||||||
|
note_ = midiNote;
|
||||||
|
|
||||||
|
noteOn(noteToFrequency(midiNote), velocity);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1,19 +1,21 @@
|
|||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include "ConfigService.hpp"
|
#include "config/ConfigService.hpp"
|
||||||
#include "LoggerService.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 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
|
// an instrument is owned by a voice
|
||||||
class Instrument {
|
class Instrument {
|
||||||
|
|
||||||
public:
|
public:
|
||||||
|
|
||||||
Instrument() = default;
|
Instrument() = default;
|
||||||
Instrument(ConfigService* config, LoggerService* logger);
|
Instrument(ConfigService* config, LoggerService* logger, float sampleRate, AudioParams* params);
|
||||||
~Instrument() = default;
|
~Instrument() = default;
|
||||||
|
|
||||||
|
void startNote(uint8_t midiNote, float velocity);
|
||||||
virtual void noteOn(float frequency, float velocity);
|
virtual void noteOn(float frequency, float velocity);
|
||||||
virtual void noteOff();
|
virtual void noteOff();
|
||||||
|
|
||||||
@@ -21,13 +23,23 @@ public:
|
|||||||
|
|
||||||
virtual float process(bool& scopeTrigger);
|
virtual float process(bool& scopeTrigger);
|
||||||
|
|
||||||
|
uint8_t note() { return note_; }
|
||||||
|
float frequency() { return noteToFrequency(note_); }
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
|
|
||||||
float sampleRate_ = 44100.0f;
|
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;
|
bool active_ = false;
|
||||||
|
|
||||||
ConfigService* config_;
|
ConfigService* config_;
|
||||||
LoggerService* logger_;
|
LoggerService* logger_;
|
||||||
|
AudioParams* params_;
|
||||||
|
|
||||||
|
uint8_t note_;
|
||||||
|
|
||||||
static constexpr float pi = 3.14159265358979323846f;
|
static constexpr float pi = 3.14159265358979323846f;
|
||||||
float phase_ = 0.0f;
|
float phase_ = 0.0f;
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
|
|
||||||
#include "PianoString.hpp"
|
#include "PianoString.hpp"
|
||||||
|
|
||||||
PianoString::PianoString(ConfigService* config, LoggerService* logger) : Instrument(config, logger) {
|
PianoString::PianoString(ConfigService* config, LoggerService* logger, float sampleRate, AudioParams* params) : Instrument(config, logger, sampleRate, params) {
|
||||||
|
|
||||||
stringY_current_.resize(segmentCount_ + 1);
|
stringY_current_.resize(segmentCount_ + 1);
|
||||||
stringY_previous_.resize(segmentCount_ + 1);
|
stringY_previous_.resize(segmentCount_ + 1);
|
||||||
@@ -13,7 +13,7 @@ PianoString::PianoString(ConfigService* config, LoggerService* logger) : Instrum
|
|||||||
void PianoString::noteOn(float frequency, float velocity) {
|
void PianoString::noteOn(float frequency, float velocity) {
|
||||||
|
|
||||||
active_ = true;
|
active_ = true;
|
||||||
frequency_ = frequency;
|
frequency_ = frequency / 1.03746174945f; // empirical adjustment (original was 83 cents sharp)
|
||||||
|
|
||||||
damping_ = 0.5f;
|
damping_ = 0.5f;
|
||||||
rms_ = 5.0f;
|
rms_ = 5.0f;
|
||||||
@@ -84,9 +84,9 @@ float PianoString::process(bool& scopeTrigger) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
phaseTracker_ += frequency_ / sampleRate_;
|
phaseTracker_ += frequency_ / sampleRate_;
|
||||||
if(phaseTracker_ > 1.0f) {
|
if(phaseTracker_ > 2.0f) {
|
||||||
phaseTracker_ -= 1.0f;
|
phaseTracker_ -= 2.0f;
|
||||||
scopeTrigger = true;
|
if(!scopeTrigger) scopeTrigger = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
float sampleOut = stringY_next_[static_cast<size_t>(samplePosition_*segmentCount_)];
|
float sampleOut = stringY_next_[static_cast<size_t>(samplePosition_*segmentCount_)];
|
||||||
@@ -103,6 +103,7 @@ void PianoString::recalculateConstants() {
|
|||||||
|
|
||||||
waveVelocity_ = 2.0f * frequency_ * L_;
|
waveVelocity_ = 2.0f * frequency_ * L_;
|
||||||
|
|
||||||
|
// calculate number of super-sampling steps needed to maintain string stability
|
||||||
samplingSteps_ = 0;
|
samplingSteps_ = 0;
|
||||||
do {
|
do {
|
||||||
samplingSteps_++;
|
samplingSteps_++;
|
||||||
@@ -117,8 +118,8 @@ void PianoString::recalculateConstants() {
|
|||||||
segmentCount_ = static_cast<size_t>(std::sqrt((-a+std::sqrt(a*a+4.0*b))/(2.0*b))) - 1;
|
segmentCount_ = static_cast<size_t>(std::sqrt((-a+std::sqrt(a*a+4.0*b))/(2.0*b))) - 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
} while (segmentCount_ < 60);
|
} while (segmentCount_ < 80);
|
||||||
|
// cap segment count at 80 if above
|
||||||
segmentCount_ = std::min(segmentCount_, static_cast<size_t>(80));
|
segmentCount_ = std::min(segmentCount_, static_cast<size_t>(80));
|
||||||
|
|
||||||
dx_ = L_ / static_cast<float>(segmentCount_);
|
dx_ = L_ / static_cast<float>(segmentCount_);
|
||||||
|
|||||||
@@ -3,14 +3,14 @@
|
|||||||
|
|
||||||
#include <cmath>
|
#include <cmath>
|
||||||
|
|
||||||
#include "synth/Instrument.hpp"
|
#include "synth/instruments/Instrument.hpp"
|
||||||
|
|
||||||
class PianoString : public Instrument {
|
class PianoString : public Instrument {
|
||||||
|
|
||||||
public:
|
public:
|
||||||
|
|
||||||
PianoString() = default;
|
PianoString() = default;
|
||||||
PianoString(ConfigService* config, LoggerService* logger);
|
PianoString(ConfigService* config, LoggerService* logger, float sampleRate, AudioParams* params);
|
||||||
~PianoString() = default;
|
~PianoString() = default;
|
||||||
|
|
||||||
void noteOn(float frequency, float velocity) override;
|
void noteOn(float frequency, float velocity) override;
|
||||||
|
|||||||
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) {
|
KeyboardController::KeyboardController(ConfigService* config, LoggerService* logger, NoteQueue* queue) : config_(config), logger_(logger), queue_(queue) {
|
||||||
|
|
||||||
// load keymap from config service
|
// load keymap from config service
|
||||||
if(!(config->getConfig<KeymapConfig>("KeyboardController", "Main", &configuration_))) {
|
if(!(config->getConfig<KeymapParams, KeymapConfig>("KeyboardController", "Main", &configuration_))) {
|
||||||
logger_->log("Keyboard", LogFlag::Error, "Failed to get logger configuration fom config service");
|
logger_->log("Keyboard", LogFlag::Error, "Failed to get logger configuration from config service");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -7,7 +7,8 @@
|
|||||||
#include <unordered_map>
|
#include <unordered_map>
|
||||||
|
|
||||||
#include "NoteQueue.hpp"
|
#include "NoteQueue.hpp"
|
||||||
#include "ConfigService.hpp"
|
#include "config/ConfigService.hpp"
|
||||||
|
#include "config/KeymapConfig.hpp"
|
||||||
#include "LoggerService.hpp"
|
#include "LoggerService.hpp"
|
||||||
|
|
||||||
// The keyboardcontroller handles user inputs from a keyboard and maps them to note events
|
// The keyboardcontroller handles user inputs from a keyboard and maps them to note events
|
||||||
@@ -32,7 +33,7 @@ private:
|
|||||||
LoggerService* logger_;
|
LoggerService* logger_;
|
||||||
|
|
||||||
// keymap is key -> midi note id
|
// keymap is key -> midi note id
|
||||||
KeymapConfig configuration_;
|
KeymapParams configuration_;
|
||||||
|
|
||||||
static constexpr float defaultVelocity_ = 0.8f;
|
static constexpr float defaultVelocity_ = 0.8f;
|
||||||
|
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
#include <unordered_set>
|
#include <unordered_set>
|
||||||
|
|
||||||
#include "NoteQueue.hpp"
|
#include "NoteQueue.hpp"
|
||||||
#include "ConfigService.hpp"
|
#include "config/ConfigService.hpp"
|
||||||
#include "LoggerService.hpp"
|
#include "LoggerService.hpp"
|
||||||
|
|
||||||
class MidiController {
|
class MidiController {
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
#include <cstdint>
|
#include <cstdint>
|
||||||
#include <chrono>
|
#include <chrono>
|
||||||
|
|
||||||
#include "ConfigService.hpp"
|
#include "config/ConfigService.hpp"
|
||||||
#include "LoggerService.hpp"
|
#include "LoggerService.hpp"
|
||||||
|
|
||||||
enum NoteEventType {
|
enum NoteEventType {
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
#include <iostream>
|
#include <iostream>
|
||||||
|
|
||||||
ScopeBuffer::ScopeBuffer(QObject *parent) : QObject(parent) {
|
ScopeBuffer::ScopeBuffer(QObject *parent) : QObject(parent) {
|
||||||
|
// timer is set in qml
|
||||||
}
|
}
|
||||||
|
|
||||||
ScopeBuffer::ScopeBuffer(ConfigService* config, LoggerService* logger, size_t size) :
|
ScopeBuffer::ScopeBuffer(ConfigService* config, LoggerService* logger, size_t size) :
|
||||||
@@ -45,7 +45,7 @@ void Scope::paint(QPainter *painter) {
|
|||||||
|
|
||||||
if(scopeBuffer_ != nullptr) scopeBuffer_->read(buffer_);
|
if(scopeBuffer_ != nullptr) scopeBuffer_->read(buffer_);
|
||||||
|
|
||||||
// TODO: scale to max amplitude ?
|
// TODO: scale to max amplitude ? probably make it configurable if you want to make scope options
|
||||||
float maxAmp = 1.0f;
|
float maxAmp = 1.0f;
|
||||||
/*
|
/*
|
||||||
for(float s : buffer_) {
|
for(float s : buffer_) {
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
#include <vector>
|
#include <vector>
|
||||||
#include <atomic>
|
#include <atomic>
|
||||||
|
|
||||||
#include "ConfigService.hpp"
|
#include "config/ConfigService.hpp"
|
||||||
#include "LoggerService.hpp"
|
#include "LoggerService.hpp"
|
||||||
|
|
||||||
class ScopeBuffer : public QObject {
|
class ScopeBuffer : public QObject {
|
||||||
|
|||||||
@@ -2,31 +2,43 @@
|
|||||||
#include "Synth.hpp"
|
#include "Synth.hpp"
|
||||||
|
|
||||||
Synth::Synth(ConfigService* config, LoggerService* logger, ScopeBuffer* scope, NoteQueue* queue) :
|
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
|
||||||
|
|
||||||
filter_.setSampleRate(44100.0f);
|
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);
|
filter_.setParams(Filter::Type::BiquadLowpass, 4000.0f, 0.707f);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void Synth::handleNoteEvent(const NoteEvent& event) {
|
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
|
// stop all voices that are currently playing this note
|
||||||
for(Voice& v : voices_) {
|
for(Instrument* i : instruments_) {
|
||||||
if(v.isActive() && v.note() == event.note) {
|
if(i->isActive() && i->note() == event.note) {
|
||||||
v.noteOff();
|
i->noteOff();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// call note-on for an idle voice for the note-event
|
||||||
if(event.type == NoteEventType::NoteOn) {
|
if(event.type == NoteEventType::NoteOn) {
|
||||||
|
Instrument* i = findFreeVoice();
|
||||||
Voice* v = findFreeVoice();
|
if(i != nullptr) i->startNote(event.note, event.velocity);
|
||||||
if(v != nullptr) v->noteOn(event.note, event.velocity);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -38,11 +50,12 @@ void Synth::process(float* out, size_t nFrames) {
|
|||||||
handleNoteEvent(noteEvent);
|
handleNoteEvent(noteEvent);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// find lowest frequency voice for scope triggering
|
||||||
size_t lowestVoice = 0;
|
size_t lowestVoice = 0;
|
||||||
float lowestFreq = 100000.0f;
|
float lowestFreq = FLT_MAX;
|
||||||
for(size_t i = 0; i < voices_.size(); i++) {
|
for(size_t i = 0; i < instruments_.size(); i++) {
|
||||||
if(!voices_[i].isActive()) continue;
|
if(!instruments_[i]->isActive()) continue;
|
||||||
float currentFreq = voices_[i].frequency();
|
float currentFreq = instruments_[i]->frequency();
|
||||||
if(currentFreq < lowestFreq) {
|
if(currentFreq < lowestFreq) {
|
||||||
lowestVoice = i;
|
lowestVoice = i;
|
||||||
lowestFreq = currentFreq;
|
lowestFreq = currentFreq;
|
||||||
@@ -54,13 +67,16 @@ void Synth::process(float* out, size_t nFrames) {
|
|||||||
float sampleOut = 0.0f;
|
float sampleOut = 0.0f;
|
||||||
bool triggered = false;
|
bool triggered = false;
|
||||||
bool once = false;
|
bool once = false;
|
||||||
|
|
||||||
|
// generate n samples for the output buffer
|
||||||
for(size_t i = 0; i < nFrames; i++) {
|
for(size_t i = 0; i < nFrames; i++) {
|
||||||
|
|
||||||
|
// process all active voices and mix their produced samples
|
||||||
float mix = 0.0f;
|
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;
|
bool temp = false;
|
||||||
if(!voices_[j].isActive()) continue;
|
if(!instruments_[j]->isActive()) continue;
|
||||||
mix += voices_[j].process(temp);
|
mix += instruments_[j]->process(temp);
|
||||||
if(j == lowestVoice) triggered = temp;
|
if(j == lowestVoice) triggered = temp;
|
||||||
}
|
}
|
||||||
mix = tanh(mix); // prevent clipping
|
mix = tanh(mix); // prevent clipping
|
||||||
@@ -78,24 +94,23 @@ void Synth::process(float* out, size_t nFrames) {
|
|||||||
|
|
||||||
scope_->spinlock(false);
|
scope_->spinlock(false);
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Voice* Synth::findFreeVoice() {
|
Instrument* Synth::findFreeVoice() {
|
||||||
|
|
||||||
for(Voice& v : voices_) {
|
for(Instrument* i : instruments_) {
|
||||||
if(!v.isActive()) {
|
if(!i->isActive()) {
|
||||||
return &v;
|
return i;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return nullptr;
|
return nullptr;
|
||||||
}
|
}
|
||||||
|
|
||||||
Voice* Synth::findVoiceByNote(uint8_t note) {
|
Instrument* Synth::findVoiceByNote(uint8_t note) {
|
||||||
|
|
||||||
for(Voice& v : voices_) {
|
for(Instrument* i : instruments_) {
|
||||||
if(v.isActive() && v.note() == note) {
|
if(i->isActive() && i->note() == note) {
|
||||||
return &v;
|
return i;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return nullptr;
|
return nullptr;
|
||||||
|
|||||||
@@ -1,12 +1,14 @@
|
|||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include "ConfigService.hpp"
|
#include "config/ConfigService.hpp"
|
||||||
|
#include "config/AudioConfig.hpp"
|
||||||
#include "LoggerService.hpp"
|
#include "LoggerService.hpp"
|
||||||
#include "NoteQueue.hpp"
|
#include "NoteQueue.hpp"
|
||||||
#include "Voice.hpp"
|
|
||||||
#include "Scope.hpp"
|
#include "Scope.hpp"
|
||||||
#include "Filter.hpp"
|
#include "Filter.hpp"
|
||||||
|
#include "Instruments/Instrument.hpp"
|
||||||
|
#include "InstrumentFactory.hpp"
|
||||||
|
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
@@ -22,19 +24,21 @@ public:
|
|||||||
|
|
||||||
private:
|
private:
|
||||||
|
|
||||||
Voice* findFreeVoice();
|
Instrument* findFreeVoice();
|
||||||
Voice* findVoiceByNote(uint8_t note);
|
Instrument* findVoiceByNote(uint8_t note);
|
||||||
|
|
||||||
std::vector<uint8_t> sustainedNotes_;
|
std::vector<uint8_t> sustainedNotes_;
|
||||||
Filter filter_;
|
Filter filter_;
|
||||||
|
|
||||||
// voices
|
// voices
|
||||||
static constexpr size_t MAX_VOICES = 32;
|
size_t maxVoices;
|
||||||
std::array<Voice, MAX_VOICES> voices_;
|
std::vector<Instrument*> instruments_;
|
||||||
|
|
||||||
ConfigService* config_;
|
ConfigService* config_;
|
||||||
|
AudioParams engineConfiguration_;
|
||||||
LoggerService* logger_;
|
LoggerService* logger_;
|
||||||
ScopeBuffer* scope_;;
|
ScopeBuffer* scope_;
|
||||||
NoteQueue* noteQueue_;
|
NoteQueue* noteQueue_;
|
||||||
|
InstrumentFactory instrumentFactory_;
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,35 +0,0 @@
|
|||||||
|
|
||||||
#include "Voice.hpp"
|
|
||||||
|
|
||||||
Voice::Voice(ConfigService* config, LoggerService* logger) :
|
|
||||||
config_(config), logger_(logger) {
|
|
||||||
|
|
||||||
// TODO: instrument factory
|
|
||||||
instrument_ = PianoString(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,43 +0,0 @@
|
|||||||
|
|
||||||
#pragma once
|
|
||||||
|
|
||||||
#include <stdint.h>
|
|
||||||
|
|
||||||
#include "Instrument.hpp"
|
|
||||||
#include "Instruments/PianoString.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_; }
|
|
||||||
float frequency() { return noteToFrequency(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_;
|
|
||||||
PianoString 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;
|
||||||
|
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user