Seedbox v0.1 + firmware, generic kit/synth, ROOT/SCALE pages
Some checks failed
check / check (push) Has been cancelled

First commit of the repo. Contents beyond the v0.1 simulator and layout:

- Full Pico 2 W firmware (firmware/app, drivers, engine port, host test).
- BANK page and 404 mode A/B removed; learned pads play exactly what was learned.
- 404/MiniFreak renamed to KIT/SYNTH throughout (jacks, code, sim, docs,
  layout labels; wiring rebuilt).
- ROLL returns to page select after a re-roll; a freshly learned track turns on.
- LCD contrast 66.
- ROOT and SCALE pages: 21 scales, AUTO = the seed picks; global and sticky.

Intended music change: the scale table grew from 8 to 21 and AUTO picks from
all of them, so every seed's scale changed. Vectors regenerated with
npm run vectors, plus new key-override cases. Flash record is now version 2.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-11 20:39:36 +03:00
commit 2196633ed5
77 changed files with 52207 additions and 0 deletions

45
firmware/app/app.h Normal file
View File

@@ -0,0 +1,45 @@
/* Seedbox firmware: state shared between core 0 (UI) and core 1 (sequencer). Mirrors sim/app.js. */
#ifndef SEEDBOX_APP_H
#define SEEDBOX_APP_H
#include <stdint.h>
#include "pico/critical_section.h"
#include "pico/util/queue.h"
#include "engine.h"
#define NKIT 7 /* kit tracks are 0..6 (lane 0); LEARN index = track index */
#define NSLOTS 8
typedef struct { uint8_t used, ch, note; } pad_t; /* learned pad: ch 0..15 */
/* Everything the sequencer reads. Core 0 writes it under g_live_lock; core 1 copies it under the same lock. */
typedef struct {
sb_state state;
int gen, energy, drift, bpm;
int scale, root; /* ROOT / SCALE pages: -1 = AUTO (the seed picks) */
uint8_t on[SB_NT], inv[SB_NT]; /* inv: flipped while the button is held */
pad_t learn[NKIT];
int mono_ch; /* synth channel 1..16 */
int mono_oct; /* -3..3 */
int clock; /* send clock + start/stop */
} live_t;
extern live_t g_live;
extern critical_section_t g_live_lock;
/* core 0 -> core 1 */
enum { CMD_START, CMD_STOP, CMD_RELEASE_TRACK, CMD_BPM };
typedef struct { uint8_t cmd; int16_t arg; } cmd_t;
extern queue_t g_cmdq;
/* core 1 -> core 0, for the screen */
extern volatile int g_running;
extern volatile int g_cur_bar; /* bar being heard, -1 if none */
extern volatile uint32_t g_hit_until_ms[SB_NT];
void seq_core1_main(void);
/* app.js learned(): the channel and note a kit track was learned with. Returns 0 if not learned. */
int learned(const live_t *L, int tr, int *ch, int *note);
#endif

260
firmware/app/main.c Normal file
View File

