melanchall / drywetmidi

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

DllNotFoundException: Melanchall_DryWetMidi_Native64 in Unity #140

Closed BoxOfClicks closed 3 years ago

BoxOfClicks commented 3 years ago

Apologies if I'm missing something basic but after importing DWM into a Unity project I'm receiving the following error. I wonder if it's something to do with the recent changes to e.g. CommonApi? Any thoughts or ideas I could test are much appreciated.

melanchall commented 3 years ago

Hi,

Yes, structure of the project has been changed. I'll write detailed instruction later.

You need native binaries placed along with the main DLL of the library. Please take latest master files from here: https://github.com/melanchall/drywetmidi/releases/download/v6.0.0/DryWetMIDI.6.0.0-bin-native.zip. Extract the archive and place the files near the main DLL.

Please let me know if it works.

BoxOfClicks commented 3 years ago

Thank you so much for the fast response @melanchall. I spent yesterday investigating and getting things working. With your instructions I was able to quickly get things compiling - many thanks for that - unfortunately no matter what I tried, Unity would crash on enter play mode (2021.1.21). For now we've switched to RtMidi. Is it correct to assume that DWM is also constrained by Windows in the way that a device can only be used exclusively?

melanchall commented 3 years ago

@BoxOfClicks On Windows devices can be used exclusively only, yes. It's not a "feature" of DWM, that's how MIDI works on Windows. On macOS there is no such limitation so devices can be used by DWM from different applications.

Can you please provide information for me so I can investigate the problem:

  1. What caused the crash? What instruction in the code?
  2. Maybe you can send me your Unity project so I can catch the bug on my side?

Thanks

BoxOfClicks commented 3 years ago

Thanks @melanchall. I can try and create a simple repro project possibly later today. In the meantime, the code was a simple modified version of this example: https://melanchall.github.io/drywetmidi/articles/devices/Input-device.html In a Monobehaviour Start() method: var allMidiDevices = InputDevice.GetAll(); foreach(var d in allMidiDevices) { d.EventReceived += HandleMidiMessage; d.StartEventsListening(); //This call causes a crash to desktop } Obviously also with the HandleMidiMessage defined and a call to dispose the input devices on destroy.

melanchall commented 3 years ago

@BoxOfClicks I've just checked this code with Unity 2021.1.26f1:

void Start()
{
    Debug.Log("A");
    var allMidiDevices = InputDevice.GetAll();

    Debug.Log("B");
    foreach (var d in allMidiDevices)
    {
        Debug.Log($"Setup device '{d.Name}'");
        d.EventReceived += HandleMidiMessage;
        Debug.Log("Start events listening");
        d.StartEventsListening(); //This call causes a crash to desktop
        Debug.Log("C");
    }
}

and got valid log messages without crash:

A
UnityEngine.Debug:Log (object)
Test:Start () (at Assets/Test.cs:9)

B
UnityEngine.Debug:Log (object)
Test:Start () (at Assets/Test.cs:12)

Setup device 'LoopBe Internal MIDI'
UnityEngine.Debug:Log (object)
Test:Start () (at Assets/Test.cs:15)

Start events listening
UnityEngine.Debug:Log (object)
Test:Start () (at Assets/Test.cs:17)

C
UnityEngine.Debug:Log (object)
Test:Start () (at Assets/Test.cs:19)

Setup device 'Game Controller 1'
UnityEngine.Debug:Log (object)
Test:Start () (at Assets/Test.cs:15)

Start events listening
UnityEngine.Debug:Log (object)
Test:Start () (at Assets/Test.cs:17)

C
UnityEngine.Debug:Log (object)
Test:Start () (at Assets/Test.cs:19)

Setup device 'MIDI A'
UnityEngine.Debug:Log (object)
Test:Start () (at Assets/Test.cs:15)

Start events listening
UnityEngine.Debug:Log (object)
Test:Start () (at Assets/Test.cs:17)

C
UnityEngine.Debug:Log (object)
Test:Start () (at Assets/Test.cs:19)

Setup device 'MIDI B'
UnityEngine.Debug:Log (object)
Test:Start () (at Assets/Test.cs:15)

Start events listening
UnityEngine.Debug:Log (object)
Test:Start () (at Assets/Test.cs:17)

C
UnityEngine.Debug:Log (object)
Test:Start () (at Assets/Test.cs:19)

Setup device 'MIDI C'
UnityEngine.Debug:Log (object)
Test:Start () (at Assets/Test.cs:15)

Start events listening
UnityEngine.Debug:Log (object)
Test:Start () (at Assets/Test.cs:17)

C
UnityEngine.Debug:Log (object)
Test:Start () (at Assets/Test.cs:19)

Setup device 'Keyboard'
UnityEngine.Debug:Log (object)
Test:Start () (at Assets/Test.cs:15)

