diff --git a/src/lib/exporters/ableton/builders.ts b/src/lib/exporters/ableton/builders.ts index 1429205..6c96c87 100644 --- a/src/lib/exporters/ableton/builders.ts +++ b/src/lib/exporters/ableton/builders.ts @@ -29,6 +29,7 @@ import { filterEffectValues, reverbEffectValues, } from './effects'; +import { getSimplerPlaybackState } from './playback'; import { ALSDrumBranch } from './types/drumBranch'; import { ALSDrumRack } from './types/drumRack'; import { ALSChorus } from './types/effectChorus'; @@ -388,7 +389,7 @@ async function buildMidiClip( async function buildSimplerDevice(koTrack: AblTrack, abletonVersion: AbletonVersion) { const simplerTemplate = await loadTemplate('simpler', abletonVersion); const device = structuredClone(simplerTemplate.OriginalSimpler); - const projectOneShot = koTrack.playMode === 'oneshot'; + const playbackState = getSimplerPlaybackState(koTrack.playMode, koTrack.oneShotOverride); const samplePart = device.Player.MultiSampleMap.SampleParts.MultiSamplePart; const loopModulators = device.Player.LoopModulators; const simplerFilter = device.Filter.Slot.Value.SimplerFilter; @@ -413,21 +414,10 @@ async function buildSimplerDevice(koTrack: AblTrack, abletonVersion: AbletonVers device.VolumeAndPan.OneShotEnvelope.FadeInTime.Manual['@Value'] = 0; device.VolumeAndPan.OneShotEnvelope.FadeOutTime.Manual['@Value'] = 0; device.VolumeAndPan.OneShotEnvelope.SustainMode.Manual['@Value'] = 0; - switch (koTrack.playMode) { - case 'legato': - device.Globals.PlaybackMode['@Value'] = 0; - device.Globals.NumVoices['@Value'] = 1; - device.Globals.RetriggerMode['@Value'] = 'false'; - break; - case 'oneshot': - device.Globals.PlaybackMode['@Value'] = 1; - break; - case 'key': - device.Globals.PlaybackMode['@Value'] = 0; - break; - } - if (koTrack.oneShotOverride) { - device.Globals.PlaybackMode['@Value'] = 1; + device.Globals.PlaybackMode['@Value'] = playbackState.playbackMode; + if (playbackState.monophonic) { + device.Globals.NumVoices['@Value'] = 1; + device.Globals.RetriggerMode['@Value'] = 'false'; } const sampleRef = device.Player.MultiSampleMap.SampleParts.MultiSamplePart.SampleRef; if (abletonVersion === '10') { @@ -466,22 +456,22 @@ async function buildSimplerDevice(koTrack: AblTrack, abletonVersion: AbletonVers // setting velocity scaling to 20% to match KO behavior device.VolumeAndPan.VolumeVelScale.Manual['@Value'] = 0.2; - const attackTarget = projectOneShot + const attackTarget = playbackState.useOneShotEnvelope ? device.VolumeAndPan.OneShotEnvelope.FadeInTime : device.VolumeAndPan.Envelope.AttackTime; - const releaseTarget = projectOneShot + const releaseTarget = playbackState.useOneShotEnvelope ? device.VolumeAndPan.OneShotEnvelope.FadeOutTime : device.VolumeAndPan.Envelope.ReleaseTime; attackTarget.Manual['@Value'] = simplerEnvelopeMilliseconds( koTrack.attack, playbackDuration, - projectOneShot, + playbackState.useOneShotEnvelope, ); releaseTarget.Manual['@Value'] = simplerEnvelopeMilliseconds( koTrack.release, playbackDuration, - projectOneShot, - projectOneShot, + playbackState.useOneShotEnvelope, + playbackState.useOneShotEnvelope, ); device.VolumeAndPan.Panorama.Manual['@Value'] = koTrack.pan; @@ -490,7 +480,7 @@ async function buildSimplerDevice(koTrack: AblTrack, abletonVersion: AbletonVers attackTarget.Manual['@Value'] = simplerEnvelopeMilliseconds( koTrack.faderParams[FaderParam.ATK] * 255, playbackDuration, - projectOneShot, + playbackState.useOneShotEnvelope, ); } @@ -499,8 +489,8 @@ async function buildSimplerDevice(koTrack: AblTrack, abletonVersion: AbletonVers releaseTarget.Manual['@Value'] = simplerEnvelopeMilliseconds( koTrack.faderParams[FaderParam.REL] * 255, playbackDuration, - projectOneShot, - projectOneShot, + playbackState.useOneShotEnvelope, + playbackState.useOneShotEnvelope, ); } diff --git a/src/lib/exporters/ableton/playback.ts b/src/lib/exporters/ableton/playback.ts index 500eeea..a42e7ca 100644 --- a/src/lib/exporters/ableton/playback.ts +++ b/src/lib/exporters/ableton/playback.ts @@ -5,3 +5,14 @@ export type AbletonPlaybackMode = 'oneshot' | 'key' | 'legato'; export function hasOneShotOverride(padCode: PadCode, oneShotPads: PadCode[] = []) { return oneShotPads.includes(padCode); } + +export function getSimplerPlaybackState( + projectMode: AbletonPlaybackMode, + oneShotOverride: boolean, +) { + return { + playbackMode: projectMode === 'oneshot' || oneShotOverride ? 1 : 0, + monophonic: projectMode === 'legato', + useOneShotEnvelope: projectMode === 'oneshot', + }; +} diff --git a/tests/ableton-playback.test.ts b/tests/ableton-playback.test.ts index b17bfd8..4ac9d06 100644 --- a/tests/ableton-playback.test.ts +++ b/tests/ableton-playback.test.ts @@ -1,6 +1,9 @@ import assert from 'node:assert/strict'; import test from 'node:test'; -import { hasOneShotOverride } from '../src/lib/exporters/ableton/playback'; +import { + getSimplerPlaybackState, + hasOneShotOverride, +} from '../src/lib/exporters/ableton/playback'; import { PadCode } from '../src/types/types'; const project7Overrides = ['b0', 'c1', 'c8', 'c9', 'd3'] as PadCode[]; @@ -17,3 +20,29 @@ test('applies the selective Project 7 One-Shot overrides', () => { test('preserves project playback modes when no override is selected', () => { assert.equal(hasOneShotOverride('b0'), false); }); + +test('changes only the active playback mode for forced KEY and LEGATO pads', () => { + assert.deepEqual(getSimplerPlaybackState('key', true), { + playbackMode: 1, + monophonic: false, + useOneShotEnvelope: false, + }); + assert.deepEqual(getSimplerPlaybackState('legato', true), { + playbackMode: 1, + monophonic: true, + useOneShotEnvelope: false, + }); +}); + +test('uses One-Shot envelopes only for native EP ONE pads', () => { + assert.deepEqual(getSimplerPlaybackState('oneshot', false), { + playbackMode: 1, + monophonic: false, + useOneShotEnvelope: true, + }); + assert.deepEqual(getSimplerPlaybackState('legato', false), { + playbackMode: 0, + monophonic: true, + useOneShotEnvelope: false, + }); +});