#!/usr/bin/php -n -q
<?php
// XRNS2MIDI version 0.23 by Marvin Tjon (Bantai)
//
// Last modified on 26 March 2010
// Based on XRNS2MIDI version 0.03 by Dac Chartrand
//
// Requires:
// - midi_class_v175
// - xrns2midi_classes.php
// - xrns_functions.php
// To-Do
// - poly-aftertouch for volume
// - effect column commands, eg. 0Exx
$time['start'] = microtime(true);
$xrns2midi_version = "0.23";
// ----------------------------------------------------------------------------
// System Config
// ----------------------------------------------------------------------------
if (!defined('DEBUGGER_VERSION'))
{ //FOR USERS
// Check PHP Version. PHP versions below 5.2.3 may
// lower this script's performance to 3% and even lower!
echo "\nCurrent PHP version: ". PHP_VERSION . "\n";
if (version_compare(PHP_VERSION, "5.2.3", "<"))
{
echo "\n*=====================================*\n";
echo "\n WARNING: Update your PHP installation!\n\n PHP versions below 5.2.3 may increase\n execution time by a factor 30 to 90.\n\n";
echo "*=====================================*\n";
sleep(3);
}
// Script usually exectues in a few seconds. During the conversion from xml to mid,
// the script may hang if the midi events are incorrectly sorted.
set_time_limit(240); //4 minutes
// Validate song
$schema_check = true;
}
else
{ //FOR DEBUGGERS
// Do not validate song
$schema_check = false;
}
// ----------------------------------------------------------------------------
// Variables
// ----------------------------------------------------------------------------
// Files are extracted in the root temp folder.
$tmp_dir = '/temp';
// Exit status
$err = 0;
// ----------------------------------------------------------------------------
// Requires
// ----------------------------------------------------------------------------
require_once('xrns_functions.php');
require_once('midi_class_v175/classes/midi.class.php');
require_once("xrns2midi_classes.php");
// ----------------------------------------------------------------------------
// Check Variables
// ----------------------------------------------------------------------------
// get filename component of path
if (isset($argv[0])) $argv[0] = basename($argv[0]);
else $argv[0] = $_SERVER['SCRIPT_NAME'];
if (!is_dir($tmp_dir)) {
$tmp_dir = get_temp_dir();
if (!$tmp_dir) die("Error: Please set \$tmp_dir in $argv[0] to an existing directory.\n");
}
// ----------------------------------------------------------------------------
// Check User Input
// ----------------------------------------------------------------------------
/*
POSSIBLE FUTURE OPTIONS
1. export instrument
2. export unique patterns only (disregarding Pattern Sequence)
3. inter-instrument `NNA: Cut` (notes from one instrument kills notes from the other instrument)
4. include effects
5. line-expansion style: tempo change or line-expansion
*/
if (isset($argv[1]) && !isset($argv[2]) || isset($argv[2]) && !isset($argv[1]))
{
echo "Error: $argv[0] expects at least 2 parameters.\n";
echo "Usage: `php $argv[0] /path/to/input.xrns output.mid`\n";
echo "$argv[0] will output output.mid, output.txt and output.xml to the current working directory.\n";
die();
}
elseif (isset($argv[1]) && isset($argv[2]))
{ // Specify by command line
if (!file_exists($argv[1]))
die("Error: Input file `".$argv[1]."` not found.\n");
$xrns_file = $argv[1];
$mid_file = $argv[2];
}
elseif (isset($_GET['xrns']) && isset($_GET['mid']))
{ //Server version: xrns2midi.php?xrns=filename.xrns&mid=filename.mid
//TODO: security, form, upload
if (!file_exists($_GET['xrns']))
die("Error: Input file `".$_GET['xrns']."` not found.\n");
$xrns_file = $_GET['xrns'];
$mid_file = $_GET['mid'];
}
else
{ //Specify manually
$xrns_file = "C:/[bb5entries]/test/ff00.xrns";
$mid_file = "C:/[bb5entries]/test/ff00.$xrns2midi_version.mid";
if (!file_exists($xrns_file))
die("Error: Input file not found.\n");
}
// ----------------------------------------------------------------------------
// Unpack
// ----------------------------------------------------------------------------
echo "\n---------------------------------------\n";
echo "XRNS2MIDI v$xrns2midi_version is working...\n";
echo date("D M j G:i:s T Y\n");
echo "---------------------------------------\n\n";
echo "Using temporary directory: $tmp_dir\n";
echo "Input: $xrns_file \nOutput: $mid_file (+.xml, +.txt)\n\n";
// Create a unique directory
$unzip1 = $tmp_dir . '/xrns2midi_' . md5(uniqid(mt_rand(), true)) . '_Track01/';
// Unzip song1
$result = UnzipAllFiles($xrns_file, $unzip1);
if($result === FALSE) {
echo "Error: There was a problem unzipping the first file.\n";
die();
}
// Load XML
$sx = simplexml_load_file($unzip1 . 'Song.xml');
// ----------------------------------------------------------------------------
// Validate XRNS against schema
// ----------------------------------------------------------------------------
echo "XRNS Document Version: " . $doc_version = $sx['doc_version'] . "\n";
if ($schema_check)
{
if (!xrns_xsd_check($sx, $doc_version))
{
obliterate_directory($unzip1);
die("\n". $xrns_file . " failed to validate against schema. The song appears to be broken.\n\n" );
}
else
{
echo "Document has succesfully passed validation.\n";
//unfortunately also displayed when the schema is not found
}
}
else
{
echo "Document validation disabled.\n";
}
echo "\n";
// ----------------------------------------------------------------------------
// Create a data structure array for easier conversion
// ----------------------------------------------------------------------------
$time['collection_start'] = microtime(true);
// ----------------------------------------------------------------------------
// Timing algorithms
// Convert Renoise bpm to MIDI bpm
// ----------------------------------------------------------------------------
//initial bpm and speed
$bpm = (int)$sx->GlobalSongData->BeatsPerMin;
$speed = (int)$sx->GlobalSongData->TicksPerLine;
// New LPB or old speed?
if ($sx['doc_version'] >= 14) $lpb = (int)$sx->GlobalSongData->LinesPerBeat;
else $lpb = 0;
$master = new MasterTrack($bpm, $speed, $lpb);
$midiList = new MidiList;
$midiList->addMaster($master);
/*
MidiList
MidiTracks
id (#miditrack)
instr (instrument number)
name (instrument name)
channel
MidiEvents[line][tick][originating note column][subevent]
getMsg()
*/
// ================================================================
// PARSE SEQUENCE
// ================================================================
$seq = array();
if ($sx->PatternSequence->SequenceEntries->SequenceEntry) {
// Renoise XSD, doc_version 21
for ($i = 0; $i < count($sx->PatternSequence->SequenceEntries->SequenceEntry); ++$i) {
for ($j = 0; $j < count($sx->PatternSequence->SequenceEntries->SequenceEntry[$i]->Pattern); ++$j) {
$seq[] = (int)$sx->PatternSequence->SequenceEntries->SequenceEntry[$i]->Pattern[$j];
}
}
}
else
{
foreach ($sx->PatternSequence->PatternSequence->Pattern as $p)
{
$seq[] = (int)$p;
}
}
// ================================================================
// PARSE PATTERNS
// ================================================================
$pn = 0;
$lines = 0;
foreach ($seq as $pn=>&$patternid)
{
$p = $sx->PatternPool->Patterns->Pattern[$patternid];
//foreach ($sx->PatternPool->Patterns->Pattern as $p) {
$track = 0; // reset track
$NumberOfLines[$pn] = (int)$p->NumberOfLines;
//$patternid = (int)$sx->PatternSequence->PatternSequence->Pattern[$pn];
if (isset($p->Name)) $patternName = (string)$p->Name; else $patternName = '';
echo " Parsing pattern [$pn]: $patternid $patternName... \n";
$master->addEventArr($lines,0, 0,11,array('type'=>'Marker', 'string'=>"Pattern [$pn]: $patternid $patternName"));
foreach (array($p->Tracks->PatternTrack,$p->Tracks->PatternMasterTrack, $p->Tracks->PatternSendTrack) as $tx)
foreach ($tx as $x){
if ($x->Lines->Line) foreach ($x->Lines->Line as $y) {
if ((int)$y['index'] > $NumberOfLines[$pn] - 1)
break;
//position in absolute lines from start
$pos = (int) ($y['index'] + $lines);
// ----------------------------------------------------------------
// Note Columns
// ----------------------------------------------------------------
$trk = null;
$n_column = 100 * $track;
$nn=0;
if ($y->NoteColumns->NoteColumn) foreach ($y->NoteColumns->NoteColumn as $z)
{
$n_column += $nn++;
// Parse note string into note number
if($z->Note)
$note = $master->str2note((string)$z->Note);
else
$note = null;
// Fetch Instrument Number
if($z->Instrument)
{
//midi tracks start with 1, so instr has to be inc by 1
$inst = (int) intval((string) $z->Instrument, 16) + 1;
//$id = $inst + 1; //midi track number
// Instrument Name and other settings
if (($trk = MidiList::getTrack($inst)) === false)
{
//create new track and receive a reference to it
$trk = $midiList->addTrack($inst);
if (isset($sx->Instruments->Instrument[$inst-1]->Name))
$trk->setName(sprintf("%02X: %s", $inst-1, (string) $sx->Instruments->Instrument[$inst-1]->Name));
else
$trk->setName(sprintf("%02X: Unnamed", $inst-1));
}
}
else
$inst = 0;
if ($note && $note != 'Off' && $inst != 0)
{
//NNA: note-cut (only by this instr)
$lnF = MasterTrack::getLastNote($n_column); //find last note originating from this note-column
if ($lnF !== false && $lnF->evttype != 2) //if there is a new note with instr number and there is a note before it
{
//OPTION: NNA forced note-off by any subsequent note in the note-column
//TODO: tick-accurate note-off in case current note is delayed
$evt = clone $lnF; //make a copy of the last note
$evt->evttype = 2; //set it to note-off
$evt->vol = 0; //another way to set note-off
$evt->setOffset(0);
$evt->retrig = null;
if (false) //bypass note-off offset for now
{
$evt->setOffset(-1); //put the note-off n ticks before the new note
//by convention, if the offset is negative, we decrease $pos by 1 line
MidiList::getTrack($evt->inst)->addEvent($pos-1, -1, $n_column, $evt);
}
else
MidiList::getTrack($evt->inst)->addEvent($pos, 0, $n_column, $evt);
}
//if the current midicol is occupied, create a new event on a new midicol
//$evt is the last element in the events list
$evt = $trk->addEventArr($pos, 0, $n_column, 1, array('note'=>$note));
}
elseif ($note && $note == 'Off')
{
// Off Note; ignore note-offs at beginning of midicol
$lnF = MasterTrack::getLastNote($n_column); //find last note originating from this note-column
if ($lnF !== false && $lnF->evttype != 2) //if there is a new note with instr number and there is a note before it
{
$evt = clone $lnF; //make a copy of the last note
$evt->evttype = 2; //set it to note-off
$evt->vol = 0; //another way to set note-off
MidiList::getTrack($evt->inst)->addEvent($pos, 0,$n_column, $evt);
}
}
else if ($note && $inst != 0)
{
//ghost note
//Renoise disallows instr numbers on note-offs, but not vol/pan
}
else if (!$note && $inst == 0)
{
//midi cc
//instr switch feature?
}
else
{
// Do something with empty note?
}
// ----------------------------------------------------------------
// Volume, Panning
// ----------------------------------------------------------------
//applies to closest note of any instr in this col
//TODO: vol/pan fx without note/instr
if($lastnote = MasterTrack::getLastNote($n_column))
{
$master->doVolPanFx($pos, $z->Volume, $z->Panning, $lastnote);
if ($z->Panning == 90)
$midicc = true;
else
$midicc = false;
}
$n_column++;
} // end note columns
// ----------------------------------------------------------------
// Effect Columns
// ----------------------------------------------------------------
if ($y->EffectColumns->EffectColumn)
{
foreach ($y->EffectColumns->EffectColumn as $z)
{
if ($midicc !== false){
//new midicc evt
//track -> addEvent($pos, 0,$n_column, $evt);
//midicc are always in the first effect column, so reset
$midicc = false;
continue;
}
//fetch hexadecimal pattern command from song.xrns
$effectNumber = (int)intval((string)$z->Number,16);
$effectValue = (int)intval((string)$z->Value,16);
//add pattern command to events on line in col
//apply global pattern commands to midi master track
switch ($effectNumber)
{
case 0x0C: //track volume
break;
case 0xF0:
if ($effectValue >= 0x20 && $effectValue <= 0xFF)
{
$master->globals['bpm'][$pos] = $effectValue;
}
break;
case 0xF1:
if ($effectValue > 0x00 && $effectValue <= 0x1F)
{
if ($sx['doc_version'] >= 14)
{
// New LPB method
$master->globals['lpb'][$pos] = hexdec($effectValue);
}
else
{
// Old Speed Method
$master->globals['speed'][$pos] = $effectValue;
$master->globals['lpb'][$pos] = $master->getLpb($effectValue);
}
}
elseif ($effectValue == 0)
{ // F100 (Stop Song)
//ignore command for now
//$master->addEvent($pos,0,0, new MidiEvent(12, null));
}
break;
case 0xFC: //master volume
break;
//case 0xFB - Pattern break //TODO: automation follow
//case 0xFD - Delay complete pattern xx lines.
case 0xFF: //Stop all track effects and notes on track.
//Very hard to do, because notes in the track may be spreaded over several midi channels.
//$master->addEvent($pos,0,0, new MidiEvent(5, array('ch'=>4, 'val'=>0)));
break;
}
}
}
} //end lines
++$track;
} // end tracks
$pn++;
$lines += $p->NumberOfLines;
} //end patterns
$time['collection_stop'] = microtime(true);
$ms = round(($time['collection_stop'] - $time['collection_start']) * 1000);
echo "__Collected events from Song.xml in {$ms}ms \n";
//calculate LPB for every speed/tempo effect
$master->commitGlobals(0,$lines);
$time['commit_stop'] = microtime(true);
$ms = round(($time['commit_stop'] - $time['collection_stop']) * 1000);
echo "\nCommited global events in {$ms}ms \n\n";
//forced note-off on every column at end of song
foreach(MasterTrack::$lastNotes as $n_column=>&$evt)
{
if ($evt->evttype == 2)
continue;
$evt = clone $evt;
$evt->evttype = 2;
$evt->vol = 0;
$evt->offset = 0;
MidiList::getTrack($evt->inst)->addEvent($lines, 0, $n_column, $evt);
}
//add Pattern Sequencer loop
if (($cp = (int)$sx->PatternSequence->LoopSelection->CursorPos) > -1)
{
$rp = (int)$sx->PatternSequence->LoopSelection->RangePos;
$loopstart = 0;
for ($i=0; $i<$cp;$i++)
$loopstart += $NumberOfLines[$i];
$loopend = $loopstart;
for ($i=$cp; $i<$rp;$i++)
$loopend += $NumberOfLines[$i];
$master->addEventArr($loopstart,0,0,11,array('type'=>'Marker', 'string'=>'Loop start'));
$master->addEventArr($loopend,0,0,11,array('type'=>'Marker', 'string'=>'Loop end'));
}
//Sorting MidiList is useless; midi tracks are generated in order of
//the instrument's first appearance in the song.
//Take notice: use references in foreach, or the updates to the arrays
//will be ignored!
foreach($midiList as &$miditrack)
{
$id = $miditrack->id;
echo " Sending events for MidiTrack $id...\n";
foreach ($miditrack as $pos=>&$ticks)
{
//echo "Line: $pos \n";
//TODO: remove previous data
foreach ($ticks as $tick=>&$events)
{
//echo "\tTick: $tick \n";
foreach ($events as &$subevents)
{
foreach ($subevents as $evtnum=>&$evt)
{
//note-delay
//if offset is valid, move event to tick position
//if offset exceeds speed, do event on this tick, further down this loop
if (isset($evt->offset) && $evt->offset != 0)
{
$offset = $evt->offset;
$evt->offset = 0;
$speed = $master->getNearestGlobal($pos, 'speed')->val;
if ($speed > $offset)
{
$ticks[$offset][$evt->col][] = $evt;
unset($subevents[$evtnum]);
ksort($ticks);
continue;
}
}
//everything that comes after this needs this timestamp
$timestamp = $master->getTimeStamp($pos,$tick);
$evt->setTimestamp($timestamp);
//retrigger
if (isset($evt->retrig) && $evt->retrig != 0 && $evt->evttype != 2)
{
$retrig = $evt->retrig;
$evt->retrig = 0;
$speed = $master->getNearestGlobal($pos, 'speed')->val;
if ($speed > $retrig)
{
$offset = 0;
$ticksleft = $speed;
unset($lastevt);
while($ticksleft > 0)
{
if (isset($lastevt))
{
$lastevt->evttype = 2;
$ticks[$offset][$lastevt->col][] = $lastevt;
}
if ($offset == 0)
MasterTrack::$midi->addMsg($id, $evt->getMsg());
else
{
$ticks[$offset][$evt->col][] = $evt;
}
$ticksleft -= $retrig;
$offset += $retrig;
$lastevt = clone $evt;
}
ksort($ticks);
unset($subevents[$evtnum]);
continue; //this is event is done, so skip the rest
}
}
MasterTrack::$midi->addMsg($id, $evt->getMsg());
unset($subevents[$evtnum]);
}
//unset($subevents);
}
//unset($events);
}
//unset($ticks);
}
//All notes off MIDI command
//if ($id > 0)
// MasterTrack::$midi->addMsg($id, $master->getTimestamp($lines, null) . " Par ch=$id c=123 v=0");
//MIDI Meta Event: Track End
MasterTrack::$midi->addMsg($id, $master->getTimestamp($lines, null)+1 . " Meta TrkEnd");
}
$time['msg_stop'] = microtime(true);
$ms = round(($time['msg_stop'] - $time['commit_stop']) * 1000);
echo "__Completed sending events to MIDI Class in {$ms}ms \n";
// ----------------------------------------------------------------------------
// Remove temp directories, Make MIDI file
// ----------------------------------------------------------------------------
// Remove temp dir
obliterate_directory($unzip1);
// Outputting logs
$txt = MasterTrack::$midi->getTxt();
file_put_contents($mid_file.'.txt', $txt);
$xml = MasterTrack::$midi->getXML();
file_put_contents($mid_file.'.xml', $xml);
// Convert text to midi
$time['mid_start'] = microtime(true);
try
{
MasterTrack::$midi->saveMidFile($mid_file);
}
catch (Exception $e)
{
echo $e->getMessage() . "\n";
echo $e->getTraceAsString();
$err = 1;
}
$time['mid_stop'] = microtime(true);
$ms = round(($time['mid_stop'] - $time['mid_start']) * 1000);
echo "\nExported to MIDI file in {$ms}ms\n";
$time['stop'] = microtime(true);
$ms = round(($time['stop'] - $time['start']), 3);
echo "\nTotal execution time: {$ms} seconds\n";
echo "\n---------------------------------------\n";
echo "$argv[0] is done!\n";
echo date("D M j G:i:s T Y\n");
echo "---------------------------------------\n";
exit($err);
?>