//| 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 "cwObject.h" #include "cwText.h" #include "cwTime.h" #include "cwMidi.h" #include "cwMidiFile.h" #include "cwSvg.h" #include "cwDynRefTbl.h" #include "cwScoreParse.h" #include "cwSfScore.h" #include "cwSfMatch.h" #include "cwSfTrack.h" #include "cwPerfMeas.h" #include "cwPianoScore.h" #include "cwScoreFollowerPerf.h" #include "cwScoreFollower.h" #include "cwSvgScoreFollow.h" #define BOX_W 50 #define BOX_H 50 #define BORD_X 2 #define BORD_Y 2 #define ROW_0_Y 0 #define PRE_REF_IDX 0 namespace cw { namespace score_follower { struct ssf_ref_note_str; enum { kLocTId, kIdTId, kPreTId }; // bar/section or location anchor // (one for each top row of UI elements) typedef struct ssf_ref_str { unsigned tid; // See k??TId unsigned left; unsigned top; unsigned col_bottom; struct ssf_ref_note_str* refNoteL; // ref_note's linked to this ref union { const sfscore::loc_t* loc; // score location unsigned id; // bar/section id } u; } ssf_ref_t; // ref. score note that performed notes should match to typedef struct ssf_ref_note_str { ssf_ref_t* ref; // loc UI component that this ref. note belongs to const sfscore::event_t* evt; // cmScore event assoc'd with this note unsigned top; unsigned cnt; // count of perf notes that matched to this ref-note unsigned cwLocId; struct ssf_ref_note_str* link; } ssf_ref_note_t; typedef struct ssf_perf_note_str { unsigned left; unsigned top; ssf_ref_note_t* refNote; // ref. note that this perf. note matches to const ssf_note_on_t* msg; // MIDI info for this performed note unsigned seqId; // performed note sequence id == index into perfA[] of this note bool isVisibleFl; // true if this rect is visible // 'duplicate' perf notes occur when the same perf note is connected // to multiple references. This can occur when the score follower backtracks. struct ssf_perf_note_str* dupl_list; // list of 'duplicate' perf. notes. struct ssf_perf_note_str* dupl_link; } ssf_perf_note_t; typedef struct ssf_str { svg::handle_t svgH; sfscore::handle_t scoreH; sftrack::handle_t trackH; ssf_ref_t* refA; unsigned refAllocN; unsigned refN; ssf_ref_note_t* refNoteA; unsigned refNoteAllocN; unsigned refNoteN; ssf_perf_note_t* perfA; unsigned perfAllocN; unsigned perfN; unsigned minMatchLoc; unsigned maxMatchLoc; } ssf_t; rc_t _write_rect( ssf_t* p, unsigned left, unsigned top, const char* label, const char* classLabel, const char* classLabel2=nullptr, unsigned cwLocId=kInvalidId ) { rc_t rc = kOkRC; int n = textLength(classLabel) + textLength(classLabel2) + 2; char buf[ n + 1 ]; if( classLabel2 != nullptr ) { snprintf(buf,n,"%s %s",classLabel,classLabel2); buf[n] = 0; classLabel = buf; } if((rc = svg::rect(p->svgH,left,top,BOX_W,BOX_H,"class",classLabel,nullptr)) != kOkRC ) { rc = cwLogError(rc,"Error writing SVG rect."); goto errLabel; } if((rc = svg::text(p->svgH,left,top+20,label)) != kOkRC ) { rc = cwLogError(rc,"Error writing SVG text."); goto errLabel; } if( cwLocId != kInvalidId ) { unsigned locIdCharN = 31; char locIdBuf[ locIdCharN + 1 ]; snprintf(locIdBuf,locIdCharN,"%i",cwLocId); if((rc = svg::text(p->svgH,left,top+40,locIdBuf)) != kOkRC ) { rc = cwLogError(rc,"Error writing SVG cwLocId text."); goto errLabel; } } errLabel: return rc; } rc_t _write_rect_id( ssf_t* p, unsigned left, unsigned top, const char* classLabel, unsigned id ) { const unsigned labelCharN = 31; char label[labelCharN+1]; snprintf(label,labelCharN,"%5i",id); return _write_rect(p,left,top,label,classLabel); } rc_t _write_rect_pitch( ssf_t* p, unsigned left, unsigned top, const char* classLabel, unsigned midi_pitch, bool noMatchFl,unsigned cnt=kInvalidCnt, unsigned cwLocId=kInvalidId) { const unsigned labelCharN = 63; char label[labelCharN+1]; const char* classLabel2 = noMatchFl ? "no_match" : nullptr; if( midi_pitch == midi::kInvalidMidiPitch ) snprintf(label,labelCharN,"%s","pre"); else if( cnt == kInvalidCnt ) snprintf(label,labelCharN,"%s",midi::midiToSciPitch((uint8_t)midi_pitch)); else { snprintf(label,labelCharN,"%s %i",midi::midiToSciPitch((uint8_t)midi_pitch),cnt); classLabel2 = "multi_match"; } return _write_rect(p,left,top,label,classLabel,classLabel2,cwLocId); } rc_t _write( ssf_t* p, bool show_muid_fl ) { rc_t rc = kOkRC; bool fl = false; unsigned offset_x = 0; for(unsigned i=0; irefN; ++i) { ssf_ref_t* r = p->refA + i; const char* classLabel = r->tid==kLocTId ? "loc" : (r->tid==kIdTId ? "bar" : "pre"); unsigned id = r->tid==kLocTId ? r->u.loc->index : (r->tid==kIdTId ? r->u.id : 0); if( fl==false && r->tid==kLocTId && r->u.loc->index == p->minMatchLoc ) { fl = true; offset_x = r->left - (BOX_W + BORD_X) ; } if( fl && r->tid==kLocTId && r->u.loc->index > p->maxMatchLoc ) fl = false; if( !fl && r->tid != kPreTId ) continue; if( r->left < offset_x && r->tid != kPreTId ) continue; // write ref rect if((rc = _write_rect_id( p, r->left-offset_x, r->top, classLabel, id )) != kOkRC ) { rc = cwLogError(rc,"Error writing 'loc' rect id:%i.",r->u.loc->index); goto errLabel; } // write the ref-note for(ssf_ref_note_t* rn=r->refNoteL; rn!=nullptr; rn = rn->link) { unsigned pitch = rn->evt == nullptr ? (unsigned)midi::kInvalidMidiPitch : rn->evt->pitch; unsigned cnt = rn->cnt > 1 ? rn->cnt : kInvalidCnt; bool noMatchFl = rn->cnt == 0; if((rc = _write_rect_pitch( p, r->left-offset_x, rn->top, "ref_note", pitch, noMatchFl, cnt, rn->cwLocId )) != kOkRC ) { rc = cwLogError(rc,"Error writing 'ref_note' rect."); goto errLabel; } } } // write the pref-notes for(unsigned i=0; iperfN; ++i) { ssf_perf_note_t* pn = p->perfA + i; bool isPreFl = pn->refNote !=nullptr && pn->refNote->ref->tid == kPreTId; bool noMatchFl = pn->refNote==nullptr || isPreFl; // Rectangles whose 'left' coord is less than 'offset' occur prior to p->minMatchLoc // (but the 'pre' column is always shown) if( pn->left >= offset_x || isPreFl ) { pn->isVisibleFl = true; unsigned left = isPreFl ? pn->left : pn->left-offset_x; unsigned perf_id = show_muid_fl ? pn->msg->muid : pn->seqId; if((rc = _write_rect_pitch( p, left, pn->top, "perf_note", pn->msg->pitch, noMatchFl, kInvalidCnt, perf_id )) != kOkRC ) { rc = cwLogError(rc,"Error writing 'pref_note' rect."); goto errLabel; } } } for(unsigned i=0; iperfN; ++i) { ssf_perf_note_t* pn = p->perfA + i; if( pn->isVisibleFl) { ssf_perf_note_t* pn0 = pn; for(ssf_perf_note_t* pn1=pn->dupl_list; pn1!=nullptr; pn1=pn1->dupl_link) { if( pn1->isVisibleFl ) { line(p->svgH,pn0->left-offset_x,pn0->top,pn1->left-offset_x,pn0->top); pn0 = pn1; } } } } errLabel: return rc; } rc_t _destroy( ssf_t* p ) { rc_t rc; if((rc = svg::destroy(p->svgH)) != kOkRC ) rc = cwLogError(rc,"SVG file object destroy failed."); else { mem::release(p->refA); mem::release(p->refNoteA); mem::release(p->perfA); mem::release(p); } return rc; } ssf_ref_t* _create_ref( ssf_t* p, unsigned& x_coord_ref, unsigned y_coord ) { ssf_ref_t* ref = nullptr; if( p->refN < p->refAllocN ) { ref = p->refA + p->refN; ref->left = x_coord_ref; ref->top = y_coord; ref->col_bottom = y_coord + BOX_H + BORD_Y; p->refN += 1; if( p->refN == p->refAllocN ) cwLogWarning("SVG score follower reference array cache is full."); x_coord_ref += (BOX_W+BORD_X); } return ref; } void _create_pre_ref( ssf_t* p, unsigned& x_coord_ref, unsigned y_coord ) { ssf_ref_t* ref; if((ref = _create_ref(p,x_coord_ref,y_coord)) != nullptr ) { ref->tid = kPreTId; } } void _create_bar_ref( ssf_t* p, const sfscore::event_t* e, unsigned& x_coord_ref, unsigned y_coord ) { ssf_ref_t* ref; if((ref = _create_ref(p,x_coord_ref,y_coord)) != nullptr ) { ref->tid = kIdTId; ref->u.id = e->barNumb; } } void _create_loc_ref( ssf_t* p, const sfscore::event_t* e, unsigned& x_coord_ref, unsigned y_coord ) { ssf_ref_t* ref; if((ref = _create_ref(p,x_coord_ref,y_coord)) != nullptr ) { ref->tid = kLocTId; ref->u.loc = sfscore::loc_base(p->scoreH) + e->oLocId; cwAssert( ref->u.loc != nullptr and ref->u.loc->index == e->oLocId ); } } void _create_ref_array( ssf_t* p, unsigned& x_coord_ref, unsigned y_coord ) { // create the cmScoreLoc array unsigned cmEvtN = event_count(p->scoreH); unsigned loc_idx = kInvalidIdx; p->refAllocN = cmEvtN + 1; p->refA = mem::allocZ(p->refAllocN); p->refN = 0; _create_pre_ref(p, x_coord_ref, y_coord ); for(unsigned i=0; iscoreH,i); // if this is a bar marker if( e->type == score_parse::kBarTId ) _create_bar_ref(p,e,x_coord_ref,y_coord); // if this is the start of a new location if( e->oLocId != loc_idx) { _create_loc_ref(p,e,x_coord_ref,y_coord); loc_idx = e->oLocId; } } // reset the min and max loc index p->minMatchLoc = loc_idx == kInvalidIdx ? 0 : loc_idx; p->maxMatchLoc = 0; } rc_t _attach_ref_note( ssf_t* p, unsigned ref_idx, sfscore::event_t* evt ) { rc_t rc = kOkRC; ssf_ref_note_t* rn = p->refNoteA + p->refNoteN + 1; rn->ref = p->refA + ref_idx; rn->evt = evt; rn->top = rn->ref->col_bottom; rn->cnt = 0; rn->cwLocId = evt==nullptr ? 0 : evt->oLocId; rn->link = rn->ref->refNoteL; rn->ref->refNoteL = rn; rn->ref->col_bottom += BOX_H + BORD_Y; p->refNoteN += 1; if( p->refNoteN >= p->refNoteAllocN ) { rc = cwLogError(kBufTooSmallRC,"The ref note array in the SVG score writer is too small."); goto errLabel; } errLabel: return rc; } rc_t _create_ref_note_array( ssf_t* p ) { rc_t rc = kOkRC; p->refNoteAllocN = p->refAllocN + 1; p->refNoteA = mem::allocZ(p->refNoteAllocN); p->refNoteN = 0; // attach the 'pre' ref note _attach_ref_note(p,PRE_REF_IDX,nullptr); for(unsigned ri=PRE_REF_IDX+1; rirefN; ++ri) if( p->refA[ri].tid == kLocTId ) for(unsigned j=0; jrefA[ri].u.loc->evtCnt; ++j) if( p->refA[ri].u.loc->evtArray[j]->type == score_parse::kNoteOnTId && p->refNoteN < p->refNoteAllocN ) { if((rc = _attach_ref_note(p,ri,p->refA[ri].u.loc->evtArray[j])) != kOkRC ) goto errLabel; } errLabel: return rc; } ssf_ref_note_t* _find_ref_note( ssf_t* p, const sfscore::event_t* e ) { ssf_ref_note_t* rn = nullptr; for(unsigned i=0; irefN; ++i) { ssf_ref_t* r = p->refA + i; // if this is the ref record for the target location if( r->tid == kLocTId and r->u.loc->index == e->oLocId ) { // locate the ref note associated with e for(rn=r->refNoteL; rn!=nullptr; rn=rn->link ) if( rn->evt->index == e->index ) break; } } // rn SHOULD NEVER BE null BUT THIS TEST FAILS FOR PRE-PERF NOTES: SEE:**** //assert( rn != nullptr ); return rn; } rc_t _setup_perf_note( ssf_t* p, ssf_ref_t* ref, ssf_ref_note_t* rn, ssf_ref_note_t* dupl_rn, const ssf_note_on_t* msg, ssf_perf_note_t*& pnRef ) { rc_t rc = kOkRC; if( p->perfN >= p->perfAllocN ) { rc = cwLogError(kBufTooSmallRC,"The perf_note array is too small."); } else { ssf_perf_note_t* pn = p->perfA + p->perfN; if( ref->tid==kPreTId) rn = ref->refNoteL; pn->left = ref->left; pn->top = ref->col_bottom; pn->refNote = rn; pn->msg = msg; pn->seqId = p->perfN; ref->col_bottom += BOX_H + BORD_Y; p->perfN += 1; if( rn != nullptr ) rn->cnt += 1; pnRef = pn; } return rc; } rc_t _create_perf_array( ssf_t* p, const ssf_note_on_t* midiA, unsigned midiN ) { rc_t rc = kOkRC; p->perfAllocN = midiN*2; p->perfA = mem::allocZ(p->perfAllocN); p->perfN = 0; ssf_ref_t* r0 = p->refA + PRE_REF_IDX; // for each performed MIDI note for(unsigned muid=0; muidtrackH); const sftrack::result_t* resultA = result_base(p->trackH); // look for a 'match' record that refers to this perf note // by interating through the matcher->res[] result array for(unsigned j=0; jscoreH, resultA[j].scEvtIdx )) != nullptr ) { if((rn = _find_ref_note( p, e )) == nullptr ) break; assert( rn != nullptr ); ssf_perf_note_t* pn = nullptr; if((rc = _setup_perf_note(p,rn->ref,rn,rn0,midiA+muid,pn)) != kOkRC ) goto errLabel; rn0 = rn; r0 = rn->ref; // maintain a list of pointers to duplicate performance notes if( pn0 == nullptr ) pn0 = pn; else { pn->dupl_link = pn0; pn0->dupl_list = pn; } if( e->oLocId < p->minMatchLoc ) p->minMatchLoc = e->oLocId; if( e->oLocId > p->maxMatchLoc ) p->maxMatchLoc = e->oLocId; } else { // this should be impossible assert(0); } } } // if this perf note does not have any matches if( rn == nullptr ) { assert( r0 != nullptr ); ssf_perf_note_t* dummy = nullptr; if((rc = _setup_perf_note(p,r0,nullptr,nullptr,midiA+muid,dummy)) != kOkRC ) goto errLabel; } } errLabel: if( rc != kOkRC ) rc = cwLogError(rc,"create_perf_array failed."); return rc; } void _create_css( ssf_t* p ) { install_css(p->svgH,".pre","fill",0xb0b0b0,"rgb"); install_css(p->svgH,".bar","fill",0xa0a0a0,"rgb"); install_css(p->svgH,".loc","fill",0xe0e0e0,"rgb"); install_css(p->svgH,".ref_note","fill",0x40E0D0,"rgb"); install_css(p->svgH,".perf_note","fill",0xE0FFFF,"rgb"); install_css(p->svgH,".no_match","stroke",0xFF0000,"rgb"); install_css(p->svgH,".multi_match","stroke",0x0000FF,"rgb"); } ssf_t* _create( sfscore::handle_t scoreH, sftrack::handle_t trackH, const ssf_note_on_t* midiA, unsigned midiN ) { rc_t rc; ssf_t* p = mem::allocZ(); unsigned x_coord = 0; unsigned y_coord = 0; p->scoreH = scoreH; p->trackH = trackH; // create the SVG file object if((rc = svg::create(p->svgH)) != kOkRC ) { rc = cwLogError(rc,"SVG file object create failed."); goto errLabel; } _create_css(p); // create the refence row (the top row or bar and loc markers) _create_ref_array( p, x_coord, y_coord ); // create the reference notes _create_ref_note_array( p ); // create the performance array _create_perf_array(p,midiA,midiN ); errLabel: if(rc != kOkRC ) _destroy(p); return p; } } } cw::rc_t cw::score_follower::svgScoreFollowWrite( sfscore::handle_t scoreH, sftrack::handle_t trackH, ssf_note_on_t* midiA, unsigned midiN, const char* out_fname, bool show_muid_fl) { rc_t rc = kOkRC; ssf_t* p; if((p = _create(scoreH, trackH, midiA, midiN )) == nullptr ) { rc = cwLogError(rc,"Score follower SVG writer initialization failed."); goto errLabel; } if((rc = _write(p,show_muid_fl)) != kOkRC ) { rc = cwLogError(rc,"Error writing SSF reference row."); goto errLabel; } if((rc = write( p->svgH, out_fname, nullptr, svg::kStandAloneFl )) != kOkRC ) { rc = cwLogError(rc,"Error creating SSF output file."); goto errLabel; } errLabel: _destroy(p); return rc; }