moontide-mcp 2.0.2 → 2.1.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.
Files changed (46) hide show
  1. package/README.md +41 -5
  2. package/dist/ask.js +30 -8
  3. package/dist/mcp.js +60 -14
  4. package/dist/protocol.js +89 -2
  5. package/dist/reference.js +22 -0
  6. package/dist/session.js +53 -0
  7. package/package.json +1 -1
  8. package/page/ask-view.js +21 -7
  9. package/page/ask.js +292 -34
  10. package/page/change-timing.js +25 -4
  11. package/page/harmony.js +13 -0
  12. package/page/index.html +4 -3
  13. package/page/jam-model.js +11 -2
  14. package/page/jam-view.js +17 -0
  15. package/page/lights.js +278 -0
  16. package/page/main.js +1723 -140
  17. package/page/midi-clock.js +1 -1
  18. package/page/midi-out.js +12 -0
  19. package/page/mixer-groups.js +114 -0
  20. package/page/mixer-model.js +172 -29
  21. package/page/mixer-rewrite.js +30 -1
  22. package/page/mixer-view.js +282 -13
  23. package/page/note-edit.js +267 -0
  24. package/page/note-locate.js +346 -0
  25. package/page/note-panel.js +381 -0
  26. package/page/note-tag.js +126 -0
  27. package/page/page-rewrite.js +184 -0
  28. package/page/song-edit.js +230 -0
  29. package/page/song-fit.js +250 -0
  30. package/page/song-grid-model.js +491 -0
  31. package/page/song-grid-view.js +710 -0
  32. package/page/song-prefs.js +100 -0
  33. package/page/structure-check.js +216 -0
  34. package/page/structure-view.js +38 -0
  35. package/page/structure.js +760 -0
  36. package/page/style.css +879 -84
  37. package/page/takeover.js +20 -5
  38. package/page/timeline-edit-view.js +1956 -0
  39. package/page/timeline-gesture.js +279 -0
  40. package/page/timeline.js +196 -3
  41. package/page/tracks.js +37 -8
  42. package/page/ui-state.js +10 -5
  43. package/page/views.js +25 -8
  44. package/reference/functions.md +1 -0
  45. package/reference/midi.md +2 -0
  46. package/reference/structure.md +131 -0
package/page/views.js CHANGED
@@ -30,7 +30,11 @@ export const JAM_INSTRUMENTS = ['piano', 'guitar'];
30
30
  /** The two values of the Jam "Chord tones" overlay: marks the notes of the chord sounding now, or not. */
31
31
  export const JAM_CHORD_TONES = ['on', 'off'];
32
32
 
