import { KGCore } from '../core/KGCore'; import { CreateTempoRegionCommand } from '../core/commands/global-region/CreateTempoRegionCommand'; import { UpdateTempoRegionCommand } from '../core/commands/global-region/UpdateTempoRegionCommand'; import { MoveRegionCommand } from '../core/commands/region/MoveRegionCommand'; import type { KGProject } from '../core/KGProject'; import { findTempoRegionAtBar, findTempoRegionAtBeat, } from './globalTrackUtil'; import { translate } from '../i18n/translate'; export const DETECTED_TEMPO_ACTION_UPDATE_CURRENT = 'update-current-tempo'; export const DETECTED_TEMPO_ACTION_INSERT_REGION = 'insert-tempo-change'; export type DetectedTempoAction = | typeof DETECTED_TEMPO_ACTION_UPDATE_CURRENT | typeof DETECTED_TEMPO_ACTION_INSERT_REGION; export function buildDetectedTempoChoiceMessage(bpm: number): string { return translate('dialog.message.tempoApply', { bpm }); } const ALIGNMENT_EPSILON = 1e-6; interface ApplyDetectedTempoActionParams { action: DetectedTempoAction; detectedBpm: number; detectedTempo: number; detectedOffsetSeconds: number; autoAlignRegionToBeat: boolean; project: KGProject; regionId: string; regionStartBeat: number; regionTrackId: string; regionTrackIndex: number; refreshProjectState: () => void; setBpm: (bpm: number) => void; } export function getRightwardBeatAlignmentShiftBeats( detectedTempo: number, offsetSeconds: number, ): number { if (!Number.isFinite(detectedTempo) || detectedTempo <= 0 || !Number.isFinite(offsetSeconds) || offsetSeconds <= 0) { return 0; } const secondsPerBeat = 60 / detectedTempo; const offsetWithinBeat = offsetSeconds % secondsPerBeat; if (offsetWithinBeat <= ALIGNMENT_EPSILON || secondsPerBeat - offsetWithinBeat <= ALIGNMENT_EPSILON) { return 0; } return (secondsPerBeat - offsetWithinBeat) / secondsPerBeat; } export function applyDetectedTempoAction({ action, detectedBpm, detectedTempo, detectedOffsetSeconds, autoAlignRegionToBeat, project, regionId, regionStartBeat, regionTrackId, regionTrackIndex, refreshProjectState, setBpm, }: ApplyDetectedTempoActionParams): void { const core = KGCore.instance(); if (action === DETECTED_TEMPO_ACTION_UPDATE_CURRENT) { const targetRegion = findTempoRegionAtBeat(project, regionStartBeat); if (targetRegion) { core.executeCommand(new UpdateTempoRegionCommand(targetRegion.getId(), detectedBpm), { rethrow: true }); } else { setBpm(detectedBpm); } } else { const beatsPerBar = project.getTimeSignature().numerator; const targetBar = Math.max(0, Math.floor(regionStartBeat / beatsPerBar)); const existingRegionAtBar = findTempoRegionAtBar(project, targetBar); if (existingRegionAtBar && existingRegionAtBar.getStartBar() === targetBar) { core.executeCommand(new UpdateTempoRegionCommand(existingRegionAtBar.getId(), detectedBpm), { rethrow: true }); } else { const createTempoRegionCommand = new CreateTempoRegionCommand(targetBar); core.executeCommand(createTempoRegionCommand, { rethrow: true }); const createdRegion = createTempoRegionCommand.getCreatedRegion(); if (!createdRegion) { throw new Error(`Failed to create tempo region at bar ${targetBar + 1}`); } core.executeCommand(new UpdateTempoRegionCommand(createdRegion.getId(), detectedBpm), { rethrow: true }); } } if (autoAlignRegionToBeat) { const shiftBeats = getRightwardBeatAlignmentShiftBeats(detectedTempo, detectedOffsetSeconds); if (shiftBeats > 0) { core.executeCommand( MoveRegionCommand.createPositionOnlyMove( regionId, regionStartBeat + shiftBeats, regionTrackId, regionTrackIndex, ), { rethrow: true }, ); } } refreshProjectState(); }