@yam8d/m8-sdk 0.3.2 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -25,6 +25,7 @@ Client SDK for iframe applications that communicate with an M8 tracker host via
25
25
  - [M8State](#m8state)
26
26
  - [CursorPos / CursorRect / RGB / SystemInfos](#cursorpos--cursorrect--rgb--systeminfos)
27
27
  - [M8SemanticContext](#m8semanticcontext)
28
+ - [M8DescriptionPart](#m8descriptionpart)
28
29
  - [M8ParsedRow](#m8parsedrow)
29
30
  - [M8ActiveField / M8ParsedField](#m8activefield--m8parsedfield)
30
31
  - [M8FieldType](#m8fieldtype)
@@ -287,11 +288,38 @@ if (ctx?.activeField?.type === 'chainRef') {
287
288
 
288
289
  Returns a human-readable one-liner, e.g.:
289
290
 
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"`
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"`
293
294
 
294
- Returns `null` when not connected or no view is active.
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.
295
323
 
296
324
  ---
297
325
 
@@ -307,10 +335,12 @@ Closes the host connection and clears all event listeners and internal state.
307
335
 
308
336
  These are re-exported from `@yam8d/m8-sdk` for use without an `M8Client` instance.
309
337
 
310
- ### `getSemanticContext(state: M8State): M8SemanticContext | null`
338
+ ### `getSemanticContext(state: M8State, screen?: M8Screen | null): M8SemanticContext | null`
311
339
 
312
340
  Parses an `M8State` snapshot into a semantic context. Returns `null` if `state.viewName` is not set.
313
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
+
314
344
  ```ts
315
345
  import { getSemanticContext } from '@yam8d/m8-sdk'
316
346
 
@@ -325,6 +355,10 @@ if (ctx?.isGridView && ctx.row) {
325
355
 
326
356
  Returns a human-readable summary of the context (see examples above).
327
357
 
358
+ ### `describeContextParts(ctx: M8SemanticContext): M8DescriptionPart[]`
359
+
360
+ The summary as ordered, tagged parts; `describeContext()` joins their text with `' — '`.
361
+
328
362
  ### `formatFieldValue(field: M8ParsedField): string`
329
363
 
330
364
  Formats a single parsed field as a display string:
@@ -339,17 +373,40 @@ Formats a single parsed field as a display string:
339
373
  | `chainRef` / `phraseRef` / `fxCommand` / `volume` | raw value, e.g. `"0A"`, `"KIL"` |
340
374
  | empty cell | `"(empty)"` |
341
375
 
342
- ### `lookupFxCommand(cmd: string): { description: string } | undefined`
376
+ ### `lookupFxCommand(cmd: string): M8FxCommandInfo | undefined`
343
377
 
344
- Looks up a 3-character FX command name (case-insensitive) across all categories (sequencer, instrument, mixer).
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`).
345
379
 
346
380
  ```ts
347
381
  import { lookupFxCommand } from '@yam8d/m8-sdk'
348
382
 
349
- const info = lookupFxCommand('KIL')
350
- // → { description: 'Kill: stops the playing instrument after XX ticks.' }
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 }
351
390
  ```
352
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
+
353
410
  ---
354
411
 
355
412
  ## Types reference
@@ -398,18 +455,59 @@ interface M8SemanticContext {
398
455
  viewDescription: string | null // long description from schema
399
456
  viewId: string | null // item ID parsed from screen title, e.g. 'F2', '03'; null for song/instrumentpool
400
457
  isUnsaved: boolean // true when the title carries a '*' suffix (unsaved changes)
401
- isGridView: boolean // true for song/chain/phrase/table/groove/instrumentpool
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' }
402
462
  row: M8ParsedRow | null // all columns on the line at cursorPos.y
403
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
404
472
  }
405
473
  ```
406
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
+
407
504
  ### `M8ParsedRow`
408
505
 
409
506
  ```ts
410
507
  interface M8ParsedRow {
411
508
  rowIndex: number | null // hex row index from leftmost chars; null if unparseable
412
509
  rowKey: string // schema key, e.g. 'songRow', 'step', 'chainPos'
510
+ rowId: string | null // parameter views: matched schema row ('tempo', 'gain', …)
413
511
  fields: Record<string, M8ParsedField> // column key → parsed field
414
512
  }
415
513
  ```
@@ -427,6 +525,15 @@ interface M8ParsedField {
427
525
  hexValue: number | null // parsed integer; null for empty cells and text types
428
526
  isEmpty: boolean // true when '--', '---', '---00', etc.
429
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'
430
537
  }
431
538
 
432
539
  interface M8ActiveField extends M8ParsedField {
@@ -453,6 +560,16 @@ type M8FieldType =
453
560
  | 'instrumentName' // instrument name text ≤12 chars; no hexValue
454
561
  | 'eqSlot' // EQ slot assignment hex; '--' = none
455
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'
456
573
  ```
457
574
 
458
575
  ### `M8KeyName`
@@ -495,16 +612,28 @@ The semantic context parser supports these `viewName` values:
495
612
  | `chain` | `phrase` (phraseRef), `transpose` |
496
613
  | `phrase` | `note`, `vel`, `inst`, `fx1cmd`, `fx1val`, `fx2cmd`, `fx2val`, `fx3cmd`, `fx3val` |
497
614
  | `table` | `transpose`, `volume`, `fx1cmd`, `fx1val`, `fx2cmd`, `fx2val`, `fx3cmd`, `fx3val` |
498
- | `groove` | `ticks`, `ppq` (row 0 only) |
615
+ | `groove` | `ticks`, `ppq` (row 0 only), `swing` (row 0 only) |
616
+ | `scale` | `interval`, `en` (onOff), `offset` (semitoneOffset) |
499
617
  | `instrumentpool` | `name`, `dry`, `mx`, `de`, `rv`, `eq` |
500
-
501
- All other views (`isGridView: false`) return a context with `row: null` and `activeField: null`.
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.
502
630
 
503
631
  ---
504
632
 
505
633
  ## Notes
506
634
 
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.
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.
508
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.
509
638
  - **Schema file** — View and column definitions live in `src/m8-view-context.json` and are bundled into `dist/index.js` at build time.
510
639
  - **Requirement** — The app must run inside the yam8d host iframe. The `ChildHandshake` from `post-me` will never resolve in a standalone tab.
@@ -565,7 +694,9 @@ The SDK can interpret the current cursor position and row text as typed, labelle
565
694
  const ctx = m8.getSemanticContext()
566
695
  // or as a human-readable string:
567
696
  const description = m8.describeContext()
568
- // e.g. "Track 3 — Chain 0A — Song View row 02"
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()
569
700
  ```
570
701
 
571
702
  ### Standalone functions
@@ -645,6 +776,16 @@ interface M8ParsedField {
645
776
  | `instrumentName` | Instrument name text (up to 12 chars). No `hexValue`. |
646
777
  | `eqSlot` | EQ slot assignment (hex). `--` = none. |
647
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. |
648
789
 
649
790
  ### Playback indicator
650
791