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.
- package/README.md +94 -6
- package/dist/ask.js +76 -15
- package/dist/mcp.js +198 -26
- package/dist/protocol.js +130 -2
- package/dist/reference.js +22 -0
- package/dist/request-status.js +221 -0
- package/dist/session.js +256 -7
- package/package.json +1 -1
- package/page/ask-view.js +64 -57
- package/page/ask.js +363 -188
- package/page/bar-model.js +43 -2
- package/page/change-timing.js +60 -4
- package/page/harmony.js +13 -0
- package/page/index.html +7 -4
- package/page/jam-analysis.js +68 -23
- package/page/jam-model.js +196 -31
- package/page/jam-resolve.js +271 -0
- package/page/jam-view.js +182 -10
- package/page/lights.js +300 -0
- package/page/main.js +2171 -188
- package/page/midi-clock.js +23 -4
- package/page/midi-out.js +17 -2
- package/page/mixer-groups.js +114 -0
- package/page/mixer-model.js +172 -29
- package/page/mixer-rewrite.js +30 -1
- package/page/mixer-view.js +289 -14
- package/page/note-edit.js +267 -0
- package/page/note-locate.js +346 -0
- package/page/note-panel.js +402 -0
- package/page/note-tag.js +141 -0
- package/page/page-rewrite.js +184 -0
- package/page/query-check.js +21 -2
- package/page/request-status-view.js +419 -0
- package/page/request-status.js +686 -0
- package/page/song-edit.js +230 -0
- package/page/song-fit.js +250 -0
- package/page/song-grid-model.js +506 -0
- package/page/song-grid-view.js +710 -0
- package/page/song-prefs.js +100 -0
- package/page/structure-check.js +216 -0
- package/page/structure-view.js +38 -0
- package/page/structure.js +761 -0
- package/page/style.css +1218 -65
- package/page/takeover.js +20 -5
- package/page/tempo-glide.js +330 -0
- package/page/tempo-guard.js +62 -10
- package/page/timeline-edit-view.js +1956 -0
- package/page/timeline-gesture.js +279 -0
- package/page/timeline.js +197 -3
- package/page/top-bar-view.js +18 -13
- package/page/tracks.js +37 -8
- package/page/ui-state.js +10 -5
- package/page/views.js +39 -9
- package/reference/functions.md +1 -0
- package/reference/midi.md +2 -0
- package/reference/structure.md +131 -0
- 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 =
|
|
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
|
-
/**
|
|
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
|
|
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.
|
|
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 =
|
|
250
|
+
const name = titles[view];
|
|
221
251
|
return { key: digit, digit, name, label: `${digit} ${name}`, title: `${name} (key ${digit})` };
|
|
222
252
|
});
|
|
223
253
|
}
|
package/reference/functions.md
CHANGED
|
@@ -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
|
+
```
|
package/reference/transitions.md
CHANGED
|
@@ -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.
|