Compare commits

..

10 Commits

Author SHA1 Message Date
Tim
b5e63d3adf Tabbed editor 2025-11-09 05:28:02 +00:00
Tim
61bcef19aa Add Wavetable Editor 2025-11-08 15:18:05 +00:00
Tim
43b8670d4e Some splitting of code 2025-11-08 00:17:43 +00:00
Tim
c5105693a2 Fixes to the UI 2025-10-26 00:49:50 +01:00
ed
0785f6fedd Upload files to "Source" 2025-10-25 17:57:05 +00:00
ed
430ee53b98 Upload files to "Source" 2025-10-25 17:56:51 +00:00
ed
d9f672cd10 Upload files to "Source" 2025-10-25 17:56:37 +00:00
ed
a43db68120 Upload files to "Source" 2025-10-25 17:56:23 +00:00
ed
7231f66689 Upload files to "Source" 2025-10-25 17:56:12 +00:00
ed
d104e3882e Upload files to "Source" 2025-10-25 17:55:57 +00:00
92 changed files with 5544 additions and 363 deletions

Binary file not shown.

View 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 = "MPEIntroductionTutorial";
const char* const companyName = "JUCE";
const char* const versionString = "1.0.0";
const int versionNumber = 0x10000;
}
#endif

View 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).

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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 &note;
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)
};

View 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)

Binary file not shown.

View 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

View 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).

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View 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)

View File

@@ -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)
};

View File

@@ -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)
};

View File

@@ -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)
};

View File

@@ -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)
};

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

View File

@@ -43,7 +43,7 @@ namespace ProjectInfo
{
const char* const projectName = "NeuralSynth";
const char* const companyName = "Samedi Dimanche";
const char* const versionString = "0.0.1";
const int versionNumber = 0x1;
const char* const versionString = "1.0.0";
const int versionNumber = 0x10000;
}
#endif

View File

@@ -17,7 +17,7 @@
#define JucePlugin_Build_VST3 1
#endif
#ifndef JucePlugin_Build_AU
#define JucePlugin_Build_AU 1
#define JucePlugin_Build_AU 0
#endif
#ifndef JucePlugin_Build_AUv3
#define JucePlugin_Build_AUv3 0
@@ -26,7 +26,7 @@
#define JucePlugin_Build_AAX 0
#endif
#ifndef JucePlugin_Build_Standalone
#define JucePlugin_Build_Standalone 1
#define JucePlugin_Build_Standalone 0
#endif
#ifndef JucePlugin_Build_Unity
#define JucePlugin_Build_Unity 0
@@ -77,13 +77,13 @@
#define JucePlugin_EditorRequiresKeyboardFocus 0
#endif
#ifndef JucePlugin_Version
#define JucePlugin_Version 0.0.1
#define JucePlugin_Version 1.0.0
#endif
#ifndef JucePlugin_VersionCode
#define JucePlugin_VersionCode 0x1
#define JucePlugin_VersionCode 0x10000
#endif
#ifndef JucePlugin_VersionString
#define JucePlugin_VersionString "0.0.1"
#define JucePlugin_VersionString "1.0.0"
#endif
#ifndef JucePlugin_VSTUniqueID
#define JucePlugin_VSTUniqueID JucePlugin_PluginCode
@@ -155,7 +155,7 @@
#define JucePlugin_ARAFactoryID "com.SamediDimanche.NeuralSynth.factory"
#endif
#ifndef JucePlugin_ARADocumentArchiveID
#define JucePlugin_ARADocumentArchiveID "com.SamediDimanche.NeuralSynth.aradocumentarchive.0.0.1"
#define JucePlugin_ARADocumentArchiveID "com.SamediDimanche.NeuralSynth.aradocumentarchive.1.0.0"
#endif
#ifndef JucePlugin_ARACompatibleArchiveIDs
#define JucePlugin_ARACompatibleArchiveIDs ""

View File

@@ -4,9 +4,25 @@
addUsingNamespaceToJuceHeader="0" jucerFormatVersion="1" companyWebsite="www.samedidimanche.com"
bundleIdentifier="com.samedidimanche.NeuralSynth" pluginManufacturer="Samedi Dimanche"
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">
<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"
@@ -70,5 +86,26 @@
<MODULEPATH id="juce_gui_extra" path="../../../../../JUCE/modules"/>
</MODULEPATHS>
</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>
</JUCERPROJECT>

View File

