@soundtouchjs/audio-worklet
Version:
AudioWorklet implementation of the SoundTouchJS library
1,438 lines • 72.9 kB
JavaScript
//#region ../core/src/AbstractSamplePipe.ts
/**
* Abstract base class for sample processing pipes.
*
* @remarks
* Manages input and output buffers for audio sample processing chains. Subclasses should implement
* specific processing logic for audio transformation or analysis. This class is not intended to be used
* directly, but as a base for concrete audio processing stages.
*
* @typeParam TInputBuffer - Concrete input buffer type (defaults to the generic `SampleBuffer` contract).
* @typeParam TOutputBuffer - Concrete output buffer type (defaults to `TInputBuffer` so input/output share the same buffer type unless a subclass opts into different types).
*/
var AbstractSamplePipe = class {
/**
* Input buffer for audio samples.
*/
_inputBuffer;
/**
* Output buffer for processed audio samples.
*/
_outputBuffer;
/**
* Constructs an AbstractSamplePipe.
* @param options Constructor options.
*
* @remarks
* When `createBuffers` is true, both factories are required so subclasses can
* control exact buffer implementations without unsafe casting.
*/
constructor({ createBuffers = false, inputBufferFactory, outputBufferFactory } = {}) {
if (createBuffers) {
if (!inputBufferFactory || !outputBufferFactory) throw new Error("buffer factories are required when createBuffers is true");
this._inputBuffer = inputBufferFactory();
this._outputBuffer = outputBufferFactory();
} else {
this._inputBuffer = null;
this._outputBuffer = null;
}
}
/**
* Gets the input buffer.
* @returns The current input buffer instance, or null if not set.
*/
get inputBuffer() {
return this._inputBuffer;
}
/**
* Sets the input buffer.
* @param inputBuffer The new input buffer instance, or null to unset.
*/
set inputBuffer(inputBuffer) {
this._inputBuffer = inputBuffer;
}
/**
* Gets the output buffer.
* @returns The current output buffer instance, or null if not set.
*/
get outputBuffer() {
return this._outputBuffer;
}
/**
* Sets the output buffer.
* @param outputBuffer The new output buffer instance, or null to unset.
*/
set outputBuffer(outputBuffer) {
this._outputBuffer = outputBuffer;
}
/**
* Clears both input and output buffers.
*
* @remarks
* Resets the state of both input and output buffers, if present, by calling their `clear()` methods.
*/
clear() {
this._inputBuffer?.clear();
this._outputBuffer?.clear();
}
};
//#endregion
//#region ../core/src/CircularSampleBuffer.ts
var SAMPLES_PER_FRAME$1 = 2;
/**
* Circular frame buffer for interleaved stereo audio samples.
*
* @remarks
* Implements a ring buffer for stereo audio, where each frame consists of two contiguous float values (left, right).
* Maintains a movable read cursor and appends at the logical end. Capacity grows automatically as needed while preserving frame order.
* Used for efficient, low-latency audio processing where buffer wraparound and dynamic resizing are required.
*/
var CircularSampleBuffer = class {
_buffer;
_capacityFrames;
_readFrame;
_frameCount;
/**
* @param capacityFrames Initial frame capacity before automatic growth.
*/
constructor(capacityFrames = 2048) {
const normalizedCapacity = Math.max(1, Math.floor(capacityFrames));
this._capacityFrames = normalizedCapacity;
this._buffer = new Float32Array(normalizedCapacity * SAMPLES_PER_FRAME$1);
this._readFrame = 0;
this._frameCount = 0;
}
/**
* Allocated capacity expressed in frames.
* @returns The number of frames the buffer can currently hold without resizing.
*/
get capacityFrames() {
return this._capacityFrames;
}
/**
* Number of buffered frames currently readable.
* @returns The number of frames available for reading.
*/
get frameCount() {
return this._frameCount;
}
/**
* Clears the buffer without shrinking allocated capacity.
*
* @remarks
* Resets the read cursor and frame count, but does not deallocate the underlying storage.
*/
clear() {
this._readFrame = 0;
this._frameCount = 0;
}
/**
* Ensures the internal storage can hold at least `minCapacityFrames`.
*
* @param minCapacityFrames Minimum frame capacity required.
* @remarks
* Grows the buffer if needed, preserving all readable frames in order.
*/
ensureCapacity(minCapacityFrames) {
const normalizedMinCapacityFrames = Math.max(0, Math.floor(minCapacityFrames));
if (normalizedMinCapacityFrames <= this._capacityFrames) return;
const nextCapacity = Math.max(normalizedMinCapacityFrames, this._capacityFrames * 2, this._capacityFrames + 1024);
const nextBuffer = new Float32Array(nextCapacity * SAMPLES_PER_FRAME$1);
for (let frame = 0; frame < this._frameCount; frame += 1) {
const sourceIndex = (this._readFrame + frame) % this._capacityFrames * SAMPLES_PER_FRAME$1;
const destIndex = frame * SAMPLES_PER_FRAME$1;
nextBuffer[destIndex] = this._buffer[sourceIndex];
nextBuffer[destIndex + 1] = this._buffer[sourceIndex + 1];
}
this._buffer = nextBuffer;
this._capacityFrames = nextCapacity;
this._readFrame = 0;
}
/**
* Appends source frames to the end of the ring.
*
* @param source Interleaved stereo source samples.
* @param sourceFrameOffset Source offset in frames.
* @param frameCount Number of frames to append; defaults to all complete remaining frames.
* @remarks
* Automatically grows the buffer if needed. Only complete frames are appended.
*/
pushSamples(source, sourceFrameOffset = 0, frameCount = 0) {
const sourceStartSample = Math.max(0, Math.floor(sourceFrameOffset)) * SAMPLES_PER_FRAME$1;
const availableFrames = Math.max(0, Math.floor((source.length - sourceStartSample) / SAMPLES_PER_FRAME$1));
const requestedFrames = frameCount > 0 ? Math.floor(frameCount) : 0;
const framesToWrite = requestedFrames > 0 ? Math.min(requestedFrames, availableFrames) : availableFrames;
if (framesToWrite <= 0) return;
this.ensureCapacity(this._frameCount + framesToWrite);
const writeFrame = (this._readFrame + this._frameCount) % this._capacityFrames;
for (let frame = 0; frame < framesToWrite; frame += 1) {
const sourceIndex = sourceStartSample + frame * SAMPLES_PER_FRAME$1;
const destIndex = (writeFrame + frame) % this._capacityFrames * SAMPLES_PER_FRAME$1;
this._buffer[destIndex] = source[sourceIndex];
this._buffer[destIndex + 1] = source[sourceIndex + 1];
}
this._frameCount += framesToWrite;
}
/**
* Contract alias for `pushSamples`.
*
* @param source Interleaved stereo source samples.
* @param sourceFrameOffset Source offset in frames.
* @param frameCount Number of frames to append.
*/
putSamples(source, sourceFrameOffset = 0, frameCount = 0) {
this.pushSamples(source, sourceFrameOffset, frameCount);
}
/**
* Extracts frames from the ring into `target`.
*
* @param target Destination array for interleaved stereo samples.
* @param sourceFrameOffset Read offset in frames.
* @param frameCount Number of frames requested.
* @param consume When true, consumed frames are dropped from the front.
* @returns Number of frames copied.
* @remarks
* If `consume` is true, the extracted frames are removed from the buffer.
*/
extract(target, sourceFrameOffset = 0, frameCount = 0, consume = false) {
const normalizedSourceFrameOffset = Math.max(0, Math.floor(sourceFrameOffset));
const requestedFrames = frameCount > 0 ? Math.floor(frameCount) : 0;
const framesAvailable = Math.max(0, this._frameCount - normalizedSourceFrameOffset);
const framesToRead = requestedFrames > 0 ? Math.min(requestedFrames, framesAvailable) : framesAvailable;
if (framesToRead <= 0) return 0;
for (let frame = 0; frame < framesToRead; frame += 1) {
const sourceIndex = (this._readFrame + normalizedSourceFrameOffset + frame) % this._capacityFrames * SAMPLES_PER_FRAME$1;
const targetIndex = frame * SAMPLES_PER_FRAME$1;
target[targetIndex] = this._buffer[sourceIndex];
target[targetIndex + 1] = this._buffer[sourceIndex + 1];
}
if (consume) {
const framesToDrop = normalizedSourceFrameOffset + framesToRead;
this.dropFrames(framesToDrop);
}
return framesToRead;
}
/**
* Reads a single sample value by logical sample index.
*
* @param sampleIndex Logical sample index relative to the readable head.
* @returns Sample value, or `0` when the index falls outside readable data.
* @remarks
* Used for random access to individual samples within the readable region.
*/
readSample(sampleIndex) {
const normalizedSampleIndex = Math.max(0, Math.floor(sampleIndex));
const frameOffset = Math.floor(normalizedSampleIndex / SAMPLES_PER_FRAME$1);
if (frameOffset >= this._frameCount) return 0;
const channelOffset = normalizedSampleIndex % SAMPLES_PER_FRAME$1;
const sourceIndex = (this._readFrame + frameOffset) % this._capacityFrames * SAMPLES_PER_FRAME$1 + channelOffset;
return this._buffer[sourceIndex] ?? 0;
}
/**
* Drops frames from the front of the ring.
*
* @param frameCount Maximum number of frames to remove.
* @returns Number of frames removed.
* @remarks
* Advances the read cursor and reduces the frame count. If all frames are dropped, resets the read cursor.
*/
dropFrames(frameCount) {
const framesToDrop = Math.max(0, Math.min(Math.max(0, Math.floor(frameCount)), this._frameCount));
if (framesToDrop === 0) return 0;
this._readFrame = (this._readFrame + framesToDrop) % this._capacityFrames;
this._frameCount -= framesToDrop;
if (this._frameCount === 0) this._readFrame = 0;
return framesToDrop;
}
/**
* Contract alias for `dropFrames`.
*
* @param frameCount Number of frames to consume.
*/
receive(frameCount = this._frameCount) {
this.dropFrames(frameCount);
}
};
//#endregion
//#region ../core/src/FifoSampleBuffer.ts
/**
* Number of bytes per sample (Float32).
*/
var BYTES_PER_SAMPLE = 4;
/**
* Number of samples per audio frame (stereo).
*/
var SAMPLES_PER_FRAME = 2;
/**
* Number of bytes per audio frame.
*/
var BYTES_PER_FRAME = BYTES_PER_SAMPLE * SAMPLES_PER_FRAME;
/**
* Default maximum number of frames for buffer allocation.
*/
var DEFAULT_MAX_FRAMES = 131072;
/**
* Resizable interleaved sample buffer for audio processing.
*
* @remarks
* Stores stereo audio samples in a contiguous Float32Array and provides methods for efficient buffer management and sample transfer.
* Uses ES2024 ArrayBuffer for zero-allocation growth. Suitable for scenarios where buffer size may need to grow dynamically during audio processing.
*/
var FifoSampleBuffer = class {
/**
* Backing ArrayBuffer for sample storage.
* @remarks
* Underlying memory for the buffer, which may be resized as needed.
*/
_buffer;
/**
* Float32Array view of the buffer.
* @remarks
* Provides direct access to the sample data for reading and writing.
*/
_vector;
/**
* Current read position (frame index).
* @remarks
* Indicates the logical start of readable data within the buffer.
*/
_position;
/**
* Number of frames currently stored.
* @remarks
* Represents the number of complete stereo frames available for reading.
*/
_frameCount;
/**
* Creates a new FifoSampleBuffer.
* @param maxFrames Maximum number of frames for buffer allocation.
*/
constructor(maxFrames = DEFAULT_MAX_FRAMES) {
this._buffer = new ArrayBuffer(0, { maxByteLength: maxFrames * BYTES_PER_FRAME });
this._vector = new Float32Array(this._buffer);
this._position = 0;
this._frameCount = 0;
}
/**
* Returns the Float32Array view of the buffer.
* @returns The Float32Array containing the sample data.
*/
get vector() {
return this._vector;
}
/**
* Returns the current read position (frame index).
* @returns The current frame index for reading.
*/
get position() {
return this._position;
}
/**
* Returns the start sample index for reading.
* @returns The sample index corresponding to the start of readable data.
*/
get startIndex() {
return this._position * 2;
}
/**
* Returns the number of frames currently stored.
* @returns The number of complete frames available for reading.
*/
get frameCount() {
return this._frameCount;
}
/**
* Returns the end sample index for reading.
* @returns The sample index corresponding to the end of readable data.
*/
get endIndex() {
return (this._position + this._frameCount) * 2;
}
/**
* Clears the buffer and resets position and frame count.
* @remarks
* Fills the buffer with zeros and resets all internal state.
*/
clear() {
this._vector.fill(0);
this._position = 0;
this._frameCount = 0;
}
/**
* Adds empty frames to the buffer.
* @param numFrames Number of frames to add.
*/
put(numFrames) {
this._frameCount += numFrames;
}
/**
* Adds samples to the buffer from a Float32Array.
* @param samples Source samples (interleaved stereo).
* @param position Start frame index in source.
* @param numFrames Number of frames to copy (default: all available).
* @remarks
* Automatically grows the buffer if needed. Only complete frames are appended.
*/
putSamples(samples, position = 0, numFrames = 0) {
const sourceOffset = position * 2;
if (!(numFrames >= 0) || numFrames === 0) numFrames = (samples.length - sourceOffset) / 2;
const numSamples = numFrames * 2;
this.ensureCapacity(numFrames + this._frameCount);
const destOffset = this.endIndex;
this._vector.set(samples.subarray(sourceOffset, sourceOffset + numSamples), destOffset);
this._frameCount += numFrames;
}
/**
* Adds samples from another FifoSampleBuffer.
* @param buffer Source buffer.
* @param position Start frame index in source buffer.
* @param numFrames Number of frames to copy (default: all available).
*/
putBuffer(buffer, position = 0, numFrames = 0) {
if (!(numFrames >= 0) || numFrames === 0) numFrames = buffer.frameCount - position;
this.putSamples(buffer.vector, buffer.position + position, numFrames);
}
/**
* Advances the read position and reduces frame count.
* @param numFrames Number of frames to receive (default: all available).
* @remarks
* Consumed frames are no longer available for reading.
*/
receive(numFrames) {
if (numFrames === void 0 || !(numFrames >= 0) || numFrames > this._frameCount) numFrames = this._frameCount;
this._frameCount -= numFrames;
this._position += numFrames;
}
/**
* Copies and receives samples into an output array.
* @param output Destination Float32Array.
* @param numFrames Number of frames to copy and receive.
* @remarks
* Advances the read position after copying.
*/
receiveSamples(output, numFrames = 0) {
const numSamples = numFrames * 2;
const sourceOffset = this.startIndex;
output.set(this._vector.subarray(sourceOffset, sourceOffset + numSamples));
this.receive(numFrames);
}
/**
* Extracts samples into an output array without advancing position.
* @param output Destination Float32Array.
* @param position Start frame index in buffer.
* @param numFrames Number of frames to extract.
*/
extract(output, position = 0, numFrames = 0) {
const sourceOffset = this.startIndex + position * 2;
const numSamples = numFrames * 2;
output.set(this._vector.subarray(sourceOffset, sourceOffset + numSamples));
}
/**
* Ensures the buffer has capacity for at least numFrames.
* @param numFrames Minimum number of frames required.
* @remarks
* Grows the buffer if needed, preserving all readable frames in order.
*/
ensureCapacity(numFrames = 0) {
const minLength = Math.floor(numFrames * SAMPLES_PER_FRAME);
if (this._vector.length < minLength) {
const newByteLength = minLength * BYTES_PER_SAMPLE;
if (newByteLength <= this._buffer.maxByteLength) {
this.rewind();
this._buffer.resize(newByteLength);
this._vector = new Float32Array(this._buffer);
} else {
const newMaxBytes = newByteLength * 2;
const newBuffer = new ArrayBuffer(newByteLength, { maxByteLength: newMaxBytes });
const newVector = new Float32Array(newBuffer);
newVector.set(this._vector.subarray(this.startIndex, this.endIndex));
this._buffer = newBuffer;
this._vector = newVector;
this._position = 0;
}
} else this.rewind();
}
/**
* Ensures buffer has capacity for additional frames.
* @param numFrames Number of additional frames required.
*/
ensureAdditionalCapacity(numFrames = 0) {
this.ensureCapacity(this._frameCount + numFrames);
}
/**
* Moves all unread samples to the start of the buffer.
* @remarks
* Compacts the buffer so that all unread samples are at the beginning, freeing space for new data.
*/
rewind() {
if (this._position > 0) {
this._vector.set(this._vector.subarray(this.startIndex, this.endIndex));
this._position = 0;
}
}
};
//#endregion
//#region ../core/src/SampleBufferAdapter.ts
var FifoSampleBufferAdapter = class {
inputBuffer;
constructor() {
this.inputBuffer = null;
}
get frameCount() {
return this.inputBuffer?.frameCount ?? 0;
}
clear() {
this.inputBuffer = null;
}
syncFromInputBuffer(inputBuffer) {
this.inputBuffer = inputBuffer;
}
extract(target, sourceFrameOffset, frameCount) {
const buffer = this.inputBuffer;
if (buffer === null) return 0;
const availableFrames = Math.max(0, buffer.frameCount - sourceFrameOffset);
const framesToExtract = Math.max(0, Math.min(frameCount, availableFrames));
if (framesToExtract === 0) return 0;
buffer.extract(target, sourceFrameOffset, framesToExtract);
return framesToExtract;
}
receive(frameCount) {
this.inputBuffer?.receive(frameCount);
}
};
var CircularSampleBufferAdapter = class {
circularBuffer;
scratch;
constructor() {
this.circularBuffer = new CircularSampleBuffer();
this.scratch = new Float32Array(0);
}
get frameCount() {
return this.circularBuffer.frameCount;
}
clear() {
this.circularBuffer.clear();
}
syncFromInputBuffer(inputBuffer) {
if (inputBuffer instanceof FifoSampleBuffer) {
const frames = inputBuffer.frameCount;
if (frames === 0) return;
this.circularBuffer.pushSamples(inputBuffer.vector, inputBuffer.position, frames);
inputBuffer.receive(frames);
return;
}
const frames = inputBuffer.frameCount;
if (frames === 0) return;
const sampleCount = frames * 2;
if (this.scratch.length < sampleCount) this.scratch = new Float32Array(sampleCount);
inputBuffer.extract(this.scratch, 0, frames);
this.circularBuffer.pushSamples(this.scratch, 0, frames);
inputBuffer.receive(frames);
}
extract(target, sourceFrameOffset, frameCount) {
return this.circularBuffer.extract(target, sourceFrameOffset, frameCount, false);
}
receive(frameCount) {
this.circularBuffer.dropFrames(frameCount);
}
};
/** Creates an adapter that reads directly from any `SampleBuffer` contract. */
var createFifoSampleBufferAdapter = () => new FifoSampleBufferAdapter();
/**
* Creates an adapter that stages source frames in a circular buffer for
* efficient repeated reads.
*/
var createCircularSampleBufferAdapter = () => new CircularSampleBufferAdapter();
//#endregion
//#region ../interpolation-strategy-lanczos/.dist/index.js
var LANCZOS_DEFAULT_PARAMS = {
zeroCrossings: 4,
normalize: false
};
function normalizeLanczosParams(params, defaults) {
const merged = {
...defaults,
...params ?? {}
};
return {
zeroCrossings: Math.max(2, Math.min(8, Math.round(Number(merged["zeroCrossings"] ?? defaults["zeroCrossings"] ?? 4)))),
normalize: Boolean(merged["normalize"])
};
}
function applyLanczosParams(state, params) {
if (typeof state !== "object" || state === null) return;
const record = state;
record.params = {
zeroCrossings: Math.max(2, Math.round(Number(params["zeroCrossings"] ?? 4))),
normalize: Boolean(params["normalize"])
};
}
function readFrameSample(src, srcOffset, numFrames, frameIndex, channel, state) {
if (frameIndex < 0) return channel === 0 ? state.prevSampleL : state.prevSampleR;
if (frameIndex >= numFrames) return src[srcOffset + 2 * (numFrames - 1) + channel];
return src[srcOffset + 2 * frameIndex + channel];
}
function normalizedSinc(x) {
if (x === 0) return 1;
const value = Math.PI * x;
return Math.sin(value) / value;
}
function lanczosWeight(distance, radius) {
if (Math.abs(distance) >= radius) return 0;
return normalizedSinc(distance) * normalizedSinc(distance / radius);
}
var lanczosKernel = (src, srcOffset, numFrames, position, channel, state) => {
const kernelState = state;
const radius = kernelState.params.zeroCrossings;
const normalize = Boolean(kernelState.params.normalize);
const center = Math.floor(position);
const start = center - (radius - 1);
const end = center + radius;
let numerator = 0;
let denominator = 0;
for (let sampleIndex = start; sampleIndex <= end; sampleIndex += 1) {
const weight = lanczosWeight(position - sampleIndex, radius);
numerator += readFrameSample(src, srcOffset, numFrames, sampleIndex, channel, kernelState) * weight;
denominator += weight;
}
if (Math.abs(denominator) < 1e-12) return readFrameSample(src, srcOffset, numFrames, Math.round(position), channel, kernelState);
return normalize ? numerator / denominator : numerator / (denominator || 1);
};
lanczosKernel.createState = () => ({
prevSampleL: 0,
prevSampleR: 0,
params: { ...LANCZOS_DEFAULT_PARAMS }
});
/** Default Lanczos strategy registration payload. */
var lanczosStrategy = {
id: "lanczos",
baseStrategy: "linear",
kernel: lanczosKernel,
defaultParams: LANCZOS_DEFAULT_PARAMS,
normalizeParams: normalizeLanczosParams,
applyParams: applyLanczosParams
};
//#endregion
//#region ../core/src/interpolationStrategyRegistry.ts
var strategyRegistry = /* @__PURE__ */ new Map();
var activeStrategyId = "lanczos";
function readStrategySelection(strategy) {
if (typeof strategy === "string") return { id: strategy };
if (strategy !== void 0) return strategy;
return { id: activeStrategyId };
}
function readStrategyId(strategy) {
return readStrategySelection(strategy).id;
}
function requireRegisteredStrategy(strategyId) {
const registered = strategyRegistry.get(strategyId);
if (registered !== void 0) return registered;
throw new Error(`Unknown interpolation strategy id "${strategyId}". Register it before use.`);
}
function registerBuiltInInterpolationStrategy(registration) {
const baseStrategy = registration.baseStrategy ?? "lanczos";
strategyRegistry.set(registration.id, {
id: registration.id,
baseStrategy,
builtIn: true,
kernel: registration.kernel,
defaultParams: { ...registration.defaultParams ?? {} },
normalizeParams: registration.normalizeParams,
applyParams: registration.applyParams
});
}
function resolveKernelRegistration(registration, visited = /* @__PURE__ */ new Set()) {
if (registration.kernel !== void 0) return registration;
if (visited.has(registration.id)) throw new Error(`Interpolation strategy resolution cycle detected at "${registration.id}".`);
visited.add(registration.id);
return resolveKernelRegistration(requireRegisteredStrategy(registration.baseStrategy), visited);
}
function normalizeParams(registration, params) {
const defaults = registration.defaultParams;
if (registration.normalizeParams !== void 0) return registration.normalizeParams(params, defaults);
const normalized = { ...defaults };
if (params !== void 0) {
for (const [key, value] of Object.entries(params)) if (value !== void 0) normalized[key] = value;
}
return normalized;
}
/**
* Resolves a strategy to either a built-in base id or a plugin kernel.
*
* @throws Error when the strategy id is unknown.
*/
function resolveInterpolationStrategy(strategy) {
const registered = requireRegisteredStrategy(readStrategyId(strategy));
if ("kernel" in registered && registered.kernel) return registered.kernel;
return registered.baseStrategy;
}
/**
* Resolves runtime strategy state (kernel + normalized params + applier hook).
*/
function resolveInterpolationStrategyRuntime(strategy) {
const selection = readStrategySelection(strategy);
const registered = requireRegisteredStrategy(selection.id);
const kernelRegistration = resolveKernelRegistration(registered);
const kernel = kernelRegistration.kernel;
if (kernel === void 0) throw new Error(`Interpolation strategy "${selection.id}" did not resolve to a kernel.`);
const params = normalizeParams(registered, selection.params);
return {
id: registered.id,
kernel,
params,
applyParams: registered.applyParams ?? kernelRegistration.applyParams
};
}
registerBuiltInInterpolationStrategy({
...lanczosStrategy,
baseStrategy: "lanczos"
});
//#endregion
//#region ../core/src/RateTransposer.ts
/**
* Sample rate transposer for pitch and tempo manipulation.
*
* @remarks
* Used internally by SoundTouch for rate-based processing. Applies interpolation strategies to resample audio at different rates, supporting real-time pitch and tempo changes.
*/
var RateTransposer = class RateTransposer extends AbstractSamplePipe {
/**
* Current rate factor for transposition.
*/
_rate;
/**
* Source position (in frames) for the next output sample, relative to the
* current processing block where 0 is the first frame and -1 is prevSample.
*/
fractionalPosition;
/**
* Previous left channel sample for interpolation.
*/
previousLeftSample;
/**
* Previous right channel sample for interpolation.
*/
previousRightSample;
/** Scratch space used for extracted input samples. */
inputScratch;
/** Scratch space used for generated output samples. */
outputScratch;
/** Factory used when cloning or initializing adapter strategy. */
sampleBufferAdapterFactory;
/** Factory used to construct input/output chain buffers. */
sampleBufferFactory;
/** Adapter that normalizes reads from the bound input buffer. */
inputAdapter;
/** Selected interpolation strategy for transposition. */
interpolationStrategy;
/** Resolved kernel used by this transposer instance. */
resolvedInterpolationKernel;
/** Optional per-instance state for plugin kernels. */
kernelState;
/** Normalized params for the selected interpolation strategy. */
interpolationStrategyParams;
/** Optional params application hook from the strategy registration. */
applyKernelParams;
/**
* Creates a RateTransposer instance.
* @param options Constructor options.
* @remarks
* Accepts factories for buffer and adapter creation, and allows specifying the interpolation strategy.
*/
constructor({ createBuffers = false, sampleBufferAdapterFactory = createCircularSampleBufferAdapter, sampleBufferFactory = () => new CircularSampleBuffer(), interpolationStrategy } = {}) {
super({
createBuffers,
inputBufferFactory: sampleBufferFactory,
outputBufferFactory: sampleBufferFactory
});
this.fractionalPosition = -1;
this.previousLeftSample = 0;
this.previousRightSample = 0;
this._rate = 1;
this.inputScratch = new Float32Array(0);
this.outputScratch = new Float32Array(0);
this.sampleBufferAdapterFactory = sampleBufferAdapterFactory;
this.sampleBufferFactory = sampleBufferFactory;
this.inputAdapter = sampleBufferAdapterFactory();
this.interpolationStrategy = "lanczos";
this.resolvedInterpolationKernel = () => 0;
this.kernelState = void 0;
this.interpolationStrategyParams = {};
this.applyKernelParams = void 0;
this.setInterpolationStrategy(interpolationStrategy ?? "lanczos");
}
/**
* Sets the rate factor for transposition.
* @param rate Rate factor.
*/
set rate(rate) {
this._rate = rate;
}
/**
* Active interpolation strategy.
* @returns The current interpolation strategy identifier.
*/
get strategy() {
return this.interpolationStrategy;
}
/**
* Active interpolation strategy params.
* @returns The current interpolation strategy parameters.
*/
get strategyParams() {
return { ...this.interpolationStrategyParams };
}
/**
* Switches interpolation strategy at runtime.
* @param strategy The new interpolation strategy to use.
*/
setInterpolationStrategy(strategy) {
const resolved = resolveInterpolationStrategyRuntime(strategy);
this.interpolationStrategy = resolved.id;
this.resolvedInterpolationKernel = resolved.kernel;
this.interpolationStrategyParams = { ...resolved.params };
this.applyKernelParams = resolved.applyParams;
if ("createState" in this.resolvedInterpolationKernel && typeof this.resolvedInterpolationKernel.createState === "function") this.kernelState = this.resolvedInterpolationKernel.createState();
else this.kernelState = void 0;
if (this.applyKernelParams !== void 0) this.applyKernelParams(this.kernelState, this.interpolationStrategyParams);
this.reset();
}
/**
* Applies a partial params update to the current interpolation strategy.
* @param params Partial set of parameters to update.
*/
setInterpolationStrategyParams(params) {
const nextParams = { ...this.interpolationStrategyParams };
for (const [key, value] of Object.entries(params)) if (value !== void 0) nextParams[key] = value;
this.interpolationStrategyParams = nextParams;
if (this.applyKernelParams !== void 0) this.applyKernelParams(this.kernelState, this.interpolationStrategyParams);
}
/**
* Resets internal state for interpolation.
* @remarks
* Clears previous sample values and resets the fractional position for output generation.
*/
reset() {
this.fractionalPosition = -1;
this.previousLeftSample = 0;
this.previousRightSample = 0;
}
/**
* Clears buffers and resets internal state.
* @remarks
* Calls clear on all internal buffers and resets interpolation state.
*/
clear() {
super.clear();
this.inputAdapter.clear();
this.reset();
}
/**
* Creates a clone of this RateTransposer with the same rate.
* @returns Cloned RateTransposer instance.
*/
clone() {
const result = new RateTransposer({
createBuffers: false,
sampleBufferAdapterFactory: this.sampleBufferAdapterFactory,
sampleBufferFactory: this.sampleBufferFactory,
interpolationStrategy: {
id: this.interpolationStrategy,
params: this.interpolationStrategyParams
}
});
result.rate = this._rate;
return result;
}
/**
* Processes input buffer and writes transposed samples to output buffer.
* @remarks
* Reads frames from the input buffer, applies rate transposition, and writes to the output buffer.
*/
process() {
if (this._inputBuffer === null || this._outputBuffer === null) return;
this.inputAdapter.syncFromInputBuffer(this._inputBuffer);
const numFrames = this.inputAdapter.frameCount;
if (numFrames === 0) return;
const numFramesOutput = this.transpose(numFrames);
this.inputAdapter.receive(numFrames);
if (numFramesOutput > 0) this._outputBuffer.putSamples(this.outputScratch, 0, numFramesOutput);
}
/**
* Ensures temporary scratch arrays are large enough for the current frame request and estimated output size.
*
* @param numInputFrames Number of input frames that will be processed.
* @remarks
* Allocates or resizes scratch arrays as needed for efficient processing.
*/
ensureScratchCapacity(numInputFrames) {
const inputSamples = numInputFrames * 2;
if (this.inputScratch.length < inputSamples) this.inputScratch = new Float32Array(inputSamples);
const estimatedOutputFrames = Math.ceil(numInputFrames / this._rate) + 2;
const outputSamples = Math.max(0, estimatedOutputFrames) * 2;
if (this.outputScratch.length < outputSamples) this.outputScratch = new Float32Array(outputSamples);
}
/**
* Transposes input samples by the current rate.
* @param numFrames Number of input frames to transpose.
* @returns Number of output frames written.
* @remarks
* Applies the selected interpolation kernel to generate output samples at the new rate.
*/
transpose(numFrames = 0) {
if (this._inputBuffer !== null) {
this.inputAdapter.syncFromInputBuffer(this._inputBuffer);
if (numFrames === 0) numFrames = this.inputAdapter.frameCount;
}
if (numFrames === 0) return 0;
this.ensureScratchCapacity(numFrames);
const src = this.inputScratch;
const extractedFrames = this.inputAdapter.extract(src, 0, numFrames);
if (extractedFrames === 0) return 0;
numFrames = extractedFrames;
return this.transposePluginKernel(numFrames);
}
/**
* Handles transposition using a plugin kernel.
* @remarks
* Invokes the selected interpolation kernel for each output sample.
*/
transposePluginKernel(numFrames) {
const src = this.inputScratch;
const dest = this.outputScratch;
const srcOffset = 0;
const destOffset = 0;
const kernel = this.resolvedInterpolationKernel;
const state = this.kernelState;
const stateRecord = this.getKernelStateRecord(state);
if (stateRecord !== void 0) {
stateRecord.prevSampleL = this.previousLeftSample;
stateRecord.prevSampleR = this.previousRightSample;
}
let i = 0;
let position = this.fractionalPosition;
const maxPosition = numFrames - 1;
while (position <= maxPosition) {
dest[destOffset + 2 * i] = kernel(src, srcOffset, numFrames, position, 0, state);
dest[destOffset + 2 * i + 1] = kernel(src, srcOffset, numFrames, position, 1, state);
i = i + 1;
position += this._rate;
}
this.fractionalPosition = position - numFrames;
this.previousLeftSample = src[srcOffset + 2 * numFrames - 2];
this.previousRightSample = src[srcOffset + 2 * numFrames - 1];
if (stateRecord !== void 0) {
stateRecord.prevSampleL = this.previousLeftSample;
stateRecord.prevSampleR = this.previousRightSample;
}
return i;
}
/**
* Returns the kernel state record if available.
* @param state The kernel state object.
* @returns The state record with previous sample values, or undefined if not present.
*/
getKernelStateRecord(state) {
if (typeof state !== "object" || state === null) return;
const record = state;
const prevSampleL = record["prevSampleL"];
const prevSampleR = record["prevSampleR"];
if (typeof prevSampleL === "number" && typeof prevSampleR === "number") return record;
}
};
//#endregion
//#region ../core/src/Stretch.ts
/**
* Read adapter optimized for FIFO-backed buffers with a generic fallback path.
*/
var FifoStretchBufferAdapter = class {
buffer;
fallbackBuffer;
fallbackScratch;
constructor() {
this.buffer = null;
this.fallbackBuffer = new FifoSampleBuffer();
this.fallbackScratch = new Float32Array(0);
}
/**
* @param buffer Source buffer to expose through FIFO-style reads.
*/
setBuffer(buffer) {
if (buffer instanceof FifoSampleBuffer) {
this.buffer = buffer;
return;
}
const frameCount = buffer.frameCount;
if (frameCount > 0) {
const sampleCount = frameCount * 2;
if (this.fallbackScratch.length < sampleCount) this.fallbackScratch = new Float32Array(sampleCount);
buffer.extract(this.fallbackScratch, 0, frameCount);
this.fallbackBuffer.clear();
this.fallbackBuffer.putSamples(this.fallbackScratch, 0, frameCount);
buffer.receive(frameCount);
} else this.fallbackBuffer.clear();
this.buffer = this.fallbackBuffer;
}
/**
* Returns the currently bound FIFO buffer.
* @throws Error when `setBuffer` has not been called yet.
*/
getBoundBuffer() {
if (this.buffer === null) throw new Error("buffer is not set");
return this.buffer;
}
get frameCount() {
return this.getBoundBuffer().frameCount;
}
get startIndex() {
return this.getBoundBuffer().startIndex;
}
readSample(sampleIndex) {
const boundBuffer = this.getBoundBuffer();
const start = boundBuffer.startIndex;
const end = start + boundBuffer.frameCount * 2;
if (sampleIndex < start || sampleIndex >= end) return 0;
return boundBuffer.vector[sampleIndex];
}
readSubarray(start, end) {
return this.getBoundBuffer().vector.subarray(start, end);
}
receive(numFrames) {
this.getBoundBuffer().receive(numFrames);
}
receiveSamples(output, numFrames) {
this.getBoundBuffer().receiveSamples(output, numFrames);
}
};
var GenericStretchWriteBufferAdapter = class {
buffer;
constructor() {
this.buffer = null;
}
setOutputBuffer(buffer) {
this.buffer = buffer;
}
/**
* Returns the currently bound output buffer.
* @throws Error when `setOutputBuffer` has not been called.
*/
getBoundBuffer() {
if (this.buffer === null) throw new Error("output buffer is not set");
return this.buffer;
}
appendSamples(samples, numFrames) {
this.getBoundBuffer().putSamples(samples, 0, numFrames);
}
putFrom(source, position, numFrames) {
const sourceStart = source.startIndex + position * 2;
const sourceEnd = sourceStart + numFrames * 2;
const chunk = source.readSubarray(sourceStart, sourceEnd);
this.getBoundBuffer().putSamples(chunk, 0, numFrames);
}
};
var CircularStretchInputBufferAdapter = class {
circularBuffer;
rangeScratch;
constructor() {
this.circularBuffer = new CircularSampleBuffer();
this.rangeScratch = new Float32Array(0);
}
/**
* Binds a source buffer and stages its readable frames into the internal
* circular storage.
*
* @param buffer Source buffer to import.
*/
setBuffer(buffer) {
if (buffer instanceof FifoSampleBuffer) {
const frames = buffer.frameCount;
if (frames > 0) {
this.circularBuffer.pushSamples(buffer.vector, buffer.position, frames);
buffer.receive(frames);
}
return;
}
const frames = buffer.frameCount;
if (frames > 0) {
const sampleCount = frames * 2;
if (this.rangeScratch.length < sampleCount) this.rangeScratch = new Float32Array(sampleCount);
buffer.extract(this.rangeScratch, 0, frames);
this.circularBuffer.pushSamples(this.rangeScratch, 0, frames);
buffer.receive(frames);
}
}
get frameCount() {
return this.circularBuffer.frameCount;
}
get startIndex() {
return 0;
}
readSample(sampleIndex) {
return this.circularBuffer.readSample(sampleIndex);
}
/**
* Returns a contiguous range from circular storage, padding trailing values
* with zeros when the requested range extends past available data.
*/
readSubarray(start, end) {
const normalizedStart = Math.max(0, Math.floor(start));
const requestedSamples = Math.max(normalizedStart, Math.floor(end)) - normalizedStart;
const requestedFrames = Math.floor(requestedSamples / 2);
if (requestedFrames <= 0) return this.rangeScratch.subarray(0, 0);
const needed = requestedFrames * 2;
if (this.rangeScratch.length < needed) this.rangeScratch = new Float32Array(needed);
const sourceFrameOffset = Math.floor(normalizedStart / 2);
const readSamples = this.circularBuffer.extract(this.rangeScratch, sourceFrameOffset, requestedFrames, false) * 2;
if (readSamples < needed) this.rangeScratch.fill(0, readSamples, needed);
return this.rangeScratch.subarray(0, needed);
}
receive(numFrames) {
this.circularBuffer.dropFrames(numFrames);
}
receiveSamples(output, numFrames) {
this.circularBuffer.extract(output, 0, numFrames, true);
}
};
/**
* Creates a stretch input adapter that reads from FIFO-compatible buffers.
*/
var createFifoStretchInputBufferAdapter = () => new FifoStretchBufferAdapter();
/**
* Creates a stretch input adapter backed by `CircularSampleBuffer`.
*/
var createCircularStretchInputBufferAdapter = () => new CircularStretchInputBufferAdapter();
var DEFAULT_SEQUENCE_MS = 0;
var DEFAULT_SEEKWINDOW_MS = 0;
var DEFAULT_OVERLAP_MS = 8;
var AUTOSEQ_TEMPO_LOW = .25;
var AUTOSEQ_TEMPO_TOP = 4;
var AUTOSEQ_AT_MIN = 125;
var AUTOSEQ_AT_MAX = 50;
var AUTOSEQ_K = (AUTOSEQ_AT_MAX - AUTOSEQ_AT_MIN) / (AUTOSEQ_TEMPO_TOP - AUTOSEQ_TEMPO_LOW);
var AUTOSEQ_C = AUTOSEQ_AT_MIN - AUTOSEQ_K * AUTOSEQ_TEMPO_LOW;
var AUTOSEEK_AT_MIN = 25;
var AUTOSEEK_AT_MAX = 15;
var AUTOSEEK_K = (AUTOSEEK_AT_MAX - AUTOSEEK_AT_MIN) / (AUTOSEQ_TEMPO_TOP - AUTOSEQ_TEMPO_LOW);
var AUTOSEEK_C = AUTOSEEK_AT_MIN - AUTOSEEK_K * AUTOSEQ_TEMPO_LOW;
var NORMALIZED_CORRELATION_EPSILON = 1e-12;
var QUICK_SEEK_FALLBACK_THRESHOLD = 256;
var QUICK_SEEK_MIN_VALID_CANDIDATES = 8;
/**
* Time-stretch processor for tempo adjustment without affecting pitch.
* Used internally by SoundTouch for time-stretching audio.
*/
var Stretch = class Stretch extends AbstractSamplePipe {
inputBufferAdapterFactory;
sampleBufferFactory;
inputBufferAdapter;
outputBufferAdapter;
overlapScratch;
_quickSeek;
midBufferDirty;
midBuffer;
refMidBuffer;
refMidBufferEnergy;
overlapLength;
autoSeqSetting;
autoSeekSetting;
_tempo;
sampleRate;
_overlapMs;
sequenceMs;
seekWindowMs;
seekWindowLength;
seekLength;
nominalSkip;
skipFract;
sampleReq;
/**
* Creates a Stretch instance.
* @param options Constructor options.
*/
constructor({ createBuffers = false, inputBufferAdapterFactory = createFifoStretchInputBufferAdapter, sampleBufferFactory = () => new FifoSampleBuffer() } = {}) {
super({
createBuffers,
inputBufferFactory: sampleBufferFactory,
outputBufferFactory: sampleBufferFactory
});
this.inputBufferAdapterFactory = inputBufferAdapterFactory;
this.sampleBufferFactory = sampleBufferFactory;
this.inputBufferAdapter = inputBufferAdapterFactory();
this.outputBufferAdapter = new GenericStretchWriteBufferAdapter();
this.overlapScratch = new Float32Array(0);
this._quickSeek = true;
this.midBufferDirty = true;
this.midBuffer = null;
this.refMidBufferEnergy = 0;
this.overlapLength = 0;
this.autoSeqSetting = true;
this.autoSeekSetting = true;
this._tempo = 1;
this.setParameters(44100, DEFAULT_SEQUENCE_MS, DEFAULT_SEEKWINDOW_MS, DEFAULT_OVERLAP_MS);
}
clear() {
super.clear();
this.clearMidBuffer();
}
clearMidBuffer() {
this.midBufferDirty = true;
if (this.midBuffer) this.midBuffer.fill(0);
if (this.refMidBuffer) this.refMidBuffer.fill(0);
this.skipFract = 0;
}
setParameters(sampleRate, sequenceMs, seekWindowMs, overlapMs) {
if (sampleRate > 0) this.sampleRate = sampleRate;
if (overlapMs > 0) this._overlapMs = overlapMs;
if (sequenceMs > 0) {
this.sequenceMs = sequenceMs;
this.autoSeqSetting = false;
} else this.autoSeqSetting = true;
if (seekWindowMs > 0) {
this.seekWindowMs = seekWindowMs;
this.autoSeekSetting = false;
} else this.autoSeekSetting = true;
this.calculateSequenceParameters();
this.calculateOverlapLength(this._overlapMs);
this.updateTempoDerivedState();
}
set tempo(newTempo) {
this._tempo = newTempo;
this.updateTempoDerivedState();
}
get tempo() {
return this._tempo;
}
get inputChunkSize() {
return this.sampleReq;
}
get outputChunkSize() {
return this.overlapLength + Math.max(0, this.seekWindowLength - 2 * this.overlapLength);
}
calculateOverlapLength(overlapInMsec = 0) {
let newOvl = this.sampleRate * overlapInMsec / 1e3;
newOvl = newOvl < 16 ? 16 : newOvl;
newOvl -= newOvl % 8;
if (newOvl === this.overlapLength && this.midBuffer !== null) return;
this.overlapLength = newOvl;
const needed = this.overlapLength * 2;
if (!this.refMidBuffer || this.refMidBuffer.length < needed) this.refMidBuffer = new Float32Array(needed);
if (!this.midBuffer || this.midBuffer.length < needed) this.midBuffer = new Float32Array(needed);
}
checkLimits(x, mi, ma) {
return x < mi ? mi : x > ma ? ma : x;
}
calculateSequenceParameters() {
if (this.autoSeqSetting) {
let seq = AUTOSEQ_C + AUTOSEQ_K * this._tempo;
seq = this.checkLimits(seq, AUTOSEQ_AT_MAX, AUTOSEQ_AT_MIN);
this.sequenceMs = Math.floor(seq + .5);
}
if (this.autoSeekSetting) {
let seek = AUTOSEEK_C + AUTOSEEK_K * this._tempo;
seek = this.checkLimits(seek, AUTOSEEK_AT_MAX, AUTOSEEK_AT_MIN);
this.seekWindowMs = Math.floor(seek + .5);
}
this.seekWindowLength = Math.floor(this.sampleRate * this.sequenceMs / 1e3);
this.seekLength = Math.floor(this.sampleRate * this.seekWindowMs / 1e3);
this.normalizeWindowInvariants();
}
normalizeWindowInvariants() {
this.seekLength = Math.max(1, this.seekLength);
this.seekWindowLength = Math.max(this.seekWindowLength, this.overlapLength);
}
updateTempoDerivedState() {
this.calculateSequenceParameters();
this.nominalSkip = this._tempo * (this.seekWindowLength - this.overlapLength);
this.skipFract = 0;
const intskip = Math.floor(this.nominalSkip + .5);
this.sampleReq = Math.max(intskip + this.overlapLength, this.seekWindowLength) + this.seekLength;
}
/**
* Whether the fast multi-pass seek algorithm is active.
* @returns `true` if quick seek is enabled (default); `false` for exhaustive search.
*/
get quickSeek() {
return this._quickSeek;
}
set quickSeek(enable) {
this._quickSeek = enable;
}
/**
* Current overlap crossfade length in milliseconds.
* @returns The overlap period used at the current sample rate.
*/
get overlapMs() {
return this._overlapMs;
}
/**
* Sets the overlap crossfade length and recalculates derived parameters.
* @param ms Overlap period in milliseconds (must be > 0).
*/
set overlapMs(ms) {
if (ms > 0) {
this._overlapMs = ms;
this.calculateOverlapLength(this._overlapMs);
this.calculateSequenceParameters();
this.updateTempoDerivedState();
}
}
/**
* Applies a partial set of WSOLA timing parameters.
*
* @remarks
* Only the provided fields are updated; omitted fields remain unchanged.
* Pass `sequenceMs: 0` or `seekWindowMs: 0` to switch that dimension back to auto-calculation.
*
* @param params Partial set of WSOLA timing parameters to apply.
*
* @example
* stretch.setStretchParameters({ overlapMs: 12, quickSeek: false });
*/
setStretchParameters(params) {
if (params.quickSeek !== void 0) this._quickSeek = params.quickSeek;
let needsRecalc = false;
if (params.sequenceMs !== void 0) {
if (params.sequenceMs > 0) {
this.sequenceMs = params.sequenceMs;
this.autoSeqSetting = false;
} else this.autoSeqSetting = true;
needsRecalc = true;
}
if (params.seekWindowMs !== void 0) {
if (params.seekWindowMs > 0) {
this.seekWindowMs = params.seekWindowMs;
this.autoSeekSetting = false;
} else this.autoSeekSetting = true;
needsRecalc = true;
}
if (params.overlapMs !== void 0 && params.overlapMs > 0) {
this._overlapMs = params.overlapMs;
this.calculateOverlapLength(this._overlapMs);
needsRecalc = true;
}
if (needsRecalc) {
this.calculateSequenceParameters();
this.updateTempoDerivedState();
}
}
clone() {
const result = new Stretch({
createBuffers: false,
inputBufferAdapterFactory: this.inputBufferAdapterFactory,
sampleBufferFactory: this.sampleBufferFactory
});
result.tempo = this._tempo;
result.setParameters(this.sampleRate, this.sequenceMs, this.seekWindowMs, this._overlapMs);
return result;
}
seekBestOverlapPosition(inputBuffer) {
const resolvedInputBuffer = inputBuffer ?? this.getInputBufferAdapter();
if (!this._quickSeek || this.seekLength <= QUICK_SEEK_FALLBACK_THRESHOLD) return this.seekBestOverlapPositionStereo(resolvedInputBuffer);
return this.seekBestOverlapPositionStereoQuick(resolvedInputBuffer);
}
seekBestOverlapPositionStereo(inputBuffer) {
let bestOffset;
let bestCorrelation;
let correlation;
this.preCalculateCorrelationReferenceStereo();
bestOffset = 0;
bestCorrelation = -Infinity;
for (let i = 0; i < this.seekLength; i++) {
correlation = this.calculateCrossCorrelationStereo(2 * i, this.refMidBuffer, inputBuffer);
if (correlation > bestCorrelation) {
bestCorrelation = correlation;
bestOffset = i;
}
}
return bestOffset;
}
seekBestOverlapPositionStereoQuick(inputBuffer) {
let bestOffset;
let bestCorrelation;
let correlation;
let correlationOffset;
let tempOffset;
let evaluatedCandidates;
this.preCalculateCorrelationReferenceStereo();
bestCorrelation = this.calculateCrossCorrelationStereo(0, this.refMidBuffer, inputBuffer);
evaluatedCandidates = 1;
bestOffset = 0;
correlationOffset = 0;
for (let scanCount = 0; scanCount < 4; scanCount++) {
let previousTempOffset = Number.MIN_SAFE_INTEGER;
const scanOffsets = this.getQuickScanOffsets(scanCount);
for (const scanOffset of scanOffsets) {
tempOffset = correlationOffset + scanOffset;
if (tempOffset === previousTempOffset) continue;
previousTempOffset = tempOffset;
if (tempOffset < 0) continue;
if (tempOffset >= this.seekLength) continue;
correlation = this.calculateCrossCorrelationStereo(2 * tempOffset, this.refMidBuffer, inputBuffer);
evaluatedCandidates++;
if (correlation > bestCorrelation) {
bestCorrelation = correlation;
bestOffset = tempOffset;
}
}
correlationOffset = bestOffset;
}
if (evaluatedCandidates < QUICK_SEEK_MIN_VALID_CANDIDATES) return this.seekBestOverlapPositionStereo(inputBuffer);
return bestOffset;
}
getQuickScanOffsets(stage) {
const maxOffset = Math.max(1, this.seekLength - 1);
if (stage === 0) return this.generateFractionalScanOffsets(maxOffset, 2, 1, 14, 24);
if (stage === 1) return this.generateSymmetricScanOffsets(maxOffset, .2);
if (stage === 2) return this.generateSymmetricScanOffsets(maxOffset, .06);
return this.generateSymmetricScanOffsets(maxOffset, .015);
}
generateFractionalScanOffsets(maxOffset, startNumerator, stepNumerator, denominator, steps) {
const offsets = [];
const seen = /* @__PURE__ */ new Set();
const safeDenominator = Math.max(1, denominator);
const safeSteps = Math.max(1, steps);
for (let i = 0; i < safeSteps; i++) {
const numerator = startNumerator + i * stepNumerator;
const value = Math.round(maxOffset * numerator / safeDenominator);
if (value <= 0 || value >= this.seekLength || seen.has(value)) continue;
seen.add(value);
offsets.push(value);
}
return offsets;
}
generateSymmetricScanOffsets(maxOffset, spanRatio) {
const span = Math.max(1, Math.round(maxOffset * spanRatio));
const scales = [
1,
.75,
.5,
.25
];
const negative = [];
const positive = [];
const seen = /* @__PURE__ */ new Set();
for (const scale of scales) {
const magnitude = Math.max(1, Math.round(span * scale));
const neg = -magnitude;
const pos = magnitude;
if (!seen.has(neg)) {
seen.add(neg);
negative.push(neg);
}
if (!seen.has(pos)) {
seen.add(pos);
positive.push(pos);
}
}
return negative.concat(positive);
}
preCalculateCorrelationReferenceStereo() {
let energy = 0;
for (let i = 0; i < this.overlapLength; i++) {
const temp = i * (this.overlapLength - i);
const ctx = i * 2;
const left = this.midBuffer[ctx] * temp;
const right = this.midBuffer[ctx + 1] * temp;
this.refMidBuffer[ctx] = left;
this.refMidBuffer[ctx + 1] = right;
ener