Files
mediabunny/src/mp3/mp3-writer.ts
T
2025-01-26 11:45:56 +01:00

105 lines
2.7 KiB
TypeScript

import { Writer } from '../writer';
import {
computeMp3FrameSize,
getXingOffset,
MPEG_V1_BITRATES,
MPEG_V2_BITRATES,
SAMPLING_RATES,
XING,
} from './mp3-misc';
export type XingFrameData = {
mpegVersionId: number;
layer: number;
frequencyIndex: number;
channel: number;
modeExtension: number;
copyright: number;
original: number;
emphasis: number;
frameCount: number | null;
fileSize: number | null;
toc: Uint8Array | null;
};
export class Mp3Writer {
private helper = new Uint8Array(8);
private helperView = new DataView(this.helper.buffer);
constructor(private writer: Writer) {}
writeU32(value: number) {
this.helperView.setUint32(0, value, false);
this.writer.write(this.helper.subarray(0, 4));
}
writeXingFrame(data: XingFrameData) {
const startPos = this.writer.getPos();
const firstByte = 0xff;
const secondByte = 0xe0 | (data.mpegVersionId << 3) | (data.layer << 1);
const bitrateGroup = data.mpegVersionId === 3 ? MPEG_V1_BITRATES : MPEG_V2_BITRATES;
const bitrates = bitrateGroup?.[data.layer];
if (!bitrates) {
throw new Error('Invalid MPEG version and layer combination.');
}
const sampleRate = SAMPLING_RATES[data.mpegVersionId]?.[data.frequencyIndex];
if (!sampleRate || sampleRate === -1) {
throw new Error('Invalid MPEG version and frequency index combination.');
}
const padding = 0;
const neededBytes = 155;
// Let's find the lowest bitrate for which the frame size is sufficiently large to fit all the data
const bitrateIndex = bitrates.findIndex((kbr) => {
return computeMp3FrameSize(1000 * kbr, sampleRate, padding) >= neededBytes;
});
if (bitrateIndex === -1) {
throw new Error('No suitable bitrate found.');
}
const thirdByte = (bitrateIndex << 4) | (data.frequencyIndex << 2) | padding << 1;
const fourthByte = (data.channel << 6)
| (data.modeExtension << 4)
| (data.copyright << 3)
| (data.original << 2)
| data.emphasis;
this.helper[0] = firstByte;
this.helper[1] = secondByte;
this.helper[2] = thirdByte;
this.helper[3] = fourthByte;
this.writer.write(this.helper.subarray(0, 4));
const xingOffset = getXingOffset(data.mpegVersionId, data.channel);
this.writer.seek(startPos + xingOffset);
this.writeU32(XING);
let flags = 0;
if (data.frameCount !== null) {
flags |= 1;
}
if (data.fileSize !== null) {
flags |= 2;
}
if (data.toc !== null) {
flags |= 4;
}
this.writeU32(flags);
this.writeU32(data.frameCount ?? 0);
this.writeU32(data.fileSize ?? 0);
this.writer.write(data.toc ?? new Uint8Array(100));
const frameSize = computeMp3FrameSize(1000 * bitrates[bitrateIndex]!, sampleRate, padding);
this.writer.seek(startPos + frameSize);
}
}