Bach

An AI composer that creates original music in any style, to help you find inspiration.

---
format: typebulb/v1
name: Bach
---

**code.tsx**

```tsx
import React, { useState, useEffect, useRef, useCallback, useMemo } from "react";
import { createRoot } from "react-dom/client";

// ============ Types ============
type WaveformType = "sine" | "square" | "sawtooth" | "triangle";
type FilterType = "lowpass" | "highpass" | "bandpass" | "notch";

interface OscSettings { waveform: WaveformType; detune: number; gain: number }
interface FilterSettings { type: FilterType; frequency: number; Q: number }
interface EnvSettings { attack: number; decay: number; sustain: number; release: number }
interface FxSettings { delayTime: number; delayFeedback: number; reverbMix: number }

interface SequenceNote { freq: number; time: number; duration: number; voice?: number }
interface Composition { patch: SynthState; score: string | string[]; name?: string }

interface SynthState {
  osc1: OscSettings;
  osc2: OscSettings;
  filter: FilterSettings;
  env: EnvSettings;
  fx: FxSettings;
  master: number;
}

// ============ Constants ============
const NOTE_NAMES = ['C', 'C#', 'D', 'D#', 'E', 'F', 'F#', 'G', 'G#', 'A', 'A#', 'B'];
const KEY_BINDINGS: Record<string, string> = {
  'C4': 'a', 'C#4': 'w', 'D4': 's', 'D#4': 'e', 'E4': 'd', 'F4': 'f',
  'F#4': 't', 'G4': 'g', 'G#4': 'y', 'A4': 'h', 'A#4': 'u', 'B4': 'j',
  'C5': 'k', 'C#5': 'o', 'D5': 'l', 'D#5': 'p', 'E5': ';',
};

function noteToFreq(note: string): number | null {
  const m = note.match(/^([A-G])(#|b)?(\d)$/);
  if (!m) return null;
  const semi: Record<string, number> = { C: 0, D: 2, E: 4, F: 5, G: 7, A: 9, B: 11 };
  let s = semi[m[1]];
  if (m[2] === '#') s++; else if (m[2] === 'b') s--;
  return 440 * Math.pow(2, (s - 9) / 12 + (parseInt(m[3]) - 4));
}

function parseChord(tok: string): number[] {
  if (tok.startsWith('[') && tok.endsWith(']'))
    return tok.slice(1, -1).split(',').map(n => noteToFreq(n.trim())).filter((f): f is number => f !== null);
  const f = noteToFreq(tok);
  return f ? [f] : [];
}

const KEYBOARD: [string, number, string][] = [];
for (let oct = 2; oct <= 6; oct++) {
  for (let i = 0; i < NOTE_NAMES.length; i++) {
    if (oct === 6 && i > 0) break;
    const note = NOTE_NAMES[i];
    const name = `${note}${oct}`;
    const freq = noteToFreq(name)!;
    KEYBOARD.push([KEY_BINDINGS[name] || `_${name.toLowerCase().replace('#', 's')}`, freq, note]);
  }
}

const DEFAULT_STATE: SynthState = {
  osc1: { waveform: "sawtooth", detune: 0, gain: 0.3 },
  osc2: { waveform: "square", detune: -5, gain: 0.2 },
  filter: { type: "lowpass", frequency: 2000, Q: 1 },
  env: { attack: 0.01, decay: 0.1, sustain: 0.7, release: 0.3 },
  fx: { delayTime: 0, delayFeedback: 0, reverbMix: 0.2 },
  master: 0.35,
};

// ============ Formatters ============
const fmt = {
  pct: (v: number) => (v * 100).toFixed(0),
  signed: (v: number) => (v > 0 ? '+' : '') + v.toFixed(0),
  freq: (v: number) => v < 1000 ? v.toFixed(0) : (v / 1000).toFixed(1) + 'k',
  time: (v: number) => v < 1 ? (v * 1000).toFixed(0) : v.toFixed(1) + 's',
  ms: (v: number) => (v * 1000).toFixed(0),
  dec: (v: number) => v.toFixed(1),
};

function parseScore(input: any): SequenceNote[] {
  if (Array.isArray(input)) {
    if (input.length && typeof input[0] === 'string') return parseScore(input.join('\n'));
    return input;
  }
  if (typeof input !== 'string') return [];
  if (input.includes('@bpm') || input.includes('@step')) return parseTracker(input);
  const tokens = input.trim().split(/\s+/);
  const notes: SequenceNote[] = [];
  let t = 0;
  for (let i = 0; i < tokens.length; i += 2) {
    const pitch = tokens[i];
    const dur = parseFloat(tokens[i + 1]);
    if (isNaN(dur)) continue;
    if (pitch !== 'R') {
      for (const freq of parseChord(pitch))
        notes.push({ freq, time: t, duration: dur });
    }
    t += dur;
  }
  return notes;
}

function parseTracker(input: string): SequenceNote[] {
  const lines = input.split('\n');
  let bpm = 120;
  let stepNum = 1, stepDen = 8;
  let swing = 0.5;
  const rows: string[][] = [];

  for (const line of lines) {
    const t = line.trim();
    if (!t || t.startsWith('#') || /^[-=]+$/.test(t)) continue;
    if (t.startsWith('@bpm')) { bpm = parseFloat(t.split(/\s+/)[1]); continue; }
    if (t.startsWith('@step')) {
      const p = t.split(/\s+/)[1].split('/');
      stepNum = parseInt(p[0]); stepDen = parseInt(p[1]); continue;
    }
    if (t.startsWith('@swing')) { swing = parseFloat(t.split(/\s+/)[1]); continue; }
    if (t.startsWith('@')) continue;
    rows.push(t.split(/\s+/));
  }

  const stepDur = (60 / bpm) * 4 * (stepNum / stepDen);
  const pairDur = 2 * stepDur;
  const st = (i: number) => {
    const p = Math.floor(i / 2) * pairDur;
    return i % 2 === 0 ? p : p + swing * pairDur;
  };
  const voiceCount = rows.length > 0 ? Math.max(...rows.map(r => r.length)) : 0;
  const active: ({ freqs: number[]; start: number; dur: number } | null)[] = Array(voiceCount).fill(null);
  const notes: SequenceNote[] = [];

  const flush = (v: number) => {
    if (!active[v]) return;
    for (const freq of active[v]!.freqs)
      notes.push({ freq, time: active[v]!.start, duration: active[v]!.dur, voice: v });
    active[v] = null;
  };

  for (let i = 0; i < rows.length; i++) {
    const time = st(i);
    const dur = st(i + 1) - time;
    for (let v = 0; v < voiceCount; v++) {
      const tok = (rows[i][v] || '.').trim();
      if (tok === '|') {
        if (active[v]) active[v]!.dur += dur;
      } else if (tok === '.') {
        flush(v);
      } else {
        flush(v);
        const freqs = parseChord(tok);
        if (freqs.length) active[v] = { freqs, start: time, dur };
      }
    }
  }

  for (let v = 0; v < voiceCount; v++) flush(v);
  return notes;
}

// ============ Audio Engine ============
class SynthEngine {
  public ctx: AudioContext | null = null;
  private nodes: { master: GainNode; filter: BiquadFilterNode; delay: DelayNode; delayGain: GainNode; delayWet: GainNode; reverbGain: GainNode; dryGain: GainNode } | null = null;
  private activeNotes = new Map<string, { oscs: OscillatorNode[]; gains: GainNode[] }>();
  private sequenceTimeouts: number[] = [];

  init(state: SynthState): boolean {
    if (this.ctx) return false;
    this.ctx = new AudioContext();
    const { ctx } = this;

    const master = ctx.createGain();
    master.gain.value = state.master;

    const filter = ctx.createBiquadFilter();
    Object.assign(filter, { type: state.filter.type });
    filter.frequency.value = state.filter.frequency;
    filter.Q.value = state.filter.Q;

    const delay = ctx.createDelay(2);
    delay.delayTime.value = state.fx.delayTime;
    const delayGain = ctx.createGain();
    delayGain.gain.value = state.fx.delayFeedback;
    const delayWet = ctx.createGain();
    delayWet.gain.value = state.fx.delayFeedback;

    const reverb = ctx.createConvolver();
    reverb.buffer = this.createImpulse(ctx);
    const reverbGain = ctx.createGain();
    reverbGain.gain.value = state.fx.reverbMix;
    const dryGain = ctx.createGain();
    dryGain.gain.value = 1 - state.fx.reverbMix;

    // Routing
    filter.connect(dryGain);
    filter.connect(delay);
    delay.connect(delayGain).connect(delay);
    delay.connect(delayWet);
    filter.connect(reverb).connect(reverbGain);
    dryGain.connect(master);
    delayWet.connect(master);
    reverbGain.connect(master);
    master.connect(ctx.destination);

    this.nodes = { master, filter, delay, delayGain, delayWet, reverbGain, dryGain };
    return true;
  }

  private createImpulse(ctx: AudioContext) {
    const len = ctx.sampleRate * 2;
    const buf = ctx.createBuffer(2, len, ctx.sampleRate);
    for (let c = 0; c < 2; c++) {
      const data = buf.getChannelData(c);
      for (let i = 0; i < len; i++) data[i] = (Math.random() * 2 - 1) * Math.pow(1 - i / len, 2);
    }
    return buf;
  }

  updateParams(state: SynthState) {
    if (!this.nodes) return;
    const { master, filter, delay, delayGain, delayWet, reverbGain, dryGain } = this.nodes;
    master.gain.value = state.master;
    filter.type = state.filter.type;
    filter.frequency.value = state.filter.frequency;
    filter.Q.value = state.filter.Q;
    delay.delayTime.value = state.fx.delayTime;
    delayGain.gain.value = state.fx.delayFeedback;
    delayWet.gain.value = state.fx.delayFeedback;
    reverbGain.gain.value = state.fx.reverbMix;
    dryGain.gain.value = 1 - state.fx.reverbMix;
  }

  noteOn(key: string, freq: number, state: SynthState) {
    if (!this.ctx || !this.nodes || this.activeNotes.has(key)) return;
    const { ctx, nodes } = this;
    const now = ctx.currentTime;
    const oscs: OscillatorNode[] = [];
    const gains: GainNode[] = [];

    [state.osc1, state.osc2].forEach(osc => {
      if (osc.gain === 0) return;
      const o = ctx.createOscillator();
      o.type = osc.waveform;
      o.frequency.value = freq;
      o.detune.value = osc.detune;

      const g = ctx.createGain();
      g.gain.setValueAtTime(0, now);
      g.gain.linearRampToValueAtTime(osc.gain, now + state.env.attack);
      g.gain.linearRampToValueAtTime(osc.gain * state.env.sustain, now + state.env.attack + state.env.decay);

      o.connect(g).connect(nodes.filter);
      o.start(now);
      oscs.push(o);
      gains.push(g);
    });

    this.activeNotes.set(key, { oscs, gains });
  }

  noteOff(key: string, release: number) {
    const note = this.activeNotes.get(key);
    if (!this.ctx || !note) return;
    const now = this.ctx.currentTime;

    note.gains.forEach((g, i) => {
      g.gain.cancelScheduledValues(now);
      g.gain.setValueAtTime(g.gain.value, now);
      g.gain.linearRampToValueAtTime(0, now + release);
      note.oscs[i].stop(now + release + 0.1);
    });

    this.activeNotes.delete(key);
  }

  playComposition(comp: Composition, onNote: (freq: number, on: boolean, voice: number) => void, startOffset = 0) {
    if (!this.ctx || !this.nodes) return;
    this.stopSequence();
    const notes = parseScore(comp.score);

    notes.forEach((n, i) => {
      const noteEnd = n.time + n.duration;
      if (noteEnd <= startOffset) return;
      const key = `auto-${i}`;
      const v = n.voice ?? 0;
      const onDelay = Math.max(0, n.time - startOffset);
      const offDelay = noteEnd - startOffset;

      const startId = window.setTimeout(() => {
        this.noteOn(key, n.freq, comp.patch);
        onNote(n.freq, true, v);
      }, onDelay * 1000);

      const endId = window.setTimeout(() => {
        this.noteOff(key, comp.patch.env.release);
        onNote(n.freq, false, v);
      }, offDelay * 1000);

      this.sequenceTimeouts.push(startId, endId);
    });
  }

  stopSequence() {
    this.sequenceTimeouts.forEach(clearTimeout);
    this.sequenceTimeouts = [];
    this.activeNotes.forEach((_, key) => {
      if (key.startsWith('auto-')) this.noteOff(key, 0.1);
    });
  }

  get isReady() { return !!this.ctx; }
}

// ============ UI Components ============
function Knob({ value, min, max, onChange, label, format = (v: number) => v.toFixed(0) }: {
  value: number; min: number; max: number; onChange: (v: number) => void; label: string; format?: (v: number) => string;
}) {
  const drag = useRef({ active: false, startY: 0, startVal: 0 });

  const onMouseDown = (e: React.MouseEvent) => {
    drag.current = { active: true, startY: e.clientY, startVal: value };
    const onMove = (e: MouseEvent) => {
      if (!drag.current.active) return;
      const delta = (drag.current.startY - e.clientY) * (max - min) / 100;
      onChange(Math.max(min, Math.min(max, drag.current.startVal + delta)));
    };
    const onUp = () => {
      drag.current.active = false;
      document.removeEventListener('mousemove', onMove);
      document.removeEventListener('mouseup', onUp);
    };
    document.addEventListener('mousemove', onMove);
    document.addEventListener('mouseup', onUp);
  };

  const rotation = ((value - min) / (max - min)) * 270 - 135;

  return (
    <div className="knob-wrap">
      <div className="knob" onMouseDown={onMouseDown}>
        <div className="knob-inner" style={{ transform: `rotate(${rotation}deg)` }}>
          <div className="knob-tick" />
        </div>
      </div>
    </div>
  );
}

// ============ Visualizer ============
const VOICE_COLORS = ['#e94560', '#50fa7b', '#bd93f9', '#f1fa8c', '#ff79c6', '#8be9fd'];

function freqToSemi(freq: number): number {
  return Math.round(12 * Math.log2(freq / 440));
}

function semiToName(semi: number): string {
  const names = ['A', 'A#', 'B', 'C', 'C#', 'D', 'D#', 'E', 'F', 'F#', 'G', 'G#'];
  const idx = ((semi % 12) + 12) % 12;
  const octave = Math.floor((semi + 9) / 12) + 4;
  return names[idx] + octave;
}

function Visualizer({ notes, currentTime, isPlaying, voiceCount, keyboardW }: {
  notes: SequenceNote[]; currentTime: number; isPlaying: boolean; voiceCount: number; keyboardW: number;
}) {
  const bodyRef = useRef<HTMLDivElement>(null);
  const PPS = 100;
  const totalDur = notes.reduce((m, n) => Math.max(m, n.time + n.duration), 0);
  const trackH = totalDur * PPS + 40;
  const viewH = bodyRef.current?.clientHeight ?? 200;
  const offset = currentTime > 0 ? currentTime * PPS - viewH * 0.35 : 0;

  const sw = keyboardW / 49;
  const semiX = (semi: number) => (semi + 33.5) * sw;
  const cLines = [2, 3, 4, 5, 6].map(oct => semiX(freqToSemi(noteToFreq(`C${oct}`)!)));

  return (
    <div className="visualizer">
      <div className="viz-body" ref={bodyRef}>
        <div style={{ width: keyboardW, margin: '0 auto', position: 'relative' as const, height: '100%' }}>
          <div className="viz-grid">
            {cLines.map((x, i) => (
              <div key={i} className="viz-grid-line" style={{ left: x }} />
            ))}
          </div>
          <div className="viz-track" style={{ height: trackH, transform: `translateY(${-offset}px)` }}>
            {notes.map((n, i) => {
              const v = n.voice ?? 0;
              const cx = semiX(freqToSemi(n.freq));
              const active = isPlaying && currentTime >= n.time && currentTime < n.time + n.duration;
              return (
                <div key={i} className={`viz-note${active ? ' active' : ''}`}
                  style={{
                    left: cx - sw / 2,
                    width: sw,
                    top: n.time * PPS,
                    height: Math.max(3, n.duration * PPS - 2),
                    background: VOICE_COLORS[v % VOICE_COLORS.length],
                  }}
                />
              );
            })}
          </div>
          {isPlaying && <div className="viz-playhead" style={{ top: '35%' }} />}
        </div>
      </div>
    </div>
  );
}

// ============ Main App ============
function App() {
  const engine = useRef(new SynthEngine());
  const synthRef = useRef<HTMLDivElement>(null);
  const [state, setState] = useState<SynthState>(DEFAULT_STATE);
  const [keyW, setKeyW] = useState(18);
  const keyboardW = keyW * 29;
  const blackW = Math.round(keyW * 2 / 3);
  const [lastComp, setLastComp] = useState<Composition | null>(() => {
    try { return tb.insight<Composition>(); } catch (e) { return null; }
  });
  const [activeKeys, setActiveKeys] = useState<Map<string, number>>(new Map());
  const [isGenerating, setIsGenerating] = useState(false);
  const [ready, setReady] = useState(false);
  const [playTime, setPlayTime] = useState(0);
  const [playing, setPlaying] = useState(false);
  const rafId = useRef(0);
  const playStart = useRef(0);

  const vizData = useMemo(() => {
    if (!lastComp) return { notes: [] as SequenceNote[], voiceCount: 1, totalDur: 0 };
    const notes = parseScore(lastComp.score);
    const vc = notes.reduce((m, n) => Math.max(m, (n.voice ?? 0) + 1), 1);
    const dur = notes.reduce((m, n) => Math.max(m, n.time + n.duration), 0);
    return { notes, voiceCount: vc, totalDur: dur };
  }, [lastComp]);

  const init = useCallback(async () => {
    if (engine.current.init(state)) {
      setReady(true);
    } else {
      const ctx = engine.current.ctx;
      if (ctx && ctx.state === 'suspended') await ctx.resume();
    }
  }, [state]);

  useEffect(() => {
    if (ready) engine.current.updateParams(state);
  }, [state, ready]);

  const playComp = useCallback((comp: Composition, startOffset = 0) => {
    cancelAnimationFrame(rafId.current);
    engine.current.playComposition(comp, (freq, on, voice) => {
      const keyEntry = KEYBOARD.find(k => Math.abs(k[1] - freq) < 1);
      if (keyEntry) {
        setActiveKeys(prev => {
          const next = new Map(prev);
          on ? next.set(keyEntry[0], voice) : next.delete(keyEntry[0]);
          return next;
        });
      }
    }, startOffset);
    playStart.current = performance.now() - startOffset * 1000;
    setPlaying(true);
    const tick = () => {
      const t = (performance.now() - playStart.current) / 1000;
      if (t > vizData.totalDur + 0.5) { setPlaying(false); setPlayTime(0); setActiveKeys(new Map()); return; }
      setPlayTime(t);
      rafId.current = requestAnimationFrame(tick);
    };
    rafId.current = requestAnimationFrame(tick);
  }, [vizData.totalDur]);

  const pausePlaying = useCallback(() => {
    engine.current.stopSequence();
    cancelAnimationFrame(rafId.current);
    setPlaying(false);
    setActiveKeys(new Map());
  }, []);

  const rewind = useCallback(() => {
    engine.current.stopSequence();
    cancelAnimationFrame(rafId.current);
    setPlaying(false);
    setPlayTime(0);
    setActiveKeys(new Map());
  }, []);

  const noteOn = useCallback((key: string, freq: number) => {
    init();
    engine.current.noteOn(key, freq, state);
    setActiveKeys(s => new Map(s).set(key, 0));
  }, [init, state]);

  const noteOff = useCallback((key: string) => {
    engine.current.noteOff(key, state.env.release);
    setActiveKeys(s => { const n = new Map(s); n.delete(key); return n; });
  }, [state.env.release]);

  const handleCompose = async () => {
    await init();
    setIsGenerating(true);
    try {
      const result = await tb.infer<Composition>().catch(() => undefined);
      if (result) {
        setState(result.patch);
        setLastComp(result);
        engine.current.updateParams(result.patch);
        playComp(result);
      }
    } finally {
      setIsGenerating(false);
    }
  };

  useEffect(() => {
    const keyMap = new Map(KEYBOARD.map(([k, f]) => [k, f]));
    const onDown = (e: KeyboardEvent) => {
      const k = e.key.toLowerCase();
      if (!e.repeat && keyMap.has(k)) noteOn(k, keyMap.get(k)!);
    };
    const onUp = (e: KeyboardEvent) => {
      const k = e.key.toLowerCase();
      if (keyMap.has(k)) noteOff(k);
    };
    window.addEventListener('keydown', onDown);
    window.addEventListener('keyup', onUp);
    return () => { window.removeEventListener('keydown', onDown); window.removeEventListener('keyup', onUp); };
  }, [noteOn, noteOff]);

  useEffect(() => {
    const el = synthRef.current;
    if (!el) return;
    const ro = new ResizeObserver(entries => {
      const w = entries[0].contentBoxSize[0].inlineSize;
      setKeyW(Math.max(19, Math.floor((w - 10) / 29)));
    });
    ro.observe(el);
    return () => ro.disconnect();
  }, []);

  return (
    <div className="synth" ref={synthRef}>
      <div className="topbar">
        <button className={`ai-btn ${isGenerating ? 'loading' : ''}`} onClick={handleCompose}>
          {isGenerating ? 'Generating...' : '✨ AI Compose'}
        </button>

        <button
          className={`play-btn ${playing ? 'playing' : ''}`}
          onClick={async () => {
            if (playing) {
              pausePlaying();
            } else {
              await init();
              if (lastComp) playComp(lastComp, playTime >= vizData.totalDur ? 0 : playTime);
            }
          }}
          disabled={!lastComp}
        >
          {playing ? '⏸' : '▶'}
        </button>

        <button className="ctrl-btn" onClick={rewind} disabled={!lastComp}>⏮</button>

        <Knob value={state.master} min={0} max={1} onChange={v => setState(s => ({ ...s, master: v }))} label="VOL" format={fmt.pct} />
      </div>

      <div className="scroll-area">
        <div className="scroll-inner" style={{ minWidth: keyboardW + 10 }}>
        {vizData.notes.length > 0 && (
          <Visualizer notes={vizData.notes} currentTime={playTime} isPlaying={playing} voiceCount={vizData.voiceCount} keyboardW={keyboardW} />
        )}

        <div className="keyboard">
          {KEYBOARD.map(([key, freq, note]) => {
            const voice = activeKeys.get(key);
            const isBlack = note.includes('#');
            const keyStyle: React.CSSProperties = isBlack
              ? { width: blackW, margin: `0 -${blackW / 2}px` }
              : { width: keyW };
            if (voice !== undefined) keyStyle.background = VOICE_COLORS[voice % VOICE_COLORS.length];
            return (
              <div key={key} className={`key ${isBlack ? 'b' : 'w'} ${voice !== undefined ? 'on' : ''}`}
                style={keyStyle}
                onMouseDown={() => noteOn(key, freq)} onMouseUp={() => noteOff(key)} onMouseLeave={() => noteOff(key)}
                onTouchStart={e => { e.preventDefault(); noteOn(key, freq); }} onTouchEnd={() => noteOff(key)}>
                <span className="n">{note}</span>
                <span className="h">{key.length === 1 ? key.toUpperCase() : ''}</span>
              </div>
            );
          })}
        </div>
        </div>
      </div>
    </div>
  );
}

createRoot(document.getElementById("root")!).render(<App />);
```
**styles.css**

