//| 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 "cwFileSys.h" #include "cwDynRefTbl.h" #include "cwScoreParse.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_base(p->scH) + li; // count the number of note events at location li for(n=0,i=0; ievtCnt; ++i) if( lp->evtArray[i]->type == score_parse::kNoteOnTId ) ++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 == score_parse::kNoteOnTId ) { 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_base(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,score_parse::kEvenVarFl) ) s[i++] = 'e'; if( cwIsFlag(ep->flags,score_parse::kTempoVarFl) ) s[i++] = 't'; if( cwIsFlag(ep->flags,score_parse::kDynVarFl) ) 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 oLocId, 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,oLocId, 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 ].oLocId = kInvalidIdx; } switch( cp->code ) { case kSmSubIdx: midiBuf[ cp->ri-1 ].oLocId = 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->oLocId = i_opt + i; ++i; break; case kSmDelIdx: cp->oLocId = 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 oLocId = bsi + pp->ci - 1; assert(0 <= oLocId && oLocId <= (int)p->locN); loc_t* lp = p->loc + oLocId; 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 oLocId = bsi + pp->ci - 1; assert(0 <= oLocId && oLocId <= p->locN); loc_t* lp = p->loc + oLocId; 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->oLocId == kInvalidIdx ) printf("%4s%4s "," "," "); else printf("%4i%4s ",p->loc[pp->oLocId].scLocIdx," "); } printf("\nbar: "); // print the stored location index for(pp=cp; pp!=NULL; pp=pp->next) { if( pp->oLocId==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"); } unsigned cw::sfmatch::loc_to_index( handle_t h, unsigned loc ) { sfmatch_t* p = _handleToPtr(h); const loc_t* y = std::find_if(p->loc, p->loc+p->locN, [&loc](const loc_t& x){return x.scLocIdx==loc;} ); unsigned idx = y - p->loc; return idxlocN ? idx : kInvalidIdx; } cw::rc_t cw::sfmatch::test( const object_t* cfg, sfscore::handle_t scoreH ) { rc_t rc = kOkRC; 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; unsigned init_score_loc = 0; unsigned beg_perf_idx = 0; unsigned perf_cnt = 6; sfmatch::handle_t matchH; // parse the test cfg if((rc = cfg->getv( "maxScWndN", maxScWndN, "maxMidiWndN", maxMidiWndN, "gen_perf_example", gen_perf_example, "init_score_loc",init_score_loc, "beg_perf_idx", beg_perf_idx, "perf_cnt", perf_cnt, "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( gen_perf_enable_fl ) { _gen_match_test_input_from_score( scoreH, gen_perf_beg_loc_idx, gen_perf_loc_cnt, sample_rate(scoreH) ); } else { // Parse the cfg. perf[] array 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; } } // Create the cwMatch if((rc = create(matchH, scoreH, maxScWndN, maxMidiWndN )) != kOkRC ) { rc = cwLogError(rc,"Score matcher create failed."); goto errLabel; } // Set the matcher to the first location to begin matching against unsigned oLocId = loc_to_index(matchH,init_score_loc); unsigned locN = maxScWndN; // Count of MIDI events to track. perf_cnt = std::min(maxMidiWndN,perf_cnt); // Align MIDI notes to the score if((rc = exec(matchH, oLocId, locN, midiA + beg_perf_idx, perf_cnt, DBL_MAX )) != kOkRC ) { rc = cwLogError(rc,"score match failed."); goto errLabel; } // Update the cwMatch to reflect the alignment found during the exec. unsigned eli = kInvalidIdx; unsigned missCnt = 0; unsigned i_opt = oLocId; if(( eli = cw::sfmatch::sync( matchH, i_opt, midiA + beg_perf_idx, perf_cnt, &missCnt )) == kInvalidIdx ) { rc = cwLogError(rc,"score match sync failed."); goto errLabel; } // Print the state alignment. unsigned bsi = oLocId; print_path( matchH, bsi, midiA ); } errLabel: destroy(matchH); mem::release(midiA); return rc; }