Update Ableton export logic to use comprehensive playback state parameters
This commit is contained in:
@@ -389,7 +389,7 @@ async function buildMidiClip(
|
||||
async function buildSimplerDevice(koTrack: AblTrack, abletonVersion: AbletonVersion) {
|
||||
const simplerTemplate = await loadTemplate<ALSSimpler>('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);
|
||||
|
||||
@@ -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',
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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<Pad, 'file' | 'rawData' | 'midiChannel'> & {
|
||||
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,
|
||||
|
||||
@@ -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<ExportFormValues>();
|
||||
const { control } = useFormContext<ExportFormValues>();
|
||||
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 && (
|
||||
<div className="flex flex-col gap-1 mt-2">
|
||||
<span className="text-sm font-semibold">Force One-Shot</span>
|
||||
<span className="text-xs opacity-80">
|
||||
Override selected EP KEY or LEGATO pads when Live Classic cuts them too early.
|
||||
</span>
|
||||
<div className="flex flex-col gap-1 max-h-44 overflow-y-auto border border-black p-2">
|
||||
{playbackOverrides.map((item) => (
|
||||
<CheckBox
|
||||
key={item.padCode}
|
||||
checked={(abletonOneShotPads || []).includes(item.padCode)}
|
||||
onChange={(checked) => setOneShotOverride(item.padCode, checked)}
|
||||
title={item.title}
|
||||
disabled={disabled}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<CheckboxField
|
||||
name="clips"
|
||||
title="Session clips instead of arrangements"
|
||||
|
||||
@@ -43,7 +43,6 @@ function ExportProjectDialog({
|
||||
defaultValues: {
|
||||
format: EXPORT_FORMATS[0].value,
|
||||
abletonVersion: '10',
|
||||
abletonOneShotPads: [],
|
||||
projectName: `Project${projectId}`,
|
||||
includeArchivedSamples: true,
|
||||
exportAllPadsWithSamples: false,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { z } from 'zod';
|
||||
import { ExportFormatId, PadCode } from '../../../types/types';
|
||||
import { ExportFormatId } from '../../../types/types';
|
||||
|
||||
const FORMAT_IDS: [ExportFormatId, ...ExportFormatId[]] = [
|
||||
'ableton',
|
||||
@@ -11,9 +11,6 @@ const FORMAT_IDS: [ExportFormatId, ...ExportFormatId[]] = [
|
||||
export const exportFormSchema = z.object({
|
||||
format: z.enum(FORMAT_IDS),
|
||||
abletonVersion: z.enum(['10', '11']),
|
||||
abletonOneShotPads: z.array(
|
||||
z.custom<PadCode>((value) => typeof value === 'string' && /^[abcd]\d+$/.test(value)),
|
||||
),
|
||||
projectName: z.string(),
|
||||
includeArchivedSamples: z.boolean(),
|
||||
exportAllPadsWithSamples: z.boolean(),
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user