Click here to Skip to main content
13,901,147 members
Click here to Skip to main content
Add your own
alternative version

Tagged as

Stats

2.1K views
7 bookmarked
Posted 31 Jan 2019
Licenced CPOL

DryWetMIDI: Working with MIDI Devices

, 31 Jan 2019
Rate this:
Please Sign up or sign in to vote.
Overview of how to send, receive, play back and record MIDI data with DryWetMIDI

Introduction

DryWetMIDI is .NET library to work with MIDI files and MIDI devices. To learn about processing MIDI files, read "DryWetMIDI: High-level processing of MIDI files" article.

Devices API provided by the DryWetMIDI is the subject of this article. It shows how to send MIDI events to and to receive them from a MIDI device. Also, playing and recording MIDI data are described.

[^] top

Contents

  1. API Overview
  2. Input Device
  3. Output Device
  4. Devices Connector
  5. Playback
  6. Recording
  7. Links
  8. History

[^] top

API Overview

DryWetMIDI provides the ability to send MIDI data to or receive it from a MIDI device. For that purpose, there are following classes:

All these classes implement IDisposable and you should always dispose them to free devices for using by other applications. You can read more details about MIDI devices API by following the links above.

MIDI devices API classes are placed in the Melanchall.DryWetMidi.Devices namespace.

To understand what is input and output device in DryWetMIDI, take a look at the following image:

So, as you can see, although a MIDI port is MIDI IN for hardware device, it will be an output device (OutputDevice) in DryWetMIDI because your application will send MIDI data to this port. MIDI OUT of hardware device will be an input device (InputDevice) in DryWetMIDI because a program will receive MIDI data from the port.

InputDevice and OutputDevice are derived from MidiDevice class which has the following public members:

public abstract class MidiDevice : IDisposable
{
    // ...
    public event EventHandler<ErrorOccurredEventArgs> ErrorOccurred;
    // ...
    public int Id { get; }
    public string Name { get; }
    public Manufacturer DriverManufacturer { get; }
    public ushort ProductIdentifier { get; }
    public Version DriverVersion { get; }
    // ...
}

If some error occurred while sending or receiving a MIDI event, the ErrorOccurred event will be fired holding an exception caused the error.

[^] top

Input Device

In DryWetMIDI, an input MIDI device is represented by InputDevice class. It allows to receive events from a MIDI device.

To get an instance of InputDevice, you can use either GetByName or GetById static methods. ID of a MIDI device is a number from 0 to devices count minus one. To retrieve count of input MIDI devices presented in the system, there is the GetDevicesCount method. You can get all input MIDI devices with GetAll method.

After an instance of InputDevice is obtained, call StartEventsListening to start listening to incoming MIDI events going from an input MIDI device. If you don't need to listen for events anymore, call StopEventsListening. To check whether InputDevice is currently listening for events, use IsListeningForEvents property.

If an input device is listening for events, it will fire EventReceived event for each incoming MIDI event. Args of the event hold a MidiEvent received.

See API overview section for common members of a MIDI device class that are inherited by InputDevice from the base class MidiDevice.

Small example that shows receiving MIDI data:

using System;
using Melanchall.DryWetMidi.Devices;

// ...

using (var inputDevice = InputDevice.GetByName("Some MIDI device"))
{
    inputDevice.EventReceived += OnEventReceived;
    inputDevice.StartEventsListening();
}

// ...

private void OnEventReceived(object sender, MidiEventReceivedEventArgs e)
{
    var midiDevice = (MidiDevice)sender;
    Console.WriteLine($"Event received from '{midiDevice.Name}' at {DateTime.Now}: {e.Event}");
}

Note that you should always take care about disposing an InputDevice, i.e., use it inside using block or call Dispose manually. Without it, all resources taken by the device will live until GC collects them via finalizer of the InputDevice. It means that sometimes, you will not be able to use different instances of the same device across multiple applications or different pieces of a program.

By default, InputDevice will fire MidiTimeCodeReceived event when all MIDI Time Code components (MidiTimeCodeEvent events) are received forming hours:minutes:seconds:frames timestamp. You can turn this behavior off by setting RaiseMidiTimeCodeReceived to false.

