X-Git-Url: http://git.xonotic.org/?p=xonotic%2Fxonotic.git;a=blobdiff_plain;f=misc%2Ftools%2Fmidi2cfg-ng.pl;h=4fcf9760a22134d54dbbfbab75b48151bc961fc7;hp=3d96fd2467edea6533be7acb67f67becff5c50a4;hb=refs%2Fheads%2Fmaster;hpb=0cecd8ebc6dd461d9e4cf4a638b9614fb99d1693 diff --git a/misc/tools/midi2cfg-ng.pl b/misc/tools/midi2cfg-ng.pl index 3d96fd24..4fcf9760 100755 --- a/misc/tools/midi2cfg-ng.pl +++ b/misc/tools/midi2cfg-ng.pl @@ -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; }