]> git.xonotic.org Git - xonotic/xonotic.git/blobdiff - misc/tools/midi2cfg-ng.pl
Restore yet another weird path in SDL lib configs
[xonotic/xonotic.git] / misc / tools / midi2cfg-ng.pl
index 3d96fd2467edea6533be7acb67f67becff5c50a4..4fcf9760a22134d54dbbfbab75b48151bc961fc7 100755 (executable)
@@ -14,10 +14,22 @@ use constant SYS_TICRATE => 0.033333;
 
 use constant MIDI_FIRST_NONCHANNEL => 17;
 use constant MIDI_DRUMS_CHANNEL => 10;
+use constant TEXT_EVENT_CHANNEL => -1;
 
-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 %lists = ();
+my %listindexes = ();
+
+my ($config, @midilist) = @ARGV;
 
 sub unsort(@)
 {
@@ -79,7 +91,7 @@ sub botconfig_read($)
        while(<$fh>)
        {
                chomp;
-               s/\s*#.*//;
+               s/\s*\/\/.*//;
                next if /^$/;
                if(s/^\t\t//)
                {
@@ -118,6 +130,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};
@@ -148,14 +164,14 @@ sub botconfig_read($)
                                $super = $currentbot->{percussion}->{$1};
                                $currentbot->{percussion}->{$1} = $appendref = [];
                        }
-                       elsif(/^vocals$/)
+                       elsif(/^text (.*)$/)
                        {
-                               $super = $currentbot->{vocals};
-                               $currentbot->{vocals} = $appendref = [];
+                               $super = $currentbot->{text}->{$1};
+                               $currentbot->{text}->{$1} = $appendref = [];
                        }
                        else
                        {
-                               print "unknown command: $_\n";
+                               print STDERR "unknown command: $_\n";
                        }
                }
                elsif(/^bot (.*)/)
@@ -166,9 +182,42 @@ 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;
+               }
+               elsif(/^list (.*?) (.*)/)
+               {
+                       $lists{$1} = [split / /, $2];
+                       $listindexes{$1} = 0;
+               }
                else
                {
-                       print "unknown command: $_\n";
+                       print STDERR "unknown command: $_\n";
                }
        }
 
@@ -202,7 +251,7 @@ sub busybot_cmd_bot_cmdinfo(@)
                        $mintime = $_->[1]
                                if not defined $mintime or $_->[1] < $mintime;
                        $maxtime = $_->[1] + SYS_TICRATE
-                               if not defined $maxtime or $_->[1] > $maxtime;
+                               if not defined $maxtime or $_->[1] + SYS_TICRATE > $maxtime;
                }
                elsif($_->[0] eq 'busy')
                {
@@ -257,6 +306,19 @@ sub busybot_cmd_bot_test($$$@)
        return 1;
 }
 
+sub buildstring(@)
+{
+       return
+               join " ",
+               map
+               {
+                       $_ =~ /^\@(.*)$/
+                               ? do { $lists{$1}[$listindexes{$1}++ % @{$lists{$1}}]; }
+                               : $_
+               }
+               @_;
+}
+
 sub busybot_cmd_bot_execute($$@)
 {
        my ($bot, $time, @commands) = @_;
@@ -299,16 +361,25 @@ sub busybot_cmd_bot_execute($$@)
                }
                elsif($_->[0] eq 'cmd')
                {
-                       $commands .= sprintf "sv_cmd bot_cmd %d %s\n", $bot->{id}, join " ", @{$_}[1..@$_-1];
+                       $commands .= sprintf "sv_cmd bot_cmd %d %s\n", $bot->{id}, buildstring @{$_}[1..@$_-1];
+               }
+               elsif($_->[0] eq 'aim_random')
+               {
+                       $commands .= sprintf "sv_cmd bot_cmd %d aim \"%f 0 %f\"\n", $bot->{id}, $_->[1] + rand($_->[2] - $_->[1]), $_->[3];
                }
                elsif($_->[0] eq 'barrier')
                {
                        $commands .= sprintf "sv_cmd bot_cmd %d barrier\n", $bot->{id};
                        $bot->{timer} = $bot->{busytimer} = 0;
+                       undef $bot->{lastuse};
                }
                elsif($_->[0] eq 'raw')
                {
-                       $commands .= sprintf "%s\n", join " ", @{$_}[1..@$_-1];
+                       $commands .= sprintf "%s\n", buildstring @{$_}[1..@$_-1];
+               }
+               else
+               {
+                       warn "Invalid command: @$_";
                }
        }
 
