//| 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 "cwDynRefTbl.h" #include "cwScoreParse.h" #include "cwSfScore.h" #include "cwPerfMeas.h" #include "cwPianoScore.h" #include "cwSvg.h" #include "cwMidiState.h" #include "cwSvgMidi.h" #define PIX_PER_SEC 100.0 #define NOTE_HEIGHT 15.0 #define TIME_GRID_SECS 5.0 #define PITCH_LABEL_INTERVAL_SECS 10 namespace cw { namespace svg_midi { enum { kBarTypeId, kSectionTypeId }; rc_t _write_svg_rect( svg::handle_t svgH, double secs0, double secs1, double y, const char* label, unsigned color ) { rc_t rc; double x = secs0 * PIX_PER_SEC; double ww = secs1 * PIX_PER_SEC - x; double hh = NOTE_HEIGHT; if((rc = svg::rect( svgH, x, y*NOTE_HEIGHT, ww, hh, "fill", color, "rgb" )) != kOkRC ) { rc = cwLogError(rc,"Error creating SVG rect."); goto errLabel; } if((rc = svg::text( svgH, x, y*NOTE_HEIGHT+NOTE_HEIGHT, label )) != kOkRC ) { rc = cwLogError(rc,"Error writing SVG rect label."); goto errLabel; } errLabel: return rc; } rc_t _write_svg_vert_line( svg::handle_t svgH, double sec, unsigned color, unsigned minMidiPitch, unsigned maxMidiPitch ) { rc_t rc = kOkRC; if((rc = line(svgH, sec*PIX_PER_SEC, 0, sec*PIX_PER_SEC, (maxMidiPitch-minMidiPitch)*NOTE_HEIGHT, "stroke", color, "rgb")) != kOkRC ) { rc = cwLogError(rc,"Error writing SVG line."); goto errLabel; } errLabel: return rc; } rc_t _write_svg_horz_line( svg::handle_t svgH, double sec0, double sec1, double y, unsigned color ) { rc_t rc = kOkRC; if((rc = line(svgH, sec0*PIX_PER_SEC, y*NOTE_HEIGHT, sec1*PIX_PER_SEC, y*NOTE_HEIGHT, "stroke", color, "rgb")) != kOkRC ) { rc = cwLogError(rc,"Error writing SVG line."); goto errLabel; } errLabel: return rc; } rc_t _write_svg_line( svg::handle_t svgH, double sec0, double y0, double sec1, double y1, unsigned color ) { rc_t rc = kOkRC; if((rc = line(svgH, sec0*PIX_PER_SEC, y0, sec1*PIX_PER_SEC, y1, "stroke", color, "rgb")) != kOkRC ) { rc = cwLogError(rc,"Error writing SVG line."); goto errLabel; } errLabel: return rc; } const midi_state::event_t* _write_note_rect( svg::handle_t svgH, const midi_state::event_t* e0, const midi_state::event_t* e1, const midi_state::event_t* t0, unsigned minMidiPitch, unsigned maxMidiPitch ) { cwAssert( e0!=nullptr && e1!=nullptr && e0->msg != nullptr && e1->msg !=nullptr); const char* sciPitch = midi::midiToSciPitch( e0->msg->u.midi.d0 ); const unsigned labelCharN = 127; char label[ labelCharN+1 ]; unsigned muid = e0->msg->u.midi.uid; if( t0!=nullptr && e0->secs - t0->secs < PITCH_LABEL_INTERVAL_SECS ) snprintf(label,labelCharN,"%i",muid); else { snprintf(label,labelCharN,"%s - %i",sciPitch,muid); t0 = e1; } double y = -1.0 * (e0->msg->u.midi.d0 - minMidiPitch) + (maxMidiPitch - minMidiPitch); _write_svg_rect( svgH, e0->secs, e1->secs, y, label, 0xafafaf ); return t0; } void _write_sound_line( svg::handle_t svgH, const midi_state::event_t* e0, const midi_state::event_t* e1, unsigned minMidiPitch, unsigned maxMidiPitch ) { cwAssert( e0!=nullptr && e1!=nullptr && e0->msg != nullptr && e1->msg !=nullptr); double y = -1.0 * (e0->msg->u.midi.d0 - minMidiPitch) + (maxMidiPitch - minMidiPitch); _write_svg_horz_line( svgH, e0->secs, e1->secs, y, 0xafafaf ); } void _write_marker( svg::handle_t svgH, const midi_state::event_t* e, unsigned minMidiPitch, unsigned maxMidiPitch ) { unsigned color = e->msg->u.marker.typeId == kBarTypeId ? 0x0000ff : 0xff0000; _write_svg_vert_line( svgH, e->secs, color, minMidiPitch, maxMidiPitch ); unsigned labelCharN = 127; char label[labelCharN+1]; snprintf(label,labelCharN,"%i",e->msg->u.marker.value); svg::text( svgH, e->secs*PIX_PER_SEC, -20, label ); } void _write_svg_ch_note( svg::handle_t svgH, const midi_state::event_t* e0, unsigned minMidiPitch, unsigned maxMidiPitch ) { const midi_state::event_t* e = e0; const midi_state::event_t* n0 = nullptr; const midi_state::event_t* s0 = nullptr; const midi_state::event_t* t0 = nullptr; for(; e!=nullptr; e=e->link) { if( cwIsFlag(e->flags,midi_state::kMarkerEvtFl) ) { _write_marker( svgH, e, minMidiPitch, maxMidiPitch ); } if( cwIsFlag(e->flags,midi_state::kNoteOffFl) ) { if( n0 == nullptr ) { // consecutive note off msgs } else { t0 = _write_note_rect( svgH, n0, e, t0, minMidiPitch, maxMidiPitch ); } n0 = nullptr; } if( cwIsFlag(e->flags,midi_state::kNoteOnFl) ) { // if note on without note-off if( n0 != nullptr ) { // TODO: check for reattack flag t0 = _write_note_rect( svgH, n0, e, t0, minMidiPitch, maxMidiPitch ); } n0 = e; } if( cwIsFlag(e->flags,midi_state::kSoundOnFl) ) { if( s0 != nullptr ) { // consecutive sound on msgs _write_sound_line( svgH, s0, e, minMidiPitch, maxMidiPitch ); } s0 = e; } if( cwIsFlag(e->flags,midi_state::kSoundOffFl) ) { if( s0 == nullptr ) { // consecutive off msgs } else { _write_sound_line( svgH, s0, e, minMidiPitch, maxMidiPitch ); } s0 = nullptr; } } } void _write_svg_ch_pedal( svg::handle_t svgH, const midi_state::event_t* e, unsigned pedal_idx, unsigned minMidiPitch, unsigned maxMidiPitch, unsigned pedalCnt ) { const midi_state::event_t* e0 = nullptr; const midi_state::event_t* e1 = nullptr; unsigned color = 0; const char* label = nullptr; unsigned midiCtlId = midi_state::pedal_index_to_midi_ctl_id(pedal_idx); switch( midiCtlId ) { case midi::kSustainCtlMdId: label = "damp"; color = 0xf4a460; break; case midi::kSostenutoCtlMdId: label = "sost"; color = 0x7fffd4; break; case midi::kSoftPedalCtlMdId: label = "soft"; color = 0x98fb98; break; default: assert(0); } for(; e!=nullptr; e=e->link) { if( !cwIsFlag(e->flags,midi_state::kNoChangeFl) ) { if( cwIsFlag(e->flags,midi_state::kDownPedalFl) ) { if( e0 != nullptr ) { // two consecutive pedal downd - this shouldn't be possible } else { e0 = e; } } if( cwIsFlag(e->flags,midi_state::kUpPedalFl)) { if( e0 == nullptr ) { // two consecutive pedal up's } else { double y = (maxMidiPitch - minMidiPitch) + 1 + pedal_idx; _write_svg_rect( svgH, e0->secs, e->secs, y, label, color ); if( midiCtlId == midi::kSustainCtlMdId ) _write_svg_vert_line( svgH, e->secs, color, minMidiPitch, maxMidiPitch ); e0 = nullptr; } } } if( e1 != nullptr ) { unsigned yOffs = ((maxMidiPitch - minMidiPitch) + pedalCnt) * NOTE_HEIGHT; _write_svg_line( svgH, e1->secs, yOffs + e1->msg->u.midi.d1, e->secs, yOffs + e->msg->u.midi.d1, color ); } e1 = e; } } rc_t _load_from_piano_score( midi_state::handle_t msH, const char* piano_score_fname ) { rc_t rc = kOkRC; perf_score::handle_t scH; unsigned n = 0; if((rc = perf_score::create( scH, piano_score_fname )) != kOkRC ) { rc = cwLogError(rc,"The piano score load failed."); goto errLabel; } else { const perf_score::event_t* e = base_event(scH); for(; e!=nullptr; e=e->link) { uint8_t ch = 0; if( e->bar != 0 ) if((rc = setMarker(msH, e->sec, e->uid, ch, kBarTypeId, e->bar )) != kOkRC ) { rc = cwLogError(rc,"Error setting bar marker."); goto errLabel; } if( e->section != 0 ) if((rc = setMarker(msH, e->sec, e->uid, ch, kSectionTypeId, e->section )) != kOkRC ) { rc = cwLogError(rc,"Error setting section marker."); goto errLabel; } if( e->status != 0 ) { if( e->status < 255 && e->d0 < 128 && e->d1 < 128 ) { uint8_t status = (uint8_t)e->status & 0xf0; uint8_t ch = (uint8_t)e->status & 0x0f; uint8_t d0 = (uint8_t)e->d0; uint8_t d1 = (uint8_t)e->d1; //printf("%i : %i %i : %i %x %i %i\n", n, e->uid, e->loc, ch, status, d0, d1 ); if((rc = setMidiMsg(msH, e->sec, e->uid, ch, status, d0, d1 ) ) != kOkRC ) { rc = cwLogError(rc,"Error on MIDI event insertion."); goto errLabel; } ++n; } } } } errLabel: destroy(scH); return rc; } } } namespace cw { namespace svg_midi { typedef struct svg_midi_str { midi_state::handle_t msH; } svg_midi_t; svg_midi_t* _handleToPtr( handle_t h ) { return handleToPtr(h); } rc_t _destroy( svg_midi_t* p ) { midi_state::destroy(p->msH); mem::release(p); return kOkRC; } } } cw::rc_t cw::svg_midi::create( handle_t& hRef ) { rc_t rc; if((rc = destroy(hRef)) != kOkRC ) return rc; svg_midi_t* p = mem::allocZ(); if((rc = midi_state::create(p->msH, nullptr, nullptr, &midi_state::default_config())) != kOkRC ) { rc = cwLogError(rc,"midi_state create failed."); goto errLabel; } hRef.set(p); errLabel: return rc; } cw::rc_t cw::svg_midi::destroy( handle_t& hRef ) { rc_t rc = kOkRC; if(!hRef.isValid()) return rc; svg_midi_t* p = _handleToPtr(hRef); if((rc = _destroy(p)) != kOkRC ) return rc; hRef.clear(); return rc; } cw::rc_t cw::svg_midi::setMidiMsg( handle_t h, double secs, unsigned uid, unsigned ch, unsigned status, unsigned d0, unsigned d1 ) { rc_t rc; svg_midi_t* p = _handleToPtr(h); if( ch >= midi::kMidiChCnt ) { rc = cwLogError(kInvalidArgRC,"Invalid MIDI channel value: %i.",ch); goto errLabel; } if( !midi::isStatus(status)) { rc = cwLogError(kInvalidArgRC,"Invalid MIDI status value: %i.",status); goto errLabel; } if( d0>127 ) { rc = cwLogError(kInvalidArgRC,"Invalid MIDI d0 value: %i.",d0); goto errLabel; } if( d1>127 ) { rc = cwLogError(kInvalidArgRC,"Invalid MIDI d1 value: %i.",d1); goto errLabel; } if((rc = midi_state::setMidiMsg( p->msH, secs, uid, (uint8_t)ch, (uint8_t)status, (uint8_t)d0, (uint8_t)d1 )) != kOkRC ) { rc = cwLogError(rc,"midi_state MIDI msg update failed."); goto errLabel; } errLabel: return rc; } cw::rc_t cw::svg_midi::setMarker( handle_t h, double secs, unsigned uid, unsigned ch, unsigned markId, unsigned markValue ) { rc_t rc; svg_midi_t* p = _handleToPtr(h); if( ch >= midi::kMidiChCnt ) { rc = cwLogError(kInvalidArgRC,"Invalid MIDI channel value: %i.",ch); goto errLabel; } if((rc = midi_state::setMarker( p->msH, secs, uid, (uint8_t)ch, markId, markValue )) != kOkRC ) { rc = cwLogError(rc,"midi_state MIDI set marker failed."); goto errLabel; } errLabel: return rc; } cw::rc_t cw::svg_midi::write( handle_t h, const char* fname ) { rc_t rc; svg_midi_t* p = _handleToPtr(h); if((rc = write(fname,p->msH)) != kOkRC ) { rc = cwLogError(rc,"MIDI SVG write failed."); goto errLabel; } errLabel: return rc; } cw::rc_t cw::svg_midi::write( const char* fname, midi_state::handle_t msH ) { rc_t rc = kOkRC; uint8_t minMidiPitch = midi::kMidiNoteCnt; uint8_t maxMidiPitch = 0; unsigned pedal_cnt = midi_state::pedal_count( msH ); double minSec = 0.0; double maxSec = 0.0; const midi_state::event_t* evt = nullptr; svg::handle_t svgH; get_note_extents( msH, minMidiPitch, maxMidiPitch, minSec, maxSec ); printf("pitch - min:%i max:%i sec - min:%f max:%f\n",minMidiPitch,maxMidiPitch,minSec,maxSec); // create the SVG file object if((rc = svg::create(svgH)) != kOkRC ) { rc = cwLogError(rc,"SVG file object create failed."); goto errLabel; } // create the time grid for(double sec = 0.0; sec<=maxSec; sec+=TIME_GRID_SECS) _write_svg_vert_line(svgH, sec, 0xefefef, minMidiPitch, maxMidiPitch ); // create the note graphics for(uint8_t i=0; igetv( "src_file_type", src_file_type, "src_fname", src_fname, "out_fname", out_fname, "midi_state_args",midi_state_args)) != kOkRC ) { rc = cwLogError(rc,"Error parsing svg_midi::test_midi_file() arguments."); goto errLabel; } if( textCompare( src_file_type, "midi" ) == 0 ) { rc = midi_to_svg_file( src_fname, out_fname, midi_state_args ); } else if( textCompare( src_file_type, "piano_score" ) == 0 ) { rc = piano_score_to_svg_file( src_fname, out_fname, midi_state_args ); } else { rc = cwLogError(kInvalidArgRC,"Invalid file type:'%s'.",cwStringNullGuard(src_file_type)); goto errLabel; } if( rc != kOkRC ) cwLogError(rc,"The SVG file create failed."); errLabel: return rc; }