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>
171 lines
6.6 KiB
C
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, ¬e)) 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, ¬e)) 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);
|
|
}
|
|
}
|