Initial commit, basic parsing of MIDI files, doesn't parse event data yet
authorNick Downing <nick@ndcode.org>
Sun, 15 Sep 2024 00:26:35 +0000 (10:26 +1000)
committerNick Downing <nick@ndcode.org>
Sun, 15 Sep 2024 02:00:45 +0000 (12:00 +1000)
15 files changed:
doc/Standard MIDI file format, updated.pdf [new file with mode: 0644]
midi.py [new file with mode: 0755]
test/16739.mid [new file with mode: 0644]
test/28362.mid [new file with mode: 0644]
test/Bagatella Fur Elise.mid [new file with mode: 0644]
test/Fur-Elise-1.mid [new file with mode: 0644]
test/Fur-Elise-2.mid [new file with mode: 0644]
test/Fur-Elise-3.mid [new file with mode: 0644]
test/Fur_Elise.mid [new file with mode: 0644]
test/Fur_Elise.mid.mid [new file with mode: 0644]
test/for_elise_by_beethoven.mid [new file with mode: 0644]
test/fur-elise.mid [new file with mode: 0644]
test/fur_Elise_WoO59-a4.pdf [new file with mode: 0644]
test/fur_Elise_WoO59.ly [new file with mode: 0644]
test/fur_Elise_WoO59.mid [new file with mode: 0644]

