@@ -39,6 +39,7 @@ function createMockAudioContext(currentTime = 100) {
3939 currentTime,
4040 state : "running" ,
4141 resume : vi . fn ( ) ,
42+ suspend : vi . fn ( ( ) => Promise . resolve ( ) ) ,
4243 createBufferSource : vi . fn ( ( ) => sourceNode ) ,
4344 createMediaElementSource : vi . fn ( ( ) => mediaElementSourceNode ) ,
4445 createGain : vi . fn ( ( ) => gainNode ) ,
@@ -62,6 +63,8 @@ const mockBuffer = {} as AudioBuffer;
6263const mockEl = {
6364 muted : false ,
6465 volume : 0.4 ,
66+ paused : true ,
67+ addEventListener : ( ) => { } ,
6568 getAttribute : ( name : string ) => ( name === "data-playback-rate" ? "1" : null ) ,
6669} as unknown as HTMLMediaElement ;
6770
@@ -435,7 +438,7 @@ describe("WebAudioTransport", () => {
435438 it ( "keeps author boost above unity on the per-element gain node" , async ( ) => {
436439 const { transport, mock, gen } = setupTransport ( 100 ) ;
437440
438- await transport . schedulePlayback ( mockEl , mockBuffer , 0 , 0 , 0 , 1 , gen ) ;
441+ await transport . schedulePlayback ( mockEl , mockBuffer , 0 , 0 , ( ) => 0 , 1 , gen ) ;
439442 transport . setElementVolume ( mockEl , 3.98 ) ;
440443
441444 expect ( mock . gainNode . gain . value ) . toBeCloseTo ( 3.98 , 5 ) ;
@@ -444,39 +447,39 @@ describe("WebAudioTransport", () => {
444447 it ( "starts in-progress clips immediately with correct buffer offset" , async ( ) => {
445448 const { transport, mock, gen } = setupTransport ( 100 ) ;
446449
447- await transport . schedulePlayback ( mockEl , mockBuffer , 5 , 0 , 8 , 1 , gen ) ;
450+ await transport . schedulePlayback ( mockEl , mockBuffer , 5 , 0 , ( ) => 8 , 1 , gen ) ;
448451
449452 expect ( mock . startFn ) . toHaveBeenCalledWith ( 0 , 3 ) ;
450453 } ) ;
451454
452455 it ( "starts in-progress clips with mediaStart offset" , async ( ) => {
453456 const { transport, mock, gen } = setupTransport ( 100 ) ;
454457
455- await transport . schedulePlayback ( mockEl , mockBuffer , 5 , 2 , 8 , 1 , gen ) ;
458+ await transport . schedulePlayback ( mockEl , mockBuffer , 5 , 2 , ( ) => 8 , 1 , gen ) ;
456459
457460 expect ( mock . startFn ) . toHaveBeenCalledWith ( 0 , 5 ) ;
458461 } ) ;
459462
460463 it ( "schedules future clips with delay instead of playing immediately" , async ( ) => {
461464 const { transport, mock, gen } = setupTransport ( 100 ) ;
462465
463- await transport . schedulePlayback ( mockEl , mockBuffer , 10 , 0 , 2 , 1 , gen ) ;
466+ await transport . schedulePlayback ( mockEl , mockBuffer , 10 , 0 , ( ) => 2 , 1 , gen ) ;
464467
465468 expect ( mock . startFn ) . toHaveBeenCalledWith ( 108 , 0 ) ;
466469 } ) ;
467470
468471 it ( "schedules future clips with correct mediaStart" , async ( ) => {
469472 const { transport, mock, gen } = setupTransport ( 100 ) ;
470473
471- await transport . schedulePlayback ( mockEl , mockBuffer , 10 , 1.5 , 2 , 1 , gen ) ;
474+ await transport . schedulePlayback ( mockEl , mockBuffer , 10 , 1.5 , ( ) => 2 , 1 , gen ) ;
472475
473476 expect ( mock . startFn ) . toHaveBeenCalledWith ( 108 , 1.5 ) ;
474477 } ) ;
475478
476479 it ( "starts clips at exact composition start time immediately" , async ( ) => {
477480 const { transport, mock, gen } = setupTransport ( 100 ) ;
478481
479- await transport . schedulePlayback ( mockEl , mockBuffer , 5 , 0 , 5 , 1 , gen ) ;
482+ await transport . schedulePlayback ( mockEl , mockBuffer , 5 , 0 , ( ) => 5 , 1 , gen ) ;
480483
481484 expect ( mock . startFn ) . toHaveBeenCalledWith ( 0 , 0 ) ;
482485 } ) ;
@@ -486,35 +489,45 @@ describe("WebAudioTransport", () => {
486489 it ( "bounds an in-progress clip to its remaining authored window" , async ( ) => {
487490 const { transport, mock, gen } = setupTransport ( 100 ) ;
488491 // compStart=5, mediaStart=0, compTime=8 → elapsed=3; clipDuration=10 → 7 left
489- await transport . schedulePlayback ( mockEl , mockBuffer , 5 , 0 , 8 , 1 , gen , 1 , 10 ) ;
492+ await transport . schedulePlayback ( mockEl , mockBuffer , 5 , 0 , ( ) => 8 , 1 , gen , 1 , 10 ) ;
490493 expect ( mock . startFn ) . toHaveBeenCalledWith ( 0 , 3 , 7 ) ;
491494 } ) ;
492495
493496 it ( "bounds a future clip to its full authored window" , async ( ) => {
494497 const { transport, mock, gen } = setupTransport ( 100 ) ;
495498 // compStart=10, mediaStart=1.5, compTime=2 → elapsed=-8 → delay 8; clipDuration=4
496- await transport . schedulePlayback ( mockEl , mockBuffer , 10 , 1.5 , 2 , 1 , gen , 1 , 4 ) ;
499+ await transport . schedulePlayback ( mockEl , mockBuffer , 10 , 1.5 , ( ) => 2 , 1 , gen , 1 , 4 ) ;
497500 expect ( mock . startFn ) . toHaveBeenCalledWith ( 108 , 1.5 , 4 ) ;
498501 } ) ;
499502
500503 it ( "does not schedule a clip whose window has already elapsed" , async ( ) => {
501504 const { transport, mock, gen } = setupTransport ( 100 ) ;
502505 // elapsed=15 > clipDuration=10 → nothing to play
503- const result = await transport . schedulePlayback ( mockEl , mockBuffer , 5 , 0 , 20 , 1 , gen , 1 , 10 ) ;
506+ const result = await transport . schedulePlayback (
507+ mockEl ,
508+ mockBuffer ,
509+ 5 ,
510+ 0 ,
511+ ( ) => 20 ,
512+ 1 ,
513+ gen ,
514+ 1 ,
515+ 10 ,
516+ ) ;
504517 expect ( result ) . toBeNull ( ) ;
505518 expect ( mock . startFn ) . not . toHaveBeenCalled ( ) ;
506519 } ) ;
507520
508521 it ( "keeps source bounds in authored media time when global rate changes" , async ( ) => {
509522 const { transport, mock, gen } = setupTransport ( 100 ) ;
510523 // Global rate=2 changes wallclock speed, not source-time span.
511- await transport . schedulePlayback ( mockEl , mockBuffer , 5 , 0 , 8 , 1 , gen , 2 , 10 ) ;
524+ await transport . schedulePlayback ( mockEl , mockBuffer , 5 , 0 , ( ) => 8 , 1 , gen , 2 , 10 ) ;
512525 expect ( mock . startFn ) . toHaveBeenCalledWith ( 0 , 3 , 7 ) ;
513526 } ) ;
514527
515528 it ( "plays unbounded when clipDuration is omitted (legacy behavior)" , async ( ) => {
516529 const { transport, mock, gen } = setupTransport ( 100 ) ;
517- await transport . schedulePlayback ( mockEl , mockBuffer , 5 , 0 , 8 , 1 , gen ) ;
530+ await transport . schedulePlayback ( mockEl , mockBuffer , 5 , 0 , ( ) => 8 , 1 , gen ) ;
518531 expect ( mock . startFn ) . toHaveBeenCalledWith ( 0 , 3 ) ;
519532 } ) ;
520533 } ) ;
@@ -530,7 +543,7 @@ describe("WebAudioTransport", () => {
530543 // At composition t=0.5 with mediaStart=1, authored 2x has consumed one
531544 // source second. Global 0.5x makes the source node's wallclock rate 1x,
532545 // but the composition clock must still advance at global 0.5x.
533- await transport . schedulePlayback ( el , mockBuffer , 0 , 1 , 0.5 , 1 , gen , 0.5 , 2 ) ;
546+ await transport . schedulePlayback ( el , mockBuffer , 0 , 1 , ( ) => 0.5 , 1 , gen , 0.5 , 2 ) ;
534547
535548 expect ( mock . sourceNode . playbackRate . value ) . toBe ( 1 ) ;
536549 expect ( mock . startFn ) . toHaveBeenCalledWith ( 0 , 2 , 3 ) ;
@@ -544,15 +557,15 @@ describe("WebAudioTransport", () => {
544557 it ( "sets sourceNode.playbackRate.value when rate is provided" , async ( ) => {
545558 const { transport, mock, gen } = setupTransport ( 100 ) ;
546559
547- await transport . schedulePlayback ( mockEl , mockBuffer , 5 , 0 , 8 , 1 , gen , 2 ) ;
560+ await transport . schedulePlayback ( mockEl , mockBuffer , 5 , 0 , ( ) => 8 , 1 , gen , 2 ) ;
548561
549562 expect ( mock . sourceNode . playbackRate . value ) . toBe ( 2 ) ;
550563 } ) ;
551564
552565 it ( "defaults rate to 1 when not provided" , async ( ) => {
553566 const { transport, mock, gen } = setupTransport ( 100 ) ;
554567
555- await transport . schedulePlayback ( mockEl , mockBuffer , 5 , 0 , 8 , 1 , gen ) ;
568+ await transport . schedulePlayback ( mockEl , mockBuffer , 5 , 0 , ( ) => 8 , 1 , gen ) ;
556569
557570 expect ( mock . sourceNode . playbackRate . value ) . toBe ( 1 ) ;
558571 } ) ;
@@ -561,23 +574,23 @@ describe("WebAudioTransport", () => {
561574 const { transport, mock, gen } = setupTransport ( 100 ) ;
562575
563576 // compStart=10, compositionTime=2, rate=2 → 8s of comp time = 4s wallclock
564- await transport . schedulePlayback ( mockEl , mockBuffer , 10 , 0 , 2 , 1 , gen , 2 ) ;
577+ await transport . schedulePlayback ( mockEl , mockBuffer , 10 , 0 , ( ) => 2 , 1 , gen , 2 ) ;
565578
566579 expect ( mock . startFn ) . toHaveBeenCalledWith ( 104 , 0 ) ;
567580 } ) ;
568581
569582 it ( "keeps in-progress buffer offset at elapsed + mediaStart regardless of rate" , async ( ) => {
570583 const { transport, mock, gen } = setupTransport ( 100 ) ;
571584
572- await transport . schedulePlayback ( mockEl , mockBuffer , 5 , 0 , 8 , 1 , gen , 2 ) ;
585+ await transport . schedulePlayback ( mockEl , mockBuffer , 5 , 0 , ( ) => 8 , 1 , gen , 2 ) ;
573586
574587 expect ( mock . startFn ) . toHaveBeenCalledWith ( 0 , 3 ) ;
575588 } ) ;
576589
577590 it ( "setRate updates active sources in place" , async ( ) => {
578591 const { transport, mock, gen } = setupTransport ( 100 ) ;
579592
580- await transport . schedulePlayback ( mockEl , mockBuffer , 5 , 0 , 8 , 1 , gen , 1 ) ;
593+ await transport . schedulePlayback ( mockEl , mockBuffer , 5 , 0 , ( ) => 8 , 1 , gen , 1 ) ;
581594 expect ( mock . sourceNode . playbackRate . value ) . toBe ( 1 ) ;
582595
583596 transport . setRate ( 2 ) ;
@@ -590,7 +603,7 @@ describe("WebAudioTransport", () => {
590603 // scheduled, so bumping playbackRate alone left every automated parameter
591604 // running its original plan over audio moving at a different speed.
592605 const { transport, mock, gen } = setupTransport ( 100 ) ;
593- await transport . schedulePlayback ( mockEl , mockBuffer , 5 , 0 , 8 , 1 , gen , 1 ) ;
606+ await transport . schedulePlayback ( mockEl , mockBuffer , 5 , 0 , ( ) => 8 , 1 , gen , 1 ) ;
594607 const active = ( transport as unknown as { _activeSources : { fx ?: unknown } [ ] } )
595608 . _activeSources ;
596609 const setRate = vi . fn ( ) ;
@@ -609,7 +622,7 @@ describe("WebAudioTransport", () => {
609622
610623 it ( "setRate is a no-op when the rate is unchanged" , async ( ) => {
611624 const { transport, mock, gen } = setupTransport ( 100 ) ;
612- await transport . schedulePlayback ( mockEl , mockBuffer , 5 , 0 , 8 , 1 , gen , 2 ) ;
625+ await transport . schedulePlayback ( mockEl , mockBuffer , 5 , 0 , ( ) => 8 , 1 , gen , 2 ) ;
613626
614627 mock . ctx . currentTime = 100.5 ;
615628 const timeBefore = transport . getTime ( ) ;
@@ -624,7 +637,7 @@ describe("WebAudioTransport", () => {
624637
625638 it ( "setRate clamps non-finite or non-positive values to 1" , async ( ) => {
626639 const { transport, mock, gen } = setupTransport ( 100 ) ;
627- await transport . schedulePlayback ( mockEl , mockBuffer , 5 , 0 , 8 , 1 , gen , 2 ) ;
640+ await transport . schedulePlayback ( mockEl , mockBuffer , 5 , 0 , ( ) => 8 , 1 , gen , 2 ) ;
628641 expect ( mock . sourceNode . playbackRate . value ) . toBe ( 2 ) ;
629642
630643 transport . setRate ( Number . NaN ) ;
@@ -642,7 +655,7 @@ describe("WebAudioTransport", () => {
642655 it ( "getTime advances at the configured rate" , async ( ) => {
643656 const { transport, mock, gen } = setupTransport ( 100 ) ;
644657
645- await transport . schedulePlayback ( mockEl , mockBuffer , 5 , 0 , 8 , 1 , gen , 2 ) ;
658+ await transport . schedulePlayback ( mockEl , mockBuffer , 5 , 0 , ( ) => 8 , 1 , gen , 2 ) ;
646659
647660 // At schedule time, ctx.currentTime=100, compositionTime=8.
648661 expect ( transport . getTime ( ) ) . toBeCloseTo ( 8 , 10 ) ;
@@ -656,7 +669,7 @@ describe("WebAudioTransport", () => {
656669 it ( "getTime tracks composition time after a mid-playback setRate" , async ( ) => {
657670 const { transport, mock, gen } = setupTransport ( 100 ) ;
658671
659- await transport . schedulePlayback ( mockEl , mockBuffer , 5 , 0 , 8 , 1 , gen , 1 ) ;
672+ await transport . schedulePlayback ( mockEl , mockBuffer , 5 , 0 , ( ) => 8 , 1 , gen , 1 ) ;
660673 expect ( transport . getTime ( ) ) . toBeCloseTo ( 8 , 10 ) ;
661674
662675 // 0.5s passes at rate=1 → composition time = 8.5
@@ -681,7 +694,7 @@ describe("WebAudioTransport", () => {
681694 getAttribute : ( name : string ) => ( name === "data-playback-rate" ? "1" : null ) ,
682695 } as unknown as HTMLMediaElement ;
683696
684- await transport . schedulePlayback ( el , mockBuffer , 0 , 0 , 0 , 1 , gen ) ;
697+ await transport . schedulePlayback ( el , mockBuffer , 0 , 0 , ( ) => 0 , 1 , gen ) ;
685698 expect ( transport . ownsClock ( ) ) . toBe ( true ) ;
686699 expect ( el . muted ) . toBe ( true ) ;
687700
@@ -698,7 +711,7 @@ describe("WebAudioTransport", () => {
698711 getAttribute : ( name : string ) => ( name === "data-playback-rate" ? "1" : null ) ,
699712 } as unknown as HTMLMediaElement ;
700713
701- await transport . schedulePlayback ( el , mockBuffer , 0 , 0 , 0 , 1 , gen ) ;
714+ await transport . schedulePlayback ( el , mockBuffer , 0 , 0 , ( ) => 0 , 1 , gen ) ;
702715 expect ( el . muted ) . toBe ( true ) ;
703716
704717 mock . sourceNode . _fireEnded ( ) ;
@@ -712,7 +725,7 @@ describe("WebAudioTransport", () => {
712725 // already removed this entry — so the handle, its MutationObserver and any
713726 // running LFO survived the clip for the rest of the session.
714727 const { transport, mock, gen } = setupTransport ( 100 ) ;
715- await transport . schedulePlayback ( mockEl , mockBuffer , 0 , 0 , 0 , 1 , gen ) ;
728+ await transport . schedulePlayback ( mockEl , mockBuffer , 0 , 0 , ( ) => 0 , 1 , gen ) ;
716729
717730 mock . sourceNode . _fireEnded ( ) ;
718731
@@ -723,7 +736,7 @@ describe("WebAudioTransport", () => {
723736 it ( "registers onended listener on the sourceNode" , async ( ) => {
724737 const { transport, mock, gen } = setupTransport ( 100 ) ;
725738
726- await transport . schedulePlayback ( mockEl , mockBuffer , 0 , 0 , 0 , 1 , gen ) ;
739+ await transport . schedulePlayback ( mockEl , mockBuffer , 0 , 0 , ( ) => 0 , 1 , gen ) ;
727740
728741 expect ( mock . sourceNode . addEventListener ) . toHaveBeenCalledWith ( "ended" , expect . any ( Function ) ) ;
729742 } ) ;
@@ -735,7 +748,7 @@ describe("WebAudioTransport", () => {
735748 getAttribute : ( name : string ) => ( name === "data-playback-rate" ? "1" : null ) ,
736749 } as unknown as HTMLMediaElement ;
737750
738- await transport . schedulePlayback ( el , mockBuffer , 0 , 0 , 0 , 1 , gen ) ;
751+ await transport . schedulePlayback ( el , mockBuffer , 0 , 0 , ( ) => 0 , 1 , gen ) ;
739752 expect ( el . muted ) . toBe ( true ) ;
740753
741754 transport . stopAll ( ) ;
@@ -774,6 +787,7 @@ describe("WebAudioTransport", () => {
774787 currentTime,
775788 state : "running" ,
776789 resume : vi . fn ( ) ,
790+ suspend : vi . fn ( ( ) => Promise . resolve ( ) ) ,
777791 createBufferSource : vi . fn ( ( ) => ( {
778792 buffer : null as AudioBuffer | null ,
779793 playbackRate : { value : 1 } ,
@@ -853,7 +867,7 @@ describe("WebAudioTransport", () => {
853867 groupId ?: string ,
854868 ) : Promise < HTMLMediaElement > {
855869 const el = groupedAudioEl ( id , groupId ) ;
856- await transport . schedulePlayback ( el , mockBuffer , 0 , 0 , 0 , 1 , gen ) ;
870+ await transport . schedulePlayback ( el , mockBuffer , 0 , 0 , ( ) => 0 , 1 , gen ) ;
857871 return el ;
858872 }
859873
0 commit comments