Upload files to "Source"
This commit is contained in:
@@ -14,7 +14,24 @@ void NeuralSynthVoice::prepare (const juce::dsp::ProcessSpec& newSpec)
|
|||||||
|
|
||||||
// --- Oscillator
|
// --- Oscillator
|
||||||
osc.prepare (spec.sampleRate);
|
osc.prepare (spec.sampleRate);
|
||||||
setWaveform (0); // default to sine
|
osc.setWave (BlepWave::Sine);
|
||||||
|
|
||||||
|
// --- Wavetable oscillator factory banks ---
|
||||||
|
wtOsc.prepare (spec.sampleRate);
|
||||||
|
morphLfo.prepare (spec.sampleRate);
|
||||||
|
currentWtBankIndex = -1;
|
||||||
|
wtOsc2.prepare (spec.sampleRate);
|
||||||
|
morphLfo2.prepare (spec.sampleRate);
|
||||||
|
currentWtBankIndex2 = -1;
|
||||||
|
|
||||||
|
const auto& library = WT::FactoryLibrary::get();
|
||||||
|
if (! library.empty())
|
||||||
|
{
|
||||||
|
wtOsc.setBank (library.front().bank);
|
||||||
|
currentWtBankIndex = 0;
|
||||||
|
wtOsc2.setBank (library.front().bank);
|
||||||
|
currentWtBankIndex2 = 0;
|
||||||
|
}
|
||||||
|
|
||||||
// --- Scratch buffer (IMPORTANT: allocate real memory)
|
// --- Scratch buffer (IMPORTANT: allocate real memory)
|
||||||
tempBuffer.setSize ((int) spec.numChannels, (int) spec.maximumBlockSize,
|
tempBuffer.setSize ((int) spec.numChannels, (int) spec.maximumBlockSize,
|
||||||
@@ -23,6 +40,10 @@ void NeuralSynthVoice::prepare (const juce::dsp::ProcessSpec& newSpec)
|
|||||||
|
|
||||||
// --- Prepare chain elements
|
// --- Prepare chain elements
|
||||||
chain.prepare (spec);
|
chain.prepare (spec);
|
||||||
|
chain.get<masterIndex>().setRampDurationSeconds (0.02f);
|
||||||
|
chain.get<limiterIndex>().setThreshold (-1.0f);
|
||||||
|
chain.get<limiterIndex>().setRelease (0.05f);
|
||||||
|
chain.get<limiterIndex>().reset();
|
||||||
|
|
||||||
// Set maximum delay sizes BEFORE runtime changes
|
// Set maximum delay sizes BEFORE runtime changes
|
||||||
{
|
{
|
||||||
@@ -73,20 +94,109 @@ void NeuralSynthVoice::renderNextBlock (juce::AudioBuffer<float>& outputBuffer,
|
|||||||
if (! adsr.isActive())
|
if (! adsr.isActive())
|
||||||
clearCurrentNote();
|
clearCurrentNote();
|
||||||
|
|
||||||
// Apply pending waveform change (from GUI / processor thread)
|
// --- Generate oscillator into temp buffer (BLEP or Wavetable)
|
||||||
const int wf = pendingWaveform.exchange (-1, std::memory_order_acq_rel);
|
|
||||||
if (wf != -1)
|
|
||||||
setWaveform (wf);
|
|
||||||
|
|
||||||
// --- Generate oscillator into temp buffer
|
|
||||||
tempBuffer.clear();
|
tempBuffer.clear();
|
||||||
const int numCh = juce::jmin ((int) spec.numChannels, tempBuffer.getNumChannels());
|
const int numCh = juce::jmin ((int) spec.numChannels, tempBuffer.getNumChannels());
|
||||||
|
|
||||||
|
const auto& library = WT::FactoryLibrary::get();
|
||||||
|
const int librarySize = (int) library.size();
|
||||||
|
|
||||||
|
if (librarySize > 0 && shared.wtBank)
|
||||||
|
{
|
||||||
|
const int targetBank = juce::jlimit (0, librarySize - 1,
|
||||||
|
(int) std::lround (shared.wtBank->load()));
|
||||||
|
if (targetBank != currentWtBankIndex)
|
||||||
|
{
|
||||||
|
wtOsc.setBank (library[(size_t) targetBank].bank);
|
||||||
|
currentWtBankIndex = targetBank;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (librarySize > 0 && shared.wt2Bank)
|
||||||
|
{
|
||||||
|
const int targetBank2 = juce::jlimit (0, librarySize - 1,
|
||||||
|
(int) std::lround (shared.wt2Bank->load()));
|
||||||
|
if (targetBank2 != currentWtBankIndex2)
|
||||||
|
{
|
||||||
|
wtOsc2.setBank (library[(size_t) targetBank2].bank);
|
||||||
|
currentWtBankIndex2 = targetBank2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const bool useWTLayerA = (shared.wtOn && shared.wtOn->load() > 0.5f)
|
||||||
|
&& wtOsc.getFrameCount() > 0;
|
||||||
|
const bool useWTLayerB = (shared.wt2On && shared.wt2On->load() > 0.5f)
|
||||||
|
&& wtOsc2.getFrameCount() > 0;
|
||||||
|
|
||||||
|
const float morphMaxA = wtOsc.getMaxMorph();
|
||||||
|
const float morphBaseA = shared.wtMorph
|
||||||
|
? juce::jlimit (0.0f, morphMaxA, shared.wtMorph->load())
|
||||||
|
: 0.0f;
|
||||||
|
const float lfoDepthA = shared.wtLfoDepth ? shared.wtLfoDepth->load() : 0.0f;
|
||||||
|
const float lfoRateA = shared.wtLfoRate ? shared.wtLfoRate->load() : 1.0f;
|
||||||
|
const int lfoShapeA = shared.wtLfoShape ? (int) std::lround (shared.wtLfoShape->load()) : 0;
|
||||||
|
|
||||||
|
morphLfo.setRate (lfoRateA);
|
||||||
|
morphLfo.setShape (lfoShapeA);
|
||||||
|
|
||||||
|
const float depthFramesA = juce::jlimit (0.0f, morphMaxA, lfoDepthA);
|
||||||
|
|
||||||
|
const float morphMaxB = wtOsc2.getMaxMorph();
|
||||||
|
const float morphBaseB = shared.wt2Morph
|
||||||
|
? juce::jlimit (0.0f, morphMaxB, shared.wt2Morph->load())
|
||||||
|
: 0.0f;
|
||||||
|
const float lfoDepthB = shared.wt2LfoDepth ? shared.wt2LfoDepth->load() : 0.0f;
|
||||||
|
const float lfoRateB = shared.wt2LfoRate ? shared.wt2LfoRate->load() : 0.3f;
|
||||||
|
const int lfoShapeB = shared.wt2LfoShape ? (int) std::lround (shared.wt2LfoShape->load()) : 0;
|
||||||
|
|
||||||
|
morphLfo2.setRate (lfoRateB);
|
||||||
|
morphLfo2.setShape (lfoShapeB);
|
||||||
|
|
||||||
|
const float depthFramesB = juce::jlimit (0.0f, morphMaxB, lfoDepthB);
|
||||||
|
|
||||||
|
const float levelA = shared.wtLevel ? juce::jlimit (0.0f, 1.0f, shared.wtLevel->load()) : 0.0f;
|
||||||
|
const float levelB = shared.wt2Level ? juce::jlimit (0.0f, 1.0f, shared.wt2Level->load()) : 0.0f;
|
||||||
|
const float safeLevelSum = juce::jlimit (0.5f, 2.0f, levelA + levelB + 0.0001f);
|
||||||
|
const float mixGain = 0.45f / safeLevelSum;
|
||||||
|
|
||||||
for (int i = 0; i < numSamples; ++i)
|
for (int i = 0; i < numSamples; ++i)
|
||||||
{
|
{
|
||||||
const float s = osc.process();
|
float sampleA = useWTLayerA ? 0.0f : osc.process();
|
||||||
|
if (useWTLayerA)
|
||||||
|
{
|
||||||
|
const float lfoValueA = morphLfo.process();
|
||||||
|
const float headroomNegA = juce::jmin (depthFramesA, morphBaseA);
|
||||||
|
const float headroomPosA = juce::jmin (depthFramesA, morphMaxA - morphBaseA);
|
||||||
|
const float offsetA = (lfoValueA >= 0.0f ? lfoValueA * headroomPosA
|
||||||
|
: lfoValueA * headroomNegA);
|
||||||
|
const float morphValueA = juce::jlimit (0.0f, morphMaxA, morphBaseA + offsetA);
|
||||||
|
sampleA = wtOsc.process (morphValueA);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
morphLfo.process(); // advance for consistency
|
||||||
|
}
|
||||||
|
|
||||||
|
float sampleB = 0.0f;
|
||||||
|
if (useWTLayerB)
|
||||||
|
{
|
||||||
|
const float lfoValueB = morphLfo2.process();
|
||||||
|
const float headroomNegB = juce::jmin (depthFramesB, morphBaseB);
|
||||||
|
const float headroomPosB = juce::jmin (depthFramesB, morphMaxB - morphBaseB);
|
||||||
|
const float offsetB = (lfoValueB >= 0.0f ? lfoValueB * headroomPosB
|
||||||
|
: lfoValueB * headroomNegB);
|
||||||
|
const float morphValueB = juce::jlimit (0.0f, morphMaxB, morphBaseB + offsetB);
|
||||||
|
sampleB = wtOsc2.process (morphValueB);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
morphLfo2.process();
|
||||||
|
}
|
||||||
|
|
||||||
|
const float combined = mixGain * ((sampleA * levelA) + (sampleB * levelB));
|
||||||
|
|
||||||
for (int ch = 0; ch < numCh; ++ch)
|
for (int ch = 0; ch < numCh; ++ch)
|
||||||
tempBuffer.getWritePointer (ch)[i] = s;
|
tempBuffer.getWritePointer (ch)[i] = combined;
|
||||||
}
|
}
|
||||||
|
|
||||||
auto block = tempBlock.getSubBlock (0, (size_t) numSamples);
|
auto block = tempBlock.getSubBlock (0, (size_t) numSamples);
|
||||||
@@ -321,10 +431,22 @@ void NeuralSynthVoice::renderNextBlock (juce::AudioBuffer<float>& outputBuffer,
|
|||||||
void NeuralSynthVoice::noteStarted()
|
void NeuralSynthVoice::noteStarted()
|
||||||
{
|
{
|
||||||
const float freqHz = (float) getCurrentlyPlayingNote().getFrequencyInHertz();
|
const float freqHz = (float) getCurrentlyPlayingNote().getFrequencyInHertz();
|
||||||
|
const float initPhase = shared.wtPhase
|
||||||
|
? juce::jlimit (0.0f, 1.0f, shared.wtPhase->load())
|
||||||
|
: 0.0f;
|
||||||
|
|
||||||
// Oscillator frequency and phase retrigger
|
// Oscillator frequency and phase retrigger (BLEP + WT)
|
||||||
osc.setFrequency (freqHz);
|
osc.setFrequency (freqHz);
|
||||||
osc.resetPhase (0.0f);
|
osc.resetPhase (initPhase);
|
||||||
|
wtOsc.setFrequency (freqHz);
|
||||||
|
wtOsc.resetPhase (initPhase);
|
||||||
|
morphLfo.reset();
|
||||||
|
const float initPhaseB = shared.wt2Phase
|
||||||
|
? juce::jlimit (0.0f, 1.0f, shared.wt2Phase->load())
|
||||||
|
: initPhase;
|
||||||
|
wtOsc2.setFrequency (freqHz);
|
||||||
|
wtOsc2.resetPhase (initPhaseB);
|
||||||
|
morphLfo2.reset();
|
||||||
|
|
||||||
// Chorus snapshot
|
// Chorus snapshot
|
||||||
if (shared.chorusCentre) chain.get<chorusIndex>().setCentreDelay (shared.chorusCentre->load());
|
if (shared.chorusCentre) chain.get<chorusIndex>().setCentreDelay (shared.chorusCentre->load());
|
||||||
@@ -372,6 +494,7 @@ void NeuralSynthVoice::notePitchbendChanged()
|
|||||||
{
|
{
|
||||||
const float freqHz = (float) getCurrentlyPlayingNote().getFrequencyInHertz();
|
const float freqHz = (float) getCurrentlyPlayingNote().getFrequencyInHertz();
|
||||||
osc.setFrequency (freqHz);
|
osc.setFrequency (freqHz);
|
||||||
|
wtOsc.setFrequency (freqHz);
|
||||||
}
|
}
|
||||||
|
|
||||||
//==============================================================================
|
//==============================================================================
|
||||||
@@ -384,15 +507,3 @@ void NeuralSynthVoice::noteStopped (bool allowTailOff)
|
|||||||
}
|
}
|
||||||
|
|
||||||
//==============================================================================
|
//==============================================================================
|
||||||
|
|
||||||
void NeuralSynthVoice::setWaveform (int waveformType)
|
|
||||||
{
|
|
||||||
switch (juce::jlimit (0, 3, waveformType))
|
|
||||||
{
|
|
||||||
case 0: osc.setWave (BlepWave::Sine); break;
|
|
||||||
case 1: osc.setWave (BlepWave::Saw); break;
|
|
||||||
case 2: osc.setWave (BlepWave::Square); break;
|
|
||||||
case 3: osc.setWave (BlepWave::Triangle); break;
|
|
||||||
default: osc.setWave (BlepWave::Sine); break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,8 +1,11 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
#include <JuceHeader.h>
|
#include <JuceHeader.h>
|
||||||
#include <functional> // <-- for std::function used by WaveShaper
|
#include <functional> // <-- for std::function used by WaveShaper
|
||||||
|
#include <memory>
|
||||||
|
#include <cmath>
|
||||||
#include "NeuralSharedParams.h"
|
#include "NeuralSharedParams.h"
|
||||||
#include "BlepOsc.h"
|
#include "BlepOsc.h"
|
||||||
|
#include "WavetableOsc.h"
|
||||||
|
|
||||||
//==============================================================================
|
//==============================================================================
|
||||||
// A single polyBLEP oscillator voice with per-voice ADSR, filter ADSR,
|
// A single polyBLEP oscillator voice with per-voice ADSR, filter ADSR,
|
||||||
@@ -25,11 +28,45 @@ public:
|
|||||||
void noteTimbreChanged() override {}
|
void noteTimbreChanged() override {}
|
||||||
void noteKeyStateChanged() override {}
|
void noteKeyStateChanged() override {}
|
||||||
|
|
||||||
// Called from the processor when the GUI waveform param changes
|
private:
|
||||||
void changeWaveform (int wf) { setWaveform (wf); }
|
struct MorphLFO
|
||||||
|
{
|
||||||
|
void prepare (double sr) { sampleRate = juce::jmax (1.0, sr); updateIncrement(); }
|
||||||
|
void reset() { phase = 0.0f; }
|
||||||
|
void setRate (float hz) { rate = juce::jlimit (0.0f, 30.0f, hz); updateIncrement(); }
|
||||||
|
void setShape (int idx) { shape = juce::jlimit (0, 3, idx); }
|
||||||
|
float process()
|
||||||
|
{
|
||||||
|
float value = 0.0f;
|
||||||
|
|
||||||
|
switch (shape)
|
||||||
|
{
|
||||||
|
case 1: value = 1.0f - 4.0f * std::abs(phase - 0.5f); break; // Triangle
|
||||||
|
case 2: value = 2.0f * phase - 1.0f; break; // Ramp up
|
||||||
|
case 3: value = 1.0f - 2.0f * phase; break; // Ramp down
|
||||||
|
default: value = std::sin (juce::MathConstants<float>::twoPi * phase); break; // Sine
|
||||||
|
}
|
||||||
|
|
||||||
|
phase += phaseInc;
|
||||||
|
if (phase >= 1.0f)
|
||||||
|
phase -= 1.0f;
|
||||||
|
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
private:
|
private:
|
||||||
void setWaveform (int waveformType);
|
void updateIncrement()
|
||||||
|
{
|
||||||
|
phaseInc = (float) (rate / (float) sampleRate);
|
||||||
|
if (phaseInc < 0.0f) phaseInc = 0.0f;
|
||||||
|
}
|
||||||
|
|
||||||
|
double sampleRate { 44100.0 };
|
||||||
|
float rate { 1.0f };
|
||||||
|
float phase { 0.0f };
|
||||||
|
float phaseInc { 0.0f };
|
||||||
|
int shape { 0 };
|
||||||
|
};
|
||||||
|
|
||||||
//=== Processing chain (without oscillator) ===============================
|
//=== Processing chain (without oscillator) ===============================
|
||||||
using DelayLine = juce::dsp::DelayLine<float,
|
using DelayLine = juce::dsp::DelayLine<float,
|
||||||
@@ -77,9 +114,14 @@ private:
|
|||||||
|
|
||||||
juce::dsp::ProcessSpec spec {};
|
juce::dsp::ProcessSpec spec {};
|
||||||
|
|
||||||
// ==== Oscillator (polyBLEP) ============================================
|
// ==== Oscillators ======================================================
|
||||||
BlepOsc osc;
|
BlepOsc osc; // polyBLEP oscillator
|
||||||
std::atomic<int> pendingWaveform {-1}; // set by changeWaveform()
|
WT::Osc wtOsc; // wavetable oscillator (shared bank)
|
||||||
|
MorphLFO morphLfo;
|
||||||
|
int currentWtBankIndex { -1 };
|
||||||
|
WT::Osc wtOsc2; // secondary wavetable oscillator
|
||||||
|
MorphLFO morphLfo2;
|
||||||
|
int currentWtBankIndex2 { -1 };
|
||||||
|
|
||||||
// ==== Envelopes & Filter ===============================================
|
// ==== Envelopes & Filter ===============================================
|
||||||
juce::ADSR adsr;
|
juce::ADSR adsr;
|
||||||
|
|||||||
@@ -2,6 +2,11 @@
|
|||||||
#include <JuceHeader.h>
|
#include <JuceHeader.h>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
#include <cmath>
|
#include <cmath>
|
||||||
|
#include <functional>
|
||||||
|
#include <initializer_list>
|
||||||
|
#include <limits>
|
||||||
|
#include <utility>
|
||||||
|
#include <array>
|
||||||
|
|
||||||
// ============================== Design =======================================
|
// ============================== Design =======================================
|
||||||
// - Bank with F frames, each frame is a single-cycle table of N samples.
|
// - Bank with F frames, each frame is a single-cycle table of N samples.
|
||||||
@@ -114,12 +119,17 @@ namespace WT
|
|||||||
time.data[2 * n + 1] = 0.0f;
|
time.data[2 * n + 1] = 0.0f;
|
||||||
}
|
}
|
||||||
fft.performRealOnlyForwardTransform(time.data.data());
|
fft.performRealOnlyForwardTransform(time.data.data());
|
||||||
|
const auto spectrum = time.data; // snapshot packed spectrum for reuse
|
||||||
// After JUCE real FFT, bins are laid out as: Re[0], Re[N/2], Re[1], Im[1], Re[2], Im[2], ...
|
// After JUCE real FFT, bins are laid out as: Re[0], Re[N/2], Re[1], Im[1], Re[2], Im[2], ...
|
||||||
// We'll reconstruct complex bins for easy masking.
|
// We'll reconstruct complex bins for easy masking.
|
||||||
|
|
||||||
// Helper to zero all harmonics above kMax (inclusive index in [0..N/2])
|
// Helper to zero all harmonics above kMax (inclusive index in [0..N/2])
|
||||||
auto maskAndIFFT = [&](int level, int kMax)
|
auto maskAndIFFT = [&](int level, int kMax)
|
||||||
{
|
{
|
||||||
|
// Restore the original spectrum before masking this mip level
|
||||||
|
for (size_t idx = 0; idx < spectrum.size(); ++idx)
|
||||||
|
time.data[idx] = spectrum[idx];
|
||||||
|
|
||||||
// Copy time.data into working complex bins
|
// Copy time.data into working complex bins
|
||||||
auto* bins = freq.asComplex();
|
auto* bins = freq.asComplex();
|
||||||
// DC & Nyquist are purely real in real-FFT
|
// DC & Nyquist are purely real in real-FFT
|
||||||
@@ -220,19 +230,762 @@ namespace WT
|
|||||||
std::vector<std::vector<std::vector<float>>> tables;
|
std::vector<std::vector<std::vector<float>>> tables;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
struct Preset
|
||||||
|
{
|
||||||
|
juce::String category;
|
||||||
|
juce::String name;
|
||||||
|
std::shared_ptr<Bank> bank;
|
||||||
|
};
|
||||||
|
|
||||||
|
class FactoryLibrary
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
static const std::vector<Preset>& get()
|
||||||
|
{
|
||||||
|
static const std::vector<Preset> presets = buildFactoryLibrary();
|
||||||
|
return presets;
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
using WaveFn = std::function<float(float)>;
|
||||||
|
|
||||||
|
static WaveFn additive(const std::initializer_list<std::pair<int, float>>& partials)
|
||||||
|
{
|
||||||
|
const auto coeffs = std::vector<std::pair<int, float>>(partials);
|
||||||
|
return [coeffs](float phase)
|
||||||
|
{
|
||||||
|
float v = 0.0f;
|
||||||
|
for (auto [harm, gain] : coeffs)
|
||||||
|
v += gain * std::sin((float)harm * phase);
|
||||||
|
return v;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
static WaveFn pulse(float duty)
|
||||||
|
{
|
||||||
|
duty = juce::jlimit(0.01f, 0.99f, duty);
|
||||||
|
return [duty](float phase)
|
||||||
|
{
|
||||||
|
const float norm = phase / juce::MathConstants<float>::twoPi;
|
||||||
|
return (norm < duty ? 1.0f : -1.0f);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
static WaveFn bendFold(float amount)
|
||||||
|
{
|
||||||
|
return [amount](float phase)
|
||||||
|
{
|
||||||
|
float x = std::sin(phase);
|
||||||
|
x = juce::jlimit(-1.0f, 1.0f, x + amount * x * x * x);
|
||||||
|
return x;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
static std::vector<float> renderWave(size_t tableSize, const WaveFn& fn)
|
||||||
|
{
|
||||||
|
std::vector<float> table(tableSize, 0.0f);
|
||||||
|
for (size_t n = 0; n < tableSize; ++n)
|
||||||
|
{
|
||||||
|
const float phase = (float)(juce::MathConstants<double>::twoPi * (double)n / (double)tableSize);
|
||||||
|
table[n] = fn(phase);
|
||||||
|
}
|
||||||
|
// Remove any DC component before normalising so waves stay centred.
|
||||||
|
float mean = 0.0f;
|
||||||
|
for (float v : table)
|
||||||
|
mean += v;
|
||||||
|
mean /= (float)tableSize;
|
||||||
|
for (auto& v : table)
|
||||||
|
v -= mean;
|
||||||
|
Bank::normalise(table);
|
||||||
|
return table;
|
||||||
|
}
|
||||||
|
|
||||||
|
static std::vector<std::vector<float>> generateFrames(size_t tableSize,
|
||||||
|
const std::vector<WaveFn>& keyWaves,
|
||||||
|
int frames)
|
||||||
|
{
|
||||||
|
std::vector<std::vector<float>> out((size_t)frames, std::vector<float>(tableSize, 0.0f));
|
||||||
|
if (keyWaves.empty())
|
||||||
|
return out;
|
||||||
|
|
||||||
|
std::vector<std::vector<float>> rendered;
|
||||||
|
rendered.reserve(keyWaves.size());
|
||||||
|
for (const auto& fn : keyWaves)
|
||||||
|
rendered.push_back(renderWave(tableSize, fn));
|
||||||
|
|
||||||
|
if (rendered.size() == 1)
|
||||||
|
{
|
||||||
|
for (auto& frame : out)
|
||||||
|
frame = rendered.front();
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
const int segments = (int)rendered.size() - 1;
|
||||||
|
for (int f = 0; f < frames; ++f)
|
||||||
|
{
|
||||||
|
const float globalT = (float) f / (float) juce::jmax(1, frames - 1);
|
||||||
|
const float scaled = globalT * (float) segments;
|
||||||
|
const int seg = juce::jlimit(0, segments - 1, (int) std::floor(scaled));
|
||||||
|
const float t = scaled - (float) seg;
|
||||||
|
|
||||||
|
const auto& A = rendered[(size_t) seg];
|
||||||
|
const auto& B = rendered[(size_t) (seg + 1)];
|
||||||
|
auto& dst = out[(size_t) f];
|
||||||
|
|
||||||
|
for (size_t i = 0; i < tableSize; ++i)
|
||||||
|
dst[i] = juce::jmap(t, A[i], B[i]);
|
||||||
|
|
||||||
|
Bank::normalise(dst);
|
||||||
|
}
|
||||||
|
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
static std::vector<Preset> buildFactoryLibrary()
|
||||||
|
{
|
||||||
|
const size_t tableSize = 2048;
|
||||||
|
const int frames = 16;
|
||||||
|
const int levels = 6;
|
||||||
|
|
||||||
|
std::vector<Preset> presets;
|
||||||
|
presets.reserve(240);
|
||||||
|
|
||||||
|
const WaveFn sine = [](float ph){ return std::sin(ph); };
|
||||||
|
const WaveFn sawUp = [](float ph){
|
||||||
|
const float norm = (ph / juce::MathConstants<float>::twoPi) - std::floor(ph / juce::MathConstants<float>::twoPi);
|
||||||
|
return 2.0f * norm - 1.0f;
|
||||||
|
};
|
||||||
|
const WaveFn sawDown = [](float ph){
|
||||||
|
const float norm = (ph / juce::MathConstants<float>::twoPi) - std::floor(ph / juce::MathConstants<float>::twoPi);
|
||||||
|
return 1.0f - 2.0f * norm;
|
||||||
|
};
|
||||||
|
const WaveFn triangle = [](float ph){
|
||||||
|
float norm = ph / juce::MathConstants<float>::twoPi;
|
||||||
|
norm -= std::floor(norm);
|
||||||
|
float tri = norm < 0.25f ? norm * 4.0f :
|
||||||
|
norm < 0.75f ? 2.0f - norm * 4.0f :
|
||||||
|
norm * 4.0f - 4.0f;
|
||||||
|
return juce::jlimit(-1.0f, 1.0f, tri);
|
||||||
|
};
|
||||||
|
const WaveFn square50 = pulse(0.5f);
|
||||||
|
const WaveFn pulse30 = pulse(0.3f);
|
||||||
|
const WaveFn pulse10 = pulse(0.1f);
|
||||||
|
const WaveFn organ = additive({ {1, 1.0f}, {2, 0.5f}, {3, 0.35f}, {4, 0.2f} });
|
||||||
|
const WaveFn choir = additive({ {1, 1.0f}, {3, 0.4f}, {5, 0.25f}, {7, 0.18f} });
|
||||||
|
const WaveFn bell = additive({ {1, 1.0f}, {2, 0.7f}, {6, 0.45f}, {8, 0.3f}, {9, 0.2f} });
|
||||||
|
const WaveFn hollow = additive({ {2, 1.0f}, {4, 0.6f}, {6, 0.3f}, {8, 0.15f} });
|
||||||
|
const WaveFn airy = additive({ {1, 1.0f}, {4, 0.6f}, {6, 0.25f}, {9, 0.18f} });
|
||||||
|
const WaveFn bendSoft = bendFold(0.4f);
|
||||||
|
const WaveFn bendHard = bendFold(1.0f);
|
||||||
|
const WaveFn clipped = [](float ph){ return std::tanh(2.5f * std::sin(ph)); };
|
||||||
|
const WaveFn evenStack = additive({ {2, 1.0f}, {6, 0.6f}, {10, 0.4f} });
|
||||||
|
const WaveFn oddStack = additive({ {1, 1.0f}, {5, 0.6f}, {9, 0.3f} });
|
||||||
|
|
||||||
|
auto mix = [](std::initializer_list<std::pair<WaveFn, float>> parts)
|
||||||
|
{
|
||||||
|
std::vector<WaveFn> funcs;
|
||||||
|
std::vector<float> weights;
|
||||||
|
funcs.reserve(parts.size());
|
||||||
|
weights.reserve(parts.size());
|
||||||
|
for (const auto& entry : parts)
|
||||||
|
{
|
||||||
|
funcs.push_back(entry.first);
|
||||||
|
weights.push_back(entry.second);
|
||||||
|
}
|
||||||
|
|
||||||
|
return WaveFn([funcs, weights](float phase) mutable
|
||||||
|
{
|
||||||
|
float v = 0.0f;
|
||||||
|
for (size_t i = 0; i < funcs.size(); ++i)
|
||||||
|
v += weights[i] * funcs[i](phase);
|
||||||
|
return v;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
auto makeAdditive = [](const std::vector<std::pair<int, float>>& partials)
|
||||||
|
{
|
||||||
|
auto coeffs = partials;
|
||||||
|
return WaveFn([coeffs](float phase) mutable
|
||||||
|
{
|
||||||
|
float v = 0.0f;
|
||||||
|
for (auto [harm, gain] : coeffs)
|
||||||
|
v += gain * std::sin((float) harm * phase);
|
||||||
|
return v;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
auto formatIndex = [](int idx)
|
||||||
|
{
|
||||||
|
return juce::String(idx + 1).paddedLeft('0', 2);
|
||||||
|
};
|
||||||
|
|
||||||
|
auto sanitise = [](const juce::String& source, const juce::String& fallback)
|
||||||
|
{
|
||||||
|
juce::String cleaned;
|
||||||
|
for (int i = 0; i < source.length(); ++i)
|
||||||
|
{
|
||||||
|
auto ch = source[i];
|
||||||
|
if (ch >= 32 && ch <= 126)
|
||||||
|
cleaned += ch;
|
||||||
|
}
|
||||||
|
cleaned = cleaned.trim();
|
||||||
|
return cleaned.isEmpty() ? fallback : cleaned;
|
||||||
|
};
|
||||||
|
|
||||||
|
auto addPreset = [&](const juce::String& category,
|
||||||
|
const juce::String& name,
|
||||||
|
const std::vector<WaveFn>& keys)
|
||||||
|
{
|
||||||
|
auto bank = std::make_shared<Bank>(tableSize, frames, levels);
|
||||||
|
bank->setRawFrames(generateFrames(tableSize, keys, frames));
|
||||||
|
bank->buildMipmaps();
|
||||||
|
const juce::String safeCategory = sanitise(category, juce::String("Misc"));
|
||||||
|
const juce::String fallbackName = juce::String("Preset ") + juce::String(presets.size() + 1);
|
||||||
|
const juce::String safeName = sanitise(name, fallbackName);
|
||||||
|
presets.push_back({ safeCategory, safeName, bank });
|
||||||
|
};
|
||||||
|
|
||||||
|
// Electric Piano
|
||||||
|
for (int i = 0; i < 20; ++i)
|
||||||
|
{
|
||||||
|
const float t = (float) i / 19.0f;
|
||||||
|
const float brightness = juce::jmap(t, 0.35f, 0.85f);
|
||||||
|
const float bellMix = juce::jmap(t, 0.15f, 0.45f);
|
||||||
|
|
||||||
|
std::vector<std::pair<int, float>> attackCoeffs {
|
||||||
|
{ 1, 1.0f },
|
||||||
|
{ 2, 0.45f * brightness },
|
||||||
|
{ 3, 0.32f * brightness },
|
||||||
|
{ 4, 0.18f * brightness },
|
||||||
|
{ 5, 0.12f * bellMix },
|
||||||
|
{ 6, 0.08f * bellMix },
|
||||||
|
{ 8, 0.05f * bellMix }
|
||||||
|
};
|
||||||
|
|
||||||
|
std::vector<std::pair<int, float>> bodyCoeffs {
|
||||||
|
{ 1, 1.0f },
|
||||||
|
{ 2, 0.4f * brightness },
|
||||||
|
{ 3, 0.25f * brightness },
|
||||||
|
{ 4, 0.16f * bellMix },
|
||||||
|
{ 6, 0.10f * bellMix },
|
||||||
|
{ 9, 0.06f * bellMix }
|
||||||
|
};
|
||||||
|
|
||||||
|
std::vector<std::pair<int, float>> releaseCoeffs {
|
||||||
|
{ 1, 1.0f },
|
||||||
|
{ 2, 0.30f },
|
||||||
|
{ 3, 0.22f },
|
||||||
|
{ 5, 0.12f * bellMix },
|
||||||
|
{ 7, 0.10f * bellMix }
|
||||||
|
};
|
||||||
|
|
||||||
|
auto attack = makeAdditive(attackCoeffs);
|
||||||
|
auto body = makeAdditive(bodyCoeffs);
|
||||||
|
auto release= makeAdditive(releaseCoeffs);
|
||||||
|
auto shimmer = mix({
|
||||||
|
{ airy, 0.35f + 0.20f * t },
|
||||||
|
{ bell, 0.30f + 0.20f * t },
|
||||||
|
{ oddStack, 0.25f }
|
||||||
|
});
|
||||||
|
|
||||||
|
const juce::String name = "EP Tines " + formatIndex(i);
|
||||||
|
addPreset("Electric Piano", name, { attack, body, release, shimmer });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Organ
|
||||||
|
for (int i = 0; i < 20; ++i)
|
||||||
|
{
|
||||||
|
const float t = (float) i / 19.0f;
|
||||||
|
const float even = juce::jmap(t, 0.30f, 0.65f);
|
||||||
|
const float odd = juce::jmap(t, 0.35f, 0.75f);
|
||||||
|
const float perc = juce::jmap(t, 0.10f, 0.35f);
|
||||||
|
|
||||||
|
std::vector<std::pair<int, float>> drawbarCoeffs {
|
||||||
|
{ 1, 1.0f },
|
||||||
|
{ 2, 0.45f * even },
|
||||||
|
{ 3, 0.38f * odd },
|
||||||
|
{ 4, 0.28f * even },
|
||||||
|
{ 5, 0.24f * odd },
|
||||||
|
{ 6, 0.18f * even },
|
||||||
|
{ 8, 0.12f * odd }
|
||||||
|
};
|
||||||
|
|
||||||
|
auto drawbar = makeAdditive(drawbarCoeffs);
|
||||||
|
auto chorusMix = mix({
|
||||||
|
{ organ, 0.65f },
|
||||||
|
{ choir, 0.35f + 0.15f * t },
|
||||||
|
{ airy, 0.25f }
|
||||||
|
});
|
||||||
|
auto bright = mix({
|
||||||
|
{ organ, 0.60f },
|
||||||
|
{ sawUp, 0.35f + 0.20f * t },
|
||||||
|
{ oddStack, 0.25f + 0.10f * t }
|
||||||
|
});
|
||||||
|
auto percussion = mix({
|
||||||
|
{ bell, 0.30f + 0.25f * perc },
|
||||||
|
{ sine, 0.40f },
|
||||||
|
{ organ, 0.35f }
|
||||||
|
});
|
||||||
|
|
||||||
|
const juce::String name = "Organ Drawbar " + formatIndex(i);
|
||||||
|
addPreset("Organ", name, { drawbar, chorusMix, bright, percussion });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bass
|
||||||
|
for (int i = 0; i < 20; ++i)
|
||||||
|
{
|
||||||
|
const float t = (float) i / 19.0f;
|
||||||
|
const float grit = juce::jmap(t, 0.20f, 0.70f);
|
||||||
|
const float hollowAmt = juce::jmap(t, 0.15f, 0.50f);
|
||||||
|
|
||||||
|
std::vector<std::pair<int, float>> subCoeffs {
|
||||||
|
{ 1, 1.0f },
|
||||||
|
{ 2, 0.35f },
|
||||||
|
{ 3, 0.22f * grit },
|
||||||
|
{ 4, 0.15f * hollowAmt }
|
||||||
|
};
|
||||||
|
|
||||||
|
auto sub = makeAdditive(subCoeffs);
|
||||||
|
auto body = mix({
|
||||||
|
{ sawDown, 0.70f },
|
||||||
|
{ triangle, 0.45f },
|
||||||
|
{ bendSoft, 0.35f * grit }
|
||||||
|
});
|
||||||
|
auto growl = mix({
|
||||||
|
{ bendHard, 0.35f * grit },
|
||||||
|
{ clipped, 0.30f + 0.20f * t },
|
||||||
|
{ pulse30, 0.24f }
|
||||||
|
});
|
||||||
|
auto snap = mix({
|
||||||
|
{ pulse10, 0.28f },
|
||||||
|
{ oddStack, 0.26f },
|
||||||
|
{ bendSoft, 0.30f }
|
||||||
|
});
|
||||||
|
|
||||||
|
const juce::String name = "Bass Sculpt " + formatIndex(i);
|
||||||
|
addPreset("Bass", name, { sub, body, growl, snap });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Drums (GM mapped)
|
||||||
|
static const std::array<std::pair<int, const char*>, 20> gmDrumOrder = {{
|
||||||
|
{35, "Acoustic Bass Drum"}, {36, "Bass Drum 1"},
|
||||||
|
{37, "Side Stick"}, {38, "Acoustic Snare"},
|
||||||
|
{39, "Hand Clap"}, {40, "Electric Snare"},
|
||||||
|
{41, "Low Floor Tom"}, {42, "Closed Hi-Hat"},
|
||||||
|
{43, "High Floor Tom"}, {44, "Pedal Hi-Hat"},
|
||||||
|
{45, "Low Tom"}, {46, "Open Hi-Hat"},
|
||||||
|
{47, "Low-Mid Tom"}, {48, "Hi-Mid Tom"},
|
||||||
|
{49, "Crash Cymbal 1"}, {50, "High Tom"},
|
||||||
|
{51, "Ride Cymbal 1"}, {52, "Chinese Cymbal"},
|
||||||
|
{53, "Ride Bell"}, {54, "Tambourine"}
|
||||||
|
}};
|
||||||
|
|
||||||
|
for (int i = 0; i < 20; ++i)
|
||||||
|
{
|
||||||
|
const auto& gm = gmDrumOrder[(size_t) i];
|
||||||
|
const int gmNumber = gm.first;
|
||||||
|
const juce::String label (gm.second);
|
||||||
|
const float accent = (float) ((i % 4) + 1) / 4.0f;
|
||||||
|
|
||||||
|
std::vector<WaveFn> waves;
|
||||||
|
|
||||||
|
if (gmNumber == 35 || gmNumber == 36)
|
||||||
|
{
|
||||||
|
const float clickAmt = juce::jmap(accent, 0.18f, 0.35f);
|
||||||
|
const float bodyAmt = juce::jmap(accent, 0.75f, 0.95f);
|
||||||
|
|
||||||
|
std::vector<std::pair<int, float>> lowCoeffs {
|
||||||
|
{ 1, bodyAmt },
|
||||||
|
{ 2, 0.32f },
|
||||||
|
{ 3, 0.20f * accent },
|
||||||
|
{ 4, 0.15f * accent }
|
||||||
|
};
|
||||||
|
|
||||||
|
auto low = makeAdditive(lowCoeffs);
|
||||||
|
auto punch = mix({
|
||||||
|
{ sine, 0.70f },
|
||||||
|
{ bendSoft, 0.40f + 0.15f * accent },
|
||||||
|
{ hollow, 0.25f * accent }
|
||||||
|
});
|
||||||
|
auto click = mix({
|
||||||
|
{ pulse10, clickAmt },
|
||||||
|
{ oddStack, 0.25f },
|
||||||
|
{ bell, 0.20f + 0.10f * accent }
|
||||||
|
});
|
||||||
|
auto tail = mix({
|
||||||
|
{ sine, 0.70f },
|
||||||
|
{ triangle, 0.30f },
|
||||||
|
{ airy, 0.22f }
|
||||||
|
});
|
||||||
|
waves = { low, punch, click, tail };
|
||||||
|
}
|
||||||
|
else if (gmNumber == 37 || gmNumber == 38 || gmNumber == 39 || gmNumber == 40)
|
||||||
|
{
|
||||||
|
const float snap = juce::jmap(accent, 0.30f, 0.65f);
|
||||||
|
auto strike = mix({
|
||||||
|
{ pulse30, 0.40f + 0.20f * snap },
|
||||||
|
{ oddStack, 0.30f },
|
||||||
|
{ bendHard, 0.25f + 0.10f * snap }
|
||||||
|
});
|
||||||
|
auto noise = mix({
|
||||||
|
{ sawUp, 0.50f },
|
||||||
|
{ evenStack, 0.40f },
|
||||||
|
{ bell, 0.20f + 0.10f * snap }
|
||||||
|
});
|
||||||
|
std::vector<std::pair<int, float>> bodyCoeffs {
|
||||||
|
{ 1, 1.0f },
|
||||||
|
{ 2, 0.35f },
|
||||||
|
{ 3, 0.24f },
|
||||||
|
{ 5, 0.15f * snap }
|
||||||
|
};
|
||||||
|
auto body = makeAdditive(bodyCoeffs);
|
||||||
|
auto tail = mix({
|
||||||
|
{ airy, 0.50f },
|
||||||
|
{ choir, 0.30f },
|
||||||
|
{ bell, 0.25f }
|
||||||
|
});
|
||||||
|
waves = { strike, noise, body, tail };
|
||||||
|
}
|
||||||
|
else if (gmNumber == 41 || gmNumber == 43 || gmNumber == 45
|
||||||
|
|| gmNumber == 47 || gmNumber == 48 || gmNumber == 50)
|
||||||
|
{
|
||||||
|
const float tone = juce::jmap(accent, 0.40f, 0.80f);
|
||||||
|
std::vector<std::pair<int, float>> bodyCoeffs {
|
||||||
|
{ 1, 1.0f },
|
||||||
|
{ 2, 0.40f * tone },
|
||||||
|
{ 3, 0.28f * tone },
|
||||||
|
{ 4, 0.18f }
|
||||||
|
};
|
||||||
|
auto body = makeAdditive(bodyCoeffs);
|
||||||
|
auto strike = mix({
|
||||||
|
{ pulse30, 0.30f + 0.15f * tone },
|
||||||
|
{ bendSoft, 0.35f },
|
||||||
|
{ oddStack, 0.25f }
|
||||||
|
});
|
||||||
|
auto ring = mix({
|
||||||
|
{ evenStack, 0.40f },
|
||||||
|
{ airy, 0.25f + 0.12f * tone },
|
||||||
|
{ bell, 0.20f }
|
||||||
|
});
|
||||||
|
auto tail = mix({
|
||||||
|
{ sine, 0.60f },
|
||||||
|
{ triangle, 0.30f },
|
||||||
|
{ airy, 0.25f }
|
||||||
|
});
|
||||||
|
waves = { strike, body, ring, tail };
|
||||||
|
}
|
||||||
|
else if (gmNumber == 42 || gmNumber == 44 || gmNumber == 46)
|
||||||
|
{
|
||||||
|
const float metallicAmt = juce::jmap(accent, 0.50f, 0.90f);
|
||||||
|
auto metallic = mix({
|
||||||
|
{ oddStack, 0.60f },
|
||||||
|
{ evenStack, 0.50f },
|
||||||
|
{ bell, 0.35f + 0.15f * accent }
|
||||||
|
});
|
||||||
|
auto closed = mix({
|
||||||
|
{ metallic, 0.80f },
|
||||||
|
{ pulse10, 0.25f },
|
||||||
|
{ sawUp, 0.25f }
|
||||||
|
});
|
||||||
|
auto open = mix({
|
||||||
|
{ evenStack, 0.45f },
|
||||||
|
{ bell, 0.40f + 0.15f * accent },
|
||||||
|
{ airy, 0.35f }
|
||||||
|
});
|
||||||
|
auto shimmer = mix({
|
||||||
|
{ bell, 0.45f },
|
||||||
|
{ oddStack, 0.30f },
|
||||||
|
{ choir, 0.25f }
|
||||||
|
});
|
||||||
|
waves = { closed, metallic, open, shimmer };
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
const float spread = juce::jmap(accent, 0.40f, 0.85f);
|
||||||
|
auto strike = mix({
|
||||||
|
{ sawUp, 0.50f },
|
||||||
|
{ bendHard, 0.40f },
|
||||||
|
{ pulse10, 0.30f }
|
||||||
|
});
|
||||||
|
auto wash = mix({
|
||||||
|
{ evenStack, 0.50f + 0.20f * spread },
|
||||||
|
{ oddStack, 0.45f },
|
||||||
|
{ bell, 0.40f + 0.15f * spread }
|
||||||
|
});
|
||||||
|
auto bellLayer = mix({
|
||||||
|
{ bell, 0.55f + 0.15f * spread },
|
||||||
|
{ choir, 0.30f },
|
||||||
|
{ sine, 0.25f }
|
||||||
|
});
|
||||||
|
auto tail = mix({
|
||||||
|
{ airy, 0.50f },
|
||||||
|
{ bell, 0.35f },
|
||||||
|
{ evenStack, 0.30f }
|
||||||
|
});
|
||||||
|
waves = { strike, wash, bellLayer, tail };
|
||||||
|
}
|
||||||
|
|
||||||
|
const juce::String name = "GM " + juce::String(gmNumber) + " " + label;
|
||||||
|
addPreset("Drums", name, waves);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Strings
|
||||||
|
for (int i = 0; i < 20; ++i)
|
||||||
|
{
|
||||||
|
const float t = (float) i / 19.0f;
|
||||||
|
const float sheen = juce::jmap(t, 0.25f, 0.60f);
|
||||||
|
const float warmth = juce::jmap(t, 0.30f, 0.70f);
|
||||||
|
|
||||||
|
auto ensemble = mix({
|
||||||
|
{ sine, 0.60f },
|
||||||
|
{ triangle, 0.50f },
|
||||||
|
{ choir, 0.35f + 0.15f * warmth },
|
||||||
|
{ airy, 0.25f + 0.10f * sheen }
|
||||||
|
});
|
||||||
|
auto bowMotion = mix({
|
||||||
|
{ sawUp, 0.40f },
|
||||||
|
{ sawDown, 0.35f },
|
||||||
|
{ airy, 0.30f },
|
||||||
|
{ bendSoft, 0.20f }
|
||||||
|
});
|
||||||
|
auto shimmer = mix({
|
||||||
|
{ choir, 0.40f },
|
||||||
|
{ airy, 0.35f + 0.15f * sheen },
|
||||||
|
{ bell, 0.20f }
|
||||||
|
});
|
||||||
|
auto sustain = mix({
|
||||||
|
{ sine, 0.55f },
|
||||||
|
{ triangle, 0.35f },
|
||||||
|
{ organ, 0.25f }
|
||||||
|
});
|
||||||
|
|
||||||
|
const juce::String name = "Strings Ensemble " + formatIndex(i);
|
||||||
|
addPreset("Strings", name, { ensemble, bowMotion, shimmer, sustain });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Brass
|
||||||
|
for (int i = 0; i < 20; ++i)
|
||||||
|
{
|
||||||
|
const float t = (float) i / 19.0f;
|
||||||
|
const float edge = juce::jmap(t, 0.30f, 0.75f);
|
||||||
|
|
||||||
|
auto section = mix({
|
||||||
|
{ sawUp, 0.60f },
|
||||||
|
{ sawDown, 0.35f },
|
||||||
|
{ organ, 0.30f },
|
||||||
|
{ bendSoft, 0.20f * edge }
|
||||||
|
});
|
||||||
|
auto growl = mix({
|
||||||
|
{ bendHard, 0.35f + 0.20f * edge },
|
||||||
|
{ clipped, 0.30f },
|
||||||
|
{ pulse30, 0.20f }
|
||||||
|
});
|
||||||
|
auto brassPad = mix({
|
||||||
|
{ organ, 0.45f },
|
||||||
|
{ choir, 0.30f },
|
||||||
|
{ airy, 0.30f }
|
||||||
|
});
|
||||||
|
auto fanfare = mix({
|
||||||
|
{ evenStack, 0.35f + 0.20f * edge },
|
||||||
|
{ oddStack, 0.30f },
|
||||||
|
{ bell, 0.20f }
|
||||||
|
});
|
||||||
|
|
||||||
|
const juce::String name = "Brass Section " + formatIndex(i);
|
||||||
|
addPreset("Brass", name, { section, growl, brassPad, fanfare });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Choir
|
||||||
|
for (int i = 0; i < 20; ++i)
|
||||||
|
{
|
||||||
|
const float t = (float) i / 19.0f;
|
||||||
|
const float breath = juce::jmap(t, 0.20f, 0.60f);
|
||||||
|
|
||||||
|
auto vowels = mix({
|
||||||
|
{ choir, 0.65f },
|
||||||
|
{ airy, 0.40f },
|
||||||
|
{ sine, 0.20f }
|
||||||
|
});
|
||||||
|
auto ahFormant = mix({
|
||||||
|
{ choir, 0.50f + 0.20f * breath },
|
||||||
|
{ organ, 0.30f },
|
||||||
|
{ airy, 0.25f }
|
||||||
|
});
|
||||||
|
auto shimmer = mix({
|
||||||
|
{ airy, 0.40f + 0.20f * breath },
|
||||||
|
{ bell, 0.25f },
|
||||||
|
{ sine, 0.20f }
|
||||||
|
});
|
||||||
|
auto pad = mix({
|
||||||
|
{ choir, 0.45f },
|
||||||
|
{ sine, 0.30f },
|
||||||
|
{ triangle, 0.25f }
|
||||||
|
});
|
||||||
|
|
||||||
|
const juce::String name = "Choir Aura " + formatIndex(i);
|
||||||
|
addPreset("Choir", name, { vowels, ahFormant, shimmer, pad });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pad
|
||||||
|
for (int i = 0; i < 20; ++i)
|
||||||
|
{
|
||||||
|
const float t = (float) i / 19.0f;
|
||||||
|
const float motion = juce::jmap(t, 0.30f, 0.75f);
|
||||||
|
|
||||||
|
auto warm = mix({
|
||||||
|
{ sine, 0.55f },
|
||||||
|
{ organ, 0.40f },
|
||||||
|
{ airy, 0.30f }
|
||||||
|
});
|
||||||
|
auto evolving = mix({
|
||||||
|
{ choir, 0.35f + 0.20f * motion },
|
||||||
|
{ bendSoft, 0.30f },
|
||||||
|
{ airy, 0.35f + 0.15f * motion }
|
||||||
|
});
|
||||||
|
auto shimmer = mix({
|
||||||
|
{ bell, 0.30f },
|
||||||
|
{ airy, 0.35f + 0.20f * motion },
|
||||||
|
{ evenStack, 0.25f }
|
||||||
|
});
|
||||||
|
auto sub = mix({
|
||||||
|
{ sine, 0.50f },
|
||||||
|
{ triangle, 0.35f },
|
||||||
|
{ hollow, 0.25f }
|
||||||
|
});
|
||||||
|
|
||||||
|
const juce::String name = "Pad Horizon " + formatIndex(i);
|
||||||
|
addPreset("Pad", name, { warm, evolving, shimmer, sub });
|
||||||
|
}
|
||||||
|
|
||||||
|
// SFX
|
||||||
|
for (int i = 0; i < 20; ++i)
|
||||||
|
{
|
||||||
|
const float t = (float) i / 19.0f;
|
||||||
|
const float chaos = juce::jmap(t, 0.40f, 0.90f);
|
||||||
|
|
||||||
|
auto motionFx = mix({
|
||||||
|
{ bendSoft, 0.40f + 0.20f * chaos },
|
||||||
|
{ bendHard, 0.35f + 0.20f * chaos },
|
||||||
|
{ sawUp, 0.30f }
|
||||||
|
});
|
||||||
|
auto shimmerFx = mix({
|
||||||
|
{ bell, 0.30f + 0.25f * chaos },
|
||||||
|
{ airy, 0.30f },
|
||||||
|
{ evenStack, 0.25f }
|
||||||
|
});
|
||||||
|
auto glitch = mix({
|
||||||
|
{ clipped, 0.40f },
|
||||||
|
{ pulse30, 0.30f },
|
||||||
|
{ oddStack, 0.30f }
|
||||||
|
});
|
||||||
|
auto atmosphere = mix({
|
||||||
|
{ airy, 0.45f + 0.20f * chaos },
|
||||||
|
{ choir, 0.30f },
|
||||||
|
{ organ, 0.20f }
|
||||||
|
});
|
||||||
|
|
||||||
|
const juce::String name = "SFX Motion " + formatIndex(i);
|
||||||
|
addPreset("SFX", name, { motionFx, shimmerFx, glitch, atmosphere });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Lead
|
||||||
|
for (int i = 0; i < 20; ++i)
|
||||||
|
{
|
||||||
|
const float t = (float) i / 19.0f;
|
||||||
|
const float bite = juce::jmap(t, 0.30f, 0.85f);
|
||||||
|
|
||||||
|
auto classic = mix({
|
||||||
|
{ sawUp, 0.60f },
|
||||||
|
{ sawDown, 0.35f },
|
||||||
|
{ pulse30, 0.25f }
|
||||||
|
});
|
||||||
|
auto sharp = mix({
|
||||||
|
{ pulse10, 0.35f + 0.20f * bite },
|
||||||
|
{ bendSoft, 0.30f },
|
||||||
|
{ oddStack, 0.25f }
|
||||||
|
});
|
||||||
|
auto silky = mix({
|
||||||
|
{ triangle, 0.40f },
|
||||||
|
{ sine, 0.35f },
|
||||||
|
{ airy, 0.25f }
|
||||||
|
});
|
||||||
|
auto grit = mix({
|
||||||
|
{ bendHard, 0.35f + 0.20f * bite },
|
||||||
|
{ clipped, 0.30f },
|
||||||
|
{ pulse30, 0.20f }
|
||||||
|
});
|
||||||
|
|
||||||
|
const juce::String name = "Lead Vector " + formatIndex(i);
|
||||||
|
addPreset("Lead", name, { classic, sharp, silky, grit });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pluck
|
||||||
|
for (int i = 0; i < 20; ++i)
|
||||||
|
{
|
||||||
|
const float t = (float) i / 19.0f;
|
||||||
|
const float sparkle = juce::jmap(t, 0.25f, 0.70f);
|
||||||
|
|
||||||
|
auto transient = mix({
|
||||||
|
{ pulse10, 0.35f + 0.20f * sparkle },
|
||||||
|
{ oddStack, 0.30f },
|
||||||
|
{ bell, 0.25f }
|
||||||
|
});
|
||||||
|
auto body = mix({
|
||||||
|
{ sawDown, 0.50f },
|
||||||
|
{ triangle, 0.40f },
|
||||||
|
{ sine, 0.30f }
|
||||||
|
});
|
||||||
|
auto shimmer = mix({
|
||||||
|
{ bell, 0.30f + 0.20f * sparkle },
|
||||||
|
{ airy, 0.30f },
|
||||||
|
{ evenStack, 0.25f }
|
||||||
|
});
|
||||||
|
auto decay = mix({
|
||||||
|
{ sine, 0.50f },
|
||||||
|
{ hollow, 0.25f },
|
||||||
|
{ airy, 0.30f }
|
||||||
|
});
|
||||||
|
|
||||||
|
const juce::String name = "Pluck Spark " + formatIndex(i);
|
||||||
|
addPreset("Pluck", name, { transient, body, shimmer, decay });
|
||||||
|
}
|
||||||
|
|
||||||
|
return presets;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// =======================================================================
|
// =======================================================================
|
||||||
// Wavetable Oscillator
|
// Wavetable Oscillator
|
||||||
// =======================================================================
|
// =======================================================================
|
||||||
class Osc
|
class Osc
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
void prepare (double sr) { sampleRate = sr; }
|
void prepare (double sr)
|
||||||
void setBank (std::shared_ptr<Bank> b) { bank = std::move(b); }
|
{
|
||||||
void setFrequency (float f) { freq = juce::jmax(0.0f, f); phaseInc = freq / (float)sampleRate; }
|
sampleRate = juce::jmax (1.0, sr);
|
||||||
void setMorph (float m) { morph = m; } // 0..frames-1 (continuous)
|
setFrequency (freq);
|
||||||
|
}
|
||||||
|
void setBank (std::shared_ptr<Bank> b)
|
||||||
|
{
|
||||||
|
bank = std::move(b);
|
||||||
|
if (bank)
|
||||||
|
morph = juce::jlimit (0.0f, (float) (bank->getFrames() - 1), morph);
|
||||||
|
}
|
||||||
|
void setFrequency (float f)
|
||||||
|
{
|
||||||
|
const float nyquist = 0.5f * (float) sampleRate;
|
||||||
|
freq = juce::jlimit (0.0f, juce::jmax (0.0f, nyquist), f);
|
||||||
|
phaseInc = freq / (float) sampleRate;
|
||||||
|
}
|
||||||
|
void setMorph (float m)
|
||||||
|
{
|
||||||
|
morph = clampMorph (m);
|
||||||
|
} // 0..frames-1 (continuous)
|
||||||
void resetPhase (float p = 0.0f) { phase = juce::jlimit(0.0f, 1.0f, p); }
|
void resetPhase (float p = 0.0f) { phase = juce::jlimit(0.0f, 1.0f, p); }
|
||||||
|
[[nodiscard]] int getFrameCount() const noexcept { return bank ? bank->getFrames() : 0; }
|
||||||
|
[[nodiscard]] float getMaxMorph() const noexcept { return bank ? (float)(bank->getFrames() - 1) : 0.0f; }
|
||||||
|
|
||||||
float process()
|
float process(float morphOverride = std::numeric_limits<float>::quiet_NaN())
|
||||||
{
|
{
|
||||||
if (!bank) return 0.0f;
|
if (!bank) return 0.0f;
|
||||||
|
|
||||||
@@ -241,8 +994,10 @@ namespace WT
|
|||||||
const float preferL0 = 1.0f - juce::jlimit(0.0f, 1.0f,
|
const float preferL0 = 1.0f - juce::jlimit(0.0f, 1.0f,
|
||||||
(float)l0 - (float)bank->chooseLevel(freq * 0.99f, sampleRate));
|
(float)l0 - (float)bank->chooseLevel(freq * 0.99f, sampleRate));
|
||||||
|
|
||||||
const float s0 = bank->lookup(morph, l0, phase);
|
const float morphValue = std::isnan(morphOverride) ? morph : clampMorph (morphOverride);
|
||||||
const float s1 = bank->lookup(morph, l1, phase);
|
|
||||||
|
const float s0 = bank->lookup(morphValue, l0, phase);
|
||||||
|
const float s1 = bank->lookup(morphValue, l1, phase);
|
||||||
const float out = juce::jmap(preferL0, s1, s0); // simple crossfade
|
const float out = juce::jmap(preferL0, s1, s0); // simple crossfade
|
||||||
|
|
||||||
phase += phaseInc;
|
phase += phaseInc;
|
||||||
@@ -251,6 +1006,13 @@ namespace WT
|
|||||||
}
|
}
|
||||||
|
|
||||||
private:
|
private:
|
||||||
|
float clampMorph (float m) const noexcept
|
||||||
|
{
|
||||||
|
if (!bank) return juce::jmax (0.0f, m);
|
||||||
|
const float maxMorph = (float) (bank->getFrames() - 1);
|
||||||
|
return juce::jlimit (0.0f, maxMorph, m);
|
||||||
|
}
|
||||||
|
|
||||||
std::shared_ptr<Bank> bank;
|
std::shared_ptr<Bank> bank;
|
||||||
double sampleRate { 44100.0 };
|
double sampleRate { 44100.0 };
|
||||||
float freq { 0.0f };
|
float freq { 0.0f };
|
||||||
|
|||||||
Reference in New Issue
Block a user