Display a MIDI Track

17

7

Background

MIDI files are quite different from WAV or MP3 audio files. MP3 and WAV files contain bytes representing a "recording" of the audio, while MIDI files have a series of MIDI messages stored in MIDI events informing a MIDI synthesizer which virtual instrument to play or a MIDI sequencer the playback tempo that should be used. These messages are stored in tracks, and a collection of tracks makes up a MIDI sequence, whose events can be analyzed by a sequencer and have its messages transmitted from the sequencer to a synthesizer's receiver.

Most of the time the MIDI messages stored in MIDI events are Note On messages which tell the synthesizer to play a particular note, or Note Off messages which tell the synthesizer to stop playing the note. These messages contain two data bytes, the first of which informs the synthesizer of the velocity of the note (higher velocity results in a louder note), and the second of which tells the synthesizer the note to play (i.e. Middle C). The events themselves also contain ticks which serve the purpose of telling the sequencer when to send the messages.

The Challenge

The challenge is to write a full program or a function that analyzes a series of Note On and Note Off MIDI messages in a single-track MIDI sequence and outputs to STDOUT a chart showing when particular notes are on, when they are off, and the velocity of these notes. The vertical axis of the chart represents the note value and should be labeled as described below, and the horizontal axis represents time in MIDI ticks (though it should remain unlabeled to reduce complexity and spacing issues).

Your input may be four separate arrays or lists, each containing a series of integer values; a two-dimensional array or list containing four sub-arrays/sub-lists with a series of integer values; or any other convenient means; this represents the collection MIDI events with Note On and Note Off messages in the track. The values in the first of these arrays specifies the note, the second the velocity, the third the note on event tick, and the fourth the note off event tick. For instance, given four arrays such as these:

{60, 62, 64, 65,  67}
{20, 40, 60, 80, 100}
{ 0,  4,  8, 12,  16}
{ 2,  6, 10, 14,  18}

Analyzing the first element of each array gives two events: an event at tick 0 with a message that has a Note On command, note 60 (Middle C), and note velocity of 20; and an event at tick 2 with a message that has a Note Off command with the same note and velocity.

Rules

The chart should feature the numbers 0 through 127 displayed in decreasing order on the left side (representing the note value), when the note starts, the duration of each note (Note Off tick minus Note On tick), and the note's velocity. The symbols representing the notes are dependent on their velocity:

  • 0-15: O
  • 16-31: =
  • 32-47: #
  • 48-63: -
  • 64-79: @
  • 80-95: +
  • 96-111: 0
  • 112-127: *

You can assume the following:

  • The values for note and velocity will be within the range [0, 127].
  • The lengths of each of the four arrays will always equal each other.

Here are a few examples:

{60, 62, 64, 65,  67}
{20, 40, 60, 80, 100}
{ 0,  4,  8, 12,  16}
{ 2,  6, 10, 14,  18}

127|
126|
125|
...
67 |                00
66 |
65 |            ++
64 |        --
63 |
62 |    ##
61 |
60 |==
59 |
...
2  |
1  |
0  |


{60, 48, 62, 47, 64, 45,  65,  43, 67, 41, 65, 43, 64, 45,  62, 47, 60, 48}
{63, 31, 75, 90, 12, 23, 122, 104, 33, 19, 57, 42,  5, 82, 109, 86, 95, 71}
{0,   0,  2,  2,  4,  4,   6,   6,  8,  8, 10, 10, 12, 12,  14, 14, 16, 16}
{2,   2,  4,  4,  6,  6,   8,   8, 10, 10, 12, 12, 14, 14,  16, 16, 18, 18}

127|
126|
...
68 |
67 |        ##
66 |
65 |      **  --
64 |    OO      OO
63 |
62 |  @@          00
61 |
60 |--              ++
59 |
...
49 |
48 |==              @@
47 |  ++          ++
46 |
45 |    ==      ++
44 |
43 |      00  ##
42 |
41 |        ==
40 |
...
1  |
0  |

Here's an example that displays the first few notes of Ode to Joy:

{48, 55, 64, 64, 65, 67, 55, 67, 65, 64, 62, 52, 55,  60,  60,  62,  64,  55, 64, 62, 62}
{45, 45, 63, 63, 63, 63, 89, 66, 66, 66, 66, 30, 30, 103, 103, 103, 103, 127, 55, 55, 55}
{ 0,  0,  0,  4,  8, 12, 16, 16, 20, 24, 28, 32, 32,  32,  36,  40,  44,  48, 48, 54, 56}
{16, 16,  2,  6, 10, 14, 32, 18, 22, 26, 30, 48, 48,  34,  38,  42,  46,  64, 50, 55, 64}

