moontide-mcp 2.2.0 → 2.4.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 +54 -383
- package/dist/ask.js +12 -4
- package/dist/catalog.js +118 -9
- package/dist/instruments.js +83 -9
- package/dist/mcp.js +3 -3
- package/dist/midi-state.js +43 -1
- package/dist/protocol.js +80 -3
- package/dist/reference.js +12 -0
- package/dist/vocabulary.js +420 -0
- package/package.json +3 -2
- package/page/ask-view.js +77 -2
- package/page/ask.js +57 -4
- package/page/bar-model.js +1 -0
- package/page/index.html +2 -0
- package/page/instruments-model.js +50 -3
- package/page/instruments-view.js +14 -2
- package/page/main.js +348 -7
- package/page/midi-in.js +407 -0
- package/page/midi-out.js +552 -46
- package/page/midi-plan.js +42 -6
- package/page/midi-rig.js +286 -13
- package/page/midi-settings.js +507 -0
- package/page/played-buffer.js +208 -0
- package/page/scratch-gesture.js +239 -0
- package/page/scratch-listen.js +260 -0
- package/page/scratch-model.js +1696 -0
- package/page/scratch-view.js +2176 -0
- package/page/style.css +842 -0
- package/page/views.js +5 -5
- package/reference/midi.md +2 -0
- package/reference/motif.md +83 -0
- package/reference/settings.md +161 -0
|
@@ -0,0 +1,507 @@
|
|
|
1
|
+
// The bytes of the settings written in `.midi()`, and the check that keeps every system exclusive message to an
|
|
2
|
+
// instrument inside the areas that hold nothing for good. Pure: no DOM at import and no import of the server's
|
|
3
|
+
// build, so node:test, page/midi-out.js, the probe and the bench can all load it.
|
|
4
|
+
//
|
|
5
|
+
// A setting is sent as one Roland data set. Its address comes from the model file (part 1's address for a tone or
|
|
6
|
+
// a part setting); this module adds the part, cuts the value into 4-bit pieces and adds the checksum. Before any
|
|
7
|
+
// byte leaves the page the same check reads the finished message again: a message that would write outside the
|
|
8
|
+
// instrument's temporary scene and temporary tones is never sent, whoever made it.
|
|
9
|
+
//
|
|
10
|
+
// A model may instead map a setting to an NRPN of the family it names (`nrpnFamily`). The page knows one family, the
|
|
11
|
+
// part offsets of the Roland GS family, and sends an NRPN only for a pair that family lists, as the five channel
|
|
12
|
+
// messages the manual documents. The same module checks those bytes again before they are sent (nrpnProblem).
|
|
13
|
+
|
|
14
|
+
import { fill } from './midi-plan.js';
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* The words of a refusal. The first lines of a refused evaluation are built from these (the closing line is added by
|
|
18
|
+
* the caller); the last four are single lines for the page's own log.
|
|
19
|
+
*/
|
|
20
|
+
export const SETTINGS_COPY = Object.freeze({
|
|
21
|
+
kindNotSent: '"{setting}" of {id} is {kind} in its model, and Moontide sends only Roland data sets and the NRPN offsets it knows.',
|
|
22
|
+
unguarded: 'Moontide does not know which addresses of {id} are temporary, so it sends none of its settings.',
|
|
23
|
+
temporaryOnly: "Moontide sends a setting only where it can check that nothing is written to the instrument's memory.",
|
|
24
|
+
partAddress: '"{setting}" in {level} for {id} has no address for part {part} (channel {channel}).',
|
|
25
|
+
addressLength: '"{setting}" in {level} for {id} has an address of {length} bytes, and the data sets of this instrument take addresses of {size} bytes.',
|
|
26
|
+
memoryWrite: '"{setting}" in {level} for {id} would write outside the temporary scene and tones of the instrument (address {address}), so it is not sent.',
|
|
27
|
+
sendRefused: 'Moontide: a {level} setting for "{id}" was not sent: {reason}.',
|
|
28
|
+
resetRefused: 'Moontide: the reset of "{id}" was not sent: {reason}.',
|
|
29
|
+
sysexDropped: 'Moontide: a system exclusive message from the code to "{id}" was not sent: {reason}.',
|
|
30
|
+
messageDropped: 'Moontide: a MIDI message from the code to "{id}" was not sent: {reason}, so Moontide cannot check that it writes nothing to the memory of the instrument.',
|
|
31
|
+
nrpnUnknown: '"{setting}" of {id} is an NRPN in its model, and the model names no NRPN family Moontide knows.',
|
|
32
|
+
nrpnNotListed: '"{setting}" of {id} maps to NRPN {pair}, which is not a part offset of the {family} family.',
|
|
33
|
+
nrpnRange: '"{setting}" of {id} is an offset of the {family} family and takes whole numbers from {min} to {max}, not {value}.',
|
|
34
|
+
nrpnRhythm: '"{setting}" of {id} is an offset of a melodic part, and channel {channel} is a rhythm channel of its model.',
|
|
35
|
+
nrpnScene: '"{setting}" of {id} is an NRPN, which reaches one channel, so it is not a scene setting.',
|
|
36
|
+
engineRow: '"{setting}" has a row for each engine of {id}, and Moontide cannot tell which one reaches the sound on channel {channel}.',
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
// The refusals that mean Moontide cannot check that a setting writes nothing to the memory of the instrument: they add the
|
|
40
|
+
// temporaryOnly line.
|
|
41
|
+
const GUARD_KEYS = new Set(['unguarded', 'memoryWrite', 'nrpnUnknown', 'nrpnNotListed']);
|
|
42
|
+
|
|
43
|
+
// One address range, first and last address included, as 7-bit bytes, with the step between two parts of a tone or part
|
|
44
|
+
// setting whose part 1 address lies in it (in address units, so base 128: 0 means no per-part setting may point there).
|
|
45
|
+
const range = (first, last, partStep) => Object.freeze({ first: Object.freeze(first), last: Object.freeze(last), partStep });
|
|
46
|
+
|
|
47
|
+
// The areas of an instrument that hold nothing for good, by the Roland model ID of its data sets, the number of bytes of
|
|
48
|
+
// its addresses (`addressSize`) and the step between two parts in each area (`partStep`).
|
|
49
|
+
//
|
|
50
|
+
// The JUPITER-X family (model ID 00 00 00 65, four-byte addresses): its temporary scene 01 00 xx xx, where part n is 128
|
|
51
|
+
// further per part (a step of 1 in the third byte), and the temporary tones of parts 1 to 4 of the engines its settings map
|
|
52
|
+
// lists (02 00 to 02 03, 02 10 to 02 13, 02 50 to 02 53, and 02 20 for the piano engine, which has part 1 only), 16384 further
|
|
53
|
+
// per part (a step of 1 in the second byte). The system block (00), the setup block (00 10), the user scenes and tones (40 to
|
|
54
|
+
// 69) and the editor block (7F) keep settings for good or switch the scene, so no data set goes there.
|
|
55
|
+
//
|
|
56
|
+
// The MT-32 (model ID 16, three-byte addresses): the timbre temporary area of the parts 04 00 00 to 04 0F 2F (246 bytes per
|
|
57
|
+
// part), the patch temporary area of the parts 03 00 00 to 03 00 7F (16 bytes per part), the patch area of the rhythm part
|
|
58
|
+
// and the rhythm setup 03 01 00 to 03 03 0F, the master tune and reverb of the system area 10 00 00 to 10 00 03 and its master
|
|
59
|
+
// volume 10 00 16, and the display 20 00 00 to 20 00 13 and 20 01 00. The rest of the system area, the partial reserve and the
|
|
60
|
+
// MIDI channel of each part (10 00 04 to 10 00 15), is left out: one message there could move the parts off the channels the
|
|
61
|
+
// setup file plays. No per-part setting may point at the rhythm, system or display areas (a step of 0). The patch memory (05), the
|
|
62
|
+
// timbre memory (08), the write request (40) and the timbre area reached through the basic channel (02) keep sounds or copy
|
|
63
|
+
// working memory into stored ones, so no data set goes there. `exact` lists the one message that passes outside the ranges
|
|
64
|
+
// when the reset of the instrument sends it, though it is not a setting and the code may never send it: the all-parameters
|
|
65
|
+
// reset at 7F 00 00 with the data 00, which the shipped MT-32 model sends when the instrument connects.
|
|
66
|
+
//
|
|
67
|
+
// `parts` is how many parts have an address at the tone and the part level. The guard is keyed on the message: every Roland
|
|
68
|
+
// system exclusive message with a model ID of this list is checked, on any port. An instrument whose model ID is not in this
|
|
69
|
+
// list gets none of its settings; other system exclusive messages are not Moontide's to check.
|
|
70
|
+
export const TEMPORARY_AREAS = Object.freeze([
|
|
71
|
+
Object.freeze({
|
|
72
|
+
model: Object.freeze([0x00, 0x00, 0x00, 0x65]),
|
|
73
|
+
addressSize: 4,
|
|
74
|
+
ranges: Object.freeze([
|
|
75
|
+
range([0x01, 0x00, 0x00, 0x00], [0x01, 0x00, 0x7f, 0x7f], 128),
|
|
76
|
+
range([0x02, 0x00, 0x00, 0x00], [0x02, 0x03, 0x7f, 0x7f], 16384),
|
|
77
|
+
range([0x02, 0x10, 0x00, 0x00], [0x02, 0x13, 0x7f, 0x7f], 16384),
|
|
78
|
+
range([0x02, 0x20, 0x00, 0x00], [0x02, 0x20, 0x7f, 0x7f], 16384),
|
|
79
|
+
range([0x02, 0x50, 0x00, 0x00], [0x02, 0x53, 0x7f, 0x7f], 16384),
|
|
80
|
+
]),
|
|
81
|
+
parts: Object.freeze({ tone: 4, part: 5 }),
|
|
82
|
+
exact: Object.freeze([]),
|
|
83
|
+
}),
|
|
84
|
+
Object.freeze({
|
|
85
|
+
model: Object.freeze([0x16]),
|
|
86
|
+
addressSize: 3,
|
|
87
|
+
ranges: Object.freeze([
|
|
88
|
+
range([0x04, 0x00, 0x00], [0x04, 0x0f, 0x2f], 246),
|
|
89
|
+
range([0x03, 0x00, 0x00], [0x03, 0x00, 0x7f], 16),
|
|
90
|
+
range([0x03, 0x01, 0x00], [0x03, 0x03, 0x0f], 0),
|
|
91
|
+
range([0x10, 0x00, 0x00], [0x10, 0x00, 0x03], 0),
|
|
92
|
+
range([0x10, 0x00, 0x16], [0x10, 0x00, 0x16], 0),
|
|
93
|
+
range([0x20, 0x00, 0x00], [0x20, 0x00, 0x13], 0),
|
|
94
|
+
range([0x20, 0x01, 0x00], [0x20, 0x01, 0x00], 0),
|
|
95
|
+
]),
|
|
96
|
+
parts: Object.freeze({ tone: 8, part: 8 }),
|
|
97
|
+
exact: Object.freeze([Object.freeze({ address: Object.freeze([0x7f, 0x00, 0x00]), data: Object.freeze([0x00]) })]),
|
|
98
|
+
}),
|
|
99
|
+
]);
|
|
100
|
+
|
|
101
|
+
// The longest buffer the memory guard reads from one send of the code (64 KiB, well above any system exclusive dump a
|
|
102
|
+
// device takes in one message). A longer one is refused before a byte is read.
|
|
103
|
+
export const MAX_GUARDED_BYTES = 65536;
|
|
104
|
+
|
|
105
|
+
// Why the memory guard refuses a message, by reason (the reason travels to the server, the text goes to the page log).
|
|
106
|
+
const GUARD_TEXT = Object.freeze({
|
|
107
|
+
'not-bytes': 'it is not a list of bytes',
|
|
108
|
+
'too-long': `it is longer than ${MAX_GUARDED_BYTES} bytes`,
|
|
109
|
+
incomplete: 'it is not a complete message',
|
|
110
|
+
'not-data-set': 'it is not a Roland data set (command 12) for this instrument',
|
|
111
|
+
'unknown-areas': 'the temporary areas of this instrument are not known',
|
|
112
|
+
checksum: 'the checksum does not match',
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
const refusal = (reason, text = GUARD_TEXT[reason]) => ({ reason, text });
|
|
116
|
+
|
|
117
|
+
// Device ID 17, the byte the settings map and the server's reset both use.
|
|
118
|
+
export const ROLAND_DEVICE = 0x10;
|
|
119
|
+
|
|
120
|
+
const DATA_SET = 0x12;
|
|
121
|
+
|
|
122
|
+
/** The hex text of bytes: upper-case, two digits each, joined by single spaces. */
|
|
123
|
+
const hexText = (bytes) => bytes.map((byte) => byte.toString(16).toUpperCase().padStart(2, '0')).join(' ');
|
|
124
|
+
|
|
125
|
+
const sameBytes = (a, b) => a.length === b.length && a.every((byte, index) => byte === b[index]);
|
|
126
|
+
|
|
127
|
+
/** The checksum that brings the sum of the address and data bytes to a multiple of 128 (0 stays 0). */
|
|
128
|
+
function checksumOf(bytes) {
|
|
129
|
+
const sum = bytes.reduce((total, byte) => total + byte, 0);
|
|
130
|
+
return (128 - (sum % 128)) % 128;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/** The temporary areas entry whose model ID equals the settings' `rolandModel` byte for byte, else null. */
|
|
134
|
+
export function guardOf(settings) {
|
|
135
|
+
if (settings === null || settings === undefined || !Array.isArray(settings.rolandModel)) return null;
|
|
136
|
+
return TEMPORARY_AREAS.find((area) => sameBytes(area.model, settings.rolandModel)) ?? null;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* The part a channel plays, as the instrument numbers its parts: 1 plus the place of the channel among the channels of
|
|
141
|
+
* the model, ascending. A model with melodic channels 1 to 4 and a rhythm channel 10 gives channel 10 part 5.
|
|
142
|
+
*/
|
|
143
|
+
export function partOf(instrument, channel) {
|
|
144
|
+
const channels = instrument.modelChannels.map((entry) => entry.channel).sort((a, b) => a - b);
|
|
145
|
+
const index = channels.indexOf(channel);
|
|
146
|
+
if (index < 0) throw new Error(`Moontide: channel ${channel} is not a channel of the model of "${instrument.id}".`);
|
|
147
|
+
return index + 1;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* The byte Web MIDI sends for a value that is not an object: the number it converts to, truncated toward zero, modulo
|
|
152
|
+
* 256 (NaN and the infinities give 0), so '240', 496 and -16 are all F0. Converting a primitive runs no code.
|
|
153
|
+
*/
|
|
154
|
+
function octetOf(value) {
|
|
155
|
+
const number = Number(value);
|
|
156
|
+
if (!Number.isFinite(number)) return 0;
|
|
157
|
+
return ((Math.trunc(number) % 256) + 256) % 256;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const PRIMITIVE = new Set(['number', 'string', 'boolean', 'undefined']);
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* The bytes a send of the code hands on, read once, exactly as Web MIDI turns them into bytes: { bytes }, or { problem }
|
|
164
|
+
* with a reason and its text. A number is one byte; an array-like is read by its length, which must be a whole number
|
|
165
|
+
* up to MAX_GUARDED_BYTES (checked before any element is read), and no element may be an object, a function, a symbol or
|
|
166
|
+
* a big integer (converting an object could run the code's own functions). Never throws: a getter that throws is a
|
|
167
|
+
* refusal too. The caller
|
|
168
|
+
* hands on these bytes, not the original, so what was checked is what is sent.
|
|
169
|
+
*/
|
|
170
|
+
export function readSentBytes(data) {
|
|
171
|
+
try {
|
|
172
|
+
if (typeof data === 'number') return { bytes: [octetOf(data)] };
|
|
173
|
+
if (data === null || typeof data !== 'object') return { problem: refusal('not-bytes') };
|
|
174
|
+
const length = data.length;
|
|
175
|
+
if (!Number.isInteger(length) || length < 0) return { problem: refusal('not-bytes') };
|
|
176
|
+
if (length > MAX_GUARDED_BYTES) return { problem: refusal('too-long') };
|
|
177
|
+
const bytes = [];
|
|
178
|
+
for (let index = 0; index < length; index++) {
|
|
179
|
+
const value = data[index];
|
|
180
|
+
if (value !== null && !PRIMITIVE.has(typeof value)) return { problem: refusal('not-bytes') };
|
|
181
|
+
bytes.push(octetOf(value));
|
|
182
|
+
}
|
|
183
|
+
return { bytes };
|
|
184
|
+
} catch {
|
|
185
|
+
return { problem: refusal('not-bytes') };
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/** An address of 7-bit bytes as one number (the base-128 carry the Roland addresses use). */
|
|
190
|
+
const addressNumber = (address) => address.reduce((total, byte) => total * 128 + byte, 0);
|
|
191
|
+
|
|
192
|
+
/** The `size` 7-bit bytes of an address number. */
|
|
193
|
+
const addressBytes = (number, size) => Array.from({ length: size }, (_, index) => Math.floor(number / 128 ** (size - 1 - index)) % 128);
|
|
194
|
+
|
|
195
|
+
/** The range of the guard that holds an address (as a number), or undefined. */
|
|
196
|
+
const rangeOf = (guard, first) => guard.ranges.find((candidate) => first >= addressNumber(candidate.first) && first <= addressNumber(candidate.last));
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Why one system exclusive message (F0 to F7) may not be sent with this guard, or null. It passes only as a complete
|
|
200
|
+
* Roland data set for the guard's model (with the device ID `device`, any device ID when it is undefined) whose every
|
|
201
|
+
* written address, from the start address to the one of its last data byte, lies inside one of the guard's temporary
|
|
202
|
+
* ranges, with the matching checksum. With `allowExact`, a message that is one of the guard's exact messages (the reset, with
|
|
203
|
+
* its address and data byte for byte) passes too, once its checksum is right.
|
|
204
|
+
*/
|
|
205
|
+
function dataSetProblem(guard, message, device, allowExact) {
|
|
206
|
+
const head = [0xf0, 0x41, device ?? message[2], ...guard.model, DATA_SET];
|
|
207
|
+
const addressEnd = head.length + guard.addressSize;
|
|
208
|
+
// Room for at least one data byte, the checksum and the end byte.
|
|
209
|
+
if (message.length < addressEnd + 3) return refusal('incomplete');
|
|
210
|
+
for (let index = 1; index < message.length - 1; index++) if (message[index] > 0x7f) return refusal('incomplete');
|
|
211
|
+
if (!head.every((byte, index) => message[index] === byte)) return refusal('not-data-set');
|
|
212
|
+
const address = message.slice(head.length, addressEnd);
|
|
213
|
+
const first = addressNumber(address);
|
|
214
|
+
const last = first + (message.length - addressEnd - 2) - 1;
|
|
215
|
+
const data = message.slice(addressEnd, -2);
|
|
216
|
+
const exact = allowExact && guard.exact.some((candidate) => sameBytes(candidate.address, address) && sameBytes(candidate.data, data));
|
|
217
|
+
if (exact) return checksumOf(message.slice(head.length, -2)) === message[message.length - 2] ? null : refusal('checksum');
|
|
218
|
+
const inside = rangeOf(guard, first);
|
|
219
|
+
if (inside === undefined) return refusal('outside', `address ${hexText(address)} is outside the temporary scene and tones`);
|
|
220
|
+
if (last > addressNumber(inside.last)) {
|
|
221
|
+
return refusal('outside', `the data from address ${hexText(address)} runs past the temporary scene and tones (to ${hexText(addressBytes(last, guard.addressSize))})`);
|
|
222
|
+
}
|
|
223
|
+
if (checksumOf(message.slice(head.length, -2)) !== message[message.length - 2]) return refusal('checksum');
|
|
224
|
+
return null;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/** The guard of a system exclusive message: the TEMPORARY_AREAS entry of its Roland model ID (bytes 3 to 6), else null. */
|
|
228
|
+
function guardOfMessage(message) {
|
|
229
|
+
if (message[1] !== 0x41 || message.length < 7) return null;
|
|
230
|
+
return TEMPORARY_AREAS.find((area) => area.model.every((byte, index) => message[3 + index] === byte)) ?? null;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* Why a buffer of bytes (as readSentBytes gives them) may not be sent, on any port, or null. The guard is keyed on the
|
|
235
|
+
* message itself: the whole buffer is read, and every system exclusive message in it (from F0 to the next status byte,
|
|
236
|
+
* which must be F7 for a complete message) that is a Roland message with a model ID of TEMPORARY_AREAS must be a complete
|
|
237
|
+
* data set into that model's temporary areas (dataSetProblem, any device ID). Every other message passes untouched: a
|
|
238
|
+
* channel or real-time message, another maker's or another Roland model's system exclusive message (a GS message, for
|
|
239
|
+
* one). The first problem is the answer, { reason, text }, and the whole buffer is refused. Only the reset of an
|
|
240
|
+
* instrument passes `allowReset`, which lets the guard's exact messages through: the code's own messages never do, since
|
|
241
|
+
* the MT-32 all-parameters reset puts the factory contents back.
|
|
242
|
+
*/
|
|
243
|
+
export function sysexProblem(bytes, { allowReset = false } = {}) {
|
|
244
|
+
let index = bytes.indexOf(0xf0);
|
|
245
|
+
while (index >= 0) {
|
|
246
|
+
let end = index + 1;
|
|
247
|
+
while (end < bytes.length && bytes[end] < 0x80) end++;
|
|
248
|
+
const complete = end < bytes.length && bytes[end] === 0xf7;
|
|
249
|
+
const message = bytes.slice(index, complete ? end + 1 : end);
|
|
250
|
+
const guard = guardOfMessage(message);
|
|
251
|
+
if (guard !== null) {
|
|
252
|
+
if (!complete) return refusal('incomplete');
|
|
253
|
+
const problem = dataSetProblem(guard, message, undefined, allowReset === true);
|
|
254
|
+
if (problem !== null) return problem;
|
|
255
|
+
}
|
|
256
|
+
index = bytes.indexOf(0xf0, end);
|
|
257
|
+
}
|
|
258
|
+
return null;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* Why a message Moontide built for an instrument with this guard (a setting) may not be sent, as { reason, text }, or
|
|
263
|
+
* null. Stricter than sysexProblem: every F0 in it must start a complete data set for exactly this guard's model and
|
|
264
|
+
* device ID 17. Throws when there is no guard. The guard's exact messages (the reset) are not settings and do not pass here.
|
|
265
|
+
*/
|
|
266
|
+
export function ownMessageProblem(guard, bytes) {
|
|
267
|
+
if (guard === null || guard === undefined) throw new Error('Moontide: the memory check needs the temporary areas of the instrument.');
|
|
268
|
+
const message = Array.from(bytes);
|
|
269
|
+
let index = message.indexOf(0xf0);
|
|
270
|
+
while (index >= 0) {
|
|
271
|
+
const end = message.indexOf(0xf7, index + 1);
|
|
272
|
+
if (end < 0) return refusal('incomplete');
|
|
273
|
+
const problem = dataSetProblem(guard, message.slice(index, end + 1), ROLAND_DEVICE, false);
|
|
274
|
+
if (problem !== null) return problem;
|
|
275
|
+
index = message.indexOf(0xf0, end + 1);
|
|
276
|
+
}
|
|
277
|
+
return null;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/** ownMessageProblem as a text, or null. Throws when there is no guard. */
|
|
281
|
+
export function memoryWriteProblem(guard, bytes) {
|
|
282
|
+
return ownMessageProblem(guard, bytes)?.text ?? null;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// The NRPN families Moontide knows, by the name a model gives. `gs`: the eight part offsets of the Roland GS family
|
|
286
|
+
// (tone modify 1 to 8 of the SC-55 manual's NRPN list), as [MSB, LSB]. Data goes from 14 to 114 and 64 means no change; the
|
|
287
|
+
// values are offsets from the normal sound of the instrument, so the lowest value moves the sound down and closes nothing.
|
|
288
|
+
export const KNOWN_NRPNS = Object.freeze({
|
|
289
|
+
gs: Object.freeze({
|
|
290
|
+
min: 14,
|
|
291
|
+
max: 114,
|
|
292
|
+
pairs: Object.freeze([
|
|
293
|
+
Object.freeze([0x01, 0x08]),
|
|
294
|
+
Object.freeze([0x01, 0x09]),
|
|
295
|
+
Object.freeze([0x01, 0x0a]),
|
|
296
|
+
Object.freeze([0x01, 0x20]),
|
|
297
|
+
Object.freeze([0x01, 0x21]),
|
|
298
|
+
Object.freeze([0x01, 0x63]),
|
|
299
|
+
Object.freeze([0x01, 0x64]),
|
|
300
|
+
Object.freeze([0x01, 0x66]),
|
|
301
|
+
]),
|
|
302
|
+
}),
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
// The control changes of one NRPN set, in the order the manual documents them: parameter MSB, parameter LSB, data entry, and
|
|
306
|
+
// the null pair (RPN 127 127) that closes the parameter. There is no control 38.
|
|
307
|
+
const NRPN_MSB = 99;
|
|
308
|
+
const NRPN_LSB = 98;
|
|
309
|
+
const DATA_ENTRY = 6;
|
|
310
|
+
const NRPN_NULL_MSB = 101;
|
|
311
|
+
const NRPN_NULL_LSB = 100;
|
|
312
|
+
const NRPN_SET_BYTES = 15;
|
|
313
|
+
|
|
314
|
+
const NRPN_TEXT = Object.freeze({
|
|
315
|
+
family: 'the NRPN family of this instrument is not known',
|
|
316
|
+
shape: 'it is not the five control changes of one NRPN set, on one channel, in the documented order',
|
|
317
|
+
notListed: 'it is not a part offset of the NRPN family of this instrument',
|
|
318
|
+
range: 'its value is outside the range of the NRPN family of this instrument',
|
|
319
|
+
closed: 'the null that closes the NRPN is not 127',
|
|
320
|
+
rhythm: 'it is an offset of a melodic part sent to a rhythm channel',
|
|
321
|
+
scene: 'an NRPN reaches one channel, so it is not a scene setting',
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
const nrpnRefusal = (key) => refusal('nrpn', NRPN_TEXT[key]);
|
|
325
|
+
|
|
326
|
+
/**
|
|
327
|
+
* The fifteen bytes of one NRPN set on a channel (1 to 16): control 99 the MSB, control 98 the LSB, control 6 the value, then
|
|
328
|
+
* control 101 and control 100 at 127. Builds what it is given: whether the pair and the value are allowed is nrpnProblem's.
|
|
329
|
+
*/
|
|
330
|
+
export function nrpnSet(channel, msb, lsb, value) {
|
|
331
|
+
if (!Number.isInteger(channel) || channel < 1 || channel > 16) throw new Error(`Moontide: channel ${channel} is not a MIDI channel (1 to 16).`);
|
|
332
|
+
const status = 0xb0 | (channel - 1);
|
|
333
|
+
return [status, NRPN_MSB, msb, status, NRPN_LSB, lsb, status, DATA_ENTRY, value, status, NRPN_NULL_MSB, 0x7f, status, NRPN_NULL_LSB, 0x7f];
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
/**
|
|
337
|
+
* Why a buffer may not be sent as the NRPN set of a part offset, as { reason, text }, or null. It must be exactly five control
|
|
338
|
+
* changes on `channel` in the documented order, for a family Moontide knows, a pair that family lists and a value inside the
|
|
339
|
+
* family's range, with both nulls at 127. Every byte after a status byte must be a data byte.
|
|
340
|
+
*/
|
|
341
|
+
export function nrpnProblem(family, channel, bytes) {
|
|
342
|
+
const known = typeof family === 'string' && Object.hasOwn(KNOWN_NRPNS, family) ? KNOWN_NRPNS[family] : null;
|
|
343
|
+
if (known === null) return nrpnRefusal('family');
|
|
344
|
+
if (!Array.isArray(bytes) || bytes.length !== NRPN_SET_BYTES || !Number.isInteger(channel) || channel < 1 || channel > 16) return nrpnRefusal('shape');
|
|
345
|
+
const status = 0xb0 | (channel - 1);
|
|
346
|
+
const controls = [NRPN_MSB, NRPN_LSB, DATA_ENTRY, NRPN_NULL_MSB, NRPN_NULL_LSB];
|
|
347
|
+
for (let index = 0; index < controls.length; index++) {
|
|
348
|
+
const [first, control, value] = bytes.slice(index * 3, index * 3 + 3);
|
|
349
|
+
if (first !== status || control !== controls[index] || !Number.isInteger(value) || value < 0 || value > 0x7f) return nrpnRefusal('shape');
|
|
350
|
+
}
|
|
351
|
+
if (!known.pairs.some(([msb, lsb]) => msb === bytes[2] && lsb === bytes[5])) return nrpnRefusal('notListed');
|
|
352
|
+
if (bytes[8] < known.min || bytes[8] > known.max) return nrpnRefusal('range');
|
|
353
|
+
if (bytes[11] !== 0x7f || bytes[14] !== 0x7f) return nrpnRefusal('closed');
|
|
354
|
+
return null;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
/**
|
|
358
|
+
* The refusal of an NRPN set the page is about to send for `instrument` (as the rig holds it now), or null: the checks of
|
|
359
|
+
* nrpnProblem against the family of its model, on the channel of the set, and never on a rhythm channel of the model or at scene level.
|
|
360
|
+
*/
|
|
361
|
+
export function ownNrpnProblem(instrument, level, bytes) {
|
|
362
|
+
if (level === 'scene') return nrpnRefusal('scene');
|
|
363
|
+
const channel = Array.isArray(bytes) && bytes.length > 0 && (bytes[0] & 0xf0) === 0xb0 ? (bytes[0] & 0x0f) + 1 : undefined;
|
|
364
|
+
const problem = nrpnProblem(instrument.settings?.nrpnFamily ?? null, channel, bytes);
|
|
365
|
+
if (problem !== null) return problem;
|
|
366
|
+
if (instrument.modelChannels.some((entry) => entry.channel === channel && entry.role === 'rhythm')) return nrpnRefusal('rhythm');
|
|
367
|
+
return null;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
/** The value cut into 4-bit pieces by the size of the row (1, 2 or 4 bytes), the biggest piece first. */
|
|
371
|
+
function dataOf(size, value) {
|
|
372
|
+
if (size === 1) return [value];
|
|
373
|
+
if (size === 2) return [(value >> 4) & 15, value & 15];
|
|
374
|
+
return [(value >> 12) & 15, (value >> 8) & 15, (value >> 4) & 15, value & 15];
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
const LARGEST = Object.freeze({ 1: 127, 2: 255, 4: 65535 });
|
|
378
|
+
|
|
379
|
+
/**
|
|
380
|
+
* The message of an NRPN row: { bytes } (the fifteen bytes of one set), or { problem } with a `key` of SETTINGS_COPY. Refused,
|
|
381
|
+
* in this order: at scene level, for a family Moontide does not know, for a pair the family does not list, for a value outside the
|
|
382
|
+
* family's range (a value past 127, which an NRPN cannot carry, among them), on a rhythm channel of the model. A value that is not
|
|
383
|
+
* a whole number throws: the page check already holds it.
|
|
384
|
+
*/
|
|
385
|
+
function nrpnMessage({ nrpnFamily, level, message, channel, rhythm, value }) {
|
|
386
|
+
if (level === 'scene') return { problem: { key: 'nrpnScene' } };
|
|
387
|
+
const family = typeof nrpnFamily === 'string' && Object.hasOwn(KNOWN_NRPNS, nrpnFamily) ? KNOWN_NRPNS[nrpnFamily] : null;
|
|
388
|
+
if (family === null) return { problem: { key: 'nrpnUnknown' } };
|
|
389
|
+
if (!Number.isInteger(value)) throw new Error(`Moontide: the value ${value} does not fit an NRPN (0 to 127).`);
|
|
390
|
+
if (!family.pairs.some(([msb, lsb]) => msb === message.msb && lsb === message.lsb)) {
|
|
391
|
+
return { problem: { key: 'nrpnNotListed', pair: hexText([message.msb, message.lsb]), family: nrpnFamily } };
|
|
392
|
+
}
|
|
393
|
+
if (value < family.min || value > family.max) return { problem: { key: 'nrpnRange', family: nrpnFamily, min: family.min, max: family.max, value } };
|
|
394
|
+
if (rhythm) return { problem: { key: 'nrpnRhythm' } };
|
|
395
|
+
return { bytes: nrpnSet(channel, message.msb, message.lsb, value) };
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
/**
|
|
399
|
+
* The message of one setting: { bytes } for the finished data set or NRPN set, or { problem } with a `key` of SETTINGS_COPY
|
|
400
|
+
* (kindNotSent with its `kind` text, unguarded, partAddress, addressLength with its `length` and `size`, memoryWrite with its
|
|
401
|
+
* `address`, and the NRPN keys). `part` is the part of the channel (ignored for a scene setting). A tone or part setting
|
|
402
|
+
* adds the part step of the area its part 1 address lies in, once per part after the first (counted in base 128, and the
|
|
403
|
+
* result must stay in that same area); a part past the guard's `parts` for the level, an area with no part step and a part
|
|
404
|
+
* after the first with no area have no address (partAddress). A scene setting uses its address as written. An address whose
|
|
405
|
+
* length is not the guard's `addressSize` is addressLength. A value outside the size's range, or an address of neither 3 nor 4
|
|
406
|
+
* bytes, throws: the page check and the reading of the rig already hold both. An NRPN row needs `nrpnFamily` (the family the
|
|
407
|
+
* model names, or null), `channel` and `rhythm` (whether that channel is a rhythm channel of the model); it needs no guard.
|
|
408
|
+
*/
|
|
409
|
+
export function settingMessage({ guard, level, message, part, value, nrpnFamily = null, channel = 1, rhythm = false }) {
|
|
410
|
+
if (message.kind === 'nrpn') return nrpnMessage({ nrpnFamily, level, message, channel, rhythm, value });
|
|
411
|
+
if (message.kind !== 'roland-dt1') return { problem: { key: 'kindNotSent', kind: 'a control change' } };
|
|
412
|
+
if (guard === null || guard === undefined) return { problem: { key: 'unguarded' } };
|
|
413
|
+
const largest = LARGEST[message.size];
|
|
414
|
+
if (largest === undefined) throw new Error(`Moontide: a data set of ${message.size} bytes is not one of 1, 2 or 4.`);
|
|
415
|
+
if (!Number.isInteger(value) || value < 0 || value > largest) {
|
|
416
|
+
throw new Error(`Moontide: the value ${value} does not fit a data set of ${message.size} bytes (0 to ${largest}).`);
|
|
417
|
+
}
|
|
418
|
+
if (message.address.length !== 3 && message.address.length !== 4) throw new Error('Moontide: the address of a data set must be 3 or 4 bytes.');
|
|
419
|
+
if (message.address.length !== guard.addressSize) {
|
|
420
|
+
return { problem: { key: 'addressLength', length: message.address.length, size: guard.addressSize } };
|
|
421
|
+
}
|
|
422
|
+
let address = [...message.address];
|
|
423
|
+
if (level !== 'scene') {
|
|
424
|
+
const first = addressNumber(address);
|
|
425
|
+
const inside = rangeOf(guard, first);
|
|
426
|
+
// Only the parts the guard's instrument has an address for at this level, and only inside an area that has a part step.
|
|
427
|
+
// An address in no area stays as written for part 1, so the memory check below names it; for a later part it has no address.
|
|
428
|
+
if (inside === undefined) {
|
|
429
|
+
if (part !== 1) return { problem: { key: 'partAddress' } };
|
|
430
|
+
} else {
|
|
431
|
+
if (part > guard.parts[level] || inside.partStep === 0) return { problem: { key: 'partAddress' } };
|
|
432
|
+
const moved = first + inside.partStep * (part - 1);
|
|
433
|
+
if (moved > addressNumber(inside.last)) return { problem: { key: 'partAddress' } };
|
|
434
|
+
address = addressBytes(moved, guard.addressSize);
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
const body = [...address, ...dataOf(message.size, value)];
|
|
438
|
+
const bytes = [0xf0, 0x41, ROLAND_DEVICE, ...guard.model, DATA_SET, ...body, checksumOf(body), 0xf7];
|
|
439
|
+
if (memoryWriteProblem(guard, bytes) !== null) return { problem: { key: 'memoryWrite', address: hexText(address) } };
|
|
440
|
+
return { bytes };
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
/**
|
|
444
|
+
* The row of a written name: { row }, or { problem } with the key engineRow. A tone name may have one row per engine, so the
|
|
445
|
+
* row is the one that reaches the engine of the sound (`engine`, from the slot); a name with several rows and no engine to
|
|
446
|
+
* choose by, or no row for that engine, is refused, never sent as the first row. A name the level lacks throws: the page check
|
|
447
|
+
* already holds it.
|
|
448
|
+
*/
|
|
449
|
+
function rowOf(instrument, level, name, engine) {
|
|
450
|
+
const rows = instrument.settings[level].filter((candidate) => candidate.name === name);
|
|
451
|
+
if (rows.length === 0) throw new Error(`Moontide: "${name}" is not a ${level} setting of "${instrument.id}".`);
|
|
452
|
+
if (level !== 'tone' || rows.length === 1) return { row: rows[0] };
|
|
453
|
+
const row = typeof engine === 'string' ? rows.find((candidate) => candidate.engines !== undefined && candidate.engines.includes(engine)) : undefined;
|
|
454
|
+
if (row === undefined) return { problem: { key: 'engineRow' } };
|
|
455
|
+
return { row };
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
/**
|
|
459
|
+
* The data sets of everything the code writes, built from the Maps checkSetup returned. `settings` (tone and part
|
|
460
|
+
* settings per channel slot) and `scenes` (scene settings per instrument) give back `slots` keyed like `settings`,
|
|
461
|
+
* { instrument, channel, part, toneBytes: [{ name, bytes }], partBytes: [{ name, bytes }] } in written order, and
|
|
462
|
+
* `scenes` keyed by instrument id, { instrument, sceneBytes: [{ name, bytes }] }. `lines` is null, or the lines of the
|
|
463
|
+
* first problem (slots in Map order, tone before part, then the scenes); on a problem `slots` and `scenes` are empty.
|
|
464
|
+
*/
|
|
465
|
+
export function buildSettings({ rig, settings, scenes }) {
|
|
466
|
+
const slots = new Map();
|
|
467
|
+
const sceneSlots = new Map();
|
|
468
|
+
const refused = (lines) => ({ lines, slots: new Map(), scenes: new Map() });
|
|
469
|
+
const instrumentOf = (id) => rig.instruments.find((candidate) => candidate.id === id);
|
|
470
|
+
|
|
471
|
+
/** The lines of a problem of one setting. */
|
|
472
|
+
const linesOf = (problem, values) => {
|
|
473
|
+
const line = fill(SETTINGS_COPY[problem.key], { ...values, ...problem });
|
|
474
|
+
return GUARD_KEYS.has(problem.key) ? [line, SETTINGS_COPY.temporaryOnly] : [line];
|
|
475
|
+
};
|
|
476
|
+
|
|
477
|
+
for (const [key, entry] of settings) {
|
|
478
|
+
const instrument = instrumentOf(entry.instrument);
|
|
479
|
+
const guard = guardOf(instrument.settings);
|
|
480
|
+
const part = partOf(instrument, entry.channel);
|
|
481
|
+
const rhythm = instrument.modelChannels.some((candidate) => candidate.channel === entry.channel && candidate.role === 'rhythm');
|
|
482
|
+
const slot = { instrument: entry.instrument, channel: entry.channel, part, toneBytes: [], partBytes: [] };
|
|
483
|
+
for (const level of ['tone', 'part']) {
|
|
484
|
+
for (const [name, value] of entry[level]) {
|
|
485
|
+
const { row, problem } = rowOf(instrument, level, name, entry.engine);
|
|
486
|
+
const result = problem !== undefined ? { problem } : settingMessage({ guard, level, message: row.message, part, value, nrpnFamily: instrument.settings.nrpnFamily, channel: entry.channel, rhythm });
|
|
487
|
+
if (result.problem !== undefined) return refused(linesOf(result.problem, { setting: name, level, id: instrument.id, part, channel: entry.channel }));
|
|
488
|
+
slot[level === 'tone' ? 'toneBytes' : 'partBytes'].push({ name, bytes: result.bytes });
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
slots.set(key, slot);
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
for (const [id, entry] of scenes) {
|
|
495
|
+
const instrument = instrumentOf(entry.instrument);
|
|
496
|
+
const guard = guardOf(instrument.settings);
|
|
497
|
+
const sceneBytes = [];
|
|
498
|
+
for (const [name, value] of entry.values) {
|
|
499
|
+
const row = instrument.settings.scene.find((candidate) => candidate.name === name);
|
|
500
|
+
const result = settingMessage({ guard, level: 'scene', message: row.message, part: 1, value, nrpnFamily: instrument.settings.nrpnFamily });
|
|
501
|
+
if (result.problem !== undefined) return refused(linesOf(result.problem, { setting: name, level: 'scene', id: instrument.id, part: 1, channel: 1 }));
|
|
502
|
+
sceneBytes.push({ name, bytes: result.bytes });
|
|
503
|
+
}
|
|
504
|
+
sceneSlots.set(id, { instrument: entry.instrument, sceneBytes });
|
|
505
|
+
}
|
|
506
|
+
return { lines: null, slots, scenes: sceneSlots };
|
|
507
|
+
}
|