@yam8d/m8-sdk 0.3.2 → 0.3.3

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/dist/index.js CHANGED
@@ -7,386 +7,35 @@ var r = {
7
7
  playbackIndicator: "The M8 renders a '>' character one column to the left of the first data column when that step/row/track is currently playing. This character is part of the text stream; the editing cursor is a separate rect overlay.",
8
8
  emptyCell: "An empty or unused cell displays '--' (2 chars) or '---' (3 chars) or '---00' (command+value). Parsed hexValue will be null for these.",
9
9
  relativeHex: "Transpose and some FX values use relative hex: 00 = no change, 01-7F = +1-127, FF-80 = -1-128. See 'Relative and Absolute FX Commands' in the manual.",
10
- rowIndex: "Row/step indices are parsed from the leftmost characters of currentLine. They are hex for all grid views."
10
+ rowIndex: "Row/step indices are parsed from the leftmost characters of currentLine. They are hex for all grid views.",
11
+ titleAsterisk: "A '*' next to the item ID in the screen title (e.g. 'PHRASE 03*') indicates the item is used elsewhere in the chain or song (phrase view); on other views it marks unsaved changes. See 'An asterisk next to the phrase number' in the manual."
11
12
  },
12
13
  fieldTypes: {
13
14
  chainRef: "Reference to a chain number (hex 00-FF). '--' means empty/unused.",
14
15
  phraseRef: "Reference to a phrase number (hex 00-FF). '--' means empty/unused.",
15
16
  note: "Musical note in M8 format: [A-G][#|-][0-9] e.g. 'C-4', 'A#3'. '---' = empty. 'OFF' = note-off.",
16
- velocity: "Note velocity 00-7F (hex). Default is 64 (hex). '--' = inherit.",
17
+ velocity: "Note velocity 00-7F (hex), from 00 (silence) to 7F (maximum). Default is 64 (hex). '--' = inherit.",
17
18
  instrumentRef: "Instrument number 00-7F (hex). '--' = no retrigger (keep current instrument).",
18
19
  fxCommand: "3-character sequencer or instrument FX command name, e.g. 'KIL', 'ARP', 'DEL'. '---' = none.",
19
20
  fxValue: "2-char hex value 00-FF accompanying an FX command. Interpretation depends on the command.",
20
21
  transpose: "Semitone or tick transpose. Relative: 00=none, 01-7F=+1-+127, FF-80=-1--128.",
21
22
  ticks: "Number of ticks this step consumes in a groove (hex 01-FF). '00' skips the step. '--' = empty (groove loops at this point).",
22
23
  volume: "Volume multiplier 00-FF (hex). Multiplied with the phrase-row velocity. '--' = no override.",
23
- ppq: "Pulses Per Quarter note: resolution of the groove clock (decimal). Only editable on groove row 0.",
24
+ ppq: "Pulses Per Quarter note: resolution of the groove clock (decimal). One of 24 (default), 48, 96 or 192. Only present on groove row 0; absent on all other rows.",
24
25
  instrumentName: "Instrument name string (up to 12 characters). Text field, no hex value.",
25
26
  eqSlot: "EQ slot assignment (hex). '--' = no EQ applied to this instrument.",
26
- rowIndex: "The positional index of this row in the current view (song row, chain position, phrase step, etc.)."
27
- },
28
- views: {
29
- song: {
30
- title: "Song View",
31
- type: "grid",
32
- description: "Arranges chains across 8 independent tracks to form the song structure. Each row is a song position; each column is a track.",
33
- rowField: {
34
- x: 0,
35
- width: 2,
36
- parse: "hex",
37
- type: "rowIndex",
38
- key: "songRow",
39
- description: "Song row 00-FF. Each track loops back to row 00 when it reaches '--'."
40
- },
41
- columns: [
42
- {
43
- x: 3,
44
- width: 2,
45
- key: "track1",
46
- type: "chainRef",
47
- label: "Track 1",
48
- meta: { trackIndex: 1 }
49
- },
50
- {
51
- x: 6,
52
- width: 2,
53
- key: "track2",
54
- type: "chainRef",
55
- label: "Track 2",
56
- meta: { trackIndex: 2 }
57
- },
58
- {
59
- x: 9,
60
- width: 2,
61
- key: "track3",
62
- type: "chainRef",
63
- label: "Track 3",
64
- meta: { trackIndex: 3 }
65
- },
66
- {
67
- x: 12,
68
- width: 2,
69
- key: "track4",
70
- type: "chainRef",
71
- label: "Track 4",
72
- meta: { trackIndex: 4 }
73
- },
74
- {
75
- x: 15,
76
- width: 2,
77
- key: "track5",
78
- type: "chainRef",
79
- label: "Track 5",
80
- meta: { trackIndex: 5 }
81
- },
82
- {
83
- x: 18,
84
- width: 2,
85
- key: "track6",
86
- type: "chainRef",
87
- label: "Track 6",
88
- meta: { trackIndex: 6 }
89
- },
90
- {
91
- x: 21,
92
- width: 2,
93
- key: "track7",
94
- type: "chainRef",
95
- label: "Track 7",
96
- meta: { trackIndex: 7 }
97
- },
98
- {
99
- x: 24,
100
- width: 2,
101
- key: "track8",
102
- type: "chainRef",
103
- label: "Track 8",
104
- meta: { trackIndex: 8 }
105
- }
106
- ]
107
- },
108
- chain: {
109
- title: "Chain View",
110
- type: "grid",
111
- description: "An ordered list of up to 16 phrases. The first '--' phrase ends the chain and loops back to the song.",
112
- rowField: {
113
- x: 0,
114
- width: 1,
115
- parse: "hex",
116
- type: "rowIndex",
117
- key: "chainPos",
118
- description: "Position within the chain (0-F, max 16 entries)."
119
- },
120
- columns: [{
121
- x: 2,
122
- width: 2,
123
- key: "phrase",
124
- type: "phraseRef",
125
- label: "Phrase (PH)",
126
- description: "Phrase number to play at this chain position."
127
- }, {
128
- x: 5,
129
- width: 2,
130
- key: "transpose",
131
- type: "transpose",
132
- label: "Transpose (TSP)",
133
- description: "Optional note transpose in semitones applied to every note in the phrase (relative hex)."
134
- }]
135
- },
136
- phrase: {
137
- title: "Phrase View",
138
- type: "grid",
139
- description: "A 16-step sequence. Each step holds a note, velocity, instrument assignment, and up to 3 FX command+value pairs.",
140
- rowField: {
141
- x: 0,
142
- width: 1,
143
- parse: "hex",
144
- type: "rowIndex",
145
- key: "step",
146
- description: "Step index 0-F (16th note by default; altered by groove)."
147
- },
148
- columns: [
149
- {
150
- x: 2,
151
- width: 3,
152
- key: "note",
153
- type: "note",
154
- label: "Note (N)"
155
- },
156
- {
157
- x: 6,
158
- width: 2,
159
- key: "vel",
160
- type: "velocity",
161
- label: "Velocity (V)"
162
- },
163
- {
164
- x: 9,
165
- width: 2,
166
- key: "inst",
167
- type: "instrumentRef",
168
- label: "Instrument (I)"
169
- },
170
- {
171
- x: 12,
172
- width: 3,
173
- key: "fx1cmd",
174
- type: "fxCommand",
175
- label: "FX1 Command"
176
- },
177
- {
178
- x: 15,
179
- width: 2,
180
- key: "fx1val",
181
- type: "fxValue",
182
- label: "FX1 Value"
183
- },
184
- {
185
- x: 18,
186
- width: 3,
187
- key: "fx2cmd",
188
- type: "fxCommand",
189
- label: "FX2 Command"
190
- },
191
- {
192
- x: 21,
193
- width: 2,
194
- key: "fx2val",
195
- type: "fxValue",
196
- label: "FX2 Value"
197
- },
198
- {
199
- x: 24,
200
- width: 3,
201
- key: "fx3cmd",
202
- type: "fxCommand",
203
- label: "FX3 Command"
204
- },
205
- {
206
- x: 27,
207
- width: 2,
208
- key: "fx3val",
209
- type: "fxValue",
210
- label: "FX3 Value"
211
- }
212
- ]
213
- },
214
- table: {
215
- title: "Table View",
216
- type: "grid",
217
- description: "A 16-step mini-sequencer that runs alongside an instrument. Used for arpeggios, multi-stage envelopes, and automated FX.",
218
- rowField: {
219
- x: 0,
220
- width: 1,
221
- parse: "hex",
222
- type: "rowIndex",
223
- key: "step",
224
- description: "Table step 0-F. Loops from bottom unless a HOP command redirects."
225
- },
226
- columns: [
227
- {
228
- x: 2,
229
- width: 2,
230
- key: "transpose",
231
- type: "transpose",
232
- label: "Transpose (N)",
233
- description: "Transposes the instrument for this step (relative hex). '--' = no transpose."
234
- },
235
- {
236
- x: 5,
237
- width: 2,
238
- key: "volume",
239
- type: "volume",
240
- label: "Volume (V)",
241
- description: "Volume multiplier applied on top of the phrase velocity (hex). '--' = no override."
242
- },
243
- {
244
- x: 8,
245
- width: 3,
246
- key: "fx1cmd",
247
- type: "fxCommand",
248
- label: "FX1 Command",
249
- description: "3-char command name for table FX slot 1."
250
- },
251
- {
252
- x: 11,
253
- width: 2,
254
- key: "fx1val",
255
- type: "fxValue",
256
- label: "FX1 Value",
257
- description: "Value for table FX1."
258
- },
259
- {
260
- x: 14,
261
- width: 3,
262
- key: "fx2cmd",
263
- type: "fxCommand",
264
- label: "FX2 Command",
265
- description: "3-char command name for table FX slot 2."
266
- },
267
- {
268
- x: 17,
269
- width: 2,
270
- key: "fx2val",
271
- type: "fxValue",
272
- label: "FX2 Value",
273
- description: "Value for table FX2."
274
- },
275
- {
276
- x: 20,
277
- width: 3,
278
- key: "fx3cmd",
279
- type: "fxCommand",
280
- label: "FX3 Command",
281
- description: "3-char command name for table FX slot 3."
282
- },
283
- {
284
- x: 23,
285
- width: 2,
286
- key: "fx3val",
287
- type: "fxValue",
288
- label: "FX3 Value",
289
- description: "Value for table FX3."
290
- }
291
- ]
292
- },
293
- groove: {
294
- title: "Groove View",
295
- type: "grid",
296
- description: "Defines the timing of each phrase step in ticks. Used to create swing, shuffle, triplets, and other rhythmic feels.",
297
- rowField: {
298
- x: 0,
299
- width: 1,
300
- parse: "hex",
301
- type: "rowIndex",
302
- key: "step",
303
- description: "Groove step 0-F. The groove loops when a '--' entry is reached."
304
- },
305
- columns: [{
306
- x: 2,
307
- width: 2,
308
- key: "ticks",
309
- type: "ticks",
310
- label: "Ticks (TIC)",
311
- description: "Number of ticks this step takes. Default is 06 (at 24 PPQN = 1 sixteenth note). '00' skips the step."
312
- }, {
313
- x: 6,
314
- width: 2,
315
- key: "ppq",
316
- type: "ppq",
317
- label: "PPQ",
318
- description: "Pulses Per Quarter note. Only present on groove row 0; absent on all other rows."
319
- }]
320
- },
321
- instrumentpool: {
322
- title: "Instrument Pool",
323
- type: "grid",
324
- description: "Lists all instrument slots with their mix routing. Each row shows the instrument name and its dry/FX send levels.",
325
- rowField: {
326
- x: 0,
327
- width: 2,
328
- parse: "hex",
329
- type: "rowIndex",
330
- key: "instIndex",
331
- description: "Instrument slot index 00-7F."
332
- },
333
- columns: [
334
- {
335
- x: 3,
336
- width: 12,
337
- key: "name",
338
- type: "instrumentName",
339
- label: "Instrument Name",
340
- description: "Name of the instrument in this slot (up to 12 chars). '------------' = empty slot."
341
- },
342
- {
343
- x: 16,
344
- width: 2,
345
- key: "dry",
346
- type: "volume",
347
- label: "Dry (DRY)",
348
- description: "Direct output volume for this instrument (hex 00-FF)."
349
- },
350
- {
351
- x: 19,
352
- width: 2,
353
- key: "mx",
354
- type: "volume",
355
- label: "ModFX (MX)",
356
- description: "ModFX send level (hex 00-FF)."
357
- },
358
- {
359
- x: 22,
360
- width: 2,
361
- key: "de",
362
- type: "volume",
363
- label: "Delay (DE)",
364
- description: "Delay send level (hex 00-FF)."
365
- },
366
- {
367
- x: 25,
368
- width: 2,
369
- key: "rv",
370
- type: "volume",
371
- label: "Reverb (RV)",
372
- description: "Reverb send level (hex 00-FF)."
373
- },
374
- {
375
- x: 28,
376
- width: 2,
377
- key: "eq",
378
- type: "eqSlot",
379
- label: "EQ Slot (EQ)",
380
- description: "EQ slot assignment. '--' = no EQ."
381
- }
382
- ]
383
- }
27
+ rowIndex: "The positional index of this row in the current view (song row, chain position, phrase step, etc.).",
28
+ noteInterval: "Scale row label: one of the 12 note intervals C, C#, D, D#, E, F, F#, G, G#, A, A#, B (sharps use '#', naturals may render a dim '-' pad, e.g. 'C-').",
29
+ onOff: "Toggle field displayed as 'ON' or 'OFF' ('--' = unset). Used by the scale view 'EN' column.",
30
+ semitoneOffset: "Signed semitone offset in decimal notation with two decimals (-24.00 to +24.00), e.g. '00.00', '-00.50', '12.00'. Used by the scale view 'OFFSET' column.",
31
+ swing: "Groove swing amount as a percentage with one decimal, e.g. '50.0%' = straight timing, higher = swung. Only present on groove row 0; absent on all other rows."
384
32
  },
