@yam8d/m8-sdk 0.3.3 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. package/README.md +898 -655
  2. package/dist/index.js +4470 -249
  3. package/dist/index.js.map +1 -1
  4. package/package.json +1 -1
  5. package/types/client.d.ts +94 -5
  6. package/types/engine/astar.d.ts +15 -0
  7. package/types/engine/audio.d.ts +29 -0
  8. package/types/engine/bus.d.ts +13 -0
  9. package/types/engine/connectWithRetry.d.ts +9 -0
  10. package/types/engine/connection.d.ts +56 -0
  11. package/types/engine/cursorNavigator.d.ts +15 -0
  12. package/types/engine/engine.d.ts +30 -0
  13. package/types/engine/eventBus.d.ts +5 -0
  14. package/types/engine/host/fileBrowserHelpers.d.ts +59 -0
  15. package/types/engine/host/fileBrowserSearch.d.ts +13 -0
  16. package/types/engine/host/floatEditor.d.ts +2 -0
  17. package/types/engine/host/hostIO.d.ts +30 -0
  18. package/types/engine/host/hostMethods.d.ts +22 -0
  19. package/types/engine/host/hostState.d.ts +7 -0
  20. package/types/engine/host/keySequences.d.ts +2 -0
  21. package/types/engine/host/midiTransport.d.ts +10 -0
  22. package/types/engine/host/valueEditors.d.ts +7 -0
  23. package/types/engine/host/valueParsers.d.ts +24 -0
  24. package/types/engine/index.d.ts +14 -0
  25. package/types/engine/keys.d.ts +28 -0
  26. package/types/engine/macroRunner.d.ts +15 -0
  27. package/types/engine/protocol.d.ts +138 -0
  28. package/types/engine/queuedReaders.d.ts +18 -0
  29. package/types/engine/store.d.ts +65 -0
  30. package/types/engine/viewExtractor.d.ts +27 -0
  31. package/types/engine/viewGraph.d.ts +9 -0
  32. package/types/engine/viewNavigation.d.ts +16 -0
  33. package/types/fxCommands.d.ts +47 -0
  34. package/types/fxInsight.d.ts +136 -0
  35. package/types/index.d.ts +9 -3
  36. package/types/parameterViews.d.ts +104 -0
  37. package/types/scaleInsight.d.ts +47 -0
  38. package/types/theme.d.ts +42 -0
  39. package/types/types.d.ts +131 -0
  40. package/types/viewContext.d.ts +216 -15
