char-slop/chordpad.vibed.software

git clone https://git.t4t.associates/char-slop/chordpad.vibed.software

Charlotte Somcbor project file formata5e999d

main
2.8 KiB85 linesraw
1import { chordSymbol, parseChord } from "../theory/chord.ts";
2import { midiName } from "../theory/pitch.ts";
3import type { Key } from "../theory/key.ts";
4import { respell, voiceTrack } from "./harmony.ts";
5import {
6  type ChordClip,
7  type ChordTrack,
8  copyChords,
9  copyNotes,
10  keyOf,
11  type NoteClip,
12  type NoteTrack,
13  type Project,
14} from "./model.ts";
15
16/** how chords go on the system clipboard between tabs of this app */
17export const CHORDS_MIME = "application/x-chordpad-chords+json";
18type ClipboardChords = { key: Project["key"]; clip: ChordClip };
19
20/** chords to put on the clipboard: exactly, for this app, and as plain text for anything else */
21export function chordsToClipboard(
22  track: ChordTrack,
23  ids: readonly string[],
24  key: Project["key"],
25): { data: string; text: string } | undefined {
26  const clip = copyChords(track, ids);
27  if (!clip) return undefined;
28  // letter names: a roman numeral means nothing outside the key it's in
29  const text = voiceTrack(track, keyOf(key))
30    .filter(v => ids.includes(v.event.id))
31    .map(v => (v.chord ? chordSymbol(v.chord) : v.label))
32    .join(" ");
33  return { data: JSON.stringify({ key, clip } satisfies ClipboardChords), text };
34}
35
36/** copied chords, written to name the same chords in the key they're pasted into */
37export function chordsFromClipboard(data: string, into: Key): ChordClip | undefined {
38  let parsed: ClipboardChords;
39  try {
40    parsed = JSON.parse(data);
41  } catch {
42    return undefined;
43  }
44  if (!Array.isArray(parsed?.clip?.chords) || !parsed.key) return undefined;
45  const from = keyOf(parsed.key);
46  return {
47    ...parsed.clip,
48    chords: parsed.clip.chords.map(c =>
49      c.symbol ? { ...c, symbol: respell(c.symbol, from, into) } : c,
50    ),
51  };
52}
53
54/** chord symbols pasted as text ("Dm7 G7 | Cmaj7"), each the given length; what won't parse is left out */
55export function chordsFromText(text: string, key: Key, length: number): ChordClip | undefined {
56  const symbols = text.split(/[\s|,]+/).filter(s => s && parseChord(s, key));
57  if (!symbols.length) return undefined;
58  return {
59    chords: symbols.map((symbol, i) => ({ start: i * length, dur: length, symbol })),
60    span: symbols.length * length,
61  };
62}
63
64export const NOTES_MIME = "application/x-chordpad-notes+json";
65
66/** notes to put on the clipboard: exactly, and as their names for anything else */
67export function notesToClipboard(
68  track: NoteTrack,
69  ids: readonly string[],
70  key: Key,
71): { data: string; text: string } | undefined {
72  const clip = copyNotes(track, ids);
73  if (!clip) return undefined;
74  const text = clip.notes.map(n => midiName(n.pitch, key.tonic)).join(" ");
75  return { data: JSON.stringify(clip), text };
76}
77
78export function notesFromClipboard(data: string): NoteClip | undefined {
79  try {
80    const clip = JSON.parse(data);
81    return Array.isArray(clip?.notes) ? clip : undefined;
82  } catch {
83    return undefined;
84  }
85}