From a511b00493d9162eeea60e709b8be2a0f1c67a96 Mon Sep 17 00:00:00 2001 From: kevin Date: Sat, 10 Apr 2021 13:38:49 -0400 Subject: [PATCH] audio_midi : Initial implementation of audio_midi io-based app. --- Makefile.am | 4 +- cwIoAudioMidi.cpp | 721 +++++++++++++++++++++++++++++ cwIoAudioMidi.h | 16 + html/audio_midi/css/ui.css | 75 +++ html/audio_midi/index.html | 25 + html/audio_midi/js/ui.js | 906 +++++++++++++++++++++++++++++++++++++ html/audio_midi/ui.cfg | 33 ++ 7 files changed, 1778 insertions(+), 2 deletions(-) create mode 100644 cwIoAudioMidi.cpp create mode 100644 cwIoAudioMidi.h create mode 100644 html/audio_midi/css/ui.css create mode 100644 html/audio_midi/index.html create mode 100644 html/audio_midi/js/ui.js create mode 100644 html/audio_midi/ui.cfg diff --git a/Makefile.am b/Makefile.am index dc496d0..06d538e 100644 --- a/Makefile.am +++ b/Makefile.am @@ -50,8 +50,8 @@ libcwHDR += src/libcw/cwMidiPort.h src/libcw/cwAudioD libcwSRC += src/libcw/cwMidiPort.cpp src/libcw/cwMidiAlsa.cpp src/libcw/cwAudioDeviceAlsa.cpp src/libcw/cwAudioDeviceTest.cpp if cwWEBSOCK -libcwHDR += src/libcw/cwIo.h src/libcw/cwIoTest.h src/libcw/cwIoSocketChat.h src/libcw/cwIoAudioPanel.h -libcwSRC += src/libcw/cwIo.cpp src/libcw/cwIoTest.cpp src/libcw/cwIoSocketChat.cpp src/libcw/cwIoAudioPanel.cpp +libcwHDR += src/libcw/cwIo.h src/libcw/cwIoTest.h src/libcw/cwIoSocketChat.h src/libcw/cwIoAudioPanel.h src/libcw/cwIoAudioMidi.h +libcwSRC += src/libcw/cwIo.cpp src/libcw/cwIoTest.cpp src/libcw/cwIoSocketChat.cpp src/libcw/cwIoAudioPanel.cpp src/libcw/cwIoAudioMidi.cpp endif endif diff --git a/cwIoAudioMidi.cpp b/cwIoAudioMidi.cpp new file mode 100644 index 0000000..feee438 --- /dev/null +++ b/cwIoAudioMidi.cpp @@ -0,0 +1,721 @@ +#include "cwCommon.h" +#include "cwLog.h" +#include "cwCommonImpl.h" +#include "cwMem.h" +#include "cwObject.h" +#include "cwFileSys.h" +#include "cwFile.h" +#include "cwTime.h" +#include "cwMidiDecls.h" +#include "cwMidi.h" +#include "cwUiDecls.h" +#include "cwIo.h" +#include "cwIoAudioMidi.h" + +namespace cw +{ + namespace io + { + namespace audio_midi + { + + // Application Id's for UI elements + enum + { + // Resource Based elements + kPanelDivId = 1000, + kQuitBtnId, + kIoReportBtnId, + kReportBtnId, + kRecordCheckId, + kStartBtnId, + kStopBtnId, + kClearBtnId, + kMsgCntId, + kSaveBtnId, + kOpenBtnId, + kFnStringId + }; + + enum + { + kAmMidiTimerId + }; + + + // Application Id's for the resource based UI elements. + ui::appIdMap_t mapA[] = + { + { ui::kRootAppId, kPanelDivId, "panelDivId" }, + { kPanelDivId, kQuitBtnId, "quitBtnId" }, + { kPanelDivId, kIoReportBtnId, "ioReportBtnId" }, + { kPanelDivId, kReportBtnId, "reportBtnId" }, + + { kPanelDivId, kRecordCheckId, "recordCheckId" }, + { kPanelDivId, kStartBtnId, "startBtnId" }, + { kPanelDivId, kStopBtnId, "stopBtnId" }, + { kPanelDivId, kClearBtnId, "clearBtnId" }, + { kPanelDivId, kMsgCntId, "msgCntId" }, + + { kPanelDivId, kSaveBtnId, "saveBtnId" }, + { kPanelDivId, kOpenBtnId, "openBtnId" }, + { kPanelDivId, kFnStringId, "filenameId" }, + + }; + + unsigned mapN = sizeof(mapA)/sizeof(mapA[0]); + + typedef struct am_audio_str + { + time::spec_t timestamp; + unsigned chCnt; + + + } am_audio_t; + + typedef struct am_midi_msg_str + { + unsigned devIdx; + unsigned portIdx; + time::spec_t timestamp; + uint8_t ch; + uint8_t status; + uint8_t d0; + uint8_t d1; + + unsigned microsec; + + } am_midi_msg_t; + + typedef struct app_str + { + const char* record_dir; + const char* record_fn; + const char* record_fn_ext; + am_midi_msg_t* midiMsgArray; + unsigned midiMsgArrayN; + unsigned midiMsgArrayInIdx; + unsigned midiMsgArrayOutIdx; + unsigned midi_timer_period_micro_sec; + + const char* midiOutDevLabel; + const char* midiOutPortLabel; + unsigned midiOutDevIdx; + unsigned midiOutPortIdx; + + time::spec_t play_time; + char* filename; + + time::spec_t start_time; + unsigned midiFilterCnt; + + bool recordFl; + bool startedFl; + + handle_t ioH; + } app_t; + + rc_t _parseCfg(app_t* app, const object_t* cfg ) + { + rc_t rc = kOkRC; + + if((rc = cfg->getv( + "record_dir", app->record_dir, + "record_fn", app->record_fn, + "record_fn_ext", app->record_fn_ext, + "max_midi_msg_count", app->midiMsgArrayN, + "midi_timer_period_micro_sec", app->midi_timer_period_micro_sec, + "midi_out_device", app->midiOutDevLabel, + "midi_out_port", app->midiOutPortLabel)) != kOkRC ) + { + rc = cwLogError(kSyntaxErrorRC,"Audio MIDI app configuration parse failed."); + } + + // allocate the MIDI msg buffer + app->midiMsgArray = mem::allocZ( app->midiMsgArrayN ); + + // verify that the output directory exists + filesys::makeDir(app->record_dir); + + return rc; + } + + void _free( app_t& app ) + { + mem::release(app.midiMsgArray); + mem::release(app.filename); + } + + rc_t _resolve_midi_device_port_index( app_t* app ) + { + rc_t rc = kOkRC; + + if((app->midiOutDevIdx = io::midiDeviceIndex(app->ioH,app->midiOutDevLabel)) == kInvalidIdx ) + { + rc = cwLogError(kInvalidArgRC,"The MIDI output device: '%s' was not found.", cwStringNullGuard(app->midiOutDevLabel) ); + } + + if((app->midiOutPortIdx = io::midiDevicePortIndex(app->ioH,app->midiOutDevIdx,false,app->midiOutPortLabel)) == kInvalidIdx ) + { + rc = cwLogError(kInvalidArgRC,"The MIDI output port: '%s' was not found.", cwStringNullGuard(app->midiOutPortLabel) ); + } + + printf("MIDI DEV: %i PORT:%i\n",app->midiOutDevIdx,app->midiOutPortIdx); + + return rc; + } + + void _set_midi_msg_next_index( app_t* app, unsigned next_idx ) + { + app->midiMsgArrayInIdx = next_idx; + + io::uiSendValue( app->ioH, kInvalidId, uiFindElementUuId(app->ioH,kMsgCntId), app->midiMsgArrayInIdx ); + + } + + void _set_midi_msg_next_play_index(app_t* app, unsigned next_idx) + { + app->midiMsgArrayOutIdx = next_idx; + } + + + void _on_start_btn( app_t* app ) + { + app->startedFl = true; + + time::get(app->start_time); + + if( app->recordFl ) + { + app->midiFilterCnt = 0; + + _set_midi_msg_next_index(app, 0 ); + } + else + { + _set_midi_msg_next_play_index(app,0); + io::timerStart( app->ioH, io::timerIdToIndex(app->ioH, kAmMidiTimerId) ); + time::get(app->play_time); + } + } + + void _on_stop_btn( app_t* app ) + { + app->startedFl = false; + + time::spec_t t1; + time::get(t1); + + if( app->recordFl ) + { + + // set the 'microsec' value for each MIDI msg + for(unsigned i=0; imidiMsgArrayInIdx; ++i) + { + app->midiMsgArray[i].microsec = time::elapsedMicros(app->midiMsgArray[0].timestamp,app->midiMsgArray[i].timestamp); + } + + cwLogInfo("MIDI messages recorded: %i filtered: %i\n",app->midiMsgArrayInIdx, app->midiFilterCnt ); + + } + else + { + io::timerStop( app->ioH, io::timerIdToIndex(app->ioH, kAmMidiTimerId) ); + } + + cwLogInfo("Runtime: %5.2f seconds.", time::elapsedMs(app->start_time,t1)/1000.0 ); + } + + rc_t _read_midi( app_t* app ) + { + rc_t rc = kOkRC; + char* fn = filesys::makeFn( app->record_dir, app->filename, NULL, NULL ); + file::handle_t fH; + unsigned n = 0; + + if((rc = file::open(fH,fn,file::kReadFl)) != kOkRC ) + { + rc = cwLogError(kOpenFailRC,"Unable to locate the file: '%s'.", fn ); + goto errLabel; + } + + if((rc = file::read(fH,n)) != kOkRC ) + { + rc = cwLogError(kReadFailRC,"Header read failed on Audio-MIDI file: '%s'.", fn ); + goto errLabel; + } + + if( n > app->midiMsgArrayN ) + { + cwLogWarning("The count of message in Audio-MIDI file '%s' reduced from %i to %i.", fn, n, app->midiMsgArrayN ); + n = app->midiMsgArrayN; + } + + if((rc = file::read(fH,app->midiMsgArray,n*sizeof(am_midi_msg_t))) != kOkRC ) + { + rc = cwLogError(kReadFailRC,"Data read failed on Audio-MIDI file: '%s'.", fn ); + goto errLabel; + } + + _set_midi_msg_next_index(app, n ); + + cwLogInfo("Read %i from '%s'.",n,fn); + + errLabel: + mem::release(fn); + + return rc; + } + + rc_t _write_midi( app_t* app ) + { + rc_t rc = kOkRC; + char* fn = nullptr; + file::handle_t fH; + + if( app->midiMsgArrayInIdx == 0 ) + { + cwLogWarning("Nothing to write."); + return rc; + } + + // form the filename + if((fn = filesys::makeVersionedFn( app->record_dir, app->record_fn, app->record_fn_ext, NULL )) == nullptr ) + { + rc = cwLogError(kOpFailRC,"Unable to form versioned filename in '%s' with prefix: '%s' and extension: '%s'.", + cwStringNullGuard(app->record_dir), + cwStringNullGuard(app->record_fn), + cwStringNullGuard(app->record_fn_ext)); + } + + // 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 header + if((rc = write(fH,app->midiMsgArrayInIdx)) != kOkRC ) + { + rc = cwLogError(kWriteFailRC,"Header write to '%s' failed.",cwStringNullGuard(fn)); + goto errLabel; + } + + // write the file data + if((rc = write(fH,app->midiMsgArray,sizeof(am_midi_msg_t)*app->midiMsgArrayInIdx)) != kOkRC ) + { + rc = cwLogError(kWriteFailRC,"Data write to '%s' failed.",cwStringNullGuard(fn)); + goto errLabel; + } + + // update UI msg count + io::uiSendValue( app->ioH, kInvalidId, uiFindElementUuId(app->ioH,kMsgCntId), app->midiMsgArrayInIdx ); + + file::close(fH); + + cwLogInfo("Saved %i events to '%s'.", app->midiMsgArrayInIdx, fn ); + + errLabel: + mem::release(fn); + return rc; + } + + void _print_midi_msg( const am_midi_msg_t* mm ) + { + printf("%i %i : %10i : %2i 0x%02x 0x%02x 0x%02x\n", mm->devIdx, mm->portIdx, mm->microsec, mm->ch, mm->status, mm->d0, mm->d1 ); + } + + void _report_midi( app_t* app ) + { + for(unsigned i=0; imidiMsgArrayInIdx; ++i) + { + am_midi_msg_t* mm = app->midiMsgArray + i; + _print_midi_msg(mm); + } + } + + rc_t _onUiInit(app_t* app, const ui_msg_t& m ) + { + rc_t rc = kOkRC; + + return rc; + } + + rc_t _onUiValue(app_t* app, const ui_msg_t& m ) + { + rc_t rc = kOkRC; + + switch( m.appId ) + { + case kQuitBtnId: + io::stop( app->ioH ); + break; + + case kIoReportBtnId: + io::report( app->ioH ); + break; + + case kReportBtnId: + _report_midi(app); + break; + + case kSaveBtnId: + _write_midi(app); + break; + + case kOpenBtnId: + printf("open btn\n"); + _read_midi(app); + break; + + case kRecordCheckId: + cwLogInfo("Record:%i",m.value->u.b); + app->recordFl = m.value->u.b; + break; + + case kStartBtnId: + cwLogInfo("Start"); + _on_start_btn(app); + break; + + case kStopBtnId: + cwLogInfo("Stop"); + _on_stop_btn(app); + break; + + case kClearBtnId: + cwLogInfo("Clear"); + _set_midi_msg_next_index(app, 0 ); + break; + + case kFnStringId: + mem::release(app->filename); + app->filename = mem::duplStr(m.value->u.s); + printf("filename:%s\n",app->filename); + break; + } + + return rc; + } + + rc_t _onUiEcho(app_t* app, const ui_msg_t& m ) + { + rc_t rc = kOkRC; + return rc; + } + + rc_t uiCb( app_t* app, const ui_msg_t& m ) + { + rc_t rc = kOkRC; + + switch( m.opId ) + { + case ui::kConnectOpId: + cwLogInfo("UI Connected: wsSessId:%i.",m.wsSessId); + break; + + case ui::kDisconnectOpId: + cwLogInfo("UI Disconnected: wsSessId:%i.",m.wsSessId); + break; + + case ui::kInitOpId: + _onUiInit(app,m); + break; + + case ui::kValueOpId: + _onUiValue( app, m ); + break; + + case ui::kEchoOpId: + _onUiEcho( app, m ); + break; + + case ui::kIdleOpId: + break; + + case ui::kInvalidOpId: + // fall through + default: + assert(0); + break; + + } + + return rc; + } + + + rc_t timerCb(app_t* app, 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( app->startedFl && (app->recordFl==false) && (app->midiMsgArrayOutIdx < app->midiMsgArrayInIdx)) + { + time::spec_t t; + time::get(t); + + unsigned cur_time_us = time::elapsedMicros(app->play_time,t); + + while( app->midiMsgArray[ app->midiMsgArrayOutIdx ].microsec <= cur_time_us ) + { + + am_midi_msg_t* mm = app->midiMsgArray + app->midiMsgArrayOutIdx; + + //_print_midi_msg(mm); + + io::midiDeviceSend( app->ioH, app->midiOutDevIdx, app->midiOutPortIdx, mm->status + mm->ch, mm->d0, mm->d1 ); + + _set_midi_msg_next_play_index(app, app->midiMsgArrayOutIdx+1 ); + + // if all MIDI messages have been played + if( app->midiMsgArrayOutIdx >= app->midiMsgArrayInIdx ) + { + _on_stop_btn(app); + break; + } + } + } + + return rc; + } + + bool _midi_filter( const midi::msg_t* mm ) + { + //bool drop_fl = (mm->status & 0xf0) == midi::kCtlMdId && (64 <= mm->d0) && (mm->d0 <= 67) && (mm->d1 < 25); + //return drop_fl; + return false; + } + + rc_t midiCb( app_t* app, const 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 ) + { + // this is a sys ex msg use: pkt->sysExMsg[j] + } + else // this is a triple + { + if( app->recordFl && app->startedFl ) + { + + // verify that space exists in the record buffer + if( app->midiMsgArrayInIdx >= app->midiMsgArrayN ) + { + _on_stop_btn(app); + rc = cwLogError(kBufTooSmallRC,"MIDI message record buffer is full. % messages.",app->midiMsgArrayN); + goto errLabel; + } + else + { + // copy the msg into the record buffer + am_midi_msg_t* am = app->midiMsgArray + app->midiMsgArrayInIdx; + midi::msg_t* mm = pkt->msgArray + j; + + if( _midi_filter(mm) ) + { + app->midiFilterCnt++; + } + else + { + am->devIdx = pkt->devIdx; + am->portIdx = pkt->portIdx; + am->timestamp = mm->timeStamp; + am->ch = mm->status & 0x0f; + am->status = mm->status & 0xf0; + am->d0 = mm->d0; + am->d1 = mm->d1; + + app->midiMsgArrayInIdx += 1; + + // send msg count + io::uiSendValue( app->ioH, kInvalidId, uiFindElementUuId(app->ioH,kMsgCntId), app->midiMsgArrayInIdx ); + } + } + } + } + + /* + if( pkt->msgArray == NULL ) + printf("io midi cb: 0x%x ",pkt->sysExMsg[j]); + else + { + if( !_midi_filter(pkt->msgArray + j) ) + printf("io midi cb: %ld %ld 0x%x %i %i\n", pkt->msgArray[j].timeStamp.tv_sec, pkt->msgArray[j].timeStamp.tv_nsec, pkt->msgArray[j].status, pkt->msgArray[j].d0, pkt->msgArray[j].d1); + } + */ + } + + errLabel: + return rc; + } + + rc_t audioCb( app_t* app, const audio_msg_t& m ) + { + rc_t rc = kOkRC; + + unsigned chN = std::min(m.iBufChCnt,m.oBufChCnt); + unsigned byteCnt = m.dspFrameCnt * sizeof(sample_t); + + // Copy the input to the output + for(unsigned i=0; i(arg); + + switch( m->tid ) + { + case kTimerTId: + if( m->u.timer != nullptr ) + rc = timerCb(app,*m->u.timer); + break; + + case kSerialTId: + break; + + case kMidiTId: + if( m->u.midi != nullptr ) + rc = midiCb(app,*m->u.midi); + break; + + case kAudioTId: + if( m->u.audio != nullptr ) + rc = audioCb(app,*m->u.audio); + break; + + case kAudioMeterTId: + break; + + case kSockTId: + break; + + case kWebSockTId: + break; + + case kUiTId: + rc = uiCb(app,m->u.ui); + break; + + default: + assert(0); + + } + + return rc; + } + + void _report( handle_t h ) + { + for(unsigned i=0; i + + + + UI Test App + + + + + + + + +
+

