c# midi窗体

介绍 (Introduction)

I do some MIDI sequencing and recording and I found it helpful to be able to splice sections out of a MIDI file, but I didn't have a tool that made it easy to do. In the process of creating such a tool, I made a Midi assembly that contained the core MIDI file manipulation options. I also wrote some remedial playback code at first, which used the 32-bit Windows MIDI API.

我进行了一些MIDI音序和录制,发现可以将片段从MIDI文件中拼接出来很有帮助,但是我没有一个易于操作的工具。 在创建此类工具的过程中,我制作了一个Midi程序集,其中包含核心MIDI文件操作选项。 首先,我还编写了一些补救性播放代码,该代码使用了32位Windows MIDI API。

That library grew as I added more features and shored up what I had. I added some more demos, streaming support, MIDI input support, device enumeration and more. Eventually, I had wrapped maybe 90-95% of the API, and had a battery of MIDI manipulation functions for searching and modifying in memory sequences and files.

随着我添加更多功能并支持我所拥有的,该图书馆也随之增长。 我添加了更多演示,流支持,MIDI输入支持,设备枚举等。 最终,我包装了大约90-95%的API,并拥有一系列MIDI操作功能,用于在内存序列和文件中进行搜索和修改。

In the process, MidiSlicer moved from a first class application to just another demo project, so the solution is still named MidiSlicer - I'm stuck with the GitHub of that name. The core library project is named Midi.

在此过程中, MidiSlicer从一流的应用程序迁移到了另一个演示项目,因此该解决方案仍被命名为MidiSlicer我被这个名字的GitHub所困扰。 核心库项目名为Midi

I've produced articles on using bits and pieces of it, but never a comprehensive guide, and I aim to do that here.

我已经撰写了有关使用它的点点滴滴的文章,但从未提供过全面的指南,我打算在此处进行操作。

Update: Added experimental tempo synchronization functionality to the library. It doesn't get the timing perfect because I can't get the latency low enough consistently to make it super accurate, but I've provided it in the interest of completeness.

更新 :向库中添加了实验性速度同步功能。 它不能提供完美的时序,因为我无法始终如一地将等待时间降低到足够低的延迟以使其变得非常准确,但是出于完整性考虑,我提供了它。

Update 2: Added a few MidiSequence improvements to help with locating positions within a track based on time, like GetPositionAtTime(), and GetNextEventAtPosition(). As before you can still use MidiSequence.GetContext(position).Time to get a time from  a position.

更新2:添加了一些MidiSequence改进,以帮助根据时间在轨道中定位位置,例如GetPositionAtTime()GetNextEventAtPosition() 。 和以前一样,您仍然可以使用MidiSequence.GetContext(position).Time从某个位置获取时间。

Update 3: Fixed stability issue in MidiStream. It turns out I misunderstood something about the way the MIDI driver api works, and it's not very well documented so I didn't have a lot of help. It worked, until I "optimized" it to reduce unmanaged heap fragmentation a little, but it couldn't take the optimization for reasons. It turns out it just wasn't doing with the memory what I thought it was. Anyway, I fixed that. Get this update, especially if your app is randomly crashing.

更新3:解决了MidiStream稳定性问题。 事实证明,我对MIDI驱动程序api的工作方式有误解,而且文档记录得不够好,所以我没有太多帮助。 它一直有效,直到我对其进行“优化”以减少非托管堆碎片的程度,但出于某种原因却无法进行优化。 事实证明,这与我认为的内存无关。 无论如何,我已经解决了。 获取此更新,尤其是在您的应用随机崩溃的情况下。

Update 4: Finally added MidiSequence.AddAbsoluteEvent() which is an optimized way to add a single absolutely positioned MidiEvent to a MidiSequence without having to resort to Merge() which is a bit more complicated and less efficient. This is explored more in techniques.

更新4:最后添加了MidiSequence.AddAbsoluteEvent() ,这是一种将绝对位置的MidiEvent添加到MidiSequence的优化方法,而不必求助于Merge() ,因为Merge()有点复杂且效率较低。 这在技术上有更多探讨。

Update 5: Not directly an update to this particular article, but I've published a related article here on how I get some of the trickier P/Invoke in this library to work. It covers some of the the low level internals behind MidiStream in particular.

更新5:不是直接更新此特定文章,而是在此处发布了相关文章 ,内容涉及如何使该库中更复杂的P / Invoke起作用。 它特别涵盖了MidiStream背后的一些底层内部MidiStream

Update 6: MidiStream now derives from MidiOutputDevice

更新6: MidiStream现在派生自MidiOutputDevice

Update 7: Fixed MidiSequence.ToNoteMap() bug and added MidiUI project which contains the beginnings of user interface controls for MIDI sequencing, including a piano control and a MIDI sequence visualizer control. I am still working on these so they are what I'd consider rough proofs. When I get further along I'll write an article about them.

更新7:修复了MidiSequence.ToNoteMap()错误,并添加了MidiUI项目,该项目包含用于MIDI排序的用户界面控件的开始,包括钢琴控件和MIDI序列可视化控件。 我仍在研究这些内容,因此我认为它们是粗略的证明。 当我走得更远时,我将写一篇有关它们的文章。

概念化这个混乱 (Conceptualizing this Mess)

There are two major parts of this library, though they are completely and seamlessly integrated with one another.

该库有两个主要部分,尽管它们彼此之间完全无缝地集成在一起。

One is the portion dealing with MIDI files and in-memory sequences that provide manipulation and querying.

其中之一是处理MIDI文件和内存序列的部分,提供操作和查询。

