adding stuff and cleaning stuff
This commit is contained in:
268
src/API.ts
268
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<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 {
|
||||
|
||||
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<T extends AudioNode>(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<T>(...array: T[]): T { return array[this.app.clock.time_position.beat % 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] }
|
||||
|
||||
// =============================================================
|
||||
// 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)
|
||||
}
|
||||
}
|
||||
18
src/Clock.ts
18
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();
|
||||
}
|
||||
105
src/IO/MidiConnection.ts
Normal file
105
src/IO/MidiConnection.ts
Normal 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.');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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 };
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user