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

使用 DirectMusic 开发 MIDI 应用程序

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.91/5 (42投票s)

2002年7月30日

LGPL3

25分钟阅读

viewsIcon

662429

downloadIcon

9582

DirectMusic MIDI 的封装类库。

Sample Image

0. 目录

1. 介绍

本文的主要目的是让读者对标准音乐通信方法 (MIDI) 有一个基本的了解,并解释 DirectMusic 如何控制音乐合成器的功能。它还详细介绍了如何使用 DirectMIDI 类库开发基于 MIDI 的应用程序。

2. MIDI 是什么?

2.1 什么是 MIDI?

MIDI 代表音乐乐器数字接口(Musical Instrument Digital Interface),是一种数字通信协议。自 1983 年 8 月 MIDI 1.0 规范创建以来,所有具有 MIDI 功能的设备都必须使用相同的数据结构和格式与使用相同规范的任何其他乐器协同工作。该协议是一种语言,允许连接来自不同制造商的不同乐器,并提供一个能够传输和接收数字数据,这些数据编码不同的命令,其他乐器必须遵守这些命令。

这些命令基于 MIDI 规范,并包含一种通用语言,提供有关事件的信息,例如音符开启、音符关闭、力度、时间信息、系统专用 (SysEx) 和音色更改。

MIDI 信息通过带有五针 DIN 型公头连接器的 MIDI 线缆传输。其中两针用于传输数字二进制信息 (MIDI 代码)。其中一针发出稳定的五伏电流,而另一针在 5 伏和 0 伏之间交替以表示二进制信息(开和关)。第三针是地线,其余两针目前未使用。

MIDI 制造商选择这种串行接口是因为它比并行接口便宜,并且传输范围更长。MIDI 串行接口的速度是每秒 31,250 比特。每个 MIDI 数字字需要 10 比特,因此每秒允许传输 3125 条消息。

2.2 MIDI 规范

MIDI 规范由 MIDI 制造商协会 (MMA) 发布和维护。MMA 成立于 1984 年,旨在维护和增强 MIDI 规范,以确保没有任何一家公司能够控制它。它由来自计算机和音乐行业的百余家硬件和软件公司组成,旨在改进和标准化基于 MIDI 的产品的功能。制造商 ID 号的完整列表可在 MMA 网站上找到:http://www.midi.org/about-mma/mfr_id.shtml

MIDI 的使用和规范的实施对任何人开放,没有限制,但描述完整 MIDI 规范的官方文档受版权保护,不能在任何万维网站点上访问。该规范详细介绍了所有已批准的 MIDI 消息和用途,包括通用 MIDI 和标准 MIDI 文件。目前,MMA 维护最新 MIDI 技术(如 GM2(通用 MIDI 2)、DLS2.1(可下载音色 2.1)和用于移动应用程序的 GM Lite)的规范。

3. 电脑演奏音乐

MIDI 系统的一个优点是可以使用计算机编辑和播放 MIDI 消息序列。除了其处理速度和存储容量,计算机还允许以极高的精度和简单性修改任何音乐参数。

自 MIDI 标准创建以来,几乎所有平台都出现了各种各样的商业音乐创作程序。最初的程序运行在 32 位计算机上,例如 Amiga、Atari 以及著名的 Apple Macintosh。

如今,在 PC 领域,无论是 Windows 还是 Linux,MIDI 软件开发都达到了很高的水平。

基于 MIDI 接口的应用程序也从简单的乐器互连发展到电子灯光控制、人工智能和教育应用程序领域。

在编程基于 MIDI 的系统时遇到的最常见问题是硬件级访问。幸运的是,在基于 Windows 的 PC 中,操作系统提供了两个 API,允许低级访问硬件端口。这些 API 是 Windows MIDI API 和 DirectMusic,本文将重点介绍并解释它们。

3.1 DirectMusic 与 MIDI

DirectMusic 是 DirectX 的重要组成部分,作为一组组件安装在系统中。与 DirectSound 结合使用,DirectMusic 提供了一种在游戏和其他应用程序中以交互方式播放音乐和音效的方法。其 API 为 DirectSound 提供了一个更高的抽象层,使得混合声音和应用 3D 定位等效果变得容易。它还允许同时播放多个片段和 MIDI 文件,为游戏增添更多真实感。DirectMusic 最令人兴奋的功能之一是能够控制 MIDI 设备以接收和发送音乐数据,以及使用 DLS2(可下载声音级别 2 标准),这提供了更高质量的声音合成并扩展了音色库的数量。

3.2 Win32 MIDI 编程的主要 DirectMusic COM 接口

