or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

Files

docs

constants.mdindex.mdmessage-forwarding.mdmessage-processing.mdmidi-input.mdmidi-output.mdnote-processing.mdutilities.mdwebmidi-interface.md

message-forwarding.mddocs/

0

# Message Forwarding

1

2

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.

3

4

## Capabilities

5

6

### Forwarder Construction

7

8

Create message forwarders with destination outputs and filtering options.

9

10

```javascript { .api }

11

class Forwarder {

12

/**

13

* Create a message forwarder

14

* @param destinations - Array of Output objects to forward messages to

15

* @param options - Forwarding options

16

* @param options.channels - Channel filter (number, array, or "all")

17

* @param options.types - Message types to forward (string or array)

18

* @param options.transform - Transform function for messages

19

*/

20

constructor(destinations?: Output[], options?: {

21

channels?: number | number[] | "all";

22

types?: string | string[];

23

transform?: (message: Message) => Message;

24

});

25

}

26

```

27

28

**Usage Examples:**

29

30

```javascript

31

import { WebMidi, Forwarder } from "webmidi";

32

33

await WebMidi.enable();

34

const input = WebMidi.inputs[0];

35

const output1 = WebMidi.outputs[0];

36

const output2 = WebMidi.outputs[1];

37

38

// Simple forwarder to single output

39

const simpleForwarder = new Forwarder([output1]);

40

41

// Multiple destinations

42

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

43

44

// With channel filtering

45

const channelForwarder = new Forwarder([output1], {

46

channels: [1, 2, 3, 4] // Only forward channels 1-4

47

});

48

49

// With message type filtering

50

const noteForwarder = new Forwarder([output1], {

51

types: ["noteon", "noteoff"] // Only forward note messages

52

});

53

54

// Combined filtering

55

const filteredForwarder = new Forwarder([output1, output2], {

56

channels: [1, 10], // Channels 1 and 10

57

types: ["noteon", "noteoff", "controlchange"] // Notes and CC only

58

});

59

```

60

61

### Forwarder Properties

62

63

Access forwarder destinations and configuration.

64

65

```javascript { .api }

66

/**

67

* Array of destination Output objects

68

*/

69

readonly destinations: Output[];

70

71

/**

72

* Message types being forwarded (if filtered)

73

*/

74

readonly types: string[];

75

76

/**

77

* MIDI channels being forwarded (if filtered)

78

*/

79

readonly channels: number[];

80

81

/**

82

* Whether forwarding is suspended

83

*/

84

readonly suspended: boolean;

85

```

86

87

**Usage Examples:**

88

89

```javascript

90

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

91

92

// Check destinations

93

console.log("Forwarding to", forwarder.destinations.length, "outputs");

94

forwarder.destinations.forEach((output, index) => {

95

console.log(`Destination ${index + 1}: ${output.name}`);

96

});

97

98

// Add more destinations (if implementation allows)

99

// Note: WebMidi.js may not support dynamic destination changes

100

```

101

102

### Message Forwarding

103

104

Forward messages to destination outputs.

105

106

```javascript { .api }

107

/**

108

* Forward a message to all destinations

109

* @param message - MIDI message to forward

110

*/

111

forward(message: Message): void;

112

```

113

114

**Usage Examples:**

115

116

```javascript

117

// Manual forwarding (usually handled automatically)

118

const forwarder = new Forwarder([output1]);

119

120

// Forward a specific message

121

const noteOnData = new Uint8Array([0x90, 60, 100]);

122

const message = new Message(noteOnData);

123

forwarder.forward(message);

124

125

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

126

```

127

128

## Integration with Input Ports

129

130

### Adding Forwarders to Inputs

131

132

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

133

134

```javascript

135

const input = WebMidi.inputs[0];

136

const output = WebMidi.outputs[0];

137

138

// Add forwarder to input

139

const forwarder = input.addForwarder(output);

140

141

// Add forwarder with options

142

const filteredForwarder = input.addForwarder(output, {

143

channels: [1, 2, 3],

144

types: ["noteon", "noteoff"]

145

});

146

147

// Remove forwarder

148

input.removeForwarder(forwarder);

149

150

// Check if forwarder exists

151

if (input.hasForwarder(forwarder)) {

152

console.log("Forwarder is active");

153

}

154

```

155

156

## Forwarding Patterns

157

158

### Simple MIDI Through

159

160

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

161

162

```javascript

163

async function setupMidiThrough(inputName, outputName) {

164

await WebMidi.enable();

165

166

const input = WebMidi.getInputByName(inputName);

167

const output = WebMidi.getOutputByName(outputName);

168

169

if (input && output) {

170

const forwarder = input.addForwarder(output);

171

console.log(`MIDI through: ${input.name} → ${output.name}`);

172

return forwarder;

173

} else {

174

console.error("Could not find specified input or output");

175

}

176

}

177

178

// Usage

179

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

180

```

