moontide-mcp 2.0.2 → 2.2.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 (57) hide show
  1. package/README.md +94 -6
  2. package/dist/ask.js +76 -15
  3. package/dist/mcp.js +198 -26
  4. package/dist/protocol.js +130 -2
  5. package/dist/reference.js +22 -0
  6. package/dist/request-status.js +221 -0
  7. package/dist/session.js +256 -7
  8. package/package.json +1 -1
  9. package/page/ask-view.js +64 -57
  10. package/page/ask.js +363 -188
  11. package/page/bar-model.js +43 -2
  12. package/page/change-timing.js +60 -4
  13. package/page/harmony.js +13 -0
  14. package/page/index.html +7 -4
  15. package/page/jam-analysis.js +68 -23
  16. package/page/jam-model.js +196 -31
  17. package/page/jam-resolve.js +271 -0
  18. package/page/jam-view.js +182 -10
  19. package/page/lights.js +300 -0
  20. package/page/main.js +2171 -188
  21. package/page/midi-clock.js +23 -4
  22. package/page/midi-out.js +17 -2
  23. package/page/mixer-groups.js +114 -0
  24. package/page/mixer-model.js +172 -29
  25. package/page/mixer-rewrite.js +30 -1
  26. package/page/mixer-view.js +289 -14
  27. package/page/note-edit.js +267 -0
  28. package/page/note-locate.js +346 -0
  29. package/page/note-panel.js +402 -0
  30. package/page/note-tag.js +141 -0
  31. package/page/page-rewrite.js +184 -0
  32. package/page/query-check.js +21 -2
  33. package/page/request-status-view.js +419 -0
  34. package/page/request-status.js +686 -0
  35. package/page/song-edit.js +230 -0
  36. package/page/song-fit.js +250 -0
  37. package/page/song-grid-model.js +506 -0
  38. package/page/song-grid-view.js +710 -0
  39. package/page/song-prefs.js +100 -0
  40. package/page/structure-check.js +216 -0
  41. package/page/structure-view.js +38 -0
  42. package/page/structure.js +761 -0
  43. package/page/style.css +1218 -65
  44. package/page/takeover.js +20 -5
  45. package/page/tempo-glide.js +330 -0
  46. package/page/tempo-guard.js +62 -10
  47. package/page/timeline-edit-view.js +1956 -0
  48. package/page/timeline-gesture.js +279 -0
  49. package/page/timeline.js +197 -3
  50. package/page/top-bar-view.js +18 -13
  51. package/page/tracks.js +37 -8
  52. package/page/ui-state.js +10 -5
  53. package/page/views.js +39 -9
  54. package/reference/functions.md +1 -0
  55. package/reference/midi.md +2 -0
  56. package/reference/structure.md +131 -0
  57. package/reference/transitions.md +13 -1
package/page/ui-state.js CHANGED
@@ -61,8 +61,11 @@ export const VCSL_MANIFEST_COMMIT = '9eacfc86ec4393e68a463ff52b01c19cfaa77f38';
61
61
  export const VCSL_MANIFEST_URL = 'https://raw.githubusercontent.com/felixroos/dough-samples/' + VCSL_MANIFEST_COMMIT + '/vcsl.json';
62
62
  export const VCSL_SAMPLE_BASE = 'https://cdn.jsdelivr.net/gh/sgossner/VCSL@' + VCSL_COMMIT + '/';
63
63
 
64
+ /** What every sound-load failure text ends with; the reply adds the final period. */
65
+ const REPLACED_SOUND_DUTY = '; if you replace it, tell the user which one and what replaced it, never silently';
66
+
64
67
  /** How many candidate sound names soundLoadFailureText lists before "and N more". */
65
- export const MAX_NAMED_SOUNDS = 8;
68
+ export const MAX_NAMED_SOUNDS = 7;
66
69
 
67
70
  /**
68
71
  * How long the page waits for AudioContext.resume() to reach 'running' after
@@ -219,20 +222,22 @@ export function isFetchFailure(text) {
219
222
  * The page names a sound only when it is the single candidate — a wrong name misled
220
223
  * Claude in an earlier user test. With several it lists them, sorted and de-duplicated so the text is
221
224
  * stable for any arrival order, capped at MAX_NAMED_SOUNDS so the server's 500 code point
222
- * cleanErrorDetail cap never cuts a name in half. With none, it says the page could not tell.
225
+ * cleanErrorDetail cap never cuts a name in half. With none, it says the page could not tell. Every variant ends
226
+ * with the duty to say which sound was replaced and by what: it lives here, where Claude reads about the failure,
227
+ * and not in the server instructions, which have a fixed length.
223
228
  */
