I have now revised my User Tool to deal with approximations to triplets imported by NWC from a MIDI file; the new version appears below (brm_triplify).
The main change is to recognise that not everybody will import their MIDI file using the default parameters, so the tool certainly has to cope with finer resolutions. Coarser resolutions are problematic because as the approximation to a perfect triplet gets worse, the risk of falsely identifying a sequence of notes as a triplet becomes greater and there comes a point where it is best to skip these.
The other change is to handle concatenated rests. The need for this really only becomes apparent at high resolution and it is tricky: adjacent rests behave like tied notes, except that there is no indication which ones are notionally tied together. It gets worse: NWC will happily generate, for example, a dotted 4th rest, where one component - the 4th rest - does not form part of a triplet but the dotted bit - an 8th rest - does. I hope it doesn't churn out double-dotted rests - I could cater for them but I haven't.
I attach my test file which contains, on the top stave, a variety of triplets and, on the other staves, the result of exporting and re-importing at different resolutions. No parameters are required when invoking the tool; it will work with what it is given and convert to triplets any sequence that can safely be identified.
Lawrie drew my attention to Andrew Purdam's "tripletise" user tool but this requires very specific steering; for example the first triplet in my test file, at 64th note resolution (on the bottom stave) requires steering parameters "4 8t d32 16t 64t 16t 64 8t d32". It took longer to work that out than to do the edit manually, especially since it dealt with only one other triplet in the clip. I wanted something that will automatically unscramble as much as possible of the mess that NWC makes of importing triplets, with as little intervention as possible by the user.
I have no doubt that there are other triplet 'signatures' that I should have included but haven't; that is what the Newsgroup is for!
Finally, since it hasn't evoked a reponse yet, I repeat the question: why doesn't NWC make a better job of importing triplets in the first place, when exact information is available?
-- Brian
<?php
/*******************************************************************************
brm_Triplify Version 1.01
Seeks the triplet approximation sequences that NWC has created when importing a MIDI file
and converts those sequences into normal triplets.
History:
[2009-02-14] Version 1.01 - Recognition of triplets from a wider range of MIDI import
resolutions and handling of concatenated rests
[2009-02-12] Version 1.00 - Initial version
*******************************************************************************/
require_once("lib/nwc2clips.inc");
$clip = new NWC2Clip('php://stdin');
// we shall need to assess the lengths of several notes etc, including tied notes, in units of ticks
$ticks_per_crotchet = 16; // as small as possible, since we will not be representing triplets
$tickLength = array(
"Whole"=> $ticks_per_crotchet *4,
"Half" => $ticks_per_crotchet *2,
"4th" => $ticks_per_crotchet,
"8th" => $ticks_per_crotchet /2,
"16th" => $ticks_per_crotchet /4,
"32nd" => $ticks_per_crotchet /8,
"64th" => $ticks_per_crotchet /16
);
// NWC's efforts in terms of representing a triplet come from examples. There may be more to come.
// When adding to this array, always insert sub-array elements in the order they have been observed. Also beware that
// the further these ratios stray from (1,1,1),(2,1) or (1,2) the greater is the risk of false positive triplet identification
$validLengthGroup = array (
array(6,5,5),
array(5,6,5),
array(3,3,2),
array(8,3,5), // a very poor and potentially problematic representation of a triplet, but it does occur e.g.
// with triplet (crotchet, crotchet rest, crotchet)=(8,3,5) if the import resolution is a bit low
array(11,10,11),
array(19,24,21),
array(21,11),
array(11,21),
array(11,5),
array(5,11),
array(3,5)
);
function isValidGroup ($lengthSet) {
global $validLengthGroup;
// first reduce the array by its HCF
if (sizeof($lengthSet) <= 1) return false;
$factor = true;
while($factor) {
foreach($lengthSet as $v) $factor &= !($v % 2);
if ($factor) foreach($lengthSet as $i=>$v) $lengthSet[$i] /= 2;
}
$factor = true;
while($factor) {
foreach($lengthSet as $v) $factor &= !($v % 3); // just in case ticks_per_crotchet ever aquires a factor 3!
if ($factor) foreach($lengthSet as $i=>$v) $lengthSet[$i] /= 3;
}
// now we are ready to check for a valid group of lengths
foreach ($validLengthGroup as $thisgroup) {
$result = false; // just in case the last group has different length to the array under test
if (sizeof($lengthSet) != sizeof($thisgroup)) continue;
$result = true; // assume a match until a mis-match is found
foreach($lengthSet as $i=>$v) if ($lengthSet[$i] != $thisgroup[$i]) $result=false;
if ($result) break; // found the winner, no need to go on searching
}
return $result;
}
function isTiedNote($arg) {
if (($arg->GetObjType() != "Note") &&
($arg->GetObjType() != "Chord") &&
($arg->GetObjType() != "RestChord")) return false;
$opts = $arg->GetOpts();
if (isset($opts["Pos"])) {
$pos = $opts["Pos"];
if ($arg->GetObjType() == "Note") {
$n = new NWC2NotePitchPos($pos);
$ret = false;
if ($n->Tied) $ret = true;
unset($n);
} else { // must be a Chord, so Pos is not a string but an array of strings
$ret = false;
foreach($opts["Pos"] as $k=>$v) {
$n = new NWC2NotePitchPos($v);
if ($n->Tied) $ret = true; // in practice, expect all or none to be tied
unset($n);
}
}
} else $ret=false;
return ($ret);
}
function note_length ($arg) {
// returns the base length of the item;s Dur - we deal with dotted etc elsewhere
global $tickLength;
$opts = $arg->GetOpts();
foreach ($tickLength as $notename => $value) if (isset($opts["Dur"][$notename])) $length = $value;
return $length;
}
// Track the number of conversions
$numConvertedTriplets = 0;
// MAIN PROGRAM
echo $clip->GetClipHeader()."\n";
// Use arrays $TripletQ and $lengthSet to hold candidates
$TripletQ = array();
$lengthSet = array();
$tied_note_pending=false; $last_item_was_rest = false;
foreach ($clip->Items as $item) {
$o = new NWC2ClipItem($item);
$opts = $o->GetOpts();
$is_note = in_array($o->GetObjType(), array("Chord","Note","Rest","RestChord")); // but there won't be a RestChord!
$is_rest = ($o->GetObjType() == "Rest");
$is_grace = isset($o->Opts["Dur"]["Grace"]);
$is_triplet = isset($o->Opts["Dur"]["Triplet"]);
$is_tied = isTiedNote($o) ;
$is_dotted = isset($o->Opts["Dur"]["Dotted"]);
$is_dbldotted = isset($o->Opts["Dur"]["DblDotted"]);
if ($is_note && !$is_grace && !$is_triplet) {
if ($TripletQ) array_push($TripletQ,$o); else $TripletQ = array($o); // whatever happens, remember the note
// evaluate its length
$length = note_length($o);
if ($is_dotted) $length *= 3/2;
elseif ($is_dbldotted) $length *= 7/4;
// there is at least one candidate in the queue, record length and check for triplet
if ($tied_note_pending) { // add its length to that stored for the previous note
$last_element = sizeof($lengthSet)-1;
$lengthSet[$last_element] += $length;
} elseif ($is_rest && $last_note_was_rest) { // similarly add length
if (sizeof($lengthSet)==0) array_push($lengthSet, $length); // this really shouldn't happen!
else { $last_element = sizeof($lengthSet)-1; $lengthSet[$last_element] += $length;}
} else array_push($lengthSet, $length);
if ($tied_note_pending = $is_tied) continue; // yes, really not "=="! this is ready for the next lap
// check if we have a triplet
while (true) { // start a loop so we can retest having discarded one note
if (isValidGroup($lengthSet)) { //we have a triplet, output the notes in modified form
$numConvertedTriplets++;
$length = array_sum($lengthSet)/2; // this is the un-triplet-ised length of a single element
// $dur = array_search($tickLength, $length); WHY DOESN'T THIS WORK? use alternative code
foreach($tickLength as $key => $value) if ($value == $length) {$dur = $key; break; }
// $durxtwo = array_search($tickLength, $length*2); WHY DOESN'T THIS WORK?
foreach($tickLength as $key => $value) if ($value == $length *2) {$durxtwo = $key; break; }
// Output the triplet, being two or three notes/rests/chords
$output_tied_note_pending = false; $length_index = 0; $last_output_note_was_rest = false;
foreach($TripletQ as $this) {
if ($output_tied_note_pending) {
$output_tied_note_pending = isTiedNote($this);
continue; // dumping this note and processing the next item in the queue
}
if ($last_output_note_was_rest && ($this->GetObjType() == "Rest")) {
// $last_output_note_was_rest is set anyway
continue; // dumping this partial rest and processing the next item
}
if (isset($this->Opts["Opts"]["Beam"])) unset($this->Opts["Opts"]["Beam"]);
if (isset($this->Opts["Opts"]["Stem"])) unset($this->Opts["Opts"]["Stem"]); // triplet stems don't need to be aligned
// can't get rid of Opts altogether - a rest might have Opts["VertOffset"] set - not likely though
if (isset($this->Opts["Dur"]["Dotted"])) unset($this->Opts["Dur"]["Dotted"]); // a dotted triplet makes no sense
if (isset($this->Opts["Dur"]["DblDotted"])) unset($this->Opts["Dur"]["DblDotted"]); // nor does this!
if (sizeof($lengthSet)==3) $this->Opts["Dur"] = array($dur => "","Triplet" => "");
elseif ($lengthSet[0] > $lengthSet[1]) { // first note is of double duration
$this->Opts["Dur"] = array ((($length_index == 0) ? $durxtwo : $dur)=> "","Triplet" => "");
} else { // second note is of double duration
$this->Opts["Dur"] = array ((($length_index == 0) ? $dur : $durxtwo)=> "","Triplet" => "");
}
if ($length_index == 0) $this->Opts["Dur"]["Triplet"] = "First";
if ($length_index == (sizeof($lengthSet)-1)) $this->Opts["Dur"]["Triplet"] = "End";
$output_tied_note_pending = isTiedNote($this);
// un-tie the note/chord if it is tied
if (isset($this->Opts["Pos"]) && $output_tied_note_pending) { // i.e. not a rest
if ($this->GetObjType() == "Note") {
$pos = new NWC2NotePitchPos($this->Opts["Pos"]);
$pos->Tied=false;
$this->Opts["Pos"] = $pos->ReconstructClipText();
unset($pos);
} else { // a Chord, must deal with all the Pos elements
foreach($this->Opts["Pos"] as $k=>$v) {
$pos = new NWC2NotePitchPos($v);
$pos->Tied = false;
$this->Opts["Pos"][$k] = $pos->ReconstructClipText();
unset($pos);
}
}
}
if ($this->GetObjType() == "Rest"){
unset($this->Opts["Pos"]);
$last_output_note_was_rest = ($this->GetObjType() == "Rest");
} else $last_output_note_was_rest = false;
echo $this->ReconstructClipText()."\n";
$length_index++;
}
$TripletQ = array(); $lengthSet = array(); $tied_note_pending=false; $last_output_note_was_rest=false;
break; // out of the while loop
} elseif ((sizeof($lengthSet)<3) || ($is_rest)) { // not a triplet yet but there is still time
$last_note_was_rest = $is_rest;
continue 2; // breaking out of the while loop to get a new item from the clip
} else { // not a triplet so output the first note, drop it from the stored queue and retest
$output_tied_note_pending = true;
while ($output_tied_note_pending) {
$this = array_shift($TripletQ);
if (($this->GetObjType() == "Rest") && (note_length($this) < $lengthSet[0])) {
// now it gets complicated because if the partial rest we have just removed from the queue was dotted
// we should only strip off 2/3 of it and put the rest back in the queue!
// NB ignore the double-dotted case - I have yet to see one of these!
$this_opts = $this->GetOpts();
$this_is_dotted = isset($this->Opts["Dur"]["Dotted"]);
if ($this_is_dotted) { // the rest to be output is of this duration but not dotted
unset($this->Opts["Dur"]["Dotted"]);
echo $this->ReconstructClipText()."\n";
$lengthSet[0] -= note_length($this);
$this_length = note_length($this);
// $this_dur = array_search($tickLength, $length); WHY DOESN'T THIS WORK? use alternative code
foreach($tickLength as $key => $value) if ($value == $this_length) {$this_dur = $key; break; }
unset($this->Opts["Dur"][$this_dur]);
$this_length /= 2;
// $this_dur = array_search($tickLength, $length); WHY DOESN'T THIS WORK? use alternative code
foreach($tickLength as $key => $value) if ($value == $this_length) {$this_dur = $key; break; }
$this->Opts["Dur"][$this_dur]="";
$new_size = array_unshift($TripletQ,$this);
continue 2; // break out of this while loop and retest for a triplet
} else { // not dotted so it's much simpler
echo $this->ReconstructClipText()."\n";
$lengthSet[0] -= note_length($this);
continue 2; // break out of this while loop and retest for a triplet
}
} else { // a note, chord or a single rest
$output_tied_note_pending = isTiedNote($this);
echo $this->ReconstructClipText()."\n";
}
}
$this = array_shift($lengthSet); // dump its length too
// Now we must retest because the remaining notes, if any, could be a (2 note) triplet
if ($TripletQ) continue; // repeat the while loop
} // end if triplet, maybe triplet or not triplet
} // end while(true)loop
$last_note_was_rest = $is_rest;
} else { // not a note, sequence is spoiled so output everything in the queue, plus this non-note item and start afresh
if ($TripletQ) foreach($TripletQ as $this) {
echo $this->ReconstructClipText()."\n";
}
echo $o->ReconstructClipText()."\n";
$TripletQ = array(); $lengthSet = array(); $tied_note_pending=false; $last_note_was_rest = false;
} // end if is_note else
unset($o); //???
} // end for each clip
if ($TripletQ) foreach($TripletQ as $this) {
echo $this->ReconstructClipText()."\n";
}
echo NWC2_ENDCLIP."\n";
if (!$numConvertedTriplets) {
fputs(STDERR,"No triplets were found within the selection. Check the MIDI import resolution used.");
exit(NWC2RC_ERROR);
}
exit(NWC2RC_SUCCESS);
?>