oscilloscope prototype
This commit is contained in:
13
src/API.ts
13
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;
|
||||
};
|
||||
}
|
||||
|
||||
@ -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();
|
||||
};
|
||||
|
||||
@ -13,14 +13,16 @@ export class TransportNode extends AudioWorkletNode {
|
||||
/** @type {(this: MessagePort, ev: MessageEvent<any>) => 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 {
|
||||
|
||||
@ -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 {
|
||||
|
||||
10
src/main.ts
10
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<string, HTMLButtonElement[]> = {};
|
||||
interface: ElementMap = {};
|
||||
blinkTimeouts: Record<number, number> = {};
|
||||
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);
|
||||
|
||||
Reference in New Issue
Block a user