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

42
firmware/CMakeLists.txt Normal file
View File

@@ -0,0 +1,42 @@
# Seedbox firmware for the Pico 2 W.
# cmake -S firmware -B firmware/build -G Ninja && ninja -C firmware/build
# Needs PICO_SDK_PATH (SDK 2.1 or later). The host engine test is separate: firmware/test/Makefile.
cmake_minimum_required(VERSION 3.13)
set(PICO_BOARD pico2_w CACHE STRING "Board type")
include(pico_sdk_import.cmake)
project(seedbox C CXX ASM)
set(CMAKE_C_STANDARD 11)
pico_sdk_init()
# the portable engine, the same source the host test checks
add_library(seedbox_engine INTERFACE)
target_sources(seedbox_engine INTERFACE ${CMAKE_CURRENT_LIST_DIR}/engine/engine.c)
target_include_directories(seedbox_engine INTERFACE ${CMAKE_CURRENT_LIST_DIR}/engine)
add_library(seedbox_drivers INTERFACE)
target_sources(seedbox_drivers INTERFACE
${CMAKE_CURRENT_LIST_DIR}/drivers/midi_uart.c
${CMAKE_CURRENT_LIST_DIR}/drivers/pcd8544.c
${CMAKE_CURRENT_LIST_DIR}/drivers/inputs.c
${CMAKE_CURRENT_LIST_DIR}/drivers/encoder.c
${CMAKE_CURRENT_LIST_DIR}/drivers/pots.c)
target_include_directories(seedbox_drivers INTERFACE ${CMAKE_CURRENT_LIST_DIR}/drivers)
target_link_libraries(seedbox_drivers INTERFACE pico_stdlib hardware_uart hardware_spi hardware_adc)
# bring-up: board test over USB serial
add_executable(seedbox_bringup bringup/main.c)
target_compile_options(seedbox_bringup PRIVATE -Wall -Wextra)
target_link_libraries(seedbox_bringup seedbox_engine seedbox_drivers pico_stdlib pico_cyw43_arch_none)
pico_enable_stdio_usb(seedbox_bringup 1) # also keeps the reset interface for picotool reboot -f -u
pico_enable_stdio_uart(seedbox_bringup 0) # UART0 is MIDI: stdio on it would send text to the kit device
pico_add_extra_outputs(seedbox_bringup)
# the instrument: core 0 UI, core 1 sequencer
add_executable(seedbox app/main.c app/sequencer.c app/storage.c)
target_compile_options(seedbox PRIVATE -Wall -Wextra)
target_include_directories(seedbox PRIVATE app)
target_link_libraries(seedbox seedbox_engine seedbox_drivers pico_stdlib pico_multicore pico_flash pico_rand hardware_flash)
pico_enable_stdio_usb(seedbox 1) # keeps the reset interface: picotool reboot -f -u flashes with the box closed
pico_enable_stdio_uart(seedbox 0) # UART0 is MIDI
pico_add_extra_outputs(seedbox)

94
firmware/README.md Normal file
View File

@@ -0,0 +1,94 @@
# Firmware plan
Nothing here yet. This is the plan for porting the simulator to the Pico 2 W in C with the Pico SDK.
## Rule one: the engine must match the vectors
`sim/engine.js` is the reference. The C engine is done when it reproduces every file in `test/vectors/` bit for bit:
1. **primitives.json**: `mix()`, the xorshift32 `Rng` (`next`, `below`, `chance`) and `genome()`.
2. **gen0 … gen4 \*.json**: full renders, 8 bars per case, 189 cases. Each case has a checksum (FNV-1a over the rows), so a failure shows up at once. The first differing bar tells you where to look.
Build and test the engine **on the host first**: plain C, no SDK, a test runner that reads the JSON (or a header generated from it). Only then put it on the Pico. If the music needs to change, change `engine.js`, listen in the simulator, regenerate the vectors (`npm run vectors`), and commit both together.
### JS → C traps
- **Integers**:
- Every hash and RNG value is `uint32_t`.
- `Math.imul(a,b)>>>0` is `(uint32_t)a*(uint32_t)b`.
- `x>>>n` is a logical shift on `uint32_t`.
- **`fdiv` and `mod`** are floor division and a positive modulo. C's `/` and `%` truncate toward zero, so write `fdiv`/`mod` helpers that handle negatives.
- **`>>` on negative numbers**: JS shifts arithmetically, for example `(e-64)>>2`. Use `fdiv(x,4)` or make sure the compiler does an arithmetic shift (GCC on ARM does).
- **`%` in JS** truncates like C. It only matches `mod()` when the left side is non-negative.
- **`Math.floor(a*b/c)`** with non-negative ints is plain integer division.
- **Sorting**:
- `Array.prototype.sort` is stable (ES2019), and the engine relies on it: events that tie on the sort keys keep their insertion order.
- Use a stable sort, such as insertion sort on small arrays.
- Sort keys: `renderBar` uses (t, track, voice), and AMEN humanising uses (t, voice).
- **No allocation in the audio path.** Fix the maximum events per bar and use static arrays. The vectors peak at 54 events in a bar (automata, energy 127); size buffers for 128.
- **`genome` has a `formName` string.** It's only for display. `describe()` is display only and doesn't need porting exactly.
## Structure (suggested)
```
firmware/
engine/ engine.c/.h portable, no SDK, the only thing the vectors test
test/ host test runner gcc, reads test/vectors
app/ main.c core 0: UI, screen, inputs; core 1: scheduler + MIDI out
drivers/ midi_uart.c pcd8544.c matrix.c encoder.c pots.c
CMakeLists.txt
```
## Runtime
- **Two cores.**
- Core 1 owns time: it renders a bar ahead into a double buffer and sends MIDI from a hardware timer.
- 840 ticks per quarter.
- Clock (0xF8) every 35 ticks.
- Start (0xFA) and stop (0xFC) on RUN.
- Core 0 runs the UI and hands parameter changes over through a small queue or atomics.
- **MIDI out**: UART0 TX on GP0 (OUT KIT) and UART1 TX on GP4 (OUT SYNTH), both at 31 250 baud. Running status is optional.
- **MIDI in**: UART0 RX on GP1, only used by LEARN. A note-on stores {channel, note}.
- **Track gating** matches app.js:
- Every event is rendered.
- Muted tracks skip their note-ons, but note-offs still go out.
- Unlearned kit tracks are skipped.
- A held button flips the track while held.
- Optional: changes wait for the bar line.
- **Stop** sends note-offs for everything sounding, CC 120 on each kit channel in use, and CC 120 and 123 on the synth channel.
- **Storage**: keep slots, learned pads, BPM, generator, MIDI channel and octave in the last flash sector. Write only on change, debounced.
## Drivers
- **PCD8544** (Nokia 5110):
- SPI0 (SCK GP18, MOSI GP19, CE GP17), with DC on GP20 and RST on GP21.
- Up to 4 MHz.
- The 84 × 48 framebuffer is 504 bytes.
- The screen layout and the 5 × 7 font are in app.js (`screenText`, `drawMap`, `FONT`).
- Contrast (Vop) varies between modules, so make it a setting.
- **Matrix**:
- Rows GP2, GP3, GP5 are inputs with pull-ups.
- Columns GP6–GP9 are driven low one at a time and Hi-Z otherwise, with the input buffer off or a pull-up on (E9).
- Scan at about 1 kHz with a 5–10 ms debounce.
- Key map (row, col) → track: (1,1) KICK, (1,2) KICK 2, (1,3) SNARE, (1,4) none; (2,1) HAT, (2,2) HAT 16, (2,3) PERC, (2,4) GHOST; (3,1) BASS, (3,2) MEL A, (3,3) MEL B, (3,4) PAD. Same as `PANEL` in app.js, which also orders the screen map.
- No FILL button: always pass `fill = false` to the engine (bar-4 fills still happen on their own).
- **Encoder**: GP10/GP11 quadrature with a state-table decoder; push on GP12. Long push (about 600 ms) is the SLOT save.
- **Pots**: ADC0/ADC1 (GP26/GP27). Average, then 7-bit with hysteresis so the values don't flicker.
- **SEED** (GP13) and **RUN** (GP14): inputs with pull-ups, debounced.
## Pico notes
- RP2350 erratum E9: use pull-ups, not pull-downs, on inputs. The whole design is active-low.
- `gpio_set_drive_strength(0, GPIO_DRIVE_STRENGTH_12MA)`, and the same for pin 4. Each MIDI loop draws about 6–8 mA.
- The W's wireless chip isn't used in v0.1.
- BOOTSEL can't be reached with the box closed. Keep the SDK's USB reset interface in the build (`pico_enable_stdio_usb`), so `picotool reboot -f -u` drops the board into the bootloader for flashing.
- Matrix columns that aren't being driven: input buffer off or pull-up on (E9).
## Order of work
1. Port the engine on the host until `test/vectors` pass.
2. Bring up the board with a blink and the UARTs: send a test note to each device.
3. Screen driver and font.
4. Inputs: matrix, encoder, pots, SEED, RUN.
5. Scheduler on core 1, running the ported engine. Compare it by ear with the simulator on the same seed.
6. Pages, LEARN, slots, flash storage.

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

160
firmware/bringup/main.c Normal file
View File

@@ -0,0 +1,160 @@
/* Seedbox bring-up: exercises every part of the board and reports over USB serial (tio /dev/ttyACM0).
- LED blinks once a second (on the Pico 2 W the LED hangs off the wireless chip, hence cyw43_arch).
- Test notes, alternating every half second: OUT KIT ch 1 note 36, OUT SYNTH ch 1 note 60.
- MIDI IN bytes are printed in hex.
- Every button, the encoder, its push, SEED, RUN and the pots are printed when they change.
- The LCD shows live values at the bench contrast (LCD_VOP_DEFAULT).
- At boot the engine renders three golden cases and checks their checksums on the Pico. */
#include <stdio.h>
#include "pico/stdlib.h"
#include "pico/cyw43_arch.h"
#include "engine.h"
#include "midi_uart.h"
#include "pcd8544.h"
#include "inputs.h"
#include "encoder.h"
#include "pots.h"
static const char *const KEY_NAME[12] = { "KICK", "KICK 2", "SNARE", "(1,4)", "HAT", "HAT 16", "PERC", "GHOST", "BASS", "MEL A", "MEL B", "PAD" };
static const char *const SW_NAME[3] = { "SEED", "RUN", "PUSH" };
/* engine self-test: golden cases from test/vectors (8 bars each) */
typedef struct { uint32_t seed; int gen, energy, drift; uint32_t checksum; } golden_t;
static const golden_t GOLDEN[3] = {
{ 0x5EEDB0C5, 0, 64, 32, 0x4F5DE957 }, /* 5EEDB0C5_s0-0_g0_e64_d32 */
{ 0x5EEDB0C5, 3, 127, 0, 0x29C313A5 }, /* 5EEDB0C5_s0-0_g3_e127_d0, dense */
{ 0x5EEDB0C5, 4, 64, 32, 0x8C01C1F9 }, /* 5EEDB0C5_s0-0_g4_e64_d32, AMENIZER */
};
static int engine_ok;
static uint32_t engine_worst_us;
static uint32_t engine_got[3];
static void engine_selftest(void){
static sb_bar bar; /* static: too big for the stack */
int c, b, i;
engine_ok = 1; engine_worst_us = 0;
for(c = 0; c < 3; c++){
sb_state st = { GOLDEN[c].seed, {0, 0} };
sb_params p = { GOLDEN[c].gen, GOLDEN[c].energy, GOLDEN[c].drift, 0, -1, -1 };
uint32_t h = SB_FNV_INIT;
for(b = 0; b < 8; b++){
uint64_t t0 = time_us_64();
uint32_t dt;
sb_render_bar(&st, &p, b, &bar);
dt = (uint32_t)(time_us_64() - t0);
if(dt > engine_worst_us) engine_worst_us = dt;
for(i = 0; i < bar.n; i++) h = sb_checksum_event(h, &bar.ev[i]);
}
engine_got[c] = h;
if(h != GOLDEN[c].checksum) engine_ok = 0;
}
}
static void print_banner(void){
int c;
printf("\n== Seedbox bring-up ==\n");
for(c = 0; c < 3; c++)
printf("engine gen %d e%d d%d: checksum 0x%08lX, want 0x%08lX %s\n", GOLDEN[c].gen, GOLDEN[c].energy, GOLDEN[c].drift,
(unsigned long)engine_got[c], (unsigned long)GOLDEN[c].checksum, engine_got[c] == GOLDEN[c].checksum ? "OK" : "FAIL");
printf("engine worst bar render: %lu us\n", (unsigned long)engine_worst_us);
printf("test notes: OUT KIT ch1 note 36, OUT SYNTH ch1 note 60, alternating every 500 ms\n");
printf("LCD contrast %d; encoder AB now %d (3 = at rest), + = clockwise\n\n", LCD_VOP_DEFAULT, encoder_ab());
}
int main(void){
char line[24];
uint32_t prev_in = 0, now_in;
int32_t prev_enc = 0, enc;
int prev_pot[2] = {-1, -1}, led = 0, was_connected = 0, i;
int last_key = -1, last_in[3] = {-1, -1, -1};
int note_kit_off = -1, note_synth_off = -1; /* ms counters until note-off, -1 = none */
uint32_t ms = 0;
absolute_time_t next;
int have_led;
stdio_init_all();
midi_init();
inputs_init();
encoder_init();
pots_init();
lcd_init(LCD_VOP_DEFAULT);
lcd_text(0, 0, "SEEDBOX TEST");
lcd_text(0, 8, "ENGINE...");
lcd_flush();
have_led = cyw43_arch_init() == 0;
engine_selftest();
next = get_absolute_time();
for(;;){
next = delayed_by_us(next, 1000);
sleep_until(next);
ms++;
if(stdio_usb_connected() && !was_connected) print_banner();
was_connected = stdio_usb_connected();
/* buttons and switches */
now_in = inputs_scan();
if(now_in != prev_in){
for(i = 0; i < IN_COUNT; i++){
uint32_t bit = 1u << i;
if((now_in ^ prev_in) & bit){
int down = (now_in & bit) != 0;
if(i < IN_MATRIX){ printf("key %-6s %s\n", KEY_NAME[i], down ? "down" : "up"); if(down) last_key = i; }
else if(i == IN_RUN) printf("RUN %s\n", down ? "on" : "off");
else printf("%s %s\n", SW_NAME[i - IN_SEED], down ? "down" : "up");
}
}
prev_in = now_in;
}
/* encoder */
enc = encoder_count();
if(enc != prev_enc){
printf("encoder click %ld (%+ld), raw edges %ld\n", (long)enc, (long)(enc - prev_enc), (long)encoder_steps());
prev_enc = enc;
}
/* MIDI IN */
for(;;){
int b = midi_in_read();
if(b < 0) break;
printf("midi in %02X\n", b);
last_in[0] = last_in[1]; last_in[1] = last_in[2]; last_in[2] = b;
}
/* pots, every 10 ms */
if(ms % 10 == 0){
pots_update();
for(i = 0; i < 2; i++) if(pot_value(i) != prev_pot[i]){
prev_pot[i] = pot_value(i);
printf("%s %d (raw %d)\n", i == POT_ENERGY ? "ENERGY" : "DRIFT", pot_value(i), pot_raw(i));
}
}
/* test notes and LED */
if(note_kit_off > 0 && --note_kit_off == 0){ midi_note_off(MIDI_KIT, 0, 36); note_kit_off = -1; }
if(note_synth_off > 0 && --note_synth_off == 0){ midi_note_off(MIDI_SYNTH, 0, 60); note_synth_off = -1; }
if(ms % 1000 == 0){ midi_note_on(MIDI_KIT, 0, 36, 100); note_kit_off = 150; }
if(ms % 1000 == 500){ midi_note_on(MIDI_SYNTH, 0, 60, 100); note_synth_off = 150; }
if(ms % 500 == 0 && have_led){ led = !led; cyw43_arch_gpio_put(CYW43_WL_GPIO_LED_PIN, led); }
/* screen, every 100 ms */
if(ms % 100 == 0){
lcd_clear();
lcd_text(0, 0, "SEEDBOX TEST");
snprintf(line, sizeof line, engine_ok ? "ENG OK %luUS" : "ENG FAIL", (unsigned long)engine_worst_us);
lcd_text(0, 8, line);
snprintf(line, sizeof line, "E%3d D%3d", pot_value(POT_ENERGY), pot_value(POT_DRIFT));
lcd_text(0, 16, line);
snprintf(line, sizeof line, "ENC%4ld V%3d", (long)enc, LCD_VOP_DEFAULT);
lcd_text(0, 24, line);
snprintf(line, sizeof line, "%s%s%s %s", (now_in >> IN_SEED) & 1 ? "S" : "-", (now_in >> IN_RUN) & 1 ? "R" : "-",
(now_in >> IN_PUSH) & 1 ? "P" : "-", last_key >= 0 ? KEY_NAME[last_key] : "");
lcd_text(0, 32, line);
if(last_in[2] >= 0){ snprintf(line, sizeof line, "IN %02X %02X %02X", last_in[0] & 0xFF, last_in[1] & 0xFF, last_in[2] & 0xFF); lcd_text(0, 40, line); }
else lcd_text(0, 40, "IN -");
lcd_flush();
}
}
}