@@ -0,0 +1,260 @@
/* Core 0: panel, pages, LEARN, screen, storage. Port of the UI half of sim/app.js; the sequencer is on core 1.
Hardware differences from the simulator: ENERGY/DRIFT are real pots, RUN is a toggle (its position is the state),
and the bench settings (synth channel/octave, clock) keep the simulator's defaults. */
#include <stdio.h>
#include <string.h>
#include "pico/stdlib.h"
#include "pico/multicore.h"
#include "pico/rand.h"
#include "app.h"
#include "storage.h"
#include "midi_uart.h"
#include "pcd8544.h"
#include "inputs.h"
#include "encoder.h"
#include "pots.h"
live_t g_live;
critical_section_t g_live_lock;
queue_t g_cmdq;
volatile int g_running;
volatile int g_cur_bar = -1;
volatile uint32_t g_hit_until_ms[SB_NT];
#define HOLD_MS 250
#define LONG_PUSH_MS 600
#define SAVE_AFTER_MS 1500
static const char *const TRACK_NAME[SB_NT] = { "KICK","KICK 2","SNARE","GHOST","HAT","HAT 16","PERC","BASS","MEL A","MEL B","PAD" };
static const char *const NOTE_NAME[12] = { "C","C#","D","D#","E","F","F#","G","G#","A","A#","B" };
static const char *const GEN_NAME[SB_NGEN] = { "EUCLID","SKELETON","SUBDIV","AUTOMATA","AMENIZER" };
static const int8_t PANEL[12] = { 0, 1, 2, -1, 4, 5, 6, 3, 7, 8, 9, 10 }; /* screen map = button grid; -1 = RUN */
enum { PG_SEED, PG_GEN, PG_ROOT, PG_SCALE, PG_BPM, PG_SLOT, PG_ROLL, PG_LEARN, NPAGES };
static slot_t slots[NSLOTS];
static sb_state history[16];
static int nhist;
static int page, editing, hist_sel, slot_sel, roll_sel, learn_mode, learn_tr = -1;
static struct { int active, longp; uint32_t t0; } held[SB_NT];
static char msg_text[24];
static uint32_t msg_until, now_ms;
static int dirty;
static uint32_t dirty_ms;
static void lock(void){ critical_section_enter_blocking(&g_live_lock); }
static void unlock(void){ critical_section_exit(&g_live_lock); }
static void send_cmd(int c, int arg){ cmd_t m = { (uint8_t)c, (int16_t)arg }; queue_add_blocking(&g_cmdq, &m); }
static void msg(const char *t, uint32_t ms){ snprintf(msg_text, sizeof msg_text, "%s", t); msg_until = now_ms + ms; }
static void mark_dirty(void){ dirty = 1; dirty_ms = now_ms; }
static int clampi(int x, int a, int b){ return x < a ? a : x > b ? b : x; }
static int sounds(int tr){ return g_live.on[tr] ^ g_live.inv[tr]; }
static void set_state(sb_state s){
memmove(&history[1], &history[0], sizeof history[0] * 15);
history[0] = g_live.state; if(nhist < 16) nhist++;
s.salts[0] &= 0xFFFF; s.salts[1] &= 0xFFFF;
lock(); g_live.state = s; unlock();
hist_sel = 0; mark_dirty();
}
static void new_seed(void){ sb_state s = { get_rand_32(), {0, 0} }; set_state(s); }
static void reroll(int l){ sb_state s = g_live.state; s.salts[l] = (s.salts[l] + 1) & 0xFFFF; set_state(s); msg(l ? "NEW LINE" : "NEW KIT", 700); }
static void apply_on(int tr, int v){ lock(); g_live.on[tr] = (uint8_t)v; unlock(); mark_dirty(); if(!sounds(tr)) send_cmd(CMD_RELEASE_TRACK, tr); }
static void set_inv(int tr, int v){ lock(); g_live.inv[tr] = (uint8_t)v; unlock(); if(!sounds(tr)) send_cmd(CMD_RELEASE_TRACK, tr); }
static void set_bpm(int b){ b = clampi(b, 60, 240); if(b == g_live.bpm) return; lock(); g_live.bpm = b; unlock(); send_cmd(CMD_BPM, b); mark_dirty(); }
static void enc_turn(int d){
if(!editing){ page = (page + d + NPAGES) % NPAGES; learn_mode = 0; learn_tr = -1; return; }
switch(page){
case PG_SEED: hist_sel = clampi(hist_sel + d, 0, nhist); break;
case PG_GEN: lock(); g_live.gen = (g_live.gen + d + SB_NGEN) % SB_NGEN; unlock(); mark_dirty(); break;
case PG_ROOT: lock(); g_live.root = (g_live.root + 1 + d + 13) % 13 - 1; unlock(); mark_dirty(); break; /* AUTO, C .. B */
case PG_SCALE: lock(); g_live.scale = (g_live.scale + 1 + d + SB_NSCALES + 1) % (SB_NSCALES + 1) - 1; unlock(); mark_dirty(); break; /* AUTO, MAJOR .. LOCRIAN */
case PG_BPM: set_bpm(g_live.bpm + d); break;
case PG_SLOT: slot_sel = (slot_sel + d + NSLOTS) % NSLOTS; break;
case PG_ROLL: roll_sel = (roll_sel + d + 2) % 2; break;
}
}
static void enc_push(int hold){
if(page == PG_LEARN){
if(learn_mode == 0){ learn_mode = 1; editing = 1; }
else { learn_mode = 0; learn_tr = -1; editing = 0; msg("CANCELLED", 700); }
return;
}
if(!editing){ editing = 1; return; }
if(page == PG_SEED && hist_sel > 0) set_state(history[hist_sel - 1]);
if(page == PG_SLOT){
if(hold){
char t[24];
slots[slot_sel].used = 1; slots[slot_sel].seed = g_live.state.seed;
slots[slot_sel].salts[0] = g_live.state.salts[0]; slots[slot_sel].salts[1] = g_live.state.salts[1];
slots[slot_sel].gen = (uint8_t)g_live.gen; mark_dirty();
snprintf(t, sizeof t, "SAVED S%d", slot_sel + 1); msg(t, 900);
return;
}
if(slots[slot_sel].used){
sb_state s = { slots[slot_sel].seed, { slots[slot_sel].salts[0], slots[slot_sel].salts[1] } };
lock(); g_live.gen = slots[slot_sel].gen; unlock();
set_state(s);
}
}
if(page == PG_ROLL) reroll(roll_sel); /* then back to page select */
editing = 0;
}
static void track_down(int t){
if(learn_mode == 1 || learn_mode == 2){
if(t >= NKIT){ msg("KIT ONLY", 700); return; }
learn_tr = t; learn_mode = 2; return;
}
held[t].active = 1; held[t].longp = 0; held[t].t0 = now_ms;
}
static void track_up(int t){
if(!held[t].active) return;
held[t].active = 0;
if(held[t].longp) set_inv(t, 0);
else apply_on(t, !g_live.on[t]);
}
/* MIDI IN: only note-ons matter (LEARN). Handles running status, skips realtime and system messages. */
static void midi_in_byte(int b){
static int status, nd, d[2];
if(b >= 0xF8) return;
if(b & 0x80){ status = b < 0xF0 ? b : 0; nd = 0; return; }
if(!status) return;
d[nd++] = b;
if(nd < (((status & 0xF0) == 0xC0 || (status & 0xF0) == 0xD0) ? 1 : 2)) return;
nd = 0;
if((status & 0xF0) == 0x90 && d[1] > 0 && learn_mode == 2 && learn_tr >= 0){
char t[24];
lock();
g_live.learn[learn_tr].used = 1; g_live.learn[learn_tr].ch = (uint8_t)(status & 15);
g_live.learn[learn_tr].note = (uint8_t)d[0];
g_live.on[learn_tr] = 1; /* a freshly learned track starts on */
unlock();
mark_dirty();
snprintf(t, sizeof t, "N%d CH%d OK", d[0], (status & 15) + 1); msg(t, 1400);
learn_mode = 0; learn_tr = -1; editing = 0;
}
}
/* ---------- screen: the Nokia branch of drawOled() in app.js ---------- */
static void hex8(char *out, uint32_t v){ snprintf(out, 9, "%08lX", (unsigned long)v); }
static void screen_text(char *label, char *value){
char l[24] = "", v[24] = "";
switch(page){
case PG_SEED: {
const sb_state *s = hist_sel > 0 ? &history[hist_sel - 1] : &g_live.state;
char n[16] = "";
if(hist_sel > 0) snprintf(n, sizeof n, " -%d", hist_sel);
snprintf(l, sizeof l, "SEED%s%s", n, (s->salts[0] || s->salts[1]) ? " *" : "");
hex8(v, s->seed); break; }
case PG_GEN: strcpy(l, "GENERATOR"); strcpy(v, GEN_NAME[g_live.gen]); break;
case PG_ROOT: case PG_SCALE: { /* on AUTO, show the seed's own pick */
sb_genome g; int sc = page == PG_SCALE, x = sc ? g_live.scale : g_live.root;
if(x < 0){ sb_genome_make(g_live.state.seed, &g); x = sc ? g.scale : g.root; }
snprintf(l, sizeof l, "%s%s", sc ? "SCALE" : "ROOT", (sc ? g_live.scale : g_live.root) < 0 ? " AUTO" : "");
strcpy(v, sc ? sb_scale_name(x) : NOTE_NAME[x]); break; }
case PG_BPM: strcpy(l, "TEMPO"); snprintf(v, sizeof v, "%d BPM", g_live.bpm); break;
case PG_SLOT: snprintf(l, sizeof l, "SLOT %d", slot_sel + 1); if(slots[slot_sel].used) hex8(v, slots[slot_sel].seed); else strcpy(v, "EMPTY"); break;
case PG_ROLL: strcpy(l, "RE-ROLL"); strcpy(v, roll_sel ? "LINE" : "KIT"); break;
case PG_LEARN: {
if(learn_mode == 2){ const char *n = TRACK_NAME[learn_tr]; char *o; strcpy(l, "LEARN "); o = l + 6; for(; *n; n++) if(*n != ' ') *o++ = *n; *o = 0; }
else strcpy(l, "LEARN");
strcpy(v, learn_mode == 0 ? "PUSH" : learn_mode == 1 ? "PICK TRACK" : "HIT PAD"); break; }
}
if(now_ms < msg_until) snprintf(v, sizeof v, "%s", msg_text);
l[10] = 0; v[10] = 0;
strcpy(label, l); strcpy(value, v);
}
static void draw_cell(int x, int y, int w, int h, int state){ /* 0 off, 1 on, 2 on+hit, 3 selectable (dot), 4 selected */
if(state == 1 || state == 2 || state == 4) lcd_fill(x, y, w, h, 1); else lcd_box(x, y, w, h);
if(state == 2) lcd_fill(x + 3, y + 3, w - 6, h - 6, 0);
if(state == 3) lcd_fill(x + (w >> 1) - 1, y + (h >> 1) - 1, 2, 2, 1);
}
static int cell_state(int t, int blink){
if(learn_mode > 0) return t >= NKIT ? 0 : (learn_mode == 2 && learn_tr == t) ? (blink ? 4 : 3) : 3;
if(t < NKIT && !g_live.learn[t].used) return 3; /* a dot: this kit track still needs learning */
return sounds(t) ? (now_ms < g_hit_until_ms[t] ? 2 : 1) : 0;
}
static void draw_screen(void){
char label[24], value[24];
int blink = (now_ms / 250) % 2 == 0, cur = g_cur_bar, i, k;
screen_text(label, value);
lcd_clear();
lcd_text(0, 0, label);
if(g_running && cur >= 0) for(i = 0; i < 4; i++){ int nx = 64 + i * 5; if(i == cur % 4) lcd_fill(nx, 1, 4, 5, 1); else lcd_box(nx, 1, 4, 5); }
if(editing){ lcd_fill(0, 8, 84, 10, 1); lcd_text_v(1, 9, value, 0); } else lcd_text(1, 9, value);
for(k = 0; k < 12; k++){
int t = PANEL[k], x = 1 + (k % 4) * 21, y = 20 + (k / 4) * 10;
draw_cell(x, y, 19, 8, t < 0 ? (g_running ? 1 : 0) : cell_state(t, blink));
}
lcd_flush();
}
int main(void){
uint32_t prev_in = 0, push_t0 = 0, ms = 0;
int32_t prev_enc;
int prev_pot[2] = {-1, -1}, i;
absolute_time_t next;
stdio_init_all(); /* USB: keeps the reset interface for picotool reboot -f -u */
critical_section_init(&g_live_lock);
queue_init(&g_cmdq, sizeof(cmd_t), 32);
storage_load(&g_live, slots);
midi_init();
inputs_init();
encoder_init();
pots_init();
lcd_init(LCD_VOP_DEFAULT);
g_live.energy = pot_value(POT_ENERGY); g_live.drift = pot_value(POT_DRIFT);
multicore_launch_core1(seq_core1_main);
prev_enc = encoder_count();
next = get_absolute_time();
for(;;){
uint32_t in, ch;
int32_t enc;
int b;
next = delayed_by_us(next, 1000);
sleep_until(next);
now_ms = to_ms_since_boot(get_absolute_time());
ms++;
/* buttons and switches */
in = inputs_scan(); ch = in ^ prev_in; prev_in = in;
for(i = 0; i < IN_MATRIX; i++) if(ch & (1u << i)){
int t = INPUT_KEY_TRACK[i];
if(t < 0) continue;
if(in & (1u << i)) track_down(t); else track_up(t);
}
if((ch >> IN_SEED) & 1 && !((in >> IN_SEED) & 1)) new_seed(); /* on release, like the simulator */
if((ch >> IN_RUN) & 1) send_cmd((in >> IN_RUN) & 1 ? CMD_START : CMD_STOP, 0);
if((ch >> IN_PUSH) & 1){
if((in >> IN_PUSH) & 1) push_t0 = now_ms;
else enc_push(now_ms - push_t0 > LONG_PUSH_MS);
}
for(i = 0; i < SB_NT; i++) if(held[i].active && !held[i].longp && now_ms - held[i].t0 >= HOLD_MS){ held[i].longp = 1; set_inv(i, 1); }
/* encoder */
enc = encoder_count();
while(enc != prev_enc){ int d = enc > prev_enc ? 1 : -1; prev_enc += d; enc_turn(d); }
/* MIDI IN */
while((b = midi_in_read()) >= 0) midi_in_byte(b);
/* pots */
if(ms % 10 == 0){
pots_update();
if(pot_value(POT_ENERGY) != prev_pot[0] || pot_value(POT_DRIFT) != prev_pot[1]){
prev_pot[0] = pot_value(POT_ENERGY); prev_pot[1] = pot_value(POT_DRIFT);
lock(); g_live.energy = prev_pot[0]; g_live.drift = prev_pot[1]; unlock();
}
}
/* storage: only while stopped (a flash erase pauses both cores) */
if(dirty && !g_running && now_ms - dirty_ms > SAVE_AFTER_MS){ live_t L; lock(); L = g_live; unlock(); storage_save(&L, slots); dirty = 0; }
if(ms % 40 == 0) draw_screen();
}
}

