From 9fbf91db06a6d4f4b5cd8bb45389a731bb86bf22 Mon Sep 17 00:00:00 2001 From: Richard Date: Sun, 13 Apr 2025 18:48:02 +0100 Subject: initial --- site/app/base/base.js | 512 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 512 insertions(+) create mode 100644 site/app/base/base.js (limited to 'site/app/base/base.js') 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 -- cgit v1.2.3