rtp_midi 0.1.3 copy "rtp_midi: ^0.1.3" to clipboard
rtp_midi: ^0.1.3 copied to clipboard

Cross-platform RTP-MIDI (RFC 6295) implementation in pure Dart. Network MIDI transport for iOS, Android, macOS, Windows, and Linux.

rtp_midi #

Cross-platform RTP-MIDI implementation in pure Dart. Network MIDI transport for iOS, Android, macOS, Windows, and Linux.

The first open-source RTP-MIDI library for Dart/Flutter — connect to macOS Network MIDI sessions, rtpMIDI (Windows), DAWs, and hardware over WiFi.

Status #

Fully functional — session management, all MIDI 1.0 messages, and complete recovery journal are implemented and tested. Verified against macOS CoreMIDI.

Quick Start #

Host (accept incoming connections) #

import 'package:rtp_midi/rtp_midi.dart';

final host = RtpMidiHost(
  config: RtpMidiConfig(name: 'My Dart Session', port: 5004),
);

await host.start();
print('Listening on port ${host.controlPort}');

host.onSessionConnected.listen((session) {
  print('Connected: ${session.remoteName}');

  session.onMidiMessage.listen((msg) {
    print('Received: $msg');
  });

  session.onStateChanged.listen((state) {
    if (state == SessionState.ready) {
      session.send(NoteOn(channel: 0, note: 60, velocity: 100));
    }
  });
});

Client (discover and connect) #

import 'package:rtp_midi/rtp_midi.dart';

final client = RtpMidiClient(
  config: RtpMidiConfig(name: 'My Dart Client'),
);

// Connect to a known address
final session = await client.connectToAddress('192.168.1.50', 5004);
print('Connected to ${session.remoteName}');

// Wait for startup sync (required for Apple compatibility)
await session.onReady;

// Send MIDI
session.send(NoteOn(channel: 0, note: 60, velocity: 100));
session.send(ControlChange(channel: 0, controller: 7, value: 100));

// Receive MIDI
session.onMidiMessage.listen((msg) {
  print('Received: $msg');
});

Configuration #

const config = RtpMidiConfig(
  name: 'My Session',         // Session name visible to peers
  port: 5004,                 // Control port (even); 0 = auto-assign
  clockSyncInterval: Duration(seconds: 10),
  maxInvitationRetries: 12,
  invitationRetryBaseInterval: Duration(milliseconds: 1500),
  sessionTimeout: Duration(seconds: 60),
);

Architecture #

┌─────────────────────────────────────────┐
│           Public API Layer              │
│  RtpMidiHost · RtpMidiClient · Session  │
├─────────────────────────────────────────┤
│         Session Management              │
│  mDNS discovery · Invitation (IN/OK/    │
│  NO/BY) · Clock sync (CK0/CK1/CK2)    │
├─────────────────────────────────────────┤
│          RTP MIDI Payload               │
│  RTP header · MIDI command section ·    │
│  Delta timestamps · SysEx fragmentation │
├─────────────────────────────────────────┤
│         Recovery Journal                │
│  System chapters (D,V,Q,F,X) ·         │
│  Channel chapters (P,C,M,W,N,E,T,A) ·  │
│  Checkpoint management                  │
├─────────────────────────────────────────┤
│           UDP Transport                 │
│  dart:io RawDatagramSocket · Control    │
│  port (even) + Data port (odd)          │
└─────────────────────────────────────────┘

The library follows functional core / imperative shell:

  • Functional core — packet codecs, state machine, clock sync math are pure functions with no IO. Thoroughly unit tested.
  • Imperative shell — UDP sockets, timers, and mDNS are thin wrappers that delegate all logic to the core.

mDNS #

Service discovery and registration use abstract interfaces (MdnsBroadcaster / MdnsDiscoverer). Implement them with your preferred mDNS library:

No-op implementations are provided for direct-connection-only use cases.

The service type is _apple-midi._udp.

Session Lifecycle #

idle → invitingControl → invitingData → connected → synchronizing → ready
                                                                      │
                                                              disconnecting
                                                                      │
                                                               disconnected

Sessions use a two-phase invitation (control port, then data port) followed by periodic CK0/CK1/CK2 clock synchronization, matching Apple's RTP-MIDI implementation.

Apple's CoreMIDI requires multiple rapid clock sync exchanges at startup before it routes MIDI. The library handles this automatically — session.onReady completes when the remote peer is ready to accept MIDI.

Testing #

Comprehensive test suite covering all codecs, state machine, recovery journal (system + channel chapters), journal pipeline integration, and MIDI roundtrip integration.

dart test                            # Run all tests
dart format --set-exit-if-changed .  # Check formatting
dart analyze --fatal-infos .         # Check for lint issues

For real-device validation against macOS Network MIDI:

# On the Mac: capture decoded MIDI output
brew install receivemidi
receivemidi dev "Network Session"

# From Dart: send every MIDI message type
dart run example/midi_message_test.dart 192.168.1.50 5004

Apple Compatibility #

Verified against macOS 26.3 Network MIDI (Audio MIDI Setup + MIDI Monitor). All MIDI 1.0 message types confirmed working: Note On/Off, Control Change, Program Change, Pitch Bend, Channel/Poly Aftertouch, SysEx, and all System Real-Time messages. Recovery journals verified via Wireshark packet capture — all chapters dissect correctly.

Key Apple-specific requirements handled automatically:

  • Rapid startup clock sync (6 exchanges, matching rtpmidid)
  • RTP marker bit M=0 (RFC 6295)
  • Random sequence number start (RFC 3550)
  • Recovery journal in every data packet (J=1, chapters P/C/W/N/T/A)

License #

MIT

0
likes
160
points
91
downloads

Documentation

API reference

Publisher

unverified uploader

Weekly Downloads

Cross-platform RTP-MIDI (RFC 6295) implementation in pure Dart. Network MIDI transport for iOS, Android, macOS, Windows, and Linux.

Repository (GitHub)
View/report issues

Topics

#midi #rtp #music #network

License

MIT (license)

More

Packages that depend on rtp_midi