3 # A program for processing Musescore XML files and halving the times of all the notes
4 # together with anything else that may be relevant (eg Time Sig, rests, trailing
5 # '_' after lyrics etc).
7 # Having written this and seen that there isn't really any state preserved from
8 # from one XML clause to another, it could all be done in an XSLT stylesheet. But I've
11 # Copyright (c) Dirk Koopman 2016
20 use File::Temp qw{ :mktemp };
27 our %half = ( # decode from one note length to its half
44 our %yesno = ( qw(yes 1 no 0) ); # used for turning translating yes/no text values
47 our $dbg = 0; # show debugging
48 our $removebeam = 1; # if set remove any BeamMode clauses
52 binmode STDOUT, "utf8";
54 foreach my $fn (@ARGV) {
57 usage() if $fn =~ /^\-+[\?h]/i;
58 $dbg ^= 1 if $fn =~ /^\-+x/;
59 $removebeam ^= 1 if $fn =~ /^\-+b/;
61 my ($ifn, $ofn, $tfn);
63 my ($name, $path, $suffix) = fileparse($fn, qr/\.[^.]*/);
64 if ($suffix eq ".mscx" || $suffix eq ".mscz") {
66 $ofn = $path . $name . "-halved.mscx";
68 # extract out the zipped up .mscx file from an .mscz archive
69 if ($suffix eq '.mscz') {
70 $tfn = mktemp("/tmp/msczXXXXXXX");
73 system("unzip -p $ifn $xifn > $tfn");
74 $ifn = $tfn; # the tmp file is the actual input.
77 usage("Only Musescore .mscx or .mscz files allowed (got: $fn)");
80 process($ifn, $ofn, $fn);
89 my ($ifn, $ofn, $fn) = @_;
91 my $p = XML::LibXML->new();
92 my $doc = eval { $p->load_xml(location=>$ifn) };
94 usage("Invalid Musescore file detected (in $fn) $@") unless $doc;
98 my ($muse) = $doc->findnodes('/museScore');
100 my ($v) = $muse->findnodes('./@version');
101 $version = $v->to_literal if $v;
103 if (!$version || $version < 2) {
104 $version ||= "Unknown";
105 usage("Version $version detected in $fn, this program will only work with MuseScore 2 (or greater) files");
108 my $of = IO::File->new(">$ofn") or usage("Cannot open $ofn $!");
110 foreach my $staff ($doc->findnodes('/museScore/Score/Staff')) {
111 my ($sigN, $sigD); # current time sig values (may be needed later)
112 my $syllabic = 0; # track syllabic mode (whether we are in the middle of a word in lyrics).
113 display($staff) if $dbg;
114 foreach my $measure ($staff->findnodes('./Measure')) {
117 # obtain the measure no and any len attr. Change the len attribute
118 my ($l) = $measure->findnodes('./@len');
120 my ($t,$b) = split m{/}, $l->to_literal;
126 foreach my $node ($measure->findnodes('./*')) {
127 if ($node->nodeType == XML_ELEMENT_NODE) {
128 my $name = $node->nodeName;
129 if ($name eq 'Rest') {
130 my ($dt) = $node->findnodes('./durationType');
132 my $type = $dt->to_literal;
133 if ($type eq 'measure') {
134 my ($nz) = $node->findnodes('./duration/@z');
135 my ($nn) = $node->findnodes('./duration/@n');
136 my $was = $nn->to_literal;
138 my $z = $nz->to_literal;
139 display($staff, $measure, $node, "$type $z/$was -> $z/$now") if $dbg;
142 display($staff, $measure, $node, "$type -> $half{$type}") if $dbg;
143 $dt->firstChild->setData($half{$type});
146 } elsif ($name eq 'Chord') {
147 my ($dt) = $node->findnodes('./durationType');
149 my $type = $dt->to_literal;
150 display($staff, $measure, $node, "type $type -> $half{$type}") if $dbg;
151 $dt->firstChild->setData($half{$type});
153 my ($bm) = $node->findnodes('./BeamMode');
155 my $v = $bm->to_literal;
157 display($staff, $measure, $node, "remove BeamMode '$v'") if $dbg;
158 $node->removeChild($bm);
161 my ($lyrics) = $node->findnodes('./Lyrics');
163 my ($ticks) = $lyrics->findnodes('./ticks');
165 my $v = $ticks->to_literal;
167 display($staff, $measure, $node, $lyrics, "ticks $v -> $newv") if $dbg;
168 $ticks->firstChild->setData($newv);
171 # determine where we are in a word and if there is a <syllabic>
172 # clause, note its value (which is "in word" or "not in word")
174 # This is for dealing with musicxml imports where there is no
175 # explicit detection of trailing '-' signs, if there are such signs and
176 # there is no <syllabic> clause, add one of the correct sort and remove
177 # any trailing '-' from the text.
179 # Sadly, it's too much hard work to deal with any trailing '_' 'cos
180 # mscore calulates the distance in advance because they appear
181 # to be too lazy to have another <syllabic> state to deal with
182 # it. Manual edit will therefore be required. Hopefully, not
184 my ($syl) = $lyrics->findnodes('./syllabic');
186 my $v = $syl->to_literal;
187 if ($v eq 'begin' || $v eq 'middle') {
188 display($staff, $measure, $node, $lyrics, "syllabic $v = $syllabic -> 1") if $dbg;
190 } elsif ($v eq 'end') {
191 display($staff, $measure, $node, $lyrics, "syllabic $v = $syllabic -> 0") if $dbg;
195 my ($text) = $lyrics->findnodes('text/text()');
197 my $v = $text->to_literal;
202 $newv = 'begin' unless $syllabic;
203 $newv = 'middle' if $syllabic;
205 $newtext =~ s/[-–]+$//;
207 $newv = 'end' if $syllabic;
211 display($staff, $measure, $node, $lyrics, "text '$v' -> '$newtext' create syllabic $newv sylstate $syllabic -> $newstate") if $dbg;
212 $syllabic = $newstate;
213 $text->setData($newtext) if $v ne $newtext;
214 my $newsyl = $doc->createElement('syllabic');
215 $newsyl->appendText($newv);
216 $lyrics->appendChild($newsyl);
221 } elsif ($name eq 'TimeSig') {
222 my ($sN) = $node->findnodes('./sigN');
223 my ($sD) = $node->findnodes('./sigD');
225 my $sn = $sN->to_literal;
226 my $sd = $sD->to_literal;
228 display($staff, $measure, $node, "$sn/$sd -> $sn/$newsd") if $dbg;
231 $sD->firstChild->setData($newsd);
239 print $of $doc->toString($doc);
247 foreach my $node (@_) {
248 if ((ref $node) =~ /XML/ && $node->nodeType == XML_ELEMENT_NODE) {
249 $s .= $node->nodeName . " ";
250 my @attr = $node->findnodes('@*');
252 $s .= $_->nodeName . " ";
253 $s .= $_->to_literal . " ";
268 my ($name, $path, $suffix) = fileparse($0, qr/\.[^.]*/);
269 $name = "$name$suffix: ";
275 say "${name}version $VERSION usage: [-b] [-x] <filename.msc[xz]> ...\n";
276 say "\tA program to halve the note values of a MuseScore 2.x file.\n";
277 say "\tThis designed to be used to convert 'early music' note values";
278 say "\tinto something more 'modern'. It will also sort out things like";
279 say "\tinter-syllablic hyphenation if it comes across trailing hyphens";
280 say "\tsuch as come from imported Finale musicxml files.";
281 say "\n\tfilenames: 'a.mscz' (or 'a.mscx') will be written to 'a-halved.mscx'.";
282 say "\tYou can do several files at a time!\n";
283 say "\n\tArguments:";
284 say "\t-b - normally any beaming is converted to auto, use this to retain beaming info";
285 say "\t-x - enable debugging (actually more a stream of conscienceness)";