Start events listening
UnityEngine.Debug:Log (object)
Test:Start () (at Assets/Test.cs:17)

C
UnityEngine.Debug:Log (object)
Test:Start () (at Assets/Test.cs:19)

So it seems all works fine within DryWetMIDI.

I can try and create a simple repro project possibly later today.

It would be great. Also can you please catch exactly what device leads to crash? Maybe you can use my code to log all actions?

And one more question: are you really need to listen all devices in the system?

Thanks

melanchall commented 3 years ago

Well, I've got some strange things, BUT they appear only if disposing is omitted. So I really need your project to see how you work with devices.

melanchall commented 3 years ago

@BoxOfClicks Any news on sample project? :)

melanchall commented 3 years ago

I've added Using in Unity article which shows example of simple script with devices. Please see how they should be obtained and disposed. I've tested the script within Unity and all works fine.

@BoxOfClicks I'm closing the issue since there is no info from you. Feel free to reopen if you can provide sample project which can reproduce your problem.

schuster-rainer commented 2 years ago

I'm using it in Visual Scripting in a custom unit. and get the same exception, albeit I'm not using any output device. Is this intended?

using Unity.VisualScripting;
using UnityEngine;
using Melanchall.DryWetMidi.Core;
using Melanchall.DryWetMidi.Interaction;
using System;
using Melanchall.DryWetMidi.Multimedia;

public class MidiPlayer : Unit
{
    [DoNotSerialize]
    public ValueInput fileNameInput;
    [DoNotSerialize]
    public ControlInput inputTrigger;

    [DoNotSerialize]
    public ControlInput playTrigger;

    [DoNotSerialize]
    public ControlOutput outputTrigger;

    [DoNotSerialize]
    public ControlOutput noteTrigger;

    [DoNotSerialize]
    public ValueOutput notesCountOutput;

    [DoNotSerialize]
    public ValueOutput noteOutput;

    [DoNotSerialize]
    private MidiFile midiFile;

    [DoNotSerialize]
    private NotePlaybackData currentNote;

    private Playback playback;

    protected override void Definition()
    {

        inputTrigger = ControlInput("", (flow) =>
        {
            var file = flow.GetValue<string>(fileNameInput);
            midiFile = MidiFile.Read(Application.streamingAssetsPath + "/" + file);
            //resultValue = flow.GetValue<string>(myValueA) + flow.GetValue<string>(myValueB) + "!!!";
            return outputTrigger;
        });

        playTrigger = ControlInput("play", (flow) =>
        {
            //if (midiFile == null) return noteTrigger;

            playback = midiFile.GetPlayback();

            playback.NoteCallback += delegate (NotePlaybackData rawNoteData, long rawTime, long rawLength, TimeSpan playbackTime)
                {
                    currentNote = rawNoteData;
                    return rawNoteData;
                };
            try
            {
                playback.Start();
            }
            catch (Exception ex)
            {
                Debug.LogWarning(ex);
            }

            return noteTrigger;
        });

        outputTrigger = ControlOutput("");
        noteTrigger = ControlOutput("note played");
        fileNameInput = ValueInput<string>("fileName", string.Empty);
        notesCountOutput = ValueOutput<int>("notes count", (flow) => {
            var notes = midiFile.GetNotes();
            return notes.Count;
        });
        noteOutput = ValueOutput<NotePlaybackData>("current note", (flow) => {
            return currentNote;
        });

        //We need fileName to be set to let the unit process
        Requirement(fileNameInput, inputTrigger); 
        //Requirement(myValueB, inputTrigger); //To display that we need the myValueB value to be set to let the unit process
        Succession(inputTrigger, outputTrigger); //To display that the input trigger port input will exits at the output trigger port exit. Not setting your succession also grays out the connected nodes but the execution is still done.
        Succession(playTrigger, noteTrigger);
        Assignment(playTrigger, noteOutput);
        Assignment(inputTrigger, notesCountOutput);//To display the data that is written when the inputTrigger is triggered to the result string output.
    }

    protected override void BeforeUndefine()
    {
        base.BeforeUndefine();
        playback.NoteCallback = null;
        playback.Dispose();

    }
}
melanchall commented 2 years ago

Hi @schuster-rainer,

Is this intended?

Yes, because you use Playback class. By default it uses HighPrecisionTickGenerator (you can read more here). It's a timer that can fire at interval of 1 ms. Such high-precision timer is platform-dependent and thus it is implemented via native layer provided by _Melanchall_DryWetMidiNative64.dll (.dylib) file. So you just need to follow the instruction (please see point 3 in the list there).

Of course you can override default behavior and use your own tick generator implementation. In this case it will be up to you how to implement it, native libraries of DryWetMIDI won't be required. But DryWetMIDI tries to provide the best implementation (in terms of CPU and power usage) for each supported platform (right now, high-precision tick generator supported on Windows and macOS).