]> git.xonotic.org Git - xonotic/xonotic.git/blobdiff - misc/tools/midi2cfg-ng.pl
Merge branch 'master' of git://de.git.xonotic.org/xonotic/xonotic
[xonotic/xonotic.git] / misc / tools / midi2cfg-ng.pl
index cf7dfb2fa60a333fb1ee0800c3996d59e44ffff2..3e178a24d3463aab4e021da807f0d16d541799f6 100755 (executable)
@@ -8,12 +8,25 @@ use MIDI;
 use MIDI::Opus;
 use Storable;
 
+# workaround for possible refire time problems
+use constant SYS_TICRATE => 0.033333;
+#use constant SYS_TICRATE => 0;
+
 use constant MIDI_FIRST_NONCHANNEL => 17;
 use constant MIDI_DRUMS_CHANNEL => 10;
 
-die "Usage: $0 filename.conf timeoffset_preinit timeoffset_postinit timeoffset_predone timeoffset_postdone timeoffset_preintermission timeoffset_postintermission midifile1 transpose1 midifile2 transpose2 ..."
-       unless @ARGV > 7 and @ARGV % 2;
-my ($config, $timeoffset_preinit, $timeoffset_postinit, $timeoffset_predone, $timeoffset_postdone, $timeoffset_preintermission, $timeoffset_postintermission, @midilist) = @ARGV;
+die "Usage: $0 filename.conf midifile1 transpose1 midifile2 transpose2 ..."
+       unless @ARGV > 1 and @ARGV % 2;
+
+my $timeoffset_preinit = 2;
+my $timeoffset_postinit = 2;
+my $timeoffset_predone = 2;
+my $timeoffset_postdone = 2;
+my $timeoffset_preintermission = 2;
+my $timeoffset_postintermission = 2;
+my $time_forgetfulness = 1.5;
+
+my ($config, @midilist) = @ARGV;
 
 sub unsort(@)
 {
@@ -114,6 +127,10 @@ sub botconfig_read($)
                        {
                                $currentbot->{channels} = { map { $_ => 1 } split /\s+/, $1 };
                        }
+                       elsif(/^programs (.*)/)
+                       {
+                               $currentbot->{programs} = { map { $_ => 1 } split /\s+/, $1 };
+                       }
                        elsif(/^init$/)
                        {
                                $super = $currentbot->{init};
@@ -144,6 +161,11 @@ sub botconfig_read($)
                                $super = $currentbot->{percussion}->{$1};
                                $currentbot->{percussion}->{$1} = $appendref = [];
                        }
+                       elsif(/^vocals$/)
+                       {
+                               $super = $currentbot->{vocals};
+                               $currentbot->{vocals} = $appendref = [];
+                       }
                        else
                        {
                                print "unknown command: $_\n";
@@ -157,6 +179,34 @@ sub botconfig_read($)
                {
                        $precommands .= "$1\n";
                }
+               elsif(/^timeoffset_preinit (.*)/)
+               {
+                       $timeoffset_preinit = $1;
+               }
+               elsif(/^timeoffset_postinit (.*)/)
+               {
+                       $timeoffset_postinit = $1;
+               }
+               elsif(/^timeoffset_predone (.*)/)
+               {
+                       $timeoffset_predone = $1;
+               }
+               elsif(/^timeoffset_postdone (.*)/)
+               {
+                       $timeoffset_postdone = $1;
+               }
+               elsif(/^timeoffset_preintermission (.*)/)
+               {
+                       $timeoffset_preintermission = $1;
+               }
+               elsif(/^timeoffset_postintermission (.*)/)
+               {
+                       $timeoffset_postintermission = $1;
+               }
+               elsif(/^time_forgetfulness (.*)/)
+               {
+                       $time_forgetfulness = $1;
+               }
                else
                {
                        print "unknown command: $_\n";
@@ -177,20 +227,73 @@ sub botconfig_read($)
 my $busybots_orig = botconfig_read $config;
 
 
-sub busybot_cmd_bot_test($$@)
+# returns: ($mintime, $maxtime, $busytime)
+sub busybot_cmd_bot_cmdinfo(@)
 {
-       my ($bot, $time, @commands) = @_;
+       my (@commands) = @_;
+
+       my $mintime = undef;
+       my $maxtime = undef;
+       my $busytime = undef;
+
+       for(@commands)
+       {
+               if($_->[0] eq 'time')
+               {
+                       $mintime = $_->[1]
+                               if not defined $mintime or $_->[1] < $mintime;
+                       $maxtime = $_->[1] + SYS_TICRATE
+                               if not defined $maxtime or $_->[1] + SYS_TICRATE > $maxtime;
+               }
+               elsif($_->[0] eq 'busy')
+               {
+                       $busytime = $_->[1] + SYS_TICRATE;
+               }
+       }
+
+       return ($mintime, $maxtime, $busytime);
+}
+
+sub busybot_cmd_bot_matchtime($$$@)
+{
+       my ($bot, $targettime, $targetbusytime, @commands) = @_;
+
+       # I want to execute @commands so that I am free on $targettime and $targetbusytime
+       # when do I execute it then?
+
+       my ($mintime, $maxtime, $busytime) = busybot_cmd_bot_cmdinfo @commands;
+
+       my $tstart_max = defined $maxtime ? $targettime - $maxtime : $targettime;
+       my $tstart_busy = defined $busytime ? $targetbusytime - $busytime : $targettime;
+
+       return $tstart_max < $tstart_busy ? $tstart_max : $tstart_busy;
+}
+
+# TODO function to find out whether, and when, to insert a command before another command to make it possible
+# (note-off before note-on)
+
+sub busybot_cmd_bot_test($$$@)
+{
+       my ($bot, $time, $force, @commands) = @_;
 
        my $bottime = defined $bot->{timer} ? $bot->{timer} : -1;
        my $botbusytime = defined $bot->{busytimer} ? $bot->{busytimer} : -1;
 
-       return 0
-               if $time < $botbusytime;
-       
-       my $mintime = (@commands && ($commands[0]->[0] eq 'time')) ? $commands[0]->[1] : 0;
+       my ($mintime, $maxtime, $busytime) = busybot_cmd_bot_cmdinfo @commands;
 
-       return 0
-               if $time + $mintime < $bottime;
+       if($time < $botbusytime)
+       {
+               warn "FORCE: $time < $botbusytime"
+                       if $force;
+               return $force;
+       }
+       
+       if(defined $mintime and $time + $mintime < $bottime)
+       {
+               warn "FORCE: $time + $mintime < $bottime"
+                       if $force;
+               return $force;
+       }
        
        return 1;
 }
@@ -204,11 +307,15 @@ sub busybot_cmd_bot_execute($$@)
                if($_->[0] eq 'time')
                {
                        $commands .= sprintf "sv_cmd bot_cmd %d wait_until %f\n", $bot->{id}, $time + $_->[1];
-                       $bot->{timer} = $time + $_->[1];
+                       if($bot->{timer} > $time + $_->[1] + SYS_TICRATE)
+                       {
+                               #use Carp; carp "Negative wait: $bot->{timer} <= @{[$time + $_->[1] + SYS_TICRATE]}";
+                       }
+                       $bot->{timer} = $time + $_->[1] + SYS_TICRATE;
                }
                elsif($_->[0] eq 'busy')
                {
-                       $bot->{busytimer} = $time + $_->[1];
+                       $bot->{busytimer} = $time + $_->[1] + SYS_TICRATE;
                }
                elsif($_->[0] eq 'buttons')
                {
@@ -239,6 +346,7 @@ sub busybot_cmd_bot_execute($$@)
                {
                        $commands .= sprintf "sv_cmd bot_cmd %d barrier\n", $bot->{id};
                        $bot->{timer} = $bot->{busytimer} = 0;
+                       undef $bot->{lastuse};
                }
                elsif($_->[0] eq 'raw')
                {
@@ -269,35 +377,69 @@ sub busybot_note_off_bot($$$$)
 {
        my ($bot, $time, $channel, $note) = @_;
        #print STDERR "note off $bot:$time:$channel:$note\n";
-       return 1
-               if $channel == 10;
-       my $cmds = $bot->{notes_off}->{$note - ($bot->{transpose} || 0) - $transpose};
+       my ($busychannel, $busynote, $cmds) = @{$bot->{busy}};
        return 1
                if not defined $cmds; # note off cannot fail
-       $bot->{busy} = 0;
-       #--$busy;
-       #print STDERR "BUSY: $busy bots (OFF)\n";
-       busybot_cmd_bot_execute $bot, $time + $notetime, @$cmds; 
+       die "Wrong note-off?!?"
+               if $busychannel != $channel || $busynote ne $note;
+       $bot->{busy} = undef;
+
+       my $t = $time + $notetime;
+       my ($mintime, $maxtime, $busytime) = busybot_cmd_bot_cmdinfo @$cmds;
+
+       # perform note-off "as soon as we can"
+       $t = $bot->{busytimer}
+               if $t < $bot->{busytimer};
+       $t = $bot->{timer} - $mintime
+               if $t < $bot->{timer} - $mintime;
+
+       busybot_cmd_bot_execute $bot, $t, @$cmds; 
        return 1;
 }
 
-sub busybot_note_on_bot($$$$$)
+sub busybot_get_cmds_bot($$$)
 {
-       my ($bot, $time, $channel, $note, $init) = @_;
-       return -1 # I won't play on this channel
-               if defined $bot->{channels} and not $bot->{channels}->{$channel};
-       my $cmds;
-       my $cmds_off;
-       if($channel == 10)
+       my ($bot, $channel, $note) = @_;
+       my ($k0, $k1, $cmds, $cmds_off) = (undef, undef, undef, undef);
+       if($channel <= 0)
+       {
+               # vocals
+               $cmds = $bot->{vocals};
+               if(defined $cmds)
+               {
+                       $cmds = [ map { [ map { $_ eq '%s' ? $note : $_ } @$_ ] } @$cmds ];
+               }
+               $k0 = "vocals";
+               $k1 = $channel;
+       }
+       elsif($channel == 10)
        {
+               # percussion
                $cmds = $bot->{percussion}->{$note};
-               $cmds_off = undef;
+               $k0 = "percussion";
+               $k1 = $note;
        }
        else
        {
+               # music
                $cmds = $bot->{notes_on}->{$note - ($bot->{transpose} || 0) - $transpose};
                $cmds_off = $bot->{notes_off}->{$note - ($bot->{transpose} || 0) - $transpose};
+               $k0 = "note";
+               $k1 = $note - ($bot->{transpose} || 0) - $transpose;
        }
+       return ($cmds, $cmds_off, $k0, $k1);
+}
+
+sub busybot_note_on_bot($$$$$$$)
+{
+       my ($bot, $time, $channel, $program, $note, $init, $force) = @_;
+       return -1 # I won't play on this channel
+               if defined $bot->{channels} and not $bot->{channels}->{$channel};
+       return -1 # I won't play this program
+               if defined $bot->{programs} and not $bot->{programs}->{$program};
+
+       my ($cmds, $cmds_off, $k0, $k1) = busybot_get_cmds_bot($bot, $channel, $note);
+
        return -1 # I won't play this note
                if not defined $cmds;
        return 0
@@ -306,7 +448,7 @@ sub busybot_note_on_bot($$$$$)
        if($init)
        {
                return 0
-                       if not busybot_cmd_bot_test $bot, $time + $notetime, @$cmds; 
+                       if not busybot_cmd_bot_test $bot, $time + $notetime, $force, @$cmds; 
                busybot_cmd_bot_execute $bot, 0, ['cmd', 'wait', $timeoffset_preinit];
                busybot_cmd_bot_execute $bot, 0, ['barrier'];
                busybot_cmd_bot_execute $bot, 0, @{$bot->{init}}
@@ -316,20 +458,35 @@ sub busybot_note_on_bot($$$$$)
                {
                        busybot_intermission_bot $bot;
                }
+               # we always did a barrier, so we know this works
                busybot_cmd_bot_execute $bot, $time + $notetime, @$cmds; 
        }
        else
        {
                return 0
-                       if not busybot_cmd_bot_test $bot, $time + $notetime, @$cmds; 
+                       if not busybot_cmd_bot_test $bot, $time + $notetime, $force, @$cmds; 
                busybot_cmd_bot_execute $bot, $time + $notetime, @$cmds; 
        }
        if(defined $cmds and defined $cmds_off)
        {
-               $bot->{busy} = 1;
-               #++$busy;
-               #print STDERR "BUSY: $busy bots (ON)\n";
+               $bot->{busy} = [$channel, $note, $cmds_off];
+       }
+       ++$bot->{seen}{$k0}{$k1};
+
+       if(($bot->{lastuse} // -666) >= $time - $time_forgetfulness && $channel == $bot->{lastchannel})
+       {
+               $bot->{lastchannelsequence} += 1;
        }
+       else
+       {
+               $bot->{lastchannelsequence} = 1;
+       }
+       $bot->{lastuse} = $time;
+       $bot->{lastchannel} = $channel;
+
+#      print STDERR "$time $bot->{id} $channel:$note\n"
+#              if $channel == 11;
+
        return 1;
 }
 
@@ -346,8 +503,10 @@ sub busybot_note_off($$$)
 {
        my ($time, $channel, $note) = @_;
 
-       #print STDERR "note off $time:$channel:$note\n";
+#      print STDERR "note off $time:$channel:$note\n";
 
+       return 0
+               if $channel <= 0;
        return 0
                if $channel == 10;
 
@@ -361,55 +520,169 @@ sub busybot_note_off($$$)
        return 0;
 }
 
-sub busybot_note_on($$$)
+sub botsort($$$$@)
 {
-       my ($time, $channel, $note) = @_;
+       my ($time, $channel, $program, $note, @bots) = @_;
+       return
+               map
+               {
+                       $_->[0]
+               }
+               sort
+               {
+                       $b->[1] <=> $a->[1]
+                       or
+                       ($a->[0]->{lastuse} // -666) <=> ($b->[0]->{lastuse} // -666)
+                       or
+                       $a->[2] <=> $b->[2]
+               }
+               map
+               {
+                       my $q = 0;
+                       if($channel != 10) # percussion just should do round robin
+                       {
+                               if(($_->{lastuse} // -666) >= $time - $time_forgetfulness)
+                               {
+                                       if($channel == $_->{lastchannel})
+                                       {
+                                               $q += $_->{lastchannelsequence};
+                                       }
+                                       else
+                                       {
+                                               # better leave this one alone
+                                               $q -= $_->{lastchannelsequence};
+                                       }
+                               }
+                       }
+                       [$_, $q, rand]
+               }
+               @bots;
+}
+
+sub busybot_note_on($$$$)
+{
+       my ($time, $channel, $program, $note) = @_;
 
        if($notechannelbots{$channel}{$note})
        {
                busybot_note_off $time, $channel, $note;
        }
 
-       #print STDERR "note on $time:$channel:$note\n";
+#      print STDERR "note on $time:$channel:$note\n";
 
        my $overflow = 0;
 
-       for(unsort @busybots_allocated)
+       my @epicfailbots = ();
+
+       for(botsort $time, $channel, $program, $note, @busybots_allocated)
        {
-               my $canplay = busybot_note_on_bot $_, $time, $channel, $note, 0;
+               my $canplay = busybot_note_on_bot $_, $time, $channel, $program, $note, 0, 0;
                if($canplay > 0)
                {
                        $notechannelbots{$channel}{$note} = $_;
                        return 1;
                }
-               $overflow = 1
+               push @epicfailbots, $_
                        if $canplay == 0;
                # wrong
        }
 
+       my $needalloc = 0;
+
        for(unsort 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;
+               my $canplay = busybot_note_on_bot $bot, $time, $channel, $program, $note, 1, 0;
                if($canplay > 0)
                {
-                       die "noalloc\n"
-                               if $noalloc;
-                       --$busybots->{$_}->{count};
-                       $notechannelbots{$channel}{$note} = $bot;
-                       push @busybots_allocated, $bot;
-                       return 1;
+                       if($noalloc)
+                       {
+                               $needalloc = 1;
+                       }
+                       else
+                       {
+                               --$busybots->{$_}->{count};
+                               $notechannelbots{$channel}{$note} = $bot;
+                               push @busybots_allocated, $bot;
+                               return 1;
+                       }
                }
                die "Fresh bot cannot play stuff"
                        if $canplay == 0;
        }
 
-       if($overflow)
+       if(@epicfailbots)
+       {
+               # we cannot add a new bot to play this
+               # we could try finding a bot that could play this, and force him to stop the note!
+
+               my @candidates = (); # contains: [$bot, $score, $offtime]
+
+               # put in all currently busy bots that COULD play this, if they did a note-off first
+               for my $bot(@epicfailbots)
+               {
+                       next
+                               if $busybots->{$bot->{classname}}->{count} != 0;
+                       next
+                               unless $bot->{busy};
+                       my ($busy_chan, $busy_note, $busy_cmds_off) = @{$bot->{busy}};
+                       next
+                               unless $busy_cmds_off;
+                       my ($cmds, $cmds_off, $k0, $k1) = busybot_get_cmds_bot $bot, $channel, $note;
+                       next
+                               unless $cmds;
+                       my ($mintime, $maxtime, $busytime) = busybot_cmd_bot_cmdinfo @$cmds;
+                       my ($mintime_off, $maxtime_off, $busytime_off) = busybot_cmd_bot_cmdinfo @$busy_cmds_off;
+
+                       my $noteofftime = busybot_cmd_bot_matchtime $bot, $time + $notetime + $mintime, $time + $notetime, @$busy_cmds_off;
+                       next
+                               if $noteofftime < $bot->{busytimer};
+                       next
+                               if $noteofftime + $mintime_off < $bot->{timer};
+
+                       my $score = 0;
+                       # prefer turning off long notes
+                       $score +=  100 * ($noteofftime - $bot->{timer});
+                       # prefer turning off low notes
+                       $score +=    1 * (-$note);
+                       # prefer turning off notes that already play on another channel
+                       $score += 1000 * (grep { $_ != $busy_chan && $notechannelbots{$_}{$busy_note} && $notechannelbots{$_}{$busy_note}{busy} } keys %notechannelbots);
+
+                       push @candidates, [$bot, $score, $noteofftime];
+               }
+
+               # we found one?
+
+               if(@candidates)
+               {
+                       @candidates = sort { $a->[1] <=> $b->[1] } @candidates;
+                       my ($bot, $score, $offtime) = @{(pop @candidates)};
+                       my $oldchan = $bot->{busy}->[0];
+                       my $oldnote = $bot->{busy}->[1];
+                       busybot_note_off $offtime - $notetime, $oldchan, $oldnote;
+                       my $canplay = busybot_note_on_bot $bot, $time, $channel, $program, $note, 0, 1;
+                       die "Canplay but not?"
+                               if $canplay <= 0;
+                       warn "Made $channel:$note play by stopping $oldchan:$oldnote";
+                       $notechannelbots{$channel}{$note} = $bot;
+                       return 1;
+               }
+       }
+
+       die "noalloc\n"
+               if $needalloc;
+
+       if(@epicfailbots)
        {
                warn "Not enough bots to play this (when playing $channel:$note)";
+#              for(@epicfailbots)
+#              {
+#                      my $b = $_->{busy};
+#                      warn "$_->{classname} -> @{[$b ? qq{$b->[0]:$b->[1]} : 'none']} @{[$_->{timer} - $notetime]} ($time)\n";
+#              }
        }
        else
        {
@@ -498,12 +771,37 @@ sub ConvertMIDI($$)
                        push @allmidievents, [$command, $tick, $sequence++, $track, @data];
                }
        }
+
+       if(open my $fh, "$filename.vocals")
+       {
+               my $scale = 1;
+               my $shift = 0;
+               for(<$fh>)
+               {
+                       chomp;
+                       my ($tick, $file) = split /\s+/, $_;
+                       if($tick eq 'scale')
+                       {
+                               $scale = $file;
+                       }
+                       elsif($tick eq 'shift')
+                       {
+                               $shift = $file;
+                       }
+                       else
+                       {
+                               push @allmidievents, ['note_on', $tick * $scale + $shift, $sequence++, -1, -1, $file];
+                               push @allmidievents, ['note_off', $tick * $scale + $shift, $sequence++, -1, -1, $file];
+                       }
+               }
+       }
+
        @allmidievents = sort { $a->[1] <=> $b->[1] or $a->[2] <=> $b->[2] } @allmidievents;
 
        my %midinotes = ();
-       my $note_min = undef;
-       my $note_max = undef;
        my $notes_stuck = 0;
+       my %notes_seen = ();
+       my %programs = ();
        my $t = 0;
        for(@allmidievents)
        {
@@ -512,16 +810,13 @@ sub ConvertMIDI($$)
                if($_->[0] eq 'note_on')
                {
                        my $chan = $_->[4] + 1;
-                       $note_min = $_->[5]
-                               if not defined $note_min or $_->[5] < $note_min and $chan != 10;
-                       $note_max = $_->[5]
-                               if not defined $note_max or $_->[5] > $note_max and $chan != 10;
+                       ++$notes_seen{$chan}{($programs{$chan} || 1)}{$_->[5]};
                        if($midinotes{$chan}{$_->[5]})
                        {
                                --$notes_stuck;
-                               busybot_note_off($t, $chan, $_->[5]);
+                               busybot_note_off($t - SYS_TICRATE - 0.001, $chan, $_->[5]);
                        }
-                       busybot_note_on($t, $chan, $_->[5]);
+                       busybot_note_on($t, $chan, $programs{$chan} || 1, $_->[5]);
                        ++$notes_stuck;
                        $midinotes{$chan}{$_->[5]} = 1;
                }
@@ -531,18 +826,97 @@ sub ConvertMIDI($$)
                        if($midinotes{$chan}{$_->[5]})
                        {
                                --$notes_stuck;
-                               busybot_note_off($t, $chan, $_->[5]);
+                               busybot_note_off($t - SYS_TICRATE - 0.001, $chan, $_->[5]);
                        }
                        $midinotes{$chan}{$_->[5]} = 0;
                }
+               elsif($_->[0] eq 'patch_change')
+               {
+                       my $chan = $_->[4] + 1;
+                       my $program = $_->[5] + 1;
+                       $programs{$chan} = $program;
+               }
        }
 
        print STDERR "For file $filename:\n";
-       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";
 
+       for my $testtranspose(-127..127)
+       {
+               my $toohigh = 0;
+               my $toolow = 0;
+               my $good = 0;
+               for my $channel(sort keys %notes_seen)
+               {
+                       next if $channel == 10 or $channel < 0;
+                       for my $program(sort keys %{$notes_seen{$channel}})
+                       {
+                               for my $note(sort keys %{$notes_seen{$channel}{$program}})
+                               {
+                                       my $cnt = $notes_seen{$channel}{$program}{$note};
+                                       my $votehigh = 0;
+                                       my $votelow = 0;
+                                       my $votegood = 0;
+                                       for(@busybots_allocated, grep { $_->{count} > 0 } values %$busybots)
+                                       {
+                                               next # I won't play on this channel
+                                                       if defined $_->{channels} and not $_->{channels}->{$channel};
+                                               next # I won't play this program
+                                                       if defined $_->{programs} and not $_->{programs}->{$program};
+                                               my $transposed = $note - ($_->{transpose} || 0) - $testtranspose;
+                                               if(exists $_->{notes_on}{$transposed})
+                                               {
+                                                       ++$votegood;
+                                               }
+                                               else
+                                               {
+                                                       ++$votehigh if $transposed >= 0;
+                                                       ++$votelow if $transposed < 0;
+                                               }
+                                       }
+                                       if($votegood)
+                                       {
+                                               $good += $cnt;
+                                       }
+                                       elsif($votelow >= $votehigh)
+                                       {
+                                               $toolow += $cnt;
+                                       }
+                                       else
+                                       {
+                                               $toohigh += $cnt;
+                                       }
+                               }
+                       }
+               }
+               next if !$toohigh != !$toolow;
+               print STDERR "  Transpose $testtranspose: $toohigh too high, $toolow too low, $good good\n";
+       }
+
+       for my $program(sort keys %{$notes_seen{10}})
+       {
+               for my $note(sort keys %{$notes_seen{10}{$program}})
+               {
+                       my $cnt = $notes_seen{10}{$program}{$note};
+                       my $votegood = 0;
+                       for(@busybots_allocated)
+                       {
+                               next # I won't play on this channel
+                                       if defined $_->{channels} and not $_->{channels}->{10};
+                               next # I won't play this program
+                                       if defined $_->{programs} and not $_->{programs}->{$program};
+                               if(exists $_->{percussion}{$note})
+                               {
+                                       ++$votegood;
+                               }
+                       }
+                       if(!$votegood)
+                       {
+                               print STDERR "Failed percussion $note ($cnt times)\n";
+                       }
+               }
+       }
+
        while(my ($k1, $v1) = each %midinotes)
        {
                while(my ($k2, $v2) = each %$v1)
@@ -561,9 +935,31 @@ sub ConvertMIDI($$)
 sub Deallocate()
 {
        print STDERR "Bots allocated:\n";
+       my %notehash;
+       my %counthash;
        for(@busybots_allocated)
        {
                print STDERR "$_->{id} is a $_->{classname}\n";
+               ++$counthash{$_->{classname}};
+               while(my ($type, $notehash) = each %{$_->{seen}})
+               {
+                       while(my ($k, $v) = each %$notehash)
+                       {
+                               $notehash{$_->{classname}}{$type}{$k} += $v;
+                       }
+               }
+       }
+       for my $cn(sort keys %counthash)
+       {
+               print STDERR "$counthash{$cn} bots of $cn have played:\n";
+               for my $type(sort keys %{$notehash{$cn}})
+               {
+                       for my $note(sort { $a <=> $b } keys %{$notehash{$cn}{$type}})
+                       {
+                               my $cnt = $notehash{$cn}{$type}{$note};
+                               print STDERR "  $type $note ($cnt times)\n";
+                       }
+               }
        }
        for(@busybots_allocated)
        {
@@ -597,6 +993,8 @@ for(;;)
                my @preallocate_new = map { $_->{classname} } @busybots_allocated;
                if(@preallocate_new == @preallocate)
                {
+                       print "sv_cmd bot_cmd reset\n";
+                       print "sv_cmd bot_cmd setbots @{[scalar @preallocate_new]}\n";
                        print "$precommands$commands";
                        exit 0;
                }