diff --git a/src/API.ts b/src/API.ts index 38cd77c..2cc48bf 100644 --- a/src/API.ts +++ b/src/API.ts @@ -1,130 +1,17 @@ import { Editor } from "./main"; import { tryEvaluate } from "./Evaluator"; +import { BasicSynth, PercussionSynth } from "./WebSynth"; +import { MidiConnection } from "./IO/MidiConnection"; +import * as Tone from 'tone'; // @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) - } - - private registerNode(node: T): T{ - this.audioNodes.push(node) - return node } // ============================================================= @@ -132,24 +19,6 @@ export class UserAPI { // ============================================================= 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 // ============================================================= @@ -193,7 +62,7 @@ export class UserAPI { // 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) { return this.variables[a] } else { @@ -202,11 +71,11 @@ export class UserAPI { } } - public delVar(name: string): void { + public dv(name: string): void { delete this.variables[name] } - public cleanVar(): void { + public cv(): void { this.variables = {} } @@ -219,94 +88,109 @@ export class UserAPI { 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] } + + // ============================================================= + // 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 } + bpm(bpm: number) { + this.app.clock.bpm = bpm + } time_signature(numerator: number, denominator: number) { this.app.clock.time_signature = [ numerator, denominator ] } + // ============================================================= + // Probability functions + // ============================================================= + almostNever() { return Math.random() > 0.9 } sometimes() { return Math.random() > 0.5 } rarely() { return Math.random() > 0.75 } often() { return Math.random() > 0.25 } almostAlways() { return Math.random() > 0.1 } 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 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 } // Script launcher: can launch any number of scripts script(...args: number[]): void { 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 zzfx(...thing: number[]) { 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 ( beat.includes(this.app.clock.time_position.beat) && 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 + evry(...n: number[]): boolean { + return n.some(n => this.i % n === 0) } + + mod(...pulse: number[]): boolean { - mod(pulse: number) { - return this.app.clock.time_position.pulse % pulse === 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(); - } + return pulse.some(p => this.app.clock.time_position.pulse % p === 0) } } \ No newline at end of file diff --git a/src/Clock.ts b/src/Clock.ts index 10a0edb..3f7c55f 100644 --- a/src/Clock.ts +++ b/src/Clock.ts @@ -17,8 +17,10 @@ export class Clock { time_signature: number[] time_position: TimePosition ppqn: number + tick: number constructor(public app: Editor, ctx: AudioContext) { + this.tick = 0; this.time_position = { bar: 0, beat: 0, pulse: 0 } this.bpm = 120; this.time_signature = [4, 4]; @@ -34,9 +36,7 @@ export class Clock { }) } - get pulses_per_beat(): number { - return this.ppqn / this.time_signature[1]; - } + get pulses_per_beat(): number { return this.ppqn / this.time_signature[1]; } start(): void { // Check if the clock is already running @@ -48,14 +48,6 @@ export class Clock { } } - pause(): void { - this.transportNode?.pause(); - } - - stop(): void { - this.transportNode?.stop(); - } - - // Public methods - public toString(): string { return `` } + pause = (): void => this.transportNode?.pause(); + stop = (): void => this.transportNode?.stop(); } \ No newline at end of file diff --git a/src/IO/MidiConnection.ts b/src/IO/MidiConnection.ts new file mode 100644 index 0000000..46e0dee --- /dev/null +++ b/src/IO/MidiConnection.ts @@ -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 { + 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.'); + } + } + } + diff --git a/src/TransportNode.js b/src/TransportNode.js index 8d4c44b..01e8e24 100644 --- a/src/TransportNode.js +++ b/src/TransportNode.js @@ -47,10 +47,11 @@ export class TransportNode extends AudioWorkletNode { const beatNumber = (currentTime) / beatDuration; const beatsPerBar = this.app.clock.time_signature[0]; - const barNumber = Math.floor(beatNumber / beatsPerBar) + 1; // Adding 1 to make it 1-indexed - const beatWithinBar = 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; const ppqnPosition = Math.floor((beatNumber % 1) * this.app.clock.ppqn); + this.app.clock.tick++ return { bar: barNumber, beat: beatWithinBar, ppqn: ppqnPosition }; } } \ No newline at end of file diff --git a/src/TransportProcessor.js b/src/TransportProcessor.js index dbdeb29..7b080bd 100644 --- a/src/TransportProcessor.js +++ b/src/TransportProcessor.js @@ -5,24 +5,14 @@ class TransportProcessor extends AudioWorkletProcessor { this.port.addEventListener("message", this.handleMessage); this.port.start(); this.stated = false; - /* - this.interval = 0.0001; - this.origin = currentTime; - this.next = this.origin + this.interval; - */ - } + } handleMessage = (message) => { if (message.data === "start") { this.started = true; - // this.origin = currentTime; - // this.next = this.origin + this.interval; - } else if (message.data === "pause") { - // this.next = Infinity; + } else if (message.data === "pause") { this.started = false; } else if (message.data === "stop") { - // this.origin = currentTime; - // this.next = Infinity; this.started = false; } };