127|
...
67 |            --  @@
66 |
65 |        --          @@
64 |--  --                  @@                  00  --
63 |
62 |                            @@          00            - --------
61 |
60 |                                00  00
59 |
58 |
57 |
56 |
55 |################++++++++++++++++================****************
54 |
53 |
52 |                                ================
51 |
50 |
49 |
48 |################
...
0  |

You can reduce your score by 25% if your submission takes an actual MIDI sequence as input, analyzes the Note On and Note Off messages of any track of your choosing provided it contains at least four events with Note On and Note Off messages, and outputs a chart as described above.

This is code golf, so shortest code wins. Good luck!

TNT

Posted 2015-06-25T22:55:00.820

Reputation: 2 442

Answers

6

PHP, 127 + 571 = 698 total score*

Okay, I'm claiming the bonus. :) This will take a standard MIDI file and display the output.

I've broken up the score above into the main challenge (analyze note on/off and display as chart) and the bonus challenge (read input from standard MIDI) to make scores more comparable.

Main: 170 bytes - 25% = 127

For the main, the function $d() takes the required array and displays the ASCII output. Included are all tests and output of test MIDI file below.

$d=function($a){for($l=max($n=$a[0]);$l>=min($n);){$r=' |';foreach($n as$c=>$e)while($e==$l&$a[2][$c]<$a[3][$c])$r[++$a[2][$c]+1]='O=#-@+0*'[$a[1][$c]/16];echo$l--,$r,"
";}}

Try it online!

Bonus: 761 bytes - 25% = 571

Function $m() will load a standard MIDI file (either locally or by URL) and return an array of tracks, each containing an array in the specified note format for all of the MIDI file tracks.

$m=function($f){$a=function($f){do$s=($s<<7)+(($c=unpack(C,fread($f,1))[1])&127);while($c&128);return$s;};$r=function($n){foreach($n as$e){if($e[4]==9&&$e[1]>0)foreach($n as$y=>$f)if($f[0]==$e[0]&&($f[4]==8||($f[4]==9&&$f[1]==0))){$o[0][]=$e[0];$o[1][]=$e[1];$o[2][]=$e[2];$o[3][]=$f[2];$n[$y][4]=0;break;}}return$o;};$m=fopen($f,r);while($b=fread($m,8)){$z=unpack(N2,$b)[2];if($b[3]==d){$k=unpack(n3,fread($m,$z))[3]/4;}else{$t=0;$n=[];$d=ftell($m)+$z;while(ftell($m)<$d){$t+=$a($m);if(($e=unpack(C,fread($m,1))[1])==255){fread($m,1);if($w=$a($m))fread($m,$w);}else{if($e>127)list(,$e,$h)=unpack('C*',fread($m,($y=(240&$e)>>4)==12?1:2));else$h=unpack(C,fread($m,1))[1];if($y==9|$y==8)$n[]=[$e,$h,(int)round($t/$k),0,$y];}}if($n)$u[]=$r($n);}}fclose($m);return$u;};

See it online! Obviously TIO is sandboxed as to not allow remote requests or local files, so you'll have to run this code locally to see it in action. The first [tests][TIO-jrwa60tu] in the display function includes the array result from the test MIDI file.

MIDI file load routine ungolfed:

$m=fopen($f,'r');                           // m = midi file handle
while($b=fread($m,8)){                      // read chunk header
    $z=unpack('N2',$b)[2];                  // z = current chunk size
    if($b[3]=='d'){                         // is a header chunk?
        $k=unpack('n3',fread($m,$z))[3]/4;  // k = ticks per quarter note (you can change the 4 to 8 or 16 to "zoom in" so each char represents eights or sixteenth notes)
    }else{                                  // is a track chunk?
        $t=0;                               // track/chunk time offset starts at 0
        $d=ftell($m)+$z;                    // d = end of chunk file pos
        while(ftell($m)<$d){                // q = current file pos
            $t+=$a($m);                     // decode var length for event offset and add to current time
            if(($e=unpack('C',fread($m,1))[1])==255){ // is a META event 
                fread($m,1);                // read and discard meta event type
                if($w=$a($m))
                    fread($m,$w);
            }else{                          // is a MIDI event
                if($e>127) {                // is a new event type
                    list(,$e,$h)=unpack('C*',  // if is a prog change (0x0c), event is 1 byte
                        fread($m,($y=(240&$e)>>4)==12?1:2)); // otherwise read 2 bytes
                } else {                    // is a MIDI "streaming" event, same type as last
                    $h=unpack('C',fread($m,1))[1];
                }
                if($y==9|$y==8)             // if is a Note On or Note Off
                    $n[]=[$e,$h,(int)round($t/$k),0,$y];  // add note to output
            }
        }
        if($n)                              // if this track has notes,
            $u[]=$r($n);                    // add to array of output tracks ($u)
    }
}
fclose($m); // yes, could golf this out and rely on PHP GC to close it

A test MIDI file of "Ode to Joy" that can be used downloaded here. Example use:

$d( $m( 'beethoven_ode_to_joy.mid' )[0] );      // display first track

$d( $m( 'https://www.8notes.com/school/midi/piano/beethoven_ode_to_joy.mid' )[0] );

foreach( $m( 'multi_track_song.mid' ) as $t ) {  // display all tracks
    $d( $t );
}

"Ode to Joy" MIDI file output

67 |            0000++++                                                        00000000                                                                                                                        00000000
66 |
65 |        0000        ++++                                                0000        0000                                                              @@              @@                                0000        ++++
64 |++++++++                ++++                0000000000          00000000                0000                0000                        @@@@        @@  ----        @@  ----                ++++++++++++                ++++                0000
63 |
62 |                            ++++        0000          00++++++++                            ++++        0000    000000          @@@@----        ----            @@@@        ----    ----                                    ++++        0000    000000
61 |
60 |++++                            ++++0000                        0000                            ++++0000              ++00000000            ----            ----                ----            00000000                        ++++0000    ****      ++00000000
59 |                                                        ++++++++
58 |                                                                                                                                                                                                        00000000
57 |                                                                                                                                                                                ----                            ++++++++
56 |                                                                                                                                                                        --------
55 |++++++++++++++++++++++++00000000000000000000000000000000++++++++00000000000000000000000000000000000000000000000000000000        ----------------------------------------                --------                                        0000    ++++++++00000000
54 |                                                                                                                                                                                    ----
53 |                                                                                                                                                                                                                        ++++++++
52 |                                0000000000000000                                                0000000000000000                                                                                                                ++++0000                00000000
51 |
50 |
49 |
48 |++++++++++++++++                0000000000000000                0000000000000000                0000000000000000        ++++++++                                                                                                                        00000000

Notes

In MIDI format, Note On / Note Off events are atomic, meaning that you see a Note On event at a certain time for a given note (say E5), and it's implied that it will play until a Note Off event for another E5 note is seen. As such, it's necessary to analyze the MIDI events and match up a given Note On to it's Note Off, which the code for that is 297 184 bytes. Further complicating this, it's fairly common in standard MIDI format to see a subsequent matching Note On with a velocity 0 representing the same thing as a Note Off.

This will now correctly read files that have zero-velocity Note On's instead of Note Off's, so should open most standard files.

Caveats

This is by no means a complete implementation of the MIDI format, however I have tested this with a fairly extensive collection of MIDI files and it reads them all nicely.

This submission has not been golfed to an extreme yet, so entirely likely this can be made smaller. I do think it's very unlikely that the 25% score reduction bonus would offset the code needed to read a standard MIDI file. As the (current) smallest submission that just does the ASCII display is 106 65 bytes, it would require the MIDI file routines to be implemented in 25 21 bytes to beat. I'd challenge anyone to do that (without using a language built-in or module). :)

640KB

Posted 2015-06-25T22:55:00.820

Reputation: 7 149

This is an awesome answer. Looking back at this challenge, I do agree that the bonus amount probably won't reduce scores enough to account for the overhead of reading a MIDI file. (I think bonuses are discouraged nowadays anyway.) Nevertheless I am very impressed that you took on the bonus challenge. I might give you a good bounty for it. – TNT – 2019-06-03T19:17:08.263

@TNT, thanks! Really enjoyed doing it and was interesting trying to golf file format routines for something as goofy as SMF. Great challenge! – 640KB – 2019-06-05T17:05:29.697

5

Ruby, 106 bytes

This was fun. I'm not sure why no one attempted it.

This function takes input as four array arguments and returns an array of strings, one for each line of the chart.

->a,*r{q=(0..z=127).map{|i|"%3d|"%(z-i)+" "*1e4}
a.zip(*r){|n,v,o,f|q[z-n][o+4]="O=#-@+0*"[v/16]*(f-o)}
q}

Note: This arbitrarily assumes that there will be no more than 10,000 ticks. If you run it in your terminal I suggest piping it to less so you can scroll horizontally. You can change 1e4 if you want more ticks, all the way up to 9e9, but that will take a terabyte or two of RAM.

See it on repl.it: https://repl.it/Cx4I/1

Jordan

Posted 2015-06-25T22:55:00.820

Reputation: 5 001

Thanks for the submission! But strangely enough I'm unable to see the output on repl (all I can see are the numbers 127-0 with a lot of returns between them). I've never used repl before so I wouldn't know why, though. Could you suggest a way for me to see the output properly? – TNT – 2016-08-28T17:13:04.703

That's pretty strange. It works for me. I'm not at a computer just now, but here's a screenshot from my phone: http://i.stack.imgur.com/3UCyn.jpg

– Jordan – 2016-08-28T17:16:58.407

Thanks for the screenshot. I'm thinking the problem could be the web browser that I'm using, so I'll try it in a different one later. +1 from me though. :) – TNT – 2016-08-28T17:23:39.400

2

Python 2, 163 160 156 145 bytes

This is not the golfiest way to do it, but it was one of the simplest. If I could figure out how to replace parts of strings without turning them into lists, replacing, and turning them back into strings, that would be very helpful here. Golfing suggestions welcome.

Edit: 18 bytes thanks to Leaky Nun. Try it on Ideone!

a=input();z=[" "*max(a[3])]*128
for n,v,b,e in zip(*a):z[n]=z[n][:b]+"O=#-@+0*"[v/16]*(e-b)+z[n][e:]
for i in range(128)[::-1]:print"%3d|"%i+z[i]

Sherlock9

Posted 2015-06-25T22:55:00.820

Reputation: 11 664

@LeakyNun Whoops, my bad – Loovjo – 2016-08-28T08:50:28.227

Can you use regular expression substitution? In Ruby something like str.sub(/(?<=.{20}).{3}/,"foo") is equivalent to str[20,3] = "foo". Of course, that means constructing the regexp by string interpolation/concatenation with the index/length variables—which is cheap in Ruby bytes, but maybe not in Python. – Jordan – 2016-08-28T16:32:56.750

1

Japt, 65 bytes

®Æ"O=#-@+0*"gXzG
#€Çs ú3 +'|ÃúUmg2 rÔ+5
£VhXÎVgXv)hXÎ+4Xo pXra
Vw

