midiwire 0.1.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/LICENSE +21 -0
- package/README.md +845 -0
- package/dist/midiwire.es.js +1987 -0
- package/dist/midiwire.umd.js +1 -0
- package/package.json +58 -0
- package/src/bindings/DataAttributeBinder.js +198 -0
- package/src/bindings/DataAttributeBinder.test.js +825 -0
- package/src/core/EventEmitter.js +93 -0
- package/src/core/EventEmitter.test.js +357 -0
- package/src/core/MIDIConnection.js +364 -0
- package/src/core/MIDIConnection.test.js +783 -0
- package/src/core/MIDIController.js +756 -0
- package/src/core/MIDIController.test.js +1958 -0
- package/src/core/MIDIDeviceManager.js +204 -0
- package/src/core/MIDIDeviceManager.test.js +638 -0
- package/src/core/errors.js +99 -0
- package/src/index.js +181 -0
- package/src/utils/dx7.js +1294 -0
- package/src/utils/dx7.test.js +1208 -0
- package/src/utils/midi.js +244 -0
- package/src/utils/midi.test.js +260 -0
- package/src/utils/sysex.js +98 -0
- package/src/utils/sysex.test.js +222 -0
- package/src/utils/validators.js +88 -0
- package/src/utils/validators.test.js +300 -0
package/README.md
ADDED
|
@@ -0,0 +1,845 @@
|
|
|
1
|
+
# midiwire [](https://github.com/alexferl/midiwire/actions/workflows/ci.yml) [](https://codecov.io/gh/alexferl/midiwire) [](https://badge.fury.io/js/midiwire) [](https://bundlephobia.com/package/midiwire) [](https://caniuse.com/midi)
|
|
2
|
+
|
|
3
|
+
A modern, declarative JavaScript library for creating browser-based MIDI controllers. Build synth patch editors, hardware controllers, and MIDI utilities with simple HTML data attributes or a powerful programmatic API.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- 🎛️ **Declarative HTML binding** - Use `data-midi-cc` attributes for instant MIDI control
|
|
8
|
+
- 🎹 **Full Web MIDI API** - Native browser MIDI support (Chrome, Firefox, Opera)
|
|
9
|
+
- 🔌 **Bidirectional MIDI** - Send and receive MIDI messages
|
|
10
|
+
- 🎼 **SysEx support** - Send/receive System Exclusive messages for device control
|
|
11
|
+
- 🎛️ **14-bit CC support** - High-resolution MIDI (0-16383) with automatic MSB/LSB handling
|
|
12
|
+
- ⏱️ **Debouncing** - Prevent MIDI device overload with configurable debouncing
|
|
13
|
+
- 🔌 **Hotplug support** - Detect and handle device connections/disconnections
|
|
14
|
+
- 💾 **Patch management** - Save/load patches with automatic element sync and versioning
|
|
15
|
+
- 🎹 **DX7 support** - Load and create Yamaha DX7 voice (patch) banks (.syx files)
|
|
16
|
+
- 📦 **Zero dependencies** - Lightweight and fast
|
|
17
|
+
- 🔧 **Flexible API** - Works with data attributes or programmatically
|
|
18
|
+
- 🎨 **Framework agnostic** - Use with vanilla JS, React, Vue, or anything else
|
|
19
|
+
- 📝 **Fully documented** - JSDoc types for excellent IDE support
|
|
20
|
+
|
|
21
|
+
## Table of Contents
|
|
22
|
+
|
|
23
|
+
- [Installation](#installation)
|
|
24
|
+
- [Quick Start](#quick-start)
|
|
25
|
+
- [HTML Data Attributes](#html-data-attributes-easiest)
|
|
26
|
+
- [Programmatic API](#programmatic-api)
|
|
27
|
+
- [SysEx and Bidirectional MIDI](#sysex-and-bidirectional-midi)
|
|
28
|
+
- [Device Manager](#device-manager-high-level-convenience-api)
|
|
29
|
+
- [Key Features](#key-features)
|
|
30
|
+
- [Declarative Data Attributes](#declarative-data-attributes)
|
|
31
|
+
- [14-bit MIDI Control](#14-bit-midi-control)
|
|
32
|
+
- [Debouncing](#debouncing)
|
|
33
|
+
- [Custom Controls](#custom-controls-svg-knobs-canvas-etc)
|
|
34
|
+
- [Send MIDI Messages](#send-midi-messages)
|
|
35
|
+
- [Receive MIDI Messages](#receive-midi-messages)
|
|
36
|
+
- [Device Management](#device-management)
|
|
37
|
+
- [Patch Management](#patch-management)
|
|
38
|
+
- [Automatic Patch Creation](#automatic-patch-creation)
|
|
39
|
+
- [Apply Patches](#apply-patches)
|
|
40
|
+
- [Patch Storage](#patch-storage)
|
|
41
|
+
- [Advanced: Working with Settings](#advanced-working-with-settings)
|
|
42
|
+
- [Utility Functions](#utility-functions)
|
|
43
|
+
- [MIDI Note Utilities](#midi-note-utilities)
|
|
44
|
+
- [14-bit MIDI Control](#14-bit-midi-control-1)
|
|
45
|
+
- [SysEx Utilities](#sysex-utilities)
|
|
46
|
+
- [MIDI Validators](#midi-validators)
|
|
47
|
+
- [DX7 Bank Support](#dx7-bank-support)
|
|
48
|
+
- [Working with Raw Data](#working-with-raw-data)
|
|
49
|
+
- [Device Change Events](#device-change-events)
|
|
50
|
+
- [Connection Status](#connection-status)
|
|
51
|
+
- [MIDIConnection Class](#midiconnection-class-advanced)
|
|
52
|
+
- [MIDI Event Constants](#midi-event-constants)
|
|
53
|
+
- [Shorthand Aliases](#shorthand-aliases-optional)
|
|
54
|
+
- [Use Cases](#use-cases)
|
|
55
|
+
- [Browser Support](#browser-support)
|
|
56
|
+
- [Examples](#examples)
|
|
57
|
+
- [Development](#development)
|
|
58
|
+
- [License](#license)
|
|
59
|
+
- [Credits](#credits)
|
|
60
|
+
|
|
61
|
+
## Installation
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
npm install midiwire
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
Or use directly in the browser:
|
|
68
|
+
|
|
69
|
+
```html
|
|
70
|
+
<script type="module">
|
|
71
|
+
import { createMIDIController } from "./dist/midiwire.es.js";
|
|
72
|
+
</script>
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
## Quick Start
|
|
76
|
+
|
|
77
|
+
### HTML Data Attributes (Easiest)
|
|
78
|
+
|
|
79
|
+
```html
|
|
80
|
+
<!DOCTYPE html>
|
|
81
|
+
<html>
|
|
82
|
+
<body>
|
|
83
|
+
<h1>Synth Editor</h1>
|
|
84
|
+
|
|
85
|
+
<label>
|
|
86
|
+
Filter Cutoff
|
|
87
|
+
<input type="range" min="0" max="127" data-midi-cc="74">
|
|
88
|
+
</label>
|
|
89
|
+
|
|
90
|
+
<label>
|
|
91
|
+
Resonance
|
|
92
|
+
<input type="range" min="0" max="127" data-midi-cc="71">
|
|
93
|
+
</label>
|
|
94
|
+
|
|
95
|
+
<script type="module">
|
|
96
|
+
import { createMIDIController } from "midiwire";
|
|
97
|
+
|
|
98
|
+
await createMIDIController({
|
|
99
|
+
channel: 1,
|
|
100
|
+
selector: "[data-midi-cc]"
|
|
101
|
+
});
|
|
102
|
+
</script>
|
|
103
|
+
</body>
|
|
104
|
+
</html>
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
### Programmatic API
|
|
108
|
+
|
|
109
|
+
```javascript
|
|
110
|
+
import { CONTROLLER_EVENTS, createMIDIController } from "midiwire";
|
|
111
|
+
|
|
112
|
+
// Initialize
|
|
113
|
+
const midi = await createMIDIController({
|
|
114
|
+
channel: 1,
|
|
115
|
+
output: "My Synth"
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
// Bind controls manually
|
|
119
|
+
const cutoff = document.querySelector("#cutoff");
|
|
120
|
+
midi.bind(cutoff, { cc: 74, min: 0, max: 127 });
|
|
121
|
+
|
|
122
|
+
// Bind with custom onInput callback for custom controls
|
|
123
|
+
const knob = document.querySelector("#custom-knob");
|
|
124
|
+
midi.bind(knob, {
|
|
125
|
+
cc: 75,
|
|
126
|
+
min: 0,
|
|
127
|
+
max: 100,
|
|
128
|
+
onInput: (value) => {
|
|
129
|
+
// Update custom control display
|
|
130
|
+
console.log("New value:", value);
|
|
131
|
+
}
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
// Send CC directly
|
|
135
|
+
midi.sendCC(74, 64);
|
|
136
|
+
|
|
137
|
+
// Listen to events
|
|
138
|
+
midi.on(CONTROLLER_EVENTS.CC_SEND, ({ cc, value, channel }) => {
|
|
139
|
+
console.log(`CC ${cc}: ${value} on channel ${channel}`);
|
|
140
|
+
});
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
### SysEx and Bidirectional MIDI
|
|
144
|
+
|
|
145
|
+
```javascript
|
|
146
|
+
import { CONTROLLER_EVENTS, createMIDIController, parseSysEx } from "midiwire";
|
|
147
|
+
|
|
148
|
+
// Enable SysEx and connect input/output
|
|
149
|
+
const midi = await createMIDIController({
|
|
150
|
+
channel: 1,
|
|
151
|
+
sysex: true,
|
|
152
|
+
input: "My Synth",
|
|
153
|
+
output: "My Synth"
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
// Send SysEx message
|
|
157
|
+
midi.sendSysEx([0x42, 0x30, 0x00, 0x01, 0x2F, 0x12]);
|
|
158
|
+
|
|
159
|
+
// Receive SysEx messages
|
|
160
|
+
midi.on(CONTROLLER_EVENTS.SYSEX_RECV, ({ data }) => {
|
|
161
|
+
const parsed = parseSysEx(data);
|
|
162
|
+
console.log("Manufacturer ID:", parsed.manufacturerId);
|
|
163
|
+
console.log("Payload:", parsed.payload);
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
// Receive CC messages
|
|
167
|
+
midi.on(CONTROLLER_EVENTS.CC_RECV, ({ cc, value, channel }) => {
|
|
168
|
+
console.log(`Received CC ${cc}: ${value} on channel ${channel}`);
|
|
169
|
+
});
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
### Device Manager (High-Level Convenience API)
|
|
173
|
+
|
|
174
|
+
For quick prototypes and demos, use `createMIDIDeviceManager` which bundles a MIDIController with device management utilities:
|
|
175
|
+
|
|
176
|
+
```javascript
|
|
177
|
+
import { CONTROLLER_EVENTS, createMIDIDeviceManager } from "midiwire";
|
|
178
|
+
|
|
179
|
+
// Check browser support first
|
|
180
|
+
if (!navigator.requestMIDIAccess) {
|
|
181
|
+
console.error("Web MIDI API not supported in this browser.");
|
|
182
|
+
// Handle unsupported browser (e.g., Safari)
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const deviceManager = await createMIDIDeviceManager({
|
|
186
|
+
sysex: true,
|
|
187
|
+
onStatusUpdate: (message, state) => {
|
|
188
|
+
// Update UI: "Connected to: My Synth", "Error: Device not found", etc.
|
|
189
|
+
console.log(`${state}: ${message}`);
|
|
190
|
+
},
|
|
191
|
+
onConnectionUpdate: (device, midi) => {
|
|
192
|
+
// Device connected/disconnected
|
|
193
|
+
console.log("Current device:", device?.name || "None");
|
|
194
|
+
},
|
|
195
|
+
onReady: (midi) => {
|
|
196
|
+
// Setup complete
|
|
197
|
+
console.log("MIDI ready!");
|
|
198
|
+
|
|
199
|
+
// Populate device dropdowns
|
|
200
|
+
const select = document.querySelector("#device-select");
|
|
201
|
+
select.innerHTML = midi.getOutputs()
|
|
202
|
+
.map(d => `<option value="${d.id}">${d.name}</option>`)
|
|
203
|
+
.join("");
|
|
204
|
+
|
|
205
|
+
select.addEventListener("change", (e) => {
|
|
206
|
+
midi.setOutput(e.target.value);
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
// Listen for SysEx
|
|
210
|
+
midi.on(CONTROLLER_EVENTS.SYSEX_RECV, ({ data }) => {
|
|
211
|
+
console.log("Received:", data);
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
});
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
## Key Features
|
|
218
|
+
|
|
219
|
+
### Declarative Data Attributes
|
|
220
|
+
```html
|
|
221
|
+
<!-- Standard 7-bit CC -->
|
|
222
|
+
<input type="range"
|
|
223
|
+
data-midi-cc="74"
|
|
224
|
+
data-midi-channel="1"
|
|
225
|
+
data-midi-label="Filter Cutoff">
|
|
226
|
+
|
|
227
|
+
<!-- 14-bit CC (high-resolution) -->
|
|
228
|
+
<input type="range"
|
|
229
|
+
data-midi-msb="74"
|
|
230
|
+
data-midi-lsb="75"
|
|
231
|
+
data-midi-channel="1"
|
|
232
|
+
data-midi-label="Fine Pitch">
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
### 14-bit MIDI Control
|
|
236
|
+
For high-resolution MIDI control (0-16383 range), use MSB/LSB pairs:
|
|
237
|
+
|
|
238
|
+
```javascript
|
|
239
|
+
// Programmatic 14-bit CC binding
|
|
240
|
+
midi.bind(fineControl, {
|
|
241
|
+
msb: 74, // CC 74 (MSB)
|
|
242
|
+
lsb: 75, // CC 75 (LSB)
|
|
243
|
+
is14Bit: true,
|
|
244
|
+
min: 0,
|
|
245
|
+
max: 16383
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
// Or declarative with data attributes
|
|
249
|
+
<input type="range" min="0" max="16383" data-midi-msb="74" data-midi-lsb="75">
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
### Debouncing
|
|
253
|
+
Prevent MIDI device overload by adding debouncing to high-frequency controls:
|
|
254
|
+
|
|
255
|
+
```javascript
|
|
256
|
+
// Debounce for 100ms
|
|
257
|
+
midi.bind(filterSlider, { cc: 74 }, { debounce: 100 });
|
|
258
|
+
|
|
259
|
+
// With data attributes
|
|
260
|
+
<input type="range" data-midi-cc="74" data-midi-debounce="100">
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
### Custom Controls (SVG Knobs, Canvas, etc.)
|
|
264
|
+
|
|
265
|
+
For custom UI controls that don't use standard `<input>` elements, use the `onInput` callback to create bidirectional sync:
|
|
266
|
+
|
|
267
|
+
```javascript
|
|
268
|
+
// Custom SVG knob or canvas control
|
|
269
|
+
const knob = document.querySelector("#custom-knob");
|
|
270
|
+
midi.bind(knob, {
|
|
271
|
+
cc: 74,
|
|
272
|
+
min: 0,
|
|
273
|
+
max: 127,
|
|
274
|
+
onInput: (value) => {
|
|
275
|
+
// Update your custom control's visual state
|
|
276
|
+
updateKnobVisual(knob, value);
|
|
277
|
+
knob.dataset.currentValue = value;
|
|
278
|
+
}
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
// When user interacts with the knob, trigger MIDI send
|
|
282
|
+
knob.addEventListener("mousedown", (e) => {
|
|
283
|
+
// ... drag logic calculates newValue ...
|
|
284
|
+
knob.value = newValue; // Update element value
|
|
285
|
+
if (knob.onInput) {
|
|
286
|
+
knob.onInput(newValue); // Trigger MIDI send
|
|
287
|
+
}
|
|
288
|
+
});
|
|
289
|
+
```
|
|
290
|
+
|
|
291
|
+
This enables custom controls to:
|
|
292
|
+
- Send MIDI when the user interacts with them
|
|
293
|
+
- Update their visuals when MIDI is received or patches are loaded
|
|
294
|
+
- Maintain sync with external MIDI controllers
|
|
295
|
+
|
|
296
|
+
### Send MIDI Messages
|
|
297
|
+
```javascript
|
|
298
|
+
midi.sendCC(74, 100); // Control Change
|
|
299
|
+
midi.sendNoteOn(60, 100); // Note On
|
|
300
|
+
midi.sendNoteOff(60); // Note Off
|
|
301
|
+
midi.sendSysEx([0x42, 0x30, ...]); // System Exclusive
|
|
302
|
+
```
|
|
303
|
+
|
|
304
|
+
### Receive MIDI Messages
|
|
305
|
+
```javascript
|
|
306
|
+
import { CONTROLLER_EVENTS } from "midiwire";
|
|
307
|
+
|
|
308
|
+
// Control Change (received from MIDI device)
|
|
309
|
+
midi.on(CONTROLLER_EVENTS.CC_RECV, ({ cc, value, channel }) => {
|
|
310
|
+
// Handle incoming CC
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
// SysEx messages
|
|
314
|
+
midi.on(CONTROLLER_EVENTS.SYSEX_RECV, ({ data }) => {
|
|
315
|
+
// Handle incoming SysEx
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
// Note messages
|
|
319
|
+
midi.on(CONTROLLER_EVENTS.NOTE_ON_RECV, ({ note, velocity, channel }) => {
|
|
320
|
+
// Handle incoming note on
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
midi.on(CONTROLLER_EVENTS.NOTE_OFF_RECV, ({ note, channel }) => {
|
|
324
|
+
// Handle incoming note off
|
|
325
|
+
});
|
|
326
|
+
```
|
|
327
|
+
|
|
328
|
+
### Device Management
|
|
329
|
+
```javascript
|
|
330
|
+
// List devices
|
|
331
|
+
const outputs = midi.getOutputs();
|
|
332
|
+
const inputs = midi.getInputs();
|
|
333
|
+
|
|
334
|
+
// Switch devices
|
|
335
|
+
await midi.setOutput("My Synth");
|
|
336
|
+
await midi.connectInput("My Synth");
|
|
337
|
+
|
|
338
|
+
// Get current devices
|
|
339
|
+
midi.getCurrentOutput();
|
|
340
|
+
midi.getCurrentInput();
|
|
341
|
+
```
|
|
342
|
+
|
|
343
|
+
### Patch Management
|
|
344
|
+
|
|
345
|
+
Save, load, and organize synth patches with automatic element synchronization.
|
|
346
|
+
|
|
347
|
+
#### Automatic Patch Creation
|
|
348
|
+
|
|
349
|
+
```javascript
|
|
350
|
+
// Create a patch from current state (includes all CC values and control settings)
|
|
351
|
+
const patch = midi.getPatch("My Awesome Sound");
|
|
352
|
+
console.log(patch);
|
|
353
|
+
// {
|
|
354
|
+
// name: "My Awesome Sound",
|
|
355
|
+
// device: "My Synth",
|
|
356
|
+
// timestamp: "2026-01-14T...",
|
|
357
|
+
// version: "1.0",
|
|
358
|
+
// channels: {
|
|
359
|
+
// "1": { ccs: { "74": 100, "71": 64 }, notes: {} }
|
|
360
|
+
// },
|
|
361
|
+
// settings: {
|
|
362
|
+
// "cc74": {
|
|
363
|
+
// min: 20,
|
|
364
|
+
// max: 20000,
|
|
365
|
+
// invert: false,
|
|
366
|
+
// is14Bit: false,
|
|
367
|
+
// label: "Filter Cutoff", // From data-midi-label
|
|
368
|
+
// elementId: "cutoff-slider" // From element id
|
|
369
|
+
// }
|
|
370
|
+
// }
|
|
371
|
+
// }
|
|
372
|
+
```
|
|
373
|
+
|
|
374
|
+
#### Apply Patches
|
|
375
|
+
|
|
376
|
+
When applying a patch with `setPatch()`, midiwire automatically:
|
|
377
|
+
- Sends all CC values to your MIDI device
|
|
378
|
+
- Updates bound control elements to match the saved values
|
|
379
|
+
- Converts MIDI values (0-127) back to element ranges (respecting min/max)
|
|
380
|
+
- Handles inverted controls
|
|
381
|
+
- Dispatches input events to trigger any UI updates
|
|
382
|
+
|
|
383
|
+
```javascript
|
|
384
|
+
// Load and apply a patch
|
|
385
|
+
const loaded = midi.loadPatch("My Awesome Sound");
|
|
386
|
+
if (loaded) {
|
|
387
|
+
await midi.setPatch(loaded);
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
// Or apply a patch you created
|
|
391
|
+
await midi.setPatch({
|
|
392
|
+
name: "Manual Voice",
|
|
393
|
+
channels: {
|
|
394
|
+
"1": {
|
|
395
|
+
ccs: {
|
|
396
|
+
"74": 100, // Filter cutoff
|
|
397
|
+
"71": 64 // Resonance
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
// Settings are optional - element configs are used if not provided
|
|
402
|
+
});
|
|
403
|
+
```
|
|
404
|
+
|
|
405
|
+
#### Patch Storage
|
|
406
|
+
|
|
407
|
+
```javascript
|
|
408
|
+
// Save to localStorage (persists between sessions)
|
|
409
|
+
midi.savePatch("My Awesome Sound");
|
|
410
|
+
|
|
411
|
+
// List all saved patches
|
|
412
|
+
const allPatches = midi.listPatches();
|
|
413
|
+
// [{ name: "My Awesome Sound", patch: {...} }, ...]
|
|
414
|
+
|
|
415
|
+
// Delete a patch
|
|
416
|
+
midi.deletePatch("My Awesome Sound");
|
|
417
|
+
|
|
418
|
+
// Export/import patches (for sharing or backup)
|
|
419
|
+
const patchData = JSON.stringify(midi.getPatch("My Sound"));
|
|
420
|
+
// Send to server, download as file, etc.
|
|
421
|
+
|
|
422
|
+
// Import and apply
|
|
423
|
+
const imported = JSON.parse(patchData);
|
|
424
|
+
await midi.setPatch(imported);
|
|
425
|
+
```
|
|
426
|
+
|
|
427
|
+
#### Advanced: Working with Settings
|
|
428
|
+
|
|
429
|
+
Settings store the configuration of your controls, allowing patches to restore:
|
|
430
|
+
- Custom min/max ranges (e.g., frequency in Hz)
|
|
431
|
+
- Inverted controls (e.g., resonance on some synths)
|
|
432
|
+
- Channel assignments
|
|
433
|
+
- 14-bit CC configurations
|
|
434
|
+
|
|
435
|
+
```javascript
|
|
436
|
+
// Bind a control with custom range
|
|
437
|
+
midi.bind(filterSlider, {
|
|
438
|
+
cc: 74,
|
|
439
|
+
min: 20, // 20 Hz
|
|
440
|
+
max: 20000, // 20 kHz
|
|
441
|
+
channel: 1
|
|
442
|
+
});
|
|
443
|
+
|
|
444
|
+
// Save the complete configuration
|
|
445
|
+
midi.savePatch("Bass Voice");
|
|
446
|
+
|
|
447
|
+
// Later: load and everything is restored correctly
|
|
448
|
+
const bassPatch = midi.loadPatch("Bass Voice");
|
|
449
|
+
await midi.setPatch(bassPatch); // Slider shows frequency, not 0-127
|
|
450
|
+
```
|
|
451
|
+
|
|
452
|
+
### Utility Functions
|
|
453
|
+
|
|
454
|
+
midiwire includes comprehensive utility functions for MIDI data manipulation:
|
|
455
|
+
|
|
456
|
+
#### MIDI Note Utilities
|
|
457
|
+
|
|
458
|
+
```javascript
|
|
459
|
+
import { frequencyToNote, noteNameToNumber, noteNumberToName, noteToFrequency } from "midiwire";
|
|
460
|
+
|
|
461
|
+
// Convert note names to MIDI numbers
|
|
462
|
+
const midiNote = noteNameToNumber("C4"); // 60
|
|
463
|
+
const noteName = noteNumberToName(60); // "C4"
|
|
464
|
+
|
|
465
|
+
// Frequency conversions
|
|
466
|
+
const freq = noteToFrequency(69); // 440.00 Hz (A4)
|
|
467
|
+
const noteFromFreq = frequencyToNote(440); // 69
|
|
468
|
+
```
|
|
469
|
+
|
|
470
|
+
#### 14-bit MIDI Control
|
|
471
|
+
|
|
472
|
+
Functions for working with high-resolution (0-16383) MIDI values:
|
|
473
|
+
|
|
474
|
+
```javascript
|
|
475
|
+
import { encode14BitValue, decode14BitValue, denormalize14BitValue } from "midiwire";
|
|
476
|
+
|
|
477
|
+
// Encode 14-bit value to MSB/LSB
|
|
478
|
+
const { msb, lsb } = encode14BitValue(8192); // Center value
|
|
479
|
+
console.log(msb, lsb); // 64, 0
|
|
480
|
+
|
|
481
|
+
// Decode MSB/LSB back to 14-bit
|
|
482
|
+
const value = decode14BitValue(64, 0); // 8192
|
|
483
|
+
|
|
484
|
+
// Convert MIDI value back to custom range
|
|
485
|
+
const frequency = denormalize14BitValue(8192, 20, 20000); // 10010 Hz
|
|
486
|
+
```
|
|
487
|
+
|
|
488
|
+
#### SysEx Utilities
|
|
489
|
+
|
|
490
|
+
Create and manipulate System Exclusive messages:
|
|
491
|
+
|
|
492
|
+
```javascript
|
|
493
|
+
import { createSysEx, decode7Bit, encode7Bit, isSysEx } from "midiwire";
|
|
494
|
+
|
|
495
|
+
// Create SysEx message
|
|
496
|
+
createSysEx(0x43, [0x20, 0x7F, 0x1C]);
|
|
497
|
+
// Returns: [0xF0, 0x43, 0x20, 0x7F, 0x1C, 0xF7]
|
|
498
|
+
|
|
499
|
+
// Check if data is SysEx
|
|
500
|
+
isSysEx([0xF0, 0x43, 0xF7]); // true
|
|
501
|
+
|
|
502
|
+
// Encode/decode 8-bit data to 7-bit MIDI format
|
|
503
|
+
const encoded = encode7Bit([0xFF, 0xFE, 0xFD]);
|
|
504
|
+
const decoded = decode7Bit(encoded);
|
|
505
|
+
```
|
|
506
|
+
|
|
507
|
+
#### MIDI Validators
|
|
508
|
+
|
|
509
|
+
Validate MIDI parameters before use:
|
|
510
|
+
|
|
511
|
+
```javascript
|
|
512
|
+
import { isValidCC, isValidChannel, isValidMIDIValue, isValidNote } from "midiwire";
|
|
513
|
+
|
|
514
|
+
// Validate MIDI parameters
|
|
515
|
+
if (isValidChannel(channel)) {
|
|
516
|
+
midi.sendCC(cc, value);
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
// All validation functions
|
|
520
|
+
isValidChannel(1) // true (1-16)
|
|
521
|
+
isValidCC(74) // true (0-127)
|
|
522
|
+
isValid14BitCC(31) // true (0-31 for MSB)
|
|
523
|
+
isValidMIDIValue(100) // true (0-127)
|
|
524
|
+
isValidNote(60) // true (0-127)
|
|
525
|
+
isValidVelocity(100) // true (0-127)
|
|
526
|
+
isValidProgramChange(5) // true (0-127)
|
|
527
|
+
isValidPitchBend(8192) // true (0-16383)
|
|
528
|
+
```
|
|
529
|
+
|
|
530
|
+
See the [API documentation](https://github.com/alexferl/midiwire) for complete utility function reference.
|
|
531
|
+
|
|
532
|
+
### DX7 Bank Support
|
|
533
|
+
|
|
534
|
+
Load, create, and manipulate Yamaha DX7 voice (patch) banks (.syx files):
|
|
535
|
+
|
|
536
|
+
```javascript
|
|
537
|
+
import { DX7Bank, DX7Voice } from "midiwire";
|
|
538
|
+
|
|
539
|
+
// Load a bank from a file
|
|
540
|
+
const bank = await DX7Bank.fromFile(fileInput.files[0]);
|
|
541
|
+
|
|
542
|
+
// Get all voices
|
|
543
|
+
const voices = bank.getVoices();
|
|
544
|
+
console.log(`Loaded ${voices.length} voices`);
|
|
545
|
+
|
|
546
|
+
// Get a specific voice
|
|
547
|
+
const voice = bank.getVoice(0);
|
|
548
|
+
console.log("Voice name:", voice.name);
|
|
549
|
+
|
|
550
|
+
// Read parameters (0-127 range)
|
|
551
|
+
const algorithm = voice.getParameter(110); // Algorithm 1-32
|
|
552
|
+
const feedback = voice.getParameter(111); // Feedback 0-7
|
|
553
|
+
const lfoSpeed = voice.getParameter(112); // LFO speed
|
|
554
|
+
|
|
555
|
+
// Create a new voice
|
|
556
|
+
const newPatch = DX7Voice.createDefault();
|
|
557
|
+
newPatch.setParameter(0, 50); // EG Rate 1
|
|
558
|
+
newPatch.setParameter(110, 5); // Algorithm 6
|
|
559
|
+
|
|
560
|
+
// Set voice name (10 characters max)
|
|
561
|
+
const name = "SUPER BASS";
|
|
562
|
+
for (let i = 0; i < name.length; i++) {
|
|
563
|
+
newPatch.setParameter(118 + i, name.charCodeAt(i));
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
// Replace voice in bank
|
|
567
|
+
bank.replaceVoice(0, newPatch);
|
|
568
|
+
|
|
569
|
+
// Find voices by name
|
|
570
|
+
const bassPatch = bank.findVoiceByName("BASS");
|
|
571
|
+
if (bassPatch) {
|
|
572
|
+
console.log(`Found "${bassPatch.name}" at index ${bassPatch.index}`);
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
// Export to SYX format
|
|
576
|
+
const sysexData = bank.toSysex();
|
|
577
|
+
const blob = new Blob([sysexData], { type: "application/octet-stream" });
|
|
578
|
+
|
|
579
|
+
// Unpack to 155-byte format (for detailed parameter access)
|
|
580
|
+
const unpacked = voice.unpack();
|
|
581
|
+
// unpacked[0] = OP1 EG Rate 1
|
|
582
|
+
// unpacked[4] = OP1 EG Level 1
|
|
583
|
+
// ... full DX7 parameter set
|
|
584
|
+
|
|
585
|
+
// Access parameters in unpacked format (0-168 range)
|
|
586
|
+
const unpackedAlgorithm = voice.getUnpackedParameter(146); // Algorithm 1-32
|
|
587
|
+
const operator1Level = voice.getUnpackedParameter(16); // OP1 output level
|
|
588
|
+
|
|
589
|
+
// Create voice from unpacked data
|
|
590
|
+
const customUnpacked = new Uint8Array(169);
|
|
591
|
+
// ... fill with your parameters ...
|
|
592
|
+
const customVoice = DX7Voice.fromUnpacked(customUnpacked);
|
|
593
|
+
|
|
594
|
+
// Export single voice to SYX format (VCED)
|
|
595
|
+
const singleVoiceSysex = voice.toSysEx();
|
|
596
|
+
// Useful for synths that only accept single voice dumps (e.g., KORG Volca FM)
|
|
597
|
+
|
|
598
|
+
// Add voice to first empty slot in bank
|
|
599
|
+
const newBank = new DX7Bank();
|
|
600
|
+
const slotIndex = newBank.addVoice(customVoice);
|
|
601
|
+
console.log(`Added voice to slot ${slotIndex}`);
|
|
602
|
+
|
|
603
|
+
// Convert to JSON for storage or transmission
|
|
604
|
+
const voiceJSON = voice.toJSON();
|
|
605
|
+
const bankJSON = bank.toJSON();
|
|
606
|
+
console.log(voiceJSON.name); // Voice name
|
|
607
|
+
console.log(bankJSON.voices[0].name); // First voice name
|
|
608
|
+
```
|
|
609
|
+
|
|
610
|
+
### Working with Raw Data
|
|
611
|
+
|
|
612
|
+
For advanced use cases, work directly with packed/unpacked formats:
|
|
613
|
+
|
|
614
|
+
```javascript
|
|
615
|
+
// Pack unpacked data (169 bytes) to DX7 format (128 bytes)
|
|
616
|
+
const unpackedData = new Uint8Array(169);
|
|
617
|
+
// ... fill with parameters ...
|
|
618
|
+
const packedData = DX7Voice.pack(unpackedData);
|
|
619
|
+
|
|
620
|
+
// Create voice from packed data
|
|
621
|
+
const voiceFromPacked = new DX7Voice(packedData, 0);
|
|
622
|
+
```
|
|
623
|
+
|
|
624
|
+
See [`examples/dx7.html`](examples/dx7.html) for a working demo with file upload, voice visualization, and export.
|
|
625
|
+
|
|
626
|
+
### Device Change Events
|
|
627
|
+
|
|
628
|
+
midiwire detects when MIDI devices are connected or disconnected:
|
|
629
|
+
|
|
630
|
+
```javascript
|
|
631
|
+
import { CONNECTION_EVENTS, createMIDIController } from "midiwire";
|
|
632
|
+
|
|
633
|
+
const midi = await createMIDIController({ ... });
|
|
634
|
+
|
|
635
|
+
// Listen for all device changes
|
|
636
|
+
midi.connection.on(CONNECTION_EVENTS.DEVICE_CHANGE, ({ port, state, type, device }) => {
|
|
637
|
+
console.log(`${device.name} ${type} ${state}`);
|
|
638
|
+
});
|
|
639
|
+
|
|
640
|
+
// Specific device events
|
|
641
|
+
midi.connection.on(CONNECTION_EVENTS.INPUT_DEVICE_CONNECTED, ({ device }) => {
|
|
642
|
+
console.log("Input connected:", device.name);
|
|
643
|
+
});
|
|
644
|
+
|
|
645
|
+
midi.connection.on(CONNECTION_EVENTS.INPUT_DEVICE_DISCONNECTED, ({ device }) => {
|
|
646
|
+
console.log("Input disconnected:", device.name);
|
|
647
|
+
});
|
|
648
|
+
|
|
649
|
+
midi.connection.on(CONNECTION_EVENTS.OUTPUT_DEVICE_CONNECTED, ({ device }) => {
|
|
650
|
+
console.log("Output connected:", device.name);
|
|
651
|
+
});
|
|
652
|
+
|
|
653
|
+
midi.connection.on(CONNECTION_EVENTS.OUTPUT_DEVICE_DISCONNECTED, ({ device }) => {
|
|
654
|
+
console.log("Output disconnected:", device.name);
|
|
655
|
+
});
|
|
656
|
+
```
|
|
657
|
+
|
|
658
|
+
### Connection Status
|
|
659
|
+
|
|
660
|
+
Check if MIDI connection is established before sending messages:
|
|
661
|
+
|
|
662
|
+
```javascript
|
|
663
|
+
// Check if output is connected before sending
|
|
664
|
+
if (midi.connection.isConnected()) {
|
|
665
|
+
midi.sendCC(74, 100);
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
// Check connection status
|
|
669
|
+
const status = {
|
|
670
|
+
output: midi.getCurrentOutput(),
|
|
671
|
+
input: midi.getCurrentInput(),
|
|
672
|
+
isConnected: midi.connection.isConnected()
|
|
673
|
+
};
|
|
674
|
+
|
|
675
|
+
// Get connection instance for advanced usage
|
|
676
|
+
const connection = midi.connection;
|
|
677
|
+
connection.send([0x90, 60, 100]); // Send raw MIDI bytes
|
|
678
|
+
```
|
|
679
|
+
|
|
680
|
+
For bidirectional MIDI, ensure both input and output are connected:
|
|
681
|
+
|
|
682
|
+
```javascript
|
|
683
|
+
// Full duplex MIDI
|
|
684
|
+
const midi = await createMIDIController({
|
|
685
|
+
input: "My Synth",
|
|
686
|
+
output: "My Synth"
|
|
687
|
+
});
|
|
688
|
+
|
|
689
|
+
// Then send/receive will work bidirectionally
|
|
690
|
+
midi.sendCC(74, 100); // Send to synth
|
|
691
|
+
// MIDI sent from synth knobs will trigger CC_RECV events
|
|
692
|
+
```
|
|
693
|
+
|
|
694
|
+
### MIDIConnection Class (Advanced)
|
|
695
|
+
|
|
696
|
+
For low-level MIDI access, use the `MIDIConnection` class accessible via `midi.connection`:
|
|
697
|
+
|
|
698
|
+
```javascript
|
|
699
|
+
import { CONNECTION_EVENTS, createMIDIController } from "midiwire";
|
|
700
|
+
|
|
701
|
+
const midi = await createMIDIController({ sysex: true });
|
|
702
|
+
const connection = midi.connection;
|
|
703
|
+
|
|
704
|
+
// Get all available devices
|
|
705
|
+
const outputs = connection.getOutputs();
|
|
706
|
+
const inputs = connection.getInputs();
|
|
707
|
+
console.log('Available outputs:', outputs);
|
|
708
|
+
console.log('Available inputs:', inputs);
|
|
709
|
+
|
|
710
|
+
// Connect to specific devices
|
|
711
|
+
await connection.connect("My Synth"); // By name
|
|
712
|
+
await connection.connect(0); // By index
|
|
713
|
+
await connection.connectInput("My Synth", (event) => {
|
|
714
|
+
console.log('MIDI message received:', event.data);
|
|
715
|
+
});
|
|
716
|
+
|
|
717
|
+
// Send raw MIDI messages
|
|
718
|
+
connection.send([0x90, 60, 100]); // Note on
|
|
719
|
+
connection.send([0x80, 60, 0]); // Note off
|
|
720
|
+
|
|
721
|
+
// Send SysEx messages
|
|
722
|
+
connection.sendSysEx([0x43, 0x20, 0x7F, 0x1C]);
|
|
723
|
+
|
|
724
|
+
// Check connection status
|
|
725
|
+
if (connection.isConnected()) {
|
|
726
|
+
console.log('Connected to:', connection.getCurrentOutput().name);
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
// Get current device info
|
|
730
|
+
const outputInfo = connection.getCurrentOutput();
|
|
731
|
+
const inputInfo = connection.getCurrentInput();
|
|
732
|
+
|
|
733
|
+
// Disconnect devices
|
|
734
|
+
connection.disconnect();
|
|
735
|
+
```
|
|
736
|
+
|
|
737
|
+
### MIDI Event Constants
|
|
738
|
+
```javascript
|
|
739
|
+
import { CONTROLLER_EVENTS, CONNECTION_EVENTS } from "midiwire";
|
|
740
|
+
|
|
741
|
+
// Controller events (from MIDIController):
|
|
742
|
+
CONTROLLER_EVENTS.READY // "ready" - MIDI initialized
|
|
743
|
+
CONTROLLER_EVENTS.ERROR // "error" - Error occurred
|
|
744
|
+
CONTROLLER_EVENTS.CC_SEND // "cc-send" - CC sent
|
|
745
|
+
CONTROLLER_EVENTS.CC_RECV // "cc-recv" - CC received
|
|
746
|
+
CONTROLLER_EVENTS.NOTE_ON_SEND // "note-on-send" - Note On sent
|
|
747
|
+
CONTROLLER_EVENTS.NOTE_ON_RECV // "note-on-recv" - Note On received
|
|
748
|
+
CONTROLLER_EVENTS.NOTE_OFF_SEND // "note-off-send" - Note Off sent
|
|
749
|
+
CONTROLLER_EVENTS.NOTE_OFF_RECV // "note-off-recv" - Note Off received
|
|
750
|
+
CONTROLLER_EVENTS.SYSEX_SEND // "sysex-send" - SysEx sent
|
|
751
|
+
CONTROLLER_EVENTS.SYSEX_RECV // "sysex-recv" - SysEx received
|
|
752
|
+
CONTROLLER_EVENTS.OUTPUT_CHANGED // "output-changed" - Output device changed
|
|
753
|
+
CONTROLLER_EVENTS.INPUT_CONNECTED // "input-connected" - Input device connected
|
|
754
|
+
CONTROLLER_EVENTS.DESTROYED // "destroyed" - MIDI controller destroyed
|
|
755
|
+
CONTROLLER_EVENTS.MIDI_MSG // "midi-msg" - Raw MIDI message
|
|
756
|
+
CONTROLLER_EVENTS.PATCH_SAVED // "patch-saved" - Patch saved to storage
|
|
757
|
+
CONTROLLER_EVENTS.PATCH_LOADED // "patch-loaded" - Patch loaded/applied
|
|
758
|
+
CONTROLLER_EVENTS.PATCH_DELETED // "patch-deleted" - Patch deleted from storage
|
|
759
|
+
|
|
760
|
+
// Connection events (from MIDIConnection):
|
|
761
|
+
CONNECTION_EVENTS.DEVICE_CHANGE // "device-change" - Any device change
|
|
762
|
+
CONNECTION_EVENTS.INPUT_DEVICE_CONNECTED // "input-device-connected"
|
|
763
|
+
CONNECTION_EVENTS.INPUT_DEVICE_DISCONNECTED // "input-device-disconnected"
|
|
764
|
+
CONNECTION_EVENTS.OUTPUT_DEVICE_CONNECTED // "output-device-connected"
|
|
765
|
+
CONNECTION_EVENTS.OUTPUT_DEVICE_DISCONNECTED // "output-device-disconnected"
|
|
766
|
+
```
|
|
767
|
+
|
|
768
|
+
#### Shorthand Aliases (Optional)
|
|
769
|
+
|
|
770
|
+
For cleaner code, use the shorthand aliases:
|
|
771
|
+
|
|
772
|
+
```javascript
|
|
773
|
+
import { CTRL, CONN } from "midiwire";
|
|
774
|
+
|
|
775
|
+
// Same events, shorter names
|
|
776
|
+
midi.on(CTRL.CC_SEND, handler);
|
|
777
|
+
midi.connection.on(CONN.DEVICE_CHANGE, handler);
|
|
778
|
+
|
|
779
|
+
// Real-world example
|
|
780
|
+
midi.on(CTRL.ERROR, ({ message }) => {
|
|
781
|
+
console.error("MIDI Error:", message);
|
|
782
|
+
});
|
|
783
|
+
|
|
784
|
+
midi.on(CTRL.PATCH_LOADED, ({ patch }) => {
|
|
785
|
+
console.log(`Loaded patch: ${patch.name}`);
|
|
786
|
+
});
|
|
787
|
+
```
|
|
788
|
+
|
|
789
|
+
## Use Cases
|
|
790
|
+
|
|
791
|
+
- 🎹 **Synth patch editors** - Control hardware synths from your browser
|
|
792
|
+
- 🎚️ **MIDI controllers** - Build custom web-based MIDI controllers
|
|
793
|
+
- 📊 **Parameter automation** - Record and playback MIDI CC changes
|
|
794
|
+
- 🔧 **Device configuration** - Use SysEx to configure MIDI hardware
|
|
795
|
+
- 🎵 **Educational tools** - Teach MIDI concepts with interactive demos
|
|
796
|
+
- 🎛️ **DAW integration** - Control DAW parameters from web interfaces
|
|
797
|
+
|
|
798
|
+
## Browser Support
|
|
799
|
+
|
|
800
|
+
Requires browsers with [Web MIDI API](https://caniuse.com/midi) support:
|
|
801
|
+
- ✅ Chrome/Edge 43+
|
|
802
|
+
- ✅ Firefox 108+
|
|
803
|
+
- ✅ Opera 30+
|
|
804
|
+
- ❌ Safari (not supported)
|
|
805
|
+
|
|
806
|
+
**Note:** SysEx requires explicit user permission in Chrome.
|
|
807
|
+
|
|
808
|
+
## Examples
|
|
809
|
+
|
|
810
|
+
Check out the [`examples/`](examples) folder for working demos:
|
|
811
|
+
- [`template.html`](examples/template.html) - Quick-start template for rapid prototyping (start here!)
|
|
812
|
+
- [`basic.html`](examples/basic.html) - Simple CC control with data attributes
|
|
813
|
+
- [`advanced.html`](examples/advanced.html) - All features showcase (ranges, inversion, 14-bit, debouncing)
|
|
814
|
+
- [`programmatic.html`](examples/programmatic.html) - Manual binding and custom SVG/canvas controls
|
|
815
|
+
- [`patches.html`](examples/patches.html) - Complete patch management system with localStorage
|
|
816
|
+
- [`sysex.html`](examples/sysex.html) - SysEx communication and device inquiry
|
|
817
|
+
- [`dx7.html`](examples/dx7.html) - Load and create Yamaha DX7 voice banks
|
|
818
|
+
|
|
819
|
+
## Development
|
|
820
|
+
|
|
821
|
+
```bash
|
|
822
|
+
# Install dependencies
|
|
823
|
+
npm install
|
|
824
|
+
|
|
825
|
+
# Start dev server with examples
|
|
826
|
+
npm run dev
|
|
827
|
+
|
|
828
|
+
# Build for production
|
|
829
|
+
npm run build
|
|
830
|
+
|
|
831
|
+
# Run tests
|
|
832
|
+
npm test
|
|
833
|
+
|
|
834
|
+
# Lint
|
|
835
|
+
npm run lint
|
|
836
|
+
```
|
|
837
|
+
|
|
838
|
+
## License
|
|
839
|
+
|
|
840
|
+
[MIT](LICENSE)
|
|
841
|
+
|
|
842
|
+
## Credits
|
|
843
|
+
|
|
844
|
+
- Inspired by [synthmata/ccynthmata](https://github.com/synthmata/ccynthmata).
|
|
845
|
+
- DX7 implementation based on the work of [asb2m10/dexed](https://github.com/asb2m10/dexed) and various DX7 SysEx documentation resources.
|