initial public release.

This commit is contained in:
Xiaohan-Tian
2025-08-11 18:37:21 -07:00
commit de51967b49
186 changed files with 32322 additions and 0 deletions
@@ -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);
}
}