View File

@@ -0,0 +1,45 @@
#include "encoder.h"
#include "hardware/gpio.h"
#define PIN_A 10
#define PIN_B 11
#define REST 3 /* A = B = 1: both contacts open at a detent */
#define DIR -1 /* as wired on the bench, the raw count runs down clockwise */
static volatile int32_t steps, clicks;
static volatile int8_t acc; /* edges since the last rest */
static volatile uint8_t prev;
/* index = previous AB << 2 | current AB; invalid (bounce) transitions count 0 */
static const int8_t TABLE[16] = { 0,-1, 1, 0, 1, 0, 0,-1, -1, 0, 0, 1, 0, 1,-1, 0 };
static uint8_t read_ab(void){ return (uint8_t)((gpio_get(PIN_A) << 1) | gpio_get(PIN_B)); }
static void on_edge(uint gpio, uint32_t events){
uint8_t cur;
int8_t d;
(void)events;
if(gpio != PIN_A && gpio != PIN_B) return;
cur = read_ab();
d = TABLE[(prev << 2) | cur];
steps += d; acc = (int8_t)(acc + d);
prev = cur;
if(cur == REST){
/* a full click is 4 edges; accept 2 or more in case bounce hid some, ignore wiggles of 1 */
if(acc >= 2) clicks += DIR;
else if(acc <= -2) clicks -= DIR;
acc = 0;
}
}
void encoder_init(void){
gpio_init(PIN_A); gpio_set_dir(PIN_A, GPIO_IN); gpio_pull_up(PIN_A);
gpio_init(PIN_B); gpio_set_dir(PIN_B, GPIO_IN); gpio_pull_up(PIN_B);
prev = read_ab();
gpio_set_irq_enabled_with_callback(PIN_A, GPIO_IRQ_EDGE_RISE | GPIO_IRQ_EDGE_FALL, true, on_edge);
gpio_set_irq_enabled(PIN_B, GPIO_IRQ_EDGE_RISE | GPIO_IRQ_EDGE_FALL, true);
}
int32_t encoder_count(void){ return clicks; }
int32_t encoder_steps(void){ return DIR * steps; }
int encoder_ab(void){ return read_ab(); }

View File

@@ -0,0 +1,15 @@
/* Quadrature encoder on GP10 (A) / GP11 (B), pull-ups, decoded in the GPIO interrupt with a state table.
The bench encoder gives 4 edges per click and rests with both contacts open (A = B = 1). As wired, the raw
count runs down when turned clockwise, so the driver flips it: + = clockwise.
A click counts when the shaft comes back to rest, so jitter around a detent doesn't count. */
#ifndef SEEDBOX_ENCODER_H
#define SEEDBOX_ENCODER_H
#include <stdint.h>
void encoder_init(void);
int32_t encoder_count(void); /* clicks, + = clockwise */
int32_t encoder_steps(void); /* raw edges, for the bench */
int encoder_ab(void); /* current A << 1 | B */
#endif

50
firmware/drivers/inputs.c Normal file
View File

@@ -0,0 +1,50 @@
#include "inputs.h"
#include "hardware/gpio.h"
#include "pico/time.h"
static const uint8_t ROWS[3] = {2, 3, 5};
static const uint8_t COLS[4] = {6, 7, 8, 9};
static const uint8_t SWITCHES[3] = {13, 14, 12}; /* IN_SEED, IN_RUN, IN_PUSH */
#define DEBOUNCE_MS 10 /* a change must hold this long to count */
#define LOCKOUT_MS 40 /* then the input ignores further changes this long (worn switches chatter) */
/* (1,1) KICK (1,2) KICK 2 (1,3) SNARE (1,4) none; (2,1) HAT (2,2) HAT 16 (2,3) PERC (2,4) GHOST;
(3,1) BASS (3,2) MEL A (3,3) MEL B (3,4) PAD */
const int8_t INPUT_KEY_TRACK[12] = { 0, 1, 2, -1, 4, 5, 6, 3, 7, 8, 9, 10 };
static uint32_t state;
static uint8_t count[IN_COUNT], lockout[IN_COUNT];
static void input_pullup(uint pin){ gpio_init(pin); gpio_set_dir(pin, GPIO_IN); gpio_pull_up(pin); }
void inputs_init(void){
int i;
for(i = 0; i < 3; i++) input_pullup(ROWS[i]);
for(i = 0; i < 4; i++){ input_pullup(COLS[i]); gpio_put(COLS[i], 0); } /* idle: Hi-Z with pull-up (E9) */
for(i = 0; i < 3; i++) input_pullup(SWITCHES[i]);
}
static uint32_t raw_read(void){
uint32_t raw = 0;
int r, c;
for(c = 0; c < 4; c++){
gpio_set_dir(COLS[c], GPIO_OUT); /* output latch is already 0: drive low */
busy_wait_us(5); /* settle */
for(r = 0; r < 3; r++) if(!gpio_get(ROWS[r])) raw |= 1u << (r * 4 + c);
gpio_set_dir(COLS[c], GPIO_IN); /* back to Hi-Z, pull-up holds it high */
}
for(r = 0; r < 3; r++) if(!gpio_get(SWITCHES[r])) raw |= 1u << (IN_SEED + r);
return raw;
}
uint32_t inputs_scan(void){
uint32_t raw = raw_read();
int i;
for(i = 0; i < IN_COUNT; i++){
uint32_t bit = 1u << i;
if(lockout[i]){ lockout[i]--; count[i] = 0; continue; }
if((raw & bit) != (state & bit)){ if(++count[i] >= DEBOUNCE_MS){ state ^= bit; count[i] = 0; lockout[i] = LOCKOUT_MS; } }
else count[i] = 0;
}
return state;
}

17
firmware/drivers/inputs.h Normal file
View File

@@ -0,0 +1,17 @@
/* Button matrix (rows GP2 GP3 GP5, columns GP6–GP9), SEED (GP13), RUN (GP14), encoder push (GP12).
All active-low with pull-ups (RP2350 erratum E9). Call inputs_scan() about once per millisecond. */
#ifndef SEEDBOX_INPUTS_H
#define SEEDBOX_INPUTS_H
#include <stdint.h>
/* bit index: matrix key = row * 4 + col (row 0..2, col 0..3), then the switches */
enum { IN_MATRIX = 12, IN_SEED = 12, IN_RUN = 13, IN_PUSH = 14, IN_COUNT = 15 };
/* matrix key -> track index (engine order), -1 = no button. Same as PANEL in app.js. */
extern const int8_t INPUT_KEY_TRACK[12];
void inputs_init(void);
uint32_t inputs_scan(void); /* returns the debounced state, bit set = pressed / RUN on */
#endif

View File

@@ -0,0 +1,37 @@
#include "midi_uart.h"
#include "hardware/gpio.h"
#include "hardware/uart.h"
#define PIN_OUT_KIT 0 /* UART0 TX */
#define PIN_IN 1 /* UART0 RX, 6N137 output with a 1 kΩ pull-up on the board */
#define PIN_OUT_SYNTH 4 /* UART1 TX */
#define MIDI_BAUD 31250
static uart_inst_t *const PORTS[2] = { uart0, uart1 };
void midi_init(void){
uart_init(uart0, MIDI_BAUD);
uart_init(uart1, MIDI_BAUD);
uart_set_format(uart0, 8, 1, UART_PARITY_NONE);
uart_set_format(uart1, 8, 1, UART_PARITY_NONE);
gpio_set_function(PIN_OUT_KIT, GPIO_FUNC_UART);
gpio_set_function(PIN_IN, GPIO_FUNC_UART);
gpio_set_function(PIN_OUT_SYNTH, GPIO_FUNC_UART); /* GP5 (UART1 RX) stays a matrix row */
gpio_pull_up(PIN_IN);
/* each MIDI current loop draws 6–8 mA, above the 4 mA default */
gpio_set_drive_strength(PIN_OUT_KIT, GPIO_DRIVE_STRENGTH_12MA);
gpio_set_drive_strength(PIN_OUT_SYNTH, GPIO_DRIVE_STRENGTH_12MA);
}
void midi_send(int port, const uint8_t *bytes, int n){ uart_write_blocking(PORTS[port], bytes, (size_t)n); }
void midi_note_on(int port, int ch, int note, int vel){
uint8_t m[3] = { (uint8_t)(0x90 | (ch & 15)), (uint8_t)(note & 127), (uint8_t)(vel & 127) };
midi_send(port, m, 3);
}
void midi_note_off(int port, int ch, int note){
uint8_t m[3] = { (uint8_t)(0x80 | (ch & 15)), (uint8_t)(note & 127), 0 };
midi_send(port, m, 3);
}
int midi_in_read(void){ return uart_is_readable(uart0) ? uart_getc(uart0) : -1; }

View File

@@ -0,0 +1,15 @@
/* MIDI over the two UARTs: OUT KIT = UART0 TX (GP0), OUT SYNTH = UART1 TX (GP4), IN = UART0 RX (GP1). 31 250 baud, 8N1. */
#ifndef SEEDBOX_MIDI_UART_H
#define SEEDBOX_MIDI_UART_H
#include <stdint.h>
enum { MIDI_KIT = 0, MIDI_SYNTH = 1 };
void midi_init(void);
void midi_send(int port, const uint8_t *bytes, int n); /* blocking */
void midi_note_on(int port, int ch, int note, int vel); /* ch 0..15 */
void midi_note_off(int port, int ch, int note);
int midi_in_read(void); /* next byte from MIDI IN, or -1 */
#endif

View File

