Seedbox v0.1 + firmware, generic kit/synth, ROOT/SCALE pages
Some checks failed
check / check (push) Has been cancelled
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:
45
firmware/app/app.h
Normal file
45
firmware/app/app.h
Normal 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
260
firmware/app/main.c
Normal 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
170
firmware/app/sequencer.c
Normal 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, ¬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);
|
||||
}
|
||||
}
|
||||
73
firmware/app/storage.c
Normal file
73
firmware/app/storage.c
Normal 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
15
firmware/app/storage.h
Normal 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
|
||||
Reference in New Issue
Block a user