var twst = {};
twst.Parameter = function(instr, definition, parent, transform, twist) {
var self = this;
var refreshable = false;
var changeFunc;
var value;
var initval = true;
var type;
var applicable;
var channel = (parent) ? parent.channel : instr + "_";
if (definition.hasOwnProperty("channel")) {
channel += definition.channel;
} else {
channel += definition.name.toLowerCase();
}
Object.defineProperty(this, "channel", {
get: function() { return channel; },
set: function(x) {}
});
if (definition.hasOwnProperty("options")) {
if (!definition.hasOwnProperty("automatable")) {
definition.automatable = false;
}
}
if (definition.hasOwnProperty("preset")) {
var save = {};
if (definition.hasOwnProperty("dfault")) {
save.dfault = definition.dfault;
}
if (definition.hasOwnProperty("name")) {
save.name = definition.name;
}
if (definition.preset == "fftsize") {
Object.assign(definition, {name: "FFT size", channel: "fftsize", description: "FFT size", options: [256, 512, 1024, 2048, 4096], dfault: 1, asvalue: true, automatable: false});
} else if (definition.preset == "wave") {
Object.assign(definition, {name: "Wave", description: "Wave shape to use", options: ["Sine", "Square", "Saw", "Pulse", "Triangle"], dfault: 0});
} else if (definition.preset == "instance") {
initval = false;
transform.refreshable = true;
refreshable = true;
Object.assign(definition, {
name: "Instance", description: "Other wave to use", channel: "instanceindex",
options: twist.otherInstanceNames,
automatable: false
});
changeFunc = function(index) {
var s = twist.waveforms[index].selected;
app.setControlChannel(instr + "_" + "otinststart", s[0]);
app.setControlChannel(instr + "_" + "otinstend", s[1]);
app.setControlChannel(instr + "_" + "otiinstchan", s[2]);
}
}
if (save) {
Object.assign(definition, save);
}
} // if preset
if (definition.hasOwnProperty("options") || (definition.hostrange && parent.definition.hasOwnProperty("options"))) {
type = "select";
} else {
type = "range";
}
if (definition.hasOwnProperty("conditions") && !parent) {
transform.refreshable = refreshable = true;
}
Object.defineProperty(this, "applicable", {
get: function() { return applicable; },
set: function(v) { }
});
Object.defineProperty(this, "value", {
get: function() { return value; },
set: function(v) {
if (type == "select") {
if (v < 0) {
v = 0;
} else if (v >= definition.options.length) {
v = defintion.options.length - 1;
}
if (definition.asvalue) {
value = definition.options[v];
} else {
value = v;
}
} else if (type == "range") {
if (v > definition.max) {
v = definition.max;
} else if (v < definition.min) {
v = definition.min;
} else if (v % definition.step != 0) {
if (definition.step == 1) {
v = Math.round(v);
} else {
v = Math.ceil((v - definition.min) / definition.step) * definition.step + definition.min;
}
}
value = v;
}
twist.csapp.setControlChannel(channel, value);
}
});
var automation = [];
this.definition = definition;
this.modulation = null;
this.channel = channel;
var modulationParameters = null;
if (!definition.hasOwnProperty("step")) {
definition.step = 0.0000001;
}
if (!definition.hasOwnProperty("min")) {
definition.min = 0;
}
if (!definition.hasOwnProperty("max")) {
definition.max = 1;
}
if (!definition.hasOwnProperty("automatable")) {
definition.automatable = true;
}
if (!definition.hasOwnProperty("dfault")) {
definition.dfault = 1;
}
if (parent && definition.hostrange) {
for (var o of ["step", "min", "max", "dfault", "options", "condition", "hostrange"]) {
if (parent.definition.hasOwnProperty(o)) {
definition[o] = parent.definition[o];
}
}
}
this.refresh = function() {
if (!refreshable) {
return;
}
for (var k in definition.conditions) {
var c = definition.conditions[k];
var val = transform.parameters[transform.instr + "_" + c.channel].getValue();
if (
(c.operator == "eq" && val != c.value) ||
(c.operator == "lt" && val >= c.value) ||
(c.operator == "gt" && val <= c.value) ||
(c.operator == "le" && val > c.value) ||
(c.operator == "ge" && val < c.value)
) {
applicable = false;
}
}
applicable = true;
};
this.setDefault = function() {
value = definition.dfault;
};
if (initval) {
self.setDefault();
}
this.getAutomationData = function() {
if (!self.modulation) return;
var m = twist.appdata.modulations[self.modulation];
return [m.instr, self.channel];
};
function showModulations() {
modulationShown = true;
elValueLabel.hide();
elInput.hide();
elModulations.show();
elModButton.text("Close");
if (elModulations.children().length != 0) {
elModSelect.val(0).trigger("change");
return;
}
var tb = $("
");
function buildModulation(i) {
tb.empty();
modulationParameters = [];
self.modulation = i;
let m = twist.appdata.modulations[i];
for (let x of m.parameters) {
var tp = new twst.Parameter(m.instr, x, self, transform, twist);
modulationParameters.push(tp);
tb.append(tp.getElementRow(true)); // hmm modulate the modulation with false
}
}
var selecttb = $(" ").appendTo($(")").appendTo(elModulations));
var row = $(" ").append($(" ").text("Modulation type")).appendTo(selecttb);
elModSelect = $(" ").change(function() {
self.modulation = $(this).val();
buildModulation(self.modulation);
automation.push(self);
}).appendTo($(" ").appendTo(row));
$("").append(tb).appendTo(elModulations);
for (let i in twist.appdata.modulations) {
var m = twist.appdata.modulations[i];
$(" ").text(m.name).val(i).appendTo(elModSelect);
}
elModSelect.val(0).trigger("change");
}
};
function getTransformContainer(name) {
return $("
").addClass("tfv_container").append(
$("
").addClass("tfv_header").text(name)
);
}
twst.ParameterGroup = function(def, instance, twist) {
var self = this;
this.instr = def.instr;
this.refreshable = false;
var presetParameters;
this.parameters = {};
if (def.hasOwnProperty("preset") && def.preset == "pvsynth") {
var conditions = [
{channel: "pvresmode", operator: "eq", value: 1}
];
presetParameters = [
{name: "Resynth mode", channel: "pvresmode", description: "Type of FFT resynthesis used", dfault: 0, options: ["Overlap-add", "Additive"], automatable: false},
{name: "Oscillator spread", channel: "pvaoscnum", description: "Number of oscillators used", automatable: false, conditions: conditions},
{name: "Frequency modulation", channel: "pvafreqmod", description: "Frequency modulation", dfault: 1, min: 0.01, max: 2, conditions: conditions},
{name: "Oscillator offset", channel: "pvabinoffset", description: "Oscillator bin offset", automatable: false, conditions: conditions},
{name: "Oscillator increment", channel: "pvabinoffset", description: "Oscillator bin increment", min: 1, max: 32, dfault: 1, step: 1, automatable: false, conditions: conditions}
];
}
this.refresh = function() {
if (!self.refreshable) {
return;
}
for (var k in self.parameters) {
self.parameters[k].refresh();
}
};
this.getAutomationData = function() {
var automations = [];
for (var k in self.parameters) {
var data = self.parameters[k].getAutomationData();
if (data) {
automations.push(data);
}
}
return automations;
};
this.removeParameter = function(channel) {
if (self.parameters.hasOwnProperty(channel)) {
self.parameters[channel].remove();
delete self.parameters[channel]
}
};
this.addParameter = function(pdef) {
var tp = new twst.Parameter(def.instr, pdef, null, self, twist);
self.parameters[tp.channel] = tp;
return tp;
};
function build() {
getTransformContainer(def.name).appendTo(elContainer);
var tbl = $("").appendTo(elContainer);
elTb = $(" ").appendTo(tbl);
for (let p of def.parameters) {
self.addParameter(p);
}
if (presetParameters) {
for (let p of presetParameters) {
self.addParameter(p);
}
}
self.refresh();
}
build();
};
var Transform = function(definition, instance, twist) {
var parameterGroup = new ParameterGroup(definition, instance, twist);
Object.defineProperty(this, "parameterGroup", {
get: function() { return parameterGroup; },
set: function(x) {}
});
Object.defineProperty(this, "parameters", {
get: function() { return parameterGroup.parameters; },
set: function(x) {}
});
function handleAutomation(onready) {
if (transform) {
var automations = transform.getAutomationData();
if (automations && automations.length > 0) {
var cbid = app.createCallback(function(ndata){
if (ndata.status == 1) {
onready(1);
} else {
return twist.errorHandler("Cannot parse automation data");
}
});
var call = [0, 1, cbid];
for (let i in automations) {
call.push(automations[i][0] + " \\\"" + automations[i][1] + "\\\"");
}
twist.csapp.insertScore("twst_automationprepare", call);
} else {
onready(0);
}
}
}
this.audition = function(start, end, timeUnit) {
if (twist.isProcessing || twist.inUse) return twist.errorHandler("Already in use");
errorState = "Playback error";
if (!start) {
start = instance.selection.ratio[0];
end = instance.selection.ratio[1];
} else {
if (!timeUnit) timeUnit = "seconds");
start = timeConvert(start, timeUnit);
end = timeConvert(end, timeUnit);
}
handleAutomation(function(automating){
var cbid = playPositionHandler();
operation({
instr: "twst_audition",
score: [start, end, instance.selectedChannel, definition.instr, automating]
});
});
};
this.commit = function(start, end, timeUnitPos, crossfadeIn, crossfadeOut, timeUnitCrossfade) {
if (twist.isProcessing || twist.inUse) return twist.errorHandler("Already in use");
handleAutomation(function(automating){
if (!start) {
start = instance.selection.start.ratio;
end = instance.selection.end.ratio;
} else {
if (!timeUnitPos) timeUnitPos = "seconds");
start = timeConvert(start, timeUnitPos);
end = timeConvert(end, timeUnitPos);
}
if (!crossfadeIn) {
crossfadeIn = instance.selection.ratio[0];
crossfadeOut = instance.selection.ratio[1];
} else {
if (!timeUnitPos) timeUnitPos = "seconds");
crossfadeIn = timeConvert(start, timeUnitPos);
crossfadeOut = timeConvert(end, timeUnitPos);
}
errorState = "Transform commit error";
operation({
instr: "twst_commit",
refresh: true,
score: [start, end, instance.selectedChannel, definition.instr, automating, instance.crossFade.start.ratio, instance.crossFade.end.ratio]
});
});
};
};
var TwistInstance = function(instanceIndex, twist, options) {
var self = this;
if (!options) options = {};
var transform;
var channels;
var durationSamples;
var selectedChannel = -1;
var filename;
var sr;
var csTables = [];
var Time = function(dfault, onValidate, onChange) {
var tself = this;
var value = dfault;
Object.defineProperty(this, "samples", {
get: function() { return value; },
set: function(v) {
if (value == v) return;
value = v;
if (onValidate) {
var res = onValidate(value);
if (res) {
value = res;
}
}
if (onChange) onChange(tself);
}
});
Object.defineProperty(this, "seconds", {
get: function() { return value / sr; },
set: function(v) {
tself.samples = Math.round(v * sr);
}
});
Object.defineProperty(this, "ratio", {
get: function() { return value / durationSamples; },
set: function(v) {
tself.samples = Math.round(v * durationSamples);
}
});
};
var playPosition = new Time(0);
var selection = new Time({start: 0, end: 0}, function(v) {
if (typeof(v) != "object") {
v = {start: v, end: v};
return v;
}
if (v.start > v.end) {
v.start = v.end
}
if (v.end > durationSamples) {
v.end = durationSamples);
}
}, options.onSelectionChange);
var crossFade = new Time({start: 0, end: 0}, function(v) {
iif (typeof(v) != "object") {
v = {start: v, end: v};
return v;
}
var half = Math.round(durationSamples * 0.5);
if (v.start > half) {
v.start = half;
}
if (v.end > half) {
v.end = half;
}
}, options.onCrossFadeChange);
Object.defineProperty(this, "selectedChannel", {
get: function() { return selectedChannel; },
set: function(v) {
if (channels == 1) return;
if (v >= channels) {
selectedChannel = channels - 1;
} else if (v < 0) {
selectedChannel = 0;
} else {
selectedChannel = v;
}
}
});
Object.defineProperty(this, "playPosition", {
get: function() { return playPosition; },
set: function(v) {}
});
Object.defineProperty(this, "selection", {
get: function() { return selection; },
set: function(v) {}
});
Object.defineProperty(this, "crossFade", {
get: function() { return crossFade; },
set: function(v) {}
});
Object.defineProperty(this, "instanceIndex", {
get: function() { return instanceIndex; },
set: function(v) {}
});
var durationObj;
Object.defineProperty(durationObj, "samples", {
get: function() { return durationSamples; },
set: function(v) {}
});
Object.defineProperty(durationObj, "seconds", {
get: function() { return durationSamples / sr; },
set: function(v) {}
});
Object.defineProperty(this, "duration", {
get: function() { return durationObj; },
set: function(v) {}
});
Object.defineProperty(this, "filename", {
get: function() { return filename; },
set: function(v) {}
});
Object.defineProperty(this, "sr", {
get: function() { return sr; },
set: function(v) {}
});
Object.defineProperty(this, "csTables", {
get: function() { return csTables; },
set: function(v) {}
});
function refresh(data) {
twist.errorState = "Overview refresh error";
csTables = [data.waveL];
if (data.hasOwnProperty("waveR")) {
csTables.push(data.waveR);
}
sr = data.sr;
durationSamples = Math.round(data.sr * data.duration);
if (options.onRefresh) options.onRefresh(self);
}
function getTransform(path) {
if (!twist.transforms.hasOwnProperty(path)) {
return;
}
return new ParameterGroup(transforms[path], self, twist);
}
function operation(data) {
if (!data.setUsage) data.setUsage = true;
if (!data.setProcessing) data.setProcessing = true;
if (twist.inUse || twist.isProcessing) {
return twist.errorHandler("Already processing");
}
var score = [0, -1];
if (!data.noCallback) {
cbid = twist.csapp.createCallback(function(ndata){
if (data.refresh) refresh();
if (data.onComplete) data.onComplete(ndata);
if (data.setUsage) twist.inUse = false;
if (data.setProcessing) twist.isProcessing = false;
});
score.push(cbid);
if (data.setUsage) twist.inUse = true;
if (data.setProcessing) twist.isProcessing = true;
}
if (data.onRun) options.onRun();
if (data.score) {
for (s of data.score) {
score.push(s);
}
}
twist.csapp.insertScore(data.instr, score);
} // operation
function loadFile(name) {
var cbid = twist.csapp.createCallback(async function(ndata){
await app.getCsound().fs.unlink(name);
if (ndata.status == 0) {
return twist.errorHandler("File not valid");
} else {
refresh(ndata);
}
twist.inUse = false;
twist.isProcessing = false;
});
twist.inUse = true;
twist.isProcessing = true;
app.insertScore("twst_loadfile", [0, -1, cbid, item.name]);
}
this.loadUrl = function(url, onLoad) {
twist.csapp.loadFile(url, loadFile);
};
this.loadBuffer = function(arrayBuffer, onLoad) {
twist.csapp.loadBuffer(url, loadFile);
};
this.saveFile = function(name, onSave) {
if (!onSave) {
onSave = options.onSave;
}
if (!onSave) {
return twist.errorHandler("Instance or saveFile onSave option has not been provided");
}
twist.inUse = true;
twist.isProcessing = true;
if (!name) {
name = filename;
}
if (!name) {
name = "export.wav";
}
if (!name.toLowerCase().endsWith(".wav")) {
name += ".wav";
}
var cbid = twist.csapp.createCallback(async function(ndata){
var content = await twist.csapp.getCsound().fs.readFile(name);
var blob = new Blob(content, {type: "audio/wav"});
var url = window.URL.createObjectURL(blob);
onSave(url);
twist.inUse = false;
twist.isProcessing = false;
});
twist.csapp.insertScore("twst_savefile", [0, -1, cbid, name]);
};
function timeConvert(val, mode) { // returns ratio right now
if (mode == "ratio") {
return val;
} else if (mode == "samples") {
return val / durationSamples;
} else if (mode == "seconds") {
return val / (durationSamples / sr);
}
}
this.cut = function(start, end, timeUnit) {
if (!start) {
start = self.selection.ratio[0];
end = self.selection.ratio[1];
} else {
if (!timeUnit) timeUnit = "seconds");
start = timeConvert(start, timeUnit);
end = timeConvert(end, timeUnit);
}
operation({
instr: "twst_cut",
score: [start, end, selectedChannel],
refresh: true,
});
};
this.copy = function(start, end, timeUnit) {
if (!start) {
start = self.selection.ratio[0];
end = self.selection.ratio[1];
} else {
if (!timeUnit) timeUnit = "seconds");
start = timeConvert(start, timeUnit);
end = timeConvert(end, timeUnit);
}
operation({
instr: "twst_copy",
score: [start, end, selectedChannel],
});
};
this.paste = function(start, end, timeUnit) {
if (!start) {
start = self.selection.ratio[0];
end = self.selection.ratio[1];
} else {
if (!timeUnit) timeUnit = "seconds");
start = timeConvert(start, timeUnit);
end = timeConvert(end, timeUnit);
}
operation({
instr: "twst_paste",
score: [start, end, selectedChannel],
});
};
this.pasteSpecial = function(start, end, timeUnit) {
pasteSpecial: {instr: "twst_pastespecial", refresh: true, parameters: [
{name: "Repetitions", channel: "repetitions", min: 1, max: 40, step: 1, dfault: 1, automatable: false},
{name: "Repetition random time variance ratio", channel: "timevar", min: 0, max: 1, step: 0.000001, dfault: 0, 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}]}
]},
};
this.play = function(start, end, timeUnit) {
errorState = "Playback error";
if (!start) {
start = self.selection.ratio[0];
end = self.selection.ratio[1];
} else {
if (!timeUnit) timeUnit = "seconds");
start = timeConvert(start, timeUnit);
end = timeConvert(end, timeUnit);
}
operation({
instr: "twst_play",
score: [start, end, selectedChannel],
});
};
this.stop = function() {
operation({
instr: "twst_stop"
});
};
};
var Twist = function(options) {
var twist = this;
var inUse = false;
var isProcessing = false;
var instanceIndex = 0;
var instances = [];
var transforms;
var modulations;
var onRunFunc;
this.errorState = null;
if (!options) options = {};
if (!options.appdata) {
const xhr = new XMLHttpRequest();
xhr.open("GET", "appdata.json", false);
xhr.send();
if (xhr.status == 200) {
options.appdata = JSON.parse(xhr.responseText);
} else {
throw "No appdata available";
}
}
function errorHandlerInner(error, func) {
if (!error && twist.errorState) {
error = twist.errorState;
twist.errorState = null;
} elseif (!error && !twist.errorState) {
error = "Unhandled error";
}
func(error);
}
this.errorHandler = function(error) {
if (!error && twist.errorState) {
error = twist.errorState;
twist.errorState = null;
} elseif (!error && !twist.errorState) {
error = "Unhandled error";
}
if (options.errorHandler) {
options.errorHandler(error);
} else {
throw error;
}
};
this.setPercent = function(percent) {
if (options.onPercentChange) {
options.onPercentChange(percent);
}
};
if (!csapp) {
csapp = new CSApplication({
csdUrl: "twist.csd",
csOptions: ["--omacro:TWST_FAILONLAG=1"],
onPlay: function () {
if (onRunFunc) onRunFunc();
},
errorHandler: options.errorHandler,
ioReceivers: {percent: twist.setPercent}
});
}
Object.defineProperty(this, "csapp", {
get: function() {
return csapp;
},
set: function(v) {}
});
Object.defineProperty(this, "instances", {
get: function() {
return instances;
},
set: function(v) {}
});
Object.defineProperty(this, "transforms", {
get: function() {
return transforms;
},
set: function(v) {}
});
Object.defineProperty(this, "modulations", {
get: function() {
return modulations;
},
set: function(v) {}
});
Object.defineProperty(this, "appdata", {
get: function() {
return options.appdata;
},
set: function(v) {}
});
Object.defineProperty(this, "inUse", {
get: function() {
return inUse;
},
set: function(v) {
if (inUse != v) {
inUse = v;
if (options.onUsage) options.onUsage(v);
}
}
});
Object.defineProperty(this, "isProcessing", {
get: function() {
return isProcessing;
},
set: function(v) {
if (isProcessing != v) {
isProcessing = v;
if (options.onUsage) options.onUsage(v);
}
}
});
this.run = function(onRunFunc) {
onRunFunc = onRun;
csapp.play();
};
this.createInstance = function() {
var instance = new TwistInstance(instanceIndex, twist, options.instance);
instances[instanceIndex] = instance;
instanceIndex ++;
return instance;
};
this.removeInstanceByIndex = function(index) {
if (i < 0 || i > instances.length - 2) return;
delete instances[index];
};
function getProcesses(appdata, type) {
var processes = {};
function recurse(items, prefix) {
if (!prefix) {
prefix = "/";
}
for (let item of items) {
if (item.hasOwnProperty("contents")) {
var subitems = recurse(item.contents, prefix + item.name + "/");
} else {
processes[prefix + item.name] = item;
}
}
}
recurse(appdata[type]);
return processes;
}
transforms = getProcesses(options.appdata, "transforms");
modulations = getProcesses(options.appdata, "modulations");
};
window.t = new Twist({
csapp: null,
appdata: null,
latencyCorrection: 170,
onPercentChange,
onProcessing: state => {
},
onUsage: state => {
},
instance: {
onPlayPositionChange: position => {
},
onSelectionChange: selection => {
},
onCrossFadeChange: crossfades => {
},
onRefresh: () => {
},
onPlay: () => {
},
onSave: () => {
}
}
});
$("#start_invoke").click(function(){
$("#loading").show();
t.run(function(){
$("#start").hide();
$("#loading").hide();
});
});