midi: better channel handling
[divverent/nexuiz.git] / misc / tools / midi2cfg.pl
1 #!/usr/bin/perl
2
3 # converter from Type 1 MIDI files to CFG files that control bots with the Tuba and other weapons for percussion (requires g_weaponarena all)
4 # usage:
5 #   perl midi2cfg.pl filename.mid basenote walktime "x y z" "x y z" "x y z" ... "/" "x y z" "x y z" ... > filename.cfg
6
7 use strict;
8 use warnings;
9 use MIDI;
10 use MIDI::Opus;
11
12 use constant MIDI_FIRST_NONCHANNEL => 17;
13 use constant MIDI_DRUMS_CHANNEL => 10;
14
15 my ($filename, $transpose, $walktime, $staccato, @coords) = @ARGV;
16 my @coords_percussion = ();
17 my @coords_tuba = ();
18 my $l = \@coords_tuba;
19 for(@coords)
20 {
21         if($_ eq '/')
22         {
23                 $l = \@coords_percussion;
24         }
25         else
26         {
27                 push @$l, [split /\s+/, $_];
28         }
29 }
30
31 my $opus = MIDI::Opus->new({from_file => $filename});
32 #$opus->write_to_file("/tmp/y.mid");
33 my $ticksperquarter = $opus->ticks();
34 my $tracks = $opus->tracks_r();
35 my @tempi = (); # list of start tick, time per tick pairs (calculated as seconds per quarter / ticks per quarter)
36 my $tick;
37
38 $tick = 0;
39 for($tracks->[0]->events())
40 {   
41     $tick += $_->[1];
42     if($_->[0] eq 'set_tempo')
43     {   
44         push @tempi, [$_->[1], $_->[2] * 0.000001 / $ticksperquarter];
45     }
46 }
47 sub tick2sec($)
48 {
49     my ($tick) = @_;
50     my $sec = 0;
51     my $curtempo = [0, 0.5 / $ticksperquarter];
52     for(@tempi)
53     {
54         if($_->[0] < $tick)
55         {
56                         # this event is in the past
57                         # we add the full time since the last one then
58                         $sec += ($_->[0] - $curtempo->[0]) * $curtempo->[1];
59         }   
60         else
61         {
62                         # if this event is in the future, we break
63                         last;
64         }
65                 $curtempo = $_;
66     }
67         $sec += ($tick - $curtempo->[0]) * $curtempo->[1];
68         return $sec;
69 }
70
71 # merge all to a single track
72 my @allmidievents = ();
73 my $sequence = 0;
74 for my $track(0..@$tracks-1)
75 {
76         $tick = 0;
77         for($tracks->[$track]->events())
78         {
79                 my ($command, $delta, @data) = @$_;
80                 $tick += $delta;
81                 push @allmidievents, [$command, $tick, $sequence++, $track, @data];
82         }
83 }
84 @allmidievents = sort { $a->[1] <=> $b->[1] or $a->[2] <=> $b->[2] } @allmidievents;
85
86
87
88
89
90 my @busybots_percussion = map { undef } @coords_percussion;
91 my @busybots_tuba       = map { undef } @coords_tuba;
92
93 my $notes = 0;
94 sub busybot_findfree($$$)
95 {
96         my ($time, $vchannel, $note) = @_;
97         my $l = ($vchannel < MIDI_FIRST_NONCHANNEL) ? \@busybots_tuba : \@busybots_percussion;
98         my $c = ($vchannel < MIDI_FIRST_NONCHANNEL) ? \@coords_tuba : \@coords_percussion;
99         for(0..@$l-1)
100         {
101                 if(!$l->[$_])
102                 {
103                         my $bot = {id => $_ + 1, busy => 0, busytime => 0, channel => $vchannel, curtime => -$walktime, curbuttons => 0, noteoffset => 0};
104                         $l->[$_] = $bot;
105
106                         # let the bot walk to his place
107                         printf "m $_ $c->[$_]->[0] $c->[$_]->[1] $c->[$_]->[2]\n";
108
109                         return $bot;
110                 }
111                 return $l->[$_] if
112                         (($vchannel < MIDI_FIRST_NONCHANNEL) || ($l->[$_]{channel} == $vchannel))
113                         &&
114                         !$l->[$_]{busy}
115                         &&
116                         $time > $l->[$_]{busytime};
117         }
118         die "No free channel found ($notes notes active)\n";
119 }
120
121 sub busybot_find($$)
122 {
123         my ($vchannel, $note) = @_;
124         my $l = ($vchannel < MIDI_FIRST_NONCHANNEL) ? \@busybots_tuba : \@busybots_percussion;
125         for(0..@$l-1)
126         {
127                 return $l->[$_] if
128                         $l->[$_]
129                         &&
130                         $l->[$_]{busy}
131                         &&
132                         $l->[$_]{channel} == $vchannel
133                         &&
134                         defined $l->[$_]{note}
135                         &&
136                         $l->[$_]{note} == $note;
137         }
138         return undef;
139 }
140
141 sub busybot_advance($$)
142 {
143         my ($bot, $t) = @_;
144         my $t0 = $bot->{curtime};
145         if($t != $t0)
146         {
147                 #print "sv_cmd bot_cmd $bot->{id} wait @{[$t - $t0]}\n";
148                 print "w $bot->{id} $t\n";
149         }
150         $bot->{curtime} = $t;
151 }
152
153 sub busybot_setbuttonsandadvance($$$)
154 {
155         my ($bot, $t, $b) = @_;
156         my $b0 = $bot->{curbuttons};
157         my $press = $b & ~$b0;
158         my $release = $b0 & ~$b;
159         busybot_advance $bot => $t - 0.10
160                 if $release & (32 | 64);
161         print "r $bot->{id} attack1\n" if $release & 32;
162         print "r $bot->{id} attack2\n" if $release & 64;
163         busybot_advance $bot => $t - 0.05
164                 if ($release | $press) & (1 | 2 | 4 | 8 | 16 | 128);
165         print "r $bot->{id} forward\n" if $release & 1;
166         print "r $bot->{id} backward\n" if $release & 2;
167         print "r $bot->{id} left\n" if $release & 4;
168         print "r $bot->{id} right\n" if $release & 8;
169         print "r $bot->{id} crouch\n" if $release & 16;
170         print "r $bot->{id} jump\n" if $release & 128;
171         print "p $bot->{id} forward\n" if $press & 1;
172         print "p $bot->{id} backward\n" if $press & 2;
173         print "p $bot->{id} left\n" if $press & 4;
174         print "p $bot->{id} right\n" if $press & 8;
175         print "p $bot->{id} crouch\n" if $press & 16;
176         print "p $bot->{id} jump\n" if $press & 128;
177         busybot_advance $bot => $t
178                 if $press & (32 | 64);
179         print "p $bot->{id} attack1\n" if $press & 32;
180         print "p $bot->{id} attack2\n" if $press & 64;
181         $bot->{curbuttons} = $b;
182 }
183
184 my %notes = (
185         -18 => '1lbc',
186         -17 => '1bc',
187         -16 => '1brc',
188         -13 => '1frc',
189         -12 => '1c',
190         -11 => '2lbc',
191         -10 => '1rc',
192         -9 => '1flc',
193         -8 => '1fc',
194         -7 => '1lc',
195         -6 => '1lb',
196         -5 => '1b',
197         -4 => '1br',
198         -3 => '2rc',
199         -2 => '2flc',
200         -1 => '1fl',
201         0 => '1',
202         1 => '2lb',
203         2 => '1r',
204         3 => '1fl',
205         4 => '1f',
206         5 => '1l',
207         6 => '2fr',
208         7 => '2',
209         8 => '1brj',
210         9 => '2r',
211         10 => '2fl',
212         11 => '2f',
213         12 => '2l',
214         13 => '2lbj',
215         14 => '1rj',
216         15 => '1flj',
217         16 => '1fj',
218         17 => '1lj',
219         18 => '2frj',
220         19 => '2j',
221         21 => '2rj',
222         22 => '2flj',
223         23 => '2fj',
224         24 => '2lj'
225 );
226
227 my $note_min = +99;
228 my $note_max = -99;
229 sub getnote($$)
230 {
231         my ($bot, $note) = @_;
232         $note_max = $note if $note_max < $note;
233         $note_min = $note if $note_min > $note;
234         $note -= $transpose;
235         $note -= $bot->{noteoffset};
236         my $s = $notes{$note};
237         return $s;
238 }
239
240 sub busybot_playnoteandadvance($$$)
241 {
242         my ($bot, $t, $note) = @_;
243         my $s = getnote $bot => $note;
244         return (warn("note $note not found"), 0)
245                 unless defined $s;
246         my $buttons = 0;
247         $buttons |= 1 if $s =~ /f/;
248         $buttons |= 2 if $s =~ /b/;
249         $buttons |= 4 if $s =~ /l/;
250         $buttons |= 8 if $s =~ /r/;
251         $buttons |= 16 if $s =~ /c/;
252         $buttons |= 32 if $s =~ /1/;
253         $buttons |= 64 if $s =~ /2/;
254         $buttons |= 128 if $s =~ /j/;
255         busybot_setbuttonsandadvance $bot => $t, $buttons;
256         return 1;
257 }
258
259 sub busybot_stopnoteandadvance($$$)
260 {
261         my ($bot, $t, $note) = @_;
262         my $s = getnote $bot => $note;
263         return 0
264                 unless defined $s;
265         my $buttons = $bot->{curbuttons};
266         #$buttons &= ~(32 | 64);
267         $buttons = 0;
268         busybot_setbuttonsandadvance $bot => $t, $buttons;
269         return 1;
270 }
271
272 sub note_on($$$)
273 {
274         my ($t, $channel, $note) = @_;
275         ++$notes;
276         if($channel == MIDI_DRUMS_CHANNEL)
277         {
278                 $channel = MIDI_FIRST_NONCHANNEL + $note; # percussion
279                 return if !@coords_percussion;
280         }
281         my $bot = busybot_findfree($t, $channel, $note);
282         if($channel < MIDI_FIRST_NONCHANNEL)
283         {
284                 if(busybot_playnoteandadvance $bot => $t, $note)
285                 {
286                         $bot->{busy} = 1;
287                         $bot->{note} = $note;
288                         $bot->{busytime} = $t + 0.25;
289                         if($staccato)
290                         {
291                                 busybot_stopnoteandadvance $bot => $t + 0.15, $note;
292                                 $bot->{busy} = 0;
293                         }
294                 }
295         }
296         if($channel >= MIDI_FIRST_NONCHANNEL)
297         {
298                 busybot_advance $bot => $t;
299                 print "p $bot->{id} attack1\n";
300                 print "r $bot->{id} attack1\n";
301                 $bot->{busy} = 1;
302                 $bot->{note} = $note;
303                 $bot->{busytime} = $t + 1.5;
304         }
305 }
306
307 sub note_off($$$)
308 {
309         my ($t, $channel, $note) = @_;
310         --$notes;
311         if($channel == MIDI_DRUMS_CHANNEL)
312         {
313                 $channel = MIDI_FIRST_NONCHANNEL + $note; # percussion
314         }
315         my $bot = busybot_find($channel, $note)
316                 or return;
317         $bot->{busy} = 0;
318         if($channel < MIDI_FIRST_NONCHANNEL)
319         {
320                 busybot_stopnoteandadvance $bot => $t, $note;
321                 $bot->{busytime} = $t + 0.25;
322         }
323 }
324
325 print 'alias p "sv_cmd bot_cmd $1 presskey $2"' . "\n";
326 print 'alias r "sv_cmd bot_cmd $1 releasekey $2"' . "\n";
327 print 'alias w "sv_cmd bot_cmd $1 wait_until $2"' . "\n";
328 print 'alias m "sv_cmd bot_cmd $1 moveto \"$2 $3 $4\""' . "\n";
329
330 my %midinotes = ();
331 for(@allmidievents)
332 {
333         my $t = tick2sec $_->[1];
334         my $track = $_->[3];
335         if($_->[0] eq 'note_on')
336         {
337                 my $chan = $_->[4] + 1;
338                 if($midinotes{$chan}{$_->[5]})
339                 {
340                         note_off($t, $chan, $_->[5]);
341                 }
342                 note_on($t, $chan, $_->[5]);
343                 $midinotes{$chan}{$_->[5]} = 1;
344         }
345         elsif($_->[0] eq 'note_off')
346         {
347                 my $chan = $_->[4] + 1;
348                 if($midinotes{$chan}{$_->[5]})
349                 {
350                         note_off($t, $chan, $_->[5]);
351                 }
352                 $midinotes{$chan}{$_->[5]} = 0;
353         }
354 }
355
356 print STDERR "Range of notes: $note_min .. $note_max\n";
357 print STDERR "Safe transpose range: @{[$note_max - 19]} .. @{[$note_min + 13]}\n";
358 print STDERR "Unsafe transpose range: @{[$note_max - 24]} .. @{[$note_min + 18]}\n";
359 printf STDERR "%d bots allocated for tuba, %d for percussion\n", int scalar grep { defined $_ } @busybots_tuba, int scalar grep { defined $_ } @busybots_percussion;
360
361 my $n = 0;
362 for(@busybots_percussion, @busybots_tuba)
363 {
364         ++$n if $_ && $_->{busy};
365 }
366 if($n)
367 {
368         use Data::Dumper;
369         print STDERR Dumper \%midinotes;
370         die "$n channels blocked ($notes MIDI notes)";
371 }