diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..f070881 --- /dev/null +++ b/.gitmodules @@ -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 diff --git a/CMakeLists.txt b/CMakeLists.txt index 50d0f52..a4ed177 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -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" - $ - ) - - add_custom_command(TARGET metabolus POST_BUILD - COMMAND ${CMAKE_COMMAND} -E copy_if_different - "C:/rtmidi/bin/rtmidi.dll" - $ ) 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() diff --git a/README.md b/README.md index 969363f..5841786 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/config/audio.yaml b/config/audio.yaml new file mode 100644 index 0000000..3e356bd --- /dev/null +++ b/config/audio.yaml @@ -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 diff --git a/config/profiles/default.yaml b/config/profiles/default.yaml new file mode 100644 index 0000000..7fa4d50 --- /dev/null +++ b/config/profiles/default.yaml @@ -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 diff --git a/lib/rtaudio b/lib/rtaudio new file mode 160000 index 0000000..409636b --- /dev/null +++ b/lib/rtaudio @@ -0,0 +1 @@ +Subproject commit 409636b5dcad3054ae5a9e85014bba3861b8edab diff --git a/lib/rtmidi b/lib/rtmidi new file mode 160000 index 0000000..a3233c2 --- /dev/null +++ b/lib/rtmidi @@ -0,0 +1 @@ +Subproject commit a3233c22949342f6697681e2cf2403e27fcf0c9e diff --git a/lib/yaml-cpp b/lib/yaml-cpp new file mode 160000 index 0000000..89ff142 --- /dev/null +++ b/lib/yaml-cpp @@ -0,0 +1 @@ +Subproject commit 89ff142b991af432b5d7a7cee55282f082a7e629 diff --git a/scripts/build.bat b/scripts/build.bat deleted file mode 100644 index 15999a5..0000000 --- a/scripts/build.bat +++ /dev/null @@ -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 \ No newline at end of file diff --git a/scripts/build.ps1 b/scripts/build.ps1 new file mode 100644 index 0000000..aa98e78 --- /dev/null +++ b/scripts/build.ps1 @@ -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 diff --git a/scripts/install_dependencies.ps1 b/scripts/install_dependencies.ps1 new file mode 100644 index 0000000..1e6478b --- /dev/null +++ b/scripts/install_dependencies.ps1 @@ -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 diff --git a/scripts/install_dependencies.sh b/scripts/install_dependencies.sh new file mode 100644 index 0000000..e69de29 diff --git a/src/ConfigInterface.cpp b/src/ConfigInterface.cpp new file mode 100644 index 0000000..058ae0b --- /dev/null +++ b/src/ConfigInterface.cpp @@ -0,0 +1,113 @@ + +#include "ConfigInterface.h" + +#include +#include + +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(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(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(); // 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 osc1VolumeProfile = loadEnvProfile(&config, "Osc1Volume"); + std::array fCutoffProfile = loadEnvProfile(&config, "FilterCutoff"); + std::array 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 ConfigInterface::loadEnvProfile(YAML::Node* node, std::string profile) { + + YAML::Node envelopeNode = (*node)[profile]; + + std::array paramProfile; + + for(int i = 0; i < paramProfile.size(); i++) { + paramProfile[i] = { envelopeNode[i][0].as(), envelopeNode[i][1].as(), envelopeNode[i][2].as() }; + } + + return paramProfile; +} + +std::array 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); +} diff --git a/src/ConfigInterface.h b/src/ConfigInterface.h new file mode 100644 index 0000000..084c7ad --- /dev/null +++ b/src/ConfigInterface.h @@ -0,0 +1,46 @@ + +#pragma once + +#include +#include +#include +#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 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 loadEnvProfile(YAML::Node* node, std::string profile); + std::array loadEnvProfile(std::string filename, std::string profile); + +private: + + const std::string configRoot = "config"; + + // loading parameters + ParameterStore* params_; + +}; diff --git a/src/MidiController.cpp b/src/MidiController.cpp index f1422e8..e2aee4e 100644 --- a/src/MidiController.cpp +++ b/src/MidiController.cpp @@ -9,7 +9,7 @@ MidiController::MidiController(NoteQueue& queue) : noteQueue_(queue) { midiIn_ = std::make_unique(); 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; diff --git a/src/NoteQueue.cpp b/src/NoteQueue.cpp index 85df585..1306af7 100644 --- a/src/NoteQueue.cpp +++ b/src/NoteQueue.cpp @@ -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; } diff --git a/src/NoteQueue.h b/src/NoteQueue.h index 8d87e6f..811dcd8 100644 --- a/src/NoteQueue.h +++ b/src/NoteQueue.h @@ -6,6 +6,8 @@ #include #include +#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 buffer_; + std::array buffer_; std::atomic head_{ 0 }; std::atomic tail_{ 0 }; diff --git a/src/ParameterStore.cpp b/src/ParameterStore.cpp index 49f1d94..3e7d895 100644 --- a/src/ParameterStore.cpp +++ b/src/ParameterStore.cpp @@ -1,8 +1,12 @@ #include "ParameterStore.h" +#include +#include "yaml-cpp/yaml.h" // TODO: using yaml.h outside of ConfigInterface feels spaghetti to me +#include + 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 +} diff --git a/src/main.cpp b/src/main.cpp index 92ca2a1..7d68bee 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -2,19 +2,20 @@ #include #include "ui/MainWindow.h" +#include "ConfigInterface.h" #include 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; } diff --git a/src/synth/AudioEngine.cpp b/src/synth/AudioEngine.cpp index fa1673b..6c80fdd 100644 --- a/src/synth/AudioEngine.cpp +++ b/src/synth/AudioEngine.cpp @@ -3,14 +3,12 @@ #include -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(userData)->process(static_cast(outputBuffer), nFrames); } diff --git a/src/synth/AudioEngine.h b/src/synth/AudioEngine.h index b1c6b7d..7d00764 100644 --- a/src/synth/AudioEngine.h +++ b/src/synth/AudioEngine.h @@ -5,6 +5,7 @@ #include #include +#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 ¶ms_; } + 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 diff --git a/src/synth/ScopeBuffer.cpp b/src/synth/ScopeBuffer.cpp index e418cdb..4c6682e 100644 --- a/src/synth/ScopeBuffer.cpp +++ b/src/synth/ScopeBuffer.cpp @@ -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& 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(); diff --git a/src/synth/ScopeBuffer.h b/src/synth/ScopeBuffer.h index 571b9f5..9ddae93 100644 --- a/src/synth/ScopeBuffer.h +++ b/src/synth/ScopeBuffer.h @@ -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; + }; diff --git a/src/synth/Synth.cpp b/src/synth/Synth.cpp index 0e10cb6..f21c659 100644 --- a/src/synth/Synth.cpp +++ b/src/synth/Synth.cpp @@ -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(i)); + params_[i].target = paramStore_->get(static_cast(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); + } \ No newline at end of file diff --git a/src/synth/Synth.h b/src/synth/Synth.h index b57f513..cdb4259 100644 --- a/src/synth/Synth.h +++ b/src/synth/Synth.h @@ -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 params_; diff --git a/src/synth/Voice.cpp b/src/synth/Voice.cpp index 11b9ba2..c7a03a1 100644 --- a/src/synth/Voice.cpp +++ b/src/synth/Voice.cpp @@ -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; } \ No newline at end of file diff --git a/src/synth/Voice.h b/src/synth/Voice.h index fa65795..79f12e5 100644 --- a/src/synth/Voice.h +++ b/src/synth/Voice.h @@ -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_; diff --git a/src/synth/WavetableController.cpp b/src/synth/WavetableController.cpp index 791d81d..4fa6c7b 100644 --- a/src/synth/WavetableController.cpp +++ b/src/synth/WavetableController.cpp @@ -9,7 +9,7 @@ WavetableController::WavetableController() { init(); - std::cout << "wavetable init" << std::endl; + //std::cout << "wavetable init" << std::endl; } diff --git a/src/synth/WavetableController.h b/src/synth/WavetableController.h index f03d835..83c7a1c 100644 --- a/src/synth/WavetableController.h +++ b/src/synth/WavetableController.h @@ -3,6 +3,7 @@ #include #include +#include #define SYNTH_WAVETABLE_SIZE 2048 #ifndef M_PI // I hate my stupid chungus life diff --git a/src/ui/MainWindow.cpp b/src/ui/MainWindow.cpp index 185d7c4..c51bdd9 100644 --- a/src/ui/MainWindow.cpp +++ b/src/ui/MainWindow.cpp @@ -8,7 +8,8 @@ MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent), ui_(new Ui::MainWindow), - audio_(new AudioEngine()), + config_(ConfigInterface(¶ms_)), + audio_(new AudioEngine(&config_, ¶ms_)), 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(PARAM_DEFS[static_cast(ParamId::Osc1WaveSelector1)].def)); ui_->comboOsc1WaveSelector2->setCurrentIndex(static_cast(PARAM_DEFS[static_cast(ParamId::Osc1WaveSelector2)].def)); diff --git a/src/ui/MainWindow.h b/src/ui/MainWindow.h index fe17f42..77d2ac1 100644 --- a/src/ui/MainWindow.h +++ b/src/ui/MainWindow.h @@ -4,6 +4,7 @@ #include #include +#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_; diff --git a/src/ui/widgets/EnvelopeGenerator/EnvelopeGenerator.cpp b/src/ui/widgets/EnvelopeGenerator/EnvelopeGenerator.cpp index 21ec020..09e0f2d 100644 --- a/src/ui/widgets/EnvelopeGenerator/EnvelopeGenerator.cpp +++ b/src/ui/widgets/EnvelopeGenerator/EnvelopeGenerator.cpp @@ -4,8 +4,6 @@ #include -// 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(params.r)].def); } + +void EnvelopeGenerator::init(EnvelopeId id, std::array profile) { + + EnvelopeParam params = ENV_PARAMS[static_cast(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); + +} diff --git a/src/ui/widgets/EnvelopeGenerator/EnvelopeGenerator.h b/src/ui/widgets/EnvelopeGenerator/EnvelopeGenerator.h index a412c62..935a76c 100644 --- a/src/ui/widgets/EnvelopeGenerator/EnvelopeGenerator.h +++ b/src/ui/widgets/EnvelopeGenerator/EnvelopeGenerator.h @@ -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 profile); // setters void setDepth(float v); diff --git a/src/ui/widgets/Scope/Scope.cpp b/src/ui/widgets/Scope/Scope.cpp index 1b7e3f7..1769903 100644 --- a/src/ui/widgets/Scope/Scope.cpp +++ b/src/ui/widgets/Scope/Scope.cpp @@ -2,6 +2,7 @@ #include "Scope.h" #include "ui_Scope.h" +// TODO: fix include directories because what is this #include "../../../synth/ScopeBuffer.h" #include @@ -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(); diff --git a/src/ui/widgets/Scope/Scope.h b/src/ui/widgets/Scope/Scope.h index 0689df9..0b6ad7e 100644 --- a/src/ui/widgets/Scope/Scope.h +++ b/src/ui/widgets/Scope/Scope.h @@ -32,4 +32,5 @@ private: ScopeBuffer* buffer_ = nullptr; std::vector samples_; QTimer timer_; + };