@@ -335,6 +406,8 @@ sub busybot_note_off_bot($$$$)
 {
        my ($bot, $time, $channel, $note) = @_;
        #print STDERR "note off $bot:$time:$channel:$note\n";
+       return 1
+               if not $bot->{busy};
        my ($busychannel, $busynote, $cmds) = @{$bot->{busy}};
        return 1
                if not defined $cmds; # note off cannot fail
@@ -359,16 +432,19 @@ sub busybot_get_cmds_bot($$$)
 {
        my ($bot, $channel, $note) = @_;
        my ($k0, $k1, $cmds, $cmds_off) = (undef, undef, undef, undef);
-       if($channel <= 0)
+       if($channel == TEXT_EVENT_CHANNEL)
        {
                # vocals
-               $cmds = $bot->{vocals};
+               $note =~ /^([^:]*):(.*)$/;
+               my $name = $1;
+               my $data = $2;
+               $cmds = $bot->{text}->{$name};
                if(defined $cmds)
                {
-                       $cmds = [ map { [ map { $_ eq '%s' ? $note : $_ } @$_ ] } @$cmds ];
+                       $cmds = [ map { [ map { $_ eq '%s' ? $data : $_ } @$_ ] } @$cmds ];
                }
-               $k0 = "vocals";
-               $k1 = $channel;
+               $k0 = "text";
+               $k1 = $name;
        }
        elsif($channel == 10)
        {
@@ -388,11 +464,14 @@ sub busybot_get_cmds_bot($$$)
        return ($cmds, $cmds_off, $k0, $k1);
 }
 
-sub busybot_note_on_bot($$$$$$)
+sub busybot_note_on_bot($$$$$$$)
 {
-       my ($bot, $time, $channel, $note, $init, $force) = @_;
+       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);
 
@@ -423,11 +502,26 @@ sub busybot_note_on_bot($$$$$$)
                        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)
+       if(defined $cmds_off)
        {
                $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;
 }
 
@@ -446,11 +540,6 @@ sub busybot_note_off($$$)
 
 #      print STDERR "note off $time:$channel:$note\n";
 
-       return 0
-               if $channel <= 0;
-       return 0
-               if $channel == 10;
-
        if(my $bot = $notechannelbots{$channel}{$note})
        {
                busybot_note_off_bot $bot, $time, $channel, $note;
@@ -461,12 +550,52 @@ 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})
        {
+               print STDERR "THIS SHOULD NEVER HAPPEN\n";
                busybot_note_off $time, $channel, $note;
        }
 
@@ -476,9 +605,9 @@ sub busybot_note_on($$$)
 
        my @epicfailbots = ();
 
