@hidechae/eztab-mcp 0.4.1 → 0.5.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 +11 -3
- package/dist/server.js +157 -16
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -96,7 +96,7 @@ Run standalone for testing: `task mcp` (or `npx tsx mcp/server.ts`).
|
|
|
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") |
|
|
99
|
-
| `set_tempo` | Set
|
|
99
|
+
| `set_tempo` / `remove_tempo` | Set the start BPM, or set/remove a tempo change at a measure |
|
|
100
100
|
| `set_time_signature` | Set a measure's time signature |
|
|
101
101
|
| `set_resolution` | Set a measure's subdivisions per beat (8th/16th/triplets/quintuplets) |
|
|
102
102
|
| `add_measures` / `remove_measure` | Insert/remove measures (keys & sections re-anchored) |
|
|
@@ -113,8 +113,16 @@ Run standalone for testing: `task mcp` (or `npx tsx mcp/server.ts`).
|
|
|
113
113
|
- コードは track 0 のもの(`set_chords` は track 0 の列で指定)。`write_measures(track > 0)` ではコード指定と、
|
|
114
114
|
既存小節の拍子を変える `time` を拒否する
|
|
115
115
|
- `get_tab` は全パートを小節ごとに並べて表示する(`track` で1パートに絞れる)
|
|
116
|
-
- `send_to_eztab`
|
|
117
|
-
|
|
116
|
+
- `send_to_eztab` は対応する最新の形式リビジョンをヘッダーに付けて送る(0.5.1では `5`)。
|
|
117
|
+
対応上限を超える形式のタブは閲覧のみで、編集・送信できない。
|
|
118
|
+
0.2.0以前のMCPは複数パートのタブを送り返せない(サーバが409で拒否する)。
|
|
119
|
+
|
|
120
|
+
### 小節メモの保持
|
|
121
|
+
|
|
122
|
+
0.5.1以降はアプリで入力した小節共通メモ(形式リビジョン5)を保持して編集・送信できます。
|
|
123
|
+
メモ専用の入力ツールはなく、追加・変更・削除はアプリの「小節メモ…」で行います。
|
|
124
|
+
`write_measures` で既存小節を書き換えた場合もメモを残します。小節削除ではその小節のメモも削除し、
|
|
125
|
+
挿入した空小節はメモなしになります。旧MCPはメモのあるタブを編集・送信できません。
|
|
118
126
|
|
|
119
127
|
These cover the editor's current model: variable string count, per-beat chords,
|
|
120
128
|
keys with mid-piece changes, section markers, per-measure resolution, time
|
package/dist/server.js
CHANGED
|
@@ -7,12 +7,16 @@ 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 = 5;
|
|
11
11
|
var MULTI_TRACK_FORMAT_REVISION = 2;
|
|
12
12
|
var HARMONICS_FORMAT_REVISION = 3;
|
|
13
|
+
var TEMPO_CHANGES_FORMAT_REVISION = 4;
|
|
14
|
+
var MEASURE_MEMOS_FORMAT_REVISION = 5;
|
|
13
15
|
var MAX_TRACKS = 4;
|
|
14
16
|
var DEFAULT_MAIN_TRACK_NAME = "Guitar";
|
|
15
17
|
var DEFAULT_TEMPO = 120;
|
|
18
|
+
var MIN_TEMPO = 20;
|
|
19
|
+
var MAX_TEMPO = 400;
|
|
16
20
|
var DEFAULT_MIX = { melody: 1, backing: 0.4 };
|
|
17
21
|
function shiftSectionsForInsert(sections, index) {
|
|
18
22
|
return sections.map((s) => s.measure >= index ? { ...s, measure: s.measure + 1 } : s);
|
|
@@ -28,6 +32,68 @@ function activeKeyAt(keys, measure) {
|
|
|
28
32
|
}
|
|
29
33
|
return result;
|
|
30
34
|
}
|
|
35
|
+
function tempoSourceAt(doc, measure) {
|
|
36
|
+
let result = 0;
|
|
37
|
+
for (const t of doc.tempoChanges ?? []) {
|
|
38
|
+
if (t.measure <= measure) result = t.measure;
|
|
39
|
+
else break;
|
|
40
|
+
}
|
|
41
|
+
return result;
|
|
42
|
+
}
|
|
43
|
+
function validTempo(raw) {
|
|
44
|
+
if (typeof raw !== "number" || !Number.isFinite(raw)) return null;
|
|
45
|
+
const bpm = Math.round(raw);
|
|
46
|
+
return bpm >= MIN_TEMPO && bpm <= MAX_TEMPO ? bpm : null;
|
|
47
|
+
}
|
|
48
|
+
function normalizeTempoChanges(tempo, raw, measureCount) {
|
|
49
|
+
if (!Array.isArray(raw)) return void 0;
|
|
50
|
+
const byMeasure = /* @__PURE__ */ new Map();
|
|
51
|
+
for (const t of raw) {
|
|
52
|
+
const measure = t?.measure;
|
|
53
|
+
const bpm = validTempo(t?.bpm);
|
|
54
|
+
if (!Number.isInteger(measure) || measure < 1 || measure >= measureCount || bpm === null) {
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
byMeasure.set(measure, bpm);
|
|
58
|
+
}
|
|
59
|
+
const out = [];
|
|
60
|
+
let prev = tempo;
|
|
61
|
+
for (const measure of [...byMeasure.keys()].sort((a, b) => a - b)) {
|
|
62
|
+
const bpm = byMeasure.get(measure);
|
|
63
|
+
if (bpm === prev) continue;
|
|
64
|
+
out.push({ measure, bpm });
|
|
65
|
+
prev = bpm;
|
|
66
|
+
}
|
|
67
|
+
return out.length > 0 ? out : void 0;
|
|
68
|
+
}
|
|
69
|
+
function withNormalizedTempoChanges(doc) {
|
|
70
|
+
const changes = normalizeTempoChanges(doc.tempo, doc.tempoChanges, doc.measures.length);
|
|
71
|
+
const { tempoChanges: _t, ...rest } = doc;
|
|
72
|
+
return changes ? { ...rest, tempoChanges: changes } : rest;
|
|
73
|
+
}
|
|
74
|
+
function shiftTempoChangesForInsert(doc, index) {
|
|
75
|
+
if (!doc.tempoChanges) return doc;
|
|
76
|
+
return {
|
|
77
|
+
...doc,
|
|
78
|
+
tempoChanges: doc.tempoChanges.map(
|
|
79
|
+
(t) => t.measure >= index ? { ...t, measure: t.measure + 1 } : t
|
|
80
|
+
)
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
function shiftTempoChangesForDelete(doc, index) {
|
|
84
|
+
if (!doc.tempoChanges) return doc;
|
|
85
|
+
let tempo = doc.tempo;
|
|
86
|
+
const changes = [];
|
|
87
|
+
for (const t of doc.tempoChanges) {
|
|
88
|
+
if (t.measure === index) continue;
|
|
89
|
+
const measure = t.measure > index ? t.measure - 1 : t.measure;
|
|
90
|
+
if (measure === 0) tempo = t.bpm;
|
|
91
|
+
else changes.push({ ...t, measure });
|
|
92
|
+
}
|
|
93
|
+
const canonical = normalizeTempoChanges(tempo, changes, Number.POSITIVE_INFINITY);
|
|
94
|
+
const { tempoChanges: _t, ...rest } = doc;
|
|
95
|
+
return canonical ? { ...rest, tempo, tempoChanges: canonical } : { ...rest, tempo };
|
|
96
|
+
}
|
|
31
97
|
function shiftKeysForInsert(keys, index) {
|
|
32
98
|
const shifted = keys.map((k) => k.measure >= index ? { ...k, measure: k.measure + 1 } : k);
|
|
33
99
|
if (!shifted.some((k) => k.measure === 0)) {
|
|
@@ -128,6 +194,15 @@ function makeNoteCell(fret, bend, tie, pm, harmonic) {
|
|
|
128
194
|
...harm && harmonic?.harmAt != null ? { harmAt: harmonic.harmAt } : {}
|
|
129
195
|
};
|
|
130
196
|
}
|
|
197
|
+
var MAX_MEASURE_MEMO_LENGTH = 1e3;
|
|
198
|
+
function normalizeMeasureMemo(raw) {
|
|
199
|
+
if (typeof raw !== "string") return void 0;
|
|
200
|
+
const memo = raw.replace(/\r\n?|\n/g, "\n");
|
|
201
|
+
if (memo.length > MAX_MEASURE_MEMO_LENGTH) {
|
|
202
|
+
throw new Error(`Measure memo exceeds ${MAX_MEASURE_MEMO_LENGTH} characters`);
|
|
203
|
+
}
|
|
204
|
+
return memo.trim() ? memo : void 0;
|
|
205
|
+
}
|
|
131
206
|
var CHORD_REST = "rest";
|
|
132
207
|
var CHORD_REST_LABEL = "N.C.";
|
|
133
208
|
function chordDisplay(label) {
|
|
@@ -203,7 +278,15 @@ function normalizeDocument(data) {
|
|
|
203
278
|
);
|
|
204
279
|
const rawChords = Array.isArray(m.chords) ? m.chords : typeof m.chord === "string" && m.chord ? [m.chord] : [];
|
|
205
280
|
const chords = normalizeChords(rawChords, subdivisions);
|
|
206
|
-
|
|
281
|
+
const memo = normalizeMeasureMemo(m.memo);
|
|
282
|
+
return {
|
|
283
|
+
beats,
|
|
284
|
+
beatUnit,
|
|
285
|
+
subdivisions,
|
|
286
|
+
cells,
|
|
287
|
+
chords,
|
|
288
|
+
...memo !== void 0 ? { memo } : {}
|
|
289
|
+
};
|
|
207
290
|
});
|
|
208
291
|
if (!Array.isArray(data.keys) || data.keys.length === 0) {
|
|
209
292
|
data.keys = [{ measure: 0, tonic: "C", mode: "major" }];
|
|
@@ -211,6 +294,9 @@ function normalizeDocument(data) {
|
|
|
211
294
|
if (typeof data.artist !== "string") data.artist = "";
|
|
212
295
|
if (!Array.isArray(data.sections)) data.sections = [];
|
|
213
296
|
if (typeof data.tempo !== "number") data.tempo = DEFAULT_TEMPO;
|
|
297
|
+
const tempoChanges = normalizeTempoChanges(data.tempo, data.tempoChanges, data.measures.length);
|
|
298
|
+
if (tempoChanges) data.tempoChanges = tempoChanges;
|
|
299
|
+
else delete data.tempoChanges;
|
|
214
300
|
if (!data.mix || typeof data.mix.melody !== "number") {
|
|
215
301
|
data.mix = { ...DEFAULT_MIX };
|
|
216
302
|
}
|
|
@@ -391,8 +477,9 @@ function newUid() {
|
|
|
391
477
|
return newTabId();
|
|
392
478
|
}
|
|
393
479
|
function storedMeasure(m) {
|
|
394
|
-
const { uid: _uid, beatWidthOverride: _w, ...rest } = m;
|
|
395
|
-
|
|
480
|
+
const { uid: _uid, beatWidthOverride: _w, memo, ...rest } = m;
|
|
481
|
+
const normalizedMemo = normalizeMeasureMemo(memo);
|
|
482
|
+
return normalizedMemo === void 0 ? rest : { ...rest, memo: normalizedMemo };
|
|
396
483
|
}
|
|
397
484
|
function openWorkspace(doc) {
|
|
398
485
|
if (doc.workspace) return doc;
|
|
@@ -458,8 +545,10 @@ function closeWorkspace(doc) {
|
|
|
458
545
|
mainTrack: _mt,
|
|
459
546
|
tracks: _tr,
|
|
460
547
|
formatRevision,
|
|
548
|
+
tempoChanges: rawTempoChanges,
|
|
461
549
|
...rest
|
|
462
550
|
} = doc;
|
|
551
|
+
const tempoChanges = normalizeTempoChanges(doc.tempo, rawTempoChanges, main.measures.length);
|
|
463
552
|
const out = {
|
|
464
553
|
...rest,
|
|
465
554
|
tuning: [...main.tuning],
|
|
@@ -476,10 +565,17 @@ function closeWorkspace(doc) {
|
|
|
476
565
|
const mainIsDefault = mainMeta.name === DEFAULT_MAIN_TRACK_NAME && mainMeta.volume === 1 && !mainMeta.muted;
|
|
477
566
|
if (extras.length > 0 || !mainIsDefault) out.mainTrack = mainMeta;
|
|
478
567
|
if (extras.length > 0) out.tracks = extras;
|
|
568
|
+
if (tempoChanges) out.tempoChanges = tempoChanges;
|
|
479
569
|
let required = 1;
|
|
480
|
-
if (extras.length > 0) required = MULTI_TRACK_FORMAT_REVISION;
|
|
481
|
-
if (bodies.some((b) => measuresHaveHarmonics(b.measures)))
|
|
482
|
-
|
|
570
|
+
if (extras.length > 0) required = Math.max(required, MULTI_TRACK_FORMAT_REVISION);
|
|
571
|
+
if (bodies.some((b) => measuresHaveHarmonics(b.measures))) {
|
|
572
|
+
required = Math.max(required, HARMONICS_FORMAT_REVISION);
|
|
573
|
+
}
|
|
574
|
+
if (tempoChanges) required = Math.max(required, TEMPO_CHANGES_FORMAT_REVISION);
|
|
575
|
+
if (out.measures.some((m) => m.memo !== void 0)) {
|
|
576
|
+
required = Math.max(required, MEASURE_MEMOS_FORMAT_REVISION);
|
|
577
|
+
}
|
|
578
|
+
const revision = formatRevision != null && formatRevision > SUPPORTED_FORMAT_REVISION ? formatRevision : required;
|
|
483
579
|
if (revision > 1) out.formatRevision = revision;
|
|
484
580
|
return out;
|
|
485
581
|
}
|
|
@@ -488,6 +584,20 @@ function reconcileTracks(prev, next) {
|
|
|
488
584
|
const ws = doc.workspace;
|
|
489
585
|
const measures = assignUids(prev, doc.measures);
|
|
490
586
|
if (measures !== doc.measures) doc = { ...doc, measures };
|
|
587
|
+
if (ws.active === 0 && prev) {
|
|
588
|
+
const oldMemos = new Map(mainMeasures(prev).map((m) => [m.uid, m.memo]));
|
|
589
|
+
let restored = null;
|
|
590
|
+
measures.forEach((m, i) => {
|
|
591
|
+
const memo = oldMemos.get(m.uid);
|
|
592
|
+
if (memo !== void 0 && !("memo" in m)) {
|
|
593
|
+
restored ??= [...measures];
|
|
594
|
+
restored[i] = { ...m, memo };
|
|
595
|
+
}
|
|
596
|
+
});
|
|
597
|
+
if (restored) {
|
|
598
|
+
doc = { ...doc, measures: restored };
|
|
599
|
+
}
|
|
600
|
+
}
|
|
491
601
|
let changed = false;
|
|
492
602
|
const parked = ws.parked.map((body) => {
|
|
493
603
|
if (!body) return body;
|
|
@@ -808,24 +918,28 @@ function render(doc, from = 0, to = doc.measures.length - 1, track) {
|
|
|
808
918
|
main.measures.forEach((_, m) => {
|
|
809
919
|
const k = doc.keys.find((x) => x.measure === m);
|
|
810
920
|
const s = doc.sections.find((x) => x.measure === m);
|
|
921
|
+
const t = doc.tempoChanges?.find((x) => x.measure === m);
|
|
811
922
|
if (k) ann.push(`m${m}:key=${k.tonic} ${k.mode}`);
|
|
923
|
+
if (t) ann.push(`m${m}:tempo=${t.bpm}`);
|
|
812
924
|
if (s && s.label) ann.push(`m${m}:section="${s.label}"`);
|
|
813
925
|
});
|
|
814
926
|
if (ann.length) lines.push(`Markers: ${ann.join(", ")}`);
|
|
815
927
|
main.measures.forEach((m, mi) => {
|
|
816
928
|
if (mi < from || mi > to) return;
|
|
817
929
|
const key = activeKeyAt(doc.keys, mi);
|
|
930
|
+
const change = doc.tempoChanges?.find((x) => x.measure === mi);
|
|
931
|
+
const tempoNote = change ? `, tempo ${change.bpm}` : "";
|
|
818
932
|
lines.push("");
|
|
819
933
|
if (!multi) {
|
|
820
934
|
lines.push(
|
|
821
|
-
`--- measure ${mi} (bar ${mi + 1}) (${m.beats}/${m.beatUnit}, ${subText(m)}, ${columnCount(m)} cols, key ${key.tonic} ${key.mode}) ---`
|
|
935
|
+
`--- measure ${mi} (bar ${mi + 1}) (${m.beats}/${m.beatUnit}, ${subText(m)}, ${columnCount(m)} cols, key ${key.tonic} ${key.mode}${tempoNote}) ---`
|
|
822
936
|
);
|
|
823
937
|
lines.push(chordLine(m));
|
|
824
938
|
measureRows(lines, m, main.tuning);
|
|
825
939
|
return;
|
|
826
940
|
}
|
|
827
941
|
lines.push(
|
|
828
|
-
`--- measure ${mi} (bar ${mi + 1}) (${m.beats}/${m.beatUnit}, key ${key.tonic} ${key.mode}) ---`
|
|
942
|
+
`--- measure ${mi} (bar ${mi + 1}) (${m.beats}/${m.beatUnit}, key ${key.tonic} ${key.mode}${tempoNote}) ---`
|
|
829
943
|
);
|
|
830
944
|
lines.push(`${chordLine(m)} (columns of track 0)`);
|
|
831
945
|
for (const t of shown) {
|
|
@@ -871,7 +985,7 @@ var IMPORT_TAB_FROM_IMAGE = `Transcribe the TAB in the attached image(s) into an
|
|
|
871
985
|
- From the image work out: instrument and string count (6 lines = guitar, 4 = bass), tuning if printed (assume standard otherwise), time signature, key signature, tempo mark, and how many bars you will transcribe. Printed bar numbers (e.g. 35, 36 \u2026) are just labels \u2014 the document starts at measure 0.
|
|
872
986
|
- Start with new_tab (preset "Guitar (6)" / "Bass (4)" / \u2026, measures: 1, title if known). If the user is adding to a document already open in this session, append to it instead.
|
|
873
987
|
- To transcribe several parts into one tab (e.g. guitar + bass), write the first part on track 0, then add_track for each further part and pass its \`track\` index to write_measures. Measures and time signatures are shared by all tracks; chords (@Name) go on track 0 only.
|
|
874
|
-
- Set the key with set_key when there is a key signature (1 flat \u2192 F major, or D minor if the harmony says so), and the tempo with set_tempo when one is printed.
|
|
988
|
+
- Set the key with set_key when there is a key signature (1 flat \u2192 F major, or D minor if the harmony says so), and the tempo with set_tempo when one is printed. A tempo mark printed later in the piece (e.g. \u2669=96 over a bar) is a tempo change: set_tempo with that bar's 0-based \`measure\`.
|
|
875
989
|
|
|
876
990
|
## 2. Write bar by bar with write_measures
|
|
877
991
|
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.
|
|
@@ -907,7 +1021,7 @@ var state = {
|
|
|
907
1021
|
source: null
|
|
908
1022
|
};
|
|
909
1023
|
var server = new McpServer(
|
|
910
|
-
{ name: "eztab", version: "0.
|
|
1024
|
+
{ name: "eztab", version: "0.5.1" },
|
|
911
1025
|
{
|
|
912
1026
|
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."
|
|
913
1027
|
}
|
|
@@ -938,7 +1052,7 @@ function edit(track, mutate) {
|
|
|
938
1052
|
const before = onTrack(current, track);
|
|
939
1053
|
const doc = structuredClone(before);
|
|
940
1054
|
mutate(doc);
|
|
941
|
-
state.doc = reconcileTracks(before, doc);
|
|
1055
|
+
state.doc = reconcileTracks(before, withNormalizedTempoChanges(doc));
|
|
942
1056
|
state.version += 1;
|
|
943
1057
|
return state.doc;
|
|
944
1058
|
}
|
|
@@ -1200,7 +1314,7 @@ server.tool(
|
|
|
1200
1314
|
}
|
|
1201
1315
|
}
|
|
1202
1316
|
if (!existing) doc.measures.push(built);
|
|
1203
|
-
else doc.measures[i] = built;
|
|
1317
|
+
else doc.measures[i] = a.track ? built : { ...built, memo: existing.memo };
|
|
1204
1318
|
lo = Math.min(lo, i);
|
|
1205
1319
|
hi = Math.max(hi, i);
|
|
1206
1320
|
}
|
|
@@ -1280,10 +1394,33 @@ server.tool(
|
|
|
1280
1394
|
);
|
|
1281
1395
|
server.tool(
|
|
1282
1396
|
"set_tempo",
|
|
1283
|
-
"Set the playback tempo (BPM).",
|
|
1284
|
-
{
|
|
1397
|
+
"Set the playback tempo (quarter-note BPM). Without `measure` (or 0) this is the tempo at the start of the song; with `measure` >= 1 it sets a tempo change starting at that measure (an existing change there is updated). A tempo lasts until the next change. A change equal to the tempo before it is dropped.",
|
|
1398
|
+
{
|
|
1399
|
+
bpm: z.number().int().min(20).max(400),
|
|
1400
|
+
measure: z.number().int().min(0).optional()
|
|
1401
|
+
},
|
|
1285
1402
|
async (a) => applyEdit((doc) => {
|
|
1286
|
-
|
|
1403
|
+
const measure = a.measure ?? 0;
|
|
1404
|
+
assertMeasure(doc, measure);
|
|
1405
|
+
if (measure === 0) {
|
|
1406
|
+
doc.tempo = a.bpm;
|
|
1407
|
+
return;
|
|
1408
|
+
}
|
|
1409
|
+
const changes = (doc.tempoChanges ?? []).filter((t) => t.measure !== measure);
|
|
1410
|
+
changes.push({ measure, bpm: a.bpm });
|
|
1411
|
+
doc.tempoChanges = changes.sort((x, y) => x.measure - y.measure);
|
|
1412
|
+
})
|
|
1413
|
+
);
|
|
1414
|
+
server.tool(
|
|
1415
|
+
"remove_tempo",
|
|
1416
|
+
"Remove the tempo change at `measure` (>= 1); the tempo before it then carries on. The start tempo (measure 0) cannot be removed.",
|
|
1417
|
+
{ measure: z.number().int() },
|
|
1418
|
+
async (a) => applyEdit((doc) => {
|
|
1419
|
+
if (a.measure === 0) throw new Error("cannot remove the start tempo; use set_tempo");
|
|
1420
|
+
if (tempoSourceAt(doc, a.measure) !== a.measure) {
|
|
1421
|
+
throw new Error(`no tempo change at measure ${a.measure}`);
|
|
1422
|
+
}
|
|
1423
|
+
doc.tempoChanges = doc.tempoChanges?.filter((t) => t.measure !== a.measure);
|
|
1287
1424
|
})
|
|
1288
1425
|
);
|
|
1289
1426
|
server.tool(
|
|
@@ -1345,6 +1482,7 @@ server.tool(
|
|
|
1345
1482
|
doc.measures.splice(at, 0, createMeasure(doc.tuning.length));
|
|
1346
1483
|
doc.keys = shiftKeysForInsert(doc.keys, at);
|
|
1347
1484
|
doc.sections = shiftSectionsForInsert(doc.sections, at);
|
|
1485
|
+
doc.tempoChanges = shiftTempoChangesForInsert(doc, at).tempoChanges;
|
|
1348
1486
|
}
|
|
1349
1487
|
})
|
|
1350
1488
|
);
|
|
@@ -1358,6 +1496,9 @@ server.tool(
|
|
|
1358
1496
|
doc.measures.splice(a.measure, 1);
|
|
1359
1497
|
doc.keys = shiftKeysForDelete(doc.keys, a.measure);
|
|
1360
1498
|
doc.sections = shiftSectionsForDelete(doc.sections, a.measure);
|
|
1499
|
+
const shifted = shiftTempoChangesForDelete(doc, a.measure);
|
|
1500
|
+
doc.tempo = shifted.tempo;
|
|
1501
|
+
doc.tempoChanges = shifted.tempoChanges;
|
|
1361
1502
|
})
|
|
1362
1503
|
);
|
|
1363
1504
|
server.tool(
|