Compare commits

..

5 Commits

18 changed files with 364 additions and 3 deletions

4
.gitignore vendored
View File

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

View File

@@ -57,8 +57,11 @@ add_library(sonobulus_core STATIC
src/synth/Synth.cpp
src/synth/Voice.cpp
src/synth/Filter.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)

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

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,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

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

View File

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