10 Commits

Author SHA1 Message Date
3a07cb6319 organize voice profile + tweaks 2026-02-08 16:08:29 -06:00
0f17ab09c4 consolidate linux install script 2026-02-08 14:30:03 -06:00
e2f5cb13e0 refactor parameterstore 2026-02-08 14:11:16 -06:00
a0ac5848e3 add some default wavetables 2026-02-07 19:52:59 -06:00
18ff8dfc9f configurable wavetable files 2026-02-07 18:08:38 -06:00
8b4a09fc39 checkpoint 2026-02-07 16:54:18 -06:00
3f335343e3 wavetable loading checkpoint 2026-02-07 15:13:47 -06:00
537f571f6a preliminary wavetable generation 2026-02-07 12:56:21 -06:00
ee353eadfd remove param_defaults 2026-02-03 19:30:33 -06:00
b6a1fb9b3a fix windows build 2026-02-01 20:18:57 -06:00
27 changed files with 343 additions and 215 deletions

3
.gitignore vendored
View File

@@ -1,2 +1,3 @@
build/*
.vscode/*
.vscode/*
scripts/__pycache__/*

View File

@@ -13,7 +13,7 @@ if (WIN32) # windows 11 x86_64
set_target_properties(RtAudio::rtaudio PROPERTIES
IMPORTED_LOCATION "${RtAudio_ROOT}/bin/rtaudio.dll"
IMPORTED_IMPLIB "${RtAudio_ROOT}/lib/rtaudio.lib"
INTERFACE_INCLUDE_DIRECTORIES "${RtAudio_ROOT}/include"
INTERFACE_INCLUDE_DIRECTORIES "${RtAudio_ROOT}/include/rtaudio"
)
add_library(RtMidi::rtmidi SHARED IMPORTED)
@@ -108,3 +108,13 @@ target_link_libraries(metabolus
yaml-cpp
Qt6::Widgets
)
# needed some different calls here
if (WIN32)
else()
target_link_libraries(metabolus
PRIVATE
atomic
)
endif()

View File

@@ -21,7 +21,7 @@ This synthesizer isn't very good, but it's neat :3
oscillators increase the sound complexity considerably
- [x] Create a UI scope to visualize the synthesized composite waveform
- [x] Create wavetables for more complex tone generation. Needs to be selectable from ui
- [ ] Wavetable file loading
- [x] Wavetable file loading
- [x] Create digital filters, prob biquad. Controllable from ui obv (cutoff + resonance)
- [x] Add polyphony somewhere. Probably involves a voice class. If processing power
allows it, tie a voice to each midi note
@@ -60,7 +60,7 @@ Build. The script will build and install dependencies automatically
On Windows (MSVC):
```PowerShell
.\scripts\build.ps1 # builds in build/Debug/
.\scripts\build.ps1
```
On Linux (GCC):
@@ -68,7 +68,7 @@ On Linux (GCC):
./scripts/build.sh
```
TODO: right now you need to run the executable from the executable's directory because of CWD paths and such. Needs to be fixed because annoying
Note: executing the app from the root directory will make the app use the default root level configs. Run the app from the build directory to customize your configurations with the build/config configurations.
Configure the CMake/build script if you have issues
@@ -80,13 +80,10 @@ To clean:
Note: dependencies are built into build/lib, so don't delete unless you need to rebuild the libraries
Use the install_dependencies script to manually install dependencies.
Build troubleshooting:
On windows, `bcdedit /set IncreaseUserVa 3072` solved cc1plus.exe: out of memory errors while building qt for me
## Configurations (NOT YET IMPLEMENTED)
## Configurations
Default config files are located in the config/ directory, and they are replicated into build/config/ if they dont already exist there. To edit the configurations, edit the config files in the build directory, not the defaults. Most config files are loaded/parsed at startup (TODO: investigate some reloading functions), so the program must be restarted, although not recompiled, for new configs to take effect. \
Voice profiles are saved into config files into a human-readable format (YAML) and can be edited manually or by saving within the app. \
## Wavetables (NOT YET IMPLEMENTED)
Wavetables are this synthesizer's starting point for audio synthesis. A wavetable (as defined for this synthesizer, not elsewhere) contains a single period of a particular wave-shape with a discrete number of samples. Wavetables are loaded at runtime and sampled by oscillator objects to define and mix different wave shapes. Further specifications, as well as instructions for generating your own wavetable (including an example python script << TODO), are located within config/wavetables/README.md
## Wavetables
Wavetables are this synthesizer's starting point for audio synthesis. A wavetable (as defined for this synthesizer, not elsewhere) contains a single period of a particular wave-shape with a discrete number of samples. Wavetables are loaded at runtime and sampled by oscillator objects to define and mix different wave shapes. Further specifications, as well as instructions for generating your own wavetable (including an example python script), are located within the scripts directory.

View File

@@ -14,3 +14,15 @@ stereoMode: 2
# number of samples per audio buffer
bufferSize: 512
# synth settings
#define SYNTH_PITCH_STANDARD 440.0f // frequency of home pitch
#define SYNTH_MIDI_HOME 69 // midi note index of home pitch
#define SYNTH_NOTES_PER_OCTAVE 12
# midi home note (usually A4=69) = pitchStandard hz (usually 440 or 432 hz)
pitchStandard: 440.0
midi_home: 69
notesPerOctave: 12 # changes to this give some interesting non-western harmonics

View File

@@ -4,51 +4,46 @@
# sequences in the form [x, x, x] denote [setValue, sliderMinimum, sliderMaximum]
version: 0x0003
# deprecated, useless
Osc1Freq: [100, 20, 600]
version: 0x04
# wavetable selections
OscWaveSelector1: 2
OscWaveSelector2: 3
# Frequency parameters
MasterOctaveOffset: [0, -5, 5]
MasterSemitoneOffset: [0, -12, 12]
MasterPitchOffset: [0, -100, 100]
Osc1OctaveOffset: [0, -5, 5]
Osc1SemitoneOffset: [0, -12, 12]
Osc1PitchOffset: [1.34, -100, 100]
Osc2OctaveOffset: [1, -5, 5]
Osc2SemitoneOffset: [0, -12, 12]
Osc2PitchOffset: [12.86, -100, 100]
Osc3OctaveOffset: [1, -5, 5]
Osc3SemitoneOffset: [7, -12, 12]
Osc3PitchOffset: [-8.79, -100, 100]
# gonna have something like this:
#MasterPitchOffset:
# - [0, -5, 5] # Octave
# - [0, -12, -12] # Semitone
# - [0, -100, 100] # Pitch
MasterPitchOffset:
Octave: [0, -5, 5]
Semitone: [0, -12, 12]
Pitch: [0, -100, 100]
Osc1PitchOffset:
Octave: [0, -5, 5]
Semitone: [0, -12, 12]
Pitch: [1.34, -100, 100]
Osc2PitchOffset:
Octave: [1, -5, 5]
Semitone: [0, -12, 12]
Pitch: [12.86, -100, 100]
Osc3PitchOffset:
Octave: [1, -5, 5]
Semitone: [7, -12, 12]
Pitch: [-8.79, -100, 100]
# Envelope generator parameters
Osc1Volume:
- [1, 0, 10] # Depth
- [0.05, 0, 2] # Attack
- [0.2, 0, 2] # Decay
- [0.7, 0, 1] # Sustain
- [0.2, 0, 2] # Release
Depth: [1, 0, 10]
Attack: [0.05, 0, 2]
Decay: [0.2, 0, 2]
Sustain: [0.7, 0, 1]
Release: [0.2, 0, 2]
FilterCutoff:
- [4, 0, 8] # Depth
- [0.05, 0, 2] # Attack
- [0.2, 0, 2] # Decay
- [0.2, 0, 1] # Sustain
- [0.25, 0, 2] # Release
Depth: [4, 0, 8]
Attack: [0.05, 0, 2]
Decay: [0.2, 0, 2]
Sustain: [0.2, 0, 1]
Release: [0.25, 0, 2]
FilterResonance:
- [3, 0, 8] # Depth
- [0.05, 0, 2] # Attack
- [0.2, 0, 2] # Decay
- [0.5, 0, 1] # Sustain
- [0.3, 0, 2] # Release
Depth: [3, 0, 8]
Attack: [0.05, 0, 2]
Decay: [0.2, 0, 2]
Sustain: [0.5, 0, 1]
Release: [0.3, 0, 2]

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

@@ -26,7 +26,6 @@ if (-not (Test-Path -Path $BUILD_DIR)) {
}
# detect dependencies
$libraries = @("rtaudio", "rtmidi", "yaml-cpp")
$dependencies_found = 0
foreach ($lib in $libraries) {
@@ -38,6 +37,7 @@ foreach ($lib in $libraries) {
}
}
# run the install script if dependencies not found
if (-not ($dependencies_found -eq $libraries.Count)) {
& "scripts\install_dependencies.ps1"
} else {
@@ -58,6 +58,8 @@ Write-Host "Building metabolus..."
cmake --build $BUILD_DIR --config $CONFIG
# TODO: install
cd $BUILD_DIR
cmake --install . --prefix ".\"
# link dlls
Write-Host "Deploying metabolus..."

View File

@@ -8,6 +8,26 @@ RTAUDIO_ROOT=${LIB_ROOT}/rtaudio
RTMIDI_ROOT=${LIB_ROOT}/rtmidi
YAMLCPP_ROOT=${LIB_ROOT}/yaml-cpp
# detect dependencies
libraries=("rtaudio" "rtmidi" "yaml-cpp")
dependencies_found=0
for lib in "${libraries[@]}"; do
if [[ -e "./build/lib/$lib" ]]; then
echo "found $lib"
((dependencies_found++))
else
echo "did not find $lib"
fi
done
# run the install script if dependencies not found
if [[ "$dependencies_found" -ne "${#libraries[@]}" ]]; then
./scripts/install_dependencies.sh
else
echo "All dependencies detected, skipping dependency install step..."
fi
cmake -S . -B ${BUILD_DIR} -G Ninja \
-DRtAudio_DIR="${RTAUDIO_ROOT}/share/rtaudio" \
-DRtMidi_DIR="${RTMIDI_ROOT}/share/rtmidi" \

View File

@@ -0,0 +1,52 @@
import math
from generate_wavetable import generateWavetable
WAVETABLE_FILE_NAME = "sigmoid"
# process expects a waveform from x=[0, 2pi) centered around f(x)=0
# normalization is handled by the wavetableGenerator
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
if(phase <= math.pi):
sample = 2 * (phase/math.pi) ** k - 1.0
else:
sample = -2 * (phase/math.pi - 1.0) ** k + 1.0
return sample
def sigmoid(phase):
k = 4
a = -k * (phase - math.pi)
sample = 2 / (1 + math.exp(a)) - 1
return sample
# pass in the name of your wavtable file and the process function
def main():
generateWavetable(WAVETABLE_FILE_NAME, sigmoid)
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,68 @@
# 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 compatibility)
# - binary format (float, double, int32, etc.) (RIGHT NOW I ONLY USE FLOAT)
# - domain (normal is a phase from x=0 to x=2pi)
# - range (depending on datatypes, 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 function defined in example_wavetable.py to calculate samples
# if you want a custom wavetable, copy/edit/modify the example function (desmos is great 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 + ".wt"
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

@@ -70,74 +70,76 @@ YAML::Node ConfigInterface::loadProfile(std::string filename) {
}
// extract values from the config file
std::array<ParamDefault, 5> osc1VolumeProfile = loadEnvProfile(&config, "Osc1Volume");
std::array<ParamDefault, 5> fCutoffProfile = loadEnvProfile(&config, "FilterCutoff");
std::array<ParamDefault, 5> fResonanceProfile = loadEnvProfile(&config, "FilterResonance");
std::array<Param, 5> osc1VolumeProfile = loadEnvProfile(&config, "Osc1Volume");
std::array<Param, 5> fCutoffProfile = loadEnvProfile(&config, "FilterCutoff");
std::array<Param, 5> fResonanceProfile = loadEnvProfile(&config, "FilterResonance");
std::array<ParamDefault, 3> masterPitchOffsets = {{
{ config["MasterOctaveOffset"][0].as<float>(), config["MasterOctaveOffset"][1].as<float>(), config["MasterOctaveOffset"][2].as<float>() },
{ config["MasterSemitoneOffset"][0].as<float>(), config["MasterSemitoneOffset"][1].as<float>(), config["MasterSemitoneOffset"][2].as<float>() },
{ config["MasterPitchOffset"][0].as<float>(), config["MasterPitchOffset"][1].as<float>(), config["MasterPitchOffset"][2].as<float>() },
YAML::Node masterNode = config["MasterPitchOffset"];
YAML::Node osc1Node = config["Osc1PitchOffset"];
YAML::Node osc2Node = config["Osc2PitchOffset"];
YAML::Node osc3Node = config["Osc3PitchOffset"];
std::array<Param, 3> masterPitchOffsets = {{
{ masterNode["Octave"][0].as<float>(), masterNode["Octave"][1].as<float>(), masterNode["Octave"][2].as<float>() },
{ masterNode["Semitone"][0].as<float>(), masterNode["Semitone"][1].as<float>(), masterNode["Semitone"][2].as<float>() },
{ masterNode["Pitch"][0].as<float>(), masterNode["Pitch"][1].as<float>(), masterNode["Pitch"][2].as<float>() },
}};
std::array<ParamDefault, 3> osc1PitchOffsets = {{
{ config["Osc1OctaveOffset"][0].as<float>(), config["Osc1OctaveOffset"][1].as<float>(), config["Osc1OctaveOffset"][2].as<float>() },
{ config["Osc1SemitoneOffset"][0].as<float>(), config["Osc1SemitoneOffset"][1].as<float>(), config["Osc1SemitoneOffset"][2].as<float>() },
{ config["Osc1PitchOffset"][0].as<float>(), config["Osc1PitchOffset"][1].as<float>(), config["Osc1PitchOffset"][2].as<float>() },
std::array<Param, 3> osc1PitchOffsets = {{
{ osc1Node["Octave"][0].as<float>(), osc1Node["Octave"][1].as<float>(), osc1Node["Octave"][2].as<float>() },
{ osc1Node["Semitone"][0].as<float>(), osc1Node["Semitone"][1].as<float>(), osc1Node["Semitone"][2].as<float>() },
{ osc1Node["Pitch"][0].as<float>(),osc1Node["Pitch"][1].as<float>(), osc1Node["Pitch"][2].as<float>() },
}};
std::array<ParamDefault, 3> osc2PitchOffsets = {{
{ config["Osc2OctaveOffset"][0].as<float>(), config["Osc2OctaveOffset"][1].as<float>(), config["Osc2OctaveOffset"][2].as<float>() },
{ config["Osc2SemitoneOffset"][0].as<float>(), config["Osc2SemitoneOffset"][1].as<float>(), config["Osc2SemitoneOffset"][2].as<float>() },
{ config["Osc2PitchOffset"][0].as<float>(), config["Osc2PitchOffset"][1].as<float>(), config["Osc2PitchOffset"][2].as<float>() },
std::array<Param, 3> osc2PitchOffsets = {{
{ osc2Node["Octave"][0].as<float>(), osc2Node["Octave"][1].as<float>(), osc2Node["Octave"][2].as<float>() },
{ osc2Node["Semitone"][0].as<float>(), osc2Node["Semitone"][1].as<float>(), osc2Node["Semitone"][2].as<float>() },
{ osc2Node["Pitch"][0].as<float>(), osc2Node["Pitch"][1].as<float>(), osc2Node["Pitch"][2].as<float>() },
}};
std::array<ParamDefault, 3> osc3PitchOffsets = {{
{ config["Osc3OctaveOffset"][0].as<float>(), config["Osc3OctaveOffset"][1].as<float>(), config["Osc3OctaveOffset"][2].as<float>() },
{ config["Osc3SemitoneOffset"][0].as<float>(), config["Osc3SemitoneOffset"][1].as<float>(), config["Osc3SemitoneOffset"][2].as<float>() },
{ config["Osc3PitchOffset"][0].as<float>(), config["Osc3PitchOffset"][1].as<float>(), config["Osc3PitchOffset"][2].as<float>() },
std::array<Param, 3> osc3PitchOffsets = {{
{ osc3Node["Octave"][0].as<float>(), osc3Node["Octave"][1].as<float>(), osc3Node["Octave"][2].as<float>() },
{ osc3Node["Semitone"][0].as<float>(), osc3Node["Semitone"][1].as<float>(), osc3Node["Semitone"][2].as<float>() },
{ osc3Node["Pitch"][0].as<float>(), osc3Node["Pitch"][1].as<float>(), osc3Node["Pitch"][2].as<float>() },
}};
// TODO: remove this once all the parameters are set properly
params_->resetToDefaults();
// set the values in the paramstore
params_->set(EnvelopeId::Osc1Volume, osc1VolumeProfile[0].def, osc1VolumeProfile[1].def, osc1VolumeProfile[2].def, osc1VolumeProfile[3].def, osc1VolumeProfile[4].def);
params_->set(EnvelopeId::FilterCutoff, fCutoffProfile[0].def, fCutoffProfile[1].def, fCutoffProfile[2].def, fCutoffProfile[3].def, fCutoffProfile[4].def);
params_->set(EnvelopeId::FilterResonance, fResonanceProfile[0].def, fResonanceProfile[1].def, fResonanceProfile[2].def, fResonanceProfile[3].def, fResonanceProfile[4].def);
params_->set(EnvelopeId::Osc1Volume, osc1VolumeProfile[0].val, osc1VolumeProfile[1].val, osc1VolumeProfile[2].val, osc1VolumeProfile[3].val, osc1VolumeProfile[4].val);
params_->set(EnvelopeId::FilterCutoff, fCutoffProfile[0].val, fCutoffProfile[1].val, fCutoffProfile[2].val, fCutoffProfile[3].val, fCutoffProfile[4].val);
params_->set(EnvelopeId::FilterResonance, fResonanceProfile[0].val, fResonanceProfile[1].val, fResonanceProfile[2].val, fResonanceProfile[3].val, fResonanceProfile[4].val);
// TODO: why do I bother passing in 5 values independently when I can just do an array like in loadEnvProfile ?
params_->set(ParamId::MasterOctaveOffset, masterPitchOffsets[0].def);
params_->set(ParamId::MasterSemitoneOffset, masterPitchOffsets[1].def);
params_->set(ParamId::MasterPitchOffset, masterPitchOffsets[2].def);
params_->set(ParamId::Osc1OctaveOffset, osc1PitchOffsets[0].def);
params_->set(ParamId::Osc1SemitoneOffset, osc1PitchOffsets[1].def);
params_->set(ParamId::Osc1PitchOffset, osc1PitchOffsets[2].def);
params_->set(ParamId::Osc2OctaveOffset, osc2PitchOffsets[0].def);
params_->set(ParamId::Osc2SemitoneOffset, osc2PitchOffsets[1].def);
params_->set(ParamId::Osc2PitchOffset, osc2PitchOffsets[2].def);
params_->set(ParamId::Osc3OctaveOffset, osc3PitchOffsets[0].def);
params_->set(ParamId::Osc3SemitoneOffset, osc3PitchOffsets[1].def);
params_->set(ParamId::Osc3PitchOffset, osc3PitchOffsets[2].def);
params_->set(ParamId::MasterOctaveOffset, masterPitchOffsets[0].val);
params_->set(ParamId::MasterSemitoneOffset, masterPitchOffsets[1].val);
params_->set(ParamId::MasterPitchOffset, masterPitchOffsets[2].val);
params_->set(ParamId::Osc1OctaveOffset, osc1PitchOffsets[0].val);
params_->set(ParamId::Osc1SemitoneOffset, osc1PitchOffsets[1].val);
params_->set(ParamId::Osc1PitchOffset, osc1PitchOffsets[2].val);
params_->set(ParamId::Osc2OctaveOffset, osc2PitchOffsets[0].val);
params_->set(ParamId::Osc2SemitoneOffset, osc2PitchOffsets[1].val);
params_->set(ParamId::Osc2PitchOffset, osc2PitchOffsets[2].val);
params_->set(ParamId::Osc3OctaveOffset, osc3PitchOffsets[0].val);
params_->set(ParamId::Osc3SemitoneOffset, osc3PitchOffsets[1].val);
params_->set(ParamId::Osc3PitchOffset, osc3PitchOffsets[2].val);
// TODO:
// load wavetable settings
// load oscillator pitch settings
params_->set(ParamId::Osc1WaveSelector1, static_cast<float>(config["OscWaveSelector1"].as<int>()));
params_->set(ParamId::Osc1WaveSelector2, static_cast<float>(config["OscWaveSelector2"].as<int>()));
return config;
}
std::array<ParamDefault, 5> ConfigInterface::loadEnvProfile(YAML::Node* node, std::string profile) {
std::array<Param, 5> ConfigInterface::loadEnvProfile(YAML::Node* node, std::string profile) {
YAML::Node envelopeNode = (*node)[profile];
std::array<ParamDefault, 5> paramProfile;
std::array<Param, 5> paramProfile;
for(int i = 0; i < paramProfile.size(); i++) {
paramProfile[i] = { envelopeNode[i][0].as<float>(), envelopeNode[i][1].as<float>(), envelopeNode[i][2].as<float>() };
}
paramProfile[0] = { envelopeNode["Depth"][0].as<float>(), envelopeNode["Depth"][1].as<float>(), envelopeNode["Depth"][2].as<float>() };
paramProfile[1] = { envelopeNode["Attack"][0].as<float>(), envelopeNode["Attack"][1].as<float>(), envelopeNode["Attack"][2].as<float>() };
paramProfile[2] = { envelopeNode["Decay"][0].as<float>(), envelopeNode["Decay"][1].as<float>(), envelopeNode["Decay"][2].as<float>() };
paramProfile[3] = { envelopeNode["Sustain"][0].as<float>(), envelopeNode["Sustain"][1].as<float>(), envelopeNode["Sustain"][2].as<float>() };
paramProfile[4] = { envelopeNode["Release"][0].as<float>(), envelopeNode["Release"][1].as<float>(), envelopeNode["Release"][2].as<float>() };
return paramProfile;
}
std::array<ParamDefault, 5> ConfigInterface::loadEnvProfile(std::string filename, std::string profile) {
std::array<Param, 5> ConfigInterface::loadEnvProfile(std::string filename, std::string profile) {
std::string filepath = "config/profiles/" + filename + ".yaml";
filepath = std::filesystem::absolute(filepath).string();

View File

@@ -8,7 +8,7 @@
#include "ParameterStore.h"
#define CONFIG_VERSION 0x0002
#define CONFIG_VERSION 0x04
enum class ConfigFile {
Audio = 0
@@ -33,8 +33,8 @@ public:
int getValue(ConfigFile file, std::string key, int defaultVal);
YAML::Node loadProfile(std::string filename);
std::array<ParamDefault, 5> loadEnvProfile(YAML::Node* node, std::string profile);
std::array<ParamDefault, 5> loadEnvProfile(std::string filename, std::string profile);
std::array<Param, 5> loadEnvProfile(YAML::Node* node, std::string profile);
std::array<Param, 5> loadEnvProfile(std::string filename, std::string profile);
private:

View File

@@ -6,12 +6,14 @@
#include <filesystem>
ParameterStore::ParameterStore() {
//resetToDefaults();
}
// set parameter value
void ParameterStore::set(ParamId id, float value) {
values_[static_cast<size_t>(id)].store(value, std::memory_order_relaxed);
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);
}
// set a whole envelope of parameters
@@ -27,13 +29,5 @@ void ParameterStore::set(EnvelopeId id, float depth, float a, float d, float s,
// get a single parameter
float ParameterStore::get(ParamId id) const {
return values_[static_cast<size_t>(id)].load(std::memory_order_relaxed);
}
void ParameterStore::resetToDefaults() {
for(size_t i = 0; i < PARAM_COUNT; i++) {
values_[i].store(PARAM_DEFS[i].def, std::memory_order_relaxed);
}
return values_[static_cast<size_t>(id)].load(std::memory_order_relaxed).val;
}

View File

@@ -66,45 +66,12 @@ constexpr std::array<EnvelopeParam, static_cast<size_t>(EnvelopeId::Count)> ENV_
{ ParamId::FilterResonanceDepth, ParamId::FilterResonanceEnvA, ParamId::FilterResonanceEnvD, ParamId::FilterResonanceEnvS, ParamId::FilterResonanceEnvR }, // FilterResonance
}};
struct ParamDefault {
float def;
struct Param {
float val;
float min;
float max;
};
constexpr std::array<ParamDefault, static_cast<size_t>(ParamId::Count)> PARAM_DEFS {{
{ 100.0f, 20.0f, 600.0f}, // Osc1Freq
{ 2.0f, 0.0f, 0.0f}, // OscWaveSelector1
{ 1.0f, 0.0f, 0.0f}, // OscWaveSelector2
{ 0.0f, -5.0f, 5.0f}, // MasterOctaveOffset
{ 0.0f, -12.0f, 12.0f}, // MasterSemitoneOffset
{ 0.0f, -100.0f, 100.0f}, // MasterPitchOffset
{ 0.0f, -5.0f, 5.0f}, // Osc1OctaveOffset
{ 0.0f, -12.0f, 12.0f}, // Osc1SemitoneOffset
{ 0.0f, -100.0f, 100.0f}, // Osc1PitchOffset
{ 1.0f, -5.0f, 5.0f}, // Osc2OctaveOffset
{ 0.0f, -12.0f, 12.0f}, // Osc2SemitoneOffset
{ 0.0f, -100.0f, 100.0f}, // Osc2PitchOffset
{ 1.0f, -5.0f, 5.0f}, // Osc3OctaveOffset
{ 7.0f, -12.0f, 12.0f}, // Osc3SemitoneOffset
{ 1.96f, -100.0f, 100.0f}, // Osc3PitchOffset
{ 1.0f, 0.0f, 2.0f}, // Osc1VolumeDepth
{ 0.05f, 0.0f, 2.0f}, // Osc1VolumeEnvA
{ 0.2f, 0.0f, 2.0f}, // Osc1VolumeEnvD
{ 0.7f, 0.0f, 1.0f}, // Osc1VolumeEnvS
{ 0.2f, 0.0f, 2.0f}, // Osc1VolumeEnvR
{ 4.0f, 0.0f, 8.0f}, // FilterCutoffDepth
{ 0.05f, 0.0f, 2.0f}, // FilterCutoffEnvA
{ 0.20f, 0.0f, 2.0f}, // FilterCutoffEnvD
{ 0.2f, 0.0f, 1.0f}, // FilterCutoffEnvS
{ 0.25f, 0.0f, 2.0f}, // FilterCutoffEnvR
{ 3.0f, 0.0f, 8.0f}, // FilterResonanceDepth
{ 0.05f, 0.0f, 2.0f}, // FilterResonanceEnvA
{ 0.20f, 0.0f, 2.0f}, // FilterResonanceEnvD
{ 0.5f, 0.0f, 1.0f}, // FilterResonanceEnvS
{ 0.30f, 0.0f, 2.0f}, // FilterResonanceEnvR
}};
constexpr size_t PARAM_COUNT = static_cast<size_t>(ParamId::Count);
class ParameterStore {
@@ -119,10 +86,9 @@ public:
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)); }
void resetToDefaults();
private:
std::array<std::atomic<float>, PARAM_COUNT> values_;
std::array<std::atomic<Param>, PARAM_COUNT> values_;
};

View File

@@ -24,8 +24,7 @@ float Oscillator::process(uint8_t note, float detune, bool& scopeTrigger) {
float Oscillator::process(float frequency, bool& scopeTrigger) {
float pitchOffset = 0.5f;
float phaseInc = pitchOffset * 2.0f * M_PI * frequency / sampleRate_;
float phaseInc = 2.0f * M_PI * frequency / sampleRate_;
float sampleOut = wavetable_->sample(activeWavetable_, phase_);

View File

@@ -81,6 +81,7 @@ float Voice::process(float* params, bool& scopeTrigger) {
float velocityGain = std::lerp(velocityCenter, velocity_, velocitySensitivity);
float gain = gainEnv * getParam(ParamId::Osc1VolumeDepth) * velocityGain;
gain *= (100.0f - static_cast<float>(note_)) * 0.005f + 0.75;
// sample generation
uint8_t osc1Wave = (static_cast<uint8_t>(std::round(getParam(ParamId::Osc1WaveSelector1))));

View File

@@ -3,6 +3,7 @@
#include <cmath>
#include <iostream>
#include <fstream>
WavetableController::WavetableController() {
// load from files
@@ -15,35 +16,25 @@ WavetableController::WavetableController() {
void WavetableController::init() {
wavetables_.resize(4); // resize for however many files we find
// don't really know how the files are gonna work
// but I'd like two files- a yaml that contains metadata like name, length, range, datatype, etc.
// and the main data be just a big array of that data type in a binary file
// although having it all in a single bin makes the most sense with the metadata being in the header
float phase = 0.0f;
float phaseInc = 2.0f * M_PI / static_cast<float>(SYNTH_WAVETABLE_SIZE);
for(int i = 0; i < SYNTH_WAVETABLE_SIZE; i++) {
wavetables_[0][i] = std::sin(phase) / 0.707f; // sine
wavetables_[1][i] = (phase >= M_PI) ? 1.0f : -1.0f; // square
wavetables_[2][i] = ((phase / M_PI) - 1.0f) / 0.577f; // saw
// triangle
float tri = 0.0f;
if(phase <= M_PI/2.0f) {
tri = phase * 2.0f/M_PI;
} else if(phase <= 3.0f*M_PI/2.0f) {
tri = phase * -2.0f/M_PI + 2.0f;
} else {
tri = phase * 2.0f/M_PI - 4.0f;
// find wavetable files
std::vector<std::filesystem::path> wavetableFiles;
for(std::filesystem::directory_entry entry : std::filesystem::directory_iterator(wavetablesRoot_)) {
if(std::filesystem::is_regular_file(entry.status())) {
wavetableFiles.push_back(entry.path());
}
wavetables_[3][i] = tri / 0.577f;
phase += phaseInc;
}
uint32_t wavetableCount = wavetableFiles.size();
wavetables_.resize(wavetableCount);
// load the wavetable files
for(int i = 0; i < wavetableCount; i++) {
std::cout << "loading wavetable file [" << i << "]: " << wavetableFiles[i] << std::endl;
std::ifstream inputFile(wavetableFiles[i], std::ios::in | std::ios::binary);
if(!inputFile) std::cout << "error opening file" << std::endl;
inputFile.read(reinterpret_cast<char*>(wavetables_[i].data()), SYNTH_WAVETABLE_SIZE * sizeof(float));
}
// wavetable data structure is best explained in scripts/generate_wavetable.py
}

View File

@@ -4,6 +4,7 @@
#include <array>
#include <vector>
#include <cstdint>
#include <filesystem>
#define SYNTH_WAVETABLE_SIZE 2048
#ifndef M_PI // I hate my stupid chungus life
@@ -30,6 +31,8 @@ private:
std::vector<Wavetable> wavetables_;
const std::filesystem::path wavetablesRoot_ = "./config/wavetables";
};

View File

@@ -48,6 +48,17 @@ MainWindow::MainWindow(QWidget *parent) :
this, [this](int index) {
audio_->parameters()->set(ParamId::Osc1WaveSelector2, index);
});
ui_->comboOsc1WaveSelector1->clear();
ui_->comboOsc1WaveSelector2->clear();
for(std::filesystem::directory_entry entry : std::filesystem::directory_iterator("config/wavetables")) {
if(std::filesystem::is_regular_file(entry.status())) {
std::string fileName = entry.path().string().substr(18);
fileName.erase(fileName.length() - 3);
ui_->comboOsc1WaveSelector1->addItem(QString::fromStdString(fileName));
ui_->comboOsc1WaveSelector2->addItem(QString::fromStdString(fileName));
}
}
// rogue sliders, TODO: clean these up in a package
connect(ui_->sliderMasterOctave, &SmartSlider::valueChanged,
@@ -146,50 +157,54 @@ void MainWindow::onResetClicked() {
// TODO: clean these up, maybe put them in a package like the envelope generators (it'll help encapsulate the int-snapping business)
// what I might do is make a variable-length slider-package object
ui_->sliderMasterOctave->setResolution(configRoot["MasterOctaveOffset"][2].as<int>() - configRoot["MasterOctaveOffset"][1].as<int>());
ui_->sliderMasterOctave->setRange(configRoot["MasterOctaveOffset"][1].as<int>(), configRoot["MasterOctaveOffset"][2].as<int>());
ui_->sliderMasterOctave->setValue(configRoot["MasterOctaveOffset"][0].as<int>());
YAML::Node masterNode = configRoot["MasterPitchOffset"];
YAML::Node osc1Node = configRoot["Osc1PitchOffset"];
YAML::Node osc2Node = configRoot["Osc2PitchOffset"];
YAML::Node osc3Node = configRoot["Osc3PitchOffset"];
ui_->sliderMasterOctave->setResolution(masterNode["Octave"][2].as<int>() - masterNode["Octave"][1].as<int>());
ui_->sliderMasterOctave->setRange(masterNode["Octave"][1].as<int>(), masterNode["Octave"][2].as<int>());
ui_->sliderMasterOctave->setValue(masterNode["Octave"][0].as<int>());
ui_->sliderMasterSemitone->setResolution(configRoot["MasterSemitoneOffset"][2].as<int>() - configRoot["MasterSemitoneOffset"][1].as<int>());
ui_->sliderMasterSemitone->setRange(configRoot["MasterSemitoneOffset"][1].as<int>(), configRoot["MasterSemitoneOffset"][2].as<int>());
ui_->sliderMasterSemitone->setValue(configRoot["MasterSemitoneOffset"][0].as<int>());
ui_->sliderMasterSemitone->setResolution(masterNode["Semitone"][2].as<int>() - masterNode["Semitone"][1].as<int>());
ui_->sliderMasterSemitone->setRange(masterNode["Semitone"][1].as<int>(), masterNode["Semitone"][2].as<int>());
ui_->sliderMasterSemitone->setValue(masterNode["Semitone"][0].as<int>());
ui_->sliderMasterPitch->setRange(configRoot["MasterPitchOffset"][1].as<float>(), configRoot["MasterPitchOffset"][2].as<float>());
ui_->sliderMasterPitch->setValue(configRoot["MasterPitchOffset"][0].as<float>());
ui_->sliderMasterPitch->setRange(masterNode["Pitch"][1].as<float>(), masterNode["Pitch"][2].as<float>());
ui_->sliderMasterPitch->setValue(masterNode["Pitch"][0].as<float>());
ui_->sliderOsc1Octave->setResolution(configRoot["Osc1OctaveOffset"][2].as<int>() - configRoot["Osc1OctaveOffset"][1].as<int>());
ui_->sliderOsc1Octave->setRange(configRoot["Osc1OctaveOffset"][1].as<int>(), configRoot["Osc1OctaveOffset"][2].as<int>());
ui_->sliderOsc1Octave->setValue(configRoot["Osc1OctaveOffset"][0].as<int>());
ui_->sliderOsc1Octave->setResolution(osc1Node["Octave"][2].as<int>() - osc1Node["Octave"][1].as<int>());
ui_->sliderOsc1Octave->setRange(osc1Node["Octave"][1].as<int>(), osc1Node["Octave"][2].as<int>());
ui_->sliderOsc1Octave->setValue(osc1Node["Octave"][0].as<int>());
ui_->sliderOsc1Semitone->setResolution(configRoot["Osc1SemitoneOffset"][2].as<int>() - configRoot["Osc1SemitoneOffset"][1].as<int>());
ui_->sliderOsc1Semitone->setRange(configRoot["Osc1SemitoneOffset"][1].as<int>(), configRoot["Osc1SemitoneOffset"][2].as<int>());
ui_->sliderOsc1Semitone->setValue(configRoot["Osc1SemitoneOffset"][0].as<int>());
ui_->sliderOsc1Semitone->setResolution(osc1Node["Semitone"][2].as<int>() - osc1Node["Semitone"][1].as<int>());
ui_->sliderOsc1Semitone->setRange(osc1Node["Semitone"][1].as<int>(), osc1Node["Semitone"][2].as<int>());
ui_->sliderOsc1Semitone->setValue(osc1Node["Semitone"][0].as<int>());
ui_->sliderOsc1Pitch->setRange(configRoot["Osc1PitchOffset"][1].as<float>(), configRoot["Osc1PitchOffset"][2].as<float>());
ui_->sliderOsc1Pitch->setValue(configRoot["Osc1PitchOffset"][0].as<float>());
ui_->sliderOsc1Pitch->setRange(osc1Node["Pitch"][1].as<float>(), osc1Node["Pitch"][2].as<float>());
ui_->sliderOsc1Pitch->setValue(osc1Node["Pitch"][0].as<float>());
ui_->sliderOsc2Octave->setResolution(configRoot["Osc2OctaveOffset"][2].as<int>() - configRoot["Osc2OctaveOffset"][1].as<int>());
ui_->sliderOsc2Octave->setRange(configRoot["Osc2OctaveOffset"][1].as<int>(), configRoot["Osc2OctaveOffset"][2].as<int>());
ui_->sliderOsc2Octave->setValue(configRoot["Osc2OctaveOffset"][0].as<int>());
ui_->sliderOsc2Octave->setResolution(osc2Node["Octave"][2].as<int>() - osc2Node["Octave"][1].as<int>());
ui_->sliderOsc2Octave->setRange(osc2Node["Octave"][1].as<int>(), osc2Node["Octave"][2].as<int>());
ui_->sliderOsc2Octave->setValue(osc2Node["Octave"][0].as<int>());
ui_->sliderOsc2Semitone->setResolution(configRoot["Osc2SemitoneOffset"][2].as<int>() - configRoot["Osc2SemitoneOffset"][1].as<int>());
ui_->sliderOsc2Semitone->setRange(configRoot["Osc2SemitoneOffset"][1].as<int>(), configRoot["Osc2SemitoneOffset"][2].as<int>());
ui_->sliderOsc2Semitone->setValue(configRoot["Osc2SemitoneOffset"][0].as<int>());
ui_->sliderOsc2Semitone->setResolution(osc2Node["Semitone"][2].as<int>() - osc2Node["Semitone"][1].as<int>());
ui_->sliderOsc2Semitone->setRange(osc2Node["Semitone"][1].as<int>(), osc2Node["Semitone"][2].as<int>());
ui_->sliderOsc2Semitone->setValue(osc2Node["Semitone"][0].as<int>());
ui_->sliderOsc2Pitch->setRange(configRoot["Osc2PitchOffset"][1].as<float>(), configRoot["Osc2PitchOffset"][2].as<float>());
ui_->sliderOsc2Pitch->setValue(configRoot["Osc2PitchOffset"][0].as<float>());
ui_->sliderOsc2Pitch->setRange(osc2Node["Pitch"][1].as<float>(), osc2Node["Pitch"][2].as<float>());
ui_->sliderOsc2Pitch->setValue(osc2Node["Pitch"][0].as<float>());
ui_->sliderOsc3Octave->setResolution(configRoot["Osc3OctaveOffset"][2].as<int>() - configRoot["Osc3OctaveOffset"][1].as<int>());
ui_->sliderOsc3Octave->setRange(configRoot["Osc3OctaveOffset"][1].as<int>(), configRoot["Osc3OctaveOffset"][2].as<int>());
ui_->sliderOsc3Octave->setValue(configRoot["Osc3OctaveOffset"][0].as<int>());
ui_->sliderOsc3Octave->setResolution(osc3Node["Octave"][2].as<int>() - osc3Node["Octave"][1].as<int>());
ui_->sliderOsc3Octave->setRange(osc3Node["Octave"][1].as<int>(), osc3Node["Octave"][2].as<int>());
ui_->sliderOsc3Octave->setValue(osc3Node["Octave"][0].as<int>());
ui_->sliderOsc3Semitone->setResolution(configRoot["Osc3SemitoneOffset"][2].as<int>() - configRoot["Osc3SemitoneOffset"][1].as<int>());
ui_->sliderOsc3Semitone->setRange(configRoot["Osc3SemitoneOffset"][1].as<int>(), configRoot["Osc3SemitoneOffset"][2].as<int>());
ui_->sliderOsc3Semitone->setValue(configRoot["Osc3SemitoneOffset"][0].as<int>());
ui_->sliderOsc3Semitone->setResolution(osc3Node["Semitone"][2].as<int>() - osc3Node["Semitone"][1].as<int>());
ui_->sliderOsc3Semitone->setRange(osc3Node["Semitone"][1].as<int>(), osc3Node["Semitone"][2].as<int>());
ui_->sliderOsc3Semitone->setValue(osc3Node["Semitone"][0].as<int>());
ui_->sliderOsc3Pitch->setRange(osc3Node["Pitch"][1].as<float>(), osc3Node["Pitch"][2].as<float>());
ui_->sliderOsc3Pitch->setValue(osc3Node["Pitch"][0].as<float>());
ui_->sliderOsc3Pitch->setRange(configRoot["Osc3PitchOffset"][1].as<float>(), configRoot["Osc3PitchOffset"][2].as<float>());
ui_->sliderOsc3Pitch->setValue(configRoot["Osc3PitchOffset"][0].as<float>());
ui_->comboOsc1WaveSelector1->setCurrentIndex(configRoot["OscWaveSelector1"].as<int>());
ui_->comboOsc1WaveSelector2->setCurrentIndex(configRoot["OscWaveSelector2"].as<int>());

View File

@@ -75,7 +75,7 @@ void EnvelopeGenerator::emitEnvelope() {
);
}
void EnvelopeGenerator::init(EnvelopeId id, std::array<ParamDefault, 5> profile) {
void EnvelopeGenerator::init(EnvelopeId id, std::array<Param, 5> profile) {
EnvelopeParam params = ENV_PARAMS[static_cast<size_t>(id)];
@@ -85,10 +85,10 @@ void EnvelopeGenerator::init(EnvelopeId id, std::array<ParamDefault, 5> profile)
ui_->sliderSustain->setRange(profile[3].min, profile[3].max);
ui_->sliderRelease->setRange(profile[4].min, profile[4].max);
setDepth(profile[0].def);
setAttack(profile[1].def);
setDecay(profile[2].def);
setSustain(profile[3].def);
setRelease(profile[4].def);
setDepth(profile[0].val);
setAttack(profile[1].val);
setDecay(profile[2].val);
setSustain(profile[3].val);
setRelease(profile[4].val);
}

View File

@@ -17,7 +17,7 @@ public:
~EnvelopeGenerator();
// connects signals, sets parameters to a provided profile
void init(EnvelopeId id, std::array<ParamDefault, 5> profile);
void init(EnvelopeId id, std::array<Param, 5> profile);
// setters
void setDepth(float v);

0
tests/.gitkeep Normal file
View File