When NWC imports a MIDI file it does not faithfully represent triplets; correcting them manually is a tedious business so I have written a User Tool to do this.
It depends on correctly recognising the approximations that NWC generates during the import process. My current understanding of these is based on examples that I have exported and re-imported. I invite those who know NWC from the inside to help me to do better.
-- Brian
<?php
/*******************************************************************************
brm_Triplify Version 1.00
This script will seek sequences that NWC has created from importing a MIDI file containing
triplets and convert those sequences into triplets.
History:
[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 descending size
$validLengthGroup = array (
array(6,5,5),
array(3,3,2),
array(8,5,3), // a very poor and problematic representation of a triplet, but it does occur e.g.
// with triplet (crotchet, crotchet rest, crotchet)=(8,3,5). However, in the quaver version of this
// example the rest has disappeared altogether (3,NULL,1). so we have no chance!
array(11,5),
array(5,3)
);
function is_valid_group ($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);
if ($factor) foreach($lengthSet as $i=>$v) $lengthSet[$i] /= 3;
}
// then sort the array descending to match the convention in definition of validLengthGroup
rsort($lengthSet);
// now we are ready to check for a valid group of lengths
foreach ($validLengthGroup as $thisgroup) {
$result = false; // 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() == "Rest") return false; // this is caught anyway
$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);
}
// Track the number of conversions
$numConvertedTriplets = 0;
//
echo $clip->GetClipHeader()."\n";
// Use arrays $TripletQ and $lengthSet to hold candidates
$TripletQ = array();
$lengthSet = array();
$tied_note_pending=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_grace = isset($o->Opts["Dur"]["Grace"]);
$is_triplet = isset($o->Opts["Dur"]["Triplet"]); // check this
$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
foreach ($tickLength as $notename => $value) {
if (isset($opts["Dur"][$notename])) {
$length = $value;
}
}
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;
} 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 (is_valid_group($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;
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 (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"]);
echo $this->ReconstructClipText()."\n";
$length_index++;
}
$TripletQ = array(); $lengthSet = array(); $tied_note_pending=false;
break; // out of the while loop
} elseif (sizeof($lengthSet)<3) { // not a triplet yet but there is still time so store data
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);
$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
} 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;
} // end if is_note else
} // end for each clip
if ($TripletQ) foreach($TripletQ as $this) {
echo $this->ReconstructClipText()."\n";
}
echo NWC2_ENDCLIP."\n";
if (!$numConvertedTriplets) {
fputs(STDERR,"No valid triplets were found within the selection");
exit(NWC2RC_ERROR);
}
exit(NWC2RC_SUCCESS);
?>