]> git.xonotic.org Git - xonotic/xonotic.git/blob - misc/tools/midichannels.pl
f86eae4db5e8212a15aa8d2ae0773a7512af4642
[xonotic/xonotic.git] / misc / tools / midichannels.pl
1 #!/usr/bin/perl
2
3 use strict;
4 use warnings;
5 use MIDI;
6 use MIDI::Opus;
7
8 my ($filename, @others) = @ARGV;
9 my $opus = MIDI::Opus->new({from_file => $filename});
10
11 my %chanpos = (
12         note_off => 2,
13         note_on => 2,
14         key_after_touch => 2,
15         control_change => 2,
16         patch_change => 2,
17         channel_after_touch => 2,
18         pitch_wheel_change => 2
19 );
20
21 my %isclean = (
22         set_tempo => sub { 1; },
23         note_off => sub { 1; },
24         note_on => sub { 1; },
25         control_change => sub { $_[3] == 64; },
26 );
27
28 sub abstime(@)
29 {
30         my $t = 0;
31         return map { [$_->[0], $t += $_->[1], @{$_}[2..(@$_-1)]]; } @_;
32 }
33
34 sub reltime(@)
35 {
36         my $t = 0;
37         return map { my $tsave = $t; $t = $_->[1]; [$_->[0], $t - $tsave, @{$_}[2..(@$_-1)]]; } @_;
38 }
39
40 sub clean(@)
41 {
42         return reltime grep { ($isclean{$_->[0]} // sub { 0; })->(@$_) } abstime @_;
43 }
44
45 for(@others)
46 {
47         my $opus2 = MIDI::Opus->new({from_file => $_});
48         if($opus2->ticks() != $opus->ticks())
49         {
50                 my $tickfactor = $opus->ticks() / $opus2->ticks();
51                 for($opus2->tracks())
52                 {
53                         $_->events(reltime map { $_->[1] = int($_->[1] * $tickfactor + 0.5); $_; } abstime $_->events());
54                 }
55         }
56         $opus->tracks($opus->tracks(), $opus2->tracks());
57 }
58
59 while(<STDIN>)
60 {
61         chomp;
62         my @arg = split /\s+/, $_;
63         my $cmd = shift @arg;
64         print "Executing: $cmd @arg\n";
65         if($cmd eq 'clean')
66         {
67                 my $tracks = $opus->tracks_r();
68                 $tracks->[$_]->events_r([clean($tracks->[$_]->events())])
69                         for 0..@$tracks-1;
70         }
71         elsif($cmd eq 'dump')
72         {
73                 print $opus->dump({ dump_tracks => 1 });
74         }
75         elsif($cmd eq 'ticks')
76         {
77                 if(@arg)
78                 {
79                         $opus->ticks($arg[0]);
80                 }
81                 else
82                 {
83                         print "Ticks: ", $opus->ticks(), "\n";
84                 }
85         }
86         elsif($cmd eq 'tricks')
87         {
88                 print "haha, very funny\n";
89         }
90         elsif($cmd eq 'retrack')
91         {
92                 my $tracks = $opus->tracks_r();
93                 my @newtracks = ();
94                 for(0..@$tracks-1)
95                 {
96                         for(abstime $tracks->[$_]->events())
97                         {
98                                 my $p = $chanpos{$_->[0]};
99                                 if(defined $p)
100                                 {
101                                         my $c = $_->[$p] + 1;
102                                         push @{$newtracks[$c]}, $_;
103                                 }
104                                 else
105                                 {
106                                         push @{$newtracks[0]}, $_;
107                                 }
108                         }
109                 }
110                 $opus->tracks_r([map { ($_ && @$_) ? MIDI::Track->new({ events => [reltime @$_] }) : () } @newtracks]);
111         }
112         elsif($cmd eq 'program')
113         {
114                 my $tracks = $opus->tracks_r();
115                 my ($track, $channel, $program) = @arg;
116                 for(($track eq '*') ? (0..@$tracks-1) : $track)
117                 {
118                         my @events = ();
119                         my $added = 0;
120                         for(abstime $tracks->[$_]->events())
121                         {
122                                 my $p = $chanpos{$_->[0]};
123                                 if(defined $p)
124                                 {
125                                         my $c = $_->[$p] + 1;
126                                         if($channel eq '*' || $c == $channel)
127                                         {
128                                                 next
129                                                         if $_->[0] eq 'patch_change';
130                                                 if(!$added)
131                                                 {
132                                                         push @events, ['patch_change', $_->[1], $c-1, $program-1]
133                                                                 if $program;
134                                                         $added = 1;
135                                                 }
136                                         }
137                                 }
138                                 push @events, $_;
139                         }
140                         $tracks->[$_]->events_r([reltime @events]);
141                 }
142         }
143         elsif($cmd eq 'transpose')
144         {
145                 my $tracks = $opus->tracks_r();
146                 my ($track, $channel, $delta) = @arg;
147                 for(($track eq '*') ? (0..@$tracks-1) : $track)
148                 {
149                         for($tracks->[$_]->events())
150                         {
151                                 my $p = $chanpos{$_->[0]};
152                                 if(defined $p)
153                                 {
154                                         my $c = $_->[$p] + 1;
155                                         if($channel eq '*' || $c == $channel)
156                                         {
157                                                 if($_->[0] eq 'note_on' || $_->[0] eq 'note_off')
158                                                 {
159                                                         $_->[3] += $delta;
160                                                 }
161                                         }
162                                 }
163                         }
164                 }
165         }
166         elsif($cmd eq 'channel')
167         {
168                 my $tracks = $opus->tracks_r();
169                 my ($track, %chanmap) = @arg;
170                 for(($track eq '*') ? (0..@$tracks-1) : $track)
171                 {
172                         my @events = ();
173                         for(abstime $tracks->[$_]->events())
174                         {
175                                 my $p = $chanpos{$_->[0]};
176                                 if(defined $p)
177                                 {
178                                         my $c = $_->[$p] + 1;
179                                         $c = $chanmap{$c} // $chanmap{'*'} // $c;
180                                         next
181                                                 if $c == 0; # kill by setting channel to 0
182                                         $_->[$p] = $c - 1;
183                                 }
184                                 push @events, $_;
185                         }
186                         $tracks->[$_]->events_r([reltime @events]);
187                 }
188         }
189         elsif($cmd eq 'percussion')
190         {
191                 my $tracks = $opus->tracks_r();
192                 my ($track, $channel, %map) = @arg;
193                 for(($track eq '*') ? (0..@$tracks-1) : $track)
194                 {
195                         my @events = ();
196                         for(abstime $tracks->[$_]->events())
197                         {
198                                 my $p = $chanpos{$_->[0]};
199                                 if(defined $p)
200                                 {
201                                         my $c = $_->[$p] + 1;
202                                         if($channel eq '*' || $c == $channel)
203                                         {
204                                                 if($_->[0] eq 'note_on' || $_->[0] eq 'note_off')
205                                                 {
206                                                         if(length $map{$_->[3]})
207                                                         {
208                                                                 $_->[3] = $map{$_->[3]};
209                                                         }
210                                                         elsif(exists $map{$_->[3]})
211                                                         {
212                                                                 next;
213                                                         }
214                                                 }
215                                         }
216                                 }
217                                 push @events, $_;
218                         }
219                         $tracks->[$_]->events_r([reltime @events]);
220                 }
221         }
222         elsif($cmd eq 'tracks')
223         {
224                 my $tracks = $opus->tracks_r();
225                 if(@arg)
226                 {
227                         my %taken = ();
228                         my @t = ();
229                         my $force = 0;
230                         for(@arg)
231                         {
232                                 if($_ eq '--force')
233                                 {
234                                         $force = 1;
235                                         next;
236                                 }
237                                 next if $taken{$_}++ and not $force;
238                                 push @t, $tracks->[$_];
239                         }
240                         $opus->tracks_r(\@t);
241                 }
242                 else
243                 {
244                         for(0..@$tracks-1)
245                         {
246                                 print "Track $_:";
247                                 my $name = undef;
248                                 my %channels = ();
249                                 my $notes = 0;
250                                 my %notehash = ();
251                                 my $t = 0;
252                                 my $events = 0;
253                                 my $min = undef;
254                                 my $max = undef;
255                                 for($tracks->[$_]->events())
256                                 {
257                                         ++$events;
258                                         $_->[0] = 'note_off' if $_->[0] eq 'note_on' and $_->[4] == 0;
259                                         $t += $_->[1];
260                                         my $p = $chanpos{$_->[0]};
261                                         if(defined $p)
262                                         {
263                                                 my $c = $_->[$p] + 1;
264                                                 $channels{$c} //= {};
265                                                 if($_->[0] eq 'patch_change')
266                                                 {
267                                                         ++$channels{$c}{$_->[3]};
268                                                 }
269                                         }
270                                         ++$notes if $_->[0] eq 'note_on';
271                                         $notehash{$_->[2]}{$_->[3]} = $t if $_->[0] eq 'note_on';
272                                         $notehash{$_->[2]}{$_->[3]} = undef if $_->[0] eq 'note_off';
273                                         $name = $_->[2] if $_->[0] eq 'track_name';
274                                         if($_->[0] eq 'note_on')
275                                         {
276                                                 $min = $_->[3] if !defined $min || $_->[3] < $min;
277                                                 $max = $_->[3] if !defined $max || $_->[3] > $max;
278                                         }
279                                 }
280                                 my $channels = join " ", map { sprintf "%s(%s)", $_, join ",", sort { $a <=> $b } keys %{$channels{$_}} } sort { $a <=> $b } keys %channels;
281                                 my @stuck = ();
282                                 while(my ($k1, $v1) = each %notehash)
283                                 {
284                                         while(my ($k2, $v2) = each %$v1)
285                                         {
286                                                 push @stuck, sprintf "%d:%d@%.1f%%", $k1+1, $k2, $v2 * 100.0 / $t
287                                                         if defined $v2;
288                                         }
289                                 }
290                                 print " $name" if defined $name;
291                                 print " (channel $channels)" if $channels ne "";
292                                 print " ($events events)" if $events;
293                                 print " ($notes notes [$min-$max])" if $notes;
294                                 print " (notes @stuck stuck)" if @stuck;
295                                 print "\n";
296                         }
297                 }
298         }
299         elsif($cmd eq 'save')
300         {
301                 $opus->write_to_file($arg[0]);
302         }
303         else
304         {
305                 print "Unknown command, allowed commands:\n";
306                 print "  clean\n";
307                 print "  dump\n";
308                 print "  ticks [value]\n";
309                 print "  retrack\n";
310                 print "  program <track|*> <channel|*> <program (1-based)>\n";
311                 print "  transpose <track|*> <channel|*> <delta>\n";
312                 print "  channel <track|*> <channel|*> <channel> [<channel> <channel> ...]\n";
313                 print "  percussion <track|*> <channel|*> <from> <to> [<from> <to> ...]\n";
314                 print "  tracks [trackno] [trackno] ...\n";
315         }
316         print "Done with: $cmd @arg\n";
317 }