diff --git a/CMakeLists.txt b/CMakeLists.txt index 426608f..f34057f 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -55,10 +55,11 @@ add_library(sonobulus_core STATIC src/synth/NoteQueue.cpp src/synth/Scope.cpp src/synth/Synth.cpp - src/synth/Voice.cpp + #src/synth/Voice.cpp src/synth/Filter.cpp src/synth/WavetableLoader.cpp src/synth/Envelope.cpp + src/synth/InstrumentFactory.cpp src/synth/instruments/Instrument.cpp src/synth/instruments/PianoString.cpp src/synth/instruments/WavetableOscillator.cpp diff --git a/config/sonobulus.cfg b/config/sonobulus.cfg index ea523a9..0c73cef 100644 --- a/config/sonobulus.cfg +++ b/config/sonobulus.cfg @@ -8,7 +8,7 @@ Logger = ( "Info", "Warning", "Error" - ); + ); # TODO: console flags vs file flags ShowTime = 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 = ( { Id = "Main"; diff --git a/scripts/.gitkeep b/scripts/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/src/main.cpp b/src/main.cpp index df70038..752418d 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -50,4 +50,3 @@ int main(int argc, char* argv[]) { // execute app return app.exec(); } - \ No newline at end of file diff --git a/src/synth/InstrumentFactory.cpp b/src/synth/InstrumentFactory.cpp new file mode 100644 index 0000000..db97b14 --- /dev/null +++ b/src/synth/InstrumentFactory.cpp @@ -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* 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(params->sampleRate), params)); + } + + return params->maxVoices; + +} \ No newline at end of file diff --git a/src/synth/InstrumentFactory.hpp b/src/synth/InstrumentFactory.hpp new file mode 100644 index 0000000..f94ffd8 --- /dev/null +++ b/src/synth/InstrumentFactory.hpp @@ -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* 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; + +}; diff --git a/src/synth/Instruments/Instrument.cpp b/src/synth/Instruments/Instrument.cpp index c5462f4..d0f0b85 100644 --- a/src/synth/Instruments/Instrument.cpp +++ b/src/synth/Instruments/Instrument.cpp @@ -3,8 +3,16 @@ #include "string" -Instrument::Instrument(ConfigService* config, LoggerService* logger, float sampleRate) : - config_(config), logger_(logger), sampleRate_(sampleRate) { +Instrument::Instrument(ConfigService* config, LoggerService* logger, float sampleRate, AudioParams* params) : + config_(config), logger_(logger), sampleRate_(sampleRate), params_(params) { + +} + +void Instrument::startNote(uint8_t midiNote, float velocity) { + + note_ = midiNote; + + noteOn(noteToFrequency(midiNote), velocity); } diff --git a/src/synth/Instruments/Instrument.hpp b/src/synth/Instruments/Instrument.hpp index 5b509cb..14b97b2 100644 --- a/src/synth/Instruments/Instrument.hpp +++ b/src/synth/Instruments/Instrument.hpp @@ -3,6 +3,7 @@ #include "config/ConfigService.hpp" #include "LoggerService.hpp" +#include "config/AudioConfig.hpp" // an instrument is our state space model. it calculates discrete samples as a function of time, the current states, and the inputs // an instrument is owned by a voice @@ -11,9 +12,10 @@ class Instrument { public: Instrument() = default; - Instrument(ConfigService* config, LoggerService* logger, float sampleRate); + Instrument(ConfigService* config, LoggerService* logger, float sampleRate, AudioParams* params); ~Instrument() = default; + void startNote(uint8_t midiNote, float velocity); virtual void noteOn(float frequency, float velocity); virtual void noteOff(); @@ -21,13 +23,23 @@ public: 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(note - params_->midiHome) / params_->notesPerOctave); + } float sampleRate_ = 1.0f; bool active_ = false; ConfigService* config_; LoggerService* logger_; + AudioParams* params_; + + uint8_t note_; static constexpr float pi = 3.14159265358979323846f; float phase_ = 0.0f; diff --git a/src/synth/Instruments/PianoString.cpp b/src/synth/Instruments/PianoString.cpp index b5fd7b5..5116a12 100644 --- a/src/synth/Instruments/PianoString.cpp +++ b/src/synth/Instruments/PianoString.cpp @@ -1,7 +1,7 @@ #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_previous_.resize(segmentCount_ + 1); diff --git a/src/synth/Instruments/PianoString.hpp b/src/synth/Instruments/PianoString.hpp index 2ea3ecc..8b601ef 100644 --- a/src/synth/Instruments/PianoString.hpp +++ b/src/synth/Instruments/PianoString.hpp @@ -10,7 +10,7 @@ class PianoString : public Instrument { public: PianoString() = default; - PianoString(ConfigService* config, LoggerService* logger, float sampleRate); + PianoString(ConfigService* config, LoggerService* logger, float sampleRate, AudioParams* params); ~PianoString() = default; void noteOn(float frequency, float velocity) override; diff --git a/src/synth/Instruments/WavetableOscillator.cpp b/src/synth/Instruments/WavetableOscillator.cpp index 32dd39e..e3f5637 100644 --- a/src/synth/Instruments/WavetableOscillator.cpp +++ b/src/synth/Instruments/WavetableOscillator.cpp @@ -2,12 +2,17 @@ #include "WavetableOscillator.hpp" -WavetableOscillator::WavetableOscillator(ConfigService* config, LoggerService* logger, float sampleRate) { +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() { @@ -19,12 +24,18 @@ bool WavetableOscillator::isActive() { } float WavetableOscillator::process(bool& scopeTrigger) { - // return wavetable_.process() * envelope_.process(); - // more like - return 0.0f; -} -bool WavetableOscillator::loadWavetable(std::string filename) { - return false; -} + 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; +} diff --git a/src/synth/Instruments/WavetableOscillator.hpp b/src/synth/Instruments/WavetableOscillator.hpp index 19838bc..1dcd3de 100644 --- a/src/synth/Instruments/WavetableOscillator.hpp +++ b/src/synth/Instruments/WavetableOscillator.hpp @@ -5,13 +5,14 @@ #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, float sampleRate); + WavetableOscillator(ConfigService* config, LoggerService* logger, WavetableLoader* wavetabler, float sampleRate, AudioParams* params); ~WavetableOscillator() = default; void noteOn(float frequency, float velocity) override; @@ -21,12 +22,19 @@ public: float process(bool& scopeTrigger) override; + bool setWavetableId(size_t id) { wavetableId_ = id; } + private: - bool loadWavetable(std::string filename); // TODO: load from conf file instead - - std::vector wavetable_; + 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; + }; diff --git a/src/synth/Synth.cpp b/src/synth/Synth.cpp index 7a50325..ca55369 100644 --- a/src/synth/Synth.cpp +++ b/src/synth/Synth.cpp @@ -2,7 +2,7 @@ #include "Synth.hpp" Synth::Synth(ConfigService* config, LoggerService* logger, ScopeBuffer* scope, NoteQueue* queue) : - config_(config), logger_(logger), scope_(scope), noteQueue_(queue) { + config_(config), logger_(logger), scope_(scope), noteQueue_(queue), instrumentFactory_(config_, logger_) { // load audio settings from config service if(!(config->getConfig("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 // 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(engineConfiguration_.sampleRate), &engineConfiguration_)); + int32_t instrumentsCreated = instrumentFactory_.createInstruments(&instruments_, engineConfiguration_.maxVoices, static_cast(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); @@ -22,16 +29,16 @@ Synth::Synth(ConfigService* config, LoggerService* logger, ScopeBuffer* scope, N void Synth::handleNoteEvent(const NoteEvent& event) { // stop all voices that are currently playing this note - for(Voice& v : voices_) { - if(v.isActive() && v.note() == event.note) { - v.noteOff(); + for(Instrument* i : instruments_) { + if(i->isActive() && i->note() == event.note) { + i->noteOff(); } } // call note-on for an idle voice for the note-event if(event.type == NoteEventType::NoteOn) { - Voice* v = findFreeVoice(); - if(v != nullptr) v->noteOn(event.note, event.velocity); + Instrument* i = findFreeVoice(); + 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 size_t lowestVoice = 0; float lowestFreq = FLT_MAX; - for(size_t i = 0; i < voices_.size(); i++) { - if(!voices_[i].isActive()) continue; - float currentFreq = voices_[i].frequency(); + for(size_t i = 0; i < instruments_.size(); i++) { + if(!instruments_[i]->isActive()) continue; + float currentFreq = instruments_[i]->frequency(); if(currentFreq < lowestFreq) { lowestVoice = i; lowestFreq = currentFreq; @@ -66,10 +73,10 @@ void Synth::process(float* out, size_t nFrames) { // process all active voices and mix their produced samples float mix = 0.0f; - for(size_t j = 0; j < voices_.size(); j++) { + for(size_t j = 0; j < instruments_.size(); j++) { bool temp = false; - if(!voices_[j].isActive()) continue; - mix += voices_[j].process(temp); + if(!instruments_[j]->isActive()) continue; + mix += instruments_[j]->process(temp); if(j == lowestVoice) triggered = temp; } 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_) { - if(!v.isActive()) { - return &v; + for(Instrument* i : instruments_) { + if(!i->isActive()) { + return i; } } return nullptr; } -Voice* Synth::findVoiceByNote(uint8_t note) { +Instrument* Synth::findVoiceByNote(uint8_t note) { - for(Voice& v : voices_) { - if(v.isActive() && v.note() == note) { - return &v; + for(Instrument* i : instruments_) { + if(i->isActive() && i->note() == note) { + return i; } } return nullptr; diff --git a/src/synth/Synth.hpp b/src/synth/Synth.hpp index 02b0493..7c3edba 100644 --- a/src/synth/Synth.hpp +++ b/src/synth/Synth.hpp @@ -5,9 +5,10 @@ #include "config/AudioConfig.hpp" #include "LoggerService.hpp" #include "NoteQueue.hpp" -#include "Voice.hpp" #include "Scope.hpp" #include "Filter.hpp" +#include "Instruments/Instrument.hpp" +#include "InstrumentFactory.hpp" #include @@ -23,20 +24,21 @@ public: private: - Voice* findFreeVoice(); - Voice* findVoiceByNote(uint8_t note); + Instrument* findFreeVoice(); + Instrument* findVoiceByNote(uint8_t note); std::vector sustainedNotes_; Filter filter_; // voices size_t maxVoices; - std::vector voices_; + std::vector instruments_; ConfigService* config_; AudioParams engineConfiguration_; LoggerService* logger_; - ScopeBuffer* scope_;; + ScopeBuffer* scope_; NoteQueue* noteQueue_; + InstrumentFactory instrumentFactory_; }; diff --git a/src/synth/Voice.cpp b/src/synth/Voice.cpp deleted file mode 100644 index b81e78a..0000000 --- a/src/synth/Voice.cpp +++ /dev/null @@ -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); - -} diff --git a/src/synth/Voice.hpp b/src/synth/Voice.hpp deleted file mode 100644 index 7e8d476..0000000 --- a/src/synth/Voice.hpp +++ /dev/null @@ -1,44 +0,0 @@ - -#pragma once - -#include - -#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(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_; - -}; diff --git a/src/synth/WavetableLoader.hpp b/src/synth/WavetableLoader.hpp index 9ecf718..808232f 100644 --- a/src/synth/WavetableLoader.hpp +++ b/src/synth/WavetableLoader.hpp @@ -1,6 +1,8 @@ #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