{"version":3,"file":"audio_decode.cjs","names":[],"sources":["../../../../src/batteries/media/engines/audio_decode.ts"],"sourcesContent":["/**\n * A cross-environment audio-decoding {@link @nhtio/adk/batteries/media/contracts!MediaEngine}\n * backed by the `audio-decode` package (pure JS/WASM codecs — no ffmpeg, no native bindings;\n * works in Node and browsers).\n *\n * @module @nhtio/adk/batteries/media/engines/audio_decode\n *\n * @remarks\n * Declares two convert capabilities: audio containers to the virtual `pcm` token (mp3 /\n * m4a-aac / ogg-vorbis / opus / flac / wav), downmixed to mono; and a generation edge —\n * `EMPTY_MIME` → wav. Audio bytes are just bytes in a documented envelope: the generated\n * seed is a canonical 44-byte RIFF/WAVE header plus one second of 16-bit mono silence at\n * 44100 Hz, written by a dependency-free helper (generation never loads the decode peer).\n * The PCM output reports the SOURCE sample rate in `meta.sampleRate` — the pipeline's\n * transcribe step resamples to the 16 kHz transcription engines expect. For exotic\n * containers, compose an ffmpeg-backed engine instead; the capability declaration is the\n * seam.\n *\n * `audio-decode` is an optional peer dependency, lazily imported on first actual use.\n */\n\nimport { isError } from '@nhtio/adk/guards'\nimport { downmixToMono } from '../../../lib/utils/audio'\nimport { pcmToBytes, PCM_MIME, EMPTY_MIME } from '../contracts'\nimport { E_INVALID_MEDIA_PIPELINE_CONFIG } from '../exceptions'\nimport type { MediaEngine, ConvertRequest, ConvertResult } from '../contracts'\n\n/**\n * The decoded shapes audio-decode resolves to. Some codecs return an AudioBuffer-compatible\n * object (`numberOfChannels` + `getChannelData`); others (e.g. the wav path in Node) return a\n * plain `{ channelData: Float32Array[], sampleRate }` record. The engine normalizes both.\n */\nexport interface AudioBufferLike {\n  /** Channel count when the AudioBuffer-compatible shape is returned. */\n  numberOfChannels?: number\n  /** Sample rate of the decoded audio, in Hz. Present on both shapes. */\n  sampleRate: number\n  /** Per-channel sample accessor on the AudioBuffer-compatible shape. */\n  getChannelData?(channel: number): Float32Array\n  /** Raw per-channel sample arrays on the plain-record shape. */\n  channelData?: Float32Array[]\n}\n\n/** The decode function shape the `audio-decode` package exports. */\nexport type AudioDecodeFn = (bytes: Uint8Array | ArrayBuffer) => Promise<AudioBufferLike>\n\nconst channelsOf = (buffer: AudioBufferLike): Float32Array[] => {\n  if (Array.isArray(buffer.channelData)) return buffer.channelData\n  if (typeof buffer.getChannelData === 'function') {\n    const count = buffer.numberOfChannels ?? 1\n    return Array.from({ length: count }, (_, c) => buffer.getChannelData!(c))\n  }\n  throw new Error('audio-decode returned an unrecognized buffer shape')\n}\n\n/**\n * Mint one second of 16-bit mono silence at 44100 Hz as a canonical RIFF/WAVE file — the\n * 44-byte header plus zeroed sample data. Pure bytes, zero dependencies.\n */\nconst makeSilentWav = (): Uint8Array => {\n  const sampleRate = 44_100\n  const numSamples = sampleRate // 1 second\n  const bytesPerSample = 2 // 16-bit\n  const dataSize = numSamples * bytesPerSample\n  const bytes = new Uint8Array(44 + dataSize) // sample data stays zeroed = silence\n  const view = new DataView(bytes.buffer)\n  const writeAscii = (offset: number, text: string): void => {\n    for (let i = 0; i < text.length; i++) bytes[offset + i] = text.charCodeAt(i)\n  }\n  writeAscii(0, 'RIFF')\n  view.setUint32(4, 36 + dataSize, true)\n  writeAscii(8, 'WAVE')\n  writeAscii(12, 'fmt ')\n  view.setUint32(16, 16, true) // fmt chunk size\n  view.setUint16(20, 1, true) // PCM format\n  view.setUint16(22, 1, true) // mono\n  view.setUint32(24, sampleRate, true)\n  view.setUint32(28, sampleRate * bytesPerSample, true) // byte rate\n  view.setUint16(32, bytesPerSample, true) // block align\n  view.setUint16(34, 16, true) // bits per sample\n  writeAscii(36, 'data')\n  view.setUint32(40, dataSize, true)\n  return bytes\n}\n\n/** Options for {@link audioDecodeEngine}. */\nexport interface AudioDecodeEngineOptions {\n  /** Override the module resolution. Default: `import('audio-decode')`. */\n  audioDecode?: () =>\n    | AudioDecodeFn\n    | { default: AudioDecodeFn }\n    | Promise<AudioDecodeFn | { default: AudioDecodeFn }>\n}\n\n/**\n * Construct the audio-decode-backed engine.\n *\n * @param options - Optional module resolver override.\n * @returns The engine.\n */\nexport const audioDecodeEngine = (options: AudioDecodeEngineOptions = {}): MediaEngine => {\n  let fnPromise: Promise<AudioDecodeFn> | undefined\n  const getDecode = (): Promise<AudioDecodeFn> => {\n    fnPromise ??= Promise.resolve(\n      options.audioDecode ? options.audioDecode() : import('audio-decode')\n    )\n      .then((mod) => {\n        const fn = typeof mod === 'function' ? mod : (mod as { default: AudioDecodeFn }).default\n        if (typeof fn !== 'function') {\n          throw new Error('audio-decode did not resolve to a decode function')\n        }\n        return fn\n      })\n      .catch((err) => {\n        const detail = isError(err) ? err.message : String(err)\n        throw new E_INVALID_MEDIA_PIPELINE_CONFIG([\n          `the audio-decode engine could not load its peer dependency \"audio-decode\": ${detail} — install it (pnpm add audio-decode)`,\n        ])\n      })\n    return fnPromise\n  }\n\n  const convert = async (request: ConvertRequest): Promise<ConvertResult> => {\n    const decode = await getDecode()\n    const buffer = await decode(request.bytes)\n    const channels = channelsOf(buffer)\n    const pcm = downmixToMono(channels)\n    return {\n      outputs: [\n        { bytes: pcmToBytes(pcm), mimeType: PCM_MIME, meta: { sampleRate: buffer.sampleRate } },\n      ],\n    }\n  }\n\n  return {\n    id: 'audio-decode',\n    converts: [\n      {\n        from: [\n          'audio/mpeg',\n          'audio/mp3',\n          'audio/mp4',\n          'audio/aac',\n          'audio/x-m4a',\n          'audio/ogg',\n          'audio/opus',\n          'audio/flac',\n          'audio/x-flac',\n          'audio/wav',\n          'audio/x-wav',\n          'audio/wave',\n        ],\n        to: ['pcm'],\n        convert,\n      },\n      {\n        // Generation: silence in the engine's own input envelope (never loads the peer).\n        from: [EMPTY_MIME],\n        to: ['wav'],\n        convert: async (): Promise<ConvertResult> => ({\n          outputs: [{ bytes: makeSilentWav(), mimeType: 'audio/wav' }],\n        }),\n      },\n    ],\n  }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;AA8CA,IAAM,cAAc,WAA4C;CAC9D,IAAI,MAAM,QAAQ,OAAO,WAAW,GAAG,OAAO,OAAO;CACrD,IAAI,OAAO,OAAO,mBAAmB,YAAY;EAC/C,MAAM,QAAQ,OAAO,oBAAoB;EACzC,OAAO,MAAM,KAAK,EAAE,QAAQ,MAAM,IAAI,GAAG,MAAM,OAAO,eAAgB,CAAC,CAAC;CAC1E;CACA,MAAM,IAAI,MAAM,oDAAoD;AACtE;;;;;AAMA,IAAM,sBAAkC;CACtC,MAAM,aAAa;CACnB,MAAM,aAAa;CACnB,MAAM,iBAAiB;CACvB,MAAM,WAAW,aAAa;CAC9B,MAAM,QAAQ,IAAI,WAAW,KAAa;CAC1C,MAAM,OAAO,IAAI,SAAS,MAAM,MAAM;CACtC,MAAM,cAAc,QAAgB,SAAuB;EACzD,KAAK,IAAI,IAAI,GAAG,IAAI,KAAK,QAAQ,KAAK,MAAM,SAAS,KAAK,KAAK,WAAW,CAAC;CAC7E;CACA,WAAW,GAAG,MAAM;CACpB,KAAK,UAAU,GAAG,OAAe,IAAI;CACrC,WAAW,GAAG,MAAM;CACpB,WAAW,IAAI,MAAM;CACrB,KAAK,UAAU,IAAI,IAAI,IAAI;CAC3B,KAAK,UAAU,IAAI,GAAG,IAAI;CAC1B,KAAK,UAAU,IAAI,GAAG,IAAI;CAC1B,KAAK,UAAU,IAAI,YAAY,IAAI;CACnC,KAAK,UAAU,IAAI,aAAa,gBAAgB,IAAI;CACpD,KAAK,UAAU,IAAI,gBAAgB,IAAI;CACvC,KAAK,UAAU,IAAI,IAAI,IAAI;CAC3B,WAAW,IAAI,MAAM;CACrB,KAAK,UAAU,IAAI,UAAU,IAAI;CACjC,OAAO;AACT;;;;;;;AAiBA,IAAa,qBAAqB,UAAoC,CAAC,MAAmB;CACxF,IAAI;CACJ,MAAM,kBAA0C;EAC9C,cAAc,QAAQ,QACpB,QAAQ,cAAc,QAAQ,YAAY,IAAI,OAAO,eACvD,EACG,MAAM,QAAQ;GACb,MAAM,KAAK,OAAO,QAAQ,aAAa,MAAO,IAAmC;GACjF,IAAI,OAAO,OAAO,YAChB,MAAM,IAAI,MAAM,mDAAmD;GAErE,OAAO;EACT,CAAC,EACA,OAAO,QAAQ;GAEd,MAAM,IAAI,mBAAA,gCAAgC,CACxC,8EAFa,eAAA,QAAQ,GAAG,IAAI,IAAI,UAAU,OAAO,GAAG,EAEiC,sCACvF,CAAC;EACH,CAAC;EACH,OAAO;CACT;CAEA,MAAM,UAAU,OAAO,YAAoD;EAEzE,MAAM,SAAS,OAAM,MADA,UAAU,GACH,QAAQ,KAAK;EAGzC,OAAO,EACL,SAAS,CACP;GAAE,OAAO,kCAAA,WAHD,wBAAA,cADK,WAAW,MACF,CAGF,CAAG;GAAG,UAAU,kCAAA;GAAU,MAAM,EAAE,YAAY,OAAO,WAAW;EAAE,CACxF,EACF;CACF;CAEA,OAAO;EACL,IAAI;EACJ,UAAU,CACR;GACE,MAAM;IACJ;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;GACF;GACA,IAAI,CAAC,KAAK;GACV;EACF,GACA;GAEE,MAAM,CAAC,kCAAA,UAAU;GACjB,IAAI,CAAC,KAAK;GACV,SAAS,aAAqC,EAC5C,SAAS,CAAC;IAAE,OAAO,cAAc;IAAG,UAAU;GAAY,CAAC,EAC7D;EACF,CACF;CACF;AACF"}