DryWetMIDI: 处理 MIDI 设备





5.00/5 (7投票s)
概述如何使用 DryWetMIDI 发送、接收、播放和录制 MIDI 数据
介绍
DryWetMIDI 是一个用于处理 MIDI 文件和 MIDI 设备的 .NET 库。要了解 MIDI 文件处理,请阅读“DryWetMIDI: 高级 MIDI 文件处理”。
本文将重点介绍 DryWetMIDI 提供的设备 API。它演示了如何向 MIDI 设备发送 MIDI 事件以及从 MIDI 设备接收 MIDI 事件。此外,还将介绍 MIDI 数据的播放和录制。
请注意,本文档并未涵盖所有可用的 API。请阅读该库的文档以了解更多信息。
目录
- API 概述
- InputDevice
- OutputDevice
- DevicesConnector
- Playback
- Recording
- VirtualDevice
- DevicesWatcher
- 链接
- 历史
API 概述
DryWetMIDI 提供了将 MIDI 数据发送到 MIDI 设备或从 MIDI 设备接收 MIDI 数据的能力。为此,提供了以下类型:
对于 macOS,还提供以下两个类:
VirtualDevice
DevicesWatcher
DryWetMIDI 提供了 IInputDevice
和 IOutputDevice
的内置实现——分别是 InputDevice
和 OutputDevice
。在讨论设备时,我们将使用这些类而不是接口。这些类型实现了 IDisposable
,您应始终对其进行处置以释放设备供其他应用程序使用。您可以通过上述链接阅读有关 MIDI 设备 API 的更多详细信息。
此外,还有两个重要的类用于播放 MIDI 数据和捕获设备中的 MIDI 数据:
MIDI 设备 API 类位于 Melanchall.DryWetMidi.Multimedia
命名空间中。
要理解 DryWetMIDI 中的输入设备和输出设备是什么,请看下图:
因此,正如您所见,尽管硬件设备的 MIDI 端口是 MIDI IN,但在 DryWetMIDI 中它将是一个输出设备(OutputDevice
),因为您的应用程序将 **向** 此端口 **发送 MIDI 数据**。硬件设备的 MIDI OUT 将是 DryWetMIDI 中的一个输入设备(InputDevice
),因为程序将 **从** 该端口 **接收 MIDI 数据**。
InputDevice
和 OutputDevice
继承自 MidiDevice
类,该类具有以下 **public** 成员:
public abstract class MidiDevice : IDisposable
{
// ...
public event EventHandler<ErrorOccurredEventArgs> ErrorOccurred;
// ...
public string Name { get; }
// ...
}
如果在发送或接收 MIDI 事件时发生错误,将触发 ErrorOccurred
事件,其中包含导致错误的异常。
InputDevice
在 DryWetMIDI 中,输入 MIDI 设备由 InputDevice
类表示。它允许从 MIDI 设备接收事件。
要获取 InputDevice
的实例,可以使用 GetByName
或 GetByIndex
**静态** 方法。MIDI 设备的索引是从 0
到设备计数减一的数字。要检索系统中存在的输入 MIDI 设备的总数,可以使用 GetDevicesCount
方法。您可以通过 GetAll
方法获取所有输入 MIDI 设备。
获取 InputDevice
的实例后,调用 StartEventsListening
来开始监听来自输入 MIDI 设备的传入 MIDI 事件。如果您不再需要监听事件,请调用 StopEventsListening
。要检查 InputDevice
当前是否正在监听事件,请使用 IsListeningForEvents
属性。
如果输入设备正在监听事件,它将为每个传入的 MIDI 事件触发 EventReceived
事件。事件的参数包含一个收到的 MidiEvent
。
有关 MIDI 设备类从基类 MidiDevice
继承的通用成员,请参阅 API 概述 部分。
一个小例子,演示如何接收 MIDI 数据
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}");
}
请注意,您应始终注意处置 InputDevice
,即在 using
块内使用它或手动调用 Dispose
。否则,设备所占用的所有资源将一直存在,直到 GC 通过 InputDevice
的终结器对其进行回收。这意味着有时您将无法在多个应用程序或程序的不同部分之间使用同一设备的多个实例。
默认情况下,当所有 MIDI 时间码组件(MidiTimeCodeEvent
事件)都接收完毕并形成 *小时:分钟:秒:帧* 时间戳时,InputDevice
将触发 MidiTimeCodeReceived
事件。您可以通过将 RaiseMidiTimeCodeReceived
设置为 false
来禁用此行为。
如果收到无效事件,将触发 ErrorOccurred
事件,其中包含无效事件的数据。
OutputDevice
在 DryWetMIDI 中,输出 MIDI 设备由 OutputDevice
类表示。它允许向 MIDI 设备发送事件。
要获取 OutputDevice
的实例,可以使用 GetByName
或 GetByIndex
**静态** 方法。MIDI 设备的索引是从 0
到设备计数减一的数字。要检索系统中存在的输出 MIDI 设备的总数,可以使用 GetDevicesCount
方法。您可以通过 GetAll
方法获取所有输出 MIDI 设备。
using System;
using Melanchall.DryWetMidi.Multimedia;
// ...
foreach (var outputDevice in OutputDevice.GetAll())
{
Console.WriteLine(outputDevice.Name);
}
获取 OutputDevice
的实例后,可以通过 SendEvent
方法将 MIDI 事件发送到设备。您不能发送元事件,因为此类事件只能存在于 MIDI 文件中。如果您传递一个元事件类的实例,SendEvent
将什么也不做。EventSent
事件将为使用 SendEvent
发送的每个事件触发,其中包含 MIDI 事件。MIDI 事件的 DeltaTime
属性值将被忽略,事件将立即发送到设备。要考虑 delta-times,请使用 Playback
类(有关详细信息,请参阅 Playback 部分)。
如果您需要中断所有当前正在播放的音符,请调用 TurnAllNotesOff
方法,该方法将在所有通道上为所有音符编号发送 *Note Off* 事件(相当于 MIDI 设备上的“紧急停止按钮”)。
有关 MIDI 设备类从基类 MidiDevice
继承的通用成员,请参阅 API 概述 部分。
一个小例子,演示如何发送 MIDI 数据
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}");
}
请注意,您应始终注意处置 OutputDevice
,即在 using
块内使用它或手动调用 Dispose
。否则,设备所占用的所有资源将一直存在,直到 GC 通过 OutputDevice
的终结器对其进行回收。这意味着有时您将无法在多个应用程序或程序的不同部分之间使用同一设备的多个实例。
首次调用 SendEvent
方法可能需要一些时间来分配设备资源,因此,如果您想在发送 MIDI 事件时消除此操作,可以在发送任何 MIDI 事件之前调用 PrepareForEventsSending
方法。
DevicesConnector
要将一个 MIDI 设备连接到另一个设备,可以使用 DevicesConnector
类。
设备连接器将 IInputDevice
连接到多个 IOutputDevice
对象。要获取 DevicesConnector
类的实例,可以使用其构造函数或在 IInputDevice
上使用 Connect
扩展方法。您必须调用 DevicesConnector
的 Connect
方法才能使 MIDI 数据真正从输入设备流向输出设备。
下图显示了 DryWetMIDI 中设备的连接方式:
以下小例子演示了 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();
}
但是,要发送 MIDI 数据,我们需要一个 OutputDevice
。因此,下面是一个在设备之间传输 MIDI 事件的完整示例:
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();
// 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}");
}
请勿忘记在 InputDevice
上调用 StartEventsListening
,以确保 EventReceived
会被触发。
Playback
Playback
类允许通过 IOutputDevice
播放 MIDI 事件。换句话说,它在考虑事件 delta-times 的情况下将 MIDI 数据发送到输出 MIDI 设备。要获取 Playback
的实例,您必须使用其构造函数,传入 MIDI 事件集合、速度图和输出设备。
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))
{
// ...
}
此外,还为 TrackChunk
、IEnumerable<TrackChunk>
、MidiFile
和 Pattern
类提供了 GetPlayback
扩展方法,这些方法简化了 MIDI 文件实体和使用 模式 创建的音乐组合的 playback
对象的获取。
using (var outputDevice = OutputDevice.GetByName("Output MIDI device"))
using (var playback = MidiFile.Read("Some MIDI file.mid").GetPlayback(outputDevice))
{
// ...
}
GetDuration
方法以指定格式返回 playback
的总时长。
有两种播放 MIDI 数据的方法:阻塞式和非阻塞式。
阻塞式播放
如果您调用 Playback
的 Play
方法,调用线程将被阻塞,直到整个 MIDI 事件集合都被发送到 MIDI 设备。请注意,如果 Loop
属性设置为 true
,此方法的执行将是无限的。有关详细信息,请参阅下面的 播放属性。
此外,还为 TrackChunk
、IEnumerable<TrackChunk>
、MidiFile
和 Pattern
类提供了 Play
扩展方法,这些方法简化了 MIDI 文件实体和使用 模式 创建的音乐组合的播放。
using (var outputDevice = OutputDevice.GetByName("Output MIDI device"))
{
MidiFile.Read("Some MIDI file.mid").Play(outputDevice);
// ...
}
非阻塞式播放
如果您调用 Playback
的 Start
方法,调用线程将立即继续执行。要停止播放,请使用 Stop
方法。请注意,没有 Pause
方法,因为它没有用。Stop
会将播放停在调用该方法的位置。要移动到播放的开头,请使用下面 时间管理 部分中描述的 MoveToStart
方法。
您在使用此方法和 using
块时应格外小心。下面的示例演示了部分 MIDI 数据不会被播放的情况,因为 playback
在最后一个 MIDI 事件发送到输出设备之前就被处置了。
using (var outputDevice = OutputDevice.GetByName("Output MIDI device"))
using (var playback = MidiFile.Read("Some MIDI file.mid").GetPlayback(outputDevice))
{
playback.Start();
// ...
}
对于非阻塞式方法,建议在完成 playback
对象的工作后手动调用 Dispose
。
播放完成后,将触发 Finished
事件。在调用 Start
(Play
)和 Stop
时将分别触发 Started
和 Stopped
事件。
Playback Properties
让我们看看 Playback
类的一些 **public** 属性。有关完整的 API 参考,请参阅 Playback 文档。
循环
获取或设置一个值,该值指示播放是否应在最后一个事件播放后自动从第一个事件开始。如果将其设置为 true
并调用 Play
方法,调用线程将永远被阻塞。默认值为 false
。
速度
获取或设置事件播放的速度。1.0
表示正常速度,这是默认值。例如,要将 MIDI 数据播放速度减慢一倍,此属性应设置为 0.5
。传入 10.0
可将 MIDI 事件播放速度提高十倍。
InterruptNotesOnStop
获取或设置一个值,该值指示在对播放调用 Stop
方法时,当前播放的音符是否必须停止。
TrackNotes
获取或设置一个值,该值指示是否必须跟踪音符。如果为 false
,音符将仅被视为 *Note On* / *Note Off* 事件。如果为 true
,音符将被视为带有长度的对象。
如果播放在中途停止了一个音符,则在停止时将发送 *Note Off* 事件,在开始播放时发送 *Note On* 事件。如果调用了 时间管理 中的任何一个方法
- 并且旧播放位置位于一个音符上,则将为此音符发送 *Note Off*;
- 并且新播放位置位于一个音符上,则将为此音符发送 *Note On*。
IsRunning
获取一个值,该值指示播放当前是否正在运行。
NoteCallback
获取或设置一个回调,用于在音符即将播放时处理它们。
EventCallback
获取或设置一个回调,用于在 MIDI 事件即将播放时处理它们。
Snapping
提供了一种管理播放吸附点的方法。有关详细信息,请参阅下面的 Snapping 部分。
Snapping
Playback
的 Snapping
属性提供了一个 PlaybackSnapping
类的实例,该类管理播放的吸附点。吸附点是指定时间标记,可用于跳转(请参阅下面的 时间管理 部分)。
PlaybackSnapping
提供以下成员:
IEnumerable<SnapPoint> SnapPoints
返回当前为播放设置的所有吸附点(包括禁用的)。
SnapPoint<TData> AddSnapPoint<TData>(ITimeSpan time, TData data)
在给定时间添加一个带有指定数据的吸附点。数据将通过方法返回的吸附点的 Data
属性可用。
SnapPoint<Guid> AddSnapPoint(ITimeSpan time)
在指定时间添加一个吸附点,不带用户数据。Data
将包含唯一的 Guid
值。
RemoveSnapPoint<TData>(SnapPoint<TData> snapPoint)
删除一个吸附点。
RemoveSnapPointsByData<TData>(Predicate<TData> predicate)
删除所有与指定谓词定义的条件匹配的吸附点。
SnapPointsGroup SnapToGrid(IGrid grid)
在由指定网格定义的时间添加吸附点。IGrid
的实现包括 SteppedGrid
(时间由重复步长集合定义)和 ArbitraryGrid
(时间由显式指定)。将返回一个 SnapPointsGroup
实例,可用于管理由该方法添加的所有吸附点。
SnapPointsGroup SnapToNotesStarts()
在音符的开始时间添加吸附点。返回的组可用于管理添加的吸附点。
SnapPointsGroup SnapToNotesEnds()
在音符的结束时间添加吸附点。返回的组可用于管理添加的吸附点。
SnapPoint
上面方法返回的 SnapPoint<>
继承自 SnapPoint
,并添加了 Data
属性,该属性包含附加到吸附点的用户数据。SnapPoint
具有以下成员:
IsEnabled
获取或设置一个值,该值指示吸附点是否已启用。可用于打开或关闭吸附点。如果吸附点被禁用,它将不参与 时间管理 操作。
时间
获取吸附点的时间,作为 TimeSpan
实例。
SnapPointsGroup
获取吸附点所属的组,作为 SnapPointsGroup
实例。对于由 AddSnapPoint
方法添加的吸附点,此值将为 null。
SnapPointsGroup
SnapPointsGroup
具有 IsEnabled
属性,与 SnapPoint
的属性类似。如果吸附点组被禁用,其吸附点将不参与 时间管理 操作。
Time Management
您有多种选项来操作播放的当前时间:
GetCurrentTime
以指定格式返回
playback
的当前时间。MoveToStart
将
playback
位置设置为 MIDI 数据的开头。MoveToTime
将
playback
位置设置为从 MIDI 数据开始的指定时间。如果新位置大于playback
持续时间,则位置将设置为playback
的末尾。MoveForward
将
playback
位置向前移动指定的步长。如果新位置大于playback
持续时间,则位置将设置为playback
的末尾。MoveBack
将
playback
位置向后移动指定的步长。如果步长大于playback
的已用时间,则位置将设置为playback
的开头。MoveToSnapPoint(SnapPoint snapPoint)
将播放位置设置为指定吸附点的时间。如果 snapPoint
被禁用,则什么也不做。
MoveToPreviousSnapPoint(SnapPointsGroup snapPointsGroup)
将播放位置设置为属于 snapPointsGroup
的上一个吸附点(相对于当前播放时间)的时间。
MoveToPreviousSnapPoint
将播放位置设置为上一个吸附点(相对于当前播放时间)的时间。
MoveToNextSnapPoint(SnapPointsGroup snapPointsGroup)
将播放位置设置为属于指定 SnapPointsGroup
的下一个吸附点(相对于当前播放时间)的时间。
MoveToNextSnapPoint
将播放位置设置为下一个吸附点(相对于当前播放时间)的时间。
如果您想调用任何改变 playback
当前位置的方法,则无需调用 Stop
方法。
Recording
要从输入 MIDI 设备捕获 MIDI 数据,可以使用 Recording
类,该类将收集传入的 MIDI 事件。要开始录制,您需要创建一个 Recording
实例,将速度图和输入设备传递给其构造函数。
using Melanchall.DryWetMidi.Multimedia;
using Melanchall.DryWetMidi.Interaction;
// ...
using (var inputDevice = InputDevice.GetByName("Input MIDI device"))
{
var recording = new Recording(TempoMap.Default, inputDevice);
// ...
}
在开始录制之前,请不要忘记在 InputDevice
上调用 StartEventsListening
,因为 Recording
不会对您指定的设备做任何操作。
要开始录制,请调用 Start
方法。要停止录制,请调用 Stop
方法。录制停止后可以再次调用 Start
来恢复录制。要检查录制当前是否正在运行,请获取 IsRunning
属性的值。Start
和 Stop
方法分别触发 Started
和 Stopped
事件。
您可以使用 GetEvents
方法将录制的事件作为 IEnumerable<TimedEvent>
获取。
GetDuration
方法以指定格式返回录制的总时长。
来看一个 MIDI 数据录制的简单示例:
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");
}
VirtualDevice
VirtualDevice
目前仅在 macOS 上可用,它提供了一种动态创建虚拟 MIDI 设备(即环回设备/虚拟电缆)的方法。换句话说,虚拟设备是一个配对的输入和输出设备。发送到虚拟设备输出子设备的任何 MIDI 事件都将被重定向到输入子设备,因此 MIDI 数据将在没有任何转换的情况下被发送回来。
有关详细信息,请参阅该库文档的 虚拟设备 文章。
DevicesWatcher
DevicesWatcher 也仅在 macOS 上可用。它允许监视 MIDI 设备是否被添加到(插入)或从(拔出)系统中移除。
有关详细信息,请参阅该库文档的 设备监视器 文章。
Links
- NuGet 包: https://nuget.net.cn/packages/Melanchall.DryWetMidi
- GitHub 存储库: https://github.com/melanchall/drywetmidi
- 文档: https://melanchall.github.io/drywetmidi
历史
- 2021 年 10 月 22 日
- 文章已更新,以反映库 6.0.0 版本中引入的更改。
- 2021 年 6 月 27 日
- 添加了一些关于本文档并非 API 参考的说明。
- 2019 年 11 月 23 日
- 文章已更新,以反映库 5.0.0 版本中引入的更改。
- 2019 年 5 月 10 日
- 文章已更新,以反映库 4.1.0 版本中引入的更改。
- 2019 年 1 月 31 日
- 文章提交