mirror of
https://github.com/arcodange-org/mediabunny.git
synced 2026-09-27 02:43:48 +02:00
230 lines
6.9 KiB
TypeScript
230 lines
6.9 KiB
TypeScript
/*!
|
|
* 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 { Bitstream } from './bitstream';
|
|
|
|
export type AacAudioSpecificConfig = {
|
|
objectType: number;
|
|
coreObjectType: number;
|
|
frequencyIndex: number;
|
|
channelConfiguration: number;
|
|
outputSampleRate: number | null;
|
|
outputNumberOfChannels: number | null;
|
|
};
|
|
|
|
export const aacFrequencyTable = [
|
|
96000, 88200, 64000, 48000, 44100, 32000,
|
|
24000, 22050, 16000, 12000, 11025, 8000, 7350,
|
|
];
|
|
|
|
export const aacChannelMap = [-1, 1, 2, 3, 4, 5, 6, 8];
|
|
|
|
export const parseAacAudioSpecificConfig = (bytes: Uint8Array | null): AacAudioSpecificConfig => {
|
|
if (!bytes || bytes.byteLength < 2) {
|
|
throw new TypeError('AAC description must be at least 2 bytes long.');
|
|
}
|
|
|
|
const bitstream = new Bitstream(bytes);
|
|
|
|
const objectType = readAacObjectType(bitstream);
|
|
const { frequencyIndex, sampleRate } = readAacSamplingFrequency(bitstream);
|
|
|
|
const channelConfiguration = bitstream.readBits(4);
|
|
let numberOfChannels: number | null = null;
|
|
if (channelConfiguration >= 1 && channelConfiguration <= 7) {
|
|
numberOfChannels = aacChannelMap[channelConfiguration]!;
|
|
}
|
|
|
|
let coreObjectType = objectType;
|
|
let psPresent = false;
|
|
let outputSampleRate = sampleRate;
|
|
|
|
if (objectType === 5 || objectType === 29) {
|
|
// Explicit hierarchical signaling: everything read so far describes the core coder, and the rate the
|
|
// decoder actually outputs follows right here
|
|
psPresent = objectType === 29;
|
|
|
|
outputSampleRate = readAacSamplingFrequency(bitstream).sampleRate;
|
|
coreObjectType = readAacObjectType(bitstream);
|
|
|
|
if (coreObjectType === 22) {
|
|
bitstream.skipBits(4); // extensionChannelConfiguration
|
|
}
|
|
} else {
|
|
// There may be SBR/PS flags sitting behind a sync word after the config of the core coder. We find them by
|
|
// scanning for the sync word, just like FFmpeg does.
|
|
while (bitstream.getBitsLeft() > 15) {
|
|
const searchStart = bitstream.pos;
|
|
|
|
if (bitstream.readBits(11) !== 0x2b7) {
|
|
bitstream.pos = searchStart + 1;
|
|
continue;
|
|
}
|
|
|
|
if (readAacObjectType(bitstream) === 5 && bitstream.readBits(1)) {
|
|
outputSampleRate = readAacSamplingFrequency(bitstream).sampleRate;
|
|
|
|
if (bitstream.getBitsLeft() > 11 && bitstream.readBits(11) === 0x548) {
|
|
psPresent = !!bitstream.readBits(1);
|
|
}
|
|
}
|
|
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (numberOfChannels !== null && numberOfChannels > 1) {
|
|
// PS only ever upmixes a mono core. A stereo signal means whatever we read was bogus
|
|
psPresent = false;
|
|
}
|
|
|
|
return {
|
|
objectType,
|
|
coreObjectType,
|
|
frequencyIndex,
|
|
channelConfiguration,
|
|
outputSampleRate,
|
|
outputNumberOfChannels: psPresent && numberOfChannels === 1
|
|
? 2
|
|
: numberOfChannels,
|
|
};
|
|
};
|
|
|
|
const readAacObjectType = (bitstream: Bitstream) => {
|
|
const objectType = bitstream.readBits(5);
|
|
return objectType === 31 ? 32 + bitstream.readBits(6) : objectType;
|
|
};
|
|
|
|
const readAacSamplingFrequency = (bitstream: Bitstream) => {
|
|
const frequencyIndex = bitstream.readBits(4);
|
|
|
|
if (frequencyIndex === 15) {
|
|
return {
|
|
frequencyIndex,
|
|
sampleRate: bitstream.readBits(24),
|
|
};
|
|
}
|
|
|
|
return {
|
|
frequencyIndex,
|
|
sampleRate: frequencyIndex < aacFrequencyTable.length
|
|
? aacFrequencyTable[frequencyIndex]!
|
|
: null,
|
|
};
|
|
};
|
|
|
|
export const buildAacAudioSpecificConfig = (config: {
|
|
objectType: number;
|
|
outputSampleRate: number;
|
|
outputNumberOfChannels: number;
|
|
}) => {
|
|
const usesSbr = config.objectType === 5 || config.objectType === 29;
|
|
const usesPs = config.objectType === 29;
|
|
|
|
// SBR runs the core coder at half the output rate, and PS upmixes a mono core to stereo
|
|
const coreSampleRate = usesSbr ? config.outputSampleRate / 2 : config.outputSampleRate;
|
|
const coreNumberOfChannels = usesPs ? 1 : config.outputNumberOfChannels;
|
|
|
|
const channelConfiguration = aacChannelMap.indexOf(coreNumberOfChannels);
|
|
if (channelConfiguration === -1) {
|
|
throw new TypeError(`Unsupported number of channels: ${config.outputNumberOfChannels}`);
|
|
}
|
|
|
|
// Object type, sampling frequency, channel configuration, then a bare GASpecificConfig
|
|
let bitCount = 5 + 4 + 4 + 3;
|
|
if (config.objectType >= 32) {
|
|
bitCount += 6;
|
|
}
|
|
if (findAacFrequencyIndex(coreSampleRate) === 15) {
|
|
bitCount += 24;
|
|
}
|
|
if (usesSbr) {
|
|
bitCount += 4 + 5; // Extension sampling frequency and the object type of the core coder
|
|
if (findAacFrequencyIndex(config.outputSampleRate) === 15) {
|
|
bitCount += 24;
|
|
}
|
|
}
|
|
|
|
const byteCount = Math.ceil(bitCount / 8);
|
|
const bytes = new Uint8Array(byteCount);
|
|
const bitstream = new Bitstream(bytes);
|
|
|
|
writeAacObjectType(bitstream, config.objectType);
|
|
writeAacSamplingFrequency(bitstream, coreSampleRate);
|
|
bitstream.writeBits(4, channelConfiguration);
|
|
|
|
if (usesSbr) {
|
|
writeAacSamplingFrequency(bitstream, config.outputSampleRate);
|
|
writeAacObjectType(bitstream, 2); // AAC-LC underneath
|
|
}
|
|
|
|
bitstream.writeBits(3, 0); // frameLengthFlag, dependsOnCoreCoder, extensionFlag
|
|
|
|
return bytes;
|
|
};
|
|
|
|
const writeAacObjectType = (bitstream: Bitstream, objectType: number) => {
|
|
if (objectType < 32) {
|
|
bitstream.writeBits(5, objectType);
|
|
} else {
|
|
bitstream.writeBits(5, 31);
|
|
bitstream.writeBits(6, objectType - 32);
|
|
}
|
|
};
|
|
|
|
const writeAacSamplingFrequency = (bitstream: Bitstream, sampleRate: number) => {
|
|
const frequencyIndex = findAacFrequencyIndex(sampleRate);
|
|
bitstream.writeBits(4, frequencyIndex);
|
|
|
|
if (frequencyIndex === 15) {
|
|
bitstream.writeBits(24, sampleRate);
|
|
}
|
|
};
|
|
|
|
const findAacFrequencyIndex = (sampleRate: number) => {
|
|
const index = aacFrequencyTable.indexOf(sampleRate);
|
|
return index === -1 ? 15 : index;
|
|
};
|
|
|
|
export type AdtsHeaderTemplate = {
|
|
header: Uint8Array;
|
|
bitstream: Bitstream;
|
|
};
|
|
|
|
export const buildAdtsHeaderTemplate = (config: AacAudioSpecificConfig): AdtsHeaderTemplate => {
|
|
const header = new Uint8Array(7);
|
|
const bitstream = new Bitstream(header);
|
|
|
|
const { coreObjectType, frequencyIndex, channelConfiguration } = config;
|
|
const profile = coreObjectType - 1;
|
|
|
|
bitstream.writeBits(12, 0b1111_11111111); // Syncword
|
|
bitstream.writeBits(1, 0); // MPEG Version
|
|
bitstream.writeBits(2, 0); // Layer
|
|
bitstream.writeBits(1, 1); // Protection absence
|
|
bitstream.writeBits(2, profile); // Profile
|
|
bitstream.writeBits(4, frequencyIndex); // MPEG-4 Sampling Frequency Index
|
|
bitstream.writeBits(1, 0); // Private bit
|
|
bitstream.writeBits(3, channelConfiguration); // MPEG-4 Channel Configuration
|
|
bitstream.writeBits(1, 0); // Originality
|
|
bitstream.writeBits(1, 0); // Home
|
|
bitstream.writeBits(1, 0); // Copyright ID bit
|
|
bitstream.writeBits(1, 0); // Copyright ID start
|
|
bitstream.skipBits(13); // Frame length (to be filled per packet)
|
|
bitstream.writeBits(11, 0x7ff); // Buffer fullness
|
|
bitstream.writeBits(2, 0); // Number of AAC frames minus 1
|
|
// Omit CRC check
|
|
|
|
return { header, bitstream };
|
|
};
|
|
|
|
export const writeAdtsFrameLength = (bitstream: Bitstream, frameLength: number) => {
|
|
bitstream.pos = 30;
|
|
bitstream.writeBits(13, frameLength);
|
|
};
|