Skip to content

Instantly share code, notes, and snippets.

@sebmck
Created December 24, 2023 04:34
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save sebmck/792bf3247fc924019d25f888251d9126 to your computer and use it in GitHub Desktop.
Save sebmck/792bf3247fc924019d25f888251d9126 to your computer and use it in GitHub Desktop.
import { IndexedDataView } from "../IndexedDataView.ts";
import { FileInfo, FileTags, File, FileMedia, ID3v2PictureType } from "../types.ts";
enum MetadataBlockHeaderType {
STREAMINFO = 0,
PADDING = 1,
APPLICATION = 2,
SEEKTABLE = 3,
VORBIS_COMMENT = 4,
CUESHEET = 5,
PICTURE = 6,
// 7-126 : reserved
INVALID = 127 // invalid, to avoid confusion with a frame sync code
}
export function parseFLAC(view: IndexedDataView): File {
const tags: FileTags = new Map();
const media: FileMedia[] = [];
let info: undefined | FileInfo;
// "fLaC", the FLAC stream marker in ASCII, meaning byte 0 of the stream is 0x66, followed by 0x4C 0x61 0x43
view.expect("fLaC");
// Start reading metadata blocks
while (!view.atEOF()) {
const firstByte = view.readUint8();
// <1>: Last-metadata-block flag: '1' if this block is the last metadata block before the audio blocks, '0' otherwise.
const isLast = (firstByte & (1 << 7)) !== 0;
// <7>: BLOCK_TYPE
const rawBlockType = firstByte ^ (isLast ? 128 : 0);
const type: MetadataBlockHeaderType = MetadataBlockHeaderType[rawBlockType] === undefined ? MetadataBlockHeaderType.INVALID : rawBlockType
// <24>: Length (in bytes) of metadata to follow (does not include the size of the METADATA_BLOCK_HEADER)
const length = view.readUint24();
// Parse the actual block
if (type === MetadataBlockHeaderType.STREAMINFO) {
info = parseMetadataStreamInfo(view);
} else if (type === MetadataBlockHeaderType.PICTURE) {
media.push(parseMetadataBlockPicture(view));
} else if (type === MetadataBlockHeaderType.VORBIS_COMMENT) {
parseMetadataBlockVoribsComment(view, tags);
} else {
view.skip(length);
}
if (isLast) {
break;
}
}
if (info === undefined) {
throw new Error(`File didn't contain a STREAMINFO block`);
}
return {
info,
media,
tags,
};
}
// https://xiph.org/flac/format.html#metadata_block_streaminfo
function parseMetadataStreamInfo(view: IndexedDataView): FileInfo {
// <16>: The minimum block size (in samples) used in the stream.
const minBlockSize = view.readUint16();
// <16>: The maximum block size (in samples) used in the stream. (Minimum blocksize == maximum blocksize) implies a fixed-blocksize stream.
const maxBlockSize = view.readUint16();
// <24>: The minimum frame size (in bytes) used in the stream. May be 0 to imply the value is not known.
const minFrameSize = view.readUint24();
// <24>: The maximum frame size (in bytes) used in the stream. May be 0 to imply the value is not known.
const maxFrameSize = view.readUint24();
const tmp = view.readUint32();
// <20>: Sample rate in Hz. Though 20 bits are available, the maximum sample rate is limited by the structure of frame headers to 655350Hz. Also, a value of 0 is invalid.
const sampleRate = tmp >>> 12;
// <3>: (number of channels)-1. FLAC supports from 1 to 8 channels
const channels = 1 + (tmp >>> 9) & 0x07;
// <5>: (bits per sample)-1. FLAC supports from 4 to 32 bits per sample.
const bitsPerSample = 1 + (tmp >>> 4) & 0x1f;
// <36>: Total samples in stream. 'Samples' means inter-channel sample, i.e. one second of 44.1Khz audio will have 44100 samples regardless of the number of channels. A value of zero here means the number of total samples is unknown.
const samplesInStream = +((tmp & 0x0f) << 4) + view.readUint32();
// <128>: MD5 signature of the unencoded audio data. This allows the decoder to determine if an error exists in the audio data even when the error does not result in an invalid bitstream.
const md5 = view.readBytes(16);
const duration = samplesInStream / sampleRate;
return {
minBlockSize,
maxBlockSize,
minFrameSize,
maxFrameSize,
sampleRate,
channels,
bitsPerSample,
samplesInStream,
md5,
duration,
};
}
// https://xiph.org/flac/format.html#metadata_block_picture
function parseMetadataBlockPicture(view: IndexedDataView): FileMedia {
// <32>: The picture type according to the ID3v2 APIC frame
const rawType = view.readUint32();
const type: ID3v2PictureType = ID3v2PictureType[rawType] === undefined ? ID3v2PictureType.OTHER : rawType;
// <32>: The length of the MIME type string in bytes.
const mimeLen = view.readUint32();
// <n*8>: The MIME type string, in printable ASCII characters 0x20-0x7e. The MIME type may also be --> to signify that the data part is a URL of the picture instead of the picture data itself.
const mime = view.readString(mimeLen);
// <32>: The length of the description string in bytes.
const descriptionLen = view.readUint32();
// <n*8>: The description of the picture, in UTF-8.
const descriptionRaw = view.readString(descriptionLen);
const description = descriptionRaw === "" ? undefined : descriptionRaw;
// <32>: The width of the picture in pixels.
const width = view.readUint32();
// <32>: The height of the picture in pixels.
const height = view.readUint32();
// <32>: The color depth of the picture in bits-per-pixel.
const colorDepth = view.readUint32();
// <32>: For indexed-color pictures (e.g. GIF), the number of colors used, or 0 for non-indexed pictures.
const colorsUsed = view.readUint32();
// <32>: The length of the picture data in bytes.
const dataLen = view.readUint32();
// <n*8>: The binary picture data.
const data = view.readBytes(dataLen);
return {
type,
mime,
width,
height,
description,
colorDepth,
colorsUsed,
data,
};
}
// https://xiph.org/flac/format.html#metadata_block_vorbis_comment
function parseMetadataBlockVoribsComment(view: IndexedDataView, tags: FileTags): void {
const vendorLen = view.readUint32(true);
const vendor = view.readString(vendorLen);
vendor;
const commentCount = view.readUint32(true);
for (let i = 0; i < commentCount; i++) {
const commentLen = view.readUint32(true);
const comment = view.readString(commentLen);
const sepIndex = comment.indexOf("=");
if (sepIndex === -1) {
tags.set(comment, "");
} else {
const key = comment.slice(0, sepIndex);
const value = comment.slice(sepIndex + 1);
tags.set(key, value);
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment