• No se han encontrado resultados

Los procesos infinitos y sus paradojas

In document Organizaci´ on de la conferencia (página 41-188)

Opening and Using MIDI Input 59 Opening in Greater Detail 62 Controlling MIDI Input 64 The Input Queue 65 Closing the MIDI Input 67 Inside MIDI Input 68

Page 59

MIDI input--as implemented in the MaxMidi ToolKit--is very similar to MIDI output. The functions that identify, open, and close these two kinds of devices are almost identical. Likewise, both input and output devices use the same MidiEvent structure for handling MIDI data.

But there are important differences between MIDI input and output. From a program's point of view, the input process is passive--the program waits patiently for a MIDI event to arrive. On the other hand, output is active--events are sent as a result of direct action by the program. These differences are handled using two mechanisms: the application controls MIDI input by turning on or off the input device, and window messages notify the application when MIDI events are received. The program's ability to apply brakes to MIDI input ensures that events arrive only when it is ready to receive them. When events are

received, they are retrieved from MaxMidi in response to window messages. Thus, the application becomes an active partner in handling MIDI input.

Opening and Using MIDI Input

The steps needed to open an input device are almost the same we used to open an output device. Let's write a short example to illustrate the process. This program will receive MIDI events and echo them out to a MIDI output device. For the sake of simplicity, the program will open the first available input and output devices (using device IDs of 0). Here we go!

//

// Maximum MIDI -- Chapter 5: Receiving MIDI // Echo Example Program //

#include <windows.h>

#include "MaxMidi.h"

#define INPUT_DEVICE 0

#define OUTPUT_DEVICE 0 HMIN hMidiIn = 0;

HMOUT hMidiOut = 0;

LRESULT CALLBACK EchoWndProc(HWND hWnd, UINT iMsg, WPARAM wParam, LPARAM lParam) {

LPMIDIEVENT lpMsg;

switch (iMsg)

// All messages that are not completely processed above // must be processed here.

return DefWindowProc(hWnd, iMsg, wParam, lParam);

}

int PASCAL WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpszCmdLine, int nCmdShow)

wndclass.lpfnWndProc = EchoWndProc;

wndclass.hbrBackground = (HBRUSH)(COLOR_WINDOW+1);

wndclass.lpszMenuName = NULL;

wndclass.lpszClassName = "Ch5EchoExample";

RegisterClass(&wndclass);

// Create and display the main window hWnd = CreateWindow("Ch5EchoExample", "Maximum MIDI Echo Example", while(GetMessage(&msg, NULL, 0, 0)) {

Inspection of our deceptively simple program reveals several characteristics. WinMain, and its attendant message loop, create and maintain the main window. The window proc, EchoWndProc, processes four different messages. The WM_CREATE message handler attempts to open the MIDI input and output devices. If either one cannot be opened (if, for example, one does not exist), the handler will return -1, forcing the application to terminate, since the main window will not be created.

Once the main window is displayed, the program waits for something interesting to happen. Whenever a MIDI event is received by the input device, MaxMidi posts a MIDI_DATA message to our window. The GetMessage() function (in WinMain) retrieves it from Window's message queue and dispatches it to the window procedure. There, the MIDI_DATA case calls the MaxMidi function, GetMidiIn(), to retrieve the MIDI data. Each time GetMidiIn() is called, it returns a pointer to the next MIDI event (contained in a MidiEvent structure). The function returns a NULL pointer if there are no events to retrieve.

Each event that is retrieved using GetMidiIn() is echoed to the output device by calling PutMidiOut(). This method of echoing MIDI events allows an application to

Page 62

easily filter, modify, or route events as it sees fit. But it is not without drawbacks. Since the events are retrieved in response to window messages, there will be some delay. But this delay is generally small, and for the purposes of our example program it is not a problem. A more timing-sensitive program might echo events from inside of the input device's interrupt-time callback function. The MaxMidi DLLs don't implement such an echo function, but an intrepid programmer can easily add this

application-specific feature to the DLLs.

