Use decoder information for resampler sample rate, fix Opus sample rate to 48 kHz everywhere

This commit is contained in:
Vanilagy
2025-09-10 23:03:20 +02:00
parent f314f04037
commit 6f06631355
12 changed files with 52 additions and 40 deletions
+6 -3
View File
@@ -24,7 +24,7 @@
chunked: true, chunked: true,
chunkSize: 2**20 chunkSize: 2**20
}); });
const outputFormat = new Mediabunny.Mp4OutputFormat({}); const outputFormat = new Mediabunny.WavOutputFormat({});
const button = document.createElement('button'); const button = document.createElement('button');
button.textContent = 'Cancel'; button.textContent = 'Cancel';
@@ -72,6 +72,9 @@
}), }),
output, output,
audio: { audio: {
codec: 'pcm-s16',
//sampleRate: 16000,
//numberOfChannels: 1,
//discard: true, //discard: true,
//codec: 'opus', //codec: 'opus',
//bitrate: 128000, //bitrate: 128000,
@@ -106,7 +109,7 @@
*/ */
video: () => ({ video: () => ({
//discard: true, //discard: true,
forceTranscode: true, //forceTranscode: true,
//codec: 'avc', //codec: 'avc',
//fit: 'contain', //fit: 'contain',
//frameRate: 27.123, //frameRate: 27.123,
@@ -129,7 +132,7 @@
//height: 100, //height: 100,
}), }),
trim: { trim: {
start: 1, start: 0,
end: 10 end: 10
}, },
}); });
+5 -3
View File
@@ -14,10 +14,12 @@
source: new Mediabunny.BlobSource(file), source: new Mediabunny.BlobSource(file),
}); });
const videoTrack = await input.getPrimaryVideoTrack(); const audioTrack = await input.getPrimaryAudioTrack();
const sink = new Mediabunny.VideoSampleSink(videoTrack); const sink = new Mediabunny.EncodedPacketSink(audioTrack);
console.log(await sink.getSample(2.131875)); for await (const packet of sink.packets()) {
console.log(packet);
}
/* /*
const sink = new Mediabunny.EncodedPacketSink(videoTrack); const sink = new Mediabunny.EncodedPacketSink(videoTrack);
+6 -6
View File
@@ -1,12 +1,12 @@
{ {
"name": "mediabunny", "name": "mediabunny",
"version": "1.14.3", "version": "1.14.4",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "mediabunny", "name": "mediabunny",
"version": "1.14.3", "version": "1.14.4",
"license": "MPL-2.0", "license": "MPL-2.0",
"workspaces": [ "workspaces": [
"packages/*" "packages/*"
@@ -7749,9 +7749,9 @@
} }
}, },
"node_modules/mediabunny": { "node_modules/mediabunny": {
"version": "1.14.2", "version": "1.14.3",
"resolved": "https://registry.npmjs.org/mediabunny/-/mediabunny-1.14.2.tgz", "resolved": "https://registry.npmjs.org/mediabunny/-/mediabunny-1.14.3.tgz",
"integrity": "sha512-JCup3rpsJGIw3I7E7bZSmVEANCr/OwfwkH3g/Wq1TcHEJ7ymLLw+jC3+/Nnn/JLXzFnw8/I6fV3xECLnFedfHg==", "integrity": "sha512-kCvieRo6X1QDcdWLjn7o2BY/VCDeyU9nNGBVjOIOiWPoTtIekHR+viKAYaZafLEm0poBv+O2PwttL37PaSo/kA==",
"license": "MPL-2.0", "license": "MPL-2.0",
"peer": true, "peer": true,
"workspaces": [ "workspaces": [
@@ -12242,7 +12242,7 @@
}, },
"packages/mp3-encoder": { "packages/mp3-encoder": {
"name": "@mediabunny/mp3-encoder", "name": "@mediabunny/mp3-encoder",
"version": "1.14.3", "version": "1.14.4",
"license": "MPL-2.0", "license": "MPL-2.0",
"devDependencies": { "devDependencies": {
"@types/emscripten": "^1.40.1" "@types/emscripten": "^1.40.1"
+1 -1
View File
@@ -1,7 +1,7 @@
{ {
"name": "mediabunny", "name": "mediabunny",
"author": "Vanilagy", "author": "Vanilagy",
"version": "1.14.3", "version": "1.14.4",
"description": "Pure TypeScript media toolkit for reading, writing, and converting media files, directly in the browser.", "description": "Pure TypeScript media toolkit for reading, writing, and converting media files, directly in the browser.",
"type": "module", "type": "module",
"workspaces": [ "workspaces": [
+1 -1
View File
@@ -1,7 +1,7 @@
{ {
"name": "@mediabunny/mp3-encoder", "name": "@mediabunny/mp3-encoder",
"author": "Vanilagy", "author": "Vanilagy",
"version": "1.14.3", "version": "1.14.4",
"description": "MP3 encoder extension for Mediabunny, based on LAME.", "description": "MP3 encoder extension for Mediabunny, based on LAME.",
"main": "./dist/bundles/mediabunny-mp3-encoder.mjs", "main": "./dist/bundles/mediabunny-mp3-encoder.mjs",
"module": "./dist/bundles/mediabunny-mp3-encoder.mjs", "module": "./dist/bundles/mediabunny-mp3-encoder.mjs",
+1 -1
View File
@@ -621,7 +621,7 @@ export const parseAacAudioSpecificConfig = (bytes: Uint8Array | null): AacAudioS
}; };
}; };
export const OPUS_INTERNAL_SAMPLE_RATE = 48000; export const OPUS_SAMPLE_RATE = 48_000;
const PCM_CODEC_REGEX = /^pcm-([usf])(\d+)+(be)?$/; const PCM_CODEC_REGEX = /^pcm-([usf])(\d+)+(be)?$/;
+17 -17
View File
@@ -1125,8 +1125,6 @@ export class Conversion {
await this._started; await this._started;
const resampler = new AudioResampler({ const resampler = new AudioResampler({
sourceNumberOfChannels: track.numberOfChannels,
sourceSampleRate: track.sampleRate,
targetNumberOfChannels, targetNumberOfChannels,
targetSampleRate, targetSampleRate,
startTime: this._startTimestamp, startTime: this._startTimestamp,
@@ -1246,9 +1244,9 @@ class TrackSynchronizer {
* OfflineAudioContext. * OfflineAudioContext.
*/ */
export class AudioResampler { export class AudioResampler {
sourceSampleRate: number; sourceSampleRate: number | null = null;
targetSampleRate: number; targetSampleRate: number;
sourceNumberOfChannels: number; sourceNumberOfChannels: number | null = null;
targetNumberOfChannels: number; targetNumberOfChannels: number;
startTime: number; startTime: number;
endTime: number; endTime: number;
@@ -1262,20 +1260,16 @@ export class AudioResampler {
/** The highest index written to in the current buffer */ /** The highest index written to in the current buffer */
maxWrittenFrame: number; maxWrittenFrame: number;
channelMixer!: (sourceData: Float32Array, sourceFrameIndex: number, targetChannelIndex: number) => number; channelMixer!: (sourceData: Float32Array, sourceFrameIndex: number, targetChannelIndex: number) => number;
tempSourceBuffer: Float32Array; tempSourceBuffer!: Float32Array;
constructor(options: { constructor(options: {
sourceSampleRate: number;
targetSampleRate: number; targetSampleRate: number;
sourceNumberOfChannels: number;
targetNumberOfChannels: number; targetNumberOfChannels: number;
startTime: number; startTime: number;
endTime: number; endTime: number;
onSample: (sample: AudioSample) => Promise<void>; onSample: (sample: AudioSample) => Promise<void>;
}) { }) {
this.sourceSampleRate = options.sourceSampleRate;
this.targetSampleRate = options.targetSampleRate; this.targetSampleRate = options.targetSampleRate;
this.sourceNumberOfChannels = options.sourceNumberOfChannels;
this.targetNumberOfChannels = options.targetNumberOfChannels; this.targetNumberOfChannels = options.targetNumberOfChannels;
this.startTime = options.startTime; this.startTime = options.startTime;
this.endTime = options.endTime; this.endTime = options.endTime;
@@ -1287,17 +1281,14 @@ export class AudioResampler {
this.outputBuffer = new Float32Array(this.bufferSizeInSamples); this.outputBuffer = new Float32Array(this.bufferSizeInSamples);
this.bufferStartFrame = 0; this.bufferStartFrame = 0;
this.maxWrittenFrame = -1; this.maxWrittenFrame = -1;
this.setupChannelMixer();
// Pre-allocate temporary buffer for source data
this.tempSourceBuffer = new Float32Array(this.sourceSampleRate * this.sourceNumberOfChannels);
} }
/** /**
* Sets up the channel mixer to handle up/downmixing in the case where input and output channel counts don't match. * Sets up the channel mixer to handle up/downmixing in the case where input and output channel counts don't match.
*/ */
setupChannelMixer(): void { doChannelMixerSetup(): void {
assert(this.sourceNumberOfChannels !== null);
const sourceNum = this.sourceNumberOfChannels; const sourceNum = this.sourceNumberOfChannels;
const targetNum = this.targetNumberOfChannels; const targetNum = this.targetNumberOfChannels;
@@ -1415,8 +1406,17 @@ export class AudioResampler {
} }
async add(audioSample: AudioSample) { async add(audioSample: AudioSample) {
if (!audioSample || audioSample._closed) { if (this.sourceSampleRate === null) {
return; // This is the first sample, so let's init the missing data. Initting the sample rate from the decoded
// sample is more reliable than using the file's metadata, because decoders are free to emit any sample rate
// they see fit.
this.sourceSampleRate = audioSample.sampleRate;
this.sourceNumberOfChannels = audioSample.numberOfChannels;
// Pre-allocate temporary buffer for source data
this.tempSourceBuffer = new Float32Array(this.sourceSampleRate * this.sourceNumberOfChannels);
this.doChannelMixerSetup();
} }
const requiredSamples = audioSample.numberOfFrames * audioSample.numberOfChannels; const requiredSamples = audioSample.numberOfFrames * audioSample.numberOfChannels;
+6 -1
View File
@@ -12,6 +12,7 @@ import {
extractAudioCodecString, extractAudioCodecString,
extractVideoCodecString, extractVideoCodecString,
MediaCodec, MediaCodec,
OPUS_SAMPLE_RATE,
parseAacAudioSpecificConfig, parseAacAudioSpecificConfig,
parsePcmCodec, parsePcmCodec,
PCM_AUDIO_CODECS, PCM_AUDIO_CODECS,
@@ -1049,6 +1050,10 @@ export class IsobmffDemuxer extends Demuxer {
} }
} }
if (track.info.codec === 'opus') {
sampleRate = OPUS_SAMPLE_RATE; // Always the same
}
track.info.numberOfChannels = channelCount; track.info.numberOfChannels = channelCount;
track.info.sampleRate = sampleRate; track.info.sampleRate = sampleRate;
@@ -1421,7 +1426,7 @@ export class IsobmffDemuxer extends Demuxer {
track.info.codecDescription = description; track.info.codecDescription = description;
track.info.numberOfChannels = outputChannelCount; track.info.numberOfChannels = outputChannelCount;
track.info.sampleRate = inputSampleRate; // Don't copy the input sample rate, irrelevant, and output sample rate is fixed
}; break; }; break;
case 'dfLa': { // Used for FLAC audio case 'dfLa': { // Used for FLAC audio
+2
View File
@@ -18,6 +18,7 @@ import {
extractAudioCodecString, extractAudioCodecString,
extractVideoCodecString, extractVideoCodecString,
MediaCodec, MediaCodec,
OPUS_SAMPLE_RATE,
VideoCodec, VideoCodec,
} from '../codec'; } from '../codec';
import { Demuxer } from '../demuxer'; import { Demuxer } from '../demuxer';
@@ -988,6 +989,7 @@ export class MatroskaDemuxer extends Demuxer {
} else if (codecIdWithoutSuffix === CODEC_STRING_MAP.opus) { } else if (codecIdWithoutSuffix === CODEC_STRING_MAP.opus) {
this.currentTrack.info.codec = 'opus'; this.currentTrack.info.codec = 'opus';
this.currentTrack.info.codecDescription = this.currentTrack.codecPrivate; this.currentTrack.info.codecDescription = this.currentTrack.codecPrivate;
this.currentTrack.info.sampleRate = OPUS_SAMPLE_RATE; // Always the same
} else if (codecIdWithoutSuffix === CODEC_STRING_MAP.vorbis) { } else if (codecIdWithoutSuffix === CODEC_STRING_MAP.vorbis) {
this.currentTrack.info.codec = 'vorbis'; this.currentTrack.info.codec = 'vorbis';
this.currentTrack.info.codecDescription = this.currentTrack.codecPrivate; this.currentTrack.info.codecDescription = this.currentTrack.codecPrivate;
+2 -2
View File
@@ -47,7 +47,7 @@ import {
parseSubtitleTimestamp, parseSubtitleTimestamp,
} from '../subtitles'; } from '../subtitles';
import { import {
OPUS_INTERNAL_SAMPLE_RATE, OPUS_SAMPLE_RATE,
PCM_AUDIO_CODECS, PCM_AUDIO_CODECS,
PcmAudioCodec, PcmAudioCodec,
SubtitleCodec, SubtitleCodec,
@@ -293,7 +293,7 @@ export class MatroskaMuxer extends Muxer {
const header = parseOpusIdentificationHeader(bytes); const header = parseOpusIdentificationHeader(bytes);
// Use the preSkip value from the header // Use the preSkip value from the header
seekPreRollNs = Math.round(1e9 * (header.preSkip / OPUS_INTERNAL_SAMPLE_RATE)); seekPreRollNs = Math.round(1e9 * (header.preSkip / OPUS_SAMPLE_RATE));
} }
} }
+3 -3
View File
@@ -6,7 +6,7 @@
* file, You can obtain one at https://mozilla.org/MPL/2.0/. * file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/ */
import { OPUS_INTERNAL_SAMPLE_RATE } from '../codec'; import { OPUS_SAMPLE_RATE } from '../codec';
import { parseModesFromVorbisSetupPacket, parseOpusIdentificationHeader } from '../codec-data'; import { parseModesFromVorbisSetupPacket, parseOpusIdentificationHeader } from '../codec-data';
import { Demuxer } from '../demuxer'; import { Demuxer } from '../demuxer';
import { Input } from '../input'; import { Input } from '../input';
@@ -249,7 +249,7 @@ export class OggDemuxer extends Demuxer {
const header = parseOpusIdentificationHeader(firstPacket.data); const header = parseOpusIdentificationHeader(firstPacket.data);
bitstream.numberOfChannels = header.outputChannelCount; bitstream.numberOfChannels = header.outputChannelCount;
bitstream.sampleRate = header.inputSampleRate; bitstream.sampleRate = OPUS_SAMPLE_RATE; // Always the same
bitstream.codecInfo.opusInfo = { bitstream.codecInfo.opusInfo = {
preSkip: header.preSkip, preSkip: header.preSkip,
@@ -574,7 +574,7 @@ class OggAudioTrackBacking implements InputAudioTrackBacking {
constructor(public bitstream: LogicalBitstream, public demuxer: OggDemuxer) { constructor(public bitstream: LogicalBitstream, public demuxer: OggDemuxer) {
// Opus always uses a fixed sample rate for its internal calculations, even if the actual rate is different // Opus always uses a fixed sample rate for its internal calculations, even if the actual rate is different
this.internalSampleRate = bitstream.codecInfo.codec === 'opus' this.internalSampleRate = bitstream.codecInfo.codec === 'opus'
? OPUS_INTERNAL_SAMPLE_RATE ? OPUS_SAMPLE_RATE
: bitstream.sampleRate; : bitstream.sampleRate;
} }
+2 -2
View File
@@ -6,7 +6,7 @@
* file, You can obtain one at https://mozilla.org/MPL/2.0/. * file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/ */
import { OPUS_INTERNAL_SAMPLE_RATE, validateAudioChunkMetadata } from '../codec'; import { OPUS_SAMPLE_RATE, validateAudioChunkMetadata } from '../codec';
import { parseModesFromVorbisSetupPacket, parseOpusIdentificationHeader } from '../codec-data'; import { parseModesFromVorbisSetupPacket, parseOpusIdentificationHeader } from '../codec-data';
import { import {
assert, assert,
@@ -119,7 +119,7 @@ export class OggMuxer extends Muxer {
track, track,
serialNumber, serialNumber,
internalSampleRate: track.source._codec === 'opus' internalSampleRate: track.source._codec === 'opus'
? OPUS_INTERNAL_SAMPLE_RATE ? OPUS_SAMPLE_RATE
: meta.decoderConfig.sampleRate, : meta.decoderConfig.sampleRate,
codecInfo: { codecInfo: {
codec: track.source._codec, codec: track.source._codec,