<?php
// XRNS2MID version 0.23 by Marvin Tjon (Bantai)
//
// Last modified on 26 March 2010
// Based on XRNS2MID version 0.03 by Dac Chartrand
//
// This file contains helper classes for the main script:
// - xrns2midi.php
/**
* Contains an array of MidiTracks
*/
class MidiList implements IteratorAggregate
{
public static $miditracks = array();
private $n = 0;
public static $midi;
/**
* Implemented iterator to allow custom iteration over an instance of this class
*
* @return MidiTrack
*/
public function getIterator() {
$arrayObj=new ArrayObject(self::$miditracks);
return $arrayObj->getIterator();
}
/**
* Adds the MasterTrack to the MidiList
*
* @param MasterTrack $master
*/
function addMaster($master)
{
self::$miditracks[0] = $master;
}
/**
* Create new Midi Track identified by (instrument number + 1)
*
* @param int $inst
* @return MidiTrack
*/
function addTrack($inst)
{
$this->n++;
self::$miditracks[$inst] = new MidiTrack($this->n, $inst);
return end(self::$miditracks);
}
/**
* Retrieve Midi Track identified by (instrument number + 1)
*
* @param int $inst
* @return MidiTrack
*/
static function getTrack($inst)
{
if (array_key_exists($inst, self::$miditracks))
return self::$miditracks[$inst];
else
return false;
}
/**
* Fetch Midi Track labeled with its own id
*
*/
function getTrackById()
{
// not yet implemented
}
}
class MidiEvent
{
public $id;
public $inst;
public $evttype;
public $timestamp = 0;
public $offset = 0;
public $retrig;
public $col;
public $track;
public $ch;
public $note;
public $vol = 0x7F;
public $val;
public $con;
public $cc;
public $prog;
public $hex; //decimal long
public $num; //2-digit hex without 0x, separated by space
public $pan = 0x80;
public $type; //either 0xab code, or oen of Text Copyright SeqName TrkName InstrName Lyric Marker Cue
public $string;
/*
1 Note On: On <ch> <note> <vol>
2 Note Off: Off <ch> <note> <vol>
3 Poly Pressure: PoPr[PolyPr] <ch> <note> <val>
4 Channel Pressure: ChPr[ChanPr] <ch> <val>
5 Controller parameter: Par[Param] <ch> <con> <val>
6 Pitch bend: Pb <ch> <val>
7 Program change: PrCh[ProgCh] <ch> <prog>
8 Sysex message: SysEx <hex>
9 Tempo: Tempo <num> (decimal long)
10 Meta hex: Meta <type> <hex>
11 Meta text: Meta <texttype> <string>
*/
function __construct($evttype, $arr=null)
{
$this->evttype = $evttype;
if (is_array($arr))
foreach ($arr as $key => $val)
$this->$key = $val;
}
/**
* Returns a TF2M string, ready to be parsed to .mid format or MidiXML
*
* @return String
*/
function getMsg()
{
switch($this->evttype)
{
case 1:
$msg = ' On ch=' . $this->ch . ' n=' . $this->note . ' v=' . $this->vol;
break;
case 2:
$msg = ' Off ch=' . $this->ch . ' n=' . $this->note . ' v=' . $this->vol;
break;
case 5:
$msg = ' Par ch=' . $this->ch . ' c=' . $this->con . ' v=' . $this->val;
break;
case 8:
$msg = ' SysEx ' . $this->hex;
break;
case 9:
$msg = ' Tempo ' . $this->num;
break;
case 10:
$msg = ' Meta ' . $this->type . ' ' . $this->hex; //meta hex code
break;
case 11:
$msg = ' Meta ' . $this->type . ' "' . $this->string. '"'; //text between ''
break;
case 12:
$msg = ' Stop';
break;
}
return ($this->timestamp . $msg);
}
/**
* Sets timestamp
*
* @param int $timestamp
*/
function setTimeStamp($timestamp)
{
$this->timestamp = $timestamp;
}
/**
* Sets offset in ticks, for retrigger, delay, etc
*
* @param int $ticks
*/
function setOffset($ticks)
{
$this->offset = $ticks;
}
}
class MidiTrack implements IteratorAggregate
{
public static $midi = null;
public $id; //current number of miditrack, starts at 1
public $inst; //array key, instrument number + 1
public $name;
public $channel;
public $port;
public $program;
private $events = array();
public $sorted = false;
/**
* Implemented iterator to allow iteration over MidiEvents
*
* @return MidiEvent
*/
public function getIterator() {
$this->sortEvents();
$arrayObj=new ArrayObject($this->events);
return $arrayObj->getIterator();
}
/**
* Sorts MidiEvents when they are requested.
* Sets a flag when done to prevent unnecessary sort function calls.
*/
function sortEvents()
{
if ($this->sorted === false)
{
ksort($this->events);
$this->sorted = true;
}
}
/**
* Returns a sorted array of all MidiEvents of the current MidiTrack
*
* @return Array[MidiEvents]
*/
function &getEvents()
{
$this->sortEvents();
return $this->events;
}
/**
* Adds the given MidiEvent instance to the $events array.
* In the calling code, it is most of the time necessary to clone
* the event first. If it is a note-on, the event is also added
* to the $lastNotes array.
*
* @param int $pos
* @param int $col
* @param MidiEvent $evt
*/
function addEvent($pos, $tick, $col, MidiEvent $evt)
{
$evt->ch = $this->channel;
$evt->id = $this->id;
$evt->inst = $this->inst;
$evt->col = $col;
$this->events[$pos][$tick][$col][] = $evt;
if ($evt->evttype == 1 || $evt->evttype == 2) //note-on && note-off
{
MasterTrack::setLastNote($col, $evt);
}
}
/**
* Creates a new MidiEvent from the given array and stores
* it in the $events array. Note-ons are stored in the $lastNotes
* array as well.
*
* @param int $pos
* @param int $col
* @param int $type
* @param array $arr
* @return MidiEvent
*/
function addEventArr($pos, $tick, $col, $type, array $arr)
{
$arr['ch'] = $this->channel;
$arr['id'] = $this->id;
$arr['inst'] = $this->inst;
$arr['col'] = $col;
$this->events[$pos][$tick][$col][] = new MidiEvent($type, $arr);
if ($type == 1) //note-on
{
MasterTrack::setLastNote($col, end($this->events[$pos][$tick][$col]));
}
return end($this->events[$pos][$tick][$col]);
}
/**
* THIS FUNCTION IS BORKED. DO NOT USE.
*
* Returns the last event or events in the specified note-column
* wrapped in an array.
*
* @param int $col
* @return arrayOfMidiEvent
*/
function getLastEvent($col)
{
end($this->events);
return ($this->events[$col]);
}
/**
* Returns the event or events on the given line
* wrapped in an array.
*
* @param int $col
* @return arrayOfMidiEvent
*/
function getEvent($pos)
{
return $this->events[$pos];
}
/**
* Sets the MidiTrack name based on the Renoise Instrument name.
* Immediately executes the TrkName MetaEvent
*
* @param String $name
*/
function setName($name)
{
//Insert midi-track name
$this->name = $name;
$msgStr = sprintf('0 Meta TrkName "%s"', $name);
self::$midi->addMsg($this->id, $msgStr);
$msgStr = sprintf('0 Meta InstrName "%s"', $name);
self::$midi->addMsg($this->id, $msgStr);
}
function __construct($id, $inst)
{
$this->id = $id;
$this->inst = $inst;
$this->program = $this->id;
$this->channel = ($this->id - 1 ) % 15 + 1;
$this->port = (int) floor($this->id / 16);
//Create new midi-track
$tn = self::$midi->newTrack() -1;
// Midi Channel Prefix (ch 0-15)
$channelprefix = $this->channel - 1;
self::$midi->addMsg($tn, "0 Meta 0x20 " . ($channelprefix));
// Midi Port
// TODO: parsed as channel prefix in current midixml converter
//self::$midi->addMsg($tn, "0 Meta 0x21 {$this->port}");
}
}
class Globals
{
public $pos;
public $val;
public $param;
function __construct($pos, $val, $param=null)
{
$this->pos = $pos;
$this->val = $val;
$this->param = $param;
}
}
class MasterTrack extends MidiTrack
{
public $id = 0; //Midi mastertrack is always 0
public static $lastNotes = array();
public $bpm; //initial bpm
public $realbpm; //actual playback bpm
public $speed; //initial renoise ticks per beat
public $notes; //list of all midi notes
public $division = 96; // MIDI clicks per quarter note
public $lpb; //initial lpb
public $timestamp = 0; //initial timestamp
public $renoise2 = false;
//global pattern effects
//['param'][$pos] => $val
public $globals = array();
//array[$pos] => $time
public $timeStamps = array();
/*
* @override
*/
function __construct($bpm, $speed, $lpb = 0)
{
if (!self::$midi)
{
$this->bpm = $bpm;
$this->speed = $speed;
if ($lpb)
{
$this->lpb = $lpb;
$this->renoise2 = true;
}
else
{
$this->lpb = $this->getLpb($speed);
}
$this->globals['timestamp'][0] = 0;
self::$midi = new Midi();
self::$midi->open();
self::$midi->newTrack();
self::$midi->setTimebase($this->division);
$this->notes = self::$midi->getNoteList();
}
}
/**
* Stores a note-on MidiEvent in the $lastNotes array, labeled
* by the originating note-column
*
* @param unknown_type $col
* @param unknown_type $evt
*/
static function setLastNote($col, $evt)
{
self::$lastNotes[$col] = $evt;
}
/**
* Returns the last note-on event in the specified note-column
*
* @param int $col
* @return MidiEvent
*/
static function getLastNote($col)
{
if(isset(self::$lastNotes[$col]))
{
return self::$lastNotes[$col];
}
else
return false;
}
/**
* Removes the last note-on event from the specified note-column
* from the $lastNotes array
*
* @param int $col
*/
static function remLastNote($col)
{
unset(self::$lastNotes[$col]);
}
/**
* Translates the note string from Song.xml to a
* midi note number or note-off event.
*
* @param String $str
* @return int/String
*/
function str2note($str)
{
if (strtoupper($str) == 'OFF')
return "Off";
$n = strtoupper($str);
$n = str_replace('-', '', $n);
$n = str_replace('#', 's', $n);
$n = array_search($n, $this->notes);
return $n;
}
/**
* Add MIDI msgs of collected global commands
* in the specified range of lines
*
* @param int $startPos
* @param int $endPos
*/
function commitGlobals($startPos, $endPos)
{
if (!isset($this->globals['bpm']))
$this->globals['bpm'][0] = $this->bpm;
else
$this->globals['bpm'][0] = $this->getNextGlobal(0,'bpm')->val;
if (!isset($this->globals['speed']))
$this->globals['speed'][0] = $this->speed;
else
$this->globals['speed'][0]= $this->getNextGlobal(0,'speed')->val;
if (!isset($this->globals['lpb']))
$this->globals['lpb'][0] = $this->lpb;
else
$this->globals['lpb'][0]= $this->getNextGlobal(0,'lpb')->val;
//which lines contain global commands?
$arr = $this->globals['bpm'] + $this->globals['speed'];
ksort($arr);
foreach ($arr as $pos=>$val)
{
$this->setLpb($pos);
}
}
/**
* Real playback BPM; depends on explicit BPM and Speed
*/
function setRealBpm($pos, $realbpm)
{
$this->globals['realbpm'][$pos] = $realbpm;
$this->addEventArr($pos,0, 0, 9, array('num' => (int)floor(60000000 / $realbpm)));
}
/**
* Global events, such as speed, lines per beat and tempo,
* affect the whole song. They are added to the MasterTrack.
*
* Renoise's way of automation works a bit funny:
* - the last command on the line overwrites any preceding values of that command
* - all lines after the command have the same value, until overwritten
* - if no command is placed in the pattern, the default value will be
* taken, some of which set in the Playback Controls (eg. speed, tempo)
* - between the first line of the song and a command, the default value is overwritten
* by that command's value. Hence, all lines in between will have that value before
* the command has even come across!
*
* This function takes the observations above into account.
*
* @param int $pos
* @param String $param
* @return array
*/
function getNearestGlobal($pos, $param)
{
//any preceding commands?
if ($obj = $this->getPreviousGlobal($pos, $param))
return $obj;
//bpm and speed will always be set on row 0, but
// are overriden by the first occurance of a pattern command
if ($obj = $this->getNextGlobal($pos, $param))
return $obj;
return $this->getDefaultGlobal($param);
}
/**
* Return any global scope Pattern Command between the
* current line and line 0, in descending order
*
* @param int $pos
* @param String $param
* @return Globals
*/
function getPreviousGlobal($pos, $param)
{
if ($pos < 0)
return $this->getDefaultGlobal($param);
if (isset($this->globals[$param][$pos]))
{
return new Globals(floor($pos), $this->globals[$param][$pos]);
}
else if ($pos > 0) {
//$haystack = array_reverse($this->globals[$param], true);
krsort($this->globals[$param]);
$haystack = $this->globals[$param];
$prev_key = 0;
foreach ($haystack as $key => $val)
{
if ($key <= $pos)
break;
}
if (isset($key))
return new Globals($key, $val);
}
return false;
/*
if (isset($this->globals[$param][$pos]))
{
return new Globals($pos, $this->globals[$param][$pos]);
}
else if ($pos > 0) {
$haystack = $this->globals[$param];
$prev_key = 0;
foreach ($haystack as $key => $val)
{
if ($key >= $pos)
break;
$prev_key = $key;
}
if (isset($prev_key))
return new Globals($prev_key, $val);
}
return false;
*/
}
/**
* Return any global scope Pattern Command between the
* current line and the last event in the array
*
* @param int $pos
* @param String $param
* @return Globals
*/
function getNextGlobal($pos, $param)
{
if ($pos < 0) $pos = 0;
if (isset($this->globals[$param][$pos]))
{
return new Globals(floor($pos), $this->globals[$param][$pos]);
}
ksort($this->globals[$param]);
foreach ($this->globals[$param] as $key => $val)
if ($key >= $pos)
return new Globals($pos, $val);
return false;
/*
if ($pos < 0) $pos = 0;
$arr = array_keys($this->globals[$param]);
$a = max($arr);
for ($i=$pos;$i<=$a;$i++)
if (isset($this->globals[$param][$i]))
return new Globals($i, $this->globals[$param][$i]);
return false;
*/
}
/**
* Default value, explicitely set in the Playback Controls.
* They are stored on line -1 to separate them from Pattern Commands.
* Eventually they may go onto line 0, if no pattern commands are found further up the song.
*
* @param String $param
* @return Globals
*/
function getDefaultGlobal($param)
{
if (isset($this->$param))
return new Globals(0, $this->$param);
}
/**
* Calculate the Lines Per Beat on the current position.
* The value of LPB is only correct if all lines, tracks
* and patterns have been searched for pattern commands.
* Therefore this function is called by MasterTrack::commitGlobals()
* after Song.xml has been completely parsed.
*
*
* @param int $pos
*/
private function setLpb($pos)
{
$currentSpeed = $this->getNearestGlobal($pos, 'speed')->val;
//on specific speeds, the bpm does not differ from any explicitely set one
if ($this->renoise2 || 6 % $currentSpeed == 0)
$realbpm = $this->getNearestGlobal($pos, 'bpm')->val;
else //at most speeds however, the real playback bpm is different from the set one
$realbpm = round(6 / $currentSpeed * $this->getNearestGlobal($pos, 'bpm')->val, 1);
//only write if changed
if ($pos == 0 || $this->getPreviousGlobal($pos-1, 'realbpm')->val != $realbpm)
$this->setRealBpm($pos, $realbpm);
$this->globals['timestamp'][$pos] = $this->getTimeStamp($pos, 0);
}
/**
* Returns the LPB based on the number of ticks (speed)
*
* @param int $speed
* @return int
*/
function getLpb($speed)
{
//lpb does change
switch (TRUE)
{
case ($speed>3): $lpb = 4; break;
case $speed==3: $lpb = 8; break;
case $speed==2: $lpb = 12; break;
case $speed==1: $lpb = 24; break;
}
return $lpb;
}
/**
* Returns the duration between line 0 and the given line.
* Called from getTimeStamp().
*
* @param double $pos
* @return int
*/
/*
function getTime($pos)
{
if (isset($this->globals['timestamp'][$pos]))
{
return $this->globals['timestamp'][$pos];
}
//We need the most recent timestamp to base the current one on
elseif ($prevTimeStamp = $this->getPreviousGlobal($pos-1, 'timestamp'))
{
$prevLpb = $this->getPreviousGlobal($pos-1, 'lpb');
$resolution = ($this->division / $prevLpb->val);
$time = $prevTimeStamp->val
+ $resolution * ($pos - $prevTimeStamp->pos);
}
else //If not, simply count from line 0
{
$prevLpb = $this->getPreviousGlobal($pos, 'lpb');
$resolution = ($this->division / $prevLpb->val);
$time = $resolution * $pos;
}
return (int) round($time);
}
/*
/**
* Calculates absolute midi timestamp from tracker line at current speed.
*
* Since the arrays are based on integer line numbers as keys, tick
* timing is done separately by calculating the fractional line numbers as
* a function of the $offset parameter given in ticks.
*
* @param int $pos
* @return int
*/
/*
function getTimestamp($pos, $offset)
{
//TODO: add note expansion by multiplying non-event lines times lpb
//Translate offset into fractional $pos.
//At negative offset, the pos was decreased with one line by convention,
// so we can safely add the fractional offset
if ($offset != 0)
{
$ticks = $this->getNearestGlobal($pos, 'speed')->val;
$pos += ($offset / $ticks);
}
return $this->getTime($pos);
}
*/
function getTimestamp($pos, $offset)
{
$t3 = 0;
if ($offset != 0)
{
$lpb2 = $this->getPreviousGlobal($pos, 'lpb');
$speed2 = $this->getPreviousGlobal($pos, 'speed');
$reso2 = ($this->division / $lpb2->val);
$t3 = $reso2 * ($offset / $speed2->val);
}
elseif (isset($this->globals['timestamp'][$pos]))
return $this->globals['timestamp'][$pos];
$t1 = $this->getPreviousGlobal($pos-1, 'timestamp');
$lpb1 = $this->getPreviousGlobal($pos-1, 'lpb');
$reso1 = ($this->division / $lpb1->val);
$t2 = ($pos - $t1->pos) * $reso1;
$time = (int)$t1->val + $t2 + $t3;
return $time;
}
/**
* Apply volume and panning column fx to the previous note
*
* @param SimpleXML $vol
* @param SimpleXML $pan
* @param MidiEvent $lastnote
*/
function doVolPanFx($pos, $vol, $pan, $lastnote)
{
//TODO: note delay for note-offs
//TODO: double retrigs
//Done: http://tutorials.renoise.com/?n=Renoise.DelayCutTrick
//if($lastnote->evttype == 2)
// return;
if ($vol) //does the SimpleXML object contain a volume value?
{
$vol = (int) intval((string) $vol, 16);
//set default to remove any values (that can be interpreted as fx)
// outside midi range, since we are already handling fx here
$lastnote->vol = 0x7F;
//Note volume (0 - 7F, 80)
if ($vol == 0x80)
$vol == 0x7F;
if ($vol < 0x80)
$lastnote->vol = $vol;
//Delay note X ticks (D0 - DF)
//Only valid if ticks < speed; check in getTimeStamp()
elseif ($vol > 0xD0 && $vol <= 0xDF)
$lastnote->offset = $vol - 0xD0;
//Retrig note X ticks (E0 - EF)
//Notes = ceil(speed / X); X(0) = 1;
elseif ($vol > 0xE0 && $vol <= 0xEF)
$lastnote->retrig = $vol - 0xE0;
//Cut Note after X Ticks (F0 - FE)
elseif ($vol >= 0xF0 && $vol <= 0xFE)
{
$evt = clone $lastnote;
$evt->evttype = 2;
MidiList::getTrack($evt->inst)->addEvent($pos, $vol-0xF0, $evt->col, $evt);
}
}
if ($pan) //does the SimpleXML object contain a panning value?
{
$pan = (int) intval((string) $pan, 16);
//set default to remove any values (that can be interpreted as fx)
// outside midi range, since we are already handling fx here
$lastnote->pan = 0x40;
if ($pan <= 0x80)
$lastnote->pan = (int) intval((string) $pan, 16);
//Delay note X ticks (D0 - DF)
//Only valid if ticks < speed; check in getTimeStamp()
elseif ($pan > 0xD0 && $pan <= 0xDF)
$lastnote->offset = $pan - 0xD0;
//Retrig note X ticks (E0 - EF)
//Notes = ceil(speed / X); X(0) = 1;
elseif ($pan > 0xE0 && $pan <= 0xEF)
$lastnote->retrig = $pan - 0xE0;
//Cut Note after X Ticks (F0 - FE)
elseif ($pan > 0xF0 && $pan <= 0xFE)
{
$evt = clone $lastnote;
$evt->evttype = 2;
MidiList::getTrack($evt->inst)->addEvent($pos, $pan-0xF0, $evt->col, $evt);
}
}
}
}
?>