JavaScript library for MIDI communication that simplifies sending and receiving MIDI messages between browsers/Node.js and MIDI instruments
—
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.
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
});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 changesForward 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 inputThe 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");
}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");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 };
}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 };
}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);
}
}
});
}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);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);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);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);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