aboutsummaryrefslogtreecommitdiff
path: root/desiredata/src/s_midi_mmio.c
blob: fc30be4bd6d659fc78ac2ce54e22928b35a71a06 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
/* Copyright (c) 1997-1999 Miller Puckette.
* For information on usage and redistribution, and for a DISCLAIMER OF ALL
* WARRANTIES, see the file, "LICENSE.txt," in this distribution.  */

#include "desire.h"
#include <stdio.h>
#include <windows.h>
#include <mmsystem.h>
using namespace desire;

/* ------------- MIDI time stamping from audio clock ------------ */
#ifdef MIDI_TIMESTAMP

static double msw_hibuftime;
static double initsystime = -1;

/* call this whenever we reset audio */
static void msw_resetmidisync() {
    initsystime = clock_getsystime();
    msw_hibuftime = sys_getrealtime();
}

/* call this whenever we're idled waiting for audio to be ready. The routine maintains a high and low water point
   for the difference between real and DAC time. */
static void msw_midisync() {
    if (initsystime == -1) msw_resetmidisync();
    double jittersec = max(msw_dacjitterbufsallowed,msw_adcjitterbufsallowed) * REALDACBLKSIZE / sys_getsr();
    double diff = sys_getrealtime() - 0.001 * clock_gettimesince(initsystime);
    if (diff > msw_hibuftime) msw_hibuftime = diff;
    if (diff < msw_hibuftime - jittersec) {
        post("jitter excess %d %f", dac, diff);
        msw_resetmidisync();
    }
}

static double msw_midigettimefor(LARGE_INTEGER timestamp) {
    /* this is broken now... used to work when "timestamp" was derived from QueryPerformanceCounter() instead of
       the MS-approved timeGetSystemTime() call in the MIDI callback routine below. */
    return msw_tixtotime(timestamp) - msw_hibuftime;
}
#endif /* MIDI_TIMESTAMP */

/* ------------------------- MIDI output -------------------------- */
static void msw_midiouterror(const char *s, int err) {
    char t[256];
    midiOutGetErrorText(err, t, 256);
    error(s,t);
}

static HMIDIOUT hMidiOut[MAXMIDIOUTDEV];    /* output device */
static int msw_nmidiout;                    /* number of devices */

static void msw_open_midiout(int nmidiout, int *midioutvec) {
    UINT result, wRtn;
    MIDIOUTCAPS midioutcaps;
    if (nmidiout > MAXMIDIOUTDEV) nmidiout = MAXMIDIOUTDEV;
    int dev = 0;
    for (int i=0; i<nmidiout; i++) {
        MIDIOUTCAPS m;
        int devno =  midioutvec[i];
        result = midiOutOpen(&hMidiOut[dev], devno, 0, 0, CALLBACK_NULL);
        wRtn = midiOutGetDevCaps(i, (LPMIDIOUTCAPS) &m, sizeof(m));
        if (result != MMSYSERR_NOERROR) {
            error("midiOutOpen: %s",midioutcaps.szPname);
            msw_midiouterror("midiOutOpen: %s", result);
        } else {
            if (sys_verbose) error("midiOutOpen: Open %s as Port %d", midioutcaps.szPname, dev);
            dev++;
        }
    }
    msw_nmidiout = dev;
}

static void msw_close_midiout() {
    for (int i=0; i<msw_nmidiout; i++) {
        midiOutReset(hMidiOut[i]);
        midiOutClose(hMidiOut[i]);
    }
    msw_nmidiout = 0;
}

/* -------------------------- MIDI input ---------------------------- */

#define INPUT_BUFFER_SIZE 1000     // size of input buffer in events

static void msw_midiinerror(const char *s, int err) {
    char t[256];
    midiInGetErrorText(err, t, 256);
    error(s,t);
}

/* Structure to represent a single MIDI event. */
#define EVNT_F_ERROR    0x00000001L
typedef struct evemsw_tag {
    DWORD fdwEvent;
    DWORD dwDevice;
    LARGE_INTEGER timestamp;
    DWORD data;
} EVENT;
typedef EVENT FAR *LPEVENT;