If an invalid channel, system common or system real-time event received, InvalidShortEventReceived event will be fired holding the bytes that form the invalid event. If invalid system exclusive event is received, InvalidSysExEventReceived event will be fired holding sysex data.

To reset an input device, call Reset method.

[^] top

Output Device

In DryWetMIDI, an output MIDI device is represented by OutputDevice class. It allows to send events to a MIDI device.

To get an instance of OutputDevice, you can use either GetByName or GetById static methods. ID of a MIDI device is a number from 0 to devices count minus one. To retrieve count of output MIDI devices presented in the system, there is the GetDevicesCount method. You can get all output MIDI devices with GetAll method:

using System;
using Melanchall.DryWetMidi.Devices;

// ...

foreach (var outputDevice in OutputDevice.GetAll())
{
    Console.WriteLine(outputDevice.Name);
}

After an instance of OutputDevice is obtained, you can send MIDI events to device via SendEvent method. You cannot send meta events since such events can be inside a MIDI file only. If you pass an instance of meta event class, SendEvent will do nothing. EventSent event will be fired for each event sent with SendEvent holding the MIDI event. The value of DeltaTime property of MIDI events will be ignored, events will be sent to device immediately. To take delta-times into account, use Playback class (read Playback section to learn more).

If you need to interrupt all currently sounding notes, call the TurnAllNotesOff method which will send Note Off events on all channels for all note numbers (kind of "panic button" on MIDI devices).

See API overview section for common members of a MIDI device class that are inherited by OutputDevice from the base class MidiDevice.

Small example that shows sending MIDI data:

using System;
using Melanchall.DryWetMidi.Devices;
using Melanchall.DryWetMidi.Smf;

// ...

using (var outputDevice = OutputDevice.GetByName("Some MIDI device"))
{
    outputDevice.EventSent += OnEventSent;

    outputDevice.SendEvent(new NoteOnEvent());
    outputDevice.SendEvent(new NoteOffEvent());
}

// ...

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

Note that you should always take care about disposing an OutputDevice, i.e., use it inside using block or call Dispose manually. Without it, all resources taken by the device will live until GC collects them via finalizer of the OutputDevice. It means that sometimes, you will not be able to use different instances of the same device across multiple applications or different pieces of a program.

First call of SendEvent method can take some time for allocating resources for device, so if you want to eliminate this operation on sending a MIDI event, you can call PrepareForEventsSending method before any MIDI event will be sent.

Sections below describe specific properties of the OutputDevice class.

DeviceType

Gets the type of the current OutputDevice. Possible values are listed in the table below:

MidiPort MIDI hardware port
Synth Synthesizer
SquareWaveSynth Square wave synthesizer
FmSynth FM synthesizer
MidiMapper Microsoft MIDI mapper
WavetableSynth Hardware wavetable synthesizer
SoftwareSynth Software synthesizer

VoicesNumber

Gets the number of voices supported by an internal synthesizer device. If the device is a port, this member is not meaningful and will be 0.

NotesNumber

Gets the maximum number of simultaneous notes that can be played by an internal synthesizer device. If the device is a port, this member is not meaningful and will be 0.

Channels

Gets the channels that an internal synthesizer device responds to.

SupportsPatchCaching

Gets a value indicating whether device supports patch caching.

SupportsVolumeControl

Gets a value indicating whether device supports volume control.

SupportsLeftRightVolumeControl

Gets a value indicating whether device supports separate left and right volume control or not.

Volume

Gets or sets the volume of the output MIDI device. A value is an instance of the Volume class holding volume value for left and right channels. If SupportsLeftRightVolumeControl is false and you pass Volume with different values for each channel, an exception will be thrown.

[^] top

Devices Connector

To connect one MIDI device to another, there is DevicesConnector class.

Device connector connects an InputDevice with OutputDevice. To get an instance of DevicesConnector class, you can use either its constructor or Connect extension method on InputDevice. You must call Connect method of the DevicesConnector to make MIDI data actually go from InputDevice to OutputDevice.

The image below shows how devices will be connected in DryWetMIDI:

The following small example shows basic usage of DevicesConnector:

using Melanchall.DryWetMidi.Devices;

// ...

using (var inputDevice = InputDevice.GetByName("MIDI A"))
using (var outputDevice = OutputDevice.GetByName("MIDI B"))
using (var devicesConnector = new DevicesConnector(inputDevice, outputDevice))
{
    devicesConnector.Connect();
}