The other two message cases handled in the window procedure come into play during program shutdown. A WM_CLOSE message is sent to the window proc when the program begins to close. In this example, the WM_CLOSE handler's response is to close the two MIDI devices. It then calls DestroyWindow(), which removes the window from the screen and sends a WM_DESTROY message. This final message is handled in the usual manner: a quit message is sent which causes the message loop in WinMain to exit, causing the application to terminate.

Opening in Greater Detail

As the example shows, the OpenMidiIn() function accepts the same parameters as OpenMidiOut(). A window handle identifies the window that handles the MIDI_DATA and MIM_CLOSE messages. We've seen MIDI_DATA in action already.

MIM_CLOSE, like the MOM_CLOSE message, is sent when the device is closed. Most applications ignore this message, but it can be useful. For example, during shutdown it can be used to pace the orderly release of resources that are related to a particular input device. The OpenMidiIn() function is prototyped like this:

HMIN OpenMidiIn(HWND hWnd, WORD wDeviceID, HSYNC hSync, DWORD dwFlags);

The device ID serves the same function as the output device's ID (as used in OpenMidiOut()): it uniquely identifies a particular device. The device ID ranges from 0 (for the first device) to one less than the number of devices present in the system. The number of available devices is returned by the MMSYSTEM API function midiInGetNumDevs(). As in the output device case, the device ID for a particular input device is always unique, but can change from session to session. The ID will never change during the lifetime of an application; the value can change--as a result of device drivers being added or removed--only when Windows starts.

As a consequence of this behavior, applications should not save the device ID as an identification for reopening the device later. The ID may have changed, causing the

Page 63

program to open the wrong device. Instead, save the description of the device, as returned by GetMidiInDescription ().

BOOL GetMidiInDescription(WORD wDeviceID, LPSTR lpszDesc);

This function gets the name of the specified device and copies it to the string that is pointed to by lpszDesc. Make sure that lpszDesc points to enough memory to hold the entire name, which will be no longer than MAXPNAMELEN (32) bytes.

GetMidiInDescription() returns TRUE if the device exists and FALSE if it does not. But avoid attempting to retrieve the name of a device that is outside the range of devices returned by midiInGetNumDevs(), or the system may become unstable.

Getting the device ID when we have the device name is almost as easy. This function will appear later in the CMaxMidiIn class. Here is a C function that does the trick:

WORD GetIDFromName(LPSTR lpszDesc) {

WORD id;

char thisDesc[MAXPNAMELEN];

WORD MaxDevs = midiInGetNumDevs();

for(id = 0; id < MaxDevs; id++) {

GetMidiInDescription(id, thisDesc);

if(strcmp(thisDesc, lpszDesc) == 0) return id;

id++;

}

return ERR_NOMATCH;

}

The function returns an ID of ERR_NOMATCH if it cannot find a match. This usually means that the device has been removed from the system. A gentle word to the user, and a chance to choose another device, will help correct the problem.

The next parameter to OpenMidiIn() is a handle to a sync device. If the handle passed is nonzero, the sync device will timestamp each received event with the time in ticks since the last received event. Synchronization, timestamps, and ticks are discussed in elaborate detail in chapter 7. If no synchronization is desired, pass 0 in place of the sync device handle. In this case, each received event is still timestamped, but with the number of milliseconds since MIDI input was started.

Last, but not least, are a set of flags (contained in bits 12-15 of the dwFlags parameter) that allow an application to customize the behavior of the input device. The MIDI

Page 64

input device stores received events in one or more queues. The default size of the non-sysex event queue is 512 events. Other queue sizes can be selected using one of the QUEUE_ flags. These flags are the same as the ones used when opening MIDI output, and are shown in table 4.1. However, the default queue works well for most applications. Likewise, if

ENABLE_SYSEX is specified, the size and number of System Exclusive buffers can be set using the SXBUF_ group of flags.

If ENABLE_SYSEX is not set, then these extra buffers are not created (thus saving memory) and any received sysex messages are ignored. These flags, and sysex handling in general, are discussed in chapter 6.

If the open request is successful, the function returns a handle to the MIDI input devce, an HMIN. This handle is opaque. That is, it is just a numerical value and has no meaning in your application except to uniquely identify a particular output device.

The Inside MIDI Input section below reveals how this handle is used inside the MaxMidi DLLs.

Controlling MIDI Input

Simply opening a MIDI input device is not enough; input must also be enabled for the application to receive events. Events are ignored by the device--as if they were never received--until the application calls StartMidiIn(). If no sync device is attached, each received event is timestamped with the number of milliseconds since StartMidiIn() was last called. If synchronization is being used, the value of the timestamp is controlled by the sync device (attached by specifying a valid sync handle when opening the input device), as discussed in chapter 7.

Whenever the application needs to temporarily halt MIDI input, it calls StopMidiIn(). In response, the MaxMidi DLLs will ignore further MIDI events. The application can continue to retrieve any queued events from MaxMidi by calling GetMidiIn(). For example, a sequencer needs to stop input when not recording so that it doesn't continue to process MIDI_DATA messages. If it simply ignores the MIDI_DATA messages while stopped--and leaves MIDI input running--when it starts to record again, it may erroneously insert a queue-full of garbage events at the beginning of the new sequence.

Alternatively, MIDI input could remain enabled while the sequencer is stopped if any queued events are flushed before recording. This is useful when the sequencer is set to echo received events to a selected output. By continuing to process received MIDI events, the user is able to play "thru" the sequencer. Flushing events that are queued while the sequencer is not recording ensures that subsequent recording begins cleanly.

Page 65

The Input Queue

A queue allows one process--receiving data, in this case--to be decoupled from another process--such as doing something with the data. The two tasks are then asynchronous; they don't need to happen at the same time. In the case of the MaxMidi DLLs, data is received from the hardware MIDI interface--through the interface's device driver--at interrupt time. But, it would be almost impossible to do anything useful with the data if processing could only happen at interrupt time. An interrupt-time callback function, running in a 16-bit process (as it must for handling time-critical events such as MIDI), cannot call any disk I/O, memory allocation, or screen drawing functions. Unfortunately, those are exactly the kind of functions we want to call in response to received MIDI events. Luckily, circular queues come to the rescue.

When the input device receives an event, it is put into a MidiEvent structure and stored in a circular queue. The size of the queue is set, using the flags shown in table 4.1, when the device is opened. The default size is 512 events.

A circular queue is a memory buffer that wraps around, giving the appearance of an infinitely long buffer. Such a queue has an input pointer (that locates the next available storage location) and an output pointer (that locates the next location to read from the queue). When data is written to the queue, the input pointer indicates where in the queue the data will be stored. The pointer is then incremented to the next location. If the pointer reaches the end of the queue, it is repositioned to the beginning of the queue. Likewise, when reading data from the queue, the output pointer indicates the location to read the next piece of data. After the data is read from the queue, the output pointer is incremented, and wrapped around to the beginning of the queue, if necessary. Around and around the pointers go, with data being added to the queue by one part of a program and removed by another.

It is said that a greyhound--the racing dog, not the bus--must never catch the rabbit that it chases around the track. If the dog ever does catch the rabbit, it will be so disappointed to find the rabbit is nothing but a stuffed animal that it will lose the will to run. Handled with a good, strong leash, these racing retirees make excellent house pets: clean, quiet, but with a certain wistful melancholy.

Figure 5-1 A queue

Page 66

Like the greyhound, if the input pointer ever catches up with the output pointer the queue's career is over. This happens when data is being removed from the queue slower (over time) than it is being added to the queue; the queue is then said to be overrun. A queue can handle this condition in one of two ways. It can refuse or ignore new data until there is room in the queue, or it can rely on the rest of the system to prevent an overrun condition.

On the other hand, the queue is empty whenever the input and output pointers are equal. The pointers are equal when the output pointer (where data are being read) reaches the input pointer (where data will be written). Thus, all of the data in the queue has been read when the output pointer reaches the input pointer. Remember that the input pointer must never reach the output pointer, since this results in an overrun queue.