/* Structure to manage the circular input buffer. */
typedef struct circularBuffer_tag {
    HANDLE  hSelf;          /* handle to this structure */
    HANDLE  hBuffer;        /* buffer handle */
    WORD    wError;         /* error flags */
    DWORD   dwSize;         /* buffer size (in EVENTS) */
    DWORD   dwCount;        /* byte count (in EVENTS) */
    LPEVENT lpStart;        /* ptr to start of buffer */
    LPEVENT lpEnd;          /* ptr to end of buffer (last byte + 1) */
    LPEVENT lpHead;         /* ptr to head (next location to fill) */
    LPEVENT lpTail;         /* ptr to tail (next location to empty) */
} CIRCULARBUFFER;
typedef CIRCULARBUFFER FAR *LPCIRCULARBUFFER;

/* Structure to pass instance data from the application to the low-level callback function. */
typedef struct callbackInstance_tag {
    HANDLE              hSelf;
    DWORD               dwDevice;
    LPCIRCULARBUFFER    lpBuf;
} CALLBACKINSTANCEDATA;
typedef CALLBACKINSTANCEDATA FAR *LPCALLBACKINSTANCEDATA;

/* Function prototypes */
LPCALLBACKINSTANCEDATA FAR PASCAL AllocCallbackInstanceData();
void FAR PASCAL FreeCallbackInstanceData(LPCALLBACKINSTANCEDATA lpBuf);
LPCIRCULARBUFFER AllocCircularBuffer(DWORD dwSize);
void FreeCircularBuffer(LPCIRCULARBUFFER lpBuf);
WORD FAR PASCAL GetEvent(LPCIRCULARBUFFER lpBuf, LPEVENT lpEvent);

// Callback instance data pointers
LPCALLBACKINSTANCEDATA lpCallbackInstanceData[MAXMIDIINDEV];

UINT wNumDevices = 0;                    // Number of MIDI input devices opened
BOOL bRecordingEnabled = 1;             // Enable/disable recording flag
int  nNumBufferLines = 0;                // Number of lines in display buffer
RECT rectScrollClip;                    // Clipping rectangle for scrolling
LPCIRCULARBUFFER lpInputBuffer;         // Input buffer structure
EVENT incomingEvent;                    // Incoming MIDI event structure
MIDIINCAPS midiInCaps[MAXMIDIINDEV]; // Device capabilities structures
HMIDIIN hMidiIn[MAXMIDIINDEV];       // MIDI input device handles

/* AllocCallbackInstanceData - Allocates a CALLBACKINSTANCEDATA structure.  This structure is used to pass information
   to the low-level callback function, each time it receives a message. Because this structure is accessed by the
   low-level callback function, it must be allocated using GlobalAlloc() with the GMEM_SHARE and GMEM_MOVEABLE flags
   and page-locked with GlobalPageLock().
   Return:  A pointer to the allocated CALLBACKINSTANCE data structure. */
LPCALLBACKINSTANCEDATA FAR PASCAL AllocCallbackInstanceData() {
    HANDLE hMem;
    LPCALLBACKINSTANCEDATA lpBuf;
    /* Allocate and lock global memory. */
    hMem = GlobalAlloc(GMEM_SHARE | GMEM_MOVEABLE, (DWORD)sizeof(CALLBACKINSTANCEDATA));
    if(!hMem) return 0;
    lpBuf = (LPCALLBACKINSTANCEDATA)GlobalLock(hMem);
    if(!lpBuf) {GlobalFree(hMem); return 0;}
    /* Page lock the memory. */
    //GlobalPageLock((HGLOBAL)HIWORD(lpBuf));
    /* Save the handle. */
    lpBuf->hSelf = hMem;
    return lpBuf;
}

/* FreeCallbackInstanceData - Frees the given CALLBACKINSTANCEDATA structure.
 * Params:  lpBuf - Points to the CALLBACKINSTANCEDATA structure to be freed. */
