#include "PluginProcessor.h" #include "PluginEditor.h" #include "Presets/FactoryPresets.h" namespace serum { namespace { std::unique_ptr f (const char* id, const juce::String& name, float def) { return std::make_unique (juce::ParameterID { id, 1 }, name, juce::NormalisableRange (0.0f, 1.0f), def); } } // --------------------------------------------------------------------------- SerumAltAudioProcessor::SerumAltAudioProcessor() : AudioProcessor (BusesProperties().withInput ("Input", juce::AudioChannelSet::stereo(), false) .withOutput ("Output", juce::AudioChannelSet::stereo(), true)), parameters (*this, nullptr, juce::Identifier ("SerumAlt"), createParameterLayout()) { } SerumAltAudioProcessor::~SerumAltAudioProcessor() = default; juce::AudioProcessorValueTreeState::ParameterLayout SerumAltAudioProcessor::createParameterLayout() { std::vector> params; // Global params.push_back (f (ids::master, "Master", 0.8f)); params.push_back (f (ids::uiScale, "UI Scale", 0.25f)); params.push_back (f (ids::rave, "RAVE", 0.0f)); // Oscillator A params.push_back (f (ids::oscAOn, "Osc A On", 1.0f)); params.push_back (f (ids::oscAWave, "Osc A Wave", 0.0f)); params.push_back (f (ids::oscAWtPos, "Osc A WT Pos", 0.0f)); params.push_back (f (ids::oscAWarp, "Osc A Warp", 0.0f)); params.push_back (f (ids::oscAWarpAmt, "Osc A Warp Amt", 0.0f)); params.push_back (f (ids::oscACoarse, "Osc A Coarse", 0.5f)); params.push_back (f (ids::oscAFine, "Osc A Fine", 0.5f)); params.push_back (f (ids::oscALevel, "Osc A Level", 0.8f)); params.push_back (f (ids::oscAPan, "Osc A Pan", 0.5f)); params.push_back (f (ids::oscAUnison, "Osc A Unison", 0.0f)); params.push_back (f (ids::oscADetune, "Osc A Detune", 0.0f)); params.push_back (f (ids::oscASpread, "Osc A Spread", 0.0f)); params.push_back (f (ids::oscAPhase, "Osc A Phase", 0.0f)); params.push_back (f (ids::oscARandPh, "Osc A Rand Phase", 0.0f)); // Oscillator B params.push_back (f (ids::oscBOn, "Osc B On", 0.0f)); params.push_back (f (ids::oscBWave, "Osc B Wave", 1.0f / 9.0f)); params.push_back (f (ids::oscBWtPos, "Osc B WT Pos", 0.0f)); params.push_back (f (ids::oscBWarp, "Osc B Warp", 0.0f)); params.push_back (f (ids::oscBWarpAmt, "Osc B Warp Amt", 0.0f)); params.push_back (f (ids::oscBCoarse, "Osc B Coarse", 0.5f)); params.push_back (f (ids::oscBFine, "Osc B Fine", 0.5f)); params.push_back (f (ids::oscBLevel, "Osc B Level", 0.5f)); params.push_back (f (ids::oscBPan, "Osc B Pan", 0.5f)); params.push_back (f (ids::oscBUnison, "Osc B Unison", 0.0f)); params.push_back (f (ids::oscBDetune, "Osc B Detune", 0.0f)); params.push_back (f (ids::oscBSpread, "Osc B Spread", 0.0f)); params.push_back (f (ids::oscBPhase, "Osc B Phase", 0.0f)); params.push_back (f (ids::oscBRandPh, "Osc B Rand Phase", 0.0f)); // Sub params.push_back (f (ids::subOn, "Sub On", 0.0f)); params.push_back (f (ids::subShape, "Sub Shape", 0.0f)); params.push_back (f (ids::subOct, "Sub Octave", 1.0f / 2.0f)); params.push_back (f (ids::subLevel, "Sub Level", 0.5f)); // Noise params.push_back (f (ids::noiseOn, "Noise On", 0.0f)); params.push_back (f (ids::noiseType, "Noise Type", 0.0f)); params.push_back (f (ids::noiseLevel, "Noise Level", 0.5f)); // Filter 1 params.push_back (f (ids::f1On, "Filter 1 On", 1.0f)); params.push_back (f (ids::f1Type, "Filter 1 Type", 0.0f)); params.push_back (f (ids::f1Cutoff, "Filter 1 Cutoff", 0.65f)); params.push_back (f (ids::f1Res, "Filter 1 Res", 0.05f)); params.push_back (f (ids::f1Drive, "Filter 1 Drive", 0.0f)); params.push_back (f (ids::f1Key, "Filter 1 Keytrack", 0.0f)); params.push_back (f (ids::f1Slope, "Filter 1 Slope", 1.0f)); // Filter 2 params.push_back (f (ids::f2On, "Filter 2 On", 0.0f)); params.push_back (f (ids::f2Type, "Filter 2 Type", 0.0f)); params.push_back (f (ids::f2Cutoff, "Filter 2 Cutoff", 0.5f)); params.push_back (f (ids::f2Res, "Filter 2 Res", 0.0f)); params.push_back (f (ids::f2Drive, "Filter 2 Drive", 0.0f)); params.push_back (f (ids::f2Key, "Filter 2 Keytrack", 0.0f)); params.push_back (f (ids::f2Slope, "Filter 2 Slope", 1.0f)); params.push_back (f (ids::fRoute, "Filter Route", 0.0f)); params.push_back (f (ids::fMix, "Filter Mix", 0.5f)); params.push_back (f (ids::fOut, "Filter Out", 0.667f)); // Envelopes 1..4 const char* envA[4] = { ids::env1A, ids::env2A, ids::env3A, ids::env4A }; const char* envD[4] = { ids::env1D, ids::env2D, ids::env3D, ids::env4D }; const char* envS[4] = { ids::env1S, ids::env2S, ids::env3S, ids::env4S }; const char* envR[4] = { ids::env1R, ids::env2R, ids::env3R, ids::env4R }; const char* envC[4] = { ids::env1Curve, ids::env2Curve, ids::env3Curve, ids::env4Curve }; const float envADef[4] = { 0.05f, 0.1f, 0.2f, 0.2f }; const float envDDef[4] = { 0.25f, 0.3f, 0.3f, 0.3f }; const float envSDef[4] = { 0.8f, 0.5f, 0.5f, 0.5f }; const float envRDef[4] = { 0.3f, 0.3f, 0.4f, 0.4f }; for (int i = 0; i < 4; ++i) { params.push_back (f (envA[i], juce::String ("Env ") + juce::String (i + 1) + " Attack", envADef[i])); params.push_back (f (envD[i], juce::String ("Env ") + juce::String (i + 1) + " Decay", envDDef[i])); params.push_back (f (envS[i], juce::String ("Env ") + juce::String (i + 1) + " Sustain", envSDef[i])); params.push_back (f (envR[i], juce::String ("Env ") + juce::String (i + 1) + " Release", envRDef[i])); params.push_back (f (envC[i], juce::String ("Env ") + juce::String (i + 1) + " Curve", 0.5f)); } // LFOs 1..4 const char* lfoRate[4] = { ids::lfo1Rate, ids::lfo2Rate, ids::lfo3Rate, ids::lfo4Rate }; const char* lfoSync[4] = { ids::lfo1Sync, ids::lfo2Sync, ids::lfo3Sync, ids::lfo4Sync }; const char* lfoBeat[4] = { ids::lfo1Beat, ids::lfo2Beat, ids::lfo3Beat, ids::lfo4Beat }; const char* lfoShape[4] = { ids::lfo1Shape, ids::lfo2Shape, ids::lfo3Shape, ids::lfo4Shape }; const char* lfoPhase[4] = { ids::lfo1Phase, ids::lfo2Phase, ids::lfo3Phase, ids::lfo4Phase }; const char* lfoFade[4] = { ids::lfo1Fade, ids::lfo2Fade, ids::lfo3Fade, ids::lfo4Fade }; const char* lfoDelay[4] = { ids::lfo1Delay, ids::lfo2Delay, ids::lfo3Delay, ids::lfo4Delay }; const float lfoShapeDef[4] = { 0.0f, 1.0f / 6.0f, 2.0f / 6.0f, 3.0f / 6.0f }; for (int i = 0; i < 4; ++i) { const juce::String n = juce::String ("LFO ") + juce::String (i + 1); params.push_back (f (lfoRate[i], n + " Rate", 0.5f)); params.push_back (f (lfoSync[i], n + " Sync", 0.0f)); params.push_back (f (lfoBeat[i], n + " Beat", 0.5f)); params.push_back (f (lfoShape[i], n + " Shape", lfoShapeDef[i])); params.push_back (f (lfoPhase[i], n + " Phase", 0.0f)); params.push_back (f (lfoFade[i], n + " Fade", 0.0f)); params.push_back (f (lfoDelay[i], n + " Delay", 0.0f)); } // FX slots 1..8 const char* fxType[8] = { ids::fx1Type, ids::fx2Type, ids::fx3Type, ids::fx4Type, ids::fx5Type, ids::fx6Type, ids::fx7Type, ids::fx8Type }; const char* fxMix[8] = { ids::fx1Mix, ids::fx2Mix, ids::fx3Mix, ids::fx4Mix, ids::fx5Mix, ids::fx6Mix, ids::fx7Mix, ids::fx8Mix }; const char* fxP[8][4] = { { ids::fx1P1, ids::fx1P2, ids::fx1P3, ids::fx1P4 }, { ids::fx2P1, ids::fx2P2, ids::fx2P3, ids::fx2P4 }, { ids::fx3P1, ids::fx3P2, ids::fx3P3, ids::fx3P4 }, { ids::fx4P1, ids::fx4P2, ids::fx4P3, ids::fx4P4 }, { ids::fx5P1, ids::fx5P2, ids::fx5P3, ids::fx5P4 }, { ids::fx6P1, ids::fx6P2, ids::fx6P3, ids::fx6P4 }, { ids::fx7P1, ids::fx7P2, ids::fx7P3, ids::fx7P4 }, { ids::fx8P1, ids::fx8P2, ids::fx8P3, ids::fx8P4 } }; for (int i = 0; i < 8; ++i) { const juce::String n = juce::String ("FX ") + juce::String (i + 1); params.push_back (f (fxType[i], n + " Type", 0.0f)); params.push_back (f (fxMix[i], n + " Mix", 0.5f)); params.push_back (f (fxP[i][0], n + " P1", 0.5f)); params.push_back (f (fxP[i][1], n + " P2", 0.5f)); params.push_back (f (fxP[i][2], n + " P3", 0.5f)); params.push_back (f (fxP[i][3], n + " P4", 0.5f)); } // Macros params.push_back (f (ids::macro1, "Macro 1", 0.0f)); params.push_back (f (ids::macro2, "Macro 2", 0.0f)); params.push_back (f (ids::macro3, "Macro 3", 0.0f)); params.push_back (f (ids::macro4, "Macro 4", 0.0f)); return { params.begin(), params.end() }; } // --------------------------------------------------------------------------- void SerumAltAudioProcessor::prepareToPlay (double sampleRate, int samplesPerBlock) { engine.prepare (sampleRate, samplesPerBlock, parameters); } void SerumAltAudioProcessor::releaseResources() { engine.reset(); } void SerumAltAudioProcessor::processBlock (juce::AudioBuffer& buffer, juce::MidiBuffer& midi) { juce::ScopedNoDenormals noDenormals; engine.processBlock (buffer, midi, parameters, getPlayHead()); } juce::AudioProcessorEditor* SerumAltAudioProcessor::createEditor() { return new PluginEditor (*this); } // --------------------------------------------------------------------------- // Programs / presets // --------------------------------------------------------------------------- int SerumAltAudioProcessor::getNumPrograms() { return (int) getFactoryPresets().size(); } int SerumAltAudioProcessor::getCurrentProgram() { return currentProgram.load(); } void SerumAltAudioProcessor::setCurrentProgram (int index) { if (getNumPrograms() > 0) loadFactoryPreset (juce::jlimit (0, getNumPrograms() - 1, index)); } const juce::String SerumAltAudioProcessor::getProgramName (int index) { const auto& presets = getFactoryPresets(); if (index >= 0 && index < (int) presets.size()) return presets[(size_t) index].name; return {}; } void SerumAltAudioProcessor::changeProgramName (int, const juce::String&) { } int SerumAltAudioProcessor::getNumFactoryPresets() const { return (int) getFactoryPresets().size(); } void SerumAltAudioProcessor::loadFactoryPreset (int index) { const auto& presets = getFactoryPresets(); if (index < 0 || index >= (int) presets.size()) return; const juce::ScopedLock lock (engine.getControlLock()); const FactoryPreset& preset = presets[(size_t) index]; const auto* uiScaleParam = parameters.getParameter (ids::uiScale); for (auto* param : getParameters()) if (param != nullptr && param != uiScaleParam) param->setValueNotifyingHost (param->getDefaultValue()); for (const auto& kv : preset.params) if (auto* param = parameters.getParameter (kv.first)) if (param != uiScaleParam && std::isfinite (kv.second)) param->setValueNotifyingHost (juce::jlimit (0.0f, 1.0f, kv.second)); // RAVE should start off for a freshly loaded preset. if (auto* raveParam = parameters.getParameter (ids::rave)) raveParam->setValueNotifyingHost (0.0f); auto& matrix = engine.getMatrix(); matrix.clear(); for (const auto& mod : preset.mods) if (! matrix.addConnection (mod.source, mod.target, mod.depth, mod.bipolar)) continue; auto& macros = engine.getMacros(); macros.clear(); for (const auto& ma : preset.macroAssigns) if (! macros.addAssignment (ma.macro, ma.target, ma.depth)) continue; restoreLfoShapesFromState ({}); currentProgram.store (index); } // --------------------------------------------------------------------------- // RAVE / UI scale // --------------------------------------------------------------------------- void SerumAltAudioProcessor::setRaveEnabled (bool enabled) { const juce::ScopedLock lock (engine.getControlLock()); if (auto* raveParam = parameters.getParameter (ids::rave)) { raveParam->beginChangeGesture(); raveParam->setValueNotifyingHost (enabled ? 1.0f : 0.0f); raveParam->endChangeGesture(); } } bool SerumAltAudioProcessor::isRaveEnabled() const { if (auto* raveParam = parameters.getRawParameterValue (ids::rave)) return raveParam->load() > 0.5f; return false; } int SerumAltAudioProcessor::getUiScaleIndex() const { if (auto* p = parameters.getRawParameterValue (ids::uiScale)) { const float value = p->load(); if (std::isfinite (value)) return (int) std::llround (juce::jlimit (0.0f, 1.0f, value) * 4.0f); } return 1; } void SerumAltAudioProcessor::setUiScaleIndex (int index) { const juce::ScopedLock lock (engine.getControlLock()); index = juce::jlimit (0, 4, index); if (auto* p = parameters.getParameter (ids::uiScale)) p->setValueNotifyingHost ((float) index / 4.0f); } float SerumAltAudioProcessor::getUiScale() const { static constexpr float scales[5] = { 0.75f, 1.0f, 1.25f, 1.5f, 2.0f }; return scales[getUiScaleIndex()]; } // --------------------------------------------------------------------------- // State // --------------------------------------------------------------------------- void SerumAltAudioProcessor::getStateInformation (juce::MemoryBlock& destData) { const juce::ScopedLock lock (engine.getControlLock()); auto state = parameters.copyState(); for (int i = state.getNumChildren(); --i >= 0;) { const auto child = state.getChild (i); if (child.hasType ("MODMATRIX") || child.hasType ("MACROS") || child.hasType ("LFOSHAPES")) state.removeChild (i, nullptr); } state.setProperty ("currentProgram", currentProgram.load(), nullptr); state.appendChild (engine.getMatrix().toValueTree(), nullptr); state.appendChild (engine.getMacros().toValueTree(), nullptr); saveLfoShapesToState (state); std::unique_ptr xml (state.createXml()); if (xml != nullptr) copyXmlToBinary (*xml, destData); } void SerumAltAudioProcessor::setStateInformation (const void* data, int sizeInBytes) { if (data == nullptr || sizeInBytes <= 0) return; std::unique_ptr xml (getXmlFromBinary (data, sizeInBytes)); if (xml == nullptr || ! xml->hasTagName ("SerumAlt")) return; juce::ValueTree state = juce::ValueTree::fromXml (*xml); if (! state.hasType ("SerumAlt")) return; const juce::ScopedLock lock (engine.getControlLock()); // A persisted RAVE toggle is rendered non-destructively, so retain it. parameters.replaceState (state); engine.getMatrix().fromValueTree (state.getChildWithName ("MODMATRIX")); engine.getMacros().fromValueTree (state.getChildWithName ("MACROS")); restoreLfoShapesFromState (state); const double savedProgram = (double) state.getProperty ("currentProgram", 0); currentProgram.store (std::isfinite (savedProgram) ? (int) juce::jlimit (0.0, (double) juce::jmax (0, getNumPrograms() - 1), savedProgram) : 0); } void SerumAltAudioProcessor::saveLfoShapesToState (juce::ValueTree& state) const { const juce::ScopedLock lock (engine.getControlLock()); juce::ValueTree tree ("LFOSHAPES"); for (int i = 0; i < kNumLfos; ++i) { const auto& source = engine.getLfos()[(size_t) i]; const auto& data = source.getShapeData(); juce::ValueTree lfo ("LFO"); lfo.setProperty ("index", i, nullptr); lfo.setProperty ("steps", juce::jlimit (2, LFO::kShapePoints, source.getShapeSteps()), nullptr); juce::Array arr; for (int point = 0; point < juce::jmin (LFO::kShapePoints, (int) data.size()); ++point) { const float value = data[(size_t) point]; arr.add (std::isfinite (value) ? juce::jlimit (-1.0f, 1.0f, value) : 0.0f); } lfo.setProperty ("data", juce::JSON::toString (juce::var (arr), true), nullptr); tree.appendChild (lfo, nullptr); } state.appendChild (tree, nullptr); } void SerumAltAudioProcessor::restoreLfoShapesFromState (const juce::ValueTree& state) { const juce::ScopedLock lock (engine.getControlLock()); std::vector defaultShape ((size_t) LFO::kShapePoints); for (int point = 0; point < LFO::kShapePoints; ++point) defaultShape[(size_t) point] = (point % 2 == 0) ? 1.0f : -1.0f; for (int index = 0; index < kNumLfos; ++index) engine.setLfoShapeData (index, defaultShape, 16); const juce::ValueTree tree = state.getChildWithName ("LFOSHAPES"); if (! tree.isValid()) return; std::array restored {}; for (const auto& lfo : tree) { if (! lfo.hasType ("LFO")) continue; const double savedIndex = (double) lfo.getProperty ("index", -1); if (! std::isfinite (savedIndex) || savedIndex < 0.0 || savedIndex >= kNumLfos || std::floor (savedIndex) != savedIndex) continue; const int index = (int) savedIndex; if (restored[(size_t) index]) continue; const double savedSteps = (double) lfo.getProperty ("steps", 16); const int steps = std::isfinite (savedSteps) ? (int) juce::jlimit (2.0, (double) LFO::kShapePoints, savedSteps) : 16; juce::var shape = lfo.getProperty ("data"); if (shape.isString()) { const auto text = shape.toString(); if (text.length() > 8192) continue; shape = juce::JSON::parse (text); } const auto* arr = shape.getArray(); if (arr == nullptr || arr->isEmpty()) continue; const int numPoints = juce::jmin (LFO::kShapePoints, arr->size()); std::vector data; data.reserve ((size_t) numPoints); for (int point = 0; point < numPoints; ++point) { const auto& savedValue = arr->getReference (point); const double value = (savedValue.isDouble() || savedValue.isInt() || savedValue.isInt64()) ? (double) savedValue : 0.0; data.push_back (std::isfinite (value) ? (float) juce::jlimit (-1.0, 1.0, value) : 0.0f); } engine.setLfoShapeData (index, data, steps); restored[(size_t) index] = true; } } } // namespace serum // =========================================================================== // Plugin entry point (required by the JUCE plugin clients). // =========================================================================== juce::AudioProcessor* JUCE_CALLTYPE createPluginFilter() { return new serum::SerumAltAudioProcessor(); }