JuliaMusic / MIDI.jl

A Julia library for handling MIDI files
https://juliamusic.github.io/JuliaMusic_documentation.jl/latest/
MIT License
67 stars 22 forks source link

Add support for SequencerSpecific Meta Event. #172

Closed NeroBlackstone closed 1 month ago

NeroBlackstone commented 1 month ago

Is your feature request related to a problem? Please describe. If MIDI file is recorded by a physical device, it will include Sequencer Specific Event.

Sequencer specific FF 7F len data

The Sequencer specific meta-event is used to store vendor-proprietary data in a MIDI file. The Length can be any value between 0 and 228-1, specifying the number of Data bytes (values between 0 and 255) which follow. Sequencer specific records may be very long.

Currently, MIDI.jl has not defined this event in MIDI_EVENTS_DEFS, so any MIDI file opening that contains a Sequencer Specific Event will throw an exception.

Describe the solution you'd like Since the data of this event is variable length, to be honest, I don't know how to define this meta event.

"""    SequencerSpecificEvent <: MetaEvent
The `SequencerSpecificEvent` is used to store vendor-proprietary data in a MIDI file.

## Fields:
* `dT::Int` : Delta time in ticks.
* `metatype::UInt8` : Meta type byte of the event.
* `ssdata::Vector{UInt8}` : Vendor-proprietary data.
"""
SequencerSpecificEvent

const MIDI_EVENTS_DEFS = Dict(
0x7f => (
        type = :SequencerSpecificEvent,
 # this should receive multiple variable since data length is variable
        fields = ["ssdata::Vector{UInt8}"],
        decode = :(data),
        encode = :(event.ssdata)
    ),
....
)

# When construct SequencerSpecificEvent:
# MethodError: no method matching MIDI.SequencerSpecificEvent(::Int64, ::UInt8, ::UInt8, ::UInt8, ::UInt8, ::UInt8, ::UInt8, ::UInt8)
Datseris commented 1 month ago

Since the data of this event is variable length, to be honest, I don't know how to define this meta event

What do you mean? MIDI.jl already has the infrastructure to read variable length events, we already do it in the source code for some events. What stops you from using the same code for this new event?

NeroBlackstone commented 1 month ago

It's a bit complicated, but I'll try to explain the problem clearly.

Since we don't have to decode the data in Sequencer Specific Event raw data (vendor-proprietary data), we just want to move the data intact to the ssdata field of SequencerSpecificEvent type.

So the fields of SequencerSpecificEvent is:

Fields:

  • dT::Int : Delta time in ticks.
  • metatype::UInt8 : Meta type byte of the event.
  • ssdata::Vector{UInt8} : Vendor-proprietary raw data. (# here is the problem)

And if we define (store raw data so no encode and decode, just same):

0x7f => (
        type = :SequencerSpecificEvent,
        fields = ["ssdata::Vector{UInt8}"],
        decode = :(data), # raw data here
        encode = :(event.ssdata)
    ), 

We wiil use these codes to generate the Types and Constrctors:

https://github.com/JuliaMusic/MIDI.jl/blob/3868e9ebdc80ded32db37f722993544aa23464a4/src/events.jl#L120-L142

Okay, so when we construct SequencerSpecificEvent (type(dT, metatype, data)), we invoke these code to decode the Bytes first (but do nothing here except ... spread, since we want raw data):

https://github.com/JuliaMusic/MIDI.jl/blob/3868e9ebdc80ded32db37f722993544aa23464a4/src/events.jl#L131-L133

then the invoke:

https://github.com/JuliaMusic/MIDI.jl/blob/3868e9ebdc80ded32db37f722993544aa23464a4/src/events.jl#L139-L142

Since it's variable length events so length(args) never be same, and a MethodError will throw, this is problem.

NeroBlackstone commented 1 month ago

Yes Text is also variable length but it doesn't have this problem, because after decode it will turn to Vector{String}, after spread it will become normal String.

https://github.com/JuliaMusic/MIDI.jl/blob/3868e9ebdc80ded32db37f722993544aa23464a4/src/events.jl#L110-L114

What if we decode SequencerSpecificEvent as above Text?:

    0x7f => (
        type = :SequencerSpecificEvent,
        fields = ["ssdata::Vector{UInt8}"],
        decode = :([data]), # decode to Vector{Vector{UInt8}}
        encode = :(event.ssdata)
    ),

It's a infinity loop and cause StackOverflowError, because it repeatly spread and collect once and once again.

https://github.com/JuliaMusic/MIDI.jl/blob/3868e9ebdc80ded32db37f722993544aa23464a4/src/events.jl#L131-L133

NeroBlackstone commented 1 month ago

I have a ugly but work solution:

    0x7f => (
        type = :SequencerSpecificEvent,
        fields = ["ssdata::Vector{Vector{UInt8}}"],
        decode = :([[data]]),
        encode = :(event.ssdata)
    ),

And length(ssdata) is 1, ssdata[1] is raw MIDI event data

NeroBlackstone commented 1 month ago

What do you think? I can make a PR to temporarily support SequencerSpecificEvent until we can come up with a better solution. (At least opening MIDI files won't throw an exception)

NeroBlackstone commented 1 month ago

Okay I have a better solution and I will post tomorrow.