feat: added MIDI CC recording and list event table operation support.
This commit is contained in:
@@ -4,6 +4,7 @@ import {
|
||||
collectRegionMidiAutomationPoints,
|
||||
normalizeMidiAutomationPoints,
|
||||
resolveMidiAutomationValueAtBeat,
|
||||
resolveSustainExtendedEndBeat,
|
||||
type MidiAutomationPoint,
|
||||
} from './midiAutomationUtil';
|
||||
|
||||
@@ -159,4 +160,33 @@ describe('midiAutomationUtil', () => {
|
||||
expect(baked.some(point => point.beat === 5 && point.value === 6144)).toBe(true);
|
||||
expect(baked.some(point => point.beat === 6 && point.value === 8192)).toBe(true);
|
||||
});
|
||||
|
||||
it('uses step interpolation for switch-style automation', () => {
|
||||
const points = [
|
||||
{ beat: 1, value: 127 },
|
||||
{ beat: 3, value: 0 },
|
||||
];
|
||||
|
||||
expect(resolveMidiAutomationValueAtBeat(points, 2, 0, 'step')).toBe(127);
|
||||
expect(bakeMidiAutomationPointsInWindow(points, 0, 4, {
|
||||
...defaultOptions,
|
||||
defaultValue: 0,
|
||||
interpolationMode: 'step',
|
||||
quantizeValue: (value) => value,
|
||||
})).toEqual([
|
||||
{ beat: 0, value: 0 },
|
||||
{ beat: 1, value: 127 },
|
||||
{ beat: 3, value: 0 },
|
||||
]);
|
||||
});
|
||||
|
||||
it('extends note ends until the next sustain release', () => {
|
||||
const points = [
|
||||
{ beat: 1, value: 127 },
|
||||
{ beat: 4, value: 0 },
|
||||
];
|
||||
|
||||
expect(resolveSustainExtendedEndBeat(points, 2, 0)).toBe(4);
|
||||
expect(resolveSustainExtendedEndBeat(points, 5, 0)).toBe(5);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -14,6 +14,8 @@ export interface MidiAutomationBakeOptions {
|
||||
maxIntervalMs: number;
|
||||
bpm: number;
|
||||
defaultValue: number;
|
||||
interpolationMode?: 'linear' | 'step';
|
||||
quantizeValue?: (value: number) => number;
|
||||
}
|
||||
|
||||
const BEAT_EPSILON = 1e-9;
|
||||
@@ -22,6 +24,10 @@ function quantizeBakedValue(value: number): number {
|
||||
return clampMidiPitchBendValue(value);
|
||||
}
|
||||
|
||||
function quantizeValue(value: number, options: MidiAutomationBakeOptions): number {
|
||||
return options.quantizeValue ? options.quantizeValue(value) : quantizeBakedValue(value);
|
||||
}
|
||||
|
||||
function appendPoint(points: BakedMidiAutomationPoint[], nextPoint: BakedMidiAutomationPoint): void {
|
||||
const lastPoint = points[points.length - 1];
|
||||
if (!lastPoint) {
|
||||
@@ -91,7 +97,8 @@ export function collectRegionMidiAutomationPoints(
|
||||
export function resolveMidiAutomationValueAtBeat(
|
||||
points: MidiAutomationPoint[],
|
||||
beat: number,
|
||||
defaultValue: number
|
||||
defaultValue: number,
|
||||
interpolationMode: 'linear' | 'step' = 'linear'
|
||||
): number {
|
||||
const normalizedPoints = normalizeMidiAutomationPoints(points);
|
||||
if (normalizedPoints.length === 0) {
|
||||
@@ -105,6 +112,10 @@ export function resolveMidiAutomationValueAtBeat(
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
if (interpolationMode === 'step') {
|
||||
return previousPoint.value;
|
||||
}
|
||||
|
||||
return interpolateBetweenPoints(previousPoint, point, beat);
|
||||
}
|
||||
|
||||
@@ -121,9 +132,13 @@ export function bakeMidiAutomationPointsInWindow(
|
||||
options: MidiAutomationBakeOptions
|
||||
): BakedMidiAutomationPoint[] {
|
||||
const normalizedPoints = normalizeMidiAutomationPoints(points);
|
||||
const interpolationMode = options.interpolationMode ?? 'linear';
|
||||
const anchorPoint = {
|
||||
beat: windowStartBeat,
|
||||
value: quantizeBakedValue(resolveMidiAutomationValueAtBeat(normalizedPoints, windowStartBeat, options.defaultValue)),
|
||||
value: quantizeValue(
|
||||
resolveMidiAutomationValueAtBeat(normalizedPoints, windowStartBeat, options.defaultValue, interpolationMode),
|
||||
options
|
||||
),
|
||||
};
|
||||
|
||||
if (windowEndBeat <= windowStartBeat) {
|
||||
@@ -137,7 +152,22 @@ export function bakeMidiAutomationPointsInWindow(
|
||||
|
||||
const firstPoint = normalizedPoints[0];
|
||||
if (windowStartBeat < firstPoint.beat && firstPoint.beat < windowEndBeat) {
|
||||
appendPoint(bakedPoints, { beat: firstPoint.beat, value: quantizeBakedValue(firstPoint.value) });
|
||||
appendPoint(bakedPoints, { beat: firstPoint.beat, value: quantizeValue(firstPoint.value, options) });
|
||||
}
|
||||
|
||||
if (interpolationMode === 'step') {
|
||||
normalizedPoints.forEach(point => {
|
||||
if (point.beat <= windowStartBeat + BEAT_EPSILON || point.beat >= windowEndBeat - BEAT_EPSILON) {
|
||||
return;
|
||||
}
|
||||
|
||||
appendPoint(bakedPoints, {
|
||||
beat: point.beat,
|
||||
value: quantizeValue(point.value, options),
|
||||
});
|
||||
});
|
||||
|
||||
return bakedPoints;
|
||||
}
|
||||
|
||||
const maxIntervalBeats = getMaxIntervalBeats(options);
|
||||
@@ -184,10 +214,29 @@ export function bakeMidiAutomationPointsInWindow(
|
||||
|
||||
appendPoint(bakedPoints, {
|
||||
beat,
|
||||
value: quantizeBakedValue(interpolateBetweenPoints(startPoint, endPoint, beat)),
|
||||
value: quantizeValue(interpolateBetweenPoints(startPoint, endPoint, beat), options),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return bakedPoints;
|
||||
}
|
||||
|
||||
export function resolveSustainExtendedEndBeat(
|
||||
points: MidiAutomationPoint[],
|
||||
noteEndBeat: number,
|
||||
defaultValue: number
|
||||
): number {
|
||||
const normalizedPoints = normalizeMidiAutomationPoints(points);
|
||||
if (normalizedPoints.length === 0) {
|
||||
return noteEndBeat;
|
||||
}
|
||||
|
||||
const sustainValueAtEnd = resolveMidiAutomationValueAtBeat(normalizedPoints, noteEndBeat, defaultValue, 'step');
|
||||
if (sustainValueAtEnd < 64) {
|
||||
return noteEndBeat;
|
||||
}
|
||||
|
||||
const releasePoint = normalizedPoints.find(point => point.beat > noteEndBeat && point.value < 64);
|
||||
return releasePoint?.beat ?? noteEndBeat;
|
||||
}
|
||||
|
||||
@@ -27,6 +27,8 @@ export const MIDI_PITCH_BEND_CENTER = 8192;
|
||||
export const MIDI_PITCH_BEND_MAX = 16383;
|
||||
export const MIDI_PITCH_BEND_MIN_SIGNED = -8192;
|
||||
export const MIDI_PITCH_BEND_MAX_SIGNED = 8191;
|
||||
export const MIDI_CONTROLLER_MIN = 0;
|
||||
export const MIDI_CONTROLLER_MAX = 127;
|
||||
|
||||
export const pitchToNoteName = (pitch: number) => {
|
||||
const noteNames = ['C', 'C#', 'D', 'D#', 'E', 'F', 'F#', 'G', 'G#', 'A', 'A#', 'B'];
|
||||
@@ -45,6 +47,10 @@ export const clampMidiPitchBendValue = (value: number): number => (
|
||||
Math.max(MIDI_PITCH_BEND_MIN, Math.min(MIDI_PITCH_BEND_MAX, Math.round(value)))
|
||||
);
|
||||
|
||||
export const clampMidiControllerValue = (value: number): number => (
|
||||
Math.max(MIDI_CONTROLLER_MIN, Math.min(MIDI_CONTROLLER_MAX, Math.round(value)))
|
||||
);
|
||||
|
||||
export const midiPitchBendToSignedValue = (value: number): number => (
|
||||
clampMidiPitchBendValue(value) - MIDI_PITCH_BEND_CENTER
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user