```css
:root { --bg: #151515; --panel: #1c1c1c; --border: #2a2a2a; --text: #ccc; --dim: #666; --accent: #e94560; }
html[data-theme="light"] { --bg: #e4e4e4; --panel: #f0f0f0; --border: #ccc; --text: #222; --dim: #888; --accent: #d63850; }

* { box-sizing: border-box; margin: 0; padding: 0; }
html, body { height: 100%; }
body { font: 10px/1.2 'SF Mono', Consolas, monospace; background: var(--bg); color: var(--text); padding: 8px; }
.synth { display: flex; flex-direction: column; height: calc(100vh - 16px); max-width: 960px; margin: 0 auto; }

/* Topbar */
.topbar { display: flex; align-items: center; justify-content: center; gap: 12px; padding: 10px; background: var(--panel); border: 1px solid var(--border); border-radius: 4px; margin-bottom: 6px; }

.ai-btn { background: #333; border: 1px solid var(--accent); color: var(--accent); padding: 8px 16px; border-radius: 4px; font: inherit; font-size: 12px; cursor: pointer; transition: all 0.2s; }
.ai-btn:hover { background: var(--accent); color: #fff; }
.ai-btn.loading { opacity: 0.5; cursor: wait; }

.ctrl-btn { background: #444; border: 1px solid #666; color: #fff; padding: 8px 12px; border-radius: 4px; font: inherit; font-size: 14px; cursor: pointer; }
.ctrl-btn:hover { background: #555; }
.ctrl-btn:disabled { opacity: 0.3; cursor: default; }
.play-btn { background: #444; border: 1px solid #666; color: #fff; padding: 8px 12px; border-radius: 4px; font: inherit; font-size: 14px; cursor: pointer; }
.play-btn:hover { background: #555; }
.play-btn:disabled { opacity: 0.3; cursor: default; }
.play-btn.playing { background: var(--accent); border-color: var(--accent); }

.knob-wrap { display: flex; flex-direction: column; align-items: center; gap: 2px; }
.knob { width: 36px; height: 36px; background: #222; border: 2px solid #333; border-radius: 50%; cursor: pointer; }
html[data-theme="light"] .knob { background: #ddd; border-color: #bbb; }
.knob:hover { border-color: var(--accent); }
.knob-inner { width: 100%; height: 100%; display: flex; align-items: center; justify-content: center; }
.knob-tick { width: 2px; height: 10px; background: var(--accent); border-radius: 1px; transform: translateY(-5px); }
.knob-info { text-align: center; line-height: 1; }
.knob-val { font-size: 10px; font-weight: 600; display: block; }
.knob-lbl { font-size: 8px; color: var(--dim); }

/* Scroll area */
.scroll-area { flex: 1; min-height: 0; overflow-x: auto; }
.scroll-inner { display: flex; flex-direction: column; height: 100%; }

/* Visualizer */
.visualizer { flex: 1; min-height: 120px; background: var(--panel); border: 1px solid var(--border); border-radius: 4px; margin-bottom: 6px; position: relative; overflow: hidden; }
.viz-body { position: relative; overflow: hidden; height: 100%; }
.viz-grid { position: absolute; inset: 0; pointer-events: none; z-index: 0; }
.viz-grid-line { position: absolute; top: 0; bottom: 0; width: 1px; background: var(--border); opacity: 0.5; }
.viz-track { position: relative; z-index: 1; }
.viz-note { position: absolute; border-radius: 2px; opacity: 0.5; transition: opacity 0.15s; }
.viz-note.active { opacity: 1; box-shadow: 0 0 10px 2px currentColor; filter: brightness(1.3); }
.viz-playhead { position: absolute; left: 0; right: 0; height: 2px; background: var(--dim); box-shadow: 0 0 6px var(--dim); z-index: 5; pointer-events: none; }

/* Keyboard */
.keyboard { display: flex; justify-content: center; background: var(--panel); border: 1px solid var(--border); border-radius: 4px; padding: 8px 4px; }
.key { display: flex; flex-direction: column; align-items: center; justify-content: flex-end; padding-bottom: 4px; border-radius: 0 0 3px 3px; cursor: pointer; user-select: none; flex-shrink: 0; }
.key.w { height: 64px; background: #f5f5f5; border: 1px solid #aaa; z-index: 1; color: #333; }
.key.b { height: 40px; background: #1a1a1a; border: 1px solid #000; z-index: 2; color: #777; }
.key.w:hover { background: #e8e8e8; }
.key.b:hover { background: #2a2a2a; }
.key.on { color: #fff !important; transform: translateY(1px); }
.key .n { font-size: 8px; font-weight: 600; }
.key .h { font-size: 7px; opacity: .5; }
```
**index.html**

