@hidechae/eztab-mcp 0.3.0 → 0.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/server.js +267 -124
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -92,7 +92,7 @@ Run standalone for testing: `task mcp` (or `npx tsx mcp/server.ts`).
|
|
|
92
92
|
| `set_title` | Set the document title |
|
|
93
93
|
| `set_artist` | Set the document artist |
|
|
94
94
|
| `write_measures` | 小節単位の一括入力。1拍を1文字列で書き(`"6:5 6:~7"` など)、列番号への割り付けはサーバ側で行う。譜面・画像からの書き起こし向け |
|
|
95
|
-
| `set_cells` | Set tab cells: a fret number, `rest`, `mute` (cutting "x"), or `clear` (empty = sustain). Optional `bend` (0.5/1/1.5) chokes a fret note; optional `tie` ties/slurs it to the previous note (same pitch = just sustain); optional `pm` palm-mutes it |
|
|
95
|
+
| `set_cells` | Set tab cells: a fret number, `rest`, `mute` (cutting "x"), or `clear` (empty = sustain). Optional `bend` (0.5/1/1.5) chokes a fret note; optional `tie` ties/slurs it to the previous note (same pitch = just sustain); optional `pm` palm-mutes it; optional `harm` (`natural` / `artificial` / `pinch` / `none`) + `harmAt` writes a harmonic |
|
|
96
96
|
| `set_chords` | Set chord labels per column |
|
|
97
97
|
| `set_key` / `remove_key` | Set/add or remove a key (key change) at a measure |
|
|
98
98
|
| `set_section` | Set/remove a section marker (e.g. "Verse", "Chorus") |
|
package/dist/server.js
CHANGED
|
@@ -7,8 +7,9 @@ import { z } from "zod";
|
|
|
7
7
|
|
|
8
8
|
// ../src/types/tab.ts
|
|
9
9
|
var TAB_FILE_VERSION = 1;
|
|
10
|
-
var SUPPORTED_FORMAT_REVISION =
|
|
10
|
+
var SUPPORTED_FORMAT_REVISION = 3;
|
|
11
11
|
var MULTI_TRACK_FORMAT_REVISION = 2;
|
|
12
|
+
var HARMONICS_FORMAT_REVISION = 3;
|
|
12
13
|
var MAX_TRACKS = 4;
|
|
13
14
|
var DEFAULT_MAIN_TRACK_NAME = "Guitar";
|
|
14
15
|
var DEFAULT_TEMPO = 120;
|
|
@@ -41,9 +42,92 @@ function shiftKeysForDelete(keys, index) {
|
|
|
41
42
|
}
|
|
42
43
|
return next;
|
|
43
44
|
}
|
|
45
|
+
var HARMONIC_KINDS = ["natural", "artificial", "pinch"];
|
|
46
|
+
var MAX_HARM_AT = 36;
|
|
47
|
+
function checkHarmonic(fret, harm, harmAt) {
|
|
48
|
+
if (harm == null) {
|
|
49
|
+
if (harmAt != null) {
|
|
50
|
+
return { ok: false, field: "harmAt", reason: "a harmonic position needs a harmonic kind" };
|
|
51
|
+
}
|
|
52
|
+
return { ok: true, value: { fret } };
|
|
53
|
+
}
|
|
54
|
+
if (!HARMONIC_KINDS.includes(harm)) {
|
|
55
|
+
return { ok: false, field: "harm", reason: `unknown harmonic kind "${String(harm)}"` };
|
|
56
|
+
}
|
|
57
|
+
const kind = harm;
|
|
58
|
+
if (harmAt == null) {
|
|
59
|
+
if (kind === "natural" && !(fret > 0)) {
|
|
60
|
+
return { ok: false, field: "harm", reason: "a natural harmonic needs a fret above 0" };
|
|
61
|
+
}
|
|
62
|
+
return { ok: true, value: { fret, harm: kind } };
|
|
63
|
+
}
|
|
64
|
+
if (typeof harmAt !== "number" || !Number.isFinite(harmAt) || harmAt <= 0 || harmAt > MAX_HARM_AT) {
|
|
65
|
+
return {
|
|
66
|
+
ok: false,
|
|
67
|
+
field: "harmAt",
|
|
68
|
+
reason: `harmonic position must be a number above 0 and up to ${MAX_HARM_AT}`
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
if (Math.abs(harmAt * 10 - Math.round(harmAt * 10)) > 1e-6) {
|
|
72
|
+
return { ok: false, field: "harmAt", reason: "harmonic position has at most one decimal" };
|
|
73
|
+
}
|
|
74
|
+
const at = Math.round(harmAt * 10) / 10;
|
|
75
|
+
if (kind === "natural") {
|
|
76
|
+
const touch = Math.round(at);
|
|
77
|
+
if (touch > MAX_FRET) {
|
|
78
|
+
return { ok: false, field: "harmAt", reason: `natural harmonic above fret ${MAX_FRET}` };
|
|
79
|
+
}
|
|
80
|
+
return {
|
|
81
|
+
ok: true,
|
|
82
|
+
value: Number.isInteger(at) ? { fret: touch, harm: kind } : { fret: touch, harm: kind, harmAt: at }
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
if (at <= fret) {
|
|
86
|
+
return {
|
|
87
|
+
ok: false,
|
|
88
|
+
field: "harmAt",
|
|
89
|
+
reason: `the harmonic position (${at}) must be above the fretted note (${fret})`
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
return { ok: true, value: { fret, harm: kind, harmAt: at } };
|
|
93
|
+
}
|
|
94
|
+
function normalizeCell(c) {
|
|
95
|
+
if (typeof c !== "object" || c === null || !("harm" in c || "harmAt" in c)) return c;
|
|
96
|
+
const { harm, harmAt, ...rest } = c;
|
|
97
|
+
let r = checkHarmonic(c.fret, harm, harmAt);
|
|
98
|
+
if (!r.ok && r.field === "harmAt" && harm != null) r = checkHarmonic(c.fret, harm, void 0);
|
|
99
|
+
const h = r.ok ? r.value : { fret: c.fret };
|
|
100
|
+
return { ...rest, ...h };
|
|
101
|
+
}
|
|
102
|
+
function measuresHaveHarmonics(measures) {
|
|
103
|
+
return measures.some((m) => m.cells.some((row) => row.some((c) => cellHarm(c) != null)));
|
|
104
|
+
}
|
|
105
|
+
function cellFret(c) {
|
|
106
|
+
if (typeof c === "number") return c;
|
|
107
|
+
if (typeof c === "object" && c !== null) return c.fret;
|
|
108
|
+
return null;
|
|
109
|
+
}
|
|
44
110
|
function cellPalmMute(c) {
|
|
45
111
|
return typeof c === "object" && c !== null && !!c.pm;
|
|
46
112
|
}
|
|
113
|
+
function cellHarm(c) {
|
|
114
|
+
return typeof c === "object" && c !== null ? c.harm : void 0;
|
|
115
|
+
}
|
|
116
|
+
function cellHarmAt(c) {
|
|
117
|
+
return typeof c === "object" && c !== null && c.harm ? c.harmAt : void 0;
|
|
118
|
+
}
|
|
119
|
+
function makeNoteCell(fret, bend, tie, pm, harmonic) {
|
|
120
|
+
const harm = harmonic?.harm;
|
|
121
|
+
if (!(bend > 0 || tie || pm || harm)) return fret;
|
|
122
|
+
return {
|
|
123
|
+
fret,
|
|
124
|
+
...bend > 0 ? { bend } : {},
|
|
125
|
+
...tie ? { tie: true } : {},
|
|
126
|
+
...pm ? { pm: true } : {},
|
|
127
|
+
...harm ? { harm } : {},
|
|
128
|
+
...harm && harmonic?.harmAt != null ? { harmAt: harmonic.harmAt } : {}
|
|
129
|
+
};
|
|
130
|
+
}
|
|
47
131
|
var CHORD_REST = "rest";
|
|
48
132
|
var CHORD_REST_LABEL = "N.C.";
|
|
49
133
|
function chordDisplay(label) {
|
|
@@ -115,7 +199,7 @@ function normalizeDocument(data) {
|
|
|
115
199
|
});
|
|
116
200
|
const cols = subdivisions.reduce((a, b) => a + b, 0);
|
|
117
201
|
const cells = (m.cells ?? []).map(
|
|
118
|
-
(row) => Array.from({ length: cols }, (_, c) => row?.[c] ?? null)
|
|
202
|
+
(row) => Array.from({ length: cols }, (_, c) => normalizeCell(row?.[c] ?? null))
|
|
119
203
|
);
|
|
120
204
|
const rawChords = Array.isArray(m.chords) ? m.chords : typeof m.chord === "string" && m.chord ? [m.chord] : [];
|
|
121
205
|
const chords = normalizeChords(rawChords, subdivisions);
|
|
@@ -179,7 +263,7 @@ function fitTrackMeasure(raw, main, stringCount) {
|
|
|
179
263
|
const rows = Array.isArray(raw?.cells) ? raw.cells : [];
|
|
180
264
|
const cells = Array.from(
|
|
181
265
|
{ length: stringCount },
|
|
182
|
-
(_, s) => Array.from({ length: cols }, (_2, c) => rows[s]?.[c] ?? null)
|
|
266
|
+
(_, s) => Array.from({ length: cols }, (_2, c) => normalizeCell(rows[s]?.[c] ?? null))
|
|
183
267
|
);
|
|
184
268
|
return { subdivisions, cells };
|
|
185
269
|
}
|
|
@@ -307,7 +391,7 @@ function newUid() {
|
|
|
307
391
|
return newTabId();
|
|
308
392
|
}
|
|
309
393
|
function storedMeasure(m) {
|
|
310
|
-
const { uid: _uid, ...rest } = m;
|
|
394
|
+
const { uid: _uid, beatWidthOverride: _w, ...rest } = m;
|
|
311
395
|
return rest;
|
|
312
396
|
}
|
|
313
397
|
function openWorkspace(doc) {
|
|
@@ -391,12 +475,12 @@ function closeWorkspace(doc) {
|
|
|
391
475
|
const mainMeta = ws.metas[0];
|
|
392
476
|
const mainIsDefault = mainMeta.name === DEFAULT_MAIN_TRACK_NAME && mainMeta.volume === 1 && !mainMeta.muted;
|
|
393
477
|
if (extras.length > 0 || !mainIsDefault) out.mainTrack = mainMeta;
|
|
394
|
-
if (extras.length > 0)
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
478
|
+
if (extras.length > 0) out.tracks = extras;
|
|
479
|
+
let required = 1;
|
|
480
|
+
if (extras.length > 0) required = MULTI_TRACK_FORMAT_REVISION;
|
|
481
|
+
if (bodies.some((b) => measuresHaveHarmonics(b.measures))) required = HARMONICS_FORMAT_REVISION;
|
|
482
|
+
const revision = formatRevision != null && formatRevision > HARMONICS_FORMAT_REVISION ? formatRevision : required;
|
|
483
|
+
if (revision > 1) out.formatRevision = revision;
|
|
400
484
|
return out;
|
|
401
485
|
}
|
|
402
486
|
function reconcileTracks(prev, next) {
|
|
@@ -554,96 +638,6 @@ var cloud = {
|
|
|
554
638
|
}
|
|
555
639
|
};
|
|
556
640
|
|
|
557
|
-
// eztab.ts
|
|
558
|
-
function normalize(data) {
|
|
559
|
-
return normalizeDocument(data);
|
|
560
|
-
}
|
|
561
|
-
function cellText(cell) {
|
|
562
|
-
if (cell === null) return "--";
|
|
563
|
-
if (cell === "rest") return "-R";
|
|
564
|
-
if (cell === "mute") return "-x";
|
|
565
|
-
if (typeof cell === "object") {
|
|
566
|
-
const bend = cell.bend ?? 0;
|
|
567
|
-
if (cell.tie) return `~${cell.fret}`.padStart(2, "-").slice(-2);
|
|
568
|
-
const mark = bend === 0.5 ? "b" : bend === 1.5 ? "^" : bend === 1 ? "B" : "";
|
|
569
|
-
return `${cell.fret}${mark}`.padStart(2, "-").slice(-2);
|
|
570
|
-
}
|
|
571
|
-
return String(cell).padStart(2, "-");
|
|
572
|
-
}
|
|
573
|
-
function render(doc, from = 0, to = doc.measures.length - 1, track) {
|
|
574
|
-
const bodies = trackBodies(doc);
|
|
575
|
-
const metas = trackMetas(doc);
|
|
576
|
-
const multi = bodies.length > 1;
|
|
577
|
-
const main = bodies[0];
|
|
578
|
-
const shown = bodies.map((_, i) => i).filter((i) => track == null || i === track);
|
|
579
|
-
const lines = [];
|
|
580
|
-
lines.push(`Title: ${doc.title}${doc.artist ? ` \u2014 ${doc.artist}` : ""} tempo=${doc.tempo}`);
|
|
581
|
-
if (multi) {
|
|
582
|
-
lines.push(`Tracks (track index: name, tuning top\u2192bottom) measures=${main.measures.length}`);
|
|
583
|
-
metas.forEach((t, i) => {
|
|
584
|
-
lines.push(
|
|
585
|
-
` ${i}: ${t.name} ${bodies[i].tuning.join(" ")}${i === 0 ? " (main: carries the chords)" : ""}`
|
|
586
|
-
);
|
|
587
|
-
});
|
|
588
|
-
} else {
|
|
589
|
-
lines.push(`Tuning (top\u2192bottom): ${main.tuning.join(" ")} measures=${main.measures.length}`);
|
|
590
|
-
}
|
|
591
|
-
const ann = [];
|
|
592
|
-
main.measures.forEach((_, m) => {
|
|
593
|
-
const k = doc.keys.find((x) => x.measure === m);
|
|
594
|
-
const s = doc.sections.find((x) => x.measure === m);
|
|
595
|
-
if (k) ann.push(`m${m}:key=${k.tonic} ${k.mode}`);
|
|
596
|
-
if (s && s.label) ann.push(`m${m}:section="${s.label}"`);
|
|
597
|
-
});
|
|
598
|
-
if (ann.length) lines.push(`Markers: ${ann.join(", ")}`);
|
|
599
|
-
main.measures.forEach((m, mi) => {
|
|
600
|
-
if (mi < from || mi > to) return;
|
|
601
|
-
const key = activeKeyAt(doc.keys, mi);
|
|
602
|
-
lines.push("");
|
|
603
|
-
if (!multi) {
|
|
604
|
-
lines.push(
|
|
605
|
-
`--- measure ${mi} (bar ${mi + 1}) (${m.beats}/${m.beatUnit}, ${subText(m)}, ${columnCount(m)} cols, key ${key.tonic} ${key.mode}) ---`
|
|
606
|
-
);
|
|
607
|
-
lines.push(chordLine(m));
|
|
608
|
-
measureRows(lines, m, main.tuning);
|
|
609
|
-
return;
|
|
610
|
-
}
|
|
611
|
-
lines.push(
|
|
612
|
-
`--- measure ${mi} (bar ${mi + 1}) (${m.beats}/${m.beatUnit}, key ${key.tonic} ${key.mode}) ---`
|
|
613
|
-
);
|
|
614
|
-
lines.push(`${chordLine(m)} (columns of track 0)`);
|
|
615
|
-
for (const t of shown) {
|
|
616
|
-
const tm = bodies[t].measures[mi];
|
|
617
|
-
lines.push(`[track ${t}: ${metas[t].name}] ${subText(tm)}, ${columnCount(tm)} cols`);
|
|
618
|
-
measureRows(lines, tm, bodies[t].tuning);
|
|
619
|
-
}
|
|
620
|
-
});
|
|
621
|
-
return lines.join("\n");
|
|
622
|
-
}
|
|
623
|
-
function subText(m) {
|
|
624
|
-
const sub = uniformSubdivision(m);
|
|
625
|
-
return sub != null ? `${sub}/beat` : `[${m.subdivisions.join(",")}]/beat`;
|
|
626
|
-
}
|
|
627
|
-
function chordLine(m) {
|
|
628
|
-
const row = (m.chords ?? []).map((label, c) => label ? `c${c}:${chordDisplay(label)}` : null).filter(Boolean).join(" ");
|
|
629
|
-
return `chords: ${row || "(none)"}`;
|
|
630
|
-
}
|
|
631
|
-
function measureRows(lines, m, tuning) {
|
|
632
|
-
const ruler = Array.from({ length: columnCount(m) }, (_, c) => String(c).padStart(2, " ")).join(
|
|
633
|
-
" "
|
|
634
|
-
);
|
|
635
|
-
lines.push(`col: ${ruler}`);
|
|
636
|
-
m.cells.forEach((row, s) => {
|
|
637
|
-
const label = `${s + 1} ${tuning[s] ?? "?"}`.padEnd(6, " ");
|
|
638
|
-
lines.push(`${label}| ${row.map(cellText).join(" ")}`);
|
|
639
|
-
});
|
|
640
|
-
const pm = Array.from(
|
|
641
|
-
{ length: columnCount(m) },
|
|
642
|
-
(_, c) => m.cells.some((row) => cellPalmMute(row[c] ?? null)) ? "PM" : " "
|
|
643
|
-
);
|
|
644
|
-
if (pm.some((x) => x !== " ")) lines.push(`pm: ${pm.join(" ")}`);
|
|
645
|
-
}
|
|
646
|
-
|
|
647
641
|
// notation.ts
|
|
648
642
|
var BEAT_UNITS2 = [2, 4, 8, 16];
|
|
649
643
|
var BENDS = { b: 0.5, B: 1, "^": 1.5 };
|
|
@@ -659,18 +653,44 @@ function parseTime(text2) {
|
|
|
659
653
|
function parseValue(raw, where) {
|
|
660
654
|
if (raw === "x") return "mute";
|
|
661
655
|
if (raw === "r") return "rest";
|
|
662
|
-
const m = /^(~?)(\d+)([bB^
|
|
656
|
+
const m = /^(~?)(?:<(\d+(?:\.\d+)?)>|(\d+)(?:<(\d+(?:\.\d+)?)>)?)([bB^pah]*)$/.exec(raw);
|
|
663
657
|
if (!m) throw new Error(`${where}: bad value "${raw}"`);
|
|
664
|
-
const
|
|
658
|
+
const natural = m[2] != null;
|
|
659
|
+
const at = natural ? Number(m[2]) : m[4] != null ? Number(m[4]) : void 0;
|
|
660
|
+
const fret = natural ? Math.round(Number(m[2])) : Number(m[3]);
|
|
665
661
|
if (fret > MAX_FRET) throw new Error(`${where}: fret ${fret} is above ${MAX_FRET}`);
|
|
666
|
-
const mods = m[
|
|
662
|
+
const mods = m[5];
|
|
667
663
|
const bendMarks = [...mods].filter((c) => c in BENDS);
|
|
668
664
|
if (bendMarks.length > 1) throw new Error(`${where}: more than one choke mark in "${raw}"`);
|
|
669
|
-
const
|
|
670
|
-
|
|
671
|
-
if (
|
|
672
|
-
if (
|
|
673
|
-
|
|
665
|
+
const a = mods.includes("a");
|
|
666
|
+
const h = mods.includes("h");
|
|
667
|
+
if (a && h) throw new Error(`${where}: "a" and "h" cannot be combined in "${raw}"`);
|
|
668
|
+
if (natural && (a || h)) {
|
|
669
|
+
throw new Error(`${where}: a natural harmonic <pos> takes no "a"/"h" in "${raw}"`);
|
|
670
|
+
}
|
|
671
|
+
if (a && at != null) {
|
|
672
|
+
throw new Error(`${where}: "${raw}" \u2014 a position <pos> already means artificial; drop "a"`);
|
|
673
|
+
}
|
|
674
|
+
const harm = natural ? "natural" : h ? "pinch" : a || at != null ? "artificial" : void 0;
|
|
675
|
+
const checked = checkHarmonic(fret, harm, at);
|
|
676
|
+
if (!checked.ok) throw new Error(`${where}: ${checked.reason} in "${raw}"`);
|
|
677
|
+
const bend = bendMarks.length ? BENDS[bendMarks[0]] : 0;
|
|
678
|
+
return makeNoteCell(checked.value.fret, bend, !!m[1], mods.includes("p"), checked.value);
|
|
679
|
+
}
|
|
680
|
+
function noteText(cell) {
|
|
681
|
+
if (typeof cell === "number") return String(cell);
|
|
682
|
+
const bend = cell.bend ?? 0;
|
|
683
|
+
const bendMark = bend === 0.5 ? "b" : bend === 1.5 ? "^" : bend === 1 ? "B" : "";
|
|
684
|
+
let body;
|
|
685
|
+
let harmMark = "";
|
|
686
|
+
if (cell.harm === "natural") {
|
|
687
|
+
body = `<${cell.harmAt ?? cell.fret}>`;
|
|
688
|
+
} else {
|
|
689
|
+
body = cell.harm && cell.harmAt != null ? `${cell.fret}<${cell.harmAt}>` : String(cell.fret);
|
|
690
|
+
if (cell.harm === "pinch") harmMark = "h";
|
|
691
|
+
else if (cell.harm === "artificial" && cell.harmAt == null) harmMark = "a";
|
|
692
|
+
}
|
|
693
|
+
return `${cell.tie ? "~" : ""}${body}${bendMark}${harmMark}${cell.pm ? "p" : ""}`;
|
|
674
694
|
}
|
|
675
695
|
function parseBeat(text2, stringCount, where) {
|
|
676
696
|
const slots = text2.trim().split(/\s+/);
|
|
@@ -704,6 +724,18 @@ function parseBeat(text2, stringCount, where) {
|
|
|
704
724
|
return { cells, chord };
|
|
705
725
|
});
|
|
706
726
|
}
|
|
727
|
+
var STRAIGHT = [1, 2, 4, 8];
|
|
728
|
+
function uniformBeats(beats) {
|
|
729
|
+
const target = Math.max(0, ...beats.map((b) => b.length).filter((n) => STRAIGHT.includes(n)));
|
|
730
|
+
return beats.map((slots) => {
|
|
731
|
+
if (!STRAIGHT.includes(slots.length) || slots.length === target) return slots;
|
|
732
|
+
const step = target / slots.length;
|
|
733
|
+
return Array.from(
|
|
734
|
+
{ length: target },
|
|
735
|
+
(_, i) => i % step === 0 ? slots[i / step] : { cells: /* @__PURE__ */ new Map(), chord: null }
|
|
736
|
+
);
|
|
737
|
+
});
|
|
738
|
+
}
|
|
707
739
|
function buildMeasure(spec, stringCount, fallback, where = "measure") {
|
|
708
740
|
const time = spec.time ? parseTime(spec.time) : fallback;
|
|
709
741
|
if (spec.beats.length !== time.beats) {
|
|
@@ -711,7 +743,9 @@ function buildMeasure(spec, stringCount, fallback, where = "measure") {
|
|
|
711
743
|
`${where}: ${spec.beats.length} beats given but the time signature ${time.beats}/${time.beatUnit} has ${time.beats}`
|
|
712
744
|
);
|
|
713
745
|
}
|
|
714
|
-
const beats =
|
|
746
|
+
const beats = uniformBeats(
|
|
747
|
+
spec.beats.map((b, i) => parseBeat(b, stringCount, `${where} beat ${i + 1}`))
|
|
748
|
+
);
|
|
715
749
|
const measure = createMeasure(
|
|
716
750
|
stringCount,
|
|
717
751
|
time.beats,
|
|
@@ -729,6 +763,105 @@ function buildMeasure(spec, stringCount, fallback, where = "measure") {
|
|
|
729
763
|
}
|
|
730
764
|
return measure;
|
|
731
765
|
}
|
|
766
|
+
function setCellNote(prev, e, where) {
|
|
767
|
+
if (e.harm === "none" && e.harmAt != null) {
|
|
768
|
+
throw new Error(`${where}: harmAt cannot be set with harm "none"`);
|
|
769
|
+
}
|
|
770
|
+
const sameNote = cellFret(prev) === e.fret;
|
|
771
|
+
const harm = e.harm === "none" ? void 0 : e.harm ?? (sameNote ? cellHarm(prev) : void 0);
|
|
772
|
+
const keptAt = e.harmAt == null && harm != null && sameNote ? cellHarmAt(prev) : void 0;
|
|
773
|
+
let checked = checkHarmonic(e.fret, harm, e.harmAt ?? keptAt);
|
|
774
|
+
if (!checked.ok && keptAt != null) checked = checkHarmonic(e.fret, harm, void 0);
|
|
775
|
+
if (!checked.ok) throw new Error(`${where}: ${checked.reason}`);
|
|
776
|
+
return makeNoteCell(checked.value.fret, e.bend ?? 0, !!e.tie, !!e.pm, checked.value);
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
// eztab.ts
|
|
780
|
+
function normalize(data) {
|
|
781
|
+
return normalizeDocument(data);
|
|
782
|
+
}
|
|
783
|
+
function cellText(cell) {
|
|
784
|
+
if (cell === null) return "";
|
|
785
|
+
if (cell === "rest") return "R";
|
|
786
|
+
if (cell === "mute") return "x";
|
|
787
|
+
return noteText(cell);
|
|
788
|
+
}
|
|
789
|
+
function render(doc, from = 0, to = doc.measures.length - 1, track) {
|
|
790
|
+
const bodies = trackBodies(doc);
|
|
791
|
+
const metas = trackMetas(doc);
|
|
792
|
+
const multi = bodies.length > 1;
|
|
793
|
+
const main = bodies[0];
|
|
794
|
+
const shown = bodies.map((_, i) => i).filter((i) => track == null || i === track);
|
|
795
|
+
const lines = [];
|
|
796
|
+
lines.push(`Title: ${doc.title}${doc.artist ? ` \u2014 ${doc.artist}` : ""} tempo=${doc.tempo}`);
|
|
797
|
+
if (multi) {
|
|
798
|
+
lines.push(`Tracks (track index: name, tuning top\u2192bottom) measures=${main.measures.length}`);
|
|
799
|
+
metas.forEach((t, i) => {
|
|
800
|
+
lines.push(
|
|
801
|
+
` ${i}: ${t.name} ${bodies[i].tuning.join(" ")}${i === 0 ? " (main: carries the chords)" : ""}`
|
|
802
|
+
);
|
|
803
|
+
});
|
|
804
|
+
} else {
|
|
805
|
+
lines.push(`Tuning (top\u2192bottom): ${main.tuning.join(" ")} measures=${main.measures.length}`);
|
|
806
|
+
}
|
|
807
|
+
const ann = [];
|
|
808
|
+
main.measures.forEach((_, m) => {
|
|
809
|
+
const k = doc.keys.find((x) => x.measure === m);
|
|
810
|
+
const s = doc.sections.find((x) => x.measure === m);
|
|
811
|
+
if (k) ann.push(`m${m}:key=${k.tonic} ${k.mode}`);
|
|
812
|
+
if (s && s.label) ann.push(`m${m}:section="${s.label}"`);
|
|
813
|
+
});
|
|
814
|
+
if (ann.length) lines.push(`Markers: ${ann.join(", ")}`);
|
|
815
|
+
main.measures.forEach((m, mi) => {
|
|
816
|
+
if (mi < from || mi > to) return;
|
|
817
|
+
const key = activeKeyAt(doc.keys, mi);
|
|
818
|
+
lines.push("");
|
|
819
|
+
if (!multi) {
|
|
820
|
+
lines.push(
|
|
821
|
+
`--- measure ${mi} (bar ${mi + 1}) (${m.beats}/${m.beatUnit}, ${subText(m)}, ${columnCount(m)} cols, key ${key.tonic} ${key.mode}) ---`
|
|
822
|
+
);
|
|
823
|
+
lines.push(chordLine(m));
|
|
824
|
+
measureRows(lines, m, main.tuning);
|
|
825
|
+
return;
|
|
826
|
+
}
|
|
827
|
+
lines.push(
|
|
828
|
+
`--- measure ${mi} (bar ${mi + 1}) (${m.beats}/${m.beatUnit}, key ${key.tonic} ${key.mode}) ---`
|
|
829
|
+
);
|
|
830
|
+
lines.push(`${chordLine(m)} (columns of track 0)`);
|
|
831
|
+
for (const t of shown) {
|
|
832
|
+
const tm = bodies[t].measures[mi];
|
|
833
|
+
lines.push(`[track ${t}: ${metas[t].name}] ${subText(tm)}, ${columnCount(tm)} cols`);
|
|
834
|
+
measureRows(lines, tm, bodies[t].tuning);
|
|
835
|
+
}
|
|
836
|
+
});
|
|
837
|
+
return lines.join("\n");
|
|
838
|
+
}
|
|
839
|
+
function subText(m) {
|
|
840
|
+
const sub = uniformSubdivision(m);
|
|
841
|
+
return sub != null ? `${sub}/beat` : `[${m.subdivisions.join(",")}]/beat`;
|
|
842
|
+
}
|
|
843
|
+
function chordLine(m) {
|
|
844
|
+
const row = (m.chords ?? []).map((label, c) => label ? `c${c}:${chordDisplay(label)}` : null).filter(Boolean).join(" ");
|
|
845
|
+
return `chords: ${row || "(none)"}`;
|
|
846
|
+
}
|
|
847
|
+
function measureRows(lines, m, tuning) {
|
|
848
|
+
const texts = m.cells.map((row) => row.map(cellText));
|
|
849
|
+
const width = Math.max(2, ...texts.flat().map((t) => t.length));
|
|
850
|
+
const ruler = Array.from(
|
|
851
|
+
{ length: columnCount(m) },
|
|
852
|
+
(_, c) => String(c).padStart(width, " ")
|
|
853
|
+
).join(" ");
|
|
854
|
+
lines.push(`col: ${ruler}`);
|
|
855
|
+
texts.forEach((row, s) => {
|
|
856
|
+
const label = `${s + 1} ${tuning[s] ?? "?"}`.padEnd(6, " ");
|
|
857
|
+
lines.push(`${label}| ${row.map((t) => t.padStart(width, "-")).join(" ")}`);
|
|
858
|
+
});
|
|
859
|
+
const pm = Array.from(
|
|
860
|
+
{ length: columnCount(m) },
|
|
861
|
+
(_, c) => (m.cells.some((row) => cellPalmMute(row[c] ?? null)) ? "PM" : "").padStart(width, " ")
|
|
862
|
+
);
|
|
863
|
+
if (pm.some((x) => x.trim() !== "")) lines.push(`pm: ${pm.join(" ")}`);
|
|
864
|
+
}
|
|
732
865
|
|
|
733
866
|
// prompts.ts
|
|
734
867
|
var IMPORT_TAB_FROM_IMAGE = `Transcribe the TAB in the attached image(s) into an EZTab document with the eztab tools.
|
|
@@ -742,7 +875,7 @@ var IMPORT_TAB_FROM_IMAGE = `Transcribe the TAB in the attached image(s) into an
|
|
|
742
875
|
|
|
743
876
|
## 2. Write bar by bar with write_measures
|
|
744
877
|
Read each bar's rhythm first (beams, flags, dots, tuplet brackets, rests), then the fret numbers, then write it. Send a few bars per call, in order. In a fresh document the first bar goes to measure 0 (replacing the empty measure new_tab made); after that append at the current measure count.
|
|
745
|
-
- Subdivision per beat = the fewest slots that put every onset of the beat on a slot: quarter \u2192 1, eighths \u2192 2, 16ths or dotted-8th + 16th \u2192 4, triplet \u2192 3. Only onsets are stored; a held note is just empty slots ("-") after it.
|
|
878
|
+
- Subdivision per beat = the fewest slots that put every onset of the beat on a slot: quarter \u2192 1, eighths \u2192 2, 16ths or dotted-8th + 16th \u2192 4, triplet \u2192 3. Only onsets are stored; a held note is just empty slots ("-") after it. write_measures then evens out the straight beats of each measure to the finest one (a bar with any 16ths is stored all in 16ths; triplet / sextuplet beats stay as they are), so you do not need to pad beats yourself.
|
|
746
879
|
- Use the TAB numbers for frets, and the staff notation (when present) to settle rhythm and ambiguous digits. String 1 is the TOP line of the TAB.
|
|
747
880
|
- Chord names printed above the staff go on the chord row with @Name at the slot where the chord changes (usually beat 1).
|
|
748
881
|
- Rests: write S:r on the string that was ringing only when the line clearly stops (a rest symbol); otherwise leave "-".
|
|
@@ -753,7 +886,12 @@ Read each bar's rhythm first (beams, flags, dots, tuplet brackets, rests), then
|
|
|
753
886
|
- Choke / bend (C, Cho., "full", "1/2", up arrow): S:FB for full, S:Fb for \xBD, S:F^ for 1\xBD. A release / down choke: write the returning note tied.
|
|
754
887
|
- Bridge mute (M, P.M. bracket in Japanese band scores = \u30DF\u30E5\u30FC\u30C8): add p to every note under the bracket.
|
|
755
888
|
- Dead notes / brushing (x): S:x.
|
|
756
|
-
-
|
|
889
|
+
- Harmonics:
|
|
890
|
+
- Natural harmonic (fret in angle brackets <12>, <7>, "Harm.", "N.H.", a diamond notehead): S:<12>. Between frets (<2.4>, <3.2>, <5.8>): S:<2.4> as printed.
|
|
891
|
+
- Artificial harmonic (7<19>, "A.H.", "T.H." with the fretted note and the touch point): S:7<19>. Only the fretted note printed with "A.H." / "T.H.": S:7a.
|
|
892
|
+
- Pinch harmonic ("P.H." bracket, like P.M.): add h to every note under it, S:7h; with a printed position, S:7<19>h.
|
|
893
|
+
- "AH" / "A.H." is used for both artificial and pinch harmonics. Decide from context: a picked note under a P.H.-like bracket or marked over a distorted riff is pinch (h); a tapped / touched octave (typically +12) on a clean part is artificial. If unsure, choose one and list those bars in the report.
|
|
894
|
+
- Vibrato, slide-in from nowhere (/6), slide-out, arpeggio wavy line, grace notes: write the main note only.
|
|
757
895
|
- Chord diagrams / chord-form boxes drawn inside the TAB staff (no fret numbers): write the chord name on the chord row. Write the voicing as notes only if the user asks or the diagram is unambiguous.
|
|
758
896
|
- Repeats, D.S., Coda, 1st/2nd endings: transcribe the bars as printed and tell the user; do not expand them unless asked.
|
|
759
897
|
|
|
@@ -769,7 +907,7 @@ var state = {
|
|
|
769
907
|
source: null
|
|
770
908
|
};
|
|
771
909
|
var server = new McpServer(
|
|
772
|
-
{ name: "eztab", version: "0.
|
|
910
|
+
{ name: "eztab", version: "0.4.1" },
|
|
773
911
|
{
|
|
774
912
|
instructions: "All indices are 0-based: measure 0 is the first measure, string 0 is the top (highest) string, and column 0 is the first column. Notes and chords share the same column grid. Subdivisions are per beat and can differ within a measure (e.g. [2,2,3,2] mixes a triplet beat into straight eighths), so columns per measure = the sum of its per-beat subdivisions; get_tab's ruler shows the column indices. add_measures `at` is a 0-based insert position too. No need to ask the user about indexing. Exception: write_measures uses guitar string numbers (1 = top line), matching the labels get_tab prints. To write whole measures (e.g. transcribing a score or an image), prefer write_measures over set_cells \u2014 it takes one string per beat and does the column arithmetic for you. For transcribing from an image, follow the import_tab_from_image prompt. To deliver a result to the EZTab app, call send_to_eztab; to fix an existing tab, list_tabs \u2192 open_tab \u2192 edit \u2192 send_to_eztab. A tab can hold up to 4 tracks (parts, e.g. guitar + bass); track 0 is the main track. Tools that edit notes take an optional 0-based `track` (default 0). Measures and time signatures are shared by all tracks (adding/removing a measure affects every track); subdivisions are per track; chords belong to track 0 and use its columns."
|
|
775
913
|
}
|
|
@@ -969,7 +1107,7 @@ server.tool(
|
|
|
969
1107
|
);
|
|
970
1108
|
server.tool(
|
|
971
1109
|
"set_cells",
|
|
972
|
-
"Set tab cells. value: a fret number (0-24), 'rest' (stops a sustaining note), 'mute' (a muted/dead 'x' stroke for cutting), or 'clear' (empty = previous note sustains through). Optional `bend` (0.5, 1, or 1.5 semitones) chokes a fret-number note. Optional `tie` (boolean) ties/slurs a fret-number note to the previous note on the same string \u2014 a matching pitch just sustains (no re-pick), a different pitch is a slur. Optional `pm` (boolean) palm-mutes (bridge-mutes) a fret-number note. measure/string/column are all 0-based (measure 0 = the first measure, string 0 = the top line).",
|
|
1110
|
+
"Set tab cells. value: a fret number (0-24), 'rest' (stops a sustaining note), 'mute' (a muted/dead 'x' stroke for cutting), or 'clear' (empty = previous note sustains through). Optional `bend` (0.5, 1, or 1.5 semitones) chokes a fret-number note. Optional `tie` (boolean) ties/slurs a fret-number note to the previous note on the same string \u2014 a matching pitch just sustains (no re-pick), a different pitch is a slur. Optional `pm` (boolean) palm-mutes (bridge-mutes) a fret-number note. Optional `harm` makes it a harmonic: 'natural' (touch the open string at `harmAt`, or at the fret when omitted \u2014 `harmAt` may be between frets, e.g. 2.4, and then the fret is its rounded value), 'artificial' / 'pinch' (fret the note, harmonic at `harmAt` above it; pinch without `harmAt` = wherever it is picked), or 'none' to remove it. When `harm` is omitted and the cell already holds a harmonic at the same fret, the harmonic is kept. measure/string/column are all 0-based (measure 0 = the first measure, string 0 = the top line).",
|
|
973
1111
|
{
|
|
974
1112
|
edits: z.array(
|
|
975
1113
|
z.object({
|
|
@@ -979,7 +1117,9 @@ server.tool(
|
|
|
979
1117
|
value: z.union([z.number().int().min(0).max(24), z.enum(["rest", "mute", "clear"])]),
|
|
980
1118
|
bend: z.union([z.literal(0.5), z.literal(1), z.literal(1.5)]).optional(),
|
|
981
1119
|
tie: z.boolean().optional(),
|
|
982
|
-
pm: z.boolean().optional()
|
|
1120
|
+
pm: z.boolean().optional(),
|
|
1121
|
+
harm: z.enum(["natural", "artificial", "pinch", "none"]).optional(),
|
|
1122
|
+
harmAt: z.number().optional()
|
|
983
1123
|
})
|
|
984
1124
|
),
|
|
985
1125
|
track: trackParam
|
|
@@ -997,22 +1137,25 @@ server.tool(
|
|
|
997
1137
|
`column ${e.column} out of range for measure ${e.measure} (0..${cols - 1})`
|
|
998
1138
|
);
|
|
999
1139
|
}
|
|
1000
|
-
|
|
1001
|
-
if (typeof e.value
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
};
|
|
1140
|
+
const where = `measure ${e.measure} string ${e.string} column ${e.column}`;
|
|
1141
|
+
if (typeof e.value !== "number") {
|
|
1142
|
+
if (e.harm != null || e.harmAt != null) {
|
|
1143
|
+
throw new Error(`${where}: harm / harmAt need a fret-number value`);
|
|
1144
|
+
}
|
|
1145
|
+
m.cells[e.string][e.column] = e.value === "clear" ? null : e.value;
|
|
1146
|
+
continue;
|
|
1008
1147
|
}
|
|
1009
|
-
m.cells[e.string][e.column] =
|
|
1148
|
+
m.cells[e.string][e.column] = setCellNote(
|
|
1149
|
+
m.cells[e.string][e.column],
|
|
1150
|
+
{ fret: e.value, bend: e.bend, tie: e.tie, pm: e.pm, harm: e.harm, harmAt: e.harmAt },
|
|
1151
|
+
where
|
|
1152
|
+
);
|
|
1010
1153
|
}
|
|
1011
1154
|
}, a.track)
|
|
1012
1155
|
);
|
|
1013
1156
|
server.tool(
|
|
1014
1157
|
"write_measures",
|
|
1015
|
-
'Write whole measures, one text string per beat \u2014 the easiest way to transcribe a score. Each listed measure is replaced entirely (notes, chords, per-beat subdivisions). `measure` is the 0-based index; using the current measure count appends, so a transcription can be written in order without add_measures. `time` (e.g. "4/4", "6/8") is optional: it defaults to the measure being replaced, or the previous measure when appending. `beats` has exactly one string per beat of the time signature.\n\nA beat string is space-separated slots; the slot count is that beat\'s subdivision (1 = quarter, 2 = eighths, 3 = eighth triplet, 4 = sixteenths, 6 = sixteenth triplet \u2026). Pick the smallest count that puts every onset of the beat on a slot (a dotted eighth + sixteenth = 4 slots: note - - note). A slot is "-" (nothing starts there; the previous note rings on) or items joined by "+":\n S:F string S (1 = top line of the TAB, as printed) at fret F\n S:~F tied / slurred from the previous note on that string (hammer-on, pull-off, slide into F, or a tie when F is the same fret)\n S:Fb / S:FB / S:F^ choke (bend) \xBD / 1 / 1\xBD steps\n S:Fp palm (bridge) mute; combines, e.g. S:~5p, S:7Bp\n S:x muted dead-note stroke (brushing)\n S:r rest: stops the note ringing on that string\n @Name chord name onset for the chord row (e.g. @Am7, @N.C.)\nExamples: ["6:5 6:7", "5:5 5:7", "4:5 - 4:7", "@F+4:3+3:2+2:1+1:1"].',
|
|
1158
|
+
'Write whole measures, one text string per beat \u2014 the easiest way to transcribe a score. Each listed measure is replaced entirely (notes, chords, per-beat subdivisions). `measure` is the 0-based index; using the current measure count appends, so a transcription can be written in order without add_measures. `time` (e.g. "4/4", "6/8") is optional: it defaults to the measure being replaced, or the previous measure when appending. `beats` has exactly one string per beat of the time signature.\n\nA beat string is space-separated slots; the slot count is that beat\'s subdivision (1 = quarter, 2 = eighths, 3 = eighth triplet, 4 = sixteenths, 6 = sixteenth triplet \u2026). Pick the smallest count that puts every onset of the beat on a slot (a dotted eighth + sixteenth = 4 slots: note - - note). Within a measure the straight beats are evened out to the finest one (a quarter-note beat next to a sixteenth beat is stored as 4 slots), so write each beat as is; tuplet beats keep their own count. A slot is "-" (nothing starts there; the previous note rings on) or items joined by "+":\n S:F string S (1 = top line of the TAB, as printed) at fret F\n S:~F tied / slurred from the previous note on that string (hammer-on, pull-off, slide into F, or a tie when F is the same fret)\n S:Fb / S:FB / S:F^ choke (bend) \xBD / 1 / 1\xBD steps\n S:Fp palm (bridge) mute; combines, e.g. S:~5p, S:7Bp\n S:<P> natural harmonic touched at fret P (S:<12>, S:<7>; between frets S:<2.4>)\n S:F<P> artificial harmonic: fret F, harmonic at P (S:7<19>); S:Fa = artificial, position not written\n S:Fh pinch harmonic (P.H.); S:F<P>h = pinch harmonic at P (S:7<19>h)\n S:x muted dead-note stroke (brushing)\n S:r rest: stops the note ringing on that string\n @Name chord name onset for the chord row (e.g. @Am7, @N.C.)\nExamples: ["6:5 6:7", "5:5 5:7", "4:5 - 4:7", "@F+4:3+3:2+2:1+1:1"].',
|
|
1016
1159
|
{
|
|
1017
1160
|
measures: z.array(
|
|
1018
1161
|
z.object({
|