Compare commits

..

2 Commits

Author SHA1 Message Date
Gee
346c820b10 Split kit and keys; KEYS GEN with MOTIF, 303, TURING, ARP
Some checks failed
check / check (push) Has been cancelled
- genome(seed, salts) now has three parts with their own RNG streams:
  shared (steps, form, swing, arc, layback, fill odds, phase offset),
  kit (density, frame, accent cycle, free snare) and keys (scale/root,
  progression, motif, hocket, bass tone, note lengths, range). ROLL KEYS
  now renews the harmony too; ROLL KIT leaves the keys alone.
- Keys generators share one signature and can fix note lengths:
  MOTIF (the old line), 303 (acid bass, answers in the rests, drone or
  silent pad), TURING (8/16-bit shift register, DRIFT flips bits, 16-bar
  window), ARP (triad/7th arpeggio, roots on kicks).
- Pages: KIT GEN, KEYS GEN; ROLL shows KIT / KEYS; slots keep both gens.
- Flash record v3 (old settings ignored). Bring-up golden checksums updated.
- C port matches; host test and ASan/UBSan pass.

Intended music change: every seed sounds different (genome split, new
generators). Vectors regenerated with npm run vectors; the case grid now
crosses all kit and keys generators, with TURING runs past its window.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 21:50:41 +03:00
Gee
c2c8a55642 Spec: split kit and keys generators, add 303 / TURING / ARP
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 21:41:38 +03:00
27 changed files with 64405 additions and 42622 deletions

View File

@@ -24,17 +24,13 @@ The synth runs in poly mode on one channel, so bass, melody and pad share one vo
## The genome
`genome(seed)` picks, once per seed:
`genome(seed, salts)` picks, in three parts. Each part has its own random stream, so ROLL KIT and ROLL KEYS each renew one half only.
- **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
| Part | Renewed by | Picks |
|---|---|---|
| shared | SEED | **steps** (16 mostly; 12, 14 or 20), **form** (AAAB, AABA, ABAB, AAAA, ABAC, ABAA), **swing** (none to triplet feel), **arc** (flat, build, break), **layback** (snares sit late), melody/perc phase offset, fill odds |
| kit | SEED, ROLL KIT | kit density, **frame** (below), **accent cycle** (off, or every 3, 5 or 7 sixteenths across the bar line), free-snare settings |
| keys | SEED, ROLL KEYS | **scale and root** (any of the 21 below; the ROOT and SCALE pages override them), **progression** (one chord root per bar, e.g. i–vi–iv–v), keys density, motif length, hocket mask, bass colour tone, note-length style, range |
### Frames
@@ -81,7 +77,7 @@ Bass, melody and pad follow the scale. Chords stack every other scale note, so t
## Generators
Selected on the GEN page. They only change the kit; the synth tracks are the same for all of them.
Kit generators, selected on the KIT GEN page.
- **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".
@@ -89,6 +85,21 @@ Selected on the GEN page. They only change the kit; the synth tracks are the sam
- **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).
## Keys generators
Selected on the KEYS GEN page. All of them follow the bar's chord in the current scale and are split over BASS, MEL A, MEL B and PAD.
| Name | BASS | MEL A / B | PAD |
|---|---|---|---|
| **MOTIF** | the chord root on the kicks, a colour tone on alternate kicks, an approach note into the next bar | a 3–5 note motif stated and answered, hocket split | the bar's triad |
| **303** | a 16-step acid line looped over the bar: gates, accents, octave jumps, slides; moves with the chord root | 1–2 note answers in the bass's rests | silent, or a drone on the key's root |
| **TURING** | the chord root wherever the shift register's bit 0 is set; another bit lifts it an octave | the register read as a scale degree, hocket split | the bar's triad |
| **ARP** | the chord root on every kick | the chord (triad or 7th) arpeggiated up, down, up-down or shuffled, alternating A / B | the arpeggio's chord |
- **303**: ENERGY opens more gates. DRIFT variants flip accents, slides and octaves. A slide is a note held into the next one; whether it glides is up to the synth's glide or legato setting.
- **TURING**: an 8- or 16-bit register stepping every 16th. DRIFT flips the bit coming round (0 = locked loop, a quarter of the knob ≈ one flip a bar). Flips stick for 16 bars, then fall away.
- **ARP**: 8ths at low density, 16ths at high, and 16ths in fill bars.
## 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.

View File

