//| 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 "cwObject.h" #include "cwText.h" #include "cwTextBuf.h" #include "cwThread.h" #include "cwMidi.h" #include "cwMidiDecls.h" #include "cwMidiFile.h" #include "cwMidiDevice.h" #include #include "cwMidiAlsa.h" #include "cwMidiFileDev.h" namespace cw { namespace midi { namespace device { typedef enum { kStoppedStateId, kPausedStateId, kPlayingStateId } transportStateId_t; typedef struct device_str { cbFunc_t cbFunc; void* cbArg; alsa::handle_t alsaDevH; unsigned alsaPollfdN; struct pollfd* alsaPollfdA; unsigned alsa_dev_cnt; file_dev::handle_t fileDevH; unsigned file_dev_cnt; unsigned total_dev_cnt; unsigned thread_timeout_microsecs; thread::handle_t threadH; transportStateId_t fileDevStateId; unsigned long long offset_micros; unsigned long long last_posn_micros; time::spec_t start_time; ch_msg_t* buf; unsigned bufN; std::atomic buf_ii; std::atomic buf_oi; bool filterRtSenseFl; } device_t; device_t* _handleToPtr( handle_t h ) { return handleToPtr(h); } rc_t _validate_dev_index( device_t* p, unsigned devIdx ) { rc_t rc = kOkRC; if( devIdx >= p->total_dev_cnt ) rc = cwLogError(kInvalidArgRC,"Invalid MIDI device index (%i >= %i).",devIdx,p->total_dev_cnt); return rc; } unsigned _devIdxToAlsaDevIdx( device_t* p, unsigned devIdx ) { return devIdx >= p->alsa_dev_cnt ? kInvalidIdx : devIdx; } unsigned _devIdxToFileDevIdx( device_t* p, unsigned devIdx ) { return devIdx==kInvalidIdx || devIdx < p->alsa_dev_cnt ? kInvalidIdx : devIdx - p->alsa_dev_cnt; } unsigned _alsaDevIdxToDevIdx( device_t* p, unsigned alsaDevIdx ) { return alsaDevIdx; } unsigned _fileDevIdxToDevIdx( device_t* p, unsigned fileDevIdx ) { return fileDevIdx == kInvalidIdx ? kInvalidIdx : p->alsa_dev_cnt + fileDevIdx; } bool _isAlsaDevIdx( device_t* p, unsigned devIdx ) { return devIdx==kInvalidIdx ? false : devIdx < p->alsa_dev_cnt; } bool _isFileDevIdx( device_t* p, unsigned devIdx ) { return devIdx==kInvalidIdx ? false : (p->alsa_dev_cnt <= devIdx && devIdx < p->total_dev_cnt); } rc_t _destroy( device_t* p ) { rc_t rc = kOkRC; if((rc = destroy(p->threadH)) != kOkRC ) { rc = cwLogError(rc,"MIDI port thread destroy failed."); goto errLabel; } destroy(p->alsaDevH); destroy(p->fileDevH); mem::release(p->buf); mem::release(p); errLabel: return rc; } bool _thread_func( void* arg ) { device_t* p = (device_t*)arg; unsigned max_sleep_micros = p->thread_timeout_microsecs/2; unsigned sleep_millis = max_sleep_micros/1000; if( p->fileDevStateId == kPlayingStateId ) { time::spec_t cur_time = time::current_time(); unsigned elapsed_micros = time::elapsedMicros(p->start_time,cur_time); unsigned long long file_posn_micros = p->offset_micros + elapsed_micros; // Send any messages whose time has expired and get the // wait time for the next message. file_dev::exec_result_t r = exec(p->fileDevH,file_posn_micros); // If the file dev has no more messages to play then sleep for the maximum time. unsigned file_dev_sleep_micros = r.eof_fl ? max_sleep_micros : r.next_msg_wait_micros; // Prevent the wait time from being longer than the thread state change timeout. unsigned sleep_micros = std::min( max_sleep_micros, file_dev_sleep_micros ); p->last_posn_micros = file_posn_micros + sleep_micros; // If the wait time is less than one millisecond then make it one millisecond. // (remember that we allowed the file device to go 3 milliseconds ahead and // and so it is safe, and better for preventing many very short timeout's, // to wait at least 1 millisecond) sleep_millis = std::max(1U, sleep_micros/1000 ); } // TODO: Consider replacing the poll() with epoll_wait2() is apparently // optimized for more accurate time outs. // Block here waiting for ALSA events or timeout when the next file msg should be sent int sysRC = poll( p->alsaPollfdA, p->alsaPollfdN, sleep_millis ); if(sysRC == 0 ) { // time-out } else { if( sysRC > 0 ) { rc_t rc; if((rc = handleInputMsg(p->alsaDevH)) != kOkRC ) { cwLogError(rc,"ALSA MIDI dev. input failed"); } } else { cwLogSysError(kOpFailRC,sysRC,"MIDI device poll failed."); } } return true; } void _callback( void* cbArg, const packet_t* pktArray, unsigned pktCnt ) { device_t* p = (device_t*)cbArg; for(unsigned i=0; imsgArray != nullptr ) { unsigned ii = p->buf_ii.load(); unsigned oi = p->buf_oi.load(); for(unsigned j=0; jmsgCnt; ++j) { ch_msg_t* m = p->buf + ii; m->devIdx = pkt->devIdx; m->portIdx = pkt->portIdx; m->timeStamp = pkt->msgArray[j].timeStamp; m->uid = pkt->msgArray[j].uid; m->ch = pkt->msgArray[j].ch; m->status = pkt->msgArray[j].status; m->d0 = pkt->msgArray[j].d0; m->d1 = pkt->msgArray[j].d1; ii = (ii+1 == p->bufN ? 0 : ii+1); if( ii == oi ) { cwLogError(kBufTooSmallRC,"The MIDI device buffer is full %i.",p->bufN); } } p->buf_ii.store(ii); } } if( p->cbFunc != nullptr ) p->cbFunc(p->cbArg,pktArray,pktCnt); } } // device } // midi } // cw cw::rc_t cw::midi::device::create( handle_t& hRef, cbFunc_t cbFunc, void* cbArg, const char* filePortLabelA[], unsigned max_file_cnt, const char* appNameStr, const char* fileDevName, unsigned fileDevReadAheadMicros, unsigned parserBufByteCnt, bool enableBufFl, unsigned bufferMsgCnt, bool filterRtSenseFl ) { rc_t rc = kOkRC; rc_t rc1 = kOkRC; if((rc = destroy(hRef)) != kOkRC ) return rc; device_t* p = mem::allocZ(); if((rc = create( p->alsaDevH, enableBufFl ? _callback : cbFunc, enableBufFl ? p : cbArg, parserBufByteCnt, appNameStr, filterRtSenseFl)) != kOkRC ) { rc = cwLogError(rc,"ALSA MIDI device create failed."); goto errLabel; } p->alsa_dev_cnt = count(p->alsaDevH); if((rc = create( p->fileDevH, enableBufFl ? _callback : cbFunc, enableBufFl ? p : cbArg, p->alsa_dev_cnt, filePortLabelA, max_file_cnt, fileDevName, fileDevReadAheadMicros )) != kOkRC ) { rc = cwLogError(rc,"MIDI file device create failed."); goto errLabel; } p->cbFunc = cbFunc; p->cbArg = cbArg; p->file_dev_cnt = count(p->fileDevH); p->total_dev_cnt = p->alsa_dev_cnt + p->file_dev_cnt; p->alsaPollfdA = pollFdArray(p->alsaDevH,p->alsaPollfdN); p->fileDevStateId = kStoppedStateId; p->buf = mem::allocZ( bufferMsgCnt ); p->bufN = bufferMsgCnt; p->buf_ii.store(0); p->buf_oi.store(0); if((rc = thread::create(p->threadH, _thread_func, p, "midi_dev")) != kOkRC ) { rc = cwLogError(rc,"The MIDI file device thread create failed."); goto errLabel; } p->thread_timeout_microsecs = stateTimeOutMicros(p->threadH); hRef.set(p); if((rc = unpause(p->threadH)) != kOkRC ) { rc = cwLogError(rc,"Initial thread un-pause failed."); goto errLabel; } errLabel: if(rc != kOkRC ) rc1 = _destroy(p); if((rc = rcSelect(rc,rc1)) != kOkRC ) rc = cwLogError(rc,"MIDI device mgr. create failed."); return rc; } cw::rc_t cw::midi::device::create( handle_t& h, cbFunc_t cbFunc, void* cbArg, const object_t* args ) { rc_t rc = kOkRC; const char* appNameStr = nullptr; const char* fileDevName = "file_dev"; unsigned fileDevReadAheadMicros = 3000; unsigned parseBufByteCnt = 1024; bool enableBufFl = false; unsigned bufMsgCnt = 0; const object_t* file_ports = nullptr; const object_t* port = nullptr; bool filterRtSenseFl = true;; if((rc = args->getv("appNameStr",appNameStr, "fileDevName",fileDevName, "fileDevReadAheadMicros",fileDevReadAheadMicros, "parseBufByteCnt",parseBufByteCnt, "enableBufFl",enableBufFl, "bufferMsgCnt",bufMsgCnt, "file_ports",file_ports, "filterRtSenseFl",filterRtSenseFl)) != kOkRC ) { rc = cwLogError(rc,"MIDI port parse args. failed."); } else { unsigned fpi = 0; unsigned filePortArgCnt = file_ports->child_count(); const char* labelArray[ filePortArgCnt ]; memset(labelArray,0,sizeof(labelArray)); for(unsigned i=0; ichild_ele(i)) != nullptr ) { if((rc = port->getv("label",labelArray[fpi])) != kOkRC ) { rc = cwLogError(rc,"MIDI file dev. port arg parse failed."); goto errLabel; } fpi += 1; } } rc = create(h, cbFunc, cbArg, labelArray, fpi, appNameStr, fileDevName, fileDevReadAheadMicros, parseBufByteCnt, enableBufFl, bufMsgCnt, filterRtSenseFl); } errLabel: return rc; } cw::rc_t cw::midi::device::destroy( handle_t& hRef) { rc_t rc = kOkRC; if( !hRef.isValid() ) return rc; device_t* p = _handleToPtr(hRef); if((rc = _destroy(p)) != kOkRC ) { rc = cwLogError(rc,"MIDI device mgr. destroy failed."); goto errLabel; } hRef.clear(); errLabel: return rc; } bool cw::midi::device::isInitialized( handle_t h ) { return h.isValid(); } unsigned cw::midi::device::count( handle_t h ) { device_t* p = _handleToPtr(h); return p->total_dev_cnt; } const char* cw::midi::device::name( handle_t h, unsigned devIdx ) { device_t* p = _handleToPtr(h); const char* ret_name = nullptr; unsigned alsaDevIdx = kInvalidIdx; unsigned fileDevIdx = kInvalidIdx; if((alsaDevIdx = _devIdxToAlsaDevIdx(p,devIdx)) != kInvalidIdx ) ret_name = name(p->alsaDevH,alsaDevIdx); else { if((fileDevIdx = _devIdxToFileDevIdx(p,devIdx)) != kInvalidIdx) ret_name = name(p->fileDevH,fileDevIdx); else cwLogError(kInvalidArgRC,"%i is an invalid device index.",devIdx); } if( ret_name == nullptr ) cwLogError(kOpFailRC,"The name of device index %i could not be found.",devIdx); return ret_name; } unsigned cw::midi::device::nameToIndex(handle_t h, const char* deviceName) { device_t* p = _handleToPtr(h); unsigned devIdx = kInvalidIdx; if((devIdx = nameToIndex(p->alsaDevH,deviceName)) != kInvalidIdx ) devIdx = _alsaDevIdxToDevIdx(p,devIdx); else { if((devIdx = nameToIndex(p->fileDevH,deviceName)) != kInvalidIdx ) devIdx = _fileDevIdxToDevIdx(p,devIdx); } if( devIdx == kInvalidIdx ) cwLogError(kOpFailRC,"MIDI device name to index failed on '%s'.",cwStringNullGuard(deviceName)); return devIdx; } unsigned cw::midi::device::portNameToIndex( handle_t h, unsigned devIdx, unsigned flags, const char* portNameStr ) { device_t* p = _handleToPtr(h); unsigned alsaDevIdx = kInvalidIdx; unsigned fileDevIdx = kInvalidIdx; unsigned portIdx = kInvalidIdx; if((alsaDevIdx = _devIdxToAlsaDevIdx(p,devIdx)) != kInvalidIdx ) portIdx = portNameToIndex(p->alsaDevH,alsaDevIdx,flags,portNameStr); else if((fileDevIdx = _devIdxToFileDevIdx(p,devIdx)) != kInvalidIdx ) portIdx = portNameToIndex(p->fileDevH,fileDevIdx,flags,portNameStr); if( portIdx == kInvalidIdx ) cwLogError(kInvalidArgRC,"The MIDI port name '%s' could not be found.",cwStringNullGuard(portNameStr)); return portIdx; } cw::rc_t cw::midi::device::portEnable( handle_t h, unsigned devIdx, unsigned flags, unsigned portIdx, bool enableFl ) { rc_t rc = kOkRC; device_t* p = _handleToPtr(h); unsigned alsaDevIdx = kInvalidIdx; unsigned fileDevIdx = kInvalidIdx; if((alsaDevIdx = _devIdxToAlsaDevIdx(p,devIdx)) != kInvalidIdx ) rc = portEnable(p->alsaDevH,alsaDevIdx,flags,portIdx,enableFl); else if((fileDevIdx = _devIdxToFileDevIdx(p,devIdx)) != kInvalidIdx ) rc = portEnable(p->fileDevH,fileDevIdx,flags,portIdx,enableFl); if( rc != kOkRC ) rc = cwLogError(rc,"The MIDI port %s failed on dev '%s' port '%s'.",enableFl ? "enable" : "disable", cwStringNullGuard(name(h,devIdx)), cwStringNullGuard(portName(h,devIdx,flags,portIdx))); return rc; } unsigned cw::midi::device::portCount( handle_t h, unsigned devIdx, unsigned flags ) { device_t* p = _handleToPtr(h); unsigned alsaDevIdx = kInvalidIdx; unsigned fileDevIdx = kInvalidIdx; unsigned portCnt = 0; if((alsaDevIdx = _devIdxToAlsaDevIdx(p,devIdx)) != kInvalidIdx ) portCnt = portCount(p->alsaDevH,alsaDevIdx,flags); else { if((fileDevIdx = _devIdxToFileDevIdx(p,devIdx)) != kInvalidIdx ) portCnt = portCount(p->fileDevH,fileDevIdx,flags); else cwLogError(kInvalidArgRC,"The device index %i is not valid. Port count access failed.",devIdx); } return portCnt; } const char* cw::midi::device::portName( handle_t h, unsigned devIdx, unsigned flags, unsigned portIdx ) { device_t* p = _handleToPtr(h); unsigned alsaDevIdx = kInvalidIdx; unsigned fileDevIdx = kInvalidIdx; const char* name = nullptr; if((alsaDevIdx = _devIdxToAlsaDevIdx(p,devIdx)) != kInvalidIdx ) name = portName(p->alsaDevH,alsaDevIdx,flags,portIdx); else if((fileDevIdx = _devIdxToFileDevIdx(p,devIdx)) != kInvalidIdx ) name = portName(p->fileDevH,fileDevIdx,flags,portIdx); else cwLogError(kInvalidArgRC,"The device index %i is not valid."); if( name == nullptr ) cwLogError(kOpFailRC,"The access to %s port name on device index %i port index %i failed.",flags & kInMpFl ? "input" : "output", devIdx,portIdx); return name; } cw::rc_t cw::midi::device::send( handle_t h, unsigned devIdx, unsigned portIdx, uint8_t st, uint8_t d0, uint8_t d1 ) { rc_t rc = kOkRC; device_t* p = _handleToPtr(h); unsigned alsaDevIdx = kInvalidIdx; unsigned fileDevIdx = kInvalidIdx; if((alsaDevIdx = _devIdxToAlsaDevIdx(p,devIdx)) != kInvalidIdx ) rc = send(p->alsaDevH,alsaDevIdx,portIdx,st,d0,d1); else { if((fileDevIdx = _devIdxToFileDevIdx(p,devIdx)) != kInvalidIdx ) rc = send(p->fileDevH,fileDevIdx,portIdx,st,d0,d1); else rc = cwLogError(kInvalidArgRC,"The device %i is not valid.",devIdx); } if( rc != kOkRC ) rc = cwLogError(rc,"The MIDI msg (0x%x %i %i) transmit failed.",st,d0,d1); return rc; } cw::rc_t cw::midi::device::sendData( handle_t h, unsigned devIdx, unsigned portIdx, const uint8_t* dataPtr, unsigned byteCnt ) { rc_t rc = kOkRC; device_t* p = _handleToPtr(h); unsigned alsaDevIdx = kInvalidIdx; unsigned fileDevIdx = kInvalidIdx; if((alsaDevIdx = _devIdxToAlsaDevIdx(p,devIdx)) != kInvalidIdx ) rc = sendData(p->alsaDevH,alsaDevIdx,portIdx,dataPtr,byteCnt); else { if((fileDevIdx = _devIdxToFileDevIdx(p,devIdx)) != kInvalidIdx ) rc = sendData(p->fileDevH,fileDevIdx,portIdx,dataPtr,byteCnt); else rc = cwLogError(kInvalidArgRC,"The device %i is not valid.",devIdx); } if( rc != kOkRC ) rc = cwLogError(rc,"The MIDI msg transmit data failed."); return rc; } cw::rc_t cw::midi::device::openMidiFile( handle_t h, unsigned devIdx, unsigned portIdx, const char* fname ) { rc_t rc = kOkRC; device_t* p = _handleToPtr(h); if( _devIdxToFileDevIdx(p,devIdx) == kInvalidIdx ) { cwLogError(kInvalidArgRC,"The device index %i does not identify a valid file device.",devIdx); goto errLabel; } if((rc = open_midi_file( p->fileDevH, portIdx, fname)) != kOkRC ) goto errLabel; errLabel: return rc; } cw::rc_t cw::midi::device::loadMsgPacket( handle_t h, const packet_t& pkt ) { rc_t rc = kOkRC; device_t* p = _handleToPtr(h); if( _devIdxToFileDevIdx(p,pkt.devIdx) == kInvalidIdx ) { cwLogError(kInvalidArgRC,"The device index %i does not identify a valid file device.",pkt.devIdx); goto errLabel; } if((rc = load_messages( p->fileDevH, pkt.portIdx, pkt.msgArray, pkt.msgCnt)) != kOkRC ) goto errLabel; errLabel: return rc; } unsigned cw::midi::device::msgCount( handle_t h, unsigned devIdx, unsigned portIdx ) { device_t* p = _handleToPtr(h); if(_devIdxToFileDevIdx(p,devIdx) == kInvalidIdx ) { cwLogError(kInvalidArgRC,"The device index %i does not identify a valid file device.",devIdx); goto errLabel; } return msg_count( p->fileDevH, portIdx); errLabel: return 0; } cw::rc_t cw::midi::device::seekToMsg( handle_t h, unsigned devIdx, unsigned portIdx, unsigned msgIdx ) { rc_t rc = kOkRC; device_t* p = _handleToPtr(h); if(_devIdxToFileDevIdx(p,devIdx) == kInvalidIdx ) { cwLogError(kInvalidArgRC,"The device index %i does not identify a valid file device.",devIdx); goto errLabel; } if((rc = seek_to_msg_index( p->fileDevH, portIdx, msgIdx)) != kOkRC ) goto errLabel; errLabel: return rc; } cw::rc_t cw::midi::device::setEndMsg( handle_t h, unsigned devIdx, unsigned portIdx, unsigned msgIdx ) { rc_t rc = kOkRC; device_t* p = _handleToPtr(h); if(_devIdxToFileDevIdx(p,devIdx) == kInvalidIdx ) { cwLogError(kInvalidArgRC,"The device index %i does not identify a valid file device.",devIdx); goto errLabel; } if((rc = set_end_msg_index( p->fileDevH, portIdx, msgIdx)) != kOkRC ) goto errLabel; errLabel: return rc; } unsigned cw::midi::device::maxBufferMsgCount( handle_t h ) { device_t* p = _handleToPtr(h); return p->bufN; } const cw::midi::ch_msg_t* cw::midi::device::getBuffer( handle_t h, unsigned& msgCntRef ) { device_t* p = _handleToPtr(h); unsigned ii = p->buf_ii.load(); unsigned oi = p->buf_oi.load(); ch_msg_t* m = nullptr; msgCntRef = ii >= oi ? ii-oi : p->bufN - oi; if( msgCntRef > 0 ) m = p->buf + oi; return m; } cw::rc_t cw::midi::device::clearBuffer( handle_t h, unsigned msgCnt ) { if( msgCnt > 0 ) { device_t* p = _handleToPtr(h); unsigned oi = p->buf_oi.load(); oi = (oi + msgCnt) % p->bufN; p->buf_oi.store(oi); } return kOkRC; } cw::rc_t cw::midi::device::start( handle_t h ) { rc_t rc = kOkRC; device_t* p = _handleToPtr(h); if( p->fileDevStateId != kPlayingStateId ) { if((rc = rewind(p->fileDevH)) != kOkRC ) { rc = cwLogError(rc,"Rewind failed on MIDI file device."); goto errLabel; } p->start_time = time::current_time(); p->offset_micros = 0; p->last_posn_micros = 0; p->fileDevStateId = kPlayingStateId; } errLabel: if( rc != kOkRC ) rc = cwLogError(rc,"MIDI port start failed."); return rc; } cw::rc_t cw::midi::device::stop( handle_t h ) { device_t* p = _handleToPtr(h); p->fileDevStateId = kStoppedStateId; return kOkRC; } cw::rc_t cw::midi::device::pause( handle_t h, bool pause_fl ) { rc_t rc = kOkRC; device_t* p = _handleToPtr(h); switch( p->fileDevStateId ) { case kStoppedStateId: // unpausing does nothing from a 'stopped' state break; case kPausedStateId: if( !pause_fl ) { p->start_time = time::current_time(); p->fileDevStateId = kPlayingStateId; } break; case kPlayingStateId: if( pause_fl ) { p->offset_micros = p->last_posn_micros; p->fileDevStateId = kPausedStateId; } break; } return rc; } cw::rc_t cw::midi::device::report( handle_t h ) { rc_t rc = kOkRC; textBuf::handle_t tbH; if((rc = textBuf::create(tbH)) != kOkRC ) goto errLabel; report(h,tbH); printf("%s\n",text(tbH)); errLabel: destroy(tbH); return rc; } void cw::midi::device::report( handle_t h, textBuf::handle_t tbH) { device_t* p = _handleToPtr(h); report(p->alsaDevH,tbH); report(p->fileDevH,tbH); } void cw::midi::device::latency_measure_reset(handle_t h) { device_t* p = _handleToPtr(h); latency_measure_reset(p->alsaDevH); latency_measure_reset(p->fileDevH); } cw::midi::device::latency_meas_combined_result_t cw::midi::device::latency_measure_result(handle_t h) { device_t* p = _handleToPtr(h); latency_meas_combined_result_t r; r.alsa_dev = latency_measure_result(p->alsaDevH); r.file_dev = latency_measure_result(p->fileDevH); return r; }