UI Test:

+

Disconnected

+
+ +
+
+ + + diff --git a/html/audio_midi/js/ui.js b/html/audio_midi/js/ui.js new file mode 100644 index 0000000..4f0b125 --- /dev/null +++ b/html/audio_midi/js/ui.js @@ -0,0 +1,906 @@ +var _ws = null; +var _rootId = "0"; +var _nextEleId = 0; +var _focusId = null; +var _focusVal = null; + +function set_app_title( suffix, className ) +{ + var ele = document.getElementById('connectTitleId'); + ele.innerHTML = suffix + ele.className = className +} + + +function uiOnError( msg, r) +{ + console.log("Error:" + msg); +} + +function uiGetParent( r ) +{ + parent_ele = document.getElementById(r.parent_id); + + if( parent_ele == null ) + { + uiOnError("Parent not found. parent_id:" + r.parent_id,r); + } + + return parent_ele; +} + +function uiCreateEle( r ) +{ + var parent_ele; + + if((parent_ele = uiGetParent(r)) != null ) + { + ele = document.createElement(r.ele_type) + ele.id = r.ele_id; + ele.className = r.value; + + parent_ele.appendChild(ele) + } +} + +function uiRemoveChildren( r ) +{ + ele = document.getElementById(r.ele_id) + + while (ele.firstChild) + { + ele.removeChild(ele.firstChild); + } +} + +function uiDivCreate( r ) +{ uiCreateEle(r) } + +function uiLabelCreate( r ) +{ + var parent_ele; + + if((parent_ele = uiGetParent(r)) != null ) + { + ele = document.createElement("label") + ele.htmlFor = r.ele_id + ele.innerHTML = r.value; + parent_ele.appendChild(ele) + } + +} + +function uiSelectCreate( r ) +{ + uiCreateEle(r) +} + +function uiSelectClear( r ) +{ uiRemoveChildren(r) } + +function uiSelectInsert( r ) +{ + var select_ele; + + if((select_ele = uiGetParent(r)) != null ) + { + var option = document.createElement('option'); + + option.id = r.ele_id; + option.innerHTML = r.value; + option.value = r.ele_id; + option.onclick = function() { uiOnSelectClick(this) } + + select_ele.appendChild(option) + } +} + +function uiSelectChoose( r ) +{ + var select_ele; + + if((select_ele = uiGetParent(r)) != null ) + { + if( select_ele.hasChildNodes()) + { + var children = select_ele.childNodes + for(var i=0; i 0) + { + label_ele = dom_create_ele("label"); + + label_ele.innerHTML = label; + + div_ele.appendChild(label_ele) + } + } + + return ui_create_ele( div_ele, ele_type, d, dfltEleClassName ); +} + +function ui_create_div( parent_ele, d ) +{ + var div_ele = ui_create_ele( parent_ele, "div", d, "uiDiv" ); + + if( div_ele != null ) + { + + if( d.title != null ) + { + var title = d.title //.trim() + + if( title.length > 0 ) + { + var p_ele = dom_create_ele("p") + + p_ele.innerHTML = title + + div_ele.appendChild( p_ele ) + } + } + } + + return div_ele; +} + +function ui_create_panel_div( parent_ele, d ) +{ + d.type = "div" + var div_ele = ui_create_div( parent_ele, d ); + + if( !d.hasOwnProperty('className') ) + div_ele.className = "uiPanel" + + return div_ele +} + +function ui_create_row_div( parent_ele, d ) +{ + d.type = "div" + var div_ele = ui_create_div( parent_ele, d ); + + if( !d.hasOwnProperty('className') ) + div_ele.className = "uiRow" + + return div_ele +} + +function ui_create_col_div( parent_ele, d ) +{ + d.type = "div" + var div_ele = ui_create_div( parent_ele, d ); + + if( !d.hasOwnProperty('className') ) + div_ele.className = "uiCol" + + return div_ele +} + + +function ui_create_title( parent_ele, d ) +{ + var ele = ui_create_ele( parent_ele, "label", d, "uiTitle" ); + + if( ele != null ) + { + ele.innerHTML = d.title; + } + + return ele; +} + +function ui_create_button( parent_ele, d ) +{ + var ele = ui_create_ctl( parent_ele, "button", null, d, "uiButton" ); + + if( ele != null ) + { + ele.innerHTML = d.title; + ele.onclick = function() { ui_send_int_value(this,1); } + } + + return ele; +} + +function ui_create_check( parent_ele, d ) +{ + var ele = ui_create_ctl( parent_ele, "input", d.title, d, "uiCheck" ) + + if( ele != null ) + { + ele.type = "checkbox"; + + ele.onclick = function() { ui_send_bool_value(this,dom_get_checkbox(this.id)); } + + if( !d.hasOwnProperty('value') ) + ui_send_echo(ele) + else + { + dom_set_checkbox(ele.id, d.value ); + ui_send_bool_value(ele,dom_get_checkbox(ele.id)) + } + + } + return ele; +} + +// +// Note: The value of a 'select' widget is always set by the 'appId' +// of the selected 'option'. Likewise the 'appId' of the selected +// option is returned as the value of the select widget. +// +function ui_on_select( ele ) +{ + ui_send_int_value(ele,ele.options[ ele.selectedIndex ].appId); +} + +function ui_select_set_from_option_app_id( sel_ele, option_appId ) +{ + var i; + for(i=0; i