Skip to content

Commit 853d8e3

Browse files
committed
Merge remote-tracking branch 'origin/pr-24' into codex/review-community-improvements
2 parents d2ef270 + 946179c commit 853d8e3

3 files changed

Lines changed: 48 additions & 2 deletions

File tree

‎src/core/editor/render-project-timeline.ts‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { spawn } from 'node:child_process';
22
import { mkdtemp, readFile, rm, writeFile } from 'node:fs/promises';
33
import { tmpdir } from 'node:os';
44
import { join } from 'node:path';
5+
import { buildTimelineAudioMixFilter } from './timeline-audio-filter';
56

67
import { getResolvedCaptionStyle } from './captions';
78
import {
@@ -363,8 +364,7 @@ async function buildFfmpegArgs({
363364
}
364365
if (audioLabels.length > 0) {
365366
filters.push(
366-
`${audioLabels.join('')}amix=inputs=${audioLabels.length}:duration=longest:dropout_transition=0,` +
367-
`atrim=duration=${time(document.duration)},apad=whole_dur=${time(document.duration)}[aout]`
367+
`${audioLabels.join('')}${buildTimelineAudioMixFilter(audioLabels.length, document.duration)}[aout]`
368368
);
369369
}
370370

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import assert from 'node:assert/strict';
2+
import { spawnSync } from 'node:child_process';
3+
import { test } from 'node:test';
4+
import { buildTimelineAudioMixFilter } from './timeline-audio-filter';
5+
6+
const ffmpeg = process.env.BEATDESIGN_FFMPEG?.trim() || 'ffmpeg';
7+
const available = spawnSync(ffmpeg,['-version'],{stdio:'ignore'}).status === 0;
8+
function render(count:number, overlap=false) {
9+
const args=['-hide_banner','-loglevel','error'];
10+
for(let i=0;i<count;i++)args.push('-f','lavfi','-i','sine=frequency=440:sample_rate=48000:duration=1');
11+
const filters=Array.from({length:count},(_,i)=>`[${i}:a]volume=${overlap?10:1},adelay=${overlap?0:i*1000}:all=1[a${i}]`);
12+
filters.push(Array.from({length:count},(_,i)=>`[a${i}]`).join('')+buildTimelineAudioMixFilter(count,overlap?1:count)+'[out]');
13+
args.push('-filter_complex',filters.join(';'),'-map','[out]','-ac','1','-c:a','pcm_f32le','-f','f32le','pipe:1');
14+
const result=spawnSync(ffmpeg,args,{maxBuffer:4*1024*1024});
15+
assert.equal(result.status,0,result.stderr?.toString());
16+
const samples=new Float32Array(result.stdout.buffer.slice(result.stdout.byteOffset,result.stdout.byteOffset+result.stdout.byteLength));
17+
return samples;
18+
}
19+
function rms(samples:Float32Array,start:number,end:number) {
20+
let sum=0;for(let i=start;i<end;i++)sum+=samples[i]**2;
21+
return Math.sqrt(sum/(end-start));
22+
}
23+
24+
test('sequential delayed clips retain unity gain as later inputs are added',{skip:!available},()=>{
25+
const one=render(1),three=render(3),expected=rms(one,4800,43200);
26+
assert.ok(expected>0.08);
27+
for(let i=0;i<3;i++)assert.ok(Math.abs(rms(three,48000*i+4800,48000*i+43200)-expected)<0.0001);
28+
assert.equal(three.length,3*48000);
29+
});
30+
31+
test('overlapping loud clips are limited without automatic makeup or timeline delay',{skip:!available},()=>{
32+
const samples=render(2,true);
33+
let peak=0;for(const sample of samples)peak=Math.max(peak,Math.abs(sample));
34+
assert.ok(peak<=0.951 && peak>0.90,`Peak was ${peak}`);
35+
assert.equal(samples.length,48000);
36+
});
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
/** Preserve clip gains independently of silent/delayed timeline inputs. */
2+
export function buildTimelineAudioMixFilter(inputCount: number, duration: number) {
3+
if (!Number.isSafeInteger(inputCount) || inputCount < 1 || !Number.isFinite(duration) || duration <= 0)
4+
throw new Error('Audio mixing requires inputs and a positive timeline duration.');
5+
const seconds = duration.toFixed(3);
6+
return `amix=inputs=${inputCount}:duration=longest:dropout_transition=0:normalize=0,` +
7+
// Disable the limiter's automatic makeup gain; compensate its lookahead.
8+
'alimiter=limit=0.95:level=0:latency=1,' +
9+
`atrim=duration=${seconds},apad=whole_dur=${seconds}`;
10+
}

0 commit comments

Comments
 (0)