@yam8d/m8-sdk 0.5.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.
- package/README.md +61 -161
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -129,7 +129,10 @@ m8.onConnectionChange((connected) => updateUi(connected))
|
|
|
129
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
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
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
|
|
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).
|
|
133
136
|
|
|
134
137
|
---
|
|
135
138
|
|
|
@@ -261,6 +264,16 @@ const cells = await m8.getScreenCells()
|
|
|
261
264
|
cells.fg[5][3] // colour of the first note of a phrase, e.g. '#ffffff'
|
|
262
265
|
```
|
|
263
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
|
+
|
|
264
277
|
---
|
|
265
278
|
|
|
266
279
|
### Event subscriptions
|
|
@@ -311,7 +324,35 @@ m8.onTextUpdate((text, line) => {
|
|
|
311
324
|
|
|
312
325
|
#### `onKeyPress(callback: (keys: number) => void): () => void`
|
|
313
326
|
|
|
314
|
-
Fires when a physical key event occurs on the device. `keys` is a bitmask of active keys.
|
|
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
|
+
```
|
|
315
356
|
|
|
316
357
|
---
|
|
317
358
|
|
|
@@ -491,6 +532,21 @@ steps[0].fields.note.rawValue // 'c-4' (the text stream is lower case)
|
|
|
491
532
|
|
|
492
533
|
The item number of a title (`'PHRASE 1F*'` → `{ viewId: '1F', isUnsaved: true }`).
|
|
493
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
|
+
|
|
494
550
|
### Theme: `parseThemeScreen`, `observeThemeColors`, `M8_THEME_ROLES`, `M8_THEME_VIEW`
|
|
495
551
|
|
|
496
552
|
The device theme has 13 colours, edited in System Settings › EDIT THEME (view name `themesettings`, exported as `M8_THEME_VIEW`).
|
|
@@ -701,6 +757,7 @@ These events are emitted by the host and consumed internally by `M8Client`. They
|
|
|
701
757
|
| `cursorMoved` | `{ pos, rect, selectionMode }` | Cursor position or selection mode changes |
|
|
702
758
|
| `textUpdated` | `{ textUnderCursor, currentLine }` | Text under cursor or current line changes |
|
|
703
759
|
| `keyPressed` | `{ keys: number }` | A physical key event fires on the device |
|
|
760
|
+
| `transport` | `M8TransportEvent` | MIDI clock / start / stop / continue / song position, after `enableMidiTransport()` |
|
|
704
761
|
|
|
705
762
|
---
|
|
706
763
|
|
|
@@ -735,164 +792,7 @@ Grid views are read by column position (`src/m8-view-context.json`). Parameter v
|
|
|
735
792
|
## Notes
|
|
736
793
|
|
|
737
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.
|
|
738
|
-
- **
|
|
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)).
|
|
739
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.
|
|
740
797
|
- **Schema file** — View and column definitions live in `src/m8-view-context.json` and are bundled into `dist/index.js` at build time.
|
|
741
|
-
- **
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
Client SDK for iframe applications that communicate with an M8 host.
|
|
745
|
-
|
|
746
|
-
## Install
|
|
747
|
-
|
|
748
|
-
```bash
|
|
749
|
-
npm install @yam8d/m8-sdk
|
|
750
|
-
```
|
|
751
|
-
|
|
752
|
-
For local development before the package is published:
|
|
753
|
-
|
|
754
|
-
```bash
|
|
755
|
-
npm install ../yam8d/packages/m8-sdk
|
|
756
|
-
```
|
|
757
|
-
|
|
758
|
-
## Usage
|
|
759
|
-
|
|
760
|
-
```ts
|
|
761
|
-
import { createM8Client } from '@yam8d/m8-sdk'
|
|
762
|
-
|
|
763
|
-
const m8 = await createM8Client()
|
|
764
|
-
|
|
765
|
-
console.log(m8.state.viewName)
|
|
766
|
-
await m8.sendKeyPress(['play'])
|
|
767
|
-
|
|
768
|
-
const unsubscribe = m8.onStateChange((state) => {
|
|
769
|
-
console.log(state.cursorPos)
|
|
770
|
-
})
|
|
771
|
-
|
|
772
|
-
unsubscribe()
|
|
773
|
-
m8.disconnect()
|
|
774
|
-
```
|
|
775
|
-
|
|
776
|
-
For React or other environments where top-level `await` is not convenient:
|
|
777
|
-
|
|
778
|
-
```ts
|
|
779
|
-
import { createM8ClientSync } from '@yam8d/m8-sdk'
|
|
780
|
-
|
|
781
|
-
const { client, connect } = createM8ClientSync()
|
|
782
|
-
await connect()
|
|
783
|
-
```
|
|
784
|
-
|
|
785
|
-
The app must run inside the host iframe. Opening it directly in a standalone browser tab cannot establish the SDK handshake.
|
|
786
|
-
|
|
787
|
-
---
|
|
788
|
-
|
|
789
|
-
## Semantic Context
|
|
790
|
-
|
|
791
|
-
The SDK can interpret the current cursor position and row text as typed, labelled field values. This works for the five grid views (`song`, `chain`, `phrase`, `table`, `groove`) and the `instrumentpool` view.
|
|
792
|
-
|
|
793
|
-
### Via the client
|
|
794
|
-
|
|
795
|
-
```ts
|
|
796
|
-
const ctx = m8.getSemanticContext()
|
|
797
|
-
// or as a human-readable string:
|
|
798
|
-
const description = m8.describeContext()
|
|
799
|
-
// e.g. "Track 3: 0A — Track 3 — Song View row 01"
|
|
800
|
-
// or as tagged parts to build your own:
|
|
801
|
-
const parts = m8.describeContextParts()
|
|
802
|
-
```
|
|
803
|
-
|
|
804
|
-
### Standalone functions
|
|
805
|
-
|
|
806
|
-
```ts
|
|
807
|
-
import { getSemanticContext, describeContext, formatFieldValue, lookupFxCommand } from '@yam8d/m8-sdk'
|
|
808
|
-
|
|
809
|
-
const ctx = getSemanticContext(state)
|
|
810
|
-
|
|
811
|
-
if (ctx?.activeField) {
|
|
812
|
-
const field = ctx.activeField
|
|
813
|
-
|
|
814
|
-
console.log(field.key) // e.g. 'track3', 'note', 'fx1cmd'
|
|
815
|
-
console.log(field.label) // e.g. 'Track 3', 'Note (N)', 'FX1 Command'
|
|
816
|
-
console.log(field.type) // e.g. 'chainRef', 'note', 'fxCommand'
|
|
817
|
-
console.log(field.rawValue) // raw text from the line, e.g. '0A', 'C-4', 'KIL'
|
|
818
|
-
console.log(field.hexValue) // parsed integer or null (null for empty cells and text types)
|
|
819
|
-
console.log(field.isEmpty) // true when '--' / '---' / '---00'
|
|
820
|
-
|
|
821
|
-
// Song view: which track is the cursor on?
|
|
822
|
-
if (field.type === 'chainRef') {
|
|
823
|
-
console.log(field.meta?.trackIndex) // 1–8
|
|
824
|
-
}
|
|
825
|
-
|
|
826
|
-
// FX command: look up its description
|
|
827
|
-
if (field.type === 'fxCommand' && !field.isEmpty) {
|
|
828
|
-
const info = lookupFxCommand(field.rawValue)
|
|
829
|
-
console.log(info?.description)
|
|
830
|
-
}
|
|
831
|
-
}
|
|
832
|
-
|
|
833
|
-
// Row-level fields (all columns on the current line)
|
|
834
|
-
if (ctx?.row) {
|
|
835
|
-
console.log(ctx.row.rowIndex) // hex-parsed row index, e.g. 2
|
|
836
|
-
console.log(ctx.row.fields['note']) // M8ParsedField for the note column
|
|
837
|
-
}
|
|
838
|
-
```
|
|
839
|
-
|
|
840
|
-
### Returned types
|
|
841
|
-
|
|
842
|
-
```ts
|
|
843
|
-
interface M8SemanticContext {
|
|
844
|
-
viewName: string // e.g. 'song', 'phrase'
|
|
845
|
-
viewTitle: string | null // e.g. 'Song View'
|
|
846
|
-
viewDescription: string | null
|
|
847
|
-
isGridView: boolean
|
|
848
|
-
row: M8ParsedRow | null // all columns on the cursor's line
|
|
849
|
-
activeField: M8ActiveField | null // the column under the cursor
|
|
850
|
-
}
|
|
851
|
-
|
|
852
|
-
interface M8ParsedField {
|
|
853
|
-
key: string // schema key, e.g. 'fx1cmd', 'track3'
|
|
854
|
-
label: string // human label, e.g. 'FX1 Command', 'Track 3'
|
|
855
|
-
type: M8FieldType
|
|
856
|
-
rawValue: string // trimmed text from currentLine
|
|
857
|
-
hexValue: number | null
|
|
858
|
-
isEmpty: boolean
|
|
859
|
-
meta?: Record<string, unknown>
|
|
860
|
-
}
|
|
861
|
-
```
|
|
862
|
-
|
|
863
|
-
### Supported field types (`M8FieldType`)
|
|
864
|
-
|
|
865
|
-
| Type | Description |
|
|
866
|
-
|---|---|
|
|
867
|
-
| `chainRef` | Chain number (hex). `--` = empty. |
|
|
868
|
-
| `phraseRef` | Phrase number (hex). `--` = empty. |
|
|
869
|
-
| `note` | Note name, e.g. `C-4`, `A#3`. `---` = empty. |
|
|
870
|
-
| `velocity` | Note velocity 00–7F (hex). |
|
|
871
|
-
| `instrumentRef` | Instrument slot 00–7F (hex). `--` = keep current. |
|
|
872
|
-
| `fxCommand` | 3-char FX command name, e.g. `KIL`, `ARP`. `---` = none. |
|
|
873
|
-
| `fxValue` | FX value 00–FF (hex). |
|
|
874
|
-
| `transpose` | Relative transpose (hex): `00`=none, `01`–`7F`=+1–+127, `FF`–`80`=−1–−128. |
|
|
875
|
-
| `ticks` | Groove ticks per step (hex). |
|
|
876
|
-
| `volume` | Volume level 00–FF (hex). `--` = no override. |
|
|
877
|
-
| `ppq` | Pulses Per Quarter note (decimal). Groove row 0 only. |
|
|
878
|
-
| `instrumentName` | Instrument name text (up to 12 chars). No `hexValue`. |
|
|
879
|
-
| `eqSlot` | EQ slot assignment (hex). `--` = none. |
|
|
880
|
-
| `rowIndex` | Row/step index (hex). |
|
|
881
|
-
| `noteInterval` | Scale View row label (`C`, `C#`, …). |
|
|
882
|
-
| `onOff` | `ON` / `OFF` toggle. |
|
|
883
|
-
| `semitoneOffset` | Scale View detune, e.g. `-00.50`. |
|
|
884
|
-
| `swing` | Groove swing percentage. |
|
|
885
|
-
| `instrumentType` | Instrument engine (`WAVSYNTH`, `SAMPLER`, …); `text` holds the name. |
|
|
886
|
-
| `name` | Free-text name field; a row of dashes = unnamed. |
|
|
887
|
-
| `parameter` | Named editable value (`CUTOFF`, `TEMPO`, `GAIN`); read `hexValue` / `numericValue` / `values`. |
|
|
888
|
-
| `option` | Choice from a list (`00CHORUS`, `BELL`); `hexValue` is the index when shown, `text` the name. |
|
|
889
|
-
| `action` | Button caption (`LOAD`, `SAVE`, `RENDER`, `SETTINGS`); `text` holds it. |
|
|
890
|
-
| `fileEntry` | File browser row: `/folder`, `/..`, or a file name. |
|
|
891
|
-
|
|
892
|
-
### Playback indicator
|
|
893
|
-
|
|
894
|
-
When a row is actively playing, the M8 prepends a `<` or `>` character to `currentLine`. The SDK strips this automatically before parsing, so column offsets are always consistent.
|
|
895
|
-
|
|
896
|
-
### Notes file
|
|
897
|
-
|
|
898
|
-
View/column definitions live in `src/m8-view-context.json` and are bundled into `dist/index.js` at build time. All x/width values assume **font mode 0** (8×10 px cells on Headless, 12×14 px on Model:02). Font modes 1 and 2 may shift column positions.
|
|
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`.
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@yam8d/m8-sdk",
|
|
3
|
-
"version": "0.5.
|
|
4
|
-
"description": "Client SDK for
|
|
3
|
+
"version": "0.5.1",
|
|
4
|
+
"description": "Client SDK for apps that read and drive a Dirtywave M8, inside a yam8d External Apps iframe or standalone over Web Serial, WebUSB or Web MIDI.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
7
7
|
"module": "./dist/index.js",
|