mirror of
https://codeberg.org/armin/monostep.git
synced 2026-09-01 04:10:46 +02:00
MonoStep: monophonic 2-oscillator step-sequencer synth
This commit is contained in:
commit
e52e5cb161
14 changed files with 2674 additions and 0 deletions
56
Source/dsp/StepSequencer.h
Normal file
56
Source/dsp/StepSequencer.h
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
#pragma once
|
||||
|
||||
#include <array>
|
||||
#include <cmath>
|
||||
|
||||
namespace monostep
|
||||
{
|
||||
|
||||
static constexpr int numSteps = 16;
|
||||
static constexpr int minSemitone = -12;
|
||||
static constexpr int maxSemitone = 12;
|
||||
static constexpr int numRows = maxSemitone - minSemitone + 1;
|
||||
|
||||
struct Step
|
||||
{
|
||||
bool gate = false;
|
||||
int semitone = 0; // -12 .. +12
|
||||
float cents = 0.0f; // -50 .. +50
|
||||
bool slide = false;
|
||||
};
|
||||
|
||||
class StepSequencer
|
||||
{
|
||||
public:
|
||||
Step& step (int i) { return steps[i]; }
|
||||
const Step& step (int i) const { return steps[i]; }
|
||||
|
||||
void clear()
|
||||
{
|
||||
for (auto& s : steps)
|
||||
s = Step{};
|
||||
}
|
||||
|
||||
int stepsPerBeatFromRate (int rateIndex) const
|
||||
{
|
||||
// 0: 1/4, 1: 1/8, 2: 1/8T, 3: 1/16, 4: 1/32
|
||||
switch (rateIndex)
|
||||
{
|
||||
case 0: return 1;
|
||||
case 1: return 2;
|
||||
case 2: return 3;
|
||||
case 4: return 8;
|
||||
default: return 4;
|
||||
}
|
||||
}
|
||||
|
||||
float stepLenPpq (int rateIndex) const
|
||||
{
|
||||
return 1.0f / (float) stepsPerBeatFromRate (rateIndex);
|
||||
}
|
||||
|
||||
private:
|
||||
std::array<Step, numSteps> steps;
|
||||
};
|
||||
|
||||
} // namespace monostep
|
||||
189
Source/dsp/SynthVoice.h
Normal file
189
Source/dsp/SynthVoice.h
Normal file
|
|
@ -0,0 +1,189 @@
|
|||
#pragma once
|
||||
|
||||
#include <JuceHeader.h>
|
||||
#include "WaveTables.h"
|
||||
|
||||
namespace monostep
|
||||
{
|
||||
|
||||
class SynthVoice
|
||||
{
|
||||
public:
|
||||
struct Params
|
||||
{
|
||||
Waveform waveA = Waveform::saw;
|
||||
Waveform waveB = Waveform::square;
|
||||
float detuneRatio = 1.0f;
|
||||
float mix = 0.5f;
|
||||
float cutoff = 7000.0f;
|
||||
float resonance = 0.15f;
|
||||
float attack = 0.005f;
|
||||
float decay = 0.3f;
|
||||
float sustain = 0.7f;
|
||||
float release = 0.4f;
|
||||
float glide = 0.12f;
|
||||
float master = 0.9f;
|
||||
};
|
||||
|
||||
void prepare (double sr)
|
||||
{
|
||||
sampleRate = (float) sr;
|
||||
reset();
|
||||
}
|
||||
|
||||
void reset()
|
||||
{
|
||||
phaseA = phaseB = 0.0f;
|
||||
currentFreq = targetFreq = 440.0f;
|
||||
smoothing = 0.0f;
|
||||
filterLow = filterBand = 0.0f;
|
||||
env = 0.0f;
|
||||
envStage = Stage::idle;
|
||||
gate = false;
|
||||
}
|
||||
|
||||
void setParams (const Params& p) { params = p; }
|
||||
|
||||
bool isActive() const { return gate; }
|
||||
|
||||
void noteOn (float freqHz, bool legato)
|
||||
{
|
||||
targetFreq = freqHz;
|
||||
|
||||
if (gate && legato)
|
||||
{
|
||||
const float time = std::max (params.glide, 0.001f);
|
||||
smoothing = 1.0f - std::exp (-1.0f / (time * sampleRate));
|
||||
}
|
||||
else
|
||||
{
|
||||
currentFreq = targetFreq;
|
||||
smoothing = 0.0f;
|
||||
retriggerEnvelope();
|
||||
}
|
||||
|
||||
gate = true;
|
||||
}
|
||||
|
||||
void noteOff()
|
||||
{
|
||||
if (gate)
|
||||
{
|
||||
gate = false;
|
||||
|
||||
if (envStage != Stage::idle && envStage != Stage::release)
|
||||
envStage = Stage::release;
|
||||
}
|
||||
}
|
||||
|
||||
void render (juce::AudioBuffer<float>& buffer, int numSamples)
|
||||
{
|
||||
auto* out = buffer.getWritePointer (0);
|
||||
|
||||
for (int i = 0; i < numSamples; ++i)
|
||||
out[i] = renderSample();
|
||||
}
|
||||
|
||||
private:
|
||||
enum class Stage { idle, attack, decay, sustain, release };
|
||||
|
||||
void retriggerEnvelope()
|
||||
{
|
||||
envStage = Stage::attack;
|
||||
}
|
||||
|
||||
void updateEnvelope()
|
||||
{
|
||||
const float sr = sampleRate;
|
||||
|
||||
switch (envStage)
|
||||
{
|
||||
case Stage::attack:
|
||||
env += 1.0f / (params.attack * sr);
|
||||
if (env >= 1.0f) { env = 1.0f; envStage = Stage::decay; }
|
||||
break;
|
||||
|
||||
case Stage::decay:
|
||||
env -= (1.0f - params.sustain) / (params.decay * sr);
|
||||
if (env <= params.sustain) { env = params.sustain; envStage = Stage::sustain; }
|
||||
break;
|
||||
|
||||
case Stage::sustain:
|
||||
env = params.sustain;
|
||||
break;
|
||||
|
||||
case Stage::release:
|
||||
env -= 1.0f / (params.release * sr);
|
||||
if (env <= 0.0f) { env = 0.0f; envStage = Stage::idle; }
|
||||
break;
|
||||
|
||||
case Stage::idle:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void updateGlide()
|
||||
{
|
||||
if (smoothing > 0.0f)
|
||||
{
|
||||
currentFreq += (targetFreq - currentFreq) * smoothing;
|
||||
|
||||
if (std::fabs (targetFreq - currentFreq) < 0.01f)
|
||||
{
|
||||
currentFreq = targetFreq;
|
||||
smoothing = 0.0f;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
float processFilter (float input)
|
||||
{
|
||||
const float f = 2.0f * std::sin (juce::MathConstants<float>::pi * params.cutoff / sampleRate);
|
||||
const float q = 1.0f / (1.0f + params.resonance * 9.0f);
|
||||
|
||||
filterLow += f * filterBand;
|
||||
const float high = input - filterLow - q * filterBand;
|
||||
filterBand += f * high;
|
||||
|
||||
return filterLow;
|
||||
}
|
||||
|
||||
float renderSample()
|
||||
{
|
||||
updateEnvelope();
|
||||
updateGlide();
|
||||
|
||||
const float incA = currentFreq / sampleRate;
|
||||
const float incB = (currentFreq * params.detuneRatio) / sampleRate;
|
||||
|
||||
const float a = renderWave (params.waveA, phaseA, incA);
|
||||
const float b = renderWave (params.waveB, phaseB, incB);
|
||||
|
||||
phaseA = frac (phaseA + incA);
|
||||
phaseB = frac (phaseB + incB);
|
||||
|
||||
float out = (1.0f - params.mix) * a + params.mix * b;
|
||||
|
||||
out = processFilter (out);
|
||||
|
||||
return out * env * params.master;
|
||||
}
|
||||
|
||||
float sampleRate = 44100.0f;
|
||||
Params params;
|
||||
|
||||
float phaseA = 0.0f;
|
||||
float phaseB = 0.0f;
|
||||
float currentFreq = 440.0f;
|
||||
float targetFreq = 440.0f;
|
||||
float smoothing = 0.0f;
|
||||
|
||||
float filterLow = 0.0f;
|
||||
float filterBand = 0.0f;
|
||||
|
||||
float env = 0.0f;
|
||||
Stage envStage = Stage::idle;
|
||||
bool gate = false;
|
||||
};
|
||||
|
||||
} // namespace monostep
|
||||
71
Source/dsp/WaveTables.h
Normal file
71
Source/dsp/WaveTables.h
Normal file
|
|
@ -0,0 +1,71 @@
|
|||
#pragma once
|
||||
|
||||
#include <cmath>
|
||||
#include <algorithm>
|
||||
|
||||
namespace monostep
|
||||
{
|
||||
|
||||
enum class Waveform : int
|
||||
{
|
||||
sine = 0,
|
||||
triangle = 1,
|
||||
saw = 2,
|
||||
square = 3
|
||||
};
|
||||
|
||||
inline float frac (float x)
|
||||
{
|
||||
return x - std::floor (x);
|
||||
}
|
||||
|
||||
inline float polyBlep (float t, float dt)
|
||||
{
|
||||
if (t < dt)
|
||||
{
|
||||
t /= dt;
|
||||
return t + t - t * t - 1.0f;
|
||||
}
|
||||
|
||||
if (t > 1.0f - dt)
|
||||
{
|
||||
t = (t - 1.0f) / dt;
|
||||
return t * t + t + t + 1.0f;
|
||||
}
|
||||
|
||||
return 0.0f;
|
||||
}
|
||||
|
||||
inline float renderWave (Waveform w, float phase, float inc)
|
||||
{
|
||||
switch (w)
|
||||
{
|
||||
case Waveform::sine:
|
||||
return std::sin (phase * 6.283185307179586f);
|
||||
|
||||
case Waveform::triangle:
|
||||
{
|
||||
const float f = frac (phase);
|
||||
return 4.0f * std::min (f, 1.0f - f) - 1.0f;
|
||||
}
|
||||
|
||||
case Waveform::saw:
|
||||
{
|
||||
const float f = frac (phase);
|
||||
return (2.0f * f - 1.0f) - polyBlep (f, inc);
|
||||
}
|
||||
|
||||
case Waveform::square:
|
||||
{
|
||||
const float f = frac (phase);
|
||||
float v = (f < 0.5f) ? 1.0f : -1.0f;
|
||||
v += polyBlep (f, inc);
|
||||
v -= polyBlep (frac (f + 0.5f), inc);
|
||||
return v;
|
||||
}
|
||||
}
|
||||
|
||||
return 0.0f;
|
||||
}
|
||||
|
||||
} // namespace monostep
|
||||
Loading…
Add table
Add a link
Reference in a new issue