@@ -15,6 +15,7 @@ public:
addVoice(new NeuralSynthVoice(sp)); // <-- takes MPESynthesiserVoice*
// MPE synths do not use addSound(); note events are routed via MPE zones.
enableLegacyMode(maxNumVoices);
setVoiceStealingEnabled(true);
}

View File

@@ -6,10 +6,24 @@ enum class BlepWave : int { Sine = 0, Saw, Square, Triangle };
class BlepOsc
{
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 setFrequency (float f) { freq = juce::jmax (0.0f, f); inc = freq / (float) sr; }
void resetPhase (float p = 0.0f) { phase = juce::jlimit (0.0f, 1.0f, p); }
void setFrequency (float f)
{
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()
{

View File

@@ -3,13 +3,14 @@
#include <atomic>
#include <unordered_map>
#include <string>
#include <vector>
struct SliderDetail {
std::string label;
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 }
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 } },
{ "tone", { "Tone", 100.0f, 8000.0f, 10.0f, 3000.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
{
std::atomic<int> waveform{ -1 };
std::atomic<float>* wtPhase{};
// Amp ADSR
std::atomic<float>* adsrAttack{};
@@ -137,9 +156,28 @@ struct NeuralSharedParams
std::atomic<float>* filterOn{};
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
std::atomic<float>* lowGainDbls{};
std::atomic<float>* midGainDbls{};
std::atomic<float>* highGainDbls{};
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{};
};

View File

@@ -1,24 +1,31 @@
#include "PluginProcessor.h"
#include "PluginEditor.h"
#include "ScopeComponent.h"
#include "WavetableOsc.h"
#include <array>
//==============================================================================
NeuralSynthAudioProcessorEditor::NeuralSynthAudioProcessorEditor (NeuralSynthAudioProcessor& p)
: AudioProcessorEditor (&p),
audioProcessor (p),
mainScopeComponent(audioProcessor.getAudioBufferQueue())
mainScopeComponent(audioProcessor.getAudioBufferQueue()),
keyboardComponent(keyboardState, juce::MidiKeyboardComponent::horizontalKeyboard)
{
auto& tree = audioProcessor.parameters;
addAndMakeVisible(mainScopeComponent);
waveformSelector.setModel(&waveformContents);
waveformContents.onSelect = [this](int row)
{
// write to the parameter so voices update safely
audioProcessor.parameters.getParameterAsValue("waveform") = (float)juce::jlimit(0, 3, row);
};
addAndMakeVisible(waveformSelector);
keyboardState.addListener(this);
keyboardComponent.setMidiChannel(1);
keyboardComponent.setScrollButtonsVisible(true);
keyboardComponent.setKeyWidth(36.0f);
keyboardComponent.setAvailableRange(36, 96);
keyboardComponent.setVelocity(1.0f, true);
addAndMakeVisible(keyboardComponent);
presetMenuButton.setButtonText("Preset");
presetMenuButton.onClick = [this] { showPresetMenu(); };
addAndMakeVisible(presetMenuButton);
// --- Panels ---
adsrComponent.emplace(tree, "adsr", "Amp Env");
@@ -92,6 +99,76 @@ NeuralSynthAudioProcessorEditor::NeuralSynthAudioProcessorEditor (NeuralSynthAud
});
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
addAndMakeVisible(masterLevelSlider);
masterLevelLabel.setText("Master", juce::dontSendNotification);
@@ -102,18 +179,25 @@ NeuralSynthAudioProcessorEditor::NeuralSynthAudioProcessorEditor (NeuralSynthAud
masterLevelLabel.setJustificationType(juce::Justification::centred);
addAndMakeVisible(masterLevelLabel);
// Blank placeholder
addAndMakeVisible(blankPanel);
// Attach master parameter
gainAttachment = std::make_unique<juce::AudioProcessorValueTreeState::SliderAttachment>(
audioProcessor.parameters, "master", masterLevelSlider.slider);
lastPresetIndex = audioProcessor.getCurrentPresetIndex();
updatePresetButtonLabel();
startTimerHz(10);
setSize(1400, 720);
}
//==============================================================================
NeuralSynthAudioProcessorEditor::~NeuralSynthAudioProcessorEditor() = default;
NeuralSynthAudioProcessorEditor::~NeuralSynthAudioProcessorEditor()
{
stopTimer();
if (customPresetWindow != nullptr)
customPresetWindow.reset();
keyboardState.removeListener(this);
}
//==============================================================================
void NeuralSynthAudioProcessorEditor::paint (juce::Graphics& g)
@@ -124,47 +208,191 @@ void NeuralSynthAudioProcessorEditor::paint (juce::Graphics& g)
//==============================================================================
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;
grid.templateRows = {
juce::Grid::TrackInfo(juce::Grid::Fr(20)), // scope row
juce::Grid::TrackInfo(juce::Grid::Fr(40)), // row 1
juce::Grid::TrackInfo(juce::Grid::Fr(40)) // row 2
juce::Grid::TrackInfo(juce::Grid::Fr(10)), // scope band
juce::Grid::TrackInfo(juce::Grid::Fr(35)), // row 1
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 = {
juce::Grid::TrackInfo(juce::Grid::Fr(18)),
juce::Grid::TrackInfo(juce::Grid::Fr(18)),
juce::Grid::TrackInfo(juce::Grid::Fr(18)),
juce::Grid::TrackInfo(juce::Grid::Fr(18)),
juce::Grid::TrackInfo(juce::Grid::Fr(18)),
juce::Grid::TrackInfo(juce::Grid::Fr(10))
juce::Grid::TrackInfo(juce::Grid::Fr(20)),
juce::Grid::TrackInfo(juce::Grid::Fr(20)),
juce::Grid::TrackInfo(juce::Grid::Fr(20)),
juce::Grid::TrackInfo(juce::Grid::Fr(20)),
juce::Grid::TrackInfo(juce::Grid::Fr(20))
};
// Row 0
grid.items.add(juce::GridItem(mainScopeComponent)
.withArea(juce::GridItem::Span(1), juce::GridItem::Span(5)));
grid.items.add(juce::GridItem(waveformSelector)
.withArea(juce::GridItem::Span(1), juce::GridItem::Span(1)));
grid.rowGap = juce::Grid::Px(0);
grid.columnGap = juce::Grid::Px(0);
// Row 1
grid.items.add(juce::GridItem(*adsrComponent));
grid.items.add(juce::GridItem(*chorusComponent));
grid.items.add(juce::GridItem(*delayComponent));
grid.items.add(juce::GridItem(*reverbComponent));
grid.items.add(juce::GridItem(*eqComponent));
grid.items.add(juce::GridItem(masterLevelLabel));
grid.items.clear();
// Row 2
grid.items.add(juce::GridItem(*flangerComponent));
// 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(*chorusComponent ));
grid.items.add(juce::GridItem(*delayComponent ));
grid.items.add(juce::GridItem(*reverbComponent ));
grid.items.add(juce::GridItem(*eqComponent ));
// Row 3 (bottom row of panels): Flanger, Distortion, Filter, Filter Env, Wavetable
grid.items.add(juce::GridItem(*flangerComponent ));
grid.items.add(juce::GridItem(*distortionComponent));
grid.items.add(juce::GridItem(*filterComponent));
grid.items.add(juce::GridItem(*filterEnvComponent));
grid.items.add(juce::GridItem(blankPanel));
grid.items.add(juce::GridItem(masterLevelSlider));
grid.items.add(juce::GridItem(*filterComponent ));
grid.items.add(juce::GridItem(*filterEnvComponent ));
grid.items.add(juce::GridItem(*wtComponent ));
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");
}

View File

@@ -4,6 +4,7 @@
#include "PluginProcessor.h"
#include "GraphComponent.h"
#include "ScopeComponent.h"
#include "UI/CustomPresetWindow.h"
//============================== ScopeSliderComponent ==========================
// A generic panel: optional scope/graph + rotary sliders + labels.
@@ -25,6 +26,8 @@ public:
attachments.push_back(std::make_unique<juce::AudioProcessorValueTreeState::SliderAttachment>(
tree, paramGroup + "_" + name, *sliders.back()));
paramNames.push_back(name);
labels.back()->setText(sliderDetail.label, juce::dontSendNotification);
sliders.back()->setRange(sliderDetail.min, sliderDetail.max);
}
@@ -55,12 +58,74 @@ public:
addAndMakeVisible(titleLabel);
}
// Bypass toggle (per panel), id "<group>_on"
bypassButton.setButtonText("On");
bypassButton.setClickingTogglesState(true);
addAndMakeVisible(bypassButton);
bypassAttachment = std::make_unique<juce::AudioProcessorValueTreeState::ButtonAttachment>(
treeRef, paramGroupId + "_on", bypassButton);
if (tree.getParameter(paramGroupId + "_on") != nullptr)
{
hasBypass = true;
bypassButton.setButtonText("On");
bypassButton.setClickingTogglesState(true);
addAndMakeVisible(bypassButton);
bypassAttachment = std::make_unique<juce::AudioProcessorValueTreeState::ButtonAttachment>(
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) {
@@ -76,7 +141,17 @@ public:
addAndMakeVisible(*graphScope);
}
juce::Slider* getSlider(const std::string& name) { return findSlider(name); }
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
{
g.fillAll(juce::Colours::darkgrey);
@@ -89,9 +164,22 @@ private:
// --- Top bar (manual) ----------------------------------------------
auto area = getLocalBounds().reduced(10);
auto top = area.removeFromTop(22);
auto btnW = 46;
bypassButton.setBounds(top.removeFromRight(btnW).reduced(2, 1));
titleLabel.setBounds(top);
if (hasBypass)
{
auto btnW = 46;
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);
// --- Rest (grid) ----------------------------------------------------
juce::Grid grid;
@@ -146,9 +234,14 @@ private:
std::vector<std::unique_ptr<juce::Slider>> sliders;
std::vector<std::unique_ptr<juce::Label>> labels;
std::vector<std::unique_ptr<juce::AudioProcessorValueTreeState::SliderAttachment>> attachments;
std::vector<std::string> paramNames;
juce::Component* topBarAccessory{ nullptr };
int accessoryPreferredWidth{ 120 };
juce::ToggleButton bypassButton;
std::unique_ptr<juce::AudioProcessorValueTreeState::ButtonAttachment> bypassAttachment;
bool hasBypass{ false };
juce::Label titleLabel;
@@ -254,34 +347,6 @@ private:
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 =========================
class MasterVolumeComponent : public juce::Component
{
@@ -290,6 +355,7 @@ public:
{
slider.setSliderStyle(juce::Slider::LinearBarVertical);
slider.setTextBoxStyle(juce::Slider::NoTextBox, false, 20, 20);
slider.setRange(-24.0f, 24.0f, 0.1f);
addAndMakeVisible(slider);
}
@@ -302,7 +368,9 @@ public:
};
//============================== Editor =======================================
class NeuralSynthAudioProcessorEditor : public juce::AudioProcessorEditor
class NeuralSynthAudioProcessorEditor : public juce::AudioProcessorEditor,
private juce::Timer,
private juce::MidiKeyboardStateListener
{
public:
NeuralSynthAudioProcessorEditor (NeuralSynthAudioProcessor&);
@@ -310,13 +378,22 @@ public:
void paint (juce::Graphics&) 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:
NeuralSynthAudioProcessor& audioProcessor;
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (NeuralSynthAudioProcessorEditor)
juce::ListBox waveformSelector;
WaveformSelectorContents waveformContents;
void updatePresetButtonLabel();
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> chorusComponent;
@@ -327,6 +404,7 @@ private:
std::optional<ScopeSliderComponent> distortionComponent;
std::optional<ScopeSliderComponent> filterComponent;
std::optional<ScopeSliderComponent> filterEnvComponent; // Filter Env panel
std::optional<ScopeSliderComponent> wtComponent; // Wavetable panel
MasterVolumeComponent masterLevelSlider;
juce::Label masterLevelLabel;
@@ -336,6 +414,8 @@ private:
std::unique_ptr<juce::AudioProcessorValueTreeState::SliderAttachment> gainAttachment;
ScopeComponent<float> mainScopeComponent;
juce::Component blankPanel;
juce::MidiKeyboardState keyboardState;
juce::MidiKeyboardComponent keyboardComponent;
juce::ComboBox layerSelector;
bool controllingLayerB { false };
};

View File

@@ -1,13 +1,54 @@
#include "PluginProcessor.h"
#include "PluginEditor.h"
#include "WavetableOsc.h"
#include <array>
#include <map>
//==============================================================================
NeuralSynthAudioProcessor::NeuralSynthAudioProcessor()
: parameters(*this, nullptr, "PARAMETERS", createParameterLayout())
, AudioProcessor(BusesProperties().withOutput("Output", juce::AudioChannelSet::stereo(), true))
, 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) ===
sp.chorusOn = parameters.getRawParameterValue("chorus_on");
@@ -158,31 +199,28 @@ void NeuralSynthAudioProcessor::changeProgramName (int, const juce::String&) {}
void NeuralSynthAudioProcessor::prepareToPlay (double sampleRate, int samplesPerBlock)
{
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);
}
void NeuralSynthAudioProcessor::releaseResources() {}
bool NeuralSynthAudioProcessor::isBusesLayoutSupported (const BusesLayout& layouts) const
/*bool NeuralSynthAudioProcessor::isBusesLayoutSupported (const BusesLayout& layouts) const
{
if (layouts.getMainOutputChannelSet() != juce::AudioChannelSet::mono()
&& layouts.getMainOutputChannelSet() != juce::AudioChannelSet::stereo())
return false;
return true;
}
}*/
void NeuralSynthAudioProcessor::processBlock(juce::AudioSampleBuffer& buffer, juce::MidiBuffer& midiMessages)
{
const int newWaveform = sp.waveform.exchange(-1);
if (newWaveform != -1) {
audioEngine.applyToVoices([newWaveform](NeuralSynthVoice* v)
{
v->changeWaveform(newWaveform);
});
}
juce::ScopedNoDenormals noDenormals;
auto totalNumInputChannels = getTotalNumInputChannels();
auto totalNumOutputChannels = getTotalNumOutputChannels();
@@ -193,6 +231,8 @@ void NeuralSynthAudioProcessor::processBlock(juce::AudioSampleBuffer& buffer, ju
buffer.clear(i, 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());
}
@@ -210,9 +250,26 @@ void NeuralSynthAudioProcessor::setStateInformation (const void* data, int sizeI
void NeuralSynthAudioProcessor::parameterChanged(const juce::String& id, float newValue)
{
juce::ignoreUnused(newValue);
if (id == "waveform")
sp.waveform.store((int)newValue, std::memory_order_release);
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);
currentPresetIndex = -1;
}
}
//==============================================================================
@@ -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()
{
std::vector<std::unique_ptr<juce::RangedAudioParameter>> params;
params.push_back(std::make_unique<juce::AudioParameterChoice>(
"waveform", "Waveform",
juce::StringArray{ "Sine", "Saw", "Square", "Triangle" }, 0));
params.push_back(std::make_unique<juce::AudioParameterBool>(
"wt_on", "Layer A On", true));
params.push_back(std::make_unique<juce::AudioParameterBool>(
"wt2_on", "Layer B On", false));
// Per-panel bypass toggles (default OFF)
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, "distortion");
buildParams(params, "filter");
buildParams(params, "wt");
buildParams(params, "wt2");
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",
juce::NormalisableRange<float>(-24.0f, 24.0f, 0.1f), 0.5f));

