Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 13 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,10 +1,22 @@
# Changelog
# Changelog

All notable changes to the Spore firmware are documented here.
Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/); this project
uses [Semantic Versioning](https://semver.org/) (`vMAJOR.MINOR.PATCH`).

## [Unreleased]
- **Fix: audio crackle in tempo-synced delay.** When synced to BPM (1/4, 1/8, 1/8., 1/16),
the delay line crackled heavily due to two issues:
1. Incoming MIDI clock tick intervals were measured with 1 ms resolution (`System::GetNow()`),
causing the calculated tempo to oscillate by ±1 BPM on every tick due to integer quantization
and USB jitter. `MidiClock` now tracks ticks with microsecond resolution (`System::GetUs()`)
over a 24-tick (1 quarter note) sliding window.
2. The delay buffer read pointer previously jumped instantaneously on tempo fluctuations without
smoothing. `GlobalFx` now slews the delay time per sample with a one-pole lowpass filter
($\tau \approx 50\text{ ms}$), providing continuous fractional interpolation and tape-delay
smoothness without audio discontinuities.
- **Toolchain: support official Arm GNU Embedded Toolchain** alongside xPack in `scripts/env.*`,
resolving compatibility on Windows systems where Smart App Control blocks unsigned MinGW DLLs.

## [v0.5.1] - 2026-09-07
- **Maintenance release: vendored libDaisy bumped to upstream HEAD** (`c02245d` -> `cc146d5`).
Expand Down
2 changes: 1 addition & 1 deletion scripts/env.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
$envRoot = Split-Path $PSScriptRoot -Parent
$tc = Join-Path $envRoot "toolchain"
if (Test-Path $tc) {
$arm = Get-ChildItem -Path $tc -Directory -Filter "xpack-arm-none-eabi-gcc-*" -ErrorAction SilentlyContinue | Select-Object -First 1
$arm = Get-ChildItem -Path $tc -Directory -Filter "*arm-none-eabi*" -ErrorAction SilentlyContinue | Select-Object -First 1
$bt = Get-ChildItem -Path $tc -Directory -Filter "xpack-windows-build-tools-*" -ErrorAction SilentlyContinue | Select-Object -First 1
if ($arm) { $env:PATH = (Join-Path $arm.FullName "bin") + ";" + $env:PATH }
if ($bt) { $env:PATH = (Join-Path $bt.FullName "bin") + ";" + $env:PATH }
Expand Down
2 changes: 1 addition & 1 deletion scripts/env.sh
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
_envroot="$(cd "$(dirname "${BASH_SOURCE[0]:-$0}")/.." && pwd)"
_tc="$_envroot/toolchain"
if [ -d "$_tc" ]; then
_arm=$(ls -d "$_tc"/xpack-arm-none-eabi-gcc-*/bin 2>/dev/null | head -1)
_arm=$(ls -d "$_tc"/*arm-none-eabi*/bin 2>/dev/null | head -1)
_bt=$(ls -d "$_tc"/xpack-windows-build-tools-*/bin 2>/dev/null | head -1)
[ -n "$_arm" ] && export PATH="$_arm:$PATH"
[ -n "$_bt" ] && export PATH="$_bt:$PATH"
Expand Down
27 changes: 25 additions & 2 deletions src/fx/effects.h
Original file line number Diff line number Diff line change
Expand Up @@ -45,8 +45,13 @@ class GlobalFx {
bpm_ = 120.0f;
sync_ = 0;
lp_l_ = lp_r_ = 0.0f;
target_delay_ = 48000.0f * 0.3f;
cur_delay_ = target_delay_;
delay_slew_coef_ = 1.0f - expf(-1.0f / (0.05f * sample_rate));
s_fx_del_l.Init();
s_fx_del_r.Init();
s_fx_del_l.SetDelay(cur_delay_);
s_fx_del_r.SetDelay(cur_delay_);
reverb_.Init(sample_rate);
reverb_.SetFeedback(0.85f);
reverb_.SetLpFreq(12000.0f);
Expand All @@ -59,6 +64,9 @@ class GlobalFx {
s_fx_del_l.Reset();
s_fx_del_r.Reset();
lp_l_ = lp_r_ = 0.0f;
cur_delay_ = target_delay_;
s_fx_del_l.SetDelay(cur_delay_);
s_fx_del_r.SetDelay(cur_delay_);
}
wet_ramp_ = 0.0f; // fade the newly-engaged effect in from silence (~30 ms)
mode_ = m;
Expand All @@ -72,6 +80,9 @@ class GlobalFx {
s_fx_del_l.Reset();
s_fx_del_r.Reset();
lp_l_ = lp_r_ = 0.0f;
cur_delay_ = target_delay_;
s_fx_del_l.SetDelay(cur_delay_);
s_fx_del_r.SetDelay(cur_delay_);
reverb_.Init(sr_); // clears ReverbSc's internal state/tail
wet_ramp_ = 0.0f;
}
Expand All @@ -96,8 +107,7 @@ class GlobalFx {
}
if (ds < 1.0f) ds = 1.0f;
if (ds > static_cast<float>(kFxDelayMax - 1)) ds = static_cast<float>(kFxDelayMax - 1);
s_fx_del_l.SetDelay(ds);
s_fx_del_r.SetDelay(ds);
target_delay_ = ds;

fb_ = daisysp::fmap(fb, 0.0f, kDelayFbMax, daisysp::Mapping::LINEAR);
float tone_hz =
Expand All @@ -121,6 +131,16 @@ class GlobalFx {
const float wmix = mix_ * wet_ramp_; // ramped wet level avoids the engage transient
float dry_l = l[i], dry_r = r[i];
if (mode_ == DELAY) {
float diff = target_delay_ - cur_delay_;
if (fabsf(diff) > 0.001f) {
cur_delay_ += diff * delay_slew_coef_;
s_fx_del_l.SetDelay(cur_delay_);
s_fx_del_r.SetDelay(cur_delay_);
} else if (cur_delay_ != target_delay_) {
cur_delay_ = target_delay_;
s_fx_del_l.SetDelay(cur_delay_);
s_fx_del_r.SetDelay(cur_delay_);
}
float d_l = s_fx_del_l.Read();
float d_r = s_fx_del_r.Read();
// One-pole lowpass in the feedback path = darker repeats.
Expand Down Expand Up @@ -157,6 +177,9 @@ class GlobalFx {
float bpm_; // clock tempo for synced delay
int sync_; // 0 = free (knob ms), 1..4 = tempo division
Mode mode_;
float target_delay_;
float cur_delay_;
float delay_slew_coef_;
};

} // namespace synthbox
63 changes: 49 additions & 14 deletions src/io/clock.h
Original file line number Diff line number Diff line change
Expand Up @@ -16,38 +16,70 @@ namespace synthbox {

class MidiClock {
public:
static constexpr int kHistoryTicks = 24; // 1 quarter note (24 PPQN)

void Init() {
bpm_ = 120.0f;
est_ = 120.0f;
ext_ = false;
running_ = false;
last_tick_ms_ = 0;
tick_count_ = 0;
head_ = 0;
last_seen_ms_ = 0;
for (int i = 0; i < kHistoryTicks; ++i) {
history_us_[i] = 0;
}
}

// Called on each incoming MIDI timing-clock tick (24 per quarter note).
void Tick(uint32_t now) {
if (last_tick_ms_) {
uint32_t dt = now - last_tick_ms_;
if (dt > 1 && dt < 300) {
float inst = 60000.0f / (dt * 24.0f); // BPM from one tick interval
est_ = est_ * 0.85f + inst * 0.15f; // smooth jitter
// now_us is in microseconds (from daisy::System::GetUs()).
void Tick(uint32_t now_us) {
last_seen_ms_ = daisy::System::GetNow();
ext_ = true;
running_ = true;

if (tick_count_ > 0) {
// Measure time over the sliding window (up to 24 ticks == 1 quarter note).
int span = tick_count_ < kHistoryTicks ? tick_count_ : kHistoryTicks;
int oldest_idx = (head_ - span + kHistoryTicks) % kHistoryTicks;
uint32_t dt_us = now_us - history_us_[oldest_idx];

// 1 beat = 60,000,000 us. span ticks = (span / 24) beats.
// Expected dt_us = (60,000,000 * span) / (BPM * 24).
// Therefore inst BPM = (60,000,000 * span) / (dt_us * 24).
if (dt_us > 1000 && dt_us < 10000000) {
float inst = (60000000.0f * span) / (static_cast<float>(dt_us) * 24.0f);
if (inst >= 20.0f && inst <= 300.0f) {
if (tick_count_ < 6) {
est_ = inst; // lock on fast when playback starts
} else {
est_ = est_ * 0.90f + inst * 0.10f; // smooth any residual jitter
}
}
}
}
last_tick_ms_ = now;
last_seen_ms_ = now;
ext_ = true;

history_us_[head_] = now_us;
head_ = (head_ + 1) % kHistoryTicks;
if (tick_count_ < kHistoryTicks) {
tick_count_++;
}
}
void Start() {
running_ = true;
tick_count_ = 0;
}
void Start() { running_ = true; }
void Continue() { running_ = true; }
void Stop() { running_ = false; }

void SetInternalBpm(float b) { bpm_ = b < 20.0f ? 20.0f : (b > 300.0f ? 300.0f : b); }

// Call at control rate: drop back to internal tempo if clock stops arriving.
void Update(uint32_t now) {
if (ext_ && (now - last_seen_ms_) > 500) ext_ = false;
void Update(uint32_t now_ms) {
if (ext_ && (now_ms - last_seen_ms_) > 500) {
ext_ = false;
tick_count_ = 0;
}
}

float Bpm() const { return ext_ ? est_ : bpm_; }
Expand All @@ -57,7 +89,10 @@ class MidiClock {
private:
float bpm_, est_;
bool ext_, running_;
uint32_t last_tick_ms_, last_seen_ms_;
int tick_count_;
int head_;
uint32_t history_us_[kHistoryTicks];
uint32_t last_seen_ms_;
};

} // namespace synthbox
3 changes: 1 addition & 2 deletions src/io/midi_in.h
Original file line number Diff line number Diff line change
Expand Up @@ -78,10 +78,9 @@ inline bool PumpMidi(daisy::MidiUsbHandler& midi, MidiContext ctx) {
auto msg = midi.PopEvent();
switch (msg.type) {
case daisy::SystemRealTime: { // tempo clock / transport from GUI or device
uint32_t now = daisy::System::GetNow();
switch (msg.srt_type) {
case daisy::TimingClock:
clock.Tick(now);
clock.Tick(daisy::System::GetUs());
break;
case daisy::Start:
clock.Start();
Expand Down
Loading