melanchall / drywetmidi

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

Unable to send Note-On event to device #286

Closed eloyer closed 5 months ago

eloyer commented 5 months ago

Hi, apologies in advance as this seems like it must be something simple. I'm using rtpMIDI to route outgoing MIDI from my Unity app on Windows to a Mac. Even though my rtpMIDI session on Windows doesn't announce itself as an output device to drywetmidi, by modifying the demo scene script I can still get the device by its rtpMIDI session name and route the MIDI file playback to it. This is great.

With the same setup, however, so far I'm not able to send a Note-On event directly to the output device. This is my code:

NoteEvent noteEvent = new NoteOnEvent();
noteEvent.SetNoteNumber(NoteName.C, 2);
_outputDevice.SendEvent(noteEvent);

Nothing happens, and the EventSent handler I've set up on the output device isn't called. Any suggestions would be appreciated!

melanchall commented 5 months ago

Hi,

Please show full code, especially how you declare and instantiate _outputDevice.

eloyer commented 5 months ago

Here you go:

public string OutputDeviceName = "mydevice";
private OutputDevice _outputDevice;
...
_outputDevice = OutputDevice.GetByName(OutputDeviceName);
 _outputDevice.PrepareForEventsSending();
_outputDevice.EventSent += OnEventSent;
NoteEvent noteEvent = new NoteOnEvent();
noteEvent.SetNoteNumber(NoteName.C, 2);
_outputDevice.SendEvent(noteEvent);
...
private void OnEventSent(object sender, MidiEventSentEventArgs e)
{
    var midiDevice = (MidiDevice)sender;
    Console.WriteLine($"Event sent to '{midiDevice.Name}' at {DateTime.Now}: {e.Event}");
}
melanchall commented 5 months ago

Can you please post the entire file content?

eloyer commented 5 months ago

Sure, here's the whole thing (above was a somewhat simplified version):

using System;
using System.Linq;
using System.Text;
using Melanchall.DryWetMidi.Common;
using Melanchall.DryWetMidi.Composing;
using Melanchall.DryWetMidi.Core;
using Melanchall.DryWetMidi.Interaction;
using Melanchall.DryWetMidi.Multimedia;
using Melanchall.DryWetMidi.Standards;
using Melanchall.DryWetMidi.MusicTheory;
using UnityEngine;

public class DemoScript : MonoBehaviour
{
    public string OutputDeviceName = "opertoonist";

    private OutputDevice _outputDevice;
    private Playback _playback;

    private void Start()
    {
        InitializeOutputDevice();
        /*var midiFile = CreateTestFile();
        InitializeFilePlayback(midiFile);
        StartPlayback();*/
        InvokeRepeating("PlayOneNote", 1, 1);
    }

    private void OnApplicationQuit()
    {
        Debug.Log("Releasing playback and device...");

        if (_playback != null)
        {
            _playback.NotesPlaybackStarted -= OnNotesPlaybackStarted;
            _playback.NotesPlaybackFinished -= OnNotesPlaybackFinished;
            _playback.Dispose();
        }

        if (_outputDevice != null)
            _outputDevice.Dispose();

        Debug.Log("Playback and device released.");
    }

    private void InitializeOutputDevice()
    {
        Debug.Log($"Initializing output device [{OutputDeviceName}]...");

        var allOutputDevices = OutputDevice.GetAll();
        var allDevicesList = string.Join(Environment.NewLine, allOutputDevices.Select(d => $"  {d.Name}"));
        if (!allOutputDevices.Any(d => d.Name == OutputDeviceName))
        {
            Debug.Log($"There is no [{OutputDeviceName}] device presented in the system. Here the list of all device:{Environment.NewLine}{allDevicesList}");
            return;
        }

        Debug.Log($"Here the list of all devices:{Environment.NewLine}{allDevicesList}");

        _outputDevice = OutputDevice.GetByName(OutputDeviceName);
        _outputDevice.PrepareForEventsSending();
        _outputDevice.EventSent += OnEventSent;
        Debug.Log($"Output device [{OutputDeviceName}] initialized.");
    }

    private void PlayOneNote()
    {
        Debug.Log("play one note");
        NoteEvent noteEvent = new NoteOnEvent();
        noteEvent.SetNoteNumber(NoteName.C, 2);
        _outputDevice.SendEvent(noteEvent);
        Invoke("StopNote", 0.25f);
    }

    private void StopNote()
    {
        Debug.Log("stop one note");
        NoteEvent noteEvent = new NoteOffEvent();
        noteEvent.SetNoteNumber(NoteName.C, 2);
        _outputDevice.SendEvent(noteEvent);
    }

