From f6c86712aa19bd585b5608db56a6132d15e9c7eb Mon Sep 17 00:00:00 2001 From: Miika Alonen Date: Wed, 22 Nov 2023 00:22:54 +0200 Subject: [PATCH 01/34] Update zifferjs --- package.json | 2 +- yarn.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index 4eb78e2..ce31854 100644 --- a/package.json +++ b/package.json @@ -42,7 +42,7 @@ "tone": "^14.8.49", "unique-names-generator": "^4.7.1", "vite-plugin-markdown": "^2.1.0", - "zifferjs": "^0.0.39", + "zifferjs": "^0.0.40", "zzfx": "^1.2.0" } } diff --git a/yarn.lock b/yarn.lock index 41ffdb4..2a7b38b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3882,10 +3882,10 @@ yaml@^2.1.1: resolved "https://registry.yarnpkg.com/yaml/-/yaml-2.3.1.tgz#02fe0975d23cd441242aa7204e09fc28ac2ac33b" integrity sha512-2eHWfjaoXgTBC2jNM1LRef62VQa0umtvRiDSk6HSzW7RvS5YtkabJrwYLLEKWBc8a5U2PTSCs+dJjUTJdlHsWQ== -zifferjs@^0.0.39: - version "0.0.39" - resolved "https://registry.yarnpkg.com/zifferjs/-/zifferjs-0.0.39.tgz#a3916ca1b38d493edea14bf4f29948f2f6f1572e" - integrity sha512-WMSJ9SPGA/OP/9Z936anUUOM66qzuwPZaE99Qix+Q7jr4fFuoZ/Xw76m2/1C2UVk85sauAecnSfjcK3zo6nA3Q== +zifferjs@^0.0.40: + version "0.0.40" + resolved "https://registry.yarnpkg.com/zifferjs/-/zifferjs-0.0.40.tgz#51e8310a006b01fcc60cce91aebd17462a685272" + integrity sha512-0lVC52SBRTYVFR8whhJcsZwDwaHulmA6ry6bgARPWJaS33cs2e0lMfxftwCjq6pDFl9upP5VRzKxuWA6p+GELA== zzfx@^1.2.0: version "1.2.0" From b9c59ab948e4c8c8be4f695ade7be3dbcc75a4ad Mon Sep 17 00:00:00 2001 From: Miika Alonen Date: Wed, 22 Nov 2023 00:55:11 +0200 Subject: [PATCH 02/34] Update zifferjs --- package.json | 2 +- yarn.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index ce31854..22eb8f6 100644 --- a/package.json +++ b/package.json @@ -42,7 +42,7 @@ "tone": "^14.8.49", "unique-names-generator": "^4.7.1", "vite-plugin-markdown": "^2.1.0", - "zifferjs": "^0.0.40", + "zifferjs": "^0.0.41", "zzfx": "^1.2.0" } } diff --git a/yarn.lock b/yarn.lock index 2a7b38b..8c728a8 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3882,10 +3882,10 @@ yaml@^2.1.1: resolved "https://registry.yarnpkg.com/yaml/-/yaml-2.3.1.tgz#02fe0975d23cd441242aa7204e09fc28ac2ac33b" integrity sha512-2eHWfjaoXgTBC2jNM1LRef62VQa0umtvRiDSk6HSzW7RvS5YtkabJrwYLLEKWBc8a5U2PTSCs+dJjUTJdlHsWQ== -zifferjs@^0.0.40: - version "0.0.40" - resolved "https://registry.yarnpkg.com/zifferjs/-/zifferjs-0.0.40.tgz#51e8310a006b01fcc60cce91aebd17462a685272" - integrity sha512-0lVC52SBRTYVFR8whhJcsZwDwaHulmA6ry6bgARPWJaS33cs2e0lMfxftwCjq6pDFl9upP5VRzKxuWA6p+GELA== +zifferjs@^0.0.41: + version "0.0.41" + resolved "https://registry.yarnpkg.com/zifferjs/-/zifferjs-0.0.41.tgz#3f4769b1eec868e2efafc471a3eed283afaed53f" + integrity sha512-SbcSIRSubDfb+bLyy1vqoF9VBuDqNG3qDrktb7NxWUWQUTCLsJ4S/t4LsMdxu8vROWGYqVTmjS8BfOJs6qgt9A== zzfx@^1.2.0: version "1.2.0" From dbacc913e270515c193ffb3d9551df46c6ee58ab Mon Sep 17 00:00:00 2001 From: Raphael Forment Date: Wed, 22 Nov 2023 21:15:55 +0100 Subject: [PATCH 03/34] New special key: Ctrl+M to hide the interface --- index.html | 4 ++-- src/KeyActions.ts | 25 +++++++++++++++++++++++++ src/documentation/basics/keyboard.ts | 9 +++++---- src/main.ts | 9 +++++++-- 4 files changed, 39 insertions(+), 8 deletions(-) diff --git a/index.html b/index.html index 01c9daf..bf64537 100644 --- a/index.html +++ b/index.html @@ -71,7 +71,7 @@
-
+
@@ -429,7 +429,7 @@
-
-[Hydra](https://hydra.ojack.xyz/?sketch_id=mahalia_1) is a popular live-codable video synthesizer developed by [Olivia Jack](https://ojack.xyz/) and other contributors. It follows the metaphor of analog synthesizer patching to allow its user to create complex live visuals from a web browser window. Being very easy to use, extremely powerful and also very rewarding to use, Hydra has become a popular choice for adding visuals into a live code performance. Topos provides a simple way to integrate Hydra into a live coding session and to blend it with regular Topos code. +[Hydra](https://hydra.ojack.xyz/?sketch_id=mahalia_1) is a popular live-codable video synthesizer developed by [Olivia Jack](https://ojack.xyz/) and other contributors. It follows an analog synthesizer patching metaphor to encourage live coding complex shaders. Being very easy to use, extremely powerful and also very rewarding to use, Hydra has become a popular choice for adding visuals into a live code performance. ${makeExample( "Hydra integration", - `beat(4) :: app.hydra.osc(3, 0.5, 2).out()`, - true, + `beat(4) :: hydra.osc(3, 0.5, 2).out()`, + true )} -You may feel like it's doing nothing! Press ${key_shortcut( - "Ctrl+D", - )} to close the documentation. **Boom, all shiny!** +Close the documentation to see the effect: ${key_shortcut( + "Ctrl+D" + )}! **Boom, all shiny!** -Be careful not to call app.hydra too often as it can impact performances. You can use any rhythmical function like mod() function to limit the number of function calls. You can write any Topos code like [1,2,3].beat() to bring some life and movement in your Hydra sketches. +Be careful not to call hydra too often as it can impact performances. You can use any rhythmical function like beat() function to limit the number of function calls. You can write any Topos code like [1,2,3].beat() to bring some life and movement in your Hydra sketches. Stopping **Hydra** is simple: @@ -35,16 +35,35 @@ ${makeExample( "Stopping Hydra", ` beat(4) :: stop_hydra() // this one -beat(4) :: app.hydra.hush() // or this one +beat(4) :: hydra.hush() // or this one `, - true, + true )} -I won't teach you how to play with Hydra. You can find some great resources on the [Hydra website](https://hydra.ojack.xyz/): + +### Changing the resolution + +You can change Hydra resolution using this simple method: + +${makeExample( + "Changing Hydra resolution", + `hydra.setResolution(1024, 768)`, + true +)} + +### Documentation + +I won't teach Hydra. You can find some great resources directly on the [Hydra website](https://hydra.ojack.xyz/): - [Hydra interactive documentation](https://hydra.ojack.xyz/docs/) - [List of Hydra Functions](https://hydra.ojack.xyz/api/) - [Source code on GitHub](https://github.com/hydra-synth/hydra) +### The Hydra namespace + +In comparison with the basic Hydra editor, please note that you have to prefix all Hydra functions with hydra. to avoid conflicts with Topos functions. For example, osc() becomes hydra.osc(). + +${makeExample("Hydra namespace", `hydra.voronoi(20).out()`, true)} + ## GIF player Topos embeds a small .gif picture player with a small API. GIFs are automatically fading out after the given duration. Look at the following example: @@ -63,7 +82,7 @@ beat(0.25)::gif({ posX: ir(1,1200), // CSS Horizontal Position posY: ir(1, 800), // CSS Vertical Position `, - true, + true )} `; }; diff --git a/src/main.ts b/src/main.ts index fcd60a8..1d3721a 100644 --- a/src/main.ts +++ b/src/main.ts @@ -222,7 +222,7 @@ export class Editor { updateKnownUniversesView = () => { let itemTemplate = document.getElementById( - "ui-known-universe-item-template", + "ui-known-universe-item-template" ) as HTMLTemplateElement; if (!itemTemplate) { console.warn("Missing template #ui-known-universe-item-template"); @@ -250,10 +250,10 @@ export class Editor { item .querySelector(".delete-universe") ?.addEventListener("click", () => - api._deleteUniverseFromInterface(it), + api._deleteUniverseFromInterface(it) ); return item; - }), + }) ); existing_universes.innerHTML = ""; @@ -334,7 +334,7 @@ export class Editor { this.view.dispatch({ effects: this.chosenLanguage.reconfigure( - this.editor_mode == "notes" ? [markdown()] : [javascript()], + this.editor_mode == "notes" ? [markdown()] : [javascript()] ), }); @@ -343,7 +343,7 @@ export class Editor { setButtonHighlighting( button: "play" | "pause" | "stop" | "clear", - highlight: boolean, + highlight: boolean ) { document.getElementById("play-label")!.textContent = button !== "pause" ? "Pause" : "Play"; @@ -391,7 +391,7 @@ export class Editor { // All other buttons must lose the highlighting document .querySelectorAll( - possible_selectors.filter((_, index) => index != selector).join(","), + possible_selectors.filter((_, index) => index != selector).join(",") ) .forEach((button) => { button.children[0].classList.remove("animate-pulse"); @@ -437,28 +437,28 @@ export class Editor { flashBackground(color: string, duration: number): void { const domElement = this.view.dom; const gutters = domElement.getElementsByClassName( - "cm-gutter", + "cm-gutter" ) as HTMLCollectionOf; domElement.classList.add("fluid-bg-transition"); Array.from(gutters).forEach((gutter) => - gutter.classList.add("fluid-bg-transition"), + gutter.classList.add("fluid-bg-transition") ); domElement.style.backgroundColor = color; Array.from(gutters).forEach( - (gutter) => (gutter.style.backgroundColor = color), + (gutter) => (gutter.style.backgroundColor = color) ); setTimeout(() => { domElement.style.backgroundColor = ""; Array.from(gutters).forEach( - (gutter) => (gutter.style.backgroundColor = ""), + (gutter) => (gutter.style.backgroundColor = "") ); domElement.classList.remove("fluid-bg-transition"); Array.from(gutters).forEach((gutter) => - gutter.classList.remove("fluid-bg-transition"), + gutter.classList.remove("fluid-bg-transition") ); }, duration); } @@ -466,7 +466,7 @@ export class Editor { private initializeElements(): void { for (const [key, value] of Object.entries(singleElements)) { this.interface[key] = document.getElementById( - value, + value ) as ElementMap[keyof ElementMap]; } } @@ -474,7 +474,7 @@ export class Editor { private initializeButtonGroups(): void { for (const [key, ids] of Object.entries(buttonGroups)) { this.buttonElements[key] = ids.map( - (id) => document.getElementById(id) as HTMLButtonElement, + (id) => document.getElementById(id) as HTMLButtonElement ); } } @@ -501,20 +501,19 @@ export class Editor { enableStreamCapture: false, }); this.hydra = this.hydra_backend.synth; + (globalThis as any).hydra = this.hydra; + this.hydra.setResolution(1024, 768); } private setCanvas(canvas: HTMLCanvasElement): void { if (!canvas) return; const ctx = canvas.getContext("2d"); - const dpr = window.devicePixelRatio || 1; - // Assuming the canvas takes up the whole window - canvas.width = window.innerWidth * dpr * 0.25; - canvas.height = window.innerHeight * dpr * 0.25; - + canvas.width = window.innerWidth * dpr; + canvas.height = window.innerHeight * dpr; if (ctx) { - ctx.scale(dpr * 0.5, dpr * 0.5); + ctx.scale(dpr, dpr); } } } From d717fc84109e1295536e4ecb286b4426c0dfded8 Mon Sep 17 00:00:00 2001 From: Raphael Forment Date: Sun, 26 Nov 2023 13:32:38 +0100 Subject: [PATCH 10/34] Add code documentation --- src/AudioVisualisation.ts | 85 ++++++++++++++++++++++----------------- src/Clock.ts | 11 +++-- src/Documentation.ts | 23 +++++++++-- src/DomElements.ts | 6 +++ src/Evaluator.ts | 44 ++++++++++++++++---- src/FileManagement.ts | 38 +++++++++++++++-- src/main.ts | 67 +++++++++++++++++++++++++++--- 7 files changed, 212 insertions(+), 62 deletions(-) diff --git a/src/AudioVisualisation.ts b/src/AudioVisualisation.ts index 9537cbd..a74cce8 100644 --- a/src/AudioVisualisation.ts +++ b/src/AudioVisualisation.ts @@ -2,19 +2,19 @@ import { getAnalyser } from "superdough"; import { type Editor } from "./main"; -/** - * Draw a circle at a specific position on the canvas. - * @param {number} x - The x-coordinate of the circle's center. - * @param {number} y - The y-coordinate of the circle's center. - * @param {number} radius - The radius of the circle. - * @param {string} color - The fill color of the circle. - */ export const drawCircle = ( + /** + * Draw a circle at a specific position on the canvas. + * @param {number} x - The x-coordinate of the circle's center. + * @param {number} y - The y-coordinate of the circle's center. + * @param {number} radius - The radius of the circle. + * @param {string} color - The fill color of the circle. + */ app: Editor, x: number, y: number, radius: number, - color: string, + color: string ): void => { // @ts-ignore const canvas: HTMLCanvasElement = app.interface.feedback; @@ -28,15 +28,15 @@ export const drawCircle = ( ctx.closePath(); }; -/** - * Blinks a script indicator circle. - * @param script - The type of script. - * @param no - The shift amount multiplier. - */ export const blinkScript = ( + /** + * Blinks a script indicator circle. + * @param script - The type of script. + * @param no - The shift amount multiplier. + */ app: Editor, script: "local" | "global" | "init", - no?: number, + no?: number ) => { if (no !== undefined && no < 1 && no > 9) return; const blinkDuration = @@ -55,15 +55,15 @@ export const blinkScript = ( horizontalOffset + shift, app.interface.feedback.clientHeight - 15, 8, - "#fdba74", + "#fdba74" ); }; - /** - * Clears the circle at a given shift. - * @param shift - The pixel distance from the origin. - */ const _clearBlinker = (shift: number) => { + /** + * Clears the circle at a given shift. + * @param shift - The pixel distance from the origin. + */ const x = 50 + shift; const y = app.interface.feedback.clientHeight - 15; const radius = 8; @@ -91,17 +91,18 @@ export const blinkScript = ( 0, 0, (app.interface.feedback as HTMLCanvasElement).width, - (app.interface.feedback as HTMLCanvasElement).height, + (app.interface.feedback as HTMLCanvasElement).height ); }, blinkDuration); } }; -/** - * Manages animation updates using requestAnimationFrame. - * @param app - The Editor application context. - */ export const scriptBlinkers = () => { + /** + * Manages animation updates using requestAnimationFrame. + * @param app - The Editor application context. + */ + let lastFrameTime = Date.now(); const frameRate = 10; const minFrameDelay = 1000 / frameRate; @@ -134,15 +135,16 @@ export interface OscilloscopeConfig { let lastZeroCrossingType: string | null = null; // 'negToPos' or 'posToNeg' let lastRenderTime: number = 0; -/** - * Initializes and runs an oscilloscope using an AnalyzerNode. - * @param {HTMLCanvasElement} canvas - The canvas element to draw the oscilloscope. - * @param {OscilloscopeConfig} config - Configuration for the oscilloscope's appearance and behavior. - */ export const runOscilloscope = ( canvas: HTMLCanvasElement, - app: Editor, + app: Editor ): void => { + /** + * Runs the oscilloscope visualization on the provided canvas element. + * + * @param canvas - The HTMLCanvasElement on which to render the visualization. + * @param app - The Editor object containing the configuration for the oscilloscope. + */ let config = app.osc; let analyzer = getAnalyser(config.fftSize); let dataArray = new Float32Array(analyzer.frequencyBinCount); @@ -155,7 +157,7 @@ export const runOscilloscope = ( width: number, height: number, offset_height: number, - offset_width: number, + offset_width: number ) { const maxFPS = 30; const now = performance.now(); @@ -170,11 +172,11 @@ export const runOscilloscope = ( const performanceFactor = 1; const reducedDataSize = Math.floor( - freqDataArray.length * performanceFactor, + freqDataArray.length * performanceFactor ); const numBars = Math.min( reducedDataSize, - app.osc.orientation === "horizontal" ? width : height, + app.osc.orientation === "horizontal" ? width : height ); const barWidth = app.osc.orientation === "horizontal" ? width / numBars : height / numBars; @@ -187,7 +189,7 @@ export const runOscilloscope = ( for (let i = 0; i < numBars; i++) { barHeight = Math.floor( freqDataArray[Math.floor((i * freqDataArray.length) / numBars)] * - ((height / 256) * app.osc.size), + ((height / 256) * app.osc.size) ); if (app.osc.orientation === "horizontal") { @@ -195,7 +197,7 @@ export const runOscilloscope = ( x + offset_width, (height - barHeight) / 2 + offset_height, barWidth + 1, - barHeight, + barHeight ); x += barWidth; } else { @@ -203,7 +205,7 @@ export const runOscilloscope = ( (width - barHeight) / 2 + offset_width, y + offset_height, barHeight, - barWidth + 1, + barWidth + 1 ); y += barWidth; } @@ -232,12 +234,19 @@ export const runOscilloscope = ( -OFFSET_WIDTH, -OFFSET_HEIGHT, WIDTH + 2 * OFFSET_WIDTH, - HEIGHT + 2 * OFFSET_HEIGHT, + HEIGHT + 2 * OFFSET_HEIGHT ); return; } if (analyzer.fftSize !== app.osc.fftSize) { + // Disconnect and release the old analyzer if it exists + if (analyzer) { + analyzer.disconnect(); + analyzer = null; // Release the reference for garbage collection + } + + // Create a new analyzer with the updated FFT size analyzer = getAnalyser(app.osc.fftSize); dataArray = new Float32Array(analyzer.frequencyBinCount); } @@ -252,7 +261,7 @@ export const runOscilloscope = ( -OFFSET_WIDTH, -OFFSET_HEIGHT, WIDTH + 2 * OFFSET_WIDTH, - HEIGHT + 2 * OFFSET_HEIGHT, + HEIGHT + 2 * OFFSET_HEIGHT ); } canvasCtx.lineWidth = app.osc.thickness; diff --git a/src/Clock.ts b/src/Clock.ts index c184e1f..e0c3a8b 100644 --- a/src/Clock.ts +++ b/src/Clock.ts @@ -48,10 +48,7 @@ export class Clock { lastPlayPressTime: number; totalPauseTime: number; - constructor( - public app: Editor, - ctx: AudioContext, - ) { + constructor(public app: Editor, ctx: AudioContext) { this.time_position = { bar: 0, beat: 0, pulse: 0 }; this.time_signature = [4, 4]; this.logicalTime = 0; @@ -77,6 +74,12 @@ export class Clock { } convertTicksToTimeposition(ticks: number): TimePosition { + /** + * Converts ticks to a TimePosition object. + * @param ticks The number of ticks to convert. + * @returns The TimePosition object representing the converted ticks. + */ + const beatsPerBar = this.app.clock.time_signature[0]; const ppqnPosition = ticks % this.app.clock.ppqn; const beatNumber = Math.floor(ticks / this.app.clock.ppqn); diff --git a/src/Documentation.ts b/src/Documentation.ts index 7dac040..b480491 100644 --- a/src/Documentation.ts +++ b/src/Documentation.ts @@ -47,7 +47,7 @@ export const makeExampleFactory = (application: Editor): Function => { const make_example = ( description: string, code: string, - open: boolean = false, + open: boolean = false ) => { const codeId = `codeExample${application.exampleCounter++}`; // Store the code snippet in the data structure @@ -70,7 +70,11 @@ export const makeExampleFactory = (application: Editor): Function => { }; export const documentation_factory = (application: Editor) => { - // Initialize a data structure to store code examples by their unique IDs + /** + * Creates the documentation for the given application. + * @param application The editor application. + * @returns An object containing various documentation sections. + */ application.api.codeExamples = {}; return { @@ -109,6 +113,10 @@ export const documentation_factory = (application: Editor) => { }; export const showDocumentation = (app: Editor) => { + /** + * Shows or hides the documentation based on the current state of the app. + * @param app - The Editor instance. + */ if (document.getElementById("app")?.classList.contains("hidden")) { document.getElementById("app")?.classList.remove("hidden"); document.getElementById("documentation")?.classList.add("hidden"); @@ -129,6 +137,9 @@ export const showDocumentation = (app: Editor) => { }; export const hideDocumentation = () => { + /** + * Hides the documentation section and shows the main application. + */ if (document.getElementById("app")?.classList.contains("hidden")) { document.getElementById("app")?.classList.remove("hidden"); document.getElementById("documentation")?.classList.add("hidden"); @@ -136,6 +147,12 @@ export const hideDocumentation = () => { }; export const updateDocumentationContent = (app: Editor, bindings: any) => { + /** + * Updates the content of the documentation pane with the converted markdown. + * + * @param app - The editor application. + * @param bindings - Additional bindings for the showdown converter. + */ const converter = new showdown.Converter({ emoji: true, moreStyling: true, @@ -143,7 +160,7 @@ export const updateDocumentationContent = (app: Editor, bindings: any) => { extensions: [showdownHighlight({ auto_detection: true }), ...bindings], }); const converted_markdown = converter.makeHtml( - app.docs[app.currentDocumentationPane], + app.docs[app.currentDocumentationPane] ); document.getElementById("documentation-content")!.innerHTML = converted_markdown; diff --git a/src/DomElements.ts b/src/DomElements.ts index 8689eca..a7844e0 100644 --- a/src/DomElements.ts +++ b/src/DomElements.ts @@ -63,6 +63,12 @@ export const buttonGroups = { //@ts-ignore export const createDocumentationStyle = (app: Editor) => { + /** + * Creates a documentation style object. + * @param {Editor} app - The editor object. + * @returns {Object} - The documentation style object. + */ + return { h1: "text-white lg:text-4xl text-xl lg:ml-4 lg:mx-4 mx-2 lg:my-4 my-2 lg:mb-4 mb-4 border-b-4 pt-4 pb-3 px-2", h2: "text-white lg:text-3xl text-xl lg:ml-4 lg:mx-4 mx-2 lg:my-4 my-2 lg:mb-4 mb-4 border-b-2 pt-12 pb-3 px-2", diff --git a/src/Evaluator.ts b/src/Evaluator.ts index b210dba..7c5b586 100644 --- a/src/Evaluator.ts +++ b/src/Evaluator.ts @@ -8,11 +8,19 @@ const codeReplace = (code: string): string => { const tryCatchWrapper = async ( application: Editor, - code: string, + code: string ): Promise => { + /** + * Wraps the provided code in a try-catch block and executes it. + * + * @param application - The editor application. + * @param code - The code to be executed. + * @returns A promise that resolves to a boolean indicating whether the code executed successfully or not. + */ + try { await new Function(`"use strict"; ${codeReplace(code)}`).call( - application.api, + application.api ); return true; } catch (error) { @@ -26,18 +34,32 @@ const cache = new Map(); const MAX_CACHE_SIZE = 40; const addFunctionToCache = (code: string, fn: Function) => { + /** + * Adds a function to the cache. + * @param code - The code associated with the function. + * @param fn - The function to be added to the cache. + */ if (cache.size >= MAX_CACHE_SIZE) { cache.delete(cache.keys().next().value); } cache.set(code, fn); }; -// Optimized evaluate function with reduced complexity export const tryEvaluate = async ( application: Editor, code: File, - timeout = 5000, + timeout = 5000 ): Promise => { + /** + * Tries to evaluate the provided code within a specified timeout period. + * Increments the evaluation count of the code file. + * If the code is valid, updates the committed code and adds the evaluated function to the cache. + * If the code is invalid, retries the evaluation. + * @param application - The editor application. + * @param code - The code file to evaluate. + * @param timeout - The timeout period in milliseconds (default: 5000). + * @returns A Promise that resolves when the evaluation is complete. + */ code.evaluations!++; const candidateCode = code.candidate; @@ -55,7 +77,7 @@ export const tryEvaluate = async ( if (isCodeValid) { code.committed = code.candidate; const newFunction = new Function( - `"use strict"; ${codeReplace(wrappedCode)}`, + `"use strict"; ${codeReplace(wrappedCode)}` ); addFunctionToCache(candidateCode, newFunction); } else { @@ -71,8 +93,16 @@ export const tryEvaluate = async ( export const evaluate = async ( application: Editor, code: File, - timeout = 1000, + timeout = 1000 ): Promise => { + /** + * Evaluates the given code using the provided application and timeout. + * @param application The editor application. + * @param code The code file to evaluate. + * @param timeout The timeout value in milliseconds (default: 1000). + * @returns A Promise that resolves when the evaluation is complete. + */ + try { await Promise.race([ tryCatchWrapper(application, code.committed as string), @@ -87,7 +117,7 @@ export const evaluate = async ( export const evaluateOnce = async ( application: Editor, - code: string, + code: string ): Promise => { /** * Evaluates the code once without any caching or error-handling mechanisms besides the tryCatchWrapper. diff --git a/src/FileManagement.ts b/src/FileManagement.ts index 213308e..093af7a 100644 --- a/src/FileManagement.ts +++ b/src/FileManagement.ts @@ -154,7 +154,7 @@ export class AppSettings { constructor() { const settingsFromStorage = JSON.parse( - localStorage.getItem("topos") || "{}", + localStorage.getItem("topos") || "{}" ); if (settingsFromStorage && Object.keys(settingsFromStorage).length !== 0) { @@ -210,7 +210,7 @@ export class AppSettings { saveApplicationToLocalStorage( universes: Universes, - settings: Settings, + settings: Settings ): void { /** * Main method to store the application to local storage. @@ -273,6 +273,11 @@ export const emptyUrl = () => { }; export const share = async (app: Editor) => { + /** + * Shares the current state of the app by generating a URL with encoded data and copying it to the clipboard. + * @param app - The Editor instance representing the app. + * @returns A Promise that resolves to void. + */ async function bufferToBase64(buffer: Uint8Array) { const base64url: string = await new Promise((r) => { const reader = new FileReader(); @@ -323,8 +328,20 @@ export const loadUniverserFromUrl = (app: Editor): void => { export const loadUniverse = ( app: Editor, universeName: string, - universe: Universe = template_universe, + universe: Universe = template_universe ): void => { + /** + * Loads a universe into the application. + * If the universe does not exist, a fresh clone of the template universe is created and added to the application. + * The references to the selected universe are updated in the application settings. + * The editor view is updated to reflect the selected universe. + * The initialization script for the selected universe is evaluated. + * + * @param app - The Editor application instance. + * @param universeName - The name of the universe to load. + * @param universe - The template universe to clone if the specified universe does not exist. + */ + let selectedUniverse = universeName.trim(); if (app.universes[selectedUniverse] === undefined) { // Pushing a freshly cloned template universe to: @@ -346,7 +363,11 @@ export const loadUniverse = ( }; export const openUniverseModal = (): void => { - // If the modal is hidden, unhide it and hide the editor + /** + * Opens the universe modal. + * If the modal is hidden, it unhides it and hides the editor. + * If the modal is already visible, it closes the modal. + */ if ( document.getElementById("modal-buffers")!.classList.contains("invisible") ) { @@ -359,6 +380,9 @@ export const openUniverseModal = (): void => { }; export const closeUniverseModal = (): void => { + /** + * Closes the universe modal and performs necessary actions. + */ // @ts-ignore document.getElementById("buffer-search")!.value = ""; document.getElementById("editor")!.classList.remove("invisible"); @@ -366,6 +390,9 @@ export const closeUniverseModal = (): void => { }; export const openSettingsModal = (): void => { + /** + * Opens the settings modal. + */ if ( document.getElementById("modal-settings")!.classList.contains("invisible") ) { @@ -377,6 +404,9 @@ export const openSettingsModal = (): void => { }; export const closeSettingsModal = (): void => { + /** + * Closes the settings modal and performs necessary actions. + */ document.getElementById("editor")!.classList.remove("invisible"); document.getElementById("modal-settings")!.classList.add("invisible"); }; diff --git a/src/main.ts b/src/main.ts index 1d3721a..26990cc 100644 --- a/src/main.ts +++ b/src/main.ts @@ -101,6 +101,19 @@ export class Editor { public hydra: any; constructor() { + /** + * This is the entry point of the application. The Editor instance is created when the page is loaded. + * It is responsible for: + * - Initializing the user interface + * - Loading the universe from local storage + * - Initializing the audio context and the clock + * - Building the user API + * - Building the documentation + * - Installing event listeners + * - Building the CodeMirror editor + * - Evaluating the init file + */ + // ================================================================================ // Build user interface // ================================================================================ @@ -194,6 +207,11 @@ export class Editor { } private getBuffer(type: string): any { + /** + * Retrieves the buffer based on the specified type. + * @param type - The type of buffer to retrieve. + * @returns The buffer object. + */ const universe = this.universes[this.selected_universe.toString()]; return type === "locals" ? universe[type][this.local_index] @@ -221,6 +239,12 @@ export class Editor { } updateKnownUniversesView = () => { + /** + * Updates the known universes view. + * This function generates and populates a list of known universes based on the data stored in the 'universes' property. + * It retrieves the necessary HTML elements and template, creates the list, and attaches event listeners to the generated items. + * If any required elements or templates are missing, warning messages are logged and the function returns early. + */ let itemTemplate = document.getElementById( "ui-known-universe-item-template" ) as HTMLTemplateElement; @@ -261,7 +285,13 @@ export class Editor { }; changeToLocalBuffer(i: number) { - // Updating the CSS accordingly + /** + * Changes the local buffer based on the provided index. + * Updates the CSS accordingly by adding a specific class to the selected tab and removing it from other tabs. + * Updates the local index and updates the editor view. + * + * @param i The index of the tab to change the local buffer to. + */ const tabs = document.querySelectorAll('[id^="tab-"]'); const tab = tabs[i] as HTMLElement; tab.classList.add("bg-orange-300"); @@ -274,6 +304,11 @@ export class Editor { } changeModeFromInterface(mode: "global" | "local" | "init" | "notes") { + /** + * Changes the mode of the interface. + * + * @param mode - The mode to change to. Can be one of "global", "local", "init", or "notes". + */ const interface_buttons: HTMLElement[] = [ this.interface.local_button, this.interface.global_button, @@ -345,6 +380,12 @@ export class Editor { button: "play" | "pause" | "stop" | "clear", highlight: boolean ) { + /** + * Sets the highlighting for a specific button. + * + * @param button - The button to highlight ("play", "pause", "stop", or "clear"). + * @param highlight - A boolean indicating whether to highlight the button or not. + */ document.getElementById("play-label")!.textContent = button !== "pause" ? "Pause" : "Play"; if (button !== "pause") { @@ -429,12 +470,12 @@ export class Editor { } } - /** - * Flashes the background of the view and its gutters. - * @param {string} color - The color to set. - * @param {number} duration - Duration in milliseconds to maintain the color. - */ flashBackground(color: string, duration: number): void { + /** + * Flashes the background of the view and its gutters. + * @param {string} color - The color to set. + * @param {number} duration - Duration in milliseconds to maintain the color. + */ const domElement = this.view.dom; const gutters = domElement.getElementsByClassName( "cm-gutter" @@ -480,6 +521,12 @@ export class Editor { } private loadHydraSynthAsync(): void { + /** + * Loads the Hydra Synth asynchronously by creating a script element + * and appending it to the document head. * Once the script is + * loaded successfully, it initializes the Hydra Synth. If there + * is an error loading the script, it logs an error message. + */ var script = document.createElement("script"); script.src = "https://unpkg.com/hydra-synth"; script.async = true; @@ -494,6 +541,9 @@ export class Editor { } private initializeHydra(): void { + /** + * Initializes the Hydra backend and sets up the Hydra synth. + */ // @ts-ignore this.hydra_backend = new Hydra({ canvas: this.interface.hydra_canvas as HTMLCanvasElement, @@ -506,6 +556,11 @@ export class Editor { } private setCanvas(canvas: HTMLCanvasElement): void { + /** + * Sets the canvas element and configures its size and context. + * + * @param canvas - The HTMLCanvasElement to set. + */ if (!canvas) return; const ctx = canvas.getContext("2d"); const dpr = window.devicePixelRatio || 1; From c56d6b1688db42a0fad00661a7cf4db736505361 Mon Sep 17 00:00:00 2001 From: Raphael Forment Date: Sun, 26 Nov 2023 13:35:40 +0100 Subject: [PATCH 11/34] Optimizing generics file --- src/Utils/Generic.ts | 81 +++++++++++++++++++++----------------------- 1 file changed, 39 insertions(+), 42 deletions(-) diff --git a/src/Utils/Generic.ts b/src/Utils/Generic.ts index 19a078f..7b0d309 100644 --- a/src/Utils/Generic.ts +++ b/src/Utils/Generic.ts @@ -1,15 +1,15 @@ -/* - * Transforms object with arrays into array of objects - * - * @param {Record} input - Object with arrays - * @param {string[]} ignoredKeys - Keys to ignore - * @returns {Record[]} Array of objects - * - */ export function objectWithArraysToArrayOfObjects( input: Record, - arraysToArrays: string[], + arraysToArrays: string[] ): Record[] { + /* + * Transforms object with arrays into array of objects + * + * @param {Record} input - Object with arrays + * @param {string[]} ignoredKeys - Keys to ignore + * @returns {Record[]} Array of objects + * + */ const inputCopy = { ...input }; arraysToArrays.forEach((k) => { if (Array.isArray(inputCopy[k]) && !Array.isArray(inputCopy[k][0])) { @@ -24,7 +24,7 @@ export function objectWithArraysToArrayOfObjects( acc.keys.push(key); return acc; }, - { keys: [] as string[], maxLength: 0 }, + { keys: [] as string[], maxLength: 0 } ); const output: Record[] = []; @@ -42,46 +42,43 @@ export function objectWithArraysToArrayOfObjects( return output; } -/* - * Transforms array of objects into object with arrays - * - * @param {Record[]} array - Array of objects - * @param {Record} mergeObject - Object that is merged to each object in the array - * @returns {object} Merged object with arrays - * - */ export function arrayOfObjectsToObjectWithArrays>( array: T[], - mergeObject: Record = {}, + mergeObject: Record = {} ): Record { - return array.reduce( - (acc, obj) => { - const mergedObj = { ...obj, ...mergeObject }; - Object.keys(mergedObj).forEach((key) => { - if (!acc[key]) { - acc[key] = []; - } - acc[key].push(mergedObj[key]); - }); - return acc; - }, - {} as Record, - ); + /* + * Transforms array of objects into object with arrays + * + * @param {Record[]} array - Array of objects + * @param {Record} mergeObject - Object that is merged to each object in the array + * @returns {object} Merged object with arrays + * + */ + return array.reduce((acc, obj) => { + const mergedObj = { ...obj, ...mergeObject }; + Object.keys(mergedObj).forEach((key) => { + if (!acc[key]) { + acc[key] = []; + } + acc[key].push(mergedObj[key]); + }); + return acc; + }, {} as Record); } -/* - * Filter certain keys from object - * - * @param {Record} obj - Object to filter - * @param {string[]} filter - Keys to filter - * @returns {object} Filtered object - * - */ export function filterObject( obj: Record, - filter: string[], + filter: string[] ): Record { + /* + * Filter certain keys from object + * + * @param {Record} obj - Object to filter + * @param {string[]} filter - Keys to filter + * @returns {object} Filtered object + * + */ return Object.fromEntries( - Object.entries(obj).filter(([key]) => filter.includes(key)), + Object.entries(obj).filter(([key]) => filter.includes(key)) ); } From 70c20b2d4a2a1401c20962966b0f04dffcdbb5c1 Mon Sep 17 00:00:00 2001 From: Raphael Forment Date: Sun, 26 Nov 2023 13:43:46 +0100 Subject: [PATCH 12/34] Pushing the experimental SoundEvent refactoring --- src/classes/SoundEvent.ts | 345 +++++++++++++++++++++----------------- 1 file changed, 194 insertions(+), 151 deletions(-) diff --git a/src/classes/SoundEvent.ts b/src/classes/SoundEvent.ts index 15b5d68..29f39e3 100644 --- a/src/classes/SoundEvent.ts +++ b/src/classes/SoundEvent.ts @@ -16,6 +16,7 @@ import { superdough, // @ts-ignore } from "superdough"; +import { Sound } from "zifferjs/src/types"; export type SoundParams = { dur: number | number[]; @@ -35,7 +36,16 @@ export class SoundEvent extends AudibleEvent { nudge: number; sound: any; - private methodMap = { + public updateValue( + key: string, + value: T | T[] | SoundParams[] | null + ): this { + if (value == null) return this; + this.values[key] = value; + return this; + } + + private static methodMap = { volume: ["volume", "vol"], zrand: ["zrand", "zr"], curve: ["curve"], @@ -67,17 +77,23 @@ export class SoundEvent extends AudibleEvent { phaserDepth: ["phaserDepth", "phasdepth"], phaserSweep: ["phaserSweep", "phassweep"], phaserCenter: ["phaserCenter", "phascenter"], - fmadsr: (a: number, d: number, s: number, r: number) => { - this.updateValue("fmattack", a); - this.updateValue("fmdecay", d); - this.updateValue("fmsustain", s); - this.updateValue("fmrelease", r); - return this; + fmadsr: function ( + self: SoundEvent, + a: number, + d: number, + s: number, + r: number + ) { + self.updateValue("fmattack", a); + self.updateValue("fmdecay", d); + self.updateValue("fmsustain", s); + self.updateValue("fmrelease", r); + return self; }, - fmad: (a: number, d: number) => { - this.updateValue("fmattack", a); - this.updateValue("fmdecay", d); - return this; + fmad: function (self: SoundEvent, a: number, d: number) { + self.updateValue("fmattack", a); + self.updateValue("fmdecay", d); + return self; }, ftype: ["ftype"], fanchor: ["fanchor"], @@ -85,147 +101,174 @@ export class SoundEvent extends AudibleEvent { decay: ["decay", "dec"], sustain: ["sustain", "sus"], release: ["release", "rel"], - adsr: (a: number, d: number, s: number, r: number) => { - this.updateValue("attack", a); - this.updateValue("decay", d); - this.updateValue("sustain", s); - this.updateValue("release", r); - return this; + adsr: function ( + self: SoundEvent, + a: number, + d: number, + s: number, + r: number + ) { + self.updateValue("attack", a); + self.updateValue("decay", d); + self.updateValue("sustain", s); + self.updateValue("release", r); + return self; }, - ad: (a: number, d: number) => { - this.updateValue("attack", a); - this.updateValue("decay", d); - this.updateValue("sustain", 0.0); - this.updateValue("release", 0.0); - return this; + ad: function (self: SoundEvent, a: number, d: number) { + self.updateValue("attack", a); + self.updateValue("decay", d); + self.updateValue("sustain", 0.0); + self.updateValue("release", 0.0); + return self; }, lpenv: ["lpenv", "lpe"], lpattack: ["lpattack", "lpa"], lpdecay: ["lpdecay", "lpd"], lpsustain: ["lpsustain", "lps"], lprelease: ["lprelease", "lpr"], - cutoff: (value: number, resonance?: number) => { - this.updateValue("cutoff", value); + cutoff: function (self: SoundEvent, value: number, resonance?: number) { + self.updateValue("cutoff", value); if (resonance) { - this.updateValue("resonance", resonance); + self.updateValue("resonance", resonance); } - return this; + return self; }, - lpf: (value: number, resonance?: number) => { - this.updateValue("cutoff", value); + lpf: function (self: SoundEvent, value: number, resonance?: number) { + self.updateValue("cutoff", value); if (resonance) { - this.updateValue("resonance", resonance); + self.updateValue("resonance", resonance); } - return this; + return self; }, - resonance: (value: number) => { + resonance: function (self: SoundEvent, value: number) { if (value >= 0 && value <= 1) { - this.updateValue("resonance", 50 * value); + self.updateValue("resonance", 50 * value); } - return this; + return self; }, - lpadsr: (depth: number, a: number, d: number, s: number, r: number) => { - this.updateValue("lpenv", depth); - this.updateValue("lpattack", a); - this.updateValue("lpdecay", d); - this.updateValue("lpsustain", s); - this.updateValue("lprelease", r); - return this; + lpadsr: function ( + self: SoundEvent, + depth: number, + a: number, + d: number, + s: number, + r: number + ) { + self.updateValue("lpenv", depth); + self.updateValue("lpattack", a); + self.updateValue("lpdecay", d); + self.updateValue("lpsustain", s); + self.updateValue("lprelease", r); + return self; }, - lpad: (depth: number, a: number, d: number) => { - this.updateValue("lpenv", depth); - this.updateValue("lpattack", a); - this.updateValue("lpdecay", d); - this.updateValue("lpsustain", 0); - this.updateValue("lprelease", 0); - return this; + lpad: function (self: SoundEvent, depth: number, a: number, d: number) { + self.updateValue("lpenv", depth); + self.updateValue("lpattack", a); + self.updateValue("lpdecay", d); + self.updateValue("lpsustain", 0); + self.updateValue("lprelease", 0); + return self; }, hpenv: ["hpenv", "hpe"], hpattack: ["hpattack", "hpa"], hpdecay: ["hpdecay", "hpd"], hpsustain: ["hpsustain", "hpsus"], hprelease: ["hprelease", "hpr"], - hcutoff: (value: number, resonance?: number) => { - this.updateValue("hcutoff", value); + hcutoff: function (self: SoundEvent, value: number, resonance?: number) { + self.updateValue("hcutoff", value); if (resonance) { - this.updateValue("hresonance", resonance); + self.updateValue("hresonance", resonance); } - return this; + return self; }, - hpf: (value: number, resonance?: number) => { - this.updateValue("hcutoff", value); + hpf: function (self: SoundEvent, value: number, resonance?: number) { + self.updateValue("hcutoff", value); if (resonance) { - this.updateValue("hresonance", resonance); + self.updateValue("hresonance", resonance); } - return this; + return self; }, - hpq: (value: number) => { - this.updateValue("hresonance", value); - return this; + hpq: function (self: SoundEvent, value: number) { + self.updateValue("hresonance", value); + return self; }, - hpadsr: (depth: number, a: number, d: number, s: number, r: number) => { - this.updateValue("hpenv", depth); - this.updateValue("hpattack", a); - this.updateValue("hpdecay", d); - this.updateValue("hpsustain", s); - this.updateValue("hprelease", r); - return this; + hpadsr: function ( + self: SoundEvent, + depth: number, + a: number, + d: number, + s: number, + r: number + ) { + self.updateValue("hpenv", depth); + self.updateValue("hpattack", a); + self.updateValue("hpdecay", d); + self.updateValue("hpsustain", s); + self.updateValue("hprelease", r); + return self; }, - hpad: (depth: number, a: number, d: number) => { - this.updateValue("hpenv", depth); - this.updateValue("hpattack", a); - this.updateValue("hpdecay", d); - this.updateValue("hpsustain", 0); - this.updateValue("hprelease", 0); - return this; + hpad: function (self: SoundEvent, depth: number, a: number, d: number) { + self.updateValue("hpenv", depth); + self.updateValue("hpattack", a); + self.updateValue("hpdecay", d); + self.updateValue("hpsustain", 0); + self.updateValue("hprelease", 0); + return self; }, bpenv: ["bpenv", "bpe"], bpattack: ["bpattack", "bpa"], bpdecay: ["bpdecay", "bpd"], bpsustain: ["bpsustain", "bps"], bprelease: ["bprelease", "bpr"], - bandf: (value: number, resonance?: number) => { - this.updateValue("bandf", value); + bandf: function (self: SoundEvent, value: number, resonance?: number) { + self.updateValue("bandf", value); if (resonance) { - this.updateValue("bandq", resonance); + self.updateValue("bandq", resonance); } - return this; + return self; }, - bpf: (value: number, resonance?: number) => { - this.updateValue("bandf", value); + bpf: function (self: SoundEvent, value: number, resonance?: number) { + self.updateValue("bandf", value); if (resonance) { - this.updateValue("bandq", resonance); + self.updateValue("bandq", resonance); } - return this; + return self; }, bandq: ["bandq", "bpq"], - bpadsr: (depth: number, a: number, d: number, s: number, r: number) => { - this.updateValue("bpenv", depth); - this.updateValue("bpattack", a); - this.updateValue("bpdecay", d); - this.updateValue("bpsustain", s); - this.updateValue("bprelease", r); - return this; + bpadsr: function ( + self: SoundEvent, + depth: number, + a: number, + d: number, + s: number, + r: number + ) { + self.updateValue("bpenv", depth); + self.updateValue("bpattack", a); + self.updateValue("bpdecay", d); + self.updateValue("bpsustain", s); + self.updateValue("bprelease", r); + return self; }, - bpad: (depth: number, a: number, d: number) => { - this.updateValue("bpenv", depth); - this.updateValue("bpattack", a); - this.updateValue("bpdecay", d); - this.updateValue("bpsustain", 0); - this.updateValue("bprelease", 0); - return this; + bpad: function (self: SoundEvent, depth: number, a: number, d: number) { + self.updateValue("bpenv", depth); + self.updateValue("bpattack", a); + self.updateValue("bpdecay", d); + self.updateValue("bpsustain", 0); + self.updateValue("bprelease", 0); + return self; }, vib: ["vib"], vibmod: ["vibmod"], - fm: (value: number | string) => { + fm: function (self: SoundEvent, value: number | string) { if (typeof value === "number") { - this.values["fmi"] = value; + self.values["fmi"] = value; } else { let values = value.split(":"); - this.values["fmi"] = parseFloat(values[0]); - if (values.length > 1) this.values["fmh"] = parseFloat(values[1]); + self.values["fmi"] = parseFloat(values[0]); + if (values.length > 1) self.values["fmh"] = parseFloat(values[1]); } - return this; + return self; }, loop: ["loop"], loopBegin: ["loopBegin", "loopb"], @@ -233,13 +276,13 @@ export class SoundEvent extends AudibleEvent { begin: ["begin"], end: ["end"], gain: ["gain"], - dbgain: (value: number) => { - this.updateValue("gain", Math.min(Math.pow(10, value / 20), 10)); - return this; + dbgain: function (self: SoundEvent, value: number) { + self.updateValue("gain", Math.min(Math.pow(10, value / 20), 10)); + return self; }, - db: (value: number) => { - this.updateValue("gain", Math.min(Math.pow(10, value / 20), 10)); - return this; + db: function (self: SoundEvent, value: number) { + self.updateValue("gain", Math.min(Math.pow(10, value / 20), 10)); + return self; }, velocity: ["velocity", "vel"], pan: ["pan"], @@ -260,62 +303,71 @@ export class SoundEvent extends AudibleEvent { roomlp: ["roomlp", "rlp"], roomdim: ["roomdim", "rdim"], sound: ["s", "sound"], - size: (value: number) => { - this.updateValue("roomsize", value); - return this; + size: function (self: SoundEvent, value: number) { + self.updateValue("roomsize", value); + return self; }, - sz: (value: number) => { - this.updateValue("roomsize", value); - return this; + sz: function (self: SoundEvent, value: number) { + self.updateValue("roomsize", value); + return self; }, comp: ["compressor", "cmp"], - ratio: (value: number) => { - this.updateValue("compressorRatio", value); - return this; + ratio: function (self: SoundEvent, value: number) { + self.updateValue("compressorRatio", value); + return self; }, - knee: (value: number) => { - this.updateValue("compressorKnee", value); - return this; + knee: function (self: SoundEvent, value: number) { + self.updateValue("compressorKnee", value); + return self; }, - compAttack: (value: number) => { - this.updateValue("compressorAttack", value); - return this; + compAttack: function (self: SoundEvent, value: number) { + self.updateValue("compressorAttack", value); + return self; }, - compRelease: (value: number) => { - this.updateValue("compressorRelease", value); - return this; + compRelease: function (self: SoundEvent, value: number) { + self.updateValue("compressorRelease", value); + return self; }, - stretch: (beat: number) => { - this.updateValue("unit", "c"); - this.updateValue("speed", 1 / beat); - this.updateValue("cut", beat); - return this; + stretch: function (self: SoundEvent, beat: number) { + self.updateValue("unit", "c"); + self.updateValue("speed", 1 / beat); + self.updateValue("cut", beat); + return self; }, }; - constructor( - sound: string | string[] | SoundParams, - public app: Editor, - ) { + constructor(sound: string | string[] | SoundParams, public app: Editor) { super(app); this.nudge = app.dough_nudge / 100; - for (const [methodName, keys] of Object.entries(this.methodMap)) { - if (Symbol.iterator in Object(keys)) { + for (const [methodName, keys] of Object.entries(SoundEvent.methodMap)) { + if (typeof keys === "object" && Symbol.iterator in Object(keys)) { for (const key of keys as string[]) { - // @ts-ignore + // Using arrow function to maintain 'this' context this[key] = (value: number) => this.updateValue(keys[0], value); } } else { - // @ts-ignore - this[methodName] = keys; + // this[methodName] = keys.bind(this); + this[methodName] = (...args) => keys(this, ...args); } } + + // for (const [methodName, keys] of Object.entries(SoundEvent.methodMap)) { + // if (typeof keys === "object" && Symbol.iterator in Object(keys)) { + // for (const key of keys as string[]) { + // // @ts-ignore + // this[key] = (value: number) => this.updateValue(this, keys[0], value); + // } + // } else { + // // @ts-ignore + // this[methodName] = keys; + // } + // } this.values = this.processSound(sound); } private processSound = ( - sound: string | string[] | SoundParams | SoundParams[], + sound: string | string[] | SoundParams | SoundParams[] ): SoundParams => { if (Array.isArray(sound) && typeof sound[0] === "string") { const s: string[] = []; @@ -357,15 +409,6 @@ export class SoundEvent extends AudibleEvent { } }; - private updateValue( - key: string, - value: T | T[] | SoundParams[] | null, - ): this { - if (value == null) return this; - this.values[key] = value; - return this; - } - // ================================================================================ // AbstactEvent overrides // ================================================================================ @@ -395,7 +438,7 @@ export class SoundEvent extends AudibleEvent { (event.key as number) || "C4", (event.pitch as number) || 0, (event.parsedScale as number[]) || event.scale || "MAJOR", - (event.octave as number) || 0, + (event.octave as number) || 0 ); event.note = note; event.freq = midiToFreq(note); @@ -415,7 +458,7 @@ export class SoundEvent extends AudibleEvent { public invert = (howMany: number = 0) => { if (this.values.chord) { let notes = this.values.chord.map( - (obj: { [key: string]: number }) => obj.note, + (obj: { [key: string]: number }) => obj.note ); notes = howMany < 0 ? [...notes].reverse() : notes; for (let i = 0; i < Math.abs(howMany); i++) { @@ -457,7 +500,7 @@ export class SoundEvent extends AudibleEvent { superdough( filteredEvent, this.nudge - this.app.clock.deviation, - filteredEvent.dur, + filteredEvent.dur ); } }; From b935cda91ad31555576c419a38907921a1e036ea Mon Sep 17 00:00:00 2001 From: Raphael Forment Date: Sun, 26 Nov 2023 13:50:23 +0100 Subject: [PATCH 13/34] Correct typing errors --- src/classes/SoundEvent.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/classes/SoundEvent.ts b/src/classes/SoundEvent.ts index 29f39e3..cb65f6d 100644 --- a/src/classes/SoundEvent.ts +++ b/src/classes/SoundEvent.ts @@ -16,7 +16,7 @@ import { superdough, // @ts-ignore } from "superdough"; -import { Sound } from "zifferjs/src/types"; +// import { Sound } from "zifferjs/src/types"; export type SoundParams = { dur: number | number[]; @@ -347,7 +347,7 @@ export class SoundEvent extends AudibleEvent { this[key] = (value: number) => this.updateValue(keys[0], value); } } else { - // this[methodName] = keys.bind(this); + // @ts-ignore this[methodName] = (...args) => keys(this, ...args); } } From fc47d598ac14e0293549302e8b003d0e25d2007d Mon Sep 17 00:00:00 2001 From: Raphael Forment Date: Sun, 26 Nov 2023 21:22:04 +0100 Subject: [PATCH 14/34] Adding Juliette sample pack --- src/API.ts | 9 ++++--- src/documentation/samples/sample_list.ts | 32 +++++++++++++++--------- 2 files changed, 26 insertions(+), 15 deletions(-) diff --git a/src/API.ts b/src/API.ts index 55384f7..c29b6bb 100644 --- a/src/API.ts +++ b/src/API.ts @@ -57,6 +57,9 @@ export async function loadSamples() { samples("github:Bubobubobubobubo/Dough-Amiga/main", undefined, { tag: "Amiga", }), + samples("github:Bubobubobubobubo/Dough-Juj/main", undefined, { + tag: "Juliette", + }), samples("github:Bubobubobubobubo/Dough-Amen/main", undefined, { tag: "Amen", }), @@ -1294,7 +1297,7 @@ export class UserAPI { const results: boolean[] = nArray.map( (value) => (this.app.clock.pulses_since_origin - Math.floor(nudge * this.ppqn())) % - Math.floor(value * this.ppqn()) === + Math.floor(value * this.ppqn()) === 0 ); return results.some((value) => value === true); @@ -1314,7 +1317,7 @@ export class UserAPI { const results: boolean[] = nArray.map( (value) => (this.app.clock.pulses_since_origin - nudgeInPulses) % - Math.floor(value * barLength) === + Math.floor(value * barLength) === 0 ); return results.some((value) => value === true); @@ -1914,7 +1917,7 @@ export class UserAPI { // ============================================================= register = (name: string, operation: EventOperation): void => { - AbstractEvent.prototype[name] = function ( + AbstractEvent.prototype[name] = function( this: AbstractEvent, ...args: any[] ) { diff --git a/src/documentation/samples/sample_list.ts b/src/documentation/samples/sample_list.ts index 4137f9d..49b1c85 100644 --- a/src/documentation/samples/sample_list.ts +++ b/src/documentation/samples/sample_list.ts @@ -65,12 +65,12 @@ On this page, you will find an exhaustive list of all the samples currently load A very large collection of wavetables for wavetable synthesis. This collection has been released by Kristoffer Ekstrand: [AKWF Waveforms](https://www.adventurekid.se/akrt/waveforms/adventure-kid-waveforms/). Every sound sample that starts with wt_ will be looped. Look at this demo: ${makeExample( - "Wavetable synthesis made easy :)", - ` + "Wavetable synthesis made easy :)", + ` beat(0.5)::sound('wt_stereo').n([0, 1].pick()).ad(0, .25).out() `, - true, -)} + true, + )} Pick one folder and spend some time exploring it. There is a lot of different waveforms. @@ -84,12 +84,12 @@ ${samples_to_markdown(application, "Waveforms")} A set of 72 classic drum machines created by **Geikha**: [Geikha Drum Machines](https://github.com/geikha/tidal-drum-machines). To use them efficiently, it is best to use the .bank() parameter like so: ${makeExample( - "Using a classic drum machine", - ` + "Using a classic drum machine", + ` beat(0.5)::sound(['bd', 'cp'].pick()).bank("AkaiLinn").out() `, - true, -)} + true, + )} Here is the complete list of available machines: @@ -119,12 +119,12 @@ ${samples_to_markdown(application, "Amiga")} A collection of many different amen breaks. Use .stretch() to play with these: ${makeExample( - "Stretching an amen break", - ` + "Stretching an amen break", + ` beat(4)::sound('amen1').stretch(4).out() `, - true, -)} + true, + )} The stretch should be adapted based on the length of each amen break. @@ -140,5 +140,13 @@ Many live coders are expecting to find the Tidal sample library wherever they go
${samples_to_markdown(application, "Tidal")}
+ +## Juliette's voice + +This sample pack is only one folder full of french phonems! It sounds super nice. + +
+${samples_to_markdown(application, "Juliette")} +
`; }; From 22508acb9fc15bcd89b77ebaaca7db54ffd007bc Mon Sep 17 00:00:00 2001 From: Raphael Forment Date: Sun, 26 Nov 2023 23:06:49 +0100 Subject: [PATCH 15/34] lint --- .github/workflows/build_docker.yml | 17 +-- .vscode/settings.json | 3 +- README.md | 7 +- docker-compose.yml | 8 +- favicon/site.webmanifest | 34 +++--- fonts/index.css | 135 ++++++++++++----------- postcss.config.js | 2 +- src/API.ts | 74 ++++++------- src/AudioVisualisation.ts | 26 ++--- src/Clock.ts | 5 +- src/Documentation.ts | 4 +- src/Evaluator.ts | 12 +- src/FileManagement.ts | 6 +- src/Utils/Generic.ts | 33 +++--- src/WindowBehavior.ts | 10 +- src/classes/SoundEvent.ts | 25 +++-- src/classes/ZPlayer.ts | 12 +- src/documentation/more/bonus.ts | 10 +- src/documentation/samples/sample_list.ts | 24 ++-- src/main.ts | 26 ++--- tsconfig.json | 2 +- 21 files changed, 243 insertions(+), 232 deletions(-) diff --git a/.github/workflows/build_docker.yml b/.github/workflows/build_docker.yml index 80aae64..71a6250 100644 --- a/.github/workflows/build_docker.yml +++ b/.github/workflows/build_docker.yml @@ -3,28 +3,23 @@ name: Build and Push Docker Images on: push: branches: - - 'main' + - "main" jobs: topos: runs-on: ubuntu-latest steps: - - - name: Checkout + - name: Checkout uses: actions/checkout@v2 - - - name: Set up QEMU + - name: Set up QEMU uses: docker/setup-qemu-action@v3 - - - name: Set up Docker Buildx + - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 - - - name: Login to Docker Hub + - name: Login to Docker Hub uses: docker/login-action@v3 with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} - - - name: Build and push + - name: Build and push uses: docker/build-push-action@v5 with: push: true diff --git a/.vscode/settings.json b/.vscode/settings.json index 7a73a41..0967ef4 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,2 +1 @@ -{ -} \ No newline at end of file +{} diff --git a/README.md b/README.md index 2d9ee97..eb55eb5 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,7 @@

-Topos is a web-based live coding environment. It lives [here](https://topos.live). Documentation is directly embedded in the application itself. Topos is an emulation and extension of the [Monome Teletype](https://monome.org/docs/teletype/) that gradually evolved into something a bit more personal. +Topos is a web-based live coding environment. It lives [here](https://topos.live). Documentation is directly embedded in the application itself. Topos is an emulation and extension of the [Monome Teletype](https://monome.org/docs/teletype/) that gradually evolved into something a bit more personal. ![Screenshot](https://github.com/Bubobubobubobubo/Topos/blob/main/img/topos_gif.gif) @@ -46,15 +46,18 @@ The `tauri` version is only here to quickstart future developments but nothing ## Docker ### Run the application + `docker run -p 8001:80 yassinsiouda/topos:latest` ### Build and run the prod image + `docker compose --profile prod up` ### Build and run the dev image **First installation** First you need to map node_modules to your local machine for your ide intellisense to work properly + ```bash docker compose --profile dev up -d docker cp topos-dev:/app/node_modules . @@ -62,7 +65,7 @@ docker compose --profile dev down ``` **Then** + ```bash docker compose --profile dev up ``` - diff --git a/docker-compose.yml b/docker-compose.yml index 0021919..62f86ee 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,9 +1,9 @@ -version: '3.7' +version: "3.7" services: topos-dev: container_name: topos-dev profiles: ["dev"] - build: + build: context: . target: "dev" volumes: @@ -21,8 +21,8 @@ services: topos-prod: container_name: topos-prod profiles: ["prod"] - build: + build: context: . target: "prod" ports: - - "8001:80" \ No newline at end of file + - "8001:80" diff --git a/favicon/site.webmanifest b/favicon/site.webmanifest index b20abb7..fa99de7 100644 --- a/favicon/site.webmanifest +++ b/favicon/site.webmanifest @@ -1,19 +1,19 @@ { - "name": "", - "short_name": "", - "icons": [ - { - "src": "/android-chrome-192x192.png", - "sizes": "192x192", - "type": "image/png" - }, - { - "src": "/android-chrome-512x512.png", - "sizes": "512x512", - "type": "image/png" - } - ], - "theme_color": "#ffffff", - "background_color": "#ffffff", - "display": "standalone" + "name": "", + "short_name": "", + "icons": [ + { + "src": "/android-chrome-192x192.png", + "sizes": "192x192", + "type": "image/png" + }, + { + "src": "/android-chrome-512x512.png", + "sizes": "512x512", + "type": "image/png" + } + ], + "theme_color": "#ffffff", + "background_color": "#ffffff", + "display": "standalone" } diff --git a/fonts/index.css b/fonts/index.css index 967bf0c..159b271 100644 --- a/fonts/index.css +++ b/fonts/index.css @@ -1,7 +1,8 @@ @font-face { font-family: "IBM Plex Mono"; - src: url("woff2/IBMPlexMono-Regular.woff2") format("woff2"), - url("woff/IBMPlexMono-Regular.woff") format("woff"); + src: + url("woff2/IBMPlexMono-Regular.woff2") format("woff2"), + url("woff/IBMPlexMono-Regular.woff") format("woff"); font-weight: 400; font-style: normal; font-display: swap; @@ -9,8 +10,9 @@ @font-face { font-family: "IBM PLex Mono"; - src: url("woff2/IBMPlexMono-Italic.woff2") format("woff2"), - url("woff/IBMPlexMono-Italic.woff") format("woff"); + src: + url("woff2/IBMPlexMono-Italic.woff2") format("woff2"), + url("woff/IBMPlexMono-Italic.woff") format("woff"); font-weight: 400; font-style: italic; font-display: swap; @@ -18,8 +20,9 @@ @font-face { font-family: "IBM PLex Mono"; - src: url("woff2/IBMPlexMono-Bold.woff2") format("woff2"), - url("woff/IBMPlexMono-Bold.woff") format("woff"); + src: + url("woff2/IBMPlexMono-Bold.woff2") format("woff2"), + url("woff/IBMPlexMono-Bold.woff") format("woff"); font-weight: 700; font-style: normal; font-display: swap; @@ -27,8 +30,9 @@ @font-face { font-family: "IBM Plex Mono"; - src: url("woff2/IBMPlexMono-BoldItalic.woff2") format("woff2"), - url("woff/IBMPlexMono-BoldItalic.woff") format("woff"); + src: + url("woff2/IBMPlexMono-BoldItalic.woff2") format("woff2"), + url("woff/IBMPlexMono-BoldItalic.woff") format("woff"); font-weight: 700; font-style: italic; font-display: swap; @@ -37,84 +41,85 @@ @font-face { font-family: "Comic Mono"; font-weight: normal; - src: url(./woff/ComicMono.woff) format("woff"), - url(./woff2/ComicMono.woff2) format("wooff2"); + src: + url(./woff/ComicMono.woff) format("woff"), + url(./woff2/ComicMono.woff2) format("wooff2"); } @font-face { font-family: "Comic Mono"; font-weight: bold; - src: url(./woff/ComicMono-Bold.woff) format("woff"), - url(./woff/ComicMono-Bold.woff2) format("woff2"), -} - - -@font-face { - font-family: 'jgs7'; - src: url('./woff2/jgs7.woff2') format('woff2'), - url('./woff/jgs7.woff') format('woff'); - font-weight: normal; - font-style: normal; - font-display: swap; + src: + url(./woff/ComicMono-Bold.woff) format("woff"), + url(./woff/ComicMono-Bold.woff2) format("woff2"); } @font-face { - font-family: 'jgs5'; - src: url('./woff2/jgs5.woff2') format('woff2'), - url('./woff/jgs5.woff') format('woff'); - font-weight: normal; - font-style: normal; - font-display: swap; + font-family: "jgs7"; + src: + url("./woff2/jgs7.woff2") format("woff2"), + url("./woff/jgs7.woff") format("woff"); + font-weight: normal; + font-style: normal; + font-display: swap; } @font-face { - font-family: 'jgs9'; - src: url('./woff2/jgs9.woff2') format('woff2'), - url('./woff/jgs9.woff') format('woff'); - font-weight: normal; - font-style: normal; - font-display: swap; -} - - -@font-face { - font-family: 'jgs_vecto'; - src: url('./woff2/jgs_vecto.woff2') format('woff2'); - font-weight: normal; - font-style: normal; - font-display: swap; + font-family: "jgs5"; + src: + url("./woff2/jgs5.woff2") format("woff2"), + url("./woff/jgs5.woff") format("woff"); + font-weight: normal; + font-style: normal; + font-display: swap; } @font-face { - font-family: 'Steps Mono'; - src: url('./woff2/Steps-Mono.woff2') format('woff2'); - font-weight: normal; - font-style: normal; - font-display: swap; + font-family: "jgs9"; + src: + url("./woff2/jgs9.woff2") format("woff2"), + url("./woff/jgs9.woff") format("woff"); + font-weight: normal; + font-style: normal; + font-display: swap; } @font-face { - font-family: 'Steps Mono Thin'; - src: url('./woff2/Steps-Mono-Thin.woff2') format('woff2'); - font-weight: normal; - font-style: normal; - font-display: swap; + font-family: "jgs_vecto"; + src: url("./woff2/jgs_vecto.woff2") format("woff2"); + font-weight: normal; + font-style: normal; + font-display: swap; } - @font-face { - font-family: 'Jet Brains'; - src: url('./woff2/JetBrainsMono-Regular.woff2') format('woff2'); - font-weight: normal; - font-style: normal; - font-display: swap; + font-family: "Steps Mono"; + src: url("./woff2/Steps-Mono.woff2") format("woff2"); + font-weight: normal; + font-style: normal; + font-display: swap; } - @font-face { - font-family: 'Jet Brains'; - src: url('./woff2/JetBrainsMono-Bold.woff2') format('woff2'); - font-weight: 700; - font-style: normal; - font-display: swap; + font-family: "Steps Mono Thin"; + src: url("./woff2/Steps-Mono-Thin.woff2") format("woff2"); + font-weight: normal; + font-style: normal; + font-display: swap; +} + +@font-face { + font-family: "Jet Brains"; + src: url("./woff2/JetBrainsMono-Regular.woff2") format("woff2"); + font-weight: normal; + font-style: normal; + font-display: swap; +} + +@font-face { + font-family: "Jet Brains"; + src: url("./woff2/JetBrainsMono-Bold.woff2") format("woff2"); + font-weight: 700; + font-style: normal; + font-display: swap; } diff --git a/postcss.config.js b/postcss.config.js index 2e7af2b..2aa7205 100644 --- a/postcss.config.js +++ b/postcss.config.js @@ -3,4 +3,4 @@ export default { tailwindcss: {}, autoprefixer: {}, }, -} +}; diff --git a/src/API.ts b/src/API.ts index c29b6bb..3269f6d 100644 --- a/src/API.ts +++ b/src/API.ts @@ -111,7 +111,7 @@ export class UserAPI { } this.app.settings.saveApplicationToLocalStorage( this.app.universes, - this.app.settings + this.app.settings, ); this.app.updateKnownUniversesView(); }; @@ -203,7 +203,7 @@ export class UserAPI { // @ts-ignore this.errorTimeoutID = setTimeout( () => this.app.interface.error_line.classList.add("hidden"), - 2000 + 2000, ); }; @@ -217,7 +217,7 @@ export class UserAPI { // @ts-ignore this.printTimeoutID = setTimeout( () => this.app.interface.error_line.classList.add("hidden"), - 4000 + 4000, ); }; @@ -268,7 +268,7 @@ export class UserAPI { */ this.app.clock.tick = beat * this.app.clock.ppqn; this.app.clock.time_position = this.app.clock.convertTicksToTimeposition( - beat * this.app.clock.ppqn + beat * this.app.clock.ppqn, ); }; @@ -325,7 +325,7 @@ export class UserAPI { blinkScript(this.app, "local", arg); tryEvaluate( this.app, - this.app.universes[this.app.selected_universe].locals[arg] + this.app.universes[this.app.selected_universe].locals[arg], ); } }); @@ -372,7 +372,7 @@ export class UserAPI { delete this.app.universes[universe]; this.app.settings.saveApplicationToLocalStorage( this.app.universes, - this.app.settings + this.app.settings, ); this.app.updateKnownUniversesView(); }; @@ -388,7 +388,7 @@ export class UserAPI { }; this.app.settings.saveApplicationToLocalStorage( this.app.universes, - this.app.settings + this.app.settings, ); } this.app.selected_universe = "Default"; @@ -425,7 +425,7 @@ export class UserAPI { value: number | number[] = 60, velocity?: number | number[], channel?: number | number[], - port?: number | string | number[] | string[] + port?: number | string | number[] | string[], ): MidiEvent => { /** * Sends a MIDI note to the current MIDI output. @@ -500,7 +500,7 @@ export class UserAPI { }; public active_note_events = ( - channel?: number + channel?: number, ): MidiNoteEvent[] | undefined => { /** * @returns A list of currently active MIDI notes @@ -637,7 +637,7 @@ export class UserAPI { scale: number | string, channel: number = 0, port: number | string = this.MidiConnection.currentOutputIndex || 0, - soundOff: boolean = false + soundOff: boolean = false, ): void => { /** * Sends given scale to midi output for visual aid @@ -661,7 +661,7 @@ export class UserAPI { // @ts-ignore scale: number | string = 0, channel: number = 0, - port: number | string = this.MidiConnection.currentOutputIndex || 0 + port: number | string = this.MidiConnection.currentOutputIndex || 0, ): void => { /** * Hides all notes by sending all notes off to midi output @@ -676,7 +676,7 @@ export class UserAPI { midi_notes_off = ( channel: number = 0, - port: number | string = this.MidiConnection.currentOutputIndex || 0 + port: number | string = this.MidiConnection.currentOutputIndex || 0, ): void => { /** * Sends all notes off to midi output @@ -686,7 +686,7 @@ export class UserAPI { midi_sound_off = ( channel: number = 0, - port: number | string = this.MidiConnection.currentOutputIndex || 0 + port: number | string = this.MidiConnection.currentOutputIndex || 0, ): void => { /** * Sends all sound off to midi output @@ -713,7 +713,7 @@ export class UserAPI { public z = ( input: string | Generator, options: InputOptions = {}, - id: number | string = "" + id: number | string = "", ): Player => { const zid = "z" + id.toString(); const key = id === "" ? this.generateCacheKey(input, options) : zid; @@ -790,7 +790,7 @@ export class UserAPI { public counter = ( name: string | number, limit?: number, - step?: number + step?: number, ): number => { /** * Returns the current value of a counter, and increments it by the step value. @@ -1297,8 +1297,8 @@ export class UserAPI { const results: boolean[] = nArray.map( (value) => (this.app.clock.pulses_since_origin - Math.floor(nudge * this.ppqn())) % - Math.floor(value * this.ppqn()) === - 0 + Math.floor(value * this.ppqn()) === + 0, ); return results.some((value) => value === true); }; @@ -1317,8 +1317,8 @@ export class UserAPI { const results: boolean[] = nArray.map( (value) => (this.app.clock.pulses_since_origin - nudgeInPulses) % - Math.floor(value * barLength) === - 0 + Math.floor(value * barLength) === + 0, ); return results.some((value) => value === true); }; @@ -1333,7 +1333,7 @@ export class UserAPI { */ const nArray = Array.isArray(n) ? n : [n]; const results: boolean[] = nArray.map( - (value) => (this.app.clock.pulses_since_origin - nudge) % value === 0 + (value) => (this.app.clock.pulses_since_origin - nudge) % value === 0, ); return results.some((value) => value === true); }; @@ -1342,7 +1342,7 @@ export class UserAPI { public tick = (tick: number | number[], offset: number = 0): boolean => { const nArray = Array.isArray(tick) ? tick : [tick]; const results: boolean[] = nArray.map( - (value) => this.app.clock.time_position.pulse === value + offset + (value) => this.app.clock.time_position.pulse === value + offset, ); return results.some((value) => value === true); }; @@ -1391,7 +1391,7 @@ export class UserAPI { public onbar = ( bars: number[] | number, - n: number = this.app.clock.time_signature[0] + n: number = this.app.clock.time_signature[0], ): boolean => { let current_bar = (this.app.clock.time_position.bar % n) + 1; return typeof bars === "number" @@ -1419,7 +1419,7 @@ export class UserAPI { if (decimal_part <= 0) decimal_part = decimal_part + this.ppqn() * this.nominator(); final_pulses.push( - integral_part === this.cbeat() && this.cpulse() === decimal_part + integral_part === this.cbeat() && this.cpulse() === decimal_part, ); }); return final_pulses.some((p) => p == true); @@ -1501,7 +1501,7 @@ export class UserAPI { iterator: number, pulses: number, length: number, - rotate: number = 0 + rotate: number = 0, ): boolean => { /** * Returns a euclidean cycle of size length, with n pulses, rotated or not. @@ -1520,7 +1520,7 @@ export class UserAPI { div: number, pulses: number, length: number, - rotate: number = 0 + rotate: number = 0, ): boolean => { return ( this.beat(div) && this._euclidean_cycle(pulses, length, rotate).beat(div) @@ -1530,7 +1530,7 @@ export class UserAPI { _euclidean_cycle( pulses: number, length: number, - rotate: number = 0 + rotate: number = 0, ): boolean[] { if (pulses == length) return Array.from({ length }, () => true); function startsDescent(list: number[], i: number): boolean { @@ -1541,7 +1541,7 @@ export class UserAPI { if (pulses >= length) return [true]; const resList = Array.from( { length }, - (_, i) => (((pulses * (i - 1)) % length) + length) % length + (_, i) => (((pulses * (i - 1)) % length) + length) % length, ); let cycle = resList.map((_, i) => startsDescent(resList, i)); if (rotate != 0) { @@ -1660,7 +1660,7 @@ export class UserAPI { triangle = ( freq: number = 1, times: number = 1, - offset: number = 0 + offset: number = 0, ): number => { /** * Returns a triangle wave between -1 and 1. @@ -1677,7 +1677,7 @@ export class UserAPI { utriangle = ( freq: number = 1, times: number = 1, - offset: number = 0 + offset: number = 0, ): number => { /** * Returns a triangle wave between 0 and 1. @@ -1694,7 +1694,7 @@ export class UserAPI { freq: number = 1, times: number = 1, offset: number = 0, - duty: number = 0.5 + duty: number = 0.5, ): number => { /** * Returns a square wave with a specified duty cycle between -1 and 1. @@ -1714,7 +1714,7 @@ export class UserAPI { freq: number = 1, times: number = 1, offset: number = 0, - duty: number = 0.5 + duty: number = 0.5, ): number => { /** * Returns a square wave between 0 and 1. @@ -1774,7 +1774,7 @@ export class UserAPI { */ const sum = values.reduce( (accumulator, currentValue) => accumulator + currentValue, - 0 + 0, ); return sum / values.length; }; @@ -1784,7 +1784,7 @@ export class UserAPI { yMin: number, yMax: number, xMin: number, - xMax: number + xMax: number, ): number => { const percent = (inputY - yMin) / (yMax - yMin); const outputX = percent * (xMax - xMin) + xMin; @@ -1814,7 +1814,7 @@ export class UserAPI { lang: string = "en-US", voice: number = 0, rate: number = 1, - pitch: number = 1 + pitch: number = 1, ): void => { /* * Speaks the given text using the browser's speech synthesis API. @@ -1889,7 +1889,7 @@ export class UserAPI { const elements = args.slice(1); // Get the rest of the arguments as an array const timepos = this.app.clock.pulses_since_origin; const slice_count = Math.floor( - timepos / Math.floor(chunk_size * this.ppqn()) + timepos / Math.floor(chunk_size * this.ppqn()), ); return elements[slice_count % elements.length]; }; @@ -1917,7 +1917,7 @@ export class UserAPI { // ============================================================= register = (name: string, operation: EventOperation): void => { - AbstractEvent.prototype[name] = function( + AbstractEvent.prototype[name] = function ( this: AbstractEvent, ...args: any[] ) { @@ -2024,7 +2024,7 @@ export class UserAPI { ".cm-comment": { fontFamily: commentFont, }, - }) + }), ), }); }; diff --git a/src/AudioVisualisation.ts b/src/AudioVisualisation.ts index a74cce8..07a9c8e 100644 --- a/src/AudioVisualisation.ts +++ b/src/AudioVisualisation.ts @@ -14,7 +14,7 @@ export const drawCircle = ( x: number, y: number, radius: number, - color: string + color: string, ): void => { // @ts-ignore const canvas: HTMLCanvasElement = app.interface.feedback; @@ -36,7 +36,7 @@ export const blinkScript = ( */ app: Editor, script: "local" | "global" | "init", - no?: number + no?: number, ) => { if (no !== undefined && no < 1 && no > 9) return; const blinkDuration = @@ -55,7 +55,7 @@ export const blinkScript = ( horizontalOffset + shift, app.interface.feedback.clientHeight - 15, 8, - "#fdba74" + "#fdba74", ); }; @@ -91,7 +91,7 @@ export const blinkScript = ( 0, 0, (app.interface.feedback as HTMLCanvasElement).width, - (app.interface.feedback as HTMLCanvasElement).height + (app.interface.feedback as HTMLCanvasElement).height, ); }, blinkDuration); } @@ -137,7 +137,7 @@ let lastRenderTime: number = 0; export const runOscilloscope = ( canvas: HTMLCanvasElement, - app: Editor + app: Editor, ): void => { /** * Runs the oscilloscope visualization on the provided canvas element. @@ -157,7 +157,7 @@ export const runOscilloscope = ( width: number, height: number, offset_height: number, - offset_width: number + offset_width: number, ) { const maxFPS = 30; const now = performance.now(); @@ -172,11 +172,11 @@ export const runOscilloscope = ( const performanceFactor = 1; const reducedDataSize = Math.floor( - freqDataArray.length * performanceFactor + freqDataArray.length * performanceFactor, ); const numBars = Math.min( reducedDataSize, - app.osc.orientation === "horizontal" ? width : height + app.osc.orientation === "horizontal" ? width : height, ); const barWidth = app.osc.orientation === "horizontal" ? width / numBars : height / numBars; @@ -189,7 +189,7 @@ export const runOscilloscope = ( for (let i = 0; i < numBars; i++) { barHeight = Math.floor( freqDataArray[Math.floor((i * freqDataArray.length) / numBars)] * - ((height / 256) * app.osc.size) + ((height / 256) * app.osc.size), ); if (app.osc.orientation === "horizontal") { @@ -197,7 +197,7 @@ export const runOscilloscope = ( x + offset_width, (height - barHeight) / 2 + offset_height, barWidth + 1, - barHeight + barHeight, ); x += barWidth; } else { @@ -205,7 +205,7 @@ export const runOscilloscope = ( (width - barHeight) / 2 + offset_width, y + offset_height, barHeight, - barWidth + 1 + barWidth + 1, ); y += barWidth; } @@ -234,7 +234,7 @@ export const runOscilloscope = ( -OFFSET_WIDTH, -OFFSET_HEIGHT, WIDTH + 2 * OFFSET_WIDTH, - HEIGHT + 2 * OFFSET_HEIGHT + HEIGHT + 2 * OFFSET_HEIGHT, ); return; } @@ -261,7 +261,7 @@ export const runOscilloscope = ( -OFFSET_WIDTH, -OFFSET_HEIGHT, WIDTH + 2 * OFFSET_WIDTH, - HEIGHT + 2 * OFFSET_HEIGHT + HEIGHT + 2 * OFFSET_HEIGHT, ); } canvasCtx.lineWidth = app.osc.thickness; diff --git a/src/Clock.ts b/src/Clock.ts index e0c3a8b..2d78671 100644 --- a/src/Clock.ts +++ b/src/Clock.ts @@ -48,7 +48,10 @@ export class Clock { lastPlayPressTime: number; totalPauseTime: number; - constructor(public app: Editor, ctx: AudioContext) { + constructor( + public app: Editor, + ctx: AudioContext, + ) { this.time_position = { bar: 0, beat: 0, pulse: 0 }; this.time_signature = [4, 4]; this.logicalTime = 0; diff --git a/src/Documentation.ts b/src/Documentation.ts index b480491..5a86c77 100644 --- a/src/Documentation.ts +++ b/src/Documentation.ts @@ -47,7 +47,7 @@ export const makeExampleFactory = (application: Editor): Function => { const make_example = ( description: string, code: string, - open: boolean = false + open: boolean = false, ) => { const codeId = `codeExample${application.exampleCounter++}`; // Store the code snippet in the data structure @@ -160,7 +160,7 @@ export const updateDocumentationContent = (app: Editor, bindings: any) => { extensions: [showdownHighlight({ auto_detection: true }), ...bindings], }); const converted_markdown = converter.makeHtml( - app.docs[app.currentDocumentationPane] + app.docs[app.currentDocumentationPane], ); document.getElementById("documentation-content")!.innerHTML = converted_markdown; diff --git a/src/Evaluator.ts b/src/Evaluator.ts index 7c5b586..2df2272 100644 --- a/src/Evaluator.ts +++ b/src/Evaluator.ts @@ -8,7 +8,7 @@ const codeReplace = (code: string): string => { const tryCatchWrapper = async ( application: Editor, - code: string + code: string, ): Promise => { /** * Wraps the provided code in a try-catch block and executes it. @@ -20,7 +20,7 @@ const tryCatchWrapper = async ( try { await new Function(`"use strict"; ${codeReplace(code)}`).call( - application.api + application.api, ); return true; } catch (error) { @@ -48,7 +48,7 @@ const addFunctionToCache = (code: string, fn: Function) => { export const tryEvaluate = async ( application: Editor, code: File, - timeout = 5000 + timeout = 5000, ): Promise => { /** * Tries to evaluate the provided code within a specified timeout period. @@ -77,7 +77,7 @@ export const tryEvaluate = async ( if (isCodeValid) { code.committed = code.candidate; const newFunction = new Function( - `"use strict"; ${codeReplace(wrappedCode)}` + `"use strict"; ${codeReplace(wrappedCode)}`, ); addFunctionToCache(candidateCode, newFunction); } else { @@ -93,7 +93,7 @@ export const tryEvaluate = async ( export const evaluate = async ( application: Editor, code: File, - timeout = 1000 + timeout = 1000, ): Promise => { /** * Evaluates the given code using the provided application and timeout. @@ -117,7 +117,7 @@ export const evaluate = async ( export const evaluateOnce = async ( application: Editor, - code: string + code: string, ): Promise => { /** * Evaluates the code once without any caching or error-handling mechanisms besides the tryCatchWrapper. diff --git a/src/FileManagement.ts b/src/FileManagement.ts index 093af7a..145b3d1 100644 --- a/src/FileManagement.ts +++ b/src/FileManagement.ts @@ -154,7 +154,7 @@ export class AppSettings { constructor() { const settingsFromStorage = JSON.parse( - localStorage.getItem("topos") || "{}" + localStorage.getItem("topos") || "{}", ); if (settingsFromStorage && Object.keys(settingsFromStorage).length !== 0) { @@ -210,7 +210,7 @@ export class AppSettings { saveApplicationToLocalStorage( universes: Universes, - settings: Settings + settings: Settings, ): void { /** * Main method to store the application to local storage. @@ -328,7 +328,7 @@ export const loadUniverserFromUrl = (app: Editor): void => { export const loadUniverse = ( app: Editor, universeName: string, - universe: Universe = template_universe + universe: Universe = template_universe, ): void => { /** * Loads a universe into the application. diff --git a/src/Utils/Generic.ts b/src/Utils/Generic.ts index 7b0d309..e6c9190 100644 --- a/src/Utils/Generic.ts +++ b/src/Utils/Generic.ts @@ -1,6 +1,6 @@ export function objectWithArraysToArrayOfObjects( input: Record, - arraysToArrays: string[] + arraysToArrays: string[], ): Record[] { /* * Transforms object with arrays into array of objects @@ -24,7 +24,7 @@ export function objectWithArraysToArrayOfObjects( acc.keys.push(key); return acc; }, - { keys: [] as string[], maxLength: 0 } + { keys: [] as string[], maxLength: 0 }, ); const output: Record[] = []; @@ -44,7 +44,7 @@ export function objectWithArraysToArrayOfObjects( export function arrayOfObjectsToObjectWithArrays>( array: T[], - mergeObject: Record = {} + mergeObject: Record = {}, ): Record { /* * Transforms array of objects into object with arrays @@ -54,21 +54,24 @@ export function arrayOfObjectsToObjectWithArrays>( * @returns {object} Merged object with arrays * */ - return array.reduce((acc, obj) => { - const mergedObj = { ...obj, ...mergeObject }; - Object.keys(mergedObj).forEach((key) => { - if (!acc[key]) { - acc[key] = []; - } - acc[key].push(mergedObj[key]); - }); - return acc; - }, {} as Record); + return array.reduce( + (acc, obj) => { + const mergedObj = { ...obj, ...mergeObject }; + Object.keys(mergedObj).forEach((key) => { + if (!acc[key]) { + acc[key] = []; + } + acc[key].push(mergedObj[key]); + }); + return acc; + }, + {} as Record, + ); } export function filterObject( obj: Record, - filter: string[] + filter: string[], ): Record { /* * Filter certain keys from object @@ -79,6 +82,6 @@ export function filterObject( * */ return Object.fromEntries( - Object.entries(obj).filter(([key]) => filter.includes(key)) + Object.entries(obj).filter(([key]) => filter.includes(key)), ); } diff --git a/src/WindowBehavior.ts b/src/WindowBehavior.ts index b5e5a1d..1282738 100644 --- a/src/WindowBehavior.ts +++ b/src/WindowBehavior.ts @@ -32,13 +32,13 @@ export const saveBeforeExit = (app: Editor): null => { export const installWindowBehaviors = ( app: Editor, window: Window, - preventMultipleTabs: boolean = false + preventMultipleTabs: boolean = false, ) => { window.addEventListener("resize", () => - handleResize(app.interface.scope as HTMLCanvasElement) + handleResize(app.interface.scope as HTMLCanvasElement), ); window.addEventListener("resize", () => - handleResize(app.interface.feedback as HTMLCanvasElement) + handleResize(app.interface.feedback as HTMLCanvasElement), ); window.addEventListener("beforeunload", (event) => { event.preventDefault(); @@ -61,11 +61,11 @@ export const installWindowBehaviors = ( if (e.key == "page_available") { document.getElementById("all")!.classList.add("invisible"); alert( - "Topos is already opened in another tab. Close this tab now to prevent data loss." + "Topos is already opened in another tab. Close this tab now to prevent data loss.", ); } }, - false + false, ); } }; diff --git a/src/classes/SoundEvent.ts b/src/classes/SoundEvent.ts index cb65f6d..7aef672 100644 --- a/src/classes/SoundEvent.ts +++ b/src/classes/SoundEvent.ts @@ -38,7 +38,7 @@ export class SoundEvent extends AudibleEvent { public updateValue( key: string, - value: T | T[] | SoundParams[] | null + value: T | T[] | SoundParams[] | null, ): this { if (value == null) return this; this.values[key] = value; @@ -82,7 +82,7 @@ export class SoundEvent extends AudibleEvent { a: number, d: number, s: number, - r: number + r: number, ) { self.updateValue("fmattack", a); self.updateValue("fmdecay", d); @@ -106,7 +106,7 @@ export class SoundEvent extends AudibleEvent { a: number, d: number, s: number, - r: number + r: number, ) { self.updateValue("attack", a); self.updateValue("decay", d); @@ -152,7 +152,7 @@ export class SoundEvent extends AudibleEvent { a: number, d: number, s: number, - r: number + r: number, ) { self.updateValue("lpenv", depth); self.updateValue("lpattack", a); @@ -198,7 +198,7 @@ export class SoundEvent extends AudibleEvent { a: number, d: number, s: number, - r: number + r: number, ) { self.updateValue("hpenv", depth); self.updateValue("hpattack", a); @@ -241,7 +241,7 @@ export class SoundEvent extends AudibleEvent { a: number, d: number, s: number, - r: number + r: number, ) { self.updateValue("bpenv", depth); self.updateValue("bpattack", a); @@ -336,7 +336,10 @@ export class SoundEvent extends AudibleEvent { }, }; - constructor(sound: string | string[] | SoundParams, public app: Editor) { + constructor( + sound: string | string[] | SoundParams, + public app: Editor, + ) { super(app); this.nudge = app.dough_nudge / 100; @@ -367,7 +370,7 @@ export class SoundEvent extends AudibleEvent { } private processSound = ( - sound: string | string[] | SoundParams | SoundParams[] + sound: string | string[] | SoundParams | SoundParams[], ): SoundParams => { if (Array.isArray(sound) && typeof sound[0] === "string") { const s: string[] = []; @@ -438,7 +441,7 @@ export class SoundEvent extends AudibleEvent { (event.key as number) || "C4", (event.pitch as number) || 0, (event.parsedScale as number[]) || event.scale || "MAJOR", - (event.octave as number) || 0 + (event.octave as number) || 0, ); event.note = note; event.freq = midiToFreq(note); @@ -458,7 +461,7 @@ export class SoundEvent extends AudibleEvent { public invert = (howMany: number = 0) => { if (this.values.chord) { let notes = this.values.chord.map( - (obj: { [key: string]: number }) => obj.note + (obj: { [key: string]: number }) => obj.note, ); notes = howMany < 0 ? [...notes].reverse() : notes; for (let i = 0; i < Math.abs(howMany); i++) { @@ -500,7 +503,7 @@ export class SoundEvent extends AudibleEvent { superdough( filteredEvent, this.nudge - this.app.clock.deviation, - filteredEvent.dur + filteredEvent.dur, ); } }; diff --git a/src/classes/ZPlayer.ts b/src/classes/ZPlayer.ts index c4f13fa..d945fde 100644 --- a/src/classes/ZPlayer.ts +++ b/src/classes/ZPlayer.ts @@ -29,7 +29,7 @@ export class Player extends AbstractEvent { input: string | number | Generator, options: InputOptions, public app: Editor, - zid: string = "" + zid: string = "", ) { super(app); this.options = options; @@ -159,7 +159,7 @@ export class Player extends AbstractEvent { if (this.areWeThereYet()) { const event = this.next() as Pitch | Chord | ZRest; const noteLengthInSeconds = this.app.clock.convertPulseToSecond( - event.duration * 4 * this.app.clock.ppqn + event.duration * 4 * this.app.clock.ppqn, ); if (event instanceof Pitch) { const obj = event.getExisting( @@ -169,7 +169,7 @@ export class Player extends AbstractEvent { "key", "scale", "octave", - "parsedScale" + "parsedScale", ) as SoundParams; if (event.sound) name = event.sound as string; if (event.soundIndex) obj.n = event.soundIndex as number; @@ -184,14 +184,14 @@ export class Player extends AbstractEvent { "key", "scale", "octave", - "parsedScale" + "parsedScale", ); }) as SoundParams[]; const add = { dur: noteLengthInSeconds } as SoundParams; if (name) add.s = name; let sound = arrayOfObjectsToObjectWithArrays( pitches, - add + add, ) as SoundParams; return new SoundEvent(sound, this.app); } else if (event instanceof ZRest) { @@ -212,7 +212,7 @@ export class Player extends AbstractEvent { "key", "scale", "octave", - "parsedScale" + "parsedScale", ) as MidiParams; if (event instanceof Pitch) { if (event.soundIndex) obj.channel = event.soundIndex as number; diff --git a/src/documentation/more/bonus.ts b/src/documentation/more/bonus.ts index 1138c04..a262288 100644 --- a/src/documentation/more/bonus.ts +++ b/src/documentation/more/bonus.ts @@ -20,11 +20,11 @@ Some features have been included as a bonus. These features are often about patt ${makeExample( "Hydra integration", `beat(4) :: hydra.osc(3, 0.5, 2).out()`, - true + true, )} Close the documentation to see the effect: ${key_shortcut( - "Ctrl+D" + "Ctrl+D", )}! **Boom, all shiny!** Be careful not to call hydra too often as it can impact performances. You can use any rhythmical function like beat() function to limit the number of function calls. You can write any Topos code like [1,2,3].beat() to bring some life and movement in your Hydra sketches. @@ -37,7 +37,7 @@ ${makeExample( beat(4) :: stop_hydra() // this one beat(4) :: hydra.hush() // or this one `, - true + true, )} @@ -48,7 +48,7 @@ You can change Hydra resolution using this simple method: ${makeExample( "Changing Hydra resolution", `hydra.setResolution(1024, 768)`, - true + true, )} ### Documentation @@ -82,7 +82,7 @@ beat(0.25)::gif({ posX: ir(1,1200), // CSS Horizontal Position posY: ir(1, 800), // CSS Vertical Position `, - true + true, )} `; }; diff --git a/src/documentation/samples/sample_list.ts b/src/documentation/samples/sample_list.ts index 49b1c85..b8ba417 100644 --- a/src/documentation/samples/sample_list.ts +++ b/src/documentation/samples/sample_list.ts @@ -65,12 +65,12 @@ On this page, you will find an exhaustive list of all the samples currently load A very large collection of wavetables for wavetable synthesis. This collection has been released by Kristoffer Ekstrand: [AKWF Waveforms](https://www.adventurekid.se/akrt/waveforms/adventure-kid-waveforms/). Every sound sample that starts with wt_ will be looped. Look at this demo: ${makeExample( - "Wavetable synthesis made easy :)", - ` + "Wavetable synthesis made easy :)", + ` beat(0.5)::sound('wt_stereo').n([0, 1].pick()).ad(0, .25).out() `, - true, - )} + true, +)} Pick one folder and spend some time exploring it. There is a lot of different waveforms. @@ -84,12 +84,12 @@ ${samples_to_markdown(application, "Waveforms")} A set of 72 classic drum machines created by **Geikha**: [Geikha Drum Machines](https://github.com/geikha/tidal-drum-machines). To use them efficiently, it is best to use the .bank() parameter like so: ${makeExample( - "Using a classic drum machine", - ` + "Using a classic drum machine", + ` beat(0.5)::sound(['bd', 'cp'].pick()).bank("AkaiLinn").out() `, - true, - )} + true, +)} Here is the complete list of available machines: @@ -119,12 +119,12 @@ ${samples_to_markdown(application, "Amiga")} A collection of many different amen breaks. Use .stretch() to play with these: ${makeExample( - "Stretching an amen break", - ` + "Stretching an amen break", + ` beat(4)::sound('amen1').stretch(4).out() `, - true, - )} + true, +)} The stretch should be adapted based on the length of each amen break. diff --git a/src/main.ts b/src/main.ts index 26990cc..a8d8677 100644 --- a/src/main.ts +++ b/src/main.ts @@ -246,7 +246,7 @@ export class Editor { * If any required elements or templates are missing, warning messages are logged and the function returns early. */ let itemTemplate = document.getElementById( - "ui-known-universe-item-template" + "ui-known-universe-item-template", ) as HTMLTemplateElement; if (!itemTemplate) { console.warn("Missing template #ui-known-universe-item-template"); @@ -274,10 +274,10 @@ export class Editor { item .querySelector(".delete-universe") ?.addEventListener("click", () => - api._deleteUniverseFromInterface(it) + api._deleteUniverseFromInterface(it), ); return item; - }) + }), ); existing_universes.innerHTML = ""; @@ -369,7 +369,7 @@ export class Editor { this.view.dispatch({ effects: this.chosenLanguage.reconfigure( - this.editor_mode == "notes" ? [markdown()] : [javascript()] + this.editor_mode == "notes" ? [markdown()] : [javascript()], ), }); @@ -378,7 +378,7 @@ export class Editor { setButtonHighlighting( button: "play" | "pause" | "stop" | "clear", - highlight: boolean + highlight: boolean, ) { /** * Sets the highlighting for a specific button. @@ -432,7 +432,7 @@ export class Editor { // All other buttons must lose the highlighting document .querySelectorAll( - possible_selectors.filter((_, index) => index != selector).join(",") + possible_selectors.filter((_, index) => index != selector).join(","), ) .forEach((button) => { button.children[0].classList.remove("animate-pulse"); @@ -478,28 +478,28 @@ export class Editor { */ const domElement = this.view.dom; const gutters = domElement.getElementsByClassName( - "cm-gutter" + "cm-gutter", ) as HTMLCollectionOf; domElement.classList.add("fluid-bg-transition"); Array.from(gutters).forEach((gutter) => - gutter.classList.add("fluid-bg-transition") + gutter.classList.add("fluid-bg-transition"), ); domElement.style.backgroundColor = color; Array.from(gutters).forEach( - (gutter) => (gutter.style.backgroundColor = color) + (gutter) => (gutter.style.backgroundColor = color), ); setTimeout(() => { domElement.style.backgroundColor = ""; Array.from(gutters).forEach( - (gutter) => (gutter.style.backgroundColor = "") + (gutter) => (gutter.style.backgroundColor = ""), ); domElement.classList.remove("fluid-bg-transition"); Array.from(gutters).forEach((gutter) => - gutter.classList.remove("fluid-bg-transition") + gutter.classList.remove("fluid-bg-transition"), ); }, duration); } @@ -507,7 +507,7 @@ export class Editor { private initializeElements(): void { for (const [key, value] of Object.entries(singleElements)) { this.interface[key] = document.getElementById( - value + value, ) as ElementMap[keyof ElementMap]; } } @@ -515,7 +515,7 @@ export class Editor { private initializeButtonGroups(): void { for (const [key, ids] of Object.entries(buttonGroups)) { this.buttonElements[key] = ids.map( - (id) => document.getElementById(id) as HTMLButtonElement + (id) => document.getElementById(id) as HTMLButtonElement, ); } } diff --git a/tsconfig.json b/tsconfig.json index 1ad475b..641e63e 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -15,7 +15,7 @@ /* Linting */ "strict": true, - "forceConsistentCasingInFileNames": true, + "forceConsistentCasingInFileNames": true, "useDefineForClassFields": true, "noUnusedLocals": true, "noUnusedParameters": true, From 278dce0196f74244192e9bf31585c584386b0876 Mon Sep 17 00:00:00 2001 From: Miika Alonen Date: Tue, 28 Nov 2023 07:50:03 +0200 Subject: [PATCH 16/34] Fix chord & note issues --- src/classes/AbstractEvents.ts | 46 +++++++++++++++++++++++++++++- src/classes/MidiEvent.ts | 12 +------- src/classes/SoundEvent.ts | 43 ---------------------------- src/extensions/NumberExtensions.ts | 7 +++-- 4 files changed, 50 insertions(+), 58 deletions(-) diff --git a/src/classes/AbstractEvents.ts b/src/classes/AbstractEvents.ts index 1be343d..72aa7e6 100644 --- a/src/classes/AbstractEvents.ts +++ b/src/classes/AbstractEvents.ts @@ -1,5 +1,6 @@ import { type Editor } from "../main"; -import { freqToMidi, resolvePitchBend, safeScale } from "zifferjs"; +import { freqToMidi, chord as parseChord, noteNameToMidi, resolvePitchBend, safeScale } from "zifferjs"; +import { SkipEvent } from "./SkipEvent"; export type EventOperation = (instance: T, ...args: any[]) => void; @@ -310,6 +311,49 @@ export abstract class AudibleEvent extends AbstractEvent { return this; }; + protected updateValue( + key: string, + value: T | T[] | null + ): this { + if (value == null) return this; + this.values[key] = value; + return this; + } + + public note = (value: number | string | null, ...kwargs: number[]|string[]) => { + if (typeof value === "string") { + const parsedNote = noteNameToMidi(value); + return this.updateValue("note", [parsedNote, ...kwargs].flat(Infinity)); + } else if (typeof value == null || value == undefined) { + return new SkipEvent(); + } else { + return this.updateValue("note", [value, ...kwargs].flat(Infinity)); + } + }; + + public chord = (value: number|string, ...kwargs: number[]) => { + if(typeof value === "string") { + const chord = parseChord(value); + return this.updateValue("note", chord); + } else { + const chord = [value, ...kwargs].flat(Infinity); + return this.updateValue("note", chord); + } + }; + + public invert = (howMany: number = 0) => { + if (this.values.note) { + let notes = [...this.values.note]; + notes = howMany < 0 ? [...notes].reverse() : notes; + for (let i = 0; i < Math.abs(howMany); i++) { + notes[i % notes.length] += howMany <= 0 ? -12 : 12; + } + return this.updateValue("note", notes); + } else { + return this; + } + }; + freq = (value: number | number[], ...kwargs: number[]): this => { /* * This function is used to set the frequency of the Event. diff --git a/src/classes/MidiEvent.ts b/src/classes/MidiEvent.ts index e0076f6..df8fa37 100644 --- a/src/classes/MidiEvent.ts +++ b/src/classes/MidiEvent.ts @@ -1,7 +1,7 @@ import { AudibleEvent } from "./AbstractEvents"; import { type Editor } from "../main"; import { MidiConnection } from "../IO/MidiConnection"; -import { noteFromPc, chord as parseChord } from "zifferjs"; +import { noteFromPc } from "zifferjs"; import { filterObject, arrayOfObjectsToObjectWithArrays, @@ -29,16 +29,6 @@ export class MidiEvent extends AudibleEvent { this.midiConnection = app.api.MidiConnection; } - public chord = (value: string) => { - this.values.note = parseChord(value); - return this; - }; - - note = (value: number | number[]): this => { - this.values["note"] = value; - return this; - }; - sustain = (value: number | number[]): this => { this.values["sustain"] = value; return this; diff --git a/src/classes/SoundEvent.ts b/src/classes/SoundEvent.ts index 7aef672..3051f25 100644 --- a/src/classes/SoundEvent.ts +++ b/src/classes/SoundEvent.ts @@ -6,10 +6,8 @@ import { objectWithArraysToArrayOfObjects, } from "../Utils/Generic"; import { - chord as parseChord, midiToFreq, noteFromPc, - noteNameToMidi, } from "zifferjs"; import { @@ -36,15 +34,6 @@ export class SoundEvent extends AudibleEvent { nudge: number; sound: any; - public updateValue( - key: string, - value: T | T[] | SoundParams[] | null, - ): this { - if (value == null) return this; - this.values[key] = value; - return this; - } - private static methodMap = { volume: ["volume", "vol"], zrand: ["zrand", "zr"], @@ -453,38 +442,6 @@ export class SoundEvent extends AudibleEvent { this.values.freq = newArrays.freq; }; - public chord = (value: string) => { - const chord = parseChord(value); - return this.updateValue("note", chord); - }; - - public invert = (howMany: number = 0) => { - if (this.values.chord) { - let notes = this.values.chord.map( - (obj: { [key: string]: number }) => obj.note, - ); - notes = howMany < 0 ? [...notes].reverse() : notes; - for (let i = 0; i < Math.abs(howMany); i++) { - notes[i % notes.length] += howMany <= 0 ? -12 : 12; - } - const chord = notes.map((note: number) => { - return { note: note, freq: midiToFreq(note) }; - }); - return this.updateValue("chord", chord); - } else { - return this; - } - }; - public note = (value: number | string | null) => { - if (typeof value === "string") { - return this.updateValue("note", noteNameToMidi(value)); - } else if (typeof value == null || value == undefined) { - return this.updateValue("note", 0).updateValue("gain", 0); - } else { - return this.updateValue("note", value); - } - }; - out = (orbit?: number | number[]): void => { if (orbit) this.values["orbit"] = orbit; const events = objectWithArraysToArrayOfObjects(this.values, [ diff --git a/src/extensions/NumberExtensions.ts b/src/extensions/NumberExtensions.ts index 76fdc9e..84a781b 100644 --- a/src/extensions/NumberExtensions.ts +++ b/src/extensions/NumberExtensions.ts @@ -2,6 +2,7 @@ import { type UserAPI } from "../API"; import { MidiEvent } from "../classes/MidiEvent"; import { Player } from "../classes/ZPlayer"; import { SoundEvent } from "../classes/SoundEvent"; +import { SkipEvent } from "../classes/SkipEvent"; declare global { interface Number { @@ -24,7 +25,7 @@ declare global { z15(): Player; z16(): Player; midi(): MidiEvent; - sound(name: string): SoundEvent; + sound(name: string): SoundEvent|SkipEvent; } } @@ -101,11 +102,11 @@ export const makeNumberExtensions = (api: UserAPI) => { return api.midi(this.valueOf(), ...kwargs); }; - Number.prototype.sound = function (name: string) { + Number.prototype.sound = function (name: string): SoundEvent|SkipEvent { if (Number.isInteger(this.valueOf())) { return (api.sound(name) as SoundEvent).note(this.valueOf()); } else { return (api.sound(name) as SoundEvent).freq(this.valueOf()); - } + } }; }; From ee6dbf9e291d1d7e8ca238a4494f576a71725e28 Mon Sep 17 00:00:00 2001 From: Raphael Forment Date: Thu, 30 Nov 2023 15:01:19 +0100 Subject: [PATCH 17/34] clean audiovisualisation file --- src/API.ts | 3 +- src/Visuals/Blinkers.ts | 118 +++++++++++++++++ .../Oscilloscope.ts} | 119 +----------------- src/main.ts | 7 +- 4 files changed, 123 insertions(+), 124 deletions(-) create mode 100644 src/Visuals/Blinkers.ts rename src/{AudioVisualisation.ts => Visuals/Oscilloscope.ts} (68%) diff --git a/src/API.ts b/src/API.ts index 3269f6d..a4962d8 100644 --- a/src/API.ts +++ b/src/API.ts @@ -27,7 +27,8 @@ import { } from "superdough"; import { Speaker } from "./extensions/StringExtensions"; import { getScaleNotes } from "zifferjs"; -import { OscilloscopeConfig, blinkScript } from "./AudioVisualisation"; +import { OscilloscopeConfig } from "./Visuals/Oscilloscope"; +import { blinkScript } from "./Visuals/Blinkers"; import { SkipEvent } from "./classes/SkipEvent"; import { AbstractEvent, EventOperation } from "./classes/AbstractEvents"; import drums from "./tidal-drum-machines.json"; diff --git a/src/Visuals/Blinkers.ts b/src/Visuals/Blinkers.ts new file mode 100644 index 0000000..d14d765 --- /dev/null +++ b/src/Visuals/Blinkers.ts @@ -0,0 +1,118 @@ +import { type Editor } from "../main"; + +export const drawCircle = ( + /** + * Draw a circle at a specific position on the canvas. + * @param {number} x - The x-coordinate of the circle's center. + * @param {number} y - The y-coordinate of the circle's center. + * @param {number} radius - The radius of the circle. + * @param {string} color - The fill color of the circle. + */ + app: Editor, + x: number, + y: number, + radius: number, + color: string, +): void => { + // @ts-ignore + const canvas: HTMLCanvasElement = app.interface.feedback; + const ctx = canvas.getContext("2d"); + if (!ctx) return; + + ctx.beginPath(); + ctx.arc(x, y, radius, 0, Math.PI * 2); + ctx.fillStyle = color; + ctx.fill(); + ctx.closePath(); +}; + +export const blinkScript = ( + /** + * Blinks a script indicator circle. + * @param script - The type of script. + * @param no - The shift amount multiplier. + */ + app: Editor, + script: "local" | "global" | "init", + no?: number, +) => { + if (no !== undefined && no < 1 && no > 9) return; + const blinkDuration = + (app.clock.bpm / 60 / app.clock.time_signature[1]) * 200; + // @ts-ignore + const ctx = app.interface.feedback.getContext("2d"); // Assuming a canvas context + + /** + * Draws a circle at a given shift. + * @param shift - The pixel distance from the origin. + */ + const _drawBlinker = (shift: number) => { + const horizontalOffset = 50; + drawCircle( + app, + horizontalOffset + shift, + app.interface.feedback.clientHeight - 15, + 8, + "#fdba74", + ); + }; + + const _clearBlinker = (shift: number) => { + /** + * Clears the circle at a given shift. + * @param shift - The pixel distance from the origin. + */ + const x = 50 + shift; + const y = app.interface.feedback.clientHeight - 15; + const radius = 8; + ctx.clearRect(x - radius, y - radius, radius * 2, radius * 2); + }; + + if (script === "local" && no !== undefined) { + const shiftAmount = no * 25; + + // Clear existing timeout if any + if (app.blinkTimeouts[shiftAmount]) { + clearTimeout(app.blinkTimeouts[shiftAmount]); + } + + _drawBlinker(shiftAmount); + + // Save timeout ID for later clearing + // @ts-ignore + app.blinkTimeouts[shiftAmount] = setTimeout(() => { + _clearBlinker(shiftAmount); + // Clear the canvas before drawing new blinkers + (app.interface.feedback as HTMLCanvasElement) + .getContext("2d")! + .clearRect( + 0, + 0, + (app.interface.feedback as HTMLCanvasElement).width, + (app.interface.feedback as HTMLCanvasElement).height, + ); + }, blinkDuration); + } +}; + +export const scriptBlinkers = () => { + /** + * Manages animation updates using requestAnimationFrame. + * @param app - The Editor application context. + */ + + let lastFrameTime = Date.now(); + const frameRate = 10; + const minFrameDelay = 1000 / frameRate; + + const update = () => { + const now = Date.now(); + const timeSinceLastFrame = now - lastFrameTime; + + if (timeSinceLastFrame >= minFrameDelay) { + lastFrameTime = now; + } + requestAnimationFrame(update); + }; + requestAnimationFrame(update); +}; diff --git a/src/AudioVisualisation.ts b/src/Visuals/Oscilloscope.ts similarity index 68% rename from src/AudioVisualisation.ts rename to src/Visuals/Oscilloscope.ts index 07a9c8e..18309c9 100644 --- a/src/AudioVisualisation.ts +++ b/src/Visuals/Oscilloscope.ts @@ -1,123 +1,6 @@ // @ts-ignore import { getAnalyser } from "superdough"; -import { type Editor } from "./main"; - -export const drawCircle = ( - /** - * Draw a circle at a specific position on the canvas. - * @param {number} x - The x-coordinate of the circle's center. - * @param {number} y - The y-coordinate of the circle's center. - * @param {number} radius - The radius of the circle. - * @param {string} color - The fill color of the circle. - */ - app: Editor, - x: number, - y: number, - radius: number, - color: string, -): void => { - // @ts-ignore - const canvas: HTMLCanvasElement = app.interface.feedback; - const ctx = canvas.getContext("2d"); - if (!ctx) return; - - ctx.beginPath(); - ctx.arc(x, y, radius, 0, Math.PI * 2); - ctx.fillStyle = color; - ctx.fill(); - ctx.closePath(); -}; - -export const blinkScript = ( - /** - * Blinks a script indicator circle. - * @param script - The type of script. - * @param no - The shift amount multiplier. - */ - app: Editor, - script: "local" | "global" | "init", - no?: number, -) => { - if (no !== undefined && no < 1 && no > 9) return; - const blinkDuration = - (app.clock.bpm / 60 / app.clock.time_signature[1]) * 200; - // @ts-ignore - const ctx = app.interface.feedback.getContext("2d"); // Assuming a canvas context - - /** - * Draws a circle at a given shift. - * @param shift - The pixel distance from the origin. - */ - const _drawBlinker = (shift: number) => { - const horizontalOffset = 50; - drawCircle( - app, - horizontalOffset + shift, - app.interface.feedback.clientHeight - 15, - 8, - "#fdba74", - ); - }; - - const _clearBlinker = (shift: number) => { - /** - * Clears the circle at a given shift. - * @param shift - The pixel distance from the origin. - */ - const x = 50 + shift; - const y = app.interface.feedback.clientHeight - 15; - const radius = 8; - ctx.clearRect(x - radius, y - radius, radius * 2, radius * 2); - }; - - if (script === "local" && no !== undefined) { - const shiftAmount = no * 25; - - // Clear existing timeout if any - if (app.blinkTimeouts[shiftAmount]) { - clearTimeout(app.blinkTimeouts[shiftAmount]); - } - - _drawBlinker(shiftAmount); - - // Save timeout ID for later clearing - // @ts-ignore - app.blinkTimeouts[shiftAmount] = setTimeout(() => { - _clearBlinker(shiftAmount); - // Clear the canvas before drawing new blinkers - (app.interface.feedback as HTMLCanvasElement) - .getContext("2d")! - .clearRect( - 0, - 0, - (app.interface.feedback as HTMLCanvasElement).width, - (app.interface.feedback as HTMLCanvasElement).height, - ); - }, blinkDuration); - } -}; - -export const scriptBlinkers = () => { - /** - * Manages animation updates using requestAnimationFrame. - * @param app - The Editor application context. - */ - - let lastFrameTime = Date.now(); - const frameRate = 10; - const minFrameDelay = 1000 / frameRate; - - const update = () => { - const now = Date.now(); - const timeSinceLastFrame = now - lastFrameTime; - - if (timeSinceLastFrame >= minFrameDelay) { - lastFrameTime = now; - } - requestAnimationFrame(update); - }; - requestAnimationFrame(update); -}; +import { Editor } from "../main"; export interface OscilloscopeConfig { enabled: boolean; diff --git a/src/main.ts b/src/main.ts index a8d8677..4b5569a 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,9 +1,6 @@ -import { - OscilloscopeConfig, - runOscilloscope, - scriptBlinkers, -} from "./AudioVisualisation"; +import { OscilloscopeConfig, runOscilloscope } from "./Visuals/Oscilloscope"; import { EditorState, Compartment } from "@codemirror/state"; +import { scriptBlinkers } from "./Visuals/Blinkers"; import { javascript } from "@codemirror/lang-javascript"; import { markdown } from "@codemirror/lang-markdown"; import { Extension } from "@codemirror/state"; From 50ace56de8fa254fd6f06d4ee72daea3fe289c3f Mon Sep 17 00:00:00 2001 From: Raphael Forment Date: Thu, 30 Nov 2023 22:31:29 +0100 Subject: [PATCH 18/34] debug function --- src/classes/SoundEvent.ts | 63 ++++++++++++++++++++++----------------- 1 file changed, 35 insertions(+), 28 deletions(-) diff --git a/src/classes/SoundEvent.ts b/src/classes/SoundEvent.ts index 3051f25..f65eb06 100644 --- a/src/classes/SoundEvent.ts +++ b/src/classes/SoundEvent.ts @@ -66,7 +66,7 @@ export class SoundEvent extends AudibleEvent { phaserDepth: ["phaserDepth", "phasdepth"], phaserSweep: ["phaserSweep", "phassweep"], phaserCenter: ["phaserCenter", "phascenter"], - fmadsr: function ( + fmadsr: function( self: SoundEvent, a: number, d: number, @@ -79,7 +79,7 @@ export class SoundEvent extends AudibleEvent { self.updateValue("fmrelease", r); return self; }, - fmad: function (self: SoundEvent, a: number, d: number) { + fmad: function(self: SoundEvent, a: number, d: number) { self.updateValue("fmattack", a); self.updateValue("fmdecay", d); return self; @@ -90,7 +90,7 @@ export class SoundEvent extends AudibleEvent { decay: ["decay", "dec"], sustain: ["sustain", "sus"], release: ["release", "rel"], - adsr: function ( + adsr: function( self: SoundEvent, a: number, d: number, @@ -103,39 +103,43 @@ export class SoundEvent extends AudibleEvent { self.updateValue("release", r); return self; }, - ad: function (self: SoundEvent, a: number, d: number) { + ad: function(self: SoundEvent, a: number, d: number) { self.updateValue("attack", a); self.updateValue("decay", d); self.updateValue("sustain", 0.0); self.updateValue("release", 0.0); return self; }, + debug: function(self: SoundEvent) { + self.updateValue("debug", true) + return self; + }, lpenv: ["lpenv", "lpe"], lpattack: ["lpattack", "lpa"], lpdecay: ["lpdecay", "lpd"], lpsustain: ["lpsustain", "lps"], lprelease: ["lprelease", "lpr"], - cutoff: function (self: SoundEvent, value: number, resonance?: number) { + cutoff: function(self: SoundEvent, value: number, resonance?: number) { self.updateValue("cutoff", value); if (resonance) { self.updateValue("resonance", resonance); } return self; }, - lpf: function (self: SoundEvent, value: number, resonance?: number) { + lpf: function(self: SoundEvent, value: number, resonance?: number) { self.updateValue("cutoff", value); if (resonance) { self.updateValue("resonance", resonance); } return self; }, - resonance: function (self: SoundEvent, value: number) { + resonance: function(self: SoundEvent, value: number) { if (value >= 0 && value <= 1) { self.updateValue("resonance", 50 * value); } return self; }, - lpadsr: function ( + lpadsr: function( self: SoundEvent, depth: number, a: number, @@ -150,7 +154,7 @@ export class SoundEvent extends AudibleEvent { self.updateValue("lprelease", r); return self; }, - lpad: function (self: SoundEvent, depth: number, a: number, d: number) { + lpad: function(self: SoundEvent, depth: number, a: number, d: number) { self.updateValue("lpenv", depth); self.updateValue("lpattack", a); self.updateValue("lpdecay", d); @@ -163,25 +167,25 @@ export class SoundEvent extends AudibleEvent { hpdecay: ["hpdecay", "hpd"], hpsustain: ["hpsustain", "hpsus"], hprelease: ["hprelease", "hpr"], - hcutoff: function (self: SoundEvent, value: number, resonance?: number) { + hcutoff: function(self: SoundEvent, value: number, resonance?: number) { self.updateValue("hcutoff", value); if (resonance) { self.updateValue("hresonance", resonance); } return self; }, - hpf: function (self: SoundEvent, value: number, resonance?: number) { + hpf: function(self: SoundEvent, value: number, resonance?: number) { self.updateValue("hcutoff", value); if (resonance) { self.updateValue("hresonance", resonance); } return self; }, - hpq: function (self: SoundEvent, value: number) { + hpq: function(self: SoundEvent, value: number) { self.updateValue("hresonance", value); return self; }, - hpadsr: function ( + hpadsr: function( self: SoundEvent, depth: number, a: number, @@ -196,7 +200,7 @@ export class SoundEvent extends AudibleEvent { self.updateValue("hprelease", r); return self; }, - hpad: function (self: SoundEvent, depth: number, a: number, d: number) { + hpad: function(self: SoundEvent, depth: number, a: number, d: number) { self.updateValue("hpenv", depth); self.updateValue("hpattack", a); self.updateValue("hpdecay", d); @@ -209,14 +213,14 @@ export class SoundEvent extends AudibleEvent { bpdecay: ["bpdecay", "bpd"], bpsustain: ["bpsustain", "bps"], bprelease: ["bprelease", "bpr"], - bandf: function (self: SoundEvent, value: number, resonance?: number) { + bandf: function(self: SoundEvent, value: number, resonance?: number) { self.updateValue("bandf", value); if (resonance) { self.updateValue("bandq", resonance); } return self; }, - bpf: function (self: SoundEvent, value: number, resonance?: number) { + bpf: function(self: SoundEvent, value: number, resonance?: number) { self.updateValue("bandf", value); if (resonance) { self.updateValue("bandq", resonance); @@ -224,7 +228,7 @@ export class SoundEvent extends AudibleEvent { return self; }, bandq: ["bandq", "bpq"], - bpadsr: function ( + bpadsr: function( self: SoundEvent, depth: number, a: number, @@ -239,7 +243,7 @@ export class SoundEvent extends AudibleEvent { self.updateValue("bprelease", r); return self; }, - bpad: function (self: SoundEvent, depth: number, a: number, d: number) { + bpad: function(self: SoundEvent, depth: number, a: number, d: number) { self.updateValue("bpenv", depth); self.updateValue("bpattack", a); self.updateValue("bpdecay", d); @@ -249,7 +253,7 @@ export class SoundEvent extends AudibleEvent { }, vib: ["vib"], vibmod: ["vibmod"], - fm: function (self: SoundEvent, value: number | string) { + fm: function(self: SoundEvent, value: number | string) { if (typeof value === "number") { self.values["fmi"] = value; } else { @@ -265,11 +269,11 @@ export class SoundEvent extends AudibleEvent { begin: ["begin"], end: ["end"], gain: ["gain"], - dbgain: function (self: SoundEvent, value: number) { + dbgain: function(self: SoundEvent, value: number) { self.updateValue("gain", Math.min(Math.pow(10, value / 20), 10)); return self; }, - db: function (self: SoundEvent, value: number) { + db: function(self: SoundEvent, value: number) { self.updateValue("gain", Math.min(Math.pow(10, value / 20), 10)); return self; }, @@ -292,32 +296,32 @@ export class SoundEvent extends AudibleEvent { roomlp: ["roomlp", "rlp"], roomdim: ["roomdim", "rdim"], sound: ["s", "sound"], - size: function (self: SoundEvent, value: number) { + size: function(self: SoundEvent, value: number) { self.updateValue("roomsize", value); return self; }, - sz: function (self: SoundEvent, value: number) { + sz: function(self: SoundEvent, value: number) { self.updateValue("roomsize", value); return self; }, comp: ["compressor", "cmp"], - ratio: function (self: SoundEvent, value: number) { + ratio: function(self: SoundEvent, value: number) { self.updateValue("compressorRatio", value); return self; }, - knee: function (self: SoundEvent, value: number) { + knee: function(self: SoundEvent, value: number) { self.updateValue("compressorKnee", value); return self; }, - compAttack: function (self: SoundEvent, value: number) { + compAttack: function(self: SoundEvent, value: number) { self.updateValue("compressorAttack", value); return self; }, - compRelease: function (self: SoundEvent, value: number) { + compRelease: function(self: SoundEvent, value: number) { self.updateValue("compressorRelease", value); return self; }, - stretch: function (self: SoundEvent, beat: number) { + stretch: function(self: SoundEvent, beat: number) { self.updateValue("unit", "c"); self.updateValue("speed", 1 / beat); self.updateValue("cut", beat); @@ -457,6 +461,9 @@ export class SoundEvent extends AudibleEvent { if (filteredEvent.freq) { delete filteredEvent.note; } + if (this.values["debug"]) { + console.log(filteredEvent) + } superdough( filteredEvent, this.nudge - this.app.clock.deviation, From 49f7998425eec47459df0d0a595ea2960ef3a25c Mon Sep 17 00:00:00 2001 From: Raphael Forment Date: Thu, 30 Nov 2023 22:46:12 +0100 Subject: [PATCH 19/34] debug callback --- src/classes/SoundEvent.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/classes/SoundEvent.ts b/src/classes/SoundEvent.ts index f65eb06..fd71127 100644 --- a/src/classes/SoundEvent.ts +++ b/src/classes/SoundEvent.ts @@ -110,8 +110,11 @@ export class SoundEvent extends AudibleEvent { self.updateValue("release", 0.0); return self; }, - debug: function(self: SoundEvent) { + debug: function(self: SoundEvent, callback?: Function) { self.updateValue("debug", true) + if (callback) { + self.updateValue("debugFunction", callback) + } return self; }, lpenv: ["lpenv", "lpe"], @@ -462,6 +465,9 @@ export class SoundEvent extends AudibleEvent { delete filteredEvent.note; } if (this.values["debug"]) { + if (this.values["debugFunction"]) { + this.values["debugFunction"](filteredEvent) + } console.log(filteredEvent) } superdough( From c192988e70274a232dd3ec55d8b97606f4318156 Mon Sep 17 00:00:00 2001 From: Raphael Forment Date: Thu, 30 Nov 2023 22:54:40 +0100 Subject: [PATCH 20/34] better debug behavior + optional analyze --- src/classes/SoundEvent.ts | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/src/classes/SoundEvent.ts b/src/classes/SoundEvent.ts index fd71127..7916c96 100644 --- a/src/classes/SoundEvent.ts +++ b/src/classes/SoundEvent.ts @@ -110,6 +110,10 @@ export class SoundEvent extends AudibleEvent { self.updateValue("release", 0.0); return self; }, + analyze: function(self: SoundEvent) { + self.updateValue("analyze", true) + return self + }, debug: function(self: SoundEvent, callback?: Function) { self.updateValue("debug", true) if (callback) { @@ -382,12 +386,10 @@ export class SoundEvent extends AudibleEvent { s, n: n.length > 0 ? n : undefined, dur: this.app.clock.convertPulseToSecond(this.app.clock.ppqn), - analyze: true, }; } else if (typeof sound === "object") { const validatedObj: SoundParams = { dur: this.app.clock.convertPulseToSecond(this.app.clock.ppqn), - analyze: true, ...(sound as Partial), }; return validatedObj; @@ -400,10 +402,9 @@ export class SoundEvent extends AudibleEvent { s, n, dur: this.app.clock.convertPulseToSecond(this.app.clock.ppqn), - analyze: true, }; } else { - return { s: sound, dur: 0.5, analyze: true }; + return { s: sound, dur: 0.5 }; } } }; @@ -467,8 +468,9 @@ export class SoundEvent extends AudibleEvent { if (this.values["debug"]) { if (this.values["debugFunction"]) { this.values["debugFunction"](filteredEvent) + } else { + console.log(filteredEvent) } - console.log(filteredEvent) } superdough( filteredEvent, From 53821983e95ad42761f8e9288c83c48a931b2448 Mon Sep 17 00:00:00 2001 From: Raphael Forment Date: Thu, 30 Nov 2023 22:59:22 +0100 Subject: [PATCH 21/34] renaming analyze to scope and documenting --- src/classes/SoundEvent.ts | 2 +- src/documentation/more/oscilloscope.ts | 29 ++++++++++++++++++-------- 2 files changed, 21 insertions(+), 10 deletions(-) diff --git a/src/classes/SoundEvent.ts b/src/classes/SoundEvent.ts index 7916c96..6f5e216 100644 --- a/src/classes/SoundEvent.ts +++ b/src/classes/SoundEvent.ts @@ -110,7 +110,7 @@ export class SoundEvent extends AudibleEvent { self.updateValue("release", 0.0); return self; }, - analyze: function(self: SoundEvent) { + scope: function(self: SoundEvent) { self.updateValue("analyze", true) return self }, diff --git a/src/documentation/more/oscilloscope.ts b/src/documentation/more/oscilloscope.ts index 87e8036..22c2d90 100644 --- a/src/documentation/more/oscilloscope.ts +++ b/src/documentation/more/oscilloscope.ts @@ -5,11 +5,22 @@ export const oscilloscope = (application: Editor): string => { const makeExample = makeExampleFactory(application); return `# Oscilloscope -You can turn on the oscilloscope to generate interesting visuals or to inspect audio. Use the scope() function to turn it on and off. The oscilloscope is off by default. +You can turn on the oscilloscope to generate interesting visuals or to inspect audio. Use the scope() function to turn on/off the oscilloscope and to configure it. The oscilloscope is off by default. + +You need to manually feed the scope with the sounds you want to inspect: ${makeExample( - "Oscilloscope configuration", - ` + "Feeding a sine to the oscilloscope", + ` +beat(1)::sound('sine').freq(200).ad(0, .2).scope().out() +`, true + )} + +Here is a layout of the scope configuration options: + +${makeExample( + "Oscilloscope configuration", + ` scope({ enabled: true, // off by default color: "#fdba74", // any valid CSS color or "random" @@ -23,12 +34,12 @@ scope({ refresh: 1 // refresh rate (in pulses) }) `, - true, -)} + true, + )} ${makeExample( - "Demo with multiple scope mode", - ` + "Demo with multiple scope mode", + ` rhythm(.5, [4,5].dur(4*3, 4*1), 8)::sound('fhardkick').out() beat(0.25)::sound('square').freq([ [250, 250/2, 250/4].pick(), @@ -44,8 +55,8 @@ scope({enabled: true, thickness: 8, color: ['purple', 'green', 'random'].beat(), size: 0.5, fftSize: 2048}) `, - true, -)} + true, + )} Note that these values can be patterned as well! You can transform the oscilloscope into its own light show if you want. The picture is not stable anyway so you won't have much use of it for precision work :) From 2309bcd95c354c590f80b9766fb668e940dba307 Mon Sep 17 00:00:00 2001 From: Raphael Forment Date: Fri, 1 Dec 2023 09:16:35 +0100 Subject: [PATCH 22/34] remove nodeprocessor and import zyklus --- package.json | 1 + src/Clock.ts | 25 +++------------------ src/TransportProcessor.js | 47 --------------------------------------- yarn.lock | 5 +++++ 4 files changed, 9 insertions(+), 69 deletions(-) delete mode 100644 src/TransportProcessor.js diff --git a/package.json b/package.json index b09c494..55cb430 100644 --- a/package.json +++ b/package.json @@ -43,6 +43,7 @@ "unique-names-generator": "^4.7.1", "vite-plugin-markdown": "^2.1.0", "zifferjs": "^0.0.44", + "zyklus": "^0.1.4", "zzfx": "^1.2.0" } } diff --git a/src/Clock.ts b/src/Clock.ts index 2d78671..0b61162 100644 --- a/src/Clock.ts +++ b/src/Clock.ts @@ -1,7 +1,6 @@ -// @ts-ignore -import { TransportNode } from "./TransportNode"; -import TransportProcessor from "./TransportProcessor?worker&url"; import { Editor } from "./main"; +// @ts-ignore +import * as zyklus from "zyklus"; export interface TimePosition { /** @@ -37,7 +36,6 @@ export class Clock { ctx: AudioContext; logicalTime: number; - transportNode: TransportNode | null; private _bpm: number; time_signature: number[]; time_position: TimePosition; @@ -58,23 +56,12 @@ export class Clock { this.tick = 0; this._bpm = 120; this._ppqn = 48; - this.transportNode = null; this.ctx = ctx; this.running = true; this.lastPauseTime = 0; this.lastPlayPressTime = 0; this.totalPauseTime = 0; - ctx.audioWorklet - .addModule(TransportProcessor) - .then((e) => { - this.transportNode = new TransportNode(ctx, {}, this.app); - this.transportNode.connect(ctx.destination); - return e; - }) - .catch((e) => { - console.log("Error loading TransportProcessor.js:", e); - }); - } + } convertTicksToTimeposition(ticks: number): TimePosition { /** @@ -157,12 +144,10 @@ export class Clock { } set nudge(nudge: number) { - this.transportNode?.setNudge(nudge); } set bpm(bpm: number) { if (bpm > 0 && this._bpm !== bpm) { - this.transportNode?.setBPM(bpm); this._bpm = bpm; this.logicalTime = this.realTime; } @@ -183,7 +168,6 @@ export class Clock { set ppqn(ppqn: number) { if (ppqn > 0 && this._ppqn !== ppqn) { this._ppqn = ppqn; - this.transportNode?.setPPQN(ppqn); this.logicalTime = this.realTime; } } @@ -226,7 +210,6 @@ export class Clock { this.app.api.MidiConnection.sendStartMessage(); this.lastPlayPressTime = this.app.audioContext.currentTime; this.totalPauseTime += this.lastPlayPressTime - this.lastPauseTime; - this.transportNode?.start(); } public pause(): void { @@ -236,7 +219,6 @@ export class Clock { * @remark also sends a MIDI message if a port is declared */ this.running = false; - this.transportNode?.pause(); this.app.api.MidiConnection.sendStopMessage(); this.lastPauseTime = this.app.audioContext.currentTime; this.logicalTime = this.realTime; @@ -254,6 +236,5 @@ export class Clock { this.logicalTime = this.realTime; this.time_position = { bar: 0, beat: 0, pulse: 0 }; this.app.api.MidiConnection.sendStopMessage(); - this.transportNode?.stop(); } } diff --git a/src/TransportProcessor.js b/src/TransportProcessor.js deleted file mode 100644 index 20e96ce..0000000 --- a/src/TransportProcessor.js +++ /dev/null @@ -1,47 +0,0 @@ -class TransportProcessor extends AudioWorkletProcessor { - constructor(options) { - super(options); - this.port.addEventListener("message", this.handleMessage); - this.port.start(); - this.nudge = 0; - this.started = false; - this.bpm = 120; - this.ppqn = 48; - this.currentPulsePosition = 0; - } - - handleMessage = (message) => { - if (message.data && message.data.type === "ping") { - this.port.postMessage(message.data); - } else if (message.data.type === "start") { - this.started = true; - } else if (message.data.type === "pause") { - this.started = false; - } else if (message.data.type === "stop") { - this.started = false; - } else if (message.data.type === "bpm") { - this.bpm = message.data.value; - this.currentPulsePosition = currentTime; - } else if (message.data.type === "ppqn") { - this.ppqn = message.data.value; - this.currentPulsePosition = currentTime; - } else if (message.data.type === "nudge") { - this.nudge = message.data.value; - } - }; - - process(inputs, outputs, parameters) { - if (this.started) { - const adjustedCurrentTime = currentTime + this.nudge / 100; - const beatNumber = adjustedCurrentTime / (60 / this.bpm); - const currentPulsePosition = Math.ceil(beatNumber * this.ppqn); - if (currentPulsePosition > this.currentPulsePosition) { - this.currentPulsePosition = currentPulsePosition; - this.port.postMessage({ type: "bang", bpm: this.bpm }); - } - } - return true; - } -} - -registerProcessor("transport", TransportProcessor); diff --git a/yarn.lock b/yarn.lock index 83e8be2..add9134 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3887,6 +3887,11 @@ zifferjs@^0.0.44: resolved "https://registry.yarnpkg.com/zifferjs/-/zifferjs-0.0.44.tgz#c6b425166488ec05e349867e3de2460b74204449" integrity sha512-Q+0affxeUZwl+oJpsa1nb4hqHV6V4VX+pkejCQq/e9+/0H6ooTpcDQ9IDopvrWBnhA8E11k0tbwUee5TJtE8UQ== +zyklus@^0.1.4: + version "0.1.4" + resolved "https://registry.yarnpkg.com/zyklus/-/zyklus-0.1.4.tgz#229b2966fd1126ef72c6004697269118762bdcd5" + integrity sha512-hbv2cyy4nOI7P8nL8b3ki1jswoLzkUzewPgCLDdDfABryDkV5iO8DAbU25OgO5ShRZHLjXJIylwv5PJQPl3Mpw== + zzfx@^1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/zzfx/-/zzfx-1.2.0.tgz#021e5df8e1605f507e2dde15608eba22798b424b" From 5b9a59effe7e019afdbe59e7d5ce119d2239aeb3 Mon Sep 17 00:00:00 2001 From: Raphael Forment Date: Fri, 1 Dec 2023 09:26:03 +0100 Subject: [PATCH 23/34] WIP: Zyklus callback --- src/Clock.ts | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/src/Clock.ts b/src/Clock.ts index 0b61162..05be33c 100644 --- a/src/Clock.ts +++ b/src/Clock.ts @@ -1,6 +1,8 @@ import { Editor } from "./main"; // @ts-ignore -import * as zyklus from "zyklus"; +import { getAudioContext } from "superdough"; +// @ts-ignore +import "zyklus"; export interface TimePosition { /** @@ -21,8 +23,8 @@ export class Clock { * It is also responsible for starting and stopping the Clock TransportNode. * * @param app - The main application instance + * @param clock - The zyklus clock * @param ctx - The current AudioContext used by app - * @param transportNode - The TransportNode helper * @param bpm - The current beats per minute value * @param time_signature - The time signature * @param time_position - The current time position @@ -34,6 +36,7 @@ export class Clock { * @param totalPauseTime - The total time the clock has been paused / stopped */ + clock: any; ctx: AudioContext; logicalTime: number; private _bpm: number; @@ -61,8 +64,17 @@ export class Clock { this.lastPauseTime = 0; this.lastPlayPressTime = 0; this.totalPauseTime = 0; + this.clock = getAudioContext().createClock(this.clockCallback, this.pulse_duration) } + clockCallback = (time: number, duration: number, tick: number) => { + let deadline = time - getAudioContext().currentTime; + this.tick = tick; + + // Implement TransportNode clock callback and update clock info with it + + }; + convertTicksToTimeposition(ticks: number): TimePosition { /** * Converts ticks to a TimePosition object. From bb5dd6b348d826b0766882569204d0f256e537a1 Mon Sep 17 00:00:00 2001 From: Raphael Forment Date: Fri, 1 Dec 2023 10:45:35 +0100 Subject: [PATCH 24/34] first boom boom --- src/Clock.ts | 24 ++++++++++++++++++++++++ src/classes/SoundEvent.ts | 2 +- 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/src/Clock.ts b/src/Clock.ts index 05be33c..10bf0c5 100644 --- a/src/Clock.ts +++ b/src/Clock.ts @@ -1,8 +1,10 @@ import { Editor } from "./main"; +import { tryEvaluate } from "./Evaluator"; // @ts-ignore import { getAudioContext } from "superdough"; // @ts-ignore import "zyklus"; +const zeroPad = (num: number, places: number) => String(num).padStart(places, "0"); export interface TimePosition { /** @@ -48,6 +50,7 @@ export class Clock { lastPauseTime: number; lastPlayPressTime: number; totalPauseTime: number; + timeviewer: HTMLElement; constructor( public app: Editor, @@ -64,12 +67,32 @@ export class Clock { this.lastPauseTime = 0; this.lastPlayPressTime = 0; this.totalPauseTime = 0; + this.timeviewer = document.getElementById("timeviewer")!; this.clock = getAudioContext().createClock(this.clockCallback, this.pulse_duration) } clockCallback = (time: number, duration: number, tick: number) => { let deadline = time - getAudioContext().currentTime; this.tick = tick; + if (this.app.clock.running) { + if (this.app.settings.send_clock) { + this.app.api.MidiConnection.sendMidiClock(); + } + const futureTimeStamp = this.app.clock.convertTicksToTimeposition( + this.app.clock.tick, + ); + this.app.clock.time_position = futureTimeStamp; + if (futureTimeStamp.pulse % this.app.clock.ppqn == 0) { + this.timeviewer.innerHTML = `${zeroPad(futureTimeStamp.bar, 2)}:${ + futureTimeStamp.beat + 1 + } / ${this.app.clock.bpm}`; + } + if (this.app.exampleIsPlaying) { + tryEvaluate(this.app, this.app.example_buffer); + } else { + tryEvaluate(this.app, this.app.global_buffer); + } + } // Implement TransportNode clock callback and update clock info with it @@ -222,6 +245,7 @@ export class Clock { this.app.api.MidiConnection.sendStartMessage(); this.lastPlayPressTime = this.app.audioContext.currentTime; this.totalPauseTime += this.lastPlayPressTime - this.lastPauseTime; + this.clock.start() } public pause(): void { diff --git a/src/classes/SoundEvent.ts b/src/classes/SoundEvent.ts index 6f5e216..b0af7ec 100644 --- a/src/classes/SoundEvent.ts +++ b/src/classes/SoundEvent.ts @@ -474,7 +474,7 @@ export class SoundEvent extends AudibleEvent { } superdough( filteredEvent, - this.nudge - this.app.clock.deviation, + this.nudge, filteredEvent.dur, ); } From a905d9b2df202877a1f36da5abbee0517e1904f2 Mon Sep 17 00:00:00 2001 From: Raphael Forment Date: Fri, 1 Dec 2023 10:49:29 +0100 Subject: [PATCH 25/34] connect deadline to output --- src/Clock.ts | 3 +++ src/classes/SoundEvent.ts | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/src/Clock.ts b/src/Clock.ts index 10bf0c5..1d93f92 100644 --- a/src/Clock.ts +++ b/src/Clock.ts @@ -51,6 +51,7 @@ export class Clock { lastPlayPressTime: number; totalPauseTime: number; timeviewer: HTMLElement; + deadline: number; constructor( public app: Editor, @@ -65,6 +66,7 @@ export class Clock { this.ctx = ctx; this.running = true; this.lastPauseTime = 0; + this.deadline = 0; this.lastPlayPressTime = 0; this.totalPauseTime = 0; this.timeviewer = document.getElementById("timeviewer")!; @@ -73,6 +75,7 @@ export class Clock { clockCallback = (time: number, duration: number, tick: number) => { let deadline = time - getAudioContext().currentTime; + this.deadline = deadline; this.tick = tick; if (this.app.clock.running) { if (this.app.settings.send_clock) { diff --git a/src/classes/SoundEvent.ts b/src/classes/SoundEvent.ts index b0af7ec..74a9e27 100644 --- a/src/classes/SoundEvent.ts +++ b/src/classes/SoundEvent.ts @@ -474,7 +474,7 @@ export class SoundEvent extends AudibleEvent { } superdough( filteredEvent, - this.nudge, + this.app.clock.deadline, filteredEvent.dur, ); } From dada6c1614c81225abbb8e732bb9d10284823b31 Mon Sep 17 00:00:00 2001 From: Raphael Forment Date: Fri, 1 Dec 2023 10:52:42 +0100 Subject: [PATCH 26/34] connecting more stuff before fixing --- src/Clock.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/Clock.ts b/src/Clock.ts index 1d93f92..3ab8595 100644 --- a/src/Clock.ts +++ b/src/Clock.ts @@ -184,10 +184,15 @@ export class Clock { set nudge(nudge: number) { } + get tickDuration() { + return 1 / this.ppqn; + } + set bpm(bpm: number) { if (bpm > 0 && this._bpm !== bpm) { this._bpm = bpm; this.logicalTime = this.realTime; + this.clock.setDuration(() => (this.tickDuration * 60) / this.bpm); } } @@ -261,6 +266,7 @@ export class Clock { this.app.api.MidiConnection.sendStopMessage(); this.lastPauseTime = this.app.audioContext.currentTime; this.logicalTime = this.realTime; + this.clock.pause() } public stop(): void { @@ -275,5 +281,6 @@ export class Clock { this.logicalTime = this.realTime; this.time_position = { bar: 0, beat: 0, pulse: 0 }; this.app.api.MidiConnection.sendStopMessage(); + this.clock.stop(); } } From 31adc17a36652d8246848362ddba4dc65932dca7 Mon Sep 17 00:00:00 2001 From: Raphael Forment Date: Fri, 1 Dec 2023 10:59:50 +0100 Subject: [PATCH 27/34] cleaning clock file a bit --- src/Clock.ts | 97 +++++----------------------------------------------- 1 file changed, 9 insertions(+), 88 deletions(-) diff --git a/src/Clock.ts b/src/Clock.ts index 3ab8595..6568cb6 100644 --- a/src/Clock.ts +++ b/src/Clock.ts @@ -33,9 +33,6 @@ export class Clock { * @param ppqn - The pulses per quarter note * @param tick - The current tick since origin * @param running - Is the clock running? - * @param lastPauseTime - The last time the clock was paused - * @param lastPlayPressTime - The last time the clock was started - * @param totalPauseTime - The total time the clock has been paused / stopped */ clock: any; @@ -47,9 +44,6 @@ export class Clock { private _ppqn: number; tick: number; running: boolean; - lastPauseTime: number; - lastPlayPressTime: number; - totalPauseTime: number; timeviewer: HTMLElement; deadline: number; @@ -65,10 +59,7 @@ export class Clock { this._ppqn = 48; this.ctx = ctx; this.running = true; - this.lastPauseTime = 0; this.deadline = 0; - this.lastPlayPressTime = 0; - this.totalPauseTime = 0; this.timeviewer = document.getElementById("timeviewer")!; this.clock = getAudioContext().createClock(this.clockCallback, this.pulse_duration) } @@ -102,12 +93,6 @@ export class Clock { }; convertTicksToTimeposition(ticks: number): TimePosition { - /** - * Converts ticks to a TimePosition object. - * @param ticks The number of ticks to convert. - * @returns The TimePosition object representing the converted ticks. - */ - const beatsPerBar = this.app.clock.time_signature[0]; const ppqnPosition = ticks % this.app.clock.ppqn; const beatNumber = Math.floor(ticks / this.app.clock.ppqn); @@ -117,73 +102,39 @@ export class Clock { } get ticks_before_new_bar(): number { - /** - * This function returns the number of ticks separating the current moment - * from the beginning of the next bar. - * - * @returns number of ticks until next bar - */ const ticskMissingFromBeat = this.ppqn - this.time_position.pulse; const beatsMissingFromBar = this.beats_per_bar - this.time_position.beat; return beatsMissingFromBar * this.ppqn + ticskMissingFromBeat; } get next_beat_in_ticks(): number { - /** - * This function returns the number of ticks separating the current moment - * from the beginning of the next beat. - * - * @returns number of ticks until next beat - */ - return this.app.clock.pulses_since_origin + this.time_position.pulse; + return this.app.clock.pulses_since_origin + this.time_position.pulse; } get beats_per_bar(): number { - /** - * Returns the number of beats per bar. - */ - return this.time_signature[0]; + return this.time_signature[0]; } get beats_since_origin(): number { - /** - * Returns the number of beats since the origin. - * - * @returns number of beats since origin - */ - return Math.floor(this.tick / this.ppqn); + return Math.floor(this.tick / this.ppqn); } get pulses_since_origin(): number { - /** - * Returns the number of pulses since the origin. - * - * @returns number of pulses since origin - */ - return this.tick; + return this.tick; } get pulse_duration(): number { - /** - * Returns the duration of a pulse in seconds. - */ - return 60 / this.bpm / this.ppqn; + return 60 / this.bpm / this.ppqn; } public pulse_duration_at_bpm(bpm: number = this.bpm): number { - /** - * Returns the duration of a pulse in seconds at a specific bpm. - */ - return 60 / bpm / this.ppqn; + return 60 / bpm / this.ppqn; } get bpm(): number { return this._bpm; } - set nudge(nudge: number) { - } - get tickDuration() { return 1 / this.ppqn; } @@ -200,34 +151,18 @@ export class Clock { return this._ppqn; } - get realTime(): number { - return this.app.audioContext.currentTime - this.totalPauseTime; - } - - get deviation(): number { - return Math.abs(this.logicalTime - this.realTime); - } - set ppqn(ppqn: number) { if (ppqn > 0 && this._ppqn !== ppqn) { this._ppqn = ppqn; - this.logicalTime = this.realTime; } } - public incrementTick(bpm: number) { + public incrementTick() { this.tick++; - this.logicalTime += this.pulse_duration_at_bpm(bpm); } public nextTickFrom(time: number, nudge: number): number { - /** - * Compute the time remaining before the next clock tick. - * @param time - audio context currentTime - * @param nudge - nudge in the future (in seconds) - * @returns remainingTime - */ - const pulseDuration = this.pulse_duration; + const pulseDuration = this.pulse_duration; const nudgedTime = time + nudge; const nextTickTime = Math.ceil(nudgedTime / pulseDuration) * pulseDuration; const remainingTime = nextTickTime - nudgedTime; @@ -236,9 +171,6 @@ export class Clock { } public convertPulseToSecond(n: number): number { - /** - * Converts a pulse to a second. - */ return n * this.pulse_duration; } @@ -251,21 +183,12 @@ export class Clock { this.app.audioContext.resume(); this.running = true; this.app.api.MidiConnection.sendStartMessage(); - this.lastPlayPressTime = this.app.audioContext.currentTime; - this.totalPauseTime += this.lastPlayPressTime - this.lastPauseTime; this.clock.start() } public pause(): void { - /** - * Pauses the TransportNode (pauses the clock). - * - * @remark also sends a MIDI message if a port is declared - */ this.running = false; this.app.api.MidiConnection.sendStopMessage(); - this.lastPauseTime = this.app.audioContext.currentTime; - this.logicalTime = this.realTime; this.clock.pause() } @@ -277,10 +200,8 @@ export class Clock { */ this.running = false; this.tick = 0; - this.lastPauseTime = this.app.audioContext.currentTime; - this.logicalTime = this.realTime; this.time_position = { bar: 0, beat: 0, pulse: 0 }; this.app.api.MidiConnection.sendStopMessage(); this.clock.stop(); } -} +} \ No newline at end of file From a34f1a33eb3a2d68e52e5e3806b00ea0d3ff6aaa Mon Sep 17 00:00:00 2001 From: Raphael Forment Date: Fri, 1 Dec 2023 11:16:16 +0100 Subject: [PATCH 28/34] lint --- src/API.ts | 16 ++++- src/Clock.ts | 69 +++++++++++---------- src/TransportNode.js | 68 --------------------- src/classes/AbstractEvents.ts | 22 ++++--- src/classes/SoundEvent.ts | 83 ++++++++++++-------------- src/documentation/more/oscilloscope.ts | 25 ++++---- src/extensions/NumberExtensions.ts | 6 +- 7 files changed, 117 insertions(+), 172 deletions(-) delete mode 100644 src/TransportNode.js diff --git a/src/API.ts b/src/API.ts index a4962d8..63699e5 100644 --- a/src/API.ts +++ b/src/API.ts @@ -1581,7 +1581,9 @@ export class UserAPI { // Low Frequency Oscillators // ============================================================= - line = (start: number, end: number, step: number = 1): number[] => { + public range = (v: number, a: number, b: number): number => v * (b - a) + a; + + public line = (start: number, end: number, step: number = 1): number[] => { /** * Returns an array of values between start and end, with a given step. * @@ -1603,7 +1605,11 @@ export class UserAPI { return result; }; - sine = (freq: number = 1, times: number = 1, offset: number = 0): number => { + public sine = ( + freq: number = 1, + times: number = 1, + offset: number = 0, + ): number => { /** * Returns a sine wave between -1 and 1. * @@ -1617,7 +1623,11 @@ export class UserAPI { ); }; - usine = (freq: number = 1, times: number = 1, offset: number = 0): number => { + public usine = ( + freq: number = 1, + times: number = 1, + offset: number = 0, + ): number => { /** * Returns a sine wave between 0 and 1. * diff --git a/src/Clock.ts b/src/Clock.ts index 6568cb6..6583bf5 100644 --- a/src/Clock.ts +++ b/src/Clock.ts @@ -4,7 +4,8 @@ import { tryEvaluate } from "./Evaluator"; import { getAudioContext } from "superdough"; // @ts-ignore import "zyklus"; -const zeroPad = (num: number, places: number) => String(num).padStart(places, "0"); +const zeroPad = (num: number, places: number) => + String(num).padStart(places, "0"); export interface TimePosition { /** @@ -61,35 +62,37 @@ export class Clock { this.running = true; this.deadline = 0; this.timeviewer = document.getElementById("timeviewer")!; - this.clock = getAudioContext().createClock(this.clockCallback, this.pulse_duration) - } + this.clock = getAudioContext().createClock( + this.clockCallback, + this.pulse_duration, + ); + } clockCallback = (time: number, duration: number, tick: number) => { let deadline = time - getAudioContext().currentTime; this.deadline = deadline; this.tick = tick; - if (this.app.clock.running) { - if (this.app.settings.send_clock) { - this.app.api.MidiConnection.sendMidiClock(); - } - const futureTimeStamp = this.app.clock.convertTicksToTimeposition( - this.app.clock.tick, - ); - this.app.clock.time_position = futureTimeStamp; - if (futureTimeStamp.pulse % this.app.clock.ppqn == 0) { - this.timeviewer.innerHTML = `${zeroPad(futureTimeStamp.bar, 2)}:${ - futureTimeStamp.beat + 1 - } / ${this.app.clock.bpm}`; - } - if (this.app.exampleIsPlaying) { - tryEvaluate(this.app, this.app.example_buffer); - } else { - tryEvaluate(this.app, this.app.global_buffer); - } - } + if (this.app.clock.running) { + if (this.app.settings.send_clock) { + this.app.api.MidiConnection.sendMidiClock(); + } + const futureTimeStamp = this.app.clock.convertTicksToTimeposition( + this.app.clock.tick, + ); + this.app.clock.time_position = futureTimeStamp; + if (futureTimeStamp.pulse % this.app.clock.ppqn == 0) { + this.timeviewer.innerHTML = `${zeroPad(futureTimeStamp.bar, 2)}:${ + futureTimeStamp.beat + 1 + } / ${this.app.clock.bpm}`; + } + if (this.app.exampleIsPlaying) { + tryEvaluate(this.app, this.app.example_buffer); + } else { + tryEvaluate(this.app, this.app.global_buffer); + } + } // Implement TransportNode clock callback and update clock info with it - }; convertTicksToTimeposition(ticks: number): TimePosition { @@ -108,27 +111,27 @@ export class Clock { } get next_beat_in_ticks(): number { - return this.app.clock.pulses_since_origin + this.time_position.pulse; + return this.app.clock.pulses_since_origin + this.time_position.pulse; } get beats_per_bar(): number { - return this.time_signature[0]; + return this.time_signature[0]; } get beats_since_origin(): number { - return Math.floor(this.tick / this.ppqn); + return Math.floor(this.tick / this.ppqn); } get pulses_since_origin(): number { - return this.tick; + return this.tick; } get pulse_duration(): number { - return 60 / this.bpm / this.ppqn; + return 60 / this.bpm / this.ppqn; } public pulse_duration_at_bpm(bpm: number = this.bpm): number { - return 60 / bpm / this.ppqn; + return 60 / bpm / this.ppqn; } get bpm(): number { @@ -162,7 +165,7 @@ export class Clock { } public nextTickFrom(time: number, nudge: number): number { - const pulseDuration = this.pulse_duration; + const pulseDuration = this.pulse_duration; const nudgedTime = time + nudge; const nextTickTime = Math.ceil(nudgedTime / pulseDuration) * pulseDuration; const remainingTime = nextTickTime - nudgedTime; @@ -183,13 +186,13 @@ export class Clock { this.app.audioContext.resume(); this.running = true; this.app.api.MidiConnection.sendStartMessage(); - this.clock.start() + this.clock.start(); } public pause(): void { this.running = false; this.app.api.MidiConnection.sendStopMessage(); - this.clock.pause() + this.clock.pause(); } public stop(): void { @@ -204,4 +207,4 @@ export class Clock { this.app.api.MidiConnection.sendStopMessage(); this.clock.stop(); } -} \ No newline at end of file +} diff --git a/src/TransportNode.js b/src/TransportNode.js deleted file mode 100644 index 7dd5e21..0000000 --- a/src/TransportNode.js +++ /dev/null @@ -1,68 +0,0 @@ -import { tryEvaluate } from "./Evaluator"; -const zeroPad = (num, places) => String(num).padStart(places, "0"); - -export class TransportNode extends AudioWorkletNode { - constructor(context, options, application) { - super(context, "transport", options); - this.app = application; - this.port.addEventListener("message", this.handleMessage); - this.port.start(); - this.timeviewer = document.getElementById("timeviewer"); - } - - /** @type {(this: MessagePort, ev: MessageEvent) => any} */ - handleMessage = (message) => { - if (message.data) { - if (message.data.type === "bang") { - if (this.app.clock.running) { - if (this.app.settings.send_clock) { - this.app.api.MidiConnection.sendMidiClock(); - } - const futureTimeStamp = this.app.clock.convertTicksToTimeposition( - this.app.clock.tick, - ); - this.app.clock.time_position = futureTimeStamp; - if (futureTimeStamp.pulse % this.app.clock.ppqn == 0) { - this.timeviewer.innerHTML = `${zeroPad(futureTimeStamp.bar, 2)}:${ - futureTimeStamp.beat + 1 - } / ${this.app.clock.bpm}`; - } - if (this.app.exampleIsPlaying) { - tryEvaluate(this.app, this.app.example_buffer); - } else { - tryEvaluate(this.app, this.app.global_buffer); - } - this.app.clock.incrementTick(message.data.bpm); - } - } - } - }; - - start() { - this.port.postMessage({ type: "start" }); - } - - pause() { - this.port.postMessage({ type: "pause" }); - } - - resume() { - this.port.postMessage({ type: "resume" }); - } - - setBPM(bpm) { - this.port.postMessage({ type: "bpm", value: bpm }); - } - - setPPQN(ppqn) { - this.port.postMessage({ type: "ppqn", value: ppqn }); - } - - setNudge(nudge) { - this.port.postMessage({ type: "nudge", value: nudge }); - } - - stop() { - this.port.postMessage({ type: "stop" }); - } -} diff --git a/src/classes/AbstractEvents.ts b/src/classes/AbstractEvents.ts index 72aa7e6..84f5537 100644 --- a/src/classes/AbstractEvents.ts +++ b/src/classes/AbstractEvents.ts @@ -1,5 +1,11 @@ import { type Editor } from "../main"; -import { freqToMidi, chord as parseChord, noteNameToMidi, resolvePitchBend, safeScale } from "zifferjs"; +import { + freqToMidi, + chord as parseChord, + noteNameToMidi, + resolvePitchBend, + safeScale, +} from "zifferjs"; import { SkipEvent } from "./SkipEvent"; export type EventOperation = (instance: T, ...args: any[]) => void; @@ -311,16 +317,16 @@ export abstract class AudibleEvent extends AbstractEvent { return this; }; - protected updateValue( - key: string, - value: T | T[] | null - ): this { + protected updateValue(key: string, value: T | T[] | null): this { if (value == null) return this; this.values[key] = value; return this; } - public note = (value: number | string | null, ...kwargs: number[]|string[]) => { + public note = ( + value: number | string | null, + ...kwargs: number[] | string[] + ) => { if (typeof value === "string") { const parsedNote = noteNameToMidi(value); return this.updateValue("note", [parsedNote, ...kwargs].flat(Infinity)); @@ -331,8 +337,8 @@ export abstract class AudibleEvent extends AbstractEvent { } }; - public chord = (value: number|string, ...kwargs: number[]) => { - if(typeof value === "string") { + public chord = (value: number | string, ...kwargs: number[]) => { + if (typeof value === "string") { const chord = parseChord(value); return this.updateValue("note", chord); } else { diff --git a/src/classes/SoundEvent.ts b/src/classes/SoundEvent.ts index 74a9e27..cc210ee 100644 --- a/src/classes/SoundEvent.ts +++ b/src/classes/SoundEvent.ts @@ -5,10 +5,7 @@ import { arrayOfObjectsToObjectWithArrays, objectWithArraysToArrayOfObjects, } from "../Utils/Generic"; -import { - midiToFreq, - noteFromPc, -} from "zifferjs"; +import { midiToFreq, noteFromPc } from "zifferjs"; import { superdough, @@ -66,7 +63,7 @@ export class SoundEvent extends AudibleEvent { phaserDepth: ["phaserDepth", "phasdepth"], phaserSweep: ["phaserSweep", "phassweep"], phaserCenter: ["phaserCenter", "phascenter"], - fmadsr: function( + fmadsr: function ( self: SoundEvent, a: number, d: number, @@ -79,7 +76,7 @@ export class SoundEvent extends AudibleEvent { self.updateValue("fmrelease", r); return self; }, - fmad: function(self: SoundEvent, a: number, d: number) { + fmad: function (self: SoundEvent, a: number, d: number) { self.updateValue("fmattack", a); self.updateValue("fmdecay", d); return self; @@ -90,7 +87,7 @@ export class SoundEvent extends AudibleEvent { decay: ["decay", "dec"], sustain: ["sustain", "sus"], release: ["release", "rel"], - adsr: function( + adsr: function ( self: SoundEvent, a: number, d: number, @@ -103,21 +100,21 @@ export class SoundEvent extends AudibleEvent { self.updateValue("release", r); return self; }, - ad: function(self: SoundEvent, a: number, d: number) { + ad: function (self: SoundEvent, a: number, d: number) { self.updateValue("attack", a); self.updateValue("decay", d); self.updateValue("sustain", 0.0); self.updateValue("release", 0.0); return self; }, - scope: function(self: SoundEvent) { - self.updateValue("analyze", true) - return self + scope: function (self: SoundEvent) { + self.updateValue("analyze", true); + return self; }, - debug: function(self: SoundEvent, callback?: Function) { - self.updateValue("debug", true) + debug: function (self: SoundEvent, callback?: Function) { + self.updateValue("debug", true); if (callback) { - self.updateValue("debugFunction", callback) + self.updateValue("debugFunction", callback); } return self; }, @@ -126,27 +123,27 @@ export class SoundEvent extends AudibleEvent { lpdecay: ["lpdecay", "lpd"], lpsustain: ["lpsustain", "lps"], lprelease: ["lprelease", "lpr"], - cutoff: function(self: SoundEvent, value: number, resonance?: number) { + cutoff: function (self: SoundEvent, value: number, resonance?: number) { self.updateValue("cutoff", value); if (resonance) { self.updateValue("resonance", resonance); } return self; }, - lpf: function(self: SoundEvent, value: number, resonance?: number) { + lpf: function (self: SoundEvent, value: number, resonance?: number) { self.updateValue("cutoff", value); if (resonance) { self.updateValue("resonance", resonance); } return self; }, - resonance: function(self: SoundEvent, value: number) { + resonance: function (self: SoundEvent, value: number) { if (value >= 0 && value <= 1) { self.updateValue("resonance", 50 * value); } return self; }, - lpadsr: function( + lpadsr: function ( self: SoundEvent, depth: number, a: number, @@ -161,7 +158,7 @@ export class SoundEvent extends AudibleEvent { self.updateValue("lprelease", r); return self; }, - lpad: function(self: SoundEvent, depth: number, a: number, d: number) { + lpad: function (self: SoundEvent, depth: number, a: number, d: number) { self.updateValue("lpenv", depth); self.updateValue("lpattack", a); self.updateValue("lpdecay", d); @@ -174,25 +171,25 @@ export class SoundEvent extends AudibleEvent { hpdecay: ["hpdecay", "hpd"], hpsustain: ["hpsustain", "hpsus"], hprelease: ["hprelease", "hpr"], - hcutoff: function(self: SoundEvent, value: number, resonance?: number) { + hcutoff: function (self: SoundEvent, value: number, resonance?: number) { self.updateValue("hcutoff", value); if (resonance) { self.updateValue("hresonance", resonance); } return self; }, - hpf: function(self: SoundEvent, value: number, resonance?: number) { + hpf: function (self: SoundEvent, value: number, resonance?: number) { self.updateValue("hcutoff", value); if (resonance) { self.updateValue("hresonance", resonance); } return self; }, - hpq: function(self: SoundEvent, value: number) { + hpq: function (self: SoundEvent, value: number) { self.updateValue("hresonance", value); return self; }, - hpadsr: function( + hpadsr: function ( self: SoundEvent, depth: number, a: number, @@ -207,7 +204,7 @@ export class SoundEvent extends AudibleEvent { self.updateValue("hprelease", r); return self; }, - hpad: function(self: SoundEvent, depth: number, a: number, d: number) { + hpad: function (self: SoundEvent, depth: number, a: number, d: number) { self.updateValue("hpenv", depth); self.updateValue("hpattack", a); self.updateValue("hpdecay", d); @@ -220,14 +217,14 @@ export class SoundEvent extends AudibleEvent { bpdecay: ["bpdecay", "bpd"], bpsustain: ["bpsustain", "bps"], bprelease: ["bprelease", "bpr"], - bandf: function(self: SoundEvent, value: number, resonance?: number) { + bandf: function (self: SoundEvent, value: number, resonance?: number) { self.updateValue("bandf", value); if (resonance) { self.updateValue("bandq", resonance); } return self; }, - bpf: function(self: SoundEvent, value: number, resonance?: number) { + bpf: function (self: SoundEvent, value: number, resonance?: number) { self.updateValue("bandf", value); if (resonance) { self.updateValue("bandq", resonance); @@ -235,7 +232,7 @@ export class SoundEvent extends AudibleEvent { return self; }, bandq: ["bandq", "bpq"], - bpadsr: function( + bpadsr: function ( self: SoundEvent, depth: number, a: number, @@ -250,7 +247,7 @@ export class SoundEvent extends AudibleEvent { self.updateValue("bprelease", r); return self; }, - bpad: function(self: SoundEvent, depth: number, a: number, d: number) { + bpad: function (self: SoundEvent, depth: number, a: number, d: number) { self.updateValue("bpenv", depth); self.updateValue("bpattack", a); self.updateValue("bpdecay", d); @@ -260,7 +257,7 @@ export class SoundEvent extends AudibleEvent { }, vib: ["vib"], vibmod: ["vibmod"], - fm: function(self: SoundEvent, value: number | string) { + fm: function (self: SoundEvent, value: number | string) { if (typeof value === "number") { self.values["fmi"] = value; } else { @@ -276,11 +273,11 @@ export class SoundEvent extends AudibleEvent { begin: ["begin"], end: ["end"], gain: ["gain"], - dbgain: function(self: SoundEvent, value: number) { + dbgain: function (self: SoundEvent, value: number) { self.updateValue("gain", Math.min(Math.pow(10, value / 20), 10)); return self; }, - db: function(self: SoundEvent, value: number) { + db: function (self: SoundEvent, value: number) { self.updateValue("gain", Math.min(Math.pow(10, value / 20), 10)); return self; }, @@ -303,32 +300,32 @@ export class SoundEvent extends AudibleEvent { roomlp: ["roomlp", "rlp"], roomdim: ["roomdim", "rdim"], sound: ["s", "sound"], - size: function(self: SoundEvent, value: number) { + size: function (self: SoundEvent, value: number) { self.updateValue("roomsize", value); return self; }, - sz: function(self: SoundEvent, value: number) { + sz: function (self: SoundEvent, value: number) { self.updateValue("roomsize", value); return self; }, comp: ["compressor", "cmp"], - ratio: function(self: SoundEvent, value: number) { + ratio: function (self: SoundEvent, value: number) { self.updateValue("compressorRatio", value); return self; }, - knee: function(self: SoundEvent, value: number) { + knee: function (self: SoundEvent, value: number) { self.updateValue("compressorKnee", value); return self; }, - compAttack: function(self: SoundEvent, value: number) { + compAttack: function (self: SoundEvent, value: number) { self.updateValue("compressorAttack", value); return self; }, - compRelease: function(self: SoundEvent, value: number) { + compRelease: function (self: SoundEvent, value: number) { self.updateValue("compressorRelease", value); return self; }, - stretch: function(self: SoundEvent, beat: number) { + stretch: function (self: SoundEvent, beat: number) { self.updateValue("unit", "c"); self.updateValue("speed", 1 / beat); self.updateValue("cut", beat); @@ -467,16 +464,12 @@ export class SoundEvent extends AudibleEvent { } if (this.values["debug"]) { if (this.values["debugFunction"]) { - this.values["debugFunction"](filteredEvent) + this.values["debugFunction"](filteredEvent); } else { - console.log(filteredEvent) + console.log(filteredEvent); } } - superdough( - filteredEvent, - this.app.clock.deadline, - filteredEvent.dur, - ); + superdough(filteredEvent, this.app.clock.deadline, filteredEvent.dur); } }; } diff --git a/src/documentation/more/oscilloscope.ts b/src/documentation/more/oscilloscope.ts index 22c2d90..7e43a23 100644 --- a/src/documentation/more/oscilloscope.ts +++ b/src/documentation/more/oscilloscope.ts @@ -10,17 +10,18 @@ You can turn on the oscilloscope to generate interesting visuals or to inspect a You need to manually feed the scope with the sounds you want to inspect: ${makeExample( - "Feeding a sine to the oscilloscope", - ` + "Feeding a sine to the oscilloscope", + ` beat(1)::sound('sine').freq(200).ad(0, .2).scope().out() -`, true - )} +`, + true, +)} Here is a layout of the scope configuration options: ${makeExample( - "Oscilloscope configuration", - ` + "Oscilloscope configuration", + ` scope({ enabled: true, // off by default color: "#fdba74", // any valid CSS color or "random" @@ -34,12 +35,12 @@ scope({ refresh: 1 // refresh rate (in pulses) }) `, - true, - )} + true, +)} ${makeExample( - "Demo with multiple scope mode", - ` + "Demo with multiple scope mode", + ` rhythm(.5, [4,5].dur(4*3, 4*1), 8)::sound('fhardkick').out() beat(0.25)::sound('square').freq([ [250, 250/2, 250/4].pick(), @@ -55,8 +56,8 @@ scope({enabled: true, thickness: 8, color: ['purple', 'green', 'random'].beat(), size: 0.5, fftSize: 2048}) `, - true, - )} + true, +)} Note that these values can be patterned as well! You can transform the oscilloscope into its own light show if you want. The picture is not stable anyway so you won't have much use of it for precision work :) diff --git a/src/extensions/NumberExtensions.ts b/src/extensions/NumberExtensions.ts index 84a781b..6c592cd 100644 --- a/src/extensions/NumberExtensions.ts +++ b/src/extensions/NumberExtensions.ts @@ -25,7 +25,7 @@ declare global { z15(): Player; z16(): Player; midi(): MidiEvent; - sound(name: string): SoundEvent|SkipEvent; + sound(name: string): SoundEvent | SkipEvent; } } @@ -102,11 +102,11 @@ export const makeNumberExtensions = (api: UserAPI) => { return api.midi(this.valueOf(), ...kwargs); }; - Number.prototype.sound = function (name: string): SoundEvent|SkipEvent { + Number.prototype.sound = function (name: string): SoundEvent | SkipEvent { if (Number.isInteger(this.valueOf())) { return (api.sound(name) as SoundEvent).note(this.valueOf()); } else { return (api.sound(name) as SoundEvent).freq(this.valueOf()); - } + } }; }; From e5a331c6cf31c8fe3a05a23801dc87118985116c Mon Sep 17 00:00:00 2001 From: Raphael Forment Date: Fri, 1 Dec 2023 12:19:42 +0100 Subject: [PATCH 29/34] clean --- src/main.ts | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/main.ts b/src/main.ts index 4b5569a..c541883 100644 --- a/src/main.ts +++ b/src/main.ts @@ -246,20 +246,17 @@ export class Editor { "ui-known-universe-item-template", ) as HTMLTemplateElement; if (!itemTemplate) { - console.warn("Missing template #ui-known-universe-item-template"); return; } let existing_universes = document.getElementById("existing-universes"); if (!existing_universes) { - console.warn("Missing element #existing-universes"); return; } let list = document.createElement("ul"); list.className = "lg:h-80 lg:text-normal text-sm h-auto lg:w-80 w-auto lg:pb-2 lg:pt-2 overflow-y-scroll text-white lg:mb-4 border rounded-lg bg-neutral-800"; - list.append( ...Object.keys(this.universes).map((it) => { let item = itemTemplate.content.cloneNode(true) as DocumentFragment; From bcb0ddc1cb88a0ae742465500ba2a7b4bb74d00b Mon Sep 17 00:00:00 2001 From: Raphael Forment Date: Fri, 1 Dec 2023 12:30:33 +0100 Subject: [PATCH 30/34] document clock --- src/Clock.ts | 111 +++++++++++++++++++++++++++++++++++++++++---------- 1 file changed, 89 insertions(+), 22 deletions(-) diff --git a/src/Clock.ts b/src/Clock.ts index 6583bf5..7bc162b 100644 --- a/src/Clock.ts +++ b/src/Clock.ts @@ -22,27 +22,25 @@ export interface TimePosition { export class Clock { /** - * The Clock Class is responsible for keeping track of the current time. - * It is also responsible for starting and stopping the Clock TransportNode. * - * @param app - The main application instance - * @param clock - The zyklus clock - * @param ctx - The current AudioContext used by app - * @param bpm - The current beats per minute value - * @param time_signature - The time signature - * @param time_position - The current time position - * @param ppqn - The pulses per quarter note - * @param tick - The current tick since origin + * @param app - main application instance + * @param clock - zyklus clock + * @param ctx - current AudioContext used by app + * @param bpm - current beats per minute value + * @param time_signature - time signature + * @param time_position - current time position + * @param ppqn - pulses per quarter note + * @param tick - current tick since origin * @param running - Is the clock running? */ + private _bpm: number; + private _ppqn: number; clock: any; ctx: AudioContext; logicalTime: number; - private _bpm: number; time_signature: number[]; time_position: TimePosition; - private _ppqn: number; tick: number; running: boolean; timeviewer: HTMLElement; @@ -69,6 +67,14 @@ export class Clock { } clockCallback = (time: number, duration: number, tick: number) => { + /** + * Callback function for the zyklus clock. Updates the clock info and sends a + * MIDI clock message if the setting is enabled. Also evaluates the global buffer. + * + * @param time - precise AudioContext time when the tick should happen + * @param duration - seconds between each tick + * @param tick - count of the current tick + */ let deadline = time - getAudioContext().currentTime; this.deadline = deadline; this.tick = tick; @@ -96,6 +102,12 @@ export class Clock { }; convertTicksToTimeposition(ticks: number): TimePosition { + /** + * Converts ticks to a time position. + * + * @param ticks - ticks to convert + * @returns TimePosition + */ const beatsPerBar = this.app.clock.time_signature[0]; const ppqnPosition = ticks % this.app.clock.ppqn; const beatNumber = Math.floor(ticks / this.app.clock.ppqn); @@ -105,71 +117,121 @@ export class Clock { } get ticks_before_new_bar(): number { + /** + * Calculates the number of ticks before the next bar. + * + * @returns number - ticks before the next bar + */ const ticskMissingFromBeat = this.ppqn - this.time_position.pulse; const beatsMissingFromBar = this.beats_per_bar - this.time_position.beat; return beatsMissingFromBar * this.ppqn + ticskMissingFromBeat; } get next_beat_in_ticks(): number { + /** + * Calculates the number of ticks before the next beat. + * + * @returns number - ticks before the next beat + */ return this.app.clock.pulses_since_origin + this.time_position.pulse; } get beats_per_bar(): number { + /** + * Returns the number of beats per bar. + * + * @returns number - beats per bar + */ return this.time_signature[0]; } get beats_since_origin(): number { + /** + * Returns the number of beats since the origin. + * + * @returns number - beats since the origin + */ return Math.floor(this.tick / this.ppqn); } get pulses_since_origin(): number { + /** + * Returns the number of pulses since the origin. + * + * @returns number - pulses since the origin + */ return this.tick; } get pulse_duration(): number { + /** + * Returns the duration of a pulse in seconds. + * @returns number - duration of a pulse in seconds + */ return 60 / this.bpm / this.ppqn; } public pulse_duration_at_bpm(bpm: number = this.bpm): number { + /** + * Returns the duration of a pulse in seconds at a given bpm. + * + * @param bpm - bpm to calculate the pulse duration for + * @returns number - duration of a pulse in seconds + */ return 60 / bpm / this.ppqn; } get bpm(): number { + /** + * Returns the current bpm. + * @returns number - current bpm + */ return this._bpm; } - get tickDuration() { + get tickDuration(): number { + /** + * Returns the duration of a tick in seconds. + * @returns number - duration of a tick in seconds + */ return 1 / this.ppqn; } set bpm(bpm: number) { + /** + * Sets the bpm. + * @param bpm - bpm to set + */ if (bpm > 0 && this._bpm !== bpm) { this._bpm = bpm; - this.logicalTime = this.realTime; this.clock.setDuration(() => (this.tickDuration * 60) / this.bpm); } } get ppqn(): number { + /** + * Returns the current ppqn. + * @returns number - current ppqn + */ return this._ppqn; } set ppqn(ppqn: number) { + /** + * Sets the ppqn. + * @param ppqn - ppqn to set + * @returns number - current ppqn + */ if (ppqn > 0 && this._ppqn !== ppqn) { this._ppqn = ppqn; } } - public incrementTick() { - this.tick++; - } - public nextTickFrom(time: number, nudge: number): number { const pulseDuration = this.pulse_duration; const nudgedTime = time + nudge; const nextTickTime = Math.ceil(nudgedTime / pulseDuration) * pulseDuration; const remainingTime = nextTickTime - nudgedTime; - return remainingTime; } @@ -179,8 +241,8 @@ export class Clock { public start(): void { /** - * Starts the TransportNode (starts the clock). - * + * Start the clock + * * @remark also sends a MIDI message if a port is declared */ this.app.audioContext.resume(); @@ -190,6 +252,11 @@ export class Clock { } public pause(): void { + /** + * Pause the clock. + * + * @remark also sends a MIDI message if a port is declared + */ this.running = false; this.app.api.MidiConnection.sendStopMessage(); this.clock.pause(); @@ -197,7 +264,7 @@ export class Clock { public stop(): void { /** - * Stops the TransportNode (stops the clock). + * Stops the clock. * * @remark also sends a MIDI message if a port is declared */ From 0e63f872710eee4900bef39a96e8d61126003d2e Mon Sep 17 00:00:00 2001 From: Raphael Forment Date: Sat, 2 Dec 2023 10:44:12 +0100 Subject: [PATCH 31/34] prepare version --- src/API.ts | 31 +++---------------------------- src/Clock.ts | 6 +++--- src/InterfaceLogic.ts | 7 ++++--- 3 files changed, 10 insertions(+), 34 deletions(-) diff --git a/src/API.ts b/src/API.ts index 63699e5..8e4950a 100644 --- a/src/API.ts +++ b/src/API.ts @@ -1298,7 +1298,7 @@ export class UserAPI { const results: boolean[] = nArray.map( (value) => (this.app.clock.pulses_since_origin - Math.floor(nudge * this.ppqn())) % - Math.floor(value * this.ppqn()) === + Math.floor(value * this.ppqn()) === 0, ); return results.some((value) => value === true); @@ -1318,7 +1318,7 @@ export class UserAPI { const results: boolean[] = nArray.map( (value) => (this.app.clock.pulses_since_origin - nudgeInPulses) % - Math.floor(value * barLength) === + Math.floor(value * barLength) === 0, ); return results.some((value) => value === true); @@ -1790,18 +1790,6 @@ export class UserAPI { return sum / values.length; }; - public range = ( - inputY: number, - yMin: number, - yMax: number, - xMin: number, - xMax: number, - ): number => { - const percent = (inputY - yMin) / (yMax - yMin); - const outputX = percent * (xMax - xMin) + xMin; - return outputX; - }; - limit = (value: number, min: number, max: number): number => { /** * Limits a value between a minimum and a maximum. @@ -1928,7 +1916,7 @@ export class UserAPI { // ============================================================= register = (name: string, operation: EventOperation): void => { - AbstractEvent.prototype[name] = function ( + AbstractEvent.prototype[name] = function( this: AbstractEvent, ...args: any[] ) { @@ -2111,19 +2099,6 @@ export class UserAPI { // Transport functions // ============================================================= - public nudge = (nudge?: number): number => { - /** - * Sets or returns the current clock nudge. - * - * @param nudge - [optional] the nudge to set - * @returns The current nudge - */ - if (nudge) { - this.app.clock.nudge = nudge; - } - return this.app.clock.nudge; - }; - public tempo = (n?: number): number => { /** * Sets or returns the current bpm. diff --git a/src/Clock.ts b/src/Clock.ts index 7bc162b..e47c730 100644 --- a/src/Clock.ts +++ b/src/Clock.ts @@ -66,6 +66,7 @@ export class Clock { ); } + // @ts-ignore clockCallback = (time: number, duration: number, tick: number) => { /** * Callback function for the zyklus clock. Updates the clock info and sends a @@ -87,9 +88,8 @@ export class Clock { ); this.app.clock.time_position = futureTimeStamp; if (futureTimeStamp.pulse % this.app.clock.ppqn == 0) { - this.timeviewer.innerHTML = `${zeroPad(futureTimeStamp.bar, 2)}:${ - futureTimeStamp.beat + 1 - } / ${this.app.clock.bpm}`; + this.timeviewer.innerHTML = `${zeroPad(futureTimeStamp.bar, 2)}:${futureTimeStamp.beat + 1 + } / ${this.app.clock.bpm}`; } if (this.app.exampleIsPlaying) { tryEvaluate(this.app, this.app.example_buffer); diff --git a/src/InterfaceLogic.ts b/src/InterfaceLogic.ts index 0f55f3d..290a757 100644 --- a/src/InterfaceLogic.ts +++ b/src/InterfaceLogic.ts @@ -144,9 +144,10 @@ export const installInterfaceLogic = (app: Editor) => { }); app.interface.audio_nudge_range.addEventListener("input", () => { - app.clock.nudge = parseInt( - (app.interface.audio_nudge_range as HTMLInputElement).value, - ); + // TODO: rebuild this + // app.clock.nudge = parseInt( + // (app.interface.audio_nudge_range as HTMLInputElement).value, + // ); }); app.interface.dough_nudge_range.addEventListener("input", () => { From 1bc7fcd3cbd9e147b2d9abc5a13c9f5a83ee1fe9 Mon Sep 17 00:00:00 2001 From: Raphael Forment Date: Sun, 3 Dec 2023 20:37:55 +0100 Subject: [PATCH 32/34] adding links --- README.md | 21 +++++++++++----- index.html | 7 ++++-- src/documentation/basics/welcome.ts | 38 +++++++++++++++++------------ src/documentation/more/about.ts | 15 ++++++++++++ 4 files changed, 57 insertions(+), 24 deletions(-) diff --git a/README.md b/README.md index eb55eb5..101fe40 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,9 @@ # Topos: A Web-Based Algorithmic Sequencer

 | - Discord | - BuboBubo |  - Amiika | + Discord | + BuboBubo | + Amiika | About Live Coding |

