From 3ced2ba0fb53bb3765fe7f7ed55cb22d344d3bbf Mon Sep 17 00:00:00 2001 From: kevin Date: Fri, 18 Apr 2025 14:59:42 -0400 Subject: [PATCH] cwScoreFollow2.h/cpp,cwScoreFollow2Test.h/cpp,Makefile.am : Initial commit. --- Makefile.am | 3 + cwScoreFollow2.cpp | 966 +++++++++++++++++++++++++++++++++++++++++ cwScoreFollow2.h | 54 +++ cwScoreFollow2Test.cpp | 314 ++++++++++++++ cwScoreFollow2Test.h | 8 + 5 files changed, 1345 insertions(+) create mode 100644 cwScoreFollow2.cpp create mode 100644 cwScoreFollow2.h create mode 100644 cwScoreFollow2Test.cpp create mode 100644 cwScoreFollow2Test.h diff --git a/Makefile.am b/Makefile.am index bb92536..4ab0953 100644 --- a/Makefile.am +++ b/Makefile.am @@ -86,6 +86,9 @@ libcwSRC += src/libcw/cwSfMatch.cpp src/libcw/cwSfTrack.cpp src/libcw/cwSfAnalys libcwHDR += src/libcw/cwScoreFollowerPerf.h src/libcw/cwScoreFollower.h src/libcw/cwPerfMeas.h src/libcw/cwScoreFollowTest.h libcwSRC += src/libcw/cwScoreFollower.cpp src/libcw/cwPerfMeas.cpp src/libcw/cwScoreFollowTest.cpp +libcwHDR += src/libcw/cwScoreFollow2Test.h src/libcw/cwScoreFollow2.h +libcwSRC += src/libcw/cwScoreFollow2Test.cpp src/libcw/cwScoreFollow2.cpp + libcwHDR += 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/cwScoreFollow2.cpp b/cwScoreFollow2.cpp new file mode 100644 index 0000000..282fc76 --- /dev/null +++ b/cwScoreFollow2.cpp @@ -0,0 +1,966 @@ + + +//| 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 "cwTime.h" +#include "cwFile.h" +#include "cwCsv.h" +#include "cwVectOps.h" + +#include "cwDynRefTbl.h" +#include "cwScoreParse.h" +#include "cwSfScore.h" +#include "cwSfTrack.h" +#include "cwPerfMeas.h" +#include "cwPianoScore.h" +#include "cwScoreFollow2.h" + +#define INVALID_SEC (-1.0) + +namespace cw +{ + namespace score_follow_2 + { + struct sf_str; + + typedef struct trkr_str + { + struct sf_str* sf; + + unsigned new_note_idx; + + bool end_fl; + + unsigned* note_match_cntA; // note_match_cntA[ sf->noteN ] + unsigned* loc_match_cntA; // loc_match_cntA[ sf->locN ] + float* expV; // expV[ sf->pitchN ] + + unsigned prv_loc_idx; // loc reported on the previous cycle + unsigned exp_loc_idx; // expected location on this cycle + + unsigned search_bni; // current search window + unsigned search_eni; // + + double beg_score_sec; // score time of the first match + double beg_perf_sec; // perf time of the first match + + double time_delta_sum; + unsigned time_delta_cnt; + double time_fact; // score_time * time_fact = perf_time + + double prv_match_perf_sec; + unsigned decay_cnt; + + } trkr_t; + + typedef struct aff_str + { + unsigned loc_id; // location where this affinity function should be applied + unsigned bni; // pitch index associated with envA[0] + float* envA; // envA[ envN ] + unsigned envN; // count of pitch cells covered by envA[] + } aff_t; + + typedef struct wnd_str + { + unsigned loc_id; // location where this search window is used + unsigned bni; // window start pitch index + unsigned eni; // window end pitch index + } wnd_t; + + typedef struct loc_str + { + unsigned loc_id; // id this location represents + unsigned meas_num; // measure number of this location + double sec; // score time in seconds + unsigned* note_idxA; // note_idxA[ note_idxN ] indexes into noteA[] of notes starting at this location + unsigned note_idxN; // + } loc_t; + + typedef struct note_str + { + unsigned uid; // unique id identifying this note + unsigned loc_id; // location this note falls on + unsigned pitch; // note of this note + unsigned vel; // velocity of this note + } note_t; + + typedef struct result_str + { + unsigned perf_uid; + unsigned perf_pitch; + unsigned perf_vel; + unsigned match_loc_id; + } result_t; + + + typedef struct sf_str + { + args_t args; + + loc_t* locA; // locA[ locN ] + unsigned* locMapA; // locMapA[ max_loc_id ] maps loc_id to loc_index + aff_t* loc_affA; // loc_affA[ locN ] + wnd_t* loc_wndA; // loc_wndA[ locN ] + unsigned locAllocN; // + unsigned locN; // + unsigned max_loc_id; // + + note_t* noteA; // noteA[ noteN ] + unsigned noteAllocN; // + unsigned noteN; // + + unsigned beg_loc_id; + unsigned end_loc_id; + + result_t* resultA; // resultA[ resultN ] + unsigned resultAllocN; + unsigned resultN; + + trkr_t* trk; + + } sf_t; + + + //================================================================================================================ + // + // trk_t + // + + void _trkr_destroy( trkr_t* trk ) + { + if( trk != nullptr ) + { + mem::release(trk->note_match_cntA); + mem::release(trk->loc_match_cntA); + mem::release(trk->expV); + mem::release(trk); + } + } + + void _trkr_apply_affinity( trkr_t* trk, unsigned loc_id ) + { + assert( loc_id <= trk->sf->max_loc_id ); + + unsigned loc_idx = trk->sf->locMapA[ loc_id ]; + const aff_t* a = trk->sf->loc_affA + loc_idx; + + assert( a->loc_id == loc_id ); + + //printf("apply: %i %i : %i %i : decay:%i\n",loc_id,loc_idx,a->bni, a->bni + a->envN - 1, trk->decay_cnt); + trk->decay_cnt = 0; + + for(unsigned i=0; ienvN; ++i) + trk->expV[ a->bni + i ] += a->envA[i]; + + } + + rc_t _trkr_reset( trkr_t* trk, unsigned beg_loc_id ) + { + assert( beg_loc_id <= trk->sf->max_loc_id ); + + trk->new_note_idx = 0; + trk->end_fl = false; + + //printf("reset expV: %i\n",beg_loc_id); + + for(unsigned ni=0; nisf->noteN; ++ni) + { + trk->note_match_cntA[ni] = 0; + trk->expV[ni] = 0; + } + for(unsigned li=0; lisf->locN; ++li) + trk->loc_match_cntA[li] = 0; + + trk->prv_loc_idx = kInvalidIdx; + trk->exp_loc_idx = trk->sf->locMapA[ beg_loc_id ]; + + trk->search_bni = kInvalidIdx; // current search window + trk->search_eni = kInvalidIdx; // + + trk->beg_score_sec = INVALID_SEC; // score time of the first match + trk->beg_perf_sec = INVALID_SEC; // perf time of the first match + + trk->time_delta_sum = 0; + trk->time_delta_cnt = 0; + trk->time_fact = 1.0; // score_time * time_fact = perf_time + + trk->prv_match_perf_sec = INVALID_SEC; + + _trkr_apply_affinity(trk,beg_loc_id); + + errLabel: + return kOkRC; + } + + trkr_t* _trkr_create( sf_t* sf ) + { + trkr_t* trk = mem::allocZ(); + + trk->sf = sf; + trk->note_match_cntA = mem::allocZ(sf->noteN); + trk->loc_match_cntA = mem::allocZ(sf->locN); + trk->expV = mem::allocZ(sf->noteN); + + _trkr_reset(trk,sf->locA[0].loc_id); + + return trk; + } + + void _trkr_rpt_debug( trkr_t* trk, unsigned pitch, unsigned vel, unsigned loc_idx ) + { + unsigned loc_id = trk->sf->locA[loc_idx].loc_id; + + printf("loc:%i pitch:%i vel:%i bni:%i eni:%i\n",loc_id,pitch,vel,trk->search_bni,trk->search_eni); + + for(unsigned ni=trk->search_bni; ni<=trk->search_eni; ++ni) + printf("%s%4i%s ", trk->sf->noteA[ni].loc_id==loc_id ? "(":"", trk->note_match_cntA[ni], trk->sf->noteA[ni].loc_id==loc_id ? ")":""); + printf("\n"); + + for(unsigned ni=trk->search_bni; ni<=trk->search_eni; ++ni) + printf("%s%4.2f%s ", trk->sf->noteA[ni].loc_id==loc_id ? "(":"", trk->expV[ni], trk->sf->noteA[ni].loc_id==loc_id ? ")":""); + printf("\n"); + + for(unsigned ni=trk->search_bni; ni<=trk->search_eni; ++ni) + printf("%s%4i%s ", trk->sf->noteA[ni].loc_id==loc_id ? "(":"", trk->sf->noteA[ni].pitch, trk->sf->noteA[ni].loc_id==loc_id ? ")":""); + printf("\n"); + + + } + + rc_t _trkr_on_new_note( trkr_t* trk, double sec, unsigned pitch, unsigned vel, bool rpt_fl, unsigned& matched_loc_id_ref ) + { + rc_t rc = kOkRC; + double d_corr_sec = 0.0; + bool d_corr_sec_valid_fl = false; + double d_match_score_sec = 0.0; + bool d_match_score_sec_valid_fl = false; + double d_match_perf_sec = 0.0; + bool d_match_perf_sec_valid_fl = false; + int d_loc_id = 0; + bool d_loc_id_valid_fl = false; + unsigned prv_loc_id = trk->prv_loc_idx==kInvalidIdx ? kInvalidIdx : trk->sf->locA[ trk->prv_loc_idx ].loc_id; + unsigned match_loc_id = kInvalidId; + unsigned exp_loc_id = kInvalidId; + const char* rpt_status = ""; + unsigned match_ni = kInvalidIdx; + double match_val = 0; + + matched_loc_id_ref = kInvalidId; + + assert( trk->exp_loc_idx != kInvalidIdx && trk->exp_loc_idx < trk->sf->locN ); + + trk->search_bni = trk->sf->loc_wndA[ trk->exp_loc_idx ].bni; + trk->search_eni = trk->sf->loc_wndA[ trk->exp_loc_idx ].eni; + + // set 'match_ni' to the best match candidate + for(unsigned ni=trk->search_bni; ni<=trk->search_eni; ++ni) + if( trk->note_match_cntA[ni]==0 && trk->sf->noteA[ni].pitch==pitch && (match_ni==kInvalidIdx || trk->expV[ni]>match_val)) + { + match_ni = ni; + match_val = trk->expV[ni]; + } + + //if( pitch==33 ) + // _trkr_rpt_debug(trk,pitch,vel,trk->exp_loc_idx); + + // if no match candidate was found + if( match_ni == kInvalidIdx ) + { + rpt_status = "spurious"; + } + else + { + // get the loc_id that we expected to match + exp_loc_id = trk->exp_loc_idx==kInvalidIdx ? kInvalidIdx : trk->sf->locA[ trk->exp_loc_idx ].loc_id; + + // get attributes of the candidate match + match_loc_id = trk->sf->noteA[match_ni].loc_id; + unsigned match_loc_idx= trk->sf->locMapA[match_loc_id]; + double match_sec = trk->sf->locA[match_loc_idx].sec; + + // set d_loc_id to the diff between the matched loc and the expected match loc + if( exp_loc_id != kInvalidIdx ) + { + d_loc_id = (int)match_loc_id - (int)exp_loc_id; + d_loc_id_valid_fl = true; + } + + // if this is the first match since a reset then record this as the first match + // TODO: what if this point gets rejected + if( trk->beg_score_sec == INVALID_SEC ) + { + trk->beg_score_sec = match_sec; + trk->beg_perf_sec = sec; + } + + // track the score time diff. between this match and the prev score match + if( trk->prv_loc_idx!=kInvalidIdx ) + { + // delta score match time between this match and prev match + double prv_score_sec = trk->sf->locA[ trk->prv_loc_idx ].sec; + d_match_score_sec = match_sec - prv_score_sec; + d_match_score_sec_valid_fl = true; + } + + // track the perf time diff. between this score and the prev perf match + if( trk->prv_match_perf_sec != INVALID_SEC ) + { + // delta perf time between this match and prev match + d_match_perf_sec = sec - trk->prv_match_perf_sec; + d_match_perf_sec_valid_fl = true; + } + + // if there the delta score/perf match time's are valid then estimate the tempo corrected delta match time + if( d_match_score_sec_valid_fl && d_match_perf_sec_valid_fl ) + { + double d_corr_match_score_sec = d_match_score_sec / trk->time_fact; + d_corr_sec = d_match_perf_sec - d_corr_match_score_sec; + d_corr_sec_valid_fl = true; + } + + // if this is a high confidence match then update the score->perf time tempo correction factor + if( d_loc_id_valid_fl && 0 <= d_loc_id && d_loc_id < trk->sf->args.d_loc_stats_thresh ) + { + if( sec - trk->beg_perf_sec > 0 && (match_sec - trk->beg_score_sec) > 0 ) + { + trk->time_delta_sum += (match_sec - trk->beg_score_sec)/(sec - trk->beg_perf_sec); + trk->time_delta_cnt += 1; + trk->time_fact = trk->time_delta_sum / trk->time_delta_cnt; + } + } + + // set the thresh flags that are violated + bool lo_time_thresh_fl = d_corr_sec_valid_fl && fabs(d_corr_sec) > trk->sf->args.d_sec_err_thresh_lo; + bool hi_time_thresh_fl = d_loc_id_valid_fl && d_loc_id>0 && d_corr_sec_valid_fl && fabs(d_corr_sec) > trk->sf->args.d_sec_err_thresh_hi; + bool lo_loc_thresh_fl = d_loc_id_valid_fl && abs(d_loc_id) > trk->sf->args.d_loc_thresh_lo; + bool hi_loc_thresh_fl = d_loc_id_valid_fl && abs(d_loc_id) > trk->sf->args.d_loc_thresh_hi; + + // if this match breaks the threshold rules .... + if( (lo_time_thresh_fl && lo_loc_thresh_fl) || hi_loc_thresh_fl || hi_time_thresh_fl ) + { + match_ni = kInvalidIdx; // ... then reject the match + rpt_status = "rejected"; + } + else + { + // ... the match was accepted + + trk->prv_match_perf_sec = sec; + trk->prv_loc_idx = match_loc_idx; + trk->loc_match_cntA[ match_loc_idx ] += 1; + trk->note_match_cntA[ match_ni ] += 1; + + matched_loc_id_ref = match_loc_id; + + // notice if we arrived at the end of the score tracking range + if( match_loc_id >= trk->sf->end_loc_id ) + { + trk->end_fl = true; + } + else + { + + // select the next expected location by advancing to the next location that has not yet been matched + unsigned exp_loc_idx = match_loc_idx; + while( exp_loc_idx < trk->sf->locN && trk->loc_match_cntA[exp_loc_idx] >= trk->sf->locA[exp_loc_idx].note_idxN) + ++exp_loc_idx; + + // if we arrived at the end of the score + if( exp_loc_idx >= trk->sf->locN ) + trk->end_fl = true; + else + { + + exp_loc_id = trk->sf->locA[ exp_loc_idx ].loc_id; + + // apply the affinity envelope at the exected location + _trkr_apply_affinity(trk,exp_loc_id); + + // update the expected loc. for the next cycle + trk->exp_loc_idx = exp_loc_idx; + } + } + } + } + + if( rpt_fl ) + { + + printf("%4i pitch:%3i ",trk->new_note_idx, pitch); + if( prv_loc_id != kInvalidIdx ) + printf("LOC: prv:%4i ",prv_loc_id); + else + printf("LOC: prv: "); + + if( match_loc_id != kInvalidIdx ) + printf("match:%4i ",match_loc_id); + else + printf("match: "); + + if( exp_loc_id != kInvalidIdx ) + printf("exp:%4i ",exp_loc_id); + else + printf("exp: "); + + if( d_loc_id_valid_fl ) + printf(": dLoc:%4i ",d_loc_id); + else + printf(": dLoc: "); + + + if( d_match_score_sec_valid_fl ) + printf("| Match dsec score:%6.3f ",d_match_score_sec); + else + printf("| Match dsec score: "); + + if( d_match_perf_sec_valid_fl ) + printf("perf:%6.3f ",d_match_perf_sec); + else + printf("perf: "); + + if( d_corr_sec_valid_fl ) + printf("corr:%6.3f ",d_corr_sec); + else + printf("corr: "); + + //printf(" : (%f %i %f) : ",trk->time_delta_sum,trk->time_delta_cnt,trk->time_fact); + + printf("%s ",rpt_status); + + if( vel < 5 ) + printf("vel(%i) ",vel); + + printf("\n"); + + } + + trk->new_note_idx+=1; + return rc; + } + + rc_t _trkr_do_decay( trkr_t* trk ) + { + rc_t rc = kOkRC; + + if( trk->search_bni != kInvalidIdx && trk->search_eni != kInvalidIdx ) + { + //printf("decay: %i %i\n",trk->search_bni, trk->search_eni); + trk->decay_cnt += 1; + + for(unsigned ni=trk->search_bni; ni<=trk->search_eni; ++ni) + trk->expV[ni] *= trk->sf->args.decay_coeff; + } + return rc; + } + + //========================================================================================================== + // + // + // + + sf_t* _handleToPtr(handle_t h) + { + return handleToPtr(h); + } + + rc_t _destroy( sf_t* p ) + { + for(unsigned i=0; ilocN; ++i) + { + if( p->locA != nullptr ) + mem::release(p->locA[i].note_idxA); + + if( p->loc_affA != nullptr ) + mem::release(p->loc_affA[i].envA); + } + + _trkr_destroy(p->trk); + + mem::release(p->resultA); + mem::release(p->noteA); + mem::release(p->locA); + mem::release(p->locMapA); + mem::release(p->loc_wndA); + mem::release(p->loc_affA); + mem::release(p); + return kOkRC; + } + + rc_t _get_loc_and_note_count( sf_t* p, const perf_score::handle_t& scoreH ) + { + rc_t rc = kOkRC; + unsigned loc_id0 = kInvalidId; + + p->locAllocN = 0; + p->noteAllocN = 0; + p->max_loc_id = kInvalidId; + + // for each score event + for(const perf_score::event_t* e = base_event(scoreH); e != nullptr; e=e->link) + { + // if this is a note-on + if( midi::isNoteOn( e->status, e->d1 ) ) + { + p->noteAllocN += 1; + + // if this is a new location + if( loc_id0 == kInvalidId || loc_id0 != e->loc ) + { + // if this is the max location id + if( p->max_loc_id == kInvalidId || e->loc > p->max_loc_id ) + p->max_loc_id = e->loc; + + p->locAllocN += 1; + + } + + // check that location id's are in increasing order + if( loc_id0 != kInvalidId && e->loc < loc_id0 ) + { + rc = cwLogError(kInvalidStateRC,"The score location id's are not increasing. (%i < %i)",e->loc,loc_id0); + goto errLabel; + } + + loc_id0 = e->loc; + } + } + + errLabel: + return rc; + } + + rc_t _alloc_fill_loc_and_note_arrays( sf_t* p, const perf_score::handle_t& scoreH ) + { + rc_t rc = kOkRC; + unsigned loc_id0 = kInvalidId; + unsigned uid = 0; + + p->noteA = mem::allocZ(p->noteAllocN); + p->locA = mem::allocZ(p->locAllocN); + p->locMapA = mem::allocZ(p->max_loc_id+1); + p->locN = 0; + p->noteN = 0; + + // for each score event + for(const perf_score::event_t* e = base_event(scoreH); e != nullptr; e=e->link) + { + // if this is a note-on + if( midi::isNoteOn( e->status, e->d1 ) ) + { + note_t* note = p->noteA + p->noteN; + + note->uid = uid++; + note->loc_id = e->loc; + note->pitch = e->d0; + note->vel = e->d1; + + p->noteN += 1; + + // if this is a new location + if( loc_id0 == kInvalidId || loc_id0 != e->loc ) + { + loc_t* loc = p->locA + p->locN; + + loc->loc_id = e->loc; + loc->meas_num = e->meas; + loc->sec = e->sec; + + // fill in the loc_id to loc_idx map + p->locMapA[ e->loc ] = p->locN; + + p->locN += 1; + } + + loc_id0 = e->loc; + + } + } + + return rc; + } + + rc_t _alloc_and_fill_loc_note_arrays( sf_t* p ) + { + rc_t rc = kOkRC; + unsigned ni = 0; + + for(unsigned li=0; lilocN; ++li) + { + unsigned n = 0; + unsigned bni = ni; + + // count the number of notes assigned to location loc[li]->loc_id + for(; ninoteN && p->noteA[ni].loc_id == p->locA[li].loc_id; ++ni ) + ++n; + + if( n == 0 ) + { + rc = cwLogError(kInvalidStateRC,"No notes exist for score location '%i'.",p->locA[li].loc_id); + goto errLabel; + } + else + { + // allocate this loc's pitch index array + p->locA[li].note_idxA = mem::allocZ(n); + p->locA[li].note_idxN = n; + + // fill the pitch index array + for(unsigned i=0; ilocA[li].note_idxA[i] = bni + i; + + assert( p->noteA[ bni+i ].loc_id == p->locA[li].loc_id ); + } + + } + } + + errLabel: + return rc; + } + + rc_t _alloc_and_fill_search_wnd_array( sf_t* p, double pre_wnd_sec, double post_wnd_sec ) + { + rc_t rc = kOkRC; + + p->loc_wndA = mem::allocZ(p->locN); + + for(unsigned li=0; lilocN; ++li) + { + int bli = li; + unsigned eli = li; + + // look backward for window start location + while(bli-1 >= 0 && p->locA[bli-1].sec >= p->locA[li].sec - pre_wnd_sec ) + bli -= 1; + + // look forward for the window end location + while(eli+1locN && p->locA[eli+1].sec <= p->locA[li].sec + post_wnd_sec ) + eli += 1; + + // verfiy that notes exist for location 'bli' (this is a redundant check - see _alloc_and_fill_note_arrays() + if( p->locA[bli].note_idxN == 0 ) + { + rc = cwLogError(kInvalidStateRC,"No notes exist for score location '%i'.",p->locA[bli].loc_id); + goto errLabel; + } + + // verfiy that notes exist for location 'eli' (this is a redundant check - see _alloc_and_fill_note_arrays() + if( p->locA[eli].note_idxN == 0 ) + { + rc = cwLogError(kInvalidStateRC,"No notes exist for score location '%i'.",p->locA[eli].loc_id); + goto errLabel; + } + + // translate the begin/end locations to begin/end pitich indexes + p->loc_wndA[li].bni = p->locA[bli].note_idxA[0]; + p->loc_wndA[li].eni = p->locA[eli].note_idxA[ p->locA[eli].note_idxN-1 ]; + p->loc_wndA[li].loc_id = p->locA[li].loc_id; + } + + errLabel: + return rc; + } + + + float _aff_func( sf_t* p, double t0, unsigned li, double wnd_dur_sec ) + { + double t1 = p->locA[li].sec; + double dt = std::max(t0,t1)-std::min(t0,t1); + assert( dt <= wnd_dur_sec ); + return (wnd_dur_sec-dt)/wnd_dur_sec; + } + + rc_t _alloc_and_fill_affinity_wnd_arrays( sf_t*p, double pre_aff_sec, double post_aff_sec ) + { + rc_t rc = kOkRC; + unsigned locEnvN = 0; + float* locEnvV = nullptr; + + p->loc_affA = mem::allocZ(p->locN); + + // for each location + for(unsigned li = 0; lilocN; ++li) + { + int bli = li; + unsigned eli = li; + double t0 = p->locA[li].sec; + unsigned bpi = kInvalidId; + unsigned epi = kInvalidId; + + // look backward for window start location + while(bli-1 >= 0 && p->locA[bli-1].sec >= t0 - pre_aff_sec ) + bli -= 1; + + // look forward for the window end location + while(eli+1locN && p->locA[eli+1].sec <= t0 + post_aff_sec ) + eli += 1; + + // calc the count of the loc's in the aff. env. + locEnvN = (eli-bli) + 1; + locEnvV = mem::resize(locEnvV,locEnvN); + + // fill in the pre-aff wnd + for(unsigned i = bli; ilocA[eli].note_idxN > 0 ); + + // allocate the per-note envelope + bpi = p->locA[bli].note_idxA[0]; + epi = p->locA[eli].note_idxA[ p->locA[eli].note_idxN-1 ]; + + assert( bpi < p->noteN && epi < p->noteN && li < p->locN ); + + // init. the aff. env. record + p->loc_affA[ li ].loc_id = p->locA[li].loc_id; + p->loc_affA[ li ].bni = bpi; + p->loc_affA[ li ].envN = (epi-bpi)+1; + p->loc_affA[ li ].envA = mem::allocZ(p->loc_affA[ li ].envN); + + // for each note that fall withing the aff. env + for(unsigned pi = bpi; pi<=epi; ++pi) + { + // get the loc idx assoc with this note + unsigned loc_idx = p->locMapA[ p->noteA[pi].loc_id ]; + + // calc the loc. based envA[] index + unsigned loc_env_i = loc_idx - bli; + + assert( loc_env_i < locEnvN ); + assert( pi-bpi < p->loc_affA[li].envN ); + + p->loc_affA[li].envA[pi-bpi] = locEnvV[ loc_env_i ]; + } + } + + mem::release(locEnvV); + return rc; + } + + void _report_score( sf_t* p, unsigned beg_loc_id, unsigned end_loc_id ) + { + for(unsigned ni=0; ninoteN; ++ni) + if(beg_loc_id <= p->noteA[ni].loc_id && p->noteA[ni].loc_id <= end_loc_id ) + printf("%4i %6.2f %3i\n", p->noteA[ni].loc_id, p->locA[ p->locMapA[ p->noteA[ni].loc_id ] ].sec, p->noteA[ni].pitch ); + } + + void _report_affinity( sf_t* p, unsigned N=10 ) + { + for(unsigned i=0; iloc_affA[i]; + printf("%i %i %i [",a.loc_id,a.bni,a.envN); + for(unsigned j=0; jgetv("rpt_fl",args.rpt_fl, + "pre_affinity_sec", args.pre_affinity_sec, + "post_affinity_sec", args.post_affinity_sec, + "pre_wnd_sec", args.pre_wnd_sec, + "post_wnd_sec", args.post_wnd_sec, + "decay_coeff", args.decay_coeff, + "d_sec_err_thresh_lo", args.d_sec_err_thresh_lo, + "d_loc_thresh_lo", args.d_loc_thresh_lo, + "d_sec_err_thresh_hi", args.d_sec_err_thresh_hi, + "d_loc_thresh_hi", args.d_loc_thresh_hi, + "d_loc_stats_thresh", args.d_loc_stats_thresh)) != kOkRC ) + { + rc = cwLogError(rc,"SF2 parse args. failed."); + goto errLabel; + } + +errLabel: + return rc; +} + +cw::rc_t cw::score_follow_2::create( handle_t& hRef, const args_t& args ) +{ + rc_t rc; + if((rc = destroy(hRef)) != kOkRC ) + return rc; + + sf_t* p = mem::allocZ(); + + if((rc = _get_loc_and_note_count( p, args.scoreH )) != kOkRC ) + goto errLabel; + + if((rc = _alloc_fill_loc_and_note_arrays( p, args.scoreH )) != kOkRC ) + goto errLabel; + + if((rc = _alloc_and_fill_loc_note_arrays( p )) != kOkRC ) + goto errLabel; + + if((rc = _alloc_and_fill_search_wnd_array( p, args.pre_wnd_sec, args.post_wnd_sec )) != kOkRC ) + goto errLabel; + + if((rc = _alloc_and_fill_affinity_wnd_arrays(p, args.pre_affinity_sec, args.post_affinity_sec )) != kOkRC ) + goto errLabel; + + p->trk = _trkr_create(p); + + p->args = args; + + p->resultAllocN = p->noteN*2; + p->resultA = mem::allocZ(p->resultAllocN); + p->resultN = 0; + + //_report_affinity( p ); + + + hRef.set(p); + +errLabel: + if(rc != kOkRC ) + { + rc = cwLogError(rc,"Score follower create failed."); + _destroy(p); + } + + return rc; +} + +cw::rc_t cw::score_follow_2::destroy( handle_t& hRef ) +{ + rc_t rc = kOkRC; + sf_t* p = nullptr; + + if( !hRef.isValid() ) + return rc; + + p = _handleToPtr(hRef); + + if((rc = _destroy(p)) != kOkRC ) + { + rc = cwLogError(rc,"Score follow destroy failed."); + goto errLabel; + } + + hRef.clear(); + +errLabel: + return rc; +} + +cw::rc_t cw::score_follow_2::reset( handle_t h, unsigned beg_loc_id, unsigned end_loc_id ) +{ + rc_t rc = kOkRC; + sf_t* p = _handleToPtr(h); + + if( beg_loc_id > p->max_loc_id ) + { + rc = cwLogError(kInvalidArgRC,"An invalid location id (%i) was encountered.",beg_loc_id); + goto errLabel; + } + + //_report_score(p,beg_loc_id,end_loc_id); + + + p->beg_loc_id = beg_loc_id; + p->end_loc_id = end_loc_id; + + p->resultN = 0; + + _trkr_reset(p->trk,beg_loc_id); + +errLabel: + + return rc; +} + +cw::rc_t cw::score_follow_2::on_new_note( handle_t h, unsigned uid, double sec, uint8_t pitch, uint8_t vel, unsigned& loc_id ) +{ + rc_t rc = kOkRC; + sf_t* p = _handleToPtr(h); + unsigned matched_loc_id = kInvalidId; + + _trkr_on_new_note(p->trk,sec,pitch,vel, p->args.rpt_fl, matched_loc_id); + + if( p->resultN < p->resultAllocN ) + { + result_t* r = p->resultA + p->resultN; + r->perf_uid = uid; + r->perf_pitch = pitch; + r->perf_vel = vel; + r->match_loc_id = matched_loc_id; + p->resultN += 1; + } + + return rc; +} + +cw::rc_t cw::score_follow_2::do_exec( handle_t h ) +{ + rc_t rc = kOkRC; + sf_t* p = _handleToPtr(h); + + _trkr_do_decay(p->trk); + return rc; +} + +void cw::score_follow_2::report_summary( handle_t h, rpt_t& rpt_ref ) +{ + sf_t* p = _handleToPtr(h); + + rpt_ref.matchN = 0; + rpt_ref.missN = 0; + rpt_ref.spuriousN = 0; + rpt_ref.perfNoteN = 0; + + for(unsigned i=0; iresultN; ++i) + if( p->resultA[i].match_loc_id == kInvalidIdx ) + rpt_ref.spuriousN += 1; + + unsigned bli = p->locMapA[ p->beg_loc_id ]; + unsigned eli = p->locMapA[ p->end_loc_id ]; + + unsigned bni = p->locA[ bli ].note_idxA[0]; + unsigned eni = p->locA[ eli ].note_idxA[ p->locA[eli].note_idxN-1 ]; + + for(unsigned ni=bni; ni<=eni; ++ni) + if( p->trk->note_match_cntA[ni] == 0 ) + rpt_ref.missN += 1; + else + rpt_ref.matchN += 1; + + rpt_ref.perfNoteN = p->trk->new_note_idx; + + if( p->args.rpt_fl ) + cwLogInfo("Matched:%i Missed:%i Spurious:%i",rpt_ref.matchN,rpt_ref.missN,rpt_ref.spuriousN); + + + +} + + diff --git a/cwScoreFollow2.h b/cwScoreFollow2.h new file mode 100644 index 0000000..e1d1e7a --- /dev/null +++ b/cwScoreFollow2.h @@ -0,0 +1,54 @@ + +namespace cw +{ + namespace score_follow_2 + { + typedef handle< struct sf_str > handle_t; + + typedef struct args_str + { + perf_score::handle_t scoreH; + + double pre_affinity_sec; // 1.0 look back affinity duration + double post_affinity_sec; // 3.0 look forward affinity duration + double pre_wnd_sec; // 2.0 look back search window + double post_wnd_sec; // 5.0 look forward search window + + double decay_coeff; // 0.995affinity decay coeff + + double d_sec_err_thresh_lo; // 0.4 reject if d_loc > d_loc_thresh_lod and d_time > d_time_thresh_lo + int d_loc_thresh_lo; // 3 + + double d_sec_err_thresh_hi; // 1.5 reject if d_loc != 0 and d_time > d_time_thresh_hi + int d_loc_thresh_hi; // 4 reject if d_loc > d_loc_thresh_hi + int d_loc_stats_thresh; // 3 reject for time stats updates if d_loc > d_loc_stats_thresh + + bool rpt_fl; // set to turn on debug reporting + + } args_t; + + rc_t parse_args( const object_t* cfg, args_t& args ); + + rc_t create( handle_t& hRef, const args_t& args ); + + rc_t destroy( handle_t& hRef ); + + rc_t reset( handle_t h, unsigned beg_loc_id, unsigned end_loc_id ); + + rc_t on_new_note( handle_t h, unsigned uid, double sec, uint8_t pitch, uint8_t vel, unsigned& loc_id ); + + // Decay the affinity window and if necessary trigger a cycle of async background processing + rc_t do_exec( handle_t h ); + + typedef struct rpt_str + { + unsigned matchN; // count of matched notes + unsigned missN; // count of missed notes + unsigned spuriousN; // count of spurious notes + unsigned perfNoteN; // count of performed notes (count of calls to on_new_note()) + } rpt_t; + + void report_summary( handle_t h, rpt_t& rpt_ref ); + + } +} diff --git a/cwScoreFollow2Test.cpp b/cwScoreFollow2Test.cpp new file mode 100644 index 0000000..6e0e318 --- /dev/null +++ b/cwScoreFollow2Test.cpp @@ -0,0 +1,314 @@ +//| 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 "cwFileSys.h" + +#include "cwCsv.h" +#include "cwMidi.h" + +#include "cwDynRefTbl.h" +#include "cwScoreParse.h" +#include "cwSfScore.h" +#include "cwPerfMeas.h" + +#include "cwPianoScore.h" + +#include "cwPianoScore.h" +#include "cwScoreFollow2.h" +#include "cwScoreFollow2Test.h" + +namespace cw +{ + namespace score_follow_2 + { + typedef struct midi_evt_str + { + unsigned uid; + uint8_t pitch; + uint8_t vel; + unsigned sample_idx; + double sec; + } midi_evt_t; + + typedef struct midi_file_str + { + midi_evt_t* evtA; + unsigned evtN; + unsigned sampleN; + } midi_file_t; + + rc_t parse_midi_file( const char* midi_csv_fname, double srate, midi_file_t& mf ) + { + rc_t rc = kOkRC; + csv::handle_t csvH; + const char* titleA[] = { "UID","trk","amicro","type","ch","D0","D1","sci_pitch" }; + unsigned titleN = sizeof(titleA)/sizeof(titleA[0]); + unsigned line_idx = 0; + unsigned lineN = 0; + + if((rc = csv::create(csvH,midi_csv_fname,titleA,titleN)) != kOkRC ) + { + rc = cwLogError(rc,"MIDI CSV file open failed."); + goto errLabel; + } + + if((rc = line_count(csvH,lineN)) != kOkRC ) + { + rc = cwLogError(rc,"MIDI CSV line count access failed."); + goto errLabel; + } + + mf.evtA = mem::allocZ(lineN); + mf.evtN = 0; + + for(; (rc = next_line(csvH)) == kOkRC; ++line_idx ) + { + unsigned uid; + unsigned amicro; + const char* type = nullptr; + unsigned ch; + unsigned d0; + unsigned d1; + + if((rc = getv(csvH,"UID",uid,"amicro",amicro,"type",type,"ch",ch,"D0",d0,"D1",d1)) != kOkRC ) + { + cwLogError(rc,"Error reading CSV line %i.",line_idx+1); + goto errLabel; + } + + if(textIsEqual(type,"non")) + { + midi_evt_t& m = mf.evtA[ mf.evtN++ ]; + + assert( d0 <= 127 && d1 <= 127 ); + + m.pitch = (uint8_t)d0; + m.vel = (uint8_t)d1; + m.sec = amicro/1000000.0; + m.sample_idx = (unsigned)(m.sec * srate); + m.uid = uid; + + mf.sampleN = m.sample_idx; + } + } + + errLabel: + destroy(csvH); + + if( rc == kEofRC ) + rc = kOkRC; + + if( rc != kOkRC ) + rc = cwLogError(rc,"MIDI csv file parse failed on '%s'.",cwStringNullGuard(midi_csv_fname)); + return rc; + } + + rc_t run( perf_score::handle_t& scoreH, + double srate, + unsigned smp_per_cycle, + const char* midi_csv_fname, + const object_t* sf_cfg, + unsigned beg_loc_id, + unsigned end_loc_id, + score_follow_2::rpt_t& rpt_ref ) + { + + rc_t rc = kOkRC; + midi_file_t mf{ .evtA = nullptr, .evtN=0, .sampleN=0 }; + score_follow_2::args_t sf_args{}; + score_follow_2::handle_t sfH; + unsigned midi_evt_idx = 0; + + // parse args + if((rc = score_follow_2::parse_args(sf_cfg,sf_args)) != kOkRC ) + { + rc = cwLogError(rc,"SF2 parse arg. failed."); + goto errLabel; + } + + // parse MIDI file into an array + if((rc = parse_midi_file( midi_csv_fname, srate, mf )) != kOkRC ) + goto errLabel; + + //cwLogInfo("Following: (%i notes found) sample count:%i %s.",mf.evtN,mf.sampleN,midi_csv_fname); + + sf_args.scoreH = scoreH; + + // create the score-follower + if((rc = score_follow_2::create(sfH,sf_args)) != kOkRC ) + { + rc = cwLogError(rc,"Score follower create failed."); + goto errLabel; + } + + if((rc = score_follow_2::reset(sfH,beg_loc_id,end_loc_id)) != kOkRC ) + { + rc = cwLogError(rc,"Score follower reset failed."); + goto errLabel; + } + + // for each cycle + for(unsigned smp_idx=0; smp_idx < mf.sampleN; smp_idx += smp_per_cycle) + { + while( smp_idx <= mf.evtA[midi_evt_idx].sample_idx && mf.evtA[midi_evt_idx].sample_idx < smp_idx + smp_per_cycle ) + { + const midi_evt_t* e = mf.evtA + midi_evt_idx++; + unsigned loc_id; + + //printf("%f pitch:%i vel:%i\n",e->sec,e->pitch,e->vel); + + if((rc = on_new_note( sfH, e->uid, e->sec, e->pitch, e->vel, loc_id )) != kOkRC ) + { + rc = cwLogError(rc,"SF2 note processing failed on note UID:%i.",e->uid); + goto errLabel; + } + } + + if((rc = do_exec(sfH)) != kOkRC ) + { + rc = cwLogError(rc,"SF2 exec processing failed."); + goto errLabel; + } + + } + + score_follow_2::report_summary(sfH,rpt_ref); + + errLabel: + mem::release(mf.evtA); + destroy(sfH); + + return rc; + } + } +} + + +cw::rc_t cw::score_follow_2::test( const object_t* cfg ) +{ + rc_t rc = kOkRC; + double srate = 48000.0; + unsigned smp_per_cycle = 64; + unsigned limit_perf_N = -1; + const char* c_score_fname = nullptr; + char* score_fname = nullptr; + const object_t* fileL = nullptr; + const object_t* sf_cfg = nullptr; + + unsigned tot_missN = 0; + unsigned tot_spuriousN = 0; + unsigned tot_passN = 0; + unsigned tot_failN = 0; + + perf_score::handle_t scoreH; + + if((rc = cfg->getv("srate",srate, + "smp_per_cycle",smp_per_cycle, + "limit_perf_N",limit_perf_N, + "score_fname",c_score_fname, + "fileL",fileL, + "follower",sf_cfg)) != kOkRC ) + { + rc = cwLogError(rc,"SF2 test argument parsing failed."); + goto errLabel; + } + + score_fname = filesys::expandPath( c_score_fname ); + + // create the score + if((rc = perf_score::create(scoreH,score_fname)) != kOkRC ) + { + rc = cwLogError(rc,"Score create failed on '%s'.",cwStringNullGuard(score_fname)); + goto errLabel; + } + + // for each file + for(unsigned i=0; rc==kOkRC && ichild_count(); ++i) + { + unsigned beg_loc_id = kInvalidId; + unsigned end_loc_id = kInvalidId; + unsigned min_perf_noteN = 0; + unsigned max_spuriousN = 0; + const char* c_folder = nullptr; + char* folder = nullptr; + const object_t* takeL = nullptr; + + score_follow_2::rpt_t sf_rpt; + + // read the file record + if((rc = fileL->child_ele(i)->getv("beg_loc",beg_loc_id, + "end_loc",end_loc_id, + "min_perf_noteN",min_perf_noteN, + "max_spuriousN", max_spuriousN, + "folder",c_folder, + "takeL",takeL)) != kOkRC ) + { + rc = cwLogError(rc,"Parse of SF2 test case at index %i.",i); + goto errLabel; + } + + folder = filesys::expandPath( c_folder ); + + // for each take number + for(unsigned j=0; jchild_count(); ++j) + { + char* record_folder = nullptr; + unsigned take_num; + + // read the take number + if((rc = takeL->child_ele(j)->value(take_num)) != kOkRC ) + { + rc = cwLogError(rc,"Error parsing take number on test '%s' index '%i'.",cwStringNullGuard(c_folder),j); + break; + } + + // form the record_### folder name + record_folder = mem::printf(nullptr,"record_%i",take_num); + + // form MIDI csv file name + char* midi_csv_fname = filesys::makeFn(folder,"fix_midi","csv", record_folder, nullptr ); + + if((rc = run(scoreH, srate, smp_per_cycle, midi_csv_fname, sf_cfg, beg_loc_id, end_loc_id, sf_rpt )) != kOkRC ) + { + rc = cwLogError(rc,"SF2 test run failed on '%s'.",cwStringNullGuard(midi_csv_fname)); + break; + } + + cwLogInfo("Notes:%i Match:%i Miss:%i Spurious:%i : %s",sf_rpt.perfNoteN,sf_rpt.matchN,sf_rpt.missN,sf_rpt.spuriousN,cwStringNullGuard(midi_csv_fname)); + + if( sf_rpt.perfNoteN > min_perf_noteN && sf_rpt.spuriousN < max_spuriousN ) + { + tot_passN += 1; + tot_missN += sf_rpt.missN; + tot_spuriousN += sf_rpt.spuriousN; + } + else + { + tot_failN += 1; + } + + //printf("%s\n",fname); + + mem::release(record_folder); + mem::release(midi_csv_fname); + + } + + mem::release(folder); + + + } + + cwLogInfo("Total M+S:%i missed:%i spurious:%i pass:%i fail:%i",tot_missN+tot_spuriousN,tot_missN,tot_spuriousN,tot_passN,tot_failN); + +errLabel: + destroy(scoreH); + mem::release(score_fname); + return rc; +} diff --git a/cwScoreFollow2Test.h b/cwScoreFollow2Test.h new file mode 100644 index 0000000..0595c20 --- /dev/null +++ b/cwScoreFollow2Test.h @@ -0,0 +1,8 @@ + +namespace cw +{ + namespace score_follow_2 + { + rc_t test( const object_t* cfg ); + } +}