import { chordSymbol, parseChord } from "../theory/chord.ts"; import { midiName } from "../theory/pitch.ts"; import type { Key } from "../theory/key.ts"; import { respell, voiceTrack } from "./harmony.ts"; import { type ChordClip, type ChordTrack, copyChords, copyNotes, keyOf, type NoteClip, type NoteTrack, type Project, } from "./model.ts"; /** how chords go on the system clipboard between tabs of this app */ export const CHORDS_MIME = "application/x-chordpad-chords+json"; type ClipboardChords = { key: Project["key"]; clip: ChordClip }; /** chords to put on the clipboard: exactly, for this app, and as plain text for anything else */ export function chordsToClipboard( track: ChordTrack, ids: readonly string[], key: Project["key"], ): { data: string; text: string } | undefined { const clip = copyChords(track, ids); if (!clip) return undefined; // letter names: a roman numeral means nothing outside the key it's in const text = voiceTrack(track, keyOf(key)) .filter(v => ids.includes(v.event.id)) .map(v => (v.chord ? chordSymbol(v.chord) : v.label)) .join(" "); return { data: JSON.stringify({ key, clip } satisfies ClipboardChords), text }; } /** copied chords, written to name the same chords in the key they're pasted into */ export function chordsFromClipboard(data: string, into: Key): ChordClip | undefined { let parsed: ClipboardChords; try { parsed = JSON.parse(data); } catch { return undefined; } if (!Array.isArray(parsed?.clip?.chords) || !parsed.key) return undefined; const from = keyOf(parsed.key); return { ...parsed.clip, chords: parsed.clip.chords.map(c => c.symbol ? { ...c, symbol: respell(c.symbol, from, into) } : c, ), }; } /** chord symbols pasted as text ("Dm7 G7 | Cmaj7"), each the given length; what won't parse is left out */ export function chordsFromText(text: string, key: Key, length: number): ChordClip | undefined { const symbols = text.split(/[\s|,]+/).filter(s => s && parseChord(s, key)); if (!symbols.length) return undefined; return { chords: symbols.map((symbol, i) => ({ start: i * length, dur: length, symbol })), span: symbols.length * length, }; } export const NOTES_MIME = "application/x-chordpad-notes+json"; /** notes to put on the clipboard: exactly, and as their names for anything else */ export function notesToClipboard( track: NoteTrack, ids: readonly string[], key: Key, ): { data: string; text: string } | undefined { const clip = copyNotes(track, ids); if (!clip) return undefined; const text = clip.notes.map(n => midiName(n.pitch, key.tonic)).join(" "); return { data: JSON.stringify(clip), text }; } export function notesFromClipboard(data: string): NoteClip | undefined { try { const clip = JSON.parse(data); return Array.isArray(clip?.notes) ? clip : undefined; } catch { return undefined; } }