CC-2301 : showing waveform cue/fade editors inside of a jquery dialog box.
This commit is contained in:
parent
811abc4baf
commit
13c8e5f146
23 changed files with 3090 additions and 1 deletions
|
@ -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 {
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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"/>
|
||||
|
|
|
@ -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;
|
||||
}
|
|
@ -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();
|
||||
|
|
162
airtime_mvc/public/js/waveformplaylist/config.js
Normal file
162
airtime_mvc/public/js/waveformplaylist/config.js
Normal file
|
@ -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;
|
||||
};
|
||||
};
|
520
airtime_mvc/public/js/waveformplaylist/controls.js
vendored
Normal file
520
airtime_mvc/public/js/waveformplaylist/controls.js
vendored
Normal file
|
@ -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);
|
||||
}
|
||||
};
|
||||
|
72
airtime_mvc/public/js/waveformplaylist/curves.js
Normal file
72
airtime_mvc/public/js/waveformplaylist/curves.js
Normal file
|
@ -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;
|
||||
};
|
||||
|
140
airtime_mvc/public/js/waveformplaylist/fades.js
Normal file
140
airtime_mvc/public/js/waveformplaylist/fades.js
Normal file
|
@ -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);
|
||||
};
|
86
airtime_mvc/public/js/waveformplaylist/loader.js
Normal file
86
airtime_mvc/public/js/waveformplaylist/loader.js
Normal file
|
@ -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);
|
||||
}
|
16
airtime_mvc/public/js/waveformplaylist/local_storage.js
Normal file
16
airtime_mvc/public/js/waveformplaylist/local_storage.js
Normal file
|
@ -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;
|
||||
};
|
57
airtime_mvc/public/js/waveformplaylist/observer/observer.js
Normal file
57
airtime_mvc/public/js/waveformplaylist/observer/observer.js
Normal file
|
@ -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: []};
|
||||
}
|
57
airtime_mvc/public/js/waveformplaylist/observer/observer.js~
Normal file
57
airtime_mvc/public/js/waveformplaylist/observer/observer.js~
Normal file
|
@ -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: []};
|
||||
}
|
328
airtime_mvc/public/js/waveformplaylist/playlist.js
Normal file
328
airtime_mvc/public/js/waveformplaylist/playlist.js
Normal file
|
@ -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);
|
||||
};
|
||||
|
161
airtime_mvc/public/js/waveformplaylist/playout.js
Normal file
161
airtime_mvc/public/js/waveformplaylist/playout.js
Normal file
|
@ -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);
|
||||
};
|
||||
|
|
@ -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>
|
|
@ -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>
|
151
airtime_mvc/public/js/waveformplaylist/time_scale.js
Normal file
151
airtime_mvc/public/js/waveformplaylist/time_scale.js
Normal file
|
@ -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);
|
||||
};
|
||||
|
686
airtime_mvc/public/js/waveformplaylist/track.js
Normal file
686
airtime_mvc/public/js/waveformplaylist/track.js
Normal file
|
@ -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;
|
||||
};
|
||||
|
430
airtime_mvc/public/js/waveformplaylist/track_render.js
Normal file
430
airtime_mvc/public/js/waveformplaylist/track_render.js
Normal file
|
@ -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);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
Loading…
Add table
Add a link
Reference in a new issue