CtrlK
BlogDocsLog inGet started
Tessl Logo

tessl/npm-webmidi

JavaScript library for MIDI communication that simplifies sending and receiving MIDI messages between browsers/Node.js and MIDI instruments

Pending
Overview
Eval results
Files

message-forwarding.mddocs/

Message Forwarding

Message forwarding provides functionality to route MIDI messages from input ports to output ports automatically. The Forwarder class enables flexible message routing with filtering and transformation capabilities.

Capabilities

Forwarder Construction

Create message forwarders with destination outputs and filtering options.

class Forwarder {
  /**
   * Create a message forwarder
   * @param destinations - Array of Output objects to forward messages to
   * @param options - Forwarding options
   * @param options.channels - Channel filter (number, array, or "all")
   * @param options.types - Message types to forward (string or array)
   * @param options.transform - Transform function for messages
   */
  constructor(destinations?: Output[], options?: {
    channels?: number | number[] | "all";
    types?: string | string[];
    transform?: (message: Message) => Message;
  });
}

Usage Examples:

import { WebMidi, Forwarder } from "webmidi";

await WebMidi.enable();
const input = WebMidi.inputs[0];
const output1 = WebMidi.outputs[0];
const output2 = WebMidi.outputs[1];

// Simple forwarder to single output
const simpleForwarder = new Forwarder([output1]);

// Multiple destinations
const multiForwarder = new Forwarder([output1, output2]);

// With channel filtering
const channelForwarder = new Forwarder([output1], {
  channels: [1, 2, 3, 4] // Only forward channels 1-4
});

// With message type filtering
const noteForwarder = new Forwarder([output1], {
  types: ["noteon", "noteoff"] // Only forward note messages
});

// Combined filtering
const filteredForwarder = new Forwarder([output1, output2], {
  channels: [1, 10], // Channels 1 and 10
  types: ["noteon", "noteoff", "controlchange"] // Notes and CC only
});

Forwarder Properties

Access forwarder destinations and configuration.

/**
 * Array of destination Output objects
 */
readonly destinations: Output[];

/**
 * Message types being forwarded (if filtered)
 */
readonly types: string[];

/**
 * MIDI channels being forwarded (if filtered)
 */
readonly channels: number[];

/**
 * Whether forwarding is suspended
 */
readonly suspended: boolean;

Usage Examples:

const forwarder = new Forwarder([output1, output2]);

// Check destinations
console.log("Forwarding to", forwarder.destinations.length, "outputs");
forwarder.destinations.forEach((output, index) => {
  console.log(`Destination ${index + 1}: ${output.name}`);
});

// Add more destinations (if implementation allows)
// Note: WebMidi.js may not support dynamic destination changes

Message Forwarding

Forward messages to destination outputs.

/**
 * Forward a message to all destinations
 * @param message - MIDI message to forward
 */
forward(message: Message): void;

Usage Examples:

// Manual forwarding (usually handled automatically)
const forwarder = new Forwarder([output1]);

// Forward a specific message
const noteOnData = new Uint8Array([0x90, 60, 100]);
const message = new Message(noteOnData);
forwarder.forward(message);

// This is typically used internally when forwarder is attached to input

Integration with Input Ports

Adding Forwarders to Inputs

The primary way to use forwarders is to attach them to input ports.

const input = WebMidi.inputs[0];
const output = WebMidi.outputs[0];

// Add forwarder to input
const forwarder = input.addForwarder(output);

// Add forwarder with options
const filteredForwarder = input.addForwarder(output, {
  channels: [1, 2, 3],
  types: ["noteon", "noteoff"]
});

// Remove forwarder
input.removeForwarder(forwarder);

// Check if forwarder exists
if (input.hasForwarder(forwarder)) {
  console.log("Forwarder is active");
}

Forwarding Patterns

Simple MIDI Through

Forward all messages from input to output (MIDI through).

async function setupMidiThrough(inputName, outputName) {
  await WebMidi.enable();
  
  const input = WebMidi.getInputByName(inputName);
  const output = WebMidi.getOutputByName(outputName);
  
  if (input && output) {
    const forwarder = input.addForwarder(output);
    console.log(`MIDI through: ${input.name} → ${output.name}`);
    return forwarder;
  } else {
    console.error("Could not find specified input or output");
  }
}

// Usage
const throughForwarder = await setupMidiThrough("My Keyboard", "My Synth");

Channel Routing

Route specific channels to different outputs.

