Is FLAC decoded PCM guaranteed to start at the sample of an arbitrary seekpoint, if we fetch via HTTP range request from the seekpoints noted offset (on a fixed frame size e.g. 1024)?
I encounter missing samples at the start.
Broader context of my problem: I want to build a streaming app.
I do fetch 3 *.flac files (with 4, 5 and 7 channels), decode them, then stitch the 16 channels together for later Ambisonics decoding. They have to be sample accurate. I already know that I have to feed more data then seekpoint_A_offset to seekpoint_B_offset in order to guarantee the minimum length to B, but what is too much can be cut, we know how much samples we need by the parsed SEEKTABLE.
However I have to be certain of the fact that the decoder started on seekpoint_A_sample.
Can you confirm? (otherwise the error is somewhere else in my signal chain)
My toolchain:
1. $ ffmpeg # for splitting the original 16 channel audio file
2. $ flac --compression-level-8 # for encoding
--blocksize=1024
--force-raw-format
--endian=little
--sign=signed
--channels=7
--bps={bits_per_sample}
--sample-rate={sample_rate}
--no-seektable
3. $ metaflac --dont-use-padding --add-seekpoint=2530x # for adding seekpoints
# that are sample synchronous
# acros files
4. HTTP range requests to fetch parts of the 3 flac files
from the given offset values of the seekpoints
5. @wasm-audio-decoders/flac for decoding the files in parallel
In this code I have so far we miss samples between windows like A→B here B→C:
/**
* All info for a Range-Request A→B
* (incl. FUDGE, and EOF edge case).
*
* usage:
* const {byteStart, byteEnd, wantSamples} =
* SeekPoints.rangeFor(aIdx, bIdx, seekTable);
*
* const bytes = await fetchRange(url, byteStart, byteEnd);
*
* @param seekTable full Seektable
* @param indexA Start index (inclusive)
* @param indexB End index (inclusive!)
* @param fudgeBytes optional Buffer (default 64 KiB)
* @returns {
* byteStart: number,
* byteEnd: number|null, // null → to EOF
* wantSamples: number // sample_B – sample_A
* }
*/
static rangeFor(indexA, indexB, seekTable, fudgeBytes = 64 * 1024) {
if (indexA < 0 || indexB >= seekTable.length || indexA >= indexB)
throw new Error('invalid indices');
const byteStart = seekTable[indexA].streamOffset;
const nextOff = seekTable[indexB + 1]?.streamOffset ?? null;
const byteEnd = nextOff ? nextOff + fudgeBytes - 1 : null;
const wantSamples = seekTable[indexB].sample - seekTable[indexA].sample;
return {byteStart, byteEnd, wantSamples};
}
/**
* Fetches a byte range and returns it as an Uint8Array.
* @param {string} url
* @param {number} offset
* @param {number} length
* @returns {Promise<Uint8Array>}
*/
async #fetchRange(url, offset, length) {
const headers = {};
// Open‑ended request if length is not positive
if (length === undefined || length <= 0) {
headers.Range = `bytes=${offset}-`;
} else {
headers.Range = `bytes=${offset}-${offset + length - 1}`;
}
const response = await fetch(url, {headers});
if (!response.ok && response.status !== 206) {
throw new Error(`[fetchRange] Unexpected HTTP response ${response.status} for ${url}`);
}
const arrayBuffer = await response.arrayBuffer();
return new Uint8Array(arrayBuffer);
}
/**
* Fetch + decode PCM between two given seekpoints in a given FLAC using SeekPoints.rangeFor().
* Ensures last frame completeness via nextSeekpoint + FUDGE and trims
* the decoded PCM to exactly sample_B − sample_A.
*
* This function is an internal step that will be called multiple times
* until the buffer is filled.
*
* @returns {Promise<Float32Array>[]}
*/
#fetchAndDecodeFlacFileFromSeekPointAtoB(
url,
seekTable,
firstIndex,
lastIndex,
sampleRate,
expectedChannels
) {
console.log("[#fetchAndDecodeFlacFileFromSeekPointAtoB", firstIndex, lastIndex);
const {byteStart, byteEnd, wantSamples} = SeekPoints.rangeFor(firstIndex, lastIndex, seekTable);
const byteLength = byteEnd !== null ? (byteEnd - byteStart) : undefined;
return this.#fetchRange(url, byteStart, byteLength)
.then(rawFlacData => {
const decoder = new FLACDecoderWebWorker();
return decoder.ready
.then(() => decoder.decode(rawFlacData))
.finally(() => decoder.free());
})
.then(decoded => {
if (!decoded.channelData || decoded.channelData.length !== expectedChannels) {
throw new Error(`[decode] Channel count mismatch: expected ${expectedChannels}, got ${decoded.channelData?.length}`);
}
// Per-channel trimming
return decoded.channelData.map((channelArray, channel) => {
if (channelArray.length === wantSamples) {
return channelArray;
} else if (channelArray.length > wantSamples) {
// Overlong, slice
return channelArray.subarray(0, wantSamples);
} else {
throw new Error(`[decode] File ${url} channel ${channel} decoded ${channelArray.length} samples, expected ${wantSamples}`);
}
});
});
}
// ---- invocation ----
const fetchAndDecodeAllFlacFilesPromises = this.fileList.map((url, fileIndex) => {
const meta = this.flacMetaData[fileIndex];
const seekTable = meta.seekTable;
return this.#fetchAndDecodeFlacFileFromSeekPointAtoB(
url,
seekTable,
this.#windowSeekpointFirstIndex,
this.#windowSeekpointLastIndex,
this.#trackSampleRate,
meta.streamInfo.channels
);
});
const decodedPcmFromAllFlacFiles = await Promise.all(fetchAndDecodeAllFlacFilesPromises);
decodedPcmFromAllFlacFiles.flat().forEach((channelData, chIndex) => {
const first64 = channelData.slice(0, 64);
const last64 = channelData.slice(-64);
if (chIndex === 0) {
console.log(`[Channel ${chIndex}] First 64 samples:`, first64);
console.log(`[Channel ${chIndex}] Last 64 samples:`, last64);
}
});
byteranges, and a known seek point byte offset? Or are you using a custom range metric likeseekpointand the interpreting that and seeking on the server?async #fetchRange(url, offset, length) { const headers = {}; if (length === undefined || length <= 0) { headers.Range = ``bytes=${offset}-``; } else { headers.Range = ``bytes=${offset}-${offset + length - 1}``; } const response = await fetch(url, {headers});