//| Copyright: (C) 2020-2024 Kevin Larke //| License: GNU GPL version 3.0 or above. See the accompanying LICENSE file. #include "cwCommon.h" #include "cwLog.h" #include "cwCommonImpl.h" #include "cwTest.h" #include "cwMem.h" #include "cwText.h" #include "cwObject.h" #include "cwMidi.h" #include "cwMidiFile.h" #include "cwFileSys.h" #include "cwDynRefTbl.h" #include "cwScoreParse.h" #include "cwSfScore.h" #include "cwSfMatch.h" #include "cwSfTrack.h" namespace cw { namespace sftrack { typedef struct sftrack_str { sfscore::handle_t scH; callback_func_t cbFunc; void* cbArg; sfmatch::handle_t matchH; unsigned mn; // length of midiBuf[] sfmatch::midi_t* midiBuf; // midiBuf[mn] MIDI event window result_t* res; // res[rn] result buffer unsigned rn; // length of res[] (set to 2*score event count) unsigned ri; // next avail res[] recd. double s_opt; // unsigned missCnt; // current count of consecutive trailing non-matches unsigned ili; // index into sfmatch_t.loc[] to start scan following reset unsigned eli; // index into sfmatch_t.loc[] of the last positive match. unsigned mni; // current count of MIDI events since the last call to reset() unsigned mbi; // index of oldest MIDI event in midiBuf[]; stays at 0 when the buffer is full. unsigned begSyncLocIdx; // start of score window, in mp->loc[], of best match in previous scan unsigned initHopCnt; // max window hops during the initial (when the MIDI buffer fills for first time) sync scan unsigned stepCnt; // count of forward/backward score loc's to examine for a match during _step(). unsigned maxMissCnt; // max. number of consecutive non-matches during step prior to executing a _scan(). unsigned scanCnt; // current count of times a resync-scan was executed during _step() unsigned flags; } sftrack_t; sftrack_t* _handleToPtr( handle_t h ) { return handleToPtr(h); } rc_t _destroy( sftrack_t* p ) { rc_t rc = kOkRC; destroy(p->matchH); mem::release(p->midiBuf); mem::release(p->res); mem::release(p); return rc; } rc_t _reset( sftrack_t* p, unsigned scLocIdx ) { rc_t rc = kOkRC; p->mbi = max_midi_wnd_count(p->matchH); p->mni = 0; p->begSyncLocIdx = kInvalidIdx; p->s_opt = DBL_MAX; p->missCnt = 0; p->scanCnt = 0; p->ri = 0; p->eli = kInvalidIdx; p->ili = 0; unsigned locN = loc_count(p->matchH); const sfmatch::loc_t* loc = loc_base(p->matchH); // convert scLocIdx to an index into p->mp->loc[] unsigned i = 0; for(unsigned safety_idx=0; safety_idx<10; ++safety_idx) { for(i=0; iili = i; break; } assert(locN>0); if( i!=locN || scLocIdx==loc[locN-1].scLocIdx) break; scLocIdx += 1; } if( i==locN) { rc = cwLogError(kOpFailRC, "Score matcher reset failed."); goto errLabel; } errLabel: return rc; } bool _input_midi( sftrack_t* p, double sec, unsigned smpIdx, unsigned muid, unsigned status, midi::byte_t d0, midi::byte_t d1 ) { if( (status&0xf0) != midi::kNoteOnMdId) return false; if( d1 == 0 ) return false; unsigned mi = p->mn-1; //printf("%3i %4s\n",p->mni,cmMidiToSciPitch(d0,NULL,0)); // shift the new MIDI event onto the end of the MIDI buffer memmove(p->midiBuf, p->midiBuf+1, sizeof(sfmatch::midi_t)*mi); p->midiBuf[mi].oLocId = kInvalidIdx; p->midiBuf[mi].scEvtIdx = kInvalidIdx; p->midiBuf[mi].mni = p->mni++; p->midiBuf[mi].sec = sec; p->midiBuf[mi].smpIdx = smpIdx; p->midiBuf[mi].muid = muid; p->midiBuf[mi].pitch = d0; p->midiBuf[mi].vel = d1; if( p->mbi > 0 ) --p->mbi; return true; } void _store_result( sftrack_t* p, unsigned oLocId, unsigned scEvtIdx, unsigned flags, const sfmatch::midi_t* mp, double cost ) { // don't store missed score note results assert( mp != NULL ); bool matchFl = cwIsFlag(flags,sfmatch::kSmMatchFl); bool tpFl = oLocId!=kInvalidIdx && matchFl; bool fpFl = oLocId==kInvalidIdx || matchFl==false; result_t * rp = NULL; unsigned result_idx = kInvalidIdx; result_t r; assert( tpFl==false || (tpFl==true && oLocId != kInvalidIdx ) ); // it is possible that the same MIDI event is reported more than once // (due to step->scan back tracking) - try to find previous result records // associated with this MIDI event // TODO: This process looks expensive - as the result array grows a linear // search is done over the entire length looking for previous matches. // - and why do we want this behavior anyway? if( cwIsFlag(p->flags,kBacktrackResultsFl) ) { for(unsigned i=0; iri; ++i) if( p->res[i].mni == mp->mni ) { // if this is not the first time this note was reported and it is a true positive if( tpFl ) { rp = p->res + i; result_idx = i; break; } // a match was found but this was not a true-pos so ignore it return; } } if( rp == NULL ) { // if the result array is full ... if( p->ri >= p->rn ) { // then use a single record to hold the result so that we can still make the callback rp = &r; memset(rp,0,sizeof(r)); } else { // otherwise append select the next available record to receive the result rp = p->res + p->ri; result_idx = p->ri; ++p->ri; } } // BUG BUG BUG BUG: // for some reason oLocId seems to be set to scEvtIdx and so we replace it // with the correct value here - but this problem seems to originate in sfMatch // which is where it should be fixed if( scEvtIdx != kInvalidIdx ) { const sfscore::event_t* evt = event( p->scH, scEvtIdx ); assert(evt != nullptr ); oLocId = evt->oLocId; } rp->index = result_idx; rp->oLocId = oLocId; rp->scEvtIdx = scEvtIdx; rp->mni = mp->mni; rp->muid = mp->muid; rp->sec = mp->sec; rp->smpIdx = mp->smpIdx; rp->pitch = mp->pitch; rp->vel = mp->vel; rp->flags = flags | (tpFl ? sfmatch::kSmTruePosFl : 0) | (fpFl ? sfmatch::kSmFalsePosFl : 0); rp->cost = cost; if( p->cbFunc != NULL ) p->cbFunc(p->cbArg,rp); } unsigned _scan( sftrack_t* p, unsigned bli, unsigned hopCnt ) { assert( p->matchH.isValid() && sfmatch::max_midi_wnd_count(p->matchH) > 0 ); unsigned i_opt = kInvalidIdx; double s_opt = DBL_MAX; rc_t rc = kOkRC; unsigned mmn = sfmatch::max_midi_wnd_count(p->matchH); unsigned msn = sfmatch::max_score_wnd_count(p->matchH); unsigned i; // initialize the internal values set by this function p->missCnt = 0; p->eli = kInvalidIdx; p->s_opt = DBL_MAX; // if the MIDI buf is not full if( p->mbi != 0 ) return kInvalidIdx; // calc the edit distance from pitchV[] to a sliding score window for(i=0; rc==kOkRC && (hopCnt==kInvalidCnt || imatchH, bli + i, msn, p->midiBuf, mmn, s_opt ); double opt_cost = cost(p->matchH); switch(rc) { case kOkRC: // normal result if( opt_cost < s_opt ) { s_opt = opt_cost; i_opt = bli + i; } break; case kEofRC: // score window encountered the end of the score break; default: // error state return kInvalidIdx; } } // store the cost assoc'd with i_opt p->s_opt = s_opt; if( i_opt == kInvalidIdx ) return kInvalidIdx; // set the oLocId field in midiBuf[], trailing miss count and // return the latest positive-match oLocId p->eli = sfmatch::sync(p->matchH,i_opt,p->midiBuf,mmn,&p->missCnt); // if no positive matches were found if( p->eli == kInvalidIdx ) i_opt = kInvalidIdx; else { // record result for(const sfmatch::path_t* cp = optimal_path(p->matchH); cp!=NULL; cp=cp->next) if( cp->code != sfmatch::kSmInsIdx ) _store_result(p, cp->oLocId, cp->scEvtIdx, cp->flags, p->midiBuf + cp->ri - 1,p->s_opt); } return i_opt; } rc_t _step( sftrack_t* p ) { unsigned pitch = p->midiBuf[ p->mn-1 ].pitch; unsigned oLocId = kInvalidIdx; unsigned pidx = kInvalidIdx; unsigned locN = loc_count(p->matchH); const sfmatch::loc_t* loc = loc_base(p->matchH); // the tracker must be sync'd to step if( p->eli == kInvalidIdx ) return cwLogError(kInvalidArgRC, "The p->eli value must be valid to perform a step operation."); // if the end of the score has been reached if( p->eli + 1 >= locN ) return kEofRC; // attempt to match to next location first if( (pidx = match_index(loc + p->eli + 1, pitch)) != kInvalidIdx ) { oLocId = p->eli + 1; } else { // for(unsigned i=2; istepCnt; ++i) { // go forward if( p->eli+i < locN && (pidx=match_index(loc + p->eli + i, pitch))!=kInvalidIdx ) { oLocId = p->eli + i; break; } // go backward if( p->eli >= (i-1) && (pidx=match_index(loc + p->eli - (i-1), pitch))!=kInvalidIdx ) { oLocId = p->eli - (i-1); break; } } } unsigned scEvtIdx = oLocId==kInvalidIdx ? kInvalidIdx : loc[oLocId].evtV[pidx].scEvtIdx; p->midiBuf[ p->mn-1 ].oLocId = oLocId; p->midiBuf[ p->mn-1 ].scEvtIdx = scEvtIdx; if( oLocId == kInvalidIdx ) ++p->missCnt; else { p->missCnt = 0; p->eli = oLocId; } // store the result _store_result(p, oLocId, scEvtIdx, oLocId!=kInvalidIdx ? sfmatch::kSmMatchFl : 0, p->midiBuf + p->mn - 1, cost(p->matchH) ); if( p->missCnt >= p->maxMissCnt ) { unsigned begScanLocIdx = p->eli > p->mn ? p->eli - p->mn : 0; p->s_opt = DBL_MAX; unsigned bli = _scan(p,begScanLocIdx,p->mn*2); ++p->scanCnt; // if the scan failed find a match if( bli == kInvalidIdx ) return cwLogError(kOpFailRC, "Scan resync. failed."); } return kOkRC; } } } cw::rc_t cw::sftrack::create( handle_t& hRef, sfscore::handle_t scH, // Score handle. See cmScore.h. unsigned scWndN, // Length of the scores active search area. ** See Notes. unsigned midiWndN, // Length of the MIDI active note buffer. ** See Notes. unsigned flags, callback_func_t cbFunc, // A cmScMatcherCb_t function to be called to notify the recipient of changes in the score matcher status. void* cbArg ) // User argument to 'cbFunc'. { rc_t rc = kOkRC; if((rc = destroy(hRef)) != kOkRC ) return rc; if( midiWndN > scWndN ) return cwLogError(kInvalidArgRC, "The score alignment MIDI event buffer length (%i) must be less than the score window length (%i).",midiWndN,scWndN); sftrack_t* p = mem::allocZ(); if(( rc = sfmatch::create(p->matchH,scH,scWndN,midiWndN)) != kOkRC ) { cwLogError(rc,"sfmatch create failed."); goto errLabel; } p->scH = scH; p->cbFunc = cbFunc; p->cbArg = cbArg; p->mn = midiWndN; p->midiBuf = mem::resize(p->midiBuf,p->mn); p->initHopCnt = 50; p->stepCnt = 3; p->maxMissCnt = p->stepCnt+1; p->rn = 2 * event_count(scH); p->res = mem::resize(p->res,p->rn); p->flags = flags; _reset(p,0); hRef.set(p); errLabel: if(rc != kOkRC ) _destroy(p); return rc; } cw::rc_t cw::sftrack::destroy( handle_t& hRef ) { rc_t rc = kOkRC; if(!hRef.isValid()) return rc; sftrack_t* p = _handleToPtr(hRef); if((rc = _destroy(p)) != kOkRC ) return rc; hRef.clear(); return rc; } cw::rc_t cw::sftrack::reset( handle_t h, unsigned scLocIdx ) { sftrack_t* p = _handleToPtr(h); return _reset(p,scLocIdx); } cw::rc_t cw::sftrack::exec( handle_t h, double sec, unsigned smpIdx, unsigned muid, unsigned status, midi::byte_t d0, midi::byte_t d1, unsigned* scLocIdxPtr ) { sftrack_t* p = _handleToPtr(h); bool fl = p->mbi > 0; rc_t rc = kOkRC; unsigned org_eli = p->eli; if( scLocIdxPtr != NULL ) *scLocIdxPtr = kInvalidIdx; // update the MIDI buffer with the incoming note if( _input_midi(p,sec,smpIdx,muid,status,d0,d1) == false ) return rc; // if the MIDI buffer transitioned to full then perform an initial scan sync. if( fl && p->mbi == 0 ) { if( (p->begSyncLocIdx = _scan(p,p->ili,p->initHopCnt)) == kInvalidIdx ) { rc = kInvalidArgRC; // signal init. scan sync. fail } else { //cmScMatcherPrintPath(p); } } else { // if the MIDI buffer is full then perform a step sync. if( !fl && p->mbi == 0 ) rc = _step(p); } // if we lost sync if( p->eli == kInvalidIdx ) { // IF WE LOST SYNC THEN WE BETTER DO SOMETHING - LIKE INCREASE THE SCAN HOPS // ON THE NEXT EVENT. p->eli = org_eli; } else { if( scLocIdxPtr!=NULL && p->eli != org_eli ) { const sfmatch::loc_t* loc = loc_base(p->matchH); // printf("LOC:%i bar:%i\n",p->eli,loc[p->eli].barNumb); *scLocIdxPtr = loc[p->eli].scLocIdx; } } return rc; } unsigned cw::sftrack::result_count( handle_t h ) { sftrack_t* p = _handleToPtr(h); return p->ri; } const cw::sftrack::result_t* cw::sftrack::result_base( handle_t h ) { sftrack_t* p = _handleToPtr(h); return p->res; } void cw::sftrack::print( handle_t h ) { sftrack_t* p = _handleToPtr(h); sfmatch::print_path( p->matchH, p->begSyncLocIdx, p->midiBuf ); } namespace cw { namespace sftrack { void _test_cb_func( void* arg, result_t* rp ) { printf("mni:%i muid:%i loc:%i scevt:%i\n",rp->mni,rp->muid,rp->oLocId,rp->scEvtIdx); } } } cw::rc_t cw::sftrack::test( const object_t* cfg, sfscore::handle_t scoreH ) { rc_t rc = kOkRC; bool report_midi_file_fl = false; bool report_track_fl = false; const object_t* perf = nullptr; bool perf_enable_fl = false; bool print_fl = false; bool backtrack_fl = false; unsigned perf_loc_idx = 0; const char* perf_midi_fname = nullptr; unsigned maxScWndN = 10; unsigned maxMidiWndN = 7; double srate = sample_rate(scoreH);; unsigned flags = 0; const midi::file::trackMsg_t** midiMsgA = nullptr; unsigned midiMsgN = 0; sftrack::handle_t trackH; midi::file::handle_t mfH; // parse the test cfg if((rc = cfg->getv("maxScWndN", maxScWndN, "maxMidiWndN", maxMidiWndN, "report_midi_file_fl",report_midi_file_fl, "report_track_fl",report_track_fl, "print_fl",print_fl, "backtrack_fl",backtrack_fl, "perf", perf)) != kOkRC ) { rc = cwLogError(rc,"sfscore test parse params failed."); goto errLabel; } if((rc = perf->getv( "enable_fl", perf_enable_fl, "loc_idx", perf_loc_idx, "midi_fname", perf_midi_fname )) != kOkRC ) { rc = cwLogError(rc,"sfscore test parse params 'perf' failed."); goto errLabel; } flags += print_fl ? kPrintFl : 0; flags += backtrack_fl ? kBacktrackResultsFl : 0; // create the score tracker if((rc = create(trackH, scoreH, maxScWndN, maxMidiWndN, flags, _test_cb_func, nullptr )) != kOkRC ) { rc = cwLogError(rc,"sftrack create failed."); goto errLabel; } if((rc = reset(trackH, perf_loc_idx )) != kOkRC ) { rc = cwLogError(rc,"sftrack reset failed."); goto errLabel; } // open the MIDI file if((rc = open(mfH, perf_midi_fname)) != kOkRC ) { rc = cwLogError(rc,"midi file create failed on '%s'",cwStringNullGuard(perf_midi_fname)); goto errLabel; } if( report_midi_file_fl ) printMsgs(mfH,log::globalHandle()); midiMsgN = msgCount(mfH); midiMsgA = msgArray(mfH); // iterate through the MIDI file for(unsigned i=0; istatus) ) { double secs = trk->amicro / 1000000.0; unsigned smpIdx = secs / srate; if((rc = exec(trackH, secs, smpIdx, trk->uid, trk->status, trk->u.chMsgPtr->d0, trk->u.chMsgPtr->d1, &scLocIdx)) != kOkRC ) { if( rc != kEofRC ) rc = cwLogError(rc,"tracker exec() failed."); goto errLabel; } } } errLabel: midi::file::close(mfH); destroy(trackH); return rc; }