But to send MIDI data, we need an OutputDevice. So below is a complete example of transferring MIDI events between devices:

using System;
using Melanchall.DryWetMidi.Devices;
using Melanchall.DryWetMidi.Smf;

// ...

using (var inputB = InputDevice.GetByName("MIDI B"))
{
    inputB.EventReceived += OnEventReceived;
    inputB.StartEventsListening();

    using (var outputA = OutputDevice.GetByName("MIDI A"))
    {
        outputA.EventSent += OnEventSent;

        using (var inputA = InputDevice.GetByName("MIDI A"))
        using (var outputB = OutputDevice.GetByName("MIDI B"))
        using (var devicesConnector = inputA.Connect(outputB))
        {
            devicesConnector.Connect();

            // These events will be handled by OnEventSent on MIDI A and
            // OnEventReceived on MIDI B
            outputA.SendEvent(new NoteOnEvent());
            outputA.SendEvent(new NoteOffEvent());
        }
    }
}

// ...

private void OnEventReceived(object sender, MidiEventReceivedEventArgs e)
{
    var midiDevice = (MidiDevice)sender;
    Console.WriteLine($"Event received from '{midiDevice.Name}' at {DateTime.Now}: {e.Event}");
}

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

Don't forget to call StartEventsListening on InputDevice to make sure EventReceived will be fired.

Note that you should always take care about disposing a DevicesConnector, i.e., use it inside using block or call Dispose. Without it, all resources taken by the devices connector will live until GC collects them. It means that sometimes, you will not be able to use different instances of the same device across multiple applications or different pieces of a program.

[^] top

Playback

Playback class allows to play MIDI events via an OutputDevice. In other words, it sends MIDI data to output MIDI device taking events delta-times into account. To get an instance of the Playback, you must use its constructor passing collection of MIDI events, tempo map and output device:

using Melanchall.DryWetMidi.Devices;
using Melanchall.DryWetMidi.Smf;
using Melanchall.DryWetMidi.Smf.Interaction;

var eventsToPlay = new MidiEvent[]
{
    new NoteOnEvent(),
    new NoteOffEvent
    {
        DeltaTime = 100
    }
};

using (var outputDevice = OutputDevice.GetByName("Output MIDI device"))
using (var playback = new Playback(eventsToPlay, TempoMap.Default, outputDevice))
{
    // ...
}

There are also extension methods GetPlayback for TrackChunk, IEnumerable<TrackChunk>, MidiFile and Pattern classes which simplify obtaining a playback object for MIDI file entities and musical composition created with patterns:

using (var outputDevice = OutputDevice.GetByName("Output MIDI device"))
using (var playback = MidiFile.Read("Some MIDI file.mid").GetPlayback(outputDevice))
{
    // ...
}

GetDuration method returns the total duration of a playback in the specified format.

There are two approaches of playing MIDI data: blocking and non-blocking.

Blocking Playback

If you call Play method of the Playback, the calling thread will be blocked until the entire collection of MIDI events will be sent to MIDI device. Note that execution of this method will be infinite if the Loop property is set to true. See playback properties below to learn more.

There are also extension methods Play for TrackChunk, IEnumerable<TrackChunk>, MidiFile and Pattern classes which simplify playing MIDI file entities and musical composition created with patterns:

using (var outputDevice = OutputDevice.GetByName("Output MIDI device"))
{
    MidiFile.Read("Some MIDI file.mid").Play(outputDevice);

    // ...
}

Non-Blocking Playback

Is you call Start method of the Playback, execution of the calling thread will continue immediately after the method is called. To stop playback, use Stop method. Note that there is no Pause method since it is useless. Stop leaves playback at the point where the method was called. To move to the start of the playback, use MoveToStart method described in the Time management section below.

You should be very careful with this approach and using block. The example below shows the case where part of MIDI data will not be played because playback is disposed before the last MIDI event will be sent to output device:

using (var outputDevice = OutputDevice.GetByName("Output MIDI device"))
using (var playback = MidiFile.Read("Some MIDI file.mid").GetPlayback(outputDevice))
{
    playback.Start();

    // ...
}

With non-blocking approach, it is recommended to call Dispose manually after you've finished work with playback object.

