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; if (!name) name = basename(url); 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); }); } }