@@ -246,15 +246,15 @@ describe("AudioFxGroup carve", () => {
246246 } ) ;
247247} ) ;
248248
249- describe ( "AudioFxGroup dynamic carve" , ( ) => {
249+ describe ( "AudioFxGroup carve analysis " , ( ) => {
250250 const carvedChain = JSON . stringify ( {
251251 version : 1 ,
252252 nodes : [ { type : "lowpass" , id : "n1" , params : { frequency : 400 , q : 0.9 , poles : "2" } } ] ,
253253 } ) ;
254254 // Strength 0 carves frequencies only — no level ducking — so the spectral
255255 // cases measure just the spectral half. A case that wants the duck raises it.
256- const settings = ( dynamic : boolean , over : Record < string , unknown > = { } ) =>
257- JSON . stringify ( { source : "vo" , strength : 0 , dynamic , ...over } ) ;
256+ const settings = ( over : Record < string , unknown > = { } ) =>
257+ JSON . stringify ( { sources : [ "vo" ] , strength : 0 , ...over } ) ;
258258
259259 /** The value written for one attribute, whatever order the writes landed in. */
260260 const writeFor = ( calls : unknown [ ] [ ] , attr : string ) =>
@@ -267,9 +267,6 @@ describe("AudioFxGroup dynamic carve", () => {
267267 select . dispatchEvent ( new Event ( "change" , { bubbles : true } ) ) ;
268268 } ;
269269
270- const dynamicBox = ( host : HTMLElement ) =>
271- host . querySelector < HTMLInputElement > ( ".hf-fx-carve-dynamic" ) ! ;
272-
273270 /** A voice with a pause in it, decoded through a stubbed offline context. */
274271 function stubDecode ( ) : void {
275272 const sampleRate = 48000 ;
@@ -292,114 +289,11 @@ describe("AudioFxGroup dynamic carve", () => {
292289
293290 afterEach ( ( ) => vi . unstubAllGlobals ( ) ) ;
294291
295- it ( "records the choice in the carve settings" , ( ) => {
296- const { host, onSetAttributeQuiet } = mount ( {
297- "fx-chain" : carvedChain ,
298- "fx-carve" : settings ( false ) ,
299- } ) ;
300- act ( ( ) => dynamicBox ( host ) . click ( ) ) ;
301- const write = onSetAttributeQuiet . mock . calls . find ( ( c ) => c [ 0 ] === "data-fx-carve" ) ;
302- expect ( JSON . parse ( String ( write ! [ 1 ] ) ) . dynamic ) . toBe ( true ) ;
303- } ) ;
304-
305- it ( "automates the carve filters' gain from the voice, in the bed's own time" , async ( ) => {
306- stubDecode ( ) ;
307- // Voice starts 10s into the composition, bed at 0: the envelope is measured
308- // against the voice but read from the start of the bed, so it has to shift.
309- const { host, onSetAttributeQuiet } = mount ( {
310- "fx-chain" : carvedChain ,
311- // No source yet: picking one is what applies the carve.
312- "fx-carve" : settings ( true , { source : "" } ) ,
313- start : "0" ,
314- } ) ;
315- const vo = document . getElementById ( "vo" ) ! ;
316- vo . setAttribute ( "data-start" , "10" ) ;
317- vo . setAttribute ( "src" , "voice.wav" ) ;
318- await act ( async ( ) => {
319- pickSource ( host , "vo" ) ;
320- } ) ;
321-
322- // Chain first, then automation: a lane naming a node the chain does not
323- // carry yet is dropped when it is read back.
324- const order = onSetAttributeQuiet . mock . calls . map ( ( c ) => c [ 0 ] ) ;
325- // The settings land first, then the filters they imply, then the envelopes.
326- expect ( order . indexOf ( "data-fx-chain" ) ) . toBeLessThan ( order . indexOf ( "data-automation" ) ) ;
327-
328- const carved = writeFor ( onSetAttributeQuiet . mock . calls , "data-fx-chain" ) . nodes ;
329- const carveNode = carved . find ( ( n : { fromCarve ?: boolean } ) => n . fromCarve ) ;
330- expect ( carveNode . id ) . toBeTruthy ( ) ;
331-
332- const lanes = writeFor ( onSetAttributeQuiet . mock . calls , "data-automation" ) . lanes ;
333- const lane = lanes . find ( ( l : { target : string } ) => l . target === `fx.${ carveNode . id } .gain` ) as {
334- points : { t : number ; v : number } [ ] ;
335- } ;
336- expect ( lane ) . toBeTruthy ( ) ;
337- // Flat at the bed's own start, before the voice exists at all.
338- expect ( lane . points [ 0 ] ) . toMatchObject ( { t : 0 , v : 0 } ) ;
339- // The voice's pause is at 0-1s of its own clip, so 10-11s of the bed's.
340- expect ( lane . points . find ( ( p ) => p . t > 10.5 && p . t < 11 ) ?. v ?? 0 ) . toBe ( 0 ) ;
341- // And it cuts once the voice speaks, a second later. Depth is per band and
342- // relative to that band's own peak in the voice, so the invariant is that the
343- // envelope gets most of the way to what the analysis put on the node — not a
344- // fixed number of dB, which changes with the band the analysis chose.
345- const bandGain = Number ( carveNode . params ?. gain ?? 0 ) ;
346- // At least half the depth the analysis put on the node; the exact floor
347- // depends on which band it chose and how the envelope was thinned.
348- expect ( Math . min ( ...lane . points . map ( ( p ) => p . v ) ) ) . toBeLessThanOrEqual ( bandGain * 0.5 ) ;
349- // Ends back at no cut, so the bed is not left dipped for the rest of the clip.
350- expect ( lane . points . at ( - 1 ) ! . v ) . toBe ( 0 ) ;
351- } ) ;
352-
353- it ( "adds a gain stage that ducks the bed under the voice, automated when dynamic" , async ( ) => {
354- // Carving frequencies cannot beat a bed that is simply louder than the
355- // voice. The level half rides a gain node the carve owns, so the track's own
356- // volume lane is left alone.
357- stubDecode ( ) ;
358- const { host, onSetAttributeQuiet } = mount ( {
359- "fx-chain" : carvedChain ,
360- "fx-carve" : settings ( true , { strength : 1 , source : "" } ) ,
361- start : "0" ,
362- } ) ;
363- const vo = document . getElementById ( "vo" ) ! ;
364- vo . setAttribute ( "data-start" , "0" ) ;
365- vo . setAttribute ( "src" , "voice.wav" ) ;
366- // The bed is measured too — "how far over the voice is it" needs both.
367- document . getElementById ( "bed" ) ! . setAttribute ( "src" , "bed.m4a" ) ;
368- await act ( async ( ) => {
369- pickSource ( host , "vo" ) ;
370- } ) ;
371-
372- const nodes = writeFor ( onSetAttributeQuiet . mock . calls , "data-fx-chain" ) . nodes ;
373- const gain = nodes . find ( ( n : { type : string } ) => n . type === "gain" ) ;
374- expect ( gain ) . toBeTruthy ( ) ;
375- expect ( gain . fromCarve ) . toBe ( true ) ;
376- // Dynamic hands the value to the envelope, so the static one stays at unity.
377- expect ( gain . params . gain ) . toBe ( 0 ) ;
378-
379- const lanes = writeFor ( onSetAttributeQuiet . mock . calls , "data-automation" ) . lanes ;
380- const duckLane = lanes . find ( ( l : { target : string } ) => l . target === `fx.${ gain . id } .gain` ) ;
381- expect ( duckLane ) . toBeTruthy ( ) ;
382- expect ( Math . min ( ...duckLane . points . map ( ( p : { v : number } ) => p . v ) ) ) . toBeLessThan ( 0 ) ;
383- // Every carved band gets an envelope reaching that band's own analysed depth.
384- for ( const node of nodes . filter ( ( n : { type : string } ) => n . type === "peaking" ) ) {
385- const lane = lanes . find ( ( l : { target : string } ) => l . target === `fx.${ node . id } .gain` ) as
386- | { points : { v : number } [ ] }
387- | undefined ;
388- expect ( lane , `band ${ node . id } has no envelope` ) . toBeTruthy ( ) ;
389- const deepest = Math . min ( ...lane ! . points . map ( ( p ) => p . v ) ) ;
390- expect ( deepest ) . toBeLessThanOrEqual ( 0 ) ;
391- expect ( deepest ) . toBeGreaterThanOrEqual ( node . params . gain - 0.2 ) ;
392- expect ( deepest ) . toBeLessThanOrEqual ( node . params . gain * 0.5 ) ;
393- }
394- // The author's own volume lane is not something a carve gets to touch.
395- expect ( lanes . some ( ( l : { target : string } ) => l . target === "volume" ) ) . toBe ( false ) ;
396- } ) ;
397-
398- it ( "holds one measured value when the carve is not dynamic" , async ( ) => {
292+ it ( "holds one measured value from the voice and bed" , async ( ) => {
399293 stubDecode ( ) ;
400294 const { host, onSetAttributeQuiet } = mount ( {
401295 "fx-chain" : carvedChain ,
402- "fx-carve" : settings ( false , { strength : 1 , source : "" } ) ,
296+ "fx-carve" : settings ( { strength : 1 , sources : [ ] } ) ,
403297 start : "0" ,
404298 } ) ;
405299 const vo = document . getElementById ( "vo" ) ! ;
@@ -413,15 +307,15 @@ describe("AudioFxGroup dynamic carve", () => {
413307 const nodes = writeFor ( onSetAttributeQuiet . mock . calls , "data-fx-chain" ) . nodes ;
414308 const gain = nodes . find ( ( n : { type : string } ) => n . type === "gain" ) ;
415309 expect ( gain . params . gain ) . toBeLessThan ( 0 ) ;
416- // Nothing to schedule: a static carve is a value, not an envelope.
310+ // Nothing to schedule: a carve is a value, not an envelope.
417311 expect ( onSetAttributeQuiet . mock . calls . some ( ( c ) => c [ 0 ] === "data-automation" ) ) . toBe ( false ) ;
418312 } ) ;
419313
420314 it ( "carves frequencies only when the duck is off" , async ( ) => {
421315 stubDecode ( ) ;
422316 const { host, onSetAttributeQuiet } = mount ( {
423317 "fx-chain" : carvedChain ,
424- "fx-carve" : settings ( true , { strength : 0 , source : "" } ) ,
318+ "fx-carve" : settings ( { strength : 0 , sources : [ ] } ) ,
425319 start : "0" ,
426320 } ) ;
427321 document . getElementById ( "vo" ) ! . setAttribute ( "src" , "voice.wav" ) ;
@@ -439,7 +333,7 @@ describe("AudioFxGroup dynamic carve", () => {
439333 stubDecode ( ) ;
440334 const { host, onSetAttributeQuiet } = mount ( {
441335 "fx-chain" : JSON . stringify ( { version : 1 , nodes : [ ] } ) ,
442- "fx-carve" : JSON . stringify ( { source : "" , strength : 0.25 , dynamic : true } ) ,
336+ "fx-carve" : JSON . stringify ( { sources : [ ] , strength : 0.25 } ) ,
443337 start : "0" ,
444338 } ) ;
445339 document . getElementById ( "vo" ) ! . setAttribute ( "src" , "voice.wav" ) ;
@@ -448,44 +342,17 @@ describe("AudioFxGroup dynamic carve", () => {
448342 pickSource ( host , "vo" ) ;
449343 } ) ;
450344 const written = onSetAttributeQuiet . mock . calls . map ( ( c ) => c [ 0 ] ) ;
451- expect ( written ) . toEqual ( [ "data-fx-carve" , "data-fx-chain" , "data-automation" ] ) ;
345+ expect ( written ) . toEqual ( [ "data-fx-carve" , "data-fx-chain" ] ) ;
452346 const nodes = JSON . parse (
453347 String ( onSetAttributeQuiet . mock . calls . find ( ( c ) => c [ 0 ] === "data-fx-chain" ) ! [ 1 ] ) ,
454348 ) . nodes ;
455349 expect ( nodes . every ( ( n : { fromCarve ?: boolean } ) => n . fromCarve ) ) . toBe ( true ) ;
456350 } ) ;
457351
458- it ( "re-applies when dynamic is switched on, not just when strength moves" , async ( ) => {
459- stubDecode ( ) ;
460- const carvedAlready = JSON . stringify ( {
461- version : 1 ,
462- nodes : [
463- {
464- type : "peaking" ,
465- id : "n1" ,
466- fromCarve : true ,
467- params : { frequency : 1000 , gain : - 6 , q : 1.4 } ,
468- } ,
469- ] ,
470- } ) ;
471- const { host, onSetAttributeQuiet } = mount ( {
472- "fx-chain" : carvedAlready ,
473- "fx-carve" : settings ( false , { strength : 0.25 } ) ,
474- start : "0" ,
475- } ) ;
476- document . getElementById ( "vo" ) ! . setAttribute ( "src" , "voice.wav" ) ;
477- document . getElementById ( "bed" ) ! . setAttribute ( "src" , "bed.m4a" ) ;
478- await act ( async ( ) => {
479- host . querySelector < HTMLInputElement > ( ".hf-fx-carve-dynamic" ) ! . click ( ) ;
480- } ) ;
481- // Static and dynamic are different chains, so the switch has to rebuild them.
482- expect ( onSetAttributeQuiet . mock . calls . some ( ( c ) => c [ 0 ] === "data-fx-chain" ) ) . toBe ( true ) ;
483- } ) ;
484-
485352 it ( "re-applies an existing carve when strength moves" , async ( ) => {
486353 // Strength is the whole control surface, so it has to act on what is already
487- // applied. Left to the button alone, a carve kept the filters and envelopes
488- // its old strength produced and the knob silently described nothing.
354+ // applied. Left to the button alone, a carve kept the filters its old
355+ // strength produced and the knob silently described nothing.
489356 stubDecode ( ) ;
490357 const carvedAlready = JSON . stringify ( {
491358 version : 1 ,
@@ -501,7 +368,7 @@ describe("AudioFxGroup dynamic carve", () => {
501368 } ) ;
502369 const { host, onSetAttributeQuiet } = mount ( {
503370 "fx-chain" : carvedAlready ,
504- "fx-carve" : settings ( true , { strength : 0.25 } ) ,
371+ "fx-carve" : settings ( { strength : 0.25 } ) ,
505372 start : "0" ,
506373 } ) ;
507374 document . getElementById ( "vo" ) ! . setAttribute ( "src" , "voice.wav" ) ;
@@ -516,9 +383,8 @@ describe("AudioFxGroup dynamic carve", () => {
516383
517384 const written = onSetAttributeQuiet . mock . calls . map ( ( c ) => c [ 0 ] ) ;
518385 expect ( written ) . toContain ( "data-fx-carve" ) ;
519- // The settings land first, then the filters they imply, then the envelopes .
386+ // The settings land first, then the filters they imply.
520387 expect ( written . indexOf ( "data-fx-carve" ) ) . toBeLessThan ( written . indexOf ( "data-fx-chain" ) ) ;
521- expect ( written . indexOf ( "data-fx-chain" ) ) . toBeLessThan ( written . indexOf ( "data-automation" ) ) ;
522388
523389 const chainWrite = onSetAttributeQuiet . mock . calls . find ( ( c ) => c [ 0 ] === "data-fx-chain" ) ;
524390 const nodes = JSON . parse ( String ( chainWrite ! [ 1 ] ) ) . nodes ;
@@ -537,7 +403,7 @@ describe("AudioFxGroup dynamic carve", () => {
537403 stubDecode ( ) ;
538404 const { host, onSetAttributeQuiet } = mount ( {
539405 "fx-chain" : JSON . stringify ( { version : 1 , nodes : [ ] } ) ,
540- "fx-carve" : settings ( true , { strength : 0.25 , source : "" } ) ,
406+ "fx-carve" : settings ( { strength : 0.25 , sources : [ ] } ) ,
541407 start : "0" ,
542408 } ) ;
543409 const dial = host . querySelector < HTMLInputElement > ( ".hf-fx-carve input[type=range]" ) ! ;
@@ -566,7 +432,7 @@ describe("AudioFxGroup dynamic carve", () => {
566432 } ) ;
567433 const { host, onSetAttributeQuiet, onSetAttributeLive } = mount ( {
568434 "fx-chain" : carvedAlready ,
569- "fx-carve" : settings ( true , { strength : 0.25 } ) ,
435+ "fx-carve" : settings ( { strength : 0.25 } ) ,
570436 start : "0" ,
571437 } ) ;
572438 document . getElementById ( "vo" ) ! . setAttribute ( "src" , "voice.wav" ) ;
@@ -580,36 +446,6 @@ describe("AudioFxGroup dynamic carve", () => {
580446 expect ( onSetAttributeLive . mock . calls . every ( ( c ) => c [ 0 ] === "data-fx-carve" ) ) . toBe ( true ) ;
581447 expect ( onSetAttributeQuiet . mock . calls . some ( ( c ) => c [ 0 ] === "data-fx-chain" ) ) . toBe ( false ) ;
582448 } ) ;
583-
584- it ( "drops the envelopes when dynamic is switched back off" , async ( ) => {
585- // An automated gain ignores the panel's depth, so leaving the lanes behind
586- // would keep the filters following a voice with nothing saying they do.
587- const automation = JSON . stringify ( {
588- version : 1 ,
589- lanes : [
590- { target : "fx.n2.gain" , points : [ { t : 0 , v : 0 } ] } ,
591- { target : "volume" , points : [ { t : 0 , v : 1 } ] } ,
592- ] ,
593- } ) ;
594- const withCarveNode = JSON . stringify ( {
595- version : 1 ,
596- nodes : [
597- { type : "peaking" , id : "n2" , fromCarve : true , params : { frequency : 1000 , gain : - 6 } } ,
598- ] ,
599- } ) ;
600- const { host, onSetAttributeQuiet } = mount ( {
601- "fx-chain" : withCarveNode ,
602- "fx-carve" : settings ( true ) ,
603- automation,
604- } ) ;
605- await act ( async ( ) => {
606- dynamicBox ( host ) . click ( ) ;
607- } ) ;
608- const write = onSetAttributeQuiet . mock . calls . find ( ( c ) => c [ 0 ] === "data-automation" ) ;
609- expect ( write ) . toBeTruthy ( ) ;
610- const lanes = JSON . parse ( String ( write ! [ 1 ] ) ) . lanes ;
611- expect ( lanes . map ( ( l : { target : string } ) => l . target ) ) . toEqual ( [ "volume" ] ) ;
612- } ) ;
613449} ) ;
614450
615451describe ( "AudioFxGroup successive edits" , ( ) => {
0 commit comments