oscilloscope prototype

This commit is contained in:
2023-10-22 22:49:56 +02:00
parent d6577718a6
commit 816429b9fa
5 changed files with 147 additions and 29 deletions

View File

@ -26,7 +26,7 @@ import {
} from "superdough"; } from "superdough";
import { Speaker } from "./StringExtensions"; import { Speaker } from "./StringExtensions";
import { getScaleNotes } from "zifferjs"; import { getScaleNotes } from "zifferjs";
import { blinkScript } from "./AudioVisualisation"; import { OscilloscopeConfig, blinkScript } from "./AudioVisualisation";
interface ControlChange { interface ControlChange {
channel: number; channel: number;
@ -1968,4 +1968,15 @@ export class UserAPI {
*/ */
return array.concat(array.slice(0, array.length - 1).reverse()); 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;
};
} }

View File

@ -1,5 +1,5 @@
// @ts-ignore // @ts-ignore
import { analyser, getAnalyzerData } from "superdough"; import { getAnalyser } from "superdough";
import { type Editor } from "./main"; 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();
};

View File

@ -13,14 +13,16 @@ export class TransportNode extends AudioWorkletNode {
/** @type {(this: MessagePort, ev: MessageEvent<any>) => any} */ /** @type {(this: MessagePort, ev: MessageEvent<any>) => any} */
handleMessage = (message) => { handleMessage = (message) => {
if (message.data && message.data.type === "bang") { 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++; this.app.clock.tick++;
const futureTimeStamp = this.app.clock.convertTicksToTimeposition( const futureTimeStamp = this.app.clock.convertTicksToTimeposition(
this.app.clock.tick this.app.clock.tick
); );
this.app.clock.time_position = futureTimeStamp; this.app.clock.time_position = futureTimeStamp;
this.timeviewer.innerHTML = `${zeroPad(futureTimeStamp.bar, 2)}:${futureTimeStamp.beat + 1 this.timeviewer.innerHTML = `${zeroPad(futureTimeStamp.bar, 2)}:${
}:${zeroPad(futureTimeStamp.pulse, 2)} / ${this.app.clock.bpm}`; futureTimeStamp.beat + 1
}:${zeroPad(futureTimeStamp.pulse, 2)} / ${this.app.clock.bpm}`;
if (this.app.exampleIsPlaying) { if (this.app.exampleIsPlaying) {
tryEvaluate(this.app, this.app.example_buffer); tryEvaluate(this.app, this.app.example_buffer);
} else { } else {

View File

@ -1,6 +1,11 @@
import { type Editor } from "../main"; import { type Editor } from "../main";
import { AudibleEvent } from "./AbstractEvents"; import { AudibleEvent } from "./AbstractEvents";
import { chord as parseChord, midiToFreq, noteFromPc, noteNameToMidi } from "zifferjs"; import {
chord as parseChord,
midiToFreq,
noteFromPc,
noteNameToMidi,
} from "zifferjs";
import { import {
superdough, superdough,
@ -10,10 +15,9 @@ import {
export type SoundParams = { export type SoundParams = {
dur: number; dur: number;
s?: string; s?: string;
} };
export class SoundEvent extends AudibleEvent { export class SoundEvent extends AudibleEvent {
nudge: number; nudge: number;
constructor(sound: string | object, public app: Editor) { constructor(sound: string | object, public app: Editor) {
@ -25,9 +29,10 @@ export class SoundEvent extends AudibleEvent {
s: sound.split(":")[0], s: sound.split(":")[0],
n: sound.split(":")[1], n: sound.split(":")[1],
dur: app.clock.convertPulseToSecond(app.clock.ppqn), dur: app.clock.convertPulseToSecond(app.clock.ppqn),
analyze: true,
}; };
} else { } else {
this.values = { s: sound, dur: 0.5 }; this.values = { s: sound, dur: 0.5, analyze: true };
} }
} else { } else {
this.values = sound; this.values = sound;
@ -116,7 +121,7 @@ export class SoundEvent extends AudibleEvent {
this.sustain(0.0); this.sustain(0.0);
this.release(0.0); this.release(0.0);
return this; return this;
} };
// Lowpass filter // Lowpass filter
public lpenv = (value: number) => this.updateValue("lpenv", value); public lpenv = (value: number) => this.updateValue("lpenv", value);
@ -247,30 +252,43 @@ export class SoundEvent extends AudibleEvent {
// Frequency management // Frequency management
public sound = (value: string) => this.updateValue("s", value); 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") { if (typeof value === "string") {
const chord = parseChord(value); 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") { } 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) { } 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); return this.updateValue("chord", value);
} };
public invert = (howMany: number = 0) => { public invert = (howMany: number = 0) => {
if (this.values.chord) { 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; notes = howMany < 0 ? [...notes].reverse() : notes;
for (let i = 0; i < Math.abs(howMany); i++) { for (let i = 0; i < Math.abs(howMany); i++) {
notes[i % notes.length] += howMany <= 0 ? -12 : 12; 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); return this.updateValue("chord", chord);
} else { } else {
return this; return this;
} }
} };
public snd = this.sound; public snd = this.sound;
public cut = (value: number) => this.updateValue("cut", value); public cut = (value: number) => this.updateValue("cut", value);
public clip = (value: number) => this.updateValue("clip", value); public clip = (value: number) => this.updateValue("clip", value);
@ -309,7 +327,7 @@ export class SoundEvent extends AudibleEvent {
// Reverb management // Reverb management
public room = (value: number) => this.updateValue("room", value); 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 roomfade = (value: number) => this.updateValue("roomfade", value);
public rfade = this.roomfade; public rfade = this.roomfade;
public roomlp = (value: number) => this.updateValue("roomlp", value); public roomlp = (value: number) => this.updateValue("roomlp", value);
@ -320,25 +338,26 @@ export class SoundEvent extends AudibleEvent {
public sz = this.size; public sz = this.size;
// Compressor // Compressor
public comp = (value: number) => this.updateValue('compressor', value); public comp = (value: number) => this.updateValue("compressor", value);
public cmp = this.comp; 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 rt = this.ratio;
public knee = (value: number) => this.updateValue('compressorKnee', value); public knee = (value: number) => this.updateValue("compressorKnee", value);
public kn = this.knee; 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 cmpa = this.compAttack;
public compRelease = (value: number) => this.updateValue('compressorRelease', value); public compRelease = (value: number) =>
this.updateValue("compressorRelease", value);
public cmpr = this.compRelease; public cmpr = this.compRelease;
// Unit // Unit
public stretch = (beat: number) => { public stretch = (beat: number) => {
this.updateValue("unit", "c"); this.updateValue("unit", "c");
this.updateValue("speed", 1 / beat) this.updateValue("speed", 1 / beat);
this.updateValue("cut", beat) this.updateValue("cut", beat);
return this; return this;
} };
// ================================================================================ // ================================================================================
// AbstactEvent overrides // AbstactEvent overrides
@ -368,7 +387,7 @@ export class SoundEvent extends AudibleEvent {
if (this.values.chord) { if (this.values.chord) {
this.values.chord.forEach((obj: { [key: string]: number }) => { this.values.chord.forEach((obj: { [key: string]: number }) => {
const copy = { ...this.values }; const copy = { ...this.values };
copy.freq = obj.freq copy.freq = obj.freq;
superdough(copy, this.nudge, this.values.dur); superdough(copy, this.nudge, this.values.dur);
}); });
} else { } else {

View File

@ -1,3 +1,4 @@
import { OscilloscopeConfig, runOscilloscope } from "./AudioVisualisation";
import { EditorState, Compartment } from "@codemirror/state"; import { EditorState, Compartment } from "@codemirror/state";
import { javascript } from "@codemirror/lang-javascript"; import { javascript } from "@codemirror/lang-javascript";
import { markdown } from "@codemirror/lang-markdown"; import { markdown } from "@codemirror/lang-markdown";
@ -58,6 +59,14 @@ export class Editor {
buttonElements: Record<string, HTMLButtonElement[]> = {}; buttonElements: Record<string, HTMLButtonElement[]> = {};
interface: ElementMap = {}; interface: ElementMap = {};
blinkTimeouts: Record<number, number> = {}; blinkTimeouts: Record<number, number> = {};
oscilloscope_config: OscilloscopeConfig = {
enabled: true,
color: "#fdba74",
thickness: 2,
fftSize: 2048,
orientation: "horizontal",
is3D: true,
};
// UserAPI // UserAPI
api: UserAPI; api: UserAPI;
@ -137,6 +146,7 @@ export class Editor {
// ================================================================================ // ================================================================================
installEditor(this); installEditor(this);
runOscilloscope(this.interface.feedback as HTMLCanvasElement, this);
// First evaluation of the init file // First evaluation of the init file
tryEvaluate(this, this.universes[this.selected_universe.toString()].init); tryEvaluate(this, this.universes[this.selected_universe.toString()].init);