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)
11 use constant MIDI_FIRST_NONCHANNEL => 17;
12 use constant MIDI_DRUMS_CHANNEL => 10;
14 die "Usage: $0 filename.conf timeoffset_preinit timeoffset_postinit timeoffset_predone timeoffset_postdone timeoffset_preintermission timeoffset_postintermission midifile1 transpose1 midifile2 transpose2 ..."
15 unless @ARGV > 7 and @ARGV % 2;
16 my ($config, $timeoffset_preinit, $timeoffset_postinit, $timeoffset_predone, $timeoffset_postdone, $timeoffset_preintermission, $timeoffset_postintermission, @midilist) = @ARGV;
20 return map { $_->[0] } sort { $a->[1] <=> $b->[1] } map { [$_, rand] } @_;
26 my @busybots_allocated;
30 my $lowestnotestart = undef;
39 my $currentbot = undef;
40 my $appendref = undef;
49 my @cmd = split /\s+/, $_;
50 if($cmd[0] eq 'super')
52 push @$appendref, @$super
55 elsif($cmd[0] eq 'percussion') # simple import
57 push @$appendref, @{$currentbot->{percussion}->{$cmd[1]}};
61 push @$appendref, \@cmd;
73 $currentbot->{$_} = Storable::dclone $base->{$_}; # copy array items as new array
77 $currentbot->{$_} = $base->{$_};
80 # better: do some merging TODO
84 $currentbot->{count} = $1;
86 elsif(/^transpose (\d+)/)
88 $currentbot->{transpose} += $1;
90 elsif(/^channels (.*)/)
92 $currentbot->{channels} = { map { $_ => 1 } split /\s+/, $1 };
96 $super = $currentbot->{init};
97 $currentbot->{init} = $appendref = [];
99 elsif(/^intermission$/)
101 $super = $currentbot->{intermission};
102 $currentbot->{intermission} = $appendref = [];
106 $super = $currentbot->{done};
107 $currentbot->{done} = $appendref = [];
109 elsif(/^note on (-?\d+)/)
111 $super = $currentbot->{notes_on}->{$1};
112 $currentbot->{notes_on}->{$1} = $appendref = [];
114 elsif(/^note off (-?\d+)/)
116 $super = $currentbot->{notes_off}->{$1};
117 $currentbot->{notes_off}->{$1} = $appendref = [];
119 elsif(/^percussion (\d+)/)
121 $super = $currentbot->{percussion}->{$1};
122 $currentbot->{percussion}->{$1} = $appendref = [];
126 print "unknown command: $_\n";
131 $currentbot = ($bots{$1} ||= {count => 0, transpose => 0});
135 $precommands .= "$1\n";
139 print "unknown command: $_\n";
145 for(values %{$_->{notes_on}}, values %{$_->{percussion}})
147 my $t = $_->[0]->[0] eq 'time' ? $_->[0]->[1] : 0;
148 $lowestnotestart = $t if not defined $lowestnotestart or $t < $lowestnotestart;
154 my $busybots_orig = botconfig_read $config;
157 sub busybot_cmd_bot_test($$@)
159 my ($bot, $time, @commands) = @_;
161 my $bottime = defined $bot->{timer} ? $bot->{timer} : -1;
162 my $botbusytime = defined $bot->{busytimer} ? $bot->{busytimer} : -1;
165 if $time < $botbusytime;
167 my $mintime = (@commands && ($commands[0]->[0] eq 'time')) ? $commands[0]->[1] : 0;
170 if $time + $mintime < $bottime;
175 sub busybot_cmd_bot_execute($$@)
177 my ($bot, $time, @commands) = @_;
181 if($_->[0] eq 'time')
183 $commands .= sprintf "sv_cmd bot_cmd %d wait_until %f\n", $bot->{id}, $time + $_->[1];
184 $bot->{timer} = $time + $_->[1];
186 elsif($_->[0] eq 'busy')
188 $bot->{busytimer} = $time + $_->[1];
190 elsif($_->[0] eq 'buttons')
192 my %buttons_release = %{$bot->{buttons} ||= {}};
196 delete $buttons_release{$1};
198 for(keys %buttons_release)
200 $commands .= sprintf "sv_cmd bot_cmd %d releasekey %s\n", $bot->{id}, $_;
201 delete $bot->{buttons}->{$_};
207 $commands .= sprintf "sv_cmd bot_cmd %d presskey %s\n", $bot->{id}, $_;
208 $bot->{buttons}->{$_} = 1;
211 elsif($_->[0] eq 'cmd')
213 $commands .= sprintf "sv_cmd bot_cmd %d %s\n", $bot->{id}, join " ", @{$_}[1..@$_-1];
215 elsif($_->[0] eq 'barrier')
217 $commands .= sprintf "sv_cmd bot_cmd %d barrier\n", $bot->{id};
218 $bot->{timer} = $bot->{busytimer} = 0;
220 elsif($_->[0] eq 'raw')
222 $commands .= sprintf "%s\n", join " ", @{$_}[1..@$_-1];
229 my $intermissions = 0;
231 sub busybot_intermission_bot($)
234 busybot_cmd_bot_execute $bot, 0, ['cmd', 'wait', $timeoffset_preintermission];
235 busybot_cmd_bot_execute $bot, 0, ['barrier'];
236 if($bot->{intermission})
238 busybot_cmd_bot_execute $bot, 0, @{$bot->{intermission}};
240 busybot_cmd_bot_execute $bot, 0, ['barrier'];
241 $notetime = $timeoffset_postintermission - $lowestnotestart;
244 sub busybot_note_off_bot($$$$)
246 my ($bot, $time, $channel, $note) = @_;
249 my $cmds = $bot->{notes_off}->{$note - $bot->{transpose} - $transpose};
251 if not defined $cmds; # note off cannot fail
253 busybot_cmd_bot_execute $bot, $time + $notetime, @$cmds;
257 sub busybot_note_on_bot($$$$$)
259 my ($bot, $time, $channel, $note, $init) = @_;
260 return -1 # I won't play on this channel
261 if defined $bot->{channels} and not $bot->{channels}->{$channel};
267 $cmds = $bot->{percussion}->{$note};
271 $cmds = $bot->{notes_on}->{$note - $bot->{transpose} - $transpose};
272 my $cmds_off = $bot->{notes_off}->{$note - $bot->{transpose} - $transpose};
273 if(defined $cmds and defined $cmds_off)
278 return -1 # I won't play this note
279 if not defined $cmds;
283 if not busybot_cmd_bot_test $bot, $time + $notetime, @$cmds;
284 busybot_cmd_bot_execute $bot, 0, ['cmd', 'wait', $timeoffset_preinit];
285 busybot_cmd_bot_execute $bot, 0, ['barrier'];
286 busybot_cmd_bot_execute $bot, 0, @{$bot->{init}}
288 busybot_cmd_bot_execute $bot, 0, ['barrier'];
289 for(1..$intermissions)
291 busybot_intermission_bot $bot;
293 busybot_cmd_bot_execute $bot, $time + $notetime, @$cmds;
298 if not busybot_cmd_bot_test $bot, $time + $notetime, @$cmds;
299 busybot_cmd_bot_execute $bot, $time + $notetime, @$cmds;
306 $busybots = Storable::dclone $busybots_orig;
307 @busybots_allocated = ();
308 %notechannelbots = ();
310 $notetime = $timeoffset_postinit - $lowestnotestart;
313 sub busybot_note_off($$$)
315 my ($time, $channel, $note) = @_;
320 if(my $bot = $notechannelbots{$channel}{$note})
322 busybot_note_off_bot $bot, $time, $channel, $note;
323 delete $notechannelbots{$channel}{$note};
330 sub busybot_note_on($$$)
332 my ($time, $channel, $note) = @_;
334 if($notechannelbots{$channel}{$note})
336 busybot_note_off $time, $channel, $note;
341 for(unsort @busybots_allocated)
343 my $canplay = busybot_note_on_bot $_, $time, $channel, $note, 0;
346 $notechannelbots{$channel}{$note} = $_;
354 for(unsort keys %$busybots)
356 next if $busybots->{$_}->{count} <= 0;
357 my $bot = Storable::dclone $busybots->{$_};
358 $bot->{id} = @busybots_allocated + 1;
359 $bot->{classname} = $_;
360 my $canplay = busybot_note_on_bot $bot, $time, $channel, $note, 1;
365 --$busybots->{$_}->{count};
366 $notechannelbots{$channel}{$note} = $bot;
367 push @busybots_allocated, $bot;
376 warn "Not enough bots to play this (when playing $channel:$note)";
380 warn "Note $channel:$note cannot be played by any bot";
388 my (@preallocate) = @_;
392 die "Cannot preallocate any more $_ bots"
393 if $busybots->{$_}->{count} <= 0;
394 my $bot = Storable::dclone $busybots->{$_};
395 $bot->{id} = @busybots_allocated + 1;
396 $bot->{classname} = $_;
397 busybot_cmd_bot_execute $bot, 0, ['cmd', 'wait', $timeoffset_preinit];
398 busybot_cmd_bot_execute $bot, 0, ['barrier'];
399 busybot_cmd_bot_execute $bot, 0, @{$bot->{init}}
401 busybot_cmd_bot_execute $bot, 0, ['barrier'];
402 --$busybots->{$_}->{count};
403 push @busybots_allocated, $bot;
409 my ($filename, $trans) = @_;
412 my $opus = MIDI::Opus->new({from_file => $filename});
413 my $ticksperquarter = $opus->ticks();
414 my $tracks = $opus->tracks_r();
415 my @tempi = (); # list of start tick, time per tick pairs (calculated as seconds per quarter / ticks per quarter)
419 for($tracks->[0]->events())
422 if($_->[0] eq 'set_tempo')
424 push @tempi, [$tick, $_->[2] * 0.000001 / $ticksperquarter];
431 my $curtempo = [0, 0.5 / $ticksperquarter];
436 # this event is in the past
437 # we add the full time since the last one then
438 $sec += ($_->[0] - $curtempo->[0]) * $curtempo->[1];
442 # if this event is in the future, we break
447 $sec += ($tick - $curtempo->[0]) * $curtempo->[1];
451 # merge all to a single track
452 my @allmidievents = ();
454 for my $track(0..@$tracks-1)
457 for($tracks->[$track]->events())
459 my ($command, $delta, @data) = @$_;
460 $command = 'note_off' if $command eq 'note_on' and $data[2] == 0;
462 push @allmidievents, [$command, $tick, $sequence++, $track, @data];
465 @allmidievents = sort { $a->[1] <=> $b->[1] or $a->[2] <=> $b->[2] } @allmidievents;
468 my $note_min = undef;
469 my $note_max = undef;
474 $t = $tick2sec->($_->[1]);
476 if($_->[0] eq 'note_on')
478 my $chan = $_->[4] + 1;
480 if not defined $note_min or $_->[5] < $note_min and $chan != 10;
482 if not defined $note_max or $_->[5] > $note_max and $chan != 10;
483 if($midinotes{$chan}{$_->[5]})
486 busybot_note_off($t, $chan, $_->[5]);
488 busybot_note_on($t, $chan, $_->[5]);
490 $midinotes{$chan}{$_->[5]} = 1;
492 elsif($_->[0] eq 'note_off')
494 my $chan = $_->[4] + 1;
495 if($midinotes{$chan}{$_->[5]})
498 busybot_note_off($t, $chan, $_->[5]);
500 $midinotes{$chan}{$_->[5]} = 0;
504 print STDERR "For file $filename:\n";
505 print STDERR " Range of notes: $note_min .. $note_max\n";
506 print STDERR " Safe transpose range: @{[$note_max - 19]} .. @{[$note_min + 13]}\n";
507 print STDERR " Unsafe transpose range: @{[$note_max - 27]} .. @{[$note_min + 18]}\n";
508 print STDERR " Stuck notes: $notes_stuck\n";
510 while(my ($k1, $v1) = each %midinotes)
512 while(my ($k2, $v2) = each %$v1)
514 busybot_note_off($t, $k1, $k2);
518 for(@busybots_allocated)
520 busybot_intermission_bot $_;
527 print STDERR "Bots allocated:\n";
528 for(@busybots_allocated)
530 print STDERR "$_->{id} is a $_->{classname}\n";
532 for(@busybots_allocated)
534 busybot_cmd_bot_execute $_, 0, ['cmd', 'wait', $timeoffset_predone];
535 busybot_cmd_bot_execute $_, 0, ['barrier'];
538 busybot_cmd_bot_execute $_, 0, @{$_->{done}};
540 busybot_cmd_bot_execute $_, 0, ['cmd', 'wait', $timeoffset_postdone];
541 busybot_cmd_bot_execute $_, 0, ['barrier'];
545 my @preallocate = ();
552 Preallocate(@preallocate);
556 my $filename = shift @l;
557 my $transpose = shift @l;
558 ConvertMIDI($filename, $transpose);
561 my @preallocate_new = map { $_->{classname} } @busybots_allocated;
562 if(@preallocate_new == @preallocate)
564 print "$precommands$commands";
567 @preallocate = @preallocate_new;
572 unless $@ eq "noalloc\n";