From da89e5c3e7f9eb2e7ac8133d71cba9e6bfd6f486 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Mon, 30 Dec 2024 22:12:32 +0100 Subject: [PATCH] add src folder + support strudel label statements --- index.html | 2 +- main.js | 208 ------------------------------------- src/main.js | 101 ++++++++++++++++++ src/strudel.js | 166 +++++++++++++++++++++++++++++ style.css => src/style.css | 0 5 files changed, 268 insertions(+), 209 deletions(-) delete mode 100644 main.js create mode 100644 src/main.js create mode 100644 src/strudel.js rename style.css => src/style.css (100%) diff --git a/index.html b/index.html index 17f5a94..8f7093b 100644 --- a/index.html +++ b/index.html @@ -26,6 +26,6 @@ - + diff --git a/main.js b/main.js deleted file mode 100644 index 4dee2e6..0000000 --- a/main.js +++ /dev/null @@ -1,208 +0,0 @@ -import { EditorView, basicSetup } from "codemirror"; -import { javascript } from "@codemirror/lang-javascript"; -import { oneDark } from "@codemirror/theme-one-dark"; -import { EditorState, Prec } from "@codemirror/state"; -import { yCollab } from "y-codemirror.next"; -import { Session } from "@flok-editor/session"; -import { flashField, evalKeymap, remoteEvalFlash } from "@flok-editor/cm-eval"; -import { UndoManager } from "yjs"; - -import "./style.css"; - -// strudel -import { controls, evalScope, repl, stack, evaluate } from "@strudel/core"; -// import { Framer } from "@strudel/draw"; -import { registerSoundfonts } from "@strudel/soundfonts"; -import { transpiler } from "@strudel/transpiler"; -import { - getAudioContext, - initAudio, - registerSynthSounds, - samples, - webaudioOutput, -} from "@strudel/webaudio"; - -const audioReady = initAudio().then(() => { - console.log("audio ready"); -}); -const onError = (err) => { - console.error(err); -}; - -async function loadSamples() { - const ds = "https://raw.githubusercontent.com/felixroos/dough-samples/main/"; - return Promise.all([ - samples(`${ds}/tidal-drum-machines.json`), - samples(`${ds}/piano.json`), - samples(`${ds}/Dirt-Samples.json`), - samples(`${ds}/EmuSP12.json`), - samples(`${ds}/vcsl.json`), - ]); -} - -class StrudelSession { - constructor() { - this.init().then(() => { - console.log("strudel init done", this.repl); - }); - this.patterns = {}; - } - async init() { - await evalScope( - import("@strudel/core"), - import("@strudel/mini"), - import("@strudel/tonal"), - import("@strudel/soundfonts"), - import("@strudel/webaudio"), - controls - ); - try { - await Promise.all([ - loadSamples(), - registerSynthSounds(), - registerSoundfonts(), - ]); - } catch (err) { - onError(err); - } - - this.repl = repl({ - defaultOutput: webaudioOutput, - afterEval: (options) => { - // assumes docId is injected at end end as a comment - /* const docId = options.code.split("//").slice(-1)[0]; - if (!docId) return; - const miniLocations = options.meta?.miniLocations; - updateDocumentsContext(docId, { miniLocations }); */ - }, - beforeEval: () => {}, - onSchedulerError: (e) => onError(`${e}`), - onEvalError: (e) => onError(`${e}`), - getTime: () => getAudioContext().currentTime, - transpiler, - }); - } - async eval(msg) { - try { - const { body: code, docId } = msg; - // little hack that injects the docId at the end of the code to make it available in afterEval - //const pattern = await this._repl.evaluate(`${code}//${docId}`); - const { pattern } = await evaluate( - //`${code}//${docId}`, - code, - transpiler - // { id: '?' } - ); - if (pattern) { - //this.patterns[docId] = pattern.docId(docId); // docId is needed for highlighting - this.patterns[docId] = pattern; // docId is needed for highlighting - console.log("this.patterns", this.patterns); - const allPatterns = stack(...Object.values(this.patterns)); - await this.repl.scheduler.setPattern(allPatterns, true); - } - } catch (err) { - console.error(err); - this._onError(`${err}`); - } - } -} - -const strudel = new StrudelSession(); - -let patterns = {}; -// - -const flokBasicSetup = (doc) => { - const text = doc.getText(); - const undoManager = new UndoManager(text); - const web = true; - - return [ - flashField(), - remoteEvalFlash(doc), - Prec.high(evalKeymap(doc, { web })), - yCollab(text, doc.session.awareness, { undoManager }), - ]; -}; - -const createEditor = (doc) => { - console.log("createEditor", doc); - if (!["slot1", "slot2"].includes(doc.id)) { - console.warn( - `ignoring doc with id "${doc.id}". only slot1 and slot2 is allowed rn..` - ); - return; - } - const state = EditorState.create({ - doc: doc.content, - extensions: [ - basicSetup, - flokBasicSetup(doc), - javascript(), - EditorView.lineWrapping, - oneDark, - ], - }); - - const editorEl = document.querySelector(`#${doc.id} .editor`); - const view = new EditorView({ - state, - parent: editorEl, - }); - - const targetEl = document.querySelector(`#${doc.id} .target`); - targetEl.value = doc.target; - - targetEl.addEventListener("change", (e) => { - doc.target = e.target.value; - }); - doc.session.on(`change-target:${doc.id}`, () => { - targetEl.value = doc.target; - }); - - return [state, view]; -}; - -const handleMessage = (msg) => { - console.log("message", msg); - try { - const pattern = evaluate(msg.body); - console.log("pattern"); - patterns[msg.docId] = pattern; - } catch (err) { - console.error(`eval error: ${err.message}`); - } -}; - -const handleEvalHydra = (msg) => { - console.log("eval:hydra", msg); - // evaluate hydra code here... -}; - -const session = new Session("default", { - // changed this part to what flok.cc uses - hostname: "flok.cc", - port: "", //parseInt(port), - isSecure: true, -}); -window.session = session; - -session.on("change", (...args) => console.log("change", ...args)); -session.on("message", handleMessage); -session.on("eval:hydra", handleEvalHydra); -session.on("eval:strudel", (msg) => strudel.eval(msg)); - -session.on("sync", () => { - // If session is empty, create two documents - if (session.getDocuments().length === 0) { - session.setActiveDocuments([ - { id: "slot1", target: "strudel" }, - { id: "slot2", target: "hydra" }, - ]); - } - - // Create editors for each document - session.getDocuments().map((doc) => createEditor(doc)); -}); - -session.initialize(); diff --git a/src/main.js b/src/main.js new file mode 100644 index 0000000..cd549f4 --- /dev/null +++ b/src/main.js @@ -0,0 +1,101 @@ +import { EditorView, basicSetup } from "codemirror"; +import { javascript } from "@codemirror/lang-javascript"; +import { oneDark } from "@codemirror/theme-one-dark"; +import { EditorState, Prec } from "@codemirror/state"; +import { yCollab } from "y-codemirror.next"; +import { Session } from "@flok-editor/session"; +import { flashField, evalKeymap, remoteEvalFlash } from "@flok-editor/cm-eval"; +import { UndoManager } from "yjs"; +import { StrudelSession } from "./strudel"; + +import "./style.css"; + +const onError = (err) => { + console.error(err); +}; + +const strudel = new StrudelSession({ onError }); + +const flokBasicSetup = (doc) => { + const text = doc.getText(); + const undoManager = new UndoManager(text); + const web = true; + + return [ + flashField(), + remoteEvalFlash(doc), + Prec.high(evalKeymap(doc, { web, defaultMode: "document" })), + yCollab(text, doc.session.awareness, { undoManager }), + ]; +}; + +const createEditor = (doc) => { + console.log("createEditor", doc); + if (!["slot1", "slot2"].includes(doc.id)) { + console.warn( + `ignoring doc with id "${doc.id}". only slot1 and slot2 is allowed rn..` + ); + return; + } + const state = EditorState.create({ + doc: doc.content, + extensions: [ + basicSetup, + flokBasicSetup(doc), + javascript(), + EditorView.lineWrapping, + oneDark, + ], + }); + + const editorEl = document.querySelector(`#${doc.id} .editor`); + const view = new EditorView({ + state, + parent: editorEl, + }); + + const targetEl = document.querySelector(`#${doc.id} .target`); + targetEl.value = doc.target; + + targetEl.addEventListener("change", (e) => { + doc.target = e.target.value; + }); + doc.session.on(`change-target:${doc.id}`, () => { + targetEl.value = doc.target; + }); + + return [state, view]; +}; + +const handleEvalHydra = (msg) => { + console.log("eval:hydra", msg); + // evaluate hydra code here... +}; + +const session = new Session("default", { + // changed this part to what flok.cc uses + hostname: "flok.cc", + port: "", //parseInt(port), + isSecure: true, +}); +window.session = session; + +session.on("change", (...args) => console.log("change", ...args)); +session.on("message", (msg) => console.log("message", msg)); +session.on("eval:hydra", handleEvalHydra); +session.on("eval:strudel", (msg) => strudel.eval(msg)); + +session.on("sync", () => { + // If session is empty, create two documents + if (session.getDocuments().length === 0) { + session.setActiveDocuments([ + { id: "slot1", target: "strudel" }, + { id: "slot2", target: "hydra" }, + ]); + } + + // Create editors for each document + session.getDocuments().map((doc) => createEditor(doc)); +}); + +session.initialize(); diff --git a/src/strudel.js b/src/strudel.js new file mode 100644 index 0000000..191b120 --- /dev/null +++ b/src/strudel.js @@ -0,0 +1,166 @@ +import { + controls, + evalScope, + repl, + stack, + evaluate, + silence, +} from "@strudel/core"; +// import { Framer } from "@strudel/draw"; +import { registerSoundfonts } from "@strudel/soundfonts"; +import { transpiler } from "@strudel/transpiler"; +import { + getAudioContext, + initAudio, + registerSynthSounds, + samples, + webaudioOutput, +} from "@strudel/webaudio"; + +export class StrudelSession { + constructor({ onError }) { + this.init().then(() => { + console.log("strudel init done", this.repl); + }); + this.patterns = {}; + this.pPatterns = {}; + this.allTransform = undefined; + this.anonymousIndex = 0; + this.onError = onError; + } + loadSamples() { + const ds = + "https://raw.githubusercontent.com/felixroos/dough-samples/main/"; + return Promise.all([ + samples(`${ds}/tidal-drum-machines.json`), + samples(`${ds}/piano.json`), + samples(`${ds}/Dirt-Samples.json`), + samples(`${ds}/EmuSP12.json`), + samples(`${ds}/vcsl.json`), + ]); + } + + async init() { + initAudio(); + await evalScope( + import("@strudel/core"), + import("@strudel/mini"), + import("@strudel/tonal"), + import("@strudel/soundfonts"), + import("@strudel/webaudio"), + controls + ); + try { + await Promise.all([ + this.loadSamples(), + registerSynthSounds(), + registerSoundfonts(), + ]); + } catch (err) { + this.onError(err); + } + + this.repl = repl({ + defaultOutput: webaudioOutput, + afterEval: (options) => { + // assumes docId is injected at end end as a comment + /* const docId = options.code.split("//").slice(-1)[0]; + if (!docId) return; + const miniLocations = options.meta?.miniLocations; + updateDocumentsContext(docId, { miniLocations }); */ + }, + beforeEval: () => {}, + onSchedulerError: (e) => this.onError(`${e}`), + onEvalError: (e) => this.onError(`${e}`), + getTime: () => getAudioContext().currentTime, + transpiler, + }); + this.injectPatternMethods(); + } + + hush() { + this.pPatterns = {}; + this.anonymousIndex = 0; + this.allTransform = undefined; + return silence; + } + + // set pattern methods that use this repl via closure + injectPatternMethods() { + const self = this; + Pattern.prototype.p = function (id) { + if (typeof id === "string" && (id.startsWith("_") || id.endsWith("_"))) { + // allows muting a pattern x with x_ or _x + return silence; + } + console.log("p", id); + if (id === "$") { + // allows adding anonymous patterns with $: + id = `$${self.anonymousIndex}`; + self.anonymousIndex++; + } + self.pPatterns[id] = this; + return this; + }; + Pattern.prototype.q = function () { + return silence; + }; + const all = (transform) => { + this.allTransform = transform; + return silence; + }; + /* const stop = () => this.repl.scheduler.stop(); + const start = () => this.repl.scheduler.start(); + const pause = () => this.repl.scheduler.pause(); + const toggle = () => this.repl.scheduler.toggle(); */ + const setCps = (cps) => this.repl.scheduler.setCps(cps); + const setCpm = (cpm) => this.repl.scheduler.setCps(cpm / 60); + /* const cpm = register("cpm", function (cpm, pat) { + return pat._fast(cpm / 60 / scheduler.cps); + }); */ + return evalScope({ + // cpm, + all, + hush: () => this.hush(), + setCps, + setcps: setCps, + setCpm, + setcpm: setCpm, + }); + } + + async eval(msg, conversational = false) { + try { + !conversational && this.hush(); + const { body: code, docId } = msg; + // little hack that injects the docId at the end of the code to make it available in afterEval + /* const { pattern } = */ await evaluate( + //`${code}//${docId}`, + code, + transpiler + // { id: '?' } + ); + let pattern = silence; + + if (Object.keys(this.pPatterns).length) { + let patterns = Object.values(this.pPatterns); + pattern = stack(...patterns); + } + if (this.allTransform) { + pattern = this.allTransform(pattern); + } + + console.log("eval done", this.pPatterns); + if (pattern) { + //this.patterns[docId] = pattern.docId(docId); // docId is needed for highlighting + this.patterns[docId] = pattern; // docId is needed for highlighting + console.log("this.patterns", this.patterns); + const allPatterns = stack(...Object.values(this.patterns)); + await this.repl.scheduler.setPattern(allPatterns, true); + } + } catch (err) { + console.error(err); + this.onError(`${err}`); + } + } +} diff --git a/style.css b/src/style.css similarity index 100% rename from style.css rename to src/style.css