From 094fb570499ea818ad07f44b8d7404dae7d0456d Mon Sep 17 00:00:00 2001 From: Raphael Forment Date: Sun, 22 Oct 2023 15:56:42 +0200 Subject: [PATCH 01/10] adding new canvas for feedback --- index.html | 14 ++++++++++++++ src/AudioVisualisation.ts | 2 ++ src/DomElements.ts | 1 + 3 files changed, 17 insertions(+) create mode 100644 src/AudioVisualisation.ts diff --git a/index.html b/index.html index 2d3c60c..2b115d8 100644 --- a/index.html +++ b/index.html @@ -34,6 +34,19 @@ display: block; } + #feedback { + position: fixed; /* ignore margins */ + top: 0px; + left: 0px; + width: 100%; /* fill screen */ + height: 100%; + background-size: cover; + overflow-y: hidden; + z-index: -1; /* place behind everything else */ + display: block; + } + + details br { display: none; @@ -447,6 +460,7 @@
+
diff --git a/src/AudioVisualisation.ts b/src/AudioVisualisation.ts new file mode 100644 index 0000000..423fb51 --- /dev/null +++ b/src/AudioVisualisation.ts @@ -0,0 +1,2 @@ +// @ts-ignore +import { analyser, getAnalyzerData } from "superdough"; diff --git a/src/DomElements.ts b/src/DomElements.ts index c592c35..f5d8c57 100644 --- a/src/DomElements.ts +++ b/src/DomElements.ts @@ -47,6 +47,7 @@ export const singleElements = { dough_nudge_range: "dough_nudge", error_line: "error_line", hydra_canvas: "hydra-bg", + feedback: "feedback", }; export const buttonGroups = { From d6577718a6c1a5fde0135959847f2bceb7b75ede Mon Sep 17 00:00:00 2001 From: Raphael Forment Date: Sun, 22 Oct 2023 21:13:18 +0200 Subject: [PATCH 02/10] adding script vis --- index.html | 5 ++ src/API.ts | 12 ++-- src/AudioVisualisation.ts | 113 ++++++++++++++++++++++++++++++++++++++ src/WindowBehavior.ts | 18 ++++++ src/main.ts | 46 +++++++++++++--- 5 files changed, 183 insertions(+), 11 deletions(-) diff --git a/index.html b/index.html index 2b115d8..11a7f62 100644 --- a/index.html +++ b/index.html @@ -22,6 +22,11 @@ padding: 0; } + .fluid-bg-transition { + transition: background-color 0.05s ease-in-out; + } + + #hydra-bg { position: fixed; /* ignore margins */ top: 0px; diff --git a/src/API.ts b/src/API.ts index 1c604b6..9367af5 100644 --- a/src/API.ts +++ b/src/API.ts @@ -26,6 +26,7 @@ import { } from "superdough"; import { Speaker } from "./StringExtensions"; import { getScaleNotes } from "zifferjs"; +import { blinkScript } from "./AudioVisualisation"; interface ControlChange { channel: number; @@ -269,10 +270,13 @@ export class UserAPI { * @returns The result of the evaluation */ args.forEach((arg) => { - tryEvaluate( - this.app, - this.app.universes[this.app.selected_universe].locals[arg] - ); + if (arg >= 1 && arg <= 9) { + blinkScript(this.app, "local", arg); + tryEvaluate( + this.app, + this.app.universes[this.app.selected_universe].locals[arg] + ); + } }); }; s = this.script; diff --git a/src/AudioVisualisation.ts b/src/AudioVisualisation.ts index 423fb51..307d760 100644 --- a/src/AudioVisualisation.ts +++ b/src/AudioVisualisation.ts @@ -1,2 +1,115 @@ // @ts-ignore import { analyser, getAnalyzerData } 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 = ( + 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(); +}; + +/** + * Blinks a script indicator circle. + * @param script - The type of script. + * @param no - The shift amount multiplier. + */ +export const blinkScript = ( + 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" + ); + }; + + /** + * Clears the circle at a given shift. + * @param shift - The pixel distance from the origin. + */ + const _clearBlinker = (shift: number) => { + 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 + 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 + ); + drawEmptyBlinkers(app); + }, blinkDuration); + } +}; + +/** + * Draws a series of 9 white circles. + * @param app - The Editor application context. + */ +export const drawEmptyBlinkers = (app: Editor) => { + for (let no = 1; no <= 9; no++) { + const shiftAmount = no * 25; + drawCircle( + app, + 50 + shiftAmount, + app.interface.feedback.clientHeight - 15, + 8, + "white" + ); + } +}; diff --git a/src/WindowBehavior.ts b/src/WindowBehavior.ts index fe3a6bd..84a966f 100644 --- a/src/WindowBehavior.ts +++ b/src/WindowBehavior.ts @@ -1,10 +1,28 @@ import { type Editor } from "./main"; +const handleResize = (app: Editor) => { + const canvas = app.interface.feedback as HTMLCanvasElement | null; // add type guard + if (!canvas) return; + canvas.width = window.innerWidth; + canvas.height = window.innerHeight; + const ctx = canvas.getContext("2d"); + const dpr = window.devicePixelRatio || 1; + + // Assuming the canvas takes up the whole window + canvas.width = window.innerWidth * dpr; + canvas.height = window.innerHeight * dpr; + + if (ctx) { + ctx.scale(dpr, dpr); + } +}; + export const installWindowBehaviors = ( app: Editor, window: Window, preventMultipleTabs: boolean = false ) => { + window.addEventListener("resize", () => handleResize(app)); window.addEventListener("beforeunload", () => { // @ts-ignore event.preventDefault(); diff --git a/src/main.ts b/src/main.ts index a9064b1..52f612e 100644 --- a/src/main.ts +++ b/src/main.ts @@ -24,6 +24,7 @@ import showdown from "showdown"; import { makeStringExtensions } from "./StringExtensions"; import { installInterfaceLogic } from "./InterfaceLogic"; import { installWindowBehaviors } from "./WindowBehavior"; +import { drawEmptyBlinkers } from "./AudioVisualisation"; export class Editor { // Universes and settings @@ -56,6 +57,7 @@ export class Editor { show_error: boolean = false; buttonElements: Record = {}; interface: ElementMap = {}; + blinkTimeouts: Record = {}; // UserAPI api: UserAPI; @@ -79,6 +81,7 @@ export class Editor { this.initializeElements(); this.initializeButtonGroups(); this.initializeHydra(); + this.setCanvas(); // ================================================================================ // Loading the universe from local storage @@ -127,6 +130,7 @@ export class Editor { registerFillKeys(this); registerOnKeyDown(this); installInterfaceLogic(this); + drawEmptyBlinkers(this); // ================================================================================ // Building CodeMirror Editor @@ -381,25 +385,36 @@ export class Editor { } /** - * @param color the color to flash the background - * @param duration the duration of the flash + * 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 { - // Set the flashing color - this.view.dom.style.backgroundColor = color; - const gutters = this.view.dom.getElementsByClassName( + const domElement = this.view.dom; + const gutters = domElement.getElementsByClassName( "cm-gutter" ) as HTMLCollectionOf; + + domElement.classList.add("fluid-bg-transition"); + Array.from(gutters).forEach((gutter) => + gutter.classList.add("fluid-bg-transition") + ); + + domElement.style.backgroundColor = color; Array.from(gutters).forEach( (gutter) => (gutter.style.backgroundColor = color) ); - // Reset to original color after duration setTimeout(() => { - this.view.dom.style.backgroundColor = ""; + domElement.style.backgroundColor = ""; Array.from(gutters).forEach( (gutter) => (gutter.style.backgroundColor = "") ); + + domElement.classList.remove("fluid-bg-transition"); + Array.from(gutters).forEach((gutter) => + gutter.classList.remove("fluid-bg-transition") + ); }, duration); } @@ -428,6 +443,23 @@ export class Editor { }); this.hydra = this.hydra_backend.synth; } + + private setCanvas(): void { + const canvas = this.interface.feedback as HTMLCanvasElement | null; // add type guard + + 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; + canvas.height = window.innerHeight * dpr; + + if (ctx) { + ctx.scale(dpr, dpr); + } + } } let app = new Editor(); From 816429b9fa1984b6c254eb3a8ed761b6ee1acb8c Mon Sep 17 00:00:00 2001 From: Raphael Forment Date: Sun, 22 Oct 2023 22:49:56 +0200 Subject: [PATCH 03/10] oscilloscope prototype --- src/API.ts | 13 ++++++- src/AudioVisualisation.ts | 78 ++++++++++++++++++++++++++++++++++++++- src/TransportNode.js | 8 ++-- src/classes/SoundEvent.ts | 67 +++++++++++++++++++++------------ src/main.ts | 10 +++++ 5 files changed, 147 insertions(+), 29 deletions(-) diff --git a/src/API.ts b/src/API.ts index 9367af5..19ba16a 100644 --- a/src/API.ts +++ b/src/API.ts @@ -26,7 +26,7 @@ import { } from "superdough"; import { Speaker } from "./StringExtensions"; import { getScaleNotes } from "zifferjs"; -import { blinkScript } from "./AudioVisualisation"; +import { OscilloscopeConfig, blinkScript } from "./AudioVisualisation"; interface ControlChange { channel: number; @@ -1968,4 +1968,15 @@ export class UserAPI { */ return array.concat(array.slice(0, array.length - 1).reverse()); }; + + // ============================================================= + // Oscilloscope Configuration + // ============================================================= + public scope = (config: OscilloscopeConfig): void => { + /** + * Configures the oscilloscope. + * @param config - The configuration object + */ + this.app.oscilloscope_config = config; + }; } diff --git a/src/AudioVisualisation.ts b/src/AudioVisualisation.ts index 307d760..7232edb 100644 --- a/src/AudioVisualisation.ts +++ b/src/AudioVisualisation.ts @@ -1,5 +1,5 @@ // @ts-ignore -import { analyser, getAnalyzerData } from "superdough"; +import { getAnalyser } from "superdough"; import { type Editor } from "./main"; /** @@ -113,3 +113,79 @@ export const drawEmptyBlinkers = (app: Editor) => { ); } }; + +export interface OscilloscopeConfig { + enabled: boolean; + color: string; + thickness: number; + fftSize: number; // multiples of 256 + orientation: "horizontal" | "vertical"; + is3D: boolean; +} + +/** + * 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 +): void => { + let config = app.oscilloscope_config; + let analyzer = getAnalyser(config.fftSize); + let dataArray = new Float32Array(analyzer.frequencyBinCount); + const canvasCtx = canvas.getContext("2d")!; + const WIDTH = canvas.width; + const HEIGHT = canvas.height; + + function draw() { + if (!app.oscilloscope_config.enabled) { + canvasCtx.clearRect(0, 0, WIDTH, HEIGHT); + return; + } + + // Update analyzer and dataArray if fftSize changes + if (analyzer.fftSize !== app.oscilloscope_config.fftSize) { + analyzer = getAnalyser(app.oscilloscope_config.fftSize); + dataArray = new Float32Array(analyzer.frequencyBinCount); + } + + requestAnimationFrame(draw); + analyzer.getFloatTimeDomainData(dataArray); + + canvasCtx.fillStyle = "rgba(0, 0, 0, 0)"; + canvasCtx.fillRect(0, 0, WIDTH, HEIGHT); + canvasCtx.clearRect(0, 0, WIDTH, HEIGHT); + + canvasCtx.lineWidth = app.oscilloscope_config.thickness; + canvasCtx.strokeStyle = app.oscilloscope_config.color; + canvasCtx.beginPath(); + + // Drawing logic varies based on orientation and 3D setting + if (app.oscilloscope_config.is3D) { + // For demonstration, assume dataArray alternates between left and right channel + for (let i = 0; i < dataArray.length; i += 2) { + const x = dataArray[i] * WIDTH + WIDTH / 2; + const y = dataArray[i + 1] * HEIGHT + HEIGHT / 2; + i === 0 ? canvasCtx.moveTo(x, y) : canvasCtx.lineTo(x, y); + } + } else if (app.oscilloscope_config.orientation === "horizontal") { + let x = 0; + const sliceWidth = (WIDTH * 1.0) / dataArray.length; + for (let i = 0; i < dataArray.length; i++) { + const v = dataArray[i] * 0.5 * HEIGHT; + const y = v + HEIGHT / 2; + i === 0 ? canvasCtx.moveTo(x, y) : canvasCtx.lineTo(x, y); + x += sliceWidth; + } + canvasCtx.lineTo(WIDTH, HEIGHT / 2); + } else { + // Vertical drawing logic + } + + canvasCtx.stroke(); + } + + draw(); +}; diff --git a/src/TransportNode.js b/src/TransportNode.js index 12d9e66..32e6939 100644 --- a/src/TransportNode.js +++ b/src/TransportNode.js @@ -13,14 +13,16 @@ export class TransportNode extends AudioWorkletNode { /** @type {(this: MessagePort, ev: MessageEvent) => any} */ handleMessage = (message) => { if (message.data && message.data.type === "bang") { - if (this.app.settings.send_clock) this.app.api.MidiConnection.sendMidiClock(); + if (this.app.settings.send_clock) + this.app.api.MidiConnection.sendMidiClock(); this.app.clock.tick++; const futureTimeStamp = this.app.clock.convertTicksToTimeposition( this.app.clock.tick ); this.app.clock.time_position = futureTimeStamp; - this.timeviewer.innerHTML = `${zeroPad(futureTimeStamp.bar, 2)}:${futureTimeStamp.beat + 1 - }:${zeroPad(futureTimeStamp.pulse, 2)} / ${this.app.clock.bpm}`; + this.timeviewer.innerHTML = `${zeroPad(futureTimeStamp.bar, 2)}:${ + futureTimeStamp.beat + 1 + }:${zeroPad(futureTimeStamp.pulse, 2)} / ${this.app.clock.bpm}`; if (this.app.exampleIsPlaying) { tryEvaluate(this.app, this.app.example_buffer); } else { diff --git a/src/classes/SoundEvent.ts b/src/classes/SoundEvent.ts index 28968fc..213ea1e 100644 --- a/src/classes/SoundEvent.ts +++ b/src/classes/SoundEvent.ts @@ -1,6 +1,11 @@ import { type Editor } from "../main"; import { AudibleEvent } from "./AbstractEvents"; -import { chord as parseChord, midiToFreq, noteFromPc, noteNameToMidi } from "zifferjs"; +import { + chord as parseChord, + midiToFreq, + noteFromPc, + noteNameToMidi, +} from "zifferjs"; import { superdough, @@ -10,10 +15,9 @@ import { export type SoundParams = { dur: number; s?: string; -} +}; export class SoundEvent extends AudibleEvent { - nudge: number; constructor(sound: string | object, public app: Editor) { @@ -25,9 +29,10 @@ export class SoundEvent extends AudibleEvent { s: sound.split(":")[0], n: sound.split(":")[1], dur: app.clock.convertPulseToSecond(app.clock.ppqn), + analyze: true, }; } else { - this.values = { s: sound, dur: 0.5 }; + this.values = { s: sound, dur: 0.5, analyze: true }; } } else { this.values = sound; @@ -116,7 +121,7 @@ export class SoundEvent extends AudibleEvent { this.sustain(0.0); this.release(0.0); return this; - } + }; // Lowpass filter public lpenv = (value: number) => this.updateValue("lpenv", value); @@ -247,30 +252,43 @@ export class SoundEvent extends AudibleEvent { // Frequency management public sound = (value: string) => this.updateValue("s", value); - public chord = (value: string | object[] | number[] | number, ...kwargs: number[]) => { + public chord = ( + value: string | object[] | number[] | number, + ...kwargs: number[] + ) => { if (typeof value === "string") { const chord = parseChord(value); - value = chord.map((note: number) => { return { note: note, freq: midiToFreq(note) } }); + value = chord.map((note: number) => { + return { note: note, freq: midiToFreq(note) }; + }); } else if (value instanceof Array && typeof value[0] === "number") { - value = (value as number[]).map((note: number) => { return { note: note, freq: midiToFreq(note) } }); + value = (value as number[]).map((note: number) => { + return { note: note, freq: midiToFreq(note) }; + }); } else if (typeof value === "number" && kwargs.length > 0) { - value = [value, ...kwargs].map((note: number) => { return { note: note, freq: midiToFreq(note) } }); + value = [value, ...kwargs].map((note: number) => { + return { note: note, freq: midiToFreq(note) }; + }); } return this.updateValue("chord", value); - } + }; public invert = (howMany: number = 0) => { if (this.values.chord) { - let notes = this.values.chord.map((obj: { [key: string]: number }) => obj.note); + 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) } }); + const chord = notes.map((note: number) => { + return { note: note, freq: midiToFreq(note) }; + }); return this.updateValue("chord", chord); } else { return this; } - } + }; public snd = this.sound; public cut = (value: number) => this.updateValue("cut", value); public clip = (value: number) => this.updateValue("clip", value); @@ -309,7 +327,7 @@ export class SoundEvent extends AudibleEvent { // Reverb management public room = (value: number) => this.updateValue("room", value); - public rm = this.room + public rm = this.room; public roomfade = (value: number) => this.updateValue("roomfade", value); public rfade = this.roomfade; public roomlp = (value: number) => this.updateValue("roomlp", value); @@ -320,25 +338,26 @@ export class SoundEvent extends AudibleEvent { public sz = this.size; // Compressor - public comp = (value: number) => this.updateValue('compressor', value); + public comp = (value: number) => this.updateValue("compressor", value); public cmp = this.comp; - public ratio = (value: number) => this.updateValue('compressorRatio', value); + public ratio = (value: number) => this.updateValue("compressorRatio", value); public rt = this.ratio; - public knee = (value: number) => this.updateValue('compressorKnee', value); + public knee = (value: number) => this.updateValue("compressorKnee", value); public kn = this.knee; - public compAttack = (value: number) => this.updateValue('compressorAttack', value); + public compAttack = (value: number) => + this.updateValue("compressorAttack", value); public cmpa = this.compAttack; - public compRelease = (value: number) => this.updateValue('compressorRelease', value); + public compRelease = (value: number) => + this.updateValue("compressorRelease", value); public cmpr = this.compRelease; - // Unit public stretch = (beat: number) => { this.updateValue("unit", "c"); - this.updateValue("speed", 1 / beat) - this.updateValue("cut", beat) + this.updateValue("speed", 1 / beat); + this.updateValue("cut", beat); return this; - } + }; // ================================================================================ // AbstactEvent overrides @@ -368,7 +387,7 @@ export class SoundEvent extends AudibleEvent { if (this.values.chord) { this.values.chord.forEach((obj: { [key: string]: number }) => { const copy = { ...this.values }; - copy.freq = obj.freq + copy.freq = obj.freq; superdough(copy, this.nudge, this.values.dur); }); } else { diff --git a/src/main.ts b/src/main.ts index 52f612e..ef8058e 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,3 +1,4 @@ +import { OscilloscopeConfig, runOscilloscope } from "./AudioVisualisation"; import { EditorState, Compartment } from "@codemirror/state"; import { javascript } from "@codemirror/lang-javascript"; import { markdown } from "@codemirror/lang-markdown"; @@ -58,6 +59,14 @@ export class Editor { buttonElements: Record = {}; interface: ElementMap = {}; blinkTimeouts: Record = {}; + oscilloscope_config: OscilloscopeConfig = { + enabled: true, + color: "#fdba74", + thickness: 2, + fftSize: 2048, + orientation: "horizontal", + is3D: true, + }; // UserAPI api: UserAPI; @@ -137,6 +146,7 @@ export class Editor { // ================================================================================ installEditor(this); + runOscilloscope(this.interface.feedback as HTMLCanvasElement, this); // First evaluation of the init file tryEvaluate(this, this.universes[this.selected_universe.toString()].init); From 9ca8853539a07e0e9eafa4b43981b0d580f3b08b Mon Sep 17 00:00:00 2001 From: Raphael Forment Date: Sun, 22 Oct 2023 23:23:19 +0200 Subject: [PATCH 04/10] more work on oscilloscope --- index.html | 2 +- src/API.ts | 2 +- src/AudioVisualisation.ts | 35 ++++++++++++++++++++++------------- src/main.ts | 4 +++- 4 files changed, 27 insertions(+), 16 deletions(-) diff --git a/index.html b/index.html index 11a7f62..e394563 100644 --- a/index.html +++ b/index.html @@ -465,8 +465,8 @@
- +
diff --git a/src/API.ts b/src/API.ts index 19ba16a..92cff71 100644 --- a/src/API.ts +++ b/src/API.ts @@ -1977,6 +1977,6 @@ export class UserAPI { * Configures the oscilloscope. * @param config - The configuration object */ - this.app.oscilloscope_config = config; + this.app.osc = config; }; } diff --git a/src/AudioVisualisation.ts b/src/AudioVisualisation.ts index 7232edb..3bef205 100644 --- a/src/AudioVisualisation.ts +++ b/src/AudioVisualisation.ts @@ -121,6 +121,7 @@ export interface OscilloscopeConfig { fftSize: number; // multiples of 256 orientation: "horizontal" | "vertical"; is3D: boolean; + size: number; } /** @@ -132,7 +133,7 @@ export const runOscilloscope = ( canvas: HTMLCanvasElement, app: Editor ): void => { - let config = app.oscilloscope_config; + let config = app.osc; let analyzer = getAnalyser(config.fftSize); let dataArray = new Float32Array(analyzer.frequencyBinCount); const canvasCtx = canvas.getContext("2d")!; @@ -140,14 +141,14 @@ export const runOscilloscope = ( const HEIGHT = canvas.height; function draw() { - if (!app.oscilloscope_config.enabled) { + if (!app.osc.enabled) { canvasCtx.clearRect(0, 0, WIDTH, HEIGHT); return; } // Update analyzer and dataArray if fftSize changes - if (analyzer.fftSize !== app.oscilloscope_config.fftSize) { - analyzer = getAnalyser(app.oscilloscope_config.fftSize); + if (analyzer.fftSize !== app.osc.fftSize) { + analyzer = getAnalyser(app.osc.fftSize); dataArray = new Float32Array(analyzer.frequencyBinCount); } @@ -158,28 +159,36 @@ export const runOscilloscope = ( canvasCtx.fillRect(0, 0, WIDTH, HEIGHT); canvasCtx.clearRect(0, 0, WIDTH, HEIGHT); - canvasCtx.lineWidth = app.oscilloscope_config.thickness; - canvasCtx.strokeStyle = app.oscilloscope_config.color; + canvasCtx.lineWidth = app.osc.thickness; + + if (app.osc.color === "random") { + if (app.clock.time_position.pulse % 16 === 0) { + canvasCtx.strokeStyle = `hsl(${Math.random() * 360}, 100%, 50%)`; + } + } else { + canvasCtx.strokeStyle = app.osc.color; + } canvasCtx.beginPath(); // Drawing logic varies based on orientation and 3D setting - if (app.oscilloscope_config.is3D) { + if (app.osc.is3D) { // For demonstration, assume dataArray alternates between left and right channel for (let i = 0; i < dataArray.length; i += 2) { - const x = dataArray[i] * WIDTH + WIDTH / 2; - const y = dataArray[i + 1] * HEIGHT + HEIGHT / 2; + const x = (dataArray[i] * WIDTH * app.osc.size) / 2 + WIDTH / 4; + const y = (dataArray[i + 1] * HEIGHT * app.osc.size) / 2 + HEIGHT / 4; i === 0 ? canvasCtx.moveTo(x, y) : canvasCtx.lineTo(x, y); } - } else if (app.oscilloscope_config.orientation === "horizontal") { + } else if (app.osc.orientation === "horizontal") { let x = 0; const sliceWidth = (WIDTH * 1.0) / dataArray.length; + const yOffset = HEIGHT / 4; // Adjust this to move the oscilloscope up for (let i = 0; i < dataArray.length; i++) { - const v = dataArray[i] * 0.5 * HEIGHT; - const y = v + HEIGHT / 2; + const v = dataArray[i] * 0.5 * HEIGHT * app.osc.size; + const y = v + yOffset; i === 0 ? canvasCtx.moveTo(x, y) : canvasCtx.lineTo(x, y); x += sliceWidth; } - canvasCtx.lineTo(WIDTH, HEIGHT / 2); + canvasCtx.lineTo(WIDTH, yOffset); } else { // Vertical drawing logic } diff --git a/src/main.ts b/src/main.ts index ef8058e..e65ffc8 100644 --- a/src/main.ts +++ b/src/main.ts @@ -59,13 +59,15 @@ export class Editor { buttonElements: Record = {}; interface: ElementMap = {}; blinkTimeouts: Record = {}; - oscilloscope_config: OscilloscopeConfig = { + osc: OscilloscopeConfig = { enabled: true, color: "#fdba74", + randomColor: true, thickness: 2, fftSize: 2048, orientation: "horizontal", is3D: true, + size: 1, }; // UserAPI From b2f77b3b384f6d393e52e4a10fc4efebc42036b4 Mon Sep 17 00:00:00 2001 From: Raphael Forment Date: Sun, 22 Oct 2023 23:28:08 +0200 Subject: [PATCH 05/10] adding another canvas --- index.html | 21 ++++----------------- src/DomElements.ts | 1 + src/WindowBehavior.ts | 10 +++++++--- src/main.ts | 10 ++++------ 4 files changed, 16 insertions(+), 26 deletions(-) diff --git a/index.html b/index.html index e394563..1c161cd 100644 --- a/index.html +++ b/index.html @@ -27,7 +27,7 @@ } - #hydra-bg { + .fullscreencanvas { position: fixed; /* ignore margins */ top: 0px; left: 0px; @@ -39,20 +39,6 @@ display: block; } - #feedback { - position: fixed; /* ignore margins */ - top: 0px; - left: 0px; - width: 100%; /* fill screen */ - height: 100%; - background-size: cover; - overflow-y: hidden; - z-index: -1; /* place behind everything else */ - display: block; - } - - - details br { display: none; } @@ -465,8 +451,9 @@
- - + + +
diff --git a/src/DomElements.ts b/src/DomElements.ts index f5d8c57..de5b0ce 100644 --- a/src/DomElements.ts +++ b/src/DomElements.ts @@ -48,6 +48,7 @@ export const singleElements = { error_line: "error_line", hydra_canvas: "hydra-bg", feedback: "feedback", + scope: "scope", }; export const buttonGroups = { diff --git a/src/WindowBehavior.ts b/src/WindowBehavior.ts index 84a966f..2ddba53 100644 --- a/src/WindowBehavior.ts +++ b/src/WindowBehavior.ts @@ -1,7 +1,6 @@ import { type Editor } from "./main"; -const handleResize = (app: Editor) => { - const canvas = app.interface.feedback as HTMLCanvasElement | null; // add type guard +const handleResize = (canvas: HTMLCanvasElement) => { if (!canvas) return; canvas.width = window.innerWidth; canvas.height = window.innerHeight; @@ -22,7 +21,12 @@ export const installWindowBehaviors = ( window: Window, preventMultipleTabs: boolean = false ) => { - window.addEventListener("resize", () => handleResize(app)); + window.addEventListener("resize", () => + handleResize(app.interface.scope as HTMLCanvasElement) + ); + window.addEventListener("resize", () => + handleResize(app.interface.feedback as HTMLCanvasElement) + ); window.addEventListener("beforeunload", () => { // @ts-ignore event.preventDefault(); diff --git a/src/main.ts b/src/main.ts index e65ffc8..7d796be 100644 --- a/src/main.ts +++ b/src/main.ts @@ -62,7 +62,6 @@ export class Editor { osc: OscilloscopeConfig = { enabled: true, color: "#fdba74", - randomColor: true, thickness: 2, fftSize: 2048, orientation: "horizontal", @@ -92,7 +91,8 @@ export class Editor { this.initializeElements(); this.initializeButtonGroups(); this.initializeHydra(); - this.setCanvas(); + this.setCanvas(this.interface.feedback as HTMLCanvasElement); + this.setCanvas(this.interface.scope as HTMLCanvasElement); // ================================================================================ // Loading the universe from local storage @@ -148,7 +148,7 @@ export class Editor { // ================================================================================ installEditor(this); - runOscilloscope(this.interface.feedback as HTMLCanvasElement, this); + runOscilloscope(this.interface.scope as HTMLCanvasElement, this); // First evaluation of the init file tryEvaluate(this, this.universes[this.selected_universe.toString()].init); @@ -456,9 +456,7 @@ export class Editor { this.hydra = this.hydra_backend.synth; } - private setCanvas(): void { - const canvas = this.interface.feedback as HTMLCanvasElement | null; // add type guard - + private setCanvas(canvas: HTMLCanvasElement): void { if (!canvas) return; const ctx = canvas.getContext("2d"); From aaffacb265e68098907e242aefe40a0100ee9256 Mon Sep 17 00:00:00 2001 From: Raphael Forment Date: Sun, 22 Oct 2023 23:38:57 +0200 Subject: [PATCH 06/10] tweaking defaults --- src/API.ts | 6 +++++- src/AudioVisualisation.ts | 2 +- src/main.ts | 6 +++--- 3 files changed, 9 insertions(+), 5 deletions(-) diff --git a/src/API.ts b/src/API.ts index 92cff71..74521f4 100644 --- a/src/API.ts +++ b/src/API.ts @@ -26,7 +26,11 @@ import { } from "superdough"; import { Speaker } from "./StringExtensions"; import { getScaleNotes } from "zifferjs"; -import { OscilloscopeConfig, blinkScript } from "./AudioVisualisation"; +import { + OscilloscopeConfig, + blinkScript, + runOscilloscope, +} from "./AudioVisualisation"; interface ControlChange { channel: number; diff --git a/src/AudioVisualisation.ts b/src/AudioVisualisation.ts index 3bef205..64c3145 100644 --- a/src/AudioVisualisation.ts +++ b/src/AudioVisualisation.ts @@ -141,6 +141,7 @@ export const runOscilloscope = ( const HEIGHT = canvas.height; function draw() { + requestAnimationFrame(draw); if (!app.osc.enabled) { canvasCtx.clearRect(0, 0, WIDTH, HEIGHT); return; @@ -152,7 +153,6 @@ export const runOscilloscope = ( dataArray = new Float32Array(analyzer.frequencyBinCount); } - requestAnimationFrame(draw); analyzer.getFloatTimeDomainData(dataArray); canvasCtx.fillStyle = "rgba(0, 0, 0, 0)"; diff --git a/src/main.ts b/src/main.ts index 7d796be..471b9c0 100644 --- a/src/main.ts +++ b/src/main.ts @@ -62,10 +62,10 @@ export class Editor { osc: OscilloscopeConfig = { enabled: true, color: "#fdba74", - thickness: 2, - fftSize: 2048, + thickness: 4, + fftSize: 256, orientation: "horizontal", - is3D: true, + is3D: false, size: 1, }; From 70b44cbd29e93a70d3014e00bd3a860ec442fc70 Mon Sep 17 00:00:00 2001 From: Raphael Forment Date: Sun, 22 Oct 2023 23:53:57 +0200 Subject: [PATCH 07/10] adding new doc page --- index.html | 1 + src/API.ts | 9 ++++++++- src/Documentation.ts | 2 ++ src/InterfaceLogic.ts | 1 + src/documentation/oscilloscope.ts | 8 ++++++++ 5 files changed, 20 insertions(+), 1 deletion(-) create mode 100644 src/documentation/oscilloscope.ts diff --git a/index.html b/index.html index 1c161cd..1d47cc7 100644 --- a/index.html +++ b/index.html @@ -164,6 +164,7 @@

More

diff --git a/src/API.ts b/src/API.ts index 74521f4..d42333e 100644 --- a/src/API.ts +++ b/src/API.ts @@ -1976,11 +1976,18 @@ export class UserAPI { // ============================================================= // Oscilloscope Configuration // ============================================================= + public scope = (config: OscilloscopeConfig): void => { /** * Configures the oscilloscope. * @param config - The configuration object */ - this.app.osc = config; + // Dispatch the config to the old object so that missing options + // are still specified + + this.app.osc = { + ...this.app.osc, + ...config, + }; }; } diff --git a/src/Documentation.ts b/src/Documentation.ts index 114d9e3..a2dea88 100644 --- a/src/Documentation.ts +++ b/src/Documentation.ts @@ -1,5 +1,6 @@ import { type Editor } from "./main"; import { introduction } from "./documentation/introduction"; +import { oscilloscope } from "./documentation/oscilloscope"; import { samples } from "./documentation/samples"; import { chaining } from "./documentation/chaining"; import { software_interface } from "./documentation/interface"; @@ -85,6 +86,7 @@ export const documentation_factory = (application: Editor) => { functions: functions(application), reference: reference(), shortcuts: shortcuts(), + oscilloscope: oscilloscope(application), bonus: bonus(application), about: about(), }; diff --git a/src/InterfaceLogic.ts b/src/InterfaceLogic.ts index 867020b..85e2eac 100644 --- a/src/InterfaceLogic.ts +++ b/src/InterfaceLogic.ts @@ -451,6 +451,7 @@ export const installInterfaceLogic = (app: Editor) => { "shortcuts", "about", "bonus", + "oscilloscope", ].forEach((e) => { let name = `docs_` + e; document.getElementById(name)!.addEventListener("click", async () => { diff --git a/src/documentation/oscilloscope.ts b/src/documentation/oscilloscope.ts new file mode 100644 index 0000000..3b1dda6 --- /dev/null +++ b/src/documentation/oscilloscope.ts @@ -0,0 +1,8 @@ +import { type Editor } from "../main"; +import { makeExampleFactory } from "../Documentation"; + +export const oscilloscope = (application: Editor): string => { + const makeExample = makeExampleFactory(application); + return `# Oscilloscope + `; +}; From 3900f5eef510f62667623fb04a12c936df0f969a Mon Sep 17 00:00:00 2001 From: Raphael Forment Date: Mon, 23 Oct 2023 00:05:55 +0200 Subject: [PATCH 08/10] flesh out oscilloscope documentation --- src/AudioVisualisation.ts | 12 ++++++++++-- src/documentation/oscilloscope.ts | 21 +++++++++++++++++++++ 2 files changed, 31 insertions(+), 2 deletions(-) diff --git a/src/AudioVisualisation.ts b/src/AudioVisualisation.ts index 64c3145..b28fcae 100644 --- a/src/AudioVisualisation.ts +++ b/src/AudioVisualisation.ts @@ -172,7 +172,6 @@ export const runOscilloscope = ( // Drawing logic varies based on orientation and 3D setting if (app.osc.is3D) { - // For demonstration, assume dataArray alternates between left and right channel for (let i = 0; i < dataArray.length; i += 2) { const x = (dataArray[i] * WIDTH * app.osc.size) / 2 + WIDTH / 4; const y = (dataArray[i + 1] * HEIGHT * app.osc.size) / 2 + HEIGHT / 4; @@ -190,7 +189,16 @@ export const runOscilloscope = ( } canvasCtx.lineTo(WIDTH, yOffset); } else { - // Vertical drawing logic + let y = 0; + const sliceHeight = (HEIGHT * 1.0) / dataArray.length; + const xOffset = WIDTH / 4; // Adjust this to move the oscilloscope to the side + for (let i = 0; i < dataArray.length; i++) { + const v = dataArray[i] * 0.5 * WIDTH * app.osc.size; + const x = v + xOffset; + i === 0 ? canvasCtx.moveTo(x, y) : canvasCtx.lineTo(x, y); + y += sliceHeight; + } + canvasCtx.lineTo(xOffset, HEIGHT); } canvasCtx.stroke(); diff --git a/src/documentation/oscilloscope.ts b/src/documentation/oscilloscope.ts index 3b1dda6..4d1f371 100644 --- a/src/documentation/oscilloscope.ts +++ b/src/documentation/oscilloscope.ts @@ -4,5 +4,26 @@ import { makeExampleFactory } from "../Documentation"; 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. + +${makeExample( + "Oscilloscope configuration", + ` +scope({ + enabled: true, // off by default + color: "#fdba74", // any valid CSS color or "random" + thickness: 4, // stroke thickness + fftSize: 256, // multiples of 128 + orientation: "horizontal", // "vertical" or "horizontal" + is3D: false, // 3D oscilloscope + size: 1, // size of the oscilloscope +}) + `, + 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 3c1b0a32852dddf52c4cb222cb906c50348fe879 Mon Sep 17 00:00:00 2001 From: Raphael Forment Date: Mon, 23 Oct 2023 00:22:38 +0200 Subject: [PATCH 09/10] cleaning --- src/AudioVisualisation.ts | 5 ++--- src/main.ts | 2 +- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/src/AudioVisualisation.ts b/src/AudioVisualisation.ts index b28fcae..5ce3192 100644 --- a/src/AudioVisualisation.ts +++ b/src/AudioVisualisation.ts @@ -147,7 +147,6 @@ export const runOscilloscope = ( return; } - // Update analyzer and dataArray if fftSize changes if (analyzer.fftSize !== app.osc.fftSize) { analyzer = getAnalyser(app.osc.fftSize); dataArray = new Float32Array(analyzer.frequencyBinCount); @@ -180,7 +179,7 @@ export const runOscilloscope = ( } else if (app.osc.orientation === "horizontal") { let x = 0; const sliceWidth = (WIDTH * 1.0) / dataArray.length; - const yOffset = HEIGHT / 4; // Adjust this to move the oscilloscope up + const yOffset = HEIGHT / 4; for (let i = 0; i < dataArray.length; i++) { const v = dataArray[i] * 0.5 * HEIGHT * app.osc.size; const y = v + yOffset; @@ -191,7 +190,7 @@ export const runOscilloscope = ( } else { let y = 0; const sliceHeight = (HEIGHT * 1.0) / dataArray.length; - const xOffset = WIDTH / 4; // Adjust this to move the oscilloscope to the side + const xOffset = WIDTH / 4; for (let i = 0; i < dataArray.length; i++) { const v = dataArray[i] * 0.5 * WIDTH * app.osc.size; const x = v + xOffset; diff --git a/src/main.ts b/src/main.ts index 471b9c0..c7a9ac8 100644 --- a/src/main.ts +++ b/src/main.ts @@ -60,7 +60,7 @@ export class Editor { interface: ElementMap = {}; blinkTimeouts: Record = {}; osc: OscilloscopeConfig = { - enabled: true, + enabled: false, color: "#fdba74", thickness: 4, fftSize: 256, From 07f33a0126753fabb99d7955b1bcb9aad7ed13eb Mon Sep 17 00:00:00 2001 From: Raphael Forment Date: Mon, 23 Oct 2023 00:23:44 +0200 Subject: [PATCH 10/10] second round of cleaning --- src/API.ts | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/src/API.ts b/src/API.ts index d42333e..d621df5 100644 --- a/src/API.ts +++ b/src/API.ts @@ -26,11 +26,7 @@ import { } from "superdough"; import { Speaker } from "./StringExtensions"; import { getScaleNotes } from "zifferjs"; -import { - OscilloscopeConfig, - blinkScript, - runOscilloscope, -} from "./AudioVisualisation"; +import { OscilloscopeConfig, blinkScript } from "./AudioVisualisation"; interface ControlChange { channel: number; @@ -1982,9 +1978,6 @@ export class UserAPI { * Configures the oscilloscope. * @param config - The configuration object */ - // Dispatch the config to the old object so that missing options - // are still specified - this.app.osc = { ...this.app.osc, ...config,