From 5e3130503d6e88b3d8f22839425eeb235b9b382c Mon Sep 17 00:00:00 2001 From: kevin Date: Sat, 5 Aug 2023 12:35:29 -0400 Subject: [PATCH] cwSfMatch.h/cpp, cwSfTrack.h/cpp, Makefile.am : Initial commit. --- Makefile.am | 7 +- cwSfMatch.cpp | 894 ++++++++++++++++++++++++++++++++++++++++++++++++++ cwSfMatch.h | 179 ++++++++++ cwSfTrack.cpp | 569 ++++++++++++++++++++++++++++++++ cwSfTrack.h | 123 +++++++ 5 files changed, 1770 insertions(+), 2 deletions(-) create mode 100644 cwSfMatch.cpp create mode 100644 cwSfMatch.h create mode 100644 cwSfTrack.cpp create mode 100644 cwSfTrack.h diff --git a/Makefile.am b/Makefile.am index 1914939..5e27ade 100644 --- a/Makefile.am +++ b/Makefile.am @@ -67,8 +67,11 @@ libcwSRC += src/libcw/cwIoMidiRecordPlay.cpp src/libcw/cwIoAudioRecordPlay.cpp libcwHDR += src/libcw/cwIoPresetSelApp.h src/libcw/cwPianoScore.h src/libcw/cwPresetSel.h src/libcw/cwVelTableTuner.h libcwSRC += src/libcw/cwIoPresetSelApp.cpp src/libcw/cwPianoScore.cpp src/libcw/cwPresetSel.cpp src/libcw/cwVelTableTuner.cpp -libcwHDR += src/libcw/cwCmInterface.h src/libcw/cwScoreFollower.h src/libcw/cwSfScoreParser.h src/libcw/cwSfScore.h -libcwSRC += src/libcw/cwCmInterface.cpp src/libcw/cwScoreFollower.cpp src/libcw/cwSfScoreParser.cpp src/libcw/cwSfScore.cpp +libcwHDR += src/libcw/cwCmInterface.h src/libcw/cwScoreFollower.h +libcwSRC += src/libcw/cwCmInterface.cpp src/libcw/cwScoreFollower.cpp + +libcwHDR += src/libcw/cwSfScoreParser.h src/libcw/cwSfScore.h src/libcw/cwSfMatch.h src/libcw/cwSfTrack.h +libcwSRC += src/libcw/cwSfScoreParser.cpp src/libcw/cwSfScore.cpp src/libcw/cwSfMatch.cpp src/libcw/cwSfTrack.cpp libcwHDR += src/libcw/cwScoreFollowerPerf.h src/libcw/cwMidiState.h src/libcw/cwSvgMidi.h src/libcw/cwSvgScoreFollow.h libcwSRC += src/libcw/cwMidiState.cpp src/libcw/cwSvgMidi.cpp src/libcw/cwSvgScoreFollow.cpp diff --git a/cwSfMatch.cpp b/cwSfMatch.cpp new file mode 100644 index 0000000..15f1818 --- /dev/null +++ b/cwSfMatch.cpp @@ -0,0 +1,894 @@ +#include "cwCommon.h" +#include "cwLog.h" +#include "cwCommonImpl.h" +#include "cwMem.h" +#include "cwText.h" +#include "cwObject.h" +#include "cwMidi.h" +#include "cwFileSys.h" +#include "cwSfScore.h" +#include "cwSfMatch.h" + +namespace cw +{ + namespace sfmatch + { + sfmatch_t* _handleToPtr( handle_t h ) + { return handleToPtr(h); } + + rc_t _destroy( sfmatch_t* p ) + { + rc_t rc = kOkRC; + + unsigned i; + for(i=0; ilocN; ++i) + mem::release(p->loc[i].evtV); + + mem::release(p->loc); + mem::release(p->m); + mem::release(p->p_mem); + + mem::release(p); + return rc; + }; + + void _cmScMatchInitLoc( sfmatch_t* p ) + { + unsigned li,ei; + + p->locN = event_count(p->scH); + p->loc = mem::resizeZ(p->loc,p->locN); + + + // for each score location + for(li=0,ei=0; liscH); ++li) + { + unsigned i,n; + + const sfscore::loc_t* lp = loc(p->scH,li); + + // count the number of note events at location li + for(n=0,i=0; ievtCnt; ++i) + if( lp->evtArray[i]->type == sfscore::kNonEvtScId ) + ++n; + + assert( ei+n <= p->locN ); + + // duplicate each note at location li n times + for(i=0; iloc[ei+i].evtCnt = n; + p->loc[ei+i].evtV = mem::allocZ(n); + p->loc[ei+i].scLocIdx = li; + p->loc[ei+i].barNumb = lp->barNumb; + + for(j=0,k=0; jevtCnt; ++j) + if( lp->evtArray[j]->type == sfscore::kNonEvtScId ) + { + p->loc[ei+i].evtV[k].pitch = lp->evtArray[j]->pitch; + p->loc[ei+i].evtV[k].scEvtIdx = lp->evtArray[j]->index; + ++k; + } + } + + ei += n; + } + + assert(ei<=p->locN); + p->locN = ei; + } + + rc_t _cmScMatchInitMtx( sfmatch_t* p, unsigned rn, unsigned cn ) + { + //if( rn >p->mrn && cn > p->mcn ) + if( rn*cn > p->mrn*p->mcn ) + { + return cwLogError(kInvalidArgRC, "MIDI sequence length must be less than %i. Score sequence length must be less than %i.",p->mmn,p->msn); + } + + // if the size of the mtx is not changing then there is nothing to do + if( rn == p->rn && cn == p->cn ) + return kOkRC; + + // update the mtx size + p->rn = rn; + p->cn = cn; + + // fill in the default values for the first row + // and column of the DP matrix + unsigned i,j,k; + for(i=0; im[ i + (j*rn) ].v[k] = v[k]; + } + + return kOkRC; + } + + value_t* _cmScMatchValPtr( sfmatch_t* p, unsigned i, unsigned j, unsigned rn, unsigned cn ) + { + assert( i < rn && j < cn ); + + return p->m + i + (j*rn); + } + + bool _cmScMatchIsTrans( sfmatch_t* p, const midi_t* midiV, const value_t* v1p, unsigned bsi, unsigned i, unsigned j, unsigned rn, unsigned cn ) + { + bool fl = false; + value_t* v0p = _cmScMatchValPtr(p,i,j,rn,cn); + + if( i>=1 && j>=1 + && v1p->v[kSmMinIdx] == v1p->v[kSmSubIdx] + && cwIsNotFlag(v1p->flags,kSmMatchFl) + && v0p->v[kSmMinIdx] == v0p->v[kSmSubIdx] + && cwIsNotFlag(v0p->flags,kSmMatchFl) + ) + { + unsigned c00 = midiV[i-1].pitch; + unsigned c01 = midiV[i ].pitch; + loc_t* c10 = p->loc + bsi + j - 1; + loc_t* c11 = p->loc + bsi + j; + fl = is_match(c11,c00) && is_match(c10,c01); + } + return fl; + } + + unsigned _cmScMatchMin( sfmatch_t* p, unsigned i, unsigned j, unsigned rn, unsigned cn ) + { + return _cmScMatchValPtr(p,i,j,rn,cn)->v[kSmMinIdx]; + } + + // Return false if bsi + cn > p->locN + // pitchV[rn-1] + bool _cmScMatchCalcMtx( sfmatch_t* p, unsigned bsi, const midi_t* midiV, unsigned rn, unsigned cn ) + { + // loc[begScanLocIdx:begScanLocIdx+cn-1] must be valid + if( bsi + cn > p->locN ) + return false; + + unsigned i,j; + + for(j=1; jloc + bsi + j - 1; + unsigned pitch = midiV[i-1].pitch; + value_t* vp = _cmScMatchValPtr(p,i,j,rn,cn); + unsigned idx = match_index(loc,pitch); + vp->flags = idx==kInvalidIdx ? 0 : kSmMatchFl; + vp->scEvtIdx = idx==kInvalidIdx ? kInvalidIdx : loc->evtV[idx].scEvtIdx; + unsigned cost = cwIsFlag(vp->flags,kSmMatchFl) ? 0 : 1; + vp->v[kSmSubIdx] = _cmScMatchMin(p,i-1,j-1, rn, cn) + cost; + vp->v[kSmDelIdx] = _cmScMatchMin(p,i-1,j , rn, cn) + 1; + vp->v[kSmInsIdx] = _cmScMatchMin(p,i, j-1, rn, cn) + 1; + vp->v[kSmMinIdx] = std::min( vp->v[kSmSubIdx], std::min(vp->v[kSmDelIdx],vp->v[kSmInsIdx])); + vp->flags |= _cmScMatchIsTrans(p,midiV,vp,bsi,i-1,j-1,rn,cn) ? kSmTransFl : 0; + } + + return true; + } + + void _cmScMatchPrintMtx( sfmatch_t* r, unsigned rn, unsigned cn) + { + unsigned i,j,k; + for(i=0; iv[k]); + if( kflags,kSmMatchFl)?'m':' ',cwIsFlag(vp->flags,kSmTransFl)?'t':' '); + + } + printf("\n"); + } + } + + void _cmScMatchPathPush( sfmatch_t* r, unsigned code, unsigned ri, unsigned ci, unsigned flags, unsigned scEvtIdx ) + { + assert(r->p_avl != NULL ); + + path_t* p = r->p_avl; + r->p_avl = r->p_avl->next; + + p->code = code; + p->ri = ri; + p->ci = ci; + p->flags = code==kSmSubIdx && cwIsFlag(flags,kSmMatchFl) ? kSmMatchFl : 0; + p->flags |= cwIsFlag(flags,kSmTransFl) ? kSmTransFl : 0; + p->scEvtIdx= scEvtIdx; + p->next = r->p_cur; + r->p_cur = p; + } + + void _cmScMatchPathPop( sfmatch_t* r ) + { + assert( r->p_cur != NULL ); + path_t* tp = r->p_cur->next; + r->p_cur->next = r->p_avl; + r->p_avl = r->p_cur; + r->p_cur = tp; + } + + + double _cmScMatchCalcCandidateCost( sfmatch_t* r ) + { + path_t* cp = r->p_cur; + path_t* bp = r->p_cur; + path_t* ep = NULL; + + // skip leading inserts + for(; cp!=NULL; cp=cp->next) + if( cp->code != kSmInsIdx ) + { + bp = cp; + break; + } + + // skip to trailing inserts + for(; cp!=NULL; cp=cp->next) + if( cp->code!=kSmInsIdx ) + ep = cp; + + // count remaining path length + assert( ep!=NULL ); + unsigned n=1; + for(cp=bp; cp!=ep; cp=cp->next) + ++n; + + double gapCnt = 0; + double penalty = 0; + bool pfl = cwIsFlag(bp->flags,kSmMatchFl); + unsigned i; + + cp = bp; + for(i=0; inext) + { + // a gap is a transition from a matching subst. to an insert or deletion + //if( pc != cp->code && cp->code != kSmSubIdx && pc==kSmSubIdx && pfl==true ) + if( pfl==true && cwIsFlag(cp->flags,kSmMatchFl)==false ) + ++gapCnt; + + // + switch( cp->code ) + { + case kSmSubIdx: + penalty += cwIsFlag(cp->flags,kSmMatchFl) ? 0 : 1; + penalty -= cwIsFlag(cp->flags,kSmTransFl) ? 1 : 0; + break; + + case kSmDelIdx: + penalty += 1; + break; + + case kSmInsIdx: + penalty += 1; + break; + } + + pfl = cwIsFlag(cp->flags,kSmMatchFl); + + } + + double cost = gapCnt/n + penalty; + + //printf("n:%i gaps:%f gap_score:%f penalty:%f score:%f\n",n,gapCnt,gapCnt/n,penalty,score); + + return cost; + + } + + double _cmScMatchEvalCandidate( sfmatch_t* r, double min_cost, double cost ) + { + + if( min_cost == DBL_MAX || cost < min_cost) + { + // copy the p_cur to p_opt[] + path_t* cp = r->p_cur; + unsigned i; + + for(i=0; cp!=NULL && ipn; cp=cp->next,++i) + { + r->p_opt[i].code = cp->code; + r->p_opt[i].ri = cp->ri; + r->p_opt[i].ci = cp->ci; + r->p_opt[i].flags = cp->flags; + r->p_opt[i].scEvtIdx= cp->scEvtIdx; + r->p_opt[i].next = cp->next==NULL ? NULL : r->p_opt + i + 1; + } + + assert( i < r->pn ); + r->p_opt[i].code = 0; // terminate with code=0 + min_cost = cost; + } + + return min_cost; + } + + + // NOTE: IF THE COST CALCULATION WAS BUILT INTO THE RECURSION THEN + // THIS FUNCTION COULD BE MADE MORE EFFICIENT BECAUSE PATHS WHICH + // EXCEEDED THE min_cost COULD BE SHORT CIRCUITED. + // + // traverse the solution matrix from the lower-right to + // the upper-left. + double _cmScMatchGenPaths( sfmatch_t* r, int i, int j, unsigned rn, unsigned cn, double min_cost ) + { + unsigned m; + + // stop when the upper-right is encountered + if( i==0 && j==0 ) + return _cmScMatchEvalCandidate(r, min_cost, _cmScMatchCalcCandidateCost(r) ); + + value_t* vp = _cmScMatchValPtr(r,i,j,rn,cn); + + // for each possible dir: up,left,up-left + for(m=1; mv[m] == vp->v[kSmMinIdx] ) + { + // prepend to the current candidate path: r->p_cur + _cmScMatchPathPush(r,m,i,j,vp->flags,vp->scEvtIdx); + + int ii = i-1; + int jj = j-1; + + switch(m) + { + case kSmSubIdx: + break; + + case kSmDelIdx: + jj = j; + break; + + case kSmInsIdx: + ii = i; + break; + + default: + { assert(0); } + } + + // recurse! + min_cost = _cmScMatchGenPaths(r,ii,jj,rn,cn,min_cost); + + // remove the first element from the current path + _cmScMatchPathPop(r); + } + + return min_cost; + + } + + double _cmScMatchAlign( sfmatch_t* p, unsigned rn, unsigned cn, double min_cost ) + { + int i = rn-1; + int j = cn-1; + unsigned m = _cmScMatchMin(p,i,j,rn,cn); + + if( m==std::max(rn,cn) ) + printf("Edit distance is at max: %i. No Match.\n",m); + else + min_cost = _cmScMatchGenPaths(p,i,j,rn,cn,min_cost); + + return min_cost; + } + + void _cmScMatchMidiEvtFlags( sfmatch_t* p, const loc_t* lp, unsigned evtIdx, char* s, unsigned sn ) + { + const sfscore::loc_t* slp = sfscore::loc(p->scH,lp->scLocIdx); + + assert( evtIdx < slp->evtCnt ); + + const sfscore::event_t* ep = slp->evtArray[evtIdx]; + unsigned i = 0; + + s[0] = 0; + + if( cwIsFlag(ep->flags,sfscore::kEvenScFl) ) + s[i++] = 'e'; + + if( cwIsFlag(ep->flags,sfscore::kTempoScFl) ) + s[i++] = 't'; + + if( cwIsFlag(ep->flags,sfscore::kDynScFl) ) + s[i++] = 'd'; + + if( cwIsFlag(ep->flags,sfscore::kGraceScFl) ) + s[i++] = 'g'; + + s[i++] = 0; + + assert( i <= sn ); + + } + + + void _gen_match_test_input_from_score( sfscore::handle_t scoreH, unsigned begLocIdx, unsigned locCnt, double srate ) + { + printf("[\n"); + for(unsigned i=begLocIdx, k=0; ievtCnt; ++j) + { + const sfscore::event_t* e = loc->evtArray[j]; + unsigned smpIdx = e->secs * srate; + printf("{ muid:%i smpIdx:%i pitch:%i vel:%i },\n",k,smpIdx,e->pitch,e->vel); + ++k; + } + + } + printf("]\n"); + + } + } +} + +cw::rc_t cw::sfmatch::create( handle_t& hRef, sfscore::handle_t scoreH, unsigned maxScWndN, unsigned maxMidiWndN ) +{ + rc_t rc = kOkRC; + sfmatch_t* p = nullptr; + + if((rc = destroy(hRef)) != kOkRC ) + return rc; + + p = mem::allocZ(); + + p->scH = scoreH; + p->mrn = maxMidiWndN + 1; + p->mcn = maxScWndN + 1; + p->mmn = maxMidiWndN; + p->msn = maxScWndN; + + _cmScMatchInitLoc(p); + + p->m = mem::resizeZ( p->m, p->mrn*p->mcn ); + p->pn = p->mrn + p->mcn; + p->p_mem = mem::resizeZ( p->p_mem, 2*p->pn ); + p->p_avl = p->p_mem; + p->p_cur = NULL; + p->p_opt = p->p_mem + p->pn; + + // put pn path records on the available list + for(unsigned i=0; ipn; ++i) + { + p->p_mem[i].next = ipn-1 ? p->p_mem + i + 1 : NULL; + p->p_opt[i].next = ipn-1 ? p->p_opt + i + 1 : NULL; + } + + hRef.set(p); + + return rc; +} + +cw::rc_t cw::sfmatch::destroy( handle_t& hRef ) +{ + rc_t rc = kOkRC; + if(!hRef.isValid()) + return rc; + + sfmatch_t* p = _handleToPtr(hRef); + + if((rc = _destroy(p)) != kOkRC ) + goto errLabel; + + errLabel: + return rc; +} + +cw::rc_t cw::sfmatch::exec( handle_t h, unsigned locIdx, unsigned locN, const midi_t* midiV, unsigned midiN, double min_cost ) +{ + rc_t rc; + unsigned rn = midiN + 1; + unsigned cn = locN + 1; + sfmatch_t* p = _handleToPtr(h); + + // set the DP matrix default values + if((rc = _cmScMatchInitMtx(p, rn, cn )) != kOkRC ) + return rc; + + // _cmScMatchCalcMtx() returns false if the score window exceeds the length of the score + if(!_cmScMatchCalcMtx(p,locIdx, midiV, rn, cn) ) + return kEofRC; + + //_cmScMatchPrintMtx(p,rn,cn); + + // locate the path through the DP matrix with the lowest edit distance (cost) + p->opt_cost = _cmScMatchAlign(p, rn, cn, min_cost); + + return rc; +} + + +unsigned cw::sfmatch::sync( handle_t h, unsigned i_opt, midi_t* midiBuf, unsigned midiN, unsigned* missCntPtr ) +{ + sfmatch_t* p = _handleToPtr(h); + path_t* cp = p->p_opt; + unsigned missCnt = 0; + unsigned esi = kInvalidIdx; + unsigned i; + + for(i=0; cp!=NULL; cp=cp->next) + { + // there is no MIDI note associated with 'inserts' + if( cp->code != kSmInsIdx ) + { + assert( cp->ri > 0 ); + midiBuf[ cp->ri-1 ].locIdx = kInvalidIdx; + } + + switch( cp->code ) + { + case kSmSubIdx: + midiBuf[ cp->ri-1 ].locIdx = i_opt + i; + midiBuf[ cp->ri-1 ].scEvtIdx = cp->scEvtIdx; + + if( cwIsFlag(cp->flags,kSmMatchFl) ) + { + esi = i_opt + i; + missCnt = 0; + } + else + { + ++missCnt; + } + // fall through + + case kSmInsIdx: + cp->locIdx = i_opt + i; + ++i; + break; + + case kSmDelIdx: + cp->locIdx = kInvalidIdx; + ++missCnt; + break; + } + } + + if( missCntPtr != NULL ) + *missCntPtr = missCnt; + + return esi; +} + +void cw::sfmatch::print_path( handle_t h, unsigned bsi, const midi_t* midiV ) +{ + assert( bsi != kInvalidIdx ); + + sfmatch_t* p = _handleToPtr(h); + path_t* cp = p->p_opt; + path_t* pp = cp; + int polyN = 0; + int i; + + printf("loc: "); + + // get the polyphony count for the score window + for(i=0; pp!=NULL; pp=pp->next) + { + loc_t* lp = p->loc + bsi + pp->ci; + if( pp->code!=kSmDelIdx ) + { + if(lp->evtCnt > (unsigned)polyN) + polyN = lp->evtCnt; + + printf("%4i%4s ",bsi+i," "); + ++i; + } + else + printf("%4s%4s "," "," "); + } + + printf("\n"); + + // print the score notes + for(i=polyN; i>0; --i) + { + printf("%3i: ",i); + for(pp=cp; pp!=NULL; pp=pp->next) + { + + if( pp->code!=kSmDelIdx ) + { + int locIdx = bsi + pp->ci - 1; + assert(0 <= locIdx && locIdx <= (int)p->locN); + loc_t* lp = p->loc + locIdx; + + if( lp->evtCnt >= (unsigned) + i ) + { + unsigned sn = 6; + char s[sn]; + _cmScMatchMidiEvtFlags(p,lp,i-1,s,sn ); + printf("%4s%-4s ",midi::midiToSciPitch(lp->evtV[i-1].pitch,NULL,0),s); + } + else + printf("%4s%4s "," "," "); + } + else + printf("%4s%4s ", (pp->code==kSmDelIdx? "-" : " ")," "); + + /* + int locIdx = bsi + pp->ci - 1; + assert(0 <= locIdx && locIdx <= p->locN); + loc_t* lp = p->loc + locIdx; + if( pp->code!=kSmDelIdx && lp->evtCnt >= i ) + printf("%4s ",cmMidiToSciPitch(lp->evtV[i-1].pitch,NULL,0)); + else + printf("%4s ", pp->code==kSmDelIdx? "-" : " "); + */ + } + printf("\n"); + } + + printf("mid: "); + + // print the MIDI buffer + for(pp=cp; pp!=NULL; pp=pp->next) + { + if( pp->code!=kSmInsIdx ) + printf("%4s%4s ",midi::midiToSciPitch(midiV[pp->ri-1].pitch,NULL,0)," "); + else + printf("%4s%4s ",pp->code==kSmInsIdx?"-":" "," "); + } + + printf("\nvel: "); + + // print the MIDI velocity + for(pp=cp; pp!=NULL; pp=pp->next) + { + if( pp->code!=kSmInsIdx ) + printf("%4i%4s ",midiV[pp->ri-1].vel," "); + else + printf("%4s%4s ",pp->code==kSmInsIdx?"-":" "," "); + } + + printf("\nmni: "); + + // print the MIDI buffer index (mni) + for(pp=cp; pp!=NULL; pp=pp->next) + { + if( pp->code!=kSmInsIdx ) + printf("%4i%4s ",midiV[pp->ri-1].mni," "); + else + printf("%4s%4s ",pp->code==kSmInsIdx?"-":" "," "); + } + + printf("\n op: "); + + // print the substitute/insert/delete operation + for(pp=cp; pp!=NULL; pp=pp->next) + { + char c = ' '; + switch( pp->code ) + { + case kSmSubIdx: c = 's'; break; + case kSmDelIdx: c = 'd'; break; + case kSmInsIdx: c = 'i'; break; + default: + { assert(0); } + } + + printf("%4c%4s ",c," "); + } + + printf("\n "); + + // give substitute attribute (match or transpose) + for(pp=cp; pp!=NULL; pp=pp->next) + { + char s[3]; + int k = 0; + if( cwIsFlag(pp->flags,kSmMatchFl) ) + s[k++] = 'm'; + + if( cwIsFlag(pp->flags,kSmTransFl) ) + s[k++] = 't'; + + s[k] = 0; + + printf("%4s%4s ",s," "); + } + + printf("\nscl: "); + + // print the stored location index + for(pp=cp; pp!=NULL; pp=pp->next) + { + if( pp->locIdx == kInvalidIdx ) + printf("%4s%4s "," "," "); + else + printf("%4i%4s ",p->loc[pp->locIdx].scLocIdx," "); + } + + printf("\nbar: "); + + // print the stored location index + for(pp=cp; pp!=NULL; pp=pp->next) + { + if( pp->locIdx==kInvalidIdx || pp->scEvtIdx==kInvalidIdx ) + printf("%4s%4s "," "," "); + else + { + const sfscore::event_t* ep = sfscore::event(p->scH, pp->scEvtIdx ); + printf("%4i%4s ",ep->barNumb," "); + } + } + + + printf("\nsec: "); + + // print seconds + unsigned begSmpIdx = kInvalidIdx; + for(pp=cp; pp!=NULL; pp=pp->next) + { + if( pp->code!=kSmInsIdx ) + { + if( begSmpIdx == kInvalidIdx ) + begSmpIdx = midiV[pp->ri-1].smpIdx; + + printf("%2.2f%4s ", (double)(midiV[pp->ri-1].smpIdx - begSmpIdx)/96000.0," "); + + } + else + printf("%4s%4s ",pp->code==kSmInsIdx?"-":" "," "); + + + } + + + printf("\n\n"); + +} + + +cw::rc_t cw::sfmatch::test( const object_t* cfg ) +{ + rc_t rc = kOkRC; + const char* score_csv_fname = nullptr; + sfscore::dyn_ref_t* dynRefA = nullptr; + unsigned dynRefN = 0;; + double srate = 48000.0; + const object_t* dynArrayNode = nullptr; + const object_t* perf = nullptr; + const object_t* gen_perf_example = nullptr; + bool gen_perf_enable_fl = false; + unsigned gen_perf_beg_loc_idx = 0; + unsigned gen_perf_loc_cnt = 0; + midi_t* midiA = nullptr; + unsigned maxScWndN = 10; + unsigned maxMidiWndN = 7; + sfscore::handle_t scoreH; + sfmatch::handle_t matchH; + + // parse the test cfg + if((rc = cfg->getv( "cm_score_fname", score_csv_fname, + "srate", srate, + "dyn_ref", dynArrayNode, + "maxScWndN", maxScWndN, + "maxMidiWndN", maxMidiWndN, + "gen_perf_example", gen_perf_example, + "perf", perf)) != kOkRC ) + { + rc = cwLogError(rc,"sfscore test parse params failed."); + goto errLabel; + } + + if((rc = gen_perf_example->getv( "enable_fl", gen_perf_enable_fl, + "beg_loc_idx", gen_perf_beg_loc_idx, + "loc_cnt", gen_perf_loc_cnt )) != kOkRC ) + { + rc = cwLogError(rc,"sfscore test parse params 'gen_perf_example' failed."); + goto errLabel; + } + + if((rc = sfscore::parse_dyn_ref_cfg( dynArrayNode, dynRefA, dynRefN )) != kOkRC ) + { + rc = cwLogError(rc,"The reference dynamics array parse failed."); + goto errLabel; + } + + if((rc = sfscore::create(scoreH,score_csv_fname,srate,dynRefA,dynRefN)) != kOkRC ) + { + rc = cwLogError(rc,"Score test create failed."); + goto errLabel; + } + + if( gen_perf_enable_fl ) + { + _gen_match_test_input_from_score( scoreH, gen_perf_beg_loc_idx, gen_perf_loc_cnt, srate ); + } + else + { + unsigned midiN = perf->child_count(); + + midiA = mem::allocZ(midiN); + + for(unsigned i=0; ichild_ele(i); + + if((rc = node->getv("muid",midiA[i].muid, + "smpIdx",midiA[i].smpIdx, + "pitch", midiA[i].pitch, + "vel", midiA[i].vel)) != kOkRC ) + { + rc = cwLogError(rc,"parsing 'perf' record at index '%i' failed.",i); + goto errLabel; + } + } + + if((rc = create(matchH, scoreH, maxScWndN, maxMidiWndN )) != kOkRC ) + { + rc = cwLogError(rc,"Score matcher create failed."); + goto errLabel; + } + + unsigned locIdx = 0; + unsigned locN = maxScWndN; + unsigned perf_idx = 3; + unsigned perf_cnt = 4; + + if((rc = exec(matchH, locIdx, locN, midiA + perf_idx, perf_cnt, DBL_MAX )) != kOkRC ) + { + rc = cwLogError(rc,"score match failed."); + goto errLabel; + } + + unsigned eli = kInvalidIdx; + unsigned missCnt = 0; + unsigned i_opt = locIdx; + if(( eli = cw::sfmatch::sync( matchH, i_opt, midiA + perf_idx, perf_cnt, &missCnt )) == kInvalidIdx ) + { + rc = cwLogError(rc,"score match sync failed."); + goto errLabel; + } + + unsigned bsi = locIdx; + print_path( matchH, bsi, midiA ); + + } + + errLabel: + sfmatch::destroy(matchH); + sfscore::destroy(scoreH); + mem::release(dynRefA); + mem::release(midiA); + + return rc; +} diff --git a/cwSfMatch.h b/cwSfMatch.h new file mode 100644 index 0000000..cfb3312 --- /dev/null +++ b/cwSfMatch.h @@ -0,0 +1,179 @@ +#ifndef cwScoreMatch_h +#define cwScoreMatch_h + +namespace cw +{ + namespace sfmatch + { + enum + { + kSmMinIdx, // + kSmSubIdx, // 'substitute' - may or may not match + kSmDelIdx, // 'delete' - delete a MIDI note + kSmInsIdx, // 'insert' - insert a space in the score + kSmCnt + }; + + enum + { + kSmMatchFl = 0x01, + kSmTransFl = 0x02, + kSmTruePosFl = 0x04, + kSmFalsePosFl = 0x08, + kSmBarFl = 0x10, + kSmNoteFl = 0x20 + }; + + // Dynamic Programming (DP) matrix element + typedef struct + { + unsigned v[kSmCnt]; // cost for each operation + unsigned flags; // cmSmMatchFl | cmSmTransFl + unsigned scEvtIdx; + } value_t; + + // List record used to track a path through the DP matrix p->m[,] + typedef struct path_str + { + unsigned code; // kSmXXXIdx + unsigned ri; // matrix row index + unsigned ci; // matrix col index + unsigned flags; // cmSmMatchFl | cmSmTransFl + unsigned locIdx; // p->loc index or cmInvalidIdx + unsigned scEvtIdx; // scScore event index + struct path_str* next; // + } path_t; + + typedef struct event_str + { + unsigned pitch; // + unsigned scEvtIdx; // scScore event index + } event_t; + + // Score location record. + typedef struct + { + unsigned evtCnt; // count of score events at this location (i.e. a chord will have more than one event at a given location) + event_t* evtV; // evtV[evtCnt] + unsigned scLocIdx; // scH score location index + int barNumb; // bar number of this location + } loc_t; + + typedef struct + { + unsigned mni; // unique identifier for this MIDI note - used to recognize when the sfmatcher backtracks. + unsigned muid; // MIDI file event msg unique id (See cmMidiTrackMsg_t.uid) + unsigned smpIdx; // time stamp of this event + unsigned pitch; // MIDI note pitch + unsigned vel; // " " velocity + unsigned locIdx; // location assoc'd with this MIDI evt (kInvalidIdx if not a matching or non-matching 'substitute') + unsigned scEvtIdx; // sfscore event index assoc'd with this event + } midi_t; + + typedef struct sfmatch_str + { + sfscore::handle_t scH; // score handle + + unsigned locN; // Same as sfscore::loc_count() + loc_t* loc; // loc[locN] One element for each score location + + // DP matrix + unsigned mrn; // max m[] row count (midi) + unsigned rn; // cur m[] row count + unsigned mcn; // max m[] column count (score) + unsigned cn; // cur m[] column count + value_t* m; // m[mrn,mcn] DP matrix + + unsigned mmn; // max length of midiBuf[] (mrn-1) + unsigned msn; // max length of score window (mcn-1) + + unsigned pn; // mrn+mcn + path_t* p_mem; // pmem[ 2*pn ] - pre-allocated path memory + path_t* p_avl; // available path record linked list + path_t* p_cur; // current path linked list + path_t* p_opt; // p_opt[pn] - current best alignment as a linked list + double opt_cost; // last p_opt cost set by exec() + } sfmatch_t; + + typedef handle handle_t; + + // This matcher determines the optimal alignment of a short list of MIDI notes + // within a longer score window. It is designed to work for a single + // limited size alignment. + // + // 1) This matcher cannot handle multiple instances of the same pitch occuring + // at the same 'location'. + // + // 2) Because each note of a chord is spread out over multiple locations, and + // there is no way to indicate that a note in the chord is already 'in-use'. + // If a MIDI note which is part of the chord is repeated, in error, it will + // appear to be correct (a positive match will be assigned to + // the second (and possible successive notes)). + + + // maxScWndN - max length of the score window (also the max value of 'locN' in subsequent calls to exec()). + // maxMidiWndN - max length of the performance (MIDI) buffer to compare to the score window + // (also the max value of 'midiN' in subsequent calls to exec()). + rc_t create( handle_t& hRef, sfscore::handle_t scoreH, unsigned maxScWndN, unsigned maxMidiWndN ); + rc_t destroy( handle_t& hRef ); + + // Locate the position in p->loc[locIdx:locIdx+locN-1] which bests matches midiV[0:midiN]. + // The result of this function is to update p_opt[] + // The optimal path p_opt[] will only be updated if the edit_cost associated 'midiV[0:midiN]'. + // with the best match is less than 'min_cost'. + // Set 'min_cost' to DBL_MAX to force p_opt[] to be updated. + // Returns kEofRC if locIdx + locN > p->locN - note that this is not necessarily an error. + rc_t exec( handle_t h, unsigned locIdx, unsigned locN, const midi_t* midiV, unsigned midiN, double min_cost ); + + + + // This function updates the midiBuf[] fields 'locIdx' and 'scEvtIdx' after an alignment + // has been found via an earlier call to 'exec()'. + // + // Traverse the least cost path and: + // + // 1) Returns, esi, the score location index of the last MIDI note + // which has a positive match with the score and assign + // the internal score index to cp->locIdx. + // + // 2) Set cmScAlignPath_t.locIdx - index into p->loc[] associated + // with each path element that is a 'substitute' or an 'insert'. + // + // 3) Set *missCnPtr: the count of trailing non-positive matches in midiBuf[]. + // + // i_opt is index into p->loc[] of p->p_opt. + unsigned sync( handle_t h, unsigned i_opt, midi_t* midiBuf, unsigned midiN, unsigned* missCntPtr ); + + + // Print a matched path. + // cp - pointer to the first path element + // bsi - score location index of the first element in the score window which was used to form the path. + // midiV - pointer to the first element of the MIDI buffer used to form the path. + void print_path( handle_t h, unsigned bsi, const midi_t* midiV ); + + // Returns the index into loc->evtV[] of pitch. + inline unsigned match_index( const loc_t* loc, unsigned pitch ) + { + for(unsigned i=0; ievtCnt; ++i) + if( loc->evtV[i].pitch == pitch ) + return i; + return kInvalidIdx; + } + + // Return true if 'pitch' occurs at loc. + inline bool is_match( const loc_t* loc, unsigned pitch ) { return match_index(loc,pitch) != kInvalidIdx; } + + inline unsigned max_midi_wnd_count( handle_t h ) { return handleToPtr(h)->mmn; } + inline unsigned max_score_wnd_count( handle_t h ) { return handleToPtr(h)->msn; } + inline unsigned loc_count( handle_t h ) { return handleToPtr(h)->locN; } + inline const cw::sfmatch::loc_t* loc_base( handle_t h ) { return handleToPtr(h)->loc; } + inline double cost( handle_t h ) { return handleToPtr(h)->opt_cost; } + inline const cw::sfmatch::path_t* optimal_path( handle_t h ) { return handleToPtr(h)->p_opt; } + + + rc_t test( const object_t* cfg ); + + } +} + +#endif diff --git a/cwSfTrack.cpp b/cwSfTrack.cpp new file mode 100644 index 0000000..a83a0b1 --- /dev/null +++ b/cwSfTrack.cpp @@ -0,0 +1,569 @@ +#include "cwCommon.h" +#include "cwLog.h" +#include "cwCommonImpl.h" +#include "cwMem.h" +#include "cwText.h" +#include "cwObject.h" +#include "cwMidi.h" +#include "cwMidiFile.h" +#include "cwFileSys.h" +#include "cwSfScore.h" +#include "cwSfMatch.h" +#include "cwSfTrack.h" + +namespace cw +{ + namespace sftrack + { + 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; + while(1) + { + 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, 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].locIdx = kInvalidIdx; + p->midiBuf[mi].scEvtIdx = kInvalidIdx; + p->midiBuf[mi].mni = p->mni++; + 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 locIdx, unsigned scEvtIdx, unsigned flags, const sfmatch::midi_t* mp ) + { + // don't store missed score note results + assert( mp != NULL ); + bool matchFl = cwIsFlag(flags,sfmatch::kSmMatchFl); + bool tpFl = locIdx!=kInvalidIdx && matchFl; + bool fpFl = locIdx==kInvalidIdx || matchFl==false; + result_t * rp = NULL; + unsigned i; + result_t r; + + assert( tpFl==false || (tpFl==true && locIdx != 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 + for(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; + break; + } + + // a match was found but this was not a true-pos so ignore it + return; + } + + if( rp == NULL ) + { + if( p->ri >= p->rn ) + { + rp = &r; + memset(rp,0,sizeof(r)); + } + else + { + rp = p->res + p->ri; + ++p->ri; + } + } + + rp->locIdx = locIdx; + rp->scEvtIdx = scEvtIdx; + rp->mni = mp->mni; + rp->muid = mp->muid; + rp->smpIdx = mp->smpIdx; + rp->pitch = mp->pitch; + rp->vel = mp->vel; + rp->flags = flags | (tpFl ? sfmatch::kSmTruePosFl : 0) | (fpFl ? sfmatch::kSmFalsePosFl : 0); + + 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 locIdx field in midiBuf[], trailing miss count and + // return the latest positive-match locIdx + 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->locIdx, cp->scEvtIdx, cp->flags, p->midiBuf + cp->ri - 1); + } + + return i_opt; + + } + + rc_t _step( sftrack_t* p ) + { + unsigned pitch = p->midiBuf[ p->mn-1 ].pitch; + unsigned locIdx = 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 ) + { + locIdx = 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 ) + { + locIdx = p->eli + i; + break; + } + + // go backward + if( p->eli >= (i-1) && (pidx=match_index(loc + p->eli - (i-1), pitch))!=kInvalidIdx ) + { + locIdx = p->eli - (i-1); + break; + } + } + } + + unsigned scEvtIdx = locIdx==kInvalidIdx ? kInvalidIdx : loc[locIdx].evtV[pidx].scEvtIdx; + + p->midiBuf[ p->mn-1 ].locIdx = locIdx; + p->midiBuf[ p->mn-1 ].scEvtIdx = scEvtIdx; + + if( locIdx == kInvalidIdx ) + ++p->missCnt; + else + { + p->missCnt = 0; + p->eli = locIdx; + } + + // store the result + _store_result(p, locIdx, scEvtIdx, locIdx!=kInvalidIdx ? sfmatch::kSmMatchFl : 0, p->midiBuf + p->mn - 1); + + 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, + double srate, // System sample rate. + 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. + 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->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->printFl = false; + + _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, 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,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; +} + +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->locIdx,rp->scEvtIdx); + } + } +} + +cw::rc_t cw::sftrack::test( const object_t* cfg ) +{ + rc_t rc = kOkRC; + + const char* score_csv_fname = nullptr; + double srate = 48000.0; + unsigned maxScWndN = 10; + unsigned maxMidiWndN = 7; + bool report_midi_file_fl = false; + bool report_score_fl = false; + bool report_track_fl = false; + + sfscore::dyn_ref_t* dynRefA = nullptr; + unsigned dynRefN = 0;; + const object_t* dynArrayNode = nullptr; + + const object_t* perf = nullptr; + bool perf_enable_fl = false; + unsigned perf_loc_idx = 0; + const char* perf_midi_fname = nullptr; + + + const midi::file::trackMsg_t** midiMsgA = nullptr; + unsigned midiMsgN = 0; + sfscore::handle_t scoreH; + sftrack::handle_t trackH; + midi::file::handle_t mfH; + + // parse the test cfg + if((rc = cfg->getv( "cm_score_fname", score_csv_fname, + "srate", srate, + "dyn_ref", dynArrayNode, + "maxScWndN", maxScWndN, + "maxMidiWndN", maxMidiWndN, + "report_midi_file_fl",report_midi_file_fl, + "report_score_fl",report_score_fl, + "report_track_fl",report_track_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; + } + + // dynamic reference + if((rc = sfscore::parse_dyn_ref_cfg( dynArrayNode, dynRefA, dynRefN )) != kOkRC ) + { + rc = cwLogError(rc,"The reference dynamics array parse failed."); + goto errLabel; + } + + // create the score + if((rc = sfscore::create(scoreH,score_csv_fname,srate,dynRefA,dynRefN)) != kOkRC ) + { + rc = cwLogError(rc,"Score test create failed."); + goto errLabel; + } + + if( report_score_fl ) + report(scoreH); + + // create the score tracker + if((rc = create(trackH, srate, scoreH, maxScWndN, maxMidiWndN, _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) ) + if((rc = exec(trackH, 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); + sftrack::destroy(trackH); + sfscore::destroy(scoreH); + mem::release(dynRefA); + + return rc; +} diff --git a/cwSfTrack.h b/cwSfTrack.h new file mode 100644 index 0000000..1c285d3 --- /dev/null +++ b/cwSfTrack.h @@ -0,0 +1,123 @@ +#ifndef cwSfTracker_h +#define cwSfTracker_h + +namespace cw +{ + namespace sftrack + { + typedef struct result_str + { + unsigned locIdx; // index into cmScMatch_t.loc[] + unsigned scEvtIdx; // score event index + unsigned mni; // index of the performed MIDI event associated with this score location + unsigned smpIdx; // sample time index of performed MIDI event + unsigned muid; // MIDI file event msg unique id (See cmMidiTrackMsg_t.uid) + unsigned pitch; // performed pitch + unsigned vel; // performed velocity + unsigned flags; // smTruePosFl | smFalsePosFl + } result_t; + + + struct sftrack_str; + typedef void (*callback_func_t)( void* arg, result_t* rp ); + + typedef struct sftrack_str + { + callback_func_t cbFunc; + void* cbArg; + sfmatch::handle_t matchH; + unsigned mn; // size of midiBuf[] + sfmatch::midi_t* midiBuf; // midiBuf[mn] + + result_t* res; // res[rn] + 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 loc[] to start scan following reset + unsigned eli; // index into loc[] of the last positive match. + unsigned mni; // current count of MIDI events since the last call to cmScMatcherReset() + 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 cmScMatcherStep(). + 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 cmScMatcherStep() + + bool printFl; + } sftrack_t; + + typedef handle handle_t; + + rc_t create( handle_t& hRef, + double srate, // System sample rate. + 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. + 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'. + + // Notes: + // The cmScMatcher maintains an internal cmScMatch object which is used to attempt to find the + // best match between the current MIDI active note buffer and the current score search area. + // 'scWndN' is used to set the cmScMatch 'locN' argument. + // 'midiWndN' sets the length of the MIDI FIFO which is used to match to the score with + // each recceived MIDI note. + // 'midiWndN' must be <= 'scWndN'. + + rc_t destroy( handle_t& hRef ); + + // 'scLocIdx' is a score index as used by cmScoreLoc(scH) not into p->mp->loc[]. + rc_t reset( handle_t h, unsigned scLocIdx ); + + // Slide a score window 'hopCnt' times, beginning at 'bli' (an + // index into p->mp->loc[]) looking for the best match to p->midiBuf[]. + // The score window contain scWndN (p->mp->mcn-1) score locations. + // Returns the index into p->mp->loc[] of the start of the best + // match score window. The score associated + // with this match is stored in s_opt. + //unsigned scan( handle_t h, unsigned bli, unsigned hopCnt ); + + // Step forward/back by p->stepCnt from p->eli. + // p->eli must therefore be valid prior to calling this function. + // If more than p->maxMissCnt consecutive MIDI events are + // missed then automatically run cmScAlignScan(). + // Return cmEofRC if the end of the score is encountered. + // Return cmSubSysFailRC if an internal scan resync. failed. + //rc_t step( handle_t h ); + + // This function calls cmScMatcherScan() and cmScMatcherStep() internally. + // If 'status' is not kNonMidiMdId then the function returns without changing the + // state of the object. In other words the matcher only recognizes MIDI note-on messages. + // If the MIDI note passed by the call results in a successful match then + // p->eli will be updated to the location in p->mp->loc[] of the latest + // match, the MIDI note in p->midiBuf[] associated with this match + // will be assigned a valid locIdx and scLocIdx values, and *scLocIdxPtr + // will be set with the matched scLocIdx of the match. + // If this call does not result in a successful match *scLocIdxPtr is set + // to cmInvalidIdx. + // 'muid' is the unique id associated with this MIDI event under the circumstances + // that the event came from a MIDI file. See cmMidiFile.h cmMidiTrackMsg_t.uid. + // Return: + // cmOkRC - Continue processing MIDI events. + // cmEofRC - The end of the score was encountered. + // cmInvalidArgRC - scan failed or the object was in an invalid state to attempt a match. + // cmSubSysFailRC - a scan resync failed in cmScMatcherStep(). + rc_t exec( handle_t h, + unsigned smpIdx, + unsigned muid, + unsigned status, + midi::byte_t d0, + midi::byte_t d1, + unsigned* scLocIdxPtr ); + + void print( handle_t h ); + + rc_t test( const object_t* cfg ); + + } +} + + +#endif