From 6f77cdf9a0db60bc72aed967c5f86e7c1fb65de5 Mon Sep 17 00:00:00 2001 From: Raphael Forment Date: Fri, 28 Jul 2023 19:13:17 +0200 Subject: [PATCH] Some functions --- src/API.ts | 207 +++++++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 193 insertions(+), 14 deletions(-) diff --git a/src/API.ts b/src/API.ts index 6b5b318..38cd77c 100644 --- a/src/API.ts +++ b/src/API.ts @@ -3,16 +3,121 @@ import { tryEvaluate } from "./Evaluator"; // @ts-ignore 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 { + 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 { variables: { [key: string]: any } = {} globalGain: GainNode audioNodes: AudioNode[] = [] + MidiConnection: MidiConnection = new MidiConnection() 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) } @@ -22,18 +127,98 @@ export class UserAPI { return node } - killAll() { + // ============================================================= + // Utility functions + // ============================================================= + log = console.log + + public killAll():void { this.audioNodes.forEach(node => { node.disconnect() }) } - var(name: string, value: any) { - this.variables[name] = value + // Web Audio Gain and Node Management + mute():void { + this.globalGain.gain.value = 0 } - get(name: string) { return this.variables[name] } + + volume(volume: number):void { + this.globalGain.gain.value = volume + } + + vol = this.volume + + + // ============================================================= + // MIDI related functions + // ============================================================= + + public midi_outputs(): void { + console.log(this.MidiConnection.listMidiOutputs()) + } + + public midi_output(outputName: string): void { + if (!outputName) { + console.log(this.MidiConnection.getCurrentMidiPort()) + } else { + this.MidiConnection.switchMidiOutput(outputName) + } + } + + public midi_connect(outputName: string): void { + this.MidiConnection.switchMidiOutput(outputName) + } + + public note(note: number, velocity: number, duration: number): void { + this.MidiConnection.sendMidiNote( + note, velocity, duration + ) + } + + public midi_clock(): void { + this.MidiConnection.sendMidiClock() + } + + public cc(control: number, value: number): void { + this.MidiConnection.sendMidiControlChange(control, value) + } + + public midi_panic(): void { + this.MidiConnection.panic() + } + + + // ============================================================= + // Variable related functions + // ============================================================= + + public var(a: number | string, b?: number): number { + if (typeof a === 'string' && b === undefined) { + return this.variables[a] + } else { + this.variables[a] = b + return this.variables[a] + } + } + + public delVar(name: string): void { + delete this.variables[name] + } + + public cleanVar(): void { + this.variables = {} + } + + // ============================================================= + // Small algorithmic functions + // ============================================================= pick(...array: T[]): T { return array[Math.floor(Math.random() * array.length)] } + + seqbeat(...array: T[]): T { return array[this.app.clock.time_position.beat % array.length] } + seqbar(...array: T[]): T { return array[this.app.clock.time_position.bar % array.length] } + seqpulse(...array: T[]): T { return array[this.app.clock.time_position.pulse % array.length] } bpm(bpm: number) { this.app.clock.bpm = bpm } @@ -66,10 +251,12 @@ export class UserAPI { beat(...beat: number[]): boolean { return ( 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 } + pulse(...pulse: number[]) { return pulse.includes(this.app.clock.time_position.pulse) && this.app.clock.time_position.pulse == 1 } @@ -78,14 +265,6 @@ export class UserAPI { return this.app.clock.time_position.pulse % pulse === 0 } - mute() { - this.globalGain.gain.value = 0 - } - - volume(volume: number) { - this.globalGain.gain.value = volume - } - vol = this.volume beep(