feat: persist track mute/solo status
This commit is contained in:
@@ -96,8 +96,8 @@ const TrackInfoItem: React.FC<TrackInfoItemProps> = ({
|
|||||||
const volumeInputRef = useRef<HTMLInputElement>(null);
|
const volumeInputRef = useRef<HTMLInputElement>(null);
|
||||||
// Local flag to track slider interaction; not used for rendering
|
// Local flag to track slider interaction; not used for rendering
|
||||||
const isAdjustingVolumeRef = useRef(false);
|
const isAdjustingVolumeRef = useRef(false);
|
||||||
const [muted, setMuted] = useState(false);
|
const [muted, setMuted] = useState(track.getMuted());
|
||||||
const [solo, setSolo] = useState(false);
|
const [solo, setSolo] = useState(track.getSolo());
|
||||||
|
|
||||||
// Close dropdown when clicking outside
|
// Close dropdown when clicking outside
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -135,11 +135,10 @@ const TrackInfoItem: React.FC<TrackInfoItemProps> = ({
|
|||||||
setVolume(track.getVolume());
|
setVolume(track.getVolume());
|
||||||
}, [allTracks, track]);
|
}, [allTracks, track]);
|
||||||
|
|
||||||
// Sync mute/solo UI with audio interface state on track/project changes
|
// Sync mute/solo UI with the track model on track/project changes
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const audioInterface = KGAudioInterface.instance();
|
setMuted(track.getMuted());
|
||||||
setMuted(audioInterface.getTrackMuted(track.getId().toString()));
|
setSolo(track.getSolo());
|
||||||
setSolo(audioInterface.getTrackSolo(track.getId().toString()));
|
|
||||||
}, [allTracks, track]);
|
}, [allTracks, track]);
|
||||||
|
|
||||||
// Handle track name edit within the component
|
// Handle track name edit within the component
|
||||||
@@ -252,22 +251,20 @@ const TrackInfoItem: React.FC<TrackInfoItemProps> = ({
|
|||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
const next = !muted;
|
const next = !muted;
|
||||||
setMuted(next);
|
setMuted(next);
|
||||||
try {
|
useProjectStore.getState().updateTrackProperties(track.getId(), { muted: next }).catch(err => {
|
||||||
KGAudioInterface.instance().setTrackMute(track.getId().toString(), next);
|
setMuted(track.getMuted());
|
||||||
} catch (err) {
|
|
||||||
console.error('Failed to toggle mute:', err);
|
console.error('Failed to toggle mute:', err);
|
||||||
}
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleToggleSolo = (e: React.MouseEvent<HTMLButtonElement>) => {
|
const handleToggleSolo = (e: React.MouseEvent<HTMLButtonElement>) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
const next = !solo;
|
const next = !solo;
|
||||||
setSolo(next);
|
setSolo(next);
|
||||||
try {
|
useProjectStore.getState().updateTrackProperties(track.getId(), { solo: next }).catch(err => {
|
||||||
KGAudioInterface.instance().setTrackSolo(track.getId().toString(), next);
|
setSolo(track.getSolo());
|
||||||
} catch (err) {
|
|
||||||
console.error('Failed to toggle solo:', err);
|
console.error('Failed to toggle solo:', err);
|
||||||
}
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
// Handle track click
|
// Handle track click
|
||||||
|
|||||||
@@ -53,7 +53,7 @@ export class KGProject {
|
|||||||
@WithDefault(0)
|
@WithDefault(0)
|
||||||
private projectStructureVersion: number = 0;
|
private projectStructureVersion: number = 0;
|
||||||
|
|
||||||
public static readonly CURRENT_PROJECT_STRUCTURE_VERSION: number = 10;
|
public static readonly CURRENT_PROJECT_STRUCTURE_VERSION: number = 11;
|
||||||
|
|
||||||
@Expose()
|
@Expose()
|
||||||
@Type(() => KGTrack, {
|
@Type(() => KGTrack, {
|
||||||
|
|||||||
@@ -242,11 +242,13 @@ export class KGAudioInterface {
|
|||||||
console.log(`Creating audio bus for track ${trackId} with instrument ${instrumentType}`);
|
console.log(`Creating audio bus for track ${trackId} with instrument ${instrumentType}`);
|
||||||
|
|
||||||
// Create new audio bus
|
// Create new audio bus
|
||||||
// Initialize with track's stored volume if available
|
// Initialize with track's stored mix state if available
|
||||||
const project = KGCore.instance().getCurrentProject();
|
const project = KGCore.instance().getCurrentProject();
|
||||||
const track = project.getTracks().find(t => t.getId().toString() === trackId);
|
const track = project.getTracks().find(t => t.getId().toString() === trackId);
|
||||||
const initialVolume = track ? track.getVolume() : AUDIO_INTERFACE_CONSTANTS.DEFAULT_TRACK_VOLUME;
|
const initialVolume = track ? track.getVolume() : AUDIO_INTERFACE_CONSTANTS.DEFAULT_TRACK_VOLUME;
|
||||||
const audioBus = await KGAudioBus.create(instrumentType, initialVolume, 0);
|
const initialMuted = track ? track.getMuted() : false;
|
||||||
|
const initialSolo = track ? track.getSolo() : false;
|
||||||
|
const audioBus = await KGAudioBus.create(instrumentType, initialVolume, 0, initialMuted, initialSolo);
|
||||||
|
|
||||||
// Connect to master gain if available, otherwise to destination
|
// Connect to master gain if available, otherwise to destination
|
||||||
if (this.masterGain) {
|
if (this.masterGain) {
|
||||||
@@ -299,7 +301,11 @@ export class KGAudioInterface {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
console.log(`Creating audio player bus for track ${trackId}`);
|
console.log(`Creating audio player bus for track ${trackId}`);
|
||||||
const playerBus = await KGAudioPlayerBus.create(volume, 0);
|
const project = KGCore.instance().getCurrentProject();
|
||||||
|
const track = project.getTracks().find(t => t.getId().toString() === trackId);
|
||||||
|
const initialMuted = track ? track.getMuted() : false;
|
||||||
|
const initialSolo = track ? track.getSolo() : false;
|
||||||
|
const playerBus = await KGAudioPlayerBus.create(volume, 0, initialMuted, initialSolo);
|
||||||
|
|
||||||
if (this.masterGain) {
|
if (this.masterGain) {
|
||||||
playerBus.connect(this.masterGain);
|
playerBus.connect(this.masterGain);
|
||||||
|
|||||||
@@ -0,0 +1,87 @@
|
|||||||
|
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||||
|
import { KGCore } from '../../KGCore';
|
||||||
|
import { KGProject } from '../../KGProject';
|
||||||
|
import { KGAudioInterface } from '../../audio-interface/KGAudioInterface';
|
||||||
|
import { KGTrack } from '../../track/KGTrack';
|
||||||
|
import { UpdateTrackCommand } from './UpdateTrackCommand';
|
||||||
|
|
||||||
|
vi.mock('../../KGCore', () => ({
|
||||||
|
KGCore: {
|
||||||
|
instance: vi.fn(),
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock('../../audio-interface/KGAudioInterface', () => ({
|
||||||
|
KGAudioInterface: {
|
||||||
|
instance: vi.fn(),
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
describe('UpdateTrackCommand', () => {
|
||||||
|
let track: KGTrack;
|
||||||
|
let project: KGProject;
|
||||||
|
const mockCore = {
|
||||||
|
getCurrentProject: vi.fn(),
|
||||||
|
};
|
||||||
|
const mockAudioInterface = {
|
||||||
|
setTrackVolume: vi.fn(),
|
||||||
|
setTrackInstrument: vi.fn(),
|
||||||
|
setTrackMute: vi.fn(),
|
||||||
|
setTrackSolo: vi.fn(),
|
||||||
|
};
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.clearAllMocks();
|
||||||
|
track = new KGTrack('Track 1', 1);
|
||||||
|
project = new KGProject('Test', 32, 0, 125, undefined, undefined, undefined, undefined, undefined, 1, [track], 11);
|
||||||
|
mockCore.getCurrentProject.mockReturnValue(project);
|
||||||
|
vi.mocked(KGCore.instance).mockReturnValue(mockCore as unknown as KGCore);
|
||||||
|
vi.mocked(KGAudioInterface.instance).mockReturnValue(mockAudioInterface as unknown as KGAudioInterface);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('updates muted state and propagates to the audio interface', () => {
|
||||||
|
const command = new UpdateTrackCommand(1, { muted: true });
|
||||||
|
|
||||||
|
command.execute();
|
||||||
|
|
||||||
|
expect(track.getMuted()).toBe(true);
|
||||||
|
expect(mockAudioInterface.setTrackMute).toHaveBeenCalledWith('1', true);
|
||||||
|
expect(command.getChangedProperties()).toEqual(new Set(['muted']));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('updates solo state and propagates to the audio interface', () => {
|
||||||
|
const command = new UpdateTrackCommand(1, { solo: true });
|
||||||
|
|
||||||
|
command.execute();
|
||||||
|
|
||||||
|
expect(track.getSolo()).toBe(true);
|
||||||
|
expect(mockAudioInterface.setTrackSolo).toHaveBeenCalledWith('1', true);
|
||||||
|
expect(command.getChangedProperties()).toEqual(new Set(['solo']));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('restores muted and solo state on undo', () => {
|
||||||
|
track.setMuted(true);
|
||||||
|
track.setSolo(true);
|
||||||
|
const command = new UpdateTrackCommand(1, { muted: false, solo: false });
|
||||||
|
|
||||||
|
command.execute();
|
||||||
|
command.undo();
|
||||||
|
|
||||||
|
expect(track.getMuted()).toBe(true);
|
||||||
|
expect(track.getSolo()).toBe(true);
|
||||||
|
expect(mockAudioInterface.setTrackMute).toHaveBeenLastCalledWith('1', true);
|
||||||
|
expect(mockAudioInterface.setTrackSolo).toHaveBeenLastCalledWith('1', true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('treats unchanged mute and solo values as no-ops', () => {
|
||||||
|
const command = new UpdateTrackCommand(1, { muted: false, solo: false });
|
||||||
|
|
||||||
|
command.execute();
|
||||||
|
|
||||||
|
expect(track.getMuted()).toBe(false);
|
||||||
|
expect(track.getSolo()).toBe(false);
|
||||||
|
expect(mockAudioInterface.setTrackMute).not.toHaveBeenCalled();
|
||||||
|
expect(mockAudioInterface.setTrackSolo).not.toHaveBeenCalled();
|
||||||
|
expect(command.getChangedProperties()).toEqual(new Set());
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -12,6 +12,8 @@ export interface TrackUpdateProperties {
|
|||||||
instrument?: InstrumentType; // Only applies to MIDI tracks
|
instrument?: InstrumentType; // Only applies to MIDI tracks
|
||||||
type?: TrackType;
|
type?: TrackType;
|
||||||
volume?: number;
|
volume?: number;
|
||||||
|
muted?: boolean;
|
||||||
|
solo?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -47,6 +49,8 @@ export class UpdateTrackCommand extends KGCommand {
|
|||||||
name: this.targetTrack.getName(),
|
name: this.targetTrack.getName(),
|
||||||
type: this.targetTrack.getType(),
|
type: this.targetTrack.getType(),
|
||||||
volume: this.targetTrack.getVolume(),
|
volume: this.targetTrack.getVolume(),
|
||||||
|
muted: this.targetTrack.getMuted(),
|
||||||
|
solo: this.targetTrack.getSolo(),
|
||||||
};
|
};
|
||||||
|
|
||||||
// Store original instrument if it's a MIDI track
|
// Store original instrument if it's a MIDI track
|
||||||
@@ -103,6 +107,32 @@ export class UpdateTrackCommand extends KGCommand {
|
|||||||
updatedProperties.push(`volume: ${originalVolume} → ${newVolume}`);
|
updatedProperties.push(`volume: ${originalVolume} → ${newVolume}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (this.newProperties.muted !== undefined && this.newProperties.muted !== this.originalProperties.muted) {
|
||||||
|
const newMuted = this.newProperties.muted;
|
||||||
|
const originalMuted = this.originalProperties.muted;
|
||||||
|
|
||||||
|
this.targetTrack.setMuted(newMuted);
|
||||||
|
|
||||||
|
const audioInterface = KGAudioInterface.instance();
|
||||||
|
audioInterface.setTrackMute(this.trackId.toString(), newMuted);
|
||||||
|
|
||||||
|
this.changedProperties.add('muted');
|
||||||
|
updatedProperties.push(`muted: ${originalMuted} → ${newMuted}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.newProperties.solo !== undefined && this.newProperties.solo !== this.originalProperties.solo) {
|
||||||
|
const newSolo = this.newProperties.solo;
|
||||||
|
const originalSolo = this.originalProperties.solo;
|
||||||
|
|
||||||
|
this.targetTrack.setSolo(newSolo);
|
||||||
|
|
||||||
|
const audioInterface = KGAudioInterface.instance();
|
||||||
|
audioInterface.setTrackSolo(this.trackId.toString(), newSolo);
|
||||||
|
|
||||||
|
this.changedProperties.add('solo');
|
||||||
|
updatedProperties.push(`solo: ${originalSolo} → ${newSolo}`);
|
||||||
|
}
|
||||||
|
|
||||||
if (updatedProperties.length > 0) {
|
if (updatedProperties.length > 0) {
|
||||||
console.log(`Updated track ${this.trackId}: ${updatedProperties.join(', ')}`);
|
console.log(`Updated track ${this.trackId}: ${updatedProperties.join(', ')}`);
|
||||||
} else {
|
} else {
|
||||||
@@ -156,6 +186,24 @@ export class UpdateTrackCommand extends KGCommand {
|
|||||||
restoredProperties.push(`volume: ${this.originalProperties.volume}`);
|
restoredProperties.push(`volume: ${this.originalProperties.volume}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (this.changedProperties.has('muted') && this.originalProperties.muted !== undefined) {
|
||||||
|
this.targetTrack.setMuted(this.originalProperties.muted);
|
||||||
|
|
||||||
|
const audioInterface = KGAudioInterface.instance();
|
||||||
|
audioInterface.setTrackMute(this.trackId.toString(), this.originalProperties.muted);
|
||||||
|
|
||||||
|
restoredProperties.push(`muted: ${this.originalProperties.muted}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.changedProperties.has('solo') && this.originalProperties.solo !== undefined) {
|
||||||
|
this.targetTrack.setSolo(this.originalProperties.solo);
|
||||||
|
|
||||||
|
const audioInterface = KGAudioInterface.instance();
|
||||||
|
audioInterface.setTrackSolo(this.trackId.toString(), this.originalProperties.solo);
|
||||||
|
|
||||||
|
restoredProperties.push(`solo: ${this.originalProperties.solo}`);
|
||||||
|
}
|
||||||
|
|
||||||
console.log(`Restored track ${this.trackId}: ${restoredProperties.join(', ')}`);
|
console.log(`Restored track ${this.trackId}: ${restoredProperties.join(', ')}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -175,6 +223,12 @@ export class UpdateTrackCommand extends KGCommand {
|
|||||||
if (this.newProperties.volume !== undefined) {
|
if (this.newProperties.volume !== undefined) {
|
||||||
updatedProps.push('volume');
|
updatedProps.push('volume');
|
||||||
}
|
}
|
||||||
|
if (this.newProperties.muted !== undefined) {
|
||||||
|
updatedProps.push('muted');
|
||||||
|
}
|
||||||
|
if (this.newProperties.solo !== undefined) {
|
||||||
|
updatedProps.push('solo');
|
||||||
|
}
|
||||||
|
|
||||||
if (updatedProps.length === 1) {
|
if (updatedProps.length === 1) {
|
||||||
return `Update track "${trackName}" ${updatedProps[0]}`;
|
return `Update track "${trackName}" ${updatedProps[0]}`;
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
||||||
import { KGProjectStorage, DuplicateEntryError } from './KGProjectStorage';
|
import { KGProjectStorage, DuplicateEntryError } from './KGProjectStorage';
|
||||||
import { KGProject } from '../KGProject';
|
import { KGProject } from '../KGProject';
|
||||||
|
import { KGTrack } from '../track/KGTrack';
|
||||||
|
|
||||||
// --- OPFS mock infrastructure ---
|
// --- OPFS mock infrastructure ---
|
||||||
|
|
||||||
@@ -121,6 +122,22 @@ describe('KGProjectStorage', () => {
|
|||||||
expect(loaded!.getBpm()).toBe(120);
|
expect(loaded!.getBpm()).toBe(120);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('preserves track mute and solo state when saving and loading', async () => {
|
||||||
|
const track = new KGTrack('Track 1', 1);
|
||||||
|
track.setMuted(true);
|
||||||
|
track.setSolo(true);
|
||||||
|
const project = new KGProject('My Song', 16, 0, 120, undefined, undefined, undefined, undefined, undefined, 1, [track], 11);
|
||||||
|
|
||||||
|
await storage.save('My Song', project);
|
||||||
|
|
||||||
|
const loaded = await storage.load('My Song');
|
||||||
|
|
||||||
|
expect(loaded).not.toBeNull();
|
||||||
|
expect(loaded!.getTracks()).toHaveLength(1);
|
||||||
|
expect(loaded!.getTracks()[0].getMuted()).toBe(true);
|
||||||
|
expect(loaded!.getTracks()[0].getSolo()).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
it('creates meta.json and media/ directory on save', async () => {
|
it('creates meta.json and media/ directory on save', async () => {
|
||||||
const project = createTestProject('My Song');
|
const project = createTestProject('My Song');
|
||||||
await storage.save('My Song', project);
|
await storage.save('My Song', project);
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import { upgradeToV7 } from './upgradeToV7';
|
|||||||
import { upgradeToV8 } from './upgradeToV8';
|
import { upgradeToV8 } from './upgradeToV8';
|
||||||
import { upgradeToV9 } from './upgradeToV9';
|
import { upgradeToV9 } from './upgradeToV9';
|
||||||
import { upgradeToV10 } from './upgradeToV10';
|
import { upgradeToV10 } from './upgradeToV10';
|
||||||
|
import { upgradeToV11 } from './upgradeToV11';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 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.
|
||||||
@@ -68,6 +69,10 @@ export function upgradeProjectToLatest(project: KGProject): KGProject {
|
|||||||
workingProject = upgradeToV10(workingProject);
|
workingProject = upgradeToV10(workingProject);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
case 11: {
|
||||||
|
workingProject = upgradeToV11(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}`);
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ describe('upgradeToV10', () => {
|
|||||||
|
|
||||||
const upgraded = upgradeProjectToLatest(project);
|
const upgraded = upgradeProjectToLatest(project);
|
||||||
|
|
||||||
expect(upgraded.getProjectStructureVersion()).toBe(10);
|
expect(upgraded.getProjectStructureVersion()).toBe(11);
|
||||||
expect(upgraded.getTracks()[0].getVolumeAutomation()).toHaveLength(1);
|
expect(upgraded.getTracks()[0].getVolumeAutomation()).toHaveLength(1);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -0,0 +1,45 @@
|
|||||||
|
import { describe, expect, it } from 'vitest';
|
||||||
|
import { KGProject } from '../KGProject';
|
||||||
|
import { KGTrack } from '../track/KGTrack';
|
||||||
|
import { upgradeProjectToLatest } from './KGProjectUpgrader';
|
||||||
|
import { upgradeToV11 } from './upgradeToV11';
|
||||||
|
|
||||||
|
describe('upgradeToV11', () => {
|
||||||
|
it('initializes missing mute and solo flags on legacy tracks', () => {
|
||||||
|
const track = new KGTrack('Legacy Track', 1);
|
||||||
|
delete (track as unknown as { muted?: unknown }).muted;
|
||||||
|
delete (track as unknown as { solo?: unknown }).solo;
|
||||||
|
const project = new KGProject('Test', 32, 0, 125, undefined, undefined, undefined, undefined, undefined, 1, [track], 10);
|
||||||
|
|
||||||
|
upgradeToV11(project);
|
||||||
|
|
||||||
|
expect(track.getMuted()).toBe(false);
|
||||||
|
expect(track.getSolo()).toBe(false);
|
||||||
|
expect(project.getProjectStructureVersion()).toBe(11);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('preserves existing mute and solo flags', () => {
|
||||||
|
const track = new KGTrack('Legacy Track', 1);
|
||||||
|
track.setMuted(true);
|
||||||
|
track.setSolo(true);
|
||||||
|
const project = new KGProject('Test', 32, 0, 125, undefined, undefined, undefined, undefined, undefined, 1, [track], 10);
|
||||||
|
|
||||||
|
upgradeToV11(project);
|
||||||
|
|
||||||
|
expect(track.getMuted()).toBe(true);
|
||||||
|
expect(track.getSolo()).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('upgrades legacy projects through the main upgrader path', () => {
|
||||||
|
const track = new KGTrack('Legacy Track', 1);
|
||||||
|
delete (track as unknown as { muted?: unknown }).muted;
|
||||||
|
delete (track as unknown as { solo?: unknown }).solo;
|
||||||
|
const project = new KGProject('Test', 32, 0, 125, undefined, undefined, undefined, undefined, undefined, 1, [track], 10);
|
||||||
|
|
||||||
|
const upgraded = upgradeProjectToLatest(project);
|
||||||
|
|
||||||
|
expect(upgraded.getProjectStructureVersion()).toBe(11);
|
||||||
|
expect(upgraded.getTracks()[0].getMuted()).toBe(false);
|
||||||
|
expect(upgraded.getTracks()[0].getSolo()).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
import { KGProject } from '../KGProject';
|
||||||
|
|
||||||
|
export function upgradeToV11(project: KGProject): KGProject {
|
||||||
|
try {
|
||||||
|
for (const track of project.getTracks()) {
|
||||||
|
const muted = (track as unknown as { muted?: unknown }).muted;
|
||||||
|
if (typeof muted !== 'boolean') {
|
||||||
|
track.setMuted(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
const solo = (track as unknown as { solo?: unknown }).solo;
|
||||||
|
if (typeof solo !== 'boolean') {
|
||||||
|
track.setSolo(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
project.setProjectStructureVersion(11);
|
||||||
|
}
|
||||||
|
|
||||||
|
return project;
|
||||||
|
}
|
||||||
@@ -57,7 +57,7 @@ describe('upgradeToV8', () => {
|
|||||||
|
|
||||||
const upgraded = upgradeProjectToLatest(project);
|
const upgraded = upgradeProjectToLatest(project);
|
||||||
|
|
||||||
expect(upgraded.getProjectStructureVersion()).toBe(10);
|
expect(upgraded.getProjectStructureVersion()).toBe(11);
|
||||||
expect((upgraded.getTracks()[0].getRegions()[0] as KGMidiRegion).getPitchBends()).toEqual([]);
|
expect((upgraded.getTracks()[0].getRegions()[0] as KGMidiRegion).getPitchBends()).toEqual([]);
|
||||||
expect((upgraded.getTracks()[0].getRegions()[0] as KGMidiRegion).getControllerEventsByType()).toHaveLength(128);
|
expect((upgraded.getTracks()[0].getRegions()[0] as KGMidiRegion).getControllerEventsByType()).toHaveLength(128);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -38,6 +38,14 @@ export class KGTrack {
|
|||||||
@WithDefault(AUDIO_INTERFACE_CONSTANTS.DEFAULT_TRACK_VOLUME)
|
@WithDefault(AUDIO_INTERFACE_CONSTANTS.DEFAULT_TRACK_VOLUME)
|
||||||
protected volume: number = AUDIO_INTERFACE_CONSTANTS.DEFAULT_TRACK_VOLUME;
|
protected volume: number = AUDIO_INTERFACE_CONSTANTS.DEFAULT_TRACK_VOLUME;
|
||||||
|
|
||||||
|
@Expose()
|
||||||
|
@WithDefault(false)
|
||||||
|
protected muted: boolean = false;
|
||||||
|
|
||||||
|
@Expose()
|
||||||
|
@WithDefault(false)
|
||||||
|
protected solo: boolean = false;
|
||||||
|
|
||||||
@Expose()
|
@Expose()
|
||||||
@Type(() => KGRegion, {
|
@Type(() => KGRegion, {
|
||||||
discriminator: {
|
discriminator: {
|
||||||
@@ -91,6 +99,14 @@ export class KGTrack {
|
|||||||
return this.volume;
|
return this.volume;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public getMuted(): boolean {
|
||||||
|
return this.muted;
|
||||||
|
}
|
||||||
|
|
||||||
|
public getSolo(): boolean {
|
||||||
|
return this.solo;
|
||||||
|
}
|
||||||
|
|
||||||
// Setters
|
// Setters
|
||||||
public setName(name: string): void {
|
public setName(name: string): void {
|
||||||
this.name = name;
|
this.name = name;
|
||||||
@@ -120,6 +136,14 @@ export class KGTrack {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public setMuted(muted: boolean): void {
|
||||||
|
this.muted = muted;
|
||||||
|
}
|
||||||
|
|
||||||
|
public setSolo(solo: boolean): void {
|
||||||
|
this.solo = solo;
|
||||||
|
}
|
||||||
|
|
||||||
public setRegions(regions: KGRegion[]): void {
|
public setRegions(regions: KGRegion[]): void {
|
||||||
this.regions = regions;
|
this.regions = regions;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -873,6 +873,14 @@ export const useProjectStore = create<ProjectState>((set, get) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Reapply restored mute/solo state after all buses exist so solo logic can be
|
||||||
|
// computed against the full track set.
|
||||||
|
for (const track of tracks) {
|
||||||
|
const trackId = track.getId().toString();
|
||||||
|
audioInterface.setTrackMute(trackId, track.getMuted());
|
||||||
|
audioInterface.setTrackSolo(trackId, track.getSolo());
|
||||||
|
}
|
||||||
|
|
||||||
// Update CSS variables
|
// Update CSS variables
|
||||||
updateTimeSignatureCSS(timeSignature);
|
updateTimeSignatureCSS(timeSignature);
|
||||||
updateMaxBarsCSS(maxBars);
|
updateMaxBarsCSS(maxBars);
|
||||||
|
|||||||
Reference in New Issue
Block a user