//| 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 "cwTime.h" #include "cwFile.h" #include "cwObject.h" #include "cwFileSys.h" #include "cwText.h" #include "cwTextBuf.h" #include "cwMidi.h" #include "cwMidiDecls.h" #include "cwMidiFile.h" #include "cwMidiDevice.h" #include #include "cwMidiFileDev.h" namespace cw { namespace midi { namespace device { namespace file_dev { typedef struct file_msg_str { unsigned long long amicro; // msg_t* msg; // msg_t as declared in cwMidiDecls.h unsigned file_idx; unsigned msg_idx; } file_msg_t; typedef struct file_str { char* label; char* fname; msg_t* msgA; unsigned msgN; bool enable_fl; } file_t; typedef struct file_dev_str { cbFunc_t cbFunc; void* cbArg; file_t* fileA; // fileA[ fileN ] unsigned fileN; // file_msg_t* msgA; // msgA[ msgN ] unsigned msgAllocN; // unsigned msgN; unsigned devCnt; // always 1 unsigned base_dev_idx; char* dev_name; bool is_activeFl; // The following indexes are all int p->msgA[ p->msgN ] unsigned beg_msg_idx; // beg_msg_idx is the first msg to transmit unsigned end_msg_idx; // end_msg_idx indicates the last msg to transmit, end_msg_idx+1 will not be transmitted unsigned next_wr_msg_idx; // next_wr_msg_idx is the next msg to transmit unsigned next_rd_msg_idx; // next_rd_msg_idx is the last msg transmitted unsigned long long start_delay_micros; unsigned long long read_ahead_micros; bool latency_meas_enable_in_fl; bool latency_meas_enable_out_fl; latency_meas_result_t latency_meas_result; } file_dev_t; file_dev_t * _handleToPtr(handle_t h) { return handleToPtr(h); } rc_t _validate_file_index(file_dev_t* p, unsigned file_idx) { rc_t rc = kOkRC; if( file_idx >= p->fileN ) rc = cwLogError(kInvalidArgRC,"The MIDI device file/port index %i is invalid.",file_idx); return rc; } bool _file_exists( const file_t& r ) { return r.msgN > 0; } rc_t _validate_file_existence(file_dev_t* p, unsigned file_idx ) { rc_t rc; if((rc = _validate_file_index(p,file_idx)) == kOkRC ) if( !_file_exists(p->fileA[file_idx]) ) rc = cwLogError(kInvalidArgRC,"The MIDI device file at file/port index %i does not exist.",file_idx); return rc; } rc_t _validate_dev_index(file_dev_t* p, unsigned dev_idx ) { rc_t rc = kOkRC; if( dev_idx >= p->devCnt ) rc = cwLogError(kInvalidArgRC,"The MIDI file device index %i is invalid.",dev_idx ); return rc; } rc_t _validate_port_index(file_dev_t* p, unsigned port_idx ) { return _validate_file_index(p,port_idx); } void _reset_indexes( file_dev_t* p ) { p->beg_msg_idx = kInvalidIdx; p->end_msg_idx = kInvalidIdx; p->next_wr_msg_idx = kInvalidIdx; p->next_rd_msg_idx = kInvalidIdx; } rc_t _close_file( file_dev_t* p, unsigned file_idx ) { rc_t rc = kOkRC; if((rc = _validate_file_index(p,file_idx)) != kOkRC ) goto errLabel; if( _file_exists(p->fileA[file_idx] ) ) { // if the beg/end msg index refers to the file being closed then invalidate the beg/end msg idx if( p->beg_msg_idx != kInvalidIdx && p->beg_msg_idx < p->msgN && p->msgA[ p->beg_msg_idx ].file_idx == file_idx ) p->beg_msg_idx = kInvalidIdx; if( p->end_msg_idx != kInvalidIdx && p->end_msg_idx < p->msgN && p->msgA[ p->end_msg_idx ].file_idx == file_idx ) p->end_msg_idx = kInvalidIdx; mem::release(p->fileA[file_idx].fname); p->fileA[file_idx].msgN = 0; } errLabel: return rc; } rc_t _destroy( file_dev_t* p ) { rc_t rc = kOkRC; for(unsigned i=0; ifileN; ++i) { mem::release(p->fileA[i].msgA); mem::release(p->fileA[i].label); mem::release(p->fileA[i].fname); } mem::release(p->fileA); mem::release(p->msgA); mem::release(p->dev_name); mem::release(p); //errLabel: return rc; } rc_t _open_midi_file( file_dev_t* p, unsigned file_idx, const char* fname ) { rc_t rc = kOkRC; midi::file::handle_t mfH; if((rc = open(mfH, fname)) != kOkRC ) { rc = cwLogError(rc,"MIDI file open failed on '%s'.",cwStringNullGuard(fname)); goto errLabel; } else { unsigned msg_idx = 0; unsigned msgN = msgCount(mfH);; const file::trackMsg_t** fileMsgPtrA = msgArray(mfH); p->fileA[file_idx].msgA = mem::resizeZ(p->fileA[file_idx].msgA,msgN); for(unsigned j=0; jstatus) ) { msg_t* m = p->fileA[file_idx].msgA + msg_idx; m->uid = j; m->ch = fileMsgPtrA[j]->u.chMsgPtr->ch; m->status = fileMsgPtrA[j]->status; m->d0 = fileMsgPtrA[j]->u.chMsgPtr->d0; m->d1 = fileMsgPtrA[j]->u.chMsgPtr->d1; m->timeStamp = time::microsecondsToSpec(fileMsgPtrA[j]->amicro); msg_idx += 1; } p->fileA[file_idx].msgN = msg_idx; p->fileA[file_idx].fname = mem::duplStr(fname); close( mfH ); } errLabel: return rc; } unsigned _calc_msg_count( file_dev_t* p ) { unsigned msgAllocN = 0; for(unsigned i=0; ifileN; ++i) if( p->fileA[i].msgA != nullptr ) msgAllocN += p->fileA[i].msgN; return msgAllocN; } // Set msg_idx to kInvalidIdx to seek to the current value of p->beg_msg_idx // or 0 if p->beg_msg_idx was never set. // If msg_idx is a valid msg index then it will be assigned to p->beg_msg_idx rc_t _seek_to_msg_index( file_dev_t* p, unsigned file_idx, unsigned msg_idx ) { rc_t rc; unsigned i = 0; unsigned beg_msg_idx = p->beg_msg_idx; if((rc = _validate_file_existence(p,file_idx)) != kOkRC ) goto errLabel; // if no target msg was given ... if( msg_idx == kInvalidIdx ) { // ... then use the previous target msg or 0 beg_msg_idx = p->beg_msg_idx == kInvalidIdx ? 0 : p->beg_msg_idx; } else // if a target msg was given .. { // locate the msg in p->msgA[] for(i=0; imsgN; ++i) if( p->msgA[i].file_idx == file_idx && p->msgA[i].msg_idx == msg_idx ) { p->beg_msg_idx = i; beg_msg_idx = i; break; } if( i == p->msgN ) { rc = cwLogError(kEleNotFoundRC,"The 'begin' MIDI file event at index %i in %s was not found.",msg_idx,cwStringNullGuard(p->fileA[file_idx].label)); goto errLabel; } } p->next_wr_msg_idx = beg_msg_idx; p->next_rd_msg_idx = beg_msg_idx; errLabel: return rc; } // Set file_idx and msg_idx to kInvalidIdx to make the p->msgN the end index. // Set msg_idx to kInvalidIdx to make the last p->fileA[file_idx].msgN the end index. rc_t _set_end_msg_index( file_dev_t* p, unsigned file_idx, unsigned msg_idx ) { rc_t rc = kOkRC; unsigned i = 0; if( file_idx == kInvalidIdx ) p->end_msg_idx = p->msgN - 1; else { if((rc = _validate_file_existence(p,file_idx)) != kOkRC ) goto errLabel; if( msg_idx == kInvalidIdx ) msg_idx = p->fileA[ file_idx ].msgN - 1; // locate the msg in p->msgA[] for(i=0; imsgN; ++i) if( p->msgA[i].file_idx == file_idx && p->msgA[i].msg_idx == msg_idx ) { p->end_msg_idx = i; break; } if( i == p->msgN ) { rc = cwLogError(kEleNotFoundRC,"The 'end' MIDI file event at index %i in %s was not found.",msg_idx,cwStringNullGuard(p->fileA[file_idx].label)); goto errLabel; } } errLabel: return rc; } unsigned _fill_msg_array_from_msg( file_dev_t* p, unsigned file_idx, msg_t* msgA, unsigned msgN, unsigned msg_idx ) { unsigned k = msg_idx; for(unsigned j=0; jmsgA[k].msg_idx = j; p->msgA[k].amicro = time::specToMicroseconds(msgA[j].timeStamp); p->msgA[k].msg = msgA + j; p->msgA[k].file_idx = file_idx; ++k; } return k; } void _fill_msg_array( file_dev_t* p ) { unsigned msg_idx = 0; for(unsigned i=0; ifileN; ++i) if( p->fileA[i].msgA != nullptr ) msg_idx = _fill_msg_array_from_msg(p,i,p->fileA[i].msgA,p->fileA[i].msgN,msg_idx); p->msgN = msg_idx; } // Combine all the file msg's into a single array and sort them on file_msg_t.amicro. rc_t _prepare_msg_array( file_dev_t* p ) { rc_t rc = kOkRC; // save the current starting message unsigned beg_file_idx = p->beg_msg_idx==kInvalidIdx ? kInvalidIdx : p->msgA[ p->beg_msg_idx ].file_idx; unsigned beg_msg_idx = p->beg_msg_idx==kInvalidIdx ? kInvalidIdx : p->msgA[ p->beg_msg_idx ].msg_idx; // if the 'beg' file does not exist if( beg_file_idx != kInvalidIdx && _file_exists(p->fileA[ beg_file_idx ])==false ) { beg_file_idx = kInvalidIdx; beg_msg_idx = kInvalidIdx; } // save the current ending message unsigned end_file_idx = p->end_msg_idx==kInvalidIdx ? kInvalidIdx : p->msgA[ p->end_msg_idx ].file_idx; unsigned end_msg_idx = p->end_msg_idx==kInvalidIdx ? kInvalidIdx : p->msgA[ p->end_msg_idx ].msg_idx; // if the 'end' file does not exist if( end_file_idx != kInvalidIdx && _file_exists(p->fileA[ end_file_idx ])==false ) { end_file_idx = kInvalidIdx; end_msg_idx = kInvalidIdx; } // calc the count of message in all the files p->msgAllocN = _calc_msg_count(p); // allocate a single array to hold the messages from all the files p->msgA = mem::resize(p->msgA,p->msgAllocN); // fill p->msgA[] from each of the files _fill_msg_array(p); // sort p->msgA[] on msgA[].amicro auto f = [](const file_msg_t& a0,const file_msg_t& a1) -> bool { return a0.amicro < a1.amicro; }; std::sort(p->msgA,p->msgA+p->msgN,f); // by default we rewind to the first msg p->next_wr_msg_idx = 0; p->next_rd_msg_idx = 0; // if a valid seek position exists then reset it if( beg_file_idx != kInvalidIdx && beg_msg_idx != kInvalidIdx ) if((rc = _seek_to_msg_index( p, beg_file_idx, beg_msg_idx )) != kOkRC ) rc = cwLogError(rc,"The MIDI file device starting output message could not be restored."); if( end_file_idx != kInvalidIdx && end_msg_idx != kInvalidIdx ) if((rc = _set_end_msg_index(p, end_file_idx, end_msg_idx )) != kOkRC ) rc = cwLogError(rc,"The MIDI file device ending output message could not be restored."); return rc; } void _update_active_flag( file_dev_t* p ) { unsigned i=0; for(; ifileN; ++i) if( _file_exists(p->fileA[i]) && p->fileA[i].enable_fl ) break; p->is_activeFl = i < p->fileN; } rc_t _enable_file( handle_t h, unsigned file_idx, bool enable_fl ) { rc_t rc; file_dev_t* p = _handleToPtr(h); if((rc = _validate_file_existence(p, file_idx)) != kOkRC ) goto errLabel; p->fileA[ file_idx ].enable_fl = enable_fl; _update_active_flag(p); errLabel: if(rc != kOkRC ) rc = cwLogError(rc,"MIDI file device %s failed on file index %i.", enable_fl ? "enable" : "disable", file_idx ); return rc; } void _callback( file_dev_t* p, unsigned file_idx, msg_t* msgA, unsigned msgN ) { if( p->cbFunc != nullptr ) { packet_t pkt = {}; pkt.devIdx = p->base_dev_idx; pkt.portIdx = file_idx; pkt.msgArray = msgA; pkt.msgCnt = msgN; p->cbFunc( p->cbArg, &pkt, 1 ); } } void _packetize_and_transmit_msgs( file_dev_t* p, unsigned xmt_msg_cnt ) { msg_t msgA[ xmt_msg_cnt ]; unsigned pkt_msg_idx = 0; unsigned file_idx_0 = kInvalidIdx; assert( p->next_rd_msg_idx != kInvalidIdx && p->next_wr_msg_idx != kInvalidIdx ); for(; p->next_rd_msg_idx < p->next_wr_msg_idx && pkt_msg_idx < xmt_msg_cnt; ++p->next_rd_msg_idx) { const file_msg_t& m = p->msgA[ p->next_rd_msg_idx ]; if( p->fileA[ m.file_idx ].enable_fl && m.msg != nullptr ) { // if( p->latency_meas_enable_in_fl && isNoteOn(m.msg->status,m.msg->d1) ) { p->latency_meas_enable_in_fl = false; time::get(p->latency_meas_result.note_on_input_ts); } // if the file_idx is not the same as the previous messages then // send the currently stored messages from msgA[] - because packet // messages must all belong to the same port if( file_idx_0 != kInvalidIdx && m.file_idx != file_idx_0 ) { _callback(p,file_idx_0,msgA,pkt_msg_idx); pkt_msg_idx = 0; } msgA[pkt_msg_idx] = *m.msg; file_idx_0 = m.file_idx; pkt_msg_idx += 1; } } if( pkt_msg_idx > 0 ) _callback(p,file_idx_0,msgA,pkt_msg_idx); } rc_t _load_messages( handle_t h, unsigned file_idx, const char* fname, const msg_t* msgA, unsigned msgN ) { rc_t rc; file_dev_t* p = _handleToPtr(h); if((rc = _validate_file_index(p,file_idx)) != kOkRC ) goto errLabel; if((rc = _close_file(p,file_idx)) != kOkRC ) goto errLabel; if( fname != nullptr ) { if((rc = _open_midi_file(p,file_idx,fname)) != kOkRC ) goto errLabel; } else { if( msgA != nullptr ) { p->fileA[ file_idx ].msgA = mem::allocZ(msgN); p->fileA[ file_idx ].msgN = msgN; memcpy(p->fileA[ file_idx ].msgA, msgA, msgN*sizeof(msg_t)); } else { assert(0); } } if((rc = _prepare_msg_array(p)) != kOkRC ) goto errLabel; p->fileA[ file_idx ].enable_fl = true; errLabel: _update_active_flag(p); if( rc != kOkRC ) rc = cwLogError(rc,"MIDI file device msg. load port failed %i.",file_idx); return rc; } } } } } cw::rc_t cw::midi::device::file_dev::create( handle_t& hRef, cbFunc_t cbFunc, void* cbArg, unsigned baseDevIdx, const char* labelA[], unsigned max_file_cnt, const char* dev_name, unsigned read_ahead_micros) { rc_t rc; if((rc = destroy(hRef)) != kOkRC ) return rc; file_dev_t* p = mem::allocZ(); p->cbFunc = cbFunc; p->cbArg = cbArg; p->fileN = max_file_cnt; p->fileA = mem::allocZ(p->fileN); p->devCnt = 1; p->is_activeFl = false; p->read_ahead_micros = read_ahead_micros; p->base_dev_idx = baseDevIdx; p->dev_name = mem::duplStr(dev_name); _reset_indexes(p); for(unsigned i=0; ifileN; ++i) { if( labelA[i] != nullptr ) { p->fileA[i].label = mem::duplStr(labelA[i]); } else { rc = cwLogError(kInvalidArgRC,"Count of MIDI file device labels must match the max file count."); goto errLabel; } } hRef.set(p); errLabel: if(rc != kOkRC ) _destroy(p); return rc; } cw::rc_t cw::midi::device::file_dev::destroy( handle_t& hRef ) { rc_t rc = kOkRC; if(!hRef.isValid() ) return rc; file_dev_t* p = _handleToPtr(hRef); if((rc = _destroy(p)) != kOkRC ) { rc = cwLogError(rc,"MIDI file device destroy failed."); goto errLabel; } hRef.clear(); errLabel: return rc; } bool cw::midi::device::file_dev::is_active( handle_t h ) { file_dev_t* p = _handleToPtr(h); return p->is_activeFl; } unsigned cw::midi::device::file_dev::file_count( handle_t h ) { file_dev_t* p = _handleToPtr(h); return p->fileN; } cw::rc_t cw::midi::device::file_dev::open_midi_file( handle_t h, unsigned file_idx, const char* fname ) { return _load_messages(h,file_idx,fname,nullptr,0); } cw::rc_t cw::midi::device::file_dev::load_messages( handle_t h, unsigned file_idx, const msg_t* msgA, unsigned msgN ) { return _load_messages(h,file_idx,nullptr,msgA,msgN); } cw::rc_t cw::midi::device::file_dev::enable_file( handle_t h, unsigned file_idx, bool enableFl ) { return _enable_file(h,file_idx,enableFl); } cw::rc_t cw::midi::device::file_dev::enable_file( handle_t h, unsigned file_idx ) { return _enable_file(h,file_idx,true); } cw::rc_t cw::midi::device::file_dev::disable_file( handle_t h,unsigned file_idx ) { return _enable_file(h,file_idx,false); } unsigned cw::midi::device::file_dev::count( handle_t h ) { file_dev_t* p = _handleToPtr(h); return p->devCnt; } const char* cw::midi::device::file_dev::name( handle_t h, unsigned devIdx ) { file_dev_t* p = _handleToPtr(h); return _validate_dev_index(p,devIdx)==kOkRC ? p->dev_name : nullptr; } unsigned cw::midi::device::file_dev::nameToIndex(handle_t h, const char* deviceName) { file_dev_t* p = _handleToPtr(h); return textIsEqual(deviceName,p->dev_name) ? 0 : kInvalidIdx; } unsigned cw::midi::device::file_dev::portCount( handle_t h, unsigned devIdx, unsigned flags ) { file_dev_t* p = _handleToPtr(h); if(_validate_dev_index(p,devIdx) != kOkRC ) return 0; return flags & kInMpFl ? p->fileN : 0; } const char* cw::midi::device::file_dev::portName( handle_t h, unsigned devIdx, unsigned flags, unsigned portIdx ) { file_dev_t* p = _handleToPtr(h); if( _validate_dev_index(p,devIdx) != kOkRC ) return nullptr; if( _validate_port_index(p,portIdx) != kOkRC ) return nullptr; return p->fileA[portIdx].label; } unsigned cw::midi::device::file_dev::portNameToIndex( handle_t h, unsigned devIdx, unsigned flags, const char* portName ) { file_dev_t* p = _handleToPtr(h); if( _validate_dev_index(p,devIdx) != kOkRC ) return kInvalidIdx; if( flags & kInMpFl ) { for(unsigned i=0; ifileN; ++i) if( textIsEqual(p->fileA[i].label,portName) ) return i; } return kInvalidIdx; } cw::rc_t cw::midi::device::file_dev::portEnable( handle_t h, unsigned devIdx, unsigned flags, unsigned portIdx, bool enableFl ) { rc_t rc = kOkRC; file_dev_t* p = _handleToPtr(h); if((rc = _validate_dev_index(p,devIdx)) != kOkRC ) goto errLabel; if( flags & kInMpFl ) { rc = enable_file(h,portIdx,enableFl); } else { rc = cwLogError(kNotImplementedRC,"MIDI file dev output enable/disable has not been implemented."); } errLabel: if(rc != kOkRC ) rc = cwLogError(rc,"MIDI file dev port enable/disable failed."); return rc; } unsigned cw::midi::device::file_dev::msg_count( handle_t h, unsigned file_idx ) { file_dev_t* p = _handleToPtr(h); rc_t rc; if((rc = _validate_file_existence(p,file_idx)) != kOkRC ) goto errLabel; return p->msgN; errLabel: return 0; } cw::rc_t cw::midi::device::file_dev::seek_to_msg_index( handle_t h, unsigned file_idx, unsigned msg_idx ) { file_dev_t* p = _handleToPtr(h); return _seek_to_msg_index(p,file_idx,msg_idx); } cw::rc_t cw::midi::device::file_dev::set_end_msg_index( handle_t h, unsigned file_idx, unsigned msg_idx ) { file_dev_t* p = _handleToPtr(h); return _set_end_msg_index(p, file_idx, msg_idx ); } cw::rc_t cw::midi::device::file_dev::rewind( handle_t h ) { file_dev_t* p = _handleToPtr(h); if( p->beg_msg_idx == kInvalidIdx ) { p->next_wr_msg_idx = 0; p->next_rd_msg_idx = 0; } else { p->next_wr_msg_idx = p->beg_msg_idx; p->next_rd_msg_idx = p->beg_msg_idx; } return kOkRC; } cw::rc_t cw::midi::device::file_dev::set_start_delay( handle_t h, unsigned start_delay_micros ) { file_dev_t* p = _handleToPtr(h); p->start_delay_micros = start_delay_micros; return kOkRC; } cw::midi::device::file_dev::exec_result_t cw::midi::device::file_dev::exec( handle_t h, unsigned long long cur_time_us ) { exec_result_t r; file_dev_t*p = _handleToPtr(h); // p->end_msg_idx indicates the last msg to transmit, but we wait until the msg following p->end_msg_idx to // actually stop transmitting and set r.eof_fl. This will facilitate providing a natural time gap to loop to the beginning. unsigned end_msg_idx = p->end_msg_idx == kInvalidIdx ? p->msgN-1 : p->end_msg_idx; // if there are no messages left to send if( p->next_wr_msg_idx==kInvalidIdx || p->next_wr_msg_idx >= p->msgN || p->next_wr_msg_idx > p->end_msg_idx ) { r.next_msg_wait_micros = 0; r.xmit_cnt = 0; r.eof_fl = true; } else { if( cur_time_us < p->start_delay_micros ) { r.xmit_cnt = 0; r.eof_fl = false; r.next_msg_wait_micros = p->start_delay_micros - cur_time_us; } else { cur_time_us -= p->start_delay_micros; unsigned base_msg_idx = p->beg_msg_idx == kInvalidIdx ? 0 : p->beg_msg_idx; assert( base_msg_idx <= p->next_wr_msg_idx ); unsigned long long msg_time_us = p->msgA[ p->next_wr_msg_idx ].amicro - p->msgA[ base_msg_idx ].amicro; unsigned xmit_cnt = 0; unsigned long long end_time_us = cur_time_us + p->read_ahead_micros; // for all msgs <= current time + read_ahead_micros while( msg_time_us <= end_time_us ) { // // consume msg here // xmit_cnt += 1; // advance to next msg p->next_wr_msg_idx += 1; // check for EOF if( p->next_wr_msg_idx >= p->msgN) break; // time of next msg msg_time_us = p->msgA[ p->next_wr_msg_idx ].amicro - p->msgA[ base_msg_idx ].amicro; // if we went past the end msg then stop if( p->next_wr_msg_idx > end_msg_idx ) break; } assert( p->next_wr_msg_idx >= p->msgN || msg_time_us > cur_time_us ); r.xmit_cnt = xmit_cnt; r.eof_fl = p->next_wr_msg_idx >= p->msgN; r.next_msg_wait_micros = r.eof_fl ? 0 : msg_time_us - cur_time_us; // callback with output msg's if( xmit_cnt ) _packetize_and_transmit_msgs( p, xmit_cnt ); } } return r; } cw::rc_t cw::midi::device::file_dev::send( handle_t h, unsigned devIdx, unsigned portIdx, uint8_t st, uint8_t d0, uint8_t d1 ) { return cwLogError(kNotImplementedRC,"MIDI file dev send() not implemented."); file_dev_t* p = _handleToPtr(h); if( p->latency_meas_enable_out_fl && isNoteOn(st,d1) ) { p->latency_meas_enable_out_fl = false; time::get(p->latency_meas_result.note_on_output_ts); } } cw::rc_t cw::midi::device::file_dev::sendData( handle_t h, unsigned devIdx, unsigned portIdx, const uint8_t* dataPtr, unsigned byteCnt ) { return cwLogError(kNotImplementedRC,"MIDI file dev send() not implemented."); } void cw::midi::device::file_dev::latency_measure_reset(handle_t h) { file_dev_t* p = _handleToPtr(h); p->latency_meas_result.note_on_input_ts = {}; p->latency_meas_result.note_on_output_ts = {}; p->latency_meas_enable_in_fl = true; p->latency_meas_enable_out_fl = true; } cw::midi::device::latency_meas_result_t cw::midi::device::file_dev::latency_measure_result(handle_t h) { file_dev_t* p = _handleToPtr(h); return p->latency_meas_result; } void cw::midi::device::file_dev::report( handle_t h, textBuf::handle_t tbH) { file_dev_t* p = _handleToPtr(h); print(tbH,"%i : Device: '%s'\n",p->base_dev_idx,p->dev_name); if( p->fileN ) print(tbH," Input:\n"); for(unsigned i=0; ifileN; ++i) { const char* fname = ""; if( p->fileA[i].fname != nullptr ) { fname = p->fileA[i].fname; } else if( p->fileA[i].msgA != nullptr ) { fname = "msg array"; } print(tbH," port:%i '%s' ena:%i msg count:%i %s\n", i, cwStringNullGuard(p->fileA[i].label), p->fileA[i].enable_fl, p->fileA[i].msgN, fname ); } }