feat(dsp): add core synthesizer oscillators, wavetable and voice engine
This commit is contained in:
@@ -0,0 +1,320 @@
|
|||||||
|
#include "Engine.h"
|
||||||
|
|
||||||
|
namespace serum
|
||||||
|
{
|
||||||
|
|
||||||
|
namespace
|
||||||
|
{
|
||||||
|
inline float v (juce::AudioProcessorValueTreeState& apvts, const char* id)
|
||||||
|
{
|
||||||
|
if (auto* p = apvts.getRawParameterValue (id))
|
||||||
|
return p->load();
|
||||||
|
return 0.0f;
|
||||||
|
}
|
||||||
|
|
||||||
|
inline int vic (juce::AudioProcessorValueTreeState& apvts, const char* id, int maxValue)
|
||||||
|
{
|
||||||
|
return juce::jlimit (0, maxValue, (int) std::llround (v (apvts, id) * maxValue));
|
||||||
|
}
|
||||||
|
|
||||||
|
inline float limit (float x) noexcept
|
||||||
|
{
|
||||||
|
const float ax = std::fabs (x);
|
||||||
|
if (ax < 0.8f)
|
||||||
|
return x;
|
||||||
|
const float over = ax - 0.8f;
|
||||||
|
const float clipped = 0.8f + std::tanh (over) * 0.2f;
|
||||||
|
return std::copysign (clipped, x);
|
||||||
|
}
|
||||||
|
|
||||||
|
const char* kEnvAttack[kNumEnvelopes] = { ids::env1A, ids::env2A, ids::env3A, ids::env4A };
|
||||||
|
const char* kEnvDecay[kNumEnvelopes] = { ids::env1D, ids::env2D, ids::env3D, ids::env4D };
|
||||||
|
const char* kEnvSustain[kNumEnvelopes]= { ids::env1S, ids::env2S, ids::env3S, ids::env4S };
|
||||||
|
const char* kEnvRelease[kNumEnvelopes]= { ids::env1R, ids::env2R, ids::env3R, ids::env4R };
|
||||||
|
const char* kEnvCurve[kNumEnvelopes] = { ids::env1Curve, ids::env2Curve, ids::env3Curve, ids::env4Curve };
|
||||||
|
|
||||||
|
const char* kLfoRate[kNumLfos] = { ids::lfo1Rate, ids::lfo2Rate, ids::lfo3Rate, ids::lfo4Rate };
|
||||||
|
const char* kLfoSync[kNumLfos] = { ids::lfo1Sync, ids::lfo2Sync, ids::lfo3Sync, ids::lfo4Sync };
|
||||||
|
const char* kLfoBeat[kNumLfos] = { ids::lfo1Beat, ids::lfo2Beat, ids::lfo3Beat, ids::lfo4Beat };
|
||||||
|
const char* kLfoShape[kNumLfos] = { ids::lfo1Shape, ids::lfo2Shape, ids::lfo3Shape, ids::lfo4Shape };
|
||||||
|
const char* kLfoPhase[kNumLfos] = { ids::lfo1Phase, ids::lfo2Phase, ids::lfo3Phase, ids::lfo4Phase };
|
||||||
|
const char* kLfoFade[kNumLfos] = { ids::lfo1Fade, ids::lfo2Fade, ids::lfo3Fade, ids::lfo4Fade };
|
||||||
|
const char* kLfoDelay[kNumLfos] = { ids::lfo1Delay, ids::lfo2Delay, ids::lfo3Delay, ids::lfo4Delay };
|
||||||
|
|
||||||
|
const char* kFxType[kNumFxSlots] = { ids::fx1Type, ids::fx2Type, ids::fx3Type, ids::fx4Type,
|
||||||
|
ids::fx5Type, ids::fx6Type, ids::fx7Type, ids::fx8Type };
|
||||||
|
const char* kFxMix[kNumFxSlots] = { ids::fx1Mix, ids::fx2Mix, ids::fx3Mix, ids::fx4Mix,
|
||||||
|
ids::fx5Mix, ids::fx6Mix, ids::fx7Mix, ids::fx8Mix };
|
||||||
|
const char* kFxP1[kNumFxSlots] = { ids::fx1P1, ids::fx2P1, ids::fx3P1, ids::fx4P1,
|
||||||
|
ids::fx5P1, ids::fx6P1, ids::fx7P1, ids::fx8P1 };
|
||||||
|
const char* kFxP2[kNumFxSlots] = { ids::fx1P2, ids::fx2P2, ids::fx3P2, ids::fx4P2,
|
||||||
|
ids::fx5P2, ids::fx6P2, ids::fx7P2, ids::fx8P2 };
|
||||||
|
const char* kFxP3[kNumFxSlots] = { ids::fx1P3, ids::fx2P3, ids::fx3P3, ids::fx4P3,
|
||||||
|
ids::fx5P3, ids::fx6P3, ids::fx7P3, ids::fx8P3 };
|
||||||
|
const char* kFxP4[kNumFxSlots] = { ids::fx1P4, ids::fx2P4, ids::fx3P4, ids::fx4P4,
|
||||||
|
ids::fx5P4, ids::fx6P4, ids::fx7P4, ids::fx8P4 };
|
||||||
|
}
|
||||||
|
|
||||||
|
void Engine::prepare (double sampleRate, int maxBlockSize)
|
||||||
|
{
|
||||||
|
sr = sampleRate;
|
||||||
|
blockSize = maxBlockSize;
|
||||||
|
|
||||||
|
for (auto& voice : voices)
|
||||||
|
voice.prepare (sampleRate, maxBlockSize);
|
||||||
|
for (auto& lfo : lfos)
|
||||||
|
lfo.prepare (sampleRate);
|
||||||
|
fx.prepare (sampleRate, maxBlockSize);
|
||||||
|
mixBuffer.setSize (2, maxBlockSize, false, false, true);
|
||||||
|
reset();
|
||||||
|
}
|
||||||
|
|
||||||
|
void Engine::reset()
|
||||||
|
{
|
||||||
|
for (auto& voice : voices)
|
||||||
|
voice.reset();
|
||||||
|
for (auto& lfo : lfos)
|
||||||
|
lfo.reset();
|
||||||
|
fx.reset();
|
||||||
|
pitchBend = 0.0f;
|
||||||
|
modWheel = 0.0f;
|
||||||
|
mixBuffer.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
void Engine::setLfoShapeData (int index, const std::vector<float>& data, int steps)
|
||||||
|
{
|
||||||
|
index = juce::jlimit (0, kNumLfos - 1, index);
|
||||||
|
lfos[(size_t) index].setShapeData (data, steps);
|
||||||
|
}
|
||||||
|
|
||||||
|
int Engine::getActiveVoiceCount() const
|
||||||
|
{
|
||||||
|
int count = 0;
|
||||||
|
for (const auto& v : voices)
|
||||||
|
if (v.isActive())
|
||||||
|
++count;
|
||||||
|
return count;
|
||||||
|
}
|
||||||
|
|
||||||
|
SynthVoice* Engine::findFreeVoice()
|
||||||
|
{
|
||||||
|
for (auto& v : voices)
|
||||||
|
if (! v.isActive())
|
||||||
|
return &v;
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
SynthVoice* Engine::stealVoice()
|
||||||
|
{
|
||||||
|
// Prefer stealing an already-released voice, then the oldest active one.
|
||||||
|
SynthVoice* best = nullptr;
|
||||||
|
juce::uint64 bestId = std::numeric_limits<juce::uint64>::max();
|
||||||
|
|
||||||
|
for (auto& v : voices)
|
||||||
|
if (v.isActive() && v.isReleased() && v.getNoteId() < bestId)
|
||||||
|
{
|
||||||
|
best = &v;
|
||||||
|
bestId = v.getNoteId();
|
||||||
|
}
|
||||||
|
if (best != nullptr)
|
||||||
|
return best;
|
||||||
|
|
||||||
|
for (auto& v : voices)
|
||||||
|
if (v.isActive() && v.getNoteId() < bestId)
|
||||||
|
{
|
||||||
|
best = &v;
|
||||||
|
bestId = v.getNoteId();
|
||||||
|
}
|
||||||
|
return best != nullptr ? best : &voices[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
void Engine::noteOn (int noteNumber, float velocity01)
|
||||||
|
{
|
||||||
|
SynthVoice* voice = findFreeVoice();
|
||||||
|
if (voice == nullptr)
|
||||||
|
voice = stealVoice();
|
||||||
|
|
||||||
|
const double freq = juce::MidiMessage::getMidiNoteInHertz (noteNumber);
|
||||||
|
voice->noteOn (noteNumber, juce::jlimit (0.0f, 1.0f, velocity01), freq, (juce::uint32) (++noteCounter));
|
||||||
|
}
|
||||||
|
|
||||||
|
void Engine::noteOff (int noteNumber)
|
||||||
|
{
|
||||||
|
for (auto& v : voices)
|
||||||
|
if (v.isActive() && v.getNote() == noteNumber && ! v.isReleased())
|
||||||
|
v.noteOff();
|
||||||
|
}
|
||||||
|
|
||||||
|
void Engine::allNotesOff()
|
||||||
|
{
|
||||||
|
for (auto& v : voices)
|
||||||
|
if (v.isActive())
|
||||||
|
v.noteOff();
|
||||||
|
}
|
||||||
|
|
||||||
|
void Engine::readOscParams (juce::AudioProcessorValueTreeState& apvts, const char* prefix, OscParams& o)
|
||||||
|
{
|
||||||
|
const juce::String p = prefix;
|
||||||
|
auto g = [&] (const char* suffix) { return v (apvts, (p + suffix).toRawUTF8()); };
|
||||||
|
|
||||||
|
o.enabled = g ("On") > 0.5f;
|
||||||
|
o.wave = juce::jlimit (0, kNumWavetables - 1, (int) std::llround (g ("Wave") * (kNumWavetables - 1)));
|
||||||
|
o.wtPos = g ("WtPos");
|
||||||
|
o.warp = (int) std::llround (g ("Warp") * 7.0f);
|
||||||
|
o.warpAmt = g ("WarpAmt");
|
||||||
|
o.coarse = (int) std::llround (g ("Coarse") * 48.0f) - 24;
|
||||||
|
o.fine = (int) std::llround (g ("Fine") * 200.0f) - 100;
|
||||||
|
o.level = g ("Level");
|
||||||
|
o.pan = g ("Pan") * 2.0f - 1.0f;
|
||||||
|
o.unison = 1 + (int) std::llround (g ("Unison") * 15.0f);
|
||||||
|
o.detune = g ("Detune");
|
||||||
|
o.spread = g ("Spread");
|
||||||
|
o.phase = g ("Phase");
|
||||||
|
o.randPhase = g ("RandPh");
|
||||||
|
}
|
||||||
|
|
||||||
|
void Engine::processBlock (juce::AudioBuffer<float>& buffer, juce::MidiBuffer& midi,
|
||||||
|
juce::AudioProcessorValueTreeState& apvts,
|
||||||
|
juce::AudioPlayHead* playhead)
|
||||||
|
{
|
||||||
|
const int n = buffer.getNumSamples();
|
||||||
|
const int numCh = buffer.getNumChannels();
|
||||||
|
|
||||||
|
// Tempo.
|
||||||
|
if (playhead != nullptr)
|
||||||
|
if (auto pos = playhead->getPosition())
|
||||||
|
if (auto b = pos->getBpm())
|
||||||
|
bpm = *b;
|
||||||
|
|
||||||
|
// MIDI.
|
||||||
|
for (const auto meta : midi)
|
||||||
|
{
|
||||||
|
const auto m = meta.getMessage();
|
||||||
|
if (m.isNoteOn() && m.getVelocity() > 0)
|
||||||
|
noteOn (m.getNoteNumber(), m.getFloatVelocity());
|
||||||
|
else if (m.isNoteOff() || (m.isNoteOn() && m.getVelocity() == 0))
|
||||||
|
noteOff (m.getNoteNumber());
|
||||||
|
else if (m.isPitchWheel())
|
||||||
|
pitchBend = (m.getPitchWheelValue() - 8192) / 8192.0f;
|
||||||
|
else if (m.isController())
|
||||||
|
{
|
||||||
|
if (m.getControllerNumber() == 1)
|
||||||
|
modWheel = m.getControllerValue() / 127.0f;
|
||||||
|
else if (m.getControllerNumber() == 120 || m.getControllerNumber() == 123)
|
||||||
|
allNotesOff();
|
||||||
|
}
|
||||||
|
else if (m.isAllNotesOff() || m.isAllSoundOff())
|
||||||
|
allNotesOff();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prepare the voice mix buffer.
|
||||||
|
mixBuffer.setSize (2, n, false, false, true);
|
||||||
|
mixBuffer.clear();
|
||||||
|
float* mixL = mixBuffer.getWritePointer (0);
|
||||||
|
float* mixR = mixBuffer.getWritePointer (1);
|
||||||
|
|
||||||
|
// Advance LFOs and capture their values (control rate).
|
||||||
|
float lfoValues[kNumLfos];
|
||||||
|
for (int i = 0; i < kNumLfos; ++i)
|
||||||
|
{
|
||||||
|
lfos[(size_t) i].setTempo (bpm);
|
||||||
|
lfos[(size_t) i].setParams (v (apvts, kLfoRate[i]),
|
||||||
|
v (apvts, kLfoSync[i]) > 0.5f,
|
||||||
|
v (apvts, kLfoBeat[i]),
|
||||||
|
vic (apvts, kLfoShape[i], 6),
|
||||||
|
v (apvts, kLfoPhase[i]),
|
||||||
|
v (apvts, kLfoFade[i]),
|
||||||
|
v (apvts, kLfoDelay[i]));
|
||||||
|
for (int s = 0; s < n; ++s)
|
||||||
|
lfos[(size_t) i].process();
|
||||||
|
lfoValues[i] = lfos[(size_t) i].getValue();
|
||||||
|
}
|
||||||
|
|
||||||
|
const float macroValues[kNumMacros] = { v (apvts, ids::macro1), v (apvts, ids::macro2),
|
||||||
|
v (apvts, ids::macro3), v (apvts, ids::macro4) };
|
||||||
|
|
||||||
|
// Build the render context.
|
||||||
|
RenderContext ctx;
|
||||||
|
ctx.sampleRate = sr;
|
||||||
|
ctx.wavetables = &wavetables;
|
||||||
|
for (int i = 0; i < kNumLfos; ++i) ctx.lfoValues[i] = lfoValues[i];
|
||||||
|
for (int i = 0; i < kNumMacros; ++i) ctx.macroValues[i] = macroValues[i];
|
||||||
|
ctx.modWheel = modWheel;
|
||||||
|
ctx.pitchBend = pitchBend;
|
||||||
|
ctx.pitchBendRange = 2.0f;
|
||||||
|
ctx.matrix = &matrix;
|
||||||
|
ctx.macros = ¯os;
|
||||||
|
|
||||||
|
readOscParams (apvts, "oscA", ctx.oscA);
|
||||||
|
readOscParams (apvts, "oscB", ctx.oscB);
|
||||||
|
|
||||||
|
ctx.subOn = v (apvts, ids::subOn) > 0.5f;
|
||||||
|
ctx.subShape = vic (apvts, ids::subShape, 1);
|
||||||
|
ctx.subOct = vic (apvts, ids::subOct, 2) - 2;
|
||||||
|
ctx.subLevel = v (apvts, ids::subLevel);
|
||||||
|
|
||||||
|
ctx.noiseOn = v (apvts, ids::noiseOn) > 0.5f;
|
||||||
|
ctx.noiseType = vic (apvts, ids::noiseType, 1);
|
||||||
|
ctx.noiseLevel = v (apvts, ids::noiseLevel);
|
||||||
|
|
||||||
|
ctx.filters.f1On = v (apvts, ids::f1On) > 0.5f;
|
||||||
|
ctx.filters.f1Type = vic (apvts, ids::f1Type, 6);
|
||||||
|
ctx.filters.f1Cutoff = v (apvts, ids::f1Cutoff);
|
||||||
|
ctx.filters.f1Res = v (apvts, ids::f1Res);
|
||||||
|
ctx.filters.f1Drive = v (apvts, ids::f1Drive);
|
||||||
|
ctx.filters.f1Key = v (apvts, ids::f1Key);
|
||||||
|
ctx.filters.f1Slope = vic (apvts, ids::f1Slope, 2);
|
||||||
|
|
||||||
|
ctx.filters.f2On = v (apvts, ids::f2On) > 0.5f;
|
||||||
|
ctx.filters.f2Type = vic (apvts, ids::f2Type, 6);
|
||||||
|
ctx.filters.f2Cutoff = v (apvts, ids::f2Cutoff);
|
||||||
|
ctx.filters.f2Res = v (apvts, ids::f2Res);
|
||||||
|
ctx.filters.f2Drive = v (apvts, ids::f2Drive);
|
||||||
|
ctx.filters.f2Key = v (apvts, ids::f2Key);
|
||||||
|
ctx.filters.f2Slope = vic (apvts, ids::f2Slope, 2);
|
||||||
|
|
||||||
|
ctx.filters.route = vic (apvts, ids::fRoute, 2);
|
||||||
|
ctx.filters.mix = v (apvts, ids::fMix);
|
||||||
|
ctx.filters.out = v (apvts, ids::fOut) * 1.5f;
|
||||||
|
|
||||||
|
for (int i = 0; i < kNumEnvelopes; ++i)
|
||||||
|
{
|
||||||
|
ctx.envAttack[i] = v (apvts, kEnvAttack[i]);
|
||||||
|
ctx.envDecay[i] = v (apvts, kEnvDecay[i]);
|
||||||
|
ctx.envSustain[i] = v (apvts, kEnvSustain[i]);
|
||||||
|
ctx.envRelease[i] = v (apvts, kEnvRelease[i]);
|
||||||
|
ctx.envCurve[i] = v (apvts, kEnvCurve[i]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Render all active voices into the mix buffer.
|
||||||
|
for (auto& voice : voices)
|
||||||
|
if (voice.isActive())
|
||||||
|
voice.render (mixL, mixR, n, ctx);
|
||||||
|
|
||||||
|
// FX rack.
|
||||||
|
FxSlotParams slots[kNumFxSlots];
|
||||||
|
for (int i = 0; i < kNumFxSlots; ++i)
|
||||||
|
{
|
||||||
|
slots[i].type = juce::jlimit (0, (int) FxType::Count - 1, (int) std::llround (v (apvts, kFxType[i]) * ((int) FxType::Count - 1)));
|
||||||
|
slots[i].mix = v (apvts, kFxMix[i]);
|
||||||
|
slots[i].p[0] = v (apvts, kFxP1[i]);
|
||||||
|
slots[i].p[1] = v (apvts, kFxP2[i]);
|
||||||
|
slots[i].p[2] = v (apvts, kFxP3[i]);
|
||||||
|
slots[i].p[3] = v (apvts, kFxP4[i]);
|
||||||
|
}
|
||||||
|
|
||||||
|
fx.process (mixBuffer, slots, kNumFxSlots);
|
||||||
|
|
||||||
|
// Master + soft limiting.
|
||||||
|
const float master = v (apvts, ids::master);
|
||||||
|
|
||||||
|
for (int ch = 0; ch < numCh; ++ch)
|
||||||
|
{
|
||||||
|
float* dest = buffer.getWritePointer (ch);
|
||||||
|
const float* src = mixBuffer.getReadPointer (ch < 2 ? ch : 0);
|
||||||
|
for (int i = 0; i < n; ++i)
|
||||||
|
dest[i] = limit (src[i] * master);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace serum
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <JuceHeader.h>
|
||||||
|
#include "Params.h"
|
||||||
|
#include "Wavetable.h"
|
||||||
|
#include "SynthVoice.h"
|
||||||
|
#include "LFO.h"
|
||||||
|
#include "FXProcessor.h"
|
||||||
|
#include "ModulationMatrix.h"
|
||||||
|
#include "MacroControls.h"
|
||||||
|
|
||||||
|
namespace serum
|
||||||
|
{
|
||||||
|
|
||||||
|
// ===========================================================================
|
||||||
|
// The synthesis engine: a fixed pool of voices, four global LFOs, a wavetable
|
||||||
|
// library and the FX rack. Owns all per-block DSP and MIDI handling.
|
||||||
|
// ===========================================================================
|
||||||
|
class Engine
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
void prepare (double sampleRate, int blockSize);
|
||||||
|
void reset();
|
||||||
|
|
||||||
|
void processBlock (juce::AudioBuffer<float>& buffer,
|
||||||
|
juce::MidiBuffer& midi,
|
||||||
|
juce::AudioProcessorValueTreeState& apvts,
|
||||||
|
juce::AudioPlayHead* playhead);
|
||||||
|
|
||||||
|
// MIDI
|
||||||
|
void noteOn (int noteNumber, float velocity01);
|
||||||
|
void noteOff (int noteNumber);
|
||||||
|
void allNotesOff();
|
||||||
|
|
||||||
|
// Modulation / DSP accessors (read-only for the GUI).
|
||||||
|
ModulationMatrix& getMatrix() { return matrix; }
|
||||||
|
MacroControls& getMacros() { return macros; }
|
||||||
|
WavetableLibrary& getWavetables() { return wavetables; }
|
||||||
|
const std::array<LFO, kNumLfos>& getLfos() const { return lfos; }
|
||||||
|
void setLfoShapeData (int index, const std::vector<float>& data, int steps);
|
||||||
|
|
||||||
|
int getActiveVoiceCount() const;
|
||||||
|
|
||||||
|
private:
|
||||||
|
std::array<SynthVoice, kNumVoices> voices;
|
||||||
|
std::array<LFO, kNumLfos> lfos;
|
||||||
|
WavetableLibrary wavetables;
|
||||||
|
FXProcessor fx;
|
||||||
|
ModulationMatrix matrix;
|
||||||
|
MacroControls macros;
|
||||||
|
|
||||||
|
double sr = 44100.0;
|
||||||
|
int blockSize = 512;
|
||||||
|
double bpm = 120.0;
|
||||||
|
float pitchBend = 0.0f;
|
||||||
|
float modWheel = 0.0f;
|
||||||
|
juce::uint64 noteCounter = 0;
|
||||||
|
|
||||||
|
juce::AudioBuffer<float> mixBuffer;
|
||||||
|
|
||||||
|
SynthVoice* findFreeVoice();
|
||||||
|
SynthVoice* stealVoice();
|
||||||
|
|
||||||
|
void readOscParams (juce::AudioProcessorValueTreeState& apvts, const char* prefix, OscParams& out);
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace serum
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
#include "NoiseOscillator.h"
|
||||||
|
|
||||||
|
namespace serum
|
||||||
|
{
|
||||||
|
|
||||||
|
void NoiseOscillator::reset()
|
||||||
|
{
|
||||||
|
pinkB0 = pinkB1 = pinkB2 = 0.0f;
|
||||||
|
}
|
||||||
|
|
||||||
|
void NoiseOscillator::noteOn (juce::uint32 seed)
|
||||||
|
{
|
||||||
|
rng.setSeed (seed);
|
||||||
|
}
|
||||||
|
|
||||||
|
void NoiseOscillator::processAdd (int type, float level, float& out) noexcept
|
||||||
|
{
|
||||||
|
if (level <= 0.0f)
|
||||||
|
return;
|
||||||
|
|
||||||
|
const float white = rng.nextFloat() * 2.0f - 1.0f;
|
||||||
|
|
||||||
|
float s = white;
|
||||||
|
if (type == (int) NoiseType::Pink)
|
||||||
|
{
|
||||||
|
// Paul Kellet's economy pink-noise filter.
|
||||||
|
pinkB0 = 0.99765f * pinkB0 + white * 0.0990460f;
|
||||||
|
pinkB1 = 0.96300f * pinkB1 + white * 0.2965164f;
|
||||||
|
pinkB2 = 0.57000f * pinkB2 + white * 1.0526913f;
|
||||||
|
s = pinkB0 + pinkB1 + pinkB2 + white * 0.1848f;
|
||||||
|
}
|
||||||
|
|
||||||
|
out += s * level;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace serum
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <JuceHeader.h>
|
||||||
|
#include "Params.h"
|
||||||
|
|
||||||
|
namespace serum
|
||||||
|
{
|
||||||
|
|
||||||
|
// ===========================================================================
|
||||||
|
// Per-voice white/pink noise generator.
|
||||||
|
// ===========================================================================
|
||||||
|
class NoiseOscillator
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
void prepare (double sampleRate) { sr = sampleRate; reset(); }
|
||||||
|
void reset();
|
||||||
|
|
||||||
|
void noteOn (juce::uint32 seed);
|
||||||
|
|
||||||
|
// Accumulate into out.
|
||||||
|
void processAdd (int type, float level, float& out) noexcept;
|
||||||
|
|
||||||
|
private:
|
||||||
|
double sr = 44100.0;
|
||||||
|
juce::Random rng;
|
||||||
|
float pinkB0 = 0.0f, pinkB1 = 0.0f, pinkB2 = 0.0f;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace serum
|
||||||
@@ -0,0 +1,133 @@
|
|||||||
|
#include "Oscillator.h"
|
||||||
|
|
||||||
|
namespace serum
|
||||||
|
{
|
||||||
|
|
||||||
|
void Oscillator::reset()
|
||||||
|
{
|
||||||
|
for (auto& v : voices)
|
||||||
|
{
|
||||||
|
v.phase = 0.0;
|
||||||
|
v.detuneRatio = 1.0;
|
||||||
|
v.pan = 0.0f;
|
||||||
|
v.level = 1.0f;
|
||||||
|
}
|
||||||
|
activeUnison = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Oscillator::noteOn (double freqHz, const OscParams& p, juce::uint32 seed)
|
||||||
|
{
|
||||||
|
jassert (freqHz > 0.0);
|
||||||
|
rng.setSeed (seed);
|
||||||
|
|
||||||
|
const int uni = juce::jlimit (1, kMaxUnison, p.unison);
|
||||||
|
activeUnison = uni;
|
||||||
|
|
||||||
|
const double basePhase = (double) p.phase * kTwoPi;
|
||||||
|
|
||||||
|
for (int v = 0; v < uni; ++v)
|
||||||
|
{
|
||||||
|
// Even phase spacing prevents cancellation across unison voices.
|
||||||
|
double offset = (uni > 1) ? ((double) v / (double) uni) * kTwoPi : 0.0;
|
||||||
|
double random = rng.nextFloat() * (double) p.randPhase * kTwoPi;
|
||||||
|
voices[(size_t) v].phase = basePhase + offset + random;
|
||||||
|
|
||||||
|
// Detune: linear spread in cents, 0..50 cents at full depth.
|
||||||
|
double detuneCents = 0.0;
|
||||||
|
if (uni > 1)
|
||||||
|
detuneCents = (double) p.detune * 50.0 * ((double) (v - (uni - 1) / 2.0) / (double) ((uni - 1) / 2.0));
|
||||||
|
voices[(size_t) v].detuneRatio = std::pow (2.0, detuneCents / 1200.0);
|
||||||
|
|
||||||
|
// Stereo spread.
|
||||||
|
float panPos = (uni > 1) ? ((float) v / (float) (uni - 1) - 0.5f) * 2.0f * p.spread : 0.0f;
|
||||||
|
voices[(size_t) v].pan = panPos;
|
||||||
|
|
||||||
|
// Gain scaling with a centre emphasis for odd unison counts.
|
||||||
|
float lvl = 1.0f / std::sqrt ((float) uni);
|
||||||
|
if ((uni & 1) && v == uni / 2)
|
||||||
|
lvl *= 1.3f;
|
||||||
|
voices[(size_t) v].level = lvl;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
float Oscillator::warpPhase (float phase, const OscParams& p) const noexcept
|
||||||
|
{
|
||||||
|
const float amt = p.warpAmt;
|
||||||
|
switch ((WarpMode) p.warp)
|
||||||
|
{
|
||||||
|
case WarpMode::None: return phase;
|
||||||
|
case WarpMode::BendPlus: return phase + amt * 0.5f * std::sin (phase * (float) kTwoPi);
|
||||||
|
case WarpMode::BendMinus: return phase - amt * 0.5f * std::sin (phase * (float) kTwoPi);
|
||||||
|
case WarpMode::Sync: return std::fmod (phase * (1.0f + amt * 7.0f), 1.0f);
|
||||||
|
case WarpMode::Asym:
|
||||||
|
if (amt < 0.001f) return phase;
|
||||||
|
return std::pow (phase, 1.0f + amt * 3.0f);
|
||||||
|
case WarpMode::Mirror:
|
||||||
|
{
|
||||||
|
const float mirror = 2.0f * std::abs (phase - 0.5f);
|
||||||
|
return phase + (mirror - phase) * amt;
|
||||||
|
}
|
||||||
|
case WarpMode::PWM: return phase;
|
||||||
|
case WarpMode::Fold: return phase;
|
||||||
|
default: return phase;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
float Oscillator::warpSample (float sample, const OscParams& p) const noexcept
|
||||||
|
{
|
||||||
|
const float amt = p.warpAmt;
|
||||||
|
switch ((WarpMode) p.warp)
|
||||||
|
{
|
||||||
|
case WarpMode::PWM:
|
||||||
|
{
|
||||||
|
const float threshold = (2.0f * amt - 1.0f) * 0.9f;
|
||||||
|
return std::tanh ((sample - threshold) * 4.0f);
|
||||||
|
}
|
||||||
|
case WarpMode::Fold:
|
||||||
|
return std::sin (sample * (1.0f + amt * 5.0f) * 1.5707963267948966f);
|
||||||
|
default:
|
||||||
|
return sample;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void Oscillator::processAdd (const Wavetable& wt, const OscParams& p, double freqHz,
|
||||||
|
float& outL, float& outR) noexcept
|
||||||
|
{
|
||||||
|
if (! p.enabled || p.level <= 0.0f || freqHz <= 0.0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
const int uni = juce::jlimit (1, kMaxUnison, p.unison);
|
||||||
|
const float framePos = p.wtPos * 255.0f;
|
||||||
|
const double phaseInc = kTwoPi * freqHz / sr;
|
||||||
|
|
||||||
|
float accL = 0.0f;
|
||||||
|
float accR = 0.0f;
|
||||||
|
|
||||||
|
for (int v = 0; v < uni; ++v)
|
||||||
|
{
|
||||||
|
auto& sv = voices[(size_t) v];
|
||||||
|
|
||||||
|
sv.phase += phaseInc * sv.detuneRatio;
|
||||||
|
sv.phase -= std::floor (sv.phase * (1.0 / kTwoPi)) * kTwoPi;
|
||||||
|
if (sv.phase >= kTwoPi) sv.phase -= kTwoPi;
|
||||||
|
if (sv.phase < 0.0) sv.phase += kTwoPi;
|
||||||
|
|
||||||
|
float phase01 = (float) (sv.phase * (1.0 / kTwoPi));
|
||||||
|
float sample = wt.readSafe (framePos, warpPhase (phase01, p));
|
||||||
|
sample = warpSample (sample, p);
|
||||||
|
|
||||||
|
// Constant-power pan.
|
||||||
|
const float panAngle = (sv.pan + 1.0f) * 0.5f * 1.5707963267948966f;
|
||||||
|
const float panL = std::cos (panAngle);
|
||||||
|
const float panR = std::sin (panAngle);
|
||||||
|
|
||||||
|
const float gain = sv.level * p.level;
|
||||||
|
accL += sample * gain * panL;
|
||||||
|
accR += sample * gain * panR;
|
||||||
|
}
|
||||||
|
|
||||||
|
outL += accL;
|
||||||
|
outR += accR;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace serum
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <JuceHeader.h>
|
||||||
|
#include "Params.h"
|
||||||
|
#include "Wavetable.h"
|
||||||
|
|
||||||
|
namespace serum
|
||||||
|
{
|
||||||
|
|
||||||
|
// ===========================================================================
|
||||||
|
// Per-oscillator parameters (snapshot read from the APVTS each block).
|
||||||
|
// ===========================================================================
|
||||||
|
struct OscParams
|
||||||
|
{
|
||||||
|
bool enabled = true;
|
||||||
|
int wave = 0;
|
||||||
|
float wtPos = 0.0f;
|
||||||
|
int warp = 0;
|
||||||
|
float warpAmt = 0.0f;
|
||||||
|
int coarse = 0;
|
||||||
|
int fine = 0;
|
||||||
|
float level = 1.0f;
|
||||||
|
float pan = 0.0f;
|
||||||
|
int unison = 1;
|
||||||
|
float detune = 0.0f;
|
||||||
|
float spread = 0.0f;
|
||||||
|
float phase = 0.0f;
|
||||||
|
float randPhase = 0.0f;
|
||||||
|
};
|
||||||
|
|
||||||
|
// ===========================================================================
|
||||||
|
// A polyphonic oscillator with wavetable morphing, warp modes and unison
|
||||||
|
// (1..16 sub-voices). Per-voice state (phase) lives here; params are read each
|
||||||
|
// block from the OscParams snapshot.
|
||||||
|
// ===========================================================================
|
||||||
|
class Oscillator
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
Oscillator() = default;
|
||||||
|
|
||||||
|
void prepare (double sampleRate) { sr = sampleRate; reset(); }
|
||||||
|
void reset();
|
||||||
|
|
||||||
|
// (Re)configure unison sub-voices: phase offsets, detune, pan and gain.
|
||||||
|
void noteOn (double freqHz, const OscParams& p, juce::uint32 seed);
|
||||||
|
|
||||||
|
// Accumulate this oscillator's contribution into outL/outR.
|
||||||
|
void processAdd (const Wavetable& wt, const OscParams& p, double freqHz,
|
||||||
|
float& outL, float& outR) noexcept;
|
||||||
|
|
||||||
|
int getActiveUnison() const noexcept { return activeUnison; }
|
||||||
|
|
||||||
|
private:
|
||||||
|
struct SubVoice
|
||||||
|
{
|
||||||
|
double phase = 0.0;
|
||||||
|
double detuneRatio = 1.0;
|
||||||
|
float pan = 0.0f;
|
||||||
|
float level = 1.0f;
|
||||||
|
};
|
||||||
|
|
||||||
|
std::array<SubVoice, kMaxUnison> voices;
|
||||||
|
int activeUnison = 1;
|
||||||
|
double sr = 44100.0;
|
||||||
|
juce::Random rng;
|
||||||
|
|
||||||
|
static constexpr double kTwoPi = 6.28318530717958647692;
|
||||||
|
|
||||||
|
float warpPhase (float phase, const OscParams& p) const noexcept;
|
||||||
|
float warpSample (float sample, const OscParams& p) const noexcept;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace serum
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
#include "SubOscillator.h"
|
||||||
|
|
||||||
|
namespace serum
|
||||||
|
{
|
||||||
|
|
||||||
|
void SubOscillator::noteOn (double freqHz, int octaveShift)
|
||||||
|
{
|
||||||
|
// octaveShift: -2, -1 or 0 (from the subOct param).
|
||||||
|
const double mult = (octaveShift == -2) ? 0.25 : (octaveShift == -1) ? 0.5 : 1.0;
|
||||||
|
(void) freqHz;
|
||||||
|
(void) mult;
|
||||||
|
phase = 0.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
void SubOscillator::processAdd (double freqHz, int shape, float level, float& out) noexcept
|
||||||
|
{
|
||||||
|
if (level <= 0.0f || freqHz <= 0.0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
// The octave shift is baked into freqHz by the voice; here we just phase-accumulate.
|
||||||
|
const double inc = kTwoPi * freqHz / sr;
|
||||||
|
phase += inc;
|
||||||
|
phase -= std::floor (phase * (1.0 / kTwoPi)) * kTwoPi;
|
||||||
|
|
||||||
|
float s = 0.0f;
|
||||||
|
if (shape == (int) SubShape::Triangle)
|
||||||
|
{
|
||||||
|
const float p = (float) (phase * (1.0 / kTwoPi));
|
||||||
|
s = 1.0f - 4.0f * std::abs (p - 0.5f);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
s = (float) std::sin (phase);
|
||||||
|
}
|
||||||
|
|
||||||
|
out += s * level;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace serum
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <JuceHeader.h>
|
||||||
|
#include "Params.h"
|
||||||
|
|
||||||
|
namespace serum
|
||||||
|
{
|
||||||
|
|
||||||
|
// ===========================================================================
|
||||||
|
// Simple sub oscillator: sine or triangle, one/two octaves down or unison.
|
||||||
|
// ===========================================================================
|
||||||
|
class SubOscillator
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
void prepare (double sampleRate) { sr = sampleRate; phase = 0.0; }
|
||||||
|
void reset() { phase = 0.0; }
|
||||||
|
|
||||||
|
void noteOn (double freqHz, int octaveShift);
|
||||||
|
|
||||||
|
// Accumulate into out (mono; the caller pans/levels it).
|
||||||
|
void processAdd (double freqHz, int shape, float level, float& out) noexcept;
|
||||||
|
|
||||||
|
private:
|
||||||
|
double sr = 44100.0;
|
||||||
|
double phase = 0.0;
|
||||||
|
static constexpr double kTwoPi = 6.28318530717958647692;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace serum
|
||||||
@@ -0,0 +1,220 @@
|
|||||||
|
#include "SynthVoice.h"
|
||||||
|
|
||||||
|
namespace serum
|
||||||
|
{
|
||||||
|
|
||||||
|
namespace
|
||||||
|
{
|
||||||
|
inline float clampF (float v, float lo, float hi) noexcept
|
||||||
|
{
|
||||||
|
return v < lo ? lo : (v > hi ? hi : v);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void SynthVoice::prepare (double sampleRate, int maxBlockSize)
|
||||||
|
{
|
||||||
|
oscA.prepare (sampleRate);
|
||||||
|
oscB.prepare (sampleRate);
|
||||||
|
sub.prepare (sampleRate);
|
||||||
|
noise.prepare (sampleRate);
|
||||||
|
filters.prepare (sampleRate, maxBlockSize);
|
||||||
|
for (auto& e : env)
|
||||||
|
e.prepare (sampleRate);
|
||||||
|
scratch.setSize (2, maxBlockSize, false, false, true);
|
||||||
|
reset();
|
||||||
|
}
|
||||||
|
|
||||||
|
void SynthVoice::reset()
|
||||||
|
{
|
||||||
|
oscA.reset(); oscB.reset();
|
||||||
|
sub.reset(); noise.reset();
|
||||||
|
filters.reset();
|
||||||
|
for (auto& e : env)
|
||||||
|
e.reset();
|
||||||
|
note = -1;
|
||||||
|
velocity = 0.0f;
|
||||||
|
baseFreq = 0.0;
|
||||||
|
active = released = false;
|
||||||
|
lastUnisonA = lastUnisonB = 1;
|
||||||
|
noteRandom = 0.5f;
|
||||||
|
scratch.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
void SynthVoice::noteOn (int noteNumber, float velocity01, double freqHz, juce::uint32 noteSeed)
|
||||||
|
{
|
||||||
|
note = noteNumber;
|
||||||
|
velocity = velocity01;
|
||||||
|
baseFreq = freqHz;
|
||||||
|
active = true;
|
||||||
|
released = false;
|
||||||
|
seed = noteSeed;
|
||||||
|
lastUnisonA = lastUnisonB = 0; // force unison reconfigure on first render
|
||||||
|
|
||||||
|
juce::Random rng (noteSeed);
|
||||||
|
noteRandom = rng.nextFloat();
|
||||||
|
|
||||||
|
sub.noteOn (freqHz, -1);
|
||||||
|
noise.noteOn (noteSeed);
|
||||||
|
|
||||||
|
for (auto& e : env)
|
||||||
|
e.noteOn();
|
||||||
|
}
|
||||||
|
|
||||||
|
void SynthVoice::noteOff()
|
||||||
|
{
|
||||||
|
if (! active || released)
|
||||||
|
return;
|
||||||
|
released = true;
|
||||||
|
for (auto& e : env)
|
||||||
|
e.noteOff();
|
||||||
|
}
|
||||||
|
|
||||||
|
void SynthVoice::render (float* outL, float* outR, int numSamples, const RenderContext& ctx) noexcept
|
||||||
|
{
|
||||||
|
if (! active || numSamples <= 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
// 1. Refresh envelope parameters (cheap; also lets UI edits affect held notes).
|
||||||
|
for (int i = 0; i < kNumEnvelopes; ++i)
|
||||||
|
env[(size_t) i].setParams (maps::toSeconds (ctx.envAttack[i]),
|
||||||
|
maps::toSeconds (ctx.envDecay[i]),
|
||||||
|
ctx.envSustain[i],
|
||||||
|
maps::toSeconds (ctx.envRelease[i]),
|
||||||
|
ctx.envCurve[i]);
|
||||||
|
|
||||||
|
// 2. Block-start envelope values (control-rate modulation sources).
|
||||||
|
float envStart[kNumEnvelopes];
|
||||||
|
for (int i = 0; i < kNumEnvelopes; ++i)
|
||||||
|
envStart[i] = env[(size_t) i].getValue();
|
||||||
|
|
||||||
|
// 3. Per-voice modulation source values.
|
||||||
|
float src[kNumModSources];
|
||||||
|
src[(int) ModSource::Lfo1] = ctx.lfoValues[0];
|
||||||
|
src[(int) ModSource::Lfo2] = ctx.lfoValues[1];
|
||||||
|
src[(int) ModSource::Lfo3] = ctx.lfoValues[2];
|
||||||
|
src[(int) ModSource::Lfo4] = ctx.lfoValues[3];
|
||||||
|
src[(int) ModSource::Env1] = envStart[0];
|
||||||
|
src[(int) ModSource::Env2] = envStart[1];
|
||||||
|
src[(int) ModSource::Env3] = envStart[2];
|
||||||
|
src[(int) ModSource::Env4] = envStart[3];
|
||||||
|
src[(int) ModSource::Velocity] = velocity;
|
||||||
|
src[(int) ModSource::Note] = clampF ((float) note / 127.0f, 0.0f, 1.0f);
|
||||||
|
src[(int) ModSource::ModWheel] = ctx.modWheel;
|
||||||
|
src[(int) ModSource::PitchBend]= ctx.pitchBend;
|
||||||
|
src[(int) ModSource::Macro1] = ctx.macroValues[0];
|
||||||
|
src[(int) ModSource::Macro2] = ctx.macroValues[1];
|
||||||
|
src[(int) ModSource::Macro3] = ctx.macroValues[2];
|
||||||
|
src[(int) ModSource::Macro4] = ctx.macroValues[3];
|
||||||
|
src[(int) ModSource::Random] = noteRandom;
|
||||||
|
|
||||||
|
// 4. Accumulate modulation offsets (block rate).
|
||||||
|
std::array<float, kNumModTargets> mod { { } };
|
||||||
|
if (ctx.matrix != nullptr)
|
||||||
|
{
|
||||||
|
for (const auto& c : ctx.matrix->connections)
|
||||||
|
{
|
||||||
|
float v = src[(int) c.source];
|
||||||
|
if (c.bipolar && ! isBipolarSource (c.source))
|
||||||
|
v = v * 2.0f - 1.0f;
|
||||||
|
mod[(int) c.target] += v * c.depth;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (ctx.macros != nullptr)
|
||||||
|
{
|
||||||
|
for (int m = 0; m < kNumMacros; ++m)
|
||||||
|
for (const auto& a : ctx.macros->assignments[(size_t) m])
|
||||||
|
mod[(int) a.target] += ctx.macroValues[m] * a.depth;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5. Modulated oscillator parameters.
|
||||||
|
OscParams a = ctx.oscA;
|
||||||
|
OscParams b = ctx.oscB;
|
||||||
|
a.level = clampF (a.level + mod[(int) ModTarget::OscALevel], 0.0f, 1.0f);
|
||||||
|
a.pan = clampF (a.pan + mod[(int) ModTarget::OscAPan], -1.0f, 1.0f);
|
||||||
|
a.wtPos = clampF (a.wtPos + mod[(int) ModTarget::OscAWtPos], 0.0f, 1.0f);
|
||||||
|
a.unison = (int) std::llround (clampF ((float) a.unison + mod[(int) ModTarget::OscAUnison] * 16.0f, 1.0f, 16.0f));
|
||||||
|
a.detune = clampF (a.detune + mod[(int) ModTarget::OscADetune], 0.0f, 1.0f);
|
||||||
|
a.spread = clampF (a.spread + mod[(int) ModTarget::OscASpread], 0.0f, 1.0f);
|
||||||
|
a.warpAmt = clampF (a.warpAmt+ mod[(int) ModTarget::OscAWarpAmt], 0.0f, 1.0f);
|
||||||
|
|
||||||
|
b.level = clampF (b.level + mod[(int) ModTarget::OscBLevel], 0.0f, 1.0f);
|
||||||
|
b.pan = clampF (b.pan + mod[(int) ModTarget::OscBPan], -1.0f, 1.0f);
|
||||||
|
b.wtPos = clampF (b.wtPos + mod[(int) ModTarget::OscBWtPos], 0.0f, 1.0f);
|
||||||
|
b.unison = (int) std::llround (clampF ((float) b.unison + mod[(int) ModTarget::OscBUnison] * 16.0f, 1.0f, 16.0f));
|
||||||
|
b.detune = clampF (b.detune + mod[(int) ModTarget::OscBDetune], 0.0f, 1.0f);
|
||||||
|
b.spread = clampF (b.spread + mod[(int) ModTarget::OscBSpread], 0.0f, 1.0f);
|
||||||
|
b.warpAmt = clampF (b.warpAmt+ mod[(int) ModTarget::OscBWarpAmt], 0.0f, 1.0f);
|
||||||
|
|
||||||
|
// 6. Frequency (pitch bend + mod matrix pitch + per-osc coarse/fine).
|
||||||
|
const double semis = ctx.pitchBend * ctx.pitchBendRange + mod[(int) ModTarget::Pitch] * 24.0;
|
||||||
|
const double bent = baseFreq * std::pow (2.0, semis / 12.0);
|
||||||
|
const double freqA = bent * std::pow (2.0, (double) a.coarse / 12.0 + (double) a.fine / 1200.0);
|
||||||
|
const double freqB = bent * std::pow (2.0, (double) b.coarse / 12.0 + (double) b.fine / 1200.0);
|
||||||
|
|
||||||
|
// Reconfigure unison only when the integer count changes (avoids phase reset
|
||||||
|
// on every block when unison is LFO-modulated at control rate).
|
||||||
|
if (a.unison != lastUnisonA) { oscA.noteOn (freqA, a, seed); lastUnisonA = a.unison; }
|
||||||
|
if (b.unison != lastUnisonB) { oscB.noteOn (freqB, b, seed + 1); lastUnisonB = b.unison; }
|
||||||
|
|
||||||
|
// 7. Modulated filter parameters.
|
||||||
|
FilterBankParams fb = ctx.filters;
|
||||||
|
fb.f1Cutoff = clampF (fb.f1Cutoff + mod[(int) ModTarget::Filter1Cutoff], 0.0f, 1.0f);
|
||||||
|
fb.f1Res = clampF (fb.f1Res + mod[(int) ModTarget::Filter1Res], 0.0f, 1.0f);
|
||||||
|
fb.f1Drive = clampF (fb.f1Drive + mod[(int) ModTarget::Filter1Drive], 0.0f, 1.0f);
|
||||||
|
fb.f2Cutoff = clampF (fb.f2Cutoff + mod[(int) ModTarget::Filter2Cutoff], 0.0f, 1.0f);
|
||||||
|
fb.f2Res = clampF (fb.f2Res + mod[(int) ModTarget::Filter2Res], 0.0f, 1.0f);
|
||||||
|
fb.f2Drive = clampF (fb.f2Drive + mod[(int) ModTarget::Filter2Drive], 0.0f, 1.0f);
|
||||||
|
fb.mix = clampF (fb.mix + mod[(int) ModTarget::FilterMix], 0.0f, 1.0f);
|
||||||
|
fb.out = clampF (fb.out + mod[(int) ModTarget::FilterOut], 0.0f, 1.5f);
|
||||||
|
|
||||||
|
// 8. Amp modulation + velocity.
|
||||||
|
const float ampMod = 1.0f + mod[(int) ModTarget::Amp];
|
||||||
|
const float velGain = velocity * velocity + 0.001f;
|
||||||
|
|
||||||
|
// 9. Generate oscillators/sub/noise into scratch.
|
||||||
|
float* sL = scratch.getWritePointer (0);
|
||||||
|
float* sR = scratch.getWritePointer (1);
|
||||||
|
const Wavetable& wtA = ctx.wavetables->getTable (a.wave);
|
||||||
|
const Wavetable& wtB = ctx.wavetables->getTable (b.wave);
|
||||||
|
const double subMult = (ctx.subOct == -2) ? 0.25 : (ctx.subOct == -1) ? 0.5 : 1.0;
|
||||||
|
|
||||||
|
for (int i = 0; i < numSamples; ++i)
|
||||||
|
{
|
||||||
|
float l = 0.0f, r = 0.0f;
|
||||||
|
oscA.processAdd (wtA, a, freqA, l, r);
|
||||||
|
oscB.processAdd (wtB, b, freqB, l, r);
|
||||||
|
|
||||||
|
float mono = 0.0f;
|
||||||
|
if (ctx.subOn)
|
||||||
|
sub.processAdd (bent * subMult, ctx.subShape, ctx.subLevel, mono);
|
||||||
|
if (ctx.noiseOn)
|
||||||
|
noise.processAdd (ctx.noiseType, ctx.noiseLevel, mono);
|
||||||
|
l += mono;
|
||||||
|
r += mono;
|
||||||
|
|
||||||
|
sL[i] = l;
|
||||||
|
sR[i] = r;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 10. Filter bank (control rate).
|
||||||
|
filters.process (sL, sR, numSamples, fb, (float) baseFreq);
|
||||||
|
|
||||||
|
// 11. Amp envelope (per sample) and advance the remaining envelopes.
|
||||||
|
for (int i = 0; i < numSamples; ++i)
|
||||||
|
{
|
||||||
|
const float amp = env[0].process();
|
||||||
|
env[1].process();
|
||||||
|
env[2].process();
|
||||||
|
env[3].process();
|
||||||
|
|
||||||
|
const float gain = amp * ampMod * velGain;
|
||||||
|
outL[i] += sL[i] * gain;
|
||||||
|
outR[i] += sR[i] * gain;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 12. Release completes when the amp envelope has fully decayed.
|
||||||
|
if (released && ! env[0].isActive())
|
||||||
|
active = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace serum
|
||||||
@@ -0,0 +1,98 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <JuceHeader.h>
|
||||||
|
#include "Params.h"
|
||||||
|
#include "Wavetable.h"
|
||||||
|
#include "Oscillator.h"
|
||||||
|
#include "SubOscillator.h"
|
||||||
|
#include "NoiseOscillator.h"
|
||||||
|
#include "FilterBank.h"
|
||||||
|
#include "Envelope.h"
|
||||||
|
#include "ModulationMatrix.h"
|
||||||
|
#include "MacroControls.h"
|
||||||
|
|
||||||
|
namespace serum
|
||||||
|
{
|
||||||
|
|
||||||
|
// ===========================================================================
|
||||||
|
// Everything a voice needs to render one block. Global (non per-voice) values
|
||||||
|
// are provided by the engine; the voice adds its own per-voice modulation.
|
||||||
|
// ===========================================================================
|
||||||
|
struct RenderContext
|
||||||
|
{
|
||||||
|
double sampleRate = 44100.0;
|
||||||
|
const WavetableLibrary* wavetables = nullptr;
|
||||||
|
|
||||||
|
float lfoValues[kNumLfos] { 0.0f, 0.0f, 0.0f, 0.0f };
|
||||||
|
float macroValues[kNumMacros] { 0.0f, 0.0f, 0.0f, 0.0f };
|
||||||
|
float modWheel = 0.0f;
|
||||||
|
float pitchBend = 0.0f;
|
||||||
|
float pitchBendRange = 2.0f;
|
||||||
|
|
||||||
|
const ModulationMatrix* matrix = nullptr;
|
||||||
|
const MacroControls* macros = nullptr;
|
||||||
|
|
||||||
|
OscParams oscA;
|
||||||
|
OscParams oscB;
|
||||||
|
|
||||||
|
bool subOn = false;
|
||||||
|
int subShape = 0;
|
||||||
|
int subOct = -1;
|
||||||
|
float subLevel = 0.0f;
|
||||||
|
|
||||||
|
bool noiseOn = false;
|
||||||
|
int noiseType = 0;
|
||||||
|
float noiseLevel = 0.0f;
|
||||||
|
|
||||||
|
FilterBankParams filters;
|
||||||
|
|
||||||
|
float envAttack[4] { 0.01f, 0.01f, 0.01f, 0.01f };
|
||||||
|
float envDecay[4] { 0.2f, 0.2f, 0.2f, 0.2f };
|
||||||
|
float envSustain[4]{ 0.7f, 0.7f, 0.7f, 0.7f };
|
||||||
|
float envRelease[4]{ 0.3f, 0.3f, 0.3f, 0.3f };
|
||||||
|
float envCurve[4] { 0.5f, 0.5f, 0.5f, 0.5f };
|
||||||
|
};
|
||||||
|
|
||||||
|
// ===========================================================================
|
||||||
|
// One synthesis voice: two oscillators + sub + noise -> filter bank -> amp
|
||||||
|
// envelope. Four envelopes provide per-voice modulation sources.
|
||||||
|
// ===========================================================================
|
||||||
|
class SynthVoice
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
void prepare (double sampleRate, int maxBlockSize);
|
||||||
|
void reset();
|
||||||
|
|
||||||
|
void noteOn (int noteNumber, float velocity, double freqHz, juce::uint32 seed);
|
||||||
|
void noteOff();
|
||||||
|
|
||||||
|
void render (float* outL, float* outR, int numSamples, const RenderContext& ctx) noexcept;
|
||||||
|
|
||||||
|
bool isActive() const noexcept { return active; }
|
||||||
|
bool isReleased() const noexcept { return released; }
|
||||||
|
int getNote() const noexcept { return note; }
|
||||||
|
juce::uint64 getNoteId() const noexcept { return noteId; }
|
||||||
|
|
||||||
|
private:
|
||||||
|
Oscillator oscA, oscB;
|
||||||
|
SubOscillator sub;
|
||||||
|
NoiseOscillator noise;
|
||||||
|
FilterBank filters;
|
||||||
|
std::array<Envelope, 4> env;
|
||||||
|
|
||||||
|
int note = -1;
|
||||||
|
float velocity = 0.0f;
|
||||||
|
double baseFreq = 0.0;
|
||||||
|
bool active = false;
|
||||||
|
bool released = false;
|
||||||
|
juce::uint64 noteId = 0;
|
||||||
|
|
||||||
|
int lastUnisonA = 1, lastUnisonB = 1;
|
||||||
|
|
||||||
|
juce::uint32 seed = 0;
|
||||||
|
float noteRandom = 0.5f;
|
||||||
|
|
||||||
|
juce::AudioBuffer<float> scratch; // 2 channels
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace serum
|
||||||
@@ -0,0 +1,348 @@
|
|||||||
|
#include "Wavetable.h"
|
||||||
|
|
||||||
|
namespace serum
|
||||||
|
{
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Wavetable
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
void Wavetable::clear()
|
||||||
|
{
|
||||||
|
frames.clear();
|
||||||
|
name = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
void Wavetable::buildHarmonic (const juce::String& n, HarmonicAmpFn ampFn, int numHarmonics)
|
||||||
|
{
|
||||||
|
name = n;
|
||||||
|
frames.assign (kFrames, std::vector<float> (kTableSize, 0.0f));
|
||||||
|
|
||||||
|
const float invN = 1.0f / (float) kTableSize;
|
||||||
|
constexpr double twoPi = 6.28318530717958647692;
|
||||||
|
|
||||||
|
for (int f = 0; f < kFrames; ++f)
|
||||||
|
{
|
||||||
|
auto& frame = frames[(size_t) f];
|
||||||
|
|
||||||
|
// Precompute per-harmonic rotation step (cos/sin of the angular increment).
|
||||||
|
std::vector<double> cosStep ((size_t) numHarmonics + 1, 0.0);
|
||||||
|
std::vector<double> sinStep ((size_t) numHarmonics + 1, 0.0);
|
||||||
|
std::vector<double> mag ((size_t) numHarmonics + 1, 0.0);
|
||||||
|
std::vector<double> phase ((size_t) numHarmonics + 1, 0.0);
|
||||||
|
|
||||||
|
double maxAmp = 1e-9;
|
||||||
|
for (int h = 1; h <= numHarmonics; ++h)
|
||||||
|
{
|
||||||
|
const float a = ampFn (f, h);
|
||||||
|
const double m = std::abs ((double) a);
|
||||||
|
mag[(size_t) h] = m;
|
||||||
|
phase[(size_t) h] = (a < 0.0f) ? twoPi * 0.5 : 0.0; // sign -> 0 or pi/... using sine base
|
||||||
|
const double ang = twoPi * (double) h * (double) invN;
|
||||||
|
cosStep[(size_t) h] = std::cos (ang);
|
||||||
|
sinStep[(size_t) h] = std::sin (ang);
|
||||||
|
maxAmp = std::max (maxAmp, m);
|
||||||
|
}
|
||||||
|
|
||||||
|
double peak = 1e-9;
|
||||||
|
for (int h = 1; h <= numHarmonics; ++h)
|
||||||
|
{
|
||||||
|
if (mag[(size_t) h] < 1e-9)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
// Start the rotating phasor at the harmonic's phase offset.
|
||||||
|
double s = std::sin (phase[(size_t) h]);
|
||||||
|
double c = std::cos (phase[(size_t) h]);
|
||||||
|
const double cs = cosStep[(size_t) h];
|
||||||
|
const double ss = sinStep[(size_t) h];
|
||||||
|
const double m = mag[(size_t) h];
|
||||||
|
|
||||||
|
for (int i = 0; i < kTableSize; ++i)
|
||||||
|
{
|
||||||
|
frame[(size_t) i] += (float) (m * s);
|
||||||
|
const double s2 = s * cs + c * ss;
|
||||||
|
c = c * cs - s * ss;
|
||||||
|
s = s2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Normalise each frame to unity peak so morphing keeps a stable level.
|
||||||
|
for (int i = 0; i < kTableSize; ++i)
|
||||||
|
peak = std::max (peak, (double) std::abs (frame[(size_t) i]));
|
||||||
|
|
||||||
|
if (peak > 1e-6)
|
||||||
|
{
|
||||||
|
const float g = (float) (1.0 / peak);
|
||||||
|
for (int i = 0; i < kTableSize; ++i)
|
||||||
|
frame[(size_t) i] *= g;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
float Wavetable::read (float framePos, float phase) const noexcept
|
||||||
|
{
|
||||||
|
if (frames.empty())
|
||||||
|
return 0.0f;
|
||||||
|
|
||||||
|
framePos = juce::jlimit (0.0f, (float) (kFrames - 1), framePos);
|
||||||
|
int f0 = (int) framePos;
|
||||||
|
float frac = framePos - (float) f0;
|
||||||
|
int f1 = juce::jmin (f0 + 1, kFrames - 1);
|
||||||
|
|
||||||
|
float p = phase * (float) kTableSize;
|
||||||
|
int i0 = (int) p;
|
||||||
|
if (i0 < 0) i0 = 0;
|
||||||
|
float t = p - (float) i0;
|
||||||
|
int i1 = (i0 + 1) & (kTableSize - 1);
|
||||||
|
i0 &= (kTableSize - 1);
|
||||||
|
|
||||||
|
const auto& a = frames[(size_t) f0];
|
||||||
|
const auto& b = frames[(size_t) f1];
|
||||||
|
const float s0 = lerp (a[(size_t) i0], a[(size_t) i1], t);
|
||||||
|
const float s1 = lerp (b[(size_t) i0], b[(size_t) i1], t);
|
||||||
|
return lerp (s0, s1, frac);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Wavetable::copyFrame (int frameIndex, float* dest, int numSamples) const
|
||||||
|
{
|
||||||
|
if (frames.empty() || dest == nullptr)
|
||||||
|
return;
|
||||||
|
|
||||||
|
frameIndex = juce::jlimit (0, kFrames - 1, frameIndex);
|
||||||
|
const auto& frame = frames[(size_t) frameIndex];
|
||||||
|
const int n = juce::jmin (numSamples, kTableSize);
|
||||||
|
for (int i = 0; i < n; ++i)
|
||||||
|
dest[i] = frame[(size_t) i];
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Library
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
void WavetableLibrary::prebuild()
|
||||||
|
{
|
||||||
|
for (int i = 0; i < kNumWavetables; ++i)
|
||||||
|
getTable (i);
|
||||||
|
}
|
||||||
|
|
||||||
|
const Wavetable& WavetableLibrary::getTable (int index) const
|
||||||
|
{
|
||||||
|
index = juce::jlimit (0, kNumWavetables - 1, index);
|
||||||
|
if (!built[(size_t) index])
|
||||||
|
buildTable (index);
|
||||||
|
return tables[(size_t) index];
|
||||||
|
}
|
||||||
|
|
||||||
|
void WavetableLibrary::buildTable (int index) const
|
||||||
|
{
|
||||||
|
switch (index)
|
||||||
|
{
|
||||||
|
case 0: tables[(size_t) index] = makeBasic(); break;
|
||||||
|
case 1: tables[(size_t) index] = makeSawPwm(); break;
|
||||||
|
case 2: tables[(size_t) index] = makeSquareSync(); break;
|
||||||
|
case 3: tables[(size_t) index] = makeTriangleFold(); break;
|
||||||
|
case 4: tables[(size_t) index] = makeVowel(); break;
|
||||||
|
case 5: tables[(size_t) index] = makeOrgan(); break;
|
||||||
|
case 6: tables[(size_t) index] = makeWarmSaw(); break;
|
||||||
|
case 7: tables[(size_t) index] = makeDigital(); break;
|
||||||
|
case 8: tables[(size_t) index] = makeGlass(); break;
|
||||||
|
case 9: tables[(size_t) index] = makeBass(); break;
|
||||||
|
default: tables[(size_t) index] = makeBasic(); break;
|
||||||
|
}
|
||||||
|
built[(size_t) index] = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace
|
||||||
|
{
|
||||||
|
inline float normT (int frame) { return (float) frame / 255.0f; }
|
||||||
|
constexpr float kPi = 3.14159265358979323846f;
|
||||||
|
}
|
||||||
|
|
||||||
|
// sine -> saw morph
|
||||||
|
Wavetable WavetableLibrary::makeBasic()
|
||||||
|
{
|
||||||
|
Wavetable wt;
|
||||||
|
wt.buildHarmonic ("Basic Shapes", [] (int f, int h)
|
||||||
|
{
|
||||||
|
const float t = normT (f);
|
||||||
|
const float sine = (h == 1) ? 1.0f : 0.0f;
|
||||||
|
const float saw = 1.0f / (float) h;
|
||||||
|
return sine + (saw - sine) * t;
|
||||||
|
}, 48);
|
||||||
|
return wt;
|
||||||
|
}
|
||||||
|
|
||||||
|
// narrow pulse -> wide pulse
|
||||||
|
Wavetable WavetableLibrary::makeSawPwm()
|
||||||
|
{
|
||||||
|
Wavetable wt;
|
||||||
|
wt.buildHarmonic ("Saw PWM", [] (int f, int h)
|
||||||
|
{
|
||||||
|
const float t = normT (f);
|
||||||
|
const float duty = 0.08f + 0.42f * t; // 8% -> 50%
|
||||||
|
const float a = std::sin (kPi * (float) h * duty);
|
||||||
|
return (2.0f / ((float) h * kPi)) * a;
|
||||||
|
}, 48);
|
||||||
|
return wt;
|
||||||
|
}
|
||||||
|
|
||||||
|
// hard-sync style comb sweep
|
||||||
|
Wavetable WavetableLibrary::makeSquareSync()
|
||||||
|
{
|
||||||
|
Wavetable wt;
|
||||||
|
wt.buildHarmonic ("Square Sync", [] (int f, int h)
|
||||||
|
{
|
||||||
|
const float t = normT (f);
|
||||||
|
const float ratio = 1.0f + 3.0f * t;
|
||||||
|
const float comb = 0.5f + 0.5f * std::cos (2.0f * kPi * (float) h * ratio);
|
||||||
|
return (1.0f / std::pow ((float) h, 0.8f)) * comb;
|
||||||
|
}, 48);
|
||||||
|
return wt;
|
||||||
|
}
|
||||||
|
|
||||||
|
// triangle -> folded/saw-ish
|
||||||
|
Wavetable WavetableLibrary::makeTriangleFold()
|
||||||
|
{
|
||||||
|
Wavetable wt;
|
||||||
|
wt.buildHarmonic ("Triangle Fold", [] (int f, int h)
|
||||||
|
{
|
||||||
|
const float t = normT (f);
|
||||||
|
// Triangle: odd harmonics with alternating sign, 1/h^2.
|
||||||
|
float tri = 0.0f;
|
||||||
|
if ((h & 1) == 1)
|
||||||
|
{
|
||||||
|
const int k = (h - 1) / 2;
|
||||||
|
const float sign = ((k & 1) == 0) ? 1.0f : -1.0f;
|
||||||
|
tri = sign * 8.0f / (kPi * kPi * (float) (h * h));
|
||||||
|
}
|
||||||
|
const float saw = 1.0f / (float) h;
|
||||||
|
return tri + (saw - tri) * t;
|
||||||
|
}, 48);
|
||||||
|
return wt;
|
||||||
|
}
|
||||||
|
|
||||||
|
// formant morph a -> e -> i
|
||||||
|
Wavetable WavetableLibrary::makeVowel()
|
||||||
|
{
|
||||||
|
Wavetable wt;
|
||||||
|
wt.buildHarmonic ("Vowel", [] (int f, int h)
|
||||||
|
{
|
||||||
|
const float t = normT (f);
|
||||||
|
// Three formant sets (Hz) with f0 = 55 Hz.
|
||||||
|
const float f0 = 55.0f;
|
||||||
|
const float aa[3] = { 730.0f, 1090.0f, 2440.0f };
|
||||||
|
const float ee[3] = { 530.0f, 1840.0f, 2480.0f };
|
||||||
|
const float ii[3] = { 270.0f, 2290.0f, 3010.0f };
|
||||||
|
|
||||||
|
float from[3], to[3];
|
||||||
|
if (t < 0.5f)
|
||||||
|
{
|
||||||
|
const float u = t * 2.0f;
|
||||||
|
for (int k = 0; k < 3; ++k) { from[k] = aa[k]; to[k] = ee[k]; }
|
||||||
|
(void) u;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
const float u = (t - 0.5f) * 2.0f;
|
||||||
|
for (int k = 0; k < 3; ++k) { from[k] = ee[k]; to[k] = ii[k]; }
|
||||||
|
(void) u;
|
||||||
|
}
|
||||||
|
|
||||||
|
float tt = (t < 0.5f) ? t * 2.0f : (t - 0.5f) * 2.0f;
|
||||||
|
float sum = 0.0f;
|
||||||
|
const float sigma = 1.8f; // formant bandwidth in harmonic units
|
||||||
|
for (int k = 0; k < 3; ++k)
|
||||||
|
{
|
||||||
|
const float fc = from[k] + (to[k] - from[k]) * tt;
|
||||||
|
const float center = fc / f0;
|
||||||
|
const float d = ((float) h - center) / sigma;
|
||||||
|
sum += std::exp (-0.5f * d * d) * (k == 0 ? 1.0f : 0.6f);
|
||||||
|
}
|
||||||
|
return sum;
|
||||||
|
}, 48);
|
||||||
|
return wt;
|
||||||
|
}
|
||||||
|
|
||||||
|
// drawbar organ morph
|
||||||
|
Wavetable WavetableLibrary::makeOrgan()
|
||||||
|
{
|
||||||
|
Wavetable wt;
|
||||||
|
wt.buildHarmonic ("Organ", [] (int f, int h)
|
||||||
|
{
|
||||||
|
const float t = normT (f);
|
||||||
|
// two drawbar registrations (16', 8', 5 1/3', 4', 2 2/3', 2', ...)
|
||||||
|
const float regA[8] = { 0.0f, 1.0f, 0.0f, 0.35f, 0.0f, 0.2f, 0.0f, 0.1f };
|
||||||
|
const float regB[8] = { 0.5f, 1.0f, 0.4f, 0.6f, 0.25f, 0.5f, 0.2f, 0.35f };
|
||||||
|
if (h > 8) return 0.0f;
|
||||||
|
const float a = regA[h - 1];
|
||||||
|
const float b = regB[h - 1];
|
||||||
|
return a + (b - a) * t;
|
||||||
|
}, 8);
|
||||||
|
return wt;
|
||||||
|
}
|
||||||
|
|
||||||
|
// warm lowpassed saw -> brighter
|
||||||
|
Wavetable WavetableLibrary::makeWarmSaw()
|
||||||
|
{
|
||||||
|
Wavetable wt;
|
||||||
|
wt.buildHarmonic ("Warm Saw", [] (int f, int h)
|
||||||
|
{
|
||||||
|
const float t = normT (f);
|
||||||
|
const float cutoff = 10.0f + 34.0f * t; // harmonic rolloff centre
|
||||||
|
const float rolloff = std::exp (-((float) h / cutoff) * ((float) h / cutoff));
|
||||||
|
const float body = 1.0f / std::pow ((float) h, 0.9f);
|
||||||
|
return body * (0.4f + 0.6f * rolloff);
|
||||||
|
}, 48);
|
||||||
|
return wt;
|
||||||
|
}
|
||||||
|
|
||||||
|
// additive, brighter and slightly combed
|
||||||
|
Wavetable WavetableLibrary::makeDigital()
|
||||||
|
{
|
||||||
|
Wavetable wt;
|
||||||
|
wt.buildHarmonic ("Digital", [] (int f, int h)
|
||||||
|
{
|
||||||
|
const float t = normT (f);
|
||||||
|
const float comb = 0.5f + 0.5f * std::cos ((float) h * 0.9f * (1.0f + t));
|
||||||
|
return (1.0f / std::pow ((float) h, 0.6f)) * (0.5f + 0.5f * comb);
|
||||||
|
}, 48);
|
||||||
|
return wt;
|
||||||
|
}
|
||||||
|
|
||||||
|
// inharmonic bell-like partial clusters
|
||||||
|
Wavetable WavetableLibrary::makeGlass()
|
||||||
|
{
|
||||||
|
Wavetable wt;
|
||||||
|
wt.buildHarmonic ("Glass", [] (int f, int h)
|
||||||
|
{
|
||||||
|
const float t = normT (f);
|
||||||
|
const float c1 = 1.0f + t * 2.5f;
|
||||||
|
const float c2 = 3.6f + t * 3.4f;
|
||||||
|
const float c3 = 6.2f + t * 4.0f;
|
||||||
|
const float sigma = 0.9f;
|
||||||
|
float sum = 0.0f;
|
||||||
|
const float centers[3] = { c1, c2, c3 };
|
||||||
|
const float gains[3] = { 1.0f, 0.7f, 0.4f };
|
||||||
|
for (int k = 0; k < 3; ++k)
|
||||||
|
{
|
||||||
|
const float d = ((float) h - centers[k]) / sigma;
|
||||||
|
sum += gains[k] * std::exp (-0.5f * d * d);
|
||||||
|
}
|
||||||
|
return sum;
|
||||||
|
}, 48);
|
||||||
|
return wt;
|
||||||
|
}
|
||||||
|
|
||||||
|
// sub-heavy -> fuller bass
|
||||||
|
Wavetable WavetableLibrary::makeBass()
|
||||||
|
{
|
||||||
|
Wavetable wt;
|
||||||
|
wt.buildHarmonic ("Bass", [] (int f, int h)
|
||||||
|
{
|
||||||
|
const float t = normT (f);
|
||||||
|
const float sub = (h == 1) ? 1.0f : ((h == 2) ? 0.4f : ((h == 3) ? 0.12f : 0.0f));
|
||||||
|
const float full = 1.0f / std::pow ((float) h, 1.1f);
|
||||||
|
return sub + (full - sub) * t;
|
||||||
|
}, 48);
|
||||||
|
return wt;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace serum
|
||||||
@@ -0,0 +1,81 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <JuceHeader.h>
|
||||||
|
#include "Params.h"
|
||||||
|
|
||||||
|
namespace serum
|
||||||
|
{
|
||||||
|
|
||||||
|
// ===========================================================================
|
||||||
|
// A single wavetable: 256 frames of 2048 samples each. Frames morph smoothly
|
||||||
|
// from a simple base shape to a complex one. Lookups interpolate bilinearly
|
||||||
|
// (across frames and within a frame). Tables are RAM-resident and generated
|
||||||
|
// lazily by the WavetableLibrary.
|
||||||
|
// ===========================================================================
|
||||||
|
class Wavetable
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
static constexpr int kFrames = 256;
|
||||||
|
static constexpr int kTableSize = 2048;
|
||||||
|
|
||||||
|
Wavetable() = default;
|
||||||
|
|
||||||
|
juce::String name;
|
||||||
|
std::vector<std::vector<float>> frames; // [frame][sample]
|
||||||
|
|
||||||
|
bool isValid() const noexcept { return (int) frames.size() == kFrames; }
|
||||||
|
|
||||||
|
void clear();
|
||||||
|
|
||||||
|
// Build a table from a per-frame harmonic amplitude function.
|
||||||
|
// ampFn(frameIndex, harmonicNumber) -> amplitude (harmonic 1 = fundamental)
|
||||||
|
using HarmonicAmpFn = std::function<float (int, int)>;
|
||||||
|
void buildHarmonic (const juce::String& name, HarmonicAmpFn ampFn, int numHarmonics = 64);
|
||||||
|
|
||||||
|
// Read the table at a fractional frame position and a phase in [0,1).
|
||||||
|
float read (float framePos, float phase) const noexcept;
|
||||||
|
|
||||||
|
// Read with phase wrapped and clamped frame position.
|
||||||
|
float readSafe (float framePos, float phase) const noexcept
|
||||||
|
{
|
||||||
|
phase -= std::floor (phase);
|
||||||
|
return read (juce::jlimit (0.0f, (float) kFrames - 1.001f, framePos), phase);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Copy raw samples of a frame into a destination buffer (for the GUI).
|
||||||
|
void copyFrame (int frameIndex, float* dest, int numSamples) const;
|
||||||
|
|
||||||
|
private:
|
||||||
|
static float lerp (float a, float b, float t) noexcept { return a + (b - a) * t; }
|
||||||
|
};
|
||||||
|
|
||||||
|
// ===========================================================================
|
||||||
|
// Lazily-built library of factory wavetables. Indices map to kWavetableNames.
|
||||||
|
// ===========================================================================
|
||||||
|
class WavetableLibrary
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
const Wavetable& getTable (int index) const;
|
||||||
|
int size() const noexcept { return kNumWavetables; }
|
||||||
|
|
||||||
|
// Force generation of all tables (used at prepare time).
|
||||||
|
void prebuild();
|
||||||
|
|
||||||
|
private:
|
||||||
|
mutable std::array<Wavetable, kNumWavetables> tables;
|
||||||
|
mutable std::array<bool, kNumWavetables> built { { false } };
|
||||||
|
|
||||||
|
void buildTable (int index) const;
|
||||||
|
static Wavetable makeBasic();
|
||||||
|
static Wavetable makeSawPwm();
|
||||||
|
static Wavetable makeSquareSync();
|
||||||
|
static Wavetable makeTriangleFold();
|
||||||
|
static Wavetable makeVowel();
|
||||||
|
static Wavetable makeOrgan();
|
||||||
|
static Wavetable makeWarmSaw();
|
||||||
|
static Wavetable makeDigital();
|
||||||
|
static Wavetable makeGlass();
|
||||||
|
static Wavetable makeBass();
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace serum
|
||||||
Reference in New Issue
Block a user