568 lines
15 KiB
C
568 lines
15 KiB
C
#include <sys/time.h> // gettimeofday()
|
|
#include <unistd.h> // usleep()
|
|
//#include <time.h> // clock_gettime()
|
|
#include "cmPrefix.h"
|
|
#include "cmGlobal.h"
|
|
#include "cmRpt.h"
|
|
#include "cmErr.h"
|
|
#include "cmCtx.h"
|
|
#include "cmMem.h"
|
|
#include "cmMallocDebug.h"
|
|
#include "cmFile.h"
|
|
#include "cmMidi.h"
|
|
#include "cmMidiPort.h"
|
|
#include "cmMidiFile.h"
|
|
#include "cmMidiFilePlay.h"
|
|
|
|
#ifdef OS_OSX
|
|
#include "osx/clock_gettime_stub.h"
|
|
#endif
|
|
|
|
#ifdef OS_LINUX
|
|
#include <time.h> // clock_gettime()
|
|
#endif
|
|
|
|
typedef struct
|
|
{
|
|
cmErr_t err;
|
|
cmCtx_t ctx;
|
|
cmMfpCallback_t cbFunc;
|
|
void* userCbPtr;
|
|
void* printDataPtr;
|
|
unsigned memBlockByteCnt;
|
|
cmMidiFileH_t mfH; // midi file handle
|
|
bool closeFileFl; // true mfH should be closed when this midi file player is closed
|
|
unsigned ticksPerQN; // global for file
|
|
unsigned microsPerTick; // set via tempo
|
|
unsigned etime; // usecs elapsed since transmitting prev msg
|
|
unsigned mtime; // usecs to wait before transmitting next msg
|
|
unsigned msgN; // count of pointers in msgV[]
|
|
unsigned msgIdx; // index into msgV[] of next msg to transmit
|
|
const cmMidiTrackMsg_t** msgV; // array of msg pointers
|
|
|
|
} cmMfp_t;
|
|
|
|
cmMfpH_t cmMfpNullHandle = cmSTATIC_NULL_HANDLE;
|
|
|
|
#define _cmMfpError( mfp, rc ) _cmMfpOnError(mfp, rc, __LINE__,__FILE__,__FUNCTION__ )
|
|
|
|
// note: mfp may be NULL
|
|
cmMfpRC_t _cmMfpOnError( cmMfp_t* mfp, cmMfpRC_t rc, int line, const char* fn, const char* func )
|
|
{
|
|
return cmErrMsg(&mfp->err,rc,"rc:%i %i %s %s\n",rc,line,func,fn);
|
|
}
|
|
|
|
cmMfp_t* _cmMfpHandleToPtr( cmMfpH_t h )
|
|
{
|
|
cmMfp_t* p = (cmMfp_t*)h.h;
|
|
assert(p != NULL);
|
|
return p;
|
|
}
|
|
|
|
void _cmMfpUpdateMicrosPerTick( cmMfp_t* mfp, unsigned microsPerQN )
|
|
{
|
|
mfp->microsPerTick = microsPerQN / mfp->ticksPerQN;
|
|
printf("microsPerTick: %i bpm:%i ticksPerQN:%i\n", mfp->microsPerTick,microsPerQN,mfp->ticksPerQN);
|
|
}
|
|
|
|
cmMfpRC_t cmMfpCreate( cmMfpH_t* hp, cmMfpCallback_t cbFunc, void* userCbPtr, cmCtx_t* ctx )
|
|
{
|
|
cmMfp_t* p = cmMemAllocZ( cmMfp_t, 1 );
|
|
|
|
cmErrSetup(&p->err,&ctx->rpt,"MIDI File Player");
|
|
p->ctx = *ctx;
|
|
p->cbFunc = cbFunc;
|
|
p->userCbPtr = userCbPtr;
|
|
p->mfH.h = NULL;
|
|
p->closeFileFl = false;
|
|
p->ticksPerQN = 0;
|
|
p->microsPerTick = 0;
|
|
p->etime = 0;
|
|
p->msgN = 0;
|
|
p->msgV = NULL;
|
|
p->msgIdx = 0;
|
|
hp->h = p;
|
|
return kOkMfpRC;
|
|
}
|
|
|
|
cmMfpRC_t cmMfpDestroy( cmMfpH_t* hp )
|
|
{
|
|
if( hp == NULL )
|
|
return kOkMfpRC;
|
|
|
|
if( cmMfpIsValid(*hp) )
|
|
{
|
|
cmMfp_t* p = _cmMfpHandleToPtr(*hp);
|
|
|
|
if( cmMidiFileIsNull(p->mfH)==false && p->closeFileFl==true )
|
|
cmMidiFileClose(&p->mfH);
|
|
|
|
cmMemFree(p);
|
|
hp->h = NULL;
|
|
}
|
|
|
|
return kOkMfpRC;
|
|
}
|
|
|
|
bool cmMfpIsValid( cmMfpH_t h )
|
|
{ return h.h != NULL; }
|
|
|
|
cmMfpRC_t cmMfpLoadFile( cmMfpH_t h, const char* fn )
|
|
{
|
|
cmMfpRC_t rc = kOkMfpRC;
|
|
cmMfp_t* p = _cmMfpHandleToPtr(h);
|
|
cmMidiFileH_t mfH = cmMidiFileNullHandle;
|
|
|
|
if((rc = cmMidiFileOpen( fn, &mfH, &p->ctx )) != kOkMfRC )
|
|
return _cmMfpError(p,kFileOpenFailMfpRC);
|
|
|
|
if((rc= cmMfpLoadHandle( h, mfH )) == kOkMfpRC )
|
|
p->closeFileFl = true;
|
|
|
|
return rc;
|
|
}
|
|
|
|
cmMfpRC_t cmMfpLoadHandle( cmMfpH_t h, cmMidiFileH_t mfH )
|
|
{
|
|
cmMfp_t* p = _cmMfpHandleToPtr(h);
|
|
|
|
// if a file has already been assigned to this player
|
|
if( (cmMidiFileIsNull(p->mfH) == false) && p->closeFileFl)
|
|
{
|
|
// close the existing file
|
|
cmMidiFileClose(&p->mfH);
|
|
}
|
|
|
|
// get the count of msg's in the new midi file
|
|
if((p->msgN = cmMidiFileMsgCount(mfH)) == cmInvalidCnt )
|
|
return _cmMfpError(p,kInvalidFileMfpRC);
|
|
|
|
// get a pointer to the first mesage
|
|
if((p->msgV = cmMidiFileMsgArray(mfH)) == NULL )
|
|
return _cmMfpError(p,kInvalidFileMfpRC);
|
|
|
|
// get the count of ticks per qn
|
|
if((p->ticksPerQN = cmMidiFileTicksPerQN( mfH )) == 0 )
|
|
return _cmMfpError(p,kSmpteTickNotImplMfpRC);
|
|
|
|
// set the initial tempo to 120
|
|
_cmMfpUpdateMicrosPerTick(p,60000000/120);
|
|
|
|
p->msgIdx = 0;
|
|
p->mfH = mfH;
|
|
p->etime = 0;
|
|
p->mtime = 0;
|
|
p->closeFileFl= false;
|
|
|
|
//if( p->msgIdx > 0 )
|
|
// p->mtime = p->msgV[0]->tick * p->microsPerTick;
|
|
|
|
return kOkMfpRC;
|
|
}
|
|
|
|
cmMfpRC_t cmMfpSeek( cmMfpH_t h, unsigned offsUsecs )
|
|
{
|
|
cmMfp_t* p = _cmMfpHandleToPtr(h);
|
|
unsigned msgOffsUsecs = 0;
|
|
unsigned msgIdx;
|
|
unsigned newMicrosPerTick;
|
|
|
|
// if the requested offset is past the end of the file then return EOF
|
|
if((msgIdx = cmMidiFileSeekUsecs( p->mfH, offsUsecs, &msgOffsUsecs, &newMicrosPerTick )) == cmInvalidIdx )
|
|
{
|
|
p->msgIdx = p->msgN;
|
|
return _cmMfpError(p,kEndOfFileMfpRC);
|
|
}
|
|
|
|
if( msgIdx < p->msgIdx )
|
|
p->msgIdx = 0;
|
|
|
|
p->mtime = msgOffsUsecs;
|
|
p->etime = 0;
|
|
p->microsPerTick = newMicrosPerTick;
|
|
p->msgIdx = msgIdx;
|
|
|
|
assert(p->mtime >= 0);
|
|
|
|
return kOkMfpRC;
|
|
}
|
|
|
|
// p 0 1 n 2
|
|
// v v v v v
|
|
// xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
|
|
// 012345678901234567890123456780
|
|
// 0 1 2
|
|
//
|
|
// p = 3 = prev msg sent
|
|
// n = 19 = next msg to send
|
|
// 0 = 6 = call to cmMfpClock()
|
|
// 1 = 12 = call to cmMfpClock()
|
|
// 2 = 22 = call to cmMfpClock()
|
|
//
|
|
// dusecs etime mtime
|
|
// 0 n/a 3 13
|
|
// 1 6 9 7
|
|
// 2 10 19 -3
|
|
//
|
|
|
|
cmMfpRC_t cmMfpClock( cmMfpH_t h, unsigned dusecs )
|
|
{
|
|
cmMfp_t* p = _cmMfpHandleToPtr(h);
|
|
|
|
if( p->msgIdx >= p->msgN )
|
|
return kEndOfFileMfpRC;
|
|
|
|
// get a pointer to the next msg to send
|
|
const cmMidiTrackMsg_t* mp = p->msgV[p->msgIdx];
|
|
|
|
// p->etime is the interval of time between when the last msg was
|
|
// sent and the end of the time window for this mfpClock() cycle
|
|
p->etime += dusecs;
|
|
|
|
//printf("init e:%i d:%i\n",p->etime, p->mtime);
|
|
|
|
// if the elapsed time (etime) since the last msg is greater or equal
|
|
// to the delta time to the next msg (mtime)
|
|
while( p->etime >= p->mtime )
|
|
{
|
|
//printf("e:%i d:%i\n",p->etime, p->mtime);
|
|
|
|
if( mp->status == kMetaStId && mp->metaId == kTempoMdId )
|
|
_cmMfpUpdateMicrosPerTick(p,mp->u.iVal );
|
|
|
|
|
|
p->cbFunc( p->userCbPtr, p->mtime, mp );
|
|
|
|
++(p->msgIdx);
|
|
|
|
if( p->msgIdx >= p->msgN )
|
|
break;
|
|
|
|
// get the next msg to send
|
|
mp = p->msgV[p->msgIdx];
|
|
|
|
// we probably went past the actual mtime - so update etime
|
|
// with the delta usecs from the msg just sent and the current time
|
|
p->etime -= p->mtime;
|
|
|
|
// calc the delta usecs from the message just sent to the next msg to send
|
|
//p->mtime = (mp->tick - p->msgV[p->msgIdx-1]->tick) * p->microsPerTick;
|
|
p->mtime = mp->dtick * p->microsPerTick;
|
|
|
|
}
|
|
|
|
return p->msgIdx >= p->msgN ? kEndOfFileMfpRC : kOkMfpRC;
|
|
|
|
}
|
|
|
|
|
|
void mfpPrint( void* userDataPtr, const char* fmt, va_list vl )
|
|
{
|
|
vprintf(fmt,vl);
|
|
}
|
|
|
|
// this assumes that the seconds have been normalized to a recent start time
|
|
// so as to avoid overflow
|
|
unsigned _cmMfpElapsedMicroSecs( const struct timespec* t0, const struct timespec* t1 )
|
|
{
|
|
// convert seconds to usecs
|
|
long u0 = t0->tv_sec * 1000000;
|
|
long u1 = t1->tv_sec * 1000000;
|
|
|
|
// convert nanoseconds to usec
|
|
u0 += t0->tv_nsec / 1000;
|
|
u1 += t1->tv_nsec / 1000;
|
|
|
|
// take diff between t1 and t0
|
|
return u1 - u0;
|
|
}
|
|
|
|
void _cmMfpTestTimer()
|
|
{
|
|
useconds_t suspendUsecs = 15 * 1000;
|
|
struct timespec t0,t1,t2;
|
|
unsigned accum = 0;
|
|
unsigned i;
|
|
unsigned n = 4000;
|
|
|
|
// t0 will be the base time which all other times will be
|
|
// set relative to.
|
|
clock_gettime(CLOCK_REALTIME,&t0);
|
|
t2 = t0;
|
|
t2.tv_sec = 0;
|
|
|
|
for(i=0; i<n; ++i)
|
|
{
|
|
usleep(suspendUsecs);
|
|
|
|
|
|
clock_gettime(CLOCK_REALTIME,&t1);
|
|
t1.tv_sec -= t0.tv_sec;
|
|
|
|
unsigned d0usec = _cmMfpElapsedMicroSecs(&t0,&t1);
|
|
unsigned d1usec = _cmMfpElapsedMicroSecs(&t2,&t1);
|
|
|
|
accum += d1usec;
|
|
|
|
if( i == n-1 )
|
|
printf("%i %i %i\n",d0usec,d1usec,accum);
|
|
|
|
t2 = t1;
|
|
}
|
|
|
|
}
|
|
|
|
// midi file player callback test function
|
|
void _cmMfpCallbackTest( void* userCbPtr, unsigned dmicros, const cmMidiTrackMsg_t* msgPtr )
|
|
{
|
|
if( kNoteOffMdId <= msgPtr->status && msgPtr->status <= kPbendMdId )
|
|
cmMpDeviceSend( 0, 0, msgPtr->status+msgPtr->u.chMsgPtr->ch, msgPtr->u.chMsgPtr->d0,msgPtr->u.chMsgPtr->d1);
|
|
|
|
//printf("%i 0x%x 0x%x %i\n",msgPtr->tick,msgPtr->status,msgPtr->metaId,msgPtr->trkIdx);
|
|
}
|
|
|
|
// midi port callback test function
|
|
void _cmMpCallbackTest( const cmMidiPacket_t* pktArray, unsigned pktCnt )
|
|
{}
|
|
|
|
cmMfpRC_t cmMfpTest( const char* fn, cmCtx_t* ctx )
|
|
{
|
|
cmMfpH_t mfpH = cmMfpNullHandle;
|
|
cmMfpRC_t rc;
|
|
useconds_t suspendUsecs = 15 * 1000;
|
|
struct timespec t0,t1,base;
|
|
//unsigned i;
|
|
//unsigned n = 4000;
|
|
unsigned mdParserBufByteCnt = 1024;
|
|
|
|
printf("Initializing MIDI Devices...\n");
|
|
cmMpInitialize( ctx, _cmMpCallbackTest, NULL, mdParserBufByteCnt,"app" );
|
|
|
|
//mdReport();
|
|
|
|
printf("Creating Player...\n");
|
|
if((rc = cmMfpCreate( &mfpH, _cmMfpCallbackTest, NULL, ctx )) != kOkMfpRC )
|
|
return rc;
|
|
|
|
printf("Loading MIDI file...\n");
|
|
if((rc = cmMfpLoadFile( mfpH, fn )) != kOkMfpRC )
|
|
goto errLabel;
|
|
|
|
if((rc = cmMfpSeek( mfpH, 60 * 1000000 )) != kOkMfpRC )
|
|
goto errLabel;
|
|
|
|
clock_gettime(CLOCK_REALTIME,&base);
|
|
t0 = base;
|
|
t0.tv_sec = 0;
|
|
|
|
//for(i=0; i<n; ++i)
|
|
while(rc != kEndOfFileMfpRC)
|
|
{
|
|
usleep(suspendUsecs);
|
|
|
|
clock_gettime(CLOCK_REALTIME,&t1);
|
|
t1.tv_sec -= base.tv_sec;
|
|
|
|
unsigned dusecs = _cmMfpElapsedMicroSecs(&t0,&t1);
|
|
|
|
rc = cmMfpClock( mfpH, dusecs );
|
|
//printf("%i %i\n",dusecs,rc);
|
|
t0 = t1;
|
|
}
|
|
|
|
errLabel:
|
|
cmMfpDestroy(&mfpH);
|
|
|
|
cmMpFinalize();
|
|
|
|
return rc;
|
|
}
|
|
|
|
|
|
//------------------------------------------------------------------------------------------------------------
|
|
#include "cmFloatTypes.h"
|
|
#include "cmComplexTypes.h"
|
|
#include "cmLinkedHeap.h"
|
|
#include "cmSymTbl.h"
|
|
#include "cmAudioFile.h"
|
|
#include "cmProcObj.h"
|
|
#include "cmProcTemplateMain.h"
|
|
#include "cmVectOps.h"
|
|
|
|
#include "cmProc.h"
|
|
#include "cmProc2.h"
|
|
|
|
|
|
enum
|
|
{
|
|
kOkMfptRC = cmOkRC,
|
|
kMfpFailMfptRC,
|
|
kAudioFileFailMfptRC,
|
|
kProcObjFailMfptRC
|
|
};
|
|
|
|
typedef struct
|
|
{
|
|
cmErr_t* err;
|
|
cmMidiSynth* msp;
|
|
} _cmMfpTest2CbData_t;
|
|
|
|
|
|
// Called by the MIDI file player to send a msg to the MIDI synth.
|
|
void _cmMfpCb( void* userCbPtr, unsigned dmicros, const cmMidiTrackMsg_t* msgPtr )
|
|
{
|
|
if( kNoteOffMdId <= msgPtr->status && msgPtr->status <= kPbendMdId )
|
|
{
|
|
cmMidiPacket_t pkt;
|
|
cmMidiMsg msg;
|
|
_cmMfpTest2CbData_t* d = (_cmMfpTest2CbData_t*)userCbPtr;
|
|
|
|
msg.deltaUs = dmicros;
|
|
msg.status = msgPtr->status + msgPtr->u.chMsgPtr->ch;
|
|
msg.d0 = msgPtr->u.chMsgPtr->d0;
|
|
msg.d1 = msgPtr->u.chMsgPtr->d1;
|
|
|
|
pkt.cbDataPtr = NULL;
|
|
pkt.devIdx = cmInvalidIdx;
|
|
pkt.portIdx = cmInvalidIdx;
|
|
pkt.msgArray = &msg;
|
|
pkt.sysExMsg = NULL;
|
|
pkt.msgCnt = 1;
|
|
|
|
if( cmMidiSynthOnMidi( d->msp, &pkt, 1 ) != cmOkRC )
|
|
cmErrMsg(d->err,kProcObjFailMfptRC,"Synth. MIDI receive failed.");
|
|
}
|
|
}
|
|
|
|
// Called by the MIDI synth to send a msg to the voice bank.
|
|
int _cmMidiSynthCb( struct cmMidiVoice_str* voicePtr, unsigned sel, cmSample_t* outChArray[], unsigned outChCnt )
|
|
{
|
|
return cmWtVoiceBankExec( ((cmWtVoiceBank*)voicePtr->pgm.cbDataPtr), voicePtr, sel, outChArray, outChCnt );
|
|
}
|
|
|
|
|
|
// BUG BUG BUG: THIS FUNCTION IS NOT TESTED!!!!!
|
|
cmRC_t cmMfpTest2( const char* midiFn, const char* audioFn, cmCtx_t* ctx )
|
|
{
|
|
cmRC_t rc = kOkMfptRC;
|
|
cmMfpH_t mfpH = cmMfpNullHandle;
|
|
_cmMfpTest2CbData_t cbData;
|
|
cmErr_t err;
|
|
cmAudioFileH_t afH = cmNullAudioFileH;
|
|
cmRC_t afRC = kOkAfRC;
|
|
double afSrate = 44100;
|
|
unsigned afBits = 16;
|
|
unsigned afChCnt = 1;
|
|
cmCtx* cctx;
|
|
cmMidiSynth* msp;
|
|
cmWtVoiceBank* vbp;
|
|
unsigned msPgmCnt = 127;
|
|
cmMidiSynthPgm msPgmArray[ msPgmCnt ];
|
|
unsigned msVoiceCnt = 36;
|
|
unsigned procSmpCnt = 64;
|
|
unsigned i;
|
|
|
|
cmErrSetup(&err,&ctx->rpt,"MFP Test 2");
|
|
|
|
// create the MIDI file player
|
|
if( cmMfpCreate(&mfpH, _cmMfpCb, &cbData, ctx ) != kOkMfpRC )
|
|
return cmErrMsg(&err,kMfpFailMfptRC,"MIDI file player create failed.");
|
|
|
|
// create an output audio file
|
|
if( cmAudioFileIsValid( afH = cmAudioFileNewCreate(audioFn, afSrate, afBits, afChCnt, &afRC, &ctx->rpt))==false)
|
|
{
|
|
rc = cmErrMsg(&err,kAudioFileFailMfptRC,"The audio file create failed.");
|
|
goto errLabel;
|
|
}
|
|
|
|
// load the midi file into the player
|
|
if( cmMfpLoadFile( mfpH, midiFn ) != kOkMfpRC )
|
|
{
|
|
rc = cmErrMsg(&err,kMfpFailMfptRC,"MIDI file load failed.");
|
|
goto errLabel;
|
|
}
|
|
|
|
// create the proc obj context
|
|
if((cctx = cmCtxAlloc(NULL, &ctx->rpt, cmLHeapNullHandle, cmSymTblNullHandle )) == NULL)
|
|
{
|
|
rc = cmErrMsg(&err,kProcObjFailMfptRC,"cmCtx allocate failed.");
|
|
goto errLabel;
|
|
}
|
|
|
|
// create the voice bank
|
|
if((vbp = cmWtVoiceBankAlloc(cctx, NULL, afSrate, procSmpCnt, msVoiceCnt, afChCnt )) == NULL)
|
|
{
|
|
rc = cmErrMsg(&err,kProcObjFailMfptRC,"WT voice bank allocate failed.");
|
|
goto errLabel;
|
|
}
|
|
|
|
// a MIDI synth
|
|
if((msp = cmMidiSynthAlloc(cctx, NULL, msPgmArray, msPgmCnt, msVoiceCnt, procSmpCnt, afChCnt, afSrate )) == NULL )
|
|
{
|
|
rc = cmErrMsg(&err,kProcObjFailMfptRC,"MIDI synth allocate failed.");
|
|
goto errLabel;
|
|
}
|
|
|
|
cbData.msp = msp;
|
|
cbData.err = &err;
|
|
|
|
// load all of the the MIDI pgm recds with the same settings
|
|
for(i=0; i<msPgmCnt; ++i)
|
|
{
|
|
msPgmArray[i].pgm = i;
|
|
msPgmArray[i].cbPtr = _cmMidiSynthCb; // Call this function to update voices using this pgm
|
|
msPgmArray[i].cbDataPtr = vbp; // Voice bank containing the voice states.
|
|
}
|
|
|
|
|
|
unsigned dusecs = floor((double)procSmpCnt * 1000000. / afSrate);
|
|
|
|
while(rc != kEndOfFileMfpRC)
|
|
{
|
|
// update the MFP's current time and call _cmMfpCb() for MIDI msgs whose time has elapsed
|
|
rc = cmMfpClock( mfpH, dusecs );
|
|
|
|
// check for MFP errors
|
|
if(rc!=kOkMfpRC && rc!=kEndOfFileMfpRC)
|
|
{
|
|
cmErrMsg(&err,kMfpFailMfptRC,"MIDI file player exec failed.");
|
|
goto errLabel;
|
|
}
|
|
|
|
// generate audio based on the current state of the synth voices
|
|
if( cmMidiSynthExec(msp, NULL, 0 ) != cmOkRC )
|
|
{
|
|
cmErrMsg(&err,kProcObjFailMfptRC,"MIDI synth exec. failed.");
|
|
goto errLabel;
|
|
}
|
|
|
|
// write the last frame of synth. generated audio to the output file
|
|
if( cmAudioFileWriteSample(afH, procSmpCnt, msp->outChCnt, msp->outChArray ) != kOkAfRC )
|
|
{
|
|
cmErrMsg(&err,kProcObjFailMfptRC,"Audio file write failed.");
|
|
goto errLabel;
|
|
}
|
|
|
|
}
|
|
|
|
|
|
errLabel:
|
|
if( cmMidiSynthFree(&msp) != cmOkRC )
|
|
cmErrMsg(&err,kProcObjFailMfptRC,"MIDI synth. free failed.");
|
|
|
|
if( cmWtVoiceBankFree(&vbp) != cmOkRC )
|
|
cmErrMsg(&err,kProcObjFailMfptRC,"WT voice free failed.");
|
|
|
|
if( cmCtxFree(&cctx) != cmOkRC )
|
|
cmErrMsg(&err,kProcObjFailMfptRC,"cmCtx free failed.");
|
|
|
|
if( cmAudioFileDelete(&afH) )
|
|
cmErrMsg(&err,kAudioFileFailMfptRC,"The audio file close failed.");
|
|
|
|
if( cmMfpDestroy(&mfpH) != kOkMfpRC )
|
|
cmErrMsg(&err,kMfpFailMfptRC,"MIDI file player destroy failed.");
|
|
|
|
|
|
return rc;
|
|
}
|