adding stuff and cleaning stuff

This commit is contained in:
2023-07-29 00:03:19 +02:00
parent 6f77cdf9a0
commit 1096414625
5 changed files with 191 additions and 219 deletions

View File

@ -1,130 +1,17 @@
import { Editor } from "./main"; import { Editor } from "./main";
import { tryEvaluate } from "./Evaluator"; import { tryEvaluate } from "./Evaluator";
import { BasicSynth, PercussionSynth } from "./WebSynth";
import { MidiConnection } from "./IO/MidiConnection";
import * as Tone from 'tone';
// @ts-ignore // @ts-ignore
import { ZZFX, zzfx } from "zzfx"; import { ZZFX, zzfx } from "zzfx";
class MidiConnection{
private midiAccess: MIDIAccess | null = null;
private midiOutputs: MIDIOutput[] = [];
private currentOutputIndex: number = 0;
private scheduledNotes: { [noteNumber: number]: number } = {}; // { noteNumber: timeoutId }
constructor() {
this.initializeMidiAccess();
}
private async initializeMidiAccess(): Promise<void> {
try {
this.midiAccess = await navigator.requestMIDIAccess();
this.midiOutputs = Array.from(this.midiAccess.outputs.values());
if (this.midiOutputs.length === 0) {
console.warn('No MIDI outputs available.');
}
} catch (error) {
console.error('Failed to initialize MIDI:', error);
}
}
public getCurrentMidiPort(): string | null {
if (this.midiOutputs.length > 0 && this.currentOutputIndex >= 0 && this.currentOutputIndex < this.midiOutputs.length) {
return this.midiOutputs[this.currentOutputIndex].name;
} else {
console.error('No MIDI output selected or available.');
return null;
}
}
public sendMidiClock(): void {
const output = this.midiOutputs[this.currentOutputIndex];
if (output) {
output.send([0xF8]); // Send a single MIDI clock message
} else {
console.error('MIDI output not available.');
}
}
public switchMidiOutput(outputName: string): boolean {
const index = this.midiOutputs.findIndex((output) => output.name === outputName);
if (index !== -1) {
this.currentOutputIndex = index;
return true;
} else {
console.error(`MIDI output "${outputName}" not found.`);
return false;
}
}
public listMidiOutputs(): void {
console.log('Available MIDI Outputs:');
this.midiOutputs.forEach((output, index) => {
console.log(`${index + 1}. ${output.name}`);
});
}
public sendMidiNote(noteNumber: number, velocity: number, durationMs: number): void {
const output = this.midiOutputs[this.currentOutputIndex];
if (output) {
const noteOnMessage = [0x90, noteNumber, velocity];
const noteOffMessage = [0x80, noteNumber, 0];
// Send Note On
output.send(noteOnMessage);
// Schedule Note Off
const timeoutId = setTimeout(() => {
output.send(noteOffMessage);
delete this.scheduledNotes[noteNumber];
}, durationMs);
this.scheduledNotes[noteNumber] = timeoutId;
} else {
console.error('MIDI output not available.');
}
}
public sendMidiControlChange(controlNumber: number, value: number): void {
const output = this.midiOutputs[this.currentOutputIndex];
if (output) {
output.send([0xB0, controlNumber, value]); // Control Change
} else {
console.error('MIDI output not available.');
}
}
public panic(): void {
const output = this.midiOutputs[this.currentOutputIndex];
if (output) {
for (const noteNumber in this.scheduledNotes) {
const timeoutId = this.scheduledNotes[noteNumber];
clearTimeout(timeoutId);
output.send([0x80, parseInt(noteNumber), 0]); // Note Off
}
this.scheduledNotes = {};
} else {
console.error('MIDI output not available.');
}
}
}
export class UserAPI { export class UserAPI {
variables: { [key: string]: any } = {} variables: { [key: string]: any } = {}
globalGain: GainNode
audioNodes: AudioNode[] = []
MidiConnection: MidiConnection = new MidiConnection() MidiConnection: MidiConnection = new MidiConnection()
constructor(public app: Editor) { constructor(public app: Editor) {
this.globalGain = this.app.audioContext.createGain()
// Give default parameters to the reverb
this.globalGain.gain.value = 0.2;
this.globalGain.connect(this.app.audioContext.destination)
}
private registerNode<T extends AudioNode>(node: T): T{
this.audioNodes.push(node)
return node
} }
// ============================================================= // =============================================================
@ -132,24 +19,6 @@ export class UserAPI {
// ============================================================= // =============================================================
log = console.log log = console.log
public killAll():void {
this.audioNodes.forEach(node => {
node.disconnect()
})
}
// Web Audio Gain and Node Management
mute():void {
this.globalGain.gain.value = 0
}
volume(volume: number):void {
this.globalGain.gain.value = volume
}
vol = this.volume
// ============================================================= // =============================================================
// MIDI related functions // MIDI related functions
// ============================================================= // =============================================================
@ -193,7 +62,7 @@ export class UserAPI {
// Variable related functions // Variable related functions
// ============================================================= // =============================================================
public var(a: number | string, b?: number): number { public v(a: number | string, b?: any): any {
if (typeof a === 'string' && b === undefined) { if (typeof a === 'string' && b === undefined) {
return this.variables[a] return this.variables[a]
} else { } else {
@ -202,11 +71,11 @@ export class UserAPI {
} }
} }
public delVar(name: string): void { public dv(name: string): void {
delete this.variables[name] delete this.variables[name]
} }
public cleanVar(): void { public cv(): void {
this.variables = {} this.variables = {}
} }
@ -220,21 +89,73 @@ export class UserAPI {
seqbar<T>(...array: T[]): T { return array[this.app.clock.time_position.bar % array.length] } seqbar<T>(...array: T[]): T { return array[this.app.clock.time_position.bar % array.length] }
seqpulse<T>(...array: T[]): T { return array[this.app.clock.time_position.pulse % array.length] } seqpulse<T>(...array: T[]): T { return array[this.app.clock.time_position.pulse % array.length] }
bpm(bpm: number) { this.app.clock.bpm = bpm } // =============================================================
// Randomness functions
// =============================================================
randI(min: number, max: number): number { return Math.floor(Math.random() * (max - min + 1)) + min }
randF(min: number, max: number): number { return Math.random() * (max - min) + min }
rI = this.randI; rF = this.randF
// =============================================================
// Quantification functions
// =============================================================
quantize(value: number, quantization: number[]): number {
// Takes a value, and a quantization array, and returns the closest value in the quantization array
// Example: quantize(0.7, [0, 0.5, 1]) => 0.5
// If the quantization array is empty, return the value
if (quantization.length === 0) { return value }
let closest = quantization[0]
quantization.forEach(q => {
if (Math.abs(q - value) < Math.abs(closest - value)) { closest = q }
})
return closest
}
clamp(value: number, min: number, max: number): number {
return Math.min(Math.max(value, min), max)
}
// =============================================================
// Time functions
// =============================================================
bpm(bpm: number) {
this.app.clock.bpm = bpm
}
time_signature(numerator: number, denominator: number) { time_signature(numerator: number, denominator: number) {
this.app.clock.time_signature = [ numerator, denominator ] this.app.clock.time_signature = [ numerator, denominator ]
} }
// =============================================================
// Probability functions
// =============================================================
almostNever() { return Math.random() > 0.9 } almostNever() { return Math.random() > 0.9 }
sometimes() { return Math.random() > 0.5 } sometimes() { return Math.random() > 0.5 }
rarely() { return Math.random() > 0.75 } rarely() { return Math.random() > 0.75 }
often() { return Math.random() > 0.25 } often() { return Math.random() > 0.25 }
almostAlways() { return Math.random() > 0.1 } almostAlways() { return Math.random() > 0.1 }
randInt(min: number, max: number) { return Math.floor(Math.random() * (max - min + 1)) + min } randInt(min: number, max: number) { return Math.floor(Math.random() * (max - min + 1)) + min }
dice(sides: number) { return Math.floor(Math.random() * sides) + 1 }
// =============================================================
// Iterator functions (for loops, with evaluation count, etc...)
// =============================================================
// Iterators
get i() { return this.app.universes[this.app.selected_universe].global.evaluations } get i() { return this.app.universes[this.app.selected_universe].global.evaluations }
get e1() { return this.app.universes[this.app.selected_universe].locals[0].evaluations }
get e2() { return this.app.universes[this.app.selected_universe].locals[1].evaluations }
get e3() { return this.app.universes[this.app.selected_universe].locals[2].evaluations }
get e4() { return this.app.universes[this.app.selected_universe].locals[3].evaluations }
get e5() { return this.app.universes[this.app.selected_universe].locals[4].evaluations }
get e6() { return this.app.universes[this.app.selected_universe].locals[5].evaluations }
get e7() { return this.app.universes[this.app.selected_universe].locals[6].evaluations }
get e8() { return this.app.universes[this.app.selected_universe].locals[7].evaluations }
get e9() { return this.app.universes[this.app.selected_universe].locals[8].evaluations }
e(index:number) { return this.app.universes[this.app.selected_universe].locals[index].evaluations } e(index:number) { return this.app.universes[this.app.selected_universe].locals[index].evaluations }
@ -242,71 +163,34 @@ export class UserAPI {
script(...args: number[]): void { script(...args: number[]): void {
args.forEach(arg => { tryEvaluate(this.app, this.app.universes[this.app.selected_universe].locals[arg]) }) args.forEach(arg => { tryEvaluate(this.app, this.app.universes[this.app.selected_universe].locals[arg]) })
} }
s = this.script
// Small ZZFX interface for playing with this synth // Small ZZFX interface for playing with this synth
zzfx(...thing: number[]) { zzfx(...thing: number[]) {
zzfx(...thing); zzfx(...thing);
} }
beat(...beat: number[]): boolean { // =============================================================
// Time markers
// =============================================================
get tick(): number { return this.app.clock.tick }
get bar(): number { return this.app.clock.time_position.bar }
get pulse(): number { return this.app.clock.time_position.pulse }
get beat(): number { return this.app.clock.time_position.beat }
onbeat(...beat: number[]): boolean {
return ( return (
beat.includes(this.app.clock.time_position.beat) beat.includes(this.app.clock.time_position.beat)
&& this.app.clock.time_position.pulse == 1 && this.app.clock.time_position.pulse == 1
) )
} }
every(n: number): boolean { return this.i % n === 0 } evry(...n: number[]): boolean {
return n.some(n => this.i % n === 0)
pulse(...pulse: number[]) {
return pulse.includes(this.app.clock.time_position.pulse) && this.app.clock.time_position.pulse == 1
} }
mod(pulse: number) { mod(...pulse: number[]): boolean {
return this.app.clock.time_position.pulse % pulse === 0
}
return pulse.some(p => this.app.clock.time_position.pulse % p === 0)
beep(
frequency: number = 400, duration: number = 0.2,
type: OscillatorType = "sine", filter: BiquadFilterType = "lowpass",
cutoff: number = 10000, resonance: number = 1,
) {
const oscillator = this.registerNode(this.app.audioContext.createOscillator());
const gainNode = this.registerNode(this.app.audioContext.createGain());
const limiterNode = this.registerNode(this.app.audioContext.createDynamicsCompressor());
const filterNode = this.registerNode(this.app.audioContext.createBiquadFilter());
// All this for the limiter
limiterNode.threshold.setValueAtTime(-5.0, this.app.audioContext.currentTime);
limiterNode.knee.setValueAtTime(0, this.app.audioContext.currentTime);
limiterNode.ratio.setValueAtTime(20.0, this.app.audioContext.currentTime);
limiterNode.attack.setValueAtTime(0.001, this.app.audioContext.currentTime);
limiterNode.release.setValueAtTime(0.05, this.app.audioContext.currentTime);
// Filter
filterNode.type = filter;
filterNode.frequency.value = cutoff;
filterNode.Q.value = resonance;
oscillator.type = type;
oscillator.frequency.value = frequency || 400;
gainNode.gain.value = 0.25;
oscillator
.connect(filterNode)
.connect(gainNode)
.connect(limiterNode)
.connect(this.globalGain)
oscillator.start();
gainNode.gain.exponentialRampToValueAtTime(0.00001, this.app.audioContext.currentTime + duration);
oscillator.stop(this.app.audioContext.currentTime + duration);
// Clean everything after a node has been played
oscillator.onended = () => {
oscillator.disconnect();
gainNode.disconnect();
filterNode.disconnect();
limiterNode.disconnect();
}
} }
} }

View File

@ -17,8 +17,10 @@ export class Clock {
time_signature: number[] time_signature: number[]
time_position: TimePosition time_position: TimePosition
ppqn: number ppqn: number
tick: number
constructor(public app: Editor, ctx: AudioContext) { constructor(public app: Editor, ctx: AudioContext) {
this.tick = 0;
this.time_position = { bar: 0, beat: 0, pulse: 0 } this.time_position = { bar: 0, beat: 0, pulse: 0 }
this.bpm = 120; this.bpm = 120;
this.time_signature = [4, 4]; this.time_signature = [4, 4];
@ -34,9 +36,7 @@ export class Clock {
}) })
} }
get pulses_per_beat(): number { get pulses_per_beat(): number { return this.ppqn / this.time_signature[1]; }
return this.ppqn / this.time_signature[1];
}
start(): void { start(): void {
// Check if the clock is already running // Check if the clock is already running
@ -48,14 +48,6 @@ export class Clock {
} }
} }
pause(): void { pause = (): void => this.transportNode?.pause();
this.transportNode?.pause(); stop = (): void => this.transportNode?.stop();
}
stop(): void {
this.transportNode?.stop();
}
// Public methods
public toString(): string { return `` }
} }

