From 6f0663135505965c0c120e98707ab2f07c6f65d9 Mon Sep 17 00:00:00 2001 From: Vanilagy <1696106+Vanilagy@users.noreply.github.com> Date: Wed, 10 Sep 2025 23:03:20 +0200 Subject: [PATCH] Use decoder information for resampler sample rate, fix Opus sample rate to 48 kHz everywhere --- dev/convert.html | 9 +++++--- dev/demux.html | 8 +++++--- package-lock.json | 12 +++++------ package.json | 2 +- packages/mp3-encoder/package.json | 2 +- src/codec.ts | 2 +- src/conversion.ts | 34 +++++++++++++++---------------- src/isobmff/isobmff-demuxer.ts | 7 ++++++- src/matroska/matroska-demuxer.ts | 2 ++ src/matroska/matroska-muxer.ts | 4 ++-- src/ogg/ogg-demuxer.ts | 6 +++--- src/ogg/ogg-muxer.ts | 4 ++-- 12 files changed, 52 insertions(+), 40 deletions(-) diff --git a/dev/convert.html b/dev/convert.html index 7bc9ffe..a50cbd5 100644 --- a/dev/convert.html +++ b/dev/convert.html @@ -24,7 +24,7 @@ chunked: true, chunkSize: 2**20 }); - const outputFormat = new Mediabunny.Mp4OutputFormat({}); + const outputFormat = new Mediabunny.WavOutputFormat({}); const button = document.createElement('button'); button.textContent = 'Cancel'; @@ -72,6 +72,9 @@ }), output, audio: { + codec: 'pcm-s16', + //sampleRate: 16000, + //numberOfChannels: 1, //discard: true, //codec: 'opus', //bitrate: 128000, @@ -106,7 +109,7 @@ */ video: () => ({ //discard: true, - forceTranscode: true, + //forceTranscode: true, //codec: 'avc', //fit: 'contain', //frameRate: 27.123, @@ -129,7 +132,7 @@ //height: 100, }), trim: { - start: 1, + start: 0, end: 10 }, }); diff --git a/dev/demux.html b/dev/demux.html index d787459..b844f7d 100644 --- a/dev/demux.html +++ b/dev/demux.html @@ -14,10 +14,12 @@ source: new Mediabunny.BlobSource(file), }); - const videoTrack = await input.getPrimaryVideoTrack(); - const sink = new Mediabunny.VideoSampleSink(videoTrack); + const audioTrack = await input.getPrimaryAudioTrack(); + 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); diff --git a/package-lock.json b/package-lock.json index 6d7ca5d..72f8bfe 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "mediabunny", - "version": "1.14.3", + "version": "1.14.4", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "mediabunny", - "version": "1.14.3", + "version": "1.14.4", "license": "MPL-2.0", "workspaces": [ "packages/*" @@ -7749,9 +7749,9 @@ } }, "node_modules/mediabunny": { - "version": "1.14.2", - "resolved": "https://registry.npmjs.org/mediabunny/-/mediabunny-1.14.2.tgz", - "integrity": "sha512-JCup3rpsJGIw3I7E7bZSmVEANCr/OwfwkH3g/Wq1TcHEJ7ymLLw+jC3+/Nnn/JLXzFnw8/I6fV3xECLnFedfHg==", + "version": "1.14.3", + "resolved": "https://registry.npmjs.org/mediabunny/-/mediabunny-1.14.3.tgz", + "integrity": "sha512-kCvieRo6X1QDcdWLjn7o2BY/VCDeyU9nNGBVjOIOiWPoTtIekHR+viKAYaZafLEm0poBv+O2PwttL37PaSo/kA==", "license": "MPL-2.0", "peer": true, "workspaces": [ @@ -12242,7 +12242,7 @@ }, "packages/mp3-encoder": { "name": "@mediabunny/mp3-encoder", - "version": "1.14.3", + "version": "1.14.4", "license": "MPL-2.0", "devDependencies": { "@types/emscripten": "^1.40.1" diff --git a/package.json b/package.json index 5ef01e4..0ffae19 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "mediabunny", "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.", "type": "module", "workspaces": [ diff --git a/packages/mp3-encoder/package.json b/packages/mp3-encoder/package.json index 14cb8af..cc977a9 100644 --- a/packages/mp3-encoder/package.json +++ b/packages/mp3-encoder/package.json @@ -1,7 +1,7 @@ { "name": "@mediabunny/mp3-encoder", "author": "Vanilagy", - "version": "1.14.3", + "version": "1.14.4", "description": "MP3 encoder extension for Mediabunny, based on LAME.", "main": "./dist/bundles/mediabunny-mp3-encoder.mjs", "module": "./dist/bundles/mediabunny-mp3-encoder.mjs", diff --git a/src/codec.ts b/src/codec.ts index 119f7cb..2e90cfd 100644 --- a/src/codec.ts +++ b/src/codec.ts @@ -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)?$/; diff --git a/src/conversion.ts b/src/conversion.ts index 19ab3f9..0192590 100644 --- a/src/conversion.ts +++ b/src/conversion.ts @@ -1125,8 +1125,6 @@ export class Conversion { await this._started; const resampler = new AudioResampler({ - sourceNumberOfChannels: track.numberOfChannels, - sourceSampleRate: track.sampleRate, targetNumberOfChannels, targetSampleRate, startTime: this._startTimestamp, @@ -1246,9 +1244,9 @@ class TrackSynchronizer { * OfflineAudioContext. */ export class AudioResampler { - sourceSampleRate: number; + sourceSampleRate: number | null = null; targetSampleRate: number; - sourceNumberOfChannels: number; + sourceNumberOfChannels: number | null = null; targetNumberOfChannels: number; startTime: number; endTime: number; @@ -1262,20 +1260,16 @@ export class AudioResampler { /** The highest index written to in the current buffer */ maxWrittenFrame: number; channelMixer!: (sourceData: Float32Array, sourceFrameIndex: number, targetChannelIndex: number) => number; - tempSourceBuffer: Float32Array; + tempSourceBuffer!: Float32Array; constructor(options: { - sourceSampleRate: number; targetSampleRate: number; - sourceNumberOfChannels: number; targetNumberOfChannels: number; startTime: number; endTime: number; onSample: (sample: AudioSample) => Promise; }) { - this.sourceSampleRate = options.sourceSampleRate; this.targetSampleRate = options.targetSampleRate; - this.sourceNumberOfChannels = options.sourceNumberOfChannels; this.targetNumberOfChannels = options.targetNumberOfChannels; this.startTime = options.startTime; this.endTime = options.endTime; @@ -1287,17 +1281,14 @@ export class AudioResampler { this.outputBuffer = new Float32Array(this.bufferSizeInSamples); this.bufferStartFrame = 0; 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. */ - setupChannelMixer(): void { + doChannelMixerSetup(): void { + assert(this.sourceNumberOfChannels !== null); + const sourceNum = this.sourceNumberOfChannels; const targetNum = this.targetNumberOfChannels; @@ -1415,8 +1406,17 @@ export class AudioResampler { } async add(audioSample: AudioSample) { - if (!audioSample || audioSample._closed) { - return; + if (this.sourceSampleRate === null) { + // 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; diff --git a/src/isobmff/isobmff-demuxer.ts b/src/isobmff/isobmff-demuxer.ts index fddbb3e..84fce37 100644 --- a/src/isobmff/isobmff-demuxer.ts +++ b/src/isobmff/isobmff-demuxer.ts @@ -12,6 +12,7 @@ import { extractAudioCodecString, extractVideoCodecString, MediaCodec, + OPUS_SAMPLE_RATE, parseAacAudioSpecificConfig, parsePcmCodec, 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.sampleRate = sampleRate; @@ -1421,7 +1426,7 @@ export class IsobmffDemuxer extends Demuxer { track.info.codecDescription = description; track.info.numberOfChannels = outputChannelCount; - track.info.sampleRate = inputSampleRate; + // Don't copy the input sample rate, irrelevant, and output sample rate is fixed }; break; case 'dfLa': { // Used for FLAC audio diff --git a/src/matroska/matroska-demuxer.ts b/src/matroska/matroska-demuxer.ts index 66a0455..bda341e 100644 --- a/src/matroska/matroska-demuxer.ts +++ b/src/matroska/matroska-demuxer.ts @@ -18,6 +18,7 @@ import { extractAudioCodecString, extractVideoCodecString, MediaCodec, + OPUS_SAMPLE_RATE, VideoCodec, } from '../codec'; import { Demuxer } from '../demuxer'; @@ -988,6 +989,7 @@ export class MatroskaDemuxer extends Demuxer { } else if (codecIdWithoutSuffix === CODEC_STRING_MAP.opus) { this.currentTrack.info.codec = 'opus'; this.currentTrack.info.codecDescription = this.currentTrack.codecPrivate; + this.currentTrack.info.sampleRate = OPUS_SAMPLE_RATE; // Always the same } else if (codecIdWithoutSuffix === CODEC_STRING_MAP.vorbis) { this.currentTrack.info.codec = 'vorbis'; this.currentTrack.info.codecDescription = this.currentTrack.codecPrivate; diff --git a/src/matroska/matroska-muxer.ts b/src/matroska/matroska-muxer.ts index d878f6e..93f052c 100644 --- a/src/matroska/matroska-muxer.ts +++ b/src/matroska/matroska-muxer.ts @@ -47,7 +47,7 @@ import { parseSubtitleTimestamp, } from '../subtitles'; import { - OPUS_INTERNAL_SAMPLE_RATE, + OPUS_SAMPLE_RATE, PCM_AUDIO_CODECS, PcmAudioCodec, SubtitleCodec, @@ -293,7 +293,7 @@ export class MatroskaMuxer extends Muxer { const header = parseOpusIdentificationHeader(bytes); // 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)); } } diff --git a/src/ogg/ogg-demuxer.ts b/src/ogg/ogg-demuxer.ts index 940a5d1..264eabf 100644 --- a/src/ogg/ogg-demuxer.ts +++ b/src/ogg/ogg-demuxer.ts @@ -6,7 +6,7 @@ * 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 { Demuxer } from '../demuxer'; import { Input } from '../input'; @@ -249,7 +249,7 @@ export class OggDemuxer extends Demuxer { const header = parseOpusIdentificationHeader(firstPacket.data); bitstream.numberOfChannels = header.outputChannelCount; - bitstream.sampleRate = header.inputSampleRate; + bitstream.sampleRate = OPUS_SAMPLE_RATE; // Always the same bitstream.codecInfo.opusInfo = { preSkip: header.preSkip, @@ -574,7 +574,7 @@ class OggAudioTrackBacking implements InputAudioTrackBacking { 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 this.internalSampleRate = bitstream.codecInfo.codec === 'opus' - ? OPUS_INTERNAL_SAMPLE_RATE + ? OPUS_SAMPLE_RATE : bitstream.sampleRate; } diff --git a/src/ogg/ogg-muxer.ts b/src/ogg/ogg-muxer.ts index 05f79a7..8b7de06 100644 --- a/src/ogg/ogg-muxer.ts +++ b/src/ogg/ogg-muxer.ts @@ -6,7 +6,7 @@ * 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 { assert, @@ -119,7 +119,7 @@ export class OggMuxer extends Muxer { track, serialNumber, internalSampleRate: track.source._codec === 'opus' - ? OPUS_INTERNAL_SAMPLE_RATE + ? OPUS_SAMPLE_RATE : meta.decoderConfig.sampleRate, codecInfo: { codec: track.source._codec,