Files
KGStudio/src/util/audioTempoDetectionActions.ts
T
2026-05-30 23:06:17 -07:00

116 lines
3.8 KiB
TypeScript

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();
}