Files
seedbox/firmware/app/main.c
Gee 2196633ed5
Some checks failed
check / check (push) Has been cancelled
Seedbox v0.1 + firmware, generic kit/synth, ROOT/SCALE pages
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>
2026-10-11 20:39:36 +03:00

261 lines
12 KiB
C

/* 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();
}
}