#pragma once #include #include #include #include #include #include #include #include // ============================== 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 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* asComplex() { return reinterpret_cast*>(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(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>& 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> frames; frames.resize((size_t)numFrames, std::vector(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::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::twoPi) - 1.0); }; auto sq = [](float ph) { return ph < juce::MathConstants::pi ? 1.0f : -1.0f; }; auto tri = [](float ph) { float v = (float)(2.0 * std::abs(2.0 * (ph / juce::MathConstants::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 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& 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> raw; // [level][frame][sample] std::vector>> tables; }; struct Preset { juce::String category; juce::String name; std::shared_ptr bank; }; class FactoryLibrary { public: static const std::vector& get() { static const std::vector presets = buildFactoryLibrary(); return presets; } private: using WaveFn = std::function; static WaveFn additive(const std::initializer_list>& partials) { const auto coeffs = std::vector>(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::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 renderWave(size_t tableSize, const WaveFn& fn) { std::vector table(tableSize, 0.0f); for (size_t n = 0; n < tableSize; ++n) { const float phase = (float)(juce::MathConstants::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> generateFrames(size_t tableSize, const std::vector& keyWaves, int frames) { std::vector> out((size_t)frames, std::vector(tableSize, 0.0f)); if (keyWaves.empty()) return out; std::vector> 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 buildFactoryLibrary() { const size_t tableSize = 2048; const int frames = 16; const int levels = 6; std::vector presets; presets.reserve(240); const WaveFn sine = [](float ph){ return std::sin(ph); }; const WaveFn sawUp = [](float ph){ const float norm = (ph / juce::MathConstants::twoPi) - std::floor(ph / juce::MathConstants::twoPi); return 2.0f * norm - 1.0f; }; const WaveFn sawDown = [](float ph){ const float norm = (ph / juce::MathConstants::twoPi) - std::floor(ph / juce::MathConstants::twoPi); return 1.0f - 2.0f * norm; }; const WaveFn triangle = [](float ph){ float norm = ph / juce::MathConstants::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> parts) { std::vector funcs; std::vector 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>& 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& keys) { auto bank = std::make_shared(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> 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> 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> 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> 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> 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, 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 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> 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> 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> 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 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::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; 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