Contributors

@@ -12,15 +12,24 @@

+ Buy Me a Coffee at ko-fi.com

-Topos is a web-based live coding environment. It lives [here](https://topos.live). Documentation is directly embedded in the application itself. Topos is an emulation and extension of the [Monome Teletype](https://monome.org/docs/teletype/) that gradually evolved into something a bit more personal. - +Topos is a web based live coding environment. Topos is capable of many things: +- it is a music sequencer made for improvisation and composition alike +- it is a synthesizer capable of additive, substractive, FM and wavetable +synthesis, backed up by a [powerful web based audio engine](https://www.npmjs.com/package/superdough) +- it can also generate video thanks to [Hydra](https://hydra.ojack.xyz/) and +custom oscilloscopes, frequency visualizers and image sequencing capabilities +- it can be used to sequence other MIDI devices (and soon.. OSC!) +- it is made to be used without the need of installing anything, always ready at + [https://topos.live](https://topos.live) +- Topos is also an emulation and personal extension of the [Monome Teletype](https://monome.org/docs/teletype/) ![Screenshot](https://github.com/Bubobubobubobubo/Topos/blob/main/img/topos_gif.gif) ## Disclaimer -**Topos** is a fairly young project developed by two part time hobbyists :) Do not expect stable features and/or user support in the initial development stage. Contributors and curious people are welcome! The software is working quite well and we are continuously striving to improve it. +**Topos** is still a young project developed by two hobbyists :) Contributions are welcome! We wish to be as inclusive and welcoming as possible to your ideas and suggestions! The software is working quite well and we are continuously striving to improve it. ## Installation (for devs and contributors) diff --git a/index.html b/index.html index 3a96625..edde30b 100644 --- a/index.html +++ b/index.html @@ -209,10 +209,13 @@
Community
- +
- + +
+
+
diff --git a/src/documentation/basics/welcome.ts b/src/documentation/basics/welcome.ts index d75b392..a714d35 100644 --- a/src/documentation/basics/welcome.ts +++ b/src/documentation/basics/welcome.ts @@ -12,30 +12,30 @@ Welcome to the **Topos** documentation. You can jump here anytime by pressing ${ )}. Press again to make the documentation disappear. Contributions are much appreciated! The documentation [lives here](https://github.com/Bubobubobubobubo/topos/tree/main/src/documentation). ${makeExample( - "Welcome! Eval to get started", - examples[Math.floor(Math.random() * examples.length)], - true, -)} + "Welcome! Eval to get started", + examples[Math.floor(Math.random() * examples.length)], + true, + )} # What is Topos? Topos is an _algorithmic_ sequencer. Topos is also a _live coding_ environment. To sum it up, think: "_making music in real time through code_". Code used as an expressive medium for musical improvisation! Topos uses small algorithms to represent musical sequences and processes. ${makeExample( - "Small algorithms for direct musical expression", - ` + "Small algorithms for direct musical expression", + ` rhythm(.5, 4, 8) :: sound('drum').out() rhythm(.25, [5, 7].beat(2), 8) :: sound(['hc', 'fikea', 'hat'].pick(1)) .lpf([500, 4000+usine(1/2)*2000]).pan(r(0, 1)).ad(0, [1, .5]) .db(-ir(1,8)).speed([1,[0.5, 2].pick()]).room(0.5).size(3).o(4).out() beat([2,0.5].dur(13.5, 0.5))::snd('fsoftsnare') .n(0).speed([1, 0.5]).o(4).out()`, - false, -)} + false, + )} ${makeExample( - "Computer music should be immediate and intuitive", - ` + "Computer music should be immediate and intuitive", + ` let chord_prog = [0, 0, 5].bar() // Chord progression beat(.25)::snd('sine') .note(chord_prog + [60, 64, 67, 71].mouseX() @@ -47,19 +47,19 @@ beat(.25)::snd('sine') .delay(0.5).delayt(0.25).delayfb(0.7) // Delay .room(0.5).size(8) // Reverb .out()`, - false, -)} + false, + )} ${makeExample( - "Making the web less dreadful, one beep at at time", - ` + "Making the web less dreadful, one beep at at time", + ` beat(.5) :: sound('sid').n($(2)) .room(1).speed([1,2].pick()).out() beat(.25) :: sound('sid').note( [34, 36, 41].beat(.25) + [[0,-24].pick(),12].beat()) .room(0.9).size(0.9).n(4).out()`, - false, -)} + false, + )} Topos is deeply inspired by the [Monome Teletype](https://monome.org/). The Teletype is/was an open source hardware module for Eurorack synthesizers. While the Teletype was initially born as an hardware module, Topos aims to be a web-browser based cousin of it! It is a sequencer, a scriptable interface, a companion for algorithmic music-making. Topos wishes to fullfill the same goal as the Teletype, keeping the same spirit alive on the web. It is free, open-source, and made to be shared and used by everyone. Learn more about live coding on [livecoding.fr](https://livecoding.fr). @@ -68,5 +68,11 @@ Topos is deeply inspired by the [Monome Teletype](https://monome.org/). The Tele Reloading the application will get you one random song example to study every time. Press ${key_shortcut( "F5", )} and listen to them all! The demo songs are also used a bit everywhere in the documentation to illustrate some of the working principles :). + +## Support + +

You can Buy Me a Coffee at ko-fi.com to support the development :)

