mirror of
https://codeberg.org/armin/monostep.git
synced 2026-09-01 04:10:46 +02:00
779 lines
28 KiB
C++
779 lines
28 KiB
C++
#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<juce::AudioParameterInt> (paramId (pOsc1Wave), "Osc 1 Wave", 0, monostep::numWaveforms - 1, 2));
|
|
layout.add (std::make_unique<juce::AudioParameterInt> (paramId (pOsc2Wave), "Osc 2 Wave", 0, monostep::numWaveforms - 1, 3));
|
|
|
|
layout.add (std::make_unique<juce::AudioParameterInt> (paramId (pOsc1Coarse), "Osc 1 Coarse", -12, 12, 0));
|
|
layout.add (std::make_unique<juce::AudioParameterInt> (paramId (pOsc2Coarse), "Osc 2 Coarse", -12, 12, 0));
|
|
|
|
layout.add (std::make_unique<juce::AudioParameterFloat> (paramId (pDetune),
|
|
"Osc 2 Detune", juce::NormalisableRange<float> (-100.0f, 100.0f), 0.0f,
|
|
juce::AudioParameterFloatAttributes().withLabel ("ct")));
|
|
|
|
layout.add (std::make_unique<juce::AudioParameterFloat> (paramId (pMix),
|
|
"Osc Mix", juce::NormalisableRange<float> (0.0f, 1.0f), 0.5f));
|
|
|
|
layout.add (std::make_unique<juce::AudioParameterFloat> (paramId (pFine1),
|
|
"Phase Offset", juce::NormalisableRange<float> (-1.0f, 1.0f), 0.0f,
|
|
juce::AudioParameterFloatAttributes().withLabel ("±1")));
|
|
|
|
layout.add (std::make_unique<juce::AudioParameterFloat> (paramId (pCutoff),
|
|
"Filter Cutoff", juce::NormalisableRange<float> (40.0f, 16000.0f, 0.01f, 0.3f), 7000.0f,
|
|
juce::AudioParameterFloatAttributes().withLabel ("Hz")));
|
|
|
|
layout.add (std::make_unique<juce::AudioParameterFloat> (paramId (pRes),
|
|
"Filter Res", juce::NormalisableRange<float> (0.0f, 1.0f), 0.15f));
|
|
|
|
layout.add (std::make_unique<juce::AudioParameterFloat> (paramId (pAttack),
|
|
"Attack", juce::NormalisableRange<float> (0.001f, 2.0f, 0.001f, 0.3f), 0.005f,
|
|
juce::AudioParameterFloatAttributes().withLabel ("s")));
|
|
|
|
layout.add (std::make_unique<juce::AudioParameterFloat> (paramId (pDecay),
|
|
"Decay", juce::NormalisableRange<float> (0.001f, 3.0f, 0.001f, 0.3f), 0.3f,
|
|
juce::AudioParameterFloatAttributes().withLabel ("s")));
|
|
|
|
layout.add (std::make_unique<juce::AudioParameterFloat> (paramId (pSustain),
|
|
"Sustain", juce::NormalisableRange<float> (0.0f, 1.0f), 0.7f));
|
|
|
|
layout.add (std::make_unique<juce::AudioParameterFloat> (paramId (pRelease),
|
|
"Release", juce::NormalisableRange<float> (0.01f, 4.0f, 0.001f, 0.3f), 0.4f,
|
|
juce::AudioParameterFloatAttributes().withLabel ("s")));
|
|
|
|
layout.add (std::make_unique<juce::AudioParameterFloat> (paramId (pGlide),
|
|
"Glide", juce::NormalisableRange<float> (0.0f, 1.0f, 0.001f, 0.5f), 0.12f,
|
|
juce::AudioParameterFloatAttributes().withLabel ("s")));
|
|
|
|
layout.add (std::make_unique<juce::AudioParameterFloat> (paramId (pMaster),
|
|
"Master", juce::NormalisableRange<float> (0.0f, 1.0f), 0.9f));
|
|
|
|
layout.add (std::make_unique<juce::AudioParameterInt> (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<juce::AudioParameterChoice> (paramId (pSeqRate), "Rate", rates, 3));
|
|
|
|
layout.add (std::make_unique<juce::AudioParameterInt> (paramId (pSeqRoot), "Root", -24, 24, 0));
|
|
|
|
juce::StringArray octaves { "-2", "-1", "0", "+1", "+2" };
|
|
layout.add (std::make_unique<juce::AudioParameterChoice> (paramId (pSeqOctave), "Octave", octaves, 3));
|
|
|
|
layout.add (std::make_unique<juce::AudioParameterFloat> (paramId (pSeqSwing),
|
|
"Swing", juce::NormalisableRange<float> (0.0f, 0.6f), 0.0f));
|
|
|
|
layout.add (std::make_unique<juce::AudioParameterFloat> (paramId (pSeqGateLen),
|
|
"Gate Length", juce::NormalisableRange<float> (0.05f, 1.0f), 0.8f));
|
|
|
|
layout.add (std::make_unique<juce::AudioParameterFloat> (paramId (pDrive),
|
|
"Distortion Drive", juce::NormalisableRange<float> (0.0f, 1.0f), 0.0f));
|
|
|
|
layout.add (std::make_unique<juce::AudioParameterFloat> (paramId (pRingMod),
|
|
"Ring Mod", juce::NormalisableRange<float> (0.0f, 1.0f), 0.0f));
|
|
|
|
layout.add (std::make_unique<juce::AudioParameterFloat> (paramId (pAccent),
|
|
"Accent", juce::NormalisableRange<float> (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 (monostep::numWaveforms));
|
|
applyParamValue ("osc2Wave", (float) random.nextInt (monostep::numWaveforms));
|
|
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<float>& 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<float> 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<juce::XmlElement> xml (state.createXml());
|
|
copyXmlToBinary (*xml, destData);
|
|
}
|
|
|
|
void MonostepAudioProcessor::setStateInformation (const void* data, int sizeInBytes)
|
|
{
|
|
std::unique_ptr<juce::XmlElement> 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();
|
|
}
|