Compare commits
7 Commits
295a869839
...
feature/wa
| Author | SHA1 | Date | |
|---|---|---|---|
| 5d86018d06 | |||
| 54996469d1 | |||
| 2436946a3c | |||
| b00e7a053c | |||
| fdb55bb78d | |||
| 8de4081a50 | |||
| eb84c93b7b |
2
.gitignore
vendored
2
.gitignore
vendored
@@ -1,2 +1,4 @@
|
||||
|
||||
build/*
|
||||
|
||||
*/__pycache__/*
|
||||
@@ -55,10 +55,14 @@ 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
|
||||
)
|
||||
|
||||
if (WIN32)
|
||||
|
||||
@@ -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";
|
||||
|
||||
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")
|
||||
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"
|
||||
|
||||
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);
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
uint8_t note() { return note_; }
|
||||
float frequency() { return noteToFrequency(note_); }
|
||||
|
||||
protected:
|
||||
|
||||
inline float noteToFrequency(uint8_t note) {
|
||||
return params_->pitchStandard * pow(params_->octaveConstant, static_cast<float>(note - params_->midiHome) / params_->notesPerOctave);
|
||||
}
|
||||
|
||||
float sampleRate_ = 1.0f;
|
||||
bool active_ = false;
|
||||
|
||||
ConfigService* config_;
|
||||
LoggerService* logger_;
|
||||
AudioParams* params_;
|
||||
|
||||
uint8_t note_;
|
||||
|
||||
static constexpr float pi = 3.14159265358979323846f;
|
||||
float phase_ = 0.0f;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
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;
|
||||
|
||||
};
|
||||
@@ -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<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
|
||||
// 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_.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;
|
||||
|
||||
@@ -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 <vector>
|
||||
|
||||
@@ -23,20 +24,21 @@ public:
|
||||
|
||||
private:
|
||||
|
||||
Voice* findFreeVoice();
|
||||
Voice* findVoiceByNote(uint8_t note);
|
||||
Instrument* findFreeVoice();
|
||||
Instrument* findVoiceByNote(uint8_t note);
|
||||
|
||||
std::vector<uint8_t> sustainedNotes_;
|
||||
Filter filter_;
|
||||
|
||||
// voices
|
||||
size_t maxVoices;
|
||||
std::vector<Voice> voices_;
|
||||
std::vector<Instrument*> instruments_;
|
||||
|
||||
ConfigService* config_;
|
||||
AudioParams engineConfiguration_;
|
||||
LoggerService* logger_;
|
||||
ScopeBuffer* scope_;;
|
||||
ScopeBuffer* scope_;
|
||||
NoteQueue* noteQueue_;
|
||||
InstrumentFactory instrumentFactory_;
|
||||
|
||||
};
|
||||
|
||||
@@ -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);
|
||||
|
||||
}
|
||||
@@ -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_;
|
||||
|
||||
};
|
||||
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;
|
||||
|
||||
};
|
||||
@@ -1,2 +0,0 @@
|
||||
|
||||
haiii :3
|
||||
Reference in New Issue
Block a user