aboutsummaryrefslogtreecommitdiff
path: root/site/app/twine/channel.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/twine/channel.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/twine/channel.js')
-rw-r--r--site/app/twine/channel.js499
1 files changed, 499 insertions, 0 deletions
diff --git a/site/app/twine/channel.js b/site/app/twine/channel.js
new file mode 100644
index 0000000..18f1b3a
--- /dev/null
+++ b/site/app/twine/channel.js
@@ -0,0 +1,499 @@
+var Insert = function(channel, options) {
+ var insert = this;
+};
+
+
+var Channel = function(timeline, index) {
+ var channel = this;
+ this.clips = {};
+ this.width = null;
+ this.offset = null;
+ var heights = [25, 100];
+ this.height = heights[0];
+ this.index = index;
+ this.name = "Channel " + (index + 1);
+ var elChannel = $("<div />").addClass("twine_channel").css({
+ height: channel.height + "px"
+ }).appendTo(timeline.element);
+ this.inserts = [];
+ var splines = {};
+
+ var heightState = 0;
+ var elChannelControl = $("<div />").addClass("twine_channelcontrol").text(channel.name).appendTo(elChannel).dblclick(function(){
+ heightState = 1 - heightState;
+ channel.setHeight(heights[heightState]);
+ }).click(function(){
+ channel.showDetails();
+ }).on("contextmenu", function(e){
+ return twirl.contextMenu.show(e, [
+ {name: "Rename", click: function(){
+ var el = $("<div />");
+ $("<h4 />").text("Rename channel").appendTo(el);
+ var ti = $("<input />").val(channel.name).appendTo(el);
+ twirl.prompt.show(el, function(){
+ channel.setName(ti.val());
+ });
+ }}
+ ]);
+ });
+
+ var elChannelClips = $("<div />").addClass("twine_channelclips").appendTo(elChannel).mousedown(twine.timeline.dragSelection);
+
+ var elChannelSpline = $("<div />").css({
+ position: "absolute",
+ width: "100%",
+ height: "75%",
+ left: "0px",
+ top: "25%",
+ "z-index": 31,
+ opacity: 0.57,
+ "background-color": "var(--bgColor1)",
+ display: "none"
+ }).appendTo(elChannelClips);
+
+
+ channel.defaultChannels = {
+ amp: "twine_camp" + channel.index,
+ pan: "twine_cpan" + channel.index,
+ mute: "twine_cmute" + channel.index,
+ solo: twine.mixer.soloChannel
+ };
+
+
+ this.setName = function(name) {
+ channel.name = name;
+ elChannelControl.text(channel.name);
+ };
+
+ this.exportData = async function() {
+ var saveData = {
+ clips: [],
+ inserts: [],
+ name: channel.name,
+ amp: (await app.getControlChannel(channel.defaultChannels.amp)),
+ pan: (await app.getControlChannel(channel.defaultChannels.pan))
+ };
+
+ for (var ins in channel.inserts) {
+
+ }
+ for (var i in channel.clips) {
+ if (channel.clips[i]) {
+ saveData.clips.push(await channel.clips[i].exportData());
+ }
+ }
+ return saveData;
+ };
+
+ this.remove = function(noredraw) {
+ elChannel.remove();
+ console.log("remove channel");
+ for (let c in channel.clips) {
+ if (channel.clips[c]) channel.clips[c].destroy();
+ }
+ for (let c in timeline.channels) {
+ if (timeline.channels[c] == channel) {
+ timeline.channels.splice(c, 1);
+ }
+ }
+ if (!noredraw) timeline.drawGrid();
+ };
+
+ this.importData = async function(loadData, ftMap) {
+ await app.setControlChannel(channel.defaultChannels.amp, loadData.amp);
+ await app.setControlChannel(channel.defaultChannels.pan, loadData.pan);
+ channel.setName(loadData.name);
+ for (var i in channel.clips) {
+ channel.clips[i].remove();
+ delete channel.clips[i];
+ }
+
+ for (let cl of loadData.clips) {
+ if (cl.table[0]) {
+ cl.table[0] = ftMap[cl.table[0]];
+ }
+ if (cl.table[1]) {
+ cl.table[1] = ftMap[cl.table[1]];
+ }
+ var c = new Clip(twine);
+ c.importData(cl);
+ channel.addClip(c);
+ }
+ };
+
+ this.addClip = function(clip) {
+ channel.clips[clip.data.id] = clip;
+ clip.channel = channel;
+ elChannelClips.append(clip.element);
+ clip.redraw();
+ };
+
+ this.removeClip = function(clip) {
+ //clip.element.detach();
+ //clip.channel = null;
+ channel.clips[clip.data.id] = null;
+ //delete channel.clips[clip.data.id];
+ };
+
+ this.changeBeatEnd = function(original, newBeatEnd, noredraw) {
+ var ratio = original / newBeatEnd;
+ for (let s in splines) {
+ splines[s].spline.resize(ratio, noredraw);
+ }
+ };
+
+
+ this.getCsChannelName = function() {
+ return "mxchan" + channel.index;
+ };
+
+ var elAddInsert;
+ var elInsertsArea;
+ var insertID = 0;
+ function removeInsert(id, noCompile) {
+ var index;
+ for (let ci in channel.inserts) {
+ if (channel.inserts[ci].id == id) {
+ index = ci;
+ break;
+ }
+ }
+ if (index == null) return;
+ var i = channel.inserts[index];
+ i.element.remove();
+ i.transform.remove();
+ channel.inserts.splice(index, 1);
+ refreshInserts();
+ if (!noCompile) channel.compileInstr();
+ }
+
+
+ async function createDefaultSplines() {
+ var splineColour = getComputedStyle(document.body).getPropertyValue("--fgColor2");
+ var ampVal, panVal;
+ if (twine.offline) {
+ ampVal = 1;
+ panVal = 0;
+ } else {
+ ampVal = await app.getControlChannel(channel.defaultChannels.amp);
+ panVal = await app.getControlChannel(channel.defaultChannels.pan);
+ }
+ var def = [
+ {name: "default_amp", dfault: 1, constraints: [0, 2, ampVal, 0.00001], channel: channel.defaultChannels.amp},
+ {name: "default_pan", dfault: 0, constraints: [-1, 1, panVal, 0.00001], channel: channel.defaultChannels.pan}
+ ];
+
+ for (let d of def) {
+ splines[d.name] = {
+ element: $("<div />").addClass("twca" + channel.index).addClass("twine_spline").attr("id", "twca" + channel.index + d.name).hide().appendTo(elChannelSpline),
+ show: function() {
+ $(".twca" + channel.index).hide();
+ splines[d.name].element.show();
+ splines[d.name].spline.redraw();
+ },
+ hide: function() {
+ splines[d.name].element.hide();
+ },
+ channel: d.channel
+ };
+ }
+ for (let d of def) {
+ splines[d.name].spline = new SplineEdit(
+ splines[d.name].element, splineColour,
+ twine.timeline.getTotalBeatDuration,
+ d.constraints, d.name
+ )
+ }
+ }
+
+
+ function addInsert(definition, noCompile) {
+ var container = $("<div />").addClass("twine_channeldetails_insert").appendTo(elInsertsArea);
+ container.css("left", (channel.inserts.length * 500) + "px");
+ var id = insertID ++;
+ var uniqueID = parseInt(channel.index.toString() + id.toString());
+ var t = new twirl.transform.Transform({
+ uniqueID: uniqueID,
+ element: container,
+ definition: definition,
+ splineElement: elChannelSpline,
+ getRegionFunc: function() {
+ return [0, 1];
+ },
+ getDurationFunc: timeline.getTotalBeatDuration,
+ onClose: function() {
+ removeInsert(id);
+ },
+ onAutomationClick: function(state) {
+ if (state && channel.height == heights[0]) {
+ channel.expand(); // TODO: set selected thing in dropdown
+ }
+ },
+ unmanagedAutomation: true,
+ unmanagedModulation: true,
+ host: twine
+ });
+ channel.inserts.push({
+ transform: t,
+ element: container,
+ id: id
+ });
+
+ for (let i in t.parameters) {
+ let paramID = (channel.inserts.length - 1) + "_" + i;
+ let tp = t.parameters[i];
+ splines[paramID] = {
+ element: $("<div />").addClass("twca" + channel.index).addClass("twine_spline").attr("id", "twca" + channel.index + paramID).hide().appendTo(elChannelSpline),
+ show: function() {
+ $(".twca" + channel.index).hide();
+ splines[paramID].element.show();
+ splines[paramID].spline.redraw();
+ },
+ hide: function() {
+ splines[paramID].element.hide();
+ },
+ channel: tp.sendChannel
+ };
+ tp.createAutomationSpline(splines[paramID].element);
+ splines[paramID].spline = tp.automation;
+ }
+ refreshAutomationSelectors();
+ if (!noCompile) channel.compileInstr();
+ }
+
+ this.hasOverlap = function(clip, proposedBeat, proposedLength) {
+ if (!proposedBeat) proposedBeat = clip.data.position;
+ if (!proposedLength) proposedLength = clip.data.playLength;
+ var clipEnd;
+ var proposedEnd;
+ for (var i in channel.clips) {
+ var c = channel.clips[i];
+ if (c && c != clip) {
+ clipEnd = c.data.position + c.data.playLength;
+ proposedEnd = proposedBeat + proposedLength;
+ if ((proposedBeat < clipEnd && proposedEnd > c.data.position) || (c.data.position > proposedBeat && clipEnd < proposedEnd)) {
+ return true;
+ }
+ }
+ }
+ return false;
+ };
+
+ function refreshInserts() {
+ elInsertsArea.empty();
+ for (let i in channel.inserts) {
+ var el = channel.inserts[i].element;
+ el.css("left", (i * 500) + "px");
+ elInsertsArea.append(el);
+ }
+ }
+
+ this.showDetails = function() {
+ if (!elAddInsert) {
+ elAddInsert = $("<div />").addClass("twine_channeldetails_insertnew");
+ elInsertsArea = $("<div />").addClass("twine_channeldetails_inserts");
+ var data = {};
+ for (var c of twirl.appdata.transforms) {
+ var catName = c.name;
+ for (var t of c.contents) {
+ if (t.twine) {
+ if (!data[c.name]) {
+ data[c.name] = {
+ name: c.name,
+ description: c.description,
+ contents: []
+ };
+ }
+ data[c.name].contents.push(t);
+ }
+ }
+ }
+ var ttv = new twirl.transform.TreeView({
+ element: elAddInsert,
+ items: Object.values(data),
+ click: addInsert
+ });
+ }
+ if (twine.timeline.selectedChannel != channel) {
+ twine.timeline.selectedChannel = channel;
+ $(".twine_channelcontrol").css("background-color", "var(--bgColor2)");
+ elChannelControl.css("background-color", "var(--bgColor1)");
+ $("#twine_channeldetails").append(elAddInsert).append(elInsertsArea).show();
+ }
+ twine.ui.showPane(twine.ui.pane.CHANNEL);
+ };
+
+ this.compileInstr = async function() {
+ if (twine.offline) return;
+ var instrName = "twine_channel" + channel.index;
+ var instr = "instr " + instrName + "\n"
+ + "aL, aR bus_read \"" + channel.getCsChannelName() + "\"\n";
+ for (let i of channel.inserts) {
+ instr += "chnset aL, \"twstfeedL\"\n"
+ + "chnset aR, \"twstfeedR\"\n"
+ + "aoutL, aoutR subinstr \"" + i.transform.instr + "\", " + i.transform.uniqueID + "\n"
+ + "aL, aR twst_setapplymode chnget:i(\"applymode" + i.transform.uniqueID + "\"), aL, aR, aoutL, aoutR\n"
+ }
+ instr += "kamp chnget \"" + channel.defaultChannels.amp + "\"\n" +
+ "kpan chnget \"" + channel.defaultChannels.pan + "\"\n" +
+ "kmute chnget \"" + channel.defaultChannels.mute + "\"\n" +
+ "ksolo chnget \"" + channel.defaultChannels.solo + "\"\n" +
+ "kaudible = (1 - kmute) * ((ksolo == -1 || ksolo == " + channel.index + ") ? 1 : 0)\n" +
+ "aL *= kamp * (1 - kpan) * kaudible\naR *= kamp * kpan * kaudible\n"
+ instr += "bus_mix \"twine_master\", aL, aR\n";
+ instr += "endin";
+ await app.compileOrc(instr);
+ };
+
+ this.automationChanged = function() {
+ var changed = false;
+ for (let i in splines) {
+ if (splines[i].spline && splines[i].spline.changed) {
+ console.log("automation changed");
+ return true;
+ }
+ }
+ return changed;
+ };
+
+ this.getAutomationData = function(start, end) {
+ var data = [];
+ for (let i in splines) {
+ var linsegData = splines[i].spline.getLinsegData(start, end, true);
+ if (linsegData) {
+ data.push("chnset linseg:k(" + linsegData + "), \"" + splines[i].channel + "\"");
+ }
+ }
+ return data;
+ };
+
+ this.refreshOffset = function() {
+ channel.offset = elChannelClips.offset();
+ };
+
+ this.redraw = function(noClipWaveRedraw) {
+ channel.width = parseFloat(elChannelClips.css("width"));
+ channel.refreshOffset();
+ refreshAutomationSelectors();
+ for (let c in channel.clips) {
+ if (channel.clips[c]) channel.clips[c].redraw(noClipWaveRedraw);
+ }
+
+ if (channel.height > heights[0]) {
+ for (var d in splines) {
+ console.log(splines[d]);
+ splines[d].spline.setRange(timeline.data.regionStart, timeline.data.regionEnd);
+ }
+ }
+ };
+
+ this.showAutomation = function(groupIndex, nameIndex) {
+ elAutomationSelectGroup.val(groupIndex).change();
+ elAutomationSelector.val(nameIndex).change();
+ };
+
+ var elAutomationSelectGroup;
+ var elAutomationSelector;
+ function refreshAutomationSelectors() {
+ if (channel.height <= heights[0]) {
+ if (elAutomationSelectGroup && elAutomationSelector) {
+ elAutomationSelectGroup.hide();
+ elAutomationSelector.hide();
+ }
+ return;
+ }
+
+ var valGroup = (elAutomationSelectGroup) ? elAutomationSelectGroup.val() : 0;
+ var valItem = (elAutomationSelector) ? elAutomationSelector.val() : 0;
+
+ var items = [
+ {name: "Mixer", items: [
+ {name: "Gain", onselect: function(){
+ splines.default_amp.show();
+ }},
+ {name: "Pan", onselect: function(){
+ splines.default_pan.show();
+ }}
+ ]}
+ ];
+
+ for (let i in channel.inserts) {
+ let ci = channel.inserts[i];
+ var obj = {name: ci.transform.name, items: []};
+ items.push(obj);
+ for (let it in ci.transform.parameters)
+ obj.items.push({
+ name: ci.transform.parameters[it].definition.name,
+ onselect: function() {
+ splines[i + "_" + it].show();
+ }
+ });
+ }
+
+ if (!elAutomationSelectGroup && !elAutomationSelector) {
+ var el = $("<div />").addClass("twine_automationselectors").appendTo(elChannelControl);
+ elAutomationSelectGroup = $("<select />").addClass("twine_automationselect").appendTo(el).on("change", changeAutomationGroup);
+ el.append("<br />");
+ elAutomationSelector = $("<select />").addClass("twine_automationselect").appendTo(el).on("change", changeAutomationItem);
+
+ for (let i in items) {
+ $("<option />").val(i).text(items[i].name).appendTo(elAutomationSelectGroup);
+ }
+ }
+
+ elAutomationSelectGroup.show();
+ elAutomationSelector.show();
+
+ function changeAutomationGroup() {
+ var val = $(this).val();
+ elAutomationSelector.empty();
+ for (let si in items[val].items) {
+ $("<option />").val(si).text(items[val].items[si].name).appendTo(elAutomationSelector);
+ }
+ elAutomationSelector.val(0).change();
+ }
+
+ function changeAutomationItem() {
+ var groupVal = elAutomationSelectGroup.val();
+ var val = $(this).val();
+ items[groupVal].items[val].onselect();
+ }
+
+ elAutomationSelectGroup.val(valGroup).change();
+ elAutomationSelector.val(valItem).change();
+ }
+
+ this.setHeight = function(h) {
+ if (h < heights[0]) h = heights[0];
+ channel.height = h;
+ elChannel.css("height", channel.height + "px");
+ if (h == heights[0]) {
+ elChannelSpline.hide();
+ } else {
+ elChannelSpline.show();
+ for (let i of channel.inserts) {
+ i.transform.redraw(timeline.data.regionStart, timeline.data.regionEnd);
+ }
+ }
+ channel.redraw();
+ };
+
+ this.expand = function() {
+ channel.setHeight(heights[1]);
+ };
+
+ this.contract = function() {
+ channel.setHeight(heights[0]);
+ };
+
+ (async function boot() {
+ channel.redraw();
+ if (!twine.offline) {
+ await app.setControlChannel(channel.defaultChannels.amp, 0.8);
+ await app.setControlChannel(channel.defaultChannels.pan, 0.5);
+ await app.setControlChannel(channel.defaultChannels.mute, 0);
+ await channel.compileInstr();
+ }
+ await createDefaultSplines();
+ })();
+}; \ No newline at end of file