View File

@@ -12,6 +12,15 @@ class NeuralSynthAudioProcessor : public juce::AudioProcessor,
private juce::AudioProcessorValueTreeState::Listener
{
public:
struct PresetDefinition
{
juce::String category;
juce::String name;
int wtBankIndex;
int wt2BankIndex { -1 };
std::vector<std::pair<juce::String, float>> parameterValues;
};
NeuralSynthAudioProcessor();
~NeuralSynthAudioProcessor() override;
@@ -53,6 +62,10 @@ public:
const std::string& paramGroup);
juce::AudioProcessorValueTreeState::ParameterLayout createParameterLayout();
void applyPreset(int index);
const std::vector<PresetDefinition>& getFactoryPresets() const noexcept { return factoryPresets; }
int getCurrentPresetIndex() const noexcept { return currentPresetIndex; }
// Utilities
juce::MidiMessageCollector& getMidiMessageCollector() noexcept { return midiMessageCollector; }
AudioBufferQueue<float>& getAudioBufferQueue() noexcept { return audioBufferQueue; }
@@ -87,4 +100,13 @@ private:
// Scope collector (uses audioBufferQueue, so declare after it)
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;
};

View File

@@ -1,5 +1,13 @@
#include "SynthVoice.h"
#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
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)
tempBuffer.setSize ((int) spec.numChannels, (int) spec.maximumBlockSize,
@@ -23,6 +48,10 @@ void NeuralSynthVoice::prepare (const juce::dsp::ProcessSpec& newSpec)
// --- Prepare chain elements
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
{
@@ -73,234 +102,120 @@ void NeuralSynthVoice::renderNextBlock (juce::AudioBuffer<float>& outputBuffer,
if (! adsr.isActive())
clearCurrentNote();
// Apply pending waveform change (from GUI / processor thread)
const int wf = pendingWaveform.exchange (-1, std::memory_order_acq_rel);
if (wf != -1)
setWaveform (wf);
// --- Generate oscillator into temp buffer
// --- Generate oscillator into temp buffer (BLEP or Wavetable)
tempBuffer.clear();
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)
{
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)
tempBuffer.getWritePointer (ch)[i] = s;
tempBuffer.getWritePointer (ch)[i] = combined;
}
auto block = tempBlock.getSubBlock (0, (size_t) numSamples);
// ================================================================
// Flanger (pre-filter) manual per-sample to set varying delay
// ================================================================
{
auto& flanger = chain.get<flangerIndex>();
renderFlanger(numSamples, numCh);
renderADSR(numSamples, numCh);
renderChorus(block);
renderSimpleDelay(block);
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
@@ -321,10 +236,22 @@ void NeuralSynthVoice::renderNextBlock (juce::AudioBuffer<float>& outputBuffer,
void NeuralSynthVoice::noteStarted()
{
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.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
if (shared.chorusCentre) chain.get<chorusIndex>().setCentreDelay (shared.chorusCentre->load());
@@ -372,6 +299,7 @@ void NeuralSynthVoice::notePitchbendChanged()
{
const float freqHz = (float) getCurrentlyPlayingNote().getFrequencyInHertz();
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;
}
}

View File

@@ -1,8 +1,11 @@
#pragma once
#include <JuceHeader.h>
#include <functional> // <-- for std::function used by WaveShaper
#include <memory>
#include <cmath>
#include "NeuralSharedParams.h"
#include "BlepOsc.h"
#include "WavetableOsc.h"
//==============================================================================
// A single polyBLEP oscillator voice with per-voice ADSR, filter ADSR,
@@ -25,11 +28,45 @@ public:
void noteTimbreChanged() override {}
void noteKeyStateChanged() override {}
// Called from the processor when the GUI waveform param changes
void changeWaveform (int wf) { setWaveform (wf); }
private:
void setWaveform (int waveformType);
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:
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) ===============================
using DelayLine = juce::dsp::DelayLine<float,
@@ -41,6 +78,15 @@ private:
using Reverb = juce::dsp::Reverb;
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
{
flangerIndex = 0,
@@ -77,9 +123,14 @@ private:
juce::dsp::ProcessSpec spec {};
// ==== Oscillator (polyBLEP) ============================================
BlepOsc osc;
std::atomic<int> pendingWaveform {-1}; // set by changeWaveform()
// ==== Oscillators ======================================================
BlepOsc osc; // polyBLEP oscillator
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 ===============================================
juce::ADSR adsr;

46
Source/SynthVoice/ADSR.h Normal file
View 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);
}
}
}
}

