From 3cde68e0ae808e84fe94082833a89b1a38f5512c Mon Sep 17 00:00:00 2001 From: Vanilagy <1696106+Vanilagy@users.noreply.github.com> Date: Sat, 9 May 2026 00:40:25 +0200 Subject: [PATCH] Fix AudioResampler emitting timestamps before startTimestamp (fixes #366) --- dev/convert.html | 9 ++++++--- src/conversion.ts | 1 + src/resample.ts | 11 +++++++---- test/browser/conversion.test.ts | 29 ++++++++++++++++++++++++++++- 4 files changed, 42 insertions(+), 8 deletions(-) diff --git a/dev/convert.html b/dev/convert.html index f85cc61..c94dbb9 100644 --- a/dev/convert.html +++ b/dev/convert.html @@ -96,7 +96,8 @@ input, output, audio: { - discard: true, + codec: 'aac', + forceTranscode: true, //forceTranscode: true, //sampleFormat: 's16', }, @@ -125,7 +126,7 @@ }, */ video: { - //discard: true, + discard: true, }, tags: {} ?? { title: 'Bigggy', @@ -145,7 +146,9 @@ } }, trim: { - end: 10, + start: 300.14984567374756 - 100, + end: 310.1548298151939 - 100, + //end: 10, //start: startTime, //end: startTime + 2, }, diff --git a/src/conversion.ts b/src/conversion.ts index 8c3182b..584ec71 100644 --- a/src/conversion.ts +++ b/src/conversion.ts @@ -1889,6 +1889,7 @@ export class Conversion { startTime: this._startTimestamp, endTime: this._endTimestamp, onSample: async (sample) => { + assert(sample.timestamp >= this._startTimestamp); sample.setTimestamp(sample.timestamp - this._startTimestamp); await this._registerAudioSample(trackOptions, outputTrackId, source, sample); diff --git a/src/resample.ts b/src/resample.ts index 727492d..4686a9d 100644 --- a/src/resample.ts +++ b/src/resample.ts @@ -20,7 +20,6 @@ export class AudioResampler { targetSampleRate: number; sourceNumberOfChannels: number | null = null; targetNumberOfChannels: number; - startTime: number; endTime: number; onSample: (sample: AudioSample) => Promise; @@ -33,6 +32,7 @@ export class AudioResampler { maxWrittenFrame: number | null = null; channelMixer!: (sourceData: Float32Array, sourceFrameIndex: number, targetChannelIndex: number) => number; tempSourceBuffer!: Float32Array; + timestampOffset: number; constructor(options: { targetSampleRate: number; @@ -43,7 +43,6 @@ export class AudioResampler { }) { this.targetSampleRate = options.targetSampleRate; this.targetNumberOfChannels = options.targetNumberOfChannels; - this.startTime = options.startTime; this.endTime = options.endTime; this.onSample = options.onSample; @@ -51,7 +50,11 @@ export class AudioResampler { this.bufferSizeInSamples = this.bufferSizeInFrames * this.targetNumberOfChannels; this.outputBuffer = new Float32Array(this.bufferSizeInSamples); - this.bufferStartFrame = Math.floor(this.startTime * this.targetSampleRate); + + this.bufferStartFrame = Math.floor(options.startTime * this.targetSampleRate); + // Set to ensure that if the buffer start frame lands on a fractional sample, that the first timestamp still + // comes out as exactly startTime + this.timestampOffset = options.startTime - this.bufferStartFrame / this.targetSampleRate; } /** @@ -273,7 +276,7 @@ export class AudioResampler { format: 'f32', sampleRate: this.targetSampleRate, numberOfChannels: this.targetNumberOfChannels, - timestamp: timestampSeconds, + timestamp: timestampSeconds + this.timestampOffset, data: outputData, }); diff --git a/test/browser/conversion.test.ts b/test/browser/conversion.test.ts index 561fe4f..cce12bb 100644 --- a/test/browser/conversion.test.ts +++ b/test/browser/conversion.test.ts @@ -1,6 +1,6 @@ import { ALL_FORMATS } from '../../src/input-format.js'; import { Input } from '../../src/input.js'; -import { AdtsOutputFormat, HlsOutputFormat, Mp4OutputFormat, MpegTsOutputFormat } from '../../src/output-format.js'; +import { AdtsOutputFormat, HlsOutputFormat, Mp4OutputFormat, MpegTsOutputFormat, WavOutputFormat } from '../../src/output-format.js'; import { Output, OutputTrackGroup } from '../../src/output.js'; import { BufferSource, CustomPathedSource, UrlSource } from '../../src/source.js'; import { expect, test } from 'vitest'; @@ -369,3 +369,30 @@ test('HLS track assignability can be overridden', async () => { expect(newMasterPlayist).not.toBe(masterPlayist); expect(newMasterPlayist.match(/\.m3u8/g)?.length).toBe(1); }); + +test('Fractional audio sample boundary', async () => { + using input = new Input({ + source: new UrlSource('/trim-buck-bunny-ffmpeg.ts'), + formats: ALL_FORMATS, + }); + + const output = new Output({ + format: new WavOutputFormat(), + target: new BufferTarget(), + }); + + const conversion = await Conversion.init({ + input, + output, + video: { + discard: true, + }, + audio: { + forceTranscode: true, + }, + trim: { + start: 0.4 / 48000, + }, + }); + await conversion.execute(); +});