@hidechae/eztab-mcp 0.6.0 → 0.7.0
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 +11 -0
- package/dist/server.js +188 -6
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -103,6 +103,7 @@ Run standalone for testing: `task mcp` (or `npx tsx mcp/server.ts`).
|
|
|
103
103
|
| `set_tuning` | Replace tuning / string count (`track` で対象パートを指定) |
|
|
104
104
|
| `add_track` / `remove_track` | パート(ベース・2本目のギターなど)の追加・削除。メインを含めて最大4、track 0(メイン)は削除不可 |
|
|
105
105
|
| `set_tone` | パートの再生音色を設定する(`track` で対象パートを指定)。`add_track` でも `tone` を指定できる |
|
|
106
|
+
| `set_backing_style` | 伴奏(コード)の音を設定する: `pad`(既定)/ `guitar` / `piano` / `bass`(ルートのみ) |
|
|
106
107
|
|
|
107
108
|
### 複数パート(トラック)
|
|
108
109
|
|
|
@@ -125,10 +126,20 @@ Run standalone for testing: `task mcp` (or `npx tsx mcp/server.ts`).
|
|
|
125
126
|
- 値は `clean`(既定)/ `crunch` / `distortion` / `acoustic` / `piano` / `synth` / `strings`
|
|
126
127
|
- 値の意味はパートの楽器で変わる。名前かチューニングでベースと判定されるパートでは、
|
|
127
128
|
`crunch` はオーバードライブ、`acoustic` はウッドベース、`synth` はシンセベースとして鳴る
|
|
129
|
+
- ベースと判定されるパートに設定できるのは `clean` / `crunch` / `acoustic` / `synth` だけ(0.6.1以降。
|
|
130
|
+
それ以外は `set_tone` / `add_track` がエラーを返す)
|
|
128
131
|
- `get_tab` はクリーン以外の音色をパート一覧に `[distortion]` のように表示する
|
|
129
132
|
(1パートのタブでは `tone=distortion`)
|
|
130
133
|
- 0.5.2以前のMCPは、クリーン以外の音色を持つタブを編集・送信できない
|
|
131
134
|
|
|
135
|
+
### 伴奏の音
|
|
136
|
+
|
|
137
|
+
0.7.0以降は伴奏(コード)の音(docs/design/0011-backing-styles.md)を `set_backing_style` で設定できます。
|
|
138
|
+
|
|
139
|
+
- `pad`(既定。従来の音)/ `guitar` / `piano` / `bass`(コードの構成音を鳴らさず、ルートだけをベースで弾く。分数コードは分母の音)
|
|
140
|
+
- `get_tab` はパッド以外のとき、先頭行に `backing=guitar` のように表示する
|
|
141
|
+
- 形式リビジョンは変わらない。0.6.1以前のMCPでも、設定されたタブを編集・送信でき、値は保持される
|
|
142
|
+
|
|
132
143
|
### 小節メモの保持
|
|
133
144
|
|
|
134
145
|
0.5.1以降はアプリで入力した小節共通メモ(形式リビジョン5)を保持して編集・送信できます。
|
package/dist/server.js
CHANGED
|
@@ -28,6 +28,15 @@ function normalizeTone(raw) {
|
|
|
28
28
|
return raw !== "clean" && TONES.includes(raw) ? raw : void 0;
|
|
29
29
|
}
|
|
30
30
|
var DEFAULT_MAIN_TRACK_NAME = "Guitar";
|
|
31
|
+
var BACKING_STYLES = ["pad", "guitar", "piano", "bass"];
|
|
32
|
+
function normalizeBackingStyle(raw) {
|
|
33
|
+
return raw !== "pad" && BACKING_STYLES.includes(raw) ? raw : void 0;
|
|
34
|
+
}
|
|
35
|
+
function withBackingStyle(doc, style) {
|
|
36
|
+
const { backingStyle: _old, ...rest } = doc;
|
|
37
|
+
const stored = normalizeBackingStyle(style);
|
|
38
|
+
return stored ? { ...rest, backingStyle: stored } : rest;
|
|
39
|
+
}
|
|
31
40
|
var DEFAULT_TEMPO = 120;
|
|
32
41
|
var MIN_TEMPO = 20;
|
|
33
42
|
var MAX_TEMPO = 400;
|
|
@@ -328,6 +337,9 @@ function normalizeDocument(data) {
|
|
|
328
337
|
if (!data.mix || typeof data.mix.melody !== "number") {
|
|
329
338
|
data.mix = { ...DEFAULT_MIX };
|
|
330
339
|
}
|
|
340
|
+
const backingStyle = normalizeBackingStyle(data.backingStyle);
|
|
341
|
+
if (backingStyle) data.backingStyle = backingStyle;
|
|
342
|
+
else delete data.backingStyle;
|
|
331
343
|
normalizeTracks(data);
|
|
332
344
|
data.version = TAB_FILE_VERSION;
|
|
333
345
|
return data;
|
|
@@ -503,6 +515,31 @@ function setBeatSubdivision(measure, beat, subdivision) {
|
|
|
503
515
|
return remapToSubdivisions(measure, subs);
|
|
504
516
|
}
|
|
505
517
|
|
|
518
|
+
// ../src/lib/music.ts
|
|
519
|
+
var LETTER_PC = {
|
|
520
|
+
C: 0,
|
|
521
|
+
D: 2,
|
|
522
|
+
E: 4,
|
|
523
|
+
F: 5,
|
|
524
|
+
G: 7,
|
|
525
|
+
A: 9,
|
|
526
|
+
B: 11
|
|
527
|
+
};
|
|
528
|
+
function noteToMidi(note) {
|
|
529
|
+
const m = /^([A-Ga-g])([#♯b♭]*)(-?\d+)?$/.exec(note.trim());
|
|
530
|
+
if (!m) return null;
|
|
531
|
+
let pc = LETTER_PC[m[1].toUpperCase()];
|
|
532
|
+
for (const ch of m[2]) {
|
|
533
|
+
if (ch === "#" || ch === "\u266F") pc += 1;
|
|
534
|
+
if (ch === "b" || ch === "\u266D") pc -= 1;
|
|
535
|
+
}
|
|
536
|
+
const octave = m[3] != null ? parseInt(m[3], 10) : 4;
|
|
537
|
+
return (octave + 1) * 12 + pc;
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
// ../src/lib/playbackRate.ts
|
|
541
|
+
var PLAYBACK_RATES = Array.from({ length: 36 }, (_, i) => (5 + i) / 20);
|
|
542
|
+
|
|
506
543
|
// ../src/lib/tracks.ts
|
|
507
544
|
function newUid() {
|
|
508
545
|
return newTabId();
|
|
@@ -742,6 +779,128 @@ function storedSize(doc) {
|
|
|
742
779
|
return new TextEncoder().encode(JSON.stringify(closeWorkspace(doc))).length;
|
|
743
780
|
}
|
|
744
781
|
|
|
782
|
+
// ../src/lib/player.ts
|
|
783
|
+
function instrumentOf(tuning, name = "") {
|
|
784
|
+
if (/bass|ベース/i.test(name)) return "bass";
|
|
785
|
+
const open = tuning.map(noteToMidi).filter((m) => m != null);
|
|
786
|
+
return open.length > 0 && Math.max(...open) < 50 ? "bass" : "guitar";
|
|
787
|
+
}
|
|
788
|
+
var BUFFER_BUDGET = 12e6;
|
|
789
|
+
var PREPARE_CAPACITY = BUFFER_BUDGET / 2;
|
|
790
|
+
var OPEN = 2e4;
|
|
791
|
+
var GUITAR_CLEAN = {
|
|
792
|
+
pickupHz: 4500,
|
|
793
|
+
pickupQ: 4,
|
|
794
|
+
tightHz: 20,
|
|
795
|
+
pushDb: 0,
|
|
796
|
+
drive: 1,
|
|
797
|
+
stage1: null,
|
|
798
|
+
drive2: 1,
|
|
799
|
+
stage2: null,
|
|
800
|
+
interHpHz: 20,
|
|
801
|
+
interLpHz: OPEN,
|
|
802
|
+
bodyHz: 110,
|
|
803
|
+
bodyDb: 0,
|
|
804
|
+
cabHz: OPEN,
|
|
805
|
+
cabQ: 0,
|
|
806
|
+
lowCutHz: 70,
|
|
807
|
+
level: 1
|
|
808
|
+
};
|
|
809
|
+
var BASS_CLEAN = { ...GUITAR_CLEAN, pickupHz: 2600, pickupQ: 1, lowCutHz: 30 };
|
|
810
|
+
var DIRECT = { ...GUITAR_CLEAN, pickupHz: 16e3, pickupQ: 0, lowCutHz: 30 };
|
|
811
|
+
var PIANO = { ...DIRECT, pickupHz: 7e3, bodyHz: 220, bodyDb: 3, level: 1.3 };
|
|
812
|
+
var CRUNCH = { gain: 16, bias: 0.3 };
|
|
813
|
+
var CHAINS = {
|
|
814
|
+
guitar: {
|
|
815
|
+
clean: GUITAR_CLEAN,
|
|
816
|
+
crunch: {
|
|
817
|
+
...GUITAR_CLEAN,
|
|
818
|
+
tightHz: 120,
|
|
819
|
+
pushDb: 2,
|
|
820
|
+
drive: 0.5,
|
|
821
|
+
stage1: CRUNCH,
|
|
822
|
+
bodyHz: 2800,
|
|
823
|
+
bodyDb: 2,
|
|
824
|
+
cabHz: 5200,
|
|
825
|
+
cabQ: 1,
|
|
826
|
+
lowCutHz: 80,
|
|
827
|
+
level: 0.26
|
|
828
|
+
},
|
|
829
|
+
distortion: {
|
|
830
|
+
...GUITAR_CLEAN,
|
|
831
|
+
pickupQ: 2,
|
|
832
|
+
tightHz: 250,
|
|
833
|
+
pushDb: 6,
|
|
834
|
+
drive: 0.5,
|
|
835
|
+
stage1: { gain: 24, bias: 0.25 },
|
|
836
|
+
// The next tube stage is inverted, so it clips the other half harder.
|
|
837
|
+
drive2: 1,
|
|
838
|
+
stage2: { gain: 5, bias: -0.35 },
|
|
839
|
+
interHpHz: 160,
|
|
840
|
+
interLpHz: 6500,
|
|
841
|
+
bodyHz: 2800,
|
|
842
|
+
bodyDb: 4,
|
|
843
|
+
cabHz: 4800,
|
|
844
|
+
cabQ: 3,
|
|
845
|
+
lowCutHz: 90,
|
|
846
|
+
level: 0.17
|
|
847
|
+
},
|
|
848
|
+
acoustic: { ...GUITAR_CLEAN, pickupHz: 9e3, pickupQ: 0, bodyDb: 5, level: 1.2 },
|
|
849
|
+
piano: PIANO,
|
|
850
|
+
synth: { ...DIRECT, level: 0.8 },
|
|
851
|
+
strings: { ...DIRECT, level: 0.8 }
|
|
852
|
+
},
|
|
853
|
+
bass: {
|
|
854
|
+
clean: BASS_CLEAN,
|
|
855
|
+
// Overdrive.
|
|
856
|
+
crunch: {
|
|
857
|
+
...BASS_CLEAN,
|
|
858
|
+
tightHz: 40,
|
|
859
|
+
pushDb: 3,
|
|
860
|
+
drive: 0.5,
|
|
861
|
+
stage1: { gain: 22, bias: 0.3 },
|
|
862
|
+
cabHz: 3500,
|
|
863
|
+
cabQ: 1,
|
|
864
|
+
level: 0.24
|
|
865
|
+
},
|
|
866
|
+
// The mixer offers a bass only the tones in tonesFor(); these three still
|
|
867
|
+
// play if a tab stores them (a guitar track retuned to a bass, say).
|
|
868
|
+
distortion: {
|
|
869
|
+
...BASS_CLEAN,
|
|
870
|
+
tightHz: 60,
|
|
871
|
+
pushDb: 4,
|
|
872
|
+
drive: 0.5,
|
|
873
|
+
stage1: { gain: 24, bias: 0.25 },
|
|
874
|
+
stage2: { gain: 5, bias: -0.35 },
|
|
875
|
+
interHpHz: 50,
|
|
876
|
+
interLpHz: 5e3,
|
|
877
|
+
cabHz: 3e3,
|
|
878
|
+
cabQ: 2,
|
|
879
|
+
lowCutHz: 35,
|
|
880
|
+
level: 0.15
|
|
881
|
+
},
|
|
882
|
+
// Upright bass.
|
|
883
|
+
acoustic: {
|
|
884
|
+
...BASS_CLEAN,
|
|
885
|
+
pickupHz: 3e3,
|
|
886
|
+
pickupQ: 0,
|
|
887
|
+
bodyHz: 100,
|
|
888
|
+
bodyDb: 6,
|
|
889
|
+
level: 1.4
|
|
890
|
+
},
|
|
891
|
+
piano: PIANO,
|
|
892
|
+
// Synth bass.
|
|
893
|
+
synth: { ...DIRECT, level: 0.8 },
|
|
894
|
+
strings: { ...DIRECT, level: 0.8 }
|
|
895
|
+
}
|
|
896
|
+
};
|
|
897
|
+
|
|
898
|
+
// ../src/lib/tones.ts
|
|
899
|
+
var BASS_TONES = ["clean", "crunch", "acoustic", "synth"];
|
|
900
|
+
function tonesFor(instrument) {
|
|
901
|
+
return instrument === "bass" ? BASS_TONES : TONES;
|
|
902
|
+
}
|
|
903
|
+
|
|
745
904
|
// ../src/lib/config.ts
|
|
746
905
|
var CLOUD_BASE_URL = "https://eztab-sync-736328841982.asia-northeast1.run.app";
|
|
747
906
|
|
|
@@ -953,7 +1112,9 @@ function render(doc, from = 0, to = doc.measures.length - 1, track) {
|
|
|
953
1112
|
const main = bodies[0];
|
|
954
1113
|
const shown = bodies.map((_, i) => i).filter((i) => track == null || i === track);
|
|
955
1114
|
const lines = [];
|
|
956
|
-
lines.push(
|
|
1115
|
+
lines.push(
|
|
1116
|
+
`Title: ${doc.title}${doc.artist ? ` \u2014 ${doc.artist}` : ""} tempo=${doc.tempo}${doc.backingStyle ? ` backing=${doc.backingStyle}` : ""}`
|
|
1117
|
+
);
|
|
957
1118
|
if (multi) {
|
|
958
1119
|
lines.push(`Tracks (track index: name, tuning top\u2192bottom) measures=${main.measures.length}`);
|
|
959
1120
|
metas.forEach((t, i) => {
|
|
@@ -1073,7 +1234,7 @@ var state = {
|
|
|
1073
1234
|
source: null
|
|
1074
1235
|
};
|
|
1075
1236
|
var server = new McpServer(
|
|
1076
|
-
{ name: "eztab", version: "0.
|
|
1237
|
+
{ name: "eztab", version: "0.7.0" },
|
|
1077
1238
|
{
|
|
1078
1239
|
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."
|
|
1079
1240
|
}
|
|
@@ -1563,17 +1724,38 @@ server.tool(
|
|
|
1563
1724
|
}, a.track)
|
|
1564
1725
|
);
|
|
1565
1726
|
var toneParam = z.enum(TONES).describe(
|
|
1566
|
-
"How the track sounds in playback.
|
|
1727
|
+
"How the track sounds in playback. Guitar tracks: clean, crunch (light overdrive), distortion, acoustic, piano, synth, strings. Bass tracks (named or tuned like a bass): clean, crunch (overdrive), acoustic (upright bass), synth (synth bass) only."
|
|
1567
1728
|
);
|
|
1729
|
+
function withTone(doc, track, tone) {
|
|
1730
|
+
const instrument = instrumentOf(trackBodies(doc)[track].tuning, trackMetas(doc)[track].name);
|
|
1731
|
+
const offered = tonesFor(instrument);
|
|
1732
|
+
if (!offered.includes(tone)) {
|
|
1733
|
+
throw new Error(
|
|
1734
|
+
`a ${instrument} track can't use the "${tone}" tone; choose one of: ${offered.join(", ")}`
|
|
1735
|
+
);
|
|
1736
|
+
}
|
|
1737
|
+
return setTrackTone(doc, track, tone);
|
|
1738
|
+
}
|
|
1568
1739
|
server.tool(
|
|
1569
1740
|
"set_tone",
|
|
1570
|
-
"Set a track's playback tone
|
|
1741
|
+
"Set a track's playback tone. Only the sound changes; the notes stay as written.",
|
|
1571
1742
|
{ tone: toneParam, track: trackParam },
|
|
1572
1743
|
async (a) => {
|
|
1573
1744
|
const doc = edit(0, () => {
|
|
1574
1745
|
});
|
|
1575
1746
|
onTrack(doc, a.track);
|
|
1576
|
-
state.doc =
|
|
1747
|
+
state.doc = withTone(doc, a.track ?? 0, a.tone);
|
|
1748
|
+
return text(render(state.doc));
|
|
1749
|
+
}
|
|
1750
|
+
);
|
|
1751
|
+
server.tool(
|
|
1752
|
+
"set_backing_style",
|
|
1753
|
+
"Set how the chord backing (the chords written with set_chords / @Name) sounds in playback: pad (the default soft pad), guitar, piano, or bass (only the root \u2014 or a slash chord's bass note \u2014 on a bass).",
|
|
1754
|
+
{ style: z.enum(BACKING_STYLES) },
|
|
1755
|
+
async (a) => {
|
|
1756
|
+
const doc = edit(0, () => {
|
|
1757
|
+
});
|
|
1758
|
+
state.doc = withBackingStyle(doc, a.style);
|
|
1577
1759
|
return text(render(state.doc));
|
|
1578
1760
|
}
|
|
1579
1761
|
);
|
|
@@ -1603,7 +1785,7 @@ server.tool(
|
|
|
1603
1785
|
if (trackMetas(added).length === before) {
|
|
1604
1786
|
throw new Error("a tab can hold at most 4 tracks");
|
|
1605
1787
|
}
|
|
1606
|
-
const next =
|
|
1788
|
+
const next = withTone(added, before, a.tone ?? "clean");
|
|
1607
1789
|
state.doc = next;
|
|
1608
1790
|
return text(`Added track ${before} "${a.name}".
|
|
1609
1791
|
|