diff --git a/airtime_mvc/application/controllers/LibraryController.php b/airtime_mvc/application/controllers/LibraryController.php index ef0a8ba9f..d258bd766 100644 --- a/airtime_mvc/application/controllers/LibraryController.php +++ b/airtime_mvc/application/controllers/LibraryController.php @@ -52,6 +52,24 @@ class LibraryController extends Zend_Controller_Action $this->view->headScript()->appendFile($baseUrl.'js/airtime/library/spl.js?'.$CC_CONFIG['airtime_version'], 'text/javascript'); $this->view->headScript()->appendFile($baseUrl.'js/airtime/playlist/smart_blockbuilder.js?'.$CC_CONFIG['airtime_version'], 'text/javascript'); + + $this->view->headScript()->appendFile($baseUrl.'js/waveformplaylist/observer/observer.js?'.$CC_CONFIG['airtime_version'], 'text/javascript'); + $this->view->headScript()->appendFile($baseUrl.'js/waveformplaylist/config.js?'.$CC_CONFIG['airtime_version'], 'text/javascript'); + $this->view->headScript()->appendFile($baseUrl.'js/waveformplaylist/curves.js?'.$CC_CONFIG['airtime_version'], 'text/javascript'); + $this->view->headScript()->appendFile($baseUrl.'js/waveformplaylist/fades.js?'.$CC_CONFIG['airtime_version'], 'text/javascript'); + $this->view->headScript()->appendFile($baseUrl.'js/waveformplaylist/local_storage.js?'.$CC_CONFIG['airtime_version'], 'text/javascript'); + $this->view->headScript()->appendFile($baseUrl.'js/waveformplaylist/controls.js?'.$CC_CONFIG['airtime_version'], 'text/javascript'); + $this->view->headScript()->appendFile($baseUrl.'js/waveformplaylist/playout.js?'.$CC_CONFIG['airtime_version'], 'text/javascript'); + $this->view->headScript()->appendFile($baseUrl.'js/waveformplaylist/track_render.js?'.$CC_CONFIG['airtime_version'], 'text/javascript'); + $this->view->headScript()->appendFile($baseUrl.'js/waveformplaylist/track.js?'.$CC_CONFIG['airtime_version'], 'text/javascript'); + $this->view->headScript()->appendFile($baseUrl.'js/waveformplaylist/time_scale.js?'.$CC_CONFIG['airtime_version'], 'text/javascript'); + $this->view->headScript()->appendFile($baseUrl.'js/waveformplaylist/playlist.js?'.$CC_CONFIG['airtime_version'], 'text/javascript'); + + //arbitrary attributes need to be allowed to set an id for the templates. + $this->view->headScript()->setAllowArbitraryAttributes(true); + //$this->view->headScript()->appendScript(file_get_contents(APPLICATION_PATH.'/../public/js/waveformplaylist/templates/bottombar.tpl'), + // 'text/template', array('id' => 'tpl_playlist_cues', 'noescape' => true)); + $this->view->headLink()->appendStylesheet($baseUrl.'css/playlist_builder.css?'.$CC_CONFIG['airtime_version']); try { diff --git a/airtime_mvc/application/controllers/PlaylistController.php b/airtime_mvc/application/controllers/PlaylistController.php index 3f0dd66b7..1134499ce 100644 --- a/airtime_mvc/application/controllers/PlaylistController.php +++ b/airtime_mvc/application/controllers/PlaylistController.php @@ -10,6 +10,7 @@ class PlaylistController extends Zend_Controller_Action ->addActionContext('move-items', 'json') ->addActionContext('delete-items', 'json') ->addActionContext('set-fade', 'json') + ->addActionContext('set-crossfade', 'json') ->addActionContext('set-cue', 'json') ->addActionContext('new', 'json') ->addActionContext('edit', 'json') @@ -417,6 +418,33 @@ class PlaylistController extends Zend_Controller_Action $this->playlistUnknownError($e); } } + + public function setCrossfadeAction() + { + $id1 = $this->_getParam('id1'); + $id2 = $this->_getParam('id2'); + $type = $this->_getParam('type'); + $fadeIn = $this->_getParam('fadeIn', 0); + $fadeOut = $this->_getParam('fadeOut', 0); + $offset = $this->_getParam('offset', 0); + + try { + $obj = $this->getPlaylist($type); + $response = $obj->createCrossfade($id1, $fadeOut, $id2, $fadeIn, $offset); + + if (!isset($response["error"])) { + $this->createUpdateResponse($obj); + } else { + $this->view->error = $response["error"]; + } + } catch (PlaylistOutDatedException $e) { + $this->playlistOutdated($e); + } catch (PlaylistNotFoundException $e) { + $this->playlistNotFound($type); + } catch (Exception $e) { + $this->playlistUnknownError($e); + } + } public function getPlaylistFadesAction() { diff --git a/airtime_mvc/application/controllers/PreferenceController.php b/airtime_mvc/application/controllers/PreferenceController.php index 0ccb348a3..7d0f21210 100644 --- a/airtime_mvc/application/controllers/PreferenceController.php +++ b/airtime_mvc/application/controllers/PreferenceController.php @@ -43,7 +43,9 @@ class PreferenceController extends Zend_Controller_Action if ($form->isValid($values)) { Application_Model_Preference::SetHeadTitle($values["stationName"], $this->view); - Application_Model_Preference::SetDefaultFade($values["stationDefaultFade"]); + Application_Model_Preference::SetDefaultCrossfadeDuration($values["stationDefaultCrossfadeDuration"]); + Application_Model_Preference::SetDefaultFadeIn($values["stationDefaultFadeIn"]); + Application_Model_Preference::SetDefaultFadeOut($values["stationDefaultFadeOut"]); Application_Model_Preference::SetAllow3rdPartyApi($values["thirdPartyApi"]); Application_Model_Preference::SetDefaultLocale($values["locale"]); Application_Model_Preference::SetDefaultTimezone($values["timezone"]); diff --git a/airtime_mvc/application/forms/GeneralPreferences.php b/airtime_mvc/application/forms/GeneralPreferences.php index 3d249ce47..19e3fc064 100644 --- a/airtime_mvc/application/forms/GeneralPreferences.php +++ b/airtime_mvc/application/forms/GeneralPreferences.php @@ -12,11 +12,9 @@ class Application_Form_GeneralPreferences extends Zend_Form_SubForm array('ViewScript', array('viewScript' => 'form/preferences_general.phtml')) )); - $defaultFade = Application_Model_Preference::GetDefaultFade(); - if ($defaultFade == "") { - $defaultFade = '0.5'; - } - + $defaultFadeIn = Application_Model_Preference::GetDefaultFadeIn(); + $defaultFadeOut = Application_Model_Preference::GetDefaultFadeOut(); + //Station name $this->addElement('text', 'stationName', array( 'class' => 'input_text', @@ -28,11 +26,30 @@ class Application_Form_GeneralPreferences extends Zend_Form_SubForm 'ViewHelper' ) )); + + //Default station fade in + $this->addElement('text', 'stationDefaultCrossfadeDuration', array( + 'class' => 'input_text', + 'label' => _('Default Crossfade Duration (s):'), + 'required' => true, + 'filters' => array('StringTrim'), + 'validators' => array( + array( + $rangeValidator, + $notEmptyValidator, + 'regex', false, array('/^[0-9]{1,2}(\.\d{1})?$/', 'messages' => _('enter a time in seconds 0{.0}')) + ) + ), + 'value' => Application_Model_Preference::GetDefaultCrossfadeDuration(), + 'decorators' => array( + 'ViewHelper' + ) + )); - //Default station fade - $this->addElement('text', 'stationDefaultFade', array( + //Default station fade in + $this->addElement('text', 'stationDefaultFadeIn', array( 'class' => 'input_text', - 'label' => _('Default Fade (s):'), + 'label' => _('Default Fade In (s):'), 'required' => true, 'filters' => array('StringTrim'), 'validators' => array( @@ -42,11 +59,30 @@ class Application_Form_GeneralPreferences extends Zend_Form_SubForm 'regex', false, array('/^[0-9]{1,2}(\.\d{1})?$/', 'messages' => _('enter a time in seconds 0{.0}')) ) ), - 'value' => $defaultFade, + 'value' => $defaultFadeIn, 'decorators' => array( 'ViewHelper' ) )); + + //Default station fade out + $this->addElement('text', 'stationDefaultFadeOut', array( + 'class' => 'input_text', + 'label' => _('Default Fade Out (s):'), + 'required' => true, + 'filters' => array('StringTrim'), + 'validators' => array( + array( + $rangeValidator, + $notEmptyValidator, + 'regex', false, array('/^[0-9]{1,2}(\.\d{1})?$/', 'messages' => _('enter a time in seconds 0{.0}')) + ) + ), + 'value' => $defaultFadeOut, + 'decorators' => array( + 'ViewHelper' + ) + )); $third_party_api = new Zend_Form_Element_Radio('thirdPartyApi'); $third_party_api->setLabel( diff --git a/airtime_mvc/application/layouts/scripts/layout.phtml b/airtime_mvc/application/layouts/scripts/layout.phtml index dedda7c88..b72305687 100644 --- a/airtime_mvc/application/layouts/scripts/layout.phtml +++ b/airtime_mvc/application/layouts/scripts/layout.phtml @@ -33,5 +33,46 @@
layout()->content ?>
+ + + + diff --git a/airtime_mvc/application/models/Block.php b/airtime_mvc/application/models/Block.php index b364e5eb5..931445d2d 100644 --- a/airtime_mvc/application/models/Block.php +++ b/airtime_mvc/application/models/Block.php @@ -99,13 +99,8 @@ class Application_Model_Block implements Application_Model_LibraryEditable $this->block->save(); } - $defaultFade = Application_Model_Preference::GetDefaultFade(); - if ($defaultFade !== "") { - //fade is in format SS.uuuuuu - - $this->blockItem["fadein"] = $defaultFade; - $this->blockItem["fadeout"] = $defaultFade; - } + $this->blockItem["fadein"] = Application_Model_Preference::GetDefaultFadeIn(); + $this->blockItem["fadeout"] = Application_Model_Preference::GetDefaultFadeOut(); $this->con = isset($con) ? $con : Propel::getConnection(CcBlockPeer::DATABASE_NAME); $this->id = $this->block->getDbId(); @@ -200,6 +195,7 @@ SELECT pc.id AS id, pc.cueout, pc.fadein, pc.fadeout, + pc.trackoffset, bl.type, f.LENGTH AS orig_length, f.id AS item_id, @@ -234,7 +230,15 @@ SQL; foreach ($rows as &$row) { $clipSec = Application_Common_DateHelper::playlistTimeToSeconds($row['length']); + + $row['trackSec'] = $clipSec; + + $row['cueInSec'] = Application_Common_DateHelper::playlistTimeToSeconds($row['cuein']); + $row['cueOutSec'] = Application_Common_DateHelper::playlistTimeToSeconds($row['cueout']); + + $trackoffset = $row['trackoffset']; $offset += $clipSec; + $offset -= $trackoffset; $offset_cliplength = Application_Common_DateHelper::secondsToPlaylistTime($offset); //format the length for UI. @@ -668,6 +672,29 @@ SQL; return array($fadeIn, $fadeOut); } + + /* + * create a crossfade from item in cc_playlist_contents with $id1 to item $id2. + * + * $fadeOut length of fade out in seconds if $id1 + * $fadeIn length of fade in in seconds of $id2 + * $offset time in seconds from end of $id1 that $id2 will begin to play. + */ + public function createCrossfade($id1, $fadeOut, $id2, $fadeIn, $offset) + { + $this->con->beginTransaction(); + + try { + $this->changeFadeInfo($id1, null, $fadeOut); + $this->changeFadeInfo($id2, $fadeIn, null, $offset); + + $this->con->commit(); + + } catch (Exception $e) { + $this->con->rollback(); + throw $e; + } + } /** * Change fadeIn and fadeOut values for block Element @@ -680,7 +707,7 @@ SQL; * new value in ss.ssssss or extent format * @return boolean */ - public function changeFadeInfo($id, $fadeIn, $fadeOut) + public function changeFadeInfo($id, $fadeIn, $fadeOut, $offset=null) { //See issue CC-2065, pad the fadeIn and fadeOut so that it is TIME compatable with the DB schema //For the top level PlayList either fadeIn or fadeOut will sometimes be Null so need a gaurd against @@ -714,6 +741,12 @@ SQL; } $row->setDbFadein($fadeIn); + if (!is_null($offset)) { + $row->setDbTrackOffset($offset); + Logging::info("Setting offset {$offset} on item {$id}"); + $row->save($this->con); + } + } if (!is_null($fadeOut)) { diff --git a/airtime_mvc/application/models/Playlist.php b/airtime_mvc/application/models/Playlist.php index ea584b76c..9d45f7daa 100644 --- a/airtime_mvc/application/models/Playlist.php +++ b/airtime_mvc/application/models/Playlist.php @@ -36,6 +36,7 @@ class Application_Model_Playlist implements Application_Model_LibraryEditable "cueout" => "00:00:00", "fadein" => "0.0", "fadeout" => "0.0", + "crossfadeDuration" => 0 ); //using propel's phpNames. @@ -60,13 +61,9 @@ class Application_Model_Playlist implements Application_Model_LibraryEditable $this->pl->save(); } - $defaultFade = Application_Model_Preference::GetDefaultFade(); - if ($defaultFade !== "") { - //fade is in format SS.uuuuuu - - $this->plItem["fadein"] = $defaultFade; - $this->plItem["fadeout"] = $defaultFade; - } + $this->plItem["fadein"] = Application_Model_Preference::GetDefaultFadeIn(); + $this->plItem["fadeout"] = Application_Model_Preference::GetDefaultFadeOut(); + $this->plItem["crossfadeDuration"] = Application_Model_Preference::GetDefaultCrossfadeDuration(); $this->con = isset($con) ? $con : Propel::getConnection(CcPlaylistPeer::DATABASE_NAME); $this->id = $this->pl->getDbId(); @@ -166,6 +163,7 @@ class Application_Model_Playlist implements Application_Model_LibraryEditable pc.cueout, pc.fadein, pc.fadeout, + pc.trackoffset, f.id AS item_id, f.track_title, f.artist_name AS creator, @@ -193,6 +191,7 @@ SQL; pc.cueout, pc.fadein, pc.fadeout, + pc.trackoffset, ws.id AS item_id, (ws.name || ': ' || ws.url) AS title, sub.login AS creator, @@ -213,6 +212,7 @@ SQL; pc.cueout, pc.fadein, pc.fadeout, + pc.trackoffset, bl.id AS item_id, bl.name AS title, sbj.login AS creator, @@ -227,6 +227,7 @@ SQL; AND pc.TYPE = 2)) AS temp ORDER BY temp.position; SQL; + //Logging::info($sql); $params = array( ':playlist_id1'=>$this->id, ':playlist_id2'=>$this->id, ':playlist_id3'=>$this->id); @@ -238,8 +239,18 @@ SQL; $offset = 0; foreach ($rows as &$row) { + + //Logging::info($row); + $clipSec = Application_Common_DateHelper::playlistTimeToSeconds($row['length']); + $row['trackSec'] = $clipSec; + + $row['cueInSec'] = Application_Common_DateHelper::playlistTimeToSeconds($row['cuein']); + $row['cueOutSec'] = Application_Common_DateHelper::playlistTimeToSeconds($row['cueout']); + + $trackoffset = $row['trackoffset']; $offset += $clipSec; + $offset -= $trackoffset; $offset_cliplength = Application_Common_DateHelper::secondsToPlaylistTime($offset); //format the length for UI. @@ -356,6 +367,7 @@ SQL; $row->setDbFadeout(Application_Common_DateHelper::secondsToPlaylistTime($info["fadeout"])); if ($info["ftype"] == "audioclip") { $row->setDbFileId($info["id"]); + $row->setDbTrackOffset($info["crossfadeDuration"]); $type = 0; } elseif ($info["ftype"] == "stream") { $row->setDbStreamId($info["id"]); @@ -645,19 +657,42 @@ SQL; return array($fadeIn, $fadeOut); } + + /* + * create a crossfade from item in cc_playlist_contents with $id1 to item $id2. + * + * $fadeOut length of fade out in seconds if $id1 + * $fadeIn length of fade in in seconds of $id2 + * $offset time in seconds from end of $id1 that $id2 will begin to play. + */ + public function createCrossfade($id1, $fadeOut, $id2, $fadeIn, $offset) + { + $this->con->beginTransaction(); + + try { + $this->changeFadeInfo($id1, null, $fadeOut); + $this->changeFadeInfo($id2, $fadeIn, null, $offset); + + $this->con->commit(); + + } catch (Exception $e) { + $this->con->rollback(); + throw $e; + } + } /** * Change fadeIn and fadeOut values for playlist Element * - * @param int $pos - * position of audioclip in playlist + * @param int $id + * id of audioclip in playlist contents table. * @param string $fadeIn * new value in ss.ssssss or extent format * @param string $fadeOut * new value in ss.ssssss or extent format * @return boolean */ - public function changeFadeInfo($id, $fadeIn, $fadeOut) + public function changeFadeInfo($id, $fadeIn, $fadeOut, $offset=null) { //See issue CC-2065, pad the fadeIn and fadeOut so that it is TIME compatable with the DB schema //For the top level PlayList either fadeIn or fadeOut will sometimes be Null so need a gaurd against @@ -683,6 +718,12 @@ SQL; $fadeIn = $clipLength; } $row->setDbFadein($fadeIn); + + if (!is_null($offset)) { + $row->setDbTrackOffset($offset); + Logging::info("Setting offset {$offset} on item {$id}"); + $row->save($this->con); + } } if (!is_null($fadeOut)) { diff --git a/airtime_mvc/application/models/Preference.php b/airtime_mvc/application/models/Preference.php index 2af97d193..f2eeee8a4 100644 --- a/airtime_mvc/application/models/Preference.php +++ b/airtime_mvc/application/models/Preference.php @@ -194,6 +194,57 @@ class Application_Model_Preference return new DateTime($date, new DateTimeZone("UTC")); } } + + public static function SetDefaultCrossfadeDuration($duration) + { + self::setValue("default_crossfade_duration", $duration); + } + + public static function GetDefaultCrossfadeDuration() + { + $duration = self::getValue("default_crossfade_duration"); + + if ($duration === "") { + // the default value of the fade is 00.5 + return "0"; + } + + return $duration; + } + + public static function SetDefaultFadeIn($fade) + { + self::setValue("default_fade_in", $fade); + } + + public static function GetDefaultFadeIn() + { + $fade = self::getValue("default_fade_in"); + + if ($fade === "") { + // the default value of the fade is 00.5 + return "00.5"; + } + + return $fade; + } + + public static function SetDefaultFadeOut($fade) + { + self::setValue("default_fade_out", $fade); + } + + public static function GetDefaultFadeOut() + { + $fade = self::getValue("default_fade_out"); + + if ($fade === "") { + // the default value of the fade is 00.5 + return "00.5"; + } + + return $fade; + } public static function SetDefaultFade($fade) { diff --git a/airtime_mvc/application/models/Scheduler.php b/airtime_mvc/application/models/Scheduler.php index 17888fe46..7f6272cee 100644 --- a/airtime_mvc/application/models/Scheduler.php +++ b/airtime_mvc/application/models/Scheduler.php @@ -17,6 +17,8 @@ class Application_Model_Scheduler private $epochNow; private $nowDT; private $user; + + private $crossfadeDuration; private $checkUserPermissions = true; @@ -38,6 +40,8 @@ class Application_Model_Scheduler } $this->user = Application_Model_User::getCurrentUser(); + + $this->crossfadeDuration = Application_Model_Preference::GetDefaultCrossfadeDuration(); } public function setCheckUserPermissions($value) @@ -201,12 +205,9 @@ class Application_Model_Scheduler $data["cuein"] = $file->getDbCuein(); $data["cueout"] = $file->getDbCueout(); - $defaultFade = Application_Model_Preference::GetDefaultFade(); - if (isset($defaultFade)) { - //fade is in format SS.uuuuuu - $data["fadein"] = $defaultFade; - $data["fadeout"] = $defaultFade; - } + //fade is in format SS.uuuuuu + $data["fadein"] = Application_Model_Preference::GetDefaultFadeIn(); + $data["fadeout"] = Application_Model_Preference::GetDefaultFadeOut(); $files[] = $data; } @@ -260,12 +261,11 @@ class Application_Model_Scheduler $cuein = Application_Common_DateHelper::calculateLengthInSeconds($data["cuein"]); $cueout = Application_Common_DateHelper::calculateLengthInSeconds($data["cueout"]); $data["cliplength"] = Application_Common_DateHelper::secondsToPlaylistTime($cueout - $cuein); - $defaultFade = Application_Model_Preference::GetDefaultFade(); - if (isset($defaultFade)) { - //fade is in format SS.uuuuuu - $data["fadein"] = $defaultFade; - $data["fadeout"] = $defaultFade; - } + + //fade is in format SS.uuuuuu + $data["fadein"] = Application_Model_Preference::GetDefaultFadeIn(); + $data["fadeout"] = Application_Model_Preference::GetDefaultFadeOut(); + $data["type"] = 0; $files[] = $data; } @@ -286,12 +286,9 @@ class Application_Model_Scheduler $data["cueout"] = $stream->getDbLength(); $data["type"] = 1; - $defaultFade = Application_Model_Preference::GetDefaultFade(); - if (isset($defaultFade)) { - //fade is in format SS.uuuuuu - $data["fadein"] = $defaultFade; - $data["fadeout"] = $defaultFade; - } + //fade is in format SS.uuuuuu + $data["fadein"] = Application_Model_Preference::GetDefaultFadeIn(); + $data["fadeout"] = Application_Model_Preference::GetDefaultFadeOut(); $files[] = $data; } @@ -321,12 +318,11 @@ class Application_Model_Scheduler $cuein = Application_Common_DateHelper::calculateLengthInSeconds($data["cuein"]); $cueout = Application_Common_DateHelper::calculateLengthInSeconds($data["cueout"]); $data["cliplength"] = Application_Common_DateHelper::secondsToPlaylistTime($cueout - $cuein); - $defaultFade = Application_Model_Preference::GetDefaultFade(); - if (isset($defaultFade)) { - //fade is in format SS.uuuuuu - $data["fadein"] = $defaultFade; - $data["fadeout"] = $defaultFade; - } + + //fade is in format SS.uuuuuu + $data["fadein"] = Application_Model_Preference::GetDefaultFadeIn(); + $data["fadeout"] = Application_Model_Preference::GetDefaultFadeOut(); + $data["type"] = 0; $files[] = $data; } @@ -336,6 +332,31 @@ class Application_Model_Scheduler return $files; } + + /* + * @param DateTime startDT in UTC + * @param string duration + * in format H:i:s.u (could be more that 24 hours) + * + * @return DateTime endDT in UTC + */ + private function findTimeDifference($p_startDT, $p_seconds) + { + $startEpoch = $p_startDT->format("U.u"); + + //add two float numbers to 6 subsecond precision + //DateTime::createFromFormat("U.u") will have a problem if there is no decimal in the resulting number. + $newEpoch = bcsub($startEpoch , (string) $p_seconds, 6); + + $dt = DateTime::createFromFormat("U.u", $newEpoch, new DateTimeZone("UTC")); + + if ($dt === false) { + //PHP 5.3.2 problem + $dt = DateTime::createFromFormat("U", intval($newEpoch), new DateTimeZone("UTC")); + } + + return $dt; + } /* * @param DateTime startDT in UTC @@ -393,6 +414,43 @@ class Application_Model_Scheduler return $nextDT; } + + /* + * @param int $showInstance + * This function recalculates the start/end times of items in a gapless show to + * account for crossfade durations. + */ + private function calculateCrossfades($showInstance) + { + Logging::info("adjusting start, end times of scheduled items to account for crossfades show instance #".$showInstance); + + $instance = CcShowInstancesQuery::create()->findPK($showInstance, $this->con); + if (is_null($instance)) { + throw new OutDatedScheduleException(_("The schedule you're viewing is out of date!")); + } + + $itemStartDT = $instance->getDbStarts(null); + $itemEndDT = null; + + $schedule = CcScheduleQuery::create() + ->filterByDbInstanceId($showInstance) + ->orderByDbStarts() + ->find($this->con); + + foreach ($schedule as $item) { + + $itemEndDT = $item->getDbEnds(null); + + $item + ->setDbStarts($itemStartDT) + ->setDbEnds($itemEndDT); + + $itemStartDT = $this->findTimeDifference($itemEndDT, $this->crossfadeDuration); + $itemEndDT = $this->findEndTime($itemStartDT, $item->getDbClipLength()); + } + + $schedule->save($this->con); + } /* * @param int $showInstance @@ -648,6 +706,8 @@ class Application_Model_Scheduler $pend = microtime(true); Logging::debug("adjusting all following items."); Logging::debug(floatval($pend) - floatval($pstart)); + + $this->calculateCrossfades($instance->getDbId()); } }//for each instance @@ -929,6 +989,7 @@ class Application_Model_Scheduler foreach ($showInstances as $instance) { $this->removeGaps($instance); + $this->calculateCrossfades($instance); } } diff --git a/airtime_mvc/application/models/airtime/map/CcBlockcontentsTableMap.php b/airtime_mvc/application/models/airtime/map/CcBlockcontentsTableMap.php index b366a1adf..ec74e8b7e 100644 --- a/airtime_mvc/application/models/airtime/map/CcBlockcontentsTableMap.php +++ b/airtime_mvc/application/models/airtime/map/CcBlockcontentsTableMap.php @@ -42,6 +42,7 @@ class CcBlockcontentsTableMap extends TableMap { $this->addForeignKey('BLOCK_ID', 'DbBlockId', 'INTEGER', 'cc_block', 'ID', false, null, null); $this->addForeignKey('FILE_ID', 'DbFileId', 'INTEGER', 'cc_files', 'ID', false, null, null); $this->addColumn('POSITION', 'DbPosition', 'INTEGER', false, null, null); + $this->addColumn('TRACKOFFSET', 'DbTrackOffset', 'REAL', true, null, 0); $this->addColumn('CLIPLENGTH', 'DbCliplength', 'VARCHAR', false, null, '00:00:00'); $this->addColumn('CUEIN', 'DbCuein', 'VARCHAR', false, null, '00:00:00'); $this->addColumn('CUEOUT', 'DbCueout', 'VARCHAR', false, null, '00:00:00'); diff --git a/airtime_mvc/application/models/airtime/map/CcPlaylistcontentsTableMap.php b/airtime_mvc/application/models/airtime/map/CcPlaylistcontentsTableMap.php index 68fde5847..3122f64d5 100644 --- a/airtime_mvc/application/models/airtime/map/CcPlaylistcontentsTableMap.php +++ b/airtime_mvc/application/models/airtime/map/CcPlaylistcontentsTableMap.php @@ -45,6 +45,7 @@ class CcPlaylistcontentsTableMap extends TableMap { $this->addColumn('STREAM_ID', 'DbStreamId', 'INTEGER', false, null, null); $this->addColumn('TYPE', 'DbType', 'SMALLINT', true, null, 0); $this->addColumn('POSITION', 'DbPosition', 'INTEGER', false, null, null); + $this->addColumn('TRACKOFFSET', 'DbTrackOffset', 'REAL', true, null, 0); $this->addColumn('CLIPLENGTH', 'DbCliplength', 'VARCHAR', false, null, '00:00:00'); $this->addColumn('CUEIN', 'DbCuein', 'VARCHAR', false, null, '00:00:00'); $this->addColumn('CUEOUT', 'DbCueout', 'VARCHAR', false, null, '00:00:00'); diff --git a/airtime_mvc/application/models/airtime/om/BaseCcBlockcontents.php b/airtime_mvc/application/models/airtime/om/BaseCcBlockcontents.php index d6beab8b4..f2e6fb6d1 100644 --- a/airtime_mvc/application/models/airtime/om/BaseCcBlockcontents.php +++ b/airtime_mvc/application/models/airtime/om/BaseCcBlockcontents.php @@ -48,6 +48,13 @@ abstract class BaseCcBlockcontents extends BaseObject implements Persistent */ protected $position; + /** + * The value for the trackoffset field. + * Note: this column has a database default value of: 0 + * @var double + */ + protected $trackoffset; + /** * The value for the cliplength field. * Note: this column has a database default value of: '00:00:00' @@ -118,6 +125,7 @@ abstract class BaseCcBlockcontents extends BaseObject implements Persistent */ public function applyDefaultValues() { + $this->trackoffset = 0; $this->cliplength = '00:00:00'; $this->cuein = '00:00:00'; $this->cueout = '00:00:00'; @@ -175,6 +183,16 @@ abstract class BaseCcBlockcontents extends BaseObject implements Persistent return $this->position; } + /** + * Get the [trackoffset] column value. + * + * @return double + */ + public function getDbTrackOffset() + { + return $this->trackoffset; + } + /** * Get the [cliplength] column value. * @@ -359,6 +377,26 @@ abstract class BaseCcBlockcontents extends BaseObject implements Persistent return $this; } // setDbPosition() + /** + * Set the value of [trackoffset] column. + * + * @param double $v new value + * @return CcBlockcontents The current object (for fluent API support) + */ + public function setDbTrackOffset($v) + { + if ($v !== null) { + $v = (double) $v; + } + + if ($this->trackoffset !== $v || $this->isNew()) { + $this->trackoffset = $v; + $this->modifiedColumns[] = CcBlockcontentsPeer::TRACKOFFSET; + } + + return $this; + } // setDbTrackOffset() + /** * Set the value of [cliplength] column. * @@ -529,6 +567,10 @@ abstract class BaseCcBlockcontents extends BaseObject implements Persistent */ public function hasOnlyDefaultValues() { + if ($this->trackoffset !== 0) { + return false; + } + if ($this->cliplength !== '00:00:00') { return false; } @@ -575,11 +617,12 @@ abstract class BaseCcBlockcontents extends BaseObject implements Persistent $this->block_id = ($row[$startcol + 1] !== null) ? (int) $row[$startcol + 1] : null; $this->file_id = ($row[$startcol + 2] !== null) ? (int) $row[$startcol + 2] : null; $this->position = ($row[$startcol + 3] !== null) ? (int) $row[$startcol + 3] : null; - $this->cliplength = ($row[$startcol + 4] !== null) ? (string) $row[$startcol + 4] : null; - $this->cuein = ($row[$startcol + 5] !== null) ? (string) $row[$startcol + 5] : null; - $this->cueout = ($row[$startcol + 6] !== null) ? (string) $row[$startcol + 6] : null; - $this->fadein = ($row[$startcol + 7] !== null) ? (string) $row[$startcol + 7] : null; - $this->fadeout = ($row[$startcol + 8] !== null) ? (string) $row[$startcol + 8] : null; + $this->trackoffset = ($row[$startcol + 4] !== null) ? (double) $row[$startcol + 4] : null; + $this->cliplength = ($row[$startcol + 5] !== null) ? (string) $row[$startcol + 5] : null; + $this->cuein = ($row[$startcol + 6] !== null) ? (string) $row[$startcol + 6] : null; + $this->cueout = ($row[$startcol + 7] !== null) ? (string) $row[$startcol + 7] : null; + $this->fadein = ($row[$startcol + 8] !== null) ? (string) $row[$startcol + 8] : null; + $this->fadeout = ($row[$startcol + 9] !== null) ? (string) $row[$startcol + 9] : null; $this->resetModified(); $this->setNew(false); @@ -588,7 +631,7 @@ abstract class BaseCcBlockcontents extends BaseObject implements Persistent $this->ensureConsistency(); } - return $startcol + 9; // 9 = CcBlockcontentsPeer::NUM_COLUMNS - CcBlockcontentsPeer::NUM_LAZY_LOAD_COLUMNS). + return $startcol + 10; // 10 = CcBlockcontentsPeer::NUM_COLUMNS - CcBlockcontentsPeer::NUM_LAZY_LOAD_COLUMNS). } catch (Exception $e) { throw new PropelException("Error populating CcBlockcontents object", $e); @@ -947,18 +990,21 @@ abstract class BaseCcBlockcontents extends BaseObject implements Persistent return $this->getDbPosition(); break; case 4: - return $this->getDbCliplength(); + return $this->getDbTrackOffset(); break; case 5: - return $this->getDbCuein(); + return $this->getDbCliplength(); break; case 6: - return $this->getDbCueout(); + return $this->getDbCuein(); break; case 7: - return $this->getDbFadein(); + return $this->getDbCueout(); break; case 8: + return $this->getDbFadein(); + break; + case 9: return $this->getDbFadeout(); break; default: @@ -989,11 +1035,12 @@ abstract class BaseCcBlockcontents extends BaseObject implements Persistent $keys[1] => $this->getDbBlockId(), $keys[2] => $this->getDbFileId(), $keys[3] => $this->getDbPosition(), - $keys[4] => $this->getDbCliplength(), - $keys[5] => $this->getDbCuein(), - $keys[6] => $this->getDbCueout(), - $keys[7] => $this->getDbFadein(), - $keys[8] => $this->getDbFadeout(), + $keys[4] => $this->getDbTrackOffset(), + $keys[5] => $this->getDbCliplength(), + $keys[6] => $this->getDbCuein(), + $keys[7] => $this->getDbCueout(), + $keys[8] => $this->getDbFadein(), + $keys[9] => $this->getDbFadeout(), ); if ($includeForeignObjects) { if (null !== $this->aCcFiles) { @@ -1046,18 +1093,21 @@ abstract class BaseCcBlockcontents extends BaseObject implements Persistent $this->setDbPosition($value); break; case 4: - $this->setDbCliplength($value); + $this->setDbTrackOffset($value); break; case 5: - $this->setDbCuein($value); + $this->setDbCliplength($value); break; case 6: - $this->setDbCueout($value); + $this->setDbCuein($value); break; case 7: - $this->setDbFadein($value); + $this->setDbCueout($value); break; case 8: + $this->setDbFadein($value); + break; + case 9: $this->setDbFadeout($value); break; } // switch() @@ -1088,11 +1138,12 @@ abstract class BaseCcBlockcontents extends BaseObject implements Persistent if (array_key_exists($keys[1], $arr)) $this->setDbBlockId($arr[$keys[1]]); if (array_key_exists($keys[2], $arr)) $this->setDbFileId($arr[$keys[2]]); if (array_key_exists($keys[3], $arr)) $this->setDbPosition($arr[$keys[3]]); - if (array_key_exists($keys[4], $arr)) $this->setDbCliplength($arr[$keys[4]]); - if (array_key_exists($keys[5], $arr)) $this->setDbCuein($arr[$keys[5]]); - if (array_key_exists($keys[6], $arr)) $this->setDbCueout($arr[$keys[6]]); - if (array_key_exists($keys[7], $arr)) $this->setDbFadein($arr[$keys[7]]); - if (array_key_exists($keys[8], $arr)) $this->setDbFadeout($arr[$keys[8]]); + if (array_key_exists($keys[4], $arr)) $this->setDbTrackOffset($arr[$keys[4]]); + if (array_key_exists($keys[5], $arr)) $this->setDbCliplength($arr[$keys[5]]); + if (array_key_exists($keys[6], $arr)) $this->setDbCuein($arr[$keys[6]]); + if (array_key_exists($keys[7], $arr)) $this->setDbCueout($arr[$keys[7]]); + if (array_key_exists($keys[8], $arr)) $this->setDbFadein($arr[$keys[8]]); + if (array_key_exists($keys[9], $arr)) $this->setDbFadeout($arr[$keys[9]]); } /** @@ -1108,6 +1159,7 @@ abstract class BaseCcBlockcontents extends BaseObject implements Persistent if ($this->isColumnModified(CcBlockcontentsPeer::BLOCK_ID)) $criteria->add(CcBlockcontentsPeer::BLOCK_ID, $this->block_id); if ($this->isColumnModified(CcBlockcontentsPeer::FILE_ID)) $criteria->add(CcBlockcontentsPeer::FILE_ID, $this->file_id); if ($this->isColumnModified(CcBlockcontentsPeer::POSITION)) $criteria->add(CcBlockcontentsPeer::POSITION, $this->position); + if ($this->isColumnModified(CcBlockcontentsPeer::TRACKOFFSET)) $criteria->add(CcBlockcontentsPeer::TRACKOFFSET, $this->trackoffset); if ($this->isColumnModified(CcBlockcontentsPeer::CLIPLENGTH)) $criteria->add(CcBlockcontentsPeer::CLIPLENGTH, $this->cliplength); if ($this->isColumnModified(CcBlockcontentsPeer::CUEIN)) $criteria->add(CcBlockcontentsPeer::CUEIN, $this->cuein); if ($this->isColumnModified(CcBlockcontentsPeer::CUEOUT)) $criteria->add(CcBlockcontentsPeer::CUEOUT, $this->cueout); @@ -1177,6 +1229,7 @@ abstract class BaseCcBlockcontents extends BaseObject implements Persistent $copyObj->setDbBlockId($this->block_id); $copyObj->setDbFileId($this->file_id); $copyObj->setDbPosition($this->position); + $copyObj->setDbTrackOffset($this->trackoffset); $copyObj->setDbCliplength($this->cliplength); $copyObj->setDbCuein($this->cuein); $copyObj->setDbCueout($this->cueout); @@ -1336,6 +1389,7 @@ abstract class BaseCcBlockcontents extends BaseObject implements Persistent $this->block_id = null; $this->file_id = null; $this->position = null; + $this->trackoffset = null; $this->cliplength = null; $this->cuein = null; $this->cueout = null; diff --git a/airtime_mvc/application/models/airtime/om/BaseCcBlockcontentsPeer.php b/airtime_mvc/application/models/airtime/om/BaseCcBlockcontentsPeer.php index 7bee15279..6cdb1e265 100644 --- a/airtime_mvc/application/models/airtime/om/BaseCcBlockcontentsPeer.php +++ b/airtime_mvc/application/models/airtime/om/BaseCcBlockcontentsPeer.php @@ -26,7 +26,7 @@ abstract class BaseCcBlockcontentsPeer { const TM_CLASS = 'CcBlockcontentsTableMap'; /** The total number of columns. */ - const NUM_COLUMNS = 9; + const NUM_COLUMNS = 10; /** The number of lazy-loaded columns. */ const NUM_LAZY_LOAD_COLUMNS = 0; @@ -43,6 +43,9 @@ abstract class BaseCcBlockcontentsPeer { /** the column name for the POSITION field */ const POSITION = 'cc_blockcontents.POSITION'; + /** the column name for the TRACKOFFSET field */ + const TRACKOFFSET = 'cc_blockcontents.TRACKOFFSET'; + /** the column name for the CLIPLENGTH field */ const CLIPLENGTH = 'cc_blockcontents.CLIPLENGTH'; @@ -74,12 +77,12 @@ abstract class BaseCcBlockcontentsPeer { * e.g. self::$fieldNames[self::TYPE_PHPNAME][0] = 'Id' */ private static $fieldNames = array ( - BasePeer::TYPE_PHPNAME => array ('DbId', 'DbBlockId', 'DbFileId', 'DbPosition', 'DbCliplength', 'DbCuein', 'DbCueout', 'DbFadein', 'DbFadeout', ), - BasePeer::TYPE_STUDLYPHPNAME => array ('dbId', 'dbBlockId', 'dbFileId', 'dbPosition', 'dbCliplength', 'dbCuein', 'dbCueout', 'dbFadein', 'dbFadeout', ), - BasePeer::TYPE_COLNAME => array (self::ID, self::BLOCK_ID, self::FILE_ID, self::POSITION, self::CLIPLENGTH, self::CUEIN, self::CUEOUT, self::FADEIN, self::FADEOUT, ), - BasePeer::TYPE_RAW_COLNAME => array ('ID', 'BLOCK_ID', 'FILE_ID', 'POSITION', 'CLIPLENGTH', 'CUEIN', 'CUEOUT', 'FADEIN', 'FADEOUT', ), - BasePeer::TYPE_FIELDNAME => array ('id', 'block_id', 'file_id', 'position', 'cliplength', 'cuein', 'cueout', 'fadein', 'fadeout', ), - BasePeer::TYPE_NUM => array (0, 1, 2, 3, 4, 5, 6, 7, 8, ) + BasePeer::TYPE_PHPNAME => array ('DbId', 'DbBlockId', 'DbFileId', 'DbPosition', 'DbTrackOffset', 'DbCliplength', 'DbCuein', 'DbCueout', 'DbFadein', 'DbFadeout', ), + BasePeer::TYPE_STUDLYPHPNAME => array ('dbId', 'dbBlockId', 'dbFileId', 'dbPosition', 'dbTrackOffset', 'dbCliplength', 'dbCuein', 'dbCueout', 'dbFadein', 'dbFadeout', ), + BasePeer::TYPE_COLNAME => array (self::ID, self::BLOCK_ID, self::FILE_ID, self::POSITION, self::TRACKOFFSET, self::CLIPLENGTH, self::CUEIN, self::CUEOUT, self::FADEIN, self::FADEOUT, ), + BasePeer::TYPE_RAW_COLNAME => array ('ID', 'BLOCK_ID', 'FILE_ID', 'POSITION', 'TRACKOFFSET', 'CLIPLENGTH', 'CUEIN', 'CUEOUT', 'FADEIN', 'FADEOUT', ), + BasePeer::TYPE_FIELDNAME => array ('id', 'block_id', 'file_id', 'position', 'trackoffset', 'cliplength', 'cuein', 'cueout', 'fadein', 'fadeout', ), + BasePeer::TYPE_NUM => array (0, 1, 2, 3, 4, 5, 6, 7, 8, 9, ) ); /** @@ -89,12 +92,12 @@ abstract class BaseCcBlockcontentsPeer { * e.g. self::$fieldNames[BasePeer::TYPE_PHPNAME]['Id'] = 0 */ private static $fieldKeys = array ( - BasePeer::TYPE_PHPNAME => array ('DbId' => 0, 'DbBlockId' => 1, 'DbFileId' => 2, 'DbPosition' => 3, 'DbCliplength' => 4, 'DbCuein' => 5, 'DbCueout' => 6, 'DbFadein' => 7, 'DbFadeout' => 8, ), - BasePeer::TYPE_STUDLYPHPNAME => array ('dbId' => 0, 'dbBlockId' => 1, 'dbFileId' => 2, 'dbPosition' => 3, 'dbCliplength' => 4, 'dbCuein' => 5, 'dbCueout' => 6, 'dbFadein' => 7, 'dbFadeout' => 8, ), - BasePeer::TYPE_COLNAME => array (self::ID => 0, self::BLOCK_ID => 1, self::FILE_ID => 2, self::POSITION => 3, self::CLIPLENGTH => 4, self::CUEIN => 5, self::CUEOUT => 6, self::FADEIN => 7, self::FADEOUT => 8, ), - BasePeer::TYPE_RAW_COLNAME => array ('ID' => 0, 'BLOCK_ID' => 1, 'FILE_ID' => 2, 'POSITION' => 3, 'CLIPLENGTH' => 4, 'CUEIN' => 5, 'CUEOUT' => 6, 'FADEIN' => 7, 'FADEOUT' => 8, ), - BasePeer::TYPE_FIELDNAME => array ('id' => 0, 'block_id' => 1, 'file_id' => 2, 'position' => 3, 'cliplength' => 4, 'cuein' => 5, 'cueout' => 6, 'fadein' => 7, 'fadeout' => 8, ), - BasePeer::TYPE_NUM => array (0, 1, 2, 3, 4, 5, 6, 7, 8, ) + BasePeer::TYPE_PHPNAME => array ('DbId' => 0, 'DbBlockId' => 1, 'DbFileId' => 2, 'DbPosition' => 3, 'DbTrackOffset' => 4, 'DbCliplength' => 5, 'DbCuein' => 6, 'DbCueout' => 7, 'DbFadein' => 8, 'DbFadeout' => 9, ), + BasePeer::TYPE_STUDLYPHPNAME => array ('dbId' => 0, 'dbBlockId' => 1, 'dbFileId' => 2, 'dbPosition' => 3, 'dbTrackOffset' => 4, 'dbCliplength' => 5, 'dbCuein' => 6, 'dbCueout' => 7, 'dbFadein' => 8, 'dbFadeout' => 9, ), + BasePeer::TYPE_COLNAME => array (self::ID => 0, self::BLOCK_ID => 1, self::FILE_ID => 2, self::POSITION => 3, self::TRACKOFFSET => 4, self::CLIPLENGTH => 5, self::CUEIN => 6, self::CUEOUT => 7, self::FADEIN => 8, self::FADEOUT => 9, ), + BasePeer::TYPE_RAW_COLNAME => array ('ID' => 0, 'BLOCK_ID' => 1, 'FILE_ID' => 2, 'POSITION' => 3, 'TRACKOFFSET' => 4, 'CLIPLENGTH' => 5, 'CUEIN' => 6, 'CUEOUT' => 7, 'FADEIN' => 8, 'FADEOUT' => 9, ), + BasePeer::TYPE_FIELDNAME => array ('id' => 0, 'block_id' => 1, 'file_id' => 2, 'position' => 3, 'trackoffset' => 4, 'cliplength' => 5, 'cuein' => 6, 'cueout' => 7, 'fadein' => 8, 'fadeout' => 9, ), + BasePeer::TYPE_NUM => array (0, 1, 2, 3, 4, 5, 6, 7, 8, 9, ) ); /** @@ -170,6 +173,7 @@ abstract class BaseCcBlockcontentsPeer { $criteria->addSelectColumn(CcBlockcontentsPeer::BLOCK_ID); $criteria->addSelectColumn(CcBlockcontentsPeer::FILE_ID); $criteria->addSelectColumn(CcBlockcontentsPeer::POSITION); + $criteria->addSelectColumn(CcBlockcontentsPeer::TRACKOFFSET); $criteria->addSelectColumn(CcBlockcontentsPeer::CLIPLENGTH); $criteria->addSelectColumn(CcBlockcontentsPeer::CUEIN); $criteria->addSelectColumn(CcBlockcontentsPeer::CUEOUT); @@ -180,6 +184,7 @@ abstract class BaseCcBlockcontentsPeer { $criteria->addSelectColumn($alias . '.BLOCK_ID'); $criteria->addSelectColumn($alias . '.FILE_ID'); $criteria->addSelectColumn($alias . '.POSITION'); + $criteria->addSelectColumn($alias . '.TRACKOFFSET'); $criteria->addSelectColumn($alias . '.CLIPLENGTH'); $criteria->addSelectColumn($alias . '.CUEIN'); $criteria->addSelectColumn($alias . '.CUEOUT'); diff --git a/airtime_mvc/application/models/airtime/om/BaseCcBlockcontentsQuery.php b/airtime_mvc/application/models/airtime/om/BaseCcBlockcontentsQuery.php index 6cc00d53c..f648e3640 100644 --- a/airtime_mvc/application/models/airtime/om/BaseCcBlockcontentsQuery.php +++ b/airtime_mvc/application/models/airtime/om/BaseCcBlockcontentsQuery.php @@ -10,6 +10,7 @@ * @method CcBlockcontentsQuery orderByDbBlockId($order = Criteria::ASC) Order by the block_id column * @method CcBlockcontentsQuery orderByDbFileId($order = Criteria::ASC) Order by the file_id column * @method CcBlockcontentsQuery orderByDbPosition($order = Criteria::ASC) Order by the position column + * @method CcBlockcontentsQuery orderByDbTrackOffset($order = Criteria::ASC) Order by the trackoffset column * @method CcBlockcontentsQuery orderByDbCliplength($order = Criteria::ASC) Order by the cliplength column * @method CcBlockcontentsQuery orderByDbCuein($order = Criteria::ASC) Order by the cuein column * @method CcBlockcontentsQuery orderByDbCueout($order = Criteria::ASC) Order by the cueout column @@ -20,6 +21,7 @@ * @method CcBlockcontentsQuery groupByDbBlockId() Group by the block_id column * @method CcBlockcontentsQuery groupByDbFileId() Group by the file_id column * @method CcBlockcontentsQuery groupByDbPosition() Group by the position column + * @method CcBlockcontentsQuery groupByDbTrackOffset() Group by the trackoffset column * @method CcBlockcontentsQuery groupByDbCliplength() Group by the cliplength column * @method CcBlockcontentsQuery groupByDbCuein() Group by the cuein column * @method CcBlockcontentsQuery groupByDbCueout() Group by the cueout column @@ -45,6 +47,7 @@ * @method CcBlockcontents findOneByDbBlockId(int $block_id) Return the first CcBlockcontents filtered by the block_id column * @method CcBlockcontents findOneByDbFileId(int $file_id) Return the first CcBlockcontents filtered by the file_id column * @method CcBlockcontents findOneByDbPosition(int $position) Return the first CcBlockcontents filtered by the position column + * @method CcBlockcontents findOneByDbTrackOffset(double $trackoffset) Return the first CcBlockcontents filtered by the trackoffset column * @method CcBlockcontents findOneByDbCliplength(string $cliplength) Return the first CcBlockcontents filtered by the cliplength column * @method CcBlockcontents findOneByDbCuein(string $cuein) Return the first CcBlockcontents filtered by the cuein column * @method CcBlockcontents findOneByDbCueout(string $cueout) Return the first CcBlockcontents filtered by the cueout column @@ -55,6 +58,7 @@ * @method array findByDbBlockId(int $block_id) Return CcBlockcontents objects filtered by the block_id column * @method array findByDbFileId(int $file_id) Return CcBlockcontents objects filtered by the file_id column * @method array findByDbPosition(int $position) Return CcBlockcontents objects filtered by the position column + * @method array findByDbTrackOffset(double $trackoffset) Return CcBlockcontents objects filtered by the trackoffset column * @method array findByDbCliplength(string $cliplength) Return CcBlockcontents objects filtered by the cliplength column * @method array findByDbCuein(string $cuein) Return CcBlockcontents objects filtered by the cuein column * @method array findByDbCueout(string $cueout) Return CcBlockcontents objects filtered by the cueout column @@ -279,6 +283,37 @@ abstract class BaseCcBlockcontentsQuery extends ModelCriteria return $this->addUsingAlias(CcBlockcontentsPeer::POSITION, $dbPosition, $comparison); } + /** + * Filter the query on the trackoffset column + * + * @param double|array $dbTrackOffset The value to use as filter. + * Accepts an associative array('min' => $minValue, 'max' => $maxValue) + * @param string $comparison Operator to use for the column comparison, defaults to Criteria::EQUAL + * + * @return CcBlockcontentsQuery The current query, for fluid interface + */ + public function filterByDbTrackOffset($dbTrackOffset = null, $comparison = null) + { + if (is_array($dbTrackOffset)) { + $useMinMax = false; + if (isset($dbTrackOffset['min'])) { + $this->addUsingAlias(CcBlockcontentsPeer::TRACKOFFSET, $dbTrackOffset['min'], Criteria::GREATER_EQUAL); + $useMinMax = true; + } + if (isset($dbTrackOffset['max'])) { + $this->addUsingAlias(CcBlockcontentsPeer::TRACKOFFSET, $dbTrackOffset['max'], Criteria::LESS_EQUAL); + $useMinMax = true; + } + if ($useMinMax) { + return $this; + } + if (null === $comparison) { + $comparison = Criteria::IN; + } + } + return $this->addUsingAlias(CcBlockcontentsPeer::TRACKOFFSET, $dbTrackOffset, $comparison); + } + /** * Filter the query on the cliplength column * diff --git a/airtime_mvc/application/models/airtime/om/BaseCcPlaylistcontents.php b/airtime_mvc/application/models/airtime/om/BaseCcPlaylistcontents.php index f8630b15b..cf6e53a98 100644 --- a/airtime_mvc/application/models/airtime/om/BaseCcPlaylistcontents.php +++ b/airtime_mvc/application/models/airtime/om/BaseCcPlaylistcontents.php @@ -67,6 +67,13 @@ abstract class BaseCcPlaylistcontents extends BaseObject implements Persistent */ protected $position; + /** + * The value for the trackoffset field. + * Note: this column has a database default value of: 0 + * @var double + */ + protected $trackoffset; + /** * The value for the cliplength field. * Note: this column has a database default value of: '00:00:00' @@ -143,6 +150,7 @@ abstract class BaseCcPlaylistcontents extends BaseObject implements Persistent public function applyDefaultValues() { $this->type = 0; + $this->trackoffset = 0; $this->cliplength = '00:00:00'; $this->cuein = '00:00:00'; $this->cueout = '00:00:00'; @@ -230,6 +238,16 @@ abstract class BaseCcPlaylistcontents extends BaseObject implements Persistent return $this->position; } + /** + * Get the [trackoffset] column value. + * + * @return double + */ + public function getDbTrackOffset() + { + return $this->trackoffset; + } + /** * Get the [cliplength] column value. * @@ -478,6 +496,26 @@ abstract class BaseCcPlaylistcontents extends BaseObject implements Persistent return $this; } // setDbPosition() + /** + * Set the value of [trackoffset] column. + * + * @param double $v new value + * @return CcPlaylistcontents The current object (for fluent API support) + */ + public function setDbTrackOffset($v) + { + if ($v !== null) { + $v = (double) $v; + } + + if ($this->trackoffset !== $v || $this->isNew()) { + $this->trackoffset = $v; + $this->modifiedColumns[] = CcPlaylistcontentsPeer::TRACKOFFSET; + } + + return $this; + } // setDbTrackOffset() + /** * Set the value of [cliplength] column. * @@ -652,6 +690,10 @@ abstract class BaseCcPlaylistcontents extends BaseObject implements Persistent return false; } + if ($this->trackoffset !== 0) { + return false; + } + if ($this->cliplength !== '00:00:00') { return false; } @@ -701,11 +743,12 @@ abstract class BaseCcPlaylistcontents extends BaseObject implements Persistent $this->stream_id = ($row[$startcol + 4] !== null) ? (int) $row[$startcol + 4] : null; $this->type = ($row[$startcol + 5] !== null) ? (int) $row[$startcol + 5] : null; $this->position = ($row[$startcol + 6] !== null) ? (int) $row[$startcol + 6] : null; - $this->cliplength = ($row[$startcol + 7] !== null) ? (string) $row[$startcol + 7] : null; - $this->cuein = ($row[$startcol + 8] !== null) ? (string) $row[$startcol + 8] : null; - $this->cueout = ($row[$startcol + 9] !== null) ? (string) $row[$startcol + 9] : null; - $this->fadein = ($row[$startcol + 10] !== null) ? (string) $row[$startcol + 10] : null; - $this->fadeout = ($row[$startcol + 11] !== null) ? (string) $row[$startcol + 11] : null; + $this->trackoffset = ($row[$startcol + 7] !== null) ? (double) $row[$startcol + 7] : null; + $this->cliplength = ($row[$startcol + 8] !== null) ? (string) $row[$startcol + 8] : null; + $this->cuein = ($row[$startcol + 9] !== null) ? (string) $row[$startcol + 9] : null; + $this->cueout = ($row[$startcol + 10] !== null) ? (string) $row[$startcol + 10] : null; + $this->fadein = ($row[$startcol + 11] !== null) ? (string) $row[$startcol + 11] : null; + $this->fadeout = ($row[$startcol + 12] !== null) ? (string) $row[$startcol + 12] : null; $this->resetModified(); $this->setNew(false); @@ -714,7 +757,7 @@ abstract class BaseCcPlaylistcontents extends BaseObject implements Persistent $this->ensureConsistency(); } - return $startcol + 12; // 12 = CcPlaylistcontentsPeer::NUM_COLUMNS - CcPlaylistcontentsPeer::NUM_LAZY_LOAD_COLUMNS). + return $startcol + 13; // 13 = CcPlaylistcontentsPeer::NUM_COLUMNS - CcPlaylistcontentsPeer::NUM_LAZY_LOAD_COLUMNS). } catch (Exception $e) { throw new PropelException("Error populating CcPlaylistcontents object", $e); @@ -1099,18 +1142,21 @@ abstract class BaseCcPlaylistcontents extends BaseObject implements Persistent return $this->getDbPosition(); break; case 7: - return $this->getDbCliplength(); + return $this->getDbTrackOffset(); break; case 8: - return $this->getDbCuein(); + return $this->getDbCliplength(); break; case 9: - return $this->getDbCueout(); + return $this->getDbCuein(); break; case 10: - return $this->getDbFadein(); + return $this->getDbCueout(); break; case 11: + return $this->getDbFadein(); + break; + case 12: return $this->getDbFadeout(); break; default: @@ -1144,11 +1190,12 @@ abstract class BaseCcPlaylistcontents extends BaseObject implements Persistent $keys[4] => $this->getDbStreamId(), $keys[5] => $this->getDbType(), $keys[6] => $this->getDbPosition(), - $keys[7] => $this->getDbCliplength(), - $keys[8] => $this->getDbCuein(), - $keys[9] => $this->getDbCueout(), - $keys[10] => $this->getDbFadein(), - $keys[11] => $this->getDbFadeout(), + $keys[7] => $this->getDbTrackOffset(), + $keys[8] => $this->getDbCliplength(), + $keys[9] => $this->getDbCuein(), + $keys[10] => $this->getDbCueout(), + $keys[11] => $this->getDbFadein(), + $keys[12] => $this->getDbFadeout(), ); if ($includeForeignObjects) { if (null !== $this->aCcFiles) { @@ -1213,18 +1260,21 @@ abstract class BaseCcPlaylistcontents extends BaseObject implements Persistent $this->setDbPosition($value); break; case 7: - $this->setDbCliplength($value); + $this->setDbTrackOffset($value); break; case 8: - $this->setDbCuein($value); + $this->setDbCliplength($value); break; case 9: - $this->setDbCueout($value); + $this->setDbCuein($value); break; case 10: - $this->setDbFadein($value); + $this->setDbCueout($value); break; case 11: + $this->setDbFadein($value); + break; + case 12: $this->setDbFadeout($value); break; } // switch() @@ -1258,11 +1308,12 @@ abstract class BaseCcPlaylistcontents extends BaseObject implements Persistent if (array_key_exists($keys[4], $arr)) $this->setDbStreamId($arr[$keys[4]]); if (array_key_exists($keys[5], $arr)) $this->setDbType($arr[$keys[5]]); if (array_key_exists($keys[6], $arr)) $this->setDbPosition($arr[$keys[6]]); - if (array_key_exists($keys[7], $arr)) $this->setDbCliplength($arr[$keys[7]]); - if (array_key_exists($keys[8], $arr)) $this->setDbCuein($arr[$keys[8]]); - if (array_key_exists($keys[9], $arr)) $this->setDbCueout($arr[$keys[9]]); - if (array_key_exists($keys[10], $arr)) $this->setDbFadein($arr[$keys[10]]); - if (array_key_exists($keys[11], $arr)) $this->setDbFadeout($arr[$keys[11]]); + if (array_key_exists($keys[7], $arr)) $this->setDbTrackOffset($arr[$keys[7]]); + if (array_key_exists($keys[8], $arr)) $this->setDbCliplength($arr[$keys[8]]); + if (array_key_exists($keys[9], $arr)) $this->setDbCuein($arr[$keys[9]]); + if (array_key_exists($keys[10], $arr)) $this->setDbCueout($arr[$keys[10]]); + if (array_key_exists($keys[11], $arr)) $this->setDbFadein($arr[$keys[11]]); + if (array_key_exists($keys[12], $arr)) $this->setDbFadeout($arr[$keys[12]]); } /** @@ -1281,6 +1332,7 @@ abstract class BaseCcPlaylistcontents extends BaseObject implements Persistent if ($this->isColumnModified(CcPlaylistcontentsPeer::STREAM_ID)) $criteria->add(CcPlaylistcontentsPeer::STREAM_ID, $this->stream_id); if ($this->isColumnModified(CcPlaylistcontentsPeer::TYPE)) $criteria->add(CcPlaylistcontentsPeer::TYPE, $this->type); if ($this->isColumnModified(CcPlaylistcontentsPeer::POSITION)) $criteria->add(CcPlaylistcontentsPeer::POSITION, $this->position); + if ($this->isColumnModified(CcPlaylistcontentsPeer::TRACKOFFSET)) $criteria->add(CcPlaylistcontentsPeer::TRACKOFFSET, $this->trackoffset); if ($this->isColumnModified(CcPlaylistcontentsPeer::CLIPLENGTH)) $criteria->add(CcPlaylistcontentsPeer::CLIPLENGTH, $this->cliplength); if ($this->isColumnModified(CcPlaylistcontentsPeer::CUEIN)) $criteria->add(CcPlaylistcontentsPeer::CUEIN, $this->cuein); if ($this->isColumnModified(CcPlaylistcontentsPeer::CUEOUT)) $criteria->add(CcPlaylistcontentsPeer::CUEOUT, $this->cueout); @@ -1353,6 +1405,7 @@ abstract class BaseCcPlaylistcontents extends BaseObject implements Persistent $copyObj->setDbStreamId($this->stream_id); $copyObj->setDbType($this->type); $copyObj->setDbPosition($this->position); + $copyObj->setDbTrackOffset($this->trackoffset); $copyObj->setDbCliplength($this->cliplength); $copyObj->setDbCuein($this->cuein); $copyObj->setDbCueout($this->cueout); @@ -1564,6 +1617,7 @@ abstract class BaseCcPlaylistcontents extends BaseObject implements Persistent $this->stream_id = null; $this->type = null; $this->position = null; + $this->trackoffset = null; $this->cliplength = null; $this->cuein = null; $this->cueout = null; diff --git a/airtime_mvc/application/models/airtime/om/BaseCcPlaylistcontentsPeer.php b/airtime_mvc/application/models/airtime/om/BaseCcPlaylistcontentsPeer.php index d90906568..0e23f44c3 100644 --- a/airtime_mvc/application/models/airtime/om/BaseCcPlaylistcontentsPeer.php +++ b/airtime_mvc/application/models/airtime/om/BaseCcPlaylistcontentsPeer.php @@ -26,7 +26,7 @@ abstract class BaseCcPlaylistcontentsPeer { const TM_CLASS = 'CcPlaylistcontentsTableMap'; /** The total number of columns. */ - const NUM_COLUMNS = 12; + const NUM_COLUMNS = 13; /** The number of lazy-loaded columns. */ const NUM_LAZY_LOAD_COLUMNS = 0; @@ -52,6 +52,9 @@ abstract class BaseCcPlaylistcontentsPeer { /** the column name for the POSITION field */ const POSITION = 'cc_playlistcontents.POSITION'; + /** the column name for the TRACKOFFSET field */ + const TRACKOFFSET = 'cc_playlistcontents.TRACKOFFSET'; + /** the column name for the CLIPLENGTH field */ const CLIPLENGTH = 'cc_playlistcontents.CLIPLENGTH'; @@ -83,12 +86,12 @@ abstract class BaseCcPlaylistcontentsPeer { * e.g. self::$fieldNames[self::TYPE_PHPNAME][0] = 'Id' */ private static $fieldNames = array ( - BasePeer::TYPE_PHPNAME => array ('DbId', 'DbPlaylistId', 'DbFileId', 'DbBlockId', 'DbStreamId', 'DbType', 'DbPosition', 'DbCliplength', 'DbCuein', 'DbCueout', 'DbFadein', 'DbFadeout', ), - BasePeer::TYPE_STUDLYPHPNAME => array ('dbId', 'dbPlaylistId', 'dbFileId', 'dbBlockId', 'dbStreamId', 'dbType', 'dbPosition', 'dbCliplength', 'dbCuein', 'dbCueout', 'dbFadein', 'dbFadeout', ), - BasePeer::TYPE_COLNAME => array (self::ID, self::PLAYLIST_ID, self::FILE_ID, self::BLOCK_ID, self::STREAM_ID, self::TYPE, self::POSITION, self::CLIPLENGTH, self::CUEIN, self::CUEOUT, self::FADEIN, self::FADEOUT, ), - BasePeer::TYPE_RAW_COLNAME => array ('ID', 'PLAYLIST_ID', 'FILE_ID', 'BLOCK_ID', 'STREAM_ID', 'TYPE', 'POSITION', 'CLIPLENGTH', 'CUEIN', 'CUEOUT', 'FADEIN', 'FADEOUT', ), - BasePeer::TYPE_FIELDNAME => array ('id', 'playlist_id', 'file_id', 'block_id', 'stream_id', 'type', 'position', 'cliplength', 'cuein', 'cueout', 'fadein', 'fadeout', ), - BasePeer::TYPE_NUM => array (0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, ) + BasePeer::TYPE_PHPNAME => array ('DbId', 'DbPlaylistId', 'DbFileId', 'DbBlockId', 'DbStreamId', 'DbType', 'DbPosition', 'DbTrackOffset', 'DbCliplength', 'DbCuein', 'DbCueout', 'DbFadein', 'DbFadeout', ), + BasePeer::TYPE_STUDLYPHPNAME => array ('dbId', 'dbPlaylistId', 'dbFileId', 'dbBlockId', 'dbStreamId', 'dbType', 'dbPosition', 'dbTrackOffset', 'dbCliplength', 'dbCuein', 'dbCueout', 'dbFadein', 'dbFadeout', ), + BasePeer::TYPE_COLNAME => array (self::ID, self::PLAYLIST_ID, self::FILE_ID, self::BLOCK_ID, self::STREAM_ID, self::TYPE, self::POSITION, self::TRACKOFFSET, self::CLIPLENGTH, self::CUEIN, self::CUEOUT, self::FADEIN, self::FADEOUT, ), + BasePeer::TYPE_RAW_COLNAME => array ('ID', 'PLAYLIST_ID', 'FILE_ID', 'BLOCK_ID', 'STREAM_ID', 'TYPE', 'POSITION', 'TRACKOFFSET', 'CLIPLENGTH', 'CUEIN', 'CUEOUT', 'FADEIN', 'FADEOUT', ), + BasePeer::TYPE_FIELDNAME => array ('id', 'playlist_id', 'file_id', 'block_id', 'stream_id', 'type', 'position', 'trackoffset', 'cliplength', 'cuein', 'cueout', 'fadein', 'fadeout', ), + BasePeer::TYPE_NUM => array (0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, ) ); /** @@ -98,12 +101,12 @@ abstract class BaseCcPlaylistcontentsPeer { * e.g. self::$fieldNames[BasePeer::TYPE_PHPNAME]['Id'] = 0 */ private static $fieldKeys = array ( - BasePeer::TYPE_PHPNAME => array ('DbId' => 0, 'DbPlaylistId' => 1, 'DbFileId' => 2, 'DbBlockId' => 3, 'DbStreamId' => 4, 'DbType' => 5, 'DbPosition' => 6, 'DbCliplength' => 7, 'DbCuein' => 8, 'DbCueout' => 9, 'DbFadein' => 10, 'DbFadeout' => 11, ), - BasePeer::TYPE_STUDLYPHPNAME => array ('dbId' => 0, 'dbPlaylistId' => 1, 'dbFileId' => 2, 'dbBlockId' => 3, 'dbStreamId' => 4, 'dbType' => 5, 'dbPosition' => 6, 'dbCliplength' => 7, 'dbCuein' => 8, 'dbCueout' => 9, 'dbFadein' => 10, 'dbFadeout' => 11, ), - BasePeer::TYPE_COLNAME => array (self::ID => 0, self::PLAYLIST_ID => 1, self::FILE_ID => 2, self::BLOCK_ID => 3, self::STREAM_ID => 4, self::TYPE => 5, self::POSITION => 6, self::CLIPLENGTH => 7, self::CUEIN => 8, self::CUEOUT => 9, self::FADEIN => 10, self::FADEOUT => 11, ), - BasePeer::TYPE_RAW_COLNAME => array ('ID' => 0, 'PLAYLIST_ID' => 1, 'FILE_ID' => 2, 'BLOCK_ID' => 3, 'STREAM_ID' => 4, 'TYPE' => 5, 'POSITION' => 6, 'CLIPLENGTH' => 7, 'CUEIN' => 8, 'CUEOUT' => 9, 'FADEIN' => 10, 'FADEOUT' => 11, ), - BasePeer::TYPE_FIELDNAME => array ('id' => 0, 'playlist_id' => 1, 'file_id' => 2, 'block_id' => 3, 'stream_id' => 4, 'type' => 5, 'position' => 6, 'cliplength' => 7, 'cuein' => 8, 'cueout' => 9, 'fadein' => 10, 'fadeout' => 11, ), - BasePeer::TYPE_NUM => array (0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, ) + BasePeer::TYPE_PHPNAME => array ('DbId' => 0, 'DbPlaylistId' => 1, 'DbFileId' => 2, 'DbBlockId' => 3, 'DbStreamId' => 4, 'DbType' => 5, 'DbPosition' => 6, 'DbTrackOffset' => 7, 'DbCliplength' => 8, 'DbCuein' => 9, 'DbCueout' => 10, 'DbFadein' => 11, 'DbFadeout' => 12, ), + BasePeer::TYPE_STUDLYPHPNAME => array ('dbId' => 0, 'dbPlaylistId' => 1, 'dbFileId' => 2, 'dbBlockId' => 3, 'dbStreamId' => 4, 'dbType' => 5, 'dbPosition' => 6, 'dbTrackOffset' => 7, 'dbCliplength' => 8, 'dbCuein' => 9, 'dbCueout' => 10, 'dbFadein' => 11, 'dbFadeout' => 12, ), + BasePeer::TYPE_COLNAME => array (self::ID => 0, self::PLAYLIST_ID => 1, self::FILE_ID => 2, self::BLOCK_ID => 3, self::STREAM_ID => 4, self::TYPE => 5, self::POSITION => 6, self::TRACKOFFSET => 7, self::CLIPLENGTH => 8, self::CUEIN => 9, self::CUEOUT => 10, self::FADEIN => 11, self::FADEOUT => 12, ), + BasePeer::TYPE_RAW_COLNAME => array ('ID' => 0, 'PLAYLIST_ID' => 1, 'FILE_ID' => 2, 'BLOCK_ID' => 3, 'STREAM_ID' => 4, 'TYPE' => 5, 'POSITION' => 6, 'TRACKOFFSET' => 7, 'CLIPLENGTH' => 8, 'CUEIN' => 9, 'CUEOUT' => 10, 'FADEIN' => 11, 'FADEOUT' => 12, ), + BasePeer::TYPE_FIELDNAME => array ('id' => 0, 'playlist_id' => 1, 'file_id' => 2, 'block_id' => 3, 'stream_id' => 4, 'type' => 5, 'position' => 6, 'trackoffset' => 7, 'cliplength' => 8, 'cuein' => 9, 'cueout' => 10, 'fadein' => 11, 'fadeout' => 12, ), + BasePeer::TYPE_NUM => array (0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, ) ); /** @@ -182,6 +185,7 @@ abstract class BaseCcPlaylistcontentsPeer { $criteria->addSelectColumn(CcPlaylistcontentsPeer::STREAM_ID); $criteria->addSelectColumn(CcPlaylistcontentsPeer::TYPE); $criteria->addSelectColumn(CcPlaylistcontentsPeer::POSITION); + $criteria->addSelectColumn(CcPlaylistcontentsPeer::TRACKOFFSET); $criteria->addSelectColumn(CcPlaylistcontentsPeer::CLIPLENGTH); $criteria->addSelectColumn(CcPlaylistcontentsPeer::CUEIN); $criteria->addSelectColumn(CcPlaylistcontentsPeer::CUEOUT); @@ -195,6 +199,7 @@ abstract class BaseCcPlaylistcontentsPeer { $criteria->addSelectColumn($alias . '.STREAM_ID'); $criteria->addSelectColumn($alias . '.TYPE'); $criteria->addSelectColumn($alias . '.POSITION'); + $criteria->addSelectColumn($alias . '.TRACKOFFSET'); $criteria->addSelectColumn($alias . '.CLIPLENGTH'); $criteria->addSelectColumn($alias . '.CUEIN'); $criteria->addSelectColumn($alias . '.CUEOUT'); diff --git a/airtime_mvc/application/models/airtime/om/BaseCcPlaylistcontentsQuery.php b/airtime_mvc/application/models/airtime/om/BaseCcPlaylistcontentsQuery.php index 25ea29378..ef3d3877a 100644 --- a/airtime_mvc/application/models/airtime/om/BaseCcPlaylistcontentsQuery.php +++ b/airtime_mvc/application/models/airtime/om/BaseCcPlaylistcontentsQuery.php @@ -13,6 +13,7 @@ * @method CcPlaylistcontentsQuery orderByDbStreamId($order = Criteria::ASC) Order by the stream_id column * @method CcPlaylistcontentsQuery orderByDbType($order = Criteria::ASC) Order by the type column * @method CcPlaylistcontentsQuery orderByDbPosition($order = Criteria::ASC) Order by the position column + * @method CcPlaylistcontentsQuery orderByDbTrackOffset($order = Criteria::ASC) Order by the trackoffset column * @method CcPlaylistcontentsQuery orderByDbCliplength($order = Criteria::ASC) Order by the cliplength column * @method CcPlaylistcontentsQuery orderByDbCuein($order = Criteria::ASC) Order by the cuein column * @method CcPlaylistcontentsQuery orderByDbCueout($order = Criteria::ASC) Order by the cueout column @@ -26,6 +27,7 @@ * @method CcPlaylistcontentsQuery groupByDbStreamId() Group by the stream_id column * @method CcPlaylistcontentsQuery groupByDbType() Group by the type column * @method CcPlaylistcontentsQuery groupByDbPosition() Group by the position column + * @method CcPlaylistcontentsQuery groupByDbTrackOffset() Group by the trackoffset column * @method CcPlaylistcontentsQuery groupByDbCliplength() Group by the cliplength column * @method CcPlaylistcontentsQuery groupByDbCuein() Group by the cuein column * @method CcPlaylistcontentsQuery groupByDbCueout() Group by the cueout column @@ -58,6 +60,7 @@ * @method CcPlaylistcontents findOneByDbStreamId(int $stream_id) Return the first CcPlaylistcontents filtered by the stream_id column * @method CcPlaylistcontents findOneByDbType(int $type) Return the first CcPlaylistcontents filtered by the type column * @method CcPlaylistcontents findOneByDbPosition(int $position) Return the first CcPlaylistcontents filtered by the position column + * @method CcPlaylistcontents findOneByDbTrackOffset(double $trackoffset) Return the first CcPlaylistcontents filtered by the trackoffset column * @method CcPlaylistcontents findOneByDbCliplength(string $cliplength) Return the first CcPlaylistcontents filtered by the cliplength column * @method CcPlaylistcontents findOneByDbCuein(string $cuein) Return the first CcPlaylistcontents filtered by the cuein column * @method CcPlaylistcontents findOneByDbCueout(string $cueout) Return the first CcPlaylistcontents filtered by the cueout column @@ -71,6 +74,7 @@ * @method array findByDbStreamId(int $stream_id) Return CcPlaylistcontents objects filtered by the stream_id column * @method array findByDbType(int $type) Return CcPlaylistcontents objects filtered by the type column * @method array findByDbPosition(int $position) Return CcPlaylistcontents objects filtered by the position column + * @method array findByDbTrackOffset(double $trackoffset) Return CcPlaylistcontents objects filtered by the trackoffset column * @method array findByDbCliplength(string $cliplength) Return CcPlaylistcontents objects filtered by the cliplength column * @method array findByDbCuein(string $cuein) Return CcPlaylistcontents objects filtered by the cuein column * @method array findByDbCueout(string $cueout) Return CcPlaylistcontents objects filtered by the cueout column @@ -388,6 +392,37 @@ abstract class BaseCcPlaylistcontentsQuery extends ModelCriteria return $this->addUsingAlias(CcPlaylistcontentsPeer::POSITION, $dbPosition, $comparison); } + /** + * Filter the query on the trackoffset column + * + * @param double|array $dbTrackOffset The value to use as filter. + * Accepts an associative array('min' => $minValue, 'max' => $maxValue) + * @param string $comparison Operator to use for the column comparison, defaults to Criteria::EQUAL + * + * @return CcPlaylistcontentsQuery The current query, for fluid interface + */ + public function filterByDbTrackOffset($dbTrackOffset = null, $comparison = null) + { + if (is_array($dbTrackOffset)) { + $useMinMax = false; + if (isset($dbTrackOffset['min'])) { + $this->addUsingAlias(CcPlaylistcontentsPeer::TRACKOFFSET, $dbTrackOffset['min'], Criteria::GREATER_EQUAL); + $useMinMax = true; + } + if (isset($dbTrackOffset['max'])) { + $this->addUsingAlias(CcPlaylistcontentsPeer::TRACKOFFSET, $dbTrackOffset['max'], Criteria::LESS_EQUAL); + $useMinMax = true; + } + if ($useMinMax) { + return $this; + } + if (null === $comparison) { + $comparison = Criteria::IN; + } + } + return $this->addUsingAlias(CcPlaylistcontentsPeer::TRACKOFFSET, $dbTrackOffset, $comparison); + } + /** * Filter the query on the cliplength column * diff --git a/airtime_mvc/application/views/scripts/form/preferences_general.phtml b/airtime_mvc/application/views/scripts/form/preferences_general.phtml index 08db4abc5..a438fb446 100644 --- a/airtime_mvc/application/views/scripts/form/preferences_general.phtml +++ b/airtime_mvc/application/views/scripts/form/preferences_general.phtml @@ -14,14 +14,40 @@ -
- +
+
-
- element->getElement('stationDefaultFade') ?> - element->getElement('stationDefaultFade')->hasErrors()) : ?> +
+ element->getElement('stationDefaultFadeIn') ?> + element->getElement('stationDefaultFadeIn')->hasErrors()) : ?> + +
+
+ +
+
+ element->getElement('stationDefaultFadeOut') ?> + element->getElement('stationDefaultFadeOut')->hasErrors()) : ?> + + +
+
+ +
+
+ element->getElement('stationDefaultCrossfadeDuration') ?> + element->getElement('stationDefaultCrossfadeDuration')->hasErrors()) : ?> + diff --git a/airtime_mvc/application/views/scripts/playlist/set-cue.phtml b/airtime_mvc/application/views/scripts/playlist/set-cue.phtml index 176d8b8ac..8dee1ca3f 100644 --- a/airtime_mvc/application/views/scripts/playlist/set-cue.phtml +++ b/airtime_mvc/application/views/scripts/playlist/set-cue.phtml @@ -1,11 +1,14 @@
+
+ +
-
+
cueIn; ?>
-
+
cueOut; ?>
diff --git a/airtime_mvc/application/views/scripts/playlist/set-fade.phtml b/airtime_mvc/application/views/scripts/playlist/set-fade.phtml index 17f1e7c60..510ed441e 100644 --- a/airtime_mvc/application/views/scripts/playlist/set-fade.phtml +++ b/airtime_mvc/application/views/scripts/playlist/set-fade.phtml @@ -1,16 +1,23 @@
+
+ +
item1Type == 0) {?>
-
+
fadeOut; ?>
item2Type == 0) {?>
-
+
fadeIn; ?>
-
+
diff --git a/airtime_mvc/application/views/scripts/playlist/update.phtml b/airtime_mvc/application/views/scripts/playlist/update.phtml index 20ef17665..7c718dd59 100644 --- a/airtime_mvc/application/views/scripts/playlist/update.phtml +++ b/airtime_mvc/application/views/scripts/playlist/update.phtml @@ -8,6 +8,15 @@ if ($item['type'] == 2) { $bl= new Application_Model_Block($item['item_id']); $staticBlock = $bl->isStatic(); } +else if ($item['type'] == 0) { + $audiofile = Application_Model_StoredFile::RecallById($item['item_id']); + $fileUrl = $audiofile->getFileUrl(); +} + +if (($i < count($items) -1) && ($items[$i+1]['type'] == 0)) { + $nextAudiofile = Application_Model_StoredFile::RecallById($items[$i+1]['item_id']); + $nextFileUrl = $nextAudiofile->getFileUrl(); +} ?>
  • " unqid="">
    @@ -65,6 +74,7 @@ if ($item['type'] == 2) { 'id' => $item["id"], 'cueIn' => $item['cuein'], 'cueOut' => $item['cueout'], + 'uri' => $fileUrl, 'origLength' => $item['orig_length'])); ?>
    @@ -80,8 +90,16 @@ if ($item['type'] == 2) { 'item2' => $items[$i+1]['id'], 'item1Type' => $items[$i]['type'], 'item2Type' => $items[$i+1]['type'], + 'item1Url' => $fileUrl, + 'item2Url' => $nextFileUrl, 'fadeOut' => $items[$i]['fadeout'], - 'fadeIn' => $items[$i+1]['fadein'])); ?> + 'fadeIn' => $items[$i+1]['fadein'], + 'offset' => $items[$i]['trackSec'] - $items[$i+1]['trackoffset'], + 'cueIn1' => $items[$i]['cueInSec'], + 'cueOut1' => $items[$i]['cueOutSec'], + 'cueIn2' => $items[$i+1]['cueInSec'], + 'cueOut2' => $items[$i+1]['cueOutSec']) + ); ?> diff --git a/airtime_mvc/build/schema.xml b/airtime_mvc/build/schema.xml index c0ec104dd..2c24d3c71 100644 --- a/airtime_mvc/build/schema.xml +++ b/airtime_mvc/build/schema.xml @@ -231,6 +231,7 @@ --> + @@ -269,6 +270,7 @@ + diff --git a/airtime_mvc/build/sql/schema.sql b/airtime_mvc/build/sql/schema.sql index ecd63a8f2..3c5110cfb 100644 --- a/airtime_mvc/build/sql/schema.sql +++ b/airtime_mvc/build/sql/schema.sql @@ -298,6 +298,7 @@ CREATE TABLE "cc_playlistcontents" "stream_id" INTEGER, "type" INT2 default 0 NOT NULL, "position" INTEGER, + "trackoffset" FLOAT default 0 NOT NULL, "cliplength" interval default '00:00:00', "cuein" interval default '00:00:00', "cueout" interval default '00:00:00', @@ -347,6 +348,7 @@ CREATE TABLE "cc_blockcontents" "block_id" INTEGER, "file_id" INTEGER, "position" INTEGER, + "trackoffset" FLOAT default 0 NOT NULL, "cliplength" interval default '00:00:00', "cuein" interval default '00:00:00', "cueout" interval default '00:00:00', diff --git a/airtime_mvc/public/css/playlist_builder.css b/airtime_mvc/public/css/playlist_builder.css index 43b942a7f..7d828bd31 100644 --- a/airtime_mvc/public/css/playlist_builder.css +++ b/airtime_mvc/public/css/playlist_builder.css @@ -28,7 +28,7 @@ height: 28px; margin: 0 7px 20px 0; }*/ -#side_playlist input,#side_playlist textarea { +#side_playlist textarea { width: 200px; } @@ -585,3 +585,28 @@ li.spl_empty { .expand-block-separate { border-top: 1px solid #5B5B5B; } + +.channel-wrapper { + position: relative; +} + +.channel { + position: absolute; + margin: 0; + padding: 0; +} + +.state-select { + cursor: text; +} + +.playlist-tracks { + overflow-x: auto; + overflow-y: hidden; +} + +.playlist-fade { + position: absolute; + background-color: rgba(0,0,0,0.1); + z-index: 1000; +} \ No newline at end of file diff --git a/airtime_mvc/public/js/airtime/library/spl.js b/airtime_mvc/public/js/airtime/library/spl.js index 93d1cd80d..a34ce0f08 100644 --- a/airtime_mvc/public/js/airtime/library/spl.js +++ b/airtime_mvc/public/js/airtime/library/spl.js @@ -137,6 +137,84 @@ var AIRTIME = (function(AIRTIME){ highlightActive(li.find('.spl_cue')); }); } + + /* used from waveform pop-up */ + function changeCues($el, id, cueIn, cueOut) { + + var url = baseUrl+"Playlist/set-cue", + lastMod = getModified(), + type = $('#obj_type').val(), + li; + + if (!isTimeValid(cueIn)){ + $el.find('.cue-in-error').val($.i18n._("please put in a time '00:00:00 (.0)'")).show(); + return; + } + else { + $el.find('.cue-in-error').hide(); + } + + if (!isTimeValid(cueOut)){ + $el.find('.cue-out-error').val($.i18n._("please put in a time '00:00:00 (.0)'")).show(); + return; + } + else { + $el.find('.cue-out-error').hide(); + } + + $.post(url, + {format: "json", cueIn: cueIn, cueOut: cueOut, id: id, modified: lastMod, type: type}, + function(json){ + + $el.dialog('destroy'); + $el.remove(); + + if (json.error !== undefined){ + playlistError(json); + return; + } + if (json.cue_error !== undefined) { + showError(span, json.cue_error); + return; + } + + setPlaylistContent(json); + + li = $('#side_playlist li[unqid='+id+']'); + li.find(".cue-edit").toggle(); + highlightActive(li); + highlightActive(li.find('.spl_cue')); + }); + } + + /* used from waveform pop-up */ + function changeCrossfade($el, id1, id2, fadeIn, fadeOut, offset) { + + var url = baseUrl+"Playlist/set-crossfade", + lastMod = getModified(), + type = $('#obj_type').val(), + li, id; + + $.post(url, + {format: "json", fadeIn: fadeIn, fadeOut: fadeOut, id1: id1, id2: id2, offset: offset, modified: lastMod, type: type}, + function(json){ + + $el.dialog('destroy'); + $el.remove(); + + if (json.error !== undefined){ + playlistError(json); + return; + } + + setPlaylistContent(json); + + id = id1 === undefined ? id2 : id1; + li = $('#side_playlist li[unqid='+id+']'); + li.find('.crossfade').toggle(); + highlightActive(li.find('.spl_fade_control')); + }); + } function changeFadeIn(event) { event.preventDefault(); @@ -292,6 +370,8 @@ var AIRTIME = (function(AIRTIME){ } function setPlaylistContent(json) { + var $html = $(json.html); + $('#spl_name > a') .empty() .append(json.name); @@ -305,7 +385,7 @@ var AIRTIME = (function(AIRTIME){ $('#spl_sortable').off('focusout keydown'); $('#spl_sortable') .empty() - .append(json.html); + .append($html); setCueEvents(); setFadeEvents(); setModified(json.modified); @@ -515,6 +595,11 @@ var AIRTIME = (function(AIRTIME){ temp.on("focusout", ".spl_cue_out span", changeCueOut); temp.on("keydown", ".spl_cue_out span", submitOnEnter); + + //remove show waveform buttons since web audio api is not supported. + if (!(window.AudioContext || window.webkitAudioContext)) { + temp.find('.pl-waveform-cues-btn').parent().remove(); + } } //sets events dynamically for the fade editor. @@ -525,6 +610,11 @@ var AIRTIME = (function(AIRTIME){ temp.on("focusout", ".spl_fade_out span", changeFadeOut); temp.on("keydown", ".spl_fade_out span", submitOnEnter); + + //remove show waveform buttons since web audio api is not supported. + if (!(window.AudioContext || window.webkitAudioContext)) { + temp.find('.pl-waveform-fades-btn').parent().remove(); + } } function initialEvents() { @@ -1061,6 +1151,206 @@ var AIRTIME = (function(AIRTIME){ playlistRequest(sUrl, oData); }; + mod.showFadesWaveform = function(e) { + var $el = $(e.target), + $parent = $el.parents("dl"), + $fadeOut = $parent.find(".spl_fade_out"), + $fadeIn = $parent.find(".spl_fade_in"), + $html = $($("#tmpl-pl-fades").html()), + tracks = [], + dim = AIRTIME.utilities.findViewportDimensions(), + playlistEditor, + id1, id2; + + function removeDialog() { + playlistEditor.stop(); + + $html.dialog("destroy"); + $html.remove(); + } + + if ($fadeOut.length > 0) { + + tracks.push({ + src: $fadeOut.data("fadeout"), + cuein: $fadeOut.data("cuein"), + cueout: $fadeOut.data("cueout"), + fades: [{ + shape: $fadeOut.data("type"), + type: "FadeOut", + end: $fadeOut.data("cueout") - $fadeOut.data("cuein"), + start: $fadeOut.data("cueout") - $fadeOut.data("cuein") - $fadeOut.data("length") + }], + states: { + 'fadein': false, + 'shift': false + } + }); + + id1 = $fadeOut.data("item"); + } + + if ($fadeIn.length > 0) { + + tracks.push({ + src: $fadeIn.data("fadein"), + start: $fadeIn.data("offset"), + cuein: $fadeIn.data("cuein"), + cueout: $fadeIn.data("cueout"), + fades: [{ + shape: $fadeIn.data("type"), + type: "FadeIn", + end: $fadeIn.data("length"), + start: 0 + }], + states: { + 'fadeout': false, + 'shift': false + } + }); + + id2 = $fadeIn.data("item"); + } + + //set the first track to not be moveable (might only be one track depending on what follows) + //tracks[0].states["shift"] = false; + + $html.dialog({ + modal: true, + title: "Fade Editor", + show: 'clip', + hide: 'clip', + width: dim.width - 100, + height: dim.height - 100, + buttons: [ + {text: "Cancel", click: removeDialog}, + {text: "Save", click: function() { + var json = playlistEditor.getJson(), + offset, + fadeIn, fadeOut, + fade; + + playlistEditor.stop(); + + if (json.length === 1) { + + fade = json[0]["fades"][0]; + + if (fade["type"] === "FadeOut") { + fadeOut = fade["end"] - fade["start"]; + } + else { + fadeIn = fade["end"] - fade["start"]; + } + } + else { + + offset = json[0]["end"] - json[1]["start"]; + + fade = json[0]["fades"][0]; + fadeOut = fade["end"] - fade["start"]; + + fade = json[1]["fades"][0]; + fadeIn = fade["end"] - fade["start"]; + } + + changeCrossfade($html, id1, id2, fadeIn.toFixed(1), fadeOut.toFixed(1), offset); + }} + ], + open: function (event, ui) { + + var config = new Config({ + resolution: 15000, + state: "shift", + mono: true, + waveHeight: 80, + container: $html[0], + UITheme: "jQueryUI", + timeFormat: 'hh:mm:ss.u' + }); + + playlistEditor = new PlaylistEditor(); + playlistEditor.setConfig(config); + playlistEditor.init(tracks); + }, + close: removeDialog + }); + }; + + mod.showCuesWaveform = function(e) { + var $el = $(e.target), + $li = $el.parents("li"), + id = $li.attr("unqid"), + $parent = $el.parent(), + uri = $parent.data("uri"), + $html = $($("#tmpl-pl-cues").html()), + tracks = [{ + src: uri + }], + cueIn = $li.find('.spl_cue_in').data("cueIn"), + cueOut = $li.find('.spl_cue_out').data("cueOut"), + dim = AIRTIME.utilities.findViewportDimensions(), + playlistEditor; + + function removeDialog() { + playlistEditor.stop(); + + $html.dialog("destroy"); + $html.remove(); + } + + $html.find('.editor-cue-in').val(cueIn); + $html.find('.editor-cue-out').val(cueOut); + + $html.on("click", ".set-cue-in", function(e) { + var cueIn = $html.find('.audio_start').val(); + + $html.find('.editor-cue-in').val(cueIn); + }); + + $html.on("click", ".set-cue-out", function(e) { + var cueOut = $html.find('.audio_end').val(); + + $html.find('.editor-cue-out').val(cueOut); + }); + + $html.dialog({ + modal: true, + title: "Cue Editor", + show: 'clip', + hide: 'clip', + width: dim.width - 100, + height: dim.height - 100, + buttons: [ + {text: "Cancel", click: removeDialog}, + {text: "Save", click: function() { + var cueIn = $html.find('.editor-cue-in').val(), + cueOut = $html.find('.editor-cue-out').val(); + + playlistEditor.stop(); + + changeCues($html, id, cueIn, cueOut); + }} + ], + open: function (event, ui) { + + var config = new Config({ + resolution: 15000, + mono: true, + waveHeight: 80, + container: $html[0], + UITheme: "jQueryUI", + timeFormat: 'hh:mm:ss.u' + }); + + playlistEditor = new PlaylistEditor(); + playlistEditor.setConfig(config); + playlistEditor.init(tracks); + }, + close: removeDialog + }); + }; + mod.init = function() { /* $.contextMenu({ @@ -1089,6 +1379,14 @@ var AIRTIME = (function(AIRTIME){ AIRTIME.playlist.fnWsDelete(); }}); + $pl.delegate(".pl-waveform-cues-btn", {"click": function(ev){ + AIRTIME.playlist.showCuesWaveform(ev); + }}); + + $pl.delegate(".pl-waveform-fades-btn", {"click": function(ev){ + AIRTIME.playlist.showFadesWaveform(ev); + }}); + setPlaylistEntryEvents(); setCueEvents(); setFadeEvents(); diff --git a/airtime_mvc/public/js/waveformplaylist/config.js b/airtime_mvc/public/js/waveformplaylist/config.js new file mode 100644 index 000000000..7b0ae3ecf --- /dev/null +++ b/airtime_mvc/public/js/waveformplaylist/config.js @@ -0,0 +1,171 @@ +/* + Stores configuration settings for the playlist builder. + A container object (ex a div) must be passed in, the playlist will be built on this element. +*/ + +var Config = function(params) { + + var that = this, + defaultParams; + + defaultParams = { + + ac: new (window.AudioContext || window.webkitAudioContext), + + resolution: 4096, //resolution - samples per pixel to draw. + timeFormat: 'hh:mm:ss.uuu', + mono: true, //whether to draw multiple channels or combine them. + fadeType: 'logarithmic', + + timescale: false, //whether or not to include the time measure. + + UITheme: "default", // bootstrap || jQueryUI || default + + waveColor: 'grey', + progressColor: 'orange', + loadingColor: 'purple', + cursorColor: 'green', + markerColor: 'green', + selectBorderColor: 'red', + selectBackgroundColor: 'rgba(0,0,0,0.1)', + + timeColor: 'grey', + fontColor: 'black', + fadeColor: 'black', + + waveHeight: 128, //height of each canvas element a waveform is on. + + trackscroll: { + left: 0, + top: 0 + }, + + state: 'select', + + cursorPos: 0 //value is kept in seconds. + }; + + params = Object.create(params); + Object.keys(defaultParams).forEach(function(key) { + if (!(key in params)) { + params[key] = defaultParams[key]; + } + }); + + + /* + Start of all getter methods for config. + */ + + that.getContainer = function getContainer() { + return params.container; + }; + + that.isTimeScaleEnabled = function isTimeScaleEnabled() { + return params.timescale; + }; + + that.getFadeType = function getFadeType() { + return params.fadeType; + }; + + that.isDisplayMono = function isDisplayMono() { + return params.mono; + }; + + that.getUITheme = function getUITheme() { + return params.UITheme; + }; + + that.getCursorPos = function getCursorPos() { + return params.cursorPos; + }; + + that.getState = function getState() { + return params.state; + }; + + that.getAudioContext = function getAudioContext() { + return params.ac; + }; + + that.getSampleRate = function getSampleRate() { + return params.ac.sampleRate; + }; + + that.getCurrentTime = function getCurrentTime() { + return params.ac.currentTime; + }; + + that.getTimeFormat = function getTimeFormat() { + return params.timeFormat; + }; + + that.getResolution = function getResolution() { + return params.resolution; + }; + + that.getWaveHeight = function getWaveHeight() { + return params.waveHeight; + }; + + that.getColorScheme = function getColorScheme() { + return { + waveColor: params.waveColor, + progressColor: params.progressColor, + loadingColor: params.loadingColor, + cursorColor: params.cursorColor, + markerColor: params.markerColor, + timeColor: params.timeColor, + fontColor: params.fontColor, + fadeColor: params.fadeColor, + selectBorderColor: params.selectBorderColor, + selectBackgroundColor: params.selectBackgroundColor, + }; + }; + + that.getTrackScroll = function getTrackScroll() { + var scroll = params.trackscroll; + + return { + left: scroll.left, + top: scroll.top + }; + }; + + + /* + Start of all setter methods for config. + */ + + that.setResolution = function setResolution(resolution) { + params.resolution = resolution; + }; + + that.setTimeFormat = function setTimeFormat(format) { + params.timeFormat = format; + }; + + that.setFadeType = function setFadeType(type) { + params.fadeType = type; + }; + + that.setDisplayMono = function setDisplayMono(bool) { + params.mono = bool; + }; + + that.setCursorPos = function setCursorPos(pos) { + params.cursorPos = pos; + }; + + that.setState = function setState(state) { + params.state = state; + }; + + that.setTrackScroll = function setTrackScroll(left, top) { + var scroll = params.trackscroll; + + scroll.left = left; + scroll.top = top; + }; +}; diff --git a/airtime_mvc/public/js/waveformplaylist/controls.js b/airtime_mvc/public/js/waveformplaylist/controls.js new file mode 100644 index 000000000..d83af02f0 --- /dev/null +++ b/airtime_mvc/public/js/waveformplaylist/controls.js @@ -0,0 +1,562 @@ +'use strict'; + +var AudioControls = function() { + +}; + +AudioControls.prototype.groups = { + "audio-select": ["btns_audio_tools", "btns_fade"] +}; + +AudioControls.prototype.classes = { + "btn-state-active": "btn btn-mini active", + "btn-state-default": "btn btn-mini", + "disabled": "disabled", + "active": "active" +}; + +AudioControls.prototype.events = { + "btn_rewind": { + click: "rewindAudio" + }, + + "btn_play": { + click: "playAudio" + }, + + "btn_stop": { + click: "stopAudio" + }, + + "btn_cursor": { + click: "changeState" + }, + + "btn_select": { + click: "changeState" + }, + + "btn_shift": { + click: "changeState" + }, + + "btn_fadein": { + click: "changeState" + }, + + "btn_fadeout": { + click: "changeState" + }, + + + "btns_fade": { + click: "createFade" + }, + + "btn_save": { + click: "save" + }, + + "btn_open": { + click: "open" + }, + + "btn_trim_audio": { + click: "trimAudio" + }, + + "time_format": { + change: "changeTimeFormat" + }, + + "audio_start": { + blur: "validateCueIn" + }, + + "audio_end": { + blur: "validateCueOut" + }, + + "audio_pos": { + + }, + + "audio_resolution": { + change: "changeResolution" + } +}; + +AudioControls.prototype.validateCue = function(value) { + var validators, + regex, + result; + + validators = { + "seconds": /^\d+$/, + + "thousandths": /^\d+\.\d{3}$/, + + "hh:mm:ss": /^[0-9]{2,}:[0-5][0-9]:[0-5][0-9]$/, + + "hh:mm:ss.u": /^[0-9]{2,}:[0-5][0-9]:[0-5][0-9]\.\d{1}$/, + + "hh:mm:ss.uu": /^[0-9]{2,}:[0-5][0-9]:[0-5][0-9]\.\d{2}$/, + + "hh:mm:ss.uuu": /^[0-9]{2,}:[0-5][0-9]:[0-5][0-9]\.\d{3}$/ + }; + + regex = validators[this.timeFormat]; + result = regex.test(value); + + return result; +}; + +AudioControls.prototype.cueToSeconds = function(value) { + var converter, + func, + seconds; + + function clockConverter(value) { + var data = value.split(":"), + hours = parseInt(data[0], 10) * 3600, + mins = parseInt(data[1], 10) * 60, + secs = parseFloat(data[2]), + seconds; + + seconds = hours + mins + secs; + + return seconds; + } + + converter = { + "seconds": function(value) { + return parseInt(value, 10); + }, + + "thousandths": function(value) { + return parseFloat(value); + }, + + "hh:mm:ss": function(value) { + return clockConverter(value); + }, + + "hh:mm:ss.u": function(value) { + return clockConverter(value); + }, + + "hh:mm:ss.uu": function(value) { + return clockConverter(value); + }, + + "hh:mm:ss.uuu": function(value) { + return clockConverter(value); + } + }; + + func = converter[this.timeFormat]; + seconds = func(value); + + return seconds; +}; + +AudioControls.prototype.cueFormatters = function(format) { + + function clockFormat(seconds, decimals) { + var hours, + minutes, + secs, + result; + + hours = parseInt(seconds / 3600, 10) % 24; + minutes = parseInt(seconds / 60, 10) % 60; + secs = seconds % 60; + secs = secs.toFixed(decimals); + + result = (hours < 10 ? "0" + hours : hours) + ":" + (minutes < 10 ? "0" + minutes : minutes) + ":" + (secs < 10 ? "0" + secs : secs); + + return result; + } + + var formats = { + "seconds": function (seconds) { + return seconds.toFixed(0); + }, + + "thousandths": function (seconds) { + return seconds.toFixed(3); + }, + + "hh:mm:ss": function (seconds) { + return clockFormat(seconds, 0); + }, + + "hh:mm:ss.u": function (seconds) { + return clockFormat(seconds, 1); + }, + + "hh:mm:ss.uu": function (seconds) { + return clockFormat(seconds, 2); + }, + + "hh:mm:ss.uuu": function (seconds) { + return clockFormat(seconds, 3); + } + }; + + return formats[format]; +}; + +AudioControls.prototype.init = function(config) { + var that = this, + className, + event, + events = this.events, + tmpEl, + func, + state, + container, + tmpBtn; + + makePublisher(this); + + this.ctrls = {}; + this.config = config; + container = this.config.getContainer(); + state = this.config.getState(); + + tmpBtn = document.getElementsByClassName("btn_"+state)[0]; + + if (tmpBtn) { + this.activateButton(tmpBtn); + } + + for (className in events) { + + tmpEl = container.getElementsByClassName(className)[0]; + this.ctrls[className] = tmpEl; + + for (event in events[className]) { + + if (tmpEl) { + func = that[events[className][event]].bind(that); + tmpEl.addEventListener(event, func); + } + } + } + + if (this.ctrls["time_format"]) { + this.ctrls["time_format"].value = this.config.getTimeFormat(); + } + + if (this.ctrls["audio_resolution"]) { + this.ctrls["audio_resolution"].value = this.config.getResolution(); + } + + this.timeFormat = this.config.getTimeFormat(); + + //Kept in seconds so time format change can update fields easily. + this.currentSelectionValues = undefined; + + this.onCursorSelection({ + start: 0, + end: 0 + }); +}; + +AudioControls.prototype.changeTimeFormat = function(e) { + var format = e.target.value, + func, start, end; + + format = (this.cueFormatters(format) !== undefined) ? format : "hh:mm:ss"; + this.config.setTimeFormat(format); + this.timeFormat = format; + + if (this.currentSelectionValues !== undefined) { + func = this.cueFormatters(format); + start = this.currentSelectionValues.start; + end = this.currentSelectionValues.end; + + if (this.ctrls["audio_start"]) { + this.ctrls["audio_start"].value = func(start); + } + + if (this.ctrls["audio_end"]) { + this.ctrls["audio_end"].value = func(end); + } + } +}; + +AudioControls.prototype.changeResolution = function(e) { + var res = parseInt(e.target.value, 10); + + this.config.setResolution(res); + this.fire("changeresolution", res); +}; + +AudioControls.prototype.validateCueIn = function(e) { + var value = e.target.value, + end, + startSecs; + + if (this.validateCue(value)) { + end = this.currentSelectionValues.end; + startSecs = this.cueToSeconds(value); + + if (startSecs <= end) { + this.notifySelectionUpdate(startSecs, end); + this.currentSelectionValues.start = startSecs; + return; + } + } + + //time entered was otherwise invalid. + e.target.value = this.cueFormatters(this.timeFormat)(this.currentSelectionValues.start); +}; + +AudioControls.prototype.validateCueOut = function(e) { + var value = e.target.value, + start, + endSecs; + + if (this.validateCue(value)) { + start = this.currentSelectionValues.start; + endSecs = this.cueToSeconds(value); + + if (endSecs >= start) { + this.notifySelectionUpdate(start, endSecs); + this.currentSelectionValues.end = endSecs; + return; + } + } + + //time entered was otherwise invalid. + e.target.value = this.cueFormatters(this.timeFormat)(this.currentSelectionValues.end); +}; + +AudioControls.prototype.activateButtonGroup = function(id) { + var el = document.getElementById(id), + btns, + classes = this.classes, + i, len; + + if (el === null) { + return; + } + + btns = el.getElementsByTagName("a"); + + for (i = 0, len = btns.length; i < len; i++) { + btns[i].classList.remove(classes["disabled"]); + } +}; + +AudioControls.prototype.deactivateButtonGroup = function(id) { + var el = document.getElementById(id), + btns, + classes = this.classes, + i, len; + + if (el === null) { + return; + } + + btns = el.getElementsByTagName("a"); + + for (i = 0, len = btns.length; i < len; i++) { + btns[i].classList.add(classes["disabled"]); + } +}; + +AudioControls.prototype.activateAudioSelection = function() { + var ids = this.groups["audio-select"], + i, len; + + for (i = 0, len = ids.length; i < len; i++) { + this.activateButtonGroup(ids[i]); + } +}; + +AudioControls.prototype.deactivateAudioSelection = function() { + var ids = this.groups["audio-select"], + i, len; + + for (i = 0, len = ids.length; i < len; i++) { + this.deactivateButtonGroup(ids[i]); + } +}; + +AudioControls.prototype.save = function() { + + this.fire('playlistsave', this); +}; + +AudioControls.prototype.open = function() { + + this.fire('playlistrestore', this); +}; + +AudioControls.prototype.rewindAudio = function() { + + this.fire('rewindaudio', this); +}; + +AudioControls.prototype.playAudio = function() { + + this.fire('playaudio', this); +}; + +AudioControls.prototype.stopAudio = function() { + + this.fire('stopaudio', this); +}; + +AudioControls.prototype.activateButton = function(el) { + if (el) { + el.classList.add(this.classes["active"]); + } +}; + +AudioControls.prototype.deactivateButton = function(el) { + if (el) { + el.classList.remove(this.classes["active"]); + } +}; + +AudioControls.prototype.enableButton = function(el) { + if (el) { + el.classList.remove(this.classes["disabled"]); + } +}; + +AudioControls.prototype.disableButton = function(el) { + if (el) { + el.classList.add(this.classes["disabled"]); + } +}; + +AudioControls.prototype.changeState = function(e) { + var el = e.currentTarget, + prevEl = el.parentElement.getElementsByClassName('active')[0], + state = el.dataset.state; + + this.deactivateButton(prevEl); + this.activateButton(el); + + this.config.setState(state); + this.fire('changestate', this); +}; + +AudioControls.prototype.zeroCrossing = function(e) { + var el = e.target, + disabled, + classes = this.classes; + + disabled = el.classList.contains(classes["disabled"]); + + if (!disabled) { + this.fire('trackedit', { + type: "zeroCrossing" + }); + } +}; + +AudioControls.prototype.trimAudio = function(e) { + var el = e.target, + disabled, + classes = this.classes; + + disabled = el.classList.contains(classes["disabled"]); + + if (!disabled) { + this.fire('trackedit', { + type: "trimAudio" + }); + } +}; + +AudioControls.prototype.removeAudio = function(e) { + var el = e.target, + disabled, + classes = this.classes; + + disabled = el.classList.contains(classes["disabled"]); + + if (!disabled) { + this.fire('trackedit', { + type: "removeAudio" + }); + } +}; + +AudioControls.prototype.createFade = function(e) { + var el = e.target, + shape = el.dataset.shape, + type = el.dataset.type, + disabled, + classes = this.classes; + + disabled = el.classList.contains(classes["disabled"]); + + if (!disabled) { + this.fire('trackedit', { + type: "createFade", + args: { + type: type, + shape: shape + } + }); + } +}; + +AudioControls.prototype.onAudioSelection = function() { + this.activateAudioSelection(); +}; + +AudioControls.prototype.onAudioDeselection = function() { + this.deactivateAudioSelection(); +}; + +/* + start, end in seconds +*/ +AudioControls.prototype.notifySelectionUpdate = function(start, end) { + + this.fire('changeselection', { + start: start, + end: end + }); +}; + +/* + start, end in seconds +*/ +AudioControls.prototype.onCursorSelection = function(args) { + var startFormat = this.cueFormatters(this.timeFormat)(args.start), + endFormat = this.cueFormatters(this.timeFormat)(args.end), + start = this.cueToSeconds(startFormat), + end = this.cueToSeconds(endFormat); + + this.currentSelectionValues = { + start: start, + end:end + }; + + if (this.ctrls["audio_start"]) { + this.ctrls["audio_start"].value = startFormat; + } + + if (this.ctrls["audio_end"]) { + this.ctrls["audio_end"].value = endFormat; + } +}; + +/* + args {seconds, pixels} +*/ +AudioControls.prototype.onAudioUpdate = function(args) { + if (this.ctrls["audio_pos"]) { + this.ctrls["audio_pos"].value = this.cueFormatters(this.timeFormat)(args.seconds); + } +}; + diff --git a/airtime_mvc/public/js/waveformplaylist/curves.js b/airtime_mvc/public/js/waveformplaylist/curves.js new file mode 100644 index 000000000..17c364050 --- /dev/null +++ b/airtime_mvc/public/js/waveformplaylist/curves.js @@ -0,0 +1,72 @@ +var Curves = {}; + +Curves.createLinearBuffer = function createLinearBuffer(length, rotation) { + var curve = new Float32Array(length), + i, x, + scale = length - 1; + + for (i = 0; i < length; i++) { + x = i / scale; + + if (rotation > 0) { + curve[i] = x; + } + else { + curve[i] = 1 - x; + } + } + return curve; +}; + +Curves.createExponentialBuffer = function createExponentialBuffer(length, rotation) { + var curve = new Float32Array(length), + i, x, + scale = length - 1, + index; + + for (i = 0; i < length; i++) { + x = i / scale; + index = rotation > 0 ? i : length - 1 - i; + + curve[index] = Math.exp(2 * x - 1) / Math.exp(1); + } + return curve; +}; + +//creating a curve to simulate an S-curve with setValueCurveAtTime. +Curves.createSCurveBuffer = function createSCurveBuffer(length, phase) { + var curve = new Float32Array(length), + i; + + for (i = 0; i < length; ++i) { + curve[i] = (Math.sin((Math.PI * i / length) - phase)) /2 + 0.5; + } + return curve; +}; + +//creating a curve to simulate a logarithmic curve with setValueCurveAtTime. +Curves.createLogarithmicBuffer = function createLogarithmicBuffer(length, base, rotation) { + var curve = new Float32Array(length), + index, + key = ""+length+base+rotation, + store = [], + x = 0, + i; + + if (store[key]) { + return store[key]; + } + + for (i = 0; i < length; i++) { + //index for the curve array. + index = rotation > 0 ? i : length - 1 - i; + + x = i / length; + curve[index] = Math.log(1 + base*x) / Math.log(1 + base); + } + + store[key] = curve; + + return curve; +}; + diff --git a/airtime_mvc/public/js/waveformplaylist/fades.js b/airtime_mvc/public/js/waveformplaylist/fades.js new file mode 100644 index 000000000..059bda629 --- /dev/null +++ b/airtime_mvc/public/js/waveformplaylist/fades.js @@ -0,0 +1,140 @@ +var Fades = function() {}; + +Fades.prototype.init = function init(sampleRate) { + + this.sampleRate = sampleRate; +} + +/* +The setValueCurveAtTime method +Sets an array of arbitrary parameter values starting at the given time for the given duration. The number of values will be scaled to fit into the desired duration. + +The values parameter is a Float32Array representing a parameter value curve. These values will apply starting at the given time and lasting for the given duration. + +The startTime parameter is the time in the same time coordinate system as AudioContext.currentTime. + +The duration parameter is the amount of time in seconds (after the time parameter) where values will be calculated according to the values parameter.. + +During the time interval: startTime <= t < startTime + duration, values will be calculated: + + v(t) = values[N * (t - startTime) / duration], where N is the length of the values array. + +After the end of the curve time interval (t >= startTime + duration), the value will remain constant at the final curve value, until there is another automation event (if any). +*/ + +Fades.prototype.sCurveFadeIn = function sCurveFadeIn(gain, start, duration, options) { + var curve; + + curve = Curves.createSCurveBuffer(this.sampleRate, (Math.PI/2)); + gain.setValueCurveAtTime(curve, start, duration); +}; + +Fades.prototype.sCurveFadeOut = function sCurveFadeOut(gain, start, duration, options) { + var curve; + + curve = Curves.createSCurveBuffer(this.sampleRate, -(Math.PI/2)); + gain.setValueCurveAtTime(curve, start, duration); +}; + +/* + +The linearRampToValueAtTime method +Schedules a linear continuous change in parameter value from the previous scheduled parameter value to the given value. + +The value parameter is the value the parameter will linearly ramp to at the given time. + +The endTime parameter is the time in the same time coordinate system as AudioContext.currentTime. + +The value during the time interval T0 <= t < T1 (where T0 is the time of the previous event and T1 is the endTime parameter passed into this method) will be calculated as: + + v(t) = V0 + (V1 - V0) * ((t - T0) / (T1 - T0)) + +Where V0 is the value at the time T0 and V1 is the value parameter passed into this method. + +If there are no more events after this LinearRampToValue event then for t >= T1, v(t) = V1 + +*/ +Fades.prototype.linearFadeIn = function linearFadeIn(gain, start, duration, options) { + + gain.linearRampToValueAtTime(0, start); + gain.linearRampToValueAtTime(1, start + duration); +}; + +Fades.prototype.linearFadeOut = function linearFadeOut(gain, start, duration, options) { + + gain.linearRampToValueAtTime(1, start); + gain.linearRampToValueAtTime(0, start + duration); +}; + +/* +DOES NOT WORK PROPERLY USING 0 + +The exponentialRampToValueAtTime method +Schedules an exponential continuous change in parameter value from the previous scheduled parameter value to the given value. Parameters representing filter frequencies and playback rate are best changed exponentially because of the way humans perceive sound. + +The value parameter is the value the parameter will exponentially ramp to at the given time. An exception will be thrown if this value is less than or equal to 0, or if the value at the time of the previous event is less than or equal to 0. + +The endTime parameter is the time in the same time coordinate system as AudioContext.currentTime. + +The value during the time interval T0 <= t < T1 (where T0 is the time of the previous event and T1 is the endTime parameter passed into this method) will be calculated as: + + v(t) = V0 * (V1 / V0) ^ ((t - T0) / (T1 - T0)) + +Where V0 is the value at the time T0 and V1 is the value parameter passed into this method. + +If there are no more events after this ExponentialRampToValue event then for t >= T1, v(t) = V1 +*/ +Fades.prototype.exponentialFadeIn = function exponentialFadeIn(gain, start, duration, options) { + + gain.exponentialRampToValueAtTime(0.01, start); + gain.exponentialRampToValueAtTime(1, start + duration); +}; + +Fades.prototype.exponentialFadeOut = function exponentialFadeOut(gain, start, duration, options) { + + gain.exponentialRampToValueAtTime(1, start); + gain.exponentialRampToValueAtTime(0.01, start + duration); +}; + +Fades.prototype.logarithmicFadeIn = function logarithmicFadeIn(gain, start, duration, options) { + var curve, + base = options.base; + + base = typeof base !== 'undefined' ? base : 10; + + curve = Curves.createLogarithmicBuffer(this.sampleRate, base, 1); + gain.setValueCurveAtTime(curve, start, duration); +}; + +Fades.prototype.logarithmicFadeOut = function logarithmicFadeOut(gain, start, duration, options) { + var curve, + base = options.base; + + base = typeof base !== 'undefined' ? base : 10; + + curve = Curves.createLogarithmicBuffer(this.sampleRate, base, -1); + gain.setValueCurveAtTime(curve, start, duration); +}; + +/** + Calls the appropriate fade type with options + + options { + start, + duration, + base (for logarithmic) + } +*/ +Fades.prototype.createFadeIn = function createFadeIn(gain, type, options) { + var method = type + "FadeIn", + fn = this[method]; + + fn.call(this, gain, options.start, options.duration, options); +}; + +Fades.prototype.createFadeOut = function createFadeOut(gain, type, options) { + var method = type + "FadeOut", + fn = this[method]; + + fn.call(this, gain, options.start, options.duration, options); +}; diff --git a/airtime_mvc/public/js/waveformplaylist/loader.js b/airtime_mvc/public/js/waveformplaylist/loader.js new file mode 100644 index 000000000..03e5ae080 --- /dev/null +++ b/airtime_mvc/public/js/waveformplaylist/loader.js @@ -0,0 +1,86 @@ +'use strict'; + +var BufferLoader = function() { + +} + +BufferLoader.prototype.init = function(params) { + + var loader = this; + loader.context = params.context; + loader.bufferList = []; + loader.loadCount = 0; + + loader.defaultParams = { + + }; + + loader.params = Object.create(params); + Object.keys(loader.defaultParams).forEach(function (key) { + if (!(key in params)) { params[key] = loader.defaultParams[key]; } + }); +} + +BufferLoader.prototype.requestBuffer = function(url, name) { + var loader = this, + request = new XMLHttpRequest(); + + request.open("GET", url, true); + request.responseType = "arraybuffer"; + + request.onload = function() { + loader.context.decodeAudioData(request.response, function(buffer) { + if (!buffer) { + alert('error decoding file data: '+url); + return; + } + + loader.loadCount++; + loader.onAudioFileLoad(name, buffer); + + if (loader.loadCount === loader.urlList.length) { + loader.onAudioFilesDone(loader.bufferList); + } + }, + function(error) { + console.error('decodeAudioData error',error); + }); + } + + request.onerror = function(){ + alert('BufferLoader: XHR error'); + }; + + request.send(); +}; + +BufferLoader.prototype.loadAudio = function(aUrls, callback) { + + var names=[]; + var paths=[]; + + for (var name in aUrls) { + var path = aUrls[name]; + names.push(name); + paths.push(path); + } + + this.urlList = paths; + + var i, + length; + + for (i = 0, length = paths.length; i < length; i++) { + this.requestBuffer(paths[i], names[i]); + } +} + +BufferLoader.prototype.onAudioFileLoad = function(name, buffer) { + + this.bufferList[name] = buffer; +} + +BufferLoader.prototype.onAudioFilesDone = function(bufferList) { + var fn = this.params.onComplete; + fn(bufferList); +} diff --git a/airtime_mvc/public/js/waveformplaylist/local_storage.js b/airtime_mvc/public/js/waveformplaylist/local_storage.js new file mode 100644 index 000000000..f814f173d --- /dev/null +++ b/airtime_mvc/public/js/waveformplaylist/local_storage.js @@ -0,0 +1,16 @@ +var Storage = function() {}; + +Storage.prototype.save = function save(name, playlist) { + var json = JSON.stringify(playlist); + + localStorage.setItem(name, json); +}; + +Storage.prototype.restore = function restore(name) { + var JSONstring = localStorage.getItem(name), + data; + + data = JSON.parse(JSONstring); + + return data; +}; diff --git a/airtime_mvc/public/js/waveformplaylist/observer/observer.js b/airtime_mvc/public/js/waveformplaylist/observer/observer.js new file mode 100644 index 000000000..5be2bb3d9 --- /dev/null +++ b/airtime_mvc/public/js/waveformplaylist/observer/observer.js @@ -0,0 +1,57 @@ +/* +Code taken from http://www.jspatterns.com/book/7/observer-game.html + +Pub/Sub +*/ + +var publisher = { + subscribers: { + any: [] + }, + on: function (type, fn, context) { + type = type || 'any'; + fn = typeof fn === "function" ? fn : context[fn]; + + if (typeof this.subscribers[type] === "undefined") { + this.subscribers[type] = []; + } + this.subscribers[type].push({fn: fn, context: context || this}); + }, + remove: function (type, fn, context) { + this.visitSubscribers('unsubscribe', type, fn, context); + }, + fire: function (type, publication) { + this.visitSubscribers('publish', type, publication); + }, + reset: function (type) { + this.subscribers[type] = undefined; + }, + visitSubscribers: function (action, type, arg, context) { + var pubtype = type || 'any', + subscribers = this.subscribers[pubtype], + i, + max = subscribers ? subscribers.length : 0; + + for (i = 0; i < max; i += 1) { + if (action === 'publish') { + subscribers[i].fn.call(subscribers[i].context, arg); + } + else { + if (subscribers[i].fn === arg && subscribers[i].context === context) { + subscribers.splice(i, 1); + } + } + } + } +}; + + +function makePublisher(o) { + var i; + for (i in publisher) { + if (publisher.hasOwnProperty(i) && typeof publisher[i] === "function") { + o[i] = publisher[i]; + } + } + o.subscribers = {any: []}; +} diff --git a/airtime_mvc/public/js/waveformplaylist/observer/observer.js~ b/airtime_mvc/public/js/waveformplaylist/observer/observer.js~ new file mode 100644 index 000000000..2367a50f9 --- /dev/null +++ b/airtime_mvc/public/js/waveformplaylist/observer/observer.js~ @@ -0,0 +1,57 @@ +/* +Code taken from http://www.jspatterns.com/book/7/observer-game.html + +Pub/Sub +*/ + +var publisher = { + subscribers: { + any: [] + }, + on: function (type, fn, context) { + type = type || 'any'; + fn = typeof fn === "function" ? fn : context[fn]; + + if (typeof this.subscribers[type] === "undefined") { + this.subscribers[type] = []; + } + this.subscribers[type].push({fn: fn, context: context || this}); + }, + remove: function (type, fn, context) { + this.visitSubscribers('unsubscribe', type, fn, context); + }, + fire: function (type, publication) { + this.visitSubscribers('publish', type, publication); + }, + reset: function (type) { + + }, + visitSubscribers: function (action, type, arg, context) { + var pubtype = type || 'any', + subscribers = this.subscribers[pubtype], + i, + max = subscribers ? subscribers.length : 0; + + for (i = 0; i < max; i += 1) { + if (action === 'publish') { + subscribers[i].fn.call(subscribers[i].context, arg); + } + else { + if (subscribers[i].fn === arg && subscribers[i].context === context) { + subscribers.splice(i, 1); + } + } + } + } +}; + + +function makePublisher(o) { + var i; + for (i in publisher) { + if (publisher.hasOwnProperty(i) && typeof publisher[i] === "function") { + o[i] = publisher[i]; + } + } + o.subscribers = {any: []}; +} diff --git a/airtime_mvc/public/js/waveformplaylist/playlist.js b/airtime_mvc/public/js/waveformplaylist/playlist.js new file mode 100644 index 000000000..ff47e8e75 --- /dev/null +++ b/airtime_mvc/public/js/waveformplaylist/playlist.js @@ -0,0 +1,349 @@ +'use strict'; + +var PlaylistEditor = function() { + +}; + +PlaylistEditor.prototype.setConfig = function(config) { + this.config = config; +}; + +PlaylistEditor.prototype.init = function(tracks) { + + var that = this, + i, + len, + container = this.config.getContainer(), + div = container.getElementsByClassName("playlist-tracks")[0], + fragment = document.createDocumentFragment(), + trackEditor, + trackElem, + timeScale, + audioControls; + + makePublisher(this); + + this.storage = new Storage(); + + this.trackContainer = div; + this.trackEditors = []; + + audioControls = new AudioControls(); + audioControls.init(this.config); + + if (this.config.isTimeScaleEnabled()) { + timeScale = new TimeScale(); + timeScale.init(this.config); + audioControls.on("changeresolution", "onResolutionChange", timeScale); + this.on("trackscroll", "onTrackScroll", timeScale); + } + + this.timeScale = timeScale; + + for (i = 0, len = tracks.length; i < len; i++) { + + trackEditor = new TrackEditor(); + trackEditor.setConfig(this.config); + trackElem = trackEditor.loadTrack(tracks[i]); + + this.trackEditors.push(trackEditor); + fragment.appendChild(trackElem); + + audioControls.on("trackedit", "onTrackEdit", trackEditor); + audioControls.on("changeresolution", "onResolutionChange", trackEditor); + + trackEditor.on("activateSelection", "onAudioSelection", audioControls); + trackEditor.on("deactivateSelection", "onAudioDeselection", audioControls); + trackEditor.on("changecursor", "onCursorSelection", audioControls); + trackEditor.on("changecursor", "onSelectUpdate", this); + } + + div.innerHTML = ''; + div.appendChild(fragment); + div.onscroll = this.onTrackScroll.bind(that); + + this.sampleRate = this.config.getSampleRate(); + + this.scrollTimeout = false; + + //for setInterval that's toggled during play/stop. + this.interval; + + this.on("playbackcursor", "onAudioUpdate", audioControls); + + audioControls.on("playlistsave", "save", this); + audioControls.on("playlistrestore", "restore", this); + audioControls.on("rewindaudio", "rewind", this); + audioControls.on("playaudio", "play", this); + audioControls.on("stopaudio", "stop", this); + audioControls.on("trimaudio", "onTrimAudio", this); + audioControls.on("removeaudio", "onRemoveAudio", this); + audioControls.on("changestate", "onStateChange", this); + audioControls.on("changeselection", "onSelectionChange", this); +}; + +PlaylistEditor.prototype.onTrimAudio = function() { + var track = this.activeTrack, + selected = track.getSelectedArea(), + start, end; + + if (selected === undefined) { + return; + } + + track.trim(selected.start, selected.end); +}; + +PlaylistEditor.prototype.onRemoveAudio = function() { + var track = this.activeTrack, + selected = track.getSelectedArea(), + start, end; + + if (selected === undefined) { + return; + } + + track.removeAudio(selected.start, selected.end); +}; + +PlaylistEditor.prototype.onSelectionChange = function(args) { + + if (this.activeTrack === undefined) { + return; + } + + var res = this.config.getResolution(), + start = ~~(args.start * this.sampleRate / res), + end = ~~(args.end * this.sampleRate / res); + + this.config.setCursorPos(args.start); + this.activeTrack.setSelectedArea(start, end); + this.activeTrack.updateEditor(-1, undefined, undefined, true); +}; + +PlaylistEditor.prototype.onStateChange = function() { + var that = this, + editors = this.trackEditors, + i, + len, + editor, + state = this.config.getState(); + + for(i = 0, len = editors.length; i < len; i++) { + editors[i].deactivate(); + editors[i].setState(state); + } +}; + +PlaylistEditor.prototype.onTrackScroll = function(e) { + var that = this, + el = e.srcElement; + + if (that.scrollTimeout) return; + + //limit the scroll firing to every 25ms. + that.scrollTimeout = setTimeout(function() { + + that.config.setTrackScroll(el.scrollLeft, el.scrollTop); + that.fire('trackscroll', e); + that.scrollTimeout = false; + }, 25); +}; + +PlaylistEditor.prototype.activateTrack = function(trackEditor) { + var that = this, + editors = this.trackEditors, + i, + len, + editor; + + for (i = 0, len = editors.length; i < len; i++) { + editor = editors[i]; + + if (editor === trackEditor) { + editor.activate(); + this.activeTrack = trackEditor; + } + else { + editor.deactivate(); + } + } +}; + +PlaylistEditor.prototype.onSelectUpdate = function(event) { + + this.activateTrack(event.editor); +}; + +PlaylistEditor.prototype.resetCursor = function() { + this.config.setCursorPos(0); + this.notifySelectUpdate(0, 0); +}; + +PlaylistEditor.prototype.onCursorSelection = function(args) { + this.activateTrack(args.editor); +}; + +PlaylistEditor.prototype.rewind = function() { + + if (this.activeTrack !== undefined) { + this.activeTrack.resetCursor(); + } + else { + this.resetCursor(); + } + + this.stop(); +}; + +/* + returns selected time in global (playlist relative) seconds. +*/ +PlaylistEditor.prototype.getSelected = function() { + var selected, + start, + end; + + if (this.activeTrack) { + selected = this.activeTrack.selectedArea; + if (selected !== undefined && (selected.end > selected.start)) { + return this.activeTrack.getSelectedPlayTime(); + } + } +}; + +PlaylistEditor.prototype.isPlaying = function() { + var that = this, + editors = this.trackEditors, + i, + len, + isPlaying = false; + + for (i = 0, len = editors.length; i < len; i++) { + isPlaying = isPlaying || editors[i].isPlaying(); + } + + return isPlaying; +}; + +PlaylistEditor.prototype.play = function() { + var that = this, + editors = this.trackEditors, + i, + len, + currentTime = this.config.getCurrentTime(), + delay = 0.2, + startTime = this.config.getCursorPos(), + endTime, + selected = this.getSelected(); + + if (selected !== undefined) { + startTime = selected.startTime; + endTime = selected.endTime; + } + + for (i = 0, len = editors.length; i < len; i++) { + editors[i].schedulePlay(currentTime, delay, startTime, endTime); + } + + this.lastPlay = currentTime + delay; + this.interval = setInterval(that.updateEditor.bind(that), 25); +}; + +PlaylistEditor.prototype.stop = function() { + var editors = this.trackEditors, + i, + len, + currentTime = this.config.getCurrentTime(); + + clearInterval(this.interval); + + for (i = 0, len = editors.length; i < len; i++) { + editors[i].scheduleStop(currentTime); + editors[i].updateEditor(-1, undefined, undefined, true); + } +}; + +PlaylistEditor.prototype.updateEditor = function() { + var editors = this.trackEditors, + i, + len, + currentTime = this.config.getCurrentTime(), + elapsed = currentTime - this.lastPlay, + res = this.config.getResolution(), + cursorPos = this.config.getCursorPos(), + cursorPixel, + playbackSec, + selected = this.getSelected(), + start, end, + highlighted = false; + + if (selected !== undefined) { + start = ~~(selected.startTime * this.sampleRate / res); + end = Math.ceil(selected.endTime * this.sampleRate / res); + highlighted = true; + } + + if (this.isPlaying()) { + + if (elapsed) { + playbackSec = cursorPos + elapsed; + cursorPixel = Math.ceil(playbackSec * this.sampleRate / res); + + for (i = 0, len = editors.length; i < len; i++) { + editors[i].updateEditor(cursorPixel, start, end, highlighted); + } + + this.fire("playbackcursor", { + "seconds": playbackSec, + "pixels": cursorPixel + }); + } + } + else { + clearInterval(this.interval); + + for (i = 0, len = editors.length; i < len; i++) { + editors[i].updateEditor(-1, undefined, undefined, true); + } + } +}; + +PlaylistEditor.prototype.getJson = function() { + var editors = this.trackEditors, + i, + len, + info = [], + json; + + for (i = 0, len = editors.length; i < len; i++) { + info.push(editors[i].getTrackDetails()); + } + + json = JSON.stringify(info); + + return info; +}; + +PlaylistEditor.prototype.save = function() { + var editors = this.trackEditors, + i, + len, + info = []; + + for (i = 0, len = editors.length; i < len; i++) { + info.push(editors[i].getTrackDetails()); + } + + this.storage.save("test", info); +}; + +PlaylistEditor.prototype.restore = function() { + var state; + + state = this.storage.restore("test"); + + this.trackContainer.innerHTML=''; + this.init(state); +}; + diff --git a/airtime_mvc/public/js/waveformplaylist/playout.js b/airtime_mvc/public/js/waveformplaylist/playout.js new file mode 100644 index 000000000..8a30098b2 --- /dev/null +++ b/airtime_mvc/public/js/waveformplaylist/playout.js @@ -0,0 +1,161 @@ +'use strict'; + +var AudioPlayout = function() { + +}; + +AudioPlayout.prototype.init = function(config) { + + makePublisher(this); + + this.config = config; + this.ac = this.config.getAudioContext(); + + this.fadeMaker = new Fades(); + this.fadeMaker.init(this.ac.sampleRate); + + this.gainNode = undefined; + this.destination = this.ac.destination; + this.analyser = this.ac.createAnalyser(); + this.analyser.connect(this.destination); +}; + +AudioPlayout.prototype.getBuffer = function() { + return this.buffer; +}; + +AudioPlayout.prototype.setBuffer = function(buffer) { + this.buffer = buffer; +}; + +/* + param relPos: cursor position in seconds relative to this track. + can be negative if the cursor is placed before the start of this track etc. +*/ +AudioPlayout.prototype.applyFades = function(fades, relPos, now, delay) { + var id, + fade, + fn, + options, + startTime, + duration; + + this.gainNode && this.gainNode.disconnect(); + this.gainNode = this.ac.createGainNode(); + + for (id in fades) { + + fade = fades[id]; + + if (relPos <= fade.start) { + startTime = now + (fade.start - relPos) + delay; + duration = fade.end - fade.start; + } + else if (relPos > fade.start && relPos < fade.end) { + startTime = now - (relPos - fade.start) + delay; + duration = fade.end - fade.start; + } + + options = { + start: startTime, + duration: duration + }; + + if (fades.hasOwnProperty(id)) { + fn = this.fadeMaker["create"+fade.type]; + fn.call(this.fadeMaker, this.gainNode.gain, fade.shape, options); + } + } +}; + +/** + * Loads audiobuffer. + * + * @param {AudioBuffer} audioData Audio data. + */ +AudioPlayout.prototype.loadData = function (audioData, cb) { + var that = this; + + this.ac.decodeAudioData( + audioData, + function (buffer) { + that.buffer = buffer; + cb(buffer); + }, + Error + ); +}; + +AudioPlayout.prototype.isUnScheduled = function() { + return this.source && (this.source.playbackState === this.source.UNSCHEDULED_STATE); +}; + +AudioPlayout.prototype.isScheduled = function() { + return this.source && (this.source.playbackState === this.source.SCHEDULED_STATE); +}; + +AudioPlayout.prototype.isPlaying = function() { + return this.source && (this.source.playbackState === this.source.PLAYING_STATE); +}; + +AudioPlayout.prototype.isFinished = function() { + return this.source && (this.source.playbackState === this.source.FINISHED_STATE); +}; + +AudioPlayout.prototype.getDuration = function() { + return this.buffer.duration; +}; + +AudioPlayout.prototype.getPlayOffset = function() { + var offset = 0; + + //TODO needs a fix for when the buffer naturally plays out. But also have to mind the entire playlist. + if (this.playing) { + offset = this.secondsOffset + (this.ac.currentTime - this.playTime); + } + else { + offset = this.secondsOffset; + } + + return offset; +}; + +AudioPlayout.prototype.setPlayedPercents = function(percent) { + this.secondsOffset = this.getDuration() * percent; +}; + +AudioPlayout.prototype.getPlayedPercents = function() { + return this.getPlayOffset() / this.getDuration(); +}; + +AudioPlayout.prototype.setSource = function(source) { + this.source && this.source.disconnect(); + this.source = source; + this.source.buffer = this.buffer; + + this.source.connect(this.gainNode); + this.gainNode.connect(this.analyser); +}; + +/* + source.start is picky when passing the end time. + If rounding error causes a number to make the source think + it is playing slightly more samples than it has it won't play at all. + Unfortunately it doesn't seem to work if you just give it a start time. +*/ +AudioPlayout.prototype.play = function(when, start, duration) { + if (!this.buffer) { + console.error("no buffer to play"); + return; + } + + this.setSource(this.ac.createBufferSource()); + + this.source.start(when || 0, start, duration); +}; + +AudioPlayout.prototype.stop = function(when) { + + this.source && this.source.stop(when || 0); +}; + diff --git a/airtime_mvc/public/js/waveformplaylist/templates/bottombar.tpl b/airtime_mvc/public/js/waveformplaylist/templates/bottombar.tpl new file mode 100644 index 000000000..bd2d2051c --- /dev/null +++ b/airtime_mvc/public/js/waveformplaylist/templates/bottombar.tpl @@ -0,0 +1,25 @@ +
    + + + + + +
    diff --git a/airtime_mvc/public/js/waveformplaylist/templates/bottombar.tpl~ b/airtime_mvc/public/js/waveformplaylist/templates/bottombar.tpl~ new file mode 100644 index 000000000..bd2d2051c --- /dev/null +++ b/airtime_mvc/public/js/waveformplaylist/templates/bottombar.tpl~ @@ -0,0 +1,25 @@ +
    + + + + + +
    diff --git a/airtime_mvc/public/js/waveformplaylist/time_scale.js b/airtime_mvc/public/js/waveformplaylist/time_scale.js new file mode 100644 index 000000000..e762df42b --- /dev/null +++ b/airtime_mvc/public/js/waveformplaylist/time_scale.js @@ -0,0 +1,151 @@ +'use strict'; + +var TimeScale = function() { + +}; + +TimeScale.prototype.init = function(config) { + + var that = this, + canv, + div; + + makePublisher(this); + + div = document.getElementsByClassName("playlist-time-scale")[0]; + + if (div === undefined) { + return; + } + + canv = document.createElement("canvas"); + this.canv = canv; + this.context = canv.getContext('2d'); + this.config = config; + this.container = div; //container for the main time scale. + + //TODO check for window resizes to set these. + this.width = this.container.clientWidth; + this.height = this.container.clientHeight; + + canv.setAttribute('width', this.width); + canv.setAttribute('height', this.height); + + //array of divs displaying time every 30 seconds. (TODO should make this depend on resolution) + this.times = []; + + this.prevScrollPos = 0; //checking the horizontal scroll (must update timeline above in case of change) + + this.drawScale(); +}; + +/* + Return time in format mm:ss +*/ +TimeScale.prototype.formatTime = function(seconds) { + var out, m, s; + + s = seconds % 60; + m = (seconds - s) / 60; + + if (s < 10) { + s = "0"+s; + } + + out = m + ":" + s; + + return out; +}; + +TimeScale.prototype.clear = function() { + + this.container.innerHTML = ""; + this.context.clearRect(0, 0, this.width, this.height); +}; + +TimeScale.prototype.drawScale = function(offset) { + var cc = this.context, + canv = this.canv, + colors = this.config.getColorScheme(), + pix, + res = this.config.getResolution(), + SR = this.config.getSampleRate(), + pixPerSec = SR/res, + pixOffset = offset || 0, //caused by scrolling horizontally + i, + end, + counter = 0, + pixIndex, + container = this.container, + width = this.width, + height = this.height, + div, + time, + sTime, + fragment = document.createDocumentFragment(), + scaleY, + scaleHeight; + + + this.clear(); + + fragment.appendChild(canv); + cc.fillStyle = colors.timeColor; + end = width + pixOffset; + + for (i = 0; i < end; i = i + pixPerSec) { + + pixIndex = ~~(i); + pix = pixIndex - pixOffset; + + if (pixIndex >= pixOffset) { + + //put a timestamp every 30 seconds. + if (counter % 30 === 0) { + + sTime = this.formatTime(counter); + time = document.createTextNode(sTime); + div = document.createElement("div"); + + div.style.left = pix+"px"; + div.appendChild(time); + fragment.appendChild(div); + + scaleHeight = 10; + scaleY = height - scaleHeight; + } + else if (counter % 5 === 0) { + scaleHeight = 5; + scaleY = height - scaleHeight; + } + else { + scaleHeight = 2; + scaleY = height - scaleHeight; + } + + cc.fillRect(pix, scaleY, 1, scaleHeight); + } + + counter++; + } + + container.appendChild(fragment); +}; + +TimeScale.prototype.onTrackScroll = function() { + var scroll = this.config.getTrackScroll(), + scrollX = scroll.left; + + if (scrollX !== this.prevScrollPos) { + this.prevScrollPos = scrollX; + this.drawScale(scrollX); + } +}; + +TimeScale.prototype.onResolutionChange = function() { + var scroll = this.config.getTrackScroll(), + scrollX = scroll.left; + + this.drawScale(scrollX); +}; + diff --git a/airtime_mvc/public/js/waveformplaylist/track.js b/airtime_mvc/public/js/waveformplaylist/track.js new file mode 100644 index 000000000..2e0969165 --- /dev/null +++ b/airtime_mvc/public/js/waveformplaylist/track.js @@ -0,0 +1,860 @@ +'use strict'; + +var TrackEditor = function() { + +}; + +TrackEditor.prototype.classes = { + "cursor": [ + "state-select" + ], + + "select": [ + "state-select" + ], + + "fadein": [ + "state-select" + ], + + "fadeout": [ + "state-select" + ], + + "shift": [ + "state-shift" + ], + + "active": [ + "active" + ], + + "disabled": [ + "disabled" + ] +}; + +TrackEditor.prototype.events = { + "cursor": { + "mousedown": "selectCursorPos" + }, + + "select": { + "mousedown": "selectStart" + }, + + "fadein": { + "mousedown": "selectFadeIn" + }, + + "fadeout": { + "mousedown": "selectFadeOut" + }, + + "shift": { + "mousedown": "timeShift" + } +}; + +TrackEditor.prototype.setConfig = function(config) { + this.config = config; +}; + +TrackEditor.prototype.setWidth = function(width) { + this.width = width; +}; + +TrackEditor.prototype.init = function(src, start, end, fades, cues, stateConfig) { + + var statesEnabled = { + 'cursor': true, + 'fadein': true, + 'fadeout': true, + 'select': true, + 'shift': true + }; + + //extend enabled states config. + Object.keys(statesEnabled).forEach(function (key) { + statesEnabled[key] = (key in stateConfig) ? stateConfig[key] : statesEnabled[key]; + }); + + this.enabledStates = statesEnabled; + + makePublisher(this); + + this.container = document.createElement("div"); + + this.drawer = new WaveformDrawer(); + this.drawer.init(this.container, this.config); + + this.playout = new AudioPlayout(); + this.playout.init(this.config); + + this.sampleRate = this.config.getSampleRate(); + this.resolution = this.config.getResolution(); + + //value is a float in seconds + this.startTime = start || 0; + //value is a float in seconds + this.endTime = end || 0; //set properly in onTrackLoad. + + this.leftOffset = this.secondsToSamples(this.startTime); //value is measured in samples. + + this.prevStateEvents = {}; + this.setState(this.config.getState()); + + this.fades = {}; + if (fades !== undefined && fades.length > 0) { + + for (var i = 0; i < fades.length; i++) { + this.fades[this.getFadeId()] = fades[i]; + } + } + + if (cues.cuein !== undefined) { + this.setCuePoints(this.secondsToSamples(cues.cuein), this.secondsToSamples(cues.cueout)); + } + + this.selectedArea = undefined; //selected area of track stored as inclusive buffer indices to the audio buffer. + this.active = false; + + this.container.classList.add("channel-wrapper"); + this.container.style.left = this.leftOffset; + + this.drawer.drawLoading(); + + return this.container; +}; + +TrackEditor.prototype.getFadeId = function() { + var id = ""+Math.random(); + + return id.replace(".", ""); +}; + +TrackEditor.prototype.getBuffer = function() { + return this.playout.getBuffer(); +}; + +TrackEditor.prototype.setBuffer = function(buffer) { + this.playout.setBuffer(buffer); +}; + + +TrackEditor.prototype.loadTrack = function(track) { + var el; + + el = this.init( + track.src, + track.start, + track.end, + track.fades, + { + cuein: track.cuein, + cueout: track.cueout + }, + track.states || {} + ); + this.loadBuffer(track.src); + + return el; +}; + +/** + * Loads an audio file via XHR. + */ +TrackEditor.prototype.loadBuffer = function(src) { + var that = this, + xhr = new XMLHttpRequest(); + + xhr.responseType = 'arraybuffer'; + + xhr.addEventListener('progress', function(e) { + var percentComplete; + + if (e.lengthComputable) { + percentComplete = e.loaded / e.total * 100; + that.drawer.updateLoader(percentComplete); + } + + }, false); + + xhr.addEventListener('load', function(e) { + that.src = src; + that.drawer.setLoaderState("decoding"); + + that.playout.loadData( + e.target.response, + that.onTrackLoad.bind(that) + ); + }, false); + + xhr.open('GET', src, true); + xhr.send(); +}; + +TrackEditor.prototype.drawTrack = function(buffer) { + + this.drawer.drawBuffer(buffer, this.getPixelOffset(this.leftOffset), this.cues); + this.drawer.drawFades(this.fades); +}; + +TrackEditor.prototype.onTrackLoad = function(buffer) { + var res; + + if (this.cues === undefined) { + this.setCuePoints(0, buffer.length - 1); + } + //adjust if the length was inaccurate and cueout is set to a higher sample than we actually have. + else if (this.cues.cueout > (buffer.length - 1)) { + this.cues.cueout = buffer.length - 1; + } + + if (this.width !== undefined) { + res = Math.ceil(buffer.length / this.width); + + this.config.setResolution(res); + this.resolution = res; + } + + this.drawTrack(buffer); +}; + +TrackEditor.prototype.samplesToSeconds = function(samples) { + return samples / this.sampleRate; +}; + +TrackEditor.prototype.secondsToSamples = function(seconds) { + return Math.ceil(seconds * this.sampleRate); +}; + +TrackEditor.prototype.samplesToPixels = function(samples) { + return ~~(samples / this.resolution); +}; + +TrackEditor.prototype.pixelsToSamples = function(pixels) { + return ~~(pixels * this.resolution); +}; + +TrackEditor.prototype.pixelsToSeconds = function(pixels) { + return pixels * this.resolution / this.sampleRate; +}; + +TrackEditor.prototype.secondsToPixels = function(seconds) { + return ~~(seconds * this.sampleRate / this.resolution); +}; + +TrackEditor.prototype.getPixelOffset = function() { + return this.leftOffset / this.resolution; +}; + +TrackEditor.prototype.activate = function() { + this.active = true; + this.container.classList.add("active"); +}; + +TrackEditor.prototype.deactivate = function() { + this.active = false; + this.selectedArea = undefined; + this.container.classList.remove("active"); + this.updateEditor(-1, undefined, undefined, true); +}; + +/* start of state methods */ + +TrackEditor.prototype.timeShift = function(e) { + var el = e.currentTarget, //want the events placed on the channel wrapper. + startX = e.pageX, + diffX = 0, + origX = 0, + updatedX = 0, + editor = this, + res = editor.resolution, + scroll = this.config.getTrackScroll(), + scrollX = scroll.left; + + origX = editor.leftOffset / res; + + //dynamically put an event on the element. + el.onmousemove = function(e) { + var endX = e.pageX; + + diffX = endX - startX; + updatedX = origX + diffX; + editor.drawer.setTimeShift(updatedX); + editor.leftOffset = editor.pixelsToSamples(updatedX); + }; + el.onmouseup = function() { + var delta; + + el.onmousemove = el.onmouseup = null; + editor.leftOffset = editor.pixelsToSamples(updatedX); + delta = editor.pixelsToSeconds(diffX); + + //update track's start and end time relative to the playlist. + editor.startTime = editor.startTime + delta; + editor.endTime = editor.endTime + delta; + }; +}; + +/* + startTime, endTime in seconds. +*/ +TrackEditor.prototype.notifySelectUpdate = function(startTime, endTime) { + + this.fire('changecursor', { + start: startTime, + end: endTime, + editor: this + }); +}; + + +TrackEditor.prototype.getSelectedPlayTime = function() { + var selected = this.selectedArea, + offset = this.leftOffset, + start = this.samplesToSeconds(offset + selected.start), + end = this.samplesToSeconds(offset + selected.end); + + return { + startTime: start, + endTime: end + } +}; + + +TrackEditor.prototype.getSelectedArea = function() { + return this.selectedArea; +}; + +/* + start, end in samples. (relative to cuein/cueout) +*/ +TrackEditor.prototype.adjustSelectedArea = function(start, end) { + var buffer = this.getBuffer(), + cues = this.cues; + + if (start === undefined || start < 0) { + start = 0; + } + + if (end === undefined) { + end = cues.cueout - cues.cuein; + } + + if (end > buffer.length - 1) { + end = buffer.length - 1; + } + + return { + start: start, + end: end + }; +}; + +/* + start, end in pixels +*/ +TrackEditor.prototype.setSelectedArea = function(start, end, shiftKey) { + var left, + right, + currentStart, + currentEnd, + sampLeft, + sampRight, + buffer = this.getBuffer(); + + //extending selected area since shift is pressed. + if (shiftKey && (end - start === 0) && (this.prevSelectedArea !== undefined)) { + + currentStart = this.samplesToPixels(this.prevSelectedArea.start); + currentEnd = this.samplesToPixels(this.prevSelectedArea.end); + + if (start < currentStart) { + left = start; + right = currentEnd; + } + else if (end > currentEnd) { + left = currentStart; + right = end; + } + //it's ambigous otherwise, cut off the smaller duration. + else { + if ((start - currentStart) < (currentEnd - start)) { + left = start; + right = currentEnd; + } + else { + left = currentStart; + right = end; + } + } + } + else { + left = start; + right = end; + } + + sampLeft = left === undefined ? undefined : this.pixelsToSamples(left); + sampRight = right === undefined ? undefined : this.pixelsToSamples(right); + + this.prevSelectedArea = this.selectedArea; + this.selectedArea = this.adjustSelectedArea(sampLeft, sampRight); +}; + +TrackEditor.prototype.activateAudioSelection = function() { + + this.fire("activateSelection"); +}; + +TrackEditor.prototype.deactivateAudioSelection = function() { + + this.fire("deactivateSelection"); +}; + +TrackEditor.prototype.findLayerOffset = function(e) { + var layerOffset = 0, + parent; + + if (e.target.tagName !== "CANVAS") { + layerOffset = -1; + } + else { + //have to check if a fade canvas was selected. (Must add left offset) + parent = e.target.parentNode; + + if (parent.classList.contains('playlist-fade')) { + layerOffset = parent.offsetLeft; + } + } + + return layerOffset; +}; + +TrackEditor.prototype.selectStart = function(e) { + var el = e.currentTarget, //want the events placed on the channel wrapper. + editor = this, + startX = e.layerX || e.offsetX, //relative to e.target (want the canvas). + prevX = e.layerX || e.offsetX, + offset = this.leftOffset, + startTime, + layerOffset; + + layerOffset = this.findLayerOffset(e); + if (layerOffset < 0) { + return; + } + startX = startX + layerOffset; + prevX = prevX + layerOffset; + + editor.setSelectedArea(startX, startX); + startTime = editor.samplesToSeconds(offset + editor.selectedArea.start); + + editor.updateEditor(-1, undefined, undefined, true); + editor.notifySelectUpdate(startTime, startTime); + + //dynamically put an event on the element. + el.onmousemove = function(e) { + var currentX = layerOffset + (e.layerX || e.offsetX), + delta = currentX - prevX, + minX = Math.min(prevX, currentX, startX), + maxX = Math.max(prevX, currentX, startX), + selectStart, + selectEnd, + startTime, endTime; + + if (currentX > startX) { + selectStart = startX; + selectEnd = currentX; + } + else { + selectStart = currentX; + selectEnd = startX; + } + + startTime = editor.samplesToSeconds(offset + editor.selectedArea.start); + endTime = editor.samplesToSeconds(offset + editor.selectedArea.end); + + editor.setSelectedArea(selectStart, selectEnd); + editor.updateEditor(-1, undefined, undefined, true); + editor.notifySelectUpdate(startTime, endTime); + prevX = currentX; + }; + el.onmouseup = function(e) { + var endX = layerOffset + (e.layerX || e.offsetX), + minX, maxX, + startTime, endTime; + + minX = Math.min(startX, endX); + maxX = Math.max(startX, endX); + + editor.setSelectedArea(minX, maxX, e.shiftKey); + + minX = editor.samplesToPixels(offset + editor.selectedArea.start); + maxX = editor.samplesToPixels(offset + editor.selectedArea.end); + + el.onmousemove = el.onmouseup = null; + + //if more than one pixel is selected, listen to possible fade events. + if (Math.abs(minX - maxX)) { + editor.activateAudioSelection(); + } + else { + editor.deactivateAudioSelection(); + } + + startTime = editor.samplesToSeconds(offset + editor.selectedArea.start); + endTime = editor.samplesToSeconds(offset + editor.selectedArea.end); + + editor.updateEditor(-1, undefined, undefined, true); + editor.config.setCursorPos(startTime); + editor.notifySelectUpdate(startTime, endTime); + }; +}; + +TrackEditor.prototype.selectCursorPos = function(e) { + var editor = this, + startX = e.layerX || e.offsetX, //relative to e.target (want the canvas). + offset = this.leftOffset, + startTime, + endTime, + layerOffset; + + layerOffset = this.findLayerOffset(e); + if (layerOffset < 0) { + return; + } + startX = startX + layerOffset; + + editor.setSelectedArea(startX, startX); + startTime = editor.samplesToSeconds(offset + editor.selectedArea.start); + endTime = editor.samplesToSeconds(offset + editor.selectedArea.end); + + editor.updateEditor(-1, undefined, undefined, true); + editor.config.setCursorPos(startTime); + editor.notifySelectUpdate(startTime, endTime); + + editor.deactivateAudioSelection(); +}; + +TrackEditor.prototype.selectFadeIn = function(e) { + var startX = e.layerX || e.offsetX, //relative to e.target (want the canvas). + layerOffset, + FADETYPE = "FadeIn", + shape = this.config.getFadeType(); + + layerOffset = this.findLayerOffset(e); + if (layerOffset < 0) { + return; + } + startX = startX + layerOffset; + + this.setSelectedArea(undefined, startX); + this.removeFadeType(FADETYPE); + this.createFade(FADETYPE, shape); +}; + +TrackEditor.prototype.selectFadeOut = function(e) { + var startX = e.layerX || e.offsetX, //relative to e.target (want the canvas). + layerOffset, + FADETYPE = "FadeOut", + shape = this.config.getFadeType(); + + layerOffset = this.findLayerOffset(e); + if (layerOffset < 0) { + return; + } + startX = startX + layerOffset; + + this.setSelectedArea(startX, undefined); + this.removeFadeType(FADETYPE); + this.createFade(FADETYPE, shape); +}; + +/* end of state methods */ + +TrackEditor.prototype.saveFade = function(id, type, shape, start, end) { + + this.fades[id] = { + type: type, + shape: shape, + start: start, + end: end + }; + + return id; +}; + +TrackEditor.prototype.removeFade = function(id) { + + delete this.fades[id]; + this.drawer.removeFade(id); +}; + +TrackEditor.prototype.removeFadeType = function(type) { + var id, + fades = this.fades, + fade; + + for (id in fades) { + fade = fades[id]; + + if (fade.type === type) { + this.removeFade(id); + } + } +}; + +/* + Cue points are stored internally in the editor as sample indices for highest precision. + + sample at index cueout is not included. +*/ +TrackEditor.prototype.setCuePoints = function(cuein, cueout) { + var offset = this.cues ? this.cues.cuein : 0; + + this.cues = { + cuein: offset + cuein, + cueout: offset + cueout + }; + + this.duration = (cueout - cuein) / this.sampleRate; + this.endTime = this.duration + this.startTime; +}; + +/* + Will remove all audio samples from the track's buffer except for the currently selected area. + Used to set cuein / cueout points in the audio. + + start, end are indices into the audio buffer and are inclusive. +*/ +TrackEditor.prototype.trim = function(start, end) { + + this.setCuePoints(start, end+1); + this.resetCursor(); + this.drawTrack(this.getBuffer()); +}; + + +/* + Will remove all audio samples from the track's buffer in the currently selected area. + + start, end are indices into the audio buffer and are inclusive. +*/ +TrackEditor.prototype.removeAudio = function(start, end) { + +}; + +TrackEditor.prototype.onTrackEdit = function(event) { + var type = event.type, + method = "on" + type.charAt(0).toUpperCase() + type.slice(1); + + if (this.active === true) { + this[method].call(this, event.args); + } +}; + +TrackEditor.prototype.createFade = function(type, shape) { + var selected = this.selectedArea, + start = this.samplesToPixels(selected.start), + end = this.samplesToPixels(selected.end), + startTime = this.samplesToSeconds(selected.start), + endTime = this.samplesToSeconds(selected.end), + id = this.getFadeId(); + + this.resetCursor(); + this.saveFade(id, type, shape, startTime, endTime); + this.updateEditor(-1, undefined, undefined, true); + this.drawer.drawFade(id, type, shape, start, end); +}; + +TrackEditor.prototype.onCreateFade = function(args) { + this.createFade(args.type, args.shape); + this.deactivateAudioSelection(); +}; + +TrackEditor.prototype.onZeroCrossing = function() { + var selected = this.getSelectedArea(), + startTime, + endTime, + offset = this.leftOffset; + + this.selectedArea = this.findNearestZeroCrossing(selected.start, selected.end); + + startTime = this.samplesToSeconds(offset + this.selectedArea.start); + endTime = this.samplesToSeconds(offset + this.selectedArea.end); + this.notifySelectUpdate(startTime, endTime); + this.updateEditor(-1, undefined, undefined, true); +}; + +TrackEditor.prototype.onTrimAudio = function() { + var selected = this.getSelectedArea(); + + this.trim(selected.start, selected.end); + this.deactivateAudioSelection(); +}; + +TrackEditor.prototype.onRemoveAudio = function() { + var selected = this.getSelectedArea(); + + this.removeAudio(selected.start, selected.end); + this.deactivateAudioSelection(); +}; + +TrackEditor.prototype.setState = function(state) { + var that = this, + stateEvents = this.events[state], + stateClasses = this.classes[state], + disabledClasses = this.classes['disabled'], + enabledStates = this.enabledStates, + container = this.container, + prevState = this.currentState, + prevStateClasses, + prevStateEvents = this.prevStateEvents, + func, event, cl, + i, len; + + if (prevState) { + prevStateClasses = this.classes[prevState]; + + if (enabledStates[prevState] === true) { + for (event in prevStateEvents) { + container.removeEventListener(event, prevStateEvents[event]); + } + this.prevStateEvents = {}; + + for (i = 0, len = prevStateClasses.length; i < len; i++) { + container.classList.remove(prevStateClasses[i]); + } + } + else { + for (i = 0, len = disabledClasses.length; i < len; i++) { + container.classList.remove(disabledClasses[i]); + } + } + } + + if (enabledStates[state] === true) { + for (event in stateEvents) { + func = that[stateEvents[event]].bind(that); + //need to keep track of the added events for later removal since a new function is returned after using "bind" + this.prevStateEvents[event] = func; + container.addEventListener(event, func); + } + for (i = 0, len = stateClasses.length; i < len; i++) { + container.classList.add(stateClasses[i]); + } + } + else { + for (i = 0, len = disabledClasses.length; i < len; i++) { + container.classList.add(disabledClasses[i]); + } + } + + this.currentState = state; +}; + +TrackEditor.prototype.onResolutionChange = function(res) { + var selected = this.selectedArea; + + this.resolution = res; + this.drawTrack(this.getBuffer()); + + if (this.active === true && this.selectedArea !== undefined) { + + this.updateEditor(-1, this.samplesToPixels(selected.start), this.samplesToPixels(selected.end), true); + } +}; + +TrackEditor.prototype.isPlaying = function() { + return this.playout.isScheduled() || this.playout.isPlaying(); +}; + +/* + startTime, endTime in seconds (float). +*/ +TrackEditor.prototype.schedulePlay = function(now, delay, startTime, endTime) { + var start, + duration, + relPos, + when = now + delay, + window = (endTime) ? (endTime - startTime) : undefined, + cueOffset = this.cues.cuein / this.sampleRate; + + //track has no content to play. + if (this.endTime <= startTime) return; + + //track does not start in this selection. + if (window && (startTime + window) < this.startTime) return; + + + //track should have something to play if it gets here. + + //the track starts in the future of the cursor position + if (this.startTime >= startTime) { + start = 0; + when = when + this.startTime - startTime; //schedule additional delay for this audio node. + window = window - (this.startTime - startTime); + duration = (endTime) ? Math.min(window, this.duration) : this.duration; + } + else { + start = startTime - this.startTime; + duration = (endTime) ? Math.min(window, this.duration - start) : this.duration - start; + } + + start = start + cueOffset; + + relPos = startTime - this.startTime; + this.playout.applyFades(this.fades, relPos, now, delay); + this.playout.play(when, start, duration); +}; + +TrackEditor.prototype.scheduleStop = function(when) { + + this.playout.stop(when); +}; + +TrackEditor.prototype.resetCursor = function() { + this.selectedArea = undefined; + this.config.setCursorPos(0); + this.notifySelectUpdate(0, 0); +}; + +TrackEditor.prototype.updateEditor = function(cursorPos, start, end, highlighted) { + var pixelOffset = this.getPixelOffset(), + selected; + + if (this.selectedArea) { + //must pass selected area in pixels. + selected = { + start: this.samplesToPixels(this.selectedArea.start), + end: this.samplesToPixels(this.selectedArea.end) + }; + } + + this.drawer.updateEditor(cursorPos, pixelOffset, start, end, highlighted, selected); +}; + +TrackEditor.prototype.getTrackDetails = function() { + var d, + cues = this.cues, + fades = [], + id; + + for (id in this.fades) { + fades.push(this.fades[id]); + } + + d = { + start: this.startTime, + end: this.endTime, + fades: fades, + src: this.src, + cuein: this.samplesToSeconds(cues.cuein), + cueout: this.samplesToSeconds(cues.cueout) + }; + + return d; +}; + diff --git a/airtime_mvc/public/js/waveformplaylist/track_render.js b/airtime_mvc/public/js/waveformplaylist/track_render.js new file mode 100644 index 000000000..54c87945b --- /dev/null +++ b/airtime_mvc/public/js/waveformplaylist/track_render.js @@ -0,0 +1,450 @@ +'use strict'; + +var WaveformDrawer = function() { + +}; + +WaveformDrawer.prototype.init = function(container, config) { + + makePublisher(this); + + this.config = config; + this.container = container; + this.channels = []; //array of canvases, contexts, 1 for each channel displayed. + + var theme = this.config.getUITheme(); + + if (this.loaderStates[theme] !== undefined) { + this.loaderStates = this.loaderStates[theme]; + } + else { + this.loaderStates = this.loaderStates["default"]; + } +}; + +WaveformDrawer.prototype.loaderStates = { + "bootstrap": { + "downloading": "progress progress-warning", + "decoding": "progress progress-success progress-striped active", + "loader": "bar" + }, + + "jQueryUI": { + "downloading": "ui-progressbar ui-widget ui-widget-content ui-corner-all", + "decoding": "ui-progressbar ui-widget ui-widget-content ui-corner-all", + "loader": "ui-progressbar-value ui-widget-header ui-corner-left" + }, + + "default": { + "downloading": "progress", + "decoding": "decoding", + "loader": "bar" + } +}; + +WaveformDrawer.prototype.getPeaks = function(buffer, cues) { + + // Frames per pixel + var res = this.config.getResolution(), + peaks = [], + i, c, p, l, + chanLength = cues.cueout - cues.cuein, + pixels = Math.ceil(chanLength / res), + numChan = buffer.numberOfChannels, + weight = 1 / (numChan), + makeMono = this.config.isDisplayMono(), + chan, + start, + end, + vals, + max, + min, + maxPeak = -Infinity; //used to scale the waveform on the canvas. + + for (i = 0; i < pixels; i++) { + + peaks[i] = []; + + for (c = 0; c < numChan; c++) { + + chan = buffer.getChannelData(c); + chan = chan.subarray(cues.cuein, cues.cueout); + + start = i * res; + end = (i + 1) * res > chanLength ? chanLength : (i + 1) * res; + vals = chan.subarray(start, end); + max = -Infinity; + min = Infinity; + + for (p = 0, l = vals.length; p < l; p++) { + if (vals[p] > max){ + max = vals[p]; + } + if (vals[p] < min){ + min = vals[p]; + } + } + peaks[i].push({max:max, min:min}); + maxPeak = Math.max.apply(Math, [maxPeak, Math.abs(max), Math.abs(min)]); + } + + if (makeMono) { + max = min = 0; + + for (c = 0 ; c < numChan; c++) { + max = max + weight * peaks[i][c].max; + min = min + weight * peaks[i][c].min; + } + + peaks[i] = []; //need to clear out old stuff (maybe we should keep it for toggling views?). + peaks[i].push({max:max, min:min}); + } + } + + this.maxPeak = maxPeak; + this.peaks = peaks; +}; + +WaveformDrawer.prototype.setTimeShift = function(pixels) { + var i, len; + + for (i = 0, len = this.channels.length; i < len; i++) { + this.channels[i].div.style.left = pixels+"px"; + } +}; + +WaveformDrawer.prototype.updateLoader = function(percent) { + this.loader.style.width = percent+"%"; +}; + +WaveformDrawer.prototype.setLoaderState = function(state) { + this.progressDiv.className = this.loaderStates[state]; +}; + +WaveformDrawer.prototype.drawLoading = function() { + var div, + loader; + + this.height = this.config.getWaveHeight(); + + div = document.createElement("div"); + div.style.height = this.height+"px"; + + loader = document.createElement("div"); + loader.style.height = "10px"; + loader.className = this.loaderStates["loader"]; + + div.appendChild(loader); + + this.progressDiv = div; + this.loader = loader; + + this.setLoaderState("downloading"); + this.updateLoader(0); + + this.container.appendChild(div); +}; + +WaveformDrawer.prototype.drawBuffer = function(buffer, pixelOffset, cues) { + var canv, + div, + i, + top = 0, + left = 0, + makeMono = this.config.isDisplayMono(), + res = this.config.getResolution(), + numChan = makeMono? 1 : buffer.numberOfChannels, + numSamples = cues.cueout - cues.cuein + 1, + fragment = document.createDocumentFragment(), + wrapperHeight; + + this.container.innerHTML = ""; + this.channels = []; + + //width and height is per waveform canvas. + this.width = Math.ceil(numSamples / res); + this.height = this.config.getWaveHeight(); + + for (i = 0; i < numChan; i++) { + + div = document.createElement("div"); + div.classList.add("channel"); + div.classList.add("channel-"+i); + div.style.width = this.width+"px"; + div.style.height = this.height+"px"; + div.style.top = top+"px"; + div.style.left = left+"px"; + + canv = document.createElement("canvas"); + canv.setAttribute('width', this.width); + canv.setAttribute('height', this.height); + + this.channels.push({ + canvas: canv, + context: canv.getContext('2d'), + div: div + }); + + div.appendChild(canv); + fragment.appendChild(div); + + top = top + this.height; + } + + wrapperHeight = numChan * this.height; + this.container.style.height = wrapperHeight+"px"; + this.container.appendChild(fragment); + + + this.getPeaks(buffer, cues); + this.updateEditor(); + + this.setTimeShift(pixelOffset); +}; + +WaveformDrawer.prototype.drawFrame = function(chanNum, index, peaks, maxPeak, cursorPos, pixelOffset) { + var x, y, w, h, max, min, + h2 = this.height / 2, + cc = this.channels[chanNum].context, + colors = this.config.getColorScheme(); + + max = (peaks.max / maxPeak) * h2; + min = (peaks.min / maxPeak) * h2; + + w = 1; + x = index * w; + y = Math.round(h2 - max); + h = Math.ceil(max - min); + + //to prevent blank space when there is basically silence in the track. + h = h === 0 ? 1 : h; + + if (cursorPos >= (x + pixelOffset)) { + cc.fillStyle = colors.progressColor; + } + else { + cc.fillStyle = colors.waveColor; + } + + cc.fillRect(x, y, w, h); +}; + +/* + start, end are optional parameters to only redraw part of the canvas. +*/ +WaveformDrawer.prototype.draw = function(cursorPos, pixelOffset, start, end) { + var that = this, + peaks = this.peaks, + i = (start) ? start - pixelOffset : 0, + len = (end) ? end - pixelOffset + 1 : peaks.length; + + if (i < 0 && len < 0) { + return; + } + + if (i < 0) { + i = 0; + } + + if (len > peaks.length) { + len = peaks.length; + } + + this.clear(i, len); + + for (; i < len; i++) { + + peaks[i].forEach(function(peak, chanNum) { + that.drawFrame(chanNum, i, peak, that.maxPeak, cursorPos, pixelOffset); + }); + } +}; + +/* + If start/end are set clear only part of the canvas. +*/ +WaveformDrawer.prototype.clear = function(start, end) { + var i, len, + width = end - start; + + for (i = 0, len = this.channels.length; i < len; i++) { + this.channels[i].context.clearRect(start, 0, width, this.height); + } +}; + +WaveformDrawer.prototype.updateEditor = function(cursorPos, pixelOffset, start, end, highlighted, selected) { + var i, len, + fragment = document.createDocumentFragment(); + + this.container.innerHTML = ""; + + this.draw(cursorPos, pixelOffset, start, end); + + if (highlighted === true && selected !== undefined) { + var border = (selected.end - selected.start === 0) ? true : false; + this.drawHighlight(selected.start, selected.end, border); + } + + for (i = 0, len = this.channels.length; i < len; i++) { + fragment.appendChild(this.channels[i].div); + } + + this.container.appendChild(fragment); +}; + +/* + start, end in pixels. +*/ +WaveformDrawer.prototype.drawHighlight = function(start, end, isBorder) { + var i, len, + colors = this.config.getColorScheme(), + fillStyle, + ctx, + width = end - start + 1; + + fillStyle = (isBorder) ? colors.selectBorderColor : colors.selectBackgroundColor; + + for (i = 0, len = this.channels.length; i < len; i++) { + ctx = this.channels[i].context; + ctx.fillStyle = fillStyle; + ctx.fillRect(start, 0, width, this.height); + } +}; + +WaveformDrawer.prototype.sCurveFadeIn = function sCurveFadeIn(ctx, width) { + return Curves.createSCurveBuffer(width, (Math.PI/2)); +}; + +WaveformDrawer.prototype.sCurveFadeOut = function sCurveFadeOut(ctx, width) { + return Curves.createSCurveBuffer(width, -(Math.PI/2)); +}; + +WaveformDrawer.prototype.logarithmicFadeIn = function logarithmicFadeIn(ctx, width) { + return Curves.createLogarithmicBuffer(width, 10, 1); +}; + +WaveformDrawer.prototype.logarithmicFadeOut = function logarithmicFadeOut(ctx, width) { + return Curves.createLogarithmicBuffer(width, 10, -1); +}; + +WaveformDrawer.prototype.exponentialFadeIn = function exponentialFadeIn(ctx, width) { + return Curves.createExponentialBuffer(width, 1); +}; + +WaveformDrawer.prototype.exponentialFadeOut = function exponentialFadeOut(ctx, width) { + return Curves.createExponentialBuffer(width, -1); +}; + +WaveformDrawer.prototype.linearFadeIn = function linearFadeIn(ctx, width) { + return Curves.createLinearBuffer(width, 1); +}; + +WaveformDrawer.prototype.linearFadeOut = function linearFadeOut(ctx, width) { + return Curves.createLinearBuffer(width, -1); +}; + +WaveformDrawer.prototype.drawFadeCurve = function(ctx, shape, type, width) { + var method = shape+type, + fn = this[method], + colors = this.config.getColorScheme(), + curve, + i, len, + cHeight = this.height, + y; + + ctx.strokeStyle = colors.fadeColor; + + curve = fn.call(this, ctx, width); + + y = cHeight - curve[0] * cHeight; + ctx.beginPath(); + ctx.moveTo(0, y); + + for (i = 1, len = curve.length; i < len; i++) { + y = cHeight - curve[i] * cHeight; + ctx.lineTo(i, y); + } + ctx.stroke(); +}; + +WaveformDrawer.prototype.removeFade = function(id) { + var fadeClass = "playlist-fade-"+id, + el, els, + i,len; + + els = this.container.getElementsByClassName(fadeClass); + len = els.length; + + //DOM NodeList is live, use a decrementing counter. + if (len > 0) { + for (i = len-1; i >= 0; i--) { + el = els[i]; + el.parentNode.removeChild(el); + } + } +}; + +WaveformDrawer.prototype.drawFade = function(id, type, shape, start, end) { + var div, + canv, + width, + left, + fragment = document.createDocumentFragment(), + i, len, + dup, + ctx, + tmpCtx; + + if ((end - start) === 0) { + return; + } + + width = ~~(end - start + 1); + left = start; + + div = document.createElement("div"); + div.classList.add("playlist-fade"); + div.classList.add("playlist-fade-"+id); + div.style.width = width+"px"; + div.style.height = this.height+"px"; + div.style.top = 0; + div.style.left = left+"px"; + + canv = document.createElement("canvas"); + canv.setAttribute('width', width); + canv.setAttribute('height', this.height); + ctx = canv.getContext('2d'); + + this.drawFadeCurve(ctx, shape, type, width); + + div.appendChild(canv); + fragment.appendChild(div); + + for (i = 0, len = this.channels.length; i < len; i++) { + dup = fragment.cloneNode(true); + tmpCtx = dup.querySelector('canvas').getContext('2d'); + tmpCtx.drawImage(canv, 0, 0); + + this.channels[i].div.appendChild(dup); + } +}; + +WaveformDrawer.prototype.drawFades = function(fades) { + var id, + fade, + startPix, + endPix, + SR = this.config.getSampleRate(), + res = this.config.getResolution(); + + for (id in fades) { + fade = fades[id]; + + if (fades.hasOwnProperty(id)) { + startPix = fade.start * SR / res; + endPix = fade.end * SR / res; + this.drawFade(id, fade.type, fade.shape, startPix, endPix); + } + } +}; +