@@ -44,12 +44,13 @@ The Nokia 5110 has 84 × 48 pixels:
| 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 |
| **KIT GEN** | EUCLID, SKELETON, SUBDIV, AUTOMATA, AMENIZER | done |
| **KEYS GEN** | MOTIF, 303, TURING, ARP (see [Concept](concept.md#keys-generators)) | 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 |
| **SLOT** | 8 slots for seed + salts + kit and keys generators (not root or scale) | push = load, long push = save |
| **ROLL** | KIT or KEYS | push re-rolls just that half, then goes back to page select. KIT: a new drum part over the same keys. KEYS: new chords, scale (on AUTO), motif and patterns over the same drums |
| **LEARN** | — | push, press a kit track button, hit the pad on the sampler or drum machine. Push again to cancel. |
## Simulator keys

View File

@@ -13,6 +13,8 @@ Short record of why things are the way they are, so they don't get undone by acc
- **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.
- **Kit and keys are split (2026-10-11).** One seed drives both through their own generator (KIT GEN, KEYS GEN). The keys-only choices (progression, scale, motif) moved into the keys half so ROLL KEYS renews the harmony too. Every seed changed sound; vectors regenerated. ENERGY, DRIFT, bar length, form and swing stay shared, and bass may still lock to the kicks.
- **303 slides are overlaps only.** No portamento CCs: the synth channel is shared and poly, so a CC would bend every voice.
- **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.

View File

@@ -0,0 +1,135 @@
# Split kit and keys, three new keys generators
2026-10-11. Status: draft for review.
## Goal
Drums and keys each run their own generator, picked on their own page. One seed still drives both. ROLL KIT and ROLL KEYS re-roll one half. The change ships with three new keys generators: 303, TURING and ARP. Further generators get their own small specs.
## Decisions
| Question | Answer |
|---|---|
| Seeds | One seed, as now. ROLL KIT / ROLL KEYS re-roll one half through its salt. |
| What's separate | The generator menu and the seed-driven choices. ENERGY and DRIFT stay shared. |
| Coupling | Stays. Keys see the bar's kit (bass can lock to kicks). Bar length, form and swing are shared. |
| Naming | "LINE" becomes "KEYS" everywhere the user sees it: ROLL shows KIT / KEYS, NEW KEYS. |
| 303 slides | A legato overlap only, no CCs. Audible glide depends on the synth's settings. |
| Old data | None kept. The seeds' sound changes, the vectors are regenerated, and the flash record goes to v3. |
## Engine structure (`sim/engine.js`, then `firmware/engine/engine.c`)
`genome(seed)` is split into three parts, each with its own RNG stream.
| Part | Seeded by | Renewed by | Fields |
|---|---|---|---|
| shared | seed | SEED | steps, form, swing, layback, arc, fillP, rot |
| kit | seed + salt 0 | SEED, ROLL KIT | dens[0], frame, accentCycle, freeK, freeRot |
| keys | seed + salt 1 | SEED, ROLL KEYS | scale, root (AUTO), prog, motifLen, hocket, bassDeg, monoLen, monoRange, dens[1] |
- `renderBar(st, p, bar)`:
1. builds the shared part;
2. runs the kit generator `p.gen` (unchanged list);
3. runs the keys generator `p.keys`.
- The key override (`applyKey`) applies to the keys part.
- AMENIZER still forces 16 straight steps on the whole bar.
- Every keys generator has one signature. It receives:
- the keys seed and phrase variant;
- the shared and keys parts;
- steps, section and bar position;
- the bar's sorted kit events;
- the fill flag;
- ENERGY / DRIFT;
- the absolute bar index.
It returns `{bass, mel, pad}` lists of `{t, v, vel, tr}`, each with an optional `len`. Without `len`, today's note-length rules apply. MEL notes carry `tr` MEL A or MEL B.
- In the outputs, groove, nudge, swing, the melody shift and the perc phase offset stay as they are and apply to every generator.
- In C, `sb_params` gains `keys`. `describe()` names the keys generator.
## Keys generators (KEYS GEN, menu order)
The chord is the bar's chord degree from `prog`, in the current scale.
| # | Name | BASS | MEL A / B | PAD |
|---|---|---|---|---|
| 0 | MOTIF | Today's: the chord root on kicks, the colour tone on alternate kicks, an approach note | Today's motif and answer, hocket split | Today's: the bar's triad |
| 1 | 303 | Acid line (below) | Short answers in the bass gaps | Silence or a root drone |
| 2 | TURING | Roots, gated by the shift register | The register's melody, hocket split | Chord held |
| 3 | ARP | The chord root on every kick | Arpeggio, alternating A / B | The arpeggio's chord |
### 303
- **Pattern:** 16 steps, from the keys seed, looped across the bar (wrapped or cut for 12/14/20 steps). Each step has:
- a gate;
- a degree offset (mostly 0; sometimes the 3rd, 5th or 7th in scale steps);
- accent, slide and octave flags.
- **Pitch:** the bar's chord root plus the step's offset, in the bass register (36–47). The octave flag adds +12.
- **Accent:** velocity ~120. Other notes ~80.
- **Slide:** `len` reaches 20 ticks into the next note (an overlap).
- **ENERGY:** how many gated steps sound.
- **DRIFT:** the chance that a phrase variant flips some gates and flags.
- **Melody:** 1–2 note answers on chord tones, only where the bass rests for ≥ 2 steps. Short and alternating A / B.
- **Pad:** the keys seed picks silence or a drone on the key's root (C3–B3), held all bar. *Guess: 50/50.*
### TURING
- **Register:** 8 or 16 bits (from the keys seed). It rotates one bit per 16th.
- **Flips:** at each step the wrapped bit may flip. The chance follows DRIFT: 0 locks the loop. Whether absolute step k flips is decided by a hash of (keys seed, k), so a flip is the same event however we get there.
- **Window:** the register at a bar's start is rebuilt from the base register over the last 16 bars of steps. Flips stick for 16 bars and then fall away. The cost per bar is fixed, and any bar can be rendered on its own (start, the Pico's render-ahead).
- **Melody:** the register's value scaled to a scale degree over 1–2 octaves (`monoRange`), with the existing hocket split. ENERGY thins notes out (`dens[1]`).
- **Bass:** the chord root on steps where bit 0 is set. *Guess: another bit picks the root's octave.*
- **Pad:** the bar's triad, held (as MOTIF).
### ARP
- **Chord:** a triad or 7th (from the keys seed). The degrees are c, c+2, c+4 (+ c+6).
- **Direction (keys seed):** up, down, up-down, or a fixed shuffle. **Range:** 1–2 octaves (`monoRange`).
- **Rate:** 8ths at low ENERGY, 16ths at high. Notes alternate MEL A / MEL B.
- **Bass:** the chord root on every kick, with no colour tone.
- **Pad:** the same chord as the arpeggio (adds the 7th when the arp has it).
## Panel (firmware `app/main.c`, simulator `app.js`)
- Pages: SEED · KIT GEN · KEYS GEN · ROOT · SCALE · BPM · SLOT · ROLL · LEARN.
- KIT GEN is today's GEN (label `KIT GEN`). KEYS GEN cycles MOTIF, 303, TURING, ARP (label `KEYS GEN`).
- ROLL shows KIT / KEYS. The message is NEW KIT / NEW KEYS.
- A generator change is heard from the next bar.
- Slots store the seed, salts, kit gen and keys gen. ROOT / SCALE stay global (as before).
- On AUTO, ROOT / SCALE show the keys part's pick (it changes with ROLL KEYS).
## Storage
- `live_t` and the flash record gain `keys`. The record goes to version 3, and old records are ignored.
- The simulator stores `keys` in localStorage. Its slots gain `keys`.
## Vectors
- Run `npm run vectors`. The commit message says it's an intended music change.
- Cases gain `keys`. The main grid crosses the kit gens with all four keys gens, at fewer energy/drift sets so the count stays sane.
- Extra cases:
- ROLL KEYS (salt 1) changes `prog`, and ROLL KIT doesn't;
- TURING at drift 0 repeats exactly;
- TURING at bar ≥ 16 (the window);
- 303 with 12- and 20-step bars;
- ARP with a 7th chord.
- `primitives.json` genomes become the three parts.
- `firmware/bringup` golden checksums are updated.
## Docs
- `docs/controls.md`: the pages table (KIT GEN, KEYS GEN, ROLL KIT/KEYS).
- `docs/concept.md`:
- the genome split table;
- a keys generators section (the table above, in short);
- "LINE" becomes "keys".
- `docs/decisions.md`: one line each on one seed with per-half ROLL, and on the overlap-only slide.
## Testing
1. `npm test` and `cd firmware/test && make test` pass.
2. Listening in the simulator: each keys generator over two or three kit gens and seeds.
3. Bench:
- ROLL KEYS brings new chords and leaves the drums alone;
- ROLL KIT leaves the keys alone;
- KEYS GEN switches at the next bar;
- 303 slides overlap on the synth.

View File

@@ -15,7 +15,7 @@ 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 gen, keys, energy, drift, bpm; /* gen = KIT GEN, keys = KEYS GEN */
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];

View File

