Some error handling and smoothing estimated bpm

This commit is contained in:
2023-10-02 21:36:23 +03:00
parent cf45bf7952
commit 05a4f8a161
2 changed files with 94 additions and 20 deletions

View File

@ -239,11 +239,18 @@
<!-- Midi settings -->
<div class="flex flex-row">
<div class="flex items-center mb-4 ml-8">
Midi clock:&nbsp;
<label for="default-checkbox" class="ml-2 text-sm font-medium text-dark">Midi clock:&nbsp;</label>
<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 class="flex items-center mb-4 ml-8">
<label for="default-checkbox" class="ml-2 text-sm font-medium text-dark">Clock ppqn:&nbsp;</label>
<select id="midi-clock-ppqn-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="24">24</option>
<option value="48">48</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">

View File

@ -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 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;
/* MIDI clock stuff */
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 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.