@@ -0,0 +1,91 @@
#include "pcd8544.h"
#include "hardware/gpio.h"
#include "hardware/spi.h"
#include "pico/time.h"
#include <string.h>
#define PIN_CE 17
#define PIN_SCK 18
#define PIN_MOSI 19
#define PIN_DC 20
#define PIN_RST 21
#define LCD_SPI spi0
#define LCD_HZ 4000000
static uint8_t fb[LCD_W * LCD_H / 8]; /* PCD8544 layout: 6 banks of 8 rows, LSB at the top */
static void write(int data, const uint8_t *b, size_t n){
gpio_put(PIN_DC, data);
gpio_put(PIN_CE, 0);
spi_write_blocking(LCD_SPI, b, n); /* returns once the bytes are shifted out */
gpio_put(PIN_CE, 1);
}
static void cmd(uint8_t c){ write(0, &c, 1); }
void lcd_set_vop(int vop){
cmd(0x21); /* extended instruction set */
cmd((uint8_t)(0x80 | (vop & 0x7F)));
cmd(0x20);
}
void lcd_init(int vop){
spi_init(LCD_SPI, LCD_HZ);
spi_set_format(LCD_SPI, 8, SPI_CPOL_0, SPI_CPHA_0, SPI_MSB_FIRST);
gpio_set_function(PIN_SCK, GPIO_FUNC_SPI);
gpio_set_function(PIN_MOSI, GPIO_FUNC_SPI);
gpio_init(PIN_CE); gpio_set_dir(PIN_CE, GPIO_OUT); gpio_put(PIN_CE, 1);
gpio_init(PIN_DC); gpio_set_dir(PIN_DC, GPIO_OUT); gpio_put(PIN_DC, 0);
gpio_init(PIN_RST); gpio_set_dir(PIN_RST, GPIO_OUT);
gpio_put(PIN_RST, 0); sleep_ms(10); gpio_put(PIN_RST, 1); sleep_ms(10);
cmd(0x21); /* extended */
cmd((uint8_t)(0x80 | (vop & 0x7F)));/* Vop */
cmd(0x04); /* temperature coefficient 0 */
cmd(0x13); /* bias 1:48 */
cmd(0x20); /* basic, horizontal addressing */
cmd(0x0C); /* normal display */
lcd_clear(); lcd_flush();
}
void lcd_clear(void){ memset(fb, 0, sizeof fb); }
void lcd_px(int x, int y, int on){
if(x < 0 || x >= LCD_W || y < 0 || y >= LCD_H) return;
if(on) fb[(y >> 3) * LCD_W + x] |= (uint8_t)(1u << (y & 7));
else fb[(y >> 3) * LCD_W + x] &= (uint8_t)~(1u << (y & 7));
}
void lcd_fill(int x, int y, int w, int h, int on){ int i, j; for(j = y; j < y + h; j++) for(i = x; i < x + w; i++) lcd_px(i, j, on); }
/* The 5 × 7 font from sim/app.js (FONT): five column bytes per glyph, bit 0 at the top. */
typedef struct { char c; uint8_t col[5]; } glyph_t;
static const glyph_t FONT[] = {
{' ',{0,0,0,0,0}},{'+',{8,8,62,8,8}},{'-',{8,8,8,8,8}},{'.',{0,96,96,0,0}},{'/',{32,16,8,4,2}},{':',{0,54,54,0,0}},
{'#',{20,127,20,127,20}},{'*',{20,8,62,8,20}},{'>',{0,65,34,20,8}},{'?',{2,1,81,9,6}},{'!',{0,0,95,0,0}},
{'0',{62,81,73,69,62}},{'1',{0,66,127,64,0}},{'2',{66,97,81,73,70}},{'3',{33,65,69,75,49}},{'4',{24,20,18,127,16}},
{'5',{39,69,69,69,57}},{'6',{60,74,73,73,48}},{'7',{1,113,9,5,3}},{'8',{54,73,73,73,54}},{'9',{6,73,73,41,30}},
{'A',{126,17,17,17,126}},{'B',{127,73,73,73,54}},{'C',{62,65,65,65,34}},{'D',{127,65,65,34,28}},{'E',{127,73,73,73,65}},
{'F',{127,9,9,9,1}},{'G',{62,65,73,73,122}},{'H',{127,8,8,8,127}},{'I',{0,65,127,65,0}},{'J',{32,64,65,63,1}},
{'K',{127,8,20,34,65}},{'L',{127,64,64,64,64}},{'M',{127,2,12,2,127}},{'N',{127,4,8,16,127}},{'O',{62,65,65,65,62}},
{'P',{127,9,9,9,6}},{'Q',{62,65,81,33,94}},{'R',{127,9,25,41,70}},{'S',{70,73,73,73,49}},{'T',{1,1,127,1,1}},
{'U',{63,64,64,64,63}},{'V',{31,32,64,32,31}},{'W',{63,64,56,64,63}},{'X',{99,20,8,20,99}},{'Y',{7,8,112,8,7}},
{'Z',{97,81,73,69,67}}
};
static const glyph_t *glyph(char c){
size_t i;
if(c >= 'a' && c <= 'z') c = (char)(c - 32);
for(i = 0; i < sizeof FONT / sizeof FONT[0]; i++) if(FONT[i].c == c) return &FONT[i];
return glyph('?');
}
void lcd_text_v(int x, int y, const char *s, int on){
for(; *s; s++, x += 6){
const glyph_t *g = glyph(*s);
int col, row;
for(col = 0; col < 5; col++) for(row = 0; row < 7; row++) if((g->col[col] >> row) & 1) lcd_px(x + col, y + row, on);
}
}
void lcd_text(int x, int y, const char *s){ lcd_text_v(x, y, s, 1); }
void lcd_box(int x, int y, int w, int h){ lcd_fill(x, y, w, 1, 1); lcd_fill(x, y + h - 1, w, 1, 1); lcd_fill(x, y, 1, h, 1); lcd_fill(x + w - 1, y, 1, h, 1); }
void lcd_flush(void){
cmd(0x80); cmd(0x40); /* X = 0, Y = 0 */
write(1, fb, sizeof fb);
}

View File

@@ -0,0 +1,19 @@
/* Nokia 5110 (PCD8544) on SPI0: SCK GP18, MOSI GP19, CE GP17, D/C GP20, RST GP21. 84 × 48, 504-byte framebuffer. */
#ifndef SEEDBOX_PCD8544_H
#define SEEDBOX_PCD8544_H
#define LCD_W 84
#define LCD_H 48
#define LCD_VOP_DEFAULT 66 /* contrast; varies between modules, so it's a setting. 66 suits the bench module. */
void lcd_init(int vop);
void lcd_set_vop(int vop); /* 0..127 */
void lcd_clear(void);
void lcd_px(int x, int y, int on);
void lcd_fill(int x, int y, int w, int h, int on);
void lcd_box(int x, int y, int w, int h); /* 1 px outline */
void lcd_text(int x, int y, const char *s); /* 5 × 7 font from app.js, 6 px per character, upper case */
void lcd_text_v(int x, int y, const char *s, int on); /* draws only the glyph pixels, in on (0 = inverse) */
void lcd_flush(void);
#endif

31
firmware/drivers/pots.c Normal file
View File

@@ -0,0 +1,31 @@
#include "pots.h"
#include "hardware/adc.h"
#define SAMPLES 16
#define HYSTERESIS 24 /* in 12-bit counts: 0.75 of a 7-bit step past the step's centre */
static int value[2], raw[2];
void pots_init(void){
adc_init();
adc_gpio_init(26); /* also turns off the digital input and pulls */
adc_gpio_init(27);
pots_update();
value[0] = raw[0] >> 5; value[1] = raw[1] >> 5;
}
void pots_update(void){
int i, k;
for(i = 0; i < 2; i++){
int sum = 0, centre, d;
adc_select_input((uint)i);
for(k = 0; k < SAMPLES; k++) sum += adc_read();
raw[i] = sum / SAMPLES;
centre = value[i] * 32 + 16;
d = raw[i] - centre;
if(d > 16 + HYSTERESIS || d < -16 - HYSTERESIS) value[i] = raw[i] >> 5;
}
}
int pot_value(int pot){ return value[pot]; }
int pot_raw(int pot){ return raw[pot]; }

12
firmware/drivers/pots.h Normal file
View File

@@ -0,0 +1,12 @@
/* ENERGY on ADC0 (GP26), DRIFT on ADC1 (GP27). Averaged, then 7-bit with hysteresis. */
#ifndef SEEDBOX_POTS_H
#define SEEDBOX_POTS_H
enum { POT_ENERGY = 0, POT_DRIFT = 1 };
void pots_init(void);
void pots_update(void); /* call every few ms */
int pot_value(int pot); /* 0..127 */
int pot_raw(int pot); /* last averaged 12-bit reading, for the bench */
#endif

758
firmware/engine/engine.c Normal file
View File