+ `; }; + diff --git a/src/documentation/more/about.ts b/src/documentation/more/about.ts index 652bafe..73c7997 100644 --- a/src/documentation/more/about.ts +++ b/src/documentation/more/about.ts @@ -14,6 +14,21 @@ Topos is an experimental web based algorithmic sequencer programmed by **BuboBub Topos is a free and open-source software distributed under [GPL-3.0](https://github.com/Bubobubobubobubo/Topos/blob/main/LICENSE) licence. We welcome all contributions and ideas. You can find the source code on [GitHub](https://github.com/Bubobubobubobubo/topos). You can also join us on [Discord](https://discord.gg/dnUTPbu6bN) to discuss about the project and live coding in general. +## Support the project + +You can support the project by making a small donation on [Kofi](https://ko-fi.com/Manage/). + +
+ +
+ + ## Credits - Felix Roos for the [SuperDough](https://www.npmjs.com/package/superdough) audio engine. From 9031f7b87db2d126b71a7da792de48f6e7700b71 Mon Sep 17 00:00:00 2001 From: Raphael Forment Date: Sun, 3 Dec 2023 20:38:56 +0100 Subject: [PATCH 33/34] update readme --- README.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/README.md b/README.md index 101fe40..523076a 100644 --- a/README.md +++ b/README.md @@ -12,9 +12,13 @@

+

Buy Me a Coffee at ko-fi.com +

+--------------------- + Topos is a web based live coding environment. Topos is capable of many things: - it is a music sequencer made for improvisation and composition alike - it is a synthesizer capable of additive, substractive, FM and wavetable @@ -27,6 +31,8 @@ custom oscilloscopes, frequency visualizers and image sequencing capabilities - Topos is also an emulation and personal extension of the [Monome Teletype](https://monome.org/docs/teletype/) ![Screenshot](https://github.com/Bubobubobubobubo/Topos/blob/main/img/topos_gif.gif) +--------------------- + ## Disclaimer **Topos** is still a young project developed by two hobbyists :) Contributions are welcome! We wish to be as inclusive and welcoming as possible to your ideas and suggestions! The software is working quite well and we are continuously striving to improve it. From c68a090e02b491fefc2cf8e0ca12c387c2914b01 Mon Sep 17 00:00:00 2001 From: Raphael Forment Date: Sun, 3 Dec 2023 20:39:25 +0100 Subject: [PATCH 34/34] update readme again --- README.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 523076a..71cca60 100644 --- a/README.md +++ b/README.md @@ -29,10 +29,12 @@ custom oscilloscopes, frequency visualizers and image sequencing capabilities - it is made to be used without the need of installing anything, always ready at [https://topos.live](https://topos.live) - Topos is also an emulation and personal extension of the [Monome Teletype](https://monome.org/docs/teletype/) -![Screenshot](https://github.com/Bubobubobubobubo/Topos/blob/main/img/topos_gif.gif) --------------------- +![Screenshot](https://github.com/Bubobubobubobubo/Topos/blob/main/img/topos_gif.gif) + + ## Disclaimer **Topos** is still a young project developed by two hobbyists :) Contributions are welcome! We wish to be as inclusive and welcoming as possible to your ideas and suggestions! The software is working quite well and we are continuously striving to improve it.