kommidi
v0.1.0
Published
A powerful and flexible TypeScript library for MIDI message processing with modular pipeline architecture
Maintainers
Readme
Kommidi
A powerful and flexible TypeScript library for MIDI message processing, featuring a modular pipeline architecture for real-time MIDI manipulation.
Features
- 🎹 Web MIDI API Integration - Seamless connection to MIDI devices in the browser
- 📁 MIDI File I/O - Parse and write standard MIDI files
- 🔄 Pipeline Architecture - Chain multiple MIDI processors with a clean, modular API
- 🎵 Rich Event Types - Complete support for MIDI events (Note, CC, SysEx, etc.) and Meta events
- 🔌 Multi-port Support - Handle complex routing scenarios with named input/output ports
- 📦 Minimal Dependencies - Only
eventemitter3andbuffer - 🎯 TypeScript First - Full type safety and IntelliSense support
Installation
npm install kommidiQuick Start
Basic MIDI Processing
import { WebMIDI, MIDIDevice } from 'kommidi';
// Initialize Web MIDI
const webMIDI = new WebMIDI();
await webMIDI.init();
// Create auto-selecting device (matches any available device)
const device = new MIDIDevice(webMIDI);
// Or match specific device by name pattern
// const device = new MIDIDevice(webMIDI, /Keyboard/i);
// Listen for device changes
device.on('input-changed', (port) => {
console.log('Input device changed to:', port.name);
});
device.on('output-changed', (port) => {
console.log('Output device changed to:', port.name);
});
// Simple echo: device input → device output
device.connect(device);
// Or with processing
import { Transposer } from 'kommidi';
const transposer = new Transposer();
transposer.transpose = 12;
device.connect(transposer).connect(device);Reading MIDI Files
import { MIDIFileParser } from 'kommidi';
import * as fs from 'fs';
// Note: Use Uint8Array.from() to avoid Node.js Buffer pool issues
const buffer = Uint8Array.from(fs.readFileSync('song.mid'));
const midiFile = MIDIFileParser.parse(buffer);
console.log(`Tempo: ${midiFile.ticksPerBeat} ticks per beat`);
console.log(`Tracks: ${midiFile.tracks.length}`);
midiFile.tracks.forEach((track, i) => {
console.log(`Track ${i}: ${track.name} (${track.length} events)`);
});Playing MIDI Files
import { MIDIFileParser, MIDIPlayer, ConsoleOutput } from 'kommidi';
import * as fs from 'fs';
// Load and parse MIDI file
const buffer = Uint8Array.from(fs.readFileSync('song.mid'));
const midiFile = MIDIFileParser.parse(buffer);
// Create player and output
const player = new MIDIPlayer(midiFile);
const output = new ConsoleOutput();
player.connect(output);
// Control playback
player.play();
player.pause();
player.seek(480); // Seek to tick 480
player.stop();
// Events
player.on('end', () => console.log('Playback finished'));
player.on('loop', () => console.log('Looping...'));
// Properties
console.log(player.currentTick, player.currentTimeMs);
console.log(player.durationTicks, player.durationMs);
console.log(player.progress); // 0-1Recording MIDI
import { MIDIRecorder, MIDIFileWriter } from 'kommidi';
import * as fs from 'fs';
const recorder = new MIDIRecorder();
recorder.bpm = 120;
recorder.ticksPerBeat = 480;
// Connect input to recorder
midiInput.connect(recorder);
// Start recording
recorder.start();
// ... play some notes ...
// Stop and get MIDI file
const midiFile = recorder.stop();
// Save to file
const bytes = MIDIFileWriter.write(midiFile);
fs.writeFileSync('recorded.mid', bytes);Creating Custom MIDI Events
import { NoteOn, NoteOff, ControlChange, ControlType } from 'kommidi';
// Create a Note On event
const noteOn = new NoteOn({
channel: 0,
pitch: 60, // Middle C
velocity: 100
});
// Create a Note Off event
const noteOff = new NoteOff({
channel: 0,
pitch: 60,
offVel: 0
});
// Create a Control Change event
const cc = new ControlChange({
channel: 0,
control: ControlType.HoldPedal,
value: 127
});
// Get raw MIDI bytes
console.log(noteOn.bytes); // Uint8Array [144, 60, 100]Core Concepts
Web MIDI Device Management
Kommidi provides flexible device management with two approaches:
1. Automatic Device Selection (Recommended)
Use MIDIDevice for automatic device selection based on patterns:
const webMIDI = new WebMIDI();
await webMIDI.init();
// Auto-select any device with "Keyboard" in the name
const device = new MIDIDevice(webMIDI, /Keyboard/i);
// The device starts with dummy ports and automatically switches
// to matching real devices when they connect
console.log(device.hasRealInput); // false initially
console.log(device.inputPort.name); // "(No Input)"
// When "USB Keyboard" connects:
// → device.hasRealInput becomes true
// → device.inputPort.name becomes "USB Keyboard"
// → 'input-changed' event is emitted
device.on('input-changed', (port) => {
if (device.hasRealInput) {
console.log('Real device connected:', port.name);
} else {
console.log('Using dummy device');
}
});2. Manual Port Selection
For more control, work directly with ports:
const webMIDI = new WebMIDI();
await webMIDI.init();
// List available ports
const inputs = webMIDI.getInputPorts();
const outputs = webMIDI.getOutputPorts();
console.log('Inputs:', inputs.map(p => p.name));
console.log('Outputs:', outputs.map(p => p.name));
// Get specific port
const myInput = webMIDI.getInputById('port-id-123');
// Listen for port changes
webMIDI.on('input-ports-changed', (inputs) => {
updateDeviceDropdown(inputs);
});
// Create custom device pairing
const customDevice = new MIDIDevice(webMIDI);
// Then manually control which ports to use in your applicationPipeline Architecture
Kommidi uses a pipeline-based architecture where MIDI events flow through connected nodes:
[MIDI Input] → [Processor 1] → [Processor 2] → [MIDI Output]Node Types
- MIDISource - Outputs MIDI events (e.g.,
InputPort, MIDI file player) - MIDITarget - Receives MIDI events (e.g.,
OutputPort, MIDI file recorder) - MIDIIONode - Both receives and outputs (e.g.,
Transposer, effects, routers)
Building a Processing Chain
import { WebMIDI, MIDIDevice, Transposer } from 'kommidi';
const webMIDI = new WebMIDI();
await webMIDI.init();
// Auto-select device by pattern
const device = new MIDIDevice(webMIDI, /My MIDI Device/i);
// Create processors
const transposer = new Transposer();
transposer.transpose = 7; // Perfect fifth up
// Chain them together
device
.connect(transposer)
.connect(device); // Loop back to output
// Or connect to multiple targets
const output1 = new MIDIDevice(webMIDI, /Output 1/i);
const output2 = new MIDIDevice(webMIDI, /Output 2/i);
transposer.connect(output1);
transposer.connect(output2);API Overview
Core Events
import {
NoteOn, NoteOff, // Note events
ControlChange, // Control Change
ProgramChange, // Program Change
PitchWheel, // Pitch Bend
AfterTouch, // Polyphonic Key Pressure
ChannelPressure, // Channel Pressure
SysEx, // System Exclusive
Clock, Start, Stop, // Timing messages
} from 'kommidi';Meta Events (MIDI Files)
import {
Tempo, // Set tempo
TimeSignature, // Time signature
KeySignature, // Key signature
TrackName, // Track name
Lyric, Marker, Cue, // Text events
EndOfTrack, // End of track marker
} from 'kommidi/core/MetaEvent';
// Tempo — recommended: use fromBpm() helper
const tempo = Tempo.fromBpm(120);
tempo.bpm; // 120
tempo.microSecondsPerQuarterNote; // 500000
// Time Signature: 3/4 time
// Args: numerator, log2(denominator), midiClocksPerClick, 32ndNotesPerQuarter
// Common: 4/4 = (4, 2, 24, 8), 3/4 = (3, 2, 24, 8), 6/8 = (6, 3, 24, 8)
const timeSig = new TimeSignature(3, 2, 24, 8);
// Key Signature: D major (2 sharps)
// key: -7 (7 flats) to 7 (7 sharps), 0 = C
// scale: 0 = Major, 1 = Minor
const keySig = new KeySignature(2, 0);I/O
import {
WebMIDI,
MIDIDevice,
MIDIFileParser,
MIDIFileWriter
} from 'kommidi';
// Web MIDI
const webMIDI = new WebMIDI();
await webMIDI.init({ sysex: true });
// Get available ports
const inputs = webMIDI.getInputPorts();
const outputs = webMIDI.getOutputPorts();
// Auto-select device by pattern
const device = new MIDIDevice(webMIDI, /Keyboard/i);
// Or match any device
const anyDevice = new MIDIDevice(webMIDI);
// Check device status
console.log(device.hasRealInput); // true if using real device
console.log(device.hasRealOutput); // false if using dummy device
// MIDI Files - Parse
const midiFile = MIDIFileParser.parse(buffer);
// MIDI Files - Write
const bytes = MIDIFileWriter.write(midiFile);Processing Modules
import { Transposer, ConsoleOutput } from 'kommidi';
// Transpose notes
const transposer = new Transposer();
transposer.transpose = 12; // Shift by 12 semitones
// Debug output (logs all events to console)
const consoleOutput = new ConsoleOutput();
midiInput.connect(consoleOutput);Playback & Recording
import { MIDIPlayer, MIDIRecorder } from 'kommidi';
// Play MIDI files with proper timing
const player = new MIDIPlayer(midiFile);
player.loop = true;
player.playbackRate = 1.5; // 1.5x speed
player.play();
// Playback control
player.pause();
player.seek(480); // Seek to tick position
player.seekToTime(5000); // Seek to 5 seconds
player.stop();
// State and position
player.state; // 'stopped' | 'playing' | 'paused'
player.isPlaying; // boolean
player.currentTick; // current position in ticks
player.currentTimeMs; // current position in ms
player.durationTicks; // total duration in ticks
player.durationMs; // total duration in ms
player.progress; // 0-1
// Events
player.on('play', () => {});
player.on('pause', () => {});
player.on('stop', () => {});
player.on('end', () => {});
player.on('loop', () => {});
player.on('seek', ({ tick, timeMs }) => {});
player.on('load', ({ durationTicks, durationMs }) => {});
player.on('ratechange', (rate) => {});
// Record MIDI events
const recorder = new MIDIRecorder();
recorder.bpm = 120;
recorder.ticksPerBeat = 480;
recorder.start();
// ... play some notes ...
const midiFile = recorder.stop(); // Returns MIDIFile
recorder.on('recordingStart', () => {});
recorder.on('recordingStop', () => {});Data Models
import { MIDIFile, MIDIFileParser, MIDIFileWriter, Sequencer } from 'kommidi';
// MIDIFile: event-based representation (delta times)
const midiFile = new MIDIFile({ ticksPerBeat: 480 });
// Sequencer: note-based representation (easier to work with)
const sequencer = Sequencer.fromMIDIFile(midiFile);
// Inspect tracks and notes
for (const track of sequencer.tracks) {
console.log(track.name, track.notes.length);
for (const note of track.notes) {
// { pitch, start, duration, velocity?, channel? }
}
}
// Access tempo and signature data
sequencer.tempoMap; // [{ time: 0, bpm: 120 }]
sequencer.timeSignatureChanges; // [{ time: 0, numerator: 4, denominator: 4 }]
sequencer.keySignatureChanges; // [{ time: 0, key: 0, scale: 0 }]
// Programmatically create music
const seq = new Sequencer();
seq.tempoMap.push({ time: 0, bpm: 140 });
seq.addTrack({
name: 'Piano',
defaultChannel: 0,
notes: [
{ pitch: 60, start: 0, duration: 480, velocity: 100 },
{ pitch: 64, start: 480, duration: 480, velocity: 90 },
]
});
// Convert back to MIDIFile and save
const bytes = MIDIFileWriter.write(seq.toMIDIFile());For complete API signatures, type definitions, common pitfalls, and MIDI reference values, see docs/API.md. For architecture and design details, see docs/ARCHITECTURE.md.
Advanced Usage
Creating Custom Processors
import { MIDIIONode, BaseEvent, NoteOn, EventParser } from 'kommidi';
class VelocityScaler extends MIDIIONode {
scale = 1.0;
feed(event: BaseEvent): void {
if (event instanceof NoteOn) {
// Clone to avoid mutating original event
const cloned = EventParser.cloneEvent(event) as NoteOn;
cloned.velocity = Math.min(127, Math.floor(cloned.velocity * this.scale));
this.output(cloned);
} else {
this.output(event);
}
}
}
// Usage
const scaler = new VelocityScaler();
scaler.scale = 0.8; // Reduce velocity by 20%
input.connect(scaler).connect(output);Event Filtering
import { MIDIIONode, BaseEvent, NoteOn, NoteOff } from 'kommidi';
class NoteFilter extends MIDIIONode {
feed(event: BaseEvent): void {
// Only pass through note events
if (event instanceof NoteOn || event instanceof NoteOff) {
this.output(event);
}
}
}Event Monitoring
// Listen to events without interrupting the pipeline
device.on('output', (event: BaseEvent) => {
console.log('MIDI event:', event);
});Project Structure
kommidi/
├── src/
│ ├── core/ # Core event types and base classes
│ │ ├── MIDIEvent.ts # MIDI message types
│ │ ├── MetaEvent.ts # MIDI file meta events (Tempo, TimeSignature, etc.)
│ │ ├── EventParser.ts # Binary MIDI parser
│ │ ├── Module.ts # Base classes (MIDISource, MIDITarget, MIDIIONode)
│ │ └── VarInt.ts # Variable-length integer encoding
│ ├── model/ # Data models
│ │ ├── MIDIFile.ts # MIDIFile and MIDITrack classes (event-based)
│ │ └── Sequencer.ts # Sequencer class (note-based data structure)
│ ├── io/ # Input/Output
│ │ ├── WebMIDI.ts # Web MIDI API wrapper (port management)
│ │ ├── MIDIDevice.ts # Auto-selecting device manager
│ │ ├── DummyPorts.ts # Placeholder ports (no-op implementations)
│ │ └── MIDIFileIO.ts # MIDI file parser and writer
│ ├── module/ # Built-in processors
│ │ ├── Transposer.ts # Pitch transposition
│ │ └── ConsoleOutput.ts # Debug output to console
│ └── playback/ # Playback and recording
│ ├── MIDIPlayer.ts # MIDI file playback with timing
│ └── MIDIRecorder.ts # MIDI recording to file
└── dist/ # Compiled outputArchitecture
Design Principles
- Immutability - Events are cloned before modification to prevent side effects
- Type Safety - Full TypeScript support with strict typing
- Modularity - Small, composable processors
- Performance - Minimal overhead, efficient event routing
- Extensibility - Easy to create custom processors
Key Patterns
- Pipeline Pattern - Chain processors for complex transformations
- Observer Pattern - EventEmitter for monitoring without side effects
- Factory Pattern - EventParser creates appropriate event types
Development & Build
Development
Start the development server with hot reload:
npm run devOpen http://localhost:5173 to see the interactive demo page with examples.
Testing
npm test # Run tests in watch mode
npm run test:run # Run tests once
npm run test:ui # Run tests with browser UIBuilding
Build the library for production:
npm run buildThis generates:
dist/*.es.js- ES modules (tree-shakeable, multiple entry points)dist/kommidi.umd.js- UMD bundle (single file for<script>tag)dist/*.d.ts- TypeScript declarations- Source maps for debugging
Build scripts:
npm run build- Build ES + UMDnpm run build:es- Build ES modules onlynpm run build:umd- Build UMD bundle only
Publishing
The library is ready to publish to npm:
npm run build
npm publishThe prepublishOnly script automatically builds before publishing.
Import Paths
Kommidi supports two import strategies:
Strategy 1: Main Entry Point (Recommended for most users)
Import everything from the main entry point. Modern bundlers will tree-shake unused code automatically.
import {
WebMIDI,
MIDIDevice,
NoteOn,
NoteOff,
Transposer,
MIDIFileParser,
MIDIPlayer,
Sequencer
} from 'kommidi';Strategy 2: Subpath Imports (For granular control)
Import from specific modules for explicit control over dependencies.
// Core types and base classes
import { NoteOn, NoteOff, MIDIIONode } from 'kommidi/core';
import { Tempo, TimeSignature } from 'kommidi/core/MetaEvent';
// Data models
import { MIDIFile } from 'kommidi/model/MIDIFile';
import { Sequencer } from 'kommidi/model/Sequencer';
// I/O operations
import { WebMIDI, MIDIDevice } from 'kommidi/io';
import { MIDIFileParser, MIDIFileWriter } from 'kommidi/io/MIDIFileIO';
// Processing modules
import { Transposer, ConsoleOutput } from 'kommidi/module';
// Playback and recording
import { MIDIPlayer } from 'kommidi/playback/MIDIPlayer';
import { MIDIRecorder } from 'kommidi/playback/MIDIRecorder';Both strategies are fully supported. Choose based on your preference:
- Use Strategy 1 for simplicity and convenience
- Use Strategy 2 for explicit dependency management in large projects
Browser Compatibility
- Chrome/Edge 43+ (Web MIDI API support)
- Firefox 108+ (with
dom.webmidi.enabledflag) - Safari 17+ (requires user permission)
Node.js Usage
MIDI file I/O, Sequencer, and MIDIPlayer work in Node.js without any polyfill:
import { MIDIFileParser, MIDIFileWriter, Sequencer } from 'kommidi';
import * as fs from 'fs';
// Important: Use Uint8Array.from() to avoid Node.js Buffer pool issues
const buffer = Uint8Array.from(fs.readFileSync('song.mid'));
const midiFile = MIDIFileParser.parse(buffer);
const sequencer = Sequencer.fromMIDIFile(midiFile);Web MIDI device access (WebMIDI, MIDIDevice) requires a browser environment.
MIDIPlayer depends on performance.now() and setTimeout, available in Node.js 16+.
Development Status
- ✅ Core MIDI events
- ✅ Meta events (with
Tempo.fromBpm()helper) - ✅ Web MIDI integration
- ✅ MIDI file parsing and writing
- ✅ MIDI file playback (MIDIPlayer with play/pause/stop/seek/loop)
- ✅ MIDI recording (MIDIRecorder)
- ✅ Basic processors (Transposer, ConsoleOutput)
- ✅ Sequencer (note-based data structure)
- 📋 More built-in processors (planned)
Contributing
This project follows these coding guidelines:
- Prefer functional, immutable style
- Use early returns to reduce nesting
- Add JSDoc comments to public APIs
- Write descriptive variable names
- Keep functions focused and small
License
MIT
Related Projects
- midi-parser-js - MIDI file parsing
- JZZ - MIDI library for browsers and Node.js
- Tone.js - Web Audio framework
Examples
See the examples/ directory for interactive demos and scripts.
Web (Browser)
Run npm run dev and open http://localhost:5173 to try the interactive demo page:
- Echo / Passthrough — Route MIDI input directly to output
- MIDI Player — Load and play Standard MIDI Files
- MIDI Recorder — Record MIDI input and save as
.mid - Transpose — Shift note pitches by semitones
- Velocity Scaler — Scale note velocities (loudness)
- Event Filter — Filter specific MIDI event types
Node.js Scripts
# Parse and inspect a MIDI file
tsx examples/scripts/06-read-midi-file.ts path/to/file.mid
# Play a MIDI file with console output
tsx examples/scripts/test-player.ts path/to/file.mid
# Play with looping
tsx examples/scripts/test-player.ts path/to/file.mid --loopSee examples/README.md for full details and how to create your own examples.
Made with ❤️ for musicians and developers
