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 SYS_TICRATE => 0.033333;
12 use constant MIDI_FIRST_NONCHANNEL => 17;
13 use constant MIDI_DRUMS_CHANNEL => 10;
15 die "Usage: $0 filename.conf timeoffset_preinit timeoffset_postinit timeoffset_predone timeoffset_postdone timeoffset_preintermission timeoffset_postintermission midifile1 transpose1 midifile2 transpose2 ..."
16 unless @ARGV > 7 and @ARGV % 2;
17 my ($config, $timeoffset_preinit, $timeoffset_postinit, $timeoffset_predone, $timeoffset_postdone, $timeoffset_preintermission, $timeoffset_postintermission, @midilist) = @ARGV;
21 return map { $_->[0] } sort { $a->[1] <=> $b->[1] } map { [$_, rand] } @_;
27 my ($dest, $src) = @_;
28 if(ref $src eq 'HASH')
34 $dest->{$_} = override $dest->{$_}, $src->{$_};
37 elsif(ref $src eq 'ARRAY')
43 push @$dest, override undef, $_;
48 $dest = Storable::dclone $src;
60 my @busybots_allocated;
64 my $lowestnotestart = undef;
73 my $currentbot = undef;
74 my $appendref = undef;
83 my @cmd = split /\s+/, $_;
84 if($cmd[0] eq 'super')
86 push @$appendref, @$super
89 elsif($cmd[0] eq 'percussion') # simple import
91 push @$appendref, @{$currentbot->{percussion}->{$cmd[1]}};
95 push @$appendref, \@cmd;
102 my $base = $bots{$1};
103 $currentbot = override $currentbot, $base;
105 elsif(/^count (\d+)/)
107 $currentbot->{count} = $1;
109 elsif(/^transpose (\d+)/)
111 $currentbot->{transpose} ||= 0;
112 $currentbot->{transpose} += $1;
114 elsif(/^channels (.*)/)
116 $currentbot->{channels} = { map { $_ => 1 } split /\s+/, $1 };
120 $super = $currentbot->{init};
121 $currentbot->{init} = $appendref = [];
123 elsif(/^intermission$/)
125 $super = $currentbot->{intermission};
126 $currentbot->{intermission} = $appendref = [];
130 $super = $currentbot->{done};
131 $currentbot->{done} = $appendref = [];
133 elsif(/^note on (-?\d+)/)
135 $super = $currentbot->{notes_on}->{$1};
136 $currentbot->{notes_on}->{$1} = $appendref = [];
138 elsif(/^note off (-?\d+)/)
140 $super = $currentbot->{notes_off}->{$1};
141 $currentbot->{notes_off}->{$1} = $appendref = [];
143 elsif(/^percussion (\d+)/)
145 $super = $currentbot->{percussion}->{$1};
146 $currentbot->{percussion}->{$1} = $appendref = [];
150 $super = $currentbot->{vocals};
151 $currentbot->{vocals} = $appendref = [];
155 print "unknown command: $_\n";
160 $currentbot = ($bots{$1} ||= {count => 0});
164 $precommands .= "$1\n";
168 print "unknown command: $_\n";
174 for(values %{$_->{notes_on}}, values %{$_->{percussion}})
176 my $t = $_->[0]->[0] eq 'time' ? $_->[0]->[1] : 0;
177 $lowestnotestart = $t if not defined $lowestnotestart or $t < $lowestnotestart;
183 my $busybots_orig = botconfig_read $config;
186 sub busybot_cmd_bot_test($$@)
188 my ($bot, $time, @commands) = @_;
190 my $bottime = defined $bot->{timer} ? $bot->{timer} : -1;
191 my $botbusytime = defined $bot->{busytimer} ? $bot->{busytimer} : -1;
194 if $time < $botbusytime + SYS_TICRATE;
196 my $mintime = (@commands && ($commands[0]->[0] eq 'time')) ? $commands[0]->[1] : 0;
199 if $time + $mintime < $bottime + SYS_TICRATE;
204 sub busybot_cmd_bot_execute($$@)
206 my ($bot, $time, @commands) = @_;
210 if($_->[0] eq 'time')
212 $commands .= sprintf "sv_cmd bot_cmd %d wait_until %f\n", $bot->{id}, $time + $_->[1];
213 $bot->{timer} = $time + $_->[1];
215 elsif($_->[0] eq 'busy')
217 $bot->{busytimer} = $time + $_->[1];
219 elsif($_->[0] eq 'buttons')
221 my %buttons_release = %{$bot->{buttons} ||= {}};
225 delete $buttons_release{$1};
227 for(keys %buttons_release)
229 $commands .= sprintf "sv_cmd bot_cmd %d releasekey %s\n", $bot->{id}, $_;
230 delete $bot->{buttons}->{$_};
236 $commands .= sprintf "sv_cmd bot_cmd %d presskey %s\n", $bot->{id}, $_;
237 $bot->{buttons}->{$_} = 1;
240 elsif($_->[0] eq 'cmd')
242 $commands .= sprintf "sv_cmd bot_cmd %d %s\n", $bot->{id}, join " ", @{$_}[1..@$_-1];
244 elsif($_->[0] eq 'barrier')
246 $commands .= sprintf "sv_cmd bot_cmd %d barrier\n", $bot->{id};
247 $bot->{timer} = $bot->{busytimer} = 0;
249 elsif($_->[0] eq 'raw')
251 $commands .= sprintf "%s\n", join " ", @{$_}[1..@$_-1];
258 my $intermissions = 0;
260 sub busybot_intermission_bot($)
263 busybot_cmd_bot_execute $bot, 0, ['cmd', 'wait', $timeoffset_preintermission];
264 busybot_cmd_bot_execute $bot, 0, ['barrier'];
265 if($bot->{intermission})
267 busybot_cmd_bot_execute $bot, 0, @{$bot->{intermission}};
269 busybot_cmd_bot_execute $bot, 0, ['barrier'];
270 $notetime = $timeoffset_postintermission - $lowestnotestart;
274 sub busybot_note_off_bot($$$$)
276 my ($bot, $time, $channel, $note) = @_;
277 #print STDERR "note off $bot:$time:$channel:$note\n";
280 my $cmds = $bot->{notes_off}->{$note - ($bot->{transpose} || 0) - $transpose};
282 if not defined $cmds; # note off cannot fail
285 #print STDERR "BUSY: $busy bots (OFF)\n";
286 busybot_cmd_bot_execute $bot, $time + $notetime, @$cmds;
290 sub busybot_note_on_bot($$$$$)
292 my ($bot, $time, $channel, $note, $init) = @_;
293 return -1 # I won't play on this channel
294 if defined $bot->{channels} and not $bot->{channels}->{$channel};
300 $cmds = $bot->{vocals};
304 $cmds = [ map { [ map { $_ eq '%s' ? $note : $_ } @$_ ] } @$cmds ];
307 elsif($channel == 10)
310 $cmds = $bot->{percussion}->{$note};
316 $cmds = $bot->{notes_on}->{$note - ($bot->{transpose} || 0) - $transpose};
317 $cmds_off = $bot->{notes_off}->{$note - ($bot->{transpose} || 0) - $transpose};
319 return -1 # I won't play this note
320 if not defined $cmds;
323 #print STDERR "note on $bot:$time:$channel:$note\n";
327 if not busybot_cmd_bot_test $bot, $time + $notetime, @$cmds;
328 busybot_cmd_bot_execute $bot, 0, ['cmd', 'wait', $timeoffset_preinit];
329 busybot_cmd_bot_execute $bot, 0, ['barrier'];
330 busybot_cmd_bot_execute $bot, 0, @{$bot->{init}}
332 busybot_cmd_bot_execute $bot, 0, ['barrier'];
333 for(1..$intermissions)
335 busybot_intermission_bot $bot;
337 busybot_cmd_bot_execute $bot, $time + $notetime, @$cmds;
342 if not busybot_cmd_bot_test $bot, $time + $notetime, @$cmds;
343 busybot_cmd_bot_execute $bot, $time + $notetime, @$cmds;
345 if(defined $cmds and defined $cmds_off)
349 #print STDERR "BUSY: $busy bots (ON)\n";
356 $busybots = Storable::dclone $busybots_orig;
357 @busybots_allocated = ();
358 %notechannelbots = ();
360 $notetime = $timeoffset_postinit - $lowestnotestart;
363 sub busybot_note_off($$$)
365 my ($time, $channel, $note) = @_;
367 #print STDERR "note off $time:$channel:$note\n";
374 if(my $bot = $notechannelbots{$channel}{$note})
376 busybot_note_off_bot $bot, $time, $channel, $note;
377 delete $notechannelbots{$channel}{$note};
384 sub busybot_note_on($$$)
386 my ($time, $channel, $note) = @_;
388 if($notechannelbots{$channel}{$note})
390 busybot_note_off $time, $channel, $note;
393 #print STDERR "note on $time:$channel:$note\n";
397 for(unsort @busybots_allocated)
399 my $canplay = busybot_note_on_bot $_, $time, $channel, $note, 0;
402 $notechannelbots{$channel}{$note} = $_;
410 for(unsort keys %$busybots)
412 next if $busybots->{$_}->{count} <= 0;
413 my $bot = Storable::dclone $busybots->{$_};
414 $bot->{id} = @busybots_allocated + 1;
415 $bot->{classname} = $_;
416 my $canplay = busybot_note_on_bot $bot, $time, $channel, $note, 1;
421 --$busybots->{$_}->{count};
422 $notechannelbots{$channel}{$note} = $bot;
423 push @busybots_allocated, $bot;
426 die "Fresh bot cannot play stuff"
432 warn "Not enough bots to play this (when playing $channel:$note)";
436 warn "Note $channel:$note cannot be played by any bot";
444 my (@preallocate) = @_;
448 die "Cannot preallocate any more $_ bots"
449 if $busybots->{$_}->{count} <= 0;
450 my $bot = Storable::dclone $busybots->{$_};
451 $bot->{id} = @busybots_allocated + 1;
452 $bot->{classname} = $_;
453 busybot_cmd_bot_execute $bot, 0, ['cmd', 'wait', $timeoffset_preinit];
454 busybot_cmd_bot_execute $bot, 0, ['barrier'];
455 busybot_cmd_bot_execute $bot, 0, @{$bot->{init}}
457 busybot_cmd_bot_execute $bot, 0, ['barrier'];
458 --$busybots->{$_}->{count};
459 push @busybots_allocated, $bot;
465 my ($filename, $trans) = @_;
468 my $opus = MIDI::Opus->new({from_file => $filename});
469 my $ticksperquarter = $opus->ticks();
470 my $tracks = $opus->tracks_r();
471 my @tempi = (); # list of start tick, time per tick pairs (calculated as seconds per quarter / ticks per quarter)
475 for($tracks->[0]->events())
478 if($_->[0] eq 'set_tempo')
480 push @tempi, [$tick, $_->[2] * 0.000001 / $ticksperquarter];
487 my $curtempo = [0, 0.5 / $ticksperquarter];
492 # this event is in the past
493 # we add the full time since the last one then
494 $sec += ($_->[0] - $curtempo->[0]) * $curtempo->[1];
498 # if this event is in the future, we break
503 $sec += ($tick - $curtempo->[0]) * $curtempo->[1];
507 # merge all to a single track
508 my @allmidievents = ();
510 for my $track(0..@$tracks-1)
513 for($tracks->[$track]->events())
515 my ($command, $delta, @data) = @$_;
516 $command = 'note_off' if $command eq 'note_on' and $data[2] == 0;
518 push @allmidievents, [$command, $tick, $sequence++, $track, @data];
522 if(open my $fh, "$filename.vocals")
529 my ($tick, $file) = split /\s+/, $_;
534 elsif($tick eq 'shift')
540 push @allmidievents, ['note_on', $tick * $scale + $shift, $sequence++, -1, -1, $file];
541 push @allmidievents, ['note_off', $tick * $scale + $shift, $sequence++, -1, -1, $file];
546 @allmidievents = sort { $a->[1] <=> $b->[1] or $a->[2] <=> $b->[2] } @allmidievents;
549 my $note_min = undef;
550 my $note_max = undef;
555 $t = $tick2sec->($_->[1]);
557 if($_->[0] eq 'note_on')
559 my $chan = $_->[4] + 1;
561 if $chan != 10 and $chan > 0 and (not defined $note_min or $_->[5] < $note_min);
563 if $chan != 10 and $chan > 0 and (not defined $note_max or $_->[5] > $note_max);
564 if($midinotes{$chan}{$_->[5]})
567 busybot_note_off($t, $chan, $_->[5]);
569 busybot_note_on($t, $chan, $_->[5]);
571 $midinotes{$chan}{$_->[5]} = 1;
573 elsif($_->[0] eq 'note_off')
575 my $chan = $_->[4] + 1;
576 if($midinotes{$chan}{$_->[5]})
579 busybot_note_off($t, $chan, $_->[5]);
581 $midinotes{$chan}{$_->[5]} = 0;
585 print STDERR "For file $filename:\n";
586 print STDERR " Range of notes: $note_min .. $note_max\n";
587 print STDERR " Safe transpose range: @{[$note_max - 19]} .. @{[$note_min + 13]}\n";
588 print STDERR " Unsafe transpose range: @{[$note_max - 27]} .. @{[$note_min + 18]}\n";
589 print STDERR " Stuck notes: $notes_stuck\n";
591 while(my ($k1, $v1) = each %midinotes)
593 while(my ($k2, $v2) = each %$v1)
595 busybot_note_off($t, $k1, $k2);
599 for(@busybots_allocated)
601 busybot_intermission_bot $_;
608 print STDERR "Bots allocated:\n";
609 for(@busybots_allocated)
611 print STDERR "$_->{id} is a $_->{classname}\n";
613 for(@busybots_allocated)
615 busybot_cmd_bot_execute $_, 0, ['cmd', 'wait', $timeoffset_predone];
616 busybot_cmd_bot_execute $_, 0, ['barrier'];
619 busybot_cmd_bot_execute $_, 0, @{$_->{done}};
621 busybot_cmd_bot_execute $_, 0, ['cmd', 'wait', $timeoffset_postdone];
622 busybot_cmd_bot_execute $_, 0, ['barrier'];
626 my @preallocate = ();
633 Preallocate(@preallocate);
637 my $filename = shift @l;
638 my $transpose = shift @l;
639 ConvertMIDI($filename, $transpose);
642 my @preallocate_new = map { $_->{classname} } @busybots_allocated;
643 if(@preallocate_new == @preallocate)
645 print "$precommands$commands";
648 @preallocate = @preallocate_new;
653 unless $@ eq "noalloc\n";