224
229
  export function soundLoadFailureText(detail, names) {
225
230
  const distinct = [...new Set(names)].sort();
226
231
  if (distinct.length === 1) {
227
- return `sound "${distinct[0]}" failed to load (${detail}); it plays nothing until the page is reloaded`;
232
+ return `sound "${distinct[0]}" failed to load (${detail}); it plays nothing until the page is reloaded${REPLACED_SOUND_DUTY}`;
228
233
  }
229
234
  if (distinct.length === 0) {
230
- return `a sound failed to load (${detail}), and the page could not tell which one`;
235
+ return `a sound failed to load (${detail}), and the page could not tell which one${REPLACED_SOUND_DUTY}`;
231
236
  }
232
237
  const shown = distinct.slice(0, MAX_NAMED_SOUNDS);
233
238
  const rest = distinct.length - shown.length;
234
239
  const list = rest > 0 ? `${shown.join(', ')} and ${rest} more` : shown.join(', ');
235
- return `one of these sounds failed to load (${detail}): ${list}; the one that failed plays nothing until the page is reloaded`;
240
+ return `one of these sounds failed to load (${detail}): ${list}; the one that failed plays nothing until the page is reloaded${REPLACED_SOUND_DUTY}`;
236
241
  }
237
242
 
238
243
  /**
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,10 +43,21 @@ 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
  };
45
50
 
51
+ const SONG_LAUNCH_LIST = `${SONG_LAUNCHES.slice(0, -1).join(', ')} or ${SONG_LAUNCHES[SONG_LAUNCHES.length - 1]}`;
52
+
53
+ /**
54
+ * The console line for a stored song launch choice that is not in the list. It is built from the storage key and
55
+ * the allowed values only, never from the stored text, so nothing a stranger wrote into storage is echoed.
56
+ */
57
+ export const VIEW_PREFS_COPY = Object.freeze({
58
+ badSongLaunch: `Moontide: the stored song launch choice under '${STORAGE_KEYS.songLaunch}' is not ${SONG_LAUNCH_LIST}; sections start at the end of the section that plays.`,
59
+ });
60
+
46
61
  /** Plain-word motion switch labels. */
47
62
  export const MOTION_LABELS = { on: 'Motion: on', off: 'Motion: off' };
48
63
 
@@ -92,17 +107,18 @@ export function applyKey(state, { key, focusInEditor, focusInTextEntry, ctrlKey,
92
107
  }
93
108
 
94
109
  /**
95
- * Reads the nine stored preferences from `storage` (a Storage-like object, or null). Each
110
+ * Reads the ten stored preferences from `storage` (a Storage-like object, or null). Each
96
111
  * value is validated against its closed set — layout in LAYOUTS; view 'none' or in
97
112
  * 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
113
+ * 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
114
  * becomes undefined when missing or invalid, so a stray/legacy value is never interpolated into selectors, HTML or code.
100
115
  * `rejected` lists the storage keys of the sizes that were stored but refused (a missing key is not
101
- * listed), so the page can say so once, loudly. Returns `available: false`, with every value undefined, when storage is
116
+ * listed), so the page can say so once, loudly. `songLaunchRejected` is true when a launch choice was stored but is
117
+ * not in SONG_LAUNCHES (a missing key is not), so the page can say that once too; it is false when storage is unavailable. Returns `available: false`, with every value undefined, when storage is
102
118
  * null or any read throws (private window, blocked storage).
103
119
  */
104
120
  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 };
121
+ const UNAVAILABLE_PREFS = { layout: undefined, view: undefined, motion: undefined, historyMode: undefined, jamInstrument: undefined, jamChordTones: undefined, changeTiming: undefined, songLaunch: undefined, splitA: undefined, splitB: undefined, rejected: [], songLaunchRejected: false, available: false };
106
122
  if (!storage) {
107
123
  return { ...UNAVAILABLE_PREFS };
108
124
  }
