#!/usr/local/bin/perl use warnings; use strict; ######################################################################## # # FILE: midi_pov.pl # # DESCRIPTION: Reads a MIDI file and converts the information in # the file to a POVRay include file. This output file # can be used in conjunction with midi_pov.inc to # create an animation based on the notes contained in # the MIDI file. # # Requries access to the MIDI Perl module, by Sean M. # Burke, available from CPAN. # # USAGE: ./midi_pov.pl infile.mid > outfile.inc # # Written by dan B hentschel, dan@hentschels.com. # ######################################################################## use MIDI; # ######### # Store the last note-off event # recorded. This will give us the # total run length of the MIDI file. # I know, it's a global variable, # and globals are messy. Sorry, # I'm feeling lazy right now. my $g_max_time = 0; # ##################### # The function that does all the work. sub main($) { my $in_midi = MIDI::Opus->new({from_file=>shift}); my $tracks = $in_midi->tracks_r(); my $num_tracks = scalar(@$tracks); # ################## # Obtain the number of ticks per quarter note. This value # is constant for the entire file. Most MIDI files will use # a multiple of 96 for this value since it works quite well # with common musical divisions. my $ticks_per_beat = $in_midi->ticks(); # ############# # Calculate the tempo map based on the ticks_per_beat value. # The tempo change information is generally contained in the # first track of the file. If this is not the case for the # input file, then this may not work correctly. my $tempo_map = make_tempo_map($tracks->[0], $ticks_per_beat); print_inc_header(); # ############ # Total tracks in the MIDI file. Not # all tracks will contain note events. print "#declare Num_Tracks = $num_tracks;\n"; # ####################### # Write out the note tables for each track # in the file. for (my $i = 0; $i < $num_tracks; $i++) { # ############## # Obtain a structure describing all note # events (if any) in the track. my $notes = get_track_notes($tracks->[$i], $tempo_map); # ############## # Now go through all 128 possible notes # (pitches) possible in the MIDI format. # Here, we will use names (i.e. Cs5) # rather than numbers to describe # the notes. This improves readability # of the resulting include file. foreach my $name (keys(%MIDI::note2number)) { # ############## # Check to see if the current track has # any note events associated with this # particular pitch. if ($notes->{$name}) { # ############ # Number of times this note is played # in the current track. my $num = scalar(@{$notes->{$name}}); # ########## # Counter to place the comma appropriately my $j = 1; # ########### # Record the number of notes, useful for # dereferencing the note array, and the # note array itself. print "#declare T${i}_${name}_Count = $num;\n"; print "#declare T${i}_${name}_Notes =\n"; print "array[$num][4]\n"; print "{\n"; # ############ # The array contains start and end times, # both in units of seconds, plus the velocity # of each instance of the note in question. foreach my $note (@{$notes->{$name}}) { print " {" . $note->{'start'} / 1000000; print "," . $note->{'end'} / 1000000; print "," . $note->{'velocity'}; print "," . $note->{'patch'} . "}"; # ########## # Add a comma if this isn't the last one. if ($j < $num) { print ","; } print "\n"; $j++; } print "}\n"; } else { # ################ # If there are no events for this pitch, then # create an array with a null element in it. print "#declare T${i}_${name}_Count = 0;\n"; print "#declare T${i}_${name}_Notes = array[1] {0};\n"; } } } # ####################### # Total seconds is based on the last end_note event found # in all the tracks. print "#declare Total_Seconds = " . $g_max_time / 1000000 . ";\n"; # ##################### # There are two "maps" that allow POVRay programs to map # between the MIDI note number [0..127] and the descriptor # arrays created above. I did things this way to keep the # include file somewhat readable. The track count map tells # how many notes there are in the note array for each pitch. # The track notes map provides access to the note array for # each pitch. # ############## # First we will create the track count map. print "#declare Track_Count_Map =\n"; print "array[$num_tracks][128]\n"; print "{\n"; # ############### # Loop through all tracks, whether they contain notes or not. # We will map all the tracks to their corresponding descriptors. for (my $i = 0; $i < $num_tracks; $i++) { print " {\n"; for (my $j = 0; $j < 128; $j++) { # ################ # Get the note name (i.e. Cs5) for # this note number. my $name = ${MIDI::number2note}{$j}; print " T${i}_${name}_Count"; if ($j < 127) { print ","; } print "\n"; } print " }"; if ($i < $num_tracks - 1) { print ","; } print "\n"; } print "}\n"; # ############# # Now create the track notes map. print "#declare Track_Notes_Map =\n"; print "array[$num_tracks][128]\n"; print "{\n"; # ############### # Loop through all tracks, whether they contain notes or not. # We will map all the tracks to their corresponding descriptors. for (my $i = 0; $i < $num_tracks; $i++) { print " {\n"; for (my $j = 0; $j < 128; $j++) { # ################ # Get the note name (i.e. Cs5) for # this note number. my $name = ${MIDI::number2note}{$j}; print " T${i}_${name}_Notes"; if ($j < 127) { print ","; } print "\n"; } print " }"; if ($i < $num_tracks - 1) { print ","; } print "\n"; } print "}\n"; } # ############### # This function prints a descriptive header # at the top of the include file. sub print_inc_header() { print <= Now_Time) #debug " Note " #debug str(This_Note, 0, 0) #debug " in track " #debug str(This_Track, 0, 0) #debug " is on.\\n" #end #declare This_Event = This_Event + 1; #end #declare This_Note = This_Note + 1; #end #declare This_Track = This_Track + 1; #end NOTE: Because of a lingering bug in midi_pov.pl, the time calculated in Total_Seconds tends to be a bit off. The above code example will still work because the relative times generally seem to be correct. However, when calculating the number of frames to render for your animation, you should calculate based on the actual play-time of the musical piece, and not based on the calculated Total_Seconds value. FYI -- The keys on a piano keyboard go from MIDI note number 21 (A1) through 108 (C9), and "middle C" (C5) is note number 60. Also, immediately following is a map of patch numbers to instrument names. *********************************************************/ EOF print "#declare Instrument_Map = array[128] { "; for (my $i = 0; $i < 128; $i++) { print '"'; print ${MIDI::number2patch}{"$i"}; print '"'; if ($i < 127) { print ", "; } } print " };\n\n"; } # ########################### # This function creates a hash with information # about all note events in the provided track. # The note information can be accessed within the # hash via note name (i.e. Cs5, A7, etc.) with the # following syntax: $event_array = $notes->{'Cs5'}. # # Inputs: $track - A MIDI::Track object loaded from # the input file. # $tempo - A tempo map created by calling # make_tempo_map() sub get_track_notes($$) { my $track = shift; my $tempo = shift; # ################## # Use the MIDI::Score object to obtain note start # and end information without having to parse it # from the track events myself. my $score = MIDI::Score::events_r_to_score_r($track->events_r); my $notes = {}; my $max_tempo = scalar(@$tempo); my $time = []; my @channel = (0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0); my $ms = 0; my $i; my $j; my $max = 0; # ################# # Using the tempo map, create a time map. # The time map is an array that contains # the number of microseconds that have # elapsed for each MIDI 'tick' count in # this file. The number of seconds elapsed # for a given tick can be obtained by: # $seconds = $time->[$tick] / 1000000; for ($i = 0; $i < $max_tempo; $i++) { $ms += $tempo->[$i]; push(@$time, $ms); } # ########### # The previous loop populates the time map up to the # last tempo change. However, the track is likely to # contain events that occur after the last tempo change. # Search though the Score object to find the last note # event in the current track. foreach my $note (@$score) { if ($note->[0] eq 'note') { my $end = $note->[1] + $note->[2]; if ($end > $max) { $max = $end; } } } # ############### # Now populate the time map up to the last note # event, using the last tempo to calculate the # time values. for ($j = --$i; $j <= $max; $j++) { $ms += $tempo->[$i]; push(@$time, $ms); } # ################# # If the total time of this track is longer than # the time of previous tracks, then store this as # the maximum time for the entire file. This variable # is a global variable. Sloppy, but I'm lazy. Oh well. if ($ms > $g_max_time) { $g_max_time = $ms; } # ################ # Now that we have our time map, let's go through # all the 'note' events in the Score object and # add them to the $notes hash that we will be # returning from this function. foreach my $note (@$score) { # ################ # If a patch change is specified, then dynamically update # the channels array with the correct information. if ($note->[0] eq 'patch_change') { $channel[$note->[2]] = $note->[3]; } # ################## # Ignore other events that are not tagged as 'note' if ($note->[0] eq 'note') { my $start = $note->[1]; my $end = $start + $note->[2]; my $item = {}; # ################## # Get the note name (i.e. Cs5, A7, etc.) for # the MIDI pitch number. my $name = ${MIDI::number2note}{$note->[4]}; # ################## # Calculate the time for start and end events # by dereferencing our time map created above. $item->{'start'} = $time->[$start]; $item->{'end'} = $time->[$end]; $item->{'patch'} = $channel[$note->[3]]; $item->{'velocity'} = $note->[5]; # ################ # If we haven't created an array for this # note yet, do so now. $notes->{$name} = [] unless ($notes->{$name}); # ################# # Add the note descriptor we created above to # the array for this note. push(@{ $notes->{$name} }, $item); } } # ########### # Return the hash we built. return $notes; } # ########################### # This function creates an array containing the tempo # information for each tick represented in the passed # in track. The information is stored as instantaneous # microseconds per tick for each tick in the track. # The result can be interpreted as the number of seconds # pertaining to tick number X can be calculated as the # sum of $map->[0] .. $map->[X], divided by 1,000,000. # # Inputs: $track - A MIDI::Track object loaded from # the input file. This will likely # be the first track in the file. If # this track does not contain any # tempo change events, then the script # will exit with an error status. # $ticks_per_beat - The number of ticks that are # used in this file to represent # one quarter note. sub make_tempo_map($$) { my $track = shift; my $ticks_per_beat = shift; my $map = []; my $ms = 0; # ########### # Go through the events in this track, looking for # 'set_tempo' events. foreach my $event ( $track->events ) { if ($event->[0] eq 'set_tempo') { # ################# # The [1] element of the event array contains # a delta (in ticks) from the previous event to # the current event. Use the $ms value from the # previous loop to fill in the tempo map between # the previous event and the current one. Then # store off the currently calculated $ms value to # be used in the next iteration. for (my $i = 0; $i < $event->[1]; $i++) { push(@$map, $ms); } # ############# # Calculate microseconds per tick is equal # to microseconds per quarter note, divided # by ticks per quarter note. $ms = $event->[2] / $ticks_per_beat; } } # ############### # If we don't have a final tempo value, and # the tempo map is empty, then we don't know # how fast we are going. This is likely because # the first track in the file has no tempo # information in it. This script is not set up # to handle files without tempo information in # the first track. This is technically legal to # do, but is very non-standard, and it is # unlikely that this will come up. if ($ms == 0) { die "No tempo track found!" unless scalar(@$map); } # ############# # Store the last calculated $ms value in the tempo # map. We don't know a duration for this value since # we don't know the length of the other tracks, but we # will fill this information in later. For now, we just # need one copy of this value for use later. push(@$map, $ms); return $map; } main(shift);