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