@@ -114,6 +130,7 @@ export function readViewPrefs(storage) {
114
130
  const rawJamInstrument = storage.getItem(STORAGE_KEYS.jamInstrument);
115
131
  const rawJamChordTones = storage.getItem(STORAGE_KEYS.jamChordTones);
116
132
  const rawChangeTiming = storage.getItem(STORAGE_KEYS.changeTiming);
133
+ const rawSongLaunch = storage.getItem(STORAGE_KEYS.songLaunch);
117
134
  const rawSplitA = storage.getItem(STORAGE_KEYS.splitA);
118
135
  const rawSplitB = storage.getItem(STORAGE_KEYS.splitB);
119
136
  const splitA = parseStoredSplit(rawSplitA);
@@ -121,6 +138,7 @@ export function readViewPrefs(storage) {
121
138
  const rejected = [];
122
139
  if (rawSplitA !== null && rawSplitA !== undefined && splitA === undefined) rejected.push(STORAGE_KEYS.splitA);
123
140
  if (rawSplitB !== null && rawSplitB !== undefined && splitB === undefined) rejected.push(STORAGE_KEYS.splitB);
141
+ const songLaunchRejected = rawSongLaunch !== null && rawSongLaunch !== undefined && !SONG_LAUNCHES.includes(rawSongLaunch);
124
142
  return {
125
143
  layout: LAYOUTS.includes(rawLayout) ? rawLayout : undefined,
126
144
  view: rawView === 'none' || BUILT_VIEWS.includes(rawView) ? rawView : undefined,
@@ -129,9 +147,11 @@ export function readViewPrefs(storage) {
129
147
  jamInstrument: JAM_INSTRUMENTS.includes(rawJamInstrument) ? rawJamInstrument : undefined,
130
148
  jamChordTones: JAM_CHORD_TONES.includes(rawJamChordTones) ? rawJamChordTones : undefined,
131
149
  changeTiming: CHANGE_TIMINGS.includes(rawChangeTiming) ? rawChangeTiming : undefined,
150
+ songLaunch: SONG_LAUNCHES.includes(rawSongLaunch) ? rawSongLaunch : undefined,
132
151
  splitA,
133
152
  splitB,
134
153
  rejected,
154
+ songLaunchRejected,
135
155
  available: true,
136
156
  };
137
157
  } catch {
@@ -156,7 +176,7 @@ export function writeViewPref(storage, key, value) {
156
176
  /**
157
177
  * The page's state at first render, before any keypress: the stored layout/view/motion/historyMode and
158
178
  * 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').
179
+ * setting, history 'list', Jam instrument 'piano', Jam chord tones 'off', change timing 'now', Song launch 'section').
160
180
  * motionChosen is true only when a motion value was actually stored — while false, the motion
161
181
  * value keeps following the OS live (see motionAfterOsChange).
162
182
  */
@@ -170,6 +190,7 @@ export function initialViewState({ prefs, prefersReducedMotion }) {
170
190
  jamInstrument: prefs.jamInstrument ?? 'piano',
171
191
  jamChordTones: prefs.jamChordTones ?? 'off',
172
192
  changeTiming: prefs.changeTiming ?? 'now',
193
+ songLaunch: prefs.songLaunch ?? DEFAULT_SONG_LAUNCH,
173
194
  };
174
195
  }
175
196
 
@@ -205,19 +226,28 @@ export function setChangeTiming(state, value) {
205
226
  return { ...state, changeTiming: value };
206
227
  }
207
228
 
229
+ /** Sets when a section launched from the Song grid starts: 'section', 'phrase' or 'bar'. Throws on a value outside SONG_LAUNCHES (a loud error). */
230
+ export function setSongLaunch(state, value) {
231
+ if (!SONG_LAUNCHES.includes(value)) {
232
+ throw new Error(`Moontide: unknown song launch '${String(value)}' (expected one of ${SONG_LAUNCHES.join(', ')})`);
233
+ }
234
+ return { ...state, songLaunch: value };
235
+ }
236
+
208
237
  /**
209
238
  * The view buttons of the top bar as an ordered list, listing only the given built views in key order (1..6)
210
239
  * whatever order `builtViews` lists them in. Each part is { key, digit, name, label, title }: `key` is the digit the
211
240
  * button sends to applyKey, `name` the view title, `label` the accessible name `{digit} {Name}` (it contains the
212
241
  * visible text at every fit step) and `title` the tooltip `{Name} (key {digit})`. A view without a fixed key
213
- * (VIEW_KEYS) is ignored.
242
+ * (VIEW_KEYS) is ignored. `titles` replaces VIEW_TITLES, so a view can be called by another name (the Parts view is called
243
+ * Song while the code declares a song); the key and the view itself never change.
214
244
  */
215
- export function viewButtonParts(builtViews = BUILT_VIEWS) {
245
+ export function viewButtonParts(builtViews = BUILT_VIEWS, titles = VIEW_TITLES) {
216
246
  return Object.entries(VIEW_KEYS)
217
247
  .filter(([, view]) => builtViews.includes(view))
218
248
  .sort(([a], [b]) => Number(a) - Number(b))
219
249
  .map(([digit, view]) => {
220
- const name = VIEW_TITLES[view];
250
+ const name = titles[view];
221
251
  return { key: digit, digit, name, label: `${digit} ${name}`, title: `${name} (key ${digit})` };
222
252
  });
223
253
  }
@@ -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
+ ```
@@ -133,8 +133,20 @@ A tempo set by the waiting code with `setcps` or `setcpm` applies in the schedul
133
133
 
134
134
  ## Tempo
135
135
 
136
- `setcps(n)` and `setcpm(n)` (or the chained `.cpm(n)`, see reference/functions.md "Tempo") take effect the instant the code is evaluated — each is a single value read once at that moment, not a control that is queried continuously like a signal. In a change that waits for a bar or phrase line, they take effect at the line (see "Taking over on a bar or phrase line", above). Whether tempo itself can follow a continuous signal: this bundle's own `setcps`/`setcpm` accept a plain number, and unwrap a single-value ("pure") pattern into that same plain number if given one, but do not continuously sample a changing signal.
136
+ `setcps(n)` and `setcpm(n)` (or the chained `.cpm(n)`, see reference/functions.md "Tempo") take effect the instant the code is evaluated — each is a single value read once at that moment, not a control that is queried continuously like a signal. In a change that waits for a bar or phrase line, they take effect at the line (see "Taking over on a bar or phrase line", above). Whether tempo itself can follow a continuous signal: this bundle's own `setcps`/`setcpm` accept a plain number, and unwrap a single-value ("pure") pattern into that same plain number if given one, but do not continuously sample a changing signal. A smooth change of tempo is asked for with `glideBars` (see "Tempo ramp", below).
137
137
 
138
138
  ```js
139
139
  setcpm(120) // takes effect immediately when this code evaluates, then stays fixed
140
140
  ```
141
+
142
+ ## Tempo ramp
143
+
144
+ `set_code` and `edit_code` take `glideBars`, a whole number of bars from 1 to 64, to move the tempo smoothly instead of jumping. The code states the tempo to reach with `setcps` or `setcpm`. The tempo moves from the tempo that plays to that tempo over that many bars, in equal small steps of BPM, and ends exactly on the code's tempo. It starts at once, or at the line when `at` is `'bar'` or `'phrase'`, from the tempo that plays then.
145
+
146
+ A ramp is refused, and nothing changes, when nothing plays or the music is stopped, when the code sets no tempo, and when the code sets the tempo that already plays. Send the change again without `glideBars`.
147
+
148
+ A ramp goes on through any later program that states the same tempo again, whatever its source (a Mixer move, a Song grid edit, a Timeline edit, a hand edit, `set_code`, `edit_code`, `restore_version`), and through a program that fails. A program that states another tempo cancels it, and that tempo plays at once. A program with no tempo statement cancels it, and the tempo stays where it was. A new `glideBars` replaces it, from the tempo that plays then. Stop cancels it, and the next Play starts at the code's tempo. A page reload, or another tab taking over, ends it, and the code's tempo plays.
149
+
150
+ The reply to a change states the ramp once. A ramp that ended early is told once, in a later reply; a ramp that finished is not announced. A ramp is an event of the page, never a version: it writes no tempo into the code or the history.
151
+
152
+ A pattern's `.cpm()` is worked out once, when the program runs, against the tempo at that moment (the code's own tempo when its `setcps` or `setcpm` comes first), and its length in cycles stays fixed after that, so it moves with a ramp like the rest of the music. The MIDI clock follows every step of a ramp. How a hardware instrument that follows the MIDI clock reacts to a ramp has not been verified.