jam-buddy / docs /03-data-model.md
salgadev's picture
Sync from GitHub 6feaf31d
b2e4883 verified
|
Raw
History Blame Contribute Delete
11.9 kB

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.

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.

{
  "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.

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.

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:

{
  "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

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.

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<Limb, number> = {
    "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:

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.

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.