Try it online!

Takes input as a list of notes in the format [pitch, start_tick, end_tick, velocity]. If taking input as separate lists is mandatory (i.e. one list containing all the pitches, one containing all the velocities etc.), that can be accomplished at the cost of 1 byte.

Explanation:

®Æ"O=#-@+0*"gXzG          #Gets the velocity character to use for each note
®                         # For each note in the input
 Æ                        # Replace the last item X with:
             XzG          #  Integer divide X by 16
  "O=#-@+0*"g             #  Get the character at that index in the string "O=#-@+0*"

#€Çs ú3 +'|ÃúUmg2 rÔ+5    #Generate the blank chart
#€Ç        à              # For each number X in the range [0...127]:
   s                      #  Turn X into a string
     ú3                   #  Right-pad with spaces until it is 3 characters long
        +'|               #  Add "|" to the end
            ú             # Right pad each of those with spaces to this length:
             Umg2         #  Get all the end_tick values
                  rÔ      #  Find the largest one
                    +5    #  Add 5

£VhXÎVgXv)hXÎ+4Xo pXra    #Put the notes into the chart
£                         # For each note:
     VgXv)                #  Get a line from the chart based on the note's pitch
          h               #  Overwrite part of that line:
           XÎ+4           #   Starting at index start_tick +4
               Xo         #   Overwrite characters with the velocity character
                  pXra    #   For the next end_tick - start_tick characters
 VhXÎ                     #  Put the modified line back into the chart

Vw                        #Print the chart
V                         # Get the chart
 w                        # Reverse it (so 127 is the first line)
                          # Implicitly print it

Kamil Drakari

Posted 2015-06-25T22:55:00.820

Reputation: 3 461