105
src/IO/MidiConnection.ts Normal file
View File

@ -0,0 +1,105 @@
export class MidiConnection{
private midiAccess: MIDIAccess | null = null;
private midiOutputs: MIDIOutput[] = [];
private currentOutputIndex: number = 0;
private scheduledNotes: { [noteNumber: number]: number } = {}; // { noteNumber: timeoutId }
constructor() {
this.initializeMidiAccess();
}
private async initializeMidiAccess(): Promise<void> {
try {
this.midiAccess = await navigator.requestMIDIAccess();
this.midiOutputs = Array.from(this.midiAccess.outputs.values());
if (this.midiOutputs.length === 0) {
console.warn('No MIDI outputs available.');
}
} catch (error) {
console.error('Failed to initialize MIDI:', error);
}
}
public getCurrentMidiPort(): string | null {
if (this.midiOutputs.length > 0 && this.currentOutputIndex >= 0 && this.currentOutputIndex < this.midiOutputs.length) {
return this.midiOutputs[this.currentOutputIndex].name;
} else {
console.error('No MIDI output selected or available.');
return null;
}
}
public sendMidiClock(): void {
const output = this.midiOutputs[this.currentOutputIndex];
if (output) {
output.send([0xF8]); // Send a single MIDI clock message
} else {
console.error('MIDI output not available.');
}
}
public switchMidiOutput(outputName: string): boolean {
const index = this.midiOutputs.findIndex((output) => output.name === outputName);
if (index !== -1) {
this.currentOutputIndex = index;
return true;
} else {
console.error(`MIDI output "${outputName}" not found.`);
return false;
}
}
public listMidiOutputs(): void {
console.log('Available MIDI Outputs:');
this.midiOutputs.forEach((output, index) => {
console.log(`${index + 1}. ${output.name}`);
});
}
public sendMidiNote(noteNumber: number, velocity: number, durationMs: number): void {
const output = this.midiOutputs[this.currentOutputIndex];
if (output) {
const noteOnMessage = [0x90, noteNumber, velocity];
const noteOffMessage = [0x80, noteNumber, 0];
// Send Note On
output.send(noteOnMessage);
// Schedule Note Off
const timeoutId = setTimeout(() => {
output.send(noteOffMessage);
delete this.scheduledNotes[noteNumber];
}, durationMs);
this.scheduledNotes[noteNumber] = timeoutId;
} else {
console.error('MIDI output not available.');
}
}
public sendMidiControlChange(controlNumber: number, value: number): void {
const output = this.midiOutputs[this.currentOutputIndex];
if (output) {
output.send([0xB0, controlNumber, value]); // Control Change
} else {
console.error('MIDI output not available.');
}
}
public panic(): void {
const output = this.midiOutputs[this.currentOutputIndex];
if (output) {
for (const noteNumber in this.scheduledNotes) {
const timeoutId = this.scheduledNotes[noteNumber];
clearTimeout(timeoutId);
output.send([0x80, parseInt(noteNumber), 0]); // Note Off
}
this.scheduledNotes = {};
} else {
console.error('MIDI output not available.');
}
}
}