DirectMusic 作为 DirectX 的一部分,使用了组件对象模型(COM 技术)。这意味着它是面向对象的,并且基于分布式计算。除了 COM 技术带来的巨大优势,如位置透明性、二进制标准格式和运行时多态性之外,DirectMusic COM 对象还由接口组成。在以下几行中,将介绍 DirectMusic MIDI 应用程序中涉及的最重要接口。

  • IDirectMusic8: IDirectMusic8 接口提供了管理缓冲区、端口和主时钟的方法。每个应用程序不应有超过一个此接口实例。

  • IReferenceClock: 此标准接口提供对主时钟的访问,主时钟是一个具有高分辨率的内核模式硬件计时器,用于同步系统中的所有音频播放。IReferenceClock::GetTime 方法以 100 纳秒的增量返回当前时间,作为 64 位整数(定义为 REFERENCE_TIME 类型)。

  • IDirectMusicPort8: IDirectMusicPort8 接口提供对 DirectMusicPort 对象的访问,该对象表示发送或接收音乐数据的设备,例如 MPU-401 的输入端口、MPU-401 的输出端口或 Microsoft 软件合成器。

  • IDirectMusicThru8: 此接口允许将消息从捕获端口传输到其他端口。IDirectMusicThru8::ThruChannel 方法用于在捕获端口上的通道和另一个端口上的通道之间建立或中断直通连接。

  • IDirectMusicBuffer8: IDirectMusicBuffer8 接口表示一个缓冲区,其中包含时间记录数据(通常是 MIDI 消息形式),由端口进行排序。缓冲区包含少量数据(通常小于 200 毫秒)。缓冲区至少创建 32 字节用于标准 MIDI 消息。

  • IDirectMusicLoader8: 此接口用于加载 DirectMusic 对象,例如片段、MIDI 文件、波形和 DLS 文件。提供垃圾回收。

  • IDirectMusicCollection8: IDirectMusicCollection8 接口管理 DLS 文件的乐器集,并包含将其下载到合成器端口的方法。

  • IDirectMusicInstrument8: 此接口表示 DLS 集合中的单个乐器,该乐器通过使用 IDirectMusicPort8::DownloadInstrument 下载到合成器中。

  • IDirectMusicDownloadedInstrument8: 此接口用于识别已下载到合成器中的乐器。然后使用接口指针通过调用 IDirectMusicPort8::UnloadInstrument 卸载乐器。

  • IDirectMusicPortDownload8: IDirectMusicPortDownload8 接口使应用程序能够直接与支持 DLS 的端口通信,DLS下载,并直接将内存块下载到端口。

  • IDirectMusicDownload8: IDirectMusicDownload8 接口表示用于下载到合成器端口的连续内存块。DLS合成器端口。

3.3 使用 DirectMIDI 类库开发应用程序

3.3.1 介绍 - DirectMIDI 布局

该库的核心基于其十个相关类,这些类定义了基于 MIDI 的应用程序中涉及的不同对象,并封装了实现它们的代码。

下图显示了使用 DirectMIDI 的应用程序创建的对象。

如您所见,存在一个主要对象,类型为 CDirectMusic,它封装了基于 Win32 应用程序的 DirectMusic COM 实例化。此对象负责初始化 MIDI 端口对象,这些对象分为两类:用于传入 MIDI 消息(例如 SysEx 数据或典型 MIDI 1.0 消息)的输入端口和用于发送 SysEx 格式或 MIDI 消息数据的输出端口。还有一个名为 CMasterClock 的附加对象,它提供硬件计时器作为主时钟的枚举和选择。

还有另外三个对象直接和间接与 COutputPort 对象相关联,例如 CDLSLoader,它负责加载 DLS 文件以将其存储到 CCollection 对象中。此对象表示 DLS 1.0/2.0 数据格式的一组乐器,并允许将其乐器提取到更好的容器中,称为 CInstruments 对象。这些对象负责保留特定乐器的实例,以便更好地处理和组织。

一旦我们从集合中选择了所有乐器,我们就可以开始将它们下载或卸载到合成器中的特定 MIDI 程序,以便播放它们。
除了 CInstrument 对象之外,DirectMIDI 库还提供了另一个类似的对象,它允许存储从 .wav 文件加载的波形数据和程序生成的波形。这个名为 CSampleInstrument 的对象提供了辅助函数,用于在下载到输出端口之前调整包络、LFO 和区域。

最后,CDMusicException 类处理应用程序中产生的所有异常,并显示有关导致错误的详细信息。

3.3.2 启动应用程序

3.3.2.1 第一步:设置开发环境

您可以使用 Visual Studio 和 DirectMIDI 封装库在许多不同类型的项目中启动应用程序,例如 MFC、Win32 独立和 Win32 控制台应用程序,但为了使其更简单,我将解释如何构建一个简单的 Win32 控制台应用程序,该应用程序显示库中所有可用的特性。因此,您必须启动 Visual Studio 并选择一个 Win32 控制台应用程序项目,并选择“一个简单应用程序”选项。创建简单项目后,您需要将类库的所有 DirectMIDI 头文件和 .cpp 文件包含在其中。为此,请转到菜单栏中的“项目”,选择“添加到项目”,“文件”,然后将 DirectMIDI 文件夹中与 MIDI 部分和子文件夹相关的所有文件添加到您的项目中。因此,为了创建面向 MIDI 的应用程序,我们需要包含以下必要的头文件:CDirectMidi.h、CDirectBase.hCMidiPart.h,以及包含这些头文件时所需的所有 .cpp 文件,例如 CSegment.cpp。要执行此操作,如果您有 Visual Studio 7 (.NET),您必须从主菜单中选择“项目”选项,然后单击“添加现有项”选项以包含类库文件。

