Skip to content

Web MIDI Protocol Engine

pyoneerC edited this page Sep 13, 2026 · 6 revisions

Web MIDI Protocol Engine

The bipluk.com frontend leverages the W3C Web MIDI API specification to achieve native, driverless hardware synthesizer communication directly from modern web browsers. This document outlines the technical design, security constraints, baud rate physics, and hardware timing requirements for reliable SysEx streaming.


1. Physical Layer Serial Line Physics

The standard MIDI 1.0 physical layer operates as an asynchronous, optically-isolated current-loop serial transmission system.

Fundamental Timing Equations

$$\text{Baud Rate } f_{\text{baud}} = 31{,}250 \text{ symbols/sec (bps)} \ (\pm 1%)$$

The elementary bit duration $\tau_{\text{bit}}$ is defined as:

$$\tau_{\text{bit}} = \frac{1}{f_{\text{baud}}} = \frac{1}{31{,}250 \text{ Hz}} = 3.200 \times 10^{-5} \text{ s} = 32.0 \ \mu\text{s}$$

Every standard MIDI 1.0 serial frame encodes 1 byte across a 10-bit asynchronous framing envelope:

  • $1$ Start Bit (logical low / $0 \text{ mA}$)
  • $8$ Data Bits (least significant bit first)
  • $1$ Stop Bit (logical high / $5 \text{ mA}$)

The total duration required to physically serialize an individual byte $\tau_{\text{byte}}$ is:

$$\tau_{\text{byte}} = 10 \times \tau_{\text{bit}} = 10 \times 32.0 \ \mu\text{s} = 320.0 \ \mu\text{s}$$

The maximum theoretical wire-speed throughput $R_{\text{max}}$ is:

$$R_{\text{max}} = \frac{1}{\tau_{\text{byte}}} = \frac{1}{0.000320 \text{ s}} = 3{,}125 \text{ bytes/second} \approx 3.0518 \text{ KiB/s}$$


2. Hardware FIFO Queue Dynamics & Overflow Modeling

flowchart TD
    In["Input Raw SysEx (.syx)"] --> Val["Validate F0..F7 Boundaries"]
    Val --> Chk["Verify Checksum Integrity"]
    Chk --> Queue["Split into Chunks (e.g. 128 to 256 bytes)"]

    subgraph StreamingEngine ["Chunk Transmission Loop"]
        Queue --> Pop["Dequeue Current Chunk"]
        Pop --> Out["Send Chunk via midiOutput.send(chunk)"]
        Out --> Timer["Hardware Rest Interval (30ms to 60ms)"]
        Timer --> CheckMore{"Remaining Chunks?"}
        CheckMore -- "Yes" --> Pop
        CheckMore -- "No" --> Complete["Trigger OnComplete Callback"]
    end

    Complete --> UI["Update UI: Voice Successfully Transferred"]
Loading

Buffer Overflow Differential Model

Let $Q(t)$ denote the instantaneous number of unread bytes residing within the synthesizer hardware UART FIFO buffer of physical capacity $Q_{\text{max}}$ (typically $16 \le Q_{\text{max}} \le 64$ on 1980s microprocessors).

The rate of queue accumulation is governed by:

$$\frac{dQ(t)}{dt} = R_{\text{in}}(t) - R_{\text{proc}}(t)$$

where:

  • $R_{\text{in}}(t) = 3{,}125 \text{ bytes/s}$ when the USB host is actively streaming bytes into the 5-pin DIN interface.
  • $R_{\text{proc}}(t)$ is the rate at which the synth CPU drains the FIFO via interrupt service routines (ISR).

When the instrument CPU executes heavy background routines (e.g., refreshing multiple 7-segment displays, scanning the keyboard matrix, or recalculating digital filter tables), $R_{\text{proc}}$ drops to zero for intervals $\Delta t_{\text{stall}}$:

$$\Delta t_{\text{stall}} > \frac{Q_{\text{max}}}{R_{\text{in}}} \implies Q(t) > Q_{\text{max}}$$

For an early 16-byte buffer:

$$\Delta t_{\text{critical}} = \frac{16 \text{ bytes}}{3{,}125 \text{ bytes/s}} = 5.12 \text{ milliseconds}$$

A CPU processing stall exceeding just 5.12 milliseconds causes permanent byte loss and invalidates the entire sound bank.

Optimal Inter-Chunk Pacing Formula

To guarantee zero buffer overflows, bipluk.com partitions a payload of $N$ bytes into chunks of size $S_{\text{chunk}}$, introducing an inter-chunk sleep delay $\Delta t_{\text{delay}}$ satisfying:

$$\Delta t_{\text{delay}} \ge \frac{S_{\text{chunk}}}{R_{\text{max}}} \cdot \alpha + \tau_{\text{display-refresh}}$$

where $\alpha \ge 1.25$ represents the CPU safety margin and $\tau_{\text{display-refresh}} \approx 20\text{ms}$ is the maximum display refresh latency. For $S_{\text{chunk}} = 128$:

$$\Delta t_{\text{delay}} \ge \left( \frac{128}{3125} \times 1.25 + 0.020 \right) \text{ s} \approx 51.2\text{ms} + 20\text{ms} \approx 40\text{ms to } 60\text{ms}$$


3. Web MIDI Permission State Machine

Note

Elevated Browser Privileges: Unlike standard MIDI Note On/Off commands, System Exclusive (SysEx) messaging requires explicit user authorization via browser security prompts because SysEx commands can modify non-volatile flash firmware and sound presets.

Important

Exclusive MIDI Driver Locks (Windows): On Windows systems, most USB-MIDI drivers do not support multi-client access. If a desktop DAW (such as Ableton Live, FL Studio, Cubase, or Studio One) is actively running, it may monopolize the MIDI port. Close competing audio applications before initiating Web MIDI connections in bipluk.com.

stateDiagram-v2
    [*] --> RequestAccess: navigator.requestMIDIAccess({ sysex: true })
    RequestAccess --> PermissionPrompt: Browser prompts user
    PermissionPrompt --> AccessGranted: User clicks "Allow"
    PermissionPrompt --> AccessDenied: User clicks "Block"
    
    AccessDenied --> ManualResolution: Direct user to chrome://settings/content/midiDevices
    ManualResolution --> RequestAccess: User resets permission

    AccessGranted --> EnumeratePorts: midiAccess.inputs / midiAccess.outputs
    EnumeratePorts --> PortConnected: Output port selected & ready
    PortConnected --> Transmitting: Stream throttled SysEx chunks
    Transmitting --> PortConnected: Transmission complete
Loading

4. Production JavaScript Implementation

/**
 * Paced SysEx transmission controller for vintage synthesizers.
 */
class HardwarePacedSender {
  constructor(midiOutput, options = {}) {
    this.output = midiOutput;
    this.chunkSize = options.chunkSize || 256;      // Maximum bytes per burst
    this.packetDelayMs = options.packetDelayMs || 40; // Sleep between chunks in ms
  }

  async send(dataBuffer, onProgress = null) {
    const totalBytes = dataBuffer.length;
    let offset = 0;

    while (offset < totalBytes) {
      const end = Math.min(offset + this.chunkSize, totalBytes);
      const chunk = dataBuffer.slice(offset, end);

      // Transmit to hardware
      this.output.send(chunk);
      offset = end;

      if (onProgress) {
        onProgress(Math.round((offset / totalBytes) * 100));
      }

      // Allow hardware UART FIFO to drain
      if (offset < totalBytes) {
        await new Promise(resolve => setTimeout(resolve, this.packetDelayMs));
      }
    }
  }
}

5. Related Architecture and Engineering References

Clone this wiki locally