170
firmware/app/sequencer.c Normal file
View File

@@ -0,0 +1,170 @@
/* Core 1: owns time. Renders a bar ahead with the engine, sends notes and clock when they're due.
Port of the scheduler in sim/app.js (start, stop, pump, track gating, setBpm). */
#include "app.h"
#include "midi_uart.h"
#include "pico/stdlib.h"
#include "pico/flash.h"
#include <string.h>
#define LOOK_US 30000 /* render the next bar this far ahead */
#define START_US 5000 /* gap between Start (0xFA) and the first tick */
#define QMAX 512
#define SMAX 64
typedef struct { int64_t tick; uint8_t on, tr, lane, v, vel; int16_t len; } qev_t;
typedef struct { uint8_t port, ch, note, tr; int64_t until; } sound_t;
static qev_t q[QMAX];
static int qn;
static sound_t snd[SMAX];
static int sn;
static sb_bar rbar;
static int running, bpm, bar;
static int64_t anchor_tick, next_bar_tick, next_clock;
static uint64_t anchor_us;
static struct { int bar; int64_t start, len; } played[4]; /* recently rendered bars, for g_cur_bar */
static int clampi(int x, int a, int b){ return x < a ? a : x > b ? b : x; }
int learned(const live_t *L, int tr, int *ch, int *note){
if(tr < 0 || tr >= NKIT || !L->learn[tr].used) return 0;
*ch = L->learn[tr].ch & 15; *note = L->learn[tr].note & 127; return 1;
}
static void snapshot(live_t *out){ critical_section_enter_blocking(&g_live_lock); *out = g_live; critical_section_exit(&g_live_lock); }
/* tick <-> time at the current tempo: 60e6 / (bpm * 840) µs per tick */
static uint64_t time_of(int64_t t){ return anchor_us + (uint64_t)(((t - anchor_tick) * 60000000LL) / ((int64_t)bpm * SB_TPB)); }
static int64_t tick_at(uint64_t us){ return anchor_tick + ((int64_t)(us - anchor_us) * bpm * SB_TPB) / 60000000LL; }
static void send3(int port, int a, int b, int c){ uint8_t m[3] = {(uint8_t)a, (uint8_t)b, (uint8_t)c}; midi_send(port, m, 3); }
static void send1_both(int a){ uint8_t m = (uint8_t)a; midi_send(MIDI_KIT, &m, 1); midi_send(MIDI_SYNTH, &m, 1); }
/* queue sorted by (tick, on); a new event goes after equal keys, like a stable sort of push order */
static void enqueue(int64_t tick, int on, const sb_event *e){
int i = qn;
if(qn >= QMAX) return;
while(i > 0 && (q[i - 1].tick > tick || (q[i - 1].tick == tick && q[i - 1].on > on))) i--;
memmove(&q[i + 1], &q[i], (size_t)(qn - i) * sizeof q[0]);
q[i].tick = tick; q[i].on = (uint8_t)on; q[i].tr = e->tr; q[i].lane = e->lane; q[i].v = e->v; q[i].vel = e->vel; q[i].len = e->len;
qn++;
}
static int find_sound(int port, int ch, int note){ int i; for(i = 0; i < sn; i++) if(snd[i].port == port && snd[i].ch == ch && snd[i].note == note) return i; return -1; }
static void drop_sound(int i){ snd[i] = snd[--sn]; }
/* chFor / noteFor: returns 0 when a kit track isn't learned (note === null) */
static int resolve(const live_t *L, const qev_t *e, int *ch, int *note){
if(e->lane == 1){ *ch = clampi(L->mono_ch, 1, 16) - 1; *note = clampi(e->v + 12 * L->mono_oct, 0, 127); return 1; }
return learned(L, e->tr, ch, note);
}
static void dispatch(const qev_t *e){
live_t L;
int ch, note, port = e->lane ? MIDI_SYNTH : MIDI_KIT, i;
snapshot(&L);
if(!resolve(&L, e, &ch, &note)) return;
if(e->on){
if(!(L.on[e->tr] ^ L.inv[e->tr])) return;
send3(port, 0x90 | ch, note, e->vel);
i = find_sound(port, ch, note);
if(i < 0 && sn < SMAX){ i = sn++; snd[i].port = (uint8_t)port; snd[i].ch = (uint8_t)ch; snd[i].note = (uint8_t)note; }
if(i >= 0){ snd[i].tr = e->tr; snd[i].until = e->tick + e->len; }
g_hit_until_ms[e->tr] = to_ms_since_boot(get_absolute_time()) + 80;
} else {
i = find_sound(port, ch, note);
if(i >= 0){ send3(port, 0x80 | ch, note, 0); if(snd[i].until <= e->tick) drop_sound(i); }
}
}
static void release_where(int track){ /* track < 0: any */
int i = 0;
while(i < sn){
int match = track < 0 || snd[i].tr == track;
if(match){ send3(snd[i].port, 0x80 | snd[i].ch, snd[i].note, 0); drop_sound(i); }
else i++;
}
}
static void start(void){
live_t L;
if(running) return;
snapshot(&L);
bpm = L.bpm;
anchor_us = time_us_64() + START_US; anchor_tick = 0; next_bar_tick = 0; bar = 0; next_clock = 0;
qn = 0; sn = 0; memset(played, 0xFF, sizeof played);
if(L.clock) send1_both(0xFA);
running = 1; g_running = 1;
}
static void stop(void){
live_t L;
int t, i, ch, note, used[16] = {0};
if(!running) return;
running = 0;
snapshot(&L);
release_where(-1);
for(t = 0; t < NKIT; t++) if(learned(&L, t, &ch, &note)) used[ch] = 1;
for(i = 0; i < 16; i++) if(used[i]) send3(MIDI_KIT, 0xB0 | i, 120, 0); /* All Sound Off on every kit channel */
i = clampi(L.mono_ch, 1, 16) - 1;
send3(MIDI_SYNTH, 0xB0 | i, 120, 0); send3(MIDI_SYNTH, 0xB0 | i, 123, 0);
if(L.clock) send1_both(0xFC);
qn = 0; sn = 0;
g_running = 0; g_cur_bar = -1;
}
static void set_bpm(int b){
if(running){ uint64_t now = time_us_64(); int64_t tk = tick_at(now); anchor_us = now; anchor_tick = tk; }
bpm = b;
}
static void render_ahead(uint64_t now){
while(time_of(next_bar_tick) < now + LOOK_US){
live_t L;
sb_params p;
int i;
snapshot(&L);
p.gen = L.gen; p.energy = L.energy; p.drift = L.drift; p.fill = 0; p.scale = L.scale; p.root = L.root; /* no FILL button: bar-4 fills happen on their own */
sb_render_bar(&L.state, &p, bar, &rbar);
for(i = 0; i < rbar.n; i++){
enqueue(next_bar_tick + rbar.ev[i].t, 1, &rbar.ev[i]);
enqueue(next_bar_tick + rbar.ev[i].t + rbar.ev[i].len, 0, &rbar.ev[i]);
}
played[bar & 3].bar = bar; played[bar & 3].start = next_bar_tick; played[bar & 3].len = rbar.barTicks;
next_bar_tick += rbar.barTicks; bar++;
}
}
static void update_cur_bar(uint64_t now){
int64_t tk = tick_at(now);
int i, cur = -1;
for(i = 0; i < 4; i++) if(played[i].bar >= 0 && tk >= played[i].start && tk < played[i].start + played[i].len) cur = played[i].bar;
g_cur_bar = cur;
}
void seq_core1_main(void){
flash_safe_execute_core_init(); /* lets core 0 pause this core while it writes flash */
g_cur_bar = -1;
for(;;){
cmd_t c;
uint64_t now;
while(queue_try_remove(&g_cmdq, &c)){
if(c.cmd == CMD_START) start();
else if(c.cmd == CMD_STOP) stop();
else if(c.cmd == CMD_RELEASE_TRACK) release_where(c.arg);
else if(c.cmd == CMD_BPM) set_bpm(c.arg);
}
if(!running) continue;
now = time_us_64();
render_ahead(now);
if(g_live.clock){ while(time_of(next_clock) <= now){ send1_both(0xF8); next_clock += SB_CLOCK_TICKS; } }
else next_clock = tick_at(now) / SB_CLOCK_TICKS * SB_CLOCK_TICKS + SB_CLOCK_TICKS;
while(qn && time_of(q[0].tick) <= now){
qev_t e = q[0];
qn--; memmove(&q[0], &q[1], (size_t)qn * sizeof q[0]);
dispatch(&e);
}
update_cur_bar(now);
}
}

