Files
mediabunny/src/flac/flac-misc.ts
T
d426d36386 FLAC container support (#95)
* add a test

* recognize as input format

* scaffold flac demuxer

* implement getting metadata

* Implement mime type

* read all metadata + deduplicate stubs

* Read first packet

* copyright headers

* read the first packet

* Get entire first packet, work on advancing

* iterate over all samples

* testable with bun

* stub out metadata support

* parse descriptive metadata

* All in 1 file

seems more appropriate to the philosophy

* some parameters are not needed anymore all within 1 class

* skip over bytes we are sure are not the syncword

* run prettier

* timestamp is determined based on passed blocks, not maximumBlockSize

* no binary search needed!

* simplifications

* Finish demuxer reading sequentially

* more tests + add a file with a seektable

* don't throw if (this.audioInfo.minimumBlockSize !== this.audioInfo.maximumBlockSize

* Add docs

* Update format compatibility table

* Simplification

* confirm conversion is working

* Finish

* Resolve TODO comment

* Support images (read-only)

* Returning description as Uint8Array

* Cleanup of demuxer

* Misc renames

* Object on same line

* Resolve first batch of comments

* Throw errors on corrupt blocks, correctly use requestSlice()

* Fix description field

* Explain why last frame is a bit shorter

* Add FLAC to README

* Put track backings below demuxer

* convert to methods

* Reorder container checking

* getBlockSize() -> readBlockSize()

* bitStream -> bitstream

* better naming for bytes

* use .skip()

* Don't return blockSize twice in readFlacFrameHeader

* Use enum + switch to distinguish Flac block types

* Use else-if

* Use else-if

* Compressed switch statement

* readCodedNumber

* Update flac-misc.ts

* We don't need the bits variable at all

* reorder functions in flac-demuxer

* Handle gracefully not being able to load another sample

* Update flac-demuxer.ts

* blockingbit null

* use async instead of promise.resolve

* use binary search

* Replace recursion with while loop

* Add mutex to getPacket()

* Load more data not in getPacketAtIndex, but outside

* Apply suggestion from @Vanilagy

Co-authored-by: David P. <[email protected]>

* computeDuration() reads last packet

* Update flac-demuxer.ts

* share vorbis comment reading logic

* reuse vorbis comment writing logic, set vendor always to "Mediabunny"

* flush after writign

* Use FileSlice.tempFromBytes

* assert !== null

* fixing nitpicks

* apply suggestions

* fix ogg

* We are now muxing images

* apply suggestion

* Update src/flac/flac-muxer.ts

Co-authored-by: David P. <[email protected]>

* apply suggestion

* readSampleRate()

* seek outside writeHeader()

* mention vorbis metadata +  add to metadatatags comment

* should be able to -> can

* Add test in metadata tags

* Add test for packets being byte identical after remuxing

* compare to null

* no casting to uint8array

* make test pass

* Update flac-muxer.ts

* Call validateAudioChunkMetadata() and validateAndNormalizeTimestamp()

* `onFrame` option

* emit frames using onFrame

* Run prettier over files

* Fix FLAC PICTURE block logic, small other changes

* Update docs

* Remove .only modifier

* fix remuxing and add test

* Don't throw error if parsing fails in header, since hitting a syncword might just be coincidential

* Fix remaining type errors

---------

Co-authored-by: David P. <[email protected]>
2025-09-18 21:05:09 +02:00

165 lines
4.1 KiB
TypeScript

/*!
* Copyright (c) 2025-present, Vanilagy and contributors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
import { assert, assertNever, Bitstream } from '../misc';
import { FileSlice, readBytes, readU16Be, readU8 } from '../reader';
type BlockSizeOrUncommon = number | 'uncommon-u16' | 'uncommon-u8';
type SampleRateOrUncommon =
| number
| 'uncommon-u8'
| 'uncommon-u16'
| 'uncommon-u16-10';
// https://www.rfc-editor.org/rfc/rfc9639.html#name-block-size-bits
export const getBlockSizeOrUncommon = (bits: number): BlockSizeOrUncommon | null => {
if (bits === 0b0000) {
return null;
} else if (bits === 0b0001) {
return 192;
} else if (bits >= 0b0010 && bits <= 0b0101) {
return 144 * 2 ** bits;
} else if (bits === 0b0110) {
return 'uncommon-u8';
} else if (bits === 0b0111) {
return 'uncommon-u16';
} else if (bits >= 0b1000 && bits <= 0b1111) {
return 2 ** bits;
} else {
return null;
}
};
// https://www.rfc-editor.org/rfc/rfc9639.html#name-sample-rate-bits
export const getSampleRateOrUncommon = (
sampleRateBits: number,
streamInfoSampleRate: number,
): SampleRateOrUncommon | null => {
switch (sampleRateBits) {
case 0b0000: return streamInfoSampleRate;
case 0b0001: return 88200;
case 0b0010: return 176400;
case 0b0011: return 192000;
case 0b0100: return 8000;
case 0b0101: return 16000;
case 0b0110: return 22050;
case 0b0111: return 24000;
case 0b1000: return 32000;
case 0b1001: return 44100;
case 0b1010: return 48000;
case 0b1011: return 96000;
case 0b1100: return 'uncommon-u8';
case 0b1101: return 'uncommon-u16';
case 0b1110: return 'uncommon-u16-10';
default: return null;
}
};
// https://www.rfc-editor.org/rfc/rfc9639.html#name-coded-number
export const readCodedNumber = (fileSlice: FileSlice): number => {
let ones = 0;
const bitstream1 = new Bitstream(readBytes(fileSlice, 1));
while (bitstream1.readBits(1) === 1) {
ones++;
}
if (ones === 0) {
return bitstream1.readBits(7);
}
const bitArray: number[] = [];
const extraBytes = ones - 1;
const bitstream2 = new Bitstream(readBytes(fileSlice, extraBytes));
const firstByteBits = 8 - ones - 1;
for (let i = 0; i < firstByteBits; i++) {
bitArray.unshift(bitstream1.readBits(1));
}
for (let i = 0; i < extraBytes; i++) {
for (let j = 0; j < 8; j++) {
const val = bitstream2.readBits(1);
if (j < 2) {
continue;
}
bitArray.unshift(val);
}
}
const encoded = bitArray.reduce((acc, bit, index) => {
return acc | (bit << index);
}, 0);
return encoded;
};
export const readBlockSize = (
slice: FileSlice,
blockSizeBits: BlockSizeOrUncommon,
) => {
if (blockSizeBits === 'uncommon-u16') {
return readU16Be(slice) + 1;
} else if (blockSizeBits === 'uncommon-u8') {
return readU8(slice) + 1;
} else if (typeof blockSizeBits === 'number') {
return blockSizeBits;
} else {
assertNever(blockSizeBits);
assert(false);
}
};
export const readSampleRate = (
slice: FileSlice,
sampleRateOrUncommon: SampleRateOrUncommon,
) => {
if (sampleRateOrUncommon === 'uncommon-u16') {
return readU16Be(slice);
}
if (sampleRateOrUncommon === 'uncommon-u16-10') {
return readU16Be(slice) * 10;
}
if (sampleRateOrUncommon === 'uncommon-u8') {
return readU8(slice);
}
if (typeof sampleRateOrUncommon === 'number') {
return sampleRateOrUncommon;
}
return null;
};
// https://www.rfc-editor.org/rfc/rfc9639.html#section-9.1.1
export const calculateCrc8 = (data: Uint8Array) => {
const polynomial = 0x07; // x^8 + x^2 + x^1 + x^0
let crc = 0x00; // Initialize CRC to 0
for (const byte of data) {
crc ^= byte; // XOR byte into least significant byte of crc
for (let i = 0; i < 8; i++) {
// For each bit in the byte
if ((crc & 0x80) !== 0) {
// If the leftmost bit (MSB) is set
crc = (crc << 1) ^ polynomial; // Shift left and XOR with polynomial
} else {
crc <<= 1; // Just shift left
}
crc &= 0xff; // Ensure CRC remains 8-bit
}
}
return crc;
};