async function setupChannelRouting() {
  await WebMidi.enable();
  
  const input = WebMidi.inputs[0];
  const synthOutput = WebMidi.getOutputByName("Synthesizer");
  const drumOutput = WebMidi.getOutputByName("Drum Machine");
  
  // Route channels 1-8 to synthesizer
  const synthForwarder = input.addForwarder(synthOutput, {
    channels: [1, 2, 3, 4, 5, 6, 7, 8]
  });
  
  // Route channel 10 (drums) to drum machine
  const drumForwarder = input.addForwarder(drumOutput, {
    channels: [10]
  });
  
  console.log("Channel routing established");
  return { synthForwarder, drumForwarder };
}

Message Type Filtering

Forward only specific message types.

async function setupMessageFiltering() {
  await WebMidi.enable();
  
  const input = WebMidi.inputs[0];
  const noteOutput = WebMidi.outputs[0];
  const controlOutput = WebMidi.outputs[1];
  
  // Forward only note messages
  const noteForwarder = input.addForwarder(noteOutput, {
    types: ["noteon", "noteoff", "keyaftertouch"]
  });
  
  // Forward only control messages
  const controlForwarder = input.addForwarder(controlOutput, {
    types: ["controlchange", "programchange", "pitchbend"]
  });
  
  console.log("Message type filtering established");
  return { noteForwarder, controlForwarder };
}

Multi-Zone Keyboard

Split keyboard into zones forwarding to different outputs.

async function setupKeyboardZones() {
  await WebMidi.enable();
  
  const input = WebMidi.getInputByName("88-Key Controller");
  const bassOutput = WebMidi.getOutputByName("Bass Synth");
  const leadOutput = WebMidi.getOutputByName("Lead Synth");
  const padOutput = WebMidi.getOutputByName("Pad Synth");
  
  // Lower zone: C0-B2 → Bass (Channel 1)
  const bassForwarder = input.addForwarder(bassOutput, {
    channels: [1]
    // Note: WebMidi.js forwarder doesn't have built-in note range filtering
    // You would need to implement this with custom message processing
  });
  
  // Middle zone: C3-C5 → Lead (Channel 2)  
  const leadForwarder = input.addForwarder(leadOutput, {
    channels: [2]
  });
  
  // Upper zone: C#5-C8 → Pad (Channel 3)
  const padForwarder = input.addForwarder(padOutput, {
    channels: [3]
  });
  
  return { bassForwarder, leadForwarder, padForwarder };
}

// For note range filtering, you'd need custom processing:
function setupNoteRangeForwarding(input, output, minNote, maxNote, options = {}) {
  return input.addListener("midimessage", (e) => {
    const message = e.message;
    
    // Check if it's a note message
    if (message.type === "noteon" || message.type === "noteoff") {
      const noteNumber = message.dataBytes[0];
      
      // Forward if within range
      if (noteNumber >= minNote && noteNumber <= maxNote) {
        output.send(message.rawData, options);
      }
    }
  });
}

Velocity Scaling and Transformation

While the Forwarder class itself may not support message transformation, you can implement custom forwarding with transformation.

function setupVelocityScaling(input, output, scaleFactor = 1.0, options = {}) {
  return input.addListener("midimessage", (e) => {
    const message = e.message;
    let modifiedData = Array.from(message.rawData);
    
    // Scale velocity for note messages
    if ((message.type === "noteon" || message.type === "noteoff") && modifiedData.length >= 3) {
      const originalVelocity = modifiedData[2];
      const scaledVelocity = Math.round(Math.min(127, originalVelocity * scaleFactor));
      modifiedData[2] = scaledVelocity;
    }
    
    // Forward modified message
    output.send(new Uint8Array(modifiedData), options);
  });
}

// Usage: Scale velocity down by 75%
const scalingListener = setupVelocityScaling(input, output, 0.75);

Channel Remapping

Remap MIDI channels during forwarding.

function setupChannelRemapping(input, output, channelMap) {
  return input.addListener("midimessage", (e) => {
    const message = e.message;
    
    if (message.isChannelMessage) {
      const originalChannel = message.channel;
      const newChannel = channelMap[originalChannel];
      
      if (newChannel !== undefined) {
        let modifiedData = Array.from(message.rawData);
        
        // Modify channel in status byte
        const command = (modifiedData[0] & 0xF0); // Keep command, clear channel
        const newChannelMidi = newChannel - 1;    // Convert to MIDI channel (0-15)
        modifiedData[0] = command | newChannelMidi;
        
        // Forward remapped message
        output.send(new Uint8Array(modifiedData));
      }
    } else {
      // Forward system messages unchanged
      output.send(message.rawData);
    }
  });
}