73
firmware/app/storage.c Normal file
View File

@@ -0,0 +1,73 @@
#include "storage.h"
#include "hardware/flash.h"
#include "pico/flash.h"
#include <stddef.h>
#include <string.h>
#define MAGIC 0x53425831u /* "SBX1" */
#define VERSION 2 /* 2: ROOT / SCALE added, BANK gone. Older records are ignored. */
#define OFFSET (PICO_FLASH_SIZE_BYTES - FLASH_SECTOR_SIZE)
#define BUFSZ 512
typedef struct {
uint32_t magic, version;
uint32_t seed, salts[2];
int32_t gen, bpm, scale, root, mono_ch, mono_oct;
uint8_t on[SB_NT];
pad_t learn[NKIT];
slot_t slots[NSLOTS];
uint32_t crc;
} record_t;
_Static_assert(sizeof(record_t) <= BUFSZ, "record too big");
static uint32_t crc(const record_t *r){
const uint8_t *p = (const uint8_t *)r;
uint32_t h = 0x811C9DC5u;
size_t i;
for(i = 0; i < offsetof(record_t, crc); i++) h = (h ^ p[i]) * 0x01000193u;
return h;
}
/* defaults from app.js DEF and app */
void storage_defaults(live_t *L, slot_t *slots){
int t;
memset(L, 0, sizeof *L);
L->state.seed = 0x5EEDB0C5u;
L->gen = 0; L->scale = -1; L->root = -1; L->energy = 64; L->drift = 32; L->bpm = 165;
for(t = 0; t < SB_NT; t++) L->on[t] = 1;
L->mono_ch = 1; L->mono_oct = 0; L->clock = 1;
memset(slots, 0, sizeof(slot_t) * NSLOTS);
}
int storage_load(live_t *L, slot_t *slots){
const record_t *r = (const record_t *)(XIP_BASE + OFFSET);
int t;
storage_defaults(L, slots);
if(r->magic != MAGIC || r->version != VERSION || r->crc != crc(r)) return 0;
L->state.seed = r->seed; L->state.salts[0] = r->salts[0]; L->state.salts[1] = r->salts[1];
L->gen = r->gen; L->bpm = r->bpm; L->scale = r->scale; L->root = r->root; L->mono_ch = r->mono_ch; L->mono_oct = r->mono_oct;
for(t = 0; t < SB_NT; t++) L->on[t] = r->on[t] ? 1 : 0;
memcpy(L->learn, r->learn, sizeof L->learn);
memcpy(slots, r->slots, sizeof r->slots);
return 1;
}
static void do_write(void *buf){
flash_range_erase(OFFSET, FLASH_SECTOR_SIZE);
flash_range_program(OFFSET, (const uint8_t *)buf, BUFSZ);
}
int storage_save(const live_t *L, const slot_t *slots){
static uint8_t buf[BUFSZ] __attribute__((aligned(4)));
record_t *r = (record_t *)buf;
memset(buf, 0xFF, sizeof buf);
memset(r, 0, sizeof *r);
r->magic = MAGIC; r->version = VERSION;
r->seed = L->state.seed; r->salts[0] = L->state.salts[0]; r->salts[1] = L->state.salts[1];
r->gen = L->gen; r->bpm = L->bpm; r->scale = L->scale; r->root = L->root; r->mono_ch = L->mono_ch; r->mono_oct = L->mono_oct;
memcpy(r->on, L->on, sizeof r->on);
memcpy(r->learn, L->learn, sizeof r->learn);
memcpy(r->slots, slots, sizeof r->slots);
r->crc = crc(r);
return flash_safe_execute(do_write, buf, 500) == PICO_OK;
}

15
firmware/app/storage.h Normal file
View File

@@ -0,0 +1,15 @@
/* Settings in the last flash sector: slots, learned pads, BPM, generator, root, scale, synth channel and octave,
plus the current seed and track on/off (as app.js keeps them). Erasing a sector stalls both cores for tens of
milliseconds, so main.c only saves while stopped. */
#ifndef SEEDBOX_STORAGE_H
#define SEEDBOX_STORAGE_H
#include "app.h"
typedef struct { uint8_t used, gen; uint32_t seed; uint32_t salts[2]; } slot_t;
void storage_defaults(live_t *L, slot_t *slots);
int storage_load(live_t *L, slot_t *slots); /* 1 if a valid record was found */
int storage_save(const live_t *L, const slot_t *slots);
#endif