//| 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 "cwFile.h" #include "cwFileSys.h" #include "cwMidi.h" #include "cwMidiFile.h" #include "cwDynRefTbl.h" #include "cwScoreParse.h" #include "cwSfScore.h" #include "cwSfMatch.h" #include "cwSfTrack.h" #include "cwPerfMeas.h" #include "cwScoreFollowerPerf.h" #include "cwScoreFollower.h" #include "cwMidiState.h" #include "cwSvgScoreFollow.h" namespace cw { namespace score_follower { typedef struct meas_set_str { unsigned vsi; unsigned nsi; } meas_set_t; typedef struct score_follower_str { bool enableFl; double srate; unsigned search_area_locN; unsigned key_wnd_locN; char* score_csv_fname; bool deleteScoreFl; sfscore::handle_t scoreH; sftrack::handle_t trackH; //perf_meas::handle_t measH; unsigned* result_idxA; // unsigned result_idx_allocN; // unsigned result_idx_curN; // ssf_note_on_t* perfA; // perfA[ perfN ] stored performance unsigned perfN; // unsigned perf_idx; dyn_ref_tbl::handle_t dynRefH; unsigned track_flags; } score_follower_t; score_follower_t* _handleToPtr( handle_t h ) { return handleToPtr(h); } // parse the score follower parameter record rc_t _parse_cfg( score_follower_t* p, const object_t* cfg ) { rc_t rc = kOkRC; const char* score_csv_fname = nullptr; const object_t* dyn_ref_dict = nullptr; bool track_print_fl = false; bool track_results_backtrack_fl = false; if((rc = cfg->getv("enable_flag", p->enableFl, "score_csv_fname", score_csv_fname, "search_area_locN", p->search_area_locN, "key_wnd_locN", p->key_wnd_locN, "dyn_ref", dyn_ref_dict, "track_print_fl", track_print_fl, "track_results_backtrack_fl", track_results_backtrack_fl)) != kOkRC ) { rc = cwLogError(kInvalidArgRC, "Score follower argument parsing failed."); goto errLabel; } if((p->score_csv_fname = filesys::expandPath( score_csv_fname )) == nullptr ) { rc = cwLogError(kOpFailRC,"Score follower score file expansion failed."); goto errLabel; } if((rc = dyn_ref_tbl::create( p->dynRefH, dyn_ref_dict )) != kOkRC ) { rc = cwLogError(rc,"Dynamics reference array parse failed."); goto errLabel; } p->track_flags += track_print_fl ? sftrack::kPrintFl : 0; p->track_flags += track_results_backtrack_fl ? sftrack::kBacktrackResultsFl : 0; errLabel: return rc; } rc_t _update_midi_state( score_follower_t* p, midi_state::handle_t msH ) { rc_t rc; for(unsigned i=0; iperf_idx; ++i) { if((rc = setMidiMsg( msH, p->perfA[i].sec, i, 0, midi::kNoteOnMdId, p->perfA[i].pitch, p->perfA[i].vel )) != kOkRC ) { rc = cwLogError(rc,"midi_state update failed."); goto errLabel; } } errLabel: return rc; } bool _processPedal(const char* pedalLabel, unsigned barNumb, bool curPedalDownStateFl, uint8_t d1) { bool newStateFl = midi::isPedalDown(d1); /* if( newStateFl == curPedalDownStateFl ) { const char* upDownLabel = newStateFl ? "down" : "up"; cwLogWarning("Double %s pedal %s in bar number:%i.",pedalLabel,upDownLabel,barNumb); } */ return newStateFl; } rc_t _destroy( score_follower_t* p) { destroy(p->dynRefH); mem::release(p->score_csv_fname); mem::release(p->perfA); mem::release(p->result_idxA); destroy(p->trackH); if( p->deleteScoreFl) destroy(p->scoreH); //destroy(p->measH); mem::release(p); return kOkRC; } void _score_follower_cb( void* arg, sftrack::result_t* r ) { score_follower_t* p = (score_follower_t*)arg; //printf("%4i %4i %4i %3i %3i %4s : ", r->oLocId, r->mni, r->muid, r->flags, r->pitch, midi::midiToSciPitch( r->pitch, nullptr, 0 )); if( r->scEvtIdx == kInvalidIdx ) { //cwLogInfo("Score Follower: MISS"); } else { const sfscore::event_t* score_event; // get a pointer to the matched score event if((score_event = event( p->scoreH, r->scEvtIdx )) == nullptr ) { cwLogError(kInvalidStateRC,"cm Score event index (%i) reported by the score follower is invalid.",r->scEvtIdx ); } else { // verify that the matched event buffer has available space if( p->result_idx_curN >= p->result_idx_allocN ) { cwLogError(kInvalidStateRC,"The score follower match id array is full."); } else { assert( score_event->index == r->scEvtIdx ); /* // store the performance data in the score set_perf( p->scoreH, score_event->index, r->sec, r->pitch, r->vel, r->cost ); perf_meas::result_t pmr = {0}; // Call performance measurement unit if( perf_meas::exec( p->measH, score_event, pmr ) == kOkRC && pmr.loc != kInvalidIdx && pmr.valueA != nullptr ) { double v[ perf_meas::kValCnt ]; for(unsigned i=0; i::max() ? -1 : pmr.valueA[i]; cwLogInfo("Section '%s' triggered loc:%i : dyn:%f even:%f tempo:%f cost:%f", cwStringNullGuard(pmr.sectionLabel), pmr.sectionLoc, v[ perf_meas::kDynValIdx ], v[ perf_meas::kEvenValIdx ], v[ perf_meas::kTempoValIdx ], v[ perf_meas::kMatchCostValIdx ] ); } */ // store a pointer to the m matched sfscore event p->result_idxA[ p->result_idx_curN++ ] = r->index; //score_event->index; } } } } } } cw::rc_t cw::score_follower::create( handle_t& hRef, const args_t& args ) { rc_t rc = kOkRC; if((rc = destroy(hRef)) != kOkRC ) return rc; score_follower_t* p = mem::allocZ(); unsigned track_flags = 0; track_flags += args.trackPrintFl ? sftrack::kPrintFl : 0; track_flags += args.trackResultsBacktrackFl ? sftrack::kBacktrackResultsFl : 0; p->scoreH = args.scoreH; // create the score tracker if((rc = sftrack::create( p->trackH, args.scoreH, args.scoreWndLocN, args.midiWndLocN, track_flags, _score_follower_cb, p )) != kOkRC ) { cwLogError(kOpFailRC,"The score follower create failed."); goto errLabel; } p->srate = sample_rate(p->scoreH); p->result_idx_allocN = event_count( p->scoreH )*2; // give plenty of extra space for the result_idxA[] p->result_idxA = mem::allocZ(p->result_idx_allocN); p->perfN = event_count(p->scoreH)*2; p->perfA = mem::allocZ( p->perfN ); p->perf_idx = 0; p->enableFl = args.enableFl; hRef.set(p); errLabel: if(rc != kOkRC ) _destroy(p); return rc; } cw::rc_t cw::score_follower::create( handle_t& hRef, const object_t* cfg, double srate ) { rc_t rc = kOkRC; if((rc = destroy(hRef)) != kOkRC ) return rc; score_follower_t* p = mem::allocZ(); if((rc = _parse_cfg(p,cfg)) != kOkRC ) goto errLabel; // create the score if((rc = sfscore::create( p->scoreH, p->score_csv_fname, srate, p->dynRefH )) != kOkRC ) { rc = cwLogError(kOpFailRC,"The score could not be initialized from '%s'. cmRC:%i.",cwStringNullGuard(p->score_csv_fname)); goto errLabel; } p->deleteScoreFl = true; // create the score tracker if((rc = sftrack::create( p->trackH, p->scoreH, p->search_area_locN, p->key_wnd_locN, p->track_flags, _score_follower_cb, p )) != kOkRC ) { cwLogError(kOpFailRC,"The score follower create failed."); goto errLabel; } /* if((rc = perf_meas::create( p->measH, p->scoreH )) != kOkRC ) { cwLogError(kOpFailRC,"The perf. measure object create failed."); goto errLabel; } */ p->srate = srate; p->result_idx_allocN = event_count( p->scoreH )*2; // give plenty of extra space for the result_idxA[] p->result_idxA = mem::allocZ(p->result_idx_allocN); p->perfN = event_count(p->scoreH)*2; p->perfA = mem::allocZ( p->perfN ); p->perf_idx = 0; hRef.set(p); errLabel: if( rc != kOkRC ) { _destroy(p); cwLogError(rc,"Score follower create failed."); } return rc; } cw::rc_t cw::score_follower::destroy( handle_t& hRef ) { rc_t rc = kOkRC; score_follower_t* p = nullptr; if( !hRef.isValid() ) return rc; p = _handleToPtr(hRef); if((rc = _destroy(p)) != kOkRC ) return rc; hRef.clear(); return rc; } bool cw::score_follower::is_enabled( handle_t h ) { score_follower_t* p = _handleToPtr(h); return p->enableFl; } void cw::score_follower::enable( handle_t h, bool enable_fl ) { score_follower_t* p = _handleToPtr(h); p->enableFl = enable_fl; } cw::rc_t cw::score_follower::reset( handle_t h, unsigned locId ) { rc_t rc = kOkRC; score_follower_t* p = _handleToPtr(h); if( locId != kInvalidId ) { cwLogInfo("SF Reset: loc:%i",locId); if((rc = reset( p->trackH, locId )) != kOkRC ) { rc = cwLogError(rc,"The score follower reset failed."); goto errLabel; } /* if((rc = reset( p->measH, locId )) != kOkRC ) { rc = cwLogError(rc,"The measurement unit reset failed."); goto errLabel; } */ p->perf_idx = 0; clear_result_index_array(h); clear_all_performance_data(p->scoreH); } errLabel: return rc; } cw::rc_t cw::score_follower::exec( handle_t h, double sec, unsigned smpIdx, unsigned muid, unsigned status, uint8_t d0, uint8_t d1, bool& newMatchFlRef ) { rc_t rc = kOkRC; score_follower_t* p = _handleToPtr(h); unsigned scLocIdx = kInvalidIdx; unsigned pre_result_idx_curN = p->result_idx_curN; if( !p->enableFl ) return cwLogError(kInvalidStateRC,"The score follower is not enabled."); newMatchFlRef = false; // This call results in a callback to: _score_follower_cb() // Note: pass p->perf_idx as 'muid' to the score follower rc = exec( p->trackH, sec, smpIdx, p->perf_idx, status, d0, d1, &scLocIdx ); switch( rc ) { case kOkRC: newMatchFlRef = p->result_idx_curN != pre_result_idx_curN; //printf("NM_FL:%i\n",newMatchFlRef); break; case kEofRC: rc = cwLogInfo("Score match complete."); break; case kInvalidArgRC: rc = cwLogError(kInvalidStateRC,"Score follower state is invalid."); break; case kOpFailRC: rc = cwLogError(kOpFailRC,"The score follower failed during a resync attempt."); break; default: rc = cwLogError(rc,"The score follower failed with an the error:%i",rc); } // store note-on messages if( p->perf_idx < p->perfN && midi::isNoteOn(status,(unsigned)d1) ) { ssf_note_on_t* pno = p->perfA + p->perf_idx; pno->sec = sec; pno->muid = muid; pno->pitch = d0; pno->vel = d1; p->perf_idx += 1; if( p->perf_idx >= p->perfN ) cwLogWarning("The cw score follower performance cache is full."); } return rc; } const unsigned* cw::score_follower::current_result_index_array( handle_t h, unsigned& cur_result_idx_array_cnt_ref ) { score_follower_t* p = _handleToPtr(h); cur_result_idx_array_cnt_ref = p->result_idx_curN; return p->result_idxA; } void cw::score_follower::clear_result_index_array( handle_t h ) { score_follower_t* p = _handleToPtr(h); p->result_idx_curN = 0; } /* cw::rc_t cw::score_follower::cw_loc_range( handle_t h, unsigned& minLocRef, unsigned& maxLocRef ) { rc_t rc = kOkRC; score_follower_t* p = _handleToPtr(h); minLocRef = 0; maxLocRef = loc_count(p->scoreH); return rc; } bool cw::score_follower::is_loc_in_range( handle_t h, unsigned loc ) { rc_t rc; unsigned minLoc = 0; unsigned maxLoc = 0; if((rc = cw_loc_range(h,minLoc,maxLoc)) != kOkRC ) return false; return minLoc <= loc && loc <= maxLoc; } */ unsigned cw::score_follower::has_stored_performance( handle_t h ) { score_follower_t* p = _handleToPtr(h); return p->perf_idx > 0 && result_count(p->trackH) > 0; } cw::rc_t cw::score_follower::sync_perf_to_score( handle_t h ) { rc_t rc = kOkRC; score_follower_t* p = _handleToPtr(h); unsigned resultN = 0; const sftrack::result_t* resultA = nullptr; if( !has_stored_performance(h) ) { rc = cwLogError(kInvalidStateRC,"No performance to sync."); goto errLabel; } resultN = result_count(p->trackH); resultA = result_base(p->trackH); for(unsigned i=0; i= p->perf_idx ) { rc = cwLogError(kInvalidStateRC,"Inconsistent match to perf. map: index mismatch."); goto errLabel; } // verify the pitch of the matching records if( p->perfA[ perf_idx ].pitch != resultA[i].pitch ) { rc = cwLogError(kInvalidStateRC,"Inconsistent match to perf. map: pitch mismatch %i != %i.",p->perfA[ perf_idx ].pitch ,resultA[i].pitch); goto errLabel; } //assert( resultA[i].scEvtIdx == kInvalidIdx || resultA[i].scEvtIdx < p->cmLocToCwLocN ); // if( resultA[i].scEvtIdx != kInvalidIdx ) // p->perfA[ perf_idx ].loc = p->cmLocToCwLocA[ resultA[i].scEvtIdx ]; if( resultA[i].scEvtIdx != kInvalidIdx ) p->perfA[ perf_idx ].loc = resultA[i].oLocId; } errLabel: return rc; } unsigned cw::score_follower::track_result_count( handle_t h ) { score_follower_t* p = _handleToPtr(h); return result_count(p->trackH); } const cw::sftrack::result_t* cw::score_follower::track_result( handle_t h ) { score_follower_t* p = _handleToPtr(h); return result_base(p->trackH); } unsigned cw::score_follower::perf_count( handle_t h ) { score_follower_t* p = _handleToPtr(h); return p->perf_idx; } const cw::score_follower::ssf_note_on_t* cw::score_follower::perf_base( handle_t h ) { score_follower_t* p = _handleToPtr(h); return p->perfA; } cw::rc_t cw::score_follower::write_svg_file( handle_t h, const char* out_fname, bool show_muid_fl ) { score_follower_t* p = _handleToPtr(h); return svgScoreFollowWrite( p->scoreH, p->trackH, p->perfA, p->perf_idx, out_fname, show_muid_fl ); } void cw::score_follower::score_report( handle_t h, const char* out_fname ) { score_follower_t* p = _handleToPtr(h); report(p->scoreH,out_fname); } cw::rc_t cw::score_follower::midi_state_rt_report( handle_t h, const char* out_fname ) { score_follower_t* p = _handleToPtr(h); midi_state::config_t msCfg = midi_state::default_config(); rc_t rc = kOkRC; midi_state::handle_t msH; if((rc = midi_state::create(msH,nullptr,nullptr,&msCfg)) != kOkRC ) { rc = cwLogError(rc,"midi_state create failed."); goto errLabel; } if((rc = _update_midi_state(p,msH)) != kOkRC ) goto errLabel; if((rc = report_events(msH,out_fname)) != kOkRC ) goto errLabel; errLabel: if( rc != kOkRC ) cwLogError(rc,"Score follower midi_state_rt_report() failed."); midi_state::destroy(msH); return rc; } cw::rc_t cw::score_follower::write_sync_perf_csv( handle_t h, const char* out_fname, const midi::file::trackMsg_t** msgA, unsigned msgN ) { score_follower_t* p = _handleToPtr(h); rc_t rc = kOkRC; unsigned resultN = result_count(p->trackH); auto resultA = result_base(p->trackH); bool dampPedalDownFl = false; bool sostPedalDownFl = false; bool softPedalDownFl = false; unsigned curBarNumb = 1; file::handle_t fH; if( msgN == 0 ) { cwLogWarning("Nothing to write."); return rc; } if( resultN == 0 ) { cwLogWarning("The score follower does not have any score sync. info."); return rc; } // open the file if((rc = file::open(fH,out_fname,file::kWriteFl)) != kOkRC ) { rc = cwLogError(kOpenFailRC,"Unable to create the file '%s'.",cwStringNullGuard(out_fname)); goto errLabel; } // write the header line file::printf(fH,"meas,index,voice,loc,tick,sec,dur,rval,dots,sci_pitch,dmark,dlevel,status,d0,d1,bar,section,bpm,grace,damp_down_fl,soft_down_fl,sost_down_fl\n"); for(unsigned i=0; iamicro - msgA[0]->amicro)/1000000.0; // write the event line if( midi::isChStatus(m->status) ) { uint8_t d0 = m->u.chMsgPtr->d0; uint8_t d1 = m->u.chMsgPtr->d1; if( midi::isNoteOn(m->status,d1) ) { unsigned bar = 0; const char* sectionLabel = ""; unsigned loc = score_parse::kInvalidLocId; unsigned dlevel = -1; char sciPitch[ midi::kMidiSciPitchCharCnt + 1 ]; midi::midiToSciPitch( d0, sciPitch, midi::kMidiSciPitchCharCnt ); // locate score matching record for this performed note for(unsigned i=0; iperfN ); if( p->perfA[resultA[i].muid].muid == m->uid && resultA[i].scEvtIdx != kInvalidIdx) { assert( resultA[i].pitch == d0 ); if((e = event( p->scoreH, resultA[i].scEvtIdx )) == nullptr ) { cwLogError(kInvalidStateRC,"The performed, and matched, note with muid %i does not have a valid score event index.",m->uid); goto errLabel; } bar = e->barNumb; sectionLabel = e->section != nullptr ? e->section->label : ""; curBarNumb = std::max(bar,curBarNumb); dlevel = e->dynLevel; loc = resultA[i].oLocId == kInvalidId ? (unsigned)score_parse::kInvalidLocId : resultA[i].oLocId; break; } } rc = file::printf(fH, "%i,%i,%i,%i,0,%f,0.0,0.0,0,%s,,%i,%i,%i,%i,,%s,,,%i,%i,%i\n", bar,i,1,loc,secs,sciPitch,dlevel,m->status,d0,d1,sectionLabel,dampPedalDownFl,softPedalDownFl,sostPedalDownFl); } else { if( midi::isPedal(m->status,d0) ) { switch( d0 ) { case midi::kSustainCtlMdId: dampPedalDownFl = _processPedal("damper",curBarNumb,dampPedalDownFl,d1); break; case midi::kSostenutoCtlMdId: sostPedalDownFl = _processPedal("sostenuto",curBarNumb,sostPedalDownFl,d1); break; case midi::kSoftPedalCtlMdId: softPedalDownFl = _processPedal("soft",curBarNumb,softPedalDownFl,d1); break; } } rc = file::printf(fH, ",%i,,,%i,%f,,,,,,,%i,%i,%i,,,,,%i,%i,%i\n",i,0,secs,m->status,d0,d1,dampPedalDownFl,softPedalDownFl,sostPedalDownFl); } } if( rc != kOkRC ) { rc = cwLogError(rc,"Write failed on line:%i", i+1 ); goto errLabel; } } errLabel: file::close(fH); cwLogInfo("Saved %i events to sync perf. file. '%s'.", msgN, out_fname ); return rc; }