feat: add audio region trimming with non-destructive clip offset

This commit is contained in:
Xiaohan-Tian
2026-04-10 20:17:31 -07:00
parent 39401b39c6
commit 1670679c61
8 changed files with 192 additions and 60 deletions
+26 -15
View File
@@ -54,7 +54,7 @@ const RegionItem: React.FC<RegionItemProps> = ({
audioBuffer audioBuffer
}) => { }) => {
// Get selection state and time signature from store // Get selection state and time signature from store
const { selectedRegionIds, timeSignature } = useProjectStore(); const { selectedRegionIds, timeSignature, bpm } = useProjectStore();
const isSelected = selectedRegionIds.includes(id); const isSelected = selectedRegionIds.includes(id);
const [cursor, setCursor] = useState<string>('pointer'); const [cursor, setCursor] = useState<string>('pointer');
const [resizeEdge, setResizeEdge] = useState<ResizeAction>('none'); const [resizeEdge, setResizeEdge] = useState<ResizeAction>('none');
@@ -229,10 +229,28 @@ const RegionItem: React.FC<RegionItemProps> = ({
// Get channel data (use first channel) // Get channel data (use first channel)
const channelData = audioBuffer.getChannelData(0); const channelData = audioBuffer.getChannelData(0);
const samples = channelData.length; const totalSamples = channelData.length;
const sampleRate = audioBuffer.sampleRate;
// Downsample to canvas width // Calculate visible portion based on clip offset
const samplesPerPixel = Math.max(1, Math.floor(samples / width)); const clipStartOffsetSeconds = audioRegion ? audioRegion.getClipStartOffsetSeconds() : 0;
const clipStartSample = Math.floor(clipStartOffsetSeconds * sampleRate);
// Calculate visible duration from region length in beats
const secondsPerBeat = 60 / bpm;
const regionLengthBeats = audioRegion ? audioRegion.getLength() : 0;
const visibleDurationSeconds = regionLengthBeats * secondsPerBeat;
const visibleSamples = Math.floor(visibleDurationSeconds * sampleRate);
// Clamp to buffer boundaries
const renderStartSample = Math.max(0, Math.min(clipStartSample, totalSamples));
const renderEndSample = Math.min(renderStartSample + visibleSamples, totalSamples);
const renderSampleCount = renderEndSample - renderStartSample;
if (renderSampleCount <= 0) return;
// Downsample visible portion to canvas width
const samplesPerPixel = Math.max(1, Math.floor(renderSampleCount / width));
const centerY = height / 2; const centerY = height / 2;
ctx.strokeStyle = 'rgba(255, 255, 255, 0.7)'; ctx.strokeStyle = 'rgba(255, 255, 255, 0.7)';
@@ -240,8 +258,8 @@ const RegionItem: React.FC<RegionItemProps> = ({
ctx.beginPath(); ctx.beginPath();
for (let x = 0; x < width; x++) { for (let x = 0; x < width; x++) {
const startSample = Math.floor(x * samplesPerPixel); const startSample = renderStartSample + Math.floor(x * samplesPerPixel);
const endSample = Math.min(startSample + samplesPerPixel, samples); const endSample = Math.min(startSample + samplesPerPixel, renderEndSample);
let min = 0; let min = 0;
let max = 0; let max = 0;
@@ -289,7 +307,7 @@ const RegionItem: React.FC<RegionItemProps> = ({
} else { } else {
renderNotesOnCanvas(); renderNotesOnCanvas();
} }
}, [midiRegion, audioRegion, audioBuffer, timeSignature, id, noteUpdateTrigger]); }, [midiRegion, audioRegion, audioBuffer, timeSignature, bpm, id, noteUpdateTrigger]);
// Re-render canvas when region content size changes // Re-render canvas when region content size changes
useEffect(() => { useEffect(() => {
@@ -310,7 +328,7 @@ const RegionItem: React.FC<RegionItemProps> = ({
resizeObserver.unobserve(regionContentRef.current); resizeObserver.unobserve(regionContentRef.current);
} }
}; };
}, [midiRegion, audioRegion, audioBuffer, timeSignature]); }, [midiRegion, audioRegion, audioBuffer, timeSignature, bpm]);
// Handle mouse movement to detect edge proximity // Handle mouse movement to detect edge proximity
const handleMouseMove = (e: React.MouseEvent<HTMLDivElement>) => { const handleMouseMove = (e: React.MouseEvent<HTMLDivElement>) => {
@@ -325,13 +343,6 @@ const RegionItem: React.FC<RegionItemProps> = ({
return; return;
} }
// Audio regions: move only, no resize
if (audioRegion) {
setCursor('grab');
setResizeEdge('none');
return;
}
const regionElement = e.currentTarget; const regionElement = e.currentTarget;
const rect = regionElement.getBoundingClientRect(); const rect = regionElement.getBoundingClientRect();
+72 -28
View File
@@ -4,7 +4,7 @@ import { KGMidiRegion } from '../../core/region/KGMidiRegion';
import TrackGridItem from './TrackGridItem'; import TrackGridItem from './TrackGridItem';
import { Playhead } from '../common'; import { Playhead } from '../common';
import type { RegionUI } from '../interfaces'; import type { RegionUI } from '../interfaces';
import { DEBUG_MODE } from '../../constants'; import { DEBUG_MODE, REGION_CONSTANTS } from '../../constants';
import { KGMainContentState } from '../../core/state/KGMainContentState'; import { KGMainContentState } from '../../core/state/KGMainContentState';
import { isModifierKeyPressed } from '../../util/osUtil'; import { isModifierKeyPressed } from '../../util/osUtil';
import { CreateRegionCommand, ResizeRegionCommand, MoveRegionCommand } from '../../core/commands'; import { CreateRegionCommand, ResizeRegionCommand, MoveRegionCommand } from '../../core/commands';
@@ -159,47 +159,91 @@ const TrackGridPanel: React.FC<TrackGridPanelProps> = ({
if (DEBUG_MODE.TRACK_GRID_PANEL) { if (DEBUG_MODE.TRACK_GRID_PANEL) {
console.log(`Finished resizing region ${regionId} to barNumber ${finalBarNumber}, length ${finalLength}`); console.log(`Finished resizing region ${regionId} to barNumber ${finalBarNumber}, length ${finalLength}`);
} }
// Find the region // Find the region
const region = regions.find(r => r.id === regionId); const region = regions.find(r => r.id === regionId);
if (!region) return; if (!region) return;
// Calculate new start and length in beats // Calculate new start and length in beats
const beatsPerBar = timeSignature.numerator; const beatsPerBar = timeSignature.numerator;
const newStartBeat = (finalBarNumber - 1) * beatsPerBar; let clampedBarNumber = finalBarNumber;
const newLengthInBeats = finalLength * beatsPerBar; let clampedLength = finalLength;
// Find the track that contains this region // Find the track that contains this region
const track = tracks.find(t => t.getId().toString() === region.trackId); const track = tracks.find(t => t.getId().toString() === region.trackId);
if (!track) return; if (!track) return;
// Update the region in the track's model // Update the region in the track's model
const trackRegions = track.getRegions(); const trackRegions = track.getRegions();
const midiRegion = trackRegions.find(r => r.getId() === regionId) as KGMidiRegion | undefined; const coreRegion = trackRegions.find(r => r.getId() === regionId);
if (midiRegion) { if (coreRegion) {
const oldStartBeat = midiRegion.getStartFromBeat(); const oldStartBeat = coreRegion.getStartFromBeat();
const oldBarNumber = region.barNumber; const oldBarNumber = region.barNumber;
if (DEBUG_MODE.TRACK_GRID_PANEL) { if (DEBUG_MODE.TRACK_GRID_PANEL) {
console.log(`Updating KGRegion model - Before: startBeat=${oldStartBeat}, length=${midiRegion.getLength()}`); console.log(`Updating KGRegion model - Before: startBeat=${oldStartBeat}, length=${coreRegion.getLength()}`);
console.log(`Bar numbers - old: ${oldBarNumber}, new: ${finalBarNumber}`); console.log(`Bar numbers - old: ${oldBarNumber}, new: ${finalBarNumber}`);
} }
// Clamp audio region resize to audio file boundaries
let newClipStartOffsetSeconds: number | undefined;
if (coreRegion instanceof KGAudioRegion) {
const bpm = KGCore.instance().getCurrentProject().getBpm();
const secondsPerBeat = 60 / bpm;
const clipOffset = coreRegion.getClipStartOffsetSeconds();
const audioDuration = coreRegion.getAudioDurationSeconds();
// Left edge changed — calculate new clip offset
if (clampedBarNumber !== oldBarNumber) {
const newStartBeat = (clampedBarNumber - 1) * beatsPerBar;
const beatDelta = newStartBeat - oldStartBeat;
const secondsDelta = beatDelta * secondsPerBeat;
const unclampedClipOffset = clipOffset + secondsDelta;
if (unclampedClipOffset < 0) {
// Dragged past audio start — snap to earliest allowed position
const maxLeftExtensionBeats = clipOffset / secondsPerBeat;
const minStartBeat = oldStartBeat - maxLeftExtensionBeats;
clampedBarNumber = Math.ceil(minStartBeat / beatsPerBar) + 1;
const oldEndBarNumber = oldBarNumber + (coreRegion.getLength() / beatsPerBar);
clampedLength = oldEndBarNumber - clampedBarNumber;
newClipStartOffsetSeconds = 0;
} else {
newClipStartOffsetSeconds = Math.min(unclampedClipOffset, audioDuration);
}
}
// Right edge — clamp length so it doesn't exceed remaining audio
const effectiveClipOffset = newClipStartOffsetSeconds ?? clipOffset;
const maxDurationSeconds = audioDuration - effectiveClipOffset;
const maxLengthBars = (maxDurationSeconds / secondsPerBeat) / beatsPerBar;
if (clampedLength > maxLengthBars) {
clampedLength = Math.floor(maxLengthBars);
if (clampedLength < REGION_CONSTANTS.MIN_REGION_LENGTH) {
clampedLength = REGION_CONSTANTS.MIN_REGION_LENGTH;
}
}
}
const newStartBeat = (clampedBarNumber - 1) * beatsPerBar;
const newLengthInBeats = clampedLength * beatsPerBar;
// Use command pattern to update the region position and length (note adjustments handled inside command) // Use command pattern to update the region position and length (note adjustments handled inside command)
try { try {
const command = ResizeRegionCommand.fromBarCoordinates( const command = ResizeRegionCommand.fromBarCoordinates(
regionId, regionId,
finalBarNumber, clampedBarNumber,
finalLength, clampedLength,
timeSignature timeSignature,
newClipStartOffsetSeconds
); );
KGCore.instance().executeCommand(command); KGCore.instance().executeCommand(command);
if (DEBUG_MODE.TRACK_GRID_PANEL) { if (DEBUG_MODE.TRACK_GRID_PANEL) {
console.log(`Executed ResizeRegionCommand: region ${regionId} resized using command pattern`); console.log(`Executed ResizeRegionCommand: region ${regionId} resized using command pattern`);
// Verify the command worked // Verify the command worked
const updatedRegion = track.getRegions().find(r => r.getId() === regionId); const updatedRegion = track.getRegions().find(r => r.getId() === regionId);
console.log(`Verified region in track: ${updatedRegion ? 'found' : 'not found'}, startBeat=${updatedRegion?.getStartFromBeat()}, length=${updatedRegion?.getLength()}`); console.log(`Verified region in track: ${updatedRegion ? 'found' : 'not found'}, startBeat=${updatedRegion?.getStartFromBeat()}, length=${updatedRegion?.getLength()}`);
@@ -208,15 +252,15 @@ const TrackGridPanel: React.FC<TrackGridPanelProps> = ({
console.error('Error resizing region:', error); console.error('Error resizing region:', error);
return; return;
} }
}
// Update the region in the parent component with expected model values
// Update the region in the parent component with expected model values if (onRegionUpdated) {
if (onRegionUpdated) { onRegionUpdated(
onRegionUpdated( regionId,
regionId, { barNumber: clampedBarNumber, length: clampedLength },
{ barNumber: finalBarNumber, length: finalLength }, { startBeat: newStartBeat, length: newLengthInBeats }
{ startBeat: newStartBeat, length: newLengthInBeats } );
); }
} }
}; };
+1 -1
View File
@@ -52,7 +52,7 @@ export class KGProject {
@WithDefault(0) @WithDefault(0)
private projectStructureVersion: number = 0; private projectStructureVersion: number = 0;
public static readonly CURRENT_PROJECT_STRUCTURE_VERSION: number = 5; public static readonly CURRENT_PROJECT_STRUCTURE_VERSION: number = 6;
@Expose() @Expose()
@Type(() => KGTrack, { @Type(() => KGTrack, {
+19 -4
View File
@@ -506,6 +506,10 @@ export class KGAudioInterface {
return; return;
} }
// Clip offset: where playback starts within the audio file
const clipStartOffsetSeconds = audioRegion.getClipStartOffsetSeconds();
const audioDurationSeconds = audioRegion.getAudioDurationSeconds();
// Skip regions that start before playback start position // Skip regions that start before playback start position
if (regionStartBeat < startPosition) { if (regionStartBeat < startPosition) {
// Region starts before playhead — calculate offset into the audio file // Region starts before playhead — calculate offset into the audio file
@@ -523,6 +527,12 @@ export class KGAudioInterface {
effectiveRemainingSeconds = Math.min(remainingSeconds, maxDurationSeconds); effectiveRemainingSeconds = Math.min(remainingSeconds, maxDurationSeconds);
} }
// Cap at available audio after clip offset
effectiveRemainingSeconds = Math.min(
effectiveRemainingSeconds,
audioDurationSeconds - clipStartOffsetSeconds - offsetSeconds
);
if (effectiveRemainingSeconds > 0 && playerBus.hasBuffer(audioFileId)) { if (effectiveRemainingSeconds > 0 && playerBus.hasBuffer(audioFileId)) {
// Resume slightly after the current transport boundary and // Resume slightly after the current transport boundary and
// compensate the source offset/duration. Scheduling exactly // compensate the source offset/duration. Scheduling exactly
@@ -533,7 +543,7 @@ export class KGAudioInterface {
regionEndBeat regionEndBeat
); );
const extraOffsetSeconds = (safeResumeBeat - startPosition) * secondsPerBeat; const extraOffsetSeconds = (safeResumeBeat - startPosition) * secondsPerBeat;
const adjustedOffsetSeconds = offsetSeconds + extraOffsetSeconds; const adjustedOffsetSeconds = clipStartOffsetSeconds + offsetSeconds + extraOffsetSeconds;
const adjustedRemainingSeconds = Math.max( const adjustedRemainingSeconds = Math.max(
0, 0,
effectiveRemainingSeconds - extraOffsetSeconds effectiveRemainingSeconds - extraOffsetSeconds
@@ -562,7 +572,12 @@ export class KGAudioInterface {
} }
const audioFileId = audioRegion.getAudioFileId(); const audioFileId = audioRegion.getAudioFileId();
let effectiveDurationSeconds = audioRegion.getAudioDurationSeconds(); // Effective duration: region length in seconds, capped at available audio after clip offset
const regionLengthSeconds = region.getLength() * secondsPerBeat;
let effectiveDurationSeconds = Math.min(
regionLengthSeconds,
audioDurationSeconds - clipStartOffsetSeconds
);
if (!playerBus.hasBuffer(audioFileId)) { if (!playerBus.hasBuffer(audioFileId)) {
console.warn(`No audio buffer loaded for ${audioFileId}`); console.warn(`No audio buffer loaded for ${audioFileId}`);
@@ -579,13 +594,13 @@ export class KGAudioInterface {
const regionStartTime = this.beatsToToneTime(regionStartBeat); const regionStartTime = this.beatsToToneTime(regionStartBeat);
console.log( console.log(
`Scheduling audio region "${region.getName()}" at beat ${regionStartBeat}, duration: ${effectiveDurationSeconds}s` `Scheduling audio region "${region.getName()}" at beat ${regionStartBeat}, clipOffset: ${clipStartOffsetSeconds}s, duration: ${effectiveDurationSeconds}s`
); );
const eventId = Tone.Transport.schedule((time) => { const eventId = Tone.Transport.schedule((time) => {
const hasSoloedTracks = this.hasSoloedTracks(); const hasSoloedTracks = this.hasSoloedTracks();
if (playerBus.shouldPlayWithSolo(hasSoloedTracks)) { if (playerBus.shouldPlayWithSolo(hasSoloedTracks)) {
playerBus.schedulePlayback(time + playbackDelay, audioFileId, 0, effectiveDurationSeconds); playerBus.schedulePlayback(time + playbackDelay, audioFileId, clipStartOffsetSeconds, effectiveDurationSeconds);
} }
}, regionStartTime); }, regionStartTime);
+33 -11
View File
@@ -2,6 +2,7 @@ import { KGCommand } from '../KGCommand';
import { KGCore } from '../../KGCore'; import { KGCore } from '../../KGCore';
import { KGRegion } from '../../region/KGRegion'; import { KGRegion } from '../../region/KGRegion';
import { KGMidiRegion } from '../../region/KGMidiRegion'; import { KGMidiRegion } from '../../region/KGMidiRegion';
import { KGAudioRegion } from '../../region/KGAudioRegion';
import { KGMidiNote } from '../../midi/KGMidiNote'; import { KGMidiNote } from '../../midi/KGMidiNote';
/** /**
@@ -15,7 +16,7 @@ export class ResizeRegionCommand extends KGCommand {
private originalStartFromBeat: number = 0; private originalStartFromBeat: number = 0;
private originalLength: number = 0; private originalLength: number = 0;
private targetRegion: KGRegion | null = null; private targetRegion: KGRegion | null = null;
// Store note adjustments for undo // Store note adjustments for undo
private noteAdjustments: Array<{ private noteAdjustments: Array<{
noteId: string; noteId: string;
@@ -23,11 +24,16 @@ export class ResizeRegionCommand extends KGCommand {
originalEndBeat: number; originalEndBeat: number;
}> = []; }> = [];
constructor(regionId: string, newStartFromBeat: number, newLength: number) { // Audio region clip offset support
private newClipStartOffsetSeconds?: number;
private originalClipStartOffsetSeconds: number = 0;
constructor(regionId: string, newStartFromBeat: number, newLength: number, newClipStartOffsetSeconds?: number) {
super(); super();
this.regionId = regionId; this.regionId = regionId;
this.newStartFromBeat = newStartFromBeat; this.newStartFromBeat = newStartFromBeat;
this.newLength = newLength; this.newLength = newLength;
this.newClipStartOffsetSeconds = newClipStartOffsetSeconds;
} }
execute(): void { execute(): void {
@@ -57,10 +63,10 @@ export class ResizeRegionCommand extends KGCommand {
this.originalStartFromBeat = targetRegion.getStartFromBeat(); this.originalStartFromBeat = targetRegion.getStartFromBeat();
this.originalLength = targetRegion.getLength(); this.originalLength = targetRegion.getLength();
// Handle note adjustments if start position changes (left-edge resize) // Handle note adjustments if start position changes (left-edge resize) for MIDI regions
if (this.newStartFromBeat !== this.originalStartFromBeat && targetRegion instanceof KGMidiRegion) { if (this.newStartFromBeat !== this.originalStartFromBeat && targetRegion instanceof KGMidiRegion) {
const beatOffset = this.newStartFromBeat - this.originalStartFromBeat; const beatOffset = this.newStartFromBeat - this.originalStartFromBeat;
// Store original note positions and adjust notes to maintain absolute positions // Store original note positions and adjust notes to maintain absolute positions
const notes = targetRegion.getNotes(); const notes = targetRegion.getNotes();
notes.forEach(note => { notes.forEach(note => {
@@ -70,15 +76,24 @@ export class ResizeRegionCommand extends KGCommand {
originalStartBeat: note.getStartBeat(), originalStartBeat: note.getStartBeat(),
originalEndBeat: note.getEndBeat() originalEndBeat: note.getEndBeat()
}); });
// Adjust note positions to maintain absolute position // Adjust note positions to maintain absolute position
note.setStartBeat(note.getStartBeat() - beatOffset); note.setStartBeat(note.getStartBeat() - beatOffset);
note.setEndBeat(note.getEndBeat() - beatOffset); note.setEndBeat(note.getEndBeat() - beatOffset);
}); });
console.log(`Adjusted ${notes.length} notes by offset ${-beatOffset} beats to maintain absolute positions`); console.log(`Adjusted ${notes.length} notes by offset ${-beatOffset} beats to maintain absolute positions`);
} }
// Handle clip offset for audio regions
if (targetRegion instanceof KGAudioRegion) {
this.originalClipStartOffsetSeconds = targetRegion.getClipStartOffsetSeconds();
if (this.newClipStartOffsetSeconds !== undefined) {
targetRegion.setClipStartOffsetSeconds(this.newClipStartOffsetSeconds);
console.log(`Updated audio clip offset: ${this.originalClipStartOffsetSeconds}${this.newClipStartOffsetSeconds}`);
}
}
// Apply the resize // Apply the resize
targetRegion.setStartFromBeat(this.newStartFromBeat); targetRegion.setStartFromBeat(this.newStartFromBeat);
targetRegion.setLength(this.newLength); targetRegion.setLength(this.newLength);
@@ -95,7 +110,7 @@ export class ResizeRegionCommand extends KGCommand {
// Restore note positions if they were adjusted // Restore note positions if they were adjusted
if (this.noteAdjustments.length > 0 && this.targetRegion instanceof KGMidiRegion) { if (this.noteAdjustments.length > 0 && this.targetRegion instanceof KGMidiRegion) {
const notes = this.targetRegion.getNotes(); const notes = this.targetRegion.getNotes();
// Restore each note to its original position // Restore each note to its original position
this.noteAdjustments.forEach(adjustment => { this.noteAdjustments.forEach(adjustment => {
const note = notes.find(n => n.getId() === adjustment.noteId); const note = notes.find(n => n.getId() === adjustment.noteId);
@@ -104,10 +119,16 @@ export class ResizeRegionCommand extends KGCommand {
note.setEndBeat(adjustment.originalEndBeat); note.setEndBeat(adjustment.originalEndBeat);
} }
}); });
console.log(`Restored ${this.noteAdjustments.length} notes to their original positions`); console.log(`Restored ${this.noteAdjustments.length} notes to their original positions`);
} }
// Restore clip offset for audio regions
if (this.targetRegion instanceof KGAudioRegion && this.newClipStartOffsetSeconds !== undefined) {
this.targetRegion.setClipStartOffsetSeconds(this.originalClipStartOffsetSeconds);
console.log(`Restored audio clip offset: ${this.newClipStartOffsetSeconds}${this.originalClipStartOffsetSeconds}`);
}
// Restore original region values // Restore original region values
this.targetRegion.setStartFromBeat(this.originalStartFromBeat); this.targetRegion.setStartFromBeat(this.originalStartFromBeat);
this.targetRegion.setLength(this.originalLength); this.targetRegion.setLength(this.originalLength);
@@ -184,12 +205,13 @@ export class ResizeRegionCommand extends KGCommand {
regionId: string, regionId: string,
newBarNumber: number, newBarNumber: number,
newLengthInBars: number, newLengthInBars: number,
timeSignature: { numerator: number; denominator: number } timeSignature: { numerator: number; denominator: number },
newClipStartOffsetSeconds?: number
): ResizeRegionCommand { ): ResizeRegionCommand {
const beatsPerBar = timeSignature.numerator; const beatsPerBar = timeSignature.numerator;
const newStartFromBeat = (newBarNumber - 1) * beatsPerBar; const newStartFromBeat = (newBarNumber - 1) * beatsPerBar;
const newLength = newLengthInBars * beatsPerBar; const newLength = newLengthInBars * beatsPerBar;
return new ResizeRegionCommand(regionId, newStartFromBeat, newLength); return new ResizeRegionCommand(regionId, newStartFromBeat, newLength, newClipStartOffsetSeconds);
} }
} }
@@ -4,6 +4,7 @@ import { upgradeToV2 } from './upgradeToV2';
import { upgradeToV3 } from './upgradeToV3'; import { upgradeToV3 } from './upgradeToV3';
import { upgradeToV4 } from './upgradeToV4'; import { upgradeToV4 } from './upgradeToV4';
import { upgradeToV5 } from './upgradeToV5'; import { upgradeToV5 } from './upgradeToV5';
import { upgradeToV6 } from './upgradeToV6';
/** /**
* Upgrade the given project to the latest structure version, one version at a time. * Upgrade the given project to the latest structure version, one version at a time.
@@ -43,6 +44,10 @@ export function upgradeProjectToLatest(project: KGProject): KGProject {
workingProject = upgradeToV5(workingProject); workingProject = upgradeToV5(workingProject);
break; break;
} }
case 6: {
workingProject = upgradeToV6(workingProject);
break;
}
default: { default: {
// If an upgrader is missing, throw to prevent loading incompatible structures // If an upgrader is missing, throw to prevent loading incompatible structures
throw new Error(`No upgrader found for project structure version ${nextVersion}`); throw new Error(`No upgrader found for project structure version ${nextVersion}`);
+21
View File
@@ -0,0 +1,21 @@
import { KGProject } from '../KGProject';
import { KGAudioRegion } from '../region/KGAudioRegion';
export function upgradeToV6(project: KGProject): KGProject {
try {
// Ensure all audio regions have clipStartOffsetSeconds initialized
for (const track of project.getTracks()) {
for (const region of track.getRegions()) {
if (region instanceof KGAudioRegion) {
const current = region.getClipStartOffsetSeconds?.();
if (current === undefined || current === null) {
region.setClipStartOffsetSeconds(0);
}
}
}
}
} finally {
project.setProjectStructureVersion(6);
}
return project;
}
+15 -1
View File
@@ -22,6 +22,10 @@ export class KGAudioRegion extends KGRegion {
@WithDefault(0) @WithDefault(0)
protected audioDurationSeconds: number = 0; protected audioDurationSeconds: number = 0;
@Expose()
@WithDefault(0)
protected clipStartOffsetSeconds: number = 0;
constructor( constructor(
id: string, id: string,
trackId: string, trackId: string,
@@ -31,13 +35,15 @@ export class KGAudioRegion extends KGRegion {
length: number = 0, length: number = 0,
audioFileId: string = '', audioFileId: string = '',
audioFileName: string = '', audioFileName: string = '',
audioDurationSeconds: number = 0 audioDurationSeconds: number = 0,
clipStartOffsetSeconds: number = 0
) { ) {
super(id, trackId, trackIndex, name, startFromBeat, length); super(id, trackId, trackIndex, name, startFromBeat, length);
this.__type = 'KGAudioRegion'; this.__type = 'KGAudioRegion';
this.audioFileId = audioFileId; this.audioFileId = audioFileId;
this.audioFileName = audioFileName; this.audioFileName = audioFileName;
this.audioDurationSeconds = audioDurationSeconds; this.audioDurationSeconds = audioDurationSeconds;
this.clipStartOffsetSeconds = clipStartOffsetSeconds;
} }
// Getters // Getters
@@ -66,6 +72,14 @@ export class KGAudioRegion extends KGRegion {
this.audioDurationSeconds = audioDurationSeconds; this.audioDurationSeconds = audioDurationSeconds;
} }
public getClipStartOffsetSeconds(): number {
return this.clipStartOffsetSeconds;
}
public setClipStartOffsetSeconds(clipStartOffsetSeconds: number): void {
this.clipStartOffsetSeconds = clipStartOffsetSeconds;
}
// Override getCurrentType to return specific subclass type // Override getCurrentType to return specific subclass type
public override getCurrentType(): string { public override getCurrentType(): string {
return 'KGAudioRegion'; return 'KGAudioRegion';