From 3ac2cc15b1bc1449d0688f07975c731da7091481 Mon Sep 17 00:00:00 2001 From: Roboboffin Date: Thu, 23 Oct 2025 04:00:21 +0100 Subject: [PATCH] Fix line endings and some cleanup --- .gitattributes | 39 ++ .gitignore | 4 + AGENTS.md | 19 + JuceLibraryCode/JuceHeader.h | 49 +++ JuceLibraryCode/JucePluginDefines.h | 171 ++++++++ JuceLibraryCode/ReadMe.txt | 12 + JuceLibraryCode/include_juce_audio_basics.cpp | 8 + JuceLibraryCode/include_juce_audio_basics.mm | 8 + .../include_juce_audio_devices.cpp | 8 + JuceLibraryCode/include_juce_audio_devices.mm | 8 + .../include_juce_audio_formats.cpp | 8 + JuceLibraryCode/include_juce_audio_formats.mm | 8 + .../include_juce_audio_plugin_client_AAX.cpp | 8 + .../include_juce_audio_plugin_client_AAX.mm | 8 + ...ude_juce_audio_plugin_client_AAX_utils.cpp | 8 + .../include_juce_audio_plugin_client_ARA.cpp | 8 + .../include_juce_audio_plugin_client_AU_1.mm | 8 + .../include_juce_audio_plugin_client_AU_2.mm | 8 + .../include_juce_audio_plugin_client_AUv3.mm | 8 + .../include_juce_audio_plugin_client_LV2.cpp | 8 + .../include_juce_audio_plugin_client_LV2.mm | 8 + ...de_juce_audio_plugin_client_Standalone.cpp | 8 + ...include_juce_audio_plugin_client_Unity.cpp | 8 + .../include_juce_audio_plugin_client_VST2.cpp | 8 + .../include_juce_audio_plugin_client_VST2.mm | 8 + .../include_juce_audio_plugin_client_VST3.cpp | 8 + .../include_juce_audio_plugin_client_VST3.mm | 8 + .../include_juce_audio_processors.cpp | 8 + .../include_juce_audio_processors.mm | 8 + .../include_juce_audio_processors_ara.cpp | 8 + ...include_juce_audio_processors_lv2_libs.cpp | 8 + JuceLibraryCode/include_juce_audio_utils.cpp | 8 + JuceLibraryCode/include_juce_audio_utils.mm | 8 + JuceLibraryCode/include_juce_core.cpp | 8 + JuceLibraryCode/include_juce_core.mm | 8 + .../include_juce_core_CompilationTime.cpp | 8 + .../include_juce_data_structures.cpp | 8 + .../include_juce_data_structures.mm | 8 + JuceLibraryCode/include_juce_dsp.cpp | 8 + JuceLibraryCode/include_juce_dsp.mm | 8 + JuceLibraryCode/include_juce_events.cpp | 8 + JuceLibraryCode/include_juce_events.mm | 8 + JuceLibraryCode/include_juce_graphics.cpp | 8 + JuceLibraryCode/include_juce_graphics.mm | 8 + .../include_juce_graphics_Harfbuzz.cpp | 8 + .../include_juce_graphics_Sheenbidi.c | 8 + JuceLibraryCode/include_juce_gui_basics.cpp | 8 + JuceLibraryCode/include_juce_gui_basics.mm | 8 + JuceLibraryCode/include_juce_gui_extra.cpp | 8 + JuceLibraryCode/include_juce_gui_extra.mm | 8 + NeuralSynth.iss | 98 +++++ NeuralSynth.jucer | 74 ++++ Source/AudioBufferQueue.h | 49 +++ Source/AudioEngine.h | 47 +++ Source/BlepOsc.h | 80 ++++ Source/GraphComponent.h | 99 +++++ Source/NeuralSharedParams.h | 145 +++++++ Source/PluginEditor.cpp | 170 ++++++++ Source/PluginEditor.h | 341 +++++++++++++++ Source/PluginProcessor.cpp | 270 ++++++++++++ Source/PluginProcessor.h | 90 ++++ Source/ScopeComponent.h | 102 +++++ Source/ScopeDataCollector.h | 62 +++ Source/SynthVoice.cpp | 398 ++++++++++++++++++ Source/SynthVoice.h | 97 +++++ Source/WavetableOsc.h | 261 ++++++++++++ 66 files changed, 3029 insertions(+) create mode 100644 .gitattributes create mode 100644 .gitignore create mode 100644 AGENTS.md create mode 100644 JuceLibraryCode/JuceHeader.h create mode 100644 JuceLibraryCode/JucePluginDefines.h create mode 100644 JuceLibraryCode/ReadMe.txt create mode 100644 JuceLibraryCode/include_juce_audio_basics.cpp create mode 100644 JuceLibraryCode/include_juce_audio_basics.mm create mode 100644 JuceLibraryCode/include_juce_audio_devices.cpp create mode 100644 JuceLibraryCode/include_juce_audio_devices.mm create mode 100644 JuceLibraryCode/include_juce_audio_formats.cpp create mode 100644 JuceLibraryCode/include_juce_audio_formats.mm create mode 100644 JuceLibraryCode/include_juce_audio_plugin_client_AAX.cpp create mode 100644 JuceLibraryCode/include_juce_audio_plugin_client_AAX.mm create mode 100644 JuceLibraryCode/include_juce_audio_plugin_client_AAX_utils.cpp create mode 100644 JuceLibraryCode/include_juce_audio_plugin_client_ARA.cpp create mode 100644 JuceLibraryCode/include_juce_audio_plugin_client_AU_1.mm create mode 100644 JuceLibraryCode/include_juce_audio_plugin_client_AU_2.mm create mode 100644 JuceLibraryCode/include_juce_audio_plugin_client_AUv3.mm create mode 100644 JuceLibraryCode/include_juce_audio_plugin_client_LV2.cpp create mode 100644 JuceLibraryCode/include_juce_audio_plugin_client_LV2.mm create mode 100644 JuceLibraryCode/include_juce_audio_plugin_client_Standalone.cpp create mode 100644 JuceLibraryCode/include_juce_audio_plugin_client_Unity.cpp create mode 100644 JuceLibraryCode/include_juce_audio_plugin_client_VST2.cpp create mode 100644 JuceLibraryCode/include_juce_audio_plugin_client_VST2.mm create mode 100644 JuceLibraryCode/include_juce_audio_plugin_client_VST3.cpp create mode 100644 JuceLibraryCode/include_juce_audio_plugin_client_VST3.mm create mode 100644 JuceLibraryCode/include_juce_audio_processors.cpp create mode 100644 JuceLibraryCode/include_juce_audio_processors.mm create mode 100644 JuceLibraryCode/include_juce_audio_processors_ara.cpp create mode 100644 JuceLibraryCode/include_juce_audio_processors_lv2_libs.cpp create mode 100644 JuceLibraryCode/include_juce_audio_utils.cpp create mode 100644 JuceLibraryCode/include_juce_audio_utils.mm create mode 100644 JuceLibraryCode/include_juce_core.cpp create mode 100644 JuceLibraryCode/include_juce_core.mm create mode 100644 JuceLibraryCode/include_juce_core_CompilationTime.cpp create mode 100644 JuceLibraryCode/include_juce_data_structures.cpp create mode 100644 JuceLibraryCode/include_juce_data_structures.mm create mode 100644 JuceLibraryCode/include_juce_dsp.cpp create mode 100644 JuceLibraryCode/include_juce_dsp.mm create mode 100644 JuceLibraryCode/include_juce_events.cpp create mode 100644 JuceLibraryCode/include_juce_events.mm create mode 100644 JuceLibraryCode/include_juce_graphics.cpp create mode 100644 JuceLibraryCode/include_juce_graphics.mm create mode 100644 JuceLibraryCode/include_juce_graphics_Harfbuzz.cpp create mode 100644 JuceLibraryCode/include_juce_graphics_Sheenbidi.c create mode 100644 JuceLibraryCode/include_juce_gui_basics.cpp create mode 100644 JuceLibraryCode/include_juce_gui_basics.mm create mode 100644 JuceLibraryCode/include_juce_gui_extra.cpp create mode 100644 JuceLibraryCode/include_juce_gui_extra.mm create mode 100644 NeuralSynth.iss create mode 100644 NeuralSynth.jucer create mode 100644 Source/AudioBufferQueue.h create mode 100644 Source/AudioEngine.h create mode 100644 Source/BlepOsc.h create mode 100644 Source/GraphComponent.h create mode 100644 Source/NeuralSharedParams.h create mode 100644 Source/PluginEditor.cpp create mode 100644 Source/PluginEditor.h create mode 100644 Source/PluginProcessor.cpp create mode 100644 Source/PluginProcessor.h create mode 100644 Source/ScopeComponent.h create mode 100644 Source/ScopeDataCollector.h create mode 100644 Source/SynthVoice.cpp create mode 100644 Source/SynthVoice.h create mode 100644 Source/WavetableOsc.h diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..6109090 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,39 @@ +# Auto-detect text files and normalize to LF in the repo +* text=auto + +# Explicit text rules +*.md text +*.txt text +*.json text +*.yml text +*.yaml text +*.toml text +*.c text +*.cpp text +*.h text +*.hpp text +*.rs text +*.py text +*.go text +*.js text +*.ts text +*.css text +*.html text +*.sh text eol=lf # scripts on Unix must be LF +*.bash text eol=lf + +# Windows-native scripts that should remain CRLF in working trees +*.bat text eol=crlf +*.cmd text eol=crlf +*.ps1 text eol=crlf + +# Binary (never touch line endings) +*.png binary +*.jpg binary +*.jpeg binary +*.gif binary +*.pdf binary +*.zip binary +*.exe binary +*.dll binary +*.glb binary diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..1922053 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +Builds +NeuralSynth Installer.exe +AudioPluginHost +Projucer diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..a0af7d3 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,19 @@ +# Repository Guidelines + +## Project Structure & Module Organization +NeuralSynth is a JUCE-based synthesizer plugin. Core runtime code lives in `Source/`, with `PluginProcessor.*` orchestrating audio/MIDI flow, `PluginEditor.*` driving the UI, `SynthVoice.*` implementing per-voice DSP, and helper headers such as `AudioEngine.h` and `NeuralSharedParams.h` exposing shared state. JUCE-generated scaffolding sits in `JuceLibraryCode/`; regenerate it through `NeuralSynth.jucer` rather than editing by hand. Platform build assets are under `Builds/` (for example `Builds/LinuxMakefile/`), and finished binaries default to `Builds/LinuxMakefile/build/`. Install scripting for Windows lives in `NeuralSynth.iss`. + +## Build, Test, and Development Commands +- `cd Builds/LinuxMakefile && make CONFIG=Debug` compiles the standalone app and VST3 with debug symbols. +- `cd Builds/LinuxMakefile && make CONFIG=Release` builds optimized artefacts for distribution. +- `cd Builds/LinuxMakefile && make clean` removes intermediate objects when builds misbehave. +- `./Builds/LinuxMakefile/build/NeuralSynth` launches the standalone target; VST3 binaries appear in `Builds/LinuxMakefile/NeuralSynth.vst3` and copy into `~/.vst3` when the Makefile post-build step runs. + +## Coding Style & Naming Conventions +C++ sources use 4-space indentation, brace-on-new-line functions, and JUCE’s `juce::` namespace types. Prefer `PascalCase` for classes (e.g., `NeuralSynthAudioProcessor`), camelCase for methods and members (`prepareToPlay`, `audioEngine`), and suffix queues/collectors clearly (`AudioBufferQueue`, `ScopeDataCollector`). Match existing lambda formatting in `SynthVoice.cpp`, keep includes sorted locally, and avoid editing generated files under `JuceLibraryCode/`. + +## Testing Guidelines +Automated tests are not yet configured; rely on manual validation in the standalone app or a DAW host. After each change, rebuild and audition key features: oscillator switching, chorus/delay/reverb chains, parameter automation, and MIDI input. For DSP tweaks, monitor the oscilloscope components linked to the buffer queues to confirm signal stability. Document ad-hoc test coverage in your pull request until formal tests are added. + +## Commit & Pull Request Guidelines +Follow the existing concise, imperative commit style (`Add chorus modulation`, `Fix voice detune`). Scope each commit to a logical change and format messages as a single summary line. Pull requests should describe the motivation, outline testing performed, and link issues when relevant. Include platform notes (Linux, Windows installer) and screenshots or audio clips for UI-affecting or sonic changes so reviewers can assess impact quickly. diff --git a/JuceLibraryCode/JuceHeader.h b/JuceLibraryCode/JuceHeader.h new file mode 100644 index 0000000..9eccfa7 --- /dev/null +++ b/JuceLibraryCode/JuceHeader.h @@ -0,0 +1,49 @@ +/* + + IMPORTANT! This file is auto-generated each time you save your + project - if you alter its contents, your changes may be overwritten! + + This is the header file that your files should include in order to get all the + JUCE library headers. You should avoid including the JUCE headers directly in + your own source files, because that wouldn't pick up the correct configuration + options for your app. + +*/ + +#pragma once + + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + + +#if defined (JUCE_PROJUCER_VERSION) && JUCE_PROJUCER_VERSION < JUCE_VERSION + /** If you've hit this error then the version of the Projucer that was used to generate this project is + older than the version of the JUCE modules being included. To fix this error, re-save your project + using the latest version of the Projucer or, if you aren't using the Projucer to manage your project, + remove the JUCE_PROJUCER_VERSION define. + */ + #error "This project was last saved using an outdated version of the Projucer! Re-save this project with the latest version to fix this error." +#endif + + +#if ! JUCE_DONT_DECLARE_PROJECTINFO +namespace ProjectInfo +{ + const char* const projectName = "NeuralSynth"; + const char* const companyName = "Samedi Dimanche"; + const char* const versionString = "0.0.1"; + const int versionNumber = 0x1; +} +#endif diff --git a/JuceLibraryCode/JucePluginDefines.h b/JuceLibraryCode/JucePluginDefines.h new file mode 100644 index 0000000..6c0fa21 --- /dev/null +++ b/JuceLibraryCode/JucePluginDefines.h @@ -0,0 +1,171 @@ +/* + + IMPORTANT! This file is auto-generated each time you save your + project - if you alter its contents, your changes may be overwritten! + +*/ + +#pragma once + +//============================================================================== +// Audio plugin settings.. + +#ifndef JucePlugin_Build_VST + #define JucePlugin_Build_VST 0 +#endif +#ifndef JucePlugin_Build_VST3 + #define JucePlugin_Build_VST3 1 +#endif +#ifndef JucePlugin_Build_AU + #define JucePlugin_Build_AU 1 +#endif +#ifndef JucePlugin_Build_AUv3 + #define JucePlugin_Build_AUv3 0 +#endif +#ifndef JucePlugin_Build_AAX + #define JucePlugin_Build_AAX 0 +#endif +#ifndef JucePlugin_Build_Standalone + #define JucePlugin_Build_Standalone 1 +#endif +#ifndef JucePlugin_Build_Unity + #define JucePlugin_Build_Unity 0 +#endif +#ifndef JucePlugin_Build_LV2 + #define JucePlugin_Build_LV2 0 +#endif +#ifndef JucePlugin_Enable_IAA + #define JucePlugin_Enable_IAA 0 +#endif +#ifndef JucePlugin_Enable_ARA + #define JucePlugin_Enable_ARA 0 +#endif +#ifndef JucePlugin_Name + #define JucePlugin_Name "NeuralSynth" +#endif +#ifndef JucePlugin_Desc + #define JucePlugin_Desc "NeuralSynth" +#endif +#ifndef JucePlugin_Manufacturer + #define JucePlugin_Manufacturer "Samedi Dimanche" +#endif +#ifndef JucePlugin_ManufacturerWebsite + #define JucePlugin_ManufacturerWebsite "www.samedidimanche.com" +#endif +#ifndef JucePlugin_ManufacturerEmail + #define JucePlugin_ManufacturerEmail "" +#endif +#ifndef JucePlugin_ManufacturerCode + #define JucePlugin_ManufacturerCode 0x4d616e75 +#endif +#ifndef JucePlugin_PluginCode + #define JucePlugin_PluginCode 0x4d73347a +#endif +#ifndef JucePlugin_IsSynth + #define JucePlugin_IsSynth 1 +#endif +#ifndef JucePlugin_WantsMidiInput + #define JucePlugin_WantsMidiInput 1 +#endif +#ifndef JucePlugin_ProducesMidiOutput + #define JucePlugin_ProducesMidiOutput 0 +#endif +#ifndef JucePlugin_IsMidiEffect + #define JucePlugin_IsMidiEffect 0 +#endif +#ifndef JucePlugin_EditorRequiresKeyboardFocus + #define JucePlugin_EditorRequiresKeyboardFocus 0 +#endif +#ifndef JucePlugin_Version + #define JucePlugin_Version 0.0.1 +#endif +#ifndef JucePlugin_VersionCode + #define JucePlugin_VersionCode 0x1 +#endif +#ifndef JucePlugin_VersionString + #define JucePlugin_VersionString "0.0.1" +#endif +#ifndef JucePlugin_VSTUniqueID + #define JucePlugin_VSTUniqueID JucePlugin_PluginCode +#endif +#ifndef JucePlugin_VSTCategory + #define JucePlugin_VSTCategory kPlugCategSynth +#endif +#ifndef JucePlugin_Vst3Category + #define JucePlugin_Vst3Category "Instrument|Synth" +#endif +#ifndef JucePlugin_AUMainType + #define JucePlugin_AUMainType 'aumu' +#endif +#ifndef JucePlugin_AUSubType + #define JucePlugin_AUSubType JucePlugin_PluginCode +#endif +#ifndef JucePlugin_AUExportPrefix + #define JucePlugin_AUExportPrefix NeuralSynthAU +#endif +#ifndef JucePlugin_AUExportPrefixQuoted + #define JucePlugin_AUExportPrefixQuoted "NeuralSynthAU" +#endif +#ifndef JucePlugin_AUManufacturerCode + #define JucePlugin_AUManufacturerCode JucePlugin_ManufacturerCode +#endif +#ifndef JucePlugin_CFBundleIdentifier + #define JucePlugin_CFBundleIdentifier com.samedidimanche.NeuralSynth +#endif +#ifndef JucePlugin_AAXIdentifier + #define JucePlugin_AAXIdentifier com.SamediDimanche.NeuralSynth +#endif +#ifndef JucePlugin_AAXManufacturerCode + #define JucePlugin_AAXManufacturerCode JucePlugin_ManufacturerCode +#endif +#ifndef JucePlugin_AAXProductId + #define JucePlugin_AAXProductId JucePlugin_PluginCode +#endif +#ifndef JucePlugin_AAXCategory + #define JucePlugin_AAXCategory 2048 +#endif +#ifndef JucePlugin_AAXDisableBypass + #define JucePlugin_AAXDisableBypass 0 +#endif +#ifndef JucePlugin_AAXDisableMultiMono + #define JucePlugin_AAXDisableMultiMono 0 +#endif +#ifndef JucePlugin_IAAType + #define JucePlugin_IAAType 0x61757269 +#endif +#ifndef JucePlugin_IAASubType + #define JucePlugin_IAASubType JucePlugin_PluginCode +#endif +#ifndef JucePlugin_IAAName + #define JucePlugin_IAAName "Samedi Dimanche: NeuralSynth" +#endif +#ifndef JucePlugin_VSTNumMidiInputs + #define JucePlugin_VSTNumMidiInputs 1 +#endif +#ifndef JucePlugin_VSTNumMidiOutputs + #define JucePlugin_VSTNumMidiOutputs 16 +#endif +#ifndef JucePlugin_ARAContentTypes + #define JucePlugin_ARAContentTypes 0 +#endif +#ifndef JucePlugin_ARATransformationFlags + #define JucePlugin_ARATransformationFlags 0 +#endif +#ifndef JucePlugin_ARAFactoryID + #define JucePlugin_ARAFactoryID "com.SamediDimanche.NeuralSynth.factory" +#endif +#ifndef JucePlugin_ARADocumentArchiveID + #define JucePlugin_ARADocumentArchiveID "com.SamediDimanche.NeuralSynth.aradocumentarchive.0.0.1" +#endif +#ifndef JucePlugin_ARACompatibleArchiveIDs + #define JucePlugin_ARACompatibleArchiveIDs "" +#endif +#ifndef JucePlugin_MaxNumInputChannels + #define JucePlugin_MaxNumInputChannels 0 +#endif +#ifndef JucePlugin_MaxNumOutputChannels + #define JucePlugin_MaxNumOutputChannels 2 +#endif +#ifndef JucePlugin_PreferredChannelConfigurations + #define JucePlugin_PreferredChannelConfigurations {0, 2} +#endif diff --git a/JuceLibraryCode/ReadMe.txt b/JuceLibraryCode/ReadMe.txt new file mode 100644 index 0000000..1e6784f --- /dev/null +++ b/JuceLibraryCode/ReadMe.txt @@ -0,0 +1,12 @@ + + Important Note!! + ================ + +The purpose of this folder is to contain files that are auto-generated by the Projucer, +and ALL files in this folder will be mercilessly DELETED and completely re-written whenever +the Projucer saves your project. + +Therefore, it's a bad idea to make any manual changes to the files in here, or to +put any of your own files in here if you don't want to lose them. (Of course you may choose +to add the folder's contents to your version-control system so that you can re-merge your own +modifications after the Projucer has saved its changes). diff --git a/JuceLibraryCode/include_juce_audio_basics.cpp b/JuceLibraryCode/include_juce_audio_basics.cpp new file mode 100644 index 0000000..4070844 --- /dev/null +++ b/JuceLibraryCode/include_juce_audio_basics.cpp @@ -0,0 +1,8 @@ +/* + + IMPORTANT! This file is auto-generated each time you save your + project - if you alter its contents, your changes may be overwritten! + +*/ + +#include diff --git a/JuceLibraryCode/include_juce_audio_basics.mm b/JuceLibraryCode/include_juce_audio_basics.mm new file mode 100644 index 0000000..0c09914 --- /dev/null +++ b/JuceLibraryCode/include_juce_audio_basics.mm @@ -0,0 +1,8 @@ +/* + + IMPORTANT! This file is auto-generated each time you save your + project - if you alter its contents, your changes may be overwritten! + +*/ + +#include diff --git a/JuceLibraryCode/include_juce_audio_devices.cpp b/JuceLibraryCode/include_juce_audio_devices.cpp new file mode 100644 index 0000000..c9c2d11 --- /dev/null +++ b/JuceLibraryCode/include_juce_audio_devices.cpp @@ -0,0 +1,8 @@ +/* + + IMPORTANT! This file is auto-generated each time you save your + project - if you alter its contents, your changes may be overwritten! + +*/ + +#include diff --git a/JuceLibraryCode/include_juce_audio_devices.mm b/JuceLibraryCode/include_juce_audio_devices.mm new file mode 100644 index 0000000..77e69b1 --- /dev/null +++ b/JuceLibraryCode/include_juce_audio_devices.mm @@ -0,0 +1,8 @@ +/* + + IMPORTANT! This file is auto-generated each time you save your + project - if you alter its contents, your changes may be overwritten! + +*/ + +#include diff --git a/JuceLibraryCode/include_juce_audio_formats.cpp b/JuceLibraryCode/include_juce_audio_formats.cpp new file mode 100644 index 0000000..78e74f7 --- /dev/null +++ b/JuceLibraryCode/include_juce_audio_formats.cpp @@ -0,0 +1,8 @@ +/* + + IMPORTANT! This file is auto-generated each time you save your + project - if you alter its contents, your changes may be overwritten! + +*/ + +#include diff --git a/JuceLibraryCode/include_juce_audio_formats.mm b/JuceLibraryCode/include_juce_audio_formats.mm new file mode 100644 index 0000000..0adf319 --- /dev/null +++ b/JuceLibraryCode/include_juce_audio_formats.mm @@ -0,0 +1,8 @@ +/* + + IMPORTANT! This file is auto-generated each time you save your + project - if you alter its contents, your changes may be overwritten! + +*/ + +#include diff --git a/JuceLibraryCode/include_juce_audio_plugin_client_AAX.cpp b/JuceLibraryCode/include_juce_audio_plugin_client_AAX.cpp new file mode 100644 index 0000000..a20d1e3 --- /dev/null +++ b/JuceLibraryCode/include_juce_audio_plugin_client_AAX.cpp @@ -0,0 +1,8 @@ +/* + + IMPORTANT! This file is auto-generated each time you save your + project - if you alter its contents, your changes may be overwritten! + +*/ + +#include diff --git a/JuceLibraryCode/include_juce_audio_plugin_client_AAX.mm b/JuceLibraryCode/include_juce_audio_plugin_client_AAX.mm new file mode 100644 index 0000000..0d740d8 --- /dev/null +++ b/JuceLibraryCode/include_juce_audio_plugin_client_AAX.mm @@ -0,0 +1,8 @@ +/* + + IMPORTANT! This file is auto-generated each time you save your + project - if you alter its contents, your changes may be overwritten! + +*/ + +#include diff --git a/JuceLibraryCode/include_juce_audio_plugin_client_AAX_utils.cpp b/JuceLibraryCode/include_juce_audio_plugin_client_AAX_utils.cpp new file mode 100644 index 0000000..5463093 --- /dev/null +++ b/JuceLibraryCode/include_juce_audio_plugin_client_AAX_utils.cpp @@ -0,0 +1,8 @@ +/* + + IMPORTANT! This file is auto-generated each time you save your + project - if you alter its contents, your changes may be overwritten! + +*/ + +#include diff --git a/JuceLibraryCode/include_juce_audio_plugin_client_ARA.cpp b/JuceLibraryCode/include_juce_audio_plugin_client_ARA.cpp new file mode 100644 index 0000000..d64efb0 --- /dev/null +++ b/JuceLibraryCode/include_juce_audio_plugin_client_ARA.cpp @@ -0,0 +1,8 @@ +/* + + IMPORTANT! This file is auto-generated each time you save your + project - if you alter its contents, your changes may be overwritten! + +*/ + +#include diff --git a/JuceLibraryCode/include_juce_audio_plugin_client_AU_1.mm b/JuceLibraryCode/include_juce_audio_plugin_client_AU_1.mm new file mode 100644 index 0000000..0924b03 --- /dev/null +++ b/JuceLibraryCode/include_juce_audio_plugin_client_AU_1.mm @@ -0,0 +1,8 @@ +/* + + IMPORTANT! This file is auto-generated each time you save your + project - if you alter its contents, your changes may be overwritten! + +*/ + +#include diff --git a/JuceLibraryCode/include_juce_audio_plugin_client_AU_2.mm b/JuceLibraryCode/include_juce_audio_plugin_client_AU_2.mm new file mode 100644 index 0000000..402f054 --- /dev/null +++ b/JuceLibraryCode/include_juce_audio_plugin_client_AU_2.mm @@ -0,0 +1,8 @@ +/* + + IMPORTANT! This file is auto-generated each time you save your + project - if you alter its contents, your changes may be overwritten! + +*/ + +#include diff --git a/JuceLibraryCode/include_juce_audio_plugin_client_AUv3.mm b/JuceLibraryCode/include_juce_audio_plugin_client_AUv3.mm new file mode 100644 index 0000000..5c705a2 --- /dev/null +++ b/JuceLibraryCode/include_juce_audio_plugin_client_AUv3.mm @@ -0,0 +1,8 @@ +/* + + IMPORTANT! This file is auto-generated each time you save your + project - if you alter its contents, your changes may be overwritten! + +*/ + +#include diff --git a/JuceLibraryCode/include_juce_audio_plugin_client_LV2.cpp b/JuceLibraryCode/include_juce_audio_plugin_client_LV2.cpp new file mode 100644 index 0000000..dd2858f --- /dev/null +++ b/JuceLibraryCode/include_juce_audio_plugin_client_LV2.cpp @@ -0,0 +1,8 @@ +/* + + IMPORTANT! This file is auto-generated each time you save your + project - if you alter its contents, your changes may be overwritten! + +*/ + +#include diff --git a/JuceLibraryCode/include_juce_audio_plugin_client_LV2.mm b/JuceLibraryCode/include_juce_audio_plugin_client_LV2.mm new file mode 100644 index 0000000..ef6fab7 --- /dev/null +++ b/JuceLibraryCode/include_juce_audio_plugin_client_LV2.mm @@ -0,0 +1,8 @@ +/* + + IMPORTANT! This file is auto-generated each time you save your + project - if you alter its contents, your changes may be overwritten! + +*/ + +#include diff --git a/JuceLibraryCode/include_juce_audio_plugin_client_Standalone.cpp b/JuceLibraryCode/include_juce_audio_plugin_client_Standalone.cpp new file mode 100644 index 0000000..198ae8c --- /dev/null +++ b/JuceLibraryCode/include_juce_audio_plugin_client_Standalone.cpp @@ -0,0 +1,8 @@ +/* + + IMPORTANT! This file is auto-generated each time you save your + project - if you alter its contents, your changes may be overwritten! + +*/ + +#include diff --git a/JuceLibraryCode/include_juce_audio_plugin_client_Unity.cpp b/JuceLibraryCode/include_juce_audio_plugin_client_Unity.cpp new file mode 100644 index 0000000..acf6830 --- /dev/null +++ b/JuceLibraryCode/include_juce_audio_plugin_client_Unity.cpp @@ -0,0 +1,8 @@ +/* + + IMPORTANT! This file is auto-generated each time you save your + project - if you alter its contents, your changes may be overwritten! + +*/ + +#include diff --git a/JuceLibraryCode/include_juce_audio_plugin_client_VST2.cpp b/JuceLibraryCode/include_juce_audio_plugin_client_VST2.cpp new file mode 100644 index 0000000..b019de9 --- /dev/null +++ b/JuceLibraryCode/include_juce_audio_plugin_client_VST2.cpp @@ -0,0 +1,8 @@ +/* + + IMPORTANT! This file is auto-generated each time you save your + project - if you alter its contents, your changes may be overwritten! + +*/ + +#include diff --git a/JuceLibraryCode/include_juce_audio_plugin_client_VST2.mm b/JuceLibraryCode/include_juce_audio_plugin_client_VST2.mm new file mode 100644 index 0000000..5923412 --- /dev/null +++ b/JuceLibraryCode/include_juce_audio_plugin_client_VST2.mm @@ -0,0 +1,8 @@ +/* + + IMPORTANT! This file is auto-generated each time you save your + project - if you alter its contents, your changes may be overwritten! + +*/ + +#include diff --git a/JuceLibraryCode/include_juce_audio_plugin_client_VST3.cpp b/JuceLibraryCode/include_juce_audio_plugin_client_VST3.cpp new file mode 100644 index 0000000..ac79442 --- /dev/null +++ b/JuceLibraryCode/include_juce_audio_plugin_client_VST3.cpp @@ -0,0 +1,8 @@ +/* + + IMPORTANT! This file is auto-generated each time you save your + project - if you alter its contents, your changes may be overwritten! + +*/ + +#include diff --git a/JuceLibraryCode/include_juce_audio_plugin_client_VST3.mm b/JuceLibraryCode/include_juce_audio_plugin_client_VST3.mm new file mode 100644 index 0000000..f6f7943 --- /dev/null +++ b/JuceLibraryCode/include_juce_audio_plugin_client_VST3.mm @@ -0,0 +1,8 @@ +/* + + IMPORTANT! This file is auto-generated each time you save your + project - if you alter its contents, your changes may be overwritten! + +*/ + +#include diff --git a/JuceLibraryCode/include_juce_audio_processors.cpp b/JuceLibraryCode/include_juce_audio_processors.cpp new file mode 100644 index 0000000..0dbc0b6 --- /dev/null +++ b/JuceLibraryCode/include_juce_audio_processors.cpp @@ -0,0 +1,8 @@ +/* + + IMPORTANT! This file is auto-generated each time you save your + project - if you alter its contents, your changes may be overwritten! + +*/ + +#include diff --git a/JuceLibraryCode/include_juce_audio_processors.mm b/JuceLibraryCode/include_juce_audio_processors.mm new file mode 100644 index 0000000..dac7f37 --- /dev/null +++ b/JuceLibraryCode/include_juce_audio_processors.mm @@ -0,0 +1,8 @@ +/* + + IMPORTANT! This file is auto-generated each time you save your + project - if you alter its contents, your changes may be overwritten! + +*/ + +#include diff --git a/JuceLibraryCode/include_juce_audio_processors_ara.cpp b/JuceLibraryCode/include_juce_audio_processors_ara.cpp new file mode 100644 index 0000000..1651fc5 --- /dev/null +++ b/JuceLibraryCode/include_juce_audio_processors_ara.cpp @@ -0,0 +1,8 @@ +/* + + IMPORTANT! This file is auto-generated each time you save your + project - if you alter its contents, your changes may be overwritten! + +*/ + +#include diff --git a/JuceLibraryCode/include_juce_audio_processors_lv2_libs.cpp b/JuceLibraryCode/include_juce_audio_processors_lv2_libs.cpp new file mode 100644 index 0000000..1151b5a --- /dev/null +++ b/JuceLibraryCode/include_juce_audio_processors_lv2_libs.cpp @@ -0,0 +1,8 @@ +/* + + IMPORTANT! This file is auto-generated each time you save your + project - if you alter its contents, your changes may be overwritten! + +*/ + +#include diff --git a/JuceLibraryCode/include_juce_audio_utils.cpp b/JuceLibraryCode/include_juce_audio_utils.cpp new file mode 100644 index 0000000..f31e8b6 --- /dev/null +++ b/JuceLibraryCode/include_juce_audio_utils.cpp @@ -0,0 +1,8 @@ +/* + + IMPORTANT! This file is auto-generated each time you save your + project - if you alter its contents, your changes may be overwritten! + +*/ + +#include diff --git a/JuceLibraryCode/include_juce_audio_utils.mm b/JuceLibraryCode/include_juce_audio_utils.mm new file mode 100644 index 0000000..4dfd5b4 --- /dev/null +++ b/JuceLibraryCode/include_juce_audio_utils.mm @@ -0,0 +1,8 @@ +/* + + IMPORTANT! This file is auto-generated each time you save your + project - if you alter its contents, your changes may be overwritten! + +*/ + +#include diff --git a/JuceLibraryCode/include_juce_core.cpp b/JuceLibraryCode/include_juce_core.cpp new file mode 100644 index 0000000..6f55178 --- /dev/null +++ b/JuceLibraryCode/include_juce_core.cpp @@ -0,0 +1,8 @@ +/* + + IMPORTANT! This file is auto-generated each time you save your + project - if you alter its contents, your changes may be overwritten! + +*/ + +#include diff --git a/JuceLibraryCode/include_juce_core.mm b/JuceLibraryCode/include_juce_core.mm new file mode 100644 index 0000000..db83b69 --- /dev/null +++ b/JuceLibraryCode/include_juce_core.mm @@ -0,0 +1,8 @@ +/* + + IMPORTANT! This file is auto-generated each time you save your + project - if you alter its contents, your changes may be overwritten! + +*/ + +#include diff --git a/JuceLibraryCode/include_juce_core_CompilationTime.cpp b/JuceLibraryCode/include_juce_core_CompilationTime.cpp new file mode 100644 index 0000000..789042d --- /dev/null +++ b/JuceLibraryCode/include_juce_core_CompilationTime.cpp @@ -0,0 +1,8 @@ +/* + + IMPORTANT! This file is auto-generated each time you save your + project - if you alter its contents, your changes may be overwritten! + +*/ + +#include diff --git a/JuceLibraryCode/include_juce_data_structures.cpp b/JuceLibraryCode/include_juce_data_structures.cpp new file mode 100644 index 0000000..f53f241 --- /dev/null +++ b/JuceLibraryCode/include_juce_data_structures.cpp @@ -0,0 +1,8 @@ +/* + + IMPORTANT! This file is auto-generated each time you save your + project - if you alter its contents, your changes may be overwritten! + +*/ + +#include diff --git a/JuceLibraryCode/include_juce_data_structures.mm b/JuceLibraryCode/include_juce_data_structures.mm new file mode 100644 index 0000000..db212c9 --- /dev/null +++ b/JuceLibraryCode/include_juce_data_structures.mm @@ -0,0 +1,8 @@ +/* + + IMPORTANT! This file is auto-generated each time you save your + project - if you alter its contents, your changes may be overwritten! + +*/ + +#include diff --git a/JuceLibraryCode/include_juce_dsp.cpp b/JuceLibraryCode/include_juce_dsp.cpp new file mode 100644 index 0000000..ec2c898 --- /dev/null +++ b/JuceLibraryCode/include_juce_dsp.cpp @@ -0,0 +1,8 @@ +/* + + IMPORTANT! This file is auto-generated each time you save your + project - if you alter its contents, your changes may be overwritten! + +*/ + +#include diff --git a/JuceLibraryCode/include_juce_dsp.mm b/JuceLibraryCode/include_juce_dsp.mm new file mode 100644 index 0000000..e641589 --- /dev/null +++ b/JuceLibraryCode/include_juce_dsp.mm @@ -0,0 +1,8 @@ +/* + + IMPORTANT! This file is auto-generated each time you save your + project - if you alter its contents, your changes may be overwritten! + +*/ + +#include diff --git a/JuceLibraryCode/include_juce_events.cpp b/JuceLibraryCode/include_juce_events.cpp new file mode 100644 index 0000000..33a3a69 --- /dev/null +++ b/JuceLibraryCode/include_juce_events.cpp @@ -0,0 +1,8 @@ +/* + + IMPORTANT! This file is auto-generated each time you save your + project - if you alter its contents, your changes may be overwritten! + +*/ + +#include diff --git a/JuceLibraryCode/include_juce_events.mm b/JuceLibraryCode/include_juce_events.mm new file mode 100644 index 0000000..6ad0eda --- /dev/null +++ b/JuceLibraryCode/include_juce_events.mm @@ -0,0 +1,8 @@ +/* + + IMPORTANT! This file is auto-generated each time you save your + project - if you alter its contents, your changes may be overwritten! + +*/ + +#include diff --git a/JuceLibraryCode/include_juce_graphics.cpp b/JuceLibraryCode/include_juce_graphics.cpp new file mode 100644 index 0000000..12f6750 --- /dev/null +++ b/JuceLibraryCode/include_juce_graphics.cpp @@ -0,0 +1,8 @@ +/* + + IMPORTANT! This file is auto-generated each time you save your + project - if you alter its contents, your changes may be overwritten! + +*/ + +#include diff --git a/JuceLibraryCode/include_juce_graphics.mm b/JuceLibraryCode/include_juce_graphics.mm new file mode 100644 index 0000000..ab22eb4 --- /dev/null +++ b/JuceLibraryCode/include_juce_graphics.mm @@ -0,0 +1,8 @@ +/* + + IMPORTANT! This file is auto-generated each time you save your + project - if you alter its contents, your changes may be overwritten! + +*/ + +#include diff --git a/JuceLibraryCode/include_juce_graphics_Harfbuzz.cpp b/JuceLibraryCode/include_juce_graphics_Harfbuzz.cpp new file mode 100644 index 0000000..419cf23 --- /dev/null +++ b/JuceLibraryCode/include_juce_graphics_Harfbuzz.cpp @@ -0,0 +1,8 @@ +/* + + IMPORTANT! This file is auto-generated each time you save your + project - if you alter its contents, your changes may be overwritten! + +*/ + +#include diff --git a/JuceLibraryCode/include_juce_graphics_Sheenbidi.c b/JuceLibraryCode/include_juce_graphics_Sheenbidi.c new file mode 100644 index 0000000..df5eb4b --- /dev/null +++ b/JuceLibraryCode/include_juce_graphics_Sheenbidi.c @@ -0,0 +1,8 @@ +/* + + IMPORTANT! This file is auto-generated each time you save your + project - if you alter its contents, your changes may be overwritten! + +*/ + +#include diff --git a/JuceLibraryCode/include_juce_gui_basics.cpp b/JuceLibraryCode/include_juce_gui_basics.cpp new file mode 100644 index 0000000..80a5878 --- /dev/null +++ b/JuceLibraryCode/include_juce_gui_basics.cpp @@ -0,0 +1,8 @@ +/* + + IMPORTANT! This file is auto-generated each time you save your + project - if you alter its contents, your changes may be overwritten! + +*/ + +#include diff --git a/JuceLibraryCode/include_juce_gui_basics.mm b/JuceLibraryCode/include_juce_gui_basics.mm new file mode 100644 index 0000000..708837c --- /dev/null +++ b/JuceLibraryCode/include_juce_gui_basics.mm @@ -0,0 +1,8 @@ +/* + + IMPORTANT! This file is auto-generated each time you save your + project - if you alter its contents, your changes may be overwritten! + +*/ + +#include diff --git a/JuceLibraryCode/include_juce_gui_extra.cpp b/JuceLibraryCode/include_juce_gui_extra.cpp new file mode 100644 index 0000000..ea050e5 --- /dev/null +++ b/JuceLibraryCode/include_juce_gui_extra.cpp @@ -0,0 +1,8 @@ +/* + + IMPORTANT! This file is auto-generated each time you save your + project - if you alter its contents, your changes may be overwritten! + +*/ + +#include diff --git a/JuceLibraryCode/include_juce_gui_extra.mm b/JuceLibraryCode/include_juce_gui_extra.mm new file mode 100644 index 0000000..9bb3fea --- /dev/null +++ b/JuceLibraryCode/include_juce_gui_extra.mm @@ -0,0 +1,8 @@ +/* + + IMPORTANT! This file is auto-generated each time you save your + project - if you alter its contents, your changes may be overwritten! + +*/ + +#include diff --git a/NeuralSynth.iss b/NeuralSynth.iss new file mode 100644 index 0000000..8606a46 --- /dev/null +++ b/NeuralSynth.iss @@ -0,0 +1,98 @@ +[Setup] +AppName=NeuralSynth +AppVersion=1.0.0 +DefaultDirName={pf}\NeuralSynth +DefaultGroupName=NeuralSynth +Compression=lzma2 +SolidCompression=yes +OutputDir=.\ +ArchitecturesInstallIn64BitMode=x64 +OutputBaseFilename=NeuralSynth Installer +;LicenseFile=license.rtf +SetupLogging=yes + +[Types] +Name: "full"; Description: "Full installation" +Name: "custom"; Description: "Custom installation"; Flags: iscustom + +[Components] +Name: "app"; Description: "Standalone application (.exe)"; Types: full custom; +;Name: "vst2_32"; Description: "32-bit VST2 Plugin (.dll)"; Types: full custom; +;Name: "vst2_64"; Description: "64-bit VST2 Plugin (.dll)"; Types: full custom; Check: Is64BitInstallMode; +;Name: "vst3_32"; Description: "32-bit VST3 Plugin (.vst3)"; Types: full custom; +Name: "vst3_64"; Description: "64-bit VST3 Plugin (.vst3)"; Types: full custom; Check: Is64BitInstallMode; +;Name: "rtas_32"; Description: "32-bit RTAS Plugin (.dpm)"; Types: full custom; +;Name: "aax_32"; Description: "32-bit AAX Plugin (.aaxplugin)"; Types: full custom; +;Name: "aax_64"; Description: "64-bit AAX Plugin (.aaxplugin)"; Types: full custom; Check: Is64BitInstallMode; +;Name: "manual"; Description: "User guide"; Types: full custom; Flags: fixed + +[Files] +;Source: "..\build-win\app\Win32\bin\NeuralSynth.exe"; DestDir: "{app}"; Check: not Is64BitInstallMode; Components:app; Flags: ignoreversion; +Source: "Builds\VisualStudio2022\x64\Release\Standalone Plugin\NeuralSynth.exe"; DestDir: "{app}"; Check: Is64BitInstallMode; Components:app; Flags: ignoreversion; + +;Source: "..\build-win\vst2\Win32\bin\NeuralSynth.dll"; DestDir: {code:GetVST2Dir_32}; Check: not Is64BitInstallMode; Components:vst2_32; Flags: ignoreversion; +;Source: "..\build-win\vst2\Win32\bin\NeuralSynth.dll"; DestDir: {code:GetVST2Dir_32}; Check: Is64BitInstallMode; Components:vst2_32; Flags: ignoreversion; +;Source: "..\build-win\vst2\x64\bin\NeuralSynth.dll"; DestDir: {code:GetVST2Dir_64}; Check: Is64BitInstallMode; Components:vst2_64; Flags: ignoreversion; + +;Source: "..\build-win\vst3\Win32\bin\NeuralSynth.vst3"; DestDir: "{cf}\VST3\"; Check: not Is64BitInstallMode; Components:vst3_32; Flags: ignoreversion; +;Source: "..\build-win\vst3\Win32\bin\NeuralSynth.vst3"; DestDir: "{cf32}\VST3\"; Check: Is64BitInstallMode; Components:vst3_32; Flags: ignoreversion; +Source: "Builds\VisualStudio2022\x64\Release\VST3\NeuralSynth.vst3\Contents\x86_64-win\NeuralSynth.vst3"; DestDir: "{cf64}\VST3\"; Check: Is64BitInstallMode; Components:vst3_64; Flags: ignoreversion; + +;Source: "..\build-win\rtas\bin\NeuralSynth.dpm"; DestDir: "{cf32}\Digidesign\DAE\Plug-Ins\"; Components:rtas_32; Flags: ignoreversion; +;Source: "..\build-win\rtas\bin\NeuralSynth.dpm.rsr"; DestDir: "{cf32}\Digidesign\DAE\Plug-Ins\"; Components:rtas_32; Flags: ignoreversion; + +;Source: "..\build-win\aax\bin\NeuralSynth.aaxplugin\*.*"; DestDir: "{cf32}\Avid\Audio\Plug-Ins\NeuralSynth.aaxplugin\"; Components:aax_32; Flags: ignoreversion recursesubdirs; +;Source: "..\build-win\aax\bin\NeuralSynth.aaxplugin\*.*"; DestDir: "{cf}\Avid\Audio\Plug-Ins\NeuralSynth.aaxplugin\"; Components:aax_64; Flags: ignoreversion recursesubdirs; + +;Source: "..\manual\NeuralSynth_manual.pdf"; DestDir: "{app}" +;Source: "changelog.txt"; DestDir: "{app}" +;Source: "readmewin.rtf"; DestDir: "{app}"; DestName: "readme.rtf"; Flags: isreadme + +[Icons] +Name: "{group}\NeuralSynth"; Filename: "{app}\NeuralSynth.exe" +;Name: "{group}\User guide"; Filename: "{app}\NeuralSynth_manual.pdf" +;Name: "{group}\Changelog"; Filename: "{app}\changelog.txt" +;Name: "{group}\readme"; Filename: "{app}\readme.rtf" +Name: "{group}\Uninstall NeuralSynth"; Filename: "{app}\unins000.exe" + +;[Dirs] +;Name: {cf}\Digidesign\DAE\Plugins\ + +[Code] +var + OkToCopyLog : Boolean; + VST3DirPage_32: TInputDirWizardPage; + VST3DirPage_64: TInputDirWizardPage; + +procedure InitializeWizard; +begin + if IsWin64 then begin + VST3DirPage_64 := CreateInputDirPage(wpSelectDir, + 'Confirm 64-Bit VST3 Plugin Directory', '', + 'Select the folder in which setup should install the 64-bit VST3 Plugin, then click Next.', + False, ''); + VST3DirPage_64.Add(''); + VST3DirPage_64.Values[0] := ExpandConstant('{reg:HKLM\SOFTWARE\VST,VSTPluginsPath|{pf}\Common Files\VST3}\'); + end; +end; + +function GetVST3Dir_64(Param: String): String; +begin + Result := VST3DirPage_64.Values[0] +end; + +procedure CurStepChanged(CurStep: TSetupStep); +begin + if CurStep = ssDone then + OkToCopyLog := True; +end; + +procedure DeinitializeSetup(); +begin + if OkToCopyLog then + FileCopy (ExpandConstant ('{log}'), ExpandConstant ('{app}\InstallationLogFile.log'), FALSE); + RestartReplace (ExpandConstant ('{log}'), ''); +end; + +[UninstallDelete] +Type: files; Name: "{app}\InstallationLogFile.log" \ No newline at end of file diff --git a/NeuralSynth.jucer b/NeuralSynth.jucer new file mode 100644 index 0000000..f0cdfd3 --- /dev/null +++ b/NeuralSynth.jucer @@ -0,0 +1,74 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Source/AudioBufferQueue.h b/Source/AudioBufferQueue.h new file mode 100644 index 0000000..30bb6f7 --- /dev/null +++ b/Source/AudioBufferQueue.h @@ -0,0 +1,49 @@ +#pragma once + +//============================================================================== +template +class AudioBufferQueue +{ +public: + //============================================================================== + static constexpr size_t order = 9; + static constexpr size_t bufferSize = 1U << order; + static constexpr size_t numBuffers = 5; + + //============================================================================== + void push(const SampleType* dataToPush, size_t numSamples) + { + jassert(numSamples <= bufferSize); + + int start1, size1, start2, size2; + abstractFifo.prepareToWrite(1, start1, size1, start2, size2); + + jassert(size1 <= 1); + jassert(size2 == 0); + + if (size1 > 0) + juce::FloatVectorOperations::copy(buffers[(size_t)start1].data(), dataToPush, (int)juce::jmin(bufferSize, numSamples)); + + abstractFifo.finishedWrite(size1); + } + + //============================================================================== + void pop(SampleType* outputBuffer) + { + int start1, size1, start2, size2; + abstractFifo.prepareToRead(1, start1, size1, start2, size2); + + jassert(size1 <= 1); + jassert(size2 == 0); + + if (size1 > 0) + juce::FloatVectorOperations::copy(outputBuffer, buffers[(size_t)start1].data(), (int)bufferSize); + + abstractFifo.finishedRead(size1); + } + +private: + //============================================================================== + juce::AbstractFifo abstractFifo{ numBuffers }; + std::array, numBuffers> buffers; +}; \ No newline at end of file diff --git a/Source/AudioEngine.h b/Source/AudioEngine.h new file mode 100644 index 0000000..1104e55 --- /dev/null +++ b/Source/AudioEngine.h @@ -0,0 +1,47 @@ +#pragma once + +#include "SynthVoice.h" +#include + +class NeuralAudioEngine : public juce::MPESynthesiser +{ +public: + static constexpr int maxNumVoices = 8; + + explicit NeuralAudioEngine(NeuralSharedParams& sp) + { + // Create MPE voices + for (int i = 0; i < maxNumVoices; ++i) + addVoice(new NeuralSynthVoice(sp)); // <-- takes MPESynthesiserVoice* + + // MPE synths do not use addSound(); note events are routed via MPE zones. + setVoiceStealingEnabled(true); + } + + void prepare(const juce::dsp::ProcessSpec& spec) noexcept + { + setCurrentPlaybackSampleRate(spec.sampleRate); + + for (auto* v : voices) + if (auto* nv = dynamic_cast(v)) + nv->prepare(spec); + } + + template + void applyToVoices(VoiceFunc&& fn) noexcept + { + for (auto* v : voices) + fn(dynamic_cast(v)); + } + +private: + // keep base render + using juce::MPESynthesiser::renderNextSubBlock; + + void renderNextSubBlock(juce::AudioBuffer& outputAudio, + int startSample, + int numSamples) override + { + juce::MPESynthesiser::renderNextSubBlock(outputAudio, startSample, numSamples); + } +}; diff --git a/Source/BlepOsc.h b/Source/BlepOsc.h new file mode 100644 index 0000000..4f3d894 --- /dev/null +++ b/Source/BlepOsc.h @@ -0,0 +1,80 @@ +#pragma once +#include + +enum class BlepWave : int { Sine = 0, Saw, Square, Triangle }; + +class BlepOsc +{ +public: + void prepare (double sampleRate) { sr = sampleRate; resetPhase(); } + void setWave (BlepWave w) { wave = w; } + void setFrequency (float f) { freq = juce::jmax (0.0f, f); inc = freq / (float) sr; } + void resetPhase (float p = 0.0f) { phase = juce::jlimit (0.0f, 1.0f, p); } + + inline float process() + { + // phase in [0..1) + float out = 0.0f; + float t = phase; + phase += inc; + if (phase >= 1.0f) phase -= 1.0f; + + switch (wave) + { + case BlepWave::Sine: out = std::sin (2.0f * juce::MathConstants::pi * t); break; + + case BlepWave::Saw: + { + // naive saw in [-1..1] + float s = 2.0f * t - 1.0f; + // apply BLEP at the discontinuity crossing t=0 + s -= polyBlep (t, inc); + out = s; + } break; + + case BlepWave::Square: + { + float s = (t < 0.5f ? 1.0f : -1.0f); + // rising edge at 0.0, falling at 0.5 + s += polyBlep (t, inc) - polyBlep (std::fmod (t + 0.5f, 1.0f), inc); + out = s; + } break; + + case BlepWave::Triangle: + { + // integrate the BLEP square for band-limited tri + float sq = (t < 0.5f ? 1.0f : -1.0f); + sq += polyBlep (t, inc) - polyBlep (std::fmod (t + 0.5f, 1.0f), inc); + // leaky integrator to keep DC under control + z1 = z1 + (sq - z1) * inc; + out = 2.0f * z1; // scale + } break; + } + + return out; + } + +private: + // PolyBLEP as in Valimäki/Huovilainen + static inline float polyBlep (float t, float dt) + { + // t in [0..1) + if (t < dt) + { + t /= dt; + return t + t - t * t - 1.0f; + } + else if (t > 1.0f - dt) + { + t = (t - 1.0f) / dt; + return t * t + t + t + 1.0f; + } + return 0.0f; + } + + double sr = 44100.0; + float freq = 440.0f, inc = 440.0f / 44100.0f; + float phase = 0.0f; + float z1 = 0.0f; + BlepWave wave = BlepWave::Sine; +}; diff --git a/Source/GraphComponent.h b/Source/GraphComponent.h new file mode 100644 index 0000000..cf9c6ee --- /dev/null +++ b/Source/GraphComponent.h @@ -0,0 +1,99 @@ +/* + ============================================================================== + + GraphComponent.h + Created: 4 Jul 2025 11:43:57pm + Author: timot + + ============================================================================== +*/ + +#pragma once + +#include // for std::minmax_element +#include "AudioBufferQueue.h" + +//============================================================================== +template +class GraphComponent : public juce::Component, + private juce::Timer +{ +public: + //============================================================================== + GraphComponent(SampleType minIn, SampleType maxIn, int numPointsIn) + : min(minIn), max(maxIn), numPoints(numPointsIn) + { + x.resize(numPoints); + y.resize(numPoints); + setFramesPerSecond(30); + // func will be set via setFunction before paint; provide a safe default + func = [](SampleType) noexcept { return SampleType(); }; + } + + //============================================================================== + void setFramesPerSecond(int framesPerSecond) + { + jassert(framesPerSecond > 0 && framesPerSecond < 1000); + startTimerHz(framesPerSecond); + } + + //============================================================================== + void setFunction(const std::function& f) { func = f; } + + //============================================================================== + void paint(juce::Graphics& g) override + { + g.fillAll(juce::Colours::black); + g.setColour(juce::Colours::white); + + auto area = getLocalBounds(); + + if (hasData && area.isFinite()) + { + auto h = (SampleType)area.getHeight(); + auto w = (SampleType)area.getWidth(); + + for (size_t i = 1; i < (size_t)numPoints; ++i) + { + auto px_prev = ((x[i - 1] - min) / (max - min)) * w; + auto py_prev = h - ((y[i - 1] - minY) / (maxY - minY)) * h; + + auto px_next = ((x[i] - min) / (max - min)) * w; + auto py_next = h - ((y[i] - minY) / (maxY - minY)) * h; + + g.drawLine({ px_prev, py_prev, px_next, py_next }); + } + } + } + + //============================================================================== + void resized() override {} + +private: + //============================================================================== + std::vector x, y; + SampleType minY{ SampleType() }, maxY{ SampleType(1) }; + SampleType min{}, max{}; + int numPoints{}; + std::function func; + bool hasData = false; + + //============================================================================== + void timerCallback() override + { + const SampleType step = (max - min) / (SampleType)(numPoints - 1); + + for (int i = 0; i < numPoints; i++) + { + x[(size_t)i] = min + step * (SampleType)i; + y[(size_t)i] = func(x[(size_t)i]); + } + + auto p = std::minmax_element(y.begin(), y.end()); + minY = *p.first; + maxY = *p.second; + + hasData = true; + repaint(); + } +}; diff --git a/Source/NeuralSharedParams.h b/Source/NeuralSharedParams.h new file mode 100644 index 0000000..b6ff9df --- /dev/null +++ b/Source/NeuralSharedParams.h @@ -0,0 +1,145 @@ +#pragma once + +#include +#include +#include + +struct SliderDetail { + std::string label; + float min, max, interval, defValue; +}; + +using ParamMap = std::unordered_map; + +// Each SliderDetail: { label, min, max, step, defaultValue } +const std::unordered_map PARAM_SETTINGS = { + { "chorus", { + { "rate", { "Rate", 0.0f, 1.0f, 0.1f, 0.1f } }, + { "depth", { "Depth", 0.0f, 1.0f, 0.1f, 0.1f } }, + { "centre", { "Centre", 0.0f, 1.0f, 0.1f, 0.1f } }, + { "feedback", { "Feedback", 0.0f, 1.0f, 0.1f, 0.1f } }, + { "mix", { "Mix", 0.0f, 1.0f, 0.1f, 0.1f } } + }}, + { "delay", { + { "delay", { "Delay", 0.0f, 1.0f, 0.1f, 0.1f } } + }}, + { "reverb", { + { "roomSize", { "Room Size", 0.0f, 1.0f, 0.1f, 0.1f } }, + { "damping", { "Damping", 0.0f, 1.0f, 0.1f, 0.1f } }, + { "wetLevel", { "Wet Level", 0.0f, 1.0f, 0.1f, 0.1f } }, + { "dryLevel", { "Dry Level", 0.0f, 1.0f, 0.1f, 0.1f } }, + { "width", { "Width", 0.0f, 1.0f, 0.1f, 0.1f } }, + { "freezeMode", { "Freeze Mode", 0.0f, 1.0f, 0.1f, 0.1f } } + }}, + { "adsr", { + { "attack", { "Attack", 0.0f, 1.0f, 0.1f, 0.1f } }, + { "decay", { "Decay", 0.0f, 1.0f, 0.1f, 0.1f } }, + { "sustain", { "Sustain", 0.0f, 1.0f, 0.1f, 0.1f } }, + { "release", { "Release", 0.0f, 1.0f, 0.1f, 0.1f } } + }}, + // Filter envelope group (short key: "fenv") + { "fenv", { + { "attack", { "Attack", 0.0f, 2.0f, 0.001f, 0.01f } }, + { "decay", { "Decay", 0.0f, 2.0f, 0.001f, 0.10f } }, + { "sustain", { "Sustain", 0.0f, 1.0f, 0.001f, 0.80f } }, + { "release", { "Release", 0.0f, 4.0f, 0.001f, 0.40f } }, + { "amount", { "Amount", -1.0f, 1.0f, 0.001f, 0.50f } } + }}, + { "flanger", { + { "rate", { "Rate", 0.1f, 5.0f, 0.1f, 0.1f } }, + { "depth", { "Depth", 0.1f, 10.0f, 0.1f, 0.1f } }, // ms + { "feedback", { "Feedback", 0.0f, 0.95f, 0.01f, 0.1f } }, + { "dryMix", { "Dry/Wet", 0.0f, 1.0f, 0.01f, 0.0f } }, + { "phase", { "Phase", 0.0f, 1.0f, 0.1f, 0.0f } }, + { "delay", { "Delay", 0.0f, 3.0f, 0.1f, 0.25f } } // ms base + }}, + { "filter", { + { "cutoff", { "Cutoff", 20.0f, 20000.0f, 1.0f, 1000.0f } }, + { "resonance", { "Resonance", 0.1f, 10.0f, 0.1f, 0.7f } }, + { "type", { "L/H/B", 0.0f, 2.0f, 1.0f, 0.0f } }, + { "drive", { "Drive", 0.0f, 1.0f, 0.01f, 0.0f } }, + { "mod", { "Mod", -1.0f, 1.0f, 0.1f, 0.0f } }, + { "key", { "Key", 0.0f, 1.0f, 0.1f, 0.0f } } + }}, + { "distortion", { + { "drive", { "Drive", 0.0f, 30.0f, 0.1f, 10.0f } }, + { "mix", { "Mix", 0.0f, 1.0f, 0.01f, 0.0f } }, + { "bias", { "Bias", -1.0f, 1.0f, 0.01f, 0.0f } }, + { "tone", { "Tone", 100.0f, 8000.0f, 10.0f, 3000.0f } }, + { "shape", { "Shape", 0.0f, 2.0f, 1.0f, 0.0f } } + }} +}; + +struct NeuralSharedParams +{ + std::atomic waveform{ -1 }; + + // Amp ADSR + std::atomic* adsrAttack{}; + std::atomic* adsrDecay{}; + std::atomic* adsrSustain{}; + std::atomic* adsrRelease{}; + + // Delay + std::atomic* delayTime{}; + + // Chorus + std::atomic* chorusRate{}; + std::atomic* chorusDepth{}; + std::atomic* chorusCentre{}; + std::atomic* chorusFeedback{}; + std::atomic* chorusMix{}; + + // Reverb + std::atomic* reverbRoomSize{}; + std::atomic* reverbDamping{}; + std::atomic* reverbWetLevel{}; + std::atomic* reverbDryLevel{}; + std::atomic* reverbWidth{}; + std::atomic* reverbFreezeMode{}; + + // Flanger + std::atomic* flangerRate{}; + std::atomic* flangerDepth{}; + std::atomic* flangerFeedback{}; + std::atomic* flangerDryMix{}; + std::atomic* flangerPhase{}; + std::atomic* flangerDelay{}; + + // Filter (base) + std::atomic* filterCutoff{}; + std::atomic* filterResonance{}; + std::atomic* filterType{}; + std::atomic* filterDrive{}; + std::atomic* filterMod{}; + std::atomic* filterKey{}; + + // Filter Env (polyphonic) + std::atomic* fenvAttack{}; + std::atomic* fenvDecay{}; + std::atomic* fenvSustain{}; + std::atomic* fenvRelease{}; + std::atomic* fenvAmount{}; // +/- octaves + + // Distortion + std::atomic* distortionDrive{}; + std::atomic* distortionMix{}; + std::atomic* distortionBias{}; + std::atomic* distortionTone{}; + std::atomic* distortionShape{}; + + // Per-panel bypass (AudioParameterBool, exposed as float 0/1 via getRawParameterValue) + std::atomic* chorusOn{}; + std::atomic* delayOn{}; + std::atomic* reverbOn{}; + std::atomic* flangerOn{}; + std::atomic* distortionOn{}; + std::atomic* filterOn{}; + std::atomic* eqOn{}; + + // EQ + Master + std::atomic* lowGainDbls{}; + std::atomic* midGainDbls{}; + std::atomic* highGainDbls{}; + std::atomic* masterDbls{}; +}; diff --git a/Source/PluginEditor.cpp b/Source/PluginEditor.cpp new file mode 100644 index 0000000..43ecffb --- /dev/null +++ b/Source/PluginEditor.cpp @@ -0,0 +1,170 @@ +#include "PluginProcessor.h" +#include "PluginEditor.h" +#include "ScopeComponent.h" + +//============================================================================== +NeuralSynthAudioProcessorEditor::NeuralSynthAudioProcessorEditor (NeuralSynthAudioProcessor& p) + : AudioProcessorEditor (&p), + audioProcessor (p), + mainScopeComponent(audioProcessor.getAudioBufferQueue()) +{ + auto& tree = audioProcessor.parameters; + + addAndMakeVisible(mainScopeComponent); + + waveformSelector.setModel(&waveformContents); + waveformContents.onSelect = [this](int row) + { + // write to the parameter so voices update safely + audioProcessor.parameters.getParameterAsValue("waveform") = (float)juce::jlimit(0, 3, row); + }; + addAndMakeVisible(waveformSelector); + + // --- Panels --- + adsrComponent.emplace(tree, "adsr", "Amp Env"); + adsrComponent->enableGraphScope([this](float x) { + auto& tree = this->audioProcessor.parameters; + + float A = tree.getParameter("adsr_attack")->getValue(); + float D = tree.getParameter("adsr_decay")->getValue(); + float S = tree.getParameter("adsr_sustain")->getValue(); + float R = tree.getParameter("adsr_release")->getValue(); + + const float sustainLen = 1.0f; + const float total = A + D + sustainLen + R; + A /= total; D /= total; R /= total; + + float m = 0.0f, c = 0.0f; + if (x < A) { m = 1.0f / A; c = 0.0f; } + else if (x < A + D) { m = (S - 1.0f) / D; c = 1.0f - m * A; } + else if (x < A + D + (sustainLen / total)) { m = 0.0f; c = S; } + else { m = (S / -R); c = -m; } + return m * x + c; + }); + addAndMakeVisible(*adsrComponent); + + chorusComponent.emplace(tree, "chorus", "Chorus"); + chorusComponent->enableSampleScope(audioProcessor.getChorusAudioBufferQueue()); + addAndMakeVisible(*chorusComponent); + + delayComponent.emplace(tree, "delay", "Delay"); + delayComponent->enableSampleScope(audioProcessor.getDelayAudioBufferQueue()); + addAndMakeVisible(*delayComponent); + + reverbComponent.emplace(tree, "reverb", "Reverb"); + reverbComponent->enableSampleScope(audioProcessor.getReverbAudioBufferQueue()); + addAndMakeVisible(*reverbComponent); + + eqComponent.emplace(tree, "EQ"); + addAndMakeVisible(*eqComponent); + + flangerComponent.emplace(tree, "flanger", "Flanger"); + flangerComponent->enableSampleScope(audioProcessor.getFlangerAudioBufferQueue()); + addAndMakeVisible(*flangerComponent); + + distortionComponent.emplace(tree, "distortion", "Distortion"); + distortionComponent->enableSampleScope(audioProcessor.getDistortionAudioBufferQueue()); + addAndMakeVisible(*distortionComponent); + + filterComponent.emplace(tree, "filter", "Filter"); + filterComponent->enableSampleScope(audioProcessor.getFilterAudioBufferQueue()); + addAndMakeVisible(*filterComponent); + + filterEnvComponent.emplace(tree, "fenv", "Filter Env"); + filterEnvComponent->enableGraphScope([this](float x) { + auto& tree = this->audioProcessor.parameters; + + float A = tree.getParameter("fenv_attack")->getValue(); + float D = tree.getParameter("fenv_decay")->getValue(); + float S = tree.getParameter("fenv_sustain")->getValue(); + float R = tree.getParameter("fenv_release")->getValue(); + + const float sustainLen = 1.0f; + const float total = A + D + sustainLen + R; + A /= total; D /= total; R /= total; + + float m = 0.0f, c = 0.0f; + if (x < A) { m = 1.0f / A; c = 0.0f; } + else if (x < A + D) { m = (S - 1.0f) / D; c = 1.0f - m * A; } + else if (x < A + D + (sustainLen / total)) { m = 0.0f; c = S; } + else { m = (S / -R); c = -m; } + return m * x + c; + }); + addAndMakeVisible(*filterEnvComponent); + + // Master fader + label + addAndMakeVisible(masterLevelSlider); + masterLevelLabel.setText("Master", juce::dontSendNotification); + { + juce::Font f; f.setHeight(12.0f); f.setBold(true); + masterLevelLabel.setFont(f); + } + masterLevelLabel.setJustificationType(juce::Justification::centred); + addAndMakeVisible(masterLevelLabel); + + // Blank placeholder + addAndMakeVisible(blankPanel); + + // Attach master parameter + gainAttachment = std::make_unique( + audioProcessor.parameters, "master", masterLevelSlider.slider); + + setSize(1400, 720); +} + +//============================================================================== +NeuralSynthAudioProcessorEditor::~NeuralSynthAudioProcessorEditor() = default; + +//============================================================================== +void NeuralSynthAudioProcessorEditor::paint (juce::Graphics& g) +{ + g.fillAll(getLookAndFeel().findColour (juce::ResizableWindow::backgroundColourId)); +} + +//============================================================================== +void NeuralSynthAudioProcessorEditor::resized() +{ + auto bounds = getLocalBounds().reduced(16); + + juce::Grid grid; + + grid.templateRows = { + juce::Grid::TrackInfo(juce::Grid::Fr(20)), // scope row + juce::Grid::TrackInfo(juce::Grid::Fr(40)), // row 1 + juce::Grid::TrackInfo(juce::Grid::Fr(40)) // row 2 + }; + + // 6 columns: 5 content + 1 sidebar (waveform+master) + grid.templateColumns = { + juce::Grid::TrackInfo(juce::Grid::Fr(18)), + juce::Grid::TrackInfo(juce::Grid::Fr(18)), + juce::Grid::TrackInfo(juce::Grid::Fr(18)), + juce::Grid::TrackInfo(juce::Grid::Fr(18)), + juce::Grid::TrackInfo(juce::Grid::Fr(18)), + juce::Grid::TrackInfo(juce::Grid::Fr(10)) + }; + + // Row 0 + grid.items.add(juce::GridItem(mainScopeComponent) + .withArea(juce::GridItem::Span(1), juce::GridItem::Span(5))); + grid.items.add(juce::GridItem(waveformSelector) + .withArea(juce::GridItem::Span(1), juce::GridItem::Span(1))); + + // Row 1 + grid.items.add(juce::GridItem(*adsrComponent)); + grid.items.add(juce::GridItem(*chorusComponent)); + grid.items.add(juce::GridItem(*delayComponent)); + grid.items.add(juce::GridItem(*reverbComponent)); + grid.items.add(juce::GridItem(*eqComponent)); + grid.items.add(juce::GridItem(masterLevelLabel)); + + // Row 2 + grid.items.add(juce::GridItem(*flangerComponent)); + grid.items.add(juce::GridItem(*distortionComponent)); + grid.items.add(juce::GridItem(*filterComponent)); + grid.items.add(juce::GridItem(*filterEnvComponent)); + grid.items.add(juce::GridItem(blankPanel)); + grid.items.add(juce::GridItem(masterLevelSlider)); + + grid.performLayout(bounds); +} diff --git a/Source/PluginEditor.h b/Source/PluginEditor.h new file mode 100644 index 0000000..ec9e583 --- /dev/null +++ b/Source/PluginEditor.h @@ -0,0 +1,341 @@ +#pragma once + +#include +#include "PluginProcessor.h" +#include "GraphComponent.h" +#include "ScopeComponent.h" + +//============================== ScopeSliderComponent ========================== +// A generic panel: optional scope/graph + rotary sliders + labels. +// Adds a per-panel "On" toggle (bound to "_on"). +class ScopeSliderComponent : public juce::Component { + static const int fontSize = 11; + +public: + ScopeSliderComponent(juce::AudioProcessorValueTreeState& tree, + const std::string paramGroup, + const juce::String& titleText = {}) + : paramGroupId(paramGroup), treeRef(tree) + { + const auto& sliderDetails = PARAM_SETTINGS.at(paramGroup); + + for (const auto& [name, sliderDetail] : sliderDetails) { + sliders.push_back(std::make_unique()); + labels.push_back(std::make_unique()); + attachments.push_back(std::make_unique( + tree, paramGroup + "_" + name, *sliders.back())); + + labels.back()->setText(sliderDetail.label, juce::dontSendNotification); + sliders.back()->setRange(sliderDetail.min, sliderDetail.max); + } + + for (auto& slider : sliders) + { + slider->setSliderStyle(juce::Slider::Rotary); + slider->setTextBoxStyle(juce::Slider::TextBoxBelow, false, 50, 20); + addAndMakeVisible(*slider); + } + + for (auto& label : labels) + { + juce::Font f; f.setHeight((float)fontSize); f.setBold(true); + label->setFont(f); + label->setColour(juce::Label::textColourId, juce::Colours::lightgreen); + label->setJustificationType(juce::Justification::centred); + addAndMakeVisible(*label); + } + + if (titleText.isNotEmpty()) + { + titleLabel.setText(titleText, juce::dontSendNotification); + juce::Font tf; tf.setHeight(12.0f); tf.setBold(true); + titleLabel.setFont(tf); + titleLabel.setJustificationType(juce::Justification::centredLeft); + titleLabel.setColour(juce::Label::textColourId, juce::Colours::white); + addAndMakeVisible(titleLabel); + } + + // Bypass toggle (per panel), id "_on" + bypassButton.setButtonText("On"); + bypassButton.setClickingTogglesState(true); + addAndMakeVisible(bypassButton); + bypassAttachment = std::make_unique( + treeRef, paramGroupId + "_on", bypassButton); + } + + void enableSampleScope(AudioBufferQueue& audioBufferQueue) { + scope.emplace(audioBufferQueue); + useGraphScope = false; + addAndMakeVisible(*scope); + } + + void enableGraphScope(const std::function& func) { + graphScope.emplace(0.0f, 1.0f, 100); + graphScope->setFunction(func); + useGraphScope = true; + addAndMakeVisible(*graphScope); + } + +private: + void paint(juce::Graphics& g) override + { + g.fillAll(juce::Colours::darkgrey); + g.setColour(juce::Colours::white); + g.drawRect(getLocalBounds()); + } + + void resized() override + { + // --- Top bar (manual) ---------------------------------------------- + auto area = getLocalBounds().reduced(10); + auto top = area.removeFromTop(22); + auto btnW = 46; + bypassButton.setBounds(top.removeFromRight(btnW).reduced(2, 1)); + titleLabel.setBounds(top); + + // --- Rest (grid) ---------------------------------------------------- + juce::Grid grid; + grid.templateRows = { + juce::Grid::TrackInfo(juce::Grid::Fr(55)), // scope/graph + juce::Grid::TrackInfo(juce::Grid::Fr(30)), // sliders + juce::Grid::TrackInfo(juce::Grid::Fr(15)) // labels + }; + + const int n = (int)sliders.size(); + grid.templateColumns.resize(n); + for (int i = 0; i < n; ++i) + grid.templateColumns.getReference(i) = juce::Grid::TrackInfo(juce::Grid::Fr(1)); + + grid.items.clear(); + + // Row 1: scope/graph – only add if constructed + if (useGraphScope) + { + if (graphScope) + grid.items.add(juce::GridItem(*graphScope) + .withArea(juce::GridItem::Span(1), juce::GridItem::Span(n))); + else + grid.items.add(juce::GridItem() + .withArea(juce::GridItem::Span(1), juce::GridItem::Span(n))); + } + else + { + if (scope) + grid.items.add(juce::GridItem(*scope) + .withArea(juce::GridItem::Span(1), juce::GridItem::Span(n))); + else + grid.items.add(juce::GridItem() + .withArea(juce::GridItem::Span(1), juce::GridItem::Span(n))); + } + + // Row 2: sliders + for (int i = 0; i < n; ++i) + grid.items.add(juce::GridItem(*sliders[(size_t)i])); + + // Row 3: labels + for (int i = 0; i < n; ++i) + grid.items.add(juce::GridItem(*labels[(size_t)i])); + + grid.performLayout(area); + } + + bool useGraphScope{ false }; + std::optional> scope; + std::optional> graphScope; + + std::vector> sliders; + std::vector> labels; + std::vector> attachments; + + juce::ToggleButton bypassButton; + std::unique_ptr bypassAttachment; + + juce::Label titleLabel; + + std::string paramGroupId; + juce::AudioProcessorValueTreeState& treeRef; +}; + +//============================== EqualizerComponent ============================ +// Adds an On/Off toggle bound to "eq_on". +class EqualizerComponent : public juce::Component { + static const int fontSize = 11; + +public: + explicit EqualizerComponent(juce::AudioProcessorValueTreeState& tree, + const juce::String& titleText = {}) + { + setupSlider(lowGainSlider); + setupSlider(midGainSlider); + setupSlider(highGainSlider); + + setupLabel(lowGainLabel, "L"); + setupLabel(midGainLabel, "M"); + setupLabel(highGainLabel, "H"); + + if (titleText.isNotEmpty()) + { + titleLabel.setText(titleText, juce::dontSendNotification); + juce::Font tf; tf.setHeight(13.0f); tf.setBold(true); + titleLabel.setFont(tf); + titleLabel.setJustificationType(juce::Justification::centredLeft); + titleLabel.setColour(juce::Label::textColourId, juce::Colours::white); + addAndMakeVisible(titleLabel); + } + + // Attachments + lowGainAttachment = std::make_unique(tree, "lowEQ", lowGainSlider); + midGainAttachment = std::make_unique(tree, "midEQ", midGainSlider); + highGainAttachment = std::make_unique(tree, "highEQ", highGainSlider); + + // EQ bypass toggle + bypassButton.setButtonText("On"); + bypassButton.setClickingTogglesState(true); + addAndMakeVisible(bypassButton); + bypassAttachment = std::make_unique(tree, "eq_on", bypassButton); + } + +private: + void setupSlider(juce::Slider& slider) { + slider.setRange(-24.0f, 24.0f, 0.1f); + slider.setSliderStyle(juce::Slider::LinearBarVertical); + slider.setTextBoxStyle(juce::Slider::TextBoxBelow, false, 50, 20); + addAndMakeVisible(slider); + } + + void setupLabel(juce::Label& lbl, juce::String txt) { + juce::Font f; f.setHeight((float)fontSize); f.setBold(true); + lbl.setFont(f); + lbl.setColour(juce::Label::textColourId, juce::Colours::lightgreen); + lbl.setJustificationType(juce::Justification::centred); + lbl.setText(txt, juce::dontSendNotification); + addAndMakeVisible(lbl); + } + + void paint(juce::Graphics& g) override { + g.fillAll(juce::Colours::darkgrey); + g.setColour(juce::Colours::white); + g.drawRect(getLocalBounds()); + } + + void resized() override { + auto area = getLocalBounds().reduced(10); + auto top = area.removeFromTop(22); + auto btnW = 46; + bypassButton.setBounds(top.removeFromRight(btnW).reduced(2, 1)); + titleLabel.setBounds(top); + + juce::Grid grid; + grid.templateRows = { + juce::Grid::TrackInfo(juce::Grid::Fr(1)), + juce::Grid::TrackInfo(juce::Grid::Fr(1)) + }; + grid.templateColumns = { + juce::Grid::TrackInfo(juce::Grid::Fr(1)), + juce::Grid::TrackInfo(juce::Grid::Fr(1)), + juce::Grid::TrackInfo(juce::Grid::Fr(1)) + }; + + grid.items = { + lowGainSlider, midGainSlider, highGainSlider, + lowGainLabel, midGainLabel, highGainLabel + }; + + grid.performLayout(area); + } + + juce::Slider lowGainSlider, midGainSlider, highGainSlider; + juce::Label lowGainLabel, midGainLabel, highGainLabel; + std::unique_ptr lowGainAttachment, midGainAttachment, highGainAttachment; + + juce::ToggleButton bypassButton; + std::unique_ptr bypassAttachment; + + juce::Label titleLabel; +}; + +//============================== Waveform List Model =========================== +struct WaveformSelectorContents final : public juce::ListBoxModel +{ + int getNumRows() override { return 4; } + + void paintListBoxItem(int rowNumber, juce::Graphics& g, + int width, int height, bool rowIsSelected) override + { + if (rowIsSelected) g.fillAll(juce::Colours::lightblue); + g.setColour(juce::LookAndFeel::getDefaultLookAndFeel() + .findColour(juce::Label::textColourId)); + + juce::Font f; f.setHeight((float)height * 0.7f); + g.setFont(f); + g.drawText(waves[(size_t)rowNumber], 5, 0, width, height, + juce::Justification::centredLeft, true); + } + + void selectedRowsChanged (int lastRowSelected) override + { + if (onSelect) onSelect(lastRowSelected); + } + + std::function onSelect; + + std::vector waves { "Sine", "Saw", "Square", "Triangle" }; +}; + +//============================== MasterVolumeComponent ========================= +class MasterVolumeComponent : public juce::Component +{ +public: + MasterVolumeComponent() + { + slider.setSliderStyle(juce::Slider::LinearBarVertical); + slider.setTextBoxStyle(juce::Slider::NoTextBox, false, 20, 20); + addAndMakeVisible(slider); + } + + void resized() override + { + slider.setBounds(getLocalBounds().reduced(30)); + } + + juce::Slider slider; +}; + +//============================== Editor ======================================= +class NeuralSynthAudioProcessorEditor : public juce::AudioProcessorEditor +{ +public: + NeuralSynthAudioProcessorEditor (NeuralSynthAudioProcessor&); + ~NeuralSynthAudioProcessorEditor() override; + + void paint (juce::Graphics&) override; + void resized() override; + +private: + NeuralSynthAudioProcessor& audioProcessor; + JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (NeuralSynthAudioProcessorEditor) + + juce::ListBox waveformSelector; + WaveformSelectorContents waveformContents; + + std::optional adsrComponent; // Amp Env + std::optional chorusComponent; + std::optional delayComponent; + std::optional reverbComponent; + + std::optional flangerComponent; + std::optional distortionComponent; + std::optional filterComponent; + std::optional filterEnvComponent; // Filter Env panel + + MasterVolumeComponent masterLevelSlider; + juce::Label masterLevelLabel; + + std::optional eqComponent; + + std::unique_ptr gainAttachment; + + ScopeComponent mainScopeComponent; + + juce::Component blankPanel; +}; diff --git a/Source/PluginProcessor.cpp b/Source/PluginProcessor.cpp new file mode 100644 index 0000000..f357fe3 --- /dev/null +++ b/Source/PluginProcessor.cpp @@ -0,0 +1,270 @@ +#include "PluginProcessor.h" +#include "PluginEditor.h" + +//============================================================================== +NeuralSynthAudioProcessor::NeuralSynthAudioProcessor() + : parameters(*this, nullptr, "PARAMETERS", createParameterLayout()) + , AudioProcessor(BusesProperties().withOutput("Output", juce::AudioChannelSet::stereo(), true)) + , audioEngine(sp) +{ + parameters.addParameterListener("waveform", this); + + // === Per-panel bypass (default OFF) === + sp.chorusOn = parameters.getRawParameterValue("chorus_on"); + sp.delayOn = parameters.getRawParameterValue("delay_on"); + sp.reverbOn = parameters.getRawParameterValue("reverb_on"); + sp.flangerOn = parameters.getRawParameterValue("flanger_on"); + sp.distortionOn = parameters.getRawParameterValue("distortion_on"); + sp.filterOn = parameters.getRawParameterValue("filter_on"); + sp.eqOn = parameters.getRawParameterValue("eq_on"); + + // === Chorus === + parameters.addParameterListener("chorus_rate", this); + parameters.addParameterListener("chorus_depth", this); + parameters.addParameterListener("chorus_centre", this); + parameters.addParameterListener("chorus_feedback", this); + parameters.addParameterListener("chorus_mix", this); + + sp.chorusRate = parameters.getRawParameterValue("chorus_rate"); + sp.chorusDepth = parameters.getRawParameterValue("chorus_depth"); + sp.chorusCentre = parameters.getRawParameterValue("chorus_centre"); + sp.chorusFeedback = parameters.getRawParameterValue("chorus_feedback"); + sp.chorusMix = parameters.getRawParameterValue("chorus_mix"); + + // === Delay === + parameters.addParameterListener("delay_delay", this); + sp.delayTime = parameters.getRawParameterValue("delay_delay"); + + // === Reverb === + parameters.addParameterListener("reverb_roomSize", this); + parameters.addParameterListener("reverb_damping", this); + parameters.addParameterListener("reverb_wetLevel", this); + parameters.addParameterListener("reverb_dryLevel", this); + parameters.addParameterListener("reverb_width", this); + parameters.addParameterListener("reverb_freezeMode", this); + + sp.reverbRoomSize = parameters.getRawParameterValue("reverb_roomSize"); + sp.reverbDamping = parameters.getRawParameterValue("reverb_damping"); + sp.reverbWetLevel = parameters.getRawParameterValue("reverb_wetLevel"); + sp.reverbDryLevel = parameters.getRawParameterValue("reverb_dryLevel"); + sp.reverbWidth = parameters.getRawParameterValue("reverb_width"); + sp.reverbFreezeMode= parameters.getRawParameterValue("reverb_freezeMode"); + + // === Amp ADSR === + parameters.addParameterListener("adsr_attack", this); + parameters.addParameterListener("adsr_decay", this); + parameters.addParameterListener("adsr_sustain", this); + parameters.addParameterListener("adsr_release", this); + + sp.adsrAttack = parameters.getRawParameterValue("adsr_attack"); + sp.adsrDecay = parameters.getRawParameterValue("adsr_decay"); + sp.adsrSustain = parameters.getRawParameterValue("adsr_sustain"); + sp.adsrRelease = parameters.getRawParameterValue("adsr_release"); + + // === Filter Env === + parameters.addParameterListener("fenv_attack", this); + parameters.addParameterListener("fenv_decay", this); + parameters.addParameterListener("fenv_sustain", this); + parameters.addParameterListener("fenv_release", this); + parameters.addParameterListener("fenv_amount", this); + + sp.fenvAttack = parameters.getRawParameterValue("fenv_attack"); + sp.fenvDecay = parameters.getRawParameterValue("fenv_decay"); + sp.fenvSustain = parameters.getRawParameterValue("fenv_sustain"); + sp.fenvRelease = parameters.getRawParameterValue("fenv_release"); + sp.fenvAmount = parameters.getRawParameterValue("fenv_amount"); + + // === Filter base === + parameters.addParameterListener("filter_cutoff", this); + parameters.addParameterListener("filter_resonance", this); + parameters.addParameterListener("filter_type", this); + parameters.addParameterListener("filter_drive", this); + parameters.addParameterListener("filter_mod", this); + parameters.addParameterListener("filter_key", this); + + sp.filterCutoff = parameters.getRawParameterValue("filter_cutoff"); + sp.filterResonance = parameters.getRawParameterValue("filter_resonance"); + sp.filterType = parameters.getRawParameterValue("filter_type"); + sp.filterDrive = parameters.getRawParameterValue("filter_drive"); + sp.filterMod = parameters.getRawParameterValue("filter_mod"); + sp.filterKey = parameters.getRawParameterValue("filter_key"); + + // === Distortion === + parameters.addParameterListener("distortion_drive", this); + parameters.addParameterListener("distortion_mix", this); + parameters.addParameterListener("distortion_bias", this); + parameters.addParameterListener("distortion_tone", this); + parameters.addParameterListener("distortion_shape", this); + + sp.distortionDrive = parameters.getRawParameterValue("distortion_drive"); + sp.distortionMix = parameters.getRawParameterValue("distortion_mix"); + sp.distortionBias = parameters.getRawParameterValue("distortion_bias"); + sp.distortionTone = parameters.getRawParameterValue("distortion_tone"); + sp.distortionShape = parameters.getRawParameterValue("distortion_shape"); + + // === Master / EQ === + parameters.addParameterListener("master", this); + parameters.addParameterListener("lowEQ", this); + parameters.addParameterListener("midEQ", this); + parameters.addParameterListener("highEQ", this); + + sp.masterDbls = parameters.getRawParameterValue("master"); + sp.lowGainDbls = parameters.getRawParameterValue("lowEQ"); + sp.midGainDbls = parameters.getRawParameterValue("midEQ"); + sp.highGainDbls = parameters.getRawParameterValue("highEQ"); +} + +NeuralSynthAudioProcessor::~NeuralSynthAudioProcessor() = default; + +//============================================================================== +const juce::String NeuralSynthAudioProcessor::getName() const { return JucePlugin_Name; } + +bool NeuralSynthAudioProcessor::acceptsMidi() const +{ + #if JucePlugin_WantsMidiInput + return true; + #else + return false; + #endif +} + +bool NeuralSynthAudioProcessor::producesMidi() const +{ + #if JucePlugin_ProducesMidiOutput + return true; + #else + return false; + #endif +} + +bool NeuralSynthAudioProcessor::isMidiEffect() const +{ + #if JucePlugin_IsMidiEffect + return true; + #else + return false; + #endif +} + +double NeuralSynthAudioProcessor::getTailLengthSeconds() const { return 0.0; } + +int NeuralSynthAudioProcessor::getNumPrograms() { return 1; } +int NeuralSynthAudioProcessor::getCurrentProgram() { return 0; } +void NeuralSynthAudioProcessor::setCurrentProgram (int) {} +const juce::String NeuralSynthAudioProcessor::getProgramName (int) { return {}; } +void NeuralSynthAudioProcessor::changeProgramName (int, const juce::String&) {} + +//============================================================================== +void NeuralSynthAudioProcessor::prepareToPlay (double sampleRate, int samplesPerBlock) +{ + audioEngine.prepare({ sampleRate, (juce::uint32)samplesPerBlock, 2 }); + midiMessageCollector.reset(sampleRate); +} + +void NeuralSynthAudioProcessor::releaseResources() {} + +bool NeuralSynthAudioProcessor::isBusesLayoutSupported (const BusesLayout& layouts) const +{ + if (layouts.getMainOutputChannelSet() != juce::AudioChannelSet::mono() + && layouts.getMainOutputChannelSet() != juce::AudioChannelSet::stereo()) + return false; + + return true; +} + +void NeuralSynthAudioProcessor::processBlock(juce::AudioSampleBuffer& buffer, juce::MidiBuffer& midiMessages) +{ + const int newWaveform = sp.waveform.exchange(-1); + + if (newWaveform != -1) { + audioEngine.applyToVoices([newWaveform](NeuralSynthVoice* v) + { + v->changeWaveform(newWaveform); + }); + } + + juce::ScopedNoDenormals noDenormals; + auto totalNumInputChannels = getTotalNumInputChannels(); + auto totalNumOutputChannels = getTotalNumOutputChannels(); + + midiMessageCollector.removeNextBlockOfMessages(midiMessages, buffer.getNumSamples()); + + for (int i = totalNumInputChannels; i < totalNumOutputChannels; ++i) + buffer.clear(i, 0, buffer.getNumSamples()); + + audioEngine.renderNextBlock(buffer, midiMessages, 0, buffer.getNumSamples()); + scopeDataCollector.process(buffer.getReadPointer(0), (size_t)buffer.getNumSamples()); +} + +//============================================================================== +bool NeuralSynthAudioProcessor::hasEditor() const { return true; } + +juce::AudioProcessorEditor* NeuralSynthAudioProcessor::createEditor() +{ + return new NeuralSynthAudioProcessorEditor (*this); +} + +//============================================================================== +void NeuralSynthAudioProcessor::getStateInformation (juce::MemoryBlock& destData) { juce::ignoreUnused(destData); } +void NeuralSynthAudioProcessor::setStateInformation (const void* data, int sizeInBytes) { juce::ignoreUnused(data, sizeInBytes); } + +void NeuralSynthAudioProcessor::parameterChanged(const juce::String& id, float newValue) +{ + juce::ignoreUnused(newValue); + if (id == "waveform") + sp.waveform.store((int)newValue, std::memory_order_release); +} + +//============================================================================== +// This creates new instances of the plugin.. +juce::AudioProcessor* JUCE_CALLTYPE createPluginFilter() { return new NeuralSynthAudioProcessor(); } + +void NeuralSynthAudioProcessor::buildParams(std::vector>& params, const std::string& paramGroup) { + const auto& paramGroupSettings = PARAM_SETTINGS.at(paramGroup); + + for (const auto& [name, s] : paramGroupSettings) { + params.push_back(std::make_unique( + paramGroup + "_" + name, s.label, + juce::NormalisableRange(s.min, s.max, s.interval), + s.defValue)); + } +} + +juce::AudioProcessorValueTreeState::ParameterLayout NeuralSynthAudioProcessor::createParameterLayout() +{ + std::vector> params; + + params.push_back(std::make_unique( + "waveform", "Waveform", + juce::StringArray{ "Sine", "Saw", "Square", "Triangle" }, 0)); + + // Per-panel bypass toggles (default OFF) + params.push_back(std::make_unique("chorus_on", "Chorus On", false)); + params.push_back(std::make_unique("delay_on", "Delay On", false)); + params.push_back(std::make_unique("reverb_on", "Reverb On", false)); + params.push_back(std::make_unique("flanger_on", "Flanger On", false)); + params.push_back(std::make_unique("distortion_on", "Distortion On", false)); + params.push_back(std::make_unique("filter_on", "Filter On", false)); + params.push_back(std::make_unique("eq_on", "EQ On", false)); + + buildParams(params, "adsr"); + buildParams(params, "fenv"); + buildParams(params, "chorus"); + buildParams(params, "delay"); + buildParams(params, "reverb"); + buildParams(params, "flanger"); + buildParams(params, "distortion"); + buildParams(params, "filter"); + + params.push_back(std::make_unique("master", "Master", + juce::NormalisableRange(-24.0f, 24.0f, 0.1f), 0.1f)); + + params.push_back(std::make_unique("lowEQ", "Low Gain", + juce::NormalisableRange(-24.0f, 24.0f, 0.1f), 0.5f)); + params.push_back(std::make_unique("midEQ", "Mid EQ", + juce::NormalisableRange(-24.0f, 24.0f, 0.1f), 0.8f)); + params.push_back(std::make_unique("highEQ", "High EQ", + juce::NormalisableRange(-24.0f, 24.0f, 0.1f), 1.0f)); + + return { params.begin(), params.end() }; +} diff --git a/Source/PluginProcessor.h b/Source/PluginProcessor.h new file mode 100644 index 0000000..a599382 --- /dev/null +++ b/Source/PluginProcessor.h @@ -0,0 +1,90 @@ +#pragma once + +#include +#include "AudioBufferQueue.h" +#include "AudioEngine.h" +#include "ScopeDataCollector.h" +#include "NeuralSharedParams.h" + +//============================================================================== +// Processor +class NeuralSynthAudioProcessor : public juce::AudioProcessor, + private juce::AudioProcessorValueTreeState::Listener +{ +public: + NeuralSynthAudioProcessor(); + ~NeuralSynthAudioProcessor() override; + + // AudioProcessor overrides + void prepareToPlay(double sampleRate, int samplesPerBlock) override; + void releaseResources() override; + +#ifndef JucePlugin_PreferredChannelConfigurations + bool isBusesLayoutSupported(const BusesLayout& layouts) const override; +#endif + + void processBlock(juce::AudioBuffer&, juce::MidiBuffer&) override; + + // Editor + juce::AudioProcessorEditor* createEditor() override; + bool hasEditor() const override; + + // Info + const juce::String getName() const override; + bool acceptsMidi() const override; + bool producesMidi() const override; + bool isMidiEffect() const override; + double getTailLengthSeconds() const override; + + // Programs + int getNumPrograms() override; + int getCurrentProgram() override; + void setCurrentProgram(int index) override; + const juce::String getProgramName(int index) override; + void changeProgramName(int index, const juce::String& newName) override; + + // State + void getStateInformation(juce::MemoryBlock& destData) override; + void setStateInformation(const void* data, int sizeInBytes) override; + + // Parameters + void parameterChanged(const juce::String& id, float newValue) override; + void buildParams(std::vector>& params, + const std::string& paramGroup); + juce::AudioProcessorValueTreeState::ParameterLayout createParameterLayout(); + + // Utilities + juce::MidiMessageCollector& getMidiMessageCollector() noexcept { return midiMessageCollector; } + AudioBufferQueue& getAudioBufferQueue() noexcept { return audioBufferQueue; } + + AudioBufferQueue& getChorusAudioBufferQueue() noexcept { return chorusBufferQueue; } + AudioBufferQueue& getDelayAudioBufferQueue() noexcept { return delayBufferQueue; } + AudioBufferQueue& getReverbAudioBufferQueue() noexcept { return reverbBufferQueue; } + AudioBufferQueue& getFlangerAudioBufferQueue() noexcept { return flangerBufferQueue; } + AudioBufferQueue& getDistortionAudioBufferQueue() noexcept { return distortionBufferQueue; } + AudioBufferQueue& getFilterAudioBufferQueue() noexcept { return filterBufferQueue; } + + // Public members (by JUCE convention) + juce::MidiMessageCollector midiMessageCollector; + juce::AudioProcessorValueTreeState parameters; + +private: + JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (NeuralSynthAudioProcessor) + + // ---- IMPORTANT ORDER FIX ---- + // Objects are constructed in THIS order. 'sp' must come BEFORE audioEngine. + NeuralSharedParams sp; // <— construct first + NeuralAudioEngine audioEngine; // needs a valid reference to 'sp' + + // Meter/scope queues + AudioBufferQueue audioBufferQueue; + AudioBufferQueue chorusBufferQueue; + AudioBufferQueue delayBufferQueue; + AudioBufferQueue reverbBufferQueue; + AudioBufferQueue flangerBufferQueue; + AudioBufferQueue distortionBufferQueue; + AudioBufferQueue filterBufferQueue; + + // Scope collector (uses audioBufferQueue, so declare after it) + ScopeDataCollector scopeDataCollector { audioBufferQueue }; +}; diff --git a/Source/ScopeComponent.h b/Source/ScopeComponent.h new file mode 100644 index 0000000..5d4ec4d --- /dev/null +++ b/Source/ScopeComponent.h @@ -0,0 +1,102 @@ +#pragma once + +#include "AudioBufferQueue.h" + +//============================================================================== +template +class ScopeComponent : public juce::Component, + private juce::Timer +{ +public: + using Queue = AudioBufferQueue; + + //============================================================================== + ScopeComponent(Queue& queueToUse) + : audioBufferQueue(queueToUse) + { + sampleData.fill(SampleType(0)); + setFramesPerSecond(30); + } + + //============================================================================== + void setFramesPerSecond(int framesPerSecond) + { + jassert(framesPerSecond > 0 && framesPerSecond < 1000); + startTimerHz(framesPerSecond); + } + + //============================================================================== + void paint(juce::Graphics& g) override + { + g.fillAll(juce::Colours::black); + g.setColour(juce::Colours::white); + + auto area = getLocalBounds(); + auto h = (SampleType)area.getHeight(); + auto w = (SampleType)area.getWidth(); + + // Oscilloscope + auto scopeRect = juce::Rectangle{ SampleType(0), SampleType(0), w, h / 2 }; + plot(sampleData.data(), sampleData.size(), g, scopeRect, SampleType(1), h / 4); + + // Spectrum + auto spectrumRect = juce::Rectangle{ SampleType(0), h / 2, w, h / 2 }; + plot(spectrumData.data(), spectrumData.size() / 4, g, spectrumRect); + } + + //============================================================================== + void resized() override {} + +private: + //============================================================================== + Queue& audioBufferQueue; + std::array sampleData; + + juce::dsp::FFT fft{ Queue::order }; + using WindowFun = juce::dsp::WindowingFunction; + WindowFun windowFun{ (size_t)fft.getSize(), WindowFun::hann }; + std::array spectrumData; + + //============================================================================== + void timerCallback() override + { + audioBufferQueue.pop(sampleData.data()); + juce::FloatVectorOperations::copy(spectrumData.data(), sampleData.data(), (int)sampleData.size()); + + auto fftSize = (size_t)fft.getSize(); + + jassert(spectrumData.size() == 2 * fftSize); + windowFun.multiplyWithWindowingTable(spectrumData.data(), fftSize); + fft.performFrequencyOnlyForwardTransform(spectrumData.data()); + + static constexpr auto mindB = SampleType(-160); + static constexpr auto maxdB = SampleType(0); + + for (auto& s : spectrumData) + s = juce::jmap(juce::jlimit(mindB, maxdB, juce::Decibels::gainToDecibels(s) - juce::Decibels::gainToDecibels(SampleType(fftSize))), mindB, maxdB, SampleType(0), SampleType(1)); + + repaint(); + } + + //============================================================================== + static void plot(const SampleType* data, + size_t numSamples, + juce::Graphics& g, + juce::Rectangle rect, + SampleType scaler = SampleType(1), + SampleType offset = SampleType(0)) + { + auto w = rect.getWidth(); + auto h = rect.getHeight(); + auto right = rect.getRight(); + + auto center = rect.getBottom() - offset; + auto gain = h * scaler; + + for (size_t i = 1; i < numSamples; ++i) + g.drawLine({ juce::jmap(SampleType(i - 1), SampleType(0), SampleType(numSamples - 1), SampleType(right - w), SampleType(right)), + center - gain * data[i - 1], + juce::jmap(SampleType(i), SampleType(0), SampleType(numSamples - 1), SampleType(right - w), SampleType(right)), + center - gain * data[i] }); + } +}; \ No newline at end of file diff --git a/Source/ScopeDataCollector.h b/Source/ScopeDataCollector.h new file mode 100644 index 0000000..e5cd7c8 --- /dev/null +++ b/Source/ScopeDataCollector.h @@ -0,0 +1,62 @@ +#pragma once + +template +class ScopeDataCollector +{ +public: + //============================================================================== + ScopeDataCollector(AudioBufferQueue& queueToUse) + : audioBufferQueue(queueToUse) + { + } + + //============================================================================== + void process(const SampleType* data, size_t numSamples) + { + size_t index = 0; + + if (state == State::waitingForTrigger) + { + while (index++ < numSamples) + { + auto currentSample = *data++; + + if (currentSample >= triggerLevel && prevSample < triggerLevel) + { + numCollected = 0; + state = State::collecting; + break; + } + + prevSample = currentSample; + } + } + + if (state == State::collecting) + { + while (index++ < numSamples) + { + buffer[numCollected++] = *data++; + + if (numCollected == buffer.size()) + { + audioBufferQueue.push(buffer.data(), buffer.size()); + state = State::waitingForTrigger; + prevSample = SampleType(100); + break; + } + } + } + } + +private: + //============================================================================== + AudioBufferQueue& audioBufferQueue; + std::array::bufferSize> buffer; + size_t numCollected; + SampleType prevSample = SampleType(100); + + static constexpr auto triggerLevel = SampleType(0.05); + + enum class State { waitingForTrigger, collecting } state{ State::waitingForTrigger }; +}; \ No newline at end of file diff --git a/Source/SynthVoice.cpp b/Source/SynthVoice.cpp new file mode 100644 index 0000000..929f44c --- /dev/null +++ b/Source/SynthVoice.cpp @@ -0,0 +1,398 @@ +#include "SynthVoice.h" +#include + +//============================================================================== + +NeuralSynthVoice::NeuralSynthVoice (NeuralSharedParams& sp) + : shared (sp) {} + +//============================================================================== + +void NeuralSynthVoice::prepare (const juce::dsp::ProcessSpec& newSpec) +{ + spec = newSpec; + + // --- Oscillator + osc.prepare (spec.sampleRate); + setWaveform (0); // default to sine + + // --- Scratch buffer (IMPORTANT: allocate real memory) + tempBuffer.setSize ((int) spec.numChannels, (int) spec.maximumBlockSize, + false, false, true); + tempBlock = juce::dsp::AudioBlock (tempBuffer); + + // --- Prepare chain elements + chain.prepare (spec); + + // Set maximum delay sizes BEFORE runtime changes + { + // Flanger: up to 20 ms + auto& flanger = chain.get(); + const size_t maxFlangerDelay = (size_t) juce::jmax( + 1, (size_t) std::ceil (0.020 * spec.sampleRate)); + flanger.setMaximumDelayInSamples (maxFlangerDelay); + flanger.reset(); + } + { + // Simple delay: up to 2 s + auto& delay = chain.get(); + const size_t maxDelay = (size_t) juce::jmax( + 1, (size_t) std::ceil (2.0 * spec.sampleRate)); + delay.setMaximumDelayInSamples (maxDelay); + delay.reset(); + } + + // Envelopes + adsr.setSampleRate (spec.sampleRate); + filterAdsr.setSampleRate (spec.sampleRate); + + // Filter + svf.reset(); + svf.prepare (spec); + + // Initial filter type + const int type = (int) std::lround (juce::jlimit (0.0f, 2.0f, + shared.filterType ? shared.filterType->load() : 0.0f)); + switch (type) + { + case 0: svf.setType (juce::dsp::StateVariableTPTFilterType::lowpass); break; + case 1: svf.setType (juce::dsp::StateVariableTPTFilterType::highpass); break; + case 2: svf.setType (juce::dsp::StateVariableTPTFilterType::bandpass); break; + default: break; + } +} + +//============================================================================== + +void NeuralSynthVoice::renderNextBlock (juce::AudioBuffer& outputBuffer, + int startSample, int numSamples) +{ + if (numSamples <= 0) + return; + + if (! adsr.isActive()) + clearCurrentNote(); + + // Apply pending waveform change (from GUI / processor thread) + const int wf = pendingWaveform.exchange (-1, std::memory_order_acq_rel); + if (wf != -1) + setWaveform (wf); + + // --- Generate oscillator into temp buffer + tempBuffer.clear(); + const int numCh = juce::jmin ((int) spec.numChannels, tempBuffer.getNumChannels()); + + for (int i = 0; i < numSamples; ++i) + { + const float s = osc.process(); + for (int ch = 0; ch < numCh; ++ch) + tempBuffer.getWritePointer (ch)[i] = s; + } + + auto block = tempBlock.getSubBlock (0, (size_t) numSamples); + + // ================================================================ + // Flanger (pre-filter) – manual per-sample to set varying delay + // ================================================================ + { + auto& flanger = chain.get(); + + const bool enabled = shared.flangerOn && shared.flangerOn->load() > 0.5f; + if (enabled) + { + const float rate = shared.flangerRate ? shared.flangerRate->load() : 0.0f; + float lfoPhase = shared.flangerPhase ? shared.flangerPhase->load() : 0.0f; + const float flangerDepth = shared.flangerDepth ? shared.flangerDepth->load() : 0.0f; // ms + const float mix = shared.flangerDryMix ? shared.flangerDryMix->load() : 0.0f; + const float feedback = shared.flangerFeedback ? shared.flangerFeedback->load() : 0.0f; + const float baseDelayMs = shared.flangerDelay ? shared.flangerDelay->load() : 0.25f; + + for (int i = 0; i < numSamples; ++i) + { + const float in = tempBuffer.getReadPointer (0)[i]; + + const float lfo = std::sin (lfoPhase); + const float delayMs = baseDelayMs + 0.5f * (1.0f + lfo) * flangerDepth; + const float delaySamples = juce::jmax (0.0f, delayMs * 0.001f * (float) spec.sampleRate); + + flanger.setDelay (delaySamples); + + const float delayed = flanger.popSample (0); + flanger.pushSample (0, in + delayed * feedback); + + const float out = in * (1.0f - mix) + delayed * mix; + for (int ch = 0; ch < numCh; ++ch) + tempBuffer.getWritePointer (ch)[i] = out; + + lfoPhase += juce::MathConstants::twoPi * rate / (float) spec.sampleRate; + if (lfoPhase > juce::MathConstants::twoPi) + lfoPhase -= juce::MathConstants::twoPi; + } + } + } + + // ================================================================ + // Filter with per-sample ADSR modulation (poly) + // ================================================================ + { + const bool enabled = shared.filterOn && shared.filterOn->load() > 0.5f; + + // Update filter type every block (cheap) + const int ftype = (int) std::lround (juce::jlimit (0.0f, 2.0f, + shared.filterType ? shared.filterType->load() : 0.0f)); + switch (ftype) + { + case 0: svf.setType (juce::dsp::StateVariableTPTFilterType::lowpass); break; + case 1: svf.setType (juce::dsp::StateVariableTPTFilterType::highpass); break; + case 2: svf.setType (juce::dsp::StateVariableTPTFilterType::bandpass); break; + default: break; + } + + const float qOrRes = juce::jlimit (0.1f, 10.0f, + shared.filterResonance ? shared.filterResonance->load() : 0.7f); + svf.setResonance (qOrRes); + + const float baseCutoff = juce::jlimit (20.0f, 20000.0f, + shared.filterCutoff ? shared.filterCutoff->load() : 1000.0f); + const float envAmt = shared.fenvAmount ? shared.fenvAmount->load() : 0.0f; + + for (int i = 0; i < numSamples; ++i) + { + const float envVal = filterAdsr.getNextSample(); + const float cutoff = juce::jlimit (20.0f, 20000.0f, + baseCutoff * std::pow (2.0f, envAmt * envVal)); + svf.setCutoffFrequency (cutoff); + + if (enabled) + { + for (int ch = 0; ch < numCh; ++ch) + { + float x = tempBuffer.getSample (ch, i); + x = svf.processSample (ch, x); + tempBuffer.setSample (ch, i, x); + } + } + } + } + + // ================================================================ + // Chorus + // ================================================================ + if (shared.chorusOn && shared.chorusOn->load() > 0.5f) + { + auto& chorus = chain.get(); + if (shared.chorusCentre) chorus.setCentreDelay (shared.chorusCentre->load()); + if (shared.chorusDepth) chorus.setDepth (shared.chorusDepth->load()); + if (shared.chorusFeedback) chorus.setFeedback (shared.chorusFeedback->load()); + if (shared.chorusMix) chorus.setMix (shared.chorusMix->load()); + if (shared.chorusRate) chorus.setRate (shared.chorusRate->load()); + + chain.get().process (juce::dsp::ProcessContextReplacing (block)); + } + + // ================================================================ + // Simple Delay (per-voice) + // ================================================================ + if (shared.delayOn && shared.delayOn->load() > 0.5f) + { + auto& delay = chain.get(); + const float time = shared.delayTime ? shared.delayTime->load() : 0.1f; + delay.setDelay (juce::jmax (0.0f, time * (float) spec.sampleRate)); + delay.process (juce::dsp::ProcessContextReplacing (block)); + } + + // ================================================================ + // Reverb + // ================================================================ + if (shared.reverbOn && shared.reverbOn->load() > 0.5f) + { + juce::Reverb::Parameters rp; + rp.damping = shared.reverbDamping ? shared.reverbDamping->load() : 0.0f; + rp.dryLevel = shared.reverbDryLevel ? shared.reverbDryLevel->load() : 0.0f; + rp.freezeMode = shared.reverbFreezeMode ? shared.reverbFreezeMode->load() : 0.0f; + rp.roomSize = shared.reverbRoomSize ? shared.reverbRoomSize->load() : 0.0f; + rp.wetLevel = shared.reverbWetLevel ? shared.reverbWetLevel->load() : 0.0f; + rp.width = shared.reverbWidth ? shared.reverbWidth->load() : 0.0f; + + chain.get().setParameters (rp); + chain.get().process (juce::dsp::ProcessContextReplacing (block)); + } + + // ================================================================ + // Distortion + tone (post LPF/Peak) + // ================================================================ + { + const float driveDb = shared.distortionDrive ? shared.distortionDrive->load() : 0.0f; + const float bias = juce::jlimit (-1.0f, 1.0f, shared.distortionBias ? shared.distortionBias->load() : 0.0f); + const float toneHz = juce::jlimit (100.0f, 8000.0f, shared.distortionTone ? shared.distortionTone->load() : 3000.0f); + const int shape = (int) std::lround (juce::jlimit (0.0f, 2.0f, + shared.distortionShape ? shared.distortionShape->load() : 0.0f)); + const float mix = shared.distortionMix ? shared.distortionMix->load() : 0.0f; + + auto& pre = chain.get(); + auto& sh = chain.get(); + auto& tone = chain.get(); + + pre.setGainDecibels (driveDb); + + // Explicit std::function target (works on MSVC) + if (shape == 0) sh.functionToUse = std::function{ [bias](float x) noexcept { return std::tanh (x + bias); } }; + else if (shape == 1) sh.functionToUse = std::function{ [bias](float x) noexcept { return juce::jlimit (-1.0f, 1.0f, x + bias); } }; + else sh.functionToUse = std::function{ [bias](float x) noexcept { return std::atan (x + bias) * (2.0f / juce::MathConstants::pi); } }; + + tone.coefficients = juce::dsp::IIR::Coefficients::makePeakFilter ( + spec.sampleRate, toneHz, 0.707f, + juce::Decibels::decibelsToGain (shared.highGainDbls ? shared.highGainDbls->load() : 0.0f)); + + if (shared.distortionOn && shared.distortionOn->load() > 0.5f) + { + // Wet/dry blend around the shaper + juce::AudioBuffer dryCopy (tempBuffer.getNumChannels(), numSamples); + for (int ch = 0; ch < numCh; ++ch) + dryCopy.copyFrom (ch, 0, tempBuffer, ch, 0, numSamples); + + // pre -> shaper -> tone + pre.process (juce::dsp::ProcessContextReplacing (block)); + sh.process (juce::dsp::ProcessContextReplacing (block)); + tone.process (juce::dsp::ProcessContextReplacing (block)); + + const float wet = mix, dry = 1.0f - mix; + for (int ch = 0; ch < numCh; ++ch) + { + auto* d = dryCopy.getReadPointer (ch); + auto* w = tempBuffer.getWritePointer (ch); + for (int i = 0; i < numSamples; ++i) + w[i] = dry * d[i] + wet * w[i]; + } + } + } + + // ================================================================ + // EQ + Master + Limiter (EQ guarded by eqOn) + // ================================================================ + { + const bool eqEnabled = shared.eqOn && shared.eqOn->load() > 0.5f; + + auto& eqL = chain.get(); + auto& eqM = chain.get(); + auto& eqH = chain.get(); + + if (eqEnabled) + { + eqL.coefficients = juce::dsp::IIR::Coefficients::makeLowShelf ( + spec.sampleRate, 100.0f, 0.707f, + juce::Decibels::decibelsToGain (shared.lowGainDbls ? shared.lowGainDbls->load() : 0.0f)); + + eqM.coefficients = juce::dsp::IIR::Coefficients::makePeakFilter ( + spec.sampleRate, 1000.0f, 1.0f, + juce::Decibels::decibelsToGain (shared.midGainDbls ? shared.midGainDbls->load() : 0.0f)); + + eqH.coefficients = juce::dsp::IIR::Coefficients::makePeakFilter ( + spec.sampleRate, 10000.0f, 0.707f, + juce::Decibels::decibelsToGain (shared.highGainDbls ? shared.highGainDbls->load() : 0.0f)); + + eqL.process (juce::dsp::ProcessContextReplacing (block)); + eqM.process (juce::dsp::ProcessContextReplacing (block)); + eqH.process (juce::dsp::ProcessContextReplacing (block)); + } + + chain.get().setGainDecibels (shared.masterDbls ? shared.masterDbls->load() : 0.0f); + chain.get().process (juce::dsp::ProcessContextReplacing (block)); + + chain.get().process (juce::dsp::ProcessContextReplacing (block)); + } + + // ================================================================ + // Apply AMP ADSR envelope + // ================================================================ + { + juce::AudioBuffer buf (tempBuffer.getArrayOfWritePointers(), numCh, numSamples); + adsr.applyEnvelopeToBuffer (buf, 0, numSamples); + } + + // Mix into output + juce::dsp::AudioBlock (outputBuffer) + .getSubBlock ((size_t) startSample, (size_t) numSamples) + .add (block); +} + +//============================================================================== + +void NeuralSynthVoice::noteStarted() +{ + const float freqHz = (float) getCurrentlyPlayingNote().getFrequencyInHertz(); + + // Oscillator frequency and phase retrigger + osc.setFrequency (freqHz); + osc.resetPhase (0.0f); + + // Chorus snapshot + if (shared.chorusCentre) chain.get().setCentreDelay (shared.chorusCentre->load()); + if (shared.chorusDepth) chain.get().setDepth (shared.chorusDepth->load()); + if (shared.chorusFeedback) chain.get().setFeedback (shared.chorusFeedback->load()); + if (shared.chorusMix) chain.get().setMix (shared.chorusMix->load()); + if (shared.chorusRate) chain.get().setRate (shared.chorusRate->load()); + + // Delay time (in samples) + if (shared.delayTime) + chain.get().setDelay (juce::jmax (0.0f, shared.delayTime->load() * (float) spec.sampleRate)); + + // Reverb snapshot + juce::Reverb::Parameters rp; + rp.damping = shared.reverbDamping ? shared.reverbDamping->load() : 0.0f; + rp.dryLevel = shared.reverbDryLevel ? shared.reverbDryLevel->load() : 0.0f; + rp.freezeMode = shared.reverbFreezeMode ? shared.reverbFreezeMode->load() : 0.0f; + rp.roomSize = shared.reverbRoomSize ? shared.reverbRoomSize->load() : 0.0f; + rp.wetLevel = shared.reverbWetLevel ? shared.reverbWetLevel->load() : 0.0f; + rp.width = shared.reverbWidth ? shared.reverbWidth->load() : 0.0f; + chain.get().setParameters (rp); + + // Amp ADSR + juce::ADSR::Parameters ap; + ap.attack = shared.adsrAttack ? shared.adsrAttack->load() : 0.01f; + ap.decay = shared.adsrDecay ? shared.adsrDecay->load() : 0.10f; + ap.sustain = shared.adsrSustain ? shared.adsrSustain->load() : 0.80f; + ap.release = shared.adsrRelease ? shared.adsrRelease->load() : 0.40f; + adsr.setParameters (ap); + adsr.noteOn(); + + // Filter ADSR + juce::ADSR::Parameters fp; + fp.attack = shared.fenvAttack ? shared.fenvAttack->load() : 0.01f; + fp.decay = shared.fenvDecay ? shared.fenvDecay->load() : 0.10f; + fp.sustain = shared.fenvSustain ? shared.fenvSustain->load() : 0.80f; + fp.release = shared.fenvRelease ? shared.fenvRelease->load() : 0.40f; + filterAdsr.setParameters (fp); + filterAdsr.noteOn(); +} + +//============================================================================== + +void NeuralSynthVoice::notePitchbendChanged() +{ + const float freqHz = (float) getCurrentlyPlayingNote().getFrequencyInHertz(); + osc.setFrequency (freqHz); +} + +//============================================================================== + +void NeuralSynthVoice::noteStopped (bool allowTailOff) +{ + juce::ignoreUnused (allowTailOff); + adsr.noteOff(); + filterAdsr.noteOff(); +} + +//============================================================================== + +void NeuralSynthVoice::setWaveform (int waveformType) +{ + switch (juce::jlimit (0, 3, waveformType)) + { + case 0: osc.setWave (BlepWave::Sine); break; + case 1: osc.setWave (BlepWave::Saw); break; + case 2: osc.setWave (BlepWave::Square); break; + case 3: osc.setWave (BlepWave::Triangle); break; + default: osc.setWave (BlepWave::Sine); break; + } +} diff --git a/Source/SynthVoice.h b/Source/SynthVoice.h new file mode 100644 index 0000000..00842bd --- /dev/null +++ b/Source/SynthVoice.h @@ -0,0 +1,97 @@ +#pragma once +#include +#include // <-- for std::function used by WaveShaper +#include "NeuralSharedParams.h" +#include "BlepOsc.h" + +//============================================================================== +// A single polyBLEP oscillator voice with per-voice ADSR, filter ADSR, +// flanger (delayline), simple delay, chorus, reverb, distortion, EQ, master. +class NeuralSynthVoice : public juce::MPESynthesiserVoice +{ +public: + explicit NeuralSynthVoice (NeuralSharedParams& sharedParams); + + // JUCE voice API + void prepare (const juce::dsp::ProcessSpec& spec); + void renderNextBlock (juce::AudioBuffer& outputBuffer, + int startSample, int numSamples) override; + + void noteStarted() override; + void noteStopped (bool allowTailOff) override; + void notePitchbendChanged() override; + + void notePressureChanged() override {} + void noteTimbreChanged() override {} + void noteKeyStateChanged() override {} + + // Called from the processor when the GUI waveform param changes + void changeWaveform (int wf) { setWaveform (wf); } + +private: + void setWaveform (int waveformType); + + //=== Processing chain (without oscillator) =============================== + using DelayLine = juce::dsp::DelayLine; + using IIR = juce::dsp::IIR::Filter; + using Gain = juce::dsp::Gain; + using WaveShaper = juce::dsp::WaveShaper>; // <-- fix + using Chorus = juce::dsp::Chorus; + using Reverb = juce::dsp::Reverb; + using Limiter = juce::dsp::Limiter; + + enum ChainIndex + { + flangerIndex = 0, + delayIndex, + chorusIndex, + reverbIndex, + distortionPreGain, + distortionIndex, + distortionPostLPF, + eqLowIndex, + eqMidIndex, + eqHighIndex, + masterIndex, + limiterIndex + }; + + using Chain = juce::dsp::ProcessorChain< + DelayLine, // flanger + DelayLine, // simple delay + Chorus, // chorus + Reverb, // reverb + Gain, // distortion pre-gain (drive) + WaveShaper, // distortion waveshaper + IIR, // tone / post-EQ for distortion + IIR, // EQ low + IIR, // EQ mid + IIR, // EQ high + Gain, // master gain + Limiter // safety limiter + >; + +private: + NeuralSharedParams& shared; + + juce::dsp::ProcessSpec spec {}; + + // ==== Oscillator (polyBLEP) ============================================ + BlepOsc osc; + std::atomic pendingWaveform {-1}; // set by changeWaveform() + + // ==== Envelopes & Filter =============================================== + juce::ADSR adsr; + juce::ADSR filterAdsr; + juce::dsp::StateVariableTPTFilter svf; + + // ==== Chain (FX, EQ, master, limiter) ================================== + Chain chain; + + // ==== Scratch buffer (properly allocated) =============================== + juce::AudioBuffer tempBuffer; + juce::dsp::AudioBlock tempBlock; + + JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (NeuralSynthVoice) +}; diff --git a/Source/WavetableOsc.h b/Source/WavetableOsc.h new file mode 100644 index 0000000..c36f378 --- /dev/null +++ b/Source/WavetableOsc.h @@ -0,0 +1,261 @@ +#pragma once +#include +#include +#include + +// ============================== Design ======================================= +// - Bank with F frames, each frame is a single-cycle table of N samples. +// - For each frame, we create L mip-levels: level 0 = full bandwidth, +// level l halves the permitted harmonics (spectral truncation). +// - Runtime chooses level from note frequency and sampleRate, then morphs +// between adjacent frames and crossfades between the two nearest levels. +// - Table read uses linear interpolation (cheap and good enough with N>=2048). + +namespace WT +{ + // Utility: complex array wrapper for JUCE FFT (interleaved real/imag floats) + struct ComplexBuf + { + std::vector data; // size = 2 * N + explicit ComplexBuf(size_t N = 0) { resize(N); } + void resize(size_t N) { data.assign(2 * N, 0.0f); } + juce::dsp::Complex* asComplex() { return reinterpret_cast*>(data.data()); } + }; + + // ======================================================================= + // WavetableBank: holds raw frames + mipmapped versions + // ======================================================================= + class Bank + { + public: + // N = table length (must be power-of-two for FFT), frames = number of morph frames + // mipLevels = how many spectral levels (>=1). 5 ~ 6 is plenty for synth use. + Bank(size_t N = 2048, int frames = 16, int mipLevels = 6) + : tableSize(N), numFrames(frames), numLevels(mipLevels), + fft((int)std::log2((double)N)) + { + jassert(juce::isPowerOfTwo((int)N)); + tables.resize((size_t)numLevels); + for (int l = 0; l < numLevels; ++l) + tables[(size_t)l].resize((size_t)numFrames, std::vector(tableSize, 0.0f)); + } + + size_t getSize() const { return tableSize; } + int getFrames() const { return numFrames; } + int getLevels() const { return numLevels; } + + // Provide raw “design” frames (time-domain single-cycle) then call buildMipmaps(). + // framesRaw.size() must equal numFrames, each frame length must equal tableSize. + void setRawFrames(const std::vector>& framesRaw) + { + jassert((int)framesRaw.size() == numFrames); + for (const auto& f : framesRaw) jassert(f.size() == tableSize); + raw = framesRaw; + } + + // Convenience: generate 16-frame bank morphing Sine -> Saw -> Square -> Triangle + void generateDefaultMorph() + { + std::vector> frames; + frames.resize((size_t)numFrames, std::vector(tableSize, 0.0f)); + + auto fill = [&](int idx, auto func) + { + auto& t = frames[(size_t)idx]; + for (size_t n = 0; n < tableSize; ++n) + { + const float ph = (float) (juce::MathConstants::twoPi * (double)n / (double)tableSize); + t[n] = func(ph); + } + normalise(t); + }; + + // helper waves + auto sine = [](float ph) { return std::sin(ph); }; + auto saw = [](float ph) { return (float)(2.0 * (ph / juce::MathConstants::twoPi) - 1.0); }; + auto sq = [](float ph) { return ph < juce::MathConstants::pi ? 1.0f : -1.0f; }; + auto tri = [](float ph) { + float v = (float)(2.0 * std::abs(2.0 * (ph / juce::MathConstants::twoPi) - 1.0) - 1.0); + return v; + }; + + // 0..5: sine->saw, 6..10: saw->square, 11..15: square->triangle + const int F = numFrames; + for (int i = 0; i < F; ++i) + { + const float t = (float) i / (float) juce::jmax(1, F - 1); + std::function a, b; + float mix = 0.0f; + + if (i <= 5) { a = sine; b = saw; mix = (float)i / 5.0f; } + else if (i <=10) { a = saw; b = sq; mix = (float)(i - 6) / 4.0f; } + else { a = sq; b = tri; mix = (float)(i - 11) / 4.0f; } + + fill(i, [=](float ph){ return (1.0f - mix) * a(ph) + mix * b(ph); }); + } + + setRawFrames(frames); + } + + // Build mip-levels by FFT → spectral truncation → IFFT + void buildMipmaps() + { + jassert(!raw.empty()); + ComplexBuf freq(tableSize); + ComplexBuf time(tableSize); + + for (int f = 0; f < numFrames; ++f) + { + // Forward FFT of raw frame + std::fill(freq.data.begin(), freq.data.end(), 0.0f); + for (size_t n = 0; n < tableSize; ++n) + { + time.data[2 * n + 0] = raw[(size_t)f][n]; + time.data[2 * n + 1] = 0.0f; + } + fft.performRealOnlyForwardTransform(time.data.data()); + // After JUCE real FFT, bins are laid out as: Re[0], Re[N/2], Re[1], Im[1], Re[2], Im[2], ... + // We'll reconstruct complex bins for easy masking. + + // Helper to zero all harmonics above kMax (inclusive index in [0..N/2]) + auto maskAndIFFT = [&](int level, int kMax) + { + // Copy time.data into working complex bins + auto* bins = freq.asComplex(); + // DC & Nyquist are purely real in real-FFT + bins[0].real (time.data[0]); + bins[0].imag (0.0f); + bins[tableSize/2].real (time.data[1]); + bins[tableSize/2].imag (0.0f); + + // Rebuild the rest (Re[k], Im[k]) packed starting at index 2 + for (size_t k = 1; k < tableSize/2; ++k) + { + bins[k].real (time.data[2 * k + 0]); + bins[k].imag (time.data[2 * k + 1]); + } + + // Mask + for (size_t k = (size_t)kMax + 1; k < tableSize/2; ++k) + bins[k] = { 0.0f, 0.0f }; + + // Pack back into real-FFT layout for inverse + time.data[0] = bins[0].real(); // DC + time.data[1] = bins[tableSize/2].real(); // Nyquist + for (size_t k = 1; k < tableSize/2; ++k) + { + time.data[2 * k + 0] = bins[k].real(); + time.data[2 * k + 1] = bins[k].imag(); + } + + // IFFT + fft.performRealOnlyInverseTransform(time.data.data()); + + // Copy, normalise a little (scale JUCE inverse divides by N already) + auto& dst = tables[(size_t)level][(size_t)f]; + for (size_t n = 0; n < tableSize; ++n) + dst[n] = time.data[2 * n + 0]; + + normalise(dst); + }; + + // Level 0 → all harmonics available up to N/2 - 1 + for (int l = 0; l < numLevels; ++l) + { + const int maxH = (int)((tableSize / 2) >> l); // halve per level + const int kMax = juce::jmax(1, juce::jmin(maxH, (int)tableSize/2 - 1)); + maskAndIFFT(l, kMax); + } + } + } + + // sample at (frame, level, phase in [0,1)) + inline float lookup (float frameIdx, int level, float phase) const noexcept + { + const int f0 = juce::jlimit(0, numFrames - 1, (int)std::floor(frameIdx)); + const int f1 = juce::jlimit(0, numFrames - 1, f0 + 1); + const float t = juce::jlimit(0.0f, 1.0f, frameIdx - (float)f0); + + const auto& T0 = tables[(size_t)level][(size_t)f0]; + const auto& T1 = tables[(size_t)level][(size_t)f1]; + + const float pos = phase * (float)tableSize; + const int i0 = (int) std::floor(pos) & (int)(tableSize - 1); + const int i1 = (i0 + 1) & (int)(tableSize - 1); + const float a = pos - (float) std::floor(pos); + + const float s0 = juce::jmap(a, T0[(size_t)i0], T0[(size_t)i1]); + const float s1 = juce::jmap(a, T1[(size_t)i0], T1[(size_t)i1]); + return juce::jmap(t, s0, s1); + } + + // choose mip-level for given frequency (Hz) & sampleRate + inline int chooseLevel (float freq, double sampleRate) const noexcept + { + // permitted harmonics at this pitch: + const float maxH = (float) (0.5 * sampleRate / juce::jmax(1.0f, freq)); + // level so that harmonic budget of level >= maxH, i.e. l = ceil(log2((N/2)/maxH)) + const float base = (float)(tableSize * 0.5); + const float ratio = base / juce::jmax(1.0f, maxH); + int l = (int) std::ceil (std::log2 (ratio)); + return juce::jlimit (0, numLevels - 1, l); + } + + static void normalise (std::vector& t) + { + float mx = 0.0f; + for (float v : t) mx = juce::jmax(mx, std::abs(v)); + if (mx < 1.0e-6f) return; + for (float& v : t) v /= mx; + } + + private: + size_t tableSize; + int numFrames; + int numLevels; + + juce::dsp::FFT fft; + std::vector> raw; + // [level][frame][sample] + std::vector>> tables; + }; + + // ======================================================================= + // Wavetable Oscillator + // ======================================================================= + class Osc + { + public: + void prepare (double sr) { sampleRate = sr; } + void setBank (std::shared_ptr b) { bank = std::move(b); } + void setFrequency (float f) { freq = juce::jmax(0.0f, f); phaseInc = freq / (float)sampleRate; } + void setMorph (float m) { morph = m; } // 0..frames-1 (continuous) + void resetPhase (float p = 0.0f) { phase = juce::jlimit(0.0f, 1.0f, p); } + + float process() + { + if (!bank) return 0.0f; + + const int l0 = bank->chooseLevel(freq, sampleRate); + const int l1 = juce::jmin(l0 + 1, bank->getLevels() - 1); + const float preferL0 = 1.0f - juce::jlimit(0.0f, 1.0f, + (float)l0 - (float)bank->chooseLevel(freq * 0.99f, sampleRate)); + + const float s0 = bank->lookup(morph, l0, phase); + const float s1 = bank->lookup(morph, l1, phase); + const float out = juce::jmap(preferL0, s1, s0); // simple crossfade + + phase += phaseInc; + while (phase >= 1.0f) phase -= 1.0f; + return out; + } + + private: + std::shared_ptr bank; + double sampleRate { 44100.0 }; + float freq { 0.0f }; + float morph { 0.0f }; // 0..frames-1 + float phase { 0.0f }; + float phaseInc { 0.0f }; + }; +} // namespace WT