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/README.md ADDED
@@ -0,0 +1,845 @@
1
+ # midiwire [![CI](https://github.com/alexferl/midiwire/actions/workflows/ci.yml/badge.svg)](https://github.com/alexferl/midiwire/actions/workflows/ci.yml) [![codecov](https://codecov.io/gh/alexferl/midiwire/branch/master/graph/badge.svg)](https://codecov.io/gh/alexferl/midiwire) [![npm version](https://badge.fury.io/js/midiwire.svg)](https://badge.fury.io/js/midiwire) [![Bundle Size](https://img.shields.io/bundlephobia/minzip/midiwire.svg)](https://bundlephobia.com/package/midiwire) [![Web MIDI API](https://img.shields.io/badge/Web%20MIDI-API%20Support-orange.svg)](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.