33
+ views: /* @__PURE__ */ JSON.parse("{\"song\":{\"title\":\"Song View\",\"type\":\"grid\",\"description\":\"Arranges chains across 8 independent tracks to form the song structure. Each row is a song position (up to 256 rows); each column is a track. Each track advances vertically until it reaches an empty column ('--'), then loops back to row 00. Chains containing no notes are grayed out; chains '00' or 'FE' are commonly used as silent spacers. On row '00', [UP][UP] enters track reordering mode.\",\"rowField\":{\"x\":0,\"width\":2,\"parse\":\"hex\",\"type\":\"rowIndex\",\"key\":\"songRow\",\"description\":\"Song row 00-FF. Each track loops back to row 00 when it reaches '--'.\"},\"columns\":[{\"x\":3,\"width\":2,\"key\":\"track1\",\"type\":\"chainRef\",\"label\":\"Track 1\",\"meta\":{\"trackIndex\":1}},{\"x\":6,\"width\":2,\"key\":\"track2\",\"type\":\"chainRef\",\"label\":\"Track 2\",\"meta\":{\"trackIndex\":2}},{\"x\":9,\"width\":2,\"key\":\"track3\",\"type\":\"chainRef\",\"label\":\"Track 3\",\"meta\":{\"trackIndex\":3}},{\"x\":12,\"width\":2,\"key\":\"track4\",\"type\":\"chainRef\",\"label\":\"Track 4\",\"meta\":{\"trackIndex\":4}},{\"x\":15,\"width\":2,\"key\":\"track5\",\"type\":\"chainRef\",\"label\":\"Track 5\",\"meta\":{\"trackIndex\":5}},{\"x\":18,\"width\":2,\"key\":\"track6\",\"type\":\"chainRef\",\"label\":\"Track 6\",\"meta\":{\"trackIndex\":6}},{\"x\":21,\"width\":2,\"key\":\"track7\",\"type\":\"chainRef\",\"label\":\"Track 7\",\"meta\":{\"trackIndex\":7}},{\"x\":24,\"width\":2,\"key\":\"track8\",\"type\":\"chainRef\",\"label\":\"Track 8\",\"meta\":{\"trackIndex\":8}}]},\"chain\":{\"title\":\"Chain View\",\"type\":\"grid\",\"description\":\"An ordered list of up to 16 phrases (256 steps). The first '--' phrase ends the chain and the song continues on to the next chain in the song view. The 'TSP' column optionally transposes every note in the phrase in semitones (relative hex).\",\"rowField\":{\"x\":0,\"width\":1,\"parse\":\"hex\",\"type\":\"rowIndex\",\"key\":\"chainPos\",\"description\":\"Position within the chain (0-F, max 16 entries).\"},\"columns\":[{\"x\":2,\"width\":2,\"key\":\"phrase\",\"type\":\"phraseRef\",\"label\":\"Phrase (PH)\",\"description\":\"Phrase number to play at this chain position.\"},{\"x\":5,\"width\":2,\"key\":\"transpose\",\"type\":\"transpose\",\"label\":\"Transpose (TSP)\",\"description\":\"Optional note transpose in semitones applied to every note in the phrase (relative hex).\"}]},\"phrase\":{\"title\":\"Phrase View\",\"type\":\"grid\",\"description\":\"A 16-step sequence. Each step holds a note, velocity, instrument assignment, and up to 3 FX command+value pairs. Notes cover 11 octaves (the full 128 MIDI note range); velocity defaults to 64 (hex); an empty instrument column plays the note without retriggering the instrument. A '*' next to the phrase number indicates the phrase is used elsewhere in the chain or song.\",\"rowField\":{\"x\":0,\"width\":1,\"parse\":\"hex\",\"type\":\"rowIndex\",\"key\":\"step\",\"description\":\"Step index 0-F (16th note by default; altered by groove).\"},\"columns\":[{\"x\":2,\"width\":3,\"key\":\"note\",\"type\":\"note\",\"label\":\"Note (N)\"},{\"x\":6,\"width\":2,\"key\":\"vel\",\"type\":\"velocity\",\"label\":\"Velocity (V)\"},{\"x\":9,\"width\":2,\"key\":\"inst\",\"type\":\"instrumentRef\",\"label\":\"Instrument (I)\"},{\"x\":12,\"width\":3,\"key\":\"fx1cmd\",\"type\":\"fxCommand\",\"label\":\"FX1 Command\"},{\"x\":15,\"width\":2,\"key\":\"fx1val\",\"type\":\"fxValue\",\"label\":\"FX1 Value\"},{\"x\":18,\"width\":3,\"key\":\"fx2cmd\",\"type\":\"fxCommand\",\"label\":\"FX2 Command\"},{\"x\":21,\"width\":2,\"key\":\"fx2val\",\"type\":\"fxValue\",\"label\":\"FX2 Value\"},{\"x\":24,\"width\":3,\"key\":\"fx3cmd\",\"type\":\"fxCommand\",\"label\":\"FX3 Command\"},{\"x\":27,\"width\":2,\"key\":\"fx3val\",\"type\":\"fxValue\",\"label\":\"FX3 Value\"}]},\"table\":{\"title\":\"Table View\",\"type\":\"grid\",\"description\":\"A 16-step mini-sequencer that runs alongside an instrument. Used for arpeggios, multi-stage envelopes, and automated FX. The 'N' column transposes the instrument for the step (relative hex); 'V' multiplies with the phrase-row velocity. Each instrument has a dedicated table plus 128 assignable tables (TBL command). FX columns can run at independent speeds via TIC; a TIC on the last table row affects all rows. HOP shortens or loops the table.\",\"rowField\":{\"x\":0,\"width\":1,\"parse\":\"hex\",\"type\":\"rowIndex\",\"key\":\"step\",\"description\":\"Table step 0-F. Loops from bottom unless a HOP command redirects.\"},\"columns\":[{\"x\":2,\"width\":2,\"key\":\"transpose\",\"type\":\"transpose\",\"label\":\"Transpose (N)\",\"description\":\"Transposes the instrument for this step (relative hex). '--' = no transpose.\"},{\"x\":5,\"width\":2,\"key\":\"volume\",\"type\":\"volume\",\"label\":\"Volume (V)\",\"description\":\"Volume multiplier applied on top of the phrase velocity (hex). '--' = no override.\"},{\"x\":8,\"width\":3,\"key\":\"fx1cmd\",\"type\":\"fxCommand\",\"label\":\"FX1 Command\",\"description\":\"3-char command name for table FX slot 1.\"},{\"x\":11,\"width\":2,\"key\":\"fx1val\",\"type\":\"fxValue\",\"label\":\"FX1 Value\",\"description\":\"Value for table FX1.\"},{\"x\":14,\"width\":3,\"key\":\"fx2cmd\",\"type\":\"fxCommand\",\"label\":\"FX2 Command\",\"description\":\"3-char command name for table FX slot 2.\"},{\"x\":17,\"width\":2,\"key\":\"fx2val\",\"type\":\"fxValue\",\"label\":\"FX2 Value\",\"description\":\"Value for table FX2.\"},{\"x\":20,\"width\":3,\"key\":\"fx3cmd\",\"type\":\"fxCommand\",\"label\":\"FX3 Command\",\"description\":\"3-char command name for table FX slot 3.\"},{\"x\":23,\"width\":2,\"key\":\"fx3val\",\"type\":\"fxValue\",\"label\":\"FX3 Value\",\"description\":\"Value for table FX3.\"}]},\"groove\":{\"title\":\"Groove View\",\"type\":\"grid\",\"description\":\"Defines the timing of each phrase step in ticks. Used to create swing, shuffle, triplets, and other rhythmic feels. At the default 24 PPQ a step is 6 ticks and a full phrase totals 96 ticks. The groove loops when a '--' entry is reached; a '00' entry skips the phrase step. Row 0 also holds the PPQ resolution (24/48/96/192) and the SWING percentage; editing the row 0 tick value alters rows 0 and 1 together (a common swing setting is 07,05 or 08,04).\",\"rowField\":{\"x\":0,\"width\":1,\"parse\":\"hex\",\"type\":\"rowIndex\",\"key\":\"step\",\"description\":\"Groove step 0-F. The groove loops when a '--' entry is reached.\"},\"columns\":[{\"x\":2,\"width\":2,\"key\":\"ticks\",\"type\":\"ticks\",\"label\":\"Ticks (TIC)\",\"description\":\"Number of ticks this step takes. Default is 06 (at 24 PPQN = 1 sixteenth note). '00' skips the step.\"},{\"x\":6,\"width\":2,\"key\":\"ppq\",\"type\":\"ppq\",\"label\":\"PPQ\",\"description\":\"Pulses Per Quarter note: 24, 48, 96 or 192. Only present on groove row 0; absent on all other rows.\"},{\"x\":10,\"width\":5,\"key\":\"swing\",\"type\":\"swing\",\"label\":\"Swing\",\"description\":\"Swing amount as a percentage (e.g. '50.0%' = straight). Only present on groove row 0; absent on all other rows.\"}]},\"scale\":{\"title\":\"Scale View\",\"type\":\"grid\",\"description\":\"Defines and quantizes note-related events (project/chain transpose, note entry, ARP, PIT, and the MIDIOUT instrument CHD and ADD commands). Up to 16 user-defined scales per project; scale '00' is the default for all 8 tracks and can be overridden per track with SCA/SCG FX commands. Each of the 12 note intervals can be enabled ('EN' = ON) with an optional detuning offset (-24.00 to +24.00 semitones). Rows below the 12 intervals (not column-mapped) hold: KEY (global root note for the default scale), TUNE (A4 reference in Hz, e.g. '440.00'), NAME (up to 16 characters, 92 factory scales load from /Scales/Factory), and the LOAD/SAVE actions.\",\"columns\":[{\"x\":0,\"width\":2,\"key\":\"interval\",\"type\":\"noteInterval\",\"label\":\"Note Interval\",\"description\":\"Note interval row label: C, C#, D, D#, E, F, F#, G, G#, A, A#, B.\"},{\"x\":3,\"width\":3,\"key\":\"en\",\"type\":\"onOff\",\"label\":\"Enabled (EN)\",\"description\":\"Enables this note interval in the scale: 'ON' or 'OFF' ('--' = unset).\"},{\"x\":6,\"width\":6,\"key\":\"offset\",\"type\":\"semitoneOffset\",\"label\":\"Offset (OFFSET)\",\"description\":\"Detune offset in semitones, decimal with two decimals (-24.00 to +24.00), e.g. '00.00'.\"}]},\"instrumentpool\":{\"title\":\"Instrument Pool\",\"type\":\"grid\",\"description\":\"Lists all 128 instrument slots with their mix routing. Each row shows the instrument name (up to 12 chars, '------------' = empty slot) and its dry/FX send levels. Mixer-related settings can be adjusted per instrument here; [EDIT] on an empty slot loads an instrument or sample.\",\"rowField\":{\"x\":0,\"width\":2,\"parse\":\"hex\",\"type\":\"rowIndex\",\"key\":\"instIndex\",\"description\":\"Instrument slot index 00-7F.\"},\"columns\":[{\"x\":3,\"width\":12,\"key\":\"name\",\"type\":\"instrumentName\",\"label\":\"Instrument Name\",\"description\":\"Name of the instrument in this slot (up to 12 chars). '------------' = empty slot.\"},{\"x\":16,\"width\":2,\"key\":\"dry\",\"type\":\"volume\",\"label\":\"Dry (DRY)\",\"description\":\"Direct output volume for this instrument (hex 00-FF).\"},{\"x\":19,\"width\":2,\"key\":\"mx\",\"type\":\"volume\",\"label\":\"ModFX (MX)\",\"description\":\"ModFX send level (hex 00-FF).\"},{\"x\":22,\"width\":2,\"key\":\"de\",\"type\":\"volume\",\"label\":\"Delay (DE)\",\"description\":\"Delay send level (hex 00-FF).\"},{\"x\":25,\"width\":2,\"key\":\"rv\",\"type\":\"volume\",\"label\":\"Reverb (RV)\",\"description\":\"Reverb send level (hex 00-FF).\"},{\"x\":28,\"width\":2,\"key\":\"eq\",\"type\":\"eqSlot\",\"label\":\"EQ Slot (EQ)\",\"description\":\"EQ slot assignment. '--' = no EQ.\"}]},\"inst\":{\"title\":\"Instrument View\",\"type\":\"parameter\",\"description\":\"Per-instrument parameters for the selected slot: TYPE (Wavsynth, Macrosynth, Sampler, FM Synth, Hypersynth, External Instrument, MIDI Out), LOAD/SAVE presets, NAME, TRANSP. (enable/disable transposition from scales and transpose columns), TABLE TIC (default table speed), and EQ slot, plus the synth engine parameters. Almost every parameter has an associated FX command; highlighting a parameter and navigating to the phrase or table view sets that parameter's FX command as default.\"},\"instmods\":{\"title\":\"Instrument Modulation View\",\"type\":\"parameter\",\"description\":\"Four configurable modulation slots per instrument: AHD envelope, ADSR envelope, DRUM envelope, LFO, TRIG envelope, or TRACKING, each with a destination parameter (DEST) and optional source (SRC).\"},\"mixer\":{\"title\":\"Mixer View\",\"type\":\"parameter\",\"description\":\"Mix levels for the 8 tracks, the 3 send effects (MX/DE/RE), the analog input (INPUT, with dual-mono mode) and USB input, the DJ filter (DJF), OTT compression, main EQ, limiter (LIM) and main volume (MIX). All signals are summed through OTT, main EQ, limiter, DJ filter and main volume. Most values are also exposed as FX commands (VMV, VMX, VDE, VRE, VT1-8, DJC/DJR/DJT, IVO/IMX/IDE/IRV, USB, ...).\"},\"project\":{\"title\":\"Project View\",\"type\":\"parameter\",\"description\":\"Song-wide settings such as tempo, project transpose (identical to the TSP FX command), and LIVE QUANTIZE behavior for cueing tracks in live mode. [SHIFT]+[OPTION] stores a song snapshot; [SHIFT]+[EDIT] recalls it.\"},\"effectsettings\":{\"title\":\"Effect Settings View\",\"type\":\"parameter\",\"description\":\"Settings for the 3 send effects: ModFX type/depth/frequency/width, Delay time/feedback/width, and Reverb size/decay/modulation/width/freeze — all addressable via the XMT-XMW, XDT-XDR and XRS-XRZ FX commands.\"},\"mixeq\":{\"title\":\"Mix EQ View\",\"type\":\"parameter\",\"description\":\"EQ editor for the main mix EQ slot (assign with the EQM FX command). Selecting EQ in the Mixer view and pressing [EDIT] or [SHIFT]+[RIGHT] opens this view.\"},\"modfxeq\":{\"title\":\"ModFX EQ View\",\"type\":\"parameter\",\"description\":\"EQ editor for the ModFX send path.\"},\"delayeq\":{\"title\":\"Delay EQ View\",\"type\":\"parameter\",\"description\":\"EQ editor for the Delay send path.\"},\"reverbeq\":{\"title\":\"Reverb EQ View\",\"type\":\"parameter\",\"description\":\"EQ editor for the Reverb send path.\"},\"mixscope\":{\"title\":\"Mix Scope View\",\"type\":\"parameter\",\"description\":\"Graphical scope of the main mix activity. Opened from the Mixer view with MIX selected via [EDIT] or [SHIFT]+[RIGHT].\"},\"limiterscope\":{\"title\":\"Limiter Scope View\",\"type\":\"parameter\",\"description\":\"Graphical scope of the limiter's compression activity (the white line on the MIX indicator). Opened from the Mixer view with LIM selected via [EDIT] or [SHIFT]+[RIGHT].\"},\"systemsettings\":{\"title\":\"System Settings View\",\"type\":\"parameter\",\"description\":\"System-level settings for the device.\"},\"loadinstrument\":{\"title\":\"Load Instrument\",\"type\":\"parameter\",\"description\":\"File browser for loading an instrument preset from the SD card ([EDIT] on an empty instrument slot, or LOAD in the Instrument view).\"},\"selectsavedirectory\":{\"title\":\"Select Save Directory\",\"type\":\"parameter\",\"description\":\"File browser directory-selection step when saving a preset or project.\"},\"createdirectory\":{\"title\":\"Create Directory\",\"type\":\"parameter\",\"description\":\"File browser dialog for creating a new directory on the SD card.\"}}"),
385
34
  fxCommands: {
386
35
  sequencer: {
387
36
  ARP: { description: "Arpeggio: plays 3 notes rapidly. X=first interval semitones, Y=second interval semitones." },
388
37
  ARC: { description: "Arpeggio Config: X=mode, Y=speed in ticks." },
389
- CHA: { description: "Chance: X=probability left of command (0=never, F=always), Y=probability right." },
38
+ CHA: { description: "Chance: X=probability left of command (0=never, F=always), Y=probability right. In a table: probability for everything to the left of CHA from 00 (never) to FF (always)." },
390
39
  DEL: { description: "Delay: in phrase delays entire row by XX ticks; in table delays table playhead." },
391
40
  GRV: { description: "Groove: sets groove number XX for the current track." },
392
41
  GGR: { description: "Global Groove: sets groove XX for all tracks." },
@@ -394,9 +43,9 @@ var r = {
394
43
  INS: { description: "Instrument: changes active instrument to XX as an FX command." },
395
44
  KIL: { description: "Kill: stops the playing instrument after XX ticks." },
396
45
  RND: { description: "Random: randomises the preceding FX command. X=left range, Y=right range (0=none, F=full)." },
397
- RNL: { description: "Randomise Left: randomises the FX command to the left." },
46
+ RNL: { description: "Randomise Left: randomises the FX command to the left (X=left range, Y=right range). In the first column RNL randomises the note and instrument number in phrase, or note and velocity in table." },
398
47
  RET: { description: "Retrig: retriggers current row. Y≠0: retrig every Y ticks with X volume ramp. Y=0: single retrig after X ticks." },
399
- REP: { description: "Repeat: repeats and increments the last FX command by XX each step." },
48
+ REP: { description: "Repeat: repeats and increments the last FX command by XX each step. Active until a new command or REP00 stops it; it does not need to appear on every row. A double-caret '^^' appears next to an FX lane while a REP from a previous phrase is active." },
400
49
  RTO: { description: "Repeat To: sets the min/max value that REP stops at." },
401
50
  RMX: { description: "Remix: repositions the phrase playhead (Y) on tracks to the left (selected by X)." },
402
51
  NTH: { description: "Nth Trigger: conditional trigger based on loop count." },
@@ -406,17 +55,17 @@ var r = {
406
55
  PVX: { description: "Extreme Vibrato: like PVB but with higher range." },
407
56
  SCA: { description: "Track Scale: X=key signature, Y=scale number for this track." },
408
57
  SCG: { description: "Global Scale: X=key, Y=scale number for the whole song." },
409
- SNG: { description: "Song Hop: moves this track's song position by XX rows relative to current." },
58
+ SNG: { description: "Song Hop: moves this track's song position by XX rows relative to current. Ignored if the resulting song position is not playable." },
410
59
  SED: { description: "Random Seed: sets the random seed XX for this track." },
411
60
  TBL: { description: "Table: assigns table number XX to the current instrument." },
412
61
  THO: { description: "Table Hop: jumps all table columns to position 0X." },
413
- TIC: { description: "Table Tick: sets the table tick rate. In a table, each FX column can run independently." },
414
- TBX: { description: "Aux Table: runs table XX in parallel with the instrument table. 00=stop aux table." },
62
+ TIC: { description: "Table Tick: sets the table tick rate. 00 = increment row on each instrument trigger; 01-FB = ticks per row; FC = Octave Map (playing octave maps to table row); FD = Velocity Map; FE = Note Map (pair with HOP00 on row 0C to limit to 12 notes/octave); FF = increment at 200 Hz. In a table each FX column can run independently; a TIC on the last table row affects all rows." },
63
+ TBX: { description: "Aux Table: runs table XX in parallel with the instrument table. 00=stop aux table. A TIC in the same row as a TBX affects the auxiliary table's tick rate." },
415
64
  TPO: { description: "Tempo: sets song BPM to XX (hex → decimal conversion shown in help text)." },
416
- TSP: { description: "Global Transpose: transposes the entire song by XX semitones." },
417
- NXT: { description: "Next Track Trigger: plays instrument XX on the next track using current note/velocity." },
65
+ TSP: { description: "Global Song Transpose: transposes the entire song by XX semitones. Identical to the transpose value in the Project View." },
66
+ NXT: { description: "Next Track Trigger: plays instrument XX on the track to the right using the current note/velocity. Does not work on track 8." },
418
67
  OFF: { description: "Note Off: stops the playing instrument after XX ticks. Triggers ADSR release stage." },
419
- MTT: { description: "Micro Time: adjusts row playback timing in sub-tick (1/8 tick) amounts." }
68
+ MTT: { description: "Micro Time: adjusts row playback timing in positive or negative sub-tick (1/8 tick) amounts. Negative amounts do not work on the first phrase row. Currently has no action in tables." }
420
69
  },
421
70
  instrument: {
422
71
  VOL: { description: "Volume offset (relative)." },
@@ -437,7 +86,9 @@ var r = {
437
86
  LF1: { description: "LFO 1 Frequency offset." },
438
87
  LF2: { description: "LFO 2 Frequency offset." },
439
88
  LT1: { description: "Retrigger LFO 1 (value sets phase offset)." },
440
- LT2: { description: "Retrigger LFO 2." }
89
+ LT2: { description: "Retrigger LFO 2." },
90
+ CHD: { description: "Chord (MIDIOUT instrument): triggers a chord. Quantized by the active scale." },
91
+ ADD: { description: "Add Note (MIDIOUT instrument): adds a note to the chord. Quantized by the active scale." }
441
92
  },
442
93
  mixer: {
443
94
  VMV: { description: "Main Volume: sets the main song output volume." },
@@ -456,7 +107,32 @@ var r = {
456
107
  DJR: { description: "DJ Filter Resonance." },
457
108
  DJT: { description: "DJ Filter Type." },
458
109
  EQM: { description: "Assign main mix EQ slot." },
459
- EQI: { description: "Assign current instrument EQ slot." }
110
+ EQI: { description: "Assign current instrument EQ slot." },
111
+ XMT: { description: "Send Effect: ModFX mod type and phase position (Effect Settings view)." },
112
+ XMM: { description: "Send Effect: ModFX mod depth (Effect Settings view)." },
113
+ XMF: { description: "Send Effect: ModFX mod frequency (Effect Settings view)." },
114
+ XMW: { description: "Send Effect: ModFX stereo width (Effect Settings view)." },
115
+ XMR: { description: "Send Effect: ModFX to reverb mix (Effect Settings view)." },
116
+ XDT: { description: "Send Effect: Delay time — X = left, Y = right, in large increments (Effect Settings view)." },
117
+ XDF: { description: "Send Effect: Delay feedback (Effect Settings view)." },
118
+ XDW: { description: "Send Effect: Delay stereo width (Effect Settings view)." },
119
+ XDR: { description: "Send Effect: Delay to reverb mix (Effect Settings view)." },
120
+ XRS: { description: "Send Effect: Reverb room size (Effect Settings view)." },
121
+ XRD: { description: "Send Effect: Reverb decay (Effect Settings view)." },
122
+ XRM: { description: "Send Effect: Reverb mod depth (Effect Settings view)." },
123
+ XRF: { description: "Send Effect: Reverb mod frequency (Effect Settings view)." },
124
+ XRW: { description: "Send Effect: Reverb stereo width (Effect Settings view)." },
125
+ XRZ: { description: "Send Effect: Reverb freeze — enabled with a value greater than 00, disabled at 00." },
126
+ IVO: { description: "Line Input Mixer: analog input volume (Mixer view)." },
127
+ IMX: { description: "Line Input Mixer: analog input ModFX send (Mixer view)." },
128
+ IDE: { description: "Line Input Mixer: analog input delay send (Mixer view)." },
129
+ IRV: { description: "Line Input Mixer: analog input reverb send (Mixer view). Also referenced as 'IRE' in the manual text." },
130
+ IRE: { description: "Alias of IRV: analog input reverb send (Mixer view)." },
131
+ IV2: { description: "Line Input Mixer: volume for the 2nd input (right channel in dual-mono input mode, set in the Mixer view)." },
132
+ IM2: { description: "Line Input Mixer: ModFX send for the 2nd input (right channel in dual-mono input mode)." },
133
+ ID2: { description: "Line Input Mixer: delay send for the 2nd input (right channel in dual-mono input mode)." },
134
+ IR2: { description: "Line Input Mixer: reverb send for the 2nd input (right channel in dual-mono input mode)." },
135
+ USB: { description: "USB input volume (Mixer view)." }
460
136
  }
461
137
  }
462
138
  }, i = r.fxCommands;
@@ -573,14 +249,14 @@ function h(e) {
573
249
  activeField: null
574
250
  };
575
251
  let { line: h, offset: g } = d(l), _ = e.cursorPos ? e.cursorPos.x - g : 0, v = n.rowField ? p(h, n.rowField) : null, y = {};
576
- for (let e of n.columns) y[e.key] = f(e, h);
252
+ for (let e of n.columns ?? []) y[e.key] = f(e, h);
577
253
  let b = {
578
254
  rowIndex: v,
579
255
  rowKey: n.rowField?.key ?? "row",
580
256
  fields: y
581
257
  }, x = null;
582
258
  if (e.cursorPos) {
583
- let e = m(n.columns, _);
259
+ let e = m(n.columns ?? [], _);
584
260
  if (e) {
585
261
  let n = y[e.key];
586
262
  n && (x = {
@@ -639,6 +315,8 @@ function _(e) {
639
315
  case "fxValue": return e.hexValue == null ? e.rawValue : `${e.rawValue}h (dec ${e.hexValue})`;
640
316
  case "ticks": return e.rawValue === "00" ? "00 (skip step)" : e.hexValue == null ? e.rawValue : `${e.rawValue}h (${e.hexValue} ticks)`;
641
317
  case "volume": return e.rawValue;
318
+ case "noteInterval": return `Interval ${e.rawValue}`;
319
+ case "semitoneOffset": return e.isEmpty ? "(empty)" : `${e.rawValue} st`;
642
320
  default: return e.rawValue;
643
321
  }
644
322
  }
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","names":[],"sources":["../src/m8-view-context.json","../src/viewContext.ts","../src/client.ts"],"sourcesContent":["{\r\n \"version\": \"1.0\",\r\n \"description\": \"Semantic mapping of M8 tracker views: defines the structure of each grid view so cursor position + line text can be interpreted as typed field values.\",\r\n \"notes\": {\r\n \"fontMode\": \"All x/width values assume font mode 0 (standard, 12×14 px cells, 40 cols × ~22 rows). Font modes 1 (bold uppercase) and 2 (large) may shift column positions.\",\r\n \"playbackIndicator\": \"The M8 renders a '>' character one column to the left of the first data column when that step/row/track is currently playing. This character is part of the text stream; the editing cursor is a separate rect overlay.\",\r\n \"emptyCell\": \"An empty or unused cell displays '--' (2 chars) or '---' (3 chars) or '---00' (command+value). Parsed hexValue will be null for these.\",\r\n \"relativeHex\": \"Transpose and some FX values use relative hex: 00 = no change, 01-7F = +1-127, FF-80 = -1-128. See 'Relative and Absolute FX Commands' in the manual.\",\r\n \"rowIndex\": \"Row/step indices are parsed from the leftmost characters of currentLine. They are hex for all grid views.\"\r\n },\r\n \"fieldTypes\": {\r\n \"chainRef\": \"Reference to a chain number (hex 00-FF). '--' means empty/unused.\",\r\n \"phraseRef\": \"Reference to a phrase number (hex 00-FF). '--' means empty/unused.\",\r\n \"note\": \"Musical note in M8 format: [A-G][#|-][0-9] e.g. 'C-4', 'A#3'. '---' = empty. 'OFF' = note-off.\",\r\n \"velocity\": \"Note velocity 00-7F (hex). Default is 64 (hex). '--' = inherit.\",\r\n \"instrumentRef\": \"Instrument number 00-7F (hex). '--' = no retrigger (keep current instrument).\",\r\n \"fxCommand\": \"3-character sequencer or instrument FX command name, e.g. 'KIL', 'ARP', 'DEL'. '---' = none.\",\r\n \"fxValue\": \"2-char hex value 00-FF accompanying an FX command. Interpretation depends on the command.\",\r\n \"transpose\": \"Semitone or tick transpose. Relative: 00=none, 01-7F=+1-+127, FF-80=-1--128.\",\r\n \"ticks\": \"Number of ticks this step consumes in a groove (hex 01-FF). '00' skips the step. '--' = empty (groove loops at this point).\",\r\n \"volume\": \"Volume multiplier 00-FF (hex). Multiplied with the phrase-row velocity. '--' = no override.\", \"ppq\": \"Pulses Per Quarter note: resolution of the groove clock (decimal). Only editable on groove row 0.\",\r\n \"instrumentName\": \"Instrument name string (up to 12 characters). Text field, no hex value.\",\r\n \"eqSlot\": \"EQ slot assignment (hex). '--' = no EQ applied to this instrument.\",\r\n \"rowIndex\": \"The positional index of this row in the current view (song row, chain position, phrase step, etc.).\"\r\n },\r\n \"views\": {\r\n \"song\": {\r\n \"title\": \"Song View\",\r\n \"type\": \"grid\",\r\n \"description\": \"Arranges chains across 8 independent tracks to form the song structure. Each row is a song position; each column is a track.\",\r\n \"rowField\": {\r\n \"x\": 0, \"width\": 2, \"parse\": \"hex\", \"type\": \"rowIndex\",\r\n \"key\": \"songRow\", \"description\": \"Song row 00-FF. Each track loops back to row 00 when it reaches '--'.\"\r\n },\r\n \"columns\": [\r\n { \"x\": 3, \"width\": 2, \"key\": \"track1\", \"type\": \"chainRef\", \"label\": \"Track 1\", \"meta\": { \"trackIndex\": 1 } },\r\n { \"x\": 6, \"width\": 2, \"key\": \"track2\", \"type\": \"chainRef\", \"label\": \"Track 2\", \"meta\": { \"trackIndex\": 2 } },\r\n { \"x\": 9, \"width\": 2, \"key\": \"track3\", \"type\": \"chainRef\", \"label\": \"Track 3\", \"meta\": { \"trackIndex\": 3 } },\r\n { \"x\": 12, \"width\": 2, \"key\": \"track4\", \"type\": \"chainRef\", \"label\": \"Track 4\", \"meta\": { \"trackIndex\": 4 } },\r\n { \"x\": 15, \"width\": 2, \"key\": \"track5\", \"type\": \"chainRef\", \"label\": \"Track 5\", \"meta\": { \"trackIndex\": 5 } },\r\n { \"x\": 18, \"width\": 2, \"key\": \"track6\", \"type\": \"chainRef\", \"label\": \"Track 6\", \"meta\": { \"trackIndex\": 6 } },\r\n { \"x\": 21, \"width\": 2, \"key\": \"track7\", \"type\": \"chainRef\", \"label\": \"Track 7\", \"meta\": { \"trackIndex\": 7 } },\r\n { \"x\": 24, \"width\": 2, \"key\": \"track8\", \"type\": \"chainRef\", \"label\": \"Track 8\", \"meta\": { \"trackIndex\": 8 } }\r\n ]\r\n },\r\n \"chain\": {\r\n \"title\": \"Chain View\",\r\n \"type\": \"grid\",\r\n \"description\": \"An ordered list of up to 16 phrases. The first '--' phrase ends the chain and loops back to the song.\",\r\n \"rowField\": {\r\n \"x\": 0, \"width\": 1, \"parse\": \"hex\", \"type\": \"rowIndex\",\r\n \"key\": \"chainPos\", \"description\": \"Position within the chain (0-F, max 16 entries).\"\r\n },\r\n \"columns\": [\r\n { \"x\": 2, \"width\": 2, \"key\": \"phrase\", \"type\": \"phraseRef\", \"label\": \"Phrase (PH)\", \"description\": \"Phrase number to play at this chain position.\" },\r\n { \"x\": 5, \"width\": 2, \"key\": \"transpose\", \"type\": \"transpose\", \"label\": \"Transpose (TSP)\", \"description\": \"Optional note transpose in semitones applied to every note in the phrase (relative hex).\" }\r\n ]\r\n },\r\n \"phrase\": {\r\n \"title\": \"Phrase View\",\r\n \"type\": \"grid\",\r\n \"description\": \"A 16-step sequence. Each step holds a note, velocity, instrument assignment, and up to 3 FX command+value pairs.\",\r\n \"rowField\": {\r\n \"x\": 0, \"width\": 1, \"parse\": \"hex\", \"type\": \"rowIndex\",\r\n \"key\": \"step\", \"description\": \"Step index 0-F (16th note by default; altered by groove).\"\r\n },\r\n \"columns\": [\r\n { \"x\": 2, \"width\": 3, \"key\": \"note\", \"type\": \"note\", \"label\": \"Note (N)\" },\r\n { \"x\": 6, \"width\": 2, \"key\": \"vel\", \"type\": \"velocity\", \"label\": \"Velocity (V)\" },\r\n { \"x\": 9, \"width\": 2, \"key\": \"inst\", \"type\": \"instrumentRef\", \"label\": \"Instrument (I)\" },\r\n { \"x\": 12, \"width\": 3, \"key\": \"fx1cmd\", \"type\": \"fxCommand\", \"label\": \"FX1 Command\" },\r\n { \"x\": 15, \"width\": 2, \"key\": \"fx1val\", \"type\": \"fxValue\", \"label\": \"FX1 Value\" },\r\n { \"x\": 18, \"width\": 3, \"key\": \"fx2cmd\", \"type\": \"fxCommand\", \"label\": \"FX2 Command\" },\r\n { \"x\": 21, \"width\": 2, \"key\": \"fx2val\", \"type\": \"fxValue\", \"label\": \"FX2 Value\" },\r\n { \"x\": 24, \"width\": 3, \"key\": \"fx3cmd\", \"type\": \"fxCommand\", \"label\": \"FX3 Command\" },\r\n { \"x\": 27, \"width\": 2, \"key\": \"fx3val\", \"type\": \"fxValue\", \"label\": \"FX3 Value\" }\r\n ]\r\n },\r\n \"table\": {\r\n \"title\": \"Table View\",\r\n \"type\": \"grid\",\r\n \"description\": \"A 16-step mini-sequencer that runs alongside an instrument. Used for arpeggios, multi-stage envelopes, and automated FX.\",\r\n \"rowField\": {\r\n \"x\": 0, \"width\": 1, \"parse\": \"hex\", \"type\": \"rowIndex\",\r\n \"key\": \"step\", \"description\": \"Table step 0-F. Loops from bottom unless a HOP command redirects.\"\r\n },\r\n \"columns\": [\r\n { \"x\": 2, \"width\": 2, \"key\": \"transpose\", \"type\": \"transpose\", \"label\": \"Transpose (N)\", \"description\": \"Transposes the instrument for this step (relative hex). '--' = no transpose.\" },\r\n { \"x\": 5, \"width\": 2, \"key\": \"volume\", \"type\": \"volume\", \"label\": \"Volume (V)\", \"description\": \"Volume multiplier applied on top of the phrase velocity (hex). '--' = no override.\" },\r\n { \"x\": 8, \"width\": 3, \"key\": \"fx1cmd\", \"type\": \"fxCommand\", \"label\": \"FX1 Command\", \"description\": \"3-char command name for table FX slot 1.\" },\r\n { \"x\": 11, \"width\": 2, \"key\": \"fx1val\", \"type\": \"fxValue\", \"label\": \"FX1 Value\", \"description\": \"Value for table FX1.\" },\r\n { \"x\": 14, \"width\": 3, \"key\": \"fx2cmd\", \"type\": \"fxCommand\", \"label\": \"FX2 Command\", \"description\": \"3-char command name for table FX slot 2.\" },\r\n { \"x\": 17, \"width\": 2, \"key\": \"fx2val\", \"type\": \"fxValue\", \"label\": \"FX2 Value\", \"description\": \"Value for table FX2.\" },\r\n { \"x\": 20, \"width\": 3, \"key\": \"fx3cmd\", \"type\": \"fxCommand\", \"label\": \"FX3 Command\", \"description\": \"3-char command name for table FX slot 3.\" },\r\n { \"x\": 23, \"width\": 2, \"key\": \"fx3val\", \"type\": \"fxValue\", \"label\": \"FX3 Value\", \"description\": \"Value for table FX3.\" }\r\n ]\r\n },\r\n \"groove\": {\r\n \"title\": \"Groove View\",\r\n \"type\": \"grid\",\r\n \"description\": \"Defines the timing of each phrase step in ticks. Used to create swing, shuffle, triplets, and other rhythmic feels.\",\r\n \"rowField\": {\r\n \"x\": 0, \"width\": 1, \"parse\": \"hex\", \"type\": \"rowIndex\",\r\n \"key\": \"step\", \"description\": \"Groove step 0-F. The groove loops when a '--' entry is reached.\"\r\n },\r\n \"columns\": [\r\n { \"x\": 2, \"width\": 2, \"key\": \"ticks\", \"type\": \"ticks\", \"label\": \"Ticks (TIC)\", \"description\": \"Number of ticks this step takes. Default is 06 (at 24 PPQN = 1 sixteenth note). '00' skips the step.\" },\r\n { \"x\": 6, \"width\": 2, \"key\": \"ppq\", \"type\": \"ppq\", \"label\": \"PPQ\", \"description\": \"Pulses Per Quarter note. Only present on groove row 0; absent on all other rows.\" }\r\n ]\r\n },\r\n \"instrumentpool\": {\r\n \"title\": \"Instrument Pool\",\r\n \"type\": \"grid\",\r\n \"description\": \"Lists all instrument slots with their mix routing. Each row shows the instrument name and its dry/FX send levels.\",\r\n \"rowField\": {\r\n \"x\": 0, \"width\": 2, \"parse\": \"hex\", \"type\": \"rowIndex\",\r\n \"key\": \"instIndex\", \"description\": \"Instrument slot index 00-7F.\"\r\n },\r\n \"columns\": [\r\n { \"x\": 3, \"width\": 12, \"key\": \"name\", \"type\": \"instrumentName\", \"label\": \"Instrument Name\", \"description\": \"Name of the instrument in this slot (up to 12 chars). '------------' = empty slot.\" },\r\n { \"x\": 16, \"width\": 2, \"key\": \"dry\", \"type\": \"volume\", \"label\": \"Dry (DRY)\", \"description\": \"Direct output volume for this instrument (hex 00-FF).\" },\r\n { \"x\": 19, \"width\": 2, \"key\": \"mx\", \"type\": \"volume\", \"label\": \"ModFX (MX)\", \"description\": \"ModFX send level (hex 00-FF).\" },\r\n { \"x\": 22, \"width\": 2, \"key\": \"de\", \"type\": \"volume\", \"label\": \"Delay (DE)\", \"description\": \"Delay send level (hex 00-FF).\" },\r\n { \"x\": 25, \"width\": 2, \"key\": \"rv\", \"type\": \"volume\", \"label\": \"Reverb (RV)\", \"description\": \"Reverb send level (hex 00-FF).\" },\r\n { \"x\": 28, \"width\": 2, \"key\": \"eq\", \"type\": \"eqSlot\", \"label\": \"EQ Slot (EQ)\", \"description\": \"EQ slot assignment. '--' = no EQ.\" }\r\n ]\r\n }\r\n },\r\n \"fxCommands\": {\r\n \"sequencer\": {\r\n \"ARP\": { \"description\": \"Arpeggio: plays 3 notes rapidly. X=first interval semitones, Y=second interval semitones.\" },\r\n \"ARC\": { \"description\": \"Arpeggio Config: X=mode, Y=speed in ticks.\" },\r\n \"CHA\": { \"description\": \"Chance: X=probability left of command (0=never, F=always), Y=probability right.\" },\r\n \"DEL\": { \"description\": \"Delay: in phrase delays entire row by XX ticks; in table delays table playhead.\" },\r\n \"GRV\": { \"description\": \"Groove: sets groove number XX for the current track.\" },\r\n \"GGR\": { \"description\": \"Global Groove: sets groove XX for all tracks.\" },\r\n \"HOP\": { \"description\": \"Hop: in phrase jumps to row Y of next phrase; HOPFF stops track. In table loops to row Y X times.\" },\r\n \"INS\": { \"description\": \"Instrument: changes active instrument to XX as an FX command.\" },\r\n \"KIL\": { \"description\": \"Kill: stops the playing instrument after XX ticks.\" },\r\n \"RND\": { \"description\": \"Random: randomises the preceding FX command. X=left range, Y=right range (0=none, F=full).\" },\r\n \"RNL\": { \"description\": \"Randomise Left: randomises the FX command to the left.\" },\r\n \"RET\": { \"description\": \"Retrig: retriggers current row. Y≠0: retrig every Y ticks with X volume ramp. Y=0: single retrig after X ticks.\" },\r\n \"REP\": { \"description\": \"Repeat: repeats and increments the last FX command by XX each step.\" },\r\n \"RTO\": { \"description\": \"Repeat To: sets the min/max value that REP stops at.\" },\r\n \"RMX\": { \"description\": \"Remix: repositions the phrase playhead (Y) on tracks to the left (selected by X).\" },\r\n \"NTH\": { \"description\": \"Nth Trigger: conditional trigger based on loop count.\" },\r\n \"PSL\": { \"description\": \"Pitch Slide (portamento): enables portamento for XX ticks.\" },\r\n \"PBN\": { \"description\": \"Pitch Bend: continuous pitch slide up (00-7F) or down (80-FF).\" },\r\n \"PVB\": { \"description\": \"Vibrato: X=speed, Y=depth.\" },\r\n \"PVX\": { \"description\": \"Extreme Vibrato: like PVB but with higher range.\" },\r\n \"SCA\": { \"description\": \"Track Scale: X=key signature, Y=scale number for this track.\" },\r\n \"SCG\": { \"description\": \"Global Scale: X=key, Y=scale number for the whole song.\" },\r\n \"SNG\": { \"description\": \"Song Hop: moves this track's song position by XX rows relative to current.\" },\r\n \"SED\": { \"description\": \"Random Seed: sets the random seed XX for this track.\" },\r\n \"TBL\": { \"description\": \"Table: assigns table number XX to the current instrument.\" },\r\n \"THO\": { \"description\": \"Table Hop: jumps all table columns to position 0X.\" },\r\n \"TIC\": { \"description\": \"Table Tick: sets the table tick rate. In a table, each FX column can run independently.\" },\r\n \"TBX\": { \"description\": \"Aux Table: runs table XX in parallel with the instrument table. 00=stop aux table.\" },\r\n \"TPO\": { \"description\": \"Tempo: sets song BPM to XX (hex → decimal conversion shown in help text).\" },\r\n \"TSP\": { \"description\": \"Global Transpose: transposes the entire song by XX semitones.\" },\r\n \"NXT\": { \"description\": \"Next Track Trigger: plays instrument XX on the next track using current note/velocity.\" },\r\n \"OFF\": { \"description\": \"Note Off: stops the playing instrument after XX ticks. Triggers ADSR release stage.\" },\r\n \"MTT\": { \"description\": \"Micro Time: adjusts row playback timing in sub-tick (1/8 tick) amounts.\" }\r\n },\r\n \"instrument\": {\r\n \"VOL\": { \"description\": \"Volume offset (relative).\" },\r\n \"PIT\": { \"description\": \"Pitch offset in semitones (relative).\" },\r\n \"FIN\": { \"description\": \"Fine tune: pitch offset -1 to +1 semitones (relative).\" },\r\n \"EA1\": { \"description\": \"Envelope 1 Amount offset.\" },\r\n \"EA2\": { \"description\": \"Envelope 2 Amount offset.\" },\r\n \"AT1\": { \"description\": \"Envelope 1 Attack time offset.\" },\r\n \"AT2\": { \"description\": \"Envelope 2 Attack time offset.\" },\r\n \"HO1\": { \"description\": \"Envelope 1 Hold time offset.\" },\r\n \"HO2\": { \"description\": \"Envelope 2 Hold time offset.\" },\r\n \"DE1\": { \"description\": \"Envelope 1 Decay time offset.\" },\r\n \"DE2\": { \"description\": \"Envelope 2 Decay time offset.\" },\r\n \"ET1\": { \"description\": \"Retrigger Envelope 1 (any value > 00 retriggers).\" },\r\n \"ET2\": { \"description\": \"Retrigger Envelope 2.\" },\r\n \"LA1\": { \"description\": \"LFO 1 Amount offset.\" },\r\n \"LA2\": { \"description\": \"LFO 2 Amount offset.\" },\r\n \"LF1\": { \"description\": \"LFO 1 Frequency offset.\" },\r\n \"LF2\": { \"description\": \"LFO 2 Frequency offset.\" },\r\n \"LT1\": { \"description\": \"Retrigger LFO 1 (value sets phase offset).\" },\r\n \"LT2\": { \"description\": \"Retrigger LFO 2.\" }\r\n },\r\n \"mixer\": {\r\n \"VMV\": { \"description\": \"Main Volume: sets the main song output volume.\" },\r\n \"VMX\": { \"description\": \"ModFX Volume.\" },\r\n \"VDE\": { \"description\": \"Delay Volume.\" },\r\n \"VRE\": { \"description\": \"Reverb Volume.\" },\r\n \"VT1\": { \"description\": \"Track 1 Volume.\" },\r\n \"VT2\": { \"description\": \"Track 2 Volume.\" },\r\n \"VT3\": { \"description\": \"Track 3 Volume.\" },\r\n \"VT4\": { \"description\": \"Track 4 Volume.\" },\r\n \"VT5\": { \"description\": \"Track 5 Volume.\" },\r\n \"VT6\": { \"description\": \"Track 6 Volume.\" },\r\n \"VT7\": { \"description\": \"Track 7 Volume.\" },\r\n \"VT8\": { \"description\": \"Track 8 Volume.\" },\r\n \"DJC\": { \"description\": \"DJ Filter Cutoff.\" },\r\n \"DJR\": { \"description\": \"DJ Filter Resonance.\" },\r\n \"DJT\": { \"description\": \"DJ Filter Type.\" },\r\n \"EQM\": { \"description\": \"Assign main mix EQ slot.\" },\r\n \"EQI\": { \"description\": \"Assign current instrument EQ slot.\" }\r\n }\r\n }\r\n}\r\n","/**\r\n * viewContext.ts\r\n *\r\n * Derives semantic meaning from raw M8State by mapping cursor position and\r\n * currentLine content to typed, labelled fields using the m8-view-context.json schema.\r\n *\r\n * The five grid views (song, chain, phrase, table, groove) are fully supported.\r\n * Parameter views (inst, mixer, project, …) return a minimal context with viewName only.\r\n */\r\n\r\nimport type { M8State } from './types'\r\nimport schema from './m8-view-context.json'\r\n\r\n// Public field type union \r\n\r\nexport type M8FieldType =\r\n | 'chainRef'\r\n | 'phraseRef'\r\n | 'note'\r\n | 'velocity'\r\n | 'instrumentRef'\r\n | 'fxCommand'\r\n | 'fxValue'\r\n | 'transpose'\r\n | 'ticks'\r\n | 'volume'\r\n | 'ppq'\r\n | 'instrumentName'\r\n | 'eqSlot'\r\n | 'rowIndex'\r\n\r\n// Schema shapes (mirror m8-view-context.json) \r\n\r\ninterface ColumnDef {\r\n x: number\r\n width: number\r\n key: string\r\n type: M8FieldType\r\n label: string\r\n description?: string\r\n meta?: Record<string, unknown>\r\n}\r\n\r\ninterface RowFieldDef {\r\n x: number\r\n width: number\r\n parse: 'hex' | 'decimal'\r\n type: M8FieldType\r\n key: string\r\n description?: string\r\n}\r\n\r\ninterface ViewDef {\r\n title: string\r\n type: 'grid' | 'parameter'\r\n description: string\r\n rowField?: RowFieldDef\r\n columns: ColumnDef[]\r\n}\r\n\r\n// Public result types \r\n\r\n/**\r\n * A single parsed field value extracted from the currentLine.\r\n */\r\nexport interface M8ParsedField {\r\n /** Column key as defined in the schema (e.g. 'fx1cmd', 'note', 'track3'). */\r\n key: string\r\n /** Human-readable label (e.g. 'FX1 Command', 'Note (N)'). */\r\n label: string\r\n /** Semantic type of this field. */\r\n type: M8FieldType\r\n /** Raw text extracted from currentLine (trimmed). */\r\n rawValue: string\r\n /**\r\n * Parsed integer (from hex) when the field holds a numeric value.\r\n * `null` for empty cells ('--', '---') and text-only types (note name, fxCommand).\r\n */\r\n hexValue: number | null\r\n /** True when the cell is empty/unused ('--', '---', '---00'). */\r\n isEmpty: boolean\r\n /** Extra view-specific metadata (e.g. { trackIndex: 2 } for song view track columns). */\r\n meta?: Record<string, unknown>\r\n}\r\n\r\n/**\r\n * All fields parsed from the current row (line at cursorPos.y).\r\n */\r\nexport interface M8ParsedRow {\r\n /** Row index (hex-parsed) derived from the leftmost chars of currentLine. null if not parseable. */\r\n rowIndex: number | null\r\n /** Schema key for the row index field (e.g. 'songRow', 'step', 'chainPos'). */\r\n rowKey: string\r\n /** Map of column key → parsed field for every column in this view. */\r\n fields: Record<string, M8ParsedField>\r\n}\r\n\r\n/**\r\n * The field currently under the cursor, plus row context.\r\n */\r\nexport interface M8ActiveField extends M8ParsedField {\r\n /** Repetition of the view name for convenience. */\r\n viewName: string\r\n /** Row index of the line containing this field (null if not parseable). */\r\n rowIndex: number | null\r\n}\r\n\r\n/**\r\n * Full semantic context for the current M8State.\r\n */\r\nexport interface M8SemanticContext {\r\n /** M8 view name (lowercased), e.g. 'song', 'phrase'. */\r\n viewName: string\r\n /** Human-readable view title from schema ('Song View', 'Phrase View', …). */\r\n viewTitle: string | null\r\n /** View description from the schema. */\r\n viewDescription: string | null\r\n /**\r\n * The numeric ID of the current view item, parsed from the screen title.\r\n * e.g. 'F2' from ' CHAIN F2*', '03' from ' PHRASE 03'.\r\n * null for views that have no item number (song, instrument pool).\r\n */\r\n viewId: string | null\r\n /** True when the title carries a '*' suffix, indicating unsaved changes. */\r\n isUnsaved: boolean\r\n /** Whether this view is a structured grid (song/chain/phrase/table/groove). */\r\n isGridView: boolean\r\n /** All fields on the line at cursorPos.y (null for non-grid views or missing line). */\r\n row: M8ParsedRow | null\r\n /** The specific field under the cursor (null when cursor is out of any column range). */\r\n activeField: M8ActiveField | null\r\n}\r\n\r\n// FX command lookup \r\n\r\ntype FxCategory = 'sequencer' | 'instrument' | 'mixer'\r\ntype FxCommandMap = Record<string, { description: string }>\r\n\r\nconst fxCommands = schema.fxCommands as Record<FxCategory, FxCommandMap>\r\n\r\n/**\r\n * Look up the description of a 3-character FX command name.\r\n * Returns undefined if the command is unknown.\r\n */\r\nexport function lookupFxCommand(cmd: string): { description: string } | undefined {\r\n const upper = cmd.toUpperCase()\r\n for (const cat of Object.values(fxCommands)) {\r\n if (upper in cat) return cat[upper]\r\n }\r\n return undefined\r\n}\r\n\r\n// Internal helpers \r\n\r\nconst EMPTY_PATTERNS = new Set(['--', '---', '---00', '----', '-'])\r\n\r\nfunction isEmptyRaw(raw: string): boolean {\r\n if (!raw) return true\r\n // Also count strings that are all dashes\r\n return EMPTY_PATTERNS.has(raw) || /^-+$/.test(raw)\r\n}\r\n\r\nfunction parseHex(raw: string): number | null {\r\n const trimmed = raw.trim()\r\n if (!trimmed || isEmptyRaw(trimmed)) return null\r\n const n = parseInt(trimmed, 16)\r\n return Number.isNaN(n) ? null : n\r\n}\r\n\r\nfunction extractFromLine(line: string, x: number, width: number): string {\r\n // Guard: x may exceed line length (line might be shorter than full 40-char row)\r\n if (x >= line.length) return ''\r\n return line.slice(x, Math.min(x + width, line.length))\r\n}\r\n\r\n/**\r\n * When a row is currently playing, the M8 prepends a single non-alphanumeric\r\n * indicator character (e.g. '<' or '>') to currentLine, shifting all data\r\n * positions right by 1. Strip it so column offsets remain consistent.\r\n * Returns the clean line and the number of stripped characters (0 or 1).\r\n */\r\n/**\r\n * Extracts the item ID and unsaved flag from a raw M8 screen title.\r\n * Titles follow the pattern ' VIEWNAME ID* ' where ID is 1–2 hex chars\r\n * and '*' is an optional unsaved-changes marker.\r\n * Returns { viewId: null, isUnsaved: false } for views without an item number.\r\n */\r\nfunction extractViewId(rawTitle: string | null): { viewId: string | null; isUnsaved: boolean } {\r\n if (!rawTitle) return { viewId: null, isUnsaved: false }\r\n const trimmed = rawTitle.trim()\r\n const isUnsaved = trimmed.endsWith('*')\r\n const withoutStar = isUnsaved ? trimmed.slice(0, -1).trimEnd() : trimmed\r\n const parts = withoutStar.split(/\\s+/)\r\n const last = parts[parts.length - 1] ?? ''\r\n // Accept 1–2 uppercase hex digits as a view ID\r\n const viewId = /^[0-9A-Fa-f]{1,2}$/.test(last) ? last.toUpperCase() : null\r\n return { viewId, isUnsaved }\r\n}\r\n\r\nfunction stripPlaybackIndicator(line: string): { line: string; offset: number } {\r\n if (line.length > 0 && /^[^a-zA-Z0-9 ]/.test(line[0])) {\r\n return { line: line.slice(1), offset: 1 }\r\n }\r\n return { line, offset: 0 }\r\n}\r\n\r\nfunction parseColumn(col: ColumnDef, line: string): M8ParsedField {\r\n const raw = extractFromLine(line, col.x, col.width).trim()\r\n const empty = isEmptyRaw(raw)\r\n\r\n // Numeric types we try to parse as hex\r\n const numericTypes: M8FieldType[] = [\r\n 'chainRef', 'phraseRef', 'velocity', 'instrumentRef',\r\n 'fxValue', 'transpose', 'ticks', 'volume', 'ppq', 'eqSlot', 'rowIndex',\r\n ]\r\n // Types that are decimal (not hex)\r\n const decimalTypes: M8FieldType[] = ['ppq']\r\n let hexValue: number | null = null\r\n if (!empty && numericTypes.includes(col.type)) {\r\n if (decimalTypes.includes(col.type)) {\r\n const n = parseInt(raw, 10)\r\n hexValue = Number.isNaN(n) ? null : n\r\n } else {\r\n hexValue = parseHex(raw)\r\n }\r\n }\r\n\r\n return {\r\n key: col.key,\r\n label: col.label,\r\n type: col.type,\r\n rawValue: raw,\r\n hexValue,\r\n isEmpty: empty,\r\n meta: col.meta,\r\n }\r\n}\r\n\r\nfunction parseRowIndex(line: string, rf: RowFieldDef): number | null {\r\n const raw = extractFromLine(line, rf.x, rf.width).trim()\r\n if (!raw) return null\r\n if (rf.parse === 'hex') return parseHex(raw)\r\n const n = parseInt(raw, 10)\r\n return Number.isNaN(n) ? null : n\r\n}\r\n\r\n/**\r\n * Find the column whose x range contains cursorX.\r\n * Falls back to the closest column within 1-character tolerance.\r\n */\r\nfunction findColumn(cols: ColumnDef[], cursorX: number): ColumnDef | null {\r\n // Exact range hit\r\n for (const col of cols) {\r\n if (cursorX >= col.x && cursorX < col.x + col.width) return col\r\n }\r\n // 1-char tolerance (cursor border may be 1 px off)\r\n let best: ColumnDef | null = null\r\n let bestDist = 2 // reject anything > 1 char away\r\n for (const col of cols) {\r\n const dist = Math.min(\r\n Math.abs(cursorX - col.x),\r\n Math.abs(cursorX - (col.x + col.width - 1)),\r\n )\r\n if (dist < bestDist) {\r\n bestDist = dist\r\n best = col\r\n }\r\n }\r\n return best\r\n}\r\n\r\n// Public API \r\n\r\n/**\r\n * Derives a full semantic context from the current M8State.\r\n *\r\n * @example\r\n * ```ts\r\n * const ctx = getSemanticContext(state)\r\n * if (ctx?.activeField?.type === 'chainRef') {\r\n * const track = ctx.activeField.meta?.trackIndex // 1–8\r\n * const chainNum = ctx.activeField.hexValue // 0–255 or null if empty\r\n * const songRow = ctx.row?.rowIndex\r\n * }\r\n * ```\r\n */\r\nexport function getSemanticContext(state: M8State): M8SemanticContext | null {\r\n const viewName = state.viewName\r\n if (!viewName) return null\r\n\r\n const viewDefs = schema.views as Record<string, ViewDef>\r\n const viewDef = viewDefs[viewName] ?? null\r\n\r\n const isGridView = viewDef?.type === 'grid'\r\n const viewTitle = viewDef?.title ?? null\r\n const viewDescription = viewDef?.description ?? null\r\n const { viewId, isUnsaved } = extractViewId(state.viewTitle)\r\n\r\n if (!isGridView || !viewDef) {\r\n return { viewName, viewTitle, viewDescription, viewId, isUnsaved, isGridView: false, row: null, activeField: null }\r\n }\r\n\r\n const rawLine = state.currentLine\r\n if (!rawLine) {\r\n return { viewName, viewTitle, viewDescription, viewId, isUnsaved, isGridView: true, row: null, activeField: null }\r\n }\r\n\r\n // Strip playback indicator prefix ('>' or '<') emitted when the row is playing.\r\n const { line, offset } = stripPlaybackIndicator(rawLine)\r\n const cursorX = state.cursorPos ? state.cursorPos.x - offset : 0\r\n\r\n // Parse row index\r\n const rowIndex = viewDef.rowField ? parseRowIndex(line, viewDef.rowField) : null\r\n\r\n // Parse every column defined for this view\r\n const fields: Record<string, M8ParsedField> = {}\r\n for (const col of viewDef.columns) {\r\n fields[col.key] = parseColumn(col, line)\r\n }\r\n\r\n const row: M8ParsedRow = {\r\n rowIndex,\r\n rowKey: viewDef.rowField?.key ?? 'row',\r\n fields,\r\n }\r\n\r\n // Determine active field from cursor X position\r\n let activeField: M8ActiveField | null = null\r\n if (state.cursorPos) {\r\n const col = findColumn(viewDef.columns, cursorX)\r\n if (col) {\r\n const field = fields[col.key]\r\n if (field) {\r\n activeField = { ...field, viewName, rowIndex }\r\n }\r\n }\r\n }\r\n\r\n return { viewName, viewTitle, viewDescription, viewId, isUnsaved, isGridView: true, row, activeField }\r\n}\r\n\r\n/**\r\n * Returns a short human-readable description of what the cursor is currently on.\r\n *\r\n * @example\r\n * \"Track 3 — Chain 0A — Song Row 02\"\r\n * \"Note A#4 — Step 7 (Phrase)\"\r\n * \"FX1 Command: KIL (Kill Note) — Step 0\"\r\n */\r\nexport function describeContext(ctx: M8SemanticContext): string {\r\n if (!ctx.activeField) return ctx.viewTitle ?? ctx.viewName\r\n\r\n const { activeField: f, row } = ctx\r\n const parts: string[] = []\r\n\r\n // Label + value\r\n const val = formatFieldValue(f)\r\n parts.push(`${f.label}: ${val}`)\r\n\r\n // FX command extra info\r\n if (f.type === 'fxCommand' && !f.isEmpty) {\r\n const info = lookupFxCommand(f.rawValue)\r\n if (info) parts.push(`(${info.description})`)\r\n }\r\n\r\n // FX value context: include sibling command if we're on a value column\r\n if (f.type === 'fxValue' && row) {\r\n const cmdKey = f.key.replace('val', 'cmd') // fx1val → fx1cmd, fx2val → fx2cmd …\r\n const cmdField = row.fields[cmdKey]\r\n if (cmdField && !cmdField.isEmpty) {\r\n const info = lookupFxCommand(cmdField.rawValue)\r\n const label = info ? `${cmdField.rawValue} (${info.description})` : cmdField.rawValue\r\n parts.push(`for ${label}`)\r\n }\r\n }\r\n\r\n // Track index for song view\r\n if (f.meta?.trackIndex != null) parts.push(`Track ${f.meta.trackIndex}`)\r\n\r\n // Row index\r\n if (row?.rowIndex != null) {\r\n const rowHex = row.rowIndex.toString(16).toUpperCase().padStart(2, '0')\r\n parts.push(`${ctx.viewTitle ?? ctx.viewName} row ${rowHex}`)\r\n }\r\n\r\n return parts.join(' — ')\r\n}\r\n\r\n/**\r\n * Formats a parsed field value as a human-readable string.\r\n */\r\nexport function formatFieldValue(field: M8ParsedField): string {\r\n if (field.isEmpty) return '(empty)'\r\n\r\n switch (field.type) {\r\n case 'note':\r\n if (field.rawValue === 'OFF') return 'Note Off'\r\n return `Note ${field.rawValue}`\r\n\r\n case 'transpose': {\r\n if (field.hexValue == null) return field.rawValue\r\n // Relative hex: 00=none, 01–7F=positive, 80–FF=negative (two's complement)\r\n if (field.hexValue === 0) return '±0 st'\r\n const signed = field.hexValue > 0x7f ? field.hexValue - 0x100 : field.hexValue\r\n return `${signed > 0 ? '+' : ''}${signed} st`\r\n }\r\n\r\n case 'velocity':\r\n if (field.hexValue == null) return field.rawValue\r\n return `${field.rawValue}h (${field.hexValue} / 127 max)`\r\n\r\n case 'chainRef':\r\n case 'phraseRef':\r\n case 'instrumentRef':\r\n return field.rawValue\r\n\r\n case 'fxCommand':\r\n return field.rawValue\r\n\r\n case 'fxValue':\r\n if (field.hexValue == null) return field.rawValue\r\n return `${field.rawValue}h (dec ${field.hexValue})`\r\n\r\n case 'ticks':\r\n if (field.rawValue === '00') return '00 (skip step)'\r\n if (field.hexValue == null) return field.rawValue\r\n return `${field.rawValue}h (${field.hexValue} ticks)`\r\n\r\n case 'volume':\r\n return field.rawValue\r\n\r\n default:\r\n return field.rawValue\r\n }\r\n}\r\n","// @ts-expect-error - post-me types are incomplete for the generic handshake signatures used here.\r\nimport { ChildHandshake, DebugMessenger, WindowMessenger } from 'post-me'\r\n// @ts-expect-error - post-me types are incomplete for the generic handshake signatures used here.\r\nimport type { Connection, LocalHandle, RemoteHandle } from 'post-me'\r\nimport type { M8ClientEvents, M8ClientMethods, M8HostEvents, M8HostMethods, M8KeyName, M8SdkConfig, M8State } from './types'\r\nimport { getSemanticContext, describeContext } from './viewContext'\r\nimport type { M8SemanticContext } from './viewContext'\r\n\r\nexport type { CursorPos, CursorRect, M8KeyName, M8State, RGB, SystemInfos } from './types'\r\nexport type { M8SemanticContext } from './viewContext'\r\n\r\nexport interface M8Client {\r\n readonly state: M8State\r\n readonly isConnected: boolean\r\n navigateToView(viewName: string): Promise<boolean>\r\n navigateTo(x: number, y: number): Promise<void>\r\n setValueToHex(targetHex: number): Promise<boolean>\r\n setValueToInt(targetInt: number): Promise<boolean>\r\n setValueFloat(targetFloat: number): Promise<boolean>\r\n setNote(noteString: string): Promise<boolean>\r\n setValueToString(targetString: string, exact?: boolean, searchInCurrentLine?: boolean): Promise<boolean>\r\n browseFile(targetText: string, exact?: boolean): Promise<boolean>\r\n sendKeyPress(keys: M8KeyName[]): Promise<void>\r\n sendKeyDown(keys: M8KeyName[]): Promise<void>\r\n sendKeyUp(): Promise<void>\r\n getState(): M8State\r\n fetchState(): Promise<M8State>\r\n onStateChange(callback: (state: M8State) => void): () => void\r\n onViewChange(callback: (viewName: string | null, viewTitle: string | null) => void): () => void\r\n onCursorMove(callback: (pos: M8State['cursorPos'], rect: M8State['cursorRect'], selectionMode: boolean) => void): () => void\r\n onTextUpdate(callback: (textUnderCursor: string | null, currentLine: string | null) => void): () => void\r\n onKeyPress(callback: (keys: number) => void): () => void\r\n disconnect(): void\r\n /**\r\n * Derives semantic context from the current state: view, row, and the typed\r\n * field under the cursor. Works for the five grid views (song, chain, phrase,\r\n * table, groove). Returns null when no view is active.\r\n */\r\n getSemanticContext(): M8SemanticContext | null\r\n /**\r\n * Returns a short human-readable description of what the cursor is on,\r\n * e.g. \"Track 3 — Chain 0A — Song View row 02\".\r\n */\r\n describeContext(): string | null\r\n}\r\n\r\nconst getDefaultState = (): M8State => ({\r\n viewName: null,\r\n viewTitle: null,\r\n minimapKey: null,\r\n cursorPos: null,\r\n cursorRect: null,\r\n selectionMode: false,\r\n highlightColor: null,\r\n titleColor: null,\r\n backgroundColor: null,\r\n textUnderCursor: null,\r\n currentLine: null,\r\n deviceModel: null,\r\n fontMode: null,\r\n systemInfo: null,\r\n macroRunning: false,\r\n})\r\n\r\nclass M8ClientImpl implements M8Client {\r\n private connection: Connection<M8ClientMethods, M8HostEvents, M8HostMethods, M8ClientEvents> | null = null\r\n private remoteHandle: RemoteHandle<M8HostMethods, M8HostEvents> | null = null\r\n private localHandle: LocalHandle<M8ClientMethods, M8ClientEvents> | null = null\r\n private readonly stateCallbacks = new Set<(state: M8State) => void>()\r\n private readonly viewCallbacks = new Set<(viewName: string | null, viewTitle: string | null) => void>()\r\n private readonly cursorCallbacks = new Set<(pos: M8State['cursorPos'], rect: M8State['cursorRect'], selectionMode: boolean) => void>()\r\n private readonly textCallbacks = new Set<(textUnderCursor: string | null, currentLine: string | null) => void>()\r\n private readonly keyCallbacks = new Set<(keys: number) => void>()\r\n private _state: M8State = getDefaultState()\r\n private _isConnected = false\r\n private readonly config: M8SdkConfig\r\n\r\n constructor(config: M8SdkConfig = {}) {\r\n this.config = config\r\n }\r\n\r\n get state(): M8State {\r\n return this._state\r\n }\r\n\r\n get isConnected(): boolean {\r\n return this._isConnected\r\n }\r\n\r\n async connect(): Promise<void> {\r\n if (this.connection) {\r\n return\r\n }\r\n\r\n let messenger = new WindowMessenger({\r\n localWindow: window,\r\n remoteWindow: window.parent,\r\n remoteOrigin: '*',\r\n })\r\n\r\n if (this.config.debug) {\r\n messenger = DebugMessenger(messenger, (msg: string, ...args: unknown[]) => {\r\n console.log('[M8 SDK]', msg, ...args)\r\n })\r\n }\r\n\r\n const connection = await ChildHandshake<M8ClientMethods, M8HostEvents, M8HostMethods, M8ClientEvents>(messenger, {\r\n ping: async () => 'pong',\r\n })\r\n\r\n this.connection = connection\r\n this.remoteHandle = connection.remoteHandle()\r\n this.localHandle = connection.localHandle()\r\n this._isConnected = true\r\n\r\n this.remoteHandle.addEventListener('stateChanged', (state: M8State) => {\r\n this._state = state\r\n this.stateCallbacks.forEach((cb) => {\r\n cb(state)\r\n })\r\n })\r\n\r\n this.remoteHandle.addEventListener('viewChanged', ({ viewName, viewTitle }: { viewName: string | null; viewTitle: string | null }) => {\r\n this._state = { ...this._state, viewName, viewTitle }\r\n this.viewCallbacks.forEach((cb) => {\r\n cb(viewName, viewTitle)\r\n })\r\n })\r\n\r\n this.remoteHandle.addEventListener('cursorMoved', ({ pos, rect, selectionMode }: { pos: M8State['cursorPos']; rect: M8State['cursorRect']; selectionMode: boolean }) => {\r\n this._state = { ...this._state, cursorPos: pos, cursorRect: rect, selectionMode }\r\n this.cursorCallbacks.forEach((cb) => {\r\n cb(pos, rect, selectionMode)\r\n })\r\n })\r\n\r\n this.remoteHandle.addEventListener('textUpdated', ({ textUnderCursor, currentLine }: { textUnderCursor: string | null; currentLine: string | null }) => {\r\n this._state = { ...this._state, textUnderCursor, currentLine }\r\n this.textCallbacks.forEach((cb) => {\r\n cb(textUnderCursor, currentLine)\r\n })\r\n })\r\n\r\n this.remoteHandle.addEventListener('keyPressed', ({ keys }: { keys: number }) => {\r\n this.keyCallbacks.forEach((cb) => {\r\n cb(keys)\r\n })\r\n })\r\n\r\n await this.fetchState()\r\n this.localHandle.emit('ready', undefined)\r\n }\r\n\r\n async navigateToView(viewName: string): Promise<boolean> {\r\n if (!this.remoteHandle) throw new Error('M8 SDK client is not connected')\r\n return this.remoteHandle.call('navigateToView', viewName)\r\n }\r\n\r\n async navigateTo(x: number, y: number): Promise<void> {\r\n if (!this.remoteHandle) throw new Error('M8 SDK client is not connected')\r\n return this.remoteHandle.call('navigateTo', x, y)\r\n }\r\n\r\n async setValueToHex(targetHex: number): Promise<boolean> {\r\n if (!this.remoteHandle) throw new Error('M8 SDK client is not connected')\r\n return this.remoteHandle.call('setValueToHex', targetHex)\r\n }\r\n\r\n async setValueToInt(targetInt: number): Promise<boolean> {\r\n if (!this.remoteHandle) throw new Error('M8 SDK client is not connected')\r\n return this.remoteHandle.call('setValueToInt', targetInt)\r\n }\r\n\r\n async setValueFloat(targetFloat: number): Promise<boolean> {\r\n if (!this.remoteHandle) throw new Error('M8 SDK client is not connected')\r\n return this.remoteHandle.call('setValueFloat', targetFloat)\r\n }\r\n\r\n async setNote(noteString: string): Promise<boolean> {\r\n if (!this.remoteHandle) throw new Error('M8 SDK client is not connected')\r\n return this.remoteHandle.call('setNote', noteString)\r\n }\r\n\r\n async setValueToString(targetString: string, exact = true, searchInCurrentLine = false): Promise<boolean> {\r\n if (!this.remoteHandle) throw new Error('M8 SDK client is not connected')\r\n return this.remoteHandle.call('setValueToString', targetString, exact, searchInCurrentLine)\r\n }\r\n\r\n async browseFile(targetText: string, exact = true): Promise<boolean> {\r\n if (!this.remoteHandle) throw new Error('M8 SDK client is not connected')\r\n return this.remoteHandle.call('browseFile', targetText, exact)\r\n }\r\n\r\n async sendKeyPress(keys: M8KeyName[]): Promise<void> {\r\n if (!this.remoteHandle) throw new Error('M8 SDK client is not connected')\r\n return this.remoteHandle.call('sendKeyPress', keys)\r\n }\r\n\r\n async sendKeyDown(keys: M8KeyName[]): Promise<void> {\r\n if (!this.remoteHandle) throw new Error('M8 SDK client is not connected')\r\n return this.remoteHandle.call('sendKeyDown', keys)\r\n }\r\n\r\n async sendKeyUp(): Promise<void> {\r\n if (!this.remoteHandle) throw new Error('M8 SDK client is not connected')\r\n return this.remoteHandle.call('sendKeyUp')\r\n }\r\n\r\n getState(): M8State {\r\n return this._state\r\n }\r\n\r\n async fetchState(): Promise<M8State> {\r\n if (!this.remoteHandle) throw new Error('M8 SDK client is not connected')\r\n const state = await this.remoteHandle.call('getState')\r\n this._state = state\r\n return state\r\n }\r\n\r\n onStateChange(callback: (state: M8State) => void): () => void {\r\n this.stateCallbacks.add(callback)\r\n return () => this.stateCallbacks.delete(callback)\r\n }\r\n\r\n onViewChange(callback: (viewName: string | null, viewTitle: string | null) => void): () => void {\r\n this.viewCallbacks.add(callback)\r\n return () => this.viewCallbacks.delete(callback)\r\n }\r\n\r\n onCursorMove(callback: (pos: M8State['cursorPos'], rect: M8State['cursorRect'], selectionMode: boolean) => void): () => void {\r\n this.cursorCallbacks.add(callback)\r\n return () => this.cursorCallbacks.delete(callback)\r\n }\r\n\r\n onTextUpdate(callback: (textUnderCursor: string | null, currentLine: string | null) => void): () => void {\r\n this.textCallbacks.add(callback)\r\n return () => this.textCallbacks.delete(callback)\r\n }\r\n\r\n onKeyPress(callback: (keys: number) => void): () => void {\r\n this.keyCallbacks.add(callback)\r\n return () => this.keyCallbacks.delete(callback)\r\n }\r\n\r\n disconnect(): void {\r\n this.connection?.close()\r\n this.connection = null\r\n this.remoteHandle = null\r\n this.localHandle = null\r\n this._isConnected = false\r\n this.stateCallbacks.clear()\r\n this.viewCallbacks.clear()\r\n this.cursorCallbacks.clear()\r\n this.textCallbacks.clear()\r\n this.keyCallbacks.clear()\r\n }\r\n\r\n getSemanticContext(): M8SemanticContext | null {\r\n return getSemanticContext(this._state)\r\n }\r\n\r\n describeContext(): string | null {\r\n const ctx = getSemanticContext(this._state)\r\n return ctx ? describeContext(ctx) : null\r\n }\r\n}\r\n\r\nexport async function createM8Client(config: M8SdkConfig = {}): Promise<M8Client> {\r\n const client = new M8ClientImpl(config)\r\n await client.connect()\r\n return client\r\n}\r\n\r\nexport function createM8ClientSync(config: M8SdkConfig = {}): { client: M8Client; connect: () => Promise<void> } {\r\n const client = new M8ClientImpl(config)\r\n return {\r\n client,\r\n connect: async () => {\r\n await client.connect()\r\n },\r\n }\r\n}\r\n\r\nexport default {\r\n createM8Client,\r\n createM8ClientSync,\r\n}\r\n"],"mappings":"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"}
1
+ {"version":3,"file":"index.js","names":[],"sources":["../src/m8-view-context.json","../src/viewContext.ts","../src/client.ts"],"sourcesContent":["{\r\n \"version\": \"1.0\",\r\n \"description\": \"Semantic mapping of M8 tracker views: defines the structure of each grid view so cursor position + line text can be interpreted as typed field values.\",\r\n \"notes\": {\r\n \"fontMode\": \"All x/width values assume font mode 0 (standard, 12×14 px cells, 40 cols × ~22 rows). Font modes 1 (bold uppercase) and 2 (large) may shift column positions.\",\r\n \"playbackIndicator\": \"The M8 renders a '>' character one column to the left of the first data column when that step/row/track is currently playing. This character is part of the text stream; the editing cursor is a separate rect overlay.\",\r\n \"emptyCell\": \"An empty or unused cell displays '--' (2 chars) or '---' (3 chars) or '---00' (command+value). Parsed hexValue will be null for these.\",\r\n \"relativeHex\": \"Transpose and some FX values use relative hex: 00 = no change, 01-7F = +1-127, FF-80 = -1-128. See 'Relative and Absolute FX Commands' in the manual.\",\r\n \"rowIndex\": \"Row/step indices are parsed from the leftmost characters of currentLine. They are hex for all grid views.\",\r\n \"titleAsterisk\": \"A '*' next to the item ID in the screen title (e.g. 'PHRASE 03*') indicates the item is used elsewhere in the chain or song (phrase view); on other views it marks unsaved changes. See 'An asterisk next to the phrase number' in the manual.\"\r\n },\r\n \"fieldTypes\": {\r\n \"chainRef\": \"Reference to a chain number (hex 00-FF). '--' means empty/unused.\",\r\n \"phraseRef\": \"Reference to a phrase number (hex 00-FF). '--' means empty/unused.\",\r\n \"note\": \"Musical note in M8 format: [A-G][#|-][0-9] e.g. 'C-4', 'A#3'. '---' = empty. 'OFF' = note-off.\",\r\n \"velocity\": \"Note velocity 00-7F (hex), from 00 (silence) to 7F (maximum). Default is 64 (hex). '--' = inherit.\",\r\n \"instrumentRef\": \"Instrument number 00-7F (hex). '--' = no retrigger (keep current instrument).\",\r\n \"fxCommand\": \"3-character sequencer or instrument FX command name, e.g. 'KIL', 'ARP', 'DEL'. '---' = none.\",\r\n \"fxValue\": \"2-char hex value 00-FF accompanying an FX command. Interpretation depends on the command.\",\r\n \"transpose\": \"Semitone or tick transpose. Relative: 00=none, 01-7F=+1-+127, FF-80=-1--128.\",\r\n \"ticks\": \"Number of ticks this step consumes in a groove (hex 01-FF). '00' skips the step. '--' = empty (groove loops at this point).\",\r\n \"volume\": \"Volume multiplier 00-FF (hex). Multiplied with the phrase-row velocity. '--' = no override.\", \"ppq\": \"Pulses Per Quarter note: resolution of the groove clock (decimal). One of 24 (default), 48, 96 or 192. Only present on groove row 0; absent on all other rows.\",\r\n \"instrumentName\": \"Instrument name string (up to 12 characters). Text field, no hex value.\",\r\n \"eqSlot\": \"EQ slot assignment (hex). '--' = no EQ applied to this instrument.\",\r\n \"rowIndex\": \"The positional index of this row in the current view (song row, chain position, phrase step, etc.).\",\r\n \"noteInterval\": \"Scale row label: one of the 12 note intervals C, C#, D, D#, E, F, F#, G, G#, A, A#, B (sharps use '#', naturals may render a dim '-' pad, e.g. 'C-').\",\r\n \"onOff\": \"Toggle field displayed as 'ON' or 'OFF' ('--' = unset). Used by the scale view 'EN' column.\",\r\n \"semitoneOffset\": \"Signed semitone offset in decimal notation with two decimals (-24.00 to +24.00), e.g. '00.00', '-00.50', '12.00'. Used by the scale view 'OFFSET' column.\",\r\n \"swing\": \"Groove swing amount as a percentage with one decimal, e.g. '50.0%' = straight timing, higher = swung. Only present on groove row 0; absent on all other rows.\"\r\n },\r\n \"views\": {\r\n \"song\": {\r\n \"title\": \"Song View\",\r\n \"type\": \"grid\",\r\n \"description\": \"Arranges chains across 8 independent tracks to form the song structure. Each row is a song position (up to 256 rows); each column is a track. Each track advances vertically until it reaches an empty column ('--'), then loops back to row 00. Chains containing no notes are grayed out; chains '00' or 'FE' are commonly used as silent spacers. On row '00', [UP][UP] enters track reordering mode.\",\r\n \"rowField\": {\r\n \"x\": 0, \"width\": 2, \"parse\": \"hex\", \"type\": \"rowIndex\",\r\n \"key\": \"songRow\", \"description\": \"Song row 00-FF. Each track loops back to row 00 when it reaches '--'.\"\r\n },\r\n \"columns\": [\r\n { \"x\": 3, \"width\": 2, \"key\": \"track1\", \"type\": \"chainRef\", \"label\": \"Track 1\", \"meta\": { \"trackIndex\": 1 } },\r\n { \"x\": 6, \"width\": 2, \"key\": \"track2\", \"type\": \"chainRef\", \"label\": \"Track 2\", \"meta\": { \"trackIndex\": 2 } },\r\n { \"x\": 9, \"width\": 2, \"key\": \"track3\", \"type\": \"chainRef\", \"label\": \"Track 3\", \"meta\": { \"trackIndex\": 3 } },\r\n { \"x\": 12, \"width\": 2, \"key\": \"track4\", \"type\": \"chainRef\", \"label\": \"Track 4\", \"meta\": { \"trackIndex\": 4 } },\r\n { \"x\": 15, \"width\": 2, \"key\": \"track5\", \"type\": \"chainRef\", \"label\": \"Track 5\", \"meta\": { \"trackIndex\": 5 } },\r\n { \"x\": 18, \"width\": 2, \"key\": \"track6\", \"type\": \"chainRef\", \"label\": \"Track 6\", \"meta\": { \"trackIndex\": 6 } },\r\n { \"x\": 21, \"width\": 2, \"key\": \"track7\", \"type\": \"chainRef\", \"label\": \"Track 7\", \"meta\": { \"trackIndex\": 7 } },\r\n { \"x\": 24, \"width\": 2, \"key\": \"track8\", \"type\": \"chainRef\", \"label\": \"Track 8\", \"meta\": { \"trackIndex\": 8 } }\r\n ]\r\n },\r\n \"chain\": {\r\n \"title\": \"Chain View\",\r\n \"type\": \"grid\",\r\n \"description\": \"An ordered list of up to 16 phrases (256 steps). The first '--' phrase ends the chain and the song continues on to the next chain in the song view. The 'TSP' column optionally transposes every note in the phrase in semitones (relative hex).\",\r\n \"rowField\": {\r\n \"x\": 0, \"width\": 1, \"parse\": \"hex\", \"type\": \"rowIndex\",\r\n \"key\": \"chainPos\", \"description\": \"Position within the chain (0-F, max 16 entries).\"\r\n },\r\n \"columns\": [\r\n { \"x\": 2, \"width\": 2, \"key\": \"phrase\", \"type\": \"phraseRef\", \"label\": \"Phrase (PH)\", \"description\": \"Phrase number to play at this chain position.\" },\r\n { \"x\": 5, \"width\": 2, \"key\": \"transpose\", \"type\": \"transpose\", \"label\": \"Transpose (TSP)\", \"description\": \"Optional note transpose in semitones applied to every note in the phrase (relative hex).\" }\r\n ]\r\n },\r\n \"phrase\": {\r\n \"title\": \"Phrase View\",\r\n \"type\": \"grid\",\r\n \"description\": \"A 16-step sequence. Each step holds a note, velocity, instrument assignment, and up to 3 FX command+value pairs. Notes cover 11 octaves (the full 128 MIDI note range); velocity defaults to 64 (hex); an empty instrument column plays the note without retriggering the instrument. A '*' next to the phrase number indicates the phrase is used elsewhere in the chain or song.\",\r\n \"rowField\": {\r\n \"x\": 0, \"width\": 1, \"parse\": \"hex\", \"type\": \"rowIndex\",\r\n \"key\": \"step\", \"description\": \"Step index 0-F (16th note by default; altered by groove).\"\r\n },\r\n \"columns\": [\r\n { \"x\": 2, \"width\": 3, \"key\": \"note\", \"type\": \"note\", \"label\": \"Note (N)\" },\r\n { \"x\": 6, \"width\": 2, \"key\": \"vel\", \"type\": \"velocity\", \"label\": \"Velocity (V)\" },\r\n { \"x\": 9, \"width\": 2, \"key\": \"inst\", \"type\": \"instrumentRef\", \"label\": \"Instrument (I)\" },\r\n { \"x\": 12, \"width\": 3, \"key\": \"fx1cmd\", \"type\": \"fxCommand\", \"label\": \"FX1 Command\" },\r\n { \"x\": 15, \"width\": 2, \"key\": \"fx1val\", \"type\": \"fxValue\", \"label\": \"FX1 Value\" },\r\n { \"x\": 18, \"width\": 3, \"key\": \"fx2cmd\", \"type\": \"fxCommand\", \"label\": \"FX2 Command\" },\r\n { \"x\": 21, \"width\": 2, \"key\": \"fx2val\", \"type\": \"fxValue\", \"label\": \"FX2 Value\" },\r\n { \"x\": 24, \"width\": 3, \"key\": \"fx3cmd\", \"type\": \"fxCommand\", \"label\": \"FX3 Command\" },\r\n { \"x\": 27, \"width\": 2, \"key\": \"fx3val\", \"type\": \"fxValue\", \"label\": \"FX3 Value\" }\r\n ]\r\n },\r\n \"table\": {\r\n \"title\": \"Table View\",\r\n \"type\": \"grid\",\r\n \"description\": \"A 16-step mini-sequencer that runs alongside an instrument. Used for arpeggios, multi-stage envelopes, and automated FX. The 'N' column transposes the instrument for the step (relative hex); 'V' multiplies with the phrase-row velocity. Each instrument has a dedicated table plus 128 assignable tables (TBL command). FX columns can run at independent speeds via TIC; a TIC on the last table row affects all rows. HOP shortens or loops the table.\",\r\n \"rowField\": {\r\n \"x\": 0, \"width\": 1, \"parse\": \"hex\", \"type\": \"rowIndex\",\r\n \"key\": \"step\", \"description\": \"Table step 0-F. Loops from bottom unless a HOP command redirects.\"\r\n },\r\n \"columns\": [\r\n { \"x\": 2, \"width\": 2, \"key\": \"transpose\", \"type\": \"transpose\", \"label\": \"Transpose (N)\", \"description\": \"Transposes the instrument for this step (relative hex). '--' = no transpose.\" },\r\n { \"x\": 5, \"width\": 2, \"key\": \"volume\", \"type\": \"volume\", \"label\": \"Volume (V)\", \"description\": \"Volume multiplier applied on top of the phrase velocity (hex). '--' = no override.\" },\r\n { \"x\": 8, \"width\": 3, \"key\": \"fx1cmd\", \"type\": \"fxCommand\", \"label\": \"FX1 Command\", \"description\": \"3-char command name for table FX slot 1.\" },\r\n { \"x\": 11, \"width\": 2, \"key\": \"fx1val\", \"type\": \"fxValue\", \"label\": \"FX1 Value\", \"description\": \"Value for table FX1.\" },\r\n { \"x\": 14, \"width\": 3, \"key\": \"fx2cmd\", \"type\": \"fxCommand\", \"label\": \"FX2 Command\", \"description\": \"3-char command name for table FX slot 2.\" },\r\n { \"x\": 17, \"width\": 2, \"key\": \"fx2val\", \"type\": \"fxValue\", \"label\": \"FX2 Value\", \"description\": \"Value for table FX2.\" },\r\n { \"x\": 20, \"width\": 3, \"key\": \"fx3cmd\", \"type\": \"fxCommand\", \"label\": \"FX3 Command\", \"description\": \"3-char command name for table FX slot 3.\" },\r\n { \"x\": 23, \"width\": 2, \"key\": \"fx3val\", \"type\": \"fxValue\", \"label\": \"FX3 Value\", \"description\": \"Value for table FX3.\" }\r\n ]\r\n },\r\n \"groove\": {\r\n \"title\": \"Groove View\",\r\n \"type\": \"grid\",\r\n \"description\": \"Defines the timing of each phrase step in ticks. Used to create swing, shuffle, triplets, and other rhythmic feels. At the default 24 PPQ a step is 6 ticks and a full phrase totals 96 ticks. The groove loops when a '--' entry is reached; a '00' entry skips the phrase step. Row 0 also holds the PPQ resolution (24/48/96/192) and the SWING percentage; editing the row 0 tick value alters rows 0 and 1 together (a common swing setting is 07,05 or 08,04).\",\r\n \"rowField\": {\r\n \"x\": 0, \"width\": 1, \"parse\": \"hex\", \"type\": \"rowIndex\",\r\n \"key\": \"step\", \"description\": \"Groove step 0-F. The groove loops when a '--' entry is reached.\"\r\n },\r\n \"columns\": [\r\n { \"x\": 2, \"width\": 2, \"key\": \"ticks\", \"type\": \"ticks\", \"label\": \"Ticks (TIC)\", \"description\": \"Number of ticks this step takes. Default is 06 (at 24 PPQN = 1 sixteenth note). '00' skips the step.\" },\r\n { \"x\": 6, \"width\": 2, \"key\": \"ppq\", \"type\": \"ppq\", \"label\": \"PPQ\", \"description\": \"Pulses Per Quarter note: 24, 48, 96 or 192. Only present on groove row 0; absent on all other rows.\" },\r\n { \"x\": 10, \"width\": 5, \"key\": \"swing\", \"type\": \"swing\", \"label\": \"Swing\", \"description\": \"Swing amount as a percentage (e.g. '50.0%' = straight). Only present on groove row 0; absent on all other rows.\" }\r\n ]\r\n },\r\n \"scale\": {\r\n \"title\": \"Scale View\",\r\n \"type\": \"grid\",\r\n \"description\": \"Defines and quantizes note-related events (project/chain transpose, note entry, ARP, PIT, and the MIDIOUT instrument CHD and ADD commands). Up to 16 user-defined scales per project; scale '00' is the default for all 8 tracks and can be overridden per track with SCA/SCG FX commands. Each of the 12 note intervals can be enabled ('EN' = ON) with an optional detuning offset (-24.00 to +24.00 semitones). Rows below the 12 intervals (not column-mapped) hold: KEY (global root note for the default scale), TUNE (A4 reference in Hz, e.g. '440.00'), NAME (up to 16 characters, 92 factory scales load from /Scales/Factory), and the LOAD/SAVE actions.\",\r\n \"columns\": [\r\n { \"x\": 0, \"width\": 2, \"key\": \"interval\", \"type\": \"noteInterval\", \"label\": \"Note Interval\", \"description\": \"Note interval row label: C, C#, D, D#, E, F, F#, G, G#, A, A#, B.\" },\r\n { \"x\": 3, \"width\": 3, \"key\": \"en\", \"type\": \"onOff\", \"label\": \"Enabled (EN)\", \"description\": \"Enables this note interval in the scale: 'ON' or 'OFF' ('--' = unset).\" },\r\n { \"x\": 6, \"width\": 6, \"key\": \"offset\", \"type\": \"semitoneOffset\", \"label\": \"Offset (OFFSET)\", \"description\": \"Detune offset in semitones, decimal with two decimals (-24.00 to +24.00), e.g. '00.00'.\" }\r\n ]\r\n },\r\n \"instrumentpool\": {\r\n \"title\": \"Instrument Pool\",\r\n \"type\": \"grid\",\r\n \"description\": \"Lists all 128 instrument slots with their mix routing. Each row shows the instrument name (up to 12 chars, '------------' = empty slot) and its dry/FX send levels. Mixer-related settings can be adjusted per instrument here; [EDIT] on an empty slot loads an instrument or sample.\",\r\n \"rowField\": {\r\n \"x\": 0, \"width\": 2, \"parse\": \"hex\", \"type\": \"rowIndex\",\r\n \"key\": \"instIndex\", \"description\": \"Instrument slot index 00-7F.\"\r\n },\r\n \"columns\": [\r\n { \"x\": 3, \"width\": 12, \"key\": \"name\", \"type\": \"instrumentName\", \"label\": \"Instrument Name\", \"description\": \"Name of the instrument in this slot (up to 12 chars). '------------' = empty slot.\" },\r\n { \"x\": 16, \"width\": 2, \"key\": \"dry\", \"type\": \"volume\", \"label\": \"Dry (DRY)\", \"description\": \"Direct output volume for this instrument (hex 00-FF).\" },\r\n { \"x\": 19, \"width\": 2, \"key\": \"mx\", \"type\": \"volume\", \"label\": \"ModFX (MX)\", \"description\": \"ModFX send level (hex 00-FF).\" },\r\n { \"x\": 22, \"width\": 2, \"key\": \"de\", \"type\": \"volume\", \"label\": \"Delay (DE)\", \"description\": \"Delay send level (hex 00-FF).\" },\r\n { \"x\": 25, \"width\": 2, \"key\": \"rv\", \"type\": \"volume\", \"label\": \"Reverb (RV)\", \"description\": \"Reverb send level (hex 00-FF).\" },\r\n { \"x\": 28, \"width\": 2, \"key\": \"eq\", \"type\": \"eqSlot\", \"label\": \"EQ Slot (EQ)\", \"description\": \"EQ slot assignment. '--' = no EQ.\" }\r\n ]\r\n },\r\n \"inst\": {\r\n \"title\": \"Instrument View\",\r\n \"type\": \"parameter\",\r\n \"description\": \"Per-instrument parameters for the selected slot: TYPE (Wavsynth, Macrosynth, Sampler, FM Synth, Hypersynth, External Instrument, MIDI Out), LOAD/SAVE presets, NAME, TRANSP. (enable/disable transposition from scales and transpose columns), TABLE TIC (default table speed), and EQ slot, plus the synth engine parameters. Almost every parameter has an associated FX command; highlighting a parameter and navigating to the phrase or table view sets that parameter's FX command as default.\"\r\n },\r\n \"instmods\": {\r\n \"title\": \"Instrument Modulation View\",\r\n \"type\": \"parameter\",\r\n \"description\": \"Four configurable modulation slots per instrument: AHD envelope, ADSR envelope, DRUM envelope, LFO, TRIG envelope, or TRACKING, each with a destination parameter (DEST) and optional source (SRC).\"\r\n },\r\n \"mixer\": {\r\n \"title\": \"Mixer View\",\r\n \"type\": \"parameter\",\r\n \"description\": \"Mix levels for the 8 tracks, the 3 send effects (MX/DE/RE), the analog input (INPUT, with dual-mono mode) and USB input, the DJ filter (DJF), OTT compression, main EQ, limiter (LIM) and main volume (MIX). All signals are summed through OTT, main EQ, limiter, DJ filter and main volume. Most values are also exposed as FX commands (VMV, VMX, VDE, VRE, VT1-8, DJC/DJR/DJT, IVO/IMX/IDE/IRV, USB, ...).\"\r\n },\r\n \"project\": {\r\n \"title\": \"Project View\",\r\n \"type\": \"parameter\",\r\n \"description\": \"Song-wide settings such as tempo, project transpose (identical to the TSP FX command), and LIVE QUANTIZE behavior for cueing tracks in live mode. [SHIFT]+[OPTION] stores a song snapshot; [SHIFT]+[EDIT] recalls it.\"\r\n },\r\n \"effectsettings\": {\r\n \"title\": \"Effect Settings View\",\r\n \"type\": \"parameter\",\r\n \"description\": \"Settings for the 3 send effects: ModFX type/depth/frequency/width, Delay time/feedback/width, and Reverb size/decay/modulation/width/freeze — all addressable via the XMT-XMW, XDT-XDR and XRS-XRZ FX commands.\"\r\n },\r\n \"mixeq\": {\r\n \"title\": \"Mix EQ View\",\r\n \"type\": \"parameter\",\r\n \"description\": \"EQ editor for the main mix EQ slot (assign with the EQM FX command). Selecting EQ in the Mixer view and pressing [EDIT] or [SHIFT]+[RIGHT] opens this view.\"\r\n },\r\n \"modfxeq\": {\r\n \"title\": \"ModFX EQ View\",\r\n \"type\": \"parameter\",\r\n \"description\": \"EQ editor for the ModFX send path.\"\r\n },\r\n \"delayeq\": {\r\n \"title\": \"Delay EQ View\",\r\n \"type\": \"parameter\",\r\n \"description\": \"EQ editor for the Delay send path.\"\r\n },\r\n \"reverbeq\": {\r\n \"title\": \"Reverb EQ View\",\r\n \"type\": \"parameter\",\r\n \"description\": \"EQ editor for the Reverb send path.\"\r\n },\r\n \"mixscope\": {\r\n \"title\": \"Mix Scope View\",\r\n \"type\": \"parameter\",\r\n \"description\": \"Graphical scope of the main mix activity. Opened from the Mixer view with MIX selected via [EDIT] or [SHIFT]+[RIGHT].\"\r\n },\r\n \"limiterscope\": {\r\n \"title\": \"Limiter Scope View\",\r\n \"type\": \"parameter\",\r\n \"description\": \"Graphical scope of the limiter's compression activity (the white line on the MIX indicator). Opened from the Mixer view with LIM selected via [EDIT] or [SHIFT]+[RIGHT].\"\r\n },\r\n \"systemsettings\": {\r\n \"title\": \"System Settings View\",\r\n \"type\": \"parameter\",\r\n \"description\": \"System-level settings for the device.\"\r\n },\r\n \"loadinstrument\": {\r\n \"title\": \"Load Instrument\",\r\n \"type\": \"parameter\",\r\n \"description\": \"File browser for loading an instrument preset from the SD card ([EDIT] on an empty instrument slot, or LOAD in the Instrument view).\"\r\n },\r\n \"selectsavedirectory\": {\r\n \"title\": \"Select Save Directory\",\r\n \"type\": \"parameter\",\r\n \"description\": \"File browser directory-selection step when saving a preset or project.\"\r\n },\r\n \"createdirectory\": {\r\n \"title\": \"Create Directory\",\r\n \"type\": \"parameter\",\r\n \"description\": \"File browser dialog for creating a new directory on the SD card.\"\r\n }\r\n },\r\n \"fxCommands\": {\r\n \"sequencer\": {\r\n \"ARP\": { \"description\": \"Arpeggio: plays 3 notes rapidly. X=first interval semitones, Y=second interval semitones.\" },\r\n \"ARC\": { \"description\": \"Arpeggio Config: X=mode, Y=speed in ticks.\" },\r\n \"CHA\": { \"description\": \"Chance: X=probability left of command (0=never, F=always), Y=probability right. In a table: probability for everything to the left of CHA from 00 (never) to FF (always).\" },\r\n \"DEL\": { \"description\": \"Delay: in phrase delays entire row by XX ticks; in table delays table playhead.\" },\r\n \"GRV\": { \"description\": \"Groove: sets groove number XX for the current track.\" },\r\n \"GGR\": { \"description\": \"Global Groove: sets groove XX for all tracks.\" },\r\n \"HOP\": { \"description\": \"Hop: in phrase jumps to row Y of next phrase; HOPFF stops track. In table loops to row Y X times.\" },\r\n \"INS\": { \"description\": \"Instrument: changes active instrument to XX as an FX command.\" },\r\n \"KIL\": { \"description\": \"Kill: stops the playing instrument after XX ticks.\" },\r\n \"RND\": { \"description\": \"Random: randomises the preceding FX command. X=left range, Y=right range (0=none, F=full).\" },\r\n \"RNL\": { \"description\": \"Randomise Left: randomises the FX command to the left (X=left range, Y=right range). In the first column RNL randomises the note and instrument number in phrase, or note and velocity in table.\" },\r\n \"RET\": { \"description\": \"Retrig: retriggers current row. Y≠0: retrig every Y ticks with X volume ramp. Y=0: single retrig after X ticks.\" },\r\n \"REP\": { \"description\": \"Repeat: repeats and increments the last FX command by XX each step. Active until a new command or REP00 stops it; it does not need to appear on every row. A double-caret '^^' appears next to an FX lane while a REP from a previous phrase is active.\" },\r\n \"RTO\": { \"description\": \"Repeat To: sets the min/max value that REP stops at.\" },\r\n \"RMX\": { \"description\": \"Remix: repositions the phrase playhead (Y) on tracks to the left (selected by X).\" },\r\n \"NTH\": { \"description\": \"Nth Trigger: conditional trigger based on loop count.\" },\r\n \"PSL\": { \"description\": \"Pitch Slide (portamento): enables portamento for XX ticks.\" },\r\n \"PBN\": { \"description\": \"Pitch Bend: continuous pitch slide up (00-7F) or down (80-FF).\" },\r\n \"PVB\": { \"description\": \"Vibrato: X=speed, Y=depth.\" },\r\n \"PVX\": { \"description\": \"Extreme Vibrato: like PVB but with higher range.\" },\r\n \"SCA\": { \"description\": \"Track Scale: X=key signature, Y=scale number for this track.\" },\r\n \"SCG\": { \"description\": \"Global Scale: X=key, Y=scale number for the whole song.\" },\r\n \"SNG\": { \"description\": \"Song Hop: moves this track's song position by XX rows relative to current. Ignored if the resulting song position is not playable.\" },\r\n \"SED\": { \"description\": \"Random Seed: sets the random seed XX for this track.\" },\r\n \"TBL\": { \"description\": \"Table: assigns table number XX to the current instrument.\" },\r\n \"THO\": { \"description\": \"Table Hop: jumps all table columns to position 0X.\" },\r\n \"TIC\": { \"description\": \"Table Tick: sets the table tick rate. 00 = increment row on each instrument trigger; 01-FB = ticks per row; FC = Octave Map (playing octave maps to table row); FD = Velocity Map; FE = Note Map (pair with HOP00 on row 0C to limit to 12 notes/octave); FF = increment at 200 Hz. In a table each FX column can run independently; a TIC on the last table row affects all rows.\" },\r\n \"TBX\": { \"description\": \"Aux Table: runs table XX in parallel with the instrument table. 00=stop aux table. A TIC in the same row as a TBX affects the auxiliary table's tick rate.\" },\r\n \"TPO\": { \"description\": \"Tempo: sets song BPM to XX (hex → decimal conversion shown in help text).\" },\r\n \"TSP\": { \"description\": \"Global Song Transpose: transposes the entire song by XX semitones. Identical to the transpose value in the Project View.\" },\r\n \"NXT\": { \"description\": \"Next Track Trigger: plays instrument XX on the track to the right using the current note/velocity. Does not work on track 8.\" },\r\n \"OFF\": { \"description\": \"Note Off: stops the playing instrument after XX ticks. Triggers ADSR release stage.\" },\r\n \"MTT\": { \"description\": \"Micro Time: adjusts row playback timing in positive or negative sub-tick (1/8 tick) amounts. Negative amounts do not work on the first phrase row. Currently has no action in tables.\" }\r\n },\r\n \"instrument\": {\r\n \"VOL\": { \"description\": \"Volume offset (relative).\" },\r\n \"PIT\": { \"description\": \"Pitch offset in semitones (relative).\" },\r\n \"FIN\": { \"description\": \"Fine tune: pitch offset -1 to +1 semitones (relative).\" },\r\n \"EA1\": { \"description\": \"Envelope 1 Amount offset.\" },\r\n \"EA2\": { \"description\": \"Envelope 2 Amount offset.\" },\r\n \"AT1\": { \"description\": \"Envelope 1 Attack time offset.\" },\r\n \"AT2\": { \"description\": \"Envelope 2 Attack time offset.\" },\r\n \"HO1\": { \"description\": \"Envelope 1 Hold time offset.\" },\r\n \"HO2\": { \"description\": \"Envelope 2 Hold time offset.\" },\r\n \"DE1\": { \"description\": \"Envelope 1 Decay time offset.\" },\r\n \"DE2\": { \"description\": \"Envelope 2 Decay time offset.\" },\r\n \"ET1\": { \"description\": \"Retrigger Envelope 1 (any value > 00 retriggers).\" },\r\n \"ET2\": { \"description\": \"Retrigger Envelope 2.\" },\r\n \"LA1\": { \"description\": \"LFO 1 Amount offset.\" },\r\n \"LA2\": { \"description\": \"LFO 2 Amount offset.\" },\r\n \"LF1\": { \"description\": \"LFO 1 Frequency offset.\" },\r\n \"LF2\": { \"description\": \"LFO 2 Frequency offset.\" },\r\n \"LT1\": { \"description\": \"Retrigger LFO 1 (value sets phase offset).\" },\r\n \"LT2\": { \"description\": \"Retrigger LFO 2.\" },\r\n \"CHD\": { \"description\": \"Chord (MIDIOUT instrument): triggers a chord. Quantized by the active scale.\" },\r\n \"ADD\": { \"description\": \"Add Note (MIDIOUT instrument): adds a note to the chord. Quantized by the active scale.\" }\r\n },\r\n \"mixer\": {\r\n \"VMV\": { \"description\": \"Main Volume: sets the main song output volume.\" },\r\n \"VMX\": { \"description\": \"ModFX Volume.\" },\r\n \"VDE\": { \"description\": \"Delay Volume.\" },\r\n \"VRE\": { \"description\": \"Reverb Volume.\" },\r\n \"VT1\": { \"description\": \"Track 1 Volume.\" },\r\n \"VT2\": { \"description\": \"Track 2 Volume.\" },\r\n \"VT3\": { \"description\": \"Track 3 Volume.\" },\r\n \"VT4\": { \"description\": \"Track 4 Volume.\" },\r\n \"VT5\": { \"description\": \"Track 5 Volume.\" },\r\n \"VT6\": { \"description\": \"Track 6 Volume.\" },\r\n \"VT7\": { \"description\": \"Track 7 Volume.\" },\r\n \"VT8\": { \"description\": \"Track 8 Volume.\" },\r\n \"DJC\": { \"description\": \"DJ Filter Cutoff.\" },\r\n \"DJR\": { \"description\": \"DJ Filter Resonance.\" },\r\n \"DJT\": { \"description\": \"DJ Filter Type.\" },\r\n \"EQM\": { \"description\": \"Assign main mix EQ slot.\" },\r\n \"EQI\": { \"description\": \"Assign current instrument EQ slot.\" },\r\n \"XMT\": { \"description\": \"Send Effect: ModFX mod type and phase position (Effect Settings view).\" },\r\n \"XMM\": { \"description\": \"Send Effect: ModFX mod depth (Effect Settings view).\" },\r\n \"XMF\": { \"description\": \"Send Effect: ModFX mod frequency (Effect Settings view).\" },\r\n \"XMW\": { \"description\": \"Send Effect: ModFX stereo width (Effect Settings view).\" },\r\n \"XMR\": { \"description\": \"Send Effect: ModFX to reverb mix (Effect Settings view).\" },\r\n \"XDT\": { \"description\": \"Send Effect: Delay time — X = left, Y = right, in large increments (Effect Settings view).\" },\r\n \"XDF\": { \"description\": \"Send Effect: Delay feedback (Effect Settings view).\" },\r\n \"XDW\": { \"description\": \"Send Effect: Delay stereo width (Effect Settings view).\" },\r\n \"XDR\": { \"description\": \"Send Effect: Delay to reverb mix (Effect Settings view).\" },\r\n \"XRS\": { \"description\": \"Send Effect: Reverb room size (Effect Settings view).\" },\r\n \"XRD\": { \"description\": \"Send Effect: Reverb decay (Effect Settings view).\" },\r\n \"XRM\": { \"description\": \"Send Effect: Reverb mod depth (Effect Settings view).\" },\r\n \"XRF\": { \"description\": \"Send Effect: Reverb mod frequency (Effect Settings view).\" },\r\n \"XRW\": { \"description\": \"Send Effect: Reverb stereo width (Effect Settings view).\" },\r\n \"XRZ\": { \"description\": \"Send Effect: Reverb freeze — enabled with a value greater than 00, disabled at 00.\" },\r\n \"IVO\": { \"description\": \"Line Input Mixer: analog input volume (Mixer view).\" },\r\n \"IMX\": { \"description\": \"Line Input Mixer: analog input ModFX send (Mixer view).\" },\r\n \"IDE\": { \"description\": \"Line Input Mixer: analog input delay send (Mixer view).\" },\r\n \"IRV\": { \"description\": \"Line Input Mixer: analog input reverb send (Mixer view). Also referenced as 'IRE' in the manual text.\" },\r\n \"IRE\": { \"description\": \"Alias of IRV: analog input reverb send (Mixer view).\" },\r\n \"IV2\": { \"description\": \"Line Input Mixer: volume for the 2nd input (right channel in dual-mono input mode, set in the Mixer view).\" },\r\n \"IM2\": { \"description\": \"Line Input Mixer: ModFX send for the 2nd input (right channel in dual-mono input mode).\" },\r\n \"ID2\": { \"description\": \"Line Input Mixer: delay send for the 2nd input (right channel in dual-mono input mode).\" },\r\n \"IR2\": { \"description\": \"Line Input Mixer: reverb send for the 2nd input (right channel in dual-mono input mode).\" },\r\n \"USB\": { \"description\": \"USB input volume (Mixer view).\" }\r\n }\r\n }\r\n}\r\n","/**\r\n * viewContext.ts\r\n *\r\n * Derives semantic meaning from raw M8State by mapping cursor position and\r\n * currentLine content to typed, labelled fields using the m8-view-context.json schema.\r\n *\r\n * The five grid views (song, chain, phrase, table, groove) are fully supported.\r\n * Parameter views (inst, mixer, project, …) return a minimal context with viewName only.\r\n */\r\n\r\nimport type { M8State } from './types'\r\nimport schema from './m8-view-context.json'\r\n\r\n// Public field type union \r\n\r\nexport type M8FieldType =\r\n | 'chainRef'\r\n | 'phraseRef'\r\n | 'note'\r\n | 'velocity'\r\n | 'instrumentRef'\r\n | 'fxCommand'\r\n | 'fxValue'\r\n | 'transpose'\r\n | 'ticks'\r\n | 'volume'\r\n | 'ppq'\r\n | 'instrumentName'\r\n | 'eqSlot'\r\n | 'rowIndex'\r\n | 'noteInterval'\r\n | 'onOff'\r\n | 'semitoneOffset'\r\n | 'swing'\r\n\r\n// Schema shapes (mirror m8-view-context.json) \r\n\r\ninterface ColumnDef {\r\n x: number\r\n width: number\r\n key: string\r\n type: M8FieldType\r\n label: string\r\n description?: string\r\n meta?: Record<string, unknown>\r\n}\r\n\r\ninterface RowFieldDef {\r\n x: number\r\n width: number\r\n parse: 'hex' | 'decimal'\r\n type: M8FieldType\r\n key: string\r\n description?: string\r\n}\r\n\r\ninterface ViewDef {\r\n title: string\r\n type: 'grid' | 'parameter'\r\n description: string\r\n rowField?: RowFieldDef\r\n /** Column definitions for grid views. Parameter views have no columns. */\r\n columns?: ColumnDef[]\r\n}\r\n\r\n// Public result types \r\n\r\n/**\r\n * A single parsed field value extracted from the currentLine.\r\n */\r\nexport interface M8ParsedField {\r\n /** Column key as defined in the schema (e.g. 'fx1cmd', 'note', 'track3'). */\r\n key: string\r\n /** Human-readable label (e.g. 'FX1 Command', 'Note (N)'). */\r\n label: string\r\n /** Semantic type of this field. */\r\n type: M8FieldType\r\n /** Raw text extracted from currentLine (trimmed). */\r\n rawValue: string\r\n /**\r\n * Parsed integer (from hex) when the field holds a numeric value.\r\n * `null` for empty cells ('--', '---') and text-only types (note name, fxCommand).\r\n */\r\n hexValue: number | null\r\n /** True when the cell is empty/unused ('--', '---', '---00'). */\r\n isEmpty: boolean\r\n /** Extra view-specific metadata (e.g. { trackIndex: 2 } for song view track columns). */\r\n meta?: Record<string, unknown>\r\n}\r\n\r\n/**\r\n * All fields parsed from the current row (line at cursorPos.y).\r\n */\r\nexport interface M8ParsedRow {\r\n /** Row index (hex-parsed) derived from the leftmost chars of currentLine. null if not parseable. */\r\n rowIndex: number | null\r\n /** Schema key for the row index field (e.g. 'songRow', 'step', 'chainPos'). */\r\n rowKey: string\r\n /** Map of column key → parsed field for every column in this view. */\r\n fields: Record<string, M8ParsedField>\r\n}\r\n\r\n/**\r\n * The field currently under the cursor, plus row context.\r\n */\r\nexport interface M8ActiveField extends M8ParsedField {\r\n /** Repetition of the view name for convenience. */\r\n viewName: string\r\n /** Row index of the line containing this field (null if not parseable). */\r\n rowIndex: number | null\r\n}\r\n\r\n/**\r\n * Full semantic context for the current M8State.\r\n */\r\nexport interface M8SemanticContext {\r\n /** M8 view name (lowercased), e.g. 'song', 'phrase'. */\r\n viewName: string\r\n /** Human-readable view title from schema ('Song View', 'Phrase View', …). */\r\n viewTitle: string | null\r\n /** View description from the schema. */\r\n viewDescription: string | null\r\n /**\r\n * The numeric ID of the current view item, parsed from the screen title.\r\n * e.g. 'F2' from ' CHAIN F2*', '03' from ' PHRASE 03'.\r\n * null for views that have no item number (song, instrument pool).\r\n */\r\n viewId: string | null\r\n /** True when the title carries a '*' suffix, indicating unsaved changes. */\r\n isUnsaved: boolean\r\n /** Whether this view is a structured grid (song/chain/phrase/table/groove). */\r\n isGridView: boolean\r\n /** All fields on the line at cursorPos.y (null for non-grid views or missing line). */\r\n row: M8ParsedRow | null\r\n /** The specific field under the cursor (null when cursor is out of any column range). */\r\n activeField: M8ActiveField | null\r\n}\r\n\r\n// FX command lookup \r\n\r\ntype FxCategory = 'sequencer' | 'instrument' | 'mixer'\r\ntype FxCommandMap = Record<string, { description: string }>\r\n\r\nconst fxCommands = schema.fxCommands as Record<FxCategory, FxCommandMap>\r\n\r\n/**\r\n * Look up the description of a 3-character FX command name.\r\n * Returns undefined if the command is unknown.\r\n */\r\nexport function lookupFxCommand(cmd: string): { description: string } | undefined {\r\n const upper = cmd.toUpperCase()\r\n for (const cat of Object.values(fxCommands)) {\r\n if (upper in cat) return cat[upper]\r\n }\r\n return undefined\r\n}\r\n\r\n// Internal helpers \r\n\r\nconst EMPTY_PATTERNS = new Set(['--', '---', '---00', '----', '-'])\r\n\r\nfunction isEmptyRaw(raw: string): boolean {\r\n if (!raw) return true\r\n // Also count strings that are all dashes\r\n return EMPTY_PATTERNS.has(raw) || /^-+$/.test(raw)\r\n}\r\n\r\nfunction parseHex(raw: string): number | null {\r\n const trimmed = raw.trim()\r\n if (!trimmed || isEmptyRaw(trimmed)) return null\r\n const n = parseInt(trimmed, 16)\r\n return Number.isNaN(n) ? null : n\r\n}\r\n\r\nfunction extractFromLine(line: string, x: number, width: number): string {\r\n // Guard: x may exceed line length (line might be shorter than full 40-char row)\r\n if (x >= line.length) return ''\r\n return line.slice(x, Math.min(x + width, line.length))\r\n}\r\n\r\n/**\r\n * When a row is currently playing, the M8 prepends a single non-alphanumeric\r\n * indicator character (e.g. '<' or '>') to currentLine, shifting all data\r\n * positions right by 1. Strip it so column offsets remain consistent.\r\n * Returns the clean line and the number of stripped characters (0 or 1).\r\n */\r\n/**\r\n * Extracts the item ID and unsaved flag from a raw M8 screen title.\r\n * Titles follow the pattern ' VIEWNAME ID* ' where ID is 1–2 hex chars\r\n * and '*' is an optional unsaved-changes marker.\r\n * Returns { viewId: null, isUnsaved: false } for views without an item number.\r\n */\r\nfunction extractViewId(rawTitle: string | null): { viewId: string | null; isUnsaved: boolean } {\r\n if (!rawTitle) return { viewId: null, isUnsaved: false }\r\n const trimmed = rawTitle.trim()\r\n const isUnsaved = trimmed.endsWith('*')\r\n const withoutStar = isUnsaved ? trimmed.slice(0, -1).trimEnd() : trimmed\r\n const parts = withoutStar.split(/\\s+/)\r\n const last = parts[parts.length - 1] ?? ''\r\n // Accept 1–2 uppercase hex digits as a view ID\r\n const viewId = /^[0-9A-Fa-f]{1,2}$/.test(last) ? last.toUpperCase() : null\r\n return { viewId, isUnsaved }\r\n}\r\n\r\nfunction stripPlaybackIndicator(line: string): { line: string; offset: number } {\r\n if (line.length > 0 && /^[^a-zA-Z0-9 ]/.test(line[0])) {\r\n return { line: line.slice(1), offset: 1 }\r\n }\r\n return { line, offset: 0 }\r\n}\r\n\r\nfunction parseColumn(col: ColumnDef, line: string): M8ParsedField {\r\n const raw = extractFromLine(line, col.x, col.width).trim()\r\n const empty = isEmptyRaw(raw)\r\n\r\n // Numeric types we try to parse as hex\r\n const numericTypes: M8FieldType[] = [\r\n 'chainRef', 'phraseRef', 'velocity', 'instrumentRef',\r\n 'fxValue', 'transpose', 'ticks', 'volume', 'ppq', 'eqSlot', 'rowIndex',\r\n ]\r\n // Types that are decimal (not hex)\r\n const decimalTypes: M8FieldType[] = ['ppq']\r\n let hexValue: number | null = null\r\n if (!empty && numericTypes.includes(col.type)) {\r\n if (decimalTypes.includes(col.type)) {\r\n const n = parseInt(raw, 10)\r\n hexValue = Number.isNaN(n) ? null : n\r\n } else {\r\n hexValue = parseHex(raw)\r\n }\r\n }\r\n\r\n return {\r\n key: col.key,\r\n label: col.label,\r\n type: col.type,\r\n rawValue: raw,\r\n hexValue,\r\n isEmpty: empty,\r\n meta: col.meta,\r\n }\r\n}\r\n\r\nfunction parseRowIndex(line: string, rf: RowFieldDef): number | null {\r\n const raw = extractFromLine(line, rf.x, rf.width).trim()\r\n if (!raw) return null\r\n if (rf.parse === 'hex') return parseHex(raw)\r\n const n = parseInt(raw, 10)\r\n return Number.isNaN(n) ? null : n\r\n}\r\n\r\n/**\r\n * Find the column whose x range contains cursorX.\r\n * Falls back to the closest column within 1-character tolerance.\r\n */\r\nfunction findColumn(cols: ColumnDef[], cursorX: number): ColumnDef | null {\r\n // Exact range hit\r\n for (const col of cols) {\r\n if (cursorX >= col.x && cursorX < col.x + col.width) return col\r\n }\r\n // 1-char tolerance (cursor border may be 1 px off)\r\n let best: ColumnDef | null = null\r\n let bestDist = 2 // reject anything > 1 char away\r\n for (const col of cols) {\r\n const dist = Math.min(\r\n Math.abs(cursorX - col.x),\r\n Math.abs(cursorX - (col.x + col.width - 1)),\r\n )\r\n if (dist < bestDist) {\r\n bestDist = dist\r\n best = col\r\n }\r\n }\r\n return best\r\n}\r\n\r\n// Public API \r\n\r\n/**\r\n * Derives a full semantic context from the current M8State.\r\n *\r\n * @example\r\n * ```ts\r\n * const ctx = getSemanticContext(state)\r\n * if (ctx?.activeField?.type === 'chainRef') {\r\n * const track = ctx.activeField.meta?.trackIndex // 1–8\r\n * const chainNum = ctx.activeField.hexValue // 0–255 or null if empty\r\n * const songRow = ctx.row?.rowIndex\r\n * }\r\n * ```\r\n */\r\nexport function getSemanticContext(state: M8State): M8SemanticContext | null {\r\n const viewName = state.viewName\r\n if (!viewName) return null\r\n\r\n const viewDefs = schema.views as Record<string, ViewDef>\r\n const viewDef = viewDefs[viewName] ?? null\r\n\r\n const isGridView = viewDef?.type === 'grid'\r\n const viewTitle = viewDef?.title ?? null\r\n const viewDescription = viewDef?.description ?? null\r\n const { viewId, isUnsaved } = extractViewId(state.viewTitle)\r\n\r\n if (!isGridView || !viewDef) {\r\n return { viewName, viewTitle, viewDescription, viewId, isUnsaved, isGridView: false, row: null, activeField: null }\r\n }\r\n\r\n const rawLine = state.currentLine\r\n if (!rawLine) {\r\n return { viewName, viewTitle, viewDescription, viewId, isUnsaved, isGridView: true, row: null, activeField: null }\r\n }\r\n\r\n // Strip playback indicator prefix ('>' or '<') emitted when the row is playing.\r\n const { line, offset } = stripPlaybackIndicator(rawLine)\r\n const cursorX = state.cursorPos ? state.cursorPos.x - offset : 0\r\n\r\n // Parse row index\r\n const rowIndex = viewDef.rowField ? parseRowIndex(line, viewDef.rowField) : null\r\n\r\n // Parse every column defined for this view\r\n const fields: Record<string, M8ParsedField> = {}\r\n for (const col of viewDef.columns ?? []) {\r\n fields[col.key] = parseColumn(col, line)\r\n }\r\n\r\n const row: M8ParsedRow = {\r\n rowIndex,\r\n rowKey: viewDef.rowField?.key ?? 'row',\r\n fields,\r\n }\r\n\r\n // Determine active field from cursor X position\r\n let activeField: M8ActiveField | null = null\r\n if (state.cursorPos) {\r\n const col = findColumn(viewDef.columns ?? [], cursorX)\r\n if (col) {\r\n const field = fields[col.key]\r\n if (field) {\r\n activeField = { ...field, viewName, rowIndex }\r\n }\r\n }\r\n }\r\n\r\n return { viewName, viewTitle, viewDescription, viewId, isUnsaved, isGridView: true, row, activeField }\r\n}\r\n\r\n/**\r\n * Returns a short human-readable description of what the cursor is currently on.\r\n *\r\n * @example\r\n * \"Track 3 — Chain 0A — Song Row 02\"\r\n * \"Note A#4 — Step 7 (Phrase)\"\r\n * \"FX1 Command: KIL (Kill Note) — Step 0\"\r\n */\r\nexport function describeContext(ctx: M8SemanticContext): string {\r\n if (!ctx.activeField) return ctx.viewTitle ?? ctx.viewName\r\n\r\n const { activeField: f, row } = ctx\r\n const parts: string[] = []\r\n\r\n // Label + value\r\n const val = formatFieldValue(f)\r\n parts.push(`${f.label}: ${val}`)\r\n\r\n // FX command extra info\r\n if (f.type === 'fxCommand' && !f.isEmpty) {\r\n const info = lookupFxCommand(f.rawValue)\r\n if (info) parts.push(`(${info.description})`)\r\n }\r\n\r\n // FX value context: include sibling command if we're on a value column\r\n if (f.type === 'fxValue' && row) {\r\n const cmdKey = f.key.replace('val', 'cmd') // fx1val → fx1cmd, fx2val → fx2cmd …\r\n const cmdField = row.fields[cmdKey]\r\n if (cmdField && !cmdField.isEmpty) {\r\n const info = lookupFxCommand(cmdField.rawValue)\r\n const label = info ? `${cmdField.rawValue} (${info.description})` : cmdField.rawValue\r\n parts.push(`for ${label}`)\r\n }\r\n }\r\n\r\n // Track index for song view\r\n if (f.meta?.trackIndex != null) parts.push(`Track ${f.meta.trackIndex}`)\r\n\r\n // Row index\r\n if (row?.rowIndex != null) {\r\n const rowHex = row.rowIndex.toString(16).toUpperCase().padStart(2, '0')\r\n parts.push(`${ctx.viewTitle ?? ctx.viewName} row ${rowHex}`)\r\n }\r\n\r\n return parts.join(' — ')\r\n}\r\n\r\n/**\r\n * Formats a parsed field value as a human-readable string.\r\n */\r\nexport function formatFieldValue(field: M8ParsedField): string {\r\n if (field.isEmpty) return '(empty)'\r\n\r\n switch (field.type) {\r\n case 'note':\r\n if (field.rawValue === 'OFF') return 'Note Off'\r\n return `Note ${field.rawValue}`\r\n\r\n case 'transpose': {\r\n if (field.hexValue == null) return field.rawValue\r\n // Relative hex: 00=none, 01–7F=positive, 80–FF=negative (two's complement)\r\n if (field.hexValue === 0) return '±0 st'\r\n const signed = field.hexValue > 0x7f ? field.hexValue - 0x100 : field.hexValue\r\n return `${signed > 0 ? '+' : ''}${signed} st`\r\n }\r\n\r\n case 'velocity':\r\n if (field.hexValue == null) return field.rawValue\r\n return `${field.rawValue}h (${field.hexValue} / 127 max)`\r\n\r\n case 'chainRef':\r\n case 'phraseRef':\r\n case 'instrumentRef':\r\n return field.rawValue\r\n\r\n case 'fxCommand':\r\n return field.rawValue\r\n\r\n case 'fxValue':\r\n if (field.hexValue == null) return field.rawValue\r\n return `${field.rawValue}h (dec ${field.hexValue})`\r\n\r\n case 'ticks':\r\n if (field.rawValue === '00') return '00 (skip step)'\r\n if (field.hexValue == null) return field.rawValue\r\n return `${field.rawValue}h (${field.hexValue} ticks)`\r\n\r\n case 'volume':\r\n return field.rawValue\r\n\r\n case 'noteInterval':\r\n return `Interval ${field.rawValue}`\r\n\r\n case 'semitoneOffset':\r\n return field.isEmpty ? '(empty)' : `${field.rawValue} st`\r\n\r\n default:\r\n return field.rawValue\r\n }\r\n}\r\n","// @ts-expect-error - post-me types are incomplete for the generic handshake signatures used here.\r\nimport { ChildHandshake, DebugMessenger, WindowMessenger } from 'post-me'\r\n// @ts-expect-error - post-me types are incomplete for the generic handshake signatures used here.\r\nimport type { Connection, LocalHandle, RemoteHandle } from 'post-me'\r\nimport type { M8ClientEvents, M8ClientMethods, M8HostEvents, M8HostMethods, M8KeyName, M8SdkConfig, M8State } from './types'\r\nimport { getSemanticContext, describeContext } from './viewContext'\r\nimport type { M8SemanticContext } from './viewContext'\r\n\r\nexport type { CursorPos, CursorRect, M8KeyName, M8State, RGB, SystemInfos } from './types'\r\nexport type { M8SemanticContext } from './viewContext'\r\n\r\nexport interface M8Client {\r\n readonly state: M8State\r\n readonly isConnected: boolean\r\n navigateToView(viewName: string): Promise<boolean>\r\n navigateTo(x: number, y: number): Promise<void>\r\n setValueToHex(targetHex: number): Promise<boolean>\r\n setValueToInt(targetInt: number): Promise<boolean>\r\n setValueFloat(targetFloat: number): Promise<boolean>\r\n setNote(noteString: string): Promise<boolean>\r\n setValueToString(targetString: string, exact?: boolean, searchInCurrentLine?: boolean): Promise<boolean>\r\n browseFile(targetText: string, exact?: boolean): Promise<boolean>\r\n sendKeyPress(keys: M8KeyName[]): Promise<void>\r\n sendKeyDown(keys: M8KeyName[]): Promise<void>\r\n sendKeyUp(): Promise<void>\r\n getState(): M8State\r\n fetchState(): Promise<M8State>\r\n onStateChange(callback: (state: M8State) => void): () => void\r\n onViewChange(callback: (viewName: string | null, viewTitle: string | null) => void): () => void\r\n onCursorMove(callback: (pos: M8State['cursorPos'], rect: M8State['cursorRect'], selectionMode: boolean) => void): () => void\r\n onTextUpdate(callback: (textUnderCursor: string | null, currentLine: string | null) => void): () => void\r\n onKeyPress(callback: (keys: number) => void): () => void\r\n disconnect(): void\r\n /**\r\n * Derives semantic context from the current state: view, row, and the typed\r\n * field under the cursor. Works for the five grid views (song, chain, phrase,\r\n * table, groove). Returns null when no view is active.\r\n */\r\n getSemanticContext(): M8SemanticContext | null\r\n /**\r\n * Returns a short human-readable description of what the cursor is on,\r\n * e.g. \"Track 3 — Chain 0A — Song View row 02\".\r\n */\r\n describeContext(): string | null\r\n}\r\n\r\nconst getDefaultState = (): M8State => ({\r\n viewName: null,\r\n viewTitle: null,\r\n minimapKey: null,\r\n cursorPos: null,\r\n cursorRect: null,\r\n selectionMode: false,\r\n highlightColor: null,\r\n titleColor: null,\r\n backgroundColor: null,\r\n textUnderCursor: null,\r\n currentLine: null,\r\n deviceModel: null,\r\n fontMode: null,\r\n systemInfo: null,\r\n macroRunning: false,\r\n})\r\n\r\nclass M8ClientImpl implements M8Client {\r\n private connection: Connection<M8ClientMethods, M8HostEvents, M8HostMethods, M8ClientEvents> | null = null\r\n private remoteHandle: RemoteHandle<M8HostMethods, M8HostEvents> | null = null\r\n private localHandle: LocalHandle<M8ClientMethods, M8ClientEvents> | null = null\r\n private readonly stateCallbacks = new Set<(state: M8State) => void>()\r\n private readonly viewCallbacks = new Set<(viewName: string | null, viewTitle: string | null) => void>()\r\n private readonly cursorCallbacks = new Set<(pos: M8State['cursorPos'], rect: M8State['cursorRect'], selectionMode: boolean) => void>()\r\n private readonly textCallbacks = new Set<(textUnderCursor: string | null, currentLine: string | null) => void>()\r\n private readonly keyCallbacks = new Set<(keys: number) => void>()\r\n private _state: M8State = getDefaultState()\r\n private _isConnected = false\r\n private readonly config: M8SdkConfig\r\n\r\n constructor(config: M8SdkConfig = {}) {\r\n this.config = config\r\n }\r\n\r\n get state(): M8State {\r\n return this._state\r\n }\r\n\r\n get isConnected(): boolean {\r\n return this._isConnected\r\n }\r\n\r\n async connect(): Promise<void> {\r\n if (this.connection) {\r\n return\r\n }\r\n\r\n let messenger = new WindowMessenger({\r\n localWindow: window,\r\n remoteWindow: window.parent,\r\n remoteOrigin: '*',\r\n })\r\n\r\n if (this.config.debug) {\r\n messenger = DebugMessenger(messenger, (msg: string, ...args: unknown[]) => {\r\n console.log('[M8 SDK]', msg, ...args)\r\n })\r\n }\r\n\r\n const connection = await ChildHandshake<M8ClientMethods, M8HostEvents, M8HostMethods, M8ClientEvents>(messenger, {\r\n ping: async () => 'pong',\r\n })\r\n\r\n this.connection = connection\r\n this.remoteHandle = connection.remoteHandle()\r\n this.localHandle = connection.localHandle()\r\n this._isConnected = true\r\n\r\n this.remoteHandle.addEventListener('stateChanged', (state: M8State) => {\r\n this._state = state\r\n this.stateCallbacks.forEach((cb) => {\r\n cb(state)\r\n })\r\n })\r\n\r\n this.remoteHandle.addEventListener('viewChanged', ({ viewName, viewTitle }: { viewName: string | null; viewTitle: string | null }) => {\r\n this._state = { ...this._state, viewName, viewTitle }\r\n this.viewCallbacks.forEach((cb) => {\r\n cb(viewName, viewTitle)\r\n })\r\n })\r\n\r\n this.remoteHandle.addEventListener('cursorMoved', ({ pos, rect, selectionMode }: { pos: M8State['cursorPos']; rect: M8State['cursorRect']; selectionMode: boolean }) => {\r\n this._state = { ...this._state, cursorPos: pos, cursorRect: rect, selectionMode }\r\n this.cursorCallbacks.forEach((cb) => {\r\n cb(pos, rect, selectionMode)\r\n })\r\n })\r\n\r\n this.remoteHandle.addEventListener('textUpdated', ({ textUnderCursor, currentLine }: { textUnderCursor: string | null; currentLine: string | null }) => {\r\n this._state = { ...this._state, textUnderCursor, currentLine }\r\n this.textCallbacks.forEach((cb) => {\r\n cb(textUnderCursor, currentLine)\r\n })\r\n })\r\n\r\n this.remoteHandle.addEventListener('keyPressed', ({ keys }: { keys: number }) => {\r\n this.keyCallbacks.forEach((cb) => {\r\n cb(keys)\r\n })\r\n })\r\n\r\n await this.fetchState()\r\n this.localHandle.emit('ready', undefined)\r\n }\r\n\r\n async navigateToView(viewName: string): Promise<boolean> {\r\n if (!this.remoteHandle) throw new Error('M8 SDK client is not connected')\r\n return this.remoteHandle.call('navigateToView', viewName)\r\n }\r\n\r\n async navigateTo(x: number, y: number): Promise<void> {\r\n if (!this.remoteHandle) throw new Error('M8 SDK client is not connected')\r\n return this.remoteHandle.call('navigateTo', x, y)\r\n }\r\n\r\n async setValueToHex(targetHex: number): Promise<boolean> {\r\n if (!this.remoteHandle) throw new Error('M8 SDK client is not connected')\r\n return this.remoteHandle.call('setValueToHex', targetHex)\r\n }\r\n\r\n async setValueToInt(targetInt: number): Promise<boolean> {\r\n if (!this.remoteHandle) throw new Error('M8 SDK client is not connected')\r\n return this.remoteHandle.call('setValueToInt', targetInt)\r\n }\r\n\r\n async setValueFloat(targetFloat: number): Promise<boolean> {\r\n if (!this.remoteHandle) throw new Error('M8 SDK client is not connected')\r\n return this.remoteHandle.call('setValueFloat', targetFloat)\r\n }\r\n\r\n async setNote(noteString: string): Promise<boolean> {\r\n if (!this.remoteHandle) throw new Error('M8 SDK client is not connected')\r\n return this.remoteHandle.call('setNote', noteString)\r\n }\r\n\r\n async setValueToString(targetString: string, exact = true, searchInCurrentLine = false): Promise<boolean> {\r\n if (!this.remoteHandle) throw new Error('M8 SDK client is not connected')\r\n return this.remoteHandle.call('setValueToString', targetString, exact, searchInCurrentLine)\r\n }\r\n\r\n async browseFile(targetText: string, exact = true): Promise<boolean> {\r\n if (!this.remoteHandle) throw new Error('M8 SDK client is not connected')\r\n return this.remoteHandle.call('browseFile', targetText, exact)\r\n }\r\n\r\n async sendKeyPress(keys: M8KeyName[]): Promise<void> {\r\n if (!this.remoteHandle) throw new Error('M8 SDK client is not connected')\r\n return this.remoteHandle.call('sendKeyPress', keys)\r\n }\r\n\r\n async sendKeyDown(keys: M8KeyName[]): Promise<void> {\r\n if (!this.remoteHandle) throw new Error('M8 SDK client is not connected')\r\n return this.remoteHandle.call('sendKeyDown', keys)\r\n }\r\n\r\n async sendKeyUp(): Promise<void> {\r\n if (!this.remoteHandle) throw new Error('M8 SDK client is not connected')\r\n return this.remoteHandle.call('sendKeyUp')\r\n }\r\n\r\n getState(): M8State {\r\n return this._state\r\n }\r\n\r\n async fetchState(): Promise<M8State> {\r\n if (!this.remoteHandle) throw new Error('M8 SDK client is not connected')\r\n const state = await this.remoteHandle.call('getState')\r\n this._state = state\r\n return state\r\n }\r\n\r\n onStateChange(callback: (state: M8State) => void): () => void {\r\n this.stateCallbacks.add(callback)\r\n return () => this.stateCallbacks.delete(callback)\r\n }\r\n\r\n onViewChange(callback: (viewName: string | null, viewTitle: string | null) => void): () => void {\r\n this.viewCallbacks.add(callback)\r\n return () => this.viewCallbacks.delete(callback)\r\n }\r\n\r\n onCursorMove(callback: (pos: M8State['cursorPos'], rect: M8State['cursorRect'], selectionMode: boolean) => void): () => void {\r\n this.cursorCallbacks.add(callback)\r\n return () => this.cursorCallbacks.delete(callback)\r\n }\r\n\r\n onTextUpdate(callback: (textUnderCursor: string | null, currentLine: string | null) => void): () => void {\r\n this.textCallbacks.add(callback)\r\n return () => this.textCallbacks.delete(callback)\r\n }\r\n\r\n onKeyPress(callback: (keys: number) => void): () => void {\r\n this.keyCallbacks.add(callback)\r\n return () => this.keyCallbacks.delete(callback)\r\n }\r\n\r\n disconnect(): void {\r\n this.connection?.close()\r\n this.connection = null\r\n this.remoteHandle = null\r\n this.localHandle = null\r\n this._isConnected = false\r\n this.stateCallbacks.clear()\r\n this.viewCallbacks.clear()\r\n this.cursorCallbacks.clear()\r\n this.textCallbacks.clear()\r\n this.keyCallbacks.clear()\r\n }\r\n\r\n getSemanticContext(): M8SemanticContext | null {\r\n return getSemanticContext(this._state)\r\n }\r\n\r\n describeContext(): string | null {\r\n const ctx = getSemanticContext(this._state)\r\n return ctx ? describeContext(ctx) : null\r\n }\r\n}\r\n\r\nexport async function createM8Client(config: M8SdkConfig = {}): Promise<M8Client> {\r\n const client = new M8ClientImpl(config)\r\n await client.connect()\r\n return client\r\n}\r\n\r\nexport function createM8ClientSync(config: M8SdkConfig = {}): { client: M8Client; connect: () => Promise<void> } {\r\n const client = new M8ClientImpl(config)\r\n return {\r\n client,\r\n connect: async () => {\r\n await client.connect()\r\n },\r\n }\r\n}\r\n\r\nexport default {\r\n createM8Client,\r\n createM8ClientSync,\r\n}\r\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GC+IM,IAAa,EAAO;AAM1B,SAAgB,EAAgB,GAAkD;CAChF,IAAM,IAAQ,EAAI,YAAY;CAC9B,KAAK,IAAM,KAAO,OAAO,OAAO,CAAU,GACxC,IAAI,KAAS,GAAK,OAAO,EAAI;AAGjC;AAIA,IAAM,IAAiB,IAAI,IAAI;CAAC;CAAM;CAAO;CAAS;CAAQ;AAAG,CAAC;AAElE,SAAS,EAAW,GAAsB;CAGxC,OAFK,IAEE,EAAe,IAAI,CAAG,KAAK,OAAO,KAAK,CAAG,IAFhC;AAGnB;AAEA,SAAS,EAAS,GAA4B;CAC5C,IAAM,IAAU,EAAI,KAAK;CACzB,IAAI,CAAC,KAAW,EAAW,CAAO,GAAG,OAAO;CAC5C,IAAM,IAAI,SAAS,GAAS,EAAE;CAC9B,OAAO,OAAO,MAAM,CAAC,IAAI,OAAO;AAClC;AAEA,SAAS,EAAgB,GAAc,GAAW,GAAuB;CAGvE,OADI,KAAK,EAAK,SAAe,KACtB,EAAK,MAAM,GAAG,KAAK,IAAI,IAAI,GAAO,EAAK,MAAM,CAAC;AACvD;AAcA,SAAS,EAAc,GAAwE;CAC7F,IAAI,CAAC,GAAU,OAAO;EAAE,QAAQ;EAAM,WAAW;CAAM;CACvD,IAAM,IAAU,EAAS,KAAK,GACxB,IAAY,EAAQ,SAAS,GAAG,GAEhC,KADc,IAAY,EAAQ,MAAM,GAAG,EAAE,EAAE,QAAQ,IAAI,GACvC,MAAM,KAAK,GAC/B,IAAO,EAAM,EAAM,SAAS,MAAM;CAGxC,OAAO;EAAE,QADM,qBAAqB,KAAK,CAAI,IAAI,EAAK,YAAY,IAAI;EACrD;CAAU;AAC7B;AAEA,SAAS,EAAuB,GAAgD;CAI9E,OAHI,EAAK,SAAS,KAAK,iBAAiB,KAAK,EAAK,EAAE,IAC3C;EAAE,MAAM,EAAK,MAAM,CAAC;EAAG,QAAQ;CAAE,IAEnC;EAAE;EAAM,QAAQ;CAAE;AAC3B;AAEA,SAAS,EAAY,GAAgB,GAA6B;CAChE,IAAM,IAAM,EAAgB,GAAM,EAAI,GAAG,EAAI,KAAK,EAAE,KAAK,GACnD,IAAQ,EAAW,CAAG,GAGtB,IAA8B;EAClC;EAAY;EAAa;EAAY;EACrC;EAAW;EAAa;EAAS;EAAU;EAAO;EAAU;CAC9D,GAEM,IAA8B,CAAC,KAAK,GACtC,IAA0B;CAC9B,IAAI,CAAC,KAAS,EAAa,SAAS,EAAI,IAAI,GAC1C,IAAI,EAAa,SAAS,EAAI,IAAI,GAAG;EACnC,IAAM,IAAI,SAAS,GAAK,EAAE;EAC1B,IAAW,OAAO,MAAM,CAAC,IAAI,OAAO;CACtC,OACE,IAAW,EAAS,CAAG;CAI3B,OAAO;EACL,KAAK,EAAI;EACT,OAAO,EAAI;EACX,MAAM,EAAI;EACV,UAAU;EACV;EACA,SAAS;EACT,MAAM,EAAI;CACZ;AACF;AAEA,SAAS,EAAc,GAAc,GAAgC;CACnE,IAAM,IAAM,EAAgB,GAAM,EAAG,GAAG,EAAG,KAAK,EAAE,KAAK;CACvD,IAAI,CAAC,GAAK,OAAO;CACjB,IAAI,EAAG,UAAU,OAAO,OAAO,EAAS,CAAG;CAC3C,IAAM,IAAI,SAAS,GAAK,EAAE;CAC1B,OAAO,OAAO,MAAM,CAAC,IAAI,OAAO;AAClC;AAMA,SAAS,EAAW,GAAmB,GAAmC;CAExE,KAAK,IAAM,KAAO,GAChB,IAAI,KAAW,EAAI,KAAK,IAAU,EAAI,IAAI,EAAI,OAAO,OAAO;CAG9D,IAAI,IAAyB,MACzB,IAAW;CACf,KAAK,IAAM,KAAO,GAAM;EACtB,IAAM,IAAO,KAAK,IAChB,KAAK,IAAI,IAAU,EAAI,CAAC,GACxB,KAAK,IAAI,KAAW,EAAI,IAAI,EAAI,QAAQ,EAAE,CAC5C;EACA,AAAI,IAAO,MACT,IAAW,GACX,IAAO;CAEX;CACA,OAAO;AACT;AAiBA,SAAgB,EAAmB,GAA0C;CAC3E,IAAM,IAAW,EAAM;CACvB,IAAI,CAAC,GAAU,OAAO;CAGtB,IAAM,IADW,EAAO,MACC,MAAa,MAEhC,IAAa,GAAS,SAAS,QAC/B,IAAY,GAAS,SAAS,MAC9B,IAAkB,GAAS,eAAe,MAC1C,EAAE,WAAQ,iBAAc,EAAc,EAAM,SAAS;CAE3D,IAAI,CAAC,KAAc,CAAC,GAClB,OAAO;EAAE;EAAU;EAAW;EAAiB;EAAQ;EAAW,YAAY;EAAO,KAAK;EAAM,aAAa;CAAK;CAGpH,IAAM,IAAU,EAAM;CACtB,IAAI,CAAC,GACH,OAAO;EAAE;EAAU;EAAW;EAAiB;EAAQ;EAAW,YAAY;EAAM,KAAK;EAAM,aAAa;CAAK;CAInH,IAAM,EAAE,SAAM,cAAW,EAAuB,CAAO,GACjD,IAAU,EAAM,YAAY,EAAM,UAAU,IAAI,IAAS,GAGzD,IAAW,EAAQ,WAAW,EAAc,GAAM,EAAQ,QAAQ,IAAI,MAGtE,IAAwC,CAAC;CAC/C,KAAK,IAAM,KAAO,EAAQ,WAAW,CAAC,GACpC,EAAO,EAAI,OAAO,EAAY,GAAK,CAAI;CAGzC,IAAM,IAAmB;EACvB;EACA,QAAQ,EAAQ,UAAU,OAAO;EACjC;CACF,GAGI,IAAoC;CACxC,IAAI,EAAM,WAAW;EACnB,IAAM,IAAM,EAAW,EAAQ,WAAW,CAAC,GAAG,CAAO;EACrD,IAAI,GAAK;GACP,IAAM,IAAQ,EAAO,EAAI;GACzB,AAAI,MACF,IAAc;IAAE,GAAG;IAAO;IAAU;GAAS;EAEjD;CACF;CAEA,OAAO;EAAE;EAAU;EAAW;EAAiB;EAAQ;EAAW,YAAY;EAAM;EAAK;CAAY;AACvG;AAUA,SAAgB,EAAgB,GAAgC;CAC9D,IAAI,CAAC,EAAI,aAAa,OAAO,EAAI,aAAa,EAAI;CAElD,IAAM,EAAE,aAAa,GAAG,WAAQ,GAC1B,IAAkB,CAAC,GAGnB,IAAM,EAAiB,CAAC;CAI9B,IAHA,EAAM,KAAK,GAAG,EAAE,MAAM,IAAI,GAAK,GAG3B,EAAE,SAAS,eAAe,CAAC,EAAE,SAAS;EACxC,IAAM,IAAO,EAAgB,EAAE,QAAQ;EACvC,AAAI,KAAM,EAAM,KAAK,IAAI,EAAK,YAAY,EAAE;CAC9C;CAGA,IAAI,EAAE,SAAS,aAAa,GAAK;EAC/B,IAAM,IAAS,EAAE,IAAI,QAAQ,OAAO,KAAK,GACnC,IAAW,EAAI,OAAO;EAC5B,IAAI,KAAY,CAAC,EAAS,SAAS;GACjC,IAAM,IAAO,EAAgB,EAAS,QAAQ,GACxC,IAAQ,IAAO,GAAG,EAAS,SAAS,IAAI,EAAK,YAAY,KAAK,EAAS;GAC7E,EAAM,KAAK,OAAO,GAAO;EAC3B;CACF;CAMA,IAHI,EAAE,MAAM,cAAc,QAAM,EAAM,KAAK,SAAS,EAAE,KAAK,YAAY,GAGnE,GAAK,YAAY,MAAM;EACzB,IAAM,IAAS,EAAI,SAAS,SAAS,EAAE,EAAE,YAAY,EAAE,SAAS,GAAG,GAAG;EACtE,EAAM,KAAK,GAAG,EAAI,aAAa,EAAI,SAAS,OAAO,GAAQ;CAC7D;CAEA,OAAO,EAAM,KAAK,KAAK;AACzB;AAKA,SAAgB,EAAiB,GAA8B;CAC7D,IAAI,EAAM,SAAS,OAAO;CAE1B,QAAQ,EAAM,MAAd;EACE,KAAK,QAEH,OADI,EAAM,aAAa,QAAc,aAC9B,QAAQ,EAAM;EAEvB,KAAK,aAAa;GAChB,IAAI,EAAM,YAAY,MAAM,OAAO,EAAM;GAEzC,IAAI,EAAM,aAAa,GAAG,OAAO;GACjC,IAAM,IAAS,EAAM,WAAW,MAAO,EAAM,WAAW,MAAQ,EAAM;GACtE,OAAO,GAAG,IAAS,IAAI,MAAM,KAAK,EAAO;EAC3C;EAEA,KAAK,YAEH,OADI,EAAM,YAAY,OAAa,EAAM,WAClC,GAAG,EAAM,SAAS,KAAK,EAAM,SAAS;EAE/C,KAAK;EACL,KAAK;EACL,KAAK,iBACH,OAAO,EAAM;EAEf,KAAK,aACH,OAAO,EAAM;EAEf,KAAK,WAEH,OADI,EAAM,YAAY,OAAa,EAAM,WAClC,GAAG,EAAM,SAAS,SAAS,EAAM,SAAS;EAEnD,KAAK,SAGH,OAFI,EAAM,aAAa,OAAa,mBAChC,EAAM,YAAY,OAAa,EAAM,WAClC,GAAG,EAAM,SAAS,KAAK,EAAM,SAAS;EAE/C,KAAK,UACH,OAAO,EAAM;EAEf,KAAK,gBACH,OAAO,YAAY,EAAM;EAE3B,KAAK,kBACH,OAAO,EAAM,UAAU,YAAY,GAAG,EAAM,SAAS;EAEvD,SACE,OAAO,EAAM;CACjB;AACF;;;AC/YA,IAAM,WAAkC;CACtC,UAAU;CACV,WAAW;CACX,YAAY;CACZ,WAAW;CACX,YAAY;CACZ,eAAe;CACf,gBAAgB;CAChB,YAAY;CACZ,iBAAiB;CACjB,iBAAiB;CACjB,aAAa;CACb,aAAa;CACb,UAAU;CACV,YAAY;CACZ,cAAc;AAChB,IAEM,IAAN,MAAuC;CAarC,YAAY,IAAsB,CAAC,GAAG;EACpC,kBAboG,0BAC7B,yBACE,4CACzC,IAAI,IAA8B,wCACnC,IAAI,IAAiE,0CACnE,IAAI,IAA8F,wCACpG,IAAI,IAA0E,uCAC/E,IAAI,IAA4B,iBACtC,EAAgB,uBACnB,IAIrB,KAAK,SAAS;CAChB;CAEA,IAAI,QAAiB;EACnB,OAAO,KAAK;CACd;CAEA,IAAI,cAAuB;EACzB,OAAO,KAAK;CACd;CAEA,MAAM,UAAyB;EAC7B,IAAI,KAAK,YACP;EAGF,IAAI,IAAY,IAAI,EAAgB;GAClC,aAAa;GACb,cAAc,OAAO;GACrB,cAAc;EAChB,CAAC;EAED,AAAI,KAAK,OAAO,UACd,IAAY,EAAe,IAAY,GAAa,GAAG,MAAoB;GACzE,QAAQ,IAAI,YAAY,GAAK,GAAG,CAAI;EACtC,CAAC;EAGH,IAAM,IAAa,MAAM,EAA6E,GAAW,EAC/G,MAAM,YAAY,OACpB,CAAC;EA0CD,AAxCA,KAAK,aAAa,GAClB,KAAK,eAAe,EAAW,aAAa,GAC5C,KAAK,cAAc,EAAW,YAAY,GAC1C,KAAK,eAAe,IAEpB,KAAK,aAAa,iBAAiB,iBAAiB,MAAmB;GAErE,AADA,KAAK,SAAS,GACd,KAAK,eAAe,SAAS,MAAO;IAClC,EAAG,CAAK;GACV,CAAC;EACH,CAAC,GAED,KAAK,aAAa,iBAAiB,gBAAgB,EAAE,aAAU,mBAAuE;GAEpI,AADA,KAAK,SAAS;IAAE,GAAG,KAAK;IAAQ;IAAU;GAAU,GACpD,KAAK,cAAc,SAAS,MAAO;IACjC,EAAG,GAAU,CAAS;GACxB,CAAC;EACH,CAAC,GAED,KAAK,aAAa,iBAAiB,gBAAgB,EAAE,QAAK,SAAM,uBAAwG;GAEtK,AADA,KAAK,SAAS;IAAE,GAAG,KAAK;IAAQ,WAAW;IAAK,YAAY;IAAM;GAAc,GAChF,KAAK,gBAAgB,SAAS,MAAO;IACnC,EAAG,GAAK,GAAM,CAAa;GAC7B,CAAC;EACH,CAAC,GAED,KAAK,aAAa,iBAAiB,gBAAgB,EAAE,oBAAiB,qBAAkF;GAEtJ,AADA,KAAK,SAAS;IAAE,GAAG,KAAK;IAAQ;IAAiB;GAAY,GAC7D,KAAK,cAAc,SAAS,MAAO;IACjC,EAAG,GAAiB,CAAW;GACjC,CAAC;EACH,CAAC,GAED,KAAK,aAAa,iBAAiB,eAAe,EAAE,cAA6B;GAC/E,KAAK,aAAa,SAAS,MAAO;IAChC,EAAG,CAAI;GACT,CAAC;EACH,CAAC,GAED,MAAM,KAAK,WAAW,GACtB,KAAK,YAAY,KAAK,SAAS,KAAA,CAAS;CAC1C;CAEA,MAAM,eAAe,GAAoC;EACvD,IAAI,CAAC,KAAK,cAAc,MAAU,MAAM,gCAAgC;EACxE,OAAO,KAAK,aAAa,KAAK,kBAAkB,CAAQ;CAC1D;CAEA,MAAM,WAAW,GAAW,GAA0B;EACpD,IAAI,CAAC,KAAK,cAAc,MAAU,MAAM,gCAAgC;EACxE,OAAO,KAAK,aAAa,KAAK,cAAc,GAAG,CAAC;CAClD;CAEA,MAAM,cAAc,GAAqC;EACvD,IAAI,CAAC,KAAK,cAAc,MAAU,MAAM,gCAAgC;EACxE,OAAO,KAAK,aAAa,KAAK,iBAAiB,CAAS;CAC1D;CAEA,MAAM,cAAc,GAAqC;EACvD,IAAI,CAAC,KAAK,cAAc,MAAU,MAAM,gCAAgC;EACxE,OAAO,KAAK,aAAa,KAAK,iBAAiB,CAAS;CAC1D;CAEA,MAAM,cAAc,GAAuC;EACzD,IAAI,CAAC,KAAK,cAAc,MAAU,MAAM,gCAAgC;EACxE,OAAO,KAAK,aAAa,KAAK,iBAAiB,CAAW;CAC5D;CAEA,MAAM,QAAQ,GAAsC;EAClD,IAAI,CAAC,KAAK,cAAc,MAAU,MAAM,gCAAgC;EACxE,OAAO,KAAK,aAAa,KAAK,WAAW,CAAU;CACrD;CAEA,MAAM,iBAAiB,GAAsB,IAAQ,IAAM,IAAsB,IAAyB;EACxG,IAAI,CAAC,KAAK,cAAc,MAAU,MAAM,gCAAgC;EACxE,OAAO,KAAK,aAAa,KAAK,oBAAoB,GAAc,GAAO,CAAmB;CAC5F;CAEA,MAAM,WAAW,GAAoB,IAAQ,IAAwB;EACnE,IAAI,CAAC,KAAK,cAAc,MAAU,MAAM,gCAAgC;EACxE,OAAO,KAAK,aAAa,KAAK,cAAc,GAAY,CAAK;CAC/D;CAEA,MAAM,aAAa,GAAkC;EACnD,IAAI,CAAC,KAAK,cAAc,MAAU,MAAM,gCAAgC;EACxE,OAAO,KAAK,aAAa,KAAK,gBAAgB,CAAI;CACpD;CAEA,MAAM,YAAY,GAAkC;EAClD,IAAI,CAAC,KAAK,cAAc,MAAU,MAAM,gCAAgC;EACxE,OAAO,KAAK,aAAa,KAAK,eAAe,CAAI;CACnD;CAEA,MAAM,YAA2B;EAC/B,IAAI,CAAC,KAAK,cAAc,MAAU,MAAM,gCAAgC;EACxE,OAAO,KAAK,aAAa,KAAK,WAAW;CAC3C;CAEA,WAAoB;EAClB,OAAO,KAAK;CACd;CAEA,MAAM,aAA+B;EACnC,IAAI,CAAC,KAAK,cAAc,MAAU,MAAM,gCAAgC;EACxE,IAAM,IAAQ,MAAM,KAAK,aAAa,KAAK,UAAU;EAErD,OADA,KAAK,SAAS,GACP;CACT;CAEA,cAAc,GAAgD;EAE5D,OADA,KAAK,eAAe,IAAI,CAAQ,SACnB,KAAK,eAAe,OAAO,CAAQ;CAClD;CAEA,aAAa,GAAmF;EAE9F,OADA,KAAK,cAAc,IAAI,CAAQ,SAClB,KAAK,cAAc,OAAO,CAAQ;CACjD;CAEA,aAAa,GAAgH;EAE3H,OADA,KAAK,gBAAgB,IAAI,CAAQ,SACpB,KAAK,gBAAgB,OAAO,CAAQ;CACnD;CAEA,aAAa,GAA4F;EAEvG,OADA,KAAK,cAAc,IAAI,CAAQ,SAClB,KAAK,cAAc,OAAO,CAAQ;CACjD;CAEA,WAAW,GAA8C;EAEvD,OADA,KAAK,aAAa,IAAI,CAAQ,SACjB,KAAK,aAAa,OAAO,CAAQ;CAChD;CAEA,aAAmB;EAUjB,AATA,KAAK,YAAY,MAAM,GACvB,KAAK,aAAa,MAClB,KAAK,eAAe,MACpB,KAAK,cAAc,MACnB,KAAK,eAAe,IACpB,KAAK,eAAe,MAAM,GAC1B,KAAK,cAAc,MAAM,GACzB,KAAK,gBAAgB,MAAM,GAC3B,KAAK,cAAc,MAAM,GACzB,KAAK,aAAa,MAAM;CAC1B;CAEA,qBAA+C;EAC7C,OAAO,EAAmB,KAAK,MAAM;CACvC;CAEA,kBAAiC;EAC/B,IAAM,IAAM,EAAmB,KAAK,MAAM;EAC1C,OAAO,IAAM,EAAgB,CAAG,IAAI;CACtC;AACF;AAEA,eAAsB,EAAe,IAAsB,CAAC,GAAsB;CAChF,IAAM,IAAS,IAAI,EAAa,CAAM;CAEtC,OADA,MAAM,EAAO,QAAQ,GACd;AACT;AAEA,SAAgB,EAAmB,IAAsB,CAAC,GAAuD;CAC/G,IAAM,IAAS,IAAI,EAAa,CAAM;CACtC,OAAO;EACL;EACA,SAAS,YAAY;GACnB,MAAM,EAAO,QAAQ;EACvB;CACF;AACF;AAEA,IAAA,IAAe;CACb;CACA;AACF"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yam8d/m8-sdk",
3
- "version": "0.3.2",
3
+ "version": "0.3.3",
4
4
  "description": "Client SDK for iframe applications that communicate with yam8d M8 host.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -8,7 +8,7 @@
8
8
  * Parameter views (inst, mixer, project, …) return a minimal context with viewName only.
9
9
  */
10
10
  import type { M8State } from './types';
11
- export type M8FieldType = 'chainRef' | 'phraseRef' | 'note' | 'velocity' | 'instrumentRef' | 'fxCommand' | 'fxValue' | 'transpose' | 'ticks' | 'volume' | 'ppq' | 'instrumentName' | 'eqSlot' | 'rowIndex';
11
+ export type M8FieldType = 'chainRef' | 'phraseRef' | 'note' | 'velocity' | 'instrumentRef' | 'fxCommand' | 'fxValue' | 'transpose' | 'ticks' | 'volume' | 'ppq' | 'instrumentName' | 'eqSlot' | 'rowIndex' | 'noteInterval' | 'onOff' | 'semitoneOffset' | 'swing';
12
12
  /**
13
13
  * A single parsed field value extracted from the currentLine.
14
14
  */