3 # converter from Type 1 MIDI files to BGS files that control particle effects on maps
5 # perl midi2bgs.pl filename.mid tracknumber channelnumber offset notepattern > filename.bgs
6 # track and channel numbers -1 include all events
7 # in patterns, %1$s inserts the note name, %2$d inserts the track number, and %3$d inserts the channel number
9 # perl midi2bgs.pl filename.mid -1 10 0.3 'note_%1$s_%3$d_%2$d' > filename.bgs
16 my ($filename, $trackno, $channelno, $offset, $notepattern) = @ARGV;
18 unless defined $notepattern;
20 or die "usage: $0 filename.mid {trackno|-1} {channelno|-1} offset [notepattern]\n";
22 my $opus = MIDI::Opus->new({from_file => $filename});
23 my $ticksperquarter = $opus->ticks();
24 my $tracks = $opus->tracks_r();
25 my @tempi = (); # list of start tick, time per tick pairs (calculated as seconds per quarter / ticks per quarter)
29 for($tracks->[0]->events())
32 if($_->[0] eq 'set_tempo')
34 push @tempi, [$tick, $_->[2] * 0.000001 / $ticksperquarter];
41 my $curtempo = [0, 0.5 / $ticksperquarter];
46 # this event is in the past
47 # we add the full time since the last one then
48 $sec += ($_->[0] - $curtempo->[0]) * $curtempo->[1];
52 # if this event is in the future, we break
57 $sec += ($tick - $curtempo->[0]) * $curtempo->[1];
58 return $sec + $offset;
61 my @notes = ('c', 'c#', 'd', 'd#', 'e', 'f', 'f#', 'g', 'g#', 'a', 'a#', 'b');
63 for my $octave (0..11)
69 push @notenames, uc($_) . ',' x (3 - $octave);
73 push @notenames, lc($_) . "'" x ($octave - 4);
78 # merge all to a single track
79 my @allmidievents = ();
81 for my $track(0..@$tracks-1)
84 for($tracks->[$track]->events())
86 my ($command, $delta, @data) = @$_;
88 push @allmidievents, [$command, $tick, $sequence++, $track, @data];
91 @allmidievents = sort { $a->[1] <=> $b->[1] or $a->[2] <=> $b->[2] } @allmidievents;
93 my @outevents = (); # format: name, time in seconds, velocity
97 my %notecounters_converted;
100 my $t = tick2sec $_->[1];
103 unless $trackno < 0 || $trackno == $track;
104 if($_->[0] eq 'note_on')
106 my $chan = $_->[4] + 1;
107 my $note = sprintf $notepattern, $notenames[$_->[5]], $trackno, $channelno;
108 my $velocity = $_->[6] / 127.0;
109 push @outevents, [$note, $t, $velocity]
110 if($channelno < 0 || $channelno == $chan);
111 ++$notecounters_converted{$note}
112 unless $notecounters{$chan}{$_->[5]};
113 $notecounters{$chan}{$_->[5]} = 1;
115 elsif($_->[0] eq 'note_off')
117 my $chan = $_->[4] + 1;
118 my $note = sprintf $notepattern, $notenames[$_->[5]], $trackno, $channelno;
119 my $velocity = $_->[6] / 127.0;
120 --$notecounters_converted{$note}
121 if $notecounters{$chan}{$_->[5]};
122 $notecounters{$chan}{$_->[5]} = 0;
123 if($notecounters_converted{$note} == 0)
125 push @outevents, [$note, $t, 0]
126 if($channelno < 0 || $channelno == $chan);
130 for(sort { $a->[0] cmp $b->[0] or $a->[1] <=> $b->[1] } @outevents)
132 printf "%s %13.6f %13.6f\n", @$_;