var NoteData = function() {
var self = this;
this.data = null;
fetch("../base/notedata.json").then(function(r) {
r.json().then(function(j) {
self.data = j;
});
});
};
var OperationWatchdog = function(twist) {
var self = this;
var active = false;
var lastValues = [true, false];
var firstActive = true;
var checkInterval;
var timeoutTime = 2000;
var alivetimeoutTime = 2000;
var context;
function crash() {
self.stop();
twist.sendErrorState("Unhandled exception in " + context);
var el = $("#twist_crash").show();
var elSr = $("#twist_crash_recovery");
function doomed() {
elSr.empty().append($("
").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 = $("").attr("href", "#").text("Download").click(function(e){
e.preventDefault();
twist.downloadFile("/crashL.wav");
});
if (ndata.left && !ndata.right) {
elSr.append($("").text("Your work has been recovered:"));
elSr.append(linkLeft);
} else {
elSr.append($("").text("Your work has been recovered as separate left/right channels:"));
linkLeft.text("Download left channel").appendTo(elSr);
elSr.append(" ");
var linkRight = $("").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, false];
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, false];
if (checkInterval) clearInterval(checkInterval);
};
};
var Twist = function(appdata) {
var self = this;
var audioTypes = ["audio/mpeg", "audio/mp4", "audio/ogg", "audio/vorbis", "audio/x-flac","audio/aiff","audio/x-aiff", "audio/vnd.wav", "audio/wave", "audio/x-wav", "audio/wav", "audio/flac"];
var maxsize = 1e+8; // 100 MB
this.currentTransform = null;
var errorState;
var instanceIndex = 0;
this.appdata = appdata;
this.instances = [];
var playheadInterval;
var latencyCorrection = 100;
var playing = false;
var auditioning = false;
var scope;
var recording = false;
var elCrossfades = [];
this.onPlays = [];
var elToolTip = $("").addClass("tooltip").appendTo($("body"));
this.audioContext = null;
var operationLog = [];
this.noteData = new NoteData();
var topMenu = new TopMenu(self, topMenuData, $("#twist_menubar"));
this.storage = localStorage.getItem("twist");
this.watchdog = new OperationWatchdog(self);
if (self.storage) {
self.storage = JSON.parse(self.storage);
} else {
self.storage = {};
}
this.tooltip = {
show: function(event, text) {
var margin = 100;
elToolTip.text(text).css("opacity", 0.9);
if (event.pageX >= window.innerWidth - margin) {
elToolTip.css({left: window.innerWidth - (margin * 2) + "px"});
} else {
elToolTip.css({left: (event.pageX + 20) + "px"});
}
if (event.pageY >= window.innerHeight - margin) {
elToolTip.css({top: window.innerHeight - (margin * 2) + "px"});
} else {
elToolTip.css({top: (event.pageY - 15) + "px"});
}
},
hide: function() {
elToolTip.css("opacity", 0);
}
};
this.setPlaying = function(state) {
if (playing == state) return;
playing = state;
for (var o of self.onPlays) {
o(playing, auditioning, recording);
}
if (self.currentTransform) {
self.currentTransform.setPlaying(state);
}
if (scope) {
scope.setPlaying(state);
}
};
this.saveStorage = function() {
localStorage.setItem("twist", JSON.stringify(self.storage));
};
function lastOperation() {
return operationLog[operationLog.length - 1];
}
function pushOperationLog(operation) {
var max = self.storage.commitHistoryLevel;
if (!max) {
self.storage.commitHistoryLevel = max = 16;
}
if (operationLog.length + 1 >= max) {
operationLog.shift();
}
operationLog.push(operation);
}
function showLoadNewPrompt() {
var elNewFile = $("").css({"font-size": "var(--fontSizeDefault)"});
elNewFile.append($("").text("Drag an audio file here to load")).append($("").text("or"));
$("").text("Create an empty file").css("cursor", "pointer").appendTo(elNewFile).click(function() {
elNewFile.show();
});
var tpDuration = new TransformParameter(null, {name: "Duration", min: 0.1, max: 60, dfault: 10, automatable: false, fireChanges: false}, null, null, twist);
var tpChannels = new TransformParameter(null, {name: "Channels", min: 1, max: 2, dfault: 2, step: 1, automatable: false, fireChanges: false}, null, null, twist);
var tpName = new TransformParameter(null, {name: "Name", type: "string", dfault: "New file", fireChanges: false}, null, null, twist);
var tb = $("");
$("
").append(tb).css("margin", "0 auto").appendTo(elNewFile);
tb.append(tpDuration.getElementRow(true)).append(tpChannels.getElementRow(true)).append(tpName.getElementRow(true));
$("").text("Create").appendTo(elNewFile).click(function() {
var name = tpName.getValue();
if (name.trim() == "") {
name = "New file";
}
var cbid = app.createCallback(async function(ndata) {
self.waveformTab.text(name);
await globalCallbackHandler(ndata);
if (self.currentTransform) {
self.currentTransform.refresh();
}
waveformFiles[instanceIndex] = name;
setLoadingStatus(false);
});
self.hidePrompt();
setLoadingStatus(true, false, "Creating");
app.insertScore("twst_createempty", [0, 1, cbid, tpDuration.getValue(), tpChannels.getValue()]);
});
self.showPrompt(elNewFile, null, true);
}
this.toggleScope = function(noSaveState) {
var height;
var top;
var state;
if (!scope) {
state = true;
height = "60%";
top = "40%";
var elScope = $("").addClass("twist_scope").appendTo($("#twist_waveforms"));
var type = (self.storage.scopeType) ? self.storage.scopeType : "frequency";
scope = new Analyser(
type, self, elScope, app
);
} else {
state = false;
scope.remove();
delete scope;
scope = null;
height = "100%";
top = "0px";
}
if (!noSaveState) {
self.storage.showScope = state;
self.saveStorage();
}
$(".waveform").css({height: height, top: top});
}
this.createNewInstance = function() {
var element = $("").addClass("waveform").appendTo("#twist_waveforms");
let index = waveformFiles.length;
if (index < 0) index = 0;
waveformTabs.push(
$("
").text("New file").click(function() {
if (self.isPlaying()) return;
self.waveform = index;
}).addClass("wtab_selected").appendTo("#twist_waveform_tabs")
);
undoLevels.push(0);
self.waveforms.push(
new Waveform({
target: element,
latencyCorrection: latencyCorrection,
showcrossfades: true,
crossFadeWidth: 1,
timeBar: true,
markers: [
{preset: "selectionstart"},
{preset: "selectionend"},
]
})
);
showLoadNewPrompt();
self.waveform = index;
};
this.removeInstance = function(i) {
if (!i) i = instanceIndex;
if (i < 0 || i > this.instances.length - 1) {
return;
}
self.instances[instanceindex].close();
if (instanceIndex == i) {
instanceIndex = i + ((i == 0) ? 1 : -1);
self.instances[instanceIndex].show();
}
};
t
var remoteSessionID;
var remoteSending = false;
this.sendErrorState = async function (errorText) {
if (remoteSending) return;
remoteSending = true;
var data = {
request_type: "LogError",
error: {
text: errorText,
lastOperation: lastOperation()
}
};
if (self.currentTransform) {
var state = await self.currentTransform.getState();
data.error.transformState = state;
}
if (remoteSessionID) {
data.session_id = remoteSessionID;
}
var resp = await fetch("/service/", {
method: "POST",
headers: {
"Content-type": "application/json"
},
body: JSON.stringify(data)
});
var json = await resp.json();
if (json.session_id && !remoteSessionID) {
remoteSessionID = json.session_id;
}
remoteSending = false;
}
this.errorHandler = function(text, onComplete) {
var errorText = (!text) ? errorState : text;
self.sendErrorState(errorText);
self.setPlaying(false);
self.showPrompt(errorText, onComplete);
errorState = null;
};
function playPositionHandler(noPlayhead, onComplete) {
function callback(ndata) {
if (ndata.status == 1) {
self.setPlaying(true);
if (!noPlayhead) {
watchdog.start("audition");
if (playheadInterval) {
clearInterval(playheadInterval);
}
playheadInterval = setInterval(async function(){
var val = await app.getControlChannel("playposratio");
watchdog.setActive(val);
if (val < 0 || val > 1) {
clearInterval(playheadInterval);
}
self.waveform.movePlayhead(val);
}, 50);
}
} else {
self.setPlaying(false);
if (ndata.status == -1) {
self.errorHandler("Not enough processing power to transform in realtime");
}
app.removeCallback(ndata.cbid);
if (!noPlayhead) {
watchdog.stop();
self.waveform.movePlayhead(0);
if (playheadInterval) {
clearInterval(playheadInterval);
}
}
if (onComplete) onComplete();
}
}
return app.createCallback(callback, true);
}
function operation(instr, oncompleteOrCbidOverride, showLoading, selection, noLogScript) {
var s = (selection) ? selection : self.waveform.selected;
errorState = "Operation error";
if (showLoading) {
setLoadingStatus(true);
}
var cbid;
if (!oncompleteOrCbidOverride || typeof(oncompleteOrCbidOverride) == "function") {
cbid = app.createCallback(function(ndata) {
self.waveform.cover(false);
if (oncompleteOrCbidOverride) {
oncompleteOrCbidOverride(ndata);
} else if (ndata.status && ndata.status <= 0) {
var text;
if (ndata.status == -2) {
text = "Resulting file is too large";
}
self.errorHandler(text);
}
if (showLoading) {
setLoadingStatus(false);
}
});
} else {
cbid = oncompleteOrCbidOverride;
}
if (!noLogScript) {
pushOperationLog({type: "operation", instr: instr, selection: s, instanceIndex: instanceIndex});
}
app.insertScore(instr, [0, 1, cbid, s[0], s[1], s[2]]);
}
this.isPlaying = function() {
return playing;
};
this.pasteSpecial = function() {
if (playing) return;
var elPasteSpecial = $("");
elPasteSpecial.append($("").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},
{name: "Mix crossfade", channel: "mixfade", automatable: false, conditions: [{channel: "mixpaste", operator: "eq", value: 1}]}
]
};
var tf = new Transform(elPasteSpecial, def, self);
$("").text("Paste").click(function(){
self.hidePrompt();
self.waveform.cover(true);
operation("twst_pastespecial", globalCallbackHandler, true);
}).appendTo(elPasteSpecial);
$("").text("Cancel").click(function(){
self.hidePrompt();
}).appendTo(elPasteSpecial);
self.showPrompt(elPasteSpecial, null, true);
};
this.developerConsole = function() {
$("#twist_developer").show();
$("#twist_inject_devcsound").click(async function() {
var code = $("#twist_devcsound").val();
var result = await app.compileOrc(code);
if (result == 0) {
if (!self.storage.develop) {
self.storage.develop = {};
}
self.storage.develop.csound = code;
self.saveStorage();
self.showPrompt("Successfully injected Csound code");
}
});
$("#twist_inject_devjson").click(async function() {
var code = $("#twist_devjson").val();
try {
var json = JSON.parse(code);
} catch (e) {
return self.errorHandler("Cannot parse JSON: " + e);
}
try {
self.loadTransforms(json);
} catch (e) {
return self.errorHandler("Cannot load transform: " + e);
}
if (!self.storage.develop) {
self.storage.develop = {};
}
self.storage.develop.json = code;
self.saveStorage();
self.showPrompt("Successfully injected transform definition");
});
$("#twist_exit_devcode").click(async function() {
$("#twist_developer").hide();
});
};
this.play = function() {
if (playing) return;
auditioning = false;
recording = false;
operation("twst_play", playPositionHandler(), false, null, true);
};
this.stop = function() {
if (!playing) return;
self.waveform.cover(false);
app.insertScore("twst_stop");
};
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("."));
saveNumber ++;
// 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 = $("").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);
};
var saveNumber = 1;
this.saveFile = function(name, onComplete) {
if (playing) return;
if (!name) name = formatFileName(name);
var cbid = app.createCallback(async function(ndata){
await self.downloadFile("/" + name, name);
if (onComplete) onComplete();
setLoadingStatus(false);
});
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 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 compileVariScript(script, onComplete) {
var cbid = app.createCallback(function(ndata){
onComplete(ndata.status == 1);
// should maybe automatically refresh
});
}
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 (playing) return;
auditioning = false;
recording = true;
await app.enableAudioInput();
errorState = "Recording error";
self.waveform.cover(true);
var cbid = playPositionHandler();
var s = self.waveform.selected;
var items = [0, 1, cbid, s[0], s[1], s[2]];
app.insertScore("twst_record", items);
};
this.audition = function() {
if (playing) return;
if (!self.currentTransform) {
return self.play();
}
self.currentTransform.saveState();
var s = self.waveform.selected;
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();
var items = [
0, 1, cbid, s[0], s[1], s[2],
self.currentTransform.instr, automating,
elCrossfades[0].val(), elCrossfades[1].val()
];
app.insertScore("twst_audition", items);
}, getAutomationData(s[0], s[1]));
};
var scriptStack = [];
function applyScript(audition) {
if (playing) return;
var lastData;
var script = scriptStack.shift();
if (!script) {
setLoadingStatus(false);
if (lastData) {
console.log("ass", 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.waveform.cover(true);
onComplete = (script.instr == "twst_copy") ? null : globalCallbackHandler;
operation(script.instr, function(ndata){
lastData = ndata;
self.setPlaying(false);
applyScript(audition);
}, true, script.selection);
} else if (script.type == "transform") {
errorState = ((audition) ? "Audition" : "Transform" ) + " commit error";
if (!audition) {
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);
});
}
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]
]);
}, 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);
});
} else {
applyScript(audition);
}
};
async function innerCommit() {
if (playing) return;
if (!self.currentTransform) return;
var s = self.waveform.selected;
if (!fftsizeCheck(s, self.waveform.duration)) {
return self.errorHandler("Length too short for this transform");
}
watchdog.start("commit");
self.setPlaying(true);
setLoadingStatus(true, true);
var calls = getAutomationData(s[0], s[1]);
self.currentTransform.saveState();
var state = await self.currentTransform.getState();
state.type = "transform";
state.automation = calls;
state.crossfades = [elCrossfades[0].val(), elCrossfades[1].val()];
state.selection = [s[0], s[1], s[2]];
state.instanceIndex = instanceIndex;
pushOperationLog(state);
handleAutomation(function(automating){
var cbid = app.createCallback(function(ndata) {
watchdog.stop();
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 (self.storage.autosave) {
self.saveFile(null, function() {
innerCommit();
});
} else {
innerCommit();
}
};
this.createIcon = function(definition) {
var state = true;
var active = true;
function formatPath(i) {
return "../base/icon/" + i + ".svg";
}
var el = $("");
var obj = {
el: el,
setState: function(tstate) {
if (!definition.icon2) return;
state = tstate;
if (state) {
el.attr("src", formatPath(definition.icon));
} else {
el.attr("src", formatPath(definition.icon2));
}
},
setActive: function(state) {
if (state) {
el.css("opacity", 1);
active = true;
} else {
el.css("opacity", 0.4);
active = false;
}
}
};
el.addClass("icon").css("opacity", 1).attr("src", formatPath(definition.icon)).on("mouseover", function(event){
var label = (!state && definition.label2) ? definition.label2 : definition.label;
self.tooltip.show(event, label);
}).on("mouseout", function(){
self.tooltip.hide();
}).click(function(el) {
if (active) definition.click(obj);
});
return obj;
}
function buildWavecontrols() {
var el = $("#twist_wavecontrols_inner");
var onPlayDisables = [];
var play = self.createIcon({label: "Play", icon: "play", label2: "Stop", icon2: "stop", click: function(obj){
if (self.isPlaying()) {
self.stop();
} else {
self.play();
}
}});
var audition = self.createIcon({label: "Audition", icon: "audition", label2: "Stop", icon2: "stop", click: function(obj){
if (self.isPlaying()) {
self.stop();
} else {
self.audition();
}
}});
var crossfade = self.createIcon({label: "Show crossfades", icon: "crossfade", label2: "Hide crossfades", icon2: "hide", click: function(obj){
var el = $(".crossfade");
if (el.is(":visible")) {
obj.setState(true);
el.hide();
self.storage.showCrossfades = false;
elCrossfades[0].val(0).trigger("input");
elCrossfades[1].val(0).trigger("input");
} else {
el.show();
obj.setState(false);
self.storage.showCrossfades = true;
}
self.saveStorage();
}});
var record = self.createIcon({label: "Record", icon: "record", label2: "Stop", icon2: "stop", click: function() {
if (self.isPlaying()) {
self.stop();
} else {
self.record();
}
}});
var items = [
{label: "Zoom selection", icon: "zoomSelection", click: function() {self.waveform.zoomSelection();}},
{label: "Zoom in", icon: "zoomIn", click: function() {self.waveform.zoomIn();}},
{label: "Zoom out", icon: "zoomOut", click: function() {self.waveform.zoomOut();}},
{label: "Show all", icon: "showAll", click: function() {self.waveform.setRegion(0, 1);}},
{label: "Cut", icon: "cut", disableOnPlay: true, click: self.cut},
{label: "Copy", icon: "copy", disableOnPlay: true, click: self.copy},
{label: "Paste", icon: "paste", disableOnPlay: true, click: self.paste},
{label: "Paste special", icon: "pasteSpecial", disableOnPlay: true, click: self.pasteSpecial},
{label: "Rewind", icon: "rewind", disableOnPlay: true, click: self.moveToStart},
play,
audition,
{label: "Commit", icon: "commit", disableOnPlay: true, click: self.commit},
record,
{label: "Save", icon: "save", disableOnPlay: true, click: self.saveFile},
{label: "Script", icon: "script", click: self.scriptEdit},
{label: "Developer", icon: "develop", click: self.developerConsole},
crossfade
];
for (let i of items) {
var icon;
if (i.icon) {
icon = self.createIcon(i);
if (i.disableOnPlay) {
onPlayDisables.push(icon);
}
} else {
icon = i;
}
$("
").append(icon.el).appendTo(el);
}
twist.onPlays.push(async function(playing, auditioning, recording) {
if (playing) {
if (auditioning) {
play.setActive(false);
audition.setState(false);
record.setActive(false);
} else if (recording) {
audition.setActive(false);
play.setActive(false);
record.setState(false);
} else {
audition.setActive(false);
play.setState(false);
record.setActive(false);
}
} else {
audition.setActive(true);
play.setActive(true);
play.setState(true);
audition.setState(true);
record.setActive(true);
record.setState(true);
}
for (let o of onPlayDisables) {
o.setActive(!playing);
}
});
for (let e of ["In", "Out"]) {
let elRange = $("").addClass("tp_slider").attr("type", "range").attr("min", 0).attr("max", 0.45).attr("step", 0.00001).val(0).on("input", function() {
if (e == "In") {
self.waveform.crossFadeInRatio = $(this).val();
} else {
self.waveform.crossFadeOutRatio = $(this).val();
}
});
elCrossfades.push(elRange);
$("
").addClass("crossfade").append($("").css("font-size", "8pt").text("Crossfade " + e)).append(elRange).appendTo(el);
}
var el = $(".crossfade");
if (self.storage.hasOwnProperty("showCrossfades")) {
if (self.storage.showCrossfades) {
crossfade.setState(false);
el.show();
} else {
crossfade.setState(true);
el.hide();
}
} else {
crossfade.setState(false);
el.show();
}
}
this.loadTransforms = function(transform) {
if (transform) {
var developObj;
for (var t in appdata.transforms) {
if (appdata.transforms[t].name == "Develop") {
developObj = appdata.transforms[t];
break;
}
}
if (!developObj) {
developObj = {name: "Develop", contents: []};
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 TransformsTreeView({
target: "twist_panetree",
items: appdata.transforms
}, self);
};
this.showHelp = function() {
$("#twist_help").show();
};
this.showAbout = function() {
var el = $("");
var x = $("").text("twist").appendTo(el);
$("").text("Version " + appdata.version.toFixed(1)).appendTo(el);
$("").css("font-size", "12px").text("By Richard Knight 2024").appendTo(el);
var skewMax = 30;
var skew = 0;
var skewDirection = true;
var twistInterval = setInterval(function(){
if (skewDirection) {
if (skew < skewMax) {
skew ++;
} else {
skewDirection = false;
}
} else {
if (skew > -skewMax) {
skew --;
} else {
skewDirection = true;
}
}
x.css("transform", "skewX(" + skew + "deg)");
}, 10);
self.showPrompt(el, function(){
clearInterval(twistInterval);
});
};
async function handleFileDrop(e, obj) {
e.preventDefault();
if (!e.originalEvent.dataTransfer && !e.originalEvent.files) {
return;
}
if (e.originalEvent.dataTransfer.files.length == 0) {
return;
}
self.hidePrompt();
setLoadingStatus(true, false, "Loading");
for (const item of e.originalEvent.dataTransfer.files) {
if (!audioTypes.includes(item.type)) {
return self.errorHandler("Unsupported file type", showLoadNewPrompt);
}
if (item.size > maxsize) {
return self.errorHandler("File too large", 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", showLoadNewPrompt);
} else if (ndata.status == -2) {
return self.errorHandler("File too large", showLoadNewPrompt);
} else {
self.waveformTab.text(item.name);
await globalCallbackHandler(ndata);
if (self.currentTransform) {
self.currentTransform.refresh();
}
waveformFiles[instanceIndex] = item.name;
self.hidePrompt();
setLoadingStatus(false);
}
});
app.insertScore("twst_loadfile", [0, 1, cbid, item.name]);
}
}
async function globalCallbackHandler(ndata) {
if (ndata.status && ndata.status <= 0) {
self.errorHandler();
return;
}
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";
var wavedata = [];
var duration = ndata.duration;
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);
}
}
this.bootAudio = function() {
var channelDefaultItems = ["dcblockoutputs", "tanhoutputs", "maxundo"];
for (var i of channelDefaultItems) {
if (self.storage.hasOwnProperty(i)) {
app.setControlChannel(i, self.storage[i]);
}
}
twist.setLoadingStatus(false);
if (!self.storage.hasOwnProperty("firstLoadDone")) {
self.storage.firstLoadDone = true;
self.saveStorage();
self.showPrompt($("#twist_welcome").detach().show(), self.createNewInstance);
} else {
self.createNewInstance();
}
if (self.storage.showScope) {
self.toggleScope(true);
}
};
this.boot = function() {
self.audioContext = new AudioContext();
if (self.storage.theme) {
self.setTheme(self.storage.theme, true);
}
if (self.storage.hasOwnProperty("showShortcuts")) {
if (self.storage.showShortcuts) {
$("#twist_wavecontrols_inner").show();
} else {
$("#twist_wavecontrols_inner").hide();
}
}
if (self.storage.develop) {
if (self.storage.develop.csound) {
$("#twist_devcsound").val(self.storage.develop.csound);
}
if (self.storage.develop.json) {
$("#twist_devjson").val(self.storage.develop.json);
}
}
$("#loading_background").css("opacity", 1).animate({opacity: 0.2}, 1000);
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();
}
} else {
self.showPrompt("Error changing instance");
}
});
app.insertScore("twst_setinstance", [0, 1, cbid, x]);
}
}
});
$("#twist_help").click(function() {
$(this).hide();
});
$("