From 13c8e5f146b7f8740fda6955dd0fa7c8cc9d6ff8 Mon Sep 17 00:00:00 2001
From: Naomi <naomiaro@gmail.com>
Date: Tue, 16 Apr 2013 14:37:08 -0400
Subject: [PATCH 01/26] CC-2301 : showing waveform cue/fade editors inside of a
 jquery dialog box.

---
 .../controllers/LibraryController.php         |  18 +
 .../application/layouts/scripts/layout.phtml  |  26 +
 .../views/scripts/playlist/set-cue.phtml      |   1 +
 .../views/scripts/playlist/set-fade.phtml     |   1 +
 .../views/scripts/playlist/update.phtml       |  12 +
 airtime_mvc/build/schema.xml                  |   1 +
 airtime_mvc/public/css/playlist_builder.css   |  21 +-
 airtime_mvc/public/js/airtime/library/spl.js  |  95 +++
 .../public/js/waveformplaylist/config.js      | 162 +++++
 .../public/js/waveformplaylist/controls.js    | 520 +++++++++++++
 .../public/js/waveformplaylist/curves.js      |  72 ++
 .../public/js/waveformplaylist/fades.js       | 140 ++++
 .../public/js/waveformplaylist/loader.js      |  86 +++
 .../js/waveformplaylist/local_storage.js      |  16 +
 .../js/waveformplaylist/observer/observer.js  |  57 ++
 .../js/waveformplaylist/observer/observer.js~ |  57 ++
 .../public/js/waveformplaylist/playlist.js    | 328 +++++++++
 .../public/js/waveformplaylist/playout.js     | 161 ++++
 .../waveformplaylist/templates/bottombar.tpl  |  25 +
 .../waveformplaylist/templates/bottombar.tpl~ |  25 +
 .../public/js/waveformplaylist/time_scale.js  | 151 ++++
 .../public/js/waveformplaylist/track.js       | 686 ++++++++++++++++++
 .../js/waveformplaylist/track_render.js       | 430 +++++++++++
 23 files changed, 3090 insertions(+), 1 deletion(-)
 create mode 100644 airtime_mvc/public/js/waveformplaylist/config.js
 create mode 100644 airtime_mvc/public/js/waveformplaylist/controls.js
 create mode 100644 airtime_mvc/public/js/waveformplaylist/curves.js
 create mode 100644 airtime_mvc/public/js/waveformplaylist/fades.js
 create mode 100644 airtime_mvc/public/js/waveformplaylist/loader.js
 create mode 100644 airtime_mvc/public/js/waveformplaylist/local_storage.js
 create mode 100644 airtime_mvc/public/js/waveformplaylist/observer/observer.js
 create mode 100644 airtime_mvc/public/js/waveformplaylist/observer/observer.js~
 create mode 100644 airtime_mvc/public/js/waveformplaylist/playlist.js
 create mode 100644 airtime_mvc/public/js/waveformplaylist/playout.js
 create mode 100644 airtime_mvc/public/js/waveformplaylist/templates/bottombar.tpl
 create mode 100644 airtime_mvc/public/js/waveformplaylist/templates/bottombar.tpl~
 create mode 100644 airtime_mvc/public/js/waveformplaylist/time_scale.js
 create mode 100644 airtime_mvc/public/js/waveformplaylist/track.js
 create mode 100644 airtime_mvc/public/js/waveformplaylist/track_render.js

diff --git a/airtime_mvc/application/controllers/LibraryController.php b/airtime_mvc/application/controllers/LibraryController.php
index 79dd4b2be..0f3a155b2 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/layouts/scripts/layout.phtml b/airtime_mvc/application/layouts/scripts/layout.phtml
index dedda7c88..8a37a403f 100644
--- a/airtime_mvc/application/layouts/scripts/layout.phtml
+++ b/airtime_mvc/application/layouts/scripts/layout.phtml
@@ -33,5 +33,31 @@
 </div>
 
 <div class="wrapper" id="content"><?php echo $this->layout()->content ?></div>
+
+<script id="tmpl-pl-cues" type="text/template">
+<div class="waveform-cues">
+  <div class="playlist-tracks"></div>
+  <form class="form-inline">
+    <span class="btn_play ui-state-default">Play</span>
+    <span class="btn_stop ui-state-default">Stop</span>
+    <select class="time_format">
+      <option value="seconds">seconds</option>
+      <option value="thousandths">thousandths</option>
+      <option value="hh:mm:ss">hh:mm:ss</option>
+      <option value="hh:mm:ss.uu">hh:mm:ss + hundredths</option>
+      <option value="hh:mm:ss.uuu">hh:mm:ss + milliseconds</option>
+    </select>
+    <input type="text" class="audio_start input-small">
+    <input type="text" class="audio_end input-small">
+    <input type="text" class="audio_pos input-small">
+  </form>
+</div>
+</script>
+
+<script id="tmpl-pl-fades" type="text/template">
+<div class="waveform-fades">
+  <div class="playlist-tracks"></div>
+</div>
+</script>
 </body>
 </html>
diff --git a/airtime_mvc/application/views/scripts/playlist/set-cue.phtml b/airtime_mvc/application/views/scripts/playlist/set-cue.phtml
index 176d8b8ac..5a01d8c50 100644
--- a/airtime_mvc/application/views/scripts/playlist/set-cue.phtml
+++ b/airtime_mvc/application/views/scripts/playlist/set-cue.phtml
@@ -1,3 +1,4 @@
+<div data-uri="<?php echo $this->uri; ?>"><input type="button" class="pl-waveform-cues-btn" value="Show Waveform"></input></div>
 <dl id="spl_cue_editor" class="inline-list">
     <dt><? echo _("Cue In: "); ?><span class='spl_cue_hint'><? echo _("(hh:mm:ss.t)")?></span></dt>
     <dd id="spl_cue_in_<?php echo $this->id; ?>" class="spl_cue_in">
diff --git a/airtime_mvc/application/views/scripts/playlist/set-fade.phtml b/airtime_mvc/application/views/scripts/playlist/set-fade.phtml
index 17f1e7c60..8f57d3cce 100644
--- a/airtime_mvc/application/views/scripts/playlist/set-fade.phtml
+++ b/airtime_mvc/application/views/scripts/playlist/set-fade.phtml
@@ -1,3 +1,4 @@
+<div data-fadeout="<?php echo $this->item1Url; ?>" data-fadein="<?php echo $this->item2Url; ?>"><input type="button" class="pl-waveform-fades-btn" value="Show Waveform"></input></div>
 <dl id="spl_editor" class="inline-list">
     <?php if ($this->item1Type == 0) {?>
     <dt><? echo _("Fade out: "); ?><span class='spl_cue_hint'><? echo _("(ss.t)")?></span></dt>
diff --git a/airtime_mvc/application/views/scripts/playlist/update.phtml b/airtime_mvc/application/views/scripts/playlist/update.phtml
index 20ef17665..d1d85324c 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::Recall($item['item_id']);
+	$fileUrl = $audiofile->getFileUrl();
+}
+
+if (($i < count($items) -1) && ($items[$i+1]['type'] == 0)) {
+	$nextAudiofile = Application_Model_StoredFile::Recall($items[$i+1]['item_id']);
+	$nextFileUrl = $nextAudiofile->getFileUrl();	
+}
 ?>
     <li class="ui-state-default" id="spl_<?php echo $item["id"] ?>" unqid="<?php echo $item["id"]; ?>">
         <div class="list-item-container">
@@ -65,6 +74,7 @@ if ($item['type'] == 2) {
                 'id' => $item["id"],
                 'cueIn' => $item['cuein'],
                 'cueOut' => $item['cueout'],
+            	'uri' => $fileUrl,
                 'origLength' => $item['orig_length'])); ?>
         </div>
         <?php }?>
@@ -80,6 +90,8 @@ 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'])); ?>
         </div>
diff --git a/airtime_mvc/build/schema.xml b/airtime_mvc/build/schema.xml
index b9a8a6951..6ecde0276 100644
--- a/airtime_mvc/build/schema.xml
+++ b/airtime_mvc/build/schema.xml
@@ -227,6 +227,7 @@
     -->
     <column name="type" phpName="DbType" type="SMALLINT" required="true" default="0"/>
     <column name="position" phpName="DbPosition" type="INTEGER" required="false"/>
+    <column name="starts" phpName="DbStarts" type="TIMESTAMP" required="true"/>
     <column name="cliplength" phpName="DbCliplength" type="VARCHAR" sqlType="interval" required="false" defaultValue="00:00:00"/>
     <column name="cuein" phpName="DbCuein" type="VARCHAR" sqlType="interval" required="false" defaultValue="00:00:00"/>
     <column name="cueout" phpName="DbCueout" type="VARCHAR" sqlType="interval" required="false" defaultValue="00:00:00"/>
