#!/usr/bin/env python3 """ Generate N synthetic session JSON files (daw_session_spec_v0.3) for training. Outputs are compatible with modelw.dataset.SessionDataset (tracks + clip_library + arrangement). Usage: python scripts/generate_session_corpus.py --count 200 --out synthetic/sessions --seed 42 """ from __future__ import annotations import argparse import json import random import sys from pathlib import Path # repo root on path for optional validation _ROOT = Path(__file__).resolve().parents[1] if str(_ROOT) not in sys.path: sys.path.insert(0, str(_ROOT)) # Keys as in project.key (tokenizer normalizes to KEY_*) KEY_DISPLAY = [ "C major", "C minor", "C# major", "C# minor", "D major", "D minor", "D# major", "D# minor", "E major", "E minor", "F major", "F minor", "F# major", "F# minor", "G major", "G minor", "G# major", "G# minor", "A major", "A minor", "A# major", "A# minor", "B major", "B minor", ] # Semitone offset from C for root (MIDI pitch class of tonic) _KEY_PC = { "C major": 0, "C minor": 0, "C# major": 1, "C# minor": 1, "D major": 2, "D minor": 2, "D# major": 3, "D# minor": 3, "E major": 4, "E minor": 4, "F major": 5, "F minor": 5, "F# major": 6, "F# minor": 6, "G major": 7, "G minor": 7, "G# major": 8, "G# minor": 8, "A major": 9, "A minor": 9, "A# major": 10, "A# minor": 10, "B major": 11, "B minor": 11, } STYLES = [ "trap", "reggaeton", "house", "techno", "edm", "hiphop", "lofi", "ambient", "pop", "rnb", "drill", "cinematic", ] MOOD_POOL = [ ("dark", 0.35), ("aggressive", 0.25), ("energetic", 0.2), ("calm", 0.15), ("mysterious", 0.15), ("dreamy", 0.12), ("melancholic", 0.12), ("playful", 0.1), ] SECTION_LAYOUT = [ ("intro", 1, 8), ("verse", 9, 24), ("chorus", 25, 40), ("bridge", 41, 56), ("outro", 57, 64), ] def _rng(seed: int) -> random.Random: return random.Random(seed) def drum_notes_four_bars(r: random.Random) -> list[dict]: """Dense trap-style drums over 4 bars (16 beats) for token count.""" notes: list[dict] = [] kick, snare, ch, ohat = 36, 38, 42, 46 for beat in range(16): t = float(beat) vel_k = r.randint(100, 127) vel_s = r.randint(95, 118) vel_h = r.randint(45, 95) # kick 1 and 3 in bar (beat % 4 in 0,2) if beat % 4 == 0: notes.append({"pitch": kick, "start_beat": t, "duration_beat": 0.25, "velocity": vel_k}) elif beat % 4 == 2: notes.append({"pitch": kick, "start_beat": t, "duration_beat": 0.2, "velocity": vel_k - 10}) # snare on 2 and 4 if beat % 4 == 1: notes.append({"pitch": snare, "start_beat": t, "duration_beat": 0.18, "velocity": vel_s}) # 8th hats notes.append({"pitch": ch, "start_beat": t, "duration_beat": 0.12, "velocity": vel_h}) notes.append({"pitch": ch, "start_beat": t + 0.5, "duration_beat": 0.1, "velocity": vel_h - 15}) # open hat every 4 beats if beat % 4 == 3: notes.append({"pitch": ohat, "start_beat": t + 0.75, "duration_beat": 0.2, "velocity": 85}) return notes def bass_notes_four_bars(root_pc: int, r: random.Random) -> list[dict]: """808-ish bass: root + fifth patterns, 8th notes.""" notes: list[dict] = [] root = 36 + root_pc % 12 fifth = root + 7 scale = [0, 3, 5, 7, 10] # minor-ish walk for i in range(32): t = i * 0.5 deg = scale[r.randint(0, len(scale) - 1)] pc = (root_pc + deg) % 12 pitch = 36 + pc pitch = max(28, min(pitch, 55)) notes.append( { "pitch": int(pitch), "start_beat": t, "duration_beat": 0.45, "velocity": r.randint(85, 115), } ) return notes def lead_notes_four_bars(root_pc: int, r: random.Random) -> list[dict]: """Busy lead line (16th-based) for token count.""" notes: list[dict] = [] base = 60 + root_pc % 12 for i in range(64): t = i * 0.25 step = r.choice([-2, 0, 2, 3, 5, 7, 12]) pitch = base + (step + r.randint(-2, 2)) pitch = max(55, min(pitch, 95)) notes.append( { "pitch": int(pitch), "start_beat": t, "duration_beat": 0.22, "velocity": r.randint(70, 110), } ) return notes def pad_notes_four_bars(root_pc: int, r: random.Random) -> list[dict]: """Layered pads + extra tones.""" notes: list[dict] = [] r3 = root_pc % 12 triad = [48 + r3, 52 + r3, 55 + r3, 60 + r3, 64 + r3, 67 + r3] for b in range(8): t = float(b * 2) for j, p in enumerate(triad): notes.append( { "pitch": p + r.choice([0, 12]), "start_beat": t, "duration_beat": 1.9, "velocity": 50 + j * 3 + r.randint(0, 8), } ) return notes def fx_notes_four_bars(r: random.Random) -> list[dict]: """Many short FX hits so sections pass min_seq_len.""" notes: list[dict] = [] for i in range(48): t = i * 0.25 + r.uniform(0, 0.05) notes.append( { "pitch": r.randint(60, 84), "start_beat": t, "duration_beat": 0.15, "velocity": r.randint(40, 90), } ) return notes def clip_shell(length_bars: int, notes: list[dict]) -> dict: return {"type": "midi", "ppq": 480, "timebase": "beats", "length_bars": length_bars, "notes": notes, "cc": []} def build_session(index: int, r: random.Random) -> dict: style = r.choice(STYLES) key_disp = r.choice(KEY_DISPLAY) root_pc = _KEY_PC[key_disp] bpm = r.randint(82, 174) title = f"Synthetic {style.title()} — {key_disp} ({index})" mood_primary = r.choice(MOOD_POOL)[0] _cand = [m for m in MOOD_POOL if m[0] != mood_primary] mood_secondary = r.choice(_cand)[0] energy = round(r.uniform(0.35, 0.95), 2) tension = round(r.uniform(0.25, 0.85), 2) density = round(r.uniform(0.35, 0.9), 2) lib = { "pat_drums_main": clip_shell(4, drum_notes_four_bars(r)), "pat_bass_main": clip_shell(4, bass_notes_four_bars(root_pc, r)), "pat_lead_main": clip_shell(4, lead_notes_four_bars(root_pc, r)), "pat_pad_main": clip_shell(4, pad_notes_four_bars(root_pc, r)), "pat_fx_main": clip_shell(4, fx_notes_four_bars(r)), } timeline_drums = [ {"type": "midi", "ref": "pat_drums_main", "start_bar": 1, "loop_count": 2}, {"type": "midi", "ref": "pat_drums_main", "start_bar": 9, "loop_count": 4}, {"type": "midi", "ref": "pat_drums_main", "start_bar": 25, "loop_count": 4}, {"type": "midi", "ref": "pat_drums_main", "start_bar": 41, "loop_count": 4}, {"type": "midi", "ref": "pat_drums_main", "start_bar": 57, "loop_count": 2}, ] timeline_bass = [ {"type": "midi", "ref": "pat_bass_main", "start_bar": 1, "loop_count": 2}, {"type": "midi", "ref": "pat_bass_main", "start_bar": 9, "loop_count": 4}, {"type": "midi", "ref": "pat_bass_main", "start_bar": 25, "loop_count": 4}, {"type": "midi", "ref": "pat_bass_main", "start_bar": 41, "loop_count": 4}, {"type": "midi", "ref": "pat_bass_main", "start_bar": 57, "loop_count": 2}, ] timeline_lead = [ {"type": "midi", "ref": "pat_lead_main", "start_bar": 1, "loop_count": 2}, {"type": "midi", "ref": "pat_lead_main", "start_bar": 9, "loop_count": 4}, {"type": "midi", "ref": "pat_lead_main", "start_bar": 25, "loop_count": 4}, {"type": "midi", "ref": "pat_lead_main", "start_bar": 41, "loop_count": 4}, {"type": "midi", "ref": "pat_lead_main", "start_bar": 57, "loop_count": 2}, ] timeline_pad = [ {"type": "midi", "ref": "pat_pad_main", "start_bar": 1, "loop_count": 2}, {"type": "midi", "ref": "pat_pad_main", "start_bar": 9, "loop_count": 4}, {"type": "midi", "ref": "pat_pad_main", "start_bar": 25, "loop_count": 4}, {"type": "midi", "ref": "pat_pad_main", "start_bar": 41, "loop_count": 4}, {"type": "midi", "ref": "pat_pad_main", "start_bar": 57, "loop_count": 2}, ] timeline_fx = [ {"type": "midi", "ref": "pat_fx_main", "start_bar": 1, "loop_count": 2}, {"type": "midi", "ref": "pat_fx_main", "start_bar": 9, "loop_count": 4}, {"type": "midi", "ref": "pat_fx_main", "start_bar": 25, "loop_count": 4}, {"type": "midi", "ref": "pat_fx_main", "start_bar": 41, "loop_count": 4}, {"type": "midi", "ref": "pat_fx_main", "start_bar": 57, "loop_count": 2}, ] def track_block(tid: str, name: str, role: str, timeline: list[dict]) -> dict: return { "track_id": tid, "name": name, "role": role, "semantic_labels": { "mood": [[mood_primary, 0.4], [mood_secondary, 0.25], ["energetic", 0.15], ["calm", 0.1]], "timbre": [["warm", 0.3], ["bright", 0.25], ["wide", 0.25], ["punchy", 0.2]], "envelope": [["stable", 0.4], ["short_decay", 0.3], ["long_release", 0.2], ["swell", 0.1]], "space": [["stereo", 0.35], ["mono", 0.25], ["close", 0.2], ["far", 0.2]], }, "instrument": { "plugin": "Synth.Generic", "preset": f"{role}.Generated", "macro_params": {"drive": {"__default__": 0.0, "__value__": round(r.uniform(0.0, 0.2), 2)}}, }, "mix": { "gain_db": {"__default__": 0.0, "__value__": 0.0}, "volume_db": {"__default__": -6.0, "__value__": -6.0}, "pan": {"__default__": 0.0, "__value__": 0.0}, "mute": {"__default__": False, "__value__": False}, "solo": {"__default__": False, "__value__": False}, }, "insert_fx": [], "sends": {}, "timeline": timeline, "automation": [], } return { "session_id": f"synth_corpus_{index:04d}", "schema_version": "daw_session_spec_v0.3", "metadata": { "title": title, "prompt": f"{style} track, {key_disp}, {bpm} bpm, generated corpus", "style": style, "duration_bars": 64, "created_by": "scripts/generate_session_corpus.py", }, "project": { "sample_rate_hz": 48000, "bit_depth": "24bit", "tempo_map": [{"bar": 1, "bpm": bpm}], "time_signature_map": [{"bar": 1, "numerator": 4, "denominator": 4}], "swing": {"__default__": 0.0, "__value__": round(r.uniform(0.0, 0.06), 3)}, "key": key_disp, "render": { "normalize": {"__default__": False, "__value__": False}, "dither": {"__default__": "none", "__value__": "none"}, }, }, "semantic_song_labels": { "mood": [[mood_primary, 0.45], [mood_secondary, 0.3], ["energetic", 0.15], ["calm", 0.1]], "energy": {"__default__": 0.5, "__value__": energy}, "tension": {"__default__": 0.5, "__value__": tension}, "density": {"__default__": 0.5, "__value__": density}, }, "arrangement": {"sections": [{"name": n, "bar_start": a, "bar_end": b} for n, a, b in SECTION_LAYOUT]}, "libraries": {"clip_library": lib, "automation_library": {}}, "routing": { "buses": [ { "bus_id": "BUS_VERB", "name": "Room", "plugin_chain": [ { "plugin": "FX.Reverb", "params": { "wet": {"__default__": 0.2, "__value__": 0.22}, "decay_s": {"__default__": 1.0, "__value__": 1.1}, }, } ], } ], "sidechain": [{"from": "T1", "to": "T2", "type": "compressor", "amount": 0.5}], }, "tracks": [ track_block("T1", "Drums", "drums", timeline_drums), track_block("T2", "Bass", "bass", timeline_bass), track_block("T3", "Lead", "lead", timeline_lead), track_block("T4", "Pad", "pad", timeline_pad), track_block("T5", "FX", "fx", timeline_fx), ], "master": { "target_lufs": -9.0, "plugin_chain": [ { "plugin": "FX.Limiter", "params": {"ceiling_db": {"__default__": -1.0, "__value__": -0.8}}, } ], }, "focus": { "touch_a_lot": ["groove", "low end"], "touch_some": ["width"], "leave_default": ["tempo ramps"], }, } def main() -> None: ap = argparse.ArgumentParser() ap.add_argument("--count", type=int, default=200) ap.add_argument("--out", type=Path, default=Path("synthetic/sessions")) ap.add_argument("--prefix", type=str, default="session_gen") ap.add_argument("--seed", type=int, default=42) ap.add_argument("--validate", action="store_true", help="Smoke-test SessionDataset on first file") args = ap.parse_args() args.out.mkdir(parents=True, exist_ok=True) for i in range(1, args.count + 1): r = _rng(args.seed + i * 7919) session = build_session(i, r) path = args.out / f"{args.prefix}_{i:03d}.json" with open(path, "w", encoding="utf-8") as f: json.dump(session, f, indent=2, ensure_ascii=False) if i == 1 or i == args.count: print(f"Wrote {path}") if args.validate: from modelw.dataset import SessionDataset, SessionDatasetConfig from modelw.tokenizer import MIDITokenizer, TokenizerConfig tok = MIDITokenizer(TokenizerConfig()) first = args.out / f"{args.prefix}_001.json" cfg = SessionDatasetConfig( sessions_dir=str(args.out.resolve()), cache_dir=str((_ROOT / "cache/sessions_validate").resolve()), max_files=1, train_split=1.0, ) ds = SessionDataset(cfg, tokenizer=tok, split="train", preprocess=True) print(f"Validation: {len(ds)} samples from {first.name}") print(f"Done: {args.count} files in {args.out}") if __name__ == "__main__": main()