Compare commits

..

2 Commits

Author SHA1 Message Date
Roboboffin
3ac2cc15b1 Fix line endings and some cleanup 2025-10-23 04:00:21 +01:00
Roboboffin
24eb354ace Normalize line endings per .gitattributes 2025-10-23 03:59:37 +01:00
63 changed files with 2634 additions and 2572 deletions

39
.gitattributes vendored Normal file
View File

@@ -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

2
.gitignore vendored
View File

@@ -1,2 +1,4 @@
Builds Builds
NeuralSynth Installer.exe NeuralSynth Installer.exe
AudioPluginHost
Projucer

19
AGENTS.md Normal file
View File

@@ -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 JUCEs `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.

View File

@@ -1,12 +1,12 @@
Important Note!! Important Note!!
================ ================
The purpose of this folder is to contain files that are auto-generated by the Projucer, 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 and ALL files in this folder will be mercilessly DELETED and completely re-written whenever
the Projucer saves your project. the Projucer saves your project.
Therefore, it's a bad idea to make any manual changes to the files in here, or to 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 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 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). modifications after the Projucer has saved its changes).

View File

@@ -1,8 +1,8 @@
/* /*
IMPORTANT! This file is auto-generated each time you save your IMPORTANT! This file is auto-generated each time you save your
project - if you alter its contents, your changes may be overwritten! project - if you alter its contents, your changes may be overwritten!
*/ */
#include <juce_audio_basics/juce_audio_basics.cpp> #include <juce_audio_basics/juce_audio_basics.cpp>

View File

@@ -1,8 +1,8 @@
/* /*
IMPORTANT! This file is auto-generated each time you save your IMPORTANT! This file is auto-generated each time you save your
project - if you alter its contents, your changes may be overwritten! project - if you alter its contents, your changes may be overwritten!
*/ */
#include <juce_audio_basics/juce_audio_basics.mm> #include <juce_audio_basics/juce_audio_basics.mm>

View File

@@ -1,8 +1,8 @@
/* /*
IMPORTANT! This file is auto-generated each time you save your IMPORTANT! This file is auto-generated each time you save your
project - if you alter its contents, your changes may be overwritten! project - if you alter its contents, your changes may be overwritten!
*/ */
#include <juce_audio_devices/juce_audio_devices.cpp> #include <juce_audio_devices/juce_audio_devices.cpp>

View File

@@ -1,8 +1,8 @@
/* /*
IMPORTANT! This file is auto-generated each time you save your IMPORTANT! This file is auto-generated each time you save your
project - if you alter its contents, your changes may be overwritten! project - if you alter its contents, your changes may be overwritten!
*/ */
#include <juce_audio_devices/juce_audio_devices.mm> #include <juce_audio_devices/juce_audio_devices.mm>

View File

@@ -1,8 +1,8 @@
/* /*
IMPORTANT! This file is auto-generated each time you save your IMPORTANT! This file is auto-generated each time you save your
project - if you alter its contents, your changes may be overwritten! project - if you alter its contents, your changes may be overwritten!
*/ */
#include <juce_audio_formats/juce_audio_formats.cpp> #include <juce_audio_formats/juce_audio_formats.cpp>

View File

@@ -1,8 +1,8 @@
/* /*
IMPORTANT! This file is auto-generated each time you save your IMPORTANT! This file is auto-generated each time you save your
project - if you alter its contents, your changes may be overwritten! project - if you alter its contents, your changes may be overwritten!
*/ */
#include <juce_audio_formats/juce_audio_formats.mm> #include <juce_audio_formats/juce_audio_formats.mm>

View File

@@ -1,8 +1,8 @@
/* /*
IMPORTANT! This file is auto-generated each time you save your IMPORTANT! This file is auto-generated each time you save your
project - if you alter its contents, your changes may be overwritten! project - if you alter its contents, your changes may be overwritten!
*/ */
#include <juce_audio_plugin_client/juce_audio_plugin_client_AAX.cpp> #include <juce_audio_plugin_client/juce_audio_plugin_client_AAX.cpp>

View File

@@ -1,8 +1,8 @@
/* /*
IMPORTANT! This file is auto-generated each time you save your IMPORTANT! This file is auto-generated each time you save your
project - if you alter its contents, your changes may be overwritten! project - if you alter its contents, your changes may be overwritten!
*/ */
#include <juce_audio_plugin_client/juce_audio_plugin_client_AAX.mm> #include <juce_audio_plugin_client/juce_audio_plugin_client_AAX.mm>

View File

@@ -1,8 +1,8 @@
/* /*
IMPORTANT! This file is auto-generated each time you save your IMPORTANT! This file is auto-generated each time you save your
project - if you alter its contents, your changes may be overwritten! project - if you alter its contents, your changes may be overwritten!
*/ */
#include <juce_audio_plugin_client/juce_audio_plugin_client_AAX_utils.cpp> #include <juce_audio_plugin_client/juce_audio_plugin_client_AAX_utils.cpp>

View File

@@ -1,8 +1,8 @@
/* /*
IMPORTANT! This file is auto-generated each time you save your IMPORTANT! This file is auto-generated each time you save your
project - if you alter its contents, your changes may be overwritten! project - if you alter its contents, your changes may be overwritten!
*/ */
#include <juce_audio_plugin_client/juce_audio_plugin_client_ARA.cpp> #include <juce_audio_plugin_client/juce_audio_plugin_client_ARA.cpp>

View File

@@ -1,8 +1,8 @@
/* /*
IMPORTANT! This file is auto-generated each time you save your IMPORTANT! This file is auto-generated each time you save your
project - if you alter its contents, your changes may be overwritten! project - if you alter its contents, your changes may be overwritten!
*/ */
#include <juce_audio_plugin_client/juce_audio_plugin_client_AU_1.mm> #include <juce_audio_plugin_client/juce_audio_plugin_client_AU_1.mm>

View File

@@ -1,8 +1,8 @@
/* /*
IMPORTANT! This file is auto-generated each time you save your IMPORTANT! This file is auto-generated each time you save your
project - if you alter its contents, your changes may be overwritten! project - if you alter its contents, your changes may be overwritten!
*/ */
#include <juce_audio_plugin_client/juce_audio_plugin_client_AU_2.mm> #include <juce_audio_plugin_client/juce_audio_plugin_client_AU_2.mm>

View File

@@ -1,8 +1,8 @@
/* /*
IMPORTANT! This file is auto-generated each time you save your IMPORTANT! This file is auto-generated each time you save your
project - if you alter its contents, your changes may be overwritten! project - if you alter its contents, your changes may be overwritten!
*/ */
#include <juce_audio_plugin_client/juce_audio_plugin_client_AUv3.mm> #include <juce_audio_plugin_client/juce_audio_plugin_client_AUv3.mm>

View File

@@ -1,8 +1,8 @@
/* /*
IMPORTANT! This file is auto-generated each time you save your IMPORTANT! This file is auto-generated each time you save your
project - if you alter its contents, your changes may be overwritten! project - if you alter its contents, your changes may be overwritten!
*/ */
#include <juce_audio_plugin_client/juce_audio_plugin_client_LV2.cpp> #include <juce_audio_plugin_client/juce_audio_plugin_client_LV2.cpp>

View File

@@ -1,8 +1,8 @@
/* /*
IMPORTANT! This file is auto-generated each time you save your IMPORTANT! This file is auto-generated each time you save your
project - if you alter its contents, your changes may be overwritten! project - if you alter its contents, your changes may be overwritten!
*/ */
#include <juce_audio_plugin_client/juce_audio_plugin_client_LV2.mm> #include <juce_audio_plugin_client/juce_audio_plugin_client_LV2.mm>

View File

@@ -1,8 +1,8 @@
/* /*
IMPORTANT! This file is auto-generated each time you save your IMPORTANT! This file is auto-generated each time you save your
project - if you alter its contents, your changes may be overwritten! project - if you alter its contents, your changes may be overwritten!
*/ */
#include <juce_audio_plugin_client/juce_audio_plugin_client_Standalone.cpp> #include <juce_audio_plugin_client/juce_audio_plugin_client_Standalone.cpp>

View File

@@ -1,8 +1,8 @@
/* /*
IMPORTANT! This file is auto-generated each time you save your IMPORTANT! This file is auto-generated each time you save your
project - if you alter its contents, your changes may be overwritten! project - if you alter its contents, your changes may be overwritten!
*/ */
#include <juce_audio_plugin_client/juce_audio_plugin_client_Unity.cpp> #include <juce_audio_plugin_client/juce_audio_plugin_client_Unity.cpp>

View File

@@ -1,8 +1,8 @@
/* /*
IMPORTANT! This file is auto-generated each time you save your IMPORTANT! This file is auto-generated each time you save your
project - if you alter its contents, your changes may be overwritten! project - if you alter its contents, your changes may be overwritten!
*/ */
#include <juce_audio_plugin_client/juce_audio_plugin_client_VST2.cpp> #include <juce_audio_plugin_client/juce_audio_plugin_client_VST2.cpp>

View File

@@ -1,8 +1,8 @@
/* /*
IMPORTANT! This file is auto-generated each time you save your IMPORTANT! This file is auto-generated each time you save your
project - if you alter its contents, your changes may be overwritten! project - if you alter its contents, your changes may be overwritten!
*/ */
#include <juce_audio_plugin_client/juce_audio_plugin_client_VST2.mm> #include <juce_audio_plugin_client/juce_audio_plugin_client_VST2.mm>

View File

@@ -1,8 +1,8 @@
/* /*
IMPORTANT! This file is auto-generated each time you save your IMPORTANT! This file is auto-generated each time you save your
project - if you alter its contents, your changes may be overwritten! project - if you alter its contents, your changes may be overwritten!
*/ */
#include <juce_audio_plugin_client/juce_audio_plugin_client_VST3.cpp> #include <juce_audio_plugin_client/juce_audio_plugin_client_VST3.cpp>

View File

@@ -1,8 +1,8 @@
/* /*
IMPORTANT! This file is auto-generated each time you save your IMPORTANT! This file is auto-generated each time you save your
project - if you alter its contents, your changes may be overwritten! project - if you alter its contents, your changes may be overwritten!
*/ */
#include <juce_audio_plugin_client/juce_audio_plugin_client_VST3.mm> #include <juce_audio_plugin_client/juce_audio_plugin_client_VST3.mm>

View File

@@ -1,8 +1,8 @@
/* /*
IMPORTANT! This file is auto-generated each time you save your IMPORTANT! This file is auto-generated each time you save your
project - if you alter its contents, your changes may be overwritten! project - if you alter its contents, your changes may be overwritten!
*/ */
#include <juce_audio_processors/juce_audio_processors.cpp> #include <juce_audio_processors/juce_audio_processors.cpp>

View File

@@ -1,8 +1,8 @@
/* /*
IMPORTANT! This file is auto-generated each time you save your IMPORTANT! This file is auto-generated each time you save your
project - if you alter its contents, your changes may be overwritten! project - if you alter its contents, your changes may be overwritten!
*/ */
#include <juce_audio_processors/juce_audio_processors.mm> #include <juce_audio_processors/juce_audio_processors.mm>

View File

@@ -1,8 +1,8 @@
/* /*
IMPORTANT! This file is auto-generated each time you save your IMPORTANT! This file is auto-generated each time you save your
project - if you alter its contents, your changes may be overwritten! project - if you alter its contents, your changes may be overwritten!
*/ */
#include <juce_audio_processors/juce_audio_processors_ara.cpp> #include <juce_audio_processors/juce_audio_processors_ara.cpp>

View File

@@ -1,8 +1,8 @@
/* /*
IMPORTANT! This file is auto-generated each time you save your IMPORTANT! This file is auto-generated each time you save your
project - if you alter its contents, your changes may be overwritten! project - if you alter its contents, your changes may be overwritten!
*/ */
#include <juce_audio_processors/juce_audio_processors_lv2_libs.cpp> #include <juce_audio_processors/juce_audio_processors_lv2_libs.cpp>

View File

@@ -1,8 +1,8 @@
/* /*
IMPORTANT! This file is auto-generated each time you save your IMPORTANT! This file is auto-generated each time you save your
project - if you alter its contents, your changes may be overwritten! project - if you alter its contents, your changes may be overwritten!
*/ */
#include <juce_audio_utils/juce_audio_utils.cpp> #include <juce_audio_utils/juce_audio_utils.cpp>

View File

@@ -1,8 +1,8 @@
/* /*
IMPORTANT! This file is auto-generated each time you save your IMPORTANT! This file is auto-generated each time you save your
project - if you alter its contents, your changes may be overwritten! project - if you alter its contents, your changes may be overwritten!
*/ */
#include <juce_audio_utils/juce_audio_utils.mm> #include <juce_audio_utils/juce_audio_utils.mm>

View File

@@ -1,8 +1,8 @@
/* /*
IMPORTANT! This file is auto-generated each time you save your IMPORTANT! This file is auto-generated each time you save your
project - if you alter its contents, your changes may be overwritten! project - if you alter its contents, your changes may be overwritten!
*/ */
#include <juce_core/juce_core.cpp> #include <juce_core/juce_core.cpp>

View File

@@ -1,8 +1,8 @@
/* /*
IMPORTANT! This file is auto-generated each time you save your IMPORTANT! This file is auto-generated each time you save your
project - if you alter its contents, your changes may be overwritten! project - if you alter its contents, your changes may be overwritten!
*/ */
#include <juce_core/juce_core.mm> #include <juce_core/juce_core.mm>

View File

@@ -1,8 +1,8 @@
/* /*
IMPORTANT! This file is auto-generated each time you save your IMPORTANT! This file is auto-generated each time you save your
project - if you alter its contents, your changes may be overwritten! project - if you alter its contents, your changes may be overwritten!
*/ */
#include <juce_core/juce_core_CompilationTime.cpp> #include <juce_core/juce_core_CompilationTime.cpp>

View File

@@ -1,8 +1,8 @@
/* /*
IMPORTANT! This file is auto-generated each time you save your IMPORTANT! This file is auto-generated each time you save your
project - if you alter its contents, your changes may be overwritten! project - if you alter its contents, your changes may be overwritten!
*/ */
#include <juce_data_structures/juce_data_structures.cpp> #include <juce_data_structures/juce_data_structures.cpp>

View File

@@ -1,8 +1,8 @@
/* /*
IMPORTANT! This file is auto-generated each time you save your IMPORTANT! This file is auto-generated each time you save your
project - if you alter its contents, your changes may be overwritten! project - if you alter its contents, your changes may be overwritten!
*/ */
#include <juce_data_structures/juce_data_structures.mm> #include <juce_data_structures/juce_data_structures.mm>

View File

@@ -1,8 +1,8 @@
/* /*
IMPORTANT! This file is auto-generated each time you save your IMPORTANT! This file is auto-generated each time you save your
project - if you alter its contents, your changes may be overwritten! project - if you alter its contents, your changes may be overwritten!
*/ */
#include <juce_dsp/juce_dsp.cpp> #include <juce_dsp/juce_dsp.cpp>

View File

@@ -1,8 +1,8 @@
/* /*
IMPORTANT! This file is auto-generated each time you save your IMPORTANT! This file is auto-generated each time you save your
project - if you alter its contents, your changes may be overwritten! project - if you alter its contents, your changes may be overwritten!
*/ */
#include <juce_dsp/juce_dsp.mm> #include <juce_dsp/juce_dsp.mm>

View File

@@ -1,8 +1,8 @@
/* /*
IMPORTANT! This file is auto-generated each time you save your IMPORTANT! This file is auto-generated each time you save your
project - if you alter its contents, your changes may be overwritten! project - if you alter its contents, your changes may be overwritten!
*/ */
#include <juce_events/juce_events.cpp> #include <juce_events/juce_events.cpp>

View File

@@ -1,8 +1,8 @@
/* /*
IMPORTANT! This file is auto-generated each time you save your IMPORTANT! This file is auto-generated each time you save your
project - if you alter its contents, your changes may be overwritten! project - if you alter its contents, your changes may be overwritten!
*/ */
#include <juce_events/juce_events.mm> #include <juce_events/juce_events.mm>

View File

@@ -1,8 +1,8 @@
/* /*
IMPORTANT! This file is auto-generated each time you save your IMPORTANT! This file is auto-generated each time you save your
project - if you alter its contents, your changes may be overwritten! project - if you alter its contents, your changes may be overwritten!
*/ */
#include <juce_graphics/juce_graphics.cpp> #include <juce_graphics/juce_graphics.cpp>

View File

@@ -1,8 +1,8 @@
/* /*
IMPORTANT! This file is auto-generated each time you save your IMPORTANT! This file is auto-generated each time you save your
project - if you alter its contents, your changes may be overwritten! project - if you alter its contents, your changes may be overwritten!
*/ */
#include <juce_graphics/juce_graphics.mm> #include <juce_graphics/juce_graphics.mm>

View File

@@ -1,8 +1,8 @@
/* /*
IMPORTANT! This file is auto-generated each time you save your IMPORTANT! This file is auto-generated each time you save your
project - if you alter its contents, your changes may be overwritten! project - if you alter its contents, your changes may be overwritten!
*/ */
#include <juce_graphics/juce_graphics_Harfbuzz.cpp> #include <juce_graphics/juce_graphics_Harfbuzz.cpp>

View File

@@ -1,8 +1,8 @@
/* /*
IMPORTANT! This file is auto-generated each time you save your IMPORTANT! This file is auto-generated each time you save your
project - if you alter its contents, your changes may be overwritten! project - if you alter its contents, your changes may be overwritten!
*/ */
#include <juce_graphics/juce_graphics_Sheenbidi.c> #include <juce_graphics/juce_graphics_Sheenbidi.c>

View File

@@ -1,8 +1,8 @@
/* /*
IMPORTANT! This file is auto-generated each time you save your IMPORTANT! This file is auto-generated each time you save your
project - if you alter its contents, your changes may be overwritten! project - if you alter its contents, your changes may be overwritten!
*/ */
#include <juce_gui_basics/juce_gui_basics.cpp> #include <juce_gui_basics/juce_gui_basics.cpp>

View File

@@ -1,8 +1,8 @@
/* /*
IMPORTANT! This file is auto-generated each time you save your IMPORTANT! This file is auto-generated each time you save your
project - if you alter its contents, your changes may be overwritten! project - if you alter its contents, your changes may be overwritten!
*/ */
#include <juce_gui_basics/juce_gui_basics.mm> #include <juce_gui_basics/juce_gui_basics.mm>

View File

@@ -1,8 +1,8 @@
/* /*
IMPORTANT! This file is auto-generated each time you save your IMPORTANT! This file is auto-generated each time you save your
project - if you alter its contents, your changes may be overwritten! project - if you alter its contents, your changes may be overwritten!
*/ */
#include <juce_gui_extra/juce_gui_extra.cpp> #include <juce_gui_extra/juce_gui_extra.cpp>

View File

@@ -1,8 +1,8 @@
/* /*
IMPORTANT! This file is auto-generated each time you save your IMPORTANT! This file is auto-generated each time you save your
project - if you alter its contents, your changes may be overwritten! project - if you alter its contents, your changes may be overwritten!
*/ */
#include <juce_gui_extra/juce_gui_extra.mm> #include <juce_gui_extra/juce_gui_extra.mm>

View File

@@ -7,6 +7,8 @@
pluginVSTNumMidiInputs="1" pluginChannelConfigs="{0, 2}" version="0.0.1"> pluginVSTNumMidiInputs="1" pluginChannelConfigs="{0, 2}" version="0.0.1">
<MAINGROUP id="UQstsW" name="NeuralSynth"> <MAINGROUP id="UQstsW" name="NeuralSynth">
<GROUP id="{D5B48DA9-9A47-914A-8C72-EE5E8DD868A3}" name="Source"> <GROUP id="{D5B48DA9-9A47-914A-8C72-EE5E8DD868A3}" name="Source">
<FILE id="Mkx0uo" name="BlepOsc.h" compile="0" resource="0" file="Source/BlepOsc.h"/>
<FILE id="axDpEq" name="WavetableOsc.h" compile="0" resource="0" file="Source/WavetableOsc.h"/>
<FILE id="nmKMnf" name="GraphComponent.h" compile="0" resource="0" <FILE id="nmKMnf" name="GraphComponent.h" compile="0" resource="0"
file="Source/GraphComponent.h"/> file="Source/GraphComponent.h"/>
<FILE id="CjJ141" name="NeuralSharedParams.h" compile="0" resource="0" <FILE id="CjJ141" name="NeuralSharedParams.h" compile="0" resource="0"

View File

@@ -1,49 +1,49 @@
#pragma once #pragma once
//============================================================================== //==============================================================================
template <typename SampleType> template <typename SampleType>
class AudioBufferQueue class AudioBufferQueue
{ {
public: public:
//============================================================================== //==============================================================================
static constexpr size_t order = 9; static constexpr size_t order = 9;
static constexpr size_t bufferSize = 1U << order; static constexpr size_t bufferSize = 1U << order;
static constexpr size_t numBuffers = 5; static constexpr size_t numBuffers = 5;
//============================================================================== //==============================================================================
void push(const SampleType* dataToPush, size_t numSamples) void push(const SampleType* dataToPush, size_t numSamples)
{ {
jassert(numSamples <= bufferSize); jassert(numSamples <= bufferSize);
int start1, size1, start2, size2; int start1, size1, start2, size2;
abstractFifo.prepareToWrite(1, start1, size1, start2, size2); abstractFifo.prepareToWrite(1, start1, size1, start2, size2);
jassert(size1 <= 1); jassert(size1 <= 1);
jassert(size2 == 0); jassert(size2 == 0);
if (size1 > 0) if (size1 > 0)
juce::FloatVectorOperations::copy(buffers[(size_t)start1].data(), dataToPush, (int)juce::jmin(bufferSize, numSamples)); juce::FloatVectorOperations::copy(buffers[(size_t)start1].data(), dataToPush, (int)juce::jmin(bufferSize, numSamples));
abstractFifo.finishedWrite(size1); abstractFifo.finishedWrite(size1);
} }
//============================================================================== //==============================================================================
void pop(SampleType* outputBuffer) void pop(SampleType* outputBuffer)
{ {
int start1, size1, start2, size2; int start1, size1, start2, size2;
abstractFifo.prepareToRead(1, start1, size1, start2, size2); abstractFifo.prepareToRead(1, start1, size1, start2, size2);
jassert(size1 <= 1); jassert(size1 <= 1);
jassert(size2 == 0); jassert(size2 == 0);
if (size1 > 0) if (size1 > 0)
juce::FloatVectorOperations::copy(outputBuffer, buffers[(size_t)start1].data(), (int)bufferSize); juce::FloatVectorOperations::copy(outputBuffer, buffers[(size_t)start1].data(), (int)bufferSize);
abstractFifo.finishedRead(size1); abstractFifo.finishedRead(size1);
} }
private: private:
//============================================================================== //==============================================================================
juce::AbstractFifo abstractFifo{ numBuffers }; juce::AbstractFifo abstractFifo{ numBuffers };
std::array<std::array<SampleType, bufferSize>, numBuffers> buffers; std::array<std::array<SampleType, bufferSize>, numBuffers> buffers;
}; };

View File

@@ -1,47 +1,47 @@
#pragma once #pragma once
#include "SynthVoice.h" #include "SynthVoice.h"
#include <JuceHeader.h> #include <JuceHeader.h>
class NeuralAudioEngine : public juce::MPESynthesiser class NeuralAudioEngine : public juce::MPESynthesiser
{ {
public: public:
static constexpr int maxNumVoices = 8; static constexpr int maxNumVoices = 8;
explicit NeuralAudioEngine(NeuralSharedParams& sp) explicit NeuralAudioEngine(NeuralSharedParams& sp)
{ {
// Create MPE voices // Create MPE voices
for (int i = 0; i < maxNumVoices; ++i) for (int i = 0; i < maxNumVoices; ++i)
addVoice(new NeuralSynthVoice(sp)); // <-- takes MPESynthesiserVoice* addVoice(new NeuralSynthVoice(sp)); // <-- takes MPESynthesiserVoice*
// MPE synths do not use addSound(); note events are routed via MPE zones. // MPE synths do not use addSound(); note events are routed via MPE zones.
setVoiceStealingEnabled(true); setVoiceStealingEnabled(true);
} }
void prepare(const juce::dsp::ProcessSpec& spec) noexcept void prepare(const juce::dsp::ProcessSpec& spec) noexcept
{ {
setCurrentPlaybackSampleRate(spec.sampleRate); setCurrentPlaybackSampleRate(spec.sampleRate);
for (auto* v : voices) for (auto* v : voices)
if (auto* nv = dynamic_cast<NeuralSynthVoice*>(v)) if (auto* nv = dynamic_cast<NeuralSynthVoice*>(v))
nv->prepare(spec); nv->prepare(spec);
} }
template <typename VoiceFunc> template <typename VoiceFunc>
void applyToVoices(VoiceFunc&& fn) noexcept void applyToVoices(VoiceFunc&& fn) noexcept
{ {
for (auto* v : voices) for (auto* v : voices)
fn(dynamic_cast<NeuralSynthVoice*>(v)); fn(dynamic_cast<NeuralSynthVoice*>(v));
} }
private: private:
// keep base render // keep base render
using juce::MPESynthesiser::renderNextSubBlock; using juce::MPESynthesiser::renderNextSubBlock;
void renderNextSubBlock(juce::AudioBuffer<float>& outputAudio, void renderNextSubBlock(juce::AudioBuffer<float>& outputAudio,
int startSample, int startSample,
int numSamples) override int numSamples) override
{ {
juce::MPESynthesiser::renderNextSubBlock(outputAudio, startSample, numSamples); juce::MPESynthesiser::renderNextSubBlock(outputAudio, startSample, numSamples);
} }
}; };

View File

@@ -1,80 +1,80 @@
#pragma once #pragma once
#include <JuceHeader.h> #include <JuceHeader.h>
enum class BlepWave : int { Sine = 0, Saw, Square, Triangle }; enum class BlepWave : int { Sine = 0, Saw, Square, Triangle };
class BlepOsc class BlepOsc
{ {
public: public:
void prepare (double sampleRate) { sr = sampleRate; resetPhase(); } void prepare (double sampleRate) { sr = sampleRate; resetPhase(); }
void setWave (BlepWave w) { wave = w; } void setWave (BlepWave w) { wave = w; }
void setFrequency (float f) { freq = juce::jmax (0.0f, f); inc = freq / (float) sr; } 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); } void resetPhase (float p = 0.0f) { phase = juce::jlimit (0.0f, 1.0f, p); }
inline float process() inline float process()
{ {
// phase in [0..1) // phase in [0..1)
float out = 0.0f; float out = 0.0f;
float t = phase; float t = phase;
phase += inc; phase += inc;
if (phase >= 1.0f) phase -= 1.0f; if (phase >= 1.0f) phase -= 1.0f;
switch (wave) switch (wave)
{ {
case BlepWave::Sine: out = std::sin (2.0f * juce::MathConstants<float>::pi * t); break; case BlepWave::Sine: out = std::sin (2.0f * juce::MathConstants<float>::pi * t); break;
case BlepWave::Saw: case BlepWave::Saw:
{ {
// naive saw in [-1..1] // naive saw in [-1..1]
float s = 2.0f * t - 1.0f; float s = 2.0f * t - 1.0f;
// apply BLEP at the discontinuity crossing t=0 // apply BLEP at the discontinuity crossing t=0
s -= polyBlep (t, inc); s -= polyBlep (t, inc);
out = s; out = s;
} break; } break;
case BlepWave::Square: case BlepWave::Square:
{ {
float s = (t < 0.5f ? 1.0f : -1.0f); float s = (t < 0.5f ? 1.0f : -1.0f);
// rising edge at 0.0, falling at 0.5 // rising edge at 0.0, falling at 0.5
s += polyBlep (t, inc) - polyBlep (std::fmod (t + 0.5f, 1.0f), inc); s += polyBlep (t, inc) - polyBlep (std::fmod (t + 0.5f, 1.0f), inc);
out = s; out = s;
} break; } break;
case BlepWave::Triangle: case BlepWave::Triangle:
{ {
// integrate the BLEP square for band-limited tri // integrate the BLEP square for band-limited tri
float sq = (t < 0.5f ? 1.0f : -1.0f); float sq = (t < 0.5f ? 1.0f : -1.0f);
sq += polyBlep (t, inc) - polyBlep (std::fmod (t + 0.5f, 1.0f), inc); sq += polyBlep (t, inc) - polyBlep (std::fmod (t + 0.5f, 1.0f), inc);
// leaky integrator to keep DC under control // leaky integrator to keep DC under control
z1 = z1 + (sq - z1) * inc; z1 = z1 + (sq - z1) * inc;
out = 2.0f * z1; // scale out = 2.0f * z1; // scale
} break; } break;
} }
return out; return out;
} }
private: private:
// PolyBLEP as in Valimäki/Huovilainen // PolyBLEP as in Valimäki/Huovilainen
static inline float polyBlep (float t, float dt) static inline float polyBlep (float t, float dt)
{ {
// t in [0..1) // t in [0..1)
if (t < dt) if (t < dt)
{ {
t /= dt; t /= dt;
return t + t - t * t - 1.0f; return t + t - t * t - 1.0f;
} }
else if (t > 1.0f - dt) else if (t > 1.0f - dt)
{ {
t = (t - 1.0f) / dt; t = (t - 1.0f) / dt;
return t * t + t + t + 1.0f; return t * t + t + t + 1.0f;
} }
return 0.0f; return 0.0f;
} }
double sr = 44100.0; double sr = 44100.0;
float freq = 440.0f, inc = 440.0f / 44100.0f; float freq = 440.0f, inc = 440.0f / 44100.0f;
float phase = 0.0f; float phase = 0.0f;
float z1 = 0.0f; float z1 = 0.0f;
BlepWave wave = BlepWave::Sine; BlepWave wave = BlepWave::Sine;
}; };

View File

@@ -1,99 +1,99 @@
/* /*
============================================================================== ==============================================================================
GraphComponent.h GraphComponent.h
Created: 4 Jul 2025 11:43:57pm Created: 4 Jul 2025 11:43:57pm
Author: timot Author: timot
============================================================================== ==============================================================================
*/ */
#pragma once #pragma once
#include <algorithm> // for std::minmax_element #include <algorithm> // for std::minmax_element
#include "AudioBufferQueue.h" #include "AudioBufferQueue.h"
//============================================================================== //==============================================================================
template <typename SampleType> template <typename SampleType>
class GraphComponent : public juce::Component, class GraphComponent : public juce::Component,
private juce::Timer private juce::Timer
{ {
public: public:
//============================================================================== //==============================================================================
GraphComponent(SampleType minIn, SampleType maxIn, int numPointsIn) GraphComponent(SampleType minIn, SampleType maxIn, int numPointsIn)
: min(minIn), max(maxIn), numPoints(numPointsIn) : min(minIn), max(maxIn), numPoints(numPointsIn)
{ {
x.resize(numPoints); x.resize(numPoints);
y.resize(numPoints); y.resize(numPoints);
setFramesPerSecond(30); setFramesPerSecond(30);
// func will be set via setFunction before paint; provide a safe default // func will be set via setFunction before paint; provide a safe default
func = [](SampleType) noexcept { return SampleType(); }; func = [](SampleType) noexcept { return SampleType(); };
} }
//============================================================================== //==============================================================================
void setFramesPerSecond(int framesPerSecond) void setFramesPerSecond(int framesPerSecond)
{ {
jassert(framesPerSecond > 0 && framesPerSecond < 1000); jassert(framesPerSecond > 0 && framesPerSecond < 1000);
startTimerHz(framesPerSecond); startTimerHz(framesPerSecond);
} }
//============================================================================== //==============================================================================
void setFunction(const std::function<SampleType(SampleType)>& f) { func = f; } void setFunction(const std::function<SampleType(SampleType)>& f) { func = f; }
//============================================================================== //==============================================================================
void paint(juce::Graphics& g) override void paint(juce::Graphics& g) override
{ {
g.fillAll(juce::Colours::black); g.fillAll(juce::Colours::black);
g.setColour(juce::Colours::white); g.setColour(juce::Colours::white);
auto area = getLocalBounds(); auto area = getLocalBounds();
if (hasData && area.isFinite()) if (hasData && area.isFinite())
{ {
auto h = (SampleType)area.getHeight(); auto h = (SampleType)area.getHeight();
auto w = (SampleType)area.getWidth(); auto w = (SampleType)area.getWidth();
for (size_t i = 1; i < (size_t)numPoints; ++i) for (size_t i = 1; i < (size_t)numPoints; ++i)
{ {
auto px_prev = ((x[i - 1] - min) / (max - min)) * w; auto px_prev = ((x[i - 1] - min) / (max - min)) * w;
auto py_prev = h - ((y[i - 1] - minY) / (maxY - minY)) * h; auto py_prev = h - ((y[i - 1] - minY) / (maxY - minY)) * h;
auto px_next = ((x[i] - min) / (max - min)) * w; auto px_next = ((x[i] - min) / (max - min)) * w;
auto py_next = h - ((y[i] - minY) / (maxY - minY)) * h; auto py_next = h - ((y[i] - minY) / (maxY - minY)) * h;
g.drawLine({ px_prev, py_prev, px_next, py_next }); g.drawLine({ px_prev, py_prev, px_next, py_next });
} }
} }
} }
//============================================================================== //==============================================================================
void resized() override {} void resized() override {}
private: private:
//============================================================================== //==============================================================================
std::vector<SampleType> x, y; std::vector<SampleType> x, y;
SampleType minY{ SampleType() }, maxY{ SampleType(1) }; SampleType minY{ SampleType() }, maxY{ SampleType(1) };
SampleType min{}, max{}; SampleType min{}, max{};
int numPoints{}; int numPoints{};
std::function<SampleType(SampleType)> func; std::function<SampleType(SampleType)> func;
bool hasData = false; bool hasData = false;
//============================================================================== //==============================================================================
void timerCallback() override void timerCallback() override
{ {
const SampleType step = (max - min) / (SampleType)(numPoints - 1); const SampleType step = (max - min) / (SampleType)(numPoints - 1);
for (int i = 0; i < numPoints; i++) for (int i = 0; i < numPoints; i++)
{ {
x[(size_t)i] = min + step * (SampleType)i; x[(size_t)i] = min + step * (SampleType)i;
y[(size_t)i] = func(x[(size_t)i]); y[(size_t)i] = func(x[(size_t)i]);
} }
auto p = std::minmax_element(y.begin(), y.end()); auto p = std::minmax_element(y.begin(), y.end());
minY = *p.first; minY = *p.first;
maxY = *p.second; maxY = *p.second;
hasData = true; hasData = true;
repaint(); repaint();
} }
}; };

View File

@@ -1,145 +1,145 @@
#pragma once #pragma once
#include <atomic> #include <atomic>
#include <unordered_map> #include <unordered_map>
#include <string> #include <string>
struct SliderDetail { struct SliderDetail {
std::string label; std::string label;
float min, max, interval, defValue; float min, max, interval, defValue;
}; };
using ParamMap = std::unordered_map<std::string, SliderDetail>; using ParamMap = std::unordered_map<std::string, SliderDetail>;
// Each SliderDetail: { label, min, max, step, defaultValue } // Each SliderDetail: { label, min, max, step, defaultValue }
const std::unordered_map<std::string, ParamMap> PARAM_SETTINGS = { const std::unordered_map<std::string, ParamMap> PARAM_SETTINGS = {
{ "chorus", { { "chorus", {
{ "rate", { "Rate", 0.0f, 1.0f, 0.1f, 0.1f } }, { "rate", { "Rate", 0.0f, 1.0f, 0.1f, 0.1f } },
{ "depth", { "Depth", 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 } }, { "centre", { "Centre", 0.0f, 1.0f, 0.1f, 0.1f } },
{ "feedback", { "Feedback", 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 } } { "mix", { "Mix", 0.0f, 1.0f, 0.1f, 0.1f } }
}}, }},
{ "delay", { { "delay", {
{ "delay", { "Delay", 0.0f, 1.0f, 0.1f, 0.1f } } { "delay", { "Delay", 0.0f, 1.0f, 0.1f, 0.1f } }
}}, }},
{ "reverb", { { "reverb", {
{ "roomSize", { "Room Size", 0.0f, 1.0f, 0.1f, 0.1f } }, { "roomSize", { "Room Size", 0.0f, 1.0f, 0.1f, 0.1f } },
{ "damping", { "Damping", 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 } }, { "wetLevel", { "Wet Level", 0.0f, 1.0f, 0.1f, 0.1f } },
{ "dryLevel", { "Dry 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 } }, { "width", { "Width", 0.0f, 1.0f, 0.1f, 0.1f } },
{ "freezeMode", { "Freeze Mode", 0.0f, 1.0f, 0.1f, 0.1f } } { "freezeMode", { "Freeze Mode", 0.0f, 1.0f, 0.1f, 0.1f } }
}}, }},
{ "adsr", { { "adsr", {
{ "attack", { "Attack", 0.0f, 1.0f, 0.1f, 0.1f } }, { "attack", { "Attack", 0.0f, 1.0f, 0.1f, 0.1f } },
{ "decay", { "Decay", 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 } }, { "sustain", { "Sustain", 0.0f, 1.0f, 0.1f, 0.1f } },
{ "release", { "Release", 0.0f, 1.0f, 0.1f, 0.1f } } { "release", { "Release", 0.0f, 1.0f, 0.1f, 0.1f } }
}}, }},
// Filter envelope group (short key: "fenv") // Filter envelope group (short key: "fenv")
{ "fenv", { { "fenv", {
{ "attack", { "Attack", 0.0f, 2.0f, 0.001f, 0.01f } }, { "attack", { "Attack", 0.0f, 2.0f, 0.001f, 0.01f } },
{ "decay", { "Decay", 0.0f, 2.0f, 0.001f, 0.10f } }, { "decay", { "Decay", 0.0f, 2.0f, 0.001f, 0.10f } },
{ "sustain", { "Sustain", 0.0f, 1.0f, 0.001f, 0.80f } }, { "sustain", { "Sustain", 0.0f, 1.0f, 0.001f, 0.80f } },
{ "release", { "Release", 0.0f, 4.0f, 0.001f, 0.40f } }, { "release", { "Release", 0.0f, 4.0f, 0.001f, 0.40f } },
{ "amount", { "Amount", -1.0f, 1.0f, 0.001f, 0.50f } } { "amount", { "Amount", -1.0f, 1.0f, 0.001f, 0.50f } }
}}, }},
{ "flanger", { { "flanger", {
{ "rate", { "Rate", 0.1f, 5.0f, 0.1f, 0.1f } }, { "rate", { "Rate", 0.1f, 5.0f, 0.1f, 0.1f } },
{ "depth", { "Depth", 0.1f, 10.0f, 0.1f, 0.1f } }, // ms { "depth", { "Depth", 0.1f, 10.0f, 0.1f, 0.1f } }, // ms
{ "feedback", { "Feedback", 0.0f, 0.95f, 0.01f, 0.1f } }, { "feedback", { "Feedback", 0.0f, 0.95f, 0.01f, 0.1f } },
{ "dryMix", { "Dry/Wet", 0.0f, 1.0f, 0.01f, 0.0f } }, { "dryMix", { "Dry/Wet", 0.0f, 1.0f, 0.01f, 0.0f } },
{ "phase", { "Phase", 0.0f, 1.0f, 0.1f, 0.0f } }, { "phase", { "Phase", 0.0f, 1.0f, 0.1f, 0.0f } },
{ "delay", { "Delay", 0.0f, 3.0f, 0.1f, 0.25f } } // ms base { "delay", { "Delay", 0.0f, 3.0f, 0.1f, 0.25f } } // ms base
}}, }},
{ "filter", { { "filter", {
{ "cutoff", { "Cutoff", 20.0f, 20000.0f, 1.0f, 1000.0f } }, { "cutoff", { "Cutoff", 20.0f, 20000.0f, 1.0f, 1000.0f } },
{ "resonance", { "Resonance", 0.1f, 10.0f, 0.1f, 0.7f } }, { "resonance", { "Resonance", 0.1f, 10.0f, 0.1f, 0.7f } },
{ "type", { "L/H/B", 0.0f, 2.0f, 1.0f, 0.0f } }, { "type", { "L/H/B", 0.0f, 2.0f, 1.0f, 0.0f } },
{ "drive", { "Drive", 0.0f, 1.0f, 0.01f, 0.0f } }, { "drive", { "Drive", 0.0f, 1.0f, 0.01f, 0.0f } },
{ "mod", { "Mod", -1.0f, 1.0f, 0.1f, 0.0f } }, { "mod", { "Mod", -1.0f, 1.0f, 0.1f, 0.0f } },
{ "key", { "Key", 0.0f, 1.0f, 0.1f, 0.0f } } { "key", { "Key", 0.0f, 1.0f, 0.1f, 0.0f } }
}}, }},
{ "distortion", { { "distortion", {
{ "drive", { "Drive", 0.0f, 30.0f, 0.1f, 10.0f } }, { "drive", { "Drive", 0.0f, 30.0f, 0.1f, 10.0f } },
{ "mix", { "Mix", 0.0f, 1.0f, 0.01f, 0.0f } }, { "mix", { "Mix", 0.0f, 1.0f, 0.01f, 0.0f } },
{ "bias", { "Bias", -1.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 } }, { "tone", { "Tone", 100.0f, 8000.0f, 10.0f, 3000.0f } },
{ "shape", { "Shape", 0.0f, 2.0f, 1.0f, 0.0f } } { "shape", { "Shape", 0.0f, 2.0f, 1.0f, 0.0f } }
}} }}
}; };
struct NeuralSharedParams struct NeuralSharedParams
{ {
std::atomic<int> waveform{ -1 }; std::atomic<int> waveform{ -1 };
// Amp ADSR // Amp ADSR
std::atomic<float>* adsrAttack{}; std::atomic<float>* adsrAttack{};
std::atomic<float>* adsrDecay{}; std::atomic<float>* adsrDecay{};
std::atomic<float>* adsrSustain{}; std::atomic<float>* adsrSustain{};
std::atomic<float>* adsrRelease{}; std::atomic<float>* adsrRelease{};
// Delay // Delay
std::atomic<float>* delayTime{}; std::atomic<float>* delayTime{};
// Chorus // Chorus
std::atomic<float>* chorusRate{}; std::atomic<float>* chorusRate{};
std::atomic<float>* chorusDepth{}; std::atomic<float>* chorusDepth{};
std::atomic<float>* chorusCentre{}; std::atomic<float>* chorusCentre{};
std::atomic<float>* chorusFeedback{}; std::atomic<float>* chorusFeedback{};
std::atomic<float>* chorusMix{}; std::atomic<float>* chorusMix{};
// Reverb // Reverb
std::atomic<float>* reverbRoomSize{}; std::atomic<float>* reverbRoomSize{};
std::atomic<float>* reverbDamping{}; std::atomic<float>* reverbDamping{};
std::atomic<float>* reverbWetLevel{}; std::atomic<float>* reverbWetLevel{};
std::atomic<float>* reverbDryLevel{}; std::atomic<float>* reverbDryLevel{};
std::atomic<float>* reverbWidth{}; std::atomic<float>* reverbWidth{};
std::atomic<float>* reverbFreezeMode{}; std::atomic<float>* reverbFreezeMode{};
// Flanger // Flanger
std::atomic<float>* flangerRate{}; std::atomic<float>* flangerRate{};
std::atomic<float>* flangerDepth{}; std::atomic<float>* flangerDepth{};
std::atomic<float>* flangerFeedback{}; std::atomic<float>* flangerFeedback{};
std::atomic<float>* flangerDryMix{}; std::atomic<float>* flangerDryMix{};
std::atomic<float>* flangerPhase{}; std::atomic<float>* flangerPhase{};
std::atomic<float>* flangerDelay{}; std::atomic<float>* flangerDelay{};
// Filter (base) // Filter (base)
std::atomic<float>* filterCutoff{}; std::atomic<float>* filterCutoff{};
std::atomic<float>* filterResonance{}; std::atomic<float>* filterResonance{};
std::atomic<float>* filterType{}; std::atomic<float>* filterType{};
std::atomic<float>* filterDrive{}; std::atomic<float>* filterDrive{};
std::atomic<float>* filterMod{}; std::atomic<float>* filterMod{};
std::atomic<float>* filterKey{}; std::atomic<float>* filterKey{};
// Filter Env (polyphonic) // Filter Env (polyphonic)
std::atomic<float>* fenvAttack{}; std::atomic<float>* fenvAttack{};
std::atomic<float>* fenvDecay{}; std::atomic<float>* fenvDecay{};
std::atomic<float>* fenvSustain{}; std::atomic<float>* fenvSustain{};
std::atomic<float>* fenvRelease{}; std::atomic<float>* fenvRelease{};
std::atomic<float>* fenvAmount{}; // +/- octaves std::atomic<float>* fenvAmount{}; // +/- octaves
// Distortion // Distortion
std::atomic<float>* distortionDrive{}; std::atomic<float>* distortionDrive{};
std::atomic<float>* distortionMix{}; std::atomic<float>* distortionMix{};
std::atomic<float>* distortionBias{}; std::atomic<float>* distortionBias{};
std::atomic<float>* distortionTone{}; std::atomic<float>* distortionTone{};
std::atomic<float>* distortionShape{}; std::atomic<float>* distortionShape{};
// Per-panel bypass (AudioParameterBool, exposed as float 0/1 via getRawParameterValue) // Per-panel bypass (AudioParameterBool, exposed as float 0/1 via getRawParameterValue)
std::atomic<float>* chorusOn{}; std::atomic<float>* chorusOn{};
std::atomic<float>* delayOn{}; std::atomic<float>* delayOn{};
std::atomic<float>* reverbOn{}; std::atomic<float>* reverbOn{};
std::atomic<float>* flangerOn{}; std::atomic<float>* flangerOn{};
std::atomic<float>* distortionOn{}; std::atomic<float>* distortionOn{};
std::atomic<float>* filterOn{}; std::atomic<float>* filterOn{};
std::atomic<float>* eqOn{}; std::atomic<float>* eqOn{};
// EQ + Master // EQ + Master
std::atomic<float>* lowGainDbls{}; std::atomic<float>* lowGainDbls{};
std::atomic<float>* midGainDbls{}; std::atomic<float>* midGainDbls{};
std::atomic<float>* highGainDbls{}; std::atomic<float>* highGainDbls{};
std::atomic<float>* masterDbls{}; std::atomic<float>* masterDbls{};
}; };

View File

@@ -1,170 +1,170 @@
#include "PluginProcessor.h" #include "PluginProcessor.h"
#include "PluginEditor.h" #include "PluginEditor.h"
#include "ScopeComponent.h" #include "ScopeComponent.h"
//============================================================================== //==============================================================================
NeuralSynthAudioProcessorEditor::NeuralSynthAudioProcessorEditor (NeuralSynthAudioProcessor& p) NeuralSynthAudioProcessorEditor::NeuralSynthAudioProcessorEditor (NeuralSynthAudioProcessor& p)
: AudioProcessorEditor (&p), : AudioProcessorEditor (&p),
audioProcessor (p), audioProcessor (p),
mainScopeComponent(audioProcessor.getAudioBufferQueue()) mainScopeComponent(audioProcessor.getAudioBufferQueue())
{ {
auto& tree = audioProcessor.parameters; auto& tree = audioProcessor.parameters;
addAndMakeVisible(mainScopeComponent); addAndMakeVisible(mainScopeComponent);
waveformSelector.setModel(&waveformContents); waveformSelector.setModel(&waveformContents);
waveformContents.onSelect = [this](int row) waveformContents.onSelect = [this](int row)
{ {
// write to the parameter so voices update safely // write to the parameter so voices update safely
audioProcessor.parameters.getParameterAsValue("waveform") = (float)juce::jlimit(0, 3, row); audioProcessor.parameters.getParameterAsValue("waveform") = (float)juce::jlimit(0, 3, row);
}; };
addAndMakeVisible(waveformSelector); addAndMakeVisible(waveformSelector);
// --- Panels --- // --- Panels ---
adsrComponent.emplace(tree, "adsr", "Amp Env"); adsrComponent.emplace(tree, "adsr", "Amp Env");
adsrComponent->enableGraphScope([this](float x) { adsrComponent->enableGraphScope([this](float x) {
auto& tree = this->audioProcessor.parameters; auto& tree = this->audioProcessor.parameters;
float A = tree.getParameter("adsr_attack")->getValue(); float A = tree.getParameter("adsr_attack")->getValue();
float D = tree.getParameter("adsr_decay")->getValue(); float D = tree.getParameter("adsr_decay")->getValue();
float S = tree.getParameter("adsr_sustain")->getValue(); float S = tree.getParameter("adsr_sustain")->getValue();
float R = tree.getParameter("adsr_release")->getValue(); float R = tree.getParameter("adsr_release")->getValue();
const float sustainLen = 1.0f; const float sustainLen = 1.0f;
const float total = A + D + sustainLen + R; const float total = A + D + sustainLen + R;
A /= total; D /= total; R /= total; A /= total; D /= total; R /= total;
float m = 0.0f, c = 0.0f; float m = 0.0f, c = 0.0f;
if (x < A) { m = 1.0f / A; 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) { m = (S - 1.0f) / D; c = 1.0f - m * A; }
else if (x < A + D + (sustainLen / total)) { m = 0.0f; c = S; } else if (x < A + D + (sustainLen / total)) { m = 0.0f; c = S; }
else { m = (S / -R); c = -m; } else { m = (S / -R); c = -m; }
return m * x + c; return m * x + c;
}); });
addAndMakeVisible(*adsrComponent); addAndMakeVisible(*adsrComponent);
chorusComponent.emplace(tree, "chorus", "Chorus"); chorusComponent.emplace(tree, "chorus", "Chorus");
chorusComponent->enableSampleScope(audioProcessor.getChorusAudioBufferQueue()); chorusComponent->enableSampleScope(audioProcessor.getChorusAudioBufferQueue());
addAndMakeVisible(*chorusComponent); addAndMakeVisible(*chorusComponent);
delayComponent.emplace(tree, "delay", "Delay"); delayComponent.emplace(tree, "delay", "Delay");
delayComponent->enableSampleScope(audioProcessor.getDelayAudioBufferQueue()); delayComponent->enableSampleScope(audioProcessor.getDelayAudioBufferQueue());
addAndMakeVisible(*delayComponent); addAndMakeVisible(*delayComponent);
reverbComponent.emplace(tree, "reverb", "Reverb"); reverbComponent.emplace(tree, "reverb", "Reverb");
reverbComponent->enableSampleScope(audioProcessor.getReverbAudioBufferQueue()); reverbComponent->enableSampleScope(audioProcessor.getReverbAudioBufferQueue());
addAndMakeVisible(*reverbComponent); addAndMakeVisible(*reverbComponent);
eqComponent.emplace(tree, "EQ"); eqComponent.emplace(tree, "EQ");
addAndMakeVisible(*eqComponent); addAndMakeVisible(*eqComponent);
flangerComponent.emplace(tree, "flanger", "Flanger"); flangerComponent.emplace(tree, "flanger", "Flanger");
flangerComponent->enableSampleScope(audioProcessor.getFlangerAudioBufferQueue()); flangerComponent->enableSampleScope(audioProcessor.getFlangerAudioBufferQueue());
addAndMakeVisible(*flangerComponent); addAndMakeVisible(*flangerComponent);
distortionComponent.emplace(tree, "distortion", "Distortion"); distortionComponent.emplace(tree, "distortion", "Distortion");
distortionComponent->enableSampleScope(audioProcessor.getDistortionAudioBufferQueue()); distortionComponent->enableSampleScope(audioProcessor.getDistortionAudioBufferQueue());
addAndMakeVisible(*distortionComponent); addAndMakeVisible(*distortionComponent);
filterComponent.emplace(tree, "filter", "Filter"); filterComponent.emplace(tree, "filter", "Filter");
filterComponent->enableSampleScope(audioProcessor.getFilterAudioBufferQueue()); filterComponent->enableSampleScope(audioProcessor.getFilterAudioBufferQueue());
addAndMakeVisible(*filterComponent); addAndMakeVisible(*filterComponent);
filterEnvComponent.emplace(tree, "fenv", "Filter Env"); filterEnvComponent.emplace(tree, "fenv", "Filter Env");
filterEnvComponent->enableGraphScope([this](float x) { filterEnvComponent->enableGraphScope([this](float x) {
auto& tree = this->audioProcessor.parameters; auto& tree = this->audioProcessor.parameters;
float A = tree.getParameter("fenv_attack")->getValue(); float A = tree.getParameter("fenv_attack")->getValue();
float D = tree.getParameter("fenv_decay")->getValue(); float D = tree.getParameter("fenv_decay")->getValue();
float S = tree.getParameter("fenv_sustain")->getValue(); float S = tree.getParameter("fenv_sustain")->getValue();
float R = tree.getParameter("fenv_release")->getValue(); float R = tree.getParameter("fenv_release")->getValue();
const float sustainLen = 1.0f; const float sustainLen = 1.0f;
const float total = A + D + sustainLen + R; const float total = A + D + sustainLen + R;
A /= total; D /= total; R /= total; A /= total; D /= total; R /= total;
float m = 0.0f, c = 0.0f; float m = 0.0f, c = 0.0f;
if (x < A) { m = 1.0f / A; 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) { m = (S - 1.0f) / D; c = 1.0f - m * A; }
else if (x < A + D + (sustainLen / total)) { m = 0.0f; c = S; } else if (x < A + D + (sustainLen / total)) { m = 0.0f; c = S; }
else { m = (S / -R); c = -m; } else { m = (S / -R); c = -m; }
return m * x + c; return m * x + c;
}); });
addAndMakeVisible(*filterEnvComponent); addAndMakeVisible(*filterEnvComponent);
// Master fader + label // Master fader + label
addAndMakeVisible(masterLevelSlider); addAndMakeVisible(masterLevelSlider);
masterLevelLabel.setText("Master", juce::dontSendNotification); masterLevelLabel.setText("Master", juce::dontSendNotification);
{ {
juce::Font f; f.setHeight(12.0f); f.setBold(true); juce::Font f; f.setHeight(12.0f); f.setBold(true);
masterLevelLabel.setFont(f); masterLevelLabel.setFont(f);
} }
masterLevelLabel.setJustificationType(juce::Justification::centred); masterLevelLabel.setJustificationType(juce::Justification::centred);
addAndMakeVisible(masterLevelLabel); addAndMakeVisible(masterLevelLabel);
// Blank placeholder // Blank placeholder
addAndMakeVisible(blankPanel); addAndMakeVisible(blankPanel);
// Attach master parameter // Attach master parameter
gainAttachment = std::make_unique<juce::AudioProcessorValueTreeState::SliderAttachment>( gainAttachment = std::make_unique<juce::AudioProcessorValueTreeState::SliderAttachment>(
audioProcessor.parameters, "master", masterLevelSlider.slider); audioProcessor.parameters, "master", masterLevelSlider.slider);
setSize(1400, 720); setSize(1400, 720);
} }
//============================================================================== //==============================================================================
NeuralSynthAudioProcessorEditor::~NeuralSynthAudioProcessorEditor() = default; NeuralSynthAudioProcessorEditor::~NeuralSynthAudioProcessorEditor() = default;
//============================================================================== //==============================================================================
void NeuralSynthAudioProcessorEditor::paint (juce::Graphics& g) void NeuralSynthAudioProcessorEditor::paint (juce::Graphics& g)
{ {
g.fillAll(getLookAndFeel().findColour (juce::ResizableWindow::backgroundColourId)); g.fillAll(getLookAndFeel().findColour (juce::ResizableWindow::backgroundColourId));
} }
//============================================================================== //==============================================================================
void NeuralSynthAudioProcessorEditor::resized() void NeuralSynthAudioProcessorEditor::resized()
{ {
auto bounds = getLocalBounds().reduced(16); auto bounds = getLocalBounds().reduced(16);
juce::Grid grid; juce::Grid grid;
grid.templateRows = { grid.templateRows = {
juce::Grid::TrackInfo(juce::Grid::Fr(20)), // scope row 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 1
juce::Grid::TrackInfo(juce::Grid::Fr(40)) // row 2 juce::Grid::TrackInfo(juce::Grid::Fr(40)) // row 2
}; };
// 6 columns: 5 content + 1 sidebar (waveform+master) // 6 columns: 5 content + 1 sidebar (waveform+master)
grid.templateColumns = { 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(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)) juce::Grid::TrackInfo(juce::Grid::Fr(10))
}; };
// Row 0 // Row 0
grid.items.add(juce::GridItem(mainScopeComponent) grid.items.add(juce::GridItem(mainScopeComponent)
.withArea(juce::GridItem::Span(1), juce::GridItem::Span(5))); .withArea(juce::GridItem::Span(1), juce::GridItem::Span(5)));
grid.items.add(juce::GridItem(waveformSelector) grid.items.add(juce::GridItem(waveformSelector)
.withArea(juce::GridItem::Span(1), juce::GridItem::Span(1))); .withArea(juce::GridItem::Span(1), juce::GridItem::Span(1)));
// Row 1 // Row 1
grid.items.add(juce::GridItem(*adsrComponent)); grid.items.add(juce::GridItem(*adsrComponent));
grid.items.add(juce::GridItem(*chorusComponent)); grid.items.add(juce::GridItem(*chorusComponent));
grid.items.add(juce::GridItem(*delayComponent)); grid.items.add(juce::GridItem(*delayComponent));
grid.items.add(juce::GridItem(*reverbComponent)); grid.items.add(juce::GridItem(*reverbComponent));
grid.items.add(juce::GridItem(*eqComponent)); grid.items.add(juce::GridItem(*eqComponent));
grid.items.add(juce::GridItem(masterLevelLabel)); grid.items.add(juce::GridItem(masterLevelLabel));
// Row 2 // Row 2
grid.items.add(juce::GridItem(*flangerComponent)); grid.items.add(juce::GridItem(*flangerComponent));
grid.items.add(juce::GridItem(*distortionComponent)); grid.items.add(juce::GridItem(*distortionComponent));
grid.items.add(juce::GridItem(*filterComponent)); grid.items.add(juce::GridItem(*filterComponent));
grid.items.add(juce::GridItem(*filterEnvComponent)); grid.items.add(juce::GridItem(*filterEnvComponent));
grid.items.add(juce::GridItem(blankPanel)); grid.items.add(juce::GridItem(blankPanel));
grid.items.add(juce::GridItem(masterLevelSlider)); grid.items.add(juce::GridItem(masterLevelSlider));
grid.performLayout(bounds); grid.performLayout(bounds);
} }

View File

@@ -1,341 +1,341 @@
#pragma once #pragma once
#include <JuceHeader.h> #include <JuceHeader.h>
#include "PluginProcessor.h" #include "PluginProcessor.h"
#include "GraphComponent.h" #include "GraphComponent.h"
#include "ScopeComponent.h" #include "ScopeComponent.h"
//============================== ScopeSliderComponent ========================== //============================== ScopeSliderComponent ==========================
// A generic panel: optional scope/graph + rotary sliders + labels. // A generic panel: optional scope/graph + rotary sliders + labels.
// Adds a per-panel "On" toggle (bound to "<group>_on"). // Adds a per-panel "On" toggle (bound to "<group>_on").
class ScopeSliderComponent : public juce::Component { class ScopeSliderComponent : public juce::Component {
static const int fontSize = 11; static const int fontSize = 11;
public: public:
ScopeSliderComponent(juce::AudioProcessorValueTreeState& tree, ScopeSliderComponent(juce::AudioProcessorValueTreeState& tree,
const std::string paramGroup, const std::string paramGroup,
const juce::String& titleText = {}) const juce::String& titleText = {})
: paramGroupId(paramGroup), treeRef(tree) : paramGroupId(paramGroup), treeRef(tree)
{ {
const auto& sliderDetails = PARAM_SETTINGS.at(paramGroup); const auto& sliderDetails = PARAM_SETTINGS.at(paramGroup);
for (const auto& [name, sliderDetail] : sliderDetails) { for (const auto& [name, sliderDetail] : sliderDetails) {
sliders.push_back(std::make_unique<juce::Slider>()); sliders.push_back(std::make_unique<juce::Slider>());
labels.push_back(std::make_unique<juce::Label>()); labels.push_back(std::make_unique<juce::Label>());
attachments.push_back(std::make_unique<juce::AudioProcessorValueTreeState::SliderAttachment>( attachments.push_back(std::make_unique<juce::AudioProcessorValueTreeState::SliderAttachment>(
tree, paramGroup + "_" + name, *sliders.back())); tree, paramGroup + "_" + name, *sliders.back()));
labels.back()->setText(sliderDetail.label, juce::dontSendNotification); labels.back()->setText(sliderDetail.label, juce::dontSendNotification);
sliders.back()->setRange(sliderDetail.min, sliderDetail.max); sliders.back()->setRange(sliderDetail.min, sliderDetail.max);
} }
for (auto& slider : sliders) for (auto& slider : sliders)
{ {
slider->setSliderStyle(juce::Slider::Rotary); slider->setSliderStyle(juce::Slider::Rotary);
slider->setTextBoxStyle(juce::Slider::TextBoxBelow, false, 50, 20); slider->setTextBoxStyle(juce::Slider::TextBoxBelow, false, 50, 20);
addAndMakeVisible(*slider); addAndMakeVisible(*slider);
} }
for (auto& label : labels) for (auto& label : labels)
{ {
juce::Font f; f.setHeight((float)fontSize); f.setBold(true); juce::Font f; f.setHeight((float)fontSize); f.setBold(true);
label->setFont(f); label->setFont(f);
label->setColour(juce::Label::textColourId, juce::Colours::lightgreen); label->setColour(juce::Label::textColourId, juce::Colours::lightgreen);
label->setJustificationType(juce::Justification::centred); label->setJustificationType(juce::Justification::centred);
addAndMakeVisible(*label); addAndMakeVisible(*label);
} }
if (titleText.isNotEmpty()) if (titleText.isNotEmpty())
{ {
titleLabel.setText(titleText, juce::dontSendNotification); titleLabel.setText(titleText, juce::dontSendNotification);
juce::Font tf; tf.setHeight(12.0f); tf.setBold(true); juce::Font tf; tf.setHeight(12.0f); tf.setBold(true);
titleLabel.setFont(tf); titleLabel.setFont(tf);
titleLabel.setJustificationType(juce::Justification::centredLeft); titleLabel.setJustificationType(juce::Justification::centredLeft);
titleLabel.setColour(juce::Label::textColourId, juce::Colours::white); titleLabel.setColour(juce::Label::textColourId, juce::Colours::white);
addAndMakeVisible(titleLabel); addAndMakeVisible(titleLabel);
} }
// Bypass toggle (per panel), id "<group>_on" // Bypass toggle (per panel), id "<group>_on"
bypassButton.setButtonText("On"); bypassButton.setButtonText("On");
bypassButton.setClickingTogglesState(true); bypassButton.setClickingTogglesState(true);
addAndMakeVisible(bypassButton); addAndMakeVisible(bypassButton);
bypassAttachment = std::make_unique<juce::AudioProcessorValueTreeState::ButtonAttachment>( bypassAttachment = std::make_unique<juce::AudioProcessorValueTreeState::ButtonAttachment>(
treeRef, paramGroupId + "_on", bypassButton); treeRef, paramGroupId + "_on", bypassButton);
} }
void enableSampleScope(AudioBufferQueue<float>& audioBufferQueue) { void enableSampleScope(AudioBufferQueue<float>& audioBufferQueue) {
scope.emplace(audioBufferQueue); scope.emplace(audioBufferQueue);
useGraphScope = false; useGraphScope = false;
addAndMakeVisible(*scope); addAndMakeVisible(*scope);
} }
void enableGraphScope(const std::function<float(float)>& func) { void enableGraphScope(const std::function<float(float)>& func) {
graphScope.emplace(0.0f, 1.0f, 100); graphScope.emplace(0.0f, 1.0f, 100);
graphScope->setFunction(func); graphScope->setFunction(func);
useGraphScope = true; useGraphScope = true;
addAndMakeVisible(*graphScope); addAndMakeVisible(*graphScope);
} }
private: private:
void paint(juce::Graphics& g) override void paint(juce::Graphics& g) override
{ {
g.fillAll(juce::Colours::darkgrey); g.fillAll(juce::Colours::darkgrey);
g.setColour(juce::Colours::white); g.setColour(juce::Colours::white);
g.drawRect(getLocalBounds()); g.drawRect(getLocalBounds());
} }
void resized() override void resized() override
{ {
// --- Top bar (manual) ---------------------------------------------- // --- Top bar (manual) ----------------------------------------------
auto area = getLocalBounds().reduced(10); auto area = getLocalBounds().reduced(10);
auto top = area.removeFromTop(22); auto top = area.removeFromTop(22);
auto btnW = 46; auto btnW = 46;
bypassButton.setBounds(top.removeFromRight(btnW).reduced(2, 1)); bypassButton.setBounds(top.removeFromRight(btnW).reduced(2, 1));
titleLabel.setBounds(top); titleLabel.setBounds(top);
// --- Rest (grid) ---------------------------------------------------- // --- Rest (grid) ----------------------------------------------------
juce::Grid grid; juce::Grid grid;
grid.templateRows = { grid.templateRows = {
juce::Grid::TrackInfo(juce::Grid::Fr(55)), // scope/graph juce::Grid::TrackInfo(juce::Grid::Fr(55)), // scope/graph
juce::Grid::TrackInfo(juce::Grid::Fr(30)), // sliders juce::Grid::TrackInfo(juce::Grid::Fr(30)), // sliders
juce::Grid::TrackInfo(juce::Grid::Fr(15)) // labels juce::Grid::TrackInfo(juce::Grid::Fr(15)) // labels
}; };
const int n = (int)sliders.size(); const int n = (int)sliders.size();
grid.templateColumns.resize(n); grid.templateColumns.resize(n);
for (int i = 0; i < n; ++i) for (int i = 0; i < n; ++i)
grid.templateColumns.getReference(i) = juce::Grid::TrackInfo(juce::Grid::Fr(1)); grid.templateColumns.getReference(i) = juce::Grid::TrackInfo(juce::Grid::Fr(1));
grid.items.clear(); grid.items.clear();
// Row 1: scope/graph only add if constructed // Row 1: scope/graph only add if constructed
if (useGraphScope) if (useGraphScope)
{ {
if (graphScope) if (graphScope)
grid.items.add(juce::GridItem(*graphScope) grid.items.add(juce::GridItem(*graphScope)
.withArea(juce::GridItem::Span(1), juce::GridItem::Span(n))); .withArea(juce::GridItem::Span(1), juce::GridItem::Span(n)));
else else
grid.items.add(juce::GridItem() grid.items.add(juce::GridItem()
.withArea(juce::GridItem::Span(1), juce::GridItem::Span(n))); .withArea(juce::GridItem::Span(1), juce::GridItem::Span(n)));
} }
else else
{ {
if (scope) if (scope)
grid.items.add(juce::GridItem(*scope) grid.items.add(juce::GridItem(*scope)
.withArea(juce::GridItem::Span(1), juce::GridItem::Span(n))); .withArea(juce::GridItem::Span(1), juce::GridItem::Span(n)));
else else
grid.items.add(juce::GridItem() grid.items.add(juce::GridItem()
.withArea(juce::GridItem::Span(1), juce::GridItem::Span(n))); .withArea(juce::GridItem::Span(1), juce::GridItem::Span(n)));
} }
// Row 2: sliders // Row 2: sliders
for (int i = 0; i < n; ++i) for (int i = 0; i < n; ++i)
grid.items.add(juce::GridItem(*sliders[(size_t)i])); grid.items.add(juce::GridItem(*sliders[(size_t)i]));
// Row 3: labels // Row 3: labels
for (int i = 0; i < n; ++i) for (int i = 0; i < n; ++i)
grid.items.add(juce::GridItem(*labels[(size_t)i])); grid.items.add(juce::GridItem(*labels[(size_t)i]));
grid.performLayout(area); grid.performLayout(area);
} }
bool useGraphScope{ false }; bool useGraphScope{ false };
std::optional<ScopeComponent<float>> scope; std::optional<ScopeComponent<float>> scope;
std::optional<GraphComponent<float>> graphScope; std::optional<GraphComponent<float>> graphScope;
std::vector<std::unique_ptr<juce::Slider>> sliders; std::vector<std::unique_ptr<juce::Slider>> sliders;
std::vector<std::unique_ptr<juce::Label>> labels; std::vector<std::unique_ptr<juce::Label>> labels;
std::vector<std::unique_ptr<juce::AudioProcessorValueTreeState::SliderAttachment>> attachments; std::vector<std::unique_ptr<juce::AudioProcessorValueTreeState::SliderAttachment>> attachments;
juce::ToggleButton bypassButton; juce::ToggleButton bypassButton;
std::unique_ptr<juce::AudioProcessorValueTreeState::ButtonAttachment> bypassAttachment; std::unique_ptr<juce::AudioProcessorValueTreeState::ButtonAttachment> bypassAttachment;
juce::Label titleLabel; juce::Label titleLabel;
std::string paramGroupId; std::string paramGroupId;
juce::AudioProcessorValueTreeState& treeRef; juce::AudioProcessorValueTreeState& treeRef;
}; };
//============================== EqualizerComponent ============================ //============================== EqualizerComponent ============================
// Adds an On/Off toggle bound to "eq_on". // Adds an On/Off toggle bound to "eq_on".
class EqualizerComponent : public juce::Component { class EqualizerComponent : public juce::Component {
static const int fontSize = 11; static const int fontSize = 11;
public: public:
explicit EqualizerComponent(juce::AudioProcessorValueTreeState& tree, explicit EqualizerComponent(juce::AudioProcessorValueTreeState& tree,
const juce::String& titleText = {}) const juce::String& titleText = {})
{ {
setupSlider(lowGainSlider); setupSlider(lowGainSlider);
setupSlider(midGainSlider); setupSlider(midGainSlider);
setupSlider(highGainSlider); setupSlider(highGainSlider);
setupLabel(lowGainLabel, "L"); setupLabel(lowGainLabel, "L");
setupLabel(midGainLabel, "M"); setupLabel(midGainLabel, "M");
setupLabel(highGainLabel, "H"); setupLabel(highGainLabel, "H");
if (titleText.isNotEmpty()) if (titleText.isNotEmpty())
{ {
titleLabel.setText(titleText, juce::dontSendNotification); titleLabel.setText(titleText, juce::dontSendNotification);
juce::Font tf; tf.setHeight(13.0f); tf.setBold(true); juce::Font tf; tf.setHeight(13.0f); tf.setBold(true);
titleLabel.setFont(tf); titleLabel.setFont(tf);
titleLabel.setJustificationType(juce::Justification::centredLeft); titleLabel.setJustificationType(juce::Justification::centredLeft);
titleLabel.setColour(juce::Label::textColourId, juce::Colours::white); titleLabel.setColour(juce::Label::textColourId, juce::Colours::white);
addAndMakeVisible(titleLabel); addAndMakeVisible(titleLabel);
} }
// Attachments // Attachments
lowGainAttachment = std::make_unique<juce::AudioProcessorValueTreeState::SliderAttachment>(tree, "lowEQ", lowGainSlider); lowGainAttachment = std::make_unique<juce::AudioProcessorValueTreeState::SliderAttachment>(tree, "lowEQ", lowGainSlider);
midGainAttachment = std::make_unique<juce::AudioProcessorValueTreeState::SliderAttachment>(tree, "midEQ", midGainSlider); midGainAttachment = std::make_unique<juce::AudioProcessorValueTreeState::SliderAttachment>(tree, "midEQ", midGainSlider);
highGainAttachment = std::make_unique<juce::AudioProcessorValueTreeState::SliderAttachment>(tree, "highEQ", highGainSlider); highGainAttachment = std::make_unique<juce::AudioProcessorValueTreeState::SliderAttachment>(tree, "highEQ", highGainSlider);
// EQ bypass toggle // EQ bypass toggle
bypassButton.setButtonText("On"); bypassButton.setButtonText("On");
bypassButton.setClickingTogglesState(true); bypassButton.setClickingTogglesState(true);
addAndMakeVisible(bypassButton); addAndMakeVisible(bypassButton);
bypassAttachment = std::make_unique<juce::AudioProcessorValueTreeState::ButtonAttachment>(tree, "eq_on", bypassButton); bypassAttachment = std::make_unique<juce::AudioProcessorValueTreeState::ButtonAttachment>(tree, "eq_on", bypassButton);
} }
private: private:
void setupSlider(juce::Slider& slider) { void setupSlider(juce::Slider& slider) {
slider.setRange(-24.0f, 24.0f, 0.1f); slider.setRange(-24.0f, 24.0f, 0.1f);
slider.setSliderStyle(juce::Slider::LinearBarVertical); slider.setSliderStyle(juce::Slider::LinearBarVertical);
slider.setTextBoxStyle(juce::Slider::TextBoxBelow, false, 50, 20); slider.setTextBoxStyle(juce::Slider::TextBoxBelow, false, 50, 20);
addAndMakeVisible(slider); addAndMakeVisible(slider);
} }
void setupLabel(juce::Label& lbl, juce::String txt) { void setupLabel(juce::Label& lbl, juce::String txt) {
juce::Font f; f.setHeight((float)fontSize); f.setBold(true); juce::Font f; f.setHeight((float)fontSize); f.setBold(true);
lbl.setFont(f); lbl.setFont(f);
lbl.setColour(juce::Label::textColourId, juce::Colours::lightgreen); lbl.setColour(juce::Label::textColourId, juce::Colours::lightgreen);
lbl.setJustificationType(juce::Justification::centred); lbl.setJustificationType(juce::Justification::centred);
lbl.setText(txt, juce::dontSendNotification); lbl.setText(txt, juce::dontSendNotification);
addAndMakeVisible(lbl); addAndMakeVisible(lbl);
} }
void paint(juce::Graphics& g) override { void paint(juce::Graphics& g) override {
g.fillAll(juce::Colours::darkgrey); g.fillAll(juce::Colours::darkgrey);
g.setColour(juce::Colours::white); g.setColour(juce::Colours::white);
g.drawRect(getLocalBounds()); g.drawRect(getLocalBounds());
} }
void resized() override { void resized() override {
auto area = getLocalBounds().reduced(10); auto area = getLocalBounds().reduced(10);
auto top = area.removeFromTop(22); auto top = area.removeFromTop(22);
auto btnW = 46; auto btnW = 46;
bypassButton.setBounds(top.removeFromRight(btnW).reduced(2, 1)); bypassButton.setBounds(top.removeFromRight(btnW).reduced(2, 1));
titleLabel.setBounds(top); titleLabel.setBounds(top);
juce::Grid grid; juce::Grid grid;
grid.templateRows = { grid.templateRows = {
juce::Grid::TrackInfo(juce::Grid::Fr(1)), juce::Grid::TrackInfo(juce::Grid::Fr(1)),
juce::Grid::TrackInfo(juce::Grid::Fr(1)) juce::Grid::TrackInfo(juce::Grid::Fr(1))
}; };
grid.templateColumns = { grid.templateColumns = {
juce::Grid::TrackInfo(juce::Grid::Fr(1)), juce::Grid::TrackInfo(juce::Grid::Fr(1)),
juce::Grid::TrackInfo(juce::Grid::Fr(1)), juce::Grid::TrackInfo(juce::Grid::Fr(1)),
juce::Grid::TrackInfo(juce::Grid::Fr(1)) juce::Grid::TrackInfo(juce::Grid::Fr(1))
}; };
grid.items = { grid.items = {
lowGainSlider, midGainSlider, highGainSlider, lowGainSlider, midGainSlider, highGainSlider,
lowGainLabel, midGainLabel, highGainLabel lowGainLabel, midGainLabel, highGainLabel
}; };
grid.performLayout(area); grid.performLayout(area);
} }
juce::Slider lowGainSlider, midGainSlider, highGainSlider; juce::Slider lowGainSlider, midGainSlider, highGainSlider;
juce::Label lowGainLabel, midGainLabel, highGainLabel; juce::Label lowGainLabel, midGainLabel, highGainLabel;
std::unique_ptr<juce::AudioProcessorValueTreeState::SliderAttachment> lowGainAttachment, midGainAttachment, highGainAttachment; std::unique_ptr<juce::AudioProcessorValueTreeState::SliderAttachment> lowGainAttachment, midGainAttachment, highGainAttachment;
juce::ToggleButton bypassButton; juce::ToggleButton bypassButton;
std::unique_ptr<juce::AudioProcessorValueTreeState::ButtonAttachment> bypassAttachment; std::unique_ptr<juce::AudioProcessorValueTreeState::ButtonAttachment> bypassAttachment;
juce::Label titleLabel; juce::Label titleLabel;
}; };
//============================== Waveform List Model =========================== //============================== Waveform List Model ===========================
struct WaveformSelectorContents final : public juce::ListBoxModel struct WaveformSelectorContents final : public juce::ListBoxModel
{ {
int getNumRows() override { return 4; } int getNumRows() override { return 4; }
void paintListBoxItem(int rowNumber, juce::Graphics& g, void paintListBoxItem(int rowNumber, juce::Graphics& g,
int width, int height, bool rowIsSelected) override int width, int height, bool rowIsSelected) override
{ {
if (rowIsSelected) g.fillAll(juce::Colours::lightblue); if (rowIsSelected) g.fillAll(juce::Colours::lightblue);
g.setColour(juce::LookAndFeel::getDefaultLookAndFeel() g.setColour(juce::LookAndFeel::getDefaultLookAndFeel()
.findColour(juce::Label::textColourId)); .findColour(juce::Label::textColourId));
juce::Font f; f.setHeight((float)height * 0.7f); juce::Font f; f.setHeight((float)height * 0.7f);
g.setFont(f); g.setFont(f);
g.drawText(waves[(size_t)rowNumber], 5, 0, width, height, g.drawText(waves[(size_t)rowNumber], 5, 0, width, height,
juce::Justification::centredLeft, true); juce::Justification::centredLeft, true);
} }
void selectedRowsChanged (int lastRowSelected) override void selectedRowsChanged (int lastRowSelected) override
{ {
if (onSelect) onSelect(lastRowSelected); if (onSelect) onSelect(lastRowSelected);
} }
std::function<void (int)> onSelect; std::function<void (int)> onSelect;
std::vector<juce::String> waves { "Sine", "Saw", "Square", "Triangle" }; std::vector<juce::String> waves { "Sine", "Saw", "Square", "Triangle" };
}; };
//============================== MasterVolumeComponent ========================= //============================== MasterVolumeComponent =========================
class MasterVolumeComponent : public juce::Component class MasterVolumeComponent : public juce::Component
{ {
public: public:
MasterVolumeComponent() MasterVolumeComponent()
{ {
slider.setSliderStyle(juce::Slider::LinearBarVertical); slider.setSliderStyle(juce::Slider::LinearBarVertical);
slider.setTextBoxStyle(juce::Slider::NoTextBox, false, 20, 20); slider.setTextBoxStyle(juce::Slider::NoTextBox, false, 20, 20);
addAndMakeVisible(slider); addAndMakeVisible(slider);
} }
void resized() override void resized() override
{ {
slider.setBounds(getLocalBounds().reduced(30)); slider.setBounds(getLocalBounds().reduced(30));
} }
juce::Slider slider; juce::Slider slider;
}; };
//============================== Editor ======================================= //============================== Editor =======================================
class NeuralSynthAudioProcessorEditor : public juce::AudioProcessorEditor class NeuralSynthAudioProcessorEditor : public juce::AudioProcessorEditor
{ {
public: public:
NeuralSynthAudioProcessorEditor (NeuralSynthAudioProcessor&); NeuralSynthAudioProcessorEditor (NeuralSynthAudioProcessor&);
~NeuralSynthAudioProcessorEditor() override; ~NeuralSynthAudioProcessorEditor() override;
void paint (juce::Graphics&) override; void paint (juce::Graphics&) override;
void resized() override; void resized() override;
private: private:
NeuralSynthAudioProcessor& audioProcessor; NeuralSynthAudioProcessor& audioProcessor;
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (NeuralSynthAudioProcessorEditor) JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (NeuralSynthAudioProcessorEditor)
juce::ListBox waveformSelector; juce::ListBox waveformSelector;
WaveformSelectorContents waveformContents; WaveformSelectorContents waveformContents;
std::optional<ScopeSliderComponent> adsrComponent; // Amp Env std::optional<ScopeSliderComponent> adsrComponent; // Amp Env
std::optional<ScopeSliderComponent> chorusComponent; std::optional<ScopeSliderComponent> chorusComponent;
std::optional<ScopeSliderComponent> delayComponent; std::optional<ScopeSliderComponent> delayComponent;
std::optional<ScopeSliderComponent> reverbComponent; std::optional<ScopeSliderComponent> reverbComponent;
std::optional<ScopeSliderComponent> flangerComponent; std::optional<ScopeSliderComponent> flangerComponent;
std::optional<ScopeSliderComponent> distortionComponent; std::optional<ScopeSliderComponent> distortionComponent;
std::optional<ScopeSliderComponent> filterComponent; std::optional<ScopeSliderComponent> filterComponent;
std::optional<ScopeSliderComponent> filterEnvComponent; // Filter Env panel std::optional<ScopeSliderComponent> filterEnvComponent; // Filter Env panel
MasterVolumeComponent masterLevelSlider; MasterVolumeComponent masterLevelSlider;
juce::Label masterLevelLabel; juce::Label masterLevelLabel;
std::optional<EqualizerComponent> eqComponent; std::optional<EqualizerComponent> eqComponent;
std::unique_ptr<juce::AudioProcessorValueTreeState::SliderAttachment> gainAttachment; std::unique_ptr<juce::AudioProcessorValueTreeState::SliderAttachment> gainAttachment;
ScopeComponent<float> mainScopeComponent; ScopeComponent<float> mainScopeComponent;
juce::Component blankPanel; juce::Component blankPanel;
}; };

View File

@@ -1,270 +1,270 @@
#include "PluginProcessor.h" #include "PluginProcessor.h"
#include "PluginEditor.h" #include "PluginEditor.h"
//============================================================================== //==============================================================================
NeuralSynthAudioProcessor::NeuralSynthAudioProcessor() NeuralSynthAudioProcessor::NeuralSynthAudioProcessor()
: parameters(*this, nullptr, "PARAMETERS", createParameterLayout()) : parameters(*this, nullptr, "PARAMETERS", createParameterLayout())
, AudioProcessor(BusesProperties().withOutput("Output", juce::AudioChannelSet::stereo(), true)) , AudioProcessor(BusesProperties().withOutput("Output", juce::AudioChannelSet::stereo(), true))
, audioEngine(sp) , audioEngine(sp)
{ {
parameters.addParameterListener("waveform", this); parameters.addParameterListener("waveform", this);
// === Per-panel bypass (default OFF) === // === Per-panel bypass (default OFF) ===
sp.chorusOn = parameters.getRawParameterValue("chorus_on"); sp.chorusOn = parameters.getRawParameterValue("chorus_on");
sp.delayOn = parameters.getRawParameterValue("delay_on"); sp.delayOn = parameters.getRawParameterValue("delay_on");
sp.reverbOn = parameters.getRawParameterValue("reverb_on"); sp.reverbOn = parameters.getRawParameterValue("reverb_on");
sp.flangerOn = parameters.getRawParameterValue("flanger_on"); sp.flangerOn = parameters.getRawParameterValue("flanger_on");
sp.distortionOn = parameters.getRawParameterValue("distortion_on"); sp.distortionOn = parameters.getRawParameterValue("distortion_on");
sp.filterOn = parameters.getRawParameterValue("filter_on"); sp.filterOn = parameters.getRawParameterValue("filter_on");
sp.eqOn = parameters.getRawParameterValue("eq_on"); sp.eqOn = parameters.getRawParameterValue("eq_on");
// === Chorus === // === Chorus ===
parameters.addParameterListener("chorus_rate", this); parameters.addParameterListener("chorus_rate", this);
parameters.addParameterListener("chorus_depth", this); parameters.addParameterListener("chorus_depth", this);
parameters.addParameterListener("chorus_centre", this); parameters.addParameterListener("chorus_centre", this);
parameters.addParameterListener("chorus_feedback", this); parameters.addParameterListener("chorus_feedback", this);
parameters.addParameterListener("chorus_mix", this); parameters.addParameterListener("chorus_mix", this);
sp.chorusRate = parameters.getRawParameterValue("chorus_rate"); sp.chorusRate = parameters.getRawParameterValue("chorus_rate");
sp.chorusDepth = parameters.getRawParameterValue("chorus_depth"); sp.chorusDepth = parameters.getRawParameterValue("chorus_depth");
sp.chorusCentre = parameters.getRawParameterValue("chorus_centre"); sp.chorusCentre = parameters.getRawParameterValue("chorus_centre");
sp.chorusFeedback = parameters.getRawParameterValue("chorus_feedback"); sp.chorusFeedback = parameters.getRawParameterValue("chorus_feedback");
sp.chorusMix = parameters.getRawParameterValue("chorus_mix"); sp.chorusMix = parameters.getRawParameterValue("chorus_mix");
// === Delay === // === Delay ===
parameters.addParameterListener("delay_delay", this); parameters.addParameterListener("delay_delay", this);
sp.delayTime = parameters.getRawParameterValue("delay_delay"); sp.delayTime = parameters.getRawParameterValue("delay_delay");
// === Reverb === // === Reverb ===
parameters.addParameterListener("reverb_roomSize", this); parameters.addParameterListener("reverb_roomSize", this);
parameters.addParameterListener("reverb_damping", this); parameters.addParameterListener("reverb_damping", this);
parameters.addParameterListener("reverb_wetLevel", this); parameters.addParameterListener("reverb_wetLevel", this);
parameters.addParameterListener("reverb_dryLevel", this); parameters.addParameterListener("reverb_dryLevel", this);
parameters.addParameterListener("reverb_width", this); parameters.addParameterListener("reverb_width", this);
parameters.addParameterListener("reverb_freezeMode", this); parameters.addParameterListener("reverb_freezeMode", this);
sp.reverbRoomSize = parameters.getRawParameterValue("reverb_roomSize"); sp.reverbRoomSize = parameters.getRawParameterValue("reverb_roomSize");
sp.reverbDamping = parameters.getRawParameterValue("reverb_damping"); sp.reverbDamping = parameters.getRawParameterValue("reverb_damping");
sp.reverbWetLevel = parameters.getRawParameterValue("reverb_wetLevel"); sp.reverbWetLevel = parameters.getRawParameterValue("reverb_wetLevel");
sp.reverbDryLevel = parameters.getRawParameterValue("reverb_dryLevel"); sp.reverbDryLevel = parameters.getRawParameterValue("reverb_dryLevel");
sp.reverbWidth = parameters.getRawParameterValue("reverb_width"); sp.reverbWidth = parameters.getRawParameterValue("reverb_width");
sp.reverbFreezeMode= parameters.getRawParameterValue("reverb_freezeMode"); sp.reverbFreezeMode= parameters.getRawParameterValue("reverb_freezeMode");
// === Amp ADSR === // === Amp ADSR ===
parameters.addParameterListener("adsr_attack", this); parameters.addParameterListener("adsr_attack", this);
parameters.addParameterListener("adsr_decay", this); parameters.addParameterListener("adsr_decay", this);
parameters.addParameterListener("adsr_sustain", this); parameters.addParameterListener("adsr_sustain", this);
parameters.addParameterListener("adsr_release", this); parameters.addParameterListener("adsr_release", this);
sp.adsrAttack = parameters.getRawParameterValue("adsr_attack"); sp.adsrAttack = parameters.getRawParameterValue("adsr_attack");
sp.adsrDecay = parameters.getRawParameterValue("adsr_decay"); sp.adsrDecay = parameters.getRawParameterValue("adsr_decay");
sp.adsrSustain = parameters.getRawParameterValue("adsr_sustain"); sp.adsrSustain = parameters.getRawParameterValue("adsr_sustain");
sp.adsrRelease = parameters.getRawParameterValue("adsr_release"); sp.adsrRelease = parameters.getRawParameterValue("adsr_release");
// === Filter Env === // === Filter Env ===
parameters.addParameterListener("fenv_attack", this); parameters.addParameterListener("fenv_attack", this);
parameters.addParameterListener("fenv_decay", this); parameters.addParameterListener("fenv_decay", this);
parameters.addParameterListener("fenv_sustain", this); parameters.addParameterListener("fenv_sustain", this);
parameters.addParameterListener("fenv_release", this); parameters.addParameterListener("fenv_release", this);
parameters.addParameterListener("fenv_amount", this); parameters.addParameterListener("fenv_amount", this);
sp.fenvAttack = parameters.getRawParameterValue("fenv_attack"); sp.fenvAttack = parameters.getRawParameterValue("fenv_attack");
sp.fenvDecay = parameters.getRawParameterValue("fenv_decay"); sp.fenvDecay = parameters.getRawParameterValue("fenv_decay");
sp.fenvSustain = parameters.getRawParameterValue("fenv_sustain"); sp.fenvSustain = parameters.getRawParameterValue("fenv_sustain");
sp.fenvRelease = parameters.getRawParameterValue("fenv_release"); sp.fenvRelease = parameters.getRawParameterValue("fenv_release");
sp.fenvAmount = parameters.getRawParameterValue("fenv_amount"); sp.fenvAmount = parameters.getRawParameterValue("fenv_amount");
// === Filter base === // === Filter base ===
parameters.addParameterListener("filter_cutoff", this); parameters.addParameterListener("filter_cutoff", this);
parameters.addParameterListener("filter_resonance", this); parameters.addParameterListener("filter_resonance", this);
parameters.addParameterListener("filter_type", this); parameters.addParameterListener("filter_type", this);
parameters.addParameterListener("filter_drive", this); parameters.addParameterListener("filter_drive", this);
parameters.addParameterListener("filter_mod", this); parameters.addParameterListener("filter_mod", this);
parameters.addParameterListener("filter_key", this); parameters.addParameterListener("filter_key", this);
sp.filterCutoff = parameters.getRawParameterValue("filter_cutoff"); sp.filterCutoff = parameters.getRawParameterValue("filter_cutoff");
sp.filterResonance = parameters.getRawParameterValue("filter_resonance"); sp.filterResonance = parameters.getRawParameterValue("filter_resonance");
sp.filterType = parameters.getRawParameterValue("filter_type"); sp.filterType = parameters.getRawParameterValue("filter_type");
sp.filterDrive = parameters.getRawParameterValue("filter_drive"); sp.filterDrive = parameters.getRawParameterValue("filter_drive");
sp.filterMod = parameters.getRawParameterValue("filter_mod"); sp.filterMod = parameters.getRawParameterValue("filter_mod");
sp.filterKey = parameters.getRawParameterValue("filter_key"); sp.filterKey = parameters.getRawParameterValue("filter_key");
// === Distortion === // === Distortion ===
parameters.addParameterListener("distortion_drive", this); parameters.addParameterListener("distortion_drive", this);
parameters.addParameterListener("distortion_mix", this); parameters.addParameterListener("distortion_mix", this);
parameters.addParameterListener("distortion_bias", this); parameters.addParameterListener("distortion_bias", this);
parameters.addParameterListener("distortion_tone", this); parameters.addParameterListener("distortion_tone", this);
parameters.addParameterListener("distortion_shape", this); parameters.addParameterListener("distortion_shape", this);
sp.distortionDrive = parameters.getRawParameterValue("distortion_drive"); sp.distortionDrive = parameters.getRawParameterValue("distortion_drive");
sp.distortionMix = parameters.getRawParameterValue("distortion_mix"); sp.distortionMix = parameters.getRawParameterValue("distortion_mix");
sp.distortionBias = parameters.getRawParameterValue("distortion_bias"); sp.distortionBias = parameters.getRawParameterValue("distortion_bias");
sp.distortionTone = parameters.getRawParameterValue("distortion_tone"); sp.distortionTone = parameters.getRawParameterValue("distortion_tone");
sp.distortionShape = parameters.getRawParameterValue("distortion_shape"); sp.distortionShape = parameters.getRawParameterValue("distortion_shape");
// === Master / EQ === // === Master / EQ ===
parameters.addParameterListener("master", this); parameters.addParameterListener("master", this);
parameters.addParameterListener("lowEQ", this); parameters.addParameterListener("lowEQ", this);
parameters.addParameterListener("midEQ", this); parameters.addParameterListener("midEQ", this);
parameters.addParameterListener("highEQ", this); parameters.addParameterListener("highEQ", this);
sp.masterDbls = parameters.getRawParameterValue("master"); sp.masterDbls = parameters.getRawParameterValue("master");
sp.lowGainDbls = parameters.getRawParameterValue("lowEQ"); sp.lowGainDbls = parameters.getRawParameterValue("lowEQ");
sp.midGainDbls = parameters.getRawParameterValue("midEQ"); sp.midGainDbls = parameters.getRawParameterValue("midEQ");
sp.highGainDbls = parameters.getRawParameterValue("highEQ"); sp.highGainDbls = parameters.getRawParameterValue("highEQ");
} }
NeuralSynthAudioProcessor::~NeuralSynthAudioProcessor() = default; NeuralSynthAudioProcessor::~NeuralSynthAudioProcessor() = default;
//============================================================================== //==============================================================================
const juce::String NeuralSynthAudioProcessor::getName() const { return JucePlugin_Name; } const juce::String NeuralSynthAudioProcessor::getName() const { return JucePlugin_Name; }
bool NeuralSynthAudioProcessor::acceptsMidi() const bool NeuralSynthAudioProcessor::acceptsMidi() const
{ {
#if JucePlugin_WantsMidiInput #if JucePlugin_WantsMidiInput
return true; return true;
#else #else
return false; return false;
#endif #endif
} }
bool NeuralSynthAudioProcessor::producesMidi() const bool NeuralSynthAudioProcessor::producesMidi() const
{ {
#if JucePlugin_ProducesMidiOutput #if JucePlugin_ProducesMidiOutput
return true; return true;
#else #else
return false; return false;
#endif #endif
} }
bool NeuralSynthAudioProcessor::isMidiEffect() const bool NeuralSynthAudioProcessor::isMidiEffect() const
{ {
#if JucePlugin_IsMidiEffect #if JucePlugin_IsMidiEffect
return true; return true;
#else #else
return false; return false;
#endif #endif
} }
double NeuralSynthAudioProcessor::getTailLengthSeconds() const { return 0.0; } double NeuralSynthAudioProcessor::getTailLengthSeconds() const { return 0.0; }
int NeuralSynthAudioProcessor::getNumPrograms() { return 1; } int NeuralSynthAudioProcessor::getNumPrograms() { return 1; }
int NeuralSynthAudioProcessor::getCurrentProgram() { return 0; } int NeuralSynthAudioProcessor::getCurrentProgram() { return 0; }
void NeuralSynthAudioProcessor::setCurrentProgram (int) {} void NeuralSynthAudioProcessor::setCurrentProgram (int) {}
const juce::String NeuralSynthAudioProcessor::getProgramName (int) { return {}; } const juce::String NeuralSynthAudioProcessor::getProgramName (int) { return {}; }
void NeuralSynthAudioProcessor::changeProgramName (int, const juce::String&) {} void NeuralSynthAudioProcessor::changeProgramName (int, const juce::String&) {}
//============================================================================== //==============================================================================
void NeuralSynthAudioProcessor::prepareToPlay (double sampleRate, int samplesPerBlock) void NeuralSynthAudioProcessor::prepareToPlay (double sampleRate, int samplesPerBlock)
{ {
audioEngine.prepare({ sampleRate, (juce::uint32)samplesPerBlock, 2 }); audioEngine.prepare({ sampleRate, (juce::uint32)samplesPerBlock, 2 });
midiMessageCollector.reset(sampleRate); midiMessageCollector.reset(sampleRate);
} }
void NeuralSynthAudioProcessor::releaseResources() {} void NeuralSynthAudioProcessor::releaseResources() {}
bool NeuralSynthAudioProcessor::isBusesLayoutSupported (const BusesLayout& layouts) const bool NeuralSynthAudioProcessor::isBusesLayoutSupported (const BusesLayout& layouts) const
{ {
if (layouts.getMainOutputChannelSet() != juce::AudioChannelSet::mono() if (layouts.getMainOutputChannelSet() != juce::AudioChannelSet::mono()
&& layouts.getMainOutputChannelSet() != juce::AudioChannelSet::stereo()) && layouts.getMainOutputChannelSet() != juce::AudioChannelSet::stereo())
return false; return false;
return true; return true;
} }
void NeuralSynthAudioProcessor::processBlock(juce::AudioSampleBuffer& buffer, juce::MidiBuffer& midiMessages) void NeuralSynthAudioProcessor::processBlock(juce::AudioSampleBuffer& buffer, juce::MidiBuffer& midiMessages)
{ {
const int newWaveform = sp.waveform.exchange(-1); const int newWaveform = sp.waveform.exchange(-1);
if (newWaveform != -1) { if (newWaveform != -1) {
audioEngine.applyToVoices([newWaveform](NeuralSynthVoice* v) audioEngine.applyToVoices([newWaveform](NeuralSynthVoice* v)
{ {
v->changeWaveform(newWaveform); v->changeWaveform(newWaveform);
}); });
} }
juce::ScopedNoDenormals noDenormals; juce::ScopedNoDenormals noDenormals;
auto totalNumInputChannels = getTotalNumInputChannels(); auto totalNumInputChannels = getTotalNumInputChannels();
auto totalNumOutputChannels = getTotalNumOutputChannels(); auto totalNumOutputChannels = getTotalNumOutputChannels();
midiMessageCollector.removeNextBlockOfMessages(midiMessages, buffer.getNumSamples()); midiMessageCollector.removeNextBlockOfMessages(midiMessages, buffer.getNumSamples());
for (int i = totalNumInputChannels; i < totalNumOutputChannels; ++i) for (int i = totalNumInputChannels; i < totalNumOutputChannels; ++i)
buffer.clear(i, 0, buffer.getNumSamples()); buffer.clear(i, 0, buffer.getNumSamples());
audioEngine.renderNextBlock(buffer, midiMessages, 0, buffer.getNumSamples()); audioEngine.renderNextBlock(buffer, midiMessages, 0, buffer.getNumSamples());
scopeDataCollector.process(buffer.getReadPointer(0), (size_t)buffer.getNumSamples()); scopeDataCollector.process(buffer.getReadPointer(0), (size_t)buffer.getNumSamples());
} }
//============================================================================== //==============================================================================
bool NeuralSynthAudioProcessor::hasEditor() const { return true; } bool NeuralSynthAudioProcessor::hasEditor() const { return true; }
juce::AudioProcessorEditor* NeuralSynthAudioProcessor::createEditor() juce::AudioProcessorEditor* NeuralSynthAudioProcessor::createEditor()
{ {
return new NeuralSynthAudioProcessorEditor (*this); return new NeuralSynthAudioProcessorEditor (*this);
} }
//============================================================================== //==============================================================================
void NeuralSynthAudioProcessor::getStateInformation (juce::MemoryBlock& destData) { juce::ignoreUnused(destData); } void NeuralSynthAudioProcessor::getStateInformation (juce::MemoryBlock& destData) { juce::ignoreUnused(destData); }
void NeuralSynthAudioProcessor::setStateInformation (const void* data, int sizeInBytes) { juce::ignoreUnused(data, sizeInBytes); } void NeuralSynthAudioProcessor::setStateInformation (const void* data, int sizeInBytes) { juce::ignoreUnused(data, sizeInBytes); }
void NeuralSynthAudioProcessor::parameterChanged(const juce::String& id, float newValue) void NeuralSynthAudioProcessor::parameterChanged(const juce::String& id, float newValue)
{ {
juce::ignoreUnused(newValue); juce::ignoreUnused(newValue);
if (id == "waveform") if (id == "waveform")
sp.waveform.store((int)newValue, std::memory_order_release); sp.waveform.store((int)newValue, std::memory_order_release);
} }
//============================================================================== //==============================================================================
// This creates new instances of the plugin.. // This creates new instances of the plugin..
juce::AudioProcessor* JUCE_CALLTYPE createPluginFilter() { return new NeuralSynthAudioProcessor(); } juce::AudioProcessor* JUCE_CALLTYPE createPluginFilter() { return new NeuralSynthAudioProcessor(); }
void NeuralSynthAudioProcessor::buildParams(std::vector<std::unique_ptr<juce::RangedAudioParameter>>& params, const std::string& paramGroup) { void NeuralSynthAudioProcessor::buildParams(std::vector<std::unique_ptr<juce::RangedAudioParameter>>& params, const std::string& paramGroup) {
const auto& paramGroupSettings = PARAM_SETTINGS.at(paramGroup); const auto& paramGroupSettings = PARAM_SETTINGS.at(paramGroup);
for (const auto& [name, s] : paramGroupSettings) { for (const auto& [name, s] : paramGroupSettings) {
params.push_back(std::make_unique<juce::AudioParameterFloat>( params.push_back(std::make_unique<juce::AudioParameterFloat>(
paramGroup + "_" + name, s.label, paramGroup + "_" + name, s.label,
juce::NormalisableRange<float>(s.min, s.max, s.interval), juce::NormalisableRange<float>(s.min, s.max, s.interval),
s.defValue)); s.defValue));
} }
} }
juce::AudioProcessorValueTreeState::ParameterLayout NeuralSynthAudioProcessor::createParameterLayout() juce::AudioProcessorValueTreeState::ParameterLayout NeuralSynthAudioProcessor::createParameterLayout()
{ {
std::vector<std::unique_ptr<juce::RangedAudioParameter>> params; std::vector<std::unique_ptr<juce::RangedAudioParameter>> params;
params.push_back(std::make_unique<juce::AudioParameterChoice>( params.push_back(std::make_unique<juce::AudioParameterChoice>(
"waveform", "Waveform", "waveform", "Waveform",
juce::StringArray{ "Sine", "Saw", "Square", "Triangle" }, 0)); juce::StringArray{ "Sine", "Saw", "Square", "Triangle" }, 0));
// Per-panel bypass toggles (default OFF) // Per-panel bypass toggles (default OFF)
params.push_back(std::make_unique<juce::AudioParameterBool>("chorus_on", "Chorus On", false)); params.push_back(std::make_unique<juce::AudioParameterBool>("chorus_on", "Chorus On", false));
params.push_back(std::make_unique<juce::AudioParameterBool>("delay_on", "Delay On", false)); params.push_back(std::make_unique<juce::AudioParameterBool>("delay_on", "Delay On", false));
params.push_back(std::make_unique<juce::AudioParameterBool>("reverb_on", "Reverb On", false)); params.push_back(std::make_unique<juce::AudioParameterBool>("reverb_on", "Reverb On", false));
params.push_back(std::make_unique<juce::AudioParameterBool>("flanger_on", "Flanger On", false)); params.push_back(std::make_unique<juce::AudioParameterBool>("flanger_on", "Flanger On", false));
params.push_back(std::make_unique<juce::AudioParameterBool>("distortion_on", "Distortion On", false)); params.push_back(std::make_unique<juce::AudioParameterBool>("distortion_on", "Distortion On", false));
params.push_back(std::make_unique<juce::AudioParameterBool>("filter_on", "Filter On", false)); params.push_back(std::make_unique<juce::AudioParameterBool>("filter_on", "Filter On", false));
params.push_back(std::make_unique<juce::AudioParameterBool>("eq_on", "EQ On", false)); params.push_back(std::make_unique<juce::AudioParameterBool>("eq_on", "EQ On", false));
buildParams(params, "adsr"); buildParams(params, "adsr");
buildParams(params, "fenv"); buildParams(params, "fenv");
buildParams(params, "chorus"); buildParams(params, "chorus");
buildParams(params, "delay"); buildParams(params, "delay");
buildParams(params, "reverb"); buildParams(params, "reverb");
buildParams(params, "flanger"); buildParams(params, "flanger");
buildParams(params, "distortion"); buildParams(params, "distortion");
buildParams(params, "filter"); buildParams(params, "filter");
params.push_back(std::make_unique<juce::AudioParameterFloat>("master", "Master", params.push_back(std::make_unique<juce::AudioParameterFloat>("master", "Master",
juce::NormalisableRange<float>(-24.0f, 24.0f, 0.1f), 0.1f)); juce::NormalisableRange<float>(-24.0f, 24.0f, 0.1f), 0.1f));
params.push_back(std::make_unique<juce::AudioParameterFloat>("lowEQ", "Low Gain", params.push_back(std::make_unique<juce::AudioParameterFloat>("lowEQ", "Low Gain",
juce::NormalisableRange<float>(-24.0f, 24.0f, 0.1f), 0.5f)); juce::NormalisableRange<float>(-24.0f, 24.0f, 0.1f), 0.5f));
params.push_back(std::make_unique<juce::AudioParameterFloat>("midEQ", "Mid EQ", params.push_back(std::make_unique<juce::AudioParameterFloat>("midEQ", "Mid EQ",
juce::NormalisableRange<float>(-24.0f, 24.0f, 0.1f), 0.8f)); juce::NormalisableRange<float>(-24.0f, 24.0f, 0.1f), 0.8f));
params.push_back(std::make_unique<juce::AudioParameterFloat>("highEQ", "High EQ", params.push_back(std::make_unique<juce::AudioParameterFloat>("highEQ", "High EQ",
juce::NormalisableRange<float>(-24.0f, 24.0f, 0.1f), 1.0f)); juce::NormalisableRange<float>(-24.0f, 24.0f, 0.1f), 1.0f));
return { params.begin(), params.end() }; return { params.begin(), params.end() };
} }

View File

@@ -1,90 +1,90 @@
#pragma once #pragma once
#include <JuceHeader.h> #include <JuceHeader.h>
#include "AudioBufferQueue.h" #include "AudioBufferQueue.h"
#include "AudioEngine.h" #include "AudioEngine.h"
#include "ScopeDataCollector.h" #include "ScopeDataCollector.h"
#include "NeuralSharedParams.h" #include "NeuralSharedParams.h"
//============================================================================== //==============================================================================
// Processor // Processor
class NeuralSynthAudioProcessor : public juce::AudioProcessor, class NeuralSynthAudioProcessor : public juce::AudioProcessor,
private juce::AudioProcessorValueTreeState::Listener private juce::AudioProcessorValueTreeState::Listener
{ {
public: public:
NeuralSynthAudioProcessor(); NeuralSynthAudioProcessor();
~NeuralSynthAudioProcessor() override; ~NeuralSynthAudioProcessor() override;
// AudioProcessor overrides // AudioProcessor overrides
void prepareToPlay(double sampleRate, int samplesPerBlock) override; void prepareToPlay(double sampleRate, int samplesPerBlock) override;
void releaseResources() override; void releaseResources() override;
#ifndef JucePlugin_PreferredChannelConfigurations #ifndef JucePlugin_PreferredChannelConfigurations
bool isBusesLayoutSupported(const BusesLayout& layouts) const override; bool isBusesLayoutSupported(const BusesLayout& layouts) const override;
#endif #endif
void processBlock(juce::AudioBuffer<float>&, juce::MidiBuffer&) override; void processBlock(juce::AudioBuffer<float>&, juce::MidiBuffer&) override;
// Editor // Editor
juce::AudioProcessorEditor* createEditor() override; juce::AudioProcessorEditor* createEditor() override;
bool hasEditor() const override; bool hasEditor() const override;
// Info // Info
const juce::String getName() const override; const juce::String getName() const override;
bool acceptsMidi() const override; bool acceptsMidi() const override;
bool producesMidi() const override; bool producesMidi() const override;
bool isMidiEffect() const override; bool isMidiEffect() const override;
double getTailLengthSeconds() const override; double getTailLengthSeconds() const override;
// Programs // Programs
int getNumPrograms() override; int getNumPrograms() override;
int getCurrentProgram() override; int getCurrentProgram() override;
void setCurrentProgram(int index) override; void setCurrentProgram(int index) override;
const juce::String getProgramName(int index) override; const juce::String getProgramName(int index) override;
void changeProgramName(int index, const juce::String& newName) override; void changeProgramName(int index, const juce::String& newName) override;
// State // State
void getStateInformation(juce::MemoryBlock& destData) override; void getStateInformation(juce::MemoryBlock& destData) override;
void setStateInformation(const void* data, int sizeInBytes) override; void setStateInformation(const void* data, int sizeInBytes) override;
// Parameters // Parameters
void parameterChanged(const juce::String& id, float newValue) override; void parameterChanged(const juce::String& id, float newValue) override;
void buildParams(std::vector<std::unique_ptr<juce::RangedAudioParameter>>& params, void buildParams(std::vector<std::unique_ptr<juce::RangedAudioParameter>>& params,
const std::string& paramGroup); const std::string& paramGroup);
juce::AudioProcessorValueTreeState::ParameterLayout createParameterLayout(); juce::AudioProcessorValueTreeState::ParameterLayout createParameterLayout();
// Utilities // Utilities
juce::MidiMessageCollector& getMidiMessageCollector() noexcept { return midiMessageCollector; } juce::MidiMessageCollector& getMidiMessageCollector() noexcept { return midiMessageCollector; }
AudioBufferQueue<float>& getAudioBufferQueue() noexcept { return audioBufferQueue; } AudioBufferQueue<float>& getAudioBufferQueue() noexcept { return audioBufferQueue; }
AudioBufferQueue<float>& getChorusAudioBufferQueue() noexcept { return chorusBufferQueue; } AudioBufferQueue<float>& getChorusAudioBufferQueue() noexcept { return chorusBufferQueue; }
AudioBufferQueue<float>& getDelayAudioBufferQueue() noexcept { return delayBufferQueue; } AudioBufferQueue<float>& getDelayAudioBufferQueue() noexcept { return delayBufferQueue; }
AudioBufferQueue<float>& getReverbAudioBufferQueue() noexcept { return reverbBufferQueue; } AudioBufferQueue<float>& getReverbAudioBufferQueue() noexcept { return reverbBufferQueue; }
AudioBufferQueue<float>& getFlangerAudioBufferQueue() noexcept { return flangerBufferQueue; } AudioBufferQueue<float>& getFlangerAudioBufferQueue() noexcept { return flangerBufferQueue; }
AudioBufferQueue<float>& getDistortionAudioBufferQueue() noexcept { return distortionBufferQueue; } AudioBufferQueue<float>& getDistortionAudioBufferQueue() noexcept { return distortionBufferQueue; }
AudioBufferQueue<float>& getFilterAudioBufferQueue() noexcept { return filterBufferQueue; } AudioBufferQueue<float>& getFilterAudioBufferQueue() noexcept { return filterBufferQueue; }
// Public members (by JUCE convention) // Public members (by JUCE convention)
juce::MidiMessageCollector midiMessageCollector; juce::MidiMessageCollector midiMessageCollector;
juce::AudioProcessorValueTreeState parameters; juce::AudioProcessorValueTreeState parameters;
private: private:
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (NeuralSynthAudioProcessor) JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (NeuralSynthAudioProcessor)
// ---- IMPORTANT ORDER FIX ---- // ---- IMPORTANT ORDER FIX ----
// Objects are constructed in THIS order. 'sp' must come BEFORE audioEngine. // Objects are constructed in THIS order. 'sp' must come BEFORE audioEngine.
NeuralSharedParams sp; // <— construct first NeuralSharedParams sp; // <— construct first
NeuralAudioEngine audioEngine; // needs a valid reference to 'sp' NeuralAudioEngine audioEngine; // needs a valid reference to 'sp'
// Meter/scope queues // Meter/scope queues
AudioBufferQueue<float> audioBufferQueue; AudioBufferQueue<float> audioBufferQueue;
AudioBufferQueue<float> chorusBufferQueue; AudioBufferQueue<float> chorusBufferQueue;
AudioBufferQueue<float> delayBufferQueue; AudioBufferQueue<float> delayBufferQueue;
AudioBufferQueue<float> reverbBufferQueue; AudioBufferQueue<float> reverbBufferQueue;
AudioBufferQueue<float> flangerBufferQueue; AudioBufferQueue<float> flangerBufferQueue;
AudioBufferQueue<float> distortionBufferQueue; AudioBufferQueue<float> distortionBufferQueue;
AudioBufferQueue<float> filterBufferQueue; AudioBufferQueue<float> filterBufferQueue;
// Scope collector (uses audioBufferQueue, so declare after it) // Scope collector (uses audioBufferQueue, so declare after it)
ScopeDataCollector<float> scopeDataCollector { audioBufferQueue }; ScopeDataCollector<float> scopeDataCollector { audioBufferQueue };
}; };

View File

@@ -1,102 +1,102 @@
#pragma once #pragma once
#include "AudioBufferQueue.h" #include "AudioBufferQueue.h"
//============================================================================== //==============================================================================
template <typename SampleType> template <typename SampleType>
class ScopeComponent : public juce::Component, class ScopeComponent : public juce::Component,
private juce::Timer private juce::Timer
{ {
public: public:
using Queue = AudioBufferQueue<SampleType>; using Queue = AudioBufferQueue<SampleType>;
//============================================================================== //==============================================================================
ScopeComponent(Queue& queueToUse) ScopeComponent(Queue& queueToUse)
: audioBufferQueue(queueToUse) : audioBufferQueue(queueToUse)
{ {
sampleData.fill(SampleType(0)); sampleData.fill(SampleType(0));
setFramesPerSecond(30); setFramesPerSecond(30);
} }
//============================================================================== //==============================================================================
void setFramesPerSecond(int framesPerSecond) void setFramesPerSecond(int framesPerSecond)
{ {
jassert(framesPerSecond > 0 && framesPerSecond < 1000); jassert(framesPerSecond > 0 && framesPerSecond < 1000);
startTimerHz(framesPerSecond); startTimerHz(framesPerSecond);
} }
//============================================================================== //==============================================================================
void paint(juce::Graphics& g) override void paint(juce::Graphics& g) override
{ {
g.fillAll(juce::Colours::black); g.fillAll(juce::Colours::black);
g.setColour(juce::Colours::white); g.setColour(juce::Colours::white);
auto area = getLocalBounds(); auto area = getLocalBounds();
auto h = (SampleType)area.getHeight(); auto h = (SampleType)area.getHeight();
auto w = (SampleType)area.getWidth(); auto w = (SampleType)area.getWidth();
// Oscilloscope // Oscilloscope
auto scopeRect = juce::Rectangle<SampleType>{ SampleType(0), SampleType(0), w, h / 2 }; auto scopeRect = juce::Rectangle<SampleType>{ SampleType(0), SampleType(0), w, h / 2 };
plot(sampleData.data(), sampleData.size(), g, scopeRect, SampleType(1), h / 4); plot(sampleData.data(), sampleData.size(), g, scopeRect, SampleType(1), h / 4);
// Spectrum // Spectrum
auto spectrumRect = juce::Rectangle<SampleType>{ SampleType(0), h / 2, w, h / 2 }; auto spectrumRect = juce::Rectangle<SampleType>{ SampleType(0), h / 2, w, h / 2 };
plot(spectrumData.data(), spectrumData.size() / 4, g, spectrumRect); plot(spectrumData.data(), spectrumData.size() / 4, g, spectrumRect);
} }
//============================================================================== //==============================================================================
void resized() override {} void resized() override {}
private: private:
//============================================================================== //==============================================================================
Queue& audioBufferQueue; Queue& audioBufferQueue;
std::array<SampleType, Queue::bufferSize> sampleData; std::array<SampleType, Queue::bufferSize> sampleData;
juce::dsp::FFT fft{ Queue::order }; juce::dsp::FFT fft{ Queue::order };
using WindowFun = juce::dsp::WindowingFunction<SampleType>; using WindowFun = juce::dsp::WindowingFunction<SampleType>;
WindowFun windowFun{ (size_t)fft.getSize(), WindowFun::hann }; WindowFun windowFun{ (size_t)fft.getSize(), WindowFun::hann };
std::array<SampleType, 2 * Queue::bufferSize> spectrumData; std::array<SampleType, 2 * Queue::bufferSize> spectrumData;
//============================================================================== //==============================================================================
void timerCallback() override void timerCallback() override
{ {
audioBufferQueue.pop(sampleData.data()); audioBufferQueue.pop(sampleData.data());
juce::FloatVectorOperations::copy(spectrumData.data(), sampleData.data(), (int)sampleData.size()); juce::FloatVectorOperations::copy(spectrumData.data(), sampleData.data(), (int)sampleData.size());
auto fftSize = (size_t)fft.getSize(); auto fftSize = (size_t)fft.getSize();
jassert(spectrumData.size() == 2 * fftSize); jassert(spectrumData.size() == 2 * fftSize);
windowFun.multiplyWithWindowingTable(spectrumData.data(), fftSize); windowFun.multiplyWithWindowingTable(spectrumData.data(), fftSize);
fft.performFrequencyOnlyForwardTransform(spectrumData.data()); fft.performFrequencyOnlyForwardTransform(spectrumData.data());
static constexpr auto mindB = SampleType(-160); static constexpr auto mindB = SampleType(-160);
static constexpr auto maxdB = SampleType(0); static constexpr auto maxdB = SampleType(0);
for (auto& s : spectrumData) 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)); s = juce::jmap(juce::jlimit(mindB, maxdB, juce::Decibels::gainToDecibels(s) - juce::Decibels::gainToDecibels(SampleType(fftSize))), mindB, maxdB, SampleType(0), SampleType(1));
repaint(); repaint();
} }
//============================================================================== //==============================================================================
static void plot(const SampleType* data, static void plot(const SampleType* data,
size_t numSamples, size_t numSamples,
juce::Graphics& g, juce::Graphics& g,
juce::Rectangle<SampleType> rect, juce::Rectangle<SampleType> rect,
SampleType scaler = SampleType(1), SampleType scaler = SampleType(1),
SampleType offset = SampleType(0)) SampleType offset = SampleType(0))
{ {
auto w = rect.getWidth(); auto w = rect.getWidth();
auto h = rect.getHeight(); auto h = rect.getHeight();
auto right = rect.getRight(); auto right = rect.getRight();
auto center = rect.getBottom() - offset; auto center = rect.getBottom() - offset;
auto gain = h * scaler; auto gain = h * scaler;
for (size_t i = 1; i < numSamples; ++i) 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)), g.drawLine({ juce::jmap(SampleType(i - 1), SampleType(0), SampleType(numSamples - 1), SampleType(right - w), SampleType(right)),
center - gain * data[i - 1], center - gain * data[i - 1],
juce::jmap(SampleType(i), SampleType(0), SampleType(numSamples - 1), SampleType(right - w), SampleType(right)), juce::jmap(SampleType(i), SampleType(0), SampleType(numSamples - 1), SampleType(right - w), SampleType(right)),
center - gain * data[i] }); center - gain * data[i] });
} }
}; };

View File

@@ -1,62 +1,62 @@
#pragma once #pragma once
template <typename SampleType> template <typename SampleType>
class ScopeDataCollector class ScopeDataCollector
{ {
public: public:
//============================================================================== //==============================================================================
ScopeDataCollector(AudioBufferQueue<SampleType>& queueToUse) ScopeDataCollector(AudioBufferQueue<SampleType>& queueToUse)
: audioBufferQueue(queueToUse) : audioBufferQueue(queueToUse)
{ {
} }
//============================================================================== //==============================================================================
void process(const SampleType* data, size_t numSamples) void process(const SampleType* data, size_t numSamples)
{ {
size_t index = 0; size_t index = 0;
if (state == State::waitingForTrigger) if (state == State::waitingForTrigger)
{ {
while (index++ < numSamples) while (index++ < numSamples)
{ {
auto currentSample = *data++; auto currentSample = *data++;
if (currentSample >= triggerLevel && prevSample < triggerLevel) if (currentSample >= triggerLevel && prevSample < triggerLevel)
{ {
numCollected = 0; numCollected = 0;
state = State::collecting; state = State::collecting;
break; break;
} }
prevSample = currentSample; prevSample = currentSample;
} }
} }
if (state == State::collecting) if (state == State::collecting)
{ {
while (index++ < numSamples) while (index++ < numSamples)
{ {
buffer[numCollected++] = *data++; buffer[numCollected++] = *data++;
if (numCollected == buffer.size()) if (numCollected == buffer.size())
{ {
audioBufferQueue.push(buffer.data(), buffer.size()); audioBufferQueue.push(buffer.data(), buffer.size());
state = State::waitingForTrigger; state = State::waitingForTrigger;
prevSample = SampleType(100); prevSample = SampleType(100);
break; break;
} }
} }
} }
} }
private: private:
//============================================================================== //==============================================================================
AudioBufferQueue<SampleType>& audioBufferQueue; AudioBufferQueue<SampleType>& audioBufferQueue;
std::array<SampleType, AudioBufferQueue<SampleType>::bufferSize> buffer; std::array<SampleType, AudioBufferQueue<SampleType>::bufferSize> buffer;
size_t numCollected; size_t numCollected;
SampleType prevSample = SampleType(100); SampleType prevSample = SampleType(100);
static constexpr auto triggerLevel = SampleType(0.05); static constexpr auto triggerLevel = SampleType(0.05);
enum class State { waitingForTrigger, collecting } state{ State::waitingForTrigger }; enum class State { waitingForTrigger, collecting } state{ State::waitingForTrigger };
}; };

View File

@@ -1,398 +1,398 @@
#include "SynthVoice.h" #include "SynthVoice.h"
#include <cmath> #include <cmath>
//============================================================================== //==============================================================================
NeuralSynthVoice::NeuralSynthVoice (NeuralSharedParams& sp) NeuralSynthVoice::NeuralSynthVoice (NeuralSharedParams& sp)
: shared (sp) {} : shared (sp) {}
//============================================================================== //==============================================================================
void NeuralSynthVoice::prepare (const juce::dsp::ProcessSpec& newSpec) void NeuralSynthVoice::prepare (const juce::dsp::ProcessSpec& newSpec)
{ {
spec = newSpec; spec = newSpec;
// --- Oscillator // --- Oscillator
osc.prepare (spec.sampleRate); osc.prepare (spec.sampleRate);
setWaveform (0); // default to sine setWaveform (0); // default to sine
// --- Scratch buffer (IMPORTANT: allocate real memory) // --- Scratch buffer (IMPORTANT: allocate real memory)
tempBuffer.setSize ((int) spec.numChannels, (int) spec.maximumBlockSize, tempBuffer.setSize ((int) spec.numChannels, (int) spec.maximumBlockSize,
false, false, true); false, false, true);
tempBlock = juce::dsp::AudioBlock<float> (tempBuffer); tempBlock = juce::dsp::AudioBlock<float> (tempBuffer);
// --- Prepare chain elements // --- Prepare chain elements
chain.prepare (spec); chain.prepare (spec);
// Set maximum delay sizes BEFORE runtime changes // Set maximum delay sizes BEFORE runtime changes
{ {
// Flanger: up to 20 ms // Flanger: up to 20 ms
auto& flanger = chain.get<flangerIndex>(); auto& flanger = chain.get<flangerIndex>();
const size_t maxFlangerDelay = (size_t) juce::jmax<size_t>( const size_t maxFlangerDelay = (size_t) juce::jmax<size_t>(
1, (size_t) std::ceil (0.020 * spec.sampleRate)); 1, (size_t) std::ceil (0.020 * spec.sampleRate));
flanger.setMaximumDelayInSamples (maxFlangerDelay); flanger.setMaximumDelayInSamples (maxFlangerDelay);
flanger.reset(); flanger.reset();
} }
{ {
// Simple delay: up to 2 s // Simple delay: up to 2 s
auto& delay = chain.get<delayIndex>(); auto& delay = chain.get<delayIndex>();
const size_t maxDelay = (size_t) juce::jmax<size_t>( const size_t maxDelay = (size_t) juce::jmax<size_t>(
1, (size_t) std::ceil (2.0 * spec.sampleRate)); 1, (size_t) std::ceil (2.0 * spec.sampleRate));
delay.setMaximumDelayInSamples (maxDelay); delay.setMaximumDelayInSamples (maxDelay);
delay.reset(); delay.reset();
} }
// Envelopes // Envelopes
adsr.setSampleRate (spec.sampleRate); adsr.setSampleRate (spec.sampleRate);
filterAdsr.setSampleRate (spec.sampleRate); filterAdsr.setSampleRate (spec.sampleRate);
// Filter // Filter
svf.reset(); svf.reset();
svf.prepare (spec); svf.prepare (spec);
// Initial filter type // Initial filter type
const int type = (int) std::lround (juce::jlimit (0.0f, 2.0f, const int type = (int) std::lround (juce::jlimit (0.0f, 2.0f,
shared.filterType ? shared.filterType->load() : 0.0f)); shared.filterType ? shared.filterType->load() : 0.0f));
switch (type) switch (type)
{ {
case 0: svf.setType (juce::dsp::StateVariableTPTFilterType::lowpass); break; case 0: svf.setType (juce::dsp::StateVariableTPTFilterType::lowpass); break;
case 1: svf.setType (juce::dsp::StateVariableTPTFilterType::highpass); break; case 1: svf.setType (juce::dsp::StateVariableTPTFilterType::highpass); break;
case 2: svf.setType (juce::dsp::StateVariableTPTFilterType::bandpass); break; case 2: svf.setType (juce::dsp::StateVariableTPTFilterType::bandpass); break;
default: break; default: break;
} }
} }
//============================================================================== //==============================================================================
void NeuralSynthVoice::renderNextBlock (juce::AudioBuffer<float>& outputBuffer, void NeuralSynthVoice::renderNextBlock (juce::AudioBuffer<float>& outputBuffer,
int startSample, int numSamples) int startSample, int numSamples)
{ {
if (numSamples <= 0) if (numSamples <= 0)
return; return;
if (! adsr.isActive()) if (! adsr.isActive())
clearCurrentNote(); clearCurrentNote();
// Apply pending waveform change (from GUI / processor thread) // Apply pending waveform change (from GUI / processor thread)
const int wf = pendingWaveform.exchange (-1, std::memory_order_acq_rel); const int wf = pendingWaveform.exchange (-1, std::memory_order_acq_rel);
if (wf != -1) if (wf != -1)
setWaveform (wf); setWaveform (wf);
// --- Generate oscillator into temp buffer // --- Generate oscillator into temp buffer
tempBuffer.clear(); tempBuffer.clear();
const int numCh = juce::jmin ((int) spec.numChannels, tempBuffer.getNumChannels()); const int numCh = juce::jmin ((int) spec.numChannels, tempBuffer.getNumChannels());
for (int i = 0; i < numSamples; ++i) for (int i = 0; i < numSamples; ++i)
{ {
const float s = osc.process(); const float s = osc.process();
for (int ch = 0; ch < numCh; ++ch) for (int ch = 0; ch < numCh; ++ch)
tempBuffer.getWritePointer (ch)[i] = s; tempBuffer.getWritePointer (ch)[i] = s;
} }
auto block = tempBlock.getSubBlock (0, (size_t) numSamples); auto block = tempBlock.getSubBlock (0, (size_t) numSamples);
// ================================================================ // ================================================================
// Flanger (pre-filter) manual per-sample to set varying delay // Flanger (pre-filter) manual per-sample to set varying delay
// ================================================================ // ================================================================
{ {
auto& flanger = chain.get<flangerIndex>(); auto& flanger = chain.get<flangerIndex>();
const bool enabled = shared.flangerOn && shared.flangerOn->load() > 0.5f; const bool enabled = shared.flangerOn && shared.flangerOn->load() > 0.5f;
if (enabled) if (enabled)
{ {
const float rate = shared.flangerRate ? shared.flangerRate->load() : 0.0f; const float rate = shared.flangerRate ? shared.flangerRate->load() : 0.0f;
float lfoPhase = shared.flangerPhase ? shared.flangerPhase->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 flangerDepth = shared.flangerDepth ? shared.flangerDepth->load() : 0.0f; // ms
const float mix = shared.flangerDryMix ? shared.flangerDryMix->load() : 0.0f; const float mix = shared.flangerDryMix ? shared.flangerDryMix->load() : 0.0f;
const float feedback = shared.flangerFeedback ? shared.flangerFeedback->load() : 0.0f; const float feedback = shared.flangerFeedback ? shared.flangerFeedback->load() : 0.0f;
const float baseDelayMs = shared.flangerDelay ? shared.flangerDelay->load() : 0.25f; const float baseDelayMs = shared.flangerDelay ? shared.flangerDelay->load() : 0.25f;
for (int i = 0; i < numSamples; ++i) for (int i = 0; i < numSamples; ++i)
{ {
const float in = tempBuffer.getReadPointer (0)[i]; const float in = tempBuffer.getReadPointer (0)[i];
const float lfo = std::sin (lfoPhase); const float lfo = std::sin (lfoPhase);
const float delayMs = baseDelayMs + 0.5f * (1.0f + lfo) * flangerDepth; const float delayMs = baseDelayMs + 0.5f * (1.0f + lfo) * flangerDepth;
const float delaySamples = juce::jmax (0.0f, delayMs * 0.001f * (float) spec.sampleRate); const float delaySamples = juce::jmax (0.0f, delayMs * 0.001f * (float) spec.sampleRate);
flanger.setDelay (delaySamples); flanger.setDelay (delaySamples);
const float delayed = flanger.popSample (0); const float delayed = flanger.popSample (0);
flanger.pushSample (0, in + delayed * feedback); flanger.pushSample (0, in + delayed * feedback);
const float out = in * (1.0f - mix) + delayed * mix; const float out = in * (1.0f - mix) + delayed * mix;
for (int ch = 0; ch < numCh; ++ch) for (int ch = 0; ch < numCh; ++ch)
tempBuffer.getWritePointer (ch)[i] = out; tempBuffer.getWritePointer (ch)[i] = out;
lfoPhase += juce::MathConstants<float>::twoPi * rate / (float) spec.sampleRate; lfoPhase += juce::MathConstants<float>::twoPi * rate / (float) spec.sampleRate;
if (lfoPhase > juce::MathConstants<float>::twoPi) if (lfoPhase > juce::MathConstants<float>::twoPi)
lfoPhase -= juce::MathConstants<float>::twoPi; lfoPhase -= juce::MathConstants<float>::twoPi;
} }
} }
} }
// ================================================================ // ================================================================
// Filter with per-sample ADSR modulation (poly) // Filter with per-sample ADSR modulation (poly)
// ================================================================ // ================================================================
{ {
const bool enabled = shared.filterOn && shared.filterOn->load() > 0.5f; const bool enabled = shared.filterOn && shared.filterOn->load() > 0.5f;
// Update filter type every block (cheap) // Update filter type every block (cheap)
const int ftype = (int) std::lround (juce::jlimit (0.0f, 2.0f, const int ftype = (int) std::lround (juce::jlimit (0.0f, 2.0f,
shared.filterType ? shared.filterType->load() : 0.0f)); shared.filterType ? shared.filterType->load() : 0.0f));
switch (ftype) switch (ftype)
{ {
case 0: svf.setType (juce::dsp::StateVariableTPTFilterType::lowpass); break; case 0: svf.setType (juce::dsp::StateVariableTPTFilterType::lowpass); break;
case 1: svf.setType (juce::dsp::StateVariableTPTFilterType::highpass); break; case 1: svf.setType (juce::dsp::StateVariableTPTFilterType::highpass); break;
case 2: svf.setType (juce::dsp::StateVariableTPTFilterType::bandpass); break; case 2: svf.setType (juce::dsp::StateVariableTPTFilterType::bandpass); break;
default: break; default: break;
} }
const float qOrRes = juce::jlimit (0.1f, 10.0f, const float qOrRes = juce::jlimit (0.1f, 10.0f,
shared.filterResonance ? shared.filterResonance->load() : 0.7f); shared.filterResonance ? shared.filterResonance->load() : 0.7f);
svf.setResonance (qOrRes); svf.setResonance (qOrRes);
const float baseCutoff = juce::jlimit (20.0f, 20000.0f, const float baseCutoff = juce::jlimit (20.0f, 20000.0f,
shared.filterCutoff ? shared.filterCutoff->load() : 1000.0f); shared.filterCutoff ? shared.filterCutoff->load() : 1000.0f);
const float envAmt = shared.fenvAmount ? shared.fenvAmount->load() : 0.0f; const float envAmt = shared.fenvAmount ? shared.fenvAmount->load() : 0.0f;
for (int i = 0; i < numSamples; ++i) for (int i = 0; i < numSamples; ++i)
{ {
const float envVal = filterAdsr.getNextSample(); const float envVal = filterAdsr.getNextSample();
const float cutoff = juce::jlimit (20.0f, 20000.0f, const float cutoff = juce::jlimit (20.0f, 20000.0f,
baseCutoff * std::pow (2.0f, envAmt * envVal)); baseCutoff * std::pow (2.0f, envAmt * envVal));
svf.setCutoffFrequency (cutoff); svf.setCutoffFrequency (cutoff);
if (enabled) if (enabled)
{ {
for (int ch = 0; ch < numCh; ++ch) for (int ch = 0; ch < numCh; ++ch)
{ {
float x = tempBuffer.getSample (ch, i); float x = tempBuffer.getSample (ch, i);
x = svf.processSample (ch, x); x = svf.processSample (ch, x);
tempBuffer.setSample (ch, i, x); tempBuffer.setSample (ch, i, x);
} }
} }
} }
} }
// ================================================================ // ================================================================
// Chorus // Chorus
// ================================================================ // ================================================================
if (shared.chorusOn && shared.chorusOn->load() > 0.5f) if (shared.chorusOn && shared.chorusOn->load() > 0.5f)
{ {
auto& chorus = chain.get<chorusIndex>(); auto& chorus = chain.get<chorusIndex>();
if (shared.chorusCentre) chorus.setCentreDelay (shared.chorusCentre->load()); if (shared.chorusCentre) chorus.setCentreDelay (shared.chorusCentre->load());
if (shared.chorusDepth) chorus.setDepth (shared.chorusDepth->load()); if (shared.chorusDepth) chorus.setDepth (shared.chorusDepth->load());
if (shared.chorusFeedback) chorus.setFeedback (shared.chorusFeedback->load()); if (shared.chorusFeedback) chorus.setFeedback (shared.chorusFeedback->load());
if (shared.chorusMix) chorus.setMix (shared.chorusMix->load()); if (shared.chorusMix) chorus.setMix (shared.chorusMix->load());
if (shared.chorusRate) chorus.setRate (shared.chorusRate->load()); if (shared.chorusRate) chorus.setRate (shared.chorusRate->load());
chain.get<chorusIndex>().process (juce::dsp::ProcessContextReplacing<float> (block)); chain.get<chorusIndex>().process (juce::dsp::ProcessContextReplacing<float> (block));
} }
// ================================================================ // ================================================================
// Simple Delay (per-voice) // Simple Delay (per-voice)
// ================================================================ // ================================================================
if (shared.delayOn && shared.delayOn->load() > 0.5f) if (shared.delayOn && shared.delayOn->load() > 0.5f)
{ {
auto& delay = chain.get<delayIndex>(); auto& delay = chain.get<delayIndex>();
const float time = shared.delayTime ? shared.delayTime->load() : 0.1f; const float time = shared.delayTime ? shared.delayTime->load() : 0.1f;
delay.setDelay (juce::jmax (0.0f, time * (float) spec.sampleRate)); delay.setDelay (juce::jmax (0.0f, time * (float) spec.sampleRate));
delay.process (juce::dsp::ProcessContextReplacing<float> (block)); delay.process (juce::dsp::ProcessContextReplacing<float> (block));
} }
// ================================================================ // ================================================================
// Reverb // Reverb
// ================================================================ // ================================================================
if (shared.reverbOn && shared.reverbOn->load() > 0.5f) if (shared.reverbOn && shared.reverbOn->load() > 0.5f)
{ {
juce::Reverb::Parameters rp; juce::Reverb::Parameters rp;
rp.damping = shared.reverbDamping ? shared.reverbDamping->load() : 0.0f; rp.damping = shared.reverbDamping ? shared.reverbDamping->load() : 0.0f;
rp.dryLevel = shared.reverbDryLevel ? shared.reverbDryLevel->load() : 0.0f; rp.dryLevel = shared.reverbDryLevel ? shared.reverbDryLevel->load() : 0.0f;
rp.freezeMode = shared.reverbFreezeMode ? shared.reverbFreezeMode->load() : 0.0f; rp.freezeMode = shared.reverbFreezeMode ? shared.reverbFreezeMode->load() : 0.0f;
rp.roomSize = shared.reverbRoomSize ? shared.reverbRoomSize->load() : 0.0f; rp.roomSize = shared.reverbRoomSize ? shared.reverbRoomSize->load() : 0.0f;
rp.wetLevel = shared.reverbWetLevel ? shared.reverbWetLevel->load() : 0.0f; rp.wetLevel = shared.reverbWetLevel ? shared.reverbWetLevel->load() : 0.0f;
rp.width = shared.reverbWidth ? shared.reverbWidth->load() : 0.0f; rp.width = shared.reverbWidth ? shared.reverbWidth->load() : 0.0f;
chain.get<reverbIndex>().setParameters (rp); chain.get<reverbIndex>().setParameters (rp);
chain.get<reverbIndex>().process (juce::dsp::ProcessContextReplacing<float> (block)); chain.get<reverbIndex>().process (juce::dsp::ProcessContextReplacing<float> (block));
} }
// ================================================================ // ================================================================
// Distortion + tone (post LPF/Peak) // Distortion + tone (post LPF/Peak)
// ================================================================ // ================================================================
{ {
const float driveDb = shared.distortionDrive ? shared.distortionDrive->load() : 0.0f; 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 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 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, const int shape = (int) std::lround (juce::jlimit (0.0f, 2.0f,
shared.distortionShape ? shared.distortionShape->load() : 0.0f)); shared.distortionShape ? shared.distortionShape->load() : 0.0f));
const float mix = shared.distortionMix ? shared.distortionMix->load() : 0.0f; const float mix = shared.distortionMix ? shared.distortionMix->load() : 0.0f;
auto& pre = chain.get<distortionPreGain>(); auto& pre = chain.get<distortionPreGain>();
auto& sh = chain.get<distortionIndex>(); auto& sh = chain.get<distortionIndex>();
auto& tone = chain.get<distortionPostLPF>(); auto& tone = chain.get<distortionPostLPF>();
pre.setGainDecibels (driveDb); pre.setGainDecibels (driveDb);
// Explicit std::function target (works on MSVC) // Explicit std::function target (works on MSVC)
if (shape == 0) sh.functionToUse = std::function<float(float)>{ [bias](float x) noexcept { return std::tanh (x + bias); } }; if (shape == 0) sh.functionToUse = std::function<float(float)>{ [bias](float x) noexcept { return std::tanh (x + bias); } };
else if (shape == 1) sh.functionToUse = std::function<float(float)>{ [bias](float x) noexcept { return juce::jlimit (-1.0f, 1.0f, x + bias); } }; else if (shape == 1) sh.functionToUse = std::function<float(float)>{ [bias](float x) noexcept { return juce::jlimit (-1.0f, 1.0f, x + bias); } };
else sh.functionToUse = std::function<float(float)>{ [bias](float x) noexcept { return std::atan (x + bias) * (2.0f / juce::MathConstants<float>::pi); } }; else sh.functionToUse = std::function<float(float)>{ [bias](float x) noexcept { return std::atan (x + bias) * (2.0f / juce::MathConstants<float>::pi); } };
tone.coefficients = juce::dsp::IIR::Coefficients<float>::makePeakFilter ( tone.coefficients = juce::dsp::IIR::Coefficients<float>::makePeakFilter (
spec.sampleRate, toneHz, 0.707f, spec.sampleRate, toneHz, 0.707f,
juce::Decibels::decibelsToGain (shared.highGainDbls ? shared.highGainDbls->load() : 0.0f)); juce::Decibels::decibelsToGain (shared.highGainDbls ? shared.highGainDbls->load() : 0.0f));
if (shared.distortionOn && shared.distortionOn->load() > 0.5f) if (shared.distortionOn && shared.distortionOn->load() > 0.5f)
{ {
// Wet/dry blend around the shaper // Wet/dry blend around the shaper
juce::AudioBuffer<float> dryCopy (tempBuffer.getNumChannels(), numSamples); juce::AudioBuffer<float> dryCopy (tempBuffer.getNumChannels(), numSamples);
for (int ch = 0; ch < numCh; ++ch) for (int ch = 0; ch < numCh; ++ch)
dryCopy.copyFrom (ch, 0, tempBuffer, ch, 0, numSamples); dryCopy.copyFrom (ch, 0, tempBuffer, ch, 0, numSamples);
// pre -> shaper -> tone // pre -> shaper -> tone
pre.process (juce::dsp::ProcessContextReplacing<float> (block)); pre.process (juce::dsp::ProcessContextReplacing<float> (block));
sh.process (juce::dsp::ProcessContextReplacing<float> (block)); sh.process (juce::dsp::ProcessContextReplacing<float> (block));
tone.process (juce::dsp::ProcessContextReplacing<float> (block)); tone.process (juce::dsp::ProcessContextReplacing<float> (block));
const float wet = mix, dry = 1.0f - mix; const float wet = mix, dry = 1.0f - mix;
for (int ch = 0; ch < numCh; ++ch) for (int ch = 0; ch < numCh; ++ch)
{ {
auto* d = dryCopy.getReadPointer (ch); auto* d = dryCopy.getReadPointer (ch);
auto* w = tempBuffer.getWritePointer (ch); auto* w = tempBuffer.getWritePointer (ch);
for (int i = 0; i < numSamples; ++i) for (int i = 0; i < numSamples; ++i)
w[i] = dry * d[i] + wet * w[i]; w[i] = dry * d[i] + wet * w[i];
} }
} }
} }
// ================================================================ // ================================================================
// EQ + Master + Limiter (EQ guarded by eqOn) // EQ + Master + Limiter (EQ guarded by eqOn)
// ================================================================ // ================================================================
{ {
const bool eqEnabled = shared.eqOn && shared.eqOn->load() > 0.5f; const bool eqEnabled = shared.eqOn && shared.eqOn->load() > 0.5f;
auto& eqL = chain.get<eqLowIndex>(); auto& eqL = chain.get<eqLowIndex>();
auto& eqM = chain.get<eqMidIndex>(); auto& eqM = chain.get<eqMidIndex>();
auto& eqH = chain.get<eqHighIndex>(); auto& eqH = chain.get<eqHighIndex>();
if (eqEnabled) if (eqEnabled)
{ {
eqL.coefficients = juce::dsp::IIR::Coefficients<float>::makeLowShelf ( eqL.coefficients = juce::dsp::IIR::Coefficients<float>::makeLowShelf (
spec.sampleRate, 100.0f, 0.707f, spec.sampleRate, 100.0f, 0.707f,
juce::Decibels::decibelsToGain (shared.lowGainDbls ? shared.lowGainDbls->load() : 0.0f)); juce::Decibels::decibelsToGain (shared.lowGainDbls ? shared.lowGainDbls->load() : 0.0f));
eqM.coefficients = juce::dsp::IIR::Coefficients<float>::makePeakFilter ( eqM.coefficients = juce::dsp::IIR::Coefficients<float>::makePeakFilter (
spec.sampleRate, 1000.0f, 1.0f, spec.sampleRate, 1000.0f, 1.0f,
juce::Decibels::decibelsToGain (shared.midGainDbls ? shared.midGainDbls->load() : 0.0f)); juce::Decibels::decibelsToGain (shared.midGainDbls ? shared.midGainDbls->load() : 0.0f));
eqH.coefficients = juce::dsp::IIR::Coefficients<float>::makePeakFilter ( eqH.coefficients = juce::dsp::IIR::Coefficients<float>::makePeakFilter (
spec.sampleRate, 10000.0f, 0.707f, spec.sampleRate, 10000.0f, 0.707f,
juce::Decibels::decibelsToGain (shared.highGainDbls ? shared.highGainDbls->load() : 0.0f)); juce::Decibels::decibelsToGain (shared.highGainDbls ? shared.highGainDbls->load() : 0.0f));
eqL.process (juce::dsp::ProcessContextReplacing<float> (block)); eqL.process (juce::dsp::ProcessContextReplacing<float> (block));
eqM.process (juce::dsp::ProcessContextReplacing<float> (block)); eqM.process (juce::dsp::ProcessContextReplacing<float> (block));
eqH.process (juce::dsp::ProcessContextReplacing<float> (block)); eqH.process (juce::dsp::ProcessContextReplacing<float> (block));
} }
chain.get<masterIndex>().setGainDecibels (shared.masterDbls ? shared.masterDbls->load() : 0.0f); chain.get<masterIndex>().setGainDecibels (shared.masterDbls ? shared.masterDbls->load() : 0.0f);
chain.get<masterIndex>().process (juce::dsp::ProcessContextReplacing<float> (block)); chain.get<masterIndex>().process (juce::dsp::ProcessContextReplacing<float> (block));
chain.get<limiterIndex>().process (juce::dsp::ProcessContextReplacing<float> (block)); chain.get<limiterIndex>().process (juce::dsp::ProcessContextReplacing<float> (block));
} }
// ================================================================ // ================================================================
// Apply AMP ADSR envelope // Apply AMP ADSR envelope
// ================================================================ // ================================================================
{ {
juce::AudioBuffer<float> buf (tempBuffer.getArrayOfWritePointers(), numCh, numSamples); juce::AudioBuffer<float> buf (tempBuffer.getArrayOfWritePointers(), numCh, numSamples);
adsr.applyEnvelopeToBuffer (buf, 0, numSamples); adsr.applyEnvelopeToBuffer (buf, 0, numSamples);
} }
// Mix into output // Mix into output
juce::dsp::AudioBlock<float> (outputBuffer) juce::dsp::AudioBlock<float> (outputBuffer)
.getSubBlock ((size_t) startSample, (size_t) numSamples) .getSubBlock ((size_t) startSample, (size_t) numSamples)
.add (block); .add (block);
} }
//============================================================================== //==============================================================================
void NeuralSynthVoice::noteStarted() void NeuralSynthVoice::noteStarted()
{ {
const float freqHz = (float) getCurrentlyPlayingNote().getFrequencyInHertz(); const float freqHz = (float) getCurrentlyPlayingNote().getFrequencyInHertz();
// Oscillator frequency and phase retrigger // Oscillator frequency and phase retrigger
osc.setFrequency (freqHz); osc.setFrequency (freqHz);
osc.resetPhase (0.0f); osc.resetPhase (0.0f);
// Chorus snapshot // Chorus snapshot
if (shared.chorusCentre) chain.get<chorusIndex>().setCentreDelay (shared.chorusCentre->load()); if (shared.chorusCentre) chain.get<chorusIndex>().setCentreDelay (shared.chorusCentre->load());
if (shared.chorusDepth) chain.get<chorusIndex>().setDepth (shared.chorusDepth->load()); if (shared.chorusDepth) chain.get<chorusIndex>().setDepth (shared.chorusDepth->load());
if (shared.chorusFeedback) chain.get<chorusIndex>().setFeedback (shared.chorusFeedback->load()); if (shared.chorusFeedback) chain.get<chorusIndex>().setFeedback (shared.chorusFeedback->load());
if (shared.chorusMix) chain.get<chorusIndex>().setMix (shared.chorusMix->load()); if (shared.chorusMix) chain.get<chorusIndex>().setMix (shared.chorusMix->load());
if (shared.chorusRate) chain.get<chorusIndex>().setRate (shared.chorusRate->load()); if (shared.chorusRate) chain.get<chorusIndex>().setRate (shared.chorusRate->load());
// Delay time (in samples) // Delay time (in samples)
if (shared.delayTime) if (shared.delayTime)
chain.get<delayIndex>().setDelay (juce::jmax (0.0f, shared.delayTime->load() * (float) spec.sampleRate)); chain.get<delayIndex>().setDelay (juce::jmax (0.0f, shared.delayTime->load() * (float) spec.sampleRate));
// Reverb snapshot // Reverb snapshot
juce::Reverb::Parameters rp; juce::Reverb::Parameters rp;
rp.damping = shared.reverbDamping ? shared.reverbDamping->load() : 0.0f; rp.damping = shared.reverbDamping ? shared.reverbDamping->load() : 0.0f;
rp.dryLevel = shared.reverbDryLevel ? shared.reverbDryLevel->load() : 0.0f; rp.dryLevel = shared.reverbDryLevel ? shared.reverbDryLevel->load() : 0.0f;
rp.freezeMode = shared.reverbFreezeMode ? shared.reverbFreezeMode->load() : 0.0f; rp.freezeMode = shared.reverbFreezeMode ? shared.reverbFreezeMode->load() : 0.0f;
rp.roomSize = shared.reverbRoomSize ? shared.reverbRoomSize->load() : 0.0f; rp.roomSize = shared.reverbRoomSize ? shared.reverbRoomSize->load() : 0.0f;
rp.wetLevel = shared.reverbWetLevel ? shared.reverbWetLevel->load() : 0.0f; rp.wetLevel = shared.reverbWetLevel ? shared.reverbWetLevel->load() : 0.0f;
rp.width = shared.reverbWidth ? shared.reverbWidth->load() : 0.0f; rp.width = shared.reverbWidth ? shared.reverbWidth->load() : 0.0f;
chain.get<reverbIndex>().setParameters (rp); chain.get<reverbIndex>().setParameters (rp);
// Amp ADSR // Amp ADSR
juce::ADSR::Parameters ap; juce::ADSR::Parameters ap;
ap.attack = shared.adsrAttack ? shared.adsrAttack->load() : 0.01f; ap.attack = shared.adsrAttack ? shared.adsrAttack->load() : 0.01f;
ap.decay = shared.adsrDecay ? shared.adsrDecay->load() : 0.10f; ap.decay = shared.adsrDecay ? shared.adsrDecay->load() : 0.10f;
ap.sustain = shared.adsrSustain ? shared.adsrSustain->load() : 0.80f; ap.sustain = shared.adsrSustain ? shared.adsrSustain->load() : 0.80f;
ap.release = shared.adsrRelease ? shared.adsrRelease->load() : 0.40f; ap.release = shared.adsrRelease ? shared.adsrRelease->load() : 0.40f;
adsr.setParameters (ap); adsr.setParameters (ap);
adsr.noteOn(); adsr.noteOn();
// Filter ADSR // Filter ADSR
juce::ADSR::Parameters fp; juce::ADSR::Parameters fp;
fp.attack = shared.fenvAttack ? shared.fenvAttack->load() : 0.01f; fp.attack = shared.fenvAttack ? shared.fenvAttack->load() : 0.01f;
fp.decay = shared.fenvDecay ? shared.fenvDecay->load() : 0.10f; fp.decay = shared.fenvDecay ? shared.fenvDecay->load() : 0.10f;
fp.sustain = shared.fenvSustain ? shared.fenvSustain->load() : 0.80f; fp.sustain = shared.fenvSustain ? shared.fenvSustain->load() : 0.80f;
fp.release = shared.fenvRelease ? shared.fenvRelease->load() : 0.40f; fp.release = shared.fenvRelease ? shared.fenvRelease->load() : 0.40f;
filterAdsr.setParameters (fp); filterAdsr.setParameters (fp);
filterAdsr.noteOn(); filterAdsr.noteOn();
} }
//============================================================================== //==============================================================================
void NeuralSynthVoice::notePitchbendChanged() void NeuralSynthVoice::notePitchbendChanged()
{ {
const float freqHz = (float) getCurrentlyPlayingNote().getFrequencyInHertz(); const float freqHz = (float) getCurrentlyPlayingNote().getFrequencyInHertz();
osc.setFrequency (freqHz); osc.setFrequency (freqHz);
} }
//============================================================================== //==============================================================================
void NeuralSynthVoice::noteStopped (bool allowTailOff) void NeuralSynthVoice::noteStopped (bool allowTailOff)
{ {
juce::ignoreUnused (allowTailOff); juce::ignoreUnused (allowTailOff);
adsr.noteOff(); adsr.noteOff();
filterAdsr.noteOff(); filterAdsr.noteOff();
} }
//============================================================================== //==============================================================================
void NeuralSynthVoice::setWaveform (int waveformType) void NeuralSynthVoice::setWaveform (int waveformType)
{ {
switch (juce::jlimit (0, 3, waveformType)) switch (juce::jlimit (0, 3, waveformType))
{ {
case 0: osc.setWave (BlepWave::Sine); break; case 0: osc.setWave (BlepWave::Sine); break;
case 1: osc.setWave (BlepWave::Saw); break; case 1: osc.setWave (BlepWave::Saw); break;
case 2: osc.setWave (BlepWave::Square); break; case 2: osc.setWave (BlepWave::Square); break;
case 3: osc.setWave (BlepWave::Triangle); break; case 3: osc.setWave (BlepWave::Triangle); break;
default: osc.setWave (BlepWave::Sine); break; default: osc.setWave (BlepWave::Sine); break;
} }
} }

View File

@@ -1,97 +1,97 @@
#pragma once #pragma once
#include <JuceHeader.h> #include <JuceHeader.h>
#include <functional> // <-- for std::function used by WaveShaper #include <functional> // <-- for std::function used by WaveShaper
#include "NeuralSharedParams.h" #include "NeuralSharedParams.h"
#include "BlepOsc.h" #include "BlepOsc.h"
//============================================================================== //==============================================================================
// A single polyBLEP oscillator voice with per-voice ADSR, filter ADSR, // A single polyBLEP oscillator voice with per-voice ADSR, filter ADSR,
// flanger (delayline), simple delay, chorus, reverb, distortion, EQ, master. // flanger (delayline), simple delay, chorus, reverb, distortion, EQ, master.
class NeuralSynthVoice : public juce::MPESynthesiserVoice class NeuralSynthVoice : public juce::MPESynthesiserVoice
{ {
public: public:
explicit NeuralSynthVoice (NeuralSharedParams& sharedParams); explicit NeuralSynthVoice (NeuralSharedParams& sharedParams);
// JUCE voice API // JUCE voice API
void prepare (const juce::dsp::ProcessSpec& spec); void prepare (const juce::dsp::ProcessSpec& spec);
void renderNextBlock (juce::AudioBuffer<float>& outputBuffer, void renderNextBlock (juce::AudioBuffer<float>& outputBuffer,
int startSample, int numSamples) override; int startSample, int numSamples) override;
void noteStarted() override; void noteStarted() override;
void noteStopped (bool allowTailOff) override; void noteStopped (bool allowTailOff) override;
void notePitchbendChanged() override; void notePitchbendChanged() override;
void notePressureChanged() override {} void notePressureChanged() override {}
void noteTimbreChanged() override {} void noteTimbreChanged() override {}
void noteKeyStateChanged() override {} void noteKeyStateChanged() override {}
// Called from the processor when the GUI waveform param changes // Called from the processor when the GUI waveform param changes
void changeWaveform (int wf) { setWaveform (wf); } void changeWaveform (int wf) { setWaveform (wf); }
private: private:
void setWaveform (int waveformType); void setWaveform (int waveformType);
//=== Processing chain (without oscillator) =============================== //=== Processing chain (without oscillator) ===============================
using DelayLine = juce::dsp::DelayLine<float, using DelayLine = juce::dsp::DelayLine<float,
juce::dsp::DelayLineInterpolationTypes::Linear>; juce::dsp::DelayLineInterpolationTypes::Linear>;
using IIR = juce::dsp::IIR::Filter<float>; using IIR = juce::dsp::IIR::Filter<float>;
using Gain = juce::dsp::Gain<float>; using Gain = juce::dsp::Gain<float>;
using WaveShaper = juce::dsp::WaveShaper<float, std::function<float(float)>>; // <-- fix using WaveShaper = juce::dsp::WaveShaper<float, std::function<float(float)>>; // <-- fix
using Chorus = juce::dsp::Chorus<float>; using Chorus = juce::dsp::Chorus<float>;
using Reverb = juce::dsp::Reverb; using Reverb = juce::dsp::Reverb;
using Limiter = juce::dsp::Limiter<float>; using Limiter = juce::dsp::Limiter<float>;
enum ChainIndex enum ChainIndex
{ {
flangerIndex = 0, flangerIndex = 0,
delayIndex, delayIndex,
chorusIndex, chorusIndex,
reverbIndex, reverbIndex,
distortionPreGain, distortionPreGain,
distortionIndex, distortionIndex,
distortionPostLPF, distortionPostLPF,
eqLowIndex, eqLowIndex,
eqMidIndex, eqMidIndex,
eqHighIndex, eqHighIndex,
masterIndex, masterIndex,
limiterIndex limiterIndex
}; };
using Chain = juce::dsp::ProcessorChain< using Chain = juce::dsp::ProcessorChain<
DelayLine, // flanger DelayLine, // flanger
DelayLine, // simple delay DelayLine, // simple delay
Chorus, // chorus Chorus, // chorus
Reverb, // reverb Reverb, // reverb
Gain, // distortion pre-gain (drive) Gain, // distortion pre-gain (drive)
WaveShaper, // distortion waveshaper WaveShaper, // distortion waveshaper
IIR, // tone / post-EQ for distortion IIR, // tone / post-EQ for distortion
IIR, // EQ low IIR, // EQ low
IIR, // EQ mid IIR, // EQ mid
IIR, // EQ high IIR, // EQ high
Gain, // master gain Gain, // master gain
Limiter // safety limiter Limiter // safety limiter
>; >;
private: private:
NeuralSharedParams& shared; NeuralSharedParams& shared;
juce::dsp::ProcessSpec spec {}; juce::dsp::ProcessSpec spec {};
// ==== Oscillator (polyBLEP) ============================================ // ==== Oscillator (polyBLEP) ============================================
BlepOsc osc; BlepOsc osc;
std::atomic<int> pendingWaveform {-1}; // set by changeWaveform() std::atomic<int> pendingWaveform {-1}; // set by changeWaveform()
// ==== Envelopes & Filter =============================================== // ==== Envelopes & Filter ===============================================
juce::ADSR adsr; juce::ADSR adsr;
juce::ADSR filterAdsr; juce::ADSR filterAdsr;
juce::dsp::StateVariableTPTFilter<float> svf; juce::dsp::StateVariableTPTFilter<float> svf;
// ==== Chain (FX, EQ, master, limiter) ================================== // ==== Chain (FX, EQ, master, limiter) ==================================
Chain chain; Chain chain;
// ==== Scratch buffer (properly allocated) =============================== // ==== Scratch buffer (properly allocated) ===============================
juce::AudioBuffer<float> tempBuffer; juce::AudioBuffer<float> tempBuffer;
juce::dsp::AudioBlock<float> tempBlock; juce::dsp::AudioBlock<float> tempBlock;
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (NeuralSynthVoice) JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (NeuralSynthVoice)
}; };

View File

@@ -1,261 +1,261 @@
#pragma once #pragma once
#include <JuceHeader.h> #include <JuceHeader.h>
#include <vector> #include <vector>
#include <cmath> #include <cmath>
// ============================== Design ======================================= // ============================== Design =======================================
// - Bank with F frames, each frame is a single-cycle table of N samples. // - 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, // - For each frame, we create L mip-levels: level 0 = full bandwidth,
// level l halves the permitted harmonics (spectral truncation). // level l halves the permitted harmonics (spectral truncation).
// - Runtime chooses level from note frequency and sampleRate, then morphs // - Runtime chooses level from note frequency and sampleRate, then morphs
// between adjacent frames and crossfades between the two nearest levels. // between adjacent frames and crossfades between the two nearest levels.
// - Table read uses linear interpolation (cheap and good enough with N>=2048). // - Table read uses linear interpolation (cheap and good enough with N>=2048).
namespace WT namespace WT
{ {
// Utility: complex array wrapper for JUCE FFT (interleaved real/imag floats) // Utility: complex array wrapper for JUCE FFT (interleaved real/imag floats)
struct ComplexBuf struct ComplexBuf
{ {
std::vector<float> data; // size = 2 * N std::vector<float> data; // size = 2 * N
explicit ComplexBuf(size_t N = 0) { resize(N); } explicit ComplexBuf(size_t N = 0) { resize(N); }
void resize(size_t N) { data.assign(2 * N, 0.0f); } void resize(size_t N) { data.assign(2 * N, 0.0f); }
juce::dsp::Complex<float>* asComplex() { return reinterpret_cast<juce::dsp::Complex<float>*>(data.data()); } juce::dsp::Complex<float>* asComplex() { return reinterpret_cast<juce::dsp::Complex<float>*>(data.data()); }
}; };
// ======================================================================= // =======================================================================
// WavetableBank: holds raw frames + mipmapped versions // WavetableBank: holds raw frames + mipmapped versions
// ======================================================================= // =======================================================================
class Bank class Bank
{ {
public: public:
// N = table length (must be power-of-two for FFT), frames = number of morph frames // 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. // mipLevels = how many spectral levels (>=1). 5 ~ 6 is plenty for synth use.
Bank(size_t N = 2048, int frames = 16, int mipLevels = 6) Bank(size_t N = 2048, int frames = 16, int mipLevels = 6)
: tableSize(N), numFrames(frames), numLevels(mipLevels), : tableSize(N), numFrames(frames), numLevels(mipLevels),
fft((int)std::log2((double)N)) fft((int)std::log2((double)N))
{ {
jassert(juce::isPowerOfTwo((int)N)); jassert(juce::isPowerOfTwo((int)N));
tables.resize((size_t)numLevels); tables.resize((size_t)numLevels);
for (int l = 0; l < numLevels; ++l) for (int l = 0; l < numLevels; ++l)
tables[(size_t)l].resize((size_t)numFrames, std::vector<float>(tableSize, 0.0f)); tables[(size_t)l].resize((size_t)numFrames, std::vector<float>(tableSize, 0.0f));
} }
size_t getSize() const { return tableSize; } size_t getSize() const { return tableSize; }
int getFrames() const { return numFrames; } int getFrames() const { return numFrames; }
int getLevels() const { return numLevels; } int getLevels() const { return numLevels; }
// Provide raw “design” frames (time-domain single-cycle) then call buildMipmaps(). // Provide raw “design” frames (time-domain single-cycle) then call buildMipmaps().
// framesRaw.size() must equal numFrames, each frame length must equal tableSize. // framesRaw.size() must equal numFrames, each frame length must equal tableSize.
void setRawFrames(const std::vector<std::vector<float>>& framesRaw) void setRawFrames(const std::vector<std::vector<float>>& framesRaw)
{ {
jassert((int)framesRaw.size() == numFrames); jassert((int)framesRaw.size() == numFrames);
for (const auto& f : framesRaw) jassert(f.size() == tableSize); for (const auto& f : framesRaw) jassert(f.size() == tableSize);
raw = framesRaw; raw = framesRaw;
} }
// Convenience: generate 16-frame bank morphing Sine -> Saw -> Square -> Triangle // Convenience: generate 16-frame bank morphing Sine -> Saw -> Square -> Triangle
void generateDefaultMorph() void generateDefaultMorph()
{ {
std::vector<std::vector<float>> frames; std::vector<std::vector<float>> frames;
frames.resize((size_t)numFrames, std::vector<float>(tableSize, 0.0f)); frames.resize((size_t)numFrames, std::vector<float>(tableSize, 0.0f));
auto fill = [&](int idx, auto func) auto fill = [&](int idx, auto func)
{ {
auto& t = frames[(size_t)idx]; auto& t = frames[(size_t)idx];
for (size_t n = 0; n < tableSize; ++n) for (size_t n = 0; n < tableSize; ++n)
{ {
const float ph = (float) (juce::MathConstants<double>::twoPi * (double)n / (double)tableSize); const float ph = (float) (juce::MathConstants<double>::twoPi * (double)n / (double)tableSize);
t[n] = func(ph); t[n] = func(ph);
} }
normalise(t); normalise(t);
}; };
// helper waves // helper waves
auto sine = [](float ph) { return std::sin(ph); }; auto sine = [](float ph) { return std::sin(ph); };
auto saw = [](float ph) { return (float)(2.0 * (ph / juce::MathConstants<float>::twoPi) - 1.0); }; auto saw = [](float ph) { return (float)(2.0 * (ph / juce::MathConstants<float>::twoPi) - 1.0); };
auto sq = [](float ph) { return ph < juce::MathConstants<float>::pi ? 1.0f : -1.0f; }; auto sq = [](float ph) { return ph < juce::MathConstants<float>::pi ? 1.0f : -1.0f; };
auto tri = [](float ph) { auto tri = [](float ph) {
float v = (float)(2.0 * std::abs(2.0 * (ph / juce::MathConstants<float>::twoPi) - 1.0) - 1.0); float v = (float)(2.0 * std::abs(2.0 * (ph / juce::MathConstants<float>::twoPi) - 1.0) - 1.0);
return v; return v;
}; };
// 0..5: sine->saw, 6..10: saw->square, 11..15: square->triangle // 0..5: sine->saw, 6..10: saw->square, 11..15: square->triangle
const int F = numFrames; const int F = numFrames;
for (int i = 0; i < F; ++i) for (int i = 0; i < F; ++i)
{ {
const float t = (float) i / (float) juce::jmax(1, F - 1); const float t = (float) i / (float) juce::jmax(1, F - 1);
std::function<float(float)> a, b; std::function<float(float)> a, b;
float mix = 0.0f; float mix = 0.0f;
if (i <= 5) { a = sine; b = saw; mix = (float)i / 5.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 if (i <=10) { a = saw; b = sq; mix = (float)(i - 6) / 4.0f; }
else { a = sq; b = tri; mix = (float)(i - 11) / 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); }); fill(i, [=](float ph){ return (1.0f - mix) * a(ph) + mix * b(ph); });
} }
setRawFrames(frames); setRawFrames(frames);
} }
// Build mip-levels by FFT → spectral truncation → IFFT // Build mip-levels by FFT → spectral truncation → IFFT
void buildMipmaps() void buildMipmaps()
{ {
jassert(!raw.empty()); jassert(!raw.empty());
ComplexBuf freq(tableSize); ComplexBuf freq(tableSize);
ComplexBuf time(tableSize); ComplexBuf time(tableSize);
for (int f = 0; f < numFrames; ++f) for (int f = 0; f < numFrames; ++f)
{ {
// Forward FFT of raw frame // Forward FFT of raw frame
std::fill(freq.data.begin(), freq.data.end(), 0.0f); std::fill(freq.data.begin(), freq.data.end(), 0.0f);
for (size_t n = 0; n < tableSize; ++n) for (size_t n = 0; n < tableSize; ++n)
{ {
time.data[2 * n + 0] = raw[(size_t)f][n]; time.data[2 * n + 0] = raw[(size_t)f][n];
time.data[2 * n + 1] = 0.0f; time.data[2 * n + 1] = 0.0f;
} }
fft.performRealOnlyForwardTransform(time.data.data()); 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], ... // 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. // We'll reconstruct complex bins for easy masking.
// Helper to zero all harmonics above kMax (inclusive index in [0..N/2]) // Helper to zero all harmonics above kMax (inclusive index in [0..N/2])
auto maskAndIFFT = [&](int level, int kMax) auto maskAndIFFT = [&](int level, int kMax)
{ {
// Copy time.data into working complex bins // Copy time.data into working complex bins
auto* bins = freq.asComplex(); auto* bins = freq.asComplex();
// DC & Nyquist are purely real in real-FFT // DC & Nyquist are purely real in real-FFT
bins[0].real (time.data[0]); bins[0].real (time.data[0]);
bins[0].imag (0.0f); bins[0].imag (0.0f);
bins[tableSize/2].real (time.data[1]); bins[tableSize/2].real (time.data[1]);
bins[tableSize/2].imag (0.0f); bins[tableSize/2].imag (0.0f);
// Rebuild the rest (Re[k], Im[k]) packed starting at index 2 // Rebuild the rest (Re[k], Im[k]) packed starting at index 2
for (size_t k = 1; k < tableSize/2; ++k) for (size_t k = 1; k < tableSize/2; ++k)
{ {
bins[k].real (time.data[2 * k + 0]); bins[k].real (time.data[2 * k + 0]);
bins[k].imag (time.data[2 * k + 1]); bins[k].imag (time.data[2 * k + 1]);
} }
// Mask // Mask
for (size_t k = (size_t)kMax + 1; k < tableSize/2; ++k) for (size_t k = (size_t)kMax + 1; k < tableSize/2; ++k)
bins[k] = { 0.0f, 0.0f }; bins[k] = { 0.0f, 0.0f };
// Pack back into real-FFT layout for inverse // Pack back into real-FFT layout for inverse
time.data[0] = bins[0].real(); // DC time.data[0] = bins[0].real(); // DC
time.data[1] = bins[tableSize/2].real(); // Nyquist time.data[1] = bins[tableSize/2].real(); // Nyquist
for (size_t k = 1; k < tableSize/2; ++k) for (size_t k = 1; k < tableSize/2; ++k)
{ {
time.data[2 * k + 0] = bins[k].real(); time.data[2 * k + 0] = bins[k].real();
time.data[2 * k + 1] = bins[k].imag(); time.data[2 * k + 1] = bins[k].imag();
} }
// IFFT // IFFT
fft.performRealOnlyInverseTransform(time.data.data()); fft.performRealOnlyInverseTransform(time.data.data());
// Copy, normalise a little (scale JUCE inverse divides by N already) // Copy, normalise a little (scale JUCE inverse divides by N already)
auto& dst = tables[(size_t)level][(size_t)f]; auto& dst = tables[(size_t)level][(size_t)f];
for (size_t n = 0; n < tableSize; ++n) for (size_t n = 0; n < tableSize; ++n)
dst[n] = time.data[2 * n + 0]; dst[n] = time.data[2 * n + 0];
normalise(dst); normalise(dst);
}; };
// Level 0 → all harmonics available up to N/2 - 1 // Level 0 → all harmonics available up to N/2 - 1
for (int l = 0; l < numLevels; ++l) for (int l = 0; l < numLevels; ++l)
{ {
const int maxH = (int)((tableSize / 2) >> l); // halve per level const int maxH = (int)((tableSize / 2) >> l); // halve per level
const int kMax = juce::jmax(1, juce::jmin(maxH, (int)tableSize/2 - 1)); const int kMax = juce::jmax(1, juce::jmin(maxH, (int)tableSize/2 - 1));
maskAndIFFT(l, kMax); maskAndIFFT(l, kMax);
} }
} }
} }
// sample at (frame, level, phase in [0,1)) // sample at (frame, level, phase in [0,1))
inline float lookup (float frameIdx, int level, float phase) const noexcept 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 f0 = juce::jlimit(0, numFrames - 1, (int)std::floor(frameIdx));
const int f1 = juce::jlimit(0, numFrames - 1, f0 + 1); const int f1 = juce::jlimit(0, numFrames - 1, f0 + 1);
const float t = juce::jlimit(0.0f, 1.0f, frameIdx - (float)f0); const float t = juce::jlimit(0.0f, 1.0f, frameIdx - (float)f0);
const auto& T0 = tables[(size_t)level][(size_t)f0]; const auto& T0 = tables[(size_t)level][(size_t)f0];
const auto& T1 = tables[(size_t)level][(size_t)f1]; const auto& T1 = tables[(size_t)level][(size_t)f1];
const float pos = phase * (float)tableSize; const float pos = phase * (float)tableSize;
const int i0 = (int) std::floor(pos) & (int)(tableSize - 1); const int i0 = (int) std::floor(pos) & (int)(tableSize - 1);
const int i1 = (i0 + 1) & (int)(tableSize - 1); const int i1 = (i0 + 1) & (int)(tableSize - 1);
const float a = pos - (float) std::floor(pos); const float a = pos - (float) std::floor(pos);
const float s0 = juce::jmap(a, T0[(size_t)i0], T0[(size_t)i1]); 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]); const float s1 = juce::jmap(a, T1[(size_t)i0], T1[(size_t)i1]);
return juce::jmap(t, s0, s1); return juce::jmap(t, s0, s1);
} }
// choose mip-level for given frequency (Hz) & sampleRate // choose mip-level for given frequency (Hz) & sampleRate
inline int chooseLevel (float freq, double sampleRate) const noexcept inline int chooseLevel (float freq, double sampleRate) const noexcept
{ {
// permitted harmonics at this pitch: // permitted harmonics at this pitch:
const float maxH = (float) (0.5 * sampleRate / juce::jmax(1.0f, freq)); 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)) // 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 base = (float)(tableSize * 0.5);
const float ratio = base / juce::jmax(1.0f, maxH); const float ratio = base / juce::jmax(1.0f, maxH);
int l = (int) std::ceil (std::log2 (ratio)); int l = (int) std::ceil (std::log2 (ratio));
return juce::jlimit (0, numLevels - 1, l); return juce::jlimit (0, numLevels - 1, l);
} }
static void normalise (std::vector<float>& t) static void normalise (std::vector<float>& t)
{ {
float mx = 0.0f; float mx = 0.0f;
for (float v : t) mx = juce::jmax(mx, std::abs(v)); for (float v : t) mx = juce::jmax(mx, std::abs(v));
if (mx < 1.0e-6f) return; if (mx < 1.0e-6f) return;
for (float& v : t) v /= mx; for (float& v : t) v /= mx;
} }
private: private:
size_t tableSize; size_t tableSize;
int numFrames; int numFrames;
int numLevels; int numLevels;
juce::dsp::FFT fft; juce::dsp::FFT fft;
std::vector<std::vector<float>> raw; std::vector<std::vector<float>> raw;
// [level][frame][sample] // [level][frame][sample]
std::vector<std::vector<std::vector<float>>> tables; std::vector<std::vector<std::vector<float>>> tables;
}; };
// ======================================================================= // =======================================================================
// Wavetable Oscillator // Wavetable Oscillator
// ======================================================================= // =======================================================================
class Osc class Osc
{ {
public: public:
void prepare (double sr) { sampleRate = sr; } void prepare (double sr) { sampleRate = sr; }
void setBank (std::shared_ptr<Bank> b) { bank = std::move(b); } void setBank (std::shared_ptr<Bank> b) { bank = std::move(b); }
void setFrequency (float f) { freq = juce::jmax(0.0f, f); phaseInc = freq / (float)sampleRate; } 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 setMorph (float m) { morph = m; } // 0..frames-1 (continuous)
void resetPhase (float p = 0.0f) { phase = juce::jlimit(0.0f, 1.0f, p); } void resetPhase (float p = 0.0f) { phase = juce::jlimit(0.0f, 1.0f, p); }
float process() float process()
{ {
if (!bank) return 0.0f; if (!bank) return 0.0f;
const int l0 = bank->chooseLevel(freq, sampleRate); const int l0 = bank->chooseLevel(freq, sampleRate);
const int l1 = juce::jmin(l0 + 1, bank->getLevels() - 1); const int l1 = juce::jmin(l0 + 1, bank->getLevels() - 1);
const float preferL0 = 1.0f - juce::jlimit(0.0f, 1.0f, const float preferL0 = 1.0f - juce::jlimit(0.0f, 1.0f,
(float)l0 - (float)bank->chooseLevel(freq * 0.99f, sampleRate)); (float)l0 - (float)bank->chooseLevel(freq * 0.99f, sampleRate));
const float s0 = bank->lookup(morph, l0, phase); const float s0 = bank->lookup(morph, l0, phase);
const float s1 = bank->lookup(morph, l1, phase); const float s1 = bank->lookup(morph, l1, phase);
const float out = juce::jmap(preferL0, s1, s0); // simple crossfade const float out = juce::jmap(preferL0, s1, s0); // simple crossfade
phase += phaseInc; phase += phaseInc;
while (phase >= 1.0f) phase -= 1.0f; while (phase >= 1.0f) phase -= 1.0f;
return out; return out;
} }
private: private:
std::shared_ptr<Bank> bank; std::shared_ptr<Bank> bank;
double sampleRate { 44100.0 }; double sampleRate { 44100.0 };
float freq { 0.0f }; float freq { 0.0f };
float morph { 0.0f }; // 0..frames-1 float morph { 0.0f }; // 0..frames-1
float phase { 0.0f }; float phase { 0.0f };
float phaseInc { 0.0f }; float phaseInc { 0.0f };
}; };
} // namespace WT } // namespace WT