Skip to content

Audio API ​

Methods of the audio namespace.

audio ​

audio.analyzeBPM ​

Read the track's BPM metadata tag (the host does not perform audio tempo analysis). Uses foobar2000 cached metadata when complete, falling back to reading the file directly so tracks outside playlists or library are also supported.

ParameterTypeRequiredDescription
pathstringYesTrack path; a |subsong:N suffix selects a subsong. Must not be empty.

Returns

FieldTypeDescription
bpmnumberValue of the BPM tag, between 0 and 500 (exclusive); a tag outside that range fails with NOT_FOUND.
confidencenumberAlways 1.
source"metadata"Always metadata.

success is true on success. On failure the response is { success: false, error, code }; see Error codes for code.

The call fails with NOT_FOUND when the track has no BPM tag or its value is out of range, with INVALID_HANDLE or NO_INFO when the file cannot be opened or its info cannot be read (for example because the file does not exist), and with OPERATION_FAILED for any other error.

js
const result = await fb2k.invoke('audio.analyzeBPM', {
    path: 'C:\\Music\\song.flac'
});
if (result.success === true) {
    console.log(`BPM: ${result.bpm}`);
} else if (result.code === 'NOT_FOUND') {
    console.log('No BPM tag');
}

audio.cancelDecodePcm ​

Experimental API; it may change in future releases.

Cancel a pending decodePcm task started by the same page.

ParameterTypeRequiredDescription
taskIdstringYesTask id from the decodePcm answer. Must not be empty.

Returns

FieldTypeDescription
cancelledbooleanfalse when the task has already ended, does not exist or belongs to another page; these cases are not told apart. A cancelled task receives one audio:pcmFailed with code: 'CANCELLED'.

success is true on success. On failure the response is { success: false, error, code }; see Error codes for code.

A successful cancel sends that taskId exactly one audio:pcmFailed with code: "CANCELLED", which may arrive before this answer, and nothing after it. The decode keeps running while an identical request still waits on it; otherwise a queued task is dropped and a running one is aborted.

js
const pending = await fb2k.invoke('audio.decodePcm', { path: 'C:\\Music\\song.flac' });
if (pending.success === false) throw new Error(pending.error);
await fb2k.invoke('audio.cancelDecodePcm', { taskId: pending.taskId });

audio.cancelFullWaveform ​

Cancel a pending generateFullWaveform request made by the same page.

ParameterTypeRequiredDescription
taskIdstringYesTask id from a pending answer of generateFullWaveform. Must not be empty.

Returns

FieldTypeDescription
cancelledbooleanfalse when the task has already ended, does not exist or belongs to another page; these cases are not told apart. A cancelled task receives one audio:fullWaveformFailed with code: 'CANCELLED'.

success is true on success. On failure the response is { success: false, error, code }; see Error codes for code.

A successful cancel sends that taskId exactly one audio:fullWaveformFailed with code: "CANCELLED", which may arrive before this answer, and nothing after it. The decode keeps running while another request for the same track still waits on it; otherwise a queued decode is dropped and a running one is aborted.

js
const pending = await fb2k.invoke('audio.generateFullWaveform', { path: 'C:\\Music\\song.flac' });
if (pending.success === false) throw new Error(pending.error);
if (pending.status === 'pending') {
    await fb2k.invoke('audio.cancelFullWaveform', { taskId: pending.taskId });
}

audio.decodePcm ​

Experimental API; it may change in future releases.

Decode a track, or a range of it, into float32 planar PCM delivered through a shared buffer. Answers with a task id right away; the samples arrive as a sharedbufferreceived event on chrome.webview together with audio:pcmReady, or the task ends with audio:pcmFailed.

ParameterTypeRequiredDescription
pathstringYesPath of the track to decode; a |subsong:N suffix selects a subsong. Must not be empty.
cueIndexintegerNoCue subsong index, 0-based; takes precedence over a subsong in path. At least 0.
startnumberNoStart of the range, in seconds. At least 0.
endnumberNoEnd of the range, in seconds; defaults to the end of the track and is cut to the track length. Must be greater than start. At least 0.
sampleRateintegerNoSample rate to convert to with foobar2000's resampler; defaults to the source rate. Between 8000 and 192000 inclusive.
monobooleanNoAverage all channels into one.

Returns

FieldTypeDescription
taskIdstringIdentifies the task in events, shared-buffer metadata and cancelDecodePcm; pcm_N.
status"pending"Always pending: decoded PCM is never cached.

success is true on success. On failure the response is { success: false, error, code }; see Error codes for code.

The samples do not travel through the JSON channel. When decoding finishes, the page receives two messages for the taskId, in either order:

  • a sharedbufferreceived event on window.chrome.webview: e.getBuffer() returns the samples and e.additionalData is { purpose: 'audio.decodePcm', taskId, sampleRate, channels, frames, headerBytes: 64 };
  • an audio:pcmReady event: { taskId, path, sampleRate, channels, frames, start, end, duration, truncated, resampled }, where start and end are the range actually decoded, in seconds.

A task that fails sends one audio:pcmFailed ({ taskId, path, code, error }) instead. The SDK method fb.audio.decodePcm pairs the two messages and returns a PcmBuffer; a page that uses it should not also handle these buffers itself.