现在您手头已拥有开始编写新音乐应用程序所需的所有代码。为了正确编译和链接您的项目,您的计算机中必须安装 DirectX8/9 SDK。如果您已经安装并配置好,那就没问题了;如果没有,请转到菜单栏中的“工具”,选择“选项”,然后单击“目录”选项卡以添加 DirectX8/9 头文件和库文件的路径。如果您有 Visual Studio 7 (.NET),请转到菜单栏中的“工具”,单击“选项”,然后打开“项目”文件夹。展开“显示目录”组合列表,选择“”和“包含文件”选项。最后,将头文件和库文件目录添加到各自的列表中。

3.3.2.2 第二步:第一行代码

编译器应该知道当前 .cpp 工作文件中将使用哪些外部代码。为此,您必须使用 #include 指令来告诉编译器此项目在其他文件中引用了外部代码。所需的头文件如下所示

// ANSI I/0 headers

#include <conio.h>

#include <iostream.h>

// Math header

#include <math.h>

// The class library wrapper

#include ".\\DirectMidi\\CDirectMidi.h"

// Inline library inclusion

#pragma comment (lib,"dxguid.lib") // guid definitions

#pragma comment (lib,"winmm.lib")
#pragma comment (lib,"dsound.lib")
#pragma comment (lib,"dxerr9.lib")

using namespace std;     // Standard C++ library header    

using namespace directmidi; // the wrapper global namespace 


// Maximum size for SysEx data in input port

const int SYSTEM_EXCLUSIVE_MEM = 48000; 

// Defines PI

const double PI = 3.1415926;

#Pragma comment 指令指示链接器创建包含所需库的目标文件。上面代码的最后两行定义了此示例项目中将需要的常量。

3.3.2.3 第三步:准备音乐捕获

您应该知道 CInputPort 类负责管理传入的 MIDI 事件。这些 MIDI 事件由一个线程捕获,该线程根据到达端口的数据类型调用 CReceiver 类中定义的两个重载纯虚成员函数。这两种不同类型的数据可以是无结构的 MIDI 数据(系统专用)或结构化的(典型 MIDI 消息)。

为了重写这些虚函数,我们需要从 CReceiver 派生一个类,如下所示

// Derived class from CReceiver


class CDMReceiver:public CReceiver
{
public:
    // Overriden functions

    void RecvMidiMsg(REFERENCE_TIME rt,DWORD dwChannel,DWORD dwBytesRead,
                         BYTE *lpBuffer);
    void RecvMidiMsg(REFERENCE_TIME rt,DWORD dwChannel,DWORD dwMsg);
};

完成此操作后,您可以编写一些代码来处理这些事件

// Overriden function for SysEx data capture

void CDMReceiver::RecvMidiMsg(REFERENCE_TIME lprt,DWORD dwChannel,
                               DWORD dwBytesRead,BYTE *lpBuffer)
{
    DWORD dwBytecount;
    
    // Print the received buffer

    for (dwBytecount = 0;dwBytecount < dwBytesRead;dwBytecount++)
    {    
        cout.width(2);
        cout.precision(2);
        cout.fill('0');        
        cout << hex << static_cast<int>(lpBuffer[dwBytecount]) << " ";
        if ((dwBytecount % 20) == 0) cout << endl;
        if (lpBuffer[dwBytecount] == END_SYS_EX)
            cout << "\nSystem memory dumped" << endl;
    }    
}

// Overriden function for structured MIDI data capture

void CDMReceiver::RecvMidiMsg(REFERENCE_TIME lprt,DWORD dwChannel,
                               DWORD dwMsg)
{
    unsigned char Command,Channel,Note,Velocity;
    
    // Extract MIDI parameters from a MIDI message    

    CInputPort::DecodeMidiMsg(dwMsg,&Command,&Channel,&Note,&Velocity);
    
    if (Command == NOTE_ON) //Channel #0 Note-On

        {                    
        cout << "Received on channel " << static_cast<int>(Channel) << 
                " Note " << static_cast<int>(Note) 
             << " with velocity " << static_cast<int>(Velocity) << endl;
    }
}

第一个函数读取整个接收到的 SysEx 数据缓冲区,以十六进制数字格式打印值,并检测合成器何时达到数据转储的末尾(SysEx 数据结束)。请注意,并非所有 SysEx 数据都在一次对 RecvMidiMsg 的调用中接收,可以多次连续调用此成员函数。
第二个成员函数以双字格式接收典型的 MIDI 消息,例如音符开启或程序更改。如果要将消息分解,则必须使用静态函数 CInputPort::DecodeMidiMsg 来提取每个 MIDI 字节。

3.3.2.4 第四步:初始化对象

在此步骤中,我们声明将在整个应用程序中使用的主要对象。它们如下所示

