Files
seedbox/firmware/app/sequencer.c
Gee 346c820b10
Some checks failed
check / check (push) Has been cancelled
Split kit and keys; KEYS GEN with MOTIF, 303, TURING, ARP
- genome(seed, salts) now has three parts with their own RNG streams:
  shared (steps, form, swing, arc, layback, fill odds, phase offset),
  kit (density, frame, accent cycle, free snare) and keys (scale/root,
  progression, motif, hocket, bass tone, note lengths, range). ROLL KEYS
  now renews the harmony too; ROLL KIT leaves the keys alone.
- Keys generators share one signature and can fix note lengths:
  MOTIF (the old line), 303 (acid bass, answers in the rests, drone or
  silent pad), TURING (8/16-bit shift register, DRIFT flips bits, 16-bar
  window), ARP (triad/7th arpeggio, roots on kicks).
- Pages: KIT GEN, KEYS GEN; ROLL shows KIT / KEYS; slots keep both gens.
- Flash record v3 (old settings ignored). Bring-up golden checksums updated.
- C port matches; host test and ASan/UBSan pass.

Intended music change: every seed sounds different (genome split, new
generators). Vectors regenerated with npm run vectors; the case grid now
crosses all kit and keys generators, with TURING runs past its window.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 21:50:41 +03:00

171 lines
6.6 KiB
C

/* 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.keys = L.keys; 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);
}
}