Feat: Add one-shot pad override controls for export

This commit is contained in:
2026-07-13 10:17:05 +02:00
parent 89df0e4641
commit e2eecb5b75
3 changed files with 74 additions and 3 deletions

View File

@@ -5,13 +5,17 @@ import { projectIdAtom } from '~/atoms/project';
import CheckboxField from '~/components/form/CheckboxField';
import SelectField from '~/components/form/SelectField';
import useProject from '~/hooks/useProject';
import { hasMultipleNoteVariations } from '~/lib/utils';
import { getPadDisplayName, hasMultipleNoteVariations } from '~/lib/utils';
import { PadCode } from '~/types/types';
import CheckBox from '~/components/ui/CheckBox';
import { ExportFormValues } from './exportFormSchema';
function ExportOptions({ disabled = false }: { disabled?: boolean }) {
const { control } = useFormContext<ExportFormValues>();
const { control, setValue } = 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' });
@@ -20,6 +24,48 @@ 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 [];
@@ -137,6 +183,25 @@ 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"

View File

@@ -43,6 +43,7 @@ function ExportProjectDialog({
defaultValues: {
format: EXPORT_FORMATS[0].value,
abletonVersion: '10',
abletonOneShotPads: [],
projectName: `Project${projectId}`,
includeArchivedSamples: true,
exportAllPadsWithSamples: false,

View File

@@ -1,5 +1,5 @@
import { z } from 'zod';
import { ExportFormatId } from '../../../types/types';
import { ExportFormatId, PadCode } from '../../../types/types';
const FORMAT_IDS: [ExportFormatId, ...ExportFormatId[]] = [
'ableton',
@@ -11,6 +11,11 @@ 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(),