int main(int argc, char* argv[])
{
    CDirectMusic CDMusic;
    CInputPort   CInPort;
    CDMReceiver  Receiver;    
    COutputPort  COutPort;
    CDLSLoader   CLoader;
    CCollection  CCollectionA,CCollectionB;
    CInstrument  CInstrument1,CInstrument2;
    CSampleInstrument CSample1,CSample2;    

// Continues

第一行声明了一个 CDirectMusic 类型的对象,它负责实例化和初始化 DirectMusic,并将是最后一个被销毁的对象。接下来是处理输入端口的 CInputPort。第三个是 CDMReceiver 对象,它是一个 CReceiver 派生类类型,并实现了上面看到的重写函数。COutPort 对象负责向设备发送数据并将乐器下载到端口。最后一个对象管理将在下一步中注释的可下载声音。现在,您已准备好开始调用方法并激活所有 MIDI 系统。请看下面

    // Initialize DirectMusic

try
{
    CDMusic.Initialize();
    // Initialize ports given the DirectMusic manager object

    COutPort.Initialize(CDMusic);
    CInPort.Initialize(CDMusic);
    
// Continues

以下代码激活输入和输出端口

INFOPORT PortInfo;
DWORD dwPortCount = 0;
    
// Software Synthesizer selection

do
    COutPort.GetPortInfo(++dwPortCount,&PortInfo);
while (!(PortInfo.dwFlags & DMUS_PC_SOFTWARESYNTH));

// Output port activation given the port information 

COutPort.SetPortParams(0,0,1,SET_REVERB | SET_CHORUS,44100);
COutPort.ActivatePort(&PortInfo);
cout << "Selected output port: " << PortInfo.szPortDescription << endl;
 
// Input port activation, select the first one (by default)

CInPort.GetPortInfo(1,&PortInfo);
CInPort.ActivatePort(&PortInfo,SYSTEM_EXCLUSIVE_MEM);
cout << "Selected input port: " << PortInfo.szPortDescription << endl;

// Sets up the receiver object

CInPort.SetReceiver(Receiver);

getch();

// Continues

前几行枚举所有输出端口,并选择系统中存在的第一个软件合成器,在 COutputPort::GetPortInfo 的第一个参数中给出 1 到 COutputPort::GetNumPorts 之间的一个数字。在调用 COutputPort::ActivatePort 之前,有必要调用 COutputPort::SetPortParams 方法以指示我们在输出端口中需要的功能类型(如果传递零给此方法,则假定该参数的默认配置)。然后我们可以通过传递指向 INFOPORT 结构的指针来调用 COutputPort::ActivatePort,使用在调用 COutputPort::SetPortParams 时传递的通道组数量和采样率参数来激活输出端口。通道组参数是要在软件端口中使用的 MIDI 通道组的数量,每个通道组是一组 16 个 MIDI 通道。

COutputPort::SetPortParams 方法中最重要的可配置参数之一是采样率参数,它是我们为了输出端口音质需要建立的频率(以 Hz 为单位)。在这种情况下,我们使用 44100Hz 作为采样率。

在最后三行中,我们选择用于 MIDI 捕获的输入端口,操作完全相同,但这次我们不枚举任何端口,只选择一个默认端口。请注意,CInputPort::ActivatePort 中有一个第二个参数,它指示分配系统独占数据的最大内存大小。在这种情况下,我们只保留 46.8 千字节。如果您省略此可选参数,默认值为 32 字节,足以接收标准 MIDI 数据。最后,我们通过调用 CInputPort::SetReceiver 方法来建立接收器对象。如果您关闭主括号并运行应用程序,您将获得以下输出

3.3.2.5 第五步:开始音乐捕获

一旦您初始化了输入端口,使用 DirectMIDI 从键盘捕获音乐数据就非常简单了。如果您在调用 CInPort::ActivatePort 时决定保留空间以接收系统独占数据,那么您的应用程序现在已准备好处理键盘生成的所有传入事件,包括标准 MIDI 数据。以下代码解释了如何激活捕获

  // Activates input MIDI message handling 

 CInPort.ActivateNotification();
 // Redirects messages from source global channel 0 to destination 

 // global channel 0 over channel group 0 (channels 1-16)

 CInPort.SetThru(0,0,0,COutPort);
    
// Continues

如您所见,代码的第一行激活了所有传入 MIDI 消息的通知,使用一个事件处理程序,该处理程序调用应用程序第一部分中已重写的相应虚成员函数。

DirectMIDI 下一个要评论的功能是重定向。使用重定向 (MIDI thru) 可以将 MIDI 消息从选定的输入 MIDI 端口传递到另一个输出 MIDI 端口,同时指定通道组、消息将重定向的源和目标全局通道。

以下屏幕截图显示了一个 SysEx 数据转储和正常的 MIDI 数据捕获

3.3.2.6 第六步:升级乐器限制

您尝试过新的音色字体吗?如果是这样,今天就是您的幸运日。DirectMIDI 支持加载存储在“可下载声音文件”(通常称为 DLS)中的多种声音。这项技术是 MIDI 制造商在最先进的多媒体技术中存储音色字体格式的标准。当前的 DLS2 文件格式指定了所有乐器定义:样本、LFO、低通滤波器、循环和包络发生器,这些都将在支持此功能的合成器中下载和渲染。DirectMIDI 支持两种类型的 DLS 操作:高级 DLS 和低级 DLS。

高级 DLS 是一种处理可存储在 DLS 1.0 和 2.0 文件格式中的波形乐器的方法。它们可以使用 DirectMusic Producer 等应用程序创建,该应用程序允许可视化配置前面解释的各种参数。低级 DLS 允许将 DLS 1.0 数据块直接下载到端口,从应用程序程序提供乐器发音和区域参数。

3.3.2.6.1 高级 DLS

在您的项目中使用 DLS 文件非常简单。为此,您只需声明一个 CDLSLoader 类型的对象即可加载和卸载乐器文件。您还需要声明一个 CCollection 对象来存储乐器集合,以及一个 CInstrument 对象来保留对特定乐器的引用。以下代码显示了如何将一组乐器加载和卸载到端口。

    // Initialize the Loader object  


    CLoader.Initialize();
    
    // Loads the first dls file

    CLoader.LoadDLS(".\\Media\\sample.dls",CCollectionA);
    
    // Loads the deafault GM collection of the software synthesizer

    CLoader.LoadDLS(NULL,CCollectionB);
    
    
    // Structure of the instrument information

    INSTRUMENTINFO InstInfo;
    DWORD dwInstIndex = 0;
    
    // Enumerates instruments in CollectionB

    while (CCollectionB.EnumInstrument(dwInstIndex++,&InstInfo) == S_OK)
    {    
        cout << "Instrument name: " << InstInfo.szInstName  << endl;
        cout << "Patch in collection: " << InstInfo.dwPatchInCollection 
                     << endl;
        cout << "----------------------------------------" << endl;
    }

    
    // Gets the instrument with index 214 from the CollectionB

    CCollectionB.GetInstrument(CInstrument1,214);
    // Assigns it to the MIDI program 0

    CInstrument1.SetPatch(0);
    
    
    cout << "\nSelected instrument: " 
      << CInstrument1.m_strInstName << endl;
    cout << "Source collection patch " 
      << CInstrument1.m_dwPatchInCollection << 
    " to destination MIDI program: " 
      << CInstrument1.m_dwPatchInMidi << endl;

    
    // Gets the instrument with index 0 from the CollectionA

    CCollectionA.GetInstrument(CInstrument2,0);
    // Assigns it to the MIDI program 1

    CInstrument2.SetPatch(1);
    cout << "\nSelected instrument: " 
      << CInstrument2.m_strInstName << endl;
    cout << "Source collection patch " 
      << CInstrument2.m_dwPatchInCollection << 
    " to destination MIDI program: " 
      << CInstrument2.m_dwPatchInMidi << endl;

    // Sets the note range

        
    CInstrument1.SetNoteRange(0,127);
    CInstrument2.SetNoteRange(0,127);

    // Downloads the instruments to the output ports

    COutPort.DownloadInstrument(CInstrument1);
    
    COutPort.DownloadInstrument(CInstrument2);
    
    cout << "\nInstruments downloaded" << endl;
    cout << "Playing with the instrument:" 
      << CInstrument1.m_strInstName << endl;
    cout << "Press a key to play with the second instrument..." 
        << endl;
    
    COutPort.SendMidiMsg(COutputPort::EncodeMidiMsg(
          PATCH_CHANGE,0,0,0),0);
    COutPort.SendMidiMsg(COutputPort::EncodeMidiMsg(
          NOTE_ON,0,40,127),0);
    
    getch();
    COutPort.SendMidiMsg(COutputPort::EncodeMidiMsg(
          NOTE_OFF,0,40,0),0);

    COutPort.SendMidiMsg(COutputPort::EncodeMidiMsg(
         PATCH_CHANGE,0,1,0),0);
    COutPort.SendMidiMsg(COutputPort::EncodeMidiMsg(
         NOTE_ON,0,60,127),0);

    cout << "Playing with the instrument:" 
      << CInstrument2.m_strInstName << endl;
    cout << "Press a key to exit the application..." << 
       endl;

    getch();
    COutPort.SendMidiMsg(COutputPort::EncodeMidiMsg(
        NOTE_OFF,0,60,0),0);
    // Continues

代码的第一行初始化加载器对象,该对象调用 Win32 函数 CoCreateInstance 并在进程空间中实例化 COM 对象。初始化加载器对象后,您可以使用 CDLSLoader::LoadDLS 成员函数加载 DLS 文件,该函数接受一个以 null 结尾的字符串,表示文件名,以及对 CCollection 目标对象的引用,乐器将加载到其中。当给定字符串为 null 时,DirectMidi 将加载合成器内存中定义的标准 GM/GS 集。要找出 CCollection 对象中包含哪些乐器,您必须调用 Collection::EnumInstrument 函数,该函数接受一个计数器变量,指示乐器在集合中的索引,以及一个指向 INSTRUMENTINFO 结构的指针,该结构将接收乐器的信息,即集合中的名称和音色。

您可以通过调用重载成员函数 CCollection::GetInstrument 并提供对具有集合中索引的乐器对象的引用来获取单个乐器的引用。此函数将使用乐器数据填充 CInstrument 对象的内部成员。CInstrument::SetNoteRange 方法激活乐器在产生音符开启时必须响应的键盘区域。最后,您需要通过调用成员函数 COutputPort::DownLoadInstrument 并传递乐器对象的引用,为合成器 MIDI 程序中的乐器提供一个目标。以下屏幕截图是最后代码输出的示例

3.3.2.6.2 低级 DLS

DirectMIDI 2.3 允许应用程序直接与支持 DLS 的端口通信,以便将内存块下载到其中。将数据下载到端口有两种选择:第一种是从包含要播放的数据的 .wav 文件加载波形,第二种是使用数学指令在内存中生成波形。在第一种情况下,我们需要使用静态成员函数 CDLSLoader::LoadWaveFile 加载 .wav 文件,并提供以下三个参数:指向文件路径字符串描述的指针,对目标 CSampleInstrument 对象的引用,以及指示文件所需访问权限的标志。如果文件访问标志是 DM_LOAD_FROM_FILE 常量,则 .wav 文件在需要时始终从文件读取,适用于大型文件。如果标志是 DM_USE_MEMORY 常量,则文件保留在动态内存中,从而提高访问速度。
如前所述,第二种替代方法是生成波形,可以使用 CSampleInstrument::SetWaveForm 方法设置,传递一个包含数据的 BYTE 指针和一个包含波形格式的 WAVEFORMATEX 结构(有关详细信息,请参阅 MSDN)。波形的最终目标是 CSampleInstrument 对象,它封装了执行乐器操作的代码。以下代码显示了这些功能

1    
2   // Loads the .wav file

3   CDLSLoader::LoadWaveFile(".\\media\\starbreeze.wav",CSample1,
4    DM_USE_MEMORY);
5   // Assigns the patch

6   CSample1.SetPatch(2);
7        
8   // Sets a continuous wave loop

9   CSample1.SetLoop(TRUE);
10        
11  // Sets additional wave parameters

12  CSample1.SetWaveParams(0,0,68,F_WSMP_NO_TRUNCATION); 
13        
14  REGION region;
15  ARTICPARAMS articparams;
16        
17  // Initializes structures

18  ZeroMemory(&region,sizeof(REGION));
19  ZeroMemory(&articparams,sizeof(ARTICPARAMS));
20        
21        
22  // Sets the region parameters

23  region.RangeKey.usHigh = 127;
24  region.RangeKey.usLow  = 0;
25  region.RangeVelocity.usHigh = 127;
26        
27  // Adjusts LFO    

28  articparams.LFO.tcDelay = TimeCents(10.0);
29  articparams.LFO.pcFrequency = PitchCents(5.0);
30        
31  // Sets the pitch envelope

32  articparams.PitchEG.tcAttack  = TimeCents(0.0);
33  articparams.PitchEG.tcDecay   = TimeCents(0.0);
34  articparams.PitchEG.ptSustain = PercentUnits(0.0);
35  articparams.PitchEG.tcRelease = TimeCents(0.0);
36
37        
38  // Sets the volume envelope

39  articparams.VolEG.tcAttack  = TimeCents(1.275);
40  articparams.VolEG.tcDecay   = TimeCents(0.0);
41  articparams.VolEG.ptSustain = PercentUnits(100.0);
42  articparams.VolEG.tcRelease = TimeCents(10.157);
43        
44        
45  // Sets the instrument parameters

46  CSample1.SetRegion(&region);
47  CSample1.SetArticulationParams(&articparams);
48        
49  // Allocates memory for the download interfaces

50  COutPort.AllocateMemory(CSample1);
51  
52  // Downloads the sample instrument to the port

53  COutPort.DownloadInstrument(CSample1); 
54  COutPort.SendMidiMsg(COutputPort::EncodeMidiMsg(PATCH_CHANGE,0,2,0),0);
55                    
56  cout << "Ready to play a wave sample instrument" << endl;
57
58  getch();
59
60        
61  // Assigns patch 1

62  CSample2.SetPatch(3);
63        
64  // Sets additional wave parameters

65  CSample2.SetWaveParams(0,0,68,F_WSMP_NO_TRUNCATION); 
66        
67  // Sets the instrument parameters

68  CSample2.SetLoop(TRUE);
69  CSample2.SetRegion(&region);
70  CSample2.SetArticulationParams(&articparams);
71
72  // Generates the waveform data

73  // Samples per second

74  DWORD nSamplesPerSec = 44100;
75        
76  double nTimeSec = 1.5; // Time duration of the sample

77                
78  // Number of samples

79  DWORD nSamples = static_cast<DWORD>(nTimeSec * nSamplesPerSec);
80        
81  // Digital frequency of the waveform

82  double Frequency = 1000.0/nSamplesPerSec;
83        
84  // Allocates memory for the waveform

85  WORD *pRawData = new WORD[nSamples];
86        
87  // Generates the waveform

88  for(DWORD ni = 0;ni < nSamples;ni++)
89      
90     pRawData[ni]  = static_cast<WORD>(30000*sin(2.0*PI*Frequency*ni) + 
91                5000*sin(6.0*PI*Frequency*ni) +
92                1000*sin(10.0*PI*Frequency*ni));
93    
94    
95
96 
97                                
98  // Format of the waveform

99  WAVEFORMATEX wfex = {WAVE_FORMAT_PCM,1,44100,44100,2,16,0};
100        
101  // Sets the waveform into the sample object

102  CSample2.SetWaveForm((BYTE*)pRawData,&wfex,nSamples*2);
103        
104  // Allocates interface memory

105  COutPort.AllocateMemory(CSample2);
106        
107  //Downloads the instrument to the port

108  COutPort.DownloadInstrument(CSample2);
109        
110  COutPort.SendMidiMsg(COutputPort::EncodeMidiMsg(PATCH_CHANGE,0,3,0),0);

在上面代码的前几行中,我们加载了 .wav 文件,调用了 CDLSLoader::LoadWaveFile 静态成员函数并将样本存储在内存中。

下载协议的第一个重要操作是在下载样本乐器之前为其分配一个 MIDI 程序(音色号)。因此,我们为此目的使用了 CSampleInstrument::SetPatch 方法(第 6 行)。

分配音色号后,我们可以选择是否通过使用 CSampleInstrument::SetLoop 方法(第 9 行)循环播放样本,并指定我们是需要连续播放的样本还是通常向前播放的样本。设置此参数后,我们可以继续调整其他波形播放参数,例如主音符(MIDI 统一播放音符)以及衰减和微调等其他参数(请参阅 DirectMIDI 在线文档)。为此,我们必须使用 CSampleInstrument::SetWaveParams 方法(第 12 行)。

正确下载样本的下一个基本参数是区域和发音(如果不设置这些参数,样本将不会发出声音)。在 REGION 结构中,我们设置了乐器必须响应音符开启的键盘区域。为此,我们必须将结构初始化为零,然后用相应的 MIDI 范围填充其成员(参见第 23-25 行)。对 ARTICPARAMS 结构执行相同的操作也很重要(请参阅 DirectMIDI 在线文档)。此结构包含一组成员,用于调整重要的参数,例如 LFO、音量包络 (VolEg) 和音高包络 (PitchEg)(第 28-42 行)。DirectMIDI 库提供了一组辅助函数来填充 ARTICPARAMS 结构的成员值。例如,我们有 directmidi::TimeCents 函数,它将“秒”转换为合适的输入格式(时间分)。接下来,调用 CSampleInstrument::SetRegionCSampleInstrument::SetArticulationParams 来设置这些参数(第 46 和 47 行)。

最后,您可以使用 COutputPort::DownloadInstrument 将样本乐器下载到输出端口,但首先您必须为执行所有这些操作的内部 DirectMusic 接口分配内存,调用带有样本对象引用的 CSampleInstrument::AllocateMemory 方法(第 52-55 行)。

上面代码的第二部分解释了如何生成一个简单的 1000Hz 波形,采样率为 44100Hz,每样本 16 位。第 78 到 100 行显示了波形生成。它们为所需样本数分配内存:一个与播放声音持续时间成比例的数字,在本例中为 1.5 秒(第 76 行)。

最后,在第 99 行,我们在调用 CSampleInstrument::SetWaveForm 方法之前填充 WAVEFORMATEX 结构的成员,该方法将指示 CSampleInstrument 对象原始数据缓冲区所在的位置(第 102 行)。

对于代码的其余部分,参数设置和下载操作与本节第一部分中评论的类似。

Starbreeze.wav 音量包络图

生成的波形图。

3.3.2.7 第七步:关闭应用程序

第七步也是最后一步是正确地结束应用程序。为此,您必须在应用程序结束之前调用以下成员函数

    // Breaks the redirection

    CInPort.BreakThru(0,0,0);
    // Ends the notification

    CInPort.TerminateNotification();
    
    // Unloads the collections from the loader

    CLoader.UnloadCollection(CCollectionA);
    CLoader.UnloadCollection(CCollectionB);
    
    // Unloads the instruments from the port

    COutPort.UnloadInstrument(CInstrument1);
    COutPort.UnloadInstrument(CInstrument2);
    
    // Unloads the sample instruments

    COutPort.UnloadInstrument(CSample1);
    COutPort.UnloadInstrument(CSample2);
   
    // Frees allocated memory

    COutPort.DeallocateMemory(CSample1);
    COutPort.DeallocateMemory(CSample2);
        
    // Disposes the memory

    delete [] pRawData;    
   
    // Exit 

}    
catch (CDMusicException& DMEx)
{
    cout << DMEx.GetErrorDescription() << endl;
}

return 0;
}

