//| 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 "cwFile.h" #include "cwTime.h" #include "cwMidiDecls.h" #include "cwMidi.h" #include "cwMidiFile.h" #include "cwUiDecls.h" #include "cwIo.h" #include "cwScoreFollowerPerf.h" #include "cwIoMidiRecordPlay.h" #include "cwMidiState.h" #include "cwSvgMidi.h" #define TIMER_LABEL "midi_record_play_timer" namespace cw { namespace midi_record_play { enum { kStoppedMidiStateId = 0, kPlayingMidiStateId, kStoppingMidiStateId }; typedef struct midi_state_str { uint8_t keyVel[ midi::kMidiNoteCnt ]; uint8_t ctlVal[ midi::kMidiCtlCnt ]; unsigned state; } midi_state_t; typedef struct am_midi_msg_str { unsigned devIdx; unsigned portIdx; unsigned microsec; unsigned id; time::spec_t timestamp; unsigned loc; const void* arg; uint8_t ch; uint8_t status; uint8_t d0; uint8_t d1; unsigned chordNoteCnt; // count of notes in same chord as this note (only set on Note-on messages) } am_midi_msg_t; typedef struct vel_tbl_str { uint8_t* velTblA; unsigned velTblN; bool enableFl; bool defaultFl; char* name; char* device; } vel_tbl_t; typedef struct midi_device_str { char* label; char* midiOutDevLabel; char* midiOutPortLabel; unsigned midiOutDevIdx; unsigned midiOutPortIdx; bool enableFl; unsigned velTableN; uint8_t* velTableArray; bool pedalMapEnableFl; unsigned pedalDownVelId; unsigned pedalDownVel; unsigned pedalUpVelId; unsigned pedalUpVel; unsigned pedalDownHalfVelId; unsigned pedalDownHalfVel; unsigned pedalUpHalfVelId; unsigned pedalUpHalfVel; unsigned velHistogram[ midi::kMidiVelCnt ]; bool force_damper_down_fl; unsigned force_damper_down_threshold; unsigned force_damper_down_velocity; bool damper_dead_band_enable_fl; unsigned damper_dead_band_min_value; unsigned damper_dead_band_max_value; bool scale_chord_notes_enable_fl; double scale_chord_notes_factor; midi_state_t midi_state; } midi_device_t; enum { kHalfPedalDone, kWaitForBegin, kWaitForNoteOn, kWaitForNoteOff, kWaitForPedalUp, kWaitForPedalDown, }; typedef struct midi_record_play_str { io::handle_t ioH; am_midi_msg_t* msgArray; // msgArray[ msgArrayN ] unsigned msgArrayN; // Count of messages allocated in msgArray. unsigned msgArrayInIdx; // Next available space for loaded MIDI messages (also the current count of msgs in msgArray[]) unsigned msgArrayOutIdx; // Next message to transmit in msgArray[] unsigned midi_timer_period_micro_sec; // Timer period in microseconds unsigned midi_timer_index; // unsigned all_off_delay_ms; // Wait this long before turning all notes off after the last note-on has played am_midi_msg_t* iMsgArray; // msgArray[ msgArrayN ] unsigned iMsgArrayN; // Count of messages allocated in msgArray. unsigned iMsgArrayInIdx; // Next available space for incoming MIDI messages (also the current count of msgs in msgArray[]) unsigned lastStoreIndex; midi_device_t* midiDevA; unsigned midiDevN; bool startedFl; bool recordFl; bool muteFl; bool thruFl; bool logInFl; // log incoming message when not in 'record' mode. bool logOutFl; // log outgoing messages bool supressMidiXmitFl; // don't transmit MIDI unsigned minDamperDownMs; bool velHistogramEnableFl; bool halfPedalFl; unsigned halfPedalState; unsigned halfPedalNextUs; unsigned halfPedalNoteDelayUs; unsigned halfPedalNoteDurUs; unsigned halfPedalUpDelayUs; unsigned halfPedalDownDelayUs; uint8_t halfPedalMidiPitch; uint8_t halfPedalMidiNoteVel; uint8_t halfPedalMidiPedalVel; time::spec_t play_time; time::spec_t start_time; time::spec_t end_play_event_timestamp; time::spec_t all_off_timestamp; event_callback_t cb; void* cb_arg; } midi_record_play_t; enum { kMidiRecordPlayTimerId }; midi_record_play_t* _handleToPtr( handle_t h ) { return handleToPtr(h); } rc_t _destroy( midi_record_play_t* p ) { rc_t rc = kOkRC; if(p->midi_timer_index != kInvalidIdx ) io::timerDestroy( p->ioH, p->midi_timer_index); for(unsigned i=0; imidiDevN; ++i) { mem::release(p->midiDevA[i].label); mem::release(p->midiDevA[i].midiOutDevLabel); mem::release(p->midiDevA[i].midiOutPortLabel); mem::release(p->midiDevA[i].velTableArray); } mem::release(p->midiDevA); mem::release(p->msgArray); mem::release(p->iMsgArray); mem::release(p); return rc; } unsigned _label_to_device_index( midi_record_play_t* p, const char* label ) { for(unsigned i=0; imidiDevN; ++i) if( textCompare(p->midiDevA[i].label,label) == 0 ) return i; return kInvalidIdx; } void _xmit_midi( midi_record_play_t* p, unsigned devIdx, uint8_t ch, uint8_t status, uint8_t d0, uint8_t d1 ) { if( !p->supressMidiXmitFl && p->midiDevA[devIdx].enableFl ) io::midiDeviceSend( p->ioH, p->midiDevA[devIdx].midiOutDevIdx, p->midiDevA[devIdx].midiOutPortIdx, status + ch, d0, d1 ); } // initialze the midi state to all-notes-off and all controllers=0 void _midi_state_clear( midi_record_play_t* p) { for(unsigned i=0; imidiDevN; ++i) { for(unsigned j=0; jmidiDevA[i].midi_state.keyVel[j] = 0; for(unsigned j=0; jmidiDevA[i].midi_state.ctlVal[j] = 0; p->midiDevA[i].midi_state.state = kPlayingMidiStateId; } } void _midi_state_set_state( midi_record_play_t* p, unsigned stateId ) { for(unsigned i=0; imidiDevN; ++i) p->midiDevA[i].midi_state.state = stateId; } bool _midi_state_is_state( midi_record_play_t* p, unsigned stateId ) { for(unsigned i=0; imidiDevN; ++i) if( p->midiDevA[i].midi_state.state != stateId ) return false; return true; } void _midi_state_set_stopping( midi_record_play_t* p ) { _midi_state_set_state(p,kStoppingMidiStateId); } bool _midi_state_is_stopping( midi_record_play_t* p ) { return _midi_state_is_state(p,kStoppingMidiStateId); } bool _midi_state_is_stopped( midi_record_play_t* p ) { return _midi_state_is_state(p,kStoppedMidiStateId); } unsigned _midi_state_active_note_count( midi_record_play_t* p ) { unsigned n = 0; for(unsigned i=0; imidiDevN; ++i) for(unsigned j=0; jmidiDevA[i].midi_state.keyVel[j] > 0 ? 1 : 0; return n; } rc_t _midi_state_update( midi_record_play_t* p, midi_device_t& dev, uint8_t ch, uint8_t status, uint8_t d0, uint8_t d1 ) { rc_t rc = kOkRC; if( ch != 0 ) { rc = cwLogError(kInvalidArgRC,"Only the state of MIDI channel 0 tracked."); goto errLabel; } if( d0 > 127 || d1 > 127 ) { rc = cwLogError(kInvalidArgRC,"Illegal MIDI byte value: st:%i d0:%i d1:%i",status,d0,d1); goto errLabel; } if( midi::isNoteOn(status,d1) ) { if( dev.midi_state.state == kPlayingMidiStateId ) dev.midi_state.keyVel[ d0 ] = d1; goto errLabel; } if( midi::isNoteOff(status,d1)) { dev.midi_state.keyVel[ d0 ] = 0; goto errLabel; } if( midi::isCtl(status) ) { dev.midi_state.ctlVal[ d0 ] = d1; goto errLabel; } errLabel: return rc; } bool _midi_state_are_all_notes_off( midi_record_play_t* p ) { if( _midi_state_is_stopped( p ) ) return true; if( _midi_state_is_stopping( p ) ) if( _midi_state_active_note_count( p ) == 0 ) { _midi_state_set_state(p,kStoppedMidiStateId); return true; } return false; } uint8_t _midi_state_ctl_value( midi_record_play_t* p, midi_device_t& dev, uint8_t d0 ) { return dev.midi_state.ctlVal[d0]; } void _midi_state_xmit_pedals( midi_record_play_t* p ) { uint8_t ctlA[] = { midi::kSustainCtlMdId, midi::kSostenutoCtlMdId, midi::kSoftPedalCtlMdId }; unsigned ctlN = sizeof(ctlA)/sizeof(ctlA[0]); for(unsigned i=0; imidiDevN; ++i) for(unsigned j=0; jmidiDevA[i], ctlA[j] ); _xmit_midi(p, i, 0, midi::kCtlMdId, ctlA[j], d1 ); } } void _set_chord_note_count( am_midi_msg_t* m0, const am_midi_msg_t* m, unsigned chordNoteCnt ) { for(; m0status,m0->d1) ) m0->chordNoteCnt = chordNoteCnt; } void _set_chord_note_count( midi_record_play_t* p ) { unsigned chordNoteCnt = 1; am_midi_msg_t* m0 = p->msgArray; am_midi_msg_t* m = nullptr; for(unsigned i=1; imsgArrayN; ++i) { m = p->msgArray + i; if( midi::isNoteOn(m->status,m->d1) ) { if( time::isEqual(m0->timestamp, m->timestamp) ) ++chordNoteCnt; else { _set_chord_note_count(m0,m,chordNoteCnt); chordNoteCnt=1; m0 = m; } } } _set_chord_note_count(m0,m,chordNoteCnt); } rc_t _parseCfg(midi_record_play_t* p, const object_t& cfg ) { rc_t rc = kOkRC; const object_t* midiDevL = nullptr; if((rc = cfg.getv( "max_midi_msg_count", p->msgArrayN, "midi_timer_period_micro_sec", p->midi_timer_period_micro_sec, "all_off_delay_ms", p->all_off_delay_ms, "midi_device_list", midiDevL, "log_in_flag", p->logInFl, "log_out_flag", p->logOutFl, "min_damper_down_time_ms", p->minDamperDownMs, "half_pedal_flag", p->halfPedalFl)) != kOkRC ) { rc = cwLogError(kSyntaxErrorRC,"MIDI record play configuration parse failed."); goto errLabel; } p->iMsgArrayN = p->msgArrayN; p->lastStoreIndex = kInvalidIdx; if( midiDevL->child_count() > 0 ) { p->midiDevN = midiDevL->child_count(); p->midiDevA = mem::allocZ(p->midiDevN); printf("Midi record play devices:%i\n",p->midiDevN); for(unsigned i=0; imidiDevN; ++i) { const object_t* ele = midiDevL->child_ele(i); const char* label = nullptr; const char* midiOutDevLabel = nullptr; const char* midiOutPortLabel = nullptr; const object_t* pedalRecd = nullptr; bool enableFl = false; if((rc = ele->getv( "label", label, "midi_out_device", midiOutDevLabel, "midi_out_port", midiOutPortLabel, "enableFl", enableFl)) != kOkRC ) { rc = cwLogError(kSyntaxErrorRC,"MIDI record play device list configuration parse failed."); goto errLabel; } if((rc = ele->getv_opt( "pedal", pedalRecd, "force_damper_down_fl",p->midiDevA[i].force_damper_down_fl, "force_damper_down_threshold",p->midiDevA[i].force_damper_down_threshold, "force_damper_down_velocity", p->midiDevA[i].force_damper_down_velocity, "damper_dead_band_enable_fl", p->midiDevA[i].damper_dead_band_enable_fl, "damper_dead_band_min_value", p->midiDevA[i].damper_dead_band_min_value, "damper_dead_band_max_value", p->midiDevA[i].damper_dead_band_max_value, "scale_chord_notes_enable_fl",p->midiDevA[i].scale_chord_notes_enable_fl, "scale_chord_notes_factor", p->midiDevA[i].scale_chord_notes_factor)) != kOkRC ) { rc = cwLogError(kSyntaxErrorRC,"MIDI record play device optional argument parsing failed."); goto errLabel; } cwLogInfo("Force Pedal: enabled:%i thresh:%i veloc:%i dead band: enable:%i min:%i max:%i", p->midiDevA[i].force_damper_down_fl, p->midiDevA[i].force_damper_down_threshold, p->midiDevA[i].force_damper_down_velocity, p->midiDevA[i].damper_dead_band_enable_fl, p->midiDevA[i].damper_dead_band_min_value, p->midiDevA[i].damper_dead_band_max_value ); p->midiDevA[i].label = mem::duplStr( label ); p->midiDevA[i].midiOutDevLabel = mem::duplStr( midiOutDevLabel); p->midiDevA[i].midiOutPortLabel = mem::duplStr( midiOutPortLabel); p->midiDevA[i].enableFl = enableFl; if( pedalRecd != nullptr ) { if((rc = pedalRecd->getv( "down_id", p->midiDevA[i].pedalDownVelId, "down_vel", p->midiDevA[i].pedalDownVel, "up_id", p->midiDevA[i].pedalUpVelId, "up_vel", p->midiDevA[i].pedalUpVel, "half_down_id", p->midiDevA[i].pedalDownHalfVelId, "half_down_vel", p->midiDevA[i].pedalDownHalfVel, "half_up_id", p->midiDevA[i].pedalUpHalfVelId, "half_up_vel", p->midiDevA[i].pedalUpHalfVel )) != kOkRC ) { rc = cwLogError(kSyntaxErrorRC,"An error occured while parsing the pedal record for MIDI device:'%s' port:'%s'.",midiOutDevLabel,midiOutPortLabel); goto errLabel; } else { p->midiDevA[i].pedalMapEnableFl = true; } } } } // allocate the MIDI msg buffer p->msgArray = mem::allocZ( p->msgArrayN ); p->iMsgArray = mem::allocZ( p->iMsgArrayN ); errLabel: return rc; } rc_t _stop( midi_record_play_t* p ); const am_midi_msg_t* _midi_store( midi_record_play_t* p, unsigned devIdx, unsigned portIdx, const time::spec_t& ts, uint8_t ch, uint8_t status, uint8_t d0, uint8_t d1 ) { am_midi_msg_t* am = nullptr; //if( !midi::isPedal(status,d0) ) // printf("MIDI store: %i : ch:%i st:%i d0:%i d1:%i\n",p->iMsgArrayInIdx,ch,status,d0,d1); p->lastStoreIndex = kInvalidIdx; // verify that space exists in the record buffer if( p->iMsgArrayInIdx < p->iMsgArrayN ) { // MAKE THIS ATOMIC unsigned id = p->iMsgArrayInIdx; p->lastStoreIndex = p->iMsgArrayInIdx; ++p->iMsgArrayInIdx; am = p->iMsgArray + id; am->id = id; am->devIdx = devIdx; am->portIdx = portIdx; am->timestamp = ts; am->ch = ch; am->status = status; am->d0 = d0; am->d1 = d1; } return am; } rc_t _event_callback( midi_record_play_t* p, unsigned id, const time::spec_t timestamp, unsigned loc, const void* arg, uint8_t ch, uint8_t status, uint8_t d0, uint8_t d1, unsigned chordNoteCnt, bool log_fl=true ) { rc_t rc = kOkRC; // if we have arrived at the stop time (but we may still be waiting for note-end messages) bool after_stop_time_fl = !time::isZero(p->end_play_event_timestamp) && time::isGT(timestamp,p->end_play_event_timestamp); if( after_stop_time_fl ) _midi_state_set_stopping(p); // if we are after the stop time and after the all-notes-off time // bool after_all_off_fl = after_stop_time_fl && time::isGT(timestamp,p->all_off_timestamp); // if we are after the stop time and there are no more active notes bool after_all_off_fl = after_stop_time_fl && _midi_state_are_all_notes_off(p); bool is_note_on_fl = midi::isNoteOn(status,d1); bool is_damper_fl = midi::isSustainPedal(status,d0); bool supress_fl = (is_note_on_fl && after_stop_time_fl) || p->muteFl; bool is_pedal_fl = midi::isPedal( status, d0 ); if( after_all_off_fl ) { rc = _stop(p); } else { if( p->halfPedalFl ) { if( status==midi::kCtlMdId && d0 == midi::kSustainCtlMdId && d1 != 0 ) d1 = p->halfPedalMidiPedalVel; } // for each midi device for(unsigned i=0; imidiDevN; ++i) if(p->midiDevA[i].enableFl ) { uint8_t out_d1 = d1; if( !p->halfPedalFl ) { // if this is a note-on and a vel. table exists if( is_note_on_fl and p->midiDevA[i].velTableArray != nullptr ) { // verify that the vel. is legal given the table if( d1 >= p->midiDevA[i].velTableN ) cwLogError(kInvalidIdRC,"A MIDI note-on velocity (%i) outside the velocity table (%i>%i) range was encountered on device:%s.",d1,(p->midiDevA[i].velTableN)-1,cwStringNullGuard(p->midiDevA[i].label)); else out_d1 = p->midiDevA[i].velTableArray[ d1 ]; // scale the note velocities of chords to account for their combined volumes if( p->midiDevA[i].scale_chord_notes_enable_fl && chordNoteCnt>1 ) { uint8_t delta = (uint8_t)lround(out_d1 * p->midiDevA[i].scale_chord_notes_factor * (chordNoteCnt-1) ); if( delta < out_d1 ) { //printf("%i %i %i %i\n",chordNoteCnt,delta,out_d1,out_d1-delta); out_d1 -= delta; } } } // store the note-on velocity histogram data if( p->velHistogramEnableFl && is_note_on_fl && out_d1 < midi::kMidiVelCnt ) p->midiDevA[i].velHistogram[ out_d1 ] += 1; // if the damper pedal velocity is in the dead band then don't send it if( p->midiDevA[i].damper_dead_band_enable_fl && is_pedal_fl && p->midiDevA[i].damper_dead_band_min_value <= out_d1 && out_d1 <= p->midiDevA[i].damper_dead_band_max_value ) out_d1 = 0; // if the damper pedal velocity is over the 'forcing' threshold then force the damper down if( p->midiDevA[i].force_damper_down_fl && is_damper_fl && out_d1>p->midiDevA[i].force_damper_down_threshold ) out_d1 = p->midiDevA[i].force_damper_down_velocity; // map the pedal velocity if( status==midi::kCtlMdId && d0 == midi::kSustainCtlMdId && p->midiDevA[i].pedalMapEnableFl ) { if( d1 == p->midiDevA[i].pedalUpVelId ) out_d1 = p->midiDevA[i].pedalUpVel; else if( d1 == p->midiDevA[i].pedalDownVelId ) out_d1 = p->midiDevA[i].pedalDownVel; else if( d1 == p->midiDevA[i].pedalDownHalfVelId ) out_d1 = p->midiDevA[i].pedalDownHalfVel; else { cwLogError(kInvalidIdRC,"Unexpected pedal down velocity (%i) during pedal velocity mapping. Remove the 'pedal' stanza from the MIDI device cfg to prevent pedal mapping.",d1); } } } _midi_state_update(p, p->midiDevA[i], ch, status, d0, out_d1 ); if( !supress_fl ) { _xmit_midi( p, i, ch, status, d0, out_d1 ); } } // don't inform the app if we are past the end time if( !after_stop_time_fl and p->cb ) p->cb( p->cb_arg, kMidiEventActionId, id, timestamp, loc, arg, ch, status, d0, d1 ); if( log_fl && p->logOutFl ) { // Note: The device of outgoing messages is set to p->midiDevN + 1 to distinguish it from // incoming messages. _midi_store( p, p->midiDevN, 0, timestamp, ch, status, d0, d1 ); } } return rc; } rc_t _transmit_msg( midi_record_play_t* p, const am_midi_msg_t* am, bool log_fl=true ) { return _event_callback( p, am->id, am->timestamp, am->loc, am->arg, am->ch, am->status, am->d0, am->d1, am->chordNoteCnt, log_fl ); } rc_t _transmit_note( midi_record_play_t* p, unsigned ch, unsigned pitch, unsigned vel, unsigned microsecs ) { time::spec_t ts = {}; time::microsecondsToSpec( ts, microsecs ); return _event_callback( p, kInvalidId, ts, kInvalidId, nullptr, ch, midi::kNoteOnMdId, pitch, vel, 0 ); } rc_t _transmit_ctl( midi_record_play_t* p, unsigned ch, unsigned ctlId, unsigned ctlVal, unsigned microsecs ) { time::spec_t ts = {}; time::microsecondsToSpec( ts, microsecs ); return _event_callback( p, kInvalidId, ts, kInvalidId, nullptr, ch, midi::kCtlMdId, ctlId, ctlVal, 0 ); } rc_t _transmit_pedal( midi_record_play_t* p, unsigned ch, unsigned pedalCtlId, bool pedalDownFl, unsigned microsecs ) { return _transmit_ctl( p, ch, pedalCtlId, pedalDownFl ? 127 : 0, microsecs); } void _half_pedal_update( midi_record_play_t* p, unsigned cur_time_us ) { if( cur_time_us >= p->halfPedalNextUs ) { unsigned midi_ch = 0; switch( p->halfPedalState ) { case kWaitForBegin: printf("down: %i %i\n",cur_time_us/1000,p->halfPedalMidiPedalVel); _transmit_ctl( p, midi_ch, midi::kSustainCtlMdId, p->halfPedalMidiPedalVel, cur_time_us); p->halfPedalState = kWaitForNoteOn; p->halfPedalNextUs += p->halfPedalNoteDelayUs; break; case kWaitForNoteOn: printf("note: %i\n",cur_time_us/1000); _transmit_note( p, midi_ch, p->halfPedalMidiPitch, p->halfPedalMidiNoteVel, cur_time_us ); p->halfPedalNextUs += p->halfPedalNoteDurUs; p->halfPedalState = kWaitForNoteOff; break; case kWaitForNoteOff: printf("off: %i\n",cur_time_us/1000); _transmit_note( p, midi_ch, p->halfPedalMidiPitch, 0, cur_time_us ); p->halfPedalNextUs += p->halfPedalUpDelayUs; p->halfPedalState = kWaitForPedalUp; break; case kWaitForPedalUp: printf("up: %i\n",cur_time_us/1000); _transmit_ctl( p, midi_ch, midi::kSustainCtlMdId, 0, cur_time_us); p->halfPedalNextUs += p->halfPedalDownDelayUs; p->halfPedalState = kWaitForPedalDown; break; case kWaitForPedalDown: //printf("down: %i\n",cur_time_us/1000); //_transmit_ctl( p, midi_ch, midi::kSustainCtlMdId, p->halfPedalMidiPedalVel, cur_time_us); //_stop(p); p->halfPedalState = kHalfPedalDone; break; case kHalfPedalDone: break; } } } // Set the next location to store an incoming MIDI message void _set_midi_msg_next_index( midi_record_play_t* p, unsigned next_idx ) { p->iMsgArrayInIdx = next_idx; if( next_idx == 0 ) p->lastStoreIndex = kInvalidIdx; } // Set the next index of the next MIDI message to transmit void _set_midi_msg_next_play_index(midi_record_play_t* p, unsigned next_idx) { p->msgArrayOutIdx = next_idx; } cw::rc_t _am_file_read_version_0( const char* fn, file::handle_t fH, am_midi_msg_t* amMsgArray, unsigned msgN ) { // version 0 record type typedef struct msg_str { unsigned dev_idx; unsigned port_idx; time::spec_t timestamp; uint8_t ch; uint8_t st; uint8_t d0; uint8_t d1; unsigned microsecs; } zero_msg_t; cw::rc_t rc = kOkRC; zero_msg_t* zMsgArray = mem::allocZ(msgN); unsigned fileByteN = msgN * sizeof(zero_msg_t); if((rc = file::read(fH,zMsgArray,fileByteN)) != kOkRC ) { rc = cwLogError(kReadFailRC,"Data read failed on Audio-MIDI file: '%s'.", fn ); goto errLabel; } for(unsigned i=0; idevIdx = zm->dev_idx; am->portIdx = zm->port_idx; am->microsec = zm->microsecs; am->id = i; am->timestamp = zm->timestamp; am->loc = i; am->ch = zm->ch; am->status = zm->st; am->d0 = zm->d0; am->d1 = zm->d1; } errLabel: mem::release(zMsgArray); return rc; } // Read the am_midi_msg_t records from a file written by _midi_write() // If msgArrayCntRef==0 and msgArrayRef==NULL then an array will be allocated and it is up // to the caller to release it, otherwise the msgArrayCntRef should be set to the count // of available records in msgArrayRef[]. Note the if there are more records in the file // than there are record in msgArrayRef[] then a warning will be issued and only // msgArrayCntRef records will be returned. cw::rc_t _am_file_read( const char* fn, unsigned& msgArrayCntRef, am_midi_msg_t*& msgArrayRef ) { rc_t rc = kOkRC; unsigned recordN = 0; // count of records in the ifle unsigned fileByteN = 0; // count of bytes in the file int version = 0; // version id (always a negative number) bool alloc_fl = false; bool print_fl = false; file::handle_t fH; if((rc = file::open(fH,fn,file::kReadFl)) != kOkRC ) { rc = cwLogError(kOpenFailRC,"Unable to locate the AM file: '%s'.", fn ); goto errLabel; } // read the first word - which is either a version id or the count of records in the file // if this is a version 0 file if((rc = file::read(fH,version)) != kOkRC ) { rc = cwLogError(kReadFailRC,"Version read failed on Audio-MIDI file: '%s'.", fn ); goto errLabel; } // if the version is greater than 0 then this is a version 0 file (and 'version' holds the record count. if( version > 0 ) { recordN = (unsigned)version; version = 0; } else // otherwise the second word in the file holds the size of the file { if((rc = file::read(fH,recordN)) != kOkRC ) { rc = cwLogError(kReadFailRC,"Header read failed on Audio-MIDI file: '%s'.", fn ); goto errLabel; } } // if the output msg array was not allocated - then allocate it here if( msgArrayCntRef == 0 || msgArrayRef == nullptr ) { alloc_fl = true; msgArrayRef = mem::allocZ(recordN); } else // if the msg array was allocated but is too small - then decrease the count of records to be read from the file { if( recordN > msgArrayCntRef ) { cwLogWarning("The count of message in Audio-MIDI file '%s' reduced from %i to %i.", fn, recordN, msgArrayCntRef ); recordN = msgArrayCntRef; } } if( version == 0 ) { // read the version 0 file into a temporary buffer then translate to am_midi_msg_t records if((rc = _am_file_read_version_0( fn, fH, msgArrayRef, recordN )) != kOkRC ) goto errLabel; } else { fileByteN = recordN * sizeof(am_midi_msg_t); if((rc = file::read(fH,msgArrayRef,fileByteN)) != kOkRC ) { rc = cwLogError(kReadFailRC,"Data read failed on Audio-MIDI file: '%s'.", fn ); goto errLabel; } } if( print_fl ) { for(unsigned i=0; i p->msgArrayN ) { cwLogWarning("The count of message in Audio-MIDI file '%s' reduced from %i to %i.", fn, n, p->msgArrayN ); n = p->msgArrayN; } if((rc = file::read(fH,p->msgArray,n*sizeof(am_midi_msg_t))) != kOkRC ) { rc = cwLogError(kReadFailRC,"Data read failed on Audio-MIDI file: '%s'.", fn ); goto errLabel; } p->msgArrayInIdx = n; cwLogInfo("Read %i from '%s'.",n,fn); errLabel: file::close(fH); return rc; } // Write the record buffer to an AM file rc_t _midi_write( midi_record_play_t* p, const char* fn ) { rc_t rc = kOkRC; file::handle_t fH; // NOTE: version must be a small negative number to differentiate from file version that // whose first word is the count of records in the file, rather than the version number int version = -1; if( p->iMsgArrayInIdx == 0 ) { cwLogWarning("Nothing to write."); return rc; } // open the file if((rc = file::open(fH,fn,file::kWriteFl)) != kOkRC ) { rc = cwLogError(kOpenFailRC,"Unable to create the file '%s'.",cwStringNullGuard(fn)); goto errLabel; } // write the file version if((rc = write(fH,version)) != kOkRC ) { rc = cwLogError(kWriteFailRC,"Version write to '%s' failed.",cwStringNullGuard(fn)); goto errLabel; } // write the file header if((rc = write(fH,p->iMsgArrayInIdx)) != kOkRC ) { rc = cwLogError(kWriteFailRC,"Header write to '%s' failed.",cwStringNullGuard(fn)); goto errLabel; } // write the file data if((rc = write(fH,p->iMsgArray,sizeof(am_midi_msg_t)*p->iMsgArrayInIdx)) != kOkRC ) { rc = cwLogError(kWriteFailRC,"Data write to '%s' failed.",cwStringNullGuard(fn)); goto errLabel; } errLabel: file::close(fH); cwLogInfo("Saved %i events to '%s'.", p->iMsgArrayInIdx, fn ); return rc; } rc_t _write_msgs_to_csv( const char* fn, const am_midi_msg_t* msgArray, unsigned msgArrayCnt ) { rc_t rc = kOkRC; file::handle_t fH; // open the file if((rc = file::open(fH,fn,file::kWriteFl)) != kOkRC ) { rc = cwLogError(kOpenFailRC,"Unable to create the file '%s'.",cwStringNullGuard(fn)); goto errLabel; } file::printf(fH,"dev,port,microsec,id,sec,ch,status,sci_pitch,d0,d1\n"); for(unsigned i=0; istatus == midi::kNoteOnMdId ) midi::midiToSciPitch( m->d0, sciPitch, midi::kMidiSciPitchCharCnt ); else strcpy(sciPitch,""); if((rc = file::printf(fH, "%3i,%3i,%8i,%3i,%8.4f,%2i,0x%2x,%5s,%3i,%3i\n", m->devIdx, m->portIdx, m->microsec, m->id, secs, m->ch, m->status, sciPitch, m->d0, m->d1 )) != kOkRC ) { rc = cwLogError(rc,"Write failed on line:%i", i+1 ); goto errLabel; } } errLabel: file::close(fH); return rc; } rc_t _write_csv( midi_record_play_t* p, const char* fn ) { rc_t rc = kOkRC; if( p->iMsgArrayInIdx == 0 ) { cwLogWarning("Nothing to write."); return rc; } if((rc = _write_msgs_to_csv( fn, p->iMsgArray, p->iMsgArrayInIdx )) != kOkRC ) goto errLabel; cwLogInfo("Saved %i events to '%s'.", p->iMsgArrayInIdx, fn ); errLabel: if(rc != kOkRC ) rc = cwLogError(rc,"Write to '%s' failed.", fn ); return rc; } rc_t _write_sync_csv( midi_record_play_t* p, const char* fn ) { rc_t rc = kOkRC; file::handle_t fH; if( p->iMsgArrayInIdx == 0 ) { cwLogWarning("Nothing to write."); return rc; } // open the file if((rc = file::open(fH,fn,file::kWriteFl)) != kOkRC ) { rc = cwLogError(kOpenFailRC,"Unable to create the file '%s'.",cwStringNullGuard(fn)); 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,pedal\n"); for(unsigned i=0; iiMsgArrayInIdx; ++i) { const am_midi_msg_t* m = p->iMsgArray + i; double secs = time::elapsedSecs( p->iMsgArray[0].timestamp, p->iMsgArray[i].timestamp ); // write the event line if( midi::isNoteOn(m->status,m->d1) ) { char sciPitch[ midi::kMidiSciPitchCharCnt + 1 ]; unsigned bar = 0; midi::midiToSciPitch( m->d0, sciPitch, midi::kMidiSciPitchCharCnt ); rc = file::printf(fH, "%i,%i,%i,%i,0,%f,0.0,0.0,0,%s,,,%i,%i,%i,,,,,\n", bar,i,1,m->loc,secs,sciPitch,m->status,m->d0,m->d1); } else { rc = file::printf(fH, ",%i,,,%i,%f,,,,,,,%i,%i,%i,,,,,\n",i,0,secs,m->status,m->d0,m->d1); } if( rc != kOkRC ) { rc = cwLogError(rc,"Write failed on line:%i", i+1 ); goto errLabel; } } errLabel: file::close(fH); cwLogInfo("Saved %i events to '%s'.", p->iMsgArrayInIdx, fn ); return rc; } rc_t _write_midi_meta_file(const char* meta_fn, const char* player_name, const char* take_label, unsigned sess_numb, unsigned take_numb, unsigned beg_loc, unsigned end_loc) { rc_t rc = kOkRC; const unsigned bufCharN = 256; char buf[ bufCharN+1 ]; int bN; if((bN = snprintf(buf,bufCharN,"{ player_name: \"%s\", take_label: \"%s\", session_number: %i, take_number: %i, beg_loc: %i, end_loc: %i, skip_score_follow_fl: false }",player_name,take_label,sess_numb,take_numb,beg_loc,end_loc)) == 0) { rc = cwLogError(kOpFailRC,"The meta data buffer formation failed."); goto errLabel; } if((rc = file::fnWrite(meta_fn,buf,bN)) != kOkRC ) goto errLabel; errLabel: if( rc != kOkRC ) rc = cwLogError(rc,"MIDI meta file write failed on '%s'.",cwStringNullGuard(meta_fn)); return rc; } rc_t _midi_file_write( const char* fn, const am_midi_msg_t* msgArray, unsigned msgArrayCnt ) { rc_t rc = kOkRC; const unsigned midiFileTrackCnt = 1; const unsigned midiFileTicksPerQN = 192; const unsigned midiFileTempoBpm = 120; const unsigned midiFileTrkIdx = 0; file::handle_t fH; midi::file::handle_t mfH; time::spec_t t0; if( msgArrayCnt == 0 ) { cwLogWarning("Nothing to write."); return rc; } if((rc = midi::file::create( mfH, midiFileTrackCnt, midiFileTicksPerQN )) != kOkRC ) { rc = cwLogError(rc,"MIDI file create failed. File:'%s'", cwStringNullGuard(fn)); goto errLabel; } if((rc = midi::file::insertTrackTempoMsg( mfH, midiFileTrkIdx, 0, midiFileTempoBpm )) != kOkRC ) { rc = cwLogError(rc,"MIDI file tempo message insert failed. File:'%s'", cwStringNullGuard(fn)); goto errLabel; } t0 = msgArray[0].timestamp; for(unsigned i=0; idevIdx, mm->portIdx, mm->microsec, mm->ch, mm->status, mm->d0, mm->d1, mm->chordNoteCnt ); } void _report_midi( midi_record_play_t* p ) { for(unsigned i=0; imidiDevN; ++i) { printf("vel table: %s:%s\n", cwStringNullGuard(p->midiDevA[i].midiOutDevLabel),cwStringNullGuard(p->midiDevA[i].midiOutPortLabel)); for(unsigned j=0; jmidiDevA[i].velTableN; ++j) printf("%i ",(unsigned)p->midiDevA[i].velTableArray[j]); printf("\n"); } printf("omsg cnt:%i\n",p->msgArrayInIdx); for(unsigned i=0; imsgArrayInIdx; ++i) { am_midi_msg_t* mm = p->msgArray + i; _print_midi_msg(mm); } printf("imsg cnt:%i\n",p->iMsgArrayInIdx); for(unsigned i=0; iiMsgArrayInIdx; ++i) { am_midi_msg_t* mm = p->iMsgArray + i; _print_midi_msg(mm); } } rc_t _write_vel_histogram( midi_record_play_t* p ) { const char* fname = "/home/kevin/temp/vel_histogram.txt"; rc_t rc = kOkRC; if( !p->velHistogramEnableFl ) return rc; file::handle_t h; if((rc = file::open(h,fname,file::kWriteFl)) != kOkRC ) return rc; for(unsigned i=0; imidiDevN; ++i) if(p->midiDevA[i].enableFl ) { for(unsigned j=0; jmidiDevA[i].velHistogram[j])) != kOkRC ) { rc = cwLogError(rc,"Histogram output file (%s) write failed.",fname); goto errLabel; } file::printf(h,"\n"); } errLabel: file::close(h); return rc; } // Fill the play buffer (msgArray) from the record buffer (iMsgArray) void _iMsgArray_to_msgArray(midi_record_play_t* p) { if( p->msgArrayN < p->iMsgArrayN) { mem::resize(p->msgArray,p->iMsgArrayN,mem::kZeroAllFl); p->msgArrayN = p->iMsgArrayN; } p->msgArrayOutIdx = 0; p->msgArrayInIdx = p->iMsgArrayInIdx; memcpy(p->msgArray,p->iMsgArray,p->iMsgArrayInIdx*sizeof(am_midi_msg_t)); } rc_t _stop( midi_record_play_t* p ) { rc_t rc = kOkRC; if( p->startedFl ) { p->startedFl = false; time::spec_t t1; time::get(t1); _write_vel_histogram( p ); // if we were recording if( p->recordFl ) { // set the 'microsec' value for each MIDI msg as an offset from the first message[] for(unsigned i=0; iiMsgArrayInIdx; ++i) { p->iMsgArray[i].microsec = time::elapsedMicros(p->iMsgArray[0].timestamp,p->iMsgArray[i].timestamp); } // copy the recorded messages from the input buffer to the output buffer _iMsgArray_to_msgArray(p); cwLogInfo("MIDI messages recorded: %i",p->msgArrayInIdx ); } else { io::timerStop( p->ioH, io::timerIdToIndex(p->ioH, kMidiRecordPlayTimerId) ); // TODO: // BUG BUG BUG: should work for all channels // all notes off _transmit_ctl( p, 0, 121, 0, 0 ); // reset all controllers _transmit_ctl( p, 0, 123, 0, 0 ); // all notes off _transmit_ctl( p, 0, 0, 0, 0 ); // switch to bank 0 } _midi_state_clear(p); if( p->cb != nullptr ) p->cb( p->cb_arg, kPlayerStoppedActionId, kInvalidId, t1, kInvalidId, nullptr, 0, 0, 0, 0 ); } return rc; } rc_t _midi_receive( midi_record_play_t* p, const io::midi_msg_t& m ) { rc_t rc = kOkRC; const midi::packet_t* pkt = m.pkt; // for each midi msg for(unsigned j=0; jmsgCnt; ++j) { // if this is a sys-ex msg if( pkt->msgArray == NULL ) { cwLogError(kNotImplementedRC,"Sys-ex recording not implemented."); } else // this is a triple { //if( !midi::isPedal(pkt->msgArray[j].status,pkt->msgArray[j].d0) ) // printf("IN: st:%i rec:%i : 0x%x 0x%x 0x%x\n", p->startedFl, p->recordFl, pkt->msgArray[j].status, pkt->msgArray[j].d0, pkt->msgArray[j].d1 ); if( (p->recordFl || p->logInFl) && p->startedFl ) { // verify that space exists in the record buffer if( p->iMsgArrayInIdx >= p->iMsgArrayN ) { _stop(p); rc = cwLogError(kBufTooSmallRC,"MIDI message record buffer is full. % messages.",p->iMsgArrayN); goto errLabel; } else { // copy the msg into the record buffer midi::msg_t* mm = pkt->msgArray + j; if( midi::isChStatus(mm->status) ) { //printf("R:%i %i %i\n",mm->status, mm->d0, mm->d1); const am_midi_msg_t* am = _midi_store( p, pkt->devIdx, pkt->portIdx, mm->timeStamp, mm->ch, mm->status, mm->d0, mm->d1 ); if( p->thruFl && am != nullptr ) _transmit_msg( p, am, false ); } } } } } errLabel: return rc; } rc_t _timer_callback(midi_record_play_t* p, io::timer_msg_t& m) { rc_t rc = kOkRC; // if the MIDI player is started and in 'play' mode and msg remain to be played if( p->startedFl && (p->recordFl==false) && (p->msgArrayOutIdx < p->msgArrayInIdx)) { time::spec_t t; time::get(t); unsigned cur_time_us = time::elapsedMicros(p->play_time,t); if( p->halfPedalFl ) _half_pedal_update( p, cur_time_us ); else while( p->msgArray[ p->msgArrayOutIdx ].microsec <= cur_time_us ) { am_midi_msg_t* mm = p->msgArray + p->msgArrayOutIdx; //_print_midi_msg(mm); _transmit_msg( p, mm ); _set_midi_msg_next_play_index(p, p->msgArrayOutIdx+1 ); // if all MIDI messages have been played if( p->msgArrayOutIdx >= p->msgArrayInIdx ) { _stop(p); break; } } if( p->msgArrayOutIdx < p->msgArrayInIdx ) { // the next callback should happen at now() + (next_msg_us - cur_us) time::advanceMicros(t, p->msgArray[ p->msgArrayOutIdx ].microsec - cur_time_us ); io::timerSetNextTime( p->ioH, p->midi_timer_index,t); } } return rc; } rc_t _get_take_number( const char* dir, unsigned& take_numb_ref ) { rc_t rc = kOkRC; take_numb_ref = -1; const char* suffix; if((suffix = lastMatchChar(dir,'_')) == nullptr ) { rc = kSyntaxErrorRC; goto errLabel; } if(sscanf(suffix+1,"%i",&take_numb_ref) != 1) { rc = kSyntaxErrorRC; goto errLabel; } errLabel: if( rc != kOkRC ) rc = cwLogError(rc,"The suffix of the directory name containing the 'am' file is not an underscore separated integer. (%s).",cwStringNullGuard(dir)); return rc; } } } cw::rc_t cw::midi_record_play::create( handle_t& hRef, io::handle_t ioH, const object_t& cfg, const char* velTableFname, event_callback_t cb, void* cb_arg ) { bool asyncFl = true; midi_record_play_t* p = nullptr; rc_t rc; object_t* velTblCfg = nullptr; if((rc = destroy(hRef)) != kOkRC ) return rc; p = mem::allocZ(); p->ioH = ioH; // p->ioH is used in _destory() so initialize it here // parse the cfg if((rc = _parseCfg(p,cfg)) != kOkRC ) goto errLabel; p->midi_timer_index = kInvalidIdx; p->cb = cb; p->cb_arg = cb_arg; p->halfPedalState = kHalfPedalDone; p->halfPedalNextUs = 0; p->halfPedalNoteDelayUs = 100 * 1000; p->halfPedalNoteDurUs = 1000 * 1000; p->halfPedalUpDelayUs = 1000 * 1000; p->halfPedalDownDelayUs = 1000 * 1000; p->halfPedalMidiPitch = 64; p->halfPedalMidiNoteVel = 64; p->halfPedalMidiPedalVel = 127; p->velHistogramEnableFl = true; for( unsigned i=0; imidiDevN; ++i) { midi_device_t* dev = p->midiDevA + i; if( !p->midiDevA[i].enableFl ) continue; if((dev->midiOutDevIdx = io::midiDeviceIndex(p->ioH,dev->midiOutDevLabel)) == kInvalidIdx ) { rc = cwLogError(kInvalidArgRC,"The MIDI output device: '%s' was not found.", cwStringNullGuard(dev->midiOutDevLabel) ); goto errLabel; } if((dev->midiOutPortIdx = io::midiDevicePortIndex(p->ioH,dev->midiOutDevIdx,false,dev->midiOutPortLabel)) == kInvalidIdx ) { rc = cwLogError(kInvalidArgRC,"The MIDI output port: '%s' was not found.", cwStringNullGuard(dev->midiOutPortLabel) ); goto errLabel; } printf("%s %s : %i %i\n",dev->midiOutDevLabel, dev->midiOutPortLabel, dev->midiOutDevIdx, dev->midiOutPortIdx ); } // create the MIDI playback timer if((rc = timerCreate( p->ioH, TIMER_LABEL, kMidiRecordPlayTimerId, p->midi_timer_period_micro_sec, asyncFl)) != kOkRC ) { cwLogError(rc,"Audio-MIDI timer create failed."); goto errLabel; } if((p->midi_timer_index = io::timerLabelToIndex( p->ioH, TIMER_LABEL )) == kInvalidIdx ) { cwLogError(rc,"Audio-MIDI timer index access failed."); goto errLabel; } _midi_state_clear(p); errLabel: if( rc == kOkRC ) hRef.set(p); else _destroy(p); if(velTblCfg != nullptr ) velTblCfg->free(); return rc; } cw::rc_t cw::midi_record_play::destroy( handle_t& hRef ) { rc_t rc = kOkRC; if( !hRef.isValid() ) return kOkRC; midi_record_play_t* p = _handleToPtr(hRef); if((rc = _destroy(p)) != kOkRC ) return rc; hRef.clear(); return rc; } cw::rc_t cw::midi_record_play::start( handle_t h, bool rewindFl, const time::spec_t* end_play_event_timestamp ) { midi_record_play_t* p = _handleToPtr(h); p->startedFl = true; if( p->velHistogramEnableFl ) for(unsigned i=0; imidiDevN; ++i) if(p->midiDevA[i].enableFl ) memset(p->midiDevA[i].velHistogram,0,sizeof(p->midiDevA[i].velHistogram)); // set the end play time if( end_play_event_timestamp == nullptr or time::isZero(*end_play_event_timestamp) ) time::setZero(p->end_play_event_timestamp); else { p->end_play_event_timestamp = *end_play_event_timestamp; //p->all_off_timestamp = *end_play_event_timestamp; //time::advanceMs( p->all_off_timestamp, p->all_off_delay_ms); } } time::get(p->start_time); if( p->recordFl || p->logInFl or p->logOutFl ) { _set_midi_msg_next_index(p, 0 ); } if( !p->recordFl ) { time::get(p->play_time); if( rewindFl ) _set_midi_msg_next_play_index(p,0); else { io::timerSetNextTime( p->ioH, p->midi_timer_index,p->play_time); // Set the begin play time back by the time offset of the current output event. // This will cause that event to be played back immediately. time::subtractMicros(p->play_time, p->msgArray[ p->msgArrayOutIdx ].microsec ); } if( p->halfPedalFl ) { p->halfPedalNextUs = 0; p->halfPedalState = kWaitForBegin; } io::timerStart( p->ioH, io::timerIdToIndex(p->ioH, kMidiRecordPlayTimerId) ); } return kOkRC; } cw::rc_t cw::midi_record_play::stop( handle_t h ) { midi_record_play_t* p = _handleToPtr(h); return _stop(p); } bool cw::midi_record_play::is_started( handle_t h ) { midi_record_play_t* p = _handleToPtr(h); return p->startedFl; } cw::rc_t cw::midi_record_play::clear( handle_t h ) { rc_t rc = kOkRC; midi_record_play_t* p = _handleToPtr(h); _set_midi_msg_next_index(p,0); return rc; } cw::rc_t cw::midi_record_play::set_record_state( handle_t h, bool record_fl ) { rc_t rc = kOkRC; midi_record_play_t* p = _handleToPtr(h); p->recordFl = record_fl; return rc; } bool cw::midi_record_play::record_state( handle_t h ) { midi_record_play_t* p = _handleToPtr(h); return p->recordFl; } cw::rc_t cw::midi_record_play::set_mute_state( handle_t h, bool mute_fl ) { rc_t rc = kOkRC; midi_record_play_t* p = _handleToPtr(h); p->muteFl = mute_fl; return rc; } bool cw::midi_record_play::mute_state( handle_t h ) { midi_record_play_t* p = _handleToPtr(h); return p->muteFl; } cw::rc_t cw::midi_record_play::set_thru_state( handle_t h, bool thru_fl ) { rc_t rc = kOkRC; midi_record_play_t* p = _handleToPtr(h); p->thruFl = thru_fl; return rc; } bool cw::midi_record_play::thru_state( handle_t h ) { midi_record_play_t* p = _handleToPtr(h); return p->thruFl; } cw::rc_t cw::midi_record_play::save( handle_t h, const char* fn ) { midi_record_play_t* p = _handleToPtr(h); return _midi_write(p,fn); } cw::rc_t cw::midi_record_play::save_csv( handle_t h, const char* fn ) { midi_record_play_t* p = _handleToPtr(h); return _write_csv(p,fn); } cw::rc_t cw::midi_record_play::save_synced_csv( handle_t h, const char* fn, const score_follower::ssf_note_on_t* syncA, unsigned syncN ) { rc_t rc = kOkRC; midi_record_play_t* p = _handleToPtr(h); for(unsigned i=0; iiMsgArrayInIdx; ++i) { const am_midi_msg_t* m = p->iMsgArray + i; if( midi::isNoteOn(m->status,m->d1) ) { for(unsigned j=0; jiMsgArray[i].id ) { if( p->iMsgArray[i].d0 != syncA[j].pitch ) { rc = cwLogError(kInvalidStateRC,"MIDI sync failed. Pitch mismatch. i:%i j:%i : %i %i",i,j, p->iMsgArray[i].d0 != syncA[j].pitch); //goto errLabel; continue; } p->iMsgArray[i].loc = syncA[j].loc; } } } if((rc = _write_sync_csv( p, fn )) != kOkRC ) { rc = cwLogError(rc,"MIDI sync'd CSV write failed."); goto errLabel; } errLabel: return rc; } cw::rc_t cw::midi_record_play::write_svg( handle_t h, const char* fn ) { rc_t rc; svg_midi::handle_t svgH; midi_record_play_t* p = _handleToPtr(h); if((rc = svg_midi::create( svgH )) != kOkRC ) { rc = cwLogError(rc,"svg_midi object create failed."); goto errLabel; } for(unsigned i=0; iiMsgArrayInIdx; ++i) { const am_midi_msg_t* m = p->iMsgArray + i; double sec = time::specToSeconds( m->timestamp ); if((rc = svg_midi::setMidiMsg(svgH,sec,m->id,m->ch,m->status,m->d0,m->d1)) != kOkRC ) { rc = cwLogError(rc,"svg_midi set midi msg failed."); goto errLabel; } } errLabel: svg_midi::destroy(svgH); return rc; } cw::rc_t cw::midi_record_play::open( handle_t h, const char* fn ) { midi_record_play_t* p = _handleToPtr(h); return _midi_read(p,fn); } cw::rc_t cw::midi_record_play::load( handle_t h, const midi_msg_t* msg, unsigned msg_count ) { rc_t rc = kOkRC; midi_record_play_t* p = _handleToPtr(h); unsigned extraMs = 0; unsigned curMicros = 0; if( msg_count > p->msgArrayN ) { mem::release(p->msgArray); p->msgArray = mem::allocZ( msg_count ); p->msgArrayN = msg_count; } for(unsigned i=0; i 0) { unsigned deltaMicros = time::elapsedMicros(msg[i-1].timestamp,msg[i].timestamp); curMicros += deltaMicros + (extraMs*1000); } p->msgArray[i].id = msg[i].id; p->msgArray[i].timestamp = msg[i].timestamp; p->msgArray[i].loc = msg[i].loc; p->msgArray[i].arg = msg[i].arg; p->msgArray[i].ch = msg[i].ch; p->msgArray[i].status = msg[i].status; p->msgArray[i].d0 = msg[i].d0; p->msgArray[i].d1 = msg[i].d1; p->msgArray[i].devIdx = kInvalidIdx; p->msgArray[i].portIdx = kInvalidIdx; //p->msgArray[i].microsec = time::elapsedMicros(p->msgArray[0].timestamp,p->msgArray[i].timestamp); p->msgArray[i].microsec = curMicros; //hprintf("%i %i\n",curMicros,time::elapsedMicros(p->msgArray[0].timestamp,p->msgArray[i].timestamp) ); extraMs = 0; if( midi::isSustainPedalUp(msg[i].status,msg[i].d0,msg[i].d1)) extraMs = p->minDamperDownMs; } p->msgArrayInIdx = msg_count; p->msgArrayOutIdx = 0; _set_chord_note_count(p); return rc; } unsigned cw::midi_record_play::label_to_device_index( handle_t h, const char* devLabel ) { midi_record_play_t* p = _handleToPtr(h); return _label_to_device_index( p, devLabel ); } const char* cw::midi_record_play::device_index_to_label( handle_t h, unsigned devIdx ) { midi_record_play_t* p = _handleToPtr(h); return devIdx < p->midiDevN ? p->midiDevA[devIdx].label : nullptr; } cw::rc_t cw::midi_record_play::seek( handle_t h, time::spec_t seek_timestamp ) { rc_t rc = kOkRC; midi_record_play_t* p = _handleToPtr(h); // clear the MIDI state _midi_state_clear(p); // supress MIDI transmission during the seek p->supressMidiXmitFl = true; // iterate throught the MIDI msg array for(unsigned i=0; imsgArrayInIdx; ++i) { am_midi_msg_t* mm = p->msgArray + i; // if this msg is at or after the seek target if( time::isLTE(seek_timestamp,mm->timestamp) ) { p->msgArrayOutIdx = i; break; } // transmit the msg so that the dev._midi_state is updated, // but 'supressMidiXmitFl' is set so no MIDI will actually // be transmitted _transmit_msg( p, mm, false ); } p->supressMidiXmitFl = false; _midi_state_xmit_pedals(p); return rc; } unsigned cw::midi_record_play::elapsed_micros( handle_t h ) { midi_record_play_t* p = _handleToPtr(h); time::spec_t t; time::get(t); return time::elapsedMicros( p->start_time, t ); } unsigned cw::midi_record_play::event_count( handle_t h ) { midi_record_play_t* p = _handleToPtr(h); return p->msgArrayInIdx; } unsigned cw::midi_record_play::event_index( handle_t h ) { midi_record_play_t* p = _handleToPtr(h); return p->recordFl ? p->iMsgArrayInIdx : p->msgArrayOutIdx; } unsigned cw::midi_record_play::last_store_index( handle_t h ) { midi_record_play_t* p = _handleToPtr(h); return p->recordFl ? p->lastStoreIndex : kInvalidIdx; } unsigned cw::midi_record_play::event_loc( handle_t h ) { midi_record_play_t* p = _handleToPtr(h); if( !p->recordFl && p->msgArrayOutIdx < p->msgArrayN ) return p->msgArray[ p->msgArrayOutIdx ].loc; return kInvalidId; } cw::rc_t cw::midi_record_play::exec( handle_t h, const io::msg_t& m ) { rc_t rc = kOkRC; midi_record_play_t* p = _handleToPtr(h); switch( m.tid ) { case io::kTimerTId: if( m.u.timer != nullptr ) rc = _timer_callback(p,*m.u.timer); break; case io::kMidiTId: if( m.u.midi != nullptr ) _midi_receive(p,*m.u.midi); break; default: rc = kOkRC; } return rc; } unsigned cw::midi_record_play::device_count( handle_t h ) { midi_record_play_t* p = _handleToPtr(h); return p->midiDevN; } bool cw::midi_record_play::is_device_enabled( handle_t h, unsigned devIdx ) { midi_record_play_t* p = _handleToPtr(h); bool fl = false; if( devIdx >= p->midiDevN ) cwLogError(kInvalidArgRC,"The MIDI record-play device index '%i' is invalid.",devIdx ); else fl = p->midiDevA[devIdx].enableFl; return fl; } void cw::midi_record_play::enable_device( handle_t h, unsigned devIdx, bool enableFl ) { midi_record_play_t* p = _handleToPtr(h); if( devIdx >= p->midiDevN ) cwLogError(kInvalidArgRC,"The MIDI record-play device index '%i' is invalid.",devIdx ); else { p->midiDevA[devIdx].enableFl = enableFl; //printf("Enable: %i = %i\n",devIdx,enableFl); } } cw::rc_t cw::midi_record_play::send_midi_msg( handle_t h, unsigned devIdx, uint8_t ch, uint8_t status, uint8_t d0, uint8_t d1 ) { midi_record_play_t* p = _handleToPtr(h); if( devIdx >= p->midiDevN ) return cwLogError(kInvalidArgRC,"The MIDI record-play device index '%i' is invalid.",devIdx ); if( p->midiDevA[devIdx].enableFl ) io::midiDeviceSend( p->ioH, p->midiDevA[devIdx].midiOutDevIdx, p->midiDevA[devIdx].midiOutPortIdx, status + ch, d0, d1 ); return kOkRC; } void cw::midi_record_play::half_pedal_params( handle_t h, unsigned noteDelayMs, unsigned pitch, unsigned vel, unsigned pedal_vel, unsigned noteDurMs, unsigned downDelayMs ) { midi_record_play_t* p = _handleToPtr(h); p->halfPedalNoteDelayUs = noteDelayMs * 1000; p->halfPedalNoteDurUs = noteDurMs * 1000; p->halfPedalDownDelayUs = downDelayMs * 1000; p->halfPedalMidiPitch = pitch; p->halfPedalMidiNoteVel = vel; p->halfPedalMidiPedalVel= pedal_vel; } cw::rc_t cw::midi_record_play::am_to_midi_file( const char* am_filename, const char* midi_filename, const char* csv_filename ) { rc_t rc = kOkRC; unsigned msgArrayCnt = 0; am_midi_msg_t* msgArray = nullptr; if(!filesys::isFile(am_filename)) { cwLogError(kOpenFailRC,"The AM file '%s' does not exist.",am_filename); goto errLabel; } if((rc = _am_file_read( am_filename, msgArrayCnt, msgArray )) != kOkRC ) { rc = cwLogError(rc,"Unable to read AM file '%s'.", cwStringNullGuard(am_filename)); goto errLabel; } if( midi_filename != nullptr ) if((rc = _midi_file_write( midi_filename, msgArray, msgArrayCnt )) != kOkRC ) { rc = cwLogError(rc,"Unable to write AM file '%s' to '%s'.", cwStringNullGuard(am_filename),cwStringNullGuard(midi_filename)); goto errLabel; } if( csv_filename != nullptr ) if((rc = _write_msgs_to_csv( csv_filename, msgArray, msgArrayCnt )) != kOkRC ) { rc = cwLogError(rc,"Unable to write AM file '%s' to '%s'.", cwStringNullGuard(am_filename),cwStringNullGuard(csv_filename)); goto errLabel; } errLabel: mem::release(msgArray); return rc; } cw::rc_t cw::midi_record_play::am_to_midi_dir( const char* player_name, const char* prefix_label, unsigned sess_numb, unsigned beg_loc, unsigned end_loc, const char* inDir ) { rc_t rc = kOkRC; filesys::dirEntry_t* dirEntryArray = nullptr; unsigned dirEntryCnt = 0; char* take_label = nullptr; if( !filesys::isDir(inDir) ) { rc = cwLogError(kInvalidArgRC,"The directory '%s' does not exist.",cwStringNullGuard(inDir)); goto errLabel; } if(( dirEntryArray = filesys::dirEntries( inDir, filesys::kDirFsFl | filesys::kFullPathFsFl, &dirEntryCnt )) == nullptr ) goto errLabel; for(unsigned i=0; iis_list() == false) { rc = cwLogError(kInvalidArgRC,"AM to MIDI file: invalid cfg."); goto errLabel; } // for each job specified in the list for(unsigned i=0; ichild_count(); ++i) { const object_t* job_cfg = nullptr; bool enable_fl = false; const char* prefix_label = nullptr; const char* player_name = nullptr; unsigned sess_numb = 0; unsigned beg_loc = kInvalidId; unsigned end_loc = kInvalidId; const char* dir = nullptr; // verify that each job spec is a dict if(( job_cfg = cfg->child_ele(i)) == nullptr || job_cfg->is_dict() == false ) { rc = cwLogError(kSyntaxErrorRC,"The AM to MIDI job spec. at index %i is invalid.",i); goto errLabel; } // read the job spec if((rc = job_cfg->getv("enable_fl", enable_fl, "player_name", player_name, "prefix_label", prefix_label, "sess_numb", sess_numb, "beg_loc", beg_loc, "end_loc", end_loc, "dir", dir)) != kOkRC ) { rc = cwLogError(rc,"AM to MIDI file: Unable to parse input arg's at index %i",i); goto errLabel; } if( enable_fl ) { if((rc = am_to_midi_dir(player_name,prefix_label,sess_numb,beg_loc,end_loc,dir)) != kOkRC ) { rc = cwLogError(rc,"AM to MIDI file conversion on directory:'%s' failed.", cwStringNullGuard(dir)); goto errLabel; } } } errLabel: if( rc != kOkRC ) rc = cwLogError(rc,"AM to MIDI file conversion failed."); return rc; } void cw::midi_record_play::report( handle_t h ) { midi_record_play_t* p = _handleToPtr(h); _report_midi(p); } unsigned cw::midi_record_play::dev_count( handle_t h ) { midi_record_play_t* p = _handleToPtr(h); return p->midiDevN; } cw::rc_t cw::midi_record_play::vel_table_disable( handle_t h, unsigned devIdx ) { rc_t rc = kOkRC; midi_record_play_t* p = _handleToPtr(h); unsigned begDevIdx = devIdx==kInvalidIdx ? 0 : devIdx; unsigned endDevIdx = devIdx==kInvalidIdx ? p->midiDevN : begDevIdx+1; if( devIdx != kInvalidIdx && devIdx > p->midiDevN ) { rc = cwLogError(kInvalidArgRC,"The device index '%i'is invalid.",devIdx); goto errLabel; } for(unsigned i=begDevIdx; imidiDevA[i].velTableArray); p->midiDevA[i].velTableN = 0; } errLabel: return rc; } unsigned cw::midi_record_play::vel_table_count( handle_t h, unsigned devIdx ) { midi_record_play_t* p = _handleToPtr(h); return p->midiDevA[ devIdx ].velTableN; } const uint8_t* cw::midi_record_play::vel_table( handle_t h, unsigned devIdx ) { midi_record_play_t* p = _handleToPtr(h); return p->midiDevA[ devIdx ].velTableArray; } cw::rc_t cw::midi_record_play::vel_table_set( handle_t h, unsigned devIdx, const uint8_t* tbl, unsigned tblN ) { midi_record_play_t* p = _handleToPtr(h); midi_device_t* dev = p->midiDevA + devIdx; if( tblN != dev->velTableN ) { dev->velTableArray = mem::resize(dev->velTableArray,tblN); dev->velTableN = tblN; } printf("Apply Vel Table to %s : ", cwStringNullGuard(p->midiDevA[devIdx].label)); for(unsigned i=0; ivelTableArray[i] = tbl[i]; printf("%i->%i ",i,tbl[i]); } printf("\n"); return kOkRC; }