Add source for h_grabber
[voretournament/voretournament.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, [$tick, $_->[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         use Data::Dumper;
119         print STDERR Dumper $l;
120         die "No free channel found at time $time ($notes notes active)\n";
121 }
122
123 sub busybot_find($$)
124 {
125         my ($vchannel, $note) = @_;
126         my $l = ($vchannel < MIDI_FIRST_NONCHANNEL) ? \@busybots_tuba : \@busybots_percussion;
127         for(0..@$l-1)
128         {
129                 return $l->[$_] if
130                         $l->[$_]
131                         &&
132                         $l->[$_]{busy}
133                         &&
134                         $l->[$_]{channel} == $vchannel
135                         &&
136                         defined $l->[$_]{note}
137                         &&
138                         $l->[$_]{note} == $note;
139         }
140         return undef;
141 }
142
143 sub busybot_advance($$)
144 {
145         my ($bot, $t) = @_;
146         my $t0 = $bot->{curtime};
147         if($t != $t0)
148         {
149                 #print "sv_cmd bot_cmd $bot->{id} wait @{[$t - $t0]}\n";
150                 print "w $bot->{id} $t\n";
151         }
152         $bot->{curtime} = $t;
153 }
154
155 sub busybot_setbuttonsandadvance($$$)
156 {
157         my ($bot, $t, $b) = @_;
158         my $b0 = $bot->{curbuttons};
159         my $press = $b & ~$b0;
160         my $release = $b0 & ~$b;
161         busybot_advance $bot => $t - 0.10
162                 if $release & (32 | 64);
163         print "r $bot->{id} attack1\n" if $release & 32;
164         print "r $bot->{id} attack2\n" if $release & 64;
165         busybot_advance $bot => $t - 0.05
166                 if ($release | $press) & (1 | 2 | 4 | 8 | 16 | 128);
167         print "r $bot->{id} forward\n" if $release & 1;
168         print "r $bot->{id} backward\n" if $release & 2;
169         print "r $bot->{id} left\n" if $release & 4;
170         print "r $bot->{id} right\n" if $release & 8;
171         print "r $bot->{id} crouch\n" if $release & 16;
172         print "r $bot->{id} jump\n" if $release & 128;
173         print "p $bot->{id} forward\n" if $press & 1;
174         print "p $bot->{id} backward\n" if $press & 2;
175         print "p $bot->{id} left\n" if $press & 4;
176         print "p $bot->{id} right\n" if $press & 8;
177         print "p $bot->{id} crouch\n" if $press & 16;
178         print "p $bot->{id} jump\n" if $press & 128;
179         busybot_advance $bot => $t
180                 if $press & (32 | 64);
181         print "p $bot->{id} attack1\n" if $press & 32;
182         print "p $bot->{id} attack2\n" if $press & 64;
183         $bot->{curbuttons} = $b;
184 }
185
186 my %notes = (
187         -18 => '1lbc',
188         -17 => '1bc',
189         -16 => '1brc',
190         -13 => '1frc',
191         -12 => '1c',
192         -11 => '2lbc',
193         -10 => '1rc',
194         -9 => '1flc',
195         -8 => '1fc',
196         -7 => '1lc',
197         -6 => '1lb',
198         -5 => '1b',
199         -4 => '1br',
200         -3 => '2rc',
201         -2 => '2flc',
202         -1 => '1fl',
203         0 => '1',
204         1 => '2lb',
205         2 => '1r',
206         3 => '1fl',
207         4 => '1f',
208         5 => '1l',
209         6 => '2fr',
210         7 => '2',
211         8 => '1brj',
212         9 => '2r',
213         10 => '2fl',
214         11 => '2f',
215         12 => '2l',
216         13 => '2lbj',
217         14 => '1rj',
218         15 => '1flj',
219         16 => '1fj',
220         17 => '1lj',
221         18 => '2frj',
222         19 => '2j',
223         21 => '2rj',
224         22 => '2flj',
225         23 => '2fj',
226         24 => '2lj'
227 );
228
229 my $note_min = +99;
230 my $note_max = -99;
231 sub getnote($$)
232 {
233         my ($bot, $note) = @_;
234         $note_max = $note if $note_max < $note;
235         $note_min = $note if $note_min > $note;
236         $note -= $transpose;
237         $note -= $bot->{noteoffset};
238         my $s = $notes{$note};
239         return $s;
240 }
241
242 sub busybot_playnoteandadvance($$$)
243 {
244         my ($bot, $t, $note) = @_;
245         my $s = getnote $bot => $note;
246         return (warn("note $note not found"), 0)
247                 unless defined $s;
248         my $buttons = 0;
249         $buttons |= 1 if $s =~ /f/;
250         $buttons |= 2 if $s =~ /b/;
251         $buttons |= 4 if $s =~ /l/;
252         $buttons |= 8 if $s =~ /r/;
253         $buttons |= 16 if $s =~ /c/;
254         $buttons |= 32 if $s =~ /1/;
255         $buttons |= 64 if $s =~ /2/;
256         $buttons |= 128 if $s =~ /j/;
257         busybot_setbuttonsandadvance $bot => $t, $buttons;
258         return 1;
259 }
260
261 sub busybot_stopnoteandadvance($$$)
262 {
263         my ($bot, $t, $note) = @_;
264         my $s = getnote $bot => $note;
265         return 0
266                 unless defined $s;
267         my $buttons = $bot->{curbuttons};
268         #$buttons &= ~(32 | 64);
269         $buttons = 0;
270         busybot_setbuttonsandadvance $bot => $t, $buttons;
271         return 1;
272 }
273
274 sub note_on($$$)
275 {
276         my ($t, $channel, $note) = @_;
277         ++$notes;
278         if($channel == MIDI_DRUMS_CHANNEL)
279         {
280                 $channel = MIDI_FIRST_NONCHANNEL + $note; # percussion
281                 return if !@coords_percussion;
282         }
283         my $bot = busybot_findfree($t, $channel, $note);
284         if($channel < MIDI_FIRST_NONCHANNEL)
285         {
286                 if(busybot_playnoteandadvance $bot => $t, $note)
287                 {
288                         $bot->{busy} = 1;
289                         $bot->{note} = $note;
290                         $bot->{busytime} = $t + 0.25;
291                         if($staccato)
292                         {
293                                 busybot_stopnoteandadvance $bot => $t + 0.15, $note;
294                                 $bot->{busy} = 0;
295                         }
296                 }
297         }
298         if($channel >= MIDI_FIRST_NONCHANNEL)
299         {
300                 busybot_advance $bot => $t;
301                 print "p $bot->{id} attack1\n";
302                 print "r $bot->{id} attack1\n";
303                 $bot->{busy} = 1;
304                 $bot->{note} = $note;
305                 $bot->{busytime} = $t + 1.5;
306         }
307 }
308
309 sub note_off($$$)
310 {
311         my ($t, $channel, $note) = @_;
312         --$notes;
313         if($channel == MIDI_DRUMS_CHANNEL)
314         {
315                 $channel = MIDI_FIRST_NONCHANNEL + $note; # percussion
316         }
317         my $bot = busybot_find($channel, $note)
318                 or return;
319         $bot->{busy} = 0;
320         if($channel < MIDI_FIRST_NONCHANNEL)
321         {
322                 busybot_stopnoteandadvance $bot => $t, $note;
323                 $bot->{busytime} = $t + 0.25;
324         }
325 }
326
327 print 'alias p "sv_cmd bot_cmd $1 presskey $2"' . "\n";
328 print 'alias r "sv_cmd bot_cmd $1 releasekey $2"' . "\n";
329 print 'alias w "sv_cmd bot_cmd $1 wait_until $2"' . "\n";
330 print 'alias m "sv_cmd bot_cmd $1 moveto \"$2 $3 $4\""' . "\n";
331
332 my %midinotes = ();
333 for(@allmidievents)
334 {
335         my $t = tick2sec $_->[1];
336         my $track = $_->[3];
337         if($_->[0] eq 'note_on')
338         {
339                 my $chan = $_->[4] + 1;
340                 if($midinotes{$chan}{$_->[5]})
341                 {
342                         note_off($t, $chan, $_->[5]);
343                 }
344                 note_on($t, $chan, $_->[5]);
345                 $midinotes{$chan}{$_->[5]} = 1;
346         }
347         elsif($_->[0] eq 'note_off')
348         {
349                 my $chan = $_->[4] + 1;
350                 if($midinotes{$chan}{$_->[5]})
351                 {
352                         note_off($t, $chan, $_->[5]);
353                 }
354                 $midinotes{$chan}{$_->[5]} = 0;
355         }
356 }
357
358 print STDERR "Range of notes: $note_min .. $note_max\n";
359 print STDERR "Safe transpose range: @{[$note_max - 19]} .. @{[$note_min + 13]}\n";
360 print STDERR "Unsafe transpose range: @{[$note_max - 24]} .. @{[$note_min + 18]}\n";
361 printf STDERR "%d bots allocated for tuba, %d for percussion\n", int scalar grep { defined $_ } @busybots_tuba, int scalar grep { defined $_ } @busybots_percussion;
362
363 my $n = 0;
364 for(@busybots_percussion, @busybots_tuba)
365 {
366         ++$n if $_ && $_->{busy};
367 }
368 if($n)
369 {
370         die "$n channels blocked ($notes MIDI notes)";
371 }