]> icculus.org git repositories - divverent/nexuiz.git/blob - misc/tools/midi2cfg-ng.pl
more midi2cfg stuff :P
[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.mid transpose timeoffset timeoffset2 timeoffset3 preallocatedbots..."
17         unless @ARGV >= 5;
18 my ($filename, $transpose, $timeoffset, $timeoffset2, $timeoffset3, @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                 $tick += $delta;
70                 push @allmidievents, [$command, $tick, $sequence++, $track, @data];
71         }
72 }
73 @allmidievents = sort { $a->[1] <=> $b->[1] or $a->[2] <=> $b->[2] } @allmidievents;
74
75
76 sub unsort(@)
77 {
78         return map { $_->[0] } sort { $a->[1] <=> $b->[1] } map { [$_, rand] } @_;
79 }
80
81
82
83
84
85 my $inittime = undef;
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                 else
184                 {
185                         print "unknown command: $_\n";
186                 }
187         }
188
189         my $lowesttime = undef;
190         my $highesttime = undef;
191         my $highestbusytime = undef;
192         my $lowestnotestart = undef;
193         for(values %bots)
194         {
195                 my $l = $_->{init};
196                 next unless defined $l;
197                 my $t = $l->[0]->[0] eq 'time' ? $l->[0]->[1] : 0;
198                 $lowesttime = $t if not defined $lowesttime or $t < $lowesttime;
199                 for(@$l)
200                 {
201                         if($_->[0] eq 'time')
202                         {
203                                 $highesttime = $_->[1] if not defined $highesttime or $_->[1] > $highesttime;
204                         }
205                         if($_->[0] eq 'busy')
206                         {
207                                 $highestbusytime = $_->[1] if not defined $highestbusytime or $_->[1] > $highestbusytime;
208                         }
209                 }
210                 for(values %{$_->{notes_on}}, values %{$_->{percussion}})
211                 {
212                         my $t = $_->[0]->[0] eq 'time' ? $_->[0]->[1] : 0;
213                         $lowestnotestart = $t if not defined $lowestnotestart or $t < $lowestnotestart;
214                 }
215         }
216
217         my $initdelta = $highesttime - $lowesttime - $lowestnotestart;
218         if(defined $highestbusytime)
219         {
220                 my $initdelta2 = $highestbusytime - $lowesttime;
221                 $initdelta = $initdelta2
222                         if $initdelta2 > $initdelta;
223         }
224
225         # init shall take place at $timeoffset
226         # note playing shall take place at $timeoffset + $initdelta + $timeoffset2
227
228         $inittime = $timeoffset - $lowesttime;
229         $notetime = $timeoffset + $initdelta + $timeoffset2;
230
231         print STDERR "Initialization offset: $inittime (start: @{[$inittime + $lowesttime]}, end: @{[$inittime + $highesttime]})\n";
232         print STDERR "Note offset: $notetime (start: @{[$notetime + $lowestnotestart]})\n";
233
234         return \%bots;
235 }
236
237 sub busybot_cmd_bot_test($$@)
238 {
239         my ($bot, $time, @commands) = @_;
240
241         my $bottime = defined $bot->{timer} ? $bot->{timer} : -1;
242         my $botbusytime = defined $bot->{busytimer} ? $bot->{busytimer} : -1;
243
244         return 0
245                 if $time < $botbusytime;
246         
247         my $mintime = (@commands && ($commands[0]->[0] eq 'time')) ? $commands[0]->[1] : 0;
248
249         return 0
250                 if $time + $mintime < $bottime;
251         
252         return 1;
253 }
254
255 sub busybot_cmd_bot_execute($$@)
256 {
257         my ($bot, $time, @commands) = @_;
258
259         for(@commands)
260         {
261                 if($_->[0] eq 'time')
262                 {
263                         printf "w %d %f\n", $bot->{id}, $time + $_->[1];
264                         $bot->{timer} = $time + $_->[1];
265                 }
266                 elsif($_->[0] eq 'busy')
267                 {
268                         $bot->{busytimer} = $time + $_->[1];
269                 }
270                 elsif($_->[0] eq 'buttons')
271                 {
272                         my %buttons_release = %{$bot->{buttons} ||= {}};
273                         for(@{$_}[1..@$_-1])
274                         {
275                                 /(.*)\??/ or next;
276                                 delete $buttons_release{$1};
277                         }
278                         for(keys %buttons_release)
279                         {
280                                 printf "r %d %s\n", $bot->{id}, $_;
281                                 delete $bot->{buttons}->{$_};
282                         }
283                         for(@{$_}[1..@$_-1])
284                         {
285                                 /(.*)(\?)?/ or next;
286                                 defined $2 and next;
287                                 printf "p %d %s\n", $bot->{id}, $_;
288                                 $bot->{buttons}->{$_} = 1;
289                         }
290                 }
291                 elsif($_->[0] eq 'cmd')
292                 {
293                         printf "sv_cmd bot_cmd %d %s\n", $bot->{id}, join " ", @{$_}[1..@$_-1];
294                 }
295                 elsif($_->[0] eq 'raw')
296                 {
297                         printf join " ", @{$_}[1..@$_-1];
298                 }
299         }
300
301         return 1;
302 }
303
304 sub busybot_note_off_bot($$$$)
305 {
306         my ($bot, $time, $channel, $note) = @_;
307         my $cmds = $bot->{notes_off}->{$note - $bot->{transpose} - $transpose};
308         return 1
309                 if not defined $cmds; # note off cannot fail
310         $bot->{busy} = 0;
311         busybot_cmd_bot_execute $bot, $time + $notetime, @$cmds; 
312         return 1;
313 }
314
315 sub busybot_note_on_bot($$$$$)
316 {
317         my ($bot, $time, $channel, $note, $init) = @_;
318         return -1 # I won't play on this channel
319                 if defined $bot->{channels} and not $bot->{channels}->{$channel};
320         return 0
321                 if $bot->{busy};
322         my $cmds;
323         if($channel == 10)
324         {
325                 $cmds = $bot->{percussion}->{$note};
326         }
327         else
328         {
329                 $cmds = $bot->{notes_on}->{$note - $bot->{transpose} - $transpose};
330                 my $cmds_off = $bot->{notes_off}->{$note - $bot->{transpose} - $transpose};
331                 if(defined $cmds and defined $cmds_off)
332                 {
333                         $bot->{busy} = 1;
334                 }
335         }
336         return -1 # I won't play this note
337                 if not defined $cmds;
338         if($init && $bot->{init})
339         {
340                 return 0
341                         if not busybot_cmd_bot_test $bot, $inittime, @{$bot->{init}};
342                 return 0
343                         if not busybot_cmd_bot_test $bot, $time + $notetime, @$cmds; 
344                 busybot_cmd_bot_execute $bot, $inittime, @{$bot->{init}};
345                 busybot_cmd_bot_execute $bot, $time + $notetime, @$cmds; 
346         }
347         else
348         {
349                 return 0
350                         if not busybot_cmd_bot_test $bot, $time + $notetime, @$cmds; 
351                 busybot_cmd_bot_execute $bot, $time + $notetime, @$cmds; 
352         }
353         return 1;
354 }
355
356 my $busybots = botconfig_read "midi2cfg-ng.conf";
357 my @busybots_allocated;
358 my %notechannelbots;
359
360 sub busybot_note_off($$$)
361 {
362         my ($time, $channel, $note) = @_;
363
364         return 0
365                 if $channel == 10;
366
367         if(my $bot = $notechannelbots{$channel}{$note})
368         {
369                 busybot_note_off_bot $bot, $time, $channel, $note;
370                 delete $notechannelbots{$channel}{$note};
371                 return 1;
372         }
373
374         return 0;
375 }
376
377 sub busybot_note_on($$$)
378 {
379         my ($time, $channel, $note) = @_;
380
381         if($notechannelbots{$channel}{$note})
382         {
383                 busybot_note_off $time, $channel, $note;
384         }
385
386         my $overflow = 0;
387
388         for(unsort @busybots_allocated)
389         {
390                 my $canplay = busybot_note_on_bot $_, $time, $channel, $note, 0;
391                 if($canplay > 0)
392                 {
393                         $notechannelbots{$channel}{$note} = $_;
394                         return 1;
395                 }
396                 $overflow = 1
397                         if $canplay == 0;
398                 # wrong
399         }
400
401         for(unsort keys %$busybots)
402         {
403                 next if $busybots->{$_}->{count} <= 0;
404                 my $bot = Storable::dclone $busybots->{$_};
405                 $bot->{id} = @busybots_allocated + 1;
406                 $bot->{classname} = $_;
407                 my $canplay = busybot_note_on_bot $bot, $time, $channel, $note, 1;
408                 if($canplay > 0)
409                 {
410                         --$busybots->{$_}->{count};
411                         $notechannelbots{$channel}{$note} = $bot;
412                         push @busybots_allocated, $bot;
413                         return 1;
414                 }
415                 $overflow = 1
416                         if $canplay == 0;
417         }
418
419         if($overflow)
420         {
421                 warn "Not enough bots to play this (when playing $channel:$note)";
422         }
423         else
424         {
425                 warn "Note $channel:$note cannot be played by any bot"
426         }
427
428         return 0;
429 }
430
431 print 'alias p "sv_cmd bot_cmd $1 presskey $2"' . "\n";
432 print 'alias r "sv_cmd bot_cmd $1 releasekey $2"' . "\n";
433 print 'alias w "sv_cmd bot_cmd $1 wait_until $2"' . "\n";
434 print 'alias m "sv_cmd bot_cmd $1 moveto \"$2 $3 $4\""' . "\n";
435
436 for(@preallocate)
437 {
438         die "Cannot preallocate any more $_ bots"
439                 if $busybots->{$_}->{count} <= 0;
440         my $bot = Storable::dclone $busybots->{$_};
441         $bot->{id} = @busybots_allocated + 1;
442         $bot->{classname} = $_;
443         busybot_cmd_bot_execute $bot, $inittime, @{$bot->{init}};
444         --$busybots->{$_}->{count};
445         push @busybots_allocated, $bot;
446 }
447
448 my %midinotes = ();
449 my $note_min = undef;
450 my $note_max = undef;
451 my $notes_stuck = 0;
452 my $t = 0;
453 for(@allmidievents)
454 {
455         $t = tick2sec $_->[1];
456         my $track = $_->[3];
457         if($_->[0] eq 'note_on')
458         {
459                 my $chan = $_->[4] + 1;
460                 $note_min = $_->[5]
461                         if not defined $note_min or $_->[5] < $note_min and $chan != 10;
462                 $note_max = $_->[5]
463                         if not defined $note_max or $_->[5] > $note_max and $chan != 10;
464                 if($midinotes{$chan}{$_->[5]})
465                 {
466                         --$notes_stuck;
467                         busybot_note_off($t, $chan, $_->[5]);
468                 }
469                 busybot_note_on($t, $chan, $_->[5]);
470                 ++$notes_stuck;
471                 $midinotes{$chan}{$_->[5]} = 1;
472         }
473         elsif($_->[0] eq 'note_off')
474         {
475                 my $chan = $_->[4] + 1;
476                 if($midinotes{$chan}{$_->[5]})
477                 {
478                         --$notes_stuck;
479                         busybot_note_off($t, $chan, $_->[5]);
480                 }
481                 $midinotes{$chan}{$_->[5]} = 0;
482         }
483 }
484
485 for(@busybots_allocated)
486 {
487         if($_->{done})
488         {
489                 busybot_cmd_bot_execute $_, $notetime + $t + $timeoffset3, @{$_->{done}};
490         }
491 }
492
493 print STDERR "Range of notes: $note_min .. $note_max\n";
494 print STDERR "Safe transpose range: @{[$note_max - 19]} .. @{[$note_min + 13]}\n";
495 print STDERR "Unsafe transpose range: @{[$note_max - 27]} .. @{[$note_min + 18]}\n";
496 print STDERR "Stuck notes: $notes_stuck\n";
497 print STDERR "Bots allocated:\n";
498 for(@busybots_allocated)
499 {
500         print STDERR "$_->{id} is a $_->{classname}\n";
501 }