Some error handling and smoothing estimated bpm
This commit is contained in:
@ -239,11 +239,18 @@
|
|||||||
<!-- Midi settings -->
|
<!-- Midi settings -->
|
||||||
<div class="flex flex-row">
|
<div class="flex flex-row">
|
||||||
<div class="flex items-center mb-4 ml-8">
|
<div class="flex items-center mb-4 ml-8">
|
||||||
Midi clock:
|
<label for="default-checkbox" class="ml-2 text-sm font-medium text-dark">Midi clock: </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">
|
<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>
|
<option value="-1">Internal</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</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: </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>
|
</div>
|
||||||
<!-- Information card -->
|
<!-- 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">
|
<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">
|
||||||
|
|||||||
@ -11,22 +11,32 @@ export class MidiConnection {
|
|||||||
* @param scheduledNotes - Object containing scheduled notes. Keys are note numbers and values are timeout IDs.
|
* @param scheduledNotes - Object containing scheduled notes. Keys are note numbers and values are timeout IDs.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
private api: UserAPI;
|
||||||
private midiAccess: MIDIAccess | null = null;
|
private midiAccess: MIDIAccess | null = null;
|
||||||
public midiOutputs: MIDIOutput[] = [];
|
public midiOutputs: MIDIOutput[] = [];
|
||||||
public midiInputs: MIDIInput[] = [];
|
public midiInputs: MIDIInput[] = [];
|
||||||
private currentOutputIndex: number = 0;
|
private currentOutputIndex: number = 0;
|
||||||
private currentInputIndex: number|undefined = undefined;
|
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 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) {
|
constructor(api: UserAPI) {
|
||||||
this.api = api;
|
this.api = api;
|
||||||
this.lastBPM = api.bpm();
|
this.lastBPM = api.bpm();
|
||||||
|
this.roundedBPM = this.lastBPM;
|
||||||
this.initializeMidiAccess();
|
this.initializeMidiAccess();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -48,6 +58,7 @@ export class MidiConnection {
|
|||||||
console.warn("No MIDI inputs available.");
|
console.warn("No MIDI inputs available.");
|
||||||
} else {
|
} else {
|
||||||
this.updateMidiClockSelect();
|
this.updateMidiClockSelect();
|
||||||
|
this.clockPPQNSelect();
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Failed to initialize MIDI:", 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 {
|
public registerMidiClockListener(): void {
|
||||||
/**
|
/**
|
||||||
* Registers a listener for MIDI clock messages on the currently selected MIDI input.
|
* 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) => {
|
this.midiClockInput.onmidimessage = (event: Event) => {
|
||||||
const message = event as MIDIMessageEvent;
|
const message = event as MIDIMessageEvent;
|
||||||
if (message.data[0] === 0xf8) {
|
if (message.data[0] === 0xf8) {
|
||||||
const timestamp = performance.now();
|
if(this.skipOnError>0) {
|
||||||
const delta = timestamp - this.lastClockTime;
|
this.skipOnError -= 1;
|
||||||
const bpm = 60 * (1000 / delta / 24);
|
} else {
|
||||||
this.lastClockTime = timestamp;
|
this.onMidiClock(event.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) {
|
} else if(message.data[0] === 0xfa) {
|
||||||
console.log("MIDI start received");
|
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 {
|
public estimatedBPM(): number {
|
||||||
/**
|
/**
|
||||||
* Returns the estimated BPM based on the last 24 MIDI clock messages.
|
* 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);
|
return Math.round(sum / this.clockBuffer.length);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
public sendMidiClock(): void {
|
public sendMidiClock(): void {
|
||||||
/**
|
/**
|
||||||
* Sends a single MIDI clock message to the currently selected MIDI output.
|
* Sends a single MIDI clock message to the currently selected MIDI output.
|
||||||
|
|||||||
Reference in New Issue
Block a user