initial public release.
This commit is contained in:
@@ -0,0 +1,99 @@
|
||||
import { KGCommand } from '../KGCommand';
|
||||
import { KGCore } from '../../KGCore';
|
||||
import { KGMidiTrack, type InstrumentType } from '../../track/KGMidiTrack';
|
||||
import { KGAudioInterface } from '../../audio-interface/KGAudioInterface';
|
||||
|
||||
/**
|
||||
* Command to add a new track to the project
|
||||
* Handles both the core model update and audio interface setup
|
||||
*/
|
||||
export class AddTrackCommand extends KGCommand {
|
||||
private trackId: number;
|
||||
private trackName: string;
|
||||
private instrument: InstrumentType;
|
||||
private trackIndex: number;
|
||||
private createdTrack: KGMidiTrack | null = null;
|
||||
|
||||
constructor(trackId?: number, trackName?: string, instrument: InstrumentType = 'acoustic_grand_piano') {
|
||||
super();
|
||||
|
||||
// If trackId not provided, calculate it
|
||||
if (trackId === undefined) {
|
||||
const currentProject = KGCore.instance().getCurrentProject();
|
||||
const tracks = currentProject.getTracks();
|
||||
this.trackId = tracks.length > 0
|
||||
? Math.max(...tracks.map(track => track.getId())) + 1
|
||||
: 1;
|
||||
} else {
|
||||
this.trackId = trackId;
|
||||
}
|
||||
|
||||
this.trackName = trackName || `Track ${this.trackId}`;
|
||||
this.instrument = instrument;
|
||||
|
||||
// Track index will be set during execution
|
||||
this.trackIndex = 0;
|
||||
}
|
||||
|
||||
execute(): void {
|
||||
const core = KGCore.instance();
|
||||
const currentProject = core.getCurrentProject();
|
||||
const tracks = currentProject.getTracks();
|
||||
|
||||
// Set the track index to be the last index in the array
|
||||
this.trackIndex = tracks.length;
|
||||
|
||||
// Create a new MIDI track
|
||||
this.createdTrack = new KGMidiTrack(this.trackName, this.trackId);
|
||||
this.createdTrack.setTrackIndex(this.trackIndex);
|
||||
this.createdTrack.setInstrument(this.instrument);
|
||||
|
||||
// Update the core model
|
||||
const updatedTracks = [...tracks, this.createdTrack];
|
||||
currentProject.setTracks(updatedTracks);
|
||||
|
||||
// Create audio synth for the new track (initialize with track volume)
|
||||
const audioInterface = KGAudioInterface.instance();
|
||||
audioInterface.createTrackSynth(this.trackId.toString(), this.instrument);
|
||||
|
||||
console.log(`Added track ${this.trackId} with ${this.instrument} instrument`);
|
||||
}
|
||||
|
||||
undo(): void {
|
||||
if (!this.createdTrack) {
|
||||
throw new Error('Cannot undo: no track was created');
|
||||
}
|
||||
|
||||
const core = KGCore.instance();
|
||||
const currentProject = core.getCurrentProject();
|
||||
const tracks = currentProject.getTracks();
|
||||
|
||||
// Remove the track from the core model
|
||||
const updatedTracks = tracks.filter(track => track.getId() !== this.trackId);
|
||||
currentProject.setTracks(updatedTracks);
|
||||
|
||||
// Remove audio synth for the track
|
||||
const audioInterface = KGAudioInterface.instance();
|
||||
audioInterface.removeTrackSynth(this.trackId.toString());
|
||||
|
||||
console.log(`Removed track ${this.trackId} and its audio synth`);
|
||||
}
|
||||
|
||||
getDescription(): string {
|
||||
return `Add track "${this.trackName}"`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the ID of the track that was/will be created
|
||||
*/
|
||||
public getTrackId(): number {
|
||||
return this.trackId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the created track instance (only available after execute)
|
||||
*/
|
||||
public getCreatedTrack(): KGMidiTrack | null {
|
||||
return this.createdTrack;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
import { KGCommand } from '../KGCommand';
|
||||
import { KGCore } from '../../KGCore';
|
||||
import { KGTrack } from '../../track/KGTrack';
|
||||
import { KGMidiTrack, type InstrumentType } from '../../track/KGMidiTrack';
|
||||
import { KGAudioInterface } from '../../audio-interface/KGAudioInterface';
|
||||
|
||||
/**
|
||||
* Command to remove a track from the project
|
||||
* Handles both the core model update and audio interface cleanup
|
||||
*/
|
||||
export class RemoveTrackCommand extends KGCommand {
|
||||
private trackId: number;
|
||||
private removedTrack: KGTrack | null = null;
|
||||
private originalTrackIndex: number = 0;
|
||||
private originalInstrument: InstrumentType = 'acoustic_grand_piano';
|
||||
|
||||
constructor(trackId: number) {
|
||||
super();
|
||||
this.trackId = trackId;
|
||||
}
|
||||
|
||||
execute(): void {
|
||||
const core = KGCore.instance();
|
||||
const currentProject = core.getCurrentProject();
|
||||
const tracks = currentProject.getTracks();
|
||||
|
||||
// Find the track to remove
|
||||
const trackToRemove = tracks.find(track => track.getId() === this.trackId);
|
||||
if (!trackToRemove) {
|
||||
throw new Error(`Track with ID ${this.trackId} not found`);
|
||||
}
|
||||
|
||||
// Store the track data for undo
|
||||
this.removedTrack = trackToRemove;
|
||||
this.originalTrackIndex = trackToRemove.getTrackIndex();
|
||||
|
||||
// Store instrument if it's a MIDI track
|
||||
if (trackToRemove.constructor.name === 'KGMidiTrack' && 'getInstrument' in trackToRemove) {
|
||||
this.originalInstrument = (trackToRemove as KGMidiTrack).getInstrument();
|
||||
}
|
||||
|
||||
// Remove audio synth for the track
|
||||
const audioInterface = KGAudioInterface.instance();
|
||||
audioInterface.removeTrackSynth(this.trackId.toString());
|
||||
|
||||
// Remove the track from the core model
|
||||
const updatedTracks = tracks.filter(track => track.getId() !== this.trackId);
|
||||
|
||||
// Update track indices to match new array positions
|
||||
updatedTracks.forEach((track, index) => {
|
||||
track.setTrackIndex(index);
|
||||
});
|
||||
|
||||
currentProject.setTracks(updatedTracks);
|
||||
|
||||
console.log(`Removed track ${this.trackId} and its audio synth`);
|
||||
}
|
||||
|
||||
undo(): void {
|
||||
if (!this.removedTrack) {
|
||||
throw new Error('Cannot undo: no track was removed');
|
||||
}
|
||||
|
||||
const core = KGCore.instance();
|
||||
const currentProject = core.getCurrentProject();
|
||||
const tracks = currentProject.getTracks();
|
||||
|
||||
// Restore the track to its original position
|
||||
const updatedTracks = [...tracks];
|
||||
|
||||
// Insert the track at the correct position
|
||||
if (this.originalTrackIndex >= updatedTracks.length) {
|
||||
// Add to the end
|
||||
updatedTracks.push(this.removedTrack);
|
||||
} else {
|
||||
// Insert at the original position
|
||||
updatedTracks.splice(this.originalTrackIndex, 0, this.removedTrack);
|
||||
}
|
||||
|
||||
// Update track indices to match array positions
|
||||
updatedTracks.forEach((track, index) => {
|
||||
track.setTrackIndex(index);
|
||||
});
|
||||
|
||||
// Update the core model
|
||||
currentProject.setTracks(updatedTracks);
|
||||
|
||||
// Recreate audio synth for the track
|
||||
const audioInterface = KGAudioInterface.instance();
|
||||
audioInterface.createTrackSynth(this.trackId.toString(), this.originalInstrument);
|
||||
|
||||
console.log(`Restored track ${this.trackId} with ${this.originalInstrument} instrument`);
|
||||
}
|
||||
|
||||
getDescription(): string {
|
||||
const trackName = this.removedTrack ? this.removedTrack.getName() : `Track ${this.trackId}`;
|
||||
return `Remove track "${trackName}"`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the ID of the track that was/will be removed
|
||||
*/
|
||||
public getTrackId(): number {
|
||||
return this.trackId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the removed track instance (only available after execute)
|
||||
*/
|
||||
public getRemovedTrack(): KGTrack | null {
|
||||
return this.removedTrack;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
import { KGCommand } from '../KGCommand';
|
||||
import { KGCore } from '../../KGCore';
|
||||
import { KGTrack } from '../../track/KGTrack';
|
||||
|
||||
/**
|
||||
* Command to reorder tracks in the project
|
||||
* Handles moving a track from one position to another with undo support
|
||||
*/
|
||||
export class ReorderTracksCommand extends KGCommand {
|
||||
private sourceIndex: number;
|
||||
private destinationIndex: number;
|
||||
private originalTrackOrder: KGTrack[] = [];
|
||||
|
||||
constructor(sourceIndex: number, destinationIndex: number) {
|
||||
super();
|
||||
this.sourceIndex = sourceIndex;
|
||||
this.destinationIndex = destinationIndex;
|
||||
}
|
||||
|
||||
execute(): void {
|
||||
const core = KGCore.instance();
|
||||
const currentProject = core.getCurrentProject();
|
||||
const tracks = currentProject.getTracks();
|
||||
|
||||
// Store original track order for undo
|
||||
this.originalTrackOrder = [...tracks];
|
||||
|
||||
// Validate indices
|
||||
if (this.sourceIndex < 0 || this.sourceIndex >= tracks.length) {
|
||||
throw new Error(`Invalid source index: ${this.sourceIndex}`);
|
||||
}
|
||||
if (this.destinationIndex < 0 || this.destinationIndex >= tracks.length) {
|
||||
throw new Error(`Invalid destination index: ${this.destinationIndex}`);
|
||||
}
|
||||
|
||||
// If source and destination are the same, do nothing
|
||||
if (this.sourceIndex === this.destinationIndex) {
|
||||
console.log('Source and destination indices are the same, no reordering needed');
|
||||
return;
|
||||
}
|
||||
|
||||
// Create a copy of the tracks array
|
||||
const updatedTracks = [...tracks];
|
||||
|
||||
// Remove the track from the source index
|
||||
const [movedTrack] = updatedTracks.splice(this.sourceIndex, 1);
|
||||
|
||||
// Insert the track at the destination index
|
||||
updatedTracks.splice(this.destinationIndex, 0, movedTrack);
|
||||
|
||||
// Update trackIndex for all tracks to match their array indices
|
||||
updatedTracks.forEach((track, index) => {
|
||||
track.setTrackIndex(index);
|
||||
});
|
||||
|
||||
// Update the core model
|
||||
currentProject.setTracks(updatedTracks);
|
||||
|
||||
const movedTrackName = movedTrack.getName();
|
||||
console.log(`Reordered track "${movedTrackName}" from index ${this.sourceIndex} to ${this.destinationIndex}`);
|
||||
}
|
||||
|
||||
undo(): void {
|
||||
if (this.originalTrackOrder.length === 0) {
|
||||
throw new Error('Cannot undo: no original track order stored');
|
||||
}
|
||||
|
||||
const core = KGCore.instance();
|
||||
const currentProject = core.getCurrentProject();
|
||||
|
||||
// Restore the original track order
|
||||
const restoredTracks = [...this.originalTrackOrder];
|
||||
|
||||
// Update trackIndex for all tracks to match their array indices
|
||||
restoredTracks.forEach((track, index) => {
|
||||
track.setTrackIndex(index);
|
||||
});
|
||||
|
||||
// Update the core model
|
||||
currentProject.setTracks(restoredTracks);
|
||||
|
||||
console.log(`Restored original track order (undoing reorder from ${this.sourceIndex} to ${this.destinationIndex})`);
|
||||
}
|
||||
|
||||
getDescription(): string {
|
||||
if (this.originalTrackOrder.length > 0) {
|
||||
const movedTrack = this.originalTrackOrder[this.sourceIndex];
|
||||
const trackName = movedTrack ? movedTrack.getName() : `Track ${this.sourceIndex}`;
|
||||
return `Reorder track "${trackName}" from position ${this.sourceIndex + 1} to ${this.destinationIndex + 1}`;
|
||||
}
|
||||
return `Reorder track from position ${this.sourceIndex + 1} to ${this.destinationIndex + 1}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the source index of the reorder operation
|
||||
*/
|
||||
public getSourceIndex(): number {
|
||||
return this.sourceIndex;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the destination index of the reorder operation
|
||||
*/
|
||||
public getDestinationIndex(): number {
|
||||
return this.destinationIndex;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the original track order (only available after execute)
|
||||
*/
|
||||
public getOriginalTrackOrder(): KGTrack[] {
|
||||
return this.originalTrackOrder;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,222 @@
|
||||
import { KGCommand } from '../KGCommand';
|
||||
import { KGCore } from '../../KGCore';
|
||||
import { KGTrack, TrackType } from '../../track/KGTrack';
|
||||
import { KGMidiTrack, type InstrumentType } from '../../track/KGMidiTrack';
|
||||
import { KGAudioInterface } from '../../audio-interface/KGAudioInterface';
|
||||
|
||||
/**
|
||||
* Interface defining properties that can be updated on a track
|
||||
*/
|
||||
export interface TrackUpdateProperties {
|
||||
name?: string;
|
||||
instrument?: InstrumentType; // Only applies to MIDI tracks
|
||||
type?: TrackType;
|
||||
volume?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Command to update track properties
|
||||
* Handles updating track name, instrument, and type with undo support
|
||||
*/
|
||||
export class UpdateTrackCommand extends KGCommand {
|
||||
private trackId: number;
|
||||
private newProperties: TrackUpdateProperties;
|
||||
private originalProperties: TrackUpdateProperties = {};
|
||||
private targetTrack: KGTrack | null = null;
|
||||
private changedProperties: Set<keyof TrackUpdateProperties> = new Set();
|
||||
|
||||
constructor(trackId: number, properties: TrackUpdateProperties) {
|
||||
super();
|
||||
this.trackId = trackId;
|
||||
this.newProperties = properties;
|
||||
}
|
||||
|
||||
execute(): void {
|
||||
const core = KGCore.instance();
|
||||
const currentProject = core.getCurrentProject();
|
||||
const tracks = currentProject.getTracks();
|
||||
|
||||
// Find the target track
|
||||
this.targetTrack = tracks.find(track => track.getId() === this.trackId) || null;
|
||||
if (!this.targetTrack) {
|
||||
throw new Error(`Track with ID ${this.trackId} not found`);
|
||||
}
|
||||
|
||||
// Store original properties for undo
|
||||
this.originalProperties = {
|
||||
name: this.targetTrack.getName(),
|
||||
type: this.targetTrack.getType(),
|
||||
volume: this.targetTrack.getVolume(),
|
||||
};
|
||||
|
||||
// Store original instrument if it's a MIDI track
|
||||
if (this.targetTrack instanceof KGMidiTrack) {
|
||||
this.originalProperties.instrument = this.targetTrack.getInstrument();
|
||||
}
|
||||
|
||||
// Apply updates and track what actually changes
|
||||
const updatedProperties: string[] = [];
|
||||
|
||||
// Update name
|
||||
if (this.newProperties.name !== undefined && this.newProperties.name !== this.originalProperties.name) {
|
||||
this.targetTrack.setName(this.newProperties.name);
|
||||
this.changedProperties.add('name');
|
||||
updatedProperties.push(`name: "${this.originalProperties.name}" → "${this.newProperties.name}"`);
|
||||
}
|
||||
|
||||
// Update type
|
||||
if (this.newProperties.type !== undefined && this.newProperties.type !== this.originalProperties.type) {
|
||||
this.targetTrack.setType(this.newProperties.type);
|
||||
this.changedProperties.add('type');
|
||||
updatedProperties.push(`type: ${this.originalProperties.type} → ${this.newProperties.type}`);
|
||||
}
|
||||
|
||||
// Update instrument (only for MIDI tracks)
|
||||
if (this.newProperties.instrument !== undefined && this.targetTrack instanceof KGMidiTrack) {
|
||||
const originalInstrument = this.originalProperties.instrument;
|
||||
if (this.newProperties.instrument !== originalInstrument) {
|
||||
// Update the track model
|
||||
this.targetTrack.setInstrument(this.newProperties.instrument);
|
||||
|
||||
// Update the audio interface
|
||||
const audioInterface = KGAudioInterface.instance();
|
||||
audioInterface.setTrackInstrument(this.trackId.toString(), this.newProperties.instrument);
|
||||
|
||||
this.changedProperties.add('instrument');
|
||||
updatedProperties.push(`instrument: ${originalInstrument} → ${this.newProperties.instrument}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Update volume
|
||||
if (this.newProperties.volume !== undefined && this.newProperties.volume !== this.originalProperties.volume) {
|
||||
const newVolume = this.newProperties.volume;
|
||||
const originalVolume = this.originalProperties.volume;
|
||||
|
||||
// Update the track model
|
||||
this.targetTrack.setVolume(newVolume);
|
||||
|
||||
// Update the audio interface
|
||||
const audioInterface = KGAudioInterface.instance();
|
||||
audioInterface.setTrackVolume(this.trackId.toString(), newVolume);
|
||||
|
||||
this.changedProperties.add('volume');
|
||||
updatedProperties.push(`volume: ${originalVolume} → ${newVolume}`);
|
||||
}
|
||||
|
||||
if (updatedProperties.length > 0) {
|
||||
console.log(`Updated track ${this.trackId}: ${updatedProperties.join(', ')}`);
|
||||
} else {
|
||||
console.log(`No changes applied to track ${this.trackId}`);
|
||||
}
|
||||
}
|
||||
|
||||
undo(): void {
|
||||
if (!this.targetTrack) {
|
||||
throw new Error('Cannot undo: no track was updated');
|
||||
}
|
||||
|
||||
// Only restore properties that were actually changed
|
||||
const restoredProperties: string[] = [];
|
||||
|
||||
// Restore name (only if it was changed)
|
||||
if (this.changedProperties.has('name') && this.originalProperties.name !== undefined) {
|
||||
this.targetTrack.setName(this.originalProperties.name);
|
||||
restoredProperties.push(`name: "${this.originalProperties.name}"`);
|
||||
}
|
||||
|
||||
// Restore type (only if it was changed)
|
||||
if (this.changedProperties.has('type') && this.originalProperties.type !== undefined) {
|
||||
this.targetTrack.setType(this.originalProperties.type);
|
||||
restoredProperties.push(`type: ${this.originalProperties.type}`);
|
||||
}
|
||||
|
||||
// Restore instrument (only if it was changed and track is MIDI)
|
||||
if (this.changedProperties.has('instrument') &&
|
||||
this.originalProperties.instrument !== undefined &&
|
||||
this.targetTrack instanceof KGMidiTrack) {
|
||||
// Update the track model
|
||||
this.targetTrack.setInstrument(this.originalProperties.instrument);
|
||||
|
||||
// Update the audio interface
|
||||
const audioInterface = KGAudioInterface.instance();
|
||||
audioInterface.setTrackInstrument(this.trackId.toString(), this.originalProperties.instrument);
|
||||
|
||||
restoredProperties.push(`instrument: ${this.originalProperties.instrument}`);
|
||||
}
|
||||
|
||||
// Restore volume (only if it was changed)
|
||||
if (this.changedProperties.has('volume') && this.originalProperties.volume !== undefined) {
|
||||
// Update the track model
|
||||
this.targetTrack.setVolume(this.originalProperties.volume);
|
||||
|
||||
// Update the audio interface
|
||||
const audioInterface = KGAudioInterface.instance();
|
||||
audioInterface.setTrackVolume(this.trackId.toString(), this.originalProperties.volume);
|
||||
|
||||
restoredProperties.push(`volume: ${this.originalProperties.volume}`);
|
||||
}
|
||||
|
||||
console.log(`Restored track ${this.trackId}: ${restoredProperties.join(', ')}`);
|
||||
}
|
||||
|
||||
getDescription(): string {
|
||||
const trackName = this.originalProperties.name || `Track ${this.trackId}`;
|
||||
const updatedProps: string[] = [];
|
||||
|
||||
if (this.newProperties.name !== undefined) {
|
||||
updatedProps.push('name');
|
||||
}
|
||||
if (this.newProperties.instrument !== undefined) {
|
||||
updatedProps.push('instrument');
|
||||
}
|
||||
if (this.newProperties.type !== undefined) {
|
||||
updatedProps.push('type');
|
||||
}
|
||||
if (this.newProperties.volume !== undefined) {
|
||||
updatedProps.push('volume');
|
||||
}
|
||||
|
||||
if (updatedProps.length === 1) {
|
||||
return `Update track "${trackName}" ${updatedProps[0]}`;
|
||||
} else if (updatedProps.length > 1) {
|
||||
return `Update track "${trackName}" properties`;
|
||||
}
|
||||
|
||||
return `Update track "${trackName}"`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the ID of the track being updated
|
||||
*/
|
||||
public getTrackId(): number {
|
||||
return this.trackId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the new properties being applied
|
||||
*/
|
||||
public getNewProperties(): TrackUpdateProperties {
|
||||
return this.newProperties;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the original properties (only available after execute)
|
||||
*/
|
||||
public getOriginalProperties(): TrackUpdateProperties {
|
||||
return this.originalProperties;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the target track instance (only available after execute)
|
||||
*/
|
||||
public getTargetTrack(): KGTrack | null {
|
||||
return this.targetTrack;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the properties that were actually changed (only available after execute)
|
||||
*/
|
||||
public getChangedProperties(): Set<keyof TrackUpdateProperties> {
|
||||
return new Set(this.changedProperties);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user