Pulp 1.88.0
Cross-platform audio plugin framework
Loading...
Searching...
No Matches
ump_conversion.hpp
Go to the documentation of this file.
1#pragma once
2
16
17#include <pulp/midi/buffer.hpp>
18#include <pulp/midi/message.hpp>
19#include <pulp/midi/ump.hpp>
21#include <cstdint>
22
23namespace pulp::midi {
24
25// ── 7/14/16/32-bit scaling helpers ──────────────────────────────────────
26
29inline uint16_t scale_7_to_16(uint8_t v7) {
30 // Rounded division so 100 → 51604 round-trips back to 100 (not 99).
31 return static_cast<uint16_t>(
32 (static_cast<uint32_t>(v7) * 0xFFFFu + 63u) / 127u);
33}
34
36inline uint8_t scale_16_to_7(uint16_t v16) {
37 return static_cast<uint8_t>(
38 (static_cast<uint32_t>(v16) * 127u + 0x7FFFu) / 0xFFFFu);
39}
40
43inline uint32_t scale_14_to_32(uint16_t v14) {
44 constexpr uint32_t src_max = 0x3FFFu;
45 constexpr uint32_t src_center = 0x2000u;
46 constexpr uint64_t dst_center = 0x80000000ull;
47 if (v14 <= src_center) {
48 return static_cast<uint32_t>((static_cast<uint64_t>(v14) * dst_center) / src_center);
49 }
50 const uint64_t above = static_cast<uint64_t>(v14 - src_center);
51 const uint64_t dst_above_max = 0x7FFFFFFFull;
52 return static_cast<uint32_t>(dst_center + (above * dst_above_max) / (src_max - src_center));
53}
54
56inline uint16_t scale_32_to_14(uint32_t v32) {
57 constexpr uint32_t dst_max = 0x3FFFu;
58 constexpr uint32_t dst_center = 0x2000u;
59 constexpr uint64_t src_center = 0x80000000ull;
60 if (v32 <= src_center) {
61 return static_cast<uint16_t>((static_cast<uint64_t>(v32) * dst_center) / src_center);
62 }
63 const uint64_t above = static_cast<uint64_t>(v32 - src_center);
64 const uint64_t src_above_max = 0x7FFFFFFFull;
65 return static_cast<uint16_t>(dst_center + (above * (dst_max - dst_center)) / src_above_max);
66}
67
68// ── MIDI 1.0 → UMP ──────────────────────────────────────────────────────
69
73inline UmpPacket midi1_event_to_ump2(const MidiEvent& ev, uint8_t group = 0) {
74 const uint8_t status = ev.data()[0] & 0xF0;
75 const uint8_t channel = ev.channel();
76
77 if (status == 0x90 && ev.velocity() == 0) {
78 // Velocity-0 note-on is a note-off.
79 return UmpPacket::note_off_2(group, channel, ev.note(), 0);
80 }
81 switch (status) {
82 case 0x80:
83 return UmpPacket::note_off_2(group, channel, ev.note(),
85 case 0x90:
86 return UmpPacket::note_on_2(group, channel, ev.note(),
88 case 0xB0:
89 return UmpPacket::cc_2(group, channel, ev.cc_number(),
90 static_cast<uint32_t>(ev.cc_value()) << 25);
91 case 0xE0: {
92 const uint16_t raw = static_cast<uint16_t>(
93 ev.data()[1] | (uint16_t(ev.data()[2]) << 7));
94 return UmpPacket::pitch_bend_2(group, channel, scale_14_to_32(raw));
95 }
96 default:
97 break;
98 }
99 // Fall back to a "raw bytes" UMP carrying the original status + data.
100 // System messages (status >= 0xF0: real-time clock/start/stop/active-
101 // sensing and system-common like MTC / song-position) are UMP Type 0x1
102 // (System Real Time and System Common). Everything else that reaches
103 // here (program change, channel pressure, …) is a MIDI 1.0 Channel
104 // Voice message and stays Type 0x2. Encoding system messages as Type
105 // 0x2 produced malformed UMP that transport/clock-sync consumers reject.
106 const uint8_t status_byte = ev.data()[0];
107 const uint32_t mt = (status_byte >= 0xF0) ? 0x1u : 0x2u;
108 UmpPacket p;
109 p.word_count = 1;
110 p.words[0] = (mt << 28)
111 | (uint32_t(group & 0x0F) << 24)
112 | (uint32_t(status_byte) << 16)
113 | (uint32_t(ev.size() > 1 ? ev.data()[1] : 0) << 8)
114 | uint32_t(ev.size() > 2 ? ev.data()[2] : 0);
115 return p;
116}
117
119inline void midi1_to_ump(const MidiBuffer& src, UmpBuffer& dst, uint8_t group = 0) {
120 for (const auto& ev : src) {
121 dst.add({midi1_event_to_ump2(ev, group), ev.sample_offset});
122 }
123}
124
125// ── UMP → MIDI 1.0 ──────────────────────────────────────────────────────
126
136 uint8_t program = 0;
137 bool bank_valid = false;
138 uint8_t bank_msb = 0;
139 uint8_t bank_lsb = 0;
140};
141
149 f.program = static_cast<uint8_t>((p.words[1] >> 24) & 0x7F);
150 f.bank_valid = (p.words[0] & 0x01u) != 0;
151 f.bank_msb = static_cast<uint8_t>((p.words[1] >> 8) & 0x7F);
152 f.bank_lsb = static_cast<uint8_t>(p.words[1] & 0x7F);
153 return f;
154}
155
157inline constexpr uint8_t kBankSelectMsbCc = 0;
158inline constexpr uint8_t kBankSelectLsbCc = 32;
159
170inline bool ump_to_midi1_event(const UmpPacket& p, MidiEvent& out) {
171 const auto mt = p.message_type();
172 const uint8_t ch = p.channel();
173
175 // Byte 1 is status|channel (already in words[0] bits 16-23),
176 // Byte 2 in bits 8-15, byte 3 in bits 0-7.
177 const uint8_t s = static_cast<uint8_t>((p.words[0] >> 16) & 0xFF);
178 const uint8_t d1 = static_cast<uint8_t>((p.words[0] >> 8) & 0xFF);
179 const uint8_t d2 = static_cast<uint8_t>(p.words[0] & 0xFF);
180 out = {choc::midi::ShortMessage(s, d1, d2), 0, 0.0};
181 return true;
182 }
183 if (mt == UmpMessageType::System) {
184 // System Real Time (clock 0xF8, start/continue/stop, active sensing)
185 // and System Common (MTC 0xF1, song position 0xF2, …). The UMP carries
186 // the status byte plus up to two data bytes in the same byte layout as
187 // a MIDI 1.0 Channel Voice UMP; ShortMessage derives the correct
188 // length from the status byte. Dropping these (as the prior
189 // channel-voice-only path did) silently loses clock/transport input.
190 const uint8_t s = static_cast<uint8_t>((p.words[0] >> 16) & 0xFF);
191 const uint8_t d1 = static_cast<uint8_t>((p.words[0] >> 8) & 0xFF);
192 const uint8_t d2 = static_cast<uint8_t>(p.words[0] & 0xFF);
193 out = {choc::midi::ShortMessage(s, d1, d2), 0, 0.0};
194 return true;
195 }
196 if (mt != UmpMessageType::Midi2ChannelVoice) return false;
197
198 const uint8_t status_byte = static_cast<uint8_t>((p.words[0] >> 16) & 0xF0);
199 switch (status_byte) {
200 case 0x80:
201 out = MidiEvent::note_off(ch, p.note_number(),
202 scale_16_to_7(p.velocity_16()));
203 return true;
204 case 0x90: {
205 // MIDI 2.0 velocity 0 is *not* a note-off (that's an explicit
206 // 0x80 status). Clamp to 1 so the MIDI 1.0 representation
207 // stays semantically a note-on.
208 const uint16_t v16 = p.velocity_16();
209 uint8_t v7 = scale_16_to_7(v16);
210 if (v16 > 0 && v7 == 0) v7 = 1;
211 out = MidiEvent::note_on(ch, p.note_number(), v7);
212 return true;
213 }
214 case 0xA0:
215 // Poly key pressure: note in word 0 bits 8-15, 32-bit value in
216 // word 1 (same >> 25 narrowing the CC case uses).
217 out = {choc::midi::ShortMessage(static_cast<uint8_t>(0xA0 | (ch & 0x0F)),
218 p.note_number(),
219 static_cast<uint8_t>(p.data_32() >> 25)),
220 0, 0.0};
221 return true;
222 case 0xB0:
223 out = MidiEvent::cc(ch, static_cast<uint8_t>((p.words[0] >> 8) & 0x7F),
224 static_cast<uint8_t>(p.data_32() >> 25));
225 return true;
226 case 0xC0: {
227 // Program change is NOT shaped like the CC or pressure cases: the
228 // program lives in the TOP byte of word 1, not in a 32-bit data
229 // value, and word 0's low byte is an option-flag field rather than
230 // a controller index.
231 //
232 // Known limit: a bank-valid program change is three MIDI 1.0
233 // messages (CC 0 bank MSB, CC 32 bank LSB, then the program
234 // change). This function has a single MidiEvent out-parameter and
235 // so can only emit the program change; the bank is not
236 // representable here. It is not silently discarded — the bytes
237 // stay readable via `ump_program_change_fields`, and the
238 // buffer-level `ump_to_midi1` emits the full three-message
239 // sequence. A caller using this single-event entry point directly
240 // must consult `ump_program_change_fields` if it needs the bank.
241 const auto fields = ump_program_change_fields(p);
242 out = MidiEvent::program_change(ch, fields.program);
243 return true;
244 }
245 case 0xD0:
246 // Channel pressure carries no note byte; the 32-bit value in word 1
247 // narrows to the single MIDI 1.0 data byte. This is the MPE
248 // pressure axis, so dropping it leaves an MPE plug-in reading a
249 // stream with no pressure the moment a host speaks MIDI 2.0.
250 out = {choc::midi::ShortMessage(static_cast<uint8_t>(0xD0 | (ch & 0x0F)),
251 static_cast<uint8_t>(p.data_32() >> 25), 0),
252 0, 0.0};
253 return true;
254 case 0xE0:
255 out = MidiEvent::pitch_bend(ch, scale_32_to_14(p.data_32()));
256 return true;
257 default:
258 return false;
259 }
260}
261
265 if (p.message_type() != UmpMessageType::Midi2ChannelVoice)
266 return false;
267 if ((static_cast<uint8_t>((p.words[0] >> 16) & 0xF0)) != 0xC0)
268 return false;
270}
271
281inline void ump_to_midi1(const UmpBuffer& src, MidiBuffer& dst) {
282 for (const auto& ue : src) {
283 if (ump_is_bank_valid_program_change(ue.packet)) {
284 const auto fields = ump_program_change_fields(ue.packet);
285 const uint8_t ch = ue.packet.channel();
286 auto msb = MidiEvent::cc(ch, kBankSelectMsbCc, fields.bank_msb);
287 msb.sample_offset = ue.sample_offset;
288 dst.add(msb);
289 auto lsb = MidiEvent::cc(ch, kBankSelectLsbCc, fields.bank_lsb);
290 lsb.sample_offset = ue.sample_offset;
291 dst.add(lsb);
292 }
293 MidiEvent ev{};
294 if (ump_to_midi1_event(ue.packet, ev)) {
295 ev.sample_offset = ue.sample_offset;
296 dst.add(ev);
297 }
298 }
299}
300
301} // namespace pulp::midi
Collection of timestamped MIDI events within a single audio buffer period.
Definition buffer.hpp:31
bool add(const MidiEvent &event)
Append a MIDI event to the buffer.
Definition buffer.hpp:98
A time-ordered list of UMP events for one audio block.
Definition ump_buffer.hpp:41
bool add(const UmpEvent &e)
Definition ump_buffer.hpp:63
Definition custom_node_events.hpp:26
uint16_t scale_7_to_16(uint8_t v7)
Scale a 7-bit value (0..127) to 16-bit (0..65535) per MIDI 2.0 spec.
Definition ump_conversion.hpp:29
constexpr uint8_t kBankSelectMsbCc
MIDI 1.0 controller numbers carrying Bank Select for a program change.
Definition ump_conversion.hpp:157
uint32_t scale_14_to_32(uint16_t v14)
Scale a 14-bit value (0..16383, center 8192) to 32-bit (center 0x80000000) using MIDI 2....
Definition ump_conversion.hpp:43
void midi1_to_ump(const MidiBuffer &src, UmpBuffer &dst, uint8_t group=0)
Append a MIDI 1.0 buffer to a UmpBuffer as MIDI 2.0 UMP packets.
Definition ump_conversion.hpp:119
uint16_t scale_32_to_14(uint32_t v32)
Scale a 32-bit value back to 14-bit (0..16383), inverse of scale_14_to_32.
Definition ump_conversion.hpp:56
bool ump_to_midi1_event(const UmpPacket &p, MidiEvent &out)
Convert a UMP Channel Voice packet into a MIDI 1.0 event when possible.
Definition ump_conversion.hpp:170
bool ump_is_bank_valid_program_change(const UmpPacket &p)
True when p is a MIDI 2.0 Program Change carrying a valid bank, i.e.
Definition ump_conversion.hpp:264
uint8_t scale_16_to_7(uint16_t v16)
Scale 16-bit (0..65535) down to 7-bit (0..127).
Definition ump_conversion.hpp:36
constexpr uint8_t kBankSelectLsbCc
Definition ump_conversion.hpp:158
Midi2ProgramChange ump_program_change_fields(const UmpPacket &p)
Decode a MIDI 2.0 Program Change packet's fields without converting it.
Definition ump_conversion.hpp:147
@ Midi2ChannelVoice
Definition ump.hpp:27
@ Midi1ChannelVoice
Definition ump.hpp:25
@ System
Definition ump.hpp:24
UmpPacket midi1_event_to_ump2(const MidiEvent &ev, uint8_t group=0)
Convert a single MIDI 1.0 event to a MIDI 2.0 Channel Voice UMP packet.
Definition ump_conversion.hpp:73
void ump_to_midi1(const UmpBuffer &src, MidiBuffer &dst)
Flatten a UmpBuffer into a MidiBuffer.
Definition ump_conversion.hpp:281
Decoded fields of a MIDI 2.0 Channel Voice Program Change (status 0xC).
Definition ump_conversion.hpp:135
uint8_t bank_msb
0..127, meaningful only when bank_valid
Definition ump_conversion.hpp:138
bool bank_valid
option-flag bit 0
Definition ump_conversion.hpp:137
uint8_t bank_lsb
0..127, meaningful only when bank_valid
Definition ump_conversion.hpp:139
uint8_t program
0..127
Definition ump_conversion.hpp:136
Timestamped MIDI event combining a choc::midi::ShortMessage with timing info.
Definition message.hpp:32
uint8_t cc_value() const
Definition message.hpp:115
uint8_t note() const
Definition message.hpp:112
static MidiEvent cc(uint8_t channel, uint8_t controller, uint8_t value)
Create a Control Change event.
Definition message.hpp:64
uint8_t cc_number() const
Definition message.hpp:114
static MidiEvent program_change(uint8_t channel, uint8_t program)
Create a Program Change event.
Definition message.hpp:95
const uint8_t * data() const
Raw MIDI byte data (for format adapters that need direct byte access).
Definition message.hpp:38
uint32_t size() const
Number of bytes in the MIDI message (1-3).
Definition message.hpp:40
uint8_t channel() const
MIDI channel (0-15).
Definition message.hpp:111
static MidiEvent note_on(uint8_t channel, uint8_t note, uint8_t velocity)
Create a Note On event.
Definition message.hpp:46
static MidiEvent note_off(uint8_t channel, uint8_t note, uint8_t velocity=0)
Create a Note Off event.
Definition message.hpp:54
int32_t sample_offset
Sample position within the current buffer block.
Definition message.hpp:34
uint8_t velocity() const
Definition message.hpp:113
static MidiEvent pitch_bend(uint8_t channel, uint16_t value)
Create a Pitch Bend event.
Definition message.hpp:73
Definition ump.hpp:84
static UmpPacket note_off_2(uint8_t group, uint8_t channel, uint8_t note, uint16_t velocity=0)
Definition ump.hpp:157
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
static UmpPacket pitch_bend_2(uint8_t group, uint8_t channel, uint32_t value)
Definition ump.hpp:181
Sample-accurate MIDI 2.0 UMP packet stream.