@hidechae/eztab-mcp 0.4.0 → 0.5.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 +1 -1
- package/dist/server.js +134 -15
- 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) |
|
package/dist/server.js
CHANGED
|
@@ -7,12 +7,15 @@ 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 = 4;
|
|
11
11
|
var MULTI_TRACK_FORMAT_REVISION = 2;
|
|
12
12
|
var HARMONICS_FORMAT_REVISION = 3;
|
|
13
|
+
var TEMPO_CHANGES_FORMAT_REVISION = 4;
|
|
13
14
|
var MAX_TRACKS = 4;
|
|
14
15
|
var DEFAULT_MAIN_TRACK_NAME = "Guitar";
|
|
15
16
|
var DEFAULT_TEMPO = 120;
|
|
17
|
+
var MIN_TEMPO = 20;
|
|
18
|
+
var MAX_TEMPO = 400;
|
|
16
19
|
var DEFAULT_MIX = { melody: 1, backing: 0.4 };
|
|
17
20
|
function shiftSectionsForInsert(sections, index) {
|
|
18
21
|
return sections.map((s) => s.measure >= index ? { ...s, measure: s.measure + 1 } : s);
|
|
@@ -28,6 +31,68 @@ function activeKeyAt(keys, measure) {
|
|
|
28
31
|
}
|
|
29
32
|
return result;
|
|
30
33
|
}
|
|
34
|
+
function tempoSourceAt(doc, measure) {
|
|
35
|
+
let result = 0;
|
|
36
|
+
for (const t of doc.tempoChanges ?? []) {
|
|
37
|
+
if (t.measure <= measure) result = t.measure;
|
|
38
|
+
else break;
|
|
39
|
+
}
|
|
40
|
+
return result;
|
|
41
|
+
}
|
|
42
|
+
function validTempo(raw) {
|
|
43
|
+
if (typeof raw !== "number" || !Number.isFinite(raw)) return null;
|
|
44
|
+
const bpm = Math.round(raw);
|
|
45
|
+
return bpm >= MIN_TEMPO && bpm <= MAX_TEMPO ? bpm : null;
|
|
46
|
+
}
|
|
47
|
+
function normalizeTempoChanges(tempo, raw, measureCount) {
|
|
48
|
+
if (!Array.isArray(raw)) return void 0;
|
|
49
|
+
const byMeasure = /* @__PURE__ */ new Map();
|
|
50
|
+
for (const t of raw) {
|
|
51
|
+
const measure = t?.measure;
|
|
52
|
+
const bpm = validTempo(t?.bpm);
|
|
53
|
+
if (!Number.isInteger(measure) || measure < 1 || measure >= measureCount || bpm === null) {
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
byMeasure.set(measure, bpm);
|
|
57
|
+
}
|
|
58
|
+
const out = [];
|
|
59
|
+
let prev = tempo;
|
|
60
|
+
for (const measure of [...byMeasure.keys()].sort((a, b) => a - b)) {
|
|
61
|
+
const bpm = byMeasure.get(measure);
|
|
62
|
+
if (bpm === prev) continue;
|
|
63
|
+
out.push({ measure, bpm });
|
|
64
|
+
prev = bpm;
|
|
65
|
+
}
|
|
66
|
+
return out.length > 0 ? out : void 0;
|
|
67
|
+
}
|
|
68
|
+
function withNormalizedTempoChanges(doc) {
|
|
69
|
+
const changes = normalizeTempoChanges(doc.tempo, doc.tempoChanges, doc.measures.length);
|
|
70
|
+
const { tempoChanges: _t, ...rest } = doc;
|
|
71
|
+
return changes ? { ...rest, tempoChanges: changes } : rest;
|
|
72
|
+
}
|
|
73
|
+
function shiftTempoChangesForInsert(doc, index) {
|
|
74
|
+
if (!doc.tempoChanges) return doc;
|
|
75
|
+
return {
|
|
76
|
+
...doc,
|
|
77
|
+
tempoChanges: doc.tempoChanges.map(
|
|
78
|
+
(t) => t.measure >= index ? { ...t, measure: t.measure + 1 } : t
|
|
79
|
+
)
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
function shiftTempoChangesForDelete(doc, index) {
|
|
83
|
+
if (!doc.tempoChanges) return doc;
|
|
84
|
+
let tempo = doc.tempo;
|
|
85
|
+
const changes = [];
|
|
86
|
+
for (const t of doc.tempoChanges) {
|
|
87
|
+
if (t.measure === index) continue;
|
|
88
|
+
const measure = t.measure > index ? t.measure - 1 : t.measure;
|
|
89
|
+
if (measure === 0) tempo = t.bpm;
|
|
90
|
+
else changes.push({ ...t, measure });
|
|
91
|
+
}
|
|
92
|
+
const canonical = normalizeTempoChanges(tempo, changes, Number.POSITIVE_INFINITY);
|
|
93
|
+
const { tempoChanges: _t, ...rest } = doc;
|
|
94
|
+
return canonical ? { ...rest, tempo, tempoChanges: canonical } : { ...rest, tempo };
|
|
95
|
+
}
|
|
31
96
|
function shiftKeysForInsert(keys, index) {
|
|
32
97
|
const shifted = keys.map((k) => k.measure >= index ? { ...k, measure: k.measure + 1 } : k);
|
|
33
98
|
if (!shifted.some((k) => k.measure === 0)) {
|
|
@@ -211,6 +276,9 @@ function normalizeDocument(data) {
|
|
|
211
276
|
if (typeof data.artist !== "string") data.artist = "";
|
|
212
277
|
if (!Array.isArray(data.sections)) data.sections = [];
|
|
213
278
|
if (typeof data.tempo !== "number") data.tempo = DEFAULT_TEMPO;
|
|
279
|
+
const tempoChanges = normalizeTempoChanges(data.tempo, data.tempoChanges, data.measures.length);
|
|
280
|
+
if (tempoChanges) data.tempoChanges = tempoChanges;
|
|
281
|
+
else delete data.tempoChanges;
|
|
214
282
|
if (!data.mix || typeof data.mix.melody !== "number") {
|
|
215
283
|
data.mix = { ...DEFAULT_MIX };
|
|
216
284
|
}
|
|
@@ -458,8 +526,10 @@ function closeWorkspace(doc) {
|
|
|
458
526
|
mainTrack: _mt,
|
|
459
527
|
tracks: _tr,
|
|
460
528
|
formatRevision,
|
|
529
|
+
tempoChanges: rawTempoChanges,
|
|
461
530
|
...rest
|
|
462
531
|
} = doc;
|
|
532
|
+
const tempoChanges = normalizeTempoChanges(doc.tempo, rawTempoChanges, main.measures.length);
|
|
463
533
|
const out = {
|
|
464
534
|
...rest,
|
|
465
535
|
tuning: [...main.tuning],
|
|
@@ -476,10 +546,14 @@ function closeWorkspace(doc) {
|
|
|
476
546
|
const mainIsDefault = mainMeta.name === DEFAULT_MAIN_TRACK_NAME && mainMeta.volume === 1 && !mainMeta.muted;
|
|
477
547
|
if (extras.length > 0 || !mainIsDefault) out.mainTrack = mainMeta;
|
|
478
548
|
if (extras.length > 0) out.tracks = extras;
|
|
549
|
+
if (tempoChanges) out.tempoChanges = tempoChanges;
|
|
479
550
|
let required = 1;
|
|
480
|
-
if (extras.length > 0) required = MULTI_TRACK_FORMAT_REVISION;
|
|
481
|
-
if (bodies.some((b) => measuresHaveHarmonics(b.measures)))
|
|
482
|
-
|
|
551
|
+
if (extras.length > 0) required = Math.max(required, MULTI_TRACK_FORMAT_REVISION);
|
|
552
|
+
if (bodies.some((b) => measuresHaveHarmonics(b.measures))) {
|
|
553
|
+
required = Math.max(required, HARMONICS_FORMAT_REVISION);
|
|
554
|
+
}
|
|
555
|
+
if (tempoChanges) required = Math.max(required, TEMPO_CHANGES_FORMAT_REVISION);
|
|
556
|
+
const revision = formatRevision != null && formatRevision > SUPPORTED_FORMAT_REVISION ? formatRevision : required;
|
|
483
557
|
if (revision > 1) out.formatRevision = revision;
|
|
484
558
|
return out;
|
|
485
559
|
}
|
|
@@ -724,6 +798,18 @@ function parseBeat(text2, stringCount, where) {
|
|
|
724
798
|
return { cells, chord };
|
|
725
799
|
});
|
|
726
800
|
}
|
|
801
|
+
var STRAIGHT = [1, 2, 4, 8];
|
|
802
|
+
function uniformBeats(beats) {
|
|
803
|
+
const target = Math.max(0, ...beats.map((b) => b.length).filter((n) => STRAIGHT.includes(n)));
|
|
804
|
+
return beats.map((slots) => {
|
|
805
|
+
if (!STRAIGHT.includes(slots.length) || slots.length === target) return slots;
|
|
806
|
+
const step = target / slots.length;
|
|
807
|
+
return Array.from(
|
|
808
|
+
{ length: target },
|
|
809
|
+
(_, i) => i % step === 0 ? slots[i / step] : { cells: /* @__PURE__ */ new Map(), chord: null }
|
|
810
|
+
);
|
|
811
|
+
});
|
|
812
|
+
}
|
|
727
813
|
function buildMeasure(spec, stringCount, fallback, where = "measure") {
|
|
728
814
|
const time = spec.time ? parseTime(spec.time) : fallback;
|
|
729
815
|
if (spec.beats.length !== time.beats) {
|
|
@@ -731,7 +817,9 @@ function buildMeasure(spec, stringCount, fallback, where = "measure") {
|
|
|
731
817
|
`${where}: ${spec.beats.length} beats given but the time signature ${time.beats}/${time.beatUnit} has ${time.beats}`
|
|
732
818
|
);
|
|
733
819
|
}
|
|
734
|
-
const beats =
|
|
820
|
+
const beats = uniformBeats(
|
|
821
|
+
spec.beats.map((b, i) => parseBeat(b, stringCount, `${where} beat ${i + 1}`))
|
|
822
|
+
);
|
|
735
823
|
const measure = createMeasure(
|
|
736
824
|
stringCount,
|
|
737
825
|
time.beats,
|
|
@@ -794,24 +882,28 @@ function render(doc, from = 0, to = doc.measures.length - 1, track) {
|
|
|
794
882
|
main.measures.forEach((_, m) => {
|
|
795
883
|
const k = doc.keys.find((x) => x.measure === m);
|
|
796
884
|
const s = doc.sections.find((x) => x.measure === m);
|
|
885
|
+
const t = doc.tempoChanges?.find((x) => x.measure === m);
|
|
797
886
|
if (k) ann.push(`m${m}:key=${k.tonic} ${k.mode}`);
|
|
887
|
+
if (t) ann.push(`m${m}:tempo=${t.bpm}`);
|
|
798
888
|
if (s && s.label) ann.push(`m${m}:section="${s.label}"`);
|
|
799
889
|
});
|
|
800
890
|
if (ann.length) lines.push(`Markers: ${ann.join(", ")}`);
|
|
801
891
|
main.measures.forEach((m, mi) => {
|
|
802
892
|
if (mi < from || mi > to) return;
|
|
803
893
|
const key = activeKeyAt(doc.keys, mi);
|
|
894
|
+
const change = doc.tempoChanges?.find((x) => x.measure === mi);
|
|
895
|
+
const tempoNote = change ? `, tempo ${change.bpm}` : "";
|
|
804
896
|
lines.push("");
|
|
805
897
|
if (!multi) {
|
|
806
898
|
lines.push(
|
|
807
|
-
`--- measure ${mi} (bar ${mi + 1}) (${m.beats}/${m.beatUnit}, ${subText(m)}, ${columnCount(m)} cols, key ${key.tonic} ${key.mode}) ---`
|
|
899
|
+
`--- measure ${mi} (bar ${mi + 1}) (${m.beats}/${m.beatUnit}, ${subText(m)}, ${columnCount(m)} cols, key ${key.tonic} ${key.mode}${tempoNote}) ---`
|
|
808
900
|
);
|
|
809
901
|
lines.push(chordLine(m));
|
|
810
902
|
measureRows(lines, m, main.tuning);
|
|
811
903
|
return;
|
|
812
904
|
}
|
|
813
905
|
lines.push(
|
|
814
|
-
`--- measure ${mi} (bar ${mi + 1}) (${m.beats}/${m.beatUnit}, key ${key.tonic} ${key.mode}) ---`
|
|
906
|
+
`--- measure ${mi} (bar ${mi + 1}) (${m.beats}/${m.beatUnit}, key ${key.tonic} ${key.mode}${tempoNote}) ---`
|
|
815
907
|
);
|
|
816
908
|
lines.push(`${chordLine(m)} (columns of track 0)`);
|
|
817
909
|
for (const t of shown) {
|
|
@@ -857,11 +949,11 @@ var IMPORT_TAB_FROM_IMAGE = `Transcribe the TAB in the attached image(s) into an
|
|
|
857
949
|
- 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.
|
|
858
950
|
- 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.
|
|
859
951
|
- 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.
|
|
860
|
-
- 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.
|
|
952
|
+
- 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\`.
|
|
861
953
|
|
|
862
954
|
## 2. Write bar by bar with write_measures
|
|
863
955
|
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.
|
|
864
|
-
- 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.
|
|
956
|
+
- 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.
|
|
865
957
|
- 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.
|
|
866
958
|
- Chord names printed above the staff go on the chord row with @Name at the slot where the chord changes (usually beat 1).
|
|
867
959
|
- Rests: write S:r on the string that was ringing only when the line clearly stops (a rest symbol); otherwise leave "-".
|
|
@@ -893,7 +985,7 @@ var state = {
|
|
|
893
985
|
source: null
|
|
894
986
|
};
|
|
895
987
|
var server = new McpServer(
|
|
896
|
-
{ name: "eztab", version: "0.
|
|
988
|
+
{ name: "eztab", version: "0.5.0" },
|
|
897
989
|
{
|
|
898
990
|
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."
|
|
899
991
|
}
|
|
@@ -924,7 +1016,7 @@ function edit(track, mutate) {
|
|
|
924
1016
|
const before = onTrack(current, track);
|
|
925
1017
|
const doc = structuredClone(before);
|
|
926
1018
|
mutate(doc);
|
|
927
|
-
state.doc = reconcileTracks(before, doc);
|
|
1019
|
+
state.doc = reconcileTracks(before, withNormalizedTempoChanges(doc));
|
|
928
1020
|
state.version += 1;
|
|
929
1021
|
return state.doc;
|
|
930
1022
|
}
|
|
@@ -1141,7 +1233,7 @@ server.tool(
|
|
|
1141
1233
|
);
|
|
1142
1234
|
server.tool(
|
|
1143
1235
|
"write_measures",
|
|
1144
|
-
'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:<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"].',
|
|
1236
|
+
'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"].',
|
|
1145
1237
|
{
|
|
1146
1238
|
measures: z.array(
|
|
1147
1239
|
z.object({
|
|
@@ -1266,10 +1358,33 @@ server.tool(
|
|
|
1266
1358
|
);
|
|
1267
1359
|
server.tool(
|
|
1268
1360
|
"set_tempo",
|
|
1269
|
-
"Set the playback tempo (BPM).",
|
|
1270
|
-
{
|
|
1361
|
+
"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.",
|
|
1362
|
+
{
|
|
1363
|
+
bpm: z.number().int().min(20).max(400),
|
|
1364
|
+
measure: z.number().int().min(0).optional()
|
|
1365
|
+
},
|
|
1271
1366
|
async (a) => applyEdit((doc) => {
|
|
1272
|
-
|
|
1367
|
+
const measure = a.measure ?? 0;
|
|
1368
|
+
assertMeasure(doc, measure);
|
|
1369
|
+
if (measure === 0) {
|
|
1370
|
+
doc.tempo = a.bpm;
|
|
1371
|
+
return;
|
|
1372
|
+
}
|
|
1373
|
+
const changes = (doc.tempoChanges ?? []).filter((t) => t.measure !== measure);
|
|
1374
|
+
changes.push({ measure, bpm: a.bpm });
|
|
1375
|
+
doc.tempoChanges = changes.sort((x, y) => x.measure - y.measure);
|
|
1376
|
+
})
|
|
1377
|
+
);
|
|
1378
|
+
server.tool(
|
|
1379
|
+
"remove_tempo",
|
|
1380
|
+
"Remove the tempo change at `measure` (>= 1); the tempo before it then carries on. The start tempo (measure 0) cannot be removed.",
|
|
1381
|
+
{ measure: z.number().int() },
|
|
1382
|
+
async (a) => applyEdit((doc) => {
|
|
1383
|
+
if (a.measure === 0) throw new Error("cannot remove the start tempo; use set_tempo");
|
|
1384
|
+
if (tempoSourceAt(doc, a.measure) !== a.measure) {
|
|
1385
|
+
throw new Error(`no tempo change at measure ${a.measure}`);
|
|
1386
|
+
}
|
|
1387
|
+
doc.tempoChanges = doc.tempoChanges?.filter((t) => t.measure !== a.measure);
|
|
1273
1388
|
})
|
|
1274
1389
|
);
|
|
1275
1390
|
server.tool(
|
|
@@ -1331,6 +1446,7 @@ server.tool(
|
|
|
1331
1446
|
doc.measures.splice(at, 0, createMeasure(doc.tuning.length));
|
|
1332
1447
|
doc.keys = shiftKeysForInsert(doc.keys, at);
|
|
1333
1448
|
doc.sections = shiftSectionsForInsert(doc.sections, at);
|
|
1449
|
+
doc.tempoChanges = shiftTempoChangesForInsert(doc, at).tempoChanges;
|
|
1334
1450
|
}
|
|
1335
1451
|
})
|
|
1336
1452
|
);
|
|
@@ -1344,6 +1460,9 @@ server.tool(
|
|
|
1344
1460
|
doc.measures.splice(a.measure, 1);
|
|
1345
1461
|
doc.keys = shiftKeysForDelete(doc.keys, a.measure);
|
|
1346
1462
|
doc.sections = shiftSectionsForDelete(doc.sections, a.measure);
|
|
1463
|
+
const shifted = shiftTempoChangesForDelete(doc, a.measure);
|
|
1464
|
+
doc.tempo = shifted.tempo;
|
|
1465
|
+
doc.tempoChanges = shifted.tempoChanges;
|
|
1347
1466
|
})
|
|
1348
1467
|
);
|
|
1349
1468
|
server.tool(
|