65.9K
CodeProject 正在变化。 阅读更多。
Home

DryWetMIDI: 处理 MIDI 设备

starIconstarIconstarIconstarIconstarIcon

5.00/5 (7投票s)

2019 年 1 月 31 日

CPOL

13分钟阅读

viewsIcon

38081

概述如何使用 DryWetMIDI 发送、接收、播放和录制 MIDI 数据

介绍

DryWetMIDI 是一个用于处理 MIDI 文件和 MIDI 设备的 .NET 库。要了解 MIDI 文件处理,请阅读“DryWetMIDI: 高级 MIDI 文件处理”。

本文将重点介绍 DryWetMIDI 提供的设备 API。它演示了如何向 MIDI 设备发送 MIDI 事件以及从 MIDI 设备接收 MIDI 事件。此外,还将介绍 MIDI 数据的播放和录制。

请注意,本文档并未涵盖所有可用的 API。请阅读该库的文档以了解更多信息。

[^] 返回顶部

目录

  1. API 概述
  2. InputDevice
  3. OutputDevice
  4. DevicesConnector
  5. Playback
  6. Recording
  7. VirtualDevice
  8. DevicesWatcher
  9. 链接
  10. 历史

[^] 返回顶部

API 概述

DryWetMIDI 提供了将 MIDI 数据发送到 MIDI 设备或从 MIDI 设备接收 MIDI 数据的能力。为此,提供了以下类型:

对于 macOS,还提供以下两个类:

  • VirtualDevice
  • DevicesWatcher

DryWetMIDI 提供了 IInputDeviceIOutputDevice 的内置实现——分别是 InputDeviceOutputDevice。在讨论设备时,我们将使用这些类而不是接口。这些类型实现了 IDisposable,您应始终对其进行处置以释放设备供其他应用程序使用。您可以通过上述链接阅读有关 MIDI 设备 API 的更多详细信息。

此外,还有两个重要的类用于播放 MIDI 数据和捕获设备中的 MIDI 数据:

MIDI 设备 API 类位于 Melanchall.DryWetMidi.Multimedia 命名空间中。

要理解 DryWetMIDI 中的输入设备和输出设备是什么,请看下图:

因此,正如您所见,尽管硬件设备的 MIDI 端口是 MIDI IN,但在 DryWetMIDI 中它将是一个输出设备(OutputDevice),因为您的应用程序将 **向** 此端口 **发送 MIDI 数据**。硬件设备的 MIDI OUT 将是 DryWetMIDI 中的一个输入设备(InputDevice),因为程序将 **从** 该端口 **接收 MIDI 数据**。

InputDeviceOutputDevice 继承自 MidiDevice 类,该类具有以下 **public** 成员:

public abstract class MidiDevice : IDisposable
{
    // ...
    public event EventHandler<ErrorOccurredEventArgs> ErrorOccurred;
    // ...
    public string Name { get; }
    // ...
}

如果在发送或接收 MIDI 事件时发生错误,将触发 ErrorOccurred 事件,其中包含导致错误的异常。

[^] 返回顶部

InputDevice

在 DryWetMIDI 中,输入 MIDI 设备由 InputDevice 类表示。它允许从 MIDI 设备接收事件。

要获取 InputDevice 的实例,可以使用 GetByNameGetByIndex **静态** 方法。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 的实例,可以使用 GetByNameGetByIndex **静态** 方法。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 扩展方法。您必须调用 DevicesConnectorConnect 方法才能使 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))
{
    // ...
}

此外,还为 TrackChunkIEnumerable<TrackChunk>MidiFilePattern 类提供了 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 数据的方法:阻塞式和非阻塞式。

阻塞式播放

如果您调用 PlaybackPlay 方法,调用线程将被阻塞,直到整个 MIDI 事件集合都被发送到 MIDI 设备。请注意,如果 Loop 属性设置为 true,此方法的执行将是无限的。有关详细信息,请参阅下面的 播放属性

此外,还为 TrackChunkIEnumerable<TrackChunk>MidiFilePattern 类提供了 Play 扩展方法,这些方法简化了 MIDI 文件实体和使用 模式 创建的音乐组合的播放。

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

    // ...
}

非阻塞式播放

如果您调用 PlaybackStart 方法,调用线程将立即继续执行。要停止播放,请使用 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 事件。在调用 StartPlay)和 Stop 时将分别触发 StartedStopped 事件。

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

PlaybackSnapping 属性提供了一个 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 属性的值。StartStop 方法分别触发 StartedStopped 事件。

您可以使用 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

[^] 返回顶部

历史

  • 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 日
    • 文章提交

[^] 返回顶部

© . All rights reserved.