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