@@ -28,11 +28,26 @@ export class MidiSourceMetadataParser {
2828 if ( ! tracks . ok ) {
2929 return tracks ;
3030 }
31+ const events = this . readTrackEvents ( view , tracks . tracks ) ;
32+ if ( ! events . ok ) {
33+ return events ;
34+ }
35+ const durationSeconds = this . estimateDurationSeconds ( {
36+ maxTick : events . maxTick ,
37+ tempoEvents : events . tempoEvents ,
38+ ticksPerQuarterNote : division
39+ } ) ;
3140 return {
41+ durationSeconds,
3242 format,
43+ eventCounts : events . eventCounts ,
3344 ok : true ,
3445 ticksPerQuarterNote : division ,
3546 trackCount,
47+ tempoEvents : events . tempoEvents ,
48+ tempoSummary : this . formatTempoSummary ( events . tempoEvents ) ,
49+ timeSignatureEvents : events . timeSignatureEvents ,
50+ timeSignatureSummary : this . formatTimeSignatureSummary ( events . timeSignatureEvents ) ,
3651 tracks : tracks . tracks ,
3752 validationStatus : `Valid Standard MIDI File header with ${ tracks . tracks . length } declared track chunk${ tracks . tracks . length === 1 ? "" : "s" } .`
3853 } ;
@@ -64,6 +79,215 @@ export class MidiSourceMetadataParser {
6479 return { ok : true , tracks } ;
6580 }
6681
82+ readTrackEvents ( view , tracks ) {
83+ const eventCounts = {
84+ meta : 0 ,
85+ midi : 0 ,
86+ noteOff : 0 ,
87+ noteOn : 0 ,
88+ system : 0
89+ } ;
90+ const tempoEvents = [ ] ;
91+ const timeSignatureEvents = [ ] ;
92+ let maxTick = 0 ;
93+ for ( let trackIndex = 0 ; trackIndex < tracks . length ; trackIndex += 1 ) {
94+ const track = tracks [ trackIndex ] ;
95+ let absoluteTick = 0 ;
96+ let cursor = track . offset ;
97+ let runningStatus = 0 ;
98+ const endOffset = track . offset + track . length ;
99+ while ( cursor < endOffset ) {
100+ const delta = this . readVariableLengthQuantity ( view , cursor , endOffset ) ;
101+ if ( ! delta . ok ) {
102+ return { ok : false , message : `MIDI track ${ trackIndex + 1 } has invalid delta time: ${ delta . message } ` } ;
103+ }
104+ cursor = delta . nextOffset ;
105+ absoluteTick += delta . value ;
106+ maxTick = Math . max ( maxTick , absoluteTick ) ;
107+ if ( cursor >= endOffset ) {
108+ return { ok : false , message : `MIDI track ${ trackIndex + 1 } ended after delta time without an event.` } ;
109+ }
110+ let status = view . getUint8 ( cursor ) ;
111+ if ( status < 0x80 ) {
112+ if ( ! runningStatus ) {
113+ return { ok : false , message : `MIDI track ${ trackIndex + 1 } uses running status before any channel status byte.` } ;
114+ }
115+ status = runningStatus ;
116+ } else {
117+ cursor += 1 ;
118+ runningStatus = status < 0xf0 ? status : 0 ;
119+ }
120+ if ( status === 0xff ) {
121+ const meta = this . readMetaEvent ( view , cursor , endOffset , absoluteTick , trackIndex ) ;
122+ if ( ! meta . ok ) {
123+ return meta ;
124+ }
125+ cursor = meta . nextOffset ;
126+ eventCounts . meta += 1 ;
127+ if ( meta . tempoEvent ) {
128+ tempoEvents . push ( meta . tempoEvent ) ;
129+ }
130+ if ( meta . timeSignatureEvent ) {
131+ timeSignatureEvents . push ( meta . timeSignatureEvent ) ;
132+ }
133+ continue ;
134+ }
135+ if ( status === 0xf0 || status === 0xf7 ) {
136+ const systemEvent = this . readSystemEvent ( view , cursor , endOffset , trackIndex ) ;
137+ if ( ! systemEvent . ok ) {
138+ return systemEvent ;
139+ }
140+ cursor = systemEvent . nextOffset ;
141+ eventCounts . system += 1 ;
142+ continue ;
143+ }
144+ const channelEvent = this . readChannelEvent ( view , cursor , endOffset , status , trackIndex ) ;
145+ if ( ! channelEvent . ok ) {
146+ return channelEvent ;
147+ }
148+ cursor = channelEvent . nextOffset ;
149+ eventCounts . midi += 1 ;
150+ if ( channelEvent . noteOn ) {
151+ eventCounts . noteOn += 1 ;
152+ }
153+ if ( channelEvent . noteOff ) {
154+ eventCounts . noteOff += 1 ;
155+ }
156+ }
157+ }
158+ tempoEvents . sort ( ( left , right ) => left . tick - right . tick ) ;
159+ timeSignatureEvents . sort ( ( left , right ) => left . tick - right . tick ) ;
160+ return { eventCounts, maxTick, ok : true , tempoEvents, timeSignatureEvents } ;
161+ }
162+
163+ readMetaEvent ( view , offset , endOffset , tick , trackIndex ) {
164+ if ( offset >= endOffset ) {
165+ return { ok : false , message : `MIDI track ${ trackIndex + 1 } has a truncated meta event type.` } ;
166+ }
167+ const type = view . getUint8 ( offset ) ;
168+ const length = this . readVariableLengthQuantity ( view , offset + 1 , endOffset ) ;
169+ if ( ! length . ok ) {
170+ return { ok : false , message : `MIDI track ${ trackIndex + 1 } has invalid meta event length: ${ length . message } ` } ;
171+ }
172+ const dataOffset = length . nextOffset ;
173+ const nextOffset = dataOffset + length . value ;
174+ if ( nextOffset > endOffset ) {
175+ return { ok : false , message : `MIDI track ${ trackIndex + 1 } meta event length exceeds track bytes.` } ;
176+ }
177+ if ( type === 0x51 ) {
178+ if ( length . value !== 3 ) {
179+ return { ok : false , message : `MIDI track ${ trackIndex + 1 } tempo event length ${ length . value } is invalid; expected 3 bytes.` } ;
180+ }
181+ const microsecondsPerQuarterNote = ( view . getUint8 ( dataOffset ) << 16 ) | ( view . getUint8 ( dataOffset + 1 ) << 8 ) | view . getUint8 ( dataOffset + 2 ) ;
182+ return {
183+ nextOffset,
184+ ok : true ,
185+ tempoEvent : {
186+ bpm : this . roundBpm ( 60000000 / microsecondsPerQuarterNote ) ,
187+ microsecondsPerQuarterNote,
188+ tick
189+ }
190+ } ;
191+ }
192+ if ( type === 0x58 ) {
193+ if ( length . value < 4 ) {
194+ return { ok : false , message : `MIDI track ${ trackIndex + 1 } time signature event is truncated.` } ;
195+ }
196+ const numerator = view . getUint8 ( dataOffset ) ;
197+ const denominator = 2 ** view . getUint8 ( dataOffset + 1 ) ;
198+ return {
199+ nextOffset,
200+ ok : true ,
201+ timeSignatureEvent : {
202+ denominator,
203+ numerator,
204+ tick
205+ }
206+ } ;
207+ }
208+ return { nextOffset, ok : true } ;
209+ }
210+
211+ readSystemEvent ( view , offset , endOffset , trackIndex ) {
212+ const length = this . readVariableLengthQuantity ( view , offset , endOffset ) ;
213+ if ( ! length . ok ) {
214+ return { ok : false , message : `MIDI track ${ trackIndex + 1 } has invalid system event length: ${ length . message } ` } ;
215+ }
216+ const nextOffset = length . nextOffset + length . value ;
217+ if ( nextOffset > endOffset ) {
218+ return { ok : false , message : `MIDI track ${ trackIndex + 1 } system event length exceeds track bytes.` } ;
219+ }
220+ return { nextOffset, ok : true } ;
221+ }
222+
223+ readChannelEvent ( view , offset , endOffset , status , trackIndex ) {
224+ const command = status & 0xf0 ;
225+ const dataLength = command === 0xc0 || command === 0xd0 ? 1 : 2 ;
226+ if ( status < 0x80 || status > 0xef ) {
227+ return { ok : false , message : `MIDI track ${ trackIndex + 1 } has unsupported event status 0x${ status . toString ( 16 ) } .` } ;
228+ }
229+ if ( offset + dataLength > endOffset ) {
230+ return { ok : false , message : `MIDI track ${ trackIndex + 1 } channel event is truncated.` } ;
231+ }
232+ const data2 = dataLength === 2 ? view . getUint8 ( offset + 1 ) : 0 ;
233+ return {
234+ nextOffset : offset + dataLength ,
235+ noteOff : command === 0x80 || ( command === 0x90 && data2 === 0 ) ,
236+ noteOn : command === 0x90 && data2 > 0 ,
237+ ok : true
238+ } ;
239+ }
240+
241+ readVariableLengthQuantity ( view , offset , endOffset ) {
242+ let value = 0 ;
243+ for ( let count = 0 ; count < 4 ; count += 1 ) {
244+ if ( offset + count >= endOffset ) {
245+ return { ok : false , message : "variable-length quantity is truncated." } ;
246+ }
247+ const byte = view . getUint8 ( offset + count ) ;
248+ value = ( value << 7 ) | ( byte & 0x7f ) ;
249+ if ( ( byte & 0x80 ) === 0 ) {
250+ return { nextOffset : offset + count + 1 , ok : true , value } ;
251+ }
252+ }
253+ return { ok : false , message : "variable-length quantity exceeds 4 bytes." } ;
254+ }
255+
256+ estimateDurationSeconds ( { maxTick, tempoEvents, ticksPerQuarterNote } ) {
257+ let seconds = 0 ;
258+ let tick = 0 ;
259+ let microsecondsPerQuarterNote = 500000 ;
260+ tempoEvents . forEach ( ( event ) => {
261+ if ( event . tick > tick ) {
262+ seconds += ( ( event . tick - tick ) / ticksPerQuarterNote ) * ( microsecondsPerQuarterNote / 1000000 ) ;
263+ tick = event . tick ;
264+ }
265+ microsecondsPerQuarterNote = event . microsecondsPerQuarterNote ;
266+ } ) ;
267+ if ( maxTick > tick ) {
268+ seconds += ( ( maxTick - tick ) / ticksPerQuarterNote ) * ( microsecondsPerQuarterNote / 1000000 ) ;
269+ }
270+ return Number ( seconds . toFixed ( 3 ) ) ;
271+ }
272+
273+ formatTempoSummary ( tempoEvents ) {
274+ if ( ! tempoEvents . length ) {
275+ return "Default 120 BPM" ;
276+ }
277+ return tempoEvents . map ( ( event ) => `${ event . bpm } BPM at tick ${ event . tick } ` ) . join ( "; " ) ;
278+ }
279+
280+ formatTimeSignatureSummary ( timeSignatureEvents ) {
281+ if ( ! timeSignatureEvents . length ) {
282+ return "not declared" ;
283+ }
284+ return timeSignatureEvents . map ( ( event ) => `${ event . numerator } /${ event . denominator } at tick ${ event . tick } ` ) . join ( "; " ) ;
285+ }
286+
287+ roundBpm ( value ) {
288+ return Number ( value . toFixed ( 2 ) ) . toString ( ) ;
289+ }
290+
67291 readAscii ( view , offset , length ) {
68292 if ( offset + length > view . byteLength ) {
69293 return "" ;
0 commit comments