Files
NeuralSynth/Source/WavetableOsc.h
2025-10-25 17:57:05 +00:00

1024 lines
40 KiB
C++

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