From e4ef0f97cd9a3f26557af3d9406290e207d4efa1 Mon Sep 17 00:00:00 2001 From: Victor Giers Date: Mon, 13 Jul 2026 15:58:10 +0200 Subject: [PATCH] Update Ableton export logic to use comprehensive playback state parameters --- src/lib/exporters/ableton/builders.ts | 10 +-- src/lib/exporters/ableton/playback.ts | 17 ++--- src/lib/transformers/ableton.ts | 5 -- src/routes/home/Export/ExportOptions.tsx | 69 +------------------ .../home/Export/ExportProjectDialog.tsx | 1 - src/routes/home/Export/exportFormSchema.ts | 5 +- src/types/types.ts | 1 - tests/ableton-playback.test.ts | 45 ++++-------- 8 files changed, 28 insertions(+), 125 deletions(-) diff --git a/src/lib/exporters/ableton/builders.ts b/src/lib/exporters/ableton/builders.ts index 6c96c87..48decd4 100644 --- a/src/lib/exporters/ableton/builders.ts +++ b/src/lib/exporters/ableton/builders.ts @@ -389,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 playbackState = getSimplerPlaybackState(koTrack.playMode, koTrack.oneShotOverride); + const playbackState = getSimplerPlaybackState(koTrack.playMode); const samplePart = device.Player.MultiSampleMap.SampleParts.MultiSamplePart; const loopModulators = device.Player.LoopModulators; const simplerFilter = device.Filter.Slot.Value.SimplerFilter; @@ -415,10 +415,10 @@ async function buildSimplerDevice(koTrack: AblTrack, abletonVersion: AbletonVers device.VolumeAndPan.OneShotEnvelope.FadeOutTime.Manual['@Value'] = 0; device.VolumeAndPan.OneShotEnvelope.SustainMode.Manual['@Value'] = 0; device.Globals.PlaybackMode['@Value'] = playbackState.playbackMode; - if (playbackState.monophonic) { - device.Globals.NumVoices['@Value'] = 1; - device.Globals.RetriggerMode['@Value'] = 'false'; - } + device.Globals.NumVoices['@Value'] = playbackState.voices; + device.Globals.RetriggerMode['@Value'] = playbackState.retrigger; + device.Globals.PortamentoMode.Manual['@Value'] = playbackState.portamentoMode; + device.Globals.PortamentoTime.Manual['@Value'] = playbackState.portamentoMode === 2 ? 0.1 : 50; const sampleRef = device.Player.MultiSampleMap.SampleParts.MultiSamplePart.SampleRef; if (abletonVersion === '10') { setLive10SampleReference(sampleRef.FileRef, koTrack.sampleName); diff --git a/src/lib/exporters/ableton/playback.ts b/src/lib/exporters/ableton/playback.ts index a42e7ca..75e07ad 100644 --- a/src/lib/exporters/ableton/playback.ts +++ b/src/lib/exporters/ableton/playback.ts @@ -1,18 +1,11 @@ -import { PadCode } from '../../../types/types'; - export type AbletonPlaybackMode = 'oneshot' | 'key' | 'legato'; -export function hasOneShotOverride(padCode: PadCode, oneShotPads: PadCode[] = []) { - return oneShotPads.includes(padCode); -} - -export function getSimplerPlaybackState( - projectMode: AbletonPlaybackMode, - oneShotOverride: boolean, -) { +export function getSimplerPlaybackState(projectMode: AbletonPlaybackMode) { return { - playbackMode: projectMode === 'oneshot' || oneShotOverride ? 1 : 0, - monophonic: projectMode === 'legato', + playbackMode: 1, + voices: projectMode === 'key' ? 5 : 0, + retrigger: projectMode === 'oneshot', + portamentoMode: projectMode === 'legato' ? 2 : 0, useOneShotEnvelope: projectMode === 'oneshot', }; } diff --git a/src/lib/transformers/ableton.ts b/src/lib/transformers/ableton.ts index 2837989..2b77b4a 100644 --- a/src/lib/transformers/ableton.ts +++ b/src/lib/transformers/ableton.ts @@ -10,7 +10,6 @@ import { ProjectRawData, TimeSignature, } from '../../types/types'; -import { hasOneShotOverride } from '../exporters/ableton/playback'; import { getSampleName } from '../exporters/utils'; import { findPad, findSoundByPad, findSoundIdByPad } from '../utils'; @@ -22,7 +21,6 @@ export type AblData = { export type AblTrack = Omit & { padCode: PadCode; - oneShotOverride: boolean; group: string; sampleName: string; sampleChannels: number; @@ -176,7 +174,6 @@ function abletonTransformer(data: ProjectRawData, exporterParams: ExporterParams ...omit(pad, ['file', 'rawData']), soundId, padCode: pattern.pad, - oneShotOverride: hasOneShotOverride(pattern.pad, exporterParams.abletonOneShotPads), name: sound?.meta?.name || pattern.pad, volume: pad.volume / 200, // converting from 0-200 to 0.0-1.0 sampleName: getSampleName(sound?.meta?.name, soundId), @@ -247,7 +244,6 @@ function abletonTransformer(data: ProjectRawData, exporterParams: ExporterParams ...omit(pad, ['file', 'rawData']), soundId: pad.soundId, padCode, - oneShotOverride: hasOneShotOverride(padCode, exporterParams.abletonOneShotPads), name: sound?.meta?.name || padCode, volume: pad.volume / 200, sampleName: getSampleName(sound?.meta?.name, pad.soundId), @@ -278,7 +274,6 @@ function abletonTransformer(data: ProjectRawData, exporterParams: ExporterParams const drumTrack: AblTrack = { padCode: `${group}0` as PadCode, - oneShotOverride: false, group, sampleName: '', sampleChannels: 0, diff --git a/src/routes/home/Export/ExportOptions.tsx b/src/routes/home/Export/ExportOptions.tsx index 743c041..7272974 100644 --- a/src/routes/home/Export/ExportOptions.tsx +++ b/src/routes/home/Export/ExportOptions.tsx @@ -4,18 +4,14 @@ import { useFormContext, useWatch } from 'react-hook-form'; import { projectIdAtom } from '~/atoms/project'; import CheckboxField from '~/components/form/CheckboxField'; import SelectField from '~/components/form/SelectField'; -import CheckBox from '~/components/ui/CheckBox'; import useProject from '~/hooks/useProject'; -import { getPadDisplayName, hasMultipleNoteVariations } from '~/lib/utils'; -import { PadCode } from '~/types/types'; +import { hasMultipleNoteVariations } from '~/lib/utils'; import { ExportFormValues } from './exportFormSchema'; function ExportOptions({ disabled = false }: { disabled?: boolean }) { - const { control, setValue } = useFormContext(); + const { control } = useFormContext(); const format = useWatch({ control, name: 'format' }); const includeArchivedSamples = useWatch({ control, name: 'includeArchivedSamples' }); - const exportAllPadsWithSamples = useWatch({ control, name: 'exportAllPadsWithSamples' }); - const abletonOneShotPads = useWatch({ control, name: 'abletonOneShotPads' }); const drumRackGroupA = useWatch({ control, name: 'drumRackGroupA' }); const drumRackGroupB = useWatch({ control, name: 'drumRackGroupB' }); const drumRackGroupC = useWatch({ control, name: 'drumRackGroupC' }); @@ -24,48 +20,6 @@ function ExportOptions({ disabled = false }: { disabled?: boolean }) { const projectId = useAtomValue(projectIdAtom); const { data: projectData } = useProject(projectId); - const playbackOverrides = useMemo(() => { - if (!projectData) { - return []; - } - - const usedPads = new Set( - projectData.scenes.flatMap((scene) => scene.patterns.map((pattern) => pattern.pad)), - ); - const result: Array<{ padCode: PadCode; title: string }> = []; - - Object.entries(projectData.pads).forEach(([group, pads]) => { - pads.forEach((pad, index) => { - const padCode = `${group}${index}` as PadCode; - if ( - pad.soundId <= 0 || - pad.playMode === 'oneshot' || - (!exportAllPadsWithSamples && !usedPads.has(padCode)) - ) { - return; - } - - const soundName = projectData.sounds.find((sound) => sound.id === pad.soundId)?.meta.name; - const mode = pad.playMode === 'key' ? 'KEY' : 'LEGATO'; - result.push({ - padCode, - title: `${getPadDisplayName(group, index)} ยท ${soundName || pad.soundId} (EP: ${mode})`, - }); - }); - }); - - return result; - }, [projectData, exportAllPadsWithSamples]); - - const setOneShotOverride = (padCode: PadCode, checked: boolean) => { - const current = abletonOneShotPads || []; - setValue( - 'abletonOneShotPads', - checked ? [...new Set([...current, padCode])] : current.filter((code) => code !== padCode), - { shouldDirty: true }, - ); - }; - const notesVariationWarnings = useMemo(() => { if (format === 'reaper') { return []; @@ -183,25 +137,6 @@ function ExportOptions({ disabled = false }: { disabled?: boolean }) { disabled={disabled || !includeArchivedSamples} helperText="Export all samples and create tracks for all pads with assigned samples, even if they are not used in patterns" /> - {includeArchivedSamples && playbackOverrides.length > 0 && ( -
- Force One-Shot - - Override selected EP KEY or LEGATO pads when Live Classic cuts them too early. - -
- {playbackOverrides.map((item) => ( - setOneShotOverride(item.padCode, checked)} - title={item.title} - disabled={disabled} - /> - ))} -
-
- )} ((value) => typeof value === 'string' && /^[abcd]\d+$/.test(value)), - ), projectName: z.string(), includeArchivedSamples: z.boolean(), exportAllPadsWithSamples: z.boolean(), diff --git a/src/types/types.ts b/src/types/types.ts index 0c42ee6..7f66287 100644 --- a/src/types/types.ts +++ b/src/types/types.ts @@ -217,7 +217,6 @@ export type ExportFormat = { export type ExporterParams = { abletonVersion?: '10' | '11'; - abletonOneShotPads?: PadCode[]; projectName?: string; // custom project name for exported files includeArchivedSamples?: boolean; exportAllPadsWithSamples?: boolean; // export all pads with assigned samples even if not used diff --git a/tests/ableton-playback.test.ts b/tests/ableton-playback.test.ts index babaa83..6277069 100644 --- a/tests/ableton-playback.test.ts +++ b/tests/ableton-playback.test.ts @@ -1,45 +1,30 @@ import assert from 'node:assert/strict'; import test from 'node:test'; -import { getSimplerPlaybackState, hasOneShotOverride } from '../src/lib/exporters/ableton/playback'; -import { PadCode } from '../src/types/types'; +import { getSimplerPlaybackState } from '../src/lib/exporters/ableton/playback'; -const project7Overrides = ['b0', 'c1', 'c8', 'c9', 'd3'] as PadCode[]; - -test('applies the selective Project 7 One-Shot overrides', () => { - for (const padCode of project7Overrides) { - assert.equal(hasOneShotOverride(padCode, project7Overrides), true); - } - - assert.equal(hasOneShotOverride('b3', project7Overrides), false); - assert.equal(hasOneShotOverride('b7', project7Overrides), false); -}); - -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), { +test('exports every EP playback mode as automatic One-Shot Trigger playback', () => { + assert.deepEqual(getSimplerPlaybackState('key'), { playbackMode: 1, - monophonic: false, + voices: 5, + retrigger: false, + portamentoMode: 0, useOneShotEnvelope: false, }); - assert.deepEqual(getSimplerPlaybackState('legato', true), { + assert.deepEqual(getSimplerPlaybackState('legato'), { playbackMode: 1, - monophonic: true, + voices: 0, + retrigger: false, + portamentoMode: 2, useOneShotEnvelope: false, }); }); -test('uses One-Shot envelopes only for native EP ONE pads', () => { - assert.deepEqual(getSimplerPlaybackState('oneshot', false), { +test('preserves native EP ONE envelope translation', () => { + assert.deepEqual(getSimplerPlaybackState('oneshot'), { playbackMode: 1, - monophonic: false, + voices: 0, + retrigger: true, + portamentoMode: 0, useOneShotEnvelope: true, }); - assert.deepEqual(getSimplerPlaybackState('legato', false), { - playbackMode: 0, - monophonic: true, - useOneShotEnvelope: false, - }); });