feat: add read_chord_progression tool for the Agent

This commit is contained in:
Xiaohan-Tian
2026-05-26 17:56:55 -07:00
parent 0f1bad4394
commit 395032390a
6 changed files with 507 additions and 7 deletions
@@ -0,0 +1,111 @@
import { beforeEach, describe, expect, it, vi } from 'vitest';
import { ReadChordProgressionTool } from './ReadChordProgressionTool';
import { KGProject } from '../../core/KGProject';
import { KGMidiTrack } from '../../core/track/KGMidiTrack';
import { KGMidiRegion } from '../../core/region/KGMidiRegion';
import { KGChordRegion } from '../../core/region/KGChordRegion';
import { findGlobalTrackByType } from '../../util/globalTrackUtil';
import { GlobalTrackType } from '../../core/global-track';
import { KGCore } from '../../core/KGCore';
const storeState = {
activeRegionId: null as string | null,
};
vi.mock('../../stores/projectStore', () => ({
useProjectStore: {
getState: () => storeState,
},
}));
function buildProjectWithRegionAndOptionalChords(chords: string[] = []): {
project: KGProject;
midiRegion: KGMidiRegion;
} {
const project = new KGProject('tool-test', 8, 0, 120, { numerator: 4, denominator: 4 }, 'C major');
const midiTrack = new KGMidiTrack('Melody', 1);
const midiRegion = new KGMidiRegion('midi-region-1', '1', 0, 'Melody Region', 0, 32);
midiTrack.addRegion(midiRegion);
project.setTracks([midiTrack]);
const chordTrack = findGlobalTrackByType(project, GlobalTrackType.Chord);
expect(chordTrack).not.toBeNull();
chords.forEach((symbol, index) => {
chordTrack!.addRegion(new KGChordRegion(`chord-${index}`, chordTrack!.getId(), chordTrack!.getTrackIndex(), symbol, index * 4, 4));
});
return { project, midiRegion };
}
describe('ReadChordProgressionTool', () => {
beforeEach(() => {
storeState.activeRegionId = null;
vi.restoreAllMocks();
});
it('reads chord progression from the active MIDI region', async () => {
const { project, midiRegion } = buildProjectWithRegionAndOptionalChords(['Am', 'F', 'Dm', 'E7', 'Am', 'C', 'Dm', 'E7']);
storeState.activeRegionId = midiRegion.getId();
vi.spyOn(KGCore, 'instance').mockReturnValue({
getCurrentProject: () => project,
getSelectedItems: () => [],
} as unknown as KGCore);
const tool = new ReadChordProgressionTool();
const result = await tool.execute({});
expect(result.success).toBe(true);
expect(result.result).toContain('Chord-symbol representation:');
expect(result.result).toContain('[Am]4 | [F]4 | [Dm]4 | [E7]4 | [Am]4 | [C]4 | [Dm]4 | [E7]4 |');
expect(result.result).toContain('[A, C E]4 | [F, A, C]4 | [D F A]4 | [E ^G B d]4 | [A, C E]4 | [C E G]4 | [D F A]4 | [E ^G B d]4 |');
});
it('falls back to the selected MIDI region when no active region exists', async () => {
const { project, midiRegion } = buildProjectWithRegionAndOptionalChords(['Am']);
vi.spyOn(KGCore, 'instance').mockReturnValue({
getCurrentProject: () => project,
getSelectedItems: () => [midiRegion],
} as unknown as KGCore);
const tool = new ReadChordProgressionTool();
const result = await tool.execute({});
expect(result.success).toBe(true);
expect(result.result).toContain('[Am]4 |');
});
it('returns guidance when no chord progression is defined for the region range', async () => {
const { project, midiRegion } = buildProjectWithRegionAndOptionalChords();
storeState.activeRegionId = midiRegion.getId();
vi.spyOn(KGCore, 'instance').mockReturnValue({
getCurrentProject: () => project,
getSelectedItems: () => [],
} as unknown as KGCore);
const tool = new ReadChordProgressionTool();
const result = await tool.execute({});
expect(result.success).toBe(true);
expect(result.result).toContain('No chord progression is defined for the selected MIDI region range.');
expect(result.result).toContain('read_music');
});
it('returns a clear error when no active or selected MIDI region exists', async () => {
const { project } = buildProjectWithRegionAndOptionalChords(['Am']);
vi.spyOn(KGCore, 'instance').mockReturnValue({
getCurrentProject: () => project,
getSelectedItems: () => [],
} as unknown as KGCore);
const tool = new ReadChordProgressionTool();
const result = await tool.execute({});
expect(result.success).toBe(false);
expect(result.result).toContain('No active or selected MIDI region found');
});
});
@@ -0,0 +1,60 @@
import { BaseTool } from './BaseTool';
import type { ToolResult, ToolParameter } from './BaseTool';
import { KGMidiRegion } from '../../core/region/KGMidiRegion';
import { useProjectStore } from '../../stores/projectStore';
import { KGCore } from '../../core/KGCore';
import { convertBeatRangeChordProgressionToABCNotation } from '../../util/abcNotationUtil';
/**
* Tool for reading user-defined chord progression content from the global chord track.
*/
export class ReadChordProgressionTool extends BaseTool {
readonly name = 'read_chord_progression';
readonly description = 'Read the user-defined chord progression for the currently active or selected MIDI region. The output has two representations of the same progression: first symbolic chord names such as Em7b5, then note-based ABC chord tokens. Chord progression data comes only from chord regions the user defined on the global chord track, so it may be empty. If no chord progression is defined for this range, read the notes directly with read_music.';
readonly parameters: Record<string, ToolParameter> = {};
async execute(_params: Record<string, unknown>): Promise<ToolResult> {
try {
const targetRegion = this.findTargetRegion();
if (!targetRegion) {
return this.createErrorResult(
'No active or selected MIDI region found. Please open the piano roll with a region or select a MIDI region first.'
);
}
const project = this.getCurrentProject();
const startBeat = targetRegion.getStartFromBeat();
const endBeat = startBeat + targetRegion.getLength();
const result = convertBeatRangeChordProgressionToABCNotation(project, startBeat, endBeat);
return this.createSuccessResult(result);
} catch (error) {
return this.createErrorResult(`Failed to read chord progression: ${error}`);
}
}
private findTargetRegion(): KGMidiRegion | null {
const project = this.getCurrentProject();
const tracks = project.getTracks();
const storeState = useProjectStore.getState();
if (storeState.activeRegionId) {
for (const track of tracks) {
const region = track.getRegions().find(candidate => candidate.getId() === storeState.activeRegionId);
if (region instanceof KGMidiRegion) {
return region;
}
}
}
const selectedItems = KGCore.instance().getSelectedItems();
for (const item of selectedItems) {
if (item instanceof KGMidiRegion) {
return item;
}
}
return null;
}
}
+3 -1
View File
@@ -6,14 +6,16 @@ export type { ToolResult, ToolParameter, ToolDefinition, OpenAIToolDefinition, O
import { AddNotesTool } from './AddNotesTool'; import { AddNotesTool } from './AddNotesTool';
import { RemoveNotesTool } from './RemoveNotesTool'; import { RemoveNotesTool } from './RemoveNotesTool';
import { ReadMusicTool } from './ReadMusicTool'; import { ReadMusicTool } from './ReadMusicTool';
import { ReadChordProgressionTool } from './ReadChordProgressionTool';
export { AddNotesTool, RemoveNotesTool, ReadMusicTool }; export { AddNotesTool, RemoveNotesTool, ReadMusicTool, ReadChordProgressionTool };
// Tool registry for easy access // Tool registry for easy access
export const AVAILABLE_TOOLS = { export const AVAILABLE_TOOLS = {
add_notes: AddNotesTool, add_notes: AddNotesTool,
remove_notes: RemoveNotesTool, remove_notes: RemoveNotesTool,
read_music: ReadMusicTool, read_music: ReadMusicTool,
read_chord_progression: ReadChordProgressionTool,
} as const; } as const;
export type ToolName = keyof typeof AVAILABLE_TOOLS; export type ToolName = keyof typeof AVAILABLE_TOOLS;
+71 -1
View File
@@ -5,7 +5,7 @@
import { KGCore } from './KGCore'; import { KGCore } from './KGCore';
import { KGMidiRegion } from './region/KGMidiRegion'; import { KGMidiRegion } from './region/KGMidiRegion';
import { convertRegionToABCNotation } from '../util/abcNotationUtil'; import { convertRegionToABCNotation, convertBeatRangeChordProgressionToABCNotation } from '../util/abcNotationUtil';
import { extractXMLFromString } from '../util/xmlUtil'; import { extractXMLFromString } from '../util/xmlUtil';
import { AgentCore } from '../agent/core/AgentCore'; import { AgentCore } from '../agent/core/AgentCore';
import { AVAILABLE_TOOLS } from '../agent/tools'; import { AVAILABLE_TOOLS } from '../agent/tools';
@@ -23,6 +23,7 @@ export class KGDebugger {
private constructor() { private constructor() {
console.log("🔧 KGDebugger initialized - Available methods:", [ console.log("🔧 KGDebugger initialized - Available methods:", [
'convertSelectedRegionToABCNotation(startFromBeat?)', 'convertSelectedRegionToABCNotation(startFromBeat?)',
'convertSelectedRegionChordProgressionToABCNotation()',
'testQuantizeDuration(durationBeats, timeSignature?)', 'testQuantizeDuration(durationBeats, timeSignature?)',
'debugSelectedItems()', 'debugSelectedItems()',
'createTestRegion()', 'createTestRegion()',
@@ -128,6 +129,74 @@ export class KGDebugger {
} }
} }
/**
* Convert the chord progression that overlaps the currently selected MIDI region to ABC notation
*/
public convertSelectedRegionChordProgressionToABCNotation(): void {
const core = KGCore.instance();
const selectedItems = core.getSelectedItems();
let midiRegion: KGMidiRegion | null = null;
if (selectedItems.length > 0) {
midiRegion = selectedItems.find(item => item.getCurrentType() === 'KGMidiRegion') as KGMidiRegion;
}
if (!midiRegion) {
const storeState = useProjectStore.getState();
const activeRegionId = storeState.activeRegionId;
const tracks = storeState.tracks;
if (activeRegionId) {
for (const track of tracks) {
const region = track.getRegions().find(candidate => candidate.getId() === activeRegionId);
if (region instanceof KGMidiRegion) {
midiRegion = region;
break;
}
}
}
}
if (!midiRegion) {
console.error('❌ No MIDI region found.');
console.log('💡 Try one of these:');
console.log(' • Select a region in the track grid');
console.log(' • Open a region in the piano roll editor');
return;
}
const startBeat = midiRegion.getStartFromBeat();
const endBeat = startBeat + midiRegion.getLength();
console.log(`🎼 Reading chord progression for region: "${midiRegion.getName()}"`);
console.log(`📍 Range: beats ${startBeat}-${endBeat}`);
try {
const output = convertBeatRangeChordProgressionToABCNotation(
KGCore.instance().getCurrentProject(),
startBeat,
endBeat,
);
console.log('✅ Chord progression conversion successful!');
console.log('📄 Result:');
console.log('─'.repeat(50));
console.log(output);
console.log('─'.repeat(50));
if (navigator.clipboard) {
navigator.clipboard.writeText(output).then(() => {
console.log('📋 Chord progression notation copied to clipboard!');
}).catch(() => {
console.log('📋 Could not copy to clipboard (requires HTTPS)');
});
}
} catch (error) {
console.error('❌ Error converting chord progression:', error);
}
}
/** /**
* Test the quantization duration method with a specific duration * Test the quantization duration method with a specific duration
* @param durationBeats - Duration in beats to test * @param durationBeats - Duration in beats to test
@@ -342,6 +411,7 @@ export class KGDebugger {
console.log("🔧 KGDebugger Help"); console.log("🔧 KGDebugger Help");
console.log("Available methods:"); console.log("Available methods:");
console.log(" convertSelectedRegionToABCNotation(startFromBeat?) - Convert selected region to ABC"); console.log(" convertSelectedRegionToABCNotation(startFromBeat?) - Convert selected region to ABC");
console.log(" convertSelectedRegionChordProgressionToABCNotation() - Convert selected region chord progression to dual ABC views");
console.log(" testQuantizeDuration(beats, timeSignature?) - Test quantization logic"); console.log(" testQuantizeDuration(beats, timeSignature?) - Test quantization logic");
console.log(" debugSelectedItems() - Show info about selected items"); console.log(" debugSelectedItems() - Show info about selected items");
console.log(" createTestRegion() - Create test region (not implemented)"); console.log(" createTestRegion() - Create test region (not implemented)");
@@ -1,10 +1,19 @@
import { describe, it, expect, beforeEach, vi } from 'vitest'; import { describe, it, expect, beforeEach, vi } from 'vitest';
import { plainToInstance } from 'class-transformer'; import { plainToInstance } from 'class-transformer';
import { convertRegionToABCNotation } from '../../../util/abcNotationUtil'; import {
convertRegionToABCNotation,
convertBeatRangeChordProgressionToABCNotation,
formatChordProgressionNoteLine,
formatChordProgressionSymbolLine,
getChordProgressionSegmentsForBeatRange,
} from '../../../util/abcNotationUtil';
import { KGMidiRegion } from '../../../core/region/KGMidiRegion'; import { KGMidiRegion } from '../../../core/region/KGMidiRegion';
import { KGChordRegion } from '../../../core/region/KGChordRegion';
import { KGMidiTrack } from '../../../core/track/KGMidiTrack'; import { KGMidiTrack } from '../../../core/track/KGMidiTrack';
import { KGCore } from '../../../core/KGCore'; import { KGCore } from '../../../core/KGCore';
import { KGProject } from '../../../core/KGProject'; import { KGProject } from '../../../core/KGProject';
import { findGlobalTrackByType } from '../../../util/globalTrackUtil';
import { GlobalTrackType } from '../../../core/global-track';
// Import the test fixture // Import the test fixture
import joyProjectData from '../../fixtures/joy-project.json'; import joyProjectData from '../../fixtures/joy-project.json';
@@ -222,4 +231,49 @@ E E F G | G F E D | C C D E | E3/2 D1/2 D2 | E E F G | G F E D | C C D E | D3/2
expect(musicLine).toBeTruthy(); expect(musicLine).toBeTruthy();
}); });
}); });
describe('chord progression formatting', () => {
it('formats the exact 8-bar progression in both symbolic and note-based representations', () => {
const project = new KGProject('chords', 8, 0, 120, { numerator: 4, denominator: 4 }, 'C major');
const midiTrack = new KGMidiTrack('Melody', 1);
const midiRegion = new KGMidiRegion('midi-region-1', '1', 0, 'Melody Region', 0, 32);
midiTrack.addRegion(midiRegion);
project.setTracks([midiTrack]);
const chordTrack = findGlobalTrackByType(project, GlobalTrackType.Chord);
expect(chordTrack).not.toBeNull();
const chords = ['Am', 'F', 'Dm', 'E7', 'Am', 'C', 'Dm', 'E7'];
chords.forEach((symbol, index) => {
chordTrack!.addRegion(new KGChordRegion(`chord-${index}`, chordTrack!.getId(), chordTrack!.getTrackIndex(), symbol, index * 4, 4));
});
const segments = getChordProgressionSegmentsForBeatRange(project, 0, 32);
expect(formatChordProgressionSymbolLine(segments, project.getTimeSignature())).toBe(
'[Am]4 | [F]4 | [Dm]4 | [E7]4 | [Am]4 | [C]4 | [Dm]4 | [E7]4 |'
);
expect(formatChordProgressionNoteLine(segments, project.getTimeSignature())).toBe(
'[A, C E]4 | [F, A, C]4 | [D F A]4 | [E ^G B d]4 | [A, C E]4 | [C E G]4 | [D F A]4 | [E ^G B d]4 |'
);
const output = convertBeatRangeChordProgressionToABCNotation(project, 0, 32);
expect(output).toContain('M:4/4');
expect(output).toContain('L:1/4');
expect(output).toContain('Q:1/4=120');
expect(output).toContain('K:C');
expect(output).not.toContain('X:');
expect(output).not.toContain('T:');
});
it('formats accidentals with ABC prefix notation in note-based chord output', () => {
const project = new KGProject('accidentals', 1, 0, 120, { numerator: 4, denominator: 4 }, 'C major');
const chordTrack = findGlobalTrackByType(project, GlobalTrackType.Chord);
expect(chordTrack).not.toBeNull();
chordTrack!.addRegion(new KGChordRegion('chord-1', chordTrack!.getId(), chordTrack!.getTrackIndex(), 'E7', 0, 4));
const segments = getChordProgressionSegmentsForBeatRange(project, 0, 4);
expect(formatChordProgressionNoteLine(segments, project.getTimeSignature())).toBe('[E ^G B d]4 |');
});
});
}); });
+204 -1
View File
@@ -7,10 +7,14 @@ import { KGMidiRegion } from '../core/region/KGMidiRegion';
import { KGMidiNote } from '../core/midi/KGMidiNote'; import { KGMidiNote } from '../core/midi/KGMidiNote';
import { KGCore } from '../core/KGCore'; import { KGCore } from '../core/KGCore';
import { KGProject } from '../core/KGProject'; import { KGProject } from '../core/KGProject';
import { KGChordRegion } from '../core/region/KGChordRegion';
import { pitchToNoteName } from './midiUtil'; import { pitchToNoteName } from './midiUtil';
import { beatsToTicks, getTicksPerBar, reduceFraction } from './mathUtil'; import { beatsToTicks, getTicksPerBar, reduceFraction } from './mathUtil';
import type { TimeSignature } from '../types/projectTypes'; import type { TimeSignature } from '../types/projectTypes';
import { KEY_SIGNATURE_MAP } from '../constants/coreConstants'; import { KEY_SIGNATURE_MAP } from '../constants/coreConstants';
import { getChordMidiPitches, parseChordSymbol } from './chordUtil';
import { getEffectiveBpmAtBeat, getEffectiveKeySignatureAtBeat, findGlobalTrackByType } from './globalTrackUtil';
import { GlobalTrackType } from '../core/global-track';
// MIDI timing constants // MIDI timing constants
const TICKS_PER_QUARTER_NOTE = 480; const TICKS_PER_QUARTER_NOTE = 480;
@@ -26,6 +30,12 @@ interface ABCNote {
tieWithNext: boolean; // Whether this note should be tied to the next note tieWithNext: boolean; // Whether this note should be tied to the next note
} }
export interface ChordProgressionSegment {
symbol: string;
startBeat: number;
endBeat: number;
}
// Define valid quantization fraction types // Define valid quantization fraction types
// type QuantizationFraction = '1/1' | '1/2' | '1/3' | '1/4' | '1/6' | '1/8' | '1/12' | '1/16' | '1/24' | '1/32'; // future // type QuantizationFraction = '1/1' | '1/2' | '1/3' | '1/4' | '1/6' | '1/8' | '1/12' | '1/16' | '1/24' | '1/32'; // future
type QuantizationFraction = '1/1' | '1/2' | '1/4' | '1/8' | '1/16'; type QuantizationFraction = '1/1' | '1/2' | '1/4' | '1/8' | '1/16';
@@ -58,7 +68,9 @@ function midiPitchToABCNote(pitch: number): string {
// C5 is "c", C6 is "c'", C7 is "c''" // C5 is "c", C6 is "c'", C7 is "c''"
// C3 is "C,", C2 is "C,," etc. // C3 is "C,", C2 is "C,," etc.
const baseNote = note.replace('#', '^'); // Convert sharp to ABC sharp notation const accidental = note.includes('#') ? '^' : note.includes('b') ? '_' : '';
const naturalNote = note.replace('#', '').replace('b', '');
const baseNote = `${accidental}${naturalNote}`;
if (octave >= 4) { if (octave >= 4) {
if (octave === 4) { if (octave === 4) {
@@ -177,6 +189,197 @@ function formatABCHeader(region: KGMidiRegion, project: KGProject): string {
return header.join('\n'); return header.join('\n');
} }
function formatABCSharedHeader(project: KGProject, beat: number): string {
const timeSignature = project.getTimeSignature();
const bpm = getEffectiveBpmAtBeat(project, beat);
const keySignature = getEffectiveKeySignatureAtBeat(project, beat);
const abcKeySignature = KEY_SIGNATURE_MAP[keySignature]?.abcNotationKeySignature || 'C';
return [
`M:${timeSignature.numerator}/${timeSignature.denominator}`,
`L:1/${timeSignature.denominator}`,
`Q:1/${timeSignature.denominator}=${bpm}`,
`K:${abcKeySignature}`
].join('\n');
}
function getChordRootMidi(symbol: string): number | null {
const descriptor = parseChordSymbol(symbol);
if (!descriptor) {
return null;
}
const rootToPitchClass: Record<string, number> = {
C: 0,
'C#': 1,
Db: 1,
D: 2,
'D#': 3,
Eb: 3,
E: 4,
F: 5,
'F#': 6,
Gb: 6,
G: 7,
'G#': 8,
Ab: 8,
A: 9,
'A#': 10,
Bb: 10,
B: 11,
};
const pitchClass = rootToPitchClass[descriptor.root];
if (pitchClass === undefined) {
return null;
}
const middleCOctaveMidi = 60 + pitchClass;
return middleCOctaveMidi > 64 ? middleCOctaveMidi - 12 : middleCOctaveMidi;
}
function formatBeatsToABCLength(lengthBeats: number, timeSignature: TimeSignature): string {
const ticks = beatsToTicks(lengthBeats, timeSignature);
return convertTicksToABCLength(ticks, timeSignature);
}
function splitSegmentAtBarBoundaries(
startBeat: number,
endBeat: number,
beatsPerBar: number,
): Array<{ startBeat: number; endBeat: number }> {
const segments: Array<{ startBeat: number; endBeat: number }> = [];
let currentStart = startBeat;
while (currentStart < endBeat) {
const nextBarBeat = Math.floor(currentStart / beatsPerBar + 1) * beatsPerBar;
const currentEnd = Math.min(endBeat, nextBarBeat);
segments.push({ startBeat: currentStart, endBeat: currentEnd });
currentStart = currentEnd;
}
return segments;
}
function formatTimedChordTokens(
values: string[],
segments: ChordProgressionSegment[],
timeSignature: TimeSignature,
): string {
const beatsPerBar = timeSignature.numerator;
const tokens: string[] = [];
segments.forEach((segment, index) => {
const splitSegments = splitSegmentAtBarBoundaries(segment.startBeat, segment.endBeat, beatsPerBar);
splitSegments.forEach((part) => {
const length = formatBeatsToABCLength(part.endBeat - part.startBeat, timeSignature);
tokens.push(`[${values[index]}]${length}`);
tokens.push('|');
});
});
return tokens.join(' ');
}
export function getChordProgressionSegmentsForBeatRange(
project: KGProject,
startBeat: number,
endBeat: number,
): ChordProgressionSegment[] {
if (endBeat <= startBeat) {
return [];
}
const chordTrack = findGlobalTrackByType(project, GlobalTrackType.Chord);
if (!chordTrack) {
return [];
}
return chordTrack.getRegions()
.filter((region): region is KGChordRegion => region instanceof KGChordRegion)
.map((region) => ({
region,
startBeat: Math.max(startBeat, region.getStartFromBeat()),
endBeat: Math.min(endBeat, region.getStartFromBeat() + region.getLength()),
}))
.filter(({ startBeat: segmentStart, endBeat: segmentEnd }) => segmentEnd > segmentStart)
.sort((left, right) => left.startBeat - right.startBeat)
.map(({ region, startBeat: segmentStart, endBeat: segmentEnd }) => ({
symbol: region.getSymbol(),
startBeat: segmentStart,
endBeat: segmentEnd,
}));
}
export function formatChordProgressionSymbolLine(
segments: ChordProgressionSegment[],
timeSignature: TimeSignature,
): string {
return formatTimedChordTokens(
segments.map(segment => segment.symbol),
segments,
timeSignature,
);
}
export function formatChordProgressionNoteLine(
segments: ChordProgressionSegment[],
timeSignature: TimeSignature,
): string {
const values = segments.map((segment) => {
const rootMidi = getChordRootMidi(segment.symbol);
if (rootMidi === null) {
throw new Error(`Unable to parse chord "${segment.symbol}"`);
}
const pitches = getChordMidiPitches(segment.symbol, rootMidi);
if (pitches.length === 0) {
throw new Error(`Unable to parse chord "${segment.symbol}"`);
}
return pitches.map(midiPitchToABCNote).join(' ');
});
return formatTimedChordTokens(values, segments, timeSignature);
}
export function convertBeatRangeChordProgressionToABCNotation(
project: KGProject,
startBeat: number,
endBeat: number,
): string {
const segments = getChordProgressionSegmentsForBeatRange(project, startBeat, endBeat);
const header = formatABCSharedHeader(project, startBeat);
if (segments.length === 0) {
return [
'Selected Region Chord Progression',
'This progression comes only from user-defined chord regions on the global chord track. If no chord progression is defined for this range, read the notes directly with `read_music`.',
'',
header,
'',
'No chord progression is defined for the selected MIDI region range. Use `read_music` to inspect the notes directly.'
].join('\n');
}
const timeSignature = project.getTimeSignature();
const chordSymbols = formatChordProgressionSymbolLine(segments, timeSignature);
const chordNotes = formatChordProgressionNoteLine(segments, timeSignature);
return [
'Selected Region Chord Progression',
'This progression comes only from user-defined chord regions on the global chord track. Representation 1 uses symbolic chord names such as `Em7b5`. Representation 2 rewrites the same progression as note-based ABC chord tokens.',
'',
header,
'',
'Chord-symbol representation:',
chordSymbols,
'',
'Note-based ABC chord representation:',
chordNotes,
].join('\n');
}
/** /**
* Format ABC notation body with notes * Format ABC notation body with notes
* @param notes - Array of MIDI notes to convert * @param notes - Array of MIDI notes to convert