From cf45bf79524e970470c342b2a88ed387f4813e0b Mon Sep 17 00:00:00 2001 From: Miika Alonen Date: Sun, 1 Oct 2023 03:33:16 +0300 Subject: [PATCH 1/9] First attempt --- index.html | 9 +++ src/API.ts | 7 +- src/IO/MidiConnection.ts | 149 ++++++++++++++++++++++++++++++++++++++- 3 files changed, 161 insertions(+), 4 deletions(-) diff --git a/index.html b/index.html index e658ccf..47aef23 100644 --- a/index.html +++ b/index.html @@ -236,6 +236,15 @@

Show Hovering Tips + +
+
+ Midi clock:  + +
+
diff --git a/src/API.ts b/src/API.ts index 85bb809..3627c8b 100644 --- a/src/API.ts +++ b/src/API.ts @@ -56,11 +56,12 @@ export class UserAPI { public patternCache = new LRUCache({ max: 1000, ttl: 1000 * 60 * 5 }); private errorTimeoutID: number = 0; private printTimeoutID: number = 0; - - MidiConnection: MidiConnection = new MidiConnection(); + public MidiConnection: MidiConnection; load: samples; - constructor(public app: Editor) {} + constructor(public app: Editor) { + this.MidiConnection = new MidiConnection(this); + } _loadUniverseFromInterface = (universe: string) => { this.app.loadUniverse(universe as string); diff --git a/src/IO/MidiConnection.ts b/src/IO/MidiConnection.ts index d5b1ec7..d205780 100644 --- a/src/IO/MidiConnection.ts +++ b/src/IO/MidiConnection.ts @@ -1,3 +1,5 @@ +import { UserAPI } from "../API"; + export class MidiConnection { /** * Wrapper class for Web MIDI API. Provides methods for sending MIDI messages. @@ -11,10 +13,20 @@ export class MidiConnection { private midiAccess: MIDIAccess | null = null; public midiOutputs: MIDIOutput[] = []; + public midiInputs: MIDIInput[] = []; private currentOutputIndex: number = 0; + private currentInputIndex: number|undefined = undefined; + private midiClockInput?: MIDIInput|undefined = undefined; + private lastClockTime: number = 0; + private lastBPM: number; + private clockBuffer: number[] = []; + private clockBufferLength = 100; private scheduledNotes: { [noteNumber: number]: number } = {}; // { noteNumber: timeoutId } + private api: UserAPI; - constructor() { + constructor(api: UserAPI) { + this.api = api; + this.lastBPM = api.bpm(); this.initializeMidiAccess(); } @@ -31,6 +43,12 @@ export class MidiConnection { console.warn("No MIDI outputs available."); this.currentOutputIndex = -1; } + this.midiInputs = Array.from(this.midiAccess.inputs.values()); + if (this.midiInputs.length === 0) { + console.warn("No MIDI inputs available."); + } else { + this.updateMidiClockSelect(); + } } catch (error) { console.error("Failed to initialize MIDI:", error); } @@ -92,6 +110,105 @@ export class MidiConnection { } } + public setMidiClock(inputName: string|number): void { + /** + * Sets the MIDI input to use for MIDI clock messages. + * + * @param inputName Name of the MIDI input to use for MIDI clock messages + */ + const inputIndex = this.getMidiInputIndex(inputName); + if (inputIndex !== -1) { + this.midiClockInput = this.midiInputs[inputIndex]; + this.registerMidiClockListener(); + } else { + this.midiClockInput = undefined; + } + } + + public updateMidiClockSelect() { + /** + * Updates the MIDI clock input select element with the available MIDI inputs. + */ + if(this.midiInputs.length > 0) { + const select = document.getElementById("midi-clock-input") as HTMLSelectElement; + select.innerHTML = ""; + // Defaults to internal clock + const defaultOption = document.createElement("option"); + defaultOption.value = "-1"; + defaultOption.text = "Internal"; + select.appendChild(defaultOption); + // Add MIDI inputs to clock select input + this.midiInputs.forEach((input, index) => { + const option = document.createElement("option"); + option.value = index.toString(); + option.text = input.name || "No name input"; + select.appendChild(option); + }); + select.value = this.currentInputIndex ? this.currentInputIndex.toString() : "-1"; + // Add listener + select.addEventListener("change", (event) => { + const value = (event.target as HTMLSelectElement).value; + if(value === "-1") { + if(this.midiClockInput) this.midiClockInput.onmidimessage = null; + this.midiClockInput = undefined; + } else { + this.currentInputIndex = parseInt(value); + if(this.midiClockInput) this.midiClockInput.onmidimessage = null; + this.midiClockInput = this.midiInputs[this.currentInputIndex]; + this.registerMidiClockListener(); + } + }); + } + } + + public registerMidiClockListener(): void { + /** + * Registers a listener for MIDI clock messages on the currently selected MIDI input. + */ + if (this.midiClockInput) { + this.midiClockInput.onmidimessage = (event: Event) => { + const message = event as MIDIMessageEvent; + if (message.data[0] === 0xf8) { + const timestamp = performance.now(); + const delta = timestamp - this.lastClockTime; + const bpm = 60 * (1000 / delta / 24); + this.lastClockTime = timestamp; + this.clockBuffer.push(bpm); + if(this.clockBuffer.length>this.clockBufferLength) this.clockBuffer.shift(); + const estimatedBPM = this.estimatedBPM(); + if(estimatedBPM !== this.lastBPM) { + this.api.bpm(this.estimatedBPM()); + this.lastBPM = estimatedBPM; + } + } else if(message.data[0] === 0xfa) { + console.log("MIDI start received"); + } else if(message.data[0] === 0xfc) { + console.log("MIDI stop received"); + } else if(message.data[0] === 0xfb) { + console.log("MIDI continue received"); + } else if(message.data[0] === 0xfe) { + console.log("MIDI active sensing received"); + } else { + // Ignore other MIDI messages + console.log("Ignored MIDI message: ", message.data); + } + } + } + } + + public estimatedBPM(): number { + /** + * Returns the estimated BPM based on the last 24 MIDI clock messages. + * + * @returns Estimated BPM + */ + const sum = this.clockBuffer.reduce((a, b) => a + b); + return Math.round(sum / this.clockBuffer.length); + } + + + + public sendMidiClock(): void { /** * Sends a single MIDI clock message to the currently selected MIDI output. @@ -148,6 +265,36 @@ export class MidiConnection { } } + public getMidiInputIndex(input: string | number): number { + /** + * Returns the index of the MIDI input with the specified name. + * + * @param input Name or index of the MIDI input + * @returns Index of the new MIDI input or -1 if not valid + * + */ + if (typeof input === "number") { + if (input < 0 || input >= this.midiInputs.length) { + console.error( + `Invalid MIDI input index. Index must be in the range 0-${ + this.midiInputs.length - 1 + }.` + ); + return -1; + } else { + return input; + } + } else { + const index = this.midiInputs.findIndex((o) => o.name === input); + if (index !== -1) { + return index; + } else { + console.error(`MIDI input "${input}" not found.`); + return -1; + } + } + } + public listMidiOutputs(): string { /** * Lists all available MIDI outputs to the console. From 05a4f8a161dccad72ee2a784baa3ae2f04c81dc4 Mon Sep 17 00:00:00 2001 From: Miika Alonen Date: Mon, 2 Oct 2023 21:36:23 +0300 Subject: [PATCH 2/9] Some error handling and smoothing estimated bpm --- index.html | 9 +++- src/IO/MidiConnection.ts | 101 ++++++++++++++++++++++++++++++++------- 2 files changed, 92 insertions(+), 18 deletions(-) diff --git a/index.html b/index.html index 47aef23..8b43bf0 100644 --- a/index.html +++ b/index.html @@ -239,11 +239,18 @@

- Midi clock:  +
+
+ + +

diff --git a/src/IO/MidiConnection.ts b/src/IO/MidiConnection.ts index d205780..621559b 100644 --- a/src/IO/MidiConnection.ts +++ b/src/IO/MidiConnection.ts @@ -11,22 +11,32 @@ export class MidiConnection { * @param scheduledNotes - Object containing scheduled notes. Keys are note numbers and values are timeout IDs. */ + private api: UserAPI; private midiAccess: MIDIAccess | null = null; public midiOutputs: MIDIOutput[] = []; public midiInputs: MIDIInput[] = []; private currentOutputIndex: number = 0; private currentInputIndex: number|undefined = undefined; + private scheduledNotes: { [noteNumber: number]: number } = {}; // { noteNumber: timeoutId } + + /* MIDI clock stuff */ private midiClockInput?: MIDIInput|undefined = undefined; - private lastClockTime: number = 0; + private lastTimestamp: number = 0; + private midiClockDelta: number = 0; private lastBPM: number; + private roundedBPM: number = 0; private clockBuffer: number[] = []; - private clockBufferLength = 100; - private scheduledNotes: { [noteNumber: number]: number } = {}; // { noteNumber: timeoutId } - private api: UserAPI; + private deltaBuffer: number[] = []; + private clockBufferLength = 24; + private clockPPQN = 24; + private clockTicks = 0; + private clockErrorCount = 0; + private skipOnError = 0; constructor(api: UserAPI) { this.api = api; this.lastBPM = api.bpm(); + this.roundedBPM = this.lastBPM; this.initializeMidiAccess(); } @@ -48,6 +58,7 @@ export class MidiConnection { console.warn("No MIDI inputs available."); } else { this.updateMidiClockSelect(); + this.clockPPQNSelect(); } } catch (error) { console.error("Failed to initialize MIDI:", error); @@ -161,6 +172,14 @@ export class MidiConnection { } } + clockPPQNSelect(): void { + const select = document.getElementById("midi-clock-ppqn-input") as HTMLSelectElement; + select.addEventListener("change", (event) => { + const value = (event.target as HTMLSelectElement).value; + this.clockPPQN = parseInt(value); + }); + } + public registerMidiClockListener(): void { /** * Registers a listener for MIDI clock messages on the currently selected MIDI input. @@ -169,16 +188,10 @@ export class MidiConnection { this.midiClockInput.onmidimessage = (event: Event) => { const message = event as MIDIMessageEvent; if (message.data[0] === 0xf8) { - const timestamp = performance.now(); - const delta = timestamp - this.lastClockTime; - const bpm = 60 * (1000 / delta / 24); - this.lastClockTime = timestamp; - this.clockBuffer.push(bpm); - if(this.clockBuffer.length>this.clockBufferLength) this.clockBuffer.shift(); - const estimatedBPM = this.estimatedBPM(); - if(estimatedBPM !== this.lastBPM) { - this.api.bpm(this.estimatedBPM()); - this.lastBPM = estimatedBPM; + if(this.skipOnError>0) { + this.skipOnError -= 1; + } else { + this.onMidiClock(event.timeStamp); } } else if(message.data[0] === 0xfa) { console.log("MIDI start received"); @@ -196,6 +209,63 @@ export class MidiConnection { } } + public onMidiClock(timestamp: number): void { + /** + * Called when a MIDI clock message is received. + */ + + const SMOOTH = 0.1; + this.clockTicks += 1; + + if(this.lastTimestamp > 0) { + + if(this.lastTimestamp===timestamp) { + // This is error handling for odd MIDI clock messages with the same timestamp + this.clockErrorCount+=1; + } else { + if(this.clockErrorCount>0) { + console.log("Timestamp error count: ", this.clockErrorCount); + console.log("Current timestamp: ", timestamp); + console.log("Last timestamp: ", this.lastTimestamp); + console.log("Last delta: ", this.midiClockDelta); + console.log("Current delta: ", timestamp - this.lastTimestamp); + console.log("BPMs", this.clockBuffer); + console.log("Deltas", this.deltaBuffer); + this.clockErrorCount = 0; + this.skipOnError = this.clockPPQN/4; // Skip quarter of the pulses + timestamp = 0; // timestamp 0 == lastTimestamp 0 + } else { + + if(this.midiClockDelta === 0) { + this.midiClockDelta = timestamp - this.lastTimestamp; + this.lastBPM = 60 * (1000 / this.midiClockDelta / 24); + } else { + const lastDelta = this.midiClockDelta * (1.0 - SMOOTH); + this.midiClockDelta = timestamp - this.lastTimestamp; + this.lastBPM = (60 * (1000 / (this.midiClockDelta*SMOOTH+lastDelta) / 24) * SMOOTH) + (this.lastBPM * (1.0 - SMOOTH)); + } + + this.deltaBuffer.push(this.midiClockDelta); + if(this.deltaBuffer.length>this.clockBufferLength) this.deltaBuffer.shift(); + + this.clockBuffer.push(this.lastBPM); + if(this.clockBuffer.length>this.clockBufferLength) this.clockBuffer.shift(); + + const estimatedBPM = this.estimatedBPM(); + if(estimatedBPM !== this.roundedBPM) { + this.api.bpm(estimatedBPM); + this.roundedBPM = estimatedBPM; + console.log(this.roundedBPM); + } + + } + } + } + + this.lastTimestamp = timestamp; + + } + public estimatedBPM(): number { /** * Returns the estimated BPM based on the last 24 MIDI clock messages. @@ -206,9 +276,6 @@ export class MidiConnection { return Math.round(sum / this.clockBuffer.length); } - - - public sendMidiClock(): void { /** * Sends a single MIDI clock message to the currently selected MIDI output. From 5d4c88d3558e0b4b0588e9979dce49c0629de925 Mon Sep 17 00:00:00 2001 From: Raphael Forment Date: Mon, 2 Oct 2023 20:55:58 +0200 Subject: [PATCH 3/9] Add send midi clock checkbox --- index.html | 32 +++++++++++++++++++------------- 1 file changed, 19 insertions(+), 13 deletions(-) diff --git a/index.html b/index.html index 8b43bf0..c0ffdbd 100644 --- a/index.html +++ b/index.html @@ -237,19 +237,25 @@

-
- - -
-
- - +
+
+
+ + +
+
+ + +
+
+ + +
From e856f045bbd902f50b09c3657de1885c5d7a70d4 Mon Sep 17 00:00:00 2001 From: Miika Alonen Date: Tue, 3 Oct 2023 22:33:19 +0300 Subject: [PATCH 4/9] Add start, stop and continue --- src/IO/MidiConnection.ts | 56 +++++++++++++++++++++------------------- src/classes/ZPlayer.ts | 1 + 2 files changed, 31 insertions(+), 26 deletions(-) diff --git a/src/IO/MidiConnection.ts b/src/IO/MidiConnection.ts index 621559b..9ee3cd5 100644 --- a/src/IO/MidiConnection.ts +++ b/src/IO/MidiConnection.ts @@ -1,4 +1,5 @@ import { UserAPI } from "../API"; +import { Clock } from "../Clock"; export class MidiConnection { /** @@ -85,21 +86,25 @@ export class MidiConnection { public sendStartMessage(): void { /** - * Sends a MIDI Start message to the currently selected MIDI output. + * Sends a MIDI Start message to the currently selected MIDI output and MIDI clock is not used */ - const output = this.midiOutputs[this.currentOutputIndex]; - if (output) { - output.send([0xfa]); // Send MIDI Start message + if(!this.midiClockInput) { + const output = this.midiOutputs[this.currentOutputIndex]; + if (output) { + output.send([0xfa]); // Send MIDI Start message + } } } public sendStopMessage(): void { /** - * Sends a MIDI Stop message to the currently selected MIDI output. + * Sends a MIDI Stop message to the currently selected MIDI output and MIDI clock is not used */ - const output = this.midiOutputs[this.currentOutputIndex]; - if (output) { - output.send([0xfc]); // Send MIDI Stop message + if(!this.midiClockInput) { + const output = this.midiOutputs[this.currentOutputIndex]; + if (output) { + output.send([0xfc]); // Send MIDI Stop message + } } } @@ -195,15 +200,19 @@ export class MidiConnection { } } else if(message.data[0] === 0xfa) { console.log("MIDI start received"); + this.api.stop(); + this.api.play(); } else if(message.data[0] === 0xfc) { console.log("MIDI stop received"); + this.api.pause(); } else if(message.data[0] === 0xfb) { console.log("MIDI continue received"); + this.api.play(); } else if(message.data[0] === 0xfe) { console.log("MIDI active sensing received"); } else { // Ignore other MIDI messages - console.log("Ignored MIDI message: ", message.data); + // console.log("Ignored MIDI message: ", message.data[0], message.data[1]); } } } @@ -232,32 +241,25 @@ export class MidiConnection { console.log("BPMs", this.clockBuffer); console.log("Deltas", this.deltaBuffer); this.clockErrorCount = 0; - this.skipOnError = this.clockPPQN/4; // Skip quarter of the pulses + /* I dont know why this happens. But when it does, deltas for the following messages are off. + So skipping ~ quarted of clock resolution usually helps */ + this.skipOnError = this.clockPPQN/4; timestamp = 0; // timestamp 0 == lastTimestamp 0 } else { - if(this.midiClockDelta === 0) { - this.midiClockDelta = timestamp - this.lastTimestamp; - this.lastBPM = 60 * (1000 / this.midiClockDelta / 24); - } else { - const lastDelta = this.midiClockDelta * (1.0 - SMOOTH); - this.midiClockDelta = timestamp - this.lastTimestamp; - this.lastBPM = (60 * (1000 / (this.midiClockDelta*SMOOTH+lastDelta) / 24) * SMOOTH) + (this.lastBPM * (1.0 - SMOOTH)); - } + this.midiClockDelta = timestamp - this.lastTimestamp; + this.lastBPM = 60 * (1000 / this.midiClockDelta / 24); - this.deltaBuffer.push(this.midiClockDelta); - if(this.deltaBuffer.length>this.clockBufferLength) this.deltaBuffer.shift(); - this.clockBuffer.push(this.lastBPM); if(this.clockBuffer.length>this.clockBufferLength) this.clockBuffer.shift(); const estimatedBPM = this.estimatedBPM(); if(estimatedBPM !== this.roundedBPM) { + console.log("Esimated BPM: ", estimatedBPM); this.api.bpm(estimatedBPM); this.roundedBPM = estimatedBPM; - console.log(this.roundedBPM); } - + } } } @@ -280,9 +282,11 @@ export class MidiConnection { /** * Sends a single MIDI clock message to the currently selected MIDI output. */ - const output = this.midiOutputs[this.currentOutputIndex]; - if (output) { - output.send([0xf8]); // Send a single MIDI clock message + if(!this.midiClockInput) { + const output = this.midiOutputs[this.currentOutputIndex]; + if (output) { + output.send([0xf8]); // Send a single MIDI clock message + } } } diff --git a/src/classes/ZPlayer.ts b/src/classes/ZPlayer.ts index 28f345b..dce79a8 100644 --- a/src/classes/ZPlayer.ts +++ b/src/classes/ZPlayer.ts @@ -99,6 +99,7 @@ export class Player extends Event { this.index = 0; this.waitTime = 0; this.skipIndex = 0; + this.ziffers.index = 0; } const patternIsStarting = (this.notStarted() && From c6b65c799f5988f015c06e25c9aeb272815ecbd0 Mon Sep 17 00:00:00 2001 From: Miika Alonen Date: Tue, 3 Oct 2023 23:28:29 +0300 Subject: [PATCH 5/9] Add send midi clock checkbox to localstorage --- src/AppSettings.ts | 7 +++++++ src/IO/MidiConnection.ts | 2 -- src/TransportNode.js | 2 +- src/main.ts | 13 ++++++++++++- 4 files changed, 20 insertions(+), 4 deletions(-) diff --git a/src/AppSettings.ts b/src/AppSettings.ts index 3cd2908..1caf8c0 100644 --- a/src/AppSettings.ts +++ b/src/AppSettings.ts @@ -44,6 +44,7 @@ export interface Settings { * @param line_numbers - Whether or not to show line numbers * @param time_position - Whether or not to show time position * @param tips - Whether or not to show tips + * @param send_clock - Whether or not to send midi clock */ vimMode: boolean; theme: string; @@ -54,6 +55,7 @@ export interface Settings { line_numbers: boolean; time_position: boolean; tips: boolean; + send_clock: boolean; } export const template_universe = { @@ -110,6 +112,7 @@ export class AppSettings { * @param line_numbers - Whether or not to show line numbers * @param time_position - Whether or not to show time position * @param tips - Whether or not to show tips + * @param send_clock - Whether or not to send midi clock */ @@ -122,6 +125,7 @@ export class AppSettings { public line_numbers: boolean = true; public time_position: boolean = true; public tips: boolean = true; + public send_clock: boolean = false; constructor() { const settingsFromStorage = JSON.parse( @@ -139,6 +143,7 @@ export class AppSettings { this.line_numbers = settingsFromStorage.line_numbers; this.time_position = settingsFromStorage.time_position; this.tips = settingsFromStorage.tips; + this.send_clock = settingsFromStorage.send_clock; } else { this.universes = template_universes; } @@ -162,6 +167,7 @@ export class AppSettings { line_numbers: this.line_numbers, time_position: this.time_position, tips: this.tips, + send_clock: this.send_clock, }; } @@ -183,6 +189,7 @@ export class AppSettings { this.line_numbers = settings.line_numbers; this.time_position = settings.time_position; this.tips = settings.tips; + this.send_clock = settings.send_clock; localStorage.setItem("topos", JSON.stringify(this.data)); } } diff --git a/src/IO/MidiConnection.ts b/src/IO/MidiConnection.ts index 9ee3cd5..3356962 100644 --- a/src/IO/MidiConnection.ts +++ b/src/IO/MidiConnection.ts @@ -1,5 +1,4 @@ import { UserAPI } from "../API"; -import { Clock } from "../Clock"; export class MidiConnection { /** @@ -223,7 +222,6 @@ export class MidiConnection { * Called when a MIDI clock message is received. */ - const SMOOTH = 0.1; this.clockTicks += 1; if(this.lastTimestamp > 0) { diff --git a/src/TransportNode.js b/src/TransportNode.js index c57acc8..282fb3d 100644 --- a/src/TransportNode.js +++ b/src/TransportNode.js @@ -13,7 +13,7 @@ export class TransportNode extends AudioWorkletNode { /** @type {(this: MessagePort, ev: MessageEvent) => any} */ handleMessage = (message) => { if (message.data && message.data.type === "bang") { - this.app.api.MidiConnection.sendMidiClock(); + if(this.app.settings.send_clock) this.app.api.MidiConnection.sendMidiClock(); this.app.clock.tick++; const futureTimeStamp = this.app.clock.convertTicksToTimeposition( this.app.clock.tick diff --git a/src/main.ts b/src/main.ts index ec39307..a193c54 100644 --- a/src/main.ts +++ b/src/main.ts @@ -80,7 +80,7 @@ export class Editor { exampleCounter: number = 0; exampleIsPlaying: boolean = false; - settings = new AppSettings(); + settings: AppSettings = new AppSettings(); editorExtensions: Extension[] = []; userPlugins: Extension[] = []; state: EditorState; @@ -193,6 +193,10 @@ export class Editor { "show-tips" ) as HTMLInputElement; + midi_clock_checkbox: HTMLInputElement = document.getElementById( + "send-midi-clock" + ) as HTMLInputElement; + // Editor mode selection normal_mode_button: HTMLButtonElement = document.getElementById( "normal-mode" @@ -241,6 +245,7 @@ export class Editor { this.line_numbers_checkbox.checked = this.settings.line_numbers; this.time_position_checkbox.checked = this.settings.time_position; this.tips_checkbox.checked = this.settings.tips; + this.midi_clock_checkbox.checked = this.settings.send_clock; if (!this.settings.time_position) { document.getElementById("timeviewer")!.classList.add("hidden"); } @@ -559,6 +564,7 @@ export class Editor { this.line_numbers_checkbox.checked = this.settings.line_numbers; this.time_position_checkbox.checked = this.settings.time_position; this.tips_checkbox.checked = this.settings.tips; + this.midi_clock_checkbox.checked = this.settings.send_clock; if (this.settings.vimMode) { let vim = document.getElementById("vim-mode-radio") as HTMLInputElement; @@ -654,6 +660,11 @@ export class Editor { }); }); + this.midi_clock_checkbox.addEventListener("change", () => { + let checked = this.midi_clock_checkbox.checked ? true : false; + this.settings.send_clock = checked; + }); + this.vim_mode_button.addEventListener("click", () => { this.settings.vimMode = true; this.view.dispatch({ From 327bfc5086c9b0e15bf4c0cbdcb4cc0f0b56defc Mon Sep 17 00:00:00 2001 From: Miika Alonen Date: Wed, 4 Oct 2023 00:20:50 +0300 Subject: [PATCH 6/9] Store Midi clock stuff to localstorage --- src/API.ts | 2 +- src/AppSettings.ts | 14 ++++++++++++++ src/IO/MidiConnection.ts | 34 +++++++++++++++++++--------------- src/main.ts | 11 +++++++++++ 4 files changed, 45 insertions(+), 16 deletions(-) diff --git a/src/API.ts b/src/API.ts index 3627c8b..7d07c96 100644 --- a/src/API.ts +++ b/src/API.ts @@ -60,7 +60,7 @@ export class UserAPI { load: samples; constructor(public app: Editor) { - this.MidiConnection = new MidiConnection(this); + this.MidiConnection = new MidiConnection(this, app.settings); } _loadUniverseFromInterface = (universe: string) => { diff --git a/src/AppSettings.ts b/src/AppSettings.ts index 1caf8c0..608d5f6 100644 --- a/src/AppSettings.ts +++ b/src/AppSettings.ts @@ -45,6 +45,8 @@ export interface Settings { * @param time_position - Whether or not to show time position * @param tips - Whether or not to show tips * @param send_clock - Whether or not to send midi clock + * @param midi_clock_input - The name of the midi clock input + * @param midi_clock_ppqn - The pulses per quarter note for midi clock */ vimMode: boolean; theme: string; @@ -56,6 +58,8 @@ export interface Settings { time_position: boolean; tips: boolean; send_clock: boolean; + midi_clock_input: string|undefined; + midi_clock_ppqn: number; } export const template_universe = { @@ -113,6 +117,8 @@ export class AppSettings { * @param time_position - Whether or not to show time position * @param tips - Whether or not to show tips * @param send_clock - Whether or not to send midi clock + * @param midi_clock_input - The name of the midi clock input + * @param midi_clock_ppqn - The pulses per quarter note for midi clock */ @@ -126,6 +132,8 @@ export class AppSettings { public time_position: boolean = true; public tips: boolean = true; public send_clock: boolean = false; + public midi_clock_input: string|undefined = undefined; + public midi_clock_ppqn: number = 24; constructor() { const settingsFromStorage = JSON.parse( @@ -144,6 +152,8 @@ export class AppSettings { this.time_position = settingsFromStorage.time_position; this.tips = settingsFromStorage.tips; this.send_clock = settingsFromStorage.send_clock; + this.midi_clock_input = settingsFromStorage.midi_clock_input; + this.midi_clock_ppqn = settingsFromStorage.midi_clock_ppqn || 24; } else { this.universes = template_universes; } @@ -168,6 +178,8 @@ export class AppSettings { time_position: this.time_position, tips: this.tips, send_clock: this.send_clock, + midi_clock_input: this.midi_clock_input, + midi_clock_ppqn: this.midi_clock_ppqn, }; } @@ -190,6 +202,8 @@ export class AppSettings { this.time_position = settings.time_position; this.tips = settings.tips; this.send_clock = settings.send_clock; + this.midi_clock_input = settings.midi_clock_input; + this.midi_clock_ppqn = settings.midi_clock_ppqn; localStorage.setItem("topos", JSON.stringify(this.data)); } } diff --git a/src/IO/MidiConnection.ts b/src/IO/MidiConnection.ts index 3356962..977e440 100644 --- a/src/IO/MidiConnection.ts +++ b/src/IO/MidiConnection.ts @@ -1,4 +1,5 @@ import { UserAPI } from "../API"; +import { AppSettings } from "../AppSettings"; export class MidiConnection { /** @@ -12,6 +13,7 @@ export class MidiConnection { */ private api: UserAPI; + private settings: AppSettings; private midiAccess: MIDIAccess | null = null; public midiOutputs: MIDIOutput[] = []; public midiInputs: MIDIInput[] = []; @@ -28,13 +30,13 @@ export class MidiConnection { private clockBuffer: number[] = []; private deltaBuffer: number[] = []; private clockBufferLength = 24; - private clockPPQN = 24; private clockTicks = 0; private clockErrorCount = 0; private skipOnError = 0; - constructor(api: UserAPI) { + constructor(api: UserAPI, settings: AppSettings) { this.api = api; + this.settings = settings; this.lastBPM = api.bpm(); this.roundedBPM = this.lastBPM; this.initializeMidiAccess(); @@ -58,7 +60,6 @@ export class MidiConnection { console.warn("No MIDI inputs available."); } else { this.updateMidiClockSelect(); - this.clockPPQNSelect(); } } catch (error) { console.error("Failed to initialize MIDI:", error); @@ -156,34 +157,37 @@ export class MidiConnection { this.midiInputs.forEach((input, index) => { const option = document.createElement("option"); option.value = index.toString(); - option.text = input.name || "No name input"; + option.text = input.name || index.toString(); select.appendChild(option); }); - select.value = this.currentInputIndex ? this.currentInputIndex.toString() : "-1"; + if(this.settings.midi_clock_input) { + const clockMidiInputIndex = this.getMidiInputIndex(this.settings.midi_clock_input); + select.value = clockMidiInputIndex.toString(); + if(clockMidiInputIndex > 0) { + this.midiClockInput = this.midiInputs[clockMidiInputIndex]; + this.registerMidiClockListener(); + } + } else { + select.value = "-1"; + } // Add listener select.addEventListener("change", (event) => { const value = (event.target as HTMLSelectElement).value; if(value === "-1") { if(this.midiClockInput) this.midiClockInput.onmidimessage = null; this.midiClockInput = undefined; + this.settings.midi_clock_input = undefined; } else { this.currentInputIndex = parseInt(value); if(this.midiClockInput) this.midiClockInput.onmidimessage = null; this.midiClockInput = this.midiInputs[this.currentInputIndex]; this.registerMidiClockListener(); + this.settings.midi_clock_input = this.midiClockInput.name || undefined; } }); } } - clockPPQNSelect(): void { - const select = document.getElementById("midi-clock-ppqn-input") as HTMLSelectElement; - select.addEventListener("change", (event) => { - const value = (event.target as HTMLSelectElement).value; - this.clockPPQN = parseInt(value); - }); - } - public registerMidiClockListener(): void { /** * Registers a listener for MIDI clock messages on the currently selected MIDI input. @@ -241,12 +245,12 @@ export class MidiConnection { this.clockErrorCount = 0; /* I dont know why this happens. But when it does, deltas for the following messages are off. So skipping ~ quarted of clock resolution usually helps */ - this.skipOnError = this.clockPPQN/4; + this.skipOnError = this.settings.midi_clock_ppqn/4; timestamp = 0; // timestamp 0 == lastTimestamp 0 } else { this.midiClockDelta = timestamp - this.lastTimestamp; - this.lastBPM = 60 * (1000 / this.midiClockDelta / 24); + this.lastBPM = 60 * (1000 / this.midiClockDelta / this.settings.midi_clock_ppqn); this.clockBuffer.push(this.lastBPM); if(this.clockBuffer.length>this.clockBufferLength) this.clockBuffer.shift(); diff --git a/src/main.ts b/src/main.ts index a193c54..9f821d4 100644 --- a/src/main.ts +++ b/src/main.ts @@ -197,6 +197,10 @@ export class Editor { "send-midi-clock" ) as HTMLInputElement; + midi_clock_ppqn: HTMLSelectElement = document.getElementById( + "midi-clock-ppqn-input" + ) as HTMLSelectElement; + // Editor mode selection normal_mode_button: HTMLButtonElement = document.getElementById( "normal-mode" @@ -246,6 +250,7 @@ export class Editor { this.time_position_checkbox.checked = this.settings.time_position; this.tips_checkbox.checked = this.settings.tips; this.midi_clock_checkbox.checked = this.settings.send_clock; + this.midi_clock_ppqn.value = this.settings.midi_clock_ppqn.toString(); if (!this.settings.time_position) { document.getElementById("timeviewer")!.classList.add("hidden"); } @@ -565,6 +570,7 @@ export class Editor { this.time_position_checkbox.checked = this.settings.time_position; this.tips_checkbox.checked = this.settings.tips; this.midi_clock_checkbox.checked = this.settings.send_clock; + this.midi_clock_ppqn.value = this.settings.midi_clock_ppqn.toString(); if (this.settings.vimMode) { let vim = document.getElementById("vim-mode-radio") as HTMLInputElement; @@ -665,6 +671,11 @@ export class Editor { this.settings.send_clock = checked; }); + this.midi_clock_ppqn.addEventListener("change", () => { + let value = parseInt(this.midi_clock_ppqn.value); + this.settings.midi_clock_ppqn = value; + }); + this.vim_mode_button.addEventListener("click", () => { this.settings.vimMode = true; this.view.dispatch({ From 1224bd7b328aee583b343d503709cad630feb471 Mon Sep 17 00:00:00 2001 From: Miika Alonen Date: Thu, 5 Oct 2023 20:40:45 +0300 Subject: [PATCH 7/9] Added default midi in port and routing to scripts --- index.html | 12 ++++++ src/AppSettings.ts | 15 ++++++- src/IO/MidiConnection.ts | 92 +++++++++++++++++++++++++++++++++++----- src/main.ts | 11 +++++ 4 files changed, 118 insertions(+), 12 deletions(-) diff --git a/index.html b/index.html index f67fa6f..5c4e15d 100644 --- a/index.html +++ b/index.html @@ -261,6 +261,18 @@

Send MIDI Clock

+
+
+ + +
+
+ + +
+
diff --git a/src/AppSettings.ts b/src/AppSettings.ts index 97f414c..c40c628 100644 --- a/src/AppSettings.ts +++ b/src/AppSettings.ts @@ -45,8 +45,10 @@ export interface Settings { * @param time_position - Whether or not to show time position * @param tips - Whether or not to show tips * @param send_clock - Whether or not to send midi clock + * @param midi_channels_scripts - Whether midi input channels fires scripts * @param midi_clock_input - The name of the midi clock input * @param midi_clock_ppqn - The pulses per quarter note for midi clock + * @param default_midi_input - The default midi input for incoming messages */ vimMode: boolean; theme: string; @@ -59,8 +61,10 @@ export interface Settings { load_demo_songs: boolean; tips: boolean; send_clock: boolean; + midi_channels_scripts: boolean; midi_clock_input: string|undefined; midi_clock_ppqn: number; + default_midi_input: string|undefined; } export const template_universe = { @@ -118,9 +122,10 @@ export class AppSettings { * @param time_position - Whether or not to show time position * @param tips - Whether or not to show tips * @param send_clock - Whether or not to send midi clock + * @param midi_channels_scripts - Whether midi input channels fires scripts * @param midi_clock_input - The name of the midi clock input * @param midi_clock_ppqn - The pulses per quarter note for midi clock - + * @param default_midi_input - The default midi input for incoming messages */ public vimMode: boolean = false; @@ -133,7 +138,9 @@ export class AppSettings { public time_position: boolean = true; public tips: boolean = true; public send_clock: boolean = false; + public midi_channels_scripts: boolean = true; public midi_clock_input: string|undefined = undefined; + public default_midi_input: string|undefined = undefined; public midi_clock_ppqn: number = 24; public load_demo_songs: boolean = true; @@ -154,8 +161,10 @@ export class AppSettings { this.time_position = settingsFromStorage.time_position; this.tips = settingsFromStorage.tips; this.send_clock = settingsFromStorage.send_clock; + this.midi_channels_scripts = settingsFromStorage.midi_channels_scripts; this.midi_clock_input = settingsFromStorage.midi_clock_input; this.midi_clock_ppqn = settingsFromStorage.midi_clock_ppqn || 24; + this.default_midi_input = settingsFromStorage.default_midi_input; this.load_demo_songs = settingsFromStorage.load_demo_songs; } else { this.universes = template_universes; @@ -181,8 +190,10 @@ export class AppSettings { time_position: this.time_position, tips: this.tips, send_clock: this.send_clock, + midi_channels_scripts: this.midi_channels_scripts, midi_clock_input: this.midi_clock_input, midi_clock_ppqn: this.midi_clock_ppqn, + default_midi_input: this.default_midi_input, load_demo_songs: this.load_demo_songs, }; } @@ -206,8 +217,10 @@ export class AppSettings { this.time_position = settings.time_position; this.tips = settings.tips; this.send_clock = settings.send_clock; + this.midi_channels_scripts = settings.midi_channels_scripts; this.midi_clock_input = settings.midi_clock_input; this.midi_clock_ppqn = settings.midi_clock_ppqn; + this.default_midi_input = settings.default_midi_input; this.load_demo_songs = settings.load_demo_songs; localStorage.setItem("topos", JSON.stringify(this.data)); } diff --git a/src/IO/MidiConnection.ts b/src/IO/MidiConnection.ts index 977e440..a7fec62 100644 --- a/src/IO/MidiConnection.ts +++ b/src/IO/MidiConnection.ts @@ -146,45 +146,76 @@ export class MidiConnection { * Updates the MIDI clock input select element with the available MIDI inputs. */ if(this.midiInputs.length > 0) { - const select = document.getElementById("midi-clock-input") as HTMLSelectElement; - select.innerHTML = ""; + const midiClockSelect = document.getElementById("midi-clock-input") as HTMLSelectElement; + const midiInputSelect = document.getElementById("default-midi-input") as HTMLSelectElement; + + midiClockSelect.innerHTML = ""; + midiInputSelect.innerHTML = ""; + // Defaults to internal clock const defaultOption = document.createElement("option"); defaultOption.value = "-1"; defaultOption.text = "Internal"; - select.appendChild(defaultOption); - // Add MIDI inputs to clock select input + midiClockSelect.appendChild(defaultOption); + + // Add MIDI inputs to clock select input and default midi input this.midiInputs.forEach((input, index) => { const option = document.createElement("option"); option.value = index.toString(); option.text = input.name || index.toString(); - select.appendChild(option); + midiClockSelect.appendChild(option); + midiInputSelect.appendChild(option.cloneNode(true)); }); + if(this.settings.midi_clock_input) { const clockMidiInputIndex = this.getMidiInputIndex(this.settings.midi_clock_input); - select.value = clockMidiInputIndex.toString(); + midiClockSelect.value = clockMidiInputIndex.toString(); if(clockMidiInputIndex > 0) { this.midiClockInput = this.midiInputs[clockMidiInputIndex]; this.registerMidiClockListener(); } } else { - select.value = "-1"; + midiClockSelect.value = "-1"; + } + + if(this.settings.default_midi_input) { + const defaultMidiInputIndex = this.getMidiInputIndex(this.settings.default_midi_input); + midiInputSelect.value = defaultMidiInputIndex.toString(); + if(defaultMidiInputIndex > 0) { + this.currentInputIndex = defaultMidiInputIndex; + this.registerMidiInputListener(); + } + } else { + midiInputSelect.value = ""; } - // Add listener - select.addEventListener("change", (event) => { + + // Add midi clock listener + midiClockSelect.addEventListener("change", (event) => { const value = (event.target as HTMLSelectElement).value; if(value === "-1") { if(this.midiClockInput) this.midiClockInput.onmidimessage = null; this.midiClockInput = undefined; this.settings.midi_clock_input = undefined; } else { - this.currentInputIndex = parseInt(value); + const clockInputIndex = parseInt(value); if(this.midiClockInput) this.midiClockInput.onmidimessage = null; - this.midiClockInput = this.midiInputs[this.currentInputIndex]; + this.midiClockInput = this.midiInputs[clockInputIndex]; this.registerMidiClockListener(); this.settings.midi_clock_input = this.midiClockInput.name || undefined; } }); + + // Add mini input listener + midiInputSelect.addEventListener("change", (event) => { + const value = (event.target as HTMLSelectElement).value; + if(value) { + this.unregisterMidiInputListener(); + this.currentInputIndex = parseInt(value); + this.registerMidiInputListener(); + this.settings.default_midi_input = this.midiInputs[this.currentInputIndex].name || undefined; + } + }); + } } @@ -221,6 +252,45 @@ export class MidiConnection { } } + public registerMidiInputListener(): void { + /** + * Register midi input listener and store last value as global parameter named channel_{number} + */ + if(this.currentInputIndex !== undefined) { + const input = this.midiInputs[this.currentInputIndex]; + if(input) { + input.onmidimessage = (event: Event) => { + const message = event as MIDIMessageEvent; + + // 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; + 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); + } + } + } + } + } + + public unregisterMidiInputListener(): void { + /** + * Unregister midi input listener + */ + if(this.currentInputIndex !== undefined) { + const input = this.midiInputs[this.currentInputIndex]; + if(input) { + input.onmidimessage = null; + } + } + } + + public onMidiClock(timestamp: number): void { /** * Called when a MIDI clock message is received. diff --git a/src/main.ts b/src/main.ts index 8a60818..629eb9a 100644 --- a/src/main.ts +++ b/src/main.ts @@ -212,6 +212,10 @@ export class Editor { "send-midi-clock" ) as HTMLInputElement; + midi_channels_scripts: HTMLInputElement = document.getElementById( + "midi-channels-scripts" + ) as HTMLInputElement; + midi_clock_ppqn: HTMLSelectElement = document.getElementById( "midi-clock-ppqn-input" ) as HTMLSelectElement; @@ -262,6 +266,7 @@ export class Editor { this.time_position_checkbox.checked = this.settings.time_position; this.tips_checkbox.checked = this.settings.tips; this.midi_clock_checkbox.checked = this.settings.send_clock; + this.midi_channels_scripts.checked = this.settings.midi_channels_scripts; this.midi_clock_ppqn.value = this.settings.midi_clock_ppqn.toString(); if (!this.settings.time_position) { document.getElementById("timeviewer")!.classList.add("hidden"); @@ -605,6 +610,7 @@ export class Editor { this.time_position_checkbox.checked = this.settings.time_position; this.tips_checkbox.checked = this.settings.tips; this.midi_clock_checkbox.checked = this.settings.send_clock; + this.midi_channels_scripts.checked = this.settings.midi_channels_scripts; this.midi_clock_ppqn.value = this.settings.midi_clock_ppqn.toString(); this.load_demo_songs.checked = this.settings.load_demo_songs; @@ -707,6 +713,11 @@ export class Editor { this.settings.send_clock = checked; }); + this.midi_channels_scripts.addEventListener("change", () => { + let checked = this.midi_channels_scripts.checked ? true : false; + this.settings.midi_channels_scripts = checked; + }); + this.midi_clock_ppqn.addEventListener("change", () => { let value = parseInt(this.midi_clock_ppqn.value); this.settings.midi_clock_ppqn = value; From a6728cc9075f3def63a4b99c6228b112f26b307f Mon Sep 17 00:00:00 2001 From: Miika Alonen Date: Thu, 5 Oct 2023 22:29:31 +0300 Subject: [PATCH 8/9] Now listening both clock and other events --- index.html | 2 +- src/IO/MidiConnection.ts | 134 ++++++++++++++++++++------------------- 2 files changed, 69 insertions(+), 67 deletions(-) diff --git a/index.html b/index.html index 5c4e15d..97244f8 100644 --- a/index.html +++ b/index.html @@ -265,7 +265,7 @@

diff --git a/src/IO/MidiConnection.ts b/src/IO/MidiConnection.ts index a7fec62..d07fecf 100644 --- a/src/IO/MidiConnection.ts +++ b/src/IO/MidiConnection.ts @@ -22,13 +22,13 @@ export class MidiConnection { private scheduledNotes: { [noteNumber: number]: number } = {}; // { noteNumber: timeoutId } /* MIDI clock stuff */ + private midiClockInputIndex: number|undefined = undefined; private midiClockInput?: MIDIInput|undefined = undefined; private lastTimestamp: number = 0; private midiClockDelta: number = 0; private lastBPM: number; private roundedBPM: number = 0; private clockBuffer: number[] = []; - private deltaBuffer: number[] = []; private clockBufferLength = 24; private clockTicks = 0; private clockErrorCount = 0; @@ -59,7 +59,7 @@ export class MidiConnection { if (this.midiInputs.length === 0) { console.warn("No MIDI inputs available."); } else { - this.updateMidiClockSelect(); + this.updateInputSelects(); } } catch (error) { console.error("Failed to initialize MIDI:", error); @@ -134,14 +134,15 @@ export class MidiConnection { */ const inputIndex = this.getMidiInputIndex(inputName); if (inputIndex !== -1) { + this.midiClockInputIndex = inputIndex; this.midiClockInput = this.midiInputs[inputIndex]; - this.registerMidiClockListener(); + this.registerMidiInputListener(inputIndex); } else { this.midiClockInput = undefined; } } - public updateMidiClockSelect() { + public updateInputSelects() { /** * Updates the MIDI clock input select element with the available MIDI inputs. */ @@ -152,12 +153,18 @@ export class MidiConnection { midiClockSelect.innerHTML = ""; midiInputSelect.innerHTML = ""; - // Defaults to internal clock + // Set Midi clock as Internal by default const defaultOption = document.createElement("option"); defaultOption.value = "-1"; defaultOption.text = "Internal"; midiClockSelect.appendChild(defaultOption); + // Set default input as None by default + const defaultInputOption = document.createElement("option"); + defaultInputOption.value = "-1"; + defaultInputOption.text = "None"; + midiInputSelect.appendChild(defaultInputOption); + // Add MIDI inputs to clock select input and default midi input this.midiInputs.forEach((input, index) => { const option = document.createElement("option"); @@ -172,7 +179,7 @@ export class MidiConnection { midiClockSelect.value = clockMidiInputIndex.toString(); if(clockMidiInputIndex > 0) { this.midiClockInput = this.midiInputs[clockMidiInputIndex]; - this.registerMidiClockListener(); + this.registerMidiInputListener(clockMidiInputIndex); } } else { midiClockSelect.value = "-1"; @@ -183,24 +190,25 @@ export class MidiConnection { midiInputSelect.value = defaultMidiInputIndex.toString(); if(defaultMidiInputIndex > 0) { this.currentInputIndex = defaultMidiInputIndex; - this.registerMidiInputListener(); + this.registerMidiInputListener(defaultMidiInputIndex); } } else { - midiInputSelect.value = ""; + midiInputSelect.value = "-1"; } // Add midi clock listener midiClockSelect.addEventListener("change", (event) => { const value = (event.target as HTMLSelectElement).value; if(value === "-1") { - if(this.midiClockInput) this.midiClockInput.onmidimessage = null; + if(this.midiClockInput && this.midiClockInputIndex!=this.currentInputIndex) this.midiClockInput.onmidimessage = null; this.midiClockInput = undefined; this.settings.midi_clock_input = undefined; } else { const clockInputIndex = parseInt(value); - if(this.midiClockInput) this.midiClockInput.onmidimessage = null; + this.midiClockInputIndex = clockInputIndex; + if(this.midiClockInput && this.midiClockInputIndex!=this.currentInputIndex) this.midiClockInput.onmidimessage = null; this.midiClockInput = this.midiInputs[clockInputIndex]; - this.registerMidiClockListener(); + this.registerMidiInputListener(clockInputIndex); this.settings.midi_clock_input = this.midiClockInput.name || undefined; } }); @@ -208,10 +216,14 @@ export class MidiConnection { // Add mini input listener midiInputSelect.addEventListener("change", (event) => { const value = (event.target as HTMLSelectElement).value; - if(value) { - this.unregisterMidiInputListener(); + if(value === "-1") { + if(this.currentInputIndex && this.currentInputIndex!=this.midiClockInputIndex) this.unregisterMidiInputListener(this.currentInputIndex); + this.currentInputIndex = undefined; + this.settings.default_midi_input = undefined; + } else { + if(this.currentInputIndex && this.currentInputIndex!=this.midiClockInputIndex) this.unregisterMidiInputListener(this.currentInputIndex); this.currentInputIndex = parseInt(value); - this.registerMidiInputListener(); + this.registerMidiInputListener(this.currentInputIndex); this.settings.default_midi_input = this.midiInputs[this.currentInputIndex].name || undefined; } }); @@ -219,71 +231,62 @@ export class MidiConnection { } } - public registerMidiClockListener(): void { - /** - * Registers a listener for MIDI clock messages on the currently selected MIDI input. - */ - if (this.midiClockInput) { - this.midiClockInput.onmidimessage = (event: Event) => { - const message = event as MIDIMessageEvent; - if (message.data[0] === 0xf8) { - if(this.skipOnError>0) { - this.skipOnError -= 1; - } else { - this.onMidiClock(event.timeStamp); - } - } else if(message.data[0] === 0xfa) { - console.log("MIDI start received"); - this.api.stop(); - this.api.play(); - } else if(message.data[0] === 0xfc) { - console.log("MIDI stop received"); - this.api.pause(); - } else if(message.data[0] === 0xfb) { - console.log("MIDI continue received"); - this.api.play(); - } else if(message.data[0] === 0xfe) { - console.log("MIDI active sensing received"); - } else { - // Ignore other MIDI messages - // console.log("Ignored MIDI message: ", message.data[0], message.data[1]); - } - } - } - } - - public registerMidiInputListener(): void { + public registerMidiInputListener(inputIndex: number): void { /** * Register midi input listener and store last value as global parameter named channel_{number} */ - if(this.currentInputIndex !== undefined) { - const input = this.midiInputs[this.currentInputIndex]; - if(input) { + if(inputIndex !== undefined) { + const input = this.midiInputs[inputIndex]; + if(input && !input.onmidimessage) { input.onmidimessage = (event: Event) => { const message = event as MIDIMessageEvent; - - // 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; - 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); + /* MIDI CLOCK */ + if(input.name === this.settings.midi_clock_input) { + if (message.data[0] === 0xf8) { + if(this.skipOnError>0) { + this.skipOnError -= 1; + } else { + this.onMidiClock(event.timeStamp); + } + } else if(message.data[0] === 0xfa) { + console.log("MIDI start received"); + this.api.stop(); + this.api.play(); + } else if(message.data[0] === 0xfc) { + console.log("MIDI stop received"); + this.api.pause(); + } else if(message.data[0] === 0xfb) { + console.log("MIDI continue received"); + this.api.play(); + } else if(message.data[0] === 0xfe) { + console.log("MIDI active sensing received"); + } + } + /* 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; + 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); + } } } } } } - public unregisterMidiInputListener(): void { + public unregisterMidiInputListener(inputIndex: number): void { /** * Unregister midi input listener */ - if(this.currentInputIndex !== undefined) { - const input = this.midiInputs[this.currentInputIndex]; + if(inputIndex !== undefined) { + const input = this.midiInputs[inputIndex]; if(input) { input.onmidimessage = null; } @@ -311,7 +314,6 @@ export class MidiConnection { console.log("Last delta: ", this.midiClockDelta); console.log("Current delta: ", timestamp - this.lastTimestamp); console.log("BPMs", this.clockBuffer); - console.log("Deltas", this.deltaBuffer); this.clockErrorCount = 0; /* I dont know why this happens. But when it does, deltas for the following messages are off. So skipping ~ quarted of clock resolution usually helps */ From 825c6f5487a727c659286b48538801d957b9c33f Mon Sep 17 00:00:00 2001 From: Miika Alonen Date: Fri, 6 Oct 2023 21:06:09 +0300 Subject: [PATCH 9/9] Add commands to API --- index.html | 4 +- src/API.ts | 129 ++++++++++++++++++++++++++++++- src/IO/MidiConnection.ts | 160 +++++++++++++++++++++++++++++++++++++-- 3 files changed, 283 insertions(+), 10 deletions(-) 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