diff --git a/src/core/KGProject.ts b/src/core/KGProject.ts index 28647d7..97d3831 100644 --- a/src/core/KGProject.ts +++ b/src/core/KGProject.ts @@ -52,7 +52,7 @@ export class KGProject { @WithDefault(0) private projectStructureVersion: number = 0; - public static readonly CURRENT_PROJECT_STRUCTURE_VERSION: number = 7; + public static readonly CURRENT_PROJECT_STRUCTURE_VERSION: number = 8; @Expose() @Type(() => KGTrack, { @@ -181,4 +181,3 @@ export class KGProject { this.barWidthMultiplier = barWidthMultiplier; } } - diff --git a/src/core/project-upgrader/KGProjectUpgrader.ts b/src/core/project-upgrader/KGProjectUpgrader.ts index 92100be..5bb0d56 100644 --- a/src/core/project-upgrader/KGProjectUpgrader.ts +++ b/src/core/project-upgrader/KGProjectUpgrader.ts @@ -6,6 +6,7 @@ import { upgradeToV4 } from './upgradeToV4'; import { upgradeToV5 } from './upgradeToV5'; import { upgradeToV6 } from './upgradeToV6'; import { upgradeToV7 } from './upgradeToV7'; +import { upgradeToV8 } from './upgradeToV8'; /** * Upgrade the given project to the latest structure version, one version at a time. @@ -53,6 +54,10 @@ export function upgradeProjectToLatest(project: KGProject): KGProject { workingProject = upgradeToV7(workingProject); break; } + case 8: { + workingProject = upgradeToV8(workingProject); + break; + } default: { // If an upgrader is missing, throw to prevent loading incompatible structures throw new Error(`No upgrader found for project structure version ${nextVersion}`); diff --git a/src/core/project-upgrader/upgradeToV8.test.ts b/src/core/project-upgrader/upgradeToV8.test.ts new file mode 100644 index 0000000..4370754 --- /dev/null +++ b/src/core/project-upgrader/upgradeToV8.test.ts @@ -0,0 +1,63 @@ +import { describe, expect, it } from 'vitest'; +import { KGProject } from '../KGProject'; +import { KGMidiTrack } from '../track/KGMidiTrack'; +import { KGAudioTrack } from '../track/KGAudioTrack'; +import { KGMidiRegion } from '../region/KGMidiRegion'; +import { KGAudioRegion } from '../region/KGAudioRegion'; +import { KGMidiNote } from '../midi/KGMidiNote'; +import { KGMidiPitchBend } from '../midi/KGMidiPitchBend'; +import { upgradeToV8 } from './upgradeToV8'; +import { upgradeProjectToLatest } from './KGProjectUpgrader'; + +function makeLegacyMidiRegion(id: string): KGMidiRegion { + const region = new KGMidiRegion(id, 'track-1', 0, 'Legacy MIDI', 0, 4); + region.addNote(new KGMidiNote('note-1', 0, 1, 60, 100)); + delete (region as unknown as { pitchBends?: KGMidiPitchBend[] }).pitchBends; + return region; +} + +describe('upgradeToV8', () => { + it('initializes missing pitch bend arrays on midi regions', () => { + const midiTrack = new KGMidiTrack('MIDI Track', 0); + const midiRegion = makeLegacyMidiRegion('midi-region-1'); + midiTrack.setRegions([midiRegion]); + + const audioTrack = new KGAudioTrack('Audio Track', 1); + const audioRegion = new KGAudioRegion('audio-region-1', 'track-2', 1, 'Audio', 0, 4); + audioTrack.setRegions([audioRegion]); + + const project = new KGProject('Test', 32, 0, 125, undefined, undefined, undefined, undefined, undefined, undefined, [midiTrack, audioTrack], 7); + + upgradeToV8(project); + + expect(midiRegion.getNotes()).toHaveLength(1); + expect(midiRegion.getPitchBends()).toEqual([]); + expect(audioTrack.getRegions()[0]).toBe(audioRegion); + expect(project.getProjectStructureVersion()).toBe(8); + }); + + it('preserves existing pitch bends', () => { + const midiTrack = new KGMidiTrack('MIDI Track', 0); + const midiRegion = new KGMidiRegion('midi-region-1', 'track-1', 0, 'MIDI', 0, 4); + midiRegion.addPitchBend(new KGMidiPitchBend('bend-1', 0.5, 12288)); + midiTrack.setRegions([midiRegion]); + const project = new KGProject('Test', 32, 0, 125, undefined, undefined, undefined, undefined, undefined, undefined, [midiTrack], 7); + + upgradeToV8(project); + + expect(midiRegion.getPitchBends()).toHaveLength(1); + expect(midiRegion.getPitchBends()[0].getValue()).toBe(12288); + }); + + it('upgrades legacy projects through the main upgrader path', () => { + const midiTrack = new KGMidiTrack('MIDI Track', 0); + const midiRegion = makeLegacyMidiRegion('midi-region-1'); + midiTrack.setRegions([midiRegion]); + const project = new KGProject('Test', 32, 0, 125, undefined, undefined, undefined, undefined, undefined, undefined, [midiTrack], 7); + + const upgraded = upgradeProjectToLatest(project); + + expect(upgraded.getProjectStructureVersion()).toBe(8); + expect((upgraded.getTracks()[0].getRegions()[0] as KGMidiRegion).getPitchBends()).toEqual([]); + }); +}); diff --git a/src/core/project-upgrader/upgradeToV8.ts b/src/core/project-upgrader/upgradeToV8.ts new file mode 100644 index 0000000..cc4c3dd --- /dev/null +++ b/src/core/project-upgrader/upgradeToV8.ts @@ -0,0 +1,21 @@ +import { KGProject } from '../KGProject'; +import { KGMidiRegion } from '../region/KGMidiRegion'; + +export function upgradeToV8(project: KGProject): KGProject { + try { + for (const track of project.getTracks()) { + for (const region of track.getRegions()) { + if (region instanceof KGMidiRegion) { + const candidate = (region as unknown as { pitchBends?: unknown }).pitchBends; + if (!Array.isArray(candidate)) { + region.setPitchBends([]); + } + } + } + } + } finally { + project.setProjectStructureVersion(8); + } + + return project; +} diff --git a/src/core/region/KGMidiRegion.test.ts b/src/core/region/KGMidiRegion.test.ts index 71e0fee..3c01fa2 100644 --- a/src/core/region/KGMidiRegion.test.ts +++ b/src/core/region/KGMidiRegion.test.ts @@ -397,5 +397,20 @@ describe('KGMidiRegion', () => { expect(restored.getPitchBends()[0]).toBeInstanceOf(KGMidiPitchBend); expect(restored.getPitchBends()[0].getValue()).toBe(12288); }); + + it('defaults missing legacy pitch bend data to an empty array', () => { + const restored = plainToInstance(KGMidiRegion, { + __type: 'KGMidiRegion', + id: 'legacy-region', + trackId: 'track-1', + trackIndex: 0, + name: 'Legacy Region', + startFromBeat: 0, + length: 4, + notes: [], + }); + + expect(restored.getPitchBends()).toEqual([]); + }); }); }); diff --git a/src/core/region/KGMidiRegion.ts b/src/core/region/KGMidiRegion.ts index 267b934..f5a2c0e 100644 --- a/src/core/region/KGMidiRegion.ts +++ b/src/core/region/KGMidiRegion.ts @@ -26,38 +26,44 @@ export class KGMidiRegion extends KGRegion { // Getter public getNotes(): KGMidiNote[] { + if (!this.notes) { + this.notes = []; + } return this.notes; } // Setter public setNotes(notes: KGMidiNote[]): void { - this.notes = notes; + this.notes = notes ?? []; } public getPitchBends(): KGMidiPitchBend[] { + if (!this.pitchBends) { + this.pitchBends = []; + } return this.pitchBends; } public setPitchBends(pitchBends: KGMidiPitchBend[]): void { - this.pitchBends = pitchBends; + this.pitchBends = pitchBends ?? []; } // Add a single note public addNote(note: KGMidiNote): void { - this.notes.push(note); + this.getNotes().push(note); } // Remove a note by ID public removeNote(noteId: string): void { - this.notes = this.notes.filter(note => note.getId() !== noteId); + this.notes = this.getNotes().filter(note => note.getId() !== noteId); } public addPitchBend(pitchBend: KGMidiPitchBend): void { - this.pitchBends.push(pitchBend); + this.getPitchBends().push(pitchBend); } public removePitchBend(pitchBendId: string): void { - this.pitchBends = this.pitchBends.filter(pitchBend => pitchBend.getId() !== pitchBendId); + this.pitchBends = this.getPitchBends().filter(pitchBend => pitchBend.getId() !== pitchBendId); } // Override getCurrentType to return specific subclass type