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".
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.
Please note that this article doesn't cover all available API. Please read the library documentation to learn more.
[^] top
Contents
- API Overview
- InputDevice
- OutputDevice
- DevicesConnector
- Playback
- Recording
- VirtualDevice
- DevicesWatcher
- Links
- 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 types:
For macOS following two classes are also available:
VirtualDevice
DevicesWatcher
DryWetMIDI provides built-in implementations of IInputDevice
and IOutputDevice
– InputDevice
and OutputDevice
correspondingly. Talking about devices we will use these classes instead of interfaces. These types 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.
Also there are two important classes to play MIDI data back and capture MIDI data from a device:
MIDI devices API classes are placed in the Melanchall.DryWetMidi.Multimedia
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 string Name { 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
InputDevice
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 GetByIndex
static
methods. Index 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.Multimedia;
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 event received, ErrorOccurred
event will be fired holding the data of the invalid event.
[^] top
OutputDevice
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 GetByIndex
static
methods. Index 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.Multimedia;
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.Multimedia;
using Melanchall.DryWetMidi.Core;
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.
[^] top
DevicesConnector
To connect one MIDI device to another, there is DevicesConnector
class.
Device connector connects an IInputDevice
with multiple IOutputDevice
objects. To get an instance of DevicesConnector
class, you can use either its constructor or Connect
extension method on IInputDevice
. You must call Connect
method of the DevicesConnector
to make MIDI data actually go from input device to output devices.
The image below shows how devices will be connected in DryWetMIDI:
The following small example shows basic usage of DevicesConnector
:
using Melanchall.DryWetMidi.Multimedia;
using (var inputDevice = InputDevice.GetByName("MIDI A"))
using (var outputDevice = OutputDevice.GetByName("MIDI B"))
{
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.Multimedia;
using Melanchall.DryWetMidi.Core;
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"))
{
var devicesConnector = inputA.Connect(outputB);
devicesConnector.Connect();
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.
[^] top
Playback
Playback
class allows to play MIDI events via an IOutputDevice
. 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.Core;
using Melanchall.DryWetMidi.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 some public
properties of the Playback
class. Please refer to the Playback documentation to see full API reference.
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.
InterruptNotesOnStop
Gets or sets a value indicating whether currently playing notes must be stopped when Stop
method called on playback.
TrackNotes
Gets or sets a value indicating whether notes must be tracked or not. If false
, notes will be treated as just Note On/Note Off events. If true
, notes will be treated as lengthed objects.
If playback stopped in middle of a note, then Note Off event will be sent on stop, and Note On – on playback start. If one of time management methods is called:
- and old playback's position was on a note, then Note Off will be send for that note;
- and new playback's position is on a note, then Note On will be send for that note.
IsRunning
Gets a value indicating whether playing is currently running or not.
NoteCallback
Gets or sets a callback used to process notes as they are about to be played.
EventCallback
Gets or sets a callback used to process MIDI events as they are about to be played.
Snapping
Provides a way to manage snap points for playback. See Snapping section below to learn more.
Snapping
Snapping
property of the Playback
gives an instance of the PlaybackSnapping
class which manages snap points for playback. Snap points are markers at speicified times which can be used to move to (see Time management section below).
PlaybackSnapping
provides following members:
IEnumerable<SnapPoint> SnapPoints
Returns all snap points currently set for playback (including disabled ones).
SnapPoint<TData> AddSnapPoint<TData>(ITimeSpan time, TData data)
Add a snap point with the specified data at given time. The data will be available through Data
property of the snap point returned by the method.
SnapPoint<Guid> AddSnapPoint(ITimeSpan time)
Add a snap point at the specified time without user data. Data
will hold unique Guid
value.
RemoveSnapPoint<TData>(SnapPoint<TData> snapPoint)
Remove a snap point.
RemoveSnapPointsByData<TData>(Predicate<TData> predicate)
Remove all snap points that match the conditions defined by the specified predicate.
SnapPointsGroup SnapToGrid(IGrid grid)
Add snap points at times defined by the specified grid. Implementations of IGrid
are SteppedGrid
(grid where times are defined by collection of repeating steps) and ArbitraryGrid
(grid where times are specified explicitly). An instance of SnapPointsGroup
will be returned which can be used to manage all the snap points added by the method.
SnapPointsGroup SnapToNotesStarts()
Adds snap points at start times of notes. Returned group can be used to manage added snap points.
SnapPointsGroup SnapToNotesEnds()
Adds snap points at end times of notes. Returned group can be used to manage added snap points.
SnapPoint
SnapPoint<>
returned by the methods above is inherited from SnapPoint
and just adds Data
property that holds user data attached to a snap point. SnapPoint
has following members:
IsEnabled
Gets or sets a value indicating whether a snap point is enabled or not. Can be used to turn a snap point on or off. If a snap point is disabled, it won't participate in time management operations.
Time
Gets the time of a snap point as an instance of TimeSpan
.
SnapPointsGroup
Gets the group a snap point belongs to as an instance of SnapPointsGroup
. It will be null for snap points added by AddSnapPoint
method.
SnapPointsGroup
SnapPointsGroup
has IsEnabled
property that is similar to that the SnapPoint
has. If a snap points group is disabled, its snap points won't participate in time management operations.
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
.
MoveToSnapPoint(SnapPoint snapPoint)
Sets playback position to the time of the specified snap point. If snapPoint
is disabled, nothing will happen.
MoveToPreviousSnapPoint(SnapPointsGroup snapPointsGroup)
Sets playback position to the time of the previous snap point (relative to the current time of playback) that belongs to snapPointsGroup
.
Sets playback position to the time of the previous snap point (relative to the current time of playback).
MoveToNextSnapPoint(SnapPointsGroup snapPointsGroup)
Sets playback position to the time of the next snap point (relative to the current time of playback) that belongs to the specified SnapPointsGroup
.
Sets playback position to the time of the next snap point (relative to the current time of 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.Multimedia;
using Melanchall.DryWetMidi.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
VirtualDevice
VirtualDevice
available for macOS only at now and provides a way t odynamically create virtual MIDI devices that are loopback devices (virtual cables). In other words virtual device is paired input and output device. Each MIDI event sent to the output subdevice of a virtual one will be redirected to the input subdevice, so MIDI data will be sent back without any transformation.
Please see Virtual device article of the library documentation to learn more.
[^] top
DevicesWatcher
DevicesWatcher also available for macOS only. It allows to monitor whether a MIDI device is added to (plugged) or removed from (unplugged) the system.
Please see Devices watcher article of the library documentation to learn more.
[^] top
Links
[^] top
History
- 22d October, 2021
- Article updated to reflect changes introduced in version 6.0.0 of the library
- 27th June, 2021
- Added some notes about the article is not API reference
- 23rd November, 2019
- Article updated to reflect changes introduced in version 5.0.0 of the library
- 10th May, 2019
- Article updated to reflect changes introduced in version 4.1.0 of the library
- 31st January, 2019
[^] top