@@ -0,0 +1,758 @@
/* Seedbox engine: C port of sim/engine.js (the reference). Read the two side by side:
functions keep their JS names (snake_case) and order. Comments explaining the music live in engine.js.
Port rules:
- Hashes and RNG values are uint32_t. Math.imul(a,b)>>>0 == (uint32_t)a*(uint32_t)b.
- fdiv/mod are floor division and positive modulo; use them wherever the left side can be negative.
- One RNG call per statement. C leaves the evaluation order of operands and arguments unspecified,
so f(r.below(), r.below()) would not match JS's left-to-right order.
- Sorts are stable insertion sorts (JS Array.prototype.sort is stable and the engine relies on it).
- Object key sets keyed by step (fs.K, fs.S) are flag arrays walked in ascending order, which is
how JS enumerates integer keys. */
#include "engine.h"
#include <string.h>
#define TPB SB_TPB
#define TPS SB_TPS
#define PHRASE_BARS SB_PHRASE_BARS
#define MAX_STEPS 20
static int g_overflow;
/* ---------- primitives ---------- */
uint32_t sb_mix(uint32_t a, uint32_t b){
uint32_t h = a ^ (b * 0x9E3779B1u);
h ^= h >> 16; h *= 0x85EBCA6Bu; h ^= h >> 13; h *= 0xC2B2AE35u; h ^= h >> 16;
return h ? h : 0x6D2B79F5u;
}
void sb_rng_init(sb_rng *r, uint32_t s){ r->s = s ? s : 0x6D2B79F5u; }
uint32_t sb_rng_next(sb_rng *r){ uint32_t x = r->s; x ^= x << 13; x ^= x >> 17; x ^= x << 5; r->s = x; return x; }
int sb_rng_below(sb_rng *r, int n){
if(n <= 0) return 0;
return (int)((uint32_t)((uint64_t)(sb_rng_next(r) >> 16) * (uint32_t)n) >> 16);
}
int sb_rng_chance(sb_rng *r, int p){ return (int)(sb_rng_next(r) >> 16) < p; }
static int pick_w(sb_rng *r, const int *w, int n){
int t = 0, i, x;
for(i = 0; i < n; i++) t += w[i];
x = sb_rng_below(r, t);
for(i = 0; i < n; i++){ if(x < w[i]) return i; x -= w[i]; }
return n - 1;
}
#define below sb_rng_below
#define chance sb_rng_chance
#define mix sb_mix
static int clamp(int x, int a, int b){ return x < a ? a : x > b ? b : x; }
static int imin(int a, int b){ return a < b ? a : b; }
static int imax(int a, int b){ return a > b ? a : b; }
static int P(int x){ return clamp(x, 0, 127) * 516; }
static int fdiv(int a, int b){ int q = a / b; if((a % b != 0) && ((a < 0) != (b < 0))) q--; return q; }
static int mod(int a, int b){ return ((a % b) + b) % b; }
static sb_rng rng(uint32_t s){ sb_rng r; sb_rng_init(&r, s); return r; }
/* ---------- tables ---------- */
static const int STEPS[4] = {16,12,14,20}, STEPS_W[4] = {12,3,2,2};
static const int FORMS[6][4] = {{0,0,0,1},{0,0,1,0},{0,1,0,1},{0,0,0,0},{0,1,0,2},{0,1,0,0}}, FORMS_W[6] = {5,5,3,2,3,2};
static const char *const FORM_NAMES[6] = {"AAAB","AABA","ABAB","AAAA","ABAC","ABAA"};
static const int SCALES[SB_NSCALES][8] = {
{0,2,4,5,7,9,11},{0,2,3,5,7,8,10},{0,2,3,5,7,9,10},{0,2,4,5,7,9,10},{0,2,4,6,7,9,11},{0,1,3,5,7,8,10},{0,2,3,5,7,8,11},
{0,3,5,7,10},{0,2,4,7,9},
{0,1,4,5,7,8,10},{0,1,4,5,7,8,11},{0,2,3,6,7,8,11},{0,2,3,6,7,9,10},{0,2,4,6,7,9,10},
{0,2,3,7,8},{0,1,5,7,10},{0,1,5,6,10},{0,2,4,6,9,10},{0,2,4,6,8,10},{0,1,3,4,6,7,9,10},{0,1,3,5,6,8,10}};
static const int SCALE_LEN[SB_NSCALES] = {7,7,7,7,7,7,7, 5,5, 7,7,7,7,7, 5,5,5,6,6,8,7};
static const char *const SCALE_NAMES[SB_NSCALES] = {"MAJOR","MINOR","DORIAN","MIXOLYDIAN","LYDIAN","PHRYGIAN","HARM MINOR","MINOR PENT","MAJOR PENT",
"PHRYG DOM","DBL HARM","HUNG MINOR","UKR DORIAN","LYD DOM","HIRAJOSHI","IN SEN","IWATO","PROMETHEUS","WHOLE TONE","OCTATONIC","LOCRIAN"};
const char *sb_scale_name(int scale){ return scale >= 0 && scale < SB_NSCALES ? SCALE_NAMES[scale] : "?"; }
void sb_apply_key(sb_genome *g, const sb_params *p){
if(p->scale >= 0 && p->scale < SB_NSCALES) g->scale = p->scale;
if(p->root >= 0 && p->root < 12) g->root = p->root;
}
static const int HOCKETS[8] = {0xAA,0x55,0xB4,0x6C,0x96,0xD2,0x5A,0xA5};
static const int PROGS[10][4] = {{0,0,5,4},{0,5,3,4},{0,3,0,4},{0,6,5,4},{0,2,5,4},{0,0,3,3},{0,5,0,5},{0,3,5,6},{0,1,0,6},{0,4,5,3}};
static const int SWINGS[5] = {0,21,35,50,70}, SWING_W[5] = {4,3,3,2,1};
static const int SPLITS[6] = {1,2,3,4,5,7};
typedef struct { int k[4], nk, s[2], ns, g[3], ng; } skel_t;
static const skel_t SKEL[6] = {
{{0,10},2, {4,12},2, {7,15},2}, /* two-step */
{{0,2,10,11},4, {4,12},2, {7,9,15},3}, /* amen-ish */
{{0,11},2, {8},1, {14,3},2}, /* half-time */
{{0,3,10},3, {4,12},2, {7},1}, /* think-ish */
{{0,6,11},3, {4,13},2, {9,15},2}, /* broken */
{{0,7,10},3, {4,12},2, {2,14},2} /* steppy */
};
typedef struct { int k[4], nk, s[2], ns, layer, noOne, cyc, free_; } frame_t;
static const frame_t FRAMES[8] = {
{{0},1, {4,12},2, 0,0,0,0}, /* backbeat */
{{0},1, {8},1, 0,0,0,0}, /* half-time */
{{0,4,8,12},4, {4,12},2, 1,0,0,0}, /* four-floor */
{{0,3,8,11},4, {6,14},2, 0,0,0,0}, /* 3-3-2 */
{{0,10},2, {6,12},2, 0,0,0,0}, /* broken */
{{3,10},2, {8},1, 0,1,0,0}, /* displaced */
{{0},0, {0},0, 0,0,12,0}, /* over the bar */
{{0},1, {0},0, 0,0,0,1} /* free snare */
};
static const int FRAME_W[8] = {3,3,2,2,2,1,1,2};
static const int RULES[14] = {30,45,54,57,60,73,90,105,110,122,126,150,182,225};
static const int POLY_N[6] = {5,7,9,11,12,6};
static const int NUDGE_GRP[SB_NT] = {0,0,1,3,2,2,3,0,4,4,5};
const char *sb_form_name(int form_idx){ return FORM_NAMES[form_idx]; }
/* ---------- event lists ---------- */
typedef struct { int16_t t, v, vel, a, tr; } ev_t;
typedef struct { int n; ev_t e[SB_MAX_EVENTS]; } evlist;
static void push_tr(evlist *l, int t, int v, int vel, int a, int tr){
ev_t *e;
if(l->n >= SB_MAX_EVENTS){ g_overflow = 1; return; }
e = &l->e[l->n++];
e->t = (int16_t)t; e->v = (int16_t)v; e->vel = (int16_t)vel; e->a = (int16_t)a; e->tr = (int16_t)tr;
}
static void push(evlist *l, int t, int v, int vel, int a){ push_tr(l, t, v, vel, a, 0); }
static void push_ev(evlist *l, const ev_t *e){ push_tr(l, e->t, e->v, e->vel, e->a, e->tr); }
/* keep the events where cond holds; inside cond, e points at the current event */
#define FILTER(l, cond) do{ int o_ = 0, i_; for(i_ = 0; i_ < (l)->n; i_++){ const ev_t *e = &(l)->e[i_]; if(cond) (l)->e[o_++] = *e; } (l)->n = o_; }while(0)
static void sort_tv(evlist *l){ /* stable, by (t, v) */
int i, j;
for(i = 1; i < l->n; i++){
ev_t x = l->e[i];
for(j = i - 1; j >= 0 && (l->e[j].t > x.t || (l->e[j].t == x.t && l->e[j].v > x.v)); j--) l->e[j + 1] = l->e[j];
l->e[j + 1] = x;
}
}
/* ---------- genome ---------- */
void sb_genome_make(uint32_t seed, sb_genome *g){
static const int ROT_W[4] = {4,2,2,1}, BASS_DEG[3] = {4,2,3}, ARC_W[3] = {3,2,2};
static const int ACC[5] = {0,3,5,3,7}, ACC_W[5] = {3,3,2,2,1};
sb_rng r = rng(mix(seed, 0x47454E));
int fi;
g->steps = STEPS[pick_w(&r, STEPS_W, 4)];
fi = pick_w(&r, FORMS_W, 6); g->form_idx = fi; memcpy(g->form, FORMS[fi], sizeof g->form);
g->scale = below(&r, SB_NSCALES); g->root = below(&r, 12);
g->dens[0] = 40 + below(&r, 60); g->dens[1] = 30 + below(&r, 70);
g->fillP = 40 + below(&r, 80); g->monoLen = below(&r, 3); g->monoRange = 1 + below(&r, 2);
g->rot = pick_w(&r, ROT_W, 4);
g->hocket = HOCKETS[below(&r, 8)];
g->bassDeg = BASS_DEG[below(&r, 3)];
g->prog_idx = below(&r, 10); memcpy(g->prog, PROGS[g->prog_idx], sizeof g->prog);
g->swing = SWINGS[pick_w(&r, SWING_W, 5)];
g->arc = pick_w(&r, ARC_W, 3);
g->motifLen = 3 + below(&r, 3);
g->accentCycle = ACC[pick_w(&r, ACC_W, 5)];
g->layback = below(&r, 2);
g->frame = pick_w(&r, FRAME_W, 8); g->freeK = 2 + below(&r, 3); g->freeRot = below(&r, 16);
}
static uint32_t role_seed(const sb_state *st, int r){ return mix(mix(st->seed, 0x524F4C45u + (uint32_t)r), st->salts[r]); }
static int eu(int i, int k, int n, int rot){ int j = (i + rot) % n; return ((j * k) % n) < k; }
/* groups(steps): [s, min(4, steps-s)] for s = 0, 4, 8, ... */
static int n_groups(int steps){ return (steps + 3) / 4; }
static int group_len(int steps, int gi){ return imin(4, steps - gi * 4); }
typedef struct { uint8_t K[MAX_STEPS], S[MAX_STEPS]; int noOne; } fsets;
static int sc16(int x, int steps){ return x * steps / 16; }
static void frame_sets(const sb_genome *g, int steps, int pos, fsets *fs){
const frame_t *F = &FRAMES[g->frame];
int i;
memset(fs, 0, sizeof *fs);
if(F->cyc){ for(i = 0; i < steps; i++){ int x = pos * steps + i; if(x % F->cyc == 0) fs->K[i] = 1; else if(x % F->cyc == (F->cyc >> 1)) fs->S[i] = 1; } }
else {
for(i = 0; i < F->nk; i++) fs->K[sc16(F->k[i], steps)] = 1;
if(F->free_){ for(i = 1; i < steps; i++) if(eu(i, g->freeK, steps, g->freeRot) && !fs->K[i]) fs->S[i] = 1; }
else for(i = 0; i < F->ns; i++) fs->S[sc16(F->s[i], steps)] = 1;
}
if(!F->layer) for(i = 0; i < steps; i++) if(fs->S[i]) fs->K[i] = 0;
fs->noOne = F->noOne;
}
/* divide(): velFn is chosen by mode */
enum { VEL_SUB_KICK, VEL_SUB_GHOST, VEL_SUB_BURST, VEL_SUB_HAT, VEL_SUB_PERC };
static int divide_vel(sb_rng *r, int mode, int depth){
switch(mode){
case VEL_SUB_KICK: return 96 + below(r, 16);
case VEL_SUB_GHOST: return 34 + below(r, 22);
case VEL_SUB_BURST: return 50 + below(r, 30);
case VEL_SUB_HAT: return depth == 0 ? 90 : 58 + below(r, 20);
default: return 60 + below(r, 45);
}
}
static void divide(sb_rng *r, int start, int len, int depth, int maxD, const int *W, int prob, evlist *out, int v, int mode){
if(depth < maxD){
int w[6], f, i;
memcpy(w, W, sizeof w); if(depth > 0) w[0] += depth * 3;
f = SPLITS[pick_w(r, w, 6)];
if(f > 1){ for(i = 0; i < f; i++){ int a = start + i * len / f, b = start + (i + 1) * len / f; divide(r, a, b - a, depth + 1, maxD, W, prob, out, v, mode); } return; }
}
if(chance(r, prob)){ int vel = divide_vel(r, mode, depth); push(out, start, v, vel, 0); }
}
/* ---------- kit generators ---------- */
typedef struct { int k, n, rot; } euc_t;
typedef struct { euc_t kick, ghost, hat, perc; int accent; } euclid_params_t;
static void euclid_params(uint32_t coreSeed, int steps, int d, int hd, euclid_params_t *E){
static const int K_OPTS[5] = {3,5,4,2,7}, HN_W[5] = {3,3,1,1,1};
int w[5] = {4,3,2,1,1 + (d >> 5)};
sb_rng c = rng(coreSeed);
int k = K_OPTS[pick_w(&c, w, 5)], rots[MAX_STEPS] = {0}, nr = 0, r0, hn, pn;
k = clamp(k, 1, steps >> 1);
for(r0 = 0; r0 < steps; r0++) if(((r0 * k) % steps) < k) rots[nr++] = r0;
E->kick.k = k; E->kick.n = steps; E->kick.rot = rots[below(&c, nr)];
E->ghost.k = 1 + below(&c, 3); E->ghost.n = steps; E->ghost.rot = below(&c, steps);
hn = pick_w(&c, HN_W, 5) < 2 ? steps : POLY_N[below(&c, 3)];
E->hat.k = clamp((hn * (32 + (hd >> 2))) >> 7, 1, hn); E->hat.n = hn; E->hat.rot = below(&c, hn);
pn = POLY_N[below(&c, 6)];
E->perc.k = 1 + ((pn * d) >> 9); E->perc.k += below(&c, 2); E->perc.k = clamp(E->perc.k, 1, pn - 1);
E->perc.n = pn; E->perc.rot = below(&c, pn);
E->accent = below(&c, E->hat.n);
}
static void kit_euclid(uint32_t coreSeed, uint32_t varSeed, int steps, int d, int hd, int pos, const fsets *fs, evlist *ev){
euclid_params_t E;
sb_rng vr = rng(varSeed), hr = rng(mix(varSeed, 0x484154u + (uint32_t)pos));
int i;
euclid_params(coreSeed, steps, d, hd, &E);
for(i = 0; i < steps; i++){
int t = i * TPS, x = pos * steps + i;
if(fs->S[i]) push(ev, t, 1, 112, 0);
else if(eu(i, E.ghost.k, E.ghost.n, E.ghost.rot) && chance(&vr, P(d >> 1))){ int vel = 38 + below(&vr, 20); push(ev, t, 1, vel, 0); }
if(fs->K[i]) push(ev, t, 0, i == 0 ? 120 : 112, 1);
else if(eu(i, E.kick.k, E.kick.n, E.kick.rot) && !fs->S[i] && !(i == 0 && fs->noOne)) push(ev, t, 0, 104, 0);
if(eu(x, E.hat.k, E.hat.n, E.hat.rot)){
int acc = eu(x, imax(1, E.hat.k >> 1), E.hat.n, E.accent);
push(ev, t, 2, acc ? 92 : 66, 0);
if(chance(&hr, P(hd >> 3))) push(ev, t + (TPS >> 1), 2, 44, 0);
}
if(eu(x, E.perc.k, E.perc.n, E.perc.rot)){ int vel = 70 + below(&vr, 40); push(ev, t, 3, vel, 0); }
}
}
static void kit_subdiv(uint32_t coreSeed, uint32_t varSeed, int steps, int d, int hd, int pos, const fsets *fs, evlist *ev){
static const int W_KICK[6] = {6,5,1,0,0,0}, W_GHOST[6] = {4,4,1,0,0,0}, W_BURST[6] = {1,3,4,2,3,2}, W_HAT[6] = {3,6,0,2,0,0}, W_PERC[6] = {6,2,3,0,1,1};
static evlist tmp;
sb_rng core = rng(coreSeed), vr = rng(varSeed), hr = rng(mix(varSeed, 0x484154u + (uint32_t)pos));
int ng = n_groups(steps), snT[MAX_STEPS], nsn = 0, gi, j, k, burst = -1;
if(chance(&core, P(d >> 1))) burst = below(&core, ng);
for(j = 0; j < steps; j++){
if(fs->S[j]){ int vel = 110 + below(&core, 14); push(ev, j * TPS, 1, vel, 0); snT[nsn++] = j * TPS; }
if(fs->K[j]) push(ev, j * TPS, 0, j == 0 ? 118 : 112, 1);
}
for(gi = 0; gi < ng; gi++){
int s = gi * 4 * TPS, L = group_len(steps, gi) * TPS;
tmp.n = 0;
divide(&core, s, L, 0, 1, W_KICK, P(10 + (d >> 2)), &tmp, 0, VEL_SUB_KICK);
for(j = 0; j < tmp.n; j++){
int inSn = 0;
for(k = 0; k < nsn; k++) if(snT[k] == tmp.e[j].t) inSn = 1;
if(!(tmp.e[j].t == 0 && fs->noOne) && !inSn) push_ev(ev, &tmp.e[j]);
}
divide(&vr, s, L, 0, 1, W_GHOST, P(d >> 3), ev, 1, VEL_SUB_GHOST);
if(gi == burst) divide(&hr, s, L, 0, 2, W_BURST, P(90), ev, 2, VEL_SUB_BURST);
else divide(&hr, s, L, 0, 1, W_HAT, P(36 + (hd >> 2)), ev, 2, VEL_SUB_HAT);
divide(&vr, s, L, 0, 1, W_PERC, P(d >> 2), ev, 3, VEL_SUB_PERC);
}
}
static int contains(const int *a, int n, int x){ int i; for(i = 0; i < n; i++) if(a[i] == x) return 1; return 0; }
static void kit_skeleton(uint32_t coreSeed, uint32_t varSeed, int steps, int d, int hd, int pos, const fsets *fs, int frame, evlist *ev){
static const int DIVS[3] = {2,2,1};
sb_rng core = rng(coreSeed), vr = rng(varSeed), hr = rng(mix(varSeed, 0x484154u + (uint32_t)pos));
const skel_t *sk = &SKEL[below(&core, 6)];
int sn[MAX_STEPS], nsn = 0, i, div, hp, ek, rot;
if(frame == 0) for(i = 0; i < sk->ns; i++) sn[nsn++] = sc16(sk->s[i], steps);
else for(i = 0; i < steps; i++) if(fs->S[i]) sn[nsn++] = i;
for(i = 0; i < steps; i++) if(fs->K[i]) push(ev, i * TPS, 0, 116, 1);
for(i = 0; i < sk->nk; i++){
int s = sc16(sk->k[i], steps);
if(sk->k[i] == 0) continue;
if(chance(&core, P(d >> 2))){ int dir = below(&core, 2) ? 1 : -1; s = clamp(s + dir, 1, steps - 1); }
if(!contains(sn, nsn, s)){ int vel = 104 + below(&core, 12); push(ev, s * TPS, 0, vel, 0); }
}
for(i = 0; i < nsn; i++){ int vel = 110 + below(&core, 14); push(ev, sn[i] * TPS, 1, vel, 0); }
for(i = 0; i < sk->ng; i++) if(chance(&vr, P(d))){ int vel = 36 + below(&vr, 24); push(ev, sc16(sk->g[i], steps) * TPS, 1, vel, 0); }
if(chance(&vr, P(d >> 1))){ int xs = 1 + below(&vr, steps - 1); if(!contains(sn, nsn, xs)){ int vel = 90 + below(&vr, 20); push(ev, xs * TPS, 0, vel, 0); } }
div = DIVS[below(&core, 3)]; hp = P(36 + (hd >> 2));
for(i = 0; i < steps; i += div){
if(i % 4 == 0) push(ev, i * TPS, 2, 92, 0);
else if(chance(&hr, hp)){
int vel = 60 + below(&hr, 22);
if(chance(&hr, P(hd >> 3))){ int L = TPS * div; push(ev, i * TPS, 2, vel, 0); push(ev, i * TPS + (L >> 1), 2, vel - 8, 0); }
else push(ev, i * TPS, 2, vel, 0);
}
}
ek = 1 + ((d * steps) >> 8); ek += below(&core, 3); ek = clamp(ek, 1, steps - 1);
rot = below(&core, steps);
for(i = 0; i < steps; i++) if(eu(i, ek, steps, rot)){ int vel = 64 + below(&vr, 40); push(ev, i * TPS, 3, vel, 0); }
}
static void ca_row(int rule, uint8_t *row, int steps, int gens){ /* in place */
uint8_t nx[MAX_STEPS];
int g, i;
for(g = 0; g < gens; g++){
for(i = 0; i < steps; i++){ int l = row[(i + steps - 1) % steps], c = row[i], r = row[(i + 1) % steps]; nx[i] = (uint8_t)((rule >> ((l << 2) | (c << 1) | r)) & 1); }
memcpy(row, nx, (size_t)steps);
}
}
static void rand_row(sb_rng *r, int steps, int p, uint8_t *a){ int i; for(i = 0; i < steps; i++) a[i] = (uint8_t)(chance(r, p) ? 1 : 0); }
static void kit_ca(uint32_t coreSeed, uint32_t varSeed, int steps, int d, int hd, int pos, int sec, const fsets *fs, evlist *ev){
sb_rng core = rng(coreSeed), vr = rng(varSeed), thin = rng(mix(varSeed, 0x5448u + (uint32_t)pos));
int gen = sec * 5 + pos, i, rk, rs, rh, rp;
uint8_t K[MAX_STEPS], S[MAX_STEPS], H[MAX_STEPS], C[MAX_STEPS];
rk = RULES[below(&core, 14)]; rs = RULES[below(&core, 14)]; rh = RULES[below(&vr, 14)]; rp = RULES[below(&vr, 14)];
rand_row(&core, steps, P(40), K); ca_row(rk, K, steps, gen);
rand_row(&core, steps, P(40), S); ca_row(rs, S, steps, gen);
rand_row(&vr, steps, P(64), H); ca_row(rh, H, steps, gen + pos);
rand_row(&vr, steps, P(40), C); ca_row(rp, C, steps, gen);
for(i = 0; i < steps; i++){
int t = i * TPS, back = fs->S[i];
if(back) push(ev, t, 1, 112, 0);
else if(S[i] && chance(&thin, P(d >> 1))){ int vel = 40 + below(&thin, 20); push(ev, t, 1, vel, 0); }
if(fs->K[i]) push(ev, t, 0, 118, 1);
else if(K[i] && !back && !(i == 0 && fs->noOne) && chance(&thin, P(40 + (d >> 1)))){ int vel = 96 + below(&thin, 20); push(ev, t, 0, vel, 0); }
if(H[i] && chance(&thin, P(56 + (hd >> 2)))){ int nb = H[(i + steps - 1) % steps] + H[(i + 1) % steps]; push(ev, t, 2, 58 + nb * 16, 0); }
if(C[i] && !C[(i + 1) % steps] && chance(&thin, P(d))){ int vel = 70 + below(&thin, 40); push(ev, t, 3, vel, 0); }
}
}
/* ---------- AMENIZER ---------- */
/* [step, voice, velocity, main flag]; the listed hits, then the ride from ride() in engine.js */
typedef struct { uint8_t s, v, vel, a; } amen_hit;
#define RIDE8 {0,2,82,1},{2,2,60,0},{4,2,82,1},{6,2,60,0},{8,2,82,1},{10,2,60,0},{12,2,82,1},{14,2,60,0}
static const amen_hit AMEN_A[] = {{0,0,112,1},{2,0,84,0},{10,0,104,1},{11,0,80,0},{4,1,112,1},{7,1,52,0},{9,1,86,1},{12,1,110,1},{15,1,58,0}, RIDE8};
static const amen_hit AMEN_C[] = {{0,0,112,1},{2,0,84,0},{10,0,104,1},{4,1,112,1},{7,1,52,0},{9,1,86,1},{14,1,110,1}, RIDE8};
static const amen_hit AMEN_D[] = {{2,0,104,1},{3,0,84,0},{10,0,108,1},{1,1,48,0},{4,1,112,1},{7,1,54,0},{9,1,88,1},{14,1,112,1},{10,3,110,0},
{0,2,82,1},{2,2,60,0},{4,2,82,1},{6,2,60,0},{8,2,82,1},{12,2,82,1},{14,2,60,0}}; /* crash at 10 replaces the ride */
static const amen_hit *const AMEN[4] = {AMEN_A, AMEN_A, AMEN_C, AMEN_D};
static const int AMEN_N[4] = {sizeof AMEN_A / sizeof AMEN_A[0], sizeof AMEN_A / sizeof AMEN_A[0], sizeof AMEN_C / sizeof AMEN_C[0], sizeof AMEN_D / sizeof AMEN_D[0]};
#define AMEN_SLICE_MAX 16
static void humanize_amen(evlist *ev, uint32_t seed, int bar){
int last[4] = {-1000000,-1000000,-1000000,-1000000}, lastV[4] = {0,0,0,0}, i, rideBias;
sb_rng br = rng(mix(mix(seed, 0x48554D), (uint32_t)bar));
sort_tv(ev);
rideBias = below(&br, 11) - 5;
for(i = 0; i < ev->n; i++){
ev_t *x = &ev->e[i];
int base = x->vel;
uint32_t h = mix(mix(mix(seed, 0x564C), (uint32_t)bar), (uint32_t)(x->t * 4 + x->v));
int v = base + (int)(h % 13) - 6;
if(x->v == 2) v += rideBias;
if(x->v < 2 && x->t - last[x->v] <= 2 * TPS && lastV[x->v] * 10 >= base * 9) v = fdiv(v * 80, 100);
last[x->v] = x->t; lastV[x->v] = base; x->vel = (int16_t)clamp(v, 1, 127);
}
}
static int amen_feel(uint32_t seed, int bar, int t, int v, int a){
uint32_t h = mix(mix(mix(seed, 0x46454C), (uint32_t)bar), (uint32_t)(t * 4 + v));
#define J(n) ((int)(h % (uint32_t)(2 * (n) + 1)) - (n))
if(v == 0) return J(4);
if(v == 1) return a ? 10 + J(5) : J(10);
if(v == 2) return -4 + J(6);
return J(4);
#undef J
}
static int amen_slice(int bar, int beat, amen_hit *out){
const amen_hit *b = AMEN[bar & 3];
int i, n = 0;
for(i = 0; i < AMEN_N[bar & 3]; i++) if(b[i].s >= beat * 4 && b[i].s < beat * 4 + 4) out[n++] = b[i];
return n;
}
static void kit_amen(uint32_t seed, int bar, int pos, int dr, int fill, evlist *ev){
static const int PB[4] = {2,3,5,6};
amen_hit sl[AMEN_SLICE_MAX], prev[AMEN_SLICE_MAX];
int src = fill ? 3 : pos, nprev = -1, j, k; /* nprev < 0: no previous beat yet (JS prev === null) */
for(j = 0; j < 4; j++){
int nsl = amen_slice(src, j, sl), op = -1;
sb_rng r = rng(mix(mix(seed, 0x414D454E), (uint32_t)(bar * 4 + j)));
int pc = fdiv(dr * 80, 127), pb = fdiv(pc * PB[j], 4), keep = (pos == 0 && j == 0) || (j == 0 && dr < 88), base;
if(!keep && chance(&r, P(pb))){ int w[5] = {4,2,2,2,dr > 80 ? 1 : 0}; op = pick_w(&r, w, 5); }
if(op == 0){ int b = below(&r, 4); nsl = amen_slice(b, j, sl); }
else if(op == 4){ int b = below(&r, 4), bt = below(&r, 4); nsl = amen_slice(b, bt, sl); op = 0; }
else if(op == 1 && nprev >= 0){ memcpy(sl, prev, sizeof sl); nsl = nprev; }
base = j * 4 * TPS;
for(k = 0; k < nsl; k++){
const amen_hit *s = &sl[k];
int rel = s->s % 4, t = base + rel * TPS;
if(op == 2){ if(rel < 2){ push(ev, t, s->v, s->vel, s->a); push(ev, t + 2 * TPS, s->v, imax(1, s->vel - 10), s->a); } continue; }
push(ev, t, s->v, s->vel, s->a);
if(op == 3 && s->v == 1) push(ev, t + (TPS >> 1), 1, imax(1, s->vel - 24), 0);
}
memcpy(prev, sl, sizeof prev); nprev = nsl;
}
}
/* ---------- sections, arc, shaping, fill ---------- */
static void transform_kit(evlist *ev, int steps, int sec, uint32_t seed, const fsets *fs){
sb_rng r;
int ops[2] = {0, 0}, nops = 1, i, half = (steps >> 1) * TPS, barT = steps * TPS;
if(sec == 0) return;
r = rng(mix(seed, 0x5345u + (uint32_t)sec));
ops[0] = below(&r, 8);
if(chance(&r, P(64))) ops[nops++] = below(&r, 8);
#define HAS(o) (ops[0] == (o) || (nops > 1 && ops[1] == (o)))
if(HAS(0)){
int lastK = -1;
for(i = 0; i < ev->n; i++) if(ev->e[i].v == 0 && ev->e[i].t > 0) lastK = i;
if(lastK >= 0 && ev->e[lastK].t + TPS < barT) ev->e[lastK].t += TPS;
push(ev, 3 * TPS, 0, 98, 0);
}
if(HAS(1)){
static int hs[SB_MAX_EVENTS]; /* static: keeps the Pico's 2 KB stacks free */
int nh = 0;
for(i = 0; i < ev->n; i++) if(ev->e[i].v == 2 && ev->e[i].t >= half && ev->e[i].t % TPS == 0) hs[nh++] = ev->e[i].t;
for(i = 0; i < nh; i++) if(hs[i] + TPS < barT) push(ev, hs[i] + TPS, 2, 60, 0);
}
if(HAS(2)) FILTER(ev, !(e->t < half && (e->v == 3 || (e->v == 1 && e->vel < 90))));
if(HAS(3)) for(i = 0; i < ev->n; i++) if(ev->e[i].v == 3) ev->e[i].t = (int16_t)((ev->e[i].t + 2 * TPS) % barT);
if(HAS(4)) for(i = 1; i < steps; i++) if(fs->S[i]) push(ev, (i - 1) * TPS, 1, 44, 0);
if(HAS(5)){
int ls = -1;
for(i = 0; i < ev->n; i++) if(ev->e[i].v == 1 && ev->e[i].vel >= 90) ls = i;
if(ls >= 0 && ev->e[ls].t + TPS < barT) ev->e[ls].t += TPS;
}
if(HAS(6)){
int kn = 0;
for(i = 0; i < ev->n; i++) if(ev->e[i].v == 0) kn++;
if(kn > 1) FILTER(ev, !(e->v == 0 && e->t == 0));
}
if(HAS(7)){
FILTER(ev, !(e->v == 1 && e->vel >= 90));
push(ev, (steps >> 1) * TPS, 1, 116, 0);
}
#undef HAS
}
static void arc_kit(evlist *ev, const sb_genome *g, int pos){
if(g->arc == 1 && pos < 2) FILTER(ev, !((e->v == 3 || (e->v == 1 && e->vel < 90)) && (pos == 0 || e->t % (2 * TPS) != 0)));
else if(g->arc == 2 && pos == 2){
int k0 = 1000000000, i;
for(i = 0; i < ev->n; i++) if(ev->e[i].v == 0 && ev->e[i].t < k0) k0 = ev->e[i].t;
FILTER(ev, (e->v == 0 && e->t == k0) || (e->v == 1 && e->vel >= 90) || (e->v == 2 && e->t % (2 * TPS) == 0));
}
}
static void shape_kit(evlist *ev, const sb_genome *g, int pos, int aoff, int jit, sb_rng *jr){
int i;
for(i = 0; i < ev->n; i++){
ev_t *e = &ev->e[i];
int st, sw, w, x, v;
if(e->v < 2 || e->t % TPS != 0) continue;
st = e->t / TPS; sw = st + aoff; w = (sw % 4 == 0) ? 100 : (sw % 2 == 0) ? 86 : 74; x = pos * g->steps + sw;
v = fdiv(e->vel * w, 100);
if(g->accentCycle && (x % g->accentCycle) == 0) v += 24;
if(jit) v += below(jr, 2 * jit + 1) - jit;
e->vel = (int16_t)clamp(v, 1, 127);
}
}
static void kit_fill(evlist *ev, int steps, int e, sb_rng *fr){
static evlist src;
int ng = n_groups(steps), lastGi = ng - 1, st = lastGi * 4 * TPS, L = group_len(steps, lastGi) * TPS, i, type, ps;
type = below(fr, 4);
src = *ev; ev->n = 0;
for(i = 0; i < src.n; i++) if(src.e[i].t < st) push_ev(ev, &src.e[i]);
if(type == 0){
int m = 4 + fdiv(e, 32);
push(ev, st, 0, 110, 0);
for(i = 0; i < m; i++){ int q = m - i; push(ev, st + L - L * q * q / (m * m), 1, 90 + 30 * i / m, 0); }
}
else if(type == 1){
int k = 0, t;
push(ev, st, 0, 110, 0);
for(t = st; t < st + L; t += (TPS >> 1), k++) push(ev, t, (k % 2) ? 3 : 1, (k % 2) ? 70 + k * 4 : 92 + k * 3, 0);
}
else if(type == 2){ push(ev, st, 0, 116, 0); push(ev, st + L - TPS, 1, 100, 0); push(ev, st + L - (TPS >> 1), 1, 116, 0); }
else {
ps = (ng > 1 ? ng - 2 : lastGi) * 4 * TPS;
for(i = 0; i < src.n; i++){
const ev_t *x = &src.e[i];
if(x->t >= ps && x->t < ps + (L >> 1)){ push(ev, st + (x->t - ps), x->v, x->vel, 0); push(ev, st + (L >> 1) + (x->t - ps), x->v, clamp(x->vel + 10, 1, 127), 0); }
}
}
}
/* ---------- synth tracks ---------- */
static int chord_deg(const sb_genome *g, int pos){ return g->prog[pos % PHRASE_BARS]; }
static int deg_pitch(const sb_genome *g, int base, int idx){ int n = SCALE_LEN[g->scale]; return base + g->root + 12 * fdiv(idx, n) + SCALES[g->scale][mod(idx, n)]; }
static int mel_pitch(const sb_genome *g, int idx){ int p = deg_pitch(g, 60, idx); while(p < 60) p += 12; while(p > 96) p -= 12; return p; }
static int is_chord_tone(const sb_genome *g, int pos, int idx){ int n = SCALE_LEN[g->scale], c = chord_deg(g, pos), x = mod(idx - c, n); return x == 0 || x == 2 || x == 4; }
static int nearest_chord_tone(const sb_genome *g, int pos, int idx){
int d;
for(d = 0; d < 4; d++){ if(is_chord_tone(g, pos, idx - d)) return idx - d; if(is_chord_tone(g, pos, idx + d)) return idx + d; }
return idx;
}
typedef struct { int pos[5], cont[5], n, resp; } motif_t;
static void make_motif(uint32_t rs, const sb_genome *g, motif_t *M){
static const int STP[6] = {-2,-1,1,2,-4,4}, STP_W[6] = {3,4,4,3,1,1};
sb_rng r = rng(mix(rs, 0x4D4F54));
uint8_t used[8] = {1,0,0,0,0,0,0,0};
int np = 1, i, j;
M->n = g->motifLen; M->pos[0] = 0; M->cont[0] = 0;
if(chance(&r, P(40))){ M->pos[0] = below(&r, 2) ? 1 : 2; memset(used, 0, sizeof used); used[M->pos[0]] = 1; }
while(np < M->n){ int p = 1 + below(&r, 7); if(!used[p]){ used[p] = 1; M->pos[np++] = p; } }
for(i = 1; i < M->n; i++){ int x = M->pos[i]; for(j = i - 1; j >= 0 && M->pos[j] > x; j--) M->pos[j + 1] = M->pos[j]; M->pos[j + 1] = x; }
for(i = 1; i < M->n; i++) M->cont[i] = M->cont[i - 1] + STP[pick_w(&r, STP_W, 6)];
M->resp = below(&r, 3);
}
static void sort_dedupe(evlist *l){
int i, o = 0;
sort_tv(l);
for(i = 0; i < l->n; i++){
if(o > 0 && l->e[o - 1].t == l->e[i].t && l->e[o - 1].v == l->e[i].v){ if(l->e[i].vel > l->e[o - 1].vel) l->e[o - 1].vel = l->e[i].vel; }
else l->e[o++] = l->e[i];
}
l->n = o;
for(i = 0; i < l->n; i++) l->e[i].vel = (int16_t)clamp(l->e[i].vel, 1, 127);
}
static void one_at_a_time(evlist *l){
int i, o = 0;
sort_dedupe(l);
for(i = 0; i < l->n; i++){ if(o > 0 && l->e[o - 1].t == l->e[i].t) l->e[o - 1] = l->e[i]; else l->e[o++] = l->e[i]; }
l->n = o;
}
typedef struct { const sb_genome *g; const motif_t *M; sb_rng *vr; evlist *mel; int steps, d, pos, fill, last0; } place_ctx;
static void place(const place_ctx *c, int start, const int *cont, int ncont, int anchor, int velBase){
const sb_genome *g = c->g;
int k;
for(k = 0; k < c->M->n && k < ncont; k++){
int s = start + c->M->pos[k], idx, vel;
if(s >= c->steps) break;
if(c->fill && s >= c->last0) break;
idx = anchor + cont[k];
if(s % 4 == 0) idx = nearest_chord_tone(g, c->pos, idx);
if(chance(c->vr, P(c->d >> 3))) continue;
vel = velBase + (s % 4 == 0 ? 18 : 0) + below(c->vr, 12);
push(c->mel, s * TPS, mel_pitch(g, idx), vel, 0);
if(k + 1 < c->M->n && k + 1 < ncont && chance(c->vr, P(c->d >> 2))){
int s2 = start + c->M->pos[k + 1];
if(s2 - s >= 2 && (s + 1) < c->steps) push(c->mel, (s + 1) * TPS, mel_pitch(g, idx + (cont[k + 1] > cont[k] ? 1 : -1)), velBase - 14, 0);
}
}
}
static int bp(const sb_genome *g, int idx){ int p = deg_pitch(g, 36, idx); while(p > 47) p -= 12; while(p < 36) p += 12; return p; }
static void line_tracks(uint32_t rs, int variant, const sb_genome *g, int steps, int d, int sec, int pos, const evlist *kit, int fill,
evlist *mel, evlist *bass, evlist *pad){
sb_rng core = rng(mix(rs, 0x4D4Fu + (uint32_t)sec)), vr = rng(mix(mix(rs, 0x5641u + (uint32_t)sec), (uint32_t)variant));
int n = SCALE_LEN[g->scale], barT = steps * TPS, i, j;
int ng = n_groups(steps), last0 = (ng - 1) * 4, last1 = group_len(steps, ng - 1);
static int kickList[SB_MAX_EVENTS]; /* static: keeps the Pico's 2 KB stacks free */
int nk = 0, c = chord_deg(g, pos), anchor, half, rot, nextC, tones[3];
uint8_t seen[128];
motif_t M;
place_ctx pc;
mel->n = bass->n = pad->n = 0;
for(i = 0; i < kit->n; i++) if(kit->e[i].v == 0 && kit->e[i].t % TPS == 0) kickList[nk++] = kit->e[i].t;
make_motif(rs, g, &M);
pc.g = g; pc.M = &M; pc.vr = &vr; pc.mel = mel; pc.steps = steps; pc.d = d; pc.pos = pos; pc.fill = fill; pc.last0 = last0;
anchor = c + (chance(&core, P(50)) ? 2 : 0); half = steps >> 1;
place(&pc, 0, M.cont, M.n, anchor, 82);
if(d >= 40 || steps >= 16){
int c2[5], nc2 = M.n, a2 = anchor, st2 = half;
memcpy(c2, M.cont, sizeof c2);
if(M.resp == 0) a2 = anchor + 2;
else if(M.resp == 1) for(j = 0; j < nc2; j++) c2[j] = -c2[j];
else st2 = half + 1;
if(pos == PHRASE_BARS - 1) nc2 = imax(1, nc2 - 1);
place(&pc, st2, c2, nc2, a2, 74);
}
if(pos == PHRASE_BARS - 1 && !fill){
int cs = imax(0, steps - 4);
FILTER(mel, e->t < cs * TPS);
push(mel, cs * TPS, mel_pitch(g, chord_deg(g, 0)), 96, 0);
}
if(fill){
int nxt = chord_deg(g, 0) + n, k2 = 0, tt;
for(tt = last0 * TPS; tt < (last0 + last1) * TPS; tt += TPS, k2++) push(mel, tt, mel_pitch(g, nxt - (last1 - 1 - k2)), 76 + k2 * 8, 0);
}
rot = g->rot * TPS;
for(i = 0; i < mel->n; i++) mel->e[i].t = (int16_t)((mel->e[i].t + rot) % barT);
one_at_a_time(mel);
for(i = 0; i < mel->n; i++) mel->e[i].tr = (int16_t)(((g->hocket >> (i & 7)) & 1) ? SB_T_MELA : SB_T_MELB);
nextC = chord_deg(g, pos + 1);
for(i = 0; i < nk; i++){
int t = kickList[i], idx = c;
if(t > 0 && (i % 2) == 1 && chance(&core, P(70))) idx = c + g->bassDeg;
push_tr(bass, t, bp(g, idx), t == 0 ? 112 : 98, 0, SB_T_BASS);
}
if(steps >= 8 && chance(&core, P(60))){
int at = (steps - 1) * TPS;
if(!contains(kickList, nk, at)){ int dir = below(&core, 2) ? 1 : -1; push_tr(bass, at, bp(g, nextC + dir), 84, 0, SB_T_BASS); }
}
tones[0] = c; tones[1] = c + 2; tones[2] = c + 4;
memset(seen, 0, sizeof seen);
for(i = 0; i < 3; i++){ int p = 48 + mod(deg_pitch(g, 0, tones[i]), 12); if(!seen[p]){ seen[p] = 1; push_tr(pad, 0, p, i == 0 ? 58 : 50, 0, SB_T_PAD); } }
one_at_a_time(bass);
}
static int kit_track(const ev_t *e){
if(e->v == 0) return (e->a || e->t == 0) ? SB_T_KICK : SB_T_KICK2;
if(e->v == 1) return (e->a || e->vel >= 90) ? SB_T_SNARE : SB_T_GHOST;
if(e->v == 2) return (e->a || e->vel >= 85) ? SB_T_HAT : SB_T_HAT16;
return SB_T_PERC;
}
static int groove(int t, const sb_genome *g, int isSnare, int barTicks, int swing){
if(t % TPS == 0 && (t / TPS) % 2 == 1) t += swing;
if(isSnare && g->layback) t += 12;
return imin(t, barTicks - 1);
}
/* ---------- drift ---------- */
static int amt(int d, int a, int b){ return clamp(fdiv((d - a) * 127, b - a), 0, 127); }
static int nudge(uint32_t barSeed, int t, int grp, int micro){
int mx, n;
uint32_t h;
if(!micro || grp == 5) return 0;
mx = fdiv(micro * (grp == 4 ? 30 : 48), 127);
h = mix(mix(barSeed, (uint32_t)t), (uint32_t)grp);
n = (int)(h % (uint32_t)(2 * mx + 1)) - mx;
if(grp == 2) n += fdiv(micro * 10, 127);
if(grp == 1) n -= fdiv(micro * 8, 127);
return n;
}
enum { SG_HAT, SG_PERC, SG_GHOST, SG_KICK2, SG_SNARE };
static int shift_test(const ev_t *x, int which){
switch(which){
case SG_HAT: return x->v == 2;
case SG_PERC: return x->v == 3;
case SG_GHOST: return x->v == 1 && x->vel < 90;
case SG_KICK2: return x->v == 0 && !x->a && x->t > 0;
default: return x->v == 1 && x->vel >= 90;
}
}
static void shift_group(evlist *ev, int which, int by, int barT){
int i;
for(i = 0; i < ev->n; i++) if(shift_test(&ev->e[i], which)) ev->e[i].t = (int16_t)mod(ev->e[i].t + by, barT);
}
static void triplet_warp(evlist *ev, int beat){
int s = beat * TPB, i;
for(i = 0; i < ev->n; i++){
ev_t *e = &ev->e[i];
if(e->t >= s && e->t < s + TPB){ int k = imin(2, fdiv((e->t - s) * 3 + TPB / 2, TPB)); e->t = (int16_t)(s + k * 280); }
}
}
/* ---------- render ---------- */
static void emit(sb_bar *out, int t, int lane, int v, int vel, int len, int tr){
sb_event *e;
if(out->n >= SB_MAX_EVENTS){ g_overflow = 1; return; }
e = &out->ev[out->n++];
e->t = (int16_t)t; e->lane = (uint8_t)lane; e->v = (uint8_t)v; e->vel = (uint8_t)vel; e->len = (int16_t)len; e->tr = (uint8_t)tr;
}
typedef struct { const sb_genome *g; sb_bar *out; uint32_t barSeed; int barTicks, swingBar, micro, melShift; } mono_ctx;
enum { LEN_MEL, LEN_BASS };
static void mono(const mono_ctx *c, const evlist *list, int lenRule){
const sb_genome *g = c->g;
int k;
for(k = 0; k < list->n; k++){
const ev_t *cur = &list->e[k], *nx = k + 1 < list->n ? &list->e[k + 1] : 0;
int gap = (nx ? nx->t : c->barTicks) - cur->t, len, t0;
if(lenRule == LEN_BASS) len = gap - 10;
else if(g->monoLen == 0) len = imin(90, gap - 5);
else if(g->monoLen == 1) len = imax(30, fdiv(gap, 2));
else len = (nx && nx->v != cur->v) ? gap + 20 : gap - 5;
t0 = cur->t;
if(c->melShift && (cur->tr == SB_T_MELA || cur->tr == SB_T_MELB)) t0 = mod(t0 + c->melShift, c->barTicks);
emit(c->out, clamp(groove(t0, g, 0, c->barTicks, c->swingBar) + nudge(c->barSeed, cur->t, NUDGE_GRP[cur->tr], c->micro), 0, c->barTicks - 1),
1, cur->v, cur->vel, imax(10, len - c->swingBar), cur->tr);
}
}
static void sort_out(sb_bar *out){ /* stable, by (t, tr, v) */
int i, j;
for(i = 1; i < out->n; i++){
sb_event x = out->ev[i];
for(j = i - 1; j >= 0; j--){
const sb_event *y = &out->ev[j];
if(y->t > x.t || (y->t == x.t && (y->tr > x.tr || (y->tr == x.tr && y->v > x.v)))) out->ev[j + 1] = out->ev[j];
else break;
}
out->ev[j + 1] = x;
}
}
void sb_render_bar(const sb_state *st, const sb_params *p, int bar, sb_bar *out){
static evlist kit, mel, bass, pad, melA, melB;
sb_genome g;
fsets fs;
sb_rng fr, dRng;
mono_ctx mc;
uint32_t barSeed, krs, cS, vS;
int amen = p->gen == 4, pos, phrase, sec, barTicks, i, j, r, fill;
int dr, dVar, micro, wob, slip, disp, warp, e, arcD, kd, hd, warpBeat, melShift, swingBar, variants[2];
g_overflow = 0;
sb_genome_make(st->seed, &g);
sb_apply_key(&g, p);
if(amen){ g.steps = 16; g.swing = 0; g.layback = 0; }
pos = bar % PHRASE_BARS; phrase = bar / PHRASE_BARS; sec = g.form[pos]; barTicks = g.steps * TPS;
fr = rng(mix(mix(st->seed, 0x46494C), (uint32_t)phrase));
fill = p->fill ? 1 : (!amen && pos == PHRASE_BARS - 1 && chance(&fr, P(g.fillP >> 1)));
dr = clamp(p->drift, 0, 127); dVar = imin(127, dr * 3);
micro = amt(dr, 20, 127); wob = amt(dr, 40, 127); slip = amt(dr, 55, 127); disp = amt(dr, 80, 127); warp = amt(dr, 105, 127);
if(amen){ micro = fdiv(micro, 2); wob = fdiv(wob, 2); slip = fdiv(slip, 2); disp = fdiv(amt(dr, 90, 127), 2); warp = amt(dr, 115, 127); }
dRng = rng(mix(mix(st->seed, 0x445249), (uint32_t)bar)); barSeed = mix(st->seed, 0x4E55u + (uint32_t)bar);
for(r = 0; r < 2; r++){
variants[r] = 0;
if(phrase > 0){ sb_rng vrng = rng(mix(mix(role_seed(st, r), 0xD1F7), (uint32_t)phrase)); if(chance(&vrng, P(dVar))) variants[r] = phrase; }
}
e = clamp(p->energy, 0, 127); krs = role_seed(st, 0); cS = mix(krs, 0x434F); vS = mix(mix(krs, 0x5641u + (uint32_t)sec), (uint32_t)variants[0]);
arcD = (g.arc == 1) ? (pos * 10 - 15) : 0;
kd = clamp(g.dens[0] + e - 64 + arcD, 0, 127); hd = clamp(g.dens[0] + fdiv(e - 64, 4) + arcD, 0, 127);
frame_sets(&g, g.steps, pos, &fs);
kit.n = 0;
if(amen){
kit_amen(krs, bar, pos, dr, fill, &kit);
humanize_amen(&kit, krs, bar);
if(slip){ int jt = fdiv(slip * 24, 127); for(i = 0; i < kit.n; i++){ int dv = below(&dRng, 2 * jt + 1) - jt; kit.e[i].vel = (int16_t)clamp(kit.e[i].vel + dv, 1, 127); } }
} else {
int aoff = 0;
if(p->gen == 0) kit_euclid(cS, vS, g.steps, kd, hd, pos, &fs, &kit);
else if(p->gen == 1) kit_skeleton(cS, vS, g.steps, kd, hd, pos, &fs, g.frame, &kit);
else if(p->gen == 2) kit_subdiv(cS, vS, g.steps, kd, hd, pos, &fs, &kit);
else kit_ca(cS, vS, g.steps, kd, hd, pos, sec, &fs, &kit);
transform_kit(&kit, g.steps, sec, krs, &fs);
arc_kit(&kit, &g, pos);
if(fill) kit_fill(&kit, g.steps, p->energy, &fr);
if(chance(&dRng, P(slip))) aoff = 1 + below(&dRng, 3);
shape_kit(&kit, &g, pos, aoff, fdiv(slip * 24, 127), &dRng);
}
if(disp){
static const int DIRS[2] = {TPS, -TPS};
if(chance(&dRng, P(disp))) shift_group(&kit, SG_HAT, DIRS[below(&dRng, 2)], barTicks);
if(chance(&dRng, P(disp))) shift_group(&kit, SG_PERC, DIRS[below(&dRng, 2)], barTicks);
if(chance(&dRng, P(disp >> 1))) shift_group(&kit, SG_GHOST, DIRS[below(&dRng, 2)], barTicks);
if(chance(&dRng, P(disp >> 1))) shift_group(&kit, SG_KICK2, DIRS[below(&dRng, 2)], barTicks);
if(chance(&dRng, P(disp >> 2))) shift_group(&kit, SG_SNARE, TPS, barTicks);
}
warpBeat = -1;
if(warp && chance(&dRng, P(warp))) warpBeat = below(&dRng, imax(1, fdiv(g.steps, 4)));
if(warpBeat >= 0) triplet_warp(&kit, warpBeat);
sort_dedupe(&kit);
melShift = 0;
if(disp && chance(&dRng, P(disp >> 1))) melShift = below(&dRng, 2) ? TPS : -TPS;
swingBar = g.swing;
if(wob){ int w = fdiv(wob * 40, 127); swingBar = clamp(g.swing + below(&dRng, 2 * w + 1) - w, 0, 90); }
line_tracks(role_seed(st, 1), variants[1], &g, g.steps, clamp(g.dens[1] + e - 64, 0, 127), sec, pos, &kit, fill, &mel, &bass, &pad);
out->n = 0;
for(i = 0; i < kit.n; i++){
const ev_t *k = &kit.e[i];
int tr = kit_track(k), t = k->t, nt = barTicks, len, ft;
if(tr == SB_T_PERC) t = (t + g.rot * TPS) % barTicks;
for(j = i + 1; j < kit.n; j++) if(kit.e[j].v == k->v){ nt = kit.e[j].t; break; }
len = imax(10, imin(105, nt > t ? nt - t : 105));
ft = groove(t, &g, tr == SB_T_SNARE, barTicks, swingBar) + nudge(barSeed, t, NUDGE_GRP[tr], micro) + (amen ? amen_feel(krs, bar, t, k->v, k->a) : 0);
emit(out, clamp(ft, 0, barTicks - 1), 0, k->v, k->vel, len, tr);
}
mc.g = &g; mc.out = out; mc.barSeed = barSeed; mc.barTicks = barTicks; mc.swingBar = swingBar; mc.micro = micro; mc.melShift = melShift;
melA.n = melB.n = 0;
for(i = 0; i < mel.n; i++) push_ev(mel.e[i].tr == SB_T_MELA ? &melA : &melB, &mel.e[i]);
mono(&mc, &melA, LEN_MEL); mono(&mc, &melB, LEN_MEL);
mono(&mc, &bass, LEN_BASS);
for(i = 0; i < pad.n; i++) emit(out, 0, 1, pad.e[i].v, pad.e[i].vel, barTicks - 30, SB_T_PAD);
sort_out(out);
out->bar = bar; out->pos = pos; out->phrase = phrase; out->sec = sec; out->fill = fill;
out->variants[0] = variants[0]; out->variants[1] = variants[1]; out->barTicks = barTicks; out->steps = g.steps;
out->overflow = g_overflow;
}
uint32_t sb_checksum_event(uint32_t h, const sb_event *e){
int f[6], i;
f[0] = e->t; f[1] = e->lane; f[2] = e->v; f[3] = e->vel; f[4] = e->len; f[5] = e->tr;
for(i = 0; i < 6; i++) h = (h ^ ((uint32_t)f[i] & 0xFFFFu)) * 0x01000193u;
return h;
}