181

182

### Channel Routing

183

184

Route specific channels to different outputs.

185

186

```javascript

187

async function setupChannelRouting() {

188

await WebMidi.enable();

189

190

const input = WebMidi.inputs[0];

191

const synthOutput = WebMidi.getOutputByName("Synthesizer");

192

const drumOutput = WebMidi.getOutputByName("Drum Machine");

193

194

// Route channels 1-8 to synthesizer

195

const synthForwarder = input.addForwarder(synthOutput, {

196

channels: [1, 2, 3, 4, 5, 6, 7, 8]

197

});

198

199

// Route channel 10 (drums) to drum machine

200

const drumForwarder = input.addForwarder(drumOutput, {

201

channels: [10]

202

});

203

204

console.log("Channel routing established");

205

return { synthForwarder, drumForwarder };

206

}

207

```

208

209

### Message Type Filtering

210

211

Forward only specific message types.

212

213

```javascript

214

async function setupMessageFiltering() {

215

await WebMidi.enable();

216

217

const input = WebMidi.inputs[0];

218

const noteOutput = WebMidi.outputs[0];

219

const controlOutput = WebMidi.outputs[1];

220

221

// Forward only note messages

222

const noteForwarder = input.addForwarder(noteOutput, {

223

types: ["noteon", "noteoff", "keyaftertouch"]

224

});

225

226

// Forward only control messages

227

const controlForwarder = input.addForwarder(controlOutput, {

228

types: ["controlchange", "programchange", "pitchbend"]

229

});

230

231

console.log("Message type filtering established");

232

return { noteForwarder, controlForwarder };

233

}

234

```

235

236

### Multi-Zone Keyboard

237

238

Split keyboard into zones forwarding to different outputs.

239

240

```javascript

241

async function setupKeyboardZones() {

242

await WebMidi.enable();

243

244

const input = WebMidi.getInputByName("88-Key Controller");

245

const bassOutput = WebMidi.getOutputByName("Bass Synth");

246

const leadOutput = WebMidi.getOutputByName("Lead Synth");

247

const padOutput = WebMidi.getOutputByName("Pad Synth");

248

249

// Lower zone: C0-B2 → Bass (Channel 1)

250

const bassForwarder = input.addForwarder(bassOutput, {

251

channels: [1]

252

// Note: WebMidi.js forwarder doesn't have built-in note range filtering

253

// You would need to implement this with custom message processing

254

});

255

256

// Middle zone: C3-C5 → Lead (Channel 2)

257

const leadForwarder = input.addForwarder(leadOutput, {

258

channels: [2]

259

});

260

261

// Upper zone: C#5-C8 → Pad (Channel 3)

262

const padForwarder = input.addForwarder(padOutput, {

263

channels: [3]

264

});

265

266

return { bassForwarder, leadForwarder, padForwarder };

267

}

268

269

// For note range filtering, you'd need custom processing:

270

function setupNoteRangeForwarding(input, output, minNote, maxNote, options = {}) {

271

return input.addListener("midimessage", (e) => {

272

const message = e.message;

273

274

// Check if it's a note message

275

if (message.type === "noteon" || message.type === "noteoff") {

276

const noteNumber = message.dataBytes[0];

277

278

// Forward if within range

279

if (noteNumber >= minNote && noteNumber <= maxNote) {

280

output.send(message.rawData, options);

281

}

282

}

283

});

284

}

285

```

286

287

### Velocity Scaling and Transformation

288

289

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

290

291

```javascript

292

function setupVelocityScaling(input, output, scaleFactor = 1.0, options = {}) {

293

return input.addListener("midimessage", (e) => {

294

const message = e.message;

295

let modifiedData = Array.from(message.rawData);

296

297

// Scale velocity for note messages

298

if ((message.type === "noteon" || message.type === "noteoff") && modifiedData.length >= 3) {

299

const originalVelocity = modifiedData[2];

300

const scaledVelocity = Math.round(Math.min(127, originalVelocity * scaleFactor));

301

modifiedData[2] = scaledVelocity;

302

}

303

304

// Forward modified message

305

output.send(new Uint8Array(modifiedData), options);

306

});

307

}

308

309

// Usage: Scale velocity down by 75%

310

const scalingListener = setupVelocityScaling(input, output, 0.75);

311

```

312

313

### Channel Remapping

314

315

Remap MIDI channels during forwarding.

316

317