    private MidiFile CreateTestFile()
    {
        Debug.Log("Creating test MIDI file...");

        var patternBuilder = new PatternBuilder()
            .SetNoteLength(MusicalTimeSpan.Eighth)
            .SetVelocity(SevenBitNumber.MaxValue)
            .ProgramChange(GeneralMidiProgram.Harpsichord);

        foreach (var noteNumber in SevenBitNumber.Values)
        {
            patternBuilder.Note(Melanchall.DryWetMidi.MusicTheory.Note.Get(noteNumber));
        }

        var midiFile = patternBuilder.Build().ToFile(TempoMap.Default);
        Debug.Log("Test MIDI file created.");

        return midiFile;
    }

    private void InitializeFilePlayback(MidiFile midiFile)
    {
        Debug.Log("Initializing playback...");

        _playback = midiFile.GetPlayback(_outputDevice);
        _playback.Loop = true;
        _playback.NotesPlaybackStarted += OnNotesPlaybackStarted;
        _playback.NotesPlaybackFinished += OnNotesPlaybackFinished;

        Debug.Log("Playback initialized.");
    }

    private void StartPlayback()
    {
        Debug.Log("Starting playback...");
        _playback.Start();
    }

    private void OnEventSent(object sender, MidiEventSentEventArgs e)
    {
        var midiDevice = (MidiDevice)sender;
        Console.WriteLine($"Event sent to '{midiDevice.Name}' at {DateTime.Now}: {e.Event}");
    }

    private void OnNotesPlaybackFinished(object sender, NotesEventArgs e)
    {
        LogNotes("Notes finished:", e);
    }

    private void OnNotesPlaybackStarted(object sender, NotesEventArgs e)
    {
        LogNotes("Notes started:", e);
    }

    private void LogNotes(string title, NotesEventArgs e)
    {
        var message = new StringBuilder()
            .AppendLine(title)
            .AppendLine(string.Join(Environment.NewLine, e.Notes.Select(n => $"  {n}")))
            .ToString();
        Debug.Log(message.Trim());
    }
}
melanchall commented 5 months ago

I don't know what InvokeRepeating is. Please reaplce this line

InvokeRepeating("PlayOneNote", 1, 1);

with just

PlayOneNote();

and try again.

Also can you confirm you see play one note line in the debug log?

eloyer commented 5 months ago

FYI InvokeRepeating calls a method on a timer. But yes, I've replaced it, and yes, I see play one note in the log, but nothing is sent and OnEventSent is not called.

melanchall commented 5 months ago

Looks like magic :-)

Well, first of all, I see that you use Console.WriteLine inside OnEventSent. What if you change it to Debug.Log?

If that didn't help, let's go deeper. That's how SendEvent implemented:

public void SendEvent(MidiEvent midiEvent)
{
    ThrowIfArgument.IsNull(nameof(midiEvent), midiEvent);

    if (!IsEnabled)
        return;

    EnsureDeviceIsNotDisposed();
    EnsureDeviceIsNotRemoved();
    EnsureSessionIsCreated();
    EnsureHandleIsCreated();

    if (midiEvent is ChannelEvent || midiEvent is SystemCommonEvent || midiEvent is SystemRealTimeEvent)
    {
        var message = PackShortEvent(midiEvent);
        NativeApiUtilities.HandleDevicesNativeApiResult(
            OutputDeviceApiProvider.Api.Api_SendShortEvent(_handle.DeviceHandle, message));
        OnEventSent(midiEvent);
    }
    else
    {
        var sysExEvent = midiEvent as SysExEvent;
        if (sysExEvent != null)
            SendSysExEvent(sysExEvent);
    }
}

private void OnEventSent(MidiEvent midiEvent)
{
    EventSent?.Invoke(this, new MidiEventSentEventArgs(midiEvent));
}

So there are following reasons EventSent is not fired:

  1. SendEvent is not called actually. But this is wrong since as you've said you see play one note in the log.
  2. IsEnabled is false. But it's true by default and I don't see setting it to false in your code.

As you can see in case of NoteOnEvent it's guaranteed that EventSent event will be fired (NoteOnEvent is a subclass of ChannelEvent).

Let's do some tests.

First of all, change PlayOneNote implementation to this:

private void PlayOneNote()
{
    Debug.Log("A");
    NoteEvent noteEvent = new NoteOnEvent();
    noteEvent.SetNoteNumber(NoteName.C, 2);
    Debug.Log("B");
    _outputDevice.IsEnabled = true;
    _outputDevice.SendEvent(noteEvent);
    Debug.Log($"SendEvent called on {_outputDevice.Name}");
}

And change OnEventSent to this:

private void OnEventSent(object sender, MidiEventSentEventArgs e)
{
    Debug.Log("EVENT");
}

Please send me the log.

eloyer commented 5 months ago

Speaking of magic... problem solved. Thanks for the tip to check Console.WriteLine; I changed it to Debug.Log and saw that the events were indeed being triggered. I then used Protokol on the Mac to check that the note events were being sent, and they were, but I noticed that their velocity was 0. I set the velocity to 100 and now everything seems to be working! Perhaps because velocity was 0 nothing was being triggered... In any case, thanks for your help and for this library, very happy to have discovered it.