67
firmware/engine/engine.h Normal file
View File

@@ -0,0 +1,67 @@
/* Seedbox engine: C port of sim/engine.js. Portable C99, no SDK, no allocation, integer-only.
sim/engine.js is the reference; this file must reproduce test/vectors bit for bit.
Not reentrant: render from one core only (scratch buffers are static). */
#ifndef SEEDBOX_ENGINE_H
#define SEEDBOX_ENGINE_H
#include <stdint.h>
#define SB_TPB 840 /* ticks per quarter */
#define SB_TPS 210 /* ticks per 16th */
#define SB_CLOCK_TICKS 35 /* MIDI clock every 35 ticks */
#define SB_PHRASE_BARS 4
#define SB_NT 11 /* tracks */
#define SB_NSCALES 21
#define SB_MAX_EVENTS 128 /* per bar and per scratch list; the vectors peak at 54 */
enum { SB_T_KICK, SB_T_KICK2, SB_T_SNARE, SB_T_GHOST, SB_T_HAT, SB_T_HAT16, SB_T_PERC,
SB_T_BASS, SB_T_MELA, SB_T_MELB, SB_T_PAD };
enum { SB_GEN_EUCLID, SB_GEN_SKELETON, SB_GEN_SUBDIV, SB_GEN_AUTOMATA, SB_GEN_AMENIZER, SB_NGEN };
/* hash and RNG */
uint32_t sb_mix(uint32_t a, uint32_t b);
typedef struct { uint32_t s; } sb_rng;
void sb_rng_init(sb_rng *r, uint32_t seed);
uint32_t sb_rng_next(sb_rng *r);
int sb_rng_below(sb_rng *r, int n);
int sb_rng_chance(sb_rng *r, int p);
/* genome: field names follow engine.js */
typedef struct {
int steps;
int form_idx; /* index into FORMS; form[] is a copy */
int form[4];
int scale, root;
int dens[2];
int fillP, monoLen, monoRange, rot, hocket, bassDeg;
int prog_idx; /* index into PROGS; prog[] is a copy */
int prog[4];
int swing, arc, motifLen, accentCycle, layback, frame, freeK, freeRot;
} sb_genome;
void sb_genome_make(uint32_t seed, sb_genome *g);
const char *sb_form_name(int form_idx);
typedef struct { uint32_t seed; uint32_t salts[2]; } sb_state;
typedef struct { int gen, energy, drift, fill, scale, root; } sb_params; /* scale, root: -1 = AUTO (the seed picks) */
/* one output row: [t, lane, v, vel, len, tr] */
typedef struct { int16_t t, len; uint8_t lane, v, vel, tr; } sb_event;
typedef struct {
int bar, pos, phrase, sec, fill, variants[2], barTicks, steps;
int overflow; /* nonzero if any list hit SB_MAX_EVENTS (should never happen) */
int n;
sb_event ev[SB_MAX_EVENTS];
} sb_bar;
/* ROOT / SCALE pages: overrides the genome's scale and root when they're >= 0 (app.js applyKey). */
void sb_apply_key(sb_genome *g, const sb_params *p);
const char *sb_scale_name(int scale); /* screen name, at most 10 characters */
void sb_render_bar(const sb_state *st, const sb_params *p, int bar, sb_bar *out);
/* FNV-1a over rows, each field & 0xFFFF. Feed a whole case's events in order, starting from SB_FNV_INIT. */
#define SB_FNV_INIT 0x811C9DC5u
uint32_t sb_checksum_event(uint32_t h, const sb_event *e);
#endif

