# 03 — Data Model ## Overview PatternTalk's core data is small and human-editable. Most of it lives in JSON files under `apps/web/data/` and `data/training/`. ``` Prompt (user voice/text) │ ▼ ParsedRequest ◄── onomatopoeia.json + prompt grammar │ ▼ Pattern (loaded from patterns/*.json, expanded to bars + tempo) │ ▼ MIDI events │ ▼ .mid file ``` ## Prompt grammar User input is freeform text, but parses into a structured `ParsedRequest`. ```typescript interface ParsedRequest { // Source pattern onomatopoeia?: string; // e.g. "tupatupatupa" patternId?: string; // e.g. "d-beat", "blast-traditional" patternName?: string; // e.g. "D-Beat", user-friendly // Length bars: number; // default 4 beats?: number; // override pattern beats per bar // Tempo tempo?: number; // explicit BPM tempoSource: "prompt" | "reaper" | "audio" | "default"; tempoDefault: number; // 120 if nothing else // Time signature timeSignature: { numerator: number; // default 4 denominator: number; // default 4 }; // Cymbal/voice hints cymbal?: { type: "crash" | "ride" | "china" | "hihat-open" | "hihat-closed" | "splash"; pattern?: "8ths" | "quarters" | "bell" | "wash" | "upstrokes"; }; // Accent overrides accents?: ("1" | "and-of-2" | "3" | "and-of-4" | string)[]; // Style modifiers feel?: "straight" | "swing" | "half-time" | "double-time"; intensity?: "soft" | "medium" | "brutal" | "brutal-max"; } ``` ## Onomatopoeia mapping `apps/web/data/onomatopoeia.json` is the lookup table the parser checks first. ```json { "version": 1, "entries": [ { "id": "skank-beat", "patterns": ["tupatupatupa", "tupa-tupa-tupa", "chka-chka-chka"], "patternId": "skank", "defaultCymbal": { "type": "hihat-open", "pattern": "upstrokes" }, "confidence": 0.9, "notes": "Ska-style offbeat hi-hat upstrokes." }, { "id": "blast-beat", "patterns": ["krrk-krrk-krrk", "BLAM-BLAM-BLAM", "krkrkrkrk"], "patternId": "blast-traditional", "defaultIntensity": "brutal", "confidence": 0.85, "notes": "Traditional blast beat. Kick-snare alternation." }, { "id": "boom-bap", "patterns": ["boom-bap", "boom-bap-boom-bap"], "patternId": "hiphop-basic", "confidence": 0.95 }, { "id": "china-accent", "patterns": ["tss", "chka", "BLAM"], "patternId": "china-accent", "defaultCymbal": { "type": "china", "pattern": "wash" } }, { "id": "ride-bell", "patterns": ["ding-ding-ding", "ding-ding-ding-ding"], "patternId": "ride-bell-8ths", "defaultCymbal": { "type": "ride", "pattern": "bell" } } ] } ``` ### Matching logic 1. Normalize input: lowercase, strip punctuation, collapse whitespace 2. Phonetic match: Soundex or simple Levenshtein against entries 3. Syllable count: "tupatupatupa" = 4 syllables → likely 8th-note hi-hat 4. Confidence threshold: 0.7 to auto-map, lower to ask user ### User-defined onomatopoeias Users can add their own mappings via the UI. Stored in `localStorage` per device. ```typescript interface UserOnomatopoeia { phrase: string; patternId: string; cymbal?: CymbalHint; createdAt: number; } ``` ## Pattern templates `apps/web/data/patterns/*.json` — one file per pattern. Hand-authored. **This is your moat.** ```typescript interface PatternTemplate { id: string; // unique slug name: string; // "D-Beat" description: string; // screenreader-friendly text tags: string[]; // searchable genre: "metal" | "rock" | "punk" | "pop" | "jazz" | "funk" | "latin" | "hiphop"; defaultTempo: number; // BPM defaultBars: number; // how many bars this template covers swingRatio?: number; // 0.0 = straight, 0.33 = shuffle hits: Hit[]; } interface Hit { position: number; // 0.0 to 1.0, fraction of one bar limb: Limb; velocity: number; // 0-127 MIDI velocity accent?: boolean; // visual + audio emphasis } type Limb = | "kick" // right foot | "snare" // left hand (default) or right hand (cross-handed) | "hihat" // right hand (default) or left hand (cross-handed) | "hihat-open" // same as hihat but with open hi-hat sample | "ride" | "ride-bell" | "crash" | "china" | "splash" | "tom-1" // highest | "tom-2" | "tom-3" // lowest floor tom | "cross-stick"; // rim click ``` ### Example: D-Beat `apps/web/data/patterns/d-beat.json`: ```json { "id": "d-beat", "name": "D-Beat", "description": "D-beat pattern at 180 BPM. Kick plays on every beat with 8th-note doubles, snare hits on 2 and 4, ride bell plays steady 8ths throughout. Common in Discharge, Entombed, and Scandinavian crust punk.", "tags": ["metal", "punk", "discharge", "entombed", "crust", "fast"], "genre": "metal", "defaultTempo": 180, "defaultBars": 1, "swingRatio": 0, "hits": [ { "position": 0.0, "limb": "kick", "velocity": 110 }, { "position": 0.125, "limb": "kick", "velocity": 80 }, { "position": 0.25, "limb": "snare", "velocity": 100 }, { "position": 0.375, "limb": "kick", "velocity": 80 }, { "position": 0.5, "limb": "kick", "velocity": 110 }, { "position": 0.625, "limb": "kick", "velocity": 80 }, { "position": 0.75, "limb": "snare", "velocity": 100 }, { "position": 0.875, "limb": "kick", "velocity": 80 }, { "position": 0.0, "limb": "ride-bell", "velocity": 70 }, { "position": 0.125, "limb": "ride-bell", "velocity": 70 }, { "position": 0.25, "limb": "ride-bell", "velocity": 70 }, { "position": 0.375, "limb": "ride-bell", "velocity": 70 }, { "position": 0.5, "limb": "ride-bell", "velocity": 70 }, { "position": 0.625, "limb": "ride-bell", "velocity": 70 }, { "position": 0.75, "limb": "ride-bell", "velocity": 70 }, { "position": 0.875, "limb": "ride-bell", "velocity": 70 } ] } ``` ### Pattern library — starting list **Metal (priority, 8 patterns):** - `d-beat.json` — D-beat - `blast-traditional.json` — traditional blast (kick-snare) - `blast-hammer.json` — hammer-and-tongs blast - `blast-hyper.json` — hyperblast (double kick 16ths) - `half-time-metal.json` — Mastodon/Baroness half-time - `djent-polyrhythm.json` — Meshuggah-style 4-over-3 kick - `doom-slow.json` — Sleep/Electric Wizard doom - `groove-metal.json` — mid-tempo groove metal **Punk/Hardcore (4 patterns):** - `punk-rock.json` — Ramones/Misfits 4-on-floor - `hardcore.json` — Black Flag/Converge - `skank.json` — ska skank beat (hi-hat upstrokes) - `reggae-one-drop.json` — reggae **Rock/Pop (4 patterns):** - `rock-basic.json` — AC/DC/Beatles standard rock - `rock-half-time.json` — half-time shuffle - `pop-groove.json` — modern pop - `country-train.json` — train beat **Other genres (4 patterns):** - `bossa-nova.json` — bossa nova - `jazz-swing.json` — jazz ride swing - `funk-new-orleans.json` — NOLA funk - `hiphop-basic.json` — boom-bap **Target: 18–20 patterns before hackathon, more as time permits.** ## Pattern engine Lives in `apps/web/lib/patterns/engine.ts`. ### Inputs - `PatternTemplate` - `bars: number` - `tempoBpm: number` - `timeSignature: { numerator, denominator }` - Optional `cymbal` override (replaces the default cymbal) - Optional `accents` (boosts velocity on specified positions) - Optional `feel` modifier ### Process ``` 1. Load template 2. Expand to requested bars: - If template.defaultBars === requested bars: use as-is - Else: repeat the pattern N times, optionally vary velocity (humanize) 3. Apply cymbal override: - If cymbal specified: replace ride/hihat hits with cymbal type - Velocity scaled: cymbal typically quieter than snare 4. Apply accents: - Find positions matching accent specs - Boost velocity by +20 5. Apply feel modifier: - "swing": delay 8th notes on the "and" by swingRatio - "half-time": halve the effective tempo of snare/hihat - "double-time": double it 6. Convert positions (0.0-1.0) to absolute ticks at tempoBpm 7. Return array of MIDI events ``` ### Output ```typescript interface MidiEvent { tick: number; // absolute tick position limb: Limb; velocity: number; // 0-127 duration: number; // ticks (typically 1-10 for drums) } ``` ## MIDI generation `apps/web/lib/midi/generator.ts` uses `@tonejs/midi`. ```typescript import { Midi } from "@tonejs/midi"; function eventsToMidi(events: MidiEvent[]): Uint8Array { const midi = new Midi(); midi.header.setTempo(events.bpm); // bpm on master track midi.header.timeSignature = [4, 4]; const drums = midi.addTrack(); drums.name = "PatternTalk Drums"; for (const ev of events) { drums.addNote({ midi: limbToGeneralMidi(ev.limb), ticks: ev.tick, durationTicks: ev.duration, velocity: ev.velocity / 127, }); } return midi.toArray(); } function limbToGeneralMidi(limb: Limb): number { // GM Drum Map const map: Record = { "kick": 36, "snare": 38, "cross-stick": 37, "hihat": 42, "hihat-open": 46, "ride": 51, "ride-bell": 53, "crash": 49, "china": 52, "splash": 55, "tom-1": 50, "tom-2": 47, "tom-3": 45, }; return map[limb]; } ``` ## Audio sample prompts When PatternTalk calls the audio service, the prompt includes both the pattern context and a style hint: ```typescript interface AudioPrompt { pattern: string; // pattern name limb?: Limb; // specific sample to generate styleHints: string[]; // ["brutal drums", "18-inch china", "trashy"] durationSeconds: number; // 1-5 for one-shots, 5-15 for loops intensity: "soft" | "medium" | "brutal" | "brutal-max"; loraAdapter?: string; // "brutal-drums" by default } ``` Examples: - Limb-specific one-shot: `{ pattern: "D-Beat", limb: "china", styleHints: ["trashy", "18-inch", "panic-attack"], durationSeconds: 3, intensity: "brutal-max" }` - Loop preview: `{ pattern: "D-Beat", styleHints: ["brutal drums", "tight snare"], durationSeconds: 8, intensity: "brutal" }` ## Training data manifest `data/training/manifest.yaml` describes every audio file used to train the brutal-drum LoRA. ```yaml version: 1 lora: name: patterntalk-brutal-drums base_model: stable-audio-3-small training_steps: 1500 learning_rate: 1e-4 rank: 32 samples: - id: kick-001 path: oneshots/kick-tight.wav category: kick tags: [tight, clicky, triggered] source: original-recording license: CC-BY duration_seconds: 1.2 - id: snare-discharge-001 path: oneshots/snare-trashy.wav category: snare tags: [trashy, snappy, crust] source: freesound.org/user-x license: CC-BY duration_seconds: 0.8 - id: loop-dbeat-discharge-style path: loops/dbeat-180.wav category: loop tags: [d-beat, 180bpm, discharge-style] source: original-recording license: CC-BY duration_seconds: 4.0 bpm: 180 ``` **License discipline:** All samples must be CC0, CC-BY, or original recordings. No unlicensed material. Documented in the manifest, surfaced in the model card. ## What we're explicitly not modeling (yet) - User accounts (out of scope) - Cloud-saved pattern libraries (localStorage only) - Pattern remixing/chaining - Time-signature patterns other than 4/4 (the parser supports it but templates are 4/4) - Polymeter / metric modulation - Genre mixing (one pattern per request, not hybrids) These are all reasonable post-hackathon extensions.