```html
<div id="root"></div>

```
**data.txt**

```txt
Modal Jazz

```
**infer.md**

```md
Analyze the musical guidance in the input data (style, mood, or specific instructions).
Create a musical composition and matching synthesizer patch.

1. **Patch Design**: Adjust Osc1, Osc2, Filter, Envelope, and FX to match the requested style.
2. **Score**: Compose a polyphonic piece using the vertical tracker format below.

### Register Guidelines

The keyboard spans C2–C6. Keep most melodic material in the **C3–C5** range where this synth sounds best. Bass voices can dip to C2 but should sit mostly around **C2–C3**. Avoid writing melodies or lead voices below C3 — low registers sound muddy on a synth and lack definition.

### Vertical Tracker Format

A grid where **each row is one time step** and **each column is a voice**. Time flows top-to-bottom.

#### Header

- `@bpm 120` — tempo in beats per minute
- `@step 1/8` — time resolution (fraction of a whole note: 1/4, 1/8, 1/16)
- `@swing 0.67` — swing ratio (0.5 = straight, 0.67 = triplet swing). Use for jazz, blues, shuffle feels.
- `@voices melody bass` — voice names (informational, one per column)


#### Grid Symbols

- **Note name** (`C4`, `F#3`, `Bb5`) — attack: start a new note
- `|` — sustain: hold the previous note for another step
- `.` — rest: silence (ends any held note)
- `[C4,E4,G4]` — chord: multiple simultaneous notes in one voice

