@@ -24,6 +24,12 @@ function createMockAudioBuffer(samples: number[], sampleRate = 48000): AudioBuff
2424 } as unknown as AudioBuffer ;
2525}
2626
27+ function equalPowerPan ( pan : number ) : { left : number ; right : number } {
28+ const clamped = Math . max ( - 1 , Math . min ( 1 , pan ) ) ;
29+ const angle = ( ( clamped + 1 ) * Math . PI ) / 4 ;
30+ return { left : Math . cos ( angle ) , right : Math . sin ( angle ) } ;
31+ }
32+
2733describe ( 'TauriBackend' , ( ) => {
2834 let backend : TauriBackend ;
2935
@@ -70,22 +76,22 @@ describe('TauriBackend', () => {
7076
7177 it ( 'getTrackMeter returns silent meter' , ( ) => {
7278 const meter = backend . getTrackMeter ( 'any-track' ) ;
73- expect ( meter . level ) . toBe ( - Infinity ) ;
79+ expect ( meter . level ) . toBe ( 0 ) ;
7480 expect ( meter . clipped ) . toBe ( false ) ;
7581 } ) ;
7682
77- it ( 'getTrackLevel returns -Infinity ' , ( ) => {
78- expect ( backend . getTrackLevel ( 'any-track' ) ) . toBe ( - Infinity ) ;
83+ it ( 'getTrackLevel returns silence by default ' , ( ) => {
84+ expect ( backend . getTrackLevel ( 'any-track' ) ) . toBe ( 0 ) ;
7985 } ) ;
8086
8187 it ( 'getMasterMeter returns silent meter' , ( ) => {
8288 const meter = backend . getMasterMeter ( 'output' ) ;
83- expect ( meter . level ) . toBe ( - Infinity ) ;
89+ expect ( meter . level ) . toBe ( 0 ) ;
8490 expect ( meter . clipped ) . toBe ( false ) ;
8591 } ) ;
8692
87- it ( 'getMasterLevel returns -Infinity ' , ( ) => {
88- expect ( backend . getMasterLevel ( 'input' ) ) . toBe ( - Infinity ) ;
93+ it ( 'getMasterLevel returns silence by default ' , ( ) => {
94+ expect ( backend . getMasterLevel ( 'input' ) ) . toBe ( 0 ) ;
8995 } ) ;
9096
9197 it ( 'getTrackSpectrum returns null' , ( ) => {
@@ -207,6 +213,7 @@ describe('TauriBackend', () => {
207213
208214 it ( 'schedulePlayback sends clips through native schedule and starts transport' , async ( ) => {
209215 const buffer = createMockAudioBuffer ( [ 0.125 , 0.25 , 0.5 , 1 ] ) ;
216+ const centerPan = equalPowerPan ( 0 ) ;
210217
211218 backend . schedulePlayback ( [
212219 {
@@ -217,7 +224,7 @@ describe('TauriBackend', () => {
217224 audioOffset : 0 ,
218225 clipDuration : 4 / 48000 ,
219226 } ,
220- ] , 1 , 2 ) ;
227+ ] , 1 , 3 ) ;
221228 await Promise . resolve ( ) ;
222229 await Promise . resolve ( ) ;
223230 await Promise . resolve ( ) ;
@@ -228,17 +235,63 @@ describe('TauriBackend', () => {
228235 startSample : 48000 ,
229236 lengthSamples : 4 ,
230237 gain : 1 ,
231- audioData : [ 0.125 , 0.125 , 0.25 , 0.25 , 0.5 , 0.5 , 1 , 1 ] ,
238+ audioData : [
239+ 0.125 * centerPan . left ,
240+ 0.125 * centerPan . right ,
241+ 0.25 * centerPan . left ,
242+ 0.25 * centerPan . right ,
243+ 0.5 * centerPan . left ,
244+ 0.5 * centerPan . right ,
245+ centerPan . left ,
246+ centerPan . right ,
247+ ] ,
232248 } ,
233249 ] ,
234250 } ) ;
235251 expect ( invokeMock ) . toHaveBeenCalledWith ( 'audio_transport_seek' , { samplePosition : 48000 } ) ;
236252 expect ( invokeMock ) . toHaveBeenCalledWith ( 'audio_transport_play' ) ;
237253 } ) ;
238254
255+ it ( 'uses the scheduled seek sample even if transport events update the cache first' , async ( ) => {
256+ let positionHandler : ( ( event : { payload : number } ) => void ) | null = null ;
257+ let resolveSchedule ! : ( ) => void ;
258+ invokeMock . mockImplementation ( ( command ) => {
259+ if ( command === 'audio_clip_set_schedule' ) {
260+ return new Promise ( ( resolve ) => {
261+ resolveSchedule = ( ) => resolve ( undefined ) ;
262+ } ) ;
263+ }
264+ return Promise . resolve ( undefined ) ;
265+ } ) ;
266+ listenMock . mockImplementation ( async ( _event , handler ) => {
267+ positionHandler = handler as ( event : { payload : number } ) => void ;
268+ return vi . fn ( ) ;
269+ } ) ;
270+ const buffer = createMockAudioBuffer ( [ 1 ] ) ;
271+ backend . setTimeUpdateCallback ( ( ) => { } ) ;
272+
273+ backend . schedulePlayback ( [
274+ {
275+ clipId : 'clip-1' ,
276+ trackId : 'track-1' ,
277+ startTime : 1 ,
278+ buffer,
279+ audioOffset : 0 ,
280+ clipDuration : 1 / 48000 ,
281+ } ,
282+ ] , 1 , 3 ) ;
283+ positionHandler ?.( { payload : 96000 } ) ;
284+ resolveSchedule ( ) ;
285+ await Promise . resolve ( ) ;
286+ await Promise . resolve ( ) ;
287+
288+ expect ( invokeMock ) . toHaveBeenCalledWith ( 'audio_transport_seek' , { samplePosition : 48000 } ) ;
289+ } ) ;
290+
239291 it ( 'applies cached track volume, pan, mute, and solo before native scheduling' , async ( ) => {
240292 const audibleBuffer = createMockAudioBuffer ( [ 1 , 1 ] ) ;
241293 const mutedBuffer = createMockAudioBuffer ( [ 1 , 1 ] ) ;
294+ const pan = equalPowerPan ( - 0.5 ) ;
242295 backend . ensureTrack ( 'audible' ) ;
243296 backend . ensureTrack ( 'muted' ) ;
244297 backend . setTrackParams ( 'audible' , { volume : 0.5 , pan : - 0.5 , soloed : true } ) ;
@@ -272,14 +325,15 @@ describe('TauriBackend', () => {
272325 startSample : 0 ,
273326 lengthSamples : 2 ,
274327 gain : 1 ,
275- audioData : [ 0.5 , 0.25 , 0.5 , 0.25 ] ,
328+ audioData : [ 0.5 * pan . left , 0.5 * pan . right , 0.5 * pan . left , 0.5 * pan . right ] ,
276329 } ,
277330 ] ,
278331 } ) ;
279332 } ) ;
280333
281334 it ( 'republishes active native schedule when track params change' , async ( ) => {
282335 const buffer = createMockAudioBuffer ( [ 1 , 1 ] ) ;
336+ const pan = equalPowerPan ( 0.5 ) ;
283337 invokeMock . mockResolvedValueOnce ( { slot : 0 , generation : 1 } ) ;
284338 backend . ensureTrack ( 'track-1' ) ;
285339 await Promise . resolve ( ) ;
@@ -307,7 +361,7 @@ describe('TauriBackend', () => {
307361 startSample : 0 ,
308362 lengthSamples : 2 ,
309363 gain : 1 ,
310- audioData : [ 0.125 , 0.25 , 0.125 , 0.25 ] ,
364+ audioData : [ 0.25 * pan . left , 0.25 * pan . right , 0.25 * pan . left , 0.25 * pan . right ] ,
311365 } ,
312366 ] ,
313367 } ) ;
@@ -374,6 +428,7 @@ describe('TauriBackend', () => {
374428
375429 it ( 'resamples non-48kHz buffers before sending native clips' , async ( ) => {
376430 const buffer = createMockAudioBuffer ( [ 0 , 1 ] , 24000 ) ;
431+ const centerPan = equalPowerPan ( 0 ) ;
377432
378433 backend . schedulePlayback ( [
379434 {
@@ -395,7 +450,16 @@ describe('TauriBackend', () => {
395450 startSample : 0 ,
396451 lengthSamples : 4 ,
397452 gain : 1 ,
398- audioData : [ 0 , 0 , 0.5 , 0.5 , 1 , 1 , 1 , 1 ] ,
453+ audioData : [
454+ 0 ,
455+ 0 ,
456+ 0.5 * centerPan . left ,
457+ 0.5 * centerPan . right ,
458+ centerPan . left ,
459+ centerPan . right ,
460+ centerPan . left ,
461+ centerPan . right ,
462+ ] ,
399463 } ,
400464 ] ,
401465 } ) ;
@@ -412,4 +476,28 @@ describe('TauriBackend', () => {
412476
413477 expect ( invokeMock ) . toHaveBeenCalledWith ( 'audio_get_track_meter' , expect . any ( Object ) ) ;
414478 } ) ;
479+
480+ it ( 'throttles track meter refreshes while a native request is in flight' , async ( ) => {
481+ invokeMock . mockResolvedValueOnce ( { slot : 0 , generation : 1 } ) ;
482+ backend . ensureTrack ( 'track-1' ) ;
483+ await Promise . resolve ( ) ;
484+ invokeMock . mockClear ( ) ;
485+
486+ invokeMock . mockResolvedValue ( { rms : 0.2 , peak : 0.4 , clipped : false } ) ;
487+ backend . getTrackMeter ( 'track-1' ) ;
488+ backend . getTrackMeter ( 'track-1' ) ;
489+
490+ expect ( invokeMock ) . toHaveBeenCalledTimes ( 1 ) ;
491+ expect ( invokeMock ) . toHaveBeenCalledWith ( 'audio_get_track_meter' , expect . any ( Object ) ) ;
492+ } ) ;
493+
494+ it ( 'throttles master meter refreshes while a native request is in flight' , ( ) => {
495+ invokeMock . mockResolvedValue ( { rms : 0.2 , peak : 0.4 , clipped : false } ) ;
496+
497+ backend . getMasterMeter ( 'output' ) ;
498+ backend . getMasterMeter ( 'input' ) ;
499+
500+ expect ( invokeMock ) . toHaveBeenCalledTimes ( 1 ) ;
501+ expect ( invokeMock ) . toHaveBeenCalledWith ( 'audio_get_master_meter' ) ;
502+ } ) ;
415503} ) ;
0 commit comments