{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "core",
  "title": "Audio core",
  "description": "Decibel maths, meter ballistics, zones, fader tapers, band resampling, bar level smoothing, wave line shapes, time formatting, the shared frame loop, frame emitters, shared types and the audio theme tokens. Every audiocn component depends on it.",
  "files": [
    {
      "path": "lib/audio/types.ts",
      "content": "/** A level reading for one audio channel, in dBFS. */\nexport interface ChannelLevel {\n  /** Sample peak in dBFS. `-Infinity` is digital silence. */\n  peakDb: number;\n  /** RMS level in dBFS, when the source measures it. */\n  rmsDb?: number;\n}\n\n/** One metering update: one entry per channel (1 = mono, 2 = stereo). */\nexport interface MeterFrame {\n  channels: ChannelLevel[];\n}\n\n/** One visualisation update from an analyser. */\nexport interface VisualFrame {\n  /** Frequency bands, 0..1 each, low to high. */\n  bands: Float32Array;\n  /** Ring buffer of recent broadband levels, 0..1 each. */\n  history: Float32Array;\n  /** Index of the oldest entry in `history`. */\n  historyStart: number;\n  /** Number of valid entries in `history`. */\n  historyLength: number;\n  /** Time of the newest history entry, in the requestAnimationFrame clock. */\n  historyUpdatedAt?: number;\n  /** Minimum time between entries, used for playback delay. 0 disables smoothing. */\n  historyIntervalMs?: number;\n  /** Last entry removed from a full history ring, for its scrolling edge. */\n  historyPreviousLevel?: number;\n  /** Raw time-domain samples, −1..1, for line and scope drawing. */\n  timeDomain?: Float32Array;\n  /** Sample peak of this frame in dBFS. */\n  peakDb: number;\n}\n\n/** Anything that can push frames to subscribers. */\nexport interface FrameSource<T> {\n  subscribe: (listener: (frame: T) => void) => () => void;\n}\n\nexport type MeterZoneName = \"ok\" | \"warn\" | \"clip\";\n\n/** A colour zone of a meter, starting at `fromDb` and running to the next zone. */\nexport interface MeterZone {\n  fromDb: number;\n  zone: MeterZoneName;\n}\n\n/** Maps a value (usually dB) to a 0..1 position on a control, and back. */\nexport interface Taper {\n  toPosition: (value: number) => number;\n  toValue: (position: number) => number;\n}\n\nexport type Orientation = \"horizontal\" | \"vertical\";\n",
      "type": "registry:lib"
    },
    {
      "path": "lib/audio/decibels.ts",
      "content": "/** Default bottom of a meter's range, in dBFS. */\nexport const DEFAULT_MIN_DB = -60;\n/** Default top of a meter's range, in dBFS. */\nexport const DEFAULT_MAX_DB = 0;\n/** Digital silence. */\nexport const SILENCE_DB = Number.NEGATIVE_INFINITY;\n\nconst AMPLITUDE_DB_FACTOR = 20;\nconst MINUS_SIGN = \"−\";\nconst INFINITY_SIGN = \"∞\";\n\nexport const clamp = (value: number, min: number, max: number): number =>\n  Math.min(max, Math.max(min, value));\n\n/** Decibels to linear gain. `-Infinity` gives 0. */\nexport const dbToGain = (db: number): number => {\n  if (db === SILENCE_DB) {\n    return 0;\n  }\n  return 10 ** (db / AMPLITUDE_DB_FACTOR);\n};\n\n/** Linear gain to decibels. 0 (or less) gives `-Infinity`. */\nexport const gainToDb = (gain: number): number => {\n  if (gain <= 0) {\n    return SILENCE_DB;\n  }\n  return AMPLITUDE_DB_FACTOR * Math.log10(gain);\n};\n\n/** dBFS to a 0..1 level, linear in dB over `minDb..maxDb`, clamped. */\nexport const dbToLevel = (\n  db: number,\n  minDb: number = DEFAULT_MIN_DB,\n  maxDb: number = DEFAULT_MAX_DB\n): number => {\n  if (!(db > minDb)) {\n    return 0;\n  }\n  return clamp((db - minDb) / (maxDb - minDb), 0, 1);\n};\n\n/** A 0..1 level back to dBFS over `minDb..maxDb`. The inverse of `dbToLevel`. */\nexport const levelToDb = (\n  level: number,\n  minDb: number = DEFAULT_MIN_DB,\n  maxDb: number = DEFAULT_MAX_DB\n): number => minDb + clamp(level, 0, 1) * (maxDb - minDb);\n\n/**\n * Clamps a dB value to a range. With `allowSilence`, `-Infinity` passes\n * through instead of becoming `minDb`.\n */\nexport const clampDb = (\n  db: number,\n  minDb: number,\n  maxDb: number,\n  options: { allowSilence?: boolean } = {}\n): number => {\n  if (db === SILENCE_DB && options.allowSilence) {\n    return SILENCE_DB;\n  }\n  if (Number.isNaN(db)) {\n    return minDb;\n  }\n  return clamp(db, minDb, maxDb);\n};\n\nexport interface FormatDbOptions {\n  /** Digits after the decimal point. Default 1. */\n  decimals?: number;\n  /** Append \" dB\". Default true. */\n  unit?: boolean;\n  /**\n   * `auto` shows \"+\" for positive values and \"−\" for negative ones,\n   * `negative` only shows \"−\", `never` shows no sign. Default `auto`.\n   */\n  sign?: \"auto\" | \"negative\" | \"never\";\n  /** At or below this value, show \"−∞\". Default `-Infinity`. */\n  floorDb?: number;\n}\n\n/** Formats decibels for display: \"−12.3 dB\", \"+3.0 dB\", \"−∞ dB\". */\nexport const formatDb = (db: number, options: FormatDbOptions = {}): string => {\n  const {\n    decimals = 1,\n    unit = true,\n    sign = \"auto\",\n    floorDb = SILENCE_DB,\n  } = options;\n  const suffix = unit ? \" dB\" : \"\";\n\n  // No reading yet. Two hyphens rather than a dash: hosts such as Videorc ban\n  // the em dash from user-facing copy.\n  if (Number.isNaN(db)) {\n    return `--${suffix}`;\n  }\n  if (db === SILENCE_DB || db <= floorDb) {\n    const prefix = sign === \"never\" ? \"\" : MINUS_SIGN;\n    return `${prefix}${INFINITY_SIGN}${suffix}`;\n  }\n\n  const rounded = Number(db.toFixed(decimals));\n  const magnitude = Math.abs(rounded).toFixed(decimals);\n\n  if (rounded === 0 || sign === \"never\") {\n    return `${magnitude}${suffix}`;\n  }\n  if (rounded < 0) {\n    return `${MINUS_SIGN}${magnitude}${suffix}`;\n  }\n  const plus = sign === \"auto\" ? \"+\" : \"\";\n  return `${plus}${magnitude}${suffix}`;\n};\n\n/** Sample peak of a block of samples, in dBFS. */\nexport const peakDb = (samples: Iterable<number>): number => {\n  let peak = 0;\n  for (const sample of samples) {\n    const magnitude = Math.abs(sample);\n    if (magnitude > peak) {\n      peak = magnitude;\n    }\n  }\n  return gainToDb(peak);\n};\n\n/** RMS level of a block of samples, in dBFS. */\nexport const rmsDb = (samples: Iterable<number>): number => {\n  let sumOfSquares = 0;\n  let count = 0;\n  for (const sample of samples) {\n    sumOfSquares += sample * sample;\n    count += 1;\n  }\n  if (count === 0) {\n    return SILENCE_DB;\n  }\n  return gainToDb(Math.sqrt(sumOfSquares / count));\n};\n",
      "type": "registry:lib"
    },
    {
      "path": "lib/audio/ballistics.ts",
      "content": "import { dbToGain, gainToDb } from \"@/lib/audio/decibels\";\n\n/** How a meter moves between readings. All times are in milliseconds. */\nexport interface BallisticsOptions {\n  /** Time constant for rising levels. 0 jumps instantly. */\n  attackMs: number;\n  /** Time constant for falling levels. 0 drops instantly. */\n  releaseMs: number;\n  /** How long the peak-hold marker stays before it starts to fall. 0 disables it. */\n  peakHoldMs: number;\n  /** Time constant for the peak-hold marker's fall. */\n  peakReleaseMs: number;\n}\n\nexport const BALLISTICS = {\n  instant: { attackMs: 0, peakHoldMs: 0, peakReleaseMs: 0, releaseMs: 0 },\n  peak: { attackMs: 15, peakHoldMs: 1200, peakReleaseMs: 600, releaseMs: 350 },\n  vu: { attackMs: 300, peakHoldMs: 0, peakReleaseMs: 300, releaseMs: 300 },\n} as const satisfies Record<string, BallisticsOptions>;\n\nexport type BallisticsPreset = keyof typeof BALLISTICS;\n\nexport type BallisticsInput = BallisticsPreset | Partial<BallisticsOptions>;\n\nexport const resolveBallistics = (\n  input: BallisticsInput = \"peak\"\n): BallisticsOptions => {\n  if (typeof input === \"string\") {\n    return BALLISTICS[input];\n  }\n  return { ...BALLISTICS.peak, ...input };\n};\n\nexport interface BallisticsState {\n  /** The smoothed level in dBFS. */\n  db: number;\n  /** The peak-hold level in dBFS. */\n  holdDb: number;\n}\n\nexport interface Ballistics {\n  step: (inputDb: number, nowMs: number) => BallisticsState;\n  reset: () => void;\n}\n\nconst smoothingFactor = (elapsedMs: number, timeConstantMs: number): number => {\n  if (timeConstantMs <= 0) {\n    return 1;\n  }\n  return 1 - Math.exp(-elapsedMs / timeConstantMs);\n};\n\n/**\n * Creates a meter ballistics integrator. Smoothing runs on linear amplitude,\n * so a falling level drops at a steady rate in dB, like a hardware meter.\n */\nexport const createBallistics = (input?: BallisticsInput): Ballistics => {\n  const options = resolveBallistics(input);\n  let amplitude = 0;\n  let holdAmplitude = 0;\n  let holdUntilMs = 0;\n  let lastMs: number | null = null;\n\n  const reset = () => {\n    amplitude = 0;\n    holdAmplitude = 0;\n    holdUntilMs = 0;\n    lastMs = null;\n  };\n\n  const step = (inputDb: number, nowMs: number): BallisticsState => {\n    const target = dbToGain(inputDb);\n\n    if (lastMs === null) {\n      amplitude = target;\n    } else {\n      const elapsedMs = Math.max(0, nowMs - lastMs);\n      const timeConstant =\n        target > amplitude ? options.attackMs : options.releaseMs;\n      amplitude +=\n        (target - amplitude) * smoothingFactor(elapsedMs, timeConstant);\n    }\n\n    if (options.peakHoldMs <= 0) {\n      holdAmplitude = amplitude;\n    } else if (amplitude >= holdAmplitude) {\n      holdAmplitude = amplitude;\n      holdUntilMs = nowMs + options.peakHoldMs;\n    } else if (nowMs > holdUntilMs && lastMs !== null) {\n      const elapsedMs = Math.max(0, nowMs - Math.max(lastMs, holdUntilMs));\n      holdAmplitude -=\n        (holdAmplitude - amplitude) *\n        smoothingFactor(elapsedMs, options.peakReleaseMs);\n    }\n\n    lastMs = nowMs;\n    return { db: gainToDb(amplitude), holdDb: gainToDb(holdAmplitude) };\n  };\n\n  return { reset, step };\n};\n",
      "type": "registry:lib"
    },
    {
      "path": "lib/audio/zones.ts",
      "content": "import type { MeterZone, MeterZoneName } from \"@/lib/audio/types\";\n\n/** Green below −20 dBFS, amber from −20, red from −9: the thresholds streamers know. */\nexport const DEFAULT_ZONES: MeterZone[] = [\n  { fromDb: Number.NEGATIVE_INFINITY, zone: \"ok\" },\n  { fromDb: -20, zone: \"warn\" },\n  { fromDb: -9, zone: \"clip\" },\n];\n\n/** A level at or above this counts as clipping for a clip light. */\nexport const CLIP_THRESHOLD_DB = -1;\n/** How long a clip light stays on after the last clip. */\nexport const CLIP_HOLD_MS = 1500;\n\n/** The zone a level falls in. */\nexport const zoneForDb = (\n  db: number,\n  zones: MeterZone[] = DEFAULT_ZONES\n): MeterZoneName => {\n  let current: MeterZoneName = \"ok\";\n  for (const zone of zones) {\n    if (db >= zone.fromDb) {\n      current = zone.zone;\n    }\n  }\n  return current;\n};\n",
      "type": "registry:lib"
    },
    {
      "path": "lib/audio/taper.ts",
      "content": "import { clamp, DEFAULT_MIN_DB } from \"@/lib/audio/decibels\";\nimport type { Taper } from \"@/lib/audio/types\";\n\nconst DEFAULT_FADER_MAX_DB = 6;\nconst DEFAULT_UNITY_POSITION = 0.75;\nconst AUDIO_CURVE_EXPONENT = 2;\n\n/** Equal distance per unit across the range. */\nexport const linearTaper = (min: number, max: number): Taper => ({\n  toPosition: (value) => {\n    if (!(value > min)) {\n      return 0;\n    }\n    return clamp((value - min) / (max - min), 0, 1);\n  },\n  toValue: (position) => min + clamp(position, 0, 1) * (max - min),\n});\n\nexport interface AudioTaperOptions {\n  minDb?: number;\n  maxDb?: number;\n  /** Where 0 dB sits on the travel, 0..1. Default 0.75. */\n  unityPosition?: number;\n}\n\n/**\n * A console-style fader law: more travel around 0 dB, a fast fall to the\n * bottom of the range, and linear headroom above 0 dB.\n */\nexport const audioTaper = ({\n  minDb = DEFAULT_MIN_DB,\n  maxDb = DEFAULT_FADER_MAX_DB,\n  unityPosition = DEFAULT_UNITY_POSITION,\n}: AudioTaperOptions = {}): Taper => {\n  const anchorDb = Math.min(0, maxDb);\n  const unity = maxDb > 0 ? clamp(unityPosition, 0, 1) : 1;\n\n  const toPosition = (db: number): number => {\n    if (!(db > minDb)) {\n      return 0;\n    }\n    if (db >= maxDb) {\n      return 1;\n    }\n    if (db >= anchorDb) {\n      return unity + ((db - anchorDb) / (maxDb - anchorDb)) * (1 - unity);\n    }\n    const ratio = (db - anchorDb) / (minDb - anchorDb);\n    const travel = 1 - ratio ** (1 / AUDIO_CURVE_EXPONENT);\n    return travel * unity;\n  };\n\n  const toValue = (position: number): number => {\n    const clamped = clamp(position, 0, 1);\n    if (clamped >= unity) {\n      if (unity >= 1) {\n        return anchorDb;\n      }\n      return anchorDb + ((clamped - unity) / (1 - unity)) * (maxDb - anchorDb);\n    }\n    const travel = unity > 0 ? clamped / unity : 0;\n    return anchorDb + (minDb - anchorDb) * (1 - travel) ** AUDIO_CURVE_EXPONENT;\n  };\n\n  return { toPosition, toValue };\n};\n\n/** Logarithmic law for frequency and time parameters. `min` must be above 0. */\nexport const logTaper = (min: number, max: number): Taper => {\n  const span = Math.log(max / min);\n  return {\n    toPosition: (value) => {\n      if (!(value > min)) {\n        return 0;\n      }\n      return clamp(Math.log(value / min) / span, 0, 1);\n    },\n    toValue: (position) => min * Math.exp(clamp(position, 0, 1) * span),\n  };\n};\n\nexport type TaperInput = \"linear\" | \"audio\" | \"log\" | Taper;\n\n/** Resolves a taper name or object for a range. */\nexport const resolveTaper = (\n  taper: TaperInput,\n  min: number,\n  max: number\n): Taper => {\n  if (typeof taper !== \"string\") {\n    return taper;\n  }\n  if (taper === \"audio\") {\n    return audioTaper({ maxDb: max, minDb: min });\n  }\n  if (taper === \"log\") {\n    return logTaper(min, max);\n  }\n  return linearTaper(min, max);\n};\n",
      "type": "registry:lib"
    },
    {
      "path": "lib/audio/bands.ts",
      "content": "import { clamp } from \"@/lib/audio/decibels\";\n\n/** `AnalyserNode.minDecibels` default. */\nexport const SPECTRUM_MIN_DB = -100;\n/** `AnalyserNode.maxDecibels` default. */\nexport const SPECTRUM_MAX_DB = -30;\n\nconst POWER_DB_FACTOR = 10;\n\n/** `count + 1` band edges in Hz, spaced evenly on a log scale. */\nexport const logBandEdges = (\n  count: number,\n  minHz: number,\n  maxHz: number\n): Float32Array => {\n  const edges = new Float32Array(count + 1);\n  const ratio = maxHz / minHz;\n  for (let index = 0; index <= count; index += 1) {\n    edges[index] = minHz * ratio ** (index / count);\n  }\n  return edges;\n};\n\nexport interface BandsFromSpectrumOptions {\n  minDb?: number;\n  maxDb?: number;\n}\n\n/**\n * Reduces analyser frequency data (dB per bin) to 0..1 bands between the given\n * edges. Writes into `out` and returns it.\n */\nexport const bandsFromSpectrum = (\n  spectrumDb: Float32Array,\n  sampleRate: number,\n  edges: Float32Array,\n  out: Float32Array,\n  {\n    minDb = SPECTRUM_MIN_DB,\n    maxDb = SPECTRUM_MAX_DB,\n  }: BandsFromSpectrumOptions = {}\n): Float32Array => {\n  const binCount = spectrumDb.length;\n  const hzPerBin = sampleRate / 2 / binCount;\n  const bandCount = Math.min(out.length, edges.length - 1);\n\n  for (let band = 0; band < bandCount; band += 1) {\n    const low = Math.floor((edges[band] ?? 0) / hzPerBin);\n    const high = Math.max(\n      low + 1,\n      Math.ceil((edges[band + 1] ?? 0) / hzPerBin)\n    );\n    const first = clamp(low, 0, binCount - 1);\n    const last = clamp(high, first + 1, binCount);\n    let power = 0;\n    for (let bin = first; bin < last; bin += 1) {\n      power += 10 ** ((spectrumDb[bin] ?? minDb) / POWER_DB_FACTOR);\n    }\n    const meanDb = POWER_DB_FACTOR * Math.log10(power / (last - first));\n    out[band] = clamp((meanDb - minDb) / (maxDb - minDb), 0, 1);\n  }\n  return out;\n};\n\n/**\n * Fits `length` values from a ring buffer, starting at `start`, into\n * `out.length` values. Each output takes the loudest value in its span.\n */\nexport const resampleLevels = (\n  source: ArrayLike<number>,\n  start: number,\n  length: number,\n  out: Float32Array\n): Float32Array => {\n  const size = source.length;\n  const count = out.length;\n\n  if (size === 0 || length <= 0 || count === 0) {\n    out.fill(0);\n    return out;\n  }\n\n  for (let index = 0; index < count; index += 1) {\n    const from = Math.floor((index * length) / count);\n    const to = Math.max(from + 1, Math.floor(((index + 1) * length) / count));\n    let loudest = 0;\n    for (let offset = from; offset < to; offset += 1) {\n      const value = source[(start + offset) % size] ?? 0;\n      if (value > loudest) {\n        loudest = value;\n      }\n    }\n    out[index] = loudest;\n  }\n  return out;\n};\n",
      "type": "registry:lib"
    },
    {
      "path": "lib/audio/bar-levels.ts",
      "content": "import { resampleLevels } from \"@/lib/audio/bands\";\nimport { clamp } from \"@/lib/audio/decibels\";\n\nconst RELEASE_PER_FRAME = 0.86;\nconst FRAME_MS = 16.67;\nconst SIGNAL_THRESHOLD = 0.02;\nconst MS_PER_SECOND = 1000;\n/** Bars this close to their target have nothing left to animate. */\nconst SETTLE_EPSILON = 0.002;\n\n/** What bars do with no signal. */\nexport type BarIdle = \"static\" | \"pulse\" | \"wave\";\n\nexport interface BarLevelsOptions {\n  barCount: number;\n  idle: BarIdle;\n  /** Runs a sweep, for connecting or thinking states. */\n  loading: boolean;\n  /** Resting bar size, 0..1. */\n  minLevel: number;\n  /** Symmetric around the middle bar, lows in the centre. */\n  mirrored: boolean;\n  /** No release smoothing, idle animation or sweep. */\n  reducedMotion: boolean;\n}\n\nexport interface BarLevels {\n  /** The level each bar shows, 0..1, never below `minLevel`. */\n  levels: Float32Array;\n  /**\n   * Advances to `nowMs` with the latest input levels (resampled to the bar\n   * count). Returns true when the input is above the signal floor.\n   */\n  step: (nowMs: number, input: ArrayLike<number> | null) => boolean;\n  /**\n   * True after a step that left nothing to animate: every bar is at its\n   * target, and no idle animation or loading sweep is running.\n   */\n  readonly settled: boolean;\n}\n\nconst idleLevel = (idle: BarIdle, index: number, seconds: number) => {\n  if (idle === \"pulse\") {\n    return 0.1 * (0.5 + 0.5 * Math.sin(seconds * Math.PI * 1.6));\n  }\n  if (idle === \"wave\") {\n    return 0.16 * (0.5 + 0.5 * Math.sin(seconds * 4 - index * 0.55));\n  }\n  return 0;\n};\n\nconst sweepLevel = (index: number, count: number, seconds: number) => {\n  const span = count + 6;\n  const position = ((seconds * count * 0.9) % span) - 3;\n  return 0.55 * Math.exp(-((index - position) ** 2) / 3);\n};\n\nconst targetIndexFor = (\n  index: number,\n  count: number,\n  half: number,\n  mirrored: boolean\n) => {\n  if (!mirrored) {\n    return index;\n  }\n  return Math.min(half - 1, Math.floor(Math.abs(index - (count - 1) / 2)));\n};\n\nconst loudestOf = (values: Float32Array) => {\n  let loudest = 0;\n  for (const value of values) {\n    loudest = Math.max(loudest, value);\n  }\n  return loudest;\n};\n\n/**\n * Bar levels shared by the bar visualizers: input resampled to the bar count,\n * instant attack with a smooth release, mirroring, idle animations and the\n * loading sweep.\n */\nexport const createBarLevels = ({\n  barCount,\n  idle,\n  loading,\n  minLevel,\n  mirrored,\n  reducedMotion,\n}: BarLevelsOptions): BarLevels => {\n  const half = mirrored ? Math.ceil(barCount / 2) : barCount;\n  const targets = new Float32Array(half);\n  const current = new Float32Array(barCount);\n  const levels = new Float32Array(barCount).fill(minLevel);\n  let lastMs = 0;\n  let settled = false;\n\n  const extraLevel = (index: number, seconds: number, quiet: boolean) => {\n    if (loading && !reducedMotion) {\n      return sweepLevel(index, barCount, seconds);\n    }\n    return quiet ? idleLevel(idle, index, seconds) : 0;\n  };\n\n  const readInput = (input: ArrayLike<number> | null) => {\n    if (input && input.length > 0) {\n      resampleLevels(input, 0, input.length, targets);\n    } else {\n      targets.fill(0);\n    }\n  };\n\n  const step = (nowMs: number, input: ArrayLike<number> | null) => {\n    const elapsed = lastMs === 0 ? FRAME_MS : nowMs - lastMs;\n    lastMs = nowMs;\n    const release = reducedMotion\n      ? 0\n      : RELEASE_PER_FRAME ** (elapsed / FRAME_MS);\n    const seconds = nowMs / MS_PER_SECOND;\n    readInput(input);\n    const active = loudestOf(targets) >= SIGNAL_THRESHOLD;\n    const quiet = !(reducedMotion || active);\n    const animating =\n      (loading && !reducedMotion) || (quiet && idle !== \"static\");\n    let converged = true;\n\n    for (let index = 0; index < barCount; index += 1) {\n      const target = Math.max(\n        targets[targetIndexFor(index, barCount, half, mirrored)] ?? 0,\n        extraLevel(index, seconds, quiet)\n      );\n      const previous = current[index] ?? 0;\n      const next =\n        target >= previous\n          ? target\n          : previous * release + target * (1 - release);\n      current[index] = next;\n      levels[index] = clamp(Math.max(minLevel, next), 0, 1);\n      if (Math.abs(next - target) > SETTLE_EPSILON) {\n        converged = false;\n      }\n    }\n    settled = converged && !animating;\n    return active;\n  };\n\n  return {\n    levels,\n    get settled() {\n      return settled;\n    },\n    step,\n  };\n};\n",
      "type": "registry:lib"
    },
    {
      "path": "lib/audio/wave-line.ts",
      "content": "import { createBarLevels } from \"@/lib/audio/bar-levels\";\nimport { clamp } from \"@/lib/audio/decibels\";\nimport type { VisualFrame } from \"@/lib/audio/types\";\n\nconst MS_PER_SECOND = 1000;\nconst FRAME_MS = 16.67;\nconst MAX_STEP_SECONDS = 0.05;\nconst TWO_PI = Math.PI * 2;\n/** Below this the line counts as silent. */\nconst SIGNAL_THRESHOLD = 0.02;\n/** Share of the distance to the new shape still left after one frame. */\nconst EASE_PER_FRAME = 0.45;\n/** Loudness left after one frame of silence. */\nconst LOUDNESS_RELEASE = 0.92;\n\n/** Cycles across the width, drift in radians per second and weight of each band group, lows first. */\nconst WAVE_PARTIALS = [\n  { cycles: 1.5, drift: 0.8, weight: 0.6 },\n  { cycles: 2.5, drift: -1.3, weight: 0.4 },\n  { cycles: 4, drift: 2.1, weight: 0.25 },\n  { cycles: 6.5, drift: -3.2, weight: 0.16 },\n] as const;\n\n/** Scope mode lifts quiet signals by at most this much. */\nconst SCOPE_MAX_GAIN = 4;\n/** Scope mode scales its running peak to this height. */\nconst SCOPE_TARGET = 0.9;\n/** Share of the scope's running peak left after one frame. */\nconst SCOPE_PEAK_RELEASE = 0.985;\n\n/** Widths per second the loading pulse travels. */\nconst LOADING_SPEED = 0.55;\n/** Where the loading pulse rests with reduced motion: the middle. */\nconst LOADING_REST_SECONDS = 0.7 / LOADING_SPEED;\n\n/** Most points a line can have. */\nexport const WAVE_LINE_MAX_POINTS = 513;\n\n/**\n * `wave` is a smooth wave shaped by the frequency bands. `scope` is the\n * signal itself, like an oscilloscope.\n */\nexport type WaveLineMode = \"wave\" | \"scope\";\n\nexport interface WaveLineOptions {\n  mode: WaveLineMode;\n  /** Runs a pulse along the line instead of following the signal. */\n  loading: boolean;\n  /** Visual gain. */\n  sensitivity: number;\n  /** No drift or easing: the line jumps to each new shape. */\n  reducedMotion: boolean;\n}\n\nexport interface WaveLine {\n  /** Points in use, left to right. */\n  count: number;\n  /** Height at each point, −1..1, up positive, eased toward the latest shape. */\n  heights: Float32Array;\n  /** Highest point of the latest shape before easing, 0..1. It jumps on sudden rises. */\n  peak: number;\n  /** How loud the line is, 0..1: its highest point, released slowly. */\n  loudness: number;\n  /**\n   * Advances to `nowMs` with the latest frame, over `count` points. Returns\n   * true when there is a signal.\n   */\n  step: (nowMs: number, frame: VisualFrame | null, count: number) => boolean;\n}\n\n/**\n * Where the signal first rises through zero in its first quarter, between\n * samples, so a steady tone stands still. 0 when it never does.\n */\nexport const triggerIndex = (samples: ArrayLike<number>): number => {\n  const limit = Math.floor(samples.length / 4);\n  for (let index = 1; index <= limit; index += 1) {\n    const before = samples[index - 1] ?? 0;\n    const after = samples[index] ?? 0;\n    if (before < 0 && after >= 0) {\n      return index - 1 + -before / (after - before);\n    }\n  }\n  return 0;\n};\n\nconst peakOf = (values: ArrayLike<number>, count: number) => {\n  let peak = 0;\n  for (let index = 0; index < count; index += 1) {\n    peak = Math.max(peak, Math.abs(values[index] ?? 0));\n  }\n  return peak;\n};\n\n/** Fits three quarters of the samples, from the trigger, into `count` heights. */\nconst fillScope = (\n  samples: ArrayLike<number>,\n  out: Float32Array,\n  count: number,\n  gain: number\n) => {\n  const start = triggerIndex(samples);\n  const span = Math.floor(samples.length * 0.75) - 1;\n  for (let point = 0; point < count; point += 1) {\n    const position = start + (span * point) / (count - 1);\n    const index = Math.floor(position);\n    const before = samples[index] ?? 0;\n    const after = samples[Math.min(samples.length - 1, index + 1)] ?? before;\n    const value = before + (after - before) * (position - index);\n    out[point] = clamp(value * gain, -1, 1);\n  }\n};\n\n/** A short wave packet that travels left to right. */\nconst loadingPulse = (position: number, seconds: number) => {\n  const center = ((seconds * LOADING_SPEED) % 1.4) - 0.2;\n  const envelope = Math.exp(-((position - center) ** 2) / 0.012);\n  return 0.6 * envelope * Math.sin(TWO_PI * (position * 7 - seconds * 2.5));\n};\n\n/**\n * The shape of a single line across the width, shared by the line\n * visualizers: a drifting wave shaped by the frequency bands, a triggered\n * oscilloscope trace, or a loading pulse, eased from frame to frame.\n */\nexport const createWaveLine = ({\n  loading,\n  mode,\n  reducedMotion,\n  sensitivity,\n}: WaveLineOptions): WaveLine => {\n  const targets = new Float32Array(WAVE_LINE_MAX_POINTS);\n  const bands = createBarLevels({\n    barCount: WAVE_PARTIALS.length,\n    idle: \"static\",\n    loading: false,\n    minLevel: 0,\n    mirrored: false,\n    reducedMotion,\n  });\n  const phases = new Float32Array(WAVE_PARTIALS.length);\n  const state = {\n    count: 2,\n    heights: new Float32Array(WAVE_LINE_MAX_POINTS),\n    loudness: 0,\n    peak: 0,\n  };\n  let lastMs = 0;\n  let scopePeak = 0;\n\n  const fillWave = (frame: VisualFrame | null, nowMs: number) => {\n    bands.step(nowMs, frame?.bands ?? null);\n    for (let point = 0; point < state.count; point += 1) {\n      const position = point / (state.count - 1);\n      let height = 0;\n      for (const [index, partial] of WAVE_PARTIALS.entries()) {\n        height +=\n          partial.weight *\n          (bands.levels[index] ?? 0) *\n          Math.sin(TWO_PI * partial.cycles * position + (phases[index] ?? 0));\n      }\n      targets[point] = clamp(height * sensitivity, -1, 1);\n    }\n  };\n\n  const fillTargets = (nowMs: number, frame: VisualFrame | null) => {\n    const { count } = state;\n    if (loading) {\n      const seconds = reducedMotion\n        ? LOADING_REST_SECONDS\n        : nowMs / MS_PER_SECOND;\n      for (let point = 0; point < count; point += 1) {\n        targets[point] = loadingPulse(point / (count - 1), seconds);\n      }\n      return;\n    }\n    const samples = frame?.timeDomain;\n    if (mode === \"scope\" && samples && samples.length > 1) {\n      scopePeak = Math.max(\n        peakOf(samples, samples.length),\n        scopePeak * SCOPE_PEAK_RELEASE\n      );\n      const gain =\n        sensitivity * Math.min(SCOPE_MAX_GAIN, SCOPE_TARGET / (scopePeak || 1));\n      fillScope(samples, targets, count, gain);\n      return;\n    }\n    fillWave(frame, nowMs);\n  };\n\n  const ease = (elapsedMs: number) => {\n    const share = reducedMotion\n      ? 1\n      : 1 - EASE_PER_FRAME ** (elapsedMs / FRAME_MS);\n    for (let point = 0; point < state.count; point += 1) {\n      const height = state.heights[point] ?? 0;\n      state.heights[point] = height + ((targets[point] ?? 0) - height) * share;\n    }\n    const release = LOUDNESS_RELEASE ** (elapsedMs / FRAME_MS);\n    state.loudness = Math.max(\n      peakOf(state.heights, state.count),\n      state.loudness * release\n    );\n  };\n\n  const step = (nowMs: number, frame: VisualFrame | null, count: number) => {\n    const elapsedMs = lastMs === 0 ? FRAME_MS : nowMs - lastMs;\n    lastMs = nowMs;\n    state.count = clamp(Math.round(count), 2, WAVE_LINE_MAX_POINTS);\n    if (!reducedMotion) {\n      const seconds = clamp(elapsedMs / MS_PER_SECOND, 0, MAX_STEP_SECONDS);\n      for (const [index, partial] of WAVE_PARTIALS.entries()) {\n        phases[index] =\n          ((phases[index] ?? 0) + partial.drift * seconds) % TWO_PI;\n      }\n    }\n    fillTargets(nowMs, frame);\n    state.peak = peakOf(targets, state.count);\n    ease(elapsedMs);\n    return !loading && state.peak >= SIGNAL_THRESHOLD;\n  };\n\n  return Object.assign(state, { step });\n};\n",
      "type": "registry:lib"
    },
    {
      "path": "lib/audio/time.ts",
      "content": "const SECONDS_PER_MINUTE = 60;\nconst SECONDS_PER_HOUR = 3600;\nconst MINUS_SIGN = \"−\";\n\nexport interface FormatTimeOptions {\n  /** Prefix with \"−\", for time remaining. */\n  remaining?: boolean;\n  /** Always show hours. */\n  hours?: boolean;\n}\n\nconst pad = (value: number): string => String(value).padStart(2, \"0\");\n\n/** Formats seconds as `m:ss`, or `h:mm:ss` from one hour. */\nexport const formatTime = (\n  seconds: number,\n  options: FormatTimeOptions = {}\n): string => {\n  const total = Number.isFinite(seconds) ? Math.max(0, Math.floor(seconds)) : 0;\n  const hours = Math.floor(total / SECONDS_PER_HOUR);\n  const minutes = Math.floor((total % SECONDS_PER_HOUR) / SECONDS_PER_MINUTE);\n  const secs = total % SECONDS_PER_MINUTE;\n  const prefix = options.remaining ? MINUS_SIGN : \"\";\n\n  if (hours > 0 || options.hours) {\n    return `${prefix}${hours}:${pad(minutes)}:${pad(secs)}`;\n  }\n  return `${prefix}${minutes}:${pad(secs)}`;\n};\n",
      "type": "registry:lib"
    },
    {
      "path": "lib/audio/frame-loop.ts",
      "content": "type FrameListener = (nowMs: number) => void;\ntype FramePhase = \"update\" | \"paint\";\n\nconst listeners = {\n  paint: new Set<FrameListener>(),\n  update: new Set<FrameListener>(),\n};\nlet handle: number | null = null;\nlet ticking = false;\n\nconst hasListeners = () => listeners.update.size + listeners.paint.size > 0;\n\nconst reportError = (error: unknown) => {\n  queueMicrotask(() => {\n    throw error;\n  });\n};\n\nconst tick = (nowMs: number) => {\n  handle = null;\n  ticking = true;\n  for (const phase of [listeners.update, listeners.paint]) {\n    for (const listener of phase) {\n      try {\n        listener(nowMs);\n      } catch (error) {\n        reportError(error);\n      }\n    }\n  }\n  ticking = false;\n  if (hasListeners() && handle === null) {\n    handle = requestAnimationFrame(tick);\n  }\n};\n\n/**\n * Runs sources in the update phase, then renderers in the default paint phase.\n * One loop serves the whole page and stops when nothing is subscribed.\n * The browser pauses it while the tab is hidden.\n */\nexport const subscribeFrame = (\n  listener: FrameListener,\n  phase: FramePhase = \"paint\"\n): (() => void) => {\n  listeners[phase].add(listener);\n  if (\n    handle === null &&\n    !ticking &&\n    typeof requestAnimationFrame === \"function\"\n  ) {\n    handle = requestAnimationFrame(tick);\n  }\n  return () => {\n    listeners[phase].delete(listener);\n    if (!hasListeners() && handle !== null) {\n      cancelAnimationFrame(handle);\n      handle = null;\n    }\n  };\n};\n\n/** One frame of a task. Return true while the task needs another frame. */\nexport type FrameTaskStep = (nowMs: number) => boolean;\n\nexport interface FrameTask {\n  /** Runs the step again from the next frame, if the task is asleep. */\n  wake: () => void;\n  /** Stops the task for good. A stopped task ignores `wake`. */\n  stop: () => void;\n}\n\n/**\n * Runs `step` on the shared loop for as long as it returns true. When it\n * returns false the task sleeps and requests no frames until `wake()`, so a\n * settled, silent or hidden painter costs nothing. The task starts awake.\n */\nexport const createFrameTask = (step: FrameTaskStep): FrameTask => {\n  let stopped = false;\n  let unsubscribe: (() => void) | null = null;\n\n  const sleep = () => {\n    unsubscribe?.();\n    unsubscribe = null;\n  };\n\n  const run = (nowMs: number) => {\n    if (!step(nowMs)) {\n      sleep();\n    }\n  };\n\n  const wake = () => {\n    if (!stopped && unsubscribe === null) {\n      unsubscribe = subscribeFrame(run);\n    }\n  };\n\n  wake();\n\n  return {\n    stop: () => {\n      stopped = true;\n      sleep();\n    },\n    wake,\n  };\n};\n\n/** The longest step a painter clock takes, so waking never jumps. */\nexport const MAX_FRAME_GAP_MS = 100;\n\n/**\n * A clock that follows the frames but never advances more than\n * `MAX_FRAME_GAP_MS` at once. A painter that slept, or ran in a hidden tab,\n * resumes its ballistics where they were instead of jumping to the end.\n */\nexport const createPainterClock = (): ((nowMs: number) => number) => {\n  let lastMs: number | null = null;\n  let clockMs = 0;\n  return (nowMs) => {\n    if (lastMs !== null) {\n      clockMs += Math.min(Math.max(0, nowMs - lastMs), MAX_FRAME_GAP_MS);\n    }\n    lastMs = nowMs;\n    return clockMs;\n  };\n};\n",
      "type": "registry:lib"
    },
    {
      "path": "lib/audio/frame-source.ts",
      "content": "import type { FrameSource } from \"@/lib/audio/types\";\n\nexport interface FrameEmitter<T> extends FrameSource<T> {\n  /** Sends a frame to every subscriber. */\n  emit: (frame: T) => void;\n  /** The last frame emitted, if any. */\n  readonly latest: T | undefined;\n}\n\n/**\n * Creates a frame source you push to yourself: from a WebSocket, a worker, a\n * native bridge or a test.\n */\nexport const createFrameEmitter = <T>(): FrameEmitter<T> => {\n  const subscribers = new Set<(frame: T) => void>();\n  let latest: T | undefined;\n\n  return {\n    emit: (frame) => {\n      latest = frame;\n      for (const subscriber of subscribers) {\n        subscriber(frame);\n      }\n    },\n    get latest() {\n      return latest;\n    },\n    subscribe: (listener) => {\n      subscribers.add(listener);\n      return () => {\n        subscribers.delete(listener);\n      };\n    },\n  };\n};\n\nexport interface FrameRelay<T> extends FrameSource<T> {\n  /** Forwards frames from `source`. Subscribers stay attached when it changes. */\n  setSource: (source: FrameSource<T> | null) => void;\n}\n\n/**\n * A frame source whose upstream can be swapped. Components keep a stable\n * subscription while the audio behind it is rebuilt.\n */\nexport const createFrameRelay = <T>(): FrameRelay<T> => {\n  const subscribers = new Set<(frame: T) => void>();\n  let source: FrameSource<T> | null = null;\n  let detach: (() => void) | null = null;\n\n  const forward = (frame: T) => {\n    for (const subscriber of subscribers) {\n      subscriber(frame);\n    }\n  };\n\n  const attach = () => {\n    if (!detach && source && subscribers.size > 0) {\n      detach = source.subscribe(forward);\n    }\n  };\n\n  const release = () => {\n    detach?.();\n    detach = null;\n  };\n\n  return {\n    setSource: (next) => {\n      if (next === source) {\n        return;\n      }\n      release();\n      source = next;\n      attach();\n    },\n    subscribe: (listener) => {\n      subscribers.add(listener);\n      attach();\n      return () => {\n        subscribers.delete(listener);\n        if (subscribers.size === 0) {\n          release();\n        }\n      };\n    },\n  };\n};\n",
      "type": "registry:lib"
    },
    {
      "path": "lib/audio/history.ts",
      "content": "import type { VisualFrame } from \"@/lib/audio/types\";\n\nexport const appendHistory = (\n  frame: VisualFrame,\n  level: number,\n  nowMs: number,\n  intervalMs: number\n): void => {\n  const interval = Number.isFinite(intervalMs) ? Math.max(0, intervalMs) : 0;\n  const elapsed = nowMs - (frame.historyUpdatedAt ?? 0);\n  if (elapsed < interval || frame.history.length === 0) {\n    return;\n  }\n\n  frame.historyIntervalMs = interval;\n  frame.historyUpdatedAt = nowMs;\n  const size = frame.history.length;\n  if (frame.historyLength < size) {\n    frame.history[(frame.historyStart + frame.historyLength) % size] = level;\n    frame.historyLength += 1;\n  } else {\n    frame.historyPreviousLevel = frame.history[frame.historyStart];\n    frame.history[frame.historyStart] = level;\n    frame.historyStart = (frame.historyStart + 1) % size;\n  }\n};\n",
      "type": "registry:lib"
    },
    {
      "path": "lib/audio/history-playback.ts",
      "content": "import { clamp } from \"@/lib/audio/decibels\";\nimport type { VisualFrame } from \"@/lib/audio/types\";\n\ninterface HistorySample {\n  frame: VisualFrame;\n  fromMs: number;\n  toMs: number;\n}\n\nexport const createHistoryPlayback = () => {\n  const samples: HistorySample[] = [];\n  let previousMs: number | undefined;\n  let previousLength = 0;\n  let intervalMs: number | undefined;\n\n  const clear = () => {\n    samples.length = 0;\n    previousMs = undefined;\n    previousLength = 0;\n    intervalMs = undefined;\n  };\n\n  const read = (\n    frame: VisualFrame,\n    nowMs: number\n  ): { frame: VisualFrame; progress: number } => {\n    const time = frame.historyUpdatedAt;\n    const interval = frame.historyIntervalMs;\n    if (\n      time === undefined ||\n      !Number.isFinite(time) ||\n      interval === undefined ||\n      !Number.isFinite(interval) ||\n      interval <= 0 ||\n      frame.historyLength === 0\n    ) {\n      clear();\n      return { frame, progress: 1 };\n    }\n    if (\n      intervalMs !== interval ||\n      frame.historyLength < previousLength ||\n      (previousMs !== undefined && time < previousMs)\n    ) {\n      clear();\n    }\n    intervalMs = interval;\n    previousLength = frame.historyLength;\n    if (time !== previousMs) {\n      samples.push({\n        frame: { ...frame, history: new Float32Array(frame.history) },\n        fromMs: previousMs ?? time - interval,\n        toMs: time,\n      });\n      previousMs = time;\n    }\n\n    const displayMs = nowMs - interval * 2;\n    while (samples.length > 1 && (samples[0]?.toMs ?? 0) <= displayMs) {\n      samples.shift();\n    }\n    const [sample] = samples;\n    if (!sample) {\n      return { frame, progress: 1 };\n    }\n    return {\n      frame: sample.frame,\n      progress: clamp(\n        (displayMs - sample.fromMs) / (sample.toMs - sample.fromMs),\n        0,\n        1\n      ),\n    };\n  };\n\n  return { clear, read };\n};\n",
      "type": "registry:lib"
    }
  ],
  "cssVars": {
    "theme": {
      "color-meter-ok": "var(--meter-ok)",
      "color-meter-warn": "var(--meter-warn)",
      "color-meter-clip": "var(--meter-clip)",
      "color-channel-mute": "var(--channel-mute)",
      "color-channel-solo": "var(--channel-solo)",
      "color-channel-monitor": "var(--channel-monitor)",
      "color-meter-ok-foreground": "var(--meter-ok-foreground)",
      "color-meter-warn-foreground": "var(--meter-warn-foreground)",
      "color-meter-clip-foreground": "var(--meter-clip-foreground)",
      "color-channel-mute-foreground": "var(--channel-mute-foreground)",
      "color-channel-solo-foreground": "var(--channel-solo-foreground)",
      "color-channel-monitor-foreground": "var(--channel-monitor-foreground)"
    },
    "light": {
      "meter-ok": "oklch(0.696 0.17 162.48)",
      "meter-warn": "oklch(0.795 0.184 86.047)",
      "meter-clip": "var(--destructive)",
      "channel-mute": "var(--destructive)",
      "channel-solo": "oklch(0.795 0.184 86.047)",
      "channel-monitor": "oklch(0.623 0.214 259.815)",
      "meter-ok-foreground": "color-mix(in oklch, var(--meter-ok), black 40%)",
      "meter-warn-foreground": "color-mix(in oklch, var(--meter-warn), black 40%)",
      "meter-clip-foreground": "color-mix(in oklch, var(--meter-clip), black 40%)",
      "channel-mute-foreground": "color-mix(in oklch, var(--channel-mute), black 40%)",
      "channel-solo-foreground": "color-mix(in oklch, var(--channel-solo), black 40%)",
      "channel-monitor-foreground": "color-mix(in oklch, var(--channel-monitor), black 40%)"
    },
    "dark": {
      "meter-ok": "oklch(0.765 0.177 163.223)",
      "meter-warn": "oklch(0.852 0.199 91.936)",
      "meter-clip": "var(--destructive)",
      "channel-mute": "var(--destructive)",
      "channel-solo": "oklch(0.852 0.199 91.936)",
      "channel-monitor": "oklch(0.707 0.165 254.624)",
      "meter-ok-foreground": "var(--meter-ok)",
      "meter-warn-foreground": "var(--meter-warn)",
      "meter-clip-foreground": "var(--meter-clip)",
      "channel-mute-foreground": "var(--channel-mute)",
      "channel-solo-foreground": "var(--channel-solo)",
      "channel-monitor-foreground": "var(--channel-monitor)"
    }
  },
  "type": "registry:lib"
}