Compare commits

14 Commits

Author SHA1 Message Date
35663df15c add helper function for binding smart-sliders 2026-04-18 21:55:58 -05:00
2590b03756 cleanup include paths 2026-04-18 13:48:00 -05:00
8e838c61e6 bugfix: loading more than 4 wavetables 2026-04-18 12:25:20 -05:00
5c9a37b8d4 init 2026-04-17 21:02:44 -05:00
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
35 changed files with 395 additions and 300 deletions

1
.gitignore vendored
View File

@@ -1,2 +1,3 @@
build/*
.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)
@@ -90,7 +90,9 @@ qt_add_executable(metabolus
set_target_properties(metabolus PROPERTIES AUTOUIC ON)
target_include_directories(metabolus PRIVATE
${CMAKE_SOURCE_DIR}/src/
${CMAKE_SOURCE_DIR}/src/ui
${CMAKE_SOURCE_DIR}/src/synth
${CMAKE_SOURCE_DIR}/src/ui/widgets
)
@@ -108,3 +110,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
@@ -48,11 +48,11 @@ Linux: GCC
Clone repository
```PowerShell
git clone https://github.com/Blitblank/metabalus.git --recursive
git clone https://git.vxbard.net/homeburger/metabalus.git --recursive
```
or if you forgot to --recursive:
```PowerShell
git clone https://github.com/Blitblank/metabalus.git
git clone https://git.vxbard.net/homeburger/metabalus.git
git submodule update --init --recursive
```
\
@@ -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

@@ -15,6 +15,10 @@ int main(int argc, char *argv[]) {
MainWindow window; // entry point goes to MainWindow::MainWindow()
window.show();
// TODO: logging
// TODO: separate app from the window
// i made a good debug filtering setup in ouros so could probably improt that into this project
int status = app.exec(); // app execution; blocks until window close
return status;

View File

@@ -5,9 +5,9 @@
#include <stdint.h>
#include <atomic>
#include "../ConfigInterface.h"
#include "ConfigInterface.h"
#include "Synth.h"
#include "../KeyboardController.h"
#include "KeyboardController.h"
#if defined(_WIN32)
#define AUDIO_API RtAudio::WINDOWS_WASAPI

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

@@ -11,6 +11,8 @@
#define M_PI 3.14159265358979323846
#endif
// TODO: if the amount of notes per octave is changed via config then this constant needs to be calculated at startup
// as SYNTH_Xth_ROOT_TWO
#define SYNTH_TWELFTH_ROOT_TWO 1.05946309435929526463
// TODO: you get it, also in a yml config

View File

@@ -11,13 +11,9 @@ void ScopeBuffer::push(float sample) {
buffer_[w % buffer_.size()] = sample;
}
// TODO: needs a mutex/spinlock to prevent flickering
// outputs value from the scope buffer, called by the scope widget
void ScopeBuffer::read(std::vector<float>& out) const {
// yeah this didn't work, maybe it needs to be atomic or something
//while(!spinLock_) { int x = 1 + 1; }
size_t w = writeIndex_.load(std::memory_order_relaxed);
for (size_t i = 0; i < out.size(); i++) {
size_t idx = (w + trigger_ + i * wavelength_ / out.size()) % buffer_.size();

View File

@@ -1,8 +1,8 @@
#pragma once
#include "../ParameterStore.h"
#include "../NoteQueue.h"
#include "ParameterStore.h"
#include "NoteQueue.h"
#include "Envelope.h"
#include "ScopeBuffer.h"
#include "Filter.h"

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))));
@@ -100,7 +101,7 @@ float Voice::process(float* params, bool& scopeTrigger) {
// TODO: implement controls for noise
//float scale = static_cast<float>(rand()) / static_cast<float>(RAND_MAX); // Range [0.0, 1.0]
//float noise = -1.0f + 2.0f * scale;
//float noise = (-1.0f + 2.0f * scale) * 0.2f;
// these values didn't sound good so I commented them out before I get controls for them
// mix oscillators

View File

@@ -4,7 +4,7 @@
#include "Oscillator.h"
#include "Envelope.h"
#include "Filter.h"
#include "../ParameterStore.h"
#include "ParameterStore.h"
#ifndef M_PI // I hate my stupid chungus life
#define M_PI 3.14159265358979323846

View File

@@ -3,47 +3,36 @@
#include <cmath>
#include <iostream>
#include <fstream>
WavetableController::WavetableController() {
// load from files
init();
//std::cout << "wavetable init" << std::endl;
}
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;
}
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
}
@@ -65,8 +54,8 @@ float WavetableController::sample(uint8_t wavetableIndex, float phase) {
uint32_t index = static_cast<uint32_t>(round(scaledPhase));
if(index >= SYNTH_WAVETABLE_SIZE) index = SYNTH_WAVETABLE_SIZE - 1;
if(wavetableIndex >= 4) {
wavetableIndex = 3;
if(wavetableIndex >= wavetableCount_) {
wavetableIndex = wavetableCount_ - 1;
} else if(wavetableIndex < 0) {
wavetableIndex = 0;
}

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
@@ -29,6 +30,9 @@ public:
private:
std::vector<Wavetable> wavetables_;
uint32_t wavetableCount_;
const std::filesystem::path wavetablesRoot_ = "./config/wavetables";
};

View File

@@ -2,8 +2,7 @@
#include "MainWindow.h"
#include "ui_MainWindow.h"
#include "SmartSlider/SmartSlider.h"
#include "../ParameterStore.h"
#include "ParameterStore.h"
MainWindow::MainWindow(QWidget *parent) :
QMainWindow(parent),
@@ -48,67 +47,30 @@ 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,
this, [this](float value) {
audio_->parameters()->set(ParamId::MasterOctaveOffset, value);
ui_->sliderMasterOctave->setResolution();
});
connect(ui_->sliderMasterSemitone, &SmartSlider::valueChanged,
this, [this](float value) {
audio_->parameters()->set(ParamId::MasterSemitoneOffset, value);
ui_->sliderMasterSemitone->setResolution();
});
connect(ui_->sliderMasterPitch, &SmartSlider::valueChanged,
this, [this](float value) {
audio_->parameters()->set(ParamId::MasterPitchOffset, value);
});
connect(ui_->sliderOsc1Octave, &SmartSlider::valueChanged,
this, [this](float value) {
audio_->parameters()->set(ParamId::Osc1OctaveOffset, value);
ui_->sliderOsc1Octave->setResolution();
});
connect(ui_->sliderOsc1Semitone, &SmartSlider::valueChanged,
this, [this](float value) {
audio_->parameters()->set(ParamId::Osc1SemitoneOffset, value);
ui_->sliderOsc1Semitone->setResolution();
});
connect(ui_->sliderOsc1Pitch, &SmartSlider::valueChanged,
this, [this](float value) {
audio_->parameters()->set(ParamId::Osc1PitchOffset, value);
});
connect(ui_->sliderOsc2Octave, &SmartSlider::valueChanged,
this, [this](float value) {
audio_->parameters()->set(ParamId::Osc2OctaveOffset, value);
ui_->sliderOsc2Octave->setResolution();
});
connect(ui_->sliderOsc2Semitone, &SmartSlider::valueChanged,
this, [this](float value) {
audio_->parameters()->set(ParamId::Osc2SemitoneOffset, value);
ui_->sliderOsc2Semitone->setResolution();
});
connect(ui_->sliderOsc2Pitch, &SmartSlider::valueChanged,
this, [this](float value) {
audio_->parameters()->set(ParamId::Osc2PitchOffset, value);
});
connect(ui_->sliderOsc3Octave, &SmartSlider::valueChanged,
this, [this](float value) {
audio_->parameters()->set(ParamId::Osc3OctaveOffset, value);
ui_->sliderOsc3Octave->setResolution();
});
connect(ui_->sliderOsc3Semitone, &SmartSlider::valueChanged,
this, [this](float value) {
audio_->parameters()->set(ParamId::Osc3SemitoneOffset, value);
ui_->sliderOsc3Semitone->setResolution();
});
connect(ui_->sliderOsc3Pitch, &SmartSlider::valueChanged,
this, [this](float value) {
audio_->parameters()->set(ParamId::Osc3PitchOffset, value);
});
bindSlider(ui_->sliderMasterOctave, ParamId::MasterOctaveOffset, true);
bindSlider(ui_->sliderMasterSemitone, ParamId::MasterSemitoneOffset, true);
bindSlider(ui_->sliderMasterPitch, ParamId::MasterPitchOffset);
bindSlider(ui_->sliderOsc1Octave, ParamId::Osc1OctaveOffset, true);
bindSlider(ui_->sliderOsc1Semitone, ParamId::Osc1SemitoneOffset, true);
bindSlider(ui_->sliderOsc1Pitch, ParamId::Osc1PitchOffset);
bindSlider(ui_->sliderOsc2Octave, ParamId::Osc2OctaveOffset, true);
bindSlider(ui_->sliderOsc2Semitone, ParamId::Osc2SemitoneOffset, true);
bindSlider(ui_->sliderOsc2Pitch, ParamId::Osc2PitchOffset);
bindSlider(ui_->sliderOsc3Octave, ParamId::Osc3OctaveOffset, true);
bindSlider(ui_->sliderOsc3Semitone, ParamId::Osc3SemitoneOffset, true);
bindSlider(ui_->sliderOsc3Pitch, ParamId::Osc3PitchOffset);
// synth business
audio_->start();
@@ -146,51 +108,65 @@ 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>());
// TODO: the oscillator things also need an amplitude parameter too
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(configRoot["Osc3PitchOffset"][1].as<float>(), configRoot["Osc3PitchOffset"][2].as<float>());
ui_->sliderOsc3Pitch->setValue(configRoot["Osc3PitchOffset"][0].as<float>());
ui_->sliderOsc3Pitch->setRange(osc3Node["Pitch"][1].as<float>(), osc3Node["Pitch"][2].as<float>());
ui_->sliderOsc3Pitch->setValue(osc3Node["Pitch"][0].as<float>());
ui_->comboOsc1WaveSelector1->setCurrentIndex(configRoot["OscWaveSelector1"].as<int>());
ui_->comboOsc1WaveSelector2->setCurrentIndex(configRoot["OscWaveSelector2"].as<int>());
}
void MainWindow::bindSlider(SmartSlider* slider, ParamId param, bool updateResolution)
{
connect(slider, &SmartSlider::valueChanged, this,
[this](float value, ParamId param, SmartSlider* slider, bool updateResolution) {
audio_->parameters()->set(param, value);
if (updateResolution) slider->setResolution();
});
}

View File

@@ -4,9 +4,10 @@
#include <QMainWindow>
#include <QKeyEvent>
#include "../ConfigInterface.h"
#include "../synth/AudioEngine.h"
#include "../MidiController.h"
#include "ConfigInterface.h"
#include "synth/AudioEngine.h"
#include "MidiController.h"
#include "SmartSlider/SmartSlider.h"
QT_BEGIN_NAMESPACE
namespace Ui { class MainWindow; }
@@ -29,6 +30,8 @@ private slots:
private:
void bindSlider(SmartSlider* slider, ParamId param, bool updateResolution = false);
Ui::MainWindow *ui_;
ParameterStore params_;

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

@@ -3,7 +3,7 @@
#include <QWidget>
#include "../../ParameterStore.h"
#include "ParameterStore.h"
QT_BEGIN_NAMESPACE
namespace Ui { class EnvelopeGenerator; }
@@ -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);

View File

@@ -3,7 +3,7 @@
#include "ui_Scope.h"
// TODO: fix include directories because what is this
#include "../../../synth/ScopeBuffer.h"
#include "synth/ScopeBuffer.h"
#include <QPainter>
#include <iostream>

0
tests/.gitkeep Normal file
View File