MIDI running status
While trying to implement MIDI messaging specification for KS, I've ran into an issue with a certain thing called "running status" in MIDI core spec.
MIDI events in standard MIDI file normally look like that:
00 b5 00 00
00 b5 31 00
00 b5 0a 17
It breaks down like:
00- time spec, zero (start of the track) in this caseb5- change controller at channel 500- controller ID00- controller value
However, when multiple similar event types go sequentially (as in this example), all the b5s except for the first one can be omitted:
00 b5 00 00
00 31 00
00 0a 17
The distinction between a "proper" new event and continuation of previous event type is made by lack of high bit (0x80) in that byte after time spec: if there is a bit there (like b5) - that's a new type of event, if there isn't (like 31) - that's "running status".
Although "running status" is mostly obsolete and many MIDI implementations doesn't seem to support it, it's still an interesting question on how to ponder this in KS syntax. Any ideas?
This seems like one of those cases where the consumer will need to do a bit of heavy lifting in the form of some post-processing after parsing. To do something like this properly you'd probably need to add state (i.e. store the last event code in a variable so it can be used later), and that seems like it would add too much complexity.
Unfortunately, it can't be really solved by post-processing. MIDI events can have different lengths, and you have to interprete all the previous events to understand how to intreprete the current one.
One of the options I'm thinking of is adding more magic variables, something like _repeat, that would allow to access repetition array, so you can access _repeat.last to get an element that was added last to it, i.e. element from the previous step. Of course, that's a major thing: such implementation would require to supply _repeat into constructor or something like that, and it makes it mandatory to use such type only inside a repetition (i.e. loop).
Hi, I'm running into this issue, what's the status?
@izzyreal:
Hi, I'm running into this issue, what's the status?
Actually, I believe it is possible to implement this with the current version of Kaitai Struct, and it should be relatively easy (when you know the tricks to use).
As I see it, first problem is to find out whether a message uses running status or not. This can be done with a lookahead parse instance, for example as follows (based on standard_midi_file.ksy:69-74):
track_event:
seq:
- id: v_time
type: vlq_base128_be
- id: event_header
type: u1
+ if: not uses_running_status
@@ ... @@
instances:
+ uses_running_status:
+ value: status_byte_lookahead & 0x80 == 0
+ status_byte_lookahead:
+ pos: _io.pos
+ type: u1
The second problem is, obviously, figure out how to transfer "state" from one message to the next (because for all you know, the next message might need to know the running status). Kaitai Struct doesn't really have the concept of a mutable "variable" as in traditional imperative languages - all user-defined properties are basically immutable (they don't change once evaluated). But what you can do is to track intermediate values of the "variable" using a value instance in each object of the list. Although this value instance will be immutable for each particular object element of the list, its value can of course be different than in the previous object, and it's possible to access the previous value and use it in the value instance expression to decide what the new intermediate value will be.
The way I like to do this in practice (and which I consider best) is to declare a parameter in the repeated type where you pass the previous value of the state "variable" from the previous element (or an initial value of the state to the first element) and use the special _index variable at the place of this type's usage to resolve the previous object and access the intermediate state value.
For example, see the java_class.ksy spec:
place of use - java_class.ksy:28-31
- id: constant_pool
type: 'constant_pool_entry(_index != 0 ? constant_pool[_index - 1].is_two_entries : false)'
repeat: expr
repeat-expr: constant_pool_count - 1
parameter declaration - java_class.ksy:62-67
types:
constant_pool_entry:
doc-ref: https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-4.html#jvms-4.4
params:
- id: is_prev_two_entries
type: bool
new intermediate value of the "state" - java_class.ksy:95-97
instances:
is_two_entries:
value: 'is_prev_two_entries ? false : tag == tag_enum::long or tag == tag_enum::double'
As you can see, it's nothing complicated. This pattern can certainly be applied in case of the MIDI running status too, so I don't see any functional limitation of Kaitai Struct that would block the implementation of the running status in the standard_midi_file.ksy spec.
Perhaps a bit tricky in MIDI would be to follow the MIDI spec carefully so that we update the running status if and only if we actually should, and generally behave in accordance with the spec. Skimming through the 1996 revision of MIDI 1.0 spec, it mentions several times that "Real-Time messages should not affect Running Status", then it instructs to "clear the running status buffer" when certain types of messages are received, also what to do if the buffer is empty and a message without the status byte is received, etc.
@generalmimon If I understand your approach correctly, it's what I tried last night. What I ran into is the following. We have a MIDI file with the following sequence of events:
event 1 00 90 3C 7F note on 60, velo 127
event 2 01 3C 00 note on 60, velo 0, is in running status because (0x3C & 0x80) == 0
event 3 00 3C 7F note on 60, velo 127, is in running status because (0x3C & 0x80) == 0
event 4 01 3C 7F note on 60, velo 0, is in running status because (0x3C & 0x80) == 0
With your suggested approach I get as far as giving event 2 knowledge of the status byte of event 1. But I didn't find a way to propagate this knowledge to event 3.
In terms of currently somewhat valid KS syntax (though still resulting in compilation error) I only know how to express the desired relationship in a recursive way, and I think I ran into https://github.com/kaitai-io/kaitai_struct/issues/512 because of that. Basically I tried to make an event_type field like this:
instances:
previous_event_type:
value: idx == 0 ? event_type : _parent.event[idx - 1].event_type
if: is_running_status
event_type:
value: is_running_status ? previous_event_type : (event_header & 0xf0)
is_running_status:
value: (event_type & 0x80) == 0
And the web IDE only emits the mysterious message "event_type". As far as I know you can't add any kind of recursive relationship in instances.
If Kaitai Struct would support something like array.find_first(lambda, start_offset, direction) in its instance value expression, we could determine the last emitted event type without relying on such recursion. But to my knowledge we only have .min/max for a few primitive types: https://github.com/kaitai-io/kaitai_struct/issues/139.
Attached is a MIDI file in which the mentioned note on event array occurs, as well as a .ksy that has a non-recursive way to determine running status (it's called is_continuation there). It reports the running status and the previous event_type correctly up to the first occurrence of an event in running status. You'll see that with this .ksy, SEQ2.MID is reported to have 2 consecutive note on events, whereas with the original standard MIDI .ksy you see only one.
standard_midi_file_with_running_status.ksy.zip SEQ2.MID.zip
I'd also like to mention this MIDI file inspection and hacking tool, unfortunately Windows only. But it's quite wonderful and parses running status correctly: https://jeffbourdier.github.io/midiopsy/
@izzyreal:
Basically I tried to make an
event_typefield like this:instances: previous_event_type: value: idx == 0 ? event_type : _parent.event[idx - 1].event_type if: is_running_status event_type: value: is_running_status ? previous_event_type : (event_header & 0xf0) is_running_status: value: (event_type & 0x80) == 0And the web IDE only emits the mysterious message "event_type". As far as I know you can't add any kind of recursive relationship in instances.
Well, your intuition is close in principle, but this is not exactly the approach I've suggested. In fact, it looks like you didn't properly read the first half of my message, or the second, which makes me a bit sad.
If you pass directly the previous event type (not the index) as a parameter as I suggested, there won't be any "kind of recursive relationship in instances". Please check out java_class.ksy:28-31 again.
If Kaitai Struct would support something like
array.find_first(lambda, start_offset, direction)in its instance value expression, we could determine the last emitted event type without relying on such recursion. But to my knowledge we only have.min/maxfor a few primitive types: #139.
There's absolutely no need for that in this case for sure.
@generalmimon
In fact, it looks like you didn't properly read the first half of my message, or the second, which makes me a bit sad.
You're right, I realised it later and didn't find time to amend, sorry for that. I'll try again based on your suggestion in the near future and report back.
@generalmimon That seems to work! Many thanks 👍
Modified VERY WIP standard_midi_file.ksy first stab is attached. It's still reading the header in the running status case, so it's far from complete. But it demonstrates the propagation of previous event type.
standard_midi_file_running_status_2.ksy.zip
If I get some proper improvements done I'll post it in this thread.
@izzyreal:
It's still reading the header in the running status case, so it's far from complete.
Well, it wouldn't have if you used the "lookahead parse instance" as I suggested. But at least you seem to understand the part about keeping track of the running status, so perhaps I can't expect to succeed in explaining everything 😉
@generalmimon You're right again! I really appreciate your help, and learning these techniques. I did a series of other .ksy files for the Akai MPC2000XL (ALL, APS, PGM, SND) that might benefit from them too. Maybe one day they will be good enough to submit as a contribution.
I put it all together (see attachment) and now it seems to work beautifully! The sequence of 4 note on events in SEQ2.MID is parsed as expected: