g | x | w | all
Bytes Lang Time Link
048Japt R250424T143255ZShaggy
106Ruby160828T050846ZJordan
nanPHP190207T235206Z640KB
065Japt190213T192745ZKamil Dr
145Python 2160828T074421ZSherlock

Japt -R, 56 54 51 48 bytes

Started as a golf of Kamil's solution and evolved a bit from there, so be sure to upvote them too.

Takes input as individual arrays of the form [note,velocity,off,on].

If we could assume a maximum number of ticks then Nx¤ could be replaced with for 1,000,000, for 10,000, or even just L for 100.

#o+Nx¤ç
N£gXÎ_hXÌ+3"O=#-@+0*"gXÅÎzG)pX¤r-
ÔËi|3

Try it

Explanation

#\u0080o+Nx¤ç                    :Assign to variable U
#\u0080                          :128
       o                         :Range [0,128)
        +                        :Append to each
         N                       :  Array of all inputs
          x                      :  Reduce by addition after
           ¤                     :    Slicing off the first 2 elements*
            ç                    :  Repeat space that many times
N£gXÎ_hXÌ+3"..."gXÅÎzG)pX¤r-
N£                               :Map each X in the array of inputs
  g                              :  Mutate the element in U at 0-based index
   XÎ                            :    First element of X
     _                           :    By passing it through the following function
      h                          :      Replace the characters starting at index
       XÌ+3                      :        Last element of X +3
           "..."                 :        With the character in this string
                g                :        At index
                 XÅ              :          Slice off the first element of X
                   Î             :          Get first element
                    z            :          Floor divide by
                     G           :            16
                      )          :        End indexing
                       p         :        Repeat this many times
                        X¤       :          Slice off the first 2 elements of X
                          r-     :          Reduce the remaining 2 by subtraction
ÔËi|3
Ô                                :Reverse U
 Ë                               :Map
  i|3                            :  Insert a "|" at index 3

*This works because the x method, after applying the slice method, passes each element in the array through JavaScript's parseInt function, which firstly coerces each sub-array to a string and then parses that string up to the point it reaches an invalid character - the comma, in this case - in the given base, which defaults to 10.

Output

This is the cropped output for the "Ode to Joy" test case above, if your screen isn't big enough to fit each line.

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

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}

Attempt This Online!

The code above assumes no more than 10,000 ticks; the ATO link lowers that to 100 to avoid the output being truncated. You can change 1e4 to any number you want if you run it locally but you'll run out of memory eventually. You may want to pipe the output through less so you can scroll horizontally.

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). :)

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

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]