Compare commits

...

10 Commits

46 changed files with 752 additions and 211 deletions

4
.gitignore vendored
View File

@@ -1,2 +1,4 @@
build/*
build/*
*/__pycache__/*

View File

@@ -46,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
@@ -57,8 +57,11 @@ add_library(sonobulus_core STATIC
src/synth/Synth.cpp
src/synth/Voice.cpp
src/synth/Filter.cpp
src/synth/Instrument.cpp
src/synth/Instruments/PianoString.cpp
src/synth/WavetableLoader.cpp
src/synth/Envelope.cpp
src/synth/instruments/Instrument.cpp
src/synth/instruments/PianoString.cpp
src/synth/instruments/WavetableOscillator.cpp
)
if (WIN32)

View File

@@ -19,6 +19,22 @@ 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;
}
);
KeyboardController = (
{
Id = "Main";

BIN
config/wavetables/saw.wt Normal file

Binary file not shown.

Binary file not shown.

Binary file not shown.

BIN
config/wavetables/sine.wt Normal file

Binary file not shown.

BIN
config/wavetables/square.wt Normal file

Binary file not shown.

Binary file not shown.

View 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()

View 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")

View File

@@ -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_;
};

View File

@@ -13,16 +13,10 @@ 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;
@@ -55,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.");
}
@@ -78,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 = "";
@@ -94,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;

View File

@@ -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
View 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
View 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_;
};

View 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;
}
};

View 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
View 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_;
};

View 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;
}
};

View 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;
}
};

View File

@@ -7,7 +7,7 @@
#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"
@@ -50,3 +50,4 @@ int main(int argc, char* argv[]) {
// execute app
return app.exec();
}

View File

@@ -8,6 +8,12 @@
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) {
logger_->log("Audio", LogFlag::Error, "No audio devices found.");
}
@@ -22,17 +28,26 @@ 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(&params, nullptr, RTAUDIO_FLOAT32, sampleRate_, &bufferFrames_, &AudioEngine::audioCallback, this, &options);
RtAudioErrorType status = audioDevice_.openStream(
&params,
nullptr,
RTAUDIO_FLOAT32,
configuration_.sampleRate,
&configuration_.bufferSize,
&AudioEngine::audioCallback,
this,
&options
);
if(status != RTAUDIO_NO_ERROR) {
logger_->log("Audio", LogFlag::Error, "Error opening RtAudio stream.");
return false;
}
}
status = audioDevice_.startStream();
if(status != RTAUDIO_NO_ERROR) {
@@ -41,7 +56,7 @@ bool AudioEngine::start() {
}
// 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;

View File

@@ -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
View 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
View 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;
};

View File

@@ -3,8 +3,8 @@
#include "string"
Instrument::Instrument(ConfigService* config, LoggerService* logger) :
config_(config), logger_(logger) {
Instrument::Instrument(ConfigService* config, LoggerService* logger, float sampleRate) :
config_(config), logger_(logger), sampleRate_(sampleRate) {
}

View File

@@ -1,17 +1,17 @@
#pragma once
#include "ConfigService.hpp"
#include "config/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
// an instrument is owned by a voice
class Instrument {
public:
Instrument() = default;
Instrument(ConfigService* config, LoggerService* logger);
Instrument(ConfigService* config, LoggerService* logger, float sampleRate);
~Instrument() = default;
virtual void noteOn(float frequency, float velocity);
@@ -23,7 +23,7 @@ public:
protected:
float sampleRate_ = 44100.0f;
float sampleRate_ = 1.0f;
bool active_ = false;
ConfigService* config_;

View File

@@ -1,7 +1,7 @@
#include "PianoString.hpp"
PianoString::PianoString(ConfigService* config, LoggerService* logger) : Instrument(config, logger) {
PianoString::PianoString(ConfigService* config, LoggerService* logger, float sampleRate) : Instrument(config, logger, sampleRate) {
stringY_current_.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) {
active_ = true;
frequency_ = frequency;
frequency_ = frequency / 1.03746174945f; // empirical adjustment (original was 83 cents sharp)
damping_ = 0.5f;
rms_ = 5.0f;
@@ -84,9 +84,9 @@ float PianoString::process(bool& scopeTrigger) {
}
phaseTracker_ += frequency_ / sampleRate_;
if(phaseTracker_ > 1.0f) {
phaseTracker_ -= 1.0f;
scopeTrigger = true;
if(phaseTracker_ > 2.0f) {
phaseTracker_ -= 2.0f;
if(!scopeTrigger) scopeTrigger = true;
}
float sampleOut = stringY_next_[static_cast<size_t>(samplePosition_*segmentCount_)];
@@ -103,6 +103,7 @@ void PianoString::recalculateConstants() {
waveVelocity_ = 2.0f * frequency_ * L_;
// calculate number of super-sampling steps needed to maintain string stability
samplingSteps_ = 0;
do {
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;
}
} while (segmentCount_ < 60);
} 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_);

View File

@@ -3,14 +3,14 @@
#include <cmath>
#include "synth/Instrument.hpp"
#include "synth/instruments/Instrument.hpp"
class PianoString : public Instrument {
public:
PianoString() = default;
PianoString(ConfigService* config, LoggerService* logger);
PianoString(ConfigService* config, LoggerService* logger, float sampleRate);
~PianoString() = default;
void noteOn(float frequency, float velocity) override;

View File

@@ -0,0 +1,30 @@
#include "WavetableOscillator.hpp"
WavetableOscillator::WavetableOscillator(ConfigService* config, LoggerService* logger, float sampleRate) {
}
void WavetableOscillator::noteOn(float frequency, float velocity) {
envelope_.noteOn(); // hmm we can scale this by velocity
}
void WavetableOscillator::noteOff() {
envelope_.noteOff();
}
bool WavetableOscillator::isActive() {
return envelope_.isActive();
}
float WavetableOscillator::process(bool& scopeTrigger) {
// return wavetable_.process() * envelope_.process();
// more like
return 0.0f;
}
bool WavetableOscillator::loadWavetable(std::string filename) {
return false;
}

View File

@@ -0,0 +1,32 @@
#pragma once
#include <cmath>
#include "synth/instruments/Instrument.hpp"
#include "synth/Envelope.hpp"
class WavetableOscillator : public Instrument {
public:
WavetableOscillator() = default;
WavetableOscillator(ConfigService* config, LoggerService* logger, float sampleRate);
~WavetableOscillator() = default;
void noteOn(float frequency, float velocity) override;
void noteOff() override;
bool isActive() override;
float process(bool& scopeTrigger) override;
private:
bool loadWavetable(std::string filename); // TODO: load from conf file instead
std::vector<float> wavetable_;
Envelope envelope_;
};

View File

@@ -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;
}

View File

@@ -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;

View File

@@ -7,7 +7,7 @@
#include <unordered_set>
#include "NoteQueue.hpp"
#include "ConfigService.hpp"
#include "config/ConfigService.hpp"
#include "LoggerService.hpp"
class MidiController {

View File

@@ -7,7 +7,7 @@
#include <cstdint>
#include <chrono>
#include "ConfigService.hpp"
#include "config/ConfigService.hpp"
#include "LoggerService.hpp"
enum NoteEventType {

View File

@@ -4,7 +4,7 @@
#include <iostream>
ScopeBuffer::ScopeBuffer(QObject *parent) : QObject(parent) {
// timer is set in qml
}
ScopeBuffer::ScopeBuffer(ConfigService* config, LoggerService* logger, size_t size) :
@@ -45,7 +45,7 @@ void Scope::paint(QPainter *painter) {
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;
/*
for(float s : buffer_) {

View File

@@ -8,7 +8,7 @@
#include <vector>
#include <atomic>
#include "ConfigService.hpp"
#include "config/ConfigService.hpp"
#include "LoggerService.hpp"
class ScopeBuffer : public QObject {

View File

@@ -4,18 +4,23 @@
Synth::Synth(ConfigService* config, LoggerService* logger, ScopeBuffer* scope, NoteQueue* queue) :
config_(config), logger_(logger), scope_(scope), noteQueue_(queue) {
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
voices_.assign(engineConfiguration_.maxVoices, Voice(config_, logger_, static_cast<float>(engineConfiguration_.sampleRate), &engineConfiguration_));
filter_.setSampleRate(44100.0f);
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(Voice& v : voices_) {
if(v.isActive() && v.note() == event.note) {
@@ -23,8 +28,8 @@ void Synth::handleNoteEvent(const NoteEvent& event) {
}
}
// call note-on for an idle voice for the note-event
if(event.type == NoteEventType::NoteOn) {
Voice* v = findFreeVoice();
if(v != nullptr) v->noteOn(event.note, event.velocity);
}
@@ -38,8 +43,9 @@ void Synth::process(float* out, size_t nFrames) {
handleNoteEvent(noteEvent);
}
// find lowest frequency voice for scope triggering
size_t lowestVoice = 0;
float lowestFreq = 100000.0f;
float lowestFreq = FLT_MAX;
for(size_t i = 0; i < voices_.size(); i++) {
if(!voices_[i].isActive()) continue;
float currentFreq = voices_[i].frequency();
@@ -54,8 +60,11 @@ void Synth::process(float* out, size_t nFrames) {
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++) {
bool temp = false;
@@ -78,7 +87,6 @@ void Synth::process(float* out, size_t nFrames) {
scope_->spinlock(false);
}
Voice* Synth::findFreeVoice() {

View File

@@ -1,7 +1,8 @@
#pragma once
#include "ConfigService.hpp"
#include "config/ConfigService.hpp"
#include "config/AudioConfig.hpp"
#include "LoggerService.hpp"
#include "NoteQueue.hpp"
#include "Voice.hpp"
@@ -29,10 +30,11 @@ private:
Filter filter_;
// voices
static constexpr size_t MAX_VOICES = 32;
std::array<Voice, MAX_VOICES> voices_;
size_t maxVoices;
std::vector<Voice> voices_;
ConfigService* config_;
AudioParams engineConfiguration_;
LoggerService* logger_;
ScopeBuffer* scope_;;
NoteQueue* noteQueue_;

View File

@@ -1,11 +1,11 @@
#include "Voice.hpp"
Voice::Voice(ConfigService* config, LoggerService* logger) :
config_(config), logger_(logger) {
Voice::Voice(ConfigService* config, LoggerService* logger, float sampleRate, AudioParams* params) :
config_(config), logger_(logger), sampleRate_(sampleRate), params_(params) {
// TODO: instrument factory
instrument_ = PianoString(config_, logger_);
// TODO: instrument factory << WHAT DO YOU MEAN JUST TODO A WHOLE FACTORY
instrument_ = PianoString(config_, logger_, sampleRate_);
}

View File

@@ -3,8 +3,9 @@
#include <stdint.h>
#include "Instrument.hpp"
#include "Instruments/PianoString.hpp"
#include "instruments/Instrument.hpp"
#include "instruments/PianoString.hpp"
#include "config/AudioConfig.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
@@ -13,7 +14,7 @@ class Voice {
public:
Voice() = default;
Voice(ConfigService* config, LoggerService* logger);
Voice(ConfigService* config, LoggerService* logger, float sampleRate, AudioParams* params);
~Voice() = default;
void noteOn(uint8_t midiNote, float velocity);
@@ -26,17 +27,17 @@ public:
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));
return params_->pitchStandard * pow(params_->octaveConstant, static_cast<float>(note - params_->midiHome) / params_->notesPerOctave);
}
float sampleRate_ = 1.0f;
uint8_t note_ = 0;
float velocity_ = 1.0f;
bool active_ = false;
ConfigService* config_;
AudioParams* params_;
LoggerService* logger_;
PianoString instrument_;

View 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];
}

View File

@@ -0,0 +1,40 @@
#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;
};

View File