Skip to content

Commit 27a1997

Browse files
committed
Fix SysEx payload display
Show remaining undecoded payload for Universal System Exclusive messages.
1 parent b2f4c92 commit 27a1997

6 files changed

Lines changed: 85 additions & 65 deletions

File tree

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ requires-python = '>=3.10'
2020
dependencies = [
2121
'dearpygui~=2.0.0',
2222
'dearpygui-ext~=2.0.0',
23-
'midi_const~=0.1.0',
23+
'midi_const~=0.1.2',
2424
'mido~=1.3.0', # FIXME: currently using custom 1.2.11a1 with EOX, running status and delta time support
2525
'python-rtmidi~=1.5.5', # While it's mido's default backend, we explicitly require it for some features.
2626
'pillow~=11.0.0',

src/midiexplorer/__main__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ def main() -> None:
3939
Logger.log(f"Configuring app using init file: {INIT_FILENAME}")
4040
dpg.configure_app(
4141
# FIXME: upstream documentation is misleading.
42-
load_init_file=INIT_FILENAME, # Non-modifiable vendor init file
42+
#load_init_file=INIT_FILENAME, # Non-modifiable vendor init file
4343
docking=True,
4444
docking_space=True,
4545
docking_shift_only=False,

src/midiexplorer/gui/windows/mon/__init__.py

Lines changed: 30 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -704,35 +704,36 @@ def create() -> None:
704704
# System Exclusive
705705
# -----------------
706706
with dpg.collapsing_header(label="System Exclusive", default_open=not DEBUG):
707-
708-
with dpg.child_window(tag='mon_sysex_container', height=120, border=False):
709-
with dpg.group():
710-
with dpg.group(horizontal=True):
711-
title = "ID"
712-
dpg.add_text(title)
713-
dpg.add_input_text(source='syx_id_region', readonly=True, width=200)
714-
tooltip_preconv(f"{title} (Region)", 'syx_id_region', 'syx_id_val')
715-
dpg.add_input_text(source='syx_id_group', readonly=True, width=200)
716-
tooltip_preconv(f"{title} (Group)", 'syx_id_group', 'syx_id_val')
717-
dpg.add_input_text(source='syx_id_name', readonly=True, width=200)
718-
tooltip_preconv(f"{title} (Name)", 'syx_id_name', 'syx_id_val')
719-
with dpg.group(horizontal=True):
720-
title = "Device ID"
721-
source = 'syx_device_id'
722-
dpg.add_text(title)
723-
dpg.add_input_text(source=source, readonly=True, width=50)
724-
tooltip_preconv(static_title=title, values_source=source)
725-
with dpg.group(horizontal=True, tag='syx_payload_container'):
726-
title = "Undecoded Payload"
727-
dpg.add_text(title)
728-
source = 'syx_payload'
729-
dpg.add_input_text(source=source, readonly=True, width=500)
730-
tooltip_preconv(static_title=title, values_source=source)
731-
732-
with dpg.group(tag='syx_decoded_payload', show=False):
733-
title = "Decoded Payload"
734-
dpg.add_text(title)
735-
tooltip_preconv(static_title=title, values_source='syx_payload')
707+
# TODO: review height
708+
dpg.add_child_window(tag='mon_sysex_container', height=200, border=False)
709+
710+
with dpg.group(parent='mon_sysex_container'):
711+
with dpg.group(horizontal=True):
712+
# FIXME: Option to display "Manufacturer’s ID" instead?
713+
# Only when an actual manufacturer’s ID is present?
714+
title = "ID number"
715+
dpg.add_text(title)
716+
dpg.add_input_text(source='syx_id_region', readonly=True, width=200)
717+
tooltip_preconv(f"{title} (Region)", 'syx_id_region', 'syx_id_val')
718+
dpg.add_input_text(source='syx_id_group', readonly=True, width=200)
719+
tooltip_preconv(f"{title} (Group)", 'syx_id_group', 'syx_id_val')
720+
dpg.add_input_text(source='syx_id_name', readonly=True, width=200)
721+
tooltip_preconv(f"{title} (Name)", 'syx_id_name', 'syx_id_val')
722+
with dpg.group(horizontal=True):
723+
title = "Device ID"
724+
dpg.add_text(title)
725+
source = 'syx_device_id'
726+
dpg.add_input_text(source=source, readonly=True, width=50)
727+
tooltip_preconv(static_title=title, values_source=source)
728+
with dpg.group(tag='syx_decoded_payload', show=False):
729+
title = "Decoded Payload:"
730+
dpg.add_text(title)
731+
with dpg.group(horizontal=True, tag='syx_payload_container'):
732+
title = "Undecoded Payload"
733+
dpg.add_text(title)
734+
source = 'syx_payload'
735+
dpg.add_input_text(source=source, readonly=True, width=500)
736+
tooltip_preconv(static_title=title, values_source=source)
736737

737738
# TODO: generate dynamically?
738739
with dpg.group(parent='syx_decoded_payload'):

src/midiexplorer/gui/windows/mon/blink.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,11 +79,11 @@ def get_supported_decoders() -> list:
7979
'syx_id_name',
8080
'syx_id_val',
8181
'syx_device_id',
82-
'syx_payload',
8382
'syx_sub_id1_name',
8483
'syx_sub_id1_val',
8584
'syx_sub_id2_name',
8685
'syx_sub_id2_val',
86+
'syx_payload',
8787
]
8888
return decoders
8989

src/midiexplorer/gui/windows/mon/data.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,8 +31,11 @@ def _update_gui_sysex(decoded: DecodedSysEx):
3131
set_value_preconv('syx_id_val', decoded.identifier.value)
3232
set_value_preconv('syx_device_id', decoded.device_id)
3333
set_value_preconv('syx_payload', decoded.payload.value)
34-
if isinstance(decoded.payload, DecodedUniversalSysExPayload):
34+
if not len(decoded.payload.value):
3535
dpg.hide_item('syx_payload_container')
36+
else:
37+
dpg.show_item('syx_payload_container')
38+
if isinstance(decoded.payload, DecodedUniversalSysExPayload):
3639
if decoded.payload.sub_id1_value:
3740
dpg.set_value('syx_sub_id1_name', decoded.payload.sub_id1_name)
3841
set_value_preconv('syx_sub_id1_val', decoded.payload.sub_id1_value if not None else "")
@@ -48,7 +51,6 @@ def _update_gui_sysex(decoded: DecodedSysEx):
4851
dpg.show_item('syx_decoded_payload')
4952
else:
5053
dpg.hide_item('syx_decoded_payload')
51-
dpg.show_item('syx_payload_container')
5254

5355

5456
def update_gui_monitor(data: mido.Message, static: bool = False) -> None:

src/midiexplorer/midi/decoders/sysex.py

Lines changed: 48 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -23,17 +23,22 @@
2323
# TODO: decode MMC (page 58 + dedicated spec)
2424

2525
import functools
26+
from collections import deque
2627

2728
import midi_const
2829
import mido
2930

30-
"""
31-
System exclusive ID decoder.
31+
from midiexplorer.gui import Logger
32+
3233

33-
Denoted "ID number" in the specification.
34-
Frequently referred to as "Manufacturer ID".
35-
"""
3634
class DecodedSysExId:
35+
"""
36+
System exclusive ID decoder.
37+
38+
Denoted "ID number" in the specification.
39+
Frequently referred to as "Manufacturer ID".
40+
"""
41+
3742
def __init__(self, value: int | tuple[int]):
3843
length: int
3944
try:
@@ -77,7 +82,7 @@ def region(self) -> str:
7782
index = self._raw
7883
else:
7984
index = self._raw[1]
80-
region = midi_const.SYSTEM_EXCLUSIVE_ID_REGIONS.get(index, "N.A.")
85+
region = midi_const.SYSTEM_EXCLUSIVE_ID_REGIONS.get(index, "N/A")
8186
return region
8287

8388
@functools.cached_property
@@ -98,21 +103,24 @@ def name(self) -> str:
98103

99104
class DecodedSysExPayload:
100105
_id = int
101-
_raw: int | tuple[int]
106+
_raw: deque
102107

103108
def __init__(self, identifier: DecodedSysExId, contents: int | tuple[int]):
104109
self._id = identifier
105-
self._raw = contents
110+
if isinstance(contents, int):
111+
self._raw = deque([contents])
112+
else:
113+
self._raw = deque(contents)
106114

107115
@property
108116
def value(self):
109-
return self._raw
117+
return tuple(self._raw)
110118

111119
@staticmethod
112120
def get_decoder(identifier):
113-
if identifier.value == 0x7E:
121+
if midi_const.SYSTEM_EXCLUSIVE_ID.get(identifier.value) == "Non-Real Time":
114122
return DecodedUniversalNonRealTimeSysExPayload
115-
if identifier.value == 0x7F:
123+
if midi_const.SYSTEM_EXCLUSIVE_ID.get(identifier.value) == "Real Time":
116124
return DecodedUniversalRealTimeSysExPayload
117125
return DecodedSysExPayload
118126

@@ -124,59 +132,66 @@ def __init__(self, identifier: DecodedSysExId, contents: int | tuple[int]):
124132

125133
class DecodedUniversalNonRealTimeSysExPayload(DecodedUniversalSysExPayload):
126134
def __init__(self, identifier: DecodedSysExId, contents: int | tuple[int]):
127-
if identifier.value != 0x7E:
135+
if midi_const.SYSTEM_EXCLUSIVE_ID.get(identifier.value) != "Non-Real Time":
128136
raise ValueError
129137
super().__init__(identifier, contents)
130-
next_byte: int = 0
131-
self.sub_id1_value = self._raw[next_byte]
132-
self.sub_id1_name = midi_const. \
133-
DEFINED_UNIVERSAL_SYSTEM_EXCLUSIVE_MESSAGES_NON_REAL_TIME_SUB_ID_1.get(
134-
self.sub_id1_value, "Undefined"
138+
self.sub_id1_value = self._raw.popleft()
139+
self.sub_id1_name = \
140+
midi_const.DEFINED_UNIVERSAL_SYSTEM_EXCLUSIVE_MESSAGES_NON_REAL_TIME_SUB_ID_1.get(
141+
self.sub_id1_value, "Undefined"
135142
)
136143
if self.sub_id1_value in midi_const.NON_REAL_TIME_SUB_ID_2_FROM_1:
137-
next_byte += 1
138-
self.sub_id2_value = self._raw[next_byte]
144+
self.sub_id2_value = self._raw.popleft()
139145
self.sub_id2_name = midi_const.NON_REAL_TIME_SUB_ID_2_FROM_1.get(
140146
self.sub_id1_value
141-
).get(
147+
).get(
142148
self.sub_id2_value, "Undefined"
143149
)
144150

145151

146152
class DecodedUniversalRealTimeSysExPayload(DecodedUniversalSysExPayload):
147153
def __init__(self, identifier: DecodedSysExId, contents: int | tuple[int]):
148-
if identifier.value != 0x7F:
154+
if midi_const.SYSTEM_EXCLUSIVE_ID.get(identifier.value) != "Real Time":
149155
raise ValueError
150156
super().__init__(identifier, contents)
151-
next_byte: int = 0
152-
self.sub_id1_value = self._raw[next_byte]
153-
self.sub_id1_name = midi_const. \
154-
DEFINED_UNIVERSAL_SYSTEM_EXCLUSIVE_MESSAGES_REAL_TIME_SUB_ID_1.get(
155-
self.sub_id1_value, "Undefined"
157+
self.sub_id1_value = self._raw.popleft()
158+
self.sub_id1_name = \
159+
midi_const.DEFINED_UNIVERSAL_SYSTEM_EXCLUSIVE_MESSAGES_REAL_TIME_SUB_ID_1.get(
160+
self.sub_id1_value, "Undefined"
156161
)
157162
if self.sub_id1_value in midi_const.REAL_TIME_SUB_ID_2_FROM_1:
158-
next_byte += 1
159-
self.sub_id2_value = self._raw[next_byte]
163+
self.sub_id2_value = self._raw.popleft()
160164
self.sub_id2_name = midi_const.REAL_TIME_SUB_ID_2_FROM_1.get(
161165
self.sub_id1_value
162-
).get(
166+
).get(
163167
self.sub_id2_value, "Undefined"
164168
)
165169

166170

167171
class DecodedSysEx:
168172
def __init__(self, message: tuple):
169173
if len(message) < 3:
174+
# We need at least 3 bytes:
175+
# - 1 ID number byte
176+
# - 1 device ID byte
177+
# - 1 payload byte
170178
raise ValueError("Message too short (less than 3 bytes) to be a proper system exclusive message.")
179+
171180
# Scrub EOX if present
172181
if message[-1] == mido.messages.specs.SYSEX_END:
182+
Logger().log_warning("Scrubbing EOX from SysEx payload before decoding!")
173183
self._raw = message[:-1]
174184
else:
175185
self._raw = message
186+
176187
# Determine ID length
177188
if self._raw[0] == 0x00:
178189
# 3-byte ID
179-
if len(message) < 5:
190+
if len(self._raw) < 5:
191+
# We need at least 5 bytes:
192+
# - 3 ID number bytes
193+
# - 1 device ID byte
194+
# - 1 payload byte
180195
raise ValueError(
181196
"Message too short (less than 5 bytes) to be a proper system exclusive message with a 3-byte ID."
182197
)
@@ -196,6 +211,8 @@ def _payload(self) -> int | tuple[int]:
196211
return self._raw[self._device_id_byte + 1:]
197212

198213
@functools.cached_property
199-
def payload(self) -> DecodedSysExPayload | DecodedUniversalRealTimeSysExPayload | DecodedUniversalNonRealTimeSysExPayload:
214+
def payload(
215+
self
216+
) -> DecodedSysExPayload | DecodedUniversalRealTimeSysExPayload | DecodedUniversalNonRealTimeSysExPayload:
200217
decoder = DecodedSysExPayload.get_decoder(self.identifier)
201218
return decoder(self.identifier, self._payload)

0 commit comments

Comments
 (0)