aboutsummaryrefslogtreecommitdiff
path: root/site/app/base/base.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/base/base.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/base/base.js')
-rw-r--r--site/app/base/base.js512
1 files changed, 512 insertions, 0 deletions
diff --git a/site/app/base/base.js b/site/app/base/base.js
new file mode 100644
index 0000000..8bf12f2
--- /dev/null
+++ b/site/app/base/base.js
@@ -0,0 +1,512 @@
+var CSApplication = function(appOptions) {
+ var self = this;
+ var version = 1.0;
+ var debug = window.location.protocol.startsWith("file");
+ var baseUrl;
+ if (debug) {
+ baseUrl = "https://apps.csound.1bpm.net";
+ } else {
+ baseUrl = window.location.origin;
+ }
+ var cbid = 0;
+ var callbacks = {};
+ var appPath;
+ var udoReplacements = {
+ "/interop.udo": "/interop.web.udo",
+ "/sequencing_melodic_persistence.udo": "/sequencing_melodic_persistence.web.udo"
+ };
+ var defaultStorage = {version: version};
+ var storage = localStorage.getItem("csound");
+ if (storage) {
+ storage = JSON.parse(storage);
+ if (!storage.version || storage.version != version) {
+ storage = defaultStorage;
+ }
+ } else {
+ storage = defaultStorage;
+ }
+
+ function saveStorage() {
+ localStorage.setItem("csound", JSON.stringify(storage));
+ }
+
+
+ const defaultOptions = {
+ csdUrl: null,
+ csOptions: null,
+ trackMouse: false,
+ trackTouch: false,
+ trackClick: false,
+ trackMouseSpeed: false,
+ trackTouchSpeed: false,
+ keyDownScore: null, // score line to insert with code as p4, or function passed event to return score line
+ keyUpScore: null,
+ onPlay: null,
+ onStop: null,
+ ioReceivers: null,
+ files: null
+ };
+ let csound = null;
+
+ function basename(path) {
+ return path.split("/").reverse()[0];
+ }
+
+ this.getCsound = function() {
+ return csound;
+ }
+
+ if (!appOptions) {
+ appOptions = defaultOptions;
+ } else {
+ for (var key in defaultOptions) {
+ if (!appOptions.hasOwnProperty(key)) {
+ appOptions[key] = defaultOptions[key];
+ }
+ }
+ }
+
+ async function copyDataToLocal(arrayBuffer, name) {
+ if (!csound) return;
+ const buffer = new Uint8Array(arrayBuffer);
+ await csound.fs.writeFile(name, buffer);
+ return Promise.resolve();
+ }
+
+ async function copyUrlToLocal(url, name) {
+ if (!csound) return;
+ const response = await fetch(url);
+ const arrayBuffer = await response.arrayBuffer();
+ await copyDataToLocal(arrayBuffer, name);
+ return Promise.resolve();
+ }
+
+ this.loadUrl = function(url, a2, a3) {
+ var name;
+ var func;
+ if (typeof(a2) == "function") {
+ name = basename(url);
+ func = a2;
+ } else {
+ name = a2;
+ func = a3;
+ }
+ copyUrlToLocal(url, name).then(() => {
+ if (func) func(name);
+ });
+ };
+
+ this.loadBuffer = function(buffer, a2, a3) {
+ var name;
+ var func;
+ if (typeof(a2) == "function") {
+ name = basename(url);
+ func = a2;
+ } else {
+ name = a2;
+ func = a3;
+ }
+ copyDataToLocal(buffer, name).then(() => {
+ if (func) func(name);
+ });
+ }
+
+ function runCallback(data) {
+ if (!callbacks[data.cbid]) {
+ return;
+ }
+ callbacks[data.cbid].func(data);
+ if (callbacks.hasOwnProperty(data.cbid) && !callbacks[data.cbid].persist) {
+ self.removeCallback(data.cbid);
+ }
+ }
+
+ this.createCallback = function(func, persist) {
+ thisCbid = cbid;
+ callbacks[thisCbid] = {func: func, persist: persist};
+ if (cbid > 999999) {
+ cbid = 0;
+ } else {
+ cbid ++;
+ }
+ return thisCbid;
+ };
+
+ this.removeCallback = function(cbid) {
+ delete callbacks[cbid];
+ };
+
+ this.enableAudioInput = async function() {
+ if (!csound) return;
+ return (await csound.enableAudioInput());
+ };
+
+ this.unlinkFile = function(path) {
+ if (!csound) return;
+ csound.fs.unlink(path);
+ };
+
+ this.readFile = async function(path) {
+ if (!csound) return;
+ return (await csound.fs.readFile(path));
+ };
+
+ this.writeFile = async function(path, buffer) {
+ return (await csound.fs.writeFile(path, buffer));
+ };
+
+ this.setControlChannel = function(name, value) {
+ if (!csound) return;
+ csound.setControlChannel(name, value);
+ };
+
+ this.getControlChannel = async function(name) {
+ if (!csound) return;
+ return await csound.getControlChannel(name);
+ };
+
+ this.setStringChannel = function(name, value) {
+ if (!csound) return;
+ csound.setStringChannel(name, value);
+ };
+
+ this.getStringChannel = function(name) {
+ if (!csound) return;
+ return csound.getStringChannel(name);
+ };
+
+ this.getTable = function(tableNumber) {
+ if (!csound) return;
+ return csound.getTable(tableNumber);
+ };
+
+ this.compileOrc = async function(orc) {
+ if (!csound) return;
+ return await csound.compileOrc(orc);
+ }
+
+ function handleMessage(message) {
+ if (debug) console.log(message);
+ if (message.startsWith("callback ")) {
+ runCallback(JSON.parse(message.substr(9)));
+ } else if (appOptions.errorHandler && (message.startsWith("error: ") ||
+ message.startsWith("INIT ERROR ") ||
+ message.startsWith("PERF ERROR ") ||
+ message.startsWith("perf_error: "))) {
+ appOptions.errorHandler(message);
+ } else if (appOptions.ioReceivers) {
+ for (var k in appOptions.ioReceivers) {
+ if (message.startsWith(k + " ")) {
+ appOptions.ioReceivers[k](message.substr(k.length + 1));
+ return;
+ }
+ }
+ }
+ };
+
+ var urlExistsChecked = {};
+ function urlExists(url) {
+ return new Promise((resolve, reject) => {
+ if (urlExistsChecked.hasOwnProperty(url)) {
+ resolve(urlExistsChecked[url]);
+ } else {
+ fetch(url, {
+ method: "HEAD"
+ }).then(response => {
+ var exists = (response.status.toString()[0] === "2");
+ urlExistsChecked[url] = exists;
+ resolve(exists);
+ }).catch(error => {
+ reject(false);
+ });
+ }
+ });
+ }
+
+
+ function dirName(path) {
+ return path.substring(0, path.lastIndexOf("/"));
+ }
+
+ async function loadFiles(files) {
+ for (var i = 0; i < files.length; i++) {
+ await copyUrlToLocal(files[i].url, files[i].name);
+ }
+ };
+
+
+ function urlInFiles(url, files) {
+ for (var x in files) {
+ if (url == files[x].url) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ async function scanUdoUsage(url, files, soundCollections) {
+ const req = await fetch(url);
+ const text = await req.text();
+ var soundCollections;
+ var match;
+ var doSaveStorage = false;
+ if (!storage.knownUdoPaths) storage.knownUdoPaths = {};
+ var re = /sounddb_getcollection(|id) "(.*)"|#include "(.*)"/g;
+ do {
+ match = re.exec(text);
+ if (match) {
+ if (match[3] && !match[3].endsWith("sounddb.udo")) {
+ var udoPath;
+ var dopush = false;
+ if (udoReplacements.hasOwnProperty(match[3])) {
+ udoPath = baseUrl + "/udo/" + udoReplacements[match[3]];
+ if (!urlInFiles(udoPath)) {
+ dopush = true;
+ }
+ } else {
+ if (Object.keys(storage.knownUdoPaths).includes(match[3])) {
+ udoPath = storage.knownUdoPaths[match[3]];
+ if (!urlInFiles(udoPath, files)) dopush = true;
+ } else {
+ udoPath = baseUrl + ("/udo/" + match[3]).replaceAll("//", "/");
+ if (!urlInFiles(udoPath, files)) {
+ var exists = await urlExists(udoPath);
+ if (!exists) {
+ udoPath = appPath + ("/" + match[3]).replaceAll("//", "/");
+ }
+ storage.knownUdoPaths[match[3]] = udoPath;
+ doSaveStorage = true;
+ dopush = true;
+ }
+ }
+ }
+ if (dopush) {
+ files.push({url: udoPath, name: match[3]});
+ await scanUdoUsage(udoPath, files, soundCollections);
+ }
+ }
+ if (match[2] && !appOptions.manualSoundCollections) {
+ var collections = match[2].split(",");
+ for (var x in collections) {
+ if (!soundCollections.includes(collections[x])) {
+ soundCollections.push(collections[x]);
+ }
+ }
+ }
+ }
+ } while (match);
+
+ if (doSaveStorage) {
+ saveStorage();
+ }
+ }
+
+ this.getNode = async function() {
+ if (!csound) return;
+ return await csound.getNode();
+ }
+
+ this.play = async function(log, audioContext) {
+ if (!csound) {
+ if (log) log("Loading audio engine");
+ var csdBasename = basename(appOptions.csdUrl);
+ appPath = dirName(window.location.href);
+
+ if (csdBasename == appOptions.csdUrl) {
+ appOptions.csdUrl = appPath + "/" + appOptions.csdUrl;
+ }
+ const { Csound } = await import(baseUrl + "/code/csound.js");
+ var invokeOptions = {};
+ if (audioContext) {
+ invokeOptions.audioContext = audioContext;
+ }
+ csound = await Csound(invokeOptions);
+ await csound.setDebug(0);
+ csound.on("message", handleMessage);
+ await csound.setOption("-m0");
+ await csound.setOption("-d");
+ await csound.setOption("--env:INCDIR=/");
+ await csound.setOption("-odac");
+ await csound.setOption("--omacro:WEB=1");
+ if (appOptions.csOptions) {
+ for (var i = 0; i < appOptions.csOptions.length; i++) {
+ await csound.setOption(appOptions.csOptions[i]);
+ }
+ }
+
+ if (log) log("Preparing application code");
+ var files = [{url: appOptions.csdUrl, name: csdBasename}];
+ var soundCollections = [];
+ await scanUdoUsage(appOptions.csdUrl, files, soundCollections);
+
+ if (appOptions.files) {
+ appOptions.files.forEach(function(f){
+ files.push({url: f, name: f});
+ });
+ }
+
+ if (soundCollections.length > 0) {
+ if (log) log("Preparing application sounds");
+ var udoUrl = baseUrl + "/sound/collection_udo.py?collections=" + soundCollections.join(",");
+ files.push({url: udoUrl, name: "sounddb.udo"});
+
+ const response = await fetch(baseUrl + "/sound/map.json");
+ const jdata = await response.json();
+ for (var i = 0; i < soundCollections.length; i++) {
+ var fdata = jdata[soundCollections[i]];
+ for (var j = 0; j < fdata.sounds.length; j++) {
+ var path = fdata.sounds[j].path;
+ if (!urlInFiles(path, files)) {
+ var fileObj = {url: path, name: path};
+ files.push(fileObj);
+ }
+ }
+ }
+ }
+ if (appOptions.onPlay) {
+ csound.on("play", appOptions.onPlay);
+ }
+
+ if (appOptions.onStop) {
+ csound.on("stop", appOptions.onStop);
+ }
+
+ if (log) log("Loading application files");
+ if (appOptions.files) {
+ for (var x in appOptions.files) {
+ files.push({url: appPath + "/" + appOptions.files[x], name: appOptions.files[x]});
+ }
+ }
+ await loadFiles(files);
+ if (log) log("Compiling application");
+ await csound.compileCsd(csdBasename);
+ await csound.start();
+ }
+
+ };
+
+ this.insertScoreAsync = async function(instr, extraArgs, duration, start) {
+ if (!duration) duration = 1;
+ if (!start) start = 0;
+
+ return new Promise((resolve, reject) => {
+ var cbid = self.createCallback(function(ndata){
+ resolve(ndata);
+ });
+ var args = [start, duration, cbid];
+ for (let e of extraArgs) {
+ args.push(e);
+ }
+ self.insertScore(instr, args);
+ });
+ };
+
+ this.insertScore = async function(instr, args) {
+ if (!csound) return;
+ if (!args) args = [0, 1];
+ var scoreline = "i"
+
+ function add(item) {
+ if (isNaN(item)) {
+ scoreline += "\"" + item + "\" ";
+ } else {
+ scoreline += item + " ";
+ }
+ }
+
+ add(instr);
+ if (typeof(args) == "function") {
+ [0, 1, self.createCallback(args)].forEach(add);
+ } else {
+ args.forEach(add);
+ }
+ csound.inputMessage(scoreline);
+ };
+
+ var speedTimestamp = null;
+ var lastPosX = null;
+ var lastPosY = null;
+
+ function setSpeedChannels(x, y) {
+ if (!csound) return;
+ if (speedTimestamp == null) {
+ speedTimestamp = Date.now();
+ lastPosX = x;
+ lastPosY = y;
+ return;
+ }
+ var now = Date.now();
+ var dt = now - speedTimestamp;
+ var dx = event.pageX - lastPosX;
+ var dy = event.pageY - lastPosY;
+ var speedX = Math.round(dx / dt * 100);
+ var speedY = Math.round(dy / dt * 100);
+ speedTimestamp = now;
+ lastPosX = x;
+ lastPosY = y;
+
+ csound.setControlChannel("speedX", speedX);
+ csound.setControlChannel("speedY", speedY);
+ }
+
+ function setPositionChannels(x, y) {
+ if (!csound) return;
+ csound.setControlChannel("mouseX", x / window.innerWidth);
+ csound.setControlChannel("mouseY", y / window.innerHeight);
+ }
+
+
+ if (appOptions.trackMouse || appOptions.trackMouseSpeed) {
+ document.addEventListener("mousemove", function(event) {
+ var timestamp = null;
+ var lastMouseX = null;
+ var lastMouseY = null;
+ if (appOptions.trackMouse) {
+ setPositionChannels(event.pageX, event.pageY);
+ }
+ if (appOptions.trackMouseSpeed) {
+ setSpeedChannels(event.pageX, event.pageY);
+ }
+ });
+ }
+
+ if (appOptions.trackTouch || appOptions.trackTouchSpeed) {
+ document.addEventListener("touchmove", function(event) {
+ var evt = (typeof event.originalEvent === "undefined") ? event : event.originalEvent;
+ var touch = evt.touches[0] || evt.changedTouches[0];
+ if (appOptions.trackTouch) {
+ setPositionChannels(touch.pageX, touch.pageY);
+ }
+ if (appOptions.trackTouchSpeed) {
+ setSpeedChannels(touch.pageX, touch.pageY);
+ }
+ });
+ }
+
+
+ if (appOptions.keyDownScore) {
+ document.addEventListener("keydown", function(event) {
+ if (!csound) return;
+ var scoreline = null;
+ if (typeof appOptions.keyDownScore == "function") {
+ scoreline = appOptions.keyDownScore(event);
+ } else {
+ scoreline = appOptions.keyDownScore + " " + event.code;
+ }
+ csound.inputMessage(scoreline);
+ });
+ }
+
+ if (appOptions.keyUpScore) {
+ document.addEventListener("keydown", function(event) {
+ if (!csound) return;
+ var scoreline = null;
+ if (typeof appOptions.keyUpScore == "function") {
+ scoreline = appOptions.keyUpScore(event);
+ } else {
+ scoreline = appOptions.keyUpScore + " " + event.code;
+ }
+ csound.inputMessage(scoreline);
+ });
+ }
+} \ No newline at end of file