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

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