2007-05-30 21:56:52 +00:00
|
|
|
/* ScummVM - Graphic Adventure Engine
|
|
|
|
*
|
|
|
|
* ScummVM is the legal property of its developers, whose names
|
|
|
|
* are too numerous to list here. Please refer to the COPYRIGHT
|
|
|
|
* file distributed with this source distribution.
|
2003-05-18 14:25:33 +00:00
|
|
|
*
|
|
|
|
* This program is free software; you can redistribute it and/or
|
|
|
|
* modify it under the terms of the GNU General Public License
|
|
|
|
* as published by the Free Software Foundation; either version 2
|
|
|
|
* of the License, or (at your option) any later version.
|
|
|
|
|
|
|
|
* This program is distributed in the hope that it will be useful,
|
|
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
|
|
* GNU General Public License for more details.
|
|
|
|
|
|
|
|
* You should have received a copy of the GNU General Public License
|
|
|
|
* along with this program; if not, write to the Free Software
|
2005-10-18 01:30:26 +00:00
|
|
|
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
2003-05-18 14:25:33 +00:00
|
|
|
*
|
|
|
|
*/
|
|
|
|
|
2011-02-09 01:09:01 +00:00
|
|
|
#include "audio/midiparser.h"
|
2011-04-24 08:34:27 +00:00
|
|
|
#include "common/textconsole.h"
|
2003-05-18 14:25:33 +00:00
|
|
|
#include "common/util.h"
|
|
|
|
|
2003-10-03 23:34:06 +00:00
|
|
|
/**
|
|
|
|
* The XMIDI version of MidiParser.
|
2005-07-30 21:11:48 +00:00
|
|
|
*
|
2003-10-03 23:34:06 +00:00
|
|
|
* Much of this code is adapted from the XMIDI implementation from the exult
|
|
|
|
* project.
|
|
|
|
*/
|
2003-05-18 14:25:33 +00:00
|
|
|
class MidiParser_XMIDI : public MidiParser {
|
|
|
|
protected:
|
|
|
|
NoteTimer _notes_cache[32];
|
2003-05-19 18:48:18 +00:00
|
|
|
uint32 _inserted_delta; // Track simulated deltas for note-off events
|
2003-05-18 14:25:33 +00:00
|
|
|
|
2008-07-06 18:37:52 +00:00
|
|
|
struct Loop {
|
|
|
|
byte *pos;
|
|
|
|
byte repeat;
|
|
|
|
};
|
|
|
|
|
|
|
|
Loop _loop[4];
|
|
|
|
int _loopCount;
|
|
|
|
|
2008-08-10 17:59:42 +00:00
|
|
|
XMidiCallbackProc _callbackProc;
|
|
|
|
void *_callbackData;
|
|
|
|
|
2003-05-18 14:25:33 +00:00
|
|
|
protected:
|
2004-04-29 11:51:11 +00:00
|
|
|
uint32 readVLQ2(byte * &data);
|
2003-05-20 03:27:45 +00:00
|
|
|
void resetTracking();
|
2004-04-29 11:51:11 +00:00
|
|
|
void parseNextEvent(EventInfo &info);
|
2003-05-18 23:55:53 +00:00
|
|
|
|
2003-05-18 14:25:33 +00:00
|
|
|
public:
|
2008-08-10 17:59:42 +00:00
|
|
|
MidiParser_XMIDI(XMidiCallbackProc proc, void *data) : _inserted_delta(0), _callbackProc(proc), _callbackData(data) {}
|
2003-05-19 00:12:16 +00:00
|
|
|
~MidiParser_XMIDI() { }
|
|
|
|
|
2004-04-29 11:51:11 +00:00
|
|
|
bool loadMusic(byte *data, uint32 size);
|
2003-05-18 14:25:33 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// This is a special XMIDI variable length quantity
|
2004-04-29 11:51:11 +00:00
|
|
|
uint32 MidiParser_XMIDI::readVLQ2(byte * &pos) {
|
2003-05-18 14:25:33 +00:00
|
|
|
uint32 value = 0;
|
2005-09-11 14:35:34 +00:00
|
|
|
while (!(pos[0] & 0x80)) {
|
2003-05-18 14:25:33 +00:00
|
|
|
value += *pos++;
|
|
|
|
}
|
|
|
|
return value;
|
|
|
|
}
|
|
|
|
|
2004-04-29 11:51:11 +00:00
|
|
|
void MidiParser_XMIDI::parseNextEvent(EventInfo &info) {
|
2003-05-23 04:19:47 +00:00
|
|
|
info.start = _position._play_pos;
|
2004-04-29 11:51:11 +00:00
|
|
|
info.delta = readVLQ2(_position._play_pos) - _inserted_delta;
|
2003-05-18 23:55:53 +00:00
|
|
|
|
2003-05-19 18:48:18 +00:00
|
|
|
// Process the next event.
|
|
|
|
_inserted_delta = 0;
|
2003-05-23 04:19:47 +00:00
|
|
|
info.event = *(_position._play_pos++);
|
2003-05-19 18:48:18 +00:00
|
|
|
switch (info.event >> 4) {
|
|
|
|
case 0x9: // Note On
|
2003-05-23 04:19:47 +00:00
|
|
|
info.basic.param1 = *(_position._play_pos++);
|
|
|
|
info.basic.param2 = *(_position._play_pos++);
|
2004-04-29 11:51:11 +00:00
|
|
|
info.length = readVLQ(_position._play_pos);
|
2003-05-22 15:34:30 +00:00
|
|
|
if (info.basic.param2 == 0) {
|
|
|
|
info.event = info.channel() | 0x80;
|
|
|
|
info.length = 0;
|
2003-05-19 18:48:18 +00:00
|
|
|
}
|
|
|
|
break;
|
|
|
|
|
2008-07-09 10:42:47 +00:00
|
|
|
case 0xC:
|
|
|
|
case 0xD:
|
2003-05-23 04:19:47 +00:00
|
|
|
info.basic.param1 = *(_position._play_pos++);
|
2003-05-19 19:24:22 +00:00
|
|
|
info.basic.param2 = 0;
|
2003-05-19 18:48:18 +00:00
|
|
|
break;
|
|
|
|
|
2008-07-09 10:42:47 +00:00
|
|
|
case 0x8:
|
|
|
|
case 0xA:
|
|
|
|
case 0xE:
|
|
|
|
info.basic.param1 = *(_position._play_pos++);
|
|
|
|
info.basic.param2 = *(_position._play_pos++);
|
|
|
|
break;
|
|
|
|
|
2008-07-06 18:37:52 +00:00
|
|
|
case 0xB:
|
|
|
|
info.basic.param1 = *(_position._play_pos++);
|
|
|
|
info.basic.param2 = *(_position._play_pos++);
|
|
|
|
|
2008-08-10 17:59:42 +00:00
|
|
|
// This isn't a full XMIDI implementation, but it should
|
|
|
|
// hopefully be "good enough" for most things.
|
|
|
|
|
|
|
|
switch (info.basic.param1) {
|
2008-07-06 18:37:52 +00:00
|
|
|
// Simplified XMIDI looping.
|
2008-08-10 17:59:42 +00:00
|
|
|
case 0x74: { // XMIDI_CONTROLLER_FOR_LOOP
|
|
|
|
byte *pos = _position._play_pos;
|
|
|
|
if (_loopCount < ARRAYSIZE(_loop) - 1)
|
|
|
|
_loopCount++;
|
2009-08-08 13:57:21 +00:00
|
|
|
else
|
|
|
|
warning("XMIDI: Exceeding maximum loop count %d", ARRAYSIZE(_loop));
|
2008-08-10 17:59:42 +00:00
|
|
|
|
|
|
|
_loop[_loopCount].pos = pos;
|
|
|
|
_loop[_loopCount].repeat = info.basic.param2;
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
|
|
|
|
case 0x75: // XMIDI_CONTORLLER_NEXT_BREAK
|
2008-07-06 18:37:52 +00:00
|
|
|
if (_loopCount >= 0) {
|
|
|
|
if (info.basic.param2 < 64) {
|
2008-07-08 16:25:39 +00:00
|
|
|
// End the current loop.
|
2008-07-06 18:37:52 +00:00
|
|
|
_loopCount--;
|
2008-07-08 16:25:39 +00:00
|
|
|
} else {
|
2008-07-06 18:37:52 +00:00
|
|
|
// Repeat 0 means "loop forever".
|
|
|
|
if (_loop[_loopCount].repeat) {
|
|
|
|
if (--_loop[_loopCount].repeat == 0)
|
|
|
|
_loopCount--;
|
2009-08-08 13:57:21 +00:00
|
|
|
else
|
|
|
|
_position._play_pos = _loop[_loopCount].pos;
|
|
|
|
} else {
|
|
|
|
_position._play_pos = _loop[_loopCount].pos;
|
2008-07-06 18:37:52 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2008-08-10 17:59:42 +00:00
|
|
|
break;
|
|
|
|
|
|
|
|
case 0x77: // XMIDI_CONTROLLER_CALLBACK_TRIG
|
|
|
|
if (_callbackProc)
|
|
|
|
_callbackProc(info.basic.param2, _callbackData);
|
|
|
|
break;
|
|
|
|
|
2009-05-06 14:22:13 +00:00
|
|
|
case 0x6e: // XMIDI_CONTROLLER_CHAN_LOCK
|
|
|
|
case 0x6f: // XMIDI_CONTROLLER_CHAN_LOCK_PROT
|
|
|
|
case 0x70: // XMIDI_CONTROLLER_VOICE_PROT
|
|
|
|
case 0x71: // XMIDI_CONTROLLER_TIMBRE_PROT
|
|
|
|
case 0x72: // XMIDI_CONTROLLER_BANK_CHANGE
|
|
|
|
case 0x73: // XMIDI_CONTROLLER_IND_CTRL_PREFIX
|
|
|
|
case 0x76: // XMIDI_CONTROLLER_CLEAR_BB_COUNT
|
|
|
|
case 0x78: // XMIDI_CONTROLLER_SEQ_BRANCH_INDEX
|
2008-08-10 17:59:42 +00:00
|
|
|
default:
|
|
|
|
if (info.basic.param1 >= 0x6e && info.basic.param1 <= 0x78) {
|
|
|
|
warning("Unsupported XMIDI controller %d (0x%2x)",
|
|
|
|
info.basic.param1, info.basic.param1);
|
|
|
|
}
|
2008-07-06 18:37:52 +00:00
|
|
|
}
|
2008-08-10 17:59:42 +00:00
|
|
|
|
|
|
|
// Should we really keep passing the XMIDI controller events to
|
|
|
|
// the MIDI driver, or should we turn them into some kind of
|
|
|
|
// NOP events? (Dummy meta events, perhaps?) Ah well, it has
|
|
|
|
// worked so far, so it shouldn't cause any damage...
|
|
|
|
|
2008-07-06 18:37:52 +00:00
|
|
|
break;
|
|
|
|
|
2003-05-19 18:48:18 +00:00
|
|
|
case 0xF: // Meta or SysEx event
|
|
|
|
switch (info.event & 0x0F) {
|
|
|
|
case 0x2: // Song Position Pointer
|
2003-05-23 04:19:47 +00:00
|
|
|
info.basic.param1 = *(_position._play_pos++);
|
|
|
|
info.basic.param2 = *(_position._play_pos++);
|
2003-05-18 14:25:33 +00:00
|
|
|
break;
|
|
|
|
|
2003-05-19 18:48:18 +00:00
|
|
|
case 0x3: // Song Select
|
2003-05-23 04:19:47 +00:00
|
|
|
info.basic.param1 = *(_position._play_pos++);
|
2003-05-19 19:24:22 +00:00
|
|
|
info.basic.param2 = 0;
|
2003-05-18 14:25:33 +00:00
|
|
|
break;
|
|
|
|
|
2008-07-09 10:42:47 +00:00
|
|
|
case 0x6:
|
|
|
|
case 0x8:
|
|
|
|
case 0xA:
|
|
|
|
case 0xB:
|
|
|
|
case 0xC:
|
|
|
|
case 0xE:
|
2003-05-19 19:24:22 +00:00
|
|
|
info.basic.param1 = info.basic.param2 = 0;
|
2003-05-18 14:25:33 +00:00
|
|
|
break;
|
|
|
|
|
2003-05-19 18:48:18 +00:00
|
|
|
case 0x0: // SysEx
|
2004-04-29 11:51:11 +00:00
|
|
|
info.length = readVLQ(_position._play_pos);
|
2003-05-23 04:19:47 +00:00
|
|
|
info.ext.data = _position._play_pos;
|
|
|
|
_position._play_pos += info.length;
|
2003-05-19 18:48:18 +00:00
|
|
|
break;
|
2003-05-18 14:50:10 +00:00
|
|
|
|
2003-05-19 18:48:18 +00:00
|
|
|
case 0xF: // META event
|
2003-05-23 04:19:47 +00:00
|
|
|
info.ext.type = *(_position._play_pos++);
|
2004-04-29 11:51:11 +00:00
|
|
|
info.length = readVLQ(_position._play_pos);
|
2003-05-23 04:19:47 +00:00
|
|
|
info.ext.data = _position._play_pos;
|
|
|
|
_position._play_pos += info.length;
|
2003-05-22 15:34:30 +00:00
|
|
|
if (info.ext.type == 0x51 && info.length == 3) {
|
2003-05-19 18:48:18 +00:00
|
|
|
// Tempo event. We want to make these constant 500,000.
|
2003-05-19 19:24:22 +00:00
|
|
|
info.ext.data[0] = 0x07;
|
|
|
|
info.ext.data[1] = 0xA1;
|
|
|
|
info.ext.data[2] = 0x20;
|
2003-05-18 14:25:33 +00:00
|
|
|
}
|
2003-05-19 18:48:18 +00:00
|
|
|
break;
|
2007-09-19 08:40:12 +00:00
|
|
|
|
2007-02-16 13:30:41 +00:00
|
|
|
default:
|
|
|
|
warning("MidiParser_XMIDI::parseNextEvent: Unsupported event code %x", info.event);
|
2003-05-18 14:25:33 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2004-04-29 11:51:11 +00:00
|
|
|
bool MidiParser_XMIDI::loadMusic(byte *data, uint32 size) {
|
2003-05-18 14:25:33 +00:00
|
|
|
uint32 i = 0;
|
|
|
|
byte *start;
|
|
|
|
uint32 len;
|
|
|
|
uint32 chunk_len;
|
|
|
|
char buf[32];
|
|
|
|
|
2008-07-06 18:37:52 +00:00
|
|
|
_loopCount = -1;
|
|
|
|
|
2003-05-18 14:25:33 +00:00
|
|
|
unloadMusic();
|
|
|
|
byte *pos = data;
|
|
|
|
|
2004-04-29 11:51:11 +00:00
|
|
|
if (!memcmp(pos, "FORM", 4)) {
|
2003-05-18 14:25:33 +00:00
|
|
|
pos += 4;
|
|
|
|
|
2005-07-30 21:11:48 +00:00
|
|
|
// Read length of
|
2004-04-29 11:51:11 +00:00
|
|
|
len = read4high(pos);
|
2003-05-18 14:25:33 +00:00
|
|
|
start = pos;
|
2003-11-08 23:05:04 +00:00
|
|
|
|
2003-05-18 14:25:33 +00:00
|
|
|
// XDIRless XMIDI, we can handle them here.
|
2005-07-30 21:11:48 +00:00
|
|
|
if (!memcmp(pos, "XMID", 4)) {
|
2004-04-29 11:51:11 +00:00
|
|
|
warning("XMIDI doesn't have XDIR");
|
2003-05-18 14:25:33 +00:00
|
|
|
pos += 4;
|
|
|
|
_num_tracks = 1;
|
2004-04-29 11:51:11 +00:00
|
|
|
} else if (memcmp(pos, "XDIR", 4)) {
|
2011-05-25 14:48:51 +00:00
|
|
|
// Not an XMIDI that we recognize
|
2004-04-29 11:51:11 +00:00
|
|
|
warning("Expected 'XDIR' but found '%c%c%c%c'", pos[0], pos[1], pos[2], pos[3]);
|
2003-05-18 14:25:33 +00:00
|
|
|
return false;
|
|
|
|
} else {
|
|
|
|
// Seems Valid
|
|
|
|
pos += 4;
|
|
|
|
_num_tracks = 0;
|
|
|
|
|
|
|
|
for (i = 4; i < len; i++) {
|
|
|
|
// Read 4 bytes of type
|
2004-04-29 11:51:11 +00:00
|
|
|
memcpy(buf, pos, 4);
|
2003-05-18 14:25:33 +00:00
|
|
|
pos += 4;
|
|
|
|
|
|
|
|
// Read length of chunk
|
2004-04-29 11:51:11 +00:00
|
|
|
chunk_len = read4high(pos);
|
2003-11-08 23:05:04 +00:00
|
|
|
|
2003-05-18 14:25:33 +00:00
|
|
|
// Add eight bytes
|
|
|
|
i += 8;
|
2003-11-08 23:05:04 +00:00
|
|
|
|
2011-06-09 16:16:14 +00:00
|
|
|
if (memcmp(buf, "INFO", 4) == 0) {
|
|
|
|
// Must be at least 2 bytes long
|
|
|
|
if (chunk_len < 2) {
|
|
|
|
warning("Invalid chunk length %d for 'INFO' block", (int)chunk_len);
|
|
|
|
return false;
|
|
|
|
}
|
2003-11-08 23:05:04 +00:00
|
|
|
|
2011-06-09 16:16:14 +00:00
|
|
|
_num_tracks = (byte)read2low(pos);
|
2003-05-18 14:25:33 +00:00
|
|
|
|
2011-06-09 16:16:14 +00:00
|
|
|
if (chunk_len > 2) {
|
|
|
|
warning("Chunk length %d is greater than 2", (int)chunk_len);
|
|
|
|
//pos += chunk_len - 2;
|
|
|
|
}
|
|
|
|
break;
|
2003-05-18 14:25:33 +00:00
|
|
|
}
|
2011-06-09 16:16:14 +00:00
|
|
|
|
|
|
|
// Must align
|
|
|
|
pos += (chunk_len + 1) & ~1;
|
|
|
|
i += (chunk_len + 1) & ~1;
|
2003-05-18 14:25:33 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// Didn't get to fill the header
|
|
|
|
if (_num_tracks == 0) {
|
2004-04-29 11:51:11 +00:00
|
|
|
warning("Didn't find a valid track count");
|
2003-05-18 14:25:33 +00:00
|
|
|
return false;
|
|
|
|
}
|
2003-11-08 23:05:04 +00:00
|
|
|
|
2003-05-18 14:25:33 +00:00
|
|
|
// Ok now to start part 2
|
|
|
|
// Goto the right place
|
|
|
|
pos = start + ((len + 1) & ~1);
|
2003-11-08 23:05:04 +00:00
|
|
|
|
2004-04-29 11:51:11 +00:00
|
|
|
if (memcmp(pos, "CAT ", 4)) {
|
2003-05-18 14:25:33 +00:00
|
|
|
// Not an XMID
|
2004-04-29 11:51:11 +00:00
|
|
|
warning("Expected 'CAT ' but found '%c%c%c%c'", pos[0], pos[1], pos[2], pos[3]);
|
2003-05-18 14:25:33 +00:00
|
|
|
return false;
|
|
|
|
}
|
|
|
|
pos += 4;
|
2003-11-08 23:05:04 +00:00
|
|
|
|
2003-05-18 14:25:33 +00:00
|
|
|
// Now read length of this track
|
2004-04-29 11:51:11 +00:00
|
|
|
len = read4high(pos);
|
2003-11-08 23:05:04 +00:00
|
|
|
|
2004-04-29 11:51:11 +00:00
|
|
|
if (memcmp(pos, "XMID", 4)) {
|
2003-05-18 14:25:33 +00:00
|
|
|
// Not an XMID
|
2004-04-29 11:51:11 +00:00
|
|
|
warning("Expected 'XMID' but found '%c%c%c%c'", pos[0], pos[1], pos[2], pos[3]);
|
2003-05-18 14:25:33 +00:00
|
|
|
return false;
|
|
|
|
}
|
|
|
|
pos += 4;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
// Ok it's an XMIDI.
|
|
|
|
// We're going to identify and store the location for each track.
|
2003-05-23 04:19:47 +00:00
|
|
|
if (_num_tracks > ARRAYSIZE(_tracks)) {
|
2005-05-27 12:43:19 +00:00
|
|
|
warning("Can only handle %d tracks but was handed %d", (int)ARRAYSIZE(_tracks), (int)_num_tracks);
|
2003-05-18 14:25:33 +00:00
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
int tracks_read = 0;
|
|
|
|
while (tracks_read < _num_tracks) {
|
2004-04-29 11:51:11 +00:00
|
|
|
if (!memcmp(pos, "FORM", 4)) {
|
2003-05-18 14:25:33 +00:00
|
|
|
// Skip this plus the 4 bytes after it.
|
|
|
|
pos += 8;
|
2004-04-29 11:51:11 +00:00
|
|
|
} else if (!memcmp(pos, "XMID", 4)) {
|
2003-05-18 14:25:33 +00:00
|
|
|
// Skip this.
|
|
|
|
pos += 4;
|
2004-04-29 11:51:11 +00:00
|
|
|
} else if (!memcmp(pos, "TIMB", 4)) {
|
2003-05-18 14:25:33 +00:00
|
|
|
// Custom timbres?
|
|
|
|
// We don't support them.
|
|
|
|
// Read the length, skip it, and hope there was nothing there.
|
|
|
|
pos += 4;
|
2004-04-29 11:51:11 +00:00
|
|
|
len = read4high(pos);
|
2003-05-18 14:25:33 +00:00
|
|
|
pos += (len + 1) & ~1;
|
2004-04-29 11:51:11 +00:00
|
|
|
} else if (!memcmp(pos, "EVNT", 4)) {
|
2003-05-18 14:25:33 +00:00
|
|
|
// Ahh! What we're looking for at last.
|
|
|
|
_tracks[tracks_read] = pos + 8; // Skip the EVNT and length bytes
|
|
|
|
pos += 4;
|
2004-04-29 11:51:11 +00:00
|
|
|
len = read4high(pos);
|
2003-05-18 14:25:33 +00:00
|
|
|
pos += (len + 1) & ~1;
|
|
|
|
++tracks_read;
|
|
|
|
} else {
|
2004-04-29 11:51:11 +00:00
|
|
|
warning("Hit invalid block '%c%c%c%c' while scanning for track locations", pos[0], pos[1], pos[2], pos[3]);
|
2003-05-18 14:25:33 +00:00
|
|
|
return false;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// If we got this far, we successfully established
|
|
|
|
// the locations for each of our tracks.
|
|
|
|
// Note that we assume the original data passed in
|
|
|
|
// will persist beyond this call, i.e. we do NOT
|
|
|
|
// copy the data to our own buffer. Take warning....
|
2003-05-19 18:48:18 +00:00
|
|
|
_ppqn = 60;
|
|
|
|
resetTracking();
|
2004-04-29 11:51:11 +00:00
|
|
|
setTempo(500000);
|
2003-05-19 18:48:18 +00:00
|
|
|
_inserted_delta = 0;
|
2004-04-29 11:51:11 +00:00
|
|
|
setTrack(0);
|
2003-05-18 14:25:33 +00:00
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
2003-05-20 03:27:45 +00:00
|
|
|
void MidiParser_XMIDI::resetTracking() {
|
|
|
|
MidiParser::resetTracking();
|
|
|
|
_inserted_delta = 0;
|
|
|
|
}
|
|
|
|
|
2008-08-10 17:59:42 +00:00
|
|
|
void MidiParser::defaultXMidiCallback(byte eventData, void *data) {
|
|
|
|
warning("MidiParser: defaultXMidiCallback(%d)", eventData);
|
|
|
|
}
|
|
|
|
|
|
|
|
MidiParser *MidiParser::createParser_XMIDI(XMidiCallbackProc proc, void *data) {
|
|
|
|
return new MidiParser_XMIDI(proc, data);
|
|
|
|
}
|