First attempt
This commit is contained in:
@ -236,6 +236,15 @@
|
||||
<label for="default-checkbox" class="ml-2 text-sm font-medium text-dark">Show Hovering Tips</label>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Midi settings -->
|
||||
<div class="flex flex-row">
|
||||
<div class="flex items-center mb-4 ml-8">
|
||||
Midi clock:
|
||||
<select id="midi-clock-input" class="w-32 h-8 text-sm font-medium text-black bg-gray-100 border-gray-300 rounded focus:ring-blue-500 dark:focus:ring-blue-600 dark:ring-offset-gray-800 focus:ring-2 dark:bg-gray-700 dark:border-gray-600">
|
||||
<option value="-1">Internal</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Information card -->
|
||||
<div class="flex lg:flex-row space-y-2 lg:space-y-0 flex-col w-auto min-w-screen px-4 lg:space-x-8 space-x-0">
|
||||
<a href="https://github.com/Bubobubobubobubo/Topos" class="block max-w-sm p-6 border border-gray-200 rounded-lg shadow bg-gray-800 border-gray-700 hover:bg-gray-700">
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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.
|
||||
|
||||
Reference in New Issue
Block a user