Pulp 1.88.0
Cross-platform audio plugin framework
Loading...
Searching...
No Matches
ump.hpp
Go to the documentation of this file.
1#pragma once
2
3// MIDI 2.0 Universal MIDI Packet (UMP) support
4// Implements the MIDI 2.0 UMP format for high-resolution note events,
5// per-note controllers, and backwards compatibility with MIDI 1.0.
6//
7// Key concepts:
8// - UMP: Universal MIDI Packet — 32/64/96/128-bit packets replacing raw bytes
9// - Message Type: determines packet size and interpretation
10// - Group: 0-15, logical grouping of channels (replaces cables)
11// - Channel Voice: note on/off, CC, pitch bend, etc.
12// - Per-Note: controllers, pitch bend, management per individual note
13
14#include <cstdint>
15#include <array>
16#include <string>
17
18namespace pulp::midi {
19
20// ── UMP Message Types ───────────────────────────────────────────────────
21
22enum class UmpMessageType : uint8_t {
23 Utility = 0x0, // 32-bit: JR Clock, JR Timestamp, delta clockstamp
24 System = 0x1, // 32-bit: System Real Time, System Common
25 Midi1ChannelVoice = 0x2, // 32-bit: MIDI 1.0 channel voice (backwards compat)
26 DataSysEx = 0x3, // 64-bit: SysEx, data messages
27 Midi2ChannelVoice = 0x4, // 64-bit: MIDI 2.0 channel voice (high-res)
28 Data128 = 0x5, // 128-bit: 8-byte SysEx, mixed data set
29};
30
31// ── UMP word length by message type ─────────────────────────────────────
32
44inline int ump_words_for_message_type(uint8_t message_type) {
45 switch (message_type & 0x0F) {
46 case 0x0: case 0x1: case 0x2:
47 case 0x6: case 0x7:
48 return 1;
49 case 0x3: case 0x4:
50 case 0x8: case 0x9: case 0xA:
51 return 2;
52 case 0xB: case 0xC:
53 return 3;
54 case 0x5: case 0xD: case 0xE: case 0xF:
55 return 4;
56 default:
57 return 1; // unreachable: the nibble is masked to 0x0-0xF
58 }
59}
60
61// ── UMP Status codes for MIDI 2.0 Channel Voice (type 0x4) ─────────────
62
80
81// ── UMP Packet ──────────────────────────────────────────────────────────
82
83// A Universal MIDI Packet: 1-4 32-bit words
84struct UmpPacket {
85 std::array<uint32_t, 4> words = {};
86 int word_count = 0; // 1, 2, 3, or 4
87
88 // Extract fields from word 0
90 return static_cast<UmpMessageType>((words[0] >> 28) & 0x0F);
91 }
92
93 uint8_t group() const {
94 return static_cast<uint8_t>((words[0] >> 24) & 0x0F);
95 }
96
97 uint8_t status() const {
98 return static_cast<uint8_t>((words[0] >> 16) & 0xFF);
99 }
100
101 uint8_t channel() const {
102 return static_cast<uint8_t>((words[0] >> 16) & 0x0F);
103 }
104
105 // For MIDI 2.0 Channel Voice (64-bit):
106 uint8_t note_number() const {
107 return static_cast<uint8_t>((words[0] >> 8) & 0x7F);
108 }
109
110 // MIDI 2.0 uses 16-bit velocity in word 1
111 uint16_t velocity_16() const {
112 return static_cast<uint16_t>((words[1] >> 16) & 0xFFFF);
113 }
114
115 // Convert 16-bit velocity to 7-bit for MIDI 1.0 compat
116 uint8_t velocity_7() const {
117 return static_cast<uint8_t>(velocity_16() >> 9);
118 }
119
120 // 32-bit data value (for CC, pitch bend, pressure)
121 uint32_t data_32() const { return words[1]; }
122
123 // Per-note attribute type (bits 8-15 of word 0)
124 uint8_t attribute_type() const {
125 return static_cast<uint8_t>(words[0] & 0xFF);
126 }
127
128 // Per-note attribute data (bits 0-15 of word 1)
129 uint16_t attribute_data() const {
130 return static_cast<uint16_t>(words[1] & 0xFFFF);
131 }
132
133 // Packet size in words based on message type. Delegates to the
134 // spec-complete free function so this and any raw-nibble cursor walk share
135 // one word-length table.
136 static int size_for_type(UmpMessageType type) {
137 return ump_words_for_message_type(static_cast<uint8_t>(type));
138 }
139
140 // ── Factory methods ─────────────────────────────────────────────────
141
142 // MIDI 2.0 Note On (64-bit, 16-bit velocity)
143 static UmpPacket note_on_2(uint8_t group, uint8_t channel,
144 uint8_t note, uint16_t velocity,
145 uint8_t attr_type = 0, uint16_t attr_data = 0) {
146 UmpPacket p;
147 p.word_count = 2;
148 p.words[0] = (0x4u << 28) | (uint32_t(group & 0x0F) << 24)
149 | (uint32_t(0x90 | (channel & 0x0F)) << 16)
150 | (uint32_t(note & 0x7F) << 8)
151 | uint32_t(attr_type);
152 p.words[1] = (uint32_t(velocity) << 16) | uint32_t(attr_data);
153 return p;
154 }
155
156 // MIDI 2.0 Note Off (64-bit, 16-bit velocity)
157 static UmpPacket note_off_2(uint8_t group, uint8_t channel,
158 uint8_t note, uint16_t velocity = 0) {
159 UmpPacket p;
160 p.word_count = 2;
161 p.words[0] = (0x4u << 28) | (uint32_t(group & 0x0F) << 24)
162 | (uint32_t(0x80 | (channel & 0x0F)) << 16)
163 | (uint32_t(note & 0x7F) << 8);
164 p.words[1] = (uint32_t(velocity) << 16);
165 return p;
166 }
167
168 // MIDI 2.0 Control Change (64-bit, 32-bit value)
169 static UmpPacket cc_2(uint8_t group, uint8_t channel,
170 uint8_t controller, uint32_t value) {
171 UmpPacket p;
172 p.word_count = 2;
173 p.words[0] = (0x4u << 28) | (uint32_t(group & 0x0F) << 24)
174 | (uint32_t(0xB0 | (channel & 0x0F)) << 16)
175 | (uint32_t(controller & 0x7F) << 8);
176 p.words[1] = value;
177 return p;
178 }
179
180 // MIDI 2.0 Pitch Bend (64-bit, 32-bit value)
181 static UmpPacket pitch_bend_2(uint8_t group, uint8_t channel, uint32_t value) {
182 UmpPacket p;
183 p.word_count = 2;
184 p.words[0] = (0x4u << 28) | (uint32_t(group & 0x0F) << 24)
185 | (uint32_t(0xE0 | (channel & 0x0F)) << 16);
186 p.words[1] = value;
187 return p;
188 }
189
190 // MIDI 2.0 Per-Note Pitch Bend
192 uint8_t note, uint32_t value) {
193 UmpPacket p;
194 p.word_count = 2;
195 p.words[0] = (0x4u << 28) | (uint32_t(group & 0x0F) << 24)
196 | (uint32_t(0x60 | (channel & 0x0F)) << 16)
197 | (uint32_t(note & 0x7F) << 8);
198 p.words[1] = value;
199 return p;
200 }
201
202 // MIDI 2.0 Registered Per-Note Controller (status 0x00). Byte 2 is
203 // the note number, byte 3 is the controller index per the UMP spec.
205 uint8_t note, uint8_t cc_index,
206 uint32_t value) {
207 UmpPacket p;
208 p.word_count = 2;
209 p.words[0] = (0x4u << 28) | (uint32_t(group & 0x0F) << 24)
210 | (uint32_t(0x00 | (channel & 0x0F)) << 16)
211 | (uint32_t(note & 0x7F) << 8)
212 | uint32_t(cc_index & 0x7F);
213 p.words[1] = value;
214 return p;
215 }
216
217 // MIDI 1.0 Channel Voice (32-bit backwards compat)
218 static UmpPacket midi1_note_on(uint8_t group, uint8_t channel,
219 uint8_t note, uint8_t velocity) {
220 UmpPacket p;
221 p.word_count = 1;
222 p.words[0] = (0x2u << 28) | (uint32_t(group & 0x0F) << 24)
223 | (uint32_t(0x90 | (channel & 0x0F)) << 16)
224 | (uint32_t(note & 0x7F) << 8)
225 | uint32_t(velocity & 0x7F);
226 return p;
227 }
228
229 // MIDI 2.0 Assignable Per-Note Controller (status 0x10). Byte 2 is
230 // the note number, byte 3 is the user-assigned controller index.
231 // Distinct from `registered_per_note_cc` (status 0x00) which uses
232 // standard registered controller IDs.
234 uint8_t note, uint8_t cc_index,
235 uint32_t value) {
236 UmpPacket p;
237 p.word_count = 2;
238 p.words[0] = (0x4u << 28) | (uint32_t(group & 0x0F) << 24)
239 | (uint32_t(0x10 | (channel & 0x0F)) << 16)
240 | (uint32_t(note & 0x7F) << 8)
241 | uint32_t(cc_index & 0x7F);
242 p.words[1] = value;
243 return p;
244 }
245
246 // MIDI 2.0 Per-Note Management (status 0xF0). Byte 2 is the note
247 // number; byte 3 carries flags (bit 0 = reset per-note controllers
248 // to default, bit 1 = detach previously-held controllers from the
249 // note so they stop tracking it). Word 1 is reserved by the spec.
251 uint8_t note, uint8_t flags) {
252 UmpPacket p;
253 p.word_count = 2;
254 p.words[0] = (0x4u << 28) | (uint32_t(group & 0x0F) << 24)
255 | (uint32_t(0xF0 | (channel & 0x0F)) << 16)
256 | (uint32_t(note & 0x7F) << 8)
257 | uint32_t(flags);
258 p.words[1] = 0;
259 return p;
260 }
261
262 // Per-Note Management flags (UMP spec):
263 static constexpr uint8_t kPerNoteResetControllers = 0x01;
264 static constexpr uint8_t kPerNoteDetachControllers = 0x02;
265};
266
267// ── UMP packet-stream cursor ─────────────────────────────────────────────
268
278template <typename Visitor>
279inline void walk_ump_packet(const uint32_t* words, uint32_t word_count,
280 Visitor&& visit) {
281 if (!words) return;
282 uint32_t w = 0;
283 while (w < word_count) {
284 const uint8_t mt = static_cast<uint8_t>((words[w] >> 28) & 0x0F);
285 const uint32_t n = static_cast<uint32_t>(ump_words_for_message_type(mt));
286 if (w + n > word_count) break; // truncated: declared length overruns
287 visit(mt, words + w, n);
288 w += n;
289 }
290}
291
292// ── Utility messages (UMP type 0x0) ──────────────────────────────────────
293
301enum class UtilityStatus : uint8_t {
302 Noop = 0x0,
303 JrClock = 0x1,
307};
308
311 return static_cast<UtilityStatus>((p.words[0] >> 20) & 0x0F);
312}
313
318inline uint16_t jr_value_16(const UmpPacket& p) {
319 return static_cast<uint16_t>(p.words[0] & 0xFFFF);
320}
321
324constexpr double kJrTicksPerSecond = 31250.0;
325
326inline double jr_value_seconds(uint16_t value) {
327 return static_cast<double>(value) / kJrTicksPerSecond;
328}
329
330inline double jr_value_microseconds(uint16_t value) {
331 return static_cast<double>(value) * (1'000'000.0 / kJrTicksPerSecond);
332}
333
335inline UmpPacket make_jr_clock(uint8_t group, uint16_t value) {
336 UmpPacket p;
337 p.word_count = 1;
338 p.words[0] = (0x0u << 28) | (uint32_t(group & 0x0F) << 24)
339 | (uint32_t(0x1) << 20)
340 | uint32_t(value);
341 return p;
342}
343
345inline UmpPacket make_jr_timestamp(uint8_t group, uint16_t value) {
346 UmpPacket p;
347 p.word_count = 1;
348 p.words[0] = (0x0u << 28) | (uint32_t(group & 0x0F) << 24)
349 | (uint32_t(0x2) << 20)
350 | uint32_t(value);
351 return p;
352}
353
354// ── MPE (MIDI Polyphonic Expression) ────────────────────────────────────
355
356// MPE zone configuration
357struct MpeZone {
358 uint8_t manager_channel = 0; // Zone manager (0 for lower, 15 for upper)
359 uint8_t member_channels = 0; // Number of member channels (1-15)
360
361 bool is_lower() const { return manager_channel == 0; }
362 bool is_upper() const { return manager_channel == 15; }
363
364 // Check if a channel belongs to this zone's member channels
365 bool contains_channel(uint8_t ch) const {
366 if (member_channels == 0) return false;
367 if (is_lower()) return ch >= 1 && ch <= member_channels;
368 if (is_upper()) return ch >= (15 - member_channels) && ch <= 14;
369 return false;
370 }
371};
372
373// MPE configuration for a MIDI port
374struct MpeConfig {
375 MpeZone lower_zone; // Manager on channel 0
376 MpeZone upper_zone; // Manager on channel 15
377
378 // Standard single lower zone (most common MPE setup)
379 static MpeConfig standard_lower(uint8_t members = 15) {
380 MpeConfig cfg;
381 cfg.lower_zone = {0, members};
382 return cfg;
383 }
384
385 // Dual zone (lower + upper)
386 static MpeConfig dual(uint8_t lower_members, uint8_t upper_members) {
387 MpeConfig cfg;
388 cfg.lower_zone = {0, lower_members};
389 cfg.upper_zone = {15, upper_members};
390 return cfg;
391 }
392
393 // Check if a channel is a zone manager
394 bool is_manager_channel(uint8_t ch) const {
395 return ch == lower_zone.manager_channel || ch == upper_zone.manager_channel;
396 }
397
398 // Get the zone a member channel belongs to (nullptr if none)
399 const MpeZone* zone_for_channel(uint8_t ch) const {
400 if (lower_zone.contains_channel(ch)) return &lower_zone;
401 if (upper_zone.contains_channel(ch)) return &upper_zone;
402 return nullptr;
403 }
404};
405
406} // namespace pulp::midi
Definition custom_node_events.hpp:26
constexpr double kJrTicksPerSecond
One JR Clock tick is 1/31250 of a second (~32 microseconds).
Definition ump.hpp:324
double jr_value_seconds(uint16_t value)
Definition ump.hpp:326
Midi2Status
Definition ump.hpp:63
@ RegisteredCC
Definition ump.hpp:66
@ AssignableCC
Definition ump.hpp:67
@ NoteOn
Definition ump.hpp:72
@ PerNotePitchBend
Definition ump.hpp:70
@ PolyPressure
Definition ump.hpp:73
@ PerNoteManagement
Definition ump.hpp:78
@ AssignablePerNoteCC
Definition ump.hpp:65
@ NoteOff
Definition ump.hpp:71
@ RelativeRegisteredCC
Definition ump.hpp:68
@ RegisteredPerNoteCC
Definition ump.hpp:64
@ ControlChange
Definition ump.hpp:74
@ ChannelPressure
Definition ump.hpp:76
@ PitchBend
Definition ump.hpp:77
@ RelativeAssignableCC
Definition ump.hpp:69
@ ProgramChange
Definition ump.hpp:75
UmpPacket make_jr_clock(uint8_t group, uint16_t value)
Build a JR Clock utility packet (status 0x1).
Definition ump.hpp:335
UtilityStatus
Utility message status codes (bits 20-23 of word 0 for type 0x0).
Definition ump.hpp:301
@ Noop
Definition ump.hpp:302
@ JrClock
Definition ump.hpp:303
@ DeltaClockstampTicksPerQuarter
Definition ump.hpp:305
@ JrTimestamp
Definition ump.hpp:304
@ DeltaClockstamp
Definition ump.hpp:306
UmpPacket make_jr_timestamp(uint8_t group, uint16_t value)
Build a JR Timestamp utility packet (status 0x2).
Definition ump.hpp:345
uint16_t jr_value_16(const UmpPacket &p)
Extract the 16-bit JR Clock / JR Timestamp value from a type-0 UMP packet (bits 0-15 of word 0).
Definition ump.hpp:318
int ump_words_for_message_type(uint8_t message_type)
Spec-complete UMP packet length, in 32-bit words, for a raw message-type nibble (bits 28-31 of word 0...
Definition ump.hpp:44
UtilityStatus utility_status(const UmpPacket &p)
Extract the utility status nibble from a type-0 UMP packet (bits 20-23 of word 0).
Definition ump.hpp:310
void walk_ump_packet(const uint32_t *words, uint32_t word_count, Visitor &&visit)
Walk one UMP packet's contiguous word array, visiting each complete message in order.
Definition ump.hpp:279
UmpMessageType
Definition ump.hpp:22
@ Midi2ChannelVoice
Definition ump.hpp:27
@ Midi1ChannelVoice
Definition ump.hpp:25
@ Data128
Definition ump.hpp:28
@ Utility
Definition ump.hpp:23
@ System
Definition ump.hpp:24
@ DataSysEx
Definition ump.hpp:26
double jr_value_microseconds(uint16_t value)
Definition ump.hpp:330
Definition ump.hpp:374
const MpeZone * zone_for_channel(uint8_t ch) const
Definition ump.hpp:399
static MpeConfig dual(uint8_t lower_members, uint8_t upper_members)
Definition ump.hpp:386
static MpeConfig standard_lower(uint8_t members=15)
Definition ump.hpp:379
bool is_manager_channel(uint8_t ch) const
Definition ump.hpp:394
MpeZone lower_zone
Definition ump.hpp:375
MpeZone upper_zone
Definition ump.hpp:376
Definition ump.hpp:357
bool is_upper() const
Definition ump.hpp:362
bool is_lower() const
Definition ump.hpp:361
uint8_t manager_channel
Definition ump.hpp:358
uint8_t member_channels
Definition ump.hpp:359
bool contains_channel(uint8_t ch) const
Definition ump.hpp:365
Definition ump.hpp:84
uint8_t velocity_7() const
Definition ump.hpp:116
static constexpr uint8_t kPerNoteDetachControllers
Definition ump.hpp:264
static UmpPacket midi1_note_on(uint8_t group, uint8_t channel, uint8_t note, uint8_t velocity)
Definition ump.hpp:218
static int size_for_type(UmpMessageType type)
Definition ump.hpp:136
uint16_t attribute_data() const
Definition ump.hpp:129
static constexpr uint8_t kPerNoteResetControllers
Definition ump.hpp:263
static UmpPacket per_note_management(uint8_t group, uint8_t channel, uint8_t note, uint8_t flags)
Definition ump.hpp:250
uint8_t group() const
Definition ump.hpp:93
static UmpPacket assignable_per_note_cc(uint8_t group, uint8_t channel, uint8_t note, uint8_t cc_index, uint32_t value)
Definition ump.hpp:233
uint8_t note_number() const
Definition ump.hpp:106
uint8_t channel() const
Definition ump.hpp:101
uint32_t data_32() const
Definition ump.hpp:121
static UmpPacket note_off_2(uint8_t group, uint8_t channel, uint8_t note, uint16_t velocity=0)
Definition ump.hpp:157
uint16_t velocity_16() const
Definition ump.hpp:111
static UmpPacket per_note_pitch_bend(uint8_t group, uint8_t channel, uint8_t note, uint32_t value)
Definition ump.hpp:191
static UmpPacket cc_2(uint8_t group, uint8_t channel, uint8_t controller, uint32_t value)
Definition ump.hpp:169
static UmpPacket note_on_2(uint8_t group, uint8_t channel, uint8_t note, uint16_t velocity, uint8_t attr_type=0, uint16_t attr_data=0)
Definition ump.hpp:143
uint8_t attribute_type() const
Definition ump.hpp:124
uint8_t status() const
Definition ump.hpp:97
std::array< uint32_t, 4 > words
Definition ump.hpp:85
UmpMessageType message_type() const
Definition ump.hpp:89
static UmpPacket pitch_bend_2(uint8_t group, uint8_t channel, uint32_t value)
Definition ump.hpp:181
static UmpPacket registered_per_note_cc(uint8_t group, uint8_t channel, uint8_t note, uint8_t cc_index, uint32_t value)
Definition ump.hpp:204
int word_count
Definition ump.hpp:86