package/README.md CHANGED
@@ -1,655 +1,898 @@
1
- # @yam8d/m8-sdk
2
-
3
- Client SDK for iframe applications that communicate with an M8 tracker host via [yam8d](https://github.com/mio-moto/yam8d).
4
-
5
- > **License:** MIT — © kronsilds
6
-
7
- ---
8
-
9
- ## Table of contents
10
-
11
- - [Install](#install)
12
- - [Quick start](#quick-start)
13
- - [Factory functions](#factory-functions)
14
- - [M8Client](#m8client)
15
- - [Properties](#properties)
16
- - [Navigation](#navigation)
17
- - [Value setters](#value-setters)
18
- - [Key input](#key-input)
19
- - [State](#state)
20
- - [Event subscriptions](#event-subscriptions)
21
- - [Semantic context](#semantic-context-on-the-client)
22
- - [Lifecycle](#lifecycle)
23
- - [Standalone semantic context functions](#standalone-semantic-context-functions)
24
- - [Types reference](#types-reference)
25
- - [M8State](#m8state)
26
- - [CursorPos / CursorRect / RGB / SystemInfos](#cursorpos--cursorrect--rgb--systeminfos)
27
- - [M8SemanticContext](#m8semanticcontext)
28
- - [M8ParsedRow](#m8parsedrow)
29
- - [M8ActiveField / M8ParsedField](#m8activefield--m8parsedfield)
30
- - [M8FieldType](#m8fieldtype)
31
- - [M8KeyName](#m8keyname)
32
- - [M8SdkConfig](#m8sdkconfig)
33
- - [Host events](#host-events)
34
- - [Supported views](#supported-views)
35
- - [Notes](#notes)
36
-
37
- ---
38
-
39
- ## Install
40
-
41
- ```bash
42
- npm install @yam8d/m8-sdk
43
- ```
44
-
45
- For local development before publishing:
46
-
47
- ```bash
48
- npm install ../yam8d/packages/m8-sdk
49
- ```
50
-
51
- > The app **must run inside the yam8d host iframe**. Opening it directly in a standalone browser tab will fail the SDK handshake.
52
-
53
- ---
54
-
55
- ## Quick start
56
-
57
- ```ts
58
- import { createM8Client } from '@yam8d/m8-sdk'
59
-
60
- const m8 = await createM8Client()
61
-
62
- console.log(m8.state.viewName) // e.g. 'song'
63
-
64
- await m8.sendKeyPress(['right'])
65
-
66
- const off = m8.onStateChange((state) => {
67
- console.log(state.cursorPos)
68
- })
69
-
70
- off() // unsubscribe
71
- m8.disconnect()
72
- ```
73
-
74
- ---
75
-
76
- ## Factory functions
77
-
78
- ### `createM8Client(config?): Promise<M8Client>`
79
-
80
- Creates and connects an `M8Client` in one step using top-level `await`.
81
-
82
- ```ts
83
- const m8 = await createM8Client({ debug: true })
84
- ```
85
-
86
- ### `createM8ClientSync(config?): { client: M8Client; connect: () => Promise<void> }`
87
-
88
- Returns a client instance and a deferred `connect()` call. Useful in React or other frameworks where top-level `await` is inconvenient.
89
-
90
- ```ts
91
- import { createM8ClientSync } from '@yam8d/m8-sdk'
92
-
93
- const { client: m8, connect } = createM8ClientSync()
94
-
95
- useEffect(() => {
96
- connect().then(() => console.log('connected'))
97
- return () => m8.disconnect()
98
- }, [])
99
- ```
100
-
101
- ---
102
-
103
- ## M8Client
104
-
105
- ### Properties
106
-
107
- | Property | Type | Description |
108
- |---|---|---|
109
- | `state` | `M8State` | Last known full device state (updated on every host event). |
110
- | `isConnected` | `boolean` | Whether the SDK handshake has completed. |
111
-
112
- ---
113
-
114
- ### Navigation
115
-
116
- #### `navigateToView(viewName: string): Promise<boolean>`
117
-
118
- Asks the host to navigate to a named view (e.g. `'song'`, `'phrase'`). Returns `true` on success.
119
-
120
- ```ts
121
- await m8.navigateToView('phrase')
122
- ```
123
-
124
- #### `navigateTo(x: number, y: number): Promise<void>`
125
-
126
- Moves the cursor to the given character-grid position.
127
-
128
- ```ts
129
- await m8.navigateTo(3, 5)
130
- ```
131
-
132
- ---
133
-
134
- ### Value setters
135
-
136
- All setters act on the field currently under the cursor.
137
-
138
- #### `setValueToHex(targetHex: number): Promise<boolean>`
139
-
140
- Sets the current field to the given hex integer (e.g. `0x0A`). Returns `true` if the value was successfully applied.
141
-
142
- #### `setValueToInt(targetInt: number): Promise<boolean>`
143
-
144
- Sets the current field to the given decimal integer. Returns `true` on success.
145
-
146
- #### `setValueFloat(targetFloat: number): Promise<boolean>`
147
-
148
- Sets a float parameter parsed from the whole line, e.g. `TUNE 440.00 G` or `GAIN 05.25 -19.75 -15.00`
149
- (multiple floats per line are supported; the cursor's highlight selects which one is edited).
150
- Works with the cursor on a single digit group (`440` / `00`) or spanning the whole float (`19.75`,
151
- with the sign usually excluded from the highlight). With whole-float focus, edit+up/down moves the
152
- integer part and edit+left/right the decimal part; with group focus the SDK relocates the cursor
153
- between the groups for the fastest path. Negative values are supported and step sizes are measured
154
- per field from the device.
155
- Returns `true` when the displayed value matches.
156
-
157
- #### `setNote(noteString: string): Promise<boolean>`
158
-
159
- Sets a note field using M8 note format, e.g. `'C-4'`, `'A#3'`, `'OFF'`. Returns `true` on success.
160
-
161
- #### `setValueToString(targetString: string, exact?: boolean, searchInCurrentLine?: boolean): Promise<boolean>`
162
-
163
- Sets the current field by navigating to match `targetString`.
164
-
165
- - `exact` (default `true`): require an exact string match.
166
- - `searchInCurrentLine` (default `false`): restrict the search to the current row.
167
-
168
- Returns `true` if the target was reached.
169
-
170
- #### `browseFile(targetText: string, exact?: boolean): Promise<boolean>`
171
-
172
- Navigates a file-browse dialog to an entry matching `targetText`. Returns `true` on success.
173
-
174
- ---
175
-
176
- ### Key input
177
-
178
- #### `sendKeyPress(keys: M8KeyName[]): Promise<void>`
179
-
180
- Sends a simultaneous key press + release for the given keys.
181
-
182
- ```ts
183
- await m8.sendKeyPress(['shift', 'play'])
184
- ```
185
-
186
- #### `sendKeyDown(keys: M8KeyName[]): Promise<void>`
187
-
188
- Holds the given keys down without releasing. Pair with `sendKeyUp()`.
189
-
190
- ```ts
191
- await m8.sendKeyDown(['shift'])
192
- await m8.sendKeyPress(['right'])
193
- await m8.sendKeyUp()
194
- ```
195
-
196
- #### `sendKeyUp(): Promise<void>`
197
-
198
- Releases all currently held keys.
199
-
200
- ---
201
-
202
- ### State
203
-
204
- #### `getState(): M8State`
205
-
206
- Returns the in-memory cached state synchronously. Same as reading `m8.state`.
207
-
208
- #### `fetchState(): Promise<M8State>`
209
-
210
- Requests a fresh state snapshot from the host and updates the internal cache.
211
-
212
- ```ts
213
- const state = await m8.fetchState()
214
- ```
215
-
216
- ---
217
-
218
- ### Event subscriptions
219
-
220
- Every `on*` method returns an **unsubscribe function** — call it to stop listening.
221
-
222
- #### `onStateChange(callback: (state: M8State) => void): () => void`
223
-
224
- Fires whenever any part of the device state changes.
225
-
226
- ```ts
227
- const off = m8.onStateChange((state) => {
228
- console.log(state.viewName, state.cursorPos)
229
- })
230
- off() // unsubscribe
231
- ```
232
-
233
- #### `onViewChange(callback: (viewName: string | null, viewTitle: string | null) => void): () => void`
234
-
235
- Fires when the active view changes.
236
-
237
- ```ts
238
- m8.onViewChange((name, title) => {
239
- console.log(`Switched to ${name}: ${title}`)
240
- })
241
- ```
242
-
243
- #### `onCursorMove(callback: (pos: CursorPos | null, rect: CursorRect | null, selectionMode: boolean) => void): () => void`
244
-
245
- Fires when the cursor position or selection mode changes.
246
-
247
- ```ts
248
- m8.onCursorMove((pos, rect, sel) => {
249
- console.log(`Cursor at (${pos?.x}, ${pos?.y}), selecting: ${sel}`)
250
- })
251
- ```
252
-
253
- #### `onTextUpdate(callback: (textUnderCursor: string | null, currentLine: string | null) => void): () => void`
254
-
255
- Fires when the text under the cursor or the full current line changes.
256
-
257
- ```ts
258
- m8.onTextUpdate((text, line) => {
259
- console.log('Under cursor:', text)
260
- console.log('Full line:', line)
261
- })
262
- ```
263
-
264
- #### `onKeyPress(callback: (keys: number) => void): () => void`
265
-
266
- Fires when a physical key event occurs on the device. `keys` is a bitmask of active keys.
267
-
268
- ---
269
-
270
- ### Semantic context on the client
271
-
272
- #### `getSemanticContext(): M8SemanticContext | null`
273
-
274
- Parses `m8.state` into structured field data. Returns `null` when no view is active.
275
-
276
- ```ts
277
- const ctx = m8.getSemanticContext()
278
-
279
- if (ctx?.activeField?.type === 'chainRef') {
280
- const track = ctx.activeField.meta?.trackIndex // 1–8
281
- const chain = ctx.activeField.hexValue // 0–255 or null if empty
282
- const row = ctx.row?.rowIndex
283
- }
284
- ```
285
-
286
- #### `describeContext(): string | null`
287
-
288
- Returns a human-readable one-liner, e.g.:
289
-
290
- - `"Track 3 — Chain 0A — Song View row 02"`
291
- - `"Note A#4 — Step 7 (Phrase View)"`
292
- - `"FX1 Command: KIL (Kill Note) — Step 0 — Phrase View row 03"`
293
-
294
- Returns `null` when not connected or no view is active.
295
-
296
- ---
297
-
298
- ### Lifecycle
299
-
300
- #### `disconnect(): void`
301
-
302
- Closes the host connection and clears all event listeners and internal state.
303
-
304
- ---
305
-
306
- ## Standalone semantic context functions
307
-
308
- These are re-exported from `@yam8d/m8-sdk` for use without an `M8Client` instance.
309
-
310
- ### `getSemanticContext(state: M8State): M8SemanticContext | null`
311
-
312
- Parses an `M8State` snapshot into a semantic context. Returns `null` if `state.viewName` is not set.
313
-
314
- ```ts
315
- import { getSemanticContext } from '@yam8d/m8-sdk'
316
-
317
- const ctx = getSemanticContext(state)
318
- if (ctx?.isGridView && ctx.row) {
319
- const note = ctx.row.fields['note']
320
- console.log(note?.rawValue) // e.g. 'C-4'
321
- }
322
- ```
323
-
324
- ### `describeContext(ctx: M8SemanticContext): string`
325
-
326
- Returns a human-readable summary of the context (see examples above).
327
-
328
- ### `formatFieldValue(field: M8ParsedField): string`
329
-
330
- Formats a single parsed field as a display string:
331
-
332
- | Type | Example output |
333
- |---|---|
334
- | `note` | `"Note C-4"`, `"Note Off"` |
335
- | `transpose` | `"+3 st"`, `"-12 st"`, `"±0 st"` |
336
- | `velocity` | `"57h (87 / 127 max)"` |
337
- | `fxValue` | `"1Ah (dec 26)"` |
338
- | `ticks` | `"06h (6 ticks)"`, `"00 (skip step)"` |
339
- | `chainRef` / `phraseRef` / `fxCommand` / `volume` | raw value, e.g. `"0A"`, `"KIL"` |
340
- | empty cell | `"(empty)"` |
341
-
342
- ### `lookupFxCommand(cmd: string): { description: string } | undefined`
343
-
344
- Looks up a 3-character FX command name (case-insensitive) across all categories (sequencer, instrument, mixer).
345
-
346
- ```ts
347
- import { lookupFxCommand } from '@yam8d/m8-sdk'
348
-
349
- const info = lookupFxCommand('KIL')
350
- // → { description: 'Kill: stops the playing instrument after XX ticks.' }
351
- ```
352
-
353
- ---
354
-
355
- ## Types reference
356
-
357
- ### `M8State`
358
-
359
- The complete snapshot of the device at a point in time.
360
-
361
- ```ts
362
- interface M8State {
363
- viewName: string | null // active view key, e.g. 'song', 'phrase'
364
- viewTitle: string | null // raw title string from screen, e.g. ' SONG '
365
- minimapKey: string | null // current minimap position key
366
- cursorPos: CursorPos | null // character-grid position of the cursor
367
- cursorRect: CursorRect | null // pixel rectangle of the cursor overlay
368
- selectionMode: boolean // true when a selection is active
369
- highlightColor: RGB | null // cursor highlight colour
370
- titleColor: RGB | null // view title text colour
371
- backgroundColor: RGB | null // screen background colour
372
- textUnderCursor: string | null // characters covered by the cursor rect
373
- currentLine: string | null // full text of the row at cursorPos.y
374
- deviceModel: string | null // e.g. 'Headless', 'Model:02'
375
- fontMode: number | null // 0=standard, 1=bold, 2=large
376
- systemInfo: SystemInfos | null // device-reported system properties
377
- macroRunning: boolean // true while a macro is executing
378
- macroCurrentStep?: number // current macro step index
379
- macroSequenceLength?: number // total steps in the running macro
380
- }
381
- ```
382
-
383
- ### `CursorPos / CursorRect / RGB / SystemInfos`
384
-
385
- ```ts
386
- interface CursorPos { x: number; y: number }
387
- interface CursorRect { x: number; y: number; w: number; h: number }
388
- interface RGB { r: number; g: number; b: number }
389
- interface SystemInfos { [key: string]: string | number | boolean | undefined }
390
- ```
391
-
392
- ### `M8SemanticContext`
393
-
394
- ```ts
395
- interface M8SemanticContext {
396
- viewName: string // e.g. 'song', 'phrase', 'instrumentpool'
397
- viewTitle: string | null // e.g. 'Song View'
398
- viewDescription: string | null // long description from schema
399
- viewId: string | null // item ID parsed from screen title, e.g. 'F2', '03'; null for song/instrumentpool
400
- isUnsaved: boolean // true when the title carries a '*' suffix (unsaved changes)
401
- isGridView: boolean // true for song/chain/phrase/table/groove/instrumentpool
402
- row: M8ParsedRow | null // all columns on the line at cursorPos.y
403
- activeField: M8ActiveField | null // the column currently under the cursor
404
- }
405
- ```
406
-
407
- ### `M8ParsedRow`
408
-
409
- ```ts
410
- interface M8ParsedRow {
411
- rowIndex: number | null // hex row index from leftmost chars; null if unparseable
412
- rowKey: string // schema key, e.g. 'songRow', 'step', 'chainPos'
413
- fields: Record<string, M8ParsedField> // column key → parsed field
414
- }
415
- ```
416
-
417
- ### `M8ActiveField / M8ParsedField`
418
-
419
- `M8ActiveField` extends `M8ParsedField` with two extra convenience fields.
420
-
421
- ```ts
422
- interface M8ParsedField {
423
- key: string // schema key, e.g. 'fx1cmd', 'track3', 'note'
424
- label: string // human label, e.g. 'FX1 Command', 'Track 3'
425
- type: M8FieldType
426
- rawValue: string // trimmed text sliced from currentLine
427
- hexValue: number | null // parsed integer; null for empty cells and text types
428
- isEmpty: boolean // true when '--', '---', '---00', etc.
429
- meta?: Record<string, unknown> // view-specific extras (e.g. { trackIndex: 3 })
430
- }
431
-
432
- interface M8ActiveField extends M8ParsedField {
433
- viewName: string // repeated for convenience
434
- rowIndex: number | null // row index of the line containing this field
435
- }
436
- ```
437
-
438
- ### `M8FieldType`
439
-
440
- ```ts
441
- type M8FieldType =
442
- | 'chainRef' // chain number hex 00–FF; '--' = empty
443
- | 'phraseRef' // phrase number hex 00–FF; '--' = empty
444
- | 'note' // note name e.g. 'C-4', 'A#3'; '---' = empty; 'OFF' = note-off
445
- | 'velocity' // velocity 00–7F hex; '--' = inherit
446
- | 'instrumentRef' // instrument slot 00–7F hex; '--' = keep current
447
- | 'fxCommand' // 3-char command name e.g. 'KIL'; '---' = none
448
- | 'fxValue' // FX parameter 00–FF hex
449
- | 'transpose' // relative semitones: 00=none, 01–7F=+1–+127, FF–80=−1–−128
450
- | 'ticks' // groove ticks per step hex; '00'=skip; '--'=end of groove
451
- | 'volume' // level 00–FF hex; '--' = no override
452
- | 'ppq' // pulses-per-quarter-note decimal; groove row 0 only
453
- | 'instrumentName' // instrument name text ≤12 chars; no hexValue
454
- | 'eqSlot' // EQ slot assignment hex; '--' = none
455
- | 'rowIndex' // row/step index hex
456
- ```
457
-
458
- ### `M8KeyName`
459
-
460
- ```ts
461
- type M8KeyName = 'left' | 'right' | 'up' | 'down' | 'shift' | 'play' | 'opt' | 'edit'
462
- ```
463
-
464
- ### `M8SdkConfig`
465
-
466
- ```ts
467
- interface M8SdkConfig {
468
- debug?: boolean // enables verbose postMessage logging via DebugMessenger
469
- }
470
- ```
471
-
472
- ---
473
-
474
- ## Host events
475
-
476
- These events are emitted by the host and consumed internally by `M8Client`. They are listed here for reference if you use the `post-me` connection directly.
477
-
478
- | Event | Payload | Fired when |
479
- |---|---|---|
480
- | `stateChanged` | `M8State` | Any state field changes |
481
- | `viewChanged` | `{ viewName, viewTitle }` | The active view changes |
482
- | `cursorMoved` | `{ pos, rect, selectionMode }` | Cursor position or selection mode changes |
483
- | `textUpdated` | `{ textUnderCursor, currentLine }` | Text under cursor or current line changes |
484
- | `keyPressed` | `{ keys: number }` | A physical key event fires on the device |
485
-
486
- ---
487
-
488
- ## Supported views
489
-
490
- The semantic context parser supports these `viewName` values:
491
-
492
- | `viewName` | Columns |
493
- |---|---|
494
- | `song` | `track1`–`track8` (chainRef, meta: trackIndex 1–8) |
495
- | `chain` | `phrase` (phraseRef), `transpose` |
496
- | `phrase` | `note`, `vel`, `inst`, `fx1cmd`, `fx1val`, `fx2cmd`, `fx2val`, `fx3cmd`, `fx3val` |
497
- | `table` | `transpose`, `volume`, `fx1cmd`, `fx1val`, `fx2cmd`, `fx2val`, `fx3cmd`, `fx3val` |
498
- | `groove` | `ticks`, `ppq` (row 0 only) |
499
- | `instrumentpool` | `name`, `dry`, `mx`, `de`, `rv`, `eq` |
500
-
501
- All other views (`isGridView: false`) return a context with `row: null` and `activeField: null`.
502
-
503
- ---
504
-
505
- ## Notes
506
-
507
- - **Playback indicator** — When a row is actively playing the M8 prepends `<` or `>` to `currentLine`. The SDK strips this automatically before parsing so column offsets remain consistent.
508
- - **Font mode** — All column offsets assume font mode 0 (Headless: 8×10 px cells; Model:02: 12×14 px cells). Font modes 1 (bold) and 2 (large) may shift positions.
509
- - **Schema file** — View and column definitions live in `src/m8-view-context.json` and are bundled into `dist/index.js` at build time.
510
- - **Requirement** — The app must run inside the yam8d host iframe. The `ChildHandshake` from `post-me` will never resolve in a standalone tab.
511
-
512
-
513
- Client SDK for iframe applications that communicate with an M8 host.
514
-
515
- ## Install
516
-
517
- ```bash
518
- npm install @yam8d/m8-sdk
519
- ```
520
-
521
- For local development before the package is published:
522
-
523
- ```bash
524
- npm install ../yam8d/packages/m8-sdk
525
- ```
526
-
527
- ## Usage
528
-
529
- ```ts
530
- import { createM8Client } from '@yam8d/m8-sdk'
531
-
532
- const m8 = await createM8Client()
533
-
534
- console.log(m8.state.viewName)
535
- await m8.sendKeyPress(['play'])
536
-
537
- const unsubscribe = m8.onStateChange((state) => {
538
- console.log(state.cursorPos)
539
- })
540
-
541
- unsubscribe()
542
- m8.disconnect()
543
- ```
544
-
545
- For React or other environments where top-level `await` is not convenient:
546
-
547
- ```ts
548
- import { createM8ClientSync } from '@yam8d/m8-sdk'
549
-
550
- const { client, connect } = createM8ClientSync()
551
- await connect()
552
- ```
553
-
554
- The app must run inside the host iframe. Opening it directly in a standalone browser tab cannot establish the SDK handshake.
555
-
556
- ---
557
-
558
- ## Semantic Context
559
-
560
- The SDK can interpret the current cursor position and row text as typed, labelled field values. This works for the five grid views (`song`, `chain`, `phrase`, `table`, `groove`) and the `instrumentpool` view.
561
-
562
- ### Via the client
563
-
564
- ```ts
565
- const ctx = m8.getSemanticContext()
566
- // or as a human-readable string:
567
- const description = m8.describeContext()
568
- // e.g. "Track 3 — Chain 0A — Song View row 02"
569
- ```
570
-
571
- ### Standalone functions
572
-
573
- ```ts
574
- import { getSemanticContext, describeContext, formatFieldValue, lookupFxCommand } from '@yam8d/m8-sdk'
575
-
576
- const ctx = getSemanticContext(state)
577
-
578
- if (ctx?.activeField) {
579
- const field = ctx.activeField
580
-
581
- console.log(field.key) // e.g. 'track3', 'note', 'fx1cmd'
582
- console.log(field.label) // e.g. 'Track 3', 'Note (N)', 'FX1 Command'
583
- console.log(field.type) // e.g. 'chainRef', 'note', 'fxCommand'
584
- console.log(field.rawValue) // raw text from the line, e.g. '0A', 'C-4', 'KIL'
585
- console.log(field.hexValue) // parsed integer or null (null for empty cells and text types)
586
- console.log(field.isEmpty) // true when '--' / '---' / '---00'
587
-
588
- // Song view: which track is the cursor on?
589
- if (field.type === 'chainRef') {
590
- console.log(field.meta?.trackIndex) // 1–8
591
- }
592
-
593
- // FX command: look up its description
594
- if (field.type === 'fxCommand' && !field.isEmpty) {
595
- const info = lookupFxCommand(field.rawValue)
596
- console.log(info?.description)
597
- }
598
- }
599
-
600
- // Row-level fields (all columns on the current line)
601
- if (ctx?.row) {
602
- console.log(ctx.row.rowIndex) // hex-parsed row index, e.g. 2
603
- console.log(ctx.row.fields['note']) // M8ParsedField for the note column
604
- }
605
- ```
606
-
607
- ### Returned types
608
-
609
- ```ts
610
- interface M8SemanticContext {
611
- viewName: string // e.g. 'song', 'phrase'
612
- viewTitle: string | null // e.g. 'Song View'
613
- viewDescription: string | null
614
- isGridView: boolean
615
- row: M8ParsedRow | null // all columns on the cursor's line
616
- activeField: M8ActiveField | null // the column under the cursor
617
- }
618
-
619
- interface M8ParsedField {
620
- key: string // schema key, e.g. 'fx1cmd', 'track3'
621
- label: string // human label, e.g. 'FX1 Command', 'Track 3'
622
- type: M8FieldType
623
- rawValue: string // trimmed text from currentLine
624
- hexValue: number | null
625
- isEmpty: boolean
626
- meta?: Record<string, unknown>
627
- }
628
- ```
629
-
630
- ### Supported field types (`M8FieldType`)
631
-
632
- | Type | Description |
633
- |---|---|
634
- | `chainRef` | Chain number (hex). `--` = empty. |
635
- | `phraseRef` | Phrase number (hex). `--` = empty. |
636
- | `note` | Note name, e.g. `C-4`, `A#3`. `---` = empty. |
637
- | `velocity` | Note velocity 00–7F (hex). |
638
- | `instrumentRef` | Instrument slot 00–7F (hex). `--` = keep current. |
639
- | `fxCommand` | 3-char FX command name, e.g. `KIL`, `ARP`. `---` = none. |
640
- | `fxValue` | FX value 00–FF (hex). |
641
- | `transpose` | Relative transpose (hex): `00`=none, `01`–`7F`=+1–+127, `FF`–`80`=−1–−128. |
642
- | `ticks` | Groove ticks per step (hex). |
643
- | `volume` | Volume level 00–FF (hex). `--` = no override. |
644
- | `ppq` | Pulses Per Quarter note (decimal). Groove row 0 only. |
645
- | `instrumentName` | Instrument name text (up to 12 chars). No `hexValue`. |
646
- | `eqSlot` | EQ slot assignment (hex). `--` = none. |
647
- | `rowIndex` | Row/step index (hex). |
648
-
649
- ### Playback indicator
650
-
651
- When a row is actively playing, the M8 prepends a `<` or `>` character to `currentLine`. The SDK strips this automatically before parsing, so column offsets are always consistent.
652
-
653
- ### Notes file
654
-
655
- View/column definitions live in `src/m8-view-context.json` and are bundled into `dist/index.js` at build time. All x/width values assume **font mode 0** (8×10 px cells on Headless, 12×14 px on Model:02). Font modes 1 and 2 may shift column positions.
1
+ # @yam8d/m8-sdk
2
+
3
+ Client SDK for apps that read and drive a Dirtywave M8 tracker. Inside a [yam8d](https://github.com/mio-moto/yam8d) External Apps iframe it talks to yam8d; anywhere else it connects to the M8 itself over Web Serial, WebUSB or Web MIDI ([standalone mode](#standalone-mode)). The API is the same in both modes.
4
+
5
+ > **License:** MIT — © kronsilds
6
+
7
+ ---
8
+
9
+ ## Table of contents
10
+
11
+ - [Install](#install)
12
+ - [Quick start](#quick-start)
13
+ - [Factory functions](#factory-functions)
14
+ - [Standalone mode](#standalone-mode)
15
+ - [M8Client](#m8client)
16
+ - [Properties](#properties)
17
+ - [Navigation](#navigation)
18
+ - [Value setters](#value-setters)
19
+ - [Key input](#key-input)
20
+ - [State](#state)
21
+ - [Event subscriptions](#event-subscriptions)
22
+ - [Semantic context](#semantic-context-on-the-client)
23
+ - [Device connection (standalone)](#device-connection-standalone)
24
+ - [Lifecycle](#lifecycle)
25
+ - [Standalone semantic context functions](#standalone-semantic-context-functions)
26
+ - [Types reference](#types-reference)
27
+ - [M8State](#m8state)
28
+ - [CursorPos / CursorRect / RGB / SystemInfos](#cursorpos--cursorrect--rgb--systeminfos)
29
+ - [M8SemanticContext](#m8semanticcontext)
30
+ - [M8DescriptionPart](#m8descriptionpart)
31
+ - [M8ParsedRow](#m8parsedrow)
32
+ - [M8ActiveField / M8ParsedField](#m8activefield--m8parsedfield)
33
+ - [M8FieldType](#m8fieldtype)
34
+ - [M8KeyName](#m8keyname)
35
+ - [M8SdkConfig](#m8sdkconfig)
36
+ - [Host events](#host-events)
37
+ - [Supported views](#supported-views)
38
+ - [Notes](#notes)
39
+
40
+ ---
41
+
42
+ ## Install
43
+
44
+ ```bash
45
+ npm install @yam8d/m8-sdk
46
+ ```
47
+
48
+ For local development before publishing:
49
+
50
+ ```bash
51
+ npm install ../yam8d/packages/m8-sdk
52
+ ```
53
+
54
+ > Inside yam8d the app uses yam8d's connection to the M8. Opened in its own tab, it runs in [standalone mode](#standalone-mode) and connects to the M8 itself.
55
+
56
+ ---
57
+
58
+ ## Quick start
59
+
60
+ ```ts
61
+ import { createM8Client } from '@yam8d/m8-sdk'
62
+
63
+ const m8 = await createM8Client()
64
+
65
+ console.log(m8.state.viewName) // e.g. 'song'
66
+
67
+ await m8.sendKeyPress(['right'])
68
+
69
+ const off = m8.onStateChange((state) => {
70
+ console.log(state.cursorPos)
71
+ })
72
+
73
+ off() // unsubscribe
74
+ m8.disconnect()
75
+ ```
76
+
77
+ ---
78
+
79
+ ## Factory functions
80
+
81
+ ### `createM8Client(config?): Promise<M8Client>`
82
+
83
+ Creates and connects an `M8Client` in one step using top-level `await`.
84
+
85
+ ```ts
86
+ const m8 = await createM8Client({ debug: true })
87
+ ```
88
+
89
+ ### `createM8ClientSync(config?): { client: M8Client; connect: () => Promise<void> }`
90
+
91
+ Returns a client instance and a deferred `connect()` call. Useful in React or other frameworks where top-level `await` is inconvenient.
92
+
93
+ ```ts
94
+ import { createM8ClientSync } from '@yam8d/m8-sdk'
95
+
96
+ const { client: m8, connect } = createM8ClientSync()
97
+
98
+ useEffect(() => {
99
+ connect().then(() => console.log('connected'))
100
+ return () => m8.disconnect()
101
+ }, [])
102
+ ```
103
+
104
+ ---
105
+
106
+ ## Standalone mode
107
+
108
+ `createM8Client()` decides the mode when it connects (`client.mode`):
109
+
110
+ - **`'host'`**: the page runs in an iframe that yam8d named `yam8d-external-app` (exported as `YAM8D_FRAME_NAME`). The client waits for yam8d's handshake, however long it takes.
111
+ - **`'standalone'`**: the page is a top-level tab. The client runs the same engine yam8d uses, in the page itself: the Web Serial / WebUSB / Web MIDI SysEx connection, the display protocol (SLIP, draw rect/character commands), screen text extraction, cursor tracking, view navigation over the view graph, the value editors, file browsing and the MIDI clock.
112
+ - In an iframe that yam8d did not name (an older yam8d, or another page embedding the app), the client tries the handshake for `handshakeTimeoutMs` (3 s) and then goes standalone.
113
+ - Force a mode with `createM8Client({ mode: 'host' | 'standalone' })`.
114
+
115
+ In standalone mode the M8 has to be connected before any call works:
116
+
117
+ ```ts
118
+ const m8 = await createM8Client()
119
+ if (m8.mode === 'standalone' && !m8.isConnected) {
120
+ // The browser's device picker only opens from a user gesture
121
+ connectButton.onclick = () => m8.connectDevice() // or connectDevice({ transport: 'midi' })
122
+ }
123
+ m8.onConnectionChange((connected) => updateUi(connected))
124
+ ```
125
+
126
+ - **Audio**: `connectDevice({ audio: true })` (or `connectAudio()` later) plays the M8's USB audio input through the page, as yam8d does. The browser asks for audio permission; only an input whose label contains "M8" is played, never another microphone. Without it the device stays connected and the warning goes to the console; `isAudioConnected` tells. `disconnectDevice()` stops it too.
127
+ - **MIDI clock**: `enableMidiTransport()` / `onTransport()` work the same as in yam8d: the page opens the M8's MIDI input itself (Web MIDI permission). Enable it again after each new device connection.
128
+ - At start, a standalone client reopens a device the page was granted before, without a prompt (`autoConnect: false` turns that off).
129
+ - Only one page can hold the M8's serial port. Disconnect it in yam8d (or close that tab) before connecting from another page, and call `disconnectDevice()` to hand it back.
130
+ - `getDevice()` gives standalone apps the raw protocol events (`text`, `rect`, `wave`, `key`, `system`) and device commands (`sendKeys`, `resetScreen`, `sendNoteOn`, `sendNoteOff`, `sendProgramChange`), e.g. to draw the screen themselves.
131
+ - `getDeviceSupport()` tells which of the three browser APIs are available (Chromium browsers have all three; Firefox and Safari have none).
132
+ - The engine is exported too (`createM8Connection`, `createM8Engine`, `M8KeyMask`) for apps that want to drive the device below the client.
133
+
134
+ ---
135
+
136
+ ## M8Client
137
+
138
+ ### Properties
139
+
140
+ | Property | Type | Description |
141
+ |---|---|---|
142
+ | `state` | `M8State` | Last known full device state (updated on every host event). |
143
+ | `isConnected` | `boolean` | Whether calls reach an M8: host mode, after the yam8d handshake; standalone, while a device is connected. |
144
+ | `mode` | `'host' \| 'standalone' \| null` | Chosen at connect; `null` before. |
145
+ | `deviceTransport` | `'serial' \| 'usb' \| 'midi' \| null` | Standalone: the API the device is connected through. |
146
+
147
+ ---
148
+
149
+ ### Navigation
150
+
151
+ #### `navigateToView(viewName: string): Promise<boolean>`
152
+
153
+ Asks the host to navigate to a named view (e.g. `'song'`, `'phrase'`). Returns `true` on success.
154
+
155
+ ```ts
156
+ await m8.navigateToView('phrase')
157
+ ```
158
+
159
+ #### `navigateTo(x: number, y: number): Promise<void>`
160
+
161
+ Moves the cursor to the given character-grid position.
162
+
163
+ ```ts
164
+ await m8.navigateTo(3, 5)
165
+ ```
166
+
167
+ ---
168
+
169
+ ### Value setters
170
+
171
+ All setters act on the field currently under the cursor.
172
+
173
+ #### `setValueToHex(targetHex: number): Promise<boolean>`
174
+
175
+ Sets the current field to the given hex integer (e.g. `0x0A`). Returns `true` if the value was successfully applied.
176
+
177
+ #### `setValueToInt(targetInt: number): Promise<boolean>`
178
+
179
+ Sets the current field to the given decimal integer. Returns `true` on success.
180
+
181
+ #### `setValueFloat(targetFloat: number): Promise<boolean>`
182
+
183
+ Sets a float parameter parsed from the whole line, e.g. `TUNE 440.00 G` or `GAIN 05.25 -19.75 -15.00`
184
+ (multiple floats per line are supported; the cursor's highlight selects which one is edited).
185
+ Works with the cursor on a single digit group (`440` / `00`) or spanning the whole float (`19.75`,
186
+ with the sign usually excluded from the highlight). With whole-float focus, edit+up/down moves the
187
+ integer part and edit+left/right the decimal part; with group focus the SDK relocates the cursor
188
+ between the groups for the fastest path. Negative values are supported and step sizes are measured
189
+ per field from the device.
190
+ Returns `true` when the displayed value matches.
191
+
192
+ #### `setNote(noteString: string): Promise<boolean>`
193
+
194
+ Sets a note field using M8 note format, e.g. `'C-4'`, `'A#3'`, `'OFF'`. Returns `true` on success.
195
+
196
+ #### `setValueToString(targetString: string, exact?: boolean, searchInCurrentLine?: boolean): Promise<boolean>`
197
+
198
+ Sets the current field by navigating to match `targetString`.
199
+
200
+ - `exact` (default `true`): require an exact string match.
201
+ - `searchInCurrentLine` (default `false`): restrict the search to the current row.
202
+
203
+ Returns `true` if the target was reached.
204
+
205
+ #### `browseFile(targetText: string, exact?: boolean): Promise<boolean>`
206
+
207
+ Navigates a file-browse dialog to an entry matching `targetText`. Returns `true` on success.
208
+
209
+ ---
210
+
211
+ ### Key input
212
+
213
+ #### `sendKeyPress(keys: M8KeyName[]): Promise<void>`
214
+
215
+ Sends a simultaneous key press + release for the given keys.
216
+
217
+ ```ts
218
+ await m8.sendKeyPress(['shift', 'play'])
219
+ ```
220
+
221
+ #### `sendKeyDown(keys: M8KeyName[]): Promise<void>`
222
+
223
+ Holds the given keys down without releasing. Pair with `sendKeyUp()`.
224
+
225
+ ```ts
226
+ await m8.sendKeyDown(['shift'])
227
+ await m8.sendKeyPress(['right'])
228
+ await m8.sendKeyUp()
229
+ ```
230
+
231
+ #### `sendKeyUp(): Promise<void>`
232
+
233
+ Releases all currently held keys.
234
+
235
+ ---
236
+
237
+ ### State
238
+
239
+ #### `getState(): M8State`
240
+
241
+ Returns the in-memory cached state synchronously. Same as reading `m8.state`.
242
+
243
+ #### `fetchState(): Promise<M8State>`
244
+
245
+ Requests a fresh state snapshot from the host and updates the internal cache.
246
+
247
+ ```ts
248
+ const state = await m8.fetchState()
249
+ ```
250
+
251
+ #### `getScreen(): Promise<M8Screen>`
252
+
253
+ The whole screen as text rows (40 columns, column 0 is the margin where play markers are drawn), read from the character stream without moving the cursor.
254
+
255
+ #### `getScreenCells(): Promise<M8ScreenCells>`
256
+
257
+ The same screen plus the colour of every character cell, so an app can look like the device or learn which theme colour the M8 uses for what. `fg[row][col]` and `bg[row][col]` are `'#rrggbb'`, `null` where nothing was drawn. Needs a yam8d host with SDK 0.5 or later; older hosts reject the call.
258
+
259
+ ```ts
260
+ const cells = await m8.getScreenCells()
261
+ cells.fg[5][3] // colour of the first note of a phrase, e.g. '#ffffff'
262
+ ```
263
+
264
+ ---
265
+
266
+ ### Event subscriptions
267
+
268
+ Every `on*` method returns an **unsubscribe function** — call it to stop listening.
269
+
270
+ #### `onStateChange(callback: (state: M8State) => void): () => void`
271
+
272
+ Fires whenever any part of the device state changes.
273
+
274
+ ```ts
275
+ const off = m8.onStateChange((state) => {
276
+ console.log(state.viewName, state.cursorPos)
277
+ })
278
+ off() // unsubscribe
279
+ ```
280
+
281
+ #### `onViewChange(callback: (viewName: string | null, viewTitle: string | null) => void): () => void`
282
+
283
+ Fires when the active view changes.
284
+
285
+ ```ts
286
+ m8.onViewChange((name, title) => {
287
+ console.log(`Switched to ${name}: ${title}`)
288
+ })
289
+ ```
290
+
291
+ #### `onCursorMove(callback: (pos: CursorPos | null, rect: CursorRect | null, selectionMode: boolean) => void): () => void`
292
+
293
+ Fires when the cursor position or selection mode changes.
294
+
295
+ ```ts
296
+ m8.onCursorMove((pos, rect, sel) => {
297
+ console.log(`Cursor at (${pos?.x}, ${pos?.y}), selecting: ${sel}`)
298
+ })
299
+ ```
300
+
301
+ #### `onTextUpdate(callback: (textUnderCursor: string | null, currentLine: string | null) => void): () => void`
302
+
303
+ Fires when the text under the cursor or the full current line changes.
304
+
305
+ ```ts
306
+ m8.onTextUpdate((text, line) => {
307
+ console.log('Under cursor:', text)
308
+ console.log('Full line:', line)
309
+ })
310
+ ```
311
+
312
+ #### `onKeyPress(callback: (keys: number) => void): () => void`
313
+
314
+ Fires when a physical key event occurs on the device. `keys` is a bitmask of active keys.
315
+
316
+ ---
317
+
318
+ ### Semantic context on the client
319
+
320
+ #### `getSemanticContext(): M8SemanticContext | null`
321
+
322
+ Parses `m8.state` into structured field data. Returns `null` when no view is active.
323
+
324
+ ```ts
325
+ const ctx = m8.getSemanticContext()
326
+
327
+ if (ctx?.activeField?.type === 'chainRef') {
328
+ const track = ctx.activeField.meta?.trackIndex // 1–8
329
+ const chain = ctx.activeField.hexValue // 0–255 or null if empty
330
+ const row = ctx.row?.rowIndex
331
+ }
332
+ ```
333
+
334
+ #### `describeContext(): string | null`
335
+
336
+ Returns a human-readable one-liner, e.g.:
337
+
338
+ - `"Track 3: 0A — Track 3 — Song View row 01"`
339
+ - `"Note (N): Note C-4 — Phrase View row 00"`
340
+ - `"FX2 Command: KIL — (Kill: stops the playing instrument after XX ticks) — value 03: stops the note after 3 ticks — Phrase View row 01"`
341
+
342
+ Returns `null` when not connected or no view is active. It is the text of `describeContextParts()` joined with `' — '`.
343
+
344
+ It reads the cursor against its surroundings when that changes the meaning:
345
+
346
+ - **FX values are explained** — `ARP37` on `C-4` → `"+3 and +7 semitones: C-4 D#4 G-4"`, `KIL04` → `"stops the note after 4 ticks"`, relative instrument commands show their sign.
347
+ - **REP is followed down its FX lane.** With `VOL10` above, `REP05` reads `"repeats VOL from 10 (step 1), +05 per step → 15 on this step"`, and an empty cell below it reads `"REP still active: VOL reaches 1A on this step (+05 per step since step 3)"`. A new command, `REP00`, or an `RTO` limit in the same lane ends or caps it. `^^` (a REP carried over from the previous phrase) is recognised when it can be attributed to one lane.
348
+ - **Relative commands are tracked.** Per the manual, a relative instrument command (`VOL`, `CUT`, `PIT`, `EA1`… — the ones the device help marks `(RELATIVE)`) keeps its change until a note is triggered with a `RET` or with an instrument number in the `I` column. `VOL02` twice reads `"VOL has moved by +04 in 2 steps since the instrument number on step 0; stays until a note with an instrument number (I) or a RET resets it"`, REP repeats are counted, and the `I` cell or `RET` that resets it lists what it wipes (`"resets the relative changes still in force (CUT +10, VOL +0B)"`). Only the current phrase is visible, so totals start at the phrase's own last reset.
349
+ - **Instrument-type commands say who offers them.** `FM3` → `"…; offered by FM Synth instruments"`.
350
+ - **Scale View** names the scale the enabled notes spell (`"Scale: C D E F G A B — Major (Ionian)"`), says what the row under the cursor is (`"E — a major 3rd (+4 semitones above C) — is in the scale"`), reports detuning, and explains the KEY / TUNE / NAME rows.
351
+
352
+ The surroundings come from a copy of the screen that the client refreshes in the background after every state event. It is only used when its row under the cursor still matches `currentLine`; otherwise the description quietly falls back to the cursor row alone. Call `await m8.getScreen()` or `await m8.captureSnapshot()` first when you need it guaranteed current.
353
+
354
+ #### `describeContextParts(): M8DescriptionPart[] | null`
355
+
356
+ The same description as ordered, tagged parts, so you can keep, drop, reorder or style some of them instead of parsing the string:
357
+
358
+ ```ts
359
+ m8.describeContextParts()
360
+ // [{ kind: 'field', text: 'FX2 Command: KIL' },
361
+ // { kind: 'fxInfo', text: '(Kill: stops the playing instrument after XX ticks)' },
362
+ // { kind: 'meaning', text: 'value 03: stops the note after 3 ticks' },
363
+ // { kind: 'location', text: 'Phrase View row 01' }]
364
+
365
+ // Your own view label instead of the SDK's location:
366
+ const text = m8.describeContextParts()?.filter((p) => p.kind !== 'location').map((p) => p.text).join(' · ')
367
+ ```
368
+
369
+ See [`M8DescriptionPart`](#m8descriptionpart) for the kinds. Everything a part says is also in the context as data (`activeField`, `fxLane`, `relativeFx`, `resetsRelative`, `scaleNote`, `scale`, …), for when you want to write the text yourself.
370
+
371
+ ---
372
+
373
+ ### Device connection (standalone)
374
+
375
+ #### `connectDevice(options?: { transport?: 'auto' | 'serial' | 'usb' | 'midi'; audio?: boolean }): Promise<void>`
376
+
377
+ Connects to the M8 from this page. `'auto'` (default) uses Web Serial, then WebUSB, then Web MIDI SysEx, by browser support. Without a previously granted device the browser shows its picker, which needs a user gesture: call it from a click handler. Replaces the current device. No-op in host mode.
378
+
379
+ #### `disconnectDevice(): Promise<void>`
380
+
381
+ Releases the device (and stops its audio) so yam8d or another page can open it.
382
+
383
+ #### `connectAudio(): Promise<void>` / `disconnectAudio(): Promise<void>`
384
+
385
+ Standalone: plays the M8's USB audio through the page, or stops it. `connectAudio()` rejects when no audio input named M8 exists. No-ops in host mode (yam8d plays it). `isAudioConnected` reports the state.
386
+
387
+ #### `onConnectionChange(callback: (connected: boolean) => void): () => void`
388
+
389
+ Fires when `isConnected` changes: handshake done, device connected, unplugged or released.
390
+
391
+ #### `getDevice(): M8DeviceBus | null`
392
+
393
+ Standalone: raw protocol events and device commands. `null` in host mode or while disconnected.
394
+
395
+ ---
396
+
397
+ ### Lifecycle
398
+
399
+ #### `disconnect(): void`
400
+
401
+ Closes the host connection (and the device, in standalone mode) and clears all event listeners and internal state.
402
+
403
+ ---
404
+
405
+ ## Standalone semantic context functions
406
+
407
+ These are re-exported from `@yam8d/m8-sdk` for use without an `M8Client` instance.
408
+
409
+ ### `getSemanticContext(state: M8State, screen?: M8Screen | null): M8SemanticContext | null`
410
+
411
+ Parses an `M8State` snapshot into a semantic context. Returns `null` if `state.viewName` is not set.
412
+
413
+ Pass the whole `screen` (from `getScreen()`) to fill the context's look-around fields: `steps` (all 16 phrase steps), `fxLane` (what the FX cell under the cursor means in its lane), `relativeFx`, `resetsRelative`, `carriedRepLane`, and `scale` (Scale View). A screen whose cursor row no longer matches `state.currentLine` is ignored. `lineText` is always set for grid views.
414
+
415
+ ```ts
416
+ import { getSemanticContext } from '@yam8d/m8-sdk'
417
+
418
+ const ctx = getSemanticContext(state)
419
+ if (ctx?.isGridView && ctx.row) {
420
+ const note = ctx.row.fields['note']
421
+ console.log(note?.rawValue) // e.g. 'C-4'
422
+ }
423
+ ```
424
+
425
+ ### `describeContext(ctx: M8SemanticContext): string`
426
+
427
+ Returns a human-readable summary of the context (see examples above).
428
+
429
+ ### `describeContextParts(ctx: M8SemanticContext): M8DescriptionPart[]`
430
+
431
+ The summary as ordered, tagged parts; `describeContext()` joins their text with `' — '`.
432
+
433
+ ### `formatFieldValue(field: M8ParsedField): string`
434
+
435
+ Formats a single parsed field as a display string:
436
+
437
+ | Type | Example output |
438
+ |---|---|
439
+ | `note` | `"Note C-4"`, `"Note Off"` |
440
+ | `transpose` | `"+3 st"`, `"-12 st"`, `"±0 st"` |
441
+ | `velocity` | `"57h (87 / 127 max)"` |
442
+ | `fxValue` | `"1Ah (dec 26)"` |
443
+ | `ticks` | `"06h (6 ticks)"`, `"00 (skip step)"` |
444
+ | `chainRef` / `phraseRef` / `fxCommand` / `volume` | raw value, e.g. `"0A"`, `"KIL"` |
445
+ | empty cell | `"(empty)"` |
446
+
447
+ ### `lookupFxCommand(cmd: string): M8FxCommandInfo | undefined`
448
+
449
+ Looks up a 3-character FX command name (case-insensitive) across all categories (sequencer, instrument, modulation, mixer). Modulator commands carry their slot as the third character (`EA1`, `LF3`).
450
+
451
+ ```ts
452
+ import { lookupFxCommand } from '@yam8d/m8-sdk'
453
+
454
+ lookupFxCommand('KIL')
455
+ // → { description: 'Kill: stops the playing instrument after XX ticks.',
456
+ // category: 'sequencer', relative: false, instrumentTypes: null, slot: null }
457
+ lookupFxCommand('FM3')
458
+ // → { …, category: 'instrument', relative: true, instrumentTypes: ['fmsynth'], slot: null }
459
+ lookupFxCommand('LF3')
460
+ // → { …, category: 'modulation', relative: false, instrumentTypes: null, slot: 3 }
461
+ ```
462
+
463
+ `instrumentTypes` lists the instrument types that offer the command when not every type does (`null` = all, or not an instrument command).
464
+
465
+ ### `getInstrumentFxCommands(type: string): M8InstrumentFxList | null`
466
+
467
+ The instrument commands offered by an instrument type (the "Current Instrument" section of the Effect Command Help view, opened with `[EDIT]+[UP/DOWN]` on an FX command). `type` is the key from `M8SemanticContext.instrumentType.key`.
468
+
469
+ ```ts
470
+ getInstrumentFxCommands('hypersynth')
471
+ // → { type: 'hypersynth', verified: true,
472
+ // commands: ['VOL','PIT','FIN','CRD','CVO','SWM','WID','SUB','FIL','CUT','RES','AMP','LIM','PAN','SNC','ERR','DRY','SMX','SDL','SRV'] }
473
+ ```
474
+
475
+ `verified: true` means the list was read from a real M8 (Model:02): **sampler, macrosynth, fmsynth, hypersynth**. Wavsynth and External were not available on that device, so their lists are unknown (`null`); `midiout` only holds the documented `CHD`/`ADD`. The Sequencer FX and Mixer/Effects sections are the same for every type, and the Instrument Mods section depends on the kind of each modulator slot (envelope: `EA AT HO DE ET`, LFO: `LA LO LS LF LT`).
476
+
477
+ ### `RELATIVE_FX_RULE: string`
478
+
479
+ The manual's rule for relative commands, for tooltips.
480
+
481
+ ### `parseGridScreen(viewName: string, screen: M8Screen): M8ScreenGridRow[]`
482
+
483
+ Every content row of a grid view (`song`, `chain`, `phrase`, `table`, `groove`, `instrumentpool`) read off the whole screen, top to bottom, each with the `screenRow` it is drawn on. Use it to read a full page at once instead of only the cursor row. Rows whose index does not parse are skipped, so check `rowIndex` continuity when you need the whole page.
484
+
485
+ ```ts
486
+ const steps = parseGridScreen('phrase', await m8.getScreen())
487
+ steps[0].fields.note.rawValue // 'c-4' (the text stream is lower case)
488
+ ```
489
+
490
+ ### `parseViewTitle(rawTitle: string | null): { viewId: string | null; isUnsaved: boolean }`
491
+
492
+ The item number of a title (`'PHRASE 1F*'` → `{ viewId: '1F', isUnsaved: true }`).
493
+
494
+ ### Theme: `parseThemeScreen`, `observeThemeColors`, `M8_THEME_ROLES`, `M8_THEME_VIEW`
495
+
496
+ The device theme has 13 colours, edited in System Settings › EDIT THEME (view name `themesettings`, exported as `M8_THEME_VIEW`).
497
+
498
+ - `parseThemeScreen(screen)` reads them off the Theme View screen: `{ colors: { background, textEmpty, textInfo, textDefault, textValue, textTitles, playMarkers, cursor, selection, scopeSlider, meterLow, meterMid, meterPeak }, mode: 'rgb' | 'hsv', name }`, colours as `'#rrggbb'`. HSV rows are converted (each channel read as 00-FF). It returns `null` unless all 13 rows are on screen.
499
+ - `observeThemeColors(state)` gives the colours any screen shows without opening the Theme View: `background` and `textTitles`. (`state.cursorColor` is the cursor rectangle as drawn; the M8 animates it, so it is not the theme's CURSOR colour.)
500
+ - `M8_THEME_ROLES` lists the roles with the labels the device shows.
501
+
502
+ ```ts
503
+ if (m8.state.viewName === M8_THEME_VIEW) {
504
+ const theme = parseThemeScreen(await m8.getScreen())
505
+ }
506
+ ```
507
+
508
+ ---
509
+
510
+ ## Types reference
511
+
512
+ ### `M8State`
513
+
514
+ The complete snapshot of the device at a point in time.
515
+
516
+ ```ts
517
+ interface M8State {
518
+ viewName: string | null // active view key, e.g. 'song', 'phrase'
519
+ viewTitle: string | null // raw title string from screen, e.g. ' SONG '
520
+ minimapKey: string | null // current minimap position key
521
+ cursorPos: CursorPos | null // character-grid position of the cursor
522
+ cursorRect: CursorRect | null // pixel rectangle of the cursor overlay
523
+ selectionMode: boolean // true when a selection is active
524
+ highlightColor: RGB | null // cursor highlight colour
525
+ titleColor: RGB | null // view title text colour
526
+ backgroundColor: RGB | null // screen background colour
527
+ cursorColor?: RGB | null // cursor rectangle as drawn (animated, not the theme CURSOR); SDK 0.5+
528
+ textUnderCursor: string | null // characters covered by the cursor rect
529
+ currentLine: string | null // full text of the row at cursorPos.y
530
+ deviceModel: string | null // e.g. 'Headless', 'Model:02'
531
+ fontMode: number | null // 0=standard, 1=bold, 2=large
532
+ systemInfo: SystemInfos | null // device-reported system properties
533
+ macroRunning: boolean // true while a macro is executing
534
+ macroCurrentStep?: number // current macro step index
535
+ macroSequenceLength?: number // total steps in the running macro
536
+ }
537
+ ```
538
+
539
+ ### `CursorPos / CursorRect / RGB / SystemInfos`
540
+
541
+ ```ts
542
+ interface CursorPos { x: number; y: number }
543
+ interface CursorRect { x: number; y: number; w: number; h: number }
544
+ interface RGB { r: number; g: number; b: number }
545
+ interface SystemInfos { [key: string]: string | number | boolean | undefined }
546
+ ```
547
+
548
+ ### `M8SemanticContext`
549
+
550
+ ```ts
551
+ interface M8SemanticContext {
552
+ viewName: string // e.g. 'song', 'phrase', 'instrumentpool'
553
+ viewTitle: string | null // e.g. 'Song View'
554
+ viewDescription: string | null // long description from schema
555
+ viewId: string | null // item ID parsed from screen title, e.g. 'F2', '03'; null for song/instrumentpool
556
+ isUnsaved: boolean // true when the title carries a '*' suffix (unsaved changes)
557
+ isGridView: boolean // true for song/chain/phrase/table/groove/scale/instrumentpool
558
+ isParameterView: boolean // true for the label/value views (inst, mixer, project, …)
559
+ section: string | null // effectsettings: 'MODFX' | 'DELAY' | 'REVERB'
560
+ instrumentType: { text: string; key: string | null } | null // inst view + screen, e.g. { text: 'WAVSYNTH', key: 'wavsynth' }
561
+ row: M8ParsedRow | null // all columns on the line at cursorPos.y
562
+ activeField: M8ActiveField | null // the column currently under the cursor
563
+ lineText: string | null // cursor row text, playback marker removed
564
+ steps: M8ParsedRow[] | null // phrase view + screen: all 16 steps (index = step)
565
+ carriedRepLane: number | null // FX lane (1–3) showing '^^' (REP from previous phrase)
566
+ fxLane: M8FxLaneTrace | null // phrase view + screen: what the FX cell means in its lane
567
+ relativeFx: Record<string, M8RelativeFxState> | null // phrase view + screen: relative commands in force, with net change since the last trigger
568
+ resetsRelative: Record<string, M8RelativeFxState> | null // phrase view + screen: when the cursor step triggers the instrument (I number or RET), the relative changes it puts back
569
+ scaleNote: M8ScaleNote | null // scale view: the interval row under the cursor { interval, semitone, enabled, offset }
570
+ scale: M8ScaleInfo | null // scale view + screen: the 12 intervals interpreted together
571
+ }
572
+ ```
573
+
574
+ ### `M8DescriptionPart`
575
+
576
+ ```ts
577
+ interface M8DescriptionPart {
578
+ kind: M8DescriptionPartKind
579
+ text: string
580
+ }
581
+ ```
582
+
583
+ Parts come in reading order; a kind can appear more than once.
584
+
585
+ | Kind | Example |
586
+ |---|---|
587
+ | `field` | `"FX1 Command: KIL"`, `"Tempo (TEMPO): 120.00 BPM"` |
588
+ | `fxInfo` | `"(Kill: stops the playing instrument after XX ticks)"` |
589
+ | `fxTarget` | on an FX value: `"for KIL (…)"` |
590
+ | `meaning` | `"value 03: stops the note after 3 ticks"`, `"E is detuned by −0.50 st"` |
591
+ | `fxLane` | `"REP still active: VOL reaches 09 on this step (+02 per step since step 1)"` |
592
+ | `relative` | `"relative: VOL has moved by +0C over 2 steps since …"`, `"triggers the instrument again: resets …"` |
593
+ | `optionInfo` | what the selected option does |
594
+ | `fieldInfo` | what the field is |
595
+ | `fileInfo` | `"directory: [EDIT] opens it"` |
596
+ | `instrument` | `"Wavsynth instrument"` |
597
+ | `section` | `"Delay"`, `"MOD2"` |
598
+ | `scale` | `"Scale: C D E F G A B"`, `"Major (Ionian); detuned: E −0.50 st"` |
599
+ | `track` | `"Track 3"` |
600
+ | `location` | `"Phrase View row 07"`, `"Mixer View"` |
601
+ | `line` | the cursor row's text, when no field on it is recognised |
602
+
603
+ ### `M8ParsedRow`
604
+
605
+ ```ts
606
+ interface M8ParsedRow {
607
+ rowIndex: number | null // hex row index from leftmost chars; null if unparseable
608
+ rowKey: string // schema key, e.g. 'songRow', 'step', 'chainPos'
609
+ rowId: string | null // parameter views: matched schema row ('tempo', 'gain', …)
610
+ fields: Record<string, M8ParsedField> // column key → parsed field
611
+ }
612
+ ```
613
+
614
+ ### `M8ActiveField / M8ParsedField`
615
+
616
+ `M8ActiveField` extends `M8ParsedField` with two extra convenience fields.
617
+
618
+ ```ts
619
+ interface M8ParsedField {
620
+ key: string // schema key, e.g. 'fx1cmd', 'track3', 'note'
621
+ label: string // human label, e.g. 'FX1 Command', 'Track 3'
622
+ type: M8FieldType
623
+ rawValue: string // trimmed text sliced from currentLine
624
+ hexValue: number | null // parsed integer; null for empty cells and text types
625
+ isEmpty: boolean // true when '--', '---', '---00', etc.
626
+ meta?: Record<string, unknown> // view-specific extras (e.g. { trackIndex: 3 })
627
+ // parameter views (all optional):
628
+ description?: string // what the field is
629
+ text?: string | null // non-numeric part: option name, caption, name, instrument type
630
+ numericValue?: number | null // value in its natural base (hex, decimal, fractional)
631
+ values?: number[] | null // both numbers of an 'XX:YY' pair
632
+ parts?: string[] // names of those numbers, e.g. ['left time', 'right time']
633
+ unit?: string // e.g. ' BPM', ' dB'
634
+ valueDescription?: string // what the selected option does
635
+ fileKind?: M8FileKind // file browser rows: 'parent' | 'directory' | 'instrumentPreset' | 'sample' | 'm8file' | 'file'
636
+ }
637
+
638
+ interface M8ActiveField extends M8ParsedField {
639
+ viewName: string // repeated for convenience
640
+ rowIndex: number | null // row index of the line containing this field
641
+ }
642
+ ```
643
+
644
+ ### `M8FieldType`
645
+
646
+ ```ts
647
+ type M8FieldType =
648
+ | 'chainRef' // chain number hex 00–FF; '--' = empty
649
+ | 'phraseRef' // phrase number hex 00–FF; '--' = empty
650
+ | 'note' // note name e.g. 'C-4', 'A#3'; '---' = empty; 'OFF' = note-off
651
+ | 'velocity' // velocity 00–7F hex; '--' = inherit
652
+ | 'instrumentRef' // instrument slot 00–7F hex; '--' = keep current
653
+ | 'fxCommand' // 3-char command name e.g. 'KIL'; '---' = none
654
+ | 'fxValue' // FX parameter 00–FF hex
655
+ | 'transpose' // relative semitones: 00=none, 01–7F=+1–+127, FF–80=−1–−128
656
+ | 'ticks' // groove ticks per step hex; '00'=skip; '--'=end of groove
657
+ | 'volume' // level 00–FF hex; '--' = no override
658
+ | 'ppq' // pulses-per-quarter-note decimal; groove row 0 only
659
+ | 'instrumentName' // instrument name text ≤12 chars; no hexValue
660
+ | 'eqSlot' // EQ slot assignment hex; '--' = none
661
+ | 'rowIndex' // row/step index hex
662
+ | 'noteInterval' // scale view row label: C, C#, D, …
663
+ | 'onOff' // 'ON' / 'OFF' toggle
664
+ | 'semitoneOffset' // scale view detune, e.g. '-00.50'
665
+ | 'swing' // groove swing percentage
666
+ | 'instrumentType' // instrument engine: WAVSYNTH, SAMPLER, FMSYNTH, …
667
+ | 'name' // free-text name field; dashes = unnamed
668
+ | 'parameter' // named editable value: CUTOFF, TEMPO, GAIN, …
669
+ | 'option' // choice from a list: '00CHORUS', 'BELL', 'STEREO'
670
+ | 'action' // button: LOAD, SAVE, RENDER, SETTINGS, EQ, …
671
+ | 'fileEntry' // file browser row: '/folder', '/..', 'Bass.m8i'
672
+ ```
673
+
674
+ ### `M8KeyName`
675
+
676
+ ```ts
677
+ type M8KeyName = 'left' | 'right' | 'up' | 'down' | 'shift' | 'play' | 'opt' | 'edit'
678
+ ```
679
+
680
+ ### `M8SdkConfig`
681
+
682
+ ```ts
683
+ interface M8SdkConfig {
684
+ debug?: boolean // verbose logging (postMessage traffic, engine events)
685
+ mode?: 'auto' | 'host' | 'standalone' // default 'auto', see Standalone mode
686
+ handshakeTimeoutMs?: number // auto mode in an unmarked iframe; default 3000
687
+ autoConnect?: boolean // standalone: reopen a granted device at start; default true
688
+ }
689
+ ```
690
+
691
+ ---
692
+
693
+ ## Host events
694
+
695
+ These events are emitted by the host and consumed internally by `M8Client`. They are listed here for reference if you use the `post-me` connection directly.
696
+
697
+ | Event | Payload | Fired when |
698
+ |---|---|---|
699
+ | `stateChanged` | `M8State` | Any state field changes |
700
+ | `viewChanged` | `{ viewName, viewTitle }` | The active view changes |
701
+ | `cursorMoved` | `{ pos, rect, selectionMode }` | Cursor position or selection mode changes |
702
+ | `textUpdated` | `{ textUnderCursor, currentLine }` | Text under cursor or current line changes |
703
+ | `keyPressed` | `{ keys: number }` | A physical key event fires on the device |
704
+
705
+ ---
706
+
707
+ ## Supported views
708
+
709
+ The semantic context parser supports these `viewName` values:
710
+
711
+ | `viewName` | Columns |
712
+ |---|---|
713
+ | `song` | `track1`–`track8` (chainRef, meta: trackIndex 1–8) |
714
+ | `chain` | `phrase` (phraseRef), `transpose` |
715
+ | `phrase` | `note`, `vel`, `inst`, `fx1cmd`, `fx1val`, `fx2cmd`, `fx2val`, `fx3cmd`, `fx3val` |
716
+ | `table` | `transpose`, `volume`, `fx1cmd`, `fx1val`, `fx2cmd`, `fx2val`, `fx3cmd`, `fx3val` |
717
+ | `groove` | `ticks`, `ppq` (row 0 only), `swing` (row 0 only) |
718
+ | `scale` | `interval`, `en` (onOff), `offset` (semitoneOffset) |
719
+ | `instrumentpool` | `name`, `dry`, `mx`, `de`, `rv`, `eq` |
720
+ | `inst` | header: `instrumentType`, `load`/`save` (action), `name`, `transpose` (onOff), `tableTic`, `eq`; then the type's parameters as label/value pairs (`parameter` / `option`), described per instrument type |
721
+ | `instmods` | per modulation slot: type (`option`), `dest`, and the type's parameters (`amt`, `atk`, `hold`, `dec`, `sus`, `rel`, `peak`, `body`, `osc`, `trig`, `freq`, `src`, `lval`, `hval`) |
722
+ | `mixer` | `speakerVol`, `track1`–`track8`, `modfxVol`/`delayVol`/`reverbVol`, `inputVol`/`inputVol2`/`usbVol` and their sends, `mixEq` (action), `mainVol`, `limiter`, `djFilter`, `ott` |
723
+ | `project` | `tempo`, `transpose`, `groove`/`scale`/`liveQuantize` (option), `name`, and the `load`/`save`/`new`/`render`/`bundle`/… actions |
724
+ | `effectsettings` | per `section` (MODFX / DELAY / REVERB): type, input EQ (action), depth/frequency pairs, width, sends, delay time, feedback, room size, decay/shimmer |
725
+ | `mixeq`, `modfxeq`, `delayeq`, `reverbeq` | LOW/MID/HIGH band `gain`, `freq`, `q`, `type` (option), `mode` (option) |
726
+ | `mixscope`, `limiterscope` | `zoom`, `peak`, `softClip`, `mainVol`, `limiter`, `djFilter`, `ott`, `mixEq`; `atk`/`rel`/`type`/`res`/`time`/`color` as label/value pairs |
727
+ | `systemsettings` | `backlight`, `fontOptions`, `editTheme` (action), `notePreview`, `recCountIn`, `metronomeVol`, `usbAudioMode`, `usbMainOut`, `lineInGate`, `keyDelayRep`, … |
728
+ | `loadinstrument`, `selectsavedirectory` | `entry` (fileEntry) |
729
+ | `createdirectory` | `name` |
730
+
731
+ Grid views are read by column position (`src/m8-view-context.json`). Parameter views are label/value screens: a row is recognised from its label text and its cells come from `src/m8-parameter-views.json`, whose column positions were measured on the manual's screenshots. Instrument parameters vary with the instrument type, so they are read as label/value pairs and looked up in a per-type vocabulary. Layouts that only appear for some instrument types (Sampler `SAMPLE` row, FM operators, Hypersynth chord, MIDI CC rows) are best-effort.
732
+
733
+ ---
734
+
735
+ ## Notes
736
+
737
+ - **Screen geometry** — The screen is 40 × 24 characters. Every row has a one-column left margin, so schema `x` positions count from column 1 and `cursorPos.x` is the schema `x` + 1 (the SDK accounts for this). Titles are on row 3 and content starts on row 5. The text stream is lower case (`tempo`, `b-4`, `ff`); the SDK matches case-insensitively and shows values upper case, except user-typed names and file names.
738
+ - **Playback indicator** — When a row is actively playing the M8 draws `<` or `>` in the left margin, so it appears as the first character of `currentLine`. The SDK strips this automatically before parsing so column offsets remain consistent.
739
+ - **Font mode** — All column offsets assume font mode 0 (Headless: 8×10 px cells; Model:02: 12×14 px cells). Font modes 1 (bold) and 2 (large) may shift positions.
740
+ - **Schema file** — View and column definitions live in `src/m8-view-context.json` and are bundled into `dist/index.js` at build time.
741
+ - **Requirement** — The app must run inside the yam8d host iframe. The `ChildHandshake` from `post-me` will never resolve in a standalone tab.
742
+
743
+
744
+ Client SDK for iframe applications that communicate with an M8 host.
745
+
746
+ ## Install
747
+
748
+ ```bash
749
+ npm install @yam8d/m8-sdk
750
+ ```
751
+
752
+ For local development before the package is published:
753
+
754
+ ```bash
755
+ npm install ../yam8d/packages/m8-sdk
756
+ ```
757
+
758
+ ## Usage
759
+
760
+ ```ts
761
+ import { createM8Client } from '@yam8d/m8-sdk'
762
+
763
+ const m8 = await createM8Client()
764
+
765
+ console.log(m8.state.viewName)
766
+ await m8.sendKeyPress(['play'])
767
+
768
+ const unsubscribe = m8.onStateChange((state) => {
769
+ console.log(state.cursorPos)
770
+ })
771
+
772
+ unsubscribe()
773
+ m8.disconnect()
774
+ ```
775
+
776
+ For React or other environments where top-level `await` is not convenient:
777
+
778
+ ```ts
779
+ import { createM8ClientSync } from '@yam8d/m8-sdk'
780
+
781
+ const { client, connect } = createM8ClientSync()
782
+ await connect()
783
+ ```
784
+
785
+ The app must run inside the host iframe. Opening it directly in a standalone browser tab cannot establish the SDK handshake.
786
+
787
+ ---
788
+
789
+ ## Semantic Context
790
+
791
+ The SDK can interpret the current cursor position and row text as typed, labelled field values. This works for the five grid views (`song`, `chain`, `phrase`, `table`, `groove`) and the `instrumentpool` view.
792
+
793
+ ### Via the client
794
+
795
+ ```ts
796
+ const ctx = m8.getSemanticContext()
797
+ // or as a human-readable string:
798
+ const description = m8.describeContext()
799
+ // e.g. "Track 3: 0A — Track 3 — Song View row 01"
800
+ // or as tagged parts to build your own:
801
+ const parts = m8.describeContextParts()
802
+ ```
803
+
804
+ ### Standalone functions
805
+
806
+ ```ts
807
+ import { getSemanticContext, describeContext, formatFieldValue, lookupFxCommand } from '@yam8d/m8-sdk'
808
+
809
+ const ctx = getSemanticContext(state)
810
+
811
+ if (ctx?.activeField) {
812
+ const field = ctx.activeField
813
+
814
+ console.log(field.key) // e.g. 'track3', 'note', 'fx1cmd'
815
+ console.log(field.label) // e.g. 'Track 3', 'Note (N)', 'FX1 Command'
816
+ console.log(field.type) // e.g. 'chainRef', 'note', 'fxCommand'
817
+ console.log(field.rawValue) // raw text from the line, e.g. '0A', 'C-4', 'KIL'
818
+ console.log(field.hexValue) // parsed integer or null (null for empty cells and text types)
819
+ console.log(field.isEmpty) // true when '--' / '---' / '---00'
820
+
821
+ // Song view: which track is the cursor on?
822
+ if (field.type === 'chainRef') {
823
+ console.log(field.meta?.trackIndex) // 1–8
824
+ }
825
+
826
+ // FX command: look up its description
827
+ if (field.type === 'fxCommand' && !field.isEmpty) {
828
+ const info = lookupFxCommand(field.rawValue)
829
+ console.log(info?.description)
830
+ }
831
+ }
832
+
833
+ // Row-level fields (all columns on the current line)
834
+ if (ctx?.row) {
835
+ console.log(ctx.row.rowIndex) // hex-parsed row index, e.g. 2
836
+ console.log(ctx.row.fields['note']) // M8ParsedField for the note column
837
+ }
838
+ ```
839
+
840
+ ### Returned types
841
+
842
+ ```ts
843
+ interface M8SemanticContext {
844
+ viewName: string // e.g. 'song', 'phrase'
845
+ viewTitle: string | null // e.g. 'Song View'
846
+ viewDescription: string | null
847
+ isGridView: boolean
848
+ row: M8ParsedRow | null // all columns on the cursor's line
849
+ activeField: M8ActiveField | null // the column under the cursor
850
+ }
851
+
852
+ interface M8ParsedField {
853
+ key: string // schema key, e.g. 'fx1cmd', 'track3'
854
+ label: string // human label, e.g. 'FX1 Command', 'Track 3'
855
+ type: M8FieldType
856
+ rawValue: string // trimmed text from currentLine
857
+ hexValue: number | null
858
+ isEmpty: boolean
859
+ meta?: Record<string, unknown>
860
+ }
861
+ ```
862
+
863
+ ### Supported field types (`M8FieldType`)
864
+
865
+ | Type | Description |
866
+ |---|---|
867
+ | `chainRef` | Chain number (hex). `--` = empty. |
868
+ | `phraseRef` | Phrase number (hex). `--` = empty. |
869
+ | `note` | Note name, e.g. `C-4`, `A#3`. `---` = empty. |
870
+ | `velocity` | Note velocity 00–7F (hex). |
871
+ | `instrumentRef` | Instrument slot 00–7F (hex). `--` = keep current. |
872
+ | `fxCommand` | 3-char FX command name, e.g. `KIL`, `ARP`. `---` = none. |
873
+ | `fxValue` | FX value 00–FF (hex). |
874
+ | `transpose` | Relative transpose (hex): `00`=none, `01`–`7F`=+1–+127, `FF`–`80`=−1–−128. |
875
+ | `ticks` | Groove ticks per step (hex). |
876
+ | `volume` | Volume level 00–FF (hex). `--` = no override. |
877
+ | `ppq` | Pulses Per Quarter note (decimal). Groove row 0 only. |
878
+ | `instrumentName` | Instrument name text (up to 12 chars). No `hexValue`. |
879
+ | `eqSlot` | EQ slot assignment (hex). `--` = none. |
880
+ | `rowIndex` | Row/step index (hex). |
881
+ | `noteInterval` | Scale View row label (`C`, `C#`, …). |
882
+ | `onOff` | `ON` / `OFF` toggle. |
883
+ | `semitoneOffset` | Scale View detune, e.g. `-00.50`. |
884
+ | `swing` | Groove swing percentage. |
885
+ | `instrumentType` | Instrument engine (`WAVSYNTH`, `SAMPLER`, …); `text` holds the name. |
886
+ | `name` | Free-text name field; a row of dashes = unnamed. |
887
+ | `parameter` | Named editable value (`CUTOFF`, `TEMPO`, `GAIN`); read `hexValue` / `numericValue` / `values`. |
888
+ | `option` | Choice from a list (`00CHORUS`, `BELL`); `hexValue` is the index when shown, `text` the name. |
889
+ | `action` | Button caption (`LOAD`, `SAVE`, `RENDER`, `SETTINGS`); `text` holds it. |
890
+ | `fileEntry` | File browser row: `/folder`, `/..`, or a file name. |
891
+
892
+ ### Playback indicator
893
+
894
+ When a row is actively playing, the M8 prepends a `<` or `>` character to `currentLine`. The SDK strips this automatically before parsing, so column offsets are always consistent.
895
+
896
+ ### Notes file
897
+
898
+ View/column definitions live in `src/m8-view-context.json` and are bundled into `dist/index.js` at build time. All x/width values assume **font mode 0** (8×10 px cells on Headless, 12×14 px on Model:02). Font modes 1 and 2 may shift column positions.