void FAR PASCAL FreeCallbackInstanceData(LPCALLBACKINSTANCEDATA lpBuf) {
    HANDLE hMem;
    /* Save the handle until we're through here. */
    hMem = lpBuf->hSelf;
    /* Free the structure. */
    //GlobalPageUnlock((HGLOBAL)HIWORD(lpBuf));
    GlobalUnlock(hMem);
    GlobalFree(hMem);
}

/* AllocCircularBuffer -    Allocates memory for a CIRCULARBUFFER structure
 * and a buffer of the specified size.  Each memory block is allocated 
 * with GlobalAlloc() using GMEM_SHARE and GMEM_MOVEABLE flags, locked 
 * with GlobalLock(), and page-locked with GlobalPageLock().
 * Params:  dwSize - The size of the buffer, in events.
 * Return:  A pointer to a CIRCULARBUFFER structure identifying the allocated display buffer.  NULL if the buffer could not be allocated. */
 
LPCIRCULARBUFFER AllocCircularBuffer(DWORD dwSize) {
    HANDLE hMem;
    LPCIRCULARBUFFER lpBuf;
    LPEVENT lpMem;
    /* Allocate and lock a CIRCULARBUFFER structure. */
    hMem = GlobalAlloc(GMEM_SHARE | GMEM_MOVEABLE, (DWORD)sizeof(CIRCULARBUFFER));
    if(!hMem) return 0;
    lpBuf = (LPCIRCULARBUFFER)GlobalLock(hMem);
    if(!lpBuf) {GlobalFree(hMem); return 0;}
    /* Page lock the memory.  Global memory blocks accessed by low-level callback functions must be page locked. */
#ifndef _WIN32
    GlobalSmartPageLock((HGLOBAL)HIWORD(lpBuf));
#endif
    /* Save the memory handle. */
    lpBuf->hSelf = hMem;
    /* Allocate and lock memory for the actual buffer. */
    hMem = GlobalAlloc(GMEM_SHARE | GMEM_MOVEABLE, dwSize * sizeof(EVENT));
    if(!hMem) {
#ifndef _WIN32
        GlobalSmartPageUnlock((HGLOBAL)HIWORD(lpBuf));
#endif
        GlobalUnlock(lpBuf->hSelf);
        GlobalFree(lpBuf->hSelf);
        return 0;
    }
    lpMem = (LPEVENT)GlobalLock(hMem);
    if(!lpMem) {
        GlobalFree(hMem);
#ifndef _WIN32
        GlobalSmartPageUnlock((HGLOBAL)HIWORD(lpBuf));
#endif
        GlobalUnlock(lpBuf->hSelf);
        GlobalFree(lpBuf->hSelf);
        return NULL;
    }
    /* Page lock the memory.  Global memory blocks accessed by low-level callback functions must be page locked. */
#ifndef _WIN32
    GlobalSmartPageLock((HGLOBAL)HIWORD(lpMem));
#endif
    /* Set up the CIRCULARBUFFER structure. */
    lpBuf->hBuffer = hMem;
    lpBuf->wError = 0;
    lpBuf->dwSize = dwSize;
    lpBuf->dwCount = 0L;
    lpBuf->lpStart = lpMem;
    lpBuf->lpEnd = lpMem + dwSize;
    lpBuf->lpTail = lpMem;
    lpBuf->lpHead = lpMem;
    return lpBuf;
}

/* FreeCircularBuffer - Frees the memory for the given CIRCULARBUFFER structure and the memory for the buffer it references.
 * Params:  lpBuf - Points to the CIRCULARBUFFER to be freed. */