如果您在输入 MIDI 端口对象中激活了通知以接收传入的 MIDI 事件,则您有责任现在调用 CInputPort::TerminateNotification 来完成消息处理并告诉 DirectMusic 不再发出任何事件信号。如果您在端口之间建立了直通连接,则还必须调用 CInputPort::BreakThru。此外,一旦不再需要,从内存中卸载集合很重要,调用 CDLSLoader::UnloadCollection 并调用 COutputPort::DeallocateMemory 方法释放内部 DirectMusic 接口。与从合成器内存中卸载乐器的方法 COutputPort::UnloadInstrument 相同。

尽管 DirectMIDI 会在您忘记时为您释放内存,但最好还是自己动手。

3.3.3 异常处理

一些读者向我报告了他们在防止错误传播和避免异常情况方面遇到的问题。我研究了这个问题并找到了解决方案。为了解决这个问题,我在 DirectMIDI 方案中添加了一个新类用于异常处理。这个名为 CDMusicException 的新类处理应用程序使用该库产生的所有可能错误和故障,从而摆脱了旧的繁琐的 FAILED 宏的使用。

基本上,该对象提供三个重要的属性来报告错误,它们是:m_hrCode,用于报告在 DirectMusic 接口调用中获得的 DirectX COM HRESULT 代码;m_strMethod,提供函数调用失败的方法描述;以及 m_nLine,返回错误发生时模块源代码的行号。

