#!/usr/bin/perl -w my @files = ("HeyJude_reduced", "Imagine_reduced", "Obladi_reduced", "TearsGoBy_reduced", "", "", ); $afile = $files[3]; print "\n$afile\n"; my $path = "/Users/me/Desktop"; my $outpath = "/Users/me/Desktop/compiledMIDI"; my $insuffix = '.mid'; my $outsuffix = '.csv'; # our xylo goes from midi note 69 (A4 = 440Hz) up to 94 (A#6) # although 94 is not yet installed, so 93 is top note. my $lowest_note = 69; my $highest_note = 93; #run file thru "midicsv" my $converter = "/Users/me/Desktop/MIDIstuff/midicsv"; @csvdata = `$converter < $path/$afile$insuffix`; #log the CSV data for reference #open (CSV, ">$path/$afile$outsuffix") # or die "cannot create csv output for $afile; $!"; #foreach $line(@csvdata) { # print CSV $line; #} #close CSV or die "failed to close CSV after writing; $!"; #sort csv data by timecode @data = (); foreach $line(@csvdata) { chomp $line; $fields = parse_csv($line); #$fields is a reference to an array of fields # $fields->[0] is track number # $fields->[1] is elapsed time in pulses # $fields->[2] is a command # further fields depend on which command we have push @data, $fields; } @sorted = sort by_timecode @data; #after the sort, @data is no longer needed print scalar @sorted, " lines in CSV data\n"; #get the header info foreach $d(@sorted) { if ($d->[2] eq 'Tempo') { $tempo = $d->[3]; #microseconds per quarter note } elsif ($d->[2] eq 'Header') { $ppqn = $d->[5]; #pulses per quarter note } } print "tempo is $tempo uSec per quarter note\n"; $quantum = $ppqn / 16; #we will count in 64th-note intervals print "ppqn = $ppqn; quantum = $quantum\n"; $prev_elapsed = 0; @score = (); #quantize the time intervals foreach $d(@sorted) { next unless $d->[2] eq 'Note_on_c' and $d->[5] > 0; $delta = $d->[1] - $prev_elapsed; if ($delta < $quantum) { $delta = 0; } else { $prev_elapsed = $d->[1]; } $ticks = int(0.45 + $delta / $quantum); $note = $d->[4]; #print "$delta \t $ticks \t $note\n"; push @score, "$ticks\t$note"; } print "score has ", scalar @score, " lines\n"; #now @sorted is no longer needed #We can use a coarser quantum if every delta is divisible by 2 while (divisible()) { $quantum *= 2; print "quantum is resized to $quantum\n"; foreach $s (@score) { ($ticks, $note) = split("\t", $s); $ticks /= 2; $s = "$ticks\t$note"; } } #foreach $s(@score) { # print "$s\n"; #} #do we need to transpose? find the range of notes $note_min = 150; #a high value -- MIDI scale tops at 128 $note_max = 0; #lower than lowest real MIDI note foreach $s(@score) { ($ticks, $note) = split("\t", $s); if ($note < $note_min) { $note_min = $note; } if ($note > $note_max) { $note_max = $note; } } print "lowest note in score is $note_min\n"; print "highest note is $note_max\n"; if ($highest_note >= $note_max and $lowest_note <= $note_min) { $offset = 0; print "no transposition needed\n"; } else { $offset = $highest_note - $note_max; print "transposing by $offset semitones\n"; } #in score, ticks==0 means note is part of previous chord # ticks==1 means new chord # ticks >1 means t-1 delays, then new chord #outlist will be a list of tab-separated chords @outlist = (); %chord = (); #use a hash to unduplicate notes of chord foreach $s(@score) { ($ticks, $note) = split("\t", $s); $note += $offset; #apply any transposition if ($note < $lowest_note) { $note = 0; } #eliminate out-of-range if ($ticks == 0) { $chord{$note} ++; #add note to the current chord } else { push @outlist, join("\t", keys %chord); #close out prior chord %chord = (); #setup for next chord #insert silence if ticks > 1 for ($j = $ticks; $j > 1; $j--) { push @outlist, 0; } $chord{$note} ++; #and start filling the chord } } push @outlist, join("\t", keys %chord); #flush the last chord print "the outlist has ", scalar @outlist, " lines\n"; #foreach $d(@outlist) { print "$d\n"; } #determine how many parts of harmony to code for $maxparts = 0; foreach $d(@outlist) { @listraw = split("\t", $d); $parts = 0; foreach $item(@listraw) { if ($item > 0) { $parts++; } } if ($parts > $maxparts) { $maxparts = $parts; } } print "score had up to $maxparts notes per chord\n"; #arbitrarily limit to 4-note chords if ($maxparts > 4) { $maxparts = 4; } #write xylo file header $outname = uc substr($afile, 0, 8); $fullname = substr($afile, 0, 16); $mSec_per_note = int( ($tempo * $quantum) / (1000 * $ppqn)); print "Xylophone notes should be $mSec_per_note mSec apart\n"; $tempo_msb = int($mSec_per_note / 128); #in effect, LSB is 7-bit value $tempo_lsb = $mSec_per_note - (128 * $tempo_msb); open(OUT, ">$outpath/$outname") or die "failed to create $outname as output file; $!"; print OUT 'A'; #this script does not write type C=canon print OUT chr($tempo_msb + ord(" ")); print OUT chr($tempo_lsb + ord(" ")); print OUT chr($maxparts + ord(" ")); print OUT $fullname; print OUT "\0"; #terminate the $fullname string #format music data for xylo foreach $d(@outlist) { @listraw = split("\t", $d); @listitems = sort {$b <=> $a} @listraw; #sort numerically, descending #foreach my $x(@listitems) {print "$x\t"; } #print "\n"; for (my $j = 0; $j < $maxparts; $j++) { if (defined $listitems[$j]) { print OUT pincode($listitems[$j]); } else { print OUT pincode(0); } } } close(OUT) or die "failed to close $outname; $!"; print "\n\n"; ################## sub tf { $param = shift; if ($param) { return "yes"; } else { return "no"; } } ############################# sub parse_csv { my $input = shift; my @ar = split(', ', $input); return \@ar; } ############################# sub by_timecode { $a->[1] <=> $b->[1]; } ############################# sub divisible { $div2 = 1; #true foreach $s(@score) { ($ticks, $note) = split("\t", $s); if ($ticks % 2 != 0) { $div2 = 0; #false } } return $div2; } ############################# sub pincode { #go from midi note number to arduino pin number, with offset += ' ' my @pinmap = (47, 46, 45, 44, 43, 42, 41, 40, 39, 38, 37, 36, 35, 34, 33, 32, 31, 30, 29, 28, 27, 26, 25, 24, 23, 22); my $midinumber = shift; if ($midinumber == 0) { return ' '; } my $index = $midinumber - $lowest_note; if ($index < 0) { print "ERROR: midi number $midinumber is out of range\n"; #this should have been trapped earlier return ' '; } return chr($pinmap[$index] + ord(' ')); #add ord(' ') to make file text-readable } ############################# # output file format: # (file name & file size are directory properties) # char songtype = {linear='A', canon='C'} # int tempo = # byte tempo_msb # byte tempo_lsb # char parts = 1-4, with offset += ' ' # # if canon: # byte notes_per_line, with offset += ' ' # # C-string fullname # byte[] notes, with offset += ' ' (no white space) #############################