/** * The audio thread's side of the effects that Web Audio has no node for. A pitched voice (#317) * plays one sound through a `PitchPlayer`, its pitch the AudioParam `pitch` in semitones, so it * glides, ramps and eases like any other knob. A Bitcrusher (#322) runs a `Crusher`, a Chip * (#329) a `ChipPlayer`. Loaded into the context once by the sound library; nothing on the page * imports it. */ import { type ChipShape, ChipPlayer } from '../core/soundfx/chip'; import { Crusher } from '../core/soundfx/crusher'; import { PitchPlayer } from '../core/soundfx/pitchPlayer'; /** What the audio thread has and the page's types do not. */ declare const sampleRate: number; declare const currentTime: number; declare class AudioWorkletProcessor { readonly port: MessagePort; } declare function registerProcessor( name: string, processor: new (options: { processorOptions: T }) => object, ): void; export interface PitchOptions { /** The file the samples are of: they are sent with the first voice of it only and kept here. */ file: string; channels?: Float32Array[]; /** The sound's rate over the context's. */ step: number; loop: boolean; /** Where a loop runs, in samples of the sound (#331); without them it is the whole sound. */ loopStart?: number; loopEnd?: number; /** The second of the audio clock its first sample is heard at (#332); before it, silence. */ startAt?: number; /** Samples of the sound before where it starts, for a clip of a song taken up mid-way (#334). */ skip?: number; } /** Every sound a voice has been played from, for the next voice of it – processors share this scope. */ const sounds = new Map(); class PitchProcessor extends AudioWorkletProcessor { private readonly player: PitchPlayer; private stopped = false; private startAt: number; constructor({ processorOptions }: { processorOptions: PitchOptions }) { super(); const { file, channels, step, loop, loopStart, loopEnd, startAt, skip } = processorOptions; this.startAt = startAt ?? 0; if (channels) sounds.set(file, channels); const region = loop && loopEnd !== undefined ? { start: loopStart ?? 0, end: loopEnd } : loop; this.player = new PitchPlayer(sounds.get(file) ?? [], step, sampleRate, region, skip); this.port.onmessage = (event: MessageEvent) => { if (event.data === 'leave') this.player.leaveLoop(); else this.stopped = true; }; } process( _inputs: Float32Array[][], outputs: Float32Array[][], parameters: Record, ): boolean { if (this.stopped) return false; let out = outputs[0]!; let pitch = parameters.pitch!; // A block is silence until then, the one it falls in starts on that sample. if (this.startAt > currentTime) { const wait = Math.round((this.startAt - currentTime) * sampleRate); if (wait >= (out[0]?.length ?? 0)) return true; out = out.map((channel) => channel.subarray(wait)); if (pitch.length > 1) pitch = pitch.subarray(wait); this.startAt = 0; } if (this.player.render(out, pitch)) return true; this.port.postMessage('ended'); return false; } static get parameterDescriptors() { return [{ name: 'pitch', defaultValue: 0 }]; } } registerProcessor('pitch-player', PitchProcessor); /** A Bitcrusher (#322) between two stages of a voice; it runs until the voice tells it to stop. */ class CrushProcessor extends AudioWorkletProcessor { private readonly crusher = new Crusher(sampleRate); private stopped = false; constructor() { super(); this.port.onmessage = () => { this.stopped = true; }; } process( inputs: Float32Array[][], outputs: Float32Array[][], parameters: Record, ): boolean { if (this.stopped) return false; this.crusher.render(inputs[0] ?? [], outputs[0]!, parameters.bits!, parameters.rate!, parameters.mix!); return true; } static get parameterDescriptors() { return [ { name: 'bits', defaultValue: 16 }, { name: 'rate', defaultValue: 48000 }, { name: 'mix', defaultValue: 0 }, ]; } } registerProcessor('bit-crusher', CrushProcessor); export interface ChipOptions extends ChipShape { /** The second of the audio clock its first sample is heard at; before it, silence. */ startAt: number; } /** The Chip of a note (#329): a source of its own, its frequency, duty and pitch AudioParams. */ class ChipProcessor extends AudioWorkletProcessor { private readonly player: ChipPlayer; private stopped = false; private startAt: number; constructor({ processorOptions }: { processorOptions: ChipOptions }) { super(); this.player = new ChipPlayer(processorOptions, sampleRate); this.startAt = processorOptions.startAt; this.port.onmessage = () => { this.stopped = true; }; } process( _inputs: Float32Array[][], outputs: Float32Array[][], parameters: Record, ): boolean { if (this.stopped) return false; let out = outputs[0]!; let { frequency, duty, pitch } = parameters as Record<'frequency' | 'duty' | 'pitch', Float32Array>; if (this.startAt > currentTime) { const wait = Math.round((this.startAt - currentTime) * sampleRate); if (wait >= (out[0]?.length ?? 0)) return true; const from = (values: Float32Array) => (values.length > 1 ? values.subarray(wait) : values); out = out.map((channel) => channel.subarray(wait)); [frequency, duty, pitch] = [from(frequency), from(duty), from(pitch)]; this.startAt = 0; } if (this.player.render(out, frequency, duty, pitch)) return true; this.port.postMessage('ended'); return false; } static get parameterDescriptors() { return [ { name: 'frequency', defaultValue: 440 }, { name: 'duty', defaultValue: 0.5 }, { name: 'pitch', defaultValue: 0 }, ]; } } registerProcessor('chip-player', ChipProcessor);