From 0785f6fedda8cde24cadf159a6d94cad84780437 Mon Sep 17 00:00:00 2001 From: ed Date: Sat, 25 Oct 2025 17:57:05 +0000 Subject: [PATCH] Upload files to "Source" --- Source/SynthVoice.cpp | 157 +++++++-- Source/SynthVoice.h | 56 ++- Source/WavetableOsc.h | 776 +++++++++++++++++++++++++++++++++++++++++- 3 files changed, 952 insertions(+), 37 deletions(-) diff --git a/Source/SynthVoice.cpp b/Source/SynthVoice.cpp index 929f44c..b68e2f2 100644 --- a/Source/SynthVoice.cpp +++ b/Source/SynthVoice.cpp @@ -14,7 +14,24 @@ void NeuralSynthVoice::prepare (const juce::dsp::ProcessSpec& newSpec) // --- Oscillator osc.prepare (spec.sampleRate); - setWaveform (0); // default to sine + osc.setWave (BlepWave::Sine); + + // --- Wavetable oscillator factory banks --- + wtOsc.prepare (spec.sampleRate); + morphLfo.prepare (spec.sampleRate); + currentWtBankIndex = -1; + wtOsc2.prepare (spec.sampleRate); + morphLfo2.prepare (spec.sampleRate); + currentWtBankIndex2 = -1; + + const auto& library = WT::FactoryLibrary::get(); + if (! library.empty()) + { + wtOsc.setBank (library.front().bank); + currentWtBankIndex = 0; + wtOsc2.setBank (library.front().bank); + currentWtBankIndex2 = 0; + } // --- Scratch buffer (IMPORTANT: allocate real memory) tempBuffer.setSize ((int) spec.numChannels, (int) spec.maximumBlockSize, @@ -23,6 +40,10 @@ void NeuralSynthVoice::prepare (const juce::dsp::ProcessSpec& newSpec) // --- Prepare chain elements chain.prepare (spec); + chain.get().setRampDurationSeconds (0.02f); + chain.get().setThreshold (-1.0f); + chain.get().setRelease (0.05f); + chain.get().reset(); // Set maximum delay sizes BEFORE runtime changes { @@ -73,20 +94,109 @@ void NeuralSynthVoice::renderNextBlock (juce::AudioBuffer& outputBuffer, if (! adsr.isActive()) clearCurrentNote(); - // Apply pending waveform change (from GUI / processor thread) - const int wf = pendingWaveform.exchange (-1, std::memory_order_acq_rel); - if (wf != -1) - setWaveform (wf); - - // --- Generate oscillator into temp buffer + // --- Generate oscillator into temp buffer (BLEP or Wavetable) tempBuffer.clear(); const int numCh = juce::jmin ((int) spec.numChannels, tempBuffer.getNumChannels()); + const auto& library = WT::FactoryLibrary::get(); + const int librarySize = (int) library.size(); + + if (librarySize > 0 && shared.wtBank) + { + const int targetBank = juce::jlimit (0, librarySize - 1, + (int) std::lround (shared.wtBank->load())); + if (targetBank != currentWtBankIndex) + { + wtOsc.setBank (library[(size_t) targetBank].bank); + currentWtBankIndex = targetBank; + } + } + + if (librarySize > 0 && shared.wt2Bank) + { + const int targetBank2 = juce::jlimit (0, librarySize - 1, + (int) std::lround (shared.wt2Bank->load())); + if (targetBank2 != currentWtBankIndex2) + { + wtOsc2.setBank (library[(size_t) targetBank2].bank); + currentWtBankIndex2 = targetBank2; + } + } + + const bool useWTLayerA = (shared.wtOn && shared.wtOn->load() > 0.5f) + && wtOsc.getFrameCount() > 0; + const bool useWTLayerB = (shared.wt2On && shared.wt2On->load() > 0.5f) + && wtOsc2.getFrameCount() > 0; + + const float morphMaxA = wtOsc.getMaxMorph(); + const float morphBaseA = shared.wtMorph + ? juce::jlimit (0.0f, morphMaxA, shared.wtMorph->load()) + : 0.0f; + const float lfoDepthA = shared.wtLfoDepth ? shared.wtLfoDepth->load() : 0.0f; + const float lfoRateA = shared.wtLfoRate ? shared.wtLfoRate->load() : 1.0f; + const int lfoShapeA = shared.wtLfoShape ? (int) std::lround (shared.wtLfoShape->load()) : 0; + + morphLfo.setRate (lfoRateA); + morphLfo.setShape (lfoShapeA); + + const float depthFramesA = juce::jlimit (0.0f, morphMaxA, lfoDepthA); + + const float morphMaxB = wtOsc2.getMaxMorph(); + const float morphBaseB = shared.wt2Morph + ? juce::jlimit (0.0f, morphMaxB, shared.wt2Morph->load()) + : 0.0f; + const float lfoDepthB = shared.wt2LfoDepth ? shared.wt2LfoDepth->load() : 0.0f; + const float lfoRateB = shared.wt2LfoRate ? shared.wt2LfoRate->load() : 0.3f; + const int lfoShapeB = shared.wt2LfoShape ? (int) std::lround (shared.wt2LfoShape->load()) : 0; + + morphLfo2.setRate (lfoRateB); + morphLfo2.setShape (lfoShapeB); + + const float depthFramesB = juce::jlimit (0.0f, morphMaxB, lfoDepthB); + + const float levelA = shared.wtLevel ? juce::jlimit (0.0f, 1.0f, shared.wtLevel->load()) : 0.0f; + const float levelB = shared.wt2Level ? juce::jlimit (0.0f, 1.0f, shared.wt2Level->load()) : 0.0f; + const float safeLevelSum = juce::jlimit (0.5f, 2.0f, levelA + levelB + 0.0001f); + const float mixGain = 0.45f / safeLevelSum; + for (int i = 0; i < numSamples; ++i) { - const float s = osc.process(); + float sampleA = useWTLayerA ? 0.0f : osc.process(); + if (useWTLayerA) + { + const float lfoValueA = morphLfo.process(); + const float headroomNegA = juce::jmin (depthFramesA, morphBaseA); + const float headroomPosA = juce::jmin (depthFramesA, morphMaxA - morphBaseA); + const float offsetA = (lfoValueA >= 0.0f ? lfoValueA * headroomPosA + : lfoValueA * headroomNegA); + const float morphValueA = juce::jlimit (0.0f, morphMaxA, morphBaseA + offsetA); + sampleA = wtOsc.process (morphValueA); + } + else + { + morphLfo.process(); // advance for consistency + } + + float sampleB = 0.0f; + if (useWTLayerB) + { + const float lfoValueB = morphLfo2.process(); + const float headroomNegB = juce::jmin (depthFramesB, morphBaseB); + const float headroomPosB = juce::jmin (depthFramesB, morphMaxB - morphBaseB); + const float offsetB = (lfoValueB >= 0.0f ? lfoValueB * headroomPosB + : lfoValueB * headroomNegB); + const float morphValueB = juce::jlimit (0.0f, morphMaxB, morphBaseB + offsetB); + sampleB = wtOsc2.process (morphValueB); + } + else + { + morphLfo2.process(); + } + + const float combined = mixGain * ((sampleA * levelA) + (sampleB * levelB)); + for (int ch = 0; ch < numCh; ++ch) - tempBuffer.getWritePointer (ch)[i] = s; + tempBuffer.getWritePointer (ch)[i] = combined; } auto block = tempBlock.getSubBlock (0, (size_t) numSamples); @@ -321,10 +431,22 @@ void NeuralSynthVoice::renderNextBlock (juce::AudioBuffer& outputBuffer, void NeuralSynthVoice::noteStarted() { const float freqHz = (float) getCurrentlyPlayingNote().getFrequencyInHertz(); + const float initPhase = shared.wtPhase + ? juce::jlimit (0.0f, 1.0f, shared.wtPhase->load()) + : 0.0f; - // Oscillator frequency and phase retrigger + // Oscillator frequency and phase retrigger (BLEP + WT) osc.setFrequency (freqHz); - osc.resetPhase (0.0f); + osc.resetPhase (initPhase); + wtOsc.setFrequency (freqHz); + wtOsc.resetPhase (initPhase); + morphLfo.reset(); + const float initPhaseB = shared.wt2Phase + ? juce::jlimit (0.0f, 1.0f, shared.wt2Phase->load()) + : initPhase; + wtOsc2.setFrequency (freqHz); + wtOsc2.resetPhase (initPhaseB); + morphLfo2.reset(); // Chorus snapshot if (shared.chorusCentre) chain.get().setCentreDelay (shared.chorusCentre->load()); @@ -372,6 +494,7 @@ void NeuralSynthVoice::notePitchbendChanged() { const float freqHz = (float) getCurrentlyPlayingNote().getFrequencyInHertz(); 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; - } -} diff --git a/Source/SynthVoice.h b/Source/SynthVoice.h index 00842bd..a50f043 100644 --- a/Source/SynthVoice.h +++ b/Source/SynthVoice.h @@ -1,8 +1,11 @@ #pragma once #include #include // <-- for std::function used by WaveShaper +#include +#include #include "NeuralSharedParams.h" #include "BlepOsc.h" +#include "WavetableOsc.h" //============================================================================== // A single polyBLEP oscillator voice with per-voice ADSR, filter ADSR, @@ -25,11 +28,45 @@ public: void noteTimbreChanged() override {} void noteKeyStateChanged() override {} - // Called from the processor when the GUI waveform param changes - void changeWaveform (int wf) { setWaveform (wf); } - private: - void setWaveform (int waveformType); + struct MorphLFO + { + void prepare (double sr) { sampleRate = juce::jmax (1.0, sr); updateIncrement(); } + void reset() { phase = 0.0f; } + void setRate (float hz) { rate = juce::jlimit (0.0f, 30.0f, hz); updateIncrement(); } + void setShape (int idx) { shape = juce::jlimit (0, 3, idx); } + float process() + { + float value = 0.0f; + + switch (shape) + { + case 1: value = 1.0f - 4.0f * std::abs(phase - 0.5f); break; // Triangle + case 2: value = 2.0f * phase - 1.0f; break; // Ramp up + case 3: value = 1.0f - 2.0f * phase; break; // Ramp down + default: value = std::sin (juce::MathConstants::twoPi * phase); break; // Sine + } + + phase += phaseInc; + if (phase >= 1.0f) + phase -= 1.0f; + + return value; + } + + private: + void updateIncrement() + { + phaseInc = (float) (rate / (float) sampleRate); + if (phaseInc < 0.0f) phaseInc = 0.0f; + } + + double sampleRate { 44100.0 }; + float rate { 1.0f }; + float phase { 0.0f }; + float phaseInc { 0.0f }; + int shape { 0 }; + }; //=== Processing chain (without oscillator) =============================== using DelayLine = juce::dsp::DelayLine pendingWaveform {-1}; // set by changeWaveform() + // ==== Oscillators ====================================================== + BlepOsc osc; // polyBLEP oscillator + WT::Osc wtOsc; // wavetable oscillator (shared bank) + MorphLFO morphLfo; + int currentWtBankIndex { -1 }; + WT::Osc wtOsc2; // secondary wavetable oscillator + MorphLFO morphLfo2; + int currentWtBankIndex2 { -1 }; // ==== Envelopes & Filter =============================================== juce::ADSR adsr; diff --git a/Source/WavetableOsc.h b/Source/WavetableOsc.h index c36f378..3c1bcf9 100644 --- a/Source/WavetableOsc.h +++ b/Source/WavetableOsc.h @@ -2,6 +2,11 @@ #include #include #include +#include +#include +#include +#include +#include // ============================== Design ======================================= // - Bank with F frames, each frame is a single-cycle table of N samples. @@ -114,12 +119,17 @@ namespace WT time.data[2 * n + 1] = 0.0f; } fft.performRealOnlyForwardTransform(time.data.data()); + const auto spectrum = time.data; // snapshot packed spectrum for reuse // After JUCE real FFT, bins are laid out as: Re[0], Re[N/2], Re[1], Im[1], Re[2], Im[2], ... // We'll reconstruct complex bins for easy masking. // Helper to zero all harmonics above kMax (inclusive index in [0..N/2]) auto maskAndIFFT = [&](int level, int kMax) { + // Restore the original spectrum before masking this mip level + for (size_t idx = 0; idx < spectrum.size(); ++idx) + time.data[idx] = spectrum[idx]; + // Copy time.data into working complex bins auto* bins = freq.asComplex(); // DC & Nyquist are purely real in real-FFT @@ -220,19 +230,762 @@ namespace WT std::vector>> 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 = sr; } - void setBank (std::shared_ptr b) { bank = std::move(b); } - void setFrequency (float f) { freq = juce::jmax(0.0f, f); phaseInc = freq / (float)sampleRate; } - void setMorph (float m) { morph = m; } // 0..frames-1 (continuous) + void prepare (double sr) + { + sampleRate = juce::jmax (1.0, sr); + setFrequency (freq); + } + void setBank (std::shared_ptr b) + { + bank = std::move(b); + if (bank) + morph = juce::jlimit (0.0f, (float) (bank->getFrames() - 1), morph); + } + void setFrequency (float f) + { + const float nyquist = 0.5f * (float) sampleRate; + freq = juce::jlimit (0.0f, juce::jmax (0.0f, nyquist), f); + phaseInc = freq / (float) sampleRate; + } + void setMorph (float m) + { + morph = clampMorph (m); + } // 0..frames-1 (continuous) void resetPhase (float p = 0.0f) { phase = juce::jlimit(0.0f, 1.0f, p); } + [[nodiscard]] int getFrameCount() const noexcept { return bank ? bank->getFrames() : 0; } + [[nodiscard]] float getMaxMorph() const noexcept { return bank ? (float)(bank->getFrames() - 1) : 0.0f; } - float process() + float process(float morphOverride = std::numeric_limits::quiet_NaN()) { if (!bank) return 0.0f; @@ -241,8 +994,10 @@ namespace WT const float preferL0 = 1.0f - juce::jlimit(0.0f, 1.0f, (float)l0 - (float)bank->chooseLevel(freq * 0.99f, sampleRate)); - const float s0 = bank->lookup(morph, l0, phase); - const float s1 = bank->lookup(morph, l1, phase); + const float morphValue = std::isnan(morphOverride) ? morph : clampMorph (morphOverride); + + const float s0 = bank->lookup(morphValue, l0, phase); + const float s1 = bank->lookup(morphValue, l1, phase); const float out = juce::jmap(preferL0, s1, s0); // simple crossfade phase += phaseInc; @@ -251,6 +1006,13 @@ namespace WT } private: + float clampMorph (float m) const noexcept + { + if (!bank) return juce::jmax (0.0f, m); + const float maxMorph = (float) (bank->getFrames() - 1); + return juce::jlimit (0.0f, maxMorph, m); + } + std::shared_ptr bank; double sampleRate { 44100.0 }; float freq { 0.0f };