]> icculus.org git repositories - divverent/nexuiz.git/blob - misc/tools/midi2cfg.pl
better handling of buttons
[divverent/nexuiz.git] / misc / tools / midi2cfg.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
12 my ($filename, $transpose, $walktime, @coords) = @ARGV;
13 my @coords_percussion = ();
14 my @coords_tuba = ();
15 my $l = \@coords_tuba;
16 for(@coords)
17 {
18         if($_ eq '/')
19         {
20                 $l = \@coords_percussion;
21         }
22         else
23         {
24                 push @$l, [split /\s+/, $_];
25         }
26 }
27
28 my $opus = MIDI::Opus->new({from_file => $filename});
29 my $ticksperquarter = $opus->ticks();
30 my $tracks = $opus->tracks_r();
31 my @tempi = (); # list of start tick, time per tick pairs (calculated as seconds per quarter / ticks per quarter)
32 my $tick;
33
34 $tick = 0;
35 for($tracks->[0]->events())
36 {   
37     $tick += $_->[1];
38     if($_->[0] eq 'set_tempo')
39     {   
40         push @tempi, [$_->[1], $_->[2] * 0.000001 / $ticksperquarter];
41     }
42 }
43 sub tick2sec($)
44 {
45     my ($tick) = @_;
46     my $sec = 0;
47     my $curtempo = [0, 0.5 / $ticksperquarter];
48     for(@tempi)
49     {
50         if($_->[0] < $tick)
51         {
52                         # this event is in the past
53                         # we add the full time since the last one then
54                         $sec += ($_->[0] - $curtempo->[0]) * $curtempo->[1];
55         }   
56         else
57         {
58                         # if this event is in the future, we break
59                         last;
60         }
61                 $curtempo = $_;
62     }
63         $sec += ($tick - $curtempo->[0]) * $curtempo->[1];
64         return $sec;
65 }
66
67 # merge all to a single track
68 my @allmidievents = ();
69 my $sequence = 0;
70 for my $track(0..@$tracks-1)
71 {
72         $tick = 0;
73         for($tracks->[$track]->events())
74         {
75                 my ($command, $delta, @data) = @$_;
76                 $tick += $delta;
77                 push @allmidievents, [$command, $tick, $sequence++, $track, @data];
78         }
79 }
80 @allmidievents = sort { $a->[1] <=> $b->[1] or $a->[2] <=> $b->[2] } @allmidievents;
81
82
83
84
85
86 my @busybots_percussion = map { undef } @coords_percussion;
87 my @busybots_tuba       = map { undef } @coords_tuba;
88
89 my $notes = 0;
90 sub busybot_findfree($$$)
91 {
92         my ($time, $vchannel, $note) = @_;
93         my $l = ($vchannel < 16) ? \@busybots_tuba : \@busybots_percussion;
94         for(0..@$l-1)
95         {
96                 if(!$l->[$_])
97                 {
98                         my $bot = {id => $_ + 1, busy => 0, busytime => 0, channel => $vchannel, curtime => -$walktime, curbuttons => 0, noteoffset => 0};
99                         $l->[$_] = $bot;
100                         return $bot;
101                 }
102                 return $l->[$_] if
103                         $l->[$_]{channel} == $vchannel
104                         &&
105                         !$l->[$_]{busy}
106                         &&
107                         $time > $l->[$_]{busytime};
108         }
109         die "No free channel found ($notes notes active)\n";
110 }
111
112 sub busybot_find($$)
113 {
114         my ($vchannel, $note) = @_;
115         my $l = ($vchannel < 16) ? \@busybots_tuba : \@busybots_percussion;
116         for(0..@$l-1)
117         {
118                 return $l->[$_] if
119                         $l->[$_]
120                         &&
121                         $l->[$_]{busy}
122                         &&
123                         $l->[$_]{channel} == $vchannel
124                         &&
125                         defined $l->[$_]{note}
126                         &&
127                         $l->[$_]{note} == $note;
128         }
129         return undef;
130 }
131
132 sub busybot_advance($$)
133 {
134         my ($bot, $t) = @_;
135         my $t0 = $bot->{curtime};
136         if($t != $t0)
137         {
138                 #print "sv_cmd bot_cmd $bot->{id} wait @{[$t - $t0]}\n";
139                 print "sv_cmd bot_cmd $bot->{id} wait_until $t\n";
140         }
141         $bot->{curtime} = $t;
142 }
143
144 sub busybot_setbuttonsandadvance($$$)
145 {
146         my ($bot, $t, $b) = @_;
147         my $b0 = $bot->{curbuttons};
148         my $press = $b & ~$b0;
149         my $release = $b0 & ~$b;
150         busybot_advance $bot => $t - 0.1;
151         print "sv_cmd bot_cmd $bot->{id} releasekey attack1\n" if $release & 32;
152         print "sv_cmd bot_cmd $bot->{id} releasekey attack2\n" if $release & 64;
153         busybot_advance $bot => $t - 0.05;
154         print "sv_cmd bot_cmd $bot->{id} releasekey forward\n" if $release & 1;
155         print "sv_cmd bot_cmd $bot->{id} releasekey backward\n" if $release & 2;
156         print "sv_cmd bot_cmd $bot->{id} releasekey left\n" if $release & 4;
157         print "sv_cmd bot_cmd $bot->{id} releasekey right\n" if $release & 8;
158         print "sv_cmd bot_cmd $bot->{id} releasekey crouch\n" if $release & 16;
159         print "sv_cmd bot_cmd $bot->{id} releasekey jump\n" if $release & 128;
160         print "sv_cmd bot_cmd $bot->{id} presskey forward\n" if $press & 1;
161         print "sv_cmd bot_cmd $bot->{id} presskey backward\n" if $press & 2;
162         print "sv_cmd bot_cmd $bot->{id} presskey left\n" if $press & 4;
163         print "sv_cmd bot_cmd $bot->{id} presskey right\n" if $press & 8;
164         print "sv_cmd bot_cmd $bot->{id} presskey crouch\n" if $press & 16;
165         print "sv_cmd bot_cmd $bot->{id} presskey jump\n" if $press & 128;
166         busybot_advance $bot => $t;
167         print "sv_cmd bot_cmd $bot->{id} presskey attack1\n" if $press & 32;
168         print "sv_cmd bot_cmd $bot->{id} presskey attack2\n" if $press & 64;
169         $bot->{curbuttons} = $b;
170 }
171
172 my %notes = (
173         -18 => '1lbc',
174         -17 => '1bc',
175         -16 => '1brc',
176         -13 => '1frc',
177         -12 => '1c',
178         -11 => '2lbc',
179         -10 => '1rc',
180         -9 => '1flc',
181         -8 => '1fc',
182         -7 => '1lc',
183         -6 => '1lb',
184         -5 => '1b',
185         -4 => '1br',
186         -3 => '2rc',
187         -2 => '2flc',
188         -1 => '1fl',
189         0 => '1',
190         1 => '2lb',
191         2 => '1r',
192         3 => '1fl',
193         4 => '1f',
194         5 => '1l',
195         6 => '2fr',
196         7 => '2',
197         8 => '1brj',
198         9 => '2r',
199         10 => '2fl',
200         11 => '2f',
201         12 => '2l',
202         13 => '2lbj',
203         14 => '1rj',
204         15 => '1flj',
205         16 => '1fj',
206         17 => '1lj',
207         18 => '2frj',
208         19 => '2j',
209         21 => '2rj',
210         22 => '2flj',
211         23 => '2fj',
212         24 => '2lj'
213 );
214
215 my $note_min = +99;
216 my $note_max = -99;
217 sub getnote($$)
218 {
219         my ($bot, $note) = @_;
220         $note_max = $note if $note_max < $note;
221         $note_min = $note if $note_min > $note;
222         $note -= $transpose;
223         $note -= $bot->{noteoffset};
224         my $s = $notes{$note};
225         return $s;
226 }
227
228 sub busybot_playnoteandadvance($$$)
229 {
230         my ($bot, $t, $note) = @_;
231         my $s = getnote $bot => $note;
232         return (warn("note $note not found"), 0)
233                 unless defined $s;
234         my $buttons = 0;
235         $buttons |= 1 if $s =~ /f/;
236         $buttons |= 2 if $s =~ /b/;
237         $buttons |= 4 if $s =~ /l/;
238         $buttons |= 8 if $s =~ /r/;
239         $buttons |= 16 if $s =~ /c/;
240         $buttons |= 32 if $s =~ /1/;
241         $buttons |= 64 if $s =~ /2/;
242         $buttons |= 128 if $s =~ /j/;
243         busybot_setbuttonsandadvance $bot => $t, $buttons;
244         return 1;
245 }
246
247 sub busybot_stopnoteandadvance($$$)
248 {
249         my ($bot, $t, $note) = @_;
250         my $s = getnote $bot => $note;
251         return 0
252                 unless defined $s;
253         my $buttons = $bot->{curbuttons};
254         $buttons &= ~(32 | 64);
255         #$buttons = 0;
256         busybot_setbuttonsandadvance $bot => $t, $buttons;
257         return 1;
258 }
259
260 sub note_on($$$)
261 {
262         my ($t, $channel, $note) = @_;
263         if(busybot_find($channel, $note))
264         {
265                 note_off($t, $channel, $note); # MIDI allows redoing a note-on for the same note
266         }
267         ++$notes;
268         if($channel == 10)
269         {
270                 $channel = 16 + $note; # percussion
271         }
272         my $bot = busybot_findfree($t, $channel, $note);
273         if($channel < 16)
274         {
275                 if(busybot_playnoteandadvance $bot => $t, $note)
276                 {
277                         $bot->{busy} = 1;
278                         $bot->{note} = $note;
279                         $bot->{busytime} = $t + 0.25;
280                         busybot_stopnoteandadvance $bot => $t + 0.15, $note;
281                 }
282         }
283         if($channel >= 16)
284         {
285                 busybot_advance $bot => $t;
286                 print "p $bot->{id} attack1\n";
287                 print "r $bot->{id} attack1\n";
288                 $bot->{busy} = 1;
289                 $bot->{note} = $note;
290                 $bot->{busytime} = $t + 1.5;
291         }
292 }
293
294 sub note_off($$$)
295 {
296         my ($t, $channel, $note) = @_;
297         --$notes;
298         if($channel == 10)
299         {
300                 $channel = 16 + $note; # percussion
301         }
302         my $bot = busybot_find($channel, $note)
303                 or return;
304         $bot->{busy} = 0;
305         if($channel < 16)
306         {
307                 busybot_stopnoteandadvance $bot => $t, $note;
308                 $bot->{busytime} = $t + 0.25;
309         }
310 }
311
312 print 'alias p "sv_cmd bot_cmd $1 presskey $2"' . "\n";
313 print 'alias r "sv_cmd bot_cmd $1 releasekey $2"' . "\n";
314
315 for(@allmidievents)
316 {
317         my $t = tick2sec $_->[1];
318         my $track = $_->[3];
319         if($_->[0] eq 'note_on')
320         {
321                 my $chan = $_->[4];
322                 note_on($t, $chan, $_->[5]);
323         }
324         elsif($_->[0] eq 'note_off')
325         {
326                 my $chan = $_->[4];
327                 note_off($t, $chan, $_->[5]);
328         }
329 }
330
331 print STDERR "Range of notes: $note_min .. $note_max\n";
332 print STDERR "Safe transpose range: @{[$note_max - 19]} .. @{[$note_min + 13]}\n";
333 print STDERR "Unsafe transpose range: @{[$note_max - 24]} .. @{[$note_min + 18]}\n";
334 printf STDERR "%d bots allocated for tuba, %d for percussion\n", int scalar grep { defined $_ } @busybots_tuba, int scalar grep { defined $_ } @busybots_percussion;
335
336 my $n = 0;
337 for(@busybots_percussion, @busybots_tuba)
338 {
339         ++$n if $_ && $_->{busy};
340 }
341 if($n)
342 {
343         die "$n channels blocked ($notes MIDI notes)";
344 }