```javascript

318

function setupChannelRemapping(input, output, channelMap) {

319

return input.addListener("midimessage", (e) => {

320

const message = e.message;

321

322

if (message.isChannelMessage) {

323

const originalChannel = message.channel;

324

const newChannel = channelMap[originalChannel];

325

326

if (newChannel !== undefined) {

327

let modifiedData = Array.from(message.rawData);

328

329

// Modify channel in status byte

330

const command = (modifiedData[0] & 0xF0); // Keep command, clear channel

331

const newChannelMidi = newChannel - 1; // Convert to MIDI channel (0-15)

332

modifiedData[0] = command | newChannelMidi;

333

334

// Forward remapped message

335

output.send(new Uint8Array(modifiedData));

336

}

337

} else {

338

// Forward system messages unchanged

339

output.send(message.rawData);

340

}

341

});

342

}

343

344

// Usage: Remap channels 1→2, 2→3, 3→1

345

const channelMap = { 1: 2, 2: 3, 3: 1 };

346

const remappingListener = setupChannelRemapping(input, output, channelMap);

347

```

348

349

## Advanced Forwarding Scenarios

350

351

### MIDI Router/Patchbay

352

353

Create a comprehensive MIDI routing system.

354

355

```javascript

356

class MidiRouter {

357

constructor() {

358

this.routes = new Map();

359

this.forwarders = [];

360

}

361

362

async initialize() {

363

await WebMidi.enable();

364

this.updateDeviceList();

365

}

366

367

updateDeviceList() {

368

this.inputs = WebMidi.inputs.map(input => ({ id: input.id, name: input.name }));

369

this.outputs = WebMidi.outputs.map(output => ({ id: output.id, name: output.name }));

370

}

371

372

addRoute(inputId, outputId, options = {}) {

373

const input = WebMidi.getInputById(inputId);

374

const output = WebMidi.getOutputById(outputId);

375

376

if (input && output) {

377

const forwarder = input.addForwarder(output, options);

378

const routeId = `${inputId}->${outputId}`;

379

380

this.routes.set(routeId, {

381

input: input,

382

output: output,

383

forwarder: forwarder,

384

options: options

385

});

386

387

return routeId;

388

}

389

390

return null;

391

}

392

393

removeRoute(routeId) {

394

const route = this.routes.get(routeId);

395

if (route) {

396

route.input.removeForwarder(route.forwarder);

397

this.routes.delete(routeId);

398

return true;

399

}

400

return false;

401

}

402

403

listRoutes() {

404

const routes = [];

405

for (const [routeId, route] of this.routes) {

406

routes.push({

407

id: routeId,

408

input: route.input.name,

409

output: route.output.name,

410

options: route.options

411

});

412

}

413

return routes;

414

}

415

416

clearAllRoutes() {

417

for (const routeId of this.routes.keys()) {

418

this.removeRoute(routeId);

419

}

420

}

421

}

422

423

// Usage

424

const router = new MidiRouter();

425

await router.initialize();

426

427

// Add routes

428

const route1 = router.addRoute(input1.id, output1.id, { channels: [1, 2] });

429

const route2 = router.addRoute(input1.id, output2.id, { channels: [10] });

430

431

// List active routes

432

console.log(router.listRoutes());

433

434

// Remove specific route

435

router.removeRoute(route1);

436

```

437

438

### Performance Monitoring

439

440

Monitor forwarding performance and message throughput.

441

442

```javascript

443

class ForwardingMonitor {

444

constructor(forwarder) {

445

this.forwarder = forwarder;

446

this.messageCount = 0;

447

this.startTime = Date.now();

448

this.lastResetTime = this.startTime;

449

}

450

451

// Wrap the forward method to count messages

452

wrapForwarder() {

453

const originalForward = this.forwarder.forward.bind(this.forwarder);

454

455

this.forwarder.forward = (message) => {

456

this.messageCount++;

457

return originalForward(message);

458

};

459

}

460

461

getStats() {

462

const now = Date.now();

463

const totalTime = now - this.startTime;

464

const resetTime = now - this.lastResetTime;

465

466

return {

467

messageCount: this.messageCount,

468

totalTime: totalTime,

469

messagesPerSecond: this.messageCount / (resetTime / 1000),

470

averageMessagesPerSecond: this.messageCount / (totalTime / 1000)

471

};

472

}

473

474

reset() {

475

this.messageCount = 0;

476

this.lastResetTime = Date.now();

477

}

478

}

479

480

// Usage

481

const forwarder = input.addForwarder(output);

482

const monitor = new ForwardingMonitor(forwarder);

483

monitor.wrapForwarder();

484

485

// Check stats periodically

486

setInterval(() => {

487

const stats = monitor.getStats();

488

console.log(`Messages/sec: ${stats.messagesPerSecond.toFixed(2)}`);

489

}, 1000);

490

```

491

492

## Types

493

494

```javascript { .api }

495

interface ForwarderOptions {

496

channels?: number | number[] | "all";

497

types?: string | string[];

498

transform?: (message: Message) => Message;

499

}

500

501

interface RouteInfo {

502

id: string;

503

input: string;

504

output: string;

505

options: ForwarderOptions;

506

}

507

508

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

509

```