{
	"$schema": "https://ui.shadcn.com/schema/registry-item.json",
	"name": "SoundEngine-TS",
	"title": "SoundEngine",
	"description": "The tiny Web Audio engine that renders and plays React Bits sounds, with a useSound hook.",
	"type": "registry:component",
	"files": [
		{
			"type": "registry:component",
			"path": "react-bits-sound.ts",
			"target": "@components/sounds/react-bits-sound.ts",
			"content": "'use client';\n\nimport { useCallback, useEffect, useRef } from 'react';\n\nexport type Wave = 'sine' | 'triangle' | 'square' | 'sawtooth';\n\nexport type Ramp =\n  | number\n  | { from: number; to: number; time?: number; delay?: number; curve?: number }\n  | { points: Array<[number, number]> };\n\nexport interface Envelope {\n  attack?: number;\n  rise?: number;\n  hold?: number;\n  decay?: number;\n  sustain?: number;\n  release?: number;\n  curve?: number;\n}\n\nexport interface Filter {\n  type: BiquadFilterType;\n  freq: Ramp;\n  q?: Ramp;\n  gain?: number;\n}\n\ninterface LayerBase {\n  at?: number;\n  gain?: number;\n  env?: Envelope;\n  pan?: Ramp;\n  filter?: Filter | Filter[];\n  punch?: boolean;\n  tremolo?: { rate: number; depth: number };\n}\n\nexport interface ToneLayer extends LayerBase {\n  type: 'tone';\n  freq: Ramp;\n  wave?: Wave;\n  harmonics?: number[];\n  partials?: Array<[number, number, number?]>;\n  voices?: number;\n  detune?: number;\n  fm?: { ratio: number; index: Ramp };\n  vibrato?: { rate: number; depth: number };\n}\n\nexport interface NoiseLayer extends LayerBase {\n  type: 'noise';\n  color?: 'white' | 'pink' | 'brown' | 'crackle';\n}\n\nexport interface SampleSet {\n  rate: number;\n  data: string[];\n}\n\nexport interface SampleLayer extends LayerBase {\n  type: 'sample';\n  rate?: number;\n  data?: string[];\n  pitch?: number;\n  offset?: number;\n}\n\nexport type Layer = ToneLayer | NoiseLayer | SampleLayer;\n\nexport interface Recipe {\n  duration: number;\n  layers: Layer[];\n  samples?: SampleSet;\n  seed?: number;\n  gain?: number;\n  loudness?: number;\n  highpass?: number;\n  lowpass?: number;\n  drive?: number;\n  width?: number;\n  reverb?: { mix: number; decay: number; predelay?: number; damp?: number; lowcut?: number };\n  echo?: { time: number; feedback: number; mix: number; damp?: number };\n  reverse?: boolean;\n  stutter?: { slice: number; chance: number };\n  crush?: number;\n}\n\nexport interface ReverbEffect {\n  mix?: number;\n  decay?: number;\n  tone?: number;\n}\n\nexport interface EchoEffect {\n  time?: number;\n  feedback?: number;\n  mix?: number;\n  tone?: number;\n  pingPong?: boolean;\n}\n\nexport interface FilterEffect {\n  type?: 'lowpass' | 'highpass' | 'bandpass';\n  cutoff?: number;\n  resonance?: number;\n  sweep?: number;\n}\n\nexport interface ChorusEffect {\n  mix?: number;\n  rate?: number;\n  depth?: number;\n}\n\nexport interface LofiEffect {\n  bits?: number;\n  rate?: number;\n  mix?: number;\n}\n\nexport interface Tune {\n  pitch?: number;\n  tone?: number;\n  length?: number;\n  space?: number;\n  punch?: number;\n  volume?: number;\n  variation?: number;\n  reverb?: ReverbEffect;\n  echo?: EchoEffect;\n  filter?: FilterEffect;\n  chorus?: ChorusEffect;\n  lofi?: LofiEffect;\n}\n\nexport interface RenderOptions {\n  sampleRate?: number;\n  channels?: number;\n  variant?: number;\n}\n\nconst TAU = Math.PI * 2;\nconst clamp = (value: number, min: number, max: number) => Math.min(max, Math.max(min, value));\nconst db = (value: number) => Math.pow(10, value / 20);\n\nconst random = (seed: number) => {\n  let state = seed >>> 0 || 1;\n  return () => {\n    state = (state + 0x6d2b79f5) >>> 0;\n    let t = state;\n    t = Math.imul(t ^ (t >>> 15), t | 1);\n    t ^= t + Math.imul(t ^ (t >>> 7), t | 61);\n    return ((t ^ (t >>> 14)) >>> 0) / 4294967296;\n  };\n};\n\nconst filterType = (type?: string): BiquadFilterType => (type === 'highpass' || type === 'bandpass' ? type : 'lowpass');\n\nconst normalizeTune = (tune: Tune = {}) => ({\n  pitch: clamp(tune.pitch ?? 0, -24, 24),\n  tone: clamp(tune.tone ?? 0, -1, 1),\n  length: clamp(tune.length ?? 1, 0.25, 4),\n  space: clamp(tune.space ?? 0, -1, 1),\n  punch: clamp(tune.punch ?? 0, -1, 1),\n  volume: clamp(tune.volume ?? 0, -48, 12),\n  variation: clamp(tune.variation ?? 0, 0, 1),\n  reverb: tune.reverb\n    ? {\n        mix: clamp(tune.reverb.mix ?? 0.25, 0, 1),\n        decay: clamp(tune.reverb.decay ?? 1.4, 0.1, 12),\n        tone: clamp(tune.reverb.tone ?? 0, -1, 1)\n      }\n    : null,\n  echo: tune.echo\n    ? {\n        time: clamp(tune.echo.time ?? 0.24, 0.01, 2),\n        feedback: clamp(tune.echo.feedback ?? 0.35, 0, 0.95),\n        mix: clamp(tune.echo.mix ?? 0.25, 0, 1),\n        tone: clamp(tune.echo.tone ?? 0, -1, 1),\n        pingPong: Boolean(tune.echo.pingPong)\n      }\n    : null,\n  filter: tune.filter\n    ? {\n        type: filterType(tune.filter.type),\n        cutoff: clamp(tune.filter.cutoff ?? 1000, 20, 20000),\n        resonance: clamp(tune.filter.resonance ?? 0.4, 0, 1),\n        sweep: clamp(tune.filter.sweep ?? 0, -1, 1)\n      }\n    : null,\n  chorus: tune.chorus\n    ? {\n        mix: clamp(tune.chorus.mix ?? 0.5, 0, 1),\n        rate: clamp(tune.chorus.rate ?? 0.9, 0.05, 8),\n        depth: clamp(tune.chorus.depth ?? 0.5, 0, 1)\n      }\n    : null,\n  lofi: tune.lofi\n    ? {\n        bits: clamp(Math.round(tune.lofi.bits ?? 5), 2, 16),\n        rate: clamp(tune.lofi.rate ?? 6000, 500, 48000),\n        mix: clamp(tune.lofi.mix ?? 0.7, 0, 1)\n      }\n    : null\n});\n\ntype Settings = ReturnType<typeof normalizeTune>;\n\ntype Shape = (time: number) => number;\n\nconst rampShape = (ramp: Ramp, scaleTime: number, log: boolean): Shape => {\n  if (typeof ramp === 'number') return () => ramp;\n  if ('points' in ramp) {\n    const points = ramp.points.map(([time, value]) => [time * scaleTime, value] as [number, number]);\n    return time => {\n      if (time <= points[0][0]) return points[0][1];\n      for (let i = 1; i < points.length; i += 1) {\n        const [t1, v1] = points[i];\n        if (time <= t1) {\n          const [t0, v0] = points[i - 1];\n          const y = t1 > t0 ? (time - t0) / (t1 - t0) : 1;\n          const s = y * y * (3 - 2 * y);\n          return log && v0 > 0 && v1 > 0 ? v0 * Math.pow(v1 / v0, s) : v0 + (v1 - v0) * s;\n        }\n      }\n      return points[points.length - 1][1];\n    };\n  }\n  const delay = (ramp.delay ?? 0) * scaleTime;\n  const span = Math.max(1e-4, (ramp.time ?? 0.1) * scaleTime);\n  const curve = ramp.curve ?? 2;\n  return time => {\n    const y = clamp((time - delay) / span, 0, 1);\n    const e = curve >= 1 ? 1 - Math.pow(1 - y, curve) : Math.pow(y, 1 / curve);\n    return log && ramp.from > 0 && ramp.to > 0\n      ? ramp.from * Math.pow(ramp.to / ramp.from, e)\n      : ramp.from + (ramp.to - ramp.from) * e;\n  };\n};\n\nconst envelopeLength = (env: Envelope, stretch: number) =>\n  ((env.attack ?? 0.002) + (env.hold ?? 0) + (env.decay ?? 0.2) + ((env.sustain ?? 0) > 0 ? (env.release ?? 0.1) : 0)) *\n  stretch;\n\nconst envelopeShape = (env: Envelope, stretch: number, decayScale = 1): Shape => {\n  const attack = Math.max(0.0004, (env.attack ?? 0.002) * stretch);\n  const hold = (env.hold ?? 0) * stretch;\n  const decay = Math.max(0.002, (env.decay ?? 0.2) * stretch * decayScale);\n  const sustain = clamp(env.sustain ?? 0, 0, 1);\n  const release = Math.max(0.002, (env.release ?? 0.1) * stretch);\n  const k = env.curve ?? 5;\n  const rise = env.rise;\n  const fall = (y: number) => (Math.exp(-k * y) - Math.exp(-k)) / (1 - Math.exp(-k));\n  return time => {\n    if (time <= 0) return 0;\n    if (time < attack) {\n      const y = time / attack;\n      return rise ? Math.pow(y, rise) : Math.sin((y * Math.PI) / 2);\n    }\n    let t = time - attack;\n    if (t < hold) return 1;\n    t -= hold;\n    if (t < decay) return sustain + (1 - sustain) * fall(t / decay);\n    if (sustain <= 0) return 0;\n    t -= decay;\n    if (t < release) return sustain * fall(t / release);\n    return 0;\n  };\n};\n\nconst applyCurve = (\n  param: AudioParam,\n  shape: Shape,\n  start: number,\n  length: number,\n  rate: number,\n  map = (v: number) => v\n) => {\n  const steps = clamp(Math.ceil(length * rate), 2, 4096);\n  const values = new Float32Array(steps);\n  for (let i = 0; i < steps; i += 1) values[i] = map(shape((i / (steps - 1)) * length));\n  param.value = values[0];\n  param.setValueCurveAtTime(values, start, length);\n};\n\nconst noiseBuffer = (context: BaseAudioContext, color: NoiseLayer['color'], length: number, seed: number) => {\n  const frames = Math.max(1, Math.ceil(length * context.sampleRate));\n  const buffer = context.createBuffer(1, frames, context.sampleRate);\n  const data = buffer.getChannelData(0);\n  const next = random(seed);\n  let b0 = 0;\n  let b1 = 0;\n  let b2 = 0;\n  let b3 = 0;\n  let b4 = 0;\n  let b5 = 0;\n  let b6 = 0;\n  let brown = 0;\n  const density = 45 / context.sampleRate;\n  for (let i = 0; i < frames; i += 1) {\n    const white = next() * 2 - 1;\n    if (color === 'crackle') {\n      data[i] = next() < density ? white * (0.4 + next() * 0.6) : 0;\n    } else if (color === 'pink') {\n      b0 = 0.99886 * b0 + white * 0.0555179;\n      b1 = 0.99332 * b1 + white * 0.0750759;\n      b2 = 0.969 * b2 + white * 0.153852;\n      b3 = 0.8665 * b3 + white * 0.3104856;\n      b4 = 0.55 * b4 + white * 0.5329522;\n      b5 = -0.7616 * b5 - white * 0.016898;\n      data[i] = (b0 + b1 + b2 + b3 + b4 + b5 + b6 + white * 0.5362) * 0.11;\n      b6 = white * 0.115926;\n    } else if (color === 'brown') {\n      brown = (brown + 0.02 * white) / 1.02;\n      data[i] = brown * 3.5;\n    } else {\n      data[i] = white;\n    }\n  }\n  return buffer;\n};\n\nconst impulse = (context: BaseAudioContext, decay: number, predelay: number, damp: number, seed: number) => {\n  const rate = context.sampleRate;\n  const length = Math.ceil((decay * 1.15 + predelay) * rate);\n  const buffer = context.createBuffer(2, length, rate);\n  const taps = [0.0071, 0.0113, 0.0167, 0.0229, 0.0311, 0.0373];\n  for (let channel = 0; channel < 2; channel += 1) {\n    const data = buffer.getChannelData(channel);\n    const next = random(seed * 31 + channel * 977 + 17);\n    let low = 0;\n    let energy = 0;\n    for (let i = 0; i < length; i += 1) {\n      const time = i / rate - predelay;\n      if (time < 0) continue;\n      const cutoff = damp * Math.pow(0.18, time / decay) + 300;\n      const a = 1 - Math.exp((-TAU * cutoff) / rate);\n      low += a * (next() * 2 - 1 - low);\n      const swell = Math.min(1, time / 0.004);\n      const value = low * Math.exp((-6.9 * time) / decay) * swell;\n      data[i] = value;\n      energy += value * value;\n    }\n    taps.forEach((tap, index) => {\n      const at = Math.round((predelay + tap * (channel ? 1.13 : 1)) * rate);\n      if (at < length) data[at] += (0.5 - index * 0.06) * Math.sqrt(energy / length) * 3;\n    });\n    let total = 0;\n    for (let i = 0; i < length; i += 1) total += data[i] * data[i];\n    const scale = 1 / Math.sqrt(Math.max(total, 1e-12));\n    for (let i = 0; i < length; i += 1) data[i] *= scale;\n  }\n  return buffer;\n};\n\nconst shaper = (amount: number) => {\n  const curve = new Float32Array(2048);\n  const drive = 1 + amount * 6;\n  const norm = Math.tanh(drive);\n  for (let i = 0; i < curve.length; i += 1) {\n    const x = (i / (curve.length - 1)) * 2 - 1;\n    curve[i] = Math.tanh(x * drive) / norm;\n  }\n  return curve;\n};\n\nconst filters = (layer: Layer) => (layer.filter ? (Array.isArray(layer.filter) ? layer.filter : [layer.filter]) : []);\n\nconst decodePcm = (data: string) => {\n  const bytes = atob(data);\n  const pcm = new Float32Array(bytes.length >> 1);\n  for (let i = 0; i < pcm.length; i += 1) {\n    const value = bytes.charCodeAt(i * 2) | (bytes.charCodeAt(i * 2 + 1) << 8);\n    pcm[i] = (value >= 32768 ? value - 65536 : value) / 32768;\n  }\n  return pcm;\n};\n\nconst variantsOf = (recipe: Recipe) =>\n  recipe.layers.reduce(\n    (count, layer) =>\n      layer.type === 'sample' ? Math.max(count, (layer.data ?? recipe.samples?.data ?? []).length) : count,\n    1\n  );\n\nconst buildLayer = (\n  context: OfflineAudioContext,\n  layer: Layer,\n  index: number,\n  destination: AudioNode,\n  recipe: Recipe,\n  tune: ReturnType<typeof normalizeTune>,\n  end: number,\n  variant: number\n) => {\n  const rate = context.sampleRate;\n  const stretch = tune.length;\n  const ratio = Math.pow(2, tune.pitch / 12);\n  const start = (layer.at ?? 0) * stretch;\n  const env = layer.env ?? { attack: 0.002, decay: 0.2 };\n  const sample = layer.type === 'sample' ? layer : null;\n  const bank = sample?.data ? { rate: sample.rate ?? 48000, data: sample.data } : sample ? recipe.samples : undefined;\n  const pcm =\n    sample && bank?.data.length ? decodePcm(bank.data[(variant + (sample.offset ?? 0)) % bank.data.length]) : null;\n  const speed = sample ? ratio * Math.pow(2, (sample.pitch ?? 0) / 12) : ratio;\n  const natural = pcm && bank ? pcm.length / bank.rate / speed : 0;\n  const span = Math.min((pcm && !layer.env ? natural : envelopeLength(env, stretch)) + 0.01, end - start);\n  if (span <= 0) return;\n  const nyquist = rate / 2 - 100;\n  let gain = db(layer.gain ?? 0);\n  if (layer.punch) gain *= db(tune.punch * 6);\n  const output = context.createGain();\n  output.gain.value = gain;\n  let tail: AudioNode = output;\n  if (layer.tremolo) {\n    const depth = clamp(layer.tremolo.depth, 0, 1);\n    const wobble = context.createGain();\n    wobble.gain.value = 1 - depth / 2;\n    const lfo = context.createOscillator();\n    lfo.frequency.value = layer.tremolo.rate;\n    const swing = context.createGain();\n    swing.gain.value = depth / 2;\n    lfo.connect(swing);\n    swing.connect(wobble.gain);\n    lfo.start(start);\n    lfo.stop(start + span);\n    output.connect(wobble);\n    tail = wobble;\n  }\n  filters(layer).forEach(spec => {\n    const node = context.createBiquadFilter();\n    node.type = spec.type;\n    if (spec.gain !== undefined) node.gain.value = spec.gain;\n    const freqShape = rampShape(spec.freq, stretch, true);\n    applyCurve(node.frequency, freqShape, start, span, 400, value => clamp(value * ratio, 10, nyquist));\n    if (spec.q !== undefined) applyCurve(node.Q, rampShape(spec.q, stretch, false), start, span, 200);\n    tail.connect(node);\n    tail = node;\n  });\n  if (layer.pan !== undefined) {\n    const panner = context.createStereoPanner();\n    applyCurve(panner.pan, rampShape(layer.pan, stretch, false), start, span, 200, value => clamp(value, -1, 1));\n    tail.connect(panner);\n    tail = panner;\n  }\n  tail.connect(destination);\n  const seed = (recipe.seed ?? 1) * 7919 + index * 104729;\n  if (layer.type === 'sample') {\n    if (!pcm || !bank) return;\n    const buffer = context.createBuffer(1, pcm.length, bank.rate);\n    buffer.copyToChannel(pcm, 0);\n    const source = context.createBufferSource();\n    source.buffer = buffer;\n    source.playbackRate.value = speed;\n    if (layer.env) {\n      const amp = context.createGain();\n      applyCurve(amp.gain, envelopeShape(env, stretch), start, span, 600);\n      source.connect(amp);\n      amp.connect(output);\n    } else {\n      source.connect(output);\n    }\n    source.start(start);\n    return;\n  }\n  if (layer.type === 'noise') {\n    const source = context.createBufferSource();\n    source.buffer = noiseBuffer(context, layer.color ?? 'white', span + 0.05, seed);\n    const amp = context.createGain();\n    applyCurve(amp.gain, envelopeShape(env, stretch), start, span, 600);\n    source.connect(amp);\n    amp.connect(output);\n    source.start(start);\n    source.stop(start + span);\n    return;\n  }\n  const freqShape = rampShape(layer.freq, stretch, true);\n  const partials: Array<[number, number, number]> = layer.partials\n    ? layer.partials.map(([partialRatio, partialGain, decay]) => [partialRatio, partialGain, decay ?? 1])\n    : [[1, 1, 1]];\n  const voices = Math.max(1, Math.round(layer.voices ?? 1));\n  const spread = layer.detune ?? 0;\n  partials.forEach(([partialRatio, partialGain, decayScale], partialIndex) => {\n    const amp = context.createGain();\n    applyCurve(amp.gain, envelopeShape(env, stretch, decayScale), start, span, 600, value => value * partialGain);\n    amp.connect(output);\n    for (let voice = 0; voice < voices; voice += 1) {\n      const osc = context.createOscillator();\n      if (layer.harmonics && !layer.partials) {\n        const real = new Float32Array(layer.harmonics.length + 1);\n        const imag = new Float32Array(layer.harmonics.length + 1);\n        layer.harmonics.forEach((value, harmonic) => {\n          imag[harmonic + 1] = value;\n        });\n        osc.setPeriodicWave(context.createPeriodicWave(real, imag));\n      } else {\n        osc.type = layer.wave ?? 'sine';\n      }\n      const offset = voices > 1 ? (voice / (voices - 1) - 0.5) * 2 * spread : 0;\n      applyCurve(osc.frequency, freqShape, start, span, 400, value => clamp(value * ratio * partialRatio, 1, nyquist));\n      osc.detune.value = offset;\n      if (layer.vibrato) {\n        const lfo = context.createOscillator();\n        const depth = context.createGain();\n        lfo.frequency.value = layer.vibrato.rate;\n        depth.gain.value = layer.vibrato.depth;\n        lfo.connect(depth);\n        depth.connect(osc.detune);\n        lfo.start(start);\n        lfo.stop(start + span);\n      }\n      if (layer.fm && partialIndex === 0) {\n        const fm = layer.fm;\n        const mod = context.createOscillator();\n        const depth = context.createGain();\n        applyCurve(mod.frequency, freqShape, start, span, 400, value => clamp(value * ratio * fm.ratio, 1, nyquist));\n        const indexShape = rampShape(fm.index, stretch, false);\n        applyCurve(depth.gain, time => indexShape(time) * freqShape(time) * ratio * fm.ratio, start, span, 400);\n        mod.connect(depth);\n        depth.connect(osc.frequency);\n        mod.start(start);\n        mod.stop(start + span);\n      }\n      let target: AudioNode = amp;\n      if (voices > 1) {\n        const voiceGain = context.createGain();\n        voiceGain.gain.value = 1 / Math.sqrt(voices);\n        const pan = context.createStereoPanner();\n        pan.pan.value = clamp((voice / (voices - 1) - 0.5) * 2 * (recipe.width ?? 0.6), -1, 1);\n        voiceGain.connect(pan);\n        pan.connect(amp);\n        target = voiceGain;\n      }\n      osc.connect(target);\n      osc.start(start);\n      osc.stop(start + span);\n    }\n  });\n};\n\nconst loudness = (channels: Float32Array[], rate: number) => {\n  const window = Math.max(1, Math.round(rate * 0.05));\n  let best = 0;\n  const length = channels[0].length;\n  for (let start = 0; start < length; start += Math.round(window / 2)) {\n    let sum = 0;\n    const stop = Math.min(length, start + window);\n    for (let i = start; i < stop; i += 1) {\n      for (const data of channels) sum += data[i] * data[i];\n    }\n    best = Math.max(best, sum / ((stop - start) * channels.length));\n  }\n  return Math.sqrt(best);\n};\n\nconst finish = (buffer: AudioBuffer, recipe: Recipe, tune: ReturnType<typeof normalizeTune>) => {\n  const rate = buffer.sampleRate;\n  const channels = Array.from({ length: buffer.numberOfChannels }, (_, index) => buffer.getChannelData(index));\n  if (recipe.stutter) {\n    const next = random((recipe.seed ?? 1) * 131 + 7);\n    const slice = Math.max(32, Math.round(recipe.stutter.slice * tune.length * rate));\n    const copies = channels.map(data => data.slice());\n    for (let start = 0; start < copies[0].length; start += slice) {\n      const roll = next();\n      const repeat = roll < recipe.stutter.chance;\n      const gate = roll > 1 - recipe.stutter.chance * 0.35;\n      const from = repeat ? Math.max(0, start - slice) : start;\n      channels.forEach((data, channel) => {\n        for (let i = 0; i < slice && start + i < data.length; i += 1) {\n          const edge = Math.min(1, i / 48, (slice - i) / 48);\n          data[start + i] = gate ? 0 : copies[channel][from + i] * edge;\n        }\n      });\n    }\n  }\n  if (recipe.crush) {\n    const levels = Math.pow(2, Math.round(16 - recipe.crush * 12));\n    const hold = Math.max(1, Math.round(1 + recipe.crush * 10));\n    channels.forEach(data => {\n      let held = 0;\n      for (let i = 0; i < data.length; i += 1) {\n        if (i % hold === 0) held = Math.round(data[i] * levels) / levels;\n        data[i] = data[i] * (1 - recipe.crush!) + held * recipe.crush!;\n      }\n    });\n  }\n  if (recipe.reverse) channels.forEach(data => data.reverse());\n  const floor = db(-70);\n  let first = channels[0].length;\n  channels.forEach(data => {\n    for (let i = 0; i < first; i += 1) {\n      if (Math.abs(data[i]) > floor) {\n        first = i;\n        break;\n      }\n    }\n  });\n  const lead = Math.max(0, first - Math.round(rate * 0.002));\n  if (lead > 0) {\n    channels.forEach(data => {\n      data.copyWithin(0, lead);\n      data.fill(0, data.length - lead);\n    });\n  }\n  let last = 0;\n  channels.forEach(data => {\n    for (let i = data.length - 1; i > last; i -= 1) {\n      if (Math.abs(data[i]) > floor) {\n        last = i;\n        break;\n      }\n    }\n  });\n  const length = Math.min(buffer.length, last + Math.round(rate * 0.02));\n  const fadeOut = Math.min(Math.round(rate * 0.02), length);\n  const fadeIn = Math.round(rate * 0.0006);\n  let mean = 0;\n  channels.forEach(data => {\n    for (let i = 0; i < length; i += 1) mean += data[i];\n  });\n  mean /= length * channels.length;\n  const level = loudness(channels, rate);\n  const target = db(recipe.loudness ?? -16);\n  let gain = level > 1e-6 ? target / level : 1;\n  let peak = 0;\n  channels.forEach(data => {\n    for (let i = 0; i < length; i += 1) peak = Math.max(peak, Math.abs(data[i] - mean));\n  });\n  if (peak * gain > db(-1)) gain = db(-1) / peak;\n  gain *= db(tune.volume) * db(recipe.gain ?? 0);\n  channels.forEach(data => {\n    for (let i = 0; i < data.length; i += 1) {\n      if (i >= length) {\n        data[i] = 0;\n        continue;\n      }\n      let value = (data[i] - mean) * gain;\n      if (i < fadeIn) value *= Math.sin(((i / fadeIn) * Math.PI) / 2);\n      const remaining = length - i;\n      if (remaining < fadeOut) value *= 0.5 - 0.5 * Math.cos((remaining / fadeOut) * Math.PI);\n      data[i] = Math.abs(value) > 0.999 ? Math.sign(value) * 0.999 : value;\n    }\n  });\n  return { buffer, length };\n};\n\nconst trimmed = (source: AudioBuffer, length: number) => {\n  if (length >= source.length) return source;\n  const buffer = new AudioBuffer({ length, numberOfChannels: source.numberOfChannels, sampleRate: source.sampleRate });\n  for (let channel = 0; channel < source.numberOfChannels; channel += 1) {\n    buffer.copyToChannel(source.getChannelData(channel).subarray(0, length), channel);\n  }\n  return buffer;\n};\n\nconst channelsOf = (buffer: AudioBuffer) =>\n  Array.from({ length: buffer.numberOfChannels }, (_, index) => buffer.getChannelData(index));\n\nconst peakOf = (channels: Float32Array[]) => {\n  let peak = 0;\n  channels.forEach(data => {\n    for (let i = 0; i < data.length; i += 1) peak = Math.max(peak, Math.abs(data[i]));\n  });\n  return peak;\n};\n\nconst balance = (mix: number) => [Math.cos((mix * Math.PI) / 2), Math.sin((mix * Math.PI) / 2)];\n\nconst predelayOf = (decay: number) => clamp(0.004 + decay * 0.006, 0.004, 0.03);\n\nconst addEcho = (context: OfflineAudioContext, input: AudioNode, echo: NonNullable<Settings['echo']>) => {\n  const [dryLevel, wetLevel] = balance(echo.mix);\n  const output = context.createGain();\n  const dry = context.createGain();\n  dry.gain.value = dryLevel;\n  input.connect(dry);\n  dry.connect(output);\n  const wet = context.createGain();\n  wet.gain.value = wetLevel;\n  wet.connect(output);\n  const send = context.createGain();\n  if (echo.pingPong) {\n    send.channelCount = 1;\n    send.channelCountMode = 'explicit';\n  }\n  input.connect(send);\n  const nyquist = context.sampleRate / 2 - 200;\n  const taps = (echo.pingPong ? [-0.7, 0.7] : [0]).map(side => {\n    const delay = context.createDelay(2.5);\n    delay.delayTime.value = echo.time;\n    const damp = context.createBiquadFilter();\n    damp.type = 'lowpass';\n    damp.frequency.value = Math.min(nyquist, 4200 * Math.pow(2, echo.tone * 1.6));\n    damp.Q.value = 0.5;\n    const thin = context.createBiquadFilter();\n    thin.type = 'highpass';\n    thin.frequency.value = 140;\n    thin.Q.value = 0.5;\n    const pan = context.createStereoPanner();\n    pan.pan.value = side;\n    delay.connect(damp);\n    damp.connect(thin);\n    thin.connect(pan);\n    pan.connect(wet);\n    return { delay, thin };\n  });\n  taps.forEach((tap, index) => {\n    const feedback = context.createGain();\n    feedback.gain.value = echo.feedback;\n    tap.thin.connect(feedback);\n    feedback.connect(taps[(index + 1) % taps.length].delay);\n  });\n  send.connect(taps[0].delay);\n  return output;\n};\n\nconst addReverb = (\n  context: OfflineAudioContext,\n  input: AudioNode,\n  reverb: NonNullable<Settings['reverb']>,\n  seed: number\n) => {\n  const [dryLevel, wetLevel] = balance(reverb.mix);\n  const output = context.createGain();\n  const dry = context.createGain();\n  dry.gain.value = dryLevel;\n  input.connect(dry);\n  dry.connect(output);\n  const thin = context.createBiquadFilter();\n  thin.type = 'highpass';\n  thin.frequency.value = 160;\n  thin.Q.value = 0.5;\n  const convolver = context.createConvolver();\n  convolver.normalize = false;\n  const damp = Math.min(context.sampleRate / 2 - 500, 6500 * Math.pow(2, reverb.tone * 1.4));\n  convolver.buffer = impulse(context, reverb.decay, predelayOf(reverb.decay), damp, seed + 101);\n  const wet = context.createGain();\n  wet.gain.value = wetLevel;\n  input.connect(thin);\n  thin.connect(convolver);\n  convolver.connect(wet);\n  wet.connect(output);\n  return output;\n};\n\nconst addFilter = (\n  context: OfflineAudioContext,\n  input: AudioNode,\n  filter: NonNullable<Settings['filter']>,\n  span: number\n) => {\n  const node = context.createBiquadFilter();\n  const nyquist = context.sampleRate / 2 - 200;\n  const from = clamp(filter.cutoff, 20, nyquist);\n  node.type = filter.type;\n  node.Q.value = 0.5 * Math.pow(16, filter.resonance);\n  node.frequency.setValueAtTime(from, 0);\n  if (filter.sweep !== 0) {\n    const to = clamp(filter.cutoff * Math.pow(2, filter.sweep * 4), 20, nyquist);\n    node.frequency.exponentialRampToValueAtTime(to, Math.max(0.05, span));\n  }\n  input.connect(node);\n  return node;\n};\n\nconst sine = (context: BaseAudioContext, phase: number) =>\n  context.createPeriodicWave(new Float32Array([0, Math.sin(phase)]), new Float32Array([0, Math.cos(phase)]));\n\nconst addChorus = (context: OfflineAudioContext, input: AudioNode, chorus: NonNullable<Settings['chorus']>) => {\n  const [dryLevel, wetLevel] = balance(chorus.mix);\n  const output = context.createGain();\n  const dry = context.createGain();\n  dry.gain.value = dryLevel;\n  input.connect(dry);\n  dry.connect(output);\n  const wet = context.createGain();\n  wet.gain.value = wetLevel;\n  wet.connect(output);\n  [-0.75, 0.75].forEach((side, index) => {\n    const delay = context.createDelay(0.1);\n    delay.delayTime.value = 0.014 + index * 0.005;\n    const lfo = context.createOscillator();\n    lfo.setPeriodicWave(sine(context, (index * Math.PI) / 2));\n    lfo.frequency.value = chorus.rate * (1 + index * 0.13);\n    const depth = context.createGain();\n    depth.gain.value = 0.0003 + chorus.depth * 0.0027;\n    lfo.connect(depth);\n    depth.connect(delay.delayTime);\n    lfo.start();\n    const pan = context.createStereoPanner();\n    pan.pan.value = side;\n    input.connect(delay);\n    delay.connect(pan);\n    pan.connect(wet);\n  });\n  return output;\n};\n\nconst crunch = (source: AudioBuffer, lofi: NonNullable<Settings['lofi']>) => {\n  const rate = source.sampleRate;\n  const hold = Math.max(1, rate / lofi.rate);\n  const steps = Math.pow(2, lofi.bits - 1);\n  const squash = Math.log(256);\n  const smooth = 1 - Math.exp((-TAU * Math.min(rate * 0.45, lofi.rate * 0.45)) / rate);\n  const [dryLevel, wetLevel] = balance(lofi.mix);\n  const buffer = new AudioBuffer({\n    length: source.length,\n    numberOfChannels: source.numberOfChannels,\n    sampleRate: rate\n  });\n  for (let channel = 0; channel < source.numberOfChannels; channel += 1) {\n    const input = source.getChannelData(channel);\n    const output = buffer.getChannelData(channel);\n    let held = 0;\n    let phase = hold;\n    let low = 0;\n    for (let i = 0; i < input.length; i += 1) {\n      phase += 1;\n      if (phase >= hold) {\n        phase -= hold;\n        const level = Math.round((Math.log(1 + 255 * Math.abs(input[i])) / squash) * steps) / steps;\n        held = (Math.sign(input[i]) * (Math.pow(256, level) - 1)) / 255;\n      }\n      low += smooth * (held - low);\n      output[i] = input[i] * dryLevel + low * wetLevel;\n    }\n  }\n  return buffer;\n};\n\nconst settle = (buffer: AudioBuffer, reference: AudioBuffer) => {\n  const rate = buffer.sampleRate;\n  const channels = channelsOf(buffer);\n  const source = channelsOf(reference);\n  const level = loudness(channels, rate);\n  let gain = level > 1e-9 ? loudness(source, rate) / level : 1;\n  const ceiling = Math.max(peakOf(source), db(-1));\n  const peak = peakOf(channels);\n  if (peak * gain > ceiling) gain = ceiling / peak;\n  const floor = db(-70);\n  let last = 0;\n  channels.forEach(data => {\n    for (let i = data.length - 1; i > last; i -= 1) {\n      if (Math.abs(data[i] * gain) > floor) {\n        last = i;\n        break;\n      }\n    }\n  });\n  const length = Math.min(buffer.length, last + Math.round(rate * 0.02));\n  const fade = Math.min(Math.round(rate * 0.02), length);\n  channels.forEach(data => {\n    for (let i = 0; i < length; i += 1) {\n      let value = data[i] * gain;\n      const remaining = length - i;\n      if (remaining < fade) value *= 0.5 - 0.5 * Math.cos((remaining / fade) * Math.PI);\n      data[i] = Math.abs(value) > 0.999 ? Math.sign(value) * 0.999 : value;\n    }\n  });\n  return trimmed(buffer, length);\n};\n\nconst applyEffects = async (dry: AudioBuffer, settings: Settings, seed: number) => {\n  const echo = settings.echo && settings.echo.mix > 0 ? settings.echo : null;\n  const reverb = settings.reverb && settings.reverb.mix > 0 ? settings.reverb : null;\n  const chorus = settings.chorus && settings.chorus.mix > 0 ? settings.chorus : null;\n  const lofi = settings.lofi && settings.lofi.mix > 0 ? settings.lofi : null;\n  const filter = settings.filter;\n  if (!echo && !reverb && !chorus && !lofi && !filter) return dry;\n  const rate = dry.sampleRate;\n  const echoTail = echo ? echo.time * (1 + Math.log(0.001) / Math.log(Math.max(0.05, echo.feedback))) : 0;\n  const reverbTail = reverb ? reverb.decay * 1.15 + predelayOf(reverb.decay) : 0;\n  const total = dry.duration + Math.min(12, echoTail) + reverbTail + (chorus ? 0.03 : 0) + 0.05;\n  const context = new OfflineAudioContext(dry.numberOfChannels, Math.ceil(total * rate), rate);\n  const source = context.createBufferSource();\n  source.buffer = lofi ? crunch(dry, lofi) : dry;\n  let chain: AudioNode = source;\n  if (filter) chain = addFilter(context, chain, filter, dry.duration);\n  if (chorus) chain = addChorus(context, chain, chorus);\n  if (echo) chain = addEcho(context, chain, echo);\n  if (reverb) chain = addReverb(context, chain, reverb, seed);\n  chain.connect(context.destination);\n  source.start();\n  return settle(await context.startRendering(), dry);\n};\n\nexport async function renderSound(recipe: Recipe, tune: Tune = {}, options: RenderOptions = {}) {\n  const t = normalizeTune(tune);\n  const rate = options.sampleRate ?? 48000;\n  const channelCount = options.channels ?? 2;\n  const reverb = recipe.reverb;\n  const spaceMix = reverb ? clamp(reverb.mix * (t.space < 0 ? 1 + t.space : 1 + t.space * 1.6), 0, 0.9) : 0;\n  const spaceDecay = reverb ? reverb.decay * (1 + Math.max(0, t.space) * 0.8) : 0;\n  const echoTail = recipe.echo\n    ? (recipe.echo.time * Math.log(0.001)) / Math.log(Math.max(0.05, recipe.echo.feedback))\n    : 0;\n  const body = recipe.duration * t.length;\n  const end = body + (spaceMix > 0 ? spaceDecay * 1.1 + (reverb?.predelay ?? 0) : 0) + echoTail + 0.05;\n  const context = new OfflineAudioContext(channelCount, Math.ceil(end * rate), rate);\n  const bus = context.createGain();\n  const variant = Math.max(0, Math.floor(options.variant ?? 0));\n  recipe.layers.forEach((layer, index) => buildLayer(context, layer, index, bus, recipe, t, body, variant));\n  const highpass = context.createBiquadFilter();\n  highpass.type = 'highpass';\n  highpass.frequency.value = recipe.highpass ?? 24;\n  highpass.Q.value = 0.6;\n  const lowpass = context.createBiquadFilter();\n  lowpass.type = 'lowpass';\n  lowpass.frequency.value = clamp((recipe.lowpass ?? 16000) * (1 + t.tone * 0.35), 400, rate / 2 - 200);\n  lowpass.Q.value = 0.5;\n  const low = context.createBiquadFilter();\n  low.type = 'lowshelf';\n  low.frequency.value = 280;\n  low.gain.value = -t.tone * 4;\n  const high = context.createBiquadFilter();\n  high.type = 'highshelf';\n  high.frequency.value = 3600;\n  high.gain.value = t.tone * 7;\n  bus.connect(highpass);\n  highpass.connect(low);\n  low.connect(high);\n  high.connect(lowpass);\n  let chain: AudioNode = lowpass;\n  const drive = clamp((recipe.drive ?? 0) + Math.max(0, t.punch) * 0.25, 0, 1);\n  if (drive > 0) {\n    const headroom = context.createGain();\n    headroom.gain.value = 0.5;\n    const shape = context.createWaveShaper();\n    shape.curve = shaper(drive);\n    shape.oversample = '4x';\n    chain.connect(headroom);\n    headroom.connect(shape);\n    chain = shape;\n  }\n  const mix = context.createGain();\n  chain.connect(mix);\n  if (reverb && spaceMix > 0) {\n    const convolver = context.createConvolver();\n    convolver.normalize = false;\n    convolver.buffer = impulse(context, spaceDecay, reverb.predelay ?? 0.01, reverb.damp ?? 6500, recipe.seed ?? 1);\n    const wet = context.createGain();\n    wet.gain.value = spaceMix * 2.2;\n    if (reverb.lowcut) {\n      const lowcut = context.createBiquadFilter();\n      lowcut.type = 'highpass';\n      lowcut.frequency.value = reverb.lowcut;\n      lowcut.Q.value = 0.5;\n      chain.connect(lowcut);\n      lowcut.connect(convolver);\n    } else {\n      chain.connect(convolver);\n    }\n    convolver.connect(wet);\n    wet.connect(mix);\n  }\n  if (recipe.echo) {\n    const delay = context.createDelay(4);\n    delay.delayTime.value = recipe.echo.time * t.length;\n    const feedback = context.createGain();\n    feedback.gain.value = clamp(recipe.echo.feedback, 0, 0.92);\n    const damp = context.createBiquadFilter();\n    damp.type = 'lowpass';\n    damp.frequency.value = recipe.echo.damp ?? 4200;\n    const wet = context.createGain();\n    wet.gain.value = recipe.echo.mix;\n    chain.connect(delay);\n    delay.connect(damp);\n    damp.connect(feedback);\n    feedback.connect(delay);\n    damp.connect(wet);\n    wet.connect(mix);\n  }\n  mix.connect(context.destination);\n  const rendered = await context.startRendering();\n  const { buffer, length } = finish(rendered, recipe, t);\n  return applyEffects(trimmed(buffer, length), t, recipe.seed ?? 1);\n}\n\nlet sharedContext: AudioContext | null = null;\nlet masterVolume = 1;\nlet muted = false;\nconst cache = new Map<string, Promise<AudioBuffer>>();\nconst ids = new WeakMap<Recipe, number>();\nconst lastVariant = new WeakMap<Recipe, number>();\nlet nextId = 1;\n\nconst recipeId = (recipe: Recipe) => {\n  let id = ids.get(recipe);\n  if (!id) {\n    id = nextId;\n    nextId += 1;\n    ids.set(recipe, id);\n  }\n  return id;\n};\n\nconst audio = () => {\n  if (!sharedContext) {\n    const Context =\n      window.AudioContext || (window as unknown as { webkitAudioContext: typeof AudioContext }).webkitAudioContext;\n    sharedContext = new Context({ latencyHint: 'interactive' });\n  }\n  return sharedContext;\n};\n\nconst pickVariant = (recipe: Recipe) => {\n  const count = variantsOf(recipe);\n  if (count < 2) return 0;\n  const last = lastVariant.get(recipe) ?? -1;\n  let next = Math.floor(Math.random() * (last < 0 ? count : count - 1));\n  if (last >= 0 && next >= last) next += 1;\n  lastVariant.set(recipe, next);\n  return next;\n};\n\nconst bufferFor = (recipe: Recipe, tune: Tune, rate: number, variant = 0) => {\n  const shape: Tune = {\n    pitch: tune.pitch,\n    tone: tune.tone,\n    length: tune.length,\n    space: tune.space,\n    punch: tune.punch,\n    reverb: tune.reverb,\n    echo: tune.echo,\n    filter: tune.filter,\n    chorus: tune.chorus,\n    lofi: tune.lofi\n  };\n  const key = `${recipeId(recipe)}:${rate}:${variant}:${JSON.stringify(shape)}`;\n  let pending = cache.get(key);\n  if (!pending) {\n    pending = renderSound(recipe, shape, { sampleRate: rate, variant });\n    cache.set(key, pending);\n    if (cache.size > 96) cache.delete(cache.keys().next().value as string);\n  }\n  return pending;\n};\n\nexport function setSoundVolume(value: number) {\n  masterVolume = clamp(value, 0, 1);\n}\n\nexport function setSoundMuted(value: boolean) {\n  muted = value;\n}\n\nexport function preloadSound(recipe: Recipe, tune: Tune = {}) {\n  if (typeof window === 'undefined') return;\n  for (let variant = 0; variant < variantsOf(recipe); variant += 1) {\n    bufferFor(recipe, tune, audio().sampleRate, variant).catch(() => undefined);\n  }\n}\n\nexport async function playSound(recipe: Recipe, tune: Tune = {}) {\n  if (typeof window === 'undefined' || muted || masterVolume <= 0) return;\n  const context = audio();\n  if (context.state === 'suspended') await context.resume();\n  const buffer = await bufferFor(recipe, tune, context.sampleRate, pickVariant(recipe));\n  const source = context.createBufferSource();\n  source.buffer = buffer;\n  const variation = clamp(tune.variation ?? 0, 0, 1);\n  if (variation > 0) source.detune.value = (Math.random() * 2 - 1) * variation * 80;\n  const gain = context.createGain();\n  gain.gain.value = masterVolume * db(clamp(tune.volume ?? 0, -48, 12));\n  source.connect(gain);\n  gain.connect(context.destination);\n  source.start();\n  await new Promise<void>(resolve => {\n    source.onended = () => resolve();\n  });\n}\n\nexport function useSound(recipe: Recipe, tune: Tune = {}) {\n  const latest = useRef({ recipe, tune });\n  latest.current = { recipe, tune };\n  const key = JSON.stringify(tune);\n  useEffect(() => {\n    preloadSound(latest.current.recipe, latest.current.tune);\n  }, [recipe, key]);\n  return useCallback(() => {\n    playSound(latest.current.recipe, latest.current.tune).catch(() => undefined);\n  }, []);\n}\n"
		}
	],
	"registryDependencies": [],
	"dependencies": []
}