void FreeCircularBuffer(LPCIRCULARBUFFER lpBuf) {
    HANDLE hMem;
    /* Free the buffer itself. */
#ifndef _WIN32
    GlobalSmartPageUnlock((HGLOBAL)HIWORD(lpBuf->lpStart));
#endif
    GlobalUnlock(lpBuf->hBuffer);
    GlobalFree(lpBuf->hBuffer);
    /* Free the CIRCULARBUFFER structure. */
    hMem = lpBuf->hSelf;
#ifndef _WIN32
    GlobalSmartPageUnlock((HGLOBAL)HIWORD(lpBuf));
#endif
    GlobalUnlock(hMem);
    GlobalFree(hMem);
}

/* GetEvent - Gets a MIDI event from the circular input buffer.  Events
 *  are removed from the buffer.  The corresponding PutEvent() function
 *  is called by the low-level callback function, so it must reside in
 *  the callback DLL.  PutEvent() is defined in the CALLBACK.C module.
 *
 * Params:  lpBuf - Points to the circular buffer.
 *          lpEvent - Points to an EVENT structure that is filled with the retrieved event.
 * Return:  Returns non-zero if successful, zero if there are no events to get. */
WORD FAR PASCAL GetEvent(LPCIRCULARBUFFER lpBuf, LPEVENT lpEvent) {
    /* If no event available, return */
    if (!wNumDevices || lpBuf->dwCount <= 0) return 0;
    /* Get the event. */
    *lpEvent = *lpBuf->lpTail;
    /* Decrement the byte count, bump the tail pointer. */
    --lpBuf->dwCount;
    ++lpBuf->lpTail;
    /* Wrap the tail pointer, if necessary. */
    if (lpBuf->lpTail >= lpBuf->lpEnd) lpBuf->lpTail = lpBuf->lpStart;
    return 1;
}

/* PutEvent - Puts an EVENT in a CIRCULARBUFFER.  If the buffer is full, 
 *      it sets the wError element of the CIRCULARBUFFER structure 
 *      to be non-zero.
 *
 * Params:  lpBuf - Points to the CIRCULARBUFFER.
 *          lpEvent - Points to the EVENT. */

void FAR PASCAL PutEvent(LPCIRCULARBUFFER lpBuf, LPEVENT lpEvent) {
    /* If the buffer is full, set an error and return. */
    if(lpBuf->dwCount >= lpBuf->dwSize){
        lpBuf->wError = 1;
        return;
    }
    /* Put the event in the buffer, bump the head pointer and the byte count.  */
    *lpBuf->lpHead = *lpEvent;
    ++lpBuf->lpHead;
    ++lpBuf->dwCount;
    /* Wrap the head pointer, if necessary. */
    if(lpBuf->lpHead >= lpBuf->lpEnd) lpBuf->lpHead = lpBuf->lpStart;
}

/* midiInputHandler - Low-level callback function to handle MIDI input.
 *      Installed by midiInOpen().  The input handler takes incoming
 *      MIDI events and places them in the circular input buffer.  It then
 *      notifies the application by posting a MM_MIDIINPUT message.
 *
 *      This function is accessed at interrupt time, so it should be as 
 *      fast and efficient as possible.  You can't make any
 *      Windows calls here, except PostMessage().  The only Multimedia
 *      Windows call you can make are timeGetSystemTime(), midiOutShortMsg().
 *
 * Param:   hMidiIn - Handle for the associated input device.
 *          wMsg - One of the MIM_***** messages.
 *          dwInstance - Points to CALLBACKINSTANCEDATA structure.
 *          dwParam1 - MIDI data.
 *          dwParam2 - Timestamp (in milliseconds) */
void FAR PASCAL midiInputHandler(HMIDIIN hMidiIn, WORD wMsg, DWORD dwInstance, DWORD dwParam1, DWORD dwParam2) {
    EVENT event;
    switch(wMsg) {
        case MIM_OPEN: break;
        /* The only error possible is invalid MIDI data, so just pass the invalid data on so we'll see it. */
        case MIM_ERROR:
        case MIM_DATA:
            event.fdwEvent = (wMsg == MIM_ERROR) ? EVNT_F_ERROR : 0;
            event.dwDevice = ((LPCALLBACKINSTANCEDATA)dwInstance)->dwDevice;
            event.data = dwParam1;
#ifdef MIDI_TIMESTAMP
            event.timestamp = timeGetSystemTime();
#endif
            /* Put the MIDI event in the circular input buffer. */
            PutEvent(((LPCALLBACKINSTANCEDATA)dwInstance)->lpBuf, (LPEVENT)&event);
            break;
        default: break;
    }
}