View File

@@ -0,0 +1,121 @@
# This is a copy of <PICO_SDK_PATH>/external/pico_sdk_import.cmake
# This can be dropped into an external project to help locate this SDK
# It should be include()ed prior to project()
# Copyright 2020 (c) 2020 Raspberry Pi (Trading) Ltd.
#
# Redistribution and use in source and binary forms, with or without modification, are permitted provided that the
# following conditions are met:
#
# 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following
# disclaimer.
#
# 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following
# disclaimer in the documentation and/or other materials provided with the distribution.
#
# 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products
# derived from this software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
# INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
# WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
# THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
if (DEFINED ENV{PICO_SDK_PATH} AND (NOT PICO_SDK_PATH))
set(PICO_SDK_PATH $ENV{PICO_SDK_PATH})
message("Using PICO_SDK_PATH from environment ('${PICO_SDK_PATH}')")
endif ()
if (DEFINED ENV{PICO_SDK_FETCH_FROM_GIT} AND (NOT PICO_SDK_FETCH_FROM_GIT))
set(PICO_SDK_FETCH_FROM_GIT $ENV{PICO_SDK_FETCH_FROM_GIT})
message("Using PICO_SDK_FETCH_FROM_GIT from environment ('${PICO_SDK_FETCH_FROM_GIT}')")
endif ()
if (DEFINED ENV{PICO_SDK_FETCH_FROM_GIT_PATH} AND (NOT PICO_SDK_FETCH_FROM_GIT_PATH))
set(PICO_SDK_FETCH_FROM_GIT_PATH $ENV{PICO_SDK_FETCH_FROM_GIT_PATH})
message("Using PICO_SDK_FETCH_FROM_GIT_PATH from environment ('${PICO_SDK_FETCH_FROM_GIT_PATH}')")
endif ()
if (DEFINED ENV{PICO_SDK_FETCH_FROM_GIT_TAG} AND (NOT PICO_SDK_FETCH_FROM_GIT_TAG))
set(PICO_SDK_FETCH_FROM_GIT_TAG $ENV{PICO_SDK_FETCH_FROM_GIT_TAG})
message("Using PICO_SDK_FETCH_FROM_GIT_TAG from environment ('${PICO_SDK_FETCH_FROM_GIT_TAG}')")
endif ()
if (PICO_SDK_FETCH_FROM_GIT AND NOT PICO_SDK_FETCH_FROM_GIT_TAG)
set(PICO_SDK_FETCH_FROM_GIT_TAG "master")
message("Using master as default value for PICO_SDK_FETCH_FROM_GIT_TAG")
endif()
set(PICO_SDK_PATH "${PICO_SDK_PATH}" CACHE PATH "Path to the Raspberry Pi Pico SDK")
set(PICO_SDK_FETCH_FROM_GIT "${PICO_SDK_FETCH_FROM_GIT}" CACHE BOOL "Set to ON to fetch copy of SDK from git if not otherwise locatable")
set(PICO_SDK_FETCH_FROM_GIT_PATH "${PICO_SDK_FETCH_FROM_GIT_PATH}" CACHE FILEPATH "location to download SDK")
set(PICO_SDK_FETCH_FROM_GIT_TAG "${PICO_SDK_FETCH_FROM_GIT_TAG}" CACHE FILEPATH "release tag for SDK")
if (NOT PICO_SDK_PATH)
if (PICO_SDK_FETCH_FROM_GIT)
include(FetchContent)
set(FETCHCONTENT_BASE_DIR_SAVE ${FETCHCONTENT_BASE_DIR})
if (PICO_SDK_FETCH_FROM_GIT_PATH)
get_filename_component(FETCHCONTENT_BASE_DIR "${PICO_SDK_FETCH_FROM_GIT_PATH}" REALPATH BASE_DIR "${CMAKE_SOURCE_DIR}")
endif ()
FetchContent_Declare(
pico_sdk
GIT_REPOSITORY https://github.com/raspberrypi/pico-sdk
GIT_TAG ${PICO_SDK_FETCH_FROM_GIT_TAG}
)
if (NOT pico_sdk)
message("Downloading Raspberry Pi Pico SDK")
# GIT_SUBMODULES_RECURSE was added in 3.17
if (${CMAKE_VERSION} VERSION_GREATER_EQUAL "3.17.0")
FetchContent_Populate(
pico_sdk
QUIET
GIT_REPOSITORY https://github.com/raspberrypi/pico-sdk
GIT_TAG ${PICO_SDK_FETCH_FROM_GIT_TAG}
GIT_SUBMODULES_RECURSE FALSE
SOURCE_DIR ${FETCHCONTENT_BASE_DIR}/pico_sdk-src
BINARY_DIR ${FETCHCONTENT_BASE_DIR}/pico_sdk-build
SUBBUILD_DIR ${FETCHCONTENT_BASE_DIR}/pico_sdk-subbuild
)
else ()
FetchContent_Populate(
pico_sdk
QUIET
GIT_REPOSITORY https://github.com/raspberrypi/pico-sdk
GIT_TAG ${PICO_SDK_FETCH_FROM_GIT_TAG}
SOURCE_DIR ${FETCHCONTENT_BASE_DIR}/pico_sdk-src
BINARY_DIR ${FETCHCONTENT_BASE_DIR}/pico_sdk-build
SUBBUILD_DIR ${FETCHCONTENT_BASE_DIR}/pico_sdk-subbuild
)
endif ()
set(PICO_SDK_PATH ${pico_sdk_SOURCE_DIR})
endif ()
set(FETCHCONTENT_BASE_DIR ${FETCHCONTENT_BASE_DIR_SAVE})
else ()
message(FATAL_ERROR
"SDK location was not specified. Please set PICO_SDK_PATH or set PICO_SDK_FETCH_FROM_GIT to on to fetch from git."
)
endif ()
endif ()
get_filename_component(PICO_SDK_PATH "${PICO_SDK_PATH}" REALPATH BASE_DIR "${CMAKE_BINARY_DIR}")
if (NOT EXISTS ${PICO_SDK_PATH})
message(FATAL_ERROR "Directory '${PICO_SDK_PATH}' not found")
endif ()
set(PICO_SDK_INIT_CMAKE_FILE ${PICO_SDK_PATH}/pico_sdk_init.cmake)
if (NOT EXISTS ${PICO_SDK_INIT_CMAKE_FILE})
message(FATAL_ERROR "Directory '${PICO_SDK_PATH}' does not appear to contain the Raspberry Pi Pico SDK")
endif ()
set(PICO_SDK_PATH ${PICO_SDK_PATH} CACHE PATH "Path to the Raspberry Pi Pico SDK" FORCE)
include(${PICO_SDK_INIT_CMAKE_FILE})

