Merge branch 'master' of git://git.xonotic.org/xonotic/xonotic
[xonotic/xonotic.git] / misc / tools / midi2bgs.pl
1 #!/usr/bin/perl
2
3 # converter from Type 1 MIDI files to BGS files that control particle effects on maps
4 # usage:
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
8 # example:
9 #   perl midi2bgs.pl filename.mid -1 10 0.3 'note_%1$s_%3$d_%2$d' > filename.bgs
10
11 use strict;
12 use warnings;
13 use MIDI;
14 use MIDI::Opus;
15
16 my ($filename, $trackno, $channelno, $offset, $notepattern) = @ARGV;
17 $notepattern = '%1$s'
18         unless defined $notepattern;
19 defined $offset
20         or die "usage: $0 filename.mid {trackno|-1} {channelno|-1} offset [notepattern]\n";
21
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)
26 my $tick;
27
28 $tick = 0;
29 for($tracks->[0]->events())
30 {   
31     $tick += $_->[1];
32     if($_->[0] eq 'set_tempo')
33     {   
34         push @tempi, [$tick, $_->[2] * 0.000001 / $ticksperquarter];
35     }
36 }
37 sub tick2sec($)
38 {
39     my ($tick) = @_;
40     my $sec = 0;
41     my $curtempo = [0, 0.5 / $ticksperquarter];
42     for(@tempi)
43     {
44         if($_->[0] < $tick)
45         {
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];
49         }   
50         else
51         {
52                         # if this event is in the future, we break
53                         last;
54         }
55                 $curtempo = $_;
56     }
57         $sec += ($tick - $curtempo->[0]) * $curtempo->[1];
58         return $sec + $offset;
59 }
60
61 my @notes = ('c', 'c#', 'd', 'd#', 'e', 'f', 'f#', 'g', 'g#', 'a', 'a#', 'b');
62 my @notenames = ();
63 for my $octave (0..11)
64 {
65         for(@notes)
66         {
67                 if($octave <= 3)
68                 {
69                         push @notenames, uc($_) . ',' x (3 - $octave);
70                 }
71                 else
72                 {
73                         push @notenames, lc($_) . "'" x ($octave - 4);
74                 }
75         }
76 }
77
78 # merge all to a single track
79 my @allmidievents = ();
80 my $sequence = 0;
81 for my $track(0..@$tracks-1)
82 {
83         $tick = 0;
84         for($tracks->[$track]->events())
85         {
86                 my ($command, $delta, @data) = @$_;
87                 $tick += $delta;
88                 push @allmidievents, [$command, $tick, $sequence++, $track, @data];
89         }
90 }
91 @allmidievents = sort { $a->[1] <=> $b->[1] or $a->[2] <=> $b->[2] } @allmidievents;
92
93 my @outevents = (); # format: name, time in seconds, velocity
94 $tick = 0;
95
96 my %notecounters;
97 my %notecounters_converted;
98 for(@allmidievents)
99 {
100         my $t = tick2sec $_->[1];
101         my $track = $_->[3];
102         next
103                 unless $trackno < 0 || $trackno == $track;
104         if($_->[0] eq 'note_on')
105         {
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;
114         }
115         elsif($_->[0] eq 'note_off')
116         {
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)
124                 {
125                         push @outevents, [$note, $t, 0]
126                                 if($channelno < 0 || $channelno == $chan);
127                 }
128         }
129 }
130 for(sort { $a->[0] cmp $b->[0] or $a->[1] <=> $b->[1] } @outevents)
131 {
132     printf "%s %13.6f %13.6f\n", @$_;
133 }