#include "cmPrefix.h" #include "cmGlobal.h" #include "cmFloatTypes.h" #include "cmRpt.h" #include "cmErr.h" #include "cmCtx.h" #include "cmMem.h" #include "cmMallocDebug.h" #include "cmLinkedHeap.h" #include "cmTime.h" #include "cmMidi.h" #include "cmLex.h" #include "cmCsv.h" #include "cmSymTbl.h" #include "cmMidiFile.h" #include "cmAudioFile.h" #include "cmTimeLine.h" #include "cmText.h" #include "cmFile.h" #include "cmScore.h" #include "cmScoreMatchGraphic.h" enum { kLocSmgFl = 0x0001, kBarSmgFl = 0x0002, // score bar kNoteSmgFl = 0x0004, // score note kPedalSmgFl = 0x0008, // sore damper|sot pedal kSostSmgFl = 0x0010, // score sost pedal kMidiSmgFl = 0x0020, // midi msg kNoMatchSmgFl = 0x0040, // midi or score events that were not matched kPedalDnSmgFl = 0x0080 // score pedal is down }; // Graphic box representing a score label or MIDI event typedef struct cmSmgBox_str { unsigned flags; unsigned id; // csvEventId or midiUid unsigned left; unsigned top; unsigned width; unsigned height; cmChar_t* text0; cmChar_t* text1; struct cmSmgBox_str* link; } cmSmgBox_t; // Graphic line linking secondary MIDI matches to score events typedef struct cmSmgLine_str { cmSmgBox_t* b0; cmSmgBox_t* b1; struct cmSmgLine_str* link; } cmSmgLine_t; // Score Location typedef struct cmSmgLoc_str { cmSmgBox_t* bV; // List of graphic boxes assigned to this score location } cmSmgLoc_t; // Score label typedef struct { unsigned type; // kBarEvtScId | kNonEvtScId | kPedalEvtScId unsigned barNumb; unsigned csvEventId; unsigned locIdx; cmSmgBox_t* box; } cmSmgSc_t; // Link a MIDI event to it's matched score label. typedef struct cmSmgMatch_str { cmSmgSc_t* score; struct cmSmgMatch_str* link; } cmSmgMatch_t; // MIDI file event typedef struct { double secs; unsigned uid; unsigned pitch; unsigned vel; cmSmgMatch_t* matchV; // list of matches to score events cmSmgBox_t* box; } cmSmgMidi_t; typedef struct { cmErr_t err; cmChar_t* scFn; cmSmgSc_t* scV; // scV[scN] score bars,notes, pedals unsigned scN; cmSmgLoc_t* locV; // locV[locN] score locations (from the score file) unsigned locN; cmSmgLine_t* lines; // Graphic lines used to indicate that a midi event matches to multiple score notes. // (Each match after the first gets a line from the box representing the midi event // to the matching score event) cmChar_t* mfFn; // MIDI file name cmSmgMidi_t* mV; // mV[mN] midi note-on events unsigned mN; double mfDurSecs; // midi file duration in seconds unsigned boxW; // graphic box width and height unsigned boxH; } cmSmg_t; cmSmgH_t cmSmgNullHandle = cmSTATIC_NULL_HANDLE; cmSmg_t* _cmSmgHandleToPtr( cmSmgH_t h ) { cmSmg_t* p = (cmSmg_t*)h.h; assert(p!=NULL); return p; } cmSmgRC_t _cmSmgFree( cmSmg_t* p ) { unsigned i; for(i=0; imN; ++i) { cmSmgMatch_t* m0 = p->mV[i].matchV; cmSmgMatch_t* m1 = NULL; while(m0!=NULL) { m1 = m0->link; cmMemFree(m0); m0 = m1; } } for(i=0; ilocN; ++i) { cmSmgBox_t* b0 = p->locV[i].bV; cmSmgBox_t* b1 = NULL; while(b0!=NULL) { b1 = b0->link; cmMemFree(b0->text0); cmMemFree(b0->text1); cmMemFree(b0); b0 = b1; } } cmSmgLine_t* l0 = p->lines; cmSmgLine_t* l1 = NULL; while( l0 != NULL ) { l1 = l0->link; cmMemFree(l0); l0 = l1; } cmMemFree(p->scFn); cmMemFree(p->mfFn); cmMemFree(p->scV); cmMemFree(p->mV); cmMemFree(p->locV); cmMemFree(p); return kOkSmgRC; } cmSmgBox_t* _cmSmgInsertBox( cmSmg_t* p, unsigned locIdx, unsigned flags, unsigned id, cmChar_t* text0, cmChar_t* text1 ) { assert( locIdx < p->locN ); cmSmgBox_t* b = cmMemAllocZ(cmSmgBox_t,1); b->flags = flags; b->id = id; b->text0 = text0; b->text1 = text1; if( p->locV[locIdx].bV == NULL ) { p->locV[locIdx].bV = b; } else { cmSmgBox_t* b0 = p->locV[locIdx].bV; while( b0->link!=NULL ) b0 = b0->link; b0->link = b; } return b; } cmSmgRC_t _cmSmgInitFromScore( cmCtx_t* ctx, cmSmg_t* p, const cmChar_t* scoreFn ) { cmSmgRC_t rc = kOkSmgRC; cmScH_t scH = cmScNullHandle; unsigned i,j,k; if( cmScoreInitialize(ctx,&scH,scoreFn,44100.0, NULL, 0, NULL, NULL, cmSymTblNullHandle ) != kOkScRC ) return cmErrMsg(&p->err,kScoreFailSmgRC,"Score initializatio failed on '%s'.",cmStringNullGuard(scoreFn)); p->scFn = cmMemAllocStr(scoreFn); p->scN = cmScoreEvtCount(scH); p->scV = cmMemAllocZ(cmSmgSc_t,p->scN); p->locN = cmScoreLocCount(scH); p->locV = cmMemAllocZ(cmSmgLoc_t,p->locN); // for each score location for(i=0,k=0; ievtCnt; ++j) { const cmScoreEvt_t* e = l->evtArray[j]; unsigned flags = kNoMatchSmgFl; cmChar_t* text = NULL; switch( e->type) { case kNonEvtScId: flags |= kNoteSmgFl; text = cmMemAllocStr( cmMidiToSciPitch( e->pitch, NULL, 0)); break; case kBarEvtScId: flags |= kBarSmgFl; text = cmTsPrintfP(NULL,"%i",e->barNumb); break; case kPedalEvtScId: flags |= kPedalSmgFl; text = cmTsPrintfP(NULL,"%s", cmIsFlag(e->flags,kPedalDnScFl)?"v":"^"); if( e->pitch == kSostenutoCtlMdId ) flags |= kSostSmgFl; flags |= cmIsFlag(e->flags,kPedalDnScFl) ? kPedalDnSmgFl : 0; break; } // if e is a score event of interest then store a reference to it if( flags != kNoMatchSmgFl ) { assert( k < p->scN ); p->scV[k].type = e->type; p->scV[k].csvEventId = e->csvEventId; p->scV[k].locIdx = i; p->scV[k].barNumb = e->barNumb; p->scV[k].box = _cmSmgInsertBox(p, i, flags, e->csvEventId, text, NULL ); k += 1; } } } p->scN = k; cmScoreFinalize(&scH); return rc; } cmSmgRC_t _cmSmgInitFromMidi( cmCtx_t* ctx, cmSmg_t* p, const cmChar_t* midiFn ) { cmSmgRC_t rc = kOkSmgRC; cmMidiFileH_t mfH = cmMidiFileNullHandle; unsigned i,j; if( cmMidiFileOpen(ctx, &mfH, midiFn ) != kOkMfRC ) return cmErrMsg(&p->err,kMidiFileFailSmgRC,"MIDI file open failed on '%s'.",cmStringNullGuard(midiFn)); const cmMidiTrackMsg_t** mV = cmMidiFileMsgArray(mfH); unsigned mN = cmMidiFileMsgCount(mfH); p->mV = cmMemAllocZ(cmSmgMidi_t,mN); p->mN = mN; p->mfDurSecs = cmMidiFileDurSecs(mfH); p->mfFn = cmMemAllocStr(midiFn); for(i=0,j=0; istatus) && cmMidiIsNoteOn(mV[i]->status) && (mV[i]->u.chMsgPtr->d1>0) ) { assert(j < mN); p->mV[j].secs = mV[i]->amicro / 1000000.0; p->mV[j].uid = mV[i]->uid; p->mV[j].pitch = mV[i]->u.chMsgPtr->d0; p->mV[j].vel = mV[i]->u.chMsgPtr->d1; ++j; } p->mN = j; cmMidiFileClose(&mfH); return rc; } cmSmgRC_t cmScoreMatchGraphicAlloc( cmCtx_t* ctx, cmSmgH_t* hp, const cmChar_t* scoreFn, const cmChar_t* midiFn ) { cmSmgRC_t rc; if((rc = cmScoreMatchGraphicFree(hp)) != kOkSmgRC ) return rc; cmSmg_t* p = cmMemAllocZ(cmSmg_t,1); cmErrSetup(&p->err,&ctx->rpt,"ScoreMatchGraphic"); if((rc = _cmSmgInitFromScore(ctx,p,scoreFn)) != kOkSmgRC ) goto errLabel; if((rc = _cmSmgInitFromMidi(ctx,p,midiFn)) != kOkSmgRC ) goto errLabel; p->boxW = 30; p->boxH = 50; hp->h = p; errLabel: if( rc != kOkSmgRC ) _cmSmgFree(p); return rc; } bool cmScoreMatchGraphicIsValid( cmSmgH_t h ) { return h.h != NULL; } cmSmgRC_t cmScoreMatchGraphicFree( cmSmgH_t* hp ) { cmSmgRC_t rc = kOkSmgRC; if(hp==NULL || cmScoreMatchGraphicIsValid(*hp)==false) return kOkSmgRC; cmSmg_t* p = _cmSmgHandleToPtr(*hp); if((rc = _cmSmgFree(p)) != kOkSmgRC ) return rc; hp->h = NULL; return rc; } bool cmScoreMatchGraphic( cmSmgH_t h ) { return h.h != NULL; } cmSmgRC_t cmScoreMatchGraphicInsertMidi( cmSmgH_t h, unsigned midiUid, unsigned midiPitch, unsigned midiVel, unsigned csvScoreEventId ) { cmSmg_t* p = _cmSmgHandleToPtr(h); unsigned i,j; // if this midi event did not match any score records if( csvScoreEventId == cmInvalidId ) return kOkSmgRC; assert(midiUid != cmInvalidId ); assert(midiPitch<128 && midiVel<128); // find the midi file record which matches the event for(i=0; imN; ++i) if( p->mV[i].uid == midiUid ) { // find the score record which matches the score event id for(j=0; jscN; ++j) if( p->scV[j].csvEventId == csvScoreEventId ) { // create a match record cmSmgMatch_t* m = cmMemAllocZ(cmSmgMatch_t,1); m->score = p->scV + j; // mark the box associated with this score record as 'matched' by clearing the kNoMatchSmgFl p->scV[j].box->flags = cmClrFlag(p->scV[j].box->flags,kNoMatchSmgFl); // insert the match record in the midi files match list if( p->mV[i].matchV == NULL ) p->mV[i].matchV = m; else { cmSmgMatch_t* m0 = p->mV[i].matchV; while( m0->link != NULL ) m0 = m0->link; m0->link = m; } return kOkSmgRC; } return cmErrMsg(&p->err,kScoreFailSmgRC,"The score csv event id %i not found,",csvScoreEventId); } return cmErrMsg(&p->err,kMidiFileFailSmgRC,"MIDI uid %i not found.",midiUid); } // Create a box for each MIDI event and a line for each // match beyond the first. void _cmSmgResolveMidi( cmSmg_t* p ) { unsigned prevLocIdx = 0; unsigned i; // for each midi record for(i=0; imN; ++i) { // get the first match record for this MIDI event const cmSmgMatch_t* m = p->mV[i].matchV; // get the score location for this midi event unsigned locIdx = m==NULL ? prevLocIdx : m->score->locIdx; unsigned flags = kMidiSmgFl | (m==NULL ? kNoMatchSmgFl : 0); // set the text label for this event cmChar_t* text = cmMemAllocStr( cmMidiToSciPitch( p->mV[i].pitch, NULL, 0)); // insert a box to represent this midi event cmSmgBox_t* box = _cmSmgInsertBox( p, locIdx, flags, p->mV[i].uid, text, cmTsPrintfP(NULL,"%i",p->mV[i].uid) ); prevLocIdx = locIdx; // if this midi event matched to multiple score positions if( m != NULL && m->link != NULL ) { // insert a line for each match after the first m = m->link; for(; m!=NULL; m=m->link ) { cmSmgLine_t* l = cmMemAllocZ(cmSmgLine_t,1); l->b0 = box; l->b1 = m->score->box; l->link = p->lines; p->lines = l; } } } } void _cmSmgLayout( cmSmg_t* p ) { unsigned i; unsigned bordX = 5; unsigned bordY = 5; unsigned top = p->boxH + 2*bordY; unsigned left = bordX; for(i=0; ilocN; ++i) { cmSmgLoc_t* l = p->locV + i; cmSmgBox_t* b = l->bV; // for each box attached to this location for(; b!=NULL; b=b->link) { // bar boxes are always drawn at the top of the column if( cmIsFlag(b->flags,kBarSmgFl) ) b->top = bordY; else { b->top = top; top += p->boxH + bordY; } b->left = left; b->width = p->boxW; b->height = p->boxH; } left += p->boxW + bordX; top = p->boxH + 2*bordY; } } void _cmSmgSvgSize( cmSmg_t* p, unsigned* widthRef, unsigned* heightRef ) { unsigned i; unsigned maxWidth = 0; unsigned maxHeight = 0; for(i=0; ilocN; ++i) { cmSmgBox_t* b = p->locV[i].bV; for(; b != NULL; b=b->link ) { if( b->left + b->width > maxWidth ) maxWidth = b->left + b->width; if( b->top + b->height > maxHeight ) maxHeight = b->top + b->height; } } *widthRef = maxWidth; *heightRef = maxHeight; } cmSmgRC_t cmScoreMatchGraphicWrite( cmSmgH_t h, const cmChar_t* fn ) { cmSmg_t* p = _cmSmgHandleToPtr(h); cmFileH_t fH = cmFileNullHandle; unsigned svgHeight = 0; unsigned svgWidth = 0; unsigned i; // BUG BUG BUG : this can only be called once // create a box for each midi event _cmSmgResolveMidi( p ); // layout the boxes _cmSmgLayout( p ); if( cmFileOpen(&fH,fn,kWriteFileFl,p->err.rpt) != kOkFileRC ) return cmErrMsg(&p->err,kFileFailScRC,"Graphic file create failed for '%s'.",cmStringNullGuard(fn)); _cmSmgSvgSize(p,&svgWidth,&svgHeight); svgHeight += 10; // add a right and lower border svgWidth += 10; cmFilePrintf(fH,"\n\n\n\n",svgWidth,svgHeight); for(i=0; ilocN; ++i) { cmSmgBox_t* b = p->locV[i].bV; for(; b != NULL; b=b->link ) { const cmChar_t* classStr = "score"; if( cmIsFlag(b->flags,kLocSmgFl) ) classStr = "loc"; if( cmIsFlag(b->flags,kMidiSmgFl) ) classStr = "midi"; if( cmIsFlag(b->flags,kNoMatchSmgFl) ) if( cmIsFlag(b->flags,kMidiSmgFl) ) classStr = "midi_miss"; if( cmIsFlag(b->flags,kNoMatchSmgFl) ) if( cmIsFlag(b->flags,kNoteSmgFl) ) classStr = "score_miss"; if( cmIsFlag(b->flags,kBarSmgFl) ) classStr = "bar"; if( cmIsFlag(b->flags,kPedalSmgFl) ) { if( cmIsFlag(b->flags,kSostSmgFl) ) classStr = "sost"; else classStr = "damper"; } if( cmFilePrintf(fH,"\n",b->left,b->top,b->width,b->height,classStr) != kOkFileRC ) return cmErrMsg(&p->err,kFileFailScRC,"File write failed on graphic file rect output."); if( b->text0 != NULL ) { unsigned tx = b->left + b->width/2; unsigned ty = b->top + 20; if( cmFilePrintf(fH,"%s\n",tx,ty,b->text0) != kOkFileRC ) return cmErrMsg(&p->err,kFileFailScRC,"File write failed on graphic file text output."); } if( b->text1 != NULL ) { unsigned tx = b->left + b->width/2; unsigned ty = b->top + 20 + 20; if( cmFilePrintf(fH,"%s\n",tx,ty,b->text1) != kOkFileRC ) return cmErrMsg(&p->err,kFileFailScRC,"File write failed on graphic file text output."); } } } cmSmgLine_t* l = p->lines; for(; l!=NULL; l=l->link) { unsigned x0 = l->b0->left + l->b0->width/2; unsigned y0 = l->b0->top + l->b0->height/2; unsigned x1 = l->b1->left + l->b1->width/2; unsigned y1 = l->b1->top + l->b1->height/2; if( cmFilePrintf(fH,"\n",x0,y0,x1,y1) != kOkFileRC ) return cmErrMsg(&p->err,kFileFailScRC,"File write failed on graphic file line output."); } cmFilePrint(fH,"\n\n\n"); cmFileClose(&fH); return kOkSmgRC; } cmSmgRC_t cmScoreMatchGraphicGenTimeLineBars( cmSmgH_t h, const cmChar_t* fn, unsigned srate ) { cmSmgRC_t rc = kOkSmgRC; cmFileH_t f = cmFileNullHandle; cmSmg_t* p = _cmSmgHandleToPtr(h); unsigned i = 0; if( cmFileOpen(&f,fn,kWriteFileFl,p->err.rpt) != kOkFileRC ) return cmErrMsg(&p->err,kFileSmgRC,"The time-line bar file '%s' could not be created.",cmStringNullGuard(fn)); // for each MIDI event for(i=0; imN; ++i) { // if this MIDI event matched a score event if( p->mV[i].matchV == NULL || p->mV[i].matchV->score == NULL ) continue; // backup the score position by one location - because we are looking for bar events // which are just prior to note events - because for our purposes they will have the same onset. cmSmgSc_t* s = p->mV[i].matchV->score - 1; for(; s >= p->scV; s-- ) { // if this is a bar event just preceding the matched MIDI event - then the bar happens at the same time as the note event if( s->type == kBarEvtScId ) break; // if this is a note event - then there is no preceding bar event if( s->type == kNonEvtScId ) break; } // if a bar was found if(s >= p->scV && s->type == kBarEvtScId) { unsigned bar = s->barNumb; unsigned offset = p->mV[i].secs * srate; unsigned smpCnt = p->mfDurSecs * srate - offset; cmFilePrintf(f,"{ label: \"%i\" type: \"mk\" ref: \"mf-0\" offset: %8i smpCnt:%8i trackId: 0 textStr: \"Bar %3i\" bar: %3i sec:\"%3i\" }\n",bar,offset,smpCnt,bar,bar,bar); } } cmFileClose(&f); return rc; } // Find the first MIDI event that matches this score event const cmSmgMidi_t* _cmScoreMatchGraphicScoreToMatchedMidiEvent( cmSmg_t* p, const cmSmgSc_t* sc ) { unsigned i; for(i=0; imN; ++i) { const cmSmgMatch_t* m = p->mV[i].matchV; for(; m != NULL; m=m->link ) if( sc->csvEventId == m->score->csvEventId ) return p->mV + i; } return NULL; } cmSmgRC_t _cmScoreMatchGraphicInsertMidiMsg( cmSmg_t* p, cmMidiFileH_t mfH, bool pedalDnFl, const cmSmgSc_t* s ) { const cmSmgMidi_t* m; // locate the MIDI event associated with the reference event if((m =_cmScoreMatchGraphicScoreToMatchedMidiEvent( p, s )) == NULL ) return cmErrWarnMsg(&p->err,kMatchFailSmgRC,"A sostenuto pedal msg could not be aligned to a note event."); int dtick_offset = pedalDnFl ? 1 : -1; cmMidiByte_t midi_vel = pedalDnFl ? 64 : 0; cmMidiByte_t midi_ch = 0; //printf("pedal:%s\n",pedalDnFl?"down":"up"); // insert a pedal msg relative to the reference event if( cmMidiFileInsertMsg(mfH, m->uid, dtick_offset, midi_ch, kCtlMdId, kSostenutoCtlMdId, midi_vel ) != kOkMfRC ) return cmErrWarnMsg(&p->err,kMidiFileFailSmgRC,"MIDI msg insert failed."); return kOkSmgRC; } cmSmgRC_t _cmScoreMatchGraphicUpdateSostenuto( cmSmg_t* p, cmMidiFileH_t mfH ) { cmSmgRC_t rc = kOkSmgRC; unsigned i, j = cmInvalidIdx; bool pedalUpFl = false; // for each score event for(i=0; iscN; ++i) { switch( p->scV[i].type ) { case kNonEvtScId: { // if the pedalUpFl is set then insert a sost-pedal-up msg before this note. if( pedalUpFl ) { _cmScoreMatchGraphicInsertMidiMsg(p, mfH, false, p->scV + i ); pedalUpFl = false; } j = i; // store the index of this note event (it may be needed if the next event is a sost. pedal evt.) } break; case kPedalEvtScId: // if this is a sost pedal event if( cmIsFlag(p->scV[i].box->flags,kSostSmgFl) ) { if( cmIsFlag(p->scV[i].box->flags,kPedalDnSmgFl) ) { assert( j != cmInvalidIdx ); _cmScoreMatchGraphicInsertMidiMsg(p, mfH, true, p->scV + j ); } else { pedalUpFl = true; // insert a pedal up message before the next note-on } } default: break; } } return rc; } cmSmgRC_t cmScoreMatchGraphicUpdateMidiFromScore( cmCtx_t* ctx, cmSmgH_t h, const cmChar_t* newMidiFn ) { cmSmgRC_t rc = kOkSmgRC; cmSmg_t* p = _cmSmgHandleToPtr(h); unsigned i = 0; cmMidiFileH_t mfH = cmMidiFileNullHandle; cmScH_t scH = cmScNullHandle; // open the MIDI file if( cmMidiFileOpen(ctx, &mfH, p->mfFn ) != kOkMfRC ) return cmErrMsg(&p->err,kMidiFileFailSmgRC,"MIDI file open failed on '%s'.",cmStringNullGuard(p->mfFn)); // instantiate the score from the score CSV file if( cmScoreInitialize(ctx,&scH,p->scFn,44100.0, NULL, 0, NULL, NULL, cmSymTblNullHandle ) != kOkScRC ) { rc = cmErrMsg(&p->err,kScoreFailSmgRC,"Score initializatio failed on '%s'.",cmStringNullGuard(p->scFn)); goto errLabel; } // for each MIDI note-on event for(i=0; imN; ++i) { cmSmgMidi_t* mr = p->mV + i; // only update midi events which were matched exactly once if( mr->matchV==NULL || mr->matchV->link!=NULL ) continue; // locate the matched score event const cmScoreEvt_t* s= cmScoreIdToEvt( scH, mr->matchV->score->csvEventId ); assert( s!=NULL ); // assign the score velocity to the MIDI note if(cmMidiFileSetVelocity( mfH, mr->uid, s->vel ) != kOkMfRC ) { rc = cmErrMsg(&p->err,kOpFailSmgRC,"Set velocify operation failed."); goto errLabel; } } // update the sostenuto pedal msg's in the MIDI file. _cmScoreMatchGraphicUpdateSostenuto(p, mfH ); // write the updated MIDI file if( cmMidiFileWrite( mfH, newMidiFn ) != kOkMfRC ) { rc = cmErrMsg(&p->err,kMidiFileFailSmgRC,"MIDI file write failed on '%s'.",cmStringNullGuard(newMidiFn)); goto errLabel; } errLabel: cmMidiFileClose(&mfH); cmScoreFinalize(&scH); return rc; }