diff --git a/doc/Standard MIDI file format, updated.pdf b/doc/Standard MIDI file format, updated.pdf
new file mode 100644 (file)
index 0000000..4aa0651
Binary files /dev/null and b/doc/Standard MIDI file format, updated.pdf differ
diff --git a/midi.py b/midi.py
new file mode 100755 (executable)
index 0000000..c9ff0f0
--- /dev/null
+++ b/midi.py
@@ -0,0 +1,159 @@
+#!/usr/bin/env python3
+
+import struct
+import sys
+
+CHANNEL_MESSAGE = [
+  ('Note Off', 2),
+  ('Note On', 2),
+  ('Polyphonic Key Pressure', 2),
+  ('Control Change', 2),
+  ('Program Change', 1),
+  ('Channel Pressure', 1),
+  ('Pitch Wheel Change', 2),
+]
+
+COMMON_MESSAGE = [
+  ('System Exclusive', -1),
+  ('Undefined', 0),
+  ('Song Position Pointer', 2),
+  ('Song Select', 1),
+  ('Undefined', 0),
+  ('Undefined', 0),
+  ('Tune Request', 0),
+  ('End of Exclusive', -1),
+  ('Timing Clock', 0),
+  ('Undefined', 0),
+  ('Start', 0),
+  ('Continue', 0),
+  ('Stop', 0),
+  ('Undefined', 0),
+  ('Active Sensing', 0),
+  ('Meta Event', -1),
+]
+
+META_EVENT = {
+  0x00: 'Sequence Number',
+  0x01: 'Text Event',
+  0x02: 'Copyright Notice',
+  0x03: 'Sequence/Track Name',
+  0x04: 'Instrument Name',
+  0x05: 'Lyric',
+  0x06: 'Marker',
+  0x07: 'Cue Point',
+  0x20: 'MIDI Channel Prefix',
+  0x2f: 'End of Track',
+  0x51: 'Set Tempo',
+  0x54: 'SMPTE Offset',
+  0x58: 'Time Signature',
+  0x59: 'Key Signature',
+  0x7f: 'Sequencer Specific Meta-Event',
+}
+
+with open(
+  sys.argv[1] if len(sys.argv) >= 2 else 'test/fur_Elise_WoO59.mid',
+  'rb'
+) as fin:
+  while True:
+    header = fin.read(8)
+    assert len(header) == 0 or len(header) == 8
+    if len(header) == 0:
+      break
+      
+    chunk, length = struct.unpack('>II', header)
+    print('chunk', hex(chunk), 'length', hex(length))
+    track = fin.read(length)
+    if chunk == 0x4d546864: # MThd
+      assert len(track) >= 6
+      format, ntrks, division = struct.unpack('>HHH', track[:6])
+      print(
+        'format',
+        hex(format),
+        'ntrks',
+        hex(ntrks),
+        'division',
+        hex(division)
+      )
+      assert format == 0 or format == 1 # tracks must be simultaneous
+      assert (division & 0x8000) == 0 # must be ticks per quarter note
+    elif chunk == 0x4d54726b: # MTrk
+      i = 0
+      def variable():
+        global i
+        v = 0
+        while True:
+          b = track[i]
+          i += 1
+          v = (v << 7) | (b & 0x7f)
+          if (b & 0x80) == 0:
+            break
+        return v
+
+      s = 0
+      ticks = 0
+      while i < len(track):
+        ticks += variable()
+        if track[i] & 0x80:
+          s = track[i]
+          i += 1
+        else:
+          assert s & 0x80
+
+        id = (s >> 4) & 7 # channel message ID (if < 7)
+        channel = s & 0xf # or common message ID (if id == 7)
+        if id < 7:
+          name, l = CHANNEL_MESSAGE[id]
+          d = track[i:i + l]
+          i += l
+          data = '[{0:s}]'.format(', '.join([f'0x{b:02x}' for b in d]))
+          print(
+            'ticks',
+            hex(ticks),
+            'channel',
+            hex(channel),
+            'name',
+            f'"{name:s}"',
+            'data',
+            data
+          )
+        else:
+          name, l = COMMON_MESSAGE[channel]
+          if channel == 0xf:
+            t = track[i]
+            i += 1
+            l = track[i]
+            i += 1
+            d = track[i:i + l]
+            i += l
+            _type = META_EVENT.get(t, 'Unknown')
+            if t >= 1 and t < 0x10:
+              data = f'"{d.decode():s}"'
+            else:
+              data = '[{0:s}]'.format(', '.join([f'0x{b:02x}' for b in d]))
+            print(
+              'ticks',
+              hex(ticks),
+              'name',
+              f'"{name:s}"',
+              'type',
+              f'"{_type:s}"',
+              'data',
+              data
+            )
+          else:
+            if channel == 0 or channel == 7:
+              l = track[i]
+              i += 1
+            d = track[i:i + l]
+            i += l
+            data = '[{0:s}]'.format(', '.join([f'0x{b:02x}' for b in d]))
+            print(
+              'ticks',
+              hex(ticks),
+              'name',
+              f'"{name:s}"',
+              'data',
+              data
+            )
+    else:
+      print('unknown chunk', hex(chunk))
diff --git a/test/16739.mid b/test/16739.mid
new file mode 100644 (file)
index 0000000..a6101f8
Binary files /dev/null and b/test/16739.mid differ
diff --git a/test/28362.mid b/test/28362.mid
new file mode 100644 (file)
index 0000000..7a160c1
Binary files /dev/null and b/test/28362.mid differ
diff --git a/test/Bagatella Fur Elise.mid b/test/Bagatella Fur Elise.mid
new file mode 100644 (file)
index 0000000..56a8438
Binary files /dev/null and b/test/Bagatella Fur Elise.mid differ
diff --git a/test/Fur-Elise-1.mid b/test/Fur-Elise-1.mid
new file mode 100644 (file)
index 0000000..889ab66
Binary files /dev/null and b/test/Fur-Elise-1.mid differ
diff --git a/test/Fur-Elise-2.mid b/test/Fur-Elise-2.mid
new file mode 100644 (file)
index 0000000..f4cde33
Binary files /dev/null and b/test/Fur-Elise-2.mid differ
diff --git a/test/Fur-Elise-3.mid b/test/Fur-Elise-3.mid
new file mode 100644 (file)
index 0000000..5c14541
Binary files /dev/null and b/test/Fur-Elise-3.mid differ
diff --git a/test/Fur_Elise.mid b/test/Fur_Elise.mid
new file mode 100644 (file)
index 0000000..7756823
Binary files /dev/null and b/test/Fur_Elise.mid differ
diff --git a/test/Fur_Elise.mid.mid b/test/Fur_Elise.mid.mid
new file mode 100644 (file)
index 0000000..41c7248
Binary files /dev/null and b/test/Fur_Elise.mid.mid differ
diff --git a/test/for_elise_by_beethoven.mid b/test/for_elise_by_beethoven.mid
new file mode 100644 (file)
index 0000000..b50146c
Binary files /dev/null and b/test/for_elise_by_beethoven.mid differ
diff --git a/test/fur-elise.mid b/test/fur-elise.mid
new file mode 100644 (file)
index 0000000..0bb26f4
Binary files /dev/null and b/test/fur-elise.mid differ
diff --git a/test/fur_Elise_WoO59-a4.pdf b/test/fur_Elise_WoO59-a4.pdf
new file mode 100644 (file)
index 0000000..3e4d066
Binary files /dev/null and b/test/fur_Elise_WoO59-a4.pdf differ
diff --git a/test/fur_Elise_WoO59.ly b/test/fur_Elise_WoO59.ly
new file mode 100644 (file)
index 0000000..7b48b55
--- /dev/null
@@ -0,0 +1,203 @@
+\version "2.18.2"
+
+ \header {
+  title = "Für Elise"
+  subtitle = "Clavierstuck in A Minor - WoO 59"
+  composer = "Ludwig van Beethoven"
+  mutopiatitle = "Für Elise"
+  mutopiacomposer = "BeethovenLv"
+  mutopiaopus = "WoO 59"
+  mutopiainstrument = "Piano"
+  date = "1810"
+  source = "Breitkopf & Härtel, 1888"
+  style = "Classical"
+  license = "Public Domain"
+  maintainer = "Stelios Samelis"
+  moreInfo = "keywords: fur elise, bagatelle no.25"
+
+ footer = "Mutopia-2015/08/18-931"
+ copyright =  \markup { \override #'(baseline-skip . 0 ) \right-column { \sans \bold \with-url #"http://www.MutopiaProject.org" { \abs-fontsize #9  "Mutopia " \concat { \abs-fontsize #12 \with-color #white \char ##x01C0 \abs-fontsize #9 "Project " } } } \override #'(baseline-skip . 0 ) \center-column { \abs-fontsize #11.9 \with-color #grey \bold { \char ##x01C0 \char ##x01C0 } } \override #'(baseline-skip . 0 ) \column { \abs-fontsize #8 \sans \concat { " Typeset using " \with-url #"http://www.lilypond.org" "LilyPond" " by " \maintainer " " \char ##x2014 " " \footer } \concat { \concat { \abs-fontsize #8 \sans{ " Placed in the " \with-url #"http://creativecommons.org/licenses/publicdomain" "public domain" " by the typesetter " \char ##x2014 " free to distribute, modify, and perform" } } \abs-fontsize #13 \with-color #white \char ##x01C0 } } }
+ tagline = ##f
+}
+
+\paper {
+  top-margin = 8\mm
+  bottom-margin = 9\mm
+  top-system-spacing.basic-distance = #12
+  ragged-last-bottom = ##f
+}
+
+%----definitions
+hideTupletNumber = \override TupletNumber.transparent = ##t
+posPedal = \once \override SustainPedal.extra-offset = #'(0.4 . 0)
+posDynTxt = \once \override DynamicText.extra-offset = #'(0.4 . 0)
+
+\score {
+
+ \new PianoStaff
+ <<
+ \new Staff = "up" {
+ \clef treble
+ \key a \minor
+ \time 3/8
+ \override Score.MetronomeMark.transparent = ##t
+ \tempo 4 = 72
+ \repeat volta 2 {
+ \partial 8 e''16\pp^\markup { \bold "Poco moto." }
+ dis'' e'' dis'' e'' b' d'' c'' a'8 r16 c' e' a' b'8 r16 e' gis' b'
+ c''8 r16 e'_[ e'' dis''] e'' dis'' e'' b' d'' c'' a'8 r16 c' e' a' b'8 r16 e' c'' b' }
+ \alternative { { a'4 } { a'8 \bar "" r16 b' \set Timing.measurePosition = #(ly:make-moment -1/8) c''16 d'' } 
+ }
+ \repeat volta 2 {
+ e''8. g'16[ f'' e''] d''8. f'16[ e'' d''] c''8. e'16[ d'' c''] b'8 r16 e'_[ e''] r r e''[ e'''] r r dis''
+ e''8 r16 dis'' e'' dis'' e''16 dis'' e'' b' d'' c''
+ a'8 r16 c' e' a' b'8 r16 e' gis' b' c''8 r16 e'_[ e'' dis''] e'' dis'' e'' b' d'' c'' a'8 r16 c' e' a' b'8 r16 e' c'' b'} 
+ \alternative { { a'8 r16 b'[ c'' d''] } { a'8 r16 <e' c''>[ <f' c''> <e' g' c''>] } }
+
+ \grace { f'16[ a'] } c''4 f''16. e''32 e''8([ d'']) bes''16. a''32 a''16( g'' f'' e'' d'' c'')
+ bes'8[ a'] \appoggiatura bes'32 a'32[ g' a' bes'] c''4 d''16[ dis''] e''8. e''16[ f'' a'] c''4 d''16. b'32
+ c''32[ g'' g' g''] a'[ g'' b' g''] c''[ g'' d'' g''] e''[ g'' c''' b''] a''[ g'' f'' e''] d''[ g'' f'' d'']
+ c''32[ g'' g' g''] a'[ g'' b' g''] c''[ g'' d'' g''] e''[ g'' c''' b''] a''[ g'' f'' e''] d''[ g'' f'' d'']
+ e''32[ f'' e'' dis''] e''[ b' e'' dis''] e''[ b' e'' dis''] e''8. b'16[ e'' dis'']
+ e''8. b'16([ e'']) dis''( e'') dis''([ e'']) dis''([ e'']) dis''( e'') dis'' e'' b' d'' c''
+ a'8 r16 c' e' a' b'8 r16 e' gis' b' c''8 r16 e'_[ e'' dis''] e'' dis'' e'' b' d'' c'' a'8 r16 c' e' a' b'8 r16 e' c'' b'
+ a'8 r16 b'16 c'' d'' e''8. g'16[ f'' e''] d''8. f'16[ e'' d''] c''8. e'16[ d'' c''] b'8 r16 e'_[( e'']) r
+ r16 e''[( e''']) r r dis''( e'') r r dis''[ e'' dis''] e'' dis'' e'' b' d'' c''
+ a'8 r16 c' e' a' b'8 r16 e' gis' b' c''8 r16 e' e'' dis'' e'' dis'' e'' b' d'' c'' a'8 r16 c' e' a' b'8 r16 e' c'' b'
+
+ a'8 r r <e' g' bes' cis''>4. <f' a' d''>4 <cis'' e''>16[ <d'' f''>] <gis' d'' f''>4 <gis' d'' f''>8 <a' c''! e''>4.
+ <f' d''>4 <e' c''>16[ <d' b'>] <c' fis' a'>4 <c' a'>8 <c' a'>8[ <e' c''> <d' b'>] <c' a'>4.
+ <e' g' bes' cis''>4. <f' a' d''>4 <cis'' e''>16[ <d'' f''>] <d'' f''>4 <d'' f''>8 <d'' f''>4.
+ <g' ees''>4 <f' d''>16[ <ees' c''>] <d' f' bes'>4 <d' f' a'>8 <d' f' gis'>4 <d' f' gis'>8 <c' e'! a'>4 r8 <e' b'>8 r r
+ \tupletSpan 8 
+ \tuplet 3/2 { \posDynTxt a16\pp [ c' e'] a'[ c'' e''] d''[ c'' b'] \hideTupletNumber a'[ c'' e''] a''[ c''' e'''] d'''[ c''' b''] \ottava #1 \set Staff.ottavation = \markup {8}
+ a''[ c''' e'''] a'''[ c'''' e''''] d''''[ c'''' b'''] bes'''[ a''' gis'''] g'''  [  \ottava #0 fis''' f'''] e'''[ dis''' d''']
+ cis'''[ c''' b''] bes''[ a'' gis''] g''[ fis'' f''] }
+
+ e''16 dis'' e'' b' d'' c'' a'8 r16 c' e' a' b'8 r16 e' gis' b'
+ c''8 r16 e'_[ e'' dis''] e'' dis'' e'' b' d'' c'' a'8 r16 c' e' a' b'8 r16 e' c'' b'
+ a'8 r16 b'16 c'' d'' e''8. g'16[ f'' e''] d''8. f'16[ e'' d''] c''8. e'16[ d'' c''] b'8 r16 e'_[( e'']) r
+ r16 e''[( e''']) r r dis''( e'') r r dis''[ e'' dis''] e'' dis'' e'' b' d'' c''
+ a'8 r16 c' e' a' b'8 r16 e' gis' b' c''8 r16 e'_[ e'' dis''] e'' dis'' e'' b' d'' c'' a'8 r16 c' e' a' b'8 r16 e' c'' b'
+ a'8 r \bar "|."
+}
+
+ \new Staff = "down" {
+ \clef bass
+ \key a \minor
+ \time 3/8
+ \repeat volta 2 {
+   
+ \partial 8 r8\pp R4. a,16 e a r16 r8 e,16 e gis r r8
+ a,16 e a r r8 R4. a,16 e a r r8
+ e,16 e gis r r8 }
+ \alternative { { a,16 e a r } { a,16[ e \bar "" a16] r \set Timing.measurePosition = #(ly:make-moment -1/8) r8 } }
+ \repeat volta 2 {
+ c16 g c' r r8 g,16 g b r r8
+ a,16 e a r r8 e,16 e e' r r \clef treble e'16_[( e'']) r r dis''[ e''] r r16 dis''[ e''] r r8 R4.
+ \clef bass a,16 e a r16 r8 e,16 e gis r r8
+ a,16 e a r r8 R4. a,16 e a r r8
+ e,16 e gis r r8 }
+ \alternative { { a,16 e a r r8 } { a,16[ e a] <bes c'>[ <a c'> <g bes c'>] } }
+
+ f16 a c' a c' a f bes d' bes d' bes f e' <f g bes> e' <f g bes> e' f a c' a c' a f a c' a c' a e a c' a <d d'> f
+ g16 e' g e' g f' \clef treble <c' e'>8 r16 <f' g'>[ <e' g'> <d' f' g'>] <c' e' g'>8 \clef bass <f a>8[ <g b>]
+ \clef treble c'8 r16 <f' g'>[ <e' g'> <d' f' g'>] <c' e' g'>8 \clef bass <f a>8[ <g b>] <gis b>8 r r R4.
+ R4. R4. R4. a,16 e a r16 r8 e,16 e gis r r8 a,16 e a r r8
+ R4. a,16 e a r r8 e,16 e gis r r8 a,16 e a r r8
+ c16 g c' r r8 g,16 g b r r8 a,16 e a r r8 e,16 e e' r r
+ \clef treble e'16(_[ e'']) r r dis''([ e'']) r r dis''([ e'']) r r8 R4.
+ \clef bass a,16 e a r16 r8 e,16 e gis r r8 a,16 e a r r8 R4. a,16 e a r r8 e,16 e gis r r8
+
+ a,16 a, a, a, a, a, a, a, a, a, a, a, a, a, a, a, a, a, a, a, a, a, a, a, a, a, a, a, a, a,
+ <d, a,> <d, a,> <d, a,> <d, a,> <d, a,> <d, a,> <dis, a,> <dis, a,> <dis, a,> <dis, a,> <dis, a,> <dis, a,>
+ <e, a,> <e, a,> <e, a,> <e, a,> <e, gis,> <e, gis,> <a,, a,> a, a, a, a, a, a, a, a, a, a, a, a, a, a, a, a, a, a, a, a, a, a, a,
+ bes, bes, bes, bes, bes, bes, bes, bes, bes, bes, bes, bes, bes, bes, bes, bes, bes, bes,
+ b,! b, b, b, b, b, c4 r8 <e gis>8 r r
+ a,,8 r <a c' e'> <a c' e'> r <a c' e'> <a c' e'> r <a c' e'> <a c' e'> r r R4.
+
+ R4. a,16 e a r r8 e,16 e gis r r8
+ a,16 e a r r8 R4.
+ a,16 e a r r8 e,16 e gis r r8 a,16 e a r r8
+ c16 g c' r r8 g,16 g b r r8
+ a,16 e a r r8 e,16 e( e') r r
+ \clef treble e'16(_[ e'']) r r dis''([ e'']) r r dis''([ e'']) r r8 R4.
+ \clef bass a,16 e a r16 r8 e,16 e gis r r8 a,16 e a r r8 R4.
+ a,16  e a r r8 e,16  e gis r r8  <a,, a,>8 r \bar  "|."
+}
+
+\new Dynamics = "pedalOne" {
+  \repeat volta 2 {
+  \partial 8  s8
+    s4.
+    s4\sustainOn s16. s32\sustainOff
+    \posPedal s4\sustainOn s16. s32\sustainOff
+    \posPedal s4\sustainOn s16. s32\sustainOff
+    s4.
+    s4\sustainOn s16. s32\sustainOff
+    \posPedal s4\sustainOn s16. s32\sustainOff
+  }
+  \alternative{ { s4 } { s4. } }
+  \repeat volta 2 {
+    s4\sustainOn s16. s32\sustainOff
+    \posPedal s4\sustainOn s16. s32\sustainOff
+    s4.\sustainOn
+    s4.
+    s4 s16. s32\sustainOff
+    s4.
+    s4.
+    s4\sustainOn s16. s32\sustainOff
+    s4\sustainOn s16. s32\sustainOff
+    \posPedal s4\sustainOn s16. s32\sustainOff
+    s4.
+    s4\sustainOn s16. s32\sustainOff
+    s4.
+  }
+  \alternative { { s4. }  { s4. } }
+  \repeat unfold 17 { s4. }
+  s4\sustainOn s16. s32\sustainOff
+  \posPedal s4\sustainOn s16. s32\sustainOff
+  s4.
+  s4\sustainOn s16. s32\sustainOff
+  \posPedal s4\sustainOn s16. s32\sustainOff
+  s4.
+  s4\sustainOn s16. s32\sustainOff
+  s4.
+  s4.
+  s4.\sustainOn
+  s4 s16. s32\sustainOff
+  \repeat unfold 26 { s4. }
+  s4.\sustainOn
+  \repeat unfold 3 { s4. }
+  s4 s16. s32\sustainOff
+  s4.
+  s4\sustainOn s16. s32\sustainOff
+  s4\sustainOn s16. s32\sustainOff
+  \posPedal s4\sustainOn s16. s32\sustainOff
+  s4.
+  s4\sustainOn s16. s32\sustainOff
+  \posPedal s4\sustainOn s16. s32\sustainOff
+  s4.
+  s4\sustainOn s16. s32\sustainOff
+  \posPedal s4\sustainOn s16. s32\sustainOff
+  \posPedal s4\sustainOn s16. s32\sustainOff
+  \posPedal s4.\sustainOn
+  s4 s16. s32\sustainOff
+  s4.
+  s4.
+  s4\sustainOn s16. s32\sustainOff
+  \posPedal s4\sustainOn s16. s32\sustainOff
+  s4.
+  s4.
+  s4\sustainOn s16. s32\sustainOff
+  \posPedal s4\sustainOn s16. s32\sustainOff
+  s4
+}
+>>
+
+ \layout { }
+
+ \midi { }
+
+}
diff --git a/test/fur_Elise_WoO59.mid b/test/fur_Elise_WoO59.mid
new file mode 100644 (file)
index 0000000..ac41827
Binary files /dev/null and b/test/fur_Elise_WoO59.mid differ