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";
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;
};
}

View File

@ -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();
};

View File

@ -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 {

View File

@ -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 {

View File

@ -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);