Location: PHPKode > projects > XRNS-PHP > xrns2midi_classes.php
<?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);
        }
     }
   }
}

?>
Return current item: XRNS-PHP