@yam8d/m8-sdk 0.4.0 → 0.5.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.
Files changed (37) hide show
  1. package/README.md +798 -796
  2. package/dist/index.js +3411 -363
  3. package/dist/index.js.map +1 -1
  4. package/package.json +2 -2
  5. package/types/client.d.ts +58 -2
  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/index.d.ts +9 -3
  34. package/types/scaleInsight.d.ts +1 -1
  35. package/types/theme.d.ts +42 -0
  36. package/types/types.d.ts +124 -0
  37. package/types/viewContext.d.ts +37 -0
package/README.md CHANGED
@@ -1,796 +1,798 @@
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
- - [M8DescriptionPart](#m8descriptionpart)
29
- - [M8ParsedRow](#m8parsedrow)
30
- - [M8ActiveField / M8ParsedField](#m8activefield--m8parsedfield)
31
- - [M8FieldType](#m8fieldtype)
32
- - [M8KeyName](#m8keyname)
33
- - [M8SdkConfig](#m8sdkconfig)
34
- - [Host events](#host-events)
35
- - [Supported views](#supported-views)
36
- - [Notes](#notes)
37
-
38
- ---
39
-
40
- ## Install
41
-
42
- ```bash
43
- npm install @yam8d/m8-sdk
44
- ```
45
-
46
- For local development before publishing:
47
-
48
- ```bash
49
- npm install ../yam8d/packages/m8-sdk
50
- ```
51
-
52
- > The app **must run inside the yam8d host iframe**. Opening it directly in a standalone browser tab will fail the SDK handshake.
53
-
54
- ---
55
-
56
- ## Quick start
57
-
58
- ```ts
59
- import { createM8Client } from '@yam8d/m8-sdk'
60
-
61
- const m8 = await createM8Client()
62
-
63
- console.log(m8.state.viewName) // e.g. 'song'
64
-
65
- await m8.sendKeyPress(['right'])
66
-
67
- const off = m8.onStateChange((state) => {
68
- console.log(state.cursorPos)
69
- })
70
-
71
- off() // unsubscribe
72
- m8.disconnect()
73
- ```
74
-
75
- ---
76
-
77
- ## Factory functions
78
-
79
- ### `createM8Client(config?): Promise<M8Client>`
80
-
81
- Creates and connects an `M8Client` in one step using top-level `await`.
82
-
83
- ```ts
84
- const m8 = await createM8Client({ debug: true })
85
- ```
86
-
87
- ### `createM8ClientSync(config?): { client: M8Client; connect: () => Promise<void> }`
88
-
89
- Returns a client instance and a deferred `connect()` call. Useful in React or other frameworks where top-level `await` is inconvenient.
90
-
91
- ```ts
92
- import { createM8ClientSync } from '@yam8d/m8-sdk'
93
-
94
- const { client: m8, connect } = createM8ClientSync()
95
-
96
- useEffect(() => {
97
- connect().then(() => console.log('connected'))
98
- return () => m8.disconnect()
99
- }, [])
100
- ```
101
-
102
- ---
103
-
104
- ## M8Client
105
-
106
- ### Properties
107
-
108
- | Property | Type | Description |
109
- |---|---|---|
110
- | `state` | `M8State` | Last known full device state (updated on every host event). |
111
- | `isConnected` | `boolean` | Whether the SDK handshake has completed. |
112
-
113
- ---
114
-
115
- ### Navigation
116
-
117
- #### `navigateToView(viewName: string): Promise<boolean>`
118
-
119
- Asks the host to navigate to a named view (e.g. `'song'`, `'phrase'`). Returns `true` on success.
120
-
121
- ```ts
122
- await m8.navigateToView('phrase')
123
- ```
124
-
125
- #### `navigateTo(x: number, y: number): Promise<void>`
126
-
127
- Moves the cursor to the given character-grid position.
128
-
129
- ```ts
130
- await m8.navigateTo(3, 5)
131
- ```
132
-
133
- ---
134
-
135
- ### Value setters
136
-
137
- All setters act on the field currently under the cursor.
138
-
139
- #### `setValueToHex(targetHex: number): Promise<boolean>`
140
-
141
- Sets the current field to the given hex integer (e.g. `0x0A`). Returns `true` if the value was successfully applied.
142
-
143
- #### `setValueToInt(targetInt: number): Promise<boolean>`
144
-
145
- Sets the current field to the given decimal integer. Returns `true` on success.
146
-
147
- #### `setValueFloat(targetFloat: number): Promise<boolean>`
148
-
149
- Sets a float parameter parsed from the whole line, e.g. `TUNE 440.00 G` or `GAIN 05.25 -19.75 -15.00`
150
- (multiple floats per line are supported; the cursor's highlight selects which one is edited).
151
- Works with the cursor on a single digit group (`440` / `00`) or spanning the whole float (`19.75`,
152
- with the sign usually excluded from the highlight). With whole-float focus, edit+up/down moves the
153
- integer part and edit+left/right the decimal part; with group focus the SDK relocates the cursor
154
- between the groups for the fastest path. Negative values are supported and step sizes are measured
155
- per field from the device.
156
- Returns `true` when the displayed value matches.
157
-
158
- #### `setNote(noteString: string): Promise<boolean>`
159
-
160
- Sets a note field using M8 note format, e.g. `'C-4'`, `'A#3'`, `'OFF'`. Returns `true` on success.
161
-
162
- #### `setValueToString(targetString: string, exact?: boolean, searchInCurrentLine?: boolean): Promise<boolean>`
163
-
164
- Sets the current field by navigating to match `targetString`.
165
-
166
- - `exact` (default `true`): require an exact string match.
167
- - `searchInCurrentLine` (default `false`): restrict the search to the current row.
168
-
169
- Returns `true` if the target was reached.
170
-
171
- #### `browseFile(targetText: string, exact?: boolean): Promise<boolean>`
172
-
173
- Navigates a file-browse dialog to an entry matching `targetText`. Returns `true` on success.
174
-
175
- ---
176
-
177
- ### Key input
178
-
179
- #### `sendKeyPress(keys: M8KeyName[]): Promise<void>`
180
-
181
- Sends a simultaneous key press + release for the given keys.
182
-
183
- ```ts
184
- await m8.sendKeyPress(['shift', 'play'])
185
- ```
186
-
187
- #### `sendKeyDown(keys: M8KeyName[]): Promise<void>`
188
-
189
- Holds the given keys down without releasing. Pair with `sendKeyUp()`.
190
-
191
- ```ts
192
- await m8.sendKeyDown(['shift'])
193
- await m8.sendKeyPress(['right'])
194
- await m8.sendKeyUp()
195
- ```
196
-
197
- #### `sendKeyUp(): Promise<void>`
198
-
199
- Releases all currently held keys.
200
-
201
- ---
202
-
203
- ### State
204
-
205
- #### `getState(): M8State`
206
-
207
- Returns the in-memory cached state synchronously. Same as reading `m8.state`.
208
-
209
- #### `fetchState(): Promise<M8State>`
210
-
211
- Requests a fresh state snapshot from the host and updates the internal cache.
212
-
213
- ```ts
214
- const state = await m8.fetchState()
215
- ```
216
-
217
- ---
218
-
219
- ### Event subscriptions
220
-
221
- Every `on*` method returns an **unsubscribe function** — call it to stop listening.
222
-
223
- #### `onStateChange(callback: (state: M8State) => void): () => void`
224
-
225
- Fires whenever any part of the device state changes.
226
-
227
- ```ts
228
- const off = m8.onStateChange((state) => {
229
- console.log(state.viewName, state.cursorPos)
230
- })
231
- off() // unsubscribe
232
- ```
233
-
234
- #### `onViewChange(callback: (viewName: string | null, viewTitle: string | null) => void): () => void`
235
-
236
- Fires when the active view changes.
237
-
238
- ```ts
239
- m8.onViewChange((name, title) => {
240
- console.log(`Switched to ${name}: ${title}`)
241
- })
242
- ```
243
-
244
- #### `onCursorMove(callback: (pos: CursorPos | null, rect: CursorRect | null, selectionMode: boolean) => void): () => void`
245
-
246
- Fires when the cursor position or selection mode changes.
247
-
248
- ```ts
249
- m8.onCursorMove((pos, rect, sel) => {
250
- console.log(`Cursor at (${pos?.x}, ${pos?.y}), selecting: ${sel}`)
251
- })
252
- ```
253
-
254
- #### `onTextUpdate(callback: (textUnderCursor: string | null, currentLine: string | null) => void): () => void`
255
-
256
- Fires when the text under the cursor or the full current line changes.
257
-
258
- ```ts
259
- m8.onTextUpdate((text, line) => {
260
- console.log('Under cursor:', text)
261
- console.log('Full line:', line)
262
- })
263
- ```
264
-
265
- #### `onKeyPress(callback: (keys: number) => void): () => void`
266
-
267
- Fires when a physical key event occurs on the device. `keys` is a bitmask of active keys.
268
-
269
- ---
270
-
271
- ### Semantic context on the client
272
-
273
- #### `getSemanticContext(): M8SemanticContext | null`
274
-
275
- Parses `m8.state` into structured field data. Returns `null` when no view is active.
276
-
277
- ```ts
278
- const ctx = m8.getSemanticContext()
279
-
280
- if (ctx?.activeField?.type === 'chainRef') {
281
- const track = ctx.activeField.meta?.trackIndex // 1–8
282
- const chain = ctx.activeField.hexValue // 0–255 or null if empty
283
- const row = ctx.row?.rowIndex
284
- }
285
- ```
286
-
287
- #### `describeContext(): string | null`
288
-
289
- Returns a human-readable one-liner, e.g.:
290
-
291
- - `"Track 3: 0A — Track 3 — Song View row 01"`
292
- - `"Note (N): Note C-4 — Phrase View row 00"`
293
- - `"FX2 Command: KIL — (Kill: stops the playing instrument after XX ticks) — value 03: stops the note after 3 ticks — Phrase View row 01"`
294
-
295
- Returns `null` when not connected or no view is active. It is the text of `describeContextParts()` joined with `' — '`.
296
-
297
- It reads the cursor against its surroundings when that changes the meaning:
298
-
299
- - **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.
300
- - **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.
301
- - **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.
302
- - **Instrument-type commands say who offers them.** `FM3` → `"…; offered by FM Synth instruments"`.
303
- - **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.
304
-
305
- 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.
306
-
307
- #### `describeContextParts(): M8DescriptionPart[] | null`
308
-
309
- The same description as ordered, tagged parts, so you can keep, drop, reorder or style some of them instead of parsing the string:
310
-
311
- ```ts
312
- m8.describeContextParts()
313
- // [{ kind: 'field', text: 'FX2 Command: KIL' },
314
- // { kind: 'fxInfo', text: '(Kill: stops the playing instrument after XX ticks)' },
315
- // { kind: 'meaning', text: 'value 03: stops the note after 3 ticks' },
316
- // { kind: 'location', text: 'Phrase View row 01' }]
317
-
318
- // Your own view label instead of the SDK's location:
319
- const text = m8.describeContextParts()?.filter((p) => p.kind !== 'location').map((p) => p.text).join(' · ')
320
- ```
321
-
322
- 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.
323
-
324
- ---
325
-
326
- ### Lifecycle
327
-
328
- #### `disconnect(): void`
329
-
330
- Closes the host connection and clears all event listeners and internal state.
331
-
332
- ---
333
-
334
- ## Standalone semantic context functions
335
-
336
- These are re-exported from `@yam8d/m8-sdk` for use without an `M8Client` instance.
337
-
338
- ### `getSemanticContext(state: M8State, screen?: M8Screen | null): M8SemanticContext | null`
339
-
340
- Parses an `M8State` snapshot into a semantic context. Returns `null` if `state.viewName` is not set.
341
-
342
- 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.
343
-
344
- ```ts
345
- import { getSemanticContext } from '@yam8d/m8-sdk'
346
-
347
- const ctx = getSemanticContext(state)
348
- if (ctx?.isGridView && ctx.row) {
349
- const note = ctx.row.fields['note']
350
- console.log(note?.rawValue) // e.g. 'C-4'
351
- }
352
- ```
353
-
354
- ### `describeContext(ctx: M8SemanticContext): string`
355
-
356
- Returns a human-readable summary of the context (see examples above).
357
-
358
- ### `describeContextParts(ctx: M8SemanticContext): M8DescriptionPart[]`
359
-
360
- The summary as ordered, tagged parts; `describeContext()` joins their text with `' — '`.
361
-
362
- ### `formatFieldValue(field: M8ParsedField): string`
363
-
364
- Formats a single parsed field as a display string:
365
-
366
- | Type | Example output |
367
- |---|---|
368
- | `note` | `"Note C-4"`, `"Note Off"` |
369
- | `transpose` | `"+3 st"`, `"-12 st"`, `"±0 st"` |
370
- | `velocity` | `"57h (87 / 127 max)"` |
371
- | `fxValue` | `"1Ah (dec 26)"` |
372
- | `ticks` | `"06h (6 ticks)"`, `"00 (skip step)"` |
373
- | `chainRef` / `phraseRef` / `fxCommand` / `volume` | raw value, e.g. `"0A"`, `"KIL"` |
374
- | empty cell | `"(empty)"` |
375
-
376
- ### `lookupFxCommand(cmd: string): M8FxCommandInfo | undefined`
377
-
378
- 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`).
379
-
380
- ```ts
381
- import { lookupFxCommand } from '@yam8d/m8-sdk'
382
-
383
- lookupFxCommand('KIL')
384
- // → { description: 'Kill: stops the playing instrument after XX ticks.',
385
- // category: 'sequencer', relative: false, instrumentTypes: null, slot: null }
386
- lookupFxCommand('FM3')
387
- // → { …, category: 'instrument', relative: true, instrumentTypes: ['fmsynth'], slot: null }
388
- lookupFxCommand('LF3')
389
- // → { …, category: 'modulation', relative: false, instrumentTypes: null, slot: 3 }
390
- ```
391
-
392
- `instrumentTypes` lists the instrument types that offer the command when not every type does (`null` = all, or not an instrument command).
393
-
394
- ### `getInstrumentFxCommands(type: string): M8InstrumentFxList | null`
395
-
396
- 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`.
397
-
398
- ```ts
399
- getInstrumentFxCommands('hypersynth')
400
- // → { type: 'hypersynth', verified: true,
401
- // commands: ['VOL','PIT','FIN','CRD','CVO','SWM','WID','SUB','FIL','CUT','RES','AMP','LIM','PAN','SNC','ERR','DRY','SMX','SDL','SRV'] }
402
- ```
403
-
404
- `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`).
405
-
406
- ### `RELATIVE_FX_RULE: string`
407
-
408
- The manual's rule for relative commands, for tooltips.
409
-
410
- ---
411
-
412
- ## Types reference
413
-
414
- ### `M8State`
415
-
416
- The complete snapshot of the device at a point in time.
417
-
418
- ```ts
419
- interface M8State {
420
- viewName: string | null // active view key, e.g. 'song', 'phrase'
421
- viewTitle: string | null // raw title string from screen, e.g. ' SONG '
422
- minimapKey: string | null // current minimap position key
423
- cursorPos: CursorPos | null // character-grid position of the cursor
424
- cursorRect: CursorRect | null // pixel rectangle of the cursor overlay
425
- selectionMode: boolean // true when a selection is active
426
- highlightColor: RGB | null // cursor highlight colour
427
- titleColor: RGB | null // view title text colour
428
- backgroundColor: RGB | null // screen background colour
429
- textUnderCursor: string | null // characters covered by the cursor rect
430
- currentLine: string | null // full text of the row at cursorPos.y
431
- deviceModel: string | null // e.g. 'Headless', 'Model:02'
432
- fontMode: number | null // 0=standard, 1=bold, 2=large
433
- systemInfo: SystemInfos | null // device-reported system properties
434
- macroRunning: boolean // true while a macro is executing
435
- macroCurrentStep?: number // current macro step index
436
- macroSequenceLength?: number // total steps in the running macro
437
- }
438
- ```
439
-
440
- ### `CursorPos / CursorRect / RGB / SystemInfos`
441
-
442
- ```ts
443
- interface CursorPos { x: number; y: number }
444
- interface CursorRect { x: number; y: number; w: number; h: number }
445
- interface RGB { r: number; g: number; b: number }
446
- interface SystemInfos { [key: string]: string | number | boolean | undefined }
447
- ```
448
-
449
- ### `M8SemanticContext`
450
-
451
- ```ts
452
- interface M8SemanticContext {
453
- viewName: string // e.g. 'song', 'phrase', 'instrumentpool'
454
- viewTitle: string | null // e.g. 'Song View'
455
- viewDescription: string | null // long description from schema
456
- viewId: string | null // item ID parsed from screen title, e.g. 'F2', '03'; null for song/instrumentpool
457
- isUnsaved: boolean // true when the title carries a '*' suffix (unsaved changes)
458
- isGridView: boolean // true for song/chain/phrase/table/groove/scale/instrumentpool
459
- isParameterView: boolean // true for the label/value views (inst, mixer, project, …)
460
- section: string | null // effectsettings: 'MODFX' | 'DELAY' | 'REVERB'
461
- instrumentType: { text: string; key: string | null } | null // inst view + screen, e.g. { text: 'WAVSYNTH', key: 'wavsynth' }
462
- row: M8ParsedRow | null // all columns on the line at cursorPos.y
463
- activeField: M8ActiveField | null // the column currently under the cursor
464
- lineText: string | null // cursor row text, playback marker removed
465
- steps: M8ParsedRow[] | null // phrase view + screen: all 16 steps (index = step)
466
- carriedRepLane: number | null // FX lane (1–3) showing '^^' (REP from previous phrase)
467
- fxLane: M8FxLaneTrace | null // phrase view + screen: what the FX cell means in its lane
468
- relativeFx: Record<string, M8RelativeFxState> | null // phrase view + screen: relative commands in force, with net change since the last trigger
469
- 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
470
- scaleNote: M8ScaleNote | null // scale view: the interval row under the cursor { interval, semitone, enabled, offset }
471
- scale: M8ScaleInfo | null // scale view + screen: the 12 intervals interpreted together
472
- }
473
- ```
474
-
475
- ### `M8DescriptionPart`
476
-
477
- ```ts
478
- interface M8DescriptionPart {
479
- kind: M8DescriptionPartKind
480
- text: string
481
- }
482
- ```
483
-
484
- Parts come in reading order; a kind can appear more than once.
485
-
486
- | Kind | Example |
487
- |---|---|
488
- | `field` | `"FX1 Command: KIL"`, `"Tempo (TEMPO): 120.00 BPM"` |
489
- | `fxInfo` | `"(Kill: stops the playing instrument after XX ticks)"` |
490
- | `fxTarget` | on an FX value: `"for KIL (…)"` |
491
- | `meaning` | `"value 03: stops the note after 3 ticks"`, `"E is detuned by −0.50 st"` |
492
- | `fxLane` | `"REP still active: VOL reaches 09 on this step (+02 per step since step 1)"` |
493
- | `relative` | `"relative: VOL has moved by +0C over 2 steps since …"`, `"triggers the instrument again: resets …"` |
494
- | `optionInfo` | what the selected option does |
495
- | `fieldInfo` | what the field is |
496
- | `fileInfo` | `"directory: [EDIT] opens it"` |
497
- | `instrument` | `"Wavsynth instrument"` |
498
- | `section` | `"Delay"`, `"MOD2"` |
499
- | `scale` | `"Scale: C D E F G A B"`, `"Major (Ionian); detuned: E −0.50 st"` |
500
- | `track` | `"Track 3"` |
501
- | `location` | `"Phrase View row 07"`, `"Mixer View"` |
502
- | `line` | the cursor row's text, when no field on it is recognised |
503
-
504
- ### `M8ParsedRow`
505
-
506
- ```ts
507
- interface M8ParsedRow {
508
- rowIndex: number | null // hex row index from leftmost chars; null if unparseable
509
- rowKey: string // schema key, e.g. 'songRow', 'step', 'chainPos'
510
- rowId: string | null // parameter views: matched schema row ('tempo', 'gain', …)
511
- fields: Record<string, M8ParsedField> // column key → parsed field
512
- }
513
- ```
514
-
515
- ### `M8ActiveField / M8ParsedField`
516
-
517
- `M8ActiveField` extends `M8ParsedField` with two extra convenience fields.
518
-
519
- ```ts
520
- interface M8ParsedField {
521
- key: string // schema key, e.g. 'fx1cmd', 'track3', 'note'
522
- label: string // human label, e.g. 'FX1 Command', 'Track 3'
523
- type: M8FieldType
524
- rawValue: string // trimmed text sliced from currentLine
525
- hexValue: number | null // parsed integer; null for empty cells and text types
526
- isEmpty: boolean // true when '--', '---', '---00', etc.
527
- meta?: Record<string, unknown> // view-specific extras (e.g. { trackIndex: 3 })
528
- // parameter views (all optional):
529
- description?: string // what the field is
530
- text?: string | null // non-numeric part: option name, caption, name, instrument type
531
- numericValue?: number | null // value in its natural base (hex, decimal, fractional)
532
- values?: number[] | null // both numbers of an 'XX:YY' pair
533
- parts?: string[] // names of those numbers, e.g. ['left time', 'right time']
534
- unit?: string // e.g. ' BPM', ' dB'
535
- valueDescription?: string // what the selected option does
536
- fileKind?: M8FileKind // file browser rows: 'parent' | 'directory' | 'instrumentPreset' | 'sample' | 'm8file' | 'file'
537
- }
538
-
539
- interface M8ActiveField extends M8ParsedField {
540
- viewName: string // repeated for convenience
541
- rowIndex: number | null // row index of the line containing this field
542
- }
543
- ```
544
-
545
- ### `M8FieldType`
546
-
547
- ```ts
548
- type M8FieldType =
549
- | 'chainRef' // chain number hex 00–FF; '--' = empty
550
- | 'phraseRef' // phrase number hex 00–FF; '--' = empty
551
- | 'note' // note name e.g. 'C-4', 'A#3'; '---' = empty; 'OFF' = note-off
552
- | 'velocity' // velocity 00–7F hex; '--' = inherit
553
- | 'instrumentRef' // instrument slot 00–7F hex; '--' = keep current
554
- | 'fxCommand' // 3-char command name e.g. 'KIL'; '---' = none
555
- | 'fxValue' // FX parameter 00–FF hex
556
- | 'transpose' // relative semitones: 00=none, 01–7F=+1–+127, FF–80=−1–−128
557
- | 'ticks' // groove ticks per step hex; '00'=skip; '--'=end of groove
558
- | 'volume' // level 00–FF hex; '--' = no override
559
- | 'ppq' // pulses-per-quarter-note decimal; groove row 0 only
560
- | 'instrumentName' // instrument name text ≤12 chars; no hexValue
561
- | 'eqSlot' // EQ slot assignment hex; '--' = none
562
- | 'rowIndex' // row/step index hex
563
- | 'noteInterval' // scale view row label: C, C#, D, …
564
- | 'onOff' // 'ON' / 'OFF' toggle
565
- | 'semitoneOffset' // scale view detune, e.g. '-00.50'
566
- | 'swing' // groove swing percentage
567
- | 'instrumentType' // instrument engine: WAVSYNTH, SAMPLER, FMSYNTH, …
568
- | 'name' // free-text name field; dashes = unnamed
569
- | 'parameter' // named editable value: CUTOFF, TEMPO, GAIN, …
570
- | 'option' // choice from a list: '00CHORUS', 'BELL', 'STEREO'
571
- | 'action' // button: LOAD, SAVE, RENDER, SETTINGS, EQ, …
572
- | 'fileEntry' // file browser row: '/folder', '/..', 'Bass.m8i'
573
- ```
574
-
575
- ### `M8KeyName`
576
-
577
- ```ts
578
- type M8KeyName = 'left' | 'right' | 'up' | 'down' | 'shift' | 'play' | 'opt' | 'edit'
579
- ```
580
-
581
- ### `M8SdkConfig`
582
-
583
- ```ts
584
- interface M8SdkConfig {
585
- debug?: boolean // enables verbose postMessage logging via DebugMessenger
586
- }
587
- ```
588
-
589
- ---
590
-
591
- ## Host events
592
-
593
- 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.
594
-
595
- | Event | Payload | Fired when |
596
- |---|---|---|
597
- | `stateChanged` | `M8State` | Any state field changes |
598
- | `viewChanged` | `{ viewName, viewTitle }` | The active view changes |
599
- | `cursorMoved` | `{ pos, rect, selectionMode }` | Cursor position or selection mode changes |
600
- | `textUpdated` | `{ textUnderCursor, currentLine }` | Text under cursor or current line changes |
601
- | `keyPressed` | `{ keys: number }` | A physical key event fires on the device |
602
-
603
- ---
604
-
605
- ## Supported views
606
-
607
- The semantic context parser supports these `viewName` values:
608
-
609
- | `viewName` | Columns |
610
- |---|---|
611
- | `song` | `track1`–`track8` (chainRef, meta: trackIndex 1–8) |
612
- | `chain` | `phrase` (phraseRef), `transpose` |
613
- | `phrase` | `note`, `vel`, `inst`, `fx1cmd`, `fx1val`, `fx2cmd`, `fx2val`, `fx3cmd`, `fx3val` |
614
- | `table` | `transpose`, `volume`, `fx1cmd`, `fx1val`, `fx2cmd`, `fx2val`, `fx3cmd`, `fx3val` |
615
- | `groove` | `ticks`, `ppq` (row 0 only), `swing` (row 0 only) |
616
- | `scale` | `interval`, `en` (onOff), `offset` (semitoneOffset) |
617
- | `instrumentpool` | `name`, `dry`, `mx`, `de`, `rv`, `eq` |
618
- | `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 |
619
- | `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`) |
620
- | `mixer` | `speakerVol`, `track1`–`track8`, `modfxVol`/`delayVol`/`reverbVol`, `inputVol`/`inputVol2`/`usbVol` and their sends, `mixEq` (action), `mainVol`, `limiter`, `djFilter`, `ott` |
621
- | `project` | `tempo`, `transpose`, `groove`/`scale`/`liveQuantize` (option), `name`, and the `load`/`save`/`new`/`render`/`bundle`/… actions |
622
- | `effectsettings` | per `section` (MODFX / DELAY / REVERB): type, input EQ (action), depth/frequency pairs, width, sends, delay time, feedback, room size, decay/shimmer |
623
- | `mixeq`, `modfxeq`, `delayeq`, `reverbeq` | LOW/MID/HIGH band `gain`, `freq`, `q`, `type` (option), `mode` (option) |
624
- | `mixscope`, `limiterscope` | `zoom`, `peak`, `softClip`, `mainVol`, `limiter`, `djFilter`, `ott`, `mixEq`; `atk`/`rel`/`type`/`res`/`time`/`color` as label/value pairs |
625
- | `systemsettings` | `backlight`, `fontOptions`, `editTheme` (action), `notePreview`, `recCountIn`, `metronomeVol`, `usbAudioMode`, `usbMainOut`, `lineInGate`, `keyDelayRep`, … |
626
- | `loadinstrument`, `selectsavedirectory` | `entry` (fileEntry) |
627
- | `createdirectory` | `name` |
628
-
629
- 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.
630
-
631
- ---
632
-
633
- ## Notes
634
-
635
- - **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.
636
- - **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.
637
- - **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.
638
- - **Schema file** — View and column definitions live in `src/m8-view-context.json` and are bundled into `dist/index.js` at build time.
639
- - **Requirement** — The app must run inside the yam8d host iframe. The `ChildHandshake` from `post-me` will never resolve in a standalone tab.
640
-
641
-
642
- Client SDK for iframe applications that communicate with an M8 host.
643
-
644
- ## Install
645
-
646
- ```bash
647
- npm install @yam8d/m8-sdk
648
- ```
649
-
650
- For local development before the package is published:
651
-
652
- ```bash
653
- npm install ../yam8d/packages/m8-sdk
654
- ```
655
-
656
- ## Usage
657
-
658
- ```ts
659
- import { createM8Client } from '@yam8d/m8-sdk'
660
-
661
- const m8 = await createM8Client()
662
-
663
- console.log(m8.state.viewName)
664
- await m8.sendKeyPress(['play'])
665
-
666
- const unsubscribe = m8.onStateChange((state) => {
667
- console.log(state.cursorPos)
668
- })
669
-
670
- unsubscribe()
671
- m8.disconnect()
672
- ```
673
-
674
- For React or other environments where top-level `await` is not convenient:
675
-
676
- ```ts
677
- import { createM8ClientSync } from '@yam8d/m8-sdk'
678
-
679
- const { client, connect } = createM8ClientSync()
680
- await connect()
681
- ```
682
-
683
- The app must run inside the host iframe. Opening it directly in a standalone browser tab cannot establish the SDK handshake.
684
-
685
- ---
686
-
687
- ## Semantic Context
688
-
689
- 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.
690
-
691
- ### Via the client
692
-
693
- ```ts
694
- const ctx = m8.getSemanticContext()
695
- // or as a human-readable string:
696
- const description = m8.describeContext()
697
- // e.g. "Track 3: 0A — Track 3 — Song View row 01"
698
- // or as tagged parts to build your own:
699
- const parts = m8.describeContextParts()
700
- ```
701
-
702
- ### Standalone functions
703
-
704
- ```ts
705
- import { getSemanticContext, describeContext, formatFieldValue, lookupFxCommand } from '@yam8d/m8-sdk'
706
-
707
- const ctx = getSemanticContext(state)
708
-
709
- if (ctx?.activeField) {
710
- const field = ctx.activeField
711
-
712
- console.log(field.key) // e.g. 'track3', 'note', 'fx1cmd'
713
- console.log(field.label) // e.g. 'Track 3', 'Note (N)', 'FX1 Command'
714
- console.log(field.type) // e.g. 'chainRef', 'note', 'fxCommand'
715
- console.log(field.rawValue) // raw text from the line, e.g. '0A', 'C-4', 'KIL'
716
- console.log(field.hexValue) // parsed integer or null (null for empty cells and text types)
717
- console.log(field.isEmpty) // true when '--' / '---' / '---00'
718
-
719
- // Song view: which track is the cursor on?
720
- if (field.type === 'chainRef') {
721
- console.log(field.meta?.trackIndex) // 1–8
722
- }
723
-
724
- // FX command: look up its description
725
- if (field.type === 'fxCommand' && !field.isEmpty) {
726
- const info = lookupFxCommand(field.rawValue)
727
- console.log(info?.description)
728
- }
729
- }
730
-
731
- // Row-level fields (all columns on the current line)
732
- if (ctx?.row) {
733
- console.log(ctx.row.rowIndex) // hex-parsed row index, e.g. 2
734
- console.log(ctx.row.fields['note']) // M8ParsedField for the note column
735
- }
736
- ```
737
-
738
- ### Returned types
739
-
740
- ```ts
741
- interface M8SemanticContext {
742
- viewName: string // e.g. 'song', 'phrase'
743
- viewTitle: string | null // e.g. 'Song View'
744
- viewDescription: string | null
745
- isGridView: boolean
746
- row: M8ParsedRow | null // all columns on the cursor's line
747
- activeField: M8ActiveField | null // the column under the cursor
748
- }
749
-
750
- interface M8ParsedField {
751
- key: string // schema key, e.g. 'fx1cmd', 'track3'
752
- label: string // human label, e.g. 'FX1 Command', 'Track 3'
753
- type: M8FieldType
754
- rawValue: string // trimmed text from currentLine
755
- hexValue: number | null
756
- isEmpty: boolean
757
- meta?: Record<string, unknown>
758
- }
759
- ```
760
-
761
- ### Supported field types (`M8FieldType`)
762
-
763
- | Type | Description |
764
- |---|---|
765
- | `chainRef` | Chain number (hex). `--` = empty. |
766
- | `phraseRef` | Phrase number (hex). `--` = empty. |
767
- | `note` | Note name, e.g. `C-4`, `A#3`. `---` = empty. |
768
- | `velocity` | Note velocity 00–7F (hex). |
769
- | `instrumentRef` | Instrument slot 00–7F (hex). `--` = keep current. |
770
- | `fxCommand` | 3-char FX command name, e.g. `KIL`, `ARP`. `---` = none. |
771
- | `fxValue` | FX value 00–FF (hex). |
772
- | `transpose` | Relative transpose (hex): `00`=none, `01`–`7F`=+1–+127, `FF`–`80`=−1–−128. |
773
- | `ticks` | Groove ticks per step (hex). |
774
- | `volume` | Volume level 00–FF (hex). `--` = no override. |
775
- | `ppq` | Pulses Per Quarter note (decimal). Groove row 0 only. |
776
- | `instrumentName` | Instrument name text (up to 12 chars). No `hexValue`. |
777
- | `eqSlot` | EQ slot assignment (hex). `--` = none. |
778
- | `rowIndex` | Row/step index (hex). |
779
- | `noteInterval` | Scale View row label (`C`, `C#`, …). |
780
- | `onOff` | `ON` / `OFF` toggle. |
781
- | `semitoneOffset` | Scale View detune, e.g. `-00.50`. |
782
- | `swing` | Groove swing percentage. |
783
- | `instrumentType` | Instrument engine (`WAVSYNTH`, `SAMPLER`, …); `text` holds the name. |
784
- | `name` | Free-text name field; a row of dashes = unnamed. |
785
- | `parameter` | Named editable value (`CUTOFF`, `TEMPO`, `GAIN`); read `hexValue` / `numericValue` / `values`. |
786
- | `option` | Choice from a list (`00CHORUS`, `BELL`); `hexValue` is the index when shown, `text` the name. |
787
- | `action` | Button caption (`LOAD`, `SAVE`, `RENDER`, `SETTINGS`); `text` holds it. |
788
- | `fileEntry` | File browser row: `/folder`, `/..`, or a file name. |
789
-
790
- ### Playback indicator
791
-
792
- 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.
793
-
794
- ### Notes file
795
-
796
- 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, for apps that want to drive the device below the client:
133
+ - `createM8Connection` (open a device bus) and `createM8Engine` (the engine yam8d and standalone clients run on a bus);
134
+ - `M8KeyMask` (the bit of each key in a key mask) and `pressKeys({ left, up, shift, ... })`, which builds a mask from booleans;
135
+ - `createM8Audio()` (plays the M8's USB audio input through the page: `connect()`, `disconnect()`, `connected`, `getOutputCaptureStream()` for recording), `getM8InputStream({ requireM8 })` (the raw input stream; without `requireM8` it falls back to the default input) and `M8_AUDIO_CAPTURE_CONSTRAINTS` (no echo cancellation, noise suppression or gain control).
136
+
137
+ ---
138
+
139
+ ## M8Client
140
+
141
+ ### Properties
142
+
143
+ | Property | Type | Description |
144
+ |---|---|---|
145
+ | `state` | `M8State` | Last known full device state (updated on every host event). |
146
+ | `isConnected` | `boolean` | Whether calls reach an M8: host mode, after the yam8d handshake; standalone, while a device is connected. |
147
+ | `mode` | `'host' \| 'standalone' \| null` | Chosen at connect; `null` before. |
148
+ | `deviceTransport` | `'serial' \| 'usb' \| 'midi' \| null` | Standalone: the API the device is connected through. |
149
+
150
+ ---
151
+
152
+ ### Navigation
153
+
154
+ #### `navigateToView(viewName: string): Promise<boolean>`
155
+
156
+ Asks the host to navigate to a named view (e.g. `'song'`, `'phrase'`). Returns `true` on success.
157
+
158
+ ```ts
159
+ await m8.navigateToView('phrase')
160
+ ```
161
+
162
+ #### `navigateTo(x: number, y: number): Promise<void>`
163
+
164
+ Moves the cursor to the given character-grid position.
165
+
166
+ ```ts
167
+ await m8.navigateTo(3, 5)
168
+ ```
169
+
170
+ ---
171
+
172
+ ### Value setters
173
+
174
+ All setters act on the field currently under the cursor.
175
+
176
+ #### `setValueToHex(targetHex: number): Promise<boolean>`
177
+
178
+ Sets the current field to the given hex integer (e.g. `0x0A`). Returns `true` if the value was successfully applied.
179
+
180
+ #### `setValueToInt(targetInt: number): Promise<boolean>`
181
+
182
+ Sets the current field to the given decimal integer. Returns `true` on success.
183
+
184
+ #### `setValueFloat(targetFloat: number): Promise<boolean>`
185
+
186
+ Sets a float parameter parsed from the whole line, e.g. `TUNE 440.00 G` or `GAIN 05.25 -19.75 -15.00`
187
+ (multiple floats per line are supported; the cursor's highlight selects which one is edited).
188
+ Works with the cursor on a single digit group (`440` / `00`) or spanning the whole float (`19.75`,
189
+ with the sign usually excluded from the highlight). With whole-float focus, edit+up/down moves the
190
+ integer part and edit+left/right the decimal part; with group focus the SDK relocates the cursor
191
+ between the groups for the fastest path. Negative values are supported and step sizes are measured
192
+ per field from the device.
193
+ Returns `true` when the displayed value matches.
194
+
195
+ #### `setNote(noteString: string): Promise<boolean>`
196
+
197
+ Sets a note field using M8 note format, e.g. `'C-4'`, `'A#3'`, `'OFF'`. Returns `true` on success.
198
+
199
+ #### `setValueToString(targetString: string, exact?: boolean, searchInCurrentLine?: boolean): Promise<boolean>`
200
+
201
+ Sets the current field by navigating to match `targetString`.
202
+
203
+ - `exact` (default `true`): require an exact string match.
204
+ - `searchInCurrentLine` (default `false`): restrict the search to the current row.
205
+
206
+ Returns `true` if the target was reached.
207
+
208
+ #### `browseFile(targetText: string, exact?: boolean): Promise<boolean>`
209
+
210
+ Navigates a file-browse dialog to an entry matching `targetText`. Returns `true` on success.
211
+
212
+ ---
213
+
214
+ ### Key input
215
+
216
+ #### `sendKeyPress(keys: M8KeyName[]): Promise<void>`
217
+
218
+ Sends a simultaneous key press + release for the given keys.
219
+
220
+ ```ts
221
+ await m8.sendKeyPress(['shift', 'play'])
222
+ ```
223
+
224
+ #### `sendKeyDown(keys: M8KeyName[]): Promise<void>`
225
+
226
+ Holds the given keys down without releasing. Pair with `sendKeyUp()`.
227
+
228
+ ```ts
229
+ await m8.sendKeyDown(['shift'])
230
+ await m8.sendKeyPress(['right'])
231
+ await m8.sendKeyUp()
232
+ ```
233
+
234
+ #### `sendKeyUp(): Promise<void>`
235
+
236
+ Releases all currently held keys.
237
+
238
+ ---
239
+
240
+ ### State
241
+
242
+ #### `getState(): M8State`
243
+
244
+ Returns the in-memory cached state synchronously. Same as reading `m8.state`.
245
+
246
+ #### `fetchState(): Promise<M8State>`
247
+
248
+ Requests a fresh state snapshot from the host and updates the internal cache.
249
+
250
+ ```ts
251
+ const state = await m8.fetchState()
252
+ ```
253
+
254
+ #### `getScreen(): Promise<M8Screen>`
255
+
256
+ 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.
257
+
258
+ #### `getScreenCells(): Promise<M8ScreenCells>`
259
+
260
+ 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.
261
+
262
+ ```ts
263
+ const cells = await m8.getScreenCells()
264
+ cells.fg[5][3] // colour of the first note of a phrase, e.g. '#ffffff'
265
+ ```
266
+
267
+ #### `captureSnapshot(): Promise<M8Snapshot>`
268
+
269
+ A fresh state, its semantic context, the description (as text and as parts) and the whole screen in one call. The result is plain JSON, safe to store.
270
+
271
+ ```ts
272
+ const snap = await m8.captureSnapshot()
273
+ snap.description // e.g. "Note (N): Note C-4 — Phrase View row 00"
274
+ snap.screen.lines // the screen it was read from
275
+ ```
276
+
277
+ ---
278
+
279
+ ### Event subscriptions
280
+
281
+ Every `on*` method returns an **unsubscribe function** — call it to stop listening.
282
+
283
+ #### `onStateChange(callback: (state: M8State) => void): () => void`
284
+
285
+ Fires whenever any part of the device state changes.
286
+
287
+ ```ts
288
+ const off = m8.onStateChange((state) => {
289
+ console.log(state.viewName, state.cursorPos)
290
+ })
291
+ off() // unsubscribe
292
+ ```
293
+
294
+ #### `onViewChange(callback: (viewName: string | null, viewTitle: string | null) => void): () => void`
295
+
296
+ Fires when the active view changes.
297
+
298
+ ```ts
299
+ m8.onViewChange((name, title) => {
300
+ console.log(`Switched to ${name}: ${title}`)
301
+ })
302
+ ```
303
+
304
+ #### `onCursorMove(callback: (pos: CursorPos | null, rect: CursorRect | null, selectionMode: boolean) => void): () => void`
305
+
306
+ Fires when the cursor position or selection mode changes.
307
+
308
+ ```ts
309
+ m8.onCursorMove((pos, rect, sel) => {
310
+ console.log(`Cursor at (${pos?.x}, ${pos?.y}), selecting: ${sel}`)
311
+ })
312
+ ```
313
+
314
+ #### `onTextUpdate(callback: (textUnderCursor: string | null, currentLine: string | null) => void): () => void`
315
+
316
+ Fires when the text under the cursor or the full current line changes.
317
+
318
+ ```ts
319
+ m8.onTextUpdate((text, line) => {
320
+ console.log('Under cursor:', text)
321
+ console.log('Full line:', line)
322
+ })
323
+ ```
324
+
325
+ #### `onKeyPress(callback: (keys: number) => void): () => void`
326
+
327
+ Fires when a physical key event occurs on the device. `keys` is a bitmask of active keys (see `M8KeyMask`).
328
+
329
+ #### `enableMidiTransport(): Promise<{ ok: boolean; error?: string }>`
330
+
331
+ Starts forwarding the M8's MIDI clock and transport. The M8 only sends it with SYNC OUT set in its MIDI Settings (a setting stored in each song), and the page that owns the device needs Web MIDI permission: yam8d in host mode, the app itself in standalone mode. Hosts before SDK 0.5 reject the call. In standalone mode, call it again after each new device connection.
332
+
333
+ #### `onTransport(callback: (event: M8TransportEvent) => void): () => void`
334
+
335
+ MIDI start / stop / continue, the clock, and song positions, after `enableMidiTransport()`.
336
+
337
+ ```ts
338
+ interface M8TransportEvent {
339
+ kind: 'start' | 'stop' | 'continue' | 'clock' | 'position'
340
+ ticks: number // clock ticks since the host started listening (monotonic)
341
+ at: number // epoch ms (performance.timeOrigin + timestamp), comparable across frames
342
+ msPerTick: number | null // measured tick length, null until known
343
+ spp?: number // Song Position Pointer in sixteenths, on 'position' events
344
+ }
345
+ ```
346
+
347
+ `clock` events come once per sixteenth (6 ticks at 24 PPQ), not on every tick. The M8 can send clock while stopped.
348
+
349
+ ```ts
350
+ const { ok, error } = await m8.enableMidiTransport()
351
+ if (!ok) console.warn(error)
352
+ m8.onTransport((e) => {
353
+ if (e.kind === 'position') console.log('play from sixteenth', e.spp)
354
+ })
355
+ ```
356
+
357
+ ---
358
+
359
+ ### Semantic context on the client
360
+
361
+ #### `getSemanticContext(): M8SemanticContext | null`
362
+
363
+ Parses `m8.state` into structured field data. Returns `null` when no view is active.
364
+
365
+ ```ts
366
+ const ctx = m8.getSemanticContext()
367
+
368
+ if (ctx?.activeField?.type === 'chainRef') {
369
+ const track = ctx.activeField.meta?.trackIndex // 1–8
370
+ const chain = ctx.activeField.hexValue // 0–255 or null if empty
371
+ const row = ctx.row?.rowIndex
372
+ }
373
+ ```
374
+
375
+ #### `describeContext(): string | null`
376
+
377
+ Returns a human-readable one-liner, e.g.:
378
+
379
+ - `"Track 3: 0A — Track 3 — Song View row 01"`
380
+ - `"Note (N): Note C-4 — Phrase View row 00"`
381
+ - `"FX2 Command: KIL — (Kill: stops the playing instrument after XX ticks) — value 03: stops the note after 3 ticks — Phrase View row 01"`
382
+
383
+ Returns `null` when not connected or no view is active. It is the text of `describeContextParts()` joined with `' — '`.
384
+
385
+ It reads the cursor against its surroundings when that changes the meaning:
386
+
387
+ - **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.
388
+ - **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.
389
+ - **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.
390
+ - **Instrument-type commands say who offers them.** `FM3` → `"…; offered by FM Synth instruments"`.
391
+ - **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.
392
+
393
+ 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.
394
+
395
+ #### `describeContextParts(): M8DescriptionPart[] | null`
396
+
397
+ The same description as ordered, tagged parts, so you can keep, drop, reorder or style some of them instead of parsing the string:
398
+
399
+ ```ts
400
+ m8.describeContextParts()
401
+ // [{ kind: 'field', text: 'FX2 Command: KIL' },
402
+ // { kind: 'fxInfo', text: '(Kill: stops the playing instrument after XX ticks)' },
403
+ // { kind: 'meaning', text: 'value 03: stops the note after 3 ticks' },
404
+ // { kind: 'location', text: 'Phrase View row 01' }]
405
+
406
+ // Your own view label instead of the SDK's location:
407
+ const text = m8.describeContextParts()?.filter((p) => p.kind !== 'location').map((p) => p.text).join(' · ')
408
+ ```
409
+
410
+ 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.
411
+
412
+ ---
413
+
414
+ ### Device connection (standalone)
415
+
416
+ #### `connectDevice(options?: { transport?: 'auto' | 'serial' | 'usb' | 'midi'; audio?: boolean }): Promise<void>`
417
+
418
+ 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.
419
+
420
+ #### `disconnectDevice(): Promise<void>`
421
+
422
+ Releases the device (and stops its audio) so yam8d or another page can open it.
423
+
424
+ #### `connectAudio(): Promise<void>` / `disconnectAudio(): Promise<void>`
425
+
426
+ 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.
427
+
428
+ #### `onConnectionChange(callback: (connected: boolean) => void): () => void`
429
+
430
+ Fires when `isConnected` changes: handshake done, device connected, unplugged or released.
431
+
432
+ #### `getDevice(): M8DeviceBus | null`
433
+
434
+ Standalone: raw protocol events and device commands. `null` in host mode or while disconnected.
435
+
436
+ ---
437
+
438
+ ### Lifecycle
439
+
440
+ #### `disconnect(): void`
441
+
442
+ Closes the host connection (and the device, in standalone mode) and clears all event listeners and internal state.
443
+
444
+ ---
445
+
446
+ ## Standalone semantic context functions
447
+
448
+ These are re-exported from `@yam8d/m8-sdk` for use without an `M8Client` instance.
449
+
450
+ ### `getSemanticContext(state: M8State, screen?: M8Screen | null): M8SemanticContext | null`
451
+
452
+ Parses an `M8State` snapshot into a semantic context. Returns `null` if `state.viewName` is not set.
453
+
454
+ 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.
455
+
456
+ ```ts
457
+ import { getSemanticContext } from '@yam8d/m8-sdk'
458
+
459
+ const ctx = getSemanticContext(state)
460
+ if (ctx?.isGridView && ctx.row) {
461
+ const note = ctx.row.fields['note']
462
+ console.log(note?.rawValue) // e.g. 'C-4'
463
+ }
464
+ ```
465
+
466
+ ### `describeContext(ctx: M8SemanticContext): string`
467
+
468
+ Returns a human-readable summary of the context (see examples above).
469
+
470
+ ### `describeContextParts(ctx: M8SemanticContext): M8DescriptionPart[]`
471
+
472
+ The summary as ordered, tagged parts; `describeContext()` joins their text with `' — '`.
473
+
474
+ ### `formatFieldValue(field: M8ParsedField): string`
475
+
476
+ Formats a single parsed field as a display string:
477
+
478
+ | Type | Example output |
479
+ |---|---|
480
+ | `note` | `"Note C-4"`, `"Note Off"` |
481
+ | `transpose` | `"+3 st"`, `"-12 st"`, `"±0 st"` |
482
+ | `velocity` | `"57h (87 / 127 max)"` |
483
+ | `fxValue` | `"1Ah (dec 26)"` |
484
+ | `ticks` | `"06h (6 ticks)"`, `"00 (skip step)"` |
485
+ | `chainRef` / `phraseRef` / `fxCommand` / `volume` | raw value, e.g. `"0A"`, `"KIL"` |
486
+ | empty cell | `"(empty)"` |
487
+
488
+ ### `lookupFxCommand(cmd: string): M8FxCommandInfo | undefined`
489
+
490
+ 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`).
491
+
492
+ ```ts
493
+ import { lookupFxCommand } from '@yam8d/m8-sdk'
494
+
495
+ lookupFxCommand('KIL')
496
+ // → { description: 'Kill: stops the playing instrument after XX ticks.',
497
+ // category: 'sequencer', relative: false, instrumentTypes: null, slot: null }
498
+ lookupFxCommand('FM3')
499
+ // → { …, category: 'instrument', relative: true, instrumentTypes: ['fmsynth'], slot: null }
500
+ lookupFxCommand('LF3')
501
+ // → { …, category: 'modulation', relative: false, instrumentTypes: null, slot: 3 }
502
+ ```
503
+
504
+ `instrumentTypes` lists the instrument types that offer the command when not every type does (`null` = all, or not an instrument command).
505
+
506
+ ### `getInstrumentFxCommands(type: string): M8InstrumentFxList | null`
507
+
508
+ 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`.
509
+
510
+ ```ts
511
+ getInstrumentFxCommands('hypersynth')
512
+ // → { type: 'hypersynth', verified: true,
513
+ // commands: ['VOL','PIT','FIN','CRD','CVO','SWM','WID','SUB','FIL','CUT','RES','AMP','LIM','PAN','SNC','ERR','DRY','SMX','SDL','SRV'] }
514
+ ```
515
+
516
+ `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`).
517
+
518
+ ### `RELATIVE_FX_RULE: string`
519
+
520
+ The manual's rule for relative commands, for tooltips.
521
+
522
+ ### `parseGridScreen(viewName: string, screen: M8Screen): M8ScreenGridRow[]`
523
+
524
+ 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.
525
+
526
+ ```ts
527
+ const steps = parseGridScreen('phrase', await m8.getScreen())
528
+ steps[0].fields.note.rawValue // 'c-4' (the text stream is lower case)
529
+ ```
530
+
531
+ ### `parseViewTitle(rawTitle: string | null): { viewId: string | null; isUnsaved: boolean }`
532
+
533
+ The item number of a title (`'PHRASE 1F*'` → `{ viewId: '1F', isUnsaved: true }`).
534
+
535
+ ### `readPlayPositions(viewName: string, screen: M8Screen): M8PlayPositions | null`
536
+
537
+ Reads the play markers off a grid view. On the Song view the M8 draws a marker just left of each playing chain (`00>00>01>02`), so `tracks` holds the song row of each track's marker (`null` where it is not on screen). On the chain, phrase, table and groove views the marker follows the row index of the playing row (`F>--- -- --`), so `row` holds that row. Returns `null` for views without a grid, and all nulls when nothing plays. Seen on a Model:02, firmware 6.5.
538
+
539
+ ```ts
540
+ interface M8PlayPositions {
541
+ tracks: (number | null)[] | null // song view only
542
+ row: number | null // other grid views
543
+ }
544
+ ```
545
+
546
+ ### `SCREEN_MARGIN: number`
547
+
548
+ The one-column left margin of every screen row (`1`). `screen.lines[y].slice(SCREEN_MARGIN)` lines columns up with the schema's `x` positions.
549
+
550
+ ### Theme: `parseThemeScreen`, `observeThemeColors`, `M8_THEME_ROLES`, `M8_THEME_VIEW`
551
+
552
+ The device theme has 13 colours, edited in System Settings › EDIT THEME (view name `themesettings`, exported as `M8_THEME_VIEW`).
553
+
554
+ - `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.
555
+ - `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.)
556
+ - `M8_THEME_ROLES` lists the roles with the labels the device shows.
557
+
558
+ ```ts
559
+ if (m8.state.viewName === M8_THEME_VIEW) {
560
+ const theme = parseThemeScreen(await m8.getScreen())
561
+ }
562
+ ```
563
+
564
+ ---
565
+
566
+ ## Types reference
567
+
568
+ ### `M8State`
569
+
570
+ The complete snapshot of the device at a point in time.
571
+
572
+ ```ts
573
+ interface M8State {
574
+ viewName: string | null // active view key, e.g. 'song', 'phrase'
575
+ viewTitle: string | null // raw title string from screen, e.g. ' SONG '
576
+ minimapKey: string | null // current minimap position key
577
+ cursorPos: CursorPos | null // character-grid position of the cursor
578
+ cursorRect: CursorRect | null // pixel rectangle of the cursor overlay
579
+ selectionMode: boolean // true when a selection is active
580
+ highlightColor: RGB | null // cursor highlight colour
581
+ titleColor: RGB | null // view title text colour
582
+ backgroundColor: RGB | null // screen background colour
583
+ cursorColor?: RGB | null // cursor rectangle as drawn (animated, not the theme CURSOR); SDK 0.5+
584
+ textUnderCursor: string | null // characters covered by the cursor rect
585
+ currentLine: string | null // full text of the row at cursorPos.y
586
+ deviceModel: string | null // e.g. 'Headless', 'Model:02'
587
+ fontMode: number | null // 0=standard, 1=bold, 2=large
588
+ systemInfo: SystemInfos | null // device-reported system properties
589
+ macroRunning: boolean // true while a macro is executing
590
+ macroCurrentStep?: number // current macro step index
591
+ macroSequenceLength?: number // total steps in the running macro
592
+ }
593
+ ```
594
+
595
+ ### `CursorPos / CursorRect / RGB / SystemInfos`
596
+
597
+ ```ts
598
+ interface CursorPos { x: number; y: number }
599
+ interface CursorRect { x: number; y: number; w: number; h: number }
600
+ interface RGB { r: number; g: number; b: number }
601
+ interface SystemInfos { [key: string]: string | number | boolean | undefined }
602
+ ```
603
+
604
+ ### `M8SemanticContext`
605
+
606
+ ```ts
607
+ interface M8SemanticContext {
608
+ viewName: string // e.g. 'song', 'phrase', 'instrumentpool'
609
+ viewTitle: string | null // e.g. 'Song View'
610
+ viewDescription: string | null // long description from schema
611
+ viewId: string | null // item ID parsed from screen title, e.g. 'F2', '03'; null for song/instrumentpool
612
+ isUnsaved: boolean // true when the title carries a '*' suffix (unsaved changes)
613
+ isGridView: boolean // true for song/chain/phrase/table/groove/scale/instrumentpool
614
+ isParameterView: boolean // true for the label/value views (inst, mixer, project, …)
615
+ section: string | null // effectsettings: 'MODFX' | 'DELAY' | 'REVERB'
616
+ instrumentType: { text: string; key: string | null } | null // inst view + screen, e.g. { text: 'WAVSYNTH', key: 'wavsynth' }
617
+ row: M8ParsedRow | null // all columns on the line at cursorPos.y
618
+ activeField: M8ActiveField | null // the column currently under the cursor
619
+ lineText: string | null // cursor row text, playback marker removed
620
+ steps: M8ParsedRow[] | null // phrase view + screen: all 16 steps (index = step)
621
+ carriedRepLane: number | null // FX lane (1–3) showing '^^' (REP from previous phrase)
622
+ fxLane: M8FxLaneTrace | null // phrase view + screen: what the FX cell means in its lane
623
+ relativeFx: Record<string, M8RelativeFxState> | null // phrase view + screen: relative commands in force, with net change since the last trigger
624
+ 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
625
+ scaleNote: M8ScaleNote | null // scale view: the interval row under the cursor { interval, semitone, enabled, offset }
626
+ scale: M8ScaleInfo | null // scale view + screen: the 12 intervals interpreted together
627
+ }
628
+ ```
629
+
630
+ ### `M8DescriptionPart`
631
+
632
+ ```ts
633
+ interface M8DescriptionPart {
634
+ kind: M8DescriptionPartKind
635
+ text: string
636
+ }
637
+ ```
638
+
639
+ Parts come in reading order; a kind can appear more than once.
640
+
641
+ | Kind | Example |
642
+ |---|---|
643
+ | `field` | `"FX1 Command: KIL"`, `"Tempo (TEMPO): 120.00 BPM"` |
644
+ | `fxInfo` | `"(Kill: stops the playing instrument after XX ticks)"` |
645
+ | `fxTarget` | on an FX value: `"for KIL (…)"` |
646
+ | `meaning` | `"value 03: stops the note after 3 ticks"`, `"E is detuned by −0.50 st"` |
647
+ | `fxLane` | `"REP still active: VOL reaches 09 on this step (+02 per step since step 1)"` |
648
+ | `relative` | `"relative: VOL has moved by +0C over 2 steps since …"`, `"triggers the instrument again: resets …"` |
649
+ | `optionInfo` | what the selected option does |
650
+ | `fieldInfo` | what the field is |
651
+ | `fileInfo` | `"directory: [EDIT] opens it"` |
652
+ | `instrument` | `"Wavsynth instrument"` |
653
+ | `section` | `"Delay"`, `"MOD2"` |
654
+ | `scale` | `"Scale: C D E F G A B"`, `"Major (Ionian); detuned: E −0.50 st"` |
655
+ | `track` | `"Track 3"` |
656
+ | `location` | `"Phrase View row 07"`, `"Mixer View"` |
657
+ | `line` | the cursor row's text, when no field on it is recognised |
658
+
659
+ ### `M8ParsedRow`
660
+
661
+ ```ts
662
+ interface M8ParsedRow {
663
+ rowIndex: number | null // hex row index from leftmost chars; null if unparseable
664
+ rowKey: string // schema key, e.g. 'songRow', 'step', 'chainPos'
665
+ rowId: string | null // parameter views: matched schema row ('tempo', 'gain', …)
666
+ fields: Record<string, M8ParsedField> // column key → parsed field
667
+ }
668
+ ```
669
+
670
+ ### `M8ActiveField / M8ParsedField`
671
+
672
+ `M8ActiveField` extends `M8ParsedField` with two extra convenience fields.
673
+
674
+ ```ts
675
+ interface M8ParsedField {
676
+ key: string // schema key, e.g. 'fx1cmd', 'track3', 'note'
677
+ label: string // human label, e.g. 'FX1 Command', 'Track 3'
678
+ type: M8FieldType
679
+ rawValue: string // trimmed text sliced from currentLine
680
+ hexValue: number | null // parsed integer; null for empty cells and text types
681
+ isEmpty: boolean // true when '--', '---', '---00', etc.
682
+ meta?: Record<string, unknown> // view-specific extras (e.g. { trackIndex: 3 })
683
+ // parameter views (all optional):
684
+ description?: string // what the field is
685
+ text?: string | null // non-numeric part: option name, caption, name, instrument type
686
+ numericValue?: number | null // value in its natural base (hex, decimal, fractional)
687
+ values?: number[] | null // both numbers of an 'XX:YY' pair
688
+ parts?: string[] // names of those numbers, e.g. ['left time', 'right time']
689
+ unit?: string // e.g. ' BPM', ' dB'
690
+ valueDescription?: string // what the selected option does
691
+ fileKind?: M8FileKind // file browser rows: 'parent' | 'directory' | 'instrumentPreset' | 'sample' | 'm8file' | 'file'
692
+ }
693
+
694
+ interface M8ActiveField extends M8ParsedField {
695
+ viewName: string // repeated for convenience
696
+ rowIndex: number | null // row index of the line containing this field
697
+ }
698
+ ```
699
+
700
+ ### `M8FieldType`
701
+
702
+ ```ts
703
+ type M8FieldType =
704
+ | 'chainRef' // chain number hex 00–FF; '--' = empty
705
+ | 'phraseRef' // phrase number hex 00–FF; '--' = empty
706
+ | 'note' // note name e.g. 'C-4', 'A#3'; '---' = empty; 'OFF' = note-off
707
+ | 'velocity' // velocity 00–7F hex; '--' = inherit
708
+ | 'instrumentRef' // instrument slot 00–7F hex; '--' = keep current
709
+ | 'fxCommand' // 3-char command name e.g. 'KIL'; '---' = none
710
+ | 'fxValue' // FX parameter 00–FF hex
711
+ | 'transpose' // relative semitones: 00=none, 01–7F=+1–+127, FF–80=−1–−128
712
+ | 'ticks' // groove ticks per step hex; '00'=skip; '--'=end of groove
713
+ | 'volume' // level 00–FF hex; '--' = no override
714
+ | 'ppq' // pulses-per-quarter-note decimal; groove row 0 only
715
+ | 'instrumentName' // instrument name text ≤12 chars; no hexValue
716
+ | 'eqSlot' // EQ slot assignment hex; '--' = none
717
+ | 'rowIndex' // row/step index hex
718
+ | 'noteInterval' // scale view row label: C, C#, D, …
719
+ | 'onOff' // 'ON' / 'OFF' toggle
720
+ | 'semitoneOffset' // scale view detune, e.g. '-00.50'
721
+ | 'swing' // groove swing percentage
722
+ | 'instrumentType' // instrument engine: WAVSYNTH, SAMPLER, FMSYNTH, …
723
+ | 'name' // free-text name field; dashes = unnamed
724
+ | 'parameter' // named editable value: CUTOFF, TEMPO, GAIN, …
725
+ | 'option' // choice from a list: '00CHORUS', 'BELL', 'STEREO'
726
+ | 'action' // button: LOAD, SAVE, RENDER, SETTINGS, EQ, …
727
+ | 'fileEntry' // file browser row: '/folder', '/..', 'Bass.m8i'
728
+ ```
729
+
730
+ ### `M8KeyName`
731
+
732
+ ```ts
733
+ type M8KeyName = 'left' | 'right' | 'up' | 'down' | 'shift' | 'play' | 'opt' | 'edit'
734
+ ```
735
+
736
+ ### `M8SdkConfig`
737
+
738
+ ```ts
739
+ interface M8SdkConfig {
740
+ debug?: boolean // verbose logging (postMessage traffic, engine events)
741
+ mode?: 'auto' | 'host' | 'standalone' // default 'auto', see Standalone mode
742
+ handshakeTimeoutMs?: number // auto mode in an unmarked iframe; default 3000
743
+ autoConnect?: boolean // standalone: reopen a granted device at start; default true
744
+ }
745
+ ```
746
+
747
+ ---
748
+
749
+ ## Host events
750
+
751
+ 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.
752
+
753
+ | Event | Payload | Fired when |
754
+ |---|---|---|
755
+ | `stateChanged` | `M8State` | Any state field changes |
756
+ | `viewChanged` | `{ viewName, viewTitle }` | The active view changes |
757
+ | `cursorMoved` | `{ pos, rect, selectionMode }` | Cursor position or selection mode changes |
758
+ | `textUpdated` | `{ textUnderCursor, currentLine }` | Text under cursor or current line changes |
759
+ | `keyPressed` | `{ keys: number }` | A physical key event fires on the device |
760
+ | `transport` | `M8TransportEvent` | MIDI clock / start / stop / continue / song position, after `enableMidiTransport()` |
761
+
762
+ ---
763
+
764
+ ## Supported views
765
+
766
+ The semantic context parser supports these `viewName` values:
767
+
768
+ | `viewName` | Columns |
769
+ |---|---|
770
+ | `song` | `track1`–`track8` (chainRef, meta: trackIndex 1–8) |
771
+ | `chain` | `phrase` (phraseRef), `transpose` |
772
+ | `phrase` | `note`, `vel`, `inst`, `fx1cmd`, `fx1val`, `fx2cmd`, `fx2val`, `fx3cmd`, `fx3val` |
773
+ | `table` | `transpose`, `volume`, `fx1cmd`, `fx1val`, `fx2cmd`, `fx2val`, `fx3cmd`, `fx3val` |
774
+ | `groove` | `ticks`, `ppq` (row 0 only), `swing` (row 0 only) |
775
+ | `scale` | `interval`, `en` (onOff), `offset` (semitoneOffset) |
776
+ | `instrumentpool` | `name`, `dry`, `mx`, `de`, `rv`, `eq` |
777
+ | `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 |
778
+ | `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`) |
779
+ | `mixer` | `speakerVol`, `track1`–`track8`, `modfxVol`/`delayVol`/`reverbVol`, `inputVol`/`inputVol2`/`usbVol` and their sends, `mixEq` (action), `mainVol`, `limiter`, `djFilter`, `ott` |
780
+ | `project` | `tempo`, `transpose`, `groove`/`scale`/`liveQuantize` (option), `name`, and the `load`/`save`/`new`/`render`/`bundle`/… actions |
781
+ | `effectsettings` | per `section` (MODFX / DELAY / REVERB): type, input EQ (action), depth/frequency pairs, width, sends, delay time, feedback, room size, decay/shimmer |
782
+ | `mixeq`, `modfxeq`, `delayeq`, `reverbeq` | LOW/MID/HIGH band `gain`, `freq`, `q`, `type` (option), `mode` (option) |
783
+ | `mixscope`, `limiterscope` | `zoom`, `peak`, `softClip`, `mainVol`, `limiter`, `djFilter`, `ott`, `mixEq`; `atk`/`rel`/`type`/`res`/`time`/`color` as label/value pairs |
784
+ | `systemsettings` | `backlight`, `fontOptions`, `editTheme` (action), `notePreview`, `recCountIn`, `metronomeVol`, `usbAudioMode`, `usbMainOut`, `lineInGate`, `keyDelayRep`, … |
785
+ | `loadinstrument`, `selectsavedirectory` | `entry` (fileEntry) |
786
+ | `createdirectory` | `name` |
787
+
788
+ 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.
789
+
790
+ ---
791
+
792
+ ## Notes
793
+
794
+ - **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.
795
+ - **Margin markers** — The M8 draws `<` in the left margin of the cursor row, so it can appear as the first character of `currentLine`. The SDK strips the margin before parsing so column offsets remain consistent. Play markers are drawn inside the row instead (see [`readPlayPositions`](#readplaypositionsviewname-string-screen-m8screen-m8playpositions--null)).
796
+ - **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.
797
+ - **Schema file** — View and column definitions live in `src/m8-view-context.json` and are bundled into `dist/index.js` at build time.
798
+ - **Modes** — Inside a yam8d iframe named `yam8d-external-app` the client waits for yam8d's `post-me` handshake; in a top-level tab it runs the engine in the page and connects to the M8 itself (see [Standalone mode](#standalone-mode)). In an unnamed iframe it goes standalone after `handshakeTimeoutMs`.