From 816429b9fa1984b6c254eb3a8ed761b6ee1acb8c Mon Sep 17 00:00:00 2001 From: Raphael Forment Date: Sun, 22 Oct 2023 22:49:56 +0200 Subject: [PATCH] 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);