Files
serumalt/Source/Engine.cpp
T

321 lines
12 KiB
C++

#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 = &macros;
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