The other portion deals with communicating and querying MIDI devices. This is how you read musical keyboard key presses or make sound with a synthesizer (including the wavetable synthesizer built into your computer's sound hardware)

另一部分涉及通信和查询MIDI设备。 这是您阅读音乐键盘按键或使用合成器(包括计算机的声音硬件中内置的波表合成器)发出声音的方式

Once we dive into those, we're going to cover the MIDI protocol because both files and the MIDI device API rely on the MIDI protocol format. The MIDI protocol format is described later in this section, but first, we'll cover the API for representing it.

一旦深入研究这些内容,我们将覆盖MIDI协议,因为文件和MIDI设备API都依赖于MIDI协议格式。 MIDI协议格式将在本节后面介绍,但首先,我们将介绍用于表示它的API。

MIDI API (MIDI API)

协议API (Protocol API)

讯息API (Message API)

The protocol consists primarily of MIDI messages which represent the various actions like adjusting a knob or striking a note on a keyboard. The API for the MIDI messages is relatively straightforward. It is a series of MidiMessage derivatives that closely mirror the underlying protocol, plus provide higher level representations of each action such as MidiMessageNoteOn/MidiMessageNoteOff to signify a note strike and release, and MidiMessageCC to signify a control change, such as a knob tweak.

该协议主要由MIDI 消息组成,这些消息代表各种操作,例如调节旋钮或敲击键盘上的音符。 MIDI消息的API相对简单。 它是一系列MidiMessage派生类,它们紧密地反映了底层协议,并且提供了每个动作的高级表示形式,例如MidiMessageNoteOn / MidiMessageNoteOff表示音符发出和释放, MidiMessageCC表示控件更改(例如旋钮调整)。

Since for almost all messages each type of message is a specific length each MidiMessage further derives from MidiMessageByte for a message with a single byte payload or MidiMessageWord for a message with a double byte payload which provide raw byte level access to the data in the message. Finally, these are derived by the final high level midi message that represents the message like MidiMessageNoteOn which derives from MidiMessageWord because it requires two bytes to represent it.

由于对几乎所有邮件的每个类型的消息的是一个特定的长度的每个MidiMessage从进一步导出MidiMessageByte用于与单个字节有效负载或消息MidiMessageWord用于与双字节的有效负载,其提供在所述消息中的数据的原始字节级访问的消息。 最后,它们是由最终的高级midi消息派生的,该消息代表像MidiMessageNoteOn这样的消息,该消息派生自MidiMessageWord因为它需要两个字节来表示它。

It's recommended to use the high level members like Note and Velocity on MidiMessageNoteOn to adjust the data even though it's also available through Data1 and Data2 inherited from MidiMessageWord. Each high level message has high level members that represent the specific parameters for the message as just described for MidiMessageNoteOn.

即使通过MidiMessageWord继承的Data1Data2也可以使用数据,也建议使用MidiMessageNoteOn上的NoteVelocity等高级成员来调整数据。 每个高级消息都有高级成员,这些成员代表如MidiMessageNoteOn所述的消息的特定参数。

While most messages have a fixed size payload of either zero, one, or two bytes, there are two exceptions. The first is MIDI system-exclusive messages a.k.a. sysex messages which pass device specific information to or from a MIDI output device or a MIDI input device respectively. These are represented by MidiMessageSysex which has a variable length payload represented by Data.

虽然大多数消息的固定大小的有效载荷为零,一个或两个字节,但是有两个例外。 第一个是MIDI系统专有消息,也称为sysex消息,它们分别将特定于设备的信息传递到MIDI输出设备或MIDI输入设备或从其输出。 这些由MidiMessageSysex表示,其具有由Data表示的可变长度的有效负载。

The second exception typically only appears in files, and is not sent or received over the wire from devices. These are called MIDI meta messages and provide things like tempo changes or copyright information. Despite only occurring in files, the device API wrapper will accept certain meta events like tempo changes but these are never sent to the output device as MIDI messages nor will they be received from a device. This is provided by the wrapper code itself to make it easier to read from a file directly to a device but is provided as a convenience. Basically what happens is whenever the MIDI stream wrapper finds one of these messages, it adjusts its internal tempo. These meta messages are represented by derivatives of MidiMessageMeta like MidiMessageMetaTempo which signifies a tempo change. The type of a MidiMessageMeta message is represented by Type which represents the kind of meta message and comprises the first part of the payload and the remainder of the payload is represented by Data.

第二个例外通常仅出现在文件中,而不会通过有线方式从设备发送或接收。 这些被称为MIDI元消息,并提供诸如速度变化或版权信息之类的信息。 尽管仅在文件中发生,设备API包装程序仍会接受某些元事件,例如速度变化,但这些事件不会作为MIDI消息发送到输出设备,也不会从设备接收。 包装程序代码本身提供了此功能,以使从文件直接读取到设备更容易,但为方便起见。 基本上,当MIDI流包装器发现这些消息之一时,它会调整其内部速度。 这些元消息由MidiMessageMeta的派生词表示,例如MidiMessageMetaTempo ,它表示速度变化。 MidiMessageMeta消息的TypeType表示, Type表示元消息的类型,并且包括有效负载的第一部分,而有效负载的其余部分由Data表示。

The other part of the protocol which is used in files and for queuing up messages for timed playback consists of events which are simply MIDI messages as above but also with a timestamp delta associated with them. The timestamp delta is the number of MIDI ticks since the previous message. The duration of a MIDI tick is based on the timebase (resolution) and the tempo of a sequence or queued event set. A series of events represents a particular score that is suitable for storing in a file or for queued playback. A MidiEvent represents a MIDI event which consists of Position that represents the timestamp delta in ticks and a Message which contains the associated MIDI message. While events almost always contain a timestamp delta, getting the AbsoluteEvents from MidiSequence (see below) will fill Position with the absolute position of the message within the sequence, in ticks.

协议的另一部分用在文件中,用于排队消息以便定时播放,包括事件 ,这些事件只是上述的MIDI消息,而且还具有与之相关的时间戳增量。 时间戳增量是自上一条消息以来的MIDI节拍数。 MIDI滴答声的持续时间基于时基(分辨率)和序列或排队事件集的速度。 一系列事件表示适合存储在文件中或适合排队播放的特定乐谱。 甲MidiEvent表示由一个MIDI事件Position ,表示在蜱时间戳差值和一个Message ,其包含相关MIDI消息。 虽然事件几乎总是包含时间戳记增量,但从MidiSequence获取AbsoluteEvents (请参见下文)将用序列中消息的绝对位置(以刻度为单位)填充Position

Finally, there is the MidiContext class which makes it easy to track the current position in the score, and the state of all CC knobs, and notes in a message playback stream. Basically, it holds the state of all note velocities and CC values, plus pitch wheel, current tempo, current song position, and other information. You feed it MidiMessage messages and/or MidiEvent events as you go along and it handles all of the tracking. You can then query it for the state of any aspect of the playback.

最后,还有MidiContext类,可以轻松跟踪乐谱中的当前位置以及所有CC旋钮和消息回放流中的注释的状态。 基本上,它保存所有音符速度和CC值的状态,以及音高转盘,当前速度,当前乐曲位置和其他信息。 您可以MidiMessage向其提供MidiMessage消息和/或MidiEvent事件,它可以处理所有跟踪。 然后,您可以向其查询播放任何方面的状态。

文件和序列API (File and Sequence API)

The core of the MIDI API and the basis for most of the functionality is MidiSequence, which simply contains an in-memory series of MIDI events represented by MidiEvent and various members for querying and manipulating the events. Everything operates in ticks.

MIDI API的核心和大多数功能的基础是MidiSequence ,它仅包含一系列由MidiEvent表示的内存中MIDI事件以及用于查询和操纵事件的各种成员。 一切都在滴答作响。

The Events list is your primary access to modifying a sequence event by event. It uses timestamp deltas in MidiEvent instances to represent the events. There is also a read only enumeration called AbsoluteEvents which yields MidiEvent objects with the Position set to the absolute position in the sequence, in ticks, which makes it easier sometimes to operate on. Currently, you cannot modify this enumeration but it may be a modifiable list in a future version.

Events列表是您按事件修改序列事件的主要途径。 它在MidiEvent实例中使用时间戳增量来表示事件。 还有一个名为AbsoluteEvents的只读枚举,它会产生MidiEvent对象,并将Position设置为序列中的绝对位置(以滴答为单位),这有时使操作变得更容易。 当前,您无法修改此枚举,但它可能是将来版本中的可修改列表。

The members like Lyrics, Tempos and Copyright are retrieved by scanning the sequence for the appropriate MidiMessageMeta derived messages. Currently, in order to change these, you'll have to add and remove meta messages in the sequence yourself, as these properties are read-only. This may change in a future release.

通过扫描序列以查找适当的MidiMessageMeta派生消息来检索诸如LyricsTemposCopyright类的成员。 当前,为了更改这些属性,您必须自己按顺序添加和删除元消息,因为这些属性是只读的。 这可能会在将来的版本中更改。

There are some high level queries like FirstDownBeat and FirstNoteOn which fetch the location of their respective targets.

有一些高级查询(如FirstDownBeatFirstNoteOn可获取其各自目标的位置。

Using MIDI note on/note off messages is perfect for real time performance but leaves something to be desired when it comes to higher level analysis of sequences and scores. It's often better to understand a note as something with an absolute position, a velocity and a length. MidiSequence provides the ToNoteMap() method which retrieves a list of MidiNote instances representing the notes in a sequence, complete with lengths, rather than the note on/note off paradigm. It also provides the static FromNoteMap() method which gets a sequence from a note list of MidiNote objects. This can make it easier to both create and analyze scores.

使用MIDI音符打开/音符关闭消息非常适合实时演奏,但是在进行更高级别的音序和乐谱分析时,还有一些需要改进的地方。 通常最好将音符理解为具有绝对位置,速度和长度的东西。 MidiSequence提供了ToNoteMap()方法,该方法检索表示序列中音符的MidiNote实例的列表,并带有长度,而不是音符打开/音符关闭范例。 它还提供了静态的FromNoteMap()方法,该方法从MidiNote对象的注释列表中获取序列。 这样可以更轻松地创建和分析分数。

There are also methods like AdjustTempo(), Stretch(), Resample(), GetRange(), Merge() which each return a new sequence with the indicated operation applied to it. Merge() in particular is a versatile method that allows you to combine queries across multiple sequences by merging them, or doing things like merging for playback.

还有一些方法,如AdjustTempo()Stretch()Resample()GetRange()Merge() ,每个方法都返回一个新序列,并对其应用了指示的操作。 特别是Merge()是一种通用的方法,它允许您通过合并多个序列或执行诸如合并播放之类的操作来组合多个序列中的查询。

Preview() will play the sequence on the calling thread using the optionally indicated MidiOutputDevice. It can optionally loop, but it's recommended to do this on a separate thread that you can abort as there is no way to exit the loop. This method does not stream. Instead, it sends each message immediately to the hardware. This is CPU intensive. There is a better method for playing a sequence by streaming it to the hardware, which can be done asynchronously. This is covered in the techniques section.

Preview()将使用可选的MidiOutputDevice在调用线程上播放序列。 它可以选择循环,但是建议您在一个单独的线程上执行此操作,因为无法退出循环,因此您可以中止该线程。 此方法不流式传输。 相反,它将立即将每个消息发送到硬件。 这是CPU密集型的。 有一种更好的方法可以通过将序列流传输到硬件来播放序列,该方法可以异步完成。 这将在“技术”部分中介绍。

MidiFile represents an in-memory MIDI file. A MIDI file contains multiple tracks, each represented by a MidiSequence. The first track typically - at least for "type 1" MIDI files - contains only meta messages including a tempo map, without performance messages. The API assumes this so when you're querying things like Tempo it will look on the first track.

MidiFile代表内存中的MIDI文件。 MIDI文件包含多个轨道,每个轨道由MidiSequence表示。 通常,第一轨道(至少对于“类型1” MIDI文件而言)仅包含包括速度映射的元消息,而没有演奏消息。 该API假定这样做,因此当您查询诸如Tempo之类的内容时,它将显示在第一条轨道上。

MidiFile contains many of the same members as MidiSequence which either operate on the first track or all tracks, depending on what makes sense for the operation. You can always modify each individual track itself, but remember to assign the sequence you modified back to the Tracks list at that index, because modifications to sequences always return a copy of the sequence - they don't modify the sequence itself. Any methods that modify a MIDI file like Stretch() will return a new MIDI file, similar to how MidiSequence works.

MidiFile包含许多与MidiSequence相同的成员,这些成员可以在第一个轨道上运行,也可以在所有轨道上运行,具体取决于对该操作有意义的成员。 您始终可以修改每个单独的音轨本身,但是请记住将修改后的序列分配回该索引处的“ Tracks列表,因为对序列的修改始终会返回该序列的副本-它们不会修改序列本身。 任何修改MIDI文件(如Stretch()都将返回一个新的MIDI文件,类似于MidiSequence工作方式。

MidiFile also contains ReadFrom() and WriteTo() which can read a MIDI file from a Stream or a file. Naturally, this is how you turn an in-memory representation into an actual MIDI file, or turn a MIDI file into an in-memory MidiFile. In case it isn't clear, all operations on a MidiFile operate in-memory. The process for modifying a file involves reading it, modifying it, and then writing the new, modified file to disk over the old one.

MidiFile还包含ReadFrom()WriteTo() ,它们可以从Stream或文件中读取MIDI文件。 自然地,这就是将内存表示形式转换为实际MIDI文件或将MIDI文件转换为内存MidiFile 。 如果不清楚,则对MidiFile所有操作都在内存中进行。 修改文件的过程包括读取,修改文件,然后将经过修改的新文件覆盖到旧文件上。

The support classes include MidiTimeSignature which represents a time signature, MidiKeySignature which represents a key signature, MidiNote, which we'll cover below, and MidiUtility which you shouldn't need that much.

支持类包括MidiTimeSignature其表示时间签名, MidiKeySignature这是一个关键的签名, MidiNote ,我们将在下面覆盖,并MidiUtility你应该不需要那么多。

设备API (Device API)

Note: All events are potentially called from a different thread.

注意 :可能从其他线程调用所有事件。

The device API consists primarily of MidiDevice and its derivatives, MidiOutputDevice, and MidiInputDevice which are used for communicating with MIDI devices, plus MidiStream which is used for high performance asynchronous output streaming.

设备API主要包括用于与MIDI设备通信的MidiDevice及其派生产品MidiOutputDeviceMidiInputDevice ,以及用于高性能异步输出流的MidiStream

You can enumerate each of the above off of MidiDevice's Inputs, Outputs and Streams members, but usually you'll get the stream off of MidiOutputDevice's Stream member. Each time you enumerate them the system is requeried, so you can get these lists every time you want a fresh device list, but don't query it more than you need to, obviously.

您可以枚举MidiDeviceInputsOutputsStreams成员中的每一个,但是通常您可以从MidiOutputDeviceStream成员中获得流。 每次枚举它们时,系统都会重新查询一次,因此,每当您想要一个新的设备列表时就可以获取这些列表,但是显然,查询的次数不会超过您的需要。

MidiOutputDevice includes several members for communicating with an open device. Normally, the process is to Open() it, and then begin using Send() to send messages, before finally calling Close() to close it. Reset() is kind of a panic method that sends note off messages to all channels so it basically clears all playing notes. You can also get or set the volume using the Volume property if it's supported, which takes/reports a left and right volume through MidiVolume. The object is disposable, so it will close when disposed.

MidiOutputDevice包含几个用于与打开的设备进行通信的成员。 通常,该过程是对其进行Open() ,然后开始使用Send()发送消息,然后最终调用Close()将其关闭。 Reset()是一种紧急方法,可以将音符关闭消息发送到所有通道,因此基本上可以清除所有正在播放的音符。 如果支持,您还可以使用Volume属性获取或设置音量,该属性通过MidiVolume获取/报告左右音量。 该物体是一次性的,因此将其丢弃。

MidiStream provides a more efficient way to communicate with a device that can accept several queued MIDI events and play them in the background, although it also supports sending messages immediately. Using it is similar to using MidiOutputDevice except it also must be started using Start() before the queued events will start playing, since once it's opened with Open() it starts out paused. You probably want to set the TimeBase and possibly the Tempo or MicroTempo as well.

MidiStream提供了一种与设备进行通信的更有效方式,该设备可以接受多个排队的MIDI事件并在后台播放它们,尽管它也支持立即发送消息。 使用它与使用MidiOutputDevice相似,不同之处MidiOutputDevice ,还必须使用Start()在排队的事件开始播放之前启动它,因为一旦使用Open()它,它就会开始暂停。 您可能要设置TimeBase ,还可能要设置TempoMicroTempo

If you call Send() with a MidiMessage, the message will be sent immediately to the output. If you call Send() with one or more MidiEvent objects, they will be queued for playback. Unless you're firing and forgetting once you'll need to handle the SendComplete event which will tell you when the queued events have been played. Note that you cannot queue more events until all events have been played. Send accepts tempo change messages and will respect track end messages. Other meta messages are discarded. In the techniques section, it is shown how to stream a file or sequence.

如果使用MidiMessage调用Send() ,则消息将立即发送到输出。 如果使用一个或多个MidiEvent对象调用Send() ,则它们将排队等待播放。 除非您一次激发并忘记,否则您将需要处理SendComplete事件,该事件将告诉您何时播放排队的事件。 请注意,在播放所有事件之前,您不能将更多事件排队。 发送接受速度更改消息,并将尊重曲目结束消息。 其他元消息将被丢弃。 在“技术”部分中,显示了如何流式传输文件或序列。

MidiInputDevice includes members for capturing MIDI input. What you do is you hook the relevant events including Input, Open() the device, Start() the device to begin capturing. You can easily record MIDI performances to a file using StartRecording() and EndRecording(). Each time you get a valid message, Input is fired with arguments that tell you the message and the number of milliseconds elapsed since Start() was called. There is also Error, Opened, and Closed. Error is fired if an invalid or malformed message is received.

MidiInputDevice包含用于捕获MIDI输入的成员。 您要做的是挂钩相关事件,包括InputOpen()设备, Start()设备以开始捕获。 您可以轻松地记录使用MIDI表演到文件StartRecording()EndRecording() 每次您收到一条有效消息时, Input就会触发带有参数的参数,这些参数会告诉您该消息以及自调用Start()以来经过的毫秒数。 还有ErrorOpenedClosedError如果接收到无效或畸形消息被触发。

协议格式 (Protocol Format)

讯息格式 (Message Format)

The following guide is presented as a tutorial on the MIDI protocol format, but it's not necessary to be completely familiar with it in order to use this library. All of the MIDI protocol features are wrapped by the API.

以下指南作为MIDI协议格式的教程提供,但不必完全熟悉它即可使用此库。 API封装了所有MIDI协议功能。

MIDI works using "messages" which tell an instrument what to do. MIDI messages are divided into two types: channel messages and system messages. Channel messages make up the bulk of the data stream and carry performance information, while system messages control global/ambient settings.

MIDI使用“消息”工作,该消息告诉乐器该做什么。 MIDI信息分为两种: 通道信息系统信息 。 通道消息构成了数据流的大部分,并承载性能信息,而系统消息则控制全局/环境设置。

A channel message is called a channel message because it is targeted to a particular channel. Each channel can control its own instrument and up to 16 channels are available, with channel #10 (zero based index 9) being a special channel that always carries percussion information, and the other channels being mapped to arbitrary devices. This means the MIDI protocol is capable of communicating with up to 16 individual devices at once.

频道消息之所以称为频道消息,是因为它针对特定的频道。 每个通道可以控制自己的乐器,最多可以使用16个通道,其中#10通道(从零开始的索引9)是一个特殊通道,始终携带打击乐器信息,而其他通道则映射到任意设备。 这意味着MIDI协议能够一次与多达16个单独的设备进行通信。

A system message is called a system message because it controls global/ambient settings that apply to all channels. One example is sending proprietary information to a particular piece of hardware, which is done through a "system exclusive" or "sysex" message. Another example is the special information included in MIDI files (but not present in the wire protocol) such as the tempo to play the file back at. Another example of a system message is a "system realtime message" which allows access to the transport features (play, stop, continue and setting the timing for transport devices)

系统消息之所以称为系统消息,是因为它控制适用于所有通道的全局/环境设置。 一个示例是将专有信息发送到特定的硬件,这是通过“系统独占”或“ sysex”消息完成的。 另一个示例是MIDI文件中包含的特殊信息(但在有线协议中不存在),例如播放文件的速度。 系统消息的另一个示例是“系统实时消息”,它允许访问传输功能(播放,停止,继续并设置传输设备的时间)

Each MIDI message has a "status byte" associated with it. This is usually** the first byte in a MIDI message. The status byte contains the message id in the high nibble (4-bits) and the target channel in the low nibble. Ergo, the status byte 0xC5 indicates a channel message type of 0xC and a target channel of 0x5. The high nibble must be 0x8 or greater for reasons. If the high nibble is 0xF, this is a system message, and the entire status byte is the message id since there is no channel. For example, 0xFF is a message id for a MIDI "meta event" message that can be found in MIDI files. Once again, the low nibble is part of the status if the high nibble is 0xF.

每个MIDI消息都有一个与之关联的“状态字节”。 通常,这是MIDI消息中的第一个字节。 状态字节在高半字节(4位)中包含消息ID,在低半字节中包含目标通道。 因此,状态字节0xC5表示通道消息类型为0xC,目标通道为0x5。 由于某些原因,高半字节必须为0x8或更大。 如果高半字节为0xF,则这是系统消息,并且由于没有通道,因此整个状态字节是消息ID。 例如,0xFF是在MIDI文件中可以找到的MIDI“元事件”消息的消息ID。 同样,如果高半字节为0xF,则低半字节是状态的一部分。

** due to an optimization of the protocol, it is possible that the status byte is omitted in which case the status byte from the previous message is used. This allows for "runs" of messages with the same status but different parameters to be sent without repeating the redundant byte for each message.

**由于协议的优化,可能会省略状态字节,在这种情况下,将使用上一条消息中的状态字节。 这允许发送具有相同状态但参数不同的消息“运行”,而无需为每个消息重复冗余字节。

The following channel messages are available:

以下频道消息可用:

  • 0x8 Note Off - Releases the specified note. The velocity is included in this message but not used. All notes with the specified note id are released, so if there are two Note Ons followed by one Note Off for C#4 all of the C#4 notes on that channel are released. This message is 3 bytes in length, including the status byte. The 2nd byte is the note id (0-0x7F/127), and the 3rd is the velocity (0-0x7F/127). The velocity is virtually never respected for a note off message. I'm not sure why it exists. Nothing I've ever encountered uses it. It's usually set to zero, or perhaps the same note velocity for the corresponding note on. It really doesn't matter.

    0x8 Note Off -释放指定的音符。 速度包含在此消息中,但未使用。 释放具有指定音符ID的所有音符,因此如果C#4有两个“音符打开”后跟一个“音符关闭”,则该通道上的所有C#4音符都会被释放。 该消息的长度为3个字节,包括状态字节。 第2 字节是音符ID(0-0x7F / 127),和第三是速度(0-0x7F / 127)。 对于音符消息,速度实际上从未被尊重。 我不确定为什么存在。 我从未遇到过使用它的东西。 通常设置为零,或者相应音符的音符速度可能相同。 真的没关系。

  • 0x9 Note On - Strikes and holds the specified note until a corresponding note off message is found. This message is 3 bytes in length, including the status byte. The parameters are the same as note off.

    0x9 Note On -敲击并0x9 Note On指定的音符,直到找到相应的音符关闭消息为止。 该消息的长度为3个字节,包括状态字节。 参数与注释掉相同。

  • 0xA Key Pressure/Aftertouch - Indicates the pressure that the key is being held down at. This is usually for higher end keyboards that support it, to give an after effect when a note is held depending on the pressure it is held at. This message is 3 bytes in length, including the status byte. The 2nd byte is the note id (0-0x7F/127) while the 3rd is the pressure (0-0x7F/127)

    0xA Key Pressure/Aftertouch感-表示按住按键的压力。 这通常用于支持它的高端键盘,当按住音符时,根据其所受的压力会产生后效。 该消息的长度为3个字节,包括状态字节。 第2 字节是音符ID(0-0x7F / 127),而第三是压力(0-0x7F / 127)

  • 0xB Control Change - Indicates that a controller value is to be changed to the specified value. Controllers are different for different instruments, but there are standard control codes for common controls like panning. This message is 3 bytes in length, including the status byte. The 2nd byte is the control id. There are common ids like panning (0x0A/10) and volume (7) and many that are just custom, often hardware specific or customizably mapped in your hardware to different parameters. There's a table of standard and available custom codes here. The 3rd byte is the value (0-0x7F/127) whose meaning depends heavily on what the 2nd byte is.

    0xB Control Change -指示控制器值将更改为指定值。 对于不同的乐器,控制器有所不同,但是对于诸如摇摄之类的常见控件,都有标准的控制代码。 该消息的长度为3个字节,包括状态字节。 第二个字节是控件ID。 有常见的ID,例如平移(0x0A / 10)和卷(7),并且许多ID是自定义的,通常是特定于硬件的,或者是可自定义地映射到硬件中的不同参数。 有标准和可用的自定义码表在这里 。 第三字节的值(0-0x7F / 127),其含义很大程度上取决于第二个字节是什么。

  • 0xC Patch/Program Change - Some devices have multiple different "programs" or settings that produce different sounds. For example, your synthesizer may have a program to emulate an electric piano and one to emulate a string ensemble. This message allows you to set which sound is to be played by the device. This message is 2 bytes long, including the status byte. The 2nd byte is the patch/program id (0-0x7F/127)

    0xC Patch/Program Change -某些设备具有多个不同的“程序”或设置,它们会产生不同的声音。 例如,您的合成器可能有一个模拟电子钢琴的程序和一个模拟弦乐合奏的程序。 此消息使您可以设置设备要播放的声音。 该消息的长度为2个字节,包括状态字节。 第二个字节是补丁/程序ID(0-0x7F / 127)

  • 0xD Channel Pressure/Non-Polyphonic Aftertouch - This is similar to the aftertouch message, but is geared for less sophisticated instruments that don't support polyphonic aftertouch. It affects the entire channel instead of an individual key, so it affects all playing notes. It is specified as the single greatest aftertouch value for all depressed keys. This message is 2 bytes long, including the status byte. The 2nd byte is the pressure (0x7F/127)

    0xD Channel Pressure/Non-Polyphonic Aftertouch -与触后信息类似,但适用于不支持复音后触的较复杂的乐器。 它会影响整个通道,而不是单个键,因此会影响所有弹奏音符。 它被指定为所有按下键的最大最大触后值。 该消息的长度为2个字节,包括状态字节。 第二个字节是压力(0x7F / 127)

  • 0xE Pitch Wheel Change - This indicates that the pitch wheel has moved to a new position. This generally applies an overall pitch modifier to all notes in the channel such that as the wheel is moved upward, the pitch for all playing notes is increased accordingly, and the opposite goes for moving the wheel downward. This message is 3 bytes long, including the status byte. The 2nd and 3rd byte contain the least significant 7 bits (0-0x7F/127) and the most significant 7 bits respectively, yielding a 14-bit value.

    0xE Pitch Wheel Change -表示变桨轮已移至新位置。 通常,这将总体音高修改器应用于通道中的所有音符,使得当滚轮向上移动时,所有弹奏音符的音高会相应增加,而将滚轮向下移动则相反。 该消息为3个字节长,包括状态字节。 第2 和第3 字节包含至少显著7位(0-0x7F / 127)和所述最显著7位分别,产生一个14位的值。

The following system messages are available (non-exhaustive):

以下系统消息可用(非详尽无遗):

  • 0xF0 System Exclusive - This indicates a device specific data stream is to be sent to the MIDI output port. The length of the message varies and is bookended by the End of System Exclusive message. I'm not clear on how this is transmitted just yet, but it's different in the file format than it is over the wire, which makes it one-off. In the file, the length immediately follows the status byte and is encoded as a "variable length quantity" which is covered in a bit. Finally, the data of the specified byte length follows that.

    0xF0 System Exclusive这表示设备特定的数据流将发送到MIDI输出端口。 消息的长度各不相同,并且由“系统独占结束”消息来预定。 目前尚不清楚如何传输,但是文件格式与在线传输方式不同,这使其一次完成。 在文件中,长度紧跟在状态字节之后,并被编码为“可变长度量”,该长度以位表示。 最后,随后是指定字节长度的数据。

  • 0xF7 End of System Exclusive - This indicates an end marker for a system exclusive message stream

    0xF7 End of System Exclusive -指示系统独占消息流的结束标记

  • 0xFF Meta Message - This is defined in MIDI files, but not in the wire-protocol. It indicates special data specific to files such as the tempo the file should be played at, plus additional information about the scores, like the name of the sequence, the names of the individual tracks, copyright notices, and even lyrics. These may be an arbitrary length. What follows the status byte is a byte indicating the "type" of the meta message, and then a "variable length quantity" that indicates the length, once again, followed by the data.

    0xFF Meta Message -这是在MIDI文件中定义的,但不在有线协议中定义。 它指示特定于文件的特殊数据,例如应以文件的速度播放,以及有关乐谱的其他信息,例如音序名称,单个曲目的名称,版权声明,甚至是歌词。 这些可以是任意长度。 状态字节之后是指示元消息的“类型”的字节,然后是指示长度的“可变长度数量”,该长度再一次指示数据。

Here's a sample of what messages look like over the wire.

这是一条在线消息的示例。

Note on, middle C, maximum velocity on channel 0:

注意,中间的C,通道0上的最大速度:

90 3C 7F

Patch change to 1 on channel 2:

在通道2上将补丁更改为1:

C2 01

Remember, the status byte can be omitted. Here's some note on messages to channel 0 in a run:

请记住,状态字节可以省略。 这是有关运行中发送至频道0的消息的一些注意事项:

90 3C 7F 3F 7F 42

That yields a C major chord at middle C. Each of the two messages with the status byte omitted are using the previous status byte, 0x90.

这会在中间C处产生C大调和弦。忽略状态字节的两条消息中的每条消息都使用前一个状态字节0x90。

MIDI文件格式 (The MIDI File Format)

Once you understand the MIDI wire-protocol, the file format is fairly straightforward as about 80% or more of an average MIDI file is simply MIDI messages with a timestamp on them.

一旦您了解了MIDI线协议,文件格式就非常简单了,因为平均MIDI文件的80%或更多是带有时间戳的MIDI消息。

MIDI files typically have a ".mid" extension, and like the wire-protocol it is a big-endian format. A MIDI file is laid out in "chunks." A "chunk" meanwhile, is a FourCC code (simply a 4 byte code in ASCII) which indicates the chunk type followed by a 4-byte integer value that indicates the length of the chunk, and then followed by a stream of bytes of the indicated length. The FourCC for the first chunk in the file is always "MThd". The FourCC for the only other relevant chunk type is "MTrk". All other chunk types are proprietary and should be ignored unless they are understood. The chunks are laid out sequentially, back to back in the file.

MIDI文件通常具有“ .mid ”扩展名,并且像有线协议一样,它是大尾数格式。 MIDI文件以“块”布局。 同时,“块”是FourCC代码(在ASCII中为4字节代码),它指示块的类型,其后是指示字节长度的4字节整数值,然后是字节的字节流。指示的长度。 文件中第一个块的FourCC始终为“ MThd”。 唯一其他相关块类型的FourCC是“ MTrk”。 所有其他块类型都是专有的,除非被理解,否则应将其忽略。 块顺序排列,并在文件中背对背。

The first chunk, "MThd" always has its length field set to 6 bytes. The data that follows it are 3 2-byte integers. The first indicates the MIDI file type which is almost always 1 but simple files can be type 0, and there's a specialized type - type 2 - which stores patterns. The second number is the count of "tracks" in a file. A MIDI file can contain more than one track, with each track containing its own score. The third number is the "timebase" of a MIDI file (often 480) which indicates the number of MIDI "ticks" per quarter note. How much time a tick represents depends on the current tempo.

第一个块“ MThd”始终将其长度字段设置为6个字节。 紧随其后的数据是3个2字节整数。 第一个表示MIDI文件类型,几乎总是1,但是简单文件可以是0,还有一个专门的类型-类型2-存储模式。 第二个数字是文件中“轨道”的计数。 MIDI文件可以包含多个音轨,每个音轨都包含自己的乐谱。 第三个数字是MIDI文件的“时基”(通常为480),表示每个四分音符的MIDI“滴答”数。 滴答声代表多少时间取决于当前速度。

The following chunks are "MTrk" chunks or proprietary chunks. We skip proprietary chunks, and read each "MTrk" chunk we find. An "MTrk" chunk represents a single MIDI file track (explained below) - which is essentially just MIDI messages with timestamps attached to them. A MIDI message with a timestamp on it is known as a MIDI "event." Timestamps are specified in deltas, with each timestamp being the number of ticks since the last timestamp. These are encoded in a funny way in the file. It's a byproduct of the 1980s and the limited disk space and memory at the time, especially on hardware sequencers - every byte saved was important. The deltas are encoded using a "variable length quantity".

以下块是“ MTrk”块或专有块。 我们跳过专有块,并读取找到的每个“ MTrk”块。 “ MTrk”块代表单个MIDI文件轨道(如下所述)-本质上只是带有时间戳的MIDI消息。 带有时间戳的MIDI消息称为MIDI“事件”。 时间戳以增量形式指定,每个时间戳是自上一个时间戳以来的滴答数。 这些文件以有趣的方式编码在文件中。 它是1980年代的副产品,当时的磁盘空间和内存有限,尤其是在硬件定序器上-保存的每个字节都很重要。 使用“可变长度量”对增量进行编码。

Variable length quantities are encoded as follows: They are 7 bits per byte, most significant bits first (little endian!). Each byte is high (greater than 0x7F) except the last one which must be less than 0x80. If the value is between 0 and 127, it is represented by one byte while if it was greater it would take more. Variable length quantities can in theory be any size, but in practice they must be no greater than 0xFFFFFFF - about 3.5 bytes. You can hold them with an int, but reading and writing them can be annoying.

可变长度量的编码方式如下:它们是每个字节7位,最高有效位在前(小尾数!)。 每个字节均为高字节(大于0x7F),但最后一个字节必须小于0x80。 如果该值介于0到127之间,则以一个字节表示,如果该值较大,则将花费更多。 可变长度量理论上可以是任何大小,但实际上,它们的长度不得大于0xFFFFFFF-约3.5个字节。 您可以将它们保留为整数,但是阅读和编写它们可能会很烦人。

What follows a variable length quantity delta is a MIDI message, which is at least one byte, but it will be different lengths depending on the type of message it is and some message types (meta messages and sysex messages) are variable length. It may be written without the status byte in which case the previous status byte is used. You can tell if a byte in the stream is a status byte because it will be greater than 0x7F (127) while all of the message payload will be bytes less than 0x80 (128). It's not as hard to read as it sounds. Basically for each message, you check if the byte you're on is high (> 0x7F/127) and if it is, that's your new running status byte, and the status byte for the message. If it's low, you simply consult the current status byte instead of setting it.

可变长度数量增量之后是MIDI消息,该消息至少为一个字节,但是长度将有所不同,具体取决于消息的类型,某些消息类型(元消息和sysex消息)的长度是可变的。 可以在没有状态字节的情况下写入它,在这种情况下,将使用前一个状态字节。 您可以判断流中的一个字节是否为状态字节,因为它将大于0x7F(127),而所有消息有效负载都将小于0x80(128)。 它并不像听起来那样难读。 基本上,对于每条消息,您都要检查所占用的字节是否为高字节(> 0x7F / 127),如果是,则为新的运行状态字节,以及消息的状态字节。 如果它很低,则只需查询当前状态字节而不是对其进行设置。

MIDI文件轨道 (MIDI File Tracks)

A MIDI type 1 file will usually contain multiple "tracks" (briefly mentioned above). A track usually represents a single score and multiple tracks together make up the entire performance. While this is usually laid out this way, it's actually channels, not tracks that indicate what score a particular device is to play. That is, all notes for channel 0 will be treated as part of the same score even if they are scattered throughout different tracks. Tracks are just a helpful way to organize. They don't really change the behavior of the MIDI at all. In a MIDI type 1 file - the most common type - track 0 is "special". It doesn't generally contain performance messages (channel messages). Instead, it typically contains meta information like the tempo and lyrics, while the rest of your tracks contain performance information. Laying your files out this way ensures maximum compatibility with MIDI devices out there.

MIDI类型1文件通常将包含多个“音轨”(上面已简要提及)。 一个曲目通常代表一个乐谱,而多个曲目共同构成整个演奏。 尽管通常以这种方式进行布局,但实际上是频道,而不是指示特定设备要播放的乐谱的音轨。 也就是说,通道0的所有音符即使分散在不同的音轨中也将被视为同一乐谱的一部分。 曲目只是一种有用的组织方式。 它们根本没有真正改变MIDI的行为。 在MIDI 1型文件中-最常见的类型-音轨0为“特殊”。 它通常不包含性能消息(通道消息)。 取而代之的是,它通常包含速度和歌词之类的元信息,而其他曲目则包含性能信息。 这样布置文件可确保与MIDI设备的最大兼容性。

Very important: A track must always end with the MIDI End of Track meta message.

非常重要 :轨道必须始终以MIDI轨道结束元消息结束。

Despite tracks being conceptually separate, the separation of scores is actually by channel under the covers, not by track, meaning you can have multiple tracks which when combined, represent the score for a device at a particular channel (or more than one channel). You can combine channels and tracks however you wish, just remember that all the channel messages for the same channel represent an actual score for a single device, while the tracks themselves are basically virtual/abstracted convenience items.

尽管轨道在概念上是分开的,但分数的分隔实际上是根据掩护下的渠道而不是轨道进行的,这意味着您可以拥有多个轨道,这些轨道组合起来可以代表特定信道(或多个信道)上设备的分数。 您可以随意组合频道和曲目,只需记住,同一频道的所有频道消息都代表单个设备的实际得分,而曲目本身基本上是虚拟的/抽象的便利项目。

See this page for more information on the MIDI wire-protocol and the MIDI file format .

有关MIDI线协议和MIDI文件格式的更多信息,请参见此页面 。

编码此混乱 (Coding this Mess)

The sample projects contain more or less real world code which puts the library through its paces. Here, we'll cover some basics and then go over techniques.

示例项目包含或多或少的现实世界代码,这使该图书馆得以发展。 在这里,我们将介绍一些基础知识,然后再介绍技术。

Reading and writing a MIDI file to and from disk:

在磁盘上读取和写入MIDI文件:

var file = MidiFile.ReadFrom("sample.mid");// code here modifying file...file.WriteTo("sample.mid");

Modifying a single track in a file:

修改文件中的单个轨道:

// get the 2nd track of the MIDI file
var track = file.Tracks[1];// normalize - remember all
// modifications create a copy
track = track.NormalizeVelocities();// reassign the modified track
file.Tracks[1]=track;

Enumerating MIDI devices (including rich display):

枚举MIDI设备(包括丰富的显示):

Console.WriteLine("Output devices:");
Console.WriteLine();
foreach (var dev in MidiDevice.Outputs)
{var kind = "";switch (dev.Kind){case MidiOutputDeviceKind.MidiPort:kind = "MIDI Port";break;case MidiOutputDeviceKind.Synthesizer:kind = "Synthesizer";break;case MidiOutputDeviceKind.SquareWaveSynthesizer:kind = "Square wave synthesizer";break;case MidiOutputDeviceKind.FMSynthesizer:kind = "FM synthesizer";break;case MidiOutputDeviceKind.WavetableSynthesizer:kind = "Wavetable synthesizer";break;case MidiOutputDeviceKind.SoftwareSynthesizer:kind = "Software synthesizer";break;case MidiOutputDeviceKind.MidiMapper:kind = "MIDI Mapper";break;}Console.WriteLine(dev.Name + " " + dev.Version + " " + kind);
}
Console.WriteLine();
Console.WriteLine();
Console.WriteLine("Input devices:");
Console.WriteLine();
foreach (var dev in MidiDevice.Inputs)
{Console.WriteLine(dev.Name + " " + dev.Version);
}

Opening a device and sending output:

打开设备并发送输出:

// just grab the first output device
using(var dev = MidiDevice.Outputs[0])
{// open the devicedev.Open();// send a C5 major chorddev.Send(new MidiMessageNoteOn("C5", 127, 0));dev.Send(new MidiMessageNoteOn("E5", 127, 0));dev.Send(new MidiMessageNoteOn("G5", 127, 0));Console.Error.WriteLine("Press any key to exit...");Console.ReadKey();// note offsdev.Send(new MidiMessageNoteOff("C5", 127, 0));dev.Send(new MidiMessageNoteOff("E5", 127, 0));dev.Send(new MidiMessageNoteOff("G5", 127, 0));
}

Capturing input:

捕获输入:

// just grab the first input device
using(var dev = MidiDevice.Inputs[0])
{Console.Error.WriteLine("Press any key to exit...");// hook the inputdev.Input += delegate(object s,MidiInputEventArgs ea) {Console.WriteLine(ea.Message);};// open the devicedev.Open();// start capturingdev.Start();// wait for keypressConsole.ReadKey();
}

API的技巧 (Techniques With the API)

终止序列/音轨 (Terminating Sequences/Tracks)

Important: We'll start here, since this is critical. The API will usually automatically terminate sequences for you with the end of track marker when you use operations like Merge(), Concat() or GetRange(), but if you build a sequence from scratch, you will need to insert it at the end manually. While this API will basically work without it, many, if not most MIDI applications will not, so writing a file without them is essentially akin to writing a corrupt file:

重要提示 :我们将从此处开始,因为这很关键。 当您使用Merge()Concat()GetRange()类的操作时,API通常会自动为您终止轨道标记,但是如果您从头开始构建序列,则需要在末端插入手动。 尽管此API基本上可以在没有该API的情况下运行,但即使不是大多数MIDI应用程序也无法运行,因此在没有它们的情况下编写文件本质上类似于编写损坏的文件:

track.Events.Add(new MidiEvent(0,new MidiMessageMetaEndOfTrack()));

You should rarely have to do this, but again, you'll need to if you construct your sequences manually from scratch. Also, 0 will need to be adjusted to your own delta time to get the length of the track right.

您几乎不需要这样做,但是再次,如果您是从头开始手动构建序列,则需要这样做。 同样,需要将0调整为您自己的增量时间,以获得正确的音轨长度。

连续执行序列和文件转换 (Executing Sequence and File Transformations in Series)

This is simple. Every time we do a transformation, it yields a new object so we replace the variable each time with the new result:

这很简单。 每次进行转换时,都会产生一个新对象,因此我们每次都用新结果替换变量:

// assume file (variable) is our MidiFile
// modify track #2 (index 1)
var track = file.Track[1];
track = track.NormalizeVelocities();
track = track.ScaleVelocities(.5);
track = track.Stretch(.5);
// reassign our track
file.Track[1]=track;

The same basic idea works with MidiFile instances, too.

同样的基本思想也适用于MidiFile实例。

一起搜索或分析多个轨道 (Searching or Analyzing Multiple Tracks Together)

Sometimes, you might need to search multiple tracks at once. While MidiFile provides ways to do this for common searches across all tracks in a file, you might need to operate over a list of sequences or some other source. The solution is simple: Temporarily merge your target tracks into a new track and then operate on that. For example, say you want to find the first downbeat wherever it occurs in any of the target tracks:

有时,您可能需要一次搜索多个曲目。 尽管MidiFile提供了针对文件中所有音轨进行通用搜索的方式,但您可能需要对序列列表或某些其他来源进行操作。 解决方案很简单:将目标轨道临时合并到新轨道,然后对其进行操作。 例如,假设您想找到任何目标音轨中出现的第一个低调:

// assume IList<MidiSequence> trks is declared
// and contains the list of tracks to work on
var result = MidiSequence.Merge(trks).FirstDownBeat;

You can do manual searches by looping through events in the merged tracks to. This technique works for pretty much any situation. Merge() is a versatile method and it is your friend.

您可以通过遍历合并轨道中的事件来进行手动搜索。 这项技术几乎适用于任何情况。 Merge()是一种通用方法,它是您的朋友。

插入绝对定时的事件 (Inserting Absolutely Timed Events)

It's often a heck of a lot easier to specify events in absolute time. There are a couple of ways to do it. The first is to do it directly:

在绝对时间内指定事件通常要容易得多。 有两种方法可以做到这一点。 首先是直接做到:

// myTrack represents the already existing sequence
// we want to insert an absolutely timed event into
// while absoluteTicks specifies the position at
// which to insert the message, and msg contains
// the MidiMessage to insert
myTrack.AddAbsoluteEvent(absoluteTicks,msg);

The above directly inserts an event with the message specified at the indicated absolute position. However, a lot of times, you'll need to insert a MidiMessageMetaEndTrack to an already existing track. The problem with using the above is that one of those end track messages is almost certainly already present unless you built it yourself. You'll need to remove it before adding your own. The following technique handles all of that, both inserting the new event and removing the old end track:

上面的代码直接在指定的绝对位置插入带有指定消息的事件。 但是,很多时候,您需要将MidiMessageMetaEndTrack插入到现有轨道中。 使用上述内容的问题在于,除非您自己构建了这些跟踪消息,否则几乎肯定已经存在其中一种。 您需要先删除它,然后再添加自己的。 以下技术可以解决所有这些问题,既可以插入新事件,也可以删除旧的结束轨道:

// myTrack represents the already existing sequence
// we want to insert an absolutely timed event into
// while absoluteTicks specifies the position at
// which to insert the message, and msg contains
// the MidiMessage to insert// create a new MidiSequence and add our absolutely
// timed event as the single event in this sequence
var newTrack = new MidiSequence();
newTrack.Events.Add(new MidiEvent(absoluteTicks, msg));
// now reassign myTrack with the result of merging
// it with newTrack:
myTrack = MidiSequence.Merge(myTrack,newTrack);

First, we create a new sequence and add our absolutely timed message to it. Basically, since it's the only message, the delta is the number of ticks from zero which is the same as an absolute position. Finally, we take our current sequence and reassign it with the result of merging our current sequence with the sequence we just created. All operations return new instances. We don't modify existing instances, so we often find we are reassigning variables like this.

首先,我们创建一个新序列,并向其中添加绝对定时的消息。 基本上,因为它是唯一的消息,所以增量是从零开始的滴答声数量,与绝对位置相同。 最后,我们采用当前序列并将其重新分配,以将当前序列与我们刚刚创建的序列合并。 所有操作均返回新实例。 我们不修改现有实例,因此经常发现我们正在重新分配这样的变量。

创建笔记图 (Creating a Note Map)

An easier way to do the above, at least when dealing with notes, is to use FromNoteMap(). Basically, you just queue up a list of absolutely positioned notes and then call FromNoteMap() to get a sequence from it.

至少在处理笔记时,执行上述操作的一种简便方法是使用FromNoteMap() 。 基本上,您只是将绝对定位的音符列表排队,然后调用FromNoteMap()从中获取序列。

var noteMap = new List<MidiNote>();
// add a C#5 note at position zero, channel 0,
// velocity 127, length 1/8 note @ 480 timebase
noteMap.Add(new MidiNote(0,0,"C#5",127,240));
// add a D#5 note at position 960 (1/2 note in), channel 0,
// velocity 127, length 1/8 note @ 480 timebase
noteMap.Add(new MidiNote(960,0,"D#5",127,240));
// now get a MidiSequence
var seq = MidiSequence.FromNoteMap(noteMap);

You can also get a note map from any sequence by calling ToNoteMap().

您还可以通过调用ToNoteMap()从任何序列中获取笔记图。

循环播放 (Looping)

It can be much easier to specify our loops in beats (1/4 notes at 4/4 time), so we can multiply the number of beats we need by the MidiFile's TimeBase to get our beats, at least for 4/4. I won't cover other time signatures here as that's music theory, and beyond the scope. You'll have to deal with time signatures if you want this technique to be accurate. Anyway, it's also helpful to start looping at the FirstDownBeat or the FirstNote or at least an offset of beats from one of those locations. The difference between them is FirstDownBeat hunts for a bass/kick drum while FirstNote hunts for any note. Once we compute our offset and length, we can pass them to GetRange() in order to get a MidiSequence or MidiFile with only the specified range, optionally copying the tempo, time signature, and patches from the beginning of the sequence.

指定节拍的循环会更容易(4/4次为1/4音符),因此我们可以将需要的节拍数乘以MidiFileTimeBase来获得节拍,至少4/4 。 在这里,我将不讨论其他时间签名,因为这是音乐理论,超出了范围。 如果您希望此技术准确,则必须处理时间签名。 无论如何,在FirstDownBeatFirstNote或至少从这些位置之一偏移节拍开始循环也是有帮助的。 它们之间的区别是FirstDownBeat低音/踢鼓,而FirstNote任何音符。 一旦计算了偏移量和长度,就可以将它们传递给GetRange() ,以获取仅具有指定范围的MidiSequenceMidiFile ,还可以选择从序列开头复制速度,拍号和音色。

// assume file holds a MidiFile we're working with
var start = file.FirstDownBeat;
var offset = 16; // 16 beats from start @ 4/4
var length = 8;  // copy 8 beats from start
// convert beats to ticks
offset *= file.TimeBase;
length *= file.TimeBase;
// get the range from the file, copying timing
// and patch info from the start of each track
file = file.GetRange(start+offset,length,true);
// file now contains an 8 beat loop

无需流媒体即可预览/播放 (Previewing/Playing Without Streaming)

You can play any MidiSequence or MidiFile using Preview(). This is synchronous unlike the streaming API but doesn't require the use of MidiStream. Using it from the main application thread is almost never what you want, since it blocks. This is especially true when specifying the loop argument because it will hang the calling thread indefinitely while it plays forever. What you actually want to do is spawn a thread and play it on the thread. Here's a simple technique to do just that by toggling whether it's playing or not any time this code runs:

您可以使用Preview()播放任何MidiSequenceMidiFile 。 与流式API不同,这是同步的,但不需要使用MidiStream 。 从主应用程序线程使用它几乎从来都不是您想要的,因为它会阻塞。 在指定loop参数时尤其如此,因为它会永久播放,从而无限期地挂起调用线程。 您真正想要做的是产生一个线程并在该线程上播放。 这是一种简单的技术,可以通过切换该代码是否正在运行来执行此操作:

// assume a member field is declared:
// Thread _previewThread and file
// contains a MidiFile instance
// to play.
if(null==_previewThread)
{// create a clone of file for// thread safety. not necessary// if "file" is never touched// againvar f = file.Clone();_previewThread = new Thread(() => f.Preview(0, true));_previewThread.Start();
} else {// kill the thread_previewThread.Abort();// wait for it to exit_previewThread.Join();// update our _previewThread_previewThread = null;
}

You can then call this code from the main thread to either start or stop playback of "file".

然后,您可以从主线程调用此代码以开始或停止播放“文件”。

流式预览/播放(简单) (Previewing/Playing With Streaming (Simple))

The following is the easy way to stream a sequence for playback.

以下是流式播放序列的简单方法。

// just grab the first output stream
using (var stm = MidiDevice.Streams[0])
{// open itstm.Open();// read a MIDI filevar mf = MidiFile.ReadFrom(@"..\..\Feel_good_4beatsBass.mid");// merge the tracks for playbackvar seq = MidiSequence.Merge(mf.Tracks);// set the stream timebasestm.TimeBase = mf.TimeBase;// start the playbackstm.Start();Console.Error.WriteLine("Press any key to exit...");// if we weren't looping// we wouldn't need to// hook this:stm.SendComplete += delegate (object s, EventArgs e){// loopstm.Send(seq.Events);};// kick things offstm.Send(seq.Events);// wait for exitConsole.ReadKey();
}

Note that we're only hooking SendComplete so we can loop the playback.

请注意,我们只钩住SendComplete因此可以循环播放。

流式预览/播放(复杂) (Previewing/Playing With Streaming (Complex))

The following technique allows more real-time control, but the drawback is that it's more complicated to use. This way, you can work on the stream in blocks.

以下技术可以实现更多实时控制,但缺点是使用起来更复杂。 这样,您可以分块处理流。

// demonstrate streaming a midi file 100 events at a time
// this allows you to handle files with more than 64kb
// of in-memory events (not the same as "on disk" size)
// this replays the events in a loop
var mf = MidiFile.ReadFrom(@"..\..\Bohemian-Rhapsody-1.mid"); // > 64kb!// we use 100 events, which should be safe and allow
// for some measure of SYSEX messages in the stream
// without bypassing the 64kb limit
const int EVENT_COUNT = 100;// our current cursor pos
int pos = 0;// merge our file for playback
var seq = MidiSequence.Merge(mf.Tracks);// the number of events in the seq
int len = seq.Events.Count;// stores the next set of events
var eventList = new List<MidiEvent>(EVENT_COUNT);// just grab the first output stream
// should be the wavetable synth
using (var stm = MidiDevice.Streams[0])
{// open the streamstm.Open();// start itstm.Start();// first set the timebasestm.TimeBase = mf.TimeBase;// set up our send complete handlerstm.SendComplete += delegate (object sender,EventArgs eargs){// clear the list    eventList.Clear();// iterate through the next eventsvar next = pos+EVENT_COUNT;for(;pos<next;++pos){// if it's past the end, loop itif (len <= pos){pos = 0;break;}// otherwise add the next eventeventList.Add(seq.Events[pos]);}// send the list of eventsstm.SendDirect(eventList);};// add the first eventsfor(pos = 0;pos<EVENT_COUNT;++pos){// if it's past the end, loop itif (len <= pos){pos = 0;break;}// otherwise add the next eventeventList.Add(seq.Events[pos]);}// send the list of eventsstm.SendDirect(eventList);// loop until a key is pressedConsole.Error.WriteLine("Press any key to exit...");Console.ReadKey();// close the streamstm.Close();
}

What we're doing here is merging the file's tracks into a single sequence for playback. We open the stream, and then start it, and grab up to 100 (EVENT_COUNT) events at a time and queue them using SendDirect() instead of Send(). The reason for that is the former does not buffer, although it is lower level and limited to 64kb worth of event memory. We're already buffering above so we don't need to. We've hooked SendComplete so each time if fires we grab the next 100 and then send those to the queue. If we go past the end, we reset the position to zero in order to loop. We do this until a key is pressed.

我们在这里所做的是将文件的音轨合并为单个序列进行播放。 我们打开流,然后启动它,一次捕获多达100( EVENT_COUNT )个事件,并使用SendDirect()而不是Send()它们排队。 这样做的原因是前者没有缓冲,尽管它的级别较低并且仅限于64kb的事件内存。 我们已经在上面缓冲了,所以我们不需要。 我们已经钩住了SendComplete因此,每次触发时,我们都会抓住下100个,然后将其发送到队列中。 如果经过终点,则将位置重置为零以便循环。 我们一直这样做,直到按下一个键。

录制演奏(简单) (Recording a Performance (Simple))

You can record a performance to a MidiFile quite simply by using StartRecording() and EndRecording(). Basically, what you do is you Open() the input device, optionally Start() it - it will be started for you if need be - and call StartRecording() passing a boolean value that indicates whether recording should commence immediately or wait for the first MIDI input. EndRecording() should be called when the recording is complete. You can optionally trim the remainder to the last MIDI signal received. Otherwise, all of the remaining empty time will be at the end of the file. EndRecording() returns a Type 1 MIDI file with two tracks. The first track contains the tempo map, but no performance data. The second track contains the performance data. If you want to pass the input through to the output so you can hear what you are recording you'll need to hook the Input event and Send() what you receive to an output device. This is shown below:

您可以录制性能提升到一个MidiFile很简单使用StartRecording()EndRecording() 基本上,您要做的就是Open()输入设备,也可以选择将其Start() -如果需要,它将为您启动-并调用StartRecording()传递一个布尔值,该布尔值指示记录应该立即开始还是等待记录开始。第一个MIDI输入。 录制完成后应调用EndRecording() 。 您可以选择将余数修整为最后收到的MIDI信号。 否则,所有剩余的空闲时间将在文件末尾。 EndRecording()返回带有两个音轨的Type 1 MIDI文件。 第一首曲目包含速度映射,但没有性能数据。 第二轨道包含性能数据。 如果要将输入传递到输出,以便听到正在录制的内容,则需要挂钩Input事件,并将Send()到输出设备的Send()挂钩。 如下所示:

MidiFile mf;
using (var idev = MidiDevice.Inputs[0])
{using (var odev = MidiDevice.Outputs[0]){idev.Input += delegate (object s, MidiInputEventArgs e){// this is so we can pass through and hear // our input while recordingodev.Send(e.Message);};// open the input// and outputidev.Open();odev.Open();// start recording, waiting for inputidev.StartRecording(true);// wait to end itConsole.Error.WriteLine("Press any key to stop recording...");Console.ReadKey();// get our MidiFile from thismf = idev.EndRecording();// the MIDI file is always two// tracks, with the first track// being the tempo map}
}

录制演奏(复杂) (Recording a Performance (Complex))

Recording manually allows you to do processing on the input before it is recorded. It can be somewhat involved especially since tracking the MIDI tick position can be tricky. You can use a Stopwatch for this but I prefer using the "precise time" API available in Windows 7 and beyond just to ensure there's no "drift" - see the scratch project for the Win32 P/Invoke declaration and helper property:

手动记录允许您在输入之前对输入进行处理。 特别是因为跟踪MIDI滴答声的位置可能很棘手,因此可能会涉及到一些问题。 您可以为此使用Stopwatch ,但我更喜欢使用Windows 7及更高版本中提供的“精确时间” API,以确保没有“漂移”-有关Win32 P / Invoke声明和helper属性,请参阅草稿项目:

using (var idev = MidiDevice.Inputs[0])
{// TODO: currently this doesn't let you// change the tempo in the middle of recording// match these two variables to your input rateshort timeBase = 480;var microTempo = MidiUtility.TempoToMicroTempo(120);// track 0 - meta track for tempo infovar tr0 = new MidiSequence();// our seq for recordingvar seq = new MidiSequence();// compute our timing based on current microTempo and timeBasevar ticksusec = microTempo / (double)timeBase;var tickspertick = ticksusec / (TimeSpan.TicksPerMillisecond / 1000) * 100;var pos = 0;// set this to _PreciseUtcNowTicks in order// to start recording now. Otherwise it will// not record until the first message is // received:var startTicks = 0L;using (var odev = MidiDevice.Outputs[0]){// hook up the delegateidev.Input += delegate (object s, MidiInputEventArgs ea){// initialize start ticks with the current time in ticksif (0 == startTicks)startTicks = _PreciseUtcNowTicks;// compute our current MIDI ticksvar midiTicks = (int)Math.Round((_PreciseUtcNowTicks - startTicks) / tickspertick);// pass through to playodev.Send(ea.Message);// HACK: technically the sequence isn't threadsafe but as long as this event// is not reentrant and the MidiSequence isn't touched outside this it should// be fineseq.Events.Add(new MidiEvent(midiTicks - pos, ea.Message));// this is to track our old position// so we can compute deltaspos = midiTicks;};// open the input deviceidev.Open();// open the output deviceodev.Open();// add our tempo to the beginning of track 0tr0.Events.Add(new MidiEvent(0, new MidiMessageMetaTempo(microTempo)));// start listeningidev.Start();Console.Error.WriteLine("Recording started.");// waitConsole.Error.WriteLine("Press any key to stop recording...");Console.ReadKey();// stop the buffer and flush any pending eventsidev.Stop();idev.Reset();}// create termination trackvar endTrack = new MidiSequence();var len = seq.Length;// comment the following to terminate // without the trailing empty score:len = unchecked((int)((_PreciseUtcNowTicks - startTicks) / tickspertick));endTrack.Events.Add(new MidiEvent(len, new MidiMessageMetaEndOfTrack()));// terminate the trackstr0 = MidiSequence.Merge(tr0, endTrack);seq = MidiSequence.Merge(seq, endTrack);// build a type 1 midi filevar mf = new MidiFile(1, timeBase);// add both tracksmf.Tracks.Add(tr0);mf.Tracks.Add(seq)
}

Here, the bulk of our work is in the setup and then the handling the Input event. For the setup, we have to compute the timing in terms of exactly how long a midi tick is. We get this into tickspertick which is the number of .NET "ticks" in a MIDI tick (or the reverse, I forget which now. It's confusing!). We then use this to track our position. We keep subtracting the old position from the current position to get a delta. Note that we're touching seq from another thread. This is okay because of several conditions, including the fact that seq is not touched outside of the delegate once Input starts firing. Anyway, at the end, we make sure we terminate the tracks with an end track marker and we create a simple in-memory MIDI file. This should be able to be played to listen to what was just recorded and/or written to disk. Note that recording doesn't start until the first MIDI signal received, and the silent remainder of the recording is preserved. That can easily be changed by modifying the code.

在这里,我们的大部分工作是在设置过程中,然后是处理Input事件。 对于设置,我们必须根据Midi tick的确切时间来计算时间。 我们把它输入到tickspertick ,它是MIDI滴答声中的.NET“滴答声”的数目(或者相反,我现在忘记了。令人困惑!)。 然后,我们使用它来跟踪我们的位置。 我们不断从当前位置减去旧位置以获得差值。 请注意,我们正在从另一个线程接触seq 。 这是可以的,因为有几种情况,包括一旦Input开始触发, seq就不会在委托外部被触及。 无论如何,最后,我们确保使用结束音轨标记终止音轨,并创建一个简单的内存MIDI文件。 应该可以播放它以收听刚刚记录和/或写入磁盘的内容。 请注意,只有在收到第一个MIDI信号后才开始录音,并且保留录音的其余部分。 可以通过修改代码轻松更改。

示范项目 (Demo Projects)

MidiSlicer (MidiSlicer)

MidiSlicer (pictured at the top) allows you to perform several operations on a MIDI file, like extracting portions of the MIDI file, extracting certain tracks, changing the volume, transposing, and more. It is useful for operating on raw MIDI files you have sequenced.

MidiSlicer(如上图所示)使您可以对MIDI文件执行多项操作,例如提取MIDI文件的某些部分,提取某些音轨,更改音量,移调等等。 这对处理已排序的原始MIDI文件很有用。

The main mess of code that does the magic is here in Main.cs _ProcessFile():

Main.cs _ProcessFile() ,实现魔术的主要代码混乱:

// first we clone the file to be safe
// that way in case there's no modifications
// specified in the UI we'll still return
// a copy.
var result = _file.Clone();// transpose it if specified
if(0!=TransposeUpDown.Value)result = result.Transpose((sbyte)TransposeUpDown.Value, WrapCheckBox.Checked,!DrumsCheckBox.Checked);// resample if specified
if (ResampleUpDown.Value != _file.TimeBase)result = result.Resample(unchecked((short)ResampleUpDown.Value));// compute our offset and length in ticks or beats/quarter-notes
var ofs = OffsetUpDown.Value;
var len = LengthUpDown.Value;
if (0 == UnitsCombo.SelectedIndex) // beats
{len = Math.Min(len * _file.TimeBase, _file.Length);ofs = Math.Min(ofs * _file.TimeBase, _file.Length);
}
switch (StartCombo.SelectedIndex)
{case 1:ofs += result.FirstDownBeat;break;case 2:ofs += result.FirstNoteOn;break;
}// nseq holds our patch and timing info
var nseq = new MidiSequence();
if(0!=ofs && CopyTimingPatchCheckBox.Checked)
{// we only want to scan until the// first note on// we need to check all tracks so// we merge them into mtrk and scan// thatvar mtrk = MidiSequence.Merge(result.Tracks);var end = mtrk.FirstNoteOn;if (0 == end) // break later:end = mtrk.Length;var ins = 0;for (int ic = mtrk.Events.Count, i = 0; i < ic; ++i){var ev = mtrk.Events[i];if (ev.Position >= end)break;var m = ev.Message;switch (m.Status){// the reason we don't check for MidiMessageMetaTempo// is a user might have specified MidiMessageMeta for// it instead. we want to handle bothcase 0xFF:var mm = m as MidiMessageMeta;switch (mm.Data1){case 0x51: // tempocase 0x54: // smpteif (0 == nseq.Events.Count)nseq.Events.Add(new MidiEvent(0,ev.Message.Clone()));elsenseq.Events.Insert(ins, new MidiEvent(0,ev.Message.Clone()));++ins;break;}break;default:// check if it's a patch changeif (0xC0 == (ev.Message.Status & 0xF0)){if (0 == nseq.Events.Count)nseq.Events.Add(new MidiEvent(0, ev.Message.Clone()));elsenseq.Events.Insert(ins, new MidiEvent(0, ev.Message.Clone()));// increment the insert count++ins;}break;}}// set the track to the loop lengthnseq.Events.Add(new MidiEvent((int)len, new MidiMessageMetaEndOfTrack()));
}
// see if track 0 is checked
var hasTrack0 = TrackList.GetItemChecked(0);// slice our loop out of it
if (0!=ofs || result.Length!=len)result = result.GetRange((int)ofs, (int)len,CopyTimingPatchCheckBox.Checked,false);// normalize it!
if (NormalizeCheckBox.Checked)result = result.NormalizeVelocities();// scale levels
if (1m != LevelsUpDown.Value)result = result.ScaleVelocities((double)LevelsUpDown.Value);// create a temporary copy of our
// track list
var l = new List<MidiSequence>(result.Tracks);// now clear the result
result.Tracks.Clear();
for(int ic=l.Count,i=0;i<ic;++i)
{// if the track is checked in the list// add it back to resultif(TrackList.GetItemChecked(i)){result.Tracks.Add(l[i]);}
}
if (0 < nseq.Events.Count)
{// if we don't have track zero we insert// one.if(!hasTrack0)result.Tracks.Insert(0,nseq);else{// otherwise we merge with track 0result.Tracks[0] = MidiSequence.Merge(nseq, result.Tracks[0]);                    }
}
// stretch the result. we do this
// here so the track lengths are
// correct and we don't need ofs
// or len anymore
if (1m != StretchUpDown.Value)result = result.Stretch((double)StretchUpDown.Value, AdjustTempoCheckBox.Checked);// if merge is checked merge the
// tracks
if (MergeTracksCheckBox.Checked)
{var trk = MidiSequence.Merge(result.Tracks);result.Tracks.Clear();result.Tracks.Add(trk);
}
return result;

You can see this is pretty involved, simply because there are so many options. It really runs MidiSequence through its paces, using many of the techniques outlined earlier.

您可以看到这很复杂,仅仅是因为有很多选择。 它使用前面概述的许多技术,确实可以MidiSequence自己的MidiSequence运行MidiSequence

四四 (FourByFour)

FourByFour is a simple drum machine step sequencer that can create MIDI files.

FourByFour是一个简单的鼓机步骤音序器,可以创建MIDI文件。

There's a beat control we won't cover here, but here's the main magic in Main.cs _CreateMidiFile():

这里没有节拍控件,但这是Main.cs _CreateMidiFile()的主要魔术:

var file = new MidiFile();
// we'll need a track 0 for our tempo map
var track0 = new MidiSequence();
// set the tempo at the first position
track0.Events.Add(new MidiEvent(0, new MidiMessageMetaTempo((double)TempoUpDown.Value)));
// compute the length of our loop
var len = ((int)BarsUpDown.Value) * 4 * file.TimeBase;
// add an end of track marker just so all
// of our tracks will be the loop length
track0.Events.Add(new MidiEvent(len, new MidiMessageMetaEndOfTrack()));// here we need a track end with an
// absolute position for the MIDI end
// of track meta message. We'll use this
// later to set the length of the track
var trackEnd = new MidiSequence();
trackEnd.Events.Add(new MidiEvent(len, new MidiMessageMetaEndOfTrack()));// add track 0 (our tempo map)
file.Tracks.Add(track0);// create track 1 (our drum track)
var track1 = new MidiSequence();// we're going to create a new sequence for
// each one of the drum sequencer tracks in
// the UI
var trks = new List<MidiSequence>(BeatsPanel.Controls.Count);
foreach (var ctl in BeatsPanel.Controls)
{var beat = ctl as BeatControl;// get the note for the drumvar note = beat.NoteId;// it's easier to use a note map// to build the drum sequencevar noteMap = new List<MidiNote>();for (int ic = beat.Steps.Count, i = 0; i < ic; ++i){// if the step is pressed create // a note for itif (beat.Steps[i])noteMap.Add(new MidiNote(i * (file.TimeBase / 4), 9, note, 127, file.TimeBase / 4-1));}// convert the note map to a sequence// and add it to our working trackstrks.Add(MidiSequence.FromNoteMap(noteMap));
}
// now we merge the sequences into one
var t = MidiSequence.Merge(trks);
// we merge everything down to track 1
track1 = MidiSequence.Merge(track1, t, trackEnd);
// .. and add it to the file
file.Tracks.Add(track1);
return file;

Basically, all we're doing here is using note maps to create our drum sequence, and then setting the track length and program data using the technique outlined earlier.

基本上,我们在这里要做的就是使用音符映射来创建鼓音序,然后使用前面概述的技术来设置音轨长度和程序数据。

MidiMonitor (MidiMonitor)

The MIDI monitor simply monitors a MidiInputDevice for incoming MIDI messages and displays them. It's very very simple. Here's the meat of it in Main.cs:

MIDI监视器仅监视MidiInputDevice是否有传入的MIDI消息并显示它们。 非常非常简单。 这是Main.cs中的内容

private void InputsComboBox_SelectedIndexChanged(object sender, EventArgs e)
{if (null != _device)_device.Close();_device = InputsComboBox.SelectedItem as MidiInputDevice;_device.Input +=device_Input;_device.Open();_device.Start();
}private void device_Input(object sender, MidiInputEventArgs args)
{try{Invoke(new Action(delegate () { MessagesTextBox.AppendText(args.Message.ToString() + Environment.NewLine);}));}catch{}
}

All we're doing here is capturing the incoming messages as shown earlier, and then appending it to text box. The gotchas here are since we're firing from another thread, we need to use the control's Invoke() method to marshal the code onto the main thread for execution. We also wrap it in a try/catch just in case somehow we get a message while shutting down, but I'm not sure this is necessary.

我们在这里所做的就是捕获如前所示的传入消息,然后将其附加到文本框中。 这里的陷阱是因为我们是从另一个线程中触发,因此我们需要使用控件的Invoke()方法将代码编组到主线程上以便执行。 我们也将它包装在try / catch中,以防万一我们在关闭时收到消息,但是我不确定这是必要的。

拍子节奏 (taptempo)

Taptempo demonstrates manual (as opposed to automatic) tempo synchronization functionality. The manual syncing is more accurate than using MidiStream.UseTempoSynchronization=true because it doesn't have to rely on a timer. Instead, it spins a tight loop and uses that to do the timing. Unfortunately, there is no equivalent for receiving tempo sync messages in a timely way - we must rely on callbacks so the timing isn't perfect on the receive end.

Taptempo演示了手动(而不是自动)速度同步功能。 手动同步比使用MidiStream.更准确MidiStream. UseTempoSynchronization=true因为它不必依赖计时器。 取而代之的是,它旋转一个紧密的循环并使用它来进行计时。 不幸的是,没有等效的方法可以及时接收速度同步消息-我们必须依靠回调,因此在接收端的时间安排并不理想。

刮 (scratch)

Scratch simply demonstrates some of the techniques already outlined above, so it's not worth covering here. It's basically just a playground for testing code.

Scratch仅演示了上面已经概述的一些技术,因此此处不值得介绍。 基本上,这只是一个测试代码的场所。

The CPP project that accompanies it is just a testbed for calling the API from C++ to make sure I was doing it right, but I'm not using it right now.

随附的CPP项目只是从C ++调用API的测试床,以确保我做对了,但是我现在不使用它。

虫子 (Bugs)

First, tempo-sychronization isn't very accurate, which is why it's experimental at this time. The limitation may be insurmountable.

首先,速度同步不是很准确,这就是为什么它目前处于实验阶段。 该限制可能是无法克服的。

Second, not all real-time messages are respected yet. The only synching capability is tempo.

其次,并非所有实时消息都得到尊重。 唯一的同步功能是速度。

翻译自: https://www.codeproject.com/Articles/5272315/Midi-A-Windows-MIDI-Library-in-Csharp

c# midi窗体

c# midi窗体_Midi:C#中的Windows MIDI库相关推荐

  1. 演练:在 Windows 窗体中承载 Windows Presentation Foundation 复合控件 【转载】

    http://msdn.microsoft.com/zh-cn/library/ms745781.aspx 更新:2007 年 11 月 本演练演示如何创建 WPF 复合控件,并通过使用 Elemen ...

  2. 理解Windows窗体和WPF中的跨线程调用

    你曾开发过Windows窗体程序,可能会注意到有时事件处理程序将抛出InvalidOperationException异常,信息为" 跨线程调用非法:在非创建控件的线程上访问该控件" ...

  3. 在C#中调用windows API函数

    Api函数是构筑Windws应用程序的基石,每一种Windows应用程序开发工具,它提供的底层函数都间接或直接地调用了Windows API函数,同时为了实现功能扩展,一般也都提供了调用Windows ...

  4. python禁用任务管理器_c#中禁用windows的任务管理器的方法

    以前制作的桌面锁屏软件虽然也禁用过任务管理器,但是采取的是比较笨的方法,而且对操作系统还有一定的危害.因为任务管理也是一个窗体也就是说它中也是一个独立进程,所以只需要强制性关闭这个进程即可以关闭任务管 ...

  5. 在windows系统中使用Ceres非线性优化库:(一)安装Ceres库

    (一)安装Ceres库                         1.用vcpkg安装Ceres库                 1.1.安装vcpkg 1.2.安装Ceres 1.3.配置C ...

  6. delphi语音_在Delphi中使用Windows文本语音转换

    In this tutorial I will show you how to use the Windows Speech API in Delphi. I will only cover basi ...

  7. C#中调用Windows API时的数据类型对应关系

    C#中调用Windows API时的数据类型对应关系 原文 C#中调用Windows API时的数据类型对应关系 BOOL=System.Int32 BOOLEAN=System.Int32 BYTE ...

  8. 在c#中调用windows脚本的方法

    在c#中调用windows脚本的方法 方法1:直接调用   CODE:   System.Diagnostics.Process proc = new System.Diagnostics.Proce ...

  9. 如何在Python中调用Windows的cmd命令?

    简 介: 利用os,commands,subprocess模块可以在python中调用windows中的命令.从使用方便性上来看,利用os.popen可以执行windows的程序并可以获得返回内容. ...

最新文章

  1. HTML在网页设计中是什么作用?
  2. 全球及中国原料药产业投资动态及未来运行前景分析报告2021-2027年
  3. 实战能力|一文看懂GDB调试底层实现
  4. 4x3矩阵键盘扫描法c语言,四种4*4矩阵键盘的扫描方法
  5. 【AI面试题】特征归一化
  6. 邓迎春绘画201702作品5
  7. SpringAOP 通知(adivce)- methodIntercepor
  8. Silverlight:Dependency Property(依赖属性)学习笔记
  9. 阶段2 JavaWeb+黑马旅游网_15-Maven基础_第4节 maven生命周期和概念模型图_08maven生命周期...
  10. 企业安全三步走 惠普重新思考安全战略
  11. 经典 SQL 数据库笔试题及答案整理
  12. 高效 Windows 工作环境 Java 开发环境搭建
  13. html中的排名怎么写,html制作畅销书排行榜
  14. java怎么遵循ws规范,WS-BPEL语言基础
  15. Win10安装安卓模拟器入坑记
  16. Excel散点图 如何用平滑线 连接 不连续的点
  17. 用Python读红楼梦之——一、词云绘制
  18. 行路难 李白
  19. java实现对pdf文件压缩,拆分,修改水印,添加水印
  20. 讲讲语言转换程序:将一种语言转换为另一种语言的程序

热门文章

  1. 彻底理解系统、系统思维和复杂系统的设计
  2. 记spi nor flash的调试心得
  3. React-Native开源项目GITHUB
  4. DIV布局——人电影网站(5页) HTML+CSS+JavaScript 学生DW网页设计作业成品
  5. Python爬虫案例:批量下载超清画质手机壁纸
  6. scada组态开源java_RapidScada免费开源Scada组态软件系列教程6-二次开发
  7. 联想RD650服务器主板维修,免工具设计 徒手拆解ThinkServer RD650
  8. uniapp之unipush安卓app信息推送
  9. 【Vuejs】953- Vue 项目性能优化技巧分享
  10. PS霓虹灯光字体效果一键制作方法