feat: add audio chord detection for spectrogram regions
This commit is contained in:
@@ -6,6 +6,7 @@ import { KGChordRegion } from '../../region/KGChordRegion';
|
||||
import { CreateChordRegionCommand } from './CreateChordRegionCommand';
|
||||
import { InsertChordRegionAtBeatCommand } from './InsertChordRegionAtBeatCommand';
|
||||
import { MoveGlobalRegionCommand } from './MoveGlobalRegionCommand';
|
||||
import { ReplaceChordRegionsInRangeCommand } from './ReplaceChordRegionsInRangeCommand';
|
||||
import { ResizeGlobalRegionCommand } from './ResizeGlobalRegionCommand';
|
||||
import { UpdateChordRegionCommand } from './UpdateChordRegionCommand';
|
||||
|
||||
@@ -104,4 +105,46 @@ describe('global chord region commands', () => {
|
||||
expect(chordTrack.getRegions()).toHaveLength(1);
|
||||
expect(region.getLength()).toBe(8);
|
||||
});
|
||||
|
||||
it('replaces only the requested chord span and restores the original layout on undo', () => {
|
||||
const chordTrack = getChordTrack();
|
||||
chordTrack.setRegions([
|
||||
new KGChordRegion('left', chordTrack.getId(), chordTrack.getTrackIndex(), 'C', 0, 4),
|
||||
new KGChordRegion('middle', chordTrack.getId(), chordTrack.getTrackIndex(), 'Am', 4, 4),
|
||||
new KGChordRegion('right', chordTrack.getId(), chordTrack.getTrackIndex(), 'F', 8, 4),
|
||||
]);
|
||||
|
||||
const command = new ReplaceChordRegionsInRangeCommand(2, 10, [
|
||||
{ startBeat: 2, length: 2, symbol: 'Dm' },
|
||||
{ startBeat: 4, length: 4, symbol: 'E' },
|
||||
{ startBeat: 8, length: 2, symbol: 'Am' },
|
||||
]);
|
||||
|
||||
command.execute();
|
||||
|
||||
const replacedRegions = getChordTrack().getRegions() as KGChordRegion[];
|
||||
expect(replacedRegions.map(region => ({
|
||||
symbol: region.getSymbol(),
|
||||
start: region.getStartFromBeat(),
|
||||
length: region.getLength(),
|
||||
}))).toEqual([
|
||||
{ symbol: 'C', start: 0, length: 2 },
|
||||
{ symbol: 'Dm', start: 2, length: 2 },
|
||||
{ symbol: 'E', start: 4, length: 4 },
|
||||
{ symbol: 'Am', start: 8, length: 2 },
|
||||
{ symbol: 'F', start: 10, length: 2 },
|
||||
]);
|
||||
|
||||
command.undo();
|
||||
const restoredRegions = getChordTrack().getRegions() as KGChordRegion[];
|
||||
expect(restoredRegions.map(region => ({
|
||||
symbol: region.getSymbol(),
|
||||
start: region.getStartFromBeat(),
|
||||
length: region.getLength(),
|
||||
}))).toEqual([
|
||||
{ symbol: 'C', start: 0, length: 4 },
|
||||
{ symbol: 'Am', start: 4, length: 4 },
|
||||
{ symbol: 'F', start: 8, length: 4 },
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,132 @@
|
||||
import { KGCommand } from '../KGCommand';
|
||||
import { KGCore } from '../../KGCore';
|
||||
import { GlobalTrackType } from '../../global-track';
|
||||
import { KGChordRegion } from '../../region/KGChordRegion';
|
||||
import { findGlobalTrackByType } from '../../../util/globalTrackUtil';
|
||||
import { generateUniqueId } from '../../../util/miscUtil';
|
||||
|
||||
export interface ChordRegionReplacementData {
|
||||
startBeat: number;
|
||||
length: number;
|
||||
symbol: string;
|
||||
}
|
||||
|
||||
function cloneChordRegion(region: KGChordRegion): KGChordRegion {
|
||||
return new KGChordRegion(
|
||||
region.getId(),
|
||||
region.getTrackId(),
|
||||
region.getTrackIndex(),
|
||||
region.getSymbol(),
|
||||
region.getStartFromBeat(),
|
||||
region.getLength(),
|
||||
);
|
||||
}
|
||||
|
||||
function cloneChordRegions(regions: KGChordRegion[]): KGChordRegion[] {
|
||||
return regions.map(cloneChordRegion);
|
||||
}
|
||||
|
||||
export class ReplaceChordRegionsInRangeCommand extends KGCommand {
|
||||
private readonly rangeStartBeat: number;
|
||||
private readonly rangeEndBeat: number;
|
||||
private readonly replacements: ChordRegionReplacementData[];
|
||||
private originalRegions: KGChordRegion[] | null = null;
|
||||
private nextRegions: KGChordRegion[] | null = null;
|
||||
|
||||
constructor(rangeStartBeat: number, rangeEndBeat: number, replacements: ChordRegionReplacementData[]) {
|
||||
super();
|
||||
this.rangeStartBeat = Math.max(0, rangeStartBeat);
|
||||
this.rangeEndBeat = Math.max(this.rangeStartBeat, rangeEndBeat);
|
||||
this.replacements = replacements.map(replacement => ({
|
||||
startBeat: replacement.startBeat,
|
||||
length: replacement.length,
|
||||
symbol: replacement.symbol,
|
||||
}));
|
||||
}
|
||||
|
||||
execute(): void {
|
||||
const project = KGCore.instance().getCurrentProject();
|
||||
const chordTrack = findGlobalTrackByType(project, GlobalTrackType.Chord);
|
||||
if (!chordTrack) {
|
||||
throw new Error('Chord global track not found');
|
||||
}
|
||||
|
||||
if (this.nextRegions) {
|
||||
chordTrack.setRegions(cloneChordRegions(this.nextRegions));
|
||||
return;
|
||||
}
|
||||
|
||||
const currentRegions = chordTrack.getRegions()
|
||||
.filter((region): region is KGChordRegion => region instanceof KGChordRegion)
|
||||
.sort((left, right) => left.getStartFromBeat() - right.getStartFromBeat());
|
||||
|
||||
this.originalRegions = cloneChordRegions(currentRegions);
|
||||
|
||||
const preservedRegions: KGChordRegion[] = [];
|
||||
for (const region of currentRegions) {
|
||||
const regionStart = region.getStartFromBeat();
|
||||
const regionEnd = regionStart + region.getLength();
|
||||
|
||||
if (regionEnd <= this.rangeStartBeat || regionStart >= this.rangeEndBeat) {
|
||||
preservedRegions.push(cloneChordRegion(region));
|
||||
continue;
|
||||
}
|
||||
|
||||
if (regionStart < this.rangeStartBeat) {
|
||||
preservedRegions.push(new KGChordRegion(
|
||||
region.getId(),
|
||||
region.getTrackId(),
|
||||
region.getTrackIndex(),
|
||||
region.getSymbol(),
|
||||
regionStart,
|
||||
this.rangeStartBeat - regionStart,
|
||||
));
|
||||
}
|
||||
|
||||
if (regionEnd > this.rangeEndBeat) {
|
||||
preservedRegions.push(new KGChordRegion(
|
||||
generateUniqueId('KGChordRegion'),
|
||||
region.getTrackId(),
|
||||
region.getTrackIndex(),
|
||||
region.getSymbol(),
|
||||
this.rangeEndBeat,
|
||||
regionEnd - this.rangeEndBeat,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
const replacementRegions = this.replacements
|
||||
.filter(replacement => replacement.length > 0 && replacement.symbol.trim() !== '')
|
||||
.map(replacement => new KGChordRegion(
|
||||
generateUniqueId('KGChordRegion'),
|
||||
chordTrack.getId(),
|
||||
chordTrack.getTrackIndex(),
|
||||
replacement.symbol,
|
||||
replacement.startBeat,
|
||||
replacement.length,
|
||||
));
|
||||
|
||||
this.nextRegions = [...preservedRegions, ...replacementRegions]
|
||||
.sort((left, right) => left.getStartFromBeat() - right.getStartFromBeat());
|
||||
|
||||
chordTrack.setRegions(cloneChordRegions(this.nextRegions));
|
||||
}
|
||||
|
||||
undo(): void {
|
||||
if (!this.originalRegions) {
|
||||
throw new Error('Cannot undo chord replacement without original regions');
|
||||
}
|
||||
|
||||
const project = KGCore.instance().getCurrentProject();
|
||||
const chordTrack = findGlobalTrackByType(project, GlobalTrackType.Chord);
|
||||
if (!chordTrack) {
|
||||
throw new Error('Chord global track not found during undo');
|
||||
}
|
||||
|
||||
chordTrack.setRegions(cloneChordRegions(this.originalRegions));
|
||||
}
|
||||
|
||||
getDescription(): string {
|
||||
return 'Replace chord regions in range';
|
||||
}
|
||||
}
|
||||
@@ -39,6 +39,10 @@ export { MergeMidiRegionsCommand } from './region/MergeMidiRegionsCommand';
|
||||
export { CreateGlobalMarkerRegionCommand } from './global-region/CreateGlobalMarkerRegionCommand';
|
||||
export { CreateChordRegionCommand } from './global-region/CreateChordRegionCommand';
|
||||
export { InsertChordRegionAtBeatCommand } from './global-region/InsertChordRegionAtBeatCommand';
|
||||
export {
|
||||
ReplaceChordRegionsInRangeCommand,
|
||||
type ChordRegionReplacementData,
|
||||
} from './global-region/ReplaceChordRegionsInRangeCommand';
|
||||
export { CreateKeySignatureRegionCommand } from './global-region/CreateKeySignatureRegionCommand';
|
||||
export { CreateTempoRegionCommand } from './global-region/CreateTempoRegionCommand';
|
||||
export { MoveGlobalRegionCommand } from './global-region/MoveGlobalRegionCommand';
|
||||
|
||||
Reference in New Issue
Block a user