UNPKG

mediabunny

Version:

Pure TypeScript media toolkit for reading, writing, and converting media files, directly in the browser.

122 lines (98 loc) 3.12 kB
/*! * Copyright (c) 2026-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 { Writer } from '../writer'; import { computeMp3FrameSize, getXingOffset, KILOBIT_RATES, XING, XingFlags, } from '../../shared/mp3-misc'; export type XingFrameData = { mpegVersionId: number; layer: number; frequencyIndex: number; sampleRate: 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); let lowSamplingFrequency: number; if (data.mpegVersionId & 2) { lowSamplingFrequency = (data.mpegVersionId & 1) ? 0 : 1; } else { lowSamplingFrequency = 1; } const padding = 0; const neededBytes = 155; let bitrateIndex = -1; const bitrateOffset = lowSamplingFrequency * 16 * 4 + data.layer * 16; // Let's find the lowest bitrate for which the frame size is sufficiently large to fit all the data for (let i = 0; i < 16; i++) { const kbr = KILOBIT_RATES[bitrateOffset + i]!; const size = computeMp3FrameSize(lowSamplingFrequency, data.layer, 1000 * kbr, data.sampleRate, padding); if (size >= neededBytes) { bitrateIndex = i; break; } } 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 |= XingFlags.FrameCount; } if (data.fileSize !== null) { flags |= XingFlags.FileSize; } if (data.toc !== null) { flags |= XingFlags.Toc; } this.writeU32(flags); this.writeU32(data.frameCount ?? 0); this.writeU32(data.fileSize ?? 0); this.writer.write(data.toc ?? new Uint8Array(100)); const kilobitRate = KILOBIT_RATES[bitrateOffset + bitrateIndex]!; const frameSize = computeMp3FrameSize( lowSamplingFrequency, data.layer, 1000 * kilobitRate, data.sampleRate, padding, ); this.writer.seek(startPos + frameSize); } }