c# midi播放器
Download source - 64.9 KB
下载源64.9 KB
Download latest from GitHub
从GitHub下载最新版本
Consider visiting my followup article here. It has improved code and a more thorough explanation of MIDI and the Midi library API.
考虑在这里访问我的后续文章 。 它改进了代码,并对MIDI和Midi库API进行了更全面的说明。
Quite some time ago, I wrote a VST and FL Studio plugin which used looped MIDI streams to play audio. I wrote it in C++, but I prototyped many of the MIDI operations in C# first. Later, I expanded this C# prototype code into a MIDI file editing library, which I've provided here, along with an example utility that allows for simple editing of a MIDI file.
相当一段时间以前,我编写了一个VST和FL Studio插件,该插件使用循环的MIDI流播放音频。 我用C ++编写,但是我首先用C#制作了许多MIDI操作的原型。 后来,我将此C#原型代码扩展到了我在此处提供的MIDI文件编辑库中,并提供了一个示例实用程序,该实用程序允许简单地编辑MIDI文件。
Update: Added time signature support. Small bugfix in playback.
更新:添加了时间签名支持。 播放中的小错误修正。
Update 2: Added MIDI message classes for each type of MIDI operation, and several features to the MidiSlicer app
更新2:为每种MIDI操作类型添加了MIDI消息类,并为MidiSlicer应用添加了一些功能
Update 3: Added Normalize and Level scaling. Made the Offset and Length floating point
更新3:添加了标准化和级别缩放。 制作偏移和长度浮点
Update 4: Improvements to correctness of API created MIDI sequences and files, better preview and save.
更新4:改进API创建的MIDI序列和文件的正确性,更好地预览和保存。
Update 5: Added Transpose() option to API and to GUI
更新5:向API和GUI添加了Transpose()选项
Update 6: API enhancements and improvement to overall GUI behavior
更新6: API增强和整体GUI行为的改进
MIDI stands for Musical Instrument Digital Interface. What it does is allow you to automate digital instruments similar to how one of those old player pianos work. MIDI contains all the note information - basically the "sheet music" for a song which it can then broadcast to up to 16 different digital audio devices like drum machines and synthesizers, or MIDI capable pianos and the like. Your Windows machine contains a default device that can play numerous synthesized sounds emulating various instruments like pianos and guitars. Your phone does too. Your systems can use this to play MIDI files out of your default audio device - usually your primary sound card or audio hardware. MIDI is often used by phones to store ring tones. That having been said, MIDI was originally designed for musicians, and its primary purpose is to allow musicians to record or "sequence" their performance and save it to a file for replay or editing.
MIDI代表乐器数字接口。 它的作用是让您自动化数字乐器,就像那些老式钢琴之一的工作方式一样。 MIDI包含所有音符信息-基本上是歌曲的“乐谱”,然后可以将其广播到多达16种不同的数字音频设备,例如鼓机和合成器,或具有MIDI功能的钢琴等。 您的Windows计算机包含一个默认设备,该设备可以播放模拟钢琴和吉他等各种乐器的大量合成声音。 您的手机也可以。 您的系统可以使用它来播放默认音频设备(通常是主声卡或音频硬件)中的MIDI文件。 手机通常使用MIDI来存储铃声。 话虽如此,MIDI最初是为音乐家而设计的,其主要目的是允许音乐家录制或“排序”他们的演奏并将其保存到文件中以进行重放或编辑。
MIDI is first and foremost a wire protocol, and secondly a file format. The protocol consists of realtime MIDI "messages". All a MIDI file is in its essence is the protocol stream stored as a file along with a delta time for each "message" - the time in the song the "message" should be played. A delta time plus a message is called a MIDI event. MIDI events are stored as a stream in the file for replay later. Therefore, understanding the protocol is fundamental to understanding the file format.
MIDI首先是有线协议,其次是文件格式。 该协议包括实时MIDI“消息”。 实质上,所有MIDI文件都是作为文件存储的协议流以及每个“消息”的增量时间-“消息”中歌曲应播放的时间。 增量时间加上一条消息称为MIDI事件。 MIDI事件作为流存储在文件中,以供稍后重播。 因此,了解协议是了解文件格式的基础。
Having been designed in the 1980s, MIDI defines an 8 bit wire protocol. Any strings are ASCII, and most values are 0-127 (7 bits plus a leading 0 bit) with some values being 0-255 (8 bit). There are occasionally multibyte values (larger than 8 bits) in the stream. These are always big-endian, so on Intel platforms you'll have to swap the byte order.
MIDI是1980年代设计的,它定义了8位线协议。 任何字符串都是ASCII,大多数值为0-127(7位加上前导0位),有些值为0-255(8位)。 流中偶尔会有多字节值(大于8位)。 这些始终是big-endian,因此在Intel平台上,您必须交换字节顺序。
A MIDI message at the very least contains an 8 bit "status byte." However, a message may contain additional fields/payload depending on the value of the status byte. The status byte tells us both the "type" of message (in the first 4 bits), plus the "channel" (midi "device") the message is intended for in the final 4 bits - remember the MIDI protocol allows control of up to 16 devices - from here on referred to as channels. Most MIDI messages have additional fields. For example, a "note on" message contains the note number, and the velocity of the note to be played. The following MIDI message is composed of 3 bytes. In the first byte, the 9 half is the code for note on, and 0 half is the code for channel 0. Next the node number for note C, octave 5 (48 hex below) is to be played at maximum velocity (127/7F) as indicated by the final byte, which must have the high bit set to zero, leaving you with a numeric range of 0-127:
MIDI消息至少包含一个8位的“状态字节”。 但是,一条消息可能包含其他字段/有效负载,具体取决于状态字节的值。 状态字节告诉我们消息的“类型”(在前4位中)以及消息在最后4位中用于“通道”(中部“设备”)-记住MIDI协议允许控制向上到16个设备-从此处称为通道。 大多数MIDI消息都有其他字段。 例如,“ note on”消息中包含音符编号和要播放的音符的速度。 以下MIDI信息由3个字节组成。 在第一个字节中,9的一半是音符打开的代码,0的一半是通道0的代码。接下来,音符C的节点号八度5(下面的48十六进制)将以最大速度(127 / 7F),如最后一个字节所示,该字节必须将高位设置为零,从而使您的数字范围为0-127:
90 48 7F
This will cause the device to strike the note and hold it until a note off message is found for that same note:
这将导致设备敲击该音符并按住它,直到找到该音符的音符关闭消息为止:
80 48 7F
The only difference between a note off and a note on is the first nibble is 8 hex instead of 9 hex. Most devices won't use the velocity byte for a note off message but send it anyway. I typically will use the same value I specified in note on, or zero in cases where I don't know the corresponding note on velocity. Either way should work with MIDI devices out there, but it's always possible that a device is weird. The thing about this protocol is you have to be kind of forgiving of dodgy devices, and that requires good, old fashioned testing.
音符关闭和音符打开之间的唯一区别是第一个半字节是8进制而不是9十六进制。 大多数设备不会将速度字节用于音符关闭消息,但无论如何都要发送。 通常,我将使用在音符上指定的相同值,或者在我不知道速度的相应音符的情况下使用零。 两种方法都可以与MIDI设备一起使用,但是设备总是很奇怪。 关于该协议的问题是,您必须宽容狡猾的设备,这需要良好的老式测试。
Again, different messages are different lengths. The patch/program change message indicates which "sound" a channel will use. Often MIDI devices such as synthesizers can produce many different kinds of sounds. This message allows you to send a 7-bit (0-127/7F) code (encoded as a full byte with the high bit 0) that indicates the patch to use. Selecting patch 2 gives us:
同样,不同的消息具有不同的长度。 补丁/程序更改消息指示通道将使用哪个“声音”。 诸如合成器之类的MIDI设备通常可以产生多种不同的声音。 此消息使您可以发送7位(0-127 / 7F)代码(编码为全字节,高位为0),指示要使用的补丁。 选择补丁2将为我们提供:
C0 02
You may have noticed that aside from the status byte, all of our values are 7-bit encoded as 8-bit with the high bit of zero leaving us with an effective range of values from 0-127/7F. This is important because of an optimization called a running status byte which I'll cover briefly, and is covered as well at the links in the Further Reading section.
您可能已经注意到,除了状态字节以外,我们所有的值都被7位编码为8位,高位为零,从而使我们的有效值范围为0-127 / 7F。 这一点很重要,因为有一个称为运行状态字节的优化,我将简要介绍它,在“ 进一步阅读”部分的链接中也将进行介绍。
A MIDI message may be a complete message with a status byte, or the status byte may be omitted in which case the previous status will be used. This allows for "runs" of multiple messages of the same type and sent to the same channel but with different parameters. Most of the time, this just causes extra complication for an optimization that often doesn't matter, so you don't really have to emit it but you have to be able to read it. That being said, MIDI is technically bandwidth limited so if you have a lot of events it might make sense to emit it as well.
MIDI消息可以是带有状态字节的完整消息,或者可以省略状态字节,在这种情况下,将使用先前的状态。 这允许“运行”相同类型的多个消息,并发送到相同通道但参数不同。 在大多数情况下,这只会给优化带来额外的复杂性,而这种优化通常并不重要,因此您实际上不必发出它,但必须能够阅读它。 话虽这么说,MIDI在技术上受带宽限制,所以如果您有很多事件,那么发出它也是很有意义的。
Occasionally in MIDI messages, such as a the pitch bend (status nibble E), you'll find 14-bit values are used in messages. These are encoded by the least significant 7-bits (in an 8-bit field with high bit 0) followed by the most significant 7-bits (in an 8-bit field with high bit 0)
有时在MIDI消息中,例如弯音(状态半字节E),您会发现消息中使用了14位值。 它们由最低有效7位(在高位0的8位字段中)编码,然后由最高有效7位(在高位0的8位字段中)编码
You can find a complete list of MIDI messages at the links in the Further Reading section.
您可以在“ 进一步阅读”部分的链接中找到MIDI消息的完整列表。
A MIDI file is laid out in "chunks." Each chunk is a fourCC ASCII value that indicates the "type" of the chunk. This is followed by a big-endian 4-byte integer that indicates the length of the data that follows, which is the actual data for the chunk. The first chunk type must be "MThd" and the only other common chunk type is "MTrk" Any chunk types not understood should be skipped.
MIDI文件以“块”布局。 每个块都是一个fourCC CC ASCII值,指示该块的“类型”。 这之后是一个大字节序的4字节整数,该整数指示后面的数据的长度,该数据是块的实际数据。 第一个块类型必须为“ MThd”,唯一的其他公共块类型为“ MTrk”。任何不了解的块类型都应跳过。
The "MThd" chunk contains the MIDI file type (usually 1), the track count, and the timebase (commonly 480), each encoded as big-endian 16-bit words.
“ MThd”块包含MIDI文件类型(通常为1),音轨计数和时基(通常为480),每个均编码为大端16位字。
The "MTrk" chunk contains a track which is a sequence of note on/off message events and other MIDI events. Each event is a delta time followed by a partial or complete MIDI message. The first MIDI track in a MIDI file is usually "special" in that it contains meta information about the MIDI file, including critical data like the tempo information, but also things like lyrics.
“ MTrk”块包含一个轨道,该轨道是音符开/关消息事件和其他MIDI事件的序列。 每个事件都是一个增量时间,然后是部分或完整的MIDI消息。 MIDI文件中的第一个MIDI轨道通常是“特殊”的,因为它包含有关MIDI文件的元信息,包括诸如节奏信息之类的关键数据,还包括歌词之类的东西。
The delta time is encoded as a "variable length integer" which I won't cover here, but is covered at the Standard MIDI File Format link in the Further Reading section. It indicates the number of MIDI "ticks" since the last event (hence delta.) I'll get into timing below.
增量时间被编码为“可变长度整数”,在此不做介绍,但在“进一步阅读”部分的“标准MIDI文件格式”链接中进行了介绍。 它指示自上次事件以来的MIDI“滴答声”的数量(因此发生变化)。
The message that follows can be a full MIDI message, or partial MIDI message with the status byte omitted as described previously.
后面的消息可以是完整的MIDI消息,也可以是部分MIDI消息,如前所述,其状态字节被省略了。
MIDI timing is measured in ticks. The timing of a tick varies depending on the timebase of the MIDI file, which is measured in ticks-per-quarter-note. This is also known as pulses-per-quarter-note or PPQ. That gives you the length of one beat, at the default 4/4 time. The default tempo is 120 beats per minute. The tempo and time signature are set using MIDI events with special MIDI "meta" messages (status byte of FF) and can be set throughout the playback. These are typically in the first track, and are global to all tracks.
MIDI音调以滴答度数为单位。 滴答声的时序因MIDI文件的时基而异,该时基以每四分音符滴答声为单位。 这也称为每四分之一脉冲数或PPQ。 这样一来,您的拍子长度将达到默认的4/4时间。 默认速度为每分钟120次。 使用带有特殊MIDI“元”消息(FF的状态字节)的MIDI事件设置速度和拍号,并可在整个播放过程中进行设置。 这些通常位于第一轨道,并且对所有轨道都是全局的。
I've included a couple of MIDI files I downloaded in the project directory for testing. Any copyright information is available in the MIDI file itself. A-Warm-Place.mid uses features not fully supported by this library but it "mostly" plays. The reason I've included it is it's a useful test for extending the library in the future to support SMPTE timing and proper sysex transmission.
我包含了一些我下载到项目目录中的MIDI文件以进行测试。 MIDI文件本身中提供了所有版权信息。 A-Warm-Place.mid使用的功能未得到该库的完全支持,但“大多数”可以播放。 我包含它的原因是,这是将来扩展库以支持SMPTE计时和正确sysex传输的有用测试。
The main class is MidiFile
, suitably named because this class represents MIDI data in the MIDI file format. It's not necessarily backed by a physical file. It can be created and operated on entirely in memory. It provides access to common features that apply to all tracks, plus timing information, and access to the meta information in track 0. Read a MIDI file using MidiFile.ReadFrom()
and write a MIDI file using WriteTo()
.
主类是MidiFile
,因此可以适当命名,因为此类表示MIDI文件格式的MIDI数据。 它不一定由物理文件支持。 它可以完全在内存中创建和操作。 它提供对适用于所有轨道的通用功能的访问,以及定时信息,以及对轨道0中的元信息的访问。使用MidiFile.ReadFrom()
读取MIDI文件,并使用WriteTo()
写入MIDI文件。
The other really important class here is MidiSequence
which represents a single sequence or track in a MidiFile
. This class allows you to access all of its MidiEvent
s either as relative delta based or absolute time, and provides access to any meta information stored in the sequence. Sequences can be merged with Merge()
or concatenated with Concat()
. They can be stretched or compressed with Stretch()
. You can retrieve a range of events using GetRange()
. Note that some of these operations appear on MidiFile
as well and those will operate on each track/sequence in the file.
另一个真正重要的类是MidiSequence
,它表示MidiFile
的单个序列或音轨。 此类允许您基于相对增量或绝对时间访问其所有MidiEvent
,并提供对序列中存储的任何元信息的访问。 序列可以与Merge()
或与Concat()
串联。 可以使用Stretch()
拉伸或压缩它们。 您可以使用GetRange()
检索一系列事件。 请注意,其中一些操作也会出现在MidiFile
,而这些操作将在文件中的每个音轨/序列上进行。
In the demo code, we process each track depending on the settings in the UI:
在演示代码中,我们根据UI中的设置处理每个轨道:
if (NormalizeCheckBox.Checked)
trk.NormalizeVelocities();
if (1m != LevelsUpDown.Value)
trk.ScaleVelocities((double)LevelsUpDown.Value);
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);
}
...
if (1m != StretchUpDown.Value)
trk = trk.Stretch((double)StretchUpDown.Value, AdjustTempoCheckBox.Checked);
First we handle velocity normalization and scaling. Next, we grab the offset and length of the selection. Then if it's specified in beats, we use the TimeBase
to compute the beats. The rest of the Math
calls just clamp the values to the maximum allowable length.
首先,我们处理速度归一化和缩放。 接下来,我们获取选择的偏移量和长度。 然后,如果以拍子指定,则使用TimeBase
来计算拍子。 其余的Math
调用仅将值限制为最大允许长度。
Next, if our length and offsets are different than 0 and the length of the sequence we get the range of events within that time.
接下来,如果我们的长度和偏移量不同于0,并且序列的长度不同,那么我们将获得该时间范围内的事件范围。
I've ommitted a bunch of code in the middle from the listing above, but it handles the rest of the features in the UI by calling the appropriate MidiSequence
API methods.
我在上面的清单中省略了很多代码,但是它通过调用适当的MidiSequence
API方法来处理UI中的其余功能。
Finally, if we've specified a stretch value other than 1, we call Stretch()
to stretch the track.
最后,如果我们指定的拉伸值不是1,则调用Stretch()
拉伸轨道。
MidiEvent
simply contains a Position
in ticks, and a MidiMessage
. Depending on whether this event was retrieved through Events
or AbsoluteEvents
, the Position
will be a delta time or an absolute time, respectively.
MidiEvent
只是包含一个以刻度为单位的Position
和一个MidiMessage
。 根据该事件是通过Events
还是AbsoluteEvents
检索的, Position
将分别为增量时间或绝对时间。
MidiMessage
and its derivatives represent MIDI messages of various lengths such as MidiMessageByte
and MidiMessageWord
. There are also the special MidiMessageMeta
and MidiMessageSysex
classes which represent a MIDI meta message and a MIDI system exclusive message respectively. See the Standard MIDI File Format link in the Further Reading section for more about these messages. In addition, there are MIDI messages for each type of MIDI operation, such as MidiMessageNoteOn
, MidiMessageCC
, and MidiMessageChannelPitch
.
MidiMessage
及其派生词表示各种长度的MIDI消息,例如MidiMessageByte
和MidiMessageWord
。 还有特殊的MidiMessageMeta
和MidiMessageSysex
类,分别表示MIDI元消息和MIDI系统专有消息。 有关这些消息的更多信息,请参见“进一步阅读”部分的“标准MIDI文件格式”链接。 此外,每种MIDI操作类型都有MIDI消息,例如MidiMessageNoteOn
, MidiMessageCC
和MidiMessageChannelPitch
。
MidiContext
is a class that represents the current "state" of a MIDI sequence. When using GetContext()
, one of these instances will be returned from the function and it will give you all of the current note velocities, control positions, pitch wheel position, aftertouch information and the rest. This allows you to know what is playing and how at any given moment within the sequence.
MidiContext
是一个类,代表MIDI序列的当前“状态”。 使用GetContext()
,将从函数中返回这些实例之一,它将为您提供所有当前音符速度,控制位置,音高轮位置,触后信息以及其他信息。 这样一来,您就可以知道正在播放什么以及在序列中的任何给定时刻如何播放。
MidiUtility
provides low level MIDI features and you shouldn't typically need to use it directly. It provides some low level IO methods, byte swapping, and conversion of tempo/microtempo.
MidiUtility
提供了低级MIDI功能,您通常不需要直接使用它。 它提供了一些底层IO方法,字节交换和速度/微速度的转换。
Note that to do things like set the tempo and time signature, you must add MidiMessageMeta
messages for each to the corresponding tempo or time signature change. In most files, these should be added to track #0.
请注意,要执行诸如设置速度和时间签名的操作,必须将每条消息的MidiMessageMeta
消息添加到相应的速度或时间签名更改中。 在大多数文件中,这些文件应添加到轨道#0。
The Preview()
methods do not use Win32 MCI "play" to play the file or sequence. Instead, the sequence is played on the calling thread using C# and calls to the MIDI device out Win32 API directly. Getting the timing right in that method was a total bear. You may want to dispatch it on a separate thread because it is CPU intensive. It's possible (maybe) to do this in a more CPU efficient manner by replacing the hard loop with a timer callback but that's not trivial to implement. See the demo code for the Preview thread handling.
Preview()
方法不使用Win32 MCI“播放”来播放文件或序列。 而是使用C#在调用线程上播放序列,并直接通过Win32 API调用MIDI设备。 用这种方法正确地把握时机完全是徒劳。 您可能希望将其分派到单独的线程上,因为它占用大量CPU。 可以(也许)通过将硬循环替换为计时器回调以更有效的CPU方式执行此操作,但这并非易事。 有关预览线程处理的信息,请参见演示代码。
Standard Midi File Format - describes the layout and structure of MIDI files in detail
标准Midi文件格式 -详细描述MIDI文件的布局和结构
Essentials of the MIDI Protocol - describes the MIDI message protocol in detail
MIDI协议要点 -详细描述MIDI消息协议
18th March, 2020 - Initial submission
2020年3月18 日 -初次提交
18th March, 2020 - Update 1: Added time signature support, bugfix in playback
2020年3月18 日 -更新1:添加了拍号支持,并修复了播放中的错误
18th March, 2020 - Update 2: Added several features to MidiSlicer and to the MIDI API
2020年3月18 日 -更新2:为MidiSlicer和MIDI API添加了一些功能
18th March, 2020 - Preview now loops.
2020年3月18 日 -预览现在循环播放。
20th March, 2020 - Added Normalize and Level scaling. Made the Offset and Length floating point
2020年3月20 日 -添加了归一化和水平缩放。 制作偏移和长度浮点
20th March, 2020 - Bugfix and improvement to Preview/SaveAs implementations
2020年3月20 日 -修正和改进Preview / SaveAs实施
21st March, 2020 - Improved timing in UI and of rendered tracks
2020年3月21 日 -改进了UI和渲染轨道的时序
22nd March, 2020 - Improved API, save, and preview
3月22日2020年-改进的API,保存和预览
22nd March, 2020 - Added Transpose() and Transpose GUI option
3月22日2020年-增加移调()和移调GUI选项
22nd March, 2020 - Added copyTimingAndPatchInfo option to GetRange() API methods
3月22日2020年-增加copyTimingAndPatchInfo选项GetRange()API方法
23rd March, 2020 - Improvements to API, and overal GUI functionality
3月23日2020年-改进的API,并全部测试GUI功能
翻译自: https://www.codeproject.com/Articles/5262538/A-MIDI-File-Slicer-and-MIDI-Library-in-Csharp
c# midi播放器