-       for(unsort @busybots_allocated)
+       for(botsort $time, $channel, $program, $note, @busybots_allocated)
        {
-               my $canplay = busybot_note_on_bot $_, $time, $channel, $note, 0, 0;
+               my $canplay = busybot_note_on_bot $_, $time, $channel, $program, $note, 0, 0;
                if($canplay > 0)
                {
                        $notechannelbots{$channel}{$note} = $_;
@@ -497,7 +626,7 @@ sub busybot_note_on($$$)
                my $bot = Storable::dclone $busybots->{$_};
                $bot->{id} = @busybots_allocated + 1;
                $bot->{classname} = $_;
-               my $canplay = busybot_note_on_bot $bot, $time, $channel, $note, 1, 0;
+               my $canplay = busybot_note_on_bot $bot, $time, $channel, $program, $note, 1, 0;
                if($canplay > 0)
                {
                        if($noalloc)
@@ -537,12 +666,13 @@ sub busybot_note_on($$$)
                        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, @$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 < $bot->{timer};
+                               if $noteofftime + $mintime_off < $bot->{timer};
 
                        my $score = 0;
                        # prefer turning off long notes
@@ -564,7 +694,7 @@ sub busybot_note_on($$$)
                        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, $note, 0, 1;
+                       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";
@@ -669,6 +799,8 @@ sub ConvertMIDI($$)
                        my ($command, $delta, @data) = @$_;
                        $command = 'note_off' if $command eq 'note_on' and $data[2] == 0;
                        $tick += $delta;
+                       next
+                               if $command eq 'text_event' && $data[0] !~ /:/;
                        push @allmidievents, [$command, $tick, $sequence++, $track, @data];
                }
        }
@@ -691,57 +823,223 @@ sub ConvertMIDI($$)
                        }
                        else
                        {
-                               push @allmidievents, ['note_on', $tick * $scale + $shift, $sequence++, -1, -1, $file];
-                               push @allmidievents, ['note_off', $tick * $scale + $shift, $sequence++, -1, -1, $file];
+                               push @allmidievents, ['text_event', $tick * $scale + $shift, $sequence++, -1, "vocals:$file"];
                        }
                }
        }
 
+       # HACK for broken rosegarden export: put patch changes first by clearing their sequence number
+       for(@allmidievents)
+       {
+               if($_->[0] eq 'patch_change')
+               {
+                       $_->[2] = -1;
+               }
+       }
+
+       # sort events
        @allmidievents = sort { $a->[1] <=> $b->[1] or $a->[2] <=> $b->[2] } @allmidievents;
 
+       # find the first interesting event
+       my $shift = [grep { $_->[0] eq 'note_on' || $_->[0] eq 'text_event' } @allmidievents]->[0][1];
+       die "No notes!"
+               unless defined $shift;
+
+       # shift times by first event, no boring waiting
+       $_->[0] = ($_->[0] < $shift ? 0 : $_->[0] - $shift) for @tempi;
+       $_->[1] = ($_->[1] < $shift ? 0 : $_->[1] - $shift) for @allmidievents;
+
+       # fix event list
+
        my %midinotes = ();
-       my $note_min = undef;
-       my $note_max = undef;
        my $notes_stuck = 0;
+       my %notes_seen = ();
+       my %programs = ();
        my $t = 0;