The input pointer is incremented each time an event is inserted into the queue. The pointer is reset to the head of the queue when it reaches the end of the queue, wrapping it around to the beginning. On the other hand, if the input pointer would equal the output pointer when incremented (and possibly wrapped around), the event should be refused, or else an overrun would occur, since the queue is already full.

Events are removed from the queue in a similar manner. There are no events to retrieve if the input and output pointers are the same. But if they are different, the output pointer is used to retrieve the event data from the queue. Then, the pointer is incremented and wrapped around to the head of the queue, if necessary, in the same way the input pointer is handled.

Events are put into the receive queue, in the form of MidiEvent structures, when they are received from the MIDI interface's device driver. For non-sysex messages, the device driver calls a callback function in the MaxMidi DLLs--at interrupt time--and passes the entire MIDI message. Events coming from a device driver will always have a valid status byte, accompanied by all of the data bytes that were received for that message. The callback function in MaxMidi puts the received event into the queue--incrementing and wrapping the input pointer, if necessary--and posts a MIDI_DATA message to let the client application know there is an event to process.

Using the MaxMidi DLL functions, events are retrieved from the input queue by calling GetMidiIn(). Normally, this function is called in response to a MIDI_DATA message. Since the MIDI_DATA window message passes through the Windows event queue--and thus through the application's message queue--it may take some time for the message to be processed. There are no ill effects caused by this delay, since the MIDI data is waiting patiently to be retrieved from the MIDI input queue. But, it is possible that more than one MIDI event can be received and queued before the first MIDI_DATA message is processed. To optimize the performance of MIDI input and prevent data loss, call GetMidiIn()

Page 67

inside of a loop. This will quickly retrieve all events that are waiting in the MIDI input queue and ensure that events are removed from the queue as quickly as they are inserted.

For each retrieved event, GetMidiIn() will return a pointer to a MidiEvent structure. Use this pointer to read the members of the structure, but don't hold on to or write data using this pointer. Since it is a pointer into the MIDI input queue inside of MaxMidi, the data at this address will eventually be overwritten by a newly received MIDI event. Process the event immediately, or if necessary, copy the data for later processing. If there are no events in the queue, GetMidiIn() returns a null pointer.

Closing the MIDI Input

Just as with MIDI output devices, input devices that are left open when an application terminates will cause annoying memory leaks. And just as in the case of MIDI output, orphaned input devices will prevent other applications from accessing the MIDI input hardware. The only remedy for an orphaned MIDI device, other than a sharp slap on the wrist, is to reboot the computer.

Since users generally classify that sort of behavior in the same category as tax audits and traffic tickets, always be sure to close the input device.

Closing the MIDI input device is done by calling CloseMidiIn(). This function will close the MIDI input driver and release the memory that MaxMidi allocated for the device. It is always a good idea to set the device handle to 0 after closing the device, since MaxMidi will ignore any calls made with a null device handle. Never attempt to close a device that has already been closed. Otherwise, the close function will attempt to free memory that has already been freed. Since ToolKit functions ignore null handles, setting the handle to 0 after closing the device is a good safety practice. For example:

CloseMidiIn(hMidiIn);

hMidiIn = 0; // for safety!

That is all that's involved with receiving MIDI input using the MaxMidi ToolKit. Now, MIDI input and output are just a few function calls away. But read on to understand what goes on underneath the hood of the MaxMidi input routines. Although you don't need to know more to use these functions, a firm understanding of how MaxMidi handles the Windows multimedia API for MIDI input will help make using and modifying the ToolKit even easier.

Page 68

Inside MIDI Input

Like the MIDI output routines in the ToolKit, the input functions must reside in fixed code segments. The device driver that handles the MIDI hardware sends data to the ToolKit at interrupt time via a callback. Since the callback can be called at any time, the code segment for the callback function must always be present in memory and must never move. The only way to guarantee this is to put the code into a fixed segment. And the easiest way to do this is to put the code into a DLL and set the

FIXED attribute for its code and data segments.

FIXED attribute for its code and data segments.

In document Organizaci´ on de la conferencia (página 41-188)

Documento similar