From e6008dff937fb15edaba230291c0939f3ec88f77 Mon Sep 17 00:00:00 2001 From: div0 Date: Tue, 9 Jun 2009 19:17:48 +0000 Subject: [PATCH] midi2cfg-ng: a rewrite of midi2cfg using a config file, to later support percussion easily git-svn-id: svn://svn.icculus.org/nexuiz/trunk@6949 f962a42d-fe04-0410-a3ab-8c8b0445ebaa --- misc/tools/midi2cfg-ng.conf | 349 ++++++++++++++++++++++++++++++ misc/tools/midi2cfg-ng.pl | 412 ++++++++++++++++++++++++++++++++++++ 2 files changed, 761 insertions(+) create mode 100644 misc/tools/midi2cfg-ng.conf create mode 100755 misc/tools/midi2cfg-ng.pl diff --git a/misc/tools/midi2cfg-ng.conf b/misc/tools/midi2cfg-ng.conf new file mode 100644 index 000000000..32f517315 --- /dev/null +++ b/misc/tools/midi2cfg-ng.conf @@ -0,0 +1,349 @@ +bot tuba + count 0 + init + time -2 + cmd selectweapon 16 + note on -18 + time -0.05 + buttons left backward crouch + time 0 + buttons attack1 left backward crouch + time 0.05 + buttons + busy 0.25 + note on -17 + time -0.05 + buttons backward crouch + time 0 + buttons attack1 backward crouch + time 0.05 + buttons + busy 0.25 + note on -16 + time -0.05 + buttons backward right crouch + time 0 + buttons attack1 backward right crouch + time 0.05 + buttons + busy 0.25 + note on -13 + time -0.05 + buttons forward right crouch + time 0 + buttons attack1 forward right crouch + time 0.05 + buttons + busy 0.25 + note on -12 + time -0.05 + buttons crouch + time 0 + buttons attack1 crouch + time 0.05 + buttons + busy 0.25 + note on -11 + time -0.05 + buttons left backward crouch + time 0 + buttons attack2 left backward crouch + time 0.05 + buttons + busy 0.25 + note on -10 + time -0.05 + buttons right crouch + time 0 + buttons attack1 right crouch + time 0.05 + buttons + busy 0.25 + note on -9 + time -0.05 + buttons forward left crouch + time 0 + buttons attack1 forward left crouch + time 0.05 + buttons + busy 0.25 + note on -8 + time -0.05 + buttons forward crouch + time 0 + buttons attack1 forward crouch + time 0.05 + buttons + busy 0.25 + note on -7 + time -0.05 + buttons left crouch + time 0 + buttons attack1 left crouch + time 0.05 + buttons + busy 0.25 + note on -6 + time -0.05 + buttons left backward + time 0 + buttons attack1 left backward + time 0.05 + buttons + busy 0.25 + note on -5 + time -0.05 + buttons backward + time 0 + buttons attack1 backward + time 0.05 + buttons + busy 0.25 + note on -4 + time -0.05 + buttons backward right + time 0 + buttons attack1 backward right + time 0.05 + buttons + busy 0.25 + note on -3 + time -0.05 + buttons right crouch + time 0 + buttons attack2 right crouch + time 0.05 + buttons + busy 0.25 + note on -2 + time -0.05 + buttons forward left crouch + time 0 + buttons attack2 forward left crouch + time 0.05 + buttons + busy 0.25 + note on -1 + time -0.05 + buttons forward left + time 0 + buttons attack1 forward left + time 0.05 + buttons + busy 0.25 + note on 0 + time -0.05 + buttons + time 0 + buttons attack1 + time 0.05 + buttons + busy 0.25 + note on 1 + time -0.05 + buttons left backward + time 0 + buttons attack2 left backward + time 0.05 + buttons + busy 0.25 + note on 2 + time -0.05 + buttons right + time 0 + buttons attack1 right + time 0.05 + buttons + busy 0.25 + note on 3 + time -0.05 + buttons forward left + time 0 + buttons attack1 forward left + time 0.05 + buttons + busy 0.25 + note on 4 + time -0.05 + buttons forward + time 0 + buttons attack1 forward + time 0.05 + buttons + busy 0.25 + note on 5 + time -0.05 + buttons left + time 0 + buttons attack1 left + time 0.05 + buttons + busy 0.25 + note on 6 + time -0.05 + buttons forward right + time 0 + buttons attack2 forward right + time 0.05 + buttons + busy 0.25 + note on 7 + time -0.05 + buttons + time 0 + buttons attack2 + time 0.05 + buttons + busy 0.25 + note on 8 + time -0.05 + buttons backward right jump + time 0 + buttons attack1 backward right jump + time 0.05 + buttons + busy 0.25 + note on 9 + time -0.05 + buttons right + time 0 + buttons attack2 right + time 0.05 + buttons + busy 0.25 + note on 10 + time -0.05 + buttons forward left + time 0 + buttons attack2 forward left + time 0.05 + buttons + busy 0.25 + note on 11 + time -0.05 + buttons forward + time 0 + buttons attack2 forward + time 0.05 + buttons + busy 0.25 + note on 12 + time -0.05 + buttons left + time 0 + buttons attack2 left + time 0.05 + buttons + busy 0.25 + note on 13 + time -0.05 + buttons left backward jump + time 0 + buttons attack2 left backward jump + time 0.05 + buttons + busy 0.25 + note on 14 + time -0.05 + buttons right jump + time 0 + buttons attack1 right jump + time 0.05 + buttons + busy 0.25 + note on 15 + time -0.05 + buttons forward left jump + time 0 + buttons attack1 forward left jump + time 0.05 + buttons + busy 0.25 + note on 16 + time -0.05 + buttons forward jump + time 0 + buttons attack1 forward jump + time 0.05 + buttons + busy 0.25 + note on 17 + time -0.05 + buttons left jump + time 0 + buttons attack1 left jump + time 0.05 + buttons + busy 0.25 + note on 18 + time -0.05 + buttons forward right jump + time 0 + buttons attack2 forward right jump + time 0.05 + buttons + busy 0.25 + note on 19 + time -0.05 + buttons jump + time 0 + buttons attack2 jump + time 0.05 + buttons + busy 0.25 + note on 21 + time -0.05 + buttons right jump + time 0 + buttons attack2 right jump + time 0.05 + buttons + busy 0.25 + note on 22 + time -0.05 + buttons forward left jump + time 0 + buttons attack2 forward left jump + time 0.05 + buttons + busy 0.25 + note on 23 + time -0.05 + buttons forward jump + time 0 + buttons attack2 forward jump + time 0.05 + buttons + busy 0.25 + note on 24 + time -0.05 + buttons left jump + time 0 + buttons attack2 left jump + time 0.05 + buttons + busy 0.25 + +bot tuba_red + include tuba + transpose 0 + init + time -4 + cmd cc "color 68" + super + +bot tuba_blue + include tuba + transpose 3 + init + time -4 + cmd cc "color 221" + super + +bot red + include tuba_red + count 256 + +bot blue + include tuba_blue + count 256 diff --git a/misc/tools/midi2cfg-ng.pl b/misc/tools/midi2cfg-ng.pl new file mode 100755 index 000000000..83e4253ee --- /dev/null +++ b/misc/tools/midi2cfg-ng.pl @@ -0,0 +1,412 @@ +#!/usr/bin/perl + +# converter from Type 1 MIDI files to CFG files that control bots with the Tuba and other weapons for percussion (requires g_weaponarena all) +# usage: +# perl midi2cfg.pl filename.mid basenote walktime "x y z" "x y z" "x y z" ... "/" "x y z" "x y z" ... > filename.cfg + +use strict; +use warnings; +use MIDI; +use MIDI::Opus; +use Storable; + +use constant MIDI_FIRST_NONCHANNEL => 17; +use constant MIDI_DRUMS_CHANNEL => 10; + +my ($filename, $transpose, $timeoffset) = @ARGV; + +my $opus = MIDI::Opus->new({from_file => $filename}); +#$opus->write_to_file("/tmp/y.mid"); +my $ticksperquarter = $opus->ticks(); +my $tracks = $opus->tracks_r(); +my @tempi = (); # list of start tick, time per tick pairs (calculated as seconds per quarter / ticks per quarter) +my $tick; + +$tick = 0; +for($tracks->[0]->events()) +{ + $tick += $_->[1]; + if($_->[0] eq 'set_tempo') + { + push @tempi, [$tick, $_->[2] * 0.000001 / $ticksperquarter]; + } +} +sub tick2sec($) +{ + my ($tick) = @_; + my $sec = 0; + my $curtempo = [0, 0.5 / $ticksperquarter]; + for(@tempi) + { + if($_->[0] < $tick) + { + # this event is in the past + # we add the full time since the last one then + $sec += ($_->[0] - $curtempo->[0]) * $curtempo->[1]; + } + else + { + # if this event is in the future, we break + last; + } + $curtempo = $_; + } + $sec += ($tick - $curtempo->[0]) * $curtempo->[1]; + return $sec; +} + +# merge all to a single track +my @allmidievents = (); +my $sequence = 0; +for my $track(0..@$tracks-1) +{ + $tick = 0; + for($tracks->[$track]->events()) + { + my ($command, $delta, @data) = @$_; + $tick += $delta; + push @allmidievents, [$command, $tick, $sequence++, $track, @data]; + } +} +@allmidievents = sort { $a->[1] <=> $b->[1] or $a->[2] <=> $b->[2] } @allmidievents; + + + + + + +sub botconfig_read($) +{ + my ($fn) = @_; + my %bots = (); + open my $fh, "<", $fn + or die "<$fn: $!"; + + my $currentbot = undef; + my $appendref = undef; + my $super = undef; + while(<$fh>) + { + chomp; + s/\s*#.*//; + next if /^$/; + if(s/^\t\t//) + { + my @cmd = split /\s+/, $_; + if($cmd[0] eq 'super') + { + push @$appendref, @$super; + } + else + { + push @$appendref, \@cmd; + } + } + elsif(s/^\t//) + { + if(/^include (.*)/) + { + my $base = $bots{$1}; + for(keys %$base) + { + if(ref $base->{$_}) + { + $currentbot->{$_} = Storable::dclone $base->{$_}; # copy array items as new array + } + else + { + $currentbot->{$_} = $base->{$_}; + } + } + # better: do some merging TODO + } + elsif(/^count (\d+)/) + { + $currentbot->{count} = $1; + } + elsif(/^transpose (\d+)/) + { + $currentbot->{transpose} += $1; + } + elsif(/^channels (.*)/) + { + $currentbot->{channels} = { map { $_ => 1 } split /\s+/, $1 }; + } + elsif(/^init$/) + { + $super = $currentbot->{init}; + $currentbot->{init} = $appendref = []; + } + elsif(/^note on (-?\d+)/) + { + $super = $currentbot->{notes_on}->{$1}; + $currentbot->{notes_on}->{$1} = $appendref = []; + } + elsif(/^note off (-?\d+)/) + { + $super = $currentbot->{notes_off}->{$1}; + $currentbot->{notes_off}->{$1} = $appendref = []; + } + elsif(/^percussion (\d+)/) + { + $super = $currentbot->{percussion}->{$1}; + $currentbot->{percussion}->{$1} = $appendref = []; + } + else + { + print "unknown command: $_\n"; + } + } + elsif(/^bot (.*)/) + { + $currentbot = ($bots{$1} ||= {}); + } + else + { + print "unknown command: $_\n"; + } + } + + my $lowesttimeoffset = 0; + for(values %bots) + { + my $l = $_->{init}; + next unless defined $l; + next unless $l->[0]->[0] eq 'time'; + my $t = $l->[0]->[1]; + $lowesttimeoffset = $t + if $t < $lowesttimeoffset; + } + print STDERR "Using a time adjustment of $lowesttimeoffset\n"; + $timeoffset -= $lowesttimeoffset; + + return \%bots; +} + +sub busybot_cmd_bot_test($$@) +{ + my ($bot, $time, @commands) = @_; + + my $bottime = defined $bot->{timer} ? $bot->{timer} : -$timeoffset-1; + my $botbusytime = defined $bot->{busytimer} ? $bot->{busytimer} : -$timeoffset-1; + + return 0 + if $time < $botbusytime; + + my $mintime = (@commands && ($commands[0]->[0] eq 'time')) ? $commands[0]->[1] : 0; + + return 0 + if $time + $mintime < $bottime; + + return 1; +} + +sub busybot_cmd_bot_execute($$@) +{ + my ($bot, $time, @commands) = @_; + + for(@commands) + { + if($_->[0] eq 'time') + { + printf "w %d %f\n", $bot->{id}, $time + $_->[1] + $timeoffset; + $bot->{timer} = $time + $_->[1]; + } + elsif($_->[0] eq 'busy') + { + $bot->{busytimer} = $time + $_->[1]; + } + elsif($_->[0] eq 'buttons') + { + my %buttons_release = %{$bot->{buttons} ||= {}}; + for(@{$_}[1..@$_-1]) + { + /(.*)\??/ or next; + delete $buttons_release{$1}; + } + for(keys %buttons_release) + { + printf "r %d %s\n", $bot->{id}, $_; + delete $bot->{buttons}->{$_}; + } + for(@{$_}[1..@$_-1]) + { + /(.*)(\?)?/ or next; + defined $2 and next; + printf "p %d %s\n", $bot->{id}, $_; + $bot->{buttons}->{$_} = 1; + } + } + elsif($_->[0] eq 'cmd') + { + printf "sv_cmd bot_cmd %d %s\n", $bot->{id}, join " ", @{$_}[1..@$_-1]; + } + } + + return 1; +} + +sub busybot_note_off_bot($$$$) +{ + my ($bot, $time, $channel, $note) = @_; + my $cmds = $bot->{notes_off}->{$note - $bot->{transpose} - $transpose}; + return 1 + if not defined $cmds; # note off cannot fail + busybot_cmd_bot_execute $bot, $time, @$cmds; + return 1; +} + +sub busybot_note_on_bot($$$$$) +{ + my ($bot, $time, $channel, $note, $init) = @_; + return -1 # I won't play on this channel + if defined $bot->{channels} and not grep { $_ == $channel } $bot->{channels}; + my $cmds = $bot->{notes_on}->{$note - $bot->{transpose} - $transpose}; + if(not defined $cmds) + { + $cmds = $bot->{percussion}->{$note}; + return -1 # I won't play this note + if not defined $cmds; + } + if($init && $bot->{init}) + { + return 0 + if not busybot_cmd_bot_test $bot, 0, @{$bot->{init}}; + return 0 + if not busybot_cmd_bot_test $bot, $time, @$cmds; + busybot_cmd_bot_execute $bot, 0, @{$bot->{init}}; + busybot_cmd_bot_execute $bot, $time, @$cmds; + } + else + { + return 0 + if not busybot_cmd_bot_test $bot, $time, @$cmds; + busybot_cmd_bot_execute $bot, $time, @$cmds; + } + return 1; +} + +my $busybots = botconfig_read "midi2cfg-ng.conf"; +my @busybots_allocated; +my %notechannelbots; + +sub busybot_note_off($$$) +{ + my ($time, $channel, $note) = @_; + + if(my $bot = $notechannelbots{$channel}{$note}) + { + busybot_note_off_bot $bot, $time, $channel, $note; + delete $notechannelbots{$channel}{$note}; + return 1; + } + + return 0; +} + +sub busybot_note_on($$$) +{ + my ($time, $channel, $note) = @_; + + if($notechannelbots{$channel}{$note}) + { + busybot_note_off $time, $channel, $note; + } + + my $overflow = 0; + + for(@busybots_allocated) + { + my $canplay = busybot_note_on_bot $_, $time, $channel, $note, 0; + if($canplay > 0) + { + $notechannelbots{$channel}{$note} = $_; + return 1; + } + $overflow = 1 + if $canplay == 0; + # wrong + } + + for(map { $_->[0] } sort { $a->[1] <=> $b->[1] } map { [$_, rand] } keys %$busybots) + { + next if $busybots->{$_}->{count} <= 0; + my $bot = Storable::dclone $busybots->{$_}; + $bot->{id} = @busybots_allocated + 1; + $bot->{classname} = $_; + my $canplay = busybot_note_on_bot $bot, $time, $channel, $note, 1; + if($canplay > 0) + { + --$busybots->{$_}->{count}; + $notechannelbots{$channel}{$note} = $bot; + push @busybots_allocated, $bot; + return 1; + } + $overflow = 1 + if $canplay == 0; + } + + if($overflow) + { + warn "Not enough bots to play this"; + use Data::Dumper; + print STDERR Dumper \@busybots_allocated; + } + else + { + warn "Note $channel:$note cannot be played by any bot" + } + + return 0; +} + +print 'alias p "sv_cmd bot_cmd $1 presskey $2"' . "\n"; +print 'alias r "sv_cmd bot_cmd $1 releasekey $2"' . "\n"; +print 'alias w "sv_cmd bot_cmd $1 wait_until $2"' . "\n"; +print 'alias m "sv_cmd bot_cmd $1 moveto \"$2 $3 $4\""' . "\n"; + +my %midinotes = (); +my $note_min = undef; +my $note_max = undef; +my $notes_stuck = 0; +for(@allmidievents) +{ + my $t = tick2sec $_->[1]; + my $track = $_->[3]; + if($_->[0] eq 'note_on') + { + my $chan = $_->[4] + 1; + $note_min = $_->[5] + if not defined $note_min or $_->[5] < $note_min; + $note_max = $_->[5] + if not defined $note_max or $_->[5] > $note_max; + if($midinotes{$chan}{$_->[5]}) + { + --$notes_stuck; + busybot_note_off($t, $chan, $_->[5]); + } + busybot_note_on($t, $chan, $_->[5]); + ++$notes_stuck; + $midinotes{$chan}{$_->[5]} = 1; + } + elsif($_->[0] eq 'note_off') + { + my $chan = $_->[4] + 1; + if($midinotes{$chan}{$_->[5]}) + { + --$notes_stuck; + busybot_note_off($t, $chan, $_->[5]); + } + $midinotes{$chan}{$_->[5]} = 0; + } +} + +print STDERR "Range of notes: $note_min .. $note_max\n"; +print STDERR "Safe transpose range: @{[$note_max - 19]} .. @{[$note_min + 13]}\n"; +print STDERR "Unsafe transpose range: @{[$note_max - 27]} .. @{[$note_min + 18]}\n"; +print STDERR "Stuck notes: $notes_stuck\n"; +print STDERR "Bots allocated:\n"; +for(@busybots_allocated) +{ + print STDERR "$_->{id} is a $_->{classname}\n"; +} -- 2.39.2