Skip to content

Instantly share code, notes, and snippets.

@matpalm
Created May 18, 2026 01:35
Show Gist options
  • Select an option

  • Save matpalm/42a7210b2852dc2ffb8977800ba40357 to your computer and use it in GitHub Desktop.

Select an option

Save matpalm/42a7210b2852dc2ffb8977800ba40357 to your computer and use it in GitHub Desktop.
#include <array>
#include <cstdint>
#include "daisy_patch.h"
#include "daisysp.h"
using namespace daisy;
namespace
{
constexpr size_t kNumVoices = 4;
constexpr float kMaxSeconds = 11.0f;
constexpr float kMinSeconds = 0.020f; // flanger-like minimum at full CCW
DaisyPatch patch;
float g_sample_rate = 48000.0f;
size_t g_max_len_samples = 528000;
float DSY_SDRAM_BSS circ[kNumVoices][530000];
size_t write_pos[kNumVoices] = {0, 0, 0, 0};
size_t eff_len[kNumVoices] = {2048, 2579, 3121, 4001};
struct VoicePlayback
{
bool active = false;
bool reverse = false;
float read_pos = 0.0f;
size_t samples_remaining = 0;
size_t total_samples = 1;
};
VoicePlayback voices[kNumVoices];
float env_amount = 0.0f;
float reverse_probability = 0.5f;
float len_knob = 0.0f;
uint32_t rng_state = 0x13579BDFu;
inline uint32_t Xorshift32()
{
uint32_t x = rng_state;
x ^= x << 13;
x ^= x >> 17;
x ^= x << 5;
rng_state = x;
return x;
}
inline float Rand01()
{
return (float)(Xorshift32() & 0x00FFFFFFu) / 16777215.0f;
}
bool IsPrime(size_t n)
{
if(n < 2)
{
return false;
}
if((n & 1u) == 0u)
{
return n == 2;
}
for(size_t d = 3; d * d <= n; d += 2)
{
if((n % d) == 0)
{
return false;
}
}
return true;
}
size_t NextPrime(size_t n)
{
if(n < 2)
{
return 2;
}
if((n & 1u) == 0u)
{
n += 1;
}
while(!IsPrime(n))
{
n += 2;
}
return n;
}
int WrapIndex(int idx, size_t len)
{
const int l = (int)len;
while(idx < 0)
{
idx += l;
}
while(idx >= l)
{
idx -= l;
}
return idx;
}
size_t ZeroCrossNear(size_t voice, int center, int window)
{
const size_t len = eff_len[voice];
int best = center;
int best_dist = window + 1;
for(int d = 0; d <= window; d++)
{
const int cands[2] = {center - d, center + d};
for(int k = 0; k < 2; k++)
{
const int i0 = WrapIndex(cands[k], len);
const int i1 = WrapIndex(cands[k] + 1, len);
const float a = circ[voice][i0];
const float b = circ[voice][i1];
const bool cross = (a <= 0.0f && b > 0.0f) || (a >= 0.0f && b < 0.0f);
if(cross)
{
if(d < best_dist)
{
best = i0;
best_dist = d;
}
break;
}
}
if(best_dist == 0)
{
break;
}
}
return (size_t)WrapIndex(best, len);
}
void UpdateLengths()
{
// knob3: fully CCW -> short flanger-like times, fully CW -> 11s max.
const float max_samps_f = g_sample_rate * kMaxSeconds;
const float min_samps_f = g_sample_rate * kMinSeconds;
const float longest_target = min_samps_f + (max_samps_f - min_samps_f) * len_knob;
const float ratios[kNumVoices] = {0.53f, 0.67f, 0.83f, 1.00f};
for(size_t v = 0; v < kNumVoices; v++)
{
size_t target = (size_t)(longest_target * ratios[v]);
if(target < 257)
{
target = 257;
}
if(target > g_max_len_samples)
{
target = g_max_len_samples;
}
// Use distinct primes so all lengths are pairwise relatively prime.
size_t prime = NextPrime(target + v * 10);
if(prime > g_max_len_samples)
{
prime = NextPrime(g_max_len_samples - 127);
}
eff_len[v] = prime;
if(write_pos[v] >= eff_len[v])
{
write_pos[v] %= eff_len[v];
}
}
}
void TriggerPlayback()
{
const size_t one_sec = (size_t)g_sample_rate;
for(size_t v = 0; v < kNumVoices; v++)
{
const size_t len = eff_len[v];
const bool reverse = Rand01() < reverse_probability;
// Opposite side of write position in the circular buffer.
int nominal_start = (int)write_pos[v] + (int)(len / 2);
nominal_start = WrapIndex(nominal_start, len);
// Start at nearby zero crossing to reduce click on onset.
const size_t start = ZeroCrossNear(v, nominal_start, 256);
// End near a zero crossing around nominal one-second span to reduce pop.
const int dir = reverse ? -1 : 1;
const int nominal_end = WrapIndex((int)start + dir * (int)one_sec, len);
const size_t aligned_end = ZeroCrossNear(v, nominal_end, 512);
size_t play_len;
if(!reverse)
{
if(aligned_end >= start)
{
play_len = aligned_end - start;
}
else
{
play_len = (len - start) + aligned_end;
}
}
else
{
if(start >= aligned_end)
{
play_len = start - aligned_end;
}
else
{
play_len = start + (len - aligned_end);
}
}
if(play_len < 32)
{
play_len = one_sec;
}
if(play_len > one_sec + 1024)
{
play_len = one_sec + 1024;
}
voices[v].active = true;
voices[v].reverse = reverse;
voices[v].read_pos = (float)start;
voices[v].samples_remaining = play_len;
voices[v].total_samples = play_len;
}
}
float ReadInterp(size_t voice, float pos)
{
const size_t len = eff_len[voice];
int i0 = (int)pos;
while(i0 < 0)
{
i0 += (int)len;
}
while(i0 >= (int)len)
{
i0 -= (int)len;
}
int i1 = i0 + 1;
if(i1 >= (int)len)
{
i1 -= (int)len;
}
const float frac = pos - (float)i0;
return circ[voice][i0] + (circ[voice][i1] - circ[voice][i0]) * frac;
}
inline float EnvelopeGain(const VoicePlayback& vp)
{
if(vp.total_samples <= 1)
{
return 1.0f;
}
const float phase = 1.0f - ((float)vp.samples_remaining / (float)vp.total_samples);
const float tri = 1.0f - fabsf(2.0f * phase - 1.0f);
return (1.0f - env_amount) + env_amount * tri;
}
void ProcessControls()
{
patch.ProcessAnalogControls();
patch.ProcessDigitalControls();
env_amount = patch.GetKnobValue(DaisyPatch::CTRL_1);
// knob2: CW => always forward, CCW => always backward.
reverse_probability = 1.0f - patch.GetKnobValue(DaisyPatch::CTRL_2);
len_knob = patch.GetKnobValue(DaisyPatch::CTRL_3);
UpdateLengths();
}
void DrawKnobLine(int y, const char* label, float value)
{
constexpr int kBarX = 30;
constexpr int kBarW = 90;
constexpr int kBarH = 8;
const int fill_w = (int)(value * (float)kBarW);
patch.display.SetCursor(0, y);
patch.display.WriteString(label, Font_6x8, true);
patch.display.DrawRect(kBarX, y, kBarX + kBarW, y + kBarH, true, false);
if(fill_w > 0)
{
patch.display.DrawRect(kBarX + 1,
y + 1,
kBarX + fill_w - 1,
y + kBarH - 1,
true,
true);
}
}
void UpdateDisplay()
{
patch.display.Fill(false);
const float k1 = patch.GetKnobValue(DaisyPatch::CTRL_1);
const float k2 = patch.GetKnobValue(DaisyPatch::CTRL_2);
const float k3 = patch.GetKnobValue(DaisyPatch::CTRL_3);
DrawKnobLine(0, "K1", k1);
DrawKnobLine(10, "K2", k2);
DrawKnobLine(20, "K3", k3);
FixedCapStr<32> line;
line.Append("1:");
line.AppendInt((int)(k1 * 100.0f));
line.Append(" 2:");
line.AppendInt((int)(k2 * 100.0f));
line.Append(" 3:");
line.AppendInt((int)(k3 * 100.0f));
patch.display.SetCursor(0, 38);
patch.display.WriteString(line, Font_6x8, true);
patch.display.Update();
}
}
static void AudioCallback(AudioHandle::InputBuffer in,
AudioHandle::OutputBuffer out,
size_t size)
{
ProcessControls();
if(patch.gate_input[0].Trig())
{
TriggerPlayback();
}
for(size_t i = 0; i < size; i++)
{
// Record input channel IN1 (index 1) into 4 circular buffers.
const float x = in[1][i];
for(size_t v = 0; v < kNumVoices; v++)
{
circ[v][write_pos[v]] = x;
write_pos[v]++;
if(write_pos[v] >= eff_len[v])
{
write_pos[v] = 0;
}
}
// Playback each buffer on its corresponding output.
for(size_t v = 0; v < kNumVoices; v++)
{
float y = 0.0f;
if(voices[v].active)
{
const float g = EnvelopeGain(voices[v]);
y = ReadInterp(v, voices[v].read_pos) * g;
const float step = voices[v].reverse ? -1.0f : 1.0f;
voices[v].read_pos += step;
if(voices[v].read_pos < 0.0f)
{
voices[v].read_pos += (float)eff_len[v];
}
else if(voices[v].read_pos >= (float)eff_len[v])
{
voices[v].read_pos -= (float)eff_len[v];
}
if(voices[v].samples_remaining > 0)
{
voices[v].samples_remaining--;
}
if(voices[v].samples_remaining == 0)
{
voices[v].active = false;
}
}
out[v][i] = y;
}
}
}
int main(void)
{
patch.Init();
g_sample_rate = patch.AudioSampleRate();
g_max_len_samples = (size_t)(g_sample_rate * kMaxSeconds);
if(g_max_len_samples > 529999)
{
g_max_len_samples = 529999;
}
for(size_t v = 0; v < kNumVoices; v++)
{
for(size_t i = 0; i < 530000; i++)
{
circ[v][i] = 0.0f;
}
}
patch.StartAdc();
patch.StartAudio(AudioCallback);
while(1)
{
patch.ProcessAllControls();
UpdateDisplay();
patch.DelayMs(10);
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment