import { Chord, Note } from 'tonal'; import { KGChordRegion } from '../core/region/KGChordRegion'; import { getChordMidiPitches, parseChordSymbol } from './chordUtil'; export const CHORD_REGION_IMPORT_MIME_TYPE = 'application/kgstudio-chord-region'; export const CHORD_REGION_IMPORT_VELOCITY = 127; export const CHORD_REGION_IMPORT_REGION_NAME = 'Chord Progression'; export interface ChordRegionImportPayload { draggedRegionId: string; selectedRegionIds: string[]; } export interface ImportedChordMidiNoteData { startBeat: number; endBeat: number; pitch: number; velocity: number; } export interface ChordRegionImportPlan { sourceRegionIds: string[]; startBeat: number; lengthInBeats: number; notes: ImportedChordMidiNoteData[]; } export interface ChordRegionImportError { message: string; } export type ChordRegionImportPlanResult = | { ok: true; plan: ChordRegionImportPlan } | { ok: false; error: ChordRegionImportError }; export function resolveChordRegionImportSelection( draggedRegionId: string, selectedRegionIds: string[], ): string[] { return selectedRegionIds.includes(draggedRegionId) ? selectedRegionIds : [draggedRegionId]; } function getBaseRootMidi(symbol: string): number | null { const descriptor = parseChordSymbol(symbol); if (!descriptor) { return null; } const rootLetter = descriptor.root.charAt(0).toUpperCase(); const octave = ['F', 'G', 'A', 'B'].includes(rootLetter) ? 3 : 4; return Note.midi(`${descriptor.root}${octave}`); } export function convertChordSymbolToMidiPitches(symbol: string): number[] | null { const descriptor = parseChordSymbol(symbol); if (!descriptor) { return null; } const tonalChord = Chord.get(descriptor.symbol); if (tonalChord.empty) { return null; } const rootMidi = getBaseRootMidi(descriptor.symbol); if (rootMidi === null) { return null; } const midiPitches = getChordMidiPitches(descriptor.symbol, rootMidi); if (midiPitches.length === 0) { return null; } return [rootMidi - 12, ...midiPitches]; } export function buildChordRegionImportPlan( chordRegions: KGChordRegion[], ): ChordRegionImportPlanResult { if (chordRegions.length === 0) { return { ok: false, error: { message: 'No chord regions were available to import.' }, }; } const sortedRegions = [...chordRegions].sort((left, right) => ( left.getStartFromBeat() - right.getStartFromBeat() )); const firstRegion = sortedRegions[0]; const lastRegion = sortedRegions[sortedRegions.length - 1]; const startBeat = firstRegion.getStartFromBeat(); const endBeat = lastRegion.getStartFromBeat() + lastRegion.getLength(); const notes: ImportedChordMidiNoteData[] = []; for (const region of sortedRegions) { const midiPitches = convertChordSymbolToMidiPitches(region.getSymbol()); if (!midiPitches) { return { ok: false, error: { message: `Unable to import chord "${region.getSymbol()}". Please update the chord symbol and try again.` }, }; } const noteStartBeat = region.getStartFromBeat() - startBeat; const noteEndBeat = noteStartBeat + region.getLength(); midiPitches.forEach(pitch => { notes.push({ startBeat: noteStartBeat, endBeat: noteEndBeat, pitch, velocity: CHORD_REGION_IMPORT_VELOCITY, }); }); } return { ok: true, plan: { sourceRegionIds: sortedRegions.map(region => region.getId()), startBeat, lengthInBeats: endBeat - startBeat, notes, }, }; }