View File

@ -47,10 +47,11 @@ export class TransportNode extends AudioWorkletNode {
const beatNumber = (currentTime) / beatDuration; const beatNumber = (currentTime) / beatDuration;
const beatsPerBar = this.app.clock.time_signature[0]; const beatsPerBar = this.app.clock.time_signature[0];
const barNumber = Math.floor(beatNumber / beatsPerBar) + 1; // Adding 1 to make it 1-indexed const barNumber = Math.floor(beatNumber / beatsPerBar) + 1;
const beatWithinBar = Math.floor(beatNumber % beatsPerBar) + 1; // Adding 1 to make it 1-indexed const beatWithinBar = Math.floor(beatNumber % beatsPerBar) + 1;
const ppqnPosition = Math.floor((beatNumber % 1) * this.app.clock.ppqn); const ppqnPosition = Math.floor((beatNumber % 1) * this.app.clock.ppqn);
this.app.clock.tick++
return { bar: barNumber, beat: beatWithinBar, ppqn: ppqnPosition }; return { bar: barNumber, beat: beatWithinBar, ppqn: ppqnPosition };
} }
} }

View File

@ -5,24 +5,14 @@ class TransportProcessor extends AudioWorkletProcessor {
this.port.addEventListener("message", this.handleMessage); this.port.addEventListener("message", this.handleMessage);
this.port.start(); this.port.start();
this.stated = false; this.stated = false;
/*
this.interval = 0.0001;
this.origin = currentTime;
this.next = this.origin + this.interval;
*/
} }
handleMessage = (message) => { handleMessage = (message) => {
if (message.data === "start") { if (message.data === "start") {
this.started = true; this.started = true;
// this.origin = currentTime;
// this.next = this.origin + this.interval;
} else if (message.data === "pause") { } else if (message.data === "pause") {
// this.next = Infinity;
this.started = false; this.started = false;
} else if (message.data === "stop") { } else if (message.data === "stop") {
// this.origin = currentTime;
// this.next = Infinity;
this.started = false; this.started = false;
} }
}; };