After playback finished, the Finished event will be fired. Started and Stopped events will be fired on Start (Play) and Stop calls respectively.

Playback Properties

Let's see public properties of the Playback class.

Loop

Gets or sets a value indicating whether playing should automatically start from the first event after the last one played. If you set it to true and call Play method, calling thread will be blocked forever. The default value is false.

Speed

Gets or sets the speed of events playing. 1.0 means normal speed which is the default. For example, to play MIDI data twice slower, this property should be set to 0.5. Pass 10.0 to play MIDI events ten times faster.

NoteStopPolicy

Gets or sets a value determining how currently playing notes should react on playback stopped (via Stop method). The default value is NoteStopPolicy.Hold. Possible values are listed in the table below:

Hold Do nothing and let notes playing.
Interrupt Interrupt notes by sending corresponding Note Off events.
Split Split notes at the moment of playback stopped. Notes will be interrupted by sending corresponding Note Off events, but when playback will be resumed, they will be played from point of split via sending Note On events.

IsRunning

Gets a value indicating whether playing is currently running or not.

Time Management

You have several options to manipulate by the current time of playback:

  • GetCurrentTime

    Returns the current time of a playback in the specified format.

  • MoveToStart

    Sets playback position to the beginning of the MIDI data.

  • MoveToTime

    Sets playback position to the specified time from the beginning of the MIDI data. If new position is greater than playback duration, position will be set to the end of the playback.

  • MoveForward

    Shifts playback position forward by the specified step. If new position is greater than playback duration, position will be set to the end of the playback.

  • MoveBack

    Shifts playback position back by the specified step. If step is greater than the elapsed time of playback, position will be set to the start of the playback.

You don't need to call Stop method if you want to call any method that changes the current playback position.

[^] top

Recording

To capture MIDI data from an input MIDI device, you can use Recording class which will collect incoming MIDI events. To start recording, you need to create an instance of the Recording passing tempo map and input device to its constructor:

using Melanchall.DryWetMidi.Devices;
using Melanchall.DryWetMidi.Smf.Interaction;

// ...

using (var inputDevice = InputDevice.GetByName("Input MIDI device"))
{
    var recording = new Recording(TempoMap.Default, inputDevice);

    // ...
}

Don't forget to call StartEventsListening on InputDevice before you start recording since Recording does nothing with the device you've specified.

To start recording, call Start method. To stop it, call Stop method. You can resume recording after it has been stopped by calling Start again. To check whether recording is currently running, get a value of the IsRunning property. Start and Stop methods fire Started and Stopped events respectively.

You can get recorded events as IEnumerable<TimedEvent> with the GetEvents method.

GetDuration method returns the total duration of a recording in the specified format.

Take a look at a small example of MIDI data recording:

using (var inputDevice = InputDevice.GetByName("Input MIDI device"))
{
    var recording = new Recording(TempoMap.Default, inputDevice);

    inputDevice.StartEventsListening();
    recording.Start();

    // ...

    recording.Stop();

    var recordedFile = recording.ToFile();
    recording.Dispose();
    recordedFile.Write("Recorded data.mid");
}

[^] top

Links

[^] top

History

  • 31st January, 2019: Article submitted

[^] top

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

Share

About the Author

Maxim Dobroselsky
Software Developer
Russian Federation Russian Federation
My primary skills are C#, WPF and ArcObjects/ArcGIS Pro SDK. Currently I'm working on autotests in Kaspersky Lab.

Also I'm writing music which led me to starting the DryWetMIDI project on the GitHub. DryWetMIDI is an open source .NET library written in C# for managing MIDI files. The library is currently actively developing.

Also I actively help people on Code Review Stack Exchange to improve their C# code and have some answers on WPF related questions on Stack Overflow.

You may also be interested in...

Comments and Discussions

 
PraiseAwesome! Pin
CullyLine1-Feb-19 3:58
memberCullyLine1-Feb-19 3:58 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.

Permalink | Advertise | Privacy | Cookies | Terms of Use | Mobile
Web05 | 2.8.190306.1 | Last Updated 31 Jan 2019
Article Copyright 2019 by Maxim Dobroselsky
Everything else Copyright © CodeProject, 1999-2019
Layout: fixed | fluid