import { Gain, ToneAudioNode, SynthOptions, Volume, BaseContext, Context } from 'tone'; import { Fade, Track, MidiNoteData } from '@waveform-playlist/core'; import { ZoneMap, Generator, GeneratorType } from 'soundfont2'; import { PlayoutAdapter } from '@waveform-playlist/engine'; type TrackEffectsFunction = (graphEnd: Gain, masterGainNode: ToneAudioNode, isOffline: boolean) => void | (() => void); interface ClipInfo { buffer: AudioBuffer; startTime: number; duration: number; offset: number; fadeIn?: Fade; fadeOut?: Fade; gain: number; } interface ToneTrackOptions { buffer?: AudioBuffer; clips?: ClipInfo[]; track: Track; effects?: TrackEffectsFunction; destination?: ToneAudioNode; /** Max channel count across clips — sets Panner channelCount. Default: 1 */ channelCount?: number; } /** Per-clip scheduling info and audio nodes */ interface ScheduledClip { clipInfo: ClipInfo; fadeGainNode: GainNode; scheduleId: number; } declare class ToneTrack { private scheduledClips; private activeSources; private volumeNode; private panNode; private muteGain; private track; private effectsCleanup?; private _destination; private _hasClosureEffects; private _effectsChainNode; private _scheduleGuardOffset; constructor(options: ToneTrackOptions); /** * Create and start an AudioBufferSourceNode for a clip. * Sources are one-shot: each play or loop iteration creates a fresh one. */ private startClipSource; /** * Set the schedule guard offset. Schedule callbacks for clips before this * offset are suppressed (already handled by startMidClipSources). * Must be called before transport.start() and in the loop handler. */ setScheduleGuardOffset(offset: number): void; /** * Start sources for clips that span the given Transport position. * Used for mid-playback seeking and loop boundary handling where * Transport.schedule() callbacks have already passed. * * Uses strict < for absClipStart to avoid double-creation with * schedule callbacks at exact Transport position (e.g., loopStart). */ startMidClipSources(transportOffset: number, audioContextTime: number): void; /** * Add a clip to this track at runtime. Creates a Transport.schedule event * and fadeGainNode. If playing, starts the source mid-clip if needed. */ addClip(clipInfo: ClipInfo): ScheduledClip; /** * Remove a scheduled clip by index. Clears the Transport event and * disconnects the fadeGainNode. */ removeScheduledClip(index: number): void; /** * Replace clips on this track. Diffs old vs new by buffer + timing — * unchanged clips keep their active sources playing (no audible interruption). * Changed/added/removed clips are rescheduled. Disconnecting a removed clip's * fadeGainNode silences its source immediately (audio path broken) without * needing to explicitly stop it. */ replaceClips(newClips: ClipInfo[], newStartTime?: number): void; /** Compare two clips by reference (buffer), timing, and fade properties */ private _clipsEqual; /** * Stop all active AudioBufferSourceNodes and clear the set. * Native AudioBufferSourceNodes ignore Transport state changes — * they must be explicitly stopped. */ stopAllSources(): void; /** * Schedule fade envelopes for a clip at the given AudioContext time. * Uses native GainNode.gain (AudioParam) directly — no _param workaround needed. */ private scheduleFades; /** * Prepare fade envelopes for all clips based on Transport offset. * Called before Transport.start() to schedule fades at correct AudioContext times. */ prepareFades(when: number, transportOffset: number): void; /** * Cancel all scheduled fade automation and reset to nominal gain. * Called on pause/stop to prevent stale fade envelopes. */ cancelFades(): void; setVolume(gain: number): void; setPan(pan: number): void; setMute(muted: boolean): void; setSolo(soloed: boolean): void; /** * Insert an external effects chain: reroute muteGain → node instead of * muteGain → destination. The caller wires the chain's output onward * (dawcore's EffectsManager connects it to the master bus input). * Mutually exclusive with the TrackEffectsFunction closure model. */ connectEffects(node: AudioNode): void; /** Restore the direct muteGain → destination connection. Safe when nothing is connected. */ disconnectEffects(): void; dispose(): void; get id(): string; get duration(): number; get buffer(): AudioBuffer; get muted(): boolean; get startTime(): number; } /** * Shared interface for tracks managed by TonePlayout. * Both ToneTrack (audio) and MidiToneTrack (MIDI) implement this, * allowing TonePlayout to manage them uniformly. */ interface PlayableTrack { id: string; startTime: number; muted: boolean; duration: number; stopAllSources(): void; startMidClipSources(offset: number, time: number): void; setScheduleGuardOffset(offset: number): void; prepareFades(when: number, offset: number): void; cancelFades(): void; setVolume(gain: number): void; setPan(pan: number): void; setMute(muted: boolean): void; setSolo(soloed: boolean): void; dispose(): void; } interface MidiClipInfo { notes: MidiNoteData[]; startTime: number; duration: number; offset: number; } interface MidiToneTrackOptions { clips: MidiClipInfo[]; track: Track; effects?: TrackEffectsFunction; destination?: ToneAudioNode; synthOptions?: Partial; } /** * MIDI track that always creates both melodic and percussion synths. * Per-note routing uses the `channel` field on each MidiNoteData: * channel 9 → percussion synths, all others → melodic PolySynth. * This enables flattened tracks (mixed channels) to play correctly. */ declare class MidiToneTrack implements PlayableTrack { private scheduledClips; private synth; private kickSynth; private snareSynth; private cymbalSynth; private tomSynth; private volumeNode; private panNode; private muteGain; private track; private effectsCleanup?; constructor(options: MidiToneTrackOptions); /** * Trigger a note using the appropriate synth. * Routes per-note: channel 9 → percussion synths, others → melodic PolySynth. */ private triggerNote; /** * No-op for MIDI — schedule guard is for AudioBufferSourceNode ghost tick prevention. * Tone.Part handles its own scheduling relative to Transport. */ setScheduleGuardOffset(_offset: number): void; /** * For MIDI, mid-clip sources are notes that should already be sounding. * We trigger them with their remaining duration. */ startMidClipSources(transportOffset: number, audioContextTime: number): void; /** * Stop all sounding notes and cancel scheduled Part events. */ stopAllSources(): void; /** * No-op for MIDI — MIDI uses note velocity, not gain fades. */ prepareFades(_when: number, _offset: number): void; /** * No-op for MIDI — no fade automation to cancel. */ cancelFades(): void; setVolume(gain: number): void; setPan(pan: number): void; setMute(muted: boolean): void; setSolo(soloed: boolean): void; dispose(): void; get id(): string; get duration(): number; get muted(): boolean; get startTime(): number; } /** * Result of looking up a MIDI note in the SoundFont. * Contains the AudioBuffer, playbackRate, loop points, and volume envelope. */ interface SoundFontSample { /** Cached AudioBuffer for this sample */ buffer: AudioBuffer; /** Playback rate to pitch-shift from originalPitch to target note */ playbackRate: number; /** Loop mode: 0=no loop, 1=continuous, 3=sustain loop */ loopMode: number; /** Loop start in seconds, relative to AudioBuffer start */ loopStart: number; /** Loop end in seconds, relative to AudioBuffer start */ loopEnd: number; /** Volume envelope attack time in seconds */ attackVolEnv: number; /** Volume envelope hold time in seconds */ holdVolEnv: number; /** Volume envelope decay time in seconds */ decayVolEnv: number; /** Volume envelope sustain level as linear gain 0-1 */ sustainVolEnv: number; /** Volume envelope release time in seconds */ releaseVolEnv: number; } /** * Convert SF2 timecents to seconds. * SF2 formula: seconds = 2^(timecents / 1200) * Default -12000 timecents ≈ 0.001s (effectively instant). */ declare function timecentsToSeconds(tc: number): number; /** * Get a numeric generator value from a zone map. */ declare function getGeneratorValue(generators: ZoneMap, type: GeneratorType): number | undefined; /** * Convert Int16Array sample data to Float32Array. * SF2 samples are 16-bit signed integers; Web Audio needs Float32 [-1, 1]. */ declare function int16ToFloat32(samples: Int16Array): Float32Array; /** * Input parameters for playback rate calculation. */ interface PlaybackRateParams { /** Target MIDI note number (0-127) */ midiNote: number; /** OverridingRootKey generator value, or undefined if not set */ overrideRootKey: number | undefined; /** sample.header.originalPitch (255 means unpitched) */ originalPitch: number; /** CoarseTune generator value in semitones (default 0) */ coarseTune: number; /** FineTune generator value in cents (default 0) */ fineTune: number; /** sample.header.pitchCorrection in cents (default 0) */ pitchCorrection: number; } /** * Calculate playback rate for a MIDI note using the SF2 generator chain. * * SF2 root key resolution priority: * 1. OverridingRootKey generator (per-zone, most specific) * 2. sample.header.originalPitch (sample header) * 3. MIDI note 60 (middle C fallback) * * Tuning adjustments: * - CoarseTune generator (semitones, additive) * - FineTune generator (cents, additive) * - sample.header.pitchCorrection (cents, additive) */ declare function calculatePlaybackRate(params: PlaybackRateParams): number; /** * Input parameters for loop and envelope extraction. */ interface LoopAndEnvelopeParams { /** SF2 generators zone map */ generators: ZoneMap; /** Sample header with loop points and sample rate */ header: { startLoop: number; endLoop: number; sampleRate: number; }; } /** * Extract loop points and volume envelope data from per-zone generators. * * Loop points are stored as absolute indices into the SF2 sample pool. * We convert to AudioBuffer-relative seconds by subtracting header.start * and dividing by sampleRate. * * Volume envelope times are in SF2 timecents; sustain is centibels attenuation. */ declare function extractLoopAndEnvelope(params: LoopAndEnvelopeParams): Omit; /** * Caches parsed SoundFont2 data and AudioBuffers for efficient playback. * * AudioBuffers are created lazily on first access and cached by sample index. * Pitch calculation uses the SF2 generator chain: * OverridingRootKey → sample.header.originalPitch → fallback 60 * * Audio graph per note: * AudioBufferSourceNode (playbackRate for pitch) → GainNode (velocity) → track chain */ declare class SoundFontCache { private sf2; private audioBufferCache; private context; /** * @param context Optional AudioContext for createBuffer(). If omitted, uses * an OfflineAudioContext which doesn't require user gesture — safe to * construct before user interaction (avoids Firefox autoplay warnings). */ constructor(context?: BaseAudioContext); /** * Fetch and parse an SF2 file, resolving only once it's ready to play. * Prefer this over `new SoundFontCache()` + `load()` — the returned cache * is always loaded, so it can't hit the "unloaded cache → PolySynth * fallback" path in createToneAdapter / setSoundFontCache. */ static fromUrl(url: string, options?: { context?: BaseAudioContext; signal?: AbortSignal; }): Promise; /** * Load and parse an SF2 file from a URL. */ load(url: string, signal?: AbortSignal): Promise; /** * Load from an already-fetched ArrayBuffer. */ loadFromBuffer(data: ArrayBuffer): void; get isLoaded(): boolean; /** * Look up a MIDI note and return the AudioBuffer + playbackRate. * * @param midiNote - MIDI note number (0-127) * @param bankNumber - Bank number (0 for melodic, 128 for percussion/drums) * @param presetNumber - GM program number (0-127) * @returns SoundFontSample or null if no sample found for this note */ getAudioBuffer(midiNote: number, bankNumber?: number, presetNumber?: number): SoundFontSample | null; /** * Convert Int16Array sample data to an AudioBuffer. * Uses the extracted int16ToFloat32 for the conversion, then copies into an AudioBuffer. */ private int16ToAudioBuffer; /** * Clear all cached AudioBuffers and release the parsed SF2. */ dispose(): void; } interface SoundFontToneTrackOptions { clips: MidiClipInfo[]; track: Track; soundFontCache: SoundFontCache; /** GM program number (0-127) for melodic instruments */ programNumber?: number; /** Whether this track uses percussion bank (channel 9) */ isPercussion?: boolean; effects?: TrackEffectsFunction; destination?: ToneAudioNode; } /** * MIDI track that uses SoundFont samples for playback. * * Instead of PolySynth synthesis, each note triggers the correct instrument * sample from an SF2 file, pitch-shifted via AudioBufferSourceNode.playbackRate. * * Audio graph per note: * AudioBufferSourceNode (native, one-shot, pitch-shifted) * → GainNode (native, per-note velocity) * → Volume.input (Tone.js, shared per-track) * → Panner → muteGain → effects/destination */ declare class SoundFontToneTrack implements PlayableTrack { /** Rate-limit missing sample warnings — one per track instance (a page- * lifetime static would silence diagnostics for unrelated later tracks * and soundfont swaps). */ private _missingSampleWarned; private scheduledClips; private activeSources; private soundFontCache; private programNumber; private bankNumber; private volumeNode; private panNode; private muteGain; private track; private effectsCleanup?; constructor(options: SoundFontToneTrackOptions); /** * Trigger a note by creating a native AudioBufferSourceNode from the SoundFont cache. * * Per-note routing: channel 9 → bank 128 (drums), others → bank 0 with programNumber. */ private triggerNote; /** * No-op — Tone.Part handles scheduling internally, no ghost tick guard needed. */ setScheduleGuardOffset(_offset: number): void; /** * Start notes that should already be sounding at the current transport offset. */ startMidClipSources(transportOffset: number, audioContextTime: number): void; /** * Stop all active AudioBufferSourceNodes. */ stopAllSources(): void; /** No-op for MIDI — MIDI uses note velocity, not gain fades. */ prepareFades(_when: number, _offset: number): void; /** No-op for MIDI — no fade automation to cancel. */ cancelFades(): void; setVolume(gain: number): void; setPan(pan: number): void; setMute(muted: boolean): void; setSolo(soloed: boolean): void; dispose(): void; get id(): string; get duration(): number; get muted(): boolean; get startTime(): number; } type EffectsFunction = (masterGainNode: Volume, destination: ToneAudioNode, isOffline: boolean) => void | (() => void); interface TonePlayoutOptions { tracks?: ToneTrack[]; masterGain?: number; effects?: EffectsFunction; } declare class TonePlayout { private tracks; private masterVolume; private _masterTap; private isInitialized; private soloedTracks; private manualMuteState; private effectsCleanup?; private onPlaybackCompleteCallback?; private _completionEventId; private _loopHandler; private _loopEnabled; private _loopStart; private _loopEnd; private _masterChainNode; constructor(options?: TonePlayoutOptions); private clearCompletionEvent; init(): Promise; addTrack(trackOptions: ToneTrackOptions): ToneTrack; addMidiTrack(trackOptions: MidiToneTrackOptions): MidiToneTrack; addSoundFontTrack(trackOptions: SoundFontToneTrackOptions): SoundFontToneTrack; /** * Apply solo muting after all tracks have been added. * Call this after adding all tracks to ensure solo logic is applied correctly. */ applyInitialSoloState(): void; removeTrack(trackId: string): void; getTrack(trackId: string): PlayableTrack | undefined; getTrackIds(): string[]; /** * Replace clips on a track, preserving the track's audio graph. * Only works for ToneTrack (audio clips), not MidiToneTrack. */ replaceTrackClips(trackId: string, newClips: ClipInfo[], newStartTime?: number): boolean; /** * Start mid-clip sources for a specific track at the current Transport position. * Call after adding/updating a track during active playback so clips that span * the current position produce audio immediately. */ resumeTrackMidPlayback(trackId: string): void; play(when?: number, offset?: number, duration?: number): void; pause(): void; stop(): void; setMasterGain(gain: number): void; /** The master output tap node. In the signal chain: masterVolume → tap → destination. * Connect analyzers/effects/recorders here — parallel or serial. * The tap's native GainNode is on the same standardized-audio-context as adapter.audioContext. */ get masterOutputNode(): GainNode; /** * Native GainNode behind masterVolume.input — the master-bus junction that * per-track effect chains reconnect into (pre master volume). Distinct from * masterOutputNode (the post-volume tap for analyzers). */ get masterBusInputNode(): GainNode; /** Insert an external chain on a track: muteGain → node (caller wires node onward). */ connectTrackOutput(trackId: string, node: AudioNode): void; /** Restore a track's direct connection. No-op for unknown or MIDI tracks. */ disconnectTrackOutput(trackId: string): void; /** * Insert an external master chain after the tap: masterVolume → [closure * effects] → tap → node (caller wires node.output → ctx.destination). */ connectMasterOutput(node: AudioNode): void; /** Restore tap → destination. Safe when no master chain is connected. */ disconnectMasterOutput(): void; setSolo(trackId: string, soloed: boolean): void; private updateSoloMuting; setMute(trackId: string, muted: boolean): void; setLoop(enabled: boolean, loopStart: number, loopEnd: number): void; getCurrentTime(): number; seekTo(time: number): void; dispose(): void; get context(): BaseContext; get sampleRate(): number; setOnPlaybackComplete(callback: () => void): void; } /** * Global AudioContext Manager * * Provides a single AudioContext shared across the entire application. * This context is used by Tone.js for playback and by all recording/monitoring hooks. * * Supports both native AudioContext (WAM 2.0 plugin hosting, requires * AudioListener AudioParams — Firefox fallback to standardized-audio-context) * and standardized-audio-context wrapper for cross-browser compatibility. */ interface AudioContextOptions { /** * Create the global context around a NATIVE AudioContext instead of * standardized-audio-context. Required for WAM 2.0 plugin hosting — WAM * worklets subclass the native AudioWorkletNode and cannot join a * standardized-audio-context graph. Falls back to the default context * (with a console warning) on browsers missing AudioListener AudioParams * (Firefox), where Tone.js Listener initialization would throw. */ nativeAudioContext?: boolean; /** Desired sample rate. Creates a standardized-audio-context AudioContext * at this rate, bypassing Tone.js 15.1.22's limitation. Cross-browser safe. */ sampleRate?: number; /** Latency hint passed to the AudioContext constructor. */ latencyHint?: AudioContextLatencyCategory | number; } /** * Whether this browser can run Tone.js on a raw native AudioContext. * Firefox lacks the AudioListener AudioParams (positionX/…/upZ) that Tone's * Listener wraps eagerly at context initialization (Tone.js #681) — * standardized-audio-context polyfills them, native contexts cannot. */ declare function supportsNativeContextMode(): boolean; /** * True when the global context wraps a native AudioContext (WAM-capable). */ declare function isNativeGlobalContext(): boolean; /** * Configure the global AudioContext with sample rate and latency hints. * Supports both native AudioContext (for WAM 2.0 hosting) and standardized-audio-context. * * Should be called BEFORE getGlobalContext(). If the context already exists * (e.g., from resumeGlobalAudioContext), warns and returns the existing rate. * * ```ts * configureGlobalContext({ sampleRate: 48000, latencyHint: 0 }) * configureGlobalContext({ nativeAudioContext: true, sampleRate: 48000 }) * ``` */ declare function configureGlobalContext(options: AudioContextOptions): number; /** * Get the global Tone.js Context * This is the main context for cross-browser audio operations. * Use context.createAudioWorkletNode(), context.createMediaStreamSource(), etc. * @returns The Tone.js Context instance */ declare function getGlobalContext(): Context; /** * Get or create the global AudioContext * Uses Tone.js Context for cross-browser compatibility * @returns The global AudioContext instance (rawContext from Tone.Context) */ declare function getGlobalAudioContext(): AudioContext; /** * @deprecated Use getGlobalContext() instead * Get the Tone.js Context's rawContext typed as IAudioContext * @returns The rawContext cast as IAudioContext */ declare function getGlobalToneContext(): Context; /** * Resume the global AudioContext if it's suspended * Should be called in response to a user gesture (e.g., button click) * @returns Promise that resolves when context is running */ declare function resumeGlobalAudioContext(): Promise; /** * Get the current state of the global AudioContext * @returns The AudioContext state ('suspended', 'running', or 'closed') */ declare function getGlobalAudioContextState(): AudioContextState; /** * Close the global AudioContext * Should only be called when the application is shutting down */ declare function closeGlobalAudioContext(): Promise; /** * MediaStreamSource Manager * * Manages MediaStreamAudioSourceNode instances to ensure only one source * is created per MediaStream per AudioContext. * * Web Audio API constraint: You can only create one MediaStreamAudioSourceNode * per MediaStream per AudioContext. Multiple attempts will fail or disconnect * previous sources. * * This manager ensures a single source is shared across multiple consumers * (e.g., AnalyserNode for VU meter, AudioWorkletNode for recording). * * NOTE: With Tone.js Context, you can also use context.createMediaStreamSource() * directly, which handles cross-browser compatibility internally. */ /** * Get or create a MediaStreamAudioSourceNode for the given stream * * Automatic cleanup fires when the stream's tracks end remotely (device * unplugged, remote peer stopped). NOTE: a LOCAL `track.stop()` does NOT * fire the track-level 'ended' event (per spec) — call * releaseMediaStreamSource() when tearing a stream down yourself. * * @param stream - The MediaStream to create a source for * @returns MediaStreamAudioSourceNode that can be connected to multiple nodes */ declare function getMediaStreamSource(stream: MediaStream): MediaStreamAudioSourceNode; /** * Manually release a MediaStreamSource * * Required after a local `track.stop()` (which fires no 'ended' event); * remote-ended streams clean up automatically. * * @param stream - The MediaStream to release the source for */ declare function releaseMediaStreamSource(stream: MediaStream): void; /** * Check if a MediaStreamSource exists for the given stream * * @param stream - The MediaStream to check * @returns true if a source exists for this stream */ declare function hasMediaStreamSource(stream: MediaStream): boolean; /** * Tone.js-specific fade helpers. Pure fade utilities (curves, applyFadeIn/ * applyFadeOut) live in @waveform-playlist/core — import them from there * directly (repo rule: no cross-package re-exports). */ declare function getUnderlyingAudioParam(signal: unknown): AudioParam | undefined; interface ToneAdapterOptions { effects?: EffectsFunction; /** When provided, MIDI clips use SoundFont sample playback instead of PolySynth */ soundFontCache?: SoundFontCache; /** Pulses per quarter note. Defaults to 192 (Tone.js native). */ ppqn?: number; } /** * Effects wiring hooks consumed by dawcore's EffectsManager (structural match * for its EffectsTransportLike). All hooks require native-context mode — * effect chains carry native AudioNodes (incl. WAM worklets) that cannot join * a standardized-audio-context graph. */ interface ToneEffectsTransport { connectTrackOutput(trackId: string, node: AudioNode): void; disconnectTrackOutput(trackId: string): void; connectMasterOutput(node: AudioNode): void; disconnectMasterOutput(): void; readonly masterOutputNode: AudioNode; } interface ToneAdapter extends PlayoutAdapter { /** * Provide or swap the SoundFont after creation. Rebuilds only the MIDI * tracks whose routing changes; audio tracks keep playing untouched. * Pass undefined to revert MIDI tracks to PolySynth synthesis. */ setSoundFontCache(cache: SoundFontCache | undefined): void; /** Effects wiring hooks (dawcore EffectsManager). Requires native-context mode. */ readonly transport: ToneEffectsTransport; } /** * Capability check for Tone-specific adapter features. Structural, not * instanceof — any adapter implementing setSoundFontCache passes. Narrows a * generic PlayoutAdapter so soundfont calls typecheck without casts. */ declare function isToneAdapter(adapter: PlayoutAdapter | null | undefined): adapter is ToneAdapter; declare function createToneAdapter(options?: ToneAdapterOptions): ToneAdapter; export { type AudioContextOptions, type EffectsFunction, type LoopAndEnvelopeParams, type MidiClipInfo, MidiToneTrack, type MidiToneTrackOptions, type PlayableTrack, type PlaybackRateParams, SoundFontCache, type SoundFontSample, SoundFontToneTrack, type SoundFontToneTrackOptions, type ToneAdapter, type ToneAdapterOptions, type ToneEffectsTransport, TonePlayout, type TonePlayoutOptions, ToneTrack, type ToneTrackOptions, type TrackEffectsFunction, calculatePlaybackRate, closeGlobalAudioContext, configureGlobalContext, createToneAdapter, extractLoopAndEnvelope, getGeneratorValue, getGlobalAudioContext, getGlobalAudioContextState, getGlobalContext, getGlobalToneContext, getMediaStreamSource, getUnderlyingAudioParam, hasMediaStreamSource, int16ToFloat32, isNativeGlobalContext, isToneAdapter, releaseMediaStreamSource, resumeGlobalAudioContext, supportsNativeContextMode, timecentsToSeconds };