+       my %sustain = ();
+
+       my $note_on = sub
+       {
+               my ($ev) = @_;
+               my $chan = $ev->[4] + 1;
+               ++$notes_seen{$chan}{($programs{$chan} || 1)}{$ev->[5]};
+               if($midinotes{$chan}{$ev->[5]})
+               {
+                       --$notes_stuck;
+                       busybot_note_off($t - SYS_TICRATE - 0.001, $chan, $ev->[5]);
+               }
+               busybot_note_on($t, $chan, $programs{$chan} || 1, $ev->[5]);
+               ++$notes_stuck;
+               $midinotes{$chan}{$ev->[5]} = 1;
+       };
+
+       my $note_off = sub
+       {
+               my ($ev) = @_;
+               my $chan = $ev->[4] + 1;
+               if(exists $sustain{$chan})
+               {
+                       push @{$sustain{$chan}}, $ev;
+                       return;
+               }
+               if($midinotes{$chan}{$ev->[5]})
+               {
+                       --$notes_stuck;
+                       busybot_note_off($t - SYS_TICRATE - 0.001, $chan, $ev->[5]);
+               }
+               $midinotes{$chan}{$ev->[5]} = 0;
+       };
+
+       my $text_event = sub
+       {
+               my ($ev) = @_;
+
+               my $chan = TEXT_EVENT_CHANNEL;
+
+               busybot_note_on($t, TEXT_EVENT_CHANNEL, -1, $ev->[4]);
+               busybot_note_off($t, TEXT_EVENT_CHANNEL, $ev->[4]);
+       };
+
+       my $patch_change = sub
+       {
+               my ($ev) = @_;
+               my $chan = $ev->[4] + 1;
+               my $program = $ev->[5] + 1;
+               $programs{$chan} = $program;
+       };
+
+       my $sustain_change = sub
+       {
+               my ($ev) = @_;
+               my $chan = $ev->[4] + 1;
+               if($ev->[6] == 0)
+               {
+                       # release all currently not pressed notes
+                       my $s = $sustain{$chan};
+                       delete $sustain{$chan};
+                       for(@{($s || [])})
+                       {
+                               $note_off->($_);
+                       }
+               }
+               else
+               {
+                       # no more note-off
+                       $sustain{$chan} = [];
+               }
+       };
+
        for(@allmidievents)
        {
                $t = $tick2sec->($_->[1]);
-               my $track = $_->[3];
+               my $track = $_->[3];
                if($_->[0] eq 'note_on')
                {
-                       my $chan = $_->[4] + 1;
-                       $note_min = $_->[5]
-                               if $chan != 10 and $chan > 0 and (not defined $note_min or $_->[5] < $note_min);
-                       $note_max = $_->[5]
-                               if $chan != 10 and $chan > 0 and (not defined $note_max or $_->[5] > $note_max);
-                       if($midinotes{$chan}{$_->[5]})
-                       {
-                               --$notes_stuck;
-                               busybot_note_off($t - SYS_TICRATE, $chan, $_->[5]);
-                       }
-                       busybot_note_on($t, $chan, $_->[5]);
-                       ++$notes_stuck;
-                       $midinotes{$chan}{$_->[5]} = 1;
+                       $note_on->($_);
                }
                elsif($_->[0] eq 'note_off')
                {
-                       my $chan = $_->[4] + 1;
-                       if($midinotes{$chan}{$_->[5]})
-                       {
-                               --$notes_stuck;
-                               busybot_note_off($t - SYS_TICRATE, $chan, $_->[5]);
-                       }
-                       $midinotes{$chan}{$_->[5]} = 0;
+                       $note_off->($_);
+               }
+               elsif($_->[0] eq 'text_event')
+               {
+                       $text_event->($_);
+               }
+               elsif($_->[0] eq 'patch_change')
+               {
+                       $patch_change->($_);
+               }
+               elsif($_->[0] eq 'control_change' && $_->[5] == 64) # sustain pedal
+               {
+                       $sustain_change->($_);
                }
        }
 
+       # fake events for releasing pedal
+       for(keys %sustain)
+       {
+               $sustain_change->(['control_change', $t, undef, undef, $_ - 1, 64, 0]);
+       }
+
        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;
+                       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)
@@ -779,7 +1077,7 @@ sub Deallocate()
                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}})
+                       for my $note(sort keys %{$notehash{$cn}{$type}})
                        {
                                my $cnt = $notehash{$cn}{$type}{$note};
                                print STDERR "  $type $note ($cnt times)\n";
@@ -803,6 +1101,7 @@ my @preallocate = ();
 $noalloc = 0;
 for(;;)
 {
+       %listindexes = ();
        $commands = "";
        eval
        {
@@ -818,6 +1117,7 @@ for(;;)
                my @preallocate_new = map { $_->{classname} } @busybots_allocated;
                if(@preallocate_new == @preallocate)
                {
+                       print "sv_cmd bot_cmd setbots @{[scalar @preallocate_new]}\n";
                        print "$precommands$commands";
                        exit 0;
                }