2
firmware/test/.gitignore vendored Normal file
View File

@@ -0,0 +1,2 @@
check_vectors
check_vectors_san

25
firmware/test/Makefile Normal file
View File

@@ -0,0 +1,25 @@
# Host build of the engine and the vector checker. No SDK.
# make test build and check against ../../test/vectors
# make sanitize same, with ASan + UBSan
CC ?= cc
CFLAGS ?= -std=c99 -O2 -Wall -Wextra -Wpedantic -Wshadow -Wconversion -Wno-sign-conversion -Werror
SRC = check_vectors.c json.c ../engine/engine.c
HDR = json.h ../engine/engine.h
VECTORS = ../../test/vectors
check_vectors: $(SRC) $(HDR)
$(CC) $(CFLAGS) -o $@ $(SRC)
check_vectors_san: $(SRC) $(HDR)
$(CC) $(CFLAGS) -O1 -g -fsanitize=address,undefined -fno-sanitize-recover=all -o $@ $(SRC)
test: check_vectors
./check_vectors $(VECTORS)
sanitize: check_vectors_san
./check_vectors_san $(VECTORS)
clean:
rm -f check_vectors check_vectors_san
.PHONY: test sanitize clean

View File

@@ -0,0 +1,162 @@
/* Host test runner: checks firmware/engine against test/vectors (the same files check-vectors.js uses).
Usage: check_vectors [vectors-dir] (default ../../test/vectors). Exit 0 = every check passes. */
#define _POSIX_C_SOURCE 200809L /* strdup, opendir */
#include "../engine/engine.h"
#include "json.h"
#include <dirent.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
static int fail, n, max_events;
static uint32_t hex(const jval *v){ return (uint32_t)strtoul(v->str, NULL, 16); }
static long long num(const jval *v){ return v ? v->num : -999999; }
static void check_primitives(const char *dir){
char path[1024];
jval *root, *a;
int i, j;
snprintf(path, sizeof path, "%s/primitives.json", dir);
root = json_parse_file(path);
if(!root){ printf("cannot read %s\n", path); fail++; return; }
a = json_get(root, "mix");
for(i = 0; i < a->n; i++){
jval *v = a->items[i];
uint32_t got = sb_mix(hex(json_get(v, "a")), (uint32_t)(int32_t)num(json_get(v, "b")));
n++;
if(got != hex(json_get(v, "out"))){ fail++; printf("mix %s %lld differs: got 0x%08X\n", json_get(v, "a")->str, num(json_get(v, "b")), got); }
}
a = json_get(root, "rng");
for(i = 0; i < a->n; i++){
jval *v = a->items[i], *seq = json_get(v, "next"), *bl = json_get(v, "below_1_to_16"), *ch = json_get(v, "chance_i_times_4096");
uint32_t seed = hex(json_get(v, "seed"));
sb_rng r;
n++;
sb_rng_init(&r, seed);
for(j = 0; j < seq->n; j++) if(sb_rng_next(&r) != hex(seq->items[j])){ fail++; printf("rng 0x%08X next step %d differs\n", seed, j); break; }
n++;
sb_rng_init(&r, seed);
for(j = 0; j < bl->n; j++) if(sb_rng_below(&r, j + 1) != num(bl->items[j])){ fail++; printf("rng 0x%08X below step %d differs\n", seed, j); break; }
n++;
sb_rng_init(&r, seed);
for(j = 0; j < ch->n; j++) if(sb_rng_chance(&r, j * 4096) != num(ch->items[j])){ fail++; printf("rng 0x%08X chance step %d differs\n", seed, j); break; }
}
a = json_get(root, "genomes");
for(i = 0; i < a->n; i++){
jval *v = a->items[i], *gj = json_get(v, "genome");
uint32_t seed = hex(json_get(v, "seed"));
sb_genome g;
int bad = 0, k;
n++;
sb_genome_make(seed, &g);
for(k = 0; k < gj->n; k++){
jval *f = gj->items[k];
const char *key = f->key;
#define INT(name) else if(!strcmp(key, #name)){ if(num(f) != g.name) bad = 1; }
#define ARR(name, len) else if(!strcmp(key, #name)){ if(f->n != len) bad = 1; else for(j = 0; j < len; j++) if(num(f->items[j]) != g.name[j]) bad = 1; }
if(!strcmp(key, "formName")){ if(strcmp(f->str, sb_form_name(g.form_idx))) bad = 1; }
INT(steps) INT(scale) INT(root) INT(fillP) INT(monoLen) INT(monoRange) INT(rot) INT(hocket) INT(bassDeg)
INT(swing) INT(arc) INT(motifLen) INT(accentCycle) INT(layback) INT(frame) INT(freeK) INT(freeRot)
ARR(form, 4) ARR(prog, 4) ARR(dens, 2)
else { printf("genome field %s is not checked by the C runner\n", key); bad = 1; }
#undef INT
#undef ARR
if(bad){ printf("genome 0x%08X differs at %s\n", seed, key); break; }
}
if(bad) fail++;
}
json_free(root);
}
static int cmp_str(const void *a, const void *b){ return strcmp(*(char *const *)a, *(char *const *)b); }
static void print_event(const char *tag, const sb_event *e){ printf(" %s [%d,%d,%d,%d,%d,%d]\n", tag, e->t, e->lane, e->v, e->vel, e->len, e->tr); }
static void check_case(const jval *c){
jval *bars = json_get(c, "bars"), *salts = json_get(c, "salts"), *fb = json_get(c, "fillBars");
sb_state st;
sb_params p;
sb_bar out;
uint32_t h = SB_FNV_INIT;
int b, i, firstBad = -1;
n++;
st.seed = hex(json_get(c, "seed"));
st.salts[0] = (uint32_t)num(salts->items[0]); st.salts[1] = (uint32_t)num(salts->items[1]);
p.gen = (int)num(json_get(c, "gen")); p.energy = (int)num(json_get(c, "energy")); p.drift = (int)num(json_get(c, "drift"));
p.scale = (int)num(json_get(c, "scale")); p.root = (int)num(json_get(c, "root"));
for(b = 0; b < bars->n; b++){
jval *jb = bars->items[b], *ev = json_get(jb, "events");
p.fill = 0;
for(i = 0; i < fb->n; i++) if(num(fb->items[i]) == b) p.fill = 1;
sb_render_bar(&st, &p, b, &out);
if(out.n > max_events) max_events = out.n;
for(i = 0; i < out.n; i++) h = sb_checksum_event(h, &out.ev[i]);
if(firstBad >= 0) continue;
if(out.overflow){ firstBad = b; printf("FAIL %s bar %d: event buffer overflow\n", json_get(c, "id")->str, b); continue; }
if(out.sec != num(json_get(jb, "sec")) || out.fill != num(json_get(jb, "fill")) || out.barTicks != num(json_get(jb, "barTicks"))){
firstBad = b;
printf("FAIL %s bar %d: sec/fill/barTicks got %d/%d/%d want %lld/%lld/%lld\n", json_get(c, "id")->str, b,
out.sec, out.fill, out.barTicks, num(json_get(jb, "sec")), num(json_get(jb, "fill")), num(json_get(jb, "barTicks")));
continue;
}
for(i = 0; i < out.n || i < ev->n; i++){
sb_event want = {0, 0, 0, 0, 0, 0};
int have = i < ev->n, same;
if(have){
jval *row = ev->items[i];
want.t = (int16_t)num(row->items[0]); want.lane = (uint8_t)num(row->items[1]); want.v = (uint8_t)num(row->items[2]);
want.vel = (uint8_t)num(row->items[3]); want.len = (int16_t)num(row->items[4]); want.tr = (uint8_t)num(row->items[5]);
}
same = have && i < out.n && !memcmp(&want, &out.ev[i], sizeof want);
if(!same){
firstBad = b;
printf("FAIL %s first difference in bar %d, event %d (got %d events, want %d)\n", json_get(c, "id")->str, b, i, out.n, ev->n);
if(have) print_event("want", &want);
if(i < out.n) print_event("got ", &out.ev[i]);
break;
}
}
}
if(h != hex(json_get(c, "checksum")) || firstBad >= 0){
fail++;
if(firstBad < 0) printf("FAIL %s checksum 0x%08X want %s (events matched?)\n", json_get(c, "id")->str, h, json_get(c, "checksum")->str);
}
}
int main(int argc, char **argv){
const char *dir = argc > 1 ? argv[1] : "../../test/vectors";
char *names[64];
int nn = 0, i, k;
DIR *d;
struct dirent *de;
check_primitives(dir);
d = opendir(dir);
if(!d){ printf("cannot open %s\n", dir); return 1; }
while((de = readdir(d)) && nn < 64)
if(!strncmp(de->d_name, "gen", 3) && de->d_name[3] >= '0' && de->d_name[3] <= '9') names[nn++] = strdup(de->d_name);
closedir(d);
qsort(names, (size_t)nn, sizeof names[0], cmp_str);
if(!nn){ printf("no gen*.json in %s\n", dir); fail++; }
for(i = 0; i < nn; i++){
char path[1024];
jval *root, *cases;
snprintf(path, sizeof path, "%s/%s", dir, names[i]);
root = json_parse_file(path);
if(!root){ printf("cannot read %s\n", path); fail++; continue; }
cases = json_get(root, "cases");
for(k = 0; k < cases->n; k++) check_case(cases->items[k]);
json_free(root);
free(names[i]);
}
printf("max events in a bar: %d (buffer %d)\n", max_events, SB_MAX_EVENTS);
if(fail) printf("%d of %d checks FAILED\n", fail, n);
else printf("all %d checks pass\n", n);
return fail ? 1 : 0;
}