The buffer starts with a 64-byte little-endian header followed by float32 samples, one run of capacityFrames samples per channel: sample i of channel c is at byte 64 + (c × capacityFrames + i) × 4. capacityFrames is the u32 at byte 24 of the header; only the first frames samples of each run are audio (frames is also the u32 at byte 32).

  • The buffer is read-only shared memory; do not write to it: writing crashes the page's renderer process, and with it the whole foobar2000 window. Copy the samples first (for example into an AudioBuffer) if modification is required.
  • Every listener of one sharedbufferreceived event gets the same ArrayBuffer. Call chrome.webview.releaseBuffer(buffer) once, after the last use; any access after that throws TypeError.
  • One buffer holds at most 256 MiB in 64-bit foobar2000 and 64 MiB in 32-bit. The host estimates the size from the track length before decoding, adding one second of slack, and rejects a larger request with INVALID_PARAMS, details.estimatedBytes and details.limitBytes. Five minutes of 44.1 kHz stereo take about 106 MB; lower sampleRate, set mono or decode shorter ranges to stay under the limit.
  • truncated is true when the audio ran past that estimate (the file is longer than its reported length) or its format changed partway, as in chained Ogg streams; the buffer holds the part before the cut.
  • One task decodes at a time and the rest wait in submission order. Identical requests (same file, range, sampleRate and mono) that arrive while one is in flight share its decode; each still gets its own taskId, buffer and events.
  • Errors known at request time come back in the answer: INVALID_PARAMS, INVALID_PATH, INVALID_HANDLE, NO_INFO, NOT_SUPPORTED (the WebView2 runtime cannot share buffers, or the calling page cannot be located) and OPERATION_FAILED (foobar2000 is shutting down). Later ones arrive as audio:pcmFailed: CANCELLED, INVALID_PARAMS (start is at or past the end of the track), NOT_SUPPORTED (no resampler handles the conversion), DECODER_FAILED, DECODE_FAILED, ORIGIN_DENIED (the page's origin is no longer trusted), OPERATION_FAILED (the buffer could not be allocated or posted) and UNKNOWN_ERROR.
  • Nothing is sent to a page that navigated away or whose window or panel closed before the result was ready, nor when foobar2000 exits.
js
const pending = await fb2k.invoke('audio.decodePcm', {
    path: 'C:\\Music\\song.flac',
    start: 30,
    end: 60,
    sampleRate: 22050,
});
if (pending.success === false) throw new Error(pending.error);
window.chrome.webview.addEventListener('sharedbufferreceived', (e) => {
    const info = e.additionalData;
    if (info?.purpose !== 'audio.decodePcm' || info.taskId !== pending.taskId) return;
    const buffer = e.getBuffer();
    const capacityFrames = new DataView(buffer).getUint32(24, true);
    const channel = (c) => new Float32Array(buffer, 64 + c * capacityFrames * 4, info.frames);
    console.log(info.sampleRate, channel(0).length);
    window.chrome.webview.releaseBuffer(buffer);
});

audio.generateFullWaveform ​

Compute a waveform of a whole track. A cache hit answers ready with the waveform; otherwise the answer is pending with a task id, and the result arrives as audio:fullWaveformReady or audio:fullWaveformFailed.

ParameterTypeRequiredDescription
pathstringYesTrack path; a |subsong:N suffix selects a subsong. Must not be empty.
cueIndexintegerNoSubsong index, 0-based; takes precedence over a subsong in path. At least 0.
resolutionintegerNoWaveform point resolution, clamped to 64..4096. Audio is decoded once per resolution; subsequent variations are computed from cache. Default: 256.
method"rms" | "peak"NoHow each point is taken. Default: "rms".
scale"linear" | "db"NoScale of the points: linear, or db, where 60 dB below the track's own maximum maps to 0. Ignored when signed is set. Default: "linear".
signedbooleanNoKeep PCM polarity; points fall in [-1, 1]. Default: false.
preferCachebooleanNoAnswer from the cache when possible. Default: true.

Returns

FieldTypeDescription
status"ready" | "pending"ready: the waveform is in this answer. pending: it arrives as an event for taskId.
cachedbooleanThe answer came from the cache.
taskIdstringIdentifies the task in events and cancelFullWaveform; pending answers only.
waveformnumber[]ready answers: the points, normalized by maxAmplitude.
maxAmplitudenumberready answers: the largest value of the selected sequence before normalization, in linear full-scale units. On the linear scale waveform[i] * maxAmplitude restores the level; on db, dBFS is (v * 60 - 60) + 20 * log10(maxAmplitude).
durationnumberready answers: track length in seconds.
sampleRateintegerready answers: actual decoded PCM sample rate in Hz, which can differ from the file's nominal sample rate.
channelsintegerready answers: actual decoded PCM channel count.
resolutionintegerPoint count in use.
method"rms" | "peak"As requested.
scale"linear" | "db"As requested.
signedbooleanAs requested.
pathstringThe requested path.

success is true on success. On failure the response is { success: false, error, code }; see Error codes for code.

On a cache miss, requests for the same track and resolution that are already in flight share one decode; each keeps its own taskId and gets a ready event in its own shape. At most two tracks decode at once and the rest wait in submission order, which counts toward the caller's own timeout. Cancel a request that is no longer needed with audio.cancelFullWaveform. Every taskId gets exactly one terminal event, ready or failed (code: "CANCELLED" when cancelled); a cache hit answers synchronously without one. Requests from a popup that closes, or still pending when foobar2000 exits, are dropped without an event.

Errors known at request time come back in the answer: INVALID_PARAMS, INVALID_PATH (the file cannot be found or read) and OPERATION_FAILED (foobar2000 is shutting down, or any other error). A decode that fails later ends with audio:fullWaveformFailed.

Path and Fallback Semantics

  • The C++ layer parses path|subsong:N, canonicalizes the path, and uses that canonical path for handle_create(), decoding, and cache keys.
  • If both cueIndex and path|subsong:N are supplied, cueIndex takes precedence.
  • If cached metadata lacks duration, samplerate, or channels, the host attempts a direct file read before reporting failure.
  • Normalization: Values are normalized against the maximum value of the sequence (rms uses maximum window RMS, peak uses peak amplitude, signed uses absolute peak). This value is returned as maxAmplitude. For linear, absolute level is restored via waveform[i] * maxAmplitude; for db, dBFS = (v * 60 - 60) + 20 * log10(maxAmplitude). A track is decoded only once per resolution; other combinations of method, signed, and scale are computed from cache.
js
// Minimal call: cache hit returns status 'ready', otherwise 'pending' + taskId
const res = await fb2k.invoke('audio.generateFullWaveform', {
    path: 'C:\\Music\\song.flac'
});
if (res.success === false) throw new Error(res.error);
const { status, taskId } = res;

// Higher-resolution peak waveform on a dB scale
const detailed = await fb2k.invoke('audio.generateFullWaveform', {
    path: 'C:\\Music\\song.flac',
    resolution: 1000,
    method: 'peak',
    scale: 'db'
});

audio.getOutputInfo ​

Report foobar2000's volume.

This method takes no parameters.

Returns

FieldTypeDescription
volumenumberVolume in dB, -100 (silence) to 0 (full).
volumePercentnumberThe same volume as a linear percentage, 100 * 10^(volume / 20).

success is true on success. On failure the response is { success: false, error, code }; see Error codes for code.

js
const result = await fb2k.invoke('audio.getOutputInfo');

audio.getPcmDebugState ​

Experimental API; it may change in future releases.

Report the shared-buffer support of the calling page, the decode task queue and the stream subscriptions, for tests and diagnostics.

This method takes no parameters.

Returns

FieldTypeDescription
runtimePcmRuntimeStateShared-buffer support of the calling page; both flags are false when the page cannot be located.
runtime.versionstringVersion of the WebView2 runtime installed on the machine, such as 153.0.4234.48; empty when it cannot be read.
runtime.environment12booleanThe page's environment can create shared buffers (ICoreWebView2Environment12).
runtime.webview17booleanThe page's webview can receive shared buffers (ICoreWebView2_17).
decodePcmDecodeStateThe decodePcm task queue.
decode.activeintegerTasks decoding, including aborted ones whose worker has not returned yet.
decode.queuedintegerTasks waiting for a free slot.
decode.openBufferBytesintegerBytes of decode buffers the host has not closed yet.
streamPcmStreamStateThe stream capture callback and every stream subscription of every page.
stream.callbackRegisteredbooleanThe component has a capture callback registered with the core; false when no subscription exists.
stream.intervalnumberInterval currently requested from the core, in seconds; absent when the core's default is used.
stream.chunkCountintegerChunks the callback has received since the component loaded.
stream.chunkCyclesintegerCPU cycles the callback has spent in total, from QueryThreadCycleTime.
stream.subscriptionsPcmStreamDebugEntry[]Every live stream subscription.
stream.subscriptions[].subscriptionIdstringThe subscription's id.
stream.subscriptions[].windowIdstringWindow id of the subscribing page.
stream.subscriptions[].epochintegerBuffer generation, 0 before the first chunk of audio arrived.
stream.subscriptions[].capacityFramesintegerFrames the current ring holds; 0 before the first chunk.
stream.subscriptions[].writeFramesintegerFrames written into the current ring so far, wrapping at 2^32.

success is true on success. On failure the response is { success: false, error, code }; see Error codes for code.

decode.openBufferBytes drops back to 0 once every finished task has been delivered and its buffer closed on the host side; pages keep their own views until they release them.

js
const res = await fb2k.invoke('audio.getPcmDebugState');
if (res.success === false) throw new Error(res.error);
const { runtime, decode } = res;

audio.getSpectrum ​

Compute one spectrum frame on demand. At least one spectrum subscription must exist.

ParameterTypeRequiredDescription
subscriptionIdstringNoCompute the frame with this subscription's parameters, bin output included; the other parameters are then ignored. An unknown id fails with NOT_FOUND. Must not be empty.
bandsintegerNoUsed without subscriptionId. 0 takes the largest band count among all subscriptions; the value is capped at fftSize / 2. At least 0. Default: 0.
scale"weighted" | "db"NoUsed without subscriptionId; as in subscribeSpectrum. Default: "weighted".
minFrequencynumberNoUsed without subscriptionId; as in subscribeSpectrum. At least 1. Default: 20.
maxFrequencynumberNoUsed without subscriptionId; as in subscribeSpectrum.
output"bands" | "bins"NoWithout subscriptionId only bands is accepted: the FFT size then follows the other subscriptions, so bin output needs a subscription of its own. Default: "bands".
channels"mix" | "stereo"NoWithout subscriptionId only mix is accepted. Default: "mix".

Returns

FieldTypeDescription
subscriptionIdstringSubscription the frame belongs to; absent when the request named none.
output"bands" | "bins"Output data format contained in this frame.
spectrumnumber[]Band frames: one value per band in the frame's scale; weighted values lie in [0, 1], db values have a floor of -160. Bin frames with channels: 'mix': one power value in dB per bin.
leftnumber[]Bin frames with channels: 'stereo': the first channel, one dB value per bin.
rightnumber[]Bin frames with channels: 'stereo': the second channel, or the first again for a mono stream.
bandsintegerBand frames: length of spectrum.
firstBinintegerBin frames: FFT bin index of the first value; value i is centred at (firstBin + i) * sampleRate / fftSize Hz. 0, with empty arrays, when the range holds no bin.
fftSizeintegerFFT size actually used.
scale"weighted" | "db"Scale of the values; always db for bin frames.
channels"mix" | "stereo"Bin frames: the subscription's channel layout.
channelCountintegerBin frames: channel count of the visualization stream; 0 when unknown.
sampleRateintegerSample rate of the visualization stream in Hz; 0 when unknown.
minFrequencynumberLower edge of the frequency range in Hz.
maxFrequencynumberUpper edge in Hz: the requested one capped at sampleRate / 2, or sampleRate / 2 when none was given.
state"playing" | "paused" | "stopped"Paused or stopped frames are silence.
streamTimenumberSeconds since the visualization stream last started; the frame is computed from the fftSize samples ending at this time.
hostTimenumberHost system time when the frame was computed, Unix epoch milliseconds.

success is true on success. On failure the response is { success: false, error, code }; see Error codes for code.

The answer is one frame with the fields listed under audio.subscribeSpectrum. While playback is paused or stopped the answer is a silence frame whose state says which. Without any subscription, or while a new stream has not produced data yet (about 0.7 s after the first subscription), the call fails with OPERATION_FAILED.

Pull or push

Both deliver the same frame. Pull when drawing must follow the display frame by frame or needs more than 60 frames per second: subscribe with fps: 1 to keep the subscription alive, call audio.getSpectrum with its subscriptionId from requestAnimationFrame, wait for each answer before the next call, and skip frames whose streamTime has not changed. Each call computes an FFT on the host's main thread, costing about as much as a pushed frame, so limit the calls to the frame rate you need rather than the display refresh rate. For up to 60 frames per second, pushed frames work equally well.

js
// Pull from requestAnimationFrame: one call at a time, at most about 60 calls per second
const res = await fb2k.invoke('audio.subscribeSpectrum', { fps: 1 });
if (res.success === false) throw new Error(res.error);
const { subscriptionId } = res;
let pending = false;
let lastCall = 0;
let lastStreamTime;
function tick(now) {
    requestAnimationFrame(tick);
    if (pending || now - lastCall < 15) return;
    pending = true;
    lastCall = now;
    fb2k.invoke('audio.getSpectrum', { subscriptionId })
        .then((frame) => {
            if (frame.success && frame.state === 'playing' && frame.streamTime !== lastStreamTime) {
                lastStreamTime = frame.streamTime;
                draw(frame.spectrum);
            }
        })
        .finally(() => {
            pending = false;
        });
}
requestAnimationFrame(tick);

audio.getSpectrumDebugState ​

Report the spectrum runtime as the host sees it, for tests and diagnostics. The shape is not a stable contract.

This method takes no parameters.

Returns

FieldTypeDescription
activebooleanAt least one spectrum subscription is registered.
timerRunningbooleanThe beat thread that pushes frames is running.
timerHwndintegerDeprecated; always 0.
beatSource"high-resolution" | "standard" | nullTimer of the beat thread; null while it is not running.
beatIntervalMsnumber | nullCurrent beat in milliseconds, 1000 / effectiveFps; null while the beat thread is not running.
beatsCoalescedintegerBeats dropped because the previous one had not reached the main thread yet; cumulative.
effectiveFftSizeintegerLargest FFT size requested among all subscriptions; each still computes with its own.
effectiveFpsintegerLargest frame rate requested among all subscriptions.
effectiveBandsintegerLargest band count requested among all subscriptions.
skipFramesintegerTicks the pushed frames will skip because the last one ran over its interval.
framesComputedintegerFFTs computed since the host started; flat while playback is paused or stopped.
streamReadybooleanThe visualization stream exists.
subscriptionCountintegerLength of subscriptions.
dispatchTargetCountintegerLength of dispatchTargets.
subscriptionsSpectrumDebugSubscription[]Every spectrum subscription of every page.
subscriptions[].tokenstringThe subscription's key.
subscriptions[].windowIdstringWindow id of the subscribing page.
subscriptions[].ownerHwndintegerHandle of the window that owns the subscription, as a number.
subscriptions[].eventstringEvent name of the frames.
subscriptions[].fftSizeintegerRequested FFT size.
subscriptions[].fpsintegerFrame rate after clamping.
subscriptions[].bandsintegerBand count after clamping.
subscriptions[].scale"weighted" | "db"Scale in use.
subscriptions[].backgroundThrottlebooleanAs requested.
subscriptions[].minFrequencynumberLower edge of the range in Hz.
subscriptions[].maxFrequencynumber | nullRequested upper edge in Hz; null when the range follows half the stream's sample rate.
subscriptions[].output"bands" | "bins"Output in use.
subscriptions[].channels"mix" | "stereo"Channel layout in use.
dispatchTargetsSpectrumDebugTarget[]Windows the pushed frames are delivered to.
dispatchTargets[].windowIdstringWindow id the frames go to.
dispatchTargets[].ownerHwndintegerHandle of that window, as a number.
dispatchTargets[].eventstringEvent name of the frames.
instanceCountintegerWebView2 instances in the component.
callerHwndintegerWindow handle of the calling page, as a number; 0 when unknown.
callerWindowIdstringWindow id of the calling page; empty when unknown.
callerOwnsSubscriptionbooleanThe calling page owns at least one subscription.
foregroundHwndintegerHandle of the foreground window, as a number.
foregroundPidintegerProcess id of the foreground window.
foregroundIsExternalbooleanThe foreground window belongs to another process, so the background frame limit applies.
foregroundTitlestringTitle of the foreground window.

success is true on success. On failure the response is { success: false, error, code }; see Error codes for code.

js
const result = await fb2k.invoke('audio.getSpectrumDebugState');

audio.getStreamInfo ​

Report the format of the playing track.

This method takes no parameters.

Returns

FieldTypeDescription
playingbooleanA track is playing or paused; the other fields are present only then.
sampleRateintegerSample rate in Hz.
channelsintegerChannel count.
bitrateintegerBitrate in kbps.
codecstringCodec name as foobar2000 reports it, or unknown.
durationnumberTrack length in seconds.

success is true on success. On failure the response is { success: false, error, code }; see Error codes for code.

js
const result = await fb2k.invoke('audio.getStreamInfo');

audio.getWaveform ​

Return the most recent duration seconds of the visualization stream, ending at its current time (streamTime in spectrum frames). Needs a spectrum subscription.

ParameterTypeRequiredDescription
durationnumberNoWindow length in seconds, greater than 0. At most 1. Default: 0.05.
signedbooleanNoWhen true, preserves bipolar PCM waveform clamped to [-1, 1]; when false, maps magnitude from -70..0 dB to [0, 1]. Default: false.
channels"mix" | "stereo"Nomix averages the channels into waveform; stereo returns the first two channels as left and right. Default: "mix".
pointsintegerNoThin the window to this many evenly spaced samples, without averaging; every sample is returned when omitted. Between 2 and 65536 inclusive.

Returns

FieldTypeDescription
waveformnumber[]mix answers: one value per sample.
leftnumber[]stereo answers: the first channel.
rightnumber[]stereo answers: the second channel; a copy of left when the stream has one channel.
durationnumberWindow length in seconds, as requested.
signedbooleanAs requested.
channels"mix" | "stereo"As requested.
sampleRateintegerSample rate of the visualization stream in Hz.
channelCountintegerChannel count of the visualization stream; 1 after setChannelMode with mono.

success is true on success. On failure the response is { success: false, error, code }; see Error codes for code.

Live Stream Only

This API retrieves real-time waveform slices from the currently playing stream, not an offline waveform of the entire track. To generate an offline waveform for a full file, use audio.generateFullWaveform.

  • With one channel right equals left. Channels beyond the second are ignored, not mixed in. After audio.setChannelMode with 'mono' the stream has one channel.
  • Within duration seconds of starting playback, a seek or a manual track change, the part of the window before the restart is zeros.
  • Without a spectrum subscription, or when the stream cannot supply the window, the call fails with OPERATION_FAILED.
js
const res = await fb2k.invoke('audio.getWaveform', { duration: 0.1 });
if (res.success === false) throw new Error(res.error);
const { waveform } = res;

// Two channels, 256 evenly spaced samples each (a stereo field display)
const res2 = await fb2k.invoke('audio.getWaveform', {
  duration: 0.05,
  signed: true,
  channels: 'stereo',
  points: 256,
});
if (res2.success === false) throw new Error(res2.error);
const { left, right } = res2;

audio.isVisualizationAvailable ​

Report whether foobar2000 provides an active visualization stream (required by subscribeSpectrum and getWaveform).

This method takes no parameters.

Returns

FieldTypeDescription
availablebooleanfoobar2000 offers a visualization stream.

success is true on success. On failure the response is { success: false, error, code }; see Error codes for code.

js
const result = await fb2k.invoke('audio.isVisualizationAvailable');

audio.setChannelMode ​

Choose which channels the visualization stream carries. Applies to spectrum frames and getWaveform, not to what is heard.

ParameterTypeRequiredDescription
mode"default" | "mono" | "front" | "back"NoChannels the visualization stream carries. Default: "default".

Returns

FieldTypeDescription
mode"default" | "mono" | "front" | "back"The mode now in effect.

success is true on success. On failure the response is { success: false, error, code }; see Error codes for code.

js
await fb2k.invoke('audio.setChannelMode', { mode: 'mono' });

audio.subscribeSpectrum ​

Subscribe to spectrum frames of the playing audio. Each subscription keeps its own parameters; frames arrive under event in the calling window only, at up to fps frames per second while playback runs, plus one silence frame when it pauses or stops.

ParameterTypeRequiredDescription
subscriptionIdstringNoKey of the subscription; subscribing again with the same id replaces that subscription. When omitted, the key is derived from the calling window and event. Must not be empty.
fftSizeintegerNoFFT size, a power of two; other values fail with INVALID_PARAMS. Band output raises it for 32 or more bands; bin output uses it as given. 65536 is this component's ceiling, not a foobar2000 limit, and costs about 1.5 s of PCM per frame. Between 256 and 65536 inclusive. Default: 1024.
bandsintegerNoBand output only: number of bands, clamped to 8..fftSize / 2. Default: 48.
fpsintegerNoFrames per second, clamped to 1..60. Default: 30.
scale"weighted" | "db"NoBand scale, weighted when omitted. With output: 'bins' omit it or pass db; anything else fails with INVALID_PARAMS.
output"bands" | "bins"NoWhat the frames carry; bands is ignored for bin output. Default: "bands".
channels"mix" | "stereo"NoBin output only; stereo with band output fails with INVALID_PARAMS. Default: "mix".
backgroundThrottlebooleanNotrue limits this subscription to at most 12 frames per second, usually 10 to 12, while another application is in the foreground; false keeps the full rate. Default: true.
minFrequencynumberNoLower edge of the frequency range in Hz; bands divide the range logarithmically. At least 1. Default: 20.
maxFrequencynumberNoUpper edge in Hz, greater than minFrequency. Capped at half the stream's sample rate when frames are computed, and follows it when omitted. A range entirely above that frequency yields silence frames.
eventstringNoEvent name the frames are delivered under. Must not be empty. Default: "audio:spectrum".

Returns

FieldTypeDescription
subscriptionIdstringSubscription identifier, as explicitly specified or derived by the host.
fftSizeintegerRequested FFT size; frames report the size actually used.
bandsintegerEffective band count after clamping.
fpsintegerEffective frame rate after clamping.
scale"weighted" | "db"Scale in use; always db for bin output.
backgroundThrottlebooleanAs requested.
minFrequencynumberLower edge of the range in Hz, as requested.
maxFrequencynumber | nullRequested upper edge in Hz; null when the range follows half the stream's sample rate.
output"bands" | "bins"Output registered.
channels"mix" | "stereo"Channel layout registered.
eventstringEvent name the frames arrive under.
streamReadybooleantrue once the visualization stream exists, which includes the stopped state; whether audio flows shows in the frames' state.

success is true on success. On failure the response is { success: false, error, code }; see Error codes for code.

Another subscription with a larger FFT size, band count or frame rate does not change the frames this one receives. A registered subscription always answers success: true; streamReady is true once the visualization stream exists, which includes the stopped state. Invalid parameters register nothing and fail with INVALID_PARAMS; an fftSize that is not a power of two, a maxFrequency not above minFrequency, channels: 'stereo' with band output and a scale other than db with bin output also carry details: { param, value }.

Each frame is delivered to the subscribing window only, under the subscription's event:

FieldDescription
subscriptionIdSubscription the frame belongs to. Several subscriptions on the same event name each receive their own frames, so filter on this field.
outputbands or bins. Older hosts omit it and send band frames.
spectrumBand frames: one value per band in the subscription's scale; weighted values lie in [0, 1], db values have a floor of -160. Bin frames with channels: 'mix': one power value in dB per bin.
left, rightBin frames with channels: 'stereo', in place of spectrum: the first two channels of the visualization stream; for a mono stream right repeats left.
bandsBand frames: length of spectrum. Bin frames do not carry it.
firstBinBin frames: FFT bin index of the first value; value i is centred at (firstBin + i) * sampleRate / fftSize Hz. 0, with empty arrays, when the range holds no bin.
fftSizeFFT size actually used. For band output, band counts of 32 and 64 or more raise it to at least 4096 and 8192; bin output uses the requested size.
scaleweighted or db; bin frames always carry db.
channels, channelCountBin frames: the subscription's channels and the channel count of the visualization stream, 0 when unknown.
sampleRateSample rate of the visualization stream in Hz; 0 when unknown.
minFrequency, maxFrequencyFrequency range that bands divide logarithmically and that a bin's centre must lie in: the subscription's minFrequency (20 Hz by default) to its maxFrequency capped at sampleRate / 2, or sampleRate / 2 when none was given. The answer to the subscription echoes the requested values, with maxFrequency: null when it was omitted.
stateplaying, paused or stopped. When playback pauses or stops, each subscription receives one silence frame with the new state and then no frames until playback resumes.
streamTimeSeconds since the visualization stream last started; the frame is computed from the fftSize samples ending at this time. Starting playback, seeking and manual track changes restart it at 0, where it stays for about 200 ms with silence frames; until fftSize / sampleRate seconds have passed, the part of the window before the restart is silence. Natural track transitions do not restart it. The visualization stream runs a little over ten milliseconds behind what is heard (13 ms measured on one setup).
hostTimeHost system time when the frame was computed, Unix epoch milliseconds. Date.now() - frame.hostTime measures delivery latency.

Bin output ​

With output: 'bins' each frame carries the FFT's linear bins without the band processing: no interpolation of narrow low bands, no weighting and no raise of fftSize. How to group, interpolate or smooth them is up to the page.

  • A value is 10 · log10(p) − 2.75 dB, where p is the bin's squared magnitude (stereo) or its mean over the channels (mix), rounded to 0.01 dB with a floor of -160. The powers of the bins a full-scale sine's main lobe covers sum to 0 dB, so adding bins as powers, 10 · log10(Σ 10^(v / 10)), gives the db band value for the same range. The peak bin alone reads lower because foobar2000's FFT uses a Gaussian window, which spreads a sine over neighbouring bins: at 48 kHz and 8192 points a 0 dBFS 1 kHz sine reads -3.18 dB in its peak bin, with six bins within 40 dB of it.
  • Bin k (from 1; the DC bin is never sent) is included when its centre k · sampleRate / fftSize lies in [minFrequency, maxFrequency). Pass a minFrequency below 20 to receive the lowest bins as well.
  • A silence frame keeps the length and firstBin of the subscription's previous frame, filled with -160; before any frame it is empty.
  • For custom window functions, zero-padding, or alternative transforms, compute directly from audio.subscribeStream PCM; see Custom spectrum calculation.

Frames are JSON, about 7 bytes per value. Estimated sizes and pull round trips at 48 kHz over the default range, from JSON answers of the same size measured on one machine:

fftSizechannelsValuesSizeRound trip
4096mix204614 KB0.5 ms
16384mix818556 KB1.1 ms
16384stereo16370112 KB1.9 ms
65536stereo65480448 KB6.6 ms

Most of this time is spent on foobar2000's main thread, so pick the frame rate by size. When one tick of pushed frames takes longer than the frame interval, every subscription skips the next tick.

Sampling Size Limit

fftSize must be a power of 2 between 256 and 65536; other values return an error. The upper limit of 65536 is enforced by the component rather than foobar2000. At 65536, each frame processes approximately 1.5 seconds of PCM, which is intended primarily for scenarios requiring raw bin resolution.

Band output

Band output (output: 'bands') is kept for compatibility and its values do not change. It maps the bins onto log-spaced bands, interpolates bands narrower than one bin from the neighbouring bins, and raises fftSize to at least 4096 or 8192 for 32 or 64 bands; weighted also applies a tilt, a bass shelf below 200 Hz and a display curve. New code should use bin output.

Subscription and Event Semantics

  • subscriptionId is optional: without it, the host derives the subscription key from the calling window and event. The SDK helper fb.audio.subscribeSpectrum() does not expose it.
  • Re-supplying an existing subscriptionId updates/replaces that subscription.
  • The default audio:spectrum event and custom named event payloads match the spectrum frame table above.
js
// Minimal call: all defaults, result delivered on 'audio:spectrum'
const res = await fb2k.invoke('audio.subscribeSpectrum');
if (res.success === false) throw new Error(res.error);
const { subscriptionId } = res;

// Explicit configuration
const custom = await fb2k.invoke('audio.subscribeSpectrum', {
    bands: 64,
    fftSize: 2048,
    fps: 60
});

// Linear bins in dB for the left and right channels
const bins = await fb2k.invoke('audio.subscribeSpectrum', {
    output: 'bins',
    channels: 'stereo',
    fftSize: 16384,
    fps: 60
});

audio.subscribeStream ​

Experimental API; it may change in future releases.

Subscribe to live playback PCM audio. Audio chunks played by foobar2000 are copied into a shared memory ring buffer (one dedicated buffer per subscription). The buffer is delivered via a sharedbufferreceived event on chrome.webview alongside the first audio chunk. When sample rate or channel count changes, a new buffer is allocated (incrementing epoch) and audio:stream announces the previous epoch has ended. No chunks are delivered while playback is stopped or paused.

ParameterTypeRequiredDescription
subscriptionIdstringNoCustom subscription ID; when omitted, the host generates one (pcmstream_N). Re-subscribing with an existing ID from the same page replaces the previous subscription. Must not be empty.
intervalnumberNoCallback interval to request from foobar2000, in seconds. The core rounds it to its own tick of about 16 ms and never goes above 200 ms; one subscription asking for a short interval shortens it for every stream subscription in the component. Defaults to the core's 200 ms. Between 0.01 and 0.2 inclusive.
bufferSecondsnumberNoRing buffer length, in seconds. Reduced when the buffer would exceed 64 MiB. Between 0.1 and 10 inclusive. Default: 1.

Returns

FieldTypeDescription
subscriptionIdstringSubscription ID, as specified by caller or generated by host.
intervalnumberInterval requested from the core; absent when the core's default is used.
bufferSecondsnumberRing buffer length registered, in seconds.

success is true on success. On failure the response is { success: false, error, code }; see Error codes for code.

The samples do not travel through the JSON channel. Once audio plays, the page receives a sharedbufferreceived event on window.chrome.webview: e.getBuffer() is the ring buffer and e.additionalData is { purpose: 'audio.subscribeStream', subscriptionId, epoch, sampleRate, channels, capacityFrames, headerBytes: 64 }. The same event arrives again with epoch + 1 when the sample rate or channel count changes (a track in another format, or a DSP that changes the channel count); one audio:stream event { subscriptionId, type: 'ended', epoch, reason: 'format-change' } then names the epoch that ended, whose buffer also has its ended flag set.

The buffer starts with the same 64-byte header as audio.decodePcm buffers, followed by interleaved float32 frames: the frame in slot s has channel c at byte 64 + (s × channels + c) × 4. In the header, capacityFrames (u32 at byte 24) is the number of slots, writeFrames (u32 at byte 32) counts frames written so far and wraps at 2^32, writeSlot (u32 at byte 60) is the slot the next frame goes to, seq (u32 at byte 28) is odd while the host writes, and bit 0 of flags (u32 at byte 36) is set once the host stops writing. Consumers must follow the sequence lock protocol: retry if seq is odd (host is writing) or if seq changes between the start and end of reading; the newest frame ends at writeSlot. Readers falling behind by more than capacityFrames lose the oldest frames: dropped = max(0, writeFrames − readFrames − capacityFrames). The SDK method fb.audio.subscribeStream encapsulates this protocol and delivers planar Float32Array chunks directly; a page that uses it should not also handle these buffers itself.

  • The samples are taken after the DSP chain and before ReplayGain and the volume control: what the DSP chain outputs, at full scale. Unlike audio:spectrum, they do not include ReplayGain.
  • The buffer is read-only shared memory; do not write to it: writing crashes the page's renderer process, and with it the whole foobar2000 window. Call chrome.webview.releaseBuffer(buffer) when finished with an epoch, including when audio:stream reports an epoch ended and for the final epoch after unsubscribing.
  • Chunks arrive about every 200 ms by default and at most about every 16 ms (interval). The first chunk after playback starts or resumes can hold up to 0.8 s of audio, so a bufferSeconds below that loses frames at every start.
  • While playback is stopped or paused, no new samples are written and writeFrames ceases incrementing. Seeking and track changes within the same format do not reset the buffer.
  • Each page supports up to 8 active stream subscriptions; exceeding this limit fails with OPERATION_FAILED. Subscribing with an existing subscriptionId replaces the previous subscription: its buffer receives the ended flag with no event dispatched, and the subsequent epoch continues counting.
  • Synchronous validation errors are returned in the response: INVALID_PARAMS for out-of-range values or unknown keys, NOT_SUPPORTED when the WebView2 runtime cannot share buffers or the calling page cannot be located, and OPERATION_FAILED when the host is shutting down or the 8-subscription limit is reached.
  • Subscriptions end automatically without emitting events when the page navigates, its window or panel closes, or foobar2000 exits.
js
const sub = await fb2k.invoke('audio.subscribeStream', { subscriptionId: 'meter', bufferSeconds: 0.5 });
if (sub.success === false) throw new Error(sub.error);
window.chrome.webview.addEventListener('sharedbufferreceived', (e) => {
    const info = e.additionalData;
    if (info?.purpose !== 'audio.subscribeStream' || info.subscriptionId !== sub.subscriptionId) return;
    const buffer = e.getBuffer();
    const header = new DataView(buffer);
    console.log(info.epoch, header.getUint32(16, true), header.getUint32(20, true)); // epoch, sampleRate, channels
});

audio.unsubscribeSpectrum ​

Remove spectrum subscriptions of the calling page.

ParameterTypeRequiredDescription
subscriptionIdstringNoSubscription to remove; omit to remove every spectrum subscription of the calling page. Must not be empty.

Returns

FieldTypeDescription
removedintegerSubscriptions removed: 0 or 1 with a subscriptionId, any number without. Subscriptions of other pages are never counted or removed.
subscriptionIdstringThe requested id; empty when none was given.

success is true on success. On failure the response is { success: false, error, code }; see Error codes for code.

js
await fb2k.invoke('audio.unsubscribeSpectrum', { subscriptionId });

audio.unsubscribeStream ​

Experimental API; it may change in future releases.

Remove stream subscriptions of the calling page. Their buffers get the ended flag and are closed on the host side; no event is sent.

ParameterTypeRequiredDescription
subscriptionIdstringNoSubscription to remove; omit to remove every stream subscription of the calling page. Must not be empty.

Returns

FieldTypeDescription
removedintegerSubscriptions removed: 0 or 1 with a subscriptionId, any number without. Subscriptions of other pages are never counted or removed.

success is true on success. On failure the response is { success: false, error, code }; see Error codes for code.

The removed subscriptions' buffers get their ended flag (bit 0 of the u32 at byte 36) set before the host closes its side; the page's views stay readable until the page releases them. Removing the last stream subscription in the component also unregisters the capture callback from foobar2000.

js
await fb2k.invoke('audio.unsubscribeStream', { subscriptionId: 'meter' });

Runtime behavior notes ​

  • Subscription identity & dispatching: audio.subscribeSpectrum creates a spectrum subscription for the calling window, replacing any existing subscription with the same subscriptionId. When subscriptionId is omitted, the key is derived from the calling window and event. Frames arrive under the specified event name (defaulting to audio:spectrum). SDK's fb.audio.subscribeSpectrum() creates the same underlying subscription.
  • Timer scheduling & FFT deduplication: Subscriptions maintain independent parameters. A dedicated host timer thread ticks at the highest requested fps using high-resolution timers; each subscription delivers a frame only when its own interval elapses, so pushed frames arrive close to the requested rate. Subscriptions sharing the same effective FFT size, band count, scale, and frequency range share a single FFT calculation per tick. Host main-thread dispatch overhead scales with frame count (~0.5 ms per frame).
  • Signal tap point: The scale: 'db' mode measures the signal after ReplayGain and DSP processing but before output volume attenuation; adjusting player volume does not alter spectrum readings.
  • Visualization stream dependency: audio.getSpectrum and audio.getWaveform query the real-time visualization stream. If no spectrum subscriptions exist, both calls fail with OPERATION_FAILED. If playback is active but stream buffers have not yet accumulated data, audio.getSpectrum also fails; during pause or stop, it returns a silent frame.
  • Full waveform lifecycle: audio.generateFullWaveform immediately returns status: "ready" with cached data on cache hit, or status: "pending" with a taskId on cache miss. The caller subsequently receives audio:fullWaveformReady or audio:fullWaveformFailed. An explicit cueIndex parameter takes precedence over a path|subsong:N suffix.