fix: round interpolated MIDI values to integers to deduplicate repeated consecutive values

This commit is contained in:
Xiaohan-Tian
2026-05-06 20:01:46 -07:00
parent 738628fc76
commit 0d9e2a9f9b
4 changed files with 119 additions and 3 deletions
+35
View File
@@ -71,6 +71,30 @@ describe('midiAutomationUtil', () => {
expect(bakeMidiAutomationPointsInWindow(points, 0, 1, { ...defaultOptions, maxIntervalMs: 5 })).toHaveLength(100);
});
it('quantizes the window anchor to an integer MIDI pitch-bend value', () => {
const points = [
{ beat: 0, value: 8192 },
{ beat: 1, value: 8193 },
];
expect(bakeMidiAutomationPointsInWindow(points, 0.5, 1, { ...defaultOptions, maxIntervalMs: 20 })[0]).toEqual({
beat: 0.5,
value: 8193,
});
});
it('drops adjacent interpolated points that round to the same MIDI value', () => {
const points = [
{ beat: 0, value: 8192 },
{ beat: 1, value: 8193 },
];
expect(bakeMidiAutomationPointsInWindow(points, 0, 1, { ...defaultOptions, maxIntervalMs: 20 })).toEqual([
{ beat: 0, value: 8192 },
{ beat: 0.52, value: 8193 },
]);
});
it('treats flat segments as holds without adding interior baked points', () => {
const points = [
{ beat: 0, value: 4096 },
@@ -112,6 +136,17 @@ describe('midiAutomationUtil', () => {
]);
});
it('stores baked interpolated values as integers', () => {
const points = [
{ beat: 0, value: 8192 },
{ beat: 3, value: 8195 },
];
const baked = bakeMidiAutomationPointsInWindow(points, 0, 3, { ...defaultOptions, maxIntervalMs: 500 });
expect(baked.every(point => Number.isInteger(point.value))).toBe(true);
});
it('adds a window anchor and preserves the correct loop boundary value', () => {
const points = [
{ beat: 2, value: 0 },
+25 -3
View File
@@ -1,3 +1,5 @@
import { clampMidiPitchBendValue } from './midiUtil';
export interface MidiAutomationPoint {
beat: number;
value: number;
@@ -16,6 +18,10 @@ export interface MidiAutomationBakeOptions {
const BEAT_EPSILON = 1e-9;
function quantizeBakedValue(value: number): number {
return clampMidiPitchBendValue(value);
}
function appendPoint(points: BakedMidiAutomationPoint[], nextPoint: BakedMidiAutomationPoint): void {
const lastPoint = points[points.length - 1];
if (!lastPoint) {
@@ -117,7 +123,7 @@ export function bakeMidiAutomationPointsInWindow(
const normalizedPoints = normalizeMidiAutomationPoints(points);
const anchorPoint = {
beat: windowStartBeat,
value: resolveMidiAutomationValueAtBeat(normalizedPoints, windowStartBeat, options.defaultValue),
value: quantizeBakedValue(resolveMidiAutomationValueAtBeat(normalizedPoints, windowStartBeat, options.defaultValue)),
};
if (windowEndBeat <= windowStartBeat) {
@@ -131,7 +137,7 @@ export function bakeMidiAutomationPointsInWindow(
const firstPoint = normalizedPoints[0];
if (windowStartBeat < firstPoint.beat && firstPoint.beat < windowEndBeat) {
appendPoint(bakedPoints, { beat: firstPoint.beat, value: firstPoint.value });
appendPoint(bakedPoints, { beat: firstPoint.beat, value: quantizeBakedValue(firstPoint.value) });
}
const maxIntervalBeats = getMaxIntervalBeats(options);
@@ -145,6 +151,22 @@ export function bakeMidiAutomationPointsInWindow(
continue;
}
// MIDI pitch bend playback here is event-based, not continuous on its own.
// The last emitted value is held until a later baked event changes it.
//
// That means a flat authored span such as:
// beat 1 -> value 0
// beat 2 -> value 0
// beat 3 -> value 100
// should *not* generate any intermediate events between beats 1 and 2.
// The value from beat 1 is simply held through beat 2, and the bend only
// begins once we emit the first baked point from the changing 2 -> 3 segment.
//
// In practice that first changing baked point may land slightly after beat 2
// depending on the bake interval (for example 10 ms), but it still does not
// cause an earlier gradual bend from beat 1. Skipping flat segments preserves
// the intended "hold, then change" behavior while also avoiding redundant
// scheduled pitch-bend events.
if (startPoint.value === endPoint.value) {
continue;
}
@@ -162,7 +184,7 @@ export function bakeMidiAutomationPointsInWindow(
appendPoint(bakedPoints, {
beat,
value: interpolateBetweenPoints(startPoint, endPoint, beat),
value: quantizeBakedValue(interpolateBetweenPoints(startPoint, endPoint, beat)),
});
}
}