@wcstack/midi 1.25.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.ja.md ADDED
@@ -0,0 +1,190 @@
1
+ # @wcstack/midi
2
+
3
+ > 🤖 **AI coding agents**: This README is a package-level reference, not the primary entry point for building a wcstack application. If you have not already done so, first read the repository [README](https://github.com/wcstack/wcstack#readme) and [AGENTS.md](https://github.com/wcstack/wcstack/blob/main/AGENTS.md), then use the [wcstack-app skill](https://github.com/wcstack/wcstack-skill).
4
+
5
+ `@wcstack/midi` は wcstack エコシステムのためのヘッドレスな Web MIDI コンポーネントです。
6
+
7
+ 見た目を持つ UI ウィジェットではありません。
8
+ `@wcstack/geolocation` が現在地をリアクティブな状態に変えるのと同じように、**MIDI ハードウェアをリアクティブな状態に変える非同期プリミティブノード**です。
9
+
10
+ `@wcstack/state` と組み合わせると、`<wcs-midi>` はパス契約で直接バインドできます。
11
+
12
+ - **入力面**: `input` / `output` / `channel` / `sysex` / `auto`
13
+ - **出力状態面**: `message` / `type` / `channel` / `note` / `velocity` / `control` / `value` / `devices` / `connected` / `permission` / `error`
14
+ - **コマンド**: `request` / `close` / `send`
15
+
16
+ つまり、UI 層に `navigator.requestMIDIAccess()` や `onmidimessage` の配線を書かずに、MIDI コントローラでアプリケーション状態を宣言的に駆動できます。
17
+
18
+ `@wcstack/midi` は [CSBC](https://github.com/csbc-dev/arch/blob/main/README.md)(Core / Shell / Binding Contract)アーキテクチャに従います。
19
+
20
+ - **Core**(`MidiCore`)— アクセス取得・ポート選択・メッセージのデコード・送信
21
+ - **Shell**(`<wcs-midi>`)— その状態を DOM の属性とライフサイクルに接続
22
+ - **Binding Contract**(`static wcBindable`)— 観測可能な `properties` と3つのコマンドを宣言
23
+
24
+ ## なぜ入出力で1タグなのか
25
+
26
+ MIDI の入力と出力は別機能に見えますが、ページが持つ `MIDIAccess` は**1つだけ**で、両方向はそこから派生します。`<wcs-midi-in>` と `<wcs-midi-out>` に分割しても、同じハンドルを2要素が調停する必要が生まれるだけです。そこで `<wcs-ws>` が WebSocket の送受信を1タグで扱うのと同様に、`<wcs-midi>` が両方向を担います。(`@wcstack/speech` が2タグに分かれているのは、`SpeechSynthesis` と `SpeechRecognition` が**別々の API** だからです。)
27
+
28
+ > **Chromium 限定。** Web MIDI は Chromium 系のみで動きます。Firefox は既定で無効、Safari は未実装です。API が無い環境(非セキュアコンテキストを含む)では、`<wcs-midi>` は throw せず `permission = "unsupported"` を報告します。
29
+
30
+ ## インストール
31
+
32
+ ```bash
33
+ npm install @wcstack/midi
34
+ ```
35
+
36
+ ## クイックスタート
37
+
38
+ ### 1. MIDI キーボードで音符を受ける
39
+
40
+ ```html
41
+ <script type="module" src="https://esm.run/@wcstack/state/auto"></script>
42
+ <script type="module" src="https://esm.run/@wcstack/midi/auto"></script>
43
+
44
+ <wcs-state>
45
+ <script type="module">
46
+ export default {
47
+ held: [],
48
+ $eventTokens: ["onMidi"],
49
+ $on: {
50
+ onMidi: (state, event) => {
51
+ const { type, note } = event.detail;
52
+ if (type === "noteon") state.held = [...state.held, note];
53
+ else if (type === "noteoff") state.held = state.held.filter((n) => n !== note);
54
+ },
55
+ },
56
+ };
57
+ </script>
58
+ </wcs-state>
59
+
60
+ <wcs-midi auto data-wcs="eventToken.message: onMidi"></wcs-midi>
61
+
62
+ <p data-wcs="text: held"></p>
63
+ ```
64
+
65
+ ### 2. 明示的に要求するまで何も始まらない
66
+
67
+ `requestMIDIAccess()` は権限プロンプトを出しうるため、**要素を接続しただけでは何も起きません**。`auto` 属性を付けるか(上の例)、ユーザー操作からコマンドを撃ってください。
68
+
69
+ ```html
70
+ <wcs-midi data-wcs="command.request: $command.connectMidi; connected: midiReady"></wcs-midi>
71
+
72
+ <button data-wcs="onclick: $command.connectMidi; hidden: midiReady">MIDI に接続</button>
73
+ ```
74
+
75
+ ### 3. フェーダーで値を駆動する
76
+
77
+ ```html
78
+ <wcs-midi auto channel="1" data-wcs="control: cc; value: ccValue"></wcs-midi>
79
+
80
+ <!-- CC 7 は慣習的にボリュームフェーダー -->
81
+ <meter min="0" max="127" data-wcs="value: ccValue"></meter>
82
+ ```
83
+
84
+ ### 4. MIDI デバイスへ送信する
85
+
86
+ ```html
87
+ <wcs-midi auto output="Synth" data-wcs="command.send: $command.midiSend"></wcs-midi>
88
+ ```
89
+
90
+ ```js
91
+ // 位置引数はそのまま素通しされる(command-token)
92
+ state.$command.midiSend.emit([0x90, 60, 100]);
93
+ ```
94
+
95
+ ## メッセージ面
96
+
97
+ 1つのイベント(`wcs-midi:message`)がメッセージ全体を運び、個々のフィールドは同じイベントからの派生 getter です。必要なものだけをバインドできます。
98
+
99
+ | プロパティ | 型 | 備考 |
100
+ |---|---|---|
101
+ | `message` | object | `{ data, port, portName, timestamp, ...デコード結果 }` |
102
+ | `type` | string | `noteon` / `noteoff` / `polyaftertouch` / `controlchange` / `programchange` / `aftertouch` / `pitchbend` / `sysex` / `other` |
103
+ | `channel` | number \| null | 1〜16。システムメッセージは `null` |
104
+ | `note` | number \| null | 0〜127 |
105
+ | `velocity` | number \| null | **0〜1 に正規化** |
106
+ | `control` | number \| null | コントローラ番号。生の 0〜127 |
107
+ | `value` | number \| null | 生の 0〜127。**ピッチベンドのみ -1〜1** |
108
+
109
+ 2つの正規化を行っています(さもないと全ての利用者が同じ処理を書くことになるため)。
110
+
111
+ - **velocity 0 の note-on は `noteoff` として報告します。** 多くのコントローラは本来の 0x8n note-off を送らず、note-on のまま扱うと音が鳴りっぱなしになります。生のステータスバイトは `message.data[0]` に残っています。
112
+ - **velocity は 0〜1** なので、そのままゲインに掛けられます。コントローラ値は意味がコントローラ依存なので生の値のままです。
113
+
114
+ `message` は状態ではなく **occurrence** です。同じ内容の note-on 2回は別々の打鍵なので、同値ガードは行いません。`data` はメッセージごとに新しい `Uint8Array` で、プラットフォームのバッファをそのまま渡すことはありません(RxJS の replay や React のスナップショットで保持しても安全です)。
115
+
116
+ ## ポート
117
+
118
+ | 属性 | 省略時 | 指定時 |
119
+ |---|---|---|
120
+ | `input` | **全ての**入力ポートを購読 | ポート id、または名前の前方一致(大文字小文字を無視) |
121
+ | `output` | `send()` は**最初の**出力ポートへ | ポート id、または名前の前方一致 |
122
+ | `channel` | 全チャンネルを配送 | そのチャンネル(1〜16)のみ配送。システムメッセージは常に通す |
123
+
124
+ 既定で全ポートを購読するのは MIDI の期待に合わせたものです。挿したコントローラはページが名前を指定しなくても鳴るべきです。
125
+
126
+ 接続中の要素で `input` / `output` / `channel` を変更すると、**既存のアクセスに対して購読を張り替えます**。再要求は行わないので、権限プロンプトが再度出ることはありません。
127
+
128
+ `devices` は全ポートを `{ id, name, manufacturer, direction, state }` として publish し、着脱のたびに更新します。デバイス名は一意ではない(同一機種を2台挿すと同じ名前になる)ため、特定の1台を指したい場合は id を使ってください。
129
+
130
+ ## 権限
131
+
132
+ `permission` はおなじみの4値です: `prompt` / `granted` / `denied` / `unsupported`。
133
+
134
+ `navigator.permissions.query({ name: "midi" })` に答えるブラウザでは、サイト設定で権限が取り消された場合も再要求なしに状態へ流れます。答えないブラウザでは `request()` の結果から推定します。
135
+
136
+ `sysex` はシステムエクスクルーシブの許可を要求します。これは**別個の、より制限された権限**なので、実際に SysEx が必要でなければ付けないでください。
137
+
138
+ ## `:state()` による CSS スタイリング
139
+
140
+ Shell は出力状態を `ElementInternals.states` に反映します。
141
+
142
+ ```css
143
+ wcs-midi:state(connected) { --midi-dot: limegreen; }
144
+ wcs-midi:state(denied) { --midi-dot: crimson; }
145
+ wcs-midi:state(unsupported) { --midi-dot: dimgray; }
146
+ wcs-midi:state(error) { --midi-dot: orange; }
147
+ ```
148
+
149
+ デバッグ中は `debug-states` 属性を付けると `data-wcs-state-*` にもミラーされます。`ElementInternals` が無い環境では反映だけが静かに無効化され、他の機能はそのまま動きます。
150
+
151
+ ## ヘッドレス利用(Core)
152
+
153
+ `MidiCore` は DOM を必要としない公開サーフェスです。
154
+
155
+ ```js
156
+ import { MidiCore } from "@wcstack/midi";
157
+
158
+ const core = new MidiCore({ channel: 1 });
159
+ core.addEventListener("wcs-midi:message", (e) => console.log(e.detail.type, e.detail.note));
160
+ await core.request();
161
+ core.send([0x90, 60, 100]);
162
+ // ライフサイクルは利用者の責任: dispose() しないと、生きている onmidimessage
163
+ // ハンドラがこのインスタンスをポートの寿命だけ到達可能に保つ。
164
+ core.dispose();
165
+ ```
166
+
167
+ 別経路で得たバイト列をデコードするために `parseMessage(data)` も単体で export しています。
168
+
169
+ ## エラー
170
+
171
+ throw は一切しません。失敗は状態として現れます。
172
+
173
+ | 状況 | `permission` | `error` | `errorInfo.code` |
174
+ |---|---|---|---|
175
+ | `navigator.requestMIDIAccess` 不在 | `unsupported` | `"unsupported"` | `capability-missing` |
176
+ | ユーザーが拒否 / ポリシーでブロック | `denied` | rejection のメッセージ | `not-allowed` |
177
+ | その他のアクセス失敗 | `denied` | rejection のメッセージ | `access-error` |
178
+ | `send()` の失敗 | 変化なし | throw のメッセージ | `send-failed` |
179
+
180
+ ## 注意点
181
+
182
+ - **Chromium 限定**。セキュアコンテキストが必要。
183
+ - **velocity 0 の note-on は note-off**(上記)。
184
+ - **`sysex` は別権限**で、拒否されやすい。
185
+ - **デバイス名は一意ではない**。id を推奨。
186
+ - **`statechange` は1台につき複数回飛びうる**(入力側と出力側が別々に届く)。`devices` は内容比較しているので、実際に変化していなければ再 publish されません。
187
+
188
+ ## ライセンス
189
+
190
+ MIT
package/README.md ADDED
@@ -0,0 +1,190 @@
1
+ # @wcstack/midi
2
+
3
+ > 🤖 **AI coding agents**: This README is a package-level reference, not the primary entry point for building a wcstack application. If you have not already done so, first read the repository [README](https://github.com/wcstack/wcstack#readme) and [AGENTS.md](https://github.com/wcstack/wcstack/blob/main/AGENTS.md), then use the [wcstack-app skill](https://github.com/wcstack/wcstack-skill).
4
+
5
+ `@wcstack/midi` is a headless Web MIDI component for the wcstack ecosystem.
6
+
7
+ It is not a visual UI widget.
8
+ It is an **async primitive node** that turns MIDI hardware into reactive state — the same way `@wcstack/geolocation` turns the device's location into reactive state.
9
+
10
+ With `@wcstack/state`, `<wcs-midi>` can be bound directly through path contracts:
11
+
12
+ - **input surface**: `input`, `output`, `channel`, `sysex`, `auto`
13
+ - **output state surface**: `message`, `type`, `channel`, `note`, `velocity`, `control`, `value`, `devices`, `connected`, `permission`, `error`
14
+ - **commands**: `request`, `close`, `send`
15
+
16
+ This means a MIDI controller can drive your application state declaratively in HTML, without writing `navigator.requestMIDIAccess()` or `onmidimessage` glue in your UI layer.
17
+
18
+ `@wcstack/midi` follows the [CSBC](https://github.com/csbc-dev/arch/blob/main/README.md) (Core / Shell / Binding Contract) architecture:
19
+
20
+ - **Core** (`MidiCore`) handles access, port selection, message decoding and sending
21
+ - **Shell** (`<wcs-midi>`) connects that state to DOM attributes and lifecycle
22
+ - **Binding Contract** (`static wcBindable`) declares observable `properties` plus the three commands
23
+
24
+ ## Why one tag for both directions
25
+
26
+ MIDI input and output look like two features, but a page holds exactly **one** `MIDIAccess` object, and both directions are derived from it. Splitting them into `<wcs-midi-in>` and `<wcs-midi-out>` would only force the two elements to coordinate over the same shared handle — so `<wcs-midi>` covers both, as `<wcs-ws>` does for WebSocket send/receive. (`@wcstack/speech` splits into two tags precisely because `SpeechSynthesis` and `SpeechRecognition` are two *different* APIs.)
27
+
28
+ > **Chromium only.** Web MIDI ships in Chromium-based browsers. Firefox has it behind a pref and Safari does not implement it. Where the API is absent — including any non-secure context — `<wcs-midi>` reports `permission = "unsupported"` instead of throwing.
29
+
30
+ ## Install
31
+
32
+ ```bash
33
+ npm install @wcstack/midi
34
+ ```
35
+
36
+ ## Quick Start
37
+
38
+ ### 1. Play notes from a MIDI keyboard
39
+
40
+ ```html
41
+ <script type="module" src="https://esm.run/@wcstack/state/auto"></script>
42
+ <script type="module" src="https://esm.run/@wcstack/midi/auto"></script>
43
+
44
+ <wcs-state>
45
+ <script type="module">
46
+ export default {
47
+ held: [],
48
+ $eventTokens: ["onMidi"],
49
+ $on: {
50
+ onMidi: (state, event) => {
51
+ const { type, note } = event.detail;
52
+ if (type === "noteon") state.held = [...state.held, note];
53
+ else if (type === "noteoff") state.held = state.held.filter((n) => n !== note);
54
+ },
55
+ },
56
+ };
57
+ </script>
58
+ </wcs-state>
59
+
60
+ <wcs-midi auto data-wcs="eventToken.message: onMidi"></wcs-midi>
61
+
62
+ <p data-wcs="text: held"></p>
63
+ ```
64
+
65
+ ### 2. Nothing starts until you ask
66
+
67
+ `requestMIDIAccess()` can raise a permission prompt, so **connecting the element does nothing on its own**. Either add the `auto` attribute (above), or fire the command from a user gesture:
68
+
69
+ ```html
70
+ <wcs-midi data-wcs="command.request: $command.connectMidi; connected: midiReady"></wcs-midi>
71
+
72
+ <button data-wcs="onclick: $command.connectMidi; hidden: midiReady">Connect MIDI</button>
73
+ ```
74
+
75
+ ### 3. Drive a value from a fader
76
+
77
+ ```html
78
+ <wcs-midi auto channel="1" data-wcs="control: cc; value: ccValue"></wcs-midi>
79
+
80
+ <!-- CC 7 is the conventional volume fader. -->
81
+ <meter min="0" max="127" data-wcs="value: ccValue"></meter>
82
+ ```
83
+
84
+ ### 4. Send to a MIDI device
85
+
86
+ ```html
87
+ <wcs-midi auto output="Synth" data-wcs="command.send: $command.midiSend"></wcs-midi>
88
+ ```
89
+
90
+ ```js
91
+ // Positional arguments pass through verbatim (command-token).
92
+ state.$command.midiSend.emit([0x90, 60, 100]);
93
+ ```
94
+
95
+ ## Message surface
96
+
97
+ One event (`wcs-midi:message`) carries the whole message; the individual fields are derived getters on that same event, so you bind only what you need.
98
+
99
+ | Property | Type | Notes |
100
+ |---|---|---|
101
+ | `message` | object | `{ data, port, portName, timestamp, ...decoded }` |
102
+ | `type` | string | `noteon` / `noteoff` / `polyaftertouch` / `controlchange` / `programchange` / `aftertouch` / `pitchbend` / `sysex` / `other` |
103
+ | `channel` | number \| null | 1-16; `null` for system messages |
104
+ | `note` | number \| null | 0-127 |
105
+ | `velocity` | number \| null | **normalized to 0-1** |
106
+ | `control` | number \| null | controller number, raw 0-127 |
107
+ | `value` | number \| null | raw 0-127, or **-1..1 for pitch bend** |
108
+
109
+ Two normalizations are applied so every consumer does not have to repeat them:
110
+
111
+ - **A note-on with velocity 0 is reported as `noteoff`.** Many controllers never emit a real 0x8n note-off; treating those as note-ons leaves stuck notes. The raw status byte is still in `message.data[0]`.
112
+ - **Velocity is 0-1**, so it multiplies straight into a gain. Controller values stay raw, since their meaning is per-controller.
113
+
114
+ `message` is an **occurrence**, not a state: two identical note-ons are two distinct presses, so it is never same-value guarded. `data` is a fresh `Uint8Array` on every message — the platform buffer is never handed out, so retaining a message (an RxJS replay, a React snapshot) is safe.
115
+
116
+ ## Ports
117
+
118
+ | Attribute | Omitted | Given |
119
+ |---|---|---|
120
+ | `input` | subscribe to **every** input port | port id, or a case-insensitive name prefix |
121
+ | `output` | `send()` goes to the **first** output port | port id, or a case-insensitive name prefix |
122
+ | `channel` | deliver every channel | deliver only that channel (1-16); system messages always pass |
123
+
124
+ Subscribing to everything by default matches the expectation for MIDI: a controller you plug in should just work, not require the page to name it first.
125
+
126
+ Changing `input` / `output` / `channel` on a live element **re-hooks the existing access** — it never re-requests, so there is no second permission prompt.
127
+
128
+ `devices` publishes every port as `{ id, name, manufacturer, direction, state }`, refreshed on every plug and unplug. Device names are not unique (plug in two of the same model and both report the same name), so prefer ids when a page must target a specific unit.
129
+
130
+ ## Permission
131
+
132
+ `permission` is the familiar four-value surface: `prompt` / `granted` / `denied` / `unsupported`.
133
+
134
+ Where the browser answers `navigator.permissions.query({ name: "midi" })`, a grant revoked in site settings flows into the state without a re-request. Where it does not, the state is inferred from the outcome of `request()`.
135
+
136
+ `sysex` requests the system-exclusive grant, which is a **separate and more restricted permission** — leave it off unless you actually need SysEx.
137
+
138
+ ## CSS styling with `:state()`
139
+
140
+ The Shell reflects its output state into `ElementInternals.states`:
141
+
142
+ ```css
143
+ wcs-midi:state(connected) { --midi-dot: limegreen; }
144
+ wcs-midi:state(denied) { --midi-dot: crimson; }
145
+ wcs-midi:state(unsupported) { --midi-dot: dimgray; }
146
+ wcs-midi:state(error) { --midi-dot: orange; }
147
+ ```
148
+
149
+ Add `debug-states` to also mirror them as `data-wcs-state-*` attributes while debugging. In environments without `ElementInternals`, reflection is silently disabled and everything else keeps working.
150
+
151
+ ## Headless usage (Core)
152
+
153
+ `MidiCore` is a public, framework-agnostic surface — no DOM required:
154
+
155
+ ```js
156
+ import { MidiCore } from "@wcstack/midi";
157
+
158
+ const core = new MidiCore({ channel: 1 });
159
+ core.addEventListener("wcs-midi:message", (e) => console.log(e.detail.type, e.detail.note));
160
+ await core.request();
161
+ core.send([0x90, 60, 100]);
162
+ // You own the lifecycle: without dispose(), the live onmidimessage handlers
163
+ // keep this instance reachable for as long as the ports are alive.
164
+ core.dispose();
165
+ ```
166
+
167
+ `parseMessage(data)` is exported separately for decoding bytes you obtained some other way.
168
+
169
+ ## Errors
170
+
171
+ Nothing throws. Failures surface as state:
172
+
173
+ | Situation | `permission` | `error` | `errorInfo.code` |
174
+ |---|---|---|---|
175
+ | `navigator.requestMIDIAccess` absent | `unsupported` | `"unsupported"` | `capability-missing` |
176
+ | User denied / policy blocked | `denied` | rejection message | `not-allowed` |
177
+ | Other access failure | `denied` | rejection message | `access-error` |
178
+ | `send()` failed | unchanged | throw message | `send-failed` |
179
+
180
+ ## Notes
181
+
182
+ - **Chromium only**, and a secure context is required.
183
+ - **Velocity 0 note-ons are note-offs** — see above.
184
+ - **`sysex` is a separate permission** and is often refused.
185
+ - **Device names are not unique**; prefer ids.
186
+ - **`statechange` can fire more than once per physical device** (the input and output sides arrive separately). `devices` is content-compared, so it is not republished when nothing actually changed.
187
+
188
+ ## License
189
+
190
+ MIT
package/dist/auto.js ADDED
@@ -0,0 +1,3 @@
1
+ import { bootstrapMidi } from "./index.esm.js";
2
+
3
+ bootstrapMidi();
@@ -0,0 +1 @@
1
+ import{bootstrapMidi}from"./index.esm.min.js";bootstrapMidi();