Location: PHPKode > projects > XRNS-PHP > xrns2midi.php
#!/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);

?>
Return current item: XRNS-PHP