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