#### Example
```
**insight.json**

```json
{
  "patch": {
    "osc1": { "waveform": "triangle", "detune": 0, "gain": 0.5 },
    "osc2": { "waveform": "sine", "detune": 12, "gain": 0.2 },
    "filter": { "type": "lowpass", "frequency": 1200, "Q": 1.5 },
    "env": { "attack": 0.05, "decay": 0.6, "sustain": 0.4, "release": 0.5 },
    "fx": { "delayTime": 0.3, "delayFeedback": 0.2, "reverbMix": 0.4 },
    "master": 0.5
  },
  "score": [
    "@bpm 112",
    "@step 1/8",
    "@swing 0.6",
    "@voices sax piano bass",
    "",
    "A4 [F3,B3,E4] D2",
    "| | |",
    ". . D3",
    "C5 . |",
    "| [F3,B3,E4] F2",
    "B4 | |",
    "| . G2",
    "G4 . |",
    "--------",
    "A4 [F3,B3,E4] A2",
    "| | |",
    ". . C2",
    "F4 . |",
    "G4 [F3,B3,E4] D2",
    "| | |",
    "A4 . E2",
    "| . |",
    "--------",
    "D5 [F3,B3,E4] F2",
    "| | |",
    ". . G2",
    "C5 . |",
    "B4 [F3,B3,E4] A2",
    "| | |",
    "A4 . G2",
    "G4 . |",
    "--------",
    "A4 [F3,B3,E4] D2",
    "| | |",
    ". . D3",
    ". . |",
    ". [F3,B3,E4] C3",
    ". | |",
    ". . B2",
    ". . |",
    "--------",
    "Bb4 [Gb3,C4,F4] Eb2",
    "| | |",
    ". . Eb3",
    "Db5 . |",
    "| [Gb3,C4,F4] Gb2",
    "C5 | |",
    "| . Ab2",
    "Ab4 . |",
    "--------",
    "Bb4 [Gb3,C4,F4] Bb2",
    "| | |",
    ". . Db2",
    "Gb4 . |",
    "Ab4 [Gb3,C4,F4] Eb2",
    "| | |",
    "Bb4 . F2",
    "| . |",
    "--------",
    "Eb5 [Gb3,C4,F4] Gb2",
    "| | |",
    ". . Ab2",
    "Db5 . |",
    "C5 [Gb3,C4,F4] Bb2",
    "| | |",
    "Bb4 . Ab2",
    "Ab4 . |",
    "--------",
    "A4 [F3,B3,E4] D2",
    "| | |",
    "| | |",
    ". . |",
    "[A4,C5,E5] [F3,B3,E4] D2",
    "| | |",
    "| | |",
    ". . ."
  ]
}

```
**config.json**

```json
{
  "dependencies": {
    "react": "^19.2.0",
    "react-dom": "^19.2.0"
  },
  "description": "An AI composer that creates original music in any style, to help you find inspiration.",
  "inference": {
    "title": "AI Composer",
    "dataTitle": "Musical Style/Instructions",
    "submitTitle": "Compose Music"
  }
}
```

Markdown source · More bulbs by samples · Typebulb home