diff --git a/index.html b/index.html
index 97244f8..d3fd225 100644
--- a/index.html
+++ b/index.html
@@ -270,8 +270,8 @@
-
-
+
+
diff --git a/src/API.ts b/src/API.ts
index 7d07c96..26d913d 100644
--- a/src/API.ts
+++ b/src/API.ts
@@ -1,5 +1,5 @@
import { seededRandom } from "zifferjs";
-import { MidiConnection } from "./IO/MidiConnection";
+import { MidiCCEvent, MidiConnection, MidiNoteEvent } from "./IO/MidiConnection";
import { tryEvaluate, evaluateOnce } from "./Evaluator";
import { DrunkWalk } from "./Utils/Drunk";
import { Editor } from "./main";
@@ -411,6 +411,133 @@ export class UserAPI {
this.MidiConnection.panic();
};
+ public active_note_events = (channel?: number): MidiNoteEvent[]|undefined => {
+ /**
+ * @returns A list of currently active MIDI notes
+ */
+ let events;
+ if(channel) {
+ events = this.MidiConnection.activeNotesFromChannel(channel);
+ } else {
+ events = this.MidiConnection.activeNotes;
+ }
+ if(events.length>0) return events
+ else return undefined;
+ }
+
+ public transmission(): boolean {
+ /**
+ * Returns true if there are active notes
+ */
+ return this.MidiConnection.activeNotes.length > 0;
+ }
+
+ public active_notes = (channel?: number): number[]|undefined => {
+ /**
+ * @returns A list of currently active MIDI notes
+ */
+ let notes;
+ if(channel) {
+ notes = this.MidiConnection.activeNotesFromChannel(channel).map((note) => note.note);
+ } else {
+ notes = this.MidiConnection.activeNotes.map((note) => note.note);
+ }
+ if(notes.length > 0) return notes;
+ else return undefined;
+ }
+
+ public kill_active_notes = (): void => {
+ /**
+ * Clears all active notes
+ */
+ this.MidiConnection.activeNotes = [];
+ }
+
+ public sticky_notes = (channel?: number): number[]|undefined => {
+ /**
+ *
+ * @param channel
+ * @returns
+ */
+ let notes;
+ if(channel) notes = this.MidiConnection.stickyNotesFromChannel(channel);
+ else notes = this.MidiConnection.stickyNotes;
+ if(notes.length > 0) return notes.map((e) => e.note);
+ else return undefined;
+ }
+
+ public kill_sticky_notes = (): void => {
+ /**
+ * Clears all sticky notes
+ */
+ this.MidiConnection.stickyNotes = [];
+ }
+
+ public last_event = (): MidiNoteEvent|undefined => {
+ /**
+ * @returns Returns latest unlistened note event
+ */
+ return this.MidiConnection.popNoteFromBuffer();
+ }
+
+ public last_note = (): number|undefined => {
+ /**
+ * @returns Returns latest received note
+ */
+ const note = this.MidiConnection.popNoteFromBuffer();
+ return note ? note.note : undefined;
+ }
+
+ public first_event = (): MidiNoteEvent|undefined => {
+ /**
+ * @returns Returns first unlistened note event
+ */
+ return this.MidiConnection.shiftNoteFromBuffer();
+ }
+
+ public first_note = (): number|undefined => {
+ /**
+ * @returns Returns first received note
+ */
+ const note = this.MidiConnection.shiftNoteFromBuffer();
+ return note ? note.note : undefined;
+ }
+
+ public last_channel_note = (channel: number): MidiNoteEvent|undefined => {
+ /**
+ * @returns Returns first unlistened note event on a specific channel
+ */
+ return this.MidiConnection.findNoteFromBufferInChannel(channel);
+ }
+
+ public find_channel_note = (channel: number): MidiNoteEvent|undefined => {
+ /**
+ * @returns Returns first unlistened note event on a specific channel
+ */
+ return this.MidiConnection.findNoteFromBufferInChannel(channel);
+ }
+
+ public first_cc = (): MidiCCEvent|undefined => {
+ /**
+ * @returns Returns first unlistened cc event
+ */
+ return this.MidiConnection.popCCFromBuffer();
+ }
+
+ public last_cc = (): MidiCCEvent|undefined => {
+ /**
+ * @returns Returns latest unlistened cc event
+ */
+ return this.MidiConnection.shiftCCFromBuffer();
+ }
+
+ public find_channel_cc = (channel: number): MidiCCEvent|undefined => {
+ /**
+ * @returns Returns first unlistened cc event on a specific channel
+ */
+ return this.MidiConnection.findCCFromBufferInChannel(channel);
+ }
+
// =============================================================
// Ziffers related functions
// =============================================================
diff --git a/src/IO/MidiConnection.ts b/src/IO/MidiConnection.ts
index d07fecf..64f3195 100644
--- a/src/IO/MidiConnection.ts
+++ b/src/IO/MidiConnection.ts
@@ -1,6 +1,20 @@
import { UserAPI } from "../API";
import { AppSettings } from "../AppSettings";
+export type MidiNoteEvent = {
+ note: number;
+ velocity: number;
+ channel: number;
+ timestamp: number;
+}
+
+export type MidiCCEvent = {
+ control: number;
+ value: number;
+ channel: number;
+ timestamp: number;
+}
+
export class MidiConnection {
/**
* Wrapper class for Web MIDI API. Provides methods for sending MIDI messages.
@@ -12,15 +26,23 @@ export class MidiConnection {
* @param scheduledNotes - Object containing scheduled notes. Keys are note numbers and values are timeout IDs.
*/
+ /* Midi output */
private api: UserAPI;
private settings: AppSettings;
private midiAccess: MIDIAccess | null = null;
- public midiOutputs: MIDIOutput[] = [];
- public midiInputs: MIDIInput[] = [];
+ public midiOutputs: MIDIOutput[] = [];
private currentOutputIndex: number = 0;
- private currentInputIndex: number|undefined = undefined;
private scheduledNotes: { [noteNumber: number]: number } = {}; // { noteNumber: timeoutId }
+ /* Midi input */
+ public midiInputs: MIDIInput[] = [];
+ private currentInputIndex: number|undefined = undefined;
+ public bufferLength: number = 512; // 32*16
+ public noteInputBuffer: MidiNoteEvent[] = [];
+ public ccInputBuffer: MidiCCEvent[] = [];
+ public activeNotes: MidiNoteEvent[] = [];
+ public stickyNotes: MidiNoteEvent[] = [];
+
/* MIDI clock stuff */
private midiClockInputIndex: number|undefined = undefined;
private midiClockInput?: MIDIInput|undefined = undefined;
@@ -264,23 +286,147 @@ export class MidiConnection {
}
/* DEFAULT MIDI INPUT */
if(input.name === this.settings.default_midi_input) {
- // List of all note_on messages from channels 1-16
- const all_note_ons = [0x90, 0x91, 0x92, 0x93, 0x94, 0x95, 0x96, 0x97, 0x98, 0x99, 0x9A, 0x9B, 0x9C, 0x9D,0x9E, 0x9F];
+
// If message is one of note ons
- if(all_note_ons.indexOf(message.data[0]) !== -1) {
- const channel = all_note_ons.indexOf(message.data[0])+1;
+ if(message.data[0] >= 0x90 && message.data[0] <= 0x9F) {
+ const channel = message.data[0] - 0x90 + 1;
const note = message.data[1];
const velocity = message.data[2];
this.api.variable(`channel_${channel}_note`, note);
this.api.variable(`channel_${channel}_velocity`, velocity);
+
if(this.settings.midi_channels_scripts) this.api.script(channel);
+
+ //console.log(`NOTE: ${note} VELOCITY: ${velocity} CHANNEL: ${channel}`);
+
+ this.pushToMidiInputBuffer({note, velocity, channel, timestamp: event.timeStamp});
+ this.activeNotes.push({note, velocity, channel, timestamp: event.timeStamp});
+
+ const sticky = this.removeFromStickyNotes(note, channel);
+ if(!sticky) this.stickyNotes.push({note, velocity, channel, timestamp: event.timeStamp});
}
+
+ // If note off
+ if(message.data[0] >= 0x80 && message.data[0] <= 0x8F) {
+ const channel = message.data[0] - 0x80 + 1;
+ const note = message.data[1];
+ this.removeFromActiveNotes(note, channel);
+ }
+
+ // If message is one of CCs
+ if(message.data[0]>=0xB0 && message.data[0]<=0xBF) {
+ const channel = message.data[0] - 0xB0 + 1;
+ const control = message.data[1];
+ const value = message.data[2];
+ this.api.variable(`channel_${channel}_control`, control);
+ this.api.variable(`channel_${channel}_value`, value);
+
+ //console.log(`CC: ${control} VALUE: ${value} CHANNEL: ${channel}`);
+
+ this.pushToMidiCCBuffer({control, value, channel, timestamp: event.timeStamp});
+
+ }
+
+
}
}
}
}
}
+ /* Methods for handling active midi notes */
+
+ public removeFromActiveNotes(note: number, channel: number): void {
+ const index = this.activeNotes.findIndex((e) => e.note===note && e.channel===channel);
+ if(index>=0) this.activeNotes.splice(index, 1);
+ }
+
+ public removeFromStickyNotes(note: number, channel: number): boolean {
+ const index = this.stickyNotes.findIndex((e) => e.note===note && e.channel===channel);
+ if(index>=0) {
+ this.stickyNotes.splice(index, 1);
+ return true;
+ } else { return false; }
+ }
+
+ public stickyNotesFromChannel(channel: number): MidiNoteEvent[] {
+ return this.stickyNotes.filter((e) => e.channel===channel);
+ }
+
+ public activeNotesFromChannel(channel: number): MidiNoteEvent[] {
+ return this.activeNotes.filter((e) => e.channel===channel);
+ }
+
+ public killActiveNotes(): void {
+ this.activeNotes = [];
+ }
+
+ public killActiveNotesFromChannel(channel: number): void {
+ this.activeNotes = this.activeNotes.filter((e) => e.channel!==channel);
+ }
+
+ /* Methods for handling midi input buffers */
+
+ private pushToMidiInputBuffer(event: MidiNoteEvent): void {
+ this.noteInputBuffer.push(event);
+ if(this.noteInputBuffer.length>this.bufferLength) {
+ this.noteInputBuffer.shift();
+ }
+ }
+
+ private pushToMidiCCBuffer(event: MidiCCEvent): void {
+ this.ccInputBuffer.push(event);
+ if(this.ccInputBuffer.length>this.bufferLength) {
+ this.ccInputBuffer.shift();
+ }
+ }
+
+ public shiftNoteFromBuffer(): MidiNoteEvent|undefined {
+ const event = this.noteInputBuffer.shift();
+ if(event) return event;
+ else return undefined;
+ }
+
+ public popNoteFromBuffer(): MidiNoteEvent|undefined {
+ const event = this.noteInputBuffer.pop();
+ if(event) return event;
+ else return undefined;
+ }
+
+ public popCCFromBuffer(): MidiCCEvent|undefined {
+ const event = this.ccInputBuffer.pop();
+ if(event) return event;
+ else return undefined;
+ }
+
+ public shiftCCFromBuffer(): MidiCCEvent|undefined {
+ const event = this.ccInputBuffer.shift();
+ if(event) return event;
+ else return undefined;
+ }
+
+ public findNoteFromBufferInChannel(channel: number|undefined) {
+ const index = this.noteInputBuffer.findIndex((e) => e.channel===channel);
+ if(index>=0) {
+ const event = this.noteInputBuffer[index];
+ this.noteInputBuffer.splice(index, 1);
+ return event;
+ } else {
+ return undefined;
+ }
+ }
+
+ public findCCFromBufferInChannel(channel: number|undefined) {
+ const index = this.ccInputBuffer.findIndex((e) => e.channel===channel);
+ if(index>=0) {
+ const event = this.ccInputBuffer[index];
+ this.ccInputBuffer.splice(index, 1);
+ return event;
+ } else {
+ return undefined;
+ }
+ }
+
public unregisterMidiInputListener(inputIndex: number): void {
/**
* Unregister midi input listener