aboutsummaryrefslogtreecommitdiff
path: root/site/app/twist/twist.js
diff options
context:
space:
mode:
authorRichard <q@1bpm.net>2025-04-13 18:48:02 +0100
committerRichard <q@1bpm.net>2025-04-13 18:48:02 +0100
commit9fbf91db06a6d4f4b5cd8bb45389a731bb86bf22 (patch)
tree291bd79ce340e67affa755a8a6b4f6a83cce93ea /site/app/twist/twist.js
downloadapps.csound.1bpm.net-9fbf91db06a6d4f4b5cd8bb45389a731bb86bf22.tar.gz
apps.csound.1bpm.net-9fbf91db06a6d4f4b5cd8bb45389a731bb86bf22.tar.bz2
apps.csound.1bpm.net-9fbf91db06a6d4f4b5cd8bb45389a731bb86bf22.zip
initial
Diffstat (limited to 'site/app/twist/twist.js')
-rw-r--r--site/app/twist/twist.js1248
1 files changed, 1248 insertions, 0 deletions
diff --git a/site/app/twist/twist.js b/site/app/twist/twist.js
new file mode 100644
index 0000000..a7a248c
--- /dev/null
+++ b/site/app/twist/twist.js
@@ -0,0 +1,1248 @@
+var OperationWatchdog = function(twist) {
+ var self = this;
+ var active = false;
+ var lastValues = [true, true];
+ var firstActive = true;
+ var checkInterval;
+ var timeoutTime = 30000;
+ var alivetimeoutTime = 3500;
+ var context;
+
+ function crash() {
+ self.stop();
+ twirl.sendErrorState({text: "Unhandled exception in " + context});
+ var el = $("#twist_crash").show();
+ var elSr = $("#twist_crash_recovery");
+
+ function doomed() {
+ elSr.empty().append($("<h4 />").text("Sorry, unfortunately your work cannot be saved."));
+ }
+
+ var doomedTimeout = setTimeout(doomed, 6000);
+
+ var cbid = app.createCallback(function(ndata) {
+ if (doomedTimeout) clearTimeout(doomedTimeout);
+
+ if (!ndata.left && !ndata.right) {
+ return doomed();
+ }
+ elSr.empty();
+ var text;
+ var linkLeft = $("<a />").attr("href", "#").text("Download").click(function(e){
+ e.preventDefault();
+ twist.downloadFile("/crashL.wav");
+ });
+ if (ndata.left && !ndata.right) {
+ elSr.append($("<h4 />").text("Your work has been recovered:"));
+ elSr.append(linkLeft);
+ } else {
+ elSr.append($("<h4 />").text("Your work has been recovered as separate left/right channels:"));
+ linkLeft.text("Download left channel").appendTo(elSr);
+ elSr.append("<br />");
+ var linkRight = $("<a />").attr("href", "#").text("Download right channel").click(function(e){
+ e.preventDefault();
+ twist.downloadFile("/crashR.wav");
+ }).appendTo(elSr);
+ }
+
+ });
+ app.getCsound().compileOrc("iwrittenL = 0\niwrittenR = 0\nif (gitwst_bufferL[gitwst_instanceindex] > 0) then\niwrittenL ftaudio gitwst_bufferL[gitwst_instanceindex], \"/crashL.wav\", 14\nendif\nif (gitwst_bufferR[gitwst_instanceindex] > 0) then\niwrittenR ftaudio gitwst_bufferR[gitwst_instanceindex], \"/crashR.wav\", 14\nendif\nio_sendstring(\"callback\", sprintf(\"{\\\"cbid\\\":" + cbid + ",\\\"left\\\":%d,\\\"right\\\":%d}\", iwrittenL, iwrittenR))\n");
+ }
+
+ function checkAlive() {
+ var alive = false;
+ var aliveTimeout = setTimeout(crash, alivetimeoutTime);
+ var cbid = app.createCallback(function(){
+ clearTimeout(aliveTimeout);
+ alive = true;
+ });
+ app.insertScore("twst_checkalive", [0, 1, cbid]);
+ }
+
+ this.start = function(startContext) {
+ active = true;
+ context = startContext;
+ firstActive = true;
+ lastValues = [true, true];
+ if (checkInterval) clearInterval(checkInterval);
+ checkInterval = setInterval(function() {
+ if (lastValues[0] === lastValues[1]) {
+ checkAlive();
+ }
+ }, timeoutTime);
+ };
+
+ this.setActive = function(value) {
+ if (!active) return;
+ if (firstActive) {
+ firstActive = false;
+ } else {
+ lastValues[0] = lastValues[1];
+ }
+ lastValues[1] = value;
+ };
+
+ this.stop = function() {
+ active = false;
+ firstActive = true;
+ lastValues = [true, true];
+ if (checkInterval) clearInterval(checkInterval);
+ };
+};
+
+var Twist = function() {
+ twirl.init();
+ var self = this; // TODO deprecate this in favour of below
+ var twist = this;
+ this.storage = localStorage.getItem("twist");
+ if (self.storage) {
+ self.storage = JSON.parse(self.storage);
+ } else {
+ self.storage = {
+ dcblockoutputs: 1,
+ tanhoutputs: 1,
+ maxundo: 2,
+ showShortcuts: 1,
+ commitHistoryLevel: 16,
+ scopeType: 0
+ };
+ }
+
+ twist.version = 1;
+ this.currentTransform = null;
+ var errorState;
+ var instanceIndex = 0;
+ this.waveforms = [];
+ var waveformFiles = [];
+ var waveformTabs = [];
+ var waveformLoaded = [];
+ this.playheadInterval = null;
+ var playing = false;
+ var auditioning = false;
+ var recording = false;
+ this.onPlays = [];
+ this.onInstanceChangeds = [];
+ this.operationLog = [];
+ var sr = 44100;
+ var undoLevels = [];
+ var onSave;
+ this.visible = false;
+ this.playbackLoop = false;
+ this.twine = null;
+ this.hasClipboard = false;
+ this.watchdog = new OperationWatchdog(twist);
+ this.ui = new TwistUI(twist);
+
+ this.setPlaying = function(state) {
+ if (playing == state) return;
+ playing = state;
+ for (var o of twist.onPlays) {
+ o(playing, auditioning, recording);
+ }
+ if (twist.currentTransform) {
+ twist.currentTransform.setPlaying(state);
+ }
+ twist.ui.setPlaying(state);
+
+ if (!state) {
+ twist.watchdog.stop();
+ twist.waveform.movePlayhead(0);
+ if (twist.playheadInterval) {
+ clearInterval(twist.playheadInterval);
+ }
+ }
+ };
+
+ this.saveStorage = function() {
+ localStorage.setItem("twist", JSON.stringify(twist.storage));
+ };
+
+ this.lastOperation = function() {
+ return twist.operationLog[twist.operationLog.length - 1];
+ };
+
+ this.clearOperationLog = function() {
+ twist.operationLog = [];
+ };
+
+ async function pushOperationLog(operation, logChannels) {
+ var max = twist.storage.commitHistoryLevel;
+ if (!max) {
+ twist.storage.commitHistoryLevel = max = 16;
+ }
+ if (twist.operationLog.length + 1 >= max) {
+ twist.operationLog.shift();
+ }
+ if (logChannels) {
+ if (!operation.channels) operation.channels = {};
+ for (let c of logChannels) {
+ operation.channels[c] = await app.getControlChannel(c);
+ }
+ }
+ twist.operationLog.push(operation);
+ }
+
+ this.createNewInstance = function(noShowLoadNew) {
+ var element = $("<div />").addClass("waveform").appendTo("#twist_waveforms");
+ let index = waveformFiles.length;
+
+ if (index < 0) index = 0;
+ waveformTabs.push(
+ $("<td />").text("New file").click(function() {
+ if (twist.isPlaying()) return;
+ twist.waveform = index;
+ }).addClass("wtab_selected").appendTo("#twist_waveform_tabs")
+ );
+ undoLevels.push(0);
+ var waveform = new Waveform({
+ target: element,
+ latencyCorrection: twirl.latencyCorrection,
+ showcrossfades: true,
+ crossFadeWidth: 1,
+ timeBar: true,
+ markers: [
+ {preset: "selectionstart"},
+ {preset: "selectionend"},
+ ]
+ })
+ waveform.onRegionChange = function(region) {
+ if (twist.currentTransform) {
+ twist.currentTransform.redraw(region);
+ }
+ };
+ twist.waveforms.push(waveform);
+ if (!noShowLoadNew) twist.ui.showLoadNewPrompt();
+ twist.waveform = index;
+ for (let o of twist.onInstanceChangeds) {
+ o(true, index);
+ }
+ };
+
+
+ function removeInstance(i) {
+ if (!i) i = instanceIndex;
+ if (twist.waveforms.length == 1 || i < 0 || i > twist.waveforms.length - 1) {
+ return;
+ }
+ twist.waveforms[i].destroy();
+ delete twist.waveforms[i];
+ waveformTabs[i].remove();
+ waveformLoaded[instanceIndex] = false;
+ delete waveformTabs[i]
+ if (instanceIndex == i) {
+ instanceIndex = i + ((i == 0) ? 1 : -1);
+ twist.waveform.show();
+ }
+ for (let o of twist.onInstanceChangeds) {
+ o(false, i);
+ }
+ }
+
+ this.closeInstance = function(i) {
+ removeInstance(i);
+ };
+
+
+
+ this.errorHandler = async function(text, onComplete) {
+ var errorObj = {
+ lastOperation: twist.lastOperation()
+ };
+ if (twist.currentTransform) {
+ var state = await twist.currentTransform.getState();
+ errorObj.transformState = state;
+ }
+
+ twirl.errorHandler(text, onComplete, errorObj);
+ twist.setPlaying(false);
+ };
+
+ function playPositionHandler(noPlayhead, onComplete, monitorChannels) {
+ function callback(ndata) {
+ if (ndata.status == 1) { // playing
+ twist.setPlaying(true);
+ if (!noPlayhead) {
+ twist.watchdog.start("audition");
+ if (twist.playheadInterval) {
+ clearInterval(twist.playheadInterval);
+ }
+ twist.playheadInterval = setInterval(async function(){
+ var val = await app.getControlChannel("twst_playposratio");
+ twist.watchdog.setActive(val);
+ if (val < 0 || val > 1) {
+ clearInterval(twist.playheadInterval);
+ }
+
+ var monitorValues;
+ if (monitorChannels) {
+ monitorValues = [];
+ monitorValues.push((monitorChannels[0]) ? await app.getControlChannel(monitorChannels[0]) : null);
+ monitorValues.push((monitorChannels[1]) ? await app.getControlChannel(monitorChannels[1]) : null);
+ } else {
+ monitorValues = null;
+ }
+ twist.waveform.movePlayhead(val, monitorValues);
+ }, 50);
+ }
+ return;
+ }
+ // stopped
+ app.removeCallback(ndata.cbid);
+
+ if (twist.playbackLoop && ndata.status == 0 && onComplete) {
+ return onComplete(ndata);
+ }
+ twist.setPlaying(false);
+
+ if (ndata.status == -1) {
+ var container = $("<div />");
+ $("<p />").text("Not enough processing power to transform in realtime").appendTo(container);
+ var lagHintHtml = twist.currentTransform.getLagHints();
+ if (lagHintHtml) {
+ $("<p />").html(lagHintHtml).appendTo(container);
+ }
+
+ return twirl.prompt.show(container);
+ } else if (ndata.status == 2) { // record complete
+ globalCallbackHandler(ndata);
+ }
+ if (onComplete) onComplete(ndata);
+
+ }
+ return app.createCallback(callback, true);
+ }
+
+ function operation(options) {
+ var s = (options.selection) ? options.selection : twist.waveform.selected;
+ errorState = "Operation error";
+ if (options.showLoading) {
+ twist.ui.setLoadingStatus(true);
+ }
+ var cbid;
+ if (!options.onComplete || typeof(options.onComplete) == "function") {
+ cbid = app.createCallback(function(ndata) {
+ twist.waveform.cover(false);
+ if (options.onComplete) {
+ options.onComplete(ndata);
+ } else if (ndata.status && ndata.status <= 0) {
+ var text;
+ if (ndata.status == -2) {
+ text = "Resulting file would be too large";
+ }
+ twist.errorHandler(text);
+ }
+ if (options.showLoading) {
+ twist.ui.setLoadingStatus(false);
+ }
+ });
+ } else {
+ cbid = options.onComplete;
+ }
+ if (!options.noLogScript) {
+ pushOperationLog({
+ type: "operation",
+ instr: options.instr,
+ name: options.name,
+ selection: s,
+ instanceIndex: instanceIndex
+ }, options.logScriptChannels);
+ }
+ app.insertScore(options.instr, [0, 1, cbid, s[0], s[1], s[2], (options.noCheckpoint) ? 1 : 0]);
+ }
+
+ this.isPlaying = function() {
+ return playing;
+ };
+
+ this.redraw = function() {
+ if (twist.currentTransform) {
+ twist.currentTransform.redraw();
+ }
+ for (let w of twist.waveforms) {
+ w.redraw();
+ }
+ };
+
+ this.undo = function() {
+ if (playing) return;
+ twist.waveform.cover(true);
+ operation({
+ instr: "twst_undo",
+ name: "Undo",
+ onComplete: globalCallbackHandler,
+ showLoading: true,
+ noLogScript: true
+ });
+ };
+
+ this.cut = function() {
+ if (playing) return;
+ twist.waveform.cover(true);
+ operation({
+ instr: "twst_cut",
+ name: "Cut",
+ onComplete: globalCallbackHandler,
+ showLoading: true,
+ });
+ twist.hasClipboard = true;
+ };
+
+ this.trim = function() {
+ if (playing) return;
+ twist.waveform.cover(true);
+ operation({
+ instr: "twst_trim",
+ name: "Trim",
+ onComplete: globalCallbackHandler,
+ showLoading: true,
+ });
+ };
+
+ this.delete = function() {
+ if (playing) return;
+ twist.waveform.cover(true);
+ operation({
+ instr: "twst_delete",
+ name: "Delete",
+ onComplete: globalCallbackHandler,
+ showLoading: true,
+ });
+ };
+
+ this.copy = function() {
+ if (playing) return;
+ twist.waveform.cover(true);
+ operation({
+ instr: "twst_copy",
+ name: "Copy",
+ showLoading: true,
+ });
+ twist.hasClipboard = true;
+ };
+
+ this.paste = function() {
+ if (playing) return;
+ twist.waveform.cover(true);
+ operation({
+ instr: "twst_paste",
+ name: "Paste",
+ onComplete: globalCallbackHandler,
+ showLoading: true,
+ });
+ };
+
+ this.moveToNextTransient = function() {
+ if (playing) return;
+ var cbid = app.createCallback(globalCallbackHandler);
+ var s = twist.waveform.selected;
+ app.insertScore("twst_nexttransient",
+ [0, 1, cbid, s[1], s[1], s[2]]
+ );
+ };
+
+ this.selectToNextTransient = function() {
+ if (playing) return;
+ var cbid = app.createCallback(globalCallbackHandler);
+ var s = twist.waveform.selected;
+ var selend = (s[0] == s[1]) ? s[1] + 0.000001 : s[1];
+ app.insertScore("twst_nexttransient",
+ [0, 1, cbid, s[0], selend, s[2]]
+ );
+ };
+
+ this.moveToStart = function() {
+ if (playing) return;
+ twist.waveform.setSelection(0);
+ };
+
+ this.moveToEnd = function() {
+ if (playing) return;
+ twist.waveform.setSelection(1);
+ };
+
+ this.selectAll = function() {
+ if (playing) return;
+ twist.waveform.setSelection(0, 1);
+ };
+
+ this.selectNone = function() {
+ if (playing) return;
+ twist.waveform.setSelection(0);
+ };
+
+ this.selectToEnd = function() {
+ if (playing) return;
+ twist.waveform.alterSelection(null, 1);
+ }
+
+ this.selectFromStart = function() {
+ if (playing) return;
+ twist.waveform.alterSelection(0, null);
+ }
+
+ this.pasteSpecial = function() {
+ if (playing) return;
+ var elPasteSpecial = $("<div />");
+ elPasteSpecial.append($("<h4 />").text("Paste special"));
+ var def = {
+ instr: "twst_pastespecial",
+ parameters: [
+ {name: "Repetitions", channel: "repetitions", min: 1, max: 40, step: 1, dfault: 1, automatable: false},
+ {name: "Mix paste", channel: "mixpaste", step: 1, dfault: 0, automatable: false}
+ ]
+ };
+ var tf = new twirl.transform.Transform({
+ element: elPasteSpecial,
+ definition: def,
+ host: twist
+ });
+
+ $("<button />").text("Paste").click(function(){
+ twist.ui.hidePrompt();
+ twist.waveform.cover(true);
+ operation({
+ instr: "twst_pastespecial",
+ name: "Paste special",
+ onComplete: globalCallbackHandler,
+ showLoading: true,
+ logScriptChannels: ["twst_pastespecial_repetitions", "twst_pastespecial_mixpaste"]
+ });
+
+ }).appendTo(elPasteSpecial);
+
+ $("<button />").text("Cancel").click(function(){
+ twist.ui.hidePrompt();
+ }).appendTo(elPasteSpecial);
+ twist.ui.showPrompt(elPasteSpecial, null, true);
+
+ };
+
+
+ this.play = function(playOverride) {
+ if (!waveformLoaded[instanceIndex] || (playing && !playOverride)) return;
+ auditioning = false;
+ recording = false;
+ operation({
+ instr: "twst_play",
+ name: "Play",
+ onComplete: playPositionHandler(false, function(ndata){
+ if (ndata.status != 3 && twist.playbackLoop) { // 3 = user-stopped
+ twist.play(true);
+ }
+ }),
+ noLogScript: true
+ });
+ };
+
+ this.stop = function() {
+ if (!playing || !waveformLoaded[instanceIndex]) return;
+ twist.waveform.cover(false);
+ app.insertScore("twst_stop");
+ };
+
+ var saveNumber = 1;
+ function formatFileName(name) {
+ if (!name) name = waveformTabs[instanceIndex].text();
+ if (!name.toLowerCase().endsWith(".wav")) {
+ name += ".wav";
+ }
+
+ // HACK TODO: WASM can't overwrite files
+ name = name.substr(0, name.lastIndexOf(".")) + "." + (saveNumber ++) + name.substr(name.lastIndexOf("."));
+ // END HACK
+ return name;
+ }
+
+ this.downloadFile = async function(path, name) {
+ if (!name) name = formatFileName(name);
+ var content = await app.readFile(path);
+ var blob = new Blob([content], {type: "audio/wav"});
+ var url = window.URL.createObjectURL(blob);
+ var a = $("<a />").attr("href", url).attr("download", name).appendTo($("body")).css("display", "none");
+ a[0].click();
+ setTimeout(function(){
+ a.remove();
+ window.URL.revokeObjectURL(url);
+ app.unlinkFile(path);
+ }, 20000);
+ };
+
+ this.saveFile = function(name, onComplete) {
+ if (playing) return;
+ if (onSave) {
+ twirl.loading.show("Processing");
+ var cbid = app.createCallback(function(ndata){
+ twirl.loading.hide();
+ onSave(ndata.tables);
+ });
+ app.insertScore("twst_getbuffers", [0, 1, cbid]);
+ return;
+ }
+ if (!name) name = formatFileName(name);
+ var cbid = app.createCallback(async function(ndata){
+ await self.downloadFile("/" + name, name);
+ if (onComplete) onComplete();
+ self.ui.setLoadingStatus(false);
+ });
+ self.ui.setLoadingStatus(true, true, "Saving");
+ app.insertScore("twst_savefile", [0, 1, cbid, name]);
+ };
+
+ function getAutomationData(start, end) {
+ var calls = [];
+ if (!self.currentTransform) return calls;
+ var automations = self.currentTransform.getAutomationData(start, end);
+ if (automations && automations.length > 0) {
+ for (let i in automations) {
+ if (automations[i].type == "modulation") {
+ calls.push(automations[i].data[0] + " \"" + automations[i].data[1] + "\"");
+ } else if (automations[i].type == "automation") {
+ calls.push("chnset linseg:k(" + automations[i].data + "), \"" + automations[i].channel + "\"");
+ }
+ }
+ }
+ return calls;
+ }
+
+ function handleAutomation(onready, calls) {
+ if (calls.length == 0) {
+ return onready(0);
+ }
+
+ var instr = "instr twst_automaterun\n";
+ for (let c of calls) {
+ instr += c + "\n";
+ }
+ instr += "a_ init 0\nout a_\nendin\n";
+ app.compileOrc(instr).then(function(status){
+ if (status < 0) {
+ self.errorHandler("Cannot parse automation data");
+ } else {
+ onready(1);
+ }
+ });
+ /*
+ var cbid = app.createCallback(function(ndata){
+ if (ndata.status == 1) {
+ onready(1);
+ } else {
+ self.errorHandler("Cannot parse automation data");
+ }
+ });
+
+ var call = [0, 1, cbid];
+ for (let c of calls) {
+ call.push(c);
+ }
+ app.insertScore("twst_automationprepare", call);
+ */
+ }
+
+
+ function fftsizeCheck(selected, duration) {
+ if (self.currentTransform) {
+ for (var p in self.currentTransform.parameters) {
+ if (p.indexOf("fftsize") != -1) {
+ var val = self.currentTransform.parameters[p].getValue();
+ var minTime = (val / sr) * 2;
+ if ((selected[1] - selected[0]) * duration < minTime) {
+ return false;
+ }
+ }
+ }
+ }
+ return true;
+ }
+
+ this.record = async function() {
+ if (!waveformLoaded[instanceIndex] ||playing) return;
+ auditioning = false;
+ recording = true;
+ await app.enableAudioInput();
+ errorState = "Recording error";
+ self.waveform.cover(true);
+ var s = self.waveform.selected;
+ var monitorChannels;
+ if (self.waveform.channels == 1) {
+ monitorChannels = ["recordmonitorL"];
+ } else {
+ if (s[2] == -1) {
+ monitorChannels = ["recordmonitorL", "recordmonitorR"];
+ } else if (s[2] == 0) {
+ monitorChannels = ["recordmonitorL", null];
+ } else if (s[2] == 1) {
+ monitorChannels = [null, "recordmonitorR"];
+ }
+ }
+ self.waveform.resetDrawOneValue();
+ var cbid = playPositionHandler(null, null, monitorChannels);
+ var items = [0, 1, cbid, s[0], s[1], s[2]];
+ app.insertScore("twst_record", items);
+ };
+
+ this.audition = function(playOverride) {
+ if (!waveformLoaded[instanceIndex] || (playing && !playOverride)) return;
+ if (!self.currentTransform) {
+ return self.play();
+ }
+ self.currentTransform.saveState();
+ var s = self.waveform.selected;
+ if (s[0] == s[1]) {
+ s[0] = 0;
+ s[1] = 1;
+ }
+ if (!fftsizeCheck(s, self.waveform.duration)) {
+ return self.errorHandler("Length too short for this transform");
+ }
+
+ auditioning = true;
+ recording = false;
+ errorState = "Playback error";
+ handleAutomation(function(automating){
+ var cbid = playPositionHandler(false, function(ndata){
+ if (ndata.status != 3 && self.playbackLoop) { // 3 = user-stopped
+ self.audition(true);
+ }
+ });
+ var xfade = self.ui.getCrossFadeValues();
+ var items = [
+ 0, 1, cbid, s[0], s[1], s[2],
+ self.currentTransform.instr, automating,
+ xfade[0], xfade[1]
+ ];
+ app.insertScore("twst_audition", items);
+ }, getAutomationData(s[0], s[1]));
+
+ };
+
+
+ var scriptStack = [];
+ function applyScript(audition, first, lastData) {
+ if (playing) return;
+ var noCheckpoint = !first;
+ var script = scriptStack.shift();
+ if (!script) {
+ self.ui.setLoadingStatus(false);
+ if (lastData) {
+ globalCallbackHandler(lastData);
+ }
+ self.setPlaying(false);
+ return;
+ }
+
+ if (audition) auditioning = true;
+ self.setPlaying(true);
+
+ if (script.type == "operation") {
+ if (audition) {
+ return self.errorHandler("Only transform scripts can be auditioned");
+ }
+ self.ui.setLoadingStatus(true);
+ self.waveform.cover(true);
+ onComplete = (script.instr == "twst_copy") ? null : globalCallbackHandler;
+
+ operation({
+ instr: script.instr,
+ name: script.name,
+ onComplete: function(ndata){
+ lastData = ndata;
+ self.setPlaying(false);
+ applyScript(audition, false, lastData);
+ },
+ showLoading: true,
+ selection: script.selection,
+ noLogScript: true,
+ noCheckpoint: noCheckpoint
+ });
+
+ } else if (script.type == "transform") {
+ errorState = ((audition) ? "Audition" : "Transform" ) + " commit error";
+ if (!audition) {
+ self.ui.setLoadingStatus(true, true);
+ }
+
+ for (let channel in script.channels) {
+ app.setControlChannel(channel, script.channels[channel]);
+ }
+ handleAutomation(function(automating){
+ if (audition) {
+ var cbid = playPositionHandler();
+ } else {
+ var cbid = app.createCallback(function(ndata) {
+ lastData = ndata;
+ self.setPlaying(false);
+ applyScript(audition, false, lastData);
+ });
+ }
+ var instr = "twst_" + ((audition) ? "audition" : "commit");
+
+ app.insertScore(instr, [
+ 0, -1, cbid, script.selection[0], script.selection[1], script.selection[2], script.instr, automating, script.crossfades[0], script.crossfades[1], (noCheckpoint) ? 1 : 0
+ ]);
+ }, script.automation);
+ }
+ }
+
+ this.applyScript = async function(script, audition) {
+ if (playing) return;
+ scriptStack = [];
+ if (Array.isArray(script)) {
+ if (audition) {
+ return self.errorHandler("Only single scripts can be auditioned");
+ }
+ scriptStack = script;
+ } else {
+ scriptStack = [script];
+ }
+ if (self.storage.autosave && !audition) {
+ self.saveFile(null, function() {
+ applyScript(audition, true);
+ });
+ } else {
+ applyScript(audition, true);
+ }
+ };
+
+ async function innerCommit() {
+ if (playing) return;
+ if (!self.currentTransform) return;
+ var s = self.waveform.selected;
+ if (s[0] == s[1]) {
+ s[0] = 0;
+ s[1] = 1;
+ }
+ if (!fftsizeCheck(s, self.waveform.duration)) {
+ return self.errorHandler("Length too short for this transform");
+ }
+ self.watchdog.start("commit");
+ self.setPlaying(true);
+ self.ui.setLoadingStatus(true, true, null);
+ var calls = getAutomationData(s[0], s[1]);
+
+ self.currentTransform.saveState();
+ var state = await self.currentTransform.getState();
+ state.type = "transform";
+ state.automation = calls;
+ state.crossfades = self.ui.getCrossFadeValues();
+ state.selection = [s[0], s[1], s[2]];
+ state.instanceIndex = instanceIndex;
+ pushOperationLog(state);
+
+ handleAutomation(function(automating){
+ var cbid = app.createCallback(function(ndata) {
+ self.watchdog.stop();
+ self.ui.setLoadingStatus(false);
+ self.setPlaying(false);
+ if (ndata.status > 0) {
+ globalCallbackHandler(ndata);
+ } else {
+ var text;
+ if (ndata.status == -2) {
+ text = "Resulting file is too large";
+ }
+ self.errorHandler(text);
+ }
+ });
+ errorState = "Transform commit error";
+ app.insertScore("twst_commit", [0, -1, cbid, s[0], s[1], s[2], self.currentTransform.instr, automating, state.crossfades[0],state.crossfades[1]]);
+ }, calls);
+ }
+
+ this.commit = async function() {
+ if (!waveformLoaded[instanceIndex]) return;
+ if (self.storage.autosave) {
+ self.saveFile(null, function() {
+ innerCommit();
+ });
+ } else {
+ innerCommit();
+ }
+ };
+
+ this.loadTransforms = function(transform) {
+ if (transform) {
+ var developObj;
+ for (var t in twirl.appdata.transforms) {
+ if (twirl.appdata.transforms[t].name == "Develop") {
+ developObj = twirl.appdata.transforms[t];
+ break;
+ }
+ }
+ if (!developObj) {
+ developObj = {name: "Develop", contents: []};
+ twirl.appdata.transforms.push(developObj);
+ } else {
+ for (var c in developObj.contents) {
+ if (developObj.contents[c].name == transform.name) {
+ delete developObj.contents[c];
+ }
+ }
+ }
+ developObj.contents.push(transform);
+ }
+
+ $("#twist_panetree").empty();
+ var ttv = new twirl.transform.TreeView({
+ element: $("#twist_panetree"),
+ items: twirl.appdata.transforms,
+ click: function(definition, path) {
+ if (twist.currentTransform) {
+ twist.currentTransform.remove();
+ }
+ twist.currentTransform = new twirl.transform.Transform({
+ element: $("#twist_controls_inner"),
+ definition: definition,
+ splineElement: $("#twist_splines"),
+ useStorage: true,
+ path: path,
+ otherInstanceNamesFunc: function() {
+ return twist.otherInstanceNames;
+ },
+ instancesFunc: function() {
+ return twist.waveforms;
+ },
+ getRegionFunc: function() {
+ return twist.waveform.getRegion();
+ },
+ getDurationFunc: function() {
+ return twist.waveform.getDuration();
+ },
+ onHideAutomation: function() {
+ twist.ui.deleteSupressed = false;
+ console.log("twist.ui.deleteSupressed", twist.ui.deleteSupressed);
+ },
+ onShowAutomation: function() {
+ twist.ui.deleteSupressed = true;
+ console.log("twist.ui.deleteSupressed", twist.ui.deleteSupressed);
+ },
+ host: twist
+ });
+ }
+ });
+ };
+
+ this.createEmpty = function(name, duration, channels) {
+ if (name.trim() == "") {
+ name = "New file";
+ }
+ var cbid = app.createCallback(async function(ndata) {
+ twist.waveformTab.text(name);
+ await globalCallbackHandler(ndata);
+ if (twist.currentTransform) {
+ twist.currentTransform.refresh();
+ }
+ waveformFiles[instanceIndex] = name;
+ waveformLoaded[instanceIndex] = true;
+ twist.ui.setLoadingStatus(false);
+ });
+ twist.ui.hidePrompt();
+ twist.ui.setLoadingStatus(true, false, "Creating");
+ app.insertScore("twst_createempty", [0, 1, cbid, duration, channels]);
+ };
+
+ this.setVisible = function(state) {
+ twist.visible = state;
+ var el = $("#twist");
+ if (state) {
+ el.show();
+ } else {
+ el.hide();
+ }
+ };
+
+ this.editInTwigs = function() {
+ if (!window.twigs) {
+ return twirl.prompt.show("twigs is unavailable in this session");
+ }
+ twirl.loading.show("Processing");
+ var cbid = app.createCallback(function(ndata){
+ twirl.loading.hide();
+ twigs.loadFileFromFtable(waveformFiles[instanceIndex], ndata.tables, function(ldata){
+ if (ldata.status > 0) {
+ self.setVisible(false);
+ twigs.setVisible(true);
+ }
+ }, onSave);
+ });
+ app.insertScore("twst_getbuffers", [0, 1, cbid]);
+ };
+
+ this.loadFileFromClipboard = function() {
+ if (!twist.hasClipboard) {
+ return twirl.prompt.show("Cannot create: clipboard is empty");
+ }
+ errorState = "File loading error";
+ twirl.loading.show("Loading");
+ var cbid = app.createCallback(async function(ndata){
+ self.waveformTab.text("Clipboard");
+ await globalCallbackHandler(ndata);
+ waveformFiles[instanceIndex] = "Clipboard";
+ waveformLoaded[instanceIndex] = true;
+ twirl.loading.hide();
+ });
+ app.insertScore("twst_loadclipboard", [0, 1, cbid]);
+ };
+
+ this.loadFileFromFtable = function(name, tables, onComplete, onSaveFunc) {
+ errorState = "File loading error";
+ twirl.loading.show("Loading file");
+
+ var cbid = app.createCallback(async function(ndata){
+ twirl.loading.hide();
+ if (ndata.status > 0) {
+ if (waveformTabs.length == 0) {
+ self.createNewInstance(true);
+ instanceIndex = 0;
+ }
+ self.waveformTab.text(name);
+ waveformLoaded[instanceIndex] = true;
+ await globalCallbackHandler(ndata);
+ if (self.currentTransform) {
+ self.currentTransform.refresh();
+ }
+ waveformFiles[instanceIndex] = name;
+ self.ui.hidePrompt();
+ onSave = onSaveFunc;
+ } else if (ndata.status == -1) {
+ twirl.prompt.show("File not valid");
+ } else if (ndata.status == -2) {
+ twirl.prompt.show("File too large");
+ } else {
+ twirl.prompt.show("File loading error");
+ }
+ if (onComplete) {
+ onComplete(ndata);
+ }
+ });
+ var call = [0, 1, cbid];
+ for (let t of tables) {
+ call.push(t);
+ }
+ app.insertScore("twst_loadftable", call);
+ };
+
+
+ async function handleFileDrop(e, obj) {
+ e.preventDefault();
+ if (!e.originalEvent.dataTransfer && !e.originalEvent.files) {
+ return;
+ }
+ if (e.originalEvent.dataTransfer.files.length == 0) {
+ return;
+ }
+ self.ui.hidePrompt();
+ self.ui.setLoadingStatus(true, true, "Loading");
+ for (const item of e.originalEvent.dataTransfer.files) {
+ if (!twirl.audioTypes.includes(item.type)) {
+ return self.errorHandler("Unsupported file type", self.ui.showLoadNewPrompt);
+ }
+ if (item.size > twirl.maxFileSize) {
+ return self.errorHandler("File too large", self.ui.showLoadNewPrompt);
+ }
+ errorState = "File loading error";
+ var content = await item.arrayBuffer();
+ const buffer = new Uint8Array(content);
+ await app.writeFile(item.name, buffer);
+ var cbid = app.createCallback(async function(ndata){
+ await app.unlinkFile(item.name);
+ if (ndata.status == -1) {
+ return self.errorHandler("File not valid", self.ui.showLoadNewPrompt);
+ } else if (ndata.status == -2) {
+ return self.errorHandler("File too large", self.ui.showLoadNewPrompt);
+ } else {
+ self.waveformTab.text(item.name);
+ await globalCallbackHandler(ndata);
+ if (self.currentTransform) {
+ self.currentTransform.refresh();
+ }
+ waveformFiles[instanceIndex] = item.name;
+ waveformLoaded[instanceIndex] = true;
+ self.ui.hidePrompt();
+ self.ui.setLoadingStatus(false);
+ onSave = false;
+ }
+ });
+ app.insertScore("twst_loadfile", [0, 1, cbid, item.name]);
+ }
+ }
+
+ async function globalCallbackHandler(ndata) {
+ if (ndata.status && ndata.status <= 0) {
+ var text;
+ if (ndata.status == -2) {
+ text = "Resulting file would be too large";
+ }
+ self.errorHandler(text);
+ return;
+ }
+
+ self.watchdog.start("refresh");
+
+ if (ndata.hasOwnProperty("undolevel")) {
+ self.undoLevel = ndata.undolevel;
+ }
+
+ if (ndata.hasOwnProperty("delete")) {
+ if (typeof(ndata.delete) == "string") {
+ app.unlinkFile(ndata.delete);
+ } else {
+ for (let d of ndata.delete) {
+ app.unlinkFile(d);
+ }
+ }
+ }
+
+ if (ndata.hasOwnProperty("selstart")) {
+ self.waveform.setSelection(ndata.selstart, ndata.selend);
+ }
+
+ if (ndata.hasOwnProperty("waveL")) {
+ self.waveform.cover(true);
+ errorState = "Overview refresh error";
+ setTimeout(async function(){
+ var wavedata = [];
+ var tbL = await app.getTable(ndata.waveL);
+ wavedata.push(tbL);
+ if (ndata.hasOwnProperty("waveR")) {
+ var tbR = app.getTable(ndata.waveR);
+ wavedata.push(tbR);
+ }
+ self.waveform.setData(wavedata, ndata.duration);
+ self.waveform.cover(false);
+ }, 10);
+ }
+ self.watchdog.stop();
+ }
+
+ this.bootAudio = async function(twine) {
+ var channelDefaultItems = ["dcblockoutputs", "tanhoutputs", "maxundo"];
+
+ for (var i of channelDefaultItems) {
+ if (self.storage.hasOwnProperty(i)) {
+ app.setControlChannel("twst_" + i, self.storage[i]);
+ }
+ }
+ sr = await app.getCsound().getSr();
+ if (!twine) self.ui.postBoot();
+ };
+
+ var booted = false;
+ this.boot = function(twine) {
+ if (booted) return;
+ booted = true;
+ twirl.boot();
+ self.ui.boot();
+
+ Object.defineProperty(this, "waveformTab", {
+ get: function() { return waveformTabs[instanceIndex]; },
+ set: function(x) {}
+ });
+
+ Object.defineProperty(this, "otherInstanceNames", {
+ get: function() {
+ var data = {};
+ for (var i in waveformTabs) {
+ if (i != instanceIndex) {
+ data[i] = waveformTabs[i].text();
+ }
+ }
+ return data
+ },
+ set: function(x) {}
+ });
+
+ Object.defineProperty(this, "instanceIndex", {
+ get: function() {
+ return instanceIndex
+ },
+ set: function(x) {}
+ });
+
+ Object.defineProperty(this, "undoLevel", {
+ get: function() {
+ return undoLevels[instanceIndex];
+ },
+ set: function(x) {
+ undoLevels[instanceIndex] = x;
+ }
+ });
+
+ Object.defineProperty(this, "waveform", {
+ get: function() { return self.waveforms[instanceIndex]; },
+ set: function(x) {
+ if (instanceIndex != x) {
+ if (self.waveformTab) {
+ self.waveformTab.removeClass("wtab_selected").addClass("wtab_unselected");
+ }
+ if (self.waveform) {
+ self.waveform.hide();
+ }
+ var cbid = app.createCallback(function(ndata){
+ if (ndata.status == 1) {
+ instanceIndex = x;
+ self.waveformTab.removeClass("wtab_unselected").addClass("wtab_selected");
+ self.waveform.show();
+ if (self.currentTransform) {
+ self.currentTransform.refresh();
+ self.currentTransform.redraw(self.waveform.getRegion());
+ }
+ } else {
+ self.ui.showPrompt("Error changing instance");
+ }
+ });
+ app.insertScore("twst_setinstance", [0, 1, cbid, x]);
+
+ }
+ }
+ });
+
+ if (!twine) {
+ $("<td />").text("+").click(function() {
+ self.createNewInstance();
+ }).appendTo("#twist_waveform_tabs").addClass("wtab_selected");
+
+ $("body").on("dragover", function(e) {
+ e.preventDefault();
+ e.originalEvent.dataTransfer.effectAllowed = "all";
+ e.originalEvent.dataTransfer.dropEffect = "copy";
+ return false;
+ }).on("dragleave", function(e) {
+ e.preventDefault();
+ }).on("drop", function(e) {
+ handleFileDrop(e, self);
+ });
+ } else {
+ self.twine = twine;
+ }
+
+ self.loadTransforms();
+ };
+
+}; // end twist
+
+function twist_startisolated() {
+ var csOptions = ["--omacro:TWST_FAILONLAG=1"];
+ window.twist = new Twist();
+ twist.setVisible(true);
+ window.app = new CSApplication({
+ csdUrl: "twist.csd",
+ csOptions: csOptions,
+ onPlay: function () {
+ twist.bootAudio();
+ },
+ errorHandler: twist.errorHandler,
+ ioReceivers: {percent: twist.ui.setPercent}
+ });
+
+ $("#twist_start").click(function() {
+ $(this).hide();
+ twist.boot();
+ twist.ui.setLoadingStatus(true, false, "Preparing audio engine");
+ app.play(function(text){
+ twist.ui.setLoadingStatus(true, false, text);
+ twirl.latencyCorrection = twirl.audioContext.outputLatency * 1000;
+ }, twirl.audioContext);
+ });
+}
+
+ \ No newline at end of file