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)
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
13 use constant MIDI_FIRST_NONCHANNEL => 17;
14 use constant MIDI_DRUMS_CHANNEL => 10;
16 die "Usage: $0 filename.conf filename.mid transpose timeoffset timeoffset2 timeoffset3 timeoffset4 preallocatedbots..."
18 my ($config, $filename, $transpose, $timeoffset, $timeoffset2, $timeoffset3, $timeoffset4, @preallocate) = @ARGV;
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)
28 for($tracks->[0]->events())
31 if($_->[0] eq 'set_tempo')
33 push @tempi, [$tick, $_->[2] * 0.000001 / $ticksperquarter];
40 my $curtempo = [0, 0.5 / $ticksperquarter];
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];
51 # if this event is in the future, we break
56 $sec += ($tick - $curtempo->[0]) * $curtempo->[1];
60 # merge all to a single track
61 my @allmidievents = ();
63 for my $track(0..@$tracks-1)
66 for($tracks->[$track]->events())
68 my ($command, $delta, @data) = @$_;
69 $command = 'note_off' if $command eq 'note_on' and $data[2] == 0;
71 push @allmidievents, [$command, $tick, $sequence++, $track, @data];
74 @allmidievents = sort { $a->[1] <=> $b->[1] or $a->[2] <=> $b->[2] } @allmidievents;
79 return map { $_->[0] } sort { $a->[1] <=> $b->[1] } map { [$_, rand] } @_;
94 my $currentbot = undef;
95 my $appendref = undef;
104 my @cmd = split /\s+/, $_;
105 if($cmd[0] eq 'super')
107 push @$appendref, @$super
110 elsif($cmd[0] eq 'percussion') # simple import
112 push @$appendref, @{$currentbot->{percussion}->{$cmd[1]}};
116 push @$appendref, \@cmd;
123 my $base = $bots{$1};
128 $currentbot->{$_} = Storable::dclone $base->{$_}; # copy array items as new array
132 $currentbot->{$_} = $base->{$_};
135 # better: do some merging TODO
137 elsif(/^count (\d+)/)
139 $currentbot->{count} = $1;
141 elsif(/^transpose (\d+)/)
143 $currentbot->{transpose} += $1;
145 elsif(/^channels (.*)/)
147 $currentbot->{channels} = { map { $_ => 1 } split /\s+/, $1 };
151 $super = $currentbot->{init};
152 $currentbot->{init} = $appendref = [];
156 $super = $currentbot->{done};
157 $currentbot->{done} = $appendref = [];
159 elsif(/^note on (-?\d+)/)
161 $super = $currentbot->{notes_on}->{$1};
162 $currentbot->{notes_on}->{$1} = $appendref = [];
164 elsif(/^note off (-?\d+)/)
166 $super = $currentbot->{notes_off}->{$1};
167 $currentbot->{notes_off}->{$1} = $appendref = [];
169 elsif(/^percussion (\d+)/)
171 $super = $currentbot->{percussion}->{$1};
172 $currentbot->{percussion}->{$1} = $appendref = [];
176 print "unknown command: $_\n";
181 $currentbot = ($bots{$1} ||= {count => 0, transpose => 0});
189 print "unknown command: $_\n";
193 my $lowestnotestart = undef;
196 for(values %{$_->{notes_on}}, values %{$_->{percussion}})
198 my $t = $_->[0]->[0] eq 'time' ? $_->[0]->[1] : 0;
199 $lowestnotestart = $t if not defined $lowestnotestart or $t < $lowestnotestart;
203 $notetime = $timeoffset2 - $lowestnotestart;
208 sub busybot_cmd_bot_test($$@)
210 my ($bot, $time, @commands) = @_;
212 my $bottime = defined $bot->{timer} ? $bot->{timer} : -1;
213 my $botbusytime = defined $bot->{busytimer} ? $bot->{busytimer} : -1;
216 if $time < $botbusytime;
218 my $mintime = (@commands && ($commands[0]->[0] eq 'time')) ? $commands[0]->[1] : 0;
221 if $time + $mintime < $bottime;
226 sub busybot_cmd_bot_execute($$@)
228 my ($bot, $time, @commands) = @_;
232 if($_->[0] eq 'time')
234 printf "sv_cmd bot_cmd %d wait_until %f\n", $bot->{id}, $time + $_->[1];
235 $bot->{timer} = $time + $_->[1];
237 elsif($_->[0] eq 'busy')
239 $bot->{busytimer} = $time + $_->[1];
241 elsif($_->[0] eq 'buttons')
243 my %buttons_release = %{$bot->{buttons} ||= {}};
247 delete $buttons_release{$1};
249 for(keys %buttons_release)
251 printf "sv_cmd bot_cmd %d releasekey %s\n", $bot->{id}, $_;
252 delete $bot->{buttons}->{$_};
258 printf "sv_cmd bot_cmd %d presskey %s\n", $bot->{id}, $_;
259 $bot->{buttons}->{$_} = 1;
262 elsif($_->[0] eq 'cmd')
264 printf "sv_cmd bot_cmd %d %s\n", $bot->{id}, join " ", @{$_}[1..@$_-1];
266 elsif($_->[0] eq 'raw')
268 printf "%s\n", join " ", @{$_}[1..@$_-1];
275 sub busybot_note_off_bot($$$$)
277 my ($bot, $time, $channel, $note) = @_;
278 my $cmds = $bot->{notes_off}->{$note - $bot->{transpose} - $transpose};
280 if not defined $cmds; # note off cannot fail
282 busybot_cmd_bot_execute $bot, $time + $notetime, @$cmds;
286 sub busybot_note_on_bot($$$$$)
288 my ($bot, $time, $channel, $note, $init) = @_;
289 return -1 # I won't play on this channel
290 if defined $bot->{channels} and not $bot->{channels}->{$channel};
296 $cmds = $bot->{percussion}->{$note};
300 $cmds = $bot->{notes_on}->{$note - $bot->{transpose} - $transpose};
301 my $cmds_off = $bot->{notes_off}->{$note - $bot->{transpose} - $transpose};
302 if(defined $cmds and defined $cmds_off)
307 return -1 # I won't play this note
308 if not defined $cmds;
312 if not busybot_cmd_bot_test $bot, $time + $notetime, @$cmds;
313 busybot_cmd_bot_execute $bot, 0, ['cmd', 'wait', $timeoffset];
314 busybot_cmd_bot_execute $bot, 0, ['cmd', 'barrier'];
315 busybot_cmd_bot_execute $bot, 0, @{$bot->{init}}
317 busybot_cmd_bot_execute $bot, 0, ['cmd', 'barrier'];
318 $bot->{timer} = $bot->{busytimer} = 0;
319 busybot_cmd_bot_execute $bot, $time + $notetime, @$cmds;
324 if not busybot_cmd_bot_test $bot, $time + $notetime, @$cmds;
325 busybot_cmd_bot_execute $bot, $time + $notetime, @$cmds;
330 my $busybots = botconfig_read $config;
331 my @busybots_allocated;
334 sub busybot_note_off($$$)
336 my ($time, $channel, $note) = @_;
341 if(my $bot = $notechannelbots{$channel}{$note})
343 busybot_note_off_bot $bot, $time, $channel, $note;
344 delete $notechannelbots{$channel}{$note};
351 sub busybot_note_on($$$)
353 my ($time, $channel, $note) = @_;
355 if($notechannelbots{$channel}{$note})
357 busybot_note_off $time, $channel, $note;
362 for(unsort @busybots_allocated)
364 my $canplay = busybot_note_on_bot $_, $time, $channel, $note, 0;
367 $notechannelbots{$channel}{$note} = $_;
375 for(unsort keys %$busybots)
377 next if $busybots->{$_}->{count} <= 0;
378 my $bot = Storable::dclone $busybots->{$_};
379 $bot->{id} = @busybots_allocated + 1;
380 $bot->{classname} = $_;
381 my $canplay = busybot_note_on_bot $bot, $time, $channel, $note, 1;
384 --$busybots->{$_}->{count};
385 $notechannelbots{$channel}{$note} = $bot;
386 push @busybots_allocated, $bot;
395 warn "Not enough bots to play this (when playing $channel:$note)";
399 warn "Note $channel:$note cannot be played by any bot"
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, ['cmd', 'wait', $timeoffset];
413 busybot_cmd_bot_execute $bot, 0, ['cmd', 'barrier'];
414 busybot_cmd_bot_execute $bot, 0, @{$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;
423 my $note_min = undef;
424 my $note_max = undef;
429 $t = tick2sec $_->[1];
431 if($_->[0] eq 'note_on')
433 my $chan = $_->[4] + 1;
435 if not defined $note_min or $_->[5] < $note_min and $chan != 10;
437 if not defined $note_max or $_->[5] > $note_max and $chan != 10;
438 if($midinotes{$chan}{$_->[5]})
441 busybot_note_off($t, $chan, $_->[5]);
443 busybot_note_on($t, $chan, $_->[5]);
445 $midinotes{$chan}{$_->[5]} = 1;
447 elsif($_->[0] eq 'note_off')
449 my $chan = $_->[4] + 1;
450 if($midinotes{$chan}{$_->[5]})
453 busybot_note_off($t, $chan, $_->[5]);
455 $midinotes{$chan}{$_->[5]} = 0;
459 for(@busybots_allocated)
461 busybot_cmd_bot_execute $_, 0, ['cmd', 'wait', $timeoffset3];
462 busybot_cmd_bot_execute $_, 0, ['cmd', 'barrier'];
465 busybot_cmd_bot_execute $_, 0, @{$_->{done}};
467 busybot_cmd_bot_execute $_, 0, ['cmd', 'barrier'];
468 busybot_cmd_bot_execute $_, 0, ['cmd', 'wait', $timeoffset4];
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)
478 print STDERR "$_->{id} is a $_->{classname}\n";