Compare commits

..

5 Commits

21 changed files with 372 additions and 119 deletions

View File

@@ -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/WavetableLoader.cpp
src/synth/Envelope.cpp
src/synth/InstrumentFactory.cpp
src/synth/instruments/Instrument.cpp src/synth/instruments/Instrument.cpp
src/synth/instruments/PianoString.cpp src/synth/instruments/PianoString.cpp
src/synth/instruments/WavetableOscillator.cpp
) )
if (WIN32) if (WIN32)

View File

@@ -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;
@@ -35,6 +35,7 @@ AudioEngine = (
} }
); );
# 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";

View File

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

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

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

View File

@@ -3,8 +3,16 @@
#include "string" #include "string"
Instrument::Instrument(ConfigService* config, LoggerService* logger, float sampleRate) : Instrument::Instrument(ConfigService* config, LoggerService* logger, float sampleRate, AudioParams* params) :
config_(config), logger_(logger), sampleRate_(sampleRate) { config_(config), logger_(logger), sampleRate_(sampleRate), params_(params) {
}
void Instrument::startNote(uint8_t midiNote, float velocity) {
note_ = midiNote;
noteOn(noteToFrequency(midiNote), velocity);
} }

View File

@@ -3,6 +3,7 @@
#include "config/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 instrument is owned by a voice // an instrument is owned by a voice
@@ -11,9 +12,10 @@ class Instrument {
public: public:
Instrument() = default; Instrument() = default;
Instrument(ConfigService* config, LoggerService* logger, float sampleRate); 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);
protected: uint8_t note() { return note_; }
float frequency() { return noteToFrequency(note_); }
protected:
inline float noteToFrequency(uint8_t note) {
return params_->pitchStandard * pow(params_->octaveConstant, static_cast<float>(note - params_->midiHome) / params_->notesPerOctave);
}
float sampleRate_ = 1.0f; 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;

View File

@@ -1,7 +1,7 @@
#include "PianoString.hpp" #include "PianoString.hpp"
PianoString::PianoString(ConfigService* config, LoggerService* logger, float sampleRate) : Instrument(config, logger, sampleRate) { 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);

View File

@@ -10,7 +10,7 @@ class PianoString : public Instrument {
public: public:
PianoString() = default; PianoString() = default;
PianoString(ConfigService* config, LoggerService* logger, float sampleRate); 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;

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

View File

@@ -4,13 +4,15 @@
#include <cmath> #include <cmath>
#include "synth/instruments/Instrument.hpp" #include "synth/instruments/Instrument.hpp"
#include "synth/Envelope.hpp"
#include "synth/WavetableLoader.hpp"
class PianoString : public Instrument { class WavetableOscillator : public Instrument {
public: public:
WavetableOscillator() = default; WavetableOscillator() = default;
WavetableOscillator(ConfigService* config, LoggerService* logger, float sampleRate); WavetableOscillator(ConfigService* config, LoggerService* logger, WavetableLoader* wavetabler, float sampleRate, AudioParams* params);
~WavetableOscillator() = default; ~WavetableOscillator() = default;
void noteOn(float frequency, float velocity) override; void noteOn(float frequency, float velocity) override;
@@ -20,10 +22,19 @@ public:
float process(bool& scopeTrigger) override; float process(bool& scopeTrigger) override;
bool setWavetableId(size_t id) { wavetableId_ = id; }
private: private:
bool loadWavetable(std::string filename); // TODO: load from conf file instead WavetableLoader* wavetableLoader_;
size_t wavetableId_ = 0;
std::vector<float> wavetable; Envelope envelope_;
// float phase_ = 0.0f; inherited from parent class
float frequency_ = 0.0f;
float phaseIncrement = 0.0f;
static constexpr float kPi_ = 3.14159265358979323846;
}; };

View File

@@ -2,7 +2,7 @@
#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_) {
// load audio settings from config service // load audio settings from config service
if(!(config->getConfig<AudioParams, AudioConfig>("AudioEngine", "Main", &engineConfiguration_))) { if(!(config->getConfig<AudioParams, AudioConfig>("AudioEngine", "Main", &engineConfiguration_))) {
@@ -12,7 +12,14 @@ Synth::Synth(ConfigService* config, LoggerService* logger, ScopeBuffer* scope, N
// note: this is the audio engine's configuration so we can grab sample rate // 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 // 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_)); 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_.setSampleRate(engineConfiguration_.sampleRate);
filter_.setParams(Filter::Type::BiquadLowpass, 4000.0f, 0.707f); filter_.setParams(Filter::Type::BiquadLowpass, 4000.0f, 0.707f);
@@ -22,16 +29,16 @@ Synth::Synth(ConfigService* config, LoggerService* logger, ScopeBuffer* scope, N
void Synth::handleNoteEvent(const NoteEvent& event) { void Synth::handleNoteEvent(const NoteEvent& event) {
// 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 // call note-on for an idle voice for the note-event
if(event.type == NoteEventType::NoteOn) { if(event.type == NoteEventType::NoteOn) {
Voice* v = findFreeVoice(); Instrument* i = findFreeVoice();
if(v != nullptr) v->noteOn(event.note, event.velocity); if(i != nullptr) i->startNote(event.note, event.velocity);
} }
} }
@@ -46,9 +53,9 @@ void Synth::process(float* out, size_t nFrames) {
// find lowest frequency voice for scope triggering // find lowest frequency voice for scope triggering
size_t lowestVoice = 0; size_t lowestVoice = 0;
float lowestFreq = FLT_MAX; 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;
@@ -66,10 +73,10 @@ void Synth::process(float* out, size_t nFrames) {
// process all active voices and mix their produced samples // 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
@@ -89,21 +96,21 @@ void Synth::process(float* out, size_t nFrames) {
} }
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;

View File

@@ -5,9 +5,10 @@
#include "config/AudioConfig.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>
@@ -23,20 +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
size_t maxVoices; size_t maxVoices;
std::vector<Voice> voices_; std::vector<Instrument*> instruments_;
ConfigService* config_; ConfigService* config_;
AudioParams engineConfiguration_; AudioParams engineConfiguration_;
LoggerService* logger_; LoggerService* logger_;
ScopeBuffer* scope_;; ScopeBuffer* scope_;
NoteQueue* noteQueue_; NoteQueue* noteQueue_;
InstrumentFactory instrumentFactory_;
}; };

View File

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

View File

@@ -1,44 +0,0 @@
#pragma once
#include <stdint.h>
#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
class Voice {
public:
Voice() = default;
Voice(ConfigService* config, LoggerService* logger, float sampleRate, AudioParams* params);
~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:
inline float noteToFrequency(uint8_t note) {
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

@@ -1,4 +1,42 @@
#pragma once
#pragma once
// placeholder to handle loading wavetables from files // placeholder to handle loading wavetables from files
// so that wavetables can be shared to each voice/instrument // so that wavetables can be shared to each voice/instrument
// only need to need to load once for x number of voices // 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

View File

@@ -1,2 +0,0 @@
haiii :3