@hyperframes/studio-server 0.8.111 → 0.8.113
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
|
@@ -303,7 +303,8 @@ async function measureAudio(ffmpegPath, file, tag) {
|
|
|
303
303
|
const result = await execFileAsync(ffmpegPath, loudnessMeasureArgs(file, tag), {
|
|
304
304
|
encoding: "utf8",
|
|
305
305
|
maxBuffer: 16 * 1024 * 1024,
|
|
306
|
-
timeout: FFMPEG_TIMEOUT_MS
|
|
306
|
+
timeout: FFMPEG_TIMEOUT_MS,
|
|
307
|
+
windowsHide: true
|
|
307
308
|
});
|
|
308
309
|
return parseEbur128Summary(result.stderr);
|
|
309
310
|
}
|
|
@@ -358,4 +359,4 @@ export {
|
|
|
358
359
|
NORMALIZE_TRUE_PEAK_CEILING_DBFS,
|
|
359
360
|
absoluteLoudnessPlan
|
|
360
361
|
};
|
|
361
|
-
//# sourceMappingURL=chunk-
|
|
362
|
+
//# sourceMappingURL=chunk-DSVQ2U7J.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/helpers/loudness.ts"],"sourcesContent":["import { execFile } from \"node:child_process\";\nimport { isAbsolute, relative, resolve } from \"node:path\";\nimport { promisify } from \"node:util\";\nimport { isAudibleVideoElement } from \"@hyperframes/core/audible-video\";\nimport { formatAudioGain, MAX_AUDIO_GAIN_DB } from \"@hyperframes/core/audio-gain\";\nimport { findFfBinary } from \"@hyperframes/parsers/ff-binaries\";\n\nconst execFileAsync = promisify(execFile);\n\nconst FFMPEG_TIMEOUT_MS = 120_000;\n\ninterface AttributeSpan {\n value: string;\n valueStart: number;\n valueEnd: number;\n}\n\ninterface TagRange {\n start: number;\n end: number;\n}\n\ntype MediaTagName = \"audio\" | \"video\";\n\ninterface MediaTagRange extends TagRange {\n name: MediaTagName;\n}\n\ninterface ParsedStartTag extends TagRange {\n closing: boolean;\n name: string;\n}\n\ninterface ScanResult {\n cursor: number;\n mediaRange: MediaTagRange | null;\n}\n\nexport interface AudioTag {\n tag: MediaTagName;\n id: string;\n src: string;\n volume: number;\n mediaStart: number;\n duration: number | null;\n playbackRate: number;\n start: number;\n end: number;\n volumeAttribute: AttributeSpan | null;\n}\n\nexport interface LoudnessMeasurement {\n integratedLufs: number;\n truePeakDbfs: number;\n}\n\nexport interface NormalizationTrack extends LoudnessMeasurement {\n id: string;\n volume: number;\n}\n\nexport interface AudioNormalizationPlan {\n referenceId: string;\n targetId: string;\n referenceLufs: number;\n targetLufs: number;\n previousVolume: number;\n volume: number;\n gainDb: number;\n changeDb: number;\n projectedLufs: number;\n projectedTruePeakDbfs: number;\n}\n\nfunction authoredNumber(\n raw: string | undefined,\n fallback: number,\n attribute: string,\n id: string,\n strictlyPositive = false,\n): number {\n if (raw === undefined) return fallback;\n const value = Number(raw);\n if (!Number.isFinite(value) || (strictlyPositive ? value <= 0 : value < 0)) {\n throw new Error(`Audio #${id} has an invalid ${attribute}: ${raw}`);\n }\n return value;\n}\n\nfunction attributeFromMatch(\n match: RegExpMatchArray,\n absoluteOffset: number,\n): [string, AttributeSpan] | null {\n if (match.index === undefined) return null;\n const name = match[1]?.toLowerCase();\n const value = matchedAttributeValue(match);\n if (!name || value === undefined) return null;\n\n const whole = match[0];\n const localValueStart = attributeValueStart(whole);\n const valueStart = absoluteOffset + match.index + localValueStart;\n return [name, { value, valueStart, valueEnd: valueStart + value.length }];\n}\n\nfunction matchedAttributeValue(match: RegExpMatchArray): string | undefined {\n return match[2] ?? match[3] ?? match[4];\n}\n\nfunction attributeValueStart(attribute: string): number {\n let cursor = attribute.indexOf(\"=\") + 1;\n while (/\\s/.test(attribute[cursor] ?? \"\")) cursor += 1;\n const quote = attribute[cursor];\n return quote === '\"' || quote === \"'\" ? cursor + 1 : cursor;\n}\n\nfunction attributesInTag(html: string, from: number, to: number): Map<string, AttributeSpan> {\n const tag = html.slice(from, to);\n const nameEnd = tag.search(/\\s|\\/?\\s*>/);\n const attributes = new Map<string, AttributeSpan>();\n if (nameEnd < 0) return attributes;\n\n const pattern = /([^\\s=/>]+)\\s*=\\s*(?:\"([^\"]*)\"|'([^']*)'|([^\\s\"'=<>`]+))/g;\n for (const match of tag.slice(nameEnd).matchAll(pattern)) {\n const attribute = attributeFromMatch(match, from + nameEnd);\n if (attribute) attributes.set(...attribute);\n }\n return attributes;\n}\n\nfunction startTagEnd(html: string, start: number): number {\n let quote = \"\";\n let end = start + 1;\n for (; end < html.length; end += 1) {\n const char = html[end];\n if (quote) {\n if (char === quote) quote = \"\";\n } else if (char === '\"' || char === \"'\") {\n quote = char;\n } else if (char === \">\") {\n return end + 1;\n }\n }\n throw new Error(\"Unterminated HTML start tag\");\n}\n\nfunction commentEnd(html: string, start: number): number {\n const end = html.indexOf(\"-->\", start + 4);\n if (end < 0) throw new Error(\"Unterminated HTML comment\");\n return end + 3;\n}\n\nconst isSpaceAt = (html: string, index: number) => /\\s/.test(html.charAt(index));\nconst isWordAt = (html: string, index: number) => /\\w/.test(html.charAt(index));\n\nfunction skipSpaces(html: string, index: number): number {\n let cursor = index;\n while (cursor < html.length && isSpaceAt(html, cursor)) cursor++;\n return cursor;\n}\n\nfunction parsedStartTag(html: string, start: number): ParsedStartTag | null {\n let cursor = skipSpaces(html, start + 1);\n const closing = html.charAt(cursor) === \"/\";\n if (closing) cursor = skipSpaces(html, cursor + 1);\n const nameStart = cursor;\n if (!/[a-z]/i.test(html.charAt(cursor))) return null;\n while (/[a-z\\d:-]/i.test(html.charAt(cursor + 1))) cursor++;\n let nameEnd = cursor + 1;\n while (nameEnd > nameStart && isWordAt(html, nameEnd - 1) === isWordAt(html, nameEnd)) nameEnd--;\n if (nameEnd === nameStart) return null;\n return {\n start,\n end: startTagEnd(html, start),\n closing,\n name: html.slice(nameStart, nameEnd).toLowerCase(),\n };\n}\n\nfunction rawTextElementEnd(html: string, lower: string, tag: ParsedStartTag): number | null {\n if (tag.closing || (tag.name !== \"script\" && tag.name !== \"style\")) return null;\n const closeStart = lower.indexOf(`</${tag.name}`, tag.end);\n if (closeStart < 0) throw new Error(`Unterminated <${tag.name}> element`);\n return startTagEnd(html, closeStart);\n}\n\nfunction scanMarkup(html: string, lower: string, start: number): ScanResult {\n if (html.startsWith(\"<!--\", start)) {\n return { cursor: commentEnd(html, start), mediaRange: null };\n }\n const tag = parsedStartTag(html, start);\n if (!tag) {\n const marker = html[start + 1];\n const cursor = marker === \"!\" || marker === \"?\" ? startTagEnd(html, start) : start + 1;\n return { cursor, mediaRange: null };\n }\n const rawTextEnd = rawTextElementEnd(html, lower, tag);\n if (rawTextEnd !== null) return { cursor: rawTextEnd, mediaRange: null };\n return { cursor: tag.end, mediaRange: mediaRangeOf(tag) };\n}\n\nfunction mediaRangeOf(tag: ParsedStartTag): MediaTagRange | null {\n if (tag.closing) return null;\n if (tag.name === \"audio\" || tag.name === \"video\") {\n return { start: tag.start, end: tag.end, name: tag.name };\n }\n return null;\n}\n\nfunction mediaTagRanges(html: string): MediaTagRange[] {\n const lower = html.toLowerCase();\n const ranges: MediaTagRange[] = [];\n let cursor = 0;\n while (cursor < html.length) {\n const start = html.indexOf(\"<\", cursor);\n if (start < 0) break;\n const scanned = scanMarkup(html, lower, start);\n if (scanned.mediaRange) ranges.push(scanned.mediaRange);\n cursor = scanned.cursor;\n }\n return ranges;\n}\n\nfunction trimmedAttribute(attributes: Map<string, AttributeSpan>, name: string): string {\n return attributes.get(name)?.value.trim() ?? \"\";\n}\n\n/**\n * How much of the source the composition actually plays.\n *\n * `data-duration` is the explicit trim; absent, `data-end` still bounds the\n * clip's timeline window, and the parser, the runtime and the render mixer all\n * honour it. Measuring the whole file for a `data-end`-trimmed clip reads a\n * loudness the composition never plays — and `--write` then \"corrects\" an\n * already-matched clip by tens of dB.\n */\nfunction authoredDuration(attributes: Map<string, AttributeSpan>, id: string): number | null {\n const raw = attributes.get(\"data-duration\")?.value;\n if (raw !== undefined) return authoredNumber(raw, 0, \"data-duration\", id, true);\n\n const rawEnd = attributes.get(\"data-end\")?.value;\n if (rawEnd === undefined) return null;\n const end = authoredNumber(rawEnd, 0, \"data-end\", id, true);\n const start = authoredNumber(attributes.get(\"data-start\")?.value, 0, \"data-start\", id);\n return end > start ? end - start : null;\n}\n\nfunction requiredAudioIdentity(attributes: Map<string, AttributeSpan>) {\n const id = trimmedAttribute(attributes, \"id\");\n const src = trimmedAttribute(attributes, \"src\");\n if (!id) throw new Error(\"Every normalized <audio> element needs an id\");\n if (!src) throw new Error(`Audio #${id} has no src`);\n return { id, src };\n}\n\n/** Boolean attributes carry no `=`, so the value map cannot see them. */\nfunction hasBareAttribute(html: string, range: TagRange, name: string): boolean {\n const unquoted = html.slice(range.start, range.end).replace(/\"[^\"]*\"|'[^']*'/g, '\"\"');\n return new RegExp(`\\\\s${name}(?=[\\\\s/>=])`, \"i\").test(unquoted);\n}\n\nfunction isAudibleVideoTag(\n html: string,\n range: TagRange,\n attributes: Map<string, AttributeSpan>,\n): boolean {\n return isAudibleVideoElement({\n tagName: \"video\",\n hasAttribute: (name) => attributes.has(name) || hasBareAttribute(html, range, name),\n getAttribute: (name) => attributes.get(name)?.value ?? null,\n });\n}\n\n/**\n * A video joins the scan only when it carries sound and can be addressed.\n * Silent footage, and a video no command could name, are skipped rather than\n * failing a run that is about some other clip.\n */\nfunction inScopeVideo(\n html: string,\n range: TagRange,\n attributes: Map<string, AttributeSpan>,\n): boolean {\n return (\n isAudibleVideoTag(html, range, attributes) &&\n trimmedAttribute(attributes, \"id\") !== \"\" &&\n trimmedAttribute(attributes, \"src\") !== \"\"\n );\n}\n\nfunction mediaTagFromRange(html: string, range: MediaTagRange): AudioTag | null {\n const attributes = attributesInTag(html, range.start, range.end);\n if (range.name === \"video\" && !inScopeVideo(html, range, attributes)) return null;\n const { id, src } = requiredAudioIdentity(attributes);\n const volumeAttribute = attributes.get(\"data-volume\") ?? null;\n return {\n tag: range.name,\n id,\n src,\n volume: authoredNumber(volumeAttribute?.value, 1, \"data-volume\", id),\n mediaStart: authoredNumber(\n attributes.get(\"data-media-start\")?.value,\n 0,\n \"data-media-start\",\n id,\n ),\n duration: authoredDuration(attributes, id),\n playbackRate: authoredNumber(\n attributes.get(\"data-playback-rate\")?.value,\n 1,\n \"data-playback-rate\",\n id,\n true,\n ),\n start: range.start,\n end: range.end,\n volumeAttribute,\n };\n}\n\n/** Read the authored clips with sound — `<audio>` and audible `<video>` — without reserializing. */\nexport function audioTags(html: string): AudioTag[] {\n const seen = new Set<string>();\n const tags: AudioTag[] = [];\n for (const range of mediaTagRanges(html)) {\n const tag = mediaTagFromRange(html, range);\n if (!tag) continue;\n if (seen.has(tag.id)) throw new Error(`Duplicate audio id \"${tag.id}\"`);\n seen.add(tag.id);\n tags.push(tag);\n }\n return tags;\n}\n\n/** Resolve only a file contained by the project. Remote and traversal sources are not measured. */\nexport function resolveLocalAudioPath(projectDir: string, src: string): string {\n const withoutSuffix = src.split(/[?#]/, 1)[0] ?? \"\";\n let decoded = \"\";\n try {\n decoded = decodeURIComponent(withoutSuffix);\n } catch {\n throw new Error(`Audio source must be a local project file: ${src}`);\n }\n if (!decoded || isAbsolute(decoded) || /^(?:[a-z][a-z\\d+.-]*:|\\/\\/)/i.test(decoded)) {\n throw new Error(`Audio source must be a local project file: ${src}`);\n }\n const root = resolve(projectDir);\n const file = resolve(root, decoded);\n const rel = relative(root, file);\n if (!rel || rel === \"..\" || rel.startsWith(`..${process.platform === \"win32\" ? \"\\\\\" : \"/\"}`)) {\n throw new Error(`Audio source must be a local project file: ${src}`);\n }\n return file;\n}\n\n/** Parse FFmpeg's final EBU R128 summary, not its per-window log lines. */\nexport function parseEbur128Summary(stderr: string): LoudnessMeasurement {\n const summaryAt = stderr.lastIndexOf(\"Summary:\");\n const summary = summaryAt >= 0 ? stderr.slice(summaryAt) : \"\";\n const integrated = summary.match(/\\bI:\\s*(-?\\d+(?:\\.\\d+)?)\\s+LUFS\\b/);\n const peak = summary.match(/\\bPeak:\\s*(-?\\d+(?:\\.\\d+)?)\\s+dBFS\\b/);\n const integratedLufs = Number(integrated?.[1]);\n const truePeakDbfs = Number(peak?.[1]);\n if (!Number.isFinite(integratedLufs)) {\n throw new Error(\"FFmpeg did not report integrated loudness for this audio stream\");\n }\n if (!Number.isFinite(truePeakDbfs)) {\n throw new Error(\"FFmpeg did not report true peak for this audio stream\");\n }\n return { integratedLufs, truePeakDbfs };\n}\n\nexport function gainDb(volume: number): number {\n return volume > 0 ? 20 * Math.log10(volume) : Number.NEGATIVE_INFINITY;\n}\n\n/** Calculate the target's absolute authored gain while keeping the reference unchanged. */\nexport function audioNormalizationPlan(\n referenceTrack: NormalizationTrack,\n targetTrack: NormalizationTrack,\n): AudioNormalizationPlan {\n if (!(referenceTrack.volume > 0) || !(targetTrack.volume > 0)) {\n throw new Error(\"Muted audio cannot be used for loudness matching\");\n }\n const referenceLufs = referenceTrack.integratedLufs + gainDb(referenceTrack.volume);\n const targetLufs = targetTrack.integratedLufs + gainDb(targetTrack.volume);\n const wantedGainDb = referenceLufs - targetTrack.integratedLufs;\n if (wantedGainDb > MAX_AUDIO_GAIN_DB + 1e-9) {\n throw new Error(\n `Matching #${targetTrack.id} needs ${wantedGainDb.toFixed(1)} dB, beyond the +${MAX_AUDIO_GAIN_DB} dB authoring ceiling. ` +\n `A gap this large belongs in the source file, not the mixer — mixer gain raises the noise floor with the signal. ` +\n `Normalize ${targetTrack.id}'s asset offline (e.g. ffmpeg loudnorm), or lower #${referenceTrack.id} instead.`,\n );\n }\n const volume = 10 ** (wantedGainDb / 20);\n const projectedTruePeakDbfs = targetTrack.truePeakDbfs + wantedGainDb;\n if (projectedTruePeakDbfs > 0) {\n throw new Error(\n `Matching #${targetTrack.id} would clip at +${projectedTruePeakDbfs.toFixed(1)} dBFS; limit or preprocess the source first`,\n );\n }\n return {\n referenceId: referenceTrack.id,\n targetId: targetTrack.id,\n referenceLufs,\n targetLufs,\n previousVolume: targetTrack.volume,\n volume,\n gainDb: wantedGainDb,\n changeDb: wantedGainDb - gainDb(targetTrack.volume),\n projectedLufs: targetTrack.integratedLufs + wantedGainDb,\n projectedTruePeakDbfs,\n };\n}\n\n/** Patch one authored attribute in place so scripts/styles/comments remain byte-stable. */\nexport function updateAudioVolume(html: string, id: string, volume: number): string {\n const tag = audioTags(html).find((candidate) => candidate.id === id);\n if (!tag) throw new Error(`Audio #${id} was not found`);\n const value = formatAudioGain(volume);\n if (tag.volumeAttribute) {\n return (\n html.slice(0, tag.volumeAttribute.valueStart) +\n value +\n html.slice(tag.volumeAttribute.valueEnd)\n );\n }\n\n let insertion = tag.end - 1;\n let tail = insertion - 1;\n while (tail >= tag.start && /\\s/.test(html[tail] ?? \"\")) tail -= 1;\n if (html[tail] === \"/\") {\n insertion = tail;\n while (insertion > tag.start && /\\s/.test(html[insertion - 1] ?? \"\")) insertion -= 1;\n } else {\n while (insertion > tag.start && /\\s/.test(html[insertion - 1] ?? \"\")) insertion -= 1;\n }\n return html.slice(0, insertion) + ` data-volume=\"${value}\"` + html.slice(insertion);\n}\n\n/**\n * FFmpeg arguments that measure exactly the window the composition plays.\n *\n * `-ss` / `-t` go BEFORE `-i`, as INPUT options. After `-i` they bound the\n * output, and with `-f null` there is no real output to bound: ffmpeg keeps\n * feeding the filter graph past the limit, so ebur128 integrates audio the clip\n * never plays. Measured on a 12 s file whose first 4 s — the played window — is\n * -61.8 LUFS: output-side `-t 4` reported -33.8 LUFS, input-side reports -61.8,\n * the same as physically cutting the file first.\n */\nexport function loudnessMeasureArgs(file: string, tag: MeasuredWindow): string[] {\n const args = [\"-hide_banner\", \"-nostats\"];\n if (tag.mediaStart > 0) args.push(\"-ss\", String(tag.mediaStart));\n const sourceSpan = tag.duration === null ? null : tag.duration * (tag.playbackRate ?? 1);\n if (sourceSpan !== null && sourceSpan > 0) args.push(\"-t\", String(sourceSpan));\n args.push(\"-i\", file, \"-map\", \"0:a:0\", \"-vn\", \"-af\", \"ebur128=peak=true\", \"-f\", \"null\", \"-\");\n return args;\n}\n\nexport type MeasuredWindow = Pick<AudioTag, \"mediaStart\" | \"duration\"> &\n Partial<Pick<AudioTag, \"playbackRate\">>;\n\nexport async function measureAudio(\n ffmpegPath: string,\n file: string,\n tag: MeasuredWindow,\n): Promise<LoudnessMeasurement> {\n const result = await execFileAsync(ffmpegPath, loudnessMeasureArgs(file, tag), {\n encoding: \"utf8\",\n maxBuffer: 16 * 1024 * 1024,\n timeout: FFMPEG_TIMEOUT_MS,\n });\n return parseEbur128Summary(result.stderr);\n}\n\nexport function requiredFfmpeg(): string {\n const ffmpegPath = findFfBinary(\"ffmpeg\", { configuredMustExist: true });\n if (!ffmpegPath) throw new Error(\"FFmpeg is required to measure integrated loudness\");\n return ffmpegPath;\n}\n\nexport const DEFAULT_TARGET_LUFS = -16;\nexport const NORMALIZE_TRUE_PEAK_CEILING_DBFS = -1.5;\n/** EBU R128's absolute gate: a window this quiet has no integrated loudness to match. */\nconst SILENCE_LUFS = -70;\n\nexport type LoudnessLimit = \"gain-ceiling\" | \"true-peak\";\n\nexport interface AbsoluteLoudnessPlan {\n targetId: string;\n targetLufs: number;\n sourceLufs: number;\n previousVolume: number;\n volume: number;\n gainDb: number;\n changeDb: number;\n projectedLufs: number;\n projectedTruePeakDbfs: number;\n limitedBy: LoudnessLimit | null;\n}\n\n/**\n * The authored gain that brings one clip's played window to an absolute\n * loudness. Unlike reference matching it never refuses: past the +12 dB\n * authoring ceiling or the true-peak ceiling it applies the most that fits and\n * names which limit stopped it.\n */\nexport function absoluteLoudnessPlan(\n track: NormalizationTrack,\n targetLufs: number = DEFAULT_TARGET_LUFS,\n): AbsoluteLoudnessPlan {\n if (track.integratedLufs <= SILENCE_LUFS) {\n throw new Error(`#${track.id} is silent in the played window; there is nothing to normalize`);\n }\n const wantedGainDb = targetLufs - track.integratedLufs;\n const peakRoomDb = NORMALIZE_TRUE_PEAK_CEILING_DBFS - track.truePeakDbfs;\n const limits: Array<[LoudnessLimit | null, number]> = [\n [null, wantedGainDb],\n [\"gain-ceiling\", MAX_AUDIO_GAIN_DB],\n [\"true-peak\", peakRoomDb],\n ];\n const [limitedBy, appliedGainDb] = limits.reduce((lowest, candidate) =>\n candidate[1] < lowest[1] - 1e-9 ? candidate : lowest,\n );\n const previousGainDb = track.volume > 0 ? gainDb(track.volume) : 0;\n return {\n targetId: track.id,\n targetLufs,\n sourceLufs: track.integratedLufs,\n previousVolume: track.volume,\n volume: 10 ** (appliedGainDb / 20),\n gainDb: appliedGainDb,\n changeDb: appliedGainDb - previousGainDb,\n projectedLufs: track.integratedLufs + appliedGainDb,\n projectedTruePeakDbfs: track.truePeakDbfs + appliedGainDb,\n limitedBy,\n };\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/helpers/loudness.ts"],"sourcesContent":["import { execFile } from \"node:child_process\";\nimport { isAbsolute, relative, resolve } from \"node:path\";\nimport { promisify } from \"node:util\";\nimport { isAudibleVideoElement } from \"@hyperframes/core/audible-video\";\nimport { formatAudioGain, MAX_AUDIO_GAIN_DB } from \"@hyperframes/core/audio-gain\";\nimport { findFfBinary } from \"@hyperframes/parsers/ff-binaries\";\n\nconst execFileAsync = promisify(execFile);\n\nconst FFMPEG_TIMEOUT_MS = 120_000;\n\ninterface AttributeSpan {\n value: string;\n valueStart: number;\n valueEnd: number;\n}\n\ninterface TagRange {\n start: number;\n end: number;\n}\n\ntype MediaTagName = \"audio\" | \"video\";\n\ninterface MediaTagRange extends TagRange {\n name: MediaTagName;\n}\n\ninterface ParsedStartTag extends TagRange {\n closing: boolean;\n name: string;\n}\n\ninterface ScanResult {\n cursor: number;\n mediaRange: MediaTagRange | null;\n}\n\nexport interface AudioTag {\n tag: MediaTagName;\n id: string;\n src: string;\n volume: number;\n mediaStart: number;\n duration: number | null;\n playbackRate: number;\n start: number;\n end: number;\n volumeAttribute: AttributeSpan | null;\n}\n\nexport interface LoudnessMeasurement {\n integratedLufs: number;\n truePeakDbfs: number;\n}\n\nexport interface NormalizationTrack extends LoudnessMeasurement {\n id: string;\n volume: number;\n}\n\nexport interface AudioNormalizationPlan {\n referenceId: string;\n targetId: string;\n referenceLufs: number;\n targetLufs: number;\n previousVolume: number;\n volume: number;\n gainDb: number;\n changeDb: number;\n projectedLufs: number;\n projectedTruePeakDbfs: number;\n}\n\nfunction authoredNumber(\n raw: string | undefined,\n fallback: number,\n attribute: string,\n id: string,\n strictlyPositive = false,\n): number {\n if (raw === undefined) return fallback;\n const value = Number(raw);\n if (!Number.isFinite(value) || (strictlyPositive ? value <= 0 : value < 0)) {\n throw new Error(`Audio #${id} has an invalid ${attribute}: ${raw}`);\n }\n return value;\n}\n\nfunction attributeFromMatch(\n match: RegExpMatchArray,\n absoluteOffset: number,\n): [string, AttributeSpan] | null {\n if (match.index === undefined) return null;\n const name = match[1]?.toLowerCase();\n const value = matchedAttributeValue(match);\n if (!name || value === undefined) return null;\n\n const whole = match[0];\n const localValueStart = attributeValueStart(whole);\n const valueStart = absoluteOffset + match.index + localValueStart;\n return [name, { value, valueStart, valueEnd: valueStart + value.length }];\n}\n\nfunction matchedAttributeValue(match: RegExpMatchArray): string | undefined {\n return match[2] ?? match[3] ?? match[4];\n}\n\nfunction attributeValueStart(attribute: string): number {\n let cursor = attribute.indexOf(\"=\") + 1;\n while (/\\s/.test(attribute[cursor] ?? \"\")) cursor += 1;\n const quote = attribute[cursor];\n return quote === '\"' || quote === \"'\" ? cursor + 1 : cursor;\n}\n\nfunction attributesInTag(html: string, from: number, to: number): Map<string, AttributeSpan> {\n const tag = html.slice(from, to);\n const nameEnd = tag.search(/\\s|\\/?\\s*>/);\n const attributes = new Map<string, AttributeSpan>();\n if (nameEnd < 0) return attributes;\n\n const pattern = /([^\\s=/>]+)\\s*=\\s*(?:\"([^\"]*)\"|'([^']*)'|([^\\s\"'=<>`]+))/g;\n for (const match of tag.slice(nameEnd).matchAll(pattern)) {\n const attribute = attributeFromMatch(match, from + nameEnd);\n if (attribute) attributes.set(...attribute);\n }\n return attributes;\n}\n\nfunction startTagEnd(html: string, start: number): number {\n let quote = \"\";\n let end = start + 1;\n for (; end < html.length; end += 1) {\n const char = html[end];\n if (quote) {\n if (char === quote) quote = \"\";\n } else if (char === '\"' || char === \"'\") {\n quote = char;\n } else if (char === \">\") {\n return end + 1;\n }\n }\n throw new Error(\"Unterminated HTML start tag\");\n}\n\nfunction commentEnd(html: string, start: number): number {\n const end = html.indexOf(\"-->\", start + 4);\n if (end < 0) throw new Error(\"Unterminated HTML comment\");\n return end + 3;\n}\n\nconst isSpaceAt = (html: string, index: number) => /\\s/.test(html.charAt(index));\nconst isWordAt = (html: string, index: number) => /\\w/.test(html.charAt(index));\n\nfunction skipSpaces(html: string, index: number): number {\n let cursor = index;\n while (cursor < html.length && isSpaceAt(html, cursor)) cursor++;\n return cursor;\n}\n\nfunction parsedStartTag(html: string, start: number): ParsedStartTag | null {\n let cursor = skipSpaces(html, start + 1);\n const closing = html.charAt(cursor) === \"/\";\n if (closing) cursor = skipSpaces(html, cursor + 1);\n const nameStart = cursor;\n if (!/[a-z]/i.test(html.charAt(cursor))) return null;\n while (/[a-z\\d:-]/i.test(html.charAt(cursor + 1))) cursor++;\n let nameEnd = cursor + 1;\n while (nameEnd > nameStart && isWordAt(html, nameEnd - 1) === isWordAt(html, nameEnd)) nameEnd--;\n if (nameEnd === nameStart) return null;\n return {\n start,\n end: startTagEnd(html, start),\n closing,\n name: html.slice(nameStart, nameEnd).toLowerCase(),\n };\n}\n\nfunction rawTextElementEnd(html: string, lower: string, tag: ParsedStartTag): number | null {\n if (tag.closing || (tag.name !== \"script\" && tag.name !== \"style\")) return null;\n const closeStart = lower.indexOf(`</${tag.name}`, tag.end);\n if (closeStart < 0) throw new Error(`Unterminated <${tag.name}> element`);\n return startTagEnd(html, closeStart);\n}\n\nfunction scanMarkup(html: string, lower: string, start: number): ScanResult {\n if (html.startsWith(\"<!--\", start)) {\n return { cursor: commentEnd(html, start), mediaRange: null };\n }\n const tag = parsedStartTag(html, start);\n if (!tag) {\n const marker = html[start + 1];\n const cursor = marker === \"!\" || marker === \"?\" ? startTagEnd(html, start) : start + 1;\n return { cursor, mediaRange: null };\n }\n const rawTextEnd = rawTextElementEnd(html, lower, tag);\n if (rawTextEnd !== null) return { cursor: rawTextEnd, mediaRange: null };\n return { cursor: tag.end, mediaRange: mediaRangeOf(tag) };\n}\n\nfunction mediaRangeOf(tag: ParsedStartTag): MediaTagRange | null {\n if (tag.closing) return null;\n if (tag.name === \"audio\" || tag.name === \"video\") {\n return { start: tag.start, end: tag.end, name: tag.name };\n }\n return null;\n}\n\nfunction mediaTagRanges(html: string): MediaTagRange[] {\n const lower = html.toLowerCase();\n const ranges: MediaTagRange[] = [];\n let cursor = 0;\n while (cursor < html.length) {\n const start = html.indexOf(\"<\", cursor);\n if (start < 0) break;\n const scanned = scanMarkup(html, lower, start);\n if (scanned.mediaRange) ranges.push(scanned.mediaRange);\n cursor = scanned.cursor;\n }\n return ranges;\n}\n\nfunction trimmedAttribute(attributes: Map<string, AttributeSpan>, name: string): string {\n return attributes.get(name)?.value.trim() ?? \"\";\n}\n\n/**\n * How much of the source the composition actually plays.\n *\n * `data-duration` is the explicit trim; absent, `data-end` still bounds the\n * clip's timeline window, and the parser, the runtime and the render mixer all\n * honour it. Measuring the whole file for a `data-end`-trimmed clip reads a\n * loudness the composition never plays — and `--write` then \"corrects\" an\n * already-matched clip by tens of dB.\n */\nfunction authoredDuration(attributes: Map<string, AttributeSpan>, id: string): number | null {\n const raw = attributes.get(\"data-duration\")?.value;\n if (raw !== undefined) return authoredNumber(raw, 0, \"data-duration\", id, true);\n\n const rawEnd = attributes.get(\"data-end\")?.value;\n if (rawEnd === undefined) return null;\n const end = authoredNumber(rawEnd, 0, \"data-end\", id, true);\n const start = authoredNumber(attributes.get(\"data-start\")?.value, 0, \"data-start\", id);\n return end > start ? end - start : null;\n}\n\nfunction requiredAudioIdentity(attributes: Map<string, AttributeSpan>) {\n const id = trimmedAttribute(attributes, \"id\");\n const src = trimmedAttribute(attributes, \"src\");\n if (!id) throw new Error(\"Every normalized <audio> element needs an id\");\n if (!src) throw new Error(`Audio #${id} has no src`);\n return { id, src };\n}\n\n/** Boolean attributes carry no `=`, so the value map cannot see them. */\nfunction hasBareAttribute(html: string, range: TagRange, name: string): boolean {\n const unquoted = html.slice(range.start, range.end).replace(/\"[^\"]*\"|'[^']*'/g, '\"\"');\n return new RegExp(`\\\\s${name}(?=[\\\\s/>=])`, \"i\").test(unquoted);\n}\n\nfunction isAudibleVideoTag(\n html: string,\n range: TagRange,\n attributes: Map<string, AttributeSpan>,\n): boolean {\n return isAudibleVideoElement({\n tagName: \"video\",\n hasAttribute: (name) => attributes.has(name) || hasBareAttribute(html, range, name),\n getAttribute: (name) => attributes.get(name)?.value ?? null,\n });\n}\n\n/**\n * A video joins the scan only when it carries sound and can be addressed.\n * Silent footage, and a video no command could name, are skipped rather than\n * failing a run that is about some other clip.\n */\nfunction inScopeVideo(\n html: string,\n range: TagRange,\n attributes: Map<string, AttributeSpan>,\n): boolean {\n return (\n isAudibleVideoTag(html, range, attributes) &&\n trimmedAttribute(attributes, \"id\") !== \"\" &&\n trimmedAttribute(attributes, \"src\") !== \"\"\n );\n}\n\nfunction mediaTagFromRange(html: string, range: MediaTagRange): AudioTag | null {\n const attributes = attributesInTag(html, range.start, range.end);\n if (range.name === \"video\" && !inScopeVideo(html, range, attributes)) return null;\n const { id, src } = requiredAudioIdentity(attributes);\n const volumeAttribute = attributes.get(\"data-volume\") ?? null;\n return {\n tag: range.name,\n id,\n src,\n volume: authoredNumber(volumeAttribute?.value, 1, \"data-volume\", id),\n mediaStart: authoredNumber(\n attributes.get(\"data-media-start\")?.value,\n 0,\n \"data-media-start\",\n id,\n ),\n duration: authoredDuration(attributes, id),\n playbackRate: authoredNumber(\n attributes.get(\"data-playback-rate\")?.value,\n 1,\n \"data-playback-rate\",\n id,\n true,\n ),\n start: range.start,\n end: range.end,\n volumeAttribute,\n };\n}\n\n/** Read the authored clips with sound — `<audio>` and audible `<video>` — without reserializing. */\nexport function audioTags(html: string): AudioTag[] {\n const seen = new Set<string>();\n const tags: AudioTag[] = [];\n for (const range of mediaTagRanges(html)) {\n const tag = mediaTagFromRange(html, range);\n if (!tag) continue;\n if (seen.has(tag.id)) throw new Error(`Duplicate audio id \"${tag.id}\"`);\n seen.add(tag.id);\n tags.push(tag);\n }\n return tags;\n}\n\n/** Resolve only a file contained by the project. Remote and traversal sources are not measured. */\nexport function resolveLocalAudioPath(projectDir: string, src: string): string {\n const withoutSuffix = src.split(/[?#]/, 1)[0] ?? \"\";\n let decoded = \"\";\n try {\n decoded = decodeURIComponent(withoutSuffix);\n } catch {\n throw new Error(`Audio source must be a local project file: ${src}`);\n }\n if (!decoded || isAbsolute(decoded) || /^(?:[a-z][a-z\\d+.-]*:|\\/\\/)/i.test(decoded)) {\n throw new Error(`Audio source must be a local project file: ${src}`);\n }\n const root = resolve(projectDir);\n const file = resolve(root, decoded);\n const rel = relative(root, file);\n if (!rel || rel === \"..\" || rel.startsWith(`..${process.platform === \"win32\" ? \"\\\\\" : \"/\"}`)) {\n throw new Error(`Audio source must be a local project file: ${src}`);\n }\n return file;\n}\n\n/** Parse FFmpeg's final EBU R128 summary, not its per-window log lines. */\nexport function parseEbur128Summary(stderr: string): LoudnessMeasurement {\n const summaryAt = stderr.lastIndexOf(\"Summary:\");\n const summary = summaryAt >= 0 ? stderr.slice(summaryAt) : \"\";\n const integrated = summary.match(/\\bI:\\s*(-?\\d+(?:\\.\\d+)?)\\s+LUFS\\b/);\n const peak = summary.match(/\\bPeak:\\s*(-?\\d+(?:\\.\\d+)?)\\s+dBFS\\b/);\n const integratedLufs = Number(integrated?.[1]);\n const truePeakDbfs = Number(peak?.[1]);\n if (!Number.isFinite(integratedLufs)) {\n throw new Error(\"FFmpeg did not report integrated loudness for this audio stream\");\n }\n if (!Number.isFinite(truePeakDbfs)) {\n throw new Error(\"FFmpeg did not report true peak for this audio stream\");\n }\n return { integratedLufs, truePeakDbfs };\n}\n\nexport function gainDb(volume: number): number {\n return volume > 0 ? 20 * Math.log10(volume) : Number.NEGATIVE_INFINITY;\n}\n\n/** Calculate the target's absolute authored gain while keeping the reference unchanged. */\nexport function audioNormalizationPlan(\n referenceTrack: NormalizationTrack,\n targetTrack: NormalizationTrack,\n): AudioNormalizationPlan {\n if (!(referenceTrack.volume > 0) || !(targetTrack.volume > 0)) {\n throw new Error(\"Muted audio cannot be used for loudness matching\");\n }\n const referenceLufs = referenceTrack.integratedLufs + gainDb(referenceTrack.volume);\n const targetLufs = targetTrack.integratedLufs + gainDb(targetTrack.volume);\n const wantedGainDb = referenceLufs - targetTrack.integratedLufs;\n if (wantedGainDb > MAX_AUDIO_GAIN_DB + 1e-9) {\n throw new Error(\n `Matching #${targetTrack.id} needs ${wantedGainDb.toFixed(1)} dB, beyond the +${MAX_AUDIO_GAIN_DB} dB authoring ceiling. ` +\n `A gap this large belongs in the source file, not the mixer — mixer gain raises the noise floor with the signal. ` +\n `Normalize ${targetTrack.id}'s asset offline (e.g. ffmpeg loudnorm), or lower #${referenceTrack.id} instead.`,\n );\n }\n const volume = 10 ** (wantedGainDb / 20);\n const projectedTruePeakDbfs = targetTrack.truePeakDbfs + wantedGainDb;\n if (projectedTruePeakDbfs > 0) {\n throw new Error(\n `Matching #${targetTrack.id} would clip at +${projectedTruePeakDbfs.toFixed(1)} dBFS; limit or preprocess the source first`,\n );\n }\n return {\n referenceId: referenceTrack.id,\n targetId: targetTrack.id,\n referenceLufs,\n targetLufs,\n previousVolume: targetTrack.volume,\n volume,\n gainDb: wantedGainDb,\n changeDb: wantedGainDb - gainDb(targetTrack.volume),\n projectedLufs: targetTrack.integratedLufs + wantedGainDb,\n projectedTruePeakDbfs,\n };\n}\n\n/** Patch one authored attribute in place so scripts/styles/comments remain byte-stable. */\nexport function updateAudioVolume(html: string, id: string, volume: number): string {\n const tag = audioTags(html).find((candidate) => candidate.id === id);\n if (!tag) throw new Error(`Audio #${id} was not found`);\n const value = formatAudioGain(volume);\n if (tag.volumeAttribute) {\n return (\n html.slice(0, tag.volumeAttribute.valueStart) +\n value +\n html.slice(tag.volumeAttribute.valueEnd)\n );\n }\n\n let insertion = tag.end - 1;\n let tail = insertion - 1;\n while (tail >= tag.start && /\\s/.test(html[tail] ?? \"\")) tail -= 1;\n if (html[tail] === \"/\") {\n insertion = tail;\n while (insertion > tag.start && /\\s/.test(html[insertion - 1] ?? \"\")) insertion -= 1;\n } else {\n while (insertion > tag.start && /\\s/.test(html[insertion - 1] ?? \"\")) insertion -= 1;\n }\n return html.slice(0, insertion) + ` data-volume=\"${value}\"` + html.slice(insertion);\n}\n\n/**\n * FFmpeg arguments that measure exactly the window the composition plays.\n *\n * `-ss` / `-t` go BEFORE `-i`, as INPUT options. After `-i` they bound the\n * output, and with `-f null` there is no real output to bound: ffmpeg keeps\n * feeding the filter graph past the limit, so ebur128 integrates audio the clip\n * never plays. Measured on a 12 s file whose first 4 s — the played window — is\n * -61.8 LUFS: output-side `-t 4` reported -33.8 LUFS, input-side reports -61.8,\n * the same as physically cutting the file first.\n */\nexport function loudnessMeasureArgs(file: string, tag: MeasuredWindow): string[] {\n const args = [\"-hide_banner\", \"-nostats\"];\n if (tag.mediaStart > 0) args.push(\"-ss\", String(tag.mediaStart));\n const sourceSpan = tag.duration === null ? null : tag.duration * (tag.playbackRate ?? 1);\n if (sourceSpan !== null && sourceSpan > 0) args.push(\"-t\", String(sourceSpan));\n args.push(\"-i\", file, \"-map\", \"0:a:0\", \"-vn\", \"-af\", \"ebur128=peak=true\", \"-f\", \"null\", \"-\");\n return args;\n}\n\nexport type MeasuredWindow = Pick<AudioTag, \"mediaStart\" | \"duration\"> &\n Partial<Pick<AudioTag, \"playbackRate\">>;\n\nexport async function measureAudio(\n ffmpegPath: string,\n file: string,\n tag: MeasuredWindow,\n): Promise<LoudnessMeasurement> {\n const result = await execFileAsync(ffmpegPath, loudnessMeasureArgs(file, tag), {\n encoding: \"utf8\",\n maxBuffer: 16 * 1024 * 1024,\n timeout: FFMPEG_TIMEOUT_MS,\n windowsHide: true,\n });\n return parseEbur128Summary(result.stderr);\n}\n\nexport function requiredFfmpeg(): string {\n const ffmpegPath = findFfBinary(\"ffmpeg\", { configuredMustExist: true });\n if (!ffmpegPath) throw new Error(\"FFmpeg is required to measure integrated loudness\");\n return ffmpegPath;\n}\n\nexport const DEFAULT_TARGET_LUFS = -16;\nexport const NORMALIZE_TRUE_PEAK_CEILING_DBFS = -1.5;\n/** EBU R128's absolute gate: a window this quiet has no integrated loudness to match. */\nconst SILENCE_LUFS = -70;\n\nexport type LoudnessLimit = \"gain-ceiling\" | \"true-peak\";\n\nexport interface AbsoluteLoudnessPlan {\n targetId: string;\n targetLufs: number;\n sourceLufs: number;\n previousVolume: number;\n volume: number;\n gainDb: number;\n changeDb: number;\n projectedLufs: number;\n projectedTruePeakDbfs: number;\n limitedBy: LoudnessLimit | null;\n}\n\n/**\n * The authored gain that brings one clip's played window to an absolute\n * loudness. Unlike reference matching it never refuses: past the +12 dB\n * authoring ceiling or the true-peak ceiling it applies the most that fits and\n * names which limit stopped it.\n */\nexport function absoluteLoudnessPlan(\n track: NormalizationTrack,\n targetLufs: number = DEFAULT_TARGET_LUFS,\n): AbsoluteLoudnessPlan {\n if (track.integratedLufs <= SILENCE_LUFS) {\n throw new Error(`#${track.id} is silent in the played window; there is nothing to normalize`);\n }\n const wantedGainDb = targetLufs - track.integratedLufs;\n const peakRoomDb = NORMALIZE_TRUE_PEAK_CEILING_DBFS - track.truePeakDbfs;\n const limits: Array<[LoudnessLimit | null, number]> = [\n [null, wantedGainDb],\n [\"gain-ceiling\", MAX_AUDIO_GAIN_DB],\n [\"true-peak\", peakRoomDb],\n ];\n const [limitedBy, appliedGainDb] = limits.reduce((lowest, candidate) =>\n candidate[1] < lowest[1] - 1e-9 ? candidate : lowest,\n );\n const previousGainDb = track.volume > 0 ? gainDb(track.volume) : 0;\n return {\n targetId: track.id,\n targetLufs,\n sourceLufs: track.integratedLufs,\n previousVolume: track.volume,\n volume: 10 ** (appliedGainDb / 20),\n gainDb: appliedGainDb,\n changeDb: appliedGainDb - previousGainDb,\n projectedLufs: track.integratedLufs + appliedGainDb,\n projectedTruePeakDbfs: track.truePeakDbfs + appliedGainDb,\n limitedBy,\n };\n}\n"],"mappings":"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","names":[]}
|
package/dist/helpers/loudness.js
CHANGED
package/dist/index.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hyperframes/studio-server",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.113",
|
|
4
4
|
"repository": {
|
|
5
5
|
"type": "git",
|
|
6
6
|
"url": "https://github.com/heygen-com/hyperframes",
|
|
@@ -81,8 +81,8 @@
|
|
|
81
81
|
"postcss": "^8.5.8",
|
|
82
82
|
"postcss-selector-parser": "^7.1.2",
|
|
83
83
|
"sharp": "^0.35.0",
|
|
84
|
-
"@hyperframes/core": "0.8.
|
|
85
|
-
"@hyperframes/parsers": "0.8.
|
|
84
|
+
"@hyperframes/core": "0.8.113",
|
|
85
|
+
"@hyperframes/parsers": "0.8.113"
|
|
86
86
|
},
|
|
87
87
|
"devDependencies": {
|
|
88
88
|
"@types/node": "^25.0.10",
|