This commit is contained in:
2026-01-30 18:35:47 -06:00
35 changed files with 532 additions and 166 deletions

9
.gitmodules vendored Normal file
View File

@@ -0,0 +1,9 @@
[submodule "lib/rtaudio"]
path = lib/rtaudio
url = https://github.com/thestk/rtaudio.git
[submodule "lib/rtmidi"]
path = lib/rtmidi
url = https://github.com/thestk/rtmidi.git
[submodule "lib/yaml-cpp"]
path = lib/yaml-cpp
url = https://github.com/jbeder/yaml-cpp.git

View File

@@ -9,56 +9,38 @@ find_package(Qt6 REQUIRED COMPONENTS Widgets)
if (WIN32) # windows 11 x86_64
# Header-only target (real target, no ::)
add_library(rtaudio_headers INTERFACE)
target_include_directories(rtaudio_headers INTERFACE
"C:/rtaudio/include"
"C:/rtaudio/include/rtAudio"
add_library(RtAudio::rtaudio SHARED IMPORTED)
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/rtaudio"
)
add_library(RtMidi::rtmidi SHARED IMPORTED)
set_target_properties(RtMidi::rtmidi PROPERTIES
IMPORTED_LOCATION "${RtMidi_ROOT}/bin/rtmidi.dll"
IMPORTED_IMPLIB "${RtMidi_ROOT}/lib/rtmidi.lib"
INTERFACE_INCLUDE_DIRECTORIES "${RtMidi_ROOT}/include/rtmidi"
)
# Imported binary (real target, no ::)
add_library(rtaudio_binary SHARED IMPORTED)
set_target_properties(rtaudio_binary PROPERTIES
IMPORTED_LOCATION "C:/rtaudio/bin/rtaudio.dll"
IMPORTED_IMPLIB "C:/rtaudio/lib/rtaudio.lib"
add_library(yaml-cpp SHARED IMPORTED)
set_target_properties(yaml-cpp PROPERTIES
IMPORTED_LOCATION "${yaml-cpp_ROOT}/bin/yaml-cpp.dll"
IMPORTED_IMPLIB "${yaml-cpp_ROOT}/lib/yaml-cpp.lib"
INTERFACE_INCLUDE_DIRECTORIES "${yaml-cpp_ROOT}/include"
)
# Unified interface target
add_library(rtaudio INTERFACE)
target_link_libraries(rtaudio INTERFACE
rtaudio_headers
rtaudio_binary
)
# Public alias (this is where :: belongs)
add_library(RtAudio::RtAudio ALIAS rtaudio)
add_library(rtmidi_headers INTERFACE)
target_include_directories(rtmidi_headers INTERFACE
"C:/rtmidi/include"
"C:/rtmidi/include/rtMidi"
)
add_library(rtmidi_binary SHARED IMPORTED)
set_target_properties(rtmidi_binary PROPERTIES
IMPORTED_LOCATION "C:/rtmidi/bin/rtmidi.dll"
IMPORTED_IMPLIB "C:/rtmidi/lib/rtmidi.lib"
)
add_library(rtmidi INTERFACE)
target_link_libraries(rtmidi INTERFACE
rtmidi_headers
rtmidi_binary
)
add_library(RtMidi::RtMidi ALIAS rtmidi)
else() # debian 12 x86_64
find_package(PkgConfig REQUIRED)
pkg_check_modules(RTAUDIO REQUIRED rtaudio)
pkg_check_modules(RTMIDI REQUIRED rtmidi)
pkg_check_modules(YAMLCPP REQUIRED yaml-cpp)
endif()
qt_standard_project_setup()
# TODO: prob fix this to make it less ugly
# might nest CMakeList.txt files once I clean up the directory structure
qt_add_executable(metabolus
src/main.cpp
src/ui/MainWindow.cpp
@@ -72,6 +54,8 @@ qt_add_executable(metabolus
src/MidiController.h
src/NoteQueue.cpp
src/NoteQueue.h
src/ConfigInterface.cpp
src/ConfigInterface.h
src/synth/AudioEngine.cpp
src/synth/AudioEngine.h
src/synth/Envelope.cpp
@@ -108,27 +92,19 @@ target_include_directories(metabolus PRIVATE
)
if (WIN32)
target_compile_options(metabolus PUBLIC "/Zc:__cplusplus")
target_link_libraries(metabolus
PRIVATE
RtAudio::rtaudio
RtMidi::rtmidi
yaml-cpp
Qt6::Widgets
RtAudio::RtAudio
RtMidi::RtMidi
)
add_custom_command(TARGET metabolus POST_BUILD
COMMAND ${CMAKE_COMMAND} -E copy_if_different
"C:/rtaudio/bin/rtaudio.dll"
$<TARGET_FILE_DIR:metabolus>
)
add_custom_command(TARGET metabolus POST_BUILD
COMMAND ${CMAKE_COMMAND} -E copy_if_different
"C:/rtmidi/bin/rtmidi.dll"
$<TARGET_FILE_DIR:metabolus>
)
else()
target_include_directories(metabolus PRIVATE ${RTAUDIO_INCLUDE_DIRS} ${RTMIDI_INCLUDE_DIRS})
target_link_libraries(metabolus PRIVATE Qt6::Widgets ${RTAUDIO_LIBRARIES} ${RTMIDI_LIBRARIES})
target_include_directories(metabolus PRIVATE ${RTAUDIO_INCLUDE_DIRS} ${RTMIDI_INCLUDE_DIRS} ${YAMLCPP_INCLUDE_DIRS})
target_link_libraries(metabolus PRIVATE Qt6::Widgets ${RTAUDIO_LIBRARIES} ${RTMIDI_LIBRARIES} ${YAMLCPP_LIBARIES})
target_compile_options(metabolus PRIVATE ${RTAUDIO_CFLAGS_OTHER})
endif()

View File

@@ -37,9 +37,56 @@ This synthesizer isn't very good, but it's neat :3
- [ ] Noise
- [ ] LFO modulation
## setup
TODO: instructions on build setup
Package Dependencies: Qt 6, RtAudio, RtMidi
## Build Instructions
Prerequisites:
CMake: https://cmake.org/download/ \
QtWidgets: https://www.qt.io/development/download-qt-installer-oss
Windows: MSVC (The build scripts use Visual Studio 17 2022)
Linux: GCC
Clone repository
```PowerShell
git clone https://github.com/Blitblank/metabalus.git --recursive
```
or if you forgot to --recursive:
```PowerShell
git clone https://github.com/Blitblank/metabalus.git
git submodule update --init --recursive
```
\
Build. The script will build and install dependencies automatically
On Windows (MSVC):
```PowerShell
.\scripts\build.ps1 # builds in build/Debug/
```
On Linux (GCC):
```Bash
./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
Configure the CMake/build script if you have issues
To clean:
```
.\scripts\clean.ps1
./scripts/clean.sh
```
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)
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
$ ./scripts/build.sh
PS ./scripts/build.bat

16
config/audio.yaml Normal file
View File

@@ -0,0 +1,16 @@
# audio.yaml
# Configures properties for the RtAudio engine
# Number of samples per second
sampleRate: 44100
# unconfigurable: sampleFormat; [-1, 1] float
# number of audio channels
channels: 2
# 0 = mono, 1 = stereo, 2 = pseudo-stereo
stereoMode: 2
# number of samples per audio buffer
bufferSize: 512

View File

@@ -0,0 +1,45 @@
# default.yaml
# Default voice profile
# sequences in the form [x, x, x] denote [setValue, sliderMinimum, sliderMaximum]
version: 0x0002
# deprecated, useless
Osc1Freq: [100, 20, 600]
# wavetable selections
OscWaveSelector1: 2
OscWaveSelector2: 1
# Oscillator frequency parameters
Osc1OctaveOffset: [0, -5, 5]
Osc1SemitoneOffset: [0, -12, 12]
Osc1PitchOffset: [0, -100, 100]
Osc2OctaveOffset: [1, -5, 5]
Osc2SemitoneOffset: [0, -12, 12]
Osc2PitchOffset: [0, -100, 100]
Osc3OctaveOffset: [1, -5, 5]
Osc3SemitoneOffset: [7, -12, 12]
Osc3PitchOffset: [1.96, -100, 100]
# Envelope generator parameters
Osc1Volume:
- [1, 0, 2] # Depth
- [0.05, 0, 2] # Attack
- [0.2, 0, 2] # Decay
- [0.7, 0, 1] # Sustain
- [0.2, 0, 2] # Release
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
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

1
lib/rtaudio Submodule

Submodule lib/rtaudio added at 409636b5dc

1
lib/rtmidi Submodule

Submodule lib/rtmidi added at a3233c2294

1
lib/yaml-cpp Submodule

Submodule lib/yaml-cpp added at 89ff142b99

View File

@@ -1,69 +0,0 @@
@echo off
setlocal
REM ================================
REM Configuration
REM ================================
set BUILD_DIR=build
set CONFIG=Release
set QT_ROOT=C:\Qt\6.10.1\msvc2022_64
set RTAUDIO_ROOT=C:\rtaudio
set RTMIDI_ROOT=C:\rtmidi
REM ================================
REM Environment setup
REM ================================
call "%ProgramFiles%\Microsoft Visual Studio\2022\Community\VC\Auxiliary\Build\vcvars64.bat"
set PATH=%QT_ROOT%\bin;%PATH%
REM ================================
REM Configure
REM ================================
if not exist %BUILD_DIR% (
mkdir %BUILD_DIR%
)
cmake -S . -B %BUILD_DIR% ^
-G Ninja ^
-DCMAKE_BUILD_TYPE=%CONFIG% ^
-DRTAUDIO_ROOT=%RTAUDIO_ROOT% ^
-DRTMIDI_ROOT=%RTMIDI_ROOT%
if errorlevel 1 goto error
REM ================================
REM Build
REM ================================
cmake --build %BUILD_DIR%
if errorlevel 1 goto error
REM ================================
REM Deploy Qt + RtAudio
REM ================================
cd %BUILD_DIR%
windeployqt metabolus.exe
copy "%RTAUDIO_ROOT%\bin\rtaudio.dll" .
copy "%RTMIDI_ROOT%\bin\rtmidi.dll" .
echo.
echo Build successful.
goto end
:error
echo.
echo Build FAILED.
exit /b 1
:end
endlocal
pause

76
scripts/build.ps1 Normal file
View File

@@ -0,0 +1,76 @@
$PROJECT_ROOT = $PWD
# config
$BUILD_DIR = "$PWD/build"
$CONFIG = "Release"
# change these to your need
# or TODO: make qt_root configurable
$QT_ROOT = "C:\Qt\6.10.1\msvc2022_64"
$RTAUDIO_ROOT = "$BUILD_DIR\lib\rtaudio"
$RTMIDI_ROOT = "$BUILD_DIR\lib\rtmidi"
$YAMLCPP_ROOT = "$BUILD_DIR\lib\yaml-cpp"
$CONFIG_ROOT = "$PROJECT_ROOT\config"
# setup
& "$Env:Programfiles\Microsoft Visual Studio\2022\Community\VC\Auxiliary\Build\vcvars64.bat"
$PATH="$QT_ROOT\bin;$PATH"
if (-not (Test-Path -Path $BUILD_DIR)) {
mkdir $BUILD_DIR
}
# detect dependencies
$libraries = @("rtaudio", "rtmidi", "yaml-cpp")
$dependencies_found = 0
foreach ($lib in $libraries) {
if (Test-Path -Path ".\build\lib\$lib") {
Write-Host "found $lib"
$dependencies_found++
} else {
Write-Host "did not find $lib"
}
}
if (-not ($dependencies_found -eq $libraries.Count)) {
& "scripts\install_dependencies.ps1"
} else {
Write-Host "All dependencies detected, skipping dependency install step..."
}
# configure
Write-Host "Configuring metabolus..."
cmake -S . -B $BUILD_DIR -G "Visual Studio 17 2022" `
-DQt6_ROOT="$QT_ROOT\lib\cmake\Qt6" `
-DRtAudio_ROOT="$RTAUDIO_ROOT" `
-DRtMidi_ROOT="$RTMIDI_ROOT" `
-Dyaml-cpp_ROOT="$YAMLCPP_ROOT" `
# build
Write-Host "Building metabolus..."
cmake --build $BUILD_DIR --config $CONFIG
# TODO: install
# link dlls
Write-Host "Deploying metabolus..."
cd $BUILD_DIR
& "$QT_ROOT\bin\windeployqt6.exe" .\$CONFIG\metabolus.exe
# copy dlls
Copy-Item -Path "$RTAUDIO_ROOT\bin\rtaudio.dll" -Destination .\$CONFIG
Copy-Item -Path "$RTMIDI_ROOT\bin\rtmidi.dll" -Destination .\$CONFIG
Copy-Item -Path "$YAMLCPP_ROOT\bin\yaml-cpp.dll" -Destination .\$CONFIG
# copy configs, but don't overwrite
Copy-Item -Path "$CONFIG_ROOT" -Destination ".\$CONFIG\" -Recurse -ErrorAction SilentlyContinue
cd $PROJECT_ROOT

View File

@@ -0,0 +1,42 @@
echo "Installing dependencies ... "
# TODO: add a clean (like delete build dirs) script
$project_root = $PWD
if (-not (Test-Path -Path "$PWD\build\lib")) {
mkdir "$PWD\build\lib"
}
$build_lib_dir = "$PWD\build\lib"
# rtaudio
mkdir "$build_lib_dir\rtaudio" -Force
cd $project_root\lib\rtaudio
cmake -S . -B build -G "Visual Studio 17 2022" `
-DRTDUIO_API_WASAPI=ON `
-DRTAUDIO_API_DS=OFF `
-DRT_AUDIO_API_ASIO=OFF `
-DRTAUDIO_BUILD_SHARED_LIBS=ON
cmake --build build --config Release
cmake --install build --prefix "$build_lib_dir\rtaudio"
# rtmidi
mkdir "$build_lib_dir\rtmidi" -Force
cd $project_root\lib\rtmidi
cmake -S . -B build -G "Visual Studio 17 2022" `
-DRT_MIDI_API_WINMM=ON `
-DRTMIDI_BUILD_SHARED_LIBS=ON
cmake --build build --config Release
cmake --install build --prefix "$build_lib_dir\rtmidi"
# yaml-cpp
mkdir "$build_lib_dir\yaml-cpp" -Force
cd $project_root\lib\yaml-cpp
cmake -S . -B build -G "Visual Studio 17 2022" `
-DYAML_BUILD_SHARED_LIBS=ON
cmake --build build --config Release
cmake --install build --prefix "$build_lib_dir\yaml-cpp"
cd $project_root

View File

113
src/ConfigInterface.cpp Normal file
View File

@@ -0,0 +1,113 @@
#include "ConfigInterface.h"
#include <fstream>
#include <filesystem>
namespace fs = std::filesystem;
ConfigInterface::ConfigInterface() {
//std::cout << "Config constructor" << std::endl;
}
ConfigInterface::ConfigInterface(ParameterStore* params): params_(params) {
}
// lots of checking in this to make this safe
int ConfigInterface::getValue(ConfigFile file, std::string key, int defaultVal) {
// assemble filepath
std::string filepath = configRoot + "/" + filePaths[static_cast<int>(file)];
filepath = fs::absolute(filepath).string();
// attempt to open file
YAML::Node config;
try {
YAML::Node config = YAML::LoadFile(filepath);
// read key if it exists
if(config[key]) {
return config[key].as<int>(defaultVal);
} else {
return -1; // key does not exist
}
} catch(const std::exception& e) {
std::cerr << e.what() << std::endl;
return -1;
}
// unreachable
return -1;
}
// ugly but if it works it works
void ConfigInterface::loadProfile(std::string filename) {
// load file
std::string filepath = "config/profiles/" + filename + ".yaml";
filepath = std::filesystem::absolute(filepath).string();
YAML::Node config;
try {
config = YAML::LoadFile(filepath);
} catch(const std::exception& e) {
std::cerr << e.what() << std::endl;
return;
}
// check version
int version = config["version"].as<int>(); // yaml-cpp parses unquoted hex as integers
if(version < CONFIG_VERSION) {
std::cout << "Parameter profile version " << version << "is outdated below the compatible version " << CONFIG_VERSION << std::endl;
return;
} else {
std::cout << version << std::endl;
}
// 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");
// 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);
// TODO: why do I bother passing in 5 values independently when I can just do an array ?
// VVV look down there its so easy
// TODO:
// load wavetable settings
// load oscillator pitch settings
}
std::array<ParamDefault, 5> ConfigInterface::loadEnvProfile(YAML::Node* node, std::string profile) {
YAML::Node envelopeNode = (*node)[profile];
std::array<ParamDefault, 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>() };
}
return paramProfile;
}
std::array<ParamDefault, 5> ConfigInterface::loadEnvProfile(std::string filename, std::string profile) {
std::string filepath = "config/profiles/" + filename + ".yaml";
filepath = std::filesystem::absolute(filepath).string();
YAML::Node config = YAML::LoadFile(filepath);
return loadEnvProfile(&config, profile);
}

46
src/ConfigInterface.h Normal file
View File

@@ -0,0 +1,46 @@
#pragma once
#include <string>
#include <vector>
#include <iostream>
#include "yaml-cpp/yaml.h"
#include "ParameterStore.h"
#define CONFIG_VERSION 0x0002
enum class ConfigFile {
Audio = 0
// other files here
};
// might have a config file for specifying paths to other config files instead of this
const std::vector<std::string> filePaths = {
"audio.yaml"
};
// Reads from yaml config files
// Handles things like profile loading
class ConfigInterface {
public:
ConfigInterface();
ConfigInterface(ParameterStore* params);
~ConfigInterface() = default;
int getValue(ConfigFile file, std::string key, int defaultVal);
void 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);
private:
const std::string configRoot = "config";
// loading parameters
ParameterStore* params_;
};

View File

@@ -9,7 +9,7 @@ MidiController::MidiController(NoteQueue& queue) : noteQueue_(queue) {
midiIn_ = std::make_unique<RtMidiIn>();
midiIn_->ignoreTypes(false, false, false);
} catch (RtMidiError& e) {
std::cerr << "RtMidi init failed: " << e.getMessage() << std::endl;
std::cout << "RtMidi init failed: " << e.getMessage() << std::endl;
}
// TODO: this still doesnt work on windows
}
@@ -22,7 +22,7 @@ MidiController::~MidiController() {
bool MidiController::openDefaultPort() {
if (!midiIn_) return false;
if (midiIn_->getPortCount() == 0) {
std::cerr << "No MIDI input ports available\n";
std::cout << "No MIDI input ports available" << std::endl;
return false;
}
return openPort(0);
@@ -34,7 +34,7 @@ bool MidiController::openPort(unsigned int index) {
try {
midiIn_->openPort(index);
midiIn_->setCallback(&MidiController::midiCallback, this);
std::cout << "Opened MIDI port: " << midiIn_->getPortName(index) << "\n";
std::cout << "Opened MIDI port: " << midiIn_->getPortName(index) << std::endl;
return true;
} catch (RtMidiError& e) {
std::cerr << e.getMessage() << std::endl;

View File

@@ -5,7 +5,7 @@
// add event to noteQueue, called by MidiController or keyboardController
bool NoteQueue::push(const NoteEvent& event) {
size_t head = head_.load(std::memory_order_relaxed);
size_t next = (head + 1) % SIZE;
size_t next = (head + 1) % SYNTH_NOTE_QUEUE_SIZE;
if(next == tail_.load(std::memory_order_relaxed)) return false; // full
@@ -22,7 +22,7 @@ bool NoteQueue::pop(NoteEvent& event) {
if(tail == head_.load(std::memory_order_acquire)) return false; // empty
event = buffer_[tail];
tail_.store((tail + 1) % SIZE, std::memory_order_release);
tail_.store((tail + 1) % SYNTH_NOTE_QUEUE_SIZE, std::memory_order_release);
return true;
}

View File

@@ -6,6 +6,8 @@
#include <cstdint>
#include <chrono>
#define SYNTH_NOTE_QUEUE_SIZE 128
enum class NoteEventType {
NoteOn,
NoteOff
@@ -29,9 +31,8 @@ public:
bool pop(NoteEvent& event);
private:
static constexpr size_t SIZE = 128;
std::array<NoteEvent, SIZE> buffer_;
std::array<NoteEvent, SYNTH_NOTE_QUEUE_SIZE> buffer_;
std::atomic<size_t> head_{ 0 };
std::atomic<size_t> tail_{ 0 };

View File

@@ -1,8 +1,12 @@
#include "ParameterStore.h"
#include <iostream>
#include "yaml-cpp/yaml.h" // TODO: using yaml.h outside of ConfigInterface feels spaghetti to me
#include <filesystem>
ParameterStore::ParameterStore() {
resetToDefaults();
//resetToDefaults();
}
// set parameter value
@@ -27,9 +31,9 @@ float ParameterStore::get(ParamId id) const {
}
void ParameterStore::resetToDefaults() {
for(size_t i = 0; i < PARAM_COUNT; i++) {
values_[i].store(PARAM_DEFS[i].def, std::memory_order_relaxed);
}
}
// TODO: applying parameter profiles will work similarly to above function
}

View File

@@ -2,19 +2,20 @@
#include <QApplication>
#include "ui/MainWindow.h"
#include "ConfigInterface.h"
#include <iostream>
int main(int argc, char *argv[]) {
// std::cout << "Main()" << std::endl;
QApplication app(argc, argv);
MainWindow window; // entry point goes to MainWindow::MainWindow()
window.show();
int status = app.exec(); // assembles ui
int status = app.exec(); // app execution; blocks until window close
return status;
}

View File

@@ -3,14 +3,12 @@
#include <iostream>
AudioEngine::AudioEngine() : synth_(params_) {
AudioEngine::AudioEngine(ConfigInterface* config, ParameterStore* params) : params_(params), synth_(params), config_(config) {
if(audio_.getDeviceCount() < 1) {
throw std::runtime_error("No audio devices found");
}
// TODO: get audio configurations
synth_.setSampleRate(sampleRate_);
synth_.setScopeBuffer(&scope_);
}
@@ -21,6 +19,13 @@ AudioEngine::~AudioEngine() {
bool AudioEngine::start() {
// get config values
sampleRate_ = config_->getValue(ConfigFile::Audio, "sampleRate", sampleRate_);
bufferFrames_ = config_->getValue(ConfigFile::Audio, "bufferSize", bufferFrames_);
channels_ = config_->getValue(ConfigFile::Audio, "channels", channels_);
synth_.setSampleRate(sampleRate_);
// initialize the audio engine
RtAudio::StreamParameters params;
params.deviceId = audio_.getDefaultOutputDevice();
@@ -46,11 +51,13 @@ void AudioEngine::stop() {
if(audio_.isStreamOpen()) audio_.closeStream();
}
int32_t AudioEngine::audioCallback( void* outputBuffer, void*, uint32_t nFrames, double, RtAudioStreamStatus status, void* userData) {
// called by RtAudio continuously, sends outputBuffer to audio drivers
int32_t AudioEngine::audioCallback(void* outputBuffer, void*, uint32_t nFrames, double, RtAudioStreamStatus status, void* userData) {
// error if process is too slow for the callback. If this is consistent, then need to optimize synth.process() or whatever cascades from it
if (status) std::cerr << "Stream underflow" << std::endl;
// populate audio buffer
return static_cast<AudioEngine*>(userData)->process(static_cast<float*>(outputBuffer), nFrames);
}

View File

@@ -5,6 +5,7 @@
#include <stdint.h>
#include <atomic>
#include "../ConfigInterface.h"
#include "Synth.h"
#include "../KeyboardController.h"
@@ -17,7 +18,9 @@
class AudioEngine {
public:
AudioEngine();
AudioEngine() = default;
AudioEngine(ConfigInterface* config, ParameterStore* params);
~AudioEngine();
// starts the audio stream. returns true on success and false on failure
@@ -27,7 +30,7 @@ public:
void stop();
// getters
ParameterStore* parameters() { return &params_; }
ParameterStore* parameters() { return params_; }
NoteQueue& noteQueue() { return noteQueue_; }
ScopeBuffer& scopeBuffer() { return scope_; }
@@ -39,7 +42,8 @@ private:
// calls the synth.process to generate a buffer of audio samples
int32_t process(float* out, uint32_t nFrames);
ParameterStore params_; // stores the control parameters
ConfigInterface* config_; // access to config files
ParameterStore* params_; // stores the control parameters
NoteQueue noteQueue_; // stores note events for passing between threads
Synth synth_; // generates audio
ScopeBuffer scope_ { 1024 }; // stores audio samples for visualization

View File

@@ -11,8 +11,13 @@ 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

@@ -21,6 +21,7 @@ public:
void setWavelength(int32_t wavelength) { wavelength_ = wavelength; }
int32_t trigger() { return trigger_; }
int32_t wavelength() { return wavelength_; }
void spinlock(bool lock) { spinLock_ = lock; };
// NOTE: there are limits to the wavelengths that the scope can show cleanly due to the size of the audio buffer
// at a buffer size of 256 at 44100hz the min visible steady frequency is ~172hz
@@ -33,4 +34,6 @@ private:
int32_t trigger_ = 0; // units in array indices
int32_t wavelength_ = 400;
bool spinLock_ = false;
};

View File

@@ -8,13 +8,13 @@
#define M_PI 3.14159265358979323846
#endif
Synth::Synth(const ParameterStore& params) : paramStore_(params) {
Synth::Synth(ParameterStore* params) : paramStore_(params) {
voices_.fill(Voice(params_.data(), &wavetable_));
}
void Synth::updateParams() {
for(size_t i = 0; i < PARAM_COUNT; i++) {
params_[i].target = paramStore_.get(static_cast<ParamId>(i));
params_[i].target = paramStore_->get(static_cast<ParamId>(i));
}
}
@@ -92,6 +92,9 @@ void Synth::process(float* out, uint32_t nFrames, uint32_t sampleRate) {
}
}
// lock the scope from the buffer
scope_->spinlock(true);
for (uint32_t i = 0; i < nFrames; i++) {
// updates internal buffered parameters for smoothing
@@ -133,4 +136,7 @@ void Synth::process(float* out, uint32_t nFrames, uint32_t sampleRate) {
}
}
// unlock the scope from the buffer
scope_->spinlock(false);
}

View File

@@ -15,7 +15,8 @@
class Synth {
public:
Synth(const ParameterStore& params);
Synth() = default;
Synth(ParameterStore* params);
~Synth() = default;
// generates a buffer of audio samples nFrames long
@@ -39,7 +40,7 @@ private:
Voice* findFreeVoice();
Voice* findVoiceByNote(uint8_t note);
const ParameterStore& paramStore_;
ParameterStore* paramStore_;
// smoothed params creates a buffer in case the thread controlling paramStore gets blocked
std::array<SmoothedParam, PARAM_COUNT> params_;

View File

@@ -98,7 +98,7 @@ float Voice::process(float* params, bool& scopeTrigger) {
float osc3 = oscillators_[2].process(osc3NoteOffset + note_, getParam(ParamId::Osc3PitchOffset)/100.0f, temp);
// mix oscillators
float sampleOut = (osc1 + osc2*0.5f + osc3*0.25f) * gain;
float sampleOut = (osc1 + osc2*0.25f + osc3*0.125f) * gain;
// filter sample
float baseFreq = oscillators_[0].frequency();
@@ -106,8 +106,8 @@ float Voice::process(float* params, bool& scopeTrigger) {
float resonance = resonanceEnv * getParam(ParamId::FilterResonanceDepth) * velocityGain;
filter1_.setParams(Filter::Type::BiquadLowpass, cutoffFreq, resonance);
filter2_.setParams(Filter::Type::BiquadLowpass, cutoffFreq, resonance);
sampleOut = filter1_.biquadProcess(sampleOut);
sampleOut = filter2_.biquadProcess(sampleOut);
float filteredSample = filter1_.biquadProcess(sampleOut);
// sampleOut = filter2_.biquadProcess(sampleOut); // TODO: for some reason second filter is unstable only on windows 🤷
return sampleOut;
return filteredSample;
}

View File

@@ -63,6 +63,7 @@ private:
// filters
Filter filter1_;
Filter filter2_;
// TODO: I think the filter's state being uninitialized is what's causing popping when a voice starts for the first time
// paramstore pointer
SmoothedParam* params_;

View File

@@ -9,7 +9,7 @@ WavetableController::WavetableController() {
init();
std::cout << "wavetable init" << std::endl;
//std::cout << "wavetable init" << std::endl;
}

View File

@@ -3,6 +3,7 @@
#include <array>
#include <vector>
#include <cstdint>
#define SYNTH_WAVETABLE_SIZE 2048
#ifndef M_PI // I hate my stupid chungus life

View File

@@ -8,7 +8,8 @@
MainWindow::MainWindow(QWidget *parent) :
QMainWindow(parent),
ui_(new Ui::MainWindow),
audio_(new AudioEngine()),
config_(ConfigInterface(&params_)),
audio_(new AudioEngine(&config_, &params_)),
keyboard_(audio_->noteQueue()),
midi_(audio_->noteQueue()) {
@@ -52,7 +53,10 @@ MainWindow::MainWindow(QWidget *parent) :
audio_->start();
// midi
#ifndef _WIN32
midi_.openPort(1); // TODO: error check
#endif
}
MainWindow::~MainWindow() {
@@ -71,12 +75,13 @@ void MainWindow::onResetClicked() {
// initialize to defaults
// envelopeGenerators
ui_->envelopeOsc1Volume->init(EnvelopeId::Osc1Volume);
ui_->envelopeFilterCutoff->init(EnvelopeId::FilterCutoff);
ui_->envelopeFilterResonance->init(EnvelopeId::FilterResonance);
config_.loadProfile("default");
// update ui from the paramstore
ui_->envelopeOsc1Volume->init(EnvelopeId::Osc1Volume, config_.loadEnvProfile("default", "Osc1Volume"));
ui_->envelopeFilterCutoff->init(EnvelopeId::FilterCutoff, config_.loadEnvProfile("default", "FilterCutoff"));
ui_->envelopeFilterResonance->init(EnvelopeId::FilterResonance, config_.loadEnvProfile("default", "FilterResonance"));
// comboBoxes
ui_->comboOsc1WaveSelector1->setCurrentIndex(static_cast<int>(PARAM_DEFS[static_cast<size_t>(ParamId::Osc1WaveSelector1)].def));
ui_->comboOsc1WaveSelector2->setCurrentIndex(static_cast<int>(PARAM_DEFS[static_cast<size_t>(ParamId::Osc1WaveSelector2)].def));

View File

@@ -4,6 +4,7 @@
#include <QMainWindow>
#include <QKeyEvent>
#include "../ConfigInterface.h"
#include "../synth/AudioEngine.h"
#include "../MidiController.h"
@@ -27,8 +28,11 @@ private slots:
void onResetClicked();
private:
Ui::MainWindow *ui_;
ParameterStore params_;
ConfigInterface config_;
AudioEngine* audio_ = nullptr;
KeyboardController keyboard_;
MidiController midi_;

View File

@@ -4,8 +4,6 @@
#include <iostream>
// TODO: package the rogue sliders into the envelopeGenerators with a "base" column (its what the "peak" slider in the esp synth was supposed to be)
EnvelopeGenerator::EnvelopeGenerator(QWidget* parent) : QWidget(parent), ui_(new Ui::EnvelopeGenerator) {
ui_->setupUi(this);
@@ -94,3 +92,21 @@ void EnvelopeGenerator::init(EnvelopeId id) {
setRelease(PARAM_DEFS[static_cast<size_t>(params.r)].def);
}
void EnvelopeGenerator::init(EnvelopeId id, std::array<ParamDefault, 5> profile) {
EnvelopeParam params = ENV_PARAMS[static_cast<size_t>(id)];
ui_->sliderDepth->setRange(profile[0].min, profile[0].max);
ui_->sliderAttack->setRange(profile[1].min, profile[1].max);
ui_->sliderDecay->setRange(profile[2].min, profile[2].max);
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);
}

View File

@@ -18,6 +18,7 @@ public:
// connects signals, sets parameters to the defaults defined in paramStore
void init(EnvelopeId id);
void init(EnvelopeId id, std::array<ParamDefault, 5> profile);
// setters
void setDepth(float v);

View File

@@ -2,6 +2,7 @@
#include "Scope.h"
#include "ui_Scope.h"
// TODO: fix include directories because what is this
#include "../../../synth/ScopeBuffer.h"
#include <QPainter>
@@ -23,7 +24,7 @@ void Scope::setScopeBuffer(ScopeBuffer* buffer) {
}
void Scope::paintEvent(QPaintEvent*) {
if (!buffer_) return;
if(!buffer_) return;
int32_t wavelength = buffer_->wavelength();
int32_t trigger = buffer_->trigger();

View File

@@ -32,4 +32,5 @@ private:
ScopeBuffer* buffer_ = nullptr;
std::vector<float> samples_;
QTimer timer_;
};