| Bytes | Lang | Time | Link |
|---|---|---|---|
| 048 | Japt R | 250424T143255Z | Shaggy |
| 106 | Ruby | 160828T050846Z | Jordan |
| nan | PHP | 190207T235206Z | 640KB |
| 065 | Japt | 190213T192745Z | Kamil Dr |
| 145 | Python 2 | 160828T074421Z | Sherlock |
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 L³ for 1,000,000, L² 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}
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,"
";}}
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
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]