99
firmware/test/json.c Normal file
View File

@@ -0,0 +1,99 @@
#include "json.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
static const char *p;
static int bad;
static void ws(void){ while(*p == ' ' || *p == '\n' || *p == '\r' || *p == '\t') p++; }
static void *xcalloc(size_t n, size_t s){ void *r = calloc(n, s); if(!r){ fprintf(stderr, "out of memory\n"); exit(2); } return r; }
static char *parse_str(void){
const char *s = ++p;
char *out, *o;
while(*p && *p != '"'){ if(*p == '\\' && p[1]) p++; p++; }
if(*p != '"'){ bad = 1; return NULL; }
out = o = xcalloc((size_t)(p - s) + 1, 1);
for(; s < p; s++){ if(*s == '\\') s++; *o++ = *s; }
p++;
return out;
}
static jval *parse_val(void);
static void add(jval *v, jval *item){
v->items = realloc(v->items, sizeof(jval *) * (size_t)(v->n + 1));
if(!v->items){ fprintf(stderr, "out of memory\n"); exit(2); }
v->items[v->n++] = item;
}
static jval *parse_val(void){
jval *v = xcalloc(1, sizeof *v);
ws();
if(*p == '{'){
v->type = J_OBJ; p++; ws();
if(*p == '}'){ p++; return v; }
for(;;){
char *k; jval *item;
ws(); if(*p != '"'){ bad = 1; return v; }
k = parse_str(); ws();
if(*p != ':'){ bad = 1; free(k); return v; }
p++;
item = parse_val(); item->key = k; add(v, item);
ws(); if(*p == ','){ p++; continue; }
if(*p == '}'){ p++; return v; }
bad = 1; return v;
}
}
if(*p == '['){
v->type = J_ARR; p++; ws();
if(*p == ']'){ p++; return v; }
for(;;){
add(v, parse_val());
ws(); if(*p == ','){ p++; continue; }
if(*p == ']'){ p++; return v; }
bad = 1; return v;
}
}
if(*p == '"'){ v->type = J_STR; v->str = parse_str(); return v; }
if(!strncmp(p, "true", 4)){ v->type = J_BOOL; v->num = 1; p += 4; return v; }
if(!strncmp(p, "false", 5)){ v->type = J_BOOL; v->num = 0; p += 5; return v; }
if(!strncmp(p, "null", 4)){ v->type = J_NULL; p += 4; return v; }
if(*p == '-' || (*p >= '0' && *p <= '9')){
char *end;
v->type = J_NUM; v->num = strtoll(p, &end, 10);
if(*end == '.' || *end == 'e' || *end == 'E') bad = 1; /* the vectors are integer-only */
p = end; return v;
}
bad = 1; return v;
}
jval *json_parse_file(const char *path){
FILE *f = fopen(path, "rb");
long n;
char *buf;
jval *v;
if(!f) return NULL;
fseek(f, 0, SEEK_END); n = ftell(f); fseek(f, 0, SEEK_SET);
buf = xcalloc((size_t)n + 1, 1);
if(fread(buf, 1, (size_t)n, f) != (size_t)n){ fclose(f); free(buf); return NULL; }
fclose(f);
p = buf; bad = 0;
v = parse_val();
ws();
if(*p) bad = 1;
free(buf);
if(bad){ json_free(v); return NULL; }
return v;
}
jval *json_get(const jval *o, const char *key){
int i;
if(!o || o->type != J_OBJ) return NULL;
for(i = 0; i < o->n; i++) if(!strcmp(o->items[i]->key, key)) return o->items[i];
return NULL;
}
jval *json_at(const jval *a, int i){ return (a && (a->type == J_ARR || a->type == J_OBJ) && i >= 0 && i < a->n) ? a->items[i] : NULL; }
void json_free(jval *v){
int i;
if(!v) return;
for(i = 0; i < v->n; i++) json_free(v->items[i]);
free(v->items); free(v->str); free(v->key); free(v);
}

20
firmware/test/json.h Normal file
View File

@@ -0,0 +1,20 @@
/* Minimal JSON reader for the host test runner (not used on the Pico). Integers only, no escapes beyond \" and \\. */
#ifndef SEEDBOX_JSON_H
#define SEEDBOX_JSON_H
typedef enum { J_NULL, J_BOOL, J_NUM, J_STR, J_ARR, J_OBJ } jtype;
typedef struct jval {
jtype type;
long long num; /* J_NUM, J_BOOL */
char *str; /* J_STR */
char *key; /* set when the value sits inside an object */
struct jval **items; /* J_ARR, J_OBJ */
int n;
} jval;
jval *json_parse_file(const char *path);
jval *json_get(const jval *obj, const char *key); /* NULL if missing */
jval *json_at(const jval *arr, int i);
void json_free(jval *v);
#endif