@@ -37,19 +37,37 @@ export class MidiSourceMetadataParser {
3737 tempoEvents : events . tempoEvents ,
3838 ticksPerQuarterNote : division
3939 } ) ;
40+ const measureTiming = this . createMeasureTiming ( {
41+ maxTick : events . maxTick ,
42+ ticksPerQuarterNote : division ,
43+ timeSignatureEvents : events . timeSignatureEvents
44+ } ) ;
4045 return {
46+ channelSummary : this . formatChannelSummary ( events . channels ) ,
47+ channels : events . channels ,
4148 durationSeconds,
4249 format,
4350 eventCounts : events . eventCounts ,
51+ instrumentSummary : this . formatInstrumentSummary ( events . programChanges ) ,
52+ loopSafeDurationSeconds : durationSeconds ,
53+ measureTiming,
54+ measureTimingSummary : this . formatMeasureTimingSummary ( measureTiming ) ,
55+ normalizedNotes : events . normalizedNotes ,
56+ normalizedNoteCount : events . normalizedNotes . length ,
4457 ok : true ,
58+ programChanges : events . programChanges ,
4559 ticksPerQuarterNote : division ,
4660 trackCount,
4761 tempoEvents : events . tempoEvents ,
4862 tempoSummary : this . formatTempoSummary ( events . tempoEvents ) ,
4963 timeSignatureEvents : events . timeSignatureEvents ,
5064 timeSignatureSummary : this . formatTimeSignatureSummary ( events . timeSignatureEvents ) ,
65+ trackActivity : events . trackActivity ,
66+ trackActivitySummary : this . formatTrackActivitySummary ( events . trackActivity ) ,
5167 tracks : tracks . tracks ,
52- validationStatus : `Valid Standard MIDI File header with ${ tracks . tracks . length } declared track chunk${ tracks . tracks . length === 1 ? "" : "s" } .`
68+ validationStatus : `Valid Standard MIDI File header with ${ tracks . tracks . length } declared track chunk${ tracks . tracks . length === 1 ? "" : "s" } .` ,
69+ warningSummary : this . formatWarnings ( events . warnings ) ,
70+ warnings : events . warnings
5371 } ;
5472 }
5573
@@ -87,11 +105,27 @@ export class MidiSourceMetadataParser {
87105 noteOn : 0 ,
88106 system : 0
89107 } ;
108+ const channelMap = new Map ( ) ;
109+ const openNotes = new Map ( ) ;
110+ const normalizedNotes = [ ] ;
111+ const programChanges = [ ] ;
90112 const tempoEvents = [ ] ;
113+ const trackActivity = tracks . map ( ( track , index ) => ( {
114+ eventCount : 0 ,
115+ index : index + 1 ,
116+ length : track . length ,
117+ metaEvents : 0 ,
118+ noteEvents : 0 ,
119+ notes : 0 ,
120+ programChanges : 0 ,
121+ systemEvents : 0
122+ } ) ) ;
91123 const timeSignatureEvents = [ ] ;
124+ const warnings = [ ] ;
92125 let maxTick = 0 ;
93126 for ( let trackIndex = 0 ; trackIndex < tracks . length ; trackIndex += 1 ) {
94127 const track = tracks [ trackIndex ] ;
128+ const activity = trackActivity [ trackIndex ] ;
95129 let absoluteTick = 0 ;
96130 let cursor = track . offset ;
97131 let runningStatus = 0 ;
@@ -124,6 +158,8 @@ export class MidiSourceMetadataParser {
124158 }
125159 cursor = meta . nextOffset ;
126160 eventCounts . meta += 1 ;
161+ activity . eventCount += 1 ;
162+ activity . metaEvents += 1 ;
127163 if ( meta . tempoEvent ) {
128164 tempoEvents . push ( meta . tempoEvent ) ;
129165 }
@@ -139,6 +175,9 @@ export class MidiSourceMetadataParser {
139175 }
140176 cursor = systemEvent . nextOffset ;
141177 eventCounts . system += 1 ;
178+ activity . eventCount += 1 ;
179+ activity . systemEvents += 1 ;
180+ warnings . push ( `Track ${ trackIndex + 1 } contains unsupported system/SysEx event 0x${ status . toString ( 16 ) } at tick ${ absoluteTick } ; event was skipped for normalized timeline output.` ) ;
142181 continue ;
143182 }
144183 const channelEvent = this . readChannelEvent ( view , cursor , endOffset , status , trackIndex ) ;
@@ -147,17 +186,62 @@ export class MidiSourceMetadataParser {
147186 }
148187 cursor = channelEvent . nextOffset ;
149188 eventCounts . midi += 1 ;
189+ activity . eventCount += 1 ;
190+ this . updateChannelSummary ( channelMap , channelEvent ) ;
191+ if ( channelEvent . programChange ) {
192+ activity . programChanges += 1 ;
193+ programChanges . push ( {
194+ channel : channelEvent . channel ,
195+ program : channelEvent . data1 ,
196+ tick : absoluteTick ,
197+ track : trackIndex + 1
198+ } ) ;
199+ }
150200 if ( channelEvent . noteOn ) {
151201 eventCounts . noteOn += 1 ;
202+ activity . noteEvents += 1 ;
203+ this . openNote ( openNotes , channelEvent , absoluteTick , trackIndex ) ;
152204 }
153205 if ( channelEvent . noteOff ) {
154206 eventCounts . noteOff += 1 ;
207+ activity . noteEvents += 1 ;
208+ const note = this . closeNote ( openNotes , channelEvent , absoluteTick , trackIndex ) ;
209+ if ( note ) {
210+ activity . notes += 1 ;
211+ normalizedNotes . push ( note ) ;
212+ } else {
213+ warnings . push ( `Track ${ trackIndex + 1 } has note-off without matching note-on for channel ${ channelEvent . channel } , note ${ channelEvent . noteNumber } at tick ${ absoluteTick } .` ) ;
214+ }
155215 }
156216 }
157217 }
218+ openNotes . forEach ( ( entries ) => {
219+ entries . forEach ( ( entry ) => {
220+ warnings . push ( `Track ${ entry . track } has note-on without matching note-off for channel ${ entry . channel } , note ${ entry . noteNumber } at tick ${ entry . startTick } .` ) ;
221+ } ) ;
222+ } ) ;
223+ trackActivity . forEach ( ( activity ) => {
224+ if ( ! activity . noteEvents && ! activity . programChanges && ! activity . systemEvents && activity . metaEvents <= 1 ) {
225+ warnings . push ( `Track ${ activity . index } has no note, program, tempo, or time signature activity.` ) ;
226+ }
227+ } ) ;
228+ const channels = Array . from ( channelMap . values ( ) ) . sort ( ( left , right ) => left . channel - right . channel ) ;
229+ normalizedNotes . sort ( ( left , right ) => left . startTick - right . startTick || left . channel - right . channel || left . noteNumber - right . noteNumber ) ;
230+ programChanges . sort ( ( left , right ) => left . tick - right . tick || left . channel - right . channel ) ;
158231 tempoEvents . sort ( ( left , right ) => left . tick - right . tick ) ;
159232 timeSignatureEvents . sort ( ( left , right ) => left . tick - right . tick ) ;
160- return { eventCounts, maxTick, ok : true , tempoEvents, timeSignatureEvents } ;
233+ return {
234+ channels,
235+ eventCounts,
236+ maxTick,
237+ normalizedNotes,
238+ ok : true ,
239+ programChanges,
240+ tempoEvents,
241+ timeSignatureEvents,
242+ trackActivity,
243+ warnings
244+ } ;
161245 }
162246
163247 readMetaEvent ( view , offset , endOffset , tick , trackIndex ) {
@@ -222,19 +306,83 @@ export class MidiSourceMetadataParser {
222306
223307 readChannelEvent ( view , offset , endOffset , status , trackIndex ) {
224308 const command = status & 0xf0 ;
309+ const channel = ( status & 0x0f ) + 1 ;
225310 const dataLength = command === 0xc0 || command === 0xd0 ? 1 : 2 ;
226311 if ( status < 0x80 || status > 0xef ) {
227312 return { ok : false , message : `MIDI track ${ trackIndex + 1 } has unsupported event status 0x${ status . toString ( 16 ) } .` } ;
228313 }
229314 if ( offset + dataLength > endOffset ) {
230315 return { ok : false , message : `MIDI track ${ trackIndex + 1 } channel event is truncated.` } ;
231316 }
317+ const data1 = view . getUint8 ( offset ) ;
232318 const data2 = dataLength === 2 ? view . getUint8 ( offset + 1 ) : 0 ;
233319 return {
320+ channel,
321+ command,
322+ data1,
323+ data2,
234324 nextOffset : offset + dataLength ,
325+ noteNumber : data1 ,
235326 noteOff : command === 0x80 || ( command === 0x90 && data2 === 0 ) ,
236327 noteOn : command === 0x90 && data2 > 0 ,
237- ok : true
328+ ok : true ,
329+ programChange : command === 0xc0
330+ } ;
331+ }
332+
333+ updateChannelSummary ( channelMap , event ) {
334+ if ( ! channelMap . has ( event . channel ) ) {
335+ channelMap . set ( event . channel , {
336+ channel : event . channel ,
337+ eventCount : 0 ,
338+ noteCount : 0 ,
339+ programs : [ ]
340+ } ) ;
341+ }
342+ const channel = channelMap . get ( event . channel ) ;
343+ channel . eventCount += 1 ;
344+ if ( event . noteOn ) {
345+ channel . noteCount += 1 ;
346+ }
347+ if ( event . programChange && ! channel . programs . includes ( event . data1 ) ) {
348+ channel . programs . push ( event . data1 ) ;
349+ channel . programs . sort ( ( left , right ) => left - right ) ;
350+ }
351+ }
352+
353+ openNote ( openNotes , event , absoluteTick , trackIndex ) {
354+ const key = `${ event . channel } :${ event . noteNumber } ` ;
355+ const entries = openNotes . get ( key ) || [ ] ;
356+ entries . push ( {
357+ channel : event . channel ,
358+ noteNumber : event . noteNumber ,
359+ startTick : absoluteTick ,
360+ track : trackIndex + 1 ,
361+ velocity : event . data2
362+ } ) ;
363+ openNotes . set ( key , entries ) ;
364+ }
365+
366+ closeNote ( openNotes , event , absoluteTick , trackIndex ) {
367+ const key = `${ event . channel } :${ event . noteNumber } ` ;
368+ const entries = openNotes . get ( key ) || [ ] ;
369+ const started = entries . shift ( ) ;
370+ if ( ! entries . length ) {
371+ openNotes . delete ( key ) ;
372+ } else {
373+ openNotes . set ( key , entries ) ;
374+ }
375+ if ( ! started ) {
376+ return null ;
377+ }
378+ return {
379+ channel : event . channel ,
380+ durationTicks : Math . max ( 0 , absoluteTick - started . startTick ) ,
381+ endTick : absoluteTick ,
382+ noteNumber : event . noteNumber ,
383+ startTick : started . startTick ,
384+ track : trackIndex + 1 ,
385+ velocity : started . velocity
238386 } ;
239387 }
240388
@@ -270,6 +418,17 @@ export class MidiSourceMetadataParser {
270418 return Number ( seconds . toFixed ( 3 ) ) ;
271419 }
272420
421+ createMeasureTiming ( { maxTick, ticksPerQuarterNote, timeSignatureEvents } ) {
422+ const signature = timeSignatureEvents [ 0 ] || { denominator : 4 , numerator : 4 , tick : 0 } ;
423+ const ticksPerBar = ticksPerQuarterNote * signature . numerator * ( 4 / signature . denominator ) ;
424+ return {
425+ estimatedBars : ticksPerBar ? Number ( ( maxTick / ticksPerBar ) . toFixed ( 2 ) ) : 0 ,
426+ maxTick,
427+ ticksPerBar,
428+ timeSignature : `${ signature . numerator } /${ signature . denominator } `
429+ } ;
430+ }
431+
273432 formatTempoSummary ( tempoEvents ) {
274433 if ( ! tempoEvents . length ) {
275434 return "Default 120 BPM" ;
@@ -284,6 +443,35 @@ export class MidiSourceMetadataParser {
284443 return timeSignatureEvents . map ( ( event ) => `${ event . numerator } /${ event . denominator } at tick ${ event . tick } ` ) . join ( "; " ) ;
285444 }
286445
446+ formatChannelSummary ( channels ) {
447+ if ( ! channels . length ) {
448+ return "No channel events" ;
449+ }
450+ return channels . map ( ( channel ) => `Ch ${ channel . channel } : ${ channel . noteCount } notes, ${ channel . eventCount } events` ) . join ( "; " ) ;
451+ }
452+
453+ formatInstrumentSummary ( programChanges ) {
454+ if ( ! programChanges . length ) {
455+ return "No program changes" ;
456+ }
457+ return programChanges . map ( ( event ) => `Ch ${ event . channel } program ${ event . program } at tick ${ event . tick } ` ) . join ( "; " ) ;
458+ }
459+
460+ formatMeasureTimingSummary ( measureTiming ) {
461+ return `${ measureTiming . estimatedBars } bars at ${ measureTiming . timeSignature } ` ;
462+ }
463+
464+ formatTrackActivitySummary ( trackActivity ) {
465+ if ( ! trackActivity . length ) {
466+ return "No tracks" ;
467+ }
468+ return trackActivity . map ( ( track ) => `Track ${ track . index } : ${ track . notes } notes, ${ track . eventCount } events` ) . join ( "; " ) ;
469+ }
470+
471+ formatWarnings ( warnings ) {
472+ return warnings . length ? warnings . join ( "; " ) : "none" ;
473+ }
474+
287475 roundBpm ( value ) {
288476 return Number ( value . toFixed ( 2 ) ) . toString ( ) ;
289477 }
0 commit comments