33
- /** Browser-storage keys for the remembered layout, view, motion switch, History display, the two Jam preferences, the change timing of hand evaluations and the size of the view in layout a and in layout b. */
33
+ /** When a section launched from the Song grid starts: at the end of the section that plays, on the next phrase or on the next bar. */
34
+ export const SONG_LAUNCHES = ['section', 'phrase', 'bar'];
35
+ export const DEFAULT_SONG_LAUNCH = 'section';
36
+
37
+ /** Browser-storage keys for the remembered layout, view, motion switch, History display, the two Jam preferences, the change timing of hand evaluations, the Song grid's launch choice and the size of the view in layout a and in layout b. */
34
38
  export const STORAGE_KEYS = {
35
39
  layout: 'moontide.layout',
36
40
  view: 'moontide.view',
@@ -39,6 +43,7 @@ export const STORAGE_KEYS = {
39
43
  jamInstrument: 'moontide.jamInstrument',
40
44
  jamChordTones: 'moontide.jamChordTones',
41
45
  changeTiming: 'moontide.changeTiming',
46
+ songLaunch: 'moontide.songLaunch',
42
47
  splitA: 'moontide.split.a',
43
48
  splitB: 'moontide.split.b',
44
49
  };
@@ -92,17 +97,17 @@ export function applyKey(state, { key, focusInEditor, focusInTextEntry, ctrlKey,
92
97
  }
93
98
 
94
99
  /**
95
- * Reads the nine stored preferences from `storage` (a Storage-like object, or null). Each
100
+ * Reads the ten stored preferences from `storage` (a Storage-like object, or null). Each
96
101
  * value is validated against its closed set — layout in LAYOUTS; view 'none' or in
97
102
  * BUILT_VIEWS; motion 'on' or 'off'; historyMode in HISTORY_MODES; jamInstrument in
98
- * JAM_INSTRUMENTS; jamChordTones in JAM_CHORD_TONES; changeTiming in CHANGE_TIMINGS; splitA and splitB a plain number from 15 to 85 (page/split-model.js) — and
103
+ * JAM_INSTRUMENTS; jamChordTones in JAM_CHORD_TONES; changeTiming in CHANGE_TIMINGS; songLaunch in SONG_LAUNCHES; splitA and splitB a plain number from 15 to 85 (page/split-model.js) — and
99
104
  * becomes undefined when missing or invalid, so a stray/legacy value is never interpolated into selectors, HTML or code.
100
105
  * `rejected` lists the storage keys of the sizes that were stored but refused (a missing key is not
101
106
  * listed), so the page can say so once, loudly. Returns `available: false`, with every value undefined, when storage is
102
107
  * null or any read throws (private window, blocked storage).
103
108
  */
104
109
  export function readViewPrefs(storage) {
105
- const UNAVAILABLE_PREFS = { layout: undefined, view: undefined, motion: undefined, historyMode: undefined, jamInstrument: undefined, jamChordTones: undefined, changeTiming: undefined, splitA: undefined, splitB: undefined, rejected: [], available: false };
110
+ const UNAVAILABLE_PREFS = { layout: undefined, view: undefined, motion: undefined, historyMode: undefined, jamInstrument: undefined, jamChordTones: undefined, changeTiming: undefined, songLaunch: undefined, splitA: undefined, splitB: undefined, rejected: [], available: false };
106
111
  if (!storage) {
107
112
  return { ...UNAVAILABLE_PREFS };
108
113
  }
@@ -114,6 +119,7 @@ export function readViewPrefs(storage) {
114
119
  const rawJamInstrument = storage.getItem(STORAGE_KEYS.jamInstrument);
115
120
  const rawJamChordTones = storage.getItem(STORAGE_KEYS.jamChordTones);
116
121
  const rawChangeTiming = storage.getItem(STORAGE_KEYS.changeTiming);
122
+ const rawSongLaunch = storage.getItem(STORAGE_KEYS.songLaunch);
117
123
  const rawSplitA = storage.getItem(STORAGE_KEYS.splitA);
118
124
  const rawSplitB = storage.getItem(STORAGE_KEYS.splitB);
119
125
  const splitA = parseStoredSplit(rawSplitA);
@@ -129,6 +135,7 @@ export function readViewPrefs(storage) {
129
135
  jamInstrument: JAM_INSTRUMENTS.includes(rawJamInstrument) ? rawJamInstrument : undefined,
130
136
  jamChordTones: JAM_CHORD_TONES.includes(rawJamChordTones) ? rawJamChordTones : undefined,
131
137
  changeTiming: CHANGE_TIMINGS.includes(rawChangeTiming) ? rawChangeTiming : undefined,
138
+ songLaunch: SONG_LAUNCHES.includes(rawSongLaunch) ? rawSongLaunch : undefined,
132
139
  splitA,
133
140
  splitB,
134
141
  rejected,
@@ -156,7 +163,7 @@ export function writeViewPref(storage, key, value) {
156
163
  /**
157
164
  * The page's state at first render, before any keypress: the stored layout/view/motion/historyMode and
158
165
  * Jam preferences, else the defaults (layout 'a', view 'none', motion following the OS reduce-motion
159
- * setting, history 'list', Jam instrument 'piano', Jam chord tones 'off', change timing 'now').
166
+ * setting, history 'list', Jam instrument 'piano', Jam chord tones 'off', change timing 'now', Song launch 'section').
160
167
  * motionChosen is true only when a motion value was actually stored — while false, the motion
161
168
  * value keeps following the OS live (see motionAfterOsChange).
162
169
  */
@@ -170,6 +177,7 @@ export function initialViewState({ prefs, prefersReducedMotion }) {
170
177
  jamInstrument: prefs.jamInstrument ?? 'piano',
171
178
  jamChordTones: prefs.jamChordTones ?? 'off',
172
179
  changeTiming: prefs.changeTiming ?? 'now',
180
+ songLaunch: prefs.songLaunch ?? DEFAULT_SONG_LAUNCH,
173
181
  };
174
182
  }
175
183
 
@@ -205,19 +213,28 @@ export function setChangeTiming(state, value) {
205
213
  return { ...state, changeTiming: value };
206
214
  }
207
215
 
216
+ /** Sets when a section launched from the Song grid starts: 'section', 'phrase' or 'bar'. Throws on a value outside SONG_LAUNCHES (a loud error). */
217
+ export function setSongLaunch(state, value) {
218
+ if (!SONG_LAUNCHES.includes(value)) {
219
+ throw new Error(`Moontide: unknown song launch '${String(value)}' (expected one of ${SONG_LAUNCHES.join(', ')})`);
220
+ }
221
+ return { ...state, songLaunch: value };
222
+ }
223
+
208
224
  /**
209
225
  * The view buttons of the top bar as an ordered list, listing only the given built views in key order (1..6)
210
226
  * whatever order `builtViews` lists them in. Each part is { key, digit, name, label, title }: `key` is the digit the
211
227
  * button sends to applyKey, `name` the view title, `label` the accessible name `{digit} {Name}` (it contains the
212
228
  * visible text at every fit step) and `title` the tooltip `{Name} (key {digit})`. A view without a fixed key
213
- * (VIEW_KEYS) is ignored.
229
+ * (VIEW_KEYS) is ignored. `titles` replaces VIEW_TITLES, so a view can be called by another name (the Parts view is called
230
+ * Song while the code declares a song); the key and the view itself never change.
214
231
  */
215
- export function viewButtonParts(builtViews = BUILT_VIEWS) {
232
+ export function viewButtonParts(builtViews = BUILT_VIEWS, titles = VIEW_TITLES) {
216
233
  return Object.entries(VIEW_KEYS)
217
234
  .filter(([, view]) => builtViews.includes(view))
218
235
  .sort(([a], [b]) => Number(a) - Number(b))
219
236
  .map(([digit, view]) => {
220
- const name = VIEW_TITLES[view];
237
+ const name = titles[view];
221
238
  return { key: digit, digit, name, label: `${digit} ${name}`, title: `${name} (key ${digit})` };
222
239
  });
223
240
  }
@@ -24,6 +24,7 @@ n(run(4))
24
24
  - `cat(...patterns)` / `slowcat(...patterns)` — plays one whole pattern per cycle, cycling through the list (`cat` and `slowcat` are the same function).
25
25
  - `seq(...patterns)` / `fastcat(...patterns)` — plays each pattern in its own equal slice of one cycle (`seq` and `fastcat` are the same function).
26
26
  - `arrange(...[cycles, pattern])` — plays each pattern for the given number of cycles, in order, then repeats the whole list every total number of cycles (see reference/transitions.md "Sections over many cycles").
27
+ - `arrange` takes patterns: it calls `.fast` on each item without turning it into a pattern, so a single-quoted string item throws when the code is evaluated.
27
28
  - The page's Parts view and Timeline section row show an `arrange()` call as parts only when each section is a `[length, pattern]` array whose length is a whole number above zero; the length is written as a number, as the name of a top-level `const` holding a number, or as arithmetic with `+ - * /` over those; a section may be the name of a top-level `const` holding one `[length, pattern]` array, and a spread of a top-level `const` holding such arrays is read as its sections; other `arrange()` calls still play but are not shown as parts, and the Parts view says why.
28
29
  - `polymeter(...patterns)` — like `{ }` mini-notation: several sequences of different lengths sharing one clock.
29
30
 
package/reference/midi.md CHANGED
@@ -72,6 +72,8 @@ A program change and a note at the same instant go out in no fixed order, so a p
72
72
 
73
73
  The Moontide page reads `sound` when the user has described instruments in instruments.json: `.midi('port name', { midichannel: 3, sound: 'sound name' })` makes the page send that instrument's program change before the new code plays, and get_midi_instruments lists each instrument's sounds. With a change that waits for a bar or phrase line, the page sends the program change just before the line, timestamped at the line.
74
74
 
75
+ A track's `.midi()` options are sent once when the code is evaluated, not at a section change; two variations asking different sounds of one channel are refused.
76
+
75
77
  The Moontide page also reads `controls` when the user has described instruments in instruments.json: `.midi('port name', { midichannel: 3, controls: { volume: 100 } })` sets one static level per control of that instrument, in the control's own range, and the page sends it before the new code plays. Control ids and ranges come from get_midi_instruments; a control that leaves the code goes back to the instrument's default; a sweep stays `ccn` and `ccv`. With a change that waits for a bar or phrase line, the page sends the controls just before the line, timestamped at the line.
76
78
 
77
79
  ## What a MIDI track ignores
@@ -0,0 +1,131 @@
1
+ # Song structure
2
+
3
+ Applies to @strudel/repl 1.3.0, the version the Moontide page loads (it bundles @strudel/core 1.2.6, @strudel/mini 1.2.6, @strudel/tonal 1.2.6, @strudel/transpiler 1.2.6, @strudel/webaudio 1.3.0 and @strudel/soundfonts 1.3.0).
4
+
5
+ How a piece declares its song structure: one `const song` at the top that names the sections and their lengths and says, for every track in every section, which variation plays or that the track rests, followed by a few helper lines that turn the declaration into music. The page reads the declaration from the text of the code and shows it in the Parts view; nothing is added to the code by the page, and the same code plays in plain Strudel.
6
+
7
+ ## The template
8
+
9
+ ```js
10
+ const harmony = 'C3:major';
11
+ const song = {
12
+ shift: 0,
13
+ sections: [['sectionA', 6]],
14
+ plan: { sectionA: { trackA: 'variationA' } },
15
+ groups: {},
16
+ };
17
+ const total = song.sections.reduce((a, [, n]) => a + n, 0);
18
+ const clock = timeCat(...song.sections.map(([name, n]) => [n, pure(name)])).slow(total).late(song.shift);
19
+ const play = (track, variations) => {
20
+ const cells = {};
21
+ for (const [name, , own] of song.sections) {
22
+ const key = (song.plan[name] || {})[track];
23
+ if (key === undefined) throw new Error('song: section ' + name + ' has no cell for track ' + track);
24
+ const v = key === '-' ? silence : variations[key];
25
+ if (v === undefined) throw new Error('song: section ' + name + ', track ' + track + ': no variation ' + key + ' (it has ' + Object.keys(variations).join(', ') + ')');
26
+ cells[name] = typeof v === 'function' ? v(own || harmony) : Array.isArray(v) ? v[1] : v;
27
+ }
28
+ return clock.pickRestart(cells);
29
+ };
30
+ const trackA = {
31
+ variationA: (h) => n("0 2 4").scale(h),
32
+ };
33
+ trackA: play('trackA', trackA);
34
+ ```
35
+
36
+ The block above is a complete piece with one section and one track, and it plays as it stands. The names and the length in it are placeholders.
37
+
38
+ ## The declaration
39
+
40
+ - `song.shift` is a number of cycles that moves every section start later; 0 means no shift.
41
+ - `song.sections` lists the sections in playing order. Each entry is `['name', length]`, with a whole length above zero written out as a number; a third entry, as in `['name', length, 'E3:minor']`, gives that section its own harmony. A section name can come round again later in the list.
42
+ - `song.plan` is keyed by section name, then by track name. Each cell is the name of one of that track's variations, or `'-'` for a rest. Every track has a cell in every section.
43
+ - `song.groups` is data: an object of named groups, each a list of track names or an object of groups, nested to any depth. A group changes nothing in what plays; a track can sit in one group only.
44
+
45
+ A structure with one section is valid, as the template shows. There is no limit on the number of sections, tracks, variations or group depth, and a track can rest in any section.
46
+
47
+ There is no default form. How many sections and tracks a piece has, what they are called and how long each section lasts come from the music asked for, and can differ from one piece to the next.
48
+
49
+ Write section names, track names and harmonies in single quotes inside the song declaration: a double-quoted string is a mini-notation pattern and the clock then fails with a message about `late`. The helper's messages are joined with `+`, because a template literal is a mini-notation pattern in the page too (see the tidal-differences topic).
50
+
51
+ ## The helper
52
+
53
+ The helper lines of the template are written as they are, with the declaration above them and the tracks below. The page recognises the text and says when it was changed or is missing; it never rewrites it.
54
+
55
+ The helper plays each track's variation for the section the clock names, starts it at its first cycle, and throws, naming the section and the track, for a missing cell or an unknown variation.
56
+
57
+ A section plays each track's variation from its first cycle every time the section comes round, also when the same name is used twice.
58
+
59
+ A cell that names no variation is refused when the code is evaluated, with the section and the track named. Without that check a wrong name would play one stray default tone per cycle and report nothing, so a mistake in the last section would go unseen.
60
+
61
+ ## Tracks and variations
62
+
63
+ One const per track holds only that track's variations: an object from a variation name to a variation. The track's statement is `name: play('name', name)`, with the same name as its label and as the first argument of the call.
64
+
65
+ A variation is one of three kinds:
66
+
67
+ - a pattern, played as it is;
68
+ - a function that receives the section's harmony and returns a pattern;
69
+ - a two-element array, a harmony it was written for and then a pattern: a fixed variation, played as written whatever the section's harmony is.
70
+
71
+ Write each variation out where it is listed, as a call, a function or a pair. A variation given as a bare name or another expression is refused by the page, because the text does not say whether it is a pattern, a function or a pair.
72
+
73
+ Put `.postgain(n)` and `.midi('port')` at the end of the track's own statement, not inside its variations, which is where the Mixer reads and writes them. A fixed `.gain(n)` on the statement makes the Mixer's fader write the gain instead of the post-gain. A track's `.midi()` options are sent once when the code is evaluated, not at a section change (see the midi topic).
74
+
75
+ ## Harmony
76
+
77
+ A section's harmony is its third entry; without one, the harmony is the piece's own `const harmony` line (see the theory topic). Both are single-quoted text, a scale such as `'E3:minor'` or chord symbols such as `'Am'`.
78
+
79
+ A variation in degrees is written as a function of the section's harmony, `(h) => n("0 2 4").scale(h)`; do not read a global harmony pattern inside a variation. A variation starts over at the first cycle of its section, so a global pattern read inside it starts over too and gives every section after the first the scale of the first one. A chord harmony works the same way: `(h) => chord(h).voicing()` receives the chord of its section.
80
+
81
+ To move a pattern of degrees up, add `n(k)`, not the bare number `k`. A bare number changes nothing in a pattern of degrees.
82
+
83
+ A note held across a section line is cut at the line and is not struck again; the next section's first note starts at the line, in its own harmony.
84
+
85
+ ## Changing a structure
86
+
87
+ Changing a length, or adding or removing a section, counts the position again from the new lengths, so the music can move to another section. An older piece without a structure keeps its form when only part of it changes: a small change is made in place.
88
+
89
+ ## Two more shapes
90
+
91
+ Another shape, with two sections, the second with its own harmony, and three tracks. It holds a rest, the three kinds of variation, and groups nested one level:
92
+
93
+ ```js
94
+ // (the helper lines of the first block go here)
95
+ const harmony = 'D3:dorian';
96
+ const song = {
97
+ shift: 2,
98
+ sections: [['partOne', 10], ['partTwo', 6, 'A2:minor']],
99
+ plan: {
100
+ partOne: { trackB: 'plain', trackC: 'follow', trackD: '-' },
101
+ partTwo: { trackB: 'plain', trackC: 'follow', trackD: 'fixed' },
102
+ },
103
+ groups: { lower: ['trackB'], upper: { inner: ['trackC', 'trackD'] } },
104
+ };
105
+ const trackB = { plain: note("c3 e3 g3") };
106
+ const trackC = { follow: (h) => n("0 2 4 6").scale(h) };
107
+ const trackD = { fixed: ['D3:dorian', note("d4 f4 a4")] };
108
+ trackB: play('trackB', trackB);
109
+ trackC: play('trackC', trackC);
110
+ trackD: play('trackD', trackD);
111
+ ```
112
+
113
+ A third shape, with three entries in the section list, one name coming round again, a chord harmony and a track level set on the statement:
114
+
115
+ ```js
116
+ // (the helper lines of the first block go here)
117
+ const harmony = 'Am';
118
+ const song = {
119
+ shift: 0,
120
+ sections: [['first', 12], ['second', 9], ['first', 12]],
121
+ plan: {
122
+ first: { trackE: 'low', trackF: 'follow' },
123
+ second: { trackE: 'high', trackF: '-' },
124
+ },
125
+ groups: { both: ['trackE', 'trackF'] },
126
+ };
127
+ const trackE = { low: note("c3 g3"), high: note("c4 g4 e4") };
128
+ const trackF = { follow: (h) => chord(h).voicing() };
129
+ trackE: play('trackE', trackE);
130
+ trackF: play('trackF', trackF).postgain(0.6);
131
+ ```