fix: load existing projects will crash the playback or have no sound due to missing pitchBend array in the JSON
This commit is contained in:
@@ -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 = 7;
|
public static readonly CURRENT_PROJECT_STRUCTURE_VERSION: number = 8;
|
||||||
|
|
||||||
@Expose()
|
@Expose()
|
||||||
@Type(() => KGTrack, {
|
@Type(() => KGTrack, {
|
||||||
@@ -181,4 +181,3 @@ export class KGProject {
|
|||||||
this.barWidthMultiplier = barWidthMultiplier;
|
this.barWidthMultiplier = barWidthMultiplier;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { upgradeToV4 } from './upgradeToV4';
|
|||||||
import { upgradeToV5 } from './upgradeToV5';
|
import { upgradeToV5 } from './upgradeToV5';
|
||||||
import { upgradeToV6 } from './upgradeToV6';
|
import { upgradeToV6 } from './upgradeToV6';
|
||||||
import { upgradeToV7 } from './upgradeToV7';
|
import { upgradeToV7 } from './upgradeToV7';
|
||||||
|
import { upgradeToV8 } from './upgradeToV8';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 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.
|
||||||
@@ -53,6 +54,10 @@ export function upgradeProjectToLatest(project: KGProject): KGProject {
|
|||||||
workingProject = upgradeToV7(workingProject);
|
workingProject = upgradeToV7(workingProject);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
case 8: {
|
||||||
|
workingProject = upgradeToV8(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}`);
|
||||||
|
|||||||
@@ -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([]);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -397,5 +397,20 @@ describe('KGMidiRegion', () => {
|
|||||||
expect(restored.getPitchBends()[0]).toBeInstanceOf(KGMidiPitchBend);
|
expect(restored.getPitchBends()[0]).toBeInstanceOf(KGMidiPitchBend);
|
||||||
expect(restored.getPitchBends()[0].getValue()).toBe(12288);
|
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([]);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -26,38 +26,44 @@ export class KGMidiRegion extends KGRegion {
|
|||||||
|
|
||||||
// Getter
|
// Getter
|
||||||
public getNotes(): KGMidiNote[] {
|
public getNotes(): KGMidiNote[] {
|
||||||
|
if (!this.notes) {
|
||||||
|
this.notes = [];
|
||||||
|
}
|
||||||
return this.notes;
|
return this.notes;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Setter
|
// Setter
|
||||||
public setNotes(notes: KGMidiNote[]): void {
|
public setNotes(notes: KGMidiNote[]): void {
|
||||||
this.notes = notes;
|
this.notes = notes ?? [];
|
||||||
}
|
}
|
||||||
|
|
||||||
public getPitchBends(): KGMidiPitchBend[] {
|
public getPitchBends(): KGMidiPitchBend[] {
|
||||||
|
if (!this.pitchBends) {
|
||||||
|
this.pitchBends = [];
|
||||||
|
}
|
||||||
return this.pitchBends;
|
return this.pitchBends;
|
||||||
}
|
}
|
||||||
|
|
||||||
public setPitchBends(pitchBends: KGMidiPitchBend[]): void {
|
public setPitchBends(pitchBends: KGMidiPitchBend[]): void {
|
||||||
this.pitchBends = pitchBends;
|
this.pitchBends = pitchBends ?? [];
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add a single note
|
// Add a single note
|
||||||
public addNote(note: KGMidiNote): void {
|
public addNote(note: KGMidiNote): void {
|
||||||
this.notes.push(note);
|
this.getNotes().push(note);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remove a note by ID
|
// Remove a note by ID
|
||||||
public removeNote(noteId: string): void {
|
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 {
|
public addPitchBend(pitchBend: KGMidiPitchBend): void {
|
||||||
this.pitchBends.push(pitchBend);
|
this.getPitchBends().push(pitchBend);
|
||||||
}
|
}
|
||||||
|
|
||||||
public removePitchBend(pitchBendId: string): void {
|
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
|
// Override getCurrentType to return specific subclass type
|
||||||
|
|||||||
Reference in New Issue
Block a user