#include "PluginProcessor.h" #ifndef MONOSTEP_HEADLESS #include "PluginEditor.h" #endif static const char* paramId (int index) { static const char* ids[] = { "osc1Wave", "osc2Wave", "osc1Coarse", "osc2Coarse", "detune", "mix", "cutoff", "res", "attack", "decay", "sustain", "release", "glide", "master", "seqLen", "seqRate", "seqRoot", "seqOctave", "seqSwing", "seqGateLen", "drive", "ringmod", "accent", "fine1" }; return ids[index]; } enum ParamIndex { pOsc1Wave, pOsc2Wave, pOsc1Coarse, pOsc2Coarse, pDetune, pMix, pCutoff, pRes, pAttack, pDecay, pSustain, pRelease, pGlide, pMaster, pSeqLen, pSeqRate, pSeqRoot, pSeqOctave, pSeqSwing, pSeqGateLen, pDrive, pRingMod, pAccent, pFine1, numParams }; MonostepAudioProcessor::MonostepAudioProcessor() : AudioProcessor (BusesProperties() .withOutput ("Output", juce::AudioChannelSet::stereo(), true)) , apvts (*this, nullptr, "PARAMS", createParameterLayout()) { osc1WaveParam = apvts.getRawParameterValue (paramId (pOsc1Wave)); osc2WaveParam = apvts.getRawParameterValue (paramId (pOsc2Wave)); osc1CoarseParam = apvts.getRawParameterValue (paramId (pOsc1Coarse)); osc2CoarseParam = apvts.getRawParameterValue (paramId (pOsc2Coarse)); detuneParam = apvts.getRawParameterValue (paramId (pDetune)); mixParam = apvts.getRawParameterValue (paramId (pMix)); osc1PhaseParam = apvts.getRawParameterValue (paramId (pFine1)); cutoffParam = apvts.getRawParameterValue (paramId (pCutoff)); resParam = apvts.getRawParameterValue (paramId (pRes)); attackParam = apvts.getRawParameterValue (paramId (pAttack)); decayParam = apvts.getRawParameterValue (paramId (pDecay)); sustainParam = apvts.getRawParameterValue (paramId (pSustain)); releaseParam = apvts.getRawParameterValue (paramId (pRelease)); glideParam = apvts.getRawParameterValue (paramId (pGlide)); masterParam = apvts.getRawParameterValue (paramId (pMaster)); seqLenParam = apvts.getRawParameterValue (paramId (pSeqLen)); seqRateParam = apvts.getRawParameterValue (paramId (pSeqRate)); seqRootParam = apvts.getRawParameterValue (paramId (pSeqRoot)); seqOctaveParam = apvts.getRawParameterValue (paramId (pSeqOctave)); seqSwingParam = apvts.getRawParameterValue (paramId (pSeqSwing)); seqGateLenParam = apvts.getRawParameterValue (paramId (pSeqGateLen)); driveParam = apvts.getRawParameterValue (paramId (pDrive)); ringModParam = apvts.getRawParameterValue (paramId (pRingMod)); accentParam = apvts.getRawParameterValue (paramId (pAccent)); setDefaultPattern(); } juce::AudioProcessorValueTreeState::ParameterLayout MonostepAudioProcessor::createParameterLayout() { juce::AudioProcessorValueTreeState::ParameterLayout layout; layout.add (std::make_unique (paramId (pOsc1Wave), "Osc 1 Wave", 0, 7, 2)); layout.add (std::make_unique (paramId (pOsc2Wave), "Osc 2 Wave", 0, 7, 3)); layout.add (std::make_unique (paramId (pOsc1Coarse), "Osc 1 Coarse", -12, 12, 0)); layout.add (std::make_unique (paramId (pOsc2Coarse), "Osc 2 Coarse", -12, 12, 0)); layout.add (std::make_unique (paramId (pDetune), "Osc 2 Detune", juce::NormalisableRange (-100.0f, 100.0f), 0.0f, juce::AudioParameterFloatAttributes().withLabel ("ct"))); layout.add (std::make_unique (paramId (pMix), "Osc Mix", juce::NormalisableRange (0.0f, 1.0f), 0.5f)); layout.add (std::make_unique (paramId (pFine1), "Phase Offset", juce::NormalisableRange (-1.0f, 1.0f), 0.0f, juce::AudioParameterFloatAttributes().withLabel ("±1"))); layout.add (std::make_unique (paramId (pCutoff), "Filter Cutoff", juce::NormalisableRange (40.0f, 16000.0f, 0.01f, 0.3f), 7000.0f, juce::AudioParameterFloatAttributes().withLabel ("Hz"))); layout.add (std::make_unique (paramId (pRes), "Filter Res", juce::NormalisableRange (0.0f, 1.0f), 0.15f)); layout.add (std::make_unique (paramId (pAttack), "Attack", juce::NormalisableRange (0.001f, 2.0f, 0.001f, 0.3f), 0.005f, juce::AudioParameterFloatAttributes().withLabel ("s"))); layout.add (std::make_unique (paramId (pDecay), "Decay", juce::NormalisableRange (0.001f, 3.0f, 0.001f, 0.3f), 0.3f, juce::AudioParameterFloatAttributes().withLabel ("s"))); layout.add (std::make_unique (paramId (pSustain), "Sustain", juce::NormalisableRange (0.0f, 1.0f), 0.7f)); layout.add (std::make_unique (paramId (pRelease), "Release", juce::NormalisableRange (0.01f, 4.0f, 0.001f, 0.3f), 0.4f, juce::AudioParameterFloatAttributes().withLabel ("s"))); layout.add (std::make_unique (paramId (pGlide), "Glide", juce::NormalisableRange (0.0f, 1.0f, 0.001f, 0.5f), 0.12f, juce::AudioParameterFloatAttributes().withLabel ("s"))); layout.add (std::make_unique (paramId (pMaster), "Master", juce::NormalisableRange (0.0f, 1.0f), 0.9f)); layout.add (std::make_unique (paramId (pSeqLen), "Pattern Length", 1, 16, 16)); juce::StringArray rates { "1/4", "1/8", "1/8T", "1/16", "1/32" }; layout.add (std::make_unique (paramId (pSeqRate), "Rate", rates, 3)); layout.add (std::make_unique (paramId (pSeqRoot), "Root", -24, 24, 0)); juce::StringArray octaves { "-2", "-1", "0", "+1", "+2" }; layout.add (std::make_unique (paramId (pSeqOctave), "Octave", octaves, 3)); layout.add (std::make_unique (paramId (pSeqSwing), "Swing", juce::NormalisableRange (0.0f, 0.6f), 0.0f)); layout.add (std::make_unique (paramId (pSeqGateLen), "Gate Length", juce::NormalisableRange (0.05f, 1.0f), 0.8f)); layout.add (std::make_unique (paramId (pDrive), "Distortion Drive", juce::NormalisableRange (0.0f, 1.0f), 0.0f)); layout.add (std::make_unique (paramId (pRingMod), "Ring Mod", juce::NormalisableRange (0.0f, 1.0f), 0.0f)); layout.add (std::make_unique (paramId (pAccent), "Accent", juce::NormalisableRange (0.0f, 1.0f), 0.7f)); return layout; } void MonostepAudioProcessor::setDefaultPattern() { struct Def { int step; bool gate; int semitone; bool slide; }; const Def defs[] = { { 0, true, 0, false }, { 1, true, 0, true }, { 2, true, 3, false }, { 4, true, 5, false }, { 5, true, 3, true }, { 6, true, 0, false }, { 8, true, 7, false }, { 9, true, 5, true }, { 10, true, 3, false }, { 12, true, 0, false }, { 14, true, -2, true }, }; for (const auto& d : defs) { sequencer.step (d.step).gate = d.gate; sequencer.step (d.step).semitone = d.semitone; sequencer.step (d.step).slide = d.slide; } sequencer.step (0).accent = true; sequencer.step (4).accent = true; sequencer.step (8).accent = true; } void MonostepAudioProcessor::applyParamValue (const juce::String& id, float value) { if (auto* param = apvts.getParameter (id)) { param->setValueNotifyingHost (param->getNormalisableRange().convertTo0to1 (value)); apvts.getParameterAsValue (id) = value; } } int MonostepAudioProcessor::getNumPresets() const { return 18; } const char* MonostepAudioProcessor::getPresetName (int index) const { static const char* names[] = { "Wet Tape", "Acid Wash", "Tin Can", "Bent Toy", "Neon Arcade", "Glitch Garden", "Underwater FM", "Robo Funk", "Lo-Fi Noir", "Spectral Sweep", "Broken Radio", "Plasma Organ", "Glass Bells", "Warm Pad", "Pulse Groove", "Steel Pluck", "Sub Rumble", "Tape Bounce" }; return names[juce::jlimit (0, getNumPresets() - 1, index)]; } void MonostepAudioProcessor::applyPreset (int index) { struct Preset { int osc1; int osc2; int coarse; float detune; float mix; float cutoff; float res; float attack; float decay; float sustain; float release; float glide; float gateLen; float swing; int rate; int root; int octave; float drive; float ring; float accent; }; static const Preset presets[] = { // --- gritty / glidy ----------------------------------------------------- { 0, 1, 0, 15.0f, 0.60f, 4200.0f, 0.15f, 0.02f, 0.25f, 0.70f, 0.35f, 0.25f, 0.80f, 0.10f, 2, 0, 3, 0.55f, 0.00f, 0.5f }, // Wet Tape { 2, 0, -12, -8.0f, 0.45f, 1100.0f, 0.65f, 0.003f, 0.40f, 0.10f, 0.08f, 0.12f, 0.90f, 0.00f, 3, 0, 3, 0.85f, 0.00f, 0.4f }, // Acid Wash { 3, 3, 0, 5.0f, 0.50f, 380.0f, 0.30f, 0.005f, 0.10f, 0.40f, 0.10f, 0.20f, 0.95f, 0.05f, 3, 0, 3, 0.60f, 0.00f, 0.3f }, // Tin Can { 0, 3, 7, 25.0f, 0.35f, 2500.0f, 0.20f, 0.04f, 0.20f, 0.30f, 0.60f, 0.45f, 0.85f, 0.25f, 2, 0, 3, 0.40f, 0.00f, 0.5f }, // Bent Toy { 2, 3, 0, 0.0f, 0.55f, 650.0f, 0.45f, 0.002f, 0.15f, 0.20f, 0.05f, 0.10f, 0.90f, 0.10f, 3, 0, 3, 0.95f, 0.60f, 0.3f }, // Broken Radio { 1, 2, 0, 20.0f, 0.50f, 6000.0f, 0.10f, 0.003f, 0.15f, 0.80f, 0.20f, 0.10f, 0.90f, 0.15f, 3, 0, 3, 0.20f, 0.00f, 0.6f }, // Neon Arcade { 0, 1, -12, -35.0f, 0.55f, 1800.0f, 0.40f, 0.10f, 0.35f, 0.20f, 0.80f, 0.50f, 0.70f, 0.20f, 2, 0, 3, 0.00f, 0.00f, 0.5f }, // Glitch Garden { 2, 2, 5, 10.0f, 0.40f, 3200.0f, 0.35f, 0.002f, 0.20f, 0.60f, 0.12f, 0.15f, 0.95f, 0.40f, 3, 0, 3, 0.70f, 0.20f, 0.5f }, // Robo Funk { 1, 1, -7, -15.0f, 0.50f, 2600.0f, 0.20f, 0.03f, 0.30f, 0.35f, 0.60f, 0.60f, 0.80f, 0.05f, 2, 0, 3, 0.10f, 0.00f, 0.4f }, // Lo-Fi Noir { 0, 2, 0, 8.0f, 0.60f, 7000.0f, 0.08f, 0.01f, 0.20f, 0.90f, 0.25f, 0.30f, 0.75f, 0.00f, 3, 0, 3, 0.35f, 0.35f, 0.6f }, // Spectral Sweep { 3, 0, -12, 12.0f, 0.45f, 5000.0f, 0.25f, 0.02f, 0.50f, 0.70f, 0.30f, 0.40f, 0.60f, 0.20f, 2, 0, 3, 0.30f, 0.15f, 0.5f }, // Plasma Organ { 0, 0, 0, 0.0f, 0.50f, 900.0f, 0.50f, 0.05f, 0.30f, 0.50f, 0.50f, 0.35f, 0.85f, 0.30f, 2, 0, 3, 0.25f, 0.55f, 0.3f }, // Underwater FM // --- clean / no glide / no drive --------------------------------------- { 0, 0, 0, 0.0f, 0.50f, 7000.0f, 0.10f, 0.002f, 0.25f, 0.20f, 0.30f, 0.00f, 0.35f, 0.00f, 3, 0, 4, 0.00f, 0.00f, 0.6f }, // Glass Bells { 1, 1, 0, 12.0f, 0.60f, 1200.0f, 0.25f, 0.30f, 1.20f, 0.80f, 1.50f, 0.20f, 1.00f, 0.10f, 2, 0, 2, 0.00f, 0.00f, 0.2f }, // Warm Pad { 2, 2, 0, 5.0f, 0.50f, 2500.0f, 0.15f, 0.002f, 0.20f, 0.30f, 0.12f, 0.00f, 0.50f, 0.30f, 3, 0, 3, 0.00f, 0.00f, 0.5f }, // Pulse Groove { 0, 3, 12, 0.0f, 0.40f, 1500.0f, 0.60f, 0.001f, 0.35f, 0.15f, 0.20f, 0.00f, 0.45f, 0.00f, 3, 0, 4, 0.00f, 0.35f, 0.7f }, // Steel Pluck { 0, 0, 0, -5.0f, 0.40f, 500.0f, 0.20f, 0.01f, 0.40f, 0.90f, 0.30f, 0.00f, 0.90f, 0.00f, 2, 0, 1, 0.00f, 0.00f, 0.3f }, // Sub Rumble { 1, 0, 0, 8.0f, 0.55f, 3000.0f, 0.20f, 0.005f, 0.30f, 0.60f, 0.25f, 0.30f, 0.70f, 0.15f, 2, 0, 3, 0.25f, 0.00f, 0.5f }, // Tape Bounce }; const auto& pr = presets[juce::jlimit (0, getNumPresets() - 1, index)]; applyParamValue ("osc1Wave", (float) pr.osc1); applyParamValue ("osc2Wave", (float) pr.osc2); applyParamValue ("osc2Coarse", (float) pr.coarse); applyParamValue ("detune", pr.detune); applyParamValue ("mix", pr.mix); applyParamValue ("cutoff", pr.cutoff); applyParamValue ("res", pr.res); applyParamValue ("attack", pr.attack); applyParamValue ("decay", pr.decay); applyParamValue ("sustain", pr.sustain); applyParamValue ("release", pr.release); applyParamValue ("glide", pr.glide); applyParamValue ("seqGateLen", pr.gateLen); applyParamValue ("seqSwing", pr.swing); applyParamValue ("seqRate", (float) pr.rate); applyParamValue ("seqRoot", (float) pr.root); applyParamValue ("seqOctave", (float) pr.octave); applyParamValue ("drive", pr.drive); applyParamValue ("ringmod", pr.ring); applyParamValue ("accent", pr.accent); } void MonostepAudioProcessor::randomizePattern() { const int length = juce::roundToInt (seqLenParam->load()); int filled = 0; for (int i = 0; i < monostep::numSteps; ++i) { auto& s = sequencer.step (i); if (i >= length) { s.gate = false; continue; } const float chance = (i == 0) ? 0.9f : 0.45f; s.gate = random.nextFloat() < chance; if (s.gate) { ++filled; s.semitone = random.nextInt (monostep::maxSemitone - monostep::minSemitone + 1) + monostep::minSemitone; s.cents = random.nextFloat() * 100.0f - 50.0f; s.slide = random.nextFloat() < 0.25f; s.accent = random.nextFloat() < 0.3f; } } if (filled == 0) { auto& s = sequencer.step (random.nextInt (length)); s.gate = true; s.semitone = 0; } if (onPatternChanged) onPatternChanged(); } void MonostepAudioProcessor::randomizeOsc() { applyParamValue ("osc1Wave", (float) random.nextInt (4)); applyParamValue ("osc2Wave", (float) random.nextInt (4)); applyParamValue ("osc2Coarse", (float) (random.nextInt (25) - 12)); applyParamValue ("detune", (float) (random.nextInt (101) - 50)); applyParamValue ("mix", 0.2f + random.nextFloat() * 0.6f); } void MonostepAudioProcessor::randomizeFilter() { applyParamValue ("cutoff", 200.0f * std::pow (2.0f, random.nextFloat() * 5.0f)); applyParamValue ("res", random.nextFloat() * 0.8f); } void MonostepAudioProcessor::randomizeEnv() { applyParamValue ("attack", 0.001f * std::pow (500.0f, random.nextFloat())); applyParamValue ("decay", 0.05f * std::pow (30.0f, random.nextFloat())); applyParamValue ("sustain", random.nextFloat()); applyParamValue ("release", 0.05f * std::pow (40.0f, random.nextFloat())); applyParamValue ("glide", random.nextFloat() * 0.5f); } void MonostepAudioProcessor::randomizeSeqSettings() { applyParamValue ("seqGateLen", 0.3f + random.nextFloat() * 0.7f); applyParamValue ("seqSwing", random.nextFloat() * 0.4f); applyParamValue ("seqRate", (float) random.nextInt (5)); applyParamValue ("seqLen", (float) (random.nextInt (13) + 4)); } void MonostepAudioProcessor::randomizeFx() { applyParamValue ("drive", random.nextFloat()); applyParamValue ("ringmod", random.nextFloat()); } void MonostepAudioProcessor::randomizeParams() { randomizeOsc(); randomizeFilter(); randomizeEnv(); randomizeSeqSettings(); randomizeFx(); } void MonostepAudioProcessor::prepareToPlay (double sr, int samplesPerBlock) { sampleRate = sr; voice.prepare (sr); scratch.setSize (1, samplesPerBlock); lastStep = -1; lastGateApplied = false; lastFreqApplied = -1.0f; freePpq = 0.0; wasPlaying = false; lastHostPpq = -1.0; } void MonostepAudioProcessor::releaseResources() { voice.reset(); } void MonostepAudioProcessor::updateVoiceParams() { monostep::SynthVoice::Params p; p.waveA = (monostep::Waveform) (int) osc1WaveParam->load(); p.waveB = (monostep::Waveform) (int) osc2WaveParam->load(); p.detuneRatio = std::pow (2.0f, osc2CoarseParam->load() / 12.0f) * std::pow (2.0f, detuneParam->load() / 1200.0f); p.mix = mixParam->load(); p.cutoff = cutoffParam->load(); p.resonance = resParam->load(); p.attack = attackParam->load(); p.decay = decayParam->load(); p.sustain = sustainParam->load(); p.release = releaseParam->load(); p.glide = glideParam->load(); p.master = masterParam->load(); p.drive = driveParam->load(); p.ringMod = ringModParam->load(); voice.setParams (p); } float MonostepAudioProcessor::midiToFreq (float note) const { return 440.0f * std::pow (2.0f, (note - 69.0f) / 12.0f); } void MonostepAudioProcessor::processBlock (juce::AudioBuffer& buffer, juce::MidiBuffer& midiMessages) { juce::ScopedNoDenormals noDenormals; const int numSamples = buffer.getNumSamples(); const int numChannels = buffer.getNumChannels(); buffer.clear(); if (numSamples == 0) return; updateVoiceParams(); const int rateIdx = juce::roundToInt (seqRateParam->load()); const int root = juce::roundToInt (seqRootParam->load()); const int octave = juce::roundToInt (seqOctaveParam->load()) - 2; const float swing = seqSwingParam->load(); const float stepLen = sequencer.stepLenPpq (rateIdx); const int seqLen = juce::roundToInt (seqLenParam->load()); const float cyclePpq = (float) seqLen * stepLen; // --- MIDI notes (gate/trigger for the sequencer) ------------------------- for (const auto metadata : midiMessages) { const auto msg = metadata.getMessage(); if (msg.isNoteOn()) { midiHeld = true; lastMidiNote = msg.getNoteNumber(); } else if (msg.isNoteOff()) { if (msg.getNoteNumber() == lastMidiNote || ! midiHeld) midiHeld = false; } } // --- transport / clock -------------------------------------------------- bool hostPlaying = false; double hostPpq = 0.0; double bpm = 120.0; if (auto* playHead = getPlayHead()) if (auto pos = playHead->getPosition()) { hostPlaying = pos->getIsPlaying(); if (auto ppq = pos->getPpqPosition()) hostPpq = *ppq; if (auto tempo = pos->getBpm()) bpm = *tempo; } const double ppqPerSample = (bpm / 60.0) / sampleRate; // The sequencer only runs while a MIDI note is held. When the host // transport is running we follow it, otherwise we free-run at the // fallback BPM so the pattern still steps when there is no DAW clock. if (midiHeld) { if (hostPlaying) { if (lastHostPpq >= 0.0 && hostPpq < lastHostPpq - 1.0) { lastStep = -1; lastGateApplied = false; } freePpq = hostPpq; } else { if (wasPlaying) { lastStep = -1; lastGateApplied = false; } freePpq += ppqPerSample * numSamples; } } else { if (lastGateApplied) voice.noteOff(); lastStep = -1; lastGateApplied = false; lastFreqApplied = -1.0f; playStep.store (-1); } wasPlaying = hostPlaying; lastHostPpq = hostPpq; // --- sequencer step ----------------------------------------------------- bool seqGate = false; float seqFreq = 0.0f; bool seqSlide = false; bool seqAccent = false; if (midiHeld) { const float gateLen = seqGateLenParam->load(); float ppqInCycle = (float) std::fmod (freePpq, (double) cyclePpq); if (ppqInCycle < 0.0f) ppqInCycle += cyclePpq; int stepIdx = 0; float stepStart = 0.0f; for (int i = 0; i < seqLen; ++i) { const float start = i * stepLen + ((i % 2) ? swing * stepLen : 0.0f); const float nextStart = (i + 1) * stepLen + (((i + 1) % 2) ? swing * stepLen : 0.0f); if (ppqInCycle >= start && ppqInCycle < nextStart) { stepIdx = i; stepStart = start; break; } } lastStep = stepIdx; playStep.store (stepIdx); const auto& s = sequencer.step (stepIdx); seqSlide = s.slide; seqAccent = s.accent; // A step whose successor slides keeps its gate open until the next // step so the glide into it is seamless; otherwise the gate closes // after gateLen of the step. const int nextIdx = (stepIdx + 1) % seqLen; const bool slideIntoNext = sequencer.step (nextIdx).slide; const float nextStart = (stepIdx + 1) * stepLen + (((stepIdx + 1) % 2) ? swing * stepLen : 0.0f); const float ppqWithinStep = ppqInCycle - stepStart; const float gateClose = slideIntoNext ? (nextStart - stepStart) : stepLen * gateLen; seqGate = s.gate && (ppqWithinStep < gateClose); if (seqGate) { // the held MIDI note transposes the pattern; root/octave shift it further const float note = (float) lastMidiNote + (float) root + octave * 12.0f + s.semitone; seqFreq = midiToFreq (note + s.cents / 100.0f); } } // --- decide voice event --------------------------------------------------- bool wantGate = false; float wantFreq = 0.0f; bool wantLegato = false; if (seqGate) { wantGate = true; wantFreq = seqFreq; wantLegato = seqSlide; } // Accent tracks the current step (not just gate changes), so it holds for // the whole step even when the note repeats without retriggering. voice.setAccentLevel (seqAccent ? 1.0f + accentParam->load() * 4.0f : 1.0f); if (wantGate != lastGateApplied) { if (wantGate) voice.noteOn (wantFreq, wantLegato); else voice.noteOff(); lastGateApplied = wantGate; lastFreqApplied = wantGate ? wantFreq : -1.0f; } else if (wantGate && std::fabs (wantFreq - lastFreqApplied) > 0.01f) { voice.noteOn (wantFreq, wantLegato); lastFreqApplied = wantFreq; } (void) lastStep; // --- render --------------------------------------------------------------- voice.render (scratch, numSamples); for (int ch = 0; ch < numChannels; ++ch) buffer.addFrom (ch, 0, scratch, 0, 0, numSamples); } void MonostepAudioProcessor::setStepGate (int idx, bool gate) { if (idx < 0 || idx >= monostep::numSteps) return; sequencer.step (idx).gate = gate; if (onPatternChanged) onPatternChanged(); } void MonostepAudioProcessor::setStepNote (int idx, int semitone) { if (idx < 0 || idx >= monostep::numSteps) return; sequencer.step (idx).semitone = juce::jlimit (monostep::minSemitone, monostep::maxSemitone, semitone); if (onPatternChanged) onPatternChanged(); } void MonostepAudioProcessor::setStepCents (int idx, float cents) { if (idx < 0 || idx >= monostep::numSteps) return; sequencer.step (idx).cents = juce::jlimit (-50.0f, 50.0f, cents); if (onPatternChanged) onPatternChanged(); } void MonostepAudioProcessor::setStepSlide (int idx, bool slide) { if (idx < 0 || idx >= monostep::numSteps) return; sequencer.step (idx).slide = slide; if (onPatternChanged) onPatternChanged(); } void MonostepAudioProcessor::setStepAccent (int idx, bool accent) { if (idx < 0 || idx >= monostep::numSteps) return; sequencer.step (idx).accent = accent; if (onPatternChanged) onPatternChanged(); } void MonostepAudioProcessor::shiftPattern (int dir) { sequencer.shift (dir); if (onPatternChanged) onPatternChanged(); } juce::String MonostepAudioProcessor::getStepNoteName (int idx) const { const int root = juce::roundToInt (seqRootParam->load()); const int octave = juce::roundToInt (seqOctaveParam->load()) - 2; int note = root + octave * 12 + sequencer.step (idx).semitone; if (lastMidiNote >= 0) note += lastMidiNote; return juce::MidiMessage::getMidiNoteName (note, true, true, 3); } void MonostepAudioProcessor::storeSequence (juce::ValueTree& seq) const { juce::String gates; juce::String notes; juce::String cents; juce::String slides; juce::String accents; for (int i = 0; i < monostep::numSteps; ++i) { const auto& s = sequencer.step (i); gates += s.gate ? "1" : "0"; notes += juce::String (s.semitone) + ","; cents += juce::String (s.cents, 2) + ","; slides += s.slide ? "1" : "0"; accents += s.accent ? "1" : "0"; } seq.setProperty ("gates", gates, nullptr); seq.setProperty ("notes", notes.dropLastCharacters (1), nullptr); seq.setProperty ("cents", cents.dropLastCharacters (1), nullptr); seq.setProperty ("slides", slides, nullptr); seq.setProperty ("accents", accents, nullptr); } void MonostepAudioProcessor::loadSequence (const juce::ValueTree& seq) { const auto splitFloats = [] (const juce::String& text) { juce::StringArray tokens; tokens.addTokens (text, ",", ""); juce::Array result; for (const auto& t : tokens) result.add (t.getFloatValue()); return result; }; const auto gates = seq.getProperty ("gates").toString(); const auto notes = splitFloats (seq.getProperty ("notes").toString()); const auto cents = splitFloats (seq.getProperty ("cents").toString()); const auto slides = seq.getProperty ("slides").toString(); const auto accents = seq.getProperty ("accents").toString(); for (int i = 0; i < monostep::numSteps; ++i) { auto& s = sequencer.step (i); if (i < gates.length()) s.gate = gates[i] == '1'; if (i < notes.size()) s.semitone = juce::roundToInt (notes[i]); if (i < cents.size()) s.cents = cents[i]; if (i < slides.length()) s.slide = slides[i] == '1'; if (i < accents.length()) s.accent = accents[i] == '1'; } } void MonostepAudioProcessor::getStateInformation (juce::MemoryBlock& destData) { juce::ValueTree state = apvts.copyState(); juce::ValueTree seq = state.getOrCreateChildWithName ("SEQ", nullptr); storeSequence (seq); std::unique_ptr xml (state.createXml()); copyXmlToBinary (*xml, destData); } void MonostepAudioProcessor::setStateInformation (const void* data, int sizeInBytes) { std::unique_ptr xml (juce::AudioProcessor::getXmlFromBinary (data, sizeInBytes)); if (xml == nullptr) return; const juce::ValueTree state = juce::ValueTree::fromXml (*xml); apvts.replaceState (state); if (auto seq = state.getChildWithName ("SEQ"); seq.isValid()) loadSequence (seq); if (onPatternChanged) onPatternChanged(); } juce::AudioProcessorEditor* MonostepAudioProcessor::createEditor() { #ifndef MONOSTEP_HEADLESS return new MonostepAudioProcessorEditor (*this); #else return nullptr; #endif } juce::AudioProcessor* JUCE_CALLTYPE createPluginFilter() { return new MonostepAudioProcessor(); }