Skip to content

MIDI 2.0, UMP, and Clip Files ​

MIDI 2.0 messages travel through libsonare as Universal MIDI Packets (UMPs). A UMP is the packet transport; the message type and status define the MIDI meaning. A project event stores a PPQ position plus the first two UMP words, so a MIDI 1.0 channel-voice message and a two-word MIDI 2.0 channel-voice message can share one setMidiEvents list. Project.midi* helpers make MIDI 1.0 words; Project.midi2* helpers make full-resolution MIDI 2.0 words.

For the protocol definition, see the MIDI Association’s Universal MIDI Packet and MIDI 2.0 Protocol specification. The receiver and binding limits below describe libsonare.

Value width and receiver behavior

A MIDI 2.0 UMP can carry a 16-bit note velocity, 32-bit controller or pressure value, 32-bit pitch bend, and per-note messages. The message keeps that width through the raw UMP and MIDI 2.0 Clip File paths. The instrument receiving it can still support only a subset of expression messages; see the receiver table below.

MIDI 1.0 and MIDI 2.0 through UMP
MIDI 1.0 / MT2MIDI 2.0 / MT4ProjectMidiEvent listRealtimeEngine UMPSMF2CLIPSMF conversion(loss possible)
UMP carries either one-word MIDI 1.0 or two-word MIDI 2.0 channel voice messages; the project list can preserve both before a receiver or file conversion applies its limits.

From MIDI 1.0 to MIDI 2.0 ​

A UMP channel-voice word carries a 4-bit group and a 4-bit MIDI channel. The channel voice message type distinguishes the value layout: MT 0x2 is a one-word MIDI 1.0 message, while MT 0x4 is a two-word MIDI 2.0 message. The second word is where the wider value fields live. Both message types still identify notes with 7-bit note numbers and use the same group/channel ranges.

Value or expressionMIDI 1.0 channel voiceMIDI 2.0 channel voice
Note velocity7-bit16-bit
CC, channel pressure, poly pressure7-bit32-bit
Pitch bend14-bit32-bit
Per-note controllers and per-note pitch bendNo channel-voice equivalentNative MIDI 2.0 message forms
Note-on attributeNo equivalent fieldOptional 16-bit attribute data with an attribute type

MIDI 2.0 therefore carries more value precision and adds per-note expression, but it does not make every receiver apply every message. Separate per-note messages can address simultaneously sounding notes with different note numbers independently even when they share a channel; the instrument decides which of those forms it implements.

MIDI 1.0 can combine paired CC messages into 14-bit values; the table shows the width of a single CC message. MIDI 2.0 also includes MIDI-CI for capability discovery, Profiles for agreed controller behavior, and Property Exchange for exchanging device information. These are separate from UMP encoding: the libsonare APIs described here build, store, and deliver messages, and do not negotiate those capabilities with an external device.

Project event shape ​

ProjectMidiEvent is { ppq, data0, data1? } in JavaScript and (ppq, data0, data1) in Python. data0 is the first UMP word and data1 is the second (defaulting to zero when omitted). Project event lists carry channel-voice UMPs. SysEx payloads live in a separate store and are not represented by this flat type; setMidiEvents replaces the list and discards that store.

ppq is a position in quarter notes, not a 480-ticks-per-quarter integer. An MT 0x2 MIDI 1.0 channel-voice message uses one word. An MT 0x4 MIDI 2.0 channel-voice message uses two words.

Build MIDI 2.0 events ​

The JavaScript and Python project bindings expose the same builder families:

FamilyJavaScriptPythonData
Notesmidi2NoteOn, midi2NoteOffmidi2_note_on, midi2_note_off16-bit attack/release velocity; note-on velocity 0 still means note-on
Channel expressionmidi2Cc, midi2ChannelPressure, midi2PitchBendsnake_case equivalents32-bit controller, pressure, and bend; bend center 0x80000000
Poly pressuremidi2PolyPressuremidi2_poly_pressure32-bit pressure for one note
Programmidi2Programmidi2_programProgram plus optional bank MSB/LSB in the same message
RPN / NRPNregistered/assignable and relative controller builderssnake_case equivalents32-bit values or signed deltas
Per-noteregistered/assignable controller, per-note bendsnake_case equivalentsPer-note index 0..255, or 32-bit bend
Managementmidi2PerNoteManagementmidi2_per_note_managementDetach and reset flags for one note

The note, group, channel, controller, program, bank, and note-controller indices use their protocol ranges. MIDI 2.0 attributeType and attributeData are optional on midi2NoteOn; type 3 carries pitch 7.9 data.

typescript
import { Project, init } from '@libraz/libsonare';

await init(); // required by the browser/WASM build; this WASM package also requires initialization in Node
const project = new Project();
project.setSampleRate(48000);
const { clipId } = project.addMidiClip(0, 4);
const on = Project.midi2NoteOn(0, 0, 0, 60, 0xc000);
const bend = Project.midi2PerNotePitchBend(0.25, 0, 0, 60, 0x80000000);
const off = Project.midi2NoteOff(2, 0, 0, 60, 0x8000);
try {
  project.setMidiEvents(clipId, [on, bend, off]);
} finally {
  project.delete();
}
python
import libsonare as sonare

with sonare.Project() as project:
    project.set_sample_rate(48000)
    _track_id, clip_id = project.add_midi_clip(0.0, 4.0)
    on = sonare.Project.midi2_note_on(0.0, 0, 0, 60, 0xC000)
    bend = sonare.Project.midi2_per_note_pitch_bend(0.25, 0, 0, 60, 0x80000000)
    off = sonare.Project.midi2_note_off(2.0, 0, 0, 60, 0x8000)
    project.set_midi_events(clip_id, [on, bend, off])

