@@ -7,7 +7,7 @@ import { MidiTickLookup } from '@src/midi/MidiTickLookup';
77
88import { MidiUtils } from '@src/midi/MidiUtils' ;
99import { AccentuationType } from '@src/model/AccentuationType' ;
10- import { type Automation , AutomationType , SyncPointData } from '@src/model/Automation' ;
10+ import { type Automation , AutomationType } from '@src/model/Automation' ;
1111import type { Bar } from '@src/model/Bar' ;
1212import type { Beat } from '@src/model/Beat' ;
1313import { BendPoint } from '@src/model/BendPoint' ;
@@ -58,6 +58,14 @@ class RasgueadoInfo {
5858 public brushInfos : Int32Array [ ] = [ ] ;
5959}
6060
61+ class PlayThroughContext {
62+ public synthTick : number = 0 ;
63+ public synthTime : number = 0 ;
64+ public currentTempo : number = 0 ;
65+ public automationToSyncPoint : Map < Automation , BackingTrackSyncPoint > = new Map < Automation , BackingTrackSyncPoint > ( ) ;
66+ public syncPoints ! : BackingTrackSyncPoint [ ] ;
67+ }
68+
6169/**
6270 * This generator creates a midi file using a score.
6371 */
@@ -233,6 +241,29 @@ export class MidiFileGenerator {
233241 return syncPoints ;
234242 }
235243
244+ /**
245+ * @internal
246+ */
247+ public static buildModifiedTempoLookup ( score : Score ) : Map < Automation , BackingTrackSyncPoint > {
248+ const syncPoints : BackingTrackSyncPoint [ ] = [ ] ;
249+
250+ const context = MidiFileGenerator . playThroughSong (
251+ score ,
252+ syncPoints ,
253+ ( _masterBar , _previousMasterBar , _currentTick , _currentTempo , _barOccurence ) => {
254+ // no generation
255+ } ,
256+ ( _barIndex , _currentTick , _currentTempo ) => {
257+ // no generation
258+ } ,
259+ _endTick => {
260+ // no generation
261+ }
262+ ) ;
263+
264+ return context . automationToSyncPoint ;
265+ }
266+
236267 private static playThroughSong (
237268 score : Score ,
238269 syncPoints : BackingTrackSyncPoint [ ] ,
@@ -248,7 +279,9 @@ export class MidiFileGenerator {
248279 ) {
249280 const controller : MidiPlaybackController = new MidiPlaybackController ( score ) ;
250281
251- let currentTempo = score . tempo ;
282+ const playContext = new PlayThroughContext ( ) ;
283+ playContext . currentTempo = score . tempo ;
284+ playContext . syncPoints = syncPoints ;
252285 let previousMasterBar : MasterBar | null = null ;
253286
254287 // store the previous played bar for repeats
@@ -264,27 +297,14 @@ export class MidiFileGenerator {
264297 occurence ++ ;
265298 barOccurence . set ( index , occurence ) ;
266299
267- generateMasterBar ( bar , previousMasterBar , currentTick , currentTempo , occurence ) ;
268-
269- const barSyncPoints = bar . syncPoints ;
270- if ( barSyncPoints ) {
271- for ( const syncPoint of barSyncPoints ) {
272- if ( syncPoint . syncPointValue ! . barOccurence === occurence ) {
273- const tick = currentTick + bar . calculateDuration ( ) * syncPoint . ratioPosition ;
274- syncPoints . push ( new BackingTrackSyncPoint ( tick , syncPoint . syncPointValue ! ) ) ;
275- }
276- }
277- }
278-
279- if ( bar . tempoAutomations . length > 0 ) {
280- currentTempo = bar . tempoAutomations [ 0 ] . value ;
281- }
300+ generateMasterBar ( bar , previousMasterBar , currentTick , playContext . currentTempo , occurence ) ;
282301
283- generateTracks ( index , currentTick , currentTempo ) ;
302+ const trackTempo =
303+ bar . tempoAutomations . length > 0 ? bar . tempoAutomations [ 0 ] . value : playContext . currentTempo ;
304+ generateTracks ( index , currentTick , trackTempo ) ;
284305
285- if ( bar . tempoAutomations . length > 0 ) {
286- currentTempo = bar . tempoAutomations [ bar . tempoAutomations . length - 1 ] . value ;
287- }
306+ playContext . synthTick = currentTick ;
307+ MidiFileGenerator . processBarTime ( bar , occurence , playContext ) ;
288308 }
289309
290310 controller . moveNext ( ) ;
@@ -297,24 +317,146 @@ export class MidiFileGenerator {
297317 // but where it ends according to the BPM and the remaining ticks.
298318 if ( syncPoints . length > 0 ) {
299319 const lastSyncPoint = syncPoints [ syncPoints . length - 1 ] ;
300- const remainingTicks = controller . currentTick - lastSyncPoint . tick ;
320+ const remainingTicks = controller . currentTick - lastSyncPoint . synthTick ;
301321 if ( remainingTicks > 0 ) {
302- const syncPointData = new SyncPointData ( ) ;
303-
304- // last occurence of the last bar
305- syncPointData . barOccurence = barOccurence . get ( score . masterBars . length - 1 ) ! ;
306- // same tempo as last point
307- syncPointData . modifiedTempo = lastSyncPoint . data . modifiedTempo ;
308- // interpolated end from last syncPoint
309- syncPointData . millisecondOffset =
310- lastSyncPoint . data . millisecondOffset +
311- MidiUtils . ticksToMillis ( remainingTicks , syncPointData . modifiedTempo ) ;
312-
313- syncPoints . push ( new BackingTrackSyncPoint ( controller . currentTick , syncPointData ) ) ;
322+ const backingTrackSyncPoint = new BackingTrackSyncPoint ( ) ;
323+ backingTrackSyncPoint . masterBarIndex = previousMasterBar ! . index ;
324+ backingTrackSyncPoint . masterBarOccurence = barOccurence . get ( previousMasterBar ! . index ) ! - 1 ;
325+ backingTrackSyncPoint . synthTick = controller . currentTick ;
326+ backingTrackSyncPoint . synthBpm = playContext . currentTempo ;
327+
328+ // we need to assume some BPM for the last interpolated point.
329+ // if we have more than just a start point, we keep the BPM before the last manual sync point
330+ // otherwise we have no customized sync BPM known and keep the synthesizer one.
331+ backingTrackSyncPoint . syncBpm =
332+ syncPoints . length > 1 ? syncPoints [ syncPoints . length - 2 ] . syncBpm : lastSyncPoint . synthBpm ;
333+
334+ backingTrackSyncPoint . synthTime =
335+ lastSyncPoint . synthTime + MidiUtils . ticksToMillis ( remainingTicks , lastSyncPoint . synthBpm ) ;
336+ backingTrackSyncPoint . syncTime =
337+ lastSyncPoint . syncTime + MidiUtils . ticksToMillis ( remainingTicks , backingTrackSyncPoint . syncBpm ) ;
338+
339+ // update the previous sync point according to the new time
340+ lastSyncPoint . updateSyncBpm ( backingTrackSyncPoint . synthTime , backingTrackSyncPoint . syncTime ) ;
341+
342+ syncPoints . push ( backingTrackSyncPoint ) ;
314343 }
315344 }
316345
317346 finish ( controller . currentTick ) ;
347+
348+ return playContext ;
349+ }
350+
351+ private static processBarTime ( bar : MasterBar , occurence : number , context : PlayThroughContext ) {
352+ const duration = bar . calculateDuration ( ) ;
353+ const barSyncPoints = bar . syncPoints ;
354+ const barStartTick = context . synthTick ;
355+ if ( barSyncPoints ) {
356+ MidiFileGenerator . processBarTimeWithSyncPoints ( bar , occurence , context ) ;
357+ } else {
358+ MidiFileGenerator . processBarTimeNoSyncPoints ( bar , context ) ;
359+ }
360+
361+ // don't forget the part after the last tempo change
362+ const endTick = barStartTick + duration ;
363+ const tickOffset = endTick - context . synthTick ;
364+ if ( tickOffset > 0 ) {
365+ context . synthTime += MidiUtils . ticksToMillis ( tickOffset , context . currentTempo ) ;
366+ context . synthTick = endTick ;
367+ }
368+ }
369+
370+ private static processBarTimeWithSyncPoints ( bar : MasterBar , occurence : number , context : PlayThroughContext ) {
371+ const barStartTick = context . synthTick ;
372+ const duration = bar . calculateDuration ( ) ;
373+
374+ let tempoChangeIndex = 0 ;
375+ let tickOffset : number ;
376+
377+ for ( const syncPoint of bar . syncPoints ! ) {
378+ if ( syncPoint . syncPointValue ! . barOccurence !== occurence ) {
379+ continue ;
380+ }
381+
382+ const syncPointTick = barStartTick + syncPoint . ratioPosition * duration ;
383+
384+ // first process all tempo changes until this sync point
385+ while (
386+ tempoChangeIndex < bar . tempoAutomations . length &&
387+ bar . tempoAutomations [ tempoChangeIndex ] . ratioPosition <= syncPoint . ratioPosition
388+ ) {
389+ const tempoChange = bar . tempoAutomations [ tempoChangeIndex ] ;
390+ const absoluteTick = barStartTick + tempoChange . ratioPosition * duration ;
391+ tickOffset = absoluteTick - context . synthTick ;
392+
393+ if ( tickOffset > 0 ) {
394+ context . synthTick = absoluteTick ;
395+ context . synthTime += MidiUtils . ticksToMillis ( tickOffset , context . currentTempo ) ;
396+ }
397+
398+ context . currentTempo = tempoChange . value ;
399+ tempoChangeIndex ++ ;
400+ }
401+
402+ // process time until sync point
403+ tickOffset = syncPointTick - context . synthTick ;
404+ if ( tickOffset > 0 ) {
405+ context . synthTick = syncPointTick ;
406+ context . synthTime += MidiUtils . ticksToMillis ( tickOffset , context . currentTempo ) ;
407+ }
408+
409+ // update the previous sync point according to the new time
410+ if ( context . syncPoints . length > 0 ) {
411+ context . syncPoints [ context . syncPoints . length - 1 ] . updateSyncBpm (
412+ context . synthTime ,
413+ syncPoint . syncPointValue ! . millisecondOffset
414+ ) ;
415+ }
416+
417+ // create the new sync point
418+ const backingTrackSyncPoint = new BackingTrackSyncPoint ( ) ;
419+ backingTrackSyncPoint . masterBarIndex = bar . index ;
420+ backingTrackSyncPoint . masterBarOccurence = occurence ;
421+ backingTrackSyncPoint . synthTick = syncPointTick ;
422+ backingTrackSyncPoint . synthBpm = context . currentTempo ;
423+ backingTrackSyncPoint . synthTime = context . synthTime ;
424+ backingTrackSyncPoint . syncTime = syncPoint . syncPointValue ! . millisecondOffset ;
425+ backingTrackSyncPoint . syncBpm = 0 /* calculated by next sync point */ ;
426+
427+ context . syncPoints . push ( backingTrackSyncPoint ) ;
428+ context . automationToSyncPoint . set ( syncPoint , backingTrackSyncPoint ) ;
429+ }
430+
431+ // process remaining tempo changes after all sync points
432+ while ( tempoChangeIndex < bar . tempoAutomations . length ) {
433+ const tempoChange = bar . tempoAutomations [ tempoChangeIndex ] ;
434+ const absoluteTick = barStartTick + tempoChange . ratioPosition * duration ;
435+ tickOffset = absoluteTick - context . synthTick ;
436+ if ( tickOffset > 0 ) {
437+ context . synthTick = absoluteTick ;
438+ context . synthTime += MidiUtils . ticksToMillis ( tickOffset , context . currentTempo ) ;
439+ }
440+
441+ context . currentTempo = tempoChange . value ;
442+ tempoChangeIndex ++ ;
443+ }
444+ }
445+
446+ private static processBarTimeNoSyncPoints ( bar : MasterBar , context : PlayThroughContext ) {
447+ // walk through the tempo changes
448+ const barStartTick = context . synthTick ;
449+ const duration = bar . calculateDuration ( ) ;
450+ for ( const changes of bar . tempoAutomations ) {
451+ const absoluteTick = barStartTick + changes . ratioPosition * duration ;
452+ const tickOffset = absoluteTick - context . synthTick ;
453+ if ( tickOffset > 0 ) {
454+ context . synthTick = absoluteTick ;
455+ context . synthTime += MidiUtils . ticksToMillis ( tickOffset , context . currentTempo ) ;
456+ }
457+
458+ context . currentTempo = changes . value ;
459+ }
318460 }
319461
320462 private static toChannelShort ( data : number ) : number {
0 commit comments