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>
11
.github/workflows/check.yml
vendored
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
name: check
|
||||||
|
on: [push, pull_request]
|
||||||
|
jobs:
|
||||||
|
check:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: 20
|
||||||
|
- run: npm test
|
||||||
4
.gitignore
vendored
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
node_modules/
|
||||||
|
.DS_Store
|
||||||
|
firmware/build/
|
||||||
|
*.uf2
|
||||||
49
CLAUDE.md
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
# CLAUDE.md
|
||||||
|
|
||||||
|
Seedbox: a generative MIDI rhythm and melody machine for the Raspberry Pi Pico 2 W. v0.1 is a browser simulator with a finished engine, and a checked stripboard layout. The next job is the C firmware.
|
||||||
|
|
||||||
|
Read first: `README.md`, `docs/concept.md`, `firmware/README.md`, `docs/decisions.md`.
|
||||||
|
|
||||||
|
## Ground rules
|
||||||
|
|
||||||
|
- **`sim/engine.js` is the musical reference.** The C engine must reproduce `test/vectors/` exactly. Don't "fix" the C to sound better: change the JS, listen, regenerate, commit both.
|
||||||
|
- **Never regenerate the vectors to make a failing port pass.** `npm run vectors` is only for intended changes to the music. Say so in the commit message.
|
||||||
|
- **Keep the engine integer-only and deterministic.**
|
||||||
|
- No `Math.random`, no floats in decisions, no wall-clock time.
|
||||||
|
- Use floor division through `fdiv`/`mod`.
|
||||||
|
- **`hardware/layout.js` is the only source for the stripboard.**
|
||||||
|
- After any change, run `node hardware/check-layout.js`; it must print `layout clean`.
|
||||||
|
- Then run `node tools/build-wiring.js`.
|
||||||
|
- Don't hand-edit `tools/wiring.html`.
|
||||||
|
- **Hardware facts that can't be checked here** (lug order on the user's TRS jacks, Nokia BL polarity): flag them; don't guess.
|
||||||
|
- **RP2350**:
|
||||||
|
- Inputs use pull-ups (erratum E9).
|
||||||
|
- Set GP0/GP4 drive strength to 8–12 mA.
|
||||||
|
- The pin map in `docs/hardware/pinout.md` is fixed by the board that's being built; don't move pins without updating `layout.js` and re-running the checker.
|
||||||
|
|
||||||
|
## Commands
|
||||||
|
|
||||||
|
```sh
|
||||||
|
npm test # vectors + layout, must pass before every commit
|
||||||
|
npm run vectors # regenerate vectors (intended music changes only)
|
||||||
|
node hardware/check-layout.js --nets
|
||||||
|
node tools/build-wiring.js
|
||||||
|
npx serve . → http://localhost:3000/sim/ # simulator (Web MIDI needs localhost/https)
|
||||||
|
```
|
||||||
|
|
||||||
|
No npm dependencies. Node 18+.
|
||||||
|
|
||||||
|
## Layout
|
||||||
|
|
||||||
|
- `sim/` simulator: `engine.js` (portable; also `require`-able from Node), `app.js` (browser UI, Web MIDI, scheduler, screen renderer), `index.html`.
|
||||||
|
- `test/` `cases.js` (the case list), `render.js`, `gen-vectors.js`, `check-vectors.js`, `vectors/*.json`.
|
||||||
|
- `hardware/` `layout.js`, `check-layout.js`.
|
||||||
|
- `tools/` single-file bench pages: `wiring.html` (built), `bom.html`, `panel.html`; `wiring-src/` holds the parts `wiring.html` is built from.
|
||||||
|
- `docs/` user-facing docs; `docs/hardware/svg` and the CSVs are exports from the tools.
|
||||||
|
- `firmware/` empty except the plan. Suggested structure is in its README.
|
||||||
|
|
||||||
|
## Conventions
|
||||||
|
|
||||||
|
- The user (Gee) builds by hand: stripboard, hookup wire, panel-mount parts. Write instructions as strip letter + hole number, with colour roles from `docs/hardware/wiring.md`.
|
||||||
|
- Docs are plain and short. Tables over prose for anything you'd look up.
|
||||||
|
- Commit vectors and engine changes together. Commit layout and rebuilt wiring together.
|
||||||
59
README.md
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
# Seedbox
|
||||||
|
|
||||||
|
A generative rhythm and melody machine for a Raspberry Pi Pico 2 W. One 32-bit seed grows a whole tune: a drum part for any sampler or drum machine and a bass, melody and pad line for a poly synth, both over MIDI. Built and tested with a Roland SP-404MKII and an Arturia MiniFreak. You don't program steps. You pick a seed, turn tracks on and off, and steer it with two knobs.
|
||||||
|
|
||||||
|
**Status: v0.1.** The engine and the controls are settled in a browser simulator that drives the real gear over Web MIDI. The stripboard layout has been checked by script and by an independent review. The Pico firmware is not written yet: that's the next step.
|
||||||
|
|
||||||
|
## What's in the box
|
||||||
|
|
||||||
|
| Part | |
|
||||||
|
|---|---|
|
||||||
|
| Brain | Raspberry Pi Pico 2 W (RP2350), powered and flashed through its own USB port at the back |
|
||||||
|
| Screen | Nokia 5110 LCD (PCD8544, 84 × 48) |
|
||||||
|
| Controls | Encoder with push, ENERGY and DRIFT pots, SEED button, 11 track buttons in a 4 × 3 grid, with the RUN toggle in its top-right corner |
|
||||||
|
| MIDI | IN (for learning drum pads), OUT KIT (sampler or drum machine), OUT SYNTH. 3.5 mm TRS (Type A), MIDI 1.0 3.3 V spec. |
|
||||||
|
|
||||||
|
## Repo map
|
||||||
|
|
||||||
|
```
|
||||||
|
sim/ the browser simulator: index.html, engine.js (portable engine), app.js (UI, Web MIDI)
|
||||||
|
test/ golden vectors and the scripts that make and check them
|
||||||
|
hardware/ layout.js (the stripboard, one source of truth) and check-layout.js
|
||||||
|
tools/ bench pages: wiring.html, bom.html (tick-off parts list), panel.html (panel layout)
|
||||||
|
docs/ concept, controls, MIDI setup, hardware (BOM, pinout, wiring, panel, SVG drawings)
|
||||||
|
firmware/ the port plan for the Pico (C, Pico SDK)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Run it
|
||||||
|
|
||||||
|
The simulator is a static page. Web MIDI needs a secure context, so serve it over `localhost` or HTTPS rather than opening the file directly:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
npx serve . # or: python3 -m http.server
|
||||||
|
# open http://localhost:3000/sim/ (Chrome or Edge; Firefox needs a Web MIDI add-on)
|
||||||
|
```
|
||||||
|
|
||||||
|
GitHub Pages works too: enable Pages for the repo and open `/sim/`.
|
||||||
|
|
||||||
|
Checks (Node 18 or later, no dependencies):
|
||||||
|
|
||||||
|
```sh
|
||||||
|
npm test # golden vectors + stripboard netlist
|
||||||
|
```
|
||||||
|
|
||||||
|
## Docs
|
||||||
|
|
||||||
|
- [Concept](docs/concept.md): how the music is made
|
||||||
|
- [Controls](docs/controls.md): the panel, the screen pages, the gestures
|
||||||
|
- [MIDI setup](docs/midi-setup.md): kit and synth, learn mode
|
||||||
|
- [Hardware](docs/hardware/README.md): parts, pinout, stripboard, panel
|
||||||
|
- [Firmware plan](firmware/README.md)
|
||||||
|
- [Design decisions](docs/decisions.md)
|
||||||
|
|
||||||
|
## Licence
|
||||||
|
|
||||||
|
None chosen yet. Add a `LICENSE` file before making the repo public (MIT for code and CC BY-SA for the hardware docs is a common pairing).
|
||||||
|
|
||||||
|
## Credits
|
||||||
|
|
||||||
|
The track model leans on Eric Lyon's paper "Techno Counterpoint", an analysis of Aphex Twin's tracking practice. The AMENIZER plays a re-voiced Amen break pattern (The Winstons, "Amen, Brother", 1969).
|
||||||
137
docs/concept.md
Normal file
@@ -0,0 +1,137 @@
|
|||||||
|
# Concept
|
||||||
|
|
||||||
|
Seedbox grows a whole tune from one 32-bit number. The seed decides the groove, the key, the chords, the melody and the form. Two knobs push it around while it plays, and eleven buttons decide which parts you hear. It is a performance instrument: there is no step editing.
|
||||||
|
|
||||||
|
## Tracks, not lanes
|
||||||
|
|
||||||
|
Following Eric Lyon's reading of Aphex Twin ("Techno Counterpoint"), a tune is a set of looped tracks that the player brings in and out. Every track is rendered every bar; the buttons only decide what reaches MIDI. Muting never changes the pattern underneath, so a track comes back exactly where it would have been.
|
||||||
|
|
||||||
|
| # | Track | Goes to | What it is |
|
||||||
|
|---|---|---|---|
|
||||||
|
| 0 | KICK | kit | main kicks (accented, and the one) |
|
||||||
|
| 1 | KICK 2 | kit | the other kicks |
|
||||||
|
| 2 | SNARE | kit | backbeats and loud snares (vel ≥ 90) |
|
||||||
|
| 3 | GHOST | kit | quiet snares |
|
||||||
|
| 4 | HAT | kit | hat accents (vel ≥ 85) |
|
||||||
|
| 5 | HAT 16 | kit | the rest of the hats |
|
||||||
|
| 6 | PERC | kit | percussion, shares the melody's phase offset |
|
||||||
|
| 7 | BASS | synth | root and chord degrees, follows the kick |
|
||||||
|
| 8 | MEL A | synth | the motif, first half of a hocket split |
|
||||||
|
| 9 | MEL B | synth | the other half of the hocket |
|
||||||
|
| 10 | PAD | synth | a chord held for the bar |
|
||||||
|
|
||||||
|
The synth runs in poly mode on one channel, so bass, melody and pad share one voice pool.
|
||||||
|
|
||||||
|
## The genome
|
||||||
|
|
||||||
|
`genome(seed)` picks, once per seed:
|
||||||
|
|
||||||
|
- **steps**: 16 most of the time, sometimes 12, 14 or 20 (bar length = steps × 16th)
|
||||||
|
- **form**: AAAB, AABA, ABAB, AAAA, ABAC or ABAA over a 4-bar phrase
|
||||||
|
- **scale and root**: any of the 21 scales below, any root. The ROOT and SCALE pages override them; AUTO keeps the seed's pick.
|
||||||
|
- **progression**: one chord root per bar of the phrase (e.g. i–vi–iv–v)
|
||||||
|
- **frame**: where the main kick and snare live (below)
|
||||||
|
- **swing**: none, light, medium, heavy or triplet feel
|
||||||
|
- **arc**: flat, build (bars 1–2 thin out) or break (bar 3 strips back)
|
||||||
|
- **accent cycle**: off, or an accent every 3, 5 or 7 sixteenths running across the bar line
|
||||||
|
- **motif length** (3–5 notes), **hocket mask**, **bass degree**, **melody phase offset**, **layback** (snares sit a little late), densities, fill odds
|
||||||
|
|
||||||
|
### Frames
|
||||||
|
|
||||||
|
All four kit generators share the frame, so a seed has a recognisable skeleton whichever generator plays it.
|
||||||
|
|
||||||
|
| Frame | Kick | Snare |
|
||||||
|
|---|---|---|
|
||||||
|
| backbeat | 1 | 2 and 4 |
|
||||||
|
| half-time | 1 | 3 |
|
||||||
|
| four-floor | every beat (layered) | 2 and 4 |
|
||||||
|
| 3-3-2 | 1, 1a, 3, 3a | between |
|
||||||
|
| broken | 1 and 3+ | 2+ and 4 |
|
||||||
|
| displaced | off the one | 3 |
|
||||||
|
| over the bar | every 12 sixteenths, across bar lines | halfway between |
|
||||||
|
| free snare | 1 | a Euclidean snare instead |
|
||||||
|
|
||||||
|
## Scales
|
||||||
|
|
||||||
|
Bass, melody and pad follow the scale. Chords stack every other scale note, so the scale's size changes the harmony: 5 notes give open chords, whole tone gives augmented, octatonic gives diminished.
|
||||||
|
|
||||||
|
| # | Name | Steps |
|
||||||
|
|---|---|---|
|
||||||
|
| 0 | MAJOR | 0 2 4 5 7 9 11 |
|
||||||
|
| 1 | MINOR | 0 2 3 5 7 8 10 |
|
||||||
|
| 2 | DORIAN | 0 2 3 5 7 9 10 |
|
||||||
|
| 3 | MIXOLYDIAN | 0 2 4 5 7 9 10 |
|
||||||
|
| 4 | LYDIAN | 0 2 4 6 7 9 11 |
|
||||||
|
| 5 | PHRYGIAN | 0 1 3 5 7 8 10 |
|
||||||
|
| 6 | HARM MINOR | 0 2 3 5 7 8 11 |
|
||||||
|
| 7 | MINOR PENT | 0 3 5 7 10 |
|
||||||
|
| 8 | MAJOR PENT | 0 2 4 7 9 |
|
||||||
|
| 9 | PHRYG DOM | 0 1 4 5 7 8 10 |
|
||||||
|
| 10 | DBL HARM | 0 1 4 5 7 8 11 |
|
||||||
|
| 11 | HUNG MINOR | 0 2 3 6 7 8 11 |
|
||||||
|
| 12 | UKR DORIAN | 0 2 3 6 7 9 10 |
|
||||||
|
| 13 | LYD DOM | 0 2 4 6 7 9 10 |
|
||||||
|
| 14 | HIRAJOSHI | 0 2 3 7 8 |
|
||||||
|
| 15 | IN SEN | 0 1 5 7 10 |
|
||||||
|
| 16 | IWATO | 0 1 5 6 10 |
|
||||||
|
| 17 | PROMETHEUS | 0 2 4 6 9 10 |
|
||||||
|
| 18 | WHOLE TONE | 0 2 4 6 8 10 |
|
||||||
|
| 19 | OCTATONIC | 0 1 3 4 6 7 9 10 |
|
||||||
|
| 20 | LOCRIAN | 0 1 3 5 6 8 10 |
|
||||||
|
|
||||||
|
## Generators
|
||||||
|
|
||||||
|
Selected on the GEN page. They only change the kit; the synth tracks are the same for all of them.
|
||||||
|
|
||||||
|
- **EUCLID**: Euclidean rhythms. Kick E(k,n) with a rotation that keeps the one, hats and perc often on their own cycle length (polymeter).
|
||||||
|
- **SKELETON**: the frame, plus a few kick moves and hat patterns. The most "drummer-like".
|
||||||
|
- **SUBDIV**: recursive subdivision of each beat. Clusters and rolls, calmer than it used to be.
|
||||||
|
- **AUTOMATA**: a 1-D cellular automaton decides the hits inside the frame.
|
||||||
|
- **AMENIZER**: the four bars of the Amen break, velocities included, with its own humanising and chopping (below).
|
||||||
|
|
||||||
|
## Sections
|
||||||
|
|
||||||
|
The form says which bars are A, B or C. B and C are transformations of A rather than new patterns, so the phrase hangs together. The transforms: kick answer, hat doubling, thin the first half, perc shift, ghost pickups, snare drag, drop the one, half-time switch.
|
||||||
|
|
||||||
|
## ENERGY
|
||||||
|
|
||||||
|
Pattern density. Kicks, snares, ghosts, perc and the melody thicken with it. Hats move only a quarter as much, so energy doesn't just mean "busier hats".
|
||||||
|
|
||||||
|
## DRIFT
|
||||||
|
|
||||||
|
One knob, several layers that fade in at different points. At 0 the loop is locked.
|
||||||
|
|
||||||
|
| Range | What comes in |
|
||||||
|
|---|---|
|
||||||
|
| 0–42 | phrase variations, from never to every phrase |
|
||||||
|
| 20–127 | microtiming: hats drag, snares rush, kick and bass move together |
|
||||||
|
| 40–127 | swing wobbles from bar to bar |
|
||||||
|
| 55–127 | accents slip, velocities loosen |
|
||||||
|
| 80–127 | whole parts slide a 16th early or late for a bar |
|
||||||
|
| 105–127 | now and then a beat is squeezed onto a triplet grid |
|
||||||
|
|
||||||
|
## AMENIZER
|
||||||
|
|
||||||
|
- The Amen break as a 4-bar table of step, drum, velocity and main/secondary.
|
||||||
|
- **Humanised**: each hit moves ±6 in velocity. A hit that quickly follows the same drum plays at 80%. The ride level drifts per bar.
|
||||||
|
- **Feel**: kicks ±4 ticks, main snares 10 ticks late (±5), ghosts ±10, ride 4 ticks early (±6).
|
||||||
|
- **Drift chops it per beat**, like a jungle edit. A beat can be swapped for:
|
||||||
|
- the same beat from another bar
|
||||||
|
- any beat
|
||||||
|
- a repeat
|
||||||
|
- a half-beat stutter
|
||||||
|
- a ratchet
|
||||||
|
- The chop chance rises with DRIFT. Beat 1 stays intact below DRIFT 88.
|
||||||
|
- The generic drift layers come in later and softer.
|
||||||
|
- Swing, transforms, arc and accent shaping are off.
|
||||||
|
- Bar 4 of the break is its own fill.
|
||||||
|
|
||||||
|
## Fills
|
||||||
|
|
||||||
|
There's no FILL button. Bar 4 of a phrase fills on its own now and then (roll, stutter or drop-out with pickup); how often depends on the seed. The engine still takes a fill flag, so a manual fill can come back later without touching the music.
|
||||||
|
|
||||||
|
## Determinism
|
||||||
|
|
||||||
|
The engine uses integers only, a seeded xorshift32 and a hash, so a given seed, generator, ENERGY, DRIFT and bar always give the same events. That is what lets the Pico firmware be tested against the simulator: see [the firmware plan](../firmware/README.md) and `test/`.
|
||||||
|
|
||||||
|
Timing is 840 ticks per quarter note (210 per 16th; 840 divides by 2, 3, 4, 5 and 7 for tuplets). MIDI clock goes out every 35 ticks (24 PPQN).
|
||||||
63
docs/controls.md
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
# Controls
|
||||||
|
|
||||||
|
```
|
||||||
|
┌──────────────────────────────────────────────────────────────┐
|
||||||
|
│ [ 5110 screen ] (ENERGY) [KICK][KICK2][SNR] RUN/STOP
|
||||||
|
│ (DRIFT) [HAT ][HAT16][PRC][GHST]
|
||||||
|
│ (ENC) [ SEED ] [BASS][MEL A][MLB][PAD ]
|
||||||
|
└──────────────────────────────────────────────────────────────┘
|
||||||
|
back wall: USB · MIDI IN · OUT KIT · OUT SYNTH
|
||||||
|
```
|
||||||
|
|
||||||
|
To-scale layout and drill list: [hardware/panel.md](hardware/panel.md).
|
||||||
|
|
||||||
|
## Buttons and knobs
|
||||||
|
|
||||||
|
| Control | Does |
|
||||||
|
|---|---|
|
||||||
|
| **SEED** | A new random seed. The old one goes into the history (last 16). |
|
||||||
|
| **RUN** toggle | Start / stop. Stop sends note-offs, All Sound Off (CC 120) on the learned kit channels, and CC 120 and 123 on the synth channel. |
|
||||||
|
| **ENERGY** | Density. See [concept](concept.md#energy). |
|
||||||
|
| **DRIFT** | How loose and changeable. See [concept](concept.md#drift). |
|
||||||
|
| **Track button**, tap | Track on / off. |
|
||||||
|
| **Track button**, hold (> 250 ms) | Flips the track only while held: a quick drop-out or punch-in. |
|
||||||
|
| **Encoder**, turn | Choose the page. |
|
||||||
|
| **Encoder**, push | Edit the page (the value line inverts). Turn to change, push again to leave. |
|
||||||
|
|
||||||
|
Option (bench setting in the simulator): track changes wait for the next bar line.
|
||||||
|
|
||||||
|
## Screen
|
||||||
|
|
||||||
|
The Nokia 5110 has 84 × 48 pixels:
|
||||||
|
|
||||||
|
- **Top line**: the page name.
|
||||||
|
- **Second line**: its value, inverted while you edit it.
|
||||||
|
- **Below**: a 4 × 3 map that mirrors the buttons. The top-right cell is RUN: filled while playing.
|
||||||
|
- filled = on
|
||||||
|
- outline = off
|
||||||
|
- flash = hit
|
||||||
|
- dot = a kit track not learned yet
|
||||||
|
- blinking = a change waiting for the bar line
|
||||||
|
|
||||||
|
## Pages
|
||||||
|
|
||||||
|
| Page | Edit with the encoder | Push |
|
||||||
|
|---|---|---|
|
||||||
|
| **SEED** | scroll back through the last 16 seeds (`-1`, `-2` …; `*` = re-rolled) | load the shown seed |
|
||||||
|
| **GEN** | EUCLID, SKELETON, SUBDIV, AUTOMATA, AMENIZER | done |
|
||||||
|
| **ROOT** | AUTO, C … B. AUTO shows the seed's pick | done |
|
||||||
|
| **SCALE** | AUTO, then the 21 scales (see [Concept](concept.md#scales)). AUTO shows the seed's pick | done |
|
||||||
|
| **BPM** | tempo | done |
|
||||||
|
| **SLOT** | 8 slots for seed + salts + generator (not root or scale) | push = load, long push = save |
|
||||||
|
| **ROLL** | KIT or LINE | push re-rolls just that half (a new drum part over the same tune, or the other way round), then goes back to page select |
|
||||||
|
| **LEARN** | — | push, press a kit track button, hit the pad on the sampler or drum machine. Push again to cancel. |
|
||||||
|
|
||||||
|
## Simulator keys
|
||||||
|
|
||||||
|
| Key | Does |
|
||||||
|
|---|---|
|
||||||
|
| 1 … 0, - | track buttons KICK … PAD |
|
||||||
|
| Space | SEED |
|
||||||
|
| ← → | encoder turn |
|
||||||
|
| Enter / Shift+Enter | encoder push / long push |
|
||||||
|
| R | run / stop |
|
||||||
47
docs/decisions.md
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
# Design decisions
|
||||||
|
|
||||||
|
Short record of why things are the way they are, so they don't get undone by accident.
|
||||||
|
|
||||||
|
## Music
|
||||||
|
|
||||||
|
- **Tracks are only on or off.**
|
||||||
|
- There's no auto mode. Bringing parts in and out is the performance (Lyon, "Techno Counterpoint").
|
||||||
|
- Every track is rendered every bar, so muting doesn't change the pattern.
|
||||||
|
- **No break lane.** Chopping breaks is the M8's job. Seedbox makes rhythm and melody. AMENIZER is a generator that plays a break pattern on the kit, not a sample player.
|
||||||
|
- **Energy moves hats only a quarter as much as the rest.** Before that, energy mostly meant busier hats.
|
||||||
|
- **Rhythmic frames per seed.** Before frames, most seeds put the kick on 1 and the snare on 3.
|
||||||
|
- **Sections are transforms of A**, not new patterns. The phrase hangs together.
|
||||||
|
- **Drift is continuous overlapping layers**, not steps. Wide knob spans used to do little. Drift adds displacement and weirdness at the top end.
|
||||||
|
- **AMENIZER keeps its own feel.** Its humanising and chops are tuned so that DRIFT past halfway isn't too extreme. The generic layers come in later and at half strength.
|
||||||
|
- **ROOT and SCALE are global and sticky (2026-10-11).** They survive SEED, ROLL and slot loads, so a seed can be fitted to an existing idea. AUTO lets the seed pick from all 21 scales, which changed every seed's sound (vectors regenerated). Slots don't store the key.
|
||||||
|
- **Integer-only engine** with a seeded RNG. This makes it deterministic and portable, and testable by golden vectors.
|
||||||
|
|
||||||
|
## MIDI
|
||||||
|
|
||||||
|
- **Kit pads are learned, not configured.** The bank/pad/mode tables were dropped. BANK was dropped too (2026-10-11): it only made sense for the SP-404MKII's bank layout, and the kit side should work with any sampler or drum machine. Re-learn to change kit.
|
||||||
|
- **STOP sends CC 120 on the kit channels** because some samplers (the SP-404MKII) ignore All Notes Off. SPP isn't sent.
|
||||||
|
- **The synth is in poly mode on one channel.** Bass, melody and pad share it.
|
||||||
|
- **MIDI OUT follows the 3.3 V spec (CA-033)**: 10 Ω on TX, 33 Ω on 3.3 V. An earlier draft had them swapped; the independent review caught it.
|
||||||
|
- **Generic kit and synth (2026-10-11).** Jacks, screens and docs say KIT and SYNTH, not 404 and MiniFreak. Those two are the test rig, nothing more.
|
||||||
|
|
||||||
|
## Hardware
|
||||||
|
|
||||||
|
- **6N137 for MIDI IN**, because that's the part on hand. It's pin-compatible with the 6N138 here except pin 7, which is the enable input and must be high: a link ties it to pin 8, and the 4.7 kΩ pin 7 resistor is gone. C1 (supply bypass) is required and sits right beside the chip. Never fit a 6N138 with that link in place: on a 6N138, pin 7 is the transistor base.
|
||||||
|
- **3.5 mm TRS MIDI jacks (Type A)** instead of 5-pin DIN: the DIN jacks didn't fit the box. Type A TRS gear takes a plain stereo cable; DIN gear gets a Type A adapter.
|
||||||
|
- **The Pico's USB end is at the stripboard edge** (its first pin row is strip A), so the box is powered and flashed through a slot in the back wall, with no extension cable.
|
||||||
|
- **11 momentary track buttons in a diode matrix**, with the state shown on the LCD map. Latching or lit buttons weren't available locally, and LEDs would need more pins.
|
||||||
|
- **No FILL button.** Dropped after trying the panel; fills still happen on their own in bar 4. The RUN toggle took the free corner of the grid (row 1 / column 4, outside the matrix), and GHOST moved into FILL's matrix slot (row 2 / column 4).
|
||||||
|
- **Nokia 5110** instead of a 0.91" OLED, which was too small to read.
|
||||||
|
- **Landscape panel** in an enclosure of about 80 × 150 mm.
|
||||||
|
- **One stripboard** in a plastic box, Pico socketed, everything else panel mounted. No standoffs, screws or USB extension.
|
||||||
|
- **GP0/GP4 links run under the Pico between its headers.** That keeps them from crossing header pins; it's the reason for the col-13 cut length.
|
||||||
|
- **Pad names P1–P35**, not W…, to avoid clashing with strip W.
|
||||||
|
|
||||||
|
## Process
|
||||||
|
|
||||||
|
- **The stripboard was reviewed by blind agents** with no access to the design reasoning, as well as by `check-layout.js`.
|
||||||
|
- First review (30-row DIN board): 6 issues, all fixed, then clean.
|
||||||
|
- Second review (USB at the edge, TRS jacks): no electrical faults, 12 doc and mechanical findings, all fixed. The biggest was a link note that named the ground strip instead of strip Y. The rest: USB slot height, SEED above R4, and the panel tool defaulting to portrait.
|
||||||
|
- R4 now lies flat along strip X across a cut at X23, so a normal ¼ W part fits under SEED.
|
||||||
|
- The re-check came back clean.
|
||||||
|
- **The firmware port happens in Claude Code** (this repo), against the golden vectors.
|
||||||
39
docs/hardware/README.md
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
# Hardware
|
||||||
|
|
||||||
|
| File | What |
|
||||||
|
|---|---|
|
||||||
|
| [bom.csv](bom.csv) | parts list with status (have / check / buy / optional); interactive version: `tools/bom.html` |
|
||||||
|
| [pinout.md](pinout.md) | every Pico pin used, and why |
|
||||||
|
| [wiring.md](wiring.md) | stripboard, cuts, links, off-board wires, panel wiring, build order |
|
||||||
|
| [panel.md](panel.md) | panel layout and drill list |
|
||||||
|
| [board-parts.csv](board-parts.csv), [wire-list.csv](wire-list.csv), [drill-list.csv](drill-list.csv), [cuts.txt](cuts.txt) | the tables, exported from the tools |
|
||||||
|
| [svg/](svg/) | the drawings, exported from the tools |
|
||||||
|
|
||||||
|
The source of truth for the stripboard is [`hardware/layout.js`](../../hardware/layout.js). Everything in this folder that describes the board is generated from it. After changing it:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
node hardware/check-layout.js # geometry, footprints, circuit rules
|
||||||
|
node tools/build-wiring.js # rebuilds tools/wiring.html
|
||||||
|
```
|
||||||
|
|
||||||
|
To refresh the SVGs and CSVs, open `tools/wiring.html` and `tools/panel.html` in a browser and save from there (or script it with jsdom, as the first export did).
|
||||||
|
|
||||||
|
## Build constraints this design assumes
|
||||||
|
|
||||||
|
- One stripboard carries the Pico (socketed in two female headers) and the MIDI circuits.
|
||||||
|
- Everything else is panel mounted and wired with hookup wire, in 5 colours.
|
||||||
|
- No standoffs, screws or USB extension. The board is held with foam tape or hot glue, and its USB port lines up with a slot in the box wall.
|
||||||
|
- The Nokia 5110 module sits on top of the box. Its wires go through a hole under the header.
|
||||||
|
- The enclosure is plastic, about 80 × 150 × 40 mm, laid out landscape.
|
||||||
|
- The MIDI jacks are 3.5 mm TRS, Type A.
|
||||||
|
- The Pico's USB end sits at the board edge. Its USB port powers the box and flashes the firmware through a slot in the back wall.
|
||||||
|
|
||||||
|
## Verified, and not
|
||||||
|
|
||||||
|
- **Checked**:
|
||||||
|
- `check-layout.js` covers the netlist against the pin map and the MIDI 3.3 V circuit, plus shorts, cuts and footprints.
|
||||||
|
- Two blind reviews by agents with no access to the design reasoning. Every finding was fixed; see [decisions](../decisions.md#process).
|
||||||
|
- **Not checkable from here**:
|
||||||
|
- Lug order on your TRS jacks: find tip, ring and sleeve with the meter and a plug inserted.
|
||||||
|
- RP2350 pin functions: confirm against the Pico 2 W datasheet when writing the firmware.
|
||||||
|
- Nokia BL polarity: the wiring doc says how to test it.
|
||||||
20
docs/hardware/board-parts.csv
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
part,value,holes,note
|
||||||
|
"Pico 2W","2 × 20-pin female header","A13–T13 and A20–T20 (pins 1–20 down the left, 40–21 down the right)","USB end flush with the top edge of the board, past strip A"
|
||||||
|
"U1","6N137","U5–X5, U8–X8","6N137 in a DIP-8 socket. Notch toward strip U, pin 1 at hole U5. Pin 7 (enable) is tied to pin 8 by the U10–V10 link."
|
||||||
|
"R1","220 Ω","V2 – Y2","MIDI IN current limit. 3-hole span: stand it upright, or use a small ⅛ W part flat. If learning ever misses notes, use 150 Ω."
|
||||||
|
"D1","1N4148","V4 – W4","Band (cathode) on V4, the opto LED anode side. Stands upright (1-hole span)."
|
||||||
|
"R2","1 kΩ","W10 – AB10","Opto output pull-up to 3.3 V"
|
||||||
|
"C1","100 nF","U11 – X11","6N137 supply bypass, right beside the chip: U11 is its 5 V (pin 8) strip, X11 its GND (pin 5) strip. Required. Fit it after the beep test."
|
||||||
|
"R4","10 Ω","X21 – X25","MIDI OUT KIT, TX line (RC). Lies flat along strip X, across the cut at X23 (4-hole span, ¼ W fits). SEED hangs right above it."
|
||||||
|
"R5","33 Ω ½ W","V19 – AB19","MIDI OUT KIT, 3.3 V line (RA). ½ W: a mono plug in the jack shorts it to ground."
|
||||||
|
"R6","10 Ω","U28 – Y28","MIDI OUT SYNTH, TX line (RC)"
|
||||||
|
"R7","33 Ω ½ W","V30 – AB30","MIDI OUT SYNTH, 3.3 V line (RA). ½ W, same reason as R5."
|
||||||
|
"Link","wire","A34 – Z34","5 V (VBUS) to the 5 V strip"
|
||||||
|
"Link","wire","E32 – AB32","3V3 to the 3V3 strip"
|
||||||
|
"Link","wire","R33 – AA33","GND to the GND strip"
|
||||||
|
"Link","wire","U13 – Z13","opto VCC to 5 V"
|
||||||
|
"Link","wire","U10 – V10","opto enable (pin 7) to VCC (pin 8): the 6N137 output only works with pin 7 high"
|
||||||
|
"Link","wire","X12 – AA12","opto GND to GND"
|
||||||
|
"Link","wire","W9 – B9","opto out to GP1 (MIDI RX), strip B"
|
||||||
|
"Link","wire","A15 – X15","GP0 (TX KIT) to strip X (R4), straight down under the Pico between its headers"
|
||||||
|
"Link","wire","F14 – Y14","GP4 (TX SYNTH) to strip Y (R6), straight down under the Pico between its headers"
|
||||||
|
26
docs/hardware/bom.csv
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
group,id,part,spec,qty,status,notes
|
||||||
|
"Core","pico","Raspberry Pi Pico 2W","RP2350, micro-USB","1","have","The whole engine runs on it with lots of headroom."
|
||||||
|
"Core","lcd","Nokia 5110 LCD module","PCD8544, 84×48, SPI","1","have","The module you used with Arduino."
|
||||||
|
"Core","box","Enclosure","","1","have","About 80 × 150 × 40 mm. Panel layout: tools/panel.html."
|
||||||
|
"Controls","enc","Rotary encoder with push","KY-040 module or bare EC11","1","have","Power it from 3V3."
|
||||||
|
"Controls","pots","Potentiometer, linear","B10K (anything 5k–50k linear works)","2","have","Energy and Drift."
|
||||||
|
"Controls","knobs","Knobs","to fit the pot and encoder shafts","3","check","Shaft types differ (D, knurled, 6 mm). Match them."
|
||||||
|
"Controls","btn","Momentary push button, panel mount","~12 mm, non-latching","11","check","7 kit + 4 line tracks."
|
||||||
|
"Controls","seedbtn","Momentary push button, large","16–22 mm, non-latching","1","check","SEED. Any big momentary works."
|
||||||
|
"Controls","tog","Toggle switch, SPDT","","1","have","RUN / STOP. Your old bypass switch."
|
||||||
|
"Controls","dio","1N4148 diode","or 1N4448 / 1N914 / any small-signal diode","12","buy","11 for the button matrix + 1 for MIDI IN. 1N4448 works too. Every button needs one: a missing diode gives phantom presses with three buttons held."
|
||||||
|
"MIDI","trs","3.5 mm stereo (TRS) jack, panel mount","3 lugs: tip, ring, sleeve. Not mono.","3","check","IN, OUT KIT, OUT SYNTH. MIDI TRS Type A. 6 mm hole each."
|
||||||
|
"MIDI","adapt","TRS-to-DIN MIDI adapter, Type A","female DIN, 3.5 mm plug","1","buy","For a DIN synth or sampler (e.g. MiniFreak). Must say Type A (Type B swaps tip and ring). Type A TRS gear (e.g. SP-404MKII) takes a plain stereo 3.5 mm cable."
|
||||||
|
"MIDI","opto","6N137 optocoupler","DIP-8 (6N138 also works: then drop the pin 7 link)","1","have","MIDI IN. You have three; one is used."
|
||||||
|
"MIDI","sock","DIP-8 IC socket","","1","buy","So the opto can be swapped."
|
||||||
|
"MIDI","r330","Resistor 330 Ω","220–470 Ω","1","check","LCD backlight (LED pin) to 3.3 V, inline at the module."
|
||||||
|
"MIDI","r220","Resistor 220 Ω","150–220 Ω","1","have","MIDI IN, in series with the opto LED. The 6N137 wants a bit more LED current: use 150 Ω if learning misses notes."
|
||||||
|
"MIDI","r1k","Resistor 1 kΩ","470 Ω–2.2 kΩ is fine","2","have","Opto output pull-up, NPN base."
|
||||||
|
"MIDI","r33","Resistor 33 Ω, ½ W","27–47 Ω is fine","2","buy","MIDI OUT ring (pin 4) line, per the MIDI 3.3 V spec. ½ W because a mono plug in an OUT jack shorts it straight to ground (0.33 W); a ¼ W one would cook."
|
||||||
|
"MIDI","r10","Resistor 10 Ω","10–22 Ω is fine","2","check","MIDI OUT tip (pin 5) line, from TX. Don't replace with a wire: it protects the pin."
|
||||||
|
"MIDI","cap","Ceramic capacitor 100 nF","anything 47–220 nF","3","check","C1 on the board (6N137 supply bypass, required). Optional: one per pot, soldered at the pot from wiper to the left lug, if the readings jitter."
|
||||||
|
"Screen extras","blt","NPN or PNP transistor","2N3904 / BC547 (NPN) or 2N3906 / BC557 (PNP)","1","opt","Only for a dimmable backlight. NPN if your BL lights when grounded, PNP if it lights from 3.3 V. Otherwise hard-wire it."
|
||||||
|
"Power and assembly","perf","Stripboard","at least 34 × 28 holes (about 86 × 71 mm)","1","check","Pico socketed at the top edge, so its USB port meets a slot in the back wall."
|
||||||
|
"Power and assembly","hdr","Female header strip, 1 × 20","2.54 mm","2","check","Socket the Pico instead of soldering it in."
|
||||||
|
"Power and assembly","mhdr","Male header strip, 1 × 20","2.54 mm","2","check","Soldered to the Pico so it plugs into the female headers. Skip if yours is a Pico 2 WH (headers fitted)."
|
||||||
|
"Power and assembly","tape","Double-sided foam tape or hot glue","","—","check","Holds the stripboard and the screen without screws."
|
||||||
|
5
docs/hardware/cuts.txt
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
Hole 6 on strips U to Y (5 cuts)
|
||||||
|
Hole 14 on strips U to X (4 cuts)
|
||||||
|
Hole 16 on strips A to T (20 cuts)
|
||||||
|
Hole 23 on strip X (1 cut)
|
||||||
|
Hole 25 on strips U to W (3 cuts)
|
||||||
22
docs/hardware/drill-list.csv
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
where,part,x_mm,y_mm,hole
|
||||||
|
"Lid","Screen wire hole (under the header)","11","28","Ø 8"
|
||||||
|
"Lid","Encoder","15.5","61.5","Ø 7"
|
||||||
|
"Lid","KICK","83.5","20","Ø 12"
|
||||||
|
"Lid","KICK 2","101.2","20","Ø 12"
|
||||||
|
"Lid","SNARE","118.8","20","Ø 12"
|
||||||
|
"Lid","RUN toggle","136.5","20","Ø 6"
|
||||||
|
"Lid","HAT","83.5","40","Ø 12"
|
||||||
|
"Lid","HAT 16","101.2","40","Ø 12"
|
||||||
|
"Lid","PERC","118.8","40","Ø 12"
|
||||||
|
"Lid","GHOST","136.5","40","Ø 12"
|
||||||
|
"Lid","BASS","83.5","60","Ø 12"
|
||||||
|
"Lid","MEL A","101.2","60","Ø 12"
|
||||||
|
"Lid","MEL B","118.8","60","Ø 12"
|
||||||
|
"Lid","PAD","136.5","60","Ø 12"
|
||||||
|
"Lid","ENERGY pot","63","17.3","Ø 7"
|
||||||
|
"Lid","DRIFT pot","63","40","Ø 7"
|
||||||
|
"Lid","SEED","63","62.7","Ø 16"
|
||||||
|
"Back wall","Jack MIDI IN","105","20 down","Ø 6"
|
||||||
|
"Back wall","Jack OUT · KIT","121","20 down","Ø 6"
|
||||||
|
"Back wall","Jack OUT · SYNTH","137","20 down","Ø 6"
|
||||||
|
"Back wall","USB slot (Pico port)","44","16 up from inside floor","12 × 8 slot"
|
||||||
|
46
docs/hardware/panel.md
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
# Panel
|
||||||
|
|
||||||
|
Landscape layout for a plastic enclosure of about 80 × 150 × 40 mm. The interactive tool (`tools/panel.html`) takes your box size and hole diameters and redraws this, with a fit check.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
- **Left zone**: the screen (on top of the lid), with the encoder and RUN under it.
|
||||||
|
- **Middle zone**: ENERGY, DRIFT and SEED.
|
||||||
|
- **Right zone**: the 4 × 3 block, in the same order as the screen map: 11 track buttons, with the RUN toggle in the top-right corner. Check at test fit that the toggle's body clears the OUT SYNTH jack behind it on the back wall.
|
||||||
|
- **Back wall**: the USB slot for the Pico, and three 3.5 mm MIDI jacks (IN, OUT KIT, OUT SYNTH).
|
||||||
|
|
||||||
|
## Inside the box
|
||||||
|
|
||||||
|
- **Stripboard**: it lies on the floor with its top edge (strip A) against the back wall.
|
||||||
|
- **USB**: the Pico's micro-USB socket ends up about 41 mm from the board's left edge and about 16 mm above the *inside* floor. That stack is foam tape ~1, board 1.6, female header 8.5, the Pico's header spacer 2.5, Pico 1, half the socket ~1.4. Measure your own before cutting the slot.
|
||||||
|
- **Slot size**: cut it big enough for the plug's moulding (about 12 × 8 mm), not just the metal tip. The plug has to reach the socket through the wall's thickness.
|
||||||
|
- **Fit**: the board is 71 mm front to back, which fits the box's inside depth with a little to spare. Pot and button bodies hang down from the lid above it.
|
||||||
|
- **Under SEED**: R4 (lying flat) and pads P3/P4 sit right under SEED. Keep those two wires low and check how deep your SEED button reaches before closing the box.
|
||||||
|
|
||||||
|
## Drill list
|
||||||
|
|
||||||
|
Coordinates are from the lid's top-left corner, in mm, for the default sizes in the tool. Wall holes use x as drawn, which matches the lid view; heights are from the inside floor. Re-run the tool with your measured parts before drilling.
|
||||||
|
|
||||||
|
| Where | Part | x mm | y mm | Hole |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| Lid | Screen wire hole (under the header) | 11 | 28 | Ø 8 |
|
||||||
|
| Lid | Encoder | 15.5 | 61.5 | Ø 7 |
|
||||||
|
| Lid | KICK | 83.5 | 20 | Ø 12 |
|
||||||
|
| Lid | KICK 2 | 101.2 | 20 | Ø 12 |
|
||||||
|
| Lid | SNARE | 118.8 | 20 | Ø 12 |
|
||||||
|
| Lid | RUN toggle | 136.5 | 20 | Ø 6 |
|
||||||
|
| Lid | HAT | 83.5 | 40 | Ø 12 |
|
||||||
|
| Lid | HAT 16 | 101.2 | 40 | Ø 12 |
|
||||||
|
| Lid | PERC | 118.8 | 40 | Ø 12 |
|
||||||
|
| Lid | GHOST | 136.5 | 40 | Ø 12 |
|
||||||
|
| Lid | BASS | 83.5 | 60 | Ø 12 |
|
||||||
|
| Lid | MEL A | 101.2 | 60 | Ø 12 |
|
||||||
|
| Lid | MEL B | 118.8 | 60 | Ø 12 |
|
||||||
|
| Lid | PAD | 136.5 | 60 | Ø 12 |
|
||||||
|
| Lid | ENERGY pot | 63 | 17.3 | Ø 7 |
|
||||||
|
| Lid | DRIFT pot | 63 | 40 | Ø 7 |
|
||||||
|
| Lid | SEED | 63 | 62.7 | Ø 16 |
|
||||||
|
| Back wall | Jack MIDI IN | 105 | 20 down | Ø 6 |
|
||||||
|
| Back wall | Jack OUT · KIT | 121 | 20 down | Ø 6 |
|
||||||
|
| Back wall | Jack OUT · SYNTH | 137 | 20 down | Ø 6 |
|
||||||
|
| Back wall | USB slot (Pico port) | 44 | 16 up from inside floor | 12 × 8 slot |
|
||||||
44
docs/hardware/pinout.md
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
# Pinout (Pico 2 W)
|
||||||
|
|
||||||
|
| Pico pin | GPIO | Function | Goes to | Notes |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| 1 | GP0 | UART0 TX | OUT KIT tip, via 10 Ω | drive strength 8–12 mA |
|
||||||
|
| 2 | GP1 | UART0 RX | 6N137 output (pin 6), 1 kΩ pull-up to 3.3 V | MIDI IN |
|
||||||
|
| 4 | GP2 | matrix row 1 | KICK, KICK 2, SNARE (col 4 empty) | input, pull-up |
|
||||||
|
| 5 | GP3 | matrix row 2 | HAT, HAT 16, PERC, GHOST | input, pull-up |
|
||||||
|
| 6 | GP4 | UART1 TX | OUT SYNTH tip, via 10 Ω | drive strength 8–12 mA |
|
||||||
|
| 7 | GP5 | matrix row 3 | BASS, MEL A, MEL B, PAD | input, pull-up |
|
||||||
|
| 9 | GP6 | matrix column 1 | KICK, HAT, BASS | driven low one at a time, else Hi-Z |
|
||||||
|
| 10 | GP7 | matrix column 2 | KICK 2, HAT 16, MEL A | 〃 |
|
||||||
|
| 11 | GP8 | matrix column 3 | SNARE, PERC, MEL B | 〃 |
|
||||||
|
| 12 | GP9 | matrix column 4 | GHOST, PAD | 〃 |
|
||||||
|
| 14 | GP10 | encoder A (SIA) | encoder | pull-up |
|
||||||
|
| 15 | GP11 | encoder B (SIB) | encoder | pull-up |
|
||||||
|
| 16 | GP12 | encoder push (SW) | encoder | pull-up |
|
||||||
|
| 17 | GP13 | SEED | button to ground | pull-up |
|
||||||
|
| 19 | GP14 | RUN | toggle to ground | pull-up |
|
||||||
|
| 22 | GP17 | SPI0 CSn | LCD SCE (pin 3) | can be tied to GND instead |
|
||||||
|
| 24 | GP18 | SPI0 SCK | LCD SCLK (pin 7) | |
|
||||||
|
| 25 | GP19 | SPI0 TX | LCD DN / MOSI (pin 6) | |
|
||||||
|
| 26 | GP20 | GPIO | LCD D/C (pin 5) | |
|
||||||
|
| 27 | GP21 | GPIO | LCD RST (pin 4) | |
|
||||||
|
| 31 | GP26 | ADC0 | ENERGY pot wiper | |
|
||||||
|
| 32 | GP27 | ADC1 | DRIFT pot wiper | |
|
||||||
|
| 33 | AGND | | pots, left lugs | |
|
||||||
|
| 36 | 3V3 | | 3V3 strip (AB): LCD, encoder, pots, MIDI OUT rings via 33 Ω, opto pull-up | |
|
||||||
|
| 40 | VBUS | | 5 V strip (Z): 6N137 VCC and enable only | powered from the Pico's USB |
|
||||||
|
| 23 | GND | | GND strip (AA) | all Pico GND pins are common |
|
||||||
|
|
||||||
|
Free: GP15, GP16, GP22, GP28 (ADC2), and GP17 if SCE is tied to ground. The LCD backlight runs straight from 3.3 V through 330 Ω; GP22 could dim it later through a transistor.
|
||||||
|
|
||||||
|
## Firmware notes for these pins
|
||||||
|
|
||||||
|
- **Pull-ups, not pull-downs.** RP2350 erratum E9 makes the internal pull-downs unreliable on inputs, so every input here is active-low with a pull-up.
|
||||||
|
- **Drive strength.** GP0 and GP4 drive the MIDI current loops directly, about 6–8 mA. That's above the RP2350's default 4 mA drive, so set them to 8 or 12 mA with `gpio_set_drive_strength()`.
|
||||||
|
- **Matrix.**
|
||||||
|
- Drive one column low at a time and leave the others Hi-Z, then read the three rows. On the idle columns, disable the input buffer (`gpio_set_input_enabled(pin, false)`) or enable the pull-up, so erratum E9 leakage can't hold them at a middle level.
|
||||||
|
- The diode bands point to the column.
|
||||||
|
- A short settle (a few µs) after switching columns helps.
|
||||||
|
- Debounce in software.
|
||||||
|
- **UART.** Both UARTs run at 31 250 baud, 8N1. MIDI IN is only used to learn pads, so it can be polled.
|
||||||
|
- **Nokia 5110.** It runs at 3.3 V on SPI up to 4 MHz.
|
||||||
2
docs/hardware/svg/button-matrix.svg
Normal file
|
After Width: | Height: | Size: 9.5 KiB |
2
docs/hardware/svg/encoder-pins.svg
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<svg id="enc" viewBox="0 0 300 80" role="img" aria-label="Encoder module pins" xmlns="http://www.w3.org/2000/svg" style="background:#F6F7F9"><rect x="72" y="14" width="12" height="12" fill="#111" stroke="#D4AF37" stroke-width="1.5"></rect><line x1="78" y1="26" x2="78" y2="52" stroke="#D7263D" stroke-width="4" stroke-linecap="round"></line><text x="78" y="10" text-anchor="middle" font-family="'Martian Mono', ui-monospace, Menlo, Consolas, monospace" font-size="9" fill="#18202A">VCC</text><text x="78" y="68" text-anchor="middle" font-family="'Martian Mono', ui-monospace, Menlo, Consolas, monospace" font-size="10" font-weight="700" fill="#18202A">AB4</text><rect x="108" y="14" width="12" height="12" fill="#111" stroke="#D4AF37" stroke-width="1.5"></rect><line x1="114" y1="26" x2="114" y2="52" stroke="#1E1E1E" stroke-width="4" stroke-linecap="round"></line><text x="114" y="10" text-anchor="middle" font-family="'Martian Mono', ui-monospace, Menlo, Consolas, monospace" font-size="9" fill="#18202A">GND</text><text x="114" y="68" text-anchor="middle" font-family="'Martian Mono', ui-monospace, Menlo, Consolas, monospace" font-size="10" font-weight="700" fill="#18202A">AA4</text><rect x="144" y="14" width="12" height="12" fill="#111" stroke="#D4AF37" stroke-width="1.5"></rect><line x1="150" y1="26" x2="150" y2="52" stroke="#2E9E5B" stroke-width="4" stroke-linecap="round"></line><text x="150" y="10" text-anchor="middle" font-family="'Martian Mono', ui-monospace, Menlo, Consolas, monospace" font-size="9" fill="#18202A">SW</text><text x="150" y="68" text-anchor="middle" font-family="'Martian Mono', ui-monospace, Menlo, Consolas, monospace" font-size="10" font-weight="700" fill="#18202A">P2</text><rect x="180" y="14" width="12" height="12" fill="#111" stroke="#D4AF37" stroke-width="1.5"></rect><line x1="186" y1="26" x2="186" y2="52" stroke="#2E9E5B" stroke-width="4" stroke-linecap="round"></line><text x="186" y="10" text-anchor="middle" font-family="'Martian Mono', ui-monospace, Menlo, Consolas, monospace" font-size="9" fill="#18202A">SIB</text><text x="186" y="68" text-anchor="middle" font-family="'Martian Mono', ui-monospace, Menlo, Consolas, monospace" font-size="10" font-weight="700" fill="#18202A">O2</text><rect x="216" y="14" width="12" height="12" fill="#111" stroke="#D4AF37" stroke-width="1.5"></rect><line x1="222" y1="26" x2="222" y2="52" stroke="#2E9E5B" stroke-width="4" stroke-linecap="round"></line><text x="222" y="10" text-anchor="middle" font-family="'Martian Mono', ui-monospace, Menlo, Consolas, monospace" font-size="9" fill="#18202A">SIA</text><text x="222" y="68" text-anchor="middle" font-family="'Martian Mono', ui-monospace, Menlo, Consolas, monospace" font-size="10" font-weight="700" fill="#18202A">N2</text></svg>
|
||||||
|
After Width: | Height: | Size: 2.7 KiB |
2
docs/hardware/svg/lcd-pins.svg
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<svg id="lcd" viewBox="0 0 300 92" role="img" aria-label="LCD header pins" xmlns="http://www.w3.org/2000/svg" style="background:#F6F7F9"><rect x="30.25" y="14" width="12" height="12" fill="#111" stroke="#D4AF37" stroke-width="1.5"></rect><line x1="36.25" y1="26" x2="36.25" y2="52" stroke="#D7263D" stroke-width="4" stroke-linecap="round"></line><text x="36.25" y="10" text-anchor="middle" font-family="'Martian Mono', ui-monospace, Menlo, Consolas, monospace" font-size="8" fill="#18202A">1 VCC</text><text x="36.25" y="68" text-anchor="middle" font-family="'Martian Mono', ui-monospace, Menlo, Consolas, monospace" font-size="10" font-weight="700" fill="#18202A">AB23</text><rect x="62.75" y="14" width="12" height="12" fill="#111" stroke="#D4AF37" stroke-width="1.5"></rect><line x1="68.75" y1="26" x2="68.75" y2="52" stroke="#1E1E1E" stroke-width="4" stroke-linecap="round"></line><text x="68.75" y="10" text-anchor="middle" font-family="'Martian Mono', ui-monospace, Menlo, Consolas, monospace" font-size="8" fill="#18202A">2 GND</text><text x="68.75" y="68" text-anchor="middle" font-family="'Martian Mono', ui-monospace, Menlo, Consolas, monospace" font-size="10" font-weight="700" fill="#18202A">AA23</text><rect x="95.25" y="14" width="12" height="12" fill="#111" stroke="#D4AF37" stroke-width="1.5"></rect><line x1="101.25" y1="26" x2="101.25" y2="52" stroke="#2F6FDB" stroke-width="4" stroke-linecap="round"></line><text x="101.25" y="10" text-anchor="middle" font-family="'Martian Mono', ui-monospace, Menlo, Consolas, monospace" font-size="8" fill="#18202A">3 SCE</text><text x="101.25" y="68" text-anchor="middle" font-family="'Martian Mono', ui-monospace, Menlo, Consolas, monospace" font-size="10" font-weight="700" fill="#18202A">S27</text><rect x="127.75" y="14" width="12" height="12" fill="#111" stroke="#D4AF37" stroke-width="1.5"></rect><line x1="133.75" y1="26" x2="133.75" y2="52" stroke="#2F6FDB" stroke-width="4" stroke-linecap="round"></line><text x="133.75" y="10" text-anchor="middle" font-family="'Martian Mono', ui-monospace, Menlo, Consolas, monospace" font-size="8" fill="#18202A">4 RST</text><text x="133.75" y="68" text-anchor="middle" font-family="'Martian Mono', ui-monospace, Menlo, Consolas, monospace" font-size="10" font-weight="700" fill="#18202A">N27</text><rect x="160.25" y="14" width="12" height="12" fill="#111" stroke="#D4AF37" stroke-width="1.5"></rect><line x1="166.25" y1="26" x2="166.25" y2="52" stroke="#2F6FDB" stroke-width="4" stroke-linecap="round"></line><text x="166.25" y="10" text-anchor="middle" font-family="'Martian Mono', ui-monospace, Menlo, Consolas, monospace" font-size="8" fill="#18202A">5 D/C</text><text x="166.25" y="68" text-anchor="middle" font-family="'Martian Mono', ui-monospace, Menlo, Consolas, monospace" font-size="10" font-weight="700" fill="#18202A">O27</text><rect x="192.75" y="14" width="12" height="12" fill="#111" stroke="#D4AF37" stroke-width="1.5"></rect><line x1="198.75" y1="26" x2="198.75" y2="52" stroke="#2F6FDB" stroke-width="4" stroke-linecap="round"></line><text x="198.75" y="10" text-anchor="middle" font-family="'Martian Mono', ui-monospace, Menlo, Consolas, monospace" font-size="8" fill="#18202A">6 DN</text><text x="198.75" y="68" text-anchor="middle" font-family="'Martian Mono', ui-monospace, Menlo, Consolas, monospace" font-size="10" font-weight="700" fill="#18202A">P27</text><rect x="225.25" y="14" width="12" height="12" fill="#111" stroke="#D4AF37" stroke-width="1.5"></rect><line x1="231.25" y1="26" x2="231.25" y2="52" stroke="#2F6FDB" stroke-width="4" stroke-linecap="round"></line><text x="231.25" y="10" text-anchor="middle" font-family="'Martian Mono', ui-monospace, Menlo, Consolas, monospace" font-size="8" fill="#18202A">7 SCLK</text><text x="231.25" y="68" text-anchor="middle" font-family="'Martian Mono', ui-monospace, Menlo, Consolas, monospace" font-size="10" font-weight="700" fill="#18202A">Q27</text><rect x="257.75" y="14" width="12" height="12" fill="#111" stroke="#D4AF37" stroke-width="1.5"></rect><line x1="263.75" y1="26" x2="263.75" y2="52" stroke="#D7263D" stroke-width="4" stroke-linecap="round"></line><text x="263.75" y="10" text-anchor="middle" font-family="'Martian Mono', ui-monospace, Menlo, Consolas, monospace" font-size="8" fill="#18202A">8 LED</text><text x="263.75" y="68" text-anchor="middle" font-family="'Martian Mono', ui-monospace, Menlo, Consolas, monospace" font-size="10" font-weight="700" fill="#18202A">AB22</text></svg>
|
||||||
|
After Width: | Height: | Size: 4.4 KiB |
2
docs/hardware/svg/midi-jacks-trs.svg
Normal file
|
After Width: | Height: | Size: 5.5 KiB |
2
docs/hardware/svg/panel-landscape.svg
Normal file
|
After Width: | Height: | Size: 11 KiB |
2
docs/hardware/svg/pots-seed-run.svg
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<svg id="misc" viewBox="0 0 300 150" role="img" aria-label="Pots, SEED and RUN wiring" xmlns="http://www.w3.org/2000/svg" style="background:#F6F7F9"><circle cx="45" cy="42" r="20" fill="#2C3238"></circle><text x="45" y="14" text-anchor="middle" font-family="'Martian Mono', ui-monospace, Menlo, Consolas, monospace" font-size="10" fill="#18202A">ENERGY</text><rect x="30.5" y="62" width="5" height="12" fill="#C9CFD5"></rect><line x1="33" y1="74" x2="33" y2="96" stroke="#1E1E1E" stroke-width="4" stroke-linecap="round"></line><text x="33" y="108" text-anchor="middle" font-family="'Martian Mono', ui-monospace, Menlo, Consolas, monospace" font-size="9" font-weight="700" fill="#18202A">H27</text><rect x="42.5" y="62" width="5" height="12" fill="#C9CFD5"></rect><line x1="45" y1="74" x2="45" y2="96" stroke="#2F6FDB" stroke-width="4" stroke-linecap="round"></line><text x="45" y="108" text-anchor="middle" font-family="'Martian Mono', ui-monospace, Menlo, Consolas, monospace" font-size="9" font-weight="700" fill="#18202A">J27</text><rect x="54.5" y="62" width="5" height="12" fill="#C9CFD5"></rect><line x1="57" y1="74" x2="57" y2="96" stroke="#D7263D" stroke-width="4" stroke-linecap="round"></line><text x="57" y="108" text-anchor="middle" font-family="'Martian Mono', ui-monospace, Menlo, Consolas, monospace" font-size="9" font-weight="700" fill="#18202A">AB25</text><circle cx="115" cy="42" r="20" fill="#2C3238"></circle><text x="115" y="14" text-anchor="middle" font-family="'Martian Mono', ui-monospace, Menlo, Consolas, monospace" font-size="10" fill="#18202A">DRIFT</text><rect x="100.5" y="62" width="5" height="12" fill="#C9CFD5"></rect><line x1="103" y1="74" x2="103" y2="96" stroke="#1E1E1E" stroke-width="4" stroke-linecap="round"></line><text x="103" y="108" text-anchor="middle" font-family="'Martian Mono', ui-monospace, Menlo, Consolas, monospace" font-size="9" font-weight="700" fill="#18202A">H27</text><rect x="112.5" y="62" width="5" height="12" fill="#C9CFD5"></rect><line x1="115" y1="74" x2="115" y2="96" stroke="#2F6FDB" stroke-width="4" stroke-linecap="round"></line><text x="115" y="108" text-anchor="middle" font-family="'Martian Mono', ui-monospace, Menlo, Consolas, monospace" font-size="9" font-weight="700" fill="#18202A">I27</text><rect x="124.5" y="62" width="5" height="12" fill="#C9CFD5"></rect><line x1="127" y1="74" x2="127" y2="96" stroke="#D7263D" stroke-width="4" stroke-linecap="round"></line><text x="127" y="108" text-anchor="middle" font-family="'Martian Mono', ui-monospace, Menlo, Consolas, monospace" font-size="9" font-weight="700" fill="#18202A">AB25</text><circle cx="190" cy="42" r="16" fill="#2C3238"></circle><text x="190" y="14" text-anchor="middle" font-family="'Martian Mono', ui-monospace, Menlo, Consolas, monospace" font-size="10" fill="#18202A">SEED</text><line x1="183" y1="58" x2="183" y2="96" stroke="#D9A600" stroke-width="4" stroke-linecap="round"></line><text x="183" y="108" text-anchor="middle" font-family="'Martian Mono', ui-monospace, Menlo, Consolas, monospace" font-size="9" font-weight="700" fill="#18202A">Q2</text><line x1="197" y1="58" x2="197" y2="96" stroke="#1E1E1E" stroke-width="4" stroke-linecap="round"></line><text x="197" y="108" text-anchor="middle" font-family="'Martian Mono', ui-monospace, Menlo, Consolas, monospace" font-size="9" font-weight="700" fill="#18202A">AA6</text><rect x="240" y="30" width="34" height="24" rx="3" fill="#2C3238"></rect><text x="257" y="14" text-anchor="middle" font-family="'Martian Mono', ui-monospace, Menlo, Consolas, monospace" font-size="10" fill="#18202A">RUN</text><rect x="243.5" y="54" width="5" height="10" fill="#C9CFD5"></rect><rect x="254.5" y="54" width="5" height="10" fill="#C9CFD5"></rect><line x1="257" y1="64" x2="257" y2="96" stroke="#D9A600" stroke-width="4" stroke-linecap="round"></line><text x="257" y="108" text-anchor="middle" font-family="'Martian Mono', ui-monospace, Menlo, Consolas, monospace" font-size="9" font-weight="700" fill="#18202A">S2</text><rect x="265.5" y="54" width="5" height="10" fill="#C9CFD5"></rect><line x1="268" y1="64" x2="268" y2="96" stroke="#1E1E1E" stroke-width="4" stroke-linecap="round"></line><text x="268" y="108" text-anchor="middle" font-family="'Martian Mono', ui-monospace, Menlo, Consolas, monospace" font-size="9" font-weight="700" fill="#18202A">AA6</text><text x="150" y="138" text-anchor="middle" font-family="'Martian Mono', ui-monospace, Menlo, Consolas, monospace" font-size="9" fill="#566271">lugs facing down, seen from the front</text></svg>
|
||||||
|
After Width: | Height: | Size: 4.5 KiB |
2
docs/hardware/svg/stripboard-copper-side.svg
Normal file
|
After Width: | Height: | Size: 70 KiB |
2
docs/hardware/svg/stripboard-parts-side.svg
Normal file
|
After Width: | Height: | Size: 98 KiB |
2
docs/hardware/svg/wire-map.svg
Normal file
|
After Width: | Height: | Size: 30 KiB |
36
docs/hardware/wire-list.csv
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
pad,board_hole,colour,pico_pin_or_signal,goes_to
|
||||||
|
"P1","Y1","blue","via R1 to opto","MIDI IN jack · ring (pin 4)"
|
||||||
|
"P2","W1","blue","opto cathode","MIDI IN jack · tip (pin 5)"
|
||||||
|
"P3","X26","blue","via R4 (10 Ω) from GP0","OUT KIT jack · tip (pin 5)"
|
||||||
|
"P4","V22","red","via R5 (33 Ω) from 3V3","OUT KIT jack · ring (pin 4)"
|
||||||
|
"P5","AA22","black","GND strip","OUT KIT jack · sleeve (pin 2)"
|
||||||
|
"P6","U31","blue","via R6 (10 Ω) from GP4","OUT SYNTH jack · tip (pin 5)"
|
||||||
|
"P7","V31","red","via R7 (33 Ω) from 3V3","OUT SYNTH jack · ring (pin 4)"
|
||||||
|
"P8","AA31","black","GND strip","OUT SYNTH jack · sleeve (pin 2)"
|
||||||
|
"P9","N27","blue","GP21","LCD · RST (pin 4)"
|
||||||
|
"P10","S27","blue","GP17","LCD · SCE (pin 3)"
|
||||||
|
"P11","O27","blue","GP20","LCD · D/C (pin 5)"
|
||||||
|
"P12","P27","blue","GP19","LCD · DN/MOSI (pin 6)"
|
||||||
|
"P13","Q27","blue","GP18","LCD · SCLK (pin 7)"
|
||||||
|
"P14","AB23","red","3V3 strip","LCD · VCC (pin 1)"
|
||||||
|
"P15","AA23","black","GND strip","LCD · GND (pin 2)"
|
||||||
|
"P16","AB22","red","3V3 strip","LCD · LED (pin 8) through 330 Ω"
|
||||||
|
"P17","N2","green","GP10","Encoder · SIA"
|
||||||
|
"P18","O2","green","GP11","Encoder · SIB"
|
||||||
|
"P19","P2","green","GP12","Encoder · SW"
|
||||||
|
"P20","AB4","red","3V3 strip","Encoder · VCC"
|
||||||
|
"P21","AA4","black","GND strip","Encoder · GND"
|
||||||
|
"P22","J27","blue","GP26","ENERGY pot · middle lug"
|
||||||
|
"P23","I27","blue","GP27","DRIFT pot · middle lug"
|
||||||
|
"P24","AB25","red","3V3 strip","Pots · right lugs (chain both)"
|
||||||
|
"P25","H27","black","AGND","Pots · left lugs (chain both)"
|
||||||
|
"P26","Q2","yellow","GP13","SEED · one lug"
|
||||||
|
"P27","S2","yellow","GP14","RUN toggle · middle lug"
|
||||||
|
"P28","AA6","black","GND strip","Panel ground bus · SEED other lug, RUN one outer lug"
|
||||||
|
"P29","D2","yellow","GP2","Matrix row 1 (KICK, KICK 2, SNARE)"
|
||||||
|
"P30","E2","yellow","GP3","Matrix row 2 (HAT, HAT 16, PERC, GHOST)"
|
||||||
|
"P31","G2","yellow","GP5","Matrix row 3 (BASS, MEL A, MEL B, PAD)"
|
||||||
|
"P32","I2","green","GP6","Matrix column 1 (KICK, HAT, BASS)"
|
||||||
|
"P33","J2","green","GP7","Matrix column 2 (KICK 2, HAT 16, MEL A)"
|
||||||
|
"P34","K2","green","GP8","Matrix column 3 (SNARE, PERC, MEL B)"
|
||||||
|
"P35","L2","green","GP9","Matrix column 4 (GHOST, PAD)"
|
||||||
|
239
docs/hardware/wiring.md
Normal file
@@ -0,0 +1,239 @@
|
|||||||
|
# Wiring
|
||||||
|
|
||||||
|
One stripboard carries the Pico and the MIDI circuits. Everything on the panel (screen, encoder, pots, buttons, jacks) connects to it with hookup wire.
|
||||||
|
|
||||||
|
**Board positions** are strip letter + hole number. For example V4 is strip V, hole 4, counted from the left with the parts facing you. Strips run left to right: A at the top (the USB edge), then Z, AA, AB at the bottom.
|
||||||
|
|
||||||
|
The interactive version, with the same drawings, is `tools/wiring.html`. Generated from [`hardware/layout.js`](../../hardware/layout.js).
|
||||||
|
|
||||||
|
## Wire map: panel to board
|
||||||
|
|
||||||
|
Every panel wire and the board hole it lands in: strip letter, then hole number counted from the opto end.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
| Part | Lug | Wire | Board hole |
|
||||||
|
|---|---|---|---|
|
||||||
|
| Buttons | Row 1 · KICK, KICK 2, SNARE | yellow | **D2** |
|
||||||
|
| | Row 2 · HAT, HAT 16, PERC, GHOST | yellow | **E2** |
|
||||||
|
| | Row 3 · BASS, MEL A, MEL B, PAD | yellow | **G2** |
|
||||||
|
| | Col 1 · KICK, HAT, BASS (diode band side) | green | **I2** |
|
||||||
|
| | Col 2 · KICK 2, HAT 16, MEL A | green | **J2** |
|
||||||
|
| | Col 3 · SNARE, PERC, MEL B | green | **K2** |
|
||||||
|
| | Col 4 · GHOST, PAD | green | **L2** |
|
||||||
|
| SEED | one lug | yellow | **Q2** |
|
||||||
|
| RUN toggle | middle lug (it sits in the button grid, row 1 / col 4) | yellow | **S2** |
|
||||||
|
| SEED + RUN | ground: SEED other lug, RUN one outer lug | black | **AA6** |
|
||||||
|
| Encoder | VCC (3.3 V, never 5 V) | red | **AB4** |
|
||||||
|
| | GND | black | **AA4** |
|
||||||
|
| | SW | green | **P2** |
|
||||||
|
| | SIB | green | **O2** |
|
||||||
|
| | SIA | green | **N2** |
|
||||||
|
| ENERGY pot | middle lug | blue | **J27** |
|
||||||
|
| DRIFT pot | middle lug | blue | **I27** |
|
||||||
|
| Pots | right lugs, both (3.3 V) | red | **AB25** |
|
||||||
|
| | left lugs, both (AGND) | black | **H27** |
|
||||||
|
| LCD | 1 · VCC (3.3 V) | red | **AB23** |
|
||||||
|
| | 2 · GND | black | **AA23** |
|
||||||
|
| | 3 · SCE | blue | **S27** |
|
||||||
|
| | 4 · RST | blue | **N27** |
|
||||||
|
| | 5 · D/C | blue | **O27** |
|
||||||
|
| | 6 · DN (MOSI) | blue | **P27** |
|
||||||
|
| | 7 · SCLK | blue | **Q27** |
|
||||||
|
| | 8 · LED, through a 330 Ω resistor (if dark, move to any AA hole) | red | **AB22** |
|
||||||
|
| MIDI IN jack | tip | blue | **W1** |
|
||||||
|
| | ring | blue | **Y1** |
|
||||||
|
| OUT KIT jack | tip | blue | **X26** |
|
||||||
|
| | ring | red | **V22** |
|
||||||
|
| | sleeve | black | **AA22** |
|
||||||
|
| OUT SYNTH jack | tip | blue | **U31** |
|
||||||
|
| | ring | red | **V31** |
|
||||||
|
| | sleeve | black | **AA31** |
|
||||||
|
|
||||||
|
## Wire colours
|
||||||
|
|
||||||
|
| Colour | Role |
|
||||||
|
|---|---|
|
||||||
|
| Red | 3.3 V and the MIDI OUT ring lugs |
|
||||||
|
| Black | ground |
|
||||||
|
| Yellow | button rows, SEED, RUN |
|
||||||
|
| Green | button columns, encoder |
|
||||||
|
| Blue | screen, pots, MIDI signals |
|
||||||
|
| Grey | short links on the board |
|
||||||
|
|
||||||
|
If your spools are other colours, keep the roles and rename them. Red and black for power and ground matter most.
|
||||||
|
|
||||||
|
## Board · parts side
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
- **Size**: 34 holes × 28 strips, about 86 × 71 mm.
|
||||||
|
- **Pico**: sits in two 20-pin female headers with pins 1 and 40 on strip A. Its USB end is flush with the top edge of the board, and the micro-USB socket reaches just past it.
|
||||||
|
- **Mounting**: lay the board on the box floor with that edge against the back wall, behind the USB slot. The socket is about 41 mm from the board's left edge.
|
||||||
|
- **Pads**: the numbered dots P1 … P35 are where off-board wires solder.
|
||||||
|
|
||||||
|
## Board · copper side, for cutting
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
This view is flipped: hole 1 is on the right.
|
||||||
|
|
||||||
|
How to cut:
|
||||||
|
|
||||||
|
- Use a 3–4 mm drill bit turned by hand, or a spot-face cutter.
|
||||||
|
- Check each cut with the meter's continuity beeper.
|
||||||
|
|
||||||
|
- Hole 6 on strips U to Y (5 cuts)
|
||||||
|
- Hole 14 on strips U to X (4 cuts)
|
||||||
|
- Hole 16 on strips A to T (20 cuts)
|
||||||
|
- Hole 23 on strip X (1 cut)
|
||||||
|
- Hole 25 on strips U to W (3 cuts)
|
||||||
|
|
||||||
|
### Power strips along the bottom
|
||||||
|
|
||||||
|
- **Z**: 5 V from the Pico's VBUS. Only the opto uses it.
|
||||||
|
- **AA**: ground.
|
||||||
|
- **AB**: 3.3 V from the Pico's 3V3 pin.
|
||||||
|
|
||||||
|
Every red wire to the panel starts on AB. Every black wire starts on AA, except the pots, which use the AGND strip.
|
||||||
|
|
||||||
|
## Board parts and links
|
||||||
|
|
||||||
|
| Part | Value | Holes | Note |
|
||||||
|
|---|---|---|---|
|
||||||
|
| Pico 2W | 2 × 20-pin female header | A13–T13 and A20–T20 (pins 1–20 down the left, 40–21 down the right) | USB end flush with the top edge of the board, past strip A |
|
||||||
|
| U1 | 6N137 | U5–X5, U8–X8 | 6N137 in a DIP-8 socket. Notch toward strip U, pin 1 at hole U5. Pin 7 (enable) is tied to pin 8 by the U10–V10 link. |
|
||||||
|
| R1 | 220 Ω | V2 – Y2 | MIDI IN current limit. 3-hole span: stand it upright, or use a small ⅛ W part flat. If learning ever misses notes, use 150 Ω. |
|
||||||
|
| D1 | 1N4148 | V4 – W4 | Band (cathode) on V4, the opto LED anode side. Stands upright (1-hole span). |
|
||||||
|
| R2 | 1 kΩ | W10 – AB10 | Opto output pull-up to 3.3 V |
|
||||||
|
| C1 | 100 nF | U11 – X11 | 6N137 supply bypass, right beside the chip: U11 is its 5 V (pin 8) strip, X11 its GND (pin 5) strip. Required. Fit it after the beep test. |
|
||||||
|
| R4 | 10 Ω | X21 – X25 | MIDI OUT KIT, TX line (RC). Lies flat along strip X, across the cut at X23 (4-hole span, ¼ W fits). SEED hangs right above it. |
|
||||||
|
| R5 | 33 Ω ½ W | V19 – AB19 | MIDI OUT KIT, 3.3 V line (RA). ½ W: a mono plug in the jack shorts it to ground. |
|
||||||
|
| R6 | 10 Ω | U28 – Y28 | MIDI OUT SYNTH, TX line (RC) |
|
||||||
|
| R7 | 33 Ω ½ W | V30 – AB30 | MIDI OUT SYNTH, 3.3 V line (RA). ½ W, same reason as R5. |
|
||||||
|
| Link | wire | A34 – Z34 | 5 V (VBUS) to the 5 V strip |
|
||||||
|
| Link | wire | E32 – AB32 | 3V3 to the 3V3 strip |
|
||||||
|
| Link | wire | R33 – AA33 | GND to the GND strip |
|
||||||
|
| Link | wire | U13 – Z13 | opto VCC to 5 V |
|
||||||
|
| Link | wire | U10 – V10 | opto enable (pin 7) to VCC (pin 8): the 6N137 output only works with pin 7 high |
|
||||||
|
| Link | wire | X12 – AA12 | opto GND to GND |
|
||||||
|
| Link | wire | W9 – B9 | opto out to GP1 (MIDI RX), strip B |
|
||||||
|
| Link | wire | A15 – X15 | GP0 (TX KIT) to strip X (R4), straight down under the Pico between its headers |
|
||||||
|
| Link | wire | F14 – Y14 | GP4 (TX SYNTH) to strip Y (R6), straight down under the Pico between its headers |
|
||||||
|
|
||||||
|
## Off-board wires
|
||||||
|
|
||||||
|
| Pad | Board hole | Colour | Pico pin / signal | Goes to |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| P1 | Y1 | blue | via R1 to opto | MIDI IN jack · ring (pin 4) |
|
||||||
|
| P2 | W1 | blue | opto cathode | MIDI IN jack · tip (pin 5) |
|
||||||
|
| P3 | X26 | blue | via R4 (10 Ω) from GP0 | OUT KIT jack · tip (pin 5) |
|
||||||
|
| P4 | V22 | red | via R5 (33 Ω) from 3V3 | OUT KIT jack · ring (pin 4) |
|
||||||
|
| P5 | AA22 | black | GND strip | OUT KIT jack · sleeve (pin 2) |
|
||||||
|
| P6 | U31 | blue | via R6 (10 Ω) from GP4 | OUT SYNTH jack · tip (pin 5) |
|
||||||
|
| P7 | V31 | red | via R7 (33 Ω) from 3V3 | OUT SYNTH jack · ring (pin 4) |
|
||||||
|
| P8 | AA31 | black | GND strip | OUT SYNTH jack · sleeve (pin 2) |
|
||||||
|
| P9 | N27 | blue | GP21 | LCD · RST (pin 4) |
|
||||||
|
| P10 | S27 | blue | GP17 | LCD · SCE (pin 3) |
|
||||||
|
| P11 | O27 | blue | GP20 | LCD · D/C (pin 5) |
|
||||||
|
| P12 | P27 | blue | GP19 | LCD · DN/MOSI (pin 6) |
|
||||||
|
| P13 | Q27 | blue | GP18 | LCD · SCLK (pin 7) |
|
||||||
|
| P14 | AB23 | red | 3V3 strip | LCD · VCC (pin 1) |
|
||||||
|
| P15 | AA23 | black | GND strip | LCD · GND (pin 2) |
|
||||||
|
| P16 | AB22 | red | 3V3 strip | LCD · LED (pin 8) through 330 Ω |
|
||||||
|
| P17 | N2 | green | GP10 | Encoder · SIA |
|
||||||
|
| P18 | O2 | green | GP11 | Encoder · SIB |
|
||||||
|
| P19 | P2 | green | GP12 | Encoder · SW |
|
||||||
|
| P20 | AB4 | red | 3V3 strip | Encoder · VCC |
|
||||||
|
| P21 | AA4 | black | GND strip | Encoder · GND |
|
||||||
|
| P22 | J27 | blue | GP26 | ENERGY pot · middle lug |
|
||||||
|
| P23 | I27 | blue | GP27 | DRIFT pot · middle lug |
|
||||||
|
| P24 | AB25 | red | 3V3 strip | Pots · right lugs (chain both) |
|
||||||
|
| P25 | H27 | black | AGND | Pots · left lugs (chain both) |
|
||||||
|
| P26 | Q2 | yellow | GP13 | SEED · one lug |
|
||||||
|
| P27 | S2 | yellow | GP14 | RUN toggle · middle lug |
|
||||||
|
| P28 | AA6 | black | GND strip | Panel ground bus · SEED other lug, RUN one outer lug |
|
||||||
|
| P29 | D2 | yellow | GP2 | Matrix row 1 (KICK, KICK 2, SNARE) |
|
||||||
|
| P30 | E2 | yellow | GP3 | Matrix row 2 (HAT, HAT 16, PERC, GHOST) |
|
||||||
|
| P31 | G2 | yellow | GP5 | Matrix row 3 (BASS, MEL A, MEL B, PAD) |
|
||||||
|
| P32 | I2 | green | GP6 | Matrix column 1 (KICK, HAT, BASS) |
|
||||||
|
| P33 | J2 | green | GP7 | Matrix column 2 (KICK 2, HAT 16, MEL A) |
|
||||||
|
| P34 | K2 | green | GP8 | Matrix column 3 (SNARE, PERC, MEL B) |
|
||||||
|
| P35 | L2 | green | GP9 | Matrix column 4 (GHOST, PAD) |
|
||||||
|
|
||||||
|
## Panel wiring
|
||||||
|
|
||||||
|
### Button matrix, seen from the back
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
Each button:
|
||||||
|
|
||||||
|
- One lug goes to its row wire (yellow), daisy-chained along the row.
|
||||||
|
- The other lug goes through a diode to its column wire (green), daisy-chained down the column.
|
||||||
|
- The diode's band points to the column. Solder the diodes straight onto the button lugs and sleeve them. 1N4148, 1N4448 or any small-signal diode works.
|
||||||
|
|
||||||
|
11 track buttons, each with its own diode, plus D1 on the board: 12 diodes. GHOST is wired at row 2 / column 4. The RUN toggle sits in the grid's row 1 / column 4 spot but isn't part of the matrix, so row 1's wire stops at SNARE and column 4 starts at GHOST.
|
||||||
|
|
||||||
|
Drawn from behind the panel, the side you solder on: KICK is top-right, RUN top-left.
|
||||||
|
|
||||||
|
### MIDI jacks: 3.5 mm TRS, Type A
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
| Jack | Tip (pin 5) | Ring (pin 4) | Sleeve (pin 2) |
|
||||||
|
|---|---|---|---|
|
||||||
|
| MIDI IN | P2 | P1 | stays empty |
|
||||||
|
| OUT KIT | P3 | P4 | P5 |
|
||||||
|
| OUT SYNTH | P6 | P7 | P8 |
|
||||||
|
|
||||||
|
- **Jack type**: use stereo (TRS) jacks; mono ones have no ring.
|
||||||
|
- **Lug order**: it differs between jack models. Find the tip, ring and sleeve lugs with the meter and a plug inserted.
|
||||||
|
- **Cables**: Type A is the MIDI standard, so a plain stereo 3.5 mm cable goes straight to another Type A device (e.g. SP-404MKII). A DIN device (e.g. MiniFreak) needs a TRS-to-DIN adapter that says **Type A**. A Type B one swaps tip and ring and gives silence; nothing breaks.
|
||||||
|
|
||||||
|
### Nokia 5110 module
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
- Pin order as printed on your module: 1 VCC, 2 GND, 3 SCE, 4 RST, 5 D/C, 6 DN (MOSI), 7 SCLK, 8 LED.
|
||||||
|
- **LED** (backlight): wire it through a 330 Ω resistor to AB22 (3.3 V), with the resistor inline at the module end, sleeved. If it stays dark, move that wire to any free hole on strip AA (ground). GP22 (L27) stays free; it's only needed for dimming later, through a transistor.
|
||||||
|
- **SCE** can go straight to ground instead of S27 if it's the only thing on the SPI bus. That frees GP17.
|
||||||
|
|
||||||
|
### Encoder module
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
Pin order as printed on your module: VCC, GND, SW, SIB, SIA. Power it from 3.3 V (AB4), never 5 V: its pull-up resistors would put 5 V on the Pico's pins. If clockwise steps the wrong way, swap SIA and SIB (or flip it in firmware).
|
||||||
|
|
||||||
|
### Pots, SEED, RUN
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
- **Pots**, lugs facing down, shaft toward you:
|
||||||
|
- left lug to AGND (P25)
|
||||||
|
- middle lug to its pin
|
||||||
|
- right lug to 3.3 V (P24)
|
||||||
|
- Chain the left lugs together and the right lugs together.
|
||||||
|
- If the readings jitter, solder a 100 nF capacitor at each pot, from wiper to left lug.
|
||||||
|
- **SEED**: one lug to P26, the other to the ground bus (P28).
|
||||||
|
- **RUN**: middle lug to P27, one outer lug to the ground bus. If RUN and STOP come out swapped, use the other outer lug.
|
||||||
|
|
||||||
|
## Build order and checks
|
||||||
|
|
||||||
|
### Board
|
||||||
|
|
||||||
|
1. Cut the board to 34 × 28 holes. For the top edge, cut through the row of holes above strip A, then sand it back to about 1.3 mm from the strip A holes, stopping before the strip A copper. That puts the Pico's edge flush with the board. Unsanded, the Pico sits about 1 mm back, which still works if the wall slot lets the plug's moulding through.
|
||||||
|
Make the track cuts and beep each one out.
|
||||||
|
2. Solder the two Pico headers and the DIP-8 socket (notch toward strip U). Then the resistors and the diode (band on V4).
|
||||||
|
- Keep everything low, because the SEED button hangs right above R4. R4 lies flat along strip X, across the cut at X23.
|
||||||
|
- R1's 3-hole span is tight for a ¼ W body, so stand R1 upright or use a small ⅛ W part.
|
||||||
|
- Leave C1 for step 4.
|
||||||
|
3. Solder the grey links with insulated wire. The two long ones (GP0 and GP4) run straight down under the Pico, between its two headers, so fit them before the Pico goes in.
|
||||||
|
4. Before fitting C1, with the Pico and the 6N137 out, check that AB to AA does not beep and Z to AA does not beep. Then fit C1. It can chirp briefly on the beeper once it's in.
|
||||||
|
|
||||||
|
### First power
|
||||||
|
|
||||||
|
1. Plug in the Pico only and power it from USB. Measure AB to AA: about 3.3 V. Z to AA: about 5 V.
|
||||||
|
2. Unplug, then insert the 6N137 (notch toward strip U, pin 1 at hole U5).
|
||||||
|
3. Wire the panel. Before closing the box, check that no bare lugs touch each other.
|
||||||
|
4. Flash the firmware. Set GP0 and GP4 to 8–12 mA drive (see [pinout](pinout.md)).
|
||||||
50
docs/midi-setup.md
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
# MIDI setup
|
||||||
|
|
||||||
|
```
|
||||||
|
Seedbox OUT KIT ──▶ sampler / drum machine MIDI IN
|
||||||
|
Seedbox OUT SYNTH ──▶ poly synth MIDI IN
|
||||||
|
sampler MIDI OUT ──▶ Seedbox IN (only needed to learn pads)
|
||||||
|
```
|
||||||
|
|
||||||
|
All three Seedbox jacks are 3.5 mm TRS, Type A (tip = pin 5, ring = pin 4, sleeve = pin 2).
|
||||||
|
|
||||||
|
| Other end | Cable |
|
||||||
|
|---|---|
|
||||||
|
| 3.5 mm TRS, Type A (e.g. SP-404MKII) | plain stereo 3.5 mm cable |
|
||||||
|
| 5-pin DIN (e.g. MiniFreak) | TRS-to-DIN adapter marked Type A |
|
||||||
|
| 3.5 mm TRS, Type B | Type A ↔ B converter |
|
||||||
|
|
||||||
|
A Type B adapter on a Type A jack swaps tip and ring: you get no MIDI, but nothing is damaged.
|
||||||
|
|
||||||
|
Both OUTs carry MIDI clock (start, 24 PPQN clock, stop) when clock is enabled. Song Position Pointer is not sent.
|
||||||
|
|
||||||
|
## Kit (sampler or drum machine)
|
||||||
|
|
||||||
|
Pads are learned, not typed in, so any sampler or drum machine that sends a note-on per pad works. Seedbox plays exactly the channel and note it learned. To use another bank or kit, re-learn the pads there.
|
||||||
|
|
||||||
|
### Learn a pad
|
||||||
|
|
||||||
|
1. Connect the sampler's MIDI OUT to Seedbox IN. In the simulator, choose it as the input port.
|
||||||
|
2. LEARN page, push the encoder: the screen says PICK TRACK.
|
||||||
|
3. Press a kit track button (KICK … PERC): HIT PAD.
|
||||||
|
4. Hit the pad. Seedbox stores its channel and note.
|
||||||
|
|
||||||
|
Kit tracks that aren't learned stay silent and show a dot on the screen map.
|
||||||
|
|
||||||
|
STOP sends All Sound Off (CC 120) on every learned kit channel. Some samplers ignore All Notes Off (the SP-404MKII does), but they do obey CC 120.
|
||||||
|
|
||||||
|
## Synth
|
||||||
|
|
||||||
|
- Poly mode, so bass, melody and pad can overlap.
|
||||||
|
- Set its MIDI channel to match Seedbox's synth channel (bench setting, default 1).
|
||||||
|
- An octave offset is available on the bench.
|
||||||
|
- STOP sends CC 120 and CC 123.
|
||||||
|
|
||||||
|
## Hardware notes
|
||||||
|
|
||||||
|
- The MIDI OUTs follow the MIDI 1.0 3.3 V electrical spec (CA-033):
|
||||||
|
- TX goes through 10 Ω to the tip (pin 5).
|
||||||
|
- 3.3 V goes through 33 Ω to the ring (pin 4).
|
||||||
|
- The sleeve (pin 2) is ground.
|
||||||
|
- MIDI IN uses a 6N137 optocoupler powered from 5 V, with its enable pin tied high and its output pulled up to 3.3 V.
|
||||||
|
- Details are in [hardware/wiring.md](hardware/wiring.md).
|
||||||
95
docs/superpowers/specs/2026-10-11-scale-root-design.md
Normal file
@@ -0,0 +1,95 @@
|
|||||||
|
# Scale and root selection
|
||||||
|
|
||||||
|
2026-10-11. Status: draft for review.
|
||||||
|
|
||||||
|
## Goal
|
||||||
|
|
||||||
|
Let Gee lock the line (bass, melody, pad) to a chosen root and scale, so Seedbox can play along with an existing musical idea. The seed still makes everything else.
|
||||||
|
|
||||||
|
## Decisions
|
||||||
|
|
||||||
|
| Question | Answer |
|
||||||
|
|---|---|
|
||||||
|
| Scope | Global and sticky. SEED, ROLL and loading a slot don't change it. Slots don't store it. |
|
||||||
|
| Pages | Two: ROOT and SCALE, each with AUTO. |
|
||||||
|
| AUTO | The seed picks from all 21 scales and 12 roots. Every existing seed changes scale (an intended music change). |
|
||||||
|
| Old data | None kept. It's a prototype, so the flash record starts fresh. |
|
||||||
|
|
||||||
|
## Scales
|
||||||
|
|
||||||
|
The menu order is also the table index.
|
||||||
|
|
||||||
|
| # | Screen name | Steps |
|
||||||
|
|---|---|---|
|
||||||
|
| 0 | MAJOR | 0 2 4 5 7 9 11 |
|
||||||
|
| 1 | MINOR | 0 2 3 5 7 8 10 |
|
||||||
|
| 2 | DORIAN | 0 2 3 5 7 9 10 |
|
||||||
|
| 3 | MIXOLYDIAN | 0 2 4 5 7 9 10 |
|
||||||
|
| 4 | LYDIAN | 0 2 4 6 7 9 11 |
|
||||||
|
| 5 | PHRYGIAN | 0 1 3 5 7 8 10 |
|
||||||
|
| 6 | HARM MINOR | 0 2 3 5 7 8 11 |
|
||||||
|
| 7 | MINOR PENT | 0 3 5 7 10 |
|
||||||
|
| 8 | MAJOR PENT | 0 2 4 7 9 |
|
||||||
|
| 9 | PHRYG DOM | 0 1 4 5 7 8 10 |
|
||||||
|
| 10 | DBL HARM | 0 1 4 5 7 8 11 |
|
||||||
|
| 11 | HUNG MINOR | 0 2 3 6 7 8 11 |
|
||||||
|
| 12 | UKR DORIAN | 0 2 3 6 7 9 10 |
|
||||||
|
| 13 | LYD DOM | 0 2 4 6 7 9 10 |
|
||||||
|
| 14 | HIRAJOSHI | 0 2 3 7 8 |
|
||||||
|
| 15 | IN SEN | 0 1 5 7 10 |
|
||||||
|
| 16 | IWATO | 0 1 5 6 10 |
|
||||||
|
| 17 | PROMETHEUS | 0 2 4 6 9 10 |
|
||||||
|
| 18 | WHOLE TONE | 0 2 4 6 8 10 |
|
||||||
|
| 19 | OCTATONIC | 0 1 3 4 6 7 9 10 |
|
||||||
|
| 20 | LOCRIAN | 0 1 3 5 6 8 10 |
|
||||||
|
|
||||||
|
Scales have 5 to 8 notes. Chords stay "every other scale note" (0, 2, 4 from the chord degree), so:
|
||||||
|
- 5-note scales give open chords;
|
||||||
|
- the 6-note whole tone gives augmented chords;
|
||||||
|
- the 8-note octatonic gives diminished chords.
|
||||||
|
|
||||||
|
## Engine (`sim/engine.js`, then `firmware/engine/engine.c`)
|
||||||
|
|
||||||
|
- `SCALES` and `SCALE_NAMES` become the 21-entry table above. In C, `SCALES[21][8]` and `SCALE_LEN[21]`.
|
||||||
|
- `genome()`: `g.scale = r.below(21)`. It's still one draw, so every later draw in the genome is unchanged.
|
||||||
|
- The params get `scale` (-1 = AUTO, 0..20) and `root` (-1 = AUTO, 0..11). `renderBar` overrides `g.scale` and `g.root` when they're ≥ 0, right after `genome()`, the same way AMENIZER overrides `steps`. In C this is `sb_params.scale` and `sb_params.root`.
|
||||||
|
- `describe()` and the simulator's "Key" fact show the effective scale and root.
|
||||||
|
- Integer-only and deterministic, as before.
|
||||||
|
|
||||||
|
## Vectors
|
||||||
|
|
||||||
|
- Run `npm run vectors` (an intended music change; the commit message says so).
|
||||||
|
- Add cases to `test/cases.js` with a fixed scale and root, so the C port is tested on overrides. Cover at least:
|
||||||
|
- a 5-note scale;
|
||||||
|
- the 8-note OCTATONIC;
|
||||||
|
- a root that isn't C;
|
||||||
|
- AUTO.
|
||||||
|
|
||||||
|
## Panel (firmware `app/main.c`, simulator `app.js`)
|
||||||
|
|
||||||
|
- Pages: SEED · GEN · ROOT · SCALE · BPM · SLOT · ROLL · LEARN.
|
||||||
|
- ROOT turns through AUTO, C, C#, …, B. SCALE turns through AUTO, MAJOR, …, LOCRIAN. Edit them like GEN: push, turn, push.
|
||||||
|
- Screen when set: label `ROOT` / `SCALE`, value `D` / `HIRAJOSHI`.
|
||||||
|
- Screen on AUTO: label `ROOT AUTO` / `SCALE AUTO`, value is what the seed picked.
|
||||||
|
- A change is heard from the next rendered bar, the same as GEN.
|
||||||
|
|
||||||
|
## Storage
|
||||||
|
|
||||||
|
- `live_t` gains `scale` and `root`, saved in flash. The record version goes to 2. Old records are ignored and defaults load.
|
||||||
|
- The leftovers from BANK go: the `unused` record field and `pad_t.spare`.
|
||||||
|
- Defaults: both AUTO.
|
||||||
|
- The simulator keeps them in localStorage next to `gen`.
|
||||||
|
|
||||||
|
## Docs
|
||||||
|
|
||||||
|
- `docs/controls.md`: add ROOT and SCALE to the pages table.
|
||||||
|
- `docs/concept.md`: the scale list.
|
||||||
|
- `docs/decisions.md`: add one line on global sticky key, AUTO over all 21.
|
||||||
|
|
||||||
|
## Testing
|
||||||
|
|
||||||
|
1. `npm test` passes with the new vectors.
|
||||||
|
2. `cd firmware/test && make test` passes.
|
||||||
|
3. Bench checks:
|
||||||
|
- set ROOT D and SCALE HIRAJOSHI, and check the MiniFreak notes stay in D Hirajoshi across several SEEDs;
|
||||||
|
- check that AUTO shows the seed's own pick.
|
||||||
42
firmware/CMakeLists.txt
Normal 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
@@ -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
@@ -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
@@ -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
@@ -0,0 +1,170 @@
|
|||||||
|
/* Core 1: owns time. Renders a bar ahead with the engine, sends notes and clock when they're due.
|
||||||
|
Port of the scheduler in sim/app.js (start, stop, pump, track gating, setBpm). */
|
||||||
|
#include "app.h"
|
||||||
|
#include "midi_uart.h"
|
||||||
|
#include "pico/stdlib.h"
|
||||||
|
#include "pico/flash.h"
|
||||||
|
#include <string.h>
|
||||||
|
|
||||||
|
#define LOOK_US 30000 /* render the next bar this far ahead */
|
||||||
|
#define START_US 5000 /* gap between Start (0xFA) and the first tick */
|
||||||
|
#define QMAX 512
|
||||||
|
#define SMAX 64
|
||||||
|
|
||||||
|
typedef struct { int64_t tick; uint8_t on, tr, lane, v, vel; int16_t len; } qev_t;
|
||||||
|
typedef struct { uint8_t port, ch, note, tr; int64_t until; } sound_t;
|
||||||
|
|
||||||
|
static qev_t q[QMAX];
|
||||||
|
static int qn;
|
||||||
|
static sound_t snd[SMAX];
|
||||||
|
static int sn;
|
||||||
|
static sb_bar rbar;
|
||||||
|
|
||||||
|
static int running, bpm, bar;
|
||||||
|
static int64_t anchor_tick, next_bar_tick, next_clock;
|
||||||
|
static uint64_t anchor_us;
|
||||||
|
static struct { int bar; int64_t start, len; } played[4]; /* recently rendered bars, for g_cur_bar */
|
||||||
|
|
||||||
|
static int clampi(int x, int a, int b){ return x < a ? a : x > b ? b : x; }
|
||||||
|
|
||||||
|
int learned(const live_t *L, int tr, int *ch, int *note){
|
||||||
|
if(tr < 0 || tr >= NKIT || !L->learn[tr].used) return 0;
|
||||||
|
*ch = L->learn[tr].ch & 15; *note = L->learn[tr].note & 127; return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void snapshot(live_t *out){ critical_section_enter_blocking(&g_live_lock); *out = g_live; critical_section_exit(&g_live_lock); }
|
||||||
|
|
||||||
|
/* tick <-> time at the current tempo: 60e6 / (bpm * 840) µs per tick */
|
||||||
|
static uint64_t time_of(int64_t t){ return anchor_us + (uint64_t)(((t - anchor_tick) * 60000000LL) / ((int64_t)bpm * SB_TPB)); }
|
||||||
|
static int64_t tick_at(uint64_t us){ return anchor_tick + ((int64_t)(us - anchor_us) * bpm * SB_TPB) / 60000000LL; }
|
||||||
|
|
||||||
|
static void send3(int port, int a, int b, int c){ uint8_t m[3] = {(uint8_t)a, (uint8_t)b, (uint8_t)c}; midi_send(port, m, 3); }
|
||||||
|
static void send1_both(int a){ uint8_t m = (uint8_t)a; midi_send(MIDI_KIT, &m, 1); midi_send(MIDI_SYNTH, &m, 1); }
|
||||||
|
|
||||||
|
/* queue sorted by (tick, on); a new event goes after equal keys, like a stable sort of push order */
|
||||||
|
static void enqueue(int64_t tick, int on, const sb_event *e){
|
||||||
|
int i = qn;
|
||||||
|
if(qn >= QMAX) return;
|
||||||
|
while(i > 0 && (q[i - 1].tick > tick || (q[i - 1].tick == tick && q[i - 1].on > on))) i--;
|
||||||
|
memmove(&q[i + 1], &q[i], (size_t)(qn - i) * sizeof q[0]);
|
||||||
|
q[i].tick = tick; q[i].on = (uint8_t)on; q[i].tr = e->tr; q[i].lane = e->lane; q[i].v = e->v; q[i].vel = e->vel; q[i].len = e->len;
|
||||||
|
qn++;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int find_sound(int port, int ch, int note){ int i; for(i = 0; i < sn; i++) if(snd[i].port == port && snd[i].ch == ch && snd[i].note == note) return i; return -1; }
|
||||||
|
static void drop_sound(int i){ snd[i] = snd[--sn]; }
|
||||||
|
|
||||||
|
/* chFor / noteFor: returns 0 when a kit track isn't learned (note === null) */
|
||||||
|
static int resolve(const live_t *L, const qev_t *e, int *ch, int *note){
|
||||||
|
if(e->lane == 1){ *ch = clampi(L->mono_ch, 1, 16) - 1; *note = clampi(e->v + 12 * L->mono_oct, 0, 127); return 1; }
|
||||||
|
return learned(L, e->tr, ch, note);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void dispatch(const qev_t *e){
|
||||||
|
live_t L;
|
||||||
|
int ch, note, port = e->lane ? MIDI_SYNTH : MIDI_KIT, i;
|
||||||
|
snapshot(&L);
|
||||||
|
if(!resolve(&L, e, &ch, ¬e)) return;
|
||||||
|
if(e->on){
|
||||||
|
if(!(L.on[e->tr] ^ L.inv[e->tr])) return;
|
||||||
|
send3(port, 0x90 | ch, note, e->vel);
|
||||||
|
i = find_sound(port, ch, note);
|
||||||
|
if(i < 0 && sn < SMAX){ i = sn++; snd[i].port = (uint8_t)port; snd[i].ch = (uint8_t)ch; snd[i].note = (uint8_t)note; }
|
||||||
|
if(i >= 0){ snd[i].tr = e->tr; snd[i].until = e->tick + e->len; }
|
||||||
|
g_hit_until_ms[e->tr] = to_ms_since_boot(get_absolute_time()) + 80;
|
||||||
|
} else {
|
||||||
|
i = find_sound(port, ch, note);
|
||||||
|
if(i >= 0){ send3(port, 0x80 | ch, note, 0); if(snd[i].until <= e->tick) drop_sound(i); }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void release_where(int track){ /* track < 0: any */
|
||||||
|
int i = 0;
|
||||||
|
while(i < sn){
|
||||||
|
int match = track < 0 || snd[i].tr == track;
|
||||||
|
if(match){ send3(snd[i].port, 0x80 | snd[i].ch, snd[i].note, 0); drop_sound(i); }
|
||||||
|
else i++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void start(void){
|
||||||
|
live_t L;
|
||||||
|
if(running) return;
|
||||||
|
snapshot(&L);
|
||||||
|
bpm = L.bpm;
|
||||||
|
anchor_us = time_us_64() + START_US; anchor_tick = 0; next_bar_tick = 0; bar = 0; next_clock = 0;
|
||||||
|
qn = 0; sn = 0; memset(played, 0xFF, sizeof played);
|
||||||
|
if(L.clock) send1_both(0xFA);
|
||||||
|
running = 1; g_running = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void stop(void){
|
||||||
|
live_t L;
|
||||||
|
int t, i, ch, note, used[16] = {0};
|
||||||
|
if(!running) return;
|
||||||
|
running = 0;
|
||||||
|
snapshot(&L);
|
||||||
|
release_where(-1);
|
||||||
|
for(t = 0; t < NKIT; t++) if(learned(&L, t, &ch, ¬e)) used[ch] = 1;
|
||||||
|
for(i = 0; i < 16; i++) if(used[i]) send3(MIDI_KIT, 0xB0 | i, 120, 0); /* All Sound Off on every kit channel */
|
||||||
|
i = clampi(L.mono_ch, 1, 16) - 1;
|
||||||
|
send3(MIDI_SYNTH, 0xB0 | i, 120, 0); send3(MIDI_SYNTH, 0xB0 | i, 123, 0);
|
||||||
|
if(L.clock) send1_both(0xFC);
|
||||||
|
qn = 0; sn = 0;
|
||||||
|
g_running = 0; g_cur_bar = -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void set_bpm(int b){
|
||||||
|
if(running){ uint64_t now = time_us_64(); int64_t tk = tick_at(now); anchor_us = now; anchor_tick = tk; }
|
||||||
|
bpm = b;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void render_ahead(uint64_t now){
|
||||||
|
while(time_of(next_bar_tick) < now + LOOK_US){
|
||||||
|
live_t L;
|
||||||
|
sb_params p;
|
||||||
|
int i;
|
||||||
|
snapshot(&L);
|
||||||
|
p.gen = L.gen; p.energy = L.energy; p.drift = L.drift; p.fill = 0; p.scale = L.scale; p.root = L.root; /* no FILL button: bar-4 fills happen on their own */
|
||||||
|
sb_render_bar(&L.state, &p, bar, &rbar);
|
||||||
|
for(i = 0; i < rbar.n; i++){
|
||||||
|
enqueue(next_bar_tick + rbar.ev[i].t, 1, &rbar.ev[i]);
|
||||||
|
enqueue(next_bar_tick + rbar.ev[i].t + rbar.ev[i].len, 0, &rbar.ev[i]);
|
||||||
|
}
|
||||||
|
played[bar & 3].bar = bar; played[bar & 3].start = next_bar_tick; played[bar & 3].len = rbar.barTicks;
|
||||||
|
next_bar_tick += rbar.barTicks; bar++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void update_cur_bar(uint64_t now){
|
||||||
|
int64_t tk = tick_at(now);
|
||||||
|
int i, cur = -1;
|
||||||
|
for(i = 0; i < 4; i++) if(played[i].bar >= 0 && tk >= played[i].start && tk < played[i].start + played[i].len) cur = played[i].bar;
|
||||||
|
g_cur_bar = cur;
|
||||||
|
}
|
||||||
|
|
||||||
|
void seq_core1_main(void){
|
||||||
|
flash_safe_execute_core_init(); /* lets core 0 pause this core while it writes flash */
|
||||||
|
g_cur_bar = -1;
|
||||||
|
for(;;){
|
||||||
|
cmd_t c;
|
||||||
|
uint64_t now;
|
||||||
|
while(queue_try_remove(&g_cmdq, &c)){
|
||||||
|
if(c.cmd == CMD_START) start();
|
||||||
|
else if(c.cmd == CMD_STOP) stop();
|
||||||
|
else if(c.cmd == CMD_RELEASE_TRACK) release_where(c.arg);
|
||||||
|
else if(c.cmd == CMD_BPM) set_bpm(c.arg);
|
||||||
|
}
|
||||||
|
if(!running) continue;
|
||||||
|
now = time_us_64();
|
||||||
|
render_ahead(now);
|
||||||
|
if(g_live.clock){ while(time_of(next_clock) <= now){ send1_both(0xF8); next_clock += SB_CLOCK_TICKS; } }
|
||||||
|
else next_clock = tick_at(now) / SB_CLOCK_TICKS * SB_CLOCK_TICKS + SB_CLOCK_TICKS;
|
||||||
|
while(qn && time_of(q[0].tick) <= now){
|
||||||
|
qev_t e = q[0];
|
||||||
|
qn--; memmove(&q[0], &q[1], (size_t)qn * sizeof q[0]);
|
||||||
|
dispatch(&e);
|
||||||
|
}
|
||||||
|
update_cur_bar(now);
|
||||||
|
}
|
||||||
|
}
|
||||||
73
firmware/app/storage.c
Normal file
@@ -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
@@ -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
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
45
firmware/drivers/encoder.c
Normal 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(); }
|
||||||
15
firmware/drivers/encoder.h
Normal 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
@@ -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
@@ -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
|
||||||
37
firmware/drivers/midi_uart.c
Normal 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; }
|
||||||
15
firmware/drivers/midi_uart.h
Normal 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
|
||||||
91
firmware/drivers/pcd8544.c
Normal 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);
|
||||||
|
}
|
||||||
19
firmware/drivers/pcd8544.h
Normal 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
@@ -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
@@ -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
@@ -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
@@ -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
|
||||||
121
firmware/pico_sdk_import.cmake
Normal 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
@@ -0,0 +1,2 @@
|
|||||||
|
check_vectors
|
||||||
|
check_vectors_san
|
||||||
25
firmware/test/Makefile
Normal 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
|
||||||
162
firmware/test/check_vectors.c
Normal 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
@@ -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
@@ -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
|
||||||
93
hardware/check-layout.js
Normal file
@@ -0,0 +1,93 @@
|
|||||||
|
/* Checks hardware/layout.js: geometry (on board, not on a cut, one lead per hole),
|
||||||
|
footprints (nothing parked under the Pico or the DIP socket that should not be),
|
||||||
|
and the electrical rules of the circuit (MIDI 1.0 3.3 V spec CA-033, TRS Type A jacks, Pico pin map).
|
||||||
|
Usage: node hardware/check-layout.js [--nets] exit code 0 = clean. */
|
||||||
|
var L=require('./layout.js');
|
||||||
|
function rowName(r){ return r<26 ? String.fromCharCode(65+r) : 'A'+String.fromCharCode(65+r-26); }
|
||||||
|
function pos(h){ return rowName(h[1])+(h[0]+1); }
|
||||||
|
var key=function(h){ return h[0]+','+h[1]; };
|
||||||
|
var cut={}; L.cuts.forEach(function(h){ cut[key(h)]=1; });
|
||||||
|
var issues=[];
|
||||||
|
|
||||||
|
// --- copper: each uncut run of a strip is a segment; jumpers join segments ---
|
||||||
|
function seg(h){ if(cut[key(h)]) return null; var a=h[0], b=h[0];
|
||||||
|
while(a>0 && !cut[key([a-1,h[1]])]) a--; while(b<L.W-1 && !cut[key([b+1,h[1]])]) b++; return 'r'+h[1]+':'+a+'-'+b; }
|
||||||
|
var par={}; function find(x){ while(par[x]!==undefined && par[x]!==x) x=par[x]; return x; }
|
||||||
|
function join(a,b){ a=find(a); b=find(b); if(a!==b) par[a]=b; }
|
||||||
|
|
||||||
|
// --- every lead and wire end, by name ---
|
||||||
|
var occ={}, ends={}; // ends: name -> hole
|
||||||
|
function use(h,what){
|
||||||
|
if(h[0]<0||h[1]<0||h[0]>=L.W||h[1]>=L.H) issues.push('off the board: '+what+' at '+pos(h));
|
||||||
|
if(cut[key(h)]) issues.push('on a cut: '+what+' at '+pos(h));
|
||||||
|
(occ[key(h)]=occ[key(h)]||[]).push(what);
|
||||||
|
}
|
||||||
|
L.parts.forEach(function(p){ p.pins.forEach(function(pin,i){
|
||||||
|
var nm = p.id==='pico' ? 'pico.'+pin.num+'.'+pin.n : p.id+'.'+(i+1);
|
||||||
|
if(p.id==='U1') nm='U1.'+pin.n.split(' ')[0];
|
||||||
|
if(p.id==='D1') nm='D1.'+pin.n;
|
||||||
|
use(pin.h,nm); ends[nm]=pin.h; }); });
|
||||||
|
L.jumpers.forEach(function(j,i){ use(j.a,'J'+(i+1)+'a'); use(j.b,'J'+(i+1)+'b'); var a=seg(j.a), b=seg(j.b); if(a&&b) join(a,b); });
|
||||||
|
// the Pico's GND pins are one plane inside the module
|
||||||
|
var gnds=L.parts[0].pins.filter(function(p){ return p.n==='GND'; }).map(function(p){ return seg(p.h); }).filter(Boolean);
|
||||||
|
for(var gi=1;gi<gnds.length;gi++) join(gnds[0],gnds[gi]);
|
||||||
|
L.wires.forEach(function(w,i){ var nm='P'+(i+1)+' '+w.to; use(w.h,nm); ends['P'+(i+1)]=w.h; ends[w.to]=w.h; });
|
||||||
|
Object.keys(occ).forEach(function(k){ if(occ[k].length>1) issues.push('two leads in one hole '+pos(k.split(',').map(Number))+': '+occ[k].join(' & ')); });
|
||||||
|
|
||||||
|
function net(name){ var h=ends[name]; if(!h){ issues.push('rule names an unknown end: '+name); return '?'+name; } var s=seg(h); return s?find(s):'cut'; }
|
||||||
|
function picoPin(sig){ var p=L.parts[0].pins.filter(function(x){ return x.n===sig; })[0]; return p?'pico.'+p.num+'.'+p.n:'?'; }
|
||||||
|
function same(a,b,why){ if(net(a)!==net(b)) issues.push('not connected: '+a+' and '+b+' ('+why+')'); }
|
||||||
|
function differ(a,b,why){ if(net(a)===net(b)) issues.push('shorted: '+a+' and '+b+' ('+why+')'); }
|
||||||
|
function bridge(part,a,b,why){ var n1=net(part+'.1'), n2=net(part+'.2'), A=net(a), B=net(b);
|
||||||
|
if(!((n1===A&&n2===B)||(n1===B&&n2===A))) issues.push(part+' should sit between '+a+' and '+b+' ('+why+')'); }
|
||||||
|
|
||||||
|
// --- footprints: keep-out zones ---
|
||||||
|
var pico=L.parts[0].pins, rows=pico.map(function(p){ return p.h[1]; });
|
||||||
|
var pr0=Math.min.apply(null,rows), pr1=Math.max.apply(null,rows), pc0=12, pc1=19;
|
||||||
|
L.parts.forEach(function(p){ if(p.id==='pico') return; p.pins.forEach(function(pin){
|
||||||
|
if(pin.h[1]>=pr0-1 && pin.h[1]<=pr1+1 && pin.h[0]>=pc0 && pin.h[0]<=pc1) issues.push(p.id+' lead at '+pos(pin.h)+' is inside the Pico footprint'); }); });
|
||||||
|
var u=L.parts.filter(function(p){ return p.id==='U1'; })[0].pins.map(function(p){ return p.h; });
|
||||||
|
var uc0=Math.min.apply(null,u.map(function(h){return h[0];})), uc1=Math.max.apply(null,u.map(function(h){return h[0];}));
|
||||||
|
var ur0=Math.min.apply(null,u.map(function(h){return h[1];})), ur1=Math.max.apply(null,u.map(function(h){return h[1];}));
|
||||||
|
L.parts.forEach(function(p){ if(p.id==='U1'||p.id==='pico') return; p.pins.forEach(function(pin){
|
||||||
|
if(pin.h[0]>uc0 && pin.h[0]<uc1 && pin.h[1]>=ur0 && pin.h[1]<=ur1) issues.push(p.id+' lead at '+pos(pin.h)+' is under the DIP-8 socket'); }); });
|
||||||
|
|
||||||
|
// --- the circuit ---
|
||||||
|
var VBUS=picoPin('VBUS'), V33=picoPin('3V3'), GND=picoPin('GND'), AGND=picoPin('AGND');
|
||||||
|
// rails
|
||||||
|
differ(VBUS,V33,'5 V vs 3.3 V'); differ(VBUS,GND,'5 V vs GND'); differ(V33,GND,'3.3 V vs GND');
|
||||||
|
// MIDI IN: pin 4 -> 220R -> opto anode; pin 5 -> opto cathode; 1N4148 reverse across the LED
|
||||||
|
bridge('R1','MIDI IN jack · ring (pin 4)','U1.2','MIDI IN current limit');
|
||||||
|
same('MIDI IN jack · tip (pin 5)','U1.3','MIDI IN pin 5 to opto cathode');
|
||||||
|
same('D1.K','U1.2','diode cathode on the LED anode'); same('D1.A','U1.3','diode anode on the LED cathode');
|
||||||
|
same('U1.8',VBUS,'6N137 VCC from 5 V'); same('U1.5',GND,'6N137 GND');
|
||||||
|
same('U1.6',picoPin('GP1'),'opto out to GP1 (UART0 RX)'); bridge('R2','U1.6',V33,'1k pull-up to 3.3 V');
|
||||||
|
same('U1.7',VBUS,'6N137 enable (VE) high, or the output stays off'); bridge('C1',VBUS,GND,'100 nF supply bypass, required for the 6N137');
|
||||||
|
// MIDI OUTs: TX -> 10R -> pin 5, 3V3 -> 33R -> pin 4, pin 2 -> GND
|
||||||
|
bridge('R4',picoPin('GP0'),'OUT KIT jack · tip (pin 5)','10 Ω on TX (UART0)');
|
||||||
|
bridge('R5',V33,'OUT KIT jack · ring (pin 4)','33 Ω on 3.3 V');
|
||||||
|
same('OUT KIT jack · sleeve (pin 2)',GND,'shield');
|
||||||
|
bridge('R6',picoPin('GP4'),'OUT SYNTH jack · tip (pin 5)','10 Ω on TX (UART1)');
|
||||||
|
bridge('R7',V33,'OUT SYNTH jack · ring (pin 4)','33 Ω on 3.3 V');
|
||||||
|
same('OUT SYNTH jack · sleeve (pin 2)',GND,'shield');
|
||||||
|
// panel
|
||||||
|
var SIG={'LCD · RST (pin 4)':'GP21','LCD · SCE (pin 3)':'GP17','LCD · D/C (pin 5)':'GP20','LCD · DN/MOSI (pin 6)':'GP19','LCD · SCLK (pin 7)':'GP18',
|
||||||
|
'Encoder · SIA':'GP10','Encoder · SIB':'GP11','Encoder · SW':'GP12','SEED · one lug':'GP13','RUN toggle · middle lug':'GP14',
|
||||||
|
'ENERGY pot · middle lug':'GP26','DRIFT pot · middle lug':'GP27',
|
||||||
|
'Matrix row 1 (KICK, KICK 2, SNARE)':'GP2','Matrix row 2 (HAT, HAT 16, PERC, GHOST)':'GP3','Matrix row 3 (BASS, MEL A, MEL B, PAD)':'GP5',
|
||||||
|
'Matrix column 1 (KICK, HAT, BASS)':'GP6','Matrix column 2 (KICK 2, HAT 16, MEL A)':'GP7','Matrix column 3 (SNARE, PERC, MEL B)':'GP8','Matrix column 4 (GHOST, PAD)':'GP9'};
|
||||||
|
Object.keys(SIG).forEach(function(w){ same(w,picoPin(SIG[w]),'pin map'); });
|
||||||
|
['LCD · VCC (pin 1)','LCD · LED (pin 8) through 330 Ω','Encoder · VCC','Pots · right lugs (chain both)'].forEach(function(w){ same(w,V33,'3.3 V supply'); });
|
||||||
|
['LCD · GND (pin 2)','Encoder · GND','Panel ground bus · SEED other lug, RUN one outer lug'].forEach(function(w){ same(w,GND,'ground'); });
|
||||||
|
same('Pots · left lugs (chain both)',AGND,'pots on analog ground');
|
||||||
|
// every signal net is distinct from every other
|
||||||
|
var sigs=Object.keys(SIG).map(function(w){ return picoPin(SIG[w]); }).concat([picoPin('GP0'),picoPin('GP1'),picoPin('GP4')]);
|
||||||
|
for(var i=0;i<sigs.length;i++) for(var j=i+1;j<sigs.length;j++) if(net(sigs[i])===net(sigs[j])) issues.push('shorted: '+sigs[i]+' and '+sigs[j]);
|
||||||
|
sigs.forEach(function(s){ [VBUS,V33,GND].forEach(function(r){ if(net(s)===net(r)) issues.push('shorted to a rail: '+s+' and '+r); }); });
|
||||||
|
|
||||||
|
if(process.argv.indexOf('--nets')>=0){ var nets={};
|
||||||
|
Object.keys(ends).forEach(function(n){ if(/^P\d+$/.test(n)) return; (nets[net(n)]=nets[net(n)]||[]).push(n+' @'+pos(ends[n])); });
|
||||||
|
Object.keys(nets).sort().forEach(function(k){ console.log(k); nets[k].forEach(function(x){ console.log(' '+x); }); }); }
|
||||||
|
console.log(L.parts.length+' parts, '+L.jumpers.length+' links, '+L.wires.length+' wire pads, '+L.cuts.length+' cut holes');
|
||||||
|
if(issues.length){ console.log(issues.length+' issue(s):'); issues.forEach(function(x){ console.log(' - '+x); }); process.exit(1); }
|
||||||
|
console.log('layout clean');
|
||||||
84
hardware/layout.js
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
/* Stripboard layout for Seedbox v0.1. Holes are [col,row], 0-based, seen from the COMPONENT side.
|
||||||
|
Copper strips run left to right along each row.
|
||||||
|
The Pico's pin 1 and 40 sit on the first strip (A), so its USB end is flush with the board edge. */
|
||||||
|
var LAYOUT=(function(){
|
||||||
|
var W=34, H=28;
|
||||||
|
var cuts=[];
|
||||||
|
function cut(c,r0,r1){ for(var r=r0;r<=r1;r++) cuts.push([c,r]); }
|
||||||
|
cut(15,0,19); // under the Pico: separates its left and right pin rows
|
||||||
|
cut(5,20,24); // opto: left pins / right pins, IN4 strip
|
||||||
|
cut(13,20,23); // opto area / MIDI OUT area (strip Y stays whole: it carries GP4)
|
||||||
|
cut(24,20,22); // MIDI OUT A / MIDI OUT B
|
||||||
|
cut(22,23,23); // strip X: GP0 side / KIT tip side, R4 lies across this cut
|
||||||
|
var LP=['GP0','GP1','GND','GP2','GP3','GP4','GP5','GND','GP6','GP7','GP8','GP9','GND','GP10','GP11','GP12','GP13','GND','GP14','GP15'];
|
||||||
|
var RP=['VBUS','VSYS','GND','3V3_EN','3V3','ADC_VREF','GP28','AGND','GP27','GP26','RUN','GP22','GND','GP21','GP20','GP19','GP18','GND','GP17','GP16'];
|
||||||
|
var pico={id:'pico',kind:'pico',pins:[]};
|
||||||
|
for(var i=0;i<20;i++){ pico.pins.push({h:[12,i],n:LP[i],num:i+1}); pico.pins.push({h:[19,i],n:RP[i],num:40-i}); }
|
||||||
|
var parts=[pico,
|
||||||
|
{id:'U1',kind:'dip8',label:'6N137',pins:[{h:[4,20],n:'1 NC'},{h:[4,21],n:'2 A'},{h:[4,22],n:'3 K'},{h:[4,23],n:'4 NC'},{h:[7,23],n:'5 GND'},{h:[7,22],n:'6 VO'},{h:[7,21],n:'7 VE'},{h:[7,20],n:'8 VCC'}]},
|
||||||
|
{id:'R1',kind:'res',label:'220 Ω',pins:[{h:[1,21]},{h:[1,24]}]},
|
||||||
|
{id:'D1',kind:'diode',label:'1N4148',pins:[{h:[3,21],n:'K'},{h:[3,22],n:'A'}]},
|
||||||
|
{id:'R2',kind:'res',label:'1 kΩ',pins:[{h:[9,22]},{h:[9,27]}]},
|
||||||
|
{id:'C1',kind:'cap',label:'100 nF',pins:[{h:[10,20]},{h:[10,23]}]},
|
||||||
|
{id:'R4',kind:'res',label:'10 Ω',pins:[{h:[20,23]},{h:[24,23]}]},
|
||||||
|
{id:'R5',kind:'res',label:'33 Ω ½ W',pins:[{h:[18,21]},{h:[18,27]}]},
|
||||||
|
{id:'R6',kind:'res',label:'10 Ω',pins:[{h:[27,20]},{h:[27,24]}]},
|
||||||
|
{id:'R7',kind:'res',label:'33 Ω ½ W',pins:[{h:[29,21]},{h:[29,27]}]}
|
||||||
|
];
|
||||||
|
var jumpers=[
|
||||||
|
{a:[33,0],b:[33,25],n:'5 V (VBUS) to the 5 V strip'},
|
||||||
|
{a:[31,4],b:[31,27],n:'3V3 to the 3V3 strip'},
|
||||||
|
{a:[32,17],b:[32,26],n:'GND to the GND strip'},
|
||||||
|
{a:[12,20],b:[12,25],n:'opto VCC to 5 V'},
|
||||||
|
{a:[9,20],b:[9,21],n:'opto enable (pin 7) to VCC (pin 8): the 6N137 output only works with pin 7 high'},
|
||||||
|
{a:[11,23],b:[11,26],n:'opto GND to GND'},
|
||||||
|
{a:[8,22],b:[8,1],n:'opto out to GP1 (MIDI RX), strip B'},
|
||||||
|
{a:[14,0],b:[14,23],n:'GP0 (TX KIT) to strip X (R4), straight down under the Pico between its headers'},
|
||||||
|
{a:[13,5],b:[13,24],n:'GP4 (TX SYNTH) to strip Y (R6), straight down under the Pico between its headers'}
|
||||||
|
];
|
||||||
|
/* Off-board wires. col = spool colour role. to = where the other end goes. */
|
||||||
|
var wires=[
|
||||||
|
// MIDI jacks: 3.5 mm TRS, Type A (tip = DIN pin 5, ring = pin 4, sleeve = pin 2)
|
||||||
|
{h:[0,24],col:'blue',to:'MIDI IN jack · ring (pin 4)'},
|
||||||
|
{h:[0,22],col:'blue',to:'MIDI IN jack · tip (pin 5)'},
|
||||||
|
{h:[25,23],col:'blue',to:'OUT KIT jack · tip (pin 5)'},
|
||||||
|
{h:[21,21],col:'red',to:'OUT KIT jack · ring (pin 4)'},
|
||||||
|
{h:[21,26],col:'black',to:'OUT KIT jack · sleeve (pin 2)'},
|
||||||
|
{h:[30,20],col:'blue',to:'OUT SYNTH jack · tip (pin 5)'},
|
||||||
|
{h:[30,21],col:'red',to:'OUT SYNTH jack · ring (pin 4)'},
|
||||||
|
{h:[30,26],col:'black',to:'OUT SYNTH jack · sleeve (pin 2)'},
|
||||||
|
// screen
|
||||||
|
{h:[26,13],col:'blue',to:'LCD · RST (pin 4)'},
|
||||||
|
{h:[26,18],col:'blue',to:'LCD · SCE (pin 3)'},
|
||||||
|
{h:[26,14],col:'blue',to:'LCD · D/C (pin 5)'},
|
||||||
|
{h:[26,15],col:'blue',to:'LCD · DN/MOSI (pin 6)'},
|
||||||
|
{h:[26,16],col:'blue',to:'LCD · SCLK (pin 7)'},
|
||||||
|
{h:[22,27],col:'red',to:'LCD · VCC (pin 1)'},
|
||||||
|
{h:[22,26],col:'black',to:'LCD · GND (pin 2)'},
|
||||||
|
{h:[21,27],col:'red',to:'LCD · LED (pin 8) through 330 Ω'},
|
||||||
|
// encoder
|
||||||
|
{h:[1,13],col:'green',to:'Encoder · SIA'},
|
||||||
|
{h:[1,14],col:'green',to:'Encoder · SIB'},
|
||||||
|
{h:[1,15],col:'green',to:'Encoder · SW'},
|
||||||
|
{h:[3,27],col:'red',to:'Encoder · VCC'},
|
||||||
|
{h:[3,26],col:'black',to:'Encoder · GND'},
|
||||||
|
// pots
|
||||||
|
{h:[26,9],col:'blue',to:'ENERGY pot · middle lug'},
|
||||||
|
{h:[26,8],col:'blue',to:'DRIFT pot · middle lug'},
|
||||||
|
{h:[24,27],col:'red',to:'Pots · right lugs (chain both)'},
|
||||||
|
{h:[26,7],col:'black',to:'Pots · left lugs (chain both)'},
|
||||||
|
// buttons
|
||||||
|
{h:[1,16],col:'yellow',to:'SEED · one lug'},
|
||||||
|
{h:[1,18],col:'yellow',to:'RUN toggle · middle lug'},
|
||||||
|
{h:[5,26],col:'black',to:'Panel ground bus · SEED other lug, RUN one outer lug'},
|
||||||
|
{h:[1,3],col:'yellow',to:'Matrix row 1 (KICK, KICK 2, SNARE)'},
|
||||||
|
{h:[1,4],col:'yellow',to:'Matrix row 2 (HAT, HAT 16, PERC, GHOST)'},
|
||||||
|
{h:[1,6],col:'yellow',to:'Matrix row 3 (BASS, MEL A, MEL B, PAD)'},
|
||||||
|
{h:[1,8],col:'green',to:'Matrix column 1 (KICK, HAT, BASS)'},
|
||||||
|
{h:[1,9],col:'green',to:'Matrix column 2 (KICK 2, HAT 16, MEL A)'},
|
||||||
|
{h:[1,10],col:'green',to:'Matrix column 3 (SNARE, PERC, MEL B)'},
|
||||||
|
{h:[1,11],col:'green',to:'Matrix column 4 (GHOST, PAD)'}
|
||||||
|
];
|
||||||
|
return {W:W,H:H,cuts:cuts,parts:parts,jumpers:jumpers,wires:wires};
|
||||||
|
})();
|
||||||
|
if(typeof module!=='undefined') module.exports=LAYOUT;
|
||||||
12
package.json
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"name": "seedbox",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"private": true,
|
||||||
|
"description": "Generative rhythm and melody machine for the Raspberry Pi Pico 2 W (browser simulator, golden vectors, hardware docs)",
|
||||||
|
"scripts": {
|
||||||
|
"test": "node test/check-vectors.js && node hardware/check-layout.js",
|
||||||
|
"vectors": "node test/gen-vectors.js",
|
||||||
|
"layout": "node hardware/check-layout.js --nets",
|
||||||
|
"wiring": "node hardware/check-layout.js && node tools/build-wiring.js"
|
||||||
|
}
|
||||||
|
}
|
||||||
484
sim/app.js
Normal file
@@ -0,0 +1,484 @@
|
|||||||
|
/* ================= APP (browser only: UI, Web MIDI, scheduler) ================= */
|
||||||
|
(function(){
|
||||||
|
var E=Engine, TPB=E.TPB, NT=E.NT, HOLD_MS=250;
|
||||||
|
function store(k,v){ try{ localStorage.setItem('seedbox3.'+k,JSON.stringify(v)); }catch(e){} }
|
||||||
|
function load(k,d){ try{ var v=localStorage.getItem('seedbox3.'+k); return v?JSON.parse(v):d; }catch(e){ return d; } }
|
||||||
|
function rnd32(){ try{ return crypto.getRandomValues(new Uint32Array(1))[0]>>>0; }catch(e){ return (Date.now()*2654435761)>>>0; } }
|
||||||
|
function hex(n){ return ('00000000'+(n>>>0).toString(16).toUpperCase()).slice(-8); }
|
||||||
|
function clamp(x,a,b){ return x<a?a:x>b?b:x; }
|
||||||
|
function mod(a,n){ return ((a%n)+n)%n; }
|
||||||
|
function laneTracks(l){ var a=[]; for(var t=0;t<NT;t++) if(E.TRACKS[t].lane===l) a.push(t); return a; }
|
||||||
|
var KIT=laneTracks(0);
|
||||||
|
|
||||||
|
var DEF={portDrums:'',portMono:'',portIn:'',clock:true,
|
||||||
|
kit:{learn:[null,null,null,null,null,null,null]}, mono:{ch:1,oct:0}, hw:{display:'5110',leds:false}};
|
||||||
|
var cfg=(function(saved){ var c=JSON.parse(JSON.stringify(DEF)); Object.keys(c).forEach(function(k){
|
||||||
|
if(saved[k]===undefined) return; if(typeof c[k]==='object'){ Object.keys(c[k]).forEach(function(f){ if(saved[k][f]!==undefined) c[k][f]=saved[k][f]; }); } else c[k]=saved[k]; });
|
||||||
|
return c; })(load('cfg',{}));
|
||||||
|
if(!Array.isArray(cfg.kit.learn) || cfg.kit.learn.length<7) cfg.kit.learn=[null,null,null,null,null,null,null];
|
||||||
|
function saveCfg(){ store('cfg',cfg); }
|
||||||
|
|
||||||
|
var app={ state:load('state',null)||{seed:0x5EEDB0C5,salts:[0,0]}, gen:load('gen',0), scale:load('scale',-1), root:load('root',-1), energy:load('energy',64), drift:load('drift',32), bpm:load('bpm',165),
|
||||||
|
on:load('on',null), inv:[], fill:false, history:[], slots:load('slots',[null,null,null,null,null,null,null,null]),
|
||||||
|
page:0, editing:false, histSel:0, slotSel:0, rollSel:0, learn:0, learnTr:-1 };
|
||||||
|
if(!(app.scale>=-1 && app.scale<E.SCALE_NAMES.length)) app.scale=-1;
|
||||||
|
if(!(app.root>=-1 && app.root<12)) app.root=-1;
|
||||||
|
if(!Array.isArray(app.on) || app.on.length!==NT){ app.on=[]; for(var i9=0;i9<NT;i9++) app.on.push(true); }
|
||||||
|
for(var i0=0;i0<NT;i0++) app.inv.push(false); // inv: flipped while a button is held
|
||||||
|
var PAGES=['SEED','GEN','ROOT','SCALE','BPM','SLOT','ROLL','LEARN'];
|
||||||
|
function params(fillNow){ return {gen:app.gen,energy:app.energy,drift:app.drift,fill:!!fillNow,scale:app.scale,root:app.root}; }
|
||||||
|
function save(){ store('state',app.state); store('gen',app.gen); store('scale',app.scale); store('root',app.root); store('energy',app.energy); store('drift',app.drift); store('bpm',app.bpm); }
|
||||||
|
function setState(s){
|
||||||
|
app.history.unshift({seed:app.state.seed,salts:app.state.salts.slice()}); if(app.history.length>16) app.history.pop();
|
||||||
|
app.state={seed:s.seed>>>0,salts:[s.salts[0]|0,s.salts[1]|0]}; app.histSel=0; save(); dirty=true;
|
||||||
|
}
|
||||||
|
function baseOn(tr){ return !!app.on[tr]; }
|
||||||
|
function sounds(tr,mask){ var b=baseOn(tr,mask); return app.inv[tr]?!b:b; }
|
||||||
|
var flashMsg='', flashMsgUntil=0;
|
||||||
|
function msg(t,ms){ flashMsg=t; flashMsgUntil=performance.now()+(ms||900); }
|
||||||
|
|
||||||
|
/* ---------- MIDI ---------- */
|
||||||
|
var midi=null, outs={}, ins={}, inPort=null, statusEl=document.getElementById('midiStatus');
|
||||||
|
function setStatus(t,cls){ statusEl.textContent=t; statusEl.className='pill'+(cls?' '+cls:''); }
|
||||||
|
function fillSelect(id,list,re){
|
||||||
|
var sel=document.getElementById(id), want=cfg[id], map={}; sel.innerHTML='';
|
||||||
|
var none=document.createElement('option'); none.value=''; none.textContent='(none)'; sel.appendChild(none);
|
||||||
|
list.forEach(function(o){ map[o.id]=o; var op=document.createElement('option'); op.value=o.id; op.textContent=o.name; sel.appendChild(op); });
|
||||||
|
if(!want || !map[want]){ var guess=list.filter(function(o){ return re.test(o.name); })[0]; if(guess){ want=guess.id; cfg[id]=want; } }
|
||||||
|
sel.value=map[want]?want:'';
|
||||||
|
}
|
||||||
|
function fillPorts(){
|
||||||
|
outs={}; ins={}; var lo=[], li=[];
|
||||||
|
if(midi){ midi.outputs.forEach(function(o){ outs[o.id]=o; lo.push(o); }); midi.inputs.forEach(function(o){ ins[o.id]=o; li.push(o); }); }
|
||||||
|
fillSelect('portDrums',lo,/404/i); fillSelect('portMono',lo,/minifreak|freak/i); fillSelect('portIn',li,/404/i);
|
||||||
|
hookInput();
|
||||||
|
if(midi) setStatus('MIDI: '+lo.length+' out, '+li.length+' in', lo.length?'ok':'');
|
||||||
|
}
|
||||||
|
function hookInput(){
|
||||||
|
if(inPort) inPort.onmidimessage=null;
|
||||||
|
inPort=ins[cfg.portIn]||null;
|
||||||
|
if(inPort) inPort.onmidimessage=function(e){ onMidiIn(e.data); };
|
||||||
|
}
|
||||||
|
if(navigator.requestMIDIAccess){
|
||||||
|
navigator.requestMIDIAccess({sysex:false}).then(function(m){ midi=m; m.onstatechange=fillPorts; fillPorts(); },
|
||||||
|
function(){ setStatus('MIDI blocked in this view. Open the standalone file in Chrome','bad'); fillPorts(); });
|
||||||
|
} else { setStatus('No Web MIDI here. Use Chrome or Edge on desktop','bad'); fillPorts(); }
|
||||||
|
|
||||||
|
/* Pad learn: LEARN page, push, press a kit track, then hit the pad on the sampler. Stores the exact channel + note received. */
|
||||||
|
function onMidiIn(d){
|
||||||
|
if(!d || d.length<3) return;
|
||||||
|
if((d[0]&0xF0)!==0x90 || d[2]===0) return;
|
||||||
|
if(app.learn===2 && app.learnTr>=0){
|
||||||
|
cfg.kit.learn[app.learnTr]={ch:d[0]&0x0F,note:d[1]}; saveCfg(); refreshPadFields();
|
||||||
|
applyOn(KIT[app.learnTr],true); dirty=true; // a freshly learned track starts on
|
||||||
|
msg('N'+d[1]+' CH'+((d[0]&15)+1)+' OK',1400);
|
||||||
|
app.learn=0; app.learnTr=-1; app.editing=false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function portFor(lane){ return outs[lane===1?cfg.portMono:cfg.portDrums]||null; }
|
||||||
|
/* A learned pad plays on exactly the channel and note it was learned with. */
|
||||||
|
function learned(tr){ var L=cfg.kit.learn[tr]; return L?{ch:L.ch&15,note:L.note&127}:null; }
|
||||||
|
/* Kit tracks play only once learned; an unlearned track stays silent. */
|
||||||
|
function chFor(ev){ if(ev.lane===1) return clamp(cfg.mono.ch|0,1,16)-1; var L=learned(ev.tr); return L?L.ch:0; }
|
||||||
|
function noteFor(ev){ if(ev.lane===1) return clamp(ev.v+12*(cfg.mono.oct|0),0,127); var L=learned(ev.tr); return L?L.note:null; }
|
||||||
|
function kitChannels(){ var c={}; KIT.forEach(function(t){ var L=learned(t); if(L) c[L.ch]=1; }); return Object.keys(c).map(Number); }
|
||||||
|
|
||||||
|
/* ---------- scheduler (look-ahead, timestamped sends) ---------- */
|
||||||
|
var LOOK=110, S={running:false,anchorTime:0,anchorTick:0,nextBarTick:0,bar:0,nextClock:0,queue:[],sounding:{},timer:null,log:{},mask:0}, flashes=[];
|
||||||
|
function msPerTick(){ return 60000/(app.bpm*TPB); }
|
||||||
|
function timeOf(t){ return S.anchorTime+(t-S.anchorTick)*msPerTick(); }
|
||||||
|
function tickAt(ms){ return S.anchorTick+(ms-S.anchorTime)/msPerTick(); }
|
||||||
|
function clockPorts(){ var p=[], a=outs[cfg.portDrums], b=outs[cfg.portMono]; if(a) p.push(a); if(b && b!==a) p.push(b); return p; }
|
||||||
|
function send(port,bytes,ts){ if(!port) return; try{ port.send(bytes,ts); }catch(e){} }
|
||||||
|
function start(){
|
||||||
|
if(S.running) return;
|
||||||
|
S.running=true; S.anchorTime=performance.now()+60; S.anchorTick=0; S.nextBarTick=0; S.bar=0; S.nextClock=0; S.queue=[]; S.sounding={}; S.log={}; S.mask=0;
|
||||||
|
if(cfg.clock) clockPorts().forEach(function(p){ send(p,[0xFA],S.anchorTime); });
|
||||||
|
S.timer=setInterval(pump,15); pump(); runBtn.setAttribute('aria-pressed','true');
|
||||||
|
}
|
||||||
|
function stop(){
|
||||||
|
if(!S.running) return;
|
||||||
|
clearInterval(S.timer); S.running=false;
|
||||||
|
Object.keys(outs).forEach(function(id){ try{ outs[id].clear(); }catch(e){} });
|
||||||
|
Object.keys(S.sounding).forEach(function(k){ var s=S.sounding[k]; send(s.port,[0x80|s.ch,s.note,0]); });
|
||||||
|
var pd=portFor(0), pm=portFor(1);
|
||||||
|
if(pd) kitChannels().forEach(function(c){ send(pd,[0xB0|c,120,0]); }); // samplers that ignore All Notes Off still obey All Sound Off (120)
|
||||||
|
if(pm){ var mc=clamp(cfg.mono.ch|0,1,16)-1; send(pm,[0xB0|mc,120,0]); send(pm,[0xB0|mc,123,0]); }
|
||||||
|
if(cfg.clock) clockPorts().forEach(function(p){ send(p,[0xFC]); });
|
||||||
|
S.queue=[]; S.sounding={};
|
||||||
|
for(var t=0;t<NT;t++) if(pending[t]) applyOn(t,pending[t].v);
|
||||||
|
runBtn.setAttribute('aria-pressed','false'); dirty=true;
|
||||||
|
}
|
||||||
|
function pump(){
|
||||||
|
var now=performance.now(), horizon=now+LOOK, i;
|
||||||
|
applyPending(tickAt(now));
|
||||||
|
while(timeOf(S.nextBarTick)<horizon){
|
||||||
|
var p=params(app.fill), r=E.renderBar(app.state,p,S.bar);
|
||||||
|
S.log[S.bar]={start:S.nextBarTick,r:r,mask:S.mask}; delete S.log[S.bar-12];
|
||||||
|
for(i=0;i<r.events.length;i++){ var ev=r.events[i], at=S.nextBarTick+ev.t; S.queue.push({tick:at,on:1,ev:ev,mask:S.mask}); S.queue.push({tick:at+ev.len,on:0,ev:ev}); }
|
||||||
|
S.queue.sort(function(a,b){ return a.tick-b.tick || a.on-b.on; });
|
||||||
|
S.nextBarTick+=r.barTicks; S.bar++;
|
||||||
|
}
|
||||||
|
if(cfg.clock){ var cp=clockPorts(); while(timeOf(S.nextClock)<horizon){ var ct=timeOf(S.nextClock); cp.forEach(function(p){ send(p,[0xF8],ct); }); S.nextClock+=E.CLOCK_TICKS; } }
|
||||||
|
else S.nextClock=Math.ceil(tickAt(horizon)/E.CLOCK_TICKS)*E.CLOCK_TICKS;
|
||||||
|
while(S.queue.length && timeOf(S.queue[0].tick)<horizon){
|
||||||
|
var q=S.queue.shift(), e=q.ev, ts=Math.max(now,timeOf(q.tick));
|
||||||
|
applyPending(q.tick);
|
||||||
|
var port=portFor(e.lane), ch=chFor(e), note=noteFor(e), key=(port?port.id:'-')+':'+ch+':'+note;
|
||||||
|
if(q.on){ if(!sounds(e.tr,q.mask) || note===null) continue; send(port,[0x90|ch,note,e.vel],ts); S.sounding[key]={port:port,ch:ch,note:note,tr:e.tr,until:q.tick+e.len}; flashes.push({time:ts,tr:e.tr}); }
|
||||||
|
else { var s=S.sounding[key]; if(s){ send(port,[0x80|ch,note,0],ts); if(s.until<=q.tick) delete S.sounding[key]; } }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function setBpm(b){ b=clamp(b|0,60,240); if(b===app.bpm) return; if(S.running){ var now=performance.now(), tk=tickAt(now); S.anchorTime=now; S.anchorTick=tk; } app.bpm=b; save(); }
|
||||||
|
|
||||||
|
/* ---------- tracking by hand: every track is simply on or off ---------- */
|
||||||
|
var pending=[]; for(var i1=0;i1<NT;i1++) pending.push(null);
|
||||||
|
function releaseTrack(tr){ Object.keys(S.sounding).forEach(function(k){ var s=S.sounding[k]; if(s.tr===tr){ send(s.port,[0x80|s.ch,s.note,0]); delete S.sounding[k]; } }); }
|
||||||
|
function curMask(){ return 0; }
|
||||||
|
function applyOn(tr,v){ app.on[tr]=v; pending[tr]=null; store('on',app.on); if(!sounds(tr,0)) releaseTrack(tr); }
|
||||||
|
function applyPending(tick){ for(var t=0;t<NT;t++) if(pending[t] && pending[t].at<=tick) applyOn(t,pending[t].v); }
|
||||||
|
function targetOn(tr){ return pending[tr]?pending[tr].v:app.on[tr]; }
|
||||||
|
function setOn(tr,v){
|
||||||
|
if(S.running && document.getElementById('muteQ').checked){
|
||||||
|
var now=tickAt(performance.now()), at=S.nextBarTick;
|
||||||
|
for(var b in S.log){ var L=S.log[b]; if(L.start>now && L.start<at) at=L.start; }
|
||||||
|
pending[tr] = (v===app.on[tr]) ? null : {v:v,at:at};
|
||||||
|
} else applyOn(tr,v);
|
||||||
|
}
|
||||||
|
function setInv(tr,v){ app.inv[tr]=v; if(!sounds(tr,0)) releaseTrack(tr); }
|
||||||
|
|
||||||
|
/* ---------- actions ---------- */
|
||||||
|
function newSeed(){ setState({seed:rnd32(),salts:[0,0]}); }
|
||||||
|
function reroll(l){ var s={seed:app.state.seed,salts:app.state.salts.slice()}; s.salts[l]=(s.salts[l]+1)&0xFFFF; setState(s); msg(l?'NEW LINE':'NEW KIT',700); }
|
||||||
|
function encTurn(d){
|
||||||
|
if(!app.editing){ app.page=(app.page+d+PAGES.length)%PAGES.length; app.learn=0; app.learnTr=-1; return; }
|
||||||
|
var pg=PAGES[app.page];
|
||||||
|
if(pg==='SEED') app.histSel=clamp(app.histSel+d,0,app.history.length);
|
||||||
|
else if(pg==='GEN'){ app.gen=(app.gen+d+5)%5; save(); dirty=true; }
|
||||||
|
else if(pg==='ROOT'){ app.root=mod(app.root+1+d,13)-1; save(); dirty=true; } // AUTO, C .. B
|
||||||
|
else if(pg==='SCALE'){ var ns=E.SCALE_NAMES.length+1; app.scale=mod(app.scale+1+d,ns)-1; save(); dirty=true; } // AUTO, MAJOR .. LOCRIAN
|
||||||
|
else if(pg==='BPM') setBpm(app.bpm+d);
|
||||||
|
else if(pg==='SLOT') app.slotSel=(app.slotSel+d+8)%8;
|
||||||
|
else if(pg==='ROLL') app.rollSel=(app.rollSel+d+2)%2;
|
||||||
|
}
|
||||||
|
function encPush(hold){
|
||||||
|
var pg=PAGES[app.page];
|
||||||
|
if(pg==='LEARN'){
|
||||||
|
if(app.learn===0){ app.learn=1; app.editing=true; } // now press a kit track button
|
||||||
|
else { app.learn=0; app.learnTr=-1; app.editing=false; msg('CANCELLED',700); }
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if(!app.editing){ app.editing=true; return; }
|
||||||
|
if(pg==='SEED' && app.histSel>0) setState(app.history[app.histSel-1]);
|
||||||
|
if(pg==='SLOT'){
|
||||||
|
if(hold){ app.slots[app.slotSel]={seed:app.state.seed,salts:app.state.salts.slice(),gen:app.gen}; store('slots',app.slots); msg('SAVED S'+(app.slotSel+1)); return; }
|
||||||
|
var sl=app.slots[app.slotSel]; if(sl){ app.gen=sl.gen|0; setState(sl); }
|
||||||
|
}
|
||||||
|
if(pg==='ROLL') reroll(app.rollSel); // then back to page select
|
||||||
|
app.editing=false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------- device controls ---------- */
|
||||||
|
var runBtn=(function(){ var b=document.createElement('button'); b.className='toggle'; b.id='run'; b.type='button'; b.setAttribute('aria-pressed','false'); b.setAttribute('aria-label','Run / stop');
|
||||||
|
b.innerHTML='<span class="label">Run</span><span class="tg"></span><span class="label">Stop</span>'; return b; })(), big=document.getElementById('big');
|
||||||
|
runBtn.addEventListener('click',function(){ S.running?stop():start(); });
|
||||||
|
var seedHeld=false, comboUsed=false;
|
||||||
|
big.addEventListener('pointerdown',function(e){ if(big.setPointerCapture) big.setPointerCapture(e.pointerId); seedHeld=true; comboUsed=false; big.classList.add('down'); });
|
||||||
|
big.addEventListener('pointerup',function(){ big.classList.remove('down'); if(seedHeld && !comboUsed) newSeed(); seedHeld=false; });
|
||||||
|
big.addEventListener('pointercancel',function(){ big.classList.remove('down'); seedHeld=false; });
|
||||||
|
big.addEventListener('keydown',function(e){ if(e.key==='Enter'){ e.preventDefault(); e.stopPropagation(); newSeed(); } });
|
||||||
|
|
||||||
|
/* Track buttons. Tap: on <-> off. Hold past 250 ms: flip while held.
|
||||||
|
In LEARN a kit button selects the track to learn instead. */
|
||||||
|
/* Panel grid, same order on the screen map: row 1 KICK KICK2 SNARE [RUN toggle], row 2 HAT HAT16 PERC GHOST, row 3 BASS MEL A MEL B PAD.
|
||||||
|
-1 = the RUN toggle. In the wiring matrix that slot is empty; GHOST is wired at row 2 / column 4. */
|
||||||
|
var PANEL=[0,1,2,-1, 4,5,6,3, 7,8,9,10];
|
||||||
|
var trk=document.getElementById('trk'), tleds=[], tbtns=[], held={};
|
||||||
|
function addCell(name,onDown,onUp){
|
||||||
|
var c=document.createElement('div'); c.className='tcell';
|
||||||
|
var led=document.createElement('span'); led.className='tled';
|
||||||
|
var b=document.createElement('button'); b.className='tbtn'; b.type='button'; b.setAttribute('aria-label',name);
|
||||||
|
var n=document.createElement('span'); n.className='tname'; n.textContent=name;
|
||||||
|
c.appendChild(led); c.appendChild(b); c.appendChild(n); trk.appendChild(c);
|
||||||
|
b.addEventListener('pointerdown',function(e){ if(b.setPointerCapture) b.setPointerCapture(e.pointerId); b.classList.add('down'); onDown(e); });
|
||||||
|
b.addEventListener('pointerup',function(){ b.classList.remove('down'); onUp(); });
|
||||||
|
b.addEventListener('pointercancel',function(){ b.classList.remove('down'); onUp(); });
|
||||||
|
b.addEventListener('keydown',function(e){ if((e.key==='Enter'||e.key===' ') && !e.repeat){ e.preventDefault(); e.stopPropagation(); b.classList.add('down'); onDown(e); } });
|
||||||
|
b.addEventListener('keyup',function(e){ if(e.key==='Enter'||e.key===' '){ e.preventDefault(); b.classList.remove('down'); onUp(); } });
|
||||||
|
return {led:led,btn:b};
|
||||||
|
}
|
||||||
|
function trackDown(t,e){
|
||||||
|
var pg=PAGES[app.page];
|
||||||
|
if(app.learn===1 || app.learn===2){ if(E.TRACKS[t].lane!==0){ msg('KIT ONLY',700); return; } app.learnTr=KIT.indexOf(t); app.learn=2; return; }
|
||||||
|
held[t]={t0:performance.now(),timer:setTimeout(function(){ if(held[t]){ held[t].long=true; setInv(t,true); } },HOLD_MS)};
|
||||||
|
}
|
||||||
|
function trackUp(t){
|
||||||
|
var h=held[t]; if(!h) return; clearTimeout(h.timer); delete held[t];
|
||||||
|
if(h.long) setInv(t,false);
|
||||||
|
else setOn(t,!targetOn(t));
|
||||||
|
}
|
||||||
|
PANEL.forEach(function(t){
|
||||||
|
if(t<0){ var rc=document.createElement('div'); rc.className='tcell runcell'; rc.appendChild(runBtn); trk.appendChild(rc); return; } // the RUN toggle sits in the grid, outside the matrix
|
||||||
|
var c=addCell(E.TRACKS[t].name,function(e){ trackDown(t,e); },function(){ trackUp(t); });
|
||||||
|
tleds[t]=c.led; tbtns[t]=c.btn;
|
||||||
|
});
|
||||||
|
|
||||||
|
function knob(el,valEl,get,set){
|
||||||
|
function draw(){ var v=get(); el.style.setProperty('--rot',(-135+270*v/127)+'deg'); el.setAttribute('aria-valuenow',v); valEl.textContent=v; }
|
||||||
|
var y0=0,v0=0;
|
||||||
|
el.addEventListener('pointerdown',function(e){ el.setPointerCapture(e.pointerId); y0=e.clientY; v0=get(); });
|
||||||
|
el.addEventListener('pointermove',function(e){ if(!el.hasPointerCapture(e.pointerId)) return; set(clamp(Math.round(v0+(y0-e.clientY)/1.5),0,127)); draw(); });
|
||||||
|
el.addEventListener('wheel',function(e){ e.preventDefault(); set(clamp(get()+(e.deltaY<0?2:-2),0,127)); draw(); },{passive:false});
|
||||||
|
el.addEventListener('keydown',function(e){ var d=(e.key==='ArrowUp'||e.key==='ArrowRight')?1:(e.key==='ArrowDown'||e.key==='ArrowLeft')?-1:0; if(d){ e.preventDefault(); e.stopPropagation(); set(clamp(get()+d*(e.shiftKey?8:2),0,127)); draw(); } });
|
||||||
|
draw();
|
||||||
|
}
|
||||||
|
knob(document.getElementById('kEnergy'),document.getElementById('vEnergy'),function(){return app.energy;},function(v){ app.energy=v; save(); dirty=true; });
|
||||||
|
knob(document.getElementById('kDrift'),document.getElementById('vDrift'),function(){return app.drift;},function(v){ app.drift=v; save(); dirty=true; });
|
||||||
|
|
||||||
|
var enc=document.getElementById('enc'), encDot=enc.querySelector('i'), encRot=0;
|
||||||
|
function encStep(d){ encRot+=d*18; encDot.style.setProperty('--rot',encRot+'deg'); encTurn(d); }
|
||||||
|
(function(){
|
||||||
|
var x0=0, acc=0, moved=false, t0=0;
|
||||||
|
enc.addEventListener('pointerdown',function(e){ enc.setPointerCapture(e.pointerId); x0=e.clientX; acc=0; moved=false; t0=performance.now(); });
|
||||||
|
enc.addEventListener('pointermove',function(e){ if(!enc.hasPointerCapture(e.pointerId)) return; acc+=e.clientX-x0; x0=e.clientX; while(Math.abs(acc)>=14){ moved=true; var d=acc>0?1:-1; acc-=d*14; encStep(d); } });
|
||||||
|
enc.addEventListener('pointerup',function(){ if(!moved) encPush(performance.now()-t0>600); });
|
||||||
|
enc.addEventListener('wheel',function(e){ e.preventDefault(); encStep(e.deltaY>0?1:-1); },{passive:false});
|
||||||
|
})();
|
||||||
|
|
||||||
|
var KEYMAP=['Digit1','Digit2','Digit3','Digit4','Digit5','Digit6','Digit7','Digit8','Digit9','Digit0','Minus'];
|
||||||
|
document.addEventListener('keydown',function(e){
|
||||||
|
var tg=e.target.tagName; if(tg==='INPUT'||tg==='SELECT'||tg==='TEXTAREA'||tg==='BUTTON' && (e.key==='Enter'||e.key===' ')) return;
|
||||||
|
if(e.repeat && e.key!=='ArrowLeft' && e.key!=='ArrowRight') return;
|
||||||
|
var t=KEYMAP.indexOf(e.code);
|
||||||
|
if(t>=0){ tbtns[t].classList.add('down'); trackDown(t,e); }
|
||||||
|
else if(e.key===' '){ e.preventDefault(); big.classList.add('down'); newSeed(); setTimeout(function(){ big.classList.remove('down'); },90); }
|
||||||
|
else if(e.key==='ArrowLeft'){ e.preventDefault(); encStep(-1); }
|
||||||
|
else if(e.key==='ArrowRight'){ e.preventDefault(); encStep(1); }
|
||||||
|
else if(e.key==='Enter'){ e.preventDefault(); encPush(e.shiftKey); }
|
||||||
|
else if(e.key==='r'||e.key==='R'){ S.running?stop():start(); }
|
||||||
|
});
|
||||||
|
document.addEventListener('keyup',function(e){
|
||||||
|
var t=KEYMAP.indexOf(e.code);
|
||||||
|
if(t>=0){ tbtns[t].classList.remove('down'); trackUp(t); }
|
||||||
|
});
|
||||||
|
|
||||||
|
/* ---------- bench settings ---------- */
|
||||||
|
function bind(id,get,set){ var el=document.getElementById(id); el.value=get(); el.addEventListener('change',function(){ set(el.value); saveCfg(); dirty=true; }); return el; }
|
||||||
|
bind('monoCh',function(){return cfg.mono.ch;},function(v){cfg.mono.ch=clamp(v|0,1,16);});
|
||||||
|
bind('monoOct',function(){return cfg.mono.oct;},function(v){cfg.mono.oct=clamp(v|0,-3,3);});
|
||||||
|
['portDrums','portMono'].forEach(function(id){ document.getElementById(id).addEventListener('change',function(e){ cfg[id]=e.target.value; saveCfg(); }); });
|
||||||
|
document.getElementById('portIn').addEventListener('change',function(e){ cfg.portIn=e.target.value; saveCfg(); hookInput(); });
|
||||||
|
function applyHw(){
|
||||||
|
trk.classList.toggle('noled',!cfg.hw.leds);
|
||||||
|
var D=DISPLAYS[cfg.hw.display]||DISPLAYS['5110'], w=document.getElementById('hwNote');
|
||||||
|
w.textContent = D.h===32 ? (cfg.hw.leds ? 'This screen only fits the menu (two big lines). Track status is on the LEDs.' : 'This screen only fits the menu. Without LEDs there is no way to see which tracks are on.')
|
||||||
|
: 'Menu on top, a 4 × 3 track map underneath laid out like the buttons, so the LEDs are optional.';
|
||||||
|
}
|
||||||
|
bind('hwDisplay',function(){ return cfg.hw.display; },function(v){ cfg.hw.display=DISPLAYS[v]?v:'5110'; applyHw(); });
|
||||||
|
(function(){ var el=document.getElementById('hwLeds'); el.checked=!!cfg.hw.leds; el.addEventListener('change',function(){ cfg.hw.leds=el.checked; saveCfg(); applyHw(); }); })();
|
||||||
|
var clk=document.getElementById('sendClock'); clk.checked=!!cfg.clock; clk.addEventListener('change',function(){ cfg.clock=clk.checked; saveCfg(); });
|
||||||
|
var padNotes=[];
|
||||||
|
(function(){ var box=document.getElementById('padFields');
|
||||||
|
KIT.forEach(function(t,k){
|
||||||
|
var f=document.createElement('div'); f.className='field';
|
||||||
|
var lab=document.createElement('span'); lab.className='lbl'; lab.textContent=E.TRACKS[t].name;
|
||||||
|
var nt=document.createElement('span'); nt.className='val';
|
||||||
|
f.appendChild(lab); f.appendChild(nt); box.appendChild(f); padNotes[k]=nt;
|
||||||
|
}); })();
|
||||||
|
function refreshPadFields(){
|
||||||
|
KIT.forEach(function(t,k){ var L=learned(t); padNotes[k].textContent = L ? 'ch '+(L.ch+1)+' · note '+L.note : 'not learned'; padNotes[k].classList.toggle('miss',!L); });
|
||||||
|
}
|
||||||
|
document.getElementById('clearLearn').addEventListener('click',function(){ cfg.kit.learn=[null,null,null,null,null,null,null]; saveCfg(); refreshPadFields(); });
|
||||||
|
refreshPadFields();
|
||||||
|
|
||||||
|
document.getElementById('vecGo').addEventListener('click',function(){
|
||||||
|
var n=clamp(document.getElementById('vecBars').value|0,1,64), rows=[], bars=[], p=params(false), mask=0;
|
||||||
|
for(var b=0;b<n;b++){
|
||||||
|
var r=E.renderBar(app.state,p,b);
|
||||||
|
var ev=r.events.map(function(e){ return [e.t,e.lane,e.v,e.vel,e.len,e.tr]; });
|
||||||
|
bars.push({bar:b,fill:r.fill,events:ev}); rows=rows.concat(ev);
|
||||||
|
}
|
||||||
|
document.getElementById('vecOut').value=JSON.stringify({seed:'0x'+hex(app.state.seed),salts:app.state.salts,params:{gen:app.gen,energy:app.energy,drift:app.drift},
|
||||||
|
checksum:'0x'+hex(E.checksum(rows)),format:'[tick,lane,voice,vel,len,track]; all tracks are listed, on/off is applied at output',bars:bars});
|
||||||
|
});
|
||||||
|
document.getElementById('vecCopy').addEventListener('click',function(){
|
||||||
|
var ta=document.getElementById('vecOut'), b=this;
|
||||||
|
function ok(){ b.textContent='Copied'; setTimeout(function(){ b.textContent='Copy'; },1200); }
|
||||||
|
try{ navigator.clipboard.writeText(ta.value).then(ok,function(){ ta.select(); }); }catch(e){ ta.select(); }
|
||||||
|
});
|
||||||
|
|
||||||
|
/* ---------- Screen: 1-bit framebuffer, 5x7 font, laid out for the real panel ----------
|
||||||
|
0.91" 128x32: two big lines (label, value). No room for track status: that needs LEDs.
|
||||||
|
128x64 and Nokia 84x48: small label + value on top, a track map below that mirrors the button rows. */
|
||||||
|
var DISPLAYS={
|
||||||
|
'5110':{w:84,h:48,wmm:33.1,hmm:18.9,lcd:true,name:'Nokia 3310/5110 84×48 PCD8544'},
|
||||||
|
'091':{w:128,h:32,wmm:22.38,hmm:5.58,name:'0.91" 128×32 SSD1306'},
|
||||||
|
'096':{w:128,h:64,wmm:21.74,hmm:10.86,name:'0.96" 128×64 SSD1306'},
|
||||||
|
'130':{w:128,h:64,wmm:29.42,hmm:14.70,name:'1.3" 128×64 SH1106'},
|
||||||
|
'242':{w:128,h:64,wmm:55.01,hmm:27.49,name:'2.42" 128×64 SSD1309'}
|
||||||
|
};
|
||||||
|
var 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]};
|
||||||
|
var oled=document.getElementById('oled'), octx=oled.getContext('2d'), zoom=document.getElementById('oledZoom'), zctx=zoom.getContext('2d');
|
||||||
|
var FB_W=128, fb=new Uint8Array(128*64), fbH=32;
|
||||||
|
function fitText(s,x,y,maxW,v){ if(textW(s,2)<=maxW) text(s,x,y,2,v); else text(s,x,y+4,1,v); }
|
||||||
|
function px(x,y,v){ if(x>=0&&x<FB_W&&y>=0&&y<fbH) fb[y*FB_W+x]=v; }
|
||||||
|
function fillR(x,y,w,h,v){ for(var j=y;j<y+h;j++) for(var i=x;i<x+w;i++) px(i,j,v); }
|
||||||
|
function boxR(x,y,w,h){ fillR(x,y,w,1,1); fillR(x,y+h-1,w,1,1); fillR(x,y,1,h,1); fillR(x+w-1,y,1,h,1); }
|
||||||
|
function text(s,x,y,sc,v){
|
||||||
|
s=String(s).toUpperCase();
|
||||||
|
for(var c=0;c<s.length;c++){ var g=FONT[s[c]]||FONT['?'];
|
||||||
|
for(var col=0;col<5;col++) for(var row=0;row<7;row++) if((g[col]>>row)&1) fillR(x+col*sc,y+row*sc,sc,sc,v);
|
||||||
|
x+=(sc===1?6:12); }
|
||||||
|
}
|
||||||
|
function textW(s,sc){ return String(s).length*(sc===1?6:12)-(sc===1?1:2); }
|
||||||
|
function screenText(){
|
||||||
|
var pg=PAGES[app.page], label='', value='';
|
||||||
|
if(pg==='SEED'){ var sh=app.histSel>0?app.history[app.histSel-1]:app.state; label='SEED'+(app.histSel>0?' -'+app.histSel:'')+((sh.salts[0]||sh.salts[1])?' *':''); value=hex(sh.seed); }
|
||||||
|
else if(pg==='GEN'){ label='GENERATOR'; value=E.GEN_NAMES[app.gen]; }
|
||||||
|
else if(pg==='ROOT' || pg==='SCALE'){ // on AUTO, show the seed's own pick
|
||||||
|
var sc=pg==='SCALE', x=sc?app.scale:app.root, gk=E.genome(app.state.seed);
|
||||||
|
label=(sc?'SCALE':'ROOT')+(x<0?' AUTO':''); if(x<0) x=sc?gk.scale:gk.root; value=sc?E.SCALE_NAMES[x]:E.NOTE_NAMES[x]; }
|
||||||
|
else if(pg==='BPM'){ label='TEMPO'; value=app.bpm+' BPM'; }
|
||||||
|
else if(pg==='SLOT'){ label='SLOT '+(app.slotSel+1); value=app.slots[app.slotSel]?hex(app.slots[app.slotSel].seed):'EMPTY'; }
|
||||||
|
else if(pg==='ROLL'){ label='RE-ROLL'; value=app.rollSel?'LINE':'KIT'; }
|
||||||
|
else if(pg==='LEARN'){ label= app.learn===2 ? 'LEARN '+E.TRACKS[KIT[app.learnTr]].name.replace(' ','') : 'LEARN';
|
||||||
|
value= app.learn===0 ? 'PUSH' : app.learn===1 ? 'PICK TRACK' : (inPort?'HIT PAD':'NO INPUT'); }
|
||||||
|
if(performance.now()<flashMsgUntil) value=flashMsg;
|
||||||
|
return {label:label.slice(0,10),value:String(value).slice(0,10)};
|
||||||
|
}
|
||||||
|
function drawCell(x,y,w,h,state){ // state: 0 off, 1 on, 2 on+hit, 3 selectable, 4 selected
|
||||||
|
if(state===1||state===2||state===4) fillR(x,y,w,h,1); else boxR(x,y,w,h);
|
||||||
|
if(state===2) fillR(x+3,y+3,w-6,h-6,0);
|
||||||
|
if(state===3) fillR(x+(w>>1)-1,y+(h>>1)-1,2,2,1);
|
||||||
|
}
|
||||||
|
function cellState(t,now,mask,blink){
|
||||||
|
if(app.learn>0) return E.TRACKS[t].lane!==0 ? 0 : (app.learn===2 && KIT[app.learnTr]===t) ? (blink?4:3) : 3;
|
||||||
|
if(E.TRACKS[t].lane===0 && !cfg.kit.learn[t]) return 3; // a dot: this kit track still needs learning
|
||||||
|
var on=sounds(t,mask); if(pending[t] && blink) on=!on;
|
||||||
|
return on ? (now<hitUntil[t]?2:1) : 0;
|
||||||
|
}
|
||||||
|
var lastDisp='';
|
||||||
|
function drawOled(curBar,mask){
|
||||||
|
var D=DISPLAYS[cfg.hw.display]||DISPLAYS['5110'], now=performance.now(), blink=Math.floor(now/250)%2===0, s=screenText(), i, t;
|
||||||
|
if(lastDisp!==cfg.hw.display){ lastDisp=cfg.hw.display; FB_W=D.w; fbH=D.h; oled.width=D.w; oled.height=D.h; zoom.width=D.w; zoom.height=D.h;
|
||||||
|
oled.style.width=D.wmm+'mm'; oled.style.height=D.hmm+'mm'; zoom.style.aspectRatio=D.w+' / '+D.h;
|
||||||
|
oled.parentNode.classList.toggle('lcd',!!D.lcd); zoom.classList.toggle('lcd',!!D.lcd); }
|
||||||
|
fb.fill(0);
|
||||||
|
function drawMap(x0,y0,cw,ch,gx,gy){ // 4 x 3, same as the buttons and the wiring matrix
|
||||||
|
for(var k=0;k<12;k++){ var t=PANEL[k], x=x0+(k%4)*(cw+gx), y=y0+((k/4)|0)*(ch+gy);
|
||||||
|
drawCell(x,y,cw,ch, t<0 ? (S.running?1:0) : cellState(t,now,mask,blink)); } }
|
||||||
|
if(D.lcd){
|
||||||
|
// Nokia 84x48: 14 chars per line at 1x. Label + bar dots, value (2x when it fits), then the 7+4 track map with 10 px cells.
|
||||||
|
text(s.label,0,0,1,1);
|
||||||
|
if(S.running && curBar>=0) for(i=0;i<4;i++){ var nx=64+i*5; if(i===curBar%4) fillR(nx,1,4,5,1); else boxR(nx,1,4,5); }
|
||||||
|
if(app.editing){ fillR(0,8,84,10,1); text(s.value,1,9,1,0); } else text(s.value,1,9,1,1);
|
||||||
|
drawMap(1,20,19,8,2,2);
|
||||||
|
} else if(D.h===32){
|
||||||
|
text(s.label,2,0,2,1);
|
||||||
|
if(app.editing){ fillR(0,16,128,16,1); text(s.value,2,17,2,0); } else text(s.value,2,17,2,1);
|
||||||
|
} else {
|
||||||
|
text(s.label,1,0,1,1);
|
||||||
|
if(S.running && curBar>=0) for(i=0;i<4;i++){ var bx=104+i*6; if(i===curBar%4) fillR(bx,1,5,5,1); else boxR(bx,1,5,5); }
|
||||||
|
if(app.editing){ fillR(0,9,128,18,1); text(s.value,2,11,2,0); } else text(s.value,2,11,2,1);
|
||||||
|
drawMap(1,29,30,10,2,2);
|
||||||
|
}
|
||||||
|
var img=octx.createImageData(FB_W,fbH), d=img.data, ON=D.lcd?[28,40,38]:[214,236,255], OFF=D.lcd?[150,176,160]:[3,6,10];
|
||||||
|
for(i=0;i<FB_W*fbH;i++){ var c=fb[i]?ON:OFF; d[i*4]=c[0]; d[i*4+1]=c[1]; d[i*4+2]=c[2]; d[i*4+3]=255; }
|
||||||
|
octx.putImageData(img,0,0); zctx.putImageData(img,0,0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------- LED strip ---------- */
|
||||||
|
var hitUntil=[]; for(var i2=0;i2<NT;i2++) hitUntil.push(0);
|
||||||
|
function rgba(c,a){ return 'color-mix(in srgb, '+c+' '+Math.round(a*100)+'%, transparent)'; }
|
||||||
|
function drawLeds(now,mask,T){
|
||||||
|
var blink=Math.floor(now/250)%2===0, slow=Math.floor(now/450)%2===0;
|
||||||
|
for(var t=0;t<NT;t++){
|
||||||
|
var el=tleds[t], on=sounds(t,mask), lane=E.TRACKS[t].lane?T.mono:T.kit, col, glow=false;
|
||||||
|
if(app.learn>0){
|
||||||
|
if(E.TRACKS[t].lane!==0) col='var(--bezel)';
|
||||||
|
else if(app.learn===2 && KIT[app.learnTr]===t){ col=blink?'#FFB547':'var(--bezel)'; glow=blink; }
|
||||||
|
else { col=slow?'#FFB547':'var(--bezel)'; }
|
||||||
|
} else if(E.TRACKS[t].lane===0 && !cfg.kit.learn[t]){
|
||||||
|
col=rgba(lane,0.15);
|
||||||
|
} else {
|
||||||
|
if(on){ col=now<hitUntil[t]?lane:rgba(lane,0.7); glow=now<hitUntil[t]; }
|
||||||
|
else col='var(--bezel)';
|
||||||
|
if(pending[t] && !blink) col='var(--bezel)';
|
||||||
|
}
|
||||||
|
el.style.background=col; el.style.boxShadow=glow?'0 0 10px '+col:'inset 0 1px 1px rgba(0,0,0,.4)';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------- phrase view: one row per track ---------- */
|
||||||
|
var grid=document.getElementById('grid'), gctx=grid.getContext('2d'), dirty=true, view=null, viewKey='';
|
||||||
|
function tokens(){ var cs=getComputedStyle(document.documentElement); function v(n){ return cs.getPropertyValue(n).trim(); }
|
||||||
|
return {ink:v('--ink'),muted:v('--muted'),line:v('--line'),grid:v('--grid'),kit:v('--l-kit'),mono:v('--l-mono'),accent:v('--accent'),font:v('--f-mono')}; }
|
||||||
|
function phraseBars(phrase){ var out=[]; for(var i=0;i<4;i++){ var b=phrase*4+i; out.push(S.running && S.log[b] ? S.log[b].r : E.renderBar(app.state,params(false),b)); } return out; }
|
||||||
|
function drawGrid(curTick,mask,T){
|
||||||
|
var dpr=window.devicePixelRatio||1, W=grid.clientWidth, H=grid.clientHeight;
|
||||||
|
if(grid.width!==Math.round(W*dpr)||grid.height!==Math.round(H*dpr)){ grid.width=Math.round(W*dpr); grid.height=Math.round(H*dpr); }
|
||||||
|
gctx.setTransform(dpr,0,0,dpr,0,0); gctx.clearRect(0,0,W,H);
|
||||||
|
var bars=view.bars, total=0; bars.forEach(function(b){ total+=b.barTicks; });
|
||||||
|
var X0=58, GW=W-X0-4, rows=[], y=4, i;
|
||||||
|
for(i=0;i<NT;i++) rows.push({h:(i>=7?1.4:1)});
|
||||||
|
var unit=(H-24)/rows.reduce(function(a,r){ return a+r.h; },0);
|
||||||
|
rows.forEach(function(r){ r.y=y; r.hh=r.h*unit; y+=r.hh; });
|
||||||
|
var gb=y;
|
||||||
|
gctx.font='500 10px '+T.font; gctx.textBaseline='middle';
|
||||||
|
for(i=0;i<NT;i++){ var r=rows[i], on=sounds(i,mask);
|
||||||
|
gctx.fillStyle=on?T.ink:T.line; gctx.fillText(E.TRACKS[i].name,2,r.y+r.hh/2);
|
||||||
|
gctx.strokeStyle=(i===6)?T.line:T.grid; gctx.lineWidth=1; gctx.beginPath(); gctx.moveTo(X0,Math.round(r.y+r.hh)+.5); gctx.lineTo(X0+GW,Math.round(r.y+r.hh)+.5); gctx.stroke(); }
|
||||||
|
function X(t){ return X0+t/total*GW; }
|
||||||
|
var acc=0;
|
||||||
|
bars.forEach(function(b){
|
||||||
|
for(var t=0;t<b.barTicks;t+=E.TPS){ gctx.strokeStyle=(t===0)?T.muted:(t%TPB===0)?T.line:T.grid; gctx.lineWidth=(t===0)?1.5:1;
|
||||||
|
var xx=Math.round(X(acc+t))+.5; gctx.beginPath(); gctx.moveTo(xx,4); gctx.lineTo(xx,gb); gctx.stroke(); }
|
||||||
|
gctx.fillStyle=T.muted; gctx.textBaseline='top'; gctx.fillText(String.fromCharCode(65+b.sec)+(b.fill?' fill':''),X(acc)+4,gb+6); gctx.textBaseline='middle';
|
||||||
|
acc+=b.barTicks;
|
||||||
|
});
|
||||||
|
var rng={}; bars.forEach(function(b){ b.events.forEach(function(e){ if(e.lane===1){ var q=rng[e.tr]||(rng[e.tr]={lo:127,hi:0}); q.lo=Math.min(q.lo,e.v); q.hi=Math.max(q.hi,e.v); } }); });
|
||||||
|
acc=0;
|
||||||
|
bars.forEach(function(b){
|
||||||
|
b.events.forEach(function(e){
|
||||||
|
var r=rows[e.tr], x=X(acc+e.t), yy, hh, w, on=sounds(e.tr,mask);
|
||||||
|
if(e.lane===0){ hh=r.hh-6; yy=r.y+3; w=3; }
|
||||||
|
else { var q=rng[e.tr], span=Math.max(7,q.hi-q.lo); hh=3; yy=r.y+r.hh-5-((e.v-q.lo)/span)*(r.hh-8); w=Math.max(3,Math.min((e.len/total)*GW,X0+GW-x)); }
|
||||||
|
gctx.globalAlpha=on?0.3+0.7*e.vel/127:0.12; gctx.fillStyle=e.lane?T.mono:T.kit; gctx.fillRect(x,yy,w,hh);
|
||||||
|
});
|
||||||
|
acc+=b.barTicks;
|
||||||
|
});
|
||||||
|
gctx.globalAlpha=1;
|
||||||
|
if(curTick>=0){ var px=Math.round(X(curTick))+.5; gctx.strokeStyle=T.accent; gctx.lineWidth=2; gctx.beginPath(); gctx.moveTo(px,2); gctx.lineTo(px,gb); gctx.stroke(); }
|
||||||
|
}
|
||||||
|
|
||||||
|
var factsEl=document.getElementById('facts'), posEl=document.getElementById('pos');
|
||||||
|
function frame(){
|
||||||
|
var now=performance.now(), curBar=-1, curTick=-1, phrase=0, mask, T=tokens();
|
||||||
|
if(S.running){
|
||||||
|
var tk=tickAt(now);
|
||||||
|
for(var b in S.log){ var L=S.log[b]; if(tk>=L.start && tk<L.start+L.r.barTicks){ curBar=b|0; phrase=L.r.phrase; break; } }
|
||||||
|
if(curBar>=0){ var first=S.log[phrase*4]; if(first) curTick=tk-first.start; }
|
||||||
|
}
|
||||||
|
mask=curMask();
|
||||||
|
var key=phrase+'|'+app.state.seed+'|'+app.state.salts.join('.')+'|'+app.gen+'|'+S.running;
|
||||||
|
if(dirty || key!==viewKey || (S.running && view && view.bars.some(function(b,i){ return S.log[phrase*4+i] && S.log[phrase*4+i].r!==b; }))){
|
||||||
|
view={bars:phraseBars(phrase)}; viewKey=key; dirty=false;
|
||||||
|
var g=E.applyKey(E.genome(app.state.seed),params(false)), ds=E.describe(app.state,params(false));
|
||||||
|
factsEl.innerHTML='<span>Seed <b>'+hex(app.state.seed)+'</b></span><span>Generator <b>'+E.GEN_NAMES[app.gen].toLowerCase()+'</b></span><span>Bar <b>'+(app.gen===4?16:g.steps)+' steps</b></span><span>Key <b>'+E.NOTE_NAMES[g.root]+' '+E.SCALE_NAMES[g.scale].toLowerCase()+(app.scale<0||app.root<0?' (auto)':'')+'</b></span>'+(ds?'<span><b>'+ds+'</b></span>':'');
|
||||||
|
}
|
||||||
|
var on=0; for(var t=0;t<NT;t++) if(sounds(t,mask)) on++;
|
||||||
|
posEl.innerHTML=(S.running && curBar>=0 ? '<span>Phrase <b>'+(phrase+1)+'</b></span><span>Bar <b>'+(curBar%4+1)+'/4</b></span>' : '<span>Stopped</span>')+
|
||||||
|
'<span>Tracks on <b>'+on+'/'+NT+'</b></span><span><b>'+app.bpm+'</b> BPM</span>';
|
||||||
|
while(flashes.length && flashes[0].time<=now){ hitUntil[flashes[0].tr]=now+80; flashes.shift(); }
|
||||||
|
drawLeds(now,mask,T); drawGrid(curTick,mask,T); drawOled(curBar,mask);
|
||||||
|
requestAnimationFrame(frame);
|
||||||
|
}
|
||||||
|
applyHw();
|
||||||
|
requestAnimationFrame(frame);
|
||||||
|
window.addEventListener('beforeunload',function(){ stop(); });
|
||||||
|
})();
|
||||||
553
sim/engine.js
Normal file
@@ -0,0 +1,553 @@
|
|||||||
|
/* ================= ENGINE (portable) =================
|
||||||
|
No DOM, no Math.random, no floats in decisions. 840 ticks per quarter, 210 per 16th
|
||||||
|
(840 divides by 2,3,4,5,7 for tuplets). MIDI clock every 35 ticks.
|
||||||
|
|
||||||
|
The seed builds 11 looped tracks; the player brings them in and out by hand.
|
||||||
|
Energy = pattern density (hats only lightly). Drift = how often a phrase swaps in a variation of the detail.
|
||||||
|
Musical layer: a 4-chord progression per phrase shared by bass, pad and melody; a melodic motif that is
|
||||||
|
restated, sequenced and answered across the phrase; swing and metric velocity on every lane; B/C sections
|
||||||
|
are transformations of A instead of unrelated patterns; a phrase arc (flat, build, or break in bar 3).
|
||||||
|
Lanes: 0 = kit (v = voice 0 kick, 1 snare, 2 hat, 3 perc), 1 = synth (v = MIDI pitch). */
|
||||||
|
var Engine = (function(){
|
||||||
|
var TPB=840, TPS=210, CLOCK_TICKS=35, PHRASE_BARS=4;
|
||||||
|
function mix(a,b){ var h=(a ^ Math.imul(b|0,0x9E3779B1))>>>0; h^=h>>>16; h=Math.imul(h,0x85EBCA6B)>>>0; h^=h>>>13; h=Math.imul(h,0xC2B2AE35)>>>0; h^=h>>>16; return (h>>>0)||0x6D2B79F5; }
|
||||||
|
function Rng(s){ this.s=(s>>>0)||0x6D2B79F5; }
|
||||||
|
Rng.prototype.next=function(){ var x=this.s; x=(x^(x<<13))>>>0; x=(x^(x>>>17))>>>0; x=(x^(x<<5))>>>0; this.s=x; return x; };
|
||||||
|
Rng.prototype.below=function(n){ return n<=0?0:(((this.next()>>>16)*n)>>>16); };
|
||||||
|
Rng.prototype.chance=function(p){ return (this.next()>>>16) < p; };
|
||||||
|
Rng.prototype.pickW=function(w){ var t=0,i; for(i=0;i<w.length;i++) t+=w[i]; var r=this.below(t); for(i=0;i<w.length;i++){ if(r<w[i]) return i; r-=w[i]; } return w.length-1; };
|
||||||
|
function clamp(x,a,b){ return x<a?a:x>b?b:x; }
|
||||||
|
function P(x){ return clamp(x|0,0,127)*516; } // 0..127 -> 0..65532
|
||||||
|
function fdiv(a,b){ return Math.floor(a/b); } // floor division (C port: handle negatives explicitly)
|
||||||
|
function mod(a,b){ return ((a%b)+b)%b; }
|
||||||
|
|
||||||
|
var TRACKS=[
|
||||||
|
{name:'KICK', lane:0,voice:0},
|
||||||
|
{name:'KICK 2',lane:0,voice:0},
|
||||||
|
{name:'SNARE', lane:0,voice:1},
|
||||||
|
{name:'GHOST', lane:0,voice:1},
|
||||||
|
{name:'HAT', lane:0,voice:2},
|
||||||
|
{name:'HAT 16',lane:0,voice:2},
|
||||||
|
{name:'PERC', lane:0,voice:3},
|
||||||
|
{name:'BASS', lane:1,voice:0},
|
||||||
|
{name:'MEL A', lane:1,voice:0},
|
||||||
|
{name:'MEL B', lane:1,voice:0},
|
||||||
|
{name:'PAD', lane:1,voice:0}
|
||||||
|
];
|
||||||
|
var NT=TRACKS.length;
|
||||||
|
var T_KICK=0,T_KICK2=1,T_SNARE=2,T_GHOST=3,T_HAT=4,T_HAT16=5,T_PERC=6,T_BASS=7,T_MELA=8,T_MELB=9,T_PAD=10;
|
||||||
|
|
||||||
|
var STEPS=[16,12,14,20], STEPS_W=[12,3,2,2];
|
||||||
|
var FORMS=[[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=[5,5,3,2,3,2];
|
||||||
|
var FORM_NAMES=['AAAB','AABA','ABAB','AAAA','ABAC','ABAA'];
|
||||||
|
/* Menu order = index. 5 to 8 notes; chords stack every other scale note, so pentatonics give open chords,
|
||||||
|
whole tone augmented, octatonic diminished. Names fit the 10-character screen line. */
|
||||||
|
var SCALES=[[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]];
|
||||||
|
var SCALE_NAMES=['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'];
|
||||||
|
var NOTE_NAMES=['C','C#','D','D#','E','F','F#','G','G#','A','A#','B'];
|
||||||
|
var GEN_NAMES=['EUCLID','SKELETON','SUBDIV','AUTOMATA','AMENIZER'];
|
||||||
|
var HOCKETS=[0xAA,0x55,0xB4,0x6C,0x96,0xD2,0x5A,0xA5];
|
||||||
|
/* chord roots as scale-degree indices, one per bar of the phrase */
|
||||||
|
var PROGS=[[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]];
|
||||||
|
var SWINGS=[0,21,35,50,70], SWING_W=[4,3,3,2,1]; // delay of every second 16th, in ticks (70 = triplet feel)
|
||||||
|
var ARC_NAMES=['flat','build','break'];
|
||||||
|
var SPLITS=[1,2,3,4,5,7];
|
||||||
|
var SKEL=[
|
||||||
|
{k:[0,10],s:[4,12],g:[7,15]}, // two-step
|
||||||
|
{k:[0,2,10,11],s:[4,12],g:[7,9,15]}, // amen-ish
|
||||||
|
{k:[0,11],s:[8],g:[14,3]}, // half-time
|
||||||
|
{k:[0,3,10],s:[4,12],g:[7]}, // think-ish
|
||||||
|
{k:[0,6,11],s:[4,13],g:[9,15]}, // broken
|
||||||
|
{k:[0,7,10],s:[4,12],g:[2,14]} // steppy
|
||||||
|
];
|
||||||
|
/* Rhythmic frames: where the main kick(s) and snare(s) live. Positions in 16ths of a 16-step bar, scaled to the seed's bar.
|
||||||
|
One frame per seed, shared by every generator, so the generators vary the detail and the frame varies the feel. */
|
||||||
|
var FRAMES=[
|
||||||
|
{n:'backbeat', k:[0], s:[4,12]},
|
||||||
|
{n:'half-time', k:[0], s:[8]},
|
||||||
|
{n:'four-floor', k:[0,4,8,12], s:[4,12], layer:1},
|
||||||
|
{n:'3-3-2', k:[0,3,8,11], s:[6,14]},
|
||||||
|
{n:'broken', k:[0,10], s:[6,12]},
|
||||||
|
{n:'displaced', k:[3,10], s:[8], noOne:1},
|
||||||
|
{n:'over the bar',k:null, s:null, cyc:12},
|
||||||
|
{n:'free snare', k:[0], s:null, free:1}
|
||||||
|
], FRAME_W=[3,3,2,2,2,1,1,2];
|
||||||
|
function frameSets(g,steps,pos){
|
||||||
|
var F=FRAMES[g.frame], K={}, S={}, i;
|
||||||
|
function sc(x){ return Math.floor(x*steps/16); }
|
||||||
|
if(F.cyc){ for(i=0;i<steps;i++){ var x=pos*steps+i; if(x%F.cyc===0) K[i]=1; else if(x%F.cyc===(F.cyc>>1)) S[i]=1; } }
|
||||||
|
else {
|
||||||
|
for(i=0;i<F.k.length;i++) K[sc(F.k[i])]=1;
|
||||||
|
if(F.free){ for(i=1;i<steps;i++) if(eu(i,g.freeK,steps,g.freeRot) && !K[i]) S[i]=1; }
|
||||||
|
else for(i=0;i<F.s.length;i++) S[sc(F.s[i])]=1;
|
||||||
|
}
|
||||||
|
if(!F.layer) for(i in S) if(K[i]) delete K[i]; // kick and snare share a step only in four-floor
|
||||||
|
return {K:K,S:S,noOne:!!F.noOne};
|
||||||
|
}
|
||||||
|
var RULES=[30,45,54,57,60,73,90,105,110,122,126,150,182,225];
|
||||||
|
var POLY_N=[5,7,9,11,12,6];
|
||||||
|
|
||||||
|
function genome(seed){
|
||||||
|
var r=new Rng(mix(seed,0x47454E)), fi, g={};
|
||||||
|
g.steps=STEPS[r.pickW(STEPS_W)];
|
||||||
|
fi=r.pickW(FORMS_W); g.form=FORMS[fi]; g.formName=FORM_NAMES[fi];
|
||||||
|
g.scale=r.below(SCALES.length); g.root=r.below(12);
|
||||||
|
g.dens=[40+r.below(60),30+r.below(70)];
|
||||||
|
g.fillP=40+r.below(80); g.monoLen=r.below(3); g.monoRange=1+r.below(2);
|
||||||
|
g.rot=r.pickW([4,2,2,1]); // phase offset (16ths) of the ambiguous tracks: melody, perc
|
||||||
|
g.hocket=HOCKETS[r.below(HOCKETS.length)];
|
||||||
|
g.bassDeg=[4,2,3][r.below(3)];
|
||||||
|
// musical layer
|
||||||
|
g.prog=PROGS[r.below(PROGS.length)];
|
||||||
|
g.swing=SWINGS[r.pickW(SWING_W)];
|
||||||
|
g.arc=r.pickW([3,2,2]);
|
||||||
|
g.motifLen=3+r.below(3);
|
||||||
|
g.accentCycle=[0,3,5,3,7][r.pickW([3,3,2,2,1])];
|
||||||
|
g.layback=r.below(2);
|
||||||
|
g.frame=r.pickW(FRAME_W); g.freeK=2+r.below(3); g.freeRot=r.below(16);
|
||||||
|
return g;
|
||||||
|
}
|
||||||
|
function roleSeed(st,r){ return mix(mix(st.seed,0x524F4C45+r),st.salts[r]); }
|
||||||
|
function groups(steps){ var g=[],s; for(s=0;s<steps;s+=4) g.push([s,Math.min(4,steps-s)]); return g; }
|
||||||
|
function isBack(gi){ return (gi%2)===1; }
|
||||||
|
function eu(i,k,n,rot){ var j=(i+rot)%n; return ((j*k)%n)<k; } // Bresenham Euclidean
|
||||||
|
|
||||||
|
function divide(r,start,len,depth,maxD,W,prob,out,v,velFn){
|
||||||
|
if(depth<maxD){
|
||||||
|
var w=W.slice(); if(depth>0) w[0]+=depth*3;
|
||||||
|
var f=SPLITS[r.pickW(w)];
|
||||||
|
if(f>1){ for(var i=0;i<f;i++){ var a=start+Math.floor(i*len/f), b=start+Math.floor((i+1)*len/f); divide(r,a,b-a,depth+1,maxD,W,prob,out,v,velFn); } return; }
|
||||||
|
}
|
||||||
|
if(r.chance(prob)) out.push({t:start,v:v,vel:velFn(r,depth)});
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------- kit generators. Hat accents (vel >= 85) become HAT, the rest HAT 16. ---------- */
|
||||||
|
function euclidParams(coreSeed,steps,d,hd){
|
||||||
|
var c=new Rng(coreSeed), kOpts=[3,5,4,2,7], k=kOpts[c.pickW([4,3,2,1,1+(d>>5)])];
|
||||||
|
k=clamp(k,1,steps>>1);
|
||||||
|
var rots=[], r0; for(r0=0;r0<steps;r0++) if(((r0*k)%steps)<k) rots.push(r0); // rotations that keep a kick on step 0
|
||||||
|
var kick={k:k,n:steps,rot:rots[c.below(rots.length)]};
|
||||||
|
var ghost={k:1+c.below(3),n:steps,rot:c.below(steps)};
|
||||||
|
var hn=c.pickW([3,3,1,1,1])<2?steps:POLY_N[c.below(3)];
|
||||||
|
var hat={k:clamp((hn*(32+(hd>>2)))>>7,1,hn),n:hn,rot:c.below(hn)};
|
||||||
|
var pn=POLY_N[c.below(POLY_N.length)];
|
||||||
|
var perc={k:clamp(1+((pn*d)>>9)+c.below(2),1,pn-1),n:pn,rot:c.below(pn)};
|
||||||
|
return {kick:kick,ghost:ghost,hat:hat,perc:perc,accent:c.below(hat.n)};
|
||||||
|
}
|
||||||
|
function kitEuclid(coreSeed,varSeed,steps,d,hd,pos,fs){
|
||||||
|
var E=euclidParams(coreSeed,steps,d,hd), vr=new Rng(varSeed), hr=new Rng(mix(varSeed,0x484154+pos));
|
||||||
|
var ev=[], back=fs.S, i;
|
||||||
|
for(i=0;i<steps;i++){
|
||||||
|
var t=i*TPS, x=pos*steps+i;
|
||||||
|
if(back[i]) ev.push({t:t,v:1,vel:112});
|
||||||
|
else if(eu(i,E.ghost.k,E.ghost.n,E.ghost.rot) && vr.chance(P(d>>1))) ev.push({t:t,v:1,vel:38+vr.below(20)});
|
||||||
|
if(fs.K[i]) ev.push({t:t,v:0,vel:i===0?120:112,a:1});
|
||||||
|
else if(eu(i,E.kick.k,E.kick.n,E.kick.rot) && !back[i] && !(i===0 && fs.noOne)) ev.push({t:t,v:0,vel:104});
|
||||||
|
if(eu(x,E.hat.k,E.hat.n,E.hat.rot)){
|
||||||
|
var acc=eu(x,Math.max(1,E.hat.k>>1),E.hat.n,E.accent);
|
||||||
|
ev.push({t:t,v:2,vel:acc?92:66});
|
||||||
|
if(hr.chance(P(hd>>3))) ev.push({t:t+(TPS>>1),v:2,vel:44});
|
||||||
|
}
|
||||||
|
if(eu(x,E.perc.k,E.perc.n,E.perc.rot)) ev.push({t:t,v:3,vel:70+vr.below(40)});
|
||||||
|
}
|
||||||
|
return ev;
|
||||||
|
}
|
||||||
|
function kitSubdiv(coreSeed,varSeed,steps,d,hd,pos,fs){
|
||||||
|
var core=new Rng(coreSeed), vr=new Rng(varSeed), hr=new Rng(mix(varSeed,0x484154+pos));
|
||||||
|
var ev=[], gs=groups(steps), snT=[], gi, j;
|
||||||
|
var burst = core.chance(P(d>>1)) ? core.below(gs.length) : -1;
|
||||||
|
for(j=0;j<steps;j++){ if(fs.S[j]){ ev.push({t:j*TPS,v:1,vel:110+core.below(14)}); snT.push(j*TPS); } if(fs.K[j]) ev.push({t:j*TPS,v:0,vel:j===0?118:112,a:1}); }
|
||||||
|
for(gi=0;gi<gs.length;gi++){
|
||||||
|
var s=gs[gi][0]*TPS, L=gs[gi][1]*TPS, tmp=[];
|
||||||
|
divide(core,s,L,0,1,[6,5,1,0,0,0],P(10+(d>>2)),tmp,0,function(r){ return 96+r.below(16); });
|
||||||
|
for(j=0;j<tmp.length;j++) if(!(tmp[j].t===0 && fs.noOne) && snT.indexOf(tmp[j].t)<0) ev.push(tmp[j]);
|
||||||
|
divide(vr,s,L,0,1,[4,4,1,0,0,0],P(d>>3),ev,1,function(r){ return 34+r.below(22); });
|
||||||
|
if(gi===burst) divide(hr,s,L,0,2,[1,3,4,2,3,2],P(90),ev,2,function(r){ return 50+r.below(30); });
|
||||||
|
else divide(hr,s,L,0,1,[3,6,0,2,0,0],P(36+(hd>>2)),ev,2,function(r,dp){ return dp===0?90:58+r.below(20); });
|
||||||
|
divide(vr,s,L,0,1,[6,2,3,0,1,1],P(d>>2),ev,3,function(r){ return 60+r.below(45); });
|
||||||
|
}
|
||||||
|
return ev;
|
||||||
|
}
|
||||||
|
function kitSkeleton(coreSeed,varSeed,steps,d,hd,pos,fs,frame){
|
||||||
|
var core=new Rng(coreSeed), vr=new Rng(varSeed), hr=new Rng(mix(varSeed,0x484154+pos));
|
||||||
|
var ev=[], i, sk=SKEL[core.below(SKEL.length)];
|
||||||
|
function sc(x){ return Math.floor(x*steps/16); }
|
||||||
|
var sn=[];
|
||||||
|
if(frame===0) for(i=0;i<sk.s.length;i++) sn.push(sc(sk.s[i])); // backbeat frame: the template's own snares
|
||||||
|
else for(i in fs.S) sn.push(i|0); // any other frame decides the snares
|
||||||
|
for(i in fs.K) ev.push({t:(i|0)*TPS,v:0,vel:116,a:1});
|
||||||
|
for(i=0;i<sk.k.length;i++){ var s=sc(sk.k[i]); if(sk.k[i]===0) continue; if(core.chance(P(d>>2))) s=clamp(s+(core.below(2)?1:-1),1,steps-1); if(sn.indexOf(s)<0) ev.push({t:s*TPS,v:0,vel:104+core.below(12)}); }
|
||||||
|
for(i=0;i<sn.length;i++) ev.push({t:sn[i]*TPS,v:1,vel:110+core.below(14)});
|
||||||
|
for(i=0;i<sk.g.length;i++) if(vr.chance(P(d))) ev.push({t:sc(sk.g[i])*TPS,v:1,vel:36+vr.below(24)});
|
||||||
|
if(vr.chance(P(d>>1))){ var xs=1+vr.below(steps-1); if(sn.indexOf(xs)<0) ev.push({t:xs*TPS,v:0,vel:90+vr.below(20)}); }
|
||||||
|
var div=[2,2,1][core.below(3)], hp=P(36+(hd>>2));
|
||||||
|
for(i=0;i<steps;i+=div){
|
||||||
|
if(i%4===0) ev.push({t:i*TPS,v:2,vel:92});
|
||||||
|
else if(hr.chance(hp)){ var vel=60+hr.below(22);
|
||||||
|
if(hr.chance(P(hd>>3))){ var L=TPS*div; ev.push({t:i*TPS,v:2,vel:vel}); ev.push({t:i*TPS+(L>>1),v:2,vel:vel-8}); }
|
||||||
|
else ev.push({t:i*TPS,v:2,vel:vel}); }
|
||||||
|
}
|
||||||
|
var ek=clamp(1+((d*steps)>>8)+core.below(3),1,steps-1), rot=core.below(steps);
|
||||||
|
for(i=0;i<steps;i++) if(eu(i,ek,steps,rot)) ev.push({t:i*TPS,v:3,vel:64+vr.below(40)});
|
||||||
|
return ev;
|
||||||
|
}
|
||||||
|
function caRow(rule,init,steps,gens){
|
||||||
|
var row=init.slice(), g, i;
|
||||||
|
for(g=0;g<gens;g++){ var nx=[]; for(i=0;i<steps;i++){ var l=row[(i+steps-1)%steps], c=row[i], r=row[(i+1)%steps]; nx.push((rule>>((l<<2)|(c<<1)|r))&1); } row=nx; }
|
||||||
|
return row;
|
||||||
|
}
|
||||||
|
function randRow(r,steps,p){ var a=[]; for(var i=0;i<steps;i++) a.push(r.chance(p)?1:0); return a; }
|
||||||
|
function kitCA(coreSeed,varSeed,steps,d,hd,pos,sec,fs){
|
||||||
|
var core=new Rng(coreSeed), vr=new Rng(varSeed), thin=new Rng(mix(varSeed,0x5448+pos));
|
||||||
|
var ev=[], gen=sec*5+pos, i, nR=RULES.length;
|
||||||
|
var rk=RULES[core.below(nR)], rs=RULES[core.below(nR)], rh=RULES[vr.below(nR)], rp=RULES[vr.below(nR)];
|
||||||
|
var K=caRow(rk,randRow(core,steps,P(40)),steps,gen), S=caRow(rs,randRow(core,steps,P(40)),steps,gen),
|
||||||
|
H=caRow(rh,randRow(vr,steps,P(64)),steps,gen+pos), C=caRow(rp,randRow(vr,steps,P(40)),steps,gen);
|
||||||
|
for(i=0;i<steps;i++){
|
||||||
|
var t=i*TPS, back=!!fs.S[i];
|
||||||
|
if(back) ev.push({t:t,v:1,vel:112});
|
||||||
|
else if(S[i] && thin.chance(P(d>>1))) ev.push({t:t,v:1,vel:40+thin.below(20)});
|
||||||
|
if(fs.K[i]) ev.push({t:t,v:0,vel:118,a:1});
|
||||||
|
else if(K[i] && !back && !(i===0 && fs.noOne) && thin.chance(P(40+(d>>1)))) ev.push({t:t,v:0,vel:96+thin.below(20)});
|
||||||
|
if(H[i] && thin.chance(P(56+(hd>>2)))){ var nb=H[(i+steps-1)%steps]+H[(i+1)%steps]; ev.push({t:t,v:2,vel:58+nb*16}); }
|
||||||
|
if(C[i] && !C[(i+1)%steps] && thin.chance(P(d))) ev.push({t:t,v:3,vel:70+thin.below(40)});
|
||||||
|
}
|
||||||
|
return ev;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* AMENIZER: the four bars of the Amen break, velocities included, played on the kit tracks.
|
||||||
|
[step, voice (0 kick, 1 snare, 2 ride/hat, 3 crash/perc), velocity, main kick flag]. Ride: on-beat 8ths go to HAT, off-beat to HAT 16.
|
||||||
|
Snares under 90 land on GHOST, kicks without the flag on KICK 2. This is a by-ear transcription: adjust the table to taste. */
|
||||||
|
/* [step, voice, velocity, main flag]. Main flag = KICK / SNARE / HAT track; the rest land on KICK 2 / GHOST / HAT 16.
|
||||||
|
Velocities leave headroom: backbeats ~110, offbeat kicks in the 80s, ghosts in the 50s, ride 82 / 60. */
|
||||||
|
var AMEN=(function(){
|
||||||
|
function ride(b,crashAt){ for(var s=0;s<16;s+=2) if(s!==crashAt) b.push(s%4===0?[s,2,82,1]:[s,2,60,0]); return b; }
|
||||||
|
var A=ride([[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]],-1);
|
||||||
|
var C=ride([[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]],-1);
|
||||||
|
var D=ride([[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]],10);
|
||||||
|
return [A,A,C,D];
|
||||||
|
})();
|
||||||
|
/* A drummer, not a grid: every hit moves a little in level, a hit that follows the same drum closely comes in softer,
|
||||||
|
and the ride hand has a slightly different weight every bar. */
|
||||||
|
function humanizeAmen(ev,seed,bar){
|
||||||
|
ev.sort(function(a,b){ return a.t-b.t || a.v-b.v; });
|
||||||
|
var last=[-1e6,-1e6,-1e6,-1e6], lastV=[0,0,0,0], br=new Rng(mix(mix(seed,0x48554D),bar)), rideBias=br.below(11)-5;
|
||||||
|
for(var i=0;i<ev.length;i++){
|
||||||
|
var x=ev[i], base=x.vel, h=mix(mix(mix(seed,0x564C),bar),x.t*4+x.v), v=base+(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); // a quick second stroke on a kick or snare that was just hit hard: softer
|
||||||
|
last[x.v]=x.t; lastV[x.v]=base; x.vel=clamp(v,1,127);
|
||||||
|
}
|
||||||
|
return ev;
|
||||||
|
}
|
||||||
|
/* Timing feel per drum, in ticks (210 per 16th): snares sit back, ghosts float, the ride pushes slightly. */
|
||||||
|
function amenFeel(seed,bar,t,v,a){
|
||||||
|
var h=mix(mix(mix(seed,0x46454C),bar),t*4+v);
|
||||||
|
function j(n){ return (h%(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);
|
||||||
|
}
|
||||||
|
function amenSlice(bar,beat){ var b=AMEN[bar&3], out=[], i; for(i=0;i<b.length;i++) if(b[i][0]>=beat*4 && b[i][0]<beat*4+4) out.push(b[i]); return out; }
|
||||||
|
/* Drift chops the break per beat, like a jungle edit: swap in another beat of the break, repeat the last beat,
|
||||||
|
stutter a half beat, or ratchet the snare. At 0 the break plays straight. */
|
||||||
|
function kitAmen(seed,bar,pos,dr,fill){
|
||||||
|
var src=fill?3:pos, ev=[], prev=null, j, k;
|
||||||
|
for(j=0;j<4;j++){
|
||||||
|
var sl=amenSlice(src,j), r=new Rng(mix(mix(seed,0x414D454E),bar*4+j)), op=-1;
|
||||||
|
// edits grow steadily with the knob; later beats in the bar get edited more, the first beat stays put until ~70%
|
||||||
|
var pc=fdiv(dr*80,127), pb=fdiv(pc*[2,3,5,6][j],4), keep=(pos===0 && j===0) || (j===0 && dr<88);
|
||||||
|
if(!keep && r.chance(P(pb))) op=r.pickW([4,2,2,2,dr>80?1:0]);
|
||||||
|
if(op===0){ sl=amenSlice(r.below(4),j); } // same beat from another bar: the classic edit
|
||||||
|
else if(op===4){ sl=amenSlice(r.below(4),r.below(4)); op=0; } // any beat of the break
|
||||||
|
else if(op===1 && prev){ sl=prev; }
|
||||||
|
var base=j*4*TPS;
|
||||||
|
for(k=0;k<sl.length;k++){
|
||||||
|
var s=sl[k], rel=s[0]%4, t=base+rel*TPS;
|
||||||
|
if(op===2){ if(rel<2){ ev.push({t:t,v:s[1],vel:s[2],a:s[3]}); ev.push({t:t+2*TPS,v:s[1],vel:Math.max(1,s[2]-10),a:s[3]}); } continue; }
|
||||||
|
ev.push({t:t,v:s[1],vel:s[2],a:s[3]});
|
||||||
|
if(op===3 && s[1]===1){ ev.push({t:t+(TPS>>1),v:1,vel:Math.max(1,s[2]-24),a:0}); }
|
||||||
|
}
|
||||||
|
prev=sl;
|
||||||
|
}
|
||||||
|
return ev;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Sections B and C reuse A's core pattern and transform it, so the phrase hangs together. */
|
||||||
|
function transformKit(ev,steps,sec,seed,fs){
|
||||||
|
if(sec===0) return ev;
|
||||||
|
var r=new Rng(mix(seed,0x5345+sec)), ops=[r.below(8)], i, out=[];
|
||||||
|
if(r.chance(P(64))) ops.push(r.below(8));
|
||||||
|
var half=(steps>>1)*TPS;
|
||||||
|
for(i=0;i<ev.length;i++) out.push({t:ev[i].t,v:ev[i].v,vel:ev[i].vel,a:ev[i].a});
|
||||||
|
function has(o){ return ops.indexOf(o)>=0; }
|
||||||
|
if(has(0)){ // kick answer: push the last off-beat kick a 16th later, add one before the backbeat
|
||||||
|
var lastK=-1; for(i=0;i<out.length;i++) if(out[i].v===0 && out[i].t>0) lastK=i;
|
||||||
|
if(lastK>=0 && out[lastK].t+TPS<steps*TPS) out[lastK].t+=TPS;
|
||||||
|
out.push({t:3*TPS,v:0,vel:98});
|
||||||
|
}
|
||||||
|
if(has(1)){ // second half doubles its hats into 16ths
|
||||||
|
var hs=[]; for(i=0;i<out.length;i++) if(out[i].v===2 && out[i].t>=half && out[i].t%TPS===0) hs.push(out[i].t);
|
||||||
|
for(i=0;i<hs.length;i++) if(hs[i]+TPS<steps*TPS) out.push({t:hs[i]+TPS,v:2,vel:60});
|
||||||
|
}
|
||||||
|
if(has(2)){ // first half thins out: no perc, no ghosts
|
||||||
|
out=out.filter(function(e){ return !(e.t<half && (e.v===3 || (e.v===1 && e.vel<90))); });
|
||||||
|
}
|
||||||
|
if(has(3)){ // perc moves two 16ths later
|
||||||
|
for(i=0;i<out.length;i++) if(out[i].v===3) out[i].t=(out[i].t+2*TPS)%(steps*TPS);
|
||||||
|
}
|
||||||
|
if(has(4)){ // ghost snare pickups into each main snare
|
||||||
|
for(var sk in fs.S){ var sp=sk|0; if(sp>0) out.push({t:(sp-1)*TPS,v:1,vel:44}); }
|
||||||
|
}
|
||||||
|
if(has(5)){ // snare drag: the last main snare lands a 16th late
|
||||||
|
var ls=-1; for(i=0;i<out.length;i++) if(out[i].v===1 && out[i].vel>=90) ls=i;
|
||||||
|
if(ls>=0 && out[ls].t+TPS<steps*TPS) out[ls].t+=TPS;
|
||||||
|
}
|
||||||
|
if(has(6)){ // drop the one: no kick on the downbeat, the bass and pad keep the bar
|
||||||
|
var kn=0; for(i=0;i<out.length;i++) if(out[i].v===0) kn++;
|
||||||
|
if(kn>1) out=out.filter(function(e){ return !(e.v===0 && e.t===0); });
|
||||||
|
}
|
||||||
|
if(has(7)){ // half-time switch: main snares collapse to one in the middle of the bar
|
||||||
|
out=out.filter(function(e){ return !(e.v===1 && e.vel>=90); });
|
||||||
|
out.push({t:(steps>>1)*TPS,v:1,vel:116});
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
/* Phrase arc. build: bars 1-2 drop their offbeat clutter. break: bar 3 strips to kick-on-one, backbeat, hats. */
|
||||||
|
function arcKit(ev,g,pos){
|
||||||
|
if(g.arc===1 && pos<2) return ev.filter(function(e){ return !((e.v===3 || (e.v===1 && e.vel<90)) && (pos===0 || e.t%(2*TPS)!==0)); });
|
||||||
|
if(g.arc===2 && pos===2){ var k0=1e9; for(var i=0;i<ev.length;i++) if(ev[i].v===0 && ev[i].t<k0) k0=ev[i].t;
|
||||||
|
return ev.filter(function(e){ return (e.v===0 && e.t===k0) || (e.v===1 && e.vel>=90) || (e.v===2 && e.t%(2*TPS)===0); }); }
|
||||||
|
return ev;
|
||||||
|
}
|
||||||
|
/* Velocity by metric position (beat > 8th > 16th), plus an accent cycle of 3/5/7 sixteenths that runs across the bar line. */
|
||||||
|
function shapeKit(ev,g,pos,aoff,jit,jr){
|
||||||
|
for(var i=0;i<ev.length;i++){
|
||||||
|
var e=ev[i]; if(e.v<2 || e.t%TPS!==0) continue;
|
||||||
|
var st=e.t/TPS, sw=st+aoff, w=(sw%4===0)?100:(sw%2===0)?86:74, x=pos*g.steps+sw; // aoff slides where the 'strong' 16ths are
|
||||||
|
var v=fdiv(e.vel*w,100);
|
||||||
|
if(g.accentCycle && (x%g.accentCycle)===0) v+=24;
|
||||||
|
if(jit) v+=jr.below(2*jit+1)-jit;
|
||||||
|
e.vel=clamp(v,1,127);
|
||||||
|
}
|
||||||
|
return ev;
|
||||||
|
}
|
||||||
|
function kitFill(ev,steps,e,fr){
|
||||||
|
var gs=groups(steps), last=gs[gs.length-1], st=last[0]*TPS, L=last[1]*TPS, out=[], i, type=fr.below(4);
|
||||||
|
for(i=0;i<ev.length;i++) if(ev[i].t<st) out.push(ev[i]);
|
||||||
|
if(type===0){ out.push({t:st,v:0,vel:110}); var m=4+(e>>5); for(i=0;i<m;i++){ var q=m-i; out.push({t:st+L-Math.floor(L*q*q/(m*m)),v:1,vel:90+Math.floor(30*i/m)}); } }
|
||||||
|
else if(type===1){ out.push({t:st,v:0,vel:110}); var k=0; for(var t=st;t<st+L;t+=(TPS>>1),k++) out.push({t:t,v:(k%2)?3:1,vel:(k%2)?70+k*4:92+k*3}); }
|
||||||
|
else if(type===2){ out.push({t:st,v:0,vel:116}); out.push({t:st+L-TPS,v:1,vel:100}); out.push({t:st+L-(TPS>>1),v:1,vel:116}); } // drop-out, then a two-hit pickup
|
||||||
|
else { var prev=gs.length>1?gs[gs.length-2]:last, ps=prev[0]*TPS; // previous beat, stuttered
|
||||||
|
for(i=0;i<ev.length;i++) if(ev[i].t>=ps && ev[i].t<ps+(L>>1)){ out.push({t:st+(ev[i].t-ps),v:ev[i].v,vel:ev[i].vel}); out.push({t:st+(L>>1)+(ev[i].t-ps),v:ev[i].v,vel:clamp(ev[i].vel+10,1,127)}); } }
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------- synth tracks ---------- */
|
||||||
|
function chordDeg(g,pos){ return g.prog[pos%PHRASE_BARS]; }
|
||||||
|
function degPitch(g,base,idx){ var sc=SCALES[g.scale], n=sc.length; return base+g.root+12*fdiv(idx,n)+sc[mod(idx,n)]; }
|
||||||
|
function melPitch(g,idx){ var p=degPitch(g,60,idx); while(p<60) p+=12; while(p>96) p-=12; return p; } // keep the melody above the pad
|
||||||
|
function isChordTone(g,pos,idx){ var n=SCALES[g.scale].length, c=chordDeg(g,pos), x=mod(idx-c,n); return x===0 || x===2 || x===4; }
|
||||||
|
function nearestChordTone(g,pos,idx){ for(var d=0;d<4;d++){ if(isChordTone(g,pos,idx-d)) return idx-d; if(isChordTone(g,pos,idx+d)) return idx+d; } return idx; }
|
||||||
|
/* Motif: 3-5 notes inside an 8-step window, contour in scale steps. Same motif across the whole seed. */
|
||||||
|
function makeMotif(rs,g){
|
||||||
|
var r=new Rng(mix(rs,0x4D4F54)), n=g.motifLen, pos=[0], used={0:1}, cont=[0], i;
|
||||||
|
if(r.chance(P(40))){ pos[0]=r.below(2)?1:2; used={}; used[pos[0]]=1; }
|
||||||
|
while(pos.length<n){ var p=1+r.below(7); if(!used[p]){ used[p]=1; pos.push(p); } }
|
||||||
|
pos.sort(function(a,b){ return a-b; });
|
||||||
|
for(i=1;i<n;i++){ var stp=[-2,-1,1,2,-4,4][r.pickW([3,4,4,3,1,1])]; cont.push(cont[i-1]+stp); }
|
||||||
|
return {pos:pos,cont:cont,resp:r.below(3)}; // resp: how the second half answers (0 sequence up, 1 inversion, 2 displaced)
|
||||||
|
}
|
||||||
|
function lineTracks(rs,variant,g,steps,d,sec,pos,kit,fill){
|
||||||
|
var core=new Rng(mix(rs,0x4D4F+sec)), vr=new Rng(mix(mix(rs,0x5641+sec),variant));
|
||||||
|
var n=SCALES[g.scale].length, barT=steps*TPS, mel=[], bass=[], pad=[], i, j;
|
||||||
|
var gs=groups(steps), last=gs[gs.length-1], kickList=[], M=makeMotif(rs,g), c=chordDeg(g,pos);
|
||||||
|
for(i=0;i<kit.length;i++) if(kit[i].v===0 && kit[i].t%TPS===0) kickList.push(kit[i].t);
|
||||||
|
// melody: statement anchored on the bar's chord, then an answer in the second half
|
||||||
|
function place(start,cont,anchor,velBase){
|
||||||
|
for(var k=0;k<M.pos.length && k<cont.length;k++){
|
||||||
|
var s=start+M.pos[k]; if(s>=steps) break;
|
||||||
|
if(fill && s>=last[0]) break;
|
||||||
|
var idx=anchor+cont[k];
|
||||||
|
if(s%4===0) idx=nearestChordTone(g,pos,idx); // strong positions land on chord tones
|
||||||
|
if(vr.chance(P(d>>3))) continue; // drift/energy drops the odd note
|
||||||
|
mel.push({t:s*TPS,v:melPitch(g,idx),vel:velBase+(s%4===0?18:0)+vr.below(12)});
|
||||||
|
if(k+1<M.pos.length && k+1<cont.length && vr.chance(P(d>>2))){ // passing note between motif notes
|
||||||
|
var s2=start+M.pos[k+1]; if(s2-s>=2 && (s+1)<steps) mel.push({t:(s+1)*TPS,v:melPitch(g,idx+(cont[k+1]>cont[k]?1:-1)),vel:velBase-14});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var anchor=c+(core.chance(P(50))?2:0), half=steps>>1;
|
||||||
|
place(0,M.cont,anchor,82);
|
||||||
|
if(d>=40 || steps>=16){
|
||||||
|
var c2=M.cont.slice(), a2=anchor, st2=half;
|
||||||
|
if(M.resp===0) a2=anchor+2;
|
||||||
|
else if(M.resp===1) for(j=0;j<c2.length;j++) c2[j]=-c2[j];
|
||||||
|
else st2=half+1;
|
||||||
|
if(pos===PHRASE_BARS-1){ c2=c2.slice(0,Math.max(1,c2.length-1)); } // last bar: shorter answer, then the cadence
|
||||||
|
place(st2,c2,a2,74);
|
||||||
|
}
|
||||||
|
if(pos===PHRASE_BARS-1 && !fill){ var cs=Math.max(0,steps-4); mel=mel.filter(function(m){ return m.t<cs*TPS; }); mel.push({t:cs*TPS,v:melPitch(g,chordDeg(g,0)),vel:96}); }
|
||||||
|
if(fill){ var nxt=chordDeg(g,0)+n, k2=0; for(var tt=last[0]*TPS;tt<(last[0]+last[1])*TPS;tt+=TPS,k2++) mel.push({t:tt,v:melPitch(g,nxt-(last[1]-1-k2)),vel:76+k2*8}); }
|
||||||
|
var rot=g.rot*TPS;
|
||||||
|
for(i=0;i<mel.length;i++) mel[i].t=(mel[i].t+rot)%barT;
|
||||||
|
mel=oneAtATime(mel);
|
||||||
|
for(i=0;i<mel.length;i++) mel[i].tr=((g.hocket>>(i&7))&1)?T_MELA:T_MELB;
|
||||||
|
// bass: chord root on the kicks, the seed's colour tone on alternate kicks, an approach note into the next bar
|
||||||
|
function bp(idx){ var p=degPitch(g,36,idx); while(p>47) p-=12; while(p<36) p+=12; return p; }
|
||||||
|
var nextC=chordDeg(g,pos+1);
|
||||||
|
for(i=0;i<kickList.length;i++){
|
||||||
|
var t=kickList[i], idx=c;
|
||||||
|
if(t>0 && (i%2)===1 && core.chance(P(70))) idx=c+g.bassDeg;
|
||||||
|
bass.push({t:t,v:bp(idx),vel:t===0?112:98,tr:T_BASS});
|
||||||
|
}
|
||||||
|
if(steps>=8 && core.chance(P(60))){ var at=(steps-1)*TPS; if(kickList.indexOf(at)<0) bass.push({t:at,v:bp(nextC+(core.below(2)?1:-1)),vel:84,tr:T_BASS}); }
|
||||||
|
// pad: the bar's triad (in scale), voiced inside C3-B3
|
||||||
|
var tones=[c,c+2,c+4], seen={};
|
||||||
|
for(i=0;i<tones.length;i++){ var pc=mod(degPitch(g,0,tones[i]),12), p=48+pc; if(!seen[p]){ seen[p]=1; pad.push({t:0,v:p,vel:i===0?58:50,tr:T_PAD}); } }
|
||||||
|
return {mel:mel,bass:oneAtATime(bass),pad:pad};
|
||||||
|
}
|
||||||
|
|
||||||
|
function sortDedupe(ev){
|
||||||
|
ev.sort(function(a,b){ return a.t-b.t || a.v-b.v; });
|
||||||
|
var out=[], i;
|
||||||
|
for(i=0;i<ev.length;i++){ var p=out[out.length-1]; if(p && p.t===ev[i].t && p.v===ev[i].v){ if(ev[i].vel>p.vel) p.vel=ev[i].vel; } else out.push({t:ev[i].t,v:ev[i].v,vel:ev[i].vel,tr:ev[i].tr,a:ev[i].a}); }
|
||||||
|
for(i=0;i<out.length;i++) out[i].vel=clamp(out[i].vel,1,127);
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
function oneAtATime(ev){ var o=sortDedupe(ev), out=[]; for(var i=0;i<o.length;i++){ if(out.length && out[out.length-1].t===o[i].t) out[out.length-1]=o[i]; else out.push(o[i]); } return out; }
|
||||||
|
|
||||||
|
function kitTrack(e){
|
||||||
|
if(e.v===0) return (e.a || e.t===0)?T_KICK:T_KICK2; // frame kicks are the main kick, wherever they fall
|
||||||
|
if(e.v===1) return (e.a || e.vel>=90)?T_SNARE:T_GHOST;
|
||||||
|
if(e.v===2) return (e.a || e.vel>=85)?T_HAT:T_HAT16;
|
||||||
|
return T_PERC;
|
||||||
|
}
|
||||||
|
/* Groove: every second 16th is delayed by the seed's swing; main snares can sit slightly behind. */
|
||||||
|
function groove(t,g,isSnare,barTicks,swing){
|
||||||
|
if(t%TPS===0 && (t/TPS)%2===1) t+=swing;
|
||||||
|
if(isSnare && g.layback) t+=12;
|
||||||
|
return Math.min(t,barTicks-1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* DRIFT, one knob with overlapping layers that each fade in smoothly (0 = locked loop):
|
||||||
|
0-42 phrase variations go from never to every phrase
|
||||||
|
20-127 microtiming: hits lean off the grid, hats drag, snares rush, kick and bass move together
|
||||||
|
40-127 swing wobbles from bar to bar
|
||||||
|
55-127 accents slip: the strong 16ths of the hats and perc move, velocities loosen
|
||||||
|
80-127 whole parts slide a 16th early or late for a bar (hats, perc, ghosts, second kick, melody)
|
||||||
|
105-127 a beat now and then is squeezed onto a triplet grid */
|
||||||
|
function amt(d,a,b){ return clamp(fdiv((d-a)*127,b-a),0,127); }
|
||||||
|
var NUDGE_GRP=[0,0,1,3,2,2,3,0,4,4,5]; // per track: 0 low end (kick, bass), 1 snare, 2 hats, 3 ghost/perc, 4 melody, 5 pad
|
||||||
|
function nudge(barSeed,t,grp,micro){
|
||||||
|
if(!micro || grp===5) return 0;
|
||||||
|
var mx=fdiv(micro*(grp===4?30:48),127), h=mix(mix(barSeed,t),grp), n=(h%(2*mx+1))-mx;
|
||||||
|
if(grp===2) n+=fdiv(micro*10,127); // hats drag
|
||||||
|
if(grp===1) n-=fdiv(micro*8,127); // snares rush
|
||||||
|
return n;
|
||||||
|
}
|
||||||
|
function shiftGroup(ev,test,by,barT){ for(var i=0;i<ev.length;i++) if(test(ev[i])) ev[i].t=mod(ev[i].t+by,barT); }
|
||||||
|
function tripletWarp(ev,beat){ // the four 16ths of one beat land on three triplet 8ths
|
||||||
|
var s=beat*TPB, out=[];
|
||||||
|
for(var i=0;i<ev.length;i++){ var e=ev[i]; if(e.t>=s && e.t<s+TPB){ var k=Math.min(2,fdiv((e.t-s)*3+TPB/2,TPB)); e.t=s+k*280; } out.push(e); }
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* The player's ROOT and SCALE pages: -1 (or absent) = AUTO, the seed's own pick. */
|
||||||
|
function applyKey(g,p){
|
||||||
|
if(typeof p.scale==='number' && p.scale>=0 && p.scale<SCALES.length) g.scale=p.scale;
|
||||||
|
if(typeof p.root==='number' && p.root>=0 && p.root<12) g.root=p.root;
|
||||||
|
return g;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* All tracks are rendered every bar; which ones sound is up to the player. */
|
||||||
|
function renderBar(st,p,bar){
|
||||||
|
var g=applyKey(genome(st.seed),p), amen=p.gen===4; if(amen){ g.steps=16; g.swing=0; g.layback=0; } // the break: 16 steps, straight; drift adds the swing
|
||||||
|
var pos=bar%PHRASE_BARS, phrase=(bar/PHRASE_BARS)|0, sec=g.form[pos], barTicks=g.steps*TPS, i, j;
|
||||||
|
var fr=new Rng(mix(mix(st.seed,0x46494C),phrase));
|
||||||
|
var fill = !!p.fill || (!amen && pos===PHRASE_BARS-1 && fr.chance(P(g.fillP>>1)));
|
||||||
|
var variants=[], r;
|
||||||
|
var dr=clamp(p.drift|0,0,127), dVar=Math.min(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); } // the break already has its feel: the generic layers come in later and softer
|
||||||
|
var dRng=new Rng(mix(mix(st.seed,0x445249),bar)), barSeed=mix(st.seed,0x4E55+bar);
|
||||||
|
for(r=0;r<2;r++) variants.push(phrase>0 && new Rng(mix(mix(roleSeed(st,r),0xD1F7),phrase)).chance(P(dVar)) ? phrase : 0);
|
||||||
|
var e=clamp(p.energy|0,0,127), krs=roleSeed(st,0), cS=mix(krs,0x434F), vS=mix(mix(krs,0x5641+sec),variants[0]);
|
||||||
|
var arcD=(g.arc===1)?(pos*10-15):0;
|
||||||
|
var kd=clamp(g.dens[0]+e-64+arcD,0,127), hd=clamp(g.dens[0]+((e-64)>>2)+arcD,0,127); // energy moves hats only a quarter as much
|
||||||
|
var fs=frameSets(g,g.steps,pos);
|
||||||
|
var kit;
|
||||||
|
if(amen){
|
||||||
|
kit=humanizeAmen(kitAmen(krs,bar,pos,dr,fill),krs,bar); // the break's own velocities, humanized, untouched by the shaping below
|
||||||
|
if(slip){ var jt=fdiv(slip*24,127); for(i=0;i<kit.length;i++) kit[i].vel=clamp(kit[i].vel+dRng.below(2*jt+1)-jt,1,127); }
|
||||||
|
} else {
|
||||||
|
kit = p.gen===0 ? kitEuclid(cS,vS,g.steps,kd,hd,pos,fs)
|
||||||
|
: p.gen===1 ? kitSkeleton(cS,vS,g.steps,kd,hd,pos,fs,g.frame)
|
||||||
|
: p.gen===2 ? kitSubdiv(cS,vS,g.steps,kd,hd,pos,fs)
|
||||||
|
: kitCA(cS,vS,g.steps,kd,hd,pos,sec,fs);
|
||||||
|
kit=transformKit(kit,g.steps,sec,krs,fs);
|
||||||
|
kit=arcKit(kit,g,pos);
|
||||||
|
if(fill) kit=kitFill(kit,g.steps,p.energy,fr);
|
||||||
|
var aoff = dRng.chance(P(slip)) ? 1+dRng.below(3) : 0;
|
||||||
|
kit=shapeKit(kit,g,pos,aoff,fdiv(slip*24,127),dRng);
|
||||||
|
}
|
||||||
|
if(disp){ // whole parts slide a 16th for this bar
|
||||||
|
var dirs=[TPS,-TPS];
|
||||||
|
if(dRng.chance(P(disp))) shiftGroup(kit,function(x){ return x.v===2; },dirs[dRng.below(2)],barTicks);
|
||||||
|
if(dRng.chance(P(disp))) shiftGroup(kit,function(x){ return x.v===3; },dirs[dRng.below(2)],barTicks);
|
||||||
|
if(dRng.chance(P(disp>>1))) shiftGroup(kit,function(x){ return x.v===1 && x.vel<90; },dirs[dRng.below(2)],barTicks);
|
||||||
|
if(dRng.chance(P(disp>>1))) shiftGroup(kit,function(x){ return x.v===0 && !x.a && x.t>0; },dirs[dRng.below(2)],barTicks);
|
||||||
|
if(dRng.chance(P(disp>>2))) shiftGroup(kit,function(x){ return x.v===1 && x.vel>=90; },TPS,barTicks);
|
||||||
|
}
|
||||||
|
var warpBeat = warp && dRng.chance(P(warp)) ? dRng.below(Math.max(1,fdiv(g.steps,4))) : -1;
|
||||||
|
if(warpBeat>=0) kit=tripletWarp(kit,warpBeat);
|
||||||
|
kit=sortDedupe(kit);
|
||||||
|
var melShift = disp && dRng.chance(P(disp>>1)) ? (dRng.below(2)?TPS:-TPS) : 0;
|
||||||
|
var swingBar = wob ? clamp(g.swing+dRng.below(2*fdiv(wob*40,127)+1)-fdiv(wob*40,127),0,90) : g.swing;
|
||||||
|
var lt=lineTracks(roleSeed(st,1),variants[1],g,g.steps,clamp(g.dens[1]+e-64,0,127),sec,pos,kit,fill), out=[];
|
||||||
|
for(i=0;i<kit.length;i++){
|
||||||
|
var tr=kitTrack(kit[i]), t=kit[i].t;
|
||||||
|
if(tr===T_PERC) t=(t+g.rot*TPS)%barTicks; // perc shares the melody's phase offset
|
||||||
|
var nt=barTicks; for(j=i+1;j<kit.length;j++) if(kit[j].v===kit[i].v){ nt=kit[j].t; break; }
|
||||||
|
var len=Math.max(10,Math.min(105,nt>t?nt-t:105));
|
||||||
|
out.push({t:clamp(groove(t,g,tr===T_SNARE,barTicks,swingBar)+nudge(barSeed,t,NUDGE_GRP[tr],micro)+(amen?amenFeel(krs,bar,t,kit[i].v,kit[i].a):0),0,barTicks-1),lane:0,v:kit[i].v,vel:kit[i].vel,len:len,tr:tr});
|
||||||
|
}
|
||||||
|
function mono(list,lenRule){
|
||||||
|
for(var k=0;k<list.length;k++){ var nx=list[k+1], gap=(nx?nx.t:barTicks)-list[k].t, len=lenRule(gap,nx,list[k]);
|
||||||
|
var t0=list[k].t; if(melShift && (list[k].tr===T_MELA || list[k].tr===T_MELB)) t0=mod(t0+melShift,barTicks);
|
||||||
|
out.push({t:clamp(groove(t0,g,false,barTicks,swingBar)+nudge(barSeed,list[k].t,NUDGE_GRP[list[k].tr],micro),0,barTicks-1),lane:1,v:list[k].v,vel:list[k].vel,len:Math.max(10,len-swingBar),tr:list[k].tr}); }
|
||||||
|
}
|
||||||
|
var melA=[], melB=[]; for(i=0;i<lt.mel.length;i++) (lt.mel[i].tr===T_MELA?melA:melB).push(lt.mel[i]);
|
||||||
|
function melLen(gap,nx,cur){ return g.monoLen===0?Math.min(90,gap-5):g.monoLen===1?Math.max(30,gap>>1):((nx && nx.v!==cur.v)?gap+20:gap-5); }
|
||||||
|
mono(melA,melLen); mono(melB,melLen);
|
||||||
|
mono(lt.bass,function(gap){ return gap-10; });
|
||||||
|
for(i=0;i<lt.pad.length;i++) out.push({t:0,lane:1,v:lt.pad[i].v,vel:lt.pad[i].vel,len:barTicks-30,tr:T_PAD});
|
||||||
|
out.sort(function(a,b){ return a.t-b.t || a.tr-b.tr || a.v-b.v; });
|
||||||
|
return {bar:bar,pos:pos,phrase:phrase,sec:sec,fill:fill,variants:variants,barTicks:barTicks,steps:g.steps,events:out};
|
||||||
|
}
|
||||||
|
var ROMAN=['i','ii','iii','iv','v','vi','vii'];
|
||||||
|
function describe(st,p){
|
||||||
|
var g=applyKey(genome(st.seed),p), s=[];
|
||||||
|
s.push(p.gen===4?'amen break':FRAMES[g.frame].n);
|
||||||
|
s.push('chords '+g.prog.map(function(x){ return ROMAN[x%7]; }).join('–'));
|
||||||
|
if(p.gen!==4){
|
||||||
|
s.push(g.swing?'swing '+Math.round(g.swing*100/TPS)+'%':'straight');
|
||||||
|
if(g.arc) s.push(ARC_NAMES[g.arc]);
|
||||||
|
if(g.accentCycle) s.push('accents every '+g.accentCycle);
|
||||||
|
}
|
||||||
|
if(p.gen===0){ var dd=clamp(g.dens[0]+(p.energy|0)-64,0,127), E=euclidParams(mix(roleSeed(st,0),0x434F),g.steps,dd,clamp(g.dens[0]+(((p.energy|0)-64)>>2),0,127));
|
||||||
|
s.push('kick E('+E.kick.k+','+E.kick.n+') hat E('+E.hat.k+','+E.hat.n+') perc E('+E.perc.k+','+E.perc.n+')'); }
|
||||||
|
if(g.rot) s.push('melody + perc shifted '+g.rot+'/16');
|
||||||
|
return s.join(' · ');
|
||||||
|
}
|
||||||
|
function checksum(rows){ var h=0x811C9DC5>>>0; for(var i=0;i<rows.length;i++) for(var j=0;j<rows[i].length;j++){ h=Math.imul(h^(rows[i][j]&0xFFFF),0x01000193)>>>0; } return h; }
|
||||||
|
|
||||||
|
return {TPB:TPB,TPS:TPS,CLOCK_TICKS:CLOCK_TICKS,PHRASE_BARS:PHRASE_BARS,TRACKS:TRACKS,NT:NT,
|
||||||
|
genome:genome,applyKey:applyKey,renderBar:renderBar,
|
||||||
|
describe:describe,checksum:checksum,mix:mix,Rng:Rng,SCALE_NAMES:SCALE_NAMES,NOTE_NAMES:NOTE_NAMES,GEN_NAMES:GEN_NAMES};
|
||||||
|
})();
|
||||||
|
if(typeof module!=='undefined') module.exports=Engine;
|
||||||
237
sim/index.html
Normal file
@@ -0,0 +1,237 @@
|
|||||||
|
<title>Seedbox</title>
|
||||||
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||||
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=Hanken+Grotesk:wght@400;500;600&family=Martian+Mono:wght@400;500;700&display=swap" rel="stylesheet">
|
||||||
|
<style>
|
||||||
|
/* Layout: left = the box as it will be built; right = phrase view, voice switches, bench settings. Stacks on phones. */
|
||||||
|
:root{
|
||||||
|
--bg:#E3E7EB; --surface:#F1F3F5; --panel-a:#D8DDE2; --panel-b:#BFC7CF; --ink:#19202A; --muted:#566271;
|
||||||
|
--line:#BAC2CB; --grid:#D3D9DF; --accent:#1E4C9E; --warn:#B4441F;
|
||||||
|
--l-kit:#13737A; --l-mono:#6243AB;
|
||||||
|
--oled-bg:#03060A; --cap:#262C33; --cap-hi:#3A424B; --cap-ink:#E6EBEF; --bezel:#11161C;
|
||||||
|
--f-ui:'Hanken Grotesk', system-ui, -apple-system, 'Segoe UI', sans-serif;
|
||||||
|
--f-mono:'Martian Mono', ui-monospace, 'SFMono-Regular', Menlo, Consolas, monospace;
|
||||||
|
}
|
||||||
|
@media (prefers-color-scheme: dark){ :root:not([data-theme="light"]){
|
||||||
|
--bg:#12161B; --surface:#1A1F25; --panel-a:#2A3038; --panel-b:#1E242A; --ink:#E2E7EC; --muted:#8C98A5;
|
||||||
|
--line:#333B44; --grid:#262D35; --accent:#8EB0F2; --warn:#F07B52;
|
||||||
|
--l-kit:#4CC4C9; --l-mono:#A88EF2;
|
||||||
|
--cap:#0E1216; --cap-hi:#272E36; --cap-ink:#D9E0E6; --bezel:#07090C; color-scheme:dark } }
|
||||||
|
:root[data-theme="dark"]{
|
||||||
|
--bg:#12161B; --surface:#1A1F25; --panel-a:#2A3038; --panel-b:#1E242A; --ink:#E2E7EC; --muted:#8C98A5;
|
||||||
|
--line:#333B44; --grid:#262D35; --accent:#8EB0F2; --warn:#F07B52;
|
||||||
|
--l-kit:#4CC4C9; --l-mono:#A88EF2;
|
||||||
|
--cap:#0E1216; --cap-hi:#272E36; --cap-ink:#D9E0E6; --bezel:#07090C; color-scheme:dark }
|
||||||
|
|
||||||
|
*{box-sizing:border-box}
|
||||||
|
body{background:var(--bg);color:var(--ink);font:15px/1.5 var(--f-ui);padding:20px 16px 40px}
|
||||||
|
.wrap{max-width:1000px;margin:0 auto;display:grid;grid-template-columns:minmax(0,1fr);gap:28px;align-items:start}
|
||||||
|
|
||||||
|
h1{font:700 22px/1.1 var(--f-mono);letter-spacing:-.01em;margin:0}
|
||||||
|
.sub{color:var(--muted);font-size:14px;margin:6px 0 0;max-width:62ch}
|
||||||
|
.label{font:500 10px/1.2 var(--f-mono);letter-spacing:.1em;text-transform:uppercase;color:var(--accent)}
|
||||||
|
|
||||||
|
/* the box */
|
||||||
|
.device{position:relative;border-radius:14px;padding:26px 22px 22px;background:linear-gradient(165deg,var(--panel-a),var(--panel-b));
|
||||||
|
box-shadow:inset 0 1px 0 rgba(255,255,255,.35),inset 0 -2px 0 rgba(0,0,0,.12),0 10px 30px -12px rgba(0,0,0,.35);display:grid;gap:18px}
|
||||||
|
.screw{position:absolute;width:9px;height:9px;border-radius:50%;background:radial-gradient(circle at 35% 35%,var(--cap-hi),var(--cap))}
|
||||||
|
.screw.tl{top:8px;left:8px}.screw.tr{top:8px;right:8px}.screw.bl{bottom:8px;left:8px}.screw.br{bottom:8px;right:8px}
|
||||||
|
.dev-head{display:flex;justify-content:space-between;align-items:baseline;gap:8px}
|
||||||
|
.dev-name{font:700 13px var(--f-mono);letter-spacing:.24em;color:var(--accent)}
|
||||||
|
.oledrow{display:flex;align-items:flex-end;gap:10px}
|
||||||
|
.device{max-width:820px}
|
||||||
|
.backwall{display:flex;align-items:center;gap:6px;flex-wrap:wrap;padding:6px 10px;border-radius:6px;background:rgba(0,0,0,.10);box-shadow:inset 0 1px 2px rgba(0,0,0,.2)}
|
||||||
|
.backwall .label{margin-right:8px}
|
||||||
|
.bw-spacer{flex:1}
|
||||||
|
.usb{width:22px;height:10px;border-radius:3px;background:var(--bezel)}
|
||||||
|
.zones{display:grid;grid-template-columns:auto auto minmax(0,1fr);gap:22px;align-items:center}
|
||||||
|
@media (max-width:700px){.zones{grid-template-columns:minmax(0,1fr)}}
|
||||||
|
.zone{display:grid;gap:14px;justify-items:center;min-width:0}
|
||||||
|
.zl{justify-items:start}
|
||||||
|
.zlrow{display:flex;gap:22px;align-items:flex-end}
|
||||||
|
.zm{gap:12px}
|
||||||
|
.zm .big{width:72px;height:72px}
|
||||||
|
.oled{background:var(--bezel);border-radius:3px;padding:2mm;line-height:0}
|
||||||
|
.oled.lcd{background:#3A4440;padding:2.5mm}
|
||||||
|
.oled canvas{display:block;image-rendering:pixelated;max-width:100%}
|
||||||
|
.zoomwrap{background:var(--bezel);border-radius:6px;padding:10px;line-height:0}
|
||||||
|
#oledZoom{display:block;width:100%;max-width:520px;image-rendering:pixelated}
|
||||||
|
.trk.noled .tled{visibility:hidden}
|
||||||
|
.runcell{display:grid;place-items:center}
|
||||||
|
.row{display:flex;gap:16px;align-items:flex-end;justify-content:space-between;flex-wrap:wrap}
|
||||||
|
.ctl{display:grid;justify-items:center;gap:6px}
|
||||||
|
.knob{width:62px;height:62px;border-radius:50%;position:relative;cursor:ns-resize;touch-action:none;
|
||||||
|
background:radial-gradient(circle at 40% 30%,var(--cap-hi),var(--cap) 70%);box-shadow:0 3px 6px rgba(0,0,0,.3),0 0 0 4px rgba(0,0,0,.08)}
|
||||||
|
.knob::after{content:"";position:absolute;left:50%;top:6px;width:3px;height:18px;margin-left:-1.5px;border-radius:2px;background:var(--cap-ink);transform-origin:50% 25px;transform:rotate(var(--rot,0deg))}
|
||||||
|
.knob:focus-visible,.enc:focus-visible,.cap:focus-visible,.big:focus-visible,.toggle:focus-visible,.vs:focus-visible,.btn:focus-visible{outline:2px solid var(--accent);outline-offset:3px}
|
||||||
|
.val{font:500 11px var(--f-mono);color:var(--muted);font-variant-numeric:tabular-nums}
|
||||||
|
.enc{width:52px;height:52px;border-radius:50%;cursor:ew-resize;touch-action:none;position:relative;background:repeating-conic-gradient(var(--cap) 0 6deg,var(--cap-hi) 6deg 12deg);box-shadow:0 3px 6px rgba(0,0,0,.3)}
|
||||||
|
.enc::after{content:"";position:absolute;inset:10px;border-radius:50%;background:radial-gradient(circle at 40% 30%,var(--cap-hi),var(--cap) 75%)}
|
||||||
|
.enc i{position:absolute;z-index:1;left:50%;top:14px;width:4px;height:4px;margin-left:-2px;border-radius:50%;background:var(--cap-ink);transform-origin:2px 12px;transform:rotate(var(--rot,0deg))}
|
||||||
|
.big{width:96px;height:96px;border-radius:50%;border:0;cursor:pointer;font:700 11px var(--f-mono);letter-spacing:.14em;color:var(--cap-ink);touch-action:none;
|
||||||
|
background:radial-gradient(circle at 40% 30%,var(--cap-hi),var(--cap) 72%);box-shadow:0 5px 0 rgba(0,0,0,.35),0 0 0 6px rgba(0,0,0,.1)}
|
||||||
|
.big.down{transform:translateY(3px);box-shadow:0 2px 0 rgba(0,0,0,.35),0 0 0 6px rgba(0,0,0,.1)}
|
||||||
|
.caps{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:12px;flex:1;min-width:0}
|
||||||
|
.capw{display:grid;justify-items:center;gap:6px}
|
||||||
|
.led{width:8px;height:8px;border-radius:50%;background:rgba(0,0,0,.35);box-shadow:inset 0 1px 1px rgba(0,0,0,.4)}
|
||||||
|
.led.on{background:var(--c);box-shadow:0 0 8px var(--c)}
|
||||||
|
.cap{width:100%;max-width:70px;height:38px;border:0;border-radius:6px;cursor:pointer;color:var(--cap-ink);font:600 10px var(--f-mono);letter-spacing:.08em;touch-action:none;
|
||||||
|
background:linear-gradient(var(--cap-hi),var(--cap));box-shadow:0 3px 0 rgba(0,0,0,.35)}
|
||||||
|
.cap.down{transform:translateY(2px);box-shadow:0 1px 0 rgba(0,0,0,.35)}
|
||||||
|
.cap.off{opacity:.45}
|
||||||
|
.toggle{display:grid;justify-items:center;gap:6px;background:none;border:0;cursor:pointer;padding:0;color:inherit}
|
||||||
|
.tg{width:22px;height:40px;border-radius:11px;background:var(--bezel);position:relative;box-shadow:inset 0 2px 3px rgba(0,0,0,.6)}
|
||||||
|
.tg::after{content:"";position:absolute;left:4px;width:14px;height:18px;border-radius:7px;background:var(--cap-ink);top:18px;transition:top .12s}
|
||||||
|
.toggle[aria-pressed="true"] .tg::after{top:4px}
|
||||||
|
.hint{font:400 11px/1.6 var(--f-mono);color:var(--muted);margin:12px 0 0}
|
||||||
|
.hint kbd{font:inherit;border:1px solid var(--line);border-radius:3px;padding:0 4px}
|
||||||
|
|
||||||
|
|
||||||
|
/* track buttons under a WS2812 strip */
|
||||||
|
.trk{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:12px 10px;padding:12px 10px 8px;border-radius:8px;background:rgba(0,0,0,.06);box-shadow:inset 0 1px 2px rgba(0,0,0,.15)}
|
||||||
|
.tcell{display:grid;justify-items:center;gap:6px;min-width:0}
|
||||||
|
.tled{width:11px;height:11px;border-radius:50%;background:var(--bezel);box-shadow:inset 0 1px 1px rgba(0,0,0,.6)}
|
||||||
|
.tbtn{width:34px;height:34px;border-radius:50%;border:0;cursor:pointer;touch-action:none;background:radial-gradient(circle at 40% 30%,var(--cap-hi),var(--cap) 70%);box-shadow:0 3px 0 rgba(0,0,0,.35),0 0 0 3px rgba(0,0,0,.12)}
|
||||||
|
.tbtn.down{transform:translateY(2px);box-shadow:0 1px 0 rgba(0,0,0,.35),0 0 0 3px rgba(0,0,0,.12)}
|
||||||
|
.tbtn:focus-visible{outline:2px solid var(--accent);outline-offset:3px}
|
||||||
|
.tname{font:600 9px/1.1 var(--f-mono);letter-spacing:.06em;color:var(--accent);text-align:center;white-space:nowrap}
|
||||||
|
.jacks{display:flex;align-items:center;gap:6px}
|
||||||
|
.jack{width:18px;height:18px;border-radius:50%;background:radial-gradient(circle,var(--bezel) 40%,var(--cap-hi) 44%,var(--cap) 70%)}
|
||||||
|
.jacks .label{margin-right:8px}
|
||||||
|
/* right column */
|
||||||
|
.main{display:grid;gap:18px;min-width:0}
|
||||||
|
.statusbar{display:flex;flex-wrap:wrap;gap:8px 16px;align-items:center;justify-content:space-between}
|
||||||
|
.pill{font:500 11px var(--f-mono);padding:4px 10px;border-radius:999px;border:1px solid var(--line);color:var(--muted);background:var(--surface)}
|
||||||
|
.pill.ok{color:var(--l-kit);border-color:currentColor}
|
||||||
|
.pill.bad{color:var(--warn);border-color:currentColor}
|
||||||
|
.view{background:var(--surface);border:1px solid var(--line);border-radius:10px;padding:14px;display:grid;gap:12px;min-width:0}
|
||||||
|
.facts{display:flex;flex-wrap:wrap;gap:4px 18px;font:500 12px var(--f-mono);color:var(--muted);font-variant-numeric:tabular-nums}
|
||||||
|
.facts b{color:var(--ink);font-weight:500}
|
||||||
|
#grid{display:block;width:100%;height:340px}
|
||||||
|
.voices{display:flex;flex-wrap:wrap;gap:8px;align-items:center}
|
||||||
|
.tracks{display:grid;gap:8px;flex:1;min-width:0}
|
||||||
|
.trow{display:flex;flex-wrap:wrap;gap:6px;align-items:center}
|
||||||
|
.trow .label{width:100%}
|
||||||
|
.vs{display:inline-flex;align-items:center;gap:8px;font:600 12px var(--f-mono);padding:7px 12px;border-radius:6px;cursor:pointer;
|
||||||
|
border:1px solid var(--c);background:var(--c);color:var(--surface)}
|
||||||
|
.vs .st{font-weight:500;opacity:.8;font-size:10px;text-transform:uppercase;letter-spacing:.06em}
|
||||||
|
.vs[aria-pressed="false"]{background:transparent;color:var(--muted);border-color:var(--line)}
|
||||||
|
.vs.pending{outline:2px dashed var(--accent);outline-offset:2px}
|
||||||
|
.check{display:flex;gap:8px;align-items:center;font:500 13px var(--f-ui)}
|
||||||
|
.voices .check{font-size:12px;color:var(--muted)}
|
||||||
|
.voices{align-items:flex-end}
|
||||||
|
details{background:var(--surface);border:1px solid var(--line);border-radius:10px;padding:12px 14px}
|
||||||
|
summary{cursor:pointer;font:600 14px var(--f-ui)}
|
||||||
|
.form{display:grid;grid-template-columns:repeat(auto-fit,minmax(170px,1fr));gap:12px 16px;margin-top:12px}
|
||||||
|
.field{display:grid;gap:4px;min-width:0}
|
||||||
|
.field label{font:500 11px var(--f-mono);color:var(--muted);letter-spacing:.04em}
|
||||||
|
select,input[type=number],input[type=text]{font:500 13px var(--f-mono);color:var(--ink);background:var(--bg);border:1px solid var(--line);border-radius:6px;padding:6px 8px;width:100%;min-width:0}
|
||||||
|
.btn{font:600 12px var(--f-mono);letter-spacing:.04em;border:1px solid var(--line);background:var(--bg);color:var(--ink);border-radius:6px;padding:7px 12px;cursor:pointer}
|
||||||
|
.btn:hover{border-color:var(--accent)}
|
||||||
|
textarea{width:100%;min-height:140px;font:400 11px/1.4 var(--f-mono);background:var(--bg);color:var(--ink);border:1px solid var(--line);border-radius:6px;padding:8px;resize:vertical}
|
||||||
|
.note{color:var(--muted);font-size:13px;margin:10px 0 0;max-width:70ch}
|
||||||
|
|
||||||
|
.padgrid .lbl{font:500 11px var(--f-mono);color:var(--muted)}
|
||||||
|
.padgrid .val.miss{color:var(--warn)}
|
||||||
|
.padgrid{grid-column:1/-1;display:grid;grid-template-columns:repeat(auto-fit,minmax(120px,1fr));gap:10px 12px}
|
||||||
|
.sect{font:600 12px var(--f-mono);color:var(--accent);letter-spacing:.06em;text-transform:uppercase;grid-column:1/-1;margin-top:6px}
|
||||||
|
@media (prefers-reduced-motion:reduce){*{transition:none!important}}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<div class="wrap">
|
||||||
|
<section aria-label="Device">
|
||||||
|
<div class="device">
|
||||||
|
<span class="screw tl"></span><span class="screw tr"></span><span class="screw bl"></span><span class="screw br"></span>
|
||||||
|
<div class="backwall" aria-label="Back wall">
|
||||||
|
<span class="label">back wall</span>
|
||||||
|
<span class="usb"></span><span class="label">USB</span>
|
||||||
|
<span class="bw-spacer"></span>
|
||||||
|
<span class="jack"></span><span class="label">MIDI in</span><span class="jack"></span><span class="label">Kit</span><span class="jack"></span><span class="label">Synth</span>
|
||||||
|
</div>
|
||||||
|
<div class="zones">
|
||||||
|
<div class="zone zl">
|
||||||
|
<div class="dev-head"><span class="dev-name">SEEDBOX</span></div>
|
||||||
|
<div class="oledrow"><div class="oled"><canvas id="oled" width="84" height="48" aria-label="Screen, approximately true size"></canvas></div></div>
|
||||||
|
<div class="zlrow">
|
||||||
|
<div class="ctl"><div class="enc" id="enc" tabindex="0" role="slider" aria-label="Encoder: turn to change page or value, press to edit"><i></i></div><span class="label">Enc · push</span></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="zone zm">
|
||||||
|
<div class="ctl"><div class="knob" id="kEnergy" tabindex="0" role="slider" aria-label="Energy" aria-valuemin="0" aria-valuemax="127"></div><span class="label">Energy <span class="val" id="vEnergy"></span></span></div>
|
||||||
|
<div class="ctl"><div class="knob" id="kDrift" tabindex="0" role="slider" aria-label="Drift" aria-valuemin="0" aria-valuemax="127"></div><span class="label">Drift <span class="val" id="vDrift"></span></span></div>
|
||||||
|
<div class="ctl"><button class="big" id="big" aria-label="New seed">SEED</button></div>
|
||||||
|
</div>
|
||||||
|
<div class="zone zr"><div class="trk" id="trk" aria-label="Track buttons"></div></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<p class="note" style="margin:8px 0 0">Landscape, laid out like the panel drawing: you sit at the bottom edge, cables leave from the back. The screen is drawn at roughly real size.</p>
|
||||||
|
<p class="hint"><b>Track buttons:</b> tap = on ↔ off. Hold = flip it while held (drop a track out, or punch one in).<br>
|
||||||
|
<b>Screen map / LEDs:</b> filled = on, hollow center = hit, empty = off, blinking = change waiting for the bar line. Map rows match the button rows.<br>
|
||||||
|
<b>Encoder pages:</b> SEED history · GEN · ROOT · SCALE (AUTO = the seed picks) · BPM · SLOT (push loads, hold push saves) · ROLL (re-roll kit or line) · LEARN (push, press a kit track, hit the pad).<br>
|
||||||
|
Keys: <kbd>Space</kbd> seed · <kbd>1</kbd>…<kbd>0</kbd> <kbd>-</kbd> tracks · <kbd>←</kbd><kbd>→</kbd> encoder · <kbd>Enter</kbd> push · <kbd>R</kbd> run</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="main">
|
||||||
|
<div>
|
||||||
|
<h1>Seedbox</h1>
|
||||||
|
<p class="sub">A generative rhythm and melody machine built on tracking: one 32-bit seed makes eleven looped tracks for a sampler or drum machine and a synth, and you shape the piece by switching tracks on and off. Each seed carries a chord progression, a melodic motif, a swing amount and a phrase arc, shared by every track. Energy sets how dense the patterns are; drift sets how often a phrase brings a variation.</p>
|
||||||
|
</div>
|
||||||
|
<div class="statusbar">
|
||||||
|
<span class="pill" id="midiStatus">MIDI: not connected</span>
|
||||||
|
<span class="facts" id="pos"></span>
|
||||||
|
</div>
|
||||||
|
<details open>
|
||||||
|
<summary>Hardware</summary>
|
||||||
|
<div class="form">
|
||||||
|
<div class="field"><label for="hwDisplay">Screen</label><select id="hwDisplay">
|
||||||
|
<option value="5110">Nokia 3310/5110 · 84×48 LCD (≈33×19 mm)</option>
|
||||||
|
<option value="091">0.91" OLED · 128×32 (22×6 mm)</option>
|
||||||
|
<option value="096">0.96" OLED · 128×64 (22×11 mm)</option>
|
||||||
|
<option value="130">1.3" OLED · 128×64 (29×15 mm)</option>
|
||||||
|
<option value="242">2.42" OLED · 128×64 (55×27 mm)</option></select></div>
|
||||||
|
<div class="field"><label class="check"><input type="checkbox" id="hwLeds"> LED above each button</label></div>
|
||||||
|
</div>
|
||||||
|
<p class="note" id="hwNote"></p>
|
||||||
|
<div class="zoomwrap"><canvas id="oledZoom" width="84" height="48" aria-label="Screen, magnified"></canvas></div>
|
||||||
|
<p class="note">Magnified, pixel for pixel. The panel on the left shows it at roughly real size (CSS millimetres, so it depends on your monitor).</p>
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<div class="view">
|
||||||
|
<div class="facts" id="facts"></div>
|
||||||
|
<canvas id="grid" aria-label="Current phrase, four bars"></canvas>
|
||||||
|
<label class="check" style="font-size:12px;color:var(--muted)"><input type="checkbox" id="muteQ"> Track changes wait for the next bar line</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<details open>
|
||||||
|
<summary>Routing</summary>
|
||||||
|
<div class="form">
|
||||||
|
<div class="field"><label for="portDrums">Kit output (sampler / drum machine)</label><select id="portDrums"></select></div>
|
||||||
|
<div class="field"><label for="portMono">Synth output (line)</label><select id="portMono"></select></div>
|
||||||
|
<div class="field"><label for="portIn">Kit input (for pad learn)</label><select id="portIn"></select></div>
|
||||||
|
<div class="field"><label class="check"><input type="checkbox" id="sendClock"> Send MIDI clock + start/stop</label></div>
|
||||||
|
<div class="sect">Learned pads <button class="btn" id="clearLearn" type="button" style="margin-left:8px;padding:3px 8px;font-size:10px">Clear learned</button></div>
|
||||||
|
<div id="padFields" class="padgrid"></div>
|
||||||
|
<div class="sect">Synth line</div>
|
||||||
|
<div class="field"><label for="monoCh">Channel</label><input type="number" id="monoCh" min="1" max="16"></div>
|
||||||
|
<div class="field"><label for="monoOct">Octave shift</label><input type="number" id="monoOct" min="-3" max="3"></div>
|
||||||
|
</div>
|
||||||
|
<p class="note">Bass, melody and pad all go to the synth on one channel, so put it in Poly mode. They sit in separate octaves (bass below C3, pad C3–B3, melody from C4) so their notes never collide.</p>
|
||||||
|
<p class="note">Kit tracks play only after they are learned: encoder to LEARN, push, press a track button, hit the pad on the sampler. Unlearned tracks show a dot on the screen.</p>
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary>Port check (golden vectors)</summary>
|
||||||
|
<p class="note">Exports the exact event list for the current seed and settings. The C port on the Pico must reproduce the same list and checksum.</p>
|
||||||
|
<div class="form" style="align-items:end">
|
||||||
|
<div class="field"><label for="vecBars">Bars</label><input type="number" id="vecBars" min="1" max="64" value="8"></div>
|
||||||
|
<div class="field"><button class="btn" id="vecGo">Generate</button></div>
|
||||||
|
<div class="field"><button class="btn" id="vecCopy">Copy</button></div>
|
||||||
|
</div>
|
||||||
|
<textarea id="vecOut" readonly aria-label="Golden vectors JSON"></textarea>
|
||||||
|
</details>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script src="engine.js"></script>
|
||||||
|
|
||||||
|
<script src="app.js"></script>
|
||||||
22
test/cases.js
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
/* The golden-vector case list. Shared by gen-vectors.js and check-vectors.js. */
|
||||||
|
var SEEDS=[0x5EEDB0C5,0x00000001,0xDEADBEEF,0x12345678,0xC0FFEE00,0x0BADF00D];
|
||||||
|
var SETS=[[64,32],[0,0],[127,0],[127,127],[100,90],[40,64]]; // [energy, drift]
|
||||||
|
var BARS=8;
|
||||||
|
function cases(){
|
||||||
|
var out=[];
|
||||||
|
SEEDS.forEach(function(seed){ for(var gen=0;gen<5;gen++) SETS.forEach(function(ed){
|
||||||
|
out.push({seed:seed,salts:[0,0],gen:gen,energy:ed[0],drift:ed[1],fillBars:[],scale:-1,root:-1});
|
||||||
|
}); });
|
||||||
|
// salts (re-rolling one half of the seed) and FILL
|
||||||
|
[0,2,4].forEach(function(gen){
|
||||||
|
out.push({seed:0x5EEDB0C5,salts:[3,0],gen:gen,energy:64,drift:32,fillBars:[],scale:-1,root:-1});
|
||||||
|
out.push({seed:0x5EEDB0C5,salts:[0,5],gen:gen,energy:64,drift:32,fillBars:[],scale:-1,root:-1});
|
||||||
|
out.push({seed:0x5EEDB0C5,salts:[0,0],gen:gen,energy:80,drift:50,fillBars:[3,7],scale:-1,root:-1});
|
||||||
|
});
|
||||||
|
// ROOT / SCALE pages: [seed, gen, scale, root]; -1 = AUTO. Covers 5-, 6-, 7- and 8-note scales and each override alone.
|
||||||
|
[[0x5EEDB0C5,0,14,2],[0x5EEDB0C5,2,19,9],[0x5EEDB0C5,4,0,7],[0xDEADBEEF,1,18,-1],[0x12345678,3,-1,5],[0xC0FFEE00,0,7,11],[0x00000001,1,20,0]].forEach(function(k){
|
||||||
|
out.push({seed:k[0],salts:[0,0],gen:k[1],energy:64,drift:32,fillBars:[3],scale:k[2],root:k[3]});
|
||||||
|
});
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
module.exports={cases:cases,BARS:BARS};
|
||||||
21
test/check-vectors.js
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
/* Re-renders every golden case with sim/engine.js and compares with test/vectors.
|
||||||
|
Exit code 0 = engine unchanged. Use the same files to test the C port. */
|
||||||
|
var fs=require('fs'), path=require('path');
|
||||||
|
var E=require('../sim/engine.js'), R=require('./render.js');
|
||||||
|
var dir=path.join(__dirname,'vectors'), fail=0, n=0;
|
||||||
|
var prim=JSON.parse(fs.readFileSync(path.join(dir,'primitives.json'),'utf8'));
|
||||||
|
prim.mix.forEach(function(v){ n++; if(R.hex(E.mix(parseInt(v.a,16)>>>0,v.b))!==v.out){ fail++; console.log('mix',v.a,v.b,'differs'); } });
|
||||||
|
prim.rng.forEach(function(v){ n++; var r=new E.Rng(parseInt(v.seed,16)>>>0); for(var i=0;i<16;i++) if(R.hex(r.next())!==v.next[i]){ fail++; console.log('rng',v.seed,'step',i,'differs'); break; } });
|
||||||
|
prim.genomes.forEach(function(v){ n++; if(JSON.stringify(E.genome(parseInt(v.seed,16)>>>0))!==JSON.stringify(v.genome)){ fail++; console.log('genome',v.seed,'differs'); } });
|
||||||
|
fs.readdirSync(dir).filter(function(f){ return /^gen\d/.test(f); }).sort().forEach(function(f){
|
||||||
|
JSON.parse(fs.readFileSync(path.join(dir,f),'utf8')).cases.forEach(function(c){
|
||||||
|
n++;
|
||||||
|
var got=R.renderCase({seed:parseInt(c.seed,16)>>>0,salts:c.salts,gen:c.gen,energy:c.energy,drift:c.drift,fillBars:c.fillBars,scale:c.scale,root:c.root},c.bars.length);
|
||||||
|
if(got.checksum===c.checksum) return;
|
||||||
|
fail++;
|
||||||
|
for(var b=0;b<c.bars.length;b++){ var a=JSON.stringify(c.bars[b].events), g=JSON.stringify(got.bars[b].events);
|
||||||
|
if(a!==g){ console.log('FAIL',c.id,'first difference in bar',b); break; } }
|
||||||
|
});
|
||||||
|
});
|
||||||
|
console.log(fail? fail+' of '+n+' checks FAILED' : 'all '+n+' checks pass');
|
||||||
|
process.exit(fail?1:0);
|
||||||
34
test/gen-vectors.js
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
/* Regenerates test/vectors/*.json from sim/engine.js.
|
||||||
|
Only run this when you MEAN to change the music. Otherwise run check-vectors.js. */
|
||||||
|
var fs=require('fs'), path=require('path');
|
||||||
|
var E=require('../sim/engine.js'), C=require('./cases.js'), R=require('./render.js');
|
||||||
|
var dir=path.join(__dirname,'vectors');
|
||||||
|
var FORMAT='events: [tick, lane, voice, vel, len, track]. lane 0 = kit (voice 0 kick, 1 snare, 2 hat, 3 perc), lane 1 = synth (voice = MIDI pitch). scale/root: -1 = AUTO (the seed picks). Every track is listed; on/off is applied at output. Ticks: 840 per quarter, 210 per 16th.';
|
||||||
|
|
||||||
|
// 1. primitives: hash, RNG, genome. Port these first.
|
||||||
|
var mixV=[], pairs=[[0,0],[1,0],[0,1],[0x5EEDB0C5,0x46494C],[0xFFFFFFFF,0xFFFFFFFF],[0xDEADBEEF,-1],[12345,0x4E55+7]];
|
||||||
|
pairs.forEach(function(p){ mixV.push({a:R.hex(p[0]>>>0),b:p[1]|0,out:R.hex(E.mix(p[0]>>>0,p[1]))}); });
|
||||||
|
var rngV=[]; [0,1,0x5EEDB0C5,0xDEADBEEF].forEach(function(s){ var r=new E.Rng(s), seq=[], below=[], ch=[];
|
||||||
|
for(var i=0;i<16;i++) seq.push(R.hex(r.next()));
|
||||||
|
r=new E.Rng(s); for(i=0;i<16;i++) below.push(r.below(i+1));
|
||||||
|
r=new E.Rng(s); for(i=0;i<16;i++) ch.push(r.chance(i*4096)?1:0);
|
||||||
|
rngV.push({seed:R.hex(s>>>0),next:seq,below_1_to_16:below,chance_i_times_4096:ch}); });
|
||||||
|
var genV=C.cases().filter(function(c,i,a){ return a.findIndex(function(d){ return d.seed===c.seed; })===i; })
|
||||||
|
.map(function(c){ return {seed:R.hex(c.seed),genome:E.genome(c.seed>>>0)}; });
|
||||||
|
fs.writeFileSync(path.join(dir,'primitives.json'),JSON.stringify({note:'mix(a,b) hash, xorshift32 Rng (next, below(n), chance(p)) and genome(seed). genome keys are the JS field names.',mix:mixV,rng:rngV,genomes:genV},null,1)+'\n');
|
||||||
|
|
||||||
|
// 2. full renders, one file per generator
|
||||||
|
var byGen={}, index=[];
|
||||||
|
C.cases().forEach(function(c){
|
||||||
|
var out=R.renderCase(c,C.BARS), id=R.caseId(c);
|
||||||
|
(byGen[c.gen]=byGen[c.gen]||[]).push({id:id,seed:R.hex(c.seed),salts:c.salts,gen:c.gen,energy:c.energy,drift:c.drift,fillBars:c.fillBars,scale:c.scale,root:c.root,checksum:out.checksum,bars:out.bars.map(function(b){
|
||||||
|
return {bar:b.bar,sec:b.sec,fill:b.fill,barTicks:b.barTicks,events:b.events}; })});
|
||||||
|
index.push({id:id,checksum:out.checksum});
|
||||||
|
});
|
||||||
|
Object.keys(byGen).forEach(function(g){
|
||||||
|
var s=JSON.stringify({generator:E.GEN_NAMES[g],format:FORMAT,cases:byGen[g]});
|
||||||
|
s=s.replace(/\],\[/g,'],\n['); // one event per line keeps git diffs readable
|
||||||
|
fs.writeFileSync(path.join(dir,'gen'+g+'-'+E.GEN_NAMES[g].toLowerCase()+'.json'),s+'\n');
|
||||||
|
});
|
||||||
|
fs.writeFileSync(path.join(dir,'index.json'),JSON.stringify({bars:C.BARS,cases:index},null,1)+'\n');
|
||||||
|
console.log('wrote',index.length,'cases');
|
||||||
14
test/render.js
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
/* Renders one case to rows [tick,lane,voice,vel,len,track] per bar, plus a whole-case checksum. */
|
||||||
|
var E=require('../sim/engine.js');
|
||||||
|
function hex(n){ return '0x'+('00000000'+(n>>>0).toString(16).toUpperCase()).slice(-8); }
|
||||||
|
function renderCase(c,bars){
|
||||||
|
var st={seed:c.seed>>>0,salts:c.salts.slice()}, all=[], out=[];
|
||||||
|
for(var b=0;b<bars;b++){
|
||||||
|
var r=E.renderBar(st,{gen:c.gen,energy:c.energy,drift:c.drift,fill:c.fillBars.indexOf(b)>=0,scale:c.scale,root:c.root},b);
|
||||||
|
var ev=r.events.map(function(e){ return [e.t,e.lane,e.v,e.vel,e.len,e.tr]; });
|
||||||
|
out.push({bar:b,sec:r.sec,fill:!!r.fill,barTicks:r.barTicks,events:ev}); all=all.concat(ev);
|
||||||
|
}
|
||||||
|
return {bars:out,checksum:hex(E.checksum(all))};
|
||||||
|
}
|
||||||
|
function caseId(c){ return hex(c.seed).slice(2)+'_s'+c.salts.join('-')+'_g'+c.gen+'_e'+c.energy+'_d'+c.drift+(c.fillBars.length?'_f'+c.fillBars.join('-'):'')+(c.scale>=0?'_sc'+c.scale:'')+(c.root>=0?'_r'+c.root:''); }
|
||||||
|
module.exports={renderCase:renderCase,caseId:caseId,hex:hex};
|
||||||
9356
test/vectors/gen0-euclid.json
Normal file
9247
test/vectors/gen1-skeleton.json
Normal file
8028
test/vectors/gen2-subdiv.json
Normal file
8431
test/vectors/gen3-automata.json
Normal file
9407
test/vectors/gen4-amenizer.json
Normal file
789
test/vectors/index.json
Normal file
@@ -0,0 +1,789 @@
|
|||||||
|
{
|
||||||
|
"bars": 8,
|
||||||
|
"cases": [
|
||||||
|
{
|
||||||
|
"id": "5EEDB0C5_s0-0_g0_e64_d32",
|
||||||
|
"checksum": "0x4F5DE957"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "5EEDB0C5_s0-0_g0_e0_d0",
|
||||||
|
"checksum": "0x21423F8D"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "5EEDB0C5_s0-0_g0_e127_d0",
|
||||||
|
"checksum": "0x047B79A5"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "5EEDB0C5_s0-0_g0_e127_d127",
|
||||||
|
"checksum": "0x706D45DF"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "5EEDB0C5_s0-0_g0_e100_d90",
|
||||||
|
"checksum": "0x514A841A"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "5EEDB0C5_s0-0_g0_e40_d64",
|
||||||
|
"checksum": "0xD7D4FEF1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "5EEDB0C5_s0-0_g1_e64_d32",
|
||||||
|
"checksum": "0x4175DD2C"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "5EEDB0C5_s0-0_g1_e0_d0",
|
||||||
|
"checksum": "0xC8BE2789"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "5EEDB0C5_s0-0_g1_e127_d0",
|
||||||
|
"checksum": "0xFBB77BE9"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "5EEDB0C5_s0-0_g1_e127_d127",
|
||||||
|
"checksum": "0xEFC24FAA"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "5EEDB0C5_s0-0_g1_e100_d90",
|
||||||
|
"checksum": "0x4410BA5C"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "5EEDB0C5_s0-0_g1_e40_d64",
|
||||||
|
"checksum": "0xA6DCEC5A"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "5EEDB0C5_s0-0_g2_e64_d32",
|
||||||
|
"checksum": "0x03978773"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "5EEDB0C5_s0-0_g2_e0_d0",
|
||||||
|
"checksum": "0x2381EB35"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "5EEDB0C5_s0-0_g2_e127_d0",
|
||||||
|
"checksum": "0x2F3A4EB9"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "5EEDB0C5_s0-0_g2_e127_d127",
|
||||||
|
"checksum": "0xA21D7C70"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "5EEDB0C5_s0-0_g2_e100_d90",
|
||||||
|
"checksum": "0x56AEEC48"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "5EEDB0C5_s0-0_g2_e40_d64",
|
||||||
|
"checksum": "0xB8F4407B"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "5EEDB0C5_s0-0_g3_e64_d32",
|
||||||
|
"checksum": "0x55C2ED60"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "5EEDB0C5_s0-0_g3_e0_d0",
|
||||||
|
"checksum": "0xD5FD1E2D"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "5EEDB0C5_s0-0_g3_e127_d0",
|
||||||
|
"checksum": "0x29C313A5"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "5EEDB0C5_s0-0_g3_e127_d127",
|
||||||
|
"checksum": "0x31BFD6A0"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "5EEDB0C5_s0-0_g3_e100_d90",
|
||||||
|
"checksum": "0x626428CC"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "5EEDB0C5_s0-0_g3_e40_d64",
|
||||||
|
"checksum": "0xF53B86E4"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "5EEDB0C5_s0-0_g4_e64_d32",
|
||||||
|
"checksum": "0x8C01C1F9"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "5EEDB0C5_s0-0_g4_e0_d0",
|
||||||
|
"checksum": "0x49195CD6"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "5EEDB0C5_s0-0_g4_e127_d0",
|
||||||
|
"checksum": "0x2B77305E"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "5EEDB0C5_s0-0_g4_e127_d127",
|
||||||
|
"checksum": "0xBCE32A21"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "5EEDB0C5_s0-0_g4_e100_d90",
|
||||||
|
"checksum": "0x049DE447"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "5EEDB0C5_s0-0_g4_e40_d64",
|
||||||
|
"checksum": "0x19D81B14"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "00000001_s0-0_g0_e64_d32",
|
||||||
|
"checksum": "0x1B668BA7"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "00000001_s0-0_g0_e0_d0",
|
||||||
|
"checksum": "0xB317F8A3"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "00000001_s0-0_g0_e127_d0",
|
||||||
|
"checksum": "0xB8A7A475"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "00000001_s0-0_g0_e127_d127",
|
||||||
|
"checksum": "0xFD32AF12"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "00000001_s0-0_g0_e100_d90",
|
||||||
|
"checksum": "0x2A4AF905"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "00000001_s0-0_g0_e40_d64",
|
||||||
|
"checksum": "0x261DB9CA"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "00000001_s0-0_g1_e64_d32",
|
||||||
|
"checksum": "0x963CE4D1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "00000001_s0-0_g1_e0_d0",
|
||||||
|
"checksum": "0xADEFE751"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "00000001_s0-0_g1_e127_d0",
|
||||||
|
"checksum": "0xE7CAE585"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "00000001_s0-0_g1_e127_d127",
|
||||||
|
"checksum": "0x2C0DC228"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "00000001_s0-0_g1_e100_d90",
|
||||||
|
"checksum": "0xA7173DAC"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "00000001_s0-0_g1_e40_d64",
|
||||||
|
"checksum": "0x3135C017"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "00000001_s0-0_g2_e64_d32",
|
||||||
|
"checksum": "0xFD1D01E5"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "00000001_s0-0_g2_e0_d0",
|
||||||
|
"checksum": "0xE9837637"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "00000001_s0-0_g2_e127_d0",
|
||||||
|
"checksum": "0x9D751F13"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "00000001_s0-0_g2_e127_d127",
|
||||||
|
"checksum": "0x2DCF4824"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "00000001_s0-0_g2_e100_d90",
|
||||||
|
"checksum": "0xF0ED3D0F"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "00000001_s0-0_g2_e40_d64",
|
||||||
|
"checksum": "0x370EE4B9"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "00000001_s0-0_g3_e64_d32",
|
||||||
|
"checksum": "0x403E0B96"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "00000001_s0-0_g3_e0_d0",
|
||||||
|
"checksum": "0x24DCABC8"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "00000001_s0-0_g3_e127_d0",
|
||||||
|
"checksum": "0x2800F8EA"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "00000001_s0-0_g3_e127_d127",
|
||||||
|
"checksum": "0xC62FB65A"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "00000001_s0-0_g3_e100_d90",
|
||||||
|
"checksum": "0xFED1503D"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "00000001_s0-0_g3_e40_d64",
|
||||||
|
"checksum": "0x43796A8B"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "00000001_s0-0_g4_e64_d32",
|
||||||
|
"checksum": "0x9AAECD27"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "00000001_s0-0_g4_e0_d0",
|
||||||
|
"checksum": "0xF5408301"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "00000001_s0-0_g4_e127_d0",
|
||||||
|
"checksum": "0x0438ABF1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "00000001_s0-0_g4_e127_d127",
|
||||||
|
"checksum": "0x54D1C6C0"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "00000001_s0-0_g4_e100_d90",
|
||||||
|
"checksum": "0xC73622DC"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "00000001_s0-0_g4_e40_d64",
|
||||||
|
"checksum": "0x29952EAD"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "DEADBEEF_s0-0_g0_e64_d32",
|
||||||
|
"checksum": "0x797AF814"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "DEADBEEF_s0-0_g0_e0_d0",
|
||||||
|
"checksum": "0xC2D63031"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "DEADBEEF_s0-0_g0_e127_d0",
|
||||||
|
"checksum": "0xEC6B6DC5"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "DEADBEEF_s0-0_g0_e127_d127",
|
||||||
|
"checksum": "0x3D0EE1E3"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "DEADBEEF_s0-0_g0_e100_d90",
|
||||||
|
"checksum": "0xB515BF46"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "DEADBEEF_s0-0_g0_e40_d64",
|
||||||
|
"checksum": "0x9C91B1FE"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "DEADBEEF_s0-0_g1_e64_d32",
|
||||||
|
"checksum": "0x756DCDCF"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "DEADBEEF_s0-0_g1_e0_d0",
|
||||||
|
"checksum": "0xB507F6C5"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "DEADBEEF_s0-0_g1_e127_d0",
|
||||||
|
"checksum": "0x88872B09"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "DEADBEEF_s0-0_g1_e127_d127",
|
||||||
|
"checksum": "0x8B392FD7"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "DEADBEEF_s0-0_g1_e100_d90",
|
||||||
|
"checksum": "0x027972EC"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "DEADBEEF_s0-0_g1_e40_d64",
|
||||||
|
"checksum": "0x145A52B4"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "DEADBEEF_s0-0_g2_e64_d32",
|
||||||
|
"checksum": "0x8C44AE71"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "DEADBEEF_s0-0_g2_e0_d0",
|
||||||
|
"checksum": "0xC565B151"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "DEADBEEF_s0-0_g2_e127_d0",
|
||||||
|
"checksum": "0x50688071"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "DEADBEEF_s0-0_g2_e127_d127",
|
||||||
|
"checksum": "0xB394F949"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "DEADBEEF_s0-0_g2_e100_d90",
|
||||||
|
"checksum": "0x99546B5F"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "DEADBEEF_s0-0_g2_e40_d64",
|
||||||
|
"checksum": "0x01377919"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "DEADBEEF_s0-0_g3_e64_d32",
|
||||||
|
"checksum": "0xDB5C3174"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "DEADBEEF_s0-0_g3_e0_d0",
|
||||||
|
"checksum": "0x793380FD"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "DEADBEEF_s0-0_g3_e127_d0",
|
||||||
|
"checksum": "0xDF07368D"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "DEADBEEF_s0-0_g3_e127_d127",
|
||||||
|
"checksum": "0x1BF6126D"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "DEADBEEF_s0-0_g3_e100_d90",
|
||||||
|
"checksum": "0x846CD40B"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "DEADBEEF_s0-0_g3_e40_d64",
|
||||||
|
"checksum": "0x3EF11E79"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "DEADBEEF_s0-0_g4_e64_d32",
|
||||||
|
"checksum": "0x3358C89B"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "DEADBEEF_s0-0_g4_e0_d0",
|
||||||
|
"checksum": "0xD03B2AF3"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "DEADBEEF_s0-0_g4_e127_d0",
|
||||||
|
"checksum": "0x213A658F"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "DEADBEEF_s0-0_g4_e127_d127",
|
||||||
|
"checksum": "0x6C4F5E2A"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "DEADBEEF_s0-0_g4_e100_d90",
|
||||||
|
"checksum": "0x181E5284"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "DEADBEEF_s0-0_g4_e40_d64",
|
||||||
|
"checksum": "0x1BCB4282"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "12345678_s0-0_g0_e64_d32",
|
||||||
|
"checksum": "0x5A4F3C08"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "12345678_s0-0_g0_e0_d0",
|
||||||
|
"checksum": "0x331E7A09"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "12345678_s0-0_g0_e127_d0",
|
||||||
|
"checksum": "0xA3674ADD"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "12345678_s0-0_g0_e127_d127",
|
||||||
|
"checksum": "0x83884A89"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "12345678_s0-0_g0_e100_d90",
|
||||||
|
"checksum": "0x130C35C4"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "12345678_s0-0_g0_e40_d64",
|
||||||
|
"checksum": "0xFE2109A6"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "12345678_s0-0_g1_e64_d32",
|
||||||
|
"checksum": "0x8B245E0C"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "12345678_s0-0_g1_e0_d0",
|
||||||
|
"checksum": "0x3E84A555"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "12345678_s0-0_g1_e127_d0",
|
||||||
|
"checksum": "0xAAB67769"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "12345678_s0-0_g1_e127_d127",
|
||||||
|
"checksum": "0x3DD0B079"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "12345678_s0-0_g1_e100_d90",
|
||||||
|
"checksum": "0x88F591F9"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "12345678_s0-0_g1_e40_d64",
|
||||||
|
"checksum": "0xEF44FDC2"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "12345678_s0-0_g2_e64_d32",
|
||||||
|
"checksum": "0x6D0725F2"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "12345678_s0-0_g2_e0_d0",
|
||||||
|
"checksum": "0x8C33A579"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "12345678_s0-0_g2_e127_d0",
|
||||||
|
"checksum": "0x8FFBCA05"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "12345678_s0-0_g2_e127_d127",
|
||||||
|
"checksum": "0x18819E92"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "12345678_s0-0_g2_e100_d90",
|
||||||
|
"checksum": "0x59F8D5BB"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "12345678_s0-0_g2_e40_d64",
|
||||||
|
"checksum": "0xC682051E"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "12345678_s0-0_g3_e64_d32",
|
||||||
|
"checksum": "0xD0EA040D"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "12345678_s0-0_g3_e0_d0",
|
||||||
|
"checksum": "0xA70CFBDD"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "12345678_s0-0_g3_e127_d0",
|
||||||
|
"checksum": "0x3535E98D"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "12345678_s0-0_g3_e127_d127",
|
||||||
|
"checksum": "0x16A8AB23"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "12345678_s0-0_g3_e100_d90",
|
||||||
|
"checksum": "0xAFF8C3A6"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "12345678_s0-0_g3_e40_d64",
|
||||||
|
"checksum": "0x8F9EF374"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "12345678_s0-0_g4_e64_d32",
|
||||||
|
"checksum": "0x97C66D44"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "12345678_s0-0_g4_e0_d0",
|
||||||
|
"checksum": "0xB8EA077E"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "12345678_s0-0_g4_e127_d0",
|
||||||
|
"checksum": "0x92FE51BE"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "12345678_s0-0_g4_e127_d127",
|
||||||
|
"checksum": "0xD9E2F70E"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "12345678_s0-0_g4_e100_d90",
|
||||||
|
"checksum": "0x882625B9"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "12345678_s0-0_g4_e40_d64",
|
||||||
|
"checksum": "0xC26462BD"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "C0FFEE00_s0-0_g0_e64_d32",
|
||||||
|
"checksum": "0x409C226C"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "C0FFEE00_s0-0_g0_e0_d0",
|
||||||
|
"checksum": "0x6DC8B525"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "C0FFEE00_s0-0_g0_e127_d0",
|
||||||
|
"checksum": "0xEED3EB25"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "C0FFEE00_s0-0_g0_e127_d127",
|
||||||
|
"checksum": "0x2A4D1330"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "C0FFEE00_s0-0_g0_e100_d90",
|
||||||
|
"checksum": "0x81D06CC9"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "C0FFEE00_s0-0_g0_e40_d64",
|
||||||
|
"checksum": "0xB02166B8"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "C0FFEE00_s0-0_g1_e64_d32",
|
||||||
|
"checksum": "0x9D21C166"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "C0FFEE00_s0-0_g1_e0_d0",
|
||||||
|
"checksum": "0xFEFB871D"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "C0FFEE00_s0-0_g1_e127_d0",
|
||||||
|
"checksum": "0x70E58385"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "C0FFEE00_s0-0_g1_e127_d127",
|
||||||
|
"checksum": "0xBB8654C4"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "C0FFEE00_s0-0_g1_e100_d90",
|
||||||
|
"checksum": "0x2854DE5D"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "C0FFEE00_s0-0_g1_e40_d64",
|
||||||
|
"checksum": "0x67A2217F"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "C0FFEE00_s0-0_g2_e64_d32",
|
||||||
|
"checksum": "0x7BEE9F19"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "C0FFEE00_s0-0_g2_e0_d0",
|
||||||
|
"checksum": "0x7B8332C9"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "C0FFEE00_s0-0_g2_e127_d0",
|
||||||
|
"checksum": "0x65B3D6C5"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "C0FFEE00_s0-0_g2_e127_d127",
|
||||||
|
"checksum": "0x232BB94B"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "C0FFEE00_s0-0_g2_e100_d90",
|
||||||
|
"checksum": "0x8A7EE028"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "C0FFEE00_s0-0_g2_e40_d64",
|
||||||
|
"checksum": "0xE8731798"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "C0FFEE00_s0-0_g3_e64_d32",
|
||||||
|
"checksum": "0xBA4FDBDC"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "C0FFEE00_s0-0_g3_e0_d0",
|
||||||
|
"checksum": "0x29BA4BB5"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "C0FFEE00_s0-0_g3_e127_d0",
|
||||||
|
"checksum": "0x84A8ADFD"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "C0FFEE00_s0-0_g3_e127_d127",
|
||||||
|
"checksum": "0x98E5C8DE"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "C0FFEE00_s0-0_g3_e100_d90",
|
||||||
|
"checksum": "0xC2C59DE5"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "C0FFEE00_s0-0_g3_e40_d64",
|
||||||
|
"checksum": "0x54D6D8F3"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "C0FFEE00_s0-0_g4_e64_d32",
|
||||||
|
"checksum": "0x477C2799"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "C0FFEE00_s0-0_g4_e0_d0",
|
||||||
|
"checksum": "0xF057EBE5"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "C0FFEE00_s0-0_g4_e127_d0",
|
||||||
|
"checksum": "0xBF2A4C45"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "C0FFEE00_s0-0_g4_e127_d127",
|
||||||
|
"checksum": "0x699081A3"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "C0FFEE00_s0-0_g4_e100_d90",
|
||||||
|
"checksum": "0x549E5CBC"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "C0FFEE00_s0-0_g4_e40_d64",
|
||||||
|
"checksum": "0x58BA5FEF"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "0BADF00D_s0-0_g0_e64_d32",
|
||||||
|
"checksum": "0x954515B3"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "0BADF00D_s0-0_g0_e0_d0",
|
||||||
|
"checksum": "0xECD6B65D"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "0BADF00D_s0-0_g0_e127_d0",
|
||||||
|
"checksum": "0x320DFCCD"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "0BADF00D_s0-0_g0_e127_d127",
|
||||||
|
"checksum": "0x9136FE16"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "0BADF00D_s0-0_g0_e100_d90",
|
||||||
|
"checksum": "0xBFE1652E"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "0BADF00D_s0-0_g0_e40_d64",
|
||||||
|
"checksum": "0xB06D7C1B"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "0BADF00D_s0-0_g1_e64_d32",
|
||||||
|
"checksum": "0x95EA2C4A"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "0BADF00D_s0-0_g1_e0_d0",
|
||||||
|
"checksum": "0xACE809ED"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "0BADF00D_s0-0_g1_e127_d0",
|
||||||
|
"checksum": "0x505897A5"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "0BADF00D_s0-0_g1_e127_d127",
|
||||||
|
"checksum": "0x18BB7F1F"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "0BADF00D_s0-0_g1_e100_d90",
|
||||||
|
"checksum": "0xAF303405"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "0BADF00D_s0-0_g1_e40_d64",
|
||||||
|
"checksum": "0x83D7E35B"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "0BADF00D_s0-0_g2_e64_d32",
|
||||||
|
"checksum": "0x1D9A1E48"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "0BADF00D_s0-0_g2_e0_d0",
|
||||||
|
"checksum": "0xB506D305"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "0BADF00D_s0-0_g2_e127_d0",
|
||||||
|
"checksum": "0x00B1BB95"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "0BADF00D_s0-0_g2_e127_d127",
|
||||||
|
"checksum": "0xDE2D6A59"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "0BADF00D_s0-0_g2_e100_d90",
|
||||||
|
"checksum": "0x12258F4F"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "0BADF00D_s0-0_g2_e40_d64",
|
||||||
|
"checksum": "0xBE863461"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "0BADF00D_s0-0_g3_e64_d32",
|
||||||
|
"checksum": "0x8C83C9BB"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "0BADF00D_s0-0_g3_e0_d0",
|
||||||
|
"checksum": "0x424BC27D"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "0BADF00D_s0-0_g3_e127_d0",
|
||||||
|
"checksum": "0x6D55EA75"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "0BADF00D_s0-0_g3_e127_d127",
|
||||||
|
"checksum": "0xD0A8C249"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "0BADF00D_s0-0_g3_e100_d90",
|
||||||
|
"checksum": "0xF414BA93"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "0BADF00D_s0-0_g3_e40_d64",
|
||||||
|
"checksum": "0x73277AF0"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "0BADF00D_s0-0_g4_e64_d32",
|
||||||
|
"checksum": "0xF8E95F3F"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "0BADF00D_s0-0_g4_e0_d0",
|
||||||
|
"checksum": "0x73E27C78"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "0BADF00D_s0-0_g4_e127_d0",
|
||||||
|
"checksum": "0x4907E37C"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "0BADF00D_s0-0_g4_e127_d127",
|
||||||
|
"checksum": "0xA7C479CA"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "0BADF00D_s0-0_g4_e100_d90",
|
||||||
|
"checksum": "0xF8EA9EA7"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "0BADF00D_s0-0_g4_e40_d64",
|
||||||
|
"checksum": "0xCC25D6B6"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "5EEDB0C5_s3-0_g0_e64_d32",
|
||||||
|
"checksum": "0x4BC1C255"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "5EEDB0C5_s0-5_g0_e64_d32",
|
||||||
|
"checksum": "0x1D608FBA"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "5EEDB0C5_s0-0_g0_e80_d50_f3-7",
|
||||||
|
"checksum": "0xF613C7F0"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "5EEDB0C5_s3-0_g2_e64_d32",
|
||||||
|
"checksum": "0xCDA54E56"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "5EEDB0C5_s0-5_g2_e64_d32",
|
||||||
|
"checksum": "0xCD7A1CF7"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "5EEDB0C5_s0-0_g2_e80_d50_f3-7",
|
||||||
|
"checksum": "0x13BCBDA8"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "5EEDB0C5_s3-0_g4_e64_d32",
|
||||||
|
"checksum": "0x13CF5E5E"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "5EEDB0C5_s0-5_g4_e64_d32",
|
||||||
|
"checksum": "0x5E1C452C"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "5EEDB0C5_s0-0_g4_e80_d50_f3-7",
|
||||||
|
"checksum": "0xFFD1BA41"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "5EEDB0C5_s0-0_g0_e64_d32_f3_sc14_r2",
|
||||||
|
"checksum": "0x1F87B79B"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "5EEDB0C5_s0-0_g2_e64_d32_f3_sc19_r9",
|
||||||
|
"checksum": "0x186EFC62"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "5EEDB0C5_s0-0_g4_e64_d32_f3_sc0_r7",
|
||||||
|
"checksum": "0x6A3A6680"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "DEADBEEF_s0-0_g1_e64_d32_f3_sc18",
|
||||||
|
"checksum": "0x7A5704D3"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "12345678_s0-0_g3_e64_d32_f3_r5",
|
||||||
|
"checksum": "0x6D812076"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "C0FFEE00_s0-0_g0_e64_d32_f3_sc7_r11",
|
||||||
|
"checksum": "0x28DFCADD"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "00000001_s0-0_g1_e64_d32_f3_sc20_r0",
|
||||||
|
"checksum": "0x02ADD2DB"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
506
test/vectors/primitives.json
Normal file
@@ -0,0 +1,506 @@
|
|||||||
|
{
|
||||||
|
"note": "mix(a,b) hash, xorshift32 Rng (next, below(n), chance(p)) and genome(seed). genome keys are the JS field names.",
|
||||||
|
"mix": [
|
||||||
|
{
|
||||||
|
"a": "0x00000000",
|
||||||
|
"b": 0,
|
||||||
|
"out": "0x6D2B79F5"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"a": "0x00000001",
|
||||||
|
"b": 0,
|
||||||
|
"out": "0x514E28B7"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"a": "0x00000000",
|
||||||
|
"b": 1,
|
||||||
|
"out": "0x11FD02EB"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"a": "0x5EEDB0C5",
|
||||||
|
"b": 4606284,
|
||||||
|
"out": "0x5E77CB5F"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"a": "0xFFFFFFFF",
|
||||||
|
"b": -1,
|
||||||
|
"out": "0xB19FEA7C"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"a": "0xDEADBEEF",
|
||||||
|
"b": -1,
|
||||||
|
"out": "0x95CB1F45"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"a": "0x00003039",
|
||||||
|
"b": 20060,
|
||||||
|
"out": "0x34949A58"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"rng": [
|
||||||
|
{
|
||||||
|
"seed": "0x00000000",
|
||||||
|
"next": [
|
||||||
|
"0x40AEC71F",
|
||||||
|
"0x91E00C19",
|
||||||
|
"0x9C0FE128",
|
||||||
|
"0x6570F69D",
|
||||||
|
"0x0FCE02CC",
|
||||||
|
"0x3D6B45E7",
|
||||||
|
"0xEF266E8C",
|
||||||
|
"0x7C080117",
|
||||||
|
"0xF9713F42",
|
||||||
|
"0x0DBB11CE",
|
||||||
|
"0x9FDE27EF",
|
||||||
|
"0x3F5EA5BE",
|
||||||
|
"0x96CB190A",
|
||||||
|
"0x48AE5C1F",
|
||||||
|
"0xE6924CA9",
|
||||||
|
"0x4FE05E6A"
|
||||||
|
],
|
||||||
|
"below_1_to_16": [
|
||||||
|
0,
|
||||||
|
1,
|
||||||
|
1,
|
||||||
|
1,
|
||||||
|
0,
|
||||||
|
1,
|
||||||
|
6,
|
||||||
|
3,
|
||||||
|
8,
|
||||||
|
0,
|
||||||
|
6,
|
||||||
|
2,
|
||||||
|
7,
|
||||||
|
3,
|
||||||
|
13,
|
||||||
|
4
|
||||||
|
],
|
||||||
|
"chance_i_times_4096": [
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
1,
|
||||||
|
1,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
1,
|
||||||
|
1,
|
||||||
|
1,
|
||||||
|
1,
|
||||||
|
1,
|
||||||
|
0,
|
||||||
|
1
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"seed": "0x00000001",
|
||||||
|
"next": [
|
||||||
|
"0x00042021",
|
||||||
|
"0x04080601",
|
||||||
|
"0x9DCCA8C5",
|
||||||
|
"0x1255994F",
|
||||||
|
"0x8EF917D1",
|
||||||
|
"0x2C6F5BD0",
|
||||||
|
"0x25B2331A",
|
||||||
|
"0x19F91CB2",
|
||||||
|
"0x77877125",
|
||||||
|
"0xADD02374",
|
||||||
|
"0x9E6002CB",
|
||||||
|
"0x591C9737",
|
||||||
|
"0xB4B84B8A",
|
||||||
|
"0x04E3F8AE",
|
||||||
|
"0x0536AFF5",
|
||||||
|
"0xC9C495B1"
|
||||||
|
],
|
||||||
|
"below_1_to_16": [
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
1,
|
||||||
|
0,
|
||||||
|
2,
|
||||||
|
1,
|
||||||
|
1,
|
||||||
|
0,
|
||||||
|
4,
|
||||||
|
6,
|
||||||
|
6,
|
||||||
|
4,
|
||||||
|
9,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
12
|
||||||
|
],
|
||||||
|
"chance_i_times_4096": [
|
||||||
|
0,
|
||||||
|
1,
|
||||||
|
0,
|
||||||
|
1,
|
||||||
|
0,
|
||||||
|
1,
|
||||||
|
1,
|
||||||
|
1,
|
||||||
|
1,
|
||||||
|
0,
|
||||||
|
1,
|
||||||
|
1,
|
||||||
|
1,
|
||||||
|
1,
|
||||||
|
1,
|
||||||
|
1
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"seed": "0x5EEDB0C5",
|
||||||
|
"next": [
|
||||||
|
"0xF659F35F",
|
||||||
|
"0xCE7C9F86",
|
||||||
|
"0xEC025940",
|
||||||
|
"0x426B5075",
|
||||||
|
"0x24D96CA7",
|
||||||
|
"0x20FC8821",
|
||||||
|
"0x8EE6EB7D",
|
||||||
|
"0x22A53599",
|
||||||
|
"0x06DCA5D2",
|
||||||
|
"0xDEB330C1",
|
||||||
|
"0xADC2DE14",
|
||||||
|
"0x36048794",
|
||||||
|
"0x783CC90F",
|
||||||
|
"0xC2B669A1",
|
||||||
|
"0xFFCB8260",
|
||||||
|
"0x7F7F71C3"
|
||||||
|
],
|
||||||
|
"below_1_to_16": [
|
||||||
|
0,
|
||||||
|
1,
|
||||||
|
2,
|
||||||
|
1,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
3,
|
||||||
|
1,
|
||||||
|
0,
|
||||||
|
8,
|
||||||
|
7,
|
||||||
|
2,
|
||||||
|
6,
|
||||||
|
10,
|
||||||
|
14,
|
||||||
|
7
|
||||||
|
],
|
||||||
|
"chance_i_times_4096": [
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
1,
|
||||||
|
1,
|
||||||
|
0,
|
||||||
|
1,
|
||||||
|
1,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
1,
|
||||||
|
1,
|
||||||
|
1,
|
||||||
|
0,
|
||||||
|
1
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"seed": "0xDEADBEEF",
|
||||||
|
"next": [
|
||||||
|
"0x477D20B7",
|
||||||
|
"0x8E1D9142",
|
||||||
|
"0xBA8C2458",
|
||||||
|
"0xFEE0503B",
|
||||||
|
"0x680E0348",
|
||||||
|
"0xA48DB81B",
|
||||||
|
"0x6254EA5C",
|
||||||
|
"0x1CFDAFB3",
|
||||||
|
"0x8878FDF6",
|
||||||
|
"0x6F08B4B5",
|
||||||
|
"0x4A5B273A",
|
||||||
|
"0xF9327CE4",
|
||||||
|
"0x637A51D3",
|
||||||
|
"0x01448B13",
|
||||||
|
"0x94F2C300",
|
||||||
|
"0x5EC60C69"
|
||||||
|
],
|
||||||
|
"below_1_to_16": [
|
||||||
|
0,
|
||||||
|
1,
|
||||||
|
2,
|
||||||
|
3,
|
||||||
|
2,
|
||||||
|
3,
|
||||||
|
2,
|
||||||
|
0,
|
||||||
|
4,
|
||||||
|
4,
|
||||||
|
3,
|
||||||
|
11,
|
||||||
|
5,
|
||||||
|
0,
|
||||||
|
8,
|
||||||
|
5
|
||||||
|
],
|
||||||
|
"chance_i_times_4096": [
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
1,
|
||||||
|
0,
|
||||||
|
1,
|
||||||
|
1,
|
||||||
|
0,
|
||||||
|
1,
|
||||||
|
1,
|
||||||
|
1,
|
||||||
|
1
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"genomes": [
|
||||||
|
{
|
||||||
|
"seed": "0x5EEDB0C5",
|
||||||
|
"genome": {
|
||||||
|
"steps": 14,
|
||||||
|
"form": [
|
||||||
|
0,
|
||||||
|
1,
|
||||||
|
0,
|
||||||
|
1
|
||||||
|
],
|
||||||
|
"formName": "ABAB",
|
||||||
|
"scale": 18,
|
||||||
|
"root": 6,
|
||||||
|
"dens": [
|
||||||
|
81,
|
||||||
|
37
|
||||||
|
],
|
||||||
|
"fillP": 47,
|
||||||
|
"monoLen": 1,
|
||||||
|
"monoRange": 2,
|
||||||
|
"rot": 0,
|
||||||
|
"hocket": 85,
|
||||||
|
"bassDeg": 2,
|
||||||
|
"prog": [
|
||||||
|
0,
|
||||||
|
3,
|
||||||
|
0,
|
||||||
|
4
|
||||||
|
],
|
||||||
|
"swing": 35,
|
||||||
|
"arc": 1,
|
||||||
|
"motifLen": 5,
|
||||||
|
"accentCycle": 3,
|
||||||
|
"layback": 0,
|
||||||
|
"frame": 2,
|
||||||
|
"freeK": 3,
|
||||||
|
"freeRot": 5
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"seed": "0x00000001",
|
||||||
|
"genome": {
|
||||||
|
"steps": 16,
|
||||||
|
"form": [
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
1,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
"formName": "AABA",
|
||||||
|
"scale": 16,
|
||||||
|
"root": 2,
|
||||||
|
"dens": [
|
||||||
|
80,
|
||||||
|
95
|
||||||
|
],
|
||||||
|
"fillP": 110,
|
||||||
|
"monoLen": 1,
|
||||||
|
"monoRange": 2,
|
||||||
|
"rot": 0,
|
||||||
|
"hocket": 90,
|
||||||
|
"bassDeg": 3,
|
||||||
|
"prog": [
|
||||||
|
0,
|
||||||
|
5,
|
||||||
|
0,
|
||||||
|
5
|
||||||
|
],
|
||||||
|
"swing": 21,
|
||||||
|
"arc": 1,
|
||||||
|
"motifLen": 3,
|
||||||
|
"accentCycle": 3,
|
||||||
|
"layback": 0,
|
||||||
|
"frame": 0,
|
||||||
|
"freeK": 2,
|
||||||
|
"freeRot": 1
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"seed": "0xDEADBEEF",
|
||||||
|
"genome": {
|
||||||
|
"steps": 12,
|
||||||
|
"form": [
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
"formName": "AAAA",
|
||||||
|
"scale": 10,
|
||||||
|
"root": 0,
|
||||||
|
"dens": [
|
||||||
|
91,
|
||||||
|
66
|
||||||
|
],
|
||||||
|
"fillP": 40,
|
||||||
|
"monoLen": 0,
|
||||||
|
"monoRange": 2,
|
||||||
|
"rot": 1,
|
||||||
|
"hocket": 170,
|
||||||
|
"bassDeg": 4,
|
||||||
|
"prog": [
|
||||||
|
0,
|
||||||
|
1,
|
||||||
|
0,
|
||||||
|
6
|
||||||
|
],
|
||||||
|
"swing": 50,
|
||||||
|
"arc": 2,
|
||||||
|
"motifLen": 4,
|
||||||
|
"accentCycle": 5,
|
||||||
|
"layback": 0,
|
||||||
|
"frame": 7,
|
||||||
|
"freeK": 4,
|
||||||
|
"freeRot": 3
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"seed": "0x12345678",
|
||||||
|
"genome": {
|
||||||
|
"steps": 16,
|
||||||
|
"form": [
|
||||||
|
0,
|
||||||
|
1,
|
||||||
|
0,
|
||||||
|
1
|
||||||
|
],
|
||||||
|
"formName": "ABAB",
|
||||||
|
"scale": 0,
|
||||||
|
"root": 5,
|
||||||
|
"dens": [
|
||||||
|
50,
|
||||||
|
83
|
||||||
|
],
|
||||||
|
"fillP": 111,
|
||||||
|
"monoLen": 1,
|
||||||
|
"monoRange": 1,
|
||||||
|
"rot": 2,
|
||||||
|
"hocket": 170,
|
||||||
|
"bassDeg": 3,
|
||||||
|
"prog": [
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
5,
|
||||||
|
4
|
||||||
|
],
|
||||||
|
"swing": 0,
|
||||||
|
"arc": 1,
|
||||||
|
"motifLen": 5,
|
||||||
|
"accentCycle": 3,
|
||||||
|
"layback": 0,
|
||||||
|
"frame": 1,
|
||||||
|
"freeK": 3,
|
||||||
|
"freeRot": 9
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"seed": "0xC0FFEE00",
|
||||||
|
"genome": {
|
||||||
|
"steps": 16,
|
||||||
|
"form": [
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
1,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
"formName": "AABA",
|
||||||
|
"scale": 19,
|
||||||
|
"root": 0,
|
||||||
|
"dens": [
|
||||||
|
42,
|
||||||
|
74
|
||||||
|
],
|
||||||
|
"fillP": 78,
|
||||||
|
"monoLen": 1,
|
||||||
|
"monoRange": 2,
|
||||||
|
"rot": 2,
|
||||||
|
"hocket": 210,
|
||||||
|
"bassDeg": 2,
|
||||||
|
"prog": [
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
3,
|
||||||
|
3
|
||||||
|
],
|
||||||
|
"swing": 21,
|
||||||
|
"arc": 1,
|
||||||
|
"motifLen": 3,
|
||||||
|
"accentCycle": 5,
|
||||||
|
"layback": 0,
|
||||||
|
"frame": 3,
|
||||||
|
"freeK": 3,
|
||||||
|
"freeRot": 12
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"seed": "0x0BADF00D",
|
||||||
|
"genome": {
|
||||||
|
"steps": 16,
|
||||||
|
"form": [
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
1,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
"formName": "AABA",
|
||||||
|
"scale": 9,
|
||||||
|
"root": 6,
|
||||||
|
"dens": [
|
||||||
|
46,
|
||||||
|
85
|
||||||
|
],
|
||||||
|
"fillP": 92,
|
||||||
|
"monoLen": 1,
|
||||||
|
"monoRange": 1,
|
||||||
|
"rot": 1,
|
||||||
|
"hocket": 90,
|
||||||
|
"bassDeg": 2,
|
||||||
|
"prog": [
|
||||||
|
0,
|
||||||
|
3,
|
||||||
|
0,
|
||||||
|
4
|
||||||
|
],
|
||||||
|
"swing": 35,
|
||||||
|
"arc": 2,
|
||||||
|
"motifLen": 3,
|
||||||
|
"accentCycle": 3,
|
||||||
|
"layback": 0,
|
||||||
|
"frame": 3,
|
||||||
|
"freeK": 2,
|
||||||
|
"freeRot": 9
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
243
tools/bom.html
Normal file
@@ -0,0 +1,243 @@
|
|||||||
|
<title>Seedbox Build Sheet</title>
|
||||||
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||||
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=Hanken+Grotesk:wght@400;500;600;700&family=Martian+Mono:wght@400;500;700&display=swap" rel="stylesheet">
|
||||||
|
<style>
|
||||||
|
/* Layout: one column workbench sheet. Parts list grouped by subsystem with a tick-off column, then pin map and wiring notes. */
|
||||||
|
:root{
|
||||||
|
--bg:#E8ECEF; --surface:#F6F7F9; --ink:#18202A; --muted:#566271; --line:#C3CBD3; --accent:#1E4C9E;
|
||||||
|
--have:#1D7A4F; --check:#9A6B10; --buy:#B4441F;
|
||||||
|
--f-ui:'Hanken Grotesk', system-ui, -apple-system, 'Segoe UI', sans-serif;
|
||||||
|
--f-mono:'Martian Mono', ui-monospace, Menlo, Consolas, monospace;
|
||||||
|
}
|
||||||
|
@media (prefers-color-scheme: dark){ :root:not([data-theme="light"]){
|
||||||
|
--bg:#12161B; --surface:#1A1F25; --ink:#E2E7EC; --muted:#8C98A5; --line:#333B44; --accent:#8EB0F2;
|
||||||
|
--have:#5CCB92; --check:#E2B455; --buy:#F07B52; color-scheme:dark } }
|
||||||
|
:root[data-theme="dark"]{
|
||||||
|
--bg:#12161B; --surface:#1A1F25; --ink:#E2E7EC; --muted:#8C98A5; --line:#333B44; --accent:#8EB0F2;
|
||||||
|
--have:#5CCB92; --check:#E2B455; --buy:#F07B52; color-scheme:dark }
|
||||||
|
*{box-sizing:border-box}
|
||||||
|
body{background:var(--bg);color:var(--ink);font:15px/1.55 var(--f-ui);padding-block:28px 48px;padding-inline:16px}
|
||||||
|
.wrap{max-width:980px;margin:0 auto;display:grid;gap:22px}
|
||||||
|
h1{font:700 24px/1.15 var(--f-mono);margin:0;letter-spacing:-.01em}
|
||||||
|
h2{font:700 13px var(--f-mono);letter-spacing:.08em;text-transform:uppercase;color:var(--accent);margin:0}
|
||||||
|
.sub{color:var(--muted);margin:6px 0 0;max-width:68ch}
|
||||||
|
.bar{display:flex;flex-wrap:wrap;gap:10px 16px;align-items:center;justify-content:space-between;background:var(--surface);border:1px solid var(--line);border-radius:10px;padding:12px 14px}
|
||||||
|
.tally{display:flex;flex-wrap:wrap;gap:6px 18px;font:500 12px var(--f-mono);font-variant-numeric:tabular-nums;color:var(--muted)}
|
||||||
|
.tally b{color:var(--ink)}
|
||||||
|
.ctrls{display:flex;flex-wrap:wrap;gap:8px;align-items:center}
|
||||||
|
.btn{font:600 12px var(--f-mono);border:1px solid var(--line);background:var(--bg);color:var(--ink);border-radius:6px;padding:7px 12px;cursor:pointer}
|
||||||
|
.btn:hover{border-color:var(--accent)}
|
||||||
|
.btn:focus-visible,input:focus-visible{outline:2px solid var(--accent);outline-offset:2px}
|
||||||
|
.check{display:flex;gap:8px;align-items:center;font:500 13px var(--f-ui);color:var(--muted)}
|
||||||
|
section{display:grid;gap:10px}
|
||||||
|
.tbl{overflow-x:auto;background:var(--surface);border:1px solid var(--line);border-radius:10px}
|
||||||
|
table{width:100%;border-collapse:collapse;min-width:640px}
|
||||||
|
th{font:600 10px var(--f-mono);letter-spacing:.08em;text-transform:uppercase;color:var(--muted);text-align:left;padding:9px 12px;border-bottom:1px solid var(--line)}
|
||||||
|
td{padding:9px 12px;border-bottom:1px solid var(--line);vertical-align:top}
|
||||||
|
tr:last-child td{border-bottom:0}
|
||||||
|
td.part{font-weight:600}
|
||||||
|
td.part small{display:block;font-weight:400;color:var(--muted);font-size:12px}
|
||||||
|
td.qty{font:500 13px var(--f-mono);font-variant-numeric:tabular-nums;white-space:nowrap}
|
||||||
|
td.note{color:var(--muted);font-size:13px;max-width:44ch}
|
||||||
|
td.tick{width:44px;text-align:center}
|
||||||
|
td.tick input{width:18px;height:18px;accent-color:var(--have)}
|
||||||
|
.st{display:inline-block;font:600 10px var(--f-mono);letter-spacing:.06em;text-transform:uppercase;padding:2px 7px;border-radius:999px;border:1px solid currentColor;white-space:nowrap}
|
||||||
|
.st.have{color:var(--have)} .st.check{color:var(--check)} .st.buy{color:var(--buy)} .st.opt{color:var(--muted)}
|
||||||
|
tr.got td:not(.tick){opacity:.5}
|
||||||
|
tr.got td.part{text-decoration:line-through}
|
||||||
|
.hide-got tr.got{display:none}
|
||||||
|
.mono{font-family:var(--f-mono);font-size:12.5px}
|
||||||
|
.pins td:first-child{font:600 13px var(--f-mono);white-space:nowrap}
|
||||||
|
.cols{display:grid;grid-template-columns:repeat(auto-fit,minmax(280px,1fr));gap:14px}
|
||||||
|
.card{background:var(--surface);border:1px solid var(--line);border-radius:10px;padding:14px 16px;min-width:0}
|
||||||
|
.card h3{font:700 14px var(--f-ui);margin:0 0 8px}
|
||||||
|
.card ul{margin:0;padding-left:18px;display:grid;gap:4px;font-size:14px}
|
||||||
|
.card p{margin:0 0 8px;font-size:14px}
|
||||||
|
pre{font:400 12px/1.5 var(--f-mono);margin:0;overflow-x:auto;color:var(--ink)}
|
||||||
|
.gone{color:var(--muted);font-size:14px}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<div class="wrap">
|
||||||
|
<header>
|
||||||
|
<h1>Seedbox build sheet</h1>
|
||||||
|
<p class="sub">Everything for the Pico 2W build: Nokia 5110 screen with the on-screen track map, 11 track buttons in a diode matrix, encoder, two pots, SEED, RUN, one MIDI in and two MIDI outs. "Have" means it was on your parts list; tick items off as you find them in the drawers.</p>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div class="bar">
|
||||||
|
<div class="tally" id="tally"></div>
|
||||||
|
<div class="ctrls">
|
||||||
|
<label class="check"><input type="checkbox" id="hideGot"> Hide ticked</label>
|
||||||
|
<button class="btn" id="copyBuy" type="button">Copy shopping list</button>
|
||||||
|
<button class="btn" id="reset" type="button">Clear ticks</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="parts"></div>
|
||||||
|
|
||||||
|
<section>
|
||||||
|
<h2>Pin map</h2>
|
||||||
|
<div class="tbl"><table class="pins">
|
||||||
|
<thead><tr><th>GPIO</th><th>Function</th><th>Connects to</th></tr></thead>
|
||||||
|
<tbody>
|
||||||
|
<tr><td>GP0</td><td>UART0 TX · MIDI OUT A</td><td>10 Ω → OUT KIT tip (pin 5)</td></tr>
|
||||||
|
<tr><td>GP1</td><td>UART0 RX · MIDI IN</td><td>6N137 pin 6 (1 kΩ pull-up to 3V3)</td></tr>
|
||||||
|
<tr><td>GP4</td><td>UART1 TX · MIDI OUT B</td><td>10 Ω → OUT SYNTH tip (pin 5)</td></tr>
|
||||||
|
<tr><td>GP2 GP3 GP5</td><td>Button matrix rows</td><td>Inputs with internal pull-ups</td></tr>
|
||||||
|
<tr><td>GP6 GP7 GP8 GP9</td><td>Button matrix columns</td><td>Driven low one at a time</td></tr>
|
||||||
|
<tr><td>GP10 GP11</td><td>Encoder A / B</td><td>Encoder module SIA / SIB</td></tr>
|
||||||
|
<tr><td>GP12</td><td>Encoder push</td><td>Encoder module SW</td></tr>
|
||||||
|
<tr><td>GP13</td><td>SEED button</td><td>Button to GND, internal pull-up</td></tr>
|
||||||
|
<tr><td>GP14</td><td>RUN toggle</td><td>Toggle to GND, internal pull-up</td></tr>
|
||||||
|
<tr><td>GP17</td><td>LCD SCE</td><td>Can be tied to GND instead (frees the pin)</td></tr>
|
||||||
|
<tr><td>GP18</td><td>SPI0 SCK</td><td>LCD SCLK</td></tr>
|
||||||
|
<tr><td>GP19</td><td>SPI0 TX</td><td>LCD DN (MOSI)</td></tr>
|
||||||
|
<tr><td>GP20</td><td>LCD DC</td><td>LCD D/C</td></tr>
|
||||||
|
<tr><td>GP21</td><td>LCD RST</td><td>LCD RST</td></tr>
|
||||||
|
<tr><td>GP22</td><td>Free</td><td>Only for backlight dimming later, through a transistor</td></tr>
|
||||||
|
<tr><td>GP26</td><td>ADC0 · Energy pot</td><td>Pot wiper (ends to 3V3 and AGND)</td></tr>
|
||||||
|
<tr><td>GP27</td><td>ADC1 · Drift pot</td><td>Pot wiper (ends to 3V3 and AGND)</td></tr>
|
||||||
|
<tr><td>GP15 GP16 GP28</td><td>Spare</td><td>GP28 is also an analog input</td></tr>
|
||||||
|
<tr><td>GP23 24 25 29</td><td>Reserved</td><td>Used by the Pico 2W's wireless chip, don't wire</td></tr>
|
||||||
|
</tbody>
|
||||||
|
</table></div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section>
|
||||||
|
<h2>Wiring notes</h2>
|
||||||
|
<div class="cols">
|
||||||
|
<div class="card">
|
||||||
|
<h3>MIDI OUT (×2, 3.3 V spec, TRS Type A)</h3>
|
||||||
|
<pre>3V3 ──33 Ω── ring (DIN pin 4)
|
||||||
|
TX ──10 Ω── tip (DIN pin 5)
|
||||||
|
GND ──────── sleeve (DIN pin 2)</pre>
|
||||||
|
<p style="margin-top:8px">Lug order differs between jack models: find tip, ring and sleeve with the meter and a plug inserted.</p>
|
||||||
|
</div>
|
||||||
|
<div class="card">
|
||||||
|
<h3>MIDI IN (6N137)</h3>
|
||||||
|
<pre>ring ──220 Ω── opto 2 (anode)
|
||||||
|
tip ───────── opto 3 (cathode)
|
||||||
|
1N4148 across 2–3, band to pin 2
|
||||||
|
opto 8 → VBUS (5 V) opto 5 → GND
|
||||||
|
opto 6 → GP1, 1 kΩ to 3V3
|
||||||
|
opto 7 ──────── opto 8 (enable high)
|
||||||
|
100 nF across 8–5 (required)</pre>
|
||||||
|
<p style="margin-top:8px">Pull the output up to 3V3, never 5 V. The sleeve stays unconnected on the input.</p>
|
||||||
|
</div>
|
||||||
|
<div class="card">
|
||||||
|
<h3>Button matrix (3 × 4)</h3>
|
||||||
|
<pre> C0 GP6 C1 GP7 C2 GP8 C3 GP9
|
||||||
|
R0 GP2 KICK KICK 2 SNARE (RUN toggle, not in matrix)
|
||||||
|
R1 GP3 HAT HAT 16 PERC GHOST
|
||||||
|
R2 GP5 BASS MEL A MEL B PAD</pre>
|
||||||
|
<p style="margin-top:8px">Same layout as the lid: the RUN toggle sits in row 1 / column 4, but it has its own pin (GP14), so that matrix slot is empty. Each button: row → switch → 1N4148 → column, band toward the column. The diodes let you hold several buttons at once without phantom presses.</p>
|
||||||
|
</div>
|
||||||
|
<div class="card">
|
||||||
|
<h3>Nokia 5110 module</h3>
|
||||||
|
<ul>
|
||||||
|
<li>VCC to 3V3, not 5 V. Logic connects straight to the Pico, no resistors.</li>
|
||||||
|
<li>Backlight polarity differs between red and blue boards. Test which way yours lights (BL to GND or to 3V3), then either hard-wire it or switch that side with a transistor for dimming (NPN on the ground side, PNP on the 3.3 V side).</li>
|
||||||
|
<li>Contrast is set in firmware and drifts a little with temperature. Expect to tune it once.</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div class="card">
|
||||||
|
<h3>Encoder and pots</h3>
|
||||||
|
<ul>
|
||||||
|
<li>Power the encoder module from 3V3. Its onboard pull-ups would otherwise put 5 V on the Pico pins.</li>
|
||||||
|
<li>Pots: one end to 3V3, the other to AGND (pin 33), wiper to the ADC pin. A 100 nF cap from each wiper to AGND calms jitter.</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div class="card">
|
||||||
|
<h3>Panel layout</h3>
|
||||||
|
<ul>
|
||||||
|
<li>See the panel layout page (landscape): screen with the encoder on the left, ENERGY / DRIFT / SEED in the middle, the 4 × 3 block on the right (11 track buttons, RUN toggle top-right).</li>
|
||||||
|
<li>Screen sits on the lid, wires through one hole hidden under the module. USB and the three jacks on the back wall; the Pico's USB end sits at the stripboard edge, behind the slot.</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section>
|
||||||
|
<h2>No longer needed</h2>
|
||||||
|
<p class="gone">USB panel extension, standoffs and screws (stripboard glued behind a slot instead) · 0.91" OLED (replaced by the Nokia screen) · WS2812 LED strip (the screen shows track state) · 4.7 kΩ opto resistor (6N138 only) · 470 Ω resistors (old 5 V MIDI out values) · 5-pin DIN jacks (3.5 mm TRS instead).</p>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
(function(){
|
||||||
|
/* status: have = on your parts list, check = probably in the drawer, buy = likely missing, opt = optional */
|
||||||
|
var GROUPS=[
|
||||||
|
{name:'Core',rows:[
|
||||||
|
['pico','Raspberry Pi Pico 2W','RP2350, micro-USB','1','have','The whole engine runs on it with lots of headroom.'],
|
||||||
|
['lcd','Nokia 5110 LCD module','PCD8544, 84×48, SPI','1','have','The module you used with Arduino.'],
|
||||||
|
['box','Enclosure','','1','have','About 80 × 150 × 40 mm. Panel layout: tools/panel.html.']
|
||||||
|
]},
|
||||||
|
{name:'Controls',rows:[
|
||||||
|
['enc','Rotary encoder with push','KY-040 module or bare EC11','1','have','Power it from 3V3.'],
|
||||||
|
['pots','Potentiometer, linear','B10K (anything 5k–50k linear works)','2','have','Energy and Drift.'],
|
||||||
|
['knobs','Knobs','to fit the pot and encoder shafts','3','check','Shaft types differ (D, knurled, 6 mm). Match them.'],
|
||||||
|
['btn','Momentary push button, panel mount','~12 mm, non-latching','11','check','7 kit + 4 line tracks.'],
|
||||||
|
['seedbtn','Momentary push button, large','16–22 mm, non-latching','1','check','SEED. Any big momentary works.'],
|
||||||
|
['tog','Toggle switch, SPDT','','1','have','RUN / STOP. Your old bypass switch.'],
|
||||||
|
['dio','1N4148 diode','or 1N4448 / 1N914 / any small-signal diode','12','buy','11 for the button matrix + 1 for MIDI IN. 1N4448 works too. Every button needs one: a missing diode gives phantom presses with three buttons held.']
|
||||||
|
]},
|
||||||
|
{name:'MIDI',rows:[
|
||||||
|
['trs','3.5 mm stereo (TRS) jack, panel mount','3 lugs: tip, ring, sleeve. Not mono.','3','check','IN, OUT KIT, OUT SYNTH. MIDI TRS Type A. 6 mm hole each.'],
|
||||||
|
['adapt','TRS-to-DIN MIDI adapter, Type A','female DIN, 3.5 mm plug','1','buy','For a DIN synth or sampler (e.g. MiniFreak). Must say Type A (Type B swaps tip and ring). Type A TRS gear (e.g. SP-404MKII) takes a plain stereo 3.5 mm cable.'],
|
||||||
|
['opto','6N137 optocoupler','DIP-8 (6N138 also works: then drop the pin 7 link)','1','have','MIDI IN. You have three; one is used.'],
|
||||||
|
['sock','DIP-8 IC socket','','1','buy','So the opto can be swapped.'],
|
||||||
|
['r330','Resistor 330 Ω','220–470 Ω','1','check','LCD backlight (LED pin) to 3.3 V, inline at the module.'],
|
||||||
|
['r220','Resistor 220 Ω','150–220 Ω','1','have','MIDI IN, in series with the opto LED. The 6N137 wants a bit more LED current: use 150 Ω if learning misses notes.'],
|
||||||
|
['r1k','Resistor 1 kΩ','470 Ω–2.2 kΩ is fine','2','have','Opto output pull-up, NPN base.'],
|
||||||
|
['r33','Resistor 33 Ω, ½ W','27–47 Ω is fine','2','buy','MIDI OUT ring (pin 4) line, per the MIDI 3.3 V spec. ½ W because a mono plug in an OUT jack shorts it straight to ground (0.33 W); a ¼ W one would cook.'],
|
||||||
|
['r10','Resistor 10 Ω','10–22 Ω is fine','2','check','MIDI OUT tip (pin 5) line, from TX. Don\'t replace with a wire: it protects the pin.'],
|
||||||
|
['cap','Ceramic capacitor 100 nF','anything 47–220 nF','3','check','C1 on the board (6N137 supply bypass, required). Optional: one per pot, soldered at the pot from wiper to the left lug, if the readings jitter.']
|
||||||
|
]},
|
||||||
|
{name:'Screen extras',rows:[
|
||||||
|
['blt','NPN or PNP transistor','2N3904 / BC547 (NPN) or 2N3906 / BC557 (PNP)','1','opt','Only for a dimmable backlight. NPN if your BL lights when grounded, PNP if it lights from 3.3 V. Otherwise hard-wire it.']
|
||||||
|
]},
|
||||||
|
{name:'Power and assembly',rows:[
|
||||||
|
['perf','Stripboard','at least 34 × 28 holes (about 86 × 71 mm)','1','check','Pico socketed at the top edge, so its USB port meets a slot in the back wall.'],
|
||||||
|
['hdr','Female header strip, 1 × 20','2.54 mm','2','check','Socket the Pico instead of soldering it in.'],
|
||||||
|
['mhdr','Male header strip, 1 × 20','2.54 mm','2','check','Soldered to the Pico so it plugs into the female headers. Skip if yours is a Pico 2 WH (headers fitted).'],
|
||||||
|
['tape','Double-sided foam tape or hot glue','','—','check','Holds the stripboard and the screen without screws.']
|
||||||
|
]}
|
||||||
|
];
|
||||||
|
var LABEL={have:'have',check:'check',buy:'buy',opt:'optional'};
|
||||||
|
var ticks={}; try{ ticks=JSON.parse(localStorage.getItem('seedbox.bom')||'{}')||{}; }catch(e){ ticks={}; }
|
||||||
|
function saveTicks(){ try{ localStorage.setItem('seedbox.bom',JSON.stringify(ticks)); }catch(e){} }
|
||||||
|
var root=document.getElementById('parts'), rowEls={};
|
||||||
|
GROUPS.forEach(function(g){
|
||||||
|
var sec=document.createElement('section'), h=document.createElement('h2'); h.textContent=g.name; sec.appendChild(h);
|
||||||
|
var wrap=document.createElement('div'); wrap.className='tbl';
|
||||||
|
var t=document.createElement('table'); t.innerHTML='<thead><tr><th>Got</th><th>Part</th><th>Qty</th><th>Status</th><th>Notes</th></tr></thead>';
|
||||||
|
var tb=document.createElement('tbody');
|
||||||
|
g.rows.forEach(function(r){
|
||||||
|
var tr=document.createElement('tr'), id='p_'+r[0];
|
||||||
|
tr.innerHTML='<td class="tick"><input type="checkbox" id="'+id+'" aria-label="Got '+r[1].replace(/"/g,'')+'"></td>'+
|
||||||
|
'<td class="part"></td><td class="qty"></td><td><span class="st '+r[4]+'">'+LABEL[r[4]]+'</span></td><td class="note"></td>';
|
||||||
|
var p=tr.querySelector('.part'); p.textContent=r[1]; if(r[2]){ var sm=document.createElement('small'); sm.textContent=r[2]; p.appendChild(sm); }
|
||||||
|
tr.querySelector('.qty').textContent=r[3]; tr.querySelector('.note').textContent=r[5];
|
||||||
|
var cb=tr.querySelector('input'); cb.checked=!!ticks[r[0]]; tr.classList.toggle('got',cb.checked);
|
||||||
|
cb.addEventListener('change',function(){ ticks[r[0]]=cb.checked; saveTicks(); tr.classList.toggle('got',cb.checked); tally(); });
|
||||||
|
rowEls[r[0]]={tr:tr,row:r}; tb.appendChild(tr);
|
||||||
|
});
|
||||||
|
t.appendChild(tb); wrap.appendChild(t); sec.appendChild(wrap); root.appendChild(sec);
|
||||||
|
});
|
||||||
|
function tally(){
|
||||||
|
var n=0,got=0,buy=0,miss=[];
|
||||||
|
Object.keys(rowEls).forEach(function(k){ var r=rowEls[k].row; n++; if(ticks[k]) got++; else if(r[4]!=='opt'){ miss.push(r); if(r[4]==='buy') buy++; } });
|
||||||
|
document.getElementById('tally').innerHTML='<span><b>'+got+'</b> of '+n+' ticked</span><span><b>'+miss.length+'</b> still to find</span><span><b>'+buy+'</b> likely to buy</span>';
|
||||||
|
return miss;
|
||||||
|
}
|
||||||
|
tally();
|
||||||
|
var hide=document.getElementById('hideGot');
|
||||||
|
hide.addEventListener('change',function(){ root.classList.toggle('hide-got',hide.checked); });
|
||||||
|
document.getElementById('reset').addEventListener('click',function(){ ticks={}; saveTicks(); Object.keys(rowEls).forEach(function(k){ rowEls[k].tr.querySelector('input').checked=false; rowEls[k].tr.classList.remove('got'); }); tally(); });
|
||||||
|
document.getElementById('copyBuy').addEventListener('click',function(){
|
||||||
|
var b=this, txt=tally().map(function(r){ return r[3]+' × '+r[1]+(r[2]?' ('+r[2]+')':''); }).join('\n');
|
||||||
|
function ok(){ b.textContent='Copied'; setTimeout(function(){ b.textContent='Copy shopping list'; },1400); }
|
||||||
|
try{ navigator.clipboard.writeText(txt).then(ok,function(){ b.textContent='Copy failed'; }); }catch(e){ b.textContent='Copy failed'; }
|
||||||
|
});
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
6
tools/build-wiring.js
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
/* Rebuilds tools/wiring.html from tools/wiring-src/* and hardware/layout.js (the one source of truth).
|
||||||
|
Run after editing the layout: node hardware/check-layout.js && node tools/build-wiring.js */
|
||||||
|
var fs=require('fs'), path=require('path'), d=__dirname;
|
||||||
|
function rd(p){ return fs.readFileSync(path.join(d,p),'utf8'); }
|
||||||
|
var html=rd('wiring-src/head.html')+rd('wiring-src/body.html')+'<script>\n'+rd('../hardware/layout.js')+'\n</script><script>'+rd('wiring-src/render.js')+'</script>';
|
||||||
|
fs.writeFileSync(path.join(d,'wiring.html'),html); console.log('wrote tools/wiring.html',html.length,'bytes');
|
||||||
270
tools/panel.html
Normal file
@@ -0,0 +1,270 @@
|
|||||||
|
<title>Seedbox Panel Layout</title>
|
||||||
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||||
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=Hanken+Grotesk:wght@400;500;600;700&family=Martian+Mono:wght@400;500;700&display=swap" rel="stylesheet">
|
||||||
|
<style>
|
||||||
|
/* Layout: to-scale drawing of the box unfolded (end wall, lid, side wall) beside its inputs; drill table below. */
|
||||||
|
:root{
|
||||||
|
--bg:#E8ECEF; --surface:#F6F7F9; --ink:#18202A; --muted:#566271; --line:#C3CBD3; --accent:#1E4C9E; --warn:#B4441F;
|
||||||
|
--metal:#23272C; --metal-edge:#3A4047; --print:#D9DEE3; --pcb:#B8282A; --lcd:#A6B59A;
|
||||||
|
--f-ui:'Hanken Grotesk', system-ui, -apple-system, 'Segoe UI', sans-serif;
|
||||||
|
--f-mono:'Martian Mono', ui-monospace, Menlo, Consolas, monospace;
|
||||||
|
}
|
||||||
|
@media (prefers-color-scheme: dark){ :root:not([data-theme="light"]){
|
||||||
|
--bg:#12161B; --surface:#1A1F25; --ink:#E2E7EC; --muted:#8C98A5; --line:#333B44; --accent:#8EB0F2; --warn:#F07B52;
|
||||||
|
--metal:#0B0D10; --metal-edge:#4A525B; color-scheme:dark } }
|
||||||
|
:root[data-theme="dark"]{
|
||||||
|
--bg:#12161B; --surface:#1A1F25; --ink:#E2E7EC; --muted:#8C98A5; --line:#333B44; --accent:#8EB0F2; --warn:#F07B52;
|
||||||
|
--metal:#0B0D10; --metal-edge:#4A525B; color-scheme:dark }
|
||||||
|
*{box-sizing:border-box}
|
||||||
|
body{background:var(--bg);color:var(--ink);font:15px/1.55 var(--f-ui);padding-block:28px 48px;padding-inline:16px}
|
||||||
|
.wrap{max-width:1100px;margin:0 auto;display:grid;gap:22px}
|
||||||
|
h1{font:700 24px/1.15 var(--f-mono);margin:0}
|
||||||
|
h2{font:700 13px var(--f-mono);letter-spacing:.08em;text-transform:uppercase;color:var(--accent);margin:0}
|
||||||
|
.sub{color:var(--muted);margin:6px 0 0;max-width:72ch}
|
||||||
|
.main{display:grid;grid-template-columns:minmax(0,1fr) 300px;gap:22px;align-items:start}
|
||||||
|
@media (max-width:860px){.main{grid-template-columns:minmax(0,1fr)}}
|
||||||
|
.draw{background:var(--surface);border:1px solid var(--line);border-radius:10px;padding:14px;min-width:0}
|
||||||
|
.draw svg{display:block;width:100%;height:auto;max-height:82vh}
|
||||||
|
.side{display:grid;gap:14px}
|
||||||
|
.card{background:var(--surface);border:1px solid var(--line);border-radius:10px;padding:14px 16px;display:grid;gap:10px}
|
||||||
|
.fields{display:grid;grid-template-columns:1fr 1fr;gap:10px 12px}
|
||||||
|
.field{display:grid;gap:4px;min-width:0}
|
||||||
|
.field label{font:500 11px var(--f-mono);color:var(--muted)}
|
||||||
|
input[type=number]{font:500 14px var(--f-mono);color:var(--ink);background:var(--bg);border:1px solid var(--line);border-radius:6px;padding:6px 8px;width:100%}
|
||||||
|
input:focus-visible{outline:2px solid var(--accent);outline-offset:2px}
|
||||||
|
.seg{display:grid;grid-template-columns:1fr 1fr;border:1px solid var(--line);border-radius:7px;overflow:hidden}
|
||||||
|
.segb{font:600 12px var(--f-mono);padding:8px;border:0;background:var(--bg);color:var(--muted);cursor:pointer}
|
||||||
|
.segb[aria-pressed="true"]{background:var(--accent);color:var(--surface)}
|
||||||
|
.segb:focus-visible{outline:2px solid var(--accent);outline-offset:-2px}
|
||||||
|
.warns{display:grid;gap:6px;font-size:13px}
|
||||||
|
.warns .ok{color:var(--muted)}
|
||||||
|
.warns .bad{color:var(--warn);font-weight:600}
|
||||||
|
.note{color:var(--muted);font-size:13px;margin:0}
|
||||||
|
.tbl{overflow-x:auto;background:var(--surface);border:1px solid var(--line);border-radius:10px}
|
||||||
|
table{width:100%;border-collapse:collapse;min-width:560px}
|
||||||
|
th{font:600 10px var(--f-mono);letter-spacing:.08em;text-transform:uppercase;color:var(--muted);text-align:left;padding:9px 12px;border-bottom:1px solid var(--line)}
|
||||||
|
td{padding:8px 12px;border-bottom:1px solid var(--line);font-size:14px}
|
||||||
|
td.n{font:500 13px var(--f-mono);font-variant-numeric:tabular-nums;white-space:nowrap}
|
||||||
|
tr:last-child td{border-bottom:0}
|
||||||
|
.cols{display:grid;grid-template-columns:repeat(auto-fit,minmax(280px,1fr));gap:14px}
|
||||||
|
.cols ul{margin:0;padding-left:18px;display:grid;gap:4px;font-size:14px}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<div class="wrap">
|
||||||
|
<header>
|
||||||
|
<h1>Seedbox panel layout</h1>
|
||||||
|
<p class="sub">Lid and walls, drawn to scale and unfolded, in portrait or landscape. The box size is an estimate from your photo, using the red Nokia module (about 44 mm square) as the ruler. Measure the real box and type it in: everything re-flows and the drill table updates.</p>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div class="main">
|
||||||
|
<div class="draw"><svg id="svg" role="img" aria-label="To-scale panel layout"></svg></div>
|
||||||
|
<div class="side">
|
||||||
|
<div class="card">
|
||||||
|
<h2>Box</h2>
|
||||||
|
<div class="seg" role="group" aria-label="Orientation">
|
||||||
|
<button type="button" class="segb" id="oP" aria-pressed="false">Portrait</button>
|
||||||
|
<button type="button" class="segb" id="oL" aria-pressed="true">Landscape</button>
|
||||||
|
</div>
|
||||||
|
<div class="fields">
|
||||||
|
<div class="field"><label for="W">Short side, mm</label><input type="number" id="W" value="80" min="50" max="200"></div>
|
||||||
|
<div class="field"><label for="L">Long side, mm</label><input type="number" id="L" value="150" min="80" max="300"></div>
|
||||||
|
<div class="field"><label for="H">Box height, mm</label><input type="number" id="H" value="40" min="20" max="120"></div>
|
||||||
|
<div class="field"><label for="E">Lip / wall margin, mm</label><input type="number" id="E" value="3" min="0" max="15"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="card">
|
||||||
|
<h2>Parts</h2>
|
||||||
|
<div class="fields">
|
||||||
|
<div class="field"><label for="BH">Track button hole Ø</label><input type="number" id="BH" value="12" min="6" max="24"></div>
|
||||||
|
<div class="field"><label for="BN">Its nut / cap Ø</label><input type="number" id="BN" value="15" min="6" max="30"></div>
|
||||||
|
<div class="field"><label for="SH">SEED hole Ø</label><input type="number" id="SH" value="16" min="6" max="30"></div>
|
||||||
|
<div class="field"><label for="SN">SEED nut / cap Ø</label><input type="number" id="SN" value="20" min="8" max="36"></div>
|
||||||
|
<div class="field"><label for="K">Knob Ø (pots, encoder)</label><input type="number" id="K" value="17" min="8" max="35"></div>
|
||||||
|
<div class="field"><label for="D">MIDI jack hole Ø (3.5 mm TRS)</label><input type="number" id="D" value="6" min="4" max="22" step="0.5"></div>
|
||||||
|
</div>
|
||||||
|
<p class="note">Measure your actual buttons and knobs. The nut or cap diameter is what sets how close things can sit.</p>
|
||||||
|
</div>
|
||||||
|
<div class="card">
|
||||||
|
<h2>Fit check</h2>
|
||||||
|
<div class="warns" id="warns"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<section style="display:grid;gap:10px">
|
||||||
|
<h2>Drill list</h2>
|
||||||
|
<p class="note">Lid coordinates from the top-left corner as drawn (the cable wall is at the top). Wall coordinates: x from the left as drawn, height down from the lid edge or up from the inside floor.</p>
|
||||||
|
<div class="tbl"><table><thead><tr><th>Where</th><th>Part</th><th>x mm</th><th>y mm</th><th>Hole</th></tr></thead><tbody id="drill"></tbody></table></div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="cols">
|
||||||
|
<div class="card">
|
||||||
|
<h2>Why this arrangement</h2>
|
||||||
|
<ul id="why"></ul>
|
||||||
|
</div>
|
||||||
|
<div class="card">
|
||||||
|
<h2>Before drilling</h2>
|
||||||
|
<ul>
|
||||||
|
<li>Check the inside, not just the lid: pot bodies (~17 mm) and button bodies hang down from the lid, and the socketed Pico stands about 13 mm off the stripboard. Under SEED sit R4 (flat) and pads P3/P4: keep those wires low, and check the SEED button's depth.</li>
|
||||||
|
<li>The stripboard lies on the floor with strip A against the back wall. The Pico's USB socket is about 41 mm from the board's left edge and about 16 mm above the inside floor (foam tape, board, female header, the Pico's header spacer): measure your stack and adjust the slot before cutting it. The slot has to clear the plug's moulding, not just the metal tip.</li><li>MIDI jacks: 3.5 mm stereo panel jacks, a 6 mm hole and a nut each.</li>
|
||||||
|
<li>The screen's wire hole goes under the module's 8-pin header, so the module hides it.</li>
|
||||||
|
<li>Centre-punch every mark and step-drill the big holes (a step bit is kinder to plastic too: go slowly so it doesn't crack).</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
(function(){
|
||||||
|
var NS='http://www.w3.org/2000/svg', svg=document.getElementById('svg');
|
||||||
|
var ids=['W','L','H','E','BH','BN','SH','SN','K','D'];
|
||||||
|
try{ var saved=JSON.parse(localStorage.getItem('seedbox.panel')||'{}'); ids.forEach(function(k){ if(saved[k]!=null) document.getElementById(k).value=saved[k]; }); }catch(e){}
|
||||||
|
function v(id){ var x=parseFloat(document.getElementById(id).value); return isFinite(x)?x:0; }
|
||||||
|
function css(n){ return getComputedStyle(document.documentElement).getPropertyValue(n).trim(); }
|
||||||
|
function el(tag,attrs,parent,txt){ var e=document.createElementNS(NS,tag); for(var k in attrs) e.setAttribute(k,attrs[k]); if(txt!=null) e.textContent=txt; (parent||svg).appendChild(e); return e; }
|
||||||
|
function r1(x){ return Math.round(x*10)/10; }
|
||||||
|
|
||||||
|
var ORIENT='L';
|
||||||
|
var NAMES=[['KICK','KICK 2','SNARE','RUN'],['HAT','HAT 16','PERC','GHOST'],['BASS','MEL A','MEL B','PAD']]; // RUN = the run/stop toggle, in the grid but not in the matrix
|
||||||
|
function buttons(items,cx,cy,px,py,P){ for(var r=0;r<3;r++) for(var c=0;c<4;c++) { var nm=NAMES[r][c], x=cx+(c-1.5)*px, y=cy+(r-1)*py; items.push(nm==='RUN'?{id:'run',name:'RUN toggle',x:x,y:y,hole:6,clr:6,kind:'toggle'}:{id:'b'+r+c,name:nm,x:x,y:y,hole:P.BH,clr:P.BN/2,kind:'btn'}); } }
|
||||||
|
/* Portrait: lid W wide x L deep, jacks on the far short wall, USB on the side wall. */
|
||||||
|
function layoutP(W,L,H,E,P){
|
||||||
|
var m=E+3, items=[], scr={x:m,y:m,w:44,h:44}, colX=(scr.x+scr.w+W-m)/2;
|
||||||
|
items.push({name:'Encoder',x:colX,y:m+11,hole:7,clr:P.K/2,kind:'knob'});
|
||||||
|
var rowY=scr.y+scr.h+P.K/2+5, slot=function(i){ return m+(W-2*m)*(i+0.5)/3; };
|
||||||
|
items.push({name:'ENERGY pot',x:slot(0),y:rowY,hole:7,clr:P.K/2,kind:'knob'});
|
||||||
|
items.push({name:'DRIFT pot',x:slot(1),y:rowY,hole:7,clr:P.K/2,kind:'knob'});
|
||||||
|
items.push({name:'SEED',x:slot(2),y:rowY,hole:P.SH,clr:P.SN/2,kind:'seed'});
|
||||||
|
var bc=P.BN/2, px=Math.min(20,(W-2*m-2*bc)/3), lastY=L-m-bc-4, py=Math.max(px,Math.min(20,(lastY-(rowY+P.K/2+4+bc))/2));
|
||||||
|
buttons(items,W/2,lastY-py,px,py,P);
|
||||||
|
var jacks=[], jn=['MIDI IN','OUT · KIT','OUT · SYNTH'];
|
||||||
|
for(var j=0;j<3;j++) jacks.push({name:jn[j],x:slot(j),z:H/2,hole:P.D,clr:Math.max(5,P.D/2+2)});
|
||||||
|
return {A:W,B:L,m:m,scr:scr,items:items,jacks:jacks,usbBack:null,usbSide:{y:m+22,z:13,w:12,h:7},wire:{x:scr.x+5,y:scr.y+22,hole:8},px:px,py:py,
|
||||||
|
why:['The 12 track buttons are a 4 × 3 block, the same shape as the wiring matrix and the screen map. Synth tracks on the row nearest you.',
|
||||||
|
'Screen and encoder at the far end; knobs and SEED in a middle row. Your hands work the buttons without covering the screen.',
|
||||||
|
'Cables leave from the far short wall; USB on the side.',
|
||||||
|
'Narrow and deep: fits beside other gear on a crowded desk or pedalboard.']};
|
||||||
|
}
|
||||||
|
/* Landscape: lid L wide x W deep, three zones left to right, jacks and USB on the back long wall. */
|
||||||
|
function layoutL(W,L,H,E,P){
|
||||||
|
var A=L, B=W, m=E+3, items=[], scr={x:m,y:m,w:44,h:44};
|
||||||
|
var encY=scr.y+scr.h+P.K/2+3;
|
||||||
|
items.push({name:'Encoder',x:scr.x+P.K/2+1,y:encY,hole:7,clr:P.K/2,kind:'knob'});
|
||||||
|
var bc=P.BN/2, px=Math.min(20,(A-m-(scr.x+scr.w)-Math.max(P.SN,P.K)-6-2*bc)/3), py=Math.min(20,(B-2*m-2*bc)/2);
|
||||||
|
var bx=A-m-bc-1.5*px, by=B/2;
|
||||||
|
buttons(items,bx,by,px,py,P);
|
||||||
|
var midL=scr.x+scr.w, midR=bx-1.5*px-bc, cx=(midL+midR)/2, ys=function(i){ return m+(B-2*m)*(i+0.5)/3; };
|
||||||
|
items.push({name:'ENERGY pot',x:cx,y:ys(0),hole:7,clr:P.K/2,kind:'knob'});
|
||||||
|
items.push({name:'DRIFT pot',x:cx,y:ys(1),hole:7,clr:P.K/2,kind:'knob'});
|
||||||
|
items.push({name:'SEED',x:cx,y:ys(2),hole:P.SH,clr:P.SN/2,kind:'seed'});
|
||||||
|
var jacks=[], jn=['MIDI IN','OUT · KIT','OUT · SYNTH'];
|
||||||
|
var jc=Math.max(5,P.D/2+2), jp=Math.max(16,2*jc+4);
|
||||||
|
for(var j=0;j<3;j++) jacks.push({name:jn[j],x:A-m-jc-2-(2-j)*jp,z:H/2,hole:P.D,clr:jc});
|
||||||
|
return {A:A,B:B,m:m,scr:scr,items:items,jacks:jacks,usbBack:{x:m+38,z:16,w:12,h:8},usbSide:null,wire:{x:scr.x+5,y:scr.y+22,hole:8},px:px,py:py,
|
||||||
|
why:['Three zones left to right: screen with the encoder, then ENERGY / DRIFT / SEED in a column, then the 4 × 3 block: 11 track buttons with the RUN toggle in the top-right corner.',
|
||||||
|
'The button block has the same shape as the wiring matrix and the screen map, so the LCD map sits right next to the buttons it mirrors.',
|
||||||
|
'Everything is one hand-reach from the front edge, and the screen is never under your hands.',
|
||||||
|
'All cables and USB leave from the back long wall. Wider and shallower: sits in front of a keyboard or sampler.']};
|
||||||
|
}
|
||||||
|
function layout(W,L,H,E,P){ return ORIENT==='L' ? layoutL(W,L,H,E,P) : layoutP(W,L,H,E,P); }
|
||||||
|
|
||||||
|
function check(W,L,H,E,Lo){
|
||||||
|
W=Lo.A; L=Lo.B;
|
||||||
|
var out=[], it=Lo.items, i, j;
|
||||||
|
function inRect(a,R,pad){ var dx=Math.max(R.x-a.x,0,a.x-(R.x+R.w)), dy=Math.max(R.y-a.y,0,a.y-(R.y+R.h)); return Math.hypot(dx,dy)<a.clr+pad; }
|
||||||
|
for(i=0;i<it.length;i++){
|
||||||
|
var a=it[i];
|
||||||
|
if(a.x-a.clr<E || a.x+a.clr>W-E || a.y-a.clr<E || a.y+a.clr>L-E){ a.bad=true; out.push(a.name+' runs past the usable lid area.'); }
|
||||||
|
if(inRect(a,Lo.scr,1.5)){ a.bad=true; out.push(a.name+' overlaps the screen module.'); }
|
||||||
|
for(j=i+1;j<it.length;j++){ var b=it[j], d=Math.hypot(a.x-b.x,a.y-b.y);
|
||||||
|
if(d<a.clr+b.clr+1){ a.bad=b.bad=true; out.push(a.name+' and '+b.name+' are too close ('+r1(d)+' mm apart, need '+r1(a.clr+b.clr+1)+').'); } }
|
||||||
|
}
|
||||||
|
var J=Lo.jacks;
|
||||||
|
for(i=0;i<J.length;i++){ if(J[i].z-J[i].clr<E || J[i].z+J[i].clr>H-E){ J[i].bad=true; out.push('The box is too shallow for the MIDI jacks (they need about '+r1(2*J[i].clr+2*E)+' mm of wall height).'); break; } }
|
||||||
|
if(Lo.usbBack && J[0].x-12<Lo.usbBack.x+Lo.usbBack.w/2){ out.push('The USB slot runs into the MIDI IN jack.'); }
|
||||||
|
for(i=0;i<J.length-1;i++) if(J[i+1].x-J[i].x<J[i].clr+J[i+1].clr+2){ J[i].bad=J[i+1].bad=true; out.push('MIDI jacks are too close; their nuts will clash.'); break; }
|
||||||
|
if(ORIENT==='P') out.push('Portrait does not match the stripboard: its USB edge needs the long back wall. Use Landscape for this build.');
|
||||||
|
if(Lo.px<15) out.push('Button pitch is '+r1(Lo.px)+' mm, tight for fingers. 16 mm or more plays better.');
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
function draw(){
|
||||||
|
var W=v('W'), L=v('L'), H=v('H'), E=v('E'), P={BH:v('BH'),BN:v('BN'),SH:v('SH'),SN:v('SN'),K:v('K'),D:v('D')};
|
||||||
|
try{ var s={}; ids.forEach(function(k){ s[k]=document.getElementById(k).value; }); localStorage.setItem('seedbox.panel',JSON.stringify(s)); }catch(e){}
|
||||||
|
var Lo=layout(W,L,H,E,P), warns=check(W,L,H,E,Lo);
|
||||||
|
var C={ink:css('--ink'),muted:css('--muted'),line:css('--line'),accent:css('--accent'),warn:css('--warn'),metal:css('--metal'),edge:css('--metal-edge'),print:css('--print'),pcb:css('--pcb'),lcd:css('--lcd'),font:css('--f-mono')};
|
||||||
|
svg.innerHTML='';
|
||||||
|
var LW=Lo.A, LD=Lo.B, pad=14, gap=8, ox=pad+12, oyEnd=pad+8, oyLid=oyEnd+H+gap, oxSide=ox+LW+gap, VW=oxSide+(Lo.usbSide?H:0)+pad+6, VH=oyLid+LD+pad+10;
|
||||||
|
svg.setAttribute('viewBox','0 0 '+VW+' '+VH);
|
||||||
|
var g=el('g',{'font-family':C.font,'font-size':'2.6'});
|
||||||
|
function panel(x,y,w,h,label){ el('rect',{x:x,y:y,width:w,height:h,rx:1.5,fill:C.metal,stroke:C.edge,'stroke-width':0.5},g); el('text',{x:x,y:y-2.2,fill:C.muted,'font-size':'2.8'},g,label); }
|
||||||
|
function dim(x1,y1,x2,y2,txt,vert){
|
||||||
|
el('line',{x1:x1,y1:y1,x2:x2,y2:y2,stroke:C.muted,'stroke-width':0.25},g);
|
||||||
|
[[x1,y1],[x2,y2]].forEach(function(p){ var t=vert?{x1:p[0]-1.5,y1:p[1],x2:p[0]+1.5,y2:p[1]}:{x1:p[0],y1:p[1]-1.5,x2:p[0],y2:p[1]+1.5}; t.stroke=C.muted; t['stroke-width']=0.25; el('line',t,g); });
|
||||||
|
var t=el('text',{x:(x1+x2)/2+(vert?-1.6:0),y:(y1+y2)/2+(vert?0:-1.2),fill:C.muted,'text-anchor':'middle','font-size':'2.6'},g,txt);
|
||||||
|
if(vert) t.setAttribute('transform','rotate(-90 '+((x1+x2)/2-1.6)+' '+((y1+y2)/2)+')');
|
||||||
|
}
|
||||||
|
function hole(cx,cy,d,clr,bad,kind,label,below){
|
||||||
|
if(kind==='knob'){ el('circle',{cx:cx,cy:cy,r:clr,fill:'#15181C',stroke:bad?C.warn:'#4A525B','stroke-width':bad?0.6:0.4},g); el('line',{x1:cx,y1:cy-clr+1.2,x2:cx,y2:cy-clr*0.35,stroke:'#D9DEE3','stroke-width':0.8},g); }
|
||||||
|
else if(kind==='toggle'){ el('circle',{cx:cx,cy:cy,r:clr,fill:'#2C3238',stroke:bad?C.warn:'#4A525B','stroke-width':0.4},g); el('rect',{x:cx-1,y:cy-6,width:2,height:6,rx:1,fill:'#C9CFD5'},g); }
|
||||||
|
else { el('circle',{cx:cx,cy:cy,r:clr,fill:'#2C3238',stroke:bad?C.warn:'#4A525B','stroke-width':bad?0.6:0.4},g); el('circle',{cx:cx,cy:cy,r:d/2-0.6,fill:kind==='seed'?'#3B4148':'#30363D',stroke:'#14171A','stroke-width':0.3},g); }
|
||||||
|
el('circle',{cx:cx,cy:cy,r:0.5,fill:C.accent},g);
|
||||||
|
if(label) el('text',{x:cx,y:below?cy+clr+3:cy-clr-1.2,fill:C.print,'text-anchor':'middle','font-size':'2.4','letter-spacing':'0.1'},g,label);
|
||||||
|
}
|
||||||
|
// end wall (jacks)
|
||||||
|
panel(ox,oyEnd,LW,H,ORIENT==='L'?'BACK WALL · cables':'END WALL · cables');
|
||||||
|
Lo.jacks.forEach(function(j){ var cx=ox+j.x, cy=oyEnd+j.z;
|
||||||
|
hole(cx,cy,j.hole,j.clr,j.bad,'jack'); el('text',{x:cx,y:oyEnd+H-2,fill:C.print,'text-anchor':'middle','font-size':'2.2'},g,j.name); });
|
||||||
|
// lid
|
||||||
|
panel(ox,oyLid,LW,LD,'LID · top view · you sit at the bottom edge');
|
||||||
|
el('rect',{x:ox+E,y:oyLid+E,width:LW-2*E,height:LD-2*E,fill:'none',stroke:'#3A4047','stroke-width':0.3,'stroke-dasharray':'1.2 1'},g);
|
||||||
|
var S=Lo.scr, sx=ox+S.x, sy=oyLid+S.y;
|
||||||
|
el('rect',{x:sx,y:sy,width:S.w,height:S.h,rx:1,fill:C.pcb},g);
|
||||||
|
[[2.5,2.5],[S.w-2.5,2.5],[2.5,S.h-2.5],[S.w-2.5,S.h-2.5]].forEach(function(p){ el('circle',{cx:sx+p[0],cy:sy+p[1],r:1.3,fill:'#7A1416'},g); });
|
||||||
|
el('rect',{x:sx+5,y:sy+2.5,width:33,height:36,rx:0.8,fill:'#A9B0B6'},g);
|
||||||
|
el('rect',{x:sx+7,y:sy+5,width:26,height:30,rx:0.6,fill:C.lcd},g);
|
||||||
|
for(var p=0;p<8;p++) el('circle',{cx:sx+2.5,cy:sy+12+p*2.54,r:0.55,fill:'#E9C46A'},g);
|
||||||
|
// a hint of the 4x3 track map on the LCD
|
||||||
|
for(var r=0;r<3;r++) for(var c=0;c<4;c++) el('rect',{x:sx+8.5+c*6,y:sy+20+r*4.6,width:5,height:3.6,fill:(r+c)%3?'#2A3330':'none',stroke:'#2A3330','stroke-width':0.3},g);
|
||||||
|
el('circle',{cx:ox+Lo.wire.x,cy:oyLid+Lo.wire.y,r:Lo.wire.hole/2,fill:'none',stroke:C.accent,'stroke-width':0.35,'stroke-dasharray':'0.8 0.6'},g);
|
||||||
|
Lo.items.forEach(function(it){
|
||||||
|
var lab=it.kind==='knob'||it.kind==='seed'||it.kind==='toggle'?it.name.replace(' pot','').replace(' toggle',''):it.name;
|
||||||
|
hole(ox+it.x,oyLid+it.y,it.hole,it.clr,it.bad,it.kind==='fill'?'btn':it.kind,lab,true);
|
||||||
|
});
|
||||||
|
// USB slot: side wall (portrait) or back wall (landscape)
|
||||||
|
if(Lo.usbSide){
|
||||||
|
panel(oxSide,oyLid,H,LD,'SIDE');
|
||||||
|
var U=Lo.usbSide; el('rect',{x:oxSide+H-U.z-U.h/2,y:oyLid+U.y-U.w/2,width:U.h,height:U.w,rx:1,fill:'#0A0C0E',stroke:C.accent,'stroke-width':0.35},g);
|
||||||
|
el('text',{x:oxSide+H/2,y:oyLid+U.y+U.w/2+4,fill:C.print,'text-anchor':'middle','font-size':'2.4'},g,'USB');
|
||||||
|
el('text',{x:oxSide+H/2,y:oyLid+LD-3,fill:C.muted,'text-anchor':'middle','font-size':'2.2'},g,'floor →');
|
||||||
|
} else {
|
||||||
|
var Ub=Lo.usbBack; el('rect',{x:ox+Ub.x-Ub.w/2,y:oyEnd+H-Ub.z-Ub.h/2,width:Ub.w,height:Ub.h,rx:1,fill:'#0A0C0E',stroke:C.accent,'stroke-width':0.35},g);
|
||||||
|
el('text',{x:ox+Ub.x,y:oyEnd+H-2,fill:C.print,'text-anchor':'middle','font-size':'2.2'},g,'USB');
|
||||||
|
}
|
||||||
|
// dimensions
|
||||||
|
dim(ox,oyLid+LD+6,ox+LW,oyLid+LD+6,LW+' mm');
|
||||||
|
dim(ox-6,oyLid,ox-6,oyLid+LD,LD+' mm',true);
|
||||||
|
dim(ox-6,oyEnd,ox-6,oyEnd+H,H+' mm',true);
|
||||||
|
// fit check
|
||||||
|
var wb=document.getElementById('warns'); wb.innerHTML='';
|
||||||
|
if(!warns.length){ var d0=document.createElement('div'); d0.className='ok'; d0.textContent='Everything fits. Button pitch '+r1(Lo.px)+' × '+r1(Lo.py)+' mm.'; wb.appendChild(d0); }
|
||||||
|
warns.filter(function(w,i,a){ return a.indexOf(w)===i; }).forEach(function(w){ var d=document.createElement('div'); d.className='bad'; d.textContent=w; wb.appendChild(d); });
|
||||||
|
// drill table
|
||||||
|
var tb=document.getElementById('drill'); tb.innerHTML='';
|
||||||
|
function row(where,part,x,y,h){ var tr=document.createElement('tr'); [where,part,x,y,h].forEach(function(t,i){ var td=document.createElement('td'); if(i>=2) td.className='n'; td.textContent=t; tr.appendChild(td); }); tb.appendChild(tr); }
|
||||||
|
row('Lid','Screen wire hole (under the header)',r1(Lo.wire.x),r1(Lo.wire.y),'Ø '+Lo.wire.hole);
|
||||||
|
Lo.items.forEach(function(it){ row('Lid',it.name,r1(it.x),r1(it.y),'Ø '+it.hole); });
|
||||||
|
var wn=ORIENT==='L'?'Back wall':'End wall';
|
||||||
|
Lo.jacks.forEach(function(j){ row(wn,'Jack '+j.name,r1(j.x),r1(j.z)+' down','Ø '+j.hole); });
|
||||||
|
if(Lo.usbSide) row('Side wall','USB slot (Pico port)',r1(Lo.usbSide.z)+' up from inside floor',r1(Lo.usbSide.y),Lo.usbSide.w+' × '+Lo.usbSide.h+' slot');
|
||||||
|
else row(wn,'USB slot (Pico port)',r1(Lo.usbBack.x),r1(Lo.usbBack.z)+' up from inside floor',Lo.usbBack.w+' × '+Lo.usbBack.h+' slot');
|
||||||
|
var wy=document.getElementById('why'); wy.innerHTML=''; Lo.why.forEach(function(t){ var li=document.createElement('li'); li.textContent=t; wy.appendChild(li); });
|
||||||
|
document.getElementById('oP').setAttribute('aria-pressed',String(ORIENT==='P')); document.getElementById('oL').setAttribute('aria-pressed',String(ORIENT==='L'));
|
||||||
|
}
|
||||||
|
ids.forEach(function(k){ document.getElementById(k).addEventListener('input',draw); });
|
||||||
|
try{ ORIENT=localStorage.getItem('seedbox.orient')==='P'?'P':'L'; }catch(e){}
|
||||||
|
[['oP','P'],['oL','L']].forEach(function(b){ document.getElementById(b[0]).addEventListener('click',function(){ ORIENT=b[1]; try{ localStorage.setItem('seedbox.orient',ORIENT); }catch(e){} draw(); }); });
|
||||||
|
draw();
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
111
tools/wiring-src/body.html
Normal file
@@ -0,0 +1,111 @@
|
|||||||
|
<div class="wrap">
|
||||||
|
<header>
|
||||||
|
<h1>Seedbox wiring · v0.1</h1>
|
||||||
|
<p class="sub">One stripboard carries the Pico and the MIDI circuits. Everything on the panel (screen, encoder, pots, buttons, jacks) connects to it with hookup wire. Board positions are given as strip letter + hole number, e.g. <code>X2</code> is strip X, hole 2, counted from the left with the parts facing you.</p>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<section>
|
||||||
|
<h2>Wire map · panel to board</h2>
|
||||||
|
<p class="sub">Every panel wire, where it lands. Big blue text is the board hole: strip letter, then hole number counted from the opto end (the end away from the Pico's USB).</p>
|
||||||
|
<div class="draw"><svg id="wmap" role="img" aria-label="Board with every panel wire labelled at its hole"></svg></div>
|
||||||
|
<div class="tbl"><table class="check"><thead><tr><th>Done</th><th>Part</th><th>Lug</th><th>Wire</th><th>Board hole</th></tr></thead><tbody id="wcheck"></tbody></table></div>
|
||||||
|
<p class="note">Ticks are saved in this browser. Lug drawings for each part are under Panel wiring further down.</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section>
|
||||||
|
<h2>Wire colours</h2>
|
||||||
|
<div class="card">
|
||||||
|
<div class="legend">
|
||||||
|
<span><i style="background:var(--w-red)"></i>Red · 3.3 V and the MIDI OUT supply pins</span>
|
||||||
|
<span><i style="background:var(--w-black)"></i>Black · ground</span>
|
||||||
|
<span><i style="background:var(--w-yellow)"></i>Yellow · button rows, SEED, RUN</span>
|
||||||
|
<span><i style="background:var(--w-green)"></i>Green · button columns, encoder</span>
|
||||||
|
<span><i style="background:var(--w-blue)"></i>Blue · screen, pots, MIDI signals</span>
|
||||||
|
<span><i style="background:var(--link)"></i>Grey · short links on the board</span>
|
||||||
|
</div>
|
||||||
|
<p class="note">If your five spools are other colours, keep the roles and rename them. Red and black for power and ground matter most; the rest just keep bundles readable.</p>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section>
|
||||||
|
<h2>Board · parts side</h2>
|
||||||
|
<div class="draw"><svg id="top" role="img" aria-label="Stripboard seen from the parts side"></svg></div>
|
||||||
|
<p class="note">Copper strips run left to right underneath (shown faintly). Board size 34 holes × 28 strips (about 86 × 71 mm). The Pico sits in two 20-pin female headers with pins 1 and 40 on strip A, so its USB end is flush with the top edge of the board and the micro-USB plug reaches out past it: line that edge up with a slot in the box wall. Numbered dots <b>P1</b>… (pads) are where off-board wires solder; the wire map at the top says where each one goes.</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section>
|
||||||
|
<h2>Board · copper side, for cutting</h2>
|
||||||
|
<div class="draw"><svg id="bottom" role="img" aria-label="Stripboard seen from the copper side with cuts marked"></svg></div>
|
||||||
|
<div class="cols">
|
||||||
|
<div class="card"><h3>Track cuts</h3><ul id="cutlist"></ul>
|
||||||
|
<p class="note">This view is flipped: hole 1 is on the <b>right</b>. Cut with a 3–4 mm drill bit twisted by hand or a spot-face cutter, then check each cut with the meter's continuity beeper.</p></div>
|
||||||
|
<div class="card"><h3>Power strips along the bottom</h3>
|
||||||
|
<ul><li><b>Z</b> · 5 V from the Pico's VBUS (only the opto uses it)</li><li><b>AA</b> · ground</li><li><b>AB</b> · 3.3 V from the Pico's 3V3 pin</li></ul>
|
||||||
|
<p class="note">Every red wire to the panel starts on AB, every black wire on AA (or on the AGND strip for the pots).</p></div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section>
|
||||||
|
<h2>Board parts and links</h2>
|
||||||
|
<div class="tbl"><table><thead><tr><th>Part</th><th>Value</th><th>Holes</th><th>Note</th></tr></thead><tbody id="partlist"></tbody></table></div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section>
|
||||||
|
<h2>Off-board wires</h2>
|
||||||
|
<div class="tbl"><table><thead><tr><th>#</th><th>Board hole</th><th>Colour</th><th>Pico pin / signal</th><th>Goes to</th></tr></thead><tbody id="wirelist"></tbody></table></div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section>
|
||||||
|
<h2>Panel wiring</h2>
|
||||||
|
<div class="cols">
|
||||||
|
<div class="card" style="grid-column:1/-1"><h3>Button matrix · seen from the back</h3>
|
||||||
|
<div class="draw" style="padding:6px"><svg id="matrix" role="img" aria-label="Button matrix wiring"></svg></div>
|
||||||
|
<p class="note">Each button: one lug to its <b>row</b> wire (yellow, daisy-chained along the row), the other lug through a diode to its <b>column</b> wire (green, daisy-chained down the column). The diode's band points to the column. Solder the diodes straight onto the button lugs and sleeve them. 11 track buttons, each with its own diode, plus D1 on the board: 12 diodes. GHOST is wired at row 2 / column 4 (where FILL used to be). The RUN toggle sits in the grid's row 1 / column 4 spot but is not part of the matrix: row 1's wire stops at SNARE. <span class="warn">The drawing is from behind the panel, the side you solder on: KICK is top-right, RUN top-left.</span></p></div>
|
||||||
|
<div class="card"><h3>MIDI jacks · 3.5 mm TRS, Type A</h3>
|
||||||
|
<svg id="din" viewBox="0 0 300 150" role="img" aria-label="TRS jack lugs seen from inside the box"></svg>
|
||||||
|
<ul>
|
||||||
|
<li><b>MIDI IN</b>: ring → Y1, tip → W1. Sleeve stays empty.</li>
|
||||||
|
<li><b>OUT KIT</b>: tip → X26, ring → V22, sleeve → AA22.</li>
|
||||||
|
<li><b>OUT SYNTH</b>: tip → U31, ring → V31, sleeve → AA31.</li>
|
||||||
|
</ul>
|
||||||
|
<p class="note">Use stereo (TRS) jacks; mono ones have no ring. Lug order differs between jack models, so find the tip, ring and sleeve lugs with the meter and a plug inserted. Type A is the MIDI standard: a plain stereo 3.5 mm cable goes straight to another Type A device (e.g. SP-404MKII). A DIN device (e.g. MiniFreak) needs a TRS-to-DIN adapter that says <b>Type A</b>; a Type B one swaps tip and ring and gives silence (nothing breaks).</p></div>
|
||||||
|
<div class="card"><h3>Nokia 5110 module</h3>
|
||||||
|
<svg id="lcd" viewBox="0 0 300 92" role="img" aria-label="LCD header pins"></svg>
|
||||||
|
<ul>
|
||||||
|
<li>Pin order as printed on your module: 1 VCC, 2 GND, 3 SCE, 4 RST, 5 D/C, 6 DN (MOSI), 7 SCLK, 8 LED.</li>
|
||||||
|
<li><b>LED</b> (backlight): wire it through a 330 Ω resistor to AB22 (3.3 V); put the resistor inline at the module end and sleeve it. On boards with this pin order that lights it. If it stays dark, move that wire to any free hole on strip AA (ground). GP22 (L27) is left free: it's only needed if you ever want dimming, through a transistor.</li>
|
||||||
|
<li><b>SCE</b> can go straight to ground instead of S27 if it's the only thing on the SPI bus; that frees GP17.</li>
|
||||||
|
</ul></div>
|
||||||
|
<div class="card"><h3>Encoder module</h3>
|
||||||
|
<svg id="enc" viewBox="0 0 300 80" role="img" aria-label="Encoder module pins"></svg>
|
||||||
|
<p class="note">Pin order as printed on your module: VCC, GND, SW, SIB, SIA. Power it from 3.3 V (AB4), never 5 V: its pull-up resistors would put 5 V on the Pico's pins. If turning it clockwise steps the wrong way, swap SIA and SIB (or flip it in firmware).</p></div>
|
||||||
|
<div class="card"><h3>Pots, SEED, RUN</h3>
|
||||||
|
<svg id="misc" viewBox="0 0 300 150" role="img" aria-label="Pots, SEED and RUN wiring"></svg>
|
||||||
|
<ul>
|
||||||
|
<li>Pots, lugs facing down, shaft toward you: left lug to AGND (H27), middle to its pin, right lug to 3.3 V (AB25). Chain the left lugs together and the right lugs together.</li>
|
||||||
|
<li>SEED: one lug to Q2, the other to the ground bus (AA6).</li>
|
||||||
|
<li>RUN: middle lug to S2, one outer lug to the ground bus. If RUN and STOP come out swapped, use the other outer lug.</li>
|
||||||
|
</ul></div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section>
|
||||||
|
<h2>Build order and checks</h2>
|
||||||
|
<div class="cols">
|
||||||
|
<div class="card"><h3>Board</h3>
|
||||||
|
<ol>
|
||||||
|
<li>Cut the board to 34 × 28 holes. For the top edge, cut through the row of holes above strip A, then sand it back to about 1.3 mm from the strip A holes (stop before the strip A copper). That puts the Pico's edge flush with the board; unsanded, the Pico sits about 1 mm back, which still works if the wall slot lets the plug's moulding through. Make the track cuts and beep each one out.</li>
|
||||||
|
<li>Solder the two Pico headers, the DIP-8 socket (notch toward strip U), then the resistors and the diode (band on V4). Leave C1 for step 4.</li>
|
||||||
|
<li>Solder the grey links with insulated wire. The two long ones (GP0 and GP4) run straight down <b>under the Pico, between its two headers</b>: fit them before the Pico goes in.</li>
|
||||||
|
<li>Before fitting C1, with the Pico and the 6N137 out: AB to AA must <b>not</b> beep, Z to AA must not beep. Then fit C1 (it can chirp briefly on the beeper once it is in).</li>
|
||||||
|
</ol></div>
|
||||||
|
<div class="card"><h3>First power</h3>
|
||||||
|
<ol>
|
||||||
|
<li>Plug in the Pico only, power from USB. Measure AB to AA: about 3.3 V. Z to AA: about 5 V.</li>
|
||||||
|
<li>Unplug, insert the 6N137 (notch toward strip U, pin 1 at hole U5).</li>
|
||||||
|
<li>Wire the panel. Before closing the box, check that no bare lugs touch each other.</li>
|
||||||
|
<li>The box needs the Pico firmware (the C port of this simulator) before it does anything. That's the next step. Firmware note: GP0 and GP4 drive the MIDI loops directly (about 6–8 mA), above the RP2350's default 4 mA drive, so set them to 8–12 mA with <code>gpio_set_drive_strength()</code>.</li>
|
||||||
|
</ol></div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
57
tools/wiring-src/head.html
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
<title>Seedbox Wiring</title>
|
||||||
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||||
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=Hanken+Grotesk:wght@400;500;600;700&family=Martian+Mono:wght@400;500;700&display=swap" rel="stylesheet">
|
||||||
|
<style>
|
||||||
|
/* Layout: one column build guide. Board drawings first (component side, then copper side for cuts), then wire list, then panel wiring. */
|
||||||
|
:root{
|
||||||
|
--bg:#E8ECEF; --surface:#F6F7F9; --ink:#18202A; --muted:#566271; --line:#C3CBD3; --accent:#1E4C9E; --warn:#B4441F;
|
||||||
|
--board:#D9C79E; --board-edge:#B8A57A; --copper:#C98A4B; --hole:#5B4A30; --part:#2C3238; --part-ink:#E8ECEF;
|
||||||
|
--w-red:#D7263D; --w-black:#1E1E1E; --w-yellow:#D9A600; --w-green:#2E9E5B; --w-blue:#2F6FDB; --link:#7A8794;
|
||||||
|
--f-ui:'Hanken Grotesk', system-ui, -apple-system, 'Segoe UI', sans-serif;
|
||||||
|
--f-mono:'Martian Mono', ui-monospace, Menlo, Consolas, monospace;
|
||||||
|
}
|
||||||
|
@media (prefers-color-scheme: dark){ :root:not([data-theme="light"]){
|
||||||
|
--bg:#12161B; --surface:#1A1F25; --ink:#E2E7EC; --muted:#8C98A5; --line:#333B44; --accent:#8EB0F2; --warn:#F07B52;
|
||||||
|
--board:#6B5D40; --board-edge:#4F4430; --copper:#B97A3E; --hole:#231C10; --part:#0E1114; --part-ink:#D9E0E6;
|
||||||
|
--w-black:#8E98A3; --link:#9AA6B2; color-scheme:dark } }
|
||||||
|
:root[data-theme="dark"]{
|
||||||
|
--bg:#12161B; --surface:#1A1F25; --ink:#E2E7EC; --muted:#8C98A5; --line:#333B44; --accent:#8EB0F2; --warn:#F07B52;
|
||||||
|
--board:#6B5D40; --board-edge:#4F4430; --copper:#B97A3E; --hole:#231C10; --part:#0E1114; --part-ink:#D9E0E6;
|
||||||
|
--w-black:#8E98A3; --link:#9AA6B2; color-scheme:dark }
|
||||||
|
*{box-sizing:border-box}
|
||||||
|
body{background:var(--bg);color:var(--ink);font:15px/1.55 var(--f-ui);padding-block:28px 56px;padding-inline:16px}
|
||||||
|
.wrap{max-width:1040px;margin:0 auto;display:grid;gap:26px}
|
||||||
|
h1{font:700 24px/1.15 var(--f-mono);margin:0}
|
||||||
|
h2{font:700 13px var(--f-mono);letter-spacing:.08em;text-transform:uppercase;color:var(--accent);margin:0}
|
||||||
|
h3{font:700 15px var(--f-ui);margin:0}
|
||||||
|
.sub{color:var(--muted);margin:6px 0 0;max-width:74ch}
|
||||||
|
section{display:grid;gap:12px}
|
||||||
|
.card{background:var(--surface);border:1px solid var(--line);border-radius:10px;padding:14px 16px;display:grid;gap:10px;min-width:0}
|
||||||
|
.legend{display:flex;flex-wrap:wrap;gap:8px 18px;font:500 13px var(--f-ui)}
|
||||||
|
.legend span{display:inline-flex;align-items:center;gap:8px}
|
||||||
|
.legend i{display:inline-block;width:26px;height:6px;border-radius:3px}
|
||||||
|
.draw{background:var(--surface);border:1px solid var(--line);border-radius:10px;padding:10px;overflow-x:auto}
|
||||||
|
.draw svg{display:block;width:100%;min-width:640px;height:auto}
|
||||||
|
.cols{display:grid;grid-template-columns:repeat(auto-fit,minmax(300px,1fr));gap:14px}
|
||||||
|
.tbl{overflow-x:auto;background:var(--surface);border:1px solid var(--line);border-radius:10px}
|
||||||
|
table{width:100%;border-collapse:collapse;min-width:560px}
|
||||||
|
th{font:600 10px var(--f-mono);letter-spacing:.08em;text-transform:uppercase;color:var(--muted);text-align:left;padding:9px 12px;border-bottom:1px solid var(--line)}
|
||||||
|
td{padding:8px 12px;border-bottom:1px solid var(--line);font-size:14px;vertical-align:top}
|
||||||
|
tr:last-child td{border-bottom:0}
|
||||||
|
td.m{font:500 13px var(--f-mono);white-space:nowrap;font-variant-numeric:tabular-nums}
|
||||||
|
.sw{display:inline-block;width:12px;height:12px;border-radius:3px;vertical-align:-1px;margin-right:6px}
|
||||||
|
ol,ul{margin:0;padding-left:20px;display:grid;gap:5px}
|
||||||
|
li{max-width:78ch}
|
||||||
|
.note{color:var(--muted);font-size:13px;margin:0;max-width:78ch}
|
||||||
|
.warn{color:var(--warn);font-weight:600}
|
||||||
|
code{font:500 12.5px var(--f-mono)}
|
||||||
|
table.check td{padding:7px 12px}
|
||||||
|
table.check td.hole{font:700 18px var(--f-mono);color:var(--accent)}
|
||||||
|
table.check td.part{font-weight:600;white-space:nowrap}
|
||||||
|
table.check tr.grp td{border-top:2px solid var(--line)}
|
||||||
|
table.check tr.done td{opacity:.45}
|
||||||
|
table.check tr.done td.hole{text-decoration:line-through}
|
||||||
|
table.check input{width:20px;height:20px}
|
||||||
|
#wmap{min-width:720px}
|
||||||
|
</style>
|
||||||
246
tools/wiring-src/render.js
Normal file
@@ -0,0 +1,246 @@
|
|||||||
|
(function(){
|
||||||
|
var L=LAYOUT, NS='http://www.w3.org/2000/svg';
|
||||||
|
function css(n){ return getComputedStyle(document.documentElement).getPropertyValue(n).trim(); }
|
||||||
|
function el(tag,a,p,txt){ var e=document.createElementNS(NS,tag); for(var k in a) e.setAttribute(k,a[k]); if(txt!=null) e.textContent=txt; p.appendChild(e); return e; }
|
||||||
|
function rowName(r){ return r<26 ? String.fromCharCode(65+r) : 'A'+String.fromCharCode(65+r-26); }
|
||||||
|
function pos(h){ return rowName(h[1])+(h[0]+1); }
|
||||||
|
function H(p){ return /^P\d+$/.test(p) ? pos(L.wires[+p.slice(1)-1].h) : p; }
|
||||||
|
var COL={red:'--w-red',black:'--w-black',yellow:'--w-yellow',green:'--w-green',blue:'--w-blue'};
|
||||||
|
var cutKey={}; L.cuts.forEach(function(c){ cutKey[c[0]+','+c[1]]=1; });
|
||||||
|
function strips(svg,X,C,strong){
|
||||||
|
for(var r=0;r<L.H;r++){
|
||||||
|
var start=0;
|
||||||
|
for(var c=0;c<=L.W;c++){
|
||||||
|
if(c===L.W || cutKey[c+','+r]){
|
||||||
|
if(c>start){ var x0=Math.min(X(start),X(c-1))-0.42, x1=Math.max(X(start),X(c-1))+0.42;
|
||||||
|
el('rect',{x:x0,y:r-0.36,width:x1-x0,height:0.72,rx:0.2,fill:C.copper,opacity:strong?1:0.28},svg); }
|
||||||
|
start=c+1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function holes(svg,X,C){ for(var r=0;r<L.H;r++) for(var c=0;c<L.W;c++) el('circle',{cx:X(c),cy:r,r:0.16,fill:C.hole},svg); }
|
||||||
|
function labels(svg,X,C,flip){
|
||||||
|
var g=el('g',{'font-family':css('--f-mono'),'font-size':'0.55',fill:C.muted},svg);
|
||||||
|
for(var c=0;c<L.W;c++) if(c%2===0 || c===L.W-1){ el('text',{x:X(c),y:-1.75,'text-anchor':'middle'},g,String(c+1)); el('text',{x:X(c),y:L.H+0.9,'text-anchor':'middle'},g,String(c+1)); }
|
||||||
|
for(var r=0;r<L.H;r++){ el('text',{x:-1.3,y:r+0.2,'text-anchor':'middle'},g,rowName(r)); el('text',{x:L.W+0.3,y:r+0.2,'text-anchor':'middle'},g,rowName(r)); }
|
||||||
|
}
|
||||||
|
function frame(svg){ svg.setAttribute('viewBox','-2.4 -2.6 '+(L.W+3.8)+' '+(L.H+4.0)); }
|
||||||
|
|
||||||
|
var U1label=(L.parts.filter(function(p){ return p.id==='U1'; })[0]||{}).label||'';
|
||||||
|
function drawTop(){
|
||||||
|
var svg=document.getElementById('top'); svg.innerHTML=''; frame(svg);
|
||||||
|
var C={copper:css('--copper'),hole:css('--hole'),muted:css('--muted'),part:css('--part'),pink:css('--part-ink'),link:css('--link'),warn:css('--warn'),board:css('--board'),edge:css('--board-edge'),ink:css('--ink')};
|
||||||
|
var X=function(c){ return c; };
|
||||||
|
el('rect',{x:-0.8,y:-0.5,width:L.W+0.6,height:L.H+0.3,rx:0.4,fill:C.board,stroke:C.edge,'stroke-width':0.12},svg);
|
||||||
|
strips(svg,X,C,false); holes(svg,X,C); labels(svg,X,C);
|
||||||
|
L.cuts.forEach(function(c){ var g={stroke:C.warn,'stroke-width':0.1,opacity:0.7}; el('line',Object.assign({x1:c[0]-0.22,y1:c[1]-0.22,x2:c[0]+0.22,y2:c[1]+0.22},g),svg); el('line',Object.assign({x1:c[0]-0.22,y1:c[1]+0.22,x2:c[0]+0.22,y2:c[1]-0.22},g),svg); });
|
||||||
|
var f=css('--f-mono');
|
||||||
|
L.parts.forEach(function(p){
|
||||||
|
if(p.kind==='pico'){
|
||||||
|
el('rect',{x:11.3,y:-0.55,width:8.4,height:20.1,rx:0.4,fill:C.part,opacity:0.92},svg);
|
||||||
|
el('rect',{x:14.3,y:-1.35,width:2.4,height:1.35,rx:0.2,fill:'#9AA3AD'},svg); el('text',{x:15.5,y:-0.55,'text-anchor':'middle','font-family':f,'font-size':'0.42',fill:'#111'},svg,'USB');
|
||||||
|
el('text',{x:15.5,y:1.6,'text-anchor':'middle','font-family':f,'font-size':'0.62',fill:C.pink},svg,'PICO 2W');
|
||||||
|
p.pins.forEach(function(pp){ var left=pp.h[0]===12;
|
||||||
|
el('rect',{x:pp.h[0]-0.32,y:pp.h[1]-0.32,width:0.64,height:0.64,fill:'#111',stroke:'#D4AF37','stroke-width':0.08},svg);
|
||||||
|
el('text',{x:left?pp.h[0]+0.55:pp.h[0]-0.55,y:pp.h[1]+0.16,'text-anchor':left?'start':'end','font-family':f,'font-size':'0.42',fill:C.pink},svg,pp.n); });
|
||||||
|
} else if(p.kind==='dip8'){
|
||||||
|
el('rect',{x:4.25,y:19.55,width:2.5,height:3.9,rx:0.15,fill:C.part},svg);
|
||||||
|
el('path',{d:'M 5 19.55 a 0.5 0.5 0 0 0 1 0',fill:'#555'},svg);
|
||||||
|
el('text',{x:5.5,y:21.7,'text-anchor':'middle','font-family':f,'font-size':'0.42',fill:C.pink,transform:'rotate(90 5.5 21.6)'},svg,U1label);
|
||||||
|
p.pins.forEach(function(pp){ el('rect',{x:pp.h[0]-0.25,y:pp.h[1]-0.18,width:0.5,height:0.36,fill:'#BFC6CD'},svg); });
|
||||||
|
el('text',{x:3.55,y:20.15,'text-anchor':'end','font-family':f,'font-size':'0.4',fill:C.ink},svg,'1');
|
||||||
|
} else {
|
||||||
|
var a=p.pins[0].h, b=p.pins[1].h, horiz=a[1]===b[1], mx=(a[0]+b[0])/2, my=(a[1]+b[1])/2, len=horiz?Math.abs(b[0]-a[0]):Math.abs(b[1]-a[1]);
|
||||||
|
el('line',{x1:a[0],y1:a[1],x2:b[0],y2:b[1],stroke:'#9AA3AD','stroke-width':0.12},svg);
|
||||||
|
var bw=p.kind==='cap'?0.7:0.62, bh=Math.min(len-0.5,p.kind==='diode'?0.7:2.2);
|
||||||
|
var body=p.kind==='res'?'#C9A26B':p.kind==='diode'?'#D9663A':'#3D7BC9';
|
||||||
|
if(horiz){ el('rect',{x:mx-bh/2,y:my-bw/2,width:bh,height:bw,rx:0.25,fill:body,stroke:'#00000055','stroke-width':0.05},svg);
|
||||||
|
el('text',{x:mx,y:my-0.5,'text-anchor':'middle','font-family':f,'font-size':'0.42',fill:C.ink},svg,p.id); }
|
||||||
|
else el('rect',{x:a[0]-bw/2,y:my-bh/2,width:bw,height:bh,rx:0.25,fill:body,stroke:'#00000055','stroke-width':0.05,'stroke-dasharray':p.opt?'0.2 0.12':''},svg);
|
||||||
|
if(p.kind==='diode'){ var ky=p.pins[0].n==='K'?p.pins[0].h[1]:p.pins[1].h[1]; el('rect',{x:a[0]-bw/2,y:(ky<my?my-bh/2:my+bh/2-0.16),width:bw,height:0.16,fill:'#111'},svg); }
|
||||||
|
if(!horiz) el('text',{x:a[0]+0.48,y:my+0.15,'font-family':f,'font-size':'0.42',fill:C.ink},svg,p.id+(p.opt?'*':''));
|
||||||
|
}
|
||||||
|
p.pins.forEach(function(pp){ if(p.kind!=='pico') el('circle',{cx:pp.h[0],cy:pp.h[1],r:0.13,fill:'#D4AF37'},svg); });
|
||||||
|
});
|
||||||
|
L.jumpers.forEach(function(j){
|
||||||
|
el('line',{x1:j.a[0],y1:j.a[1],x2:j.b[0],y2:j.b[1],stroke:C.link,'stroke-width':0.2,'stroke-linecap':'round',opacity:0.9},svg);
|
||||||
|
[j.a,j.b].forEach(function(h){ el('circle',{cx:h[0],cy:h[1],r:0.2,fill:C.link},svg); });
|
||||||
|
});
|
||||||
|
L.wires.forEach(function(w,i){
|
||||||
|
var col=css(COL[w.col]);
|
||||||
|
el('circle',{cx:w.h[0],cy:w.h[1],r:0.3,fill:col,stroke:'#fff','stroke-width':0.07},svg);
|
||||||
|
var right=w.h[0]>=20, tx=w.h[0]+(right?0.42:-0.42);
|
||||||
|
el('text',{x:tx,y:w.h[1]-0.3,'text-anchor':right?'start':'end','font-family':f,'font-size':'0.48','font-weight':'700',fill:C.ink},svg,'P'+(i+1));
|
||||||
|
});
|
||||||
|
var rails=[[25,'5 V'],[26,'GND'],[27,'3V3']];
|
||||||
|
rails.forEach(function(r){ el('text',{x:L.W+1.15,y:r[0]+0.2,'font-family':f,'font-size':'0.45',fill:C.warn},svg,r[1]); });
|
||||||
|
}
|
||||||
|
function drawBottom(){
|
||||||
|
var svg=document.getElementById('bottom'); svg.innerHTML=''; frame(svg);
|
||||||
|
var C={copper:css('--copper'),hole:css('--hole'),muted:css('--muted'),warn:css('--warn'),board:css('--board'),edge:css('--board-edge'),part:css('--part')};
|
||||||
|
var X=function(c){ return L.W-1-c; };
|
||||||
|
el('rect',{x:-0.8,y:-0.5,width:L.W+0.6,height:L.H+0.3,rx:0.4,fill:C.board,stroke:C.edge,'stroke-width':0.12},svg);
|
||||||
|
strips(svg,X,C,true); holes(svg,X,C); labels(svg,X,C);
|
||||||
|
el('rect',{x:X(19)-0.5,y:-0.55,width:8.4,height:20.1,rx:0.4,fill:'none',stroke:C.muted,'stroke-width':0.08,'stroke-dasharray':'0.3 0.2'},svg);
|
||||||
|
el('text',{x:X(15.5),y:-0.85,'text-anchor':'middle','font-family':css('--f-mono'),'font-size':'0.42',fill:C.muted},svg,'Pico outline (other side), USB at this edge');
|
||||||
|
L.cuts.forEach(function(c){ var x=X(c[0]);
|
||||||
|
el('rect',{x:x-0.45,y:c[1]-0.45,width:0.9,height:0.9,rx:0.15,fill:C.board},svg);
|
||||||
|
el('circle',{cx:x,cy:c[1],r:0.34,fill:'none',stroke:C.warn,'stroke-width':0.14},svg); });
|
||||||
|
var cl=document.getElementById('cutlist'); cl.innerHTML='';
|
||||||
|
var groups={}; L.cuts.forEach(function(c){ (groups[c[0]]=groups[c[0]]||[]).push(c[1]); });
|
||||||
|
Object.keys(groups).forEach(function(k){ var rs=groups[k].sort(function(a,b){return a-b;}); var li=document.createElement('li');
|
||||||
|
li.textContent='Hole '+(+k+1)+(rs.length>1?' on strips '+rowName(rs[0])+' to '+rowName(rs[rs.length-1])+' ('+rs.length+' cuts)':' on strip '+rowName(rs[0])+' (1 cut)'); cl.appendChild(li); });
|
||||||
|
}
|
||||||
|
function tables(){
|
||||||
|
var pl=document.getElementById('partlist'); pl.innerHTML='';
|
||||||
|
function row(tb,cells,mcols){ var tr=document.createElement('tr'); cells.forEach(function(c,i){ var td=document.createElement('td'); if(mcols.indexOf(i)>=0) td.className='m'; if(c && c.nodeType) td.appendChild(c); else td.textContent=c; tr.appendChild(td); }); tb.appendChild(tr); }
|
||||||
|
var notes={U1:'6N137 in a DIP-8 socket. Notch toward strip U, pin 1 at hole U5. Pin 7 (enable) is tied to pin 8 by the U10–V10 link.',R1:'MIDI IN current limit. 3-hole span: stand it upright, or use a small ⅛ W part flat. If learning ever misses notes, use 150 Ω.',D1:'Band (cathode) on V4, the opto LED anode side. Stands upright (1-hole span).',R2:'Opto output pull-up to 3.3 V',C1:'6N137 supply bypass, right beside the chip: U11 is its 5 V (pin 8) strip, X11 its GND (pin 5) strip. Required. Fit it after the beep test.',R4:'MIDI OUT KIT, TX line (RC). Lies flat along strip X, across the cut at X23 (4-hole span, ¼ W fits). SEED hangs right above it.',R5:'MIDI OUT KIT, 3.3 V line (RA). ½ W: a mono plug in the jack shorts it to ground.',R6:'MIDI OUT SYNTH, TX line (RC)',R7:'MIDI OUT SYNTH, 3.3 V line (RA). ½ W, same reason as R5.'};
|
||||||
|
row(pl,['Pico 2W','2 × 20-pin female header','A13–T13 and A20–T20 (pins 1–20 down the left, 40–21 down the right)','USB end flush with the top edge of the board, past strip A'],[2]);
|
||||||
|
L.parts.forEach(function(p){ if(p.kind==='pico') return;
|
||||||
|
row(pl,[p.id,p.label,p.kind==='dip8'?'U5–X5, U8–X8':p.pins.map(function(pp){ return pos(pp.h); }).join(' – '),notes[p.id]||''],[2]); });
|
||||||
|
L.jumpers.forEach(function(j){ row(pl,['Link','wire',pos(j.a)+' – '+pos(j.b),j.n],[2]); });
|
||||||
|
var picoAt={}; L.parts[0].pins.forEach(function(pp){ picoAt[pp.h[1]+(pp.h[0]===12?'L':'R')]=pp.n; });
|
||||||
|
function pinFor(h){ var r=h[1]; if(r>=0 && r<=19) return picoAt[r+(h[0]<15?'L':'R')]; if(r===25) return '5 V strip'; if(r===26) return 'GND strip'; if(r===27) return '3V3 strip';
|
||||||
|
return {'0,24':'via R1 to opto','0,22':'opto cathode','25,23':'via R4 (10 Ω) from GP0','21,21':'via R5 (33 Ω) from 3V3','30,20':'via R6 (10 Ω) from GP4','30,21':'via R7 (33 Ω) from 3V3'}[h[0]+','+h[1]]||''; }
|
||||||
|
var wl=document.getElementById('wirelist'); wl.innerHTML='';
|
||||||
|
L.wires.forEach(function(w,i){ var s=document.createElement('span'); var sw=document.createElement('span'); sw.className='sw'; sw.style.background='var('+COL[w.col]+')'; s.appendChild(sw); s.appendChild(document.createTextNode(w.col));
|
||||||
|
row(wl,['P'+(i+1),pos(w.h),s,pinFor(w.h),w.to],[0,1]); });
|
||||||
|
}
|
||||||
|
function drawMatrix(){ // drawn from the BACK (lug side), so the columns are mirrored: col 4 on the left, KICK top-right
|
||||||
|
var svg=document.getElementById('matrix'); svg.innerHTML=''; svg.setAttribute('viewBox','0 0 700 330');
|
||||||
|
var C={y:css('--w-yellow'),g:css('--w-green'),ink:css('--ink'),muted:css('--muted'),part:css('--part'),line:css('--line')}, f=css('--f-mono');
|
||||||
|
var names=[['KICK','KICK 2','SNARE',null],['HAT','HAT 16','PERC','GHOST'],['BASS','MEL A','MEL B','PAD']];
|
||||||
|
var x0=110, dx=118, y0=84, dy=80, rowEnd=560;
|
||||||
|
function bxOf(c){ return x0+(3-c)*dx; }
|
||||||
|
for(var c=0;c<4;c++){ var cx=bxOf(c)-38; el('line',{x1:cx,y1:c===3?y0+dy-28:y0-10,x2:cx,y2:y0+2*dy+40,stroke:C.g,'stroke-width':3},svg);
|
||||||
|
el('text',{x:cx,y:y0+2*dy+60,'text-anchor':'middle','font-family':f,'font-size':12,'font-weight':700,fill:C.ink},svg,'Col '+(c+1)+' → '+H('P'+(32+c))); }
|
||||||
|
for(var r=0;r<3;r++){ var ry=y0+r*dy-18, left=bxOf(r===0?2:3)+9;
|
||||||
|
el('line',{x1:left,y1:ry,x2:rowEnd,y2:ry,stroke:C.y,'stroke-width':3},svg);
|
||||||
|
el('text',{x:rowEnd+6,y:ry+4,'font-family':f,'font-size':12,'font-weight':700,fill:C.ink},svg,'Row '+(r+1)+' → '+H('P'+(29+r)));
|
||||||
|
for(c=0;c<4;c++){ var bx=bxOf(c), by=y0+r*dy;
|
||||||
|
if(!names[r][c]){ el('rect',{x:bx-12,y:by-16,width:24,height:32,rx:4,fill:'none',stroke:C.muted,'stroke-width':1.2,'stroke-dasharray':'4 3'},svg);
|
||||||
|
el('text',{x:bx,y:by+4,'text-anchor':'middle','font-family':f,'font-size':10,fill:C.ink},svg,'RUN');
|
||||||
|
el('text',{x:bx,y:by+34,'text-anchor':'middle','font-family':f,'font-size':9,fill:C.muted},svg,'toggle, not in matrix'); continue; }
|
||||||
|
el('circle',{cx:bx,cy:by,r:17,fill:C.part},svg);
|
||||||
|
el('text',{x:bx,y:by+34,'text-anchor':'middle','font-family':f,'font-size':11,fill:C.muted},svg,names[r][c]);
|
||||||
|
var lRow=bx+9, lDio=bx-9, colX=bx-38;
|
||||||
|
el('circle',{cx:lRow,cy:by,r:3.2,fill:'#D4AF37'},svg); el('circle',{cx:lDio,cy:by,r:3.2,fill:'#D4AF37'},svg);
|
||||||
|
el('line',{x1:lRow,y1:by-3,x2:lRow,y2:ry,stroke:C.y,'stroke-width':2},svg);
|
||||||
|
el('line',{x1:lDio,y1:by,x2:colX,y2:by,stroke:'#9AA3AD','stroke-width':1.5},svg);
|
||||||
|
el('rect',{x:colX+4,y:by-4,width:18,height:8,rx:2,fill:'#D9663A'},svg); el('rect',{x:colX+4,y:by-4,width:4,height:8,fill:'#111'},svg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
el('text',{x:x0-60,y:22,'font-family':f,'font-size':12,'font-weight':700,fill:C.ink},svg,'Seen from the BACK (lug side): KICK is top-right.');
|
||||||
|
el('text',{x:x0-60,y:38,'font-family':f,'font-size':10,fill:C.muted},svg,'Diode band → column wire. Lid flipped left to right, back wall still at the top.');
|
||||||
|
}
|
||||||
|
function drawDin(){ // 3.5 mm TRS panel jacks, Type A, lugs seen from inside the box
|
||||||
|
var svg=document.getElementById('din'); svg.innerHTML='';
|
||||||
|
var f=css('--f-mono'), ink=css('--ink'), m=css('--muted');
|
||||||
|
var jacks=[{n:'IN',x:52,w:{T:['--w-blue','P2'],R:['--w-blue','P1']}},{n:'OUT KIT',x:150,w:{T:['--w-blue','P3'],R:['--w-red','P4'],S:['--w-black','P5']}},{n:'OUT SYNTH',x:248,w:{T:['--w-blue','P6'],R:['--w-red','P7'],S:['--w-black','P8']}}];
|
||||||
|
var LUG={T:[-20,'T·5'],R:[0,'R·4'],S:[20,'S·2']};
|
||||||
|
jacks.forEach(function(j){
|
||||||
|
el('rect',{x:j.x-30,y:30,width:60,height:34,rx:4,fill:'#2C3238'},svg);
|
||||||
|
el('circle',{cx:j.x,cy:47,r:9,fill:'none',stroke:'#9AA3AD','stroke-width':2},svg);
|
||||||
|
Object.keys(LUG).forEach(function(k){ var lx=j.x+LUG[k][0], used=j.w[k];
|
||||||
|
el('rect',{x:lx-3,y:64,width:6,height:12,fill:'#C9CFD5'},svg);
|
||||||
|
if(used){ el('line',{x1:lx,y1:76,x2:lx,y2:100,stroke:css(used[0]),'stroke-width':4,'stroke-linecap':'round'},svg);
|
||||||
|
el('text',{x:lx,y:112,'text-anchor':'middle','font-family':f,'font-size':11,'font-weight':700,fill:ink},svg,H(used[1])); }
|
||||||
|
else el('text',{x:lx,y:90,'text-anchor':'middle','font-family':f,'font-size':8,fill:m},svg,'—');
|
||||||
|
el('text',{x:lx,y:124,'text-anchor':'middle','font-family':f,'font-size':8,fill:m},svg,LUG[k][1]); });
|
||||||
|
el('text',{x:j.x,y:142,'text-anchor':'middle','font-family':f,'font-size':11,fill:ink},svg,j.n); });
|
||||||
|
el('text',{x:150,y:14,'text-anchor':'middle','font-family':f,'font-size':9,fill:m},svg,'Type A · T tip = pin 5 · R ring = 4 · S sleeve = 2');
|
||||||
|
}
|
||||||
|
function pinRow(id,pins,w){
|
||||||
|
var svg=document.getElementById(id); svg.innerHTML=''; var f=css('--f-mono'), ink=css('--ink');
|
||||||
|
var n=pins.length, sp=Math.min(36,260/n), x0=150-sp*(n-1)/2;
|
||||||
|
pins.forEach(function(p,i){ var x=x0+i*sp;
|
||||||
|
el('rect',{x:x-6,y:14,width:12,height:12,fill:'#111',stroke:'#D4AF37','stroke-width':1.5},svg);
|
||||||
|
el('line',{x1:x,y1:26,x2:x,y2:52,stroke:p[1]?css(p[1]):'none','stroke-width':4,'stroke-linecap':'round'},svg);
|
||||||
|
el('text',{x:x,y:10,'text-anchor':'middle','font-family':f,'font-size':n>6?8:9,fill:ink},svg,p[0]);
|
||||||
|
el('text',{x:x,y:68,'text-anchor':'middle','font-family':f,'font-size':10,'font-weight':700,fill:ink},svg,H(p[2]||'')); });
|
||||||
|
}
|
||||||
|
function drawMisc(){
|
||||||
|
var svg=document.getElementById('misc'); svg.innerHTML=''; var f=css('--f-mono'), ink=css('--ink'), m=css('--muted');
|
||||||
|
function pot(x,label,wp){ el('circle',{cx:x,cy:42,r:20,fill:'#2C3238'},svg); el('text',{x:x,y:14,'text-anchor':'middle','font-family':f,'font-size':10,fill:ink},svg,label);
|
||||||
|
[[-12,'--w-black','P25'],[0,'--w-blue',wp],[12,'--w-red','P24']].forEach(function(l){ el('rect',{x:x+l[0]-2.5,y:62,width:5,height:12,fill:'#C9CFD5'},svg); el('line',{x1:x+l[0],y1:74,x2:x+l[0],y2:96,stroke:css(l[1]),'stroke-width':4,'stroke-linecap':'round'},svg);
|
||||||
|
el('text',{x:x+l[0],y:108,'text-anchor':'middle','font-family':f,'font-size':9,'font-weight':700,fill:ink},svg,H(l[2])); }); }
|
||||||
|
pot(45,'ENERGY','P22'); pot(115,'DRIFT','P23');
|
||||||
|
el('circle',{cx:190,cy:42,r:16,fill:'#2C3238'},svg); el('text',{x:190,y:14,'text-anchor':'middle','font-family':f,'font-size':10,fill:ink},svg,'SEED');
|
||||||
|
[[-7,'--w-yellow','P26'],[7,'--w-black','P28']].forEach(function(l){ el('line',{x1:190+l[0],y1:58,x2:190+l[0],y2:96,stroke:css(l[1]),'stroke-width':4,'stroke-linecap':'round'},svg); el('text',{x:190+l[0],y:108,'text-anchor':'middle','font-family':f,'font-size':9,'font-weight':700,fill:ink},svg,H(l[2])); });
|
||||||
|
el('rect',{x:240,y:30,width:34,height:24,rx:3,fill:'#2C3238'},svg); el('text',{x:257,y:14,'text-anchor':'middle','font-family':f,'font-size':10,fill:ink},svg,'RUN');
|
||||||
|
[[-11,null,''],[0,'--w-yellow','P27'],[11,'--w-black','P28']].forEach(function(l){ el('rect',{x:257+l[0]-2.5,y:54,width:5,height:10,fill:'#C9CFD5'},svg); if(l[1]){ el('line',{x1:257+l[0],y1:64,x2:257+l[0],y2:96,stroke:css(l[1]),'stroke-width':4,'stroke-linecap':'round'},svg); el('text',{x:257+l[0],y:108,'text-anchor':'middle','font-family':f,'font-size':9,'font-weight':700,fill:ink},svg,H(l[2])); } });
|
||||||
|
el('text',{x:150,y:138,'text-anchor':'middle','font-family':f,'font-size':9,fill:m},svg,'lugs facing down, seen from the front');
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------- Wire map: every panel wire, by part, with its board hole ---------- */
|
||||||
|
var WMAP=[ // [pad, part, lug, short label]
|
||||||
|
['P29','Buttons','Row 1 · KICK, KICK 2, SNARE','ROW 1'],['P30','Buttons','Row 2 · HAT, HAT 16, PERC, GHOST','ROW 2'],['P31','Buttons','Row 3 · BASS, MEL A, MEL B, PAD','ROW 3'],
|
||||||
|
['P32','Buttons','Col 1 · KICK, HAT, BASS (diode band side)','COL 1'],['P33','Buttons','Col 2 · KICK 2, HAT 16, MEL A','COL 2'],['P34','Buttons','Col 3 · SNARE, PERC, MEL B','COL 3'],['P35','Buttons','Col 4 · GHOST, PAD','COL 4'],
|
||||||
|
['P26','SEED','one lug','SEED'],['P27','RUN toggle','middle lug (it sits in the button grid, row 1 / col 4)','RUN'],['P28','SEED + RUN','ground: SEED other lug, RUN one outer lug','GND BUS'],
|
||||||
|
['P20','Encoder','VCC (3.3 V, never 5 V)','ENC VCC'],['P21','Encoder','GND','ENC GND'],['P19','Encoder','SW','ENC SW'],['P18','Encoder','SIB','ENC SIB'],['P17','Encoder','SIA','ENC SIA'],
|
||||||
|
['P22','ENERGY pot','middle lug','ENERGY'],['P23','DRIFT pot','middle lug','DRIFT'],['P24','Pots','right lugs, both (3.3 V)','POTS +'],['P25','Pots','left lugs, both (AGND)','POTS −'],
|
||||||
|
['P14','LCD','1 · VCC (3.3 V)','LCD VCC'],['P15','LCD','2 · GND','LCD GND'],['P10','LCD','3 · SCE','LCD SCE'],['P9','LCD','4 · RST','LCD RST'],['P11','LCD','5 · D/C','LCD D/C'],['P12','LCD','6 · DN (MOSI)','LCD DN'],['P13','LCD','7 · SCLK','LCD SCLK'],['P16','LCD','8 · LED, through a 330 Ω resistor (if dark, move to any AA hole)','LCD LED'],
|
||||||
|
['P2','MIDI IN jack','tip','IN tip'],['P1','MIDI IN jack','ring','IN ring'],
|
||||||
|
['P3','OUT KIT jack','tip','KIT tip'],['P4','OUT KIT jack','ring','KIT ring'],['P5','OUT KIT jack','sleeve','KIT slv'],
|
||||||
|
['P6','OUT SYNTH jack','tip','SYN tip'],['P7','OUT SYNTH jack','ring','SYN ring'],['P8','OUT SYNTH jack','sleeve','SYN slv']
|
||||||
|
];
|
||||||
|
function wireOf(p){ return L.wires[+p.slice(1)-1]; }
|
||||||
|
function drawMap(){
|
||||||
|
var svg=document.getElementById('wmap'); if(!svg) return; svg.innerHTML='';
|
||||||
|
var LM=10.5, RM=10.5, f=css('--f-mono'), ink=css('--ink');
|
||||||
|
svg.setAttribute('viewBox',(-LM)+' -3.2 '+(L.W+LM+RM)+' '+(L.H+4.6));
|
||||||
|
el('rect',{x:-0.8,y:-0.5,width:L.W+0.6,height:L.H+0.3,rx:0.4,fill:css('--board'),stroke:css('--board-edge'),'stroke-width':0.12,opacity:0.55},svg);
|
||||||
|
for(var r=0;r<L.H;r++) el('line',{x1:-0.4,y1:r,x2:L.W-0.6,y2:r,stroke:css('--copper'),'stroke-width':0.5,opacity:0.18},svg);
|
||||||
|
el('rect',{x:11.3,y:-0.55,width:8.4,height:20.1,rx:0.4,fill:css('--part'),opacity:0.85},svg);
|
||||||
|
el('text',{x:15.5,y:9.5,'text-anchor':'middle','font-family':f,'font-size':'1.1','font-weight':700,fill:css('--part-ink')},svg,'PICO');
|
||||||
|
el('text',{x:15.5,y:-1.2,'text-anchor':'middle','font-family':f,'font-size':'0.7',fill:css('--muted')},svg,'USB edge');
|
||||||
|
el('rect',{x:3.6,y:19.6,width:3.8,height:3.8,rx:0.2,fill:css('--part'),opacity:0.85},svg);
|
||||||
|
el('text',{x:5.5,y:21.8,'text-anchor':'middle','font-family':f,'font-size':'0.6',fill:css('--part-ink')},svg,'opto');
|
||||||
|
// strip letters on both sides, hole numbers along the bottom
|
||||||
|
for(r=0;r<L.H;r++){ el('text',{x:-1.0,y:r+0.22,'text-anchor':'end','font-family':f,'font-size':'0.5',fill:css('--muted')},svg,rowName(r)); el('text',{x:L.W-0.1,y:r+0.22,'font-family':f,'font-size':'0.5',fill:css('--muted')},svg,rowName(r)); }
|
||||||
|
for(var c=0;c<L.W;c+=2) el('text',{x:c,y:L.H+0.9,'text-anchor':'middle','font-family':f,'font-size':'0.5',fill:css('--muted')},svg,String(c+1));
|
||||||
|
var sides={L:[],R:[]};
|
||||||
|
WMAP.forEach(function(m){ var w=wireOf(m[0]); (w.h[0]<=12?sides.L:sides.R).push({m:m,w:w}); });
|
||||||
|
['L','R'].forEach(function(sd){
|
||||||
|
var list=sides[sd].sort(function(a,b){ return a.w.h[1]-b.w.h[1] || (sd==='L'?b.w.h[0]-a.w.h[0]:a.w.h[0]-b.w.h[0]); }), last=-9, k;
|
||||||
|
for(k=0;k<list.length;k++){ list[k].ly=Math.max(list[k].w.h[1],last+1.05); last=list[k].ly; }
|
||||||
|
var cap=L.H-0.6; // keep labels inside the board's height: push the stack up from the bottom
|
||||||
|
for(k=list.length-1;k>=0;k--){ list[k].ly=Math.min(list[k].ly,cap); cap=list[k].ly-1.05; }
|
||||||
|
list.forEach(function(o){
|
||||||
|
var h=o.w.h, col=css(COL[o.w.col]), ly=o.ly;
|
||||||
|
var lx=sd==='L'?-1.9:L.W+0.9;
|
||||||
|
el('polyline',{points:h[0]+','+h[1]+' '+(sd==='L'?-0.6:L.W-0.4)+','+ly+' '+lx+','+ly,fill:'none',stroke:col,'stroke-width':0.13,opacity:0.9},svg);
|
||||||
|
el('circle',{cx:h[0],cy:h[1],r:0.36,fill:col,stroke:'#fff','stroke-width':0.08},svg);
|
||||||
|
var t=el('text',{x:sd==='L'?lx-0.25:lx+0.25,y:ly+0.25,'text-anchor':sd==='L'?'end':'start','font-family':f,'font-size':'0.72',fill:ink},svg);
|
||||||
|
var a=document.createElementNS(NS,'tspan'); a.setAttribute('font-weight','700'); a.textContent=o.m[3]+' '; t.appendChild(a);
|
||||||
|
var b=document.createElementNS(NS,'tspan'); b.setAttribute('fill',css('--accent')); b.setAttribute('font-weight','700'); b.textContent=pos(h); t.appendChild(b);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
function checklist(){
|
||||||
|
var tb=document.getElementById('wcheck'); if(!tb) return; tb.innerHTML='';
|
||||||
|
var done={}; try{ done=JSON.parse(localStorage.getItem('seedbox.wires')||'{}')||{}; }catch(e){}
|
||||||
|
var prev='';
|
||||||
|
WMAP.forEach(function(m){
|
||||||
|
var w=wireOf(m[0]), tr=document.createElement('tr'), part=m[1]===prev?'':m[1]; prev=m[1];
|
||||||
|
if(part) tr.className='grp';
|
||||||
|
var td0=document.createElement('td'), cb=document.createElement('input'); cb.type='checkbox'; cb.checked=!!done[m[0]]; cb.setAttribute('aria-label','Done: '+m[1]+' '+m[2]);
|
||||||
|
cb.addEventListener('change',function(){ done[m[0]]=cb.checked; tr.classList.toggle('done',cb.checked); try{ localStorage.setItem('seedbox.wires',JSON.stringify(done)); }catch(e){} });
|
||||||
|
td0.appendChild(cb); tr.appendChild(td0); tr.classList.toggle('done',cb.checked);
|
||||||
|
var td1=document.createElement('td'); td1.className='part'; td1.textContent=part; tr.appendChild(td1);
|
||||||
|
var td2=document.createElement('td'); td2.textContent=m[2]; tr.appendChild(td2);
|
||||||
|
var td3=document.createElement('td'), sw=document.createElement('span'); sw.className='sw'; sw.style.background='var('+COL[w.col]+')'; td3.appendChild(sw); td3.appendChild(document.createTextNode(w.col)); tr.appendChild(td3);
|
||||||
|
var td4=document.createElement('td'); td4.className='hole'; td4.textContent=pos(w.h); tr.appendChild(td4);
|
||||||
|
tb.appendChild(tr);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
function all(){
|
||||||
|
drawMap(); checklist(); drawTop(); drawBottom(); tables(); drawMatrix(); drawDin();
|
||||||
|
pinRow('lcd',[['1 VCC','--w-red','P14'],['2 GND','--w-black','P15'],['3 SCE','--w-blue','P10'],['4 RST','--w-blue','P9'],['5 D/C','--w-blue','P11'],['6 DN','--w-blue','P12'],['7 SCLK','--w-blue','P13'],['8 LED','--w-red','P16']]);
|
||||||
|
pinRow('enc',[['VCC','--w-red','P20'],['GND','--w-black','P21'],['SW','--w-green','P19'],['SIB','--w-green','P18'],['SIA','--w-green','P17']]);
|
||||||
|
drawMisc();
|
||||||
|
}
|
||||||
|
all();
|
||||||
|
try{ window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change',all); }catch(e){}
|
||||||
|
})();
|
||||||
501
tools/wiring.html
Normal file
@@ -0,0 +1,501 @@
|
|||||||
|
<title>Seedbox Wiring</title>
|
||||||
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||||
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=Hanken+Grotesk:wght@400;500;600;700&family=Martian+Mono:wght@400;500;700&display=swap" rel="stylesheet">
|
||||||
|
<style>
|
||||||
|
/* Layout: one column build guide. Board drawings first (component side, then copper side for cuts), then wire list, then panel wiring. */
|
||||||
|
:root{
|
||||||
|
--bg:#E8ECEF; --surface:#F6F7F9; --ink:#18202A; --muted:#566271; --line:#C3CBD3; --accent:#1E4C9E; --warn:#B4441F;
|
||||||
|
--board:#D9C79E; --board-edge:#B8A57A; --copper:#C98A4B; --hole:#5B4A30; --part:#2C3238; --part-ink:#E8ECEF;
|
||||||
|
--w-red:#D7263D; --w-black:#1E1E1E; --w-yellow:#D9A600; --w-green:#2E9E5B; --w-blue:#2F6FDB; --link:#7A8794;
|
||||||
|
--f-ui:'Hanken Grotesk', system-ui, -apple-system, 'Segoe UI', sans-serif;
|
||||||
|
--f-mono:'Martian Mono', ui-monospace, Menlo, Consolas, monospace;
|
||||||
|
}
|
||||||
|
@media (prefers-color-scheme: dark){ :root:not([data-theme="light"]){
|
||||||
|
--bg:#12161B; --surface:#1A1F25; --ink:#E2E7EC; --muted:#8C98A5; --line:#333B44; --accent:#8EB0F2; --warn:#F07B52;
|
||||||
|
--board:#6B5D40; --board-edge:#4F4430; --copper:#B97A3E; --hole:#231C10; --part:#0E1114; --part-ink:#D9E0E6;
|
||||||
|
--w-black:#8E98A3; --link:#9AA6B2; color-scheme:dark } }
|
||||||
|
:root[data-theme="dark"]{
|
||||||
|
--bg:#12161B; --surface:#1A1F25; --ink:#E2E7EC; --muted:#8C98A5; --line:#333B44; --accent:#8EB0F2; --warn:#F07B52;
|
||||||
|
--board:#6B5D40; --board-edge:#4F4430; --copper:#B97A3E; --hole:#231C10; --part:#0E1114; --part-ink:#D9E0E6;
|
||||||
|
--w-black:#8E98A3; --link:#9AA6B2; color-scheme:dark }
|
||||||
|
*{box-sizing:border-box}
|
||||||
|
body{background:var(--bg);color:var(--ink);font:15px/1.55 var(--f-ui);padding-block:28px 56px;padding-inline:16px}
|
||||||
|
.wrap{max-width:1040px;margin:0 auto;display:grid;gap:26px}
|
||||||
|
h1{font:700 24px/1.15 var(--f-mono);margin:0}
|
||||||
|
h2{font:700 13px var(--f-mono);letter-spacing:.08em;text-transform:uppercase;color:var(--accent);margin:0}
|
||||||
|
h3{font:700 15px var(--f-ui);margin:0}
|
||||||
|
.sub{color:var(--muted);margin:6px 0 0;max-width:74ch}
|
||||||
|
section{display:grid;gap:12px}
|
||||||
|
.card{background:var(--surface);border:1px solid var(--line);border-radius:10px;padding:14px 16px;display:grid;gap:10px;min-width:0}
|
||||||
|
.legend{display:flex;flex-wrap:wrap;gap:8px 18px;font:500 13px var(--f-ui)}
|
||||||
|
.legend span{display:inline-flex;align-items:center;gap:8px}
|
||||||
|
.legend i{display:inline-block;width:26px;height:6px;border-radius:3px}
|
||||||
|
.draw{background:var(--surface);border:1px solid var(--line);border-radius:10px;padding:10px;overflow-x:auto}
|
||||||
|
.draw svg{display:block;width:100%;min-width:640px;height:auto}
|
||||||
|
.cols{display:grid;grid-template-columns:repeat(auto-fit,minmax(300px,1fr));gap:14px}
|
||||||
|
.tbl{overflow-x:auto;background:var(--surface);border:1px solid var(--line);border-radius:10px}
|
||||||
|
table{width:100%;border-collapse:collapse;min-width:560px}
|
||||||
|
th{font:600 10px var(--f-mono);letter-spacing:.08em;text-transform:uppercase;color:var(--muted);text-align:left;padding:9px 12px;border-bottom:1px solid var(--line)}
|
||||||
|
td{padding:8px 12px;border-bottom:1px solid var(--line);font-size:14px;vertical-align:top}
|
||||||
|
tr:last-child td{border-bottom:0}
|
||||||
|
td.m{font:500 13px var(--f-mono);white-space:nowrap;font-variant-numeric:tabular-nums}
|
||||||
|
.sw{display:inline-block;width:12px;height:12px;border-radius:3px;vertical-align:-1px;margin-right:6px}
|
||||||
|
ol,ul{margin:0;padding-left:20px;display:grid;gap:5px}
|
||||||
|
li{max-width:78ch}
|
||||||
|
.note{color:var(--muted);font-size:13px;margin:0;max-width:78ch}
|
||||||
|
.warn{color:var(--warn);font-weight:600}
|
||||||
|
code{font:500 12.5px var(--f-mono)}
|
||||||
|
table.check td{padding:7px 12px}
|
||||||
|
table.check td.hole{font:700 18px var(--f-mono);color:var(--accent)}
|
||||||
|
table.check td.part{font-weight:600;white-space:nowrap}
|
||||||
|
table.check tr.grp td{border-top:2px solid var(--line)}
|
||||||
|
table.check tr.done td{opacity:.45}
|
||||||
|
table.check tr.done td.hole{text-decoration:line-through}
|
||||||
|
table.check input{width:20px;height:20px}
|
||||||
|
#wmap{min-width:720px}
|
||||||
|
</style>
|
||||||
|
<div class="wrap">
|
||||||
|
<header>
|
||||||
|
<h1>Seedbox wiring · v0.1</h1>
|
||||||
|
<p class="sub">One stripboard carries the Pico and the MIDI circuits. Everything on the panel (screen, encoder, pots, buttons, jacks) connects to it with hookup wire. Board positions are given as strip letter + hole number, e.g. <code>X2</code> is strip X, hole 2, counted from the left with the parts facing you.</p>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<section>
|
||||||
|
<h2>Wire map · panel to board</h2>
|
||||||
|
<p class="sub">Every panel wire, where it lands. Big blue text is the board hole: strip letter, then hole number counted from the opto end (the end away from the Pico's USB).</p>
|
||||||
|
<div class="draw"><svg id="wmap" role="img" aria-label="Board with every panel wire labelled at its hole"></svg></div>
|
||||||
|
<div class="tbl"><table class="check"><thead><tr><th>Done</th><th>Part</th><th>Lug</th><th>Wire</th><th>Board hole</th></tr></thead><tbody id="wcheck"></tbody></table></div>
|
||||||
|
<p class="note">Ticks are saved in this browser. Lug drawings for each part are under Panel wiring further down.</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section>
|
||||||
|
<h2>Wire colours</h2>
|
||||||
|
<div class="card">
|
||||||
|
<div class="legend">
|
||||||
|
<span><i style="background:var(--w-red)"></i>Red · 3.3 V and the MIDI OUT supply pins</span>
|
||||||
|
<span><i style="background:var(--w-black)"></i>Black · ground</span>
|
||||||
|
<span><i style="background:var(--w-yellow)"></i>Yellow · button rows, SEED, RUN</span>
|
||||||
|
<span><i style="background:var(--w-green)"></i>Green · button columns, encoder</span>
|
||||||
|
<span><i style="background:var(--w-blue)"></i>Blue · screen, pots, MIDI signals</span>
|
||||||
|
<span><i style="background:var(--link)"></i>Grey · short links on the board</span>
|
||||||
|
</div>
|
||||||
|
<p class="note">If your five spools are other colours, keep the roles and rename them. Red and black for power and ground matter most; the rest just keep bundles readable.</p>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section>
|
||||||
|
<h2>Board · parts side</h2>
|
||||||
|
<div class="draw"><svg id="top" role="img" aria-label="Stripboard seen from the parts side"></svg></div>
|
||||||
|
<p class="note">Copper strips run left to right underneath (shown faintly). Board size 34 holes × 28 strips (about 86 × 71 mm). The Pico sits in two 20-pin female headers with pins 1 and 40 on strip A, so its USB end is flush with the top edge of the board and the micro-USB plug reaches out past it: line that edge up with a slot in the box wall. Numbered dots <b>P1</b>… (pads) are where off-board wires solder; the wire map at the top says where each one goes.</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section>
|
||||||
|
<h2>Board · copper side, for cutting</h2>
|
||||||
|
<div class="draw"><svg id="bottom" role="img" aria-label="Stripboard seen from the copper side with cuts marked"></svg></div>
|
||||||
|
<div class="cols">
|
||||||
|
<div class="card"><h3>Track cuts</h3><ul id="cutlist"></ul>
|
||||||
|
<p class="note">This view is flipped: hole 1 is on the <b>right</b>. Cut with a 3–4 mm drill bit twisted by hand or a spot-face cutter, then check each cut with the meter's continuity beeper.</p></div>
|
||||||
|
<div class="card"><h3>Power strips along the bottom</h3>
|
||||||
|
<ul><li><b>Z</b> · 5 V from the Pico's VBUS (only the opto uses it)</li><li><b>AA</b> · ground</li><li><b>AB</b> · 3.3 V from the Pico's 3V3 pin</li></ul>
|
||||||
|
<p class="note">Every red wire to the panel starts on AB, every black wire on AA (or on the AGND strip for the pots).</p></div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section>
|
||||||
|
<h2>Board parts and links</h2>
|
||||||
|
<div class="tbl"><table><thead><tr><th>Part</th><th>Value</th><th>Holes</th><th>Note</th></tr></thead><tbody id="partlist"></tbody></table></div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section>
|
||||||
|
<h2>Off-board wires</h2>
|
||||||
|
<div class="tbl"><table><thead><tr><th>#</th><th>Board hole</th><th>Colour</th><th>Pico pin / signal</th><th>Goes to</th></tr></thead><tbody id="wirelist"></tbody></table></div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section>
|
||||||
|
<h2>Panel wiring</h2>
|
||||||
|
<div class="cols">
|
||||||
|
<div class="card" style="grid-column:1/-1"><h3>Button matrix · seen from the back</h3>
|
||||||
|
<div class="draw" style="padding:6px"><svg id="matrix" role="img" aria-label="Button matrix wiring"></svg></div>
|
||||||
|
<p class="note">Each button: one lug to its <b>row</b> wire (yellow, daisy-chained along the row), the other lug through a diode to its <b>column</b> wire (green, daisy-chained down the column). The diode's band points to the column. Solder the diodes straight onto the button lugs and sleeve them. 11 track buttons, each with its own diode, plus D1 on the board: 12 diodes. GHOST is wired at row 2 / column 4 (where FILL used to be). The RUN toggle sits in the grid's row 1 / column 4 spot but is not part of the matrix: row 1's wire stops at SNARE. <span class="warn">The drawing is from behind the panel, the side you solder on: KICK is top-right, RUN top-left.</span></p></div>
|
||||||
|
<div class="card"><h3>MIDI jacks · 3.5 mm TRS, Type A</h3>
|
||||||
|
<svg id="din" viewBox="0 0 300 150" role="img" aria-label="TRS jack lugs seen from inside the box"></svg>
|
||||||
|
<ul>
|
||||||
|
<li><b>MIDI IN</b>: ring → Y1, tip → W1. Sleeve stays empty.</li>
|
||||||
|
<li><b>OUT KIT</b>: tip → X26, ring → V22, sleeve → AA22.</li>
|
||||||
|
<li><b>OUT SYNTH</b>: tip → U31, ring → V31, sleeve → AA31.</li>
|
||||||
|
</ul>
|
||||||
|
<p class="note">Use stereo (TRS) jacks; mono ones have no ring. Lug order differs between jack models, so find the tip, ring and sleeve lugs with the meter and a plug inserted. Type A is the MIDI standard: a plain stereo 3.5 mm cable goes straight to another Type A device (e.g. SP-404MKII). A DIN device (e.g. MiniFreak) needs a TRS-to-DIN adapter that says <b>Type A</b>; a Type B one swaps tip and ring and gives silence (nothing breaks).</p></div>
|
||||||
|
<div class="card"><h3>Nokia 5110 module</h3>
|
||||||
|
<svg id="lcd" viewBox="0 0 300 92" role="img" aria-label="LCD header pins"></svg>
|
||||||
|
<ul>
|
||||||
|
<li>Pin order as printed on your module: 1 VCC, 2 GND, 3 SCE, 4 RST, 5 D/C, 6 DN (MOSI), 7 SCLK, 8 LED.</li>
|
||||||
|
<li><b>LED</b> (backlight): wire it through a 330 Ω resistor to AB22 (3.3 V); put the resistor inline at the module end and sleeve it. On boards with this pin order that lights it. If it stays dark, move that wire to any free hole on strip AA (ground). GP22 (L27) is left free: it's only needed if you ever want dimming, through a transistor.</li>
|
||||||
|
<li><b>SCE</b> can go straight to ground instead of S27 if it's the only thing on the SPI bus; that frees GP17.</li>
|
||||||
|
</ul></div>
|
||||||
|
<div class="card"><h3>Encoder module</h3>
|
||||||
|
<svg id="enc" viewBox="0 0 300 80" role="img" aria-label="Encoder module pins"></svg>
|
||||||
|
<p class="note">Pin order as printed on your module: VCC, GND, SW, SIB, SIA. Power it from 3.3 V (AB4), never 5 V: its pull-up resistors would put 5 V on the Pico's pins. If turning it clockwise steps the wrong way, swap SIA and SIB (or flip it in firmware).</p></div>
|
||||||
|
<div class="card"><h3>Pots, SEED, RUN</h3>
|
||||||
|
<svg id="misc" viewBox="0 0 300 150" role="img" aria-label="Pots, SEED and RUN wiring"></svg>
|
||||||
|
<ul>
|
||||||
|
<li>Pots, lugs facing down, shaft toward you: left lug to AGND (H27), middle to its pin, right lug to 3.3 V (AB25). Chain the left lugs together and the right lugs together.</li>
|
||||||
|
<li>SEED: one lug to Q2, the other to the ground bus (AA6).</li>
|
||||||
|
<li>RUN: middle lug to S2, one outer lug to the ground bus. If RUN and STOP come out swapped, use the other outer lug.</li>
|
||||||
|
</ul></div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section>
|
||||||
|
<h2>Build order and checks</h2>
|
||||||
|
<div class="cols">
|
||||||
|
<div class="card"><h3>Board</h3>
|
||||||
|
<ol>
|
||||||
|
<li>Cut the board to 34 × 28 holes. For the top edge, cut through the row of holes above strip A, then sand it back to about 1.3 mm from the strip A holes (stop before the strip A copper). That puts the Pico's edge flush with the board; unsanded, the Pico sits about 1 mm back, which still works if the wall slot lets the plug's moulding through. Make the track cuts and beep each one out.</li>
|
||||||
|
<li>Solder the two Pico headers, the DIP-8 socket (notch toward strip U), then the resistors and the diode (band on V4). Leave C1 for step 4.</li>
|
||||||
|
<li>Solder the grey links with insulated wire. The two long ones (GP0 and GP4) run straight down <b>under the Pico, between its two headers</b>: fit them before the Pico goes in.</li>
|
||||||
|
<li>Before fitting C1, with the Pico and the 6N137 out: AB to AA must <b>not</b> beep, Z to AA must not beep. Then fit C1 (it can chirp briefly on the beeper once it is in).</li>
|
||||||
|
</ol></div>
|
||||||
|
<div class="card"><h3>First power</h3>
|
||||||
|
<ol>
|
||||||
|
<li>Plug in the Pico only, power from USB. Measure AB to AA: about 3.3 V. Z to AA: about 5 V.</li>
|
||||||
|
<li>Unplug, insert the 6N137 (notch toward strip U, pin 1 at hole U5).</li>
|
||||||
|
<li>Wire the panel. Before closing the box, check that no bare lugs touch each other.</li>
|
||||||
|
<li>The box needs the Pico firmware (the C port of this simulator) before it does anything. That's the next step. Firmware note: GP0 and GP4 drive the MIDI loops directly (about 6–8 mA), above the RP2350's default 4 mA drive, so set them to 8–12 mA with <code>gpio_set_drive_strength()</code>.</li>
|
||||||
|
</ol></div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
<script>
|
||||||
|
/* Stripboard layout for Seedbox v0.1. Holes are [col,row], 0-based, seen from the COMPONENT side.
|
||||||
|
Copper strips run left to right along each row.
|
||||||
|
The Pico's pin 1 and 40 sit on the first strip (A), so its USB end is flush with the board edge. */
|
||||||
|
var LAYOUT=(function(){
|
||||||
|
var W=34, H=28;
|
||||||
|
var cuts=[];
|
||||||
|
function cut(c,r0,r1){ for(var r=r0;r<=r1;r++) cuts.push([c,r]); }
|
||||||
|
cut(15,0,19); // under the Pico: separates its left and right pin rows
|
||||||
|
cut(5,20,24); // opto: left pins / right pins, IN4 strip
|
||||||
|
cut(13,20,23); // opto area / MIDI OUT area (strip Y stays whole: it carries GP4)
|
||||||
|
cut(24,20,22); // MIDI OUT A / MIDI OUT B
|
||||||
|
cut(22,23,23); // strip X: GP0 side / KIT tip side, R4 lies across this cut
|
||||||
|
var LP=['GP0','GP1','GND','GP2','GP3','GP4','GP5','GND','GP6','GP7','GP8','GP9','GND','GP10','GP11','GP12','GP13','GND','GP14','GP15'];
|
||||||
|
var RP=['VBUS','VSYS','GND','3V3_EN','3V3','ADC_VREF','GP28','AGND','GP27','GP26','RUN','GP22','GND','GP21','GP20','GP19','GP18','GND','GP17','GP16'];
|
||||||
|
var pico={id:'pico',kind:'pico',pins:[]};
|
||||||
|
for(var i=0;i<20;i++){ pico.pins.push({h:[12,i],n:LP[i],num:i+1}); pico.pins.push({h:[19,i],n:RP[i],num:40-i}); }
|
||||||
|
var parts=[pico,
|
||||||
|
{id:'U1',kind:'dip8',label:'6N137',pins:[{h:[4,20],n:'1 NC'},{h:[4,21],n:'2 A'},{h:[4,22],n:'3 K'},{h:[4,23],n:'4 NC'},{h:[7,23],n:'5 GND'},{h:[7,22],n:'6 VO'},{h:[7,21],n:'7 VE'},{h:[7,20],n:'8 VCC'}]},
|
||||||
|
{id:'R1',kind:'res',label:'220 Ω',pins:[{h:[1,21]},{h:[1,24]}]},
|
||||||
|
{id:'D1',kind:'diode',label:'1N4148',pins:[{h:[3,21],n:'K'},{h:[3,22],n:'A'}]},
|
||||||
|
{id:'R2',kind:'res',label:'1 kΩ',pins:[{h:[9,22]},{h:[9,27]}]},
|
||||||
|
{id:'C1',kind:'cap',label:'100 nF',pins:[{h:[10,20]},{h:[10,23]}]},
|
||||||
|
{id:'R4',kind:'res',label:'10 Ω',pins:[{h:[20,23]},{h:[24,23]}]},
|
||||||
|
{id:'R5',kind:'res',label:'33 Ω ½ W',pins:[{h:[18,21]},{h:[18,27]}]},
|
||||||
|
{id:'R6',kind:'res',label:'10 Ω',pins:[{h:[27,20]},{h:[27,24]}]},
|
||||||
|
{id:'R7',kind:'res',label:'33 Ω ½ W',pins:[{h:[29,21]},{h:[29,27]}]}
|
||||||
|
];
|
||||||
|
var jumpers=[
|
||||||
|
{a:[33,0],b:[33,25],n:'5 V (VBUS) to the 5 V strip'},
|
||||||
|
{a:[31,4],b:[31,27],n:'3V3 to the 3V3 strip'},
|
||||||
|
{a:[32,17],b:[32,26],n:'GND to the GND strip'},
|
||||||
|
{a:[12,20],b:[12,25],n:'opto VCC to 5 V'},
|
||||||
|
{a:[9,20],b:[9,21],n:'opto enable (pin 7) to VCC (pin 8): the 6N137 output only works with pin 7 high'},
|
||||||
|
{a:[11,23],b:[11,26],n:'opto GND to GND'},
|
||||||
|
{a:[8,22],b:[8,1],n:'opto out to GP1 (MIDI RX), strip B'},
|
||||||
|
{a:[14,0],b:[14,23],n:'GP0 (TX KIT) to strip X (R4), straight down under the Pico between its headers'},
|
||||||
|
{a:[13,5],b:[13,24],n:'GP4 (TX SYNTH) to strip Y (R6), straight down under the Pico between its headers'}
|
||||||
|
];
|
||||||
|
/* Off-board wires. col = spool colour role. to = where the other end goes. */
|
||||||
|
var wires=[
|
||||||
|
// MIDI jacks: 3.5 mm TRS, Type A (tip = DIN pin 5, ring = pin 4, sleeve = pin 2)
|
||||||
|
{h:[0,24],col:'blue',to:'MIDI IN jack · ring (pin 4)'},
|
||||||
|
{h:[0,22],col:'blue',to:'MIDI IN jack · tip (pin 5)'},
|
||||||
|
{h:[25,23],col:'blue',to:'OUT KIT jack · tip (pin 5)'},
|
||||||
|
{h:[21,21],col:'red',to:'OUT KIT jack · ring (pin 4)'},
|
||||||
|
{h:[21,26],col:'black',to:'OUT KIT jack · sleeve (pin 2)'},
|
||||||
|
{h:[30,20],col:'blue',to:'OUT SYNTH jack · tip (pin 5)'},
|
||||||
|
{h:[30,21],col:'red',to:'OUT SYNTH jack · ring (pin 4)'},
|
||||||
|
{h:[30,26],col:'black',to:'OUT SYNTH jack · sleeve (pin 2)'},
|
||||||
|
// screen
|
||||||
|
{h:[26,13],col:'blue',to:'LCD · RST (pin 4)'},
|
||||||
|
{h:[26,18],col:'blue',to:'LCD · SCE (pin 3)'},
|
||||||
|
{h:[26,14],col:'blue',to:'LCD · D/C (pin 5)'},
|
||||||
|
{h:[26,15],col:'blue',to:'LCD · DN/MOSI (pin 6)'},
|
||||||
|
{h:[26,16],col:'blue',to:'LCD · SCLK (pin 7)'},
|
||||||
|
{h:[22,27],col:'red',to:'LCD · VCC (pin 1)'},
|
||||||
|
{h:[22,26],col:'black',to:'LCD · GND (pin 2)'},
|
||||||
|
{h:[21,27],col:'red',to:'LCD · LED (pin 8) through 330 Ω'},
|
||||||
|
// encoder
|
||||||
|
{h:[1,13],col:'green',to:'Encoder · SIA'},
|
||||||
|
{h:[1,14],col:'green',to:'Encoder · SIB'},
|
||||||
|
{h:[1,15],col:'green',to:'Encoder · SW'},
|
||||||
|
{h:[3,27],col:'red',to:'Encoder · VCC'},
|
||||||
|
{h:[3,26],col:'black',to:'Encoder · GND'},
|
||||||
|
// pots
|
||||||
|
{h:[26,9],col:'blue',to:'ENERGY pot · middle lug'},
|
||||||
|
{h:[26,8],col:'blue',to:'DRIFT pot · middle lug'},
|
||||||
|
{h:[24,27],col:'red',to:'Pots · right lugs (chain both)'},
|
||||||
|
{h:[26,7],col:'black',to:'Pots · left lugs (chain both)'},
|
||||||
|
// buttons
|
||||||
|
{h:[1,16],col:'yellow',to:'SEED · one lug'},
|
||||||
|
{h:[1,18],col:'yellow',to:'RUN toggle · middle lug'},
|
||||||
|
{h:[5,26],col:'black',to:'Panel ground bus · SEED other lug, RUN one outer lug'},
|
||||||
|
{h:[1,3],col:'yellow',to:'Matrix row 1 (KICK, KICK 2, SNARE)'},
|
||||||
|
{h:[1,4],col:'yellow',to:'Matrix row 2 (HAT, HAT 16, PERC, GHOST)'},
|
||||||
|
{h:[1,6],col:'yellow',to:'Matrix row 3 (BASS, MEL A, MEL B, PAD)'},
|
||||||
|
{h:[1,8],col:'green',to:'Matrix column 1 (KICK, HAT, BASS)'},
|
||||||
|
{h:[1,9],col:'green',to:'Matrix column 2 (KICK 2, HAT 16, MEL A)'},
|
||||||
|
{h:[1,10],col:'green',to:'Matrix column 3 (SNARE, PERC, MEL B)'},
|
||||||
|
{h:[1,11],col:'green',to:'Matrix column 4 (GHOST, PAD)'}
|
||||||
|
];
|
||||||
|
return {W:W,H:H,cuts:cuts,parts:parts,jumpers:jumpers,wires:wires};
|
||||||
|
})();
|
||||||
|
if(typeof module!=='undefined') module.exports=LAYOUT;
|
||||||
|
|
||||||
|
</script><script>(function(){
|
||||||
|
var L=LAYOUT, NS='http://www.w3.org/2000/svg';
|
||||||
|
function css(n){ return getComputedStyle(document.documentElement).getPropertyValue(n).trim(); }
|
||||||
|
function el(tag,a,p,txt){ var e=document.createElementNS(NS,tag); for(var k in a) e.setAttribute(k,a[k]); if(txt!=null) e.textContent=txt; p.appendChild(e); return e; }
|
||||||
|
function rowName(r){ return r<26 ? String.fromCharCode(65+r) : 'A'+String.fromCharCode(65+r-26); }
|
||||||
|
function pos(h){ return rowName(h[1])+(h[0]+1); }
|
||||||
|
function H(p){ return /^P\d+$/.test(p) ? pos(L.wires[+p.slice(1)-1].h) : p; }
|
||||||
|
var COL={red:'--w-red',black:'--w-black',yellow:'--w-yellow',green:'--w-green',blue:'--w-blue'};
|
||||||
|
var cutKey={}; L.cuts.forEach(function(c){ cutKey[c[0]+','+c[1]]=1; });
|
||||||
|
function strips(svg,X,C,strong){
|
||||||
|
for(var r=0;r<L.H;r++){
|
||||||
|
var start=0;
|
||||||
|
for(var c=0;c<=L.W;c++){
|
||||||
|
if(c===L.W || cutKey[c+','+r]){
|
||||||
|
if(c>start){ var x0=Math.min(X(start),X(c-1))-0.42, x1=Math.max(X(start),X(c-1))+0.42;
|
||||||
|
el('rect',{x:x0,y:r-0.36,width:x1-x0,height:0.72,rx:0.2,fill:C.copper,opacity:strong?1:0.28},svg); }
|
||||||
|
start=c+1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function holes(svg,X,C){ for(var r=0;r<L.H;r++) for(var c=0;c<L.W;c++) el('circle',{cx:X(c),cy:r,r:0.16,fill:C.hole},svg); }
|
||||||
|
function labels(svg,X,C,flip){
|
||||||
|
var g=el('g',{'font-family':css('--f-mono'),'font-size':'0.55',fill:C.muted},svg);
|
||||||
|
for(var c=0;c<L.W;c++) if(c%2===0 || c===L.W-1){ el('text',{x:X(c),y:-1.75,'text-anchor':'middle'},g,String(c+1)); el('text',{x:X(c),y:L.H+0.9,'text-anchor':'middle'},g,String(c+1)); }
|
||||||
|
for(var r=0;r<L.H;r++){ el('text',{x:-1.3,y:r+0.2,'text-anchor':'middle'},g,rowName(r)); el('text',{x:L.W+0.3,y:r+0.2,'text-anchor':'middle'},g,rowName(r)); }
|
||||||
|
}
|
||||||
|
function frame(svg){ svg.setAttribute('viewBox','-2.4 -2.6 '+(L.W+3.8)+' '+(L.H+4.0)); }
|
||||||
|
|
||||||
|
var U1label=(L.parts.filter(function(p){ return p.id==='U1'; })[0]||{}).label||'';
|
||||||
|
function drawTop(){
|
||||||
|
var svg=document.getElementById('top'); svg.innerHTML=''; frame(svg);
|
||||||
|
var C={copper:css('--copper'),hole:css('--hole'),muted:css('--muted'),part:css('--part'),pink:css('--part-ink'),link:css('--link'),warn:css('--warn'),board:css('--board'),edge:css('--board-edge'),ink:css('--ink')};
|
||||||
|
var X=function(c){ return c; };
|
||||||
|
el('rect',{x:-0.8,y:-0.5,width:L.W+0.6,height:L.H+0.3,rx:0.4,fill:C.board,stroke:C.edge,'stroke-width':0.12},svg);
|
||||||
|
strips(svg,X,C,false); holes(svg,X,C); labels(svg,X,C);
|
||||||
|
L.cuts.forEach(function(c){ var g={stroke:C.warn,'stroke-width':0.1,opacity:0.7}; el('line',Object.assign({x1:c[0]-0.22,y1:c[1]-0.22,x2:c[0]+0.22,y2:c[1]+0.22},g),svg); el('line',Object.assign({x1:c[0]-0.22,y1:c[1]+0.22,x2:c[0]+0.22,y2:c[1]-0.22},g),svg); });
|
||||||
|
var f=css('--f-mono');
|
||||||
|
L.parts.forEach(function(p){
|
||||||
|
if(p.kind==='pico'){
|
||||||
|
el('rect',{x:11.3,y:-0.55,width:8.4,height:20.1,rx:0.4,fill:C.part,opacity:0.92},svg);
|
||||||
|
el('rect',{x:14.3,y:-1.35,width:2.4,height:1.35,rx:0.2,fill:'#9AA3AD'},svg); el('text',{x:15.5,y:-0.55,'text-anchor':'middle','font-family':f,'font-size':'0.42',fill:'#111'},svg,'USB');
|
||||||
|
el('text',{x:15.5,y:1.6,'text-anchor':'middle','font-family':f,'font-size':'0.62',fill:C.pink},svg,'PICO 2W');
|
||||||
|
p.pins.forEach(function(pp){ var left=pp.h[0]===12;
|
||||||
|
el('rect',{x:pp.h[0]-0.32,y:pp.h[1]-0.32,width:0.64,height:0.64,fill:'#111',stroke:'#D4AF37','stroke-width':0.08},svg);
|
||||||
|
el('text',{x:left?pp.h[0]+0.55:pp.h[0]-0.55,y:pp.h[1]+0.16,'text-anchor':left?'start':'end','font-family':f,'font-size':'0.42',fill:C.pink},svg,pp.n); });
|
||||||
|
} else if(p.kind==='dip8'){
|
||||||
|
el('rect',{x:4.25,y:19.55,width:2.5,height:3.9,rx:0.15,fill:C.part},svg);
|
||||||
|
el('path',{d:'M 5 19.55 a 0.5 0.5 0 0 0 1 0',fill:'#555'},svg);
|
||||||
|
el('text',{x:5.5,y:21.7,'text-anchor':'middle','font-family':f,'font-size':'0.42',fill:C.pink,transform:'rotate(90 5.5 21.6)'},svg,U1label);
|
||||||
|
p.pins.forEach(function(pp){ el('rect',{x:pp.h[0]-0.25,y:pp.h[1]-0.18,width:0.5,height:0.36,fill:'#BFC6CD'},svg); });
|
||||||
|
el('text',{x:3.55,y:20.15,'text-anchor':'end','font-family':f,'font-size':'0.4',fill:C.ink},svg,'1');
|
||||||
|
} else {
|
||||||
|
var a=p.pins[0].h, b=p.pins[1].h, horiz=a[1]===b[1], mx=(a[0]+b[0])/2, my=(a[1]+b[1])/2, len=horiz?Math.abs(b[0]-a[0]):Math.abs(b[1]-a[1]);
|
||||||
|
el('line',{x1:a[0],y1:a[1],x2:b[0],y2:b[1],stroke:'#9AA3AD','stroke-width':0.12},svg);
|
||||||
|
var bw=p.kind==='cap'?0.7:0.62, bh=Math.min(len-0.5,p.kind==='diode'?0.7:2.2);
|
||||||
|
var body=p.kind==='res'?'#C9A26B':p.kind==='diode'?'#D9663A':'#3D7BC9';
|
||||||
|
if(horiz){ el('rect',{x:mx-bh/2,y:my-bw/2,width:bh,height:bw,rx:0.25,fill:body,stroke:'#00000055','stroke-width':0.05},svg);
|
||||||
|
el('text',{x:mx,y:my-0.5,'text-anchor':'middle','font-family':f,'font-size':'0.42',fill:C.ink},svg,p.id); }
|
||||||
|
else el('rect',{x:a[0]-bw/2,y:my-bh/2,width:bw,height:bh,rx:0.25,fill:body,stroke:'#00000055','stroke-width':0.05,'stroke-dasharray':p.opt?'0.2 0.12':''},svg);
|
||||||
|
if(p.kind==='diode'){ var ky=p.pins[0].n==='K'?p.pins[0].h[1]:p.pins[1].h[1]; el('rect',{x:a[0]-bw/2,y:(ky<my?my-bh/2:my+bh/2-0.16),width:bw,height:0.16,fill:'#111'},svg); }
|
||||||
|
if(!horiz) el('text',{x:a[0]+0.48,y:my+0.15,'font-family':f,'font-size':'0.42',fill:C.ink},svg,p.id+(p.opt?'*':''));
|
||||||
|
}
|
||||||
|
p.pins.forEach(function(pp){ if(p.kind!=='pico') el('circle',{cx:pp.h[0],cy:pp.h[1],r:0.13,fill:'#D4AF37'},svg); });
|
||||||
|
});
|
||||||
|
L.jumpers.forEach(function(j){
|
||||||
|
el('line',{x1:j.a[0],y1:j.a[1],x2:j.b[0],y2:j.b[1],stroke:C.link,'stroke-width':0.2,'stroke-linecap':'round',opacity:0.9},svg);
|
||||||
|
[j.a,j.b].forEach(function(h){ el('circle',{cx:h[0],cy:h[1],r:0.2,fill:C.link},svg); });
|
||||||
|
});
|
||||||
|
L.wires.forEach(function(w,i){
|
||||||
|
var col=css(COL[w.col]);
|
||||||
|
el('circle',{cx:w.h[0],cy:w.h[1],r:0.3,fill:col,stroke:'#fff','stroke-width':0.07},svg);
|
||||||
|
var right=w.h[0]>=20, tx=w.h[0]+(right?0.42:-0.42);
|
||||||
|
el('text',{x:tx,y:w.h[1]-0.3,'text-anchor':right?'start':'end','font-family':f,'font-size':'0.48','font-weight':'700',fill:C.ink},svg,'P'+(i+1));
|
||||||
|
});
|
||||||
|
var rails=[[25,'5 V'],[26,'GND'],[27,'3V3']];
|
||||||
|
rails.forEach(function(r){ el('text',{x:L.W+1.15,y:r[0]+0.2,'font-family':f,'font-size':'0.45',fill:C.warn},svg,r[1]); });
|
||||||
|
}
|
||||||
|
function drawBottom(){
|
||||||
|
var svg=document.getElementById('bottom'); svg.innerHTML=''; frame(svg);
|
||||||
|
var C={copper:css('--copper'),hole:css('--hole'),muted:css('--muted'),warn:css('--warn'),board:css('--board'),edge:css('--board-edge'),part:css('--part')};
|
||||||
|
var X=function(c){ return L.W-1-c; };
|
||||||
|
el('rect',{x:-0.8,y:-0.5,width:L.W+0.6,height:L.H+0.3,rx:0.4,fill:C.board,stroke:C.edge,'stroke-width':0.12},svg);
|
||||||
|
strips(svg,X,C,true); holes(svg,X,C); labels(svg,X,C);
|
||||||
|
el('rect',{x:X(19)-0.5,y:-0.55,width:8.4,height:20.1,rx:0.4,fill:'none',stroke:C.muted,'stroke-width':0.08,'stroke-dasharray':'0.3 0.2'},svg);
|
||||||
|
el('text',{x:X(15.5),y:-0.85,'text-anchor':'middle','font-family':css('--f-mono'),'font-size':'0.42',fill:C.muted},svg,'Pico outline (other side), USB at this edge');
|
||||||
|
L.cuts.forEach(function(c){ var x=X(c[0]);
|
||||||
|
el('rect',{x:x-0.45,y:c[1]-0.45,width:0.9,height:0.9,rx:0.15,fill:C.board},svg);
|
||||||
|
el('circle',{cx:x,cy:c[1],r:0.34,fill:'none',stroke:C.warn,'stroke-width':0.14},svg); });
|
||||||
|
var cl=document.getElementById('cutlist'); cl.innerHTML='';
|
||||||
|
var groups={}; L.cuts.forEach(function(c){ (groups[c[0]]=groups[c[0]]||[]).push(c[1]); });
|
||||||
|
Object.keys(groups).forEach(function(k){ var rs=groups[k].sort(function(a,b){return a-b;}); var li=document.createElement('li');
|
||||||
|
li.textContent='Hole '+(+k+1)+(rs.length>1?' on strips '+rowName(rs[0])+' to '+rowName(rs[rs.length-1])+' ('+rs.length+' cuts)':' on strip '+rowName(rs[0])+' (1 cut)'); cl.appendChild(li); });
|
||||||
|
}
|
||||||
|
function tables(){
|
||||||
|
var pl=document.getElementById('partlist'); pl.innerHTML='';
|
||||||
|
function row(tb,cells,mcols){ var tr=document.createElement('tr'); cells.forEach(function(c,i){ var td=document.createElement('td'); if(mcols.indexOf(i)>=0) td.className='m'; if(c && c.nodeType) td.appendChild(c); else td.textContent=c; tr.appendChild(td); }); tb.appendChild(tr); }
|
||||||
|
var notes={U1:'6N137 in a DIP-8 socket. Notch toward strip U, pin 1 at hole U5. Pin 7 (enable) is tied to pin 8 by the U10–V10 link.',R1:'MIDI IN current limit. 3-hole span: stand it upright, or use a small ⅛ W part flat. If learning ever misses notes, use 150 Ω.',D1:'Band (cathode) on V4, the opto LED anode side. Stands upright (1-hole span).',R2:'Opto output pull-up to 3.3 V',C1:'6N137 supply bypass, right beside the chip: U11 is its 5 V (pin 8) strip, X11 its GND (pin 5) strip. Required. Fit it after the beep test.',R4:'MIDI OUT KIT, TX line (RC). Lies flat along strip X, across the cut at X23 (4-hole span, ¼ W fits). SEED hangs right above it.',R5:'MIDI OUT KIT, 3.3 V line (RA). ½ W: a mono plug in the jack shorts it to ground.',R6:'MIDI OUT SYNTH, TX line (RC)',R7:'MIDI OUT SYNTH, 3.3 V line (RA). ½ W, same reason as R5.'};
|
||||||
|
row(pl,['Pico 2W','2 × 20-pin female header','A13–T13 and A20–T20 (pins 1–20 down the left, 40–21 down the right)','USB end flush with the top edge of the board, past strip A'],[2]);
|
||||||
|
L.parts.forEach(function(p){ if(p.kind==='pico') return;
|
||||||
|
row(pl,[p.id,p.label,p.kind==='dip8'?'U5–X5, U8–X8':p.pins.map(function(pp){ return pos(pp.h); }).join(' – '),notes[p.id]||''],[2]); });
|
||||||
|
L.jumpers.forEach(function(j){ row(pl,['Link','wire',pos(j.a)+' – '+pos(j.b),j.n],[2]); });
|
||||||
|
var picoAt={}; L.parts[0].pins.forEach(function(pp){ picoAt[pp.h[1]+(pp.h[0]===12?'L':'R')]=pp.n; });
|
||||||
|
function pinFor(h){ var r=h[1]; if(r>=0 && r<=19) return picoAt[r+(h[0]<15?'L':'R')]; if(r===25) return '5 V strip'; if(r===26) return 'GND strip'; if(r===27) return '3V3 strip';
|
||||||
|
return {'0,24':'via R1 to opto','0,22':'opto cathode','25,23':'via R4 (10 Ω) from GP0','21,21':'via R5 (33 Ω) from 3V3','30,20':'via R6 (10 Ω) from GP4','30,21':'via R7 (33 Ω) from 3V3'}[h[0]+','+h[1]]||''; }
|
||||||
|
var wl=document.getElementById('wirelist'); wl.innerHTML='';
|
||||||
|
L.wires.forEach(function(w,i){ var s=document.createElement('span'); var sw=document.createElement('span'); sw.className='sw'; sw.style.background='var('+COL[w.col]+')'; s.appendChild(sw); s.appendChild(document.createTextNode(w.col));
|
||||||
|
row(wl,['P'+(i+1),pos(w.h),s,pinFor(w.h),w.to],[0,1]); });
|
||||||
|
}
|
||||||
|
function drawMatrix(){ // drawn from the BACK (lug side), so the columns are mirrored: col 4 on the left, KICK top-right
|
||||||
|
var svg=document.getElementById('matrix'); svg.innerHTML=''; svg.setAttribute('viewBox','0 0 700 330');
|
||||||
|
var C={y:css('--w-yellow'),g:css('--w-green'),ink:css('--ink'),muted:css('--muted'),part:css('--part'),line:css('--line')}, f=css('--f-mono');
|
||||||
|
var names=[['KICK','KICK 2','SNARE',null],['HAT','HAT 16','PERC','GHOST'],['BASS','MEL A','MEL B','PAD']];
|
||||||
|
var x0=110, dx=118, y0=84, dy=80, rowEnd=560;
|
||||||
|
function bxOf(c){ return x0+(3-c)*dx; }
|
||||||
|
for(var c=0;c<4;c++){ var cx=bxOf(c)-38; el('line',{x1:cx,y1:c===3?y0+dy-28:y0-10,x2:cx,y2:y0+2*dy+40,stroke:C.g,'stroke-width':3},svg);
|
||||||
|
el('text',{x:cx,y:y0+2*dy+60,'text-anchor':'middle','font-family':f,'font-size':12,'font-weight':700,fill:C.ink},svg,'Col '+(c+1)+' → '+H('P'+(32+c))); }
|
||||||
|
for(var r=0;r<3;r++){ var ry=y0+r*dy-18, left=bxOf(r===0?2:3)+9;
|
||||||
|
el('line',{x1:left,y1:ry,x2:rowEnd,y2:ry,stroke:C.y,'stroke-width':3},svg);
|
||||||
|
el('text',{x:rowEnd+6,y:ry+4,'font-family':f,'font-size':12,'font-weight':700,fill:C.ink},svg,'Row '+(r+1)+' → '+H('P'+(29+r)));
|
||||||
|
for(c=0;c<4;c++){ var bx=bxOf(c), by=y0+r*dy;
|
||||||
|
if(!names[r][c]){ el('rect',{x:bx-12,y:by-16,width:24,height:32,rx:4,fill:'none',stroke:C.muted,'stroke-width':1.2,'stroke-dasharray':'4 3'},svg);
|
||||||
|
el('text',{x:bx,y:by+4,'text-anchor':'middle','font-family':f,'font-size':10,fill:C.ink},svg,'RUN');
|
||||||
|
el('text',{x:bx,y:by+34,'text-anchor':'middle','font-family':f,'font-size':9,fill:C.muted},svg,'toggle, not in matrix'); continue; }
|
||||||
|
el('circle',{cx:bx,cy:by,r:17,fill:C.part},svg);
|
||||||
|
el('text',{x:bx,y:by+34,'text-anchor':'middle','font-family':f,'font-size':11,fill:C.muted},svg,names[r][c]);
|
||||||
|
var lRow=bx+9, lDio=bx-9, colX=bx-38;
|
||||||
|
el('circle',{cx:lRow,cy:by,r:3.2,fill:'#D4AF37'},svg); el('circle',{cx:lDio,cy:by,r:3.2,fill:'#D4AF37'},svg);
|
||||||
|
el('line',{x1:lRow,y1:by-3,x2:lRow,y2:ry,stroke:C.y,'stroke-width':2},svg);
|
||||||
|
el('line',{x1:lDio,y1:by,x2:colX,y2:by,stroke:'#9AA3AD','stroke-width':1.5},svg);
|
||||||
|
el('rect',{x:colX+4,y:by-4,width:18,height:8,rx:2,fill:'#D9663A'},svg); el('rect',{x:colX+4,y:by-4,width:4,height:8,fill:'#111'},svg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
el('text',{x:x0-60,y:22,'font-family':f,'font-size':12,'font-weight':700,fill:C.ink},svg,'Seen from the BACK (lug side): KICK is top-right.');
|
||||||
|
el('text',{x:x0-60,y:38,'font-family':f,'font-size':10,fill:C.muted},svg,'Diode band → column wire. Lid flipped left to right, back wall still at the top.');
|
||||||
|
}
|
||||||
|
function drawDin(){ // 3.5 mm TRS panel jacks, Type A, lugs seen from inside the box
|
||||||
|
var svg=document.getElementById('din'); svg.innerHTML='';
|
||||||
|
var f=css('--f-mono'), ink=css('--ink'), m=css('--muted');
|
||||||
|
var jacks=[{n:'IN',x:52,w:{T:['--w-blue','P2'],R:['--w-blue','P1']}},{n:'OUT KIT',x:150,w:{T:['--w-blue','P3'],R:['--w-red','P4'],S:['--w-black','P5']}},{n:'OUT SYNTH',x:248,w:{T:['--w-blue','P6'],R:['--w-red','P7'],S:['--w-black','P8']}}];
|
||||||
|
var LUG={T:[-20,'T·5'],R:[0,'R·4'],S:[20,'S·2']};
|
||||||
|
jacks.forEach(function(j){
|
||||||
|
el('rect',{x:j.x-30,y:30,width:60,height:34,rx:4,fill:'#2C3238'},svg);
|
||||||
|
el('circle',{cx:j.x,cy:47,r:9,fill:'none',stroke:'#9AA3AD','stroke-width':2},svg);
|
||||||
|
Object.keys(LUG).forEach(function(k){ var lx=j.x+LUG[k][0], used=j.w[k];
|
||||||
|
el('rect',{x:lx-3,y:64,width:6,height:12,fill:'#C9CFD5'},svg);
|
||||||
|
if(used){ el('line',{x1:lx,y1:76,x2:lx,y2:100,stroke:css(used[0]),'stroke-width':4,'stroke-linecap':'round'},svg);
|
||||||
|
el('text',{x:lx,y:112,'text-anchor':'middle','font-family':f,'font-size':11,'font-weight':700,fill:ink},svg,H(used[1])); }
|
||||||
|
else el('text',{x:lx,y:90,'text-anchor':'middle','font-family':f,'font-size':8,fill:m},svg,'—');
|
||||||
|
el('text',{x:lx,y:124,'text-anchor':'middle','font-family':f,'font-size':8,fill:m},svg,LUG[k][1]); });
|
||||||
|
el('text',{x:j.x,y:142,'text-anchor':'middle','font-family':f,'font-size':11,fill:ink},svg,j.n); });
|
||||||
|
el('text',{x:150,y:14,'text-anchor':'middle','font-family':f,'font-size':9,fill:m},svg,'Type A · T tip = pin 5 · R ring = 4 · S sleeve = 2');
|
||||||
|
}
|
||||||
|
function pinRow(id,pins,w){
|
||||||
|
var svg=document.getElementById(id); svg.innerHTML=''; var f=css('--f-mono'), ink=css('--ink');
|
||||||
|
var n=pins.length, sp=Math.min(36,260/n), x0=150-sp*(n-1)/2;
|
||||||
|
pins.forEach(function(p,i){ var x=x0+i*sp;
|
||||||
|
el('rect',{x:x-6,y:14,width:12,height:12,fill:'#111',stroke:'#D4AF37','stroke-width':1.5},svg);
|
||||||
|
el('line',{x1:x,y1:26,x2:x,y2:52,stroke:p[1]?css(p[1]):'none','stroke-width':4,'stroke-linecap':'round'},svg);
|
||||||
|
el('text',{x:x,y:10,'text-anchor':'middle','font-family':f,'font-size':n>6?8:9,fill:ink},svg,p[0]);
|
||||||
|
el('text',{x:x,y:68,'text-anchor':'middle','font-family':f,'font-size':10,'font-weight':700,fill:ink},svg,H(p[2]||'')); });
|
||||||
|
}
|
||||||
|
function drawMisc(){
|
||||||
|
var svg=document.getElementById('misc'); svg.innerHTML=''; var f=css('--f-mono'), ink=css('--ink'), m=css('--muted');
|
||||||
|
function pot(x,label,wp){ el('circle',{cx:x,cy:42,r:20,fill:'#2C3238'},svg); el('text',{x:x,y:14,'text-anchor':'middle','font-family':f,'font-size':10,fill:ink},svg,label);
|
||||||
|
[[-12,'--w-black','P25'],[0,'--w-blue',wp],[12,'--w-red','P24']].forEach(function(l){ el('rect',{x:x+l[0]-2.5,y:62,width:5,height:12,fill:'#C9CFD5'},svg); el('line',{x1:x+l[0],y1:74,x2:x+l[0],y2:96,stroke:css(l[1]),'stroke-width':4,'stroke-linecap':'round'},svg);
|
||||||
|
el('text',{x:x+l[0],y:108,'text-anchor':'middle','font-family':f,'font-size':9,'font-weight':700,fill:ink},svg,H(l[2])); }); }
|
||||||
|
pot(45,'ENERGY','P22'); pot(115,'DRIFT','P23');
|
||||||
|
el('circle',{cx:190,cy:42,r:16,fill:'#2C3238'},svg); el('text',{x:190,y:14,'text-anchor':'middle','font-family':f,'font-size':10,fill:ink},svg,'SEED');
|
||||||
|
[[-7,'--w-yellow','P26'],[7,'--w-black','P28']].forEach(function(l){ el('line',{x1:190+l[0],y1:58,x2:190+l[0],y2:96,stroke:css(l[1]),'stroke-width':4,'stroke-linecap':'round'},svg); el('text',{x:190+l[0],y:108,'text-anchor':'middle','font-family':f,'font-size':9,'font-weight':700,fill:ink},svg,H(l[2])); });
|
||||||
|
el('rect',{x:240,y:30,width:34,height:24,rx:3,fill:'#2C3238'},svg); el('text',{x:257,y:14,'text-anchor':'middle','font-family':f,'font-size':10,fill:ink},svg,'RUN');
|
||||||
|
[[-11,null,''],[0,'--w-yellow','P27'],[11,'--w-black','P28']].forEach(function(l){ el('rect',{x:257+l[0]-2.5,y:54,width:5,height:10,fill:'#C9CFD5'},svg); if(l[1]){ el('line',{x1:257+l[0],y1:64,x2:257+l[0],y2:96,stroke:css(l[1]),'stroke-width':4,'stroke-linecap':'round'},svg); el('text',{x:257+l[0],y:108,'text-anchor':'middle','font-family':f,'font-size':9,'font-weight':700,fill:ink},svg,H(l[2])); } });
|
||||||
|
el('text',{x:150,y:138,'text-anchor':'middle','font-family':f,'font-size':9,fill:m},svg,'lugs facing down, seen from the front');
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------- Wire map: every panel wire, by part, with its board hole ---------- */
|
||||||
|
var WMAP=[ // [pad, part, lug, short label]
|
||||||
|
['P29','Buttons','Row 1 · KICK, KICK 2, SNARE','ROW 1'],['P30','Buttons','Row 2 · HAT, HAT 16, PERC, GHOST','ROW 2'],['P31','Buttons','Row 3 · BASS, MEL A, MEL B, PAD','ROW 3'],
|
||||||
|
['P32','Buttons','Col 1 · KICK, HAT, BASS (diode band side)','COL 1'],['P33','Buttons','Col 2 · KICK 2, HAT 16, MEL A','COL 2'],['P34','Buttons','Col 3 · SNARE, PERC, MEL B','COL 3'],['P35','Buttons','Col 4 · GHOST, PAD','COL 4'],
|
||||||
|
['P26','SEED','one lug','SEED'],['P27','RUN toggle','middle lug (it sits in the button grid, row 1 / col 4)','RUN'],['P28','SEED + RUN','ground: SEED other lug, RUN one outer lug','GND BUS'],
|
||||||
|
['P20','Encoder','VCC (3.3 V, never 5 V)','ENC VCC'],['P21','Encoder','GND','ENC GND'],['P19','Encoder','SW','ENC SW'],['P18','Encoder','SIB','ENC SIB'],['P17','Encoder','SIA','ENC SIA'],
|
||||||
|
['P22','ENERGY pot','middle lug','ENERGY'],['P23','DRIFT pot','middle lug','DRIFT'],['P24','Pots','right lugs, both (3.3 V)','POTS +'],['P25','Pots','left lugs, both (AGND)','POTS −'],
|
||||||
|
['P14','LCD','1 · VCC (3.3 V)','LCD VCC'],['P15','LCD','2 · GND','LCD GND'],['P10','LCD','3 · SCE','LCD SCE'],['P9','LCD','4 · RST','LCD RST'],['P11','LCD','5 · D/C','LCD D/C'],['P12','LCD','6 · DN (MOSI)','LCD DN'],['P13','LCD','7 · SCLK','LCD SCLK'],['P16','LCD','8 · LED, through a 330 Ω resistor (if dark, move to any AA hole)','LCD LED'],
|
||||||
|
['P2','MIDI IN jack','tip','IN tip'],['P1','MIDI IN jack','ring','IN ring'],
|
||||||
|
['P3','OUT KIT jack','tip','KIT tip'],['P4','OUT KIT jack','ring','KIT ring'],['P5','OUT KIT jack','sleeve','KIT slv'],
|
||||||
|
['P6','OUT SYNTH jack','tip','SYN tip'],['P7','OUT SYNTH jack','ring','SYN ring'],['P8','OUT SYNTH jack','sleeve','SYN slv']
|
||||||
|
];
|
||||||
|
function wireOf(p){ return L.wires[+p.slice(1)-1]; }
|
||||||
|
function drawMap(){
|
||||||
|
var svg=document.getElementById('wmap'); if(!svg) return; svg.innerHTML='';
|
||||||
|
var LM=10.5, RM=10.5, f=css('--f-mono'), ink=css('--ink');
|
||||||
|
svg.setAttribute('viewBox',(-LM)+' -3.2 '+(L.W+LM+RM)+' '+(L.H+4.6));
|
||||||
|
el('rect',{x:-0.8,y:-0.5,width:L.W+0.6,height:L.H+0.3,rx:0.4,fill:css('--board'),stroke:css('--board-edge'),'stroke-width':0.12,opacity:0.55},svg);
|
||||||
|
for(var r=0;r<L.H;r++) el('line',{x1:-0.4,y1:r,x2:L.W-0.6,y2:r,stroke:css('--copper'),'stroke-width':0.5,opacity:0.18},svg);
|
||||||
|
el('rect',{x:11.3,y:-0.55,width:8.4,height:20.1,rx:0.4,fill:css('--part'),opacity:0.85},svg);
|
||||||
|
el('text',{x:15.5,y:9.5,'text-anchor':'middle','font-family':f,'font-size':'1.1','font-weight':700,fill:css('--part-ink')},svg,'PICO');
|
||||||
|
el('text',{x:15.5,y:-1.2,'text-anchor':'middle','font-family':f,'font-size':'0.7',fill:css('--muted')},svg,'USB edge');
|
||||||
|
el('rect',{x:3.6,y:19.6,width:3.8,height:3.8,rx:0.2,fill:css('--part'),opacity:0.85},svg);
|
||||||
|
el('text',{x:5.5,y:21.8,'text-anchor':'middle','font-family':f,'font-size':'0.6',fill:css('--part-ink')},svg,'opto');
|
||||||
|
// strip letters on both sides, hole numbers along the bottom
|
||||||
|
for(r=0;r<L.H;r++){ el('text',{x:-1.0,y:r+0.22,'text-anchor':'end','font-family':f,'font-size':'0.5',fill:css('--muted')},svg,rowName(r)); el('text',{x:L.W-0.1,y:r+0.22,'font-family':f,'font-size':'0.5',fill:css('--muted')},svg,rowName(r)); }
|
||||||
|
for(var c=0;c<L.W;c+=2) el('text',{x:c,y:L.H+0.9,'text-anchor':'middle','font-family':f,'font-size':'0.5',fill:css('--muted')},svg,String(c+1));
|
||||||
|
var sides={L:[],R:[]};
|
||||||
|
WMAP.forEach(function(m){ var w=wireOf(m[0]); (w.h[0]<=12?sides.L:sides.R).push({m:m,w:w}); });
|
||||||
|
['L','R'].forEach(function(sd){
|
||||||
|
var list=sides[sd].sort(function(a,b){ return a.w.h[1]-b.w.h[1] || (sd==='L'?b.w.h[0]-a.w.h[0]:a.w.h[0]-b.w.h[0]); }), last=-9, k;
|
||||||
|
for(k=0;k<list.length;k++){ list[k].ly=Math.max(list[k].w.h[1],last+1.05); last=list[k].ly; }
|
||||||
|
var cap=L.H-0.6; // keep labels inside the board's height: push the stack up from the bottom
|
||||||
|
for(k=list.length-1;k>=0;k--){ list[k].ly=Math.min(list[k].ly,cap); cap=list[k].ly-1.05; }
|
||||||
|
list.forEach(function(o){
|
||||||
|
var h=o.w.h, col=css(COL[o.w.col]), ly=o.ly;
|
||||||
|
var lx=sd==='L'?-1.9:L.W+0.9;
|
||||||
|
el('polyline',{points:h[0]+','+h[1]+' '+(sd==='L'?-0.6:L.W-0.4)+','+ly+' '+lx+','+ly,fill:'none',stroke:col,'stroke-width':0.13,opacity:0.9},svg);
|
||||||
|
el('circle',{cx:h[0],cy:h[1],r:0.36,fill:col,stroke:'#fff','stroke-width':0.08},svg);
|
||||||
|
var t=el('text',{x:sd==='L'?lx-0.25:lx+0.25,y:ly+0.25,'text-anchor':sd==='L'?'end':'start','font-family':f,'font-size':'0.72',fill:ink},svg);
|
||||||
|
var a=document.createElementNS(NS,'tspan'); a.setAttribute('font-weight','700'); a.textContent=o.m[3]+' '; t.appendChild(a);
|
||||||
|
var b=document.createElementNS(NS,'tspan'); b.setAttribute('fill',css('--accent')); b.setAttribute('font-weight','700'); b.textContent=pos(h); t.appendChild(b);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
function checklist(){
|
||||||
|
var tb=document.getElementById('wcheck'); if(!tb) return; tb.innerHTML='';
|
||||||
|
var done={}; try{ done=JSON.parse(localStorage.getItem('seedbox.wires')||'{}')||{}; }catch(e){}
|
||||||
|
var prev='';
|
||||||
|
WMAP.forEach(function(m){
|
||||||
|
var w=wireOf(m[0]), tr=document.createElement('tr'), part=m[1]===prev?'':m[1]; prev=m[1];
|
||||||
|
if(part) tr.className='grp';
|
||||||
|
var td0=document.createElement('td'), cb=document.createElement('input'); cb.type='checkbox'; cb.checked=!!done[m[0]]; cb.setAttribute('aria-label','Done: '+m[1]+' '+m[2]);
|
||||||
|
cb.addEventListener('change',function(){ done[m[0]]=cb.checked; tr.classList.toggle('done',cb.checked); try{ localStorage.setItem('seedbox.wires',JSON.stringify(done)); }catch(e){} });
|
||||||
|
td0.appendChild(cb); tr.appendChild(td0); tr.classList.toggle('done',cb.checked);
|
||||||
|
var td1=document.createElement('td'); td1.className='part'; td1.textContent=part; tr.appendChild(td1);
|
||||||
|
var td2=document.createElement('td'); td2.textContent=m[2]; tr.appendChild(td2);
|
||||||
|
var td3=document.createElement('td'), sw=document.createElement('span'); sw.className='sw'; sw.style.background='var('+COL[w.col]+')'; td3.appendChild(sw); td3.appendChild(document.createTextNode(w.col)); tr.appendChild(td3);
|
||||||
|
var td4=document.createElement('td'); td4.className='hole'; td4.textContent=pos(w.h); tr.appendChild(td4);
|
||||||
|
tb.appendChild(tr);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
function all(){
|
||||||
|
drawMap(); checklist(); drawTop(); drawBottom(); tables(); drawMatrix(); drawDin();
|
||||||
|
pinRow('lcd',[['1 VCC','--w-red','P14'],['2 GND','--w-black','P15'],['3 SCE','--w-blue','P10'],['4 RST','--w-blue','P9'],['5 D/C','--w-blue','P11'],['6 DN','--w-blue','P12'],['7 SCLK','--w-blue','P13'],['8 LED','--w-red','P16']]);
|
||||||
|
pinRow('enc',[['VCC','--w-red','P20'],['GND','--w-black','P21'],['SW','--w-green','P19'],['SIB','--w-green','P18'],['SIA','--w-green','P17']]);
|
||||||
|
drawMisc();
|
||||||
|
}
|
||||||
|
all();
|
||||||
|
try{ window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change',all); }catch(e){}
|
||||||
|
})();
|
||||||
|
</script>
|
||||||