From 9f0613c99f27a30bb35cddc1e399aab6bd150567 Mon Sep 17 00:00:00 2001 From: "Abdullah M. Nazif" Date: Fri, 9 Oct 2026 01:15:35 +0800 Subject: [PATCH] fix(cli): normalize the audio passage the clip plays --- .../src/helpers/loudness.test.ts | 33 +++++++++++++++++++ .../studio-server/src/helpers/loudness.ts | 16 +++++---- 2 files changed, 43 insertions(+), 6 deletions(-) diff --git a/packages/studio-server/src/helpers/loudness.test.ts b/packages/studio-server/src/helpers/loudness.test.ts index f49d16006fe..898d9093d24 100644 --- a/packages/studio-server/src/helpers/loudness.test.ts +++ b/packages/studio-server/src/helpers/loudness.test.ts @@ -13,6 +13,39 @@ const track = (integratedLufs: number, truePeakDbfs: number, volume = 1) => ({ truePeakDbfs, }); +describe("audioTags playback offsets", () => { + it.each([ + ['data-playback-start="4"', 4], + ['data-playback-start="4" data-media-start="1"', 4], + ['data-playback-start="0" data-media-start="4"', 0], + ['data-playback-start="4" data-media-start="invalid"', 4], + ['data-playback-start="-1" data-media-start="3"', 3], + ['data-playback-start="1.5s" data-media-start="3"', 3], + ['data-playback-start="" data-media-start="3"', 3], + ['data-playback-start="invalid"', 0], + ['data-media-start="3"', 3], + ])("measures the selected in-point for %s", (attributes, expected) => { + const html = ``; + expect(audioTags(html)[0]).toEqual(expect.objectContaining({ mediaStart: expected })); + }); + + it("measures a video from its playback in-point at its authored speed", () => { + const html = ``; + const [tag] = audioTags(html); + expect(tag).toBeDefined(); + if (!tag) throw new Error("missing video"); + const args = loudnessMeasureArgs("talk.mp4", tag); + expect(args.slice(args.indexOf("-ss"), args.indexOf("-i"))).toEqual(["-ss", "4", "-t", "4"]); + }); + + it("preserves the selected in-point when writing the matched gain", () => { + const html = ``; + const changed = updateAudioVolume(html, "voice", 0.5); + expect(changed).toBe(html.replace('data-volume="1"', 'data-volume="0.5"')); + expect(audioTags(changed)[0]).toEqual(expect.objectContaining({ mediaStart: 4, volume: 0.5 })); + }); +}); + describe("audioTags with video", () => { it("includes a video with sound as a normalizable clip", () => { const html = ``; diff --git a/packages/studio-server/src/helpers/loudness.ts b/packages/studio-server/src/helpers/loudness.ts index bdff307b65b..018569307cb 100644 --- a/packages/studio-server/src/helpers/loudness.ts +++ b/packages/studio-server/src/helpers/loudness.ts @@ -4,6 +4,7 @@ import { promisify } from "node:util"; import { isAudibleVideoElement } from "@hyperframes/core/audible-video"; import { formatAudioGain, MAX_AUDIO_GAIN_DB } from "@hyperframes/core/audio-gain"; import { findFfBinary } from "@hyperframes/parsers/ff-binaries"; +import { readMediaOffsetSeconds } from "@hyperframes/parsers/media-duration"; import { requestedProjectPath } from "./requestSubPath.js"; const execFileAsync = promisify(execFile); @@ -253,6 +254,14 @@ function requiredAudioIdentity(attributes: Map) { return { id, src }; } +function authoredMediaStart(attributes: Map, id: string): number { + return readMediaOffsetSeconds((name) => { + const raw = attributes.get(name)?.value; + if (name === "data-media-start") authoredNumber(raw, 0, name, id); + return raw; + }); +} + /** Boolean attributes carry no `=`, so the value map cannot see them. */ function hasBareAttribute(html: string, range: TagRange, name: string): boolean { const unquoted = html.slice(range.start, range.end).replace(/"[^"]*"|'[^']*'/g, '""'); @@ -298,12 +307,7 @@ function mediaTagFromRange(html: string, range: MediaTagRange): AudioTag | null id, src, volume: authoredNumber(volumeAttribute?.value, 1, "data-volume", id), - mediaStart: authoredNumber( - attributes.get("data-media-start")?.value, - 0, - "data-media-start", - id, - ), + mediaStart: authoredMediaStart(attributes, id), duration: authoredDuration(attributes, id), playbackRate: authoredNumber( attributes.get("data-playback-rate")?.value,