// Usage: Remap channels 1→2, 2→3, 3→1
const channelMap = { 1: 2, 2: 3, 3: 1 };
const remappingListener = setupChannelRemapping(input, output, channelMap);

Advanced Forwarding Scenarios

MIDI Router/Patchbay

Create a comprehensive MIDI routing system.

class MidiRouter {
  constructor() {
    this.routes = new Map();
    this.forwarders = [];
  }
  
  async initialize() {
    await WebMidi.enable();
    this.updateDeviceList();
  }
  
  updateDeviceList() {
    this.inputs = WebMidi.inputs.map(input => ({ id: input.id, name: input.name }));
    this.outputs = WebMidi.outputs.map(output => ({ id: output.id, name: output.name }));
  }
  
  addRoute(inputId, outputId, options = {}) {
    const input = WebMidi.getInputById(inputId);
    const output = WebMidi.getOutputById(outputId);
    
    if (input && output) {
      const forwarder = input.addForwarder(output, options);
      const routeId = `${inputId}->${outputId}`;
      
      this.routes.set(routeId, {
        input: input,
        output: output,
        forwarder: forwarder,
        options: options
      });
      
      return routeId;
    }
    
    return null;
  }
  
  removeRoute(routeId) {
    const route = this.routes.get(routeId);
    if (route) {
      route.input.removeForwarder(route.forwarder);
      this.routes.delete(routeId);
      return true;
    }
    return false;
  }
  
  listRoutes() {
    const routes = [];
    for (const [routeId, route] of this.routes) {
      routes.push({
        id: routeId,
        input: route.input.name,
        output: route.output.name,
        options: route.options
      });
    }
    return routes;
  }
  
  clearAllRoutes() {
    for (const routeId of this.routes.keys()) {
      this.removeRoute(routeId);
    }
  }
}

// Usage
const router = new MidiRouter();
await router.initialize();

// Add routes
const route1 = router.addRoute(input1.id, output1.id, { channels: [1, 2] });
const route2 = router.addRoute(input1.id, output2.id, { channels: [10] });

// List active routes
console.log(router.listRoutes());

// Remove specific route
router.removeRoute(route1);

Performance Monitoring

Monitor forwarding performance and message throughput.

class ForwardingMonitor {
  constructor(forwarder) {
    this.forwarder = forwarder;
    this.messageCount = 0;
    this.startTime = Date.now();
    this.lastResetTime = this.startTime;
  }
  
  // Wrap the forward method to count messages
  wrapForwarder() {
    const originalForward = this.forwarder.forward.bind(this.forwarder);
    
    this.forwarder.forward = (message) => {
      this.messageCount++;
      return originalForward(message);
    };
  }
  
  getStats() {
    const now = Date.now();
    const totalTime = now - this.startTime;
    const resetTime = now - this.lastResetTime;
    
    return {
      messageCount: this.messageCount,
      totalTime: totalTime,
      messagesPerSecond: this.messageCount / (resetTime / 1000),
      averageMessagesPerSecond: this.messageCount / (totalTime / 1000)
    };
  }
  
  reset() {
    this.messageCount = 0;
    this.lastResetTime = Date.now();
  }
}

// Usage
const forwarder = input.addForwarder(output);
const monitor = new ForwardingMonitor(forwarder);
monitor.wrapForwarder();

// Check stats periodically
setInterval(() => {
  const stats = monitor.getStats();
  console.log(`Messages/sec: ${stats.messagesPerSecond.toFixed(2)}`);
}, 1000);

Types

interface ForwarderOptions {
  channels?: number | number[] | "all";
  types?: string | string[];
  transform?: (message: Message) => Message;
}

interface RouteInfo {
  id: string;
  input: string;
  output: string;
  options: ForwarderOptions;
}

type MessageType = "noteon" | "noteoff" | "keyaftertouch" | "controlchange" | "programchange" | "channelaftertouch" | "pitchbend" | "sysex";

Install with Tessl CLI

npx tessl i tessl/npm-webmidi

docs

constants.md

index.md

message-forwarding.md

message-processing.md

midi-input.md

midi-output.md

note-processing.md

utilities.md

webmidi-interface.md

tile.json