feat: persist track mute/solo status

This commit is contained in:
Xiaohan-Tian
2026-05-17 13:06:47 -07:00
parent cbd66ab4f6
commit 9c197ecd08
13 changed files with 285 additions and 21 deletions
@@ -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());
});
});
+55 -1
View File
@@ -12,6 +12,8 @@ export interface TrackUpdateProperties {
instrument?: InstrumentType; // Only applies to MIDI tracks
type?: TrackType;
volume?: number;
muted?: boolean;
solo?: boolean;
}
/**
@@ -47,6 +49,8 @@ export class UpdateTrackCommand extends KGCommand {
name: this.targetTrack.getName(),
type: this.targetTrack.getType(),
volume: this.targetTrack.getVolume(),
muted: this.targetTrack.getMuted(),
solo: this.targetTrack.getSolo(),
};
// Store original instrument if it's a MIDI track
@@ -103,6 +107,32 @@ export class UpdateTrackCommand extends KGCommand {
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) {
console.log(`Updated track ${this.trackId}: ${updatedProperties.join(', ')}`);
} else {
@@ -156,6 +186,24 @@ export class UpdateTrackCommand extends KGCommand {
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(', ')}`);
}
@@ -175,6 +223,12 @@ export class UpdateTrackCommand extends KGCommand {
if (this.newProperties.volume !== undefined) {
updatedProps.push('volume');
}
if (this.newProperties.muted !== undefined) {
updatedProps.push('muted');
}
if (this.newProperties.solo !== undefined) {
updatedProps.push('solo');
}
if (updatedProps.length === 1) {
return `Update track "${trackName}" ${updatedProps[0]}`;
@@ -219,4 +273,4 @@ export class UpdateTrackCommand extends KGCommand {
public getChangedProperties(): Set<keyof TrackUpdateProperties> {
return new Set(this.changedProperties);
}
}
}