aboutsummaryrefslogtreecommitdiff
path: root/site/app/base
diff options
context:
space:
mode:
Diffstat (limited to 'site/app/base')
-rw-r--r--site/app/base/analyser.js97
-rw-r--r--site/app/base/base.js512
-rw-r--r--site/app/base/controls.js214
-rw-r--r--site/app/base/interop_work.deprecated.csd108
-rw-r--r--site/app/base/spline-edit.js453
-rw-r--r--site/app/base/waveform.js1076
6 files changed, 2460 insertions, 0 deletions
diff --git a/site/app/base/analyser.js b/site/app/base/analyser.js
new file mode 100644
index 0000000..ac55d42
--- /dev/null
+++ b/site/app/base/analyser.js
@@ -0,0 +1,97 @@
+var Analyser = function(type, twist, elContainer, csApp) {
+ var self = this;
+ var scopeNode = twirl.audioContext.createAnalyser();
+ var node;
+ var elCanvas = $("<canvas />").css({
+ position: "absolute",
+ width: "100%",
+ height: "100%",
+ top: "0px",
+ left: "0px"
+ }).addClass("twist_scope").appendTo(elContainer);
+ var context = elCanvas[0].getContext("2d");
+ var playing = false;
+
+ this.remove = function() {
+ type = null;
+ elContainer.remove();
+ };
+
+ function frequency() {
+ if (type != 0 || !playing) return;
+ let style = getComputedStyle(document.body);
+ let width = elCanvas[0].width = elContainer.width();
+ let height = elCanvas[0].height = elContainer.height();
+
+ let freqData = new Uint8Array(scopeNode.frequencyBinCount);
+ let scaling = height / 256;
+ scopeNode.getByteFrequencyData(freqData);
+ context.fillStyle = style.getPropertyValue("--waveformBgColor");
+ context.fillRect(0, 0, width, height);
+ context.lineWidth = 2;
+ context.strokeStyle = style.getPropertyValue("--waveformFgColor");
+ context.beginPath();
+
+ for (var x = 0; x < width; x++) {
+ context.lineTo(x, height - freqData[x] * scaling);
+ }
+ context.stroke();
+ requestAnimationFrame(frequency);
+ }
+
+ function oscilloscope() {
+ if (type != 1 || !playing) return;
+ let style = getComputedStyle(document.body);
+ let width = elCanvas[0].width = elContainer.width();
+ let height = elCanvas[0].height = elContainer.height();
+
+ let timeData = new Uint8Array(scopeNode.frequencyBinCount);
+ let scaling = height / 256;
+ let risingEdge = 0;
+ let edgeThreshold = 5;
+ scopeNode.getByteTimeDomainData(timeData);
+
+ context.fillStyle = style.getPropertyValue("--waveformBgColor");
+ context.fillRect(0, 0, width, height);
+ context.lineWidth = 2;
+ context.strokeStyle = style.getPropertyValue("--waveformFgColor");
+ context.beginPath();
+
+ while (timeData[risingEdge++] - 128 > 0 && risingEdge <= width);
+ if (risingEdge >= width) risingEdge = 0;
+
+ while (timeData[risingEdge++] - 128 < edgeThreshold && risingEdge <= width);
+ if (risingEdge >= width) risingEdge = 0;
+
+ for (var x = risingEdge; x < timeData.length && x - risingEdge < width; x++) {
+ context.lineTo(x - risingEdge, height - timeData[x] * scaling);
+ }
+ context.stroke();
+ requestAnimationFrame(oscilloscope);
+ }
+
+ this.setPlaying = function(state) {
+ playing = state;
+ if (playing) {
+ if (type == 0) {
+ frequency();
+ } else if (type == 1) {
+ oscilloscope();
+ }
+ }
+ };
+
+ this.setType = function(v) {
+ type = v;
+ self.setPlaying(playing);
+ };
+
+ async function boot() {
+ node = await csApp.getNode();
+ node.connect(scopeNode);
+ if (twist.isPlaying()) {
+ self.setPlaying(true);
+ }
+ }
+ boot();
+}; \ No newline at end of file
diff --git a/site/app/base/base.js b/site/app/base/base.js
new file mode 100644
index 0000000..8bf12f2
--- /dev/null
+++ b/site/app/base/base.js
@@ -0,0 +1,512 @@
+var CSApplication = function(appOptions) {
+ var self = this;
+ var version = 1.0;
+ var debug = window.location.protocol.startsWith("file");
+ var baseUrl;
+ if (debug) {
+ baseUrl = "https://apps.csound.1bpm.net";
+ } else {
+ baseUrl = window.location.origin;
+ }
+ var cbid = 0;
+ var callbacks = {};
+ var appPath;
+ var udoReplacements = {
+ "/interop.udo": "/interop.web.udo",
+ "/sequencing_melodic_persistence.udo": "/sequencing_melodic_persistence.web.udo"
+ };
+ var defaultStorage = {version: version};
+ var storage = localStorage.getItem("csound");
+ if (storage) {
+ storage = JSON.parse(storage);
+ if (!storage.version || storage.version != version) {
+ storage = defaultStorage;
+ }
+ } else {
+ storage = defaultStorage;
+ }
+
+ function saveStorage() {
+ localStorage.setItem("csound", JSON.stringify(storage));
+ }
+
+
+ const defaultOptions = {
+ csdUrl: null,
+ csOptions: null,
+ trackMouse: false,
+ trackTouch: false,
+ trackClick: false,
+ trackMouseSpeed: false,
+ trackTouchSpeed: false,
+ keyDownScore: null, // score line to insert with code as p4, or function passed event to return score line
+ keyUpScore: null,
+ onPlay: null,
+ onStop: null,
+ ioReceivers: null,
+ files: null
+ };
+ let csound = null;
+
+ function basename(path) {
+ return path.split("/").reverse()[0];
+ }
+
+ this.getCsound = function() {
+ return csound;
+ }
+
+ if (!appOptions) {
+ appOptions = defaultOptions;
+ } else {
+ for (var key in defaultOptions) {
+ if (!appOptions.hasOwnProperty(key)) {
+ appOptions[key] = defaultOptions[key];
+ }
+ }
+ }
+
+ async function copyDataToLocal(arrayBuffer, name) {
+ if (!csound) return;
+ const buffer = new Uint8Array(arrayBuffer);
+ await csound.fs.writeFile(name, buffer);
+ return Promise.resolve();
+ }
+
+ async function copyUrlToLocal(url, name) {
+ if (!csound) return;
+ const response = await fetch(url);
+ const arrayBuffer = await response.arrayBuffer();
+ await copyDataToLocal(arrayBuffer, name);
+ return Promise.resolve();
+ }
+
+ this.loadUrl = function(url, a2, a3) {
+ var name;
+ var func;
+ if (typeof(a2) == "function") {
+ name = basename(url);
+ func = a2;
+ } else {
+ name = a2;
+ func = a3;
+ }
+ copyUrlToLocal(url, name).then(() => {
+ if (func) func(name);
+ });
+ };
+
+ this.loadBuffer = function(buffer, a2, a3) {
+ var name;
+ var func;
+ if (typeof(a2) == "function") {
+ name = basename(url);
+ func = a2;
+ } else {
+ name = a2;
+ func = a3;
+ }
+ copyDataToLocal(buffer, name).then(() => {
+ if (func) func(name);
+ });
+ }
+
+ function runCallback(data) {
+ if (!callbacks[data.cbid]) {
+ return;
+ }
+ callbacks[data.cbid].func(data);
+ if (callbacks.hasOwnProperty(data.cbid) && !callbacks[data.cbid].persist) {
+ self.removeCallback(data.cbid);
+ }
+ }
+
+ this.createCallback = function(func, persist) {
+ thisCbid = cbid;
+ callbacks[thisCbid] = {func: func, persist: persist};
+ if (cbid > 999999) {
+ cbid = 0;
+ } else {
+ cbid ++;
+ }
+ return thisCbid;
+ };
+
+ this.removeCallback = function(cbid) {
+ delete callbacks[cbid];
+ };
+
+ this.enableAudioInput = async function() {
+ if (!csound) return;
+ return (await csound.enableAudioInput());
+ };
+
+ this.unlinkFile = function(path) {
+ if (!csound) return;
+ csound.fs.unlink(path);
+ };
+
+ this.readFile = async function(path) {
+ if (!csound) return;
+ return (await csound.fs.readFile(path));
+ };
+
+ this.writeFile = async function(path, buffer) {
+ return (await csound.fs.writeFile(path, buffer));
+ };
+
+ this.setControlChannel = function(name, value) {
+ if (!csound) return;
+ csound.setControlChannel(name, value);
+ };
+
+ this.getControlChannel = async function(name) {
+ if (!csound) return;
+ return await csound.getControlChannel(name);
+ };
+
+ this.setStringChannel = function(name, value) {
+ if (!csound) return;
+ csound.setStringChannel(name, value);
+ };
+
+ this.getStringChannel = function(name) {
+ if (!csound) return;
+ return csound.getStringChannel(name);
+ };
+
+ this.getTable = function(tableNumber) {
+ if (!csound) return;
+ return csound.getTable(tableNumber);
+ };
+
+ this.compileOrc = async function(orc) {
+ if (!csound) return;
+ return await csound.compileOrc(orc);
+ }
+
+ function handleMessage(message) {
+ if (debug) console.log(message);
+ if (message.startsWith("callback ")) {
+ runCallback(JSON.parse(message.substr(9)));
+ } else if (appOptions.errorHandler && (message.startsWith("error: ") ||
+ message.startsWith("INIT ERROR ") ||
+ message.startsWith("PERF ERROR ") ||
+ message.startsWith("perf_error: "))) {
+ appOptions.errorHandler(message);
+ } else if (appOptions.ioReceivers) {
+ for (var k in appOptions.ioReceivers) {
+ if (message.startsWith(k + " ")) {
+ appOptions.ioReceivers[k](message.substr(k.length + 1));
+ return;
+ }
+ }
+ }
+ };
+
+ var urlExistsChecked = {};
+ function urlExists(url) {
+ return new Promise((resolve, reject) => {
+ if (urlExistsChecked.hasOwnProperty(url)) {
+ resolve(urlExistsChecked[url]);
+ } else {
+ fetch(url, {
+ method: "HEAD"
+ }).then(response => {
+ var exists = (response.status.toString()[0] === "2");
+ urlExistsChecked[url] = exists;
+ resolve(exists);
+ }).catch(error => {
+ reject(false);
+ });
+ }
+ });
+ }
+
+
+ function dirName(path) {
+ return path.substring(0, path.lastIndexOf("/"));
+ }
+
+ async function loadFiles(files) {
+ for (var i = 0; i < files.length; i++) {
+ await copyUrlToLocal(files[i].url, files[i].name);
+ }
+ };
+
+
+ function urlInFiles(url, files) {
+ for (var x in files) {
+ if (url == files[x].url) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ async function scanUdoUsage(url, files, soundCollections) {
+ const req = await fetch(url);
+ const text = await req.text();
+ var soundCollections;
+ var match;
+ var doSaveStorage = false;
+ if (!storage.knownUdoPaths) storage.knownUdoPaths = {};
+ var re = /sounddb_getcollection(|id) "(.*)"|#include "(.*)"/g;
+ do {
+ match = re.exec(text);
+ if (match) {
+ if (match[3] && !match[3].endsWith("sounddb.udo")) {
+ var udoPath;
+ var dopush = false;
+ if (udoReplacements.hasOwnProperty(match[3])) {
+ udoPath = baseUrl + "/udo/" + udoReplacements[match[3]];
+ if (!urlInFiles(udoPath)) {
+ dopush = true;
+ }
+ } else {
+ if (Object.keys(storage.knownUdoPaths).includes(match[3])) {
+ udoPath = storage.knownUdoPaths[match[3]];
+ if (!urlInFiles(udoPath, files)) dopush = true;
+ } else {
+ udoPath = baseUrl + ("/udo/" + match[3]).replaceAll("//", "/");
+ if (!urlInFiles(udoPath, files)) {
+ var exists = await urlExists(udoPath);
+ if (!exists) {
+ udoPath = appPath + ("/" + match[3]).replaceAll("//", "/");
+ }
+ storage.knownUdoPaths[match[3]] = udoPath;
+ doSaveStorage = true;
+ dopush = true;
+ }
+ }
+ }
+ if (dopush) {
+ files.push({url: udoPath, name: match[3]});
+ await scanUdoUsage(udoPath, files, soundCollections);
+ }
+ }
+ if (match[2] && !appOptions.manualSoundCollections) {
+ var collections = match[2].split(",");
+ for (var x in collections) {
+ if (!soundCollections.includes(collections[x])) {
+ soundCollections.push(collections[x]);
+ }
+ }
+ }
+ }
+ } while (match);
+
+ if (doSaveStorage) {
+ saveStorage();
+ }
+ }
+
+ this.getNode = async function() {
+ if (!csound) return;
+ return await csound.getNode();
+ }
+
+ this.play = async function(log, audioContext) {
+ if (!csound) {
+ if (log) log("Loading audio engine");
+ var csdBasename = basename(appOptions.csdUrl);
+ appPath = dirName(window.location.href);
+
+ if (csdBasename == appOptions.csdUrl) {
+ appOptions.csdUrl = appPath + "/" + appOptions.csdUrl;
+ }
+ const { Csound } = await import(baseUrl + "/code/csound.js");
+ var invokeOptions = {};
+ if (audioContext) {
+ invokeOptions.audioContext = audioContext;
+ }
+ csound = await Csound(invokeOptions);
+ await csound.setDebug(0);
+ csound.on("message", handleMessage);
+ await csound.setOption("-m0");
+ await csound.setOption("-d");
+ await csound.setOption("--env:INCDIR=/");
+ await csound.setOption("-odac");
+ await csound.setOption("--omacro:WEB=1");
+ if (appOptions.csOptions) {
+ for (var i = 0; i < appOptions.csOptions.length; i++) {
+ await csound.setOption(appOptions.csOptions[i]);
+ }
+ }
+
+ if (log) log("Preparing application code");
+ var files = [{url: appOptions.csdUrl, name: csdBasename}];
+ var soundCollections = [];
+ await scanUdoUsage(appOptions.csdUrl, files, soundCollections);
+
+ if (appOptions.files) {
+ appOptions.files.forEach(function(f){
+ files.push({url: f, name: f});
+ });
+ }
+
+ if (soundCollections.length > 0) {
+ if (log) log("Preparing application sounds");
+ var udoUrl = baseUrl + "/sound/collection_udo.py?collections=" + soundCollections.join(",");
+ files.push({url: udoUrl, name: "sounddb.udo"});
+
+ const response = await fetch(baseUrl + "/sound/map.json");
+ const jdata = await response.json();
+ for (var i = 0; i < soundCollections.length; i++) {
+ var fdata = jdata[soundCollections[i]];
+ for (var j = 0; j < fdata.sounds.length; j++) {
+ var path = fdata.sounds[j].path;
+ if (!urlInFiles(path, files)) {
+ var fileObj = {url: path, name: path};
+ files.push(fileObj);
+ }
+ }
+ }
+ }
+ if (appOptions.onPlay) {
+ csound.on("play", appOptions.onPlay);
+ }
+
+ if (appOptions.onStop) {
+ csound.on("stop", appOptions.onStop);
+ }
+
+ if (log) log("Loading application files");
+ if (appOptions.files) {
+ for (var x in appOptions.files) {
+ files.push({url: appPath + "/" + appOptions.files[x], name: appOptions.files[x]});
+ }
+ }
+ await loadFiles(files);
+ if (log) log("Compiling application");
+ await csound.compileCsd(csdBasename);
+ await csound.start();
+ }
+
+ };
+
+ this.insertScoreAsync = async function(instr, extraArgs, duration, start) {
+ if (!duration) duration = 1;
+ if (!start) start = 0;
+
+ return new Promise((resolve, reject) => {
+ var cbid = self.createCallback(function(ndata){
+ resolve(ndata);
+ });
+ var args = [start, duration, cbid];
+ for (let e of extraArgs) {
+ args.push(e);
+ }
+ self.insertScore(instr, args);
+ });
+ };
+
+ this.insertScore = async function(instr, args) {
+ if (!csound) return;
+ if (!args) args = [0, 1];
+ var scoreline = "i"
+
+ function add(item) {
+ if (isNaN(item)) {
+ scoreline += "\"" + item + "\" ";
+ } else {
+ scoreline += item + " ";
+ }
+ }
+
+ add(instr);
+ if (typeof(args) == "function") {
+ [0, 1, self.createCallback(args)].forEach(add);
+ } else {
+ args.forEach(add);
+ }
+ csound.inputMessage(scoreline);
+ };
+
+ var speedTimestamp = null;
+ var lastPosX = null;
+ var lastPosY = null;
+
+ function setSpeedChannels(x, y) {
+ if (!csound) return;
+ if (speedTimestamp == null) {
+ speedTimestamp = Date.now();
+ lastPosX = x;
+ lastPosY = y;
+ return;
+ }
+ var now = Date.now();
+ var dt = now - speedTimestamp;
+ var dx = event.pageX - lastPosX;
+ var dy = event.pageY - lastPosY;
+ var speedX = Math.round(dx / dt * 100);
+ var speedY = Math.round(dy / dt * 100);
+ speedTimestamp = now;
+ lastPosX = x;
+ lastPosY = y;
+
+ csound.setControlChannel("speedX", speedX);
+ csound.setControlChannel("speedY", speedY);
+ }
+
+ function setPositionChannels(x, y) {
+ if (!csound) return;
+ csound.setControlChannel("mouseX", x / window.innerWidth);
+ csound.setControlChannel("mouseY", y / window.innerHeight);
+ }
+
+
+ if (appOptions.trackMouse || appOptions.trackMouseSpeed) {
+ document.addEventListener("mousemove", function(event) {
+ var timestamp = null;
+ var lastMouseX = null;
+ var lastMouseY = null;
+ if (appOptions.trackMouse) {
+ setPositionChannels(event.pageX, event.pageY);
+ }
+ if (appOptions.trackMouseSpeed) {
+ setSpeedChannels(event.pageX, event.pageY);
+ }
+ });
+ }
+
+ if (appOptions.trackTouch || appOptions.trackTouchSpeed) {
+ document.addEventListener("touchmove", function(event) {
+ var evt = (typeof event.originalEvent === "undefined") ? event : event.originalEvent;
+ var touch = evt.touches[0] || evt.changedTouches[0];
+ if (appOptions.trackTouch) {
+ setPositionChannels(touch.pageX, touch.pageY);
+ }
+ if (appOptions.trackTouchSpeed) {
+ setSpeedChannels(touch.pageX, touch.pageY);
+ }
+ });
+ }
+
+
+ if (appOptions.keyDownScore) {
+ document.addEventListener("keydown", function(event) {
+ if (!csound) return;
+ var scoreline = null;
+ if (typeof appOptions.keyDownScore == "function") {
+ scoreline = appOptions.keyDownScore(event);
+ } else {
+ scoreline = appOptions.keyDownScore + " " + event.code;
+ }
+ csound.inputMessage(scoreline);
+ });
+ }
+
+ if (appOptions.keyUpScore) {
+ document.addEventListener("keydown", function(event) {
+ if (!csound) return;
+ var scoreline = null;
+ if (typeof appOptions.keyUpScore == "function") {
+ scoreline = appOptions.keyUpScore(event);
+ } else {
+ scoreline = appOptions.keyUpScore + " " + event.code;
+ }
+ csound.inputMessage(scoreline);
+ });
+ }
+} \ No newline at end of file
diff --git a/site/app/base/controls.js b/site/app/base/controls.js
new file mode 100644
index 0000000..331f88b
--- /dev/null
+++ b/site/app/base/controls.js
@@ -0,0 +1,214 @@
+var Control = function(definition) {
+ var self = this;
+ var state = false;
+ var type;
+ var element;
+ var elTextInput;
+
+
+
+ if (!definition.hasOwnProperty("channel")) {
+ definition.channel = definition.name.toLowerCase();
+ }
+
+ if (!definition.hasOwnProperty("min")) {
+ definition.min = 0;
+ }
+
+ if (!definition.hasOwnProperty("max")) {
+ definition.max = 1;
+ }
+
+ if (!definition.hasOwnProperty("step")) {
+ definition.step = 0.000001;
+ }
+
+ if (!definition.hasOwnProperty("dfault")) {
+ definition.dfault = 0;
+ }
+
+ Object.defineProperty(this, "element", {
+ get: function() {return element;},
+ set: function(v) {}
+ });
+
+ Object.defineProperty(this, "textInputElement", {
+ get: function() {return elTextInput;},
+ set: function(v) {}
+ });
+
+ Object.defineProperty(this, "elementRow", {
+ get: function() {
+ var tr = $("<tr />");
+ $("<td />").text(definition.name).appendTo(tr);
+ $("<td />").append(element).appendTo(tr);
+ var tinput = $("<td />").appendTo(tr);
+ if (elTextInput) {
+ tinput.append(elTextInput);
+ }
+ return tr;
+ },
+ set: function(v) {}
+ });
+
+ Object.defineProperty(this, "value", {
+ get: function() {
+ var val;
+ if (type == "select") {
+ val = element.val();
+ if (definition.asValue) {
+ val = definition.options[val];
+ }
+ } else if (type == "range") {
+ val = element.val();
+ } else if (type == "checkbox") {
+ val = (element.is(":checked")) ? definition.max : definition.min;
+ } else if (type == "button") {
+ val = 0;
+ } else if (type == "toggle") {
+ val = (state) ? definition.max : definition.min;
+ }
+
+ return val;
+ },
+ set: function(v) {
+ if (type == "checkbox") {
+ element.prop("checked", v);
+ } else {
+ element.val(v);
+ }
+ }
+ });
+
+ async function createControl() {
+ if (definition.options) {
+ type = "select";
+ element = $("<select />");
+ for (var o in definition.options) {
+ $("<option />").val(o).text(definition.options[o]).appendTo(element);
+ }
+ } else if (definition.image) {
+ type = "range";
+ var baseurl = "https://apps.csound.1bpm.net/controls/";
+ var response = await fetch(baseurl + definition.image + ".json");
+ var json = await response.json();
+ var width;
+ var height;
+ if (definition.width) {
+ width = definition.width;
+ } else {
+ if (!json.cellw) json.cellw = 32;
+ width = json.cellw;
+ }
+ if (definition.height) {
+ height = definition.height;
+ } else {
+ height = json.cellh;
+ }
+
+ if (json.ctltype == 0) {
+ element = $("<input />").attr("type", "range").addClass("input-knob").attr("data-diameter", json.cellh).attr("data-src", baseurl + json.fn).attr("data-sprites", json.frames - 1).attr("data-height", height).attr("data-width", width);
+ type = "range";
+ } else if (json.ctltype == 1 || json.ctltype == 3) {
+ element = $("<input />").attr("type", "range").addClass("input-slider").attr("data-height", height).attr("data-src", baseurl + json.fn).attr("data-width", width).attr("data-sprites", json.frames - 1); //.css({width: width, height: height});
+ type = "range";
+ } else if (json.ctltype == 2) {
+ element = $("<input />").attr("type", "checkbox").addClass("input-switch").attr("data-height", height).attr("data-width", width).attr("data-src", baseurl + json.fn);
+ type = "checkbox";
+ }
+ } else {
+ if (!definition.type || definition.type == "range") {
+ type = "range";
+ element = $("<input />").attr("type", "range");
+ } else if (definition.type == "toggle") {
+ type = "toggle";
+ element = $("<button />").text(definition.labelOff);
+ } else if (definition.type == "button") {
+ type = "button";
+ element = $("<button />").text(definition.label);
+ }
+ }
+
+ if (type == "range") {
+ element.attr("min", definition.min).attr("max", definition.max).attr("step", definition.step);
+ if (!definition.noTextInput) {
+ elTextInput = $("<input />").attr("type", "number").attr("min", definition.min).attr("max", definition.max).attr("step", definition.step).change(function() {
+ change($(this).val());
+ });
+ }
+ }
+
+ if (definition.onContextMenu) {
+ element.on("contextmenu", definition.onContextMenu);
+ }
+
+ element.change(function() {
+ if (definition.onChange) {
+ definition.onChange(self.value, self);
+ }
+ if (definition.channel) {
+ sendHost();
+ }
+ });
+
+ element.on("input", function() {
+ if (type == "range") {
+ if (!definition.noSendOnInput) {
+ sendHost();
+ }
+ if (elTextInput) {
+ elTextInput.text(Math.round(self.value * 100) / 100);
+ }
+ }
+ });
+
+ function sendHost() {
+ if (window.app) {
+ app.setControlChannel(definition.channel, self.value);
+ }
+ }
+
+ function change(val) {
+ if (type == "select" || type == "range") {
+ element.val(val);
+ } else if (type == "checkbox") {
+ if (val == 1 || val == true) {
+ element.prop("checked", true);
+ } else {
+ element.prop("checked", false);
+ }
+ } else if (type == "toggle") {
+ if (val == 1 || val == true) {
+ state = true;
+ if (definition.labelOn) {
+ element.text(definition.labelOn);
+ }
+ if (definition.cssOn) {
+ element.css(definition.cssOn);
+ }
+ } else {
+ state = false;
+ if (definition.labelOff) {
+ element.text(definition.labelOff);
+ }
+ if (definition.cssOff) {
+ element.css(definition.cssOff);
+ }
+ }
+ }
+ }
+ change(definition.dfault);
+ if (!definition.noTriggerInit) {
+ element.trigger("change");
+ }
+
+ if (definition.target) {
+ definition.target.append(element);
+ }
+
+ if (definition.onReady) {
+ definition.onReady(self);
+ }
+ }
+ createControl();
+}; \ No newline at end of file
diff --git a/site/app/base/interop_work.deprecated.csd b/site/app/base/interop_work.deprecated.csd
new file mode 100644
index 0000000..8a12b18
--- /dev/null
+++ b/site/app/base/interop_work.deprecated.csd
@@ -0,0 +1,108 @@
+<CsoundSynthesizer>
+<CsOptions>
+-odac
+</CsOptions>
+<CsInstruments>
+sr = 48000
+ksmps = 64
+nchnls = 2
+0dbfs = 1
+seed 0
+
+
+
+#include "sequencing_melodic_persistence.udo"
+
+opcode jsio_sendraw, 0, S
+ Smessage xin
+ prints "CBDT "
+ prints Smessage
+ prints "\n"
+endop
+
+
+gijsio_collections[][] init 20, 300
+
+opcode jsio_getcollection
+ Scollection, SonComplete xin
+
+endop
+
+
+instr jsio_loadcollection
+ Sdata = strget(p4)
+
+endin
+
+
+
+
+
+gSjsioBuffer[] init 16
+gijsioBufferMax = 0
+gijsioBufferLock = 0
+
+
+
+
+instr jsio_bufferadd
+ icbid = p4
+ Sstatus = strget(p5)
+ Sdetails = strget(p6)
+ if (gijsioBufferLock == 1) then
+ schedule(p1, 0.1, 1, icbid, Sstatus, Sdetails)
+ else
+ gijsioBufferLock = 1
+ Sitem = sprintf("{\"cbid\":%d", icbid)
+ if (strcmp(Sstatus, "") != 0) then
+ Sitem = strcat(Sitem, sprintf(",\"status\":\"%s\"", Sstatus))
+ endif
+
+ if (strcmp(Sdetails, "") != 0) then
+ Sitem = strcat(Sitem, sprintf(",\"details\":\"%s\"", Sdetails))
+ endif
+
+ gSjsioBuffer[gijsioBufferMax] = strcat(Sitem, "}")
+ gijsioBufferMax += 1
+
+ gijsioBufferLock = 0
+ endif
+ turnoff
+endin
+
+
+instr jsio_bufferflush
+ Schannel = strget(p4)
+
+ if (gijsioBufferLock == 1) then
+ schedule(p1, 0.1, 1, Schannel)
+ else
+ gijsioBufferLock = 1
+ if (gijsioBufferMax == 0) then
+ chnset "", Schannel
+ gijsioBufferLock = 0
+ else
+ index = 0
+ Soutput = "["
+ while (index < gijsioBufferMax) do
+ if (index > 0) then
+ Soutput = strcat(Soutput, ",")
+ endif
+ Soutput = strcat(Soutput, gSjsioBuffer[index])
+ index += 1
+ od
+ Soutput = strcat(Soutput, "]")
+ chnset Soutput, Schannel
+ gijsioBufferMax = 0
+ gijsioBufferLock = 0
+ endif
+ endif
+ turnoff
+endin
+
+
+</CsInstruments>
+<CsScore>
+i "jsio_bufferadd" 0 1
+</CsScore>
+</CsoundSynthesizer> \ No newline at end of file
diff --git a/site/app/base/spline-edit.js b/site/app/base/spline-edit.js
new file mode 100644
index 0000000..06faa7e
--- /dev/null
+++ b/site/app/base/spline-edit.js
@@ -0,0 +1,453 @@
+var SplineEdit = function (elTarget, colour, duration, constraints, name) {
+ var self = this;
+ var targetName = "#" + elTarget.attr("id");
+ var svg;
+ var path;
+ var line = d3.line();
+ var yStep;
+ var selected;
+ var dragged;
+ var yScale = [constraints[0], constraints[1]];
+ var region = [0, 1];
+ this.changed = false;
+
+ Object.defineProperty(this, "width", {
+ get: function() { return parseFloat(elTarget.width()); },
+ set: function(x) {}
+ });
+
+ Object.defineProperty(this, "height", {
+ get: function() { return parseFloat(elTarget.height()); },
+ set: function(x) {}
+ });
+
+ if (constraints[3] > 0.0001) {
+ yStep = self.height / ((constraints[1] - constraints[0]) / constraints[3]);
+ }
+
+ var dfaultPos = ((constraints[2] - constraints[0]) / (constraints[1] - constraints[0]));
+ var rawPoints = [[0, dfaultPos], [1, dfaultPos]];
+ var pointRange = [0, 0];
+ var points = [...rawPoints];
+ var circlePoints = [];
+
+ this.getRawPoints = function() {
+ return rawPoints;
+ };
+
+ this.setRawPoints = function(p, noredraw) {
+ rawPoints = p;
+ if (!noredraw) self.redraw();
+ };
+
+ Object.defineProperty(this, "displayPoints", {
+ get: function() {
+ var output = [];
+ var x;
+ for (let p of points) {
+ output.push([p[0] * self.width, (1 - p[1]) * self.height]);
+ }
+ return output;
+ },
+ set: function(x) {}
+ });
+
+ this.resize = function(ratio, noredraw) {
+ for (var i in points) {
+ if (i != 0 && i != rawPoints.length - 1) {
+ rawPoints[i][0] = rawPoints[i][0] * ratio;
+ }
+ }
+ if (!noredraw) self.redraw();
+ };
+
+ function regionToAbsolute(xVal) {
+ return (xVal - region[0]) / (region[1] - region[0]);
+ }
+
+ function absoluteToRegion(xVal) {
+ return (xVal * (region[1] - region[0])) + region[0];
+ }
+
+
+ function interpolateRange(scaled, start, end) {
+ if (!start) start = region[0];
+ if (!end) end = region[1];
+ var scaling = 1 - (end - start);
+ var output = [];
+ var lastIndex;
+ var firstIndex = 0;
+ var lastIndex;
+ var yVal;
+ var xVal;
+ var allPoints = rawPoints;
+ if (start == 0 && end == 1) {
+ return {points: allPoints, indexes: [0, allPoints.length - 1]};
+ }
+
+ for (var i in allPoints) {
+ var x = allPoints[i][0];
+ if (x < start) {
+ firstIndex = parseInt(i);
+ }
+ }
+ for (var i = parseInt(allPoints.length - 1); i > firstIndex; i--) {
+ var x = allPoints[i][0];
+ if (x >= end) {
+ lastIndex = i;
+ }
+ }
+ for (var i = firstIndex; i <= lastIndex; i++) {
+ var v = allPoints[i];
+
+ xVal = (i == lastIndex) ? end : v[0];
+
+ if (i == firstIndex && v[0] != start) {
+ var next = allPoints[parseInt(i + 1)];
+ //yVal = v[1] + (start - v[0]) * (next[1] - v[1]) / (next[0] - v[0]);
+ yVal = v[1] + (v[0] - start) * (next[1] - v[1]) / (next[0] - v[0]);
+ } else if (i == lastIndex && v[0] != end) {
+ var last = allPoints[parseInt(i - 1)];
+ yVal = last[1] + (end - last[0]) * (v[1] - last[1]) / (v[0] - last[0]);
+ } else {
+ yVal = v[1];
+ }
+ if (scaled) xVal = regionToAbsolute(xVal);
+ output.push([xVal, yVal]);
+
+ }
+ return {points: output, indexes: [firstIndex, lastIndex]};
+ }
+
+ function getIndex(point) {
+ for (var i = 0; i < points.length; i ++) {
+ if (points[i][0] == point[0] && points[i][1] == point[1]) {
+ return i;
+ }
+ }
+ }
+
+
+ function getDuration() {
+ if (typeof(duration) == "function") {
+ return duration();
+ } else {
+ return duration;
+ }
+ }
+
+ var redrawing = false;
+ this.redraw = function() {
+ if (redrawing) return;
+ redrawing = true;
+ build();
+ redraw();
+ redrawing = false;
+ };
+
+ function setRangePoints() {
+ /*
+ if (points.length == rawPoints.length) {
+ //rawPoints.length = 0;
+ for (var i in points) {
+ //rawPoints[i] = [regionToAbsolute(points[i][0]), points[i][1]];
+ }
+ }*/
+ var res = interpolateRange(true);
+ pointRange = res.indexes;
+ points.length = 0;
+ circlePoints.length = 0;
+ for (let i in res.points) {
+ points.push(res.points[i]);
+ if ((region[0] == 0 && region[1] == 1)
+ || ((i != 0 || region[0] == 0) && (i != res.points.length - 1 || region[1] == 1)))
+ {
+ circlePoints.push(res.points[i]);
+ }
+ }
+ }
+
+ this.setRange = function(start, end) {
+ if (start != null) region[0] = start;
+ if (end != null) region[1] = end;
+ self.redraw();
+ };
+
+ this.setYScale = function(min, max) { // TODO unused
+ if (!min && !max) {
+ yScale = [constraints[0], constraints[1]];
+ }
+ };
+
+ this.setDuration = function(val) {
+ duration = val;
+ };
+
+ this.setConstraints = function(val) {
+ constraints = val;
+ }
+
+ function calculateValue(xy, asDuration) {
+ var x = xy[0];
+ if (asDuration) {
+ x *= getDuration();
+ }
+ var y = ((constraints[1] - constraints[0]) * xy[1]) + constraints[0];
+ return [x, y];
+ }
+
+ this.getData = function() {
+ var output = [];
+ var val;
+ var allPoints = rawPoints;
+ for (var p of allPoints) {
+ val = calculateValue(p, true);
+ output.push([Math.round(val[0] * 1000) / 1000, Math.round(val[1] * 1000) / 1000]);
+ }
+ return output;
+ };
+
+ this.getLinsegData = function(start, end, nullOnEmpty) {
+ if (nullOnEmpty && rawPoints.length == 2) return;
+ var duration = getDuration(); //(end - start) * getDuration(); // IF we are having dynamic area view
+ var rounding = 10000;
+ var output = [];
+ var lastTime = 0;
+ var time;
+ var lastIndex;
+ var firstIndex;
+ var lastIndex;
+ var allPoints = rawPoints;
+
+ for (var i in allPoints) {
+ var x = allPoints[i][0];
+ if (x <= start) {
+ firstIndex = parseInt(i);
+ }
+ }
+ for (var i = parseInt(allPoints.length - 1); i > firstIndex; i--) {
+ var x = allPoints[i][0];
+ if (x >= end) {
+ lastIndex = i;
+ }
+ }
+ for (var i = firstIndex; i <= lastIndex; i++) {
+ var v = calculateValue(allPoints[i], false);
+ if (i != firstIndex) {
+
+ time = (((i == lastIndex) ? end : v[0]) - start) * duration;
+ output.push((Math.round((time - lastTime) * rounding)) / rounding);
+ lastTime = time;
+ }
+
+ if (i == firstIndex && v[0] != start) {
+ var next = calculateValue(allPoints[parseInt(i + 1)], false);
+ //var interpVal = v[1] + (v[0] - start) * (next[1] - v[1]) / (next[0] - v[0]);
+ var interpVal = v[1] + (start - v[0]) * (next[1] - v[1]) / (next[0] - v[0]);
+ output.push(Math.round(interpVal * rounding) / rounding);
+ } else if (i == lastIndex && v[0] != end) {
+ var last = calculateValue(allPoints[parseInt(i - 1)], false);
+
+ var interpVal = last[1] + (end - last[0]) * (v[1] - last[1]) / (v[0] - last[0]);
+ //var interpVal = last[1] + (last[0] - end) * (v[1] - last[1]) / (v[0] - last[0]);
+ output.push(Math.round(interpVal * rounding) / rounding);
+ } else {
+ output.push(Math.round(v[1] * rounding) / rounding);
+ }
+
+ }
+ return output.join(",");
+ }
+
+ function getLabel(d) {
+ var val = calculateValue([absoluteToRegion(d[0]), d[1]], true);
+ var label = (Math.round(val[0] * 100) / 100) + "s<br/>" + (Math.round(val[1] * 1000) / 1000);
+ if (name) {
+ label = name + "<br/>" + label;
+ }
+ return label;
+ }
+
+ function redraw() {
+ setRangePoints();
+ path.datum(self.displayPoints);
+ svg.select("path").attr("d", line);
+ const circle = svg.selectAll("g").data(circlePoints, function(point){
+ return [point[0] * self.width, (1 - point[1]) * self.height];
+ });
+ circle.enter().append("g").call(
+ g => g.append("circle").attr("r", 0).attr("stroke", colour).attr("stroke-width", 1).attr("r", 7)
+ ).merge(circle).attr("transform", function(d){
+ return "translate(" + (d[0] * self.width) + "," + ((1 - d[1]) * self.height) + ")";
+ }).select("circle:last-child").attr("fill", function(d) {
+ if (selected && d[0] == selected[0] && d[1] == selected[1]) {
+ return "#000000";
+ } else {
+ return colour;
+ }
+ }).on("mouseover", function(event, d) {
+ twirl.tooltip.show(event, getLabel(d), colour);
+ }).on("mouseout", function(event) {
+ twirl.tooltip.hide();
+ }).on("dblclick", function(event, d) {
+ return; // TODO: not working properly particularly when zoomed
+ if (!window.twirl) return;
+ var index = getIndex(d);
+ var duration = getDuration();
+ console.log(d, regionToAbsolute(d[0]));
+ var minTime = ((points[index - 1]) ? absoluteToRegion(points[index - 1] + 0.00001) : 0) * duration;
+ var maxTime = ((points[index + 1]) ? absoluteToRegion(points[index + 1] - 0.00001) : 1) * duration;
+ var el = $("<div />");
+ var tb = $("<tbody />");
+ $("<table />").append(tb).appendTo(el);
+ var tpTime = new twirl.transform.Parameter({
+ host: null,
+ definition: {
+ name: "Time",
+ min: minTime,
+ max: maxTime,
+ dfault: d[0]
+ }
+ });
+ var tpValue = new twirl.transform.Parameter({
+ host: null,
+ definition: {
+ name: name,
+ min: constraints[0],
+ max: constraints[1],
+ dfault: d[1]
+ }
+ });
+
+ tb.append(tpTime.getElementRow(true)).append(tpValue.getElementRow(true));
+ twirl.prompt.show(el, function(){
+ d[0] = regionToAbsolute(tpTime.getValue())
+ d[1] = tpValue.getValue();
+ self.changed = true;
+ });
+ //d[0] = 0.4; d[1] = 0.4;
+ });
+ circle.exit().remove();
+ }
+
+ function build() {
+ line.curve(d3.curveLinear);
+ if (path) {
+ path.remove();
+ delete path;
+ }
+ if (svg) {
+ svg.remove();
+ delete svg;
+ }
+ svg = d3.select(targetName).append("svg").attr("width", self.width).attr("height", self.height);
+ svg.append("rect").attr("width", self.width).attr("height", self.height).attr("fill", "none");
+ path = svg.append("path").attr("fill", "none").attr("stroke", colour).attr("line-width", "3px"); //.call(redraw); // .datum(points)
+
+ svg.call(
+ d3.drag().subject(function(event){
+ let pos = event.sourceEvent.target.__data__;
+ var index;
+ var y;
+ if (!pos) {
+ var index;
+ for (var i = 0; i < rawPoints.length; i++) {
+ if (event.x > regionToAbsolute(rawPoints[i][0]) * self.width && rawPoints[i + 1] && event.x < regionToAbsolute(rawPoints[i + 1][0]) * self.width) {
+ index = i + 1;
+ if (yStep) {
+ y = Math.round(event.y / yStep) * yStep;
+ } else {
+ y = event.y;
+ }
+ //pos = [absoluteToRegion(event.x / self.width), 1 - (y / self.height)];
+ pos = [absoluteToRegion(event.x / self.width), 1 - (y / self.height)];
+ break;
+ }
+ }
+ if (index) {
+ var tmp = rawPoints.slice(0, index);
+ tmp.push(pos);
+ var newPoints = tmp.concat(rawPoints.slice(index));
+ rawPoints.length = 0;
+ Array.prototype.push.apply(rawPoints, newPoints);
+ redraw();
+ }
+ } else if (pos[0] == 0) {
+ index = 0;
+ } else if (pos[0] == 1) {
+ index = rawPoints.length - 1;
+ } else {
+ var p0;
+ var p1;
+ pos[0] = absoluteToRegion(pos[0]);
+ for (var p = 0; p < rawPoints.length; p++) {
+ p1 = (rawPoints[p + 1]) ? rawPoints[p + 1][0] : 1;
+ if (pos[0] != rawPoints[p][0]) {
+ p0 = rawPoints[p][0];
+ } else if (rawPoints[p + 1] && pos[0] == rawPoints[p + 1][0]) {
+ continue;
+ }
+
+ if (pos[0] > p0 && pos[0] < p1) {
+ index = p;
+ break;
+ }
+ }
+ }
+ return {pos: pos, index: index};
+ }).on("start", function(event) {
+ selected = event.subject.pos;
+ redraw();
+ }).on("drag", function(event) {
+ if (!event.subject) return;
+ if (!event.subject.hasOwnProperty("index")) return;
+ self.changed = true;
+ var val;
+ var pos;
+ if (event.subject.index != 0 && event.subject.index != rawPoints.length - 1) {
+ if (rawPoints[event.subject.index - 1] && event.x < regionToAbsolute(rawPoints[event.subject.index - 1][0]) * self.width) {
+ pos = rawPoints[event.subject.index - 1][0] + 0.00001;
+ } else if (rawPoints[event.subject.index - 1] && event.x > regionToAbsolute(rawPoints[event.subject.index + 1][0]) * self.width) {
+ pos = rawPoints[event.subject.index + 1][0] - 0.00001;
+ } else {
+ pos = absoluteToRegion(event.x / self.width);
+ }
+
+ pos = Math.max(0, Math.min(1, pos));
+ event.subject.pos[0] = pos;
+ }
+ if (yStep) {
+ val = Math.round(event.y / yStep) * yStep;
+ } else {
+ val = event.y;
+ }
+ val = 1 - (val / self.height);
+ event.subject.pos[1] = Math.max(0, Math.min(1, val));
+ rawPoints[event.subject.index][0] = event.subject.pos[0];
+ rawPoints[event.subject.index][1] = event.subject.pos[1];
+ redraw();
+ })
+ );
+ svg.node().focus();
+ }
+
+ d3.select(window).on("keydown", function(event){
+ if (!selected) return;
+ switch (event.key) {
+ case "Backspace":
+ case "Delete": {
+ event.preventDefault();
+ var i = rawPoints.indexOf(selected);
+ if (i != 0 && i != points.length - 1) {
+ rawPoints.splice(i, 1);
+ selected = rawPoints.length ? rawPoints[i > 0 ? i - 1 : 0] : null;
+ self.changed = true;
+ redraw();
+ }
+ }
+ }
+ });
+
+ build()
+ self.setRange(0, 1);
+};
diff --git a/site/app/base/waveform.js b/site/app/base/waveform.js
new file mode 100644
index 0000000..c6276f1
--- /dev/null
+++ b/site/app/base/waveform.js
@@ -0,0 +1,1076 @@
+var Waveform = function(options) {
+ var self = this;
+ var elTarget;
+ if (typeof(options.target) == "string") {
+ elTarget = $("#" + options.target);
+ } else {
+ elTarget = options.target;
+ }
+ var elContainerOuter = $("<div />").css({position: "absolute", width: "100%", height: "100%"}).appendTo(elTarget);
+ var elContainer = $("<div />").css({cursor: "text", position: "absolute", width: "100%", bottom: "0px", top: "0px", left: "0px"}).appendTo(elContainerOuter);
+ var elTip = $("<div />").css({position: "fixed", "font-size": "var(--fontSizeLarge)", color: "var(--fgColor1)", "text-shadow": "0px 0px 5px var(--bgColor1)", "z-index": 12}).appendTo(elContainer);
+ var elCanvases = [];
+ var elTimeBar;
+ var elPlayhead;
+ var elCrossfades = [];
+ var elMarkersRunner;
+ var crossFadeRatios = [];
+ var selected = [0, 1, -1];
+ var onSelects = [];
+ var channels;
+ var wavedata = null;
+ var regionStart = 0;
+ var regionEnd = 1;
+ var duration = 1;
+ var stereoSelectRatio = 0.2
+ var dragData = {};
+ this.markers = [];
+ var selectionMarkers = [];
+ var hasContent = false;
+ this.onRegionChange = null;
+
+ this.getRegion = function() {
+ return [regionStart, regionEnd];
+ };
+
+ function absPosToDisplayPos(x) {
+ var pos = (x - regionStart) / (regionEnd - regionStart);
+ return (pos >= 0 && pos <= 1) ? pos : null;
+ }
+
+ function displayPosToAbsPos(x) {
+ return ((regionEnd - regionStart) * x) + regionStart;
+ }
+
+ function getDisplaySelected() {
+ var hasSelection = (selected[0] != selected[1]);
+ var start = absPosToDisplayPos(selected[0]);
+ //if (start == null) start = 0;
+ var end = absPosToDisplayPos(selected[1]);
+ //if (end == null) end = 1;
+
+ if (start && !end) end = 1;
+ if (!start && end) start = 0;
+ if (!start && !end) {
+ if (hasSelection) {
+ start = 0;
+ end = 1;
+ } else {
+ start = end = 0;
+ }
+ }
+ return [
+ start,
+ end,
+ selected[2]
+ ];
+ }
+
+ if (!options) {
+ options = {};
+ }
+
+ if (options.hasOwnProperty("onSelect")) {
+ onSelects.push(options.onSelect);
+ }
+
+ if (options.hasOwnProperty("duration")) {
+ duration = options.duration;
+ }
+
+ if (!options.hasOwnProperty("latencyCorrection")) {
+ options.latencyCorrection = 0;
+ }
+
+ if (!options.hasOwnProperty("showcrossfades")) {
+ options.showcrossfades = false;
+ }
+
+ if (!options.hasOwnProperty("showGrid")) {
+ options.showGrid = true;
+ }
+
+ if (!options.hasOwnProperty("drawStyle")) {
+ options.drawStyle = "bar"; //"linebar";
+ }
+
+ if (!options.hasOwnProperty("allowSelect")) {
+ options.allowSelect = true;
+ }
+
+ if (options.allowSelect) {
+ elContainer.mousedown(mouseDownHandler).dblclick(mouseDoubleClickHandler).on("mousemove", function(e) {
+ if (channels != 2) return;
+ var ratio = (e.clientY - elContainer.offset().top) / parseFloat(elContainer.height());
+ var shown = false;
+ var text;
+ if (ratio > 1 - stereoSelectRatio) {
+ shown = true;
+ text = "R";
+ } else if (ratio < stereoSelectRatio) {
+ shown = true;
+ text = "L";
+ }
+ if (shown) {
+ elTip.show().css({left: (e.clientX + 10) + "px", top: (e.clientY - 5) + "px"}).text(text);
+ } else {
+ elTip.hide();
+ }
+ }).on("mouseleave", function(){
+ elTip.hide();
+ });
+ }
+
+ var elCover = $("<div />").css({position: "absolute", width: "100%", height: "100%", "background-color": "var(--waveformCoverColor)", opacity: "var(--waveformCoverOpacity)", display: "none", "z-index": 10}).appendTo(elContainerOuter);
+
+ var Marker = function(data, identifier) {
+ var mself = this;
+ var elMarkers;
+ var headerWidth = 14;
+ var headerHeight = 15;
+ var elLine;
+ var elHeader;
+ var drag
+ var position;
+ var onMarkerChange;
+
+ Object.defineProperty(this, "position", {
+ get: function() { return position; },
+ set: function(x) {
+ setPosition(x);
+ }
+ });
+
+ Object.defineProperty(this, "identifier", {
+ get: function() { return identifier; },
+ set: function(x) {
+ identifier = x;
+ }
+ });
+
+ if (typeof(data) == "number") {
+ position = data;
+ } else if (data.preset) {
+ if (data.preset == "selectionstart") {
+ identifier = "";
+ selectionMarkers[0] = mself;
+ onMarkerChange = function() {
+ if (position > selectionMarkers[1].position) {
+ self.alterSelection(selectionMarkers[1].position, position, null, true);
+ } else {
+ self.alterSelection(position, null, null, true);
+ }
+ };
+ onSelects.push(function(start, regionStart, end) {
+ setPosition(start);
+ });
+ } else if (data.preset == "selectionend") {
+ identifier = "";
+ selectionMarkers[1] = mself;
+ onMarkerChange = function() {
+ if (position < selectionMarkers[0].position) {
+ self.alterSelection(position, selectionMarkers[0].position, null, true);
+ } else {
+ self.alterSelection(null, position, null, true);
+ }
+ };
+ onSelects.push(function(start, regionStart, end) {
+ setPosition(end);
+ });
+ }
+ } else {
+ position = data.position;
+ if (data.hasOwnProperty("identifier")) {
+ identifier = data.identifier;
+ }
+
+ if (data.hasOwnProperty("onChange")) {
+ onMarkerChange = data.onChange;
+ }
+
+ }
+
+ function setPosition(displayPos) {
+ if (!hasContent) return;
+ if (displayPos != null) {
+ position = displayPosToAbsPos(displayPos);
+ } else {
+ displayPos = absPosToDisplayPos(position);
+ }
+ if (!elLine) {
+ elLine = $("<div />").appendTo(elContainer).addClass("waveform_marker").css({position: "absolute",
+ height: "100%", top: "0px", width: "2px", "background-color": "var(--waveformMarkerColor)", "z-index": 11
+ });
+ }
+ if (!elHeader) {
+ elHeader = $("<div />").appendTo(elMarkersRunner).addClass("waveform_marker").css({position: "absolute",
+ height: "100%", top: "0px", width: headerWidth + "px", "background-color": "var(--waveformMarkerColor)", "border-bottom-left-radius": headerWidth + "px", "border-bottom-right-radius": headerWidth + "px", "z-index": 8,
+ "text-align": "center", "font-family": "sans-serif, Arial", "font-size": "8pt", "font-weight": "bold", cursor: "move", "user-select": "none"
+ }).mousedown(handleMousedown);
+ }
+
+ if (displayPos == null) {
+ elLine.hide();
+ elHeader.hide();
+ } else {
+ var posx = elContainer.width() * displayPos;
+ elLine.show().css("left", posx + "px");
+ posx = (posx - (headerWidth / 2)) + 1;
+ elHeader.show().css("left", posx + "px").text(identifier);
+ }
+ }
+
+ this.redraw = function() {
+ if (!hasContent) return;
+ if (position < regionStart || position > regionEnd) {
+ if (elLine) elLine.hide();
+ if (elHeader) elHeader.hide();
+ } else {
+ setPosition();
+ elLine.show();
+ elHeader.show();
+ }
+ }
+
+
+ function handleMousedown(e) {
+ if (!hasContent) return;
+ var pageX = e.pageX;
+ var offset = elMarkersRunner.offset();
+ var width = elMarkersRunner.width();
+
+ function handleDrag(e) {
+ var pos = ((e.pageX - pageX) + (pageX - offset.left)) / width;
+
+ if (pos <= 1 && pos >= 0) {
+ setPosition(pos);
+ if (onMarkerChange) {
+ onMarkerChange(pos);
+ }
+ }
+ }
+ function handleMouseUp(e) {
+ $("body").off("mousemove", handleDrag).off("mouseup", handleMouseUp);
+ }
+ $("body").on("mouseup", handleMouseUp).on("mousemove", handleDrag);
+ }
+
+ setPosition();
+ }; // end marker
+
+
+ if (options.hasOwnProperty("markers")) {
+ elContainer.css("top", "15px");
+ elMarkersRunner = $("<div />").appendTo(elContainerOuter).css({position: "absolute", width: "100%", height: "15px", top: "0px", left: "0px", "background-color": "var(--waveformMarkerRunnerColor)"});
+ if (typeof(options.markers) == "object") {
+ var id = 1;
+ for (let m of options.markers) {
+ self.markers.push(new Marker(m, id++));
+ }
+ }
+ }
+
+
+ if (options.timeBar) {
+ elContainer.css({overflow: "hidden", bottom: "20px"});
+ var elTimeBarOuter = $("<div />").appendTo(elContainerOuter).css({position: "absolute", width: "100%", height: "20px", bottom: "0px", left: "0px", "background-color": "var(--waveformTimeBarBgColor)"});
+ var elTimeBarIcons = $("<div />").appendTo(elTimeBarOuter).css({position: "absolute", width: "80px", height: "100%", bottom: "0px", left: "0px"});
+ var elTimeBarContainer = $("<div />").appendTo(elTimeBarOuter).css({position: "absolute", right: "0px", height: "100%", bottom: "0px", left: "80px", "background-color": "var(--waveformTimeBarBgColor)"}).click(handleTimeBarTrackClick);
+
+ elTimeBar = $("<div />").appendTo(elTimeBarContainer).css({position: "absolute", right: "0px", height: "16px", top: "2px", left: "0px", "background-color": "var(--waveformTimeBarFgColor)"}).mousedown(handleTimeBarMousedown);
+
+ elTimeBarIcons.append(twirl.createIcon({
+ label: "Zoom selection",
+ size: 20,
+ icon: "zoomSelection",
+ click: function() {
+ self.zoomSelection()
+ }
+ }).el);
+
+ elTimeBarIcons.append(twirl.createIcon({
+ label: "Zoom in",
+ size: 20,
+ icon: "zoomIn",
+ click: function() {
+ self.zoomIn()
+ }
+ }).el);
+
+ elTimeBarIcons.append(twirl.createIcon({
+ label: "Zoom out",
+ size: 20,
+ icon: "zoomOut",
+ click: function() {
+ self.zoomOut()
+ }
+ }).el);
+
+ elTimeBarIcons.append(twirl.createIcon({
+ label: "Show all",
+ size: 20,
+ icon: "showAll",
+ click: function() {
+ self.setRegion(0, 1);
+ }
+ }).el);
+
+
+ function setTimeBarPosition(displayLeft, displayRight, setRegion) {
+ if (displayLeft >= 0 && displayRight >= 0) {
+ elTimeBar.css({left: displayLeft, right: displayRight});
+ var w = elTimeBarContainer.width();
+ if (setRegion) {
+ regionStart = displayLeft / w;
+ regionEnd = 1 - (displayRight / w);
+ if (self.onRegionChange) {
+ self.onRegionChange([regionStart, regionEnd]);
+ }
+ }
+ }
+ }
+
+ function handleTimeBarTrackClick(event) {
+ var increment = 20;
+ var apos = event.pageX - elTimeBarContainer.offset().left;
+ var left = parseInt(elTimeBar.css("left"));
+ var right = parseInt(elTimeBar.css("right"));
+ var tbWidth = parseInt(elTimeBar.css("width"));
+ if (apos < left) {
+ left -= increment;
+ right += increment;
+ } else if (apos > left + tbWidth) {
+ left += increment;
+ right -= increment;
+ } else {
+ return;
+ }
+ setTimeBarPosition(left, right, true);
+ draw();
+ }
+
+ function handleTimeBarMousedown(e) {
+ if (!hasContent) return;
+ var pageX = e.pageX;
+ var offset = elTimeBarContainer.offset();
+ var cWidth = elTimeBarContainer.width();
+ var tbWidth = elTimeBar.width();
+ var sLeft = pageX - offset.left - parseInt(elTimeBar.css("left"));
+
+ function handleDrag(e) {
+ var left = ((e.pageX - pageX) + (pageX - offset.left));
+ left = left - sLeft;
+ var end = left + tbWidth;
+ var right = cWidth - end;
+ setTimeBarPosition(left, cWidth - end, true);
+ draw(4); //draw(15);
+
+ }
+
+ function handleMouseOut(e) {
+ handleMouseUp(e);
+ }
+
+ function handleMouseUp(e) {
+ $("body").off("mousemove", handleDrag).off("mouseup", handleMouseUp).off("mouseleave", handleMouseOut);
+ function ensureDraw() {
+ if (drawing) return setTimeout(ensureDraw, 20);
+ draw();
+ }
+ ensureDraw();
+ }
+ $("body")
+ .on("mouseup", handleMouseUp)
+ .on("mousemove", handleDrag)
+ .on("mouseleave", handleMouseOut);
+ }
+ }
+
+ this.getDuration = function() {
+ return duration;
+ };
+
+ this.destroy = function() {
+ elTarget.remove();
+ };
+
+ this.show = function() {
+ elTarget.show();
+ };
+
+ this.hide = function() {
+ elTarget.hide();
+ };
+
+ this.cover = function(state) {
+ if (state) {
+ elCover.show();
+ } else {
+ elCover.hide();/*
+ setTimeout(function() {
+ elCover.hide();
+ }, options.latencyCorrection);*/
+ }
+ };
+
+ this.setOptions = function(o) {
+ Object.assign(options, o);
+ };
+
+
+ function drawCrossFades() {
+ if (!hasContent) return;
+ if (!options.showcrossfades) return;
+ if (elCrossfades.length == 0) {
+ for (var x = 0; x < 2; x++) {
+ elCrossfades.push($("<div />").css({
+ position: "absolute",
+ width: "1px",
+ "z-index": 9,
+ "line-width": "1px",
+ "height": "var(--waveformCrossfadeWidth)",
+ "background-color": "var(--waveformCrossfadeLineColor)"
+ }).appendTo(elContainer));
+ }
+ }
+ var containerHeight = elContainer.height();
+ var containerWidth = elContainer.width();
+ var displaySelected = getDisplaySelected();
+
+
+ function drawCrossfade(index) {
+ var thickness = 1;
+ var ratio = crossFadeRatios[index];
+ if (ratio == 0) {
+ elCrossfades[index].hide();
+ }
+
+ if (index == 0) {
+ var x1 = displaySelected[0] * containerWidth;
+ var y1 = containerHeight;
+ var x2 = x1 + (ratio * ((displaySelected[1] - displaySelected[0]) * containerWidth));
+ var y2 = 0;
+ } else {
+ var x1 = displaySelected[1] * containerWidth;;
+ var y1 = containerHeight;
+ var x2 = x1 - (ratio * ((displaySelected[1] - displaySelected[0]) * containerWidth));
+ var y2 = 0;
+ }
+ var length = Math.sqrt(((x2-x1) * (x2-x1)) + ((y2-y1) * (y2-y1)));
+ var centrex = ((x1 + x2) / 2) - (length / 2);
+ var centrey = ((y1 + y2) / 2) - (thickness / 2);
+ var angle = Math.atan2((y1-y2),(x1-x2))*(180/Math.PI);
+ elCrossfades[index].show().css({transform: "rotate(" + angle + "deg)", left: centrex, top: centrey, width: length + "px"});
+ }
+ drawCrossfade(0);
+ drawCrossfade(1);
+
+ };
+
+ this.alterSelection = function(start, end, channel, noOnSelects) {
+ if (!hasContent) return;
+ if (start != null) {
+ selected[0] = start;
+ }
+ if (end != null) {
+ selected[1] = end;
+ }
+
+ var displaySelected = getDisplaySelected();
+
+ if (channel == null) {
+ channel = selected[2];
+ } else {
+ selected[2] = channel;
+ }
+
+ var elWidth = elContainer.width();
+ var left = displaySelected[0] * elWidth;
+ var width = (displaySelected[1] * elWidth) - left;
+
+ if (dragData.selection) {
+ dragData.selection.css({
+ left: left,
+ width: width
+ });
+ }
+
+ if (dragData.location) {
+ dragData.location.css({
+ left: left
+ });
+ }
+
+ if (!noOnSelects && onSelects) {
+ for (let onSelect of onSelects) {
+ onSelect(start, regionStart, end, regionEnd, self);
+ }
+ }
+ drawCrossFades();
+ }
+
+ this.setSelection = function(start, end, channel) {
+ if (!hasContent) return;
+ if (!end) {
+ end = start;
+ }
+ self.alterSelection(start, end, channel);
+ };
+
+
+ function selectionMade() {
+ if (!hasContent) return;
+ var cWidth = elContainer.width();
+ var left = parseFloat(dragData.selection.css("left"));
+ var width = parseFloat(dragData.selection.css("width"));
+ var start = left / cWidth;
+ var end = (left + width) / cWidth;
+
+ selected = [
+ displayPosToAbsPos(start),
+ displayPosToAbsPos(end),
+ dragData.channel
+ ];
+
+ if (onSelects) {
+ for (let onSelect of onSelects) {
+ onSelect(start, regionStart, end, regionEnd, self);
+ }
+ }
+ drawCrossFades();
+ }
+
+ function createSelectionArea(e, leftOverride, widthOverride) {
+ var left = (leftOverride != null ) ? leftOverride : e.pageX - dragData.offset.left;
+
+ var containerHeight = parseFloat(elContainer.height());
+ var yratio = (e.pageY - dragData.offset.top) / containerHeight;
+ var heightmult = 1;
+ var topaugment = 0;
+ dragData.channel = -1;
+ if (channels == 2) {
+ if (yratio > 1 - stereoSelectRatio) {
+ heightmult = 0.5;
+ topaugment = containerHeight * 0.5;
+ dragData.channel = 1;
+ } else if (yratio < stereoSelectRatio) {
+ heightmult = 0.5;
+ dragData.channel = 0;
+ }
+ }
+ var width = (widthOverride != null) ? widthOverride : "0px";
+
+ if (dragData && dragData.selection) {
+ dragData.selection.remove();
+ }
+
+ dragData.selection = $("<div />")
+ .addClass("waveformSelection")
+ .css({
+ left: left,
+ top: topaugment,
+ position: "absolute",
+ opacity: "var(--waveformSelectOpacity)",
+ backgroundColor: "var(--waveformSelectColor)",
+ height: (100 * heightmult) + "%", //containerHeight * heightmult,
+ width: width,
+ "pointer-events": "none",
+ "z-index": 10
+ }).appendTo(elContainer);
+
+ if (dragData && dragData.location) {
+ dragData.location.remove();
+ }
+ dragData.location = $("<div />")
+ .addClass("waveformLocation")
+ .css({
+ left: left,
+ top: 0,
+ position: "absolute",
+ opacity: "var(--waveformSelectOpacity)",
+ backgroundColor: "var(--waveformLocationColor)",
+ width: "0px",
+ border: "1px solid black",
+ height: "100%", //elContainer.height(),
+ "pointer-events": "none",
+ "z-index": 11
+ }).appendTo(elContainer);
+ }
+
+ function mouseDoubleClickHandler(e) {
+ if (!hasContent) return;
+ dragData.pageX = 0;
+ dragData.offset.left = 0;
+ //dragData.pageXend = elContainer.width(); // TODO redundant
+ createSelectionArea(e, 0, elContainer.width());
+ selectionMade();
+ }
+
+ function mouseDownHandler(e) {
+ if (!hasContent) return;
+ var tolerancePx = 4;
+ if (!e.shiftKey) {
+ dragData.pageX = e.pageX;
+ dragData.pageY = e.pageY;
+ //dragData.pageXend = 0; // TODO redundant
+ dragData.elem = this;
+ dragData.offset = $(this).offset();
+ createSelectionArea(e);
+ dragData.shifted = false;
+ } else {
+ dragData.shifted = true;
+ handleDrag(e);
+ }
+
+ var elWidth = elContainer.width();
+ /*if (options.showcrossfades && elCrossfades.length != 0) {
+ elCrossfades[0].hide();
+ elCrossfades[1].hide();
+ }*/ // TODO redundant
+
+ function handleDrag(e) {
+ var origin = dragData.pageX - dragData.offset.left;
+ var dragPos = (e.pageX - dragData.pageX);
+ if (dragPos >= 0) {
+ if (dragPos + origin > elWidth) {
+ dragPos = elWidth;
+ }
+ if (dragPos <= tolerancePx) dragPos = 0;
+ dragData.selection.css({
+ left: origin + "px",
+ width: dragPos + "px"
+ });
+ } else {
+ var dpos = dragPos + origin;
+ var left;
+ var width;
+ if (dpos <= 0) {
+ left = 0;
+ width = origin;
+ } else {
+ left = dpos;
+ width = Math.abs(dragPos);
+ }
+ if (width <= tolerancePx) width = 0;
+ dragData.selection.css({
+ left: left + "px",
+ width: width + "px"
+ });
+ }
+ }
+
+ function handleMouseUp(e) {
+ $("body")
+ .off("mousemove", handleDrag)
+ .off("mouseup", handleMouseUp)
+ .off("mouseleave", handleMouseOut);
+ if (!hasContent) return;
+ //dragData.pageXend = e.pageX; // TODO redundant
+ selectionMade();
+ }
+
+ function handleMouseOut(e) {
+ if (e.clientX > $("body").width()) {
+ var left = parseFloat(dragData.selection.css("left"));
+ dragData.selection.css({width: (elContainer.width() - left) + "px"});
+ }
+ handleMouseUp(e);
+ }
+
+ $("body")
+ .on("mouseup", handleMouseUp)
+ .on("mousemove", handleDrag)
+ .on("mouseleave", handleMouseOut);
+ }
+
+ var lastDrawOneValueXpos;
+
+ this.resetDrawOneValue = function() {
+ lastDrawOneValueXpos = null;
+ };
+
+ function drawOneValue(x, values) {
+ var style = getComputedStyle(document.body);
+ var bgColour = (options.hasOwnProperty("bgColor")) ? options.bgColour : style.getPropertyValue("--waveformBgColor");
+ var fgColour = (options.hasOwnProperty("fgColor")) ? options.fgColour : style.getPropertyValue("--waveformFgColor");
+
+ function drawCanvas(canvasIndex, val) {
+ if (!val) return;
+ var elCanvas = elCanvases[canvasIndex];
+ let width = elCanvas.width();
+ let height = elCanvas.height();
+ let ctx = elCanvas[0].getContext("2d");
+ var lineWidth = 1;
+ if (lastDrawOneValueXpos) {
+ lineWidth = x - lastDrawOneValueXpos;
+ ctx.fillStyle = bgColour;
+ ctx.fillRect(lastDrawOneValueXpos, 0, lineWidth, height);
+ ctx.strokeStyle = fgColour;
+ }
+ lastDrawOneValueXpos = x;
+ ctx.lineWidth = lineWidth;
+ ctx.lineCap = "round";
+ ctx.fillStyle = fgColour;
+
+ ctx.beginPath();
+ val = (val + 1) * 0.5;
+ var posY0 = (val * height);
+ var posY1 = (height - posY0);
+ ctx.moveTo(x, posY0);
+ ctx.lineTo(x, posY1);
+ ctx.closePath();
+ ctx.stroke();
+ }
+
+ drawCanvas(0, values[0]);
+ if (values.length == 2) {
+ drawCanvas(1, values[1]);
+ }
+ }
+
+ this.movePlayhead = function(xratio, monitorValues) {
+ if (!hasContent) return;
+ setTimeout(function() {
+ var displayPos = absPosToDisplayPos(xratio);
+ if (!displayPos || displayPos < 0 || displayPos > 1) {
+ if (elPlayhead) {
+ elPlayhead.remove();
+ elPlayhead = null;
+ }
+ return;
+ }
+ var width = elContainer.width();
+ var left = Math.min(width * displayPos, width - 1);
+
+ if (monitorValues) {
+ drawOneValue(left, monitorValues);
+ }
+
+ if (!elPlayhead) {
+ elPlayhead = $("<div />")
+ .addClass("waveformPlayhead")
+ .css({
+ left: left,
+ top: 0,
+ position: "absolute",
+ backgroundColor: "var(--waveformPlayheadColor)",
+ width: "0px",
+ border: "1px solid var(--waveformPlayheadColor)",
+ height: "100%",
+ "pointer-events": "none",
+ "z-index": 13
+ }).appendTo(elContainer);
+ } else {
+ elPlayhead.css({left: left + "px"});
+ }
+ }, options.latencyCorrection);
+ };
+
+ var drawing = false;
+ async function draw(efficiency) {
+ if (!hasContent || !wavedata || wavedata.length == 0) return;
+ if (drawing) return;
+ drawing = true;
+
+ if (!efficiency) efficiency = 1;
+
+ if (elCanvases.length == 0) {
+ for (var i in wavedata) {
+ var height;
+ var top;
+ if (wavedata.length == 1) {
+ top = "0px";
+ height = "100%";
+ } else {
+ height = "50%";
+ if (i == 0) {
+ top = "0px";
+ } else {
+ top = "50%";
+ }
+ }
+ elCanvases[i] = $("<canvas />").css({position: "absolute", width: "100%", height: height, top: top, left: "0px"}).addClass("waveform_canvas").appendTo(elContainer);
+ }
+ }
+
+ for (m of self.markers) {
+ m.redraw();
+ }
+ self.alterSelection(null, null, null, true); // redraw selection and xfades
+
+
+ async function drawCanvas(canvasIndex) {
+ var elCanvas = elCanvases[canvasIndex]; //.empty();
+ elCanvas[0].width = elContainer.width();
+ elCanvas[0].height = elContainer.height() / wavedata.length;
+ let width = elCanvas.width();
+ let height = elCanvas.height();
+ let ctx = elCanvas[0].getContext("2d");
+ var wavelength;
+ var access;
+ if (typeof(wavedata[canvasIndex]) == "function") {
+ wavelength = await wavedata[canvasIndex](-1);
+ access = wavedata[canvasIndex];
+ } else {
+ wavelength = wavedata[0].length;
+ access = async function(index) {
+ return wavedata[0][index];
+ };
+ }
+
+ var start = Math.round(regionStart * wavelength);
+ var end = Math.round(regionEnd * wavelength);
+ var regionLength = Math.round((regionEnd - regionStart) * wavelength);
+ var indexStep = (regionLength / width) * efficiency;
+ var widthStep = (indexStep < 1) ? parseInt(width / regionLength) : 1;
+ widthStep = parseInt(Math.max(1, widthStep) * efficiency);
+ indexStep = parseInt(indexStep);
+
+ var style = getComputedStyle(document.body);
+ var bgColour = (options.hasOwnProperty("bgColor")) ? options.bgColor : style.getPropertyValue("--waveformBgColor");
+ var fgColour = (options.hasOwnProperty("fgColor")) ? options.fgColor : style.getPropertyValue("--waveformFgColor");
+ var val;
+ ctx.fillStyle = bgColour;
+ ctx.fillRect(0, 0, width, height);
+ ctx.strokeStyle = fgColour;
+
+ if (options.drawStyle == "line") {
+ ctx.lineCap = "butt";
+ ctx.lineWidth = 1;
+ ctx.beginPath();
+ ctx.moveTo(0, height * 0.5);
+ for (var x = 0, i = start; x < width; x+=widthStep, i+=indexStep) {
+ val = await access(i);
+ val = (val + 1) * 0.5;
+ ctx.lineTo(x, (val * height) );
+ }
+ ctx.closePath();
+ ctx.stroke();
+ } else if (options.drawStyle == "bar") {
+ ctx.lineWidth = widthStep;
+ ctx.lineCap = "round";
+ ctx.fillStyle = fgColour;
+ ctx.beginPath();
+
+ for (var x = 0, i = start; x < width; x+=widthStep, i+=indexStep) {
+ val = await access(i);
+ val = (val + 1) * 0.5;
+ var posY0 = (val * height);
+ var posY1 = (height - posY0);
+ ctx.moveTo(x, posY0);
+ ctx.lineTo(x, posY1);
+ }
+ ctx.closePath();
+ ctx.stroke();
+ } else if (options.drawStyle == "linebar") {
+ ctx.lineWidth = 1;
+ ctx.fillStyle = fgColour;
+ ctx.lineCap = "butt";
+
+ ctx.beginPath();
+ ctx.moveTo(0, height * 0.5);
+ xindex = 0;
+
+ var vals = [];
+ for (var x = 0, i = start; x < width; x+=widthStep, i+=indexStep) {
+ val = await access(i);
+ val = (val + 1) * 0.5;
+ vals[i] = val;
+ var posY0 = (val * height);
+ ctx.lineTo(x, posY0);
+ }
+
+ ctx.lineTo(width, (height * 0.5));
+
+ for ( ; x >= 0; x-=widthStep, i-=indexStep) {
+ var posY1 = (height - (vals[i] * height));
+ ctx.lineTo(x, posY1);
+ }
+
+ ctx.lineTo(0, height * 0.5);
+ ctx.fill();
+ ctx.closePath();
+ ctx.stroke();
+ } else {
+ console.log("Invalid drawStyle");
+ } // end drawing waveform
+
+ if (options.showGrid){
+ var lineSpacing = 50;
+ var lineNum = width / lineSpacing;
+ var position;
+
+ ctx.lineCap = "butt";
+ ctx.lineWidth = 1;
+ for (var x = 0; x < lineNum; x++) {
+ ctx.beginPath();
+ var left = x * lineSpacing;
+ ctx.strokeStyle = ctx.fillStyle = style.getPropertyValue("--waveformGridColor");
+ ctx.moveTo(left, 0);
+ ctx.lineTo(left, height);
+ ctx.stroke();
+
+ if ((canvasIndex == 0 && elCanvases.length == 1) ||
+ (canvasIndex == 1 && elCanvases.length == 2)) {
+
+ ctx.strokeStyle = ctx.fillStyle = style.getPropertyValue("--waveformGridTextColor");
+ positionLabel = duration * (regionStart + (((regionEnd - regionStart) / lineNum) * x));
+ ctx.fillText(Math.round(positionLabel * 1000 ) / 1000, left + 2, height - 2);
+ }
+ }
+
+ } // if showgrid
+
+ if (canvasIndex == 0 && elCanvases.length == 2) {
+ ctx.beginPath();
+ ctx.lineCap = "butt";
+ ctx.lineWidth = 1;
+ ctx.strokeStyle = style.getPropertyValue("--waveformChannelLineColor");
+ ctx.fillStyle = ctx.strokeStyle;
+ ctx.moveTo(0, height - 1);
+ ctx.lineTo(width, height - 1);
+ ctx.closePath();
+ ctx.stroke();
+ }
+ } // end drawCanvas
+
+ var drawCompletes = [];
+ for (let i in elCanvases) {
+ drawCompletes.push(false);
+ drawCanvas(i).then(function(){
+ drawCompletes[i] = true;
+ for (let c of drawCompletes) {
+ if (!c) {
+ return;
+ }
+ }
+ drawing = false;
+ });
+ }
+ }
+
+ this.redraw = function() {
+ draw();
+ };
+
+ Object.defineProperty(this, "crossFadeInRatio", {
+ get: function() { return crossFadeRatios[0]; },
+ set: function(v) {
+ crossFadeRatios[0] = v;
+ drawCrossFades();
+ }
+ });
+
+ Object.defineProperty(this, "crossFadeOutRatio", {
+ get: function() { return crossFadeRatios[1]; },
+ set: function(v) {
+ crossFadeRatios[1] = v;
+ drawCrossFades();
+ }
+ });
+
+ Object.defineProperty(this, "selected", {
+ get: function() { return selected; },
+ set: function(x) { }
+ });
+
+ Object.defineProperty(this, "duration", {
+ get: function() { return duration; },
+ set: function(x) { }
+ });
+
+ Object.defineProperty(this, "channels", {
+ get: function() { return channels; },
+ set: function(x) { }
+ });
+
+ Object.defineProperty(this, "regionStart", {
+ get: function() { return regionStart; },
+ set: function(x) {
+ self.setRegion(x, regionEnd);
+ }
+ });
+
+ Object.defineProperty(this, "showGrid", {
+ get: function() { return options.showGrid; },
+ set: function(x) {
+ options.showGrid = x;
+ draw();
+ }
+ });
+
+ Object.defineProperty(this, "regionEnd", {
+ get: function() { return regionEnd; },
+ set: function(x) {
+ self.setRegion(regionStart, x);
+ }
+ });
+
+
+ this.zoomSelection = function() {
+ if (!dragData || !dragData.location) return;
+ dragData.location.css("left", "0px");
+ self.setRegion(selected[0], selected[1]);
+ };
+
+ this.zoomOut = function() {
+ self.setRegion(regionStart * 0.9, regionEnd * 1.1);
+ };
+
+ this.zoomIn = function() {
+ self.setRegion(regionStart * 1.1, regionEnd * 0.9);
+ };
+
+ this.setRegion = function(start, end) {
+ if (!hasContent) return;
+ if (end <= start) return;
+ if (end > 1) end = 1;
+ if (start < 0) start = 0;
+ regionStart = start;
+ regionEnd = end;
+ draw();
+ if (elTimeBar) {
+ var elTbcw = elTimeBarContainer.width();
+ elTimeBar.css({left: (regionStart * elTbcw) + "px", right: ((1 - regionEnd) * elTbcw) + "px"});
+ }
+ if (self.onRegionChange) {
+ self.onRegionChange([regionStart, regionEnd]);
+ }
+ };
+
+ this.setData = function(data, nduration, noRedraw) {
+ hasContent = true;
+ wavedata = data; // should be array
+ if (channels != data.length) {
+ for (var i in elCanvases) {
+ elCanvases[i].remove();
+ }
+ delete elCanvases[i];
+ elCanvases.length = 0;
+ }
+ channels = data.length;
+ duration = (nduration) ? nduration : 1;
+ if (!noRedraw) {
+ draw();
+ }
+ };
+
+ var lastSize = [];
+ function handleResize() {
+ if (!hasContent) return;
+ var width = elContainer.width();
+ var height = elContainer.height();
+
+ if (lastSize[0] = width && lastSize[1] == height) return;
+ lastSize = [width, height];
+
+ if (dragData && dragData.selection) {
+ selectionMade();
+ }
+ draw();
+ }
+
+ if (!options.noResizeHandler) {
+ window.addEventListener("resize", handleResize);
+ }
+}