libcw/cwMidiDevice.cpp

832 lines
23 KiB
C++
Raw Normal View History

//| Copyright: (C) 2020-2024 Kevin Larke <contact AT larke DOT org>
//| 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 <poll.h>
#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<unsigned> buf_ii;
std::atomic<unsigned> buf_oi;
bool filterRtSenseFl;
} device_t;
device_t* _handleToPtr( handle_t h )
{ return handleToPtr<handle_t,device_t>(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 );
}
2024-02-21 12:47:09 +00:00
// 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; i<pktCnt; ++i)
{
const packet_t* pkt = pktArray + i;
if( pkt->msgArray != nullptr )
{
unsigned ii = p->buf_ii.load();
unsigned oi = p->buf_oi.load();
for(unsigned j=0; j<pkt->msgCnt; ++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<device_t>();
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<ch_msg_t>( 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; i<filePortArgCnt; ++i)
{
if((port = file_ports->child_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;
}