audio
Version:
Audio loading, editing, and rendering for JavaScript
250 lines (229 loc) • 13.5 kB
TypeScript
/**
* audio — paged audio instance with declarative ops.
*/
/** Time value: seconds as number, or parseable string ('1.5s', '500ms', '1:30') */
type Time = number | string
type AudioSource = AudioInstance | AudioBuffer | Float32Array[] | number
type FilterType = 'highpass' | 'lowpass' | 'bandpass' | 'notch' | 'eq' | 'lowshelf' | 'highshelf'
export interface AudioInstance {
/** Decoded PCM pages */
pages: Float32Array[][]
/** Per-channel, per-block stats (min/max/energy + registered fields) */
stats: AudioStats
/** Sample rate in Hz */
sampleRate: number
/** Effective channel count (reflects remix edits) */
readonly channels: number
/** Effective sample count (reflects structural edits) */
readonly length: number
/** Effective duration in seconds */
readonly duration: number
/** Original source reference (URL/path string, or null for PCM-backed) */
source: string | null
/** Storage mode */
storage: string
/** Promise — resolves to true when ready (decoded, mic active, etc.) */
ready: Promise<true>
/** Edit list (inspectable) */
edits: EditOp[]
/** Monotonic counter, increments on edit/undo */
version: number
/** Current position in seconds (read/write, or use seek()) */
currentTime: number
/** True when playing */
playing: boolean
/** True when paused */
paused: boolean
/** Playback volume, 0 (silent) to 1 (full). Clamped. */
volume: number
/** Whether playback is muted (independent of volume) */
muted: boolean
/** Playback speed ratio: 1 = normal, 2 = double speed, 0.5 = half. Clamped 0.0625–16. */
playbackRate: number
/** Whether playback loops */
loop: boolean
/** True when playback ended naturally (not via stop) */
ended: boolean
/** True during a seek operation */
seeking: boolean
/** Promise — resolves when playback actually starts (speaker opens), rejects on failure */
played: Promise<void>
/** Current playback block for visualization */
block: Float32Array | null
// ── Events ──────────────────────────────────────────────────────
/** Subscribe to instance event */
on(event: 'change', fn: () => void): this
on(event: 'metadata', fn: (event: { sampleRate: number, channels: number }) => void): this
on(event: 'data', fn: (event: { delta: ProgressDelta, offset: number, sampleRate: number, channels: number }) => void): this
on(event: 'progress', fn: (event: { offset: number, total: number }) => void): this
on(event: 'timeupdate', fn: (time: number) => void): this
on(event: 'ended', fn: () => void): this
on(event: 'play', fn: () => void): this
on(event: 'pause', fn: () => void): this
on(event: 'volumechange', fn: () => void): this
on(event: 'ratechange', fn: () => void): this
on(event: 'error', fn: (err: Error) => void): this
on(event: string, fn: (...args: any[]) => void): this
/** Unsubscribe from instance event */
off(event: string, fn: (...args: any[]) => void): this
/** Dispose — stop playback/recording, clear listeners, release caches */
dispose(): void
// ── Core I/O ────────────────────────────────────────────────────
/** Move playhead — preloads nearby pages, triggers seek if playing */
seek(t: number): this
/** Read audio data. Channel option returns single Float32Array. */
read(opts?: { at?: Time, duration?: Time, channel?: number, format?: string, meta?: Record<string, any> }): Promise<Float32Array[] | Float32Array | Int16Array[] | Uint8Array[] | Uint8Array>
/** Async-iterable over materialized blocks. `for await (let block of a)` */
[Symbol.asyncIterator](): AsyncGenerator<Float32Array[], void, unknown>
/** Ensure stats are fresh, return stats + block range */
stat(name: 'db' | 'rms' | 'loudness', opts?: { at?: Time, duration?: Time }): Promise<number>
stat(name: 'clipping', opts?: { at?: Time, duration?: Time }): Promise<Float32Array>
stat(name: 'clipping', opts: { bins: number, at?: Time, duration?: Time }): Promise<Float32Array>
stat(name: 'dc', opts?: { at?: Time, duration?: Time }): Promise<number>
stat(name: 'min' | 'max', opts?: { at?: Time, duration?: Time }): Promise<number>
stat(name: 'min' | 'max', opts: { bins: number, at?: Time, duration?: Time, channel?: number }): Promise<Float32Array>
stat(name: 'min' | 'max', opts: { bins: number, at?: Time, duration?: Time, channel: number[] }): Promise<Float32Array[]>
stat(name: 'spectrum', opts?: { bins?: number, at?: Time, duration?: Time, fMin?: number, fMax?: number, weight?: boolean }): Promise<Float32Array>
stat(name: 'cepstrum', opts?: { bins?: number, at?: Time, duration?: Time }): Promise<Float32Array>
stat(name: 'silence', opts?: { threshold?: number, minDuration?: number, at?: Time, duration?: Time }): Promise<{ at: number, duration: number }[]>
stat<T extends string[]>(name: T, opts?: { at?: Time, duration?: Time, bins?: number, channel?: number | number[] }): Promise<{ [K in keyof T]: number | Float32Array | Float32Array[] }>
stat(name: string, opts?: { at?: Time, duration?: Time, bins?: number, channel?: number | number[] }): Promise<number | Float32Array | Float32Array[]>
spectrum(opts?: { bins?: number, at?: Time, duration?: Time, fMin?: number, fMax?: number, weight?: boolean }): Promise<Float32Array>
cepstrum(opts?: { bins?: number, at?: Time, duration?: Time }): Promise<Float32Array>
silence(opts?: { threshold?: number, minDuration?: number, at?: Time, duration?: Time }): Promise<{ at: number, duration: number }[]>
/** Serialize to JSON */
toJSON(): { source: string | null, edits: EditOp[], sampleRate: number, channels: number, duration: number }
// ── Structural ops ───────────────────────────────────────────
crop(opts?: { at?: Time, duration?: Time }): this
insert(other: AudioSource, opts?: { at?: Time }): this
remove(opts?: { at?: Time, duration?: Time }): this
repeat(times: number, opts?: { at?: Time, duration?: Time }): this
pad(before: number, after?: number): this
speed(rate: number): this
stretch(factor: number): this
pitch(semitones: number): this
// ── Sample ops ──────────────────────────────────────────────
gain(value: number | ((t: number) => number), opts?: { at?: Time, duration?: Time, channel?: number | number[], unit?: 'db' | 'linear' }): this
fade(duration: Time, curve?: 'linear' | 'exp' | 'log' | 'cos', opts?: { at?: Time }): this
reverse(opts?: { at?: Time, duration?: Time }): this
mix(other: AudioSource, opts?: { at?: Time, duration?: Time }): this
write(data: Float32Array[] | Float32Array, opts?: { at?: Time }): this
remix(channels: number | (number | null)[]): this
pan(value: number | ((t: number) => number), opts?: { at?: Time, duration?: Time, channel?: number | number[] }): this
// ── Filters ──────────────────────────────────────────────────
filter(type: FilterType, ...params: number[]): this
filter(fn: (data: Float32Array, params: Record<string, unknown>) => Float32Array, opts?: Record<string, unknown>): this
highpass(freq: number): this
lowpass(freq: number): this
bandpass(freq: number, Q?: number): this
notch(freq: number, Q?: number): this
eq(freq: number, gain?: number, Q?: number): this
lowshelf(freq: number, gain?: number, Q?: number): this
highshelf(freq: number, gain?: number, Q?: number): this
// ── Smart ops ───────────────────────────────────────────────
trim(threshold?: number): this
normalize(): this
normalize(preset: 'streaming' | 'podcast' | 'broadcast'): this
normalize(targetDb: number, opts?: 'lufs' | { mode?: 'peak' | 'lufs' | 'rms', at?: Time, duration?: Time, channel?: number | number[] }): this
normalize(opts: { target?: number, mode?: 'peak' | 'lufs' | 'rms', at?: Time, duration?: Time, channel?: number | number[], dc?: boolean, ceiling?: number }): this
// ── Fns (registered via audio.fn) ───────────────────────────
clip(opts?: { at?: Time, duration?: Time }): AudioInstance
split(...offsets: Time[]): AudioInstance[]
undo(n?: number): EditOp | EditOp[] | null
run(...edits: EditOp[]): this
transform(fn: (channels: Float32Array[], ctx: any) => Float32Array[] | false | null): this
play(opts?: { at?: Time, duration?: Time, volume?: number, rate?: number, loop?: boolean, paused?: boolean }): this
pause(): void
resume(): void
stop(): this
save(target: string | FileSystemWritableFileStream, opts?: { format?: string, at?: Time, duration?: Time, meta?: Record<string, any> }): Promise<void>
encode(format?: string, opts?: { at?: Time, duration?: Time, meta?: Record<string, any> }): Promise<Uint8Array>
encode(opts?: { at?: Time, duration?: Time, meta?: Record<string, any> }): Promise<Uint8Array>
clone(): AudioInstance
}
export interface AudioStats {
blockSize: number
min: Float32Array[]
max: Float32Array[]
energy: Float32Array[]
[field: string]: number | Float32Array[]
}
export interface EditOp {
type: string
[key: string]: any
}
export interface AudioOpts {
sampleRate?: number
channels?: number
storage?: 'memory' | 'persistent' | 'auto'
decode?: 'worker' | 'main'
}
export interface ProgressDelta {
fromBlock: number
min: Float32Array[]
max: Float32Array[]
energy: Float32Array[]
}
export interface OpDescriptor {
process?: (chs: Float32Array[], ctx: Record<string, any>) => Float32Array[] | false | null
plan?: (segs: any[], ctx: Record<string, any>) => any[]
resolve?: (args: any[], ctx: Record<string, any>) => EditOp | EditOp[] | false | null
call?: (std: Function, ...args: any[]) => any
ch?: Function
}
/** Serialized audio instance (from toJSON) */
export interface AudioDocument {
source: string | null
edits: EditOp[]
sampleRate: number
channels: number
duration: number
}
/** No source — returns pushable instance. Use .push() to feed PCM, .record() for mic, .stop() to finalize. */
declare function audio(source?: null, opts?: AudioOpts): AudioInstance & {
push(data: Float32Array[] | Float32Array | ArrayBufferView, format?: string | { format?: string, channels?: number, sampleRate?: number }): AudioInstance
record(opts?: Record<string, any>): AudioInstance
recording: boolean
}
/** Async entry — decode from file/URL/bytes, wrap PCM/silence, concat from array, or restore from JSON */
/** Sync entry — returns instance immediately. Thenable: `await audio(src)` waits for full decode. */
declare function audio(source: string | URL | ArrayBuffer | Uint8Array | AudioBuffer | Float32Array[] | number | AudioDocument | (AudioInstance | string | URL | ArrayBuffer)[], opts?: AudioOpts): AudioInstance & PromiseLike<AudioInstance>
declare namespace audio {
/** Package version */
const version: string
/** Samples per PCM page chunk (default 1024 * BLOCK_SIZE). Set before creating instances. */
let PAGE_SIZE: number
/** Samples per stat block (default 1024). Set before creating instances. */
let BLOCK_SIZE: number
/** OPFS-backed cache backend for large files (browser only) */
function opfsCache(dirName?: string): Promise<{
read(i: number): Promise<Float32Array[]>
write(i: number, data: Float32Array[]): Promise<void>
has(i: number): Promise<boolean>
evict(i: number): Promise<void>
clear(): Promise<void>
}>
/** Sync entry — from PCM data, AudioBuffer, audio instance (structural copy), silence, function source, or typed array with format */
function from(source: Float32Array[] | AudioBuffer | AudioInstance | number, opts?: AudioOpts): AudioInstance
function from(fn: (t: number, i: number) => number | number[], opts: AudioOpts & { duration: number }): AudioInstance
function from(source: Int16Array | Int8Array | Uint8Array | Uint16Array, opts: AudioOpts & { format: string }): AudioInstance
/** Op registration and query */
function op(): Record<string, OpDescriptor>
function op(name: string): OpDescriptor | undefined
function op(name: string, descriptor: OpDescriptor | Function): void
/** Stat registration and query */
interface StatDescriptor {
/** Per-block computation during decode */
block?: (chs: Float32Array[], ctx: { sampleRate: number, [k: string]: unknown }) => number | number[]
/** Reducer for scalar/binned queries: (src, from, to) → number */
reduce?: (src: Float32Array, from: number, to: number) => number
/** Derived aggregation from block stats */
query?: (stats: AudioStats, chs: number[], from: number, to: number, sr: number) => any
}
function stat(): Record<string, StatDescriptor>
function stat(name: string): StatDescriptor | undefined
function stat(name: string, descriptor: StatDescriptor | ((chs: Float32Array[], ctx: { sampleRate: number, [k: string]: unknown }) => number | number[])): void
/** Audio instance prototype — extensible (like $.fn) */
const fn: Record<string, any>
}
export default audio