monostep/Source/PluginProcessor.cpp

516 lines
17 KiB
C++

#include "PluginProcessor.h"
#ifndef MONOSTEP_HEADLESS
#include "PluginEditor.h"
#endif
static const char* paramId (int index)
{
static const char* ids[] =
{
"osc1Wave", "osc2Wave", "detune", "mix",
"cutoff", "res", "attack", "decay", "sustain", "release", "glide", "master",
"seqLen", "seqRate", "seqRoot", "seqOctave", "seqSwing", "seqGateLen"
};
return ids[index];
}
enum ParamIndex
{
pOsc1Wave, pOsc2Wave, pDetune, pMix,
pCutoff, pRes, pAttack, pDecay, pSustain, pRelease, pGlide, pMaster,
pSeqLen, pSeqRate, pSeqRoot, pSeqOctave, pSeqSwing, pSeqGateLen,
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));
detuneParam = apvts.getRawParameterValue (paramId (pDetune));
mixParam = apvts.getRawParameterValue (paramId (pMix));
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));
setDefaultPattern();
}
juce::AudioProcessorValueTreeState::ParameterLayout MonoStepAudioProcessor::createParameterLayout()
{
juce::AudioProcessorValueTreeState::ParameterLayout layout;
layout.add (std::make_unique<juce::AudioParameterInt> (paramId (pOsc1Wave), "Osc 1 Wave", 0, 3, 2));
layout.add (std::make_unique<juce::AudioParameterInt> (paramId (pOsc2Wave), "Osc 2 Wave", 0, 3, 3));
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 (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));
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;
}
}
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, 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();
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
{
lastStep = -1;
lastGateApplied = false;
playStep.store (-1);
}
wasPlaying = hostPlaying;
lastHostPpq = hostPpq;
// --- sequencer step -----------------------------------------------------
bool seqGate = false;
float seqFreq = 0.0f;
bool seqSlide = 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;
// gate length: the step's note sustains for gateLen of the step
const float ppqWithinStep = ppqInCycle - stepStart;
seqGate = s.gate && (ppqWithinStep < stepLen * gateLen);
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;
}
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();
}
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;
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";
}
seq.setProperty ("gates", gates, nullptr);
seq.setProperty ("notes", notes.dropLastCharacters (1), nullptr);
seq.setProperty ("cents", cents.dropLastCharacters (1), nullptr);
seq.setProperty ("slides", slides, 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();
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';
}
}
void MonoStepAudioProcessor::getStateInformation (juce::MemoryBlock& destData)
{
juce::ValueTree state = apvts.state.createCopy();
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();
}