除了这三个属性,还有一个额外的方法来方便错误描述。这是通过调用 CDMusicException::GetErrorDescription() 获得的,它在捕获到异常时返回一个包含详细错误描述的 LPCTSTR 字符串。您可以在下图中看到一个示例

4. 更多信息

如果您正在寻找有关 DirectMusic 的信息,您可以在 DirectX 8/9 SDK 文档中找到它,该文档与 MSDN 库一起提供在 DirectX 主页。如果您正在寻找有关 DirectMIDI 封装库的更详细信息,您可以在 SourceForge 项目主页上的在线 DirectMIDI 开发人员参考中以及本文中可下载的源代码中获取。

5. 演示应用程序

名为 MidiStation v1.9 的演示应用程序是一个易于使用的程序,它展示了 DirectMIDI 封装类的所有功能。您可以更改输出 MIDI 端口等参数,选择任何 GM 乐器,更改八度音阶,录制您的作品,预览来自外部键盘的所有音符和消息,甚至可以使用内置的 MIDI 键盘进行演奏。享受 MIDI 的乐趣吧!

6. 历史记录

  • 2003年4月30日:文章更新。
  • 2003年5月12日:源代码更新
  • 2003年7月15日:源代码更新
  • 2003年11月25日:文章更新。
MidiStation 特点
DirectMidi 更改