View 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));
}
}

View 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
View 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));
}

View 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;
}
}
}

View 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));
}
}

View 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));
}
}

View 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);
}

View File

@@ -0,0 +1,10 @@
#pragma once
#include <JuceHeader.h>
class CustomPresetWindow : public juce::DocumentWindow
{
public:
CustomPresetWindow();
void closeButtonPressed() override;
};

View File

@@ -2,6 +2,11 @@
#include <JuceHeader.h>
#include <vector>
#include <cmath>
#include <functional>
#include <initializer_list>
#include <limits>
#include <utility>
#include <array>
// ============================== Design =======================================
// - 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;
}
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], ...
// We'll reconstruct complex bins for easy masking.
// Helper to zero all harmonics above kMax (inclusive index in [0..N/2])
auto maskAndIFFT = [&](int level, int kMax)
{
// 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
auto* bins = freq.asComplex();
// DC & Nyquist are purely real in real-FFT
@@ -220,19 +230,762 @@ namespace WT
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
// =======================================================================
class Osc
{
public:
void prepare (double sr) { sampleRate = 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; }
void setMorph (float m) { morph = m; } // 0..frames-1 (continuous)
void prepare (double sr)
{
sampleRate = juce::jmax (1.0, sr);
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); }
[[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;
@@ -241,8 +994,10 @@ namespace WT
const float preferL0 = 1.0f - juce::jlimit(0.0f, 1.0f,
(float)l0 - (float)bank->chooseLevel(freq * 0.99f, sampleRate));
const float s0 = bank->lookup(morph, l0, phase);
const float s1 = bank->lookup(morph, l1, phase);
const float morphValue = std::isnan(morphOverride) ? morph : clampMorph (morphOverride);
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
phase += phaseInc;
@@ -251,6 +1006,13 @@ namespace WT
}
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;
double sampleRate { 44100.0 };
float freq { 0.0f };

4
build.sh Executable file
View File

@@ -0,0 +1,4 @@
pushd Builds/LinuxMakefile/
make clean
make
popd