MIDI 1.0 packers remain useful for a target that expects 7-bit values: midiNoteOn, midiCc, midiChannelPressure, and the other midi* methods produce canonical MIDI 1.0 UMP words. Converting a MIDI 2.0 event to MIDI 1.0 downscales the values and drops messages with no single MIDI 1.0 equivalent; use the MIDI 1.0 path only when that loss is acceptable.

Queue raw UMP in realtime ​

RealtimeEngine.pushMidiUmp(destinationId, words, renderFrame = -1) accepts one to four most-significant-first 32-bit words. The array length must match the message type in its first word. An MT 0x4 MIDI 2.0 channel-voice message is delivered at full width. pushMidiInputUmp(words, portTimeSamples = 0) applies the same message rules to the engine-owned input source after setMidiInputSource(...).

typescript
import { Project, RealtimeEngine, init } from '@libraz/libsonare';

await init();
const engine = new RealtimeEngine(48000, 128); // control-thread queue; render on the engine's audio thread
engine.setBuiltinInstrument({}, 0);
engine.setMidiInputSource(0);
const note = Project.midi2NoteOn(0, 0, 0, 60, 0xc000);
try {
  engine.pushMidiUmp(0, [note.data0, note.data1 ?? 0], -1);
  engine.pushMidiInputUmp([note.data0, note.data1 ?? 0], 0);
} finally {
  engine.destroy();
}
python
import libsonare as sonare

engine = sonare.RealtimeEngine(48000, 128)  # queue commands; render blocks through process()
engine.set_builtin_instrument()
engine.set_midi_input_source(0)
try:
    note = sonare.Project.midi2_note_on(0.0, 0, 0, 60, 0xC000)
    engine.push_midi_ump(0, [note[1], note[2]], render_frame=-1)
finally:
    engine.close()

The AudioWorklet SonareEngine facade has a narrower pushMidiUmp(trackId, word0, renderFrame = -1) method that accepts one word. Use RealtimeEngine for a two-word MIDI 2.0 channel-voice message. Raw MT 0x3 / MT 0x5 data messages are rejected by pushMidiUmp; send a complete SysEx frame through pushMidiSysex instead. Queue-full and malformed-word errors are reported by the binding.

What the built-in receivers apply ​

All three project/realtime instrument bindings receive MIDI 2.0 channel-voice UMPs with their supplied value width. Expression support remains instrument-specific:

ReceiverAppliesBoundary
Built-in waveform synthFull-width note velocity, channel pressure, polyphonic pressure, and per-note pitch bendMPE bend handling follows the built-in synth's zone configuration
NativeSynthFull-width note velocity, channel pressure, polyphonic pressure, and per-note pitch bendRPN 0 controls bend range; other MIDI 2.0 statuses are not implied to affect every patch
SoundFont playerFull-width note velocity, channel pressure, pitch bend, and MIDI 2.0 per-note pitchPolyphonic key pressure is deliberately ignored; MPE member channels still carry channel expression

The UMP keeps the original message even when a receiver ignores one expression type. See MIDI Input for the direct raw-UMP path and the Web MIDI bridge's MIDI 1.0 convenience methods.

MIDI 2.0 Clip File (SMF2CLIP) ​

A MIDI 2.0 Clip File is an in-memory UMP container with an SMF2CLIP header. Project.exportClipFile() exports the project tempo map and MIDI clips into a single clip container; importClipFile(...) adds the imported clip. The format keeps MIDI 2.0 channel-voice messages without converting their values.

It preserves 16-bit velocity, 32-bit CC, per-note and registered controllers, and bank-valid Program Change. This is the interchange path to use when those values matter. Clip files also carry SysEx7/8 data. Export serializes the project’s separate SysEx store into the file; import restores it to that store.

typescript
import { Project, init } from '@libraz/libsonare';

await init();
const project = new Project();
const imported = new Project();
try {
  const clipFile = project.exportClipFile(); // Uint8Array beginning with "SMF2CLIP"
  const clipId = imported.importClipFile(clipFile);
  console.log(clipId);
} finally {
  imported.delete();
  project.delete();
}
python
import libsonare as sonare

with sonare.Project() as project, sonare.Project() as imported:
    clip_file = project.export_clip_file()  # bytes beginning with b"SMF2CLIP"
    clip_id = imported.import_clip_file(clip_file)

Standard MIDI File export remains useful for MIDI 1.0 tools. SMF cannot represent every MIDI 2.0 value or per-note message without loss; project export drops MIDI 2.0-only events that have no MIDI 1.0 representation. SMF export uses format 1 with a tempo/signature track and 480 ticks per quarter note, so it is a compatibility format rather than a full-fidelity MIDI 2.0 archive.

Boundaries to keep visible ​

  • A ProjectMidiEvent contains the first two UMP words and only represents channel-voice project events. It is not a general UMP or SysEx container.
  • Raw realtime pushMidiUmp accepts the message's complete one-to-four-word packet, but rejects MT 0x3 and MT 0x5 data messages. Use pushMidiSysex for SysEx.
  • MIDI 1.0 helper output uses 7-bit channel values and 14-bit pitch bend. MIDI 2.0 builders use 16-bit or 32-bit fields as named above.
  • Receiver support is narrower than packet precision. A packet can retain poly pressure or a per-note controller while a particular instrument ignores it.
  • Edit MIDI Clips — replace, validate, route, and bake project event lists
  • Audio to MIDI — create MIDI 1.0 note pairs from audio
  • MIDI Input — bind live input and distinguish direct UMP from Web MIDI
  • Project MIDI — project-level tempo, annotations, and SMF overview