melanchall / drywetmidi

.NET library to read, write, process MIDI files and to work with MIDI devices
https://melanchall.github.io/drywetmidi
MIT License
545 stars 75 forks source link

How do I identify which track a MidiEvent belongs to when a Playback is in progress? #122

Closed akira0245 closed 3 years ago

akira0245 commented 3 years ago

Hi, I'm using a custom IOutputDevice to play a PlayBack, I want this device process NoteEvent differently between different Tracks. but a NoteEvent doesn't have a track property. How should I solve this? Thank you.

melanchall commented 3 years ago

Hi,

Hmm, it's not possible right now. I'll think how to provide such info during playback and come back to you.

Thanks!

melanchall commented 3 years ago

@akira0245 Can you please say what kind of logic do you want to implement? Something like "if the number of track chunk is N, skip an event"?

akira0245 commented 3 years ago

@akira0245 Can you please say what kind of logic do you want to implement? Something like "if the number of track chunk is N, skip an event"?

If a NoteEvent has a Track property, my Device should look like this. This Device is very basic and now only handles NoteOn and NoteOff. I just need to do an action before a NoteOnEvent is played for each different track. Thank you!

class BardPlayDevice : IOutputDevice
    {
        public void PrepareForEventsSending()
        {

        }

        public void SendEvent(MidiEvent midiEvent)
        {
            var keyboard = Plugin.pluginInterface.Framework.Gui.GetAddonByName("PerformanceModeWide", 1);
            if (keyboard == null) return;

            if (midiEvent is NoteOnEvent noteOnEvent)
            {
                if (Plugin.config.OverrideGuitarTones)
                {
                    var tone = Plugin.config.TracksTone[midiEvent.Track];
                    playlib.SwitchTone(PerformanceToneChange.Address, tone);
                }

                var noteNum = noteOnEvent.NoteNumber;
                playlib.PressKey(keyboard.Address, noteNum);
            }
            else if (midiEvent is NoteOffEvent noteOffEvent)
            {
                var noteNum = noteOffEvent.NoteNumber;
                playlib.ReleaseKey(keyboard.Address, noteNum);
            }
        }

        public event EventHandler<MidiEventSentEventArgs> EventSent;
    }
akira0245 commented 3 years ago

@akira0245 Can you please say what kind of logic do you want to implement? Something like "if the number of track chunk is N, skip an event"?

Yes, it would also be handy to be able to skip events so that I don't have to regenerate a PlayBack when I enable or disable a track.

melanchall commented 3 years ago

Thanks for clarification. I'll think about how to design the new API. I suppose signature of the SendEvent will be changed to provide an event metadata.

melanchall commented 3 years ago

It seems it's quite complicated. There is a big pitfall with assigning track chunk number to event sent to a device by playback. Some events can be not presented in any track chunk and can be generated by playback on the fly. Cases where this occurs:

  1. You set new time (with MoveToTime method, for example) and new time falls in middle of some note. If TrackNotes property set to true, new NoteOnEvent will be generated, but it doesn't actually exist in a track chunk.
  2. The same problem with TrackProgram, TrackPitchValue and TrackControlValue properties.

We also can't add property like TrackChunkIndex to MidiEvent because there are a lot of situations where this property is not relevant.

I have one idea, need to check it. But anyway your task will require some additional code from you, since we can't support track chunk index in MIDI events directly due to reasons mentioned above.

akira0245 commented 3 years ago

Thanks for your work! For my purposes, any extra code is fine as long as there is a way to get enough midievent metadata. I approve it's a good idea to pass these information through the SendEvent signature.

melanchall commented 3 years ago

@akira0245 solution is ready, I'll provide it tomorrow.

melanchall commented 3 years ago

Well, the changes I've made:

Please install 5.2.1-prerelease7 version of the package from NuGet to get new changes available (or build the library from sources of the develop branch).

For your task you need:

  1. create your own implementation of TimedEvent implementing IMetadata:

    private sealed class TimedEventWithTrackChunkIndex : TimedEvent, IMetadata
    {
        public TimedEventWithTrackChunkIndex(MidiEvent midiEvent, long time, int trackChunkIndex)
            : base(midiEvent, time)
        {
            Metadata = trackChunkIndex;
        }
    
        public object Metadata { get; set; }
    }

    so Metadata property will hold the index of a track chunk.

  2. create your own implementation of Playback:

    private sealed class MyPlayback : Playback
    {
        public MyPlayback(IEnumerable<ITimedObject> timedObjects, TempoMap tempoMap)
            : base(timedObjects, tempoMap)
        {
        }
    
        protected override bool TryPlayEvent(MidiEvent midiEvent, object metadata)
        {
            // Place your logic here
            // Return true if event played (sent to plug-in); false otherwise
        }
    }

    so in fact you even don't need a custom output device. But if you want, you can do something like that:

    private sealed class MyOutputDevice : IOutputDevice
    {
        public event EventHandler<MidiEventSentEventArgs> EventSent;
    
        public void PrepareForEventsSending()
        {
        }
    
        public void SendEvent(MidiEvent midiEvent)
        {
        }
    
        public bool SendEventWithMetadata(MidiEvent midiEvent, object metadata)
        {
            // Place your logic here
            // Return true if event sent to plug-in; false otherwise
        }
    }

    and in TryPlayEvent of Playback:

    protected override bool TryPlayEvent(MidiEvent midiEvent, object metadata)
    {
        return ((MyOutputDevice)OutputDevice).SendEventWithMetadata(midiEvent, metadata);
    }
  3. create playback from instances of TimedEventWithTrackChunkIndex:

    var timedEvents = midiFile
        .GetTrackChunks()
        .SelectMany((c, i) => c.GetTimedEvents().Select(e => new TimedEventWithTrackChunkIndex(e.Event, e.Time, i)))
        .OrderBy(e => e.Time);
    
    var tempoMap = midiFile.GetTempoMap();
    
    var playback = new MyPlayback(timedEvents, tempoMap);

Modify constructor of MyPlayback if you want to pass output device. But as I said before, it seems you don't need custom output device.

And... that's all. No breaking changes in API and your task solved :)

akira0245 commented 3 years ago

@melanchall Thank you for the quick update and also for writing such detailed instructions, it solved my problem perfectly. I really appreciate DryWetMIDI and your work, thank you again!

melanchall commented 3 years ago

:rocket: 5.2.1 version is released now!

Prerelease NuGet packages will be unlisted soon, so please update the package references to the new version.

Thanks for using the library!