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 my ($filename, $transpose, $timeoffset) = @ARGV;
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)
26 for($tracks->[0]->events())
29 if($_->[0] eq 'set_tempo')
31 push @tempi, [$tick, $_->[2] * 0.000001 / $ticksperquarter];
38 my $curtempo = [0, 0.5 / $ticksperquarter];
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];
49 # if this event is in the future, we break
54 $sec += ($tick - $curtempo->[0]) * $curtempo->[1];
58 # merge all to a single track
59 my @allmidievents = ();
61 for my $track(0..@$tracks-1)
64 for($tracks->[$track]->events())
66 my ($command, $delta, @data) = @$_;
68 push @allmidievents, [$command, $tick, $sequence++, $track, @data];
71 @allmidievents = sort { $a->[1] <=> $b->[1] or $a->[2] <=> $b->[2] } @allmidievents;
76 return map { $_->[0] } sort { $a->[1] <=> $b->[1] } map { [$_, rand] } @_;
90 my $currentbot = undef;
91 my $appendref = undef;
100 my @cmd = split /\s+/, $_;
101 if($cmd[0] eq 'super')
103 push @$appendref, @$super;
107 push @$appendref, \@cmd;
114 my $base = $bots{$1};
119 $currentbot->{$_} = Storable::dclone $base->{$_}; # copy array items as new array
123 $currentbot->{$_} = $base->{$_};
126 # better: do some merging TODO
128 elsif(/^count (\d+)/)
130 $currentbot->{count} = $1;
132 elsif(/^transpose (\d+)/)
134 $currentbot->{transpose} += $1;
136 elsif(/^channels (.*)/)
138 $currentbot->{channels} = { map { $_ => 1 } split /\s+/, $1 };
142 $super = $currentbot->{init};
143 $currentbot->{init} = $appendref = [];
145 elsif(/^note on (-?\d+)/)
147 $super = $currentbot->{notes_on}->{$1};
148 $currentbot->{notes_on}->{$1} = $appendref = [];
150 elsif(/^note off (-?\d+)/)
152 $super = $currentbot->{notes_off}->{$1};
153 $currentbot->{notes_off}->{$1} = $appendref = [];
155 elsif(/^percussion (\d+)/)
157 $super = $currentbot->{percussion}->{$1};
158 $currentbot->{percussion}->{$1} = $appendref = [];
162 print "unknown command: $_\n";
167 $currentbot = ($bots{$1} ||= {});
171 print "unknown command: $_\n";
175 my $lowesttimeoffset = 0;
179 next unless defined $l;
180 next unless $l->[0]->[0] eq 'time';
181 my $t = $l->[0]->[1];
182 $lowesttimeoffset = $t
183 if $t < $lowesttimeoffset;
185 print STDERR "Using a time adjustment of $lowesttimeoffset\n";
186 $timeoffset -= $lowesttimeoffset;
191 sub busybot_cmd_bot_test($$@)
193 my ($bot, $time, @commands) = @_;
195 my $bottime = defined $bot->{timer} ? $bot->{timer} : -$timeoffset-1;
196 my $botbusytime = defined $bot->{busytimer} ? $bot->{busytimer} : -$timeoffset-1;
199 if $time < $botbusytime;
201 my $mintime = (@commands && ($commands[0]->[0] eq 'time')) ? $commands[0]->[1] : 0;
204 if $time + $mintime < $bottime;
209 sub busybot_cmd_bot_execute($$@)
211 my ($bot, $time, @commands) = @_;
215 if($_->[0] eq 'time')
217 printf "w %d %f\n", $bot->{id}, $time + $_->[1] + $timeoffset;
218 $bot->{timer} = $time + $_->[1];
220 elsif($_->[0] eq 'busy')
222 $bot->{busytimer} = $time + $_->[1];
224 elsif($_->[0] eq 'buttons')
226 my %buttons_release = %{$bot->{buttons} ||= {}};
230 delete $buttons_release{$1};
232 for(keys %buttons_release)
234 printf "r %d %s\n", $bot->{id}, $_;
235 delete $bot->{buttons}->{$_};
241 printf "p %d %s\n", $bot->{id}, $_;
242 $bot->{buttons}->{$_} = 1;
245 elsif($_->[0] eq 'cmd')
247 printf "sv_cmd bot_cmd %d %s\n", $bot->{id}, join " ", @{$_}[1..@$_-1];
254 sub busybot_note_off_bot($$$$)
256 my ($bot, $time, $channel, $note) = @_;
257 my $cmds = $bot->{notes_off}->{$note - $bot->{transpose} - $transpose};
259 if not defined $cmds; # note off cannot fail
260 busybot_cmd_bot_execute $bot, $time, @$cmds;
264 sub busybot_note_on_bot($$$$$)
266 my ($bot, $time, $channel, $note, $init) = @_;
267 return -1 # I won't play on this channel
268 if defined $bot->{channels} and not grep { $_ == $channel } $bot->{channels};
269 my $cmds = $bot->{notes_on}->{$note - $bot->{transpose} - $transpose};
270 if(not defined $cmds)
272 $cmds = $bot->{percussion}->{$note};
273 return -1 # I won't play this note
274 if not defined $cmds;
276 if($init && $bot->{init})
279 if not busybot_cmd_bot_test $bot, 0, @{$bot->{init}};
281 if not busybot_cmd_bot_test $bot, $time, @$cmds;
282 busybot_cmd_bot_execute $bot, 0, @{$bot->{init}};
283 busybot_cmd_bot_execute $bot, $time, @$cmds;
288 if not busybot_cmd_bot_test $bot, $time, @$cmds;
289 busybot_cmd_bot_execute $bot, $time, @$cmds;
294 my $busybots = botconfig_read "midi2cfg-ng.conf";
295 my @busybots_allocated;
298 sub busybot_note_off($$$)
300 my ($time, $channel, $note) = @_;
302 if(my $bot = $notechannelbots{$channel}{$note})
304 busybot_note_off_bot $bot, $time, $channel, $note;
305 delete $notechannelbots{$channel}{$note};
312 sub busybot_note_on($$$)
314 my ($time, $channel, $note) = @_;
316 if($notechannelbots{$channel}{$note})
318 busybot_note_off $time, $channel, $note;
323 for(unsort @busybots_allocated)
325 my $canplay = busybot_note_on_bot $_, $time, $channel, $note, 0;
328 $notechannelbots{$channel}{$note} = $_;
336 for(unsort keys %$busybots)
338 next if $busybots->{$_}->{count} <= 0;
339 my $bot = Storable::dclone $busybots->{$_};
340 $bot->{id} = @busybots_allocated + 1;
341 $bot->{classname} = $_;
342 my $canplay = busybot_note_on_bot $bot, $time, $channel, $note, 1;
345 --$busybots->{$_}->{count};
346 $notechannelbots{$channel}{$note} = $bot;
347 push @busybots_allocated, $bot;
356 warn "Not enough bots to play this";
358 print STDERR Dumper \@busybots_allocated;
362 warn "Note $channel:$note cannot be played by any bot"
368 print 'alias p "sv_cmd bot_cmd $1 presskey $2"' . "\n";
369 print 'alias r "sv_cmd bot_cmd $1 releasekey $2"' . "\n";
370 print 'alias w "sv_cmd bot_cmd $1 wait_until $2"' . "\n";
371 print 'alias m "sv_cmd bot_cmd $1 moveto \"$2 $3 $4\""' . "\n";
374 my $note_min = undef;
375 my $note_max = undef;
379 my $t = tick2sec $_->[1];
381 if($_->[0] eq 'note_on')
383 my $chan = $_->[4] + 1;
385 if not defined $note_min or $_->[5] < $note_min;
387 if not defined $note_max or $_->[5] > $note_max;
388 if($midinotes{$chan}{$_->[5]})
391 busybot_note_off($t, $chan, $_->[5]);
393 busybot_note_on($t, $chan, $_->[5]);
395 $midinotes{$chan}{$_->[5]} = 1;
397 elsif($_->[0] eq 'note_off')
399 my $chan = $_->[4] + 1;
400 if($midinotes{$chan}{$_->[5]})
403 busybot_note_off($t, $chan, $_->[5]);
405 $midinotes{$chan}{$_->[5]} = 0;
409 print STDERR "Range of notes: $note_min .. $note_max\n";
410 print STDERR "Safe transpose range: @{[$note_max - 19]} .. @{[$note_min + 13]}\n";
411 print STDERR "Unsafe transpose range: @{[$note_max - 27]} .. @{[$note_min + 18]}\n";
412 print STDERR "Stuck notes: $notes_stuck\n";
413 print STDERR "Bots allocated:\n";
414 for(@busybots_allocated)
416 print STDERR "$_->{id} is a $_->{classname}\n";