From 7c9b2e381cf23868995253e956805db98799ecf9 Mon Sep 17 00:00:00 2001 From: kevin Date: Sun, 21 Feb 2021 08:48:37 -0500 Subject: [PATCH] cwMidiFile.h/cpp, Makefile : Initial commit. --- Makefile.am | 4 +- cwMidiFile.cpp | 2416 ++++++++++++++++++++++++++++++++++++++++++++++++ cwMidiFile.h | 237 +++++ 3 files changed, 2655 insertions(+), 2 deletions(-) create mode 100644 cwMidiFile.cpp create mode 100644 cwMidiFile.h diff --git a/Makefile.am b/Makefile.am index 6d0f261..dc496d0 100644 --- a/Makefile.am +++ b/Makefile.am @@ -22,8 +22,8 @@ libcwSRC += src/libcw/cwThread.cpp src/libcw/cwMutex.cpp src/libcw/cwThreadMach libcwHDR += src/libcw/cwMpScNbQueue.h src/libcw/cwSpScBuf.h src/libcw/cwSpScQueueTmpl.h libcwSRC += src/libcw/cwSpScBuf.cpp src/libcw/cwSpScQueueTmpl.cpp -libcwHDR += src/libcw/cwAudioFile.h src/libcw/cwAudioFileOps.h -libcwSRC += src/libcw/cwAudioFile.cpp src/libcw/cwAudioFileOps.cpp +libcwHDR += src/libcw/cwAudioFile.h src/libcw/cwAudioFileOps.h src/libcw/cwMidiFile.h +libcwSRC += src/libcw/cwAudioFile.cpp src/libcw/cwAudioFileOps.cpp src/libcw/cwMidiFile.cpp if cwWEBSOCK libcwHDR += src/libcw/cwWebSock.h src/libcw/cwWebSockSvr.h diff --git a/cwMidiFile.cpp b/cwMidiFile.cpp new file mode 100644 index 0000000..693a2c2 --- /dev/null +++ b/cwMidiFile.cpp @@ -0,0 +1,2416 @@ +#include "cwCommon.h" +#include "cwLog.h" +#include "cwCommonImpl.h" +#include "cwMem.h" +#include "cwFile.h" +#include "cwObject.h" +#include "cwFileSys.h" +#include "cwMidi.h" +#include "cwMidiFile.h" + + +#ifdef cwBIG_ENDIAN +#define mfSwap16(v) (v) +#define mfSwap32(v) (v) +#else +#define mfSwap16(v) cwSwap16(v) +#define mfSwap32(v) cwSwap32(v) +#endif + +namespace cw +{ + namespace midi + { + namespace file + { + + typedef struct + { + unsigned cnt; // count of track records + trackMsg_t* base; // pointer to first track recd + trackMsg_t* last; // pointer to last track recd + } track_t; + + typedef struct file_str + { + cw::file::handle_t fh; // file handle (only used in open() and write()) + unsigned short fmtId; // midi file type id: 0,1,2 + unsigned short ticksPerQN; // ticks per quarter note or 0 if smpteFmtId is valid + uint8_t smpteFmtId; // smpte format or 0 if ticksPerQN is valid + uint8_t smpteTicksPerFrame; // smpte ticks per frame or 0 if ticksPerQN is valid + unsigned short trkN; // track count + track_t* trkV; // track vector + char* fn; // file name or NULL if this object did not originate from a file + unsigned msgN; // count of msg's in msgV[] + trackMsg_t** msgV; // sorted msg list + bool msgVDirtyFl; // msgV[] needs to be refreshed from trkV[] because new msg's were inserted. + unsigned nextUid; // next available msg uid + } file_t; + + const trackMsg_t** _msgArray( file_t* p ); + + file_t* _handleToPtr( handle_t h ) + { return handleToPtr(h); } + + rc_t _read8( file_t* mfp, uint8_t* p ) + { + rc_t rc; + if((rc = cw::file::readUChar(mfp->fh,p,1)) != kOkRC ) + return cwLogError(rc,"MIDI byte read failed."); + return kOkRC; + } + + rc_t _read16( file_t* mfp, unsigned short* p ) + { + rc_t rc; + if((rc = cw::file::readUShort(mfp->fh,p,1)) != kOkRC ) + return cwLogError(rc,"MIDI short read failed."); + + *p = mfSwap16(*p); + + return kOkRC; + } + + rc_t _read24( file_t* mfp, unsigned* p ) + { + rc_t rc = kOkRC; + + *p = 0; + int i = 0; + for(; i<3; ++i) + { + unsigned char c; + if((rc = cw::file::readUChar(mfp->fh,&c,1)) != kOkRC ) + return cwLogError(rc,"MIDI 24 bit integer read failed."); + *p = (*p << 8) + c; + } + + //*p =mfSwap32(*p); + + return rc; + } + + rc_t _read32( file_t* mfp, unsigned* p ) + { + rc_t rc = kOkRC; + if((rc = cw::file::readUInt(mfp->fh,p,1)) != kOkRC ) + return cwLogError(rc,"MIDI integer read failed."); + + *p = mfSwap32(*p); + + return rc; + } + + rc_t _readText( file_t* mfp, trackMsg_t* tmp, unsigned byteN ) + { + rc_t rc = kOkRC; + if( byteN == 0 ) + return kOkRC; + + char* t = mem::allocZ(byteN+1); + t[byteN] = 0; + + if((rc = cw::file::readChar(mfp->fh,t,byteN)) != kOkRC ) + return cwLogError(rc,"MIDI read text failed."); + + tmp->u.text = t; + tmp->byteCnt = byteN; + return rc; + } + + rc_t _readRecd( file_t* mfp, trackMsg_t* tmp, unsigned byteN ) + { + rc_t rc = kOkRC; + char* t = mem::allocZ(byteN); + + if((rc = cw::file::readChar(mfp->fh,t,byteN)) != kOkRC ) + return cwLogError(rc,"MIDI read record failed."); + + tmp->byteCnt = byteN; + tmp->u.voidPtr = t; + + return rc; + } + + rc_t _readVarLen( file_t* mfp, unsigned* p ) + { + rc_t rc = kOkRC; + unsigned char c; + + if((rc = cw::file::readUChar(mfp->fh,&c,1)) != kOkRC ) + return cwLogError(rc,"MIDI read variable length integer failed."); + + if( !(c & 0x80) ) + *p = c; + else + { + *p = c & 0x7f; + + do + { + if((rc = cw::file::readUChar(mfp->fh,&c,1)) != kOkRC ) + return cwLogError(rc,"MIDI read variable length integer failed."); + + *p = (*p << 7) + (c & 0x7f); + + }while( c & 0x80 ); + } + + return rc; + } + + trackMsg_t* _allocMsg( file_t* mfp, unsigned short trkIdx, unsigned dtick, uint8_t status ) + { + trackMsg_t* tmp = mem::allocZ(); + + // set the generic track record fields + tmp->dtick = dtick; + tmp->status = status; + tmp->metaId = kInvalidMetaMdId; + tmp->trkIdx = trkIdx; + tmp->byteCnt = 0; + tmp->uid = mfp->nextUid++; + + return tmp; + } + + rc_t _appendTrackMsg( file_t* mfp, unsigned short trkIdx, unsigned dtick, uint8_t status, trackMsg_t** trkMsgPtrPtr ) + { + trackMsg_t* tmp = _allocMsg( mfp, trkIdx, dtick, status ); + + // link new record onto track record chain + if( mfp->trkV[trkIdx].base == NULL ) + mfp->trkV[trkIdx].base = tmp; + else + mfp->trkV[trkIdx].last->link = tmp; + + mfp->trkV[trkIdx].last = tmp; + mfp->trkV[trkIdx].cnt++; + + + *trkMsgPtrPtr = tmp; + + return kOkRC; + } + + rc_t _readSysEx( file_t* mfp, trackMsg_t* tmp, unsigned byteN ) + { + rc_t rc = kOkRC; + uint8_t b = 0; + + if( byteN == kInvalidCnt ) + { + + long offs; + if( (rc = cw::file::tell(mfp->fh,&offs)) != kOkRC ) + return cwLogError(rc,"MIDI File 'tell' failed."); + + byteN = 0; + + // get the length of the sys-ex msg + while( !cw::file::eof(mfp->fh) && (b != kSysComEoxMdId) ) + { + if((rc = _read8(mfp,&b)) != kOkRC ) + return rc; + + ++byteN; + } + + // verify that the EOX byte was found + if( b != kSysComEoxMdId ) + return cwLogError(kSyntaxErrorRC,"MIDI file missing 'end-of-sys-ex'."); + + // rewind to the beginning of the msg + if((rc = cw::file::seek(mfp->fh,cw::file::kBeginFl,offs)) != kOkRC ) + return cwLogError(rc,"MIDI file seek failed on sys-ex read."); + + } + + // allocate memory to hold the sys-ex msg + uint8_t* mp = mem::allocZ( byteN ); + + // read the sys-ex msg from the file into msg memory + if((rc = cw::file::readUChar(mfp->fh,mp,byteN)) != kOkRC ) + return cwLogError(rc,"MIDI sys-ex read failed."); + + tmp->byteCnt = byteN; + tmp->u.sysExPtr = mp; + + return rc; + } + + rc_t _readChannelMsg( file_t* mfp, uint8_t* rsPtr, uint8_t status, trackMsg_t* tmp ) + { + rc_t rc = kOkRC; + chMsg_t* p = mem::allocZ(); + unsigned useRsFl = status <= 0x7f; + uint8_t statusCh = useRsFl ? *rsPtr : status; + + if( useRsFl ) + p->d0 = status; + else + *rsPtr = status; + + tmp->byteCnt = sizeof(chMsg_t); + tmp->status = statusCh & 0xf0; + p->ch = statusCh & 0x0f; + p->durMicros = 0; + + unsigned byteN = statusToByteCount(tmp->status); + + if( byteN==kInvalidMidiByte || byteN > 2 ) + return cwLogError(kSyntaxErrorRC,"Invalid status:0x%x %i byte cnt:%i.",tmp->status,tmp->status,byteN); + + unsigned i; + for(i=useRsFl; id0 : &p->d1; + + if((rc = _read8(mfp,b)) != kOkRC ) + return rc; + } + + // convert note-on velocity=0 to note off + if( tmp->status == kNoteOnMdId && p->d1==0 ) + tmp->status = kNoteOffMdId; + + tmp->u.chMsgPtr = p; + + return rc; + } + + rc_t _readMetaMsg( file_t* mfp, trackMsg_t* tmp ) + { + uint8_t metaId; + rc_t rc; + unsigned byteN = 0; + + if((rc = _read8(mfp,&metaId)) != kOkRC ) + return rc; + + if((rc = _readVarLen(mfp,&byteN)) != kOkRC ) + return rc; + + //printf("mt: %i 0x%x n:%i\n",metaId,metaId,byteN); + + switch( metaId ) + { + case kSeqNumbMdId: rc = _read16(mfp,&tmp->u.sVal); break; + case kTextMdId: rc = _readText(mfp,tmp,byteN); break; + case kCopyMdId: rc = _readText(mfp,tmp,byteN); break; + case kTrkNameMdId: rc = _readText(mfp,tmp,byteN); break; + case kInstrNameMdId: rc = _readText(mfp,tmp,byteN); break; + case kLyricsMdId: rc = _readText(mfp,tmp,byteN); break; + case kMarkerMdId: rc = _readText(mfp,tmp,byteN); break; + case kCuePointMdId: rc = _readText(mfp,tmp,byteN); break; + case kMidiChMdId: rc = _read8(mfp,&tmp->u.bVal); break; + case kMidiPortMdId: rc = _read8(mfp,&tmp->u.bVal); break; + case kEndOfTrkMdId: break; + case kTempoMdId: rc = _read24(mfp,&tmp->u.iVal); break; + case kSmpteMdId: rc = _readRecd(mfp,tmp,sizeof(smpte_t)); break; + case kTimeSigMdId: rc = _readRecd(mfp,tmp,sizeof(timeSig_t)); break; + case kKeySigMdId: rc = _readRecd(mfp,tmp,sizeof(keySig_t)); break; + case kSeqSpecMdId: rc = _readSysEx(mfp,tmp,byteN); break; + + default: + cw::file::seek(mfp->fh,cw::file::kCurFl,byteN); + rc = cwLogError(kProtocolErrorRC,"Unknown meta status:0x%x %i.",metaId,metaId); + } + + tmp->metaId = metaId; + + return rc; + } + + + rc_t _readTrack( file_t* mfp, unsigned short trkIdx ) + { + rc_t rc = kOkRC; + unsigned dticks = 0; + uint8_t status; + uint8_t runstatus = 0; + bool contFl = true; + + while( contFl && (rc==kOkRC)) + { + trackMsg_t* tmp = NULL; + + // read the tick count + if((rc = _readVarLen(mfp,&dticks)) != kOkRC ) + return rc; + + // read the status byte + if((rc = _read8(mfp,&status)) != kOkRC ) + return rc; + + //printf("%5i st:%i 0x%x\n",dticks,status,status); + + // append a track msg + if((rc = _appendTrackMsg( mfp, trkIdx, dticks, status, &tmp )) != kOkRC ) + return rc; + + // switch on status + switch( status ) + { + // handle sys-ex msg + case kSysExMdId: + rc = _readSysEx(mfp,tmp,kInvalidCnt); + break; + + // handle meta msg + case kMetaStId: + rc = _readMetaMsg(mfp,tmp); + + // ignore unknown meta messages + if( rc == kProtocolErrorRC ) + rc = kOkRC; + + contFl = tmp->metaId != kEndOfTrkMdId; + break; + + default: + // handle channel msg + rc = _readChannelMsg(mfp,&runstatus,status,tmp); + + } + } + return rc; + } + + + rc_t _readHdr( file_t* mfp ) + { + rc_t rc; + unsigned fileId; + unsigned chunkByteN; + + // read the file id + if((rc = _read32(mfp,&fileId)) != kOkRC ) + return rc; + + // verify the file id + if( fileId != 'MThd' ) + return cwLogError(kInvalidDataTypeRC,"Not a MIDI file."); + + // read the file chunk byte count + if((rc = _read32(mfp,&chunkByteN)) != kOkRC ) + return rc; + + // read the format id + if((rc = _read16(mfp,&mfp->fmtId)) != kOkRC ) + return rc; + + // read the track count + if((rc = _read16(mfp,&mfp->trkN)) != kOkRC ) + return rc; + + // read the ticks per quarter note + if((rc = _read16(mfp,&mfp->ticksPerQN)) != kOkRC ) + return rc; + + // if the division field was given in smpte + if( mfp->ticksPerQN & 0x8000 ) + { + mfp->smpteFmtId = (mfp->ticksPerQN & 0x7f00) >> 8; + mfp->smpteTicksPerFrame = (mfp->ticksPerQN & 0xFF); + mfp->ticksPerQN = 0; + } + + // allocate and zero the track array + if( mfp->trkN ) + mfp->trkV = mem::allocZ(mfp->trkN); + + return rc; + } + + void _drop( file_t* p ) + { + unsigned i; + unsigned n = 0; + for(i=0; itrkN; ++i) + { + track_t* trk = p->trkV + i; + trackMsg_t* m0 = NULL; + trackMsg_t* m = trk->base; + + for(; m!=NULL; m=m->link) + { + if( cwIsFlag(m->flags,kDropTrkMsgFl) ) + { + ++n; + if( m0 == NULL ) + trk->base = m->link; + else + m0->link = m->link; + } + else + { + m0 = m; + } + } + } + } + + int _sortFunc( const void *p0, const void* p1 ) + { + if( (*(trackMsg_t**)p0)->atick == (*(trackMsg_t**)p1)->atick ) + return 0; + + return (*(trackMsg_t**)p0)->atick < (*(trackMsg_t**)p1)->atick ? -1 : 1; + } + + // Set the absolute accumulated ticks (atick) value of each track message. + // The absolute accumulated ticks gives a global time ordering for all + // messages in the file. + void _setAccumulateTicks( file_t* p ) + { + trackMsg_t* nextTrkMsg[ p->trkN ]; // next msg in each track + unsigned long long atick = 0; + unsigned i; + bool fl = true; + + // iniitalize nextTrkTick[] and nextTrkMsg[] to the first msg in each track + for(i=0; itrkN; ++i) + if((nextTrkMsg[i] = p->trkV[i].base) != NULL ) + nextTrkMsg[i]->atick = nextTrkMsg[i]->dtick; + + while(1) + { + unsigned k = kInvalidIdx; + + // find the trk which has the next msg (min atick time) + for(i=0; itrkN; ++i) + if( nextTrkMsg[i]!=NULL && (k==kInvalidIdx || nextTrkMsg[i]->atick < nextTrkMsg[k]->atick) ) + k = i; + + // no next msg was found - we're done + if( k == kInvalidIdx ) + break; + + if( fl && nextTrkMsg[k]->dtick > 0 ) + { + fl = false; + nextTrkMsg[k]->dtick = 1; + nextTrkMsg[k]->atick = 1; + } + + // store the current atick + atick = nextTrkMsg[k]->atick; + + // advance the selected track to it's next message + nextTrkMsg[k] = nextTrkMsg[k]->link; + + // set the selected tracks next atick time + if( nextTrkMsg[k] != NULL ) + nextTrkMsg[k]->atick = atick + nextTrkMsg[k]->dtick; + } + } + + void _setAbsoluteTime( file_t* mfp ) + { + const trackMsg_t** msgV = _msgArray(mfp); + double microsPerQN = 60000000/120; // default tempo; + double microsPerTick = microsPerQN / mfp->ticksPerQN; + unsigned long long amicro = 0; + unsigned i; + + + for(i=0; imsgN; ++i) + { + trackMsg_t* mp = (trackMsg_t*)msgV[i]; // cast away const + unsigned dtick = 0; + + if( i > 0 ) + { + // atick must have already been set and sorted + assert( mp->atick >= msgV[i-1]->atick ); + dtick = mp->atick - msgV[i-1]->atick; + } + + amicro += microsPerTick * dtick; + mp->amicro = amicro; + + + // track tempo changes + if( mp->status == kMetaStId && mp->metaId == kTempoMdId ) + microsPerTick = mp->u.iVal / mfp->ticksPerQN; + } + + } + + rc_t _close( file_t* mfp ) + { + rc_t rc = kOkRC; + + if( mfp == NULL ) + return rc; + + mem::release(mfp->msgV); + + if( mfp->fh.isValid() ) + if((rc = cw::file::close( mfp->fh )) != kOkRC ) + rc = cwLogError(rc,"MIDI file close failed."); + + mem::release(mfp); + + return rc; + + } + + + void _linearize( file_t* mfp ) + { + unsigned trkIdx,i,j; + + if( mfp->msgVDirtyFl == false ) + return; + + mfp->msgVDirtyFl = false; + + // get the total trk msg count + mfp->msgN = 0; + for(trkIdx=0; trkIdxtrkN; ++trkIdx) + mfp->msgN += mfp->trkV[ trkIdx ].cnt; + + // allocate the trk msg index vector: msgV[] + mfp->msgV = mem::resizeZ( mfp->msgV, mfp->msgN); + + // store a pointer to every trk msg in msgV[] + for(i=0,j=0; itrkN; ++i) + { + trackMsg_t* m = mfp->trkV[i].base; + + for(; m!=NULL; m=m->link) + { + assert( j < mfp->msgN ); + + mfp->msgV[j++] = m; + } + } + + + // set the atick value in each msg + _setAccumulateTicks(mfp); + + // sort msgV[] in ascending order on atick + qsort( mfp->msgV, mfp->msgN, sizeof(trackMsg_t*), _sortFunc ); + + // set the amicro value in each msg + _setAbsoluteTime(mfp); + + + } + + // Note that p->msgV[] should always be accessed through this function + // to guarantee that the p->msgVDirtyFl is checked and msgV[] is updated + // in case msgV[] is out of sync (due to inserted msgs (see insertTrackMsg()) + // with trkV[]. + const trackMsg_t** _msgArray( file_t* p ) + { + _linearize(p); + + // this cast is needed to eliminate an apparently needless 'incompatible type' warning + return (const trackMsg_t**)p->msgV; + } + + rc_t _create( handle_t& hRef ) + { + rc_t rc = kOkRC; + file_t* p = NULL; + + if((rc = close(hRef)) != kOkRC ) + return rc; + + // allocate the midi file object + if(( p = mem::allocZ()) == NULL ) + return rc = cwLogError(kObjAllocFailRC,"MIDI file memory allocation failed."); + + if( rc != kOkRC ) + _close(p); + else + hRef.set(p); + + return rc; + + } + + rc_t _write8( file_t* mfp, unsigned char v ) + { + rc_t rc = kOkRC; + + if( (rc = cw::file::writeUChar(mfp->fh,&v,1)) != kOkRC ) + rc = cwLogError(rc,"MIDI file byte write failed."); + + return rc; + } + + rc_t _write16( file_t* mfp, unsigned short v ) + { + rc_t rc = kOkRC; + + v = mfSwap16(v); + + if((rc = cw::file::writeUShort(mfp->fh,&v,1)) != kOkRC ) + rc = cwLogError(rc,"MIDI file short integer write failed."); + + return rc; + } + + rc_t _write24( file_t* mfp, unsigned v ) + { + rc_t rc = kOkRC; + unsigned mask = 0xff0000; + int i; + + for(i = 2; i>=0; --i) + { + unsigned char c = (v & mask) >> (i*8); + mask >>= 8; + + if((rc = cw::file::writeUChar(mfp->fh,&c,1)) != kOkRC ) + { + rc = cwLogError(rc,"MIDI file 24 bit integer write failed."); + goto errLabel; + } + + } + + errLabel: + return rc; + } + + rc_t _write32( file_t* mfp, unsigned v ) + { + rc_t rc = kOkRC; + + v = mfSwap32(v); + + if((rc = cw::file::writeUInt(mfp->fh,&v,1)) != kOkRC ) + rc = cwLogError(rc,"MIDI file integer write failed."); + + return rc; + } + + rc_t _writeRecd( file_t* mfp, const void* v, unsigned byteCnt ) + { + rc_t rc = kOkRC; + + if((rc = cw::file::writeChar(mfp->fh,(const char*)v,byteCnt)) != kOkRC ) + rc = cwLogError(rc,"MIDI file write record failed."); + + return rc; + } + + rc_t _writeVarLen( file_t* mfp, unsigned v ) + { + rc_t rc = kOkRC; + unsigned buf = v & 0x7f; + + while((v >>= 7) > 0 ) + { + buf <<= 8; + buf |= 0x80; + buf += (v & 0x7f); + } + + while(1) + { + unsigned char c = (unsigned char)(buf & 0xff); + if((rc = cw::file::writeUChar(mfp->fh,&c,1)) != kOkRC ) + { + rc = cwLogError(rc,"MIDI file variable length integer write failed."); + goto errLabel; + } + + if( buf & 0x80 ) + buf >>= 8; + else + break; + } + + errLabel: + return rc; + } + + rc_t _writeHdr( file_t* mfp ) + { + rc_t rc; + unsigned fileId = 'MThd'; + unsigned chunkByteN = 6; + + // write the file id ('MThd') + if((rc = _write32(mfp,fileId)) != kOkRC ) + return rc; + + // write the file chunk byte count (always 6) + if((rc = _write32(mfp,chunkByteN)) != kOkRC ) + return rc; + + // write the MIDI file format id (0,1,2) + if((rc = _write16(mfp,mfp->fmtId)) != kOkRC ) + return rc; + + // write the track count + if((rc = _write16(mfp,mfp->trkN)) != kOkRC ) + return rc; + + unsigned short v = 0; + + // if the ticks per quarter note field is valid ... + if( mfp->ticksPerQN ) + v = mfp->ticksPerQN; + else + { + // ... otherwise the division field was given in smpte + v = mfp->smpteFmtId << 8; + v += mfp->smpteTicksPerFrame; + } + + if((rc = _write16(mfp,v)) != kOkRC ) + return rc; + + return rc; + + } + + + rc_t _writeSysEx( file_t* mfp, trackMsg_t* tmp ) + { + rc_t rc = kOkRC; + + if((rc = _write8(mfp,kSysExMdId)) != kOkRC ) + goto errLabel; + + if((rc = cw::file::writeUChar(mfp->fh,tmp->u.sysExPtr,tmp->byteCnt)) != kOkRC ) + rc = cwLogError(rc,"Sys-ex msg write failed."); + + errLabel: + return rc; + } + + rc_t _writeChannelMsg( file_t* mfp, const trackMsg_t* tmp, uint8_t* runStatus ) + { + rc_t rc = kOkRC; + unsigned byteN = statusToByteCount(tmp->status); + uint8_t status = tmp->status + tmp->u.chMsgPtr->ch; + + if( status != *runStatus ) + { + *runStatus = status; + if((rc = _write8(mfp,status)) != kOkRC ) + goto errLabel; + } + + if(byteN>=1) + if((rc = _write8(mfp,tmp->u.chMsgPtr->d0)) != kOkRC ) + goto errLabel; + + if(byteN>=2) + if((rc = _write8(mfp,tmp->u.chMsgPtr->d1)) != kOkRC ) + goto errLabel; + + errLabel: + return rc; + } + + rc_t _writeMetaMsg( file_t* mfp, const trackMsg_t* tmp ) + { + rc_t rc; + + if((rc = _write8(mfp,kMetaStId)) != kOkRC ) + return rc; + + if((rc = _write8(mfp,tmp->metaId)) != kOkRC ) + return rc; + + + switch( tmp->metaId ) + { + case kSeqNumbMdId: + if((rc = _write8(mfp,sizeof(tmp->u.sVal))) == kOkRC ) + rc = _write16(mfp,tmp->u.sVal); + break; + + case kTempoMdId: + if((rc = _write8(mfp,3)) == kOkRC ) + rc = _write24(mfp,tmp->u.iVal); + break; + + case kSmpteMdId: + if((rc = _write8(mfp,sizeof(smpte_t))) == kOkRC ) + rc = _writeRecd(mfp,tmp->u.smptePtr,sizeof(smpte_t)); + break; + + case kTimeSigMdId: + if((rc = _write8(mfp,sizeof(timeSig_t))) == kOkRC ) + rc = _writeRecd(mfp,tmp->u.timeSigPtr,sizeof(timeSig_t)); + break; + + case kKeySigMdId: + if((rc = _write8(mfp,sizeof(keySig_t))) == kOkRC ) + rc = _writeRecd(mfp,tmp->u.keySigPtr,sizeof(keySig_t)); + break; + + case kSeqSpecMdId: + if((rc = _writeVarLen(mfp,sizeof(tmp->byteCnt))) == kOkRC ) + rc = _writeRecd(mfp,tmp->u.sysExPtr,tmp->byteCnt); + break; + + case kMidiChMdId: + if((rc = _write8(mfp,sizeof(tmp->u.bVal))) == kOkRC ) + rc = _write8(mfp,tmp->u.bVal); + break; + + case kMidiPortMdId: + if((rc = _write8(mfp,sizeof(tmp->u.bVal))) == kOkRC ) + rc = _write8(mfp,tmp->u.bVal); + break; + + case kEndOfTrkMdId: + rc = _write8(mfp,0); + break; + + case kTextMdId: + case kCopyMdId: + case kTrkNameMdId: + case kInstrNameMdId: + case kLyricsMdId: + case kMarkerMdId: + case kCuePointMdId: + { + unsigned n = tmp->u.text==NULL ? 0 : strlen(tmp->u.text); + if((rc = _writeVarLen(mfp,n)) == kOkRC && n>0 ) + rc = _writeRecd(mfp,tmp->u.text,n); + } + break; + + default: + { + // ignore unknown meta messages + } + + } + + return rc; + } + + rc_t _insertEotMsg( file_t* p, unsigned trkIdx ) + { + track_t* trk = p->trkV + trkIdx; + trackMsg_t* m0 = NULL; + trackMsg_t* m = trk->base; + + // locate the current EOT msg on this track + for(; m!=NULL; m=m->link) + { + if( m->status == kMetaStId && m->metaId == kEndOfTrkMdId ) + { + // If this EOT msg is the last msg in the track ... + if( m->link == NULL ) + { + assert( m == trk->last ); + return kOkRC; // ... then there is nothing else to do + } + + // If this EOT msg is not the last in the track ... + if( m0 != NULL ) + m0->link = m->link; // ... then unlink it + + break; + } + + m0 = m; + } + + // if we get here then the last msg in the track was not an EOT msg + + // if there was no previously allocated EOT msg + if( m == NULL ) + { + m = _allocMsg(p, trkIdx, 1, kMetaStId ); + m->metaId = kEndOfTrkMdId; + trk->cnt += 1; + + } + + // link an EOT msg as the last msg on the track + + // if the track is currently empty + if( m0 == NULL ) + { + trk->base = m; + trk->last = m; + } + else // link the msg as the last on on the track + { + assert( m0 == trk->last); + m0->link = m; + m->link = NULL; + trk->last = m; + } + + return kOkRC; + + } + + rc_t _writeTrack( file_t* mfp, unsigned trkIdx ) + { + rc_t rc = kOkRC; + trackMsg_t* tmp = mfp->trkV[trkIdx].base; + uint8_t runStatus = 0; + + // be sure there is a EOT msg at the end of this track + if((rc = _insertEotMsg(mfp, trkIdx )) != kOkRC ) + return rc; + + for(; tmp != NULL; tmp=tmp->link) + { + // write the msg tick count + if((rc = _writeVarLen(mfp,tmp->dtick)) != kOkRC ) + return rc; + + // switch on status + switch( tmp->status ) + { + // handle sys-ex msg + case kSysExMdId: + rc = _writeSysEx(mfp,tmp); + break; + + // handle meta msg + case kMetaStId: + rc = _writeMetaMsg(mfp,tmp); + break; + + default: + // handle channel msg + rc = _writeChannelMsg(mfp,tmp,&runStatus); + + } + + } + + return rc; + } + + trackMsg_t* _uidToMsg( file_t* mfp, unsigned uid ) + { + unsigned i; + const trackMsg_t** msgV = _msgArray(mfp); + + for(i=0; imsgN; ++i) + if( msgV[i]->uid == uid ) + return (trackMsg_t*)msgV[i]; + + return NULL; + } + + // Returns NULL if uid is not found or if it the first msg on the track. + trackMsg_t* _msgBeforeUid( file_t* p, unsigned uid ) + { + trackMsg_t* m; + + if((m = _uidToMsg(p,uid)) == NULL ) + return NULL; + + assert( m->trkIdx < p->trkN ); + + trackMsg_t* m0 = NULL; + trackMsg_t* m1 = p->trkV[ m->trkIdx ].base; + for(; m1!=NULL; m1 = m1->link) + { + if( m1->uid == uid ) + break; + m0 = m1; + } + + return m0; + } + + unsigned _isMsgFirstOnTrack( file_t* p, unsigned uid ) + { + unsigned i; + for(i=0; itrkN; ++i) + if( p->trkV[i].base!=NULL && p->trkV[i].base->uid == uid ) + return i; + + return kInvalidIdx; + } + + unsigned _isEndMsg( trackMsg_t* m, trackMsg_t** endMsgArray, unsigned n ) + { + unsigned i = 0; + for(; iu.chMsgPtr)->durMicros = m1->amicro - m0->amicro; + + // set the note-off msg pointer + ((chMsg_t*)m0->u.chMsgPtr)->end = m1; + } + + bool _calcNoteDur( trackMsg_t* m0, trackMsg_t* m1, int noteGateFl, int sustainGateFl, bool sostGateFl ) + { + // if the note is being kept sounding because the key is still depressed, + // the sustain pedal is down or it is being held by the sostenuto pedal .... + if( noteGateFl>0 || sustainGateFl>0 || sostGateFl ) + return false; // ... do nothing + + _setDur(m0,m1); + + return true; + } + + rc_t _testReport( const object_t* cfg ) + { + rc_t rc = kOkRC; + const char* fn = nullptr; + + if((rc = cfg->getv("midiFn",fn)) != kOkRC ) + return cwLogError(kSyntaxErrorRC,"Invalid parameter to MIDI file report test."); + + char* mfn = filesys::expandPath(fn); + rc = report(mfn,log::globalHandle() ); + mem::release(mfn); + + return rc; + } + + rc_t _testCsv( const object_t* cfg ) + { + rc_t rc = kOkRC; + const char* midiFn = nullptr; + const char* csvFn = nullptr; + + if((rc = cfg->getv("midiFn",midiFn,"csvFn",csvFn)) != kOkRC ) + return cwLogError(kSyntaxErrorRC,"Invalid parameter to MIDI to CSV file conversion."); + + char* mfn = filesys::expandPath(midiFn); + char* cfn = filesys::expandPath(csvFn); + rc = genCsvFile(mfn,cfn ); + mem::release(mfn); + mem::release(cfn); + + return rc; + } + + + + } + } +} + +cw::rc_t cw::midi::file::open( handle_t& hRef, const char* fn ){ + rc_t rc = kOkRC; + unsigned short trkIdx = 0; + + if((rc = _create(hRef)) != kOkRC ) + return rc; + + file_t* p = _handleToPtr(hRef); + + // open the file + if((rc = cw::file::open(p->fh,fn, cw::file::kReadFl | cw::file::kBinaryFl)) != kOkRC ) + { + rc = cwLogError(rc,"MIDI file open failed."); + goto errLabel; + } + + // read header and setup track array + if(( rc = _readHdr(p)) != kOkRC ) + goto errLabel; + + while( !cw::file::eof(p->fh) && trkIdx < p->trkN ) + { + unsigned chkId = 0,chkN=0; + + // read the chunk id + if((rc = _read32(p,&chkId)) != kOkRC ) + goto errLabel; + + // read the chunk size + if((rc = _read32(p,&chkN)) != kOkRC ) + goto errLabel; + + // if this is not a trk chunk then skip it + if( chkId != (unsigned)'MTrk') + { + //if( fseek( p->fp, chkN, SEEK_CUR) != 0 ) + if((rc = cw::file::seek(p->fh,cw::file::kCurFl,chkN)) != kOkRC ) + { + rc = cwLogError(rc,"MIDI file seek failed."); + goto errLabel; + } + } + else + { + if((rc = _readTrack(p,trkIdx)) != kOkRC ) + goto errLabel; + + ++trkIdx; + } + } + + // store the file name + p->fn = mem::duplStr(fn); + + p->msgVDirtyFl = true; + _linearize(p); + + errLabel: + + if((rc = cw::file::close(p->fh)) != kOkRC ) + rc = cwLogError(rc,"MIDI file close failed."); + + if( rc != kOkRC ) + { + _close(p); + hRef.clear(); + } + + return rc; +} + +cw::rc_t cw::midi::file::create( handle_t& hRef, unsigned trkN, unsigned ticksPerQN ) +{ + rc_t rc = kOkRC; + + if((rc = _create(hRef)) != kOkRC ) + return rc; + + file_t* p = _handleToPtr(hRef); + + p->ticksPerQN = ticksPerQN; + p->fmtId = 1; + p->trkN = trkN; + p->trkV = mem::allocZ(p->trkN); + + return rc; +} + + +cw::rc_t cw::midi::file::close( handle_t& hRef ) +{ + rc_t rc = kOkRC; + + if( !hRef.isValid() ) + return kOkRC; + + file_t* p = _handleToPtr(hRef); + + if((rc = _close(p)) != kOkRC ) + return rc; + + hRef.clear(); + + return rc; +} + +cw::rc_t cw::midi::file::write( handle_t h, const char* fn ) +{ + rc_t rc = kOkRC; + file_t* mfp = _handleToPtr(h); + unsigned i; + + // create the output file + if( (rc = cw::file::open(mfp->fh,fn,cw::file::kWriteFl)) != kOkRC ) + return cwLogError(rc,"The MIDI file '%s' could not be created.",cwStringNullGuard(fn)); + + // write the file header + if((rc = _writeHdr(mfp)) != kOkRC ) + { + rc = cwLogError(rc,"The file header write failed on the MIDI file '%s'.",cwStringNullGuard(fn)); + goto errLabel; + } + + for(i=0; i < mfp->trkN; ++i ) + { + unsigned chkId = 'MTrk'; + long offs0,offs1; + + // write the track chunk id ('MTrk') + if((rc = _write32(mfp,chkId)) != kOkRC ) + goto errLabel; + + cw::file::tell(mfp->fh,&offs0); + + // write the track chunk size as zero + if((rc = _write32(mfp,0)) != kOkRC ) + goto errLabel; + + if((rc = _writeTrack(mfp,i)) != kOkRC ) + goto errLabel; + + cw::file::tell(mfp->fh,&offs1); + + cw::file::seek(mfp->fh,cw::file::kBeginFl,offs0); + + _write32(mfp,offs1-offs0-4); + + cw::file::seek(mfp->fh,cw::file::kBeginFl,offs1); + + } + + errLabel: + cw::file::close(mfp->fh); + return rc; +} + + +unsigned cw::midi::file::trackCount( handle_t h ) +{ + file_t* mfp; + + if((mfp = _handleToPtr(h)) == NULL ) + return kInvalidCnt; + + return mfp->trkN; +} + +unsigned cw::midi::file::fileType( handle_t h ) +{ + file_t* mfp; + + if((mfp = _handleToPtr(h)) == NULL ) + return kInvalidId; + + return mfp->fmtId; +} + +const char* cw::midi::file::filename( handle_t h ) +{ + file_t* mfp; + if((mfp = _handleToPtr(h)) == NULL ) + return NULL; + return mfp->fn; +} + +unsigned cw::midi::file::ticksPerQN( handle_t h ) +{ + file_t* mfp; + + if((mfp = _handleToPtr(h)) == NULL ) + return kInvalidCnt; + + return mfp->ticksPerQN; +} + +uint8_t cw::midi::file::ticksPerSmpteFrame( handle_t h ) +{ + file_t* mfp; + + if((mfp = _handleToPtr(h)) == NULL ) + return kInvalidMidiByte; + + if( mfp->ticksPerQN != 0 ) + return 0; + + return mfp->smpteTicksPerFrame; +} + +uint8_t cw::midi::file::smpteFormatId( handle_t h ) +{ + file_t* mfp; + + if((mfp = _handleToPtr(h)) == NULL ) + return kInvalidMidiByte; + + if( mfp->ticksPerQN != 0 ) + return 0; + + return mfp->smpteFmtId; +} + +unsigned cw::midi::file::trackMsgCount( handle_t h, unsigned trackIdx ) +{ + file_t* mfp; + + if((mfp = _handleToPtr(h)) == NULL ) + return kInvalidCnt; + + return mfp->trkV[trackIdx].cnt; +} + + +const cw::midi::file::trackMsg_t* cw::midi::file::trackMsg( handle_t h, unsigned trackIdx ) +{ + file_t* mfp; + + if((mfp = _handleToPtr(h)) == NULL ) + return NULL; + + return mfp->trkV[trackIdx].base; +} + +unsigned cw::midi::file::msgCount( handle_t h ) +{ + file_t* mfp; + + if((mfp = _handleToPtr(h)) == NULL ) + return kInvalidCnt; + + return mfp->msgN; +} + + +const cw::midi::file::trackMsg_t** cw::midi::file::msgArray( handle_t h ) +{ + file_t* mfp; + + if((mfp = _handleToPtr(h)) == NULL ) + return NULL; + + return _msgArray(mfp); +} + + +cw::rc_t cw::midi::file::setVelocity( handle_t h, unsigned uid, uint8_t vel ) +{ + trackMsg_t* r; + file_t* mfp = _handleToPtr(h); + + assert( mfp != NULL ); + + if((r = _uidToMsg(mfp,uid)) == NULL ) + return cwLogError(kInvalidArgRC,"The MIDI file uid %i could not be found.",uid); + + if( midi::isNoteOn(r->status) == false && midi::isNoteOff(r->status,(uint8_t)0)==false ) + return cwLogError(kInvalidArgRC,"Cannot set velocity on a non-Note-On/Off msg."); + + chMsg_t* chm = (chMsg_t*)r->u.chMsgPtr; + + chm->d1 = vel; + + return kOkRC; +} + +cw::rc_t cw::midi::file::insertMsg( handle_t h, unsigned uid, int dtick, uint8_t ch, uint8_t status, uint8_t d0, uint8_t d1 ) +{ + file_t* mfp = _handleToPtr(h); + assert( mfp != NULL ); + trackMsg_t* ref = NULL; + unsigned trkIdx = kInvalidIdx; + + // if dtick is positive ... + if( dtick >= 0 ) + { + ref = _uidToMsg(mfp,uid); // ... then get the ref. msg. + trkIdx = ref->trkIdx; + } + else // if dtick is negative ... + { + // ... get get the msg before the ref. msg. + if((ref = _msgBeforeUid(mfp,uid)) != NULL ) + trkIdx = ref->trkIdx; + else + { + // ... the ref. msg was first in the track so there is no msg before it + trkIdx = _isMsgFirstOnTrack(mfp,uid); + } + } + + // verify that the reference msg was found + if( trkIdx == kInvalidIdx ) + return cwLogError(kInvalidArgRC,"The UID (%i) reference note could not be located.",uid); + + assert( trkIdx < mfp->trkN ); + + // complete the msg setup + track_t* trk = mfp->trkV + trkIdx; + trackMsg_t* m = _allocMsg(mfp, trkIdx, abs(dtick), status ); + chMsg_t* c = mem::allocZ(); + + m->u.chMsgPtr = c; + + c->ch = ch; + c->d0 = d0; + c->d1 = d1; + + // if 'm' is prior to the first msg in the track + if( ref == NULL ) + { + // ... then make 'm' the first msg in the first msg + m->link = trk->base; + trk->base = m; + // 'm' is before ref and the track cannot be empty (because ref is in it) 'm' + // can never be the last msg in the list + } + else // ref is the msg before 'm' + { + m->link = ref->link; + ref->link = m; + + // if ref was the last msg in the trk ... + if( trk->last == ref ) + trk->last = m; //... then 'm' is now the last msg in the trk + } + + trk->cnt += 1; + + mfp->msgVDirtyFl = true; + + return kOkRC; +} + +cw::rc_t cw::midi::file::insertTrackMsg( handle_t h, unsigned trkIdx, const trackMsg_t* msg ) +{ + file_t* p = _handleToPtr(h); + + // validate the track index + if( trkIdx >= p->trkN ) + return cwLogError(kInvalidArgRC,"The track index (%i) is invalid.",trkIdx); + + // allocate a new track record + trackMsg_t* m = (trackMsg_t*)mem::allocZ(sizeof(trackMsg_t)+msg->byteCnt); + + // fill the track record + m->uid = p->nextUid++; + m->atick = msg->atick; + m->status = msg->status; + m->metaId = msg->metaId; + m->trkIdx = trkIdx; + m->byteCnt = msg->byteCnt; + memcpy(&m->u,&msg->u,sizeof(msg->u)); + + // copy the exernal data + if( msg->byteCnt > 0 ) + { + m->u.voidPtr = (m+1); + memcpy((void*)m->u.voidPtr,msg->u.voidPtr,msg->byteCnt); + } + + trackMsg_t* m0 = NULL; // msg before insertion + trackMsg_t* m1 = p->trkV[trkIdx].base; // msg after insertion + + // locate the track record before and after the new msg based on 'atick' value + for(; m1!=NULL; m1=m1->link) + { + if( m1->atick > m->atick ) + { + if( m0 == NULL ) + p->trkV[trkIdx].base = m; + else + m0->link = m; + + m->link = m1; + break; + } + + m0 = m1; + } + + // if the new track record was not inserted then it is the last msg + if( m1 == NULL ) + { + assert(m0 == p->trkV[trkIdx].last); + + // link in the new msg + if( m0 != NULL ) + m0->link = m; + + // the new msg always becomes the last msg + p->trkV[trkIdx].last = m; + + // if the new msg is the first msg inserted in this track + if( p->trkV[trkIdx].base == NULL ) + p->trkV[trkIdx].base = m; + } + + // set the dtick field of the new msg + if( m0 != NULL ) + { + assert( m->atick >= m0->atick ); + m->dtick = m->atick - m0->atick; + } + + // update the dtick field of the msg following the new msg + if( m1 != NULL ) + { + assert( m1->atick >= m->atick ); + m1->dtick = m1->atick - m->atick; + } + + p->trkV[trkIdx].cnt += 1; + p->msgVDirtyFl = true; + + + + return kOkRC; + +} + +cw::rc_t cw::midi::file::insertTrackChMsg( handle_t h, unsigned trkIdx, unsigned atick, uint8_t status, uint8_t d0, uint8_t d1 ) +{ + trackMsg_t m; + chMsg_t cm; + + memset(&m,0,sizeof(m)); + memset(&cm,0,sizeof(cm)); + + cm.ch = status & 0x0f; + cm.d0 = d0; + cm.d1 = d1; + + m.atick = atick; + m.status = status & 0xf0; + m.byteCnt = sizeof(cm); + m.u.chMsgPtr = &cm; + + assert( m.status >= kNoteOffMdId && m.status <= kPbendMdId ); + + return insertTrackMsg(h,trkIdx,&m); +} + +cw::rc_t cw::midi::file::insertTrackTempoMsg( handle_t h, unsigned trkIdx, unsigned atick, unsigned bpm ) +{ + trackMsg_t m; + + memset(&m,0,sizeof(m)); + + m.atick = atick; + m.status = kMetaStId; + m.metaId = kTempoMdId; + m.u.iVal = 60000000/bpm; // convert BPM to microsPerQN + + return insertTrackMsg(h,trkIdx,&m); +} + + +unsigned cw::midi::file::seekUsecs( handle_t h, unsigned long long offsUSecs, unsigned* msgUsecsPtr, unsigned* microsPerTickPtr ) +{ + file_t* p; + + if((p = _handleToPtr(h)) == NULL ) + return kInvalidIdx; + + if( p->msgN == 0 ) + return kInvalidIdx; + + unsigned mi; + double microsPerQN = 60000000.0/120.0; + double microsPerTick = microsPerQN / p->ticksPerQN; + double accUSecs = 0; + const trackMsg_t** msgV = _msgArray(p); + + for(mi=0; mimsgN; ++mi) + { + const trackMsg_t* mp = msgV[mi]; + + if( mp->amicro >= offsUSecs ) + break; + } + + if( mi == p->msgN ) + return kInvalidIdx; + + if( msgUsecsPtr != NULL ) + *msgUsecsPtr = round(accUSecs - offsUSecs); + + if( microsPerTickPtr != NULL ) + *microsPerTickPtr = round(microsPerTick); + + return mi; +} + +/* + 1.Move closest previous tempo msg to begin. + 2.The first msg in each track must be the first msg >= begin.time + 3.Remove all msgs > end.time - except the 'endMsg' for each note/pedal that is active at end time. + + +*/ + + +double cw::midi::file::durSecs( handle_t h ) +{ + file_t* mfp = _handleToPtr(h); + + if( mfp->msgN == 0 ) + return 0; + + const trackMsg_t** msgV = _msgArray(mfp); + + return msgV[ mfp->msgN-1 ]->amicro / 1000000.0; +} + +void cw::midi::file::calcNoteDurations( handle_t h, unsigned flags ) +{ + file_t* p; + bool warningFl = cwIsFlag(flags,kWarningsMfFl); + + if((p = _handleToPtr(h)) == NULL ) + return; + + if( p->msgN == 0 ) + return; + + unsigned mi = kInvalidId; + trackMsg_t* noteM[ kMidiNoteCnt * kMidiChCnt ]; // ptr to note-on or NULL if the note is not sounding + trackMsg_t* sustV[ kMidiChCnt ]; // ptr to last sustain pedal down msg or NULL if susteain pedal is not down + trackMsg_t* sostV[ kMidiChCnt ]; // ptr to last sost. pedal down msg or NULL if sost. pedal is not down + int noteGateM[ kMidiNoteCnt * kMidiChCnt ]; // true if the associated note key is depressed + bool sostGateM[ kMidiNoteCnt * kMidiChCnt ]; // true if the associated note was active when the sost. pedal went down + int sustGateV[ kMidiChCnt]; // true if the associated sustain pedal is down + int sostGateV[ kMidiChCnt]; // true if the associated sostenuto pedal is down + unsigned i,j; + unsigned n = 0; + + const trackMsg_t** msgV = _msgArray(p); + + // initialize the state tracking variables + for(i=0; imsgN; ++mi) + { + trackMsg_t* m = (trackMsg_t*)msgV[mi]; // cast away const + + // verify that time is also incrementing + assert( mi==0 || (mi>0 && m->amicro >= msgV[mi-1]->amicro) ); + + // ignore all non-channel messages + if( !isChStatus( m->status ) ) + continue; + + uint8_t ch = m->u.chMsgPtr->ch; // get the midi msg channel + uint8_t d0 = m->u.chMsgPtr->d0; // get the midi msg data value + + // if this is a note-on msg + if( isNoteOn(m) ) + { + unsigned k = ch*kMidiNoteCnt + d0; + + // there should be no existing sounding note instance for this pitch + if( noteGateM[k] == 0 && noteM[k] != NULL ) + { + if( warningFl ) + cwLogWarning("%i : Missing note-off instance for note on:%s",m->uid,midi::midiToSciPitch(d0,NULL,0)); + + if( cwIsFlag(flags,kDropReattacksMfFl) ) + { + m->flags |= kDropTrkMsgFl; + n += 1; + } + + } + // if this is a re-attack + if( noteM[k] != NULL ) + noteGateM[k] += 1; + else // this is a new attack + { + noteM[k] = m; + noteGateM[k] = 1; + } + } + else + + // if this is a note-off msg + if( isNoteOff(m) ) + { + unsigned k = ch*kMidiNoteCnt + d0; + trackMsg_t* m0 = noteM[k]; + + if( m0 == NULL ) + cwLogWarning("%i : Missing note-on instance for note-off:%s",m->uid,midi::midiToSciPitch(d0,NULL,0)); + else + { + // a key was released - so it should not already be up + if( noteGateM[k]==0 ) + cwLogWarning("%i : Missing note-on for note-off:%s",m->uid,midi::midiToSciPitch(d0,NULL,0)); + else + { + noteGateM[k] -= 1; // update the note gate state + + // update the sounding note status + if( _calcNoteDur(m0, m, noteGateM[k], sustGateV[ch], sostGateM[k]) ) + noteM[k] = NULL; + } + } + } + else + + // This is a sustain-pedal down msg + if( isSustainPedalDown(m) ) + { + // if the sustain channel is already down + if( warningFl && sustGateV[ch] ) + cwLogWarning("%i : The sustain pedal went down twice with no intervening release.",m->uid); + + sustGateV[ch] += 1; + + if( sustV[ch] == NULL ) + sustV[ch] = m; + + } + else + + // This is a sustain-pedal up msg + if( isSustainPedalUp(m) ) + { + // if the sustain channel is already up + if( sustGateV[ch]==0 ) + cwLogWarning("%i : The sustain pedal release message was received with no previous pedal down.",m->uid); + + if( sustGateV[ch] >= 1 ) + { + sustGateV[ch] -= 1; + + if( sustGateV[ch] == 0 ) + { + int k = ch*kMidiNoteCnt; + + // for each sounding note on this channel + for(; ku.chMsgPtr)->end = m; // set the pedal-up msg ptr. in the pedal-down msg. + sustV[ch] = NULL; + } + } + } + } + else + + // This is a sostenuto pedal-down msg + if( isSostenutoPedalDown(m) ) + { + // if the sustain channel is already down + if( warningFl && sostGateV[ch] ) + cwLogWarning("%i : The sostenuto pedal went down twice with no intervening release.",m->uid); + + // record the notes that are active when the sostenuto pedal went down + unsigned k = ch * kMidiNoteCnt; + for(i=0; i 0; + + sostGateV[ch] += 1; + } + else + + // This is a sostenuto pedal-up msg + if( isSostenutoPedalUp(m) ) + { + // if the sustain channel is already up + if( sostGateV[ch]==0 ) + cwLogWarning("%i : The sostenuto pedal release message was received with no previous pedal down.",m->uid); + + if( sostGateV[ch] >= 1 ) + { + sostGateV[ch] -= 1; + + if( sostGateV[ch] == 0 ) + { + int k = ch*kMidiNoteCnt; + + // for each note on this channel + for(; ku.chMsgPtr)->end = m; // set the pedal-up msg ptr. in the pedal-down msg. + sostV[ch] = NULL; + } + + } + } + } + + } // for each midi file event + + + if( warningFl ) + { + unsigned sustChN = 0; // count of channels where the sustain pedal was left on at the end of the file + unsigned sostChN = 0; // sostenuto + unsigned sustInstN = 0; // count of sustain on with no previous sustain off + unsigned sostInstN = 0; // sostenuto on + unsigned noteN = 0; // count of notes left on at the end of the file + unsigned noteInstN = 0; // count of reattacks + + // initialize the state tracking variables + for(i=0; imsgN == 0 ) + return; + + for(mi=0; mimsgN; ++mi) + { + trackMsg_t* mp = (trackMsg_t*)msgV[mi]; // cast away const + + // locate the first msg which has a non-zero delta tick + if( mp->dtick > 0 ) + { + mp->dtick = ticks; + break; + } + } +} + +namespace cw +{ + namespace midi + { + namespace file + { + void _printHdr( const file_t* mfp, log::handle_t logH ) + { + if( mfp->fn != NULL ) + cwLogPrintH(logH,"%s ",mfp->fn); + + cwLogPrintH(logH,"fmt:%i ticksPerQN:%i tracks:%i\n",mfp->fmtId,mfp->ticksPerQN,mfp->trkN); + + cwLogPrintH(logH," UID trk dtick atick amicro type ch D0 D1\n"); + cwLogPrintH(logH,"----- --- ---------- ---------- ---------- : ---- --- --- ---\n"); + + } + + void _printMsg( log::handle_t logH, const trackMsg_t* tmp ) + { + cwLogPrintH(logH,"%5i %3i %10u %10llu %10llu : ", + tmp->uid, + tmp->trkIdx, + tmp->dtick, + tmp->atick, + tmp->amicro ); + + if( tmp->status == kMetaStId ) + { + + switch( tmp->metaId ) + { + case kTempoMdId: + cwLogPrintH(logH,"%s bpm %i", metaStatusToLabel(tmp->metaId),60000000 / tmp->u.iVal); + break; + + case kTimeSigMdId: + cwLogPrintH(logH,"%s %i %i", metaStatusToLabel(tmp->metaId), tmp->u.timeSigPtr->num,tmp->u.timeSigPtr->den); + break; + + + default: + cwLogPrintH(logH,"%s ", metaStatusToLabel(tmp->metaId)); + + } + } + else + { + cwLogPrintH(logH,"%4s %3i %3i %3i", + statusToLabel(tmp->status), + tmp->u.chMsgPtr->ch, + tmp->u.chMsgPtr->d0, + tmp->u.chMsgPtr->d1); + } + + if( midi::isChStatus(tmp->status) && midi::isNoteOn(tmp->status) && (tmp->u.chMsgPtr->d1>0) ) + cwLogPrintH(logH," %4s ",midi::midiToSciPitch(tmp->u.chMsgPtr->d0,NULL,0)); + + + cwLogPrintH(logH,"\n"); + } + + rc_t _printCsvHdr( cw::file::handle_t fH ) + { + return cw::file::printf(fH,"UID,trk,dtick,atick,amicro,type,ch,D0,D1\n"); + } + + rc_t _printCsvRow( cw::file::handle_t fH, const trackMsg_t* m ) + { + cw::file::printf(fH,"%5i,%3i,%10u,%10llu,%10llu", + m->uid, + m->trkIdx, + m->dtick, + m->atick, + m->amicro ); + + if( m->status == kMetaStId ) + { + + switch( m->metaId ) + { + case kTempoMdId: + cw::file::printf(fH,",%s,%i,bpm,", metaStatusToLabel(m->metaId),60000000 / m->u.iVal); + break; + + case kTimeSigMdId: + cw::file::printf(fH,",%s,%i,%i,", metaStatusToLabel(m->metaId), m->u.timeSigPtr->num,m->u.timeSigPtr->den); + break; + + + default: + cw::file::printf(fH,",%s,,,", metaStatusToLabel(m->metaId)); + + } + } + else + { + cw::file::printf(fH,",%4s,%3i,%3i,%3i", + statusToLabel(m->status), + m->u.chMsgPtr->ch, + m->u.chMsgPtr->d0, + m->u.chMsgPtr->d1); + } + + bool fl = midi::isChStatus(m->status) && midi::isNoteOn(m->status) && (m->u.chMsgPtr->d1>0); + cw::file::printf(fH,",%4s",fl ? midi::midiToSciPitch(m->u.chMsgPtr->d0,NULL,0) : ""); + + + return cw::file::printf(fH,"\n"); + } + + + } + } +} + +void cw::midi::file::printMsgs( handle_t h, log::handle_t logH ) +{ + file_t* p = _handleToPtr(h); + unsigned mi; + + + _printHdr(p,logH); + + const trackMsg_t** msgV = _msgArray(p); + + for(mi=0; mimsgN; ++mi) + { + const trackMsg_t* mp = msgV[mi]; + + if( mp != NULL ) + _printMsg(logH,mp); + } + +} + +void cw::midi::file::printTrack( handle_t h, unsigned trkIdx, log::handle_t logH ) +{ + const file_t* mfp = _handleToPtr(h); + + _printHdr(mfp,logH); + + int i = trkIdx == kInvalidIdx ? 0 : trkIdx; + int n = trkIdx == kInvalidIdx ? mfp->trkN : trkIdx+1; + + for(; itrkV[i].base; + while( tmp != NULL ) + { + _printMsg(logH,tmp); + tmp = tmp->link; + } + } +} + + +cw::midi::file::density_t* cw::midi::file::noteDensity( handle_t h, unsigned* cntRef ) +{ + int msgN = msgCount(h); + const trackMsg_t** msgs = msgArray(h); + density_t* dV = mem::allocZ(msgN); + + int i,j,k; + for(i=0,k=0; istatus == kNoteOnMdId && msgs[i]->u.chMsgPtr->d1 > 0 ) + { + dV[k].uid = msgs[i]->uid; + dV[k].amicro = msgs[i]->amicro; + + // count the number of notes occuring in the time window + // between this note and one second prior to this note. + for(j=i; j>=0; --j) + { + if( msgs[i]->amicro - msgs[j]->amicro > 1000000 ) + break; + + dV[k].density += 1; + } + + k += 1; + + } + + if( cntRef != NULL ) + *cntRef = k; + + return dV; +} + + +cw::rc_t cw::midi::file::genPlotFile( const char* midiFn, const char* outFn ) +{ + rc_t rc = kOkRC; + handle_t mfH; + cw::file::handle_t fH; + unsigned i = 0; + const trackMsg_t** m = NULL; + unsigned mN = 0; + + if((rc = open( mfH, midiFn )) != kOkRC ) + return cwLogError(rc,"The MIDI file object could not be opened from '%s'.",cwStringNullGuard(midiFn)); + + if( (m = msgArray(mfH)) == NULL || (mN = msgCount(mfH)) == 0 ) + { + rc = cwLogError(kInvalidArgRC,"The MIDI file object appears to be empty."); + goto errLabel; + } + + calcNoteDurations( mfH, 0 ); + + if((rc = cw::file::open(fH,outFn,cw::file::kWriteFl)) != kOkRC ) + return cwLogError(rc,"Unable to create the file '%s'.",cwStringNullGuard(outFn)); + + for(i=0; istatus) && midi::isNoteOn(m[i]->status) && (m[i]->u.chMsgPtr->d1>0) ) + cw::file::printf(fH,"n %f %f %i %s\n",m[i]->amicro/1000000.0,m[i]->u.chMsgPtr->durMicros/1000000.0,m[i]->uid,midi::midiToSciPitch(m[i]->u.chMsgPtr->d0,NULL,0)); + + errLabel: + + close(mfH); + cw::file::close(fH); + return rc; +} + +cw::rc_t cw::midi::file::genCsvFile( const char* midiFn, const char* csvFn ) +{ + rc_t rc = kOkRC; + handle_t mfH; + cw::file::handle_t fH; + + if((rc = open( mfH, midiFn )) != kOkRC ) + return cwLogError(rc,"The MIDI file object could not be opened from '%s'.",cwStringNullGuard(midiFn)); + + calcNoteDurations( mfH, 0 ); + + if((rc = cw::file::open(fH, csvFn,cw::file::kWriteFl)) != kOkRC ) + { + rc = cwLogError(rc,"Unable to create the CSV file '%s'.",cwStringNullGuard(csvFn)); + goto errLabel; + } + else + { + + file_t* p = _handleToPtr(mfH); + + const trackMsg_t** msgV = _msgArray(p); + + _printCsvHdr(fH); + + for(unsigned mi=0; mimsgN; ++mi) + { + const trackMsg_t* mp = msgV[mi]; + + if( mp != NULL ) + _printCsvRow(fH, mp ); + } + } + + + errLabel: + + close(mfH); + cw::file::close(fH); + return rc; +} + + +/* + cw::rc_t cw::midi::file::genSvgFile( cmCtx_t* ctx, const char* midiFn, const char* outSvgFn, const char* cssFn, bool standAloneFl, bool panZoomFl ) + { + rc_t rc = kOkRC; + cmSvgH_t svgH = cmSvgNullHandle; + handle_t mfH = nullHandle; + unsigned msgN = 0; + const trackMsg_t** msgs = NULL; + unsigned noteHeight = 10; + double micros_per_sec = 1000.0; + unsigned i; + + if((rc = open(ctx,&mfH,midiFn)) != kOkRC ) + { + rc = cwLogError(rc,"Unable to open the MIDI file '%s'.",cwStringNullGuard(midiFn)); + goto errLabel; + } + + calcNoteDurations( mfH, 0 ); + + msgN = msgCount(mfH); + msgs = msgArray(mfH); + + + if( cmSvgWriterAlloc(ctx,&svgH) != kOkSvgRC ) + { + rc = cwLogError(kSvgFailMfRC,"Unable to create the MIDI SVG output file '%s'.",cwStringNullGuard(outSvgFn)); + goto errLabel; + } + + + for(i=0; istatus == kNoteOnMdId && msgs[i]->u.chMsgPtr->d1 > 0 ) + { + const trackMsg_t* m = msgs[i]; + + + if( cmSvgWriterRect(svgH, m->amicro/micros_per_sec, m->u.chMsgPtr->d0 * noteHeight, m->u.chMsgPtr->durMicros/micros_per_sec, noteHeight-1, "note" ) != kOkSvgRC ) + rc = kSvgFailMfRC; + + const char* t0 = toSciPitch(m->u.chMsgPtr->d0,NULL,0); + + if( cmSvgWriterText(svgH, (m->amicro + (m->u.chMsgPtr->durMicros/2)) / micros_per_sec, m->u.chMsgPtr->d0 * noteHeight, t0, "text" ) != kOkSvgRC ) + rc = kSvgFailMfRC; + + } + + if( rc != kOkRC ) + { + cwLogError(rc,"SVG Shape insertion failed."); + goto errLabel; + } + + unsigned dN = 0; + density_t* dV = noteDensity( mfH, &dN ); + double t0 = 0; + double y0 = 64.0; + char* tx = NULL; + + for(i = 0; iamicro / micros_per_sec; + double y1 = dV[i].density * noteHeight; + + cmSvgWriterLine(svgH, t0, y0, t1, y1, "density" ); + cmSvgWriterText(svgH, t1, y1, tx = cmTsPrintfP(tx,"%i",dV[i].density),"dtext"); + + t0 = t1; + y0 = y1; + + } + } + + cmMemFree(dV); + cmMemFree(tx); + + if( rc == kOkRC ) + if( cmSvgWriterWrite(svgH,cssFn,outSvgFn, standAloneFl, panZoomFl) != kOkSvgRC ) + rc = cwLogError(kSvgFailMfRC,"SVG file write to '%s' failed.",cwStringNullGuard(outSvgFn)); + + + errLabel: + close(&mfH); + cmSvgWriterFree(&svgH); + + return rc; + } +*/ + +cw::rc_t cw::midi::file::report( const char* midiFn, log::handle_t logH ) +{ + handle_t mfH; + rc_t rc; + + if((rc = open(mfH,midiFn)) != kOkRC ) + { + rc = cwLogError(rc,"Unable to open the MIDI file: %s\n",cwStringNullGuard(midiFn)); + goto errLabel; + } + + printMsgs(mfH, logH ); + + errLabel: + close(mfH); + + return rc; +} + + +void cw::midi::file::printControlNumbers( const char* fn ) +{ + handle_t h; + rc_t rc; + + if((rc = open( h, fn )) != kOkRC ) + { + cwLogError(rc,"MIDI file open failed on '%s'.",fn); + goto errLabel; + } + else + { + const trackMsg_t** mm; + unsigned n = msgCount(h); + if((mm = msgArray(h)) != NULL ) + { + unsigned j; + for(j=0; jstatus == kCtlMdId && m->u.chMsgPtr->d0==66 ) + printf("%i %i\n",m->u.chMsgPtr->d0,m->u.chMsgPtr->d1); + } + } + } + errLabel: + close(h); + +} + + +cw::rc_t cw::midi::file::test( const object_t* cfg ) +{ + + rc_t rc = kOkRC; + const object_t* o; + + if((o = cfg->find("rpt")) != nullptr ) + rc = _testReport(o); + + if((o = cfg->find("csv")) != nullptr ) + rc = _testCsv(o); + + return rc; + +#ifdef NOT_DEF + rc_t rc; + handle_t h; + log::handle_t logH = log::globalHandle(); + + if((rc = open(h,fn)) != kOkRC ) + { + printf("Error:%i Unable to open the file: %s\n",rc,fn); + return; + } + + calcNoteDurations( h, 0 ); + + if( 1 ) + { + //tickToMicros( h ); + //tickToSamples(h,96000,false); + printMsgs(h,logH); + } + + if( 0 ) + { + //print(h,trackCount(h)-1,&ctx->rpt); + //print(h,kInvalidIdx,&ctx->rpt); + printControlNumbers( fn ); + + } + if( 0 ) + { + printf("Tracks:%i\n",trackCount(h)); + + unsigned i = 0; + for(i=0; istatus==kMetaStId && tmp->metaId == kTempoMdId ) + { + double bpm = 60000000.0/tmp->u.iVal; + printf("Tempo:%i %f\n",tmp->u.iVal,bpm); + + tmp->u.iVal = floor( 60000000.0/69.0 ); + + break; + } + } + + write(h,"/home/kevin/temp/test0.mid"); + } + + close(h); +#endif +} diff --git a/cwMidiFile.h b/cwMidiFile.h new file mode 100644 index 0000000..2ab11cb --- /dev/null +++ b/cwMidiFile.h @@ -0,0 +1,237 @@ +#ifndef cwMidiFile_h +#define cwMidiFile_h + +namespace cw +{ + namespace midi + { + + namespace file + { + + // MIDI file timing: + // Messages in the MIDI file are time tagged with a delta offset in 'ticks' + // from the previous message in the same track. + // + // A 'tick' can be converted to microsends as follows: + // + // microsecond per tick = micros per quarter note / ticks per quarter note + // + // MpT = MpQN / TpQN + // + // TpQN is given as a constant in the MIDI file header. + // MpQN is given as the value of the MIDI file tempo message. + // + // See seekUSecs() for an example of converting ticks to milliseconds. + // + // Notes: + // As part of the file reading process, the status byte of note-on messages + // with velocity=0 are is changed to a note-off message. See _readChannelMsg(). + + + typedef struct + { + uint8_t hr; + uint8_t min; + uint8_t sec; + uint8_t frm; + uint8_t sfr; + } smpte_t; + + typedef struct + { + uint8_t num; + uint8_t den; + uint8_t metro; + uint8_t th2s; + } timeSig_t; + + typedef struct + { + uint8_t key; + uint8_t scale; + } keySig_t; + + struct midiTrackMsg_str; + + typedef struct + { + uint8_t ch; + uint8_t d0; + uint8_t d1; + unsigned durMicros; // note duration in microseconds (corrected for tempo changes) + struct trackMsg_str* end; // note-off or pedal-up message + } chMsg_t; + + enum + { + kDropTrkMsgFl = 0x01 + }; + + typedef struct trackMsg_str + { + unsigned flags; // see k???TrkMsgFl + unsigned uid; // uid's are unique among all msg's in the file + unsigned dtick; // delta ticks between events on this track (ticks between this event and the previous event on this track) + unsigned long long atick; // global (all tracks interleaved) accumulated ticks + unsigned long long amicro; // global (all tracks interleaved) accumulated microseconds adjusted for tempo changes + uint8_t status; // ch msg's have the channel value removed (it is stored in u.chMsgPtr->ch) + uint8_t metaId; // + unsigned short trkIdx; // + unsigned byteCnt; // length of data pointed to by u.voidPtr (or any other pointer in the union) + struct trackMsg_str* link; // link to next record in this track + + union + { + uint8_t bVal; + unsigned iVal; + unsigned short sVal; + const char* text; + const void* voidPtr; + const smpte_t* smptePtr; + const timeSig_t* timeSigPtr; + const keySig_t* keySigPtr; + const chMsg_t* chMsgPtr; + const uint8_t* sysExPtr; + } u; + } trackMsg_t; + + inline bool isNoteOn(const trackMsg_t* m) { return midi::isNoteOn(m->status) && (m->u.chMsgPtr->d1>0); } + inline bool isNoteOff(const trackMsg_t* m) { return midi::isNoteOff(m->status,m->u.chMsgPtr->d1); } + + inline bool isPedalUp(const trackMsg_t* m) { return midi::isPedalUp( m->status, m->u.chMsgPtr->d0, m->u.chMsgPtr->d1); } + inline bool isPedalDown(const trackMsg_t* m) { return midi::isPedalDown( m->status, m->u.chMsgPtr->d0, m->u.chMsgPtr->d1); } + + inline bool isSustainPedalUp(const trackMsg_t* m) { return midi::isSustainPedalUp( m->status,m->u.chMsgPtr->d0,m->u.chMsgPtr->d1); } + inline bool isSustainPedalDown(const trackMsg_t* m) { return midi::isSustainPedalDown( m->status,m->u.chMsgPtr->d0,m->u.chMsgPtr->d1); } + + inline bool isSostenutoPedalUp(const trackMsg_t* m) { return midi::isSostenutoPedalUp( m->status,m->u.chMsgPtr->d0,m->u.chMsgPtr->d1); } + inline bool isSostenutoPedalDown(const trackMsg_t* m) { return midi::isSostenutoPedalDown(m->status,m->u.chMsgPtr->d0,m->u.chMsgPtr->d1); } + + typedef handle handle_t; + + // Read a MIDI file. + rc_t open( handle_t& hRef, const char* fn ); + + // Create an empty MIDI file object. + rc_t create( handle_t& hRef, unsigned trkN, unsigned ticksPerQN ); + + // Release all resources associated with this MIDI file object + rc_t close( handle_t& hRef ); + + // Write this MIDI file to the specified file. + rc_t write( handle_t h, const char* fn ); + + // Return midi file format id (0,1,2) or kInvalidId if 'h' is invalid. + unsigned fileType( handle_t h ); + + // Returns ticks per quarter note or kInvalidMidiByte if 'h' is + // invalid or 0 if file uses SMPTE ticks per frame time base. + unsigned ticksPerQN( handle_t h ); + + // The file name used in an earlier call to midiFileOpen() or NULL if this + // midi file did not originate from an actual file. + const char* filename( handle_t h ); + + // Returns SMPTE ticks per frame or kInvalidMidiByte if 'h' is + // invalid or 0 if file uses ticks per quarter note time base. + uint8_t ticksPerSmpteFrame( handle_t h ); + + // Returns SMPTE format or kInvalidMidiByte if 'h' is invalid or 0 + // if file uses ticks per quarter note time base. + uint8_t smpteFormatId( handle_t h ); + + // Return the count of tracks in the file. + unsigned trackCount( handle_t h ); + + // Returns count of records in track 'trackIdx' or kInvalidCnt if 'h' is invalid. + unsigned trackMsgCount( handle_t h, unsigned trackIdx ); + + // Returns base of record chain from track 'trackIdx' or NULL if 'h' is invalid. + const trackMsg_t* trackMsg( handle_t h, unsigned trackIdx ); + + // Returns the total count of records in the midi file and the + // number in the array returned by cmMidiFileMsgArray(). + // Return kInvalidCnt if 'h' is invalid. + unsigned msgCount( handle_t h ); + + // Returns a pointer to the base of an array of pointers to all records + // in the file sorted in ascending time order. + // Returns NULL if 'h' is invalid. + const trackMsg_t** msgArray( handle_t h ); + + // Set the velocity of a note-on/off msg identified by 'uid'. + rc_t setVelocity( handle_t h, unsigned uid, uint8_t vel ); + + + // Insert a MIDI message relative to the reference msg identified by 'uid'. + // If dtick is positive/negative then the new msg is inserted after/before the reference msg. + rc_t insertMsg( handle_t h, unsigned uid, int dtick, uint8_t ch, uint8_t status, uint8_t d0, uint8_t d1 ); + + // + // Insert a new trackMsg_t into the MIDI file on the specified track. + // + // Only the following fields need be set in 'msg'. + // atick - used to position the msg in the track + // status - this field is always set (Note that channel information must stripped from the status byte and included in the channel msg data) + // metaId - this field is optional depending on the msg type + // byteCnt - used to allocate storage for the data element in 'trackMsg_t.u' + // u - the message data + // + rc_t insertTrackMsg( handle_t h, unsigned trkIdx, const trackMsg_t* msg ); + rc_t insertTrackChMsg( handle_t h, unsigned trkIdx, unsigned atick, uint8_t status, uint8_t d0, uint8_t d1 ); + rc_t insertTrackTempoMsg( handle_t h, unsigned trkIdx, unsigned atick, unsigned bpm ); + + // Return a pointer to the first msg at or after 'usecsOffs' or kInvalidIdx if no + // msg exists after 'usecsOffs'. Note that 'usecOffs' is an offset from the beginning + // of the file. + // On return *'msgUsecsPtr' is set to the actual time of the msg. + // (which will be equal to or greater than 'usecsOffs'). + unsigned seekUsecs( handle_t h, unsigned long long usecsOffs, unsigned* msgUsecsPtr, unsigned* newMicrosPerTickPtr ); + + double durSecs( handle_t h ); + + // Calculate Note Duration + enum { kWarningsMfFl=0x01, kDropReattacksMfFl=0x02 }; + void calcNoteDurations( handle_t h, unsigned flags ); + + // Set the delay prior to the first non-zero msg. + void setDelay( handle_t h, unsigned ticks ); + + void printMsgs( handle_t h, log::handle_t logH ); + void printTrack( handle_t h, unsigned trkIdx, log::handle_t logH ); + + typedef struct + { + unsigned uid; + unsigned long long amicro; + unsigned density; + } density_t; + + // Generate the note onset density measure for each note in the MIDI file. + // Delete the returned memory with a call to cmMemFree(). + density_t* noteDensity( handle_t h, unsigned* cntRef ); + + + // Generate a piano-roll plot description file which can be displayed with cmXScore.m + rc_t genPlotFile( const char* midiFn, const char* outFn ); + + rc_t genSvgFile(const char* midiFn, const char* outSvgFn, const char* cssFn, bool standAloneFl, bool panZoomFl ); + + rc_t genCsvFile( const char* midiFn, const char* csvFn ); + + // Generate a text file reportusing cmMIdiFilePrintMsgs() + rc_t report( const char* midiFn, log::handle_t logH ); + + void printControlNumbers( const char* midiFileName ); + + rc_t test( const object_t* cfg ); + + + + } + } +} + + +#endif