void msw_open_midiin(int nmidiin, int *midiinvec) {
    UINT  wRtn;
    unsigned int i;
    unsigned int ndev = 0;
    /* Allocate a circular buffer for low-level MIDI input.  This buffer is filled by the low-level callback function
       and emptied by the application. */
    lpInputBuffer = AllocCircularBuffer((DWORD)(INPUT_BUFFER_SIZE));
    if (!lpInputBuffer) {
        printf("Not enough memory available for input buffer.\n");
        return;
    }
    /* Open all MIDI input devices after allocating and setting up instance data for each device.  The instance data is
       used to pass buffer management information between the application and the low-level callback function.  It also
       includes a device ID, a handle to the MIDI Mapper, and a handle to the application's display window, so the callback
       can notify the window when input data is available.  A single callback function is used to service all opened input devices. */
    for (i=0; (i<(unsigned)nmidiin) && (i<MAXMIDIINDEV); i++) {
        if ((lpCallbackInstanceData[ndev] = AllocCallbackInstanceData()) == NULL) {
            printf("Not enough memory available.\n");
            FreeCircularBuffer(lpInputBuffer);
            return;
        }
        lpCallbackInstanceData[i]->dwDevice = i;
        lpCallbackInstanceData[i]->lpBuf = lpInputBuffer;
        wRtn = midiInOpen((LPHMIDIIN)&hMidiIn[ndev], midiinvec[i], (DWORD)midiInputHandler,
            (DWORD)lpCallbackInstanceData[ndev], CALLBACK_FUNCTION);
        if (wRtn) {
            FreeCallbackInstanceData(lpCallbackInstanceData[ndev]);
            msw_midiinerror("midiInOpen: %s", wRtn);
        } else ndev++;
    }
    /* Start MIDI input. */
    for (i=0; i<ndev; i++) {
        if (hMidiIn[i]) midiInStart(hMidiIn[i]);
    }
    wNumDevices = ndev;
}

static void msw_close_midiin() {
    unsigned int i;
    /* Stop, reset, close MIDI input.  Free callback instance data. */
    for (i=0; (i<wNumDevices) && (i<MAXMIDIINDEV); i++) {
        if (hMidiIn[i]) {
            if (sys_verbose) post("closing MIDI input %d...", i);
            midiInStop(hMidiIn[i]);
            midiInReset(hMidiIn[i]);
            midiInClose(hMidiIn[i]);
            FreeCallbackInstanceData(lpCallbackInstanceData[i]);
        }
    }
    /* Free input buffer. */
    if (lpInputBuffer) FreeCircularBuffer(lpInputBuffer);
    if (sys_verbose) post("...done");
    wNumDevices = 0;
}

/* ------------------- public routines -------------------------- */

void sys_putmidimess(int portno, int a, int b, int c) {
    DWORD foo;
    MMRESULT res;
    if (portno >= 0 && portno < msw_nmidiout) {
        foo = (a & 0xff) | ((b & 0xff) << 8) | ((c & 0xff) << 16);
        res = midiOutShortMsg(hMidiOut[portno], foo);
        if (res != MMSYSERR_NOERROR) post("MIDI out error %d", res);
    }
}

void sys_putmidibyte(int portno, int byte) {
    MMRESULT res;
    if (portno >= 0 && portno < msw_nmidiout) {
        res = midiOutShortMsg(hMidiOut[portno], byte);
        if (res != MMSYSERR_NOERROR) post("MIDI out error %d", res);
    }
}

