@yam8d/m8-sdk 0.1.0 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,5 +1,515 @@
1
1
  # @yam8d/m8-sdk
2
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
+
3
513
  Client SDK for iframe applications that communicate with an M8 host.
4
514
 
5
515
  ## Install
@@ -42,3 +552,104 @@ await connect()
42
552
  ```
43
553
 
44
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.