@@ -29,7 +29,7 @@ static const char *const TRACK_NAME[SB_NT] = { "KICK","KICK 2","SNARE","GHOST","
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 };
enum { PG_SEED, PG_GEN, PG_KEYS, PG_ROOT, PG_SCALE, PG_BPM, PG_SLOT, PG_ROLL, PG_LEARN, NPAGES };
static slot_t slots[NSLOTS];
static sb_state history[16];
@@ -57,7 +57,7 @@ static void set_state(sb_state s){
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 reroll(int l){ sb_state s = g_live.state; s.salts[l] = (s.salts[l] + 1) & 0xFFFF; set_state(s); msg(l ? "NEW KEYS" : "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); }
@@ -68,6 +68,7 @@ static void enc_turn(int d){
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_KEYS: lock(); g_live.keys = (g_live.keys + d + SB_NKEYS) % SB_NKEYS; 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;
@@ -88,13 +89,13 @@ static void enc_push(int 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();
slots[slot_sel].gen = (uint8_t)g_live.gen; slots[slot_sel].keys = (uint8_t)g_live.keys; 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();
lock(); g_live.gen = slots[slot_sel].gen; g_live.keys = slots[slot_sel].keys % SB_NKEYS; unlock();
set_state(s);
}
}
@@ -149,15 +150,16 @@ static void screen_text(char *label, char *value){
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_GEN: strcpy(l, "KIT GEN"); strcpy(v, GEN_NAME[g_live.gen]); break;
case PG_KEYS: strcpy(l, "KEYS GEN"); strcpy(v, sb_keys_name(g_live.keys)); 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; }
if(x < 0){ sb_genome_make(&g_live.state, &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_ROLL: strcpy(l, "RE-ROLL"); strcpy(v, roll_sel ? "KEYS" : "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");

View File

@@ -125,7 +125,7 @@ static void render_ahead(uint64_t now){
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 */
p.gen = L.gen; p.keys = L.keys; 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]);

View File

@@ -5,14 +5,14 @@
#include <string.h>
#define MAGIC 0x53425831u /* "SBX1" */
#define VERSION 2 /* 2: ROOT / SCALE added, BANK gone. Older records are ignored. */
#define VERSION 3 /* 3: KEYS GEN. 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;
int32_t gen, keys, bpm, scale, root, mono_ch, mono_oct;
uint8_t on[SB_NT];
pad_t learn[NKIT];
slot_t slots[NSLOTS];
@@ -33,7 +33,7 @@ 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;
L->gen = 0; L->keys = 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);
@@ -45,7 +45,7 @@ int storage_load(live_t *L, slot_t *slots){
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;
L->gen = r->gen; L->keys = r->keys; 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);
@@ -64,7 +64,7 @@ int storage_save(const live_t *L, const slot_t *slots){
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;
r->gen = L->gen; r->keys = L->keys; 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);

View File

@@ -1,4 +1,4 @@
/* Settings in the last flash sector: slots, learned pads, BPM, generator, root, scale, synth channel and octave,
/* Settings in the last flash sector: slots, learned pads, BPM, kit and keys generators, 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
@@ -6,7 +6,7 @@
#include "app.h"
typedef struct { uint8_t used, gen; uint32_t seed; uint32_t salts[2]; } slot_t;
typedef struct { uint8_t used, gen, keys; 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 */

View File

@@ -21,9 +21,9 @@ 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 */
{ 0x5EEDB0C5, 0, 64, 32, 0xC055F3A7 }, /* 5EEDB0C5_s0-0_g0_k0_e64_d32 */
{ 0x5EEDB0C5, 3, 127,127, 0x1CF02038 }, /* 5EEDB0C5_s0-0_g3_k0_e127_d127, dense */
{ 0x5EEDB0C5, 4, 64, 32, 0x487CBA24 }, /* 5EEDB0C5_s0-0_g4_k0_e64_d32, AMENIZER */
};
static int engine_ok;
static uint32_t engine_worst_us;
@@ -35,7 +35,7 @@ static void engine_selftest(void){
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 };
sb_params p = { GOLDEN[c].gen, SB_KEYS_MOTIF, 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();

View File

@@ -99,17 +99,18 @@ 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 { int16_t t, v, vel, a, tr, len; } ev_t; /* len: keys generators' fixed length, 0 = the default rule */
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;
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; e->len = 0;
}
static void push_len(evlist *l, int t, int v, int vel, int tr, int len){ int n0 = l->n; push_tr(l, t, v, vel, 0, tr); if(l->n > n0) l->e[l->n - 1].len = (int16_t)len; }
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); }
static void push_ev(evlist *l, const ev_t *e){ if(l->n >= SB_MAX_EVENTS){ g_overflow = 1; return; } l->e[l->n++] = *e; }
/* 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)
@@ -124,28 +125,35 @@ static void sort_tv(evlist *l){ /* stable, by (t, v) */
}
/* ---------- genome ---------- */
void sb_genome_make(uint32_t seed, sb_genome *g){
static uint32_t role_seed(const sb_state *st, int r){ return mix(mix(st->seed, 0x524F4C45u + (uint32_t)r), st->salts[r]); }
void sb_genome_make(const sb_state *st, 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));
sb_rng r = rng(mix(st->seed, 0x47454E)), k, l;
int fi;
/* shared */
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->fillP = 40 + below(&r, 80);
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);
/* kit */
k = rng(mix(role_seed(st, 0), 0x4B4954));
g->dens[0] = 40 + below(&k, 60); g->dens[1] = 0;
g->accentCycle = ACC[pick_w(&k, ACC_W, 5)];
g->frame = pick_w(&k, FRAME_W, 8); g->freeK = 2 + below(&k, 3); g->freeRot = below(&k, 16);
/* keys */
l = rng(mix(role_seed(st, 1), 0x4B4559));
g->scale = below(&l, SB_NSCALES); g->root = below(&l, 12);
g->dens[1] = 30 + below(&l, 70);
g->monoLen = below(&l, 3); g->monoRange = 1 + below(&l, 2);
g->hocket = HOCKETS[below(&l, 8)];
g->bassDeg = BASS_DEG[below(&l, 3)];
g->prog_idx = below(&l, 10); memcpy(g->prog, PROGS[g->prog_idx], sizeof g->prog);
g->motifLen = 3 + below(&l, 3);
}
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; }
@@ -578,6 +586,136 @@ static void line_tracks(uint32_t rs, int variant, const sb_genome *g, int steps,
one_at_a_time(bass);
}
/* ---------- keys generators ---------- */
typedef struct { uint32_t rs; int variant; const sb_genome *g; int steps, d, sec, pos; const evlist *kit; int fill, bar, drift; } keys_in;
static void keys_motif(const keys_in *in, evlist *mel, evlist *bass, evlist *pad){
line_tracks(in->rs, in->variant, in->g, in->steps, in->d, in->sec, in->pos, in->kit, in->fill, mel, bass, pad);
}
static void pad_chord(const sb_genome *g, const int *tones, int nt, evlist *pad){
uint8_t seen[128];
int i;
memset(seen, 0, sizeof seen);
for(i = 0; i < nt; 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); } }
}
typedef struct { int rank, off, up, acc, sl; } step303;
static void keys_303(const keys_in *in, evlist *mel, evlist *bass, evlist *pad){
static const int OFF[4] = {0,2,4,6}, OFF_W[4] = {10,2,3,1}, TONE[3] = {0,2,4};
const sb_genome *g = in->g;
sb_rng r = rng(mix(in->rs, 0x333033)), ar;
step303 pat[16];
uint8_t on[MAX_STEPS];
int c = chord_deg(g, in->pos), steps = in->steps, i, s, drone, thr;
mel->n = bass->n = pad->n = 0;
for(i = 0; i < 16; i++){
pat[i].rank = 0;
if(i > 0) pat[i].rank = below(&r, 128);
pat[i].off = OFF[pick_w(&r, OFF_W, 4)];
pat[i].up = chance(&r, P(24));
pat[i].acc = chance(&r, P(40));
pat[i].sl = chance(&r, P(28));
}
drone = chance(&r, P(64));
if(in->variant){
sb_rng vr = rng(mix(mix(in->rs, 0x5641u + (uint32_t)in->sec), (uint32_t)in->variant));
for(i = 0; i < 16; i++){
if(chance(&vr, P(20))) pat[i].acc = !pat[i].acc;
if(chance(&vr, P(16))) pat[i].sl = !pat[i].sl;
if(chance(&vr, P(16))) pat[i].up = !pat[i].up;
if(i > 0 && chance(&vr, P(16))) pat[i].rank = 127 - pat[i].rank;
}
}
thr = clamp(in->d + 16, 0, 128);
for(s = 0; s < steps; s++) on[s] = (uint8_t)(pat[s % 16].rank < thr);
for(s = 0; s < steps; s++) if(on[s]){
const step303 *p = &pat[s % 16];
int nx = s + 1;
while(nx < steps && !on[nx]) nx++;
push_len(bass, s * TPS, bp(g, c + p->off) + (p->up ? 12 : 0), p->acc ? 120 : 80, SB_T_BASS, p->sl ? (nx - s) * TPS + 20 : TPS >> 1);
}
ar = rng(mix(in->rs, 0x414E53u + (uint32_t)in->sec));
for(s = 0; s < steps;){
int e2 = s, tr;
if(on[s]){ s++; continue; }
while(e2 < steps && !on[e2]) e2++;
if(e2 - s >= 2 && chance(&ar, P(in->d + 32))){
tr = ((g->hocket >> (s & 7)) & 1) ? SB_T_MELA : SB_T_MELB;
push_len(mel, s * TPS, mel_pitch(g, c + TONE[below(&ar, 3)]), 84, tr, TPS - 40);
if(e2 - s >= 3 && chance(&ar, P(64))) push_len(mel, (s + 1) * TPS, mel_pitch(g, c + TONE[below(&ar, 3)]), 72, tr, TPS - 40);
}
s = e2;
}
if(drone) push_tr(pad, 0, 48 + g->root, 52, 0, SB_T_PAD);
}
typedef struct { int L, flipP; uint32_t mask, fs; } turing_t;
static uint32_t rotl_bits(uint32_t v, int s, int L){ return s ? (((v << s) | (v >> (L - s))) & ((1u << L) - 1u)) : v; }
static uint32_t turing_step(const turing_t *T, uint32_t v, int k){
uint32_t top = (v >> (T->L - 1)) & 1u;
if(T->flipP && (int)(mix(T->fs, (uint32_t)k) >> 16) < T->flipP) top ^= 1u;
return ((v << 1) | top) & T->mask;
}
static void keys_turing(const keys_in *in, evlist *mel, evlist *bass, evlist *pad){
const sb_genome *g = in->g;
sb_rng r = rng(mix(in->rs, 0x545552));
turing_t T;
uint32_t reg;
int n = SCALE_LEN[g->scale], c = chord_deg(g, in->pos), from, to, k, s, j = 0, span, tones[3];
mel->n = bass->n = pad->n = 0;
T.L = chance(&r, P(64)) ? 8 : 16; T.mask = (1u << T.L) - 1u;
reg = sb_rng_next(&r) & T.mask;
if(!reg) reg = 1;
T.flipP = P(in->drift >> 2); T.fs = mix(in->rs, 0x464C50);
from = imax(0, in->bar - 16) * in->steps; to = in->bar * in->steps;
reg = rotl_bits(reg, from % T.L, T.L);
for(k = from; k < to; k++) reg = turing_step(&T, reg, k);
span = n * g->monoRange;
for(s = 0; s < in->steps; s++){
if((int)((reg >> 2) & 7u) * 18 < in->d){
int v = mel_pitch(g, c + (int)(((reg & 0xFFu) * (uint32_t)(span + 1)) >> 8));
push_tr(mel, s * TPS, v, 70 + (int)((reg >> 5) & 3u) * 10 + (s % 4 == 0 ? 12 : 0), 0, ((g->hocket >> (j & 7)) & 1) ? SB_T_MELA : SB_T_MELB);
j++;
}
if(reg & 1u) push_tr(bass, s * TPS, bp(g, c) + (((reg >> (T.L >> 1)) & 1u) ? 12 : 0), s == 0 ? 110 : 94, 0, SB_T_BASS);
reg = turing_step(&T, reg, to + s);
}
tones[0] = c; tones[1] = c + 2; tones[2] = c + 4;
pad_chord(g, tones, 3, pad);
}
static void keys_arp(const keys_in *in, evlist *mel, evlist *bass, evlist *pad){
const sb_genome *g = in->g;
sb_rng r = rng(mix(in->rs, 0x415250));
int n = SCALE_LEN[g->scale], c = chord_deg(g, in->pos), i, o, seventh, dir, nch, chord[4], up[8], seq[16], nup = 0, nseq = 0;
int off = 0, rate, last0, s = 0, k = 0, nk = 0;
mel->n = bass->n = pad->n = 0;
seventh = chance(&r, P(64));
dir = below(&r, 4);
nch = seventh ? 4 : 3; chord[0] = c; chord[1] = c + 2; chord[2] = c + 4; chord[3] = c + 6;
for(o = 0; o < g->monoRange; o++) for(i = 0; i < nch; i++) up[nup++] = chord[i] + o * n;
if(dir == 1) for(i = nup - 1; i >= 0; i--) seq[nseq++] = up[i];
else {
for(i = 0; i < nup; i++) seq[nseq++] = up[i];
if(dir == 2) for(i = nup - 2; i > 0; i--) seq[nseq++] = up[i];
else if(dir == 3) for(i = nseq - 1; i > 0; i--){ int j = below(&r, i + 1), tmp = seq[i]; seq[i] = seq[j]; seq[j] = tmp; }
}
if(in->variant){ sb_rng vr = rng(mix(mix(in->rs, 0x5641u + (uint32_t)in->sec), (uint32_t)in->variant)); off = below(&vr, nseq); }
rate = in->d >= 64 ? 1 : 2; last0 = (n_groups(in->steps) - 1) * 4;
while(s < in->steps){
int stp = (in->fill && s >= last0) ? 1 : rate;
push_len(mel, s * TPS, mel_pitch(g, seq[(k + off) % nseq]), s % 4 == 0 ? 96 : 78, (k & 1) ? SB_T_MELB : SB_T_MELA, stp * TPS - 20);
k++; s += stp;
}
for(i = 0; i < in->kit->n; i++){
const ev_t *e = &in->kit->e[i];
if(e->v == 0 && e->t % TPS == 0){ push_tr(bass, e->t, bp(g, c), e->t == 0 ? 112 : 98, 0, SB_T_BASS); nk++; }
}
if(!nk) push_tr(bass, 0, bp(g, c), 104, 0, SB_T_BASS);
pad_chord(g, chord, nch, pad);
}
static const char *const KEYS_NAMES[SB_NKEYS] = {"MOTIF","303","TURING","ARP"};
const char *sb_keys_name(int keys){ return keys >= 0 && keys < SB_NKEYS ? KEYS_NAMES[keys] : "?"; }
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;
@@ -640,14 +778,15 @@ static void mono(const mono_ctx *c, const evlist *list, int lenRule){
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;
if(cur->len > 0) len = cur->len; /* fixed by the keys generator */
else 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);
1, cur->v, cur->vel, imax(10, cur->len > 0 ? len : len - c->swingBar), cur->tr);
}
}
static void sort_out(sb_bar *out){ /* stable, by (t, tr, v) */
@@ -674,7 +813,7 @@ void sb_render_bar(const sb_state *st, const sb_params *p, int bar, sb_bar *out)
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_genome_make(st, &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;
@@ -725,7 +864,17 @@ void sb_render_bar(const sb_state *st, const sb_params *p, int bar, sb_bar *out)
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);
{
keys_in ki;
ki.rs = role_seed(st, 1); ki.variant = variants[1]; ki.g = &g; ki.steps = g.steps; ki.d = clamp(g.dens[1] + e - 64, 0, 127);
ki.sec = sec; ki.pos = pos; ki.kit = &kit; ki.fill = fill; ki.bar = bar; ki.drift = dr;
switch(clamp(p->keys, 0, SB_NKEYS - 1)){
case SB_KEYS_303: keys_303(&ki, &mel, &bass, &pad); break;
case SB_KEYS_TURING: keys_turing(&ki, &mel, &bass, &pad); break;
case SB_KEYS_ARP: keys_arp(&ki, &mel, &bass, &pad); break;
default: keys_motif(&ki, &mel, &bass, &pad); break;
}
}
out->n = 0;
for(i = 0; i < kit.n; i++){

View File

@@ -17,6 +17,7 @@
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 };
enum { SB_KEYS_MOTIF, SB_KEYS_303, SB_KEYS_TURING, SB_KEYS_ARP, SB_NKEYS };
/* hash and RNG */
uint32_t sb_mix(uint32_t a, uint32_t b);
@@ -38,11 +39,12 @@ typedef struct {
int prog[4];
int swing, arc, motifLen, accentCycle, layback, frame, freeK, freeRot;
} sb_genome;
void sb_genome_make(uint32_t seed, sb_genome *g);
typedef struct { uint32_t seed; uint32_t salts[2]; } sb_state;
/* shared part from the seed; kit and keys parts also take their salt (ROLL KIT / ROLL KEYS) */
void sb_genome_make(const sb_state *st, 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) */
typedef struct { int gen, keys, 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;
@@ -56,7 +58,8 @@ typedef struct {
/* 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 */
const char *sb_scale_name(int scale);
const char *sb_keys_name(int keys); /* MOTIF, 303, TURING, ARP */ /* screen name, at most 10 characters */
void sb_render_bar(const sb_state *st, const sb_params *p, int bar, sb_bar *out);

View File

@@ -52,7 +52,11 @@ static void check_primitives(const char *dir){
sb_genome g;
int bad = 0, k;
n++;
sb_genome_make(seed, &g);
{
sb_state gs; jval *sl = json_get(v, "salts");
gs.seed = seed; gs.salts[0] = sl ? (uint32_t)num(sl->items[0]) : 0; gs.salts[1] = sl ? (uint32_t)num(sl->items[1]) : 0;
sb_genome_make(&gs, &g);
}
for(k = 0; k < gj->n; k++){
jval *f = gj->items[k];
const char *key = f->key;
@@ -86,7 +90,7 @@ static void check_case(const jval *c){
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.gen = (int)num(json_get(c, "gen")); p.keys = (int)num(json_get(c, "keys")); 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");

View File

@@ -18,16 +18,17 @@
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),
var app={ state:load('state',null)||{seed:0x5EEDB0C5,salts:[0,0]}, gen:load('gen',0), keys:load('keys',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.keys>=0 && app.keys<E.KEYS_NAMES.length)) app.keys=0;
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); }
var PAGES=['SEED','GEN','KEYS','ROOT','SCALE','BPM','SLOT','ROLL','LEARN'];
function params(fillNow){ return {gen:app.gen,keys:app.keys,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('keys',app.keys); 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;
@@ -150,12 +151,13 @@
/* ---------- 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 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 KEYS':'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==='KEYS'){ app.keys=mod(app.keys+d,E.KEYS_NAMES.length); 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);
@@ -172,8 +174,8 @@
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(hold){ app.slots[app.slotSel]={seed:app.state.seed,salts:app.state.salts.slice(),gen:app.gen,keys:app.keys}; store('slots',app.slots); msg('SAVED S'+(app.slotSel+1)); return; }
var sl=app.slots[app.slotSel]; if(sl){ app.gen=sl.gen|0; app.keys=(sl.keys|0)%E.KEYS_NAMES.length; save(); setState(sl); }
}
if(pg==='ROLL') reroll(app.rollSel); // then back to page select
app.editing=false;
@@ -299,7 +301,7 @@
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},
document.getElementById('vecOut').value=JSON.stringify({seed:'0x'+hex(app.state.seed),salts:app.state.salts,params:{gen:app.gen,keys:app.keys,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(){
@@ -339,13 +341,14 @@
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==='GEN'){ label='KIT GEN'; value=E.GEN_NAMES[app.gen]; }
else if(pg==='KEYS'){ label='KEYS GEN'; value=E.KEYS_NAMES[app.keys]; }
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);
var sc=pg==='SCALE', x=sc?app.scale:app.root, gk=E.genome(app.state.seed,app.state.salts);
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==='ROLL'){ label='RE-ROLL'; value=app.rollSel?'KEYS':'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;
@@ -465,11 +468,11 @@
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;
var key=phrase+'|'+app.state.seed+'|'+app.state.salts.join('.')+'|'+app.gen+'|'+app.keys+'|'+app.scale+'|'+app.root+'|'+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 g=E.applyKey(E.genome(app.state.seed,app.state.salts),params(false)), ds=E.describe(app.state,params(false));
factsEl.innerHTML='<span>Seed <b>'+hex(app.state.seed)+'</b></span><span>Kit <b>'+E.GEN_NAMES[app.gen].toLowerCase()+'</b></span><span>Keys <b>'+E.KEYS_NAMES[app.keys].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>')+

View File

@@ -91,27 +91,36 @@ var Engine = (function(){
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={};
/* Three parts, each from its own stream: shared (the bar and the song shape) comes from the seed alone;
kit and keys also take their half's salt, so ROLL KIT / ROLL KEYS renew just that half. */
function genome(seed,salts){
var sl=salts||[0,0], r=new Rng(mix(seed,0x47454E)), fi, g={};
// shared
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.fillP=40+r.below(80);
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);
// kit
var k=new Rng(mix(roleSeedOf(seed,sl,0),0x4B4954));
g.dens=[40+k.below(60),0];
g.accentCycle=[0,3,5,3,7][k.pickW([3,3,2,2,1])];
g.frame=k.pickW(FRAME_W); g.freeK=2+k.below(3); g.freeRot=k.below(16);
// keys
var l=new Rng(mix(roleSeedOf(seed,sl,1),0x4B4559));
g.scale=l.below(SCALES.length); g.root=l.below(12);
g.dens[1]=30+l.below(70);
g.monoLen=l.below(3); g.monoRange=1+l.below(2);
g.hocket=HOCKETS[l.below(HOCKETS.length)];
g.bassDeg=[4,2,3][l.below(3)];
g.prog=PROGS[l.below(PROGS.length)];
g.motifLen=3+l.below(3);
return g;
}
function roleSeed(st,r){ return mix(mix(st.seed,0x524F4C45+r),st.salts[r]); }
function roleSeedOf(seed,salts,r){ return mix(mix(seed,0x524F4C45+r),salts[r]); }
function roleSeed(st,r){ return roleSeedOf(st.seed,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
@@ -363,7 +372,9 @@ var Engine = (function(){
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){
function bassPitch(g,idx){ var p=degPitch(g,36,idx); while(p>47) p-=12; while(p<36) p+=12; return p; }
/* MOTIF: a motif stated on the bar's chord and answered, bass on the kicks with a colour tone, the triad on the pad. */
function keysMotif(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);
@@ -399,7 +410,7 @@ var Engine = (function(){
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; }
function bp(idx){ return bassPitch(g,idx); }
var nextC=chordDeg(g,pos+1);
for(i=0;i<kickList.length;i++){
var t=kickList[i], idx=c;
@@ -413,6 +424,91 @@ var Engine = (function(){
return {mel:mel,bass:oneAtATime(bass),pad:pad};
}
/* ---------- keys generators. Each gets (keys seed, phrase variant, genome, steps, keys density, section,
bar position, the bar's kit, fill, {bar, drift}) and returns {mel, bass, pad} sorted by time.
An event's optional len overrides the default note-length rule. ---------- */
function kickTimes(kit){ var a=[]; for(var i=0;i<kit.length;i++) if(kit[i].v===0 && kit[i].t%TPS===0) a.push(kit[i].t); return a; }
function padChord(g,tones){ var pad=[], seen={}; for(var i=0;i<tones.length;i++){ var p=48+mod(degPitch(g,0,tones[i]),12); if(!seen[p]){ seen[p]=1; pad.push({t:0,v:p,vel:i===0?58:50,tr:T_PAD}); } } return pad; }
/* 303: a 16-step acid line (gate rank, degree, octave, accent, slide per step) looped over the bar and moved to
each bar's chord root. ENERGY opens more gates; a DRIFT variant flips flags. Short answers fill the bass's rests;
the pad is silent or a drone on the key's root. Slides are overlaps: whether they glide is up to the synth. */
function keys303(rs,variant,g,steps,d,sec,pos,kit,fill,x){
var r=new Rng(mix(rs,0x333033)), c=chordDeg(g,pos), pat=[], i, s;
for(i=0;i<16;i++){
var rank=0; if(i>0) rank=r.below(128);
var off=[0,2,4,6][r.pickW([10,2,3,1])];
var up=r.chance(P(24)), acc=r.chance(P(40)), sl=r.chance(P(28));
pat.push({rank:rank,off:off,up:up,acc:acc,sl:sl});
}
var drone=r.chance(P(64));
if(variant){ var vr=new Rng(mix(mix(rs,0x5641+sec),variant));
for(i=0;i<16;i++){ if(vr.chance(P(20))) pat[i].acc=!pat[i].acc; if(vr.chance(P(16))) pat[i].sl=!pat[i].sl; if(vr.chance(P(16))) pat[i].up=!pat[i].up; if(i>0 && vr.chance(P(16))) pat[i].rank=127-pat[i].rank; } }
var thr=clamp(d+16,0,128), on=[], bass=[], mel=[];
for(s=0;s<steps;s++) on.push(pat[s%16].rank<thr);
for(s=0;s<steps;s++) if(on[s]){
var p=pat[s%16], nx=s+1; while(nx<steps && !on[nx]) nx++;
bass.push({t:s*TPS,v:bassPitch(g,c+p.off)+(p.up?12:0),vel:p.acc?120:80,tr:T_BASS,len:p.sl?(nx-s)*TPS+20:TPS>>1});
}
var ar=new Rng(mix(rs,0x414E53+sec));
for(s=0;s<steps;){
if(on[s]){ s++; continue; }
var e2=s; while(e2<steps && !on[e2]) e2++;
if(e2-s>=2 && ar.chance(P(d+32))){
var tr=((g.hocket>>(s&7))&1)?T_MELA:T_MELB; // the hocket split, by step
mel.push({t:s*TPS,v:melPitch(g,c+[0,2,4][ar.below(3)]),vel:84,tr:tr,len:TPS-40});
if(e2-s>=3 && ar.chance(P(64))) mel.push({t:(s+1)*TPS,v:melPitch(g,c+[0,2,4][ar.below(3)]),vel:72,tr:tr,len:TPS-40});
}
s=e2;
}
return {mel:mel,bass:bass,pad:drone?[{t:0,v:48+g.root,vel:52,tr:T_PAD}]:[]};
}
/* TURING: an 8- or 16-bit looping shift register, one step per 16th. DRIFT flips the bit coming round (0 = locked).
Whether absolute step k flips is a hash of (seed, k); the register is rebuilt over the last 16 bars, so flips
stick for 16 bars and any bar renders on its own. Melody = the register read as a scale degree; bass = the
chord root where bit 0 is set (another bit lifts it an octave); pad = the triad. */
function rotl(v,s,L){ return s?(((v<<s)|(v>>>(L-s)))&((1<<L)-1)):v; }
function keysTuring(rs,variant,g,steps,d,sec,pos,kit,fill,x){
var r=new Rng(mix(rs,0x545552)), L=r.chance(P(64))?8:16, mask=(1<<L)-1, base=r.next()&mask;
if(!base) base=1;
var n=SCALES[g.scale].length, c=chordDeg(g,pos), flipP=P(x.drift>>2), fs=mix(rs,0x464C50);
var from=Math.max(0,x.bar-16)*steps, to=x.bar*steps, reg=rotl(base,from%L,L), k, s, j=0;
function step(v,k){ var top=(v>>>(L-1))&1; if(flipP && (mix(fs,k)>>>16)<flipP) top^=1; return ((v<<1)|top)&mask; }
for(k=from;k<to;k++) reg=step(reg,k);
var span=n*g.monoRange, mel=[], bass=[];
for(s=0;s<steps;s++){
if(((reg>>>2)&7)*18<d){ mel.push({t:s*TPS,v:melPitch(g,c+(((reg&0xFF)*(span+1))>>>8)),vel:70+((reg>>>5)&3)*10+(s%4===0?12:0),tr:((g.hocket>>(j&7))&1)?T_MELA:T_MELB}); j++; }
if(reg&1) bass.push({t:s*TPS,v:bassPitch(g,c)+(((reg>>>(L>>1))&1)?12:0),vel:s===0?110:94,tr:T_BASS});
reg=step(reg,to+s);
}
return {mel:mel,bass:bass,pad:padChord(g,[c,c+2,c+4])};
}
/* ARP: the bar's chord (triad or 7th) arpeggiated up, down, up-down or in a fixed shuffle over 1-2 octaves,
8ths at low density and 16ths at high, alternating MEL A / B. Bass = the root on every kick; pad = the same chord. */
function keysArp(rs,variant,g,steps,d,sec,pos,kit,fill,x){
var r=new Rng(mix(rs,0x415250)), n=SCALES[g.scale].length, c=chordDeg(g,pos), i, o;
var seventh=r.chance(P(64)), dir=r.below(4), chord=seventh?[c,c+2,c+4,c+6]:[c,c+2,c+4], up=[], seq;
for(o=0;o<g.monoRange;o++) for(i=0;i<chord.length;i++) up.push(chord[i]+o*n);
if(dir===0) seq=up;
else if(dir===1) seq=up.slice().reverse();
else if(dir===2){ seq=up.slice(); for(i=up.length-2;i>0;i--) seq.push(up[i]); }
else { seq=up.slice(); for(i=seq.length-1;i>0;i--){ var j=r.below(i+1), tmp=seq[i]; seq[i]=seq[j]; seq[j]=tmp; } }
var off=0; if(variant) off=new Rng(mix(mix(rs,0x5641+sec),variant)).below(seq.length);
var rate=d>=64?1:2, gs=groups(steps), last0=gs[gs.length-1][0], mel=[], bass=[], s=0, k=0;
while(s<steps){
var stp=(fill && s>=last0)?1:rate;
mel.push({t:s*TPS,v:melPitch(g,seq[(k+off)%seq.length]),vel:s%4===0?96:78,tr:(k&1)?T_MELB:T_MELA,len:stp*TPS-20});
k++; s+=stp;
}
var kicks=kickTimes(kit);
for(i=0;i<kicks.length;i++) bass.push({t:kicks[i],v:bassPitch(g,c),vel:kicks[i]===0?112:98,tr:T_BASS});
if(!kicks.length) bass.push({t:0,v:bassPitch(g,c),vel:104,tr:T_BASS});
return {mel:mel,bass:bass,pad:padChord(g,chord)};
}
var KEYS=[keysMotif,keys303,keysTuring,keysArp], KEYS_NAMES=['MOTIF','303','TURING','ARP'];
function sortDedupe(ev){
ev.sort(function(a,b){ return a.t-b.t || a.v-b.v; });
var out=[], i;
@@ -467,7 +563,7 @@ var Engine = (function(){
/* 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 g=applyKey(genome(st.seed,st.salts),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)));
@@ -508,7 +604,8 @@ var Engine = (function(){
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=[];
var keysFn=KEYS[clamp(p.keys|0,0,KEYS.length-1)];
var lt=keysFn(roleSeed(st,1),variants[1],g,g.steps,clamp(g.dens[1]+e-64,0,127),sec,pos,kit,fill,{bar:bar,drift:dr}), 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
@@ -517,9 +614,9 @@ var Engine = (function(){
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]);
for(var k=0;k<list.length;k++){ var nx=list[k+1], gap=(nx?nx.t:barTicks)-list[k].t, fixed=list[k].len!=null, len=fixed?list[k].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}); }
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,fixed?len: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); }
@@ -531,8 +628,9 @@ var Engine = (function(){
}
var ROMAN=['i','ii','iii','iv','v','vi','vii'];
function describe(st,p){
var g=applyKey(genome(st.seed),p), s=[];
var g=applyKey(genome(st.seed,st.salts),p), s=[];
s.push(p.gen===4?'amen break':FRAMES[g.frame].n);
s.push('keys '+KEYS_NAMES[clamp(p.keys|0,0,KEYS.length-1)].toLowerCase());
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');
@@ -548,6 +646,6 @@ var Engine = (function(){
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};
describe:describe,checksum:checksum,mix:mix,Rng:Rng,SCALE_NAMES:SCALE_NAMES,NOTE_NAMES:NOTE_NAMES,GEN_NAMES:GEN_NAMES,KEYS_NAMES:KEYS_NAMES};
})();
if(typeof module!=='undefined') module.exports=Engine;

View File

@@ -167,7 +167,7 @@ textarea{width:100%;min-height:140px;font:400 11px/1.4 var(--f-mono);background:
<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>
<b>Encoder pages:</b> SEED history · KIT GEN · KEYS GEN (MOTIF, 303, TURING, ARP) · ROOT · SCALE (AUTO = the seed picks) · BPM · SLOT (push loads, hold push saves) · ROLL (re-roll kit or keys) · 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>

View File

@@ -1,21 +1,28 @@
/* 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]
/* The golden-vector case list. Shared by gen-vectors.js and check-vectors.js.
keys = KEYS GEN (0 MOTIF, 1 303, 2 TURING, 3 ARP); scale/root -1 = AUTO; bars (optional) overrides BARS. */
var SEEDS=[0x5EEDB0C5,0x00000001,0xDEADBEEF,0x00009AAB]; // bars of 14, 16, 12 and 20 steps
var SETS=[[64,32],[0,0],[127,127]]; // [energy, drift]
var BARS=8;
function base(seed,gen,keys,e,d){ return {seed:seed,salts:[0,0],gen:gen,keys:keys,energy:e,drift:d,fillBars:[],scale:-1,root:-1}; }
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});
// every kit generator against every keys generator
SEEDS.forEach(function(seed){ for(var gen=0;gen<5;gen++) for(var keys=0;keys<4;keys++) SETS.forEach(function(ed){
out.push(base(seed,gen,keys,ed[0],ed[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]});
// ROLL (salts) and FILL, per keys generator
for(var keys=0;keys<4;keys++){
var a=base(0x5EEDB0C5,0,keys,64,32); a.salts=[3,0]; out.push(a);
var b=base(0x5EEDB0C5,2,keys,64,32); b.salts=[0,5]; out.push(b);
var c=base(0x5EEDB0C5,keys===3?4:1,keys,80,50); c.fillBars=[3,7]; out.push(c);
}
// TURING over more than its 16-bar window: locked at drift 0, flipping at drift 64 and 127
[0,64,127].forEach(function(d){ var t=base(0x12345678,1,2,64,d); t.bars=24; out.push(t); });
// a long 303 and ARP run for the phrase variants
[1,3].forEach(function(keys){ var t=base(0xC0FFEE00,0,keys,90,100); t.bars=16; out.push(t); });
// ROOT / SCALE pages: [seed, gen, keys, scale, root]. Covers 5-, 6-, 7- and 8-note scales and each override alone.
[[0x5EEDB0C5,0,0,14,2],[0x5EEDB0C5,2,3,19,9],[0x5EEDB0C5,4,1,0,7],[0xDEADBEEF,1,2,18,-1],[0x12345678,3,0,-1,5],[0xC0FFEE00,0,1,7,11],[0x00000001,1,3,20,0]].forEach(function(k){
var c=base(k[0],k[1],k[2],64,32); c.fillBars=[3]; c.scale=k[3]; c.root=k[4]; out.push(c);
});
return out;
}

View File

@@ -6,11 +6,11 @@ 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'); } });
prim.genomes.forEach(function(v){ n++; if(JSON.stringify(E.genome(parseInt(v.seed,16)>>>0,v.salts))!==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);
var got=R.renderCase({seed:parseInt(c.seed,16)>>>0,salts:c.salts,gen:c.gen,keys:c.keys,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);

View File

@@ -13,15 +13,15 @@ var rngV=[]; [0,1,0x5EEDB0C5,0xDEADBEEF].forEach(function(s){ var r=new E.Rng(s)
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');
var genV=C.cases().filter(function(c,i,a){ return a.findIndex(function(d){ return d.seed===c.seed && d.salts.join()===c.salts.join(); })===i; })
.map(function(c){ return {seed:R.hex(c.seed),salts:c.salts,genome:E.genome(c.seed>>>0,c.salts)}; });
fs.writeFileSync(path.join(dir,'primitives.json'),JSON.stringify({note:'mix(a,b) hash, xorshift32 Rng (next, below(n), chance(p)) and genome(seed, salts). 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){
(byGen[c.gen]=byGen[c.gen]||[]).push({id:id,seed:R.hex(c.seed),salts:c.salts,gen:c.gen,keys:c.keys,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});
});

View File

@@ -3,12 +3,13 @@ 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=[];
bars=c.bars||bars;
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 r=E.renderBar(st,{gen:c.gen,keys:c.keys,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:''); }
function caseId(c){ return hex(c.seed).slice(2)+'_s'+c.salts.join('-')+'_g'+c.gen+'_k'+c.keys+'_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:'')+(c.bars?'_b'+c.bars:''); }
module.exports={renderCase:renderCase,caseId:caseId,hex:hex};

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -1,5 +1,5 @@
{
"note": "mix(a,b) hash, xorshift32 Rng (next, below(n), chance(p)) and genome(seed). genome keys are the JS field names.",
"note": "mix(a,b) hash, xorshift32 Rng (next, below(n), chance(p)) and genome(seed, salts). genome keys are the JS field names.",
"mix": [
{
"a": "0x00000000",
@@ -270,6 +270,10 @@
"genomes": [
{
"seed": "0x5EEDB0C5",
"salts": [
0,
0
],
"genome": {
"steps": 14,
"form": [
@@ -279,36 +283,40 @@
1
],
"formName": "ABAB",
"scale": 18,
"root": 6,
"fillP": 110,
"rot": 1,
"swing": 35,
"arc": 0,
"layback": 0,
"dens": [
81,
37
59,
77
],
"fillP": 47,
"monoLen": 1,
"monoRange": 2,
"rot": 0,
"hocket": 85,
"accentCycle": 3,
"frame": 7,
"freeK": 4,
"freeRot": 3,
"scale": 0,
"root": 5,
"monoLen": 0,
"monoRange": 1,
"hocket": 180,
"bassDeg": 2,
"prog": [
0,
3,
0,
6,
5,
4
],
"swing": 35,
"arc": 1,
"motifLen": 5,
"accentCycle": 3,
"layback": 0,
"frame": 2,
"freeK": 3,
"freeRot": 5
"motifLen": 5
}
},
{
"seed": "0x00000001",
"salts": [
0,
0
],
"genome": {
"steps": 16,
"form": [
@@ -318,36 +326,40 @@
0
],
"formName": "AABA",
"scale": 16,
"root": 2,
"dens": [
80,
95
],
"fillP": 110,
"monoLen": 1,
"monoRange": 2,
"fillP": 102,
"rot": 0,
"swing": 35,
"arc": 2,
"layback": 1,
"dens": [
53,
68
],
"accentCycle": 7,
"frame": 3,
"freeK": 2,
"freeRot": 15,
"scale": 19,
"root": 0,
"monoLen": 2,
"monoRange": 1,
"hocket": 90,
"bassDeg": 3,
"bassDeg": 2,
"prog": [
0,
2,
5,
0,
5
4
],
"swing": 21,
"arc": 1,
"motifLen": 3,
"accentCycle": 3,
"layback": 0,
"frame": 0,
"freeK": 2,
"freeRot": 1
"motifLen": 3
}
},
{
"seed": "0xDEADBEEF",
"salts": [
0,
0
],
"genome": {
"steps": 12,
"form": [
@@ -357,36 +369,169 @@
0
],
"formName": "AAAA",
"scale": 10,
"root": 0,
"fillP": 80,
"rot": 0,
"swing": 50,
"arc": 1,
"layback": 0,
"dens": [
91,
66
78,
78
],
"fillP": 40,
"accentCycle": 5,
"frame": 0,
"freeK": 3,
"freeRot": 14,
"scale": 5,
"root": 1,
"monoLen": 0,
"monoRange": 2,
"rot": 1,
"hocket": 170,
"bassDeg": 4,
"prog": [
0,
1,
0,
3,
5,
6
],
"swing": 50,
"arc": 2,
"motifLen": 4,
"accentCycle": 5,
"motifLen": 5
}
},
{
"seed": "0x00009AAB",
"salts": [
0,
0
],
"genome": {
"steps": 20,
"form": [
0,
1,
0,
1
],
"formName": "ABAB",
"fillP": 60,
"rot": 1,
"swing": 0,
"arc": 0,
"layback": 1,
"dens": [
66,
42
],
"accentCycle": 7,
"frame": 6,
"freeK": 2,
"freeRot": 12,
"scale": 5,
"root": 3,
"monoLen": 0,
"monoRange": 2,
"hocket": 165,
"bassDeg": 2,
"prog": [
0,
6,
5,
4
],
"motifLen": 3
}
},
{
"seed": "0x5EEDB0C5",
"salts": [
3,
0
],
"genome": {
"steps": 14,
"form": [
0,
1,
0,
1
],
"formName": "ABAB",
"fillP": 110,
"rot": 1,
"swing": 35,
"arc": 0,
"layback": 0,
"dens": [
89,
77
],
"accentCycle": 0,
"frame": 1,
"freeK": 3,
"freeRot": 12,
"scale": 0,
"root": 5,
"monoLen": 0,
"monoRange": 1,
"hocket": 180,
"bassDeg": 2,
"prog": [
0,
6,
5,
4
],
"motifLen": 5
}
},
{
"seed": "0x5EEDB0C5",
"salts": [
0,
5
],
"genome": {
"steps": 14,
"form": [
0,
1,
0,
1
],
"formName": "ABAB",
"fillP": 110,
"rot": 1,
"swing": 35,
"arc": 0,
"layback": 0,
"dens": [
59,
33
],
"accentCycle": 3,
"frame": 7,
"freeK": 4,
"freeRot": 3
"freeRot": 3,
"scale": 12,
"root": 11,
"monoLen": 2,
"monoRange": 1,
"hocket": 170,
"bassDeg": 3,
"prog": [
0,
4,
5,
3
],
"motifLen": 5
}
},
{
"seed": "0x12345678",
"salts": [
0,
0
],
"genome": {
"steps": 16,
"form": [
@@ -396,36 +541,40 @@
1
],
"formName": "ABAB",
"scale": 0,
"root": 5,
"fillP": 43,
"rot": 1,
"swing": 0,
"arc": 2,
"layback": 1,
"dens": [
50,
83
49,
72
],
"fillP": 111,
"accentCycle": 5,
"frame": 4,
"freeK": 2,
"freeRot": 5,
"scale": 10,
"root": 2,
"monoLen": 1,
"monoRange": 1,
"rot": 2,
"hocket": 170,
"bassDeg": 3,
"monoRange": 2,
"hocket": 165,
"bassDeg": 2,
"prog": [
0,
0,
6,
5,
4
],
"swing": 0,
"arc": 1,
"motifLen": 5,
"accentCycle": 3,
"layback": 0,
"frame": 1,
"freeK": 3,
"freeRot": 9
"motifLen": 5
}
},
{
"seed": "0xC0FFEE00",
"salts": [
0,
0
],
"genome": {
"steps": 16,
"form": [
@@ -435,71 +584,32 @@
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,
"fillP": 115,
"rot": 0,
"swing": 0,
"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
49,
47
],
"fillP": 92,
"monoLen": 1,
"accentCycle": 3,
"frame": 3,
"freeK": 4,
"freeRot": 9,
"scale": 12,
"root": 3,
"monoLen": 0,
"monoRange": 1,
"rot": 1,
"hocket": 90,
"bassDeg": 2,
"hocket": 180,
"bassDeg": 4,
"prog": [
0,
3,
0,
6,
5,
4
],
"swing": 35,
"arc": 2,
"motifLen": 3,
"accentCycle": 3,
"layback": 0,
"frame": 3,
"freeK": 2,
"freeRot": 9
"motifLen": 5
}
}
]