|
|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Announcements
Chapters
Services
Feature Zones
|
0. Index
1. IntroductionThe main purpose of this article is to give a basic understanding of the standard music communication method (MIDI) and explaining how DirectMusic controls the music synthesizer features. It also details how to use theDirectMIDI class library to develop applications based on MIDI.
2. What about MIDI?2.1 What is MIDI?MIDI stands for Musical Instrument Digital Interface and is a digital communication protocol. After the creation in August 1983 of MIDI 1.0 Specification every device that has MIDI capabilities must work with any other instrument that uses the same specification using the same data structures and formats. This protocol is a language which allows connecting different instruments from different manufacturers and providing a link that is capable of transmitting and receiving digital data which encode different commands to which the other instrument must comply. These commands are based on the MIDI specification and include a common language that provides information about events, such as note-on, note-off, velocity, timing information, System Exclusive (SysEx) and patch change. MIDI information is transmitted through a MIDI cable that has DIN-type male plug connectors with five pins. Two of the pins are used to transfer digital binary information (MIDI Code). One of the pins issues a steady stream of five volts, while the other pin alternates between 5 volts and 0 volts to represent binary information (on and off). The third pin is a ground and the remaining two pins are currently not in use. This serial interface was chosen by MIDI manufacturers because it is less expensive than a parallel interface and has longer range. The speed of a MIDI serial interface is 31,250 bits per second. 10 bits are needed for every MIDI digital word, therefore allowing the transmission of 3125 messages per second. 2.2 The MIDI specificationThe MIDI specification is published and maintained by the MIDI Manufacturers Association (MMA). The MMA was formed in 1984 to keep and enhance the MIDI specification so that no one company would have control. It is comprised of over one hundred hardware and software companies from both the computer and music industries with the aim to improve and standardize the capabilities of MIDI-based products. A complete list of the manufacturer's ID numbers can be found at the MMA site.The use of MIDI and the implementation of the specification is available to anyone without restriction, but the official document which describes the complete MIDI specification is copyrighted and not accessible on any WWW site. The specification details all of the approved MIDI messages and uses including General MIDI and Standard MIDI files. At present the MMA keeps specifications for the latest MIDI technlogies such as GM2 (General MIDI 2), DLS2.1(Downloadable Sounds 2.1) and GM Lite for mobile applications. 3. Computers Playing MusicOne of the advantages of the MIDI system is the possibility to use a computer for editing and playing MIDI message sequences. Besides its processing speed and its storage capacity, the computer allows modifying any music parameter with great precision and simplicity. Since the creation of the MIDI standard, a great variety of commercial programs for musical composition have appeared on almost every platform. The first programs were on 32 bit-based computers like the Amiga, the Atari and also the well known Apple Macintosh. Nowadays in the PC domain there is a high level of MIDI software development under Windows as well as under Linux. The applications based on the MIDI interface have also evolved from the simple musical instrument interconnection to the domain of electronic light control, artificial intelligence and educational applications. The most common problem found when programming a MIDI-based system is the hardware-level access. Fortunately, in Windows-based PCs there are two APIs offered by the operating system which allow accessing hardware ports at the low level. These APIs are the Windows MIDI API and DirectMusic on which this article focuses and which are explained hereby. 3.1 DirectMusic and MIDIDirectMusic is an important part of DirectX and is installed in the system as a set of components. In combination with DirectSound, DirectMusic provides a method for playing music and sound effects in games and other applications in an interactive way. Its API provides a higher abstraction layer to DirectSound which makes easy mixing sounds and applying effects like 3D positioning. It also allows performing the playback of multiple segments simultaneously and MIDI files giving more realism to the games. One of the most exciting features in DirectMusic is the possibility to control MIDI devices for receiving and sending musical data and the use of DLS2 (Downloadable Sounds Level 2 standard) which provides a higher-quality sound synthesis and extends the number of sound fonts. 3.2 Main DirectMusic COM Interfaces for Win32 MIDI ProgrammingDirectMusic, as a part of DirectX, uses the Component Object Model (COM technology). This means that it is object oriented and based on distributed computing. Besides the great advantages of the COM technology like location transparency, binay standard format and runtime polymorphism, the DirectMusic COM objects are composed of interfaces. In the following lines, the most important interfaces involved in a DirectMusic MIDI application are commented:
3.3 Developing Applications with the DirectMIDI Class Library3.3.1 Introduction - DirectMIDI layoutThe main kernel of the library is based on its ten related classes which define the different objects involved in a MIDI based application encapsulating the code to realize them. The next diagram shows the objects created by an application which uses DirectMIDI:
As you can see, there is a main object of the There are another three objects related to the Once we have all the instruments selected from the collections, we can proceed to download or unload them to or from a specific MIDI program in the synthesizer in order to play them. Finally, the 3.3.2.1 First Step: Setting up the Development EnvironmentYou can initiate the application in many different kinds of projects with your Visual Studio and the DirectMIDI wrapper library, such as MFC's, Win32 standalone and Win32 console applications, but to make it easier I'm going to explain how to build a simple Win32 console application that shows all the characteristics available in the library. Therefore, you must start up your Visual Studio and select a Win32 console application project with the "A simple application" option selected. Once you have created a simple project you need to include all the DirectMIDI headers and .cpp files of the class library in it. To do this, go to Project in the menu bar, select Add to project, Files and then add to your project all the files existing in the DirectMIDI folder related to the MIDI part and subfolders. Therefore, in order to create an application oriented to MIDI we need to include the next necessary header files: CDirectMidi.h, CDirectBase.h and CMidiPart.h and all the .cpp files required when including these headers like the CSegment.cpp. To perform this, in case you have the Visual Studio 7 (.NET) you must select the Project option from the main menu and then click on the Add existing item option to include the class library files. Now you have all the code necessary in your hands to start programming a new musical application. It's important you have installed the DirectX8/9 SDK's in your computer in order to compile and link your project correctly. If you have it already installed and configured, that's fine for this, if not, go to Tools in the menu bar, select Options and then click on the Directories tab to add the path to the DirectX8/9 headers and library files. If you have the Visual Studio 7 (.NET), go to Tools in the menu bar, click on Options and then open the Projects folder. Expand the Show Directories for combo list and select the library and include files option. Finally, add the header and library files directories to their respective lists. 3.3.2.2 Second Step: The First Lines of CodeThe compiler should know what external code is going to be used in the current .cpp file of work. For this, you must use the #include directive in order to tell the compiler there is a reference to external code for this project in other files. The required headers are shown in the code below: // 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;
The directive 3.3.2.3 Third Step: Preparing the Music CaptureYou should know that the In order to override this virtual functions we need to derive a class from // 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);
};
Once you have made this, you can program some code to process these events: // 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;
}
}
The first function reads the entire received buffer of 3.3.2.4 Fourth Step: Initializing ObjectsIn this step we declare the main objects that will be used along the application. They are shown below: 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
The first line declares an object of type // Initialize DirectMusic
try
{
CDMusic.Initialize();
// Initialize ports given the DirectMusic manager object
COutPort.Initialize(CDMusic);
CInPort.Initialize(CDMusic);
// Continues
The following code activates the input and output ports: 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
The first lines enumerate all output ports and select the first software synthesizer existing in the system given a number from 1 to One of the most important configurable parameters in the In the last three lines we select the input port for MIDI capture doing exactly the same, but this time, we don't enumerate any, we limit only to select a default one. Note that there is a second parameter in
3.3.2.5 Fifth Step: Starting the Music CaptureCapturing musical data from your keyboard is very simple with DirectMIDI as soon as you have initialized the input port. If you decided to reserve space to receive system exclusive data in the call to // 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
As you can see, the first line of code activates the notification of all the incoming MIDI messages using an event handler that calls its respective virtual member function already overridden in the first part of the application. The next DirectMIDI feature to comment is the redirection. Using the redirection (MIDI thru) you can pass MIDI messages from a selected input MIDI port to another output MIDI port specifying the channel group, the source and destination global channel where the messages will be redirected. The next screenshot shows a SysEx data dump and a normal MIDI data capture:
3.3.2.6 Sixth Step: Upgrading the Instrument LimitDo you experiment with new sound fonts? If this is your case, this is your lucky day. DirectMIDI supports loading multiple sounds stored in "Downloadable Sounds files" better known as DLS. This technology is the MIDI manufacturer's standard for soundfont format storage in the state-of-the-art multimedia technology. The current DLS2 file format specifies all the instrument definitions: samples, LFO's, low pass filters, loops and envelope generators which will be downloaded and rendered in the synthesizer that supports this feature. DirectMIDI supports two types of DLS operations which are: High level DLS and Low level DLS. High level DLS is a way to handle waveform instruments that can be stored in DLS 1.0 and 2.0 file formats. They can be created with an application like DirectMusic Producer that allows to configure visually a wide range of parameters previously explained. Low level DLS allows direct downloading of DLS 1.0 data chunks to the port, providing instrument articulations and region parameters from the application program. 3.3.2.6.1 High level DLSUsing DLS files within your project is very simple. For this, you must only declare an object of CDLSLoader type in order to load and unload the instrument files. You will also need to declare a CCollection object to store the collections of instruments and a CInstrument object to keep a reference to a particular instrument. The code below shows how to load and unload a set of instruments to the port. // 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
The first line of code initializes the loader object which calls the Win32 function You can obtain a reference to an individual instrument by calling the overloaded member function
3.3.2.6.2 Low Level DLSDirectMIDI 2.3 enables an application to communicate directly with a port that supports DLS for downloading memory chunks to it. There are two alternatives for downloading data to the port: The first one is to load the waveform from a .wav file that contains the data to playback, and the second one is to generate the waveform in memory using math instructions. In the first case, we need to load the .wav file using the static member function 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(®ion,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(®ion);
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(®ion);
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);
In the first lines of the code above we load the .wav file, calling the The first important operation of the download protocol is to assign a MIDI program (patch number) to that sample instrument before downloading it. Thus, we have the After assigning the patch number, we can choose if we want to loop the sample by using the The next essential parameters for a correct sample download are the regions and the articulations (without setting these parameters the sample will not sound). In the Finally, you can proceed to download the sample instrument to the output port by using The second part of the code seen above explains how to generate a simple 1000Hz waveform with a 44100Hz sampling rate and 16 bits per sample. The lines 78 to 100 show the waveform generation. They allocate memory for the number of required samples: A number proportional to the duration of the playing sound, in this case 1.5 seconds (76th). Finally, in the 99th line we fill the members of the For the rest of the code, the parameter setting and downloading operations are similar to those commented in the first part of this section.
The generated waveform graph. 3.3.2.7 Seventh Step: Closing Down the ApplicationThe seventh and last step is to finish the application in a suitable way. To do this, you must call the next member functions before ending your application: // 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;
}
If you activated the notification in the input MIDI port object for receiving incoming MIDI events, it is your responsibility to call now Although DirectMIDI will free the memory for you in case you forget it, it's a good idea to do it by yourself. 3.3.3 Exception HandlingA few readers have reported me about their problems preventing error propagation and avoiding exception situations. I studied the problem and came up with the solution. To solve this, I added a new class to the DirectMIDI scheme for exception handling. This new class called Basically the object provides three important properties to inform about the error, these are: Besides these three properties, there is an additional method to facilitate the error description. This is obtained by calling
4. More informationIf you are looking for information about DirectMusic, you will be able to find it in the DirectX 8/9 SDK documentation offered with the MSDN library at the DirectX home page. If you are looking for more detailed information about DirectMIDI wrapper library, you will be able to obtain it in the on-line DirectMIDI developer's reference in the SourceForge project homepage and in the sources available for downloading in this article. 5. The Demo ApplicationThe demo application called MidiStation v1.9 is an easy-to-use program that shows all the features of the DirectMIDI wrapper class. You can change parameters like the MIDI port for output, select any GM instrument, change octaves, record your compositions and preview all notes and messages from an external keyboard or even play with the built-in MIDI keyboard. Enjoy the MIDI! 6. History
| ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||