Compare commits
12 Commits
20b53a8780
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| b5e63d3adf | |||
| 61bcef19aa | |||
| 43b8670d4e | |||
| c5105693a2 | |||
| 0785f6fedd | |||
| 430ee53b98 | |||
| d9f672cd10 | |||
| a43db68120 | |||
| 7231f66689 | |||
| d104e3882e | |||
|
|
3ac2cc15b1 | ||
|
|
24eb354ace |
39
.gitattributes
vendored
Normal file
39
.gitattributes
vendored
Normal 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
2
.gitignore
vendored
@@ -1,2 +1,4 @@
|
|||||||
Builds
|
Builds
|
||||||
NeuralSynth Installer.exe
|
NeuralSynth Installer.exe
|
||||||
|
AudioPluginHost
|
||||||
|
Projucer
|
||||||
|
|||||||
19
AGENTS.md
Normal file
19
AGENTS.md
Normal 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 JUCE’s `juce::` namespace types. Prefer `PascalCase` for classes (e.g., `NeuralSynthAudioProcessor`), camelCase for methods and members (`prepareToPlay`, `audioEngine`), and suffix queues/collectors clearly (`AudioBufferQueue`, `ScopeDataCollector`). Match existing lambda formatting in `SynthVoice.cpp`, keep includes sorted locally, and avoid editing generated files under `JuceLibraryCode/`.
|
||||||
|
|
||||||
|
## Testing Guidelines
|
||||||
|
Automated tests are not yet configured; rely on manual validation in the standalone app or a DAW host. After each change, rebuild and audition key features: oscillator switching, chorus/delay/reverb chains, parameter automation, and MIDI input. For DSP tweaks, monitor the oscilloscope components linked to the buffer queues to confirm signal stability. Document ad-hoc test coverage in your pull request until formal tests are added.
|
||||||
|
|
||||||
|
## Commit & Pull Request Guidelines
|
||||||
|
Follow the existing concise, imperative commit style (`Add chorus modulation`, `Fix voice detune`). Scope each commit to a logical change and format messages as a single summary line. Pull requests should describe the motivation, outline testing performed, and link issues when relevant. Include platform notes (Linux, Windows installer) and screenshots or audio clips for UI-affecting or sonic changes so reviewers can assess impact quickly.
|
||||||
BIN
Examples/MPEIntroductionTutorial.zip
Normal file
BIN
Examples/MPEIntroductionTutorial.zip
Normal file
Binary file not shown.
@@ -0,0 +1,52 @@
|
|||||||
|
/*
|
||||||
|
|
||||||
|
IMPORTANT! This file is auto-generated each time you save your
|
||||||
|
project - if you alter its contents, your changes may be overwritten!
|
||||||
|
|
||||||
|
This is the header file that your files should include in order to get all the
|
||||||
|
JUCE library headers. You should avoid including the JUCE headers directly in
|
||||||
|
your own source files, because that wouldn't pick up the correct configuration
|
||||||
|
options for your app.
|
||||||
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
|
||||||
|
#include <juce_audio_basics/juce_audio_basics.h>
|
||||||
|
#include <juce_audio_devices/juce_audio_devices.h>
|
||||||
|
#include <juce_audio_formats/juce_audio_formats.h>
|
||||||
|
#include <juce_audio_processors/juce_audio_processors.h>
|
||||||
|
#include <juce_audio_utils/juce_audio_utils.h>
|
||||||
|
#include <juce_core/juce_core.h>
|
||||||
|
#include <juce_data_structures/juce_data_structures.h>
|
||||||
|
#include <juce_events/juce_events.h>
|
||||||
|
#include <juce_graphics/juce_graphics.h>
|
||||||
|
#include <juce_gui_basics/juce_gui_basics.h>
|
||||||
|
#include <juce_gui_extra/juce_gui_extra.h>
|
||||||
|
|
||||||
|
|
||||||
|
#if defined (JUCE_PROJUCER_VERSION) && JUCE_PROJUCER_VERSION < JUCE_VERSION
|
||||||
|
/** If you've hit this error then the version of the Projucer that was used to generate this project is
|
||||||
|
older than the version of the JUCE modules being included. To fix this error, re-save your project
|
||||||
|
using the latest version of the Projucer or, if you aren't using the Projucer to manage your project,
|
||||||
|
remove the JUCE_PROJUCER_VERSION define.
|
||||||
|
*/
|
||||||
|
#error "This project was last saved using an outdated version of the Projucer! Re-save this project with the latest version to fix this error."
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#if ! DONT_SET_USING_JUCE_NAMESPACE
|
||||||
|
// If your code uses a lot of JUCE classes, then this will obviously save you
|
||||||
|
// a lot of typing, but can be disabled by setting DONT_SET_USING_JUCE_NAMESPACE.
|
||||||
|
using namespace juce;
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#if ! JUCE_DONT_DECLARE_PROJECTINFO
|
||||||
|
namespace ProjectInfo
|
||||||
|
{
|
||||||
|
const char* const projectName = "MPEIntroductionTutorial";
|
||||||
|
const char* const companyName = "JUCE";
|
||||||
|
const char* const versionString = "1.0.0";
|
||||||
|
const int versionNumber = 0x10000;
|
||||||
|
}
|
||||||
|
#endif
|
||||||
12
Examples/MPEIntroductionTutorial/JuceLibraryCode/ReadMe.txt
Normal file
12
Examples/MPEIntroductionTutorial/JuceLibraryCode/ReadMe.txt
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
|
||||||
|
Important Note!!
|
||||||
|
================
|
||||||
|
|
||||||
|
The purpose of this folder is to contain files that are auto-generated by the Projucer,
|
||||||
|
and ALL files in this folder will be mercilessly DELETED and completely re-written whenever
|
||||||
|
the Projucer saves your project.
|
||||||
|
|
||||||
|
Therefore, it's a bad idea to make any manual changes to the files in here, or to
|
||||||
|
put any of your own files in here if you don't want to lose them. (Of course you may choose
|
||||||
|
to add the folder's contents to your version-control system so that you can re-merge your own
|
||||||
|
modifications after the Projucer has saved its changes).
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
/*
|
||||||
|
|
||||||
|
IMPORTANT! This file is auto-generated each time you save your
|
||||||
|
project - if you alter its contents, your changes may be overwritten!
|
||||||
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include <juce_audio_basics/juce_audio_basics.cpp>
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
/*
|
||||||
|
|
||||||
|
IMPORTANT! This file is auto-generated each time you save your
|
||||||
|
project - if you alter its contents, your changes may be overwritten!
|
||||||
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include <juce_audio_basics/juce_audio_basics.mm>
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
/*
|
||||||
|
|
||||||
|
IMPORTANT! This file is auto-generated each time you save your
|
||||||
|
project - if you alter its contents, your changes may be overwritten!
|
||||||
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include <juce_audio_devices/juce_audio_devices.cpp>
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
/*
|
||||||
|
|
||||||
|
IMPORTANT! This file is auto-generated each time you save your
|
||||||
|
project - if you alter its contents, your changes may be overwritten!
|
||||||
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include <juce_audio_devices/juce_audio_devices.mm>
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
/*
|
||||||
|
|
||||||
|
IMPORTANT! This file is auto-generated each time you save your
|
||||||
|
project - if you alter its contents, your changes may be overwritten!
|
||||||
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include <juce_audio_formats/juce_audio_formats.cpp>
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
/*
|
||||||
|
|
||||||
|
IMPORTANT! This file is auto-generated each time you save your
|
||||||
|
project - if you alter its contents, your changes may be overwritten!
|
||||||
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include <juce_audio_formats/juce_audio_formats.mm>
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
/*
|
||||||
|
|
||||||
|
IMPORTANT! This file is auto-generated each time you save your
|
||||||
|
project - if you alter its contents, your changes may be overwritten!
|
||||||
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include <juce_audio_processors/juce_audio_processors.cpp>
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
/*
|
||||||
|
|
||||||
|
IMPORTANT! This file is auto-generated each time you save your
|
||||||
|
project - if you alter its contents, your changes may be overwritten!
|
||||||
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include <juce_audio_processors/juce_audio_processors.mm>
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
/*
|
||||||
|
|
||||||
|
IMPORTANT! This file is auto-generated each time you save your
|
||||||
|
project - if you alter its contents, your changes may be overwritten!
|
||||||
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include <juce_audio_processors/juce_audio_processors_ara.cpp>
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
/*
|
||||||
|
|
||||||
|
IMPORTANT! This file is auto-generated each time you save your
|
||||||
|
project - if you alter its contents, your changes may be overwritten!
|
||||||
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include <juce_audio_processors/juce_audio_processors_lv2_libs.cpp>
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
/*
|
||||||
|
|
||||||
|
IMPORTANT! This file is auto-generated each time you save your
|
||||||
|
project - if you alter its contents, your changes may be overwritten!
|
||||||
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include <juce_audio_utils/juce_audio_utils.cpp>
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
/*
|
||||||
|
|
||||||
|
IMPORTANT! This file is auto-generated each time you save your
|
||||||
|
project - if you alter its contents, your changes may be overwritten!
|
||||||
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include <juce_audio_utils/juce_audio_utils.mm>
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
/*
|
||||||
|
|
||||||
|
IMPORTANT! This file is auto-generated each time you save your
|
||||||
|
project - if you alter its contents, your changes may be overwritten!
|
||||||
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include <juce_core/juce_core.cpp>
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
/*
|
||||||
|
|
||||||
|
IMPORTANT! This file is auto-generated each time you save your
|
||||||
|
project - if you alter its contents, your changes may be overwritten!
|
||||||
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include <juce_core/juce_core.mm>
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
/*
|
||||||
|
|
||||||
|
IMPORTANT! This file is auto-generated each time you save your
|
||||||
|
project - if you alter its contents, your changes may be overwritten!
|
||||||
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include <juce_core/juce_core_CompilationTime.cpp>
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
/*
|
||||||
|
|
||||||
|
IMPORTANT! This file is auto-generated each time you save your
|
||||||
|
project - if you alter its contents, your changes may be overwritten!
|
||||||
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include <juce_data_structures/juce_data_structures.cpp>
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
/*
|
||||||
|
|
||||||
|
IMPORTANT! This file is auto-generated each time you save your
|
||||||
|
project - if you alter its contents, your changes may be overwritten!
|
||||||
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include <juce_data_structures/juce_data_structures.mm>
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
/*
|
||||||
|
|
||||||
|
IMPORTANT! This file is auto-generated each time you save your
|
||||||
|
project - if you alter its contents, your changes may be overwritten!
|
||||||
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include <juce_events/juce_events.cpp>
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
/*
|
||||||
|
|
||||||
|
IMPORTANT! This file is auto-generated each time you save your
|
||||||
|
project - if you alter its contents, your changes may be overwritten!
|
||||||
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include <juce_events/juce_events.mm>
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
/*
|
||||||
|
|
||||||
|
IMPORTANT! This file is auto-generated each time you save your
|
||||||
|
project - if you alter its contents, your changes may be overwritten!
|
||||||
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include <juce_graphics/juce_graphics.cpp>
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
/*
|
||||||
|
|
||||||
|
IMPORTANT! This file is auto-generated each time you save your
|
||||||
|
project - if you alter its contents, your changes may be overwritten!
|
||||||
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include <juce_graphics/juce_graphics.mm>
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
/*
|
||||||
|
|
||||||
|
IMPORTANT! This file is auto-generated each time you save your
|
||||||
|
project - if you alter its contents, your changes may be overwritten!
|
||||||
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include <juce_graphics/juce_graphics_Harfbuzz.cpp>
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
/*
|
||||||
|
|
||||||
|
IMPORTANT! This file is auto-generated each time you save your
|
||||||
|
project - if you alter its contents, your changes may be overwritten!
|
||||||
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include <juce_graphics/juce_graphics_Sheenbidi.c>
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
/*
|
||||||
|
|
||||||
|
IMPORTANT! This file is auto-generated each time you save your
|
||||||
|
project - if you alter its contents, your changes may be overwritten!
|
||||||
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include <juce_gui_basics/juce_gui_basics.cpp>
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
/*
|
||||||
|
|
||||||
|
IMPORTANT! This file is auto-generated each time you save your
|
||||||
|
project - if you alter its contents, your changes may be overwritten!
|
||||||
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include <juce_gui_basics/juce_gui_basics.mm>
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
/*
|
||||||
|
|
||||||
|
IMPORTANT! This file is auto-generated each time you save your
|
||||||
|
project - if you alter its contents, your changes may be overwritten!
|
||||||
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include <juce_gui_extra/juce_gui_extra.cpp>
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
/*
|
||||||
|
|
||||||
|
IMPORTANT! This file is auto-generated each time you save your
|
||||||
|
project - if you alter its contents, your changes may be overwritten!
|
||||||
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include <juce_gui_extra/juce_gui_extra.mm>
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
|
||||||
|
<JUCERPROJECT name="MPEIntroductionTutorial" companyName="JUCE" version="1.0.0"
|
||||||
|
userNotes="Synthesiser using MPE specifications." companyWebsite="http://juce.com"
|
||||||
|
projectType="guiapp" useAppConfig="0" addUsingNamespaceToJuceHeader="1"
|
||||||
|
id="JGbUDb" jucerFormatVersion="1">
|
||||||
|
<MAINGROUP id="V9kDzX" name="MPEIntroductionTutorial">
|
||||||
|
<GROUP id="{DD17E4C5-2693-D5AD-A257-4695D52ED04A}" name="Source">
|
||||||
|
<FILE id="UcuJ7R" name="Main.cpp" compile="1" resource="0" file="Source/Main.cpp"/>
|
||||||
|
<FILE id="hNsrsX" name="MPEIntroductionTutorial.h" compile="0" resource="0"
|
||||||
|
file="Source/MPEIntroductionTutorial.h"/>
|
||||||
|
</GROUP>
|
||||||
|
</MAINGROUP>
|
||||||
|
<MODULES>
|
||||||
|
<MODULE id="juce_audio_basics" showAllCode="1" useLocalCopy="0" useGlobalPath="1"/>
|
||||||
|
<MODULE id="juce_audio_devices" showAllCode="1" useLocalCopy="0" useGlobalPath="1"/>
|
||||||
|
<MODULE id="juce_audio_formats" showAllCode="1" useLocalCopy="0" useGlobalPath="1"/>
|
||||||
|
<MODULE id="juce_audio_processors" showAllCode="1" useLocalCopy="0" useGlobalPath="1"/>
|
||||||
|
<MODULE id="juce_audio_utils" showAllCode="1" useLocalCopy="0" useGlobalPath="1"/>
|
||||||
|
<MODULE id="juce_core" showAllCode="1" useLocalCopy="0" useGlobalPath="1"/>
|
||||||
|
<MODULE id="juce_data_structures" showAllCode="1" useLocalCopy="0" useGlobalPath="1"/>
|
||||||
|
<MODULE id="juce_events" showAllCode="1" useLocalCopy="0" useGlobalPath="1"/>
|
||||||
|
<MODULE id="juce_graphics" showAllCode="1" useLocalCopy="0" useGlobalPath="1"/>
|
||||||
|
<MODULE id="juce_gui_basics" showAllCode="1" useLocalCopy="0" useGlobalPath="1"/>
|
||||||
|
<MODULE id="juce_gui_extra" showAllCode="1" useLocalCopy="0" useGlobalPath="1"/>
|
||||||
|
</MODULES>
|
||||||
|
<EXPORTFORMATS>
|
||||||
|
<LINUX_MAKE targetFolder="Builds/LinuxMakefile">
|
||||||
|
<CONFIGURATIONS>
|
||||||
|
<CONFIGURATION name="Debug" isDebug="1" optimisation="1" targetName="MPEIntroductionTutorial"/>
|
||||||
|
<CONFIGURATION name="Release" isDebug="0" optimisation="3" targetName="MPEIntroductionTutorial"/>
|
||||||
|
</CONFIGURATIONS>
|
||||||
|
<MODULEPATHS>
|
||||||
|
<MODULEPATH id="juce_audio_basics" path=""/>
|
||||||
|
<MODULEPATH id="juce_audio_devices" path=""/>
|
||||||
|
<MODULEPATH id="juce_audio_formats" path=""/>
|
||||||
|
<MODULEPATH id="juce_audio_processors" path=""/>
|
||||||
|
<MODULEPATH id="juce_audio_utils" path=""/>
|
||||||
|
<MODULEPATH id="juce_core" path=""/>
|
||||||
|
<MODULEPATH id="juce_data_structures" path=""/>
|
||||||
|
<MODULEPATH id="juce_events" path=""/>
|
||||||
|
<MODULEPATH id="juce_graphics" path=""/>
|
||||||
|
<MODULEPATH id="juce_gui_basics" path=""/>
|
||||||
|
<MODULEPATH id="juce_gui_extra" path=""/>
|
||||||
|
</MODULEPATHS>
|
||||||
|
</LINUX_MAKE>
|
||||||
|
</EXPORTFORMATS>
|
||||||
|
<JUCEOPTIONS/>
|
||||||
|
</JUCERPROJECT>
|
||||||
@@ -0,0 +1,536 @@
|
|||||||
|
/*
|
||||||
|
==============================================================================
|
||||||
|
|
||||||
|
This file is part of the JUCE tutorials.
|
||||||
|
Copyright (c) 2020 - Raw Material Software Limited
|
||||||
|
|
||||||
|
The code included in this file is provided under the terms of the ISC license
|
||||||
|
http://www.isc.org/downloads/software-support-policy/isc-license. Permission
|
||||||
|
To use, copy, modify, and/or distribute this software for any purpose with or
|
||||||
|
without fee is hereby granted provided that the above copyright notice and
|
||||||
|
this permission notice appear in all copies.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES,
|
||||||
|
WHETHER EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR
|
||||||
|
PURPOSE, ARE DISCLAIMED.
|
||||||
|
|
||||||
|
==============================================================================
|
||||||
|
*/
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
The block below describes the properties of this PIP. A PIP is a short snippet
|
||||||
|
of code that can be read by the Projucer and used to generate a JUCE project.
|
||||||
|
|
||||||
|
BEGIN_JUCE_PIP_METADATA
|
||||||
|
|
||||||
|
name: MPEIntroductionTutorial
|
||||||
|
version: 1.0.0
|
||||||
|
vendor: JUCE
|
||||||
|
website: http://juce.com
|
||||||
|
description: Synthesiser using MPE specifications.
|
||||||
|
|
||||||
|
dependencies: juce_audio_basics, juce_audio_devices, juce_audio_formats,
|
||||||
|
juce_audio_processors, juce_audio_utils, juce_core,
|
||||||
|
juce_data_structures, juce_events, juce_graphics,
|
||||||
|
juce_gui_basics, juce_gui_extra
|
||||||
|
exporters: xcode_mac, vs2019, linux_make
|
||||||
|
|
||||||
|
type: Component
|
||||||
|
mainClass: MainComponent
|
||||||
|
|
||||||
|
useLocalCopy: 1
|
||||||
|
|
||||||
|
END_JUCE_PIP_METADATA
|
||||||
|
|
||||||
|
*******************************************************************************/
|
||||||
|
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
//==============================================================================
|
||||||
|
class NoteComponent : public juce::Component
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
NoteComponent (const juce::MPENote& n)
|
||||||
|
: note (n), colour (juce::Colours::white)
|
||||||
|
{}
|
||||||
|
|
||||||
|
//==============================================================================
|
||||||
|
void update (const juce::MPENote& newNote, juce::Point<float> newCentre)
|
||||||
|
{
|
||||||
|
note = newNote;
|
||||||
|
centre = newCentre;
|
||||||
|
|
||||||
|
setBounds (getSquareAroundCentre (juce::jmax (getNoteOnRadius(), getNoteOffRadius(), getPressureRadius()))
|
||||||
|
.getUnion (getTextRectangle())
|
||||||
|
.getSmallestIntegerContainer()
|
||||||
|
.expanded (3));
|
||||||
|
|
||||||
|
repaint();
|
||||||
|
}
|
||||||
|
|
||||||
|
//==============================================================================
|
||||||
|
void paint (juce::Graphics& g) override
|
||||||
|
{
|
||||||
|
if (note.keyState == juce::MPENote::keyDown || note.keyState == juce::MPENote::keyDownAndSustained)
|
||||||
|
drawPressedNoteCircle (g, colour);
|
||||||
|
else if (note.keyState == juce::MPENote::sustained)
|
||||||
|
drawSustainedNoteCircle (g, colour);
|
||||||
|
else
|
||||||
|
return;
|
||||||
|
|
||||||
|
drawNoteLabel (g, colour);
|
||||||
|
}
|
||||||
|
|
||||||
|
//==============================================================================
|
||||||
|
juce::MPENote note;
|
||||||
|
juce::Colour colour;
|
||||||
|
juce::Point<float> centre;
|
||||||
|
|
||||||
|
private:
|
||||||
|
//==============================================================================
|
||||||
|
void drawPressedNoteCircle (juce::Graphics& g, juce::Colour zoneColour)
|
||||||
|
{
|
||||||
|
g.setColour (zoneColour.withAlpha (0.3f));
|
||||||
|
g.fillEllipse (translateToLocalBounds (getSquareAroundCentre (getNoteOnRadius())));
|
||||||
|
g.setColour (zoneColour);
|
||||||
|
g.drawEllipse (translateToLocalBounds (getSquareAroundCentre (getPressureRadius())), 2.0f);
|
||||||
|
}
|
||||||
|
|
||||||
|
//==============================================================================
|
||||||
|
void drawSustainedNoteCircle (juce::Graphics& g, juce::Colour zoneColour)
|
||||||
|
{
|
||||||
|
g.setColour (zoneColour);
|
||||||
|
juce::Path circle, dashedCircle;
|
||||||
|
circle.addEllipse (translateToLocalBounds (getSquareAroundCentre (getNoteOffRadius())));
|
||||||
|
const float dashLengths[] = { 3.0f, 3.0f };
|
||||||
|
juce::PathStrokeType (2.0, juce::PathStrokeType::mitered).createDashedStroke (dashedCircle, circle, dashLengths, 2);
|
||||||
|
g.fillPath (dashedCircle);
|
||||||
|
}
|
||||||
|
|
||||||
|
//==============================================================================
|
||||||
|
void drawNoteLabel (juce::Graphics& g, juce::Colour)
|
||||||
|
{
|
||||||
|
auto textBounds = translateToLocalBounds (getTextRectangle()).getSmallestIntegerContainer();
|
||||||
|
g.drawText ("+", textBounds, juce::Justification::centred);
|
||||||
|
g.drawText (juce::MidiMessage::getMidiNoteName (note.initialNote, true, true, 3), textBounds, juce::Justification::centredBottom);
|
||||||
|
g.setFont (juce::Font (22.0f, juce::Font::bold));
|
||||||
|
g.drawText (juce::String (note.midiChannel), textBounds, juce::Justification::centredTop);
|
||||||
|
}
|
||||||
|
|
||||||
|
//==============================================================================
|
||||||
|
juce::Rectangle<float> getSquareAroundCentre (float radius) const noexcept
|
||||||
|
{
|
||||||
|
return juce::Rectangle<float> (radius * 2.0f, radius * 2.0f).withCentre (centre);
|
||||||
|
}
|
||||||
|
|
||||||
|
juce::Rectangle<float> translateToLocalBounds (juce::Rectangle<float> r) const noexcept
|
||||||
|
{
|
||||||
|
return r - getPosition().toFloat();
|
||||||
|
}
|
||||||
|
|
||||||
|
juce::Rectangle<float> getTextRectangle() const noexcept
|
||||||
|
{
|
||||||
|
return juce::Rectangle<float> (30.0f, 50.0f).withCentre (centre);
|
||||||
|
}
|
||||||
|
|
||||||
|
float getNoteOnRadius() const noexcept { return note.noteOnVelocity.asUnsignedFloat() * maxNoteRadius; }
|
||||||
|
float getNoteOffRadius() const noexcept { return note.noteOffVelocity.asUnsignedFloat() * maxNoteRadius; }
|
||||||
|
float getPressureRadius() const noexcept { return note.pressure.asUnsignedFloat() * maxNoteRadius; }
|
||||||
|
|
||||||
|
static constexpr auto maxNoteRadius = 100.0f;
|
||||||
|
|
||||||
|
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (NoteComponent)
|
||||||
|
};
|
||||||
|
|
||||||
|
//==============================================================================
|
||||||
|
class Visualiser : public juce::Component,
|
||||||
|
public juce::MPEInstrument::Listener,
|
||||||
|
private juce::AsyncUpdater
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
//==============================================================================
|
||||||
|
Visualiser() {}
|
||||||
|
|
||||||
|
//==============================================================================
|
||||||
|
void paint (juce::Graphics& g) override
|
||||||
|
{
|
||||||
|
g.fillAll (juce::Colours::black);
|
||||||
|
|
||||||
|
auto noteDistance = float (getWidth()) / 128;
|
||||||
|
|
||||||
|
for (auto i = 0; i < 128; ++i)
|
||||||
|
{
|
||||||
|
auto x = noteDistance * (float) i;
|
||||||
|
auto noteHeight = int (juce::MidiMessage::isMidiNoteBlack (i) ? 0.7 * getHeight() : getHeight());
|
||||||
|
g.setColour (juce::MidiMessage::isMidiNoteBlack (i) ? juce::Colours::white : juce::Colours::grey);
|
||||||
|
g.drawLine (x, 0.0f, x, (float) noteHeight);
|
||||||
|
|
||||||
|
if (i > 0 && i % 12 == 0)
|
||||||
|
{
|
||||||
|
g.setColour (juce::Colours::grey);
|
||||||
|
auto octaveNumber = (i / 12) - 2;
|
||||||
|
g.drawText ("C" + juce::String (octaveNumber), (int) x - 15, getHeight() - 30, 30, 30, juce::Justification::centredBottom);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//==============================================================================
|
||||||
|
void noteAdded (juce::MPENote newNote) override
|
||||||
|
{
|
||||||
|
const juce::ScopedLock sl (lock);
|
||||||
|
activeNotes.add (newNote);
|
||||||
|
triggerAsyncUpdate();
|
||||||
|
}
|
||||||
|
|
||||||
|
void notePressureChanged (juce::MPENote note) override { noteChanged (note); }
|
||||||
|
void notePitchbendChanged (juce::MPENote note) override { noteChanged (note); }
|
||||||
|
void noteTimbreChanged (juce::MPENote note) override { noteChanged (note); }
|
||||||
|
void noteKeyStateChanged (juce::MPENote note) override { noteChanged (note); }
|
||||||
|
|
||||||
|
void noteChanged (juce::MPENote changedNote)
|
||||||
|
{
|
||||||
|
const juce::ScopedLock sl (lock);
|
||||||
|
|
||||||
|
for (auto& note : activeNotes)
|
||||||
|
if (note.noteID == changedNote.noteID)
|
||||||
|
note = changedNote;
|
||||||
|
|
||||||
|
triggerAsyncUpdate();
|
||||||
|
}
|
||||||
|
|
||||||
|
void noteReleased (juce::MPENote finishedNote) override
|
||||||
|
{
|
||||||
|
const juce::ScopedLock sl (lock);
|
||||||
|
|
||||||
|
for (auto i = activeNotes.size(); --i >= 0;)
|
||||||
|
if (activeNotes.getReference(i).noteID == finishedNote.noteID)
|
||||||
|
activeNotes.remove (i);
|
||||||
|
|
||||||
|
triggerAsyncUpdate();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private:
|
||||||
|
//==============================================================================
|
||||||
|
const juce::MPENote* findActiveNote (int noteID) const noexcept
|
||||||
|
{
|
||||||
|
for (auto& note : activeNotes)
|
||||||
|
if (note.noteID == noteID)
|
||||||
|
return ¬e;
|
||||||
|
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
NoteComponent* findNoteComponent (int noteID) const noexcept
|
||||||
|
{
|
||||||
|
for (auto& noteComp : noteComponents)
|
||||||
|
if (noteComp->note.noteID == noteID)
|
||||||
|
return noteComp;
|
||||||
|
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
//==============================================================================
|
||||||
|
void handleAsyncUpdate() override
|
||||||
|
{
|
||||||
|
const juce::ScopedLock sl (lock);
|
||||||
|
|
||||||
|
for (auto i = noteComponents.size(); --i >= 0;)
|
||||||
|
if (findActiveNote (noteComponents.getUnchecked(i)->note.noteID) == nullptr)
|
||||||
|
noteComponents.remove (i);
|
||||||
|
|
||||||
|
for (auto& note : activeNotes)
|
||||||
|
if (findNoteComponent (note.noteID) == nullptr)
|
||||||
|
addAndMakeVisible (noteComponents.add (new NoteComponent (note)));
|
||||||
|
|
||||||
|
for (auto& noteComp : noteComponents)
|
||||||
|
if (auto* noteInfo = findActiveNote (noteComp->note.noteID))
|
||||||
|
noteComp->update (*noteInfo, getCentrePositionForNote (*noteInfo));
|
||||||
|
}
|
||||||
|
|
||||||
|
//==============================================================================
|
||||||
|
juce::Point<float> getCentrePositionForNote (juce::MPENote note) const
|
||||||
|
{
|
||||||
|
auto n = float (note.initialNote) + float (note.totalPitchbendInSemitones);
|
||||||
|
auto x = (float) getWidth() * n / 128;
|
||||||
|
auto y = (float) getHeight() * (1 - note.timbre.asUnsignedFloat());
|
||||||
|
|
||||||
|
return { x, y };
|
||||||
|
}
|
||||||
|
|
||||||
|
//==============================================================================
|
||||||
|
juce::OwnedArray<NoteComponent> noteComponents;
|
||||||
|
juce::CriticalSection lock;
|
||||||
|
juce::Array<juce::MPENote> activeNotes;
|
||||||
|
|
||||||
|
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (Visualiser)
|
||||||
|
};
|
||||||
|
|
||||||
|
//==============================================================================
|
||||||
|
class MPEDemoSynthVoice : public juce::MPESynthesiserVoice
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
//==============================================================================
|
||||||
|
MPEDemoSynthVoice() {}
|
||||||
|
|
||||||
|
//==============================================================================
|
||||||
|
void noteStarted() override
|
||||||
|
{
|
||||||
|
jassert (currentlyPlayingNote.isValid());
|
||||||
|
jassert (currentlyPlayingNote.keyState == juce::MPENote::keyDown
|
||||||
|
|| currentlyPlayingNote.keyState == juce::MPENote::keyDownAndSustained);
|
||||||
|
|
||||||
|
// get data from the current MPENote
|
||||||
|
level .setTargetValue (currentlyPlayingNote.pressure.asUnsignedFloat());
|
||||||
|
frequency.setTargetValue (currentlyPlayingNote.getFrequencyInHertz());
|
||||||
|
timbre .setTargetValue (currentlyPlayingNote.timbre.asUnsignedFloat());
|
||||||
|
|
||||||
|
phase = 0.0;
|
||||||
|
auto cyclesPerSample = frequency.getNextValue() / currentSampleRate;
|
||||||
|
phaseDelta = 2.0 * juce::MathConstants<double>::pi * cyclesPerSample;
|
||||||
|
|
||||||
|
tailOff = 0.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
void noteStopped (bool allowTailOff) override
|
||||||
|
{
|
||||||
|
jassert (currentlyPlayingNote.keyState == juce::MPENote::off);
|
||||||
|
|
||||||
|
if (allowTailOff)
|
||||||
|
{
|
||||||
|
// start a tail-off by setting this flag. The render callback will pick up on
|
||||||
|
// this and do a fade out, calling clearCurrentNote() when it's finished.
|
||||||
|
|
||||||
|
if (tailOff == 0.0) // we only need to begin a tail-off if it's not already doing so - the
|
||||||
|
// stopNote method could be called more than once.
|
||||||
|
tailOff = 1.0;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// we're being told to stop playing immediately, so reset everything..
|
||||||
|
clearCurrentNote();
|
||||||
|
phaseDelta = 0.0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void notePressureChanged() override
|
||||||
|
{
|
||||||
|
level.setTargetValue (currentlyPlayingNote.pressure.asUnsignedFloat());
|
||||||
|
}
|
||||||
|
|
||||||
|
void notePitchbendChanged() override
|
||||||
|
{
|
||||||
|
frequency.setTargetValue (currentlyPlayingNote.getFrequencyInHertz());
|
||||||
|
}
|
||||||
|
|
||||||
|
void noteTimbreChanged() override
|
||||||
|
{
|
||||||
|
timbre.setTargetValue (currentlyPlayingNote.timbre.asUnsignedFloat());
|
||||||
|
}
|
||||||
|
|
||||||
|
void noteKeyStateChanged() override {}
|
||||||
|
|
||||||
|
void setCurrentSampleRate (double newRate) override
|
||||||
|
{
|
||||||
|
if (! juce::approximatelyEqual (currentSampleRate, newRate))
|
||||||
|
{
|
||||||
|
noteStopped (false);
|
||||||
|
currentSampleRate = newRate;
|
||||||
|
|
||||||
|
level .reset (currentSampleRate, smoothingLengthInSeconds);
|
||||||
|
timbre .reset (currentSampleRate, smoothingLengthInSeconds);
|
||||||
|
frequency.reset (currentSampleRate, smoothingLengthInSeconds);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//==============================================================================
|
||||||
|
void renderNextBlock (juce::AudioBuffer<float>& outputBuffer,
|
||||||
|
int startSample,
|
||||||
|
int numSamples) override
|
||||||
|
{
|
||||||
|
if (phaseDelta != 0.0)
|
||||||
|
{
|
||||||
|
if (tailOff > 0.0)
|
||||||
|
{
|
||||||
|
while (--numSamples >= 0)
|
||||||
|
{
|
||||||
|
auto currentSample = getNextSample() * (float) tailOff;
|
||||||
|
|
||||||
|
for (auto i = outputBuffer.getNumChannels(); --i >= 0;)
|
||||||
|
outputBuffer.addSample (i, startSample, currentSample);
|
||||||
|
|
||||||
|
++startSample;
|
||||||
|
|
||||||
|
tailOff *= 0.99;
|
||||||
|
|
||||||
|
if (tailOff <= 0.005)
|
||||||
|
{
|
||||||
|
clearCurrentNote();
|
||||||
|
|
||||||
|
phaseDelta = 0.0;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
while (--numSamples >= 0)
|
||||||
|
{
|
||||||
|
auto currentSample = getNextSample();
|
||||||
|
|
||||||
|
for (auto i = outputBuffer.getNumChannels(); --i >= 0;)
|
||||||
|
outputBuffer.addSample (i, startSample, currentSample);
|
||||||
|
|
||||||
|
++startSample;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
//==============================================================================
|
||||||
|
float getNextSample() noexcept
|
||||||
|
{
|
||||||
|
auto levelDb = (level.getNextValue() - 1.0) * maxLevelDb;
|
||||||
|
auto amplitude = std::pow (10.0f, 0.05f * levelDb) * maxLevel;
|
||||||
|
|
||||||
|
// timbre is used to blend between a sine and a square.
|
||||||
|
auto f1 = std::sin (phase);
|
||||||
|
auto f2 = std::copysign (1.0, f1);
|
||||||
|
auto a2 = timbre.getNextValue();
|
||||||
|
auto a1 = 1.0 - a2;
|
||||||
|
|
||||||
|
auto nextSample = float (amplitude * ((a1 * f1) + (a2 * f2)));
|
||||||
|
|
||||||
|
auto cyclesPerSample = frequency.getNextValue() / currentSampleRate;
|
||||||
|
phaseDelta = 2.0 * juce::MathConstants<double>::pi * cyclesPerSample;
|
||||||
|
phase = std::fmod (phase + phaseDelta, 2.0 * juce::MathConstants<double>::pi);
|
||||||
|
|
||||||
|
return nextSample;
|
||||||
|
}
|
||||||
|
|
||||||
|
//==============================================================================
|
||||||
|
juce::SmoothedValue<double> level, timbre, frequency;
|
||||||
|
|
||||||
|
double phase = 0.0;
|
||||||
|
double phaseDelta = 0.0;
|
||||||
|
double tailOff = 0.0;
|
||||||
|
|
||||||
|
// some useful constants
|
||||||
|
static constexpr auto maxLevel = 0.05;
|
||||||
|
static constexpr auto maxLevelDb = 31.0;
|
||||||
|
static constexpr auto smoothingLengthInSeconds = 0.01;
|
||||||
|
};
|
||||||
|
|
||||||
|
//==============================================================================
|
||||||
|
class MainComponent : public juce::Component,
|
||||||
|
private juce::AudioIODeviceCallback, // [1]
|
||||||
|
private juce::MidiInputCallback // [2]
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
//==============================================================================
|
||||||
|
MainComponent()
|
||||||
|
: audioSetupComp (audioDeviceManager, 0, 0, 0, 256,
|
||||||
|
true, // showMidiInputOptions must be true
|
||||||
|
true, true, false)
|
||||||
|
{
|
||||||
|
audioDeviceManager.initialise (0, 2, nullptr, true, {}, nullptr);
|
||||||
|
audioDeviceManager.addMidiInputDeviceCallback ({}, this); // [6]
|
||||||
|
audioDeviceManager.addAudioCallback (this);
|
||||||
|
|
||||||
|
addAndMakeVisible (audioSetupComp);
|
||||||
|
addAndMakeVisible (visualiserViewport);
|
||||||
|
|
||||||
|
visualiserViewport.setScrollBarsShown (false, true);
|
||||||
|
visualiserViewport.setViewedComponent (&visualiserComp, false);
|
||||||
|
visualiserViewport.setViewPositionProportionately (0.5, 0.0);
|
||||||
|
|
||||||
|
visualiserInstrument.addListener (&visualiserComp);
|
||||||
|
|
||||||
|
for (auto i = 0; i < 15; ++i)
|
||||||
|
synth.addVoice (new MPEDemoSynthVoice());
|
||||||
|
|
||||||
|
synth.enableLegacyMode (24);
|
||||||
|
synth.setVoiceStealingEnabled (false);
|
||||||
|
|
||||||
|
visualiserInstrument.enableLegacyMode (24);
|
||||||
|
|
||||||
|
setSize (650, 560);
|
||||||
|
}
|
||||||
|
|
||||||
|
~MainComponent() override
|
||||||
|
{
|
||||||
|
audioDeviceManager.removeMidiInputDeviceCallback ({}, this);
|
||||||
|
audioDeviceManager.removeAudioCallback (this);
|
||||||
|
}
|
||||||
|
|
||||||
|
//==============================================================================
|
||||||
|
void resized() override
|
||||||
|
{
|
||||||
|
auto visualiserCompWidth = 2800;
|
||||||
|
auto visualiserCompHeight = 300;
|
||||||
|
|
||||||
|
auto r = getLocalBounds();
|
||||||
|
|
||||||
|
visualiserViewport.setBounds (r.removeFromBottom (visualiserCompHeight));
|
||||||
|
visualiserComp.setBounds ({ visualiserCompWidth,
|
||||||
|
visualiserViewport.getHeight() - visualiserViewport.getScrollBarThickness() });
|
||||||
|
|
||||||
|
audioSetupComp.setBounds (r);
|
||||||
|
}
|
||||||
|
|
||||||
|
//==============================================================================
|
||||||
|
void audioDeviceIOCallbackWithContext (const float* const* /*inputChannelData*/,
|
||||||
|
int /*numInputChannels*/,
|
||||||
|
float* const* outputChannelData,
|
||||||
|
int numOutputChannels,
|
||||||
|
int numSamples,
|
||||||
|
const juce::AudioIODeviceCallbackContext& /*context*/) override
|
||||||
|
{
|
||||||
|
// make buffer
|
||||||
|
juce::AudioBuffer<float> buffer (outputChannelData, numOutputChannels, numSamples);
|
||||||
|
|
||||||
|
// clear it to silence
|
||||||
|
buffer.clear();
|
||||||
|
|
||||||
|
juce::MidiBuffer incomingMidi;
|
||||||
|
|
||||||
|
// get the MIDI messages for this audio block
|
||||||
|
midiCollector.removeNextBlockOfMessages (incomingMidi, numSamples);
|
||||||
|
|
||||||
|
// synthesise the block
|
||||||
|
synth.renderNextBlock (buffer, incomingMidi, 0, numSamples);
|
||||||
|
}
|
||||||
|
|
||||||
|
void audioDeviceAboutToStart (juce::AudioIODevice* device) override
|
||||||
|
{
|
||||||
|
auto sampleRate = device->getCurrentSampleRate();
|
||||||
|
midiCollector.reset (sampleRate);
|
||||||
|
synth.setCurrentPlaybackSampleRate (sampleRate);
|
||||||
|
}
|
||||||
|
|
||||||
|
void audioDeviceStopped() override {}
|
||||||
|
|
||||||
|
private:
|
||||||
|
//==============================================================================
|
||||||
|
void handleIncomingMidiMessage (juce::MidiInput* /*source*/,
|
||||||
|
const juce::MidiMessage& message) override
|
||||||
|
{
|
||||||
|
visualiserInstrument.processNextMidiEvent (message);
|
||||||
|
midiCollector.addMessageToQueue (message);
|
||||||
|
}
|
||||||
|
|
||||||
|
//==============================================================================
|
||||||
|
juce::AudioDeviceManager audioDeviceManager; // [3]
|
||||||
|
juce::AudioDeviceSelectorComponent audioSetupComp; // [4]
|
||||||
|
|
||||||
|
Visualiser visualiserComp;
|
||||||
|
juce::Viewport visualiserViewport;
|
||||||
|
|
||||||
|
juce::MPEInstrument visualiserInstrument;
|
||||||
|
juce::MPESynthesiser synth;
|
||||||
|
juce::MidiMessageCollector midiCollector; // [5]
|
||||||
|
|
||||||
|
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (MainComponent)
|
||||||
|
};
|
||||||
96
Examples/MPEIntroductionTutorial/Source/Main.cpp
Normal file
96
Examples/MPEIntroductionTutorial/Source/Main.cpp
Normal file
@@ -0,0 +1,96 @@
|
|||||||
|
/*
|
||||||
|
==============================================================================
|
||||||
|
|
||||||
|
This file contains the startup code for a PIP.
|
||||||
|
|
||||||
|
==============================================================================
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include <JuceHeader.h>
|
||||||
|
#include "MPEIntroductionTutorial.h"
|
||||||
|
|
||||||
|
class Application : public juce::JUCEApplication
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
//==============================================================================
|
||||||
|
Application() = default;
|
||||||
|
|
||||||
|
const juce::String getApplicationName() override { return "MPEIntroductionTutorial"; }
|
||||||
|
const juce::String getApplicationVersion() override { return "1.0.0"; }
|
||||||
|
|
||||||
|
void initialise (const juce::String&) override
|
||||||
|
{
|
||||||
|
mainWindow.reset (new MainWindow ("MPEIntroductionTutorial", std::make_unique<MainComponent>(), *this));
|
||||||
|
}
|
||||||
|
|
||||||
|
void shutdown() override { mainWindow = nullptr; }
|
||||||
|
|
||||||
|
private:
|
||||||
|
class MainWindow : public juce::DocumentWindow
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
MainWindow (const juce::String& name, std::unique_ptr<juce::Component> c, JUCEApplication& a)
|
||||||
|
: DocumentWindow (name, juce::Desktop::getInstance().getDefaultLookAndFeel()
|
||||||
|
.findColour (ResizableWindow::backgroundColourId),
|
||||||
|
juce::DocumentWindow::allButtons),
|
||||||
|
app (a)
|
||||||
|
{
|
||||||
|
setUsingNativeTitleBar (true);
|
||||||
|
|
||||||
|
#if JUCE_ANDROID || JUCE_IOS
|
||||||
|
setContentOwned (new SafeAreaComponent { std::move (c) }, true);
|
||||||
|
setFullScreen (true);
|
||||||
|
#else
|
||||||
|
setContentOwned (c.release(), true);
|
||||||
|
setResizable (true, false);
|
||||||
|
setResizeLimits (300, 250, 10000, 10000);
|
||||||
|
centreWithSize (getWidth(), getHeight());
|
||||||
|
#endif
|
||||||
|
|
||||||
|
setVisible (true);
|
||||||
|
}
|
||||||
|
|
||||||
|
void closeButtonPressed() override
|
||||||
|
{
|
||||||
|
app.systemRequestedQuit();
|
||||||
|
}
|
||||||
|
|
||||||
|
#if JUCE_ANDROID || JUCE_IOS
|
||||||
|
class SafeAreaComponent : public juce::Component
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
explicit SafeAreaComponent (std::unique_ptr<Component> c)
|
||||||
|
: content (std::move (c))
|
||||||
|
{
|
||||||
|
addAndMakeVisible (*content);
|
||||||
|
}
|
||||||
|
|
||||||
|
void resized() override
|
||||||
|
{
|
||||||
|
if (const auto* d = Desktop::getInstance().getDisplays().getDisplayForRect (getLocalBounds()))
|
||||||
|
content->setBounds (d->safeAreaInsets.subtractedFrom (getLocalBounds()));
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
std::unique_ptr<Component> content;
|
||||||
|
};
|
||||||
|
|
||||||
|
void parentSizeChanged() override
|
||||||
|
{
|
||||||
|
if (auto* c = getContentComponent())
|
||||||
|
c->resized();
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
private:
|
||||||
|
JUCEApplication& app;
|
||||||
|
|
||||||
|
//==============================================================================
|
||||||
|
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (MainWindow)
|
||||||
|
};
|
||||||
|
|
||||||
|
std::unique_ptr<MainWindow> mainWindow;
|
||||||
|
};
|
||||||
|
|
||||||
|
//==============================================================================
|
||||||
|
START_JUCE_APPLICATION (Application)
|
||||||
BIN
Examples/WavetableSynthTutorial.zip
Normal file
BIN
Examples/WavetableSynthTutorial.zip
Normal file
Binary file not shown.
52
Examples/WavetableSynthTutorial/JuceLibraryCode/JuceHeader.h
Normal file
52
Examples/WavetableSynthTutorial/JuceLibraryCode/JuceHeader.h
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
/*
|
||||||
|
|
||||||
|
IMPORTANT! This file is auto-generated each time you save your
|
||||||
|
project - if you alter its contents, your changes may be overwritten!
|
||||||
|
|
||||||
|
This is the header file that your files should include in order to get all the
|
||||||
|
JUCE library headers. You should avoid including the JUCE headers directly in
|
||||||
|
your own source files, because that wouldn't pick up the correct configuration
|
||||||
|
options for your app.
|
||||||
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
|
||||||
|
#include <juce_audio_basics/juce_audio_basics.h>
|
||||||
|
#include <juce_audio_devices/juce_audio_devices.h>
|
||||||
|
#include <juce_audio_formats/juce_audio_formats.h>
|
||||||
|
#include <juce_audio_processors/juce_audio_processors.h>
|
||||||
|
#include <juce_audio_utils/juce_audio_utils.h>
|
||||||
|
#include <juce_core/juce_core.h>
|
||||||
|
#include <juce_data_structures/juce_data_structures.h>
|
||||||
|
#include <juce_events/juce_events.h>
|
||||||
|
#include <juce_graphics/juce_graphics.h>
|
||||||
|
#include <juce_gui_basics/juce_gui_basics.h>
|
||||||
|
#include <juce_gui_extra/juce_gui_extra.h>
|
||||||
|
|
||||||
|
|
||||||
|
#if defined (JUCE_PROJUCER_VERSION) && JUCE_PROJUCER_VERSION < JUCE_VERSION
|
||||||
|
/** If you've hit this error then the version of the Projucer that was used to generate this project is
|
||||||
|
older than the version of the JUCE modules being included. To fix this error, re-save your project
|
||||||
|
using the latest version of the Projucer or, if you aren't using the Projucer to manage your project,
|
||||||
|
remove the JUCE_PROJUCER_VERSION define.
|
||||||
|
*/
|
||||||
|
#error "This project was last saved using an outdated version of the Projucer! Re-save this project with the latest version to fix this error."
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#if ! DONT_SET_USING_JUCE_NAMESPACE
|
||||||
|
// If your code uses a lot of JUCE classes, then this will obviously save you
|
||||||
|
// a lot of typing, but can be disabled by setting DONT_SET_USING_JUCE_NAMESPACE.
|
||||||
|
using namespace juce;
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#if ! JUCE_DONT_DECLARE_PROJECTINFO
|
||||||
|
namespace ProjectInfo
|
||||||
|
{
|
||||||
|
const char* const projectName = "WavetableSynthTutorial";
|
||||||
|
const char* const companyName = "JUCE";
|
||||||
|
const char* const versionString = "1.0.0";
|
||||||
|
const int versionNumber = 0x10000;
|
||||||
|
}
|
||||||
|
#endif
|
||||||
12
Examples/WavetableSynthTutorial/JuceLibraryCode/ReadMe.txt
Normal file
12
Examples/WavetableSynthTutorial/JuceLibraryCode/ReadMe.txt
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
|
||||||
|
Important Note!!
|
||||||
|
================
|
||||||
|
|
||||||
|
The purpose of this folder is to contain files that are auto-generated by the Projucer,
|
||||||
|
and ALL files in this folder will be mercilessly DELETED and completely re-written whenever
|
||||||
|
the Projucer saves your project.
|
||||||
|
|
||||||
|
Therefore, it's a bad idea to make any manual changes to the files in here, or to
|
||||||
|
put any of your own files in here if you don't want to lose them. (Of course you may choose
|
||||||
|
to add the folder's contents to your version-control system so that you can re-merge your own
|
||||||
|
modifications after the Projucer has saved its changes).
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
/*
|
||||||
|
|
||||||
|
IMPORTANT! This file is auto-generated each time you save your
|
||||||
|
project - if you alter its contents, your changes may be overwritten!
|
||||||
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include <juce_audio_basics/juce_audio_basics.cpp>
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
/*
|
||||||
|
|
||||||
|
IMPORTANT! This file is auto-generated each time you save your
|
||||||
|
project - if you alter its contents, your changes may be overwritten!
|
||||||
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include <juce_audio_basics/juce_audio_basics.mm>
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
/*
|
||||||
|
|
||||||
|
IMPORTANT! This file is auto-generated each time you save your
|
||||||
|
project - if you alter its contents, your changes may be overwritten!
|
||||||
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include <juce_audio_devices/juce_audio_devices.cpp>
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
/*
|
||||||
|
|
||||||
|
IMPORTANT! This file is auto-generated each time you save your
|
||||||
|
project - if you alter its contents, your changes may be overwritten!
|
||||||
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include <juce_audio_devices/juce_audio_devices.mm>
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
/*
|
||||||
|
|
||||||
|
IMPORTANT! This file is auto-generated each time you save your
|
||||||
|
project - if you alter its contents, your changes may be overwritten!
|
||||||
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include <juce_audio_formats/juce_audio_formats.cpp>
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
/*
|
||||||
|
|
||||||
|
IMPORTANT! This file is auto-generated each time you save your
|
||||||
|
project - if you alter its contents, your changes may be overwritten!
|
||||||
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include <juce_audio_formats/juce_audio_formats.mm>
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
/*
|
||||||
|
|
||||||
|
IMPORTANT! This file is auto-generated each time you save your
|
||||||
|
project - if you alter its contents, your changes may be overwritten!
|
||||||
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include <juce_audio_processors/juce_audio_processors.cpp>
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
/*
|
||||||
|
|
||||||
|
IMPORTANT! This file is auto-generated each time you save your
|
||||||
|
project - if you alter its contents, your changes may be overwritten!
|
||||||
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include <juce_audio_processors/juce_audio_processors.mm>
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
/*
|
||||||
|
|
||||||
|
IMPORTANT! This file is auto-generated each time you save your
|
||||||
|
project - if you alter its contents, your changes may be overwritten!
|
||||||
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include <juce_audio_processors/juce_audio_processors_ara.cpp>
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
/*
|
||||||
|
|
||||||
|
IMPORTANT! This file is auto-generated each time you save your
|
||||||
|
project - if you alter its contents, your changes may be overwritten!
|
||||||
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include <juce_audio_processors/juce_audio_processors_lv2_libs.cpp>
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
/*
|
||||||
|
|
||||||
|
IMPORTANT! This file is auto-generated each time you save your
|
||||||
|
project - if you alter its contents, your changes may be overwritten!
|
||||||
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include <juce_audio_utils/juce_audio_utils.cpp>
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
/*
|
||||||
|
|
||||||
|
IMPORTANT! This file is auto-generated each time you save your
|
||||||
|
project - if you alter its contents, your changes may be overwritten!
|
||||||
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include <juce_audio_utils/juce_audio_utils.mm>
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
/*
|
||||||
|
|
||||||
|
IMPORTANT! This file is auto-generated each time you save your
|
||||||
|
project - if you alter its contents, your changes may be overwritten!
|
||||||
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include <juce_core/juce_core.cpp>
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
/*
|
||||||
|
|
||||||
|
IMPORTANT! This file is auto-generated each time you save your
|
||||||
|
project - if you alter its contents, your changes may be overwritten!
|
||||||
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include <juce_core/juce_core.mm>
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
/*
|
||||||
|
|
||||||
|
IMPORTANT! This file is auto-generated each time you save your
|
||||||
|
project - if you alter its contents, your changes may be overwritten!
|
||||||
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include <juce_core/juce_core_CompilationTime.cpp>
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
/*
|
||||||
|
|
||||||
|
IMPORTANT! This file is auto-generated each time you save your
|
||||||
|
project - if you alter its contents, your changes may be overwritten!
|
||||||
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include <juce_data_structures/juce_data_structures.cpp>
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
/*
|
||||||
|
|
||||||
|
IMPORTANT! This file is auto-generated each time you save your
|
||||||
|
project - if you alter its contents, your changes may be overwritten!
|
||||||
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include <juce_data_structures/juce_data_structures.mm>
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
/*
|
||||||
|
|
||||||
|
IMPORTANT! This file is auto-generated each time you save your
|
||||||
|
project - if you alter its contents, your changes may be overwritten!
|
||||||
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include <juce_events/juce_events.cpp>
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
/*
|
||||||
|
|
||||||
|
IMPORTANT! This file is auto-generated each time you save your
|
||||||
|
project - if you alter its contents, your changes may be overwritten!
|
||||||
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include <juce_events/juce_events.mm>
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
/*
|
||||||
|
|
||||||
|
IMPORTANT! This file is auto-generated each time you save your
|
||||||
|
project - if you alter its contents, your changes may be overwritten!
|
||||||
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include <juce_graphics/juce_graphics.cpp>
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
/*
|
||||||
|
|
||||||
|
IMPORTANT! This file is auto-generated each time you save your
|
||||||
|
project - if you alter its contents, your changes may be overwritten!
|
||||||
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include <juce_graphics/juce_graphics.mm>
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
/*
|
||||||
|
|
||||||
|
IMPORTANT! This file is auto-generated each time you save your
|
||||||
|
project - if you alter its contents, your changes may be overwritten!
|
||||||
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include <juce_graphics/juce_graphics_Harfbuzz.cpp>
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
/*
|
||||||
|
|
||||||
|
IMPORTANT! This file is auto-generated each time you save your
|
||||||
|
project - if you alter its contents, your changes may be overwritten!
|
||||||
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include <juce_graphics/juce_graphics_Sheenbidi.c>
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
/*
|
||||||
|
|
||||||
|
IMPORTANT! This file is auto-generated each time you save your
|
||||||
|
project - if you alter its contents, your changes may be overwritten!
|
||||||
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include <juce_gui_basics/juce_gui_basics.cpp>
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
/*
|
||||||
|
|
||||||
|
IMPORTANT! This file is auto-generated each time you save your
|
||||||
|
project - if you alter its contents, your changes may be overwritten!
|
||||||
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include <juce_gui_basics/juce_gui_basics.mm>
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
/*
|
||||||
|
|
||||||
|
IMPORTANT! This file is auto-generated each time you save your
|
||||||
|
project - if you alter its contents, your changes may be overwritten!
|
||||||
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include <juce_gui_extra/juce_gui_extra.cpp>
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
/*
|
||||||
|
|
||||||
|
IMPORTANT! This file is auto-generated each time you save your
|
||||||
|
project - if you alter its contents, your changes may be overwritten!
|
||||||
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include <juce_gui_extra/juce_gui_extra.mm>
|
||||||
96
Examples/WavetableSynthTutorial/Source/Main.cpp
Normal file
96
Examples/WavetableSynthTutorial/Source/Main.cpp
Normal file
@@ -0,0 +1,96 @@
|
|||||||
|
/*
|
||||||
|
==============================================================================
|
||||||
|
|
||||||
|
This file contains the startup code for a PIP.
|
||||||
|
|
||||||
|
==============================================================================
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include <JuceHeader.h>
|
||||||
|
#include "WavetableSynthTutorial_01.h"
|
||||||
|
|
||||||
|
class Application : public juce::JUCEApplication
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
//==============================================================================
|
||||||
|
Application() = default;
|
||||||
|
|
||||||
|
const juce::String getApplicationName() override { return "WavetableSynthTutorial"; }
|
||||||
|
const juce::String getApplicationVersion() override { return "1.0.0"; }
|
||||||
|
|
||||||
|
void initialise (const juce::String&) override
|
||||||
|
{
|
||||||
|
mainWindow.reset (new MainWindow ("WavetableSynthTutorial", std::make_unique<MainContentComponent>(), *this));
|
||||||
|
}
|
||||||
|
|
||||||
|
void shutdown() override { mainWindow = nullptr; }
|
||||||
|
|
||||||
|
private:
|
||||||
|
class MainWindow : public juce::DocumentWindow
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
MainWindow (const juce::String& name, std::unique_ptr<juce::Component> c, JUCEApplication& a)
|
||||||
|
: DocumentWindow (name, juce::Desktop::getInstance().getDefaultLookAndFeel()
|
||||||
|
.findColour (ResizableWindow::backgroundColourId),
|
||||||
|
juce::DocumentWindow::allButtons),
|
||||||
|
app (a)
|
||||||
|
{
|
||||||
|
setUsingNativeTitleBar (true);
|
||||||
|
|
||||||
|
#if JUCE_ANDROID || JUCE_IOS
|
||||||
|
setContentOwned (new SafeAreaComponent { std::move (c) }, true);
|
||||||
|
setFullScreen (true);
|
||||||
|
#else
|
||||||
|
setContentOwned (c.release(), true);
|
||||||
|
setResizable (true, false);
|
||||||
|
setResizeLimits (300, 250, 10000, 10000);
|
||||||
|
centreWithSize (getWidth(), getHeight());
|
||||||
|
#endif
|
||||||
|
|
||||||
|
setVisible (true);
|
||||||
|
}
|
||||||
|
|
||||||
|
void closeButtonPressed() override
|
||||||
|
{
|
||||||
|
app.systemRequestedQuit();
|
||||||
|
}
|
||||||
|
|
||||||
|
#if JUCE_ANDROID || JUCE_IOS
|
||||||
|
class SafeAreaComponent : public juce::Component
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
explicit SafeAreaComponent (std::unique_ptr<Component> c)
|
||||||
|
: content (std::move (c))
|
||||||
|
{
|
||||||
|
addAndMakeVisible (*content);
|
||||||
|
}
|
||||||
|
|
||||||
|
void resized() override
|
||||||
|
{
|
||||||
|
if (const auto* d = Desktop::getInstance().getDisplays().getDisplayForRect (getLocalBounds()))
|
||||||
|
content->setBounds (d->safeAreaInsets.subtractedFrom (getLocalBounds()));
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
std::unique_ptr<Component> content;
|
||||||
|
};
|
||||||
|
|
||||||
|
void parentSizeChanged() override
|
||||||
|
{
|
||||||
|
if (auto* c = getContentComponent())
|
||||||
|
c->resized();
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
private:
|
||||||
|
JUCEApplication& app;
|
||||||
|
|
||||||
|
//==============================================================================
|
||||||
|
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (MainWindow)
|
||||||
|
};
|
||||||
|
|
||||||
|
std::unique_ptr<MainWindow> mainWindow;
|
||||||
|
};
|
||||||
|
|
||||||
|
//==============================================================================
|
||||||
|
START_JUCE_APPLICATION (Application)
|
||||||
@@ -0,0 +1,163 @@
|
|||||||
|
/*
|
||||||
|
==============================================================================
|
||||||
|
|
||||||
|
This file is part of the JUCE tutorials.
|
||||||
|
Copyright (c) 2020 - Raw Material Software Limited
|
||||||
|
|
||||||
|
The code included in this file is provided under the terms of the ISC license
|
||||||
|
http://www.isc.org/downloads/software-support-policy/isc-license. Permission
|
||||||
|
To use, copy, modify, and/or distribute this software for any purpose with or
|
||||||
|
without fee is hereby granted provided that the above copyright notice and
|
||||||
|
this permission notice appear in all copies.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES,
|
||||||
|
WHETHER EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR
|
||||||
|
PURPOSE, ARE DISCLAIMED.
|
||||||
|
|
||||||
|
==============================================================================
|
||||||
|
*/
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
The block below describes the properties of this PIP. A PIP is a short snippet
|
||||||
|
of code that can be read by the Projucer and used to generate a JUCE project.
|
||||||
|
|
||||||
|
BEGIN_JUCE_PIP_METADATA
|
||||||
|
|
||||||
|
name: WavetableSynthTutorial
|
||||||
|
version: 1.0.0
|
||||||
|
vendor: JUCE
|
||||||
|
website: http://juce.com
|
||||||
|
description: Wavetable synthesiser.
|
||||||
|
|
||||||
|
dependencies: juce_audio_basics, juce_audio_devices, juce_audio_formats,
|
||||||
|
juce_audio_processors, juce_audio_utils, juce_core,
|
||||||
|
juce_data_structures, juce_events, juce_graphics,
|
||||||
|
juce_gui_basics, juce_gui_extra
|
||||||
|
exporters: xcode_mac, vs2019, linux_make
|
||||||
|
|
||||||
|
type: Component
|
||||||
|
mainClass: MainContentComponent
|
||||||
|
|
||||||
|
useLocalCopy: 1
|
||||||
|
|
||||||
|
END_JUCE_PIP_METADATA
|
||||||
|
|
||||||
|
*******************************************************************************/
|
||||||
|
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
//==============================================================================
|
||||||
|
class SineOscillator
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
SineOscillator() {}
|
||||||
|
|
||||||
|
void setFrequency (float frequency, float sampleRate)
|
||||||
|
{
|
||||||
|
auto cyclesPerSample = frequency / sampleRate;
|
||||||
|
angleDelta = cyclesPerSample * juce::MathConstants<float>::twoPi;
|
||||||
|
}
|
||||||
|
|
||||||
|
forcedinline void updateAngle() noexcept
|
||||||
|
{
|
||||||
|
currentAngle += angleDelta;
|
||||||
|
if (currentAngle >= juce::MathConstants<float>::twoPi)
|
||||||
|
currentAngle -= juce::MathConstants<float>::twoPi;
|
||||||
|
}
|
||||||
|
|
||||||
|
forcedinline float getNextSample() noexcept
|
||||||
|
{
|
||||||
|
auto currentSample = std::sin (currentAngle);
|
||||||
|
updateAngle();
|
||||||
|
return currentSample;
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
float currentAngle = 0.0f, angleDelta = 0.0f;
|
||||||
|
};
|
||||||
|
|
||||||
|
//==============================================================================
|
||||||
|
class MainContentComponent : public juce::AudioAppComponent,
|
||||||
|
public juce::Timer
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
MainContentComponent()
|
||||||
|
{
|
||||||
|
cpuUsageLabel.setText ("CPU Usage", juce::dontSendNotification);
|
||||||
|
cpuUsageText.setJustificationType (juce::Justification::right);
|
||||||
|
addAndMakeVisible (cpuUsageLabel);
|
||||||
|
addAndMakeVisible (cpuUsageText);
|
||||||
|
|
||||||
|
setSize (400, 200);
|
||||||
|
setAudioChannels (0, 2); // no inputs, two outputs
|
||||||
|
startTimer (50);
|
||||||
|
}
|
||||||
|
|
||||||
|
~MainContentComponent() override
|
||||||
|
{
|
||||||
|
shutdownAudio();
|
||||||
|
}
|
||||||
|
|
||||||
|
void resized() override
|
||||||
|
{
|
||||||
|
cpuUsageLabel.setBounds (10, 10, getWidth() - 20, 20);
|
||||||
|
cpuUsageText .setBounds (10, 10, getWidth() - 20, 20);
|
||||||
|
}
|
||||||
|
|
||||||
|
void timerCallback() override
|
||||||
|
{
|
||||||
|
auto cpu = deviceManager.getCpuUsage() * 100;
|
||||||
|
cpuUsageText.setText (juce::String (cpu, 6) + " %", juce::dontSendNotification);
|
||||||
|
}
|
||||||
|
|
||||||
|
void prepareToPlay (int, double sampleRate) override
|
||||||
|
{
|
||||||
|
auto numberOfOscillators = 200; // [1]
|
||||||
|
|
||||||
|
for (auto i = 0; i < numberOfOscillators; ++i)
|
||||||
|
{
|
||||||
|
auto* oscillator = new SineOscillator(); // [2]
|
||||||
|
|
||||||
|
auto midiNote = juce::Random::getSystemRandom().nextDouble() * 36.0 + 48.0; // [3]
|
||||||
|
auto frequency = 440.0 * pow (2.0, (midiNote - 69.0) / 12.0); // [4]
|
||||||
|
|
||||||
|
oscillator->setFrequency ((float) frequency, (float) sampleRate); // [5]
|
||||||
|
oscillators.add (oscillator);
|
||||||
|
}
|
||||||
|
|
||||||
|
level = 0.25f / (float) numberOfOscillators; // [6]
|
||||||
|
}
|
||||||
|
|
||||||
|
void releaseResources() override {}
|
||||||
|
|
||||||
|
void getNextAudioBlock (const juce::AudioSourceChannelInfo& bufferToFill) override
|
||||||
|
{
|
||||||
|
auto* leftBuffer = bufferToFill.buffer->getWritePointer (0, bufferToFill.startSample); // [7]
|
||||||
|
auto* rightBuffer = bufferToFill.buffer->getWritePointer (1, bufferToFill.startSample);
|
||||||
|
|
||||||
|
bufferToFill.clearActiveBufferRegion();
|
||||||
|
|
||||||
|
for (auto oscillatorIndex = 0; oscillatorIndex < oscillators.size(); ++oscillatorIndex)
|
||||||
|
{
|
||||||
|
auto* oscillator = oscillators.getUnchecked (oscillatorIndex); // [8]
|
||||||
|
|
||||||
|
for (auto sample = 0; sample < bufferToFill.numSamples; ++sample)
|
||||||
|
{
|
||||||
|
auto levelSample = oscillator->getNextSample() * level; // [9]
|
||||||
|
|
||||||
|
leftBuffer[sample] += levelSample; // [10]
|
||||||
|
rightBuffer[sample] += levelSample;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
juce::Label cpuUsageLabel;
|
||||||
|
juce::Label cpuUsageText;
|
||||||
|
|
||||||
|
float level = 0.0f;
|
||||||
|
juce::OwnedArray<SineOscillator> oscillators;
|
||||||
|
|
||||||
|
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (MainContentComponent)
|
||||||
|
};
|
||||||
@@ -0,0 +1,196 @@
|
|||||||
|
/*
|
||||||
|
==============================================================================
|
||||||
|
|
||||||
|
This file is part of the JUCE tutorials.
|
||||||
|
Copyright (c) 2020 - Raw Material Software Limited
|
||||||
|
|
||||||
|
The code included in this file is provided under the terms of the ISC license
|
||||||
|
http://www.isc.org/downloads/software-support-policy/isc-license. Permission
|
||||||
|
To use, copy, modify, and/or distribute this software for any purpose with or
|
||||||
|
without fee is hereby granted provided that the above copyright notice and
|
||||||
|
this permission notice appear in all copies.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES,
|
||||||
|
WHETHER EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR
|
||||||
|
PURPOSE, ARE DISCLAIMED.
|
||||||
|
|
||||||
|
==============================================================================
|
||||||
|
*/
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
The block below describes the properties of this PIP. A PIP is a short snippet
|
||||||
|
of code that can be read by the Projucer and used to generate a JUCE project.
|
||||||
|
|
||||||
|
BEGIN_JUCE_PIP_METADATA
|
||||||
|
|
||||||
|
name: WavetableSynthTutorial
|
||||||
|
version: 2.0.0
|
||||||
|
vendor: JUCE
|
||||||
|
website: http://juce.com
|
||||||
|
description: Wavetable synthesiser.
|
||||||
|
|
||||||
|
dependencies: juce_audio_basics, juce_audio_devices, juce_audio_formats,
|
||||||
|
juce_audio_processors, juce_audio_utils, juce_core,
|
||||||
|
juce_data_structures, juce_events, juce_graphics,
|
||||||
|
juce_gui_basics, juce_gui_extra
|
||||||
|
exporters: xcode_mac, vs2019, linux_make
|
||||||
|
|
||||||
|
type: Component
|
||||||
|
mainClass: MainContentComponent
|
||||||
|
|
||||||
|
useLocalCopy: 1
|
||||||
|
|
||||||
|
END_JUCE_PIP_METADATA
|
||||||
|
|
||||||
|
*******************************************************************************/
|
||||||
|
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
//==============================================================================
|
||||||
|
class WavetableOscillator
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
WavetableOscillator (const juce::AudioSampleBuffer& wavetableToUse)
|
||||||
|
: wavetable (wavetableToUse)
|
||||||
|
{
|
||||||
|
jassert (wavetable.getNumChannels() == 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
void setFrequency (float frequency, float sampleRate)
|
||||||
|
{
|
||||||
|
auto tableSizeOverSampleRate = (float) wavetable.getNumSamples() / sampleRate;
|
||||||
|
tableDelta = frequency * tableSizeOverSampleRate;
|
||||||
|
}
|
||||||
|
|
||||||
|
forcedinline float getNextSample() noexcept
|
||||||
|
{
|
||||||
|
auto tableSize = (unsigned int) wavetable.getNumSamples();
|
||||||
|
|
||||||
|
auto index0 = (unsigned int) currentIndex; // [6]
|
||||||
|
auto index1 = index0 == (tableSize - 1) ? (unsigned int) 0 : index0 + 1;
|
||||||
|
|
||||||
|
auto frac = currentIndex - (float) index0; // [7]
|
||||||
|
|
||||||
|
auto* table = wavetable.getReadPointer (0); // [8]
|
||||||
|
auto value0 = table[index0];
|
||||||
|
auto value1 = table[index1];
|
||||||
|
|
||||||
|
auto currentSample = value0 + frac * (value1 - value0); // [9]
|
||||||
|
|
||||||
|
if ((currentIndex += tableDelta) > (float) tableSize) // [10]
|
||||||
|
currentIndex -= (float) tableSize;
|
||||||
|
|
||||||
|
return currentSample;
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
const juce::AudioSampleBuffer& wavetable;
|
||||||
|
float currentIndex = 0.0f, tableDelta = 0.0f;
|
||||||
|
};
|
||||||
|
|
||||||
|
//==============================================================================
|
||||||
|
class MainContentComponent : public juce::AudioAppComponent,
|
||||||
|
public juce::Timer
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
MainContentComponent()
|
||||||
|
{
|
||||||
|
cpuUsageLabel.setText ("CPU Usage", juce::dontSendNotification);
|
||||||
|
cpuUsageText.setJustificationType (juce::Justification::right);
|
||||||
|
addAndMakeVisible (cpuUsageLabel);
|
||||||
|
addAndMakeVisible (cpuUsageText);
|
||||||
|
|
||||||
|
createWavetable();
|
||||||
|
|
||||||
|
setSize (400, 200);
|
||||||
|
setAudioChannels (0, 2); // no inputs, two outputs
|
||||||
|
startTimer (50);
|
||||||
|
}
|
||||||
|
|
||||||
|
~MainContentComponent() override
|
||||||
|
{
|
||||||
|
shutdownAudio();
|
||||||
|
}
|
||||||
|
|
||||||
|
void resized() override
|
||||||
|
{
|
||||||
|
cpuUsageLabel.setBounds (10, 10, getWidth() - 20, 20);
|
||||||
|
cpuUsageText .setBounds (10, 10, getWidth() - 20, 20);
|
||||||
|
}
|
||||||
|
|
||||||
|
void timerCallback() override
|
||||||
|
{
|
||||||
|
auto cpu = deviceManager.getCpuUsage() * 100;
|
||||||
|
cpuUsageText.setText (juce::String (cpu, 6) + " %", juce::dontSendNotification);
|
||||||
|
}
|
||||||
|
|
||||||
|
void createWavetable()
|
||||||
|
{
|
||||||
|
sineTable.setSize (1, (int) tableSize);
|
||||||
|
auto* samples = sineTable.getWritePointer (0); // [3]
|
||||||
|
|
||||||
|
auto angleDelta = juce::MathConstants<double>::twoPi / (double) (tableSize - 1); // [4]
|
||||||
|
auto currentAngle = 0.0;
|
||||||
|
|
||||||
|
for (unsigned int i = 0; i < tableSize; ++i)
|
||||||
|
{
|
||||||
|
auto sample = std::sin (currentAngle); // [5]
|
||||||
|
samples[i] = (float) sample;
|
||||||
|
currentAngle += angleDelta;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void prepareToPlay (int, double sampleRate) override
|
||||||
|
{
|
||||||
|
auto numberOfOscillators = 200;
|
||||||
|
|
||||||
|
for (auto i = 0; i < numberOfOscillators; ++i)
|
||||||
|
{
|
||||||
|
auto* oscillator = new WavetableOscillator (sineTable);
|
||||||
|
|
||||||
|
auto midiNote = juce::Random::getSystemRandom().nextDouble() * 36.0 + 48.0;
|
||||||
|
auto frequency = 440.0 * pow (2.0, (midiNote - 69.0) / 12.0);
|
||||||
|
|
||||||
|
oscillator->setFrequency ((float) frequency, (float) sampleRate);
|
||||||
|
oscillators.add (oscillator);
|
||||||
|
}
|
||||||
|
|
||||||
|
level = 0.25f / (float) numberOfOscillators;
|
||||||
|
}
|
||||||
|
|
||||||
|
void releaseResources() override {}
|
||||||
|
|
||||||
|
void getNextAudioBlock (const juce::AudioSourceChannelInfo& bufferToFill) override
|
||||||
|
{
|
||||||
|
auto* leftBuffer = bufferToFill.buffer->getWritePointer (0, bufferToFill.startSample);
|
||||||
|
auto* rightBuffer = bufferToFill.buffer->getWritePointer (1, bufferToFill.startSample);
|
||||||
|
|
||||||
|
bufferToFill.clearActiveBufferRegion();
|
||||||
|
|
||||||
|
for (auto oscillatorIndex = 0; oscillatorIndex < oscillators.size(); ++oscillatorIndex)
|
||||||
|
{
|
||||||
|
auto* oscillator = oscillators.getUnchecked (oscillatorIndex);
|
||||||
|
|
||||||
|
for (auto sample = 0; sample < bufferToFill.numSamples; ++sample)
|
||||||
|
{
|
||||||
|
auto levelSample = oscillator->getNextSample() * level;
|
||||||
|
|
||||||
|
leftBuffer[sample] += levelSample;
|
||||||
|
rightBuffer[sample] += levelSample;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
juce::Label cpuUsageLabel;
|
||||||
|
juce::Label cpuUsageText;
|
||||||
|
|
||||||
|
const unsigned int tableSize = 1 << 7; // [2]
|
||||||
|
float level = 0.0f;
|
||||||
|
|
||||||
|
juce::AudioSampleBuffer sineTable; // [1]
|
||||||
|
juce::OwnedArray<WavetableOscillator> oscillators;
|
||||||
|
|
||||||
|
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (MainContentComponent)
|
||||||
|
};
|
||||||
@@ -0,0 +1,198 @@
|
|||||||
|
/*
|
||||||
|
==============================================================================
|
||||||
|
|
||||||
|
This file is part of the JUCE tutorials.
|
||||||
|
Copyright (c) 2020 - Raw Material Software Limited
|
||||||
|
|
||||||
|
The code included in this file is provided under the terms of the ISC license
|
||||||
|
http://www.isc.org/downloads/software-support-policy/isc-license. Permission
|
||||||
|
To use, copy, modify, and/or distribute this software for any purpose with or
|
||||||
|
without fee is hereby granted provided that the above copyright notice and
|
||||||
|
this permission notice appear in all copies.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES,
|
||||||
|
WHETHER EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR
|
||||||
|
PURPOSE, ARE DISCLAIMED.
|
||||||
|
|
||||||
|
==============================================================================
|
||||||
|
*/
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
The block below describes the properties of this PIP. A PIP is a short snippet
|
||||||
|
of code that can be read by the Projucer and used to generate a JUCE project.
|
||||||
|
|
||||||
|
BEGIN_JUCE_PIP_METADATA
|
||||||
|
|
||||||
|
name: WavetableSynthTutorial
|
||||||
|
version: 3.0.0
|
||||||
|
vendor: JUCE
|
||||||
|
website: http://juce.com
|
||||||
|
description: Wavetable synthesiser.
|
||||||
|
|
||||||
|
dependencies: juce_audio_basics, juce_audio_devices, juce_audio_formats,
|
||||||
|
juce_audio_processors, juce_audio_utils, juce_core,
|
||||||
|
juce_data_structures, juce_events, juce_graphics,
|
||||||
|
juce_gui_basics, juce_gui_extra
|
||||||
|
exporters: xcode_mac, vs2019, linux_make
|
||||||
|
|
||||||
|
type: Component
|
||||||
|
mainClass: MainContentComponent
|
||||||
|
|
||||||
|
useLocalCopy: 1
|
||||||
|
|
||||||
|
END_JUCE_PIP_METADATA
|
||||||
|
|
||||||
|
*******************************************************************************/
|
||||||
|
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
//==============================================================================
|
||||||
|
class WavetableOscillator
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
WavetableOscillator (const juce::AudioSampleBuffer& wavetableToUse)
|
||||||
|
: wavetable (wavetableToUse),
|
||||||
|
tableSize (wavetable.getNumSamples() - 1)
|
||||||
|
{
|
||||||
|
jassert (wavetable.getNumChannels() == 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
void setFrequency (float frequency, float sampleRate)
|
||||||
|
{
|
||||||
|
auto tableSizeOverSampleRate = (float) tableSize / sampleRate;
|
||||||
|
tableDelta = frequency * tableSizeOverSampleRate;
|
||||||
|
}
|
||||||
|
|
||||||
|
forcedinline float getNextSample() noexcept
|
||||||
|
{
|
||||||
|
auto index0 = (unsigned int) currentIndex;
|
||||||
|
auto index1 = index0 + 1;
|
||||||
|
|
||||||
|
auto frac = currentIndex - (float) index0;
|
||||||
|
|
||||||
|
auto* table = wavetable.getReadPointer (0);
|
||||||
|
auto value0 = table[index0];
|
||||||
|
auto value1 = table[index1];
|
||||||
|
|
||||||
|
auto currentSample = value0 + frac * (value1 - value0);
|
||||||
|
|
||||||
|
if ((currentIndex += tableDelta) > (float) tableSize)
|
||||||
|
currentIndex -= (float) tableSize;
|
||||||
|
|
||||||
|
return currentSample;
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
const juce::AudioSampleBuffer& wavetable;
|
||||||
|
const int tableSize;
|
||||||
|
float currentIndex = 0.0f, tableDelta = 0.0f;
|
||||||
|
};
|
||||||
|
|
||||||
|
//==============================================================================
|
||||||
|
class MainContentComponent : public juce::AudioAppComponent,
|
||||||
|
public juce::Timer
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
MainContentComponent()
|
||||||
|
{
|
||||||
|
cpuUsageLabel.setText ("CPU Usage", juce::dontSendNotification);
|
||||||
|
cpuUsageText.setJustificationType (juce::Justification::right);
|
||||||
|
addAndMakeVisible (cpuUsageLabel);
|
||||||
|
addAndMakeVisible (cpuUsageText);
|
||||||
|
|
||||||
|
createWavetable();
|
||||||
|
|
||||||
|
setSize (400, 200);
|
||||||
|
setAudioChannels (0, 2); // no inputs, two outputs
|
||||||
|
startTimer (50);
|
||||||
|
}
|
||||||
|
|
||||||
|
~MainContentComponent() override
|
||||||
|
{
|
||||||
|
shutdownAudio();
|
||||||
|
}
|
||||||
|
|
||||||
|
void resized() override
|
||||||
|
{
|
||||||
|
cpuUsageLabel.setBounds (10, 10, getWidth() - 20, 20);
|
||||||
|
cpuUsageText .setBounds (10, 10, getWidth() - 20, 20);
|
||||||
|
}
|
||||||
|
|
||||||
|
void timerCallback() override
|
||||||
|
{
|
||||||
|
auto cpu = deviceManager.getCpuUsage() * 100;
|
||||||
|
cpuUsageText.setText (juce::String (cpu, 6) + " %", juce::dontSendNotification);
|
||||||
|
}
|
||||||
|
|
||||||
|
void createWavetable()
|
||||||
|
{
|
||||||
|
sineTable.setSize (1, (int) tableSize + 1);
|
||||||
|
auto* samples = sineTable.getWritePointer (0);
|
||||||
|
|
||||||
|
auto angleDelta = juce::MathConstants<double>::twoPi / (double) (tableSize - 1);
|
||||||
|
auto currentAngle = 0.0;
|
||||||
|
|
||||||
|
for (unsigned int i = 0; i < tableSize; ++i)
|
||||||
|
{
|
||||||
|
auto sample = std::sin (currentAngle);
|
||||||
|
samples[i] = (float) sample;
|
||||||
|
currentAngle += angleDelta;
|
||||||
|
}
|
||||||
|
|
||||||
|
samples[tableSize] = samples[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
void prepareToPlay (int, double sampleRate) override
|
||||||
|
{
|
||||||
|
auto numberOfOscillators = 200;
|
||||||
|
|
||||||
|
for (auto i = 0; i < numberOfOscillators; ++i)
|
||||||
|
{
|
||||||
|
auto* oscillator = new WavetableOscillator (sineTable);
|
||||||
|
|
||||||
|
auto midiNote = juce::Random::getSystemRandom().nextDouble() * 36.0 + 48.0;
|
||||||
|
auto frequency = 440.0 * pow (2.0, (midiNote - 69.0) / 12.0);
|
||||||
|
|
||||||
|
oscillator->setFrequency ((float) frequency, (float) sampleRate);
|
||||||
|
oscillators.add (oscillator);
|
||||||
|
}
|
||||||
|
|
||||||
|
level = 0.25f / (float) numberOfOscillators;
|
||||||
|
}
|
||||||
|
|
||||||
|
void releaseResources() override {}
|
||||||
|
|
||||||
|
void getNextAudioBlock (const juce::AudioSourceChannelInfo& bufferToFill) override
|
||||||
|
{
|
||||||
|
auto* leftBuffer = bufferToFill.buffer->getWritePointer (0, bufferToFill.startSample);
|
||||||
|
auto* rightBuffer = bufferToFill.buffer->getWritePointer (1, bufferToFill.startSample);
|
||||||
|
|
||||||
|
bufferToFill.clearActiveBufferRegion();
|
||||||
|
|
||||||
|
for (auto oscillatorIndex = 0; oscillatorIndex < oscillators.size(); ++oscillatorIndex)
|
||||||
|
{
|
||||||
|
auto* oscillator = oscillators.getUnchecked (oscillatorIndex);
|
||||||
|
|
||||||
|
for (auto sample = 0; sample < bufferToFill.numSamples; ++sample)
|
||||||
|
{
|
||||||
|
auto levelSample = oscillator->getNextSample() * level;
|
||||||
|
|
||||||
|
leftBuffer[sample] += levelSample;
|
||||||
|
rightBuffer[sample] += levelSample;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
juce::Label cpuUsageLabel;
|
||||||
|
juce::Label cpuUsageText;
|
||||||
|
|
||||||
|
const unsigned int tableSize = 1 << 7;
|
||||||
|
float level = 0.0f;
|
||||||
|
|
||||||
|
juce::AudioSampleBuffer sineTable;
|
||||||
|
juce::OwnedArray<WavetableOscillator> oscillators;
|
||||||
|
|
||||||
|
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (MainContentComponent)
|
||||||
|
};
|
||||||
@@ -0,0 +1,208 @@
|
|||||||
|
/*
|
||||||
|
==============================================================================
|
||||||
|
|
||||||
|
This file is part of the JUCE tutorials.
|
||||||
|
Copyright (c) 2020 - Raw Material Software Limited
|
||||||
|
|
||||||
|
The code included in this file is provided under the terms of the ISC license
|
||||||
|
http://www.isc.org/downloads/software-support-policy/isc-license. Permission
|
||||||
|
To use, copy, modify, and/or distribute this software for any purpose with or
|
||||||
|
without fee is hereby granted provided that the above copyright notice and
|
||||||
|
this permission notice appear in all copies.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES,
|
||||||
|
WHETHER EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR
|
||||||
|
PURPOSE, ARE DISCLAIMED.
|
||||||
|
|
||||||
|
==============================================================================
|
||||||
|
*/
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
The block below describes the properties of this PIP. A PIP is a short snippet
|
||||||
|
of code that can be read by the Projucer and used to generate a JUCE project.
|
||||||
|
|
||||||
|
BEGIN_JUCE_PIP_METADATA
|
||||||
|
|
||||||
|
name: WavetableSynthTutorial
|
||||||
|
version: 4.0.0
|
||||||
|
vendor: JUCE
|
||||||
|
website: http://juce.com
|
||||||
|
description: Wavetable synthesiser.
|
||||||
|
|
||||||
|
dependencies: juce_audio_basics, juce_audio_devices, juce_audio_formats,
|
||||||
|
juce_audio_processors, juce_audio_utils, juce_core,
|
||||||
|
juce_data_structures, juce_events, juce_graphics,
|
||||||
|
juce_gui_basics, juce_gui_extra
|
||||||
|
exporters: xcode_mac, vs2019, linux_make
|
||||||
|
|
||||||
|
type: Component
|
||||||
|
mainClass: MainContentComponent
|
||||||
|
|
||||||
|
useLocalCopy: 1
|
||||||
|
|
||||||
|
END_JUCE_PIP_METADATA
|
||||||
|
|
||||||
|
*******************************************************************************/
|
||||||
|
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
//==============================================================================
|
||||||
|
class WavetableOscillator
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
WavetableOscillator (const juce::AudioSampleBuffer& wavetableToUse)
|
||||||
|
: wavetable (wavetableToUse),
|
||||||
|
tableSize (wavetable.getNumSamples() - 1)
|
||||||
|
{
|
||||||
|
jassert (wavetable.getNumChannels() == 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
void setFrequency (float frequency, float sampleRate)
|
||||||
|
{
|
||||||
|
auto tableSizeOverSampleRate = (float) tableSize / sampleRate;
|
||||||
|
tableDelta = frequency * tableSizeOverSampleRate;
|
||||||
|
}
|
||||||
|
|
||||||
|
forcedinline float getNextSample() noexcept
|
||||||
|
{
|
||||||
|
auto index0 = (unsigned int) currentIndex;
|
||||||
|
auto index1 = index0 + 1;
|
||||||
|
|
||||||
|
auto frac = currentIndex - (float) index0;
|
||||||
|
|
||||||
|
auto* table = wavetable.getReadPointer (0);
|
||||||
|
auto value0 = table[index0];
|
||||||
|
auto value1 = table[index1];
|
||||||
|
|
||||||
|
auto currentSample = value0 + frac * (value1 - value0);
|
||||||
|
|
||||||
|
if ((currentIndex += tableDelta) > (float) tableSize)
|
||||||
|
currentIndex -= (float) tableSize;
|
||||||
|
|
||||||
|
return currentSample;
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
const juce::AudioSampleBuffer& wavetable;
|
||||||
|
const int tableSize;
|
||||||
|
float currentIndex = 0.0f, tableDelta = 0.0f;
|
||||||
|
};
|
||||||
|
|
||||||
|
//==============================================================================
|
||||||
|
class MainContentComponent : public juce::AudioAppComponent,
|
||||||
|
public juce::Timer
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
MainContentComponent()
|
||||||
|
{
|
||||||
|
cpuUsageLabel.setText ("CPU Usage", juce::dontSendNotification);
|
||||||
|
cpuUsageText.setJustificationType (juce::Justification::right);
|
||||||
|
addAndMakeVisible (cpuUsageLabel);
|
||||||
|
addAndMakeVisible (cpuUsageText);
|
||||||
|
|
||||||
|
createWavetable();
|
||||||
|
|
||||||
|
setSize (400, 200);
|
||||||
|
setAudioChannels (0, 2); // no inputs, two outputs
|
||||||
|
startTimer (50);
|
||||||
|
}
|
||||||
|
|
||||||
|
~MainContentComponent() override
|
||||||
|
{
|
||||||
|
shutdownAudio();
|
||||||
|
}
|
||||||
|
|
||||||
|
void resized() override
|
||||||
|
{
|
||||||
|
cpuUsageLabel.setBounds (10, 10, getWidth() - 20, 20);
|
||||||
|
cpuUsageText .setBounds (10, 10, getWidth() - 20, 20);
|
||||||
|
}
|
||||||
|
|
||||||
|
void timerCallback() override
|
||||||
|
{
|
||||||
|
auto cpu = deviceManager.getCpuUsage() * 100;
|
||||||
|
cpuUsageText.setText (juce::String (cpu, 6) + " %", juce::dontSendNotification);
|
||||||
|
}
|
||||||
|
|
||||||
|
void createWavetable()
|
||||||
|
{
|
||||||
|
sineTable.setSize (1, (int) tableSize + 1);
|
||||||
|
sineTable.clear();
|
||||||
|
|
||||||
|
auto* samples = sineTable.getWritePointer (0);
|
||||||
|
|
||||||
|
int harmonics[] = { 1, 3, 5, 6, 7, 9, 13, 15 };
|
||||||
|
float harmonicWeights[] = { 0.5f, 0.1f, 0.05f, 0.125f, 0.09f, 0.005f, 0.002f, 0.001f }; // [1]
|
||||||
|
|
||||||
|
jassert (juce::numElementsInArray (harmonics) == juce::numElementsInArray (harmonicWeights));
|
||||||
|
|
||||||
|
for (auto harmonic = 0; harmonic < juce::numElementsInArray (harmonics); ++harmonic)
|
||||||
|
{
|
||||||
|
auto angleDelta = juce::MathConstants<double>::twoPi / (double) (tableSize - 1) * harmonics[harmonic]; // [2]
|
||||||
|
auto currentAngle = 0.0;
|
||||||
|
|
||||||
|
for (unsigned int i = 0; i < tableSize; ++i)
|
||||||
|
{
|
||||||
|
auto sample = std::sin (currentAngle);
|
||||||
|
samples[i] += (float) sample * harmonicWeights[harmonic]; // [3]
|
||||||
|
currentAngle += angleDelta;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
samples[tableSize] = samples[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
void prepareToPlay (int, double sampleRate) override
|
||||||
|
{
|
||||||
|
auto numberOfOscillators = 10;
|
||||||
|
|
||||||
|
for (auto i = 0; i < numberOfOscillators; ++i)
|
||||||
|
{
|
||||||
|
auto* oscillator = new WavetableOscillator (sineTable);
|
||||||
|
|
||||||
|
auto midiNote = juce::Random::getSystemRandom().nextDouble() * 36.0 + 48.0;
|
||||||
|
auto frequency = 440.0 * pow (2.0, (midiNote - 69.0) / 12.0);
|
||||||
|
|
||||||
|
oscillator->setFrequency ((float) frequency, (float) sampleRate);
|
||||||
|
oscillators.add (oscillator);
|
||||||
|
}
|
||||||
|
|
||||||
|
level = 0.25f / (float) numberOfOscillators;
|
||||||
|
}
|
||||||
|
|
||||||
|
void releaseResources() override {}
|
||||||
|
|
||||||
|
void getNextAudioBlock (const juce::AudioSourceChannelInfo& bufferToFill) override
|
||||||
|
{
|
||||||
|
auto* leftBuffer = bufferToFill.buffer->getWritePointer (0, bufferToFill.startSample);
|
||||||
|
auto* rightBuffer = bufferToFill.buffer->getWritePointer (1, bufferToFill.startSample);
|
||||||
|
|
||||||
|
bufferToFill.clearActiveBufferRegion();
|
||||||
|
|
||||||
|
for (auto oscillatorIndex = 0; oscillatorIndex < oscillators.size(); ++oscillatorIndex)
|
||||||
|
{
|
||||||
|
auto* oscillator = oscillators.getUnchecked (oscillatorIndex);
|
||||||
|
|
||||||
|
for (auto sample = 0; sample < bufferToFill.numSamples; ++sample)
|
||||||
|
{
|
||||||
|
auto levelSample = oscillator->getNextSample() * level;
|
||||||
|
|
||||||
|
leftBuffer[sample] += levelSample;
|
||||||
|
rightBuffer[sample] += levelSample;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
juce::Label cpuUsageLabel;
|
||||||
|
juce::Label cpuUsageText;
|
||||||
|
|
||||||
|
const unsigned int tableSize = 1 << 7;
|
||||||
|
float level = 0.0f;
|
||||||
|
|
||||||
|
juce::AudioSampleBuffer sineTable;
|
||||||
|
juce::OwnedArray<WavetableOscillator> oscillators;
|
||||||
|
|
||||||
|
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (MainContentComponent)
|
||||||
|
};
|
||||||
87
Examples/WavetableSynthTutorial/WavetableSynthTutorial.jucer
Normal file
87
Examples/WavetableSynthTutorial/WavetableSynthTutorial.jucer
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
|
||||||
|
<JUCERPROJECT name="WavetableSynthTutorial" companyName="JUCE" version="1.0.0"
|
||||||
|
userNotes="Wavetable synthesiser." companyWebsite="http://juce.com"
|
||||||
|
projectType="guiapp" useAppConfig="0" addUsingNamespaceToJuceHeader="1"
|
||||||
|
id="dBFoEo" jucerFormatVersion="1">
|
||||||
|
<MAINGROUP id="auHq7A" name="WavetableSynthTutorial">
|
||||||
|
<GROUP id="{45834796-7C04-D188-9F98-9AE7211B6CF5}" name="Source">
|
||||||
|
<FILE id="iPmvtR" name="Main.cpp" compile="1" resource="0" file="Source/Main.cpp"/>
|
||||||
|
<FILE id="EKiEzl" name="WavetableSynthTutorial_01.h" compile="0" resource="0"
|
||||||
|
file="Source/WavetableSynthTutorial_01.h"/>
|
||||||
|
</GROUP>
|
||||||
|
</MAINGROUP>
|
||||||
|
<MODULES>
|
||||||
|
<MODULE id="juce_audio_basics" showAllCode="1" useLocalCopy="0" useGlobalPath="1"/>
|
||||||
|
<MODULE id="juce_audio_devices" showAllCode="1" useLocalCopy="0" useGlobalPath="1"/>
|
||||||
|
<MODULE id="juce_audio_formats" showAllCode="1" useLocalCopy="0" useGlobalPath="1"/>
|
||||||
|
<MODULE id="juce_audio_processors" showAllCode="1" useLocalCopy="0" useGlobalPath="1"/>
|
||||||
|
<MODULE id="juce_audio_utils" showAllCode="1" useLocalCopy="0" useGlobalPath="1"/>
|
||||||
|
<MODULE id="juce_core" showAllCode="1" useLocalCopy="0" useGlobalPath="1"/>
|
||||||
|
<MODULE id="juce_data_structures" showAllCode="1" useLocalCopy="0" useGlobalPath="1"/>
|
||||||
|
<MODULE id="juce_events" showAllCode="1" useLocalCopy="0" useGlobalPath="1"/>
|
||||||
|
<MODULE id="juce_graphics" showAllCode="1" useLocalCopy="0" useGlobalPath="1"/>
|
||||||
|
<MODULE id="juce_gui_basics" showAllCode="1" useLocalCopy="0" useGlobalPath="1"/>
|
||||||
|
<MODULE id="juce_gui_extra" showAllCode="1" useLocalCopy="0" useGlobalPath="1"/>
|
||||||
|
</MODULES>
|
||||||
|
<EXPORTFORMATS>
|
||||||
|
<XCODE_MAC targetFolder="Builds/MacOSX">
|
||||||
|
<CONFIGURATIONS>
|
||||||
|
<CONFIGURATION name="Debug" isDebug="1" optimisation="1" targetName="WavetableSynthTutorial"/>
|
||||||
|
<CONFIGURATION name="Release" isDebug="0" optimisation="3" targetName="WavetableSynthTutorial"/>
|
||||||
|
</CONFIGURATIONS>
|
||||||
|
<MODULEPATHS>
|
||||||
|
<MODULEPATH id="juce_audio_basics" path=""/>
|
||||||
|
<MODULEPATH id="juce_audio_devices" path=""/>
|
||||||
|
<MODULEPATH id="juce_audio_formats" path=""/>
|
||||||
|
<MODULEPATH id="juce_audio_processors" path=""/>
|
||||||
|
<MODULEPATH id="juce_audio_utils" path=""/>
|
||||||
|
<MODULEPATH id="juce_core" path=""/>
|
||||||
|
<MODULEPATH id="juce_data_structures" path=""/>
|
||||||
|
<MODULEPATH id="juce_events" path=""/>
|
||||||
|
<MODULEPATH id="juce_graphics" path=""/>
|
||||||
|
<MODULEPATH id="juce_gui_basics" path=""/>
|
||||||
|
<MODULEPATH id="juce_gui_extra" path=""/>
|
||||||
|
</MODULEPATHS>
|
||||||
|
</XCODE_MAC>
|
||||||
|
<VS2019 targetFolder="Builds/VisualStudio2019">
|
||||||
|
<CONFIGURATIONS>
|
||||||
|
<CONFIGURATION name="Debug" isDebug="1" optimisation="1" targetName="WavetableSynthTutorial"/>
|
||||||
|
<CONFIGURATION name="Release" isDebug="0" optimisation="3" targetName="WavetableSynthTutorial"/>
|
||||||
|
</CONFIGURATIONS>
|
||||||
|
<MODULEPATHS>
|
||||||
|
<MODULEPATH id="juce_audio_basics" path=""/>
|
||||||
|
<MODULEPATH id="juce_audio_devices" path=""/>
|
||||||
|
<MODULEPATH id="juce_audio_formats" path=""/>
|
||||||
|
<MODULEPATH id="juce_audio_processors" path=""/>
|
||||||
|
<MODULEPATH id="juce_audio_utils" path=""/>
|
||||||
|
<MODULEPATH id="juce_core" path=""/>
|
||||||
|
<MODULEPATH id="juce_data_structures" path=""/>
|
||||||
|
<MODULEPATH id="juce_events" path=""/>
|
||||||
|
<MODULEPATH id="juce_graphics" path=""/>
|
||||||
|
<MODULEPATH id="juce_gui_basics" path=""/>
|
||||||
|
<MODULEPATH id="juce_gui_extra" path=""/>
|
||||||
|
</MODULEPATHS>
|
||||||
|
</VS2019>
|
||||||
|
<LINUX_MAKE targetFolder="Builds/LinuxMakefile">
|
||||||
|
<CONFIGURATIONS>
|
||||||
|
<CONFIGURATION name="Debug" isDebug="1" optimisation="1" targetName="WavetableSynthTutorial"/>
|
||||||
|
<CONFIGURATION name="Release" isDebug="0" optimisation="3" targetName="WavetableSynthTutorial"/>
|
||||||
|
</CONFIGURATIONS>
|
||||||
|
<MODULEPATHS>
|
||||||
|
<MODULEPATH id="juce_audio_basics" path=""/>
|
||||||
|
<MODULEPATH id="juce_audio_devices" path=""/>
|
||||||
|
<MODULEPATH id="juce_audio_formats" path=""/>
|
||||||
|
<MODULEPATH id="juce_audio_processors" path=""/>
|
||||||
|
<MODULEPATH id="juce_audio_utils" path=""/>
|
||||||
|
<MODULEPATH id="juce_core" path=""/>
|
||||||
|
<MODULEPATH id="juce_data_structures" path=""/>
|
||||||
|
<MODULEPATH id="juce_events" path=""/>
|
||||||
|
<MODULEPATH id="juce_graphics" path=""/>
|
||||||
|
<MODULEPATH id="juce_gui_basics" path=""/>
|
||||||
|
<MODULEPATH id="juce_gui_extra" path=""/>
|
||||||
|
</MODULEPATHS>
|
||||||
|
</LINUX_MAKE>
|
||||||
|
</EXPORTFORMATS>
|
||||||
|
<JUCEOPTIONS/>
|
||||||
|
</JUCERPROJECT>
|
||||||
@@ -43,7 +43,7 @@ namespace ProjectInfo
|
|||||||
{
|
{
|
||||||
const char* const projectName = "NeuralSynth";
|
const char* const projectName = "NeuralSynth";
|
||||||
const char* const companyName = "Samedi Dimanche";
|
const char* const companyName = "Samedi Dimanche";
|
||||||
const char* const versionString = "0.0.1";
|
const char* const versionString = "1.0.0";
|
||||||
const int versionNumber = 0x1;
|
const int versionNumber = 0x10000;
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|||||||
@@ -17,7 +17,7 @@
|
|||||||
#define JucePlugin_Build_VST3 1
|
#define JucePlugin_Build_VST3 1
|
||||||
#endif
|
#endif
|
||||||
#ifndef JucePlugin_Build_AU
|
#ifndef JucePlugin_Build_AU
|
||||||
#define JucePlugin_Build_AU 1
|
#define JucePlugin_Build_AU 0
|
||||||
#endif
|
#endif
|
||||||
#ifndef JucePlugin_Build_AUv3
|
#ifndef JucePlugin_Build_AUv3
|
||||||
#define JucePlugin_Build_AUv3 0
|
#define JucePlugin_Build_AUv3 0
|
||||||
@@ -26,7 +26,7 @@
|
|||||||
#define JucePlugin_Build_AAX 0
|
#define JucePlugin_Build_AAX 0
|
||||||
#endif
|
#endif
|
||||||
#ifndef JucePlugin_Build_Standalone
|
#ifndef JucePlugin_Build_Standalone
|
||||||
#define JucePlugin_Build_Standalone 1
|
#define JucePlugin_Build_Standalone 0
|
||||||
#endif
|
#endif
|
||||||
#ifndef JucePlugin_Build_Unity
|
#ifndef JucePlugin_Build_Unity
|
||||||
#define JucePlugin_Build_Unity 0
|
#define JucePlugin_Build_Unity 0
|
||||||
@@ -77,13 +77,13 @@
|
|||||||
#define JucePlugin_EditorRequiresKeyboardFocus 0
|
#define JucePlugin_EditorRequiresKeyboardFocus 0
|
||||||
#endif
|
#endif
|
||||||
#ifndef JucePlugin_Version
|
#ifndef JucePlugin_Version
|
||||||
#define JucePlugin_Version 0.0.1
|
#define JucePlugin_Version 1.0.0
|
||||||
#endif
|
#endif
|
||||||
#ifndef JucePlugin_VersionCode
|
#ifndef JucePlugin_VersionCode
|
||||||
#define JucePlugin_VersionCode 0x1
|
#define JucePlugin_VersionCode 0x10000
|
||||||
#endif
|
#endif
|
||||||
#ifndef JucePlugin_VersionString
|
#ifndef JucePlugin_VersionString
|
||||||
#define JucePlugin_VersionString "0.0.1"
|
#define JucePlugin_VersionString "1.0.0"
|
||||||
#endif
|
#endif
|
||||||
#ifndef JucePlugin_VSTUniqueID
|
#ifndef JucePlugin_VSTUniqueID
|
||||||
#define JucePlugin_VSTUniqueID JucePlugin_PluginCode
|
#define JucePlugin_VSTUniqueID JucePlugin_PluginCode
|
||||||
@@ -155,7 +155,7 @@
|
|||||||
#define JucePlugin_ARAFactoryID "com.SamediDimanche.NeuralSynth.factory"
|
#define JucePlugin_ARAFactoryID "com.SamediDimanche.NeuralSynth.factory"
|
||||||
#endif
|
#endif
|
||||||
#ifndef JucePlugin_ARADocumentArchiveID
|
#ifndef JucePlugin_ARADocumentArchiveID
|
||||||
#define JucePlugin_ARADocumentArchiveID "com.SamediDimanche.NeuralSynth.aradocumentarchive.0.0.1"
|
#define JucePlugin_ARADocumentArchiveID "com.SamediDimanche.NeuralSynth.aradocumentarchive.1.0.0"
|
||||||
#endif
|
#endif
|
||||||
#ifndef JucePlugin_ARACompatibleArchiveIDs
|
#ifndef JucePlugin_ARACompatibleArchiveIDs
|
||||||
#define JucePlugin_ARACompatibleArchiveIDs ""
|
#define JucePlugin_ARACompatibleArchiveIDs ""
|
||||||
|
|||||||
@@ -4,9 +4,27 @@
|
|||||||
addUsingNamespaceToJuceHeader="0" jucerFormatVersion="1" companyWebsite="www.samedidimanche.com"
|
addUsingNamespaceToJuceHeader="0" jucerFormatVersion="1" companyWebsite="www.samedidimanche.com"
|
||||||
bundleIdentifier="com.samedidimanche.NeuralSynth" pluginManufacturer="Samedi Dimanche"
|
bundleIdentifier="com.samedidimanche.NeuralSynth" pluginManufacturer="Samedi Dimanche"
|
||||||
companyName="Samedi Dimanche" pluginCharacteristicsValue="pluginIsSynth,pluginWantsMidiIn"
|
companyName="Samedi Dimanche" pluginCharacteristicsValue="pluginIsSynth,pluginWantsMidiIn"
|
||||||
pluginVSTNumMidiInputs="1" pluginChannelConfigs="{0, 2}" version="0.0.1">
|
pluginVSTNumMidiInputs="1" pluginChannelConfigs="{0, 2}" version="1.0.0"
|
||||||
|
pluginFormats="buildVST3">
|
||||||
<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">
|
||||||
|
<GROUP id="{7B17D83D-70A9-0C31-D663-B953A624AE3F}" name="UI">
|
||||||
|
<FILE id="ZL6eFk" name="CustomPresetWindow.cpp" compile="1" resource="0"
|
||||||
|
file="Source/UI/CustomPresetWindow.cpp"/>
|
||||||
|
<FILE id="Zb36sR" name="CustomPresetWindow.h" compile="0" resource="0"
|
||||||
|
file="Source/UI/CustomPresetWindow.h"/>
|
||||||
|
</GROUP>
|
||||||
|
<GROUP id="{B2D8F867-A0C5-54CA-75AD-EFA0141DDFE9}" name="SynthVoice">
|
||||||
|
<FILE id="rbLVkZ" name="ADSR.h" compile="0" resource="0" file="Source/SynthVoice/ADSR.h"/>
|
||||||
|
<FILE id="lYeoyk" name="Chorus.h" compile="0" resource="0" file="Source/SynthVoice/Chorus.h"/>
|
||||||
|
<FILE id="vBX0Mt" name="Distortion.h" compile="0" resource="0" file="Source/SynthVoice/Distortion.h"/>
|
||||||
|
<FILE id="jAtEqL" name="EQ.h" compile="0" resource="0" file="Source/SynthVoice/EQ.h"/>
|
||||||
|
<FILE id="Zeb5Xf" name="Flanger.h" compile="0" resource="0" file="Source/SynthVoice/Flanger.h"/>
|
||||||
|
<FILE id="UnqRtH" name="Reverb.h" compile="0" resource="0" file="Source/SynthVoice/Reverb.h"/>
|
||||||
|
<FILE id="ChzbrW" name="SimpleDelay.h" compile="0" resource="0" file="Source/SynthVoice/SimpleDelay.h"/>
|
||||||
|
</GROUP>
|
||||||
|
<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"
|
||||||
@@ -68,5 +86,26 @@
|
|||||||
<MODULEPATH id="juce_gui_extra" path="../../../../../JUCE/modules"/>
|
<MODULEPATH id="juce_gui_extra" path="../../../../../JUCE/modules"/>
|
||||||
</MODULEPATHS>
|
</MODULEPATHS>
|
||||||
</VS2022>
|
</VS2022>
|
||||||
|
<LINUX_MAKE targetFolder="Builds/LinuxMakefile">
|
||||||
|
<CONFIGURATIONS>
|
||||||
|
<CONFIGURATION isDebug="1" name="Debug"/>
|
||||||
|
<CONFIGURATION isDebug="0" name="Release"/>
|
||||||
|
</CONFIGURATIONS>
|
||||||
|
<MODULEPATHS>
|
||||||
|
<MODULEPATH id="juce_audio_basics" path="../../juce"/>
|
||||||
|
<MODULEPATH id="juce_audio_devices" path="../../juce"/>
|
||||||
|
<MODULEPATH id="juce_audio_formats" path="../../juce"/>
|
||||||
|
<MODULEPATH id="juce_audio_plugin_client" path="../../juce"/>
|
||||||
|
<MODULEPATH id="juce_audio_processors" path="../../juce"/>
|
||||||
|
<MODULEPATH id="juce_audio_utils" path="../../juce"/>
|
||||||
|
<MODULEPATH id="juce_core" path="../../juce"/>
|
||||||
|
<MODULEPATH id="juce_data_structures" path="../../juce"/>
|
||||||
|
<MODULEPATH id="juce_dsp" path="../../juce"/>
|
||||||
|
<MODULEPATH id="juce_events" path="../../juce"/>
|
||||||
|
<MODULEPATH id="juce_graphics" path="../../juce"/>
|
||||||
|
<MODULEPATH id="juce_gui_basics" path="../../juce"/>
|
||||||
|
<MODULEPATH id="juce_gui_extra" path="../../juce"/>
|
||||||
|
</MODULEPATHS>
|
||||||
|
</LINUX_MAKE>
|
||||||
</EXPORTFORMATS>
|
</EXPORTFORMATS>
|
||||||
</JUCERPROJECT>
|
</JUCERPROJECT>
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ public:
|
|||||||
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.
|
||||||
|
enableLegacyMode(maxNumVoices);
|
||||||
setVoiceStealingEnabled(true);
|
setVoiceStealingEnabled(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,10 +6,24 @@ 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 = juce::jmax (1.0, sampleRate);
|
||||||
|
setFrequency ((float) freq);
|
||||||
|
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)
|
||||||
void resetPhase (float p = 0.0f) { phase = juce::jlimit (0.0f, 1.0f, p); }
|
{
|
||||||
|
const float nyquist = 0.5f * (float) sr;
|
||||||
|
freq = juce::jlimit (0.0f, juce::jmax (0.0f, nyquist), f);
|
||||||
|
inc = freq / (float) sr;
|
||||||
|
}
|
||||||
|
void resetPhase (float p = 0.0f)
|
||||||
|
{
|
||||||
|
phase = juce::jlimit (0.0f, 1.0f, p);
|
||||||
|
z1 = 0.0f;
|
||||||
|
}
|
||||||
|
|
||||||
inline float process()
|
inline float process()
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -3,13 +3,14 @@
|
|||||||
#include <atomic>
|
#include <atomic>
|
||||||
#include <unordered_map>
|
#include <unordered_map>
|
||||||
#include <string>
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
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::vector<std::pair<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 = {
|
||||||
@@ -67,12 +68,30 @@ const std::unordered_map<std::string, ParamMap> PARAM_SETTINGS = {
|
|||||||
{ "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 } }
|
||||||
|
}},
|
||||||
|
{ "wt", {
|
||||||
|
{ "morph", { "Morph", 0.0f, 15.0f, 0.001f, 0.0f } },
|
||||||
|
{ "phase", { "Phase", 0.0f, 1.0f, 0.001f, 0.0f } },
|
||||||
|
{ "bank", { "Bank", 0.0f,255.0f, 1.0f, 0.0f } },
|
||||||
|
{ "lfoRate", { "LFO Rate", 0.01f, 10.0f, 0.001f, 1.0f } },
|
||||||
|
{ "lfoDepth",{ "LFO Depth", 0.0f, 8.0f, 0.001f, 0.0f } },
|
||||||
|
{ "lfoShape",{ "LFO Shape", 0.0f, 3.0f, 1.0f, 0.0f } },
|
||||||
|
{ "level", { "Level", 0.0f, 1.0f, 0.001f, 0.8f } }
|
||||||
|
}},
|
||||||
|
{ "wt2", {
|
||||||
|
{ "morph", { "Morph", 0.0f, 15.0f, 0.001f, 0.0f } },
|
||||||
|
{ "phase", { "Phase", 0.0f, 1.0f, 0.001f, 0.0f } },
|
||||||
|
{ "bank", { "Bank", 0.0f,255.0f, 1.0f, 0.0f } },
|
||||||
|
{ "lfoRate", { "LFO Rate", 0.01f, 10.0f, 0.001f, 1.0f } },
|
||||||
|
{ "lfoDepth",{ "LFO Depth", 0.0f, 8.0f, 0.001f, 0.0f } },
|
||||||
|
{ "lfoShape",{ "LFO Shape", 0.0f, 3.0f, 1.0f, 0.0f } },
|
||||||
|
{ "level", { "Level", 0.0f, 1.0f, 0.001f, 0.0f } }
|
||||||
}}
|
}}
|
||||||
};
|
};
|
||||||
|
|
||||||
struct NeuralSharedParams
|
struct NeuralSharedParams
|
||||||
{
|
{
|
||||||
std::atomic<int> waveform{ -1 };
|
std::atomic<float>* wtPhase{};
|
||||||
|
|
||||||
// Amp ADSR
|
// Amp ADSR
|
||||||
std::atomic<float>* adsrAttack{};
|
std::atomic<float>* adsrAttack{};
|
||||||
@@ -137,9 +156,28 @@ struct NeuralSharedParams
|
|||||||
std::atomic<float>* filterOn{};
|
std::atomic<float>* filterOn{};
|
||||||
std::atomic<float>* eqOn{};
|
std::atomic<float>* eqOn{};
|
||||||
|
|
||||||
|
// Wavetable
|
||||||
|
std::atomic<float>* wtOn{};
|
||||||
|
std::atomic<float>* wtMorph{};
|
||||||
|
std::atomic<float>* wtBank{};
|
||||||
|
std::atomic<float>* wtLfoRate{};
|
||||||
|
std::atomic<float>* wtLfoDepth{};
|
||||||
|
std::atomic<float>* wtLfoShape{};
|
||||||
|
std::atomic<float>* wtLevel{};
|
||||||
|
|
||||||
// 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{};
|
||||||
|
|
||||||
|
// Wavetable Layer B
|
||||||
|
std::atomic<float>* wt2Phase{};
|
||||||
|
std::atomic<float>* wt2On{};
|
||||||
|
std::atomic<float>* wt2Morph{};
|
||||||
|
std::atomic<float>* wt2Bank{};
|
||||||
|
std::atomic<float>* wt2LfoRate{};
|
||||||
|
std::atomic<float>* wt2LfoDepth{};
|
||||||
|
std::atomic<float>* wt2LfoShape{};
|
||||||
|
std::atomic<float>* wt2Level{};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,24 +1,31 @@
|
|||||||
#include "PluginProcessor.h"
|
#include "PluginProcessor.h"
|
||||||
#include "PluginEditor.h"
|
#include "PluginEditor.h"
|
||||||
#include "ScopeComponent.h"
|
#include "ScopeComponent.h"
|
||||||
|
#include "WavetableOsc.h"
|
||||||
|
#include <array>
|
||||||
|
|
||||||
//==============================================================================
|
//==============================================================================
|
||||||
NeuralSynthAudioProcessorEditor::NeuralSynthAudioProcessorEditor (NeuralSynthAudioProcessor& p)
|
NeuralSynthAudioProcessorEditor::NeuralSynthAudioProcessorEditor (NeuralSynthAudioProcessor& p)
|
||||||
: AudioProcessorEditor (&p),
|
: AudioProcessorEditor (&p),
|
||||||
audioProcessor (p),
|
audioProcessor (p),
|
||||||
mainScopeComponent(audioProcessor.getAudioBufferQueue())
|
mainScopeComponent(audioProcessor.getAudioBufferQueue()),
|
||||||
|
keyboardComponent(keyboardState, juce::MidiKeyboardComponent::horizontalKeyboard)
|
||||||
{
|
{
|
||||||
auto& tree = audioProcessor.parameters;
|
auto& tree = audioProcessor.parameters;
|
||||||
|
|
||||||
addAndMakeVisible(mainScopeComponent);
|
addAndMakeVisible(mainScopeComponent);
|
||||||
|
|
||||||
waveformSelector.setModel(&waveformContents);
|
keyboardState.addListener(this);
|
||||||
waveformContents.onSelect = [this](int row)
|
keyboardComponent.setMidiChannel(1);
|
||||||
{
|
keyboardComponent.setScrollButtonsVisible(true);
|
||||||
// write to the parameter so voices update safely
|
keyboardComponent.setKeyWidth(36.0f);
|
||||||
audioProcessor.parameters.getParameterAsValue("waveform") = (float)juce::jlimit(0, 3, row);
|
keyboardComponent.setAvailableRange(36, 96);
|
||||||
};
|
keyboardComponent.setVelocity(1.0f, true);
|
||||||
addAndMakeVisible(waveformSelector);
|
addAndMakeVisible(keyboardComponent);
|
||||||
|
|
||||||
|
presetMenuButton.setButtonText("Preset");
|
||||||
|
presetMenuButton.onClick = [this] { showPresetMenu(); };
|
||||||
|
addAndMakeVisible(presetMenuButton);
|
||||||
|
|
||||||
// --- Panels ---
|
// --- Panels ---
|
||||||
adsrComponent.emplace(tree, "adsr", "Amp Env");
|
adsrComponent.emplace(tree, "adsr", "Amp Env");
|
||||||
@@ -92,6 +99,76 @@ NeuralSynthAudioProcessorEditor::NeuralSynthAudioProcessorEditor (NeuralSynthAud
|
|||||||
});
|
});
|
||||||
addAndMakeVisible(*filterEnvComponent);
|
addAndMakeVisible(*filterEnvComponent);
|
||||||
|
|
||||||
|
auto configureBankSlider = [](juce::Slider* slider)
|
||||||
|
{
|
||||||
|
if (slider == nullptr)
|
||||||
|
return;
|
||||||
|
|
||||||
|
const auto& presets = WT::FactoryLibrary::get();
|
||||||
|
std::vector<juce::String> names;
|
||||||
|
names.reserve(presets.size());
|
||||||
|
for (const auto& preset : presets)
|
||||||
|
names.push_back(preset.name);
|
||||||
|
|
||||||
|
const double maxIndex = names.empty() ? 0.0 : (double) (names.size() - 1);
|
||||||
|
slider->setRange (0.0, maxIndex, 1.0);
|
||||||
|
slider->setNumDecimalPlacesToDisplay(0);
|
||||||
|
|
||||||
|
slider->textFromValueFunction = [names](double value)
|
||||||
|
{
|
||||||
|
if (names.empty())
|
||||||
|
return juce::String ((int) std::lround (value));
|
||||||
|
|
||||||
|
const int idx = juce::jlimit (0, (int) names.size() - 1, (int) std::lround (value));
|
||||||
|
return names[(size_t) idx];
|
||||||
|
};
|
||||||
|
|
||||||
|
slider->valueFromTextFunction = [names](const juce::String& text)
|
||||||
|
{
|
||||||
|
if (! names.empty())
|
||||||
|
{
|
||||||
|
for (size_t i = 0; i < names.size(); ++i)
|
||||||
|
if (text.equalsIgnoreCase (names[i]))
|
||||||
|
return (double) i;
|
||||||
|
}
|
||||||
|
return text.getDoubleValue();
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
auto configureShapeSlider = [](juce::Slider* slider)
|
||||||
|
{
|
||||||
|
if (slider == nullptr)
|
||||||
|
return;
|
||||||
|
|
||||||
|
static const std::array<juce::String, 4> shapeNames { "Sine", "Triangle", "Ramp Up", "Ramp Down" };
|
||||||
|
slider->setNumDecimalPlacesToDisplay(0);
|
||||||
|
slider->textFromValueFunction = [](double value)
|
||||||
|
{
|
||||||
|
const int idx = juce::jlimit (0, (int) shapeNames.size() - 1, (int) std::lround (value));
|
||||||
|
return shapeNames[(size_t) idx];
|
||||||
|
};
|
||||||
|
slider->valueFromTextFunction = [](const juce::String& text)
|
||||||
|
{
|
||||||
|
for (size_t i = 0; i < shapeNames.size(); ++i)
|
||||||
|
if (text.equalsIgnoreCase (shapeNames[i]))
|
||||||
|
return (double) i;
|
||||||
|
return text.getDoubleValue();
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
wtComponent.emplace(tree, "wt", "Layer A");
|
||||||
|
configureBankSlider (wtComponent->getSlider("bank"));
|
||||||
|
configureShapeSlider (wtComponent->getSlider("lfoShape"));
|
||||||
|
addAndMakeVisible(*wtComponent);
|
||||||
|
wtComponent->setTitleText("Layer A");
|
||||||
|
|
||||||
|
layerSelector.addItem("Layer A", 1);
|
||||||
|
layerSelector.addItem("Layer B", 2);
|
||||||
|
layerSelector.setSelectedId(1, juce::dontSendNotification);
|
||||||
|
layerSelector.onChange = [this] { handleLayerSelectionChanged(); };
|
||||||
|
wtComponent->setTopBarAccessory(&layerSelector, 118);
|
||||||
|
handleLayerSelectionChanged();
|
||||||
|
|
||||||
// Master fader + label
|
// Master fader + label
|
||||||
addAndMakeVisible(masterLevelSlider);
|
addAndMakeVisible(masterLevelSlider);
|
||||||
masterLevelLabel.setText("Master", juce::dontSendNotification);
|
masterLevelLabel.setText("Master", juce::dontSendNotification);
|
||||||
@@ -102,18 +179,25 @@ NeuralSynthAudioProcessorEditor::NeuralSynthAudioProcessorEditor (NeuralSynthAud
|
|||||||
masterLevelLabel.setJustificationType(juce::Justification::centred);
|
masterLevelLabel.setJustificationType(juce::Justification::centred);
|
||||||
addAndMakeVisible(masterLevelLabel);
|
addAndMakeVisible(masterLevelLabel);
|
||||||
|
|
||||||
// Blank placeholder
|
|
||||||
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);
|
||||||
|
|
||||||
|
lastPresetIndex = audioProcessor.getCurrentPresetIndex();
|
||||||
|
updatePresetButtonLabel();
|
||||||
|
startTimerHz(10);
|
||||||
|
|
||||||
setSize(1400, 720);
|
setSize(1400, 720);
|
||||||
}
|
}
|
||||||
|
|
||||||
//==============================================================================
|
//==============================================================================
|
||||||
NeuralSynthAudioProcessorEditor::~NeuralSynthAudioProcessorEditor() = default;
|
NeuralSynthAudioProcessorEditor::~NeuralSynthAudioProcessorEditor()
|
||||||
|
{
|
||||||
|
stopTimer();
|
||||||
|
if (customPresetWindow != nullptr)
|
||||||
|
customPresetWindow.reset();
|
||||||
|
keyboardState.removeListener(this);
|
||||||
|
}
|
||||||
|
|
||||||
//==============================================================================
|
//==============================================================================
|
||||||
void NeuralSynthAudioProcessorEditor::paint (juce::Graphics& g)
|
void NeuralSynthAudioProcessorEditor::paint (juce::Graphics& g)
|
||||||
@@ -124,47 +208,191 @@ void NeuralSynthAudioProcessorEditor::paint (juce::Graphics& g)
|
|||||||
//==============================================================================
|
//==============================================================================
|
||||||
void NeuralSynthAudioProcessorEditor::resized()
|
void NeuralSynthAudioProcessorEditor::resized()
|
||||||
{
|
{
|
||||||
auto bounds = getLocalBounds().reduced(16);
|
auto outer = getLocalBounds().reduced(16);
|
||||||
|
|
||||||
|
// --- carve out sidebar for MASTER (right side) --------------------------
|
||||||
|
const int sidebarWidth = 100; // tweak if you want it wider/narrower
|
||||||
|
auto gridArea = outer;
|
||||||
|
auto sidebar = gridArea.removeFromRight(sidebarWidth).reduced(8);
|
||||||
|
|
||||||
|
// Master label + fader in the sidebar (stacked)
|
||||||
|
{
|
||||||
|
auto top = sidebar.removeFromTop(24);
|
||||||
|
masterLevelLabel.setBounds(top.withTrimmedLeft(4));
|
||||||
|
|
||||||
|
// leave a little top margin before the fader
|
||||||
|
sidebar.removeFromTop(8);
|
||||||
|
masterLevelSlider.setBounds(sidebar);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Grid: Scope + two rows of five boxes (no gaps) ---------------------
|
||||||
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(10)), // scope band
|
||||||
juce::Grid::TrackInfo(juce::Grid::Fr(40)), // row 1
|
juce::Grid::TrackInfo(juce::Grid::Fr(35)), // row 1
|
||||||
juce::Grid::TrackInfo(juce::Grid::Fr(40)) // row 2
|
juce::Grid::TrackInfo(juce::Grid::Fr(35)), // row 2
|
||||||
|
juce::Grid::TrackInfo(juce::Grid::Fr(20)) // keyboard
|
||||||
};
|
};
|
||||||
|
|
||||||
// 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(20)),
|
||||||
juce::Grid::TrackInfo(juce::Grid::Fr(18)),
|
juce::Grid::TrackInfo(juce::Grid::Fr(20)),
|
||||||
juce::Grid::TrackInfo(juce::Grid::Fr(18)),
|
juce::Grid::TrackInfo(juce::Grid::Fr(20)),
|
||||||
juce::Grid::TrackInfo(juce::Grid::Fr(18)),
|
juce::Grid::TrackInfo(juce::Grid::Fr(20)),
|
||||||
juce::Grid::TrackInfo(juce::Grid::Fr(18)),
|
juce::Grid::TrackInfo(juce::Grid::Fr(20))
|
||||||
juce::Grid::TrackInfo(juce::Grid::Fr(10))
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Row 0
|
grid.rowGap = juce::Grid::Px(0);
|
||||||
grid.items.add(juce::GridItem(mainScopeComponent)
|
grid.columnGap = juce::Grid::Px(0);
|
||||||
.withArea(juce::GridItem::Span(1), juce::GridItem::Span(5)));
|
|
||||||
grid.items.add(juce::GridItem(waveformSelector)
|
|
||||||
.withArea(juce::GridItem::Span(1), juce::GridItem::Span(1)));
|
|
||||||
|
|
||||||
// Row 1
|
grid.items.clear();
|
||||||
|
|
||||||
|
// Row 1 (scope row)
|
||||||
|
grid.items.add(juce::GridItem(mainScopeComponent).withArea({}, juce::GridItem::Span(4)));
|
||||||
|
// Put preset button at the top-right cell of the scope row
|
||||||
|
grid.items.add(juce::GridItem(presetMenuButton)
|
||||||
|
.withArea(1, 5)
|
||||||
|
.withJustifySelf(juce::GridItem::JustifySelf::end)
|
||||||
|
.withAlignSelf(juce::GridItem::AlignSelf::start));
|
||||||
|
|
||||||
|
// Row 2 (top row of panels): Amp Env, Chorus, Delay, Reverb, EQ
|
||||||
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));
|
|
||||||
|
|
||||||
// Row 2
|
// Row 3 (bottom row of panels): Flanger, Distortion, Filter, Filter Env, Wavetable
|
||||||
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(*wtComponent ));
|
||||||
grid.items.add(juce::GridItem(masterLevelSlider));
|
|
||||||
|
|
||||||
grid.performLayout(bounds);
|
// Row 4: MIDI keyboard spans entire width
|
||||||
|
grid.items.add(juce::GridItem(keyboardComponent).withArea({}, juce::GridItem::Span(5)));
|
||||||
|
|
||||||
|
grid.performLayout(gridArea);
|
||||||
|
}
|
||||||
|
|
||||||
|
void NeuralSynthAudioProcessorEditor::handleNoteOn(juce::MidiKeyboardState*, int midiChannel, int midiNoteNumber, float velocity)
|
||||||
|
{
|
||||||
|
auto message = juce::MidiMessage::noteOn(midiChannel, midiNoteNumber, velocity);
|
||||||
|
message.setTimeStamp(juce::Time::getMillisecondCounterHiRes() * 0.001);
|
||||||
|
audioProcessor.midiMessageCollector.addMessageToQueue(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
void NeuralSynthAudioProcessorEditor::handleNoteOff(juce::MidiKeyboardState*, int midiChannel, int midiNoteNumber, float velocity)
|
||||||
|
{
|
||||||
|
auto message = juce::MidiMessage::noteOff(midiChannel, midiNoteNumber, velocity);
|
||||||
|
message.setTimeStamp(juce::Time::getMillisecondCounterHiRes() * 0.001);
|
||||||
|
audioProcessor.midiMessageCollector.addMessageToQueue(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
void NeuralSynthAudioProcessorEditor::timerCallback()
|
||||||
|
{
|
||||||
|
const int current = audioProcessor.getCurrentPresetIndex();
|
||||||
|
if (current != lastPresetIndex)
|
||||||
|
{
|
||||||
|
lastPresetIndex = current;
|
||||||
|
updatePresetButtonLabel();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void NeuralSynthAudioProcessorEditor::showPresetMenu()
|
||||||
|
{
|
||||||
|
const auto& presets = audioProcessor.getFactoryPresets();
|
||||||
|
if (presets.empty())
|
||||||
|
return;
|
||||||
|
|
||||||
|
juce::PopupMenu menu;
|
||||||
|
juce::StringArray categories;
|
||||||
|
for (const auto& preset : presets)
|
||||||
|
if (! categories.contains(preset.category))
|
||||||
|
categories.add(preset.category);
|
||||||
|
|
||||||
|
const int baseId = 1000;
|
||||||
|
const int customPresetMenuId = baseId - 1;
|
||||||
|
for (const auto& category : categories)
|
||||||
|
{
|
||||||
|
juce::PopupMenu sub;
|
||||||
|
for (int i = 0; i < (int) presets.size(); ++i)
|
||||||
|
{
|
||||||
|
if (presets[(size_t) i].category == category)
|
||||||
|
{
|
||||||
|
sub.addItem(baseId + i, presets[(size_t) i].name,
|
||||||
|
true, audioProcessor.getCurrentPresetIndex() == i);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
menu.addSubMenu(category, sub);
|
||||||
|
}
|
||||||
|
|
||||||
|
menu.addSeparator();
|
||||||
|
menu.addItem(customPresetMenuId, "Custom ...", true, false);
|
||||||
|
|
||||||
|
menu.showMenuAsync(juce::PopupMenu::Options().withParentComponent(this),
|
||||||
|
[this, baseId, customPresetMenuId](int result)
|
||||||
|
{
|
||||||
|
if (result == customPresetMenuId)
|
||||||
|
{
|
||||||
|
showCustomPresetWindow();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result >= baseId)
|
||||||
|
{
|
||||||
|
const int index = result - baseId;
|
||||||
|
audioProcessor.applyPreset(index);
|
||||||
|
lastPresetIndex = index;
|
||||||
|
updatePresetButtonLabel();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void NeuralSynthAudioProcessorEditor::showCustomPresetWindow()
|
||||||
|
{
|
||||||
|
constexpr int windowWidth = 420;
|
||||||
|
constexpr int windowHeight = 320;
|
||||||
|
|
||||||
|
if (customPresetWindow == nullptr)
|
||||||
|
{
|
||||||
|
customPresetWindow = std::make_unique<CustomPresetWindow>();
|
||||||
|
customPresetWindow->setSize(windowWidth, windowHeight);
|
||||||
|
}
|
||||||
|
|
||||||
|
customPresetWindow->centreAroundComponent(this, windowWidth, windowHeight);
|
||||||
|
customPresetWindow->setVisible(true);
|
||||||
|
customPresetWindow->toFront(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
void NeuralSynthAudioProcessorEditor::updatePresetButtonLabel()
|
||||||
|
{
|
||||||
|
const auto& presets = audioProcessor.getFactoryPresets();
|
||||||
|
const int current = audioProcessor.getCurrentPresetIndex();
|
||||||
|
|
||||||
|
juce::String label = "Preset: ";
|
||||||
|
if (current >= 0 && current < (int) presets.size())
|
||||||
|
{
|
||||||
|
const auto& preset = presets[(size_t) current];
|
||||||
|
label += preset.category + " / " + preset.name;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
label += "Custom";
|
||||||
|
}
|
||||||
|
|
||||||
|
presetMenuButton.setButtonText(label);
|
||||||
|
}
|
||||||
|
|
||||||
|
void NeuralSynthAudioProcessorEditor::handleLayerSelectionChanged()
|
||||||
|
{
|
||||||
|
const bool useLayerB = (layerSelector.getSelectedId() == 2);
|
||||||
|
if (! wtComponent || controllingLayerB == useLayerB)
|
||||||
|
return;
|
||||||
|
|
||||||
|
controllingLayerB = useLayerB;
|
||||||
|
const std::string group = useLayerB ? "wt2" : "wt";
|
||||||
|
wtComponent->reassignParamGroup(group);
|
||||||
|
wtComponent->setTitleText(useLayerB ? "Layer B" : "Layer A");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
#include "PluginProcessor.h"
|
#include "PluginProcessor.h"
|
||||||
#include "GraphComponent.h"
|
#include "GraphComponent.h"
|
||||||
#include "ScopeComponent.h"
|
#include "ScopeComponent.h"
|
||||||
|
#include "UI/CustomPresetWindow.h"
|
||||||
|
|
||||||
//============================== ScopeSliderComponent ==========================
|
//============================== ScopeSliderComponent ==========================
|
||||||
// A generic panel: optional scope/graph + rotary sliders + labels.
|
// A generic panel: optional scope/graph + rotary sliders + labels.
|
||||||
@@ -25,6 +26,8 @@ public:
|
|||||||
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()));
|
||||||
|
|
||||||
|
paramNames.push_back(name);
|
||||||
|
|
||||||
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);
|
||||||
}
|
}
|
||||||
@@ -55,13 +58,75 @@ public:
|
|||||||
addAndMakeVisible(titleLabel);
|
addAndMakeVisible(titleLabel);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Bypass toggle (per panel), id "<group>_on"
|
if (tree.getParameter(paramGroupId + "_on") != nullptr)
|
||||||
|
{
|
||||||
|
hasBypass = true;
|
||||||
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 setTitleText(const juce::String& text)
|
||||||
|
{
|
||||||
|
if (titleLabel.isVisible())
|
||||||
|
titleLabel.setText(text, juce::dontSendNotification);
|
||||||
|
}
|
||||||
|
|
||||||
|
void setTopBarAccessory(juce::Component* component, int preferredWidth = 120)
|
||||||
|
{
|
||||||
|
topBarAccessory = component;
|
||||||
|
accessoryPreferredWidth = preferredWidth;
|
||||||
|
if (topBarAccessory != nullptr)
|
||||||
|
addAndMakeVisible(*topBarAccessory);
|
||||||
|
}
|
||||||
|
|
||||||
|
void reassignParamGroup(const std::string& newGroup)
|
||||||
|
{
|
||||||
|
if (newGroup == paramGroupId)
|
||||||
|
return;
|
||||||
|
|
||||||
|
const auto& sliderDetails = PARAM_SETTINGS.at(newGroup);
|
||||||
|
jassert(sliderDetails.size() == sliders.size());
|
||||||
|
jassert(sliderDetails.size() == labels.size());
|
||||||
|
jassert(sliderDetails.size() == paramNames.size());
|
||||||
|
|
||||||
|
for (size_t i = 0; i < sliderDetails.size(); ++i)
|
||||||
|
{
|
||||||
|
const auto& [name, detail] = sliderDetails[i];
|
||||||
|
paramNames[i] = name;
|
||||||
|
sliders[i]->setRange(detail.min, detail.max, detail.interval);
|
||||||
|
labels[i]->setText(detail.label, juce::dontSendNotification);
|
||||||
|
}
|
||||||
|
|
||||||
|
attachments.clear();
|
||||||
|
attachments.reserve(sliderDetails.size());
|
||||||
|
for (size_t i = 0; i < sliderDetails.size(); ++i)
|
||||||
|
{
|
||||||
|
const auto paramId = newGroup + "_" + paramNames[i];
|
||||||
|
attachments.push_back(std::make_unique<juce::AudioProcessorValueTreeState::SliderAttachment>(
|
||||||
|
treeRef, paramId, *sliders[i]));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hasBypass)
|
||||||
|
{
|
||||||
|
bypassAttachment.reset();
|
||||||
|
if (treeRef.getParameter(newGroup + "_on") != nullptr)
|
||||||
|
{
|
||||||
|
bypassAttachment = std::make_unique<juce::AudioProcessorValueTreeState::ButtonAttachment>(
|
||||||
|
treeRef, newGroup + "_on", bypassButton);
|
||||||
|
bypassButton.setEnabled(true);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
bypassButton.setEnabled(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
paramGroupId = newGroup;
|
||||||
|
}
|
||||||
|
|
||||||
void enableSampleScope(AudioBufferQueue<float>& audioBufferQueue) {
|
void enableSampleScope(AudioBufferQueue<float>& audioBufferQueue) {
|
||||||
scope.emplace(audioBufferQueue);
|
scope.emplace(audioBufferQueue);
|
||||||
@@ -76,7 +141,17 @@ public:
|
|||||||
addAndMakeVisible(*graphScope);
|
addAndMakeVisible(*graphScope);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
juce::Slider* getSlider(const std::string& name) { return findSlider(name); }
|
||||||
|
|
||||||
private:
|
private:
|
||||||
|
juce::Slider* findSlider(const std::string& name)
|
||||||
|
{
|
||||||
|
for (size_t i = 0; i < paramNames.size(); ++i)
|
||||||
|
if (paramNames[i] == name)
|
||||||
|
return sliders[i].get();
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
void paint(juce::Graphics& g) override
|
void paint(juce::Graphics& g) override
|
||||||
{
|
{
|
||||||
g.fillAll(juce::Colours::darkgrey);
|
g.fillAll(juce::Colours::darkgrey);
|
||||||
@@ -89,8 +164,21 @@ private:
|
|||||||
// --- 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);
|
||||||
|
if (hasBypass)
|
||||||
|
{
|
||||||
auto btnW = 46;
|
auto btnW = 46;
|
||||||
bypassButton.setBounds(top.removeFromRight(btnW).reduced(2, 1));
|
auto buttonArea = top.removeFromRight(btnW);
|
||||||
|
bypassButton.setBounds(buttonArea.reduced(2, 1));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (topBarAccessory != nullptr)
|
||||||
|
{
|
||||||
|
const int widthLimit = juce::jmax(60, juce::jmin(accessoryPreferredWidth, top.getWidth()));
|
||||||
|
auto accessoryArea = top.removeFromRight(widthLimit);
|
||||||
|
topBarAccessory->setBounds(accessoryArea.reduced(2, 1));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (titleLabel.isVisible())
|
||||||
titleLabel.setBounds(top);
|
titleLabel.setBounds(top);
|
||||||
|
|
||||||
// --- Rest (grid) ----------------------------------------------------
|
// --- Rest (grid) ----------------------------------------------------
|
||||||
@@ -146,9 +234,14 @@ private:
|
|||||||
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;
|
||||||
|
std::vector<std::string> paramNames;
|
||||||
|
|
||||||
|
juce::Component* topBarAccessory{ nullptr };
|
||||||
|
int accessoryPreferredWidth{ 120 };
|
||||||
|
|
||||||
juce::ToggleButton bypassButton;
|
juce::ToggleButton bypassButton;
|
||||||
std::unique_ptr<juce::AudioProcessorValueTreeState::ButtonAttachment> bypassAttachment;
|
std::unique_ptr<juce::AudioProcessorValueTreeState::ButtonAttachment> bypassAttachment;
|
||||||
|
bool hasBypass{ false };
|
||||||
|
|
||||||
juce::Label titleLabel;
|
juce::Label titleLabel;
|
||||||
|
|
||||||
@@ -254,34 +347,6 @@ private:
|
|||||||
juce::Label titleLabel;
|
juce::Label titleLabel;
|
||||||
};
|
};
|
||||||
|
|
||||||
//============================== Waveform List Model ===========================
|
|
||||||
struct WaveformSelectorContents final : public juce::ListBoxModel
|
|
||||||
{
|
|
||||||
int getNumRows() override { return 4; }
|
|
||||||
|
|
||||||
void paintListBoxItem(int rowNumber, juce::Graphics& g,
|
|
||||||
int width, int height, bool rowIsSelected) override
|
|
||||||
{
|
|
||||||
if (rowIsSelected) g.fillAll(juce::Colours::lightblue);
|
|
||||||
g.setColour(juce::LookAndFeel::getDefaultLookAndFeel()
|
|
||||||
.findColour(juce::Label::textColourId));
|
|
||||||
|
|
||||||
juce::Font f; f.setHeight((float)height * 0.7f);
|
|
||||||
g.setFont(f);
|
|
||||||
g.drawText(waves[(size_t)rowNumber], 5, 0, width, height,
|
|
||||||
juce::Justification::centredLeft, true);
|
|
||||||
}
|
|
||||||
|
|
||||||
void selectedRowsChanged (int lastRowSelected) override
|
|
||||||
{
|
|
||||||
if (onSelect) onSelect(lastRowSelected);
|
|
||||||
}
|
|
||||||
|
|
||||||
std::function<void (int)> onSelect;
|
|
||||||
|
|
||||||
std::vector<juce::String> waves { "Sine", "Saw", "Square", "Triangle" };
|
|
||||||
};
|
|
||||||
|
|
||||||
//============================== MasterVolumeComponent =========================
|
//============================== MasterVolumeComponent =========================
|
||||||
class MasterVolumeComponent : public juce::Component
|
class MasterVolumeComponent : public juce::Component
|
||||||
{
|
{
|
||||||
@@ -290,6 +355,7 @@ public:
|
|||||||
{
|
{
|
||||||
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);
|
||||||
|
slider.setRange(-24.0f, 24.0f, 0.1f);
|
||||||
addAndMakeVisible(slider);
|
addAndMakeVisible(slider);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -302,7 +368,9 @@ public:
|
|||||||
};
|
};
|
||||||
|
|
||||||
//============================== Editor =======================================
|
//============================== Editor =======================================
|
||||||
class NeuralSynthAudioProcessorEditor : public juce::AudioProcessorEditor
|
class NeuralSynthAudioProcessorEditor : public juce::AudioProcessorEditor,
|
||||||
|
private juce::Timer,
|
||||||
|
private juce::MidiKeyboardStateListener
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
NeuralSynthAudioProcessorEditor (NeuralSynthAudioProcessor&);
|
NeuralSynthAudioProcessorEditor (NeuralSynthAudioProcessor&);
|
||||||
@@ -310,13 +378,22 @@ public:
|
|||||||
|
|
||||||
void paint (juce::Graphics&) override;
|
void paint (juce::Graphics&) override;
|
||||||
void resized() override;
|
void resized() override;
|
||||||
|
void timerCallback() override;
|
||||||
|
void handleNoteOn(juce::MidiKeyboardState*, int midiChannel, int midiNoteNumber, float velocity) override;
|
||||||
|
void handleNoteOff(juce::MidiKeyboardState*, int midiChannel, int midiNoteNumber, float velocity) 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;
|
void updatePresetButtonLabel();
|
||||||
WaveformSelectorContents waveformContents;
|
void showPresetMenu();
|
||||||
|
void showCustomPresetWindow();
|
||||||
|
void handleLayerSelectionChanged();
|
||||||
|
|
||||||
|
juce::TextButton presetMenuButton;
|
||||||
|
int lastPresetIndex { -1 };
|
||||||
|
std::unique_ptr<CustomPresetWindow> customPresetWindow;
|
||||||
|
|
||||||
std::optional<ScopeSliderComponent> adsrComponent; // Amp Env
|
std::optional<ScopeSliderComponent> adsrComponent; // Amp Env
|
||||||
std::optional<ScopeSliderComponent> chorusComponent;
|
std::optional<ScopeSliderComponent> chorusComponent;
|
||||||
@@ -327,6 +404,7 @@ private:
|
|||||||
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
|
||||||
|
std::optional<ScopeSliderComponent> wtComponent; // Wavetable panel
|
||||||
|
|
||||||
MasterVolumeComponent masterLevelSlider;
|
MasterVolumeComponent masterLevelSlider;
|
||||||
juce::Label masterLevelLabel;
|
juce::Label masterLevelLabel;
|
||||||
@@ -336,6 +414,8 @@ private:
|
|||||||
std::unique_ptr<juce::AudioProcessorValueTreeState::SliderAttachment> gainAttachment;
|
std::unique_ptr<juce::AudioProcessorValueTreeState::SliderAttachment> gainAttachment;
|
||||||
|
|
||||||
ScopeComponent<float> mainScopeComponent;
|
ScopeComponent<float> mainScopeComponent;
|
||||||
|
juce::MidiKeyboardState keyboardState;
|
||||||
juce::Component blankPanel;
|
juce::MidiKeyboardComponent keyboardComponent;
|
||||||
|
juce::ComboBox layerSelector;
|
||||||
|
bool controllingLayerB { false };
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,13 +1,54 @@
|
|||||||
#include "PluginProcessor.h"
|
#include "PluginProcessor.h"
|
||||||
#include "PluginEditor.h"
|
#include "PluginEditor.h"
|
||||||
|
#include "WavetableOsc.h"
|
||||||
|
#include <array>
|
||||||
|
#include <map>
|
||||||
|
|
||||||
//==============================================================================
|
//==============================================================================
|
||||||
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)
|
||||||
|
, factoryPresets(makeFactoryPresets())
|
||||||
{
|
{
|
||||||
parameters.addParameterListener("waveform", this);
|
parameters.addParameterListener("wt_phase", this);
|
||||||
|
parameters.addParameterListener("wt_on", this);
|
||||||
|
parameters.addParameterListener("wt_morph", this);
|
||||||
|
parameters.addParameterListener("wt_bank", this);
|
||||||
|
parameters.addParameterListener("wt_lfoRate", this);
|
||||||
|
parameters.addParameterListener("wt_lfoDepth", this);
|
||||||
|
parameters.addParameterListener("wt_lfoShape", this);
|
||||||
|
parameters.addParameterListener("wt_level", this);
|
||||||
|
|
||||||
|
parameters.addParameterListener("wt2_phase", this);
|
||||||
|
parameters.addParameterListener("wt2_on", this);
|
||||||
|
parameters.addParameterListener("wt2_morph", this);
|
||||||
|
parameters.addParameterListener("wt2_bank", this);
|
||||||
|
parameters.addParameterListener("wt2_lfoRate", this);
|
||||||
|
parameters.addParameterListener("wt2_lfoDepth", this);
|
||||||
|
parameters.addParameterListener("wt2_lfoShape", this);
|
||||||
|
parameters.addParameterListener("wt2_level", this);
|
||||||
|
|
||||||
|
sp.wtPhase = parameters.getRawParameterValue("wt_phase");
|
||||||
|
sp.wtOn = parameters.getRawParameterValue("wt_on");
|
||||||
|
sp.wtMorph = parameters.getRawParameterValue("wt_morph");
|
||||||
|
sp.wtBank = parameters.getRawParameterValue("wt_bank");
|
||||||
|
sp.wtLfoRate = parameters.getRawParameterValue("wt_lfoRate");
|
||||||
|
sp.wtLfoDepth= parameters.getRawParameterValue("wt_lfoDepth");
|
||||||
|
sp.wtLfoShape= parameters.getRawParameterValue("wt_lfoShape");
|
||||||
|
sp.wtLevel = parameters.getRawParameterValue("wt_level");
|
||||||
|
|
||||||
|
sp.wt2Phase = parameters.getRawParameterValue("wt2_phase");
|
||||||
|
sp.wt2On = parameters.getRawParameterValue("wt2_on");
|
||||||
|
sp.wt2Morph = parameters.getRawParameterValue("wt2_morph");
|
||||||
|
sp.wt2Bank = parameters.getRawParameterValue("wt2_bank");
|
||||||
|
sp.wt2LfoRate= parameters.getRawParameterValue("wt2_lfoRate");
|
||||||
|
sp.wt2LfoDepth= parameters.getRawParameterValue("wt2_lfoDepth");
|
||||||
|
sp.wt2LfoShape= parameters.getRawParameterValue("wt2_lfoShape");
|
||||||
|
sp.wt2Level = parameters.getRawParameterValue("wt2_level");
|
||||||
|
|
||||||
|
if (! factoryPresets.empty())
|
||||||
|
applyPreset(0);
|
||||||
|
|
||||||
// === Per-panel bypass (default OFF) ===
|
// === Per-panel bypass (default OFF) ===
|
||||||
sp.chorusOn = parameters.getRawParameterValue("chorus_on");
|
sp.chorusOn = parameters.getRawParameterValue("chorus_on");
|
||||||
@@ -158,31 +199,28 @@ 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 });
|
||||||
|
const auto numChannels = (juce::uint32) juce::jmax (1, getTotalNumOutputChannels());
|
||||||
|
juce::dsp::ProcessSpec limiterSpec { sampleRate, (juce::uint32) samplesPerBlock, numChannels };
|
||||||
|
outputLimiter.reset();
|
||||||
|
outputLimiter.prepare (limiterSpec);
|
||||||
|
outputLimiter.setThreshold (-0.8f);
|
||||||
|
outputLimiter.setRelease (0.05f);
|
||||||
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);
|
|
||||||
|
|
||||||
if (newWaveform != -1) {
|
|
||||||
audioEngine.applyToVoices([newWaveform](NeuralSynthVoice* v)
|
|
||||||
{
|
|
||||||
v->changeWaveform(newWaveform);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
juce::ScopedNoDenormals noDenormals;
|
juce::ScopedNoDenormals noDenormals;
|
||||||
auto totalNumInputChannels = getTotalNumInputChannels();
|
auto totalNumInputChannels = getTotalNumInputChannels();
|
||||||
auto totalNumOutputChannels = getTotalNumOutputChannels();
|
auto totalNumOutputChannels = getTotalNumOutputChannels();
|
||||||
@@ -193,6 +231,8 @@ void NeuralSynthAudioProcessor::processBlock(juce::AudioSampleBuffer& buffer, ju
|
|||||||
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());
|
||||||
|
juce::dsp::AudioBlock<float> outputBlock (buffer);
|
||||||
|
outputLimiter.process (juce::dsp::ProcessContextReplacing<float> (outputBlock));
|
||||||
scopeDataCollector.process(buffer.getReadPointer(0), (size_t)buffer.getNumSamples());
|
scopeDataCollector.process(buffer.getReadPointer(0), (size_t)buffer.getNumSamples());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -209,10 +249,27 @@ void NeuralSynthAudioProcessor::getStateInformation (juce::MemoryBlock& 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)
|
||||||
|
{
|
||||||
|
if (id == "wt_bank" && ! presetChangeInProgress)
|
||||||
|
{
|
||||||
|
const int targetBank = juce::jlimit(0, (int)WT::FactoryLibrary::get().size() - 1,
|
||||||
|
(int) std::lround(newValue));
|
||||||
|
int matched = -1;
|
||||||
|
for (int i = 0; i < (int) factoryPresets.size(); ++i)
|
||||||
|
{
|
||||||
|
if (factoryPresets[(size_t)i].wtBankIndex == targetBank)
|
||||||
|
{
|
||||||
|
matched = i;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
currentPresetIndex = matched;
|
||||||
|
}
|
||||||
|
else if (id == "wt2_bank" && ! presetChangeInProgress)
|
||||||
{
|
{
|
||||||
juce::ignoreUnused(newValue);
|
juce::ignoreUnused(newValue);
|
||||||
if (id == "waveform")
|
currentPresetIndex = -1;
|
||||||
sp.waveform.store((int)newValue, std::memory_order_release);
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
//==============================================================================
|
//==============================================================================
|
||||||
@@ -230,13 +287,610 @@ void NeuralSynthAudioProcessor::buildParams(std::vector<std::unique_ptr<juce::Ra
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
std::vector<NeuralSynthAudioProcessor::PresetDefinition> NeuralSynthAudioProcessor::makeFactoryPresets()
|
||||||
|
{
|
||||||
|
std::vector<PresetDefinition> presets;
|
||||||
|
const auto& wtLibrary = WT::FactoryLibrary::get();
|
||||||
|
if (wtLibrary.empty())
|
||||||
|
return presets;
|
||||||
|
|
||||||
|
presets.reserve(220);
|
||||||
|
|
||||||
|
std::map<juce::String, std::vector<int>> categoryToIndices;
|
||||||
|
for (int i = 0; i < (int) wtLibrary.size(); ++i)
|
||||||
|
categoryToIndices[wtLibrary[(size_t) i].category].push_back(i);
|
||||||
|
|
||||||
|
const std::array<juce::String, 12> requestedCategories = {
|
||||||
|
"Electric Piano", "Organ", "Bass", "Drums", "Strings",
|
||||||
|
"Brass", "Choir", "Pad", "SFX", "Lead", "Pluck", "Misc"
|
||||||
|
};
|
||||||
|
|
||||||
|
static const std::array<std::pair<int, const char*>, 20> gmDrumInfo = {{
|
||||||
|
{35, "Acoustic Bass Drum"},
|
||||||
|
{36, "Bass Drum 1"},
|
||||||
|
{37, "Side Stick"},
|
||||||
|
{38, "Acoustic Snare"},
|
||||||
|
{39, "Hand Clap"},
|
||||||
|
{40, "Electric Snare"},
|
||||||
|
{41, "Low Floor Tom"},
|
||||||
|
{42, "Closed Hi-Hat"},
|
||||||
|
{43, "High Floor Tom"},
|
||||||
|
{44, "Pedal Hi-Hat"},
|
||||||
|
{45, "Low Tom"},
|
||||||
|
{46, "Open Hi-Hat"},
|
||||||
|
{47, "Low-Mid Tom"},
|
||||||
|
{48, "Hi-Mid Tom"},
|
||||||
|
{49, "Crash Cymbal 1"},
|
||||||
|
{50, "High Tom"},
|
||||||
|
{51, "Ride Cymbal 1"},
|
||||||
|
{52, "Chinese Cymbal"},
|
||||||
|
{53, "Ride Bell"},
|
||||||
|
{54, "Tambourine"}
|
||||||
|
}};
|
||||||
|
|
||||||
|
auto clampMorph = [](float v) { return juce::jlimit(0.0f, 15.0f, v); };
|
||||||
|
auto clampUnit = [](float v) { return juce::jlimit(0.0f, 1.0f, v); };
|
||||||
|
auto clampLfoDepth = [](float v) { return juce::jlimit(0.0f, 8.0f, v); };
|
||||||
|
auto clampLfoRate = [](float v) { return juce::jlimit(0.01f, 10.0f, v); };
|
||||||
|
auto clampCutoff = [](float v) { return juce::jlimit(20.0f, 20000.0f, v); };
|
||||||
|
auto clampRes = [](float v) { return juce::jlimit(0.1f, 10.0f, v); };
|
||||||
|
auto clampDb = [](float v) { return juce::jlimit(-24.0f, 24.0f, v); };
|
||||||
|
|
||||||
|
for (const auto& category : requestedCategories)
|
||||||
|
{
|
||||||
|
auto mapIt = categoryToIndices.find(category);
|
||||||
|
if (mapIt == categoryToIndices.end())
|
||||||
|
continue;
|
||||||
|
|
||||||
|
const auto& indices = mapIt->second;
|
||||||
|
if (indices.size() < 20)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
for (int variant = 0; variant < 20; ++variant)
|
||||||
|
{
|
||||||
|
const bool layered = variant >= 10;
|
||||||
|
const int primaryIndex = indices[(size_t) (variant % indices.size())];
|
||||||
|
const int secondaryIndex = layered ? indices[(size_t) ((variant + 5) % indices.size())]
|
||||||
|
: primaryIndex;
|
||||||
|
|
||||||
|
const float t = (float) variant / 19.0f;
|
||||||
|
const float subT = (float) (variant % 10) / 9.0f;
|
||||||
|
|
||||||
|
const bool isDrumCategory = (category == "Drums");
|
||||||
|
std::pair<int, const char*> gmInfo { 0, "" };
|
||||||
|
if (isDrumCategory)
|
||||||
|
gmInfo = gmDrumInfo[(size_t) variant];
|
||||||
|
|
||||||
|
std::map<juce::String, float> values;
|
||||||
|
auto set = [&](const juce::String& param, float value)
|
||||||
|
{
|
||||||
|
values[param] = value;
|
||||||
|
};
|
||||||
|
|
||||||
|
set("wt_on", 1.0f);
|
||||||
|
set("wt_bank", (float) primaryIndex);
|
||||||
|
set("wt_morph", clampMorph(2.0f + 8.0f * t));
|
||||||
|
set("wt_phase", 0.0f);
|
||||||
|
set("wt_lfoRate", clampLfoRate(0.25f + 0.8f * t));
|
||||||
|
set("wt_lfoDepth", clampLfoDepth(0.6f + 2.0f * t));
|
||||||
|
set("wt_lfoShape", (float) (variant % 4));
|
||||||
|
set("wt_level", clampUnit(layered ? 0.72f : 0.85f));
|
||||||
|
|
||||||
|
set("wt2_on", layered ? 1.0f : 0.0f);
|
||||||
|
set("wt2_bank", (float) secondaryIndex);
|
||||||
|
set("wt2_morph", layered ? clampMorph(4.0f + 6.0f * (1.0f - t)) : 0.0f);
|
||||||
|
set("wt2_phase", layered ? 0.25f : 0.0f);
|
||||||
|
set("wt2_lfoRate", clampLfoRate(layered ? (0.4f + 1.3f * (1.0f - t)) : 0.3f));
|
||||||
|
set("wt2_lfoDepth", clampLfoDepth(layered ? (1.0f + 2.0f * subT) : 0.0f));
|
||||||
|
set("wt2_lfoShape", layered ? (float) ((variant + 1) % 4) : 0.0f);
|
||||||
|
set("wt2_level", layered ? clampUnit(0.5f + 0.3f * t) : 0.0f);
|
||||||
|
|
||||||
|
set("chorus_on", 0.0f);
|
||||||
|
set("chorus_rate", clampUnit(0.3f));
|
||||||
|
set("chorus_depth", clampUnit(0.3f));
|
||||||
|
set("chorus_centre", clampUnit(0.5f));
|
||||||
|
set("chorus_feedback", clampUnit(0.12f));
|
||||||
|
set("chorus_mix", clampUnit(0.2f));
|
||||||
|
|
||||||
|
set("delay_on", 0.0f);
|
||||||
|
set("delay_delay", clampUnit(0.2f));
|
||||||
|
|
||||||
|
set("reverb_on", 0.0f);
|
||||||
|
set("reverb_roomSize", clampUnit(0.3f));
|
||||||
|
set("reverb_damping", clampUnit(0.5f));
|
||||||
|
set("reverb_wetLevel", clampUnit(0.18f));
|
||||||
|
set("reverb_dryLevel", clampUnit(0.85f));
|
||||||
|
set("reverb_width", clampUnit(0.9f));
|
||||||
|
set("reverb_freezeMode", 0.0f);
|
||||||
|
|
||||||
|
set("flanger_on", 0.0f);
|
||||||
|
set("flanger_rate", clampUnit(0.35f));
|
||||||
|
set("flanger_depth", 2.5f);
|
||||||
|
set("flanger_feedback", clampUnit(0.15f));
|
||||||
|
set("flanger_dryMix", clampUnit(0.25f));
|
||||||
|
set("flanger_phase", clampUnit(0.2f));
|
||||||
|
set("flanger_delay", 0.2f);
|
||||||
|
|
||||||
|
set("distortion_on", 0.0f);
|
||||||
|
set("distortion_drive", 10.0f);
|
||||||
|
set("distortion_mix", clampUnit(0.2f));
|
||||||
|
set("distortion_bias", 0.0f);
|
||||||
|
set("distortion_tone", juce::jlimit(100.0f, 8000.0f, 2400.0f));
|
||||||
|
set("distortion_shape", 0.0f);
|
||||||
|
|
||||||
|
set("filter_on", 0.0f);
|
||||||
|
set("filter_cutoff", clampCutoff(2000.0f));
|
||||||
|
set("filter_resonance", clampRes(0.7f));
|
||||||
|
set("filter_type", 0.0f);
|
||||||
|
set("filter_drive", 0.0f);
|
||||||
|
set("filter_mod", 0.0f);
|
||||||
|
set("filter_key", 0.0f);
|
||||||
|
|
||||||
|
set("adsr_attack", clampUnit(0.02f));
|
||||||
|
set("adsr_decay", clampUnit(0.3f));
|
||||||
|
set("adsr_sustain", clampUnit(0.7f));
|
||||||
|
set("adsr_release", clampUnit(0.4f));
|
||||||
|
|
||||||
|
set("fenv_attack", juce::jlimit(0.0f, 2.0f, 0.03f));
|
||||||
|
set("fenv_decay", juce::jlimit(0.0f, 2.0f, 0.3f));
|
||||||
|
set("fenv_sustain", clampUnit(0.5f));
|
||||||
|
set("fenv_release", juce::jlimit(0.0f, 4.0f, 0.4f));
|
||||||
|
set("fenv_amount", juce::jlimit(-1.0f, 1.0f, 0.2f));
|
||||||
|
|
||||||
|
set("eq_on", 1.0f);
|
||||||
|
set("lowEQ", clampDb(0.0f));
|
||||||
|
set("midEQ", clampDb(0.0f));
|
||||||
|
set("highEQ", clampDb(0.0f));
|
||||||
|
|
||||||
|
set("master", layered ? -8.0f : -6.0f);
|
||||||
|
|
||||||
|
if (category == "Electric Piano")
|
||||||
|
{
|
||||||
|
set("chorus_on", 1.0f);
|
||||||
|
set("chorus_rate", clampUnit(0.18f + 0.25f * t));
|
||||||
|
set("chorus_depth", clampUnit(0.35f + 0.2f * t));
|
||||||
|
set("chorus_mix", clampUnit(0.2f + 0.15f * t));
|
||||||
|
set("chorus_feedback", clampUnit(0.18f + 0.1f * t));
|
||||||
|
set("reverb_on", 1.0f);
|
||||||
|
set("reverb_roomSize", clampUnit(0.35f + 0.25f * t));
|
||||||
|
set("reverb_wetLevel", clampUnit(0.2f + 0.12f * t));
|
||||||
|
set("reverb_damping", clampUnit(0.45f + 0.1f * t));
|
||||||
|
set("filter_on", 1.0f);
|
||||||
|
set("filter_cutoff", clampCutoff(1500.0f + 700.0f * t));
|
||||||
|
set("filter_resonance", clampRes(0.8f + 0.3f * t));
|
||||||
|
set("fenv_amount", juce::jlimit(-1.0f, 1.0f, 0.35f + 0.15f * t));
|
||||||
|
set("adsr_attack", clampUnit(0.02f + 0.03f * t));
|
||||||
|
set("adsr_decay", clampUnit(0.25f + 0.1f * t));
|
||||||
|
set("adsr_sustain", clampUnit(0.65f + 0.1f * t));
|
||||||
|
set("adsr_release", clampUnit(0.45f + 0.18f * t));
|
||||||
|
set("wt_level", clampUnit(layered ? 0.7f : 0.83f));
|
||||||
|
if (layered)
|
||||||
|
{
|
||||||
|
set("wt2_level", clampUnit(0.55f + 0.2f * t));
|
||||||
|
set("wt2_lfoRate", clampLfoRate(0.35f + 0.9f * (1.0f - t)));
|
||||||
|
set("wt2_lfoDepth", clampLfoDepth(1.1f + 1.4f * t));
|
||||||
|
}
|
||||||
|
set("lowEQ", clampDb(1.5f));
|
||||||
|
set("midEQ", clampDb(-1.0f + 2.5f * t));
|
||||||
|
set("highEQ", clampDb(2.0f + 3.0f * t));
|
||||||
|
}
|
||||||
|
else if (category == "Organ")
|
||||||
|
{
|
||||||
|
set("chorus_on", 1.0f);
|
||||||
|
set("chorus_rate", clampUnit(0.45f + 0.15f * t));
|
||||||
|
set("chorus_depth", clampUnit(0.3f + 0.1f * t));
|
||||||
|
set("chorus_mix", clampUnit(0.33f + 0.08f * t));
|
||||||
|
set("chorus_feedback", clampUnit(0.15f + 0.05f * t));
|
||||||
|
set("reverb_on", 1.0f);
|
||||||
|
set("reverb_roomSize", clampUnit(0.25f + 0.2f * t));
|
||||||
|
set("reverb_wetLevel", clampUnit(0.16f + 0.1f * t));
|
||||||
|
set("reverb_damping", clampUnit(0.4f + 0.15f * t));
|
||||||
|
set("filter_on", 0.0f);
|
||||||
|
set("adsr_attack", clampUnit(0.01f));
|
||||||
|
set("adsr_decay", clampUnit(0.3f));
|
||||||
|
set("adsr_sustain", clampUnit(1.0f));
|
||||||
|
set("adsr_release", clampUnit(0.3f + 0.2f * t));
|
||||||
|
set("fenv_amount", 0.0f);
|
||||||
|
set("wt_morph", clampMorph(3.0f + 5.0f * t));
|
||||||
|
set("wt_lfoRate", clampLfoRate(0.4f + 0.45f * t));
|
||||||
|
set("wt_lfoDepth", clampLfoDepth(0.25f + 0.35f * t));
|
||||||
|
if (layered)
|
||||||
|
{
|
||||||
|
set("wt2_level", clampUnit(0.5f + 0.2f * t));
|
||||||
|
set("wt2_lfoDepth", clampLfoDepth(0.4f + 0.5f * t));
|
||||||
|
set("distortion_on", variant >= 15 ? 1.0f : 0.0f);
|
||||||
|
if (variant >= 15)
|
||||||
|
{
|
||||||
|
set("distortion_drive", 9.0f + 5.0f * t);
|
||||||
|
set("distortion_mix", clampUnit(0.18f + 0.12f * t));
|
||||||
|
set("distortion_shape", 1.0f);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
set("midEQ", clampDb(1.5f + 1.0f * t));
|
||||||
|
set("highEQ", clampDb(2.0f + 1.5f * t));
|
||||||
|
set("master", layered ? -8.0f : -5.0f);
|
||||||
|
}
|
||||||
|
else if (category == "Bass")
|
||||||
|
{
|
||||||
|
set("wt_morph", clampMorph(1.5f + 5.5f * t));
|
||||||
|
set("wt_lfoRate", clampLfoRate(0.2f + 0.6f * t));
|
||||||
|
set("wt_lfoDepth", clampLfoDepth(0.5f + 1.3f * t));
|
||||||
|
set("wt_level", clampUnit(0.92f));
|
||||||
|
set("adsr_attack", clampUnit(0.005f));
|
||||||
|
set("adsr_decay", clampUnit(0.18f + 0.08f * t));
|
||||||
|
set("adsr_sustain", clampUnit(0.45f - 0.15f * t));
|
||||||
|
set("adsr_release", clampUnit(0.22f + 0.12f * t));
|
||||||
|
set("filter_on", 1.0f);
|
||||||
|
set("filter_cutoff", clampCutoff(180.0f + 720.0f * t));
|
||||||
|
set("filter_resonance", clampRes(0.85f + 0.35f * t));
|
||||||
|
set("fenv_amount", juce::jlimit(-1.0f, 1.0f, 0.6f + 0.25f * t));
|
||||||
|
set("distortion_on", (layered || variant >= 6) ? 1.0f : 0.0f);
|
||||||
|
if (values["distortion_on"] > 0.5f)
|
||||||
|
{
|
||||||
|
set("distortion_drive", 14.0f + 8.0f * t);
|
||||||
|
set("distortion_mix", clampUnit(0.25f + 0.25f * t));
|
||||||
|
set("distortion_shape", 2.0f);
|
||||||
|
}
|
||||||
|
set("lowEQ", clampDb(3.5f + 2.0f * t));
|
||||||
|
set("midEQ", clampDb(-2.5f + 1.8f * t));
|
||||||
|
set("highEQ", clampDb(-5.0f + 2.0f * t));
|
||||||
|
if (layered)
|
||||||
|
{
|
||||||
|
set("wt2_level", clampUnit(0.6f + 0.2f * t));
|
||||||
|
set("wt2_lfoDepth", clampLfoDepth(0.8f + 1.6f * t));
|
||||||
|
set("wt2_lfoRate", clampLfoRate(0.3f + 0.7f * t));
|
||||||
|
}
|
||||||
|
set("master", layered ? -7.5f : -5.0f);
|
||||||
|
}
|
||||||
|
else if (category == "Drums")
|
||||||
|
{
|
||||||
|
set("wt_morph", clampMorph(1.0f + 9.0f * t));
|
||||||
|
set("wt_lfoRate", clampLfoRate(1.2f + 2.5f * t));
|
||||||
|
set("wt_lfoDepth", clampLfoDepth(0.3f + 2.8f * t));
|
||||||
|
set("wt_level", clampUnit(0.88f));
|
||||||
|
set("adsr_attack", clampUnit(0.001f));
|
||||||
|
set("adsr_decay", clampUnit(0.12f + 0.08f * t));
|
||||||
|
set("adsr_sustain", clampUnit(0.05f));
|
||||||
|
set("adsr_release", clampUnit(0.18f + 0.12f * t));
|
||||||
|
set("fenv_amount", juce::jlimit(-1.0f, 1.0f, -0.2f + 0.4f * t));
|
||||||
|
set("distortion_on", 1.0f);
|
||||||
|
set("distortion_drive", 16.0f + 10.0f * t);
|
||||||
|
set("distortion_mix", clampUnit(0.35f + 0.2f * t));
|
||||||
|
set("distortion_shape", (float) (variant % 3));
|
||||||
|
set("distortion_bias", juce::jlimit(-1.0f, 1.0f, 0.05f * (variant % 5)));
|
||||||
|
set("reverb_on", layered ? 1.0f : (variant >= 8 ? 1.0f : 0.0f));
|
||||||
|
if (values["reverb_on"] > 0.5f)
|
||||||
|
{
|
||||||
|
set("reverb_roomSize", clampUnit(layered ? (0.45f + 0.3f * t) : 0.35f + 0.15f * t));
|
||||||
|
set("reverb_wetLevel", clampUnit(layered ? (0.3f + 0.18f * t) : 0.18f + 0.1f * t));
|
||||||
|
set("reverb_damping", clampUnit(0.45f + 0.25f * t));
|
||||||
|
}
|
||||||
|
set("filter_on", layered ? 1.0f : 0.0f);
|
||||||
|
if (values["filter_on"] > 0.5f)
|
||||||
|
{
|
||||||
|
set("filter_cutoff", clampCutoff(800.0f + 1200.0f * t));
|
||||||
|
set("filter_resonance", clampRes(1.0f + 0.6f * t));
|
||||||
|
set("fenv_amount", juce::jlimit(-1.0f, 1.0f, 0.1f + 0.35f * t));
|
||||||
|
}
|
||||||
|
if (layered)
|
||||||
|
{
|
||||||
|
set("wt2_level", clampUnit(0.6f + 0.25f * t));
|
||||||
|
set("wt2_lfoRate", clampLfoRate(0.9f + 1.6f * (1.0f - t)));
|
||||||
|
set("wt2_lfoDepth", clampLfoDepth(1.3f + 1.7f * t));
|
||||||
|
}
|
||||||
|
set("lowEQ", clampDb(4.0f + 2.0f * t));
|
||||||
|
set("midEQ", clampDb(-4.0f + 3.0f * t));
|
||||||
|
set("highEQ", clampDb(3.0f + 4.0f * t));
|
||||||
|
set("master", layered ? -8.5f : -6.5f);
|
||||||
|
}
|
||||||
|
else if (category == "Strings")
|
||||||
|
{
|
||||||
|
set("wt_morph", clampMorph(3.0f + 9.0f * t));
|
||||||
|
set("wt_lfoRate", clampLfoRate(0.18f + 0.4f * t));
|
||||||
|
set("wt_lfoDepth", clampLfoDepth(0.8f + 1.8f * t));
|
||||||
|
set("adsr_attack", clampUnit(0.22f + 0.18f * t));
|
||||||
|
set("adsr_decay", clampUnit(0.3f + 0.12f * t));
|
||||||
|
set("adsr_sustain", clampUnit(0.85f));
|
||||||
|
set("adsr_release", clampUnit(0.6f + 0.25f * t));
|
||||||
|
set("chorus_on", 1.0f);
|
||||||
|
set("chorus_rate", clampUnit(0.25f + 0.1f * t));
|
||||||
|
set("chorus_depth", clampUnit(0.45f + 0.15f * t));
|
||||||
|
set("chorus_mix", clampUnit(0.3f + 0.1f * t));
|
||||||
|
set("reverb_on", 1.0f);
|
||||||
|
set("reverb_roomSize", clampUnit(0.55f + 0.2f * t));
|
||||||
|
set("reverb_wetLevel", clampUnit(0.35f + 0.15f * t));
|
||||||
|
set("filter_on", 1.0f);
|
||||||
|
set("filter_cutoff", clampCutoff(2600.0f + 1500.0f * t));
|
||||||
|
set("filter_resonance", clampRes(0.85f + 0.25f * t));
|
||||||
|
set("fenv_amount", juce::jlimit(-1.0f, 1.0f, 0.25f + 0.1f * t));
|
||||||
|
if (layered)
|
||||||
|
{
|
||||||
|
set("wt2_level", clampUnit(0.5f + 0.25f * t));
|
||||||
|
set("wt2_lfoDepth", clampLfoDepth(1.0f + 1.5f * t));
|
||||||
|
}
|
||||||
|
set("midEQ", clampDb(-1.0f + 2.0f * t));
|
||||||
|
set("highEQ", clampDb(2.5f + 2.5f * t));
|
||||||
|
set("master", -7.5f);
|
||||||
|
}
|
||||||
|
else if (category == "Brass")
|
||||||
|
{
|
||||||
|
set("adsr_attack", clampUnit(0.05f + 0.05f * t));
|
||||||
|
set("adsr_decay", clampUnit(0.25f + 0.1f * t));
|
||||||
|
set("adsr_sustain", clampUnit(0.75f));
|
||||||
|
set("adsr_release", clampUnit(0.35f + 0.15f * t));
|
||||||
|
set("filter_on", 1.0f);
|
||||||
|
set("filter_cutoff", clampCutoff(2200.0f + 1800.0f * t));
|
||||||
|
set("filter_resonance", clampRes(1.0f + 0.3f * t));
|
||||||
|
set("fenv_amount", juce::jlimit(-1.0f, 1.0f, 0.55f + 0.25f * t));
|
||||||
|
set("reverb_on", 1.0f);
|
||||||
|
set("reverb_roomSize", clampUnit(0.4f + 0.25f * t));
|
||||||
|
set("reverb_wetLevel", clampUnit(0.3f + 0.1f * t));
|
||||||
|
set("distortion_on", (layered || variant >= 8) ? 1.0f : 0.0f);
|
||||||
|
if (values["distortion_on"] > 0.5f)
|
||||||
|
{
|
||||||
|
set("distortion_drive", 10.0f + 6.0f * t);
|
||||||
|
set("distortion_mix", clampUnit(0.18f + 0.18f * t));
|
||||||
|
set("distortion_shape", 1.0f);
|
||||||
|
}
|
||||||
|
set("flanger_on", layered ? 1.0f : 0.0f);
|
||||||
|
if (values["flanger_on"] > 0.5f)
|
||||||
|
{
|
||||||
|
set("flanger_rate", clampUnit(0.35f + 0.25f * t));
|
||||||
|
set("flanger_depth", 4.0f + 3.0f * t);
|
||||||
|
set("flanger_dryMix", clampUnit(0.5f));
|
||||||
|
}
|
||||||
|
if (layered)
|
||||||
|
{
|
||||||
|
set("wt2_level", clampUnit(0.52f + 0.22f * t));
|
||||||
|
set("wt2_lfoDepth", clampLfoDepth(0.8f + 1.4f * t));
|
||||||
|
}
|
||||||
|
set("lowEQ", clampDb(2.0f + 1.0f * t));
|
||||||
|
set("midEQ", clampDb(1.0f + 1.5f * t));
|
||||||
|
set("highEQ", clampDb(1.0f + 2.5f * t));
|
||||||
|
}
|
||||||
|
else if (category == "Choir")
|
||||||
|
{
|
||||||
|
set("adsr_attack", clampUnit(0.3f + 0.2f * t));
|
||||||
|
set("adsr_decay", clampUnit(0.4f + 0.1f * t));
|
||||||
|
set("adsr_sustain", clampUnit(0.9f));
|
||||||
|
set("adsr_release", clampUnit(0.55f + 0.3f * t));
|
||||||
|
set("chorus_on", 1.0f);
|
||||||
|
set("chorus_rate", clampUnit(0.2f + 0.15f * t));
|
||||||
|
set("chorus_depth", clampUnit(0.5f + 0.2f * t));
|
||||||
|
set("chorus_mix", clampUnit(0.35f + 0.15f * t));
|
||||||
|
set("reverb_on", 1.0f);
|
||||||
|
set("reverb_roomSize", clampUnit(0.6f + 0.25f * t));
|
||||||
|
set("reverb_wetLevel", clampUnit(0.4f + 0.15f * t));
|
||||||
|
set("filter_on", 1.0f);
|
||||||
|
set("filter_cutoff", clampCutoff(1800.0f + 800.0f * t));
|
||||||
|
set("filter_resonance", clampRes(0.7f + 0.2f * t));
|
||||||
|
set("fenv_amount", juce::jlimit(-1.0f, 1.0f, 0.15f + 0.08f * t));
|
||||||
|
set("wt_lfoDepth", clampLfoDepth(1.0f + 2.0f * t));
|
||||||
|
if (layered)
|
||||||
|
{
|
||||||
|
set("wt2_level", clampUnit(0.48f + 0.2f * t));
|
||||||
|
set("wt2_lfoDepth", clampLfoDepth(1.2f + 1.5f * t));
|
||||||
|
}
|
||||||
|
set("midEQ", clampDb(-1.5f + 1.5f * t));
|
||||||
|
set("highEQ", clampDb(3.0f + 2.0f * t));
|
||||||
|
set("master", -7.5f);
|
||||||
|
}
|
||||||
|
else if (category == "Pad")
|
||||||
|
{
|
||||||
|
set("adsr_attack", clampUnit(0.35f + 0.25f * t));
|
||||||
|
set("adsr_decay", clampUnit(0.35f + 0.15f * t));
|
||||||
|
set("adsr_sustain", clampUnit(0.85f));
|
||||||
|
set("adsr_release", clampUnit(0.7f + 0.35f * t));
|
||||||
|
set("chorus_on", 1.0f);
|
||||||
|
set("chorus_rate", clampUnit(0.18f + 0.12f * t));
|
||||||
|
set("chorus_depth", clampUnit(0.55f + 0.2f * t));
|
||||||
|
set("chorus_mix", clampUnit(0.35f + 0.15f * t));
|
||||||
|
set("reverb_on", 1.0f);
|
||||||
|
set("reverb_roomSize", clampUnit(0.65f + 0.25f * t));
|
||||||
|
set("reverb_wetLevel", clampUnit(0.4f + 0.2f * t));
|
||||||
|
set("filter_on", 1.0f);
|
||||||
|
set("filter_cutoff", clampCutoff(1500.0f + 900.0f * t));
|
||||||
|
set("filter_resonance", clampRes(0.8f + 0.25f * t));
|
||||||
|
set("fenv_amount", juce::jlimit(-1.0f, 1.0f, 0.2f + 0.12f * t));
|
||||||
|
set("flanger_on", (layered || variant % 3 == 0) ? 1.0f : 0.0f);
|
||||||
|
if (values["flanger_on"] > 0.5f)
|
||||||
|
{
|
||||||
|
set("flanger_rate", clampUnit(0.25f + 0.15f * t));
|
||||||
|
set("flanger_depth", 5.0f + 3.0f * t);
|
||||||
|
set("flanger_dryMix", clampUnit(0.4f));
|
||||||
|
}
|
||||||
|
set("wt_lfoRate", clampLfoRate(0.15f + 0.35f * t));
|
||||||
|
set("wt_lfoDepth", clampLfoDepth(1.5f + 2.5f * t));
|
||||||
|
if (layered)
|
||||||
|
{
|
||||||
|
set("wt2_level", clampUnit(0.5f + 0.3f * t));
|
||||||
|
set("wt2_lfoDepth", clampLfoDepth(1.3f + 2.0f * t));
|
||||||
|
}
|
||||||
|
set("master", -8.5f);
|
||||||
|
}
|
||||||
|
else if (category == "SFX")
|
||||||
|
{
|
||||||
|
set("wt_morph", clampMorph(4.0f + 10.0f * t));
|
||||||
|
set("wt_lfoRate", clampLfoRate(1.2f + 3.5f * t));
|
||||||
|
set("wt_lfoDepth", clampLfoDepth(2.5f + 5.0f * t));
|
||||||
|
set("wt_lfoShape", (float) (variant % 4));
|
||||||
|
set("wt_phase", (variant % 2 == 0) ? 0.0f : 0.5f);
|
||||||
|
set("chorus_on", 1.0f);
|
||||||
|
set("chorus_rate", clampUnit(0.35f + 0.3f * t));
|
||||||
|
set("chorus_depth", clampUnit(0.45f + 0.25f * t));
|
||||||
|
set("chorus_mix", clampUnit(0.3f + 0.2f * t));
|
||||||
|
set("flanger_on", 1.0f);
|
||||||
|
set("flanger_rate", clampUnit(0.45f + 0.4f * t));
|
||||||
|
set("flanger_depth", 6.0f + 4.0f * t);
|
||||||
|
set("flanger_phase", clampUnit(0.2f + 0.5f * t));
|
||||||
|
set("flanger_dryMix", clampUnit(0.5f));
|
||||||
|
set("distortion_on", 1.0f);
|
||||||
|
set("distortion_drive", 18.0f + 7.0f * t);
|
||||||
|
set("distortion_mix", clampUnit(0.35f + 0.2f * t));
|
||||||
|
set("distortion_shape", (float) ((variant + 1) % 3));
|
||||||
|
set("distortion_bias", juce::jlimit(-1.0f, 1.0f, -0.2f + 0.4f * t));
|
||||||
|
set("reverb_on", 1.0f);
|
||||||
|
set("reverb_roomSize", clampUnit(0.7f + 0.2f * t));
|
||||||
|
set("reverb_wetLevel", clampUnit(0.45f + 0.2f * t));
|
||||||
|
if (layered)
|
||||||
|
{
|
||||||
|
set("wt2_level", clampUnit(0.6f + 0.25f * t));
|
||||||
|
set("wt2_morph", clampMorph(6.0f + 7.0f * (1.0f - t)));
|
||||||
|
set("wt2_lfoRate", clampLfoRate(0.8f + 4.0f * (1.0f - t)));
|
||||||
|
set("wt2_lfoDepth", clampLfoDepth(2.0f + 4.0f * t));
|
||||||
|
set("wt2_lfoShape", (float) ((variant + 2) % 4));
|
||||||
|
}
|
||||||
|
set("lowEQ", clampDb(-4.0f + 4.0f * t));
|
||||||
|
set("midEQ", clampDb(3.0f - 4.0f * t));
|
||||||
|
set("highEQ", clampDb(6.0f + 4.0f * t));
|
||||||
|
set("master", -12.0f);
|
||||||
|
}
|
||||||
|
else if (category == "Lead")
|
||||||
|
{
|
||||||
|
set("wt_morph", clampMorph(3.0f + 8.0f * t));
|
||||||
|
set("wt_lfoRate", clampLfoRate(0.4f + 1.2f * t));
|
||||||
|
set("wt_lfoDepth", clampLfoDepth(1.0f + 2.0f * t));
|
||||||
|
set("adsr_attack", clampUnit(0.01f + 0.02f * t));
|
||||||
|
set("adsr_decay", clampUnit(0.18f + 0.08f * t));
|
||||||
|
set("adsr_sustain", clampUnit(0.85f));
|
||||||
|
set("adsr_release", clampUnit(0.25f + 0.1f * t));
|
||||||
|
set("filter_on", 1.0f);
|
||||||
|
set("filter_cutoff", clampCutoff(2300.0f + 3200.0f * t));
|
||||||
|
set("filter_resonance", clampRes(0.9f + 0.25f * t));
|
||||||
|
set("fenv_amount", juce::jlimit(-1.0f, 1.0f, 0.45f + 0.25f * t));
|
||||||
|
set("distortion_on", 1.0f);
|
||||||
|
set("distortion_drive", 12.0f + 10.0f * t);
|
||||||
|
set("distortion_mix", clampUnit(0.25f + 0.25f * t));
|
||||||
|
set("distortion_shape", (float) ((variant % 2) + 1));
|
||||||
|
set("chorus_on", (layered || variant % 3 == 0) ? 1.0f : 0.0f);
|
||||||
|
if (values["chorus_on"] > 0.5f)
|
||||||
|
{
|
||||||
|
set("chorus_rate", clampUnit(0.3f + 0.2f * t));
|
||||||
|
set("chorus_depth", clampUnit(0.4f + 0.2f * t));
|
||||||
|
set("chorus_mix", clampUnit(0.25f + 0.15f * t));
|
||||||
|
}
|
||||||
|
set("reverb_on", 1.0f);
|
||||||
|
set("reverb_roomSize", clampUnit(0.35f + 0.25f * t));
|
||||||
|
set("reverb_wetLevel", clampUnit(0.22f + 0.15f * t));
|
||||||
|
if (layered)
|
||||||
|
{
|
||||||
|
set("wt2_level", clampUnit(0.52f + 0.22f * t));
|
||||||
|
set("wt2_lfoDepth", clampLfoDepth(1.1f + 1.6f * t));
|
||||||
|
}
|
||||||
|
set("lowEQ", clampDb(-1.5f + 1.5f * t));
|
||||||
|
set("midEQ", clampDb(2.0f + 1.5f * t));
|
||||||
|
set("highEQ", clampDb(4.0f + 3.0f * t));
|
||||||
|
set("master", layered ? -7.5f : -6.0f);
|
||||||
|
}
|
||||||
|
else if (category == "Pluck")
|
||||||
|
{
|
||||||
|
set("wt_morph", clampMorph(2.5f + 7.0f * t));
|
||||||
|
set("wt_lfoRate", clampLfoRate(0.25f + 0.7f * t));
|
||||||
|
set("wt_lfoDepth", clampLfoDepth(0.6f + 1.8f * t));
|
||||||
|
set("adsr_attack", clampUnit(0.005f));
|
||||||
|
set("adsr_decay", clampUnit(0.22f + 0.15f * t));
|
||||||
|
set("adsr_sustain", clampUnit(0.2f + 0.1f * t));
|
||||||
|
set("adsr_release", clampUnit(0.25f + 0.1f * t));
|
||||||
|
set("fenv_amount", juce::jlimit(-1.0f, 1.0f, 0.4f + 0.2f * t));
|
||||||
|
set("filter_on", 1.0f);
|
||||||
|
set("filter_cutoff", clampCutoff(1800.0f + 2000.0f * t));
|
||||||
|
set("filter_resonance", clampRes(1.1f + 0.3f * t));
|
||||||
|
set("delay_on", 1.0f);
|
||||||
|
set("delay_delay", clampUnit(0.25f + 0.25f * t));
|
||||||
|
set("reverb_on", 1.0f);
|
||||||
|
set("reverb_roomSize", clampUnit(0.45f + 0.25f * t));
|
||||||
|
set("reverb_wetLevel", clampUnit(0.25f + 0.15f * t));
|
||||||
|
set("chorus_on", 0.0f);
|
||||||
|
set("distortion_on", variant >= 12 ? 1.0f : 0.0f);
|
||||||
|
if (values["distortion_on"] > 0.5f)
|
||||||
|
{
|
||||||
|
set("distortion_drive", 10.0f + 8.0f * t);
|
||||||
|
set("distortion_mix", clampUnit(0.2f + 0.2f * t));
|
||||||
|
set("distortion_shape", 2.0f);
|
||||||
|
}
|
||||||
|
if (layered)
|
||||||
|
{
|
||||||
|
set("wt2_level", clampUnit(0.48f + 0.25f * t));
|
||||||
|
set("wt2_lfoDepth", clampLfoDepth(0.9f + 1.5f * t));
|
||||||
|
}
|
||||||
|
set("lowEQ", clampDb(-1.0f + 2.0f * t));
|
||||||
|
set("midEQ", clampDb(-2.0f + 3.0f * t));
|
||||||
|
set("highEQ", clampDb(3.0f + 2.5f * t));
|
||||||
|
}
|
||||||
|
|
||||||
|
juce::String presetName;
|
||||||
|
if (isDrumCategory)
|
||||||
|
{
|
||||||
|
presetName = juce::String::formatted("GM %d %s%s",
|
||||||
|
gmInfo.first,
|
||||||
|
gmInfo.second,
|
||||||
|
layered ? " Stack" : "");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
presetName = category + (layered ? " Duo " : " Solo ")
|
||||||
|
+ juce::String(variant + 1).paddedLeft('0', 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
auto scaleLevel = [&](const juce::String& paramId, float factor)
|
||||||
|
{
|
||||||
|
auto it = values.find(paramId);
|
||||||
|
if (it != values.end())
|
||||||
|
it->second = juce::jlimit(0.0f, 1.0f, it->second * factor);
|
||||||
|
};
|
||||||
|
|
||||||
|
scaleLevel("wt_level", 0.75f);
|
||||||
|
scaleLevel("wt2_level", 0.75f);
|
||||||
|
|
||||||
|
if (auto it = values.find("master"); it != values.end())
|
||||||
|
it->second = juce::jlimit(-24.0f, 24.0f, it->second - 4.0f);
|
||||||
|
|
||||||
|
PresetDefinition def;
|
||||||
|
def.category = category;
|
||||||
|
def.name = presetName.trim();
|
||||||
|
def.wtBankIndex = primaryIndex;
|
||||||
|
def.wt2BankIndex = layered ? secondaryIndex : -1;
|
||||||
|
def.parameterValues.reserve(values.size());
|
||||||
|
for (const auto& entry : values)
|
||||||
|
def.parameterValues.emplace_back(entry.first, entry.second);
|
||||||
|
|
||||||
|
presets.push_back(std::move(def));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return presets;
|
||||||
|
}
|
||||||
|
|
||||||
|
void NeuralSynthAudioProcessor::setParameterValue(const juce::String& paramID, float value)
|
||||||
|
{
|
||||||
|
if (parameters.getParameter(paramID) != nullptr)
|
||||||
|
parameters.getParameterAsValue(paramID) = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
void NeuralSynthAudioProcessor::applyPreset(int index)
|
||||||
|
{
|
||||||
|
if (factoryPresets.empty())
|
||||||
|
return;
|
||||||
|
|
||||||
|
index = juce::jlimit(0, (int) factoryPresets.size() - 1, index);
|
||||||
|
const auto& preset = factoryPresets[(size_t) index];
|
||||||
|
|
||||||
|
juce::ScopedValueSetter<bool> guard(presetChangeInProgress, true, false);
|
||||||
|
|
||||||
|
for (const auto& entry : preset.parameterValues)
|
||||||
|
setParameterValue(entry.first, entry.second);
|
||||||
|
|
||||||
|
currentPresetIndex = index;
|
||||||
|
}
|
||||||
|
|
||||||
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::AudioParameterBool>(
|
||||||
"waveform", "Waveform",
|
"wt_on", "Layer A On", true));
|
||||||
juce::StringArray{ "Sine", "Saw", "Square", "Triangle" }, 0));
|
params.push_back(std::make_unique<juce::AudioParameterBool>(
|
||||||
|
"wt2_on", "Layer B On", false));
|
||||||
|
|
||||||
// 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));
|
||||||
@@ -255,9 +909,11 @@ juce::AudioProcessorValueTreeState::ParameterLayout NeuralSynthAudioProcessor::c
|
|||||||
buildParams(params, "flanger");
|
buildParams(params, "flanger");
|
||||||
buildParams(params, "distortion");
|
buildParams(params, "distortion");
|
||||||
buildParams(params, "filter");
|
buildParams(params, "filter");
|
||||||
|
buildParams(params, "wt");
|
||||||
|
buildParams(params, "wt2");
|
||||||
|
|
||||||
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), -6.0f));
|
||||||
|
|
||||||
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));
|
||||||
|
|||||||
@@ -12,6 +12,15 @@ class NeuralSynthAudioProcessor : public juce::AudioProcessor,
|
|||||||
private juce::AudioProcessorValueTreeState::Listener
|
private juce::AudioProcessorValueTreeState::Listener
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
|
struct PresetDefinition
|
||||||
|
{
|
||||||
|
juce::String category;
|
||||||
|
juce::String name;
|
||||||
|
int wtBankIndex;
|
||||||
|
int wt2BankIndex { -1 };
|
||||||
|
std::vector<std::pair<juce::String, float>> parameterValues;
|
||||||
|
};
|
||||||
|
|
||||||
NeuralSynthAudioProcessor();
|
NeuralSynthAudioProcessor();
|
||||||
~NeuralSynthAudioProcessor() override;
|
~NeuralSynthAudioProcessor() override;
|
||||||
|
|
||||||
@@ -53,6 +62,10 @@ public:
|
|||||||
const std::string& paramGroup);
|
const std::string& paramGroup);
|
||||||
juce::AudioProcessorValueTreeState::ParameterLayout createParameterLayout();
|
juce::AudioProcessorValueTreeState::ParameterLayout createParameterLayout();
|
||||||
|
|
||||||
|
void applyPreset(int index);
|
||||||
|
const std::vector<PresetDefinition>& getFactoryPresets() const noexcept { return factoryPresets; }
|
||||||
|
int getCurrentPresetIndex() const noexcept { return currentPresetIndex; }
|
||||||
|
|
||||||
// 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; }
|
||||||
@@ -87,4 +100,13 @@ private:
|
|||||||
|
|
||||||
// Scope collector (uses audioBufferQueue, so declare after it)
|
// Scope collector (uses audioBufferQueue, so declare after it)
|
||||||
ScopeDataCollector<float> scopeDataCollector { audioBufferQueue };
|
ScopeDataCollector<float> scopeDataCollector { audioBufferQueue };
|
||||||
|
|
||||||
|
std::vector<PresetDefinition> factoryPresets;
|
||||||
|
int currentPresetIndex { -1 };
|
||||||
|
bool presetChangeInProgress { false };
|
||||||
|
|
||||||
|
static std::vector<PresetDefinition> makeFactoryPresets();
|
||||||
|
void setParameterValue(const juce::String& paramID, float value);
|
||||||
|
|
||||||
|
juce::dsp::Limiter<float> outputLimiter;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,5 +1,13 @@
|
|||||||
#include "SynthVoice.h"
|
#include "SynthVoice.h"
|
||||||
|
|
||||||
#include <cmath>
|
#include <cmath>
|
||||||
|
#include "SynthVoice/ADSR.h"
|
||||||
|
#include "SynthVoice/Chorus.h"
|
||||||
|
#include "SynthVoice/Distortion.h"
|
||||||
|
#include "SynthVoice/EQ.h"
|
||||||
|
#include "SynthVoice/Flanger.h"
|
||||||
|
#include "SynthVoice/Reverb.h"
|
||||||
|
#include "SynthVoice/SimpleDelay.h"
|
||||||
|
|
||||||
//==============================================================================
|
//==============================================================================
|
||||||
|
|
||||||
@@ -14,7 +22,24 @@ void NeuralSynthVoice::prepare (const juce::dsp::ProcessSpec& newSpec)
|
|||||||
|
|
||||||
// --- Oscillator
|
// --- Oscillator
|
||||||
osc.prepare (spec.sampleRate);
|
osc.prepare (spec.sampleRate);
|
||||||
setWaveform (0); // default to sine
|
osc.setWave (BlepWave::Sine);
|
||||||
|
|
||||||
|
// --- Wavetable oscillator factory banks ---
|
||||||
|
wtOsc.prepare (spec.sampleRate);
|
||||||
|
morphLfo.prepare (spec.sampleRate);
|
||||||
|
currentWtBankIndex = -1;
|
||||||
|
wtOsc2.prepare (spec.sampleRate);
|
||||||
|
morphLfo2.prepare (spec.sampleRate);
|
||||||
|
currentWtBankIndex2 = -1;
|
||||||
|
|
||||||
|
const auto& library = WT::FactoryLibrary::get();
|
||||||
|
if (! library.empty())
|
||||||
|
{
|
||||||
|
wtOsc.setBank (library.front().bank);
|
||||||
|
currentWtBankIndex = 0;
|
||||||
|
wtOsc2.setBank (library.front().bank);
|
||||||
|
currentWtBankIndex2 = 0;
|
||||||
|
}
|
||||||
|
|
||||||
// --- 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,
|
||||||
@@ -23,6 +48,10 @@ void NeuralSynthVoice::prepare (const juce::dsp::ProcessSpec& newSpec)
|
|||||||
|
|
||||||
// --- Prepare chain elements
|
// --- Prepare chain elements
|
||||||
chain.prepare (spec);
|
chain.prepare (spec);
|
||||||
|
chain.get<masterIndex>().setRampDurationSeconds (0.02f);
|
||||||
|
chain.get<limiterIndex>().setThreshold (-1.0f);
|
||||||
|
chain.get<limiterIndex>().setRelease (0.05f);
|
||||||
|
chain.get<limiterIndex>().reset();
|
||||||
|
|
||||||
// Set maximum delay sizes BEFORE runtime changes
|
// Set maximum delay sizes BEFORE runtime changes
|
||||||
{
|
{
|
||||||
@@ -73,234 +102,120 @@ void NeuralSynthVoice::renderNextBlock (juce::AudioBuffer<float>& outputBuffer,
|
|||||||
if (! adsr.isActive())
|
if (! adsr.isActive())
|
||||||
clearCurrentNote();
|
clearCurrentNote();
|
||||||
|
|
||||||
// Apply pending waveform change (from GUI / processor thread)
|
// --- Generate oscillator into temp buffer (BLEP or Wavetable)
|
||||||
const int wf = pendingWaveform.exchange (-1, std::memory_order_acq_rel);
|
|
||||||
if (wf != -1)
|
|
||||||
setWaveform (wf);
|
|
||||||
|
|
||||||
// --- 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());
|
||||||
|
|
||||||
|
const auto& library = WT::FactoryLibrary::get();
|
||||||
|
const int librarySize = (int) library.size();
|
||||||
|
|
||||||
|
if (librarySize > 0 && shared.wtBank)
|
||||||
|
{
|
||||||
|
const int targetBank = juce::jlimit (0, librarySize - 1,
|
||||||
|
(int) std::lround (shared.wtBank->load()));
|
||||||
|
if (targetBank != currentWtBankIndex)
|
||||||
|
{
|
||||||
|
wtOsc.setBank (library[(size_t) targetBank].bank);
|
||||||
|
currentWtBankIndex = targetBank;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (librarySize > 0 && shared.wt2Bank)
|
||||||
|
{
|
||||||
|
const int targetBank2 = juce::jlimit (0, librarySize - 1,
|
||||||
|
(int) std::lround (shared.wt2Bank->load()));
|
||||||
|
if (targetBank2 != currentWtBankIndex2)
|
||||||
|
{
|
||||||
|
wtOsc2.setBank (library[(size_t) targetBank2].bank);
|
||||||
|
currentWtBankIndex2 = targetBank2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const bool useWTLayerA = (shared.wtOn && shared.wtOn->load() > 0.5f)
|
||||||
|
&& wtOsc.getFrameCount() > 0;
|
||||||
|
const bool useWTLayerB = (shared.wt2On && shared.wt2On->load() > 0.5f)
|
||||||
|
&& wtOsc2.getFrameCount() > 0;
|
||||||
|
|
||||||
|
const float morphMaxA = wtOsc.getMaxMorph();
|
||||||
|
const float morphBaseA = shared.wtMorph
|
||||||
|
? juce::jlimit (0.0f, morphMaxA, shared.wtMorph->load())
|
||||||
|
: 0.0f;
|
||||||
|
const float lfoDepthA = shared.wtLfoDepth ? shared.wtLfoDepth->load() : 0.0f;
|
||||||
|
const float lfoRateA = shared.wtLfoRate ? shared.wtLfoRate->load() : 1.0f;
|
||||||
|
const int lfoShapeA = shared.wtLfoShape ? (int) std::lround (shared.wtLfoShape->load()) : 0;
|
||||||
|
|
||||||
|
morphLfo.setRate (lfoRateA);
|
||||||
|
morphLfo.setShape (lfoShapeA);
|
||||||
|
|
||||||
|
const float depthFramesA = juce::jlimit (0.0f, morphMaxA, lfoDepthA);
|
||||||
|
|
||||||
|
const float morphMaxB = wtOsc2.getMaxMorph();
|
||||||
|
const float morphBaseB = shared.wt2Morph
|
||||||
|
? juce::jlimit (0.0f, morphMaxB, shared.wt2Morph->load())
|
||||||
|
: 0.0f;
|
||||||
|
const float lfoDepthB = shared.wt2LfoDepth ? shared.wt2LfoDepth->load() : 0.0f;
|
||||||
|
const float lfoRateB = shared.wt2LfoRate ? shared.wt2LfoRate->load() : 0.3f;
|
||||||
|
const int lfoShapeB = shared.wt2LfoShape ? (int) std::lround (shared.wt2LfoShape->load()) : 0;
|
||||||
|
|
||||||
|
morphLfo2.setRate (lfoRateB);
|
||||||
|
morphLfo2.setShape (lfoShapeB);
|
||||||
|
|
||||||
|
const float depthFramesB = juce::jlimit (0.0f, morphMaxB, lfoDepthB);
|
||||||
|
|
||||||
|
const float levelA = shared.wtLevel ? juce::jlimit (0.0f, 1.0f, shared.wtLevel->load()) : 0.0f;
|
||||||
|
const float levelB = shared.wt2Level ? juce::jlimit (0.0f, 1.0f, shared.wt2Level->load()) : 0.0f;
|
||||||
|
const float safeLevelSum = juce::jlimit (0.5f, 2.0f, levelA + levelB + 0.0001f);
|
||||||
|
const float mixGain = 0.45f / safeLevelSum;
|
||||||
|
|
||||||
for (int i = 0; i < numSamples; ++i)
|
for (int i = 0; i < numSamples; ++i)
|
||||||
{
|
{
|
||||||
const float s = osc.process();
|
float sampleA = useWTLayerA ? 0.0f : osc.process();
|
||||||
|
if (useWTLayerA)
|
||||||
|
{
|
||||||
|
const float lfoValueA = morphLfo.process();
|
||||||
|
const float headroomNegA = juce::jmin (depthFramesA, morphBaseA);
|
||||||
|
const float headroomPosA = juce::jmin (depthFramesA, morphMaxA - morphBaseA);
|
||||||
|
const float offsetA = (lfoValueA >= 0.0f ? lfoValueA * headroomPosA
|
||||||
|
: lfoValueA * headroomNegA);
|
||||||
|
const float morphValueA = juce::jlimit (0.0f, morphMaxA, morphBaseA + offsetA);
|
||||||
|
sampleA = wtOsc.process (morphValueA);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
morphLfo.process(); // advance for consistency
|
||||||
|
}
|
||||||
|
|
||||||
|
float sampleB = 0.0f;
|
||||||
|
if (useWTLayerB)
|
||||||
|
{
|
||||||
|
const float lfoValueB = morphLfo2.process();
|
||||||
|
const float headroomNegB = juce::jmin (depthFramesB, morphBaseB);
|
||||||
|
const float headroomPosB = juce::jmin (depthFramesB, morphMaxB - morphBaseB);
|
||||||
|
const float offsetB = (lfoValueB >= 0.0f ? lfoValueB * headroomPosB
|
||||||
|
: lfoValueB * headroomNegB);
|
||||||
|
const float morphValueB = juce::jlimit (0.0f, morphMaxB, morphBaseB + offsetB);
|
||||||
|
sampleB = wtOsc2.process (morphValueB);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
morphLfo2.process();
|
||||||
|
}
|
||||||
|
|
||||||
|
const float combined = mixGain * ((sampleA * levelA) + (sampleB * levelB));
|
||||||
|
|
||||||
for (int ch = 0; ch < numCh; ++ch)
|
for (int ch = 0; ch < numCh; ++ch)
|
||||||
tempBuffer.getWritePointer (ch)[i] = s;
|
tempBuffer.getWritePointer (ch)[i] = combined;
|
||||||
}
|
}
|
||||||
|
|
||||||
auto block = tempBlock.getSubBlock (0, (size_t) numSamples);
|
auto block = tempBlock.getSubBlock (0, (size_t) numSamples);
|
||||||
|
|
||||||
// ================================================================
|
renderFlanger(numSamples, numCh);
|
||||||
// Flanger (pre-filter) – manual per-sample to set varying delay
|
renderADSR(numSamples, numCh);
|
||||||
// ================================================================
|
renderChorus(block);
|
||||||
{
|
renderSimpleDelay(block);
|
||||||
auto& flanger = chain.get<flangerIndex>();
|
renderDistortion(numSamples, numCh, block);
|
||||||
|
renderEQ(block);
|
||||||
|
|
||||||
const bool enabled = shared.flangerOn && shared.flangerOn->load() > 0.5f;
|
|
||||||
if (enabled)
|
|
||||||
{
|
|
||||||
const float rate = shared.flangerRate ? shared.flangerRate->load() : 0.0f;
|
|
||||||
float lfoPhase = shared.flangerPhase ? shared.flangerPhase->load() : 0.0f;
|
|
||||||
const float flangerDepth = shared.flangerDepth ? shared.flangerDepth->load() : 0.0f; // ms
|
|
||||||
const float mix = shared.flangerDryMix ? shared.flangerDryMix->load() : 0.0f;
|
|
||||||
const float feedback = shared.flangerFeedback ? shared.flangerFeedback->load() : 0.0f;
|
|
||||||
const float baseDelayMs = shared.flangerDelay ? shared.flangerDelay->load() : 0.25f;
|
|
||||||
|
|
||||||
for (int i = 0; i < numSamples; ++i)
|
|
||||||
{
|
|
||||||
const float in = tempBuffer.getReadPointer (0)[i];
|
|
||||||
|
|
||||||
const float lfo = std::sin (lfoPhase);
|
|
||||||
const float delayMs = baseDelayMs + 0.5f * (1.0f + lfo) * flangerDepth;
|
|
||||||
const float delaySamples = juce::jmax (0.0f, delayMs * 0.001f * (float) spec.sampleRate);
|
|
||||||
|
|
||||||
flanger.setDelay (delaySamples);
|
|
||||||
|
|
||||||
const float delayed = flanger.popSample (0);
|
|
||||||
flanger.pushSample (0, in + delayed * feedback);
|
|
||||||
|
|
||||||
const float out = in * (1.0f - mix) + delayed * mix;
|
|
||||||
for (int ch = 0; ch < numCh; ++ch)
|
|
||||||
tempBuffer.getWritePointer (ch)[i] = out;
|
|
||||||
|
|
||||||
lfoPhase += juce::MathConstants<float>::twoPi * rate / (float) spec.sampleRate;
|
|
||||||
if (lfoPhase > juce::MathConstants<float>::twoPi)
|
|
||||||
lfoPhase -= juce::MathConstants<float>::twoPi;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ================================================================
|
|
||||||
// Filter with per-sample ADSR modulation (poly)
|
|
||||||
// ================================================================
|
|
||||||
{
|
|
||||||
const bool enabled = shared.filterOn && shared.filterOn->load() > 0.5f;
|
|
||||||
|
|
||||||
// Update filter type every block (cheap)
|
|
||||||
const int ftype = (int) std::lround (juce::jlimit (0.0f, 2.0f,
|
|
||||||
shared.filterType ? shared.filterType->load() : 0.0f));
|
|
||||||
switch (ftype)
|
|
||||||
{
|
|
||||||
case 0: svf.setType (juce::dsp::StateVariableTPTFilterType::lowpass); break;
|
|
||||||
case 1: svf.setType (juce::dsp::StateVariableTPTFilterType::highpass); break;
|
|
||||||
case 2: svf.setType (juce::dsp::StateVariableTPTFilterType::bandpass); break;
|
|
||||||
default: break;
|
|
||||||
}
|
|
||||||
|
|
||||||
const float qOrRes = juce::jlimit (0.1f, 10.0f,
|
|
||||||
shared.filterResonance ? shared.filterResonance->load() : 0.7f);
|
|
||||||
svf.setResonance (qOrRes);
|
|
||||||
|
|
||||||
const float baseCutoff = juce::jlimit (20.0f, 20000.0f,
|
|
||||||
shared.filterCutoff ? shared.filterCutoff->load() : 1000.0f);
|
|
||||||
const float envAmt = shared.fenvAmount ? shared.fenvAmount->load() : 0.0f;
|
|
||||||
|
|
||||||
for (int i = 0; i < numSamples; ++i)
|
|
||||||
{
|
|
||||||
const float envVal = filterAdsr.getNextSample();
|
|
||||||
const float cutoff = juce::jlimit (20.0f, 20000.0f,
|
|
||||||
baseCutoff * std::pow (2.0f, envAmt * envVal));
|
|
||||||
svf.setCutoffFrequency (cutoff);
|
|
||||||
|
|
||||||
if (enabled)
|
|
||||||
{
|
|
||||||
for (int ch = 0; ch < numCh; ++ch)
|
|
||||||
{
|
|
||||||
float x = tempBuffer.getSample (ch, i);
|
|
||||||
x = svf.processSample (ch, x);
|
|
||||||
tempBuffer.setSample (ch, i, x);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ================================================================
|
|
||||||
// Chorus
|
|
||||||
// ================================================================
|
|
||||||
if (shared.chorusOn && shared.chorusOn->load() > 0.5f)
|
|
||||||
{
|
|
||||||
auto& chorus = chain.get<chorusIndex>();
|
|
||||||
if (shared.chorusCentre) chorus.setCentreDelay (shared.chorusCentre->load());
|
|
||||||
if (shared.chorusDepth) chorus.setDepth (shared.chorusDepth->load());
|
|
||||||
if (shared.chorusFeedback) chorus.setFeedback (shared.chorusFeedback->load());
|
|
||||||
if (shared.chorusMix) chorus.setMix (shared.chorusMix->load());
|
|
||||||
if (shared.chorusRate) chorus.setRate (shared.chorusRate->load());
|
|
||||||
|
|
||||||
chain.get<chorusIndex>().process (juce::dsp::ProcessContextReplacing<float> (block));
|
|
||||||
}
|
|
||||||
|
|
||||||
// ================================================================
|
|
||||||
// Simple Delay (per-voice)
|
|
||||||
// ================================================================
|
|
||||||
if (shared.delayOn && shared.delayOn->load() > 0.5f)
|
|
||||||
{
|
|
||||||
auto& delay = chain.get<delayIndex>();
|
|
||||||
const float time = shared.delayTime ? shared.delayTime->load() : 0.1f;
|
|
||||||
delay.setDelay (juce::jmax (0.0f, time * (float) spec.sampleRate));
|
|
||||||
delay.process (juce::dsp::ProcessContextReplacing<float> (block));
|
|
||||||
}
|
|
||||||
|
|
||||||
// ================================================================
|
|
||||||
// Reverb
|
|
||||||
// ================================================================
|
|
||||||
if (shared.reverbOn && shared.reverbOn->load() > 0.5f)
|
|
||||||
{
|
|
||||||
juce::Reverb::Parameters rp;
|
|
||||||
rp.damping = shared.reverbDamping ? shared.reverbDamping->load() : 0.0f;
|
|
||||||
rp.dryLevel = shared.reverbDryLevel ? shared.reverbDryLevel->load() : 0.0f;
|
|
||||||
rp.freezeMode = shared.reverbFreezeMode ? shared.reverbFreezeMode->load() : 0.0f;
|
|
||||||
rp.roomSize = shared.reverbRoomSize ? shared.reverbRoomSize->load() : 0.0f;
|
|
||||||
rp.wetLevel = shared.reverbWetLevel ? shared.reverbWetLevel->load() : 0.0f;
|
|
||||||
rp.width = shared.reverbWidth ? shared.reverbWidth->load() : 0.0f;
|
|
||||||
|
|
||||||
chain.get<reverbIndex>().setParameters (rp);
|
|
||||||
chain.get<reverbIndex>().process (juce::dsp::ProcessContextReplacing<float> (block));
|
|
||||||
}
|
|
||||||
|
|
||||||
// ================================================================
|
|
||||||
// Distortion + tone (post LPF/Peak)
|
|
||||||
// ================================================================
|
|
||||||
{
|
|
||||||
const float driveDb = shared.distortionDrive ? shared.distortionDrive->load() : 0.0f;
|
|
||||||
const float bias = juce::jlimit (-1.0f, 1.0f, shared.distortionBias ? shared.distortionBias->load() : 0.0f);
|
|
||||||
const float toneHz = juce::jlimit (100.0f, 8000.0f, shared.distortionTone ? shared.distortionTone->load() : 3000.0f);
|
|
||||||
const int shape = (int) std::lround (juce::jlimit (0.0f, 2.0f,
|
|
||||||
shared.distortionShape ? shared.distortionShape->load() : 0.0f));
|
|
||||||
const float mix = shared.distortionMix ? shared.distortionMix->load() : 0.0f;
|
|
||||||
|
|
||||||
auto& pre = chain.get<distortionPreGain>();
|
|
||||||
auto& sh = chain.get<distortionIndex>();
|
|
||||||
auto& tone = chain.get<distortionPostLPF>();
|
|
||||||
|
|
||||||
pre.setGainDecibels (driveDb);
|
|
||||||
|
|
||||||
// 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); } };
|
|
||||||
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); } };
|
|
||||||
|
|
||||||
tone.coefficients = juce::dsp::IIR::Coefficients<float>::makePeakFilter (
|
|
||||||
spec.sampleRate, toneHz, 0.707f,
|
|
||||||
juce::Decibels::decibelsToGain (shared.highGainDbls ? shared.highGainDbls->load() : 0.0f));
|
|
||||||
|
|
||||||
if (shared.distortionOn && shared.distortionOn->load() > 0.5f)
|
|
||||||
{
|
|
||||||
// Wet/dry blend around the shaper
|
|
||||||
juce::AudioBuffer<float> dryCopy (tempBuffer.getNumChannels(), numSamples);
|
|
||||||
for (int ch = 0; ch < numCh; ++ch)
|
|
||||||
dryCopy.copyFrom (ch, 0, tempBuffer, ch, 0, numSamples);
|
|
||||||
|
|
||||||
// pre -> shaper -> tone
|
|
||||||
pre.process (juce::dsp::ProcessContextReplacing<float> (block));
|
|
||||||
sh.process (juce::dsp::ProcessContextReplacing<float> (block));
|
|
||||||
tone.process (juce::dsp::ProcessContextReplacing<float> (block));
|
|
||||||
|
|
||||||
const float wet = mix, dry = 1.0f - mix;
|
|
||||||
for (int ch = 0; ch < numCh; ++ch)
|
|
||||||
{
|
|
||||||
auto* d = dryCopy.getReadPointer (ch);
|
|
||||||
auto* w = tempBuffer.getWritePointer (ch);
|
|
||||||
for (int i = 0; i < numSamples; ++i)
|
|
||||||
w[i] = dry * d[i] + wet * w[i];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ================================================================
|
|
||||||
// EQ + Master + Limiter (EQ guarded by eqOn)
|
|
||||||
// ================================================================
|
|
||||||
{
|
|
||||||
const bool eqEnabled = shared.eqOn && shared.eqOn->load() > 0.5f;
|
|
||||||
|
|
||||||
auto& eqL = chain.get<eqLowIndex>();
|
|
||||||
auto& eqM = chain.get<eqMidIndex>();
|
|
||||||
auto& eqH = chain.get<eqHighIndex>();
|
|
||||||
|
|
||||||
if (eqEnabled)
|
|
||||||
{
|
|
||||||
eqL.coefficients = juce::dsp::IIR::Coefficients<float>::makeLowShelf (
|
|
||||||
spec.sampleRate, 100.0f, 0.707f,
|
|
||||||
juce::Decibels::decibelsToGain (shared.lowGainDbls ? shared.lowGainDbls->load() : 0.0f));
|
|
||||||
|
|
||||||
eqM.coefficients = juce::dsp::IIR::Coefficients<float>::makePeakFilter (
|
|
||||||
spec.sampleRate, 1000.0f, 1.0f,
|
|
||||||
juce::Decibels::decibelsToGain (shared.midGainDbls ? shared.midGainDbls->load() : 0.0f));
|
|
||||||
|
|
||||||
eqH.coefficients = juce::dsp::IIR::Coefficients<float>::makePeakFilter (
|
|
||||||
spec.sampleRate, 10000.0f, 0.707f,
|
|
||||||
juce::Decibels::decibelsToGain (shared.highGainDbls ? shared.highGainDbls->load() : 0.0f));
|
|
||||||
|
|
||||||
eqL.process (juce::dsp::ProcessContextReplacing<float> (block));
|
|
||||||
eqM.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>().process (juce::dsp::ProcessContextReplacing<float> (block));
|
|
||||||
|
|
||||||
chain.get<limiterIndex>().process (juce::dsp::ProcessContextReplacing<float> (block));
|
|
||||||
}
|
|
||||||
|
|
||||||
// ================================================================
|
// ================================================================
|
||||||
// Apply AMP ADSR envelope
|
// Apply AMP ADSR envelope
|
||||||
@@ -321,10 +236,22 @@ void NeuralSynthVoice::renderNextBlock (juce::AudioBuffer<float>& outputBuffer,
|
|||||||
void NeuralSynthVoice::noteStarted()
|
void NeuralSynthVoice::noteStarted()
|
||||||
{
|
{
|
||||||
const float freqHz = (float) getCurrentlyPlayingNote().getFrequencyInHertz();
|
const float freqHz = (float) getCurrentlyPlayingNote().getFrequencyInHertz();
|
||||||
|
const float initPhase = shared.wtPhase
|
||||||
|
? juce::jlimit (0.0f, 1.0f, shared.wtPhase->load())
|
||||||
|
: 0.0f;
|
||||||
|
|
||||||
// Oscillator frequency and phase retrigger
|
// Oscillator frequency and phase retrigger (BLEP + WT)
|
||||||
osc.setFrequency (freqHz);
|
osc.setFrequency (freqHz);
|
||||||
osc.resetPhase (0.0f);
|
osc.resetPhase (initPhase);
|
||||||
|
wtOsc.setFrequency (freqHz);
|
||||||
|
wtOsc.resetPhase (initPhase);
|
||||||
|
morphLfo.reset();
|
||||||
|
const float initPhaseB = shared.wt2Phase
|
||||||
|
? juce::jlimit (0.0f, 1.0f, shared.wt2Phase->load())
|
||||||
|
: initPhase;
|
||||||
|
wtOsc2.setFrequency (freqHz);
|
||||||
|
wtOsc2.resetPhase (initPhaseB);
|
||||||
|
morphLfo2.reset();
|
||||||
|
|
||||||
// Chorus snapshot
|
// Chorus snapshot
|
||||||
if (shared.chorusCentre) chain.get<chorusIndex>().setCentreDelay (shared.chorusCentre->load());
|
if (shared.chorusCentre) chain.get<chorusIndex>().setCentreDelay (shared.chorusCentre->load());
|
||||||
@@ -372,6 +299,7 @@ void NeuralSynthVoice::notePitchbendChanged()
|
|||||||
{
|
{
|
||||||
const float freqHz = (float) getCurrentlyPlayingNote().getFrequencyInHertz();
|
const float freqHz = (float) getCurrentlyPlayingNote().getFrequencyInHertz();
|
||||||
osc.setFrequency (freqHz);
|
osc.setFrequency (freqHz);
|
||||||
|
wtOsc.setFrequency (freqHz);
|
||||||
}
|
}
|
||||||
|
|
||||||
//==============================================================================
|
//==============================================================================
|
||||||
@@ -384,15 +312,3 @@ void NeuralSynthVoice::noteStopped (bool allowTailOff)
|
|||||||
}
|
}
|
||||||
|
|
||||||
//==============================================================================
|
//==============================================================================
|
||||||
|
|
||||||
void NeuralSynthVoice::setWaveform (int waveformType)
|
|
||||||
{
|
|
||||||
switch (juce::jlimit (0, 3, waveformType))
|
|
||||||
{
|
|
||||||
case 0: osc.setWave (BlepWave::Sine); break;
|
|
||||||
case 1: osc.setWave (BlepWave::Saw); break;
|
|
||||||
case 2: osc.setWave (BlepWave::Square); break;
|
|
||||||
case 3: osc.setWave (BlepWave::Triangle); break;
|
|
||||||
default: osc.setWave (BlepWave::Sine); break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,8 +1,11 @@
|
|||||||
#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 <memory>
|
||||||
|
#include <cmath>
|
||||||
#include "NeuralSharedParams.h"
|
#include "NeuralSharedParams.h"
|
||||||
#include "BlepOsc.h"
|
#include "BlepOsc.h"
|
||||||
|
#include "WavetableOsc.h"
|
||||||
|
|
||||||
//==============================================================================
|
//==============================================================================
|
||||||
// A single polyBLEP oscillator voice with per-voice ADSR, filter ADSR,
|
// A single polyBLEP oscillator voice with per-voice ADSR, filter ADSR,
|
||||||
@@ -25,11 +28,45 @@ public:
|
|||||||
void noteTimbreChanged() override {}
|
void noteTimbreChanged() override {}
|
||||||
void noteKeyStateChanged() override {}
|
void noteKeyStateChanged() override {}
|
||||||
|
|
||||||
// Called from the processor when the GUI waveform param changes
|
private:
|
||||||
void changeWaveform (int wf) { setWaveform (wf); }
|
struct MorphLFO
|
||||||
|
{
|
||||||
|
void prepare (double sr) { sampleRate = juce::jmax (1.0, sr); updateIncrement(); }
|
||||||
|
void reset() { phase = 0.0f; }
|
||||||
|
void setRate (float hz) { rate = juce::jlimit (0.0f, 30.0f, hz); updateIncrement(); }
|
||||||
|
void setShape (int idx) { shape = juce::jlimit (0, 3, idx); }
|
||||||
|
float process()
|
||||||
|
{
|
||||||
|
float value = 0.0f;
|
||||||
|
|
||||||
|
switch (shape)
|
||||||
|
{
|
||||||
|
case 1: value = 1.0f - 4.0f * std::abs(phase - 0.5f); break; // Triangle
|
||||||
|
case 2: value = 2.0f * phase - 1.0f; break; // Ramp up
|
||||||
|
case 3: value = 1.0f - 2.0f * phase; break; // Ramp down
|
||||||
|
default: value = std::sin (juce::MathConstants<float>::twoPi * phase); break; // Sine
|
||||||
|
}
|
||||||
|
|
||||||
|
phase += phaseInc;
|
||||||
|
if (phase >= 1.0f)
|
||||||
|
phase -= 1.0f;
|
||||||
|
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
private:
|
private:
|
||||||
void setWaveform (int waveformType);
|
void updateIncrement()
|
||||||
|
{
|
||||||
|
phaseInc = (float) (rate / (float) sampleRate);
|
||||||
|
if (phaseInc < 0.0f) phaseInc = 0.0f;
|
||||||
|
}
|
||||||
|
|
||||||
|
double sampleRate { 44100.0 };
|
||||||
|
float rate { 1.0f };
|
||||||
|
float phase { 0.0f };
|
||||||
|
float phaseInc { 0.0f };
|
||||||
|
int shape { 0 };
|
||||||
|
};
|
||||||
|
|
||||||
//=== Processing chain (without oscillator) ===============================
|
//=== Processing chain (without oscillator) ===============================
|
||||||
using DelayLine = juce::dsp::DelayLine<float,
|
using DelayLine = juce::dsp::DelayLine<float,
|
||||||
@@ -41,6 +78,15 @@ private:
|
|||||||
using Reverb = juce::dsp::Reverb;
|
using Reverb = juce::dsp::Reverb;
|
||||||
using Limiter = juce::dsp::Limiter<float>;
|
using Limiter = juce::dsp::Limiter<float>;
|
||||||
|
|
||||||
|
// Separate functions for different parts
|
||||||
|
void renderReverb(juce::dsp::AudioBlock<float> &block);
|
||||||
|
void renderSimpleDelay(juce::dsp::AudioBlock<float> &block);
|
||||||
|
void renderADSR(int numSamples, int numCh);
|
||||||
|
void renderChorus(juce::dsp::AudioBlock<float> &block);
|
||||||
|
void renderFlanger(int numSamples, int numCh);
|
||||||
|
void renderDistortion(int numSamples, int numCh, juce::dsp::AudioBlock<float> &block);
|
||||||
|
void renderEQ(juce::dsp::AudioBlock<float> &block);
|
||||||
|
|
||||||
enum ChainIndex
|
enum ChainIndex
|
||||||
{
|
{
|
||||||
flangerIndex = 0,
|
flangerIndex = 0,
|
||||||
@@ -77,9 +123,14 @@ private:
|
|||||||
|
|
||||||
juce::dsp::ProcessSpec spec {};
|
juce::dsp::ProcessSpec spec {};
|
||||||
|
|
||||||
// ==== Oscillator (polyBLEP) ============================================
|
// ==== Oscillators ======================================================
|
||||||
BlepOsc osc;
|
BlepOsc osc; // polyBLEP oscillator
|
||||||
std::atomic<int> pendingWaveform {-1}; // set by changeWaveform()
|
WT::Osc wtOsc; // wavetable oscillator (shared bank)
|
||||||
|
MorphLFO morphLfo;
|
||||||
|
int currentWtBankIndex { -1 };
|
||||||
|
WT::Osc wtOsc2; // secondary wavetable oscillator
|
||||||
|
MorphLFO morphLfo2;
|
||||||
|
int currentWtBankIndex2 { -1 };
|
||||||
|
|
||||||
// ==== Envelopes & Filter ===============================================
|
// ==== Envelopes & Filter ===============================================
|
||||||
juce::ADSR adsr;
|
juce::ADSR adsr;
|
||||||
|
|||||||
46
Source/SynthVoice/ADSR.h
Normal file
46
Source/SynthVoice/ADSR.h
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
#pragma once
|
||||||
|
#include "../SynthVoice.h"
|
||||||
|
|
||||||
|
void NeuralSynthVoice::renderADSR(int numSamples, int numCh) {
|
||||||
|
// ================================================================
|
||||||
|
// Filter with per-sample ADSR modulation (poly)
|
||||||
|
// ================================================================
|
||||||
|
const bool enabled = shared.filterOn && shared.filterOn->load() > 0.5f;
|
||||||
|
|
||||||
|
// Update filter type every block (cheap)
|
||||||
|
const int ftype = (int) std::lround (juce::jlimit (0.0f, 2.0f,
|
||||||
|
shared.filterType ? shared.filterType->load() : 0.0f));
|
||||||
|
switch (ftype)
|
||||||
|
{
|
||||||
|
case 0: svf.setType (juce::dsp::StateVariableTPTFilterType::lowpass); break;
|
||||||
|
case 1: svf.setType (juce::dsp::StateVariableTPTFilterType::highpass); break;
|
||||||
|
case 2: svf.setType (juce::dsp::StateVariableTPTFilterType::bandpass); break;
|
||||||
|
default: break;
|
||||||
|
}
|
||||||
|
|
||||||
|
const float qOrRes = juce::jlimit (0.1f, 10.0f,
|
||||||
|
shared.filterResonance ? shared.filterResonance->load() : 0.7f);
|
||||||
|
svf.setResonance (qOrRes);
|
||||||
|
|
||||||
|
const float baseCutoff = juce::jlimit (20.0f, 20000.0f,
|
||||||
|
shared.filterCutoff ? shared.filterCutoff->load() : 1000.0f);
|
||||||
|
const float envAmt = shared.fenvAmount ? shared.fenvAmount->load() : 0.0f;
|
||||||
|
|
||||||
|
for (int i = 0; i < numSamples; ++i)
|
||||||
|
{
|
||||||
|
const float envVal = filterAdsr.getNextSample();
|
||||||
|
const float cutoff = juce::jlimit (20.0f, 20000.0f,
|
||||||
|
baseCutoff * std::pow (2.0f, envAmt * envVal));
|
||||||
|
svf.setCutoffFrequency (cutoff);
|
||||||
|
|
||||||
|
if (enabled)
|
||||||
|
{
|
||||||
|
for (int ch = 0; ch < numCh; ++ch)
|
||||||
|
{
|
||||||
|
float x = tempBuffer.getSample (ch, i);
|
||||||
|
x = svf.processSample (ch, x);
|
||||||
|
tempBuffer.setSample (ch, i, x);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
19
Source/SynthVoice/Chorus.h
Normal file
19
Source/SynthVoice/Chorus.h
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
#pragma once
|
||||||
|
#include "../SynthVoice.h"
|
||||||
|
|
||||||
|
void NeuralSynthVoice::renderChorus(juce::dsp::AudioBlock<float> &block) {
|
||||||
|
// ================================================================
|
||||||
|
// Chorus
|
||||||
|
// ================================================================
|
||||||
|
if (shared.chorusOn && shared.chorusOn->load() > 0.5f)
|
||||||
|
{
|
||||||
|
auto& chorus = chain.get<chorusIndex>();
|
||||||
|
if (shared.chorusCentre) chorus.setCentreDelay (shared.chorusCentre->load());
|
||||||
|
if (shared.chorusDepth) chorus.setDepth (shared.chorusDepth->load());
|
||||||
|
if (shared.chorusFeedback) chorus.setFeedback (shared.chorusFeedback->load());
|
||||||
|
if (shared.chorusMix) chorus.setMix (shared.chorusMix->load());
|
||||||
|
if (shared.chorusRate) chorus.setRate (shared.chorusRate->load());
|
||||||
|
|
||||||
|
chain.get<chorusIndex>().process (juce::dsp::ProcessContextReplacing<float> (block));
|
||||||
|
}
|
||||||
|
}
|
||||||
54
Source/SynthVoice/Distortion.h
Normal file
54
Source/SynthVoice/Distortion.h
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
#pragma once
|
||||||
|
#include "../SynthVoice.h"
|
||||||
|
|
||||||
|
void NeuralSynthVoice::renderDistortion(
|
||||||
|
int numSamples,
|
||||||
|
int numCh,
|
||||||
|
juce::dsp::AudioBlock<float> &block) {
|
||||||
|
// ================================================================
|
||||||
|
// Distortion + tone (post LPF/Peak)
|
||||||
|
// ================================================================
|
||||||
|
const float driveDb = shared.distortionDrive ? shared.distortionDrive->load() : 0.0f;
|
||||||
|
const float bias = juce::jlimit (-1.0f, 1.0f, shared.distortionBias ? shared.distortionBias->load() : 0.0f);
|
||||||
|
const float toneHz = juce::jlimit (100.0f, 8000.0f, shared.distortionTone ? shared.distortionTone->load() : 3000.0f);
|
||||||
|
const int shape = (int) std::lround (juce::jlimit (0.0f, 2.0f,
|
||||||
|
shared.distortionShape ? shared.distortionShape->load() : 0.0f));
|
||||||
|
const float mix = shared.distortionMix ? shared.distortionMix->load() : 0.0f;
|
||||||
|
|
||||||
|
auto& pre = chain.get<distortionPreGain>();
|
||||||
|
auto& sh = chain.get<distortionIndex>();
|
||||||
|
auto& tone = chain.get<distortionPostLPF>();
|
||||||
|
|
||||||
|
pre.setGainDecibels (driveDb);
|
||||||
|
|
||||||
|
// 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); } };
|
||||||
|
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); } };
|
||||||
|
|
||||||
|
tone.coefficients = juce::dsp::IIR::Coefficients<float>::makePeakFilter (
|
||||||
|
spec.sampleRate, toneHz, 0.707f,
|
||||||
|
juce::Decibels::decibelsToGain (shared.highGainDbls ? shared.highGainDbls->load() : 0.0f));
|
||||||
|
|
||||||
|
if (shared.distortionOn && shared.distortionOn->load() > 0.5f)
|
||||||
|
{
|
||||||
|
// Wet/dry blend around the shaper
|
||||||
|
juce::AudioBuffer<float> dryCopy (tempBuffer.getNumChannels(), numSamples);
|
||||||
|
for (int ch = 0; ch < numCh; ++ch)
|
||||||
|
dryCopy.copyFrom (ch, 0, tempBuffer, ch, 0, numSamples);
|
||||||
|
|
||||||
|
// pre -> shaper -> tone
|
||||||
|
pre.process (juce::dsp::ProcessContextReplacing<float> (block));
|
||||||
|
sh.process (juce::dsp::ProcessContextReplacing<float> (block));
|
||||||
|
tone.process (juce::dsp::ProcessContextReplacing<float> (block));
|
||||||
|
|
||||||
|
const float wet = mix, dry = 1.0f - mix;
|
||||||
|
for (int ch = 0; ch < numCh; ++ch)
|
||||||
|
{
|
||||||
|
auto* d = dryCopy.getReadPointer (ch);
|
||||||
|
auto* w = tempBuffer.getWritePointer (ch);
|
||||||
|
for (int i = 0; i < numSamples; ++i)
|
||||||
|
w[i] = dry * d[i] + wet * w[i];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
38
Source/SynthVoice/EQ.h
Normal file
38
Source/SynthVoice/EQ.h
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
#pragma once
|
||||||
|
#include "../SynthVoice.h"
|
||||||
|
|
||||||
|
void NeuralSynthVoice::renderEQ(juce::dsp::AudioBlock<float> &block)
|
||||||
|
{
|
||||||
|
// ================================================================
|
||||||
|
// EQ + Master + Limiter (EQ guarded by eqOn)
|
||||||
|
// ================================================================
|
||||||
|
const bool eqEnabled = shared.eqOn && shared.eqOn->load() > 0.5f;
|
||||||
|
|
||||||
|
auto& eqL = chain.get<eqLowIndex>();
|
||||||
|
auto& eqM = chain.get<eqMidIndex>();
|
||||||
|
auto& eqH = chain.get<eqHighIndex>();
|
||||||
|
|
||||||
|
if (eqEnabled)
|
||||||
|
{
|
||||||
|
eqL.coefficients = juce::dsp::IIR::Coefficients<float>::makeLowShelf (
|
||||||
|
spec.sampleRate, 100.0f, 0.707f,
|
||||||
|
juce::Decibels::decibelsToGain (shared.lowGainDbls ? shared.lowGainDbls->load() : 0.0f));
|
||||||
|
|
||||||
|
eqM.coefficients = juce::dsp::IIR::Coefficients<float>::makePeakFilter (
|
||||||
|
spec.sampleRate, 1000.0f, 1.0f,
|
||||||
|
juce::Decibels::decibelsToGain (shared.midGainDbls ? shared.midGainDbls->load() : 0.0f));
|
||||||
|
|
||||||
|
eqH.coefficients = juce::dsp::IIR::Coefficients<float>::makePeakFilter (
|
||||||
|
spec.sampleRate, 10000.0f, 0.707f,
|
||||||
|
juce::Decibels::decibelsToGain (shared.highGainDbls ? shared.highGainDbls->load() : 0.0f));
|
||||||
|
|
||||||
|
eqL.process (juce::dsp::ProcessContextReplacing<float> (block));
|
||||||
|
eqM.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>().process (juce::dsp::ProcessContextReplacing<float> (block));
|
||||||
|
|
||||||
|
chain.get<limiterIndex>().process (juce::dsp::ProcessContextReplacing<float> (block));
|
||||||
|
}
|
||||||
43
Source/SynthVoice/Flanger.h
Normal file
43
Source/SynthVoice/Flanger.h
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
#pragma once
|
||||||
|
#include "../SynthVoice.h"
|
||||||
|
|
||||||
|
void NeuralSynthVoice::renderFlanger(int numSamples, int numCh)
|
||||||
|
{
|
||||||
|
// ================================================================
|
||||||
|
// Flanger (pre-filter) – manual per-sample to set varying delay
|
||||||
|
// ================================================================
|
||||||
|
auto& flanger = chain.get<flangerIndex>();
|
||||||
|
|
||||||
|
const bool enabled = shared.flangerOn && shared.flangerOn->load() > 0.5f;
|
||||||
|
if (enabled)
|
||||||
|
{
|
||||||
|
const float rate = shared.flangerRate ? shared.flangerRate->load() : 0.0f;
|
||||||
|
float lfoPhase = shared.flangerPhase ? shared.flangerPhase->load() : 0.0f;
|
||||||
|
const float flangerDepth = shared.flangerDepth ? shared.flangerDepth->load() : 0.0f; // ms
|
||||||
|
const float mix = shared.flangerDryMix ? shared.flangerDryMix->load() : 0.0f;
|
||||||
|
const float feedback = shared.flangerFeedback ? shared.flangerFeedback->load() : 0.0f;
|
||||||
|
const float baseDelayMs = shared.flangerDelay ? shared.flangerDelay->load() : 0.25f;
|
||||||
|
|
||||||
|
for (int i = 0; i < numSamples; ++i)
|
||||||
|
{
|
||||||
|
const float in = tempBuffer.getReadPointer (0)[i];
|
||||||
|
|
||||||
|
const float lfo = std::sin (lfoPhase);
|
||||||
|
const float delayMs = baseDelayMs + 0.5f * (1.0f + lfo) * flangerDepth;
|
||||||
|
const float delaySamples = juce::jmax (0.0f, delayMs * 0.001f * (float) spec.sampleRate);
|
||||||
|
|
||||||
|
flanger.setDelay (delaySamples);
|
||||||
|
|
||||||
|
const float delayed = flanger.popSample (0);
|
||||||
|
flanger.pushSample (0, in + delayed * feedback);
|
||||||
|
|
||||||
|
const float out = in * (1.0f - mix) + delayed * mix;
|
||||||
|
for (int ch = 0; ch < numCh; ++ch)
|
||||||
|
tempBuffer.getWritePointer (ch)[i] = out;
|
||||||
|
|
||||||
|
lfoPhase += juce::MathConstants<float>::twoPi * rate / (float) spec.sampleRate;
|
||||||
|
if (lfoPhase > juce::MathConstants<float>::twoPi)
|
||||||
|
lfoPhase -= juce::MathConstants<float>::twoPi;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
22
Source/SynthVoice/Reverb.h
Normal file
22
Source/SynthVoice/Reverb.h
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
#pragma once
|
||||||
|
#include "../SynthVoice.h"
|
||||||
|
|
||||||
|
void NeuralSynthVoice::renderReverb(juce::dsp::AudioBlock<float> &block) {
|
||||||
|
// ================================================================
|
||||||
|
// Reverb
|
||||||
|
// ================================================================
|
||||||
|
if (shared.reverbOn && shared.reverbOn->load() > 0.5f)
|
||||||
|
{
|
||||||
|
juce::Reverb::Parameters rp;
|
||||||
|
rp.damping = shared.reverbDamping ? shared.reverbDamping->load() : 0.0f;
|
||||||
|
rp.dryLevel = shared.reverbDryLevel ? shared.reverbDryLevel->load() : 0.0f;
|
||||||
|
rp.freezeMode = shared.reverbFreezeMode ? shared.reverbFreezeMode->load() : 0.0f;
|
||||||
|
rp.roomSize = shared.reverbRoomSize ? shared.reverbRoomSize->load() : 0.0f;
|
||||||
|
rp.wetLevel = shared.reverbWetLevel ? shared.reverbWetLevel->load() : 0.0f;
|
||||||
|
rp.width = shared.reverbWidth ? shared.reverbWidth->load() : 0.0f;
|
||||||
|
|
||||||
|
chain.get<reverbIndex>().setParameters (rp);
|
||||||
|
chain.get<reverbIndex>().process (juce::dsp::ProcessContextReplacing<float> (block));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
16
Source/SynthVoice/SimpleDelay.h
Normal file
16
Source/SynthVoice/SimpleDelay.h
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
#pragma once
|
||||||
|
#include "../SynthVoice.h"
|
||||||
|
|
||||||
|
void NeuralSynthVoice::renderSimpleDelay(juce::dsp::AudioBlock<float> &block)
|
||||||
|
{
|
||||||
|
// ================================================================
|
||||||
|
// Simple Delay (per-voice)
|
||||||
|
// ================================================================
|
||||||
|
if (shared.delayOn && shared.delayOn->load() > 0.5f)
|
||||||
|
{
|
||||||
|
auto& delay = chain.get<delayIndex>();
|
||||||
|
const float time = shared.delayTime ? shared.delayTime->load() : 0.1f;
|
||||||
|
delay.setDelay (juce::jmax (0.0f, time * (float) spec.sampleRate));
|
||||||
|
delay.process (juce::dsp::ProcessContextReplacing<float> (block));
|
||||||
|
}
|
||||||
|
}
|
||||||
935
Source/UI/CustomPresetWindow.cpp
Normal file
935
Source/UI/CustomPresetWindow.cpp
Normal file
@@ -0,0 +1,935 @@
|
|||||||
|
#include "CustomPresetWindow.h"
|
||||||
|
|
||||||
|
#include <array>
|
||||||
|
#include <cmath>
|
||||||
|
#include <functional>
|
||||||
|
#include <utility>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
namespace
|
||||||
|
{
|
||||||
|
constexpr int kBrowserColumns = 4;
|
||||||
|
constexpr int kBrowserRows = 10;
|
||||||
|
|
||||||
|
class WaveThumbnail : public juce::Component
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
WaveThumbnail(const std::vector<float>* tbl = nullptr) : table(tbl) {}
|
||||||
|
|
||||||
|
void setTable(const std::vector<float>* tbl) { table = tbl; repaint(); }
|
||||||
|
|
||||||
|
void setHighlight(bool shouldHighlight)
|
||||||
|
{
|
||||||
|
if (highlight == shouldHighlight)
|
||||||
|
return;
|
||||||
|
highlight = shouldHighlight;
|
||||||
|
repaint();
|
||||||
|
}
|
||||||
|
|
||||||
|
void setOnClick(std::function<void()> handler) { onClick = std::move(handler); }
|
||||||
|
|
||||||
|
void paint(juce::Graphics& g) override
|
||||||
|
{
|
||||||
|
auto bounds = getLocalBounds();
|
||||||
|
|
||||||
|
if (highlight)
|
||||||
|
{
|
||||||
|
g.setColour(juce::Colours::darkorange.withAlpha(0.35f));
|
||||||
|
g.fillRect(bounds);
|
||||||
|
}
|
||||||
|
|
||||||
|
g.setColour(juce::Colours::darkgrey);
|
||||||
|
g.drawRect(bounds);
|
||||||
|
|
||||||
|
if (table == nullptr || table->empty())
|
||||||
|
{
|
||||||
|
if (getName().isNotEmpty())
|
||||||
|
{
|
||||||
|
g.setColour(juce::Colours::grey);
|
||||||
|
g.setFont(12.0f);
|
||||||
|
g.drawText(getName(), bounds.reduced(2), juce::Justification::centred);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
g.setColour(juce::Colours::lime);
|
||||||
|
juce::Path path;
|
||||||
|
auto r = bounds;
|
||||||
|
path.startNewSubPath((float) r.getX(), (float) r.getCentreY());
|
||||||
|
|
||||||
|
const int n = (int) table->size();
|
||||||
|
for (int i = 0; i < n; i += 32)
|
||||||
|
{
|
||||||
|
const float x = juce::jmap((float) i, 0.0f, (float) n,
|
||||||
|
(float) r.getX(), (float) r.getRight());
|
||||||
|
const float y = juce::jmap((*table)[(size_t) i], -1.0f, 1.0f,
|
||||||
|
(float) r.getBottom(), (float) r.getY());
|
||||||
|
path.lineTo(x, y);
|
||||||
|
}
|
||||||
|
|
||||||
|
g.strokePath(path, juce::PathStrokeType(1.2f));
|
||||||
|
|
||||||
|
if (getName().isNotEmpty())
|
||||||
|
{
|
||||||
|
g.setColour(juce::Colours::white.withAlpha(0.9f));
|
||||||
|
g.setFont(12.0f);
|
||||||
|
g.drawText(getName(), bounds.reduced(2), juce::Justification::topRight);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void mouseDown(const juce::MouseEvent& e) override
|
||||||
|
{
|
||||||
|
juce::ignoreUnused(e);
|
||||||
|
if (onClick != nullptr)
|
||||||
|
onClick();
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
const std::vector<float>* table { nullptr };
|
||||||
|
bool highlight { false };
|
||||||
|
std::function<void()> onClick;
|
||||||
|
};
|
||||||
|
|
||||||
|
class DrawWaveComponent : public juce::Component
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
DrawWaveComponent()
|
||||||
|
{
|
||||||
|
samples.assign(kTableSize, 0.0f);
|
||||||
|
}
|
||||||
|
|
||||||
|
void clear()
|
||||||
|
{
|
||||||
|
std::fill(samples.begin(), samples.end(), 0.0f);
|
||||||
|
repaint();
|
||||||
|
}
|
||||||
|
|
||||||
|
void paint(juce::Graphics& g) override
|
||||||
|
{
|
||||||
|
auto r = getLocalBounds();
|
||||||
|
|
||||||
|
g.fillAll(juce::Colours::black.withAlpha(0.35f));
|
||||||
|
g.setColour(juce::Colours::darkgrey);
|
||||||
|
g.drawRect(r);
|
||||||
|
|
||||||
|
g.setColour(juce::Colours::white.withAlpha(0.08f));
|
||||||
|
const int hLines = 8, vLines = 16;
|
||||||
|
for (int i = 1; i < hLines; ++i)
|
||||||
|
{
|
||||||
|
const float y = juce::jmap((float) i, 0.0f, (float) hLines,
|
||||||
|
(float) r.getY(), (float) r.getBottom());
|
||||||
|
g.drawLine((float) r.getX(), y, (float) r.getRight(), y);
|
||||||
|
}
|
||||||
|
for (int i = 1; i < vLines; ++i)
|
||||||
|
{
|
||||||
|
const float x = juce::jmap((float) i, 0.0f, (float) vLines,
|
||||||
|
(float) r.getX(), (float) r.getRight());
|
||||||
|
g.drawLine(x, (float) r.getY(), x, (float) r.getBottom());
|
||||||
|
}
|
||||||
|
|
||||||
|
g.setColour(juce::Colours::white.withAlpha(0.15f));
|
||||||
|
g.drawLine((float) r.getX(), (float) r.getCentreY(),
|
||||||
|
(float) r.getRight(), (float) r.getCentreY(), 1.2f);
|
||||||
|
|
||||||
|
if (samples.empty())
|
||||||
|
return;
|
||||||
|
|
||||||
|
g.setColour(juce::Colours::deepskyblue);
|
||||||
|
juce::Path p;
|
||||||
|
const int n = (int) samples.size();
|
||||||
|
p.startNewSubPath((float) r.getX(),
|
||||||
|
juce::jmap(samples[0], -1.0f, 1.0f,
|
||||||
|
(float) r.getBottom(), (float) r.getY()));
|
||||||
|
|
||||||
|
const int step = 4;
|
||||||
|
for (int i = step; i < n; i += step)
|
||||||
|
{
|
||||||
|
const float x = juce::jmap((float) i, 0.0f, (float) n - 1,
|
||||||
|
(float) r.getX(), (float) r.getRight());
|
||||||
|
const float y = juce::jmap(samples[(size_t) i], -1.0f, 1.0f,
|
||||||
|
(float) r.getBottom(), (float) r.getY());
|
||||||
|
p.lineTo(x, y);
|
||||||
|
}
|
||||||
|
|
||||||
|
g.strokePath(p, juce::PathStrokeType(2.0f));
|
||||||
|
}
|
||||||
|
|
||||||
|
void mouseDown(const juce::MouseEvent& e) override
|
||||||
|
{
|
||||||
|
lastDrawIndex = -1;
|
||||||
|
drawAt(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
void mouseDrag(const juce::MouseEvent& e) override
|
||||||
|
{
|
||||||
|
drawAt(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
const std::vector<float>& getTable() const { return samples; }
|
||||||
|
|
||||||
|
private:
|
||||||
|
static constexpr int kTableSize = 2048;
|
||||||
|
|
||||||
|
void drawAt(const juce::MouseEvent& e)
|
||||||
|
{
|
||||||
|
auto r = getLocalBounds();
|
||||||
|
|
||||||
|
const float xNorm = juce::jlimit(0.0f, 1.0f,
|
||||||
|
(e.position.x - (float) r.getX()) / (float) r.getWidth());
|
||||||
|
const float yNorm = juce::jlimit(-1.0f, 1.0f,
|
||||||
|
juce::jmap(e.position.y, (float) r.getBottom(), (float) r.getY(), -1.0f, 1.0f));
|
||||||
|
|
||||||
|
const int n = (int) samples.size();
|
||||||
|
const int idx = (int) juce::jlimit(0.0f, (float) (n - 1), xNorm * (float) n);
|
||||||
|
|
||||||
|
if (lastDrawIndex >= 0 && lastDrawIndex != idx)
|
||||||
|
{
|
||||||
|
const int a = juce::jmin(lastDrawIndex, idx);
|
||||||
|
const int b = juce::jmax(lastDrawIndex, idx);
|
||||||
|
const float v0 = samples[(size_t) lastDrawIndex];
|
||||||
|
const float v1 = yNorm;
|
||||||
|
for (int i = a; i <= b; ++i)
|
||||||
|
{
|
||||||
|
const float t = (b == a ? 1.0f : (float) (i - a) / (float) (b - a));
|
||||||
|
setSampleAt(i, juce::jmap(t, v0, v1));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
setSampleAt(idx, yNorm);
|
||||||
|
}
|
||||||
|
|
||||||
|
lastDrawIndex = idx;
|
||||||
|
repaint();
|
||||||
|
}
|
||||||
|
|
||||||
|
void setSampleAt(int idx, float value)
|
||||||
|
{
|
||||||
|
if ((size_t) idx < samples.size())
|
||||||
|
samples[(size_t) idx] = juce::jlimit(-1.0f, 1.0f, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<float> samples;
|
||||||
|
int lastDrawIndex { -1 };
|
||||||
|
};
|
||||||
|
|
||||||
|
class MetalKnobLookAndFeel : public juce::LookAndFeel_V4
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
void drawRotarySlider(juce::Graphics& g, int x, int y, int width, int height,
|
||||||
|
float sliderPosProportional, float rotaryStartAngle,
|
||||||
|
float rotaryEndAngle, juce::Slider& slider) override
|
||||||
|
{
|
||||||
|
juce::ignoreUnused(slider);
|
||||||
|
|
||||||
|
const float radius = juce::jmin(width, height) * 0.45f;
|
||||||
|
const float cx = (float) x + (float) width * 0.5f;
|
||||||
|
const float cy = (float) y + (float) height * 0.5f;
|
||||||
|
|
||||||
|
g.setColour(juce::Colour(0xff303030));
|
||||||
|
g.fillEllipse(cx - radius, cy - radius, radius * 2.0f, radius * 2.0f);
|
||||||
|
|
||||||
|
juce::ColourGradient grad(juce::Colours::darkgrey, cx, cy - radius,
|
||||||
|
juce::Colours::black, cx, cy + radius, true);
|
||||||
|
g.setGradientFill(grad);
|
||||||
|
g.fillEllipse(cx - radius * 0.95f, cy - radius * 0.95f, radius * 1.9f, radius * 1.9f);
|
||||||
|
|
||||||
|
g.setColour(juce::Colours::lightgrey.withAlpha(0.8f));
|
||||||
|
const int tickCount = 8;
|
||||||
|
for (int i = 0; i < tickCount; ++i)
|
||||||
|
{
|
||||||
|
const float a = juce::MathConstants<float>::twoPi * (float) i / (float) tickCount;
|
||||||
|
const float r1 = radius * 1.08f;
|
||||||
|
const float r2 = radius * 1.25f;
|
||||||
|
g.drawLine(cx + r1 * std::cos(a), cy + r1 * std::sin(a),
|
||||||
|
cx + r2 * std::cos(a), cy + r2 * std::sin(a), 1.6f);
|
||||||
|
}
|
||||||
|
|
||||||
|
const float angle = rotaryStartAngle + sliderPosProportional * (rotaryEndAngle - rotaryStartAngle);
|
||||||
|
juce::Path needle;
|
||||||
|
const float rInner = radius * 0.18f;
|
||||||
|
const float rOuter = radius * 0.80f;
|
||||||
|
needle.addRoundedRectangle(-2.4f, -rOuter, 4.8f, rOuter - rInner, 1.8f);
|
||||||
|
auto transform = juce::AffineTransform::rotation(angle).translated(cx, cy);
|
||||||
|
g.setColour(juce::Colours::whitesmoke);
|
||||||
|
g.fillPath(needle, transform);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
class FlatButtonLookAndFeel : public juce::LookAndFeel_V4
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
void drawButtonBackground(juce::Graphics& g, juce::Button& b, const juce::Colour& bg,
|
||||||
|
bool isHighlighted, bool isDown) override
|
||||||
|
{
|
||||||
|
auto r = b.getLocalBounds().toFloat();
|
||||||
|
auto base = bg;
|
||||||
|
if (isDown) base = base.darker(0.15f);
|
||||||
|
else if (isHighlighted) base = base.brighter(0.08f);
|
||||||
|
|
||||||
|
g.setColour(base);
|
||||||
|
g.fillRoundedRectangle(r, 4.0f);
|
||||||
|
g.setColour(juce::Colours::black.withAlpha(0.5f));
|
||||||
|
g.drawRoundedRectangle(r.reduced(0.5f), 4.0f, 1.0f);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
struct DummyPreset
|
||||||
|
{
|
||||||
|
juce::String name;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct DummyPresetCategory
|
||||||
|
{
|
||||||
|
juce::String name;
|
||||||
|
std::vector<DummyPreset> presets;
|
||||||
|
};
|
||||||
|
|
||||||
|
const std::vector<DummyPresetCategory>& getDemoPresetCategories()
|
||||||
|
{
|
||||||
|
static const std::vector<DummyPresetCategory> categories {
|
||||||
|
{ "Pads", { { "Nebula Drift" }, { "Glass Sky" } } },
|
||||||
|
{ "Bass", { { "Driver" }, { "Submerge" } } },
|
||||||
|
{ "Leads", { { "Starlit" }, { "Binary Pulse" } } }
|
||||||
|
};
|
||||||
|
return categories;
|
||||||
|
}
|
||||||
|
|
||||||
|
class DummyWavetableSynthAudioProcessor
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
static constexpr int kTableSize = 2048;
|
||||||
|
|
||||||
|
DummyWavetableSynthAudioProcessor()
|
||||||
|
{
|
||||||
|
addWave("Sine", [](float phase) { return std::sin(phase); });
|
||||||
|
addWave("Square", [](float phase) { return std::signbit(std::sin(phase)) ? -1.0f : 1.0f; });
|
||||||
|
addWave("Saw", [](float phase) { return 2.0f * (phase / juce::MathConstants<float>::twoPi) - 1.0f; });
|
||||||
|
addWave("Triangle", [](float phase)
|
||||||
|
{
|
||||||
|
const float v = 2.0f * std::fabs(2.0f * (phase / juce::MathConstants<float>::twoPi) - 1.0f) - 1.0f;
|
||||||
|
return juce::jlimit(-1.0f, 1.0f, -v);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
int getWaveTableCount() const { return (int) tables.size(); }
|
||||||
|
|
||||||
|
const std::vector<float>* getPreviewTablePtr(int index) const
|
||||||
|
{
|
||||||
|
if (juce::isPositiveAndBelow(index, (int) tables.size()))
|
||||||
|
return &tables[(size_t) index].samples;
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
int addOrReplaceUserWavetable(const std::vector<float>& newSamples)
|
||||||
|
{
|
||||||
|
if (newSamples.empty())
|
||||||
|
return -1;
|
||||||
|
|
||||||
|
Table table;
|
||||||
|
table.name = "User " + juce::String(tables.size() + 1);
|
||||||
|
table.samples = newSamples;
|
||||||
|
table.samples.resize(kTableSize, 0.0f);
|
||||||
|
tables.push_back(std::move(table));
|
||||||
|
return (int) tables.size() - 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool isMorphLoopActive() const { return morphLoopActive; }
|
||||||
|
void setMorphLoopActive(bool active) { morphLoopActive = active; }
|
||||||
|
|
||||||
|
float getMorphDisplayValue() const { return morphDisplayValue; }
|
||||||
|
void setMorphDisplayValue(float value) { morphDisplayValue = value; }
|
||||||
|
|
||||||
|
void notifyPresetLoaded() {}
|
||||||
|
|
||||||
|
private:
|
||||||
|
struct Table
|
||||||
|
{
|
||||||
|
juce::String name;
|
||||||
|
std::vector<float> samples;
|
||||||
|
};
|
||||||
|
|
||||||
|
void addWave(const juce::String& name, const std::function<float(float)>& generator)
|
||||||
|
{
|
||||||
|
Table table;
|
||||||
|
table.name = name;
|
||||||
|
table.samples.resize(kTableSize);
|
||||||
|
for (int i = 0; i < kTableSize; ++i)
|
||||||
|
{
|
||||||
|
const float phase = juce::MathConstants<float>::twoPi * (float) i / (float) kTableSize;
|
||||||
|
table.samples[(size_t) i] = juce::jlimit(-1.0f, 1.0f, generator(phase));
|
||||||
|
}
|
||||||
|
tables.push_back(std::move(table));
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<Table> tables;
|
||||||
|
bool morphLoopActive { false };
|
||||||
|
float morphDisplayValue { 0.0f };
|
||||||
|
};
|
||||||
|
|
||||||
|
class WaveBrowserComponent : public juce::Component
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
explicit WaveBrowserComponent(DummyWavetableSynthAudioProcessor& processorRef)
|
||||||
|
: audioProcessor(processorRef)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
void setOnWaveSelected(std::function<void(int)> handler) { onWaveSelected = std::move(handler); }
|
||||||
|
|
||||||
|
void paint(juce::Graphics& g) override
|
||||||
|
{
|
||||||
|
g.fillAll(juce::Colours::black.withAlpha(0.35f));
|
||||||
|
g.setColour(juce::Colours::grey);
|
||||||
|
g.drawRect(getLocalBounds());
|
||||||
|
|
||||||
|
auto grid = getLocalBounds().reduced(8);
|
||||||
|
const int cellW = juce::jmax(1, grid.getWidth() / kBrowserColumns);
|
||||||
|
const int cellH = juce::jmax(1, grid.getHeight() / kBrowserRows);
|
||||||
|
|
||||||
|
browserCells.clear();
|
||||||
|
browserCells.reserve(kBrowserColumns * kBrowserRows);
|
||||||
|
|
||||||
|
const int waveCount = audioProcessor.getWaveTableCount();
|
||||||
|
|
||||||
|
for (int r = 0; r < kBrowserRows; ++r)
|
||||||
|
for (int c = 0; c < kBrowserColumns; ++c)
|
||||||
|
{
|
||||||
|
const int idx = r * kBrowserColumns + c;
|
||||||
|
auto cell = juce::Rectangle<int>(grid.getX() + c * cellW,
|
||||||
|
grid.getY() + r * cellH,
|
||||||
|
cellW, cellH);
|
||||||
|
browserCells.push_back(cell);
|
||||||
|
|
||||||
|
g.setColour(juce::Colours::darkgrey);
|
||||||
|
g.drawRect(cell);
|
||||||
|
|
||||||
|
if (idx < waveCount)
|
||||||
|
{
|
||||||
|
if (const auto* tbl = audioProcessor.getPreviewTablePtr(idx))
|
||||||
|
{
|
||||||
|
g.setColour(juce::Colours::lime);
|
||||||
|
juce::Path p;
|
||||||
|
p.startNewSubPath((float) cell.getX(), (float) cell.getCentreY());
|
||||||
|
const int n = (int) tbl->size();
|
||||||
|
for (int i = 0; i < n; i += 32)
|
||||||
|
{
|
||||||
|
const float x = juce::jmap((float) i, 0.0f, (float) n,
|
||||||
|
(float) cell.getX(), (float) cell.getRight());
|
||||||
|
const float y = juce::jmap((*tbl)[(size_t) i], -1.0f, 1.0f,
|
||||||
|
(float) cell.getBottom(), (float) cell.getY());
|
||||||
|
p.lineTo(x, y);
|
||||||
|
}
|
||||||
|
g.strokePath(p, juce::PathStrokeType(1.0f));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
g.setColour(juce::Colours::darkgrey);
|
||||||
|
g.drawLine((float) cell.getX(), (float) cell.getY(), (float) cell.getRight(), (float) cell.getBottom(), 0.5f);
|
||||||
|
g.drawLine((float) cell.getRight(), (float) cell.getY(), (float) cell.getX(), (float) cell.getBottom(), 0.5f);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void mouseDown(const juce::MouseEvent& e) override
|
||||||
|
{
|
||||||
|
const int waveCount = audioProcessor.getWaveTableCount();
|
||||||
|
for (size_t i = 0; i < browserCells.size(); ++i)
|
||||||
|
{
|
||||||
|
if (browserCells[i].contains(e.getPosition()))
|
||||||
|
{
|
||||||
|
if ((int) i >= waveCount)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (onWaveSelected != nullptr)
|
||||||
|
onWaveSelected((int) i);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void refresh() { repaint(); }
|
||||||
|
|
||||||
|
private:
|
||||||
|
DummyWavetableSynthAudioProcessor& audioProcessor;
|
||||||
|
std::vector<juce::Rectangle<int>> browserCells;
|
||||||
|
std::function<void(int)> onWaveSelected;
|
||||||
|
};
|
||||||
|
|
||||||
|
class EditorTabContent : public juce::Component
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
EditorTabContent(juce::Label& titleLabel, DrawWaveComponent& drawComponent)
|
||||||
|
: label(titleLabel), draw(drawComponent)
|
||||||
|
{
|
||||||
|
addAndMakeVisible(label);
|
||||||
|
addAndMakeVisible(draw);
|
||||||
|
}
|
||||||
|
|
||||||
|
void resized() override
|
||||||
|
{
|
||||||
|
auto area = getLocalBounds().reduced(12);
|
||||||
|
auto labelArea = area.removeFromTop(20);
|
||||||
|
label.setBounds(labelArea);
|
||||||
|
area.removeFromTop(6);
|
||||||
|
draw.setBounds(area);
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
juce::Label& label;
|
||||||
|
DrawWaveComponent& draw;
|
||||||
|
};
|
||||||
|
|
||||||
|
class LibraryTabContent : public juce::Component
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
explicit LibraryTabContent(WaveBrowserComponent& browserComponent)
|
||||||
|
: browser(browserComponent)
|
||||||
|
{
|
||||||
|
addAndMakeVisible(browser);
|
||||||
|
}
|
||||||
|
|
||||||
|
void resized() override
|
||||||
|
{
|
||||||
|
browser.setBounds(getLocalBounds().reduced(8));
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
WaveBrowserComponent& browser;
|
||||||
|
};
|
||||||
|
|
||||||
|
class ExampleUIPanel : public juce::Component,
|
||||||
|
private juce::Timer
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
ExampleUIPanel()
|
||||||
|
{
|
||||||
|
setSize(1100, 720);
|
||||||
|
|
||||||
|
morphSlider.setSliderStyle(juce::Slider::LinearHorizontal);
|
||||||
|
morphSlider.setTextBoxStyle(juce::Slider::NoTextBox, false, 0, 0);
|
||||||
|
addAndMakeVisible(morphSlider);
|
||||||
|
|
||||||
|
morphLoopToggle.setButtonText("Loop Morph");
|
||||||
|
morphLoopToggle.onClick = [this]
|
||||||
|
{
|
||||||
|
audioProcessor.setMorphLoopActive(morphLoopToggle.getToggleState());
|
||||||
|
};
|
||||||
|
addAndMakeVisible(morphLoopToggle);
|
||||||
|
|
||||||
|
morphLoopMode.addItemList(juce::StringArray { "Forward", "Ping-Pong", "Half Trip" }, 1);
|
||||||
|
morphLoopMode.setJustificationType(juce::Justification::centred);
|
||||||
|
morphLoopMode.setSelectedItemIndex(0);
|
||||||
|
addAndMakeVisible(morphLoopMode);
|
||||||
|
|
||||||
|
configureKnob(master);
|
||||||
|
master.setLookAndFeel(&metalKnobLNF);
|
||||||
|
addAndMakeVisible(master);
|
||||||
|
addAndMakeVisible(lblMaster);
|
||||||
|
labelAbove(lblMaster, "Master");
|
||||||
|
|
||||||
|
auto configureDefaultKnob = [this](juce::Slider& slider)
|
||||||
|
{
|
||||||
|
configureKnob(slider);
|
||||||
|
slider.setLookAndFeel(&metalKnobLNF);
|
||||||
|
addAndMakeVisible(slider);
|
||||||
|
};
|
||||||
|
|
||||||
|
for (auto* slider : { &cutoffSlider, &attack, &decay, &sustain, &release,
|
||||||
|
&lfoRate, &lfoDepth, &fenvA, &fenvD, &fenvS, &fenvR, &fenvAmt,
|
||||||
|
&chRate, &chDepth, &chDelay, &chFb, &chMix,
|
||||||
|
&rvRoom, &rvDamp, &rvWidth, &rvWet })
|
||||||
|
{
|
||||||
|
configureDefaultKnob(*slider);
|
||||||
|
}
|
||||||
|
|
||||||
|
chorusOn.setButtonText("Chorus");
|
||||||
|
reverbOn.setButtonText("Reverb");
|
||||||
|
osc2Mute.setButtonText("Deactivate Osc2");
|
||||||
|
addAndMakeVisible(chorusOn);
|
||||||
|
addAndMakeVisible(reverbOn);
|
||||||
|
addAndMakeVisible(osc2Mute);
|
||||||
|
|
||||||
|
addToBrowser.setButtonText("Add to Browser");
|
||||||
|
clearDraw.setButtonText("Clear");
|
||||||
|
presetButton.setButtonText(selectedPresetLabel);
|
||||||
|
for (auto* btn : { &addToBrowser, &clearDraw, &presetButton })
|
||||||
|
{
|
||||||
|
btn->setLookAndFeel(&flatBtnLNF);
|
||||||
|
addAndMakeVisible(*btn);
|
||||||
|
}
|
||||||
|
|
||||||
|
addToBrowser.onClick = [this]
|
||||||
|
{
|
||||||
|
const int slotIndex = audioProcessor.addOrReplaceUserWavetable(userDraw.getTable());
|
||||||
|
if (slotIndex >= 0)
|
||||||
|
{
|
||||||
|
slotIndices[(size_t) activeSlot] = slotIndex;
|
||||||
|
updateSlotThumbnails();
|
||||||
|
waveBrowser.refresh();
|
||||||
|
repaint();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
clearDraw.onClick = [this] { userDraw.clear(); };
|
||||||
|
presetButton.onClick = [this] { showPresetMenu(); };
|
||||||
|
|
||||||
|
lblDrawWave.setText("DRAW WAVE", juce::dontSendNotification);
|
||||||
|
lblDrawWave.setColour(juce::Label::textColourId, juce::Colours::white);
|
||||||
|
lblDrawWave.setJustificationType(juce::Justification::left);
|
||||||
|
|
||||||
|
addAndMakeVisible(wavetableTabs);
|
||||||
|
wavetableTabs.addTab("Editor", juce::Colours::transparentBlack, &editorTabContent, false);
|
||||||
|
wavetableTabs.addTab("Library", juce::Colours::transparentBlack, &libraryTabContent, false);
|
||||||
|
wavetableTabs.setColour(juce::TabbedComponent::outlineColourId, juce::Colours::darkgrey);
|
||||||
|
|
||||||
|
waveBrowser.setOnWaveSelected([this](int index)
|
||||||
|
{
|
||||||
|
slotIndices[(size_t) activeSlot] = index;
|
||||||
|
updateSlotThumbnails();
|
||||||
|
repaint();
|
||||||
|
});
|
||||||
|
|
||||||
|
for (auto* box : { &slotABox, &slotBBox, &slotCBox })
|
||||||
|
addAndMakeVisible(*box);
|
||||||
|
|
||||||
|
slotABox.setName("A");
|
||||||
|
slotBBox.setName("B");
|
||||||
|
slotCBox.setName("C");
|
||||||
|
slotABox.setOnClick([this]{ setActiveSlot(0); });
|
||||||
|
slotBBox.setOnClick([this]{ setActiveSlot(1); });
|
||||||
|
slotCBox.setOnClick([this]{ setActiveSlot(2); });
|
||||||
|
setActiveSlot(0);
|
||||||
|
|
||||||
|
updateSlotThumbnails();
|
||||||
|
|
||||||
|
startTimerHz(30);
|
||||||
|
}
|
||||||
|
|
||||||
|
~ExampleUIPanel() override
|
||||||
|
{
|
||||||
|
stopTimer();
|
||||||
|
master.setLookAndFeel(nullptr);
|
||||||
|
for (auto* slider : { &cutoffSlider, &attack, &decay, &sustain, &release,
|
||||||
|
&lfoRate, &lfoDepth, &fenvA, &fenvD, &fenvS, &fenvR, &fenvAmt,
|
||||||
|
&chRate, &chDepth, &chDelay, &chFb, &chMix,
|
||||||
|
&rvRoom, &rvDamp, &rvWidth, &rvWet })
|
||||||
|
{
|
||||||
|
slider->setLookAndFeel(nullptr);
|
||||||
|
}
|
||||||
|
for (auto* btn : { &addToBrowser, &clearDraw, &presetButton })
|
||||||
|
btn->setLookAndFeel(nullptr);
|
||||||
|
}
|
||||||
|
|
||||||
|
void paint(juce::Graphics& g) override
|
||||||
|
{
|
||||||
|
g.fillAll(juce::Colours::black);
|
||||||
|
|
||||||
|
auto top = getTopPanelBounds();
|
||||||
|
{
|
||||||
|
juce::Colour c1 = juce::Colour::fromRGB(105,105,110);
|
||||||
|
juce::Colour c2 = juce::Colour::fromRGB(75,75,80);
|
||||||
|
g.setGradientFill(juce::ColourGradient(c1, (float) top.getX(), (float) top.getY(),
|
||||||
|
c2, (float) top.getX(), (float) top.getBottom(), false));
|
||||||
|
g.fillRect(top);
|
||||||
|
g.setColour(juce::Colours::white.withAlpha(0.05f));
|
||||||
|
for (int y = top.getY(); y < top.getBottom(); y += 3)
|
||||||
|
g.drawLine((float) top.getX(), (float) y, (float) top.getRight(), (float) y, 1.0f);
|
||||||
|
g.setColour(juce::Colours::black.withAlpha(0.6f));
|
||||||
|
g.drawRect(top, 2);
|
||||||
|
|
||||||
|
g.setColour(juce::Colours::white);
|
||||||
|
g.setFont(18.0f);
|
||||||
|
g.drawText("RTWAVE - WAVETABLE SYNTH", 12, top.getY() + 14, 400, 22, juce::Justification::left);
|
||||||
|
}
|
||||||
|
|
||||||
|
auto bottom = getBottomPanelBounds();
|
||||||
|
{
|
||||||
|
juce::Colour c1 = juce::Colour::fromRGB(110,110,115);
|
||||||
|
juce::Colour c2 = juce::Colour::fromRGB(70,70,75);
|
||||||
|
g.setGradientFill(juce::ColourGradient(c1, (float) bottom.getX(), (float) bottom.getY(),
|
||||||
|
c2, (float) bottom.getX(), (float) bottom.getBottom(), false));
|
||||||
|
g.fillRect(bottom);
|
||||||
|
g.setColour(juce::Colours::white.withAlpha(0.05f));
|
||||||
|
for (int y = bottom.getY(); y < bottom.getBottom(); y += 3)
|
||||||
|
g.drawLine((float) bottom.getX(), (float) y, (float) bottom.getRight(), (float) y, 1.0f);
|
||||||
|
g.setColour(juce::Colours::black.withAlpha(0.6f));
|
||||||
|
g.drawRect(bottom, 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
const int blackTop = top.getBottom();
|
||||||
|
const int blackBottom = bottom.getY();
|
||||||
|
const int leftWidth = getWidth() / 2 - 20;
|
||||||
|
const int rightX = leftWidth + 30;
|
||||||
|
|
||||||
|
g.setColour(juce::Colours::darkred);
|
||||||
|
g.fillRect(juce::Rectangle<int>(rightX, blackTop + 6, getWidth() - rightX - 30, 6));
|
||||||
|
g.setColour(juce::Colours::white);
|
||||||
|
g.setFont(18.0f);
|
||||||
|
g.drawText("MORPH", rightX, blackTop + 14, getWidth() - rightX - 30, 20, juce::Justification::centred);
|
||||||
|
}
|
||||||
|
|
||||||
|
void resized() override
|
||||||
|
{
|
||||||
|
auto top = getTopPanelBounds();
|
||||||
|
auto bottom = getBottomPanelBounds();
|
||||||
|
|
||||||
|
const int blackTop = top.getBottom();
|
||||||
|
const int blackBottom = bottom.getY();
|
||||||
|
const int leftWidth = getWidth() / 2 - 20;
|
||||||
|
const int rightX = leftWidth + 30;
|
||||||
|
|
||||||
|
const int boxH = 28;
|
||||||
|
slotABox.setBounds(rightX, blackTop + 6, 130, boxH);
|
||||||
|
slotBBox.setBounds(rightX + 140, blackTop + 6, 130, boxH);
|
||||||
|
slotCBox.setBounds(rightX + 280, blackTop + 6, 130, boxH);
|
||||||
|
|
||||||
|
morphSlider.setBounds(rightX, blackTop + boxH + 10, getWidth() - rightX - 30, 18);
|
||||||
|
morphLoopToggle.setBounds(rightX, morphSlider.getBottom() + 10, 120, 22);
|
||||||
|
morphLoopMode.setBounds(morphLoopToggle.getRight() + 8, morphSlider.getBottom() + 6, 150, 26);
|
||||||
|
|
||||||
|
presetButton.setBounds(getWidth() - 130 - 10, top.getY() + 24, 130, 28);
|
||||||
|
|
||||||
|
const int padTop = morphLoopMode.getBottom() + 10;
|
||||||
|
auto tabBounds = juce::Rectangle<int>(rightX, padTop, getWidth() - rightX - 16, blackBottom - padTop - 70);
|
||||||
|
wavetableTabs.setBounds(tabBounds);
|
||||||
|
|
||||||
|
const int buttonRowY = tabBounds.getBottom() + 8;
|
||||||
|
addToBrowser.setBounds(tabBounds.getX() + 220, buttonRowY, 150, 28);
|
||||||
|
clearDraw.setBounds(addToBrowser.getRight() + 12, buttonRowY, 150, 28);
|
||||||
|
|
||||||
|
const int togglesY = bottom.getY() - 36;
|
||||||
|
reverbOn.setBounds(getWidth() - 280, togglesY, 120, 24);
|
||||||
|
osc2Mute .setBounds(getWidth() - 140, togglesY, 140, 24);
|
||||||
|
chorusOn.setBounds(tabBounds.getX(), buttonRowY, 90, 20);
|
||||||
|
|
||||||
|
const int masterW = 82;
|
||||||
|
lblMaster.setBounds(getWidth() - masterW - 150, top.getY() + 4, 80, 16);
|
||||||
|
master .setBounds(getWidth() - masterW - 150, top.getY() + 16, masterW, masterW);
|
||||||
|
|
||||||
|
const int left = bottom.getX() + 18;
|
||||||
|
const int topY = bottom.getY() + 28;
|
||||||
|
const int stepX = 96;
|
||||||
|
const int stepY = 108;
|
||||||
|
const int knobW = 72, knobH = 72;
|
||||||
|
|
||||||
|
auto place = [&](int col, int row, juce::Label& label, juce::Slider& slider, const char* text)
|
||||||
|
{
|
||||||
|
const int x = left + col * stepX;
|
||||||
|
const int y = topY + row * stepY;
|
||||||
|
labelAbove(label, text);
|
||||||
|
label.setBounds(x, y - 14, 88, 14);
|
||||||
|
slider.setBounds(x, y, knobW, knobH);
|
||||||
|
};
|
||||||
|
|
||||||
|
place (0,0, lblCutoff, cutoffSlider, "Cutoff");
|
||||||
|
place (1,0, lblAttack, attack, "Attack");
|
||||||
|
place (2,0, lblDecay, decay, "Decay");
|
||||||
|
place (3,0, lblSustain, sustain, "Sustain");
|
||||||
|
place (4,0, lblRelease, release, "Release");
|
||||||
|
place (5,0, lblLfoRate, lfoRate, "LFO Rate");
|
||||||
|
place (6,0, lblLfoDepth,lfoDepth, "LFO Depth");
|
||||||
|
place (7,0, lblFenvA, fenvA, "FEnv A");
|
||||||
|
place (8,0, lblFenvD, fenvD, "FEnv D");
|
||||||
|
place (9,0, lblFenvS, fenvS, "FEnv S");
|
||||||
|
|
||||||
|
place (0,1, lblFenvR, fenvR, "FEnv R");
|
||||||
|
place (1,1, lblFenvAmt, fenvAmt, "FEnv Amt");
|
||||||
|
place (2,1, lblChRate, chRate, "Ch Rate");
|
||||||
|
place (3,1, lblChDepth, chDepth, "Ch Depth");
|
||||||
|
place (4,1, lblChDelay, chDelay, "Ch Delay");
|
||||||
|
place (5,1, lblChFb, chFb, "Ch FB");
|
||||||
|
place (6,1, lblChMix, chMix, "Ch Mix");
|
||||||
|
place (7,1, lblRvRoom, rvRoom, "Rev Room");
|
||||||
|
place (8,1, lblRvDamp, rvDamp, "Rev Damp");
|
||||||
|
place (9,1, lblRvWidth, rvWidth, "Rev Width");
|
||||||
|
place (10,1,lblRvWet, rvWet, "Rev Wet");
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
juce::Rectangle<int> getTopPanelBounds() const { return { 0, 0, getWidth(), 80 }; }
|
||||||
|
juce::Rectangle<int> getBottomPanelBounds() const
|
||||||
|
{
|
||||||
|
const int panelHeight = 220;
|
||||||
|
return { 0, getHeight() - panelHeight, getWidth(), panelHeight };
|
||||||
|
}
|
||||||
|
|
||||||
|
void configureKnob(juce::Slider& slider)
|
||||||
|
{
|
||||||
|
slider.setRange(0.0, 1.0, 0.0);
|
||||||
|
slider.setSliderStyle(juce::Slider::Rotary);
|
||||||
|
slider.setTextBoxStyle(juce::Slider::NoTextBox, false, 0, 0);
|
||||||
|
const float start = juce::MathConstants<float>::pi * 1.25f;
|
||||||
|
const float end = start + juce::MathConstants<float>::pi * 1.5f;
|
||||||
|
slider.setRotaryParameters(start, end, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void labelAbove(juce::Label& label, const juce::String& text)
|
||||||
|
{
|
||||||
|
label.setText(text, juce::dontSendNotification);
|
||||||
|
label.setJustificationType(juce::Justification::centred);
|
||||||
|
label.setColour(juce::Label::textColourId, juce::Colours::black);
|
||||||
|
}
|
||||||
|
|
||||||
|
void setActiveSlot(int slot)
|
||||||
|
{
|
||||||
|
activeSlot = juce::jlimit(0, 2, slot);
|
||||||
|
slotABox.setHighlight(activeSlot == 0);
|
||||||
|
slotBBox.setHighlight(activeSlot == 1);
|
||||||
|
slotCBox.setHighlight(activeSlot == 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
void updateSlotThumbnails()
|
||||||
|
{
|
||||||
|
const int waveCount = audioProcessor.getWaveTableCount();
|
||||||
|
if (waveCount <= 0)
|
||||||
|
{
|
||||||
|
slotABox.setTable(nullptr);
|
||||||
|
slotBBox.setTable(nullptr);
|
||||||
|
slotCBox.setTable(nullptr);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const int maxIndex = waveCount - 1;
|
||||||
|
slotIndices[0] = juce::jlimit(0, maxIndex, slotIndices[0]);
|
||||||
|
slotIndices[1] = juce::jlimit(0, maxIndex, slotIndices[1]);
|
||||||
|
slotIndices[2] = juce::jlimit(0, maxIndex, slotIndices[2]);
|
||||||
|
|
||||||
|
slotABox.setTable(audioProcessor.getPreviewTablePtr(slotIndices[0]));
|
||||||
|
slotBBox.setTable(audioProcessor.getPreviewTablePtr(slotIndices[1]));
|
||||||
|
slotCBox.setTable(audioProcessor.getPreviewTablePtr(slotIndices[2]));
|
||||||
|
}
|
||||||
|
|
||||||
|
void showPresetMenu()
|
||||||
|
{
|
||||||
|
juce::PopupMenu menu;
|
||||||
|
const auto& categories = getDemoPresetCategories();
|
||||||
|
int idBase = 1000;
|
||||||
|
|
||||||
|
for (size_t ci = 0; ci < categories.size(); ++ci)
|
||||||
|
{
|
||||||
|
juce::PopupMenu sub;
|
||||||
|
const auto& cat = categories[ci];
|
||||||
|
for (size_t pi = 0; pi < cat.presets.size(); ++pi)
|
||||||
|
sub.addItem(idBase + (int) (ci * 100 + pi), cat.presets[pi].name);
|
||||||
|
menu.addSubMenu(cat.name, sub);
|
||||||
|
}
|
||||||
|
|
||||||
|
menu.showMenuAsync(juce::PopupMenu::Options().withTargetComponent(&presetButton),
|
||||||
|
[this, idBase](int result)
|
||||||
|
{
|
||||||
|
if (result <= 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
const int ci = (result - idBase) / 100;
|
||||||
|
const int pi = (result - idBase) % 100;
|
||||||
|
const auto& categories = getDemoPresetCategories();
|
||||||
|
if (juce::isPositiveAndBelow(ci, (int) categories.size()))
|
||||||
|
{
|
||||||
|
const auto& cat = categories[(size_t) ci];
|
||||||
|
if (juce::isPositiveAndBelow(pi, (int) cat.presets.size()))
|
||||||
|
{
|
||||||
|
selectedPresetLabel = cat.name + " / " + cat.presets[(size_t) pi].name;
|
||||||
|
presetButton.setButtonText(selectedPresetLabel);
|
||||||
|
audioProcessor.notifyPresetLoaded();
|
||||||
|
setActiveSlot(0);
|
||||||
|
repaint();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void timerCallback() override
|
||||||
|
{
|
||||||
|
updateSlotThumbnails();
|
||||||
|
|
||||||
|
const bool loopActive = morphLoopToggle.getToggleState() || audioProcessor.isMorphLoopActive();
|
||||||
|
|
||||||
|
if (! morphSlider.isMouseButtonDown() && loopActive)
|
||||||
|
{
|
||||||
|
const double now = juce::Time::getMillisecondCounterHiRes();
|
||||||
|
const double dt = (now - lastTimerMs) * 0.001;
|
||||||
|
lastTimerMs = now;
|
||||||
|
|
||||||
|
const int mode = morphLoopMode.getSelectedItemIndex();
|
||||||
|
const float speed = 0.25f;
|
||||||
|
localMorphPhase = std::fmod(localMorphPhase + speed * (float) dt, 1.0f);
|
||||||
|
float value = localMorphPhase;
|
||||||
|
|
||||||
|
switch (mode)
|
||||||
|
{
|
||||||
|
case 1:
|
||||||
|
value = (localMorphPhase < 0.5f)
|
||||||
|
? (localMorphPhase * 2.0f)
|
||||||
|
: (1.0f - (localMorphPhase - 0.5f) * 2.0f);
|
||||||
|
break;
|
||||||
|
case 2:
|
||||||
|
value = 0.5f * localMorphPhase;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
morphSlider.setValue(value, juce::dontSendNotification);
|
||||||
|
audioProcessor.setMorphDisplayValue(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
setActiveSlot(activeSlot);
|
||||||
|
repaint();
|
||||||
|
}
|
||||||
|
|
||||||
|
DummyWavetableSynthAudioProcessor audioProcessor;
|
||||||
|
MetalKnobLookAndFeel metalKnobLNF;
|
||||||
|
FlatButtonLookAndFeel flatBtnLNF;
|
||||||
|
|
||||||
|
juce::Slider morphSlider;
|
||||||
|
juce::ToggleButton morphLoopToggle;
|
||||||
|
juce::ComboBox morphLoopMode;
|
||||||
|
|
||||||
|
juce::Slider master;
|
||||||
|
juce::Label lblMaster;
|
||||||
|
|
||||||
|
juce::Slider cutoffSlider, attack, decay, sustain, release;
|
||||||
|
juce::Slider lfoRate, lfoDepth, fenvA, fenvD, fenvS, fenvR, fenvAmt;
|
||||||
|
juce::Slider chRate, chDepth, chDelay, chFb, chMix;
|
||||||
|
juce::Slider rvRoom, rvDamp, rvWidth, rvWet;
|
||||||
|
|
||||||
|
juce::ToggleButton chorusOn, reverbOn, osc2Mute;
|
||||||
|
|
||||||
|
WaveThumbnail slotABox, slotBBox, slotCBox;
|
||||||
|
DrawWaveComponent userDraw;
|
||||||
|
juce::Label lblDrawWave;
|
||||||
|
WaveBrowserComponent waveBrowser { audioProcessor };
|
||||||
|
EditorTabContent editorTabContent { lblDrawWave, userDraw };
|
||||||
|
LibraryTabContent libraryTabContent { waveBrowser };
|
||||||
|
juce::TabbedComponent wavetableTabs { juce::TabbedButtonBar::TabsAtTop };
|
||||||
|
|
||||||
|
juce::TextButton addToBrowser, clearDraw, presetButton;
|
||||||
|
|
||||||
|
juce::Label lblCutoff, lblAttack, lblDecay, lblSustain, lblRelease;
|
||||||
|
juce::Label lblLfoRate, lblLfoDepth, lblFenvA, lblFenvD, lblFenvS, lblFenvR, lblFenvAmt;
|
||||||
|
juce::Label lblChRate, lblChDepth, lblChDelay, lblChFb, lblChMix;
|
||||||
|
juce::Label lblRvRoom, lblRvDamp, lblRvWidth, lblRvWet;
|
||||||
|
std::array<int, 3> slotIndices { 0, 1, 2 };
|
||||||
|
int activeSlot { 0 };
|
||||||
|
juce::String selectedPresetLabel { "Presets" };
|
||||||
|
double lastTimerMs { juce::Time::getMillisecondCounterHiRes() };
|
||||||
|
float localMorphPhase { 0.0f };
|
||||||
|
};
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
CustomPresetWindow::CustomPresetWindow()
|
||||||
|
: juce::DocumentWindow("Custom Preset",
|
||||||
|
juce::Colours::darkgrey,
|
||||||
|
juce::DocumentWindow::closeButton)
|
||||||
|
{
|
||||||
|
setUsingNativeTitleBar(true);
|
||||||
|
setResizable(true, false);
|
||||||
|
setContentOwned(new ExampleUIPanel(), true);
|
||||||
|
setSize(5000, 5000);
|
||||||
|
}
|
||||||
|
|
||||||
|
void CustomPresetWindow::closeButtonPressed()
|
||||||
|
{
|
||||||
|
setVisible(false);
|
||||||
|
}
|
||||||
10
Source/UI/CustomPresetWindow.h
Normal file
10
Source/UI/CustomPresetWindow.h
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <JuceHeader.h>
|
||||||
|
|
||||||
|
class CustomPresetWindow : public juce::DocumentWindow
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
CustomPresetWindow();
|
||||||
|
void closeButtonPressed() override;
|
||||||
|
};
|
||||||
@@ -2,6 +2,11 @@
|
|||||||
#include <JuceHeader.h>
|
#include <JuceHeader.h>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
#include <cmath>
|
#include <cmath>
|
||||||
|
#include <functional>
|
||||||
|
#include <initializer_list>
|
||||||
|
#include <limits>
|
||||||
|
#include <utility>
|
||||||
|
#include <array>
|
||||||
|
|
||||||
// ============================== 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.
|
||||||
@@ -114,12 +119,17 @@ namespace WT
|
|||||||
time.data[2 * n + 1] = 0.0f;
|
time.data[2 * n + 1] = 0.0f;
|
||||||
}
|
}
|
||||||
fft.performRealOnlyForwardTransform(time.data.data());
|
fft.performRealOnlyForwardTransform(time.data.data());
|
||||||
|
const auto spectrum = time.data; // snapshot packed spectrum for reuse
|
||||||
// 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)
|
||||||
{
|
{
|
||||||
|
// Restore the original spectrum before masking this mip level
|
||||||
|
for (size_t idx = 0; idx < spectrum.size(); ++idx)
|
||||||
|
time.data[idx] = spectrum[idx];
|
||||||
|
|
||||||
// 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
|
||||||
@@ -220,19 +230,762 @@ namespace WT
|
|||||||
std::vector<std::vector<std::vector<float>>> tables;
|
std::vector<std::vector<std::vector<float>>> tables;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
struct Preset
|
||||||
|
{
|
||||||
|
juce::String category;
|
||||||
|
juce::String name;
|
||||||
|
std::shared_ptr<Bank> bank;
|
||||||
|
};
|
||||||
|
|
||||||
|
class FactoryLibrary
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
static const std::vector<Preset>& get()
|
||||||
|
{
|
||||||
|
static const std::vector<Preset> presets = buildFactoryLibrary();
|
||||||
|
return presets;
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
using WaveFn = std::function<float(float)>;
|
||||||
|
|
||||||
|
static WaveFn additive(const std::initializer_list<std::pair<int, float>>& partials)
|
||||||
|
{
|
||||||
|
const auto coeffs = std::vector<std::pair<int, float>>(partials);
|
||||||
|
return [coeffs](float phase)
|
||||||
|
{
|
||||||
|
float v = 0.0f;
|
||||||
|
for (auto [harm, gain] : coeffs)
|
||||||
|
v += gain * std::sin((float)harm * phase);
|
||||||
|
return v;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
static WaveFn pulse(float duty)
|
||||||
|
{
|
||||||
|
duty = juce::jlimit(0.01f, 0.99f, duty);
|
||||||
|
return [duty](float phase)
|
||||||
|
{
|
||||||
|
const float norm = phase / juce::MathConstants<float>::twoPi;
|
||||||
|
return (norm < duty ? 1.0f : -1.0f);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
static WaveFn bendFold(float amount)
|
||||||
|
{
|
||||||
|
return [amount](float phase)
|
||||||
|
{
|
||||||
|
float x = std::sin(phase);
|
||||||
|
x = juce::jlimit(-1.0f, 1.0f, x + amount * x * x * x);
|
||||||
|
return x;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
static std::vector<float> renderWave(size_t tableSize, const WaveFn& fn)
|
||||||
|
{
|
||||||
|
std::vector<float> table(tableSize, 0.0f);
|
||||||
|
for (size_t n = 0; n < tableSize; ++n)
|
||||||
|
{
|
||||||
|
const float phase = (float)(juce::MathConstants<double>::twoPi * (double)n / (double)tableSize);
|
||||||
|
table[n] = fn(phase);
|
||||||
|
}
|
||||||
|
// Remove any DC component before normalising so waves stay centred.
|
||||||
|
float mean = 0.0f;
|
||||||
|
for (float v : table)
|
||||||
|
mean += v;
|
||||||
|
mean /= (float)tableSize;
|
||||||
|
for (auto& v : table)
|
||||||
|
v -= mean;
|
||||||
|
Bank::normalise(table);
|
||||||
|
return table;
|
||||||
|
}
|
||||||
|
|
||||||
|
static std::vector<std::vector<float>> generateFrames(size_t tableSize,
|
||||||
|
const std::vector<WaveFn>& keyWaves,
|
||||||
|
int frames)
|
||||||
|
{
|
||||||
|
std::vector<std::vector<float>> out((size_t)frames, std::vector<float>(tableSize, 0.0f));
|
||||||
|
if (keyWaves.empty())
|
||||||
|
return out;
|
||||||
|
|
||||||
|
std::vector<std::vector<float>> rendered;
|
||||||
|
rendered.reserve(keyWaves.size());
|
||||||
|
for (const auto& fn : keyWaves)
|
||||||
|
rendered.push_back(renderWave(tableSize, fn));
|
||||||
|
|
||||||
|
if (rendered.size() == 1)
|
||||||
|
{
|
||||||
|
for (auto& frame : out)
|
||||||
|
frame = rendered.front();
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
const int segments = (int)rendered.size() - 1;
|
||||||
|
for (int f = 0; f < frames; ++f)
|
||||||
|
{
|
||||||
|
const float globalT = (float) f / (float) juce::jmax(1, frames - 1);
|
||||||
|
const float scaled = globalT * (float) segments;
|
||||||
|
const int seg = juce::jlimit(0, segments - 1, (int) std::floor(scaled));
|
||||||
|
const float t = scaled - (float) seg;
|
||||||
|
|
||||||
|
const auto& A = rendered[(size_t) seg];
|
||||||
|
const auto& B = rendered[(size_t) (seg + 1)];
|
||||||
|
auto& dst = out[(size_t) f];
|
||||||
|
|
||||||
|
for (size_t i = 0; i < tableSize; ++i)
|
||||||
|
dst[i] = juce::jmap(t, A[i], B[i]);
|
||||||
|
|
||||||
|
Bank::normalise(dst);
|
||||||
|
}
|
||||||
|
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
static std::vector<Preset> buildFactoryLibrary()
|
||||||
|
{
|
||||||
|
const size_t tableSize = 2048;
|
||||||
|
const int frames = 16;
|
||||||
|
const int levels = 6;
|
||||||
|
|
||||||
|
std::vector<Preset> presets;
|
||||||
|
presets.reserve(240);
|
||||||
|
|
||||||
|
const WaveFn sine = [](float ph){ return std::sin(ph); };
|
||||||
|
const WaveFn sawUp = [](float ph){
|
||||||
|
const float norm = (ph / juce::MathConstants<float>::twoPi) - std::floor(ph / juce::MathConstants<float>::twoPi);
|
||||||
|
return 2.0f * norm - 1.0f;
|
||||||
|
};
|
||||||
|
const WaveFn sawDown = [](float ph){
|
||||||
|
const float norm = (ph / juce::MathConstants<float>::twoPi) - std::floor(ph / juce::MathConstants<float>::twoPi);
|
||||||
|
return 1.0f - 2.0f * norm;
|
||||||
|
};
|
||||||
|
const WaveFn triangle = [](float ph){
|
||||||
|
float norm = ph / juce::MathConstants<float>::twoPi;
|
||||||
|
norm -= std::floor(norm);
|
||||||
|
float tri = norm < 0.25f ? norm * 4.0f :
|
||||||
|
norm < 0.75f ? 2.0f - norm * 4.0f :
|
||||||
|
norm * 4.0f - 4.0f;
|
||||||
|
return juce::jlimit(-1.0f, 1.0f, tri);
|
||||||
|
};
|
||||||
|
const WaveFn square50 = pulse(0.5f);
|
||||||
|
const WaveFn pulse30 = pulse(0.3f);
|
||||||
|
const WaveFn pulse10 = pulse(0.1f);
|
||||||
|
const WaveFn organ = additive({ {1, 1.0f}, {2, 0.5f}, {3, 0.35f}, {4, 0.2f} });
|
||||||
|
const WaveFn choir = additive({ {1, 1.0f}, {3, 0.4f}, {5, 0.25f}, {7, 0.18f} });
|
||||||
|
const WaveFn bell = additive({ {1, 1.0f}, {2, 0.7f}, {6, 0.45f}, {8, 0.3f}, {9, 0.2f} });
|
||||||
|
const WaveFn hollow = additive({ {2, 1.0f}, {4, 0.6f}, {6, 0.3f}, {8, 0.15f} });
|
||||||
|
const WaveFn airy = additive({ {1, 1.0f}, {4, 0.6f}, {6, 0.25f}, {9, 0.18f} });
|
||||||
|
const WaveFn bendSoft = bendFold(0.4f);
|
||||||
|
const WaveFn bendHard = bendFold(1.0f);
|
||||||
|
const WaveFn clipped = [](float ph){ return std::tanh(2.5f * std::sin(ph)); };
|
||||||
|
const WaveFn evenStack = additive({ {2, 1.0f}, {6, 0.6f}, {10, 0.4f} });
|
||||||
|
const WaveFn oddStack = additive({ {1, 1.0f}, {5, 0.6f}, {9, 0.3f} });
|
||||||
|
|
||||||
|
auto mix = [](std::initializer_list<std::pair<WaveFn, float>> parts)
|
||||||
|
{
|
||||||
|
std::vector<WaveFn> funcs;
|
||||||
|
std::vector<float> weights;
|
||||||
|
funcs.reserve(parts.size());
|
||||||
|
weights.reserve(parts.size());
|
||||||
|
for (const auto& entry : parts)
|
||||||
|
{
|
||||||
|
funcs.push_back(entry.first);
|
||||||
|
weights.push_back(entry.second);
|
||||||
|
}
|
||||||
|
|
||||||
|
return WaveFn([funcs, weights](float phase) mutable
|
||||||
|
{
|
||||||
|
float v = 0.0f;
|
||||||
|
for (size_t i = 0; i < funcs.size(); ++i)
|
||||||
|
v += weights[i] * funcs[i](phase);
|
||||||
|
return v;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
auto makeAdditive = [](const std::vector<std::pair<int, float>>& partials)
|
||||||
|
{
|
||||||
|
auto coeffs = partials;
|
||||||
|
return WaveFn([coeffs](float phase) mutable
|
||||||
|
{
|
||||||
|
float v = 0.0f;
|
||||||
|
for (auto [harm, gain] : coeffs)
|
||||||
|
v += gain * std::sin((float) harm * phase);
|
||||||
|
return v;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
auto formatIndex = [](int idx)
|
||||||
|
{
|
||||||
|
return juce::String(idx + 1).paddedLeft('0', 2);
|
||||||
|
};
|
||||||
|
|
||||||
|
auto sanitise = [](const juce::String& source, const juce::String& fallback)
|
||||||
|
{
|
||||||
|
juce::String cleaned;
|
||||||
|
for (int i = 0; i < source.length(); ++i)
|
||||||
|
{
|
||||||
|
auto ch = source[i];
|
||||||
|
if (ch >= 32 && ch <= 126)
|
||||||
|
cleaned += ch;
|
||||||
|
}
|
||||||
|
cleaned = cleaned.trim();
|
||||||
|
return cleaned.isEmpty() ? fallback : cleaned;
|
||||||
|
};
|
||||||
|
|
||||||
|
auto addPreset = [&](const juce::String& category,
|
||||||
|
const juce::String& name,
|
||||||
|
const std::vector<WaveFn>& keys)
|
||||||
|
{
|
||||||
|
auto bank = std::make_shared<Bank>(tableSize, frames, levels);
|
||||||
|
bank->setRawFrames(generateFrames(tableSize, keys, frames));
|
||||||
|
bank->buildMipmaps();
|
||||||
|
const juce::String safeCategory = sanitise(category, juce::String("Misc"));
|
||||||
|
const juce::String fallbackName = juce::String("Preset ") + juce::String(presets.size() + 1);
|
||||||
|
const juce::String safeName = sanitise(name, fallbackName);
|
||||||
|
presets.push_back({ safeCategory, safeName, bank });
|
||||||
|
};
|
||||||
|
|
||||||
|
// Electric Piano
|
||||||
|
for (int i = 0; i < 20; ++i)
|
||||||
|
{
|
||||||
|
const float t = (float) i / 19.0f;
|
||||||
|
const float brightness = juce::jmap(t, 0.35f, 0.85f);
|
||||||
|
const float bellMix = juce::jmap(t, 0.15f, 0.45f);
|
||||||
|
|
||||||
|
std::vector<std::pair<int, float>> attackCoeffs {
|
||||||
|
{ 1, 1.0f },
|
||||||
|
{ 2, 0.45f * brightness },
|
||||||
|
{ 3, 0.32f * brightness },
|
||||||
|
{ 4, 0.18f * brightness },
|
||||||
|
{ 5, 0.12f * bellMix },
|
||||||
|
{ 6, 0.08f * bellMix },
|
||||||
|
{ 8, 0.05f * bellMix }
|
||||||
|
};
|
||||||
|
|
||||||
|
std::vector<std::pair<int, float>> bodyCoeffs {
|
||||||
|
{ 1, 1.0f },
|
||||||
|
{ 2, 0.4f * brightness },
|
||||||
|
{ 3, 0.25f * brightness },
|
||||||
|
{ 4, 0.16f * bellMix },
|
||||||
|
{ 6, 0.10f * bellMix },
|
||||||
|
{ 9, 0.06f * bellMix }
|
||||||
|
};
|
||||||
|
|
||||||
|
std::vector<std::pair<int, float>> releaseCoeffs {
|
||||||
|
{ 1, 1.0f },
|
||||||
|
{ 2, 0.30f },
|
||||||
|
{ 3, 0.22f },
|
||||||
|
{ 5, 0.12f * bellMix },
|
||||||
|
{ 7, 0.10f * bellMix }
|
||||||
|
};
|
||||||
|
|
||||||
|
auto attack = makeAdditive(attackCoeffs);
|
||||||
|
auto body = makeAdditive(bodyCoeffs);
|
||||||
|
auto release= makeAdditive(releaseCoeffs);
|
||||||
|
auto shimmer = mix({
|
||||||
|
{ airy, 0.35f + 0.20f * t },
|
||||||
|
{ bell, 0.30f + 0.20f * t },
|
||||||
|
{ oddStack, 0.25f }
|
||||||
|
});
|
||||||
|
|
||||||
|
const juce::String name = "EP Tines " + formatIndex(i);
|
||||||
|
addPreset("Electric Piano", name, { attack, body, release, shimmer });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Organ
|
||||||
|
for (int i = 0; i < 20; ++i)
|
||||||
|
{
|
||||||
|
const float t = (float) i / 19.0f;
|
||||||
|
const float even = juce::jmap(t, 0.30f, 0.65f);
|
||||||
|
const float odd = juce::jmap(t, 0.35f, 0.75f);
|
||||||
|
const float perc = juce::jmap(t, 0.10f, 0.35f);
|
||||||
|
|
||||||
|
std::vector<std::pair<int, float>> drawbarCoeffs {
|
||||||
|
{ 1, 1.0f },
|
||||||
|
{ 2, 0.45f * even },
|
||||||
|
{ 3, 0.38f * odd },
|
||||||
|
{ 4, 0.28f * even },
|
||||||
|
{ 5, 0.24f * odd },
|
||||||
|
{ 6, 0.18f * even },
|
||||||
|
{ 8, 0.12f * odd }
|
||||||
|
};
|
||||||
|
|
||||||
|
auto drawbar = makeAdditive(drawbarCoeffs);
|
||||||
|
auto chorusMix = mix({
|
||||||
|
{ organ, 0.65f },
|
||||||
|
{ choir, 0.35f + 0.15f * t },
|
||||||
|
{ airy, 0.25f }
|
||||||
|
});
|
||||||
|
auto bright = mix({
|
||||||
|
{ organ, 0.60f },
|
||||||
|
{ sawUp, 0.35f + 0.20f * t },
|
||||||
|
{ oddStack, 0.25f + 0.10f * t }
|
||||||
|
});
|
||||||
|
auto percussion = mix({
|
||||||
|
{ bell, 0.30f + 0.25f * perc },
|
||||||
|
{ sine, 0.40f },
|
||||||
|
{ organ, 0.35f }
|
||||||
|
});
|
||||||
|
|
||||||
|
const juce::String name = "Organ Drawbar " + formatIndex(i);
|
||||||
|
addPreset("Organ", name, { drawbar, chorusMix, bright, percussion });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bass
|
||||||
|
for (int i = 0; i < 20; ++i)
|
||||||
|
{
|
||||||
|
const float t = (float) i / 19.0f;
|
||||||
|
const float grit = juce::jmap(t, 0.20f, 0.70f);
|
||||||
|
const float hollowAmt = juce::jmap(t, 0.15f, 0.50f);
|
||||||
|
|
||||||
|
std::vector<std::pair<int, float>> subCoeffs {
|
||||||
|
{ 1, 1.0f },
|
||||||
|
{ 2, 0.35f },
|
||||||
|
{ 3, 0.22f * grit },
|
||||||
|
{ 4, 0.15f * hollowAmt }
|
||||||
|
};
|
||||||
|
|
||||||
|
auto sub = makeAdditive(subCoeffs);
|
||||||
|
auto body = mix({
|
||||||
|
{ sawDown, 0.70f },
|
||||||
|
{ triangle, 0.45f },
|
||||||
|
{ bendSoft, 0.35f * grit }
|
||||||
|
});
|
||||||
|
auto growl = mix({
|
||||||
|
{ bendHard, 0.35f * grit },
|
||||||
|
{ clipped, 0.30f + 0.20f * t },
|
||||||
|
{ pulse30, 0.24f }
|
||||||
|
});
|
||||||
|
auto snap = mix({
|
||||||
|
{ pulse10, 0.28f },
|
||||||
|
{ oddStack, 0.26f },
|
||||||
|
{ bendSoft, 0.30f }
|
||||||
|
});
|
||||||
|
|
||||||
|
const juce::String name = "Bass Sculpt " + formatIndex(i);
|
||||||
|
addPreset("Bass", name, { sub, body, growl, snap });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Drums (GM mapped)
|
||||||
|
static const std::array<std::pair<int, const char*>, 20> gmDrumOrder = {{
|
||||||
|
{35, "Acoustic Bass Drum"}, {36, "Bass Drum 1"},
|
||||||
|
{37, "Side Stick"}, {38, "Acoustic Snare"},
|
||||||
|
{39, "Hand Clap"}, {40, "Electric Snare"},
|
||||||
|
{41, "Low Floor Tom"}, {42, "Closed Hi-Hat"},
|
||||||
|
{43, "High Floor Tom"}, {44, "Pedal Hi-Hat"},
|
||||||
|
{45, "Low Tom"}, {46, "Open Hi-Hat"},
|
||||||
|
{47, "Low-Mid Tom"}, {48, "Hi-Mid Tom"},
|
||||||
|
{49, "Crash Cymbal 1"}, {50, "High Tom"},
|
||||||
|
{51, "Ride Cymbal 1"}, {52, "Chinese Cymbal"},
|
||||||
|
{53, "Ride Bell"}, {54, "Tambourine"}
|
||||||
|
}};
|
||||||
|
|
||||||
|
for (int i = 0; i < 20; ++i)
|
||||||
|
{
|
||||||
|
const auto& gm = gmDrumOrder[(size_t) i];
|
||||||
|
const int gmNumber = gm.first;
|
||||||
|
const juce::String label (gm.second);
|
||||||
|
const float accent = (float) ((i % 4) + 1) / 4.0f;
|
||||||
|
|
||||||
|
std::vector<WaveFn> waves;
|
||||||
|
|
||||||
|
if (gmNumber == 35 || gmNumber == 36)
|
||||||
|
{
|
||||||
|
const float clickAmt = juce::jmap(accent, 0.18f, 0.35f);
|
||||||
|
const float bodyAmt = juce::jmap(accent, 0.75f, 0.95f);
|
||||||
|
|
||||||
|
std::vector<std::pair<int, float>> lowCoeffs {
|
||||||
|
{ 1, bodyAmt },
|
||||||
|
{ 2, 0.32f },
|
||||||
|
{ 3, 0.20f * accent },
|
||||||
|
{ 4, 0.15f * accent }
|
||||||
|
};
|
||||||
|
|
||||||
|
auto low = makeAdditive(lowCoeffs);
|
||||||
|
auto punch = mix({
|
||||||
|
{ sine, 0.70f },
|
||||||
|
{ bendSoft, 0.40f + 0.15f * accent },
|
||||||
|
{ hollow, 0.25f * accent }
|
||||||
|
});
|
||||||
|
auto click = mix({
|
||||||
|
{ pulse10, clickAmt },
|
||||||
|
{ oddStack, 0.25f },
|
||||||
|
{ bell, 0.20f + 0.10f * accent }
|
||||||
|
});
|
||||||
|
auto tail = mix({
|
||||||
|
{ sine, 0.70f },
|
||||||
|
{ triangle, 0.30f },
|
||||||
|
{ airy, 0.22f }
|
||||||
|
});
|
||||||
|
waves = { low, punch, click, tail };
|
||||||
|
}
|
||||||
|
else if (gmNumber == 37 || gmNumber == 38 || gmNumber == 39 || gmNumber == 40)
|
||||||
|
{
|
||||||
|
const float snap = juce::jmap(accent, 0.30f, 0.65f);
|
||||||
|
auto strike = mix({
|
||||||
|
{ pulse30, 0.40f + 0.20f * snap },
|
||||||
|
{ oddStack, 0.30f },
|
||||||
|
{ bendHard, 0.25f + 0.10f * snap }
|
||||||
|
});
|
||||||
|
auto noise = mix({
|
||||||
|
{ sawUp, 0.50f },
|
||||||
|
{ evenStack, 0.40f },
|
||||||
|
{ bell, 0.20f + 0.10f * snap }
|
||||||
|
});
|
||||||
|
std::vector<std::pair<int, float>> bodyCoeffs {
|
||||||
|
{ 1, 1.0f },
|
||||||
|
{ 2, 0.35f },
|
||||||
|
{ 3, 0.24f },
|
||||||
|
{ 5, 0.15f * snap }
|
||||||
|
};
|
||||||
|
auto body = makeAdditive(bodyCoeffs);
|
||||||
|
auto tail = mix({
|
||||||
|
{ airy, 0.50f },
|
||||||
|
{ choir, 0.30f },
|
||||||
|
{ bell, 0.25f }
|
||||||
|
});
|
||||||
|
waves = { strike, noise, body, tail };
|
||||||
|
}
|
||||||
|
else if (gmNumber == 41 || gmNumber == 43 || gmNumber == 45
|
||||||
|
|| gmNumber == 47 || gmNumber == 48 || gmNumber == 50)
|
||||||
|
{
|
||||||
|
const float tone = juce::jmap(accent, 0.40f, 0.80f);
|
||||||
|
std::vector<std::pair<int, float>> bodyCoeffs {
|
||||||
|
{ 1, 1.0f },
|
||||||
|
{ 2, 0.40f * tone },
|
||||||
|
{ 3, 0.28f * tone },
|
||||||
|
{ 4, 0.18f }
|
||||||
|
};
|
||||||
|
auto body = makeAdditive(bodyCoeffs);
|
||||||
|
auto strike = mix({
|
||||||
|
{ pulse30, 0.30f + 0.15f * tone },
|
||||||
|
{ bendSoft, 0.35f },
|
||||||
|
{ oddStack, 0.25f }
|
||||||
|
});
|
||||||
|
auto ring = mix({
|
||||||
|
{ evenStack, 0.40f },
|
||||||
|
{ airy, 0.25f + 0.12f * tone },
|
||||||
|
{ bell, 0.20f }
|
||||||
|
});
|
||||||
|
auto tail = mix({
|
||||||
|
{ sine, 0.60f },
|
||||||
|
{ triangle, 0.30f },
|
||||||
|
{ airy, 0.25f }
|
||||||
|
});
|
||||||
|
waves = { strike, body, ring, tail };
|
||||||
|
}
|
||||||
|
else if (gmNumber == 42 || gmNumber == 44 || gmNumber == 46)
|
||||||
|
{
|
||||||
|
const float metallicAmt = juce::jmap(accent, 0.50f, 0.90f);
|
||||||
|
auto metallic = mix({
|
||||||
|
{ oddStack, 0.60f },
|
||||||
|
{ evenStack, 0.50f },
|
||||||
|
{ bell, 0.35f + 0.15f * accent }
|
||||||
|
});
|
||||||
|
auto closed = mix({
|
||||||
|
{ metallic, 0.80f },
|
||||||
|
{ pulse10, 0.25f },
|
||||||
|
{ sawUp, 0.25f }
|
||||||
|
});
|
||||||
|
auto open = mix({
|
||||||
|
{ evenStack, 0.45f },
|
||||||
|
{ bell, 0.40f + 0.15f * accent },
|
||||||
|
{ airy, 0.35f }
|
||||||
|
});
|
||||||
|
auto shimmer = mix({
|
||||||
|
{ bell, 0.45f },
|
||||||
|
{ oddStack, 0.30f },
|
||||||
|
{ choir, 0.25f }
|
||||||
|
});
|
||||||
|
waves = { closed, metallic, open, shimmer };
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
const float spread = juce::jmap(accent, 0.40f, 0.85f);
|
||||||
|
auto strike = mix({
|
||||||
|
{ sawUp, 0.50f },
|
||||||
|
{ bendHard, 0.40f },
|
||||||
|
{ pulse10, 0.30f }
|
||||||
|
});
|
||||||
|
auto wash = mix({
|
||||||
|
{ evenStack, 0.50f + 0.20f * spread },
|
||||||
|
{ oddStack, 0.45f },
|
||||||
|
{ bell, 0.40f + 0.15f * spread }
|
||||||
|
});
|
||||||
|
auto bellLayer = mix({
|
||||||
|
{ bell, 0.55f + 0.15f * spread },
|
||||||
|
{ choir, 0.30f },
|
||||||
|
{ sine, 0.25f }
|
||||||
|
});
|
||||||
|
auto tail = mix({
|
||||||
|
{ airy, 0.50f },
|
||||||
|
{ bell, 0.35f },
|
||||||
|
{ evenStack, 0.30f }
|
||||||
|
});
|
||||||
|
waves = { strike, wash, bellLayer, tail };
|
||||||
|
}
|
||||||
|
|
||||||
|
const juce::String name = "GM " + juce::String(gmNumber) + " " + label;
|
||||||
|
addPreset("Drums", name, waves);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Strings
|
||||||
|
for (int i = 0; i < 20; ++i)
|
||||||
|
{
|
||||||
|
const float t = (float) i / 19.0f;
|
||||||
|
const float sheen = juce::jmap(t, 0.25f, 0.60f);
|
||||||
|
const float warmth = juce::jmap(t, 0.30f, 0.70f);
|
||||||
|
|
||||||
|
auto ensemble = mix({
|
||||||
|
{ sine, 0.60f },
|
||||||
|
{ triangle, 0.50f },
|
||||||
|
{ choir, 0.35f + 0.15f * warmth },
|
||||||
|
{ airy, 0.25f + 0.10f * sheen }
|
||||||
|
});
|
||||||
|
auto bowMotion = mix({
|
||||||
|
{ sawUp, 0.40f },
|
||||||
|
{ sawDown, 0.35f },
|
||||||
|
{ airy, 0.30f },
|
||||||
|
{ bendSoft, 0.20f }
|
||||||
|
});
|
||||||
|
auto shimmer = mix({
|
||||||
|
{ choir, 0.40f },
|
||||||
|
{ airy, 0.35f + 0.15f * sheen },
|
||||||
|
{ bell, 0.20f }
|
||||||
|
});
|
||||||
|
auto sustain = mix({
|
||||||
|
{ sine, 0.55f },
|
||||||
|
{ triangle, 0.35f },
|
||||||
|
{ organ, 0.25f }
|
||||||
|
});
|
||||||
|
|
||||||
|
const juce::String name = "Strings Ensemble " + formatIndex(i);
|
||||||
|
addPreset("Strings", name, { ensemble, bowMotion, shimmer, sustain });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Brass
|
||||||
|
for (int i = 0; i < 20; ++i)
|
||||||
|
{
|
||||||
|
const float t = (float) i / 19.0f;
|
||||||
|
const float edge = juce::jmap(t, 0.30f, 0.75f);
|
||||||
|
|
||||||
|
auto section = mix({
|
||||||
|
{ sawUp, 0.60f },
|
||||||
|
{ sawDown, 0.35f },
|
||||||
|
{ organ, 0.30f },
|
||||||
|
{ bendSoft, 0.20f * edge }
|
||||||
|
});
|
||||||
|
auto growl = mix({
|
||||||
|
{ bendHard, 0.35f + 0.20f * edge },
|
||||||
|
{ clipped, 0.30f },
|
||||||
|
{ pulse30, 0.20f }
|
||||||
|
});
|
||||||
|
auto brassPad = mix({
|
||||||
|
{ organ, 0.45f },
|
||||||
|
{ choir, 0.30f },
|
||||||
|
{ airy, 0.30f }
|
||||||
|
});
|
||||||
|
auto fanfare = mix({
|
||||||
|
{ evenStack, 0.35f + 0.20f * edge },
|
||||||
|
{ oddStack, 0.30f },
|
||||||
|
{ bell, 0.20f }
|
||||||
|
});
|
||||||
|
|
||||||
|
const juce::String name = "Brass Section " + formatIndex(i);
|
||||||
|
addPreset("Brass", name, { section, growl, brassPad, fanfare });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Choir
|
||||||
|
for (int i = 0; i < 20; ++i)
|
||||||
|
{
|
||||||
|
const float t = (float) i / 19.0f;
|
||||||
|
const float breath = juce::jmap(t, 0.20f, 0.60f);
|
||||||
|
|
||||||
|
auto vowels = mix({
|
||||||
|
{ choir, 0.65f },
|
||||||
|
{ airy, 0.40f },
|
||||||
|
{ sine, 0.20f }
|
||||||
|
});
|
||||||
|
auto ahFormant = mix({
|
||||||
|
{ choir, 0.50f + 0.20f * breath },
|
||||||
|
{ organ, 0.30f },
|
||||||
|
{ airy, 0.25f }
|
||||||
|
});
|
||||||
|
auto shimmer = mix({
|
||||||
|
{ airy, 0.40f + 0.20f * breath },
|
||||||
|
{ bell, 0.25f },
|
||||||
|
{ sine, 0.20f }
|
||||||
|
});
|
||||||
|
auto pad = mix({
|
||||||
|
{ choir, 0.45f },
|
||||||
|
{ sine, 0.30f },
|
||||||
|
{ triangle, 0.25f }
|
||||||
|
});
|
||||||
|
|
||||||
|
const juce::String name = "Choir Aura " + formatIndex(i);
|
||||||
|
addPreset("Choir", name, { vowels, ahFormant, shimmer, pad });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pad
|
||||||
|
for (int i = 0; i < 20; ++i)
|
||||||
|
{
|
||||||
|
const float t = (float) i / 19.0f;
|
||||||
|
const float motion = juce::jmap(t, 0.30f, 0.75f);
|
||||||
|
|
||||||
|
auto warm = mix({
|
||||||
|
{ sine, 0.55f },
|
||||||
|
{ organ, 0.40f },
|
||||||
|
{ airy, 0.30f }
|
||||||
|
});
|
||||||
|
auto evolving = mix({
|
||||||
|
{ choir, 0.35f + 0.20f * motion },
|
||||||
|
{ bendSoft, 0.30f },
|
||||||
|
{ airy, 0.35f + 0.15f * motion }
|
||||||
|
});
|
||||||
|
auto shimmer = mix({
|
||||||
|
{ bell, 0.30f },
|
||||||
|
{ airy, 0.35f + 0.20f * motion },
|
||||||
|
{ evenStack, 0.25f }
|
||||||
|
});
|
||||||
|
auto sub = mix({
|
||||||
|
{ sine, 0.50f },
|
||||||
|
{ triangle, 0.35f },
|
||||||
|
{ hollow, 0.25f }
|
||||||
|
});
|
||||||
|
|
||||||
|
const juce::String name = "Pad Horizon " + formatIndex(i);
|
||||||
|
addPreset("Pad", name, { warm, evolving, shimmer, sub });
|
||||||
|
}
|
||||||
|
|
||||||
|
// SFX
|
||||||
|
for (int i = 0; i < 20; ++i)
|
||||||
|
{
|
||||||
|
const float t = (float) i / 19.0f;
|
||||||
|
const float chaos = juce::jmap(t, 0.40f, 0.90f);
|
||||||
|
|
||||||
|
auto motionFx = mix({
|
||||||
|
{ bendSoft, 0.40f + 0.20f * chaos },
|
||||||
|
{ bendHard, 0.35f + 0.20f * chaos },
|
||||||
|
{ sawUp, 0.30f }
|
||||||
|
});
|
||||||
|
auto shimmerFx = mix({
|
||||||
|
{ bell, 0.30f + 0.25f * chaos },
|
||||||
|
{ airy, 0.30f },
|
||||||
|
{ evenStack, 0.25f }
|
||||||
|
});
|
||||||
|
auto glitch = mix({
|
||||||
|
{ clipped, 0.40f },
|
||||||
|
{ pulse30, 0.30f },
|
||||||
|
{ oddStack, 0.30f }
|
||||||
|
});
|
||||||
|
auto atmosphere = mix({
|
||||||
|
{ airy, 0.45f + 0.20f * chaos },
|
||||||
|
{ choir, 0.30f },
|
||||||
|
{ organ, 0.20f }
|
||||||
|
});
|
||||||
|
|
||||||
|
const juce::String name = "SFX Motion " + formatIndex(i);
|
||||||
|
addPreset("SFX", name, { motionFx, shimmerFx, glitch, atmosphere });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Lead
|
||||||
|
for (int i = 0; i < 20; ++i)
|
||||||
|
{
|
||||||
|
const float t = (float) i / 19.0f;
|
||||||
|
const float bite = juce::jmap(t, 0.30f, 0.85f);
|
||||||
|
|
||||||
|
auto classic = mix({
|
||||||
|
{ sawUp, 0.60f },
|
||||||
|
{ sawDown, 0.35f },
|
||||||
|
{ pulse30, 0.25f }
|
||||||
|
});
|
||||||
|
auto sharp = mix({
|
||||||
|
{ pulse10, 0.35f + 0.20f * bite },
|
||||||
|
{ bendSoft, 0.30f },
|
||||||
|
{ oddStack, 0.25f }
|
||||||
|
});
|
||||||
|
auto silky = mix({
|
||||||
|
{ triangle, 0.40f },
|
||||||
|
{ sine, 0.35f },
|
||||||
|
{ airy, 0.25f }
|
||||||
|
});
|
||||||
|
auto grit = mix({
|
||||||
|
{ bendHard, 0.35f + 0.20f * bite },
|
||||||
|
{ clipped, 0.30f },
|
||||||
|
{ pulse30, 0.20f }
|
||||||
|
});
|
||||||
|
|
||||||
|
const juce::String name = "Lead Vector " + formatIndex(i);
|
||||||
|
addPreset("Lead", name, { classic, sharp, silky, grit });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pluck
|
||||||
|
for (int i = 0; i < 20; ++i)
|
||||||
|
{
|
||||||
|
const float t = (float) i / 19.0f;
|
||||||
|
const float sparkle = juce::jmap(t, 0.25f, 0.70f);
|
||||||
|
|
||||||
|
auto transient = mix({
|
||||||
|
{ pulse10, 0.35f + 0.20f * sparkle },
|
||||||
|
{ oddStack, 0.30f },
|
||||||
|
{ bell, 0.25f }
|
||||||
|
});
|
||||||
|
auto body = mix({
|
||||||
|
{ sawDown, 0.50f },
|
||||||
|
{ triangle, 0.40f },
|
||||||
|
{ sine, 0.30f }
|
||||||
|
});
|
||||||
|
auto shimmer = mix({
|
||||||
|
{ bell, 0.30f + 0.20f * sparkle },
|
||||||
|
{ airy, 0.30f },
|
||||||
|
{ evenStack, 0.25f }
|
||||||
|
});
|
||||||
|
auto decay = mix({
|
||||||
|
{ sine, 0.50f },
|
||||||
|
{ hollow, 0.25f },
|
||||||
|
{ airy, 0.30f }
|
||||||
|
});
|
||||||
|
|
||||||
|
const juce::String name = "Pluck Spark " + formatIndex(i);
|
||||||
|
addPreset("Pluck", name, { transient, body, shimmer, decay });
|
||||||
|
}
|
||||||
|
|
||||||
|
return presets;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// =======================================================================
|
// =======================================================================
|
||||||
// Wavetable Oscillator
|
// Wavetable Oscillator
|
||||||
// =======================================================================
|
// =======================================================================
|
||||||
class Osc
|
class Osc
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
void prepare (double sr) { sampleRate = sr; }
|
void prepare (double sr)
|
||||||
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; }
|
sampleRate = juce::jmax (1.0, sr);
|
||||||
void setMorph (float m) { morph = m; } // 0..frames-1 (continuous)
|
setFrequency (freq);
|
||||||
|
}
|
||||||
|
void setBank (std::shared_ptr<Bank> b)
|
||||||
|
{
|
||||||
|
bank = std::move(b);
|
||||||
|
if (bank)
|
||||||
|
morph = juce::jlimit (0.0f, (float) (bank->getFrames() - 1), morph);
|
||||||
|
}
|
||||||
|
void setFrequency (float f)
|
||||||
|
{
|
||||||
|
const float nyquist = 0.5f * (float) sampleRate;
|
||||||
|
freq = juce::jlimit (0.0f, juce::jmax (0.0f, nyquist), f);
|
||||||
|
phaseInc = freq / (float) sampleRate;
|
||||||
|
}
|
||||||
|
void setMorph (float m)
|
||||||
|
{
|
||||||
|
morph = clampMorph (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); }
|
||||||
|
[[nodiscard]] int getFrameCount() const noexcept { return bank ? bank->getFrames() : 0; }
|
||||||
|
[[nodiscard]] float getMaxMorph() const noexcept { return bank ? (float)(bank->getFrames() - 1) : 0.0f; }
|
||||||
|
|
||||||
float process()
|
float process(float morphOverride = std::numeric_limits<float>::quiet_NaN())
|
||||||
{
|
{
|
||||||
if (!bank) return 0.0f;
|
if (!bank) return 0.0f;
|
||||||
|
|
||||||
@@ -241,8 +994,10 @@ namespace WT
|
|||||||
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 morphValue = std::isnan(morphOverride) ? morph : clampMorph (morphOverride);
|
||||||
const float s1 = bank->lookup(morph, l1, phase);
|
|
||||||
|
const float s0 = bank->lookup(morphValue, l0, phase);
|
||||||
|
const float s1 = bank->lookup(morphValue, 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;
|
||||||
@@ -251,6 +1006,13 @@ namespace WT
|
|||||||
}
|
}
|
||||||
|
|
||||||
private:
|
private:
|
||||||
|
float clampMorph (float m) const noexcept
|
||||||
|
{
|
||||||
|
if (!bank) return juce::jmax (0.0f, m);
|
||||||
|
const float maxMorph = (float) (bank->getFrames() - 1);
|
||||||
|
return juce::jlimit (0.0f, maxMorph, m);
|
||||||
|
}
|
||||||
|
|
||||||
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 };
|
||||||
|
|||||||
Reference in New Issue
Block a user