Enhance Ableton export logic with scaled note velocity and filter fader canonicalization
This commit is contained in:
@@ -52,6 +52,16 @@ export function getFaderValueAt(
|
|||||||
return value;
|
return value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getScaledNoteVelocity(
|
||||||
|
originalVelocity: number,
|
||||||
|
occurrence: AblGroupPatternOccurrence | undefined,
|
||||||
|
position: number,
|
||||||
|
) {
|
||||||
|
const faderValue = getFaderValueAt(occurrence, FaderParam.VEL, position);
|
||||||
|
const velocity = faderValue === undefined ? originalVelocity : originalVelocity * faderValue;
|
||||||
|
return Math.max(1, Math.min(127, Math.round(velocity)));
|
||||||
|
}
|
||||||
|
|
||||||
function getStaticValue(pattern: AblGroupPatternOccurrence, parameter: FaderParam) {
|
function getStaticValue(pattern: AblGroupPatternOccurrence, parameter: FaderParam) {
|
||||||
const value = pattern.faderParams[parameter];
|
const value = pattern.faderParams[parameter];
|
||||||
return value === -1 ? getFaderDefaultValue(parameter) : value;
|
return value === -1 ? getFaderDefaultValue(parameter) : value;
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ import {
|
|||||||
AbletonAutomationTargets,
|
AbletonAutomationTargets,
|
||||||
buildArrangementEnvelopes,
|
buildArrangementEnvelopes,
|
||||||
buildClipEnvelopes,
|
buildClipEnvelopes,
|
||||||
getFaderValueAt,
|
getScaledNoteVelocity,
|
||||||
} from './automation';
|
} from './automation';
|
||||||
import {
|
import {
|
||||||
chorusEffectValues,
|
chorusEffectValues,
|
||||||
@@ -30,7 +30,11 @@ import {
|
|||||||
filterEffectValues,
|
filterEffectValues,
|
||||||
reverbEffectValues,
|
reverbEffectValues,
|
||||||
} from './effects';
|
} from './effects';
|
||||||
import { convertGroupFaderValue, getFaderDefaultValue } from './faderPolicies';
|
import {
|
||||||
|
canonicalizeFilterFaderValue,
|
||||||
|
convertGroupFaderValue,
|
||||||
|
getFaderDefaultValue,
|
||||||
|
} from './faderPolicies';
|
||||||
import { AbletonMidiControllerTargets, buildMidiClipEnvelopes } from './midi';
|
import { AbletonMidiControllerTargets, buildMidiClipEnvelopes } from './midi';
|
||||||
import { getPadEnvelopeState, getPadSoundState } from './padSound';
|
import { getPadEnvelopeState, getPadSoundState } from './padSound';
|
||||||
import { getSimplerPlaybackState } from './playback';
|
import { getSimplerPlaybackState } from './playback';
|
||||||
@@ -295,15 +299,10 @@ async function buildMidiClip(
|
|||||||
dur = nextNote.position / 96 - noteData.position / 96;
|
dur = nextNote.position / 96 - noteData.position / 96;
|
||||||
}
|
}
|
||||||
|
|
||||||
const automatedVelocity = koClip.groupPattern
|
const velocity = getScaledNoteVelocity(
|
||||||
? getFaderValueAt(koClip.groupPattern, FaderParam.VEL, noteData.position)
|
noteData.velocity,
|
||||||
: undefined;
|
koClip.groupPattern,
|
||||||
const velocity = Math.max(
|
noteData.position,
|
||||||
1,
|
|
||||||
Math.min(
|
|
||||||
127,
|
|
||||||
automatedVelocity === undefined ? noteData.velocity : automatedVelocity * 127,
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -644,7 +643,10 @@ async function buildTrack(
|
|||||||
|
|
||||||
if (exporterParams.includeArchivedSamples) {
|
if (exporterParams.includeArchivedSamples) {
|
||||||
for (const parameter of [FaderParam.LPF, FaderParam.HPF] as const) {
|
for (const parameter of [FaderParam.LPF, FaderParam.HPF] as const) {
|
||||||
const staticValue = koTrack.faderParams[parameter];
|
const staticValue = canonicalizeFilterFaderValue(
|
||||||
|
parameter,
|
||||||
|
koTrack.faderParams[parameter],
|
||||||
|
);
|
||||||
const defaultValue = getFaderDefaultValue(parameter);
|
const defaultValue = getFaderDefaultValue(parameter);
|
||||||
const hasAutomation = automatedParameters.has(parameter);
|
const hasAutomation = automatedParameters.has(parameter);
|
||||||
if (hasAutomation || (staticValue !== -1 && staticValue !== defaultValue)) {
|
if (hasAutomation || (staticValue !== -1 && staticValue !== defaultValue)) {
|
||||||
|
|||||||
@@ -93,7 +93,7 @@ export const FADER_POLICIES = {
|
|||||||
toLive: (value) => Math.max(-0.5, Math.min(0.5, value - 0.5)) * 100,
|
toLive: (value) => Math.max(-0.5, Math.min(0.5, value - 0.5)) * 100,
|
||||||
},
|
},
|
||||||
[FaderParam.VEL]: {
|
[FaderParam.VEL]: {
|
||||||
defaultValue: 0,
|
defaultValue: 1,
|
||||||
renderStrategy: 'note',
|
renderStrategy: 'note',
|
||||||
targetScope: 'track',
|
targetScope: 'track',
|
||||||
status: 'research',
|
status: 'research',
|
||||||
@@ -116,6 +116,14 @@ export function getFaderDefaultValue(parameter: FaderParam) {
|
|||||||
return getFaderPolicy(parameter).defaultValue;
|
return getFaderPolicy(parameter).defaultValue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function canonicalizeFilterFaderValue(
|
||||||
|
parameter: FaderParam.LPF | FaderParam.HPF,
|
||||||
|
value: number,
|
||||||
|
) {
|
||||||
|
const defaultValue = getFaderDefaultValue(parameter);
|
||||||
|
return Math.abs(value - defaultValue) <= 0.001 ? defaultValue : value;
|
||||||
|
}
|
||||||
|
|
||||||
export function convertGroupFaderValue(
|
export function convertGroupFaderValue(
|
||||||
parameter: FaderParam,
|
parameter: FaderParam,
|
||||||
value: number,
|
value: number,
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import {
|
|||||||
buildClipEnvelopes,
|
buildClipEnvelopes,
|
||||||
convertFaderValue,
|
convertFaderValue,
|
||||||
getFaderValueAt,
|
getFaderValueAt,
|
||||||
|
getScaledNoteVelocity,
|
||||||
} from '../src/lib/exporters/ableton/automation';
|
} from '../src/lib/exporters/ableton/automation';
|
||||||
import { AblGroupPatternOccurrence, AblTrack } from '../src/lib/transformers/ableton';
|
import { AblGroupPatternOccurrence, AblTrack } from '../src/lib/transformers/ableton';
|
||||||
import { FaderParam } from '../src/types/types';
|
import { FaderParam } from '../src/types/types';
|
||||||
@@ -49,6 +50,28 @@ const track = {
|
|||||||
test('uses original note velocity until a VEL point is reached', () => {
|
test('uses original note velocity until a VEL point is reached', () => {
|
||||||
assert.equal(getFaderValueAt(occurrence, FaderParam.VEL, 96), undefined);
|
assert.equal(getFaderValueAt(occurrence, FaderParam.VEL, 96), undefined);
|
||||||
assert.equal(getFaderValueAt(occurrence, FaderParam.VEL, 192), 0.5);
|
assert.equal(getFaderValueAt(occurrence, FaderParam.VEL, 192), 0.5);
|
||||||
|
assert.equal(getScaledNoteVelocity(101, occurrence, 96), 101);
|
||||||
|
assert.equal(getScaledNoteVelocity(101, occurrence, 192), 51);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('scales P07-like note velocities with static and automated VEL values', () => {
|
||||||
|
const staticVelocity = {
|
||||||
|
...occurrence,
|
||||||
|
faderParams: {
|
||||||
|
...occurrence.faderParams,
|
||||||
|
[FaderParam.VEL]: 0.5325199961662292,
|
||||||
|
},
|
||||||
|
faderAutomation: [
|
||||||
|
{ parameter: FaderParam.VEL, position: 192, value: 0.25, flags: 0 },
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
assert.deepEqual(
|
||||||
|
[127, 127, 48].map((velocity) => getScaledNoteVelocity(velocity, staticVelocity, 96)),
|
||||||
|
[68, 68, 26],
|
||||||
|
);
|
||||||
|
assert.equal(getScaledNoteVelocity(127, staticVelocity, 192), 32);
|
||||||
|
assert.equal(getScaledNoteVelocity(0, staticVelocity, 192), 1);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('preserves each pad pitch offset while applying group PTC automation', () => {
|
test('preserves each pad pitch offset while applying group PTC automation', () => {
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import assert from 'node:assert/strict';
|
import assert from 'node:assert/strict';
|
||||||
import test from 'node:test';
|
import test from 'node:test';
|
||||||
import {
|
import {
|
||||||
|
canonicalizeFilterFaderValue,
|
||||||
convertGroupFaderValue,
|
convertGroupFaderValue,
|
||||||
FADER_POLICIES,
|
FADER_POLICIES,
|
||||||
getFaderPolicy,
|
getFaderPolicy,
|
||||||
@@ -30,3 +31,10 @@ test('uses one group-fader conversion path for mixer and pad targets', () => {
|
|||||||
assert.equal(convertGroupFaderValue(FaderParam.PAN, 0.75, { track }), 0.5);
|
assert.equal(convertGroupFaderValue(FaderParam.PAN, 0.75, { track }), 0.5);
|
||||||
assert.equal(convertGroupFaderValue(FaderParam.PTC, 0.75, { track, valueOffset: 7 }), 9.5);
|
assert.equal(convertGroupFaderValue(FaderParam.PTC, 0.75, { track, valueOffset: 7 }), 9.5);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('canonicalizes only effectively-open filter fader values', () => {
|
||||||
|
assert.equal(canonicalizeFilterFaderValue(FaderParam.LPF, 0.9996299743652344), 1);
|
||||||
|
assert.equal(canonicalizeFilterFaderValue(FaderParam.HPF, 0.0003700256347656), 0);
|
||||||
|
assert.equal(canonicalizeFilterFaderValue(FaderParam.LPF, 0.998), 0.998);
|
||||||
|
assert.equal(canonicalizeFilterFaderValue(FaderParam.HPF, 0.002), 0.002);
|
||||||
|
});
|
||||||
|
|||||||
@@ -213,7 +213,10 @@ test('serializes nested Live 10 Simplers without artificial empty Session clips'
|
|||||||
});
|
});
|
||||||
data.settings.groupFaderParams.a[FaderParam.ATK] = 0.66;
|
data.settings.groupFaderParams.a[FaderParam.ATK] = 0.66;
|
||||||
data.settings.groupFaderParams.a[FaderParam.REL] = 0.87;
|
data.settings.groupFaderParams.a[FaderParam.REL] = 0.87;
|
||||||
|
data.settings.groupFaderParams.a[FaderParam.LPF] = 0.9996299743652344;
|
||||||
|
data.settings.groupFaderParams.a[FaderParam.VEL] = 0.5325199961662292;
|
||||||
delete data.groupPatterns[0].notes[1];
|
delete data.groupPatterns[0].notes[1];
|
||||||
|
data.groupPatterns[0].notes[0] = [note(72, 0, 127), note(72, 96, 127), note(72, 192, 48)];
|
||||||
delete data.groupPatterns[1].notes[0];
|
delete data.groupPatterns[1].notes[0];
|
||||||
data.groupPatterns[1].notes[1] = [note(67, 48), note(68, 96)];
|
data.groupPatterns[1].notes[1] = [note(67, 48), note(68, 96)];
|
||||||
data.songPositions = [];
|
data.songPositions = [];
|
||||||
@@ -272,7 +275,10 @@ test('serializes nested Live 10 Simplers without artificial empty Session clips'
|
|||||||
assert.doesNotMatch(xml, /<ReturnBranch\s/);
|
assert.doesNotMatch(xml, /<ReturnBranch\s/);
|
||||||
assert.doesNotMatch(xml, /<AudioBranchSendInfo\s/);
|
assert.doesNotMatch(xml, /<AudioBranchSendInfo\s/);
|
||||||
assert.doesNotMatch(xml, /Big Break Kit/);
|
assert.doesNotMatch(xml, /Big Break Kit/);
|
||||||
|
assert.doesNotMatch(xml, /<AutoFilter\b/);
|
||||||
assert.doesNotMatch(xml, /<ClipEnvelope\b/);
|
assert.doesNotMatch(xml, /<ClipEnvelope\b/);
|
||||||
|
assert.equal([...xml.matchAll(/<MidiNoteEvent\b[^>]*\sVelocity="68"/g)].length, 2);
|
||||||
|
assert.equal([...xml.matchAll(/<MidiNoteEvent\b[^>]*\sVelocity="26"/g)].length, 1);
|
||||||
} finally {
|
} finally {
|
||||||
await server.close();
|
await server.close();
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user