midi2cfg: use barriers
[divverent/nexuiz.git] / misc / tools / midi2cfg-ng.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 use Storable;
12
13 use constant MIDI_FIRST_NONCHANNEL => 17;
14 use constant MIDI_DRUMS_CHANNEL => 10;
15
16 die "Usage: $0 filename.mid transpose timeoffset timeoffset2 timeoffset3 timeoffset4 preallocatedbots..."
17         unless @ARGV >= 6;
18 my ($filename, $transpose, $timeoffset, $timeoffset2, $timeoffset3, $timeoffset4, @preallocate) = @ARGV;
19
20 my $opus = MIDI::Opus->new({from_file => $filename});
21 #$opus->write_to_file("/tmp/y.mid");
22 my $ticksperquarter = $opus->ticks();
23 my $tracks = $opus->tracks_r();
24 my @tempi = (); # list of start tick, time per tick pairs (calculated as seconds per quarter / ticks per quarter)
25 my $tick;
26
27 $tick = 0;
28 for($tracks->[0]->events())
29 {   
30     $tick += $_->[1];
31     if($_->[0] eq 'set_tempo')
32     {   
33         push @tempi, [$tick, $_->[2] * 0.000001 / $ticksperquarter];
34     }
35 }
36 sub tick2sec($)
37 {
38     my ($tick) = @_;
39     my $sec = 0;
40     my $curtempo = [0, 0.5 / $ticksperquarter];
41     for(@tempi)
42     {
43         if($_->[0] < $tick)
44         {
45                         # this event is in the past
46                         # we add the full time since the last one then
47                         $sec += ($_->[0] - $curtempo->[0]) * $curtempo->[1];
48         }   
49         else
50         {
51                         # if this event is in the future, we break
52                         last;
53         }
54                 $curtempo = $_;
55     }
56         $sec += ($tick - $curtempo->[0]) * $curtempo->[1];
57         return $sec;
58 }
59
60 # merge all to a single track
61 my @allmidievents = ();
62 my $sequence = 0;
63 for my $track(0..@$tracks-1)
64 {
65         $tick = 0;
66         for($tracks->[$track]->events())
67         {
68                 my ($command, $delta, @data) = @$_;
69                 $tick += $delta;
70                 push @allmidievents, [$command, $tick, $sequence++, $track, @data];
71         }
72 }
73 @allmidievents = sort { $a->[1] <=> $b->[1] or $a->[2] <=> $b->[2] } @allmidievents;
74
75
76 sub unsort(@)
77 {
78         return map { $_->[0] } sort { $a->[1] <=> $b->[1] } map { [$_, rand] } @_;
79 }
80
81
82
83
84
85 my $notetime = undef;
86 sub botconfig_read($)
87 {
88         my ($fn) = @_;
89         my %bots = ();
90         open my $fh, "<", $fn
91                 or die "<$fn: $!";
92         
93         my $currentbot = undef;
94         my $appendref = undef;
95         my $super = undef;
96         while(<$fh>)
97         {
98                 chomp;
99                 s/\s*#.*//;
100                 next if /^$/;
101                 if(s/^\t\t//)
102                 {
103                         my @cmd = split /\s+/, $_;
104                         if($cmd[0] eq 'super')
105                         {
106                                 push @$appendref, @$super
107                                         if $super;
108                         }
109                         elsif($cmd[0] eq 'percussion') # simple import
110                         {
111                                 push @$appendref, @{$currentbot->{percussion}->{$cmd[1]}};
112                         }
113                         else
114                         {
115                                 push @$appendref, \@cmd;
116                         }
117                 }
118                 elsif(s/^\t//)
119                 {
120                         if(/^include (.*)/)
121                         {
122                                 my $base = $bots{$1};
123                                 for(keys %$base)
124                                 {
125                                         if(ref $base->{$_})
126                                         {
127                                                 $currentbot->{$_} = Storable::dclone $base->{$_}; # copy array items as new array
128                                         }
129                                         else
130                                         {
131                                                 $currentbot->{$_} = $base->{$_};
132                                         }
133                                 }
134                                 # better: do some merging TODO
135                         }
136                         elsif(/^count (\d+)/)
137                         {
138                                 $currentbot->{count} = $1;
139                         }
140                         elsif(/^transpose (\d+)/)
141                         {
142                                 $currentbot->{transpose} += $1;
143                         }
144                         elsif(/^channels (.*)/)
145                         {
146                                 $currentbot->{channels} = { map { $_ => 1 } split /\s+/, $1 };
147                         }
148                         elsif(/^init$/)
149                         {
150                                 $super = $currentbot->{init};
151                                 $currentbot->{init} = $appendref = [];
152                         }
153                         elsif(/^done$/)
154                         {
155                                 $super = $currentbot->{done};
156                                 $currentbot->{done} = $appendref = [];
157                         }
158                         elsif(/^note on (-?\d+)/)
159                         {
160                                 $super = $currentbot->{notes_on}->{$1};
161                                 $currentbot->{notes_on}->{$1} = $appendref = [];
162                         }
163                         elsif(/^note off (-?\d+)/)
164                         {
165                                 $super = $currentbot->{notes_off}->{$1};
166                                 $currentbot->{notes_off}->{$1} = $appendref = [];
167                         }
168                         elsif(/^percussion (\d+)/)
169                         {
170                                 $super = $currentbot->{percussion}->{$1};
171                                 $currentbot->{percussion}->{$1} = $appendref = [];
172                         }
173                         else
174                         {
175                                 print "unknown command: $_\n";
176                         }
177                 }
178                 elsif(/^bot (.*)/)
179                 {
180                         $currentbot = ($bots{$1} ||= {count => 0, transpose => 0});
181                 }
182                 else
183                 {
184                         print "unknown command: $_\n";
185                 }
186         }
187
188         my $lowestnotestart = undef;
189         for(values %bots)
190         {
191                 for(values %{$_->{notes_on}}, values %{$_->{percussion}})
192                 {
193                         my $t = $_->[0]->[0] eq 'time' ? $_->[0]->[1] : 0;
194                         $lowestnotestart = $t if not defined $lowestnotestart or $t < $lowestnotestart;
195                 }
196         }
197
198         $notetime = $timeoffset2 - $lowestnotestart;
199
200         return \%bots;
201 }
202
203 sub busybot_cmd_bot_test($$@)
204 {
205         my ($bot, $time, @commands) = @_;
206
207         my $bottime = defined $bot->{timer} ? $bot->{timer} : -1;
208         my $botbusytime = defined $bot->{busytimer} ? $bot->{busytimer} : -1;
209
210         return 0
211                 if $time < $botbusytime;
212         
213         my $mintime = (@commands && ($commands[0]->[0] eq 'time')) ? $commands[0]->[1] : 0;
214
215         return 0
216                 if $time + $mintime < $bottime;
217         
218         return 1;
219 }
220
221 sub busybot_cmd_bot_execute($$@)
222 {
223         my ($bot, $time, @commands) = @_;
224
225         for(@commands)
226         {
227                 if($_->[0] eq 'time')
228                 {
229                         printf "w %d %f\n", $bot->{id}, $time + $_->[1];
230                         $bot->{timer} = $time + $_->[1];
231                 }
232                 elsif($_->[0] eq 'busy')
233                 {
234                         $bot->{busytimer} = $time + $_->[1];
235                 }
236                 elsif($_->[0] eq 'buttons')
237                 {
238                         my %buttons_release = %{$bot->{buttons} ||= {}};
239                         for(@{$_}[1..@$_-1])
240                         {
241                                 /(.*)\??/ or next;
242                                 delete $buttons_release{$1};
243                         }
244                         for(keys %buttons_release)
245                         {
246                                 printf "r %d %s\n", $bot->{id}, $_;
247                                 delete $bot->{buttons}->{$_};
248                         }
249                         for(@{$_}[1..@$_-1])
250                         {
251                                 /(.*)(\?)?/ or next;
252                                 defined $2 and next;
253                                 printf "p %d %s\n", $bot->{id}, $_;
254                                 $bot->{buttons}->{$_} = 1;
255                         }
256                 }
257                 elsif($_->[0] eq 'cmd')
258                 {
259                         printf "sv_cmd bot_cmd %d %s\n", $bot->{id}, join " ", @{$_}[1..@$_-1];
260                 }
261                 elsif($_->[0] eq 'raw')
262                 {
263                         printf "%s\n", join " ", @{$_}[1..@$_-1];
264                 }
265         }
266
267         return 1;
268 }
269
270 sub busybot_note_off_bot($$$$)
271 {
272         my ($bot, $time, $channel, $note) = @_;
273         my $cmds = $bot->{notes_off}->{$note - $bot->{transpose} - $transpose};
274         return 1
275                 if not defined $cmds; # note off cannot fail
276         $bot->{busy} = 0;
277         busybot_cmd_bot_execute $bot, $time + $notetime, @$cmds; 
278         return 1;
279 }
280
281 sub busybot_note_on_bot($$$$$)
282 {
283         my ($bot, $time, $channel, $note, $init) = @_;
284         return -1 # I won't play on this channel
285                 if defined $bot->{channels} and not $bot->{channels}->{$channel};
286         return 0
287                 if $bot->{busy};
288         my $cmds;
289         if($channel == 10)
290         {
291                 $cmds = $bot->{percussion}->{$note};
292         }
293         else
294         {
295                 $cmds = $bot->{notes_on}->{$note - $bot->{transpose} - $transpose};
296                 my $cmds_off = $bot->{notes_off}->{$note - $bot->{transpose} - $transpose};
297                 if(defined $cmds and defined $cmds_off)
298                 {
299                         $bot->{busy} = 1;
300                 }
301         }
302         return -1 # I won't play this note
303                 if not defined $cmds;
304         if($init)
305         {
306                 return 0
307                         if not busybot_cmd_bot_test $bot, $time + $notetime, @$cmds; 
308                 busybot_cmd_bot_execute $bot, 0, ['wait', $timeoffset];
309                 busybot_cmd_bot_execute $bot, 0, ['cmd', 'barrier'];
310                 busybot_cmd_bot_execute $bot, 0, @{$bot->{init}}
311                         if @{$bot->{init}};
312                 busybot_cmd_bot_execute $bot, 0, ['cmd', 'barrier'];
313                 $bot->{timer} = $bot->{busytimer} = 0;
314                 busybot_cmd_bot_execute $bot, $time + $notetime, @$cmds; 
315         }
316         else
317         {
318                 return 0
319                         if not busybot_cmd_bot_test $bot, $time + $notetime, @$cmds; 
320                 busybot_cmd_bot_execute $bot, $time + $notetime, @$cmds; 
321         }
322         return 1;
323 }
324
325 my $busybots = botconfig_read "midi2cfg-ng.conf";
326 my @busybots_allocated;
327 my %notechannelbots;
328
329 sub busybot_note_off($$$)
330 {
331         my ($time, $channel, $note) = @_;
332
333         return 0
334                 if $channel == 10;
335
336         if(my $bot = $notechannelbots{$channel}{$note})
337         {
338                 busybot_note_off_bot $bot, $time, $channel, $note;
339                 delete $notechannelbots{$channel}{$note};
340                 return 1;
341         }
342
343         return 0;
344 }
345
346 sub busybot_note_on($$$)
347 {
348         my ($time, $channel, $note) = @_;
349
350         if($notechannelbots{$channel}{$note})
351         {
352                 busybot_note_off $time, $channel, $note;
353         }
354
355         my $overflow = 0;
356
357         for(unsort @busybots_allocated)
358         {
359                 my $canplay = busybot_note_on_bot $_, $time, $channel, $note, 0;
360                 if($canplay > 0)
361                 {
362                         $notechannelbots{$channel}{$note} = $_;
363                         return 1;
364                 }
365                 $overflow = 1
366                         if $canplay == 0;
367                 # wrong
368         }
369
370         for(unsort keys %$busybots)
371         {
372                 next if $busybots->{$_}->{count} <= 0;
373                 my $bot = Storable::dclone $busybots->{$_};
374                 $bot->{id} = @busybots_allocated + 1;
375                 $bot->{classname} = $_;
376                 my $canplay = busybot_note_on_bot $bot, $time, $channel, $note, 1;
377                 if($canplay > 0)
378                 {
379                         --$busybots->{$_}->{count};
380                         $notechannelbots{$channel}{$note} = $bot;
381                         push @busybots_allocated, $bot;
382                         return 1;
383                 }
384                 $overflow = 1
385                         if $canplay == 0;
386         }
387
388         if($overflow)
389         {
390                 warn "Not enough bots to play this (when playing $channel:$note)";
391         }
392         else
393         {
394                 warn "Note $channel:$note cannot be played by any bot"
395         }
396
397         return 0;
398 }
399
400 print 'alias p "sv_cmd bot_cmd $1 presskey $2"' . "\n";
401 print 'alias r "sv_cmd bot_cmd $1 releasekey $2"' . "\n";
402 print 'alias w "sv_cmd bot_cmd $1 wait_until $2"' . "\n";
403 print 'alias m "sv_cmd bot_cmd $1 moveto \"$2 $3 $4\""' . "\n";
404
405 for(@preallocate)
406 {
407         die "Cannot preallocate any more $_ bots"
408                 if $busybots->{$_}->{count} <= 0;
409         my $bot = Storable::dclone $busybots->{$_};
410         $bot->{id} = @busybots_allocated + 1;
411         $bot->{classname} = $_;
412         busybot_cmd_bot_execute $bot, 0, ['wait', $timeoffset];
413         busybot_cmd_bot_execute $bot, 0, ['cmd', 'barrier'];
414         busybot_cmd_bot_execute $bot, 0, @{$bot->{init}}
415                 if @{$bot->{init}};
416         busybot_cmd_bot_execute $bot, 0, ['cmd', 'barrier'];
417         $bot->{timer} = $bot->{busytimer} = 0;
418         --$busybots->{$_}->{count};
419         push @busybots_allocated, $bot;
420 }
421
422 my %midinotes = ();
423 my $note_min = undef;
424 my $note_max = undef;
425 my $notes_stuck = 0;
426 my $t = 0;
427 for(@allmidievents)
428 {
429         $t = tick2sec $_->[1];
430         my $track = $_->[3];
431         if($_->[0] eq 'note_on')
432         {
433                 my $chan = $_->[4] + 1;
434                 $note_min = $_->[5]
435                         if not defined $note_min or $_->[5] < $note_min and $chan != 10;
436                 $note_max = $_->[5]
437                         if not defined $note_max or $_->[5] > $note_max and $chan != 10;
438                 if($midinotes{$chan}{$_->[5]})
439                 {
440                         --$notes_stuck;
441                         busybot_note_off($t, $chan, $_->[5]);
442                 }
443                 busybot_note_on($t, $chan, $_->[5]);
444                 ++$notes_stuck;
445                 $midinotes{$chan}{$_->[5]} = 1;
446         }
447         elsif($_->[0] eq 'note_off')
448         {
449                 my $chan = $_->[4] + 1;
450                 if($midinotes{$chan}{$_->[5]})
451                 {
452                         --$notes_stuck;
453                         busybot_note_off($t, $chan, $_->[5]);
454                 }
455                 $midinotes{$chan}{$_->[5]} = 0;
456         }
457 }
458
459 for(@busybots_allocated)
460 {
461         busybot_cmd_bot_execute $_, 0, ['wait', $timeoffset3];
462         busybot_cmd_bot_execute $_, 0, ['cmd', 'barrier'];
463         if($_->{done})
464         {
465                 busybot_cmd_bot_execute $_, @{$_->{done}};
466         }
467         busybot_cmd_bot_execute $_, 0, ['cmd', 'barrier'];
468         busybot_cmd_bot_execute $_, 0, ['wait', $timeoffset4];
469 }
470
471 print STDERR "Range of notes: $note_min .. $note_max\n";
472 print STDERR "Safe transpose range: @{[$note_max - 19]} .. @{[$note_min + 13]}\n";
473 print STDERR "Unsafe transpose range: @{[$note_max - 27]} .. @{[$note_min + 18]}\n";
474 print STDERR "Stuck notes: $notes_stuck\n";
475 print STDERR "Bots allocated:\n";
476 for(@busybots_allocated)
477 {
478         print STDERR "$_->{id} is a $_->{classname}\n";
479 }