MidiStation 1.4.4 功能

  • 所有硬件 MIDI 端口可用
  • 交互式内置音乐键盘
  • 连接到外部 MIDI 键盘
  • 外部和内部键盘消息的可视化列表
  • GM 乐器支持
  • 八度音阶选择
  • 录制与播放

MidiStation 1.8.4 功能

  • 所有硬件和软件 MIDI 端口可用
  • 内部 GM/GS 套件加载
  • PC 键盘八度音阶控制
  • 改进的播放系统
  • 消息列表指南

MidiStation 1.9.0 功能

  • 内置且可复用的键盘控制
  • 隐式多线程同步
  • 完整八度音阶选择
  • 改进的 PC 键盘控制
  • 手形光标

MidiStation 1.9.1 功能

  • 修复了运行状态错误 (Andras22 错误)
  • 修复了 DLS 端口错误
  • 修复了消息列表错误

MidiStation 1.9.2 功能

  • 加载和保存 .MDS 序列文件
  • 无限录音


DirectMIDI 2.0b 更改

  • 改进的类析构函数
  • 软件合成器可用
  • 添加了灵活的转换函数
  • 启用 SysEx 接收和发送
  • 更好的 MIDI 端口枚举和选择方法
  • 重构类系统
  • 将外部线程适配为纯虚函数


DirectMIDI 2.1b 更改

  • 添加了异常处理
  • 修复了通道组的错误
  • 重新设计了类层次结构
  • DirectMIDI 封装在 directmidi 命名空间中
  • 乐器和端口的 UNICODE/MBCS 支持
  • 为软件合成器端口添加了延迟效果
  • 项目根据 GNU(通用公共许可证)条款发布

DirectMIDI 2.2b 更改

  • 重新设计了类接口
  • 更安全的输入端口终止线程
  • 新的 CMasterClock 类
  • DLS 文件可以从资源加载
  • DLS 乐器音符范围支持
  • 向库中添加了新的 CSampleInstrument 类
  • 样本直接下载到波形表内存
  • 支持 WAV 文件样本格式
  • 向输出端口类添加了新方法
  • 修复了小错误

DirectMIDI 2.3b 更改

  • 添加了新的 DirectMusic 类用于音频处理
  • 改进了 CMidiPort 类,支持内部缓冲区运行时调整大小
  • 3D 音频定位
© . All rights reserved.