diff --git a/airtime_mvc/public/css/playlist_builder.css b/airtime_mvc/public/css/playlist_builder.css
index 43b942a7f..19c06b848 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,22 @@ 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;
+}
\ 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..f4ceaff24 100644
--- a/airtime_mvc/public/js/airtime/library/spl.js
+++ b/airtime_mvc/public/js/airtime/library/spl.js
@@ -1061,6 +1061,93 @@ var AIRTIME = (function(AIRTIME){
 		playlistRequest(sUrl, oData);
 	};
 	
+	mod.showFadesWaveform = function(e) {
+		var $el = $(e.target),
+			$parent = $el.parent(),
+			trackEditor = new TrackEditor(),
+			audioControls = new AudioControls(),
+			trackElem,
+			config,
+			$html = $($("#tmpl-pl-fades").html()),
+			tracks = [
+			    {
+			    	src: $parent.data("fadeout")
+				},
+				{
+			    	src: $parent.data("fadein")
+				}
+			];
+		
+		//$el.replaceWith(html);
+		
+		$html.dialog({
+            modal: true,
+            title: "Fade Editor",
+            show: 'clip',
+            hide: 'clip',
+            width: 900,
+            height: 300,
+            buttons: [
+                //{text: "Submit", click: function() {doSomething()}},
+                {text: "Cancel", click: function() {$(this).dialog("close");}}
+            ]
+        });
+		
+		config = new Config({
+			resolution: 15000,
+			state: "shift",
+	        mono: true,
+	        waveHeight: 80,
+	        container: $html[0],
+	        UITheme: "jQueryUI"
+	    });
+		
+		var playlistEditor = new PlaylistEditor();
+	    playlistEditor.setConfig(config);
+	    playlistEditor.init(tracks);
+	};
+	
+	mod.showCuesWaveform = function(e) {
+		var $el = $(e.target),
+			$parent = $el.parent(),
+			uri = $parent.data("uri"),
+			trackEditor = new TrackEditor(),
+			audioControls = new AudioControls(),
+			trackElem,
+			config,
+			$html = $($("#tmpl-pl-cues").html()),
+			tracks = [{
+				src: uri
+			}];
+		
+		//$el.replaceWith(html);
+		
+		$html.dialog({
+            modal: true,
+            title: "Cue Editor",
+            show: 'clip',
+            hide: 'clip',
+            width: 900,
+            height: 300,
+            buttons: [
+                //{text: "Submit", click: function() {doSomething()}},
+                {text: "Cancel", click: function() {$(this).dialog("close");}}
+            ]
+        });
+		
+		config = new Config({
+			resolution: 15000,
+	        mono: true,
+	        waveHeight: 80,
+	        container: $html[0],
+	        UITheme: "jQueryUI"
+	    });
+		
+		var playlistEditor = new PlaylistEditor();
+	    playlistEditor.setConfig(config);
+	    playlistEditor.init(tracks);
+	};
+	
 	mod.init = function() {
 	    /*
 	    $.contextMenu({
@@ -1089,6 +1176,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..0aec0b339
--- /dev/null
+++ b/airtime_mvc/public/js/waveformplaylist/config.js
@@ -0,0 +1,162 @@
+/*
+    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.
+
+            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.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.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..0a19a0968
--- /dev/null
+++ b/airtime_mvc/public/js/waveformplaylist/controls.js
@@ -0,0 +1,520 @@
+'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_select": {
+        click: "changeState"
+    },
+
+    "btn_shift": {
+        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.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.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.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.getElementById("btn_"+state);
+
+    if (tmpBtn) {
+        tmpBtn.className = this.classes["btn-state-active"];
+    }
+
+    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.setButtonState = function(el, classname) {
+    el && el.className = this.classes[classname];
+};
+
+AudioControls.prototype.changeState = function(e) {
+    var el = e.currentTarget,
+        state = el.dataset.state;
+
+    this.el.getElementsByClassName('active')[0].className = classes["btn-state-default"];
+
+    this.setButtonState("btn-state-default");
+    this.setButtonState(el, "btn-state-active");
+
+    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..92887c0c6
--- /dev/null
+++ b/airtime_mvc/public/js/waveformplaylist/playlist.js
@@ -0,0 +1,328 @@
+'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("changestate", "onStateChange", trackEditor);
+        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;
+
+    for(i = 0, len = editors.length; i < len; i++) {
+        editors[i].deactivate();
+    }
+};
+
+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);
+    } 
+};
+
+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 @@
+<form class="form-inline">
+  <select id="time_format">
+    <option value="seconds">seconds</option>
+    <option value="thousandths">thousandths</option>
+    <option value="hh:mm:ss">hh:mm:ss</option>
+    <option value="hh:mm:ss.uu">hh:mm:ss + hundredths</option>
+    <option value="hh:mm:ss.uuu">hh:mm:ss + milliseconds</option>
+  </select>
+  <input id="audio_start" type="text" class="input-small">
+  <input id="audio_end" type="text" class="input-small">
+  <input id="audio_pos" type="text" class="input-small">
+  <select id="audio_resolution">
+    <option>4000</option>
+    <option>5000</option>
+    <option>6000</option>
+    <option>7000</option>
+    <option>8000</option>
+    <option>9000</option>
+    <option>10000</option>
+    <option>11000</option>
+    <option>12000</option>
+    <option>15000</option>
+    <option>20000</option>
+  </select>
+</form>
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 @@
+<form class="form-inline">
+  <select id="time_format">
+    <option value="seconds">seconds</option>
+    <option value="thousandths">thousandths</option>
+    <option value="hh:mm:ss">hh:mm:ss</option>
+    <option value="hh:mm:ss.uu">hh:mm:ss + hundredths</option>
+    <option value="hh:mm:ss.uuu">hh:mm:ss + milliseconds</option>
+  </select>
+  <input id="audio_start" type="text" class="input-small">
+  <input id="audio_end" type="text" class="input-small">
+  <input id="audio_pos" type="text" class="input-small">
+  <select id="audio_resolution">
+    <option>4000</option>
+    <option>5000</option>
+    <option>6000</option>
+    <option>7000</option>
+    <option>8000</option>
+    <option>9000</option>
+    <option>10000</option>
+    <option>11000</option>
+    <option>12000</option>
+    <option>15000</option>
+    <option>20000</option>
+  </select>
+</form>
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..b583ae4cb
--- /dev/null
+++ b/airtime_mvc/public/js/waveformplaylist/track.js
@@ -0,0 +1,686 @@
+'use strict';
+
+var TrackEditor = function() {
+
+};
+
+TrackEditor.prototype.states = {
+    select: {
+        events: {
+            mousedown: "selectStart"
+        },
+
+        classes: [
+            "state-select"
+        ]
+    },
+    
+    shift: {
+        events: {
+            mousedown: "timeShift"
+        },
+
+        classes: [
+            "state-shift"
+        ]
+    }
+};
+
+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) {
+   
+    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 = fades || {};
+
+    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
+        }
+    );
+    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);
+    }
+
+    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.drawer.draw(-1, this.getPixelOffset());
+};
+
+/* 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 = document.body.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.
+*/
+TrackEditor.prototype.adjustSelectedArea = function(start, end) {
+    var buffer = this.getBuffer();
+
+    if (start < 0) {
+        start = 0;
+    }
+
+    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;
+
+    //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;
+    }
+
+    this.prevSelectedArea = this.selectedArea;
+    this.selectedArea = this.adjustSelectedArea(this.pixelsToSamples(left), this.pixelsToSamples(right));
+};
+
+TrackEditor.prototype.activateAudioSelection = function() {
+
+    this.fire("activateSelection");
+};
+
+TrackEditor.prototype.deactivateAudioSelection = function() {
+
+    this.fire("deactivateSelection");
+};
+
+TrackEditor.prototype.selectStart = function(e) {
+    var el = e.currentTarget, //want the events placed on the channel wrapper.
+        editor = this,
+        //scroll = this.config.getTrackScroll(),
+        //scrollX = scroll.left,
+        //startX = scrollX + (e.layerX || e.offsetX), //relative to e.target (want the canvas).
+        //prevX = scrollX + (e.layerX || e.offsetX),
+        startX = e.layerX || e.offsetX, //relative to e.target (want the canvas).
+        prevX = e.layerX || e.offsetX,
+        offset = this.leftOffset,
+        startTime;
+
+    if (e.target.tagName !== "CANVAS") {
+        return;
+    }
+
+    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 = e.layerX || e.offsetX,
+            //currentX = scrollX + (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 = e.layerX || e.offsetX,
+            //endX = scrollX + (e.layerX || e.offsetX),
+            minX, maxX,
+            cursorPos,
+            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 = document.body.onmouseup = null;
+        
+        //if more than one pixel is selected, listen to possible fade events.
+        if (Math.abs(minX - maxX)) {
+            editor.activateAudioSelection();
+        }
+        else {
+            editor.deactivateAudioSelection();
+        }
+
+        cursorPos = startTime = editor.samplesToSeconds(offset + editor.selectedArea.start);
+        endTime = editor.samplesToSeconds(offset + editor.selectedArea.end);
+
+        editor.updateEditor(-1, undefined, undefined, true);
+        editor.config.setCursorPos(cursorPos);
+        editor.notifySelectUpdate(startTime, endTime);    
+    };
+};
+
+/* 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];
+};
+
+/*
+    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.onCreateFade = function(args) {
+    var selected = this.selectedArea,
+        pixelOffset = this.getPixelOffset(),
+        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, args.type, args.shape, startTime, endTime);
+    this.drawer.draw(-1, pixelOffset);
+    this.drawer.drawFade(id, args.type, args.shape, start, end);
+
+    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.states[state].events,
+        stateClasses = this.states[state].classes,
+        container = this.container,
+        prevState = this.currentState,
+        prevStateClasses,
+        prevStateEvents = this.prevStateEvents,
+        func, event, cl,
+        i, len;
+
+    if (prevState) {
+        prevStateClasses = this.states[prevState].classes;
+       
+        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]);
+        }
+    }
+
+    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]);
+        }
+
+    this.currentState = state;
+};
+
+TrackEditor.prototype.onStateChange = function() {
+    var state = this.config.getState();
+
+    this.setState(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;
+
+    d = {
+        start: this.startTime,
+        end: this.endTime,
+        fades: this.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..611dfcbc0
--- /dev/null
+++ b/airtime_mvc/public/js/waveformplaylist/track_render.js
@@ -0,0 +1,430 @@
+'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.drawFade = function(id, type, shape, start, end) {
+    var div,
+        canv,
+        width,
+        left,
+        fragment = document.createDocumentFragment(),
+        i, len,
+        dup,
+        ctx,
+        tmpCtx;
+
+        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);
+        }
+    }
+};
+

From 0385285741cc51bb7696d6a47ff5f6c08a306bb8 Mon Sep 17 00:00:00 2001
From: Naomi <naomiaro@gmail.com>
Date: Tue, 16 Apr 2013 17:24:30 -0400
Subject: [PATCH 02/26] CC-2301 : waveform editors are now in their own
 separate pop up. controls working for fades/cues. should add more specific
 states for fade editor.

---
 .../application/layouts/scripts/layout.phtml  |  8 ++
 airtime_mvc/public/js/airtime/library/spl.js  | 74 +++++++++----------
 .../public/js/waveformplaylist/controls.js    | 25 +++++--
 3 files changed, 58 insertions(+), 49 deletions(-)

diff --git a/airtime_mvc/application/layouts/scripts/layout.phtml b/airtime_mvc/application/layouts/scripts/layout.phtml
index 8a37a403f..b4836b324 100644
--- a/airtime_mvc/application/layouts/scripts/layout.phtml
+++ b/airtime_mvc/application/layouts/scripts/layout.phtml
@@ -57,6 +57,14 @@
 <script id="tmpl-pl-fades" type="text/template">
 <div class="waveform-fades">
   <div class="playlist-tracks"></div>
+  <div>
+    <span class="btn_play ui-state-default">Play</span>
+    <span class="btn_stop ui-state-default">Stop</span>
+  </div>
+  <div>
+    <span class="btn_select ui-state-default" data-state="select">Select</span>
+    <span class="btn_shift ui-state-default" data-state="shift">Shift</span>
+  </div>
 </div>
 </script>
 </body>
diff --git a/airtime_mvc/public/js/airtime/library/spl.js b/airtime_mvc/public/js/airtime/library/spl.js
index f4ceaff24..adb5dd3a4 100644
--- a/airtime_mvc/public/js/airtime/library/spl.js
+++ b/airtime_mvc/public/js/airtime/library/spl.js
@@ -1064,10 +1064,6 @@ var AIRTIME = (function(AIRTIME){
 	mod.showFadesWaveform = function(e) {
 		var $el = $(e.target),
 			$parent = $el.parent(),
-			trackEditor = new TrackEditor(),
-			audioControls = new AudioControls(),
-			trackElem,
-			config,
 			$html = $($("#tmpl-pl-fades").html()),
 			tracks = [
 			    {
@@ -1078,8 +1074,6 @@ var AIRTIME = (function(AIRTIME){
 				}
 			];
 		
-		//$el.replaceWith(html);
-		
 		$html.dialog({
             modal: true,
             title: "Fade Editor",
@@ -1090,38 +1084,34 @@ var AIRTIME = (function(AIRTIME){
             buttons: [
                 //{text: "Submit", click: function() {doSomething()}},
                 {text: "Cancel", click: function() {$(this).dialog("close");}}
-            ]
-        });
-		
-		config = new Config({
-			resolution: 15000,
-			state: "shift",
-	        mono: true,
-	        waveHeight: 80,
-	        container: $html[0],
-	        UITheme: "jQueryUI"
-	    });
-		
-		var playlistEditor = new PlaylistEditor();
-	    playlistEditor.setConfig(config);
-	    playlistEditor.init(tracks);
+            ],
+            open: function (event, ui) {
+            	
+            	var config = new Config({
+        			resolution: 15000,
+        			state: "shift",
+        	        mono: true,
+        	        waveHeight: 80,
+        	        container: $html[0],
+        	        UITheme: "jQueryUI"
+        	    });
+        		
+        		var playlistEditor = new PlaylistEditor();
+        	    playlistEditor.setConfig(config);
+        	    playlistEditor.init(tracks);
+            }
+        });		
 	};
 	
 	mod.showCuesWaveform = function(e) {
 		var $el = $(e.target),
 			$parent = $el.parent(),
 			uri = $parent.data("uri"),
-			trackEditor = new TrackEditor(),
-			audioControls = new AudioControls(),
-			trackElem,
-			config,
 			$html = $($("#tmpl-pl-cues").html()),
 			tracks = [{
 				src: uri
 			}];
 		
-		//$el.replaceWith(html);
-		
 		$html.dialog({
             modal: true,
             title: "Cue Editor",
@@ -1132,20 +1122,22 @@ var AIRTIME = (function(AIRTIME){
             buttons: [
                 //{text: "Submit", click: function() {doSomething()}},
                 {text: "Cancel", click: function() {$(this).dialog("close");}}
-            ]
-        });
-		
-		config = new Config({
-			resolution: 15000,
-	        mono: true,
-	        waveHeight: 80,
-	        container: $html[0],
-	        UITheme: "jQueryUI"
-	    });
-		
-		var playlistEditor = new PlaylistEditor();
-	    playlistEditor.setConfig(config);
-	    playlistEditor.init(tracks);
+            ],
+            open: function (event, ui) {
+            	
+            	var config = new Config({
+        			resolution: 15000,
+        	        mono: true,
+        	        waveHeight: 80,
+        	        container: $html[0],
+        	        UITheme: "jQueryUI"
+        	    });
+        		
+        		var playlistEditor = new PlaylistEditor();
+        	    playlistEditor.setConfig(config);
+        	    playlistEditor.init(tracks);	
+            }
+        });	
 	};
 	
 	mod.init = function() {
diff --git a/airtime_mvc/public/js/waveformplaylist/controls.js b/airtime_mvc/public/js/waveformplaylist/controls.js
index 0a19a0968..6fa468b35 100644
--- a/airtime_mvc/public/js/waveformplaylist/controls.js
+++ b/airtime_mvc/public/js/waveformplaylist/controls.js
@@ -202,10 +202,10 @@ AudioControls.prototype.init = function(config) {
     container = this.config.getContainer();
     state = this.config.getState();
 
-    tmpBtn = document.getElementById("btn_"+state);
+    tmpBtn = document.getElementsByClassName("btn_"+state)[0];
 
     if (tmpBtn) {
-        tmpBtn.className = this.classes["btn-state-active"];
+        this.activateButton(tmpBtn);
     }
 
     for (className in events) {
@@ -388,18 +388,27 @@ AudioControls.prototype.stopAudio = function() {
     this.fire('stopaudio', this);
 };
 
-AudioControls.prototype.setButtonState = function(el, classname) {
-    el && el.className = this.classes[classname];
+AudioControls.prototype.activateButton = function(el) {
+    if (el) {
+        el.classList.remove(this.classes["disabled"]);
+        el.classList.add(this.classes["active"]);
+    }
+};
+
+AudioControls.prototype.disableButton = function(el) {
+    if (el) {
+        el.classList.add(this.classes["disabled"]);
+        el.classList.remove(this.classes["active"]);
+    }
 };
 
 AudioControls.prototype.changeState = function(e) {
     var el = e.currentTarget,
+        prevEl = el.parentElement.getElementsByClassName('active')[0],
         state = el.dataset.state;
 
-    this.el.getElementsByClassName('active')[0].className = classes["btn-state-default"];
-
-    this.setButtonState("btn-state-default");
-    this.setButtonState(el, "btn-state-active");
+    this.disableButton(prevEl);
+    this.activateButton(el);
 
     this.config.setState(state);
     this.fire('changestate', this);

From 0d3c0912ba442e9bed5c8164d71895c4c8ad2e3a Mon Sep 17 00:00:00 2001
From: Naomi <naomiaro@gmail.com>
Date: Wed, 17 Apr 2013 17:17:39 -0400
Subject: [PATCH 03/26] CC-2301 : Waveform Editor

Cue editor can save values to the playlist.
---
 .../application/layouts/scripts/layout.phtml  | 29 ++++---
 airtime_mvc/public/js/airtime/library/spl.js  | 78 ++++++++++++++++++-
 .../public/js/waveformplaylist/controls.js    | 10 +++
 3 files changed, 101 insertions(+), 16 deletions(-)

diff --git a/airtime_mvc/application/layouts/scripts/layout.phtml b/airtime_mvc/application/layouts/scripts/layout.phtml
index b4836b324..930218b91 100644
--- a/airtime_mvc/application/layouts/scripts/layout.phtml
+++ b/airtime_mvc/application/layouts/scripts/layout.phtml
@@ -37,20 +37,25 @@
 <script id="tmpl-pl-cues" type="text/template">
 <div class="waveform-cues">
   <div class="playlist-tracks"></div>
-  <form class="form-inline">
+  <div class="playlist-controls">
     <span class="btn_play ui-state-default">Play</span>
     <span class="btn_stop ui-state-default">Stop</span>
-    <select class="time_format">
-      <option value="seconds">seconds</option>
-      <option value="thousandths">thousandths</option>
-      <option value="hh:mm:ss">hh:mm:ss</option>
-      <option value="hh:mm:ss.uu">hh:mm:ss + hundredths</option>
-      <option value="hh:mm:ss.uuu">hh:mm:ss + milliseconds</option>
-    </select>
-    <input type="text" class="audio_start input-small">
-    <input type="text" class="audio_end input-small">
-    <input type="text" class="audio_pos input-small">
-  </form>
+  </div>
+  <div>
+    <input type="text" class="audio_start">
+	<input type="button" class="set-cue-in" value="Set Cue In">
+    <input type="text" class="audio_end">
+    <input type="button" class="set-cue-out" value="Set Cue Out">
+    <input type="text" class="audio_pos">
+  </div>
+  <div>
+	<label for="editor-cue-in">Cue In</label>
+    <input type="text" id="editor-cue-in" class="editor-cue-in">
+	<span style="display:none" class="cue-in-error"></span>
+    <label for="editor-cue-out">Cue Out</label>
+    <input type="text" id="editor-cue-out" class="editor-cue-out">
+    <span style="display:none" class="cue-out-error"></span>
+  </div>
 </div>
 </script>
 
diff --git a/airtime_mvc/public/js/airtime/library/spl.js b/airtime_mvc/public/js/airtime/library/spl.js
index adb5dd3a4..70c91ce54 100644
--- a/airtime_mvc/public/js/airtime/library/spl.js
+++ b/airtime_mvc/public/js/airtime/library/spl.js
@@ -137,6 +137,54 @@ 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('close');
+
+                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'));
+            });
+    }
 
     function changeFadeIn(event) {
         event.preventDefault();
@@ -1093,7 +1141,8 @@ var AIRTIME = (function(AIRTIME){
         	        mono: true,
         	        waveHeight: 80,
         	        container: $html[0],
-        	        UITheme: "jQueryUI"
+        	        UITheme: "jQueryUI",
+        	        timeFormat: 'hh:mm:ss.u'
         	    });
         		
         		var playlistEditor = new PlaylistEditor();
@@ -1105,6 +1154,7 @@ var AIRTIME = (function(AIRTIME){
 	
 	mod.showCuesWaveform = function(e) {
 		var $el = $(e.target),
+			id = $el.parents("li").attr("unqid"),
 			$parent = $el.parent(),
 			uri = $parent.data("uri"),
 			$html = $($("#tmpl-pl-cues").html()),
@@ -1112,6 +1162,18 @@ var AIRTIME = (function(AIRTIME){
 				src: uri
 			}];
 		
+		$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",
@@ -1120,8 +1182,15 @@ var AIRTIME = (function(AIRTIME){
             width: 900,
             height: 300,
             buttons: [
-                //{text: "Submit", click: function() {doSomething()}},
-                {text: "Cancel", click: function() {$(this).dialog("close");}}
+                {text: "Save", click: function() {
+                	var cueIn = $html.find('.editor-cue-in').val(),
+                		cueOut = $html.find('.editor-cue-out').val();
+                	
+                	changeCues($html, id, cueIn, cueOut);
+                }},
+                {text: "Cancel", click: function() {
+                	$(this).dialog("close");
+                }}
             ],
             open: function (event, ui) {
             	
@@ -1130,7 +1199,8 @@ var AIRTIME = (function(AIRTIME){
         	        mono: true,
         	        waveHeight: 80,
         	        container: $html[0],
-        	        UITheme: "jQueryUI"
+        	        UITheme: "jQueryUI",
+        	        timeFormat: 'hh:mm:ss.u'
         	    });
         		
         		var playlistEditor = new PlaylistEditor();
diff --git a/airtime_mvc/public/js/waveformplaylist/controls.js b/airtime_mvc/public/js/waveformplaylist/controls.js
index 6fa468b35..097f2fba3 100644
--- a/airtime_mvc/public/js/waveformplaylist/controls.js
+++ b/airtime_mvc/public/js/waveformplaylist/controls.js
@@ -85,6 +85,8 @@ AudioControls.prototype.validateCue = function(value) {
 
         "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}$/
@@ -126,6 +128,10 @@ AudioControls.prototype.cueToSeconds = function(value) {
             return clockConverter(value);
         },
 
+        "hh:mm:ss.u": function(value) {
+            return clockConverter(value);
+        },
+
         "hh:mm:ss.uu": function(value) {
             return clockConverter(value);
         },
@@ -172,6 +178,10 @@ AudioControls.prototype.cueFormatters = function(format) {
             return clockFormat(seconds, 0);   
         },
 
+        "hh:mm:ss.u": function (seconds) {
+            return clockFormat(seconds, 1);   
+        },
+
         "hh:mm:ss.uu": function (seconds) {
             return clockFormat(seconds, 2);   
         },

From 04a0d9c54eb3178f07d59be1ec733580a2eb5f66 Mon Sep 17 00:00:00 2001
From: Naomi <naomiaro@gmail.com>
Date: Wed, 17 Apr 2013 18:00:16 -0400
Subject: [PATCH 04/26] CC-2301 : waveform editor

current cues being set.
---
 .../application/views/scripts/playlist/set-cue.phtml   |  4 ++--
 airtime_mvc/public/js/airtime/library/spl.js           | 10 ++++++++--
 2 files changed, 10 insertions(+), 4 deletions(-)

diff --git a/airtime_mvc/application/views/scripts/playlist/set-cue.phtml b/airtime_mvc/application/views/scripts/playlist/set-cue.phtml
index 5a01d8c50..1e8a7a315 100644
--- a/airtime_mvc/application/views/scripts/playlist/set-cue.phtml
+++ b/airtime_mvc/application/views/scripts/playlist/set-cue.phtml
@@ -1,12 +1,12 @@
 <div data-uri="<?php echo $this->uri; ?>"><input type="button" class="pl-waveform-cues-btn" value="Show Waveform"></input></div>
 <dl id="spl_cue_editor" class="inline-list">
     <dt><? echo _("Cue In: "); ?><span class='spl_cue_hint'><? echo _("(hh:mm:ss.t)")?></span></dt>
-    <dd id="spl_cue_in_<?php echo $this->id; ?>" class="spl_cue_in">
+    <dd id="spl_cue_in_<?php echo $this->id; ?>" class="spl_cue_in" data-cue-in="<?php echo $this->cueIn; ?>">
         <span contenteditable="true" class="spl_text_input"><?php echo $this->cueIn; ?></span>
     </dd>
     <dd class="edit-error"></dd>
     <dt><? echo _("Cue Out: "); ?><span class='spl_cue_hint'><? echo _("(hh:mm:ss.t)")?></span></dt>
-    <dd id="spl_cue_out_<?php echo $this->id; ?>" class="spl_cue_out">
+    <dd id="spl_cue_out_<?php echo $this->id; ?>" class="spl_cue_out" data-cue-out="<?php echo $this->cueOut; ?>">
         <span contenteditable="true" class="spl_text_input"><?php echo $this->cueOut; ?></span>
     </dd>
     <dd class="edit-error"></dd>
diff --git a/airtime_mvc/public/js/airtime/library/spl.js b/airtime_mvc/public/js/airtime/library/spl.js
index 70c91ce54..40be88922 100644
--- a/airtime_mvc/public/js/airtime/library/spl.js
+++ b/airtime_mvc/public/js/airtime/library/spl.js
@@ -1154,13 +1154,19 @@ var AIRTIME = (function(AIRTIME){
 	
 	mod.showCuesWaveform = function(e) {
 		var $el = $(e.target),
-			id = $el.parents("li").attr("unqid"),
+			$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");
+		
+		$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();

From 956bcccd2410444a17909b6bc2a901d366a2e4fe Mon Sep 17 00:00:00 2001
From: Naomi <naomiaro@gmail.com>
Date: Tue, 23 Apr 2013 16:36:35 -0400
Subject: [PATCH 05/26] CC-2301 : creating different states for the fade
 editor: cursor, fadein, fadeout.

---
 .../application/layouts/scripts/layout.phtml  |  18 ++-
 airtime_mvc/public/css/playlist_builder.css   |   6 +
 airtime_mvc/public/js/airtime/library/spl.js  |  14 ++-
 .../public/js/waveformplaylist/controls.js    |  29 ++++-
 .../public/js/waveformplaylist/track.js       | 116 ++++++++++++++++--
 5 files changed, 164 insertions(+), 19 deletions(-)

diff --git a/airtime_mvc/application/layouts/scripts/layout.phtml b/airtime_mvc/application/layouts/scripts/layout.phtml
index 930218b91..1cb0da978 100644
--- a/airtime_mvc/application/layouts/scripts/layout.phtml
+++ b/airtime_mvc/application/layouts/scripts/layout.phtml
@@ -67,9 +67,25 @@
     <span class="btn_stop ui-state-default">Stop</span>
   </div>
   <div>
-    <span class="btn_select ui-state-default" data-state="select">Select</span>
+	<span class="btn_select ui-state-default" data-state="cursor">Cursor</span>
+    <span class="btn_fadein ui-state-default" data-state="fadein">Fade In</span>
+	<span class="btn_fadeout ui-state-default" data-state="fadeout">Fade Out</span>
     <span class="btn_shift ui-state-default" data-state="shift">Shift</span>
   </div>
+  <div id="btns_fade" class="btns_fade">
+    <div class="fade-in-ctrls">
+      <a class="disabled" data-type="FadeIn" data-shape="linear">Linear</a>
+      <a class="disabled" data-type="FadeIn" data-shape="logarithmic">Logarithmic</a>
+      <a class="disabled" data-type="FadeIn" data-shape="sCurve">S-Curve</a>
+      <a class="disabled" data-type="FadeIn" data-shape="exponential">Exponential</a>
+    </div>
+    <div class="fade-out-ctrls">
+      <a class="disabled" data-type="FadeOut" data-shape="linear">Linear</a>
+      <a class="disabled" data-type="FadeOut" data-shape="logarithmic">Logarithmic</a>
+      <a class="disabled" data-type="FadeOut" data-shape="sCurve">S-Curve</a>
+      <a class="disabled" data-type="FadeOut" data-shape="exponential">Exponential</a>
+    </div>
+  </div>
 </div>
 </script>
 </body>
diff --git a/airtime_mvc/public/css/playlist_builder.css b/airtime_mvc/public/css/playlist_builder.css
index 19c06b848..7d828bd31 100644
--- a/airtime_mvc/public/css/playlist_builder.css
+++ b/airtime_mvc/public/css/playlist_builder.css
@@ -603,4 +603,10 @@ li.spl_empty {
 .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 40be88922..2aecd99a3 100644
--- a/airtime_mvc/public/js/airtime/library/spl.js
+++ b/airtime_mvc/public/js/airtime/library/spl.js
@@ -1120,15 +1120,16 @@ var AIRTIME = (function(AIRTIME){
 				{
 			    	src: $parent.data("fadein")
 				}
-			];
+			],
+			dim = AIRTIME.utilities.findViewportDimensions();
 		
 		$html.dialog({
             modal: true,
             title: "Fade Editor",
             show: 'clip',
             hide: 'clip',
-            width: 900,
-            height: 300,
+            width: dim.width - 100,
+            height: dim.height - 100,
             buttons: [
                 //{text: "Submit", click: function() {doSomething()}},
                 {text: "Cancel", click: function() {$(this).dialog("close");}}
@@ -1163,7 +1164,8 @@ var AIRTIME = (function(AIRTIME){
 				src: uri
 			}],
 			cueIn = $li.find('.spl_cue_in').data("cueIn"),
-			cueOut = $li.find('.spl_cue_out').data("cueOut");
+			cueOut = $li.find('.spl_cue_out').data("cueOut"),
+			dim = AIRTIME.utilities.findViewportDimensions();
 		
 		$html.find('.editor-cue-in').val(cueIn);
 		$html.find('.editor-cue-out').val(cueOut);
@@ -1185,8 +1187,8 @@ var AIRTIME = (function(AIRTIME){
             title: "Cue Editor",
             show: 'clip',
             hide: 'clip',
-            width: 900,
-            height: 300,
+            width: dim.width - 100,
+            height: dim.height - 100,
             buttons: [
                 {text: "Save", click: function() {
                 	var cueIn = $html.find('.editor-cue-in').val(),
diff --git a/airtime_mvc/public/js/waveformplaylist/controls.js b/airtime_mvc/public/js/waveformplaylist/controls.js
index 097f2fba3..d83af02f0 100644
--- a/airtime_mvc/public/js/waveformplaylist/controls.js
+++ b/airtime_mvc/public/js/waveformplaylist/controls.js
@@ -28,6 +28,10 @@ AudioControls.prototype.events = {
         click: "stopAudio"
     },
 
+    "btn_cursor": {
+        click: "changeState"
+    },
+
     "btn_select": {
         click: "changeState"
     },
@@ -36,6 +40,15 @@ AudioControls.prototype.events = {
         click: "changeState"
     },
 
+    "btn_fadein": {
+        click: "changeState"
+    },
+
+    "btn_fadeout": {
+        click: "changeState"
+    },
+
+
     "btns_fade": {
         click: "createFade"
     },
@@ -400,15 +413,25 @@ AudioControls.prototype.stopAudio = function() {
 
 AudioControls.prototype.activateButton = function(el) {
     if (el) {
-        el.classList.remove(this.classes["disabled"]);
         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"]);
-        el.classList.remove(this.classes["active"]);
     }
 };
 
@@ -417,7 +440,7 @@ AudioControls.prototype.changeState = function(e) {
         prevEl = el.parentElement.getElementsByClassName('active')[0],
         state = el.dataset.state;
 
-    this.disableButton(prevEl);
+    this.deactivateButton(prevEl);
     this.activateButton(el);
 
     this.config.setState(state);
diff --git a/airtime_mvc/public/js/waveformplaylist/track.js b/airtime_mvc/public/js/waveformplaylist/track.js
index b583ae4cb..a3123d0d7 100644
--- a/airtime_mvc/public/js/waveformplaylist/track.js
+++ b/airtime_mvc/public/js/waveformplaylist/track.js
@@ -5,6 +5,16 @@ var TrackEditor = function() {
 };
 
 TrackEditor.prototype.states = {
+    cursor: {
+        events: {
+            mousedown: "selectCursorPos"
+        },
+
+        classes: [
+            "state-select"
+        ]
+    },
+
     select: {
         events: {
             mousedown: "selectStart"
@@ -14,6 +24,26 @@ TrackEditor.prototype.states = {
             "state-select"
         ]
     },
+
+    fadein: {
+        events: {
+            mousedown: "selectFadeIn"
+        },
+
+        classes: [
+            "state-select"
+        ]
+    },
+
+    fadeout: {
+        events: {
+            mousedown: "selectFadeOut"
+        },
+
+        classes: [
+            "state-select"
+        ]
+    },
     
     shift: {
         events: {
@@ -218,7 +248,7 @@ TrackEditor.prototype.timeShift = function(e) {
         scroll = this.config.getTrackScroll(),
         scrollX = scroll.left;
 
-    origX = editor.leftOffset/res;
+    origX = editor.leftOffset / res;
     
     //dynamically put an event on the element.
     el.onmousemove = function(e) {
@@ -232,7 +262,7 @@ TrackEditor.prototype.timeShift = function(e) {
     el.onmouseup = function() {
         var delta;
 
-        el.onmousemove = document.body.onmouseup = null;
+        el.onmousemove = el.onmouseup = null;
         editor.leftOffset = editor.pixelsToSamples(updatedX);
         delta = editor.pixelsToSeconds(diffX);
 
@@ -299,7 +329,10 @@ TrackEditor.prototype.setSelectedArea = function(start, end, shiftKey) {
     var left, 
         right,
         currentStart,
-        currentEnd;
+        currentEnd,
+        sampLeft,
+        sampRight,
+        buffer = this.getBuffer();
 
     //extending selected area since shift is pressed.
     if (shiftKey && (end - start === 0) && (this.prevSelectedArea !== undefined)) {
@@ -332,8 +365,11 @@ TrackEditor.prototype.setSelectedArea = function(start, end, shiftKey) {
         right = end;
     }
 
+    sampLeft = left === undefined ? 0 : this.pixelsToSamples(left);
+    sampRight = right === undefined ? buffer.length - 1 : this.pixelsToSamples(right);
+
     this.prevSelectedArea = this.selectedArea;
-    this.selectedArea = this.adjustSelectedArea(this.pixelsToSamples(left), this.pixelsToSamples(right));
+    this.selectedArea = this.adjustSelectedArea(sampLeft, sampRight);
 };
 
 TrackEditor.prototype.activateAudioSelection = function() {
@@ -398,9 +434,7 @@ TrackEditor.prototype.selectStart = function(e) {
     };
     el.onmouseup = function(e) {
         var endX = e.layerX || e.offsetX,
-            //endX = scrollX + (e.layerX || e.offsetX),
             minX, maxX,
-            cursorPos,
             startTime, endTime;
 
         minX = Math.min(startX, endX);
@@ -411,7 +445,7 @@ TrackEditor.prototype.selectStart = function(e) {
         minX = editor.samplesToPixels(offset + editor.selectedArea.start);
         maxX = editor.samplesToPixels(offset + editor.selectedArea.end);
 
-        el.onmousemove = document.body.onmouseup = null;
+        el.onmousemove = el.onmouseup = null;
         
         //if more than one pixel is selected, listen to possible fade events.
         if (Math.abs(minX - maxX)) {
@@ -421,15 +455,79 @@ TrackEditor.prototype.selectStart = function(e) {
             editor.deactivateAudioSelection();
         }
 
-        cursorPos = startTime = editor.samplesToSeconds(offset + editor.selectedArea.start);
+        startTime = editor.samplesToSeconds(offset + editor.selectedArea.start);
         endTime = editor.samplesToSeconds(offset + editor.selectedArea.end);
 
         editor.updateEditor(-1, undefined, undefined, true);
-        editor.config.setCursorPos(cursorPos);
+        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;
+
+    if (e.target.tagName !== "CANVAS") {
+        return;
+    }
+
+    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 editor = this,
+        startX = e.layerX || e.offsetX, //relative to e.target (want the canvas).
+        offset = this.leftOffset,
+        startTime, 
+        endTime;
+
+    if (e.target.tagName !== "CANVAS") {
+        return;
+    }
+
+    editor.setSelectedArea(undefined, startX);
+    startTime = editor.samplesToSeconds(offset + editor.selectedArea.start);
+    endTime = editor.samplesToSeconds(offset + editor.selectedArea.end);
+
+    editor.updateEditor(-1, undefined, undefined, true);
+    editor.notifySelectUpdate(startTime, endTime);
+
+    editor.activateAudioSelection();
+};
+
+TrackEditor.prototype.selectFadeOut = function(e) {
+    var editor = this,
+        startX = e.layerX || e.offsetX, //relative to e.target (want the canvas).
+        offset = this.leftOffset,
+        startTime,
+        endTime;
+
+    if (e.target.tagName !== "CANVAS") {
+        return;
+    }
+
+    editor.setSelectedArea(startX, undefined);
+    startTime = editor.samplesToSeconds(offset + editor.selectedArea.start);
+    endTime = editor.samplesToSeconds(offset + editor.selectedArea.end);
+
+    editor.updateEditor(-1, undefined, undefined, true);
+    editor.notifySelectUpdate(startTime, endTime);
+
+    editor.activateAudioSelection();
+};
+
 /* end of state methods */
 
 TrackEditor.prototype.saveFade = function(id, type, shape, start, end) {

From 179621d687836c36778632dd0750c05ad0f75b48 Mon Sep 17 00:00:00 2001
From: Naomi <naomiaro@gmail.com>
Date: Wed, 24 Apr 2013 14:15:07 -0400
Subject: [PATCH 06/26] CC-2301 : fades editor is loading now with a leftoffset
 for the second track and the cues for both tracks.

---
 airtime_mvc/application/models/Playlist.php      |  5 +++++
 .../views/scripts/playlist/set-fade.phtml        | 12 ++++++++----
 .../views/scripts/playlist/update.phtml          |  8 +++++++-
 airtime_mvc/public/js/airtime/library/spl.js     | 13 ++++++++++---
 .../public/js/waveformplaylist/playlist.js       | 16 ++++++++++++++++
 5 files changed, 46 insertions(+), 8 deletions(-)

diff --git a/airtime_mvc/application/models/Playlist.php b/airtime_mvc/application/models/Playlist.php
index ea584b76c..0e2e9b4fe 100644
--- a/airtime_mvc/application/models/Playlist.php
+++ b/airtime_mvc/application/models/Playlist.php
@@ -239,6 +239,11 @@ SQL;
         $offset = 0;
         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']);
+            
             $offset += $clipSec;
             $offset_cliplength = Application_Common_DateHelper::secondsToPlaylistTime($offset);
 
diff --git a/airtime_mvc/application/views/scripts/playlist/set-fade.phtml b/airtime_mvc/application/views/scripts/playlist/set-fade.phtml
index 8f57d3cce..6a300eeee 100644
--- a/airtime_mvc/application/views/scripts/playlist/set-fade.phtml
+++ b/airtime_mvc/application/views/scripts/playlist/set-fade.phtml
@@ -1,17 +1,21 @@
-<div data-fadeout="<?php echo $this->item1Url; ?>" data-fadein="<?php echo $this->item2Url; ?>"><input type="button" class="pl-waveform-fades-btn" value="Show Waveform"></input></div>
 <dl id="spl_editor" class="inline-list">
+	<dd>
+	  <input type="button" class="pl-waveform-fades-btn" value="Show Waveform"></input>
+	</dd>
     <?php if ($this->item1Type == 0) {?>
     <dt><? echo _("Fade out: "); ?><span class='spl_cue_hint'><? echo _("(ss.t)")?></span></dt>
-    <dd id="spl_fade_out_<?php echo $this->item1; ?>" class="spl_fade_out">
+    <dd id="spl_fade_out_<?php echo $this->item1; ?>" class="spl_fade_out" data-fadeout="<?php echo $this->item1Url; ?>"
+    	data-cuein="<?php echo $this->cueIn1; ?>" data-cueout="<?php echo $this->cueOut1; ?>">
         <span contenteditable="true" class="spl_text_input"><?php echo $this->fadeOut; ?></span>
     </dd>
     <dd class="edit-error"></dd>
     <?php }
     if ($this->item2Type == 0) {?>
     <dt><? echo _("Fade in: "); ?><span class='spl_cue_hint'><? echo _("(ss.t)")?></span></dt>
-    <dd id="spl_fade_in_<?php echo $this->item2; ?>" class="spl_fade_in">
+    <dd id="spl_fade_in_<?php echo $this->item2; ?>" class="spl_fade_in" data-fadein="<?php echo $this->item2Url; ?>" data-offset="<?php echo $this->offset; ?>"
+    	data-cuein="<?php echo $this->cueIn2; ?>" data-cueout="<?php echo $this->cueOut2; ?>">
         <span contenteditable="true" class="spl_text_input"><?php echo $this->fadeIn; ?></span>
     </dd>
-     <dd class="edit-error"></dd>
+    <dd class="edit-error"></dd>
     <?php }?>
 </dl>
diff --git a/airtime_mvc/application/views/scripts/playlist/update.phtml b/airtime_mvc/application/views/scripts/playlist/update.phtml
index d1d85324c..b0d2b6151 100644
--- a/airtime_mvc/application/views/scripts/playlist/update.phtml
+++ b/airtime_mvc/application/views/scripts/playlist/update.phtml
@@ -93,7 +93,13 @@ if (($i < count($items) -1) && ($items[$i+1]['type'] == 0)) {
                 'item1Url' => $fileUrl,
             	'item2Url' => $nextFileUrl,
                 'fadeOut' => $items[$i]['fadeout'],
-                'fadeIn' => $items[$i+1]['fadein'])); ?>
+                'fadeIn' => $items[$i+1]['fadein'],
+            	'offset' => $items[$i]['trackSec'],
+                'cueIn1' => $items[$i]['cueInSec'],
+            	'cueOut1' => $items[$i]['cueOutSec'],
+            	'cueIn2' => $items[$i+1]['cueInSec'],
+            	'cueOut2' => $items[$i+1]['cueOutSec'])
+            ); ?>
         </div>
         <?php endif; ?>
         <?php if ($item['type'] == 2) {?>
diff --git a/airtime_mvc/public/js/airtime/library/spl.js b/airtime_mvc/public/js/airtime/library/spl.js
index 2aecd99a3..39cbbe7d2 100644
--- a/airtime_mvc/public/js/airtime/library/spl.js
+++ b/airtime_mvc/public/js/airtime/library/spl.js
@@ -1111,14 +1111,21 @@ var AIRTIME = (function(AIRTIME){
 	
 	mod.showFadesWaveform = function(e) {
 		var $el = $(e.target),
-			$parent = $el.parent(),
+			$parent = $el.parents("dl"),
+			$fadeOut = $parent.find(".spl_fade_out"),
+			$fadeIn = $parent.find(".spl_fade_in"),
 			$html = $($("#tmpl-pl-fades").html()),
 			tracks = [
 			    {
-			    	src: $parent.data("fadeout")
+			    	src: $fadeOut.data("fadeout"),
+			    	cuein: $fadeOut.data("cuein"),
+			    	cueout: $fadeOut.data("cueout")
 				},
 				{
-			    	src: $parent.data("fadein")
+			    	src: $fadeIn.data("fadein"),
+			    	start: $fadeIn.data("offset"),
+			    	cuein: $fadeIn.data("cuein"),
+			    	cueout: $fadeIn.data("cueout")
 				}
 			],
 			dim = AIRTIME.utilities.findViewportDimensions();
diff --git a/airtime_mvc/public/js/waveformplaylist/playlist.js b/airtime_mvc/public/js/waveformplaylist/playlist.js
index 92887c0c6..35927769e 100644
--- a/airtime_mvc/public/js/waveformplaylist/playlist.js
+++ b/airtime_mvc/public/js/waveformplaylist/playlist.js
@@ -304,6 +304,22 @@ PlaylistEditor.prototype.updateEditor = function() {
     } 
 };
 
+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,

From 0deaee4d0e5c506e3a8478cc5057d5e958b5989c Mon Sep 17 00:00:00 2001
From: Naomi <naomiaro@gmail.com>
Date: Wed, 24 Apr 2013 15:32:48 -0400
Subject: [PATCH 07/26] CC-2301 : disallowing movement of first track in fade
 editor.

---
 airtime_mvc/public/js/airtime/library/spl.js    |  3 ++-
 airtime_mvc/public/js/waveformplaylist/track.js | 14 ++++++++++++--
 2 files changed, 14 insertions(+), 3 deletions(-)

diff --git a/airtime_mvc/public/js/airtime/library/spl.js b/airtime_mvc/public/js/airtime/library/spl.js
index 39cbbe7d2..393571902 100644
--- a/airtime_mvc/public/js/airtime/library/spl.js
+++ b/airtime_mvc/public/js/airtime/library/spl.js
@@ -1119,7 +1119,8 @@ var AIRTIME = (function(AIRTIME){
 			    {
 			    	src: $fadeOut.data("fadeout"),
 			    	cuein: $fadeOut.data("cuein"),
-			    	cueout: $fadeOut.data("cueout")
+			    	cueout: $fadeOut.data("cueout"),
+			    	moveable: false
 				},
 				{
 			    	src: $fadeIn.data("fadein"),
diff --git a/airtime_mvc/public/js/waveformplaylist/track.js b/airtime_mvc/public/js/waveformplaylist/track.js
index a3123d0d7..f7140a0ee 100644
--- a/airtime_mvc/public/js/waveformplaylist/track.js
+++ b/airtime_mvc/public/js/waveformplaylist/track.js
@@ -64,7 +64,7 @@ TrackEditor.prototype.setWidth = function(width) {
     this.width = width;
 };
 
-TrackEditor.prototype.init = function(src, start, end, fades, cues) {
+TrackEditor.prototype.init = function(src, start, end, fades, cues, moveable) {
    
     makePublisher(this);
 
@@ -97,10 +97,15 @@ TrackEditor.prototype.init = function(src, start, end, fades, cues) {
     
     this.selectedArea = undefined; //selected area of track stored as inclusive buffer indices to the audio buffer.
     this.active = false;
+    this.canShift = moveable !== undefined ? moveable : true;
 
     this.container.classList.add("channel-wrapper");
     this.container.style.left = this.leftOffset;
 
+    if (this.canShift === false) {
+        this.container.style.position = "static";
+    }
+
     this.drawer.drawLoading();
 
     return this.container;
@@ -132,7 +137,8 @@ TrackEditor.prototype.loadTrack = function(track) {
         {
             cuein: track.cuein,
             cueout: track.cueout
-        }
+        },
+        track.moveable
     );
     this.loadBuffer(track.src);
 
@@ -248,6 +254,10 @@ TrackEditor.prototype.timeShift = function(e) {
         scroll = this.config.getTrackScroll(),
         scrollX = scroll.left;
 
+    if (this.canShift === false) {
+        return; //setting the 'left' css property has no effect, but don't want internal variable leftOffset to update.
+    }
+
     origX = editor.leftOffset / res;
     
     //dynamically put an event on the element.

From 8fea97d580ea7ef652ce040beb44c4a19357df12 Mon Sep 17 00:00:00 2001
From: Naomi <naomiaro@gmail.com>
Date: Thu, 25 Apr 2013 17:10:37 -0400
Subject: [PATCH 08/26] CC-2301 : fade editor now loads currently set fades.
 Fixing some bugs in the waveform editor.

---
 .../views/scripts/playlist/set-fade.phtml     |  6 +-
 airtime_mvc/public/js/airtime/library/spl.js  | 31 ++++--
 .../public/js/waveformplaylist/playlist.js    |  6 +-
 .../public/js/waveformplaylist/track.js       | 99 +++++++++++++------
 4 files changed, 104 insertions(+), 38 deletions(-)

diff --git a/airtime_mvc/application/views/scripts/playlist/set-fade.phtml b/airtime_mvc/application/views/scripts/playlist/set-fade.phtml
index 6a300eeee..ed5c94876 100644
--- a/airtime_mvc/application/views/scripts/playlist/set-fade.phtml
+++ b/airtime_mvc/application/views/scripts/playlist/set-fade.phtml
@@ -5,7 +5,8 @@
     <?php if ($this->item1Type == 0) {?>
     <dt><? echo _("Fade out: "); ?><span class='spl_cue_hint'><? echo _("(ss.t)")?></span></dt>
     <dd id="spl_fade_out_<?php echo $this->item1; ?>" class="spl_fade_out" data-fadeout="<?php echo $this->item1Url; ?>"
-    	data-cuein="<?php echo $this->cueIn1; ?>" data-cueout="<?php echo $this->cueOut1; ?>">
+    	data-cuein="<?php echo $this->cueIn1; ?>" data-cueout="<?php echo $this->cueOut1; ?>" data-length="<?php echo $this->fadeOut; ?>"
+    	data-type="logarithmic">
         <span contenteditable="true" class="spl_text_input"><?php echo $this->fadeOut; ?></span>
     </dd>
     <dd class="edit-error"></dd>
@@ -13,7 +14,8 @@
     if ($this->item2Type == 0) {?>
     <dt><? echo _("Fade in: "); ?><span class='spl_cue_hint'><? echo _("(ss.t)")?></span></dt>
     <dd id="spl_fade_in_<?php echo $this->item2; ?>" class="spl_fade_in" data-fadein="<?php echo $this->item2Url; ?>" data-offset="<?php echo $this->offset; ?>"
-    	data-cuein="<?php echo $this->cueIn2; ?>" data-cueout="<?php echo $this->cueOut2; ?>">
+    	data-cuein="<?php echo $this->cueIn2; ?>" data-cueout="<?php echo $this->cueOut2; ?>" data-length="<?php echo $this->fadeIn; ?>"
+    	data-type="logarithmic">
         <span contenteditable="true" class="spl_text_input"><?php echo $this->fadeIn; ?></span>
     </dd>
     <dd class="edit-error"></dd>
diff --git a/airtime_mvc/public/js/airtime/library/spl.js b/airtime_mvc/public/js/airtime/library/spl.js
index 393571902..29c5549ba 100644
--- a/airtime_mvc/public/js/airtime/library/spl.js
+++ b/airtime_mvc/public/js/airtime/library/spl.js
@@ -1120,16 +1120,29 @@ var AIRTIME = (function(AIRTIME){
 			    	src: $fadeOut.data("fadeout"),
 			    	cuein: $fadeOut.data("cuein"),
 			    	cueout: $fadeOut.data("cueout"),
-			    	moveable: false
+			    	moveable: false,
+			    	fades: [{
+			    	    shape: $fadeOut.data("type"),
+			    	    type: "FadeOut",
+			    	    end: $fadeOut.data("cueout") - $fadeOut.data("cuein"),
+			    	    start: $fadeOut.data("cueout") - $fadeOut.data("cuein") - $fadeOut.data("length")
+			    	}]
 				},
 				{
 			    	src: $fadeIn.data("fadein"),
 			    	start: $fadeIn.data("offset"),
 			    	cuein: $fadeIn.data("cuein"),
-			    	cueout: $fadeIn.data("cueout")
+			    	cueout: $fadeIn.data("cueout"),
+			    	fades: [{
+			    	    shape: $fadeIn.data("type"),
+			    	    type: "FadeIn",
+			    	    end: $fadeIn.data("length"),
+			    	    start: 0
+			    	}]
 				}
 			],
-			dim = AIRTIME.utilities.findViewportDimensions();
+			dim = AIRTIME.utilities.findViewportDimensions(),
+			playlistEditor;
 		
 		$html.dialog({
             modal: true,
@@ -1139,8 +1152,14 @@ var AIRTIME = (function(AIRTIME){
             width: dim.width - 100,
             height: dim.height - 100,
             buttons: [
-                //{text: "Submit", click: function() {doSomething()}},
-                {text: "Cancel", click: function() {$(this).dialog("close");}}
+                {text: "Save", click: function() {
+                	var json = playlistEditor.getJson();
+                	
+                	var x;
+                }},
+                {text: "Cancel", click: function() {
+                	$(this).dialog("close");
+                }}
             ],
             open: function (event, ui) {
             	
@@ -1154,7 +1173,7 @@ var AIRTIME = (function(AIRTIME){
         	        timeFormat: 'hh:mm:ss.u'
         	    });
         		
-        		var playlistEditor = new PlaylistEditor();
+        		playlistEditor = new PlaylistEditor();
         	    playlistEditor.setConfig(config);
         	    playlistEditor.init(tracks);
             }
diff --git a/airtime_mvc/public/js/waveformplaylist/playlist.js b/airtime_mvc/public/js/waveformplaylist/playlist.js
index 35927769e..0fa3a5284 100644
--- a/airtime_mvc/public/js/waveformplaylist/playlist.js
+++ b/airtime_mvc/public/js/waveformplaylist/playlist.js
@@ -289,7 +289,7 @@ PlaylistEditor.prototype.updateEditor = function() {
             playbackSec = cursorPos + elapsed;
             cursorPixel = Math.ceil(playbackSec * this.sampleRate / res);
             
-            for(i = 0, len = editors.length; i < len; i++) {
+            for (i = 0, len = editors.length; i < len; i++) {
                 editors[i].updateEditor(cursorPixel, start, end, highlighted);
             }
 
@@ -301,6 +301,10 @@ PlaylistEditor.prototype.updateEditor = function() {
     }
     else {
         clearInterval(this.interval);
+
+        for (i = 0, len = editors.length; i < len; i++) {
+            editors[i].updateEditor(-1, undefined, undefined, true);
+        }
     } 
 };
 
diff --git a/airtime_mvc/public/js/waveformplaylist/track.js b/airtime_mvc/public/js/waveformplaylist/track.js
index f7140a0ee..edb221858 100644
--- a/airtime_mvc/public/js/waveformplaylist/track.js
+++ b/airtime_mvc/public/js/waveformplaylist/track.js
@@ -89,8 +89,14 @@ TrackEditor.prototype.init = function(src, start, end, fades, cues, moveable) {
     this.prevStateEvents = {};
     this.setState(this.config.getState());
 
-    this.fades = fades || {};
-
+    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));
     }
@@ -102,10 +108,6 @@ TrackEditor.prototype.init = function(src, start, end, fades, cues, moveable) {
     this.container.classList.add("channel-wrapper");
     this.container.style.left = this.leftOffset;
 
-    if (this.canShift === false) {
-        this.container.style.position = "static";
-    }
-
     this.drawer.drawLoading();
 
     return this.container;
@@ -238,7 +240,8 @@ TrackEditor.prototype.deactivate = function() {
     this.active = false;
     this.selectedArea = undefined;
     this.container.classList.remove("active");
-    this.drawer.draw(-1, this.getPixelOffset());
+    //this.drawer.draw(-1, this.getPixelOffset());
+    this.updateEditor(-1, undefined, undefined, true);
 };
 
 /* start of state methods */
@@ -313,15 +316,20 @@ TrackEditor.prototype.getSelectedArea = function() {
 };
 
 /*
-    start, end in samples.
+    start, end in samples. (relative to cuein/cueout)
 */
 TrackEditor.prototype.adjustSelectedArea = function(start, end) {
-    var buffer = this.getBuffer();
+    var buffer = this.getBuffer(),
+        cues = this.cues;
 
-    if (start < 0) {
+    if (start === undefined || start < 0) {
         start = 0;
     }
 
+    if (end === undefined) {
+        end = cues.cueout - cues.cuein;
+    }
+
     if (end > buffer.length - 1) {
         end = buffer.length - 1;
     }
@@ -375,8 +383,8 @@ TrackEditor.prototype.setSelectedArea = function(start, end, shiftKey) {
         right = end;
     }
 
-    sampLeft = left === undefined ? 0 : this.pixelsToSamples(left);
-    sampRight = right === undefined ? buffer.length - 1 : this.pixelsToSamples(right);
+    sampLeft = left === undefined ? undefined : this.pixelsToSamples(left);
+    sampRight = right === undefined ? undefined : this.pixelsToSamples(right);
 
     this.prevSelectedArea = this.selectedArea;
     this.selectedArea = this.adjustSelectedArea(sampLeft, sampRight);
@@ -392,21 +400,40 @@ 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,
-        //scroll = this.config.getTrackScroll(),
-        //scrollX = scroll.left,
-        //startX = scrollX + (e.layerX || e.offsetX), //relative to e.target (want the canvas).
-        //prevX = scrollX + (e.layerX || e.offsetX),
         startX = e.layerX || e.offsetX, //relative to e.target (want the canvas).
         prevX = e.layerX || e.offsetX,
         offset = this.leftOffset,
-        startTime;
+        startTime,
+        layerOffset;
 
-    if (e.target.tagName !== "CANVAS") {
+    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);
@@ -416,8 +443,7 @@ TrackEditor.prototype.selectStart = function(e) {
 
     //dynamically put an event on the element.
     el.onmousemove = function(e) {
-        var currentX = e.layerX || e.offsetX,
-            //currentX = scrollX + (e.layerX || e.offsetX),
+        var currentX = layerOffset + (e.layerX || e.offsetX),
             delta = currentX - prevX,
             minX = Math.min(prevX, currentX, startX),
             maxX = Math.max(prevX, currentX, startX),
@@ -443,7 +469,7 @@ TrackEditor.prototype.selectStart = function(e) {
         prevX = currentX;
     };
     el.onmouseup = function(e) {
-        var endX = e.layerX || e.offsetX,
+        var endX = layerOffset + (e.layerX || e.offsetX),
             minX, maxX,
             startTime, endTime;
 
@@ -479,11 +505,14 @@ TrackEditor.prototype.selectCursorPos = function(e) {
         startX = e.layerX || e.offsetX, //relative to e.target (want the canvas).
         offset = this.leftOffset,
         startTime, 
-        endTime;
+        endTime,
+        layerOffset;
 
-    if (e.target.tagName !== "CANVAS") {
+    layerOffset = this.findLayerOffset(e);
+    if (layerOffset < 0) {
         return;
     }
+    startX = startX + layerOffset;
 
     editor.setSelectedArea(startX, startX);
     startTime = editor.samplesToSeconds(offset + editor.selectedArea.start);
@@ -501,11 +530,14 @@ TrackEditor.prototype.selectFadeIn = function(e) {
         startX = e.layerX || e.offsetX, //relative to e.target (want the canvas).
         offset = this.leftOffset,
         startTime, 
-        endTime;
+        endTime,
+        layerOffset;
 
-    if (e.target.tagName !== "CANVAS") {
+    layerOffset = this.findLayerOffset(e);
+    if (layerOffset < 0) {
         return;
     }
+    startX = startX + layerOffset;
 
     editor.setSelectedArea(undefined, startX);
     startTime = editor.samplesToSeconds(offset + editor.selectedArea.start);
@@ -522,11 +554,14 @@ TrackEditor.prototype.selectFadeOut = function(e) {
         startX = e.layerX || e.offsetX, //relative to e.target (want the canvas).
         offset = this.leftOffset,
         startTime,
-        endTime;
+        endTime,
+        layerOffset;
 
-    if (e.target.tagName !== "CANVAS") {
+    layerOffset = this.findLayerOffset(e);
+    if (layerOffset < 0) {
         return;
     }
+    startX = startX + layerOffset;
 
     editor.setSelectedArea(startX, undefined);
     startTime = editor.samplesToSeconds(offset + editor.selectedArea.start);
@@ -778,12 +813,18 @@ TrackEditor.prototype.updateEditor = function(cursorPos, start, end, highlighted
 
 TrackEditor.prototype.getTrackDetails = function() {
     var d,
-        cues = this.cues;
+        cues = this.cues,
+        fades = [],
+        id;
+
+    for (id in this.fades) {
+        fades.push(this.fades[id]);
+    }
 
     d = {
         start: this.startTime,
         end: this.endTime,
-        fades: this.fades,
+        fades: fades,
         src: this.src,
         cuein: this.samplesToSeconds(cues.cuein),
         cueout: this.samplesToSeconds(cues.cueout)

From 2b22828e85c916d84d12b3f59760ec43da108792 Mon Sep 17 00:00:00 2001
From: Naomi <naomiaro@gmail.com>
Date: Thu, 25 Apr 2013 17:48:35 -0400
Subject: [PATCH 09/26] CC-2301 : destorying dialogs instead of just closing to
 free up resources.

---
 airtime_mvc/public/js/airtime/library/spl.js | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/airtime_mvc/public/js/airtime/library/spl.js b/airtime_mvc/public/js/airtime/library/spl.js
index 29c5549ba..a34ac500b 100644
--- a/airtime_mvc/public/js/airtime/library/spl.js
+++ b/airtime_mvc/public/js/airtime/library/spl.js
@@ -1158,7 +1158,7 @@ var AIRTIME = (function(AIRTIME){
                 	var x;
                 }},
                 {text: "Cancel", click: function() {
-                	$(this).dialog("close");
+                	$(this).dialog("destroy");
                 }}
             ],
             open: function (event, ui) {
@@ -1224,7 +1224,7 @@ var AIRTIME = (function(AIRTIME){
                 	changeCues($html, id, cueIn, cueOut);
                 }},
                 {text: "Cancel", click: function() {
-                	$(this).dialog("close");
+                	$(this).dialog("destroy");
                 }}
             ],
             open: function (event, ui) {

From 628af95325dc418c6bafe7fa9dfde32b392ed277 Mon Sep 17 00:00:00 2001
From: Naomi <naomiaro@gmail.com>
Date: Fri, 26 Apr 2013 12:38:13 -0400
Subject: [PATCH 10/26] CC-2301 : needed to adjust cueout if length information
 is inaccurate.

---
 airtime_mvc/public/js/waveformplaylist/track.js | 4 ++++
 1 file changed, 4 insertions(+)

diff --git a/airtime_mvc/public/js/waveformplaylist/track.js b/airtime_mvc/public/js/waveformplaylist/track.js
index edb221858..66f0ee644 100644
--- a/airtime_mvc/public/js/waveformplaylist/track.js
+++ b/airtime_mvc/public/js/waveformplaylist/track.js
@@ -192,6 +192,10 @@ TrackEditor.prototype.onTrackLoad = function(buffer) {
     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);

From a4731d4af2ba57220dc667932f9bf9c3dbb2f831 Mon Sep 17 00:00:00 2001
From: Naomi <naomiaro@gmail.com>
Date: Fri, 26 Apr 2013 15:57:02 -0400
Subject: [PATCH 11/26] CC-2301 : automatically updating the fadein/fadeout for
 a track in the fadein/fadeout states. Can enable states on a per track basis.

---
 .../application/layouts/scripts/layout.phtml  |  14 --
 airtime_mvc/public/js/airtime/library/spl.js  |  10 +-
 .../public/js/waveformplaylist/config.js      |   9 +
 .../public/js/waveformplaylist/playlist.js    |   5 +-
 .../public/js/waveformplaylist/track.js       | 231 ++++++++++--------
 .../js/waveformplaylist/track_render.js       |  16 ++
 6 files changed, 162 insertions(+), 123 deletions(-)

diff --git a/airtime_mvc/application/layouts/scripts/layout.phtml b/airtime_mvc/application/layouts/scripts/layout.phtml
index 1cb0da978..b72305687 100644
--- a/airtime_mvc/application/layouts/scripts/layout.phtml
+++ b/airtime_mvc/application/layouts/scripts/layout.phtml
@@ -72,20 +72,6 @@
 	<span class="btn_fadeout ui-state-default" data-state="fadeout">Fade Out</span>
     <span class="btn_shift ui-state-default" data-state="shift">Shift</span>
   </div>
-  <div id="btns_fade" class="btns_fade">
-    <div class="fade-in-ctrls">
-      <a class="disabled" data-type="FadeIn" data-shape="linear">Linear</a>
-      <a class="disabled" data-type="FadeIn" data-shape="logarithmic">Logarithmic</a>
-      <a class="disabled" data-type="FadeIn" data-shape="sCurve">S-Curve</a>
-      <a class="disabled" data-type="FadeIn" data-shape="exponential">Exponential</a>
-    </div>
-    <div class="fade-out-ctrls">
-      <a class="disabled" data-type="FadeOut" data-shape="linear">Linear</a>
-      <a class="disabled" data-type="FadeOut" data-shape="logarithmic">Logarithmic</a>
-      <a class="disabled" data-type="FadeOut" data-shape="sCurve">S-Curve</a>
-      <a class="disabled" data-type="FadeOut" data-shape="exponential">Exponential</a>
-    </div>
-  </div>
 </div>
 </script>
 </body>
diff --git a/airtime_mvc/public/js/airtime/library/spl.js b/airtime_mvc/public/js/airtime/library/spl.js
index a34ac500b..9c8f38b22 100644
--- a/airtime_mvc/public/js/airtime/library/spl.js
+++ b/airtime_mvc/public/js/airtime/library/spl.js
@@ -1126,7 +1126,10 @@ var AIRTIME = (function(AIRTIME){
 			    	    type: "FadeOut",
 			    	    end: $fadeOut.data("cueout") - $fadeOut.data("cuein"),
 			    	    start: $fadeOut.data("cueout") - $fadeOut.data("cuein") - $fadeOut.data("length")
-			    	}]
+			    	}],
+			    	states: {
+		                'fadein': false
+		            }
 				},
 				{
 			    	src: $fadeIn.data("fadein"),
@@ -1138,7 +1141,10 @@ var AIRTIME = (function(AIRTIME){
 			    	    type: "FadeIn",
 			    	    end: $fadeIn.data("length"),
 			    	    start: 0
-			    	}]
+			    	}],
+			    	states: {
+		                'fadeout': false
+		            }
 				}
 			],
 			dim = AIRTIME.utilities.findViewportDimensions(),
diff --git a/airtime_mvc/public/js/waveformplaylist/config.js b/airtime_mvc/public/js/waveformplaylist/config.js
index 0aec0b339..7b0ae3ecf 100644
--- a/airtime_mvc/public/js/waveformplaylist/config.js
+++ b/airtime_mvc/public/js/waveformplaylist/config.js
@@ -15,6 +15,7 @@ var Config = function(params) {
             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.
 
@@ -64,6 +65,10 @@ var Config = function(params) {
             return params.timescale;
         };
 
+        that.getFadeType = function getFadeType() {
+            return params.fadeType;
+        };
+
         that.isDisplayMono = function isDisplayMono() {
             return params.mono;
         };
@@ -141,6 +146,10 @@ var Config = function(params) {
             params.timeFormat = format;
         };
 
+        that.setFadeType = function setFadeType(type) {
+            params.fadeType = type;
+        };
+
         that.setDisplayMono = function setDisplayMono(bool) {
             params.mono = bool;
         };
diff --git a/airtime_mvc/public/js/waveformplaylist/playlist.js b/airtime_mvc/public/js/waveformplaylist/playlist.js
index 0fa3a5284..ff47e8e75 100644
--- a/airtime_mvc/public/js/waveformplaylist/playlist.js
+++ b/airtime_mvc/public/js/waveformplaylist/playlist.js
@@ -49,7 +49,6 @@ PlaylistEditor.prototype.init = function(tracks) {
         this.trackEditors.push(trackEditor);
         fragment.appendChild(trackElem);
 
-        audioControls.on("changestate", "onStateChange", trackEditor);
         audioControls.on("trackedit", "onTrackEdit", trackEditor);
         audioControls.on("changeresolution", "onResolutionChange", trackEditor);
 
@@ -127,10 +126,12 @@ PlaylistEditor.prototype.onStateChange = function() {
         editors = this.trackEditors,
         i,
         len,
-        editor;
+        editor,
+        state = this.config.getState();
 
     for(i = 0, len = editors.length; i < len; i++) {
         editors[i].deactivate();
+        editors[i].setState(state);
     }
 };
 
diff --git a/airtime_mvc/public/js/waveformplaylist/track.js b/airtime_mvc/public/js/waveformplaylist/track.js
index 66f0ee644..1c6d898e1 100644
--- a/airtime_mvc/public/js/waveformplaylist/track.js
+++ b/airtime_mvc/public/js/waveformplaylist/track.js
@@ -4,55 +4,55 @@ var TrackEditor = function() {
 
 };
 
-TrackEditor.prototype.states = {
-    cursor: {
-        events: {
-            mousedown: "selectCursorPos"
-        },
+TrackEditor.prototype.classes = {
+    "cursor": [
+        "state-select"
+    ],
 
-        classes: [
-            "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: {
-        events: {
-            mousedown: "selectStart"
-        },
-
-        classes: [
-            "state-select"
-        ]
+    "select": {
+        "mousedown": "selectStart"
     },
 
-    fadein: {
-        events: {
-            mousedown: "selectFadeIn"
-        },
-
-        classes: [
-            "state-select"
-        ]
+    "fadein": {
+        "mousedown": "selectFadeIn"
     },
 
-    fadeout: {
-        events: {
-            mousedown: "selectFadeOut"
-        },
-
-        classes: [
-            "state-select"
-        ]
+    "fadeout": {
+        "mousedown": "selectFadeOut"
     },
-    
-    shift: {
-        events: {
-            mousedown: "timeShift"
-        },
 
-        classes: [
-            "state-shift"
-        ]
+    "shift": {
+        "mousedown": "timeShift"
     }
 };
 
@@ -64,7 +64,22 @@ TrackEditor.prototype.setWidth = function(width) {
     this.width = width;
 };
 
-TrackEditor.prototype.init = function(src, start, end, fades, cues, moveable) {
+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);
 
@@ -103,8 +118,7 @@ TrackEditor.prototype.init = function(src, start, end, fades, cues, moveable) {
     
     this.selectedArea = undefined; //selected area of track stored as inclusive buffer indices to the audio buffer.
     this.active = false;
-    this.canShift = moveable !== undefined ? moveable : true;
-
+    
     this.container.classList.add("channel-wrapper");
     this.container.style.left = this.leftOffset;
 
@@ -140,7 +154,7 @@ TrackEditor.prototype.loadTrack = function(track) {
             cuein: track.cuein,
             cueout: track.cueout
         },
-        track.moveable
+        track.states
     );
     this.loadBuffer(track.src);
 
@@ -244,7 +258,6 @@ TrackEditor.prototype.deactivate = function() {
     this.active = false;
     this.selectedArea = undefined;
     this.container.classList.remove("active");
-    //this.drawer.draw(-1, this.getPixelOffset());
     this.updateEditor(-1, undefined, undefined, true);
 };
 
@@ -261,10 +274,6 @@ TrackEditor.prototype.timeShift = function(e) {
         scroll = this.config.getTrackScroll(),
         scrollX = scroll.left;
 
-    if (this.canShift === false) {
-        return; //setting the 'left' css property has no effect, but don't want internal variable leftOffset to update.
-    }
-
     origX = editor.leftOffset / res;
     
     //dynamically put an event on the element.
@@ -530,12 +539,10 @@ TrackEditor.prototype.selectCursorPos = function(e) {
 };
 
 TrackEditor.prototype.selectFadeIn = function(e) {
-    var editor = this,
-        startX = e.layerX || e.offsetX, //relative to e.target (want the canvas).
-        offset = this.leftOffset,
-        startTime, 
-        endTime,
-        layerOffset;
+    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) {
@@ -543,23 +550,16 @@ TrackEditor.prototype.selectFadeIn = function(e) {
     }
     startX = startX + layerOffset;
 
-    editor.setSelectedArea(undefined, startX);
-    startTime = editor.samplesToSeconds(offset + editor.selectedArea.start);
-    endTime = editor.samplesToSeconds(offset + editor.selectedArea.end);
-
-    editor.updateEditor(-1, undefined, undefined, true);
-    editor.notifySelectUpdate(startTime, endTime);
-
-    editor.activateAudioSelection();
+    this.setSelectedArea(undefined, startX);
+    this.removeFadeType(FADETYPE);
+    this.createFade(FADETYPE, shape);
 };
 
 TrackEditor.prototype.selectFadeOut = function(e) {
-    var editor = this,
-        startX = e.layerX || e.offsetX, //relative to e.target (want the canvas).
-        offset = this.leftOffset,
-        startTime,
-        endTime,
-        layerOffset;
+    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) {
@@ -567,14 +567,9 @@ TrackEditor.prototype.selectFadeOut = function(e) {
     }
     startX = startX + layerOffset;
 
-    editor.setSelectedArea(startX, undefined);
-    startTime = editor.samplesToSeconds(offset + editor.selectedArea.start);
-    endTime = editor.samplesToSeconds(offset + editor.selectedArea.end);
-
-    editor.updateEditor(-1, undefined, undefined, true);
-    editor.notifySelectUpdate(startTime, endTime);
-
-    editor.activateAudioSelection();
+    this.setSelectedArea(startX, undefined);
+    this.removeFadeType(FADETYPE);
+    this.createFade(FADETYPE, shape);
 };
 
 /* end of state methods */
@@ -594,6 +589,21 @@ TrackEditor.prototype.saveFade = function(id, type, shape, start, end) {
 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);
+        }
+    }
 };
 
 /*
@@ -645,9 +655,8 @@ TrackEditor.prototype.onTrackEdit = function(event) {
     }
 };
 
-TrackEditor.prototype.onCreateFade = function(args) {
+TrackEditor.prototype.createFade = function(type, shape) {
     var selected = this.selectedArea,
-        pixelOffset = this.getPixelOffset(),
         start = this.samplesToPixels(selected.start),
         end = this.samplesToPixels(selected.end),
         startTime = this.samplesToSeconds(selected.start),
@@ -655,10 +664,13 @@ TrackEditor.prototype.onCreateFade = function(args) {
         id = this.getFadeId();
 
     this.resetCursor();
-    this.saveFade(id, args.type, args.shape, startTime, endTime);
-    this.drawer.draw(-1, pixelOffset);
-    this.drawer.drawFade(id, args.type, args.shape, start, end);
+    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();
 };
 
@@ -692,8 +704,10 @@ TrackEditor.prototype.onRemoveAudio = function() {
 
 TrackEditor.prototype.setState = function(state) {
     var that = this,
-        stateEvents = this.states[state].events,
-        stateClasses = this.states[state].classes,
+        stateEvents = this.events[state],
+        stateClasses = this.classes[state],
+        disabledClasses = this.classes['disabled'],
+        enabledStates = this.enabledStates,
         container = this.container,
         prevState = this.currentState,
         prevStateClasses,
@@ -702,38 +716,45 @@ TrackEditor.prototype.setState = function(state) {
         i, len;
 
     if (prevState) {
-        prevStateClasses = this.states[prevState].classes;
+        prevStateClasses = this.classes[prevState];
        
-        for (event in prevStateEvents) {
-            container.removeEventListener(event, prevStateEvents[event]);
-        }
-        this.prevStateEvents = {};
+        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]);
+            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]);
+            }
+        }  
     }
 
-    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++) {
+    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.onStateChange = function() {
-    var state = this.config.getState();
-
-    this.setState(state);
-};
-
 TrackEditor.prototype.onResolutionChange = function(res) {
     var selected = this.selectedArea;
 
diff --git a/airtime_mvc/public/js/waveformplaylist/track_render.js b/airtime_mvc/public/js/waveformplaylist/track_render.js
index 611dfcbc0..70736dab2 100644
--- a/airtime_mvc/public/js/waveformplaylist/track_render.js
+++ b/airtime_mvc/public/js/waveformplaylist/track_render.js
@@ -367,6 +367,22 @@ WaveformDrawer.prototype.drawFadeCurve = function(ctx, shape, type, width) {
     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,

From 0169c0172405c3c57cd995522c6f6e6f7efeb0b5 Mon Sep 17 00:00:00 2001
From: Naomi <naomiaro@gmail.com>
Date: Fri, 26 Apr 2013 16:23:46 -0400
Subject: [PATCH 12/26] CC-2301 : updating waveform code for states

---
 airtime_mvc/public/js/waveformplaylist/track.js | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/airtime_mvc/public/js/waveformplaylist/track.js b/airtime_mvc/public/js/waveformplaylist/track.js
index 1c6d898e1..2e0969165 100644
--- a/airtime_mvc/public/js/waveformplaylist/track.js
+++ b/airtime_mvc/public/js/waveformplaylist/track.js
@@ -154,7 +154,7 @@ TrackEditor.prototype.loadTrack = function(track) {
             cuein: track.cuein,
             cueout: track.cueout
         },
-        track.states
+        track.states || {}
     );
     this.loadBuffer(track.src);
 

From ef100f89f1913c80043c59c6f3e9602f810b02f6 Mon Sep 17 00:00:00 2001
From: Naomi <naomiaro@gmail.com>
Date: Fri, 26 Apr 2013 17:54:37 -0400
Subject: [PATCH 13/26] CC-2301 : not drawing fades of length zero.

---
 .../js/waveformplaylist/track_render.js       | 36 ++++++++++---------
 1 file changed, 20 insertions(+), 16 deletions(-)

diff --git a/airtime_mvc/public/js/waveformplaylist/track_render.js b/airtime_mvc/public/js/waveformplaylist/track_render.js
index 70736dab2..54c87945b 100644
--- a/airtime_mvc/public/js/waveformplaylist/track_render.js
+++ b/airtime_mvc/public/js/waveformplaylist/track_render.js
@@ -395,26 +395,30 @@ WaveformDrawer.prototype.drawFade = function(id, type, shape, start, end) {
         ctx,
         tmpCtx;
 
-        width = ~~(end - start + 1);
-        left = start;
+    if ((end - start) === 0) {
+        return;
+    } 
 
-        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";
+    width = ~~(end - start + 1);
+    left = start;
 
-        canv = document.createElement("canvas");
-        canv.setAttribute('width', width);
-        canv.setAttribute('height', this.height);
-        ctx = canv.getContext('2d');
+    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";
 
-        this.drawFadeCurve(ctx, shape, type, width);
+    canv = document.createElement("canvas");
+    canv.setAttribute('width', width);
+    canv.setAttribute('height', this.height);
+    ctx = canv.getContext('2d');
 
-        div.appendChild(canv);
-        fragment.appendChild(div);   
+    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);

From 410d298272b4a8158627e2c1824ec740dab2c60b Mon Sep 17 00:00:00 2001
From: Naomi <naomiaro@gmail.com>
Date: Mon, 29 Apr 2013 16:55:08 -0400
Subject: [PATCH 14/26] CC-2301 : creating an offset column to help create
 crossfades in the playlist. 	changing default fade to be a separate default
 fade in/out

---
 .../controllers/PreferenceController.php      |   3 +-
 .../application/forms/GeneralPreferences.php  |  35 ++++--
 airtime_mvc/application/models/Block.php      |  15 +--
 airtime_mvc/application/models/Playlist.php   |   9 +-
 airtime_mvc/application/models/Preference.php |  34 ++++++
 .../map/CcPlaylistcontentsTableMap.php        |   1 +
 .../airtime/om/BaseCcPlaylistcontents.php     | 102 +++++++++++++-----
 .../airtime/om/BaseCcPlaylistcontentsPeer.php |  31 +++---
 .../om/BaseCcPlaylistcontentsQuery.php        |  35 ++++++
 .../scripts/form/preferences_general.phtml    |  25 +++--
 .../views/scripts/playlist/set-fade.phtml     |   2 +-
 airtime_mvc/build/schema.xml                  |   2 +-
 airtime_mvc/build/sql/schema.sql              |   1 +
 airtime_mvc/public/js/airtime/library/spl.js  |  85 ++++++++-------
 14 files changed, 273 insertions(+), 107 deletions(-)

diff --git a/airtime_mvc/application/controllers/PreferenceController.php b/airtime_mvc/application/controllers/PreferenceController.php
index 77f615528..fba359458 100644
--- a/airtime_mvc/application/controllers/PreferenceController.php
+++ b/airtime_mvc/application/controllers/PreferenceController.php
@@ -43,7 +43,8 @@ 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::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..6e02a3200 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',
@@ -29,10 +27,10 @@ class Application_Form_GeneralPreferences extends Zend_Form_SubForm
             )
         ));
 
-        //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 +40,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/models/Block.php b/airtime_mvc/application/models/Block.php
index b364e5eb5..e8db4ee12 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();
@@ -234,6 +229,12 @@ 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']);
+            
             $offset += $clipSec;
             $offset_cliplength = Application_Common_DateHelper::secondsToPlaylistTime($offset);
 
diff --git a/airtime_mvc/application/models/Playlist.php b/airtime_mvc/application/models/Playlist.php
index 0e2e9b4fe..764a8695c 100644
--- a/airtime_mvc/application/models/Playlist.php
+++ b/airtime_mvc/application/models/Playlist.php
@@ -60,13 +60,8 @@ 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->con = isset($con) ? $con : Propel::getConnection(CcPlaylistPeer::DATABASE_NAME);
         $this->id = $this->pl->getDbId();
diff --git a/airtime_mvc/application/models/Preference.php b/airtime_mvc/application/models/Preference.php
index 2af97d193..dbb3146dd 100644
--- a/airtime_mvc/application/models/Preference.php
+++ b/airtime_mvc/application/models/Preference.php
@@ -194,6 +194,40 @@ class Application_Model_Preference
             return new DateTime($date, new DateTimeZone("UTC"));
         }
     }
+    
+    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/airtime/map/CcPlaylistcontentsTableMap.php b/airtime_mvc/application/models/airtime/map/CcPlaylistcontentsTableMap.php
index 68fde5847..af6eddd10 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('OFFSET', 'DbOffset', '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/BaseCcPlaylistcontents.php b/airtime_mvc/application/models/airtime/om/BaseCcPlaylistcontents.php
index f8630b15b..f7d66a9c6 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 offset field.
+	 * Note: this column has a database default value of: 0
+	 * @var        double
+	 */
+	protected $offset;
+
 	/**
 	 * 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->offset = 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 [offset] column value.
+	 * 
+	 * @return     double
+	 */
+	public function getDbOffset()
+	{
+		return $this->offset;
+	}
+
 	/**
 	 * Get the [cliplength] column value.
 	 * 
@@ -478,6 +496,26 @@ abstract class BaseCcPlaylistcontents extends BaseObject  implements Persistent
 		return $this;
 	} // setDbPosition()
 
+	/**
+	 * Set the value of [offset] column.
+	 * 
+	 * @param      double $v new value
+	 * @return     CcPlaylistcontents The current object (for fluent API support)
+	 */
+	public function setDbOffset($v)
+	{
+		if ($v !== null) {
+			$v = (double) $v;
+		}
+
+		if ($this->offset !== $v || $this->isNew()) {
+			$this->offset = $v;
+			$this->modifiedColumns[] = CcPlaylistcontentsPeer::OFFSET;
+		}
+
+		return $this;
+	} // setDbOffset()
+
 	/**
 	 * Set the value of [cliplength] column.
 	 * 
@@ -652,6 +690,10 @@ abstract class BaseCcPlaylistcontents extends BaseObject  implements Persistent
 				return false;
 			}
 
+			if ($this->offset !== 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->offset = ($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->getDbOffset();
 				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->getDbOffset(),
+			$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->setDbOffset($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->setDbOffset($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::OFFSET)) $criteria->add(CcPlaylistcontentsPeer::OFFSET, $this->offset);
 		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->setDbOffset($this->offset);
 		$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->offset = 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..6645977ea 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 OFFSET field */
+	const OFFSET = 'cc_playlistcontents.OFFSET';
+
 	/** 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', 'DbOffset', 'DbCliplength', 'DbCuein', 'DbCueout', 'DbFadein', 'DbFadeout', ),
+		BasePeer::TYPE_STUDLYPHPNAME => array ('dbId', 'dbPlaylistId', 'dbFileId', 'dbBlockId', 'dbStreamId', 'dbType', 'dbPosition', 'dbOffset', '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::OFFSET, 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', 'OFFSET', 'CLIPLENGTH', 'CUEIN', 'CUEOUT', 'FADEIN', 'FADEOUT', ),
+		BasePeer::TYPE_FIELDNAME => array ('id', 'playlist_id', 'file_id', 'block_id', 'stream_id', 'type', 'position', 'offset', '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, 'DbOffset' => 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, 'dbOffset' => 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::OFFSET => 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, 'OFFSET' => 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, 'offset' => 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::OFFSET);
 			$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 . '.OFFSET');
 			$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..d70d1dd17 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 orderByDbOffset($order = Criteria::ASC) Order by the offset 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 groupByDbOffset() Group by the offset 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 findOneByDbOffset(double $offset) Return the first CcPlaylistcontents filtered by the offset 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 findByDbOffset(double $offset) Return CcPlaylistcontents objects filtered by the offset 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 offset column
+	 * 
+	 * @param     double|array $dbOffset 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 filterByDbOffset($dbOffset = null, $comparison = null)
+	{
+		if (is_array($dbOffset)) {
+			$useMinMax = false;
+			if (isset($dbOffset['min'])) {
+				$this->addUsingAlias(CcPlaylistcontentsPeer::OFFSET, $dbOffset['min'], Criteria::GREATER_EQUAL);
+				$useMinMax = true;
+			}
+			if (isset($dbOffset['max'])) {
+				$this->addUsingAlias(CcPlaylistcontentsPeer::OFFSET, $dbOffset['max'], Criteria::LESS_EQUAL);
+				$useMinMax = true;
+			}
+			if ($useMinMax) {
+				return $this;
+			}
+			if (null === $comparison) {
+				$comparison = Criteria::IN;
+			}
+		}
+		return $this->addUsingAlias(CcPlaylistcontentsPeer::OFFSET, $dbOffset, $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..85582411c 100644
--- a/airtime_mvc/application/views/scripts/form/preferences_general.phtml
+++ b/airtime_mvc/application/views/scripts/form/preferences_general.phtml
@@ -14,14 +14,27 @@
                 </ul>
             <?php endif; ?>
         </dd>
-        <dt id="stationDefaultFade-label" class="block-display">
-            <label class="optional" for="stationDefaultFade"><?php echo $this->element->getElement('stationDefaultFade')->getLabel() ?></label>
+        <dt id="stationDefaultFadeIn-label" class="block-display">
+            <label class="optional" for="stationDefaultFadeIn"><?php echo $this->element->getElement('stationDefaultFadeIn')->getLabel() ?></label>
         </dt>
-        <dd id="stationDefaultFade-element" class="block-display">
-            <?php echo $this->element->getElement('stationDefaultFade') ?>
-            <?php if($this->element->getElement('stationDefaultFade')->hasErrors()) : ?>
+        <dd id="stationDefaultFadeIn-element" class="block-display">
+            <?php echo $this->element->getElement('stationDefaultFadeIn') ?>
+            <?php if($this->element->getElement('stationDefaultFadeIn')->hasErrors()) : ?>
                 <ul class='errors'>
-                    <?php foreach($this->element->getElement('stationDefaultFade')->getMessages() as $error): ?>
+                    <?php foreach($this->element->getElement('stationDefaultFadeIn')->getMessages() as $error): ?>
+                        <li><?php echo $error; ?></li>
+                    <?php endforeach; ?>
+                </ul>
+            <?php endif; ?>
+        </dd>
+        <dt id="stationDefaultFadeOut-label" class="block-display">
+            <label class="optional" for="stationDefaultFadeOut"><?php echo $this->element->getElement('stationDefaultFadeOut')->getLabel() ?></label>
+        </dt>
+        <dd id="stationDefaultFadeOut-element" class="block-display">
+            <?php echo $this->element->getElement('stationDefaultFadeOut') ?>
+            <?php if($this->element->getElement('stationDefaultFadeOut')->hasErrors()) : ?>
+                <ul class='errors'>
+                    <?php foreach($this->element->getElement('stationDefaultFadeOut')->getMessages() as $error): ?>
                         <li><?php echo $error; ?></li>
                     <?php endforeach; ?>
                 </ul>
diff --git a/airtime_mvc/application/views/scripts/playlist/set-fade.phtml b/airtime_mvc/application/views/scripts/playlist/set-fade.phtml
index ed5c94876..4c6f745af 100644
--- a/airtime_mvc/application/views/scripts/playlist/set-fade.phtml
+++ b/airtime_mvc/application/views/scripts/playlist/set-fade.phtml
@@ -13,7 +13,7 @@
     <?php }
     if ($this->item2Type == 0) {?>
     <dt><? echo _("Fade in: "); ?><span class='spl_cue_hint'><? echo _("(ss.t)")?></span></dt>
-    <dd id="spl_fade_in_<?php echo $this->item2; ?>" class="spl_fade_in" data-fadein="<?php echo $this->item2Url; ?>" data-offset="<?php echo $this->offset; ?>"
+    <dd id="spl_fade_in_<?php echo $this->item2; ?>" class="spl_fade_in" data-fadein="<?php echo $this->item2Url; ?>" data-offset="<?php if ($this->item1Type == 0) { echo $this->offset; } else { echo 0; } ?>"
     	data-cuein="<?php echo $this->cueIn2; ?>" data-cueout="<?php echo $this->cueOut2; ?>" data-length="<?php echo $this->fadeIn; ?>"
     	data-type="logarithmic">
         <span contenteditable="true" class="spl_text_input"><?php echo $this->fadeIn; ?></span>
diff --git a/airtime_mvc/build/schema.xml b/airtime_mvc/build/schema.xml
index 6ecde0276..d8bb3c4b1 100644
--- a/airtime_mvc/build/schema.xml
+++ b/airtime_mvc/build/schema.xml
@@ -227,7 +227,7 @@
     -->
     <column name="type" phpName="DbType" type="SMALLINT" required="true" default="0"/>
     <column name="position" phpName="DbPosition" type="INTEGER" required="false"/>
-    <column name="starts" phpName="DbStarts" type="TIMESTAMP" required="true"/>
+    <column name="offset" phpName="DbOffset" type="REAL" required="true" default="0"/>
     <column name="cliplength" phpName="DbCliplength" type="VARCHAR" sqlType="interval" required="false" defaultValue="00:00:00"/>
     <column name="cuein" phpName="DbCuein" type="VARCHAR" sqlType="interval" required="false" defaultValue="00:00:00"/>
     <column name="cueout" phpName="DbCueout" type="VARCHAR" sqlType="interval" required="false" defaultValue="00:00:00"/>
diff --git a/airtime_mvc/build/sql/schema.sql b/airtime_mvc/build/sql/schema.sql
index cf73470b9..1ed734aa1 100644
--- a/airtime_mvc/build/sql/schema.sql
+++ b/airtime_mvc/build/sql/schema.sql
@@ -296,6 +296,7 @@ CREATE TABLE "cc_playlistcontents"
 	"stream_id" INTEGER,
 	"type" INT2 default 0 NOT NULL,
 	"position" INTEGER,
+	"offset" 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/js/airtime/library/spl.js b/airtime_mvc/public/js/airtime/library/spl.js
index 9c8f38b22..f3a7fc8c5 100644
--- a/airtime_mvc/public/js/airtime/library/spl.js
+++ b/airtime_mvc/public/js/airtime/library/spl.js
@@ -1115,41 +1115,50 @@ var AIRTIME = (function(AIRTIME){
 			$fadeOut = $parent.find(".spl_fade_out"),
 			$fadeIn = $parent.find(".spl_fade_in"),
 			$html = $($("#tmpl-pl-fades").html()),
-			tracks = [
-			    {
-			    	src: $fadeOut.data("fadeout"),
-			    	cuein: $fadeOut.data("cuein"),
-			    	cueout: $fadeOut.data("cueout"),
-			    	moveable: false,
-			    	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
-		            }
-				},
-				{
-			    	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
-		            }
-				}
-			],
+			tracks = [],
 			dim = AIRTIME.utilities.findViewportDimensions(),
 			playlistEditor;
 		
+		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
+	            }
+			});
+		}
+
+		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
+	            }
+			});
+		}
+		
+		//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",
@@ -1158,13 +1167,13 @@ var AIRTIME = (function(AIRTIME){
             width: dim.width - 100,
             height: dim.height - 100,
             buttons: [
+                {text: "Cancel", click: function() {
+                	$(this).dialog("destroy");
+                }},
                 {text: "Save", click: function() {
                 	var json = playlistEditor.getJson();
                 	
                 	var x;
-                }},
-                {text: "Cancel", click: function() {
-                	$(this).dialog("destroy");
                 }}
             ],
             open: function (event, ui) {
@@ -1223,14 +1232,14 @@ var AIRTIME = (function(AIRTIME){
             width: dim.width - 100,
             height: dim.height - 100,
             buttons: [
+                {text: "Cancel", click: function() {
+                	$(this).dialog("destroy");
+                }},
                 {text: "Save", click: function() {
                 	var cueIn = $html.find('.editor-cue-in').val(),
                 		cueOut = $html.find('.editor-cue-out').val();
                 	
                 	changeCues($html, id, cueIn, cueOut);
-                }},
-                {text: "Cancel", click: function() {
-                	$(this).dialog("destroy");
                 }}
             ],
             open: function (event, ui) {

From 58957cc919d04bf73664758970a48d01b7f89aa5 Mon Sep 17 00:00:00 2001
From: Naomi <naomiaro@gmail.com>
Date: Mon, 29 Apr 2013 17:01:08 -0400
Subject: [PATCH 15/26] CC-2301 : running dos2unix on 3 files.

---
 .../application/forms/GeneralPreferences.php  | 34 +++++------
 airtime_mvc/application/models/Block.php      |  8 +--
 airtime_mvc/application/models/Preference.php | 60 +++++++++----------
 3 files changed, 51 insertions(+), 51 deletions(-)

diff --git a/airtime_mvc/application/forms/GeneralPreferences.php b/airtime_mvc/application/forms/GeneralPreferences.php
index 6e02a3200..f73320e28 100644
--- a/airtime_mvc/application/forms/GeneralPreferences.php
+++ b/airtime_mvc/application/forms/GeneralPreferences.php
@@ -46,23 +46,23 @@ class Application_Form_GeneralPreferences extends Zend_Form_SubForm
             )
         ));
         
-        //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'
-        		)
+        //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');
diff --git a/airtime_mvc/application/models/Block.php b/airtime_mvc/application/models/Block.php
index e8db4ee12..b15f0d633 100644
--- a/airtime_mvc/application/models/Block.php
+++ b/airtime_mvc/application/models/Block.php
@@ -99,7 +99,7 @@ class Application_Model_Block implements Application_Model_LibraryEditable
             $this->block->save();
         }
 
-        $this->blockItem["fadein"] = Application_Model_Preference::GetDefaultFadeIn();
+        $this->blockItem["fadein"] = Application_Model_Preference::GetDefaultFadeIn();
         $this->blockItem["fadeout"] = Application_Model_Preference::GetDefaultFadeOut();
 
         $this->con = isset($con) ? $con : Propel::getConnection(CcBlockPeer::DATABASE_NAME);
@@ -230,9 +230,9 @@ SQL;
 
             $clipSec = Application_Common_DateHelper::playlistTimeToSeconds($row['length']);
             
-            $row['trackSec'] = $clipSec;
-            
-            $row['cueInSec'] = Application_Common_DateHelper::playlistTimeToSeconds($row['cuein']);
+            $row['trackSec'] = $clipSec;
+            
+            $row['cueInSec'] = Application_Common_DateHelper::playlistTimeToSeconds($row['cuein']);
             $row['cueOutSec'] = Application_Common_DateHelper::playlistTimeToSeconds($row['cueout']);
             
             $offset += $clipSec;
diff --git a/airtime_mvc/application/models/Preference.php b/airtime_mvc/application/models/Preference.php
index dbb3146dd..2e0096a49 100644
--- a/airtime_mvc/application/models/Preference.php
+++ b/airtime_mvc/application/models/Preference.php
@@ -195,38 +195,38 @@ class Application_Model_Preference
         }
     }
     
-    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 SetDefaultFadeIn($fade)
+    {
+    	self::setValue("default_fade_in", $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 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)

From b2a490ac470de47652d24f1431c5b8f8ee64dab9 Mon Sep 17 00:00:00 2001
From: Naomi <naomiaro@gmail.com>
Date: Mon, 29 Apr 2013 18:09:12 -0400
Subject: [PATCH 16/26] CC-2301 : working on creating a crossfade method for
 waveforms.

---
 .../controllers/PlaylistController.php        | 27 ++++++++++++++++
 airtime_mvc/application/models/Playlist.php   | 31 +++++++++++++++++--
 2 files changed, 56 insertions(+), 2 deletions(-)

diff --git a/airtime_mvc/application/controllers/PlaylistController.php b/airtime_mvc/application/controllers/PlaylistController.php
index 3f0dd66b7..ab42823e7 100644
--- a/airtime_mvc/application/controllers/PlaylistController.php
+++ b/airtime_mvc/application/controllers/PlaylistController.php
@@ -417,6 +417,33 @@ class PlaylistController extends Zend_Controller_Action
             $this->playlistUnknownError($e);
         }
     }
+    
+    public function setCrossFadeAction()
+    {
+    	$id = $this->_getParam('id');
+    	$fadeIn = $this->_getParam('fadeIn', 0);
+    	$fadeOut = $this->_getParam('fadeOut', 0);
+    	$type = $this->_getParam('type');
+    	$offset = $this->_getParam('offset', 0);
+    
+    	try {
+    		$obj = $this->getPlaylist($type);
+    		$response = $obj->changeFadeInfo($id, $fadeIn, $fadeOut);
+    
+    		if (!isset($response["error"])) {
+    			$this->createUpdateResponse($obj);
+    			$this->view->response = $response;
+    		} else {
+    			$this->view->fade_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/models/Playlist.php b/airtime_mvc/application/models/Playlist.php
index 764a8695c..e5610ec4f 100644
--- a/airtime_mvc/application/models/Playlist.php
+++ b/airtime_mvc/application/models/Playlist.php
@@ -645,12 +645,39 @@ 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, $id2, $fadeIn, $fadeOut, $offset)
+    {
+    	$this->con->beginTransaction();
+    	
+    	try {
+    		$this->changeFadeInfo($id1, null, $fadeOut);
+    		$this->changeFadeInfo($id2, $fadeIn, null);
+    		
+    		$item = CcPlaylistcontentsQuery::create()->findPK($id2);
+    		$item->setDbOffset($offset);
+    		$item->save($this->con);
+    		
+    		$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

From 2e03e1982f4b8bbd920ebae461e62fe12f13391f Mon Sep 17 00:00:00 2001
From: Naomi <naomiaro@gmail.com>
Date: Mon, 29 Apr 2013 18:11:11 -0400
Subject: [PATCH 17/26] CC-2301 : changing more CRLF files

---
 .../controllers/PlaylistController.php        | 48 +++++++++----------
 airtime_mvc/application/models/Playlist.php   |  6 +--
 2 files changed, 27 insertions(+), 27 deletions(-)

diff --git a/airtime_mvc/application/controllers/PlaylistController.php b/airtime_mvc/application/controllers/PlaylistController.php
index ab42823e7..a52c7ea15 100644
--- a/airtime_mvc/application/controllers/PlaylistController.php
+++ b/airtime_mvc/application/controllers/PlaylistController.php
@@ -418,31 +418,31 @@ class PlaylistController extends Zend_Controller_Action
         }
     }
     
-    public function setCrossFadeAction()
-    {
-    	$id = $this->_getParam('id');
-    	$fadeIn = $this->_getParam('fadeIn', 0);
-    	$fadeOut = $this->_getParam('fadeOut', 0);
+    public function setCrossFadeAction()
+    {
+    	$id = $this->_getParam('id');
+    	$fadeIn = $this->_getParam('fadeIn', 0);
+    	$fadeOut = $this->_getParam('fadeOut', 0);
     	$type = $this->_getParam('type');
-    	$offset = $this->_getParam('offset', 0);
-    
-    	try {
-    		$obj = $this->getPlaylist($type);
-    		$response = $obj->changeFadeInfo($id, $fadeIn, $fadeOut);
-    
-    		if (!isset($response["error"])) {
-    			$this->createUpdateResponse($obj);
-    			$this->view->response = $response;
-    		} else {
-    			$this->view->fade_error = $response["error"];
-    		}
-    	} catch (PlaylistOutDatedException $e) {
-    		$this->playlistOutdated($e);
-    	} catch (PlaylistNotFoundException $e) {
-    		$this->playlistNotFound($type);
-    	} catch (Exception $e) {
-    		$this->playlistUnknownError($e);
-    	}
+    	$offset = $this->_getParam('offset', 0);
+    
+    	try {
+    		$obj = $this->getPlaylist($type);
+    		$response = $obj->changeFadeInfo($id, $fadeIn, $fadeOut);
+    
+    		if (!isset($response["error"])) {
+    			$this->createUpdateResponse($obj);
+    			$this->view->response = $response;
+    		} else {
+    			$this->view->fade_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/models/Playlist.php b/airtime_mvc/application/models/Playlist.php
index e5610ec4f..388af70fc 100644
--- a/airtime_mvc/application/models/Playlist.php
+++ b/airtime_mvc/application/models/Playlist.php
@@ -655,10 +655,10 @@ SQL;
      */
     public function createCrossfade($id1, $id2, $fadeIn, $fadeOut, $offset)
     {
-    	$this->con->beginTransaction();
-    	
+    	$this->con->beginTransaction();
+    	
     	try {
-    		$this->changeFadeInfo($id1, null, $fadeOut);
+    		$this->changeFadeInfo($id1, null, $fadeOut);
     		$this->changeFadeInfo($id2, $fadeIn, null);
     		
     		$item = CcPlaylistcontentsQuery::create()->findPK($id2);

From 0db557a570289b34cc7f6c9df0cd5c67b3319df9 Mon Sep 17 00:00:00 2001
From: Naomi <naomiaro@gmail.com>
Date: Tue, 30 Apr 2013 15:32:43 -0400
Subject: [PATCH 18/26] CC-2301 : fades editor saving offset to database for
 playlists and smart blocks.

---
 .../controllers/PlaylistController.php        |  13 +--
 airtime_mvc/application/models/Block.php      |  31 +++++-
 airtime_mvc/application/models/Playlist.php   |  16 +--
 .../airtime/map/CcBlockcontentsTableMap.php   |   1 +
 .../map/CcPlaylistcontentsTableMap.php        |   2 +-
 .../models/airtime/om/BaseCcBlockcontents.php | 102 +++++++++++++-----
 .../airtime/om/BaseCcBlockcontentsPeer.php    |  31 +++---
 .../airtime/om/BaseCcBlockcontentsQuery.php   |  35 ++++++
 .../airtime/om/BaseCcPlaylistcontents.php     |  42 ++++----
 .../airtime/om/BaseCcPlaylistcontentsPeer.php |  28 ++---
 .../om/BaseCcPlaylistcontentsQuery.php        |  26 ++---
 .../views/scripts/playlist/set-fade.phtml     |   4 +-
 airtime_mvc/build/schema.xml                  |   3 +-
 airtime_mvc/build/sql/schema.sql              |   3 +-
 airtime_mvc/public/js/airtime/library/spl.js  |  66 +++++++++++-
 15 files changed, 295 insertions(+), 108 deletions(-)

diff --git a/airtime_mvc/application/controllers/PlaylistController.php b/airtime_mvc/application/controllers/PlaylistController.php
index a52c7ea15..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')
@@ -418,23 +419,23 @@ class PlaylistController extends Zend_Controller_Action
         }
     }
     
-    public function setCrossFadeAction()
+    public function setCrossfadeAction()
     {
-    	$id = $this->_getParam('id');
+    	$id1 = $this->_getParam('id1');
+    	$id2 = $this->_getParam('id2');
+    	$type = $this->_getParam('type');
     	$fadeIn = $this->_getParam('fadeIn', 0);
     	$fadeOut = $this->_getParam('fadeOut', 0);
-    	$type = $this->_getParam('type');
     	$offset = $this->_getParam('offset', 0);
     
     	try {
     		$obj = $this->getPlaylist($type);
-    		$response = $obj->changeFadeInfo($id, $fadeIn, $fadeOut);
+    		$response = $obj->createCrossfade($id1, $fadeOut, $id2, $fadeIn, $offset);
     
     		if (!isset($response["error"])) {
     			$this->createUpdateResponse($obj);
-    			$this->view->response = $response;
     		} else {
-    			$this->view->fade_error = $response["error"];
+    			$this->view->error = $response["error"];
     		}
     	} catch (PlaylistOutDatedException $e) {
     		$this->playlistOutdated($e);
diff --git a/airtime_mvc/application/models/Block.php b/airtime_mvc/application/models/Block.php
index b15f0d633..83a22947e 100644
--- a/airtime_mvc/application/models/Block.php
+++ b/airtime_mvc/application/models/Block.php
@@ -669,6 +669,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
@@ -681,7 +704,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
@@ -715,6 +738,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 388af70fc..ad734df3f 100644
--- a/airtime_mvc/application/models/Playlist.php
+++ b/airtime_mvc/application/models/Playlist.php
@@ -653,17 +653,13 @@ SQL;
      * $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, $id2, $fadeIn, $fadeOut, $offset)
+    public function createCrossfade($id1, $fadeOut, $id2, $fadeIn, $offset)
     {
     	$this->con->beginTransaction();
     	
     	try {
     		$this->changeFadeInfo($id1, null, $fadeOut);
-    		$this->changeFadeInfo($id2, $fadeIn, null);
-    		
-    		$item = CcPlaylistcontentsQuery::create()->findPK($id2);
-    		$item->setDbOffset($offset);
-    		$item->save($this->con);
+    		$this->changeFadeInfo($id2, $fadeIn, null, $offset);
     		
     		$this->con->commit();
     		
@@ -684,7 +680,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
@@ -710,6 +706,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/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 af6eddd10..3122f64d5 100644
--- a/airtime_mvc/application/models/airtime/map/CcPlaylistcontentsTableMap.php
+++ b/airtime_mvc/application/models/airtime/map/CcPlaylistcontentsTableMap.php
@@ -45,7 +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('OFFSET', 'DbOffset', 'REAL', true, null, 0);
+		$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 f7d66a9c6..cf6e53a98 100644
--- a/airtime_mvc/application/models/airtime/om/BaseCcPlaylistcontents.php
+++ b/airtime_mvc/application/models/airtime/om/BaseCcPlaylistcontents.php
@@ -68,11 +68,11 @@ abstract class BaseCcPlaylistcontents extends BaseObject  implements Persistent
 	protected $position;
 
 	/**
-	 * The value for the offset field.
+	 * The value for the trackoffset field.
 	 * Note: this column has a database default value of: 0
 	 * @var        double
 	 */
-	protected $offset;
+	protected $trackoffset;
 
 	/**
 	 * The value for the cliplength field.
@@ -150,7 +150,7 @@ abstract class BaseCcPlaylistcontents extends BaseObject  implements Persistent
 	public function applyDefaultValues()
 	{
 		$this->type = 0;
-		$this->offset = 0;
+		$this->trackoffset = 0;
 		$this->cliplength = '00:00:00';
 		$this->cuein = '00:00:00';
 		$this->cueout = '00:00:00';
@@ -239,13 +239,13 @@ abstract class BaseCcPlaylistcontents extends BaseObject  implements Persistent
 	}
 
 	/**
-	 * Get the [offset] column value.
+	 * Get the [trackoffset] column value.
 	 * 
 	 * @return     double
 	 */
-	public function getDbOffset()
+	public function getDbTrackOffset()
 	{
-		return $this->offset;
+		return $this->trackoffset;
 	}
 
 	/**
@@ -497,24 +497,24 @@ abstract class BaseCcPlaylistcontents extends BaseObject  implements Persistent
 	} // setDbPosition()
 
 	/**
-	 * Set the value of [offset] column.
+	 * Set the value of [trackoffset] column.
 	 * 
 	 * @param      double $v new value
 	 * @return     CcPlaylistcontents The current object (for fluent API support)
 	 */
-	public function setDbOffset($v)
+	public function setDbTrackOffset($v)
 	{
 		if ($v !== null) {
 			$v = (double) $v;
 		}
 
-		if ($this->offset !== $v || $this->isNew()) {
-			$this->offset = $v;
-			$this->modifiedColumns[] = CcPlaylistcontentsPeer::OFFSET;
+		if ($this->trackoffset !== $v || $this->isNew()) {
+			$this->trackoffset = $v;
+			$this->modifiedColumns[] = CcPlaylistcontentsPeer::TRACKOFFSET;
 		}
 
 		return $this;
-	} // setDbOffset()
+	} // setDbTrackOffset()
 
 	/**
 	 * Set the value of [cliplength] column.
@@ -690,7 +690,7 @@ abstract class BaseCcPlaylistcontents extends BaseObject  implements Persistent
 				return false;
 			}
 
-			if ($this->offset !== 0) {
+			if ($this->trackoffset !== 0) {
 				return false;
 			}
 
@@ -743,7 +743,7 @@ 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->offset = ($row[$startcol + 7] !== null) ? (double) $row[$startcol + 7] : 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;
@@ -1142,7 +1142,7 @@ abstract class BaseCcPlaylistcontents extends BaseObject  implements Persistent
 				return $this->getDbPosition();
 				break;
 			case 7:
-				return $this->getDbOffset();
+				return $this->getDbTrackOffset();
 				break;
 			case 8:
 				return $this->getDbCliplength();
@@ -1190,7 +1190,7 @@ abstract class BaseCcPlaylistcontents extends BaseObject  implements Persistent
 			$keys[4] => $this->getDbStreamId(),
 			$keys[5] => $this->getDbType(),
 			$keys[6] => $this->getDbPosition(),
-			$keys[7] => $this->getDbOffset(),
+			$keys[7] => $this->getDbTrackOffset(),
 			$keys[8] => $this->getDbCliplength(),
 			$keys[9] => $this->getDbCuein(),
 			$keys[10] => $this->getDbCueout(),
@@ -1260,7 +1260,7 @@ abstract class BaseCcPlaylistcontents extends BaseObject  implements Persistent
 				$this->setDbPosition($value);
 				break;
 			case 7:
-				$this->setDbOffset($value);
+				$this->setDbTrackOffset($value);
 				break;
 			case 8:
 				$this->setDbCliplength($value);
@@ -1308,7 +1308,7 @@ 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->setDbOffset($arr[$keys[7]]);
+		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]]);
@@ -1332,7 +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::OFFSET)) $criteria->add(CcPlaylistcontentsPeer::OFFSET, $this->offset);
+		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);
@@ -1405,7 +1405,7 @@ abstract class BaseCcPlaylistcontents extends BaseObject  implements Persistent
 		$copyObj->setDbStreamId($this->stream_id);
 		$copyObj->setDbType($this->type);
 		$copyObj->setDbPosition($this->position);
-		$copyObj->setDbOffset($this->offset);
+		$copyObj->setDbTrackOffset($this->trackoffset);
 		$copyObj->setDbCliplength($this->cliplength);
 		$copyObj->setDbCuein($this->cuein);
 		$copyObj->setDbCueout($this->cueout);
@@ -1617,7 +1617,7 @@ abstract class BaseCcPlaylistcontents extends BaseObject  implements Persistent
 		$this->stream_id = null;
 		$this->type = null;
 		$this->position = null;
-		$this->offset = 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 6645977ea..0e23f44c3 100644
--- a/airtime_mvc/application/models/airtime/om/BaseCcPlaylistcontentsPeer.php
+++ b/airtime_mvc/application/models/airtime/om/BaseCcPlaylistcontentsPeer.php
@@ -52,8 +52,8 @@ abstract class BaseCcPlaylistcontentsPeer {
 	/** the column name for the POSITION field */
 	const POSITION = 'cc_playlistcontents.POSITION';
 
-	/** the column name for the OFFSET field */
-	const OFFSET = 'cc_playlistcontents.OFFSET';
+	/** the column name for the TRACKOFFSET field */
+	const TRACKOFFSET = 'cc_playlistcontents.TRACKOFFSET';
 
 	/** the column name for the CLIPLENGTH field */
 	const CLIPLENGTH = 'cc_playlistcontents.CLIPLENGTH';
@@ -86,11 +86,11 @@ 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', 'DbOffset', 'DbCliplength', 'DbCuein', 'DbCueout', 'DbFadein', 'DbFadeout', ),
-		BasePeer::TYPE_STUDLYPHPNAME => array ('dbId', 'dbPlaylistId', 'dbFileId', 'dbBlockId', 'dbStreamId', 'dbType', 'dbPosition', 'dbOffset', '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::OFFSET, 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', 'OFFSET', 'CLIPLENGTH', 'CUEIN', 'CUEOUT', 'FADEIN', 'FADEOUT', ),
-		BasePeer::TYPE_FIELDNAME => array ('id', 'playlist_id', 'file_id', 'block_id', 'stream_id', 'type', 'position', 'offset', 'cliplength', 'cuein', 'cueout', 'fadein', 'fadeout', ),
+		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, )
 	);
 
@@ -101,11 +101,11 @@ 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, 'DbOffset' => 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, 'dbOffset' => 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::OFFSET => 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, 'OFFSET' => 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, 'offset' => 7, 'cliplength' => 8, 'cuein' => 9, 'cueout' => 10, 'fadein' => 11, 'fadeout' => 12, ),
+		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, )
 	);
 
@@ -185,7 +185,7 @@ abstract class BaseCcPlaylistcontentsPeer {
 			$criteria->addSelectColumn(CcPlaylistcontentsPeer::STREAM_ID);
 			$criteria->addSelectColumn(CcPlaylistcontentsPeer::TYPE);
 			$criteria->addSelectColumn(CcPlaylistcontentsPeer::POSITION);
-			$criteria->addSelectColumn(CcPlaylistcontentsPeer::OFFSET);
+			$criteria->addSelectColumn(CcPlaylistcontentsPeer::TRACKOFFSET);
 			$criteria->addSelectColumn(CcPlaylistcontentsPeer::CLIPLENGTH);
 			$criteria->addSelectColumn(CcPlaylistcontentsPeer::CUEIN);
 			$criteria->addSelectColumn(CcPlaylistcontentsPeer::CUEOUT);
@@ -199,7 +199,7 @@ abstract class BaseCcPlaylistcontentsPeer {
 			$criteria->addSelectColumn($alias . '.STREAM_ID');
 			$criteria->addSelectColumn($alias . '.TYPE');
 			$criteria->addSelectColumn($alias . '.POSITION');
-			$criteria->addSelectColumn($alias . '.OFFSET');
+			$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 d70d1dd17..ef3d3877a 100644
--- a/airtime_mvc/application/models/airtime/om/BaseCcPlaylistcontentsQuery.php
+++ b/airtime_mvc/application/models/airtime/om/BaseCcPlaylistcontentsQuery.php
@@ -13,7 +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 orderByDbOffset($order = Criteria::ASC) Order by the offset 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
@@ -27,7 +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 groupByDbOffset() Group by the offset 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
@@ -60,7 +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 findOneByDbOffset(double $offset) Return the first CcPlaylistcontents filtered by the offset 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
@@ -74,7 +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 findByDbOffset(double $offset) Return CcPlaylistcontents objects filtered by the offset 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
@@ -393,24 +393,24 @@ abstract class BaseCcPlaylistcontentsQuery extends ModelCriteria
 	}
 
 	/**
-	 * Filter the query on the offset column
+	 * Filter the query on the trackoffset column
 	 * 
-	 * @param     double|array $dbOffset The value to use as filter.
+	 * @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 filterByDbOffset($dbOffset = null, $comparison = null)
+	public function filterByDbTrackOffset($dbTrackOffset = null, $comparison = null)
 	{
-		if (is_array($dbOffset)) {
+		if (is_array($dbTrackOffset)) {
 			$useMinMax = false;
-			if (isset($dbOffset['min'])) {
-				$this->addUsingAlias(CcPlaylistcontentsPeer::OFFSET, $dbOffset['min'], Criteria::GREATER_EQUAL);
+			if (isset($dbTrackOffset['min'])) {
+				$this->addUsingAlias(CcPlaylistcontentsPeer::TRACKOFFSET, $dbTrackOffset['min'], Criteria::GREATER_EQUAL);
 				$useMinMax = true;
 			}
-			if (isset($dbOffset['max'])) {
-				$this->addUsingAlias(CcPlaylistcontentsPeer::OFFSET, $dbOffset['max'], Criteria::LESS_EQUAL);
+			if (isset($dbTrackOffset['max'])) {
+				$this->addUsingAlias(CcPlaylistcontentsPeer::TRACKOFFSET, $dbTrackOffset['max'], Criteria::LESS_EQUAL);
 				$useMinMax = true;
 			}
 			if ($useMinMax) {
@@ -420,7 +420,7 @@ abstract class BaseCcPlaylistcontentsQuery extends ModelCriteria
 				$comparison = Criteria::IN;
 			}
 		}
-		return $this->addUsingAlias(CcPlaylistcontentsPeer::OFFSET, $dbOffset, $comparison);
+		return $this->addUsingAlias(CcPlaylistcontentsPeer::TRACKOFFSET, $dbTrackOffset, $comparison);
 	}
 
 	/**
diff --git a/airtime_mvc/application/views/scripts/playlist/set-fade.phtml b/airtime_mvc/application/views/scripts/playlist/set-fade.phtml
index 4c6f745af..510ed441e 100644
--- a/airtime_mvc/application/views/scripts/playlist/set-fade.phtml
+++ b/airtime_mvc/application/views/scripts/playlist/set-fade.phtml
@@ -6,7 +6,7 @@
     <dt><? echo _("Fade out: "); ?><span class='spl_cue_hint'><? echo _("(ss.t)")?></span></dt>
     <dd id="spl_fade_out_<?php echo $this->item1; ?>" class="spl_fade_out" data-fadeout="<?php echo $this->item1Url; ?>"
     	data-cuein="<?php echo $this->cueIn1; ?>" data-cueout="<?php echo $this->cueOut1; ?>" data-length="<?php echo $this->fadeOut; ?>"
-    	data-type="logarithmic">
+    	data-type="logarithmic" data-item="<?php echo $this->item1; ?>">
         <span contenteditable="true" class="spl_text_input"><?php echo $this->fadeOut; ?></span>
     </dd>
     <dd class="edit-error"></dd>
@@ -15,7 +15,7 @@
     <dt><? echo _("Fade in: "); ?><span class='spl_cue_hint'><? echo _("(ss.t)")?></span></dt>
     <dd id="spl_fade_in_<?php echo $this->item2; ?>" class="spl_fade_in" data-fadein="<?php echo $this->item2Url; ?>" data-offset="<?php if ($this->item1Type == 0) { echo $this->offset; } else { echo 0; } ?>"
     	data-cuein="<?php echo $this->cueIn2; ?>" data-cueout="<?php echo $this->cueOut2; ?>" data-length="<?php echo $this->fadeIn; ?>"
-    	data-type="logarithmic">
+    	data-type="logarithmic" data-item="<?php echo $this->item2; ?>">
         <span contenteditable="true" class="spl_text_input"><?php echo $this->fadeIn; ?></span>
     </dd>
     <dd class="edit-error"></dd>
diff --git a/airtime_mvc/build/schema.xml b/airtime_mvc/build/schema.xml
index d8bb3c4b1..80384eb3a 100644
--- a/airtime_mvc/build/schema.xml
+++ b/airtime_mvc/build/schema.xml
@@ -227,7 +227,7 @@
     -->
     <column name="type" phpName="DbType" type="SMALLINT" required="true" default="0"/>
     <column name="position" phpName="DbPosition" type="INTEGER" required="false"/>
-    <column name="offset" phpName="DbOffset" type="REAL" required="true" default="0"/>
+    <column name="trackoffset" phpName="DbTrackOffset" type="REAL" required="true" default="0"/>
     <column name="cliplength" phpName="DbCliplength" type="VARCHAR" sqlType="interval" required="false" defaultValue="00:00:00"/>
     <column name="cuein" phpName="DbCuein" type="VARCHAR" sqlType="interval" required="false" defaultValue="00:00:00"/>
     <column name="cueout" phpName="DbCueout" type="VARCHAR" sqlType="interval" required="false" defaultValue="00:00:00"/>
@@ -266,6 +266,7 @@
     <column name="block_id" phpName="DbBlockId" type="INTEGER" required="false"/>
     <column name="file_id" phpName="DbFileId" type="INTEGER" required="false"/>
     <column name="position" phpName="DbPosition" type="INTEGER" required="false"/>
+    <column name="trackoffset" phpName="DbTrackOffset" type="REAL" required="true" default="0"/>
     <column name="cliplength" phpName="DbCliplength" type="VARCHAR" sqlType="interval" required="false" defaultValue="00:00:00"/>
     <column name="cuein" phpName="DbCuein" type="VARCHAR" sqlType="interval" required="false" defaultValue="00:00:00"/>
     <column name="cueout" phpName="DbCueout" type="VARCHAR" sqlType="interval" required="false" defaultValue="00:00:00"/>
diff --git a/airtime_mvc/build/sql/schema.sql b/airtime_mvc/build/sql/schema.sql
index 1ed734aa1..b980dc829 100644
--- a/airtime_mvc/build/sql/schema.sql
+++ b/airtime_mvc/build/sql/schema.sql
@@ -296,7 +296,7 @@ CREATE TABLE "cc_playlistcontents"
 	"stream_id" INTEGER,
 	"type" INT2 default 0 NOT NULL,
 	"position" INTEGER,
-	"offset" FLOAT default 0 NOT NULL,
+	"trackoffset" FLOAT default 0 NOT NULL,
 	"cliplength" interval default '00:00:00',
 	"cuein" interval default '00:00:00',
 	"cueout" interval default '00:00:00',
@@ -346,6 +346,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/js/airtime/library/spl.js b/airtime_mvc/public/js/airtime/library/spl.js
index f3a7fc8c5..1d13a525c 100644
--- a/airtime_mvc/public/js/airtime/library/spl.js
+++ b/airtime_mvc/public/js/airtime/library/spl.js
@@ -166,7 +166,7 @@ var AIRTIME = (function(AIRTIME){
             {format: "json", cueIn: cueIn, cueOut: cueOut, id: id, modified: lastMod, type: type}, 
             function(json){
             	
-            	$el.dialog('close');
+            	$el.dialog('destroy');
 
                 if (json.error !== undefined){
                     playlistError(json);
@@ -185,6 +185,34 @@ var AIRTIME = (function(AIRTIME){
                 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');
+
+                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();
@@ -1117,7 +1145,8 @@ var AIRTIME = (function(AIRTIME){
 			$html = $($("#tmpl-pl-fades").html()),
 			tracks = [],
 			dim = AIRTIME.utilities.findViewportDimensions(),
-			playlistEditor;
+			playlistEditor,
+			id1, id2;
 		
 		if ($fadeOut.length > 0) {
 			
@@ -1135,6 +1164,8 @@ var AIRTIME = (function(AIRTIME){
 	                'fadein': false
 	            }
 			});
+			
+			id1 = $fadeOut.data("item");
 		}
 
 		if ($fadeIn.length > 0) {
@@ -1154,6 +1185,8 @@ var AIRTIME = (function(AIRTIME){
 	                'fadeout': false
 	            }
 			});
+			
+			id2 = $fadeIn.data("item");
 		}
 		
 		//set the first track to not be moveable (might only be one track depending on what follows)
@@ -1171,9 +1204,34 @@ var AIRTIME = (function(AIRTIME){
                 	$(this).dialog("destroy");
                 }},
                 {text: "Save", click: function() {
-                	var json = playlistEditor.getJson();
+                	var json = playlistEditor.getJson(),
+                		offset, 
+                		fadeIn, fadeOut,
+                		fade;
                 	
-                	var x;
+                	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) {

From 2462e4f14452fa1f8dd7c697d339b409a00c7549 Mon Sep 17 00:00:00 2001
From: Naomi <naomiaro@gmail.com>
Date: Tue, 30 Apr 2013 15:34:20 -0400
Subject: [PATCH 19/26] CC-2301 : CRLF fix.

---
 airtime_mvc/application/models/Block.php | 50 ++++++++++++------------
 1 file changed, 25 insertions(+), 25 deletions(-)

diff --git a/airtime_mvc/application/models/Block.php b/airtime_mvc/application/models/Block.php
index 83a22947e..c99ed22c3 100644
--- a/airtime_mvc/application/models/Block.php
+++ b/airtime_mvc/application/models/Block.php
@@ -670,27 +670,27 @@ 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;
-    	}
+    /*
+     * 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;
+    	}
     }
 
     /**
@@ -738,10 +738,10 @@ 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($offset)) {
+                	$row->setDbTrackOffset($offset);
+                	Logging::info("Setting offset {$offset} on item {$id}");
+                	$row->save($this->con);
                 }
                 
             }

From 2a1ac0ddb911481a7e3cf7fc2ca728b9fb9252bb Mon Sep 17 00:00:00 2001
From: Naomi <naomiaro@gmail.com>
Date: Tue, 30 Apr 2013 16:27:31 -0400
Subject: [PATCH 20/26] CC-2301 : updating playlist offsets in playlist builder
 to reflect crossfade overlaps.

---
 airtime_mvc/application/models/Block.php                 | 3 +++
 airtime_mvc/application/models/Playlist.php              | 9 +++++++++
 .../application/views/scripts/playlist/update.phtml      | 2 +-
 airtime_mvc/public/js/airtime/library/spl.js             | 2 ++
 4 files changed, 15 insertions(+), 1 deletion(-)

diff --git a/airtime_mvc/application/models/Block.php b/airtime_mvc/application/models/Block.php
index c99ed22c3..931445d2d 100644
--- a/airtime_mvc/application/models/Block.php
+++ b/airtime_mvc/application/models/Block.php
@@ -195,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,
@@ -235,7 +236,9 @@ SQL;
             $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.
diff --git a/airtime_mvc/application/models/Playlist.php b/airtime_mvc/application/models/Playlist.php
index ad734df3f..d0df1b19d 100644
--- a/airtime_mvc/application/models/Playlist.php
+++ b/airtime_mvc/application/models/Playlist.php
@@ -161,6 +161,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,
@@ -188,6 +189,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,
@@ -208,6 +210,7 @@ SQL;
                             pc.cueout,
                             pc.fadein,
                             pc.fadeout,
+                            pc.trackoffset,
                             bl.id AS item_id,
                             bl.name AS title,
                             sbj.login AS creator,
@@ -222,6 +225,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);
@@ -233,13 +237,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.
diff --git a/airtime_mvc/application/views/scripts/playlist/update.phtml b/airtime_mvc/application/views/scripts/playlist/update.phtml
index b0d2b6151..39ee90fa6 100644
--- a/airtime_mvc/application/views/scripts/playlist/update.phtml
+++ b/airtime_mvc/application/views/scripts/playlist/update.phtml
@@ -94,7 +94,7 @@ if (($i < count($items) -1) && ($items[$i+1]['type'] == 0)) {
             	'item2Url' => $nextFileUrl,
                 'fadeOut' => $items[$i]['fadeout'],
                 'fadeIn' => $items[$i+1]['fadein'],
-            	'offset' => $items[$i]['trackSec'],
+            	'offset' => $items[$i]['trackSec'] - $items[$i+1]['trackoffset'],
                 'cueIn1' => $items[$i]['cueInSec'],
             	'cueOut1' => $items[$i]['cueOutSec'],
             	'cueIn2' => $items[$i+1]['cueInSec'],
diff --git a/airtime_mvc/public/js/airtime/library/spl.js b/airtime_mvc/public/js/airtime/library/spl.js
index 1d13a525c..3174bcb4b 100644
--- a/airtime_mvc/public/js/airtime/library/spl.js
+++ b/airtime_mvc/public/js/airtime/library/spl.js
@@ -167,6 +167,7 @@ var AIRTIME = (function(AIRTIME){
             function(json){
             	
             	$el.dialog('destroy');
+            	$el.remove();
 
                 if (json.error !== undefined){
                     playlistError(json);
@@ -199,6 +200,7 @@ var AIRTIME = (function(AIRTIME){
             function(json){
             	
             	$el.dialog('destroy');
+            	$el.remove();
 
                 if (json.error !== undefined){
                     playlistError(json);

From f82659d582f6dedd1e5e33986466176205cf42cd Mon Sep 17 00:00:00 2001
From: Naomi <naomiaro@gmail.com>
Date: Tue, 30 Apr 2013 17:03:40 -0400
Subject: [PATCH 21/26] CC-2301 : stopping playlist editor to make sure audio
 isn't stuck playing in the background.

---
 airtime_mvc/public/js/airtime/library/spl.js | 37 ++++++++++++++------
 1 file changed, 27 insertions(+), 10 deletions(-)

diff --git a/airtime_mvc/public/js/airtime/library/spl.js b/airtime_mvc/public/js/airtime/library/spl.js
index 3174bcb4b..6f8da35f6 100644
--- a/airtime_mvc/public/js/airtime/library/spl.js
+++ b/airtime_mvc/public/js/airtime/library/spl.js
@@ -1150,6 +1150,13 @@ var AIRTIME = (function(AIRTIME){
 			playlistEditor,
 			id1, id2;
 		
+		function removeDialog() {
+			playlistEditor.stop();
+			
+        	$html.dialog("destroy");
+        	$html.remove();
+        }
+		
 		if ($fadeOut.length > 0) {
 			
 			tracks.push({
@@ -1202,15 +1209,15 @@ var AIRTIME = (function(AIRTIME){
             width: dim.width - 100,
             height: dim.height - 100,
             buttons: [
-                {text: "Cancel", click: function() {
-                	$(this).dialog("destroy");
-                }},
+                {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];
@@ -1251,7 +1258,8 @@ var AIRTIME = (function(AIRTIME){
         		playlistEditor = new PlaylistEditor();
         	    playlistEditor.setConfig(config);
         	    playlistEditor.init(tracks);
-            }
+            },
+        	close: removeDialog
         });		
 	};
 	
@@ -1267,7 +1275,15 @@ var AIRTIME = (function(AIRTIME){
 			}],
 			cueIn = $li.find('.spl_cue_in').data("cueIn"),
 			cueOut = $li.find('.spl_cue_out').data("cueOut"),
-			dim = AIRTIME.utilities.findViewportDimensions();
+			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);
@@ -1292,13 +1308,13 @@ var AIRTIME = (function(AIRTIME){
             width: dim.width - 100,
             height: dim.height - 100,
             buttons: [
-                {text: "Cancel", click: function() {
-                	$(this).dialog("destroy");
-                }},
+                {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);
                 }}
             ],
@@ -1313,10 +1329,11 @@ var AIRTIME = (function(AIRTIME){
         	        timeFormat: 'hh:mm:ss.u'
         	    });
         		
-        		var playlistEditor = new PlaylistEditor();
+        		playlistEditor = new PlaylistEditor();
         	    playlistEditor.setConfig(config);
         	    playlistEditor.init(tracks);	
-            }
+            },
+            close: removeDialog
         });	
 	};
 	

From 2b5a8b0d4b01a83edff937607c848307ab868908 Mon Sep 17 00:00:00 2001
From: Naomi <naomiaro@gmail.com>
Date: Wed, 1 May 2013 11:41:07 -0400
Subject: [PATCH 22/26] CC-2301 : fixing changed function name after merging
 with master.

---
 airtime_mvc/application/views/scripts/playlist/update.phtml | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/airtime_mvc/application/views/scripts/playlist/update.phtml b/airtime_mvc/application/views/scripts/playlist/update.phtml
index 39ee90fa6..7c718dd59 100644
--- a/airtime_mvc/application/views/scripts/playlist/update.phtml
+++ b/airtime_mvc/application/views/scripts/playlist/update.phtml
@@ -9,12 +9,12 @@ if ($item['type'] == 2) {
     $staticBlock = $bl->isStatic();
 }
 else if ($item['type'] == 0) {
-	$audiofile = Application_Model_StoredFile::Recall($item['item_id']);
+	$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::Recall($items[$i+1]['item_id']);
+	$nextAudiofile = Application_Model_StoredFile::RecallById($items[$i+1]['item_id']);
 	$nextFileUrl = $nextAudiofile->getFileUrl();	
 }
 ?>

From 517bb7557a0e6bedf6a7a04fedc31113d340af6f Mon Sep 17 00:00:00 2001
From: Naomi <naomiaro@gmail.com>
Date: Thu, 2 May 2013 16:15:21 -0400
Subject: [PATCH 23/26] CC-2301 : testing default crossfade settings

---
 .../controllers/PreferenceController.php      |   1 +
 .../application/forms/GeneralPreferences.php  |  19 +++
 airtime_mvc/application/models/Playlist.php   |   2 +
 airtime_mvc/application/models/Preference.php |  17 +++
 airtime_mvc/application/models/Scheduler.php  | 109 ++++++++++++++----
 .../scripts/form/preferences_general.phtml    |  13 +++
 6 files changed, 137 insertions(+), 24 deletions(-)

diff --git a/airtime_mvc/application/controllers/PreferenceController.php b/airtime_mvc/application/controllers/PreferenceController.php
index 75b8fc2e8..7d0f21210 100644
--- a/airtime_mvc/application/controllers/PreferenceController.php
+++ b/airtime_mvc/application/controllers/PreferenceController.php
@@ -43,6 +43,7 @@ class PreferenceController extends Zend_Controller_Action
             if ($form->isValid($values)) {
 
                 Application_Model_Preference::SetHeadTitle($values["stationName"], $this->view);
+                Application_Model_Preference::SetDefaultCrossfadeDuration($values["stationDefaultCrossfadeDuration"]);
                 Application_Model_Preference::SetDefaultFadeIn($values["stationDefaultFadeIn"]);
                 Application_Model_Preference::SetDefaultFadeOut($values["stationDefaultFadeOut"]);
                 Application_Model_Preference::SetAllow3rdPartyApi($values["thirdPartyApi"]);
diff --git a/airtime_mvc/application/forms/GeneralPreferences.php b/airtime_mvc/application/forms/GeneralPreferences.php
index f73320e28..29c819fac 100644
--- a/airtime_mvc/application/forms/GeneralPreferences.php
+++ b/airtime_mvc/application/forms/GeneralPreferences.php
@@ -26,6 +26,25 @@ 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 in
         $this->addElement('text', 'stationDefaultFadeIn', array(
diff --git a/airtime_mvc/application/models/Playlist.php b/airtime_mvc/application/models/Playlist.php
index d0df1b19d..806d57955 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.
@@ -62,6 +63,7 @@ class Application_Model_Playlist implements Application_Model_LibraryEditable
 
         $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();
diff --git a/airtime_mvc/application/models/Preference.php b/airtime_mvc/application/models/Preference.php
index 2e0096a49..0735c87e4 100644
--- a/airtime_mvc/application/models/Preference.php
+++ b/airtime_mvc/application/models/Preference.php
@@ -195,6 +195,23 @@ class Application_Model_Preference
         }
     }
     
+    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);
diff --git a/airtime_mvc/application/models/Scheduler.php b/airtime_mvc/application/models/Scheduler.php
index 3778cf007..f2c435e2d 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/views/scripts/form/preferences_general.phtml b/airtime_mvc/application/views/scripts/form/preferences_general.phtml
index 85582411c..a438fb446 100644
--- a/airtime_mvc/application/views/scripts/form/preferences_general.phtml
+++ b/airtime_mvc/application/views/scripts/form/preferences_general.phtml
@@ -40,6 +40,19 @@
                 </ul>
             <?php endif; ?>
         </dd>
+        <dt id="stationDefaultCrossfadeDuration-label" class="block-display">
+            <label class="optional" for="stationDefaultCrossfadeDuration"><?php echo $this->element->getElement('stationDefaultCrossfadeDuration')->getLabel() ?></label>
+        </dt>
+        <dd id="stationDefaultCrossfadeDuration-element" class="block-display">
+            <?php echo $this->element->getElement('stationDefaultCrossfadeDuration') ?>
+            <?php if($this->element->getElement('stationDefaultCrossfadeDuration')->hasErrors()) : ?>
+                <ul class='errors'>
+                    <?php foreach($this->element->getElement('stationDefaultCrossfadeDuration')->getMessages() as $error): ?>
+                        <li><?php echo $error; ?></li>
+                    <?php endforeach; ?>
+                </ul>
+            <?php endif; ?>
+        </dd>
         <dt id="thirdPartyApi-label" class="block-display">
             <label class="optional"><?php echo $this->element->getElement('thirdPartyApi')->getLabel() ?></label>
         </dt>

From 435bb83588c519ed65fe9ef0d4b4ae36d87378b1 Mon Sep 17 00:00:00 2001
From: Naomi <naomiaro@gmail.com>
Date: Thu, 2 May 2013 16:52:30 -0400
Subject: [PATCH 24/26] CC-2301 : dos2unix being run

---
 .../application/forms/GeneralPreferences.php  |  34 +++---
 airtime_mvc/application/models/Preference.php |  30 ++---
 airtime_mvc/application/models/Scheduler.php  | 108 +++++++++---------
 3 files changed, 86 insertions(+), 86 deletions(-)

diff --git a/airtime_mvc/application/forms/GeneralPreferences.php b/airtime_mvc/application/forms/GeneralPreferences.php
index 29c819fac..19e3fc064 100644
--- a/airtime_mvc/application/forms/GeneralPreferences.php
+++ b/airtime_mvc/application/forms/GeneralPreferences.php
@@ -27,23 +27,23 @@ class Application_Form_GeneralPreferences extends Zend_Form_SubForm
             )
         ));
         
-        //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 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 in
diff --git a/airtime_mvc/application/models/Preference.php b/airtime_mvc/application/models/Preference.php
index 0735c87e4..f2eeee8a4 100644
--- a/airtime_mvc/application/models/Preference.php
+++ b/airtime_mvc/application/models/Preference.php
@@ -195,21 +195,21 @@ class Application_Model_Preference
         }
     }
     
-    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 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)
diff --git a/airtime_mvc/application/models/Scheduler.php b/airtime_mvc/application/models/Scheduler.php
index 77527f72f..7f6272cee 100644
--- a/airtime_mvc/application/models/Scheduler.php
+++ b/airtime_mvc/application/models/Scheduler.php
@@ -262,8 +262,8 @@ class Application_Model_Scheduler
                                 $cueout = Application_Common_DateHelper::calculateLengthInSeconds($data["cueout"]);
                                 $data["cliplength"] = Application_Common_DateHelper::secondsToPlaylistTime($cueout - $cuein);
                                 
-                                //fade is in format SS.uuuuuu
-                                $data["fadein"] = Application_Model_Preference::GetDefaultFadeIn();
+                                //fade is in format SS.uuuuuu
+                                $data["fadein"] = Application_Model_Preference::GetDefaultFadeIn();
                                 $data["fadeout"] = Application_Model_Preference::GetDefaultFadeOut();
                                 
                                 $data["type"] = 0;
@@ -333,29 +333,29 @@ 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
+    *  @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;
     }
 
     /*
@@ -415,41 +415,41 @@ class Application_Model_Scheduler
         return $nextDT;
     }
     
-    /*
-     * @param int $showInstance
+    /*
+     * @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!"));
-    	}
-    
+     *   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 = null;
+    
+    	$schedule = CcScheduleQuery::create()
+    		->filterByDbInstanceId($showInstance)
+    		->orderByDbStarts()
+    		->find($this->con);
+    
+    	foreach ($schedule as $item) {
     
     		$itemEndDT = $item->getDbEnds(null);
-    		
+    		
     		$item
-    			->setDbStarts($itemStartDT)
-    			->setDbEnds($itemEndDT);
-    
+    			->setDbStarts($itemStartDT)
+    			->setDbEnds($itemEndDT);
+    
     		$itemStartDT = $this->findTimeDifference($itemEndDT, $this->crossfadeDuration);
-    		$itemEndDT = $this->findEndTime($itemStartDT, $item->getDbClipLength());
-    	}
-    
-    	$schedule->save($this->con);
+    		$itemEndDT = $this->findEndTime($itemStartDT, $item->getDbClipLength());
+    	}
+    
+    	$schedule->save($this->con);
     }
 
     /*

From 75e4e0c9b6ae06a154a1484972c5351bf4d9da5c Mon Sep 17 00:00:00 2001
From: Naomi <naomiaro@gmail.com>
Date: Fri, 3 May 2013 14:42:42 -0400
Subject: [PATCH 25/26] CC-2301 : only showing waveform buttons if the browser
 supports the audio api.

---
 airtime_mvc/application/models/Playlist.php        |  1 +
 .../views/scripts/playlist/set-cue.phtml           |  4 +++-
 airtime_mvc/public/js/airtime/library/spl.js       | 14 +++++++++++++-
 3 files changed, 17 insertions(+), 2 deletions(-)

diff --git a/airtime_mvc/application/models/Playlist.php b/airtime_mvc/application/models/Playlist.php
index 806d57955..9d45f7daa 100644
--- a/airtime_mvc/application/models/Playlist.php
+++ b/airtime_mvc/application/models/Playlist.php
@@ -367,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"]);
diff --git a/airtime_mvc/application/views/scripts/playlist/set-cue.phtml b/airtime_mvc/application/views/scripts/playlist/set-cue.phtml
index 1e8a7a315..8dee1ca3f 100644
--- a/airtime_mvc/application/views/scripts/playlist/set-cue.phtml
+++ b/airtime_mvc/application/views/scripts/playlist/set-cue.phtml
@@ -1,5 +1,7 @@
-<div data-uri="<?php echo $this->uri; ?>"><input type="button" class="pl-waveform-cues-btn" value="Show Waveform"></input></div>
 <dl id="spl_cue_editor" class="inline-list">
+	<dd data-uri="<?php echo $this->uri; ?>">
+	  <input type="button" class="pl-waveform-cues-btn" value="Show Waveform"></input>
+	</dd>
     <dt><? echo _("Cue In: "); ?><span class='spl_cue_hint'><? echo _("(hh:mm:ss.t)")?></span></dt>
     <dd id="spl_cue_in_<?php echo $this->id; ?>" class="spl_cue_in" data-cue-in="<?php echo $this->cueIn; ?>">
         <span contenteditable="true" class="spl_text_input"><?php echo $this->cueIn; ?></span>
diff --git a/airtime_mvc/public/js/airtime/library/spl.js b/airtime_mvc/public/js/airtime/library/spl.js
index 6f8da35f6..417fdd5a9 100644
--- a/airtime_mvc/public/js/airtime/library/spl.js
+++ b/airtime_mvc/public/js/airtime/library/spl.js
@@ -370,6 +370,8 @@ var AIRTIME = (function(AIRTIME){
 	}
 	
 	function setPlaylistContent(json) {
+		var $html = $(json.html);
+		
 		$('#spl_name > a')
 			.empty()
 			.append(json.name);
@@ -383,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);
@@ -593,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.
@@ -603,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() {	

From ce81691743c6f2fa03b6aa32216eb358ca1d52fb Mon Sep 17 00:00:00 2001
From: Naomi <naomiaro@gmail.com>
Date: Fri, 3 May 2013 15:32:07 -0400
Subject: [PATCH 26/26] CC-2301 : not allowing any sliding with only global
 crossfade durations.

---
 airtime_mvc/public/js/airtime/library/spl.js | 8 +++++---
 1 file changed, 5 insertions(+), 3 deletions(-)

diff --git a/airtime_mvc/public/js/airtime/library/spl.js b/airtime_mvc/public/js/airtime/library/spl.js
index 417fdd5a9..a34ce0f08 100644
--- a/airtime_mvc/public/js/airtime/library/spl.js
+++ b/airtime_mvc/public/js/airtime/library/spl.js
@@ -1182,7 +1182,8 @@ var AIRTIME = (function(AIRTIME){
 		    	    start: $fadeOut.data("cueout") - $fadeOut.data("cuein") - $fadeOut.data("length")
 		    	}],
 		    	states: {
-	                'fadein': false
+	                'fadein': false,
+	                'shift': false
 	            }
 			});
 			
@@ -1203,7 +1204,8 @@ var AIRTIME = (function(AIRTIME){
 		    	    start: 0
 		    	}],
 		    	states: {
-	                'fadeout': false
+	                'fadeout': false,
+	                'shift': false
 	            }
 			});
 			
@@ -1211,7 +1213,7 @@ var AIRTIME = (function(AIRTIME){
 		}
 		
 		//set the first track to not be moveable (might only be one track depending on what follows)
-		tracks[0].states["shift"] = false;
+		//tracks[0].states["shift"] = false;
 		
 		$html.dialog({
             modal: true,