void sys_poll_midi() {
    static EVENT msw_nextevent;
    static int msw_isnextevent;
#ifdef MIDI_TIMESTAMP
    static double msw_nexteventtime;
#endif
    while (1) {
        if (!msw_isnextevent) {
            if (!GetEvent(lpInputBuffer, &msw_nextevent)) break;
            msw_isnextevent = 1;
#ifdef MIDI_TIMESTAMP
            msw_nexteventtime = msw_midigettimefor(&foo.timestamp);
#endif
        }
#ifdef MIDI_TIMESTAMP
        if (0.001 * clock_gettimesince(initsystime) >= msw_nexteventtime)
#endif
        {
            int msgtype = ((msw_nextevent.data & 0xf0) >> 4) - 8;
            int commandbyte = msw_nextevent.data & 0xff;
            int byte1 = (msw_nextevent.data >> 8) & 0xff;
            int byte2 = (msw_nextevent.data >> 16) & 0xff;
            int portno = msw_nextevent.dwDevice;
            switch (msgtype) {
            case 0: case 1: case 2: case 3: case 6:
                sys_midibytein(portno, commandbyte);
                sys_midibytein(portno, byte1);
                sys_midibytein(portno, byte2);
                break; 
            case 4: case 5:
                sys_midibytein(portno, commandbyte);
                sys_midibytein(portno, byte1);
                break;
            case 7:
                sys_midibytein(portno, commandbyte);
                break;
            }
            msw_isnextevent = 0;
        }
    }
}

void sys_do_open_midi(int nmidiin, int *midiinvec, int nmidiout, int *midioutvec) {
    if (nmidiout) msw_open_midiout(nmidiout, midioutvec);
    if (nmidiin) {
        post("Warning: midi input is dangerous in Microsoft Windows; see Pd manual)");
        msw_open_midiin(nmidiin, midiinvec);
    }
}

void sys_close_midi() {
    msw_close_midiin();
    msw_close_midiout();
}

#if 0
/* list the audio and MIDI device names */
void sys_listmididevs() {
    /* for MIDI and audio in and out, get the number of devices. Then get the capabilities of each device and print its description. */
    UINT ndevices = midiInGetNumDevs();
    for (unsigned i=0; i<ndevices; i++) {
        MIDIINCAPS  m; UINT wRtn = midiInGetDevCaps( i, (LPMIDIINCAPS)  &m, sizeof(m));
        if (wRtn) msw_midiinerror("midiInGetDevCaps: %s", wRtn); else error("MIDI input device #%d: %s", i+1, m.szPname);
    }
    ndevices = midiOutGetNumDevs();
    for (unsigned i=0; i<devices; i++) {
        MIDIOUTCAPS m; UINT wRtn = midiOutGetDevCaps(i, (LPMIDIOUTCAPS) &m, sizeof(m));
        if (wRtn) msw_midiouterror("midiOutGetDevCaps: %s", wRtn); else error("MIDI output device #%d: %s", i+1, m.szPname);
    }
}
#endif

void midi_getdevs(char *indevlist, int *nindevs, char *outdevlist, int *noutdevs, int maxndev, int devdescsize) {
    int  nin = min(maxndev,int( midiInGetNumDevs()));
    int nout = min(maxndev,int(midiOutGetNumDevs()));
    for (int i=0; i<nin; i++) {
        MIDIINCAPS m;  UINT wRtn =  midiInGetDevCaps(i, (LPMIDIINCAPS)  &m, sizeof(m));
        strncpy(indevlist  + i*devdescsize, (wRtn ? "???" : m.szPname), devdescsize);  indevlist[(i+1)*devdescsize - 1] = 0;}
    for (int i=0; i<nout; i++) {
        MIDIOUTCAPS m; UINT wRtn = midiOutGetDevCaps(i, (LPMIDIOUTCAPS) &m, sizeof(m));
        strncpy(outdevlist + i*devdescsize, (wRtn ? "???" : m.szPname), devdescsize); outdevlist[(i+1)*devdescsize - 1] = 0;}
    *nindevs = nin;
    *noutdevs = nout;
}