cwIoPresetSelApp, cwPianoScore: Initial commit.
This commit is contained in:
parent
0a8641e1e5
commit
7a01d29210
@ -62,6 +62,9 @@ libcwSRC += src/libcw/cwIo.cpp src/libcw/cwIoTest.cpp src/libcw/cwIoSocketChat
|
|||||||
libcwHDR += src/libcw/cwIoMidiRecordPlay.h src/libcw/cwIoAudioRecordPlay.h src/libcw/cwIoAudioMidiApp.h
|
libcwHDR += src/libcw/cwIoMidiRecordPlay.h src/libcw/cwIoAudioRecordPlay.h src/libcw/cwIoAudioMidiApp.h
|
||||||
libcwSRC += src/libcw/cwIoMidiRecordPlay.cpp src/libcw/cwIoAudioRecordPlay.cpp src/libcw/cwIoAudioMidiApp.cpp
|
libcwSRC += src/libcw/cwIoMidiRecordPlay.cpp src/libcw/cwIoAudioRecordPlay.cpp src/libcw/cwIoAudioMidiApp.cpp
|
||||||
|
|
||||||
|
libcwHDR += src/libcw/cwIoPresetSelApp.h src/libcw/cwPianoScore.h
|
||||||
|
libcwSRC += src/libcw/cwIoPresetSelApp.cpp src/libcw/cwPianoScore.cpp
|
||||||
|
|
||||||
endif
|
endif
|
||||||
|
|
||||||
|
|
||||||
|
602
cwIoPresetSelApp.cpp
Normal file
602
cwIoPresetSelApp.cpp
Normal file
@ -0,0 +1,602 @@
|
|||||||
|
#include "cwCommon.h"
|
||||||
|
#include "cwLog.h"
|
||||||
|
#include "cwCommonImpl.h"
|
||||||
|
#include "cwMem.h"
|
||||||
|
#include "cwObject.h"
|
||||||
|
#include "cwText.h"
|
||||||
|
#include "cwFileSys.h"
|
||||||
|
#include "cwFile.h"
|
||||||
|
#include "cwTime.h"
|
||||||
|
#include "cwMidiDecls.h"
|
||||||
|
#include "cwMidi.h"
|
||||||
|
#include "cwUiDecls.h"
|
||||||
|
#include "cwIo.h"
|
||||||
|
#include "cwIoMidiRecordPlay.h"
|
||||||
|
#include "cwIoPresetSelApp.h"
|
||||||
|
#include "cwPianoScore.h"
|
||||||
|
|
||||||
|
namespace cw
|
||||||
|
{
|
||||||
|
namespace preset_sel_app
|
||||||
|
{
|
||||||
|
// Application Id's for UI elements
|
||||||
|
enum
|
||||||
|
{
|
||||||
|
// Resource Based elements
|
||||||
|
kPanelDivId = 1000,
|
||||||
|
kQuitBtnId,
|
||||||
|
kIoReportBtnId,
|
||||||
|
kReportBtnId,
|
||||||
|
|
||||||
|
kStartBtnId,
|
||||||
|
kStopBtnId,
|
||||||
|
|
||||||
|
kMidiThruCheckId,
|
||||||
|
kCurMidiEvtCntId,
|
||||||
|
kTotalMidiEvtCntId,
|
||||||
|
|
||||||
|
kCurAudioSecsId,
|
||||||
|
kTotalAudioSecsId,
|
||||||
|
|
||||||
|
kSaveBtnId,
|
||||||
|
kLoadBtnId,
|
||||||
|
kFnStringId,
|
||||||
|
|
||||||
|
kLocNumbId
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
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, kStartBtnId, "startBtnId" },
|
||||||
|
{ kPanelDivId, kStopBtnId, "stopBtnId" },
|
||||||
|
|
||||||
|
{ kPanelDivId, kMidiThruCheckId, "midiThruCheckId" },
|
||||||
|
{ kPanelDivId, kCurMidiEvtCntId, "curMidiEvtCntId" },
|
||||||
|
{ kPanelDivId, kTotalMidiEvtCntId, "totalMidiEvtCntId" },
|
||||||
|
|
||||||
|
{ kPanelDivId, kCurAudioSecsId, "curAudioSecsId" },
|
||||||
|
{ kPanelDivId, kTotalAudioSecsId, "totalAudioSecsId" },
|
||||||
|
|
||||||
|
|
||||||
|
{ kPanelDivId, kSaveBtnId, "saveBtnId" },
|
||||||
|
{ kPanelDivId, kLoadBtnId, "loadBtnId" },
|
||||||
|
{ kPanelDivId, kFnStringId, "filenameId" },
|
||||||
|
|
||||||
|
{ kPanelDivId, kLocNumbId, "locNumbId" }
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
unsigned mapN = sizeof(mapA)/sizeof(mapA[0]);
|
||||||
|
|
||||||
|
typedef struct loc_map_str
|
||||||
|
{
|
||||||
|
unsigned loc;
|
||||||
|
time::spec_t timestamp;
|
||||||
|
} loc_map_t;
|
||||||
|
|
||||||
|
typedef struct app_str
|
||||||
|
{
|
||||||
|
io::handle_t ioH;
|
||||||
|
|
||||||
|
const char* record_dir;
|
||||||
|
const char* record_folder;
|
||||||
|
const char* record_fn_ext;
|
||||||
|
char* directory;
|
||||||
|
const char* scoreFn;
|
||||||
|
|
||||||
|
midi_record_play::handle_t mrpH;
|
||||||
|
//audio_record_play::handle_t arpH;
|
||||||
|
|
||||||
|
score::handle_t scoreH;
|
||||||
|
loc_map_t* locMap;
|
||||||
|
unsigned locMapN;
|
||||||
|
|
||||||
|
} app_t;
|
||||||
|
|
||||||
|
rc_t _parseCfg(app_t* app, const object_t* cfg )
|
||||||
|
{
|
||||||
|
rc_t rc = kOkRC;
|
||||||
|
|
||||||
|
const object_t* params = nullptr;
|
||||||
|
|
||||||
|
if((rc = cfg->getv( "params", params)) != kOkRC )
|
||||||
|
{
|
||||||
|
rc = cwLogError(kSyntaxErrorRC,"Preset Select App 'params' cfg record not found.");
|
||||||
|
goto errLabel;
|
||||||
|
}
|
||||||
|
|
||||||
|
if((rc = params->getv( "record_dir", app->record_dir,
|
||||||
|
"record_folder", app->record_folder,
|
||||||
|
"record_fn_ext", app->record_fn_ext,
|
||||||
|
"score_fn", app->scoreFn )) != kOkRC )
|
||||||
|
{
|
||||||
|
rc = cwLogError(kSyntaxErrorRC,"Preset Select App configuration parse failed.");
|
||||||
|
}
|
||||||
|
|
||||||
|
// verify that the output directory exists
|
||||||
|
if((rc = filesys::isDir(app->record_dir)) != kOkRC )
|
||||||
|
if((rc = filesys::makeDir(app->record_dir)) != kOkRC )
|
||||||
|
rc = cwLogError(rc,"Unable to create the base output directory:%s.",cwStringNullGuard(app->record_dir));
|
||||||
|
|
||||||
|
|
||||||
|
errLabel:
|
||||||
|
|
||||||
|
return rc;
|
||||||
|
}
|
||||||
|
|
||||||
|
rc_t _free( app_t& app )
|
||||||
|
{
|
||||||
|
mem::release(app.locMap);
|
||||||
|
mem::release(app.directory);
|
||||||
|
return kOkRC;
|
||||||
|
}
|
||||||
|
|
||||||
|
char* _form_versioned_directory(app_t* app)
|
||||||
|
{
|
||||||
|
char* dir = nullptr;
|
||||||
|
|
||||||
|
for(unsigned version_numb=0; true; ++version_numb)
|
||||||
|
{
|
||||||
|
unsigned n = textLength(app->record_folder) + 32;
|
||||||
|
char folder[n+1];
|
||||||
|
|
||||||
|
snprintf(folder,n,"%s_%i",app->record_folder,version_numb);
|
||||||
|
|
||||||
|
if((dir = filesys::makeFn(app->record_dir,folder, NULL, NULL)) == nullptr )
|
||||||
|
{
|
||||||
|
cwLogError(kOpFailRC,"Unable to form a versioned directory from:'%s'",cwStringNullGuard(app->record_dir));
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
if( !filesys::isDir(dir) )
|
||||||
|
break;
|
||||||
|
|
||||||
|
mem::release(dir);
|
||||||
|
}
|
||||||
|
|
||||||
|
return dir;
|
||||||
|
}
|
||||||
|
|
||||||
|
void _midi_play_callback( void* arg, unsigned id, const time::spec_t timestamp, uint8_t ch, uint8_t status, uint8_t d0, uint8_t d1 )
|
||||||
|
{
|
||||||
|
app_t* app = (app_t*)arg;
|
||||||
|
const unsigned buf_byte_cnt = 256;
|
||||||
|
char buf[ buf_byte_cnt ];
|
||||||
|
if( id != kInvalidId )
|
||||||
|
{
|
||||||
|
event_to_string( app->scoreH, id, buf, buf_byte_cnt );
|
||||||
|
printf("%s\n",buf);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
rc_t _on_ui_save( app_t* app )
|
||||||
|
{
|
||||||
|
rc_t rc0 = kOkRC;
|
||||||
|
rc_t rc1 = kOkRC;
|
||||||
|
char* dir = nullptr;
|
||||||
|
char* fn = nullptr;
|
||||||
|
|
||||||
|
if((dir = _form_versioned_directory(app)) == nullptr )
|
||||||
|
return cwLogError(kOpFailRC,"Unable to form the versioned directory string.");
|
||||||
|
|
||||||
|
if( !filesys::isDir(dir) )
|
||||||
|
if((rc0 = filesys::makeDir(dir)) != kOkRC )
|
||||||
|
{
|
||||||
|
rc0 = cwLogError(rc0,"Attempt to create directory: '%s' failed.", cwStringNullGuard(dir));
|
||||||
|
goto errLabel;
|
||||||
|
}
|
||||||
|
|
||||||
|
if((fn = filesys::makeFn(dir,"midi","am",nullptr)) != nullptr )
|
||||||
|
{
|
||||||
|
if((rc0 = midi_record_play::save( app->mrpH, fn )) != kOkRC )
|
||||||
|
rc0 = cwLogError(rc0,"MIDI file '%s' save failed.",fn);
|
||||||
|
|
||||||
|
mem::release(fn);
|
||||||
|
}
|
||||||
|
|
||||||
|
if((fn = filesys::makeFn(dir,"audio","wav",nullptr)) != nullptr )
|
||||||
|
{
|
||||||
|
//if((rc1 = audio_record_play::save( app->arpH, fn )) != kOkRC )
|
||||||
|
// rc1 = cwLogError(rc1,"Audio file '%s' save failed.",fn);
|
||||||
|
|
||||||
|
mem::release(fn);
|
||||||
|
}
|
||||||
|
|
||||||
|
errLabel:
|
||||||
|
mem::release(dir);
|
||||||
|
|
||||||
|
return rcSelect(rc0,rc1);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
rc_t _on_ui_load( app_t* app )
|
||||||
|
{
|
||||||
|
rc_t rc = kOkRC;
|
||||||
|
const score::event_t* e = nullptr;
|
||||||
|
unsigned midiEventN = 0;
|
||||||
|
midi_record_play::midi_msg_t* m = nullptr;
|
||||||
|
|
||||||
|
printf("Loading\n");
|
||||||
|
|
||||||
|
// create the score
|
||||||
|
if((rc = score::create( app->scoreH, app->scoreFn )) != kOkRC )
|
||||||
|
{
|
||||||
|
cwLogError(rc,"Score create failed on '%s'.",app->scoreFn);
|
||||||
|
goto errLabel;
|
||||||
|
}
|
||||||
|
|
||||||
|
// get the count of MIDI events
|
||||||
|
if((e = score::base_event( app->scoreH )) != nullptr )
|
||||||
|
for(; e!=nullptr; e=e->link)
|
||||||
|
if( e->status != 0 )
|
||||||
|
midiEventN += 1;
|
||||||
|
|
||||||
|
// copy the MIDI events
|
||||||
|
if((e = score::base_event( app->scoreH )) != nullptr )
|
||||||
|
{
|
||||||
|
|
||||||
|
// allocate the locMap[]
|
||||||
|
mem::free(app->locMap);
|
||||||
|
app->locMap = mem::allocZ<loc_map_t>( midiEventN );
|
||||||
|
app->locMapN = midiEventN;
|
||||||
|
|
||||||
|
// allocate the the player msg array
|
||||||
|
m = mem::allocZ<midi_record_play::midi_msg_t>( midiEventN );
|
||||||
|
|
||||||
|
// load the player msg array
|
||||||
|
for(unsigned i=0; e!=nullptr && i<midiEventN; e=e->link)
|
||||||
|
if( e->status != 0 )
|
||||||
|
{
|
||||||
|
time::millisecondsToSpec(m[i].timestamp, (unsigned)(e->sec*1000) );
|
||||||
|
m[i].ch = e->status & 0x0f;
|
||||||
|
m[i].status = e->status & 0xf0;
|
||||||
|
m[i].d0 = e->d0;
|
||||||
|
m[i].d1 = e->d1;
|
||||||
|
m[i].id = e->uid;
|
||||||
|
|
||||||
|
app->locMap[i].loc = e->loc;
|
||||||
|
app->locMap[i].timestamp = m[i].timestamp;
|
||||||
|
|
||||||
|
++i;
|
||||||
|
}
|
||||||
|
|
||||||
|
// load the player with the msg list
|
||||||
|
if((rc = midi_record_play::load( app->mrpH, m, midiEventN )) != kOkRC )
|
||||||
|
{
|
||||||
|
cwLogError(rc,"MIDI player load failed.");
|
||||||
|
goto errLabel;
|
||||||
|
}
|
||||||
|
|
||||||
|
mem::free(m);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
cwLogInfo("'%s' loaded.",app->scoreFn);
|
||||||
|
|
||||||
|
errLabel:
|
||||||
|
|
||||||
|
io::uiSendValue( app->ioH, kInvalidId, uiFindElementUuId(app->ioH,kCurMidiEvtCntId), midi_record_play::event_index(app->mrpH) );
|
||||||
|
io::uiSendValue( app->ioH, kInvalidId, uiFindElementUuId(app->ioH,kTotalMidiEvtCntId), midi_record_play::event_count(app->mrpH) );
|
||||||
|
|
||||||
|
return rc;
|
||||||
|
}
|
||||||
|
|
||||||
|
rc_t _on_ui_start( app_t* app )
|
||||||
|
{
|
||||||
|
rc_t rc=kOkRC;
|
||||||
|
|
||||||
|
if((rc = midi_record_play::start(app->mrpH)) != kOkRC )
|
||||||
|
{
|
||||||
|
rc = cwLogError(rc,"MIDI start failed.");
|
||||||
|
goto errLabel;
|
||||||
|
}
|
||||||
|
|
||||||
|
//if((rc = audio_record_play::start(app->arpH)) != kOkRC )
|
||||||
|
//{
|
||||||
|
// rc = cwLogError(rc,"Audio start failed.");
|
||||||
|
// goto errLabel;
|
||||||
|
//}
|
||||||
|
|
||||||
|
io::uiSendValue( app->ioH, kInvalidId, uiFindElementUuId(app->ioH,kCurMidiEvtCntId), midi_record_play::event_index(app->mrpH) );
|
||||||
|
//io::uiSendValue( app->ioH, kInvalidId, uiFindElementUuId(app->ioH,kCurAudioSecsId), audio_record_play::current_loc_seconds(app->arpH) );
|
||||||
|
|
||||||
|
errLabel:
|
||||||
|
return rc;
|
||||||
|
}
|
||||||
|
|
||||||
|
rc_t _on_ui_stop( app_t* app )
|
||||||
|
{
|
||||||
|
rc_t rc = kOkRC;
|
||||||
|
|
||||||
|
if((rc = midi_record_play::stop(app->mrpH)) != kOkRC )
|
||||||
|
{
|
||||||
|
rc = cwLogError(rc,"MIDI start failed.");
|
||||||
|
goto errLabel;
|
||||||
|
}
|
||||||
|
|
||||||
|
//if((rc = audio_record_play::stop(app->arpH)) != kOkRC )
|
||||||
|
//{
|
||||||
|
// rc = cwLogError(rc,"Audio start failed.");
|
||||||
|
// goto errLabel;
|
||||||
|
//}
|
||||||
|
|
||||||
|
errLabel:
|
||||||
|
return rc;
|
||||||
|
}
|
||||||
|
|
||||||
|
rc_t _set_midi_thru_state( app_t* app, bool thru_fl )
|
||||||
|
{
|
||||||
|
rc_t rc = kOkRC;
|
||||||
|
|
||||||
|
if((rc = midi_record_play::set_thru_state(app->mrpH,thru_fl)) != kOkRC )
|
||||||
|
rc = cwLogError(rc,"%s MIDI thru state failed.",thru_fl ? "Enable" : "Disable" );
|
||||||
|
|
||||||
|
return rc;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
rc_t _on_ui_loc(app_t* app, unsigned loc)
|
||||||
|
{
|
||||||
|
rc_t rc = kOkRC;
|
||||||
|
unsigned i=0;
|
||||||
|
for(; i<app->locMapN; ++i)
|
||||||
|
{
|
||||||
|
if( app->locMap[i].loc == loc )
|
||||||
|
{
|
||||||
|
//
|
||||||
|
if((rc = midi_record_play::seek( app->mrpH, app->locMap[i].timestamp )) != kOkRC )
|
||||||
|
{
|
||||||
|
rc = cwLogError(rc,"MIDI seek failed.");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
start( app->mrpH, false );
|
||||||
|
io::uiSendValue( app->ioH, kInvalidId, uiFindElementUuId(app->ioH,kCurMidiEvtCntId), midi_record_play::event_index(app->mrpH) );
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if( i == app->locMapN )
|
||||||
|
rc = cwLogError(kOpFailRC,"The location '%i' could not be found.",loc);
|
||||||
|
|
||||||
|
return rc;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
rc_t _onUiInit(app_t* app, const io::ui_msg_t& m )
|
||||||
|
{
|
||||||
|
rc_t rc = kOkRC;
|
||||||
|
|
||||||
|
return rc;
|
||||||
|
}
|
||||||
|
|
||||||
|
rc_t _onUiValue(app_t* app, const io::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:
|
||||||
|
break;
|
||||||
|
|
||||||
|
case kSaveBtnId:
|
||||||
|
_on_ui_save(app);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case kLoadBtnId:
|
||||||
|
_on_ui_load(app);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case kMidiThruCheckId:
|
||||||
|
cwLogInfo("MIDI thru:%i",m.value->u.b);
|
||||||
|
_set_midi_thru_state(app, m.value->u.b);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case kStartBtnId:
|
||||||
|
_on_ui_start(app);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case kStopBtnId:
|
||||||
|
_on_ui_stop(app);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case kFnStringId:
|
||||||
|
mem::release(app->directory);
|
||||||
|
app->directory = mem::duplStr(m.value->u.s);
|
||||||
|
//printf("filename:%s\n",app->directory);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case kLocNumbId:
|
||||||
|
_on_ui_loc(app, m.value->u.i);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return rc;
|
||||||
|
}
|
||||||
|
|
||||||
|
rc_t _onUiEcho(app_t* app, const io::ui_msg_t& m )
|
||||||
|
{
|
||||||
|
rc_t rc = kOkRC;
|
||||||
|
return rc;
|
||||||
|
}
|
||||||
|
|
||||||
|
rc_t _ui_callback( app_t* app, const io::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;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The main application callback
|
||||||
|
rc_t _io_callback( void* arg, const io::msg_t* m )
|
||||||
|
{
|
||||||
|
rc_t rc = kOkRC;
|
||||||
|
app_t* app = reinterpret_cast<app_t*>(arg);
|
||||||
|
|
||||||
|
if( app->mrpH.isValid() )
|
||||||
|
{
|
||||||
|
midi_record_play::exec( app->mrpH, *m );
|
||||||
|
if( midi_record_play::is_started(app->mrpH) )
|
||||||
|
io::uiSendValue( app->ioH, kInvalidId, uiFindElementUuId(app->ioH,kCurMidiEvtCntId), midi_record_play::event_index(app->mrpH) );
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
if( app->arpH.isValid() )
|
||||||
|
{
|
||||||
|
audio_record_play::exec( app->arpH, *m );
|
||||||
|
if( audio_record_play::is_started(app->arpH) )
|
||||||
|
io::uiSendValue( app->ioH, kInvalidId, uiFindElementUuId(app->ioH,kCurAudioSecsId), audio_record_play::current_loc_seconds(app->arpH) );
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
|
switch( m->tid )
|
||||||
|
{
|
||||||
|
case io::kTimerTId:
|
||||||
|
break;
|
||||||
|
|
||||||
|
case io::kSerialTId:
|
||||||
|
break;
|
||||||
|
|
||||||
|
case io::kMidiTId:
|
||||||
|
break;
|
||||||
|
|
||||||
|
case io::kAudioTId:
|
||||||
|
break;
|
||||||
|
|
||||||
|
case io::kAudioMeterTId:
|
||||||
|
break;
|
||||||
|
|
||||||
|
case io::kSockTId:
|
||||||
|
break;
|
||||||
|
|
||||||
|
case io::kWebSockTId:
|
||||||
|
break;
|
||||||
|
|
||||||
|
case io::kUiTId:
|
||||||
|
rc = _ui_callback(app,m->u.ui);
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
assert(0);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
return rc;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
cw::rc_t cw::preset_sel_app::main( const object_t* cfg )
|
||||||
|
{
|
||||||
|
rc_t rc;
|
||||||
|
app_t app = {};
|
||||||
|
|
||||||
|
// Parse the configuration
|
||||||
|
if((rc = _parseCfg(&app,cfg)) != kOkRC )
|
||||||
|
goto errLabel;
|
||||||
|
|
||||||
|
// create the io framework instance
|
||||||
|
if((rc = io::create(app.ioH,cfg,_io_callback,&app,mapA,mapN)) != kOkRC )
|
||||||
|
{
|
||||||
|
rc = cwLogError(kOpFailRC,"IO Framework create failed.");
|
||||||
|
goto errLabel;
|
||||||
|
}
|
||||||
|
|
||||||
|
// create the MIDI record-play object
|
||||||
|
if((rc = midi_record_play::create(app.mrpH,app.ioH,*cfg,_midi_play_callback,&app)) != kOkRC )
|
||||||
|
{
|
||||||
|
rc = cwLogError(rc,"MIDI record-play object create failed.");
|
||||||
|
goto errLabel;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
// create the audio record-play object
|
||||||
|
if((rc = audio_record_play::create(app.arpH,app.ioH,*cfg)) != kOkRC )
|
||||||
|
{
|
||||||
|
rc = cwLogError(rc,"Audio record-play object create failed.");
|
||||||
|
goto errLabel;
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
|
// start the io framework instance
|
||||||
|
if((rc = io::start(app.ioH)) != kOkRC )
|
||||||
|
{
|
||||||
|
rc = cwLogError(rc,"Preset-select app start failed.");
|
||||||
|
goto errLabel;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// execute the io framework
|
||||||
|
while( !isShuttingDown(app.ioH))
|
||||||
|
{
|
||||||
|
exec(app.ioH);
|
||||||
|
sleepMs(50);
|
||||||
|
}
|
||||||
|
|
||||||
|
errLabel:
|
||||||
|
_free(app);
|
||||||
|
io::destroy(app.ioH);
|
||||||
|
printf("Preset-select Done.\n");
|
||||||
|
return rc;
|
||||||
|
|
||||||
|
}
|
13
cwIoPresetSelApp.h
Normal file
13
cwIoPresetSelApp.h
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
#ifndef cwIoPresetSelApp_h
|
||||||
|
#define cwIoPresetSelApp_h
|
||||||
|
|
||||||
|
namespace cw
|
||||||
|
{
|
||||||
|
namespace preset_sel_app
|
||||||
|
{
|
||||||
|
rc_t main( const object_t* cfg );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#endif
|
234
cwPianoScore.cpp
Normal file
234
cwPianoScore.cpp
Normal file
@ -0,0 +1,234 @@
|
|||||||
|
#include "cwCommon.h"
|
||||||
|
#include "cwLog.h"
|
||||||
|
#include "cwCommonImpl.h"
|
||||||
|
#include "cwMem.h"
|
||||||
|
#include "cwObject.h"
|
||||||
|
#include "cwPianoScore.h"
|
||||||
|
#include "cwMidi.h"
|
||||||
|
|
||||||
|
namespace cw
|
||||||
|
{
|
||||||
|
namespace score
|
||||||
|
{
|
||||||
|
typedef struct score_str
|
||||||
|
{
|
||||||
|
event_t* base;
|
||||||
|
event_t* end;
|
||||||
|
} score_t;
|
||||||
|
|
||||||
|
score_t* _handleToPtr(handle_t h)
|
||||||
|
{
|
||||||
|
return handleToPtr<handle_t,score_t>(h);
|
||||||
|
}
|
||||||
|
|
||||||
|
rc_t _destroy( score_t* p )
|
||||||
|
{
|
||||||
|
rc_t rc = kOkRC;
|
||||||
|
event_t* e = p->base;
|
||||||
|
while( e != nullptr )
|
||||||
|
{
|
||||||
|
event_t* e0 = e->link;
|
||||||
|
|
||||||
|
mem::free(e);
|
||||||
|
|
||||||
|
e = e0;
|
||||||
|
}
|
||||||
|
return rc;
|
||||||
|
}
|
||||||
|
|
||||||
|
rc_t _parse_event_list( score_t* p, const object_t* cfg )
|
||||||
|
{
|
||||||
|
rc_t rc;
|
||||||
|
const object_t* eventL;
|
||||||
|
|
||||||
|
if((rc = cfg->getv( "evtL", eventL )) != kOkRC || eventL->is_list()==false )
|
||||||
|
rc = cwLogError( rc, "Unable to locate the 'evtL' configuration tag.");
|
||||||
|
else
|
||||||
|
{
|
||||||
|
unsigned eventN = eventL->child_count();
|
||||||
|
|
||||||
|
for(unsigned i=0; i<eventN; ++i)
|
||||||
|
{
|
||||||
|
const char* sci_pitch = nullptr;
|
||||||
|
const char* dmark = nullptr;
|
||||||
|
const char* grace_mark = nullptr;
|
||||||
|
|
||||||
|
const object_t* evt_cfg = eventL->child_ele(i);
|
||||||
|
|
||||||
|
event_t* e = mem::allocZ<event_t>();
|
||||||
|
|
||||||
|
if((rc = evt_cfg->getv( "meas", e->meas,
|
||||||
|
"voice", e->voice,
|
||||||
|
"loc", e->loc,
|
||||||
|
"tick", e->tick,
|
||||||
|
"sec", e->sec )) != kOkRC )
|
||||||
|
{
|
||||||
|
rc = cwLogError(rc,"Score parse failed on required event fields at event index:%i.",i);
|
||||||
|
goto errLabel;
|
||||||
|
}
|
||||||
|
|
||||||
|
if((rc = evt_cfg->getv_opt( "rval", e->rval,
|
||||||
|
"sci_pitch", sci_pitch,
|
||||||
|
"dmark", dmark,
|
||||||
|
"dlevel", e->dlevel,
|
||||||
|
"status", e->status,
|
||||||
|
"d0", e->d0,
|
||||||
|
"d1", e->d1,
|
||||||
|
"grace", grace_mark,
|
||||||
|
"section", e->section,
|
||||||
|
"bpm", e->bpm,
|
||||||
|
"bar", e->bar)) != kOkRC )
|
||||||
|
{
|
||||||
|
rc = cwLogError(rc,"Score parse failed on optional event fields at event index:%i.",i);
|
||||||
|
goto errLabel;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
if( sci_pitch != nullptr )
|
||||||
|
memcpy(e->sci_pitch,sci_pitch,sizeof(e->sci_pitch));
|
||||||
|
|
||||||
|
if( dmark != nullptr )
|
||||||
|
memcpy(e->dmark,dmark,sizeof(e->dmark));
|
||||||
|
|
||||||
|
if( grace_mark != nullptr )
|
||||||
|
memcpy(e->grace_mark,grace_mark,sizeof(e->grace_mark));
|
||||||
|
|
||||||
|
// assign the UID
|
||||||
|
e->uid = i;
|
||||||
|
|
||||||
|
// link the event into the event list
|
||||||
|
if( p->end != nullptr )
|
||||||
|
p->end->link = e;
|
||||||
|
else
|
||||||
|
p->base = e;
|
||||||
|
|
||||||
|
p->end = e;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
errLabel:
|
||||||
|
return rc;
|
||||||
|
}
|
||||||
|
|
||||||
|
const event_t* _uid_to_event( score_t* p, unsigned uid )
|
||||||
|
{
|
||||||
|
const event_t* e = p->base;
|
||||||
|
for(; e!=nullptr; e=e->link)
|
||||||
|
if( e->uid == uid )
|
||||||
|
return e;
|
||||||
|
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
cw::rc_t cw::score::create( handle_t& hRef, const char* fn )
|
||||||
|
{
|
||||||
|
rc_t rc;
|
||||||
|
object_t* cfg = nullptr;
|
||||||
|
|
||||||
|
if((rc = destroy(hRef)) != kOkRC )
|
||||||
|
return rc;
|
||||||
|
|
||||||
|
// parse the cfg file
|
||||||
|
if((rc = objectFromFile( fn, cfg )) != kOkRC )
|
||||||
|
{
|
||||||
|
rc = cwLogError(rc,"Score parse failed on file: '%s'.", fn);
|
||||||
|
goto errLabel;
|
||||||
|
}
|
||||||
|
|
||||||
|
rc = create(hRef,cfg);
|
||||||
|
|
||||||
|
errLabel:
|
||||||
|
return rc;
|
||||||
|
}
|
||||||
|
|
||||||
|
cw::rc_t cw::score::create( handle_t& hRef, const object_t* cfg )
|
||||||
|
{
|
||||||
|
rc_t rc;
|
||||||
|
if((rc = destroy(hRef)) != kOkRC )
|
||||||
|
return rc;
|
||||||
|
|
||||||
|
score_t* p = mem::allocZ< score_t >();
|
||||||
|
|
||||||
|
// parse the event list
|
||||||
|
if((rc = _parse_event_list(p, cfg)) != kOkRC )
|
||||||
|
{
|
||||||
|
rc = cwLogError(rc,"Score event list parse failed.");
|
||||||
|
goto errLabel;
|
||||||
|
}
|
||||||
|
|
||||||
|
hRef.set(p);
|
||||||
|
|
||||||
|
errLabel:
|
||||||
|
if( rc != kOkRC )
|
||||||
|
destroy(hRef);
|
||||||
|
|
||||||
|
return rc;
|
||||||
|
}
|
||||||
|
|
||||||
|
cw::rc_t cw::score::destroy( handle_t& hRef )
|
||||||
|
{
|
||||||
|
rc_t rc = kOkRC;
|
||||||
|
|
||||||
|
if( !hRef.isValid() )
|
||||||
|
return rc;
|
||||||
|
|
||||||
|
score_t* p = _handleToPtr(hRef);
|
||||||
|
|
||||||
|
if((rc = _destroy(p)) != kOkRC )
|
||||||
|
return rc;
|
||||||
|
|
||||||
|
mem::release(p);
|
||||||
|
hRef.clear();
|
||||||
|
|
||||||
|
return rc;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
unsigned cw::score::event_count( handle_t h )
|
||||||
|
{
|
||||||
|
score_t* p = _handleToPtr(h);
|
||||||
|
unsigned n = 0;
|
||||||
|
for(event_t* e=p->base; e!=nullptr; e=e->link)
|
||||||
|
++n;
|
||||||
|
|
||||||
|
return n;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
const cw::score::event_t* cw::score::base_event( handle_t h )
|
||||||
|
{
|
||||||
|
score_t* p = _handleToPtr(h);
|
||||||
|
return p->base;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
cw::rc_t cw::score::event_to_string( handle_t h, unsigned uid, char* buf, unsigned buf_byte_cnt )
|
||||||
|
{
|
||||||
|
score_t* p = _handleToPtr(h);
|
||||||
|
const event_t* e = nullptr;
|
||||||
|
rc_t rc = kOkRC;
|
||||||
|
|
||||||
|
if((e = _uid_to_event( p, uid )) == nullptr )
|
||||||
|
rc = cwLogError(kInvalidIdRC,"A score event with uid=%i does not exist.",uid);
|
||||||
|
else
|
||||||
|
{
|
||||||
|
const char* sci_pitch = strlen(e->sci_pitch) ? e->sci_pitch : "";
|
||||||
|
const char* dyn_mark = strlen(e->dmark) ? e->dmark : "";
|
||||||
|
const char* grace_mark= strlen(e->grace_mark) ? e->grace_mark : "";
|
||||||
|
|
||||||
|
if( midi::isSustainPedal( e->status, e->d0 ) )
|
||||||
|
sci_pitch = midi::isPedalDown( e->status, e->d0, e->d1 ) ? "Dv" : "D^";
|
||||||
|
else
|
||||||
|
if( midi::isSostenutoPedal( e->status, e->d0 ) )
|
||||||
|
sci_pitch = midi::isPedalDown( e->status, e->d0, e->d1 ) ? "Sv" : "S^";
|
||||||
|
|
||||||
|
snprintf(buf,buf_byte_cnt,"uid:%5i meas:%4i loc:%4i tick:%8i sec:%8.3f %4s %5s %3s (st:0x%02x d1:0x%02x d0:0x%02x)", e->uid, e->meas, e->loc, e->tick, e->sec, sci_pitch, dyn_mark, grace_mark, e->status, e->d0, e->d1 );
|
||||||
|
}
|
||||||
|
|
||||||
|
return rc;
|
||||||
|
}
|
47
cwPianoScore.h
Normal file
47
cwPianoScore.h
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
#ifndef cwPianoScore_h
|
||||||
|
#define cwPianoScore_h
|
||||||
|
|
||||||
|
namespace cw
|
||||||
|
{
|
||||||
|
namespace score
|
||||||
|
{
|
||||||
|
typedef handle<struct score_str> handle_t;
|
||||||
|
|
||||||
|
typedef struct event_str
|
||||||
|
{
|
||||||
|
unsigned uid; // unique id for this event
|
||||||
|
unsigned meas; // measure number
|
||||||
|
unsigned voice; // score number
|
||||||
|
unsigned loc; // score location
|
||||||
|
unsigned tick; // event tick location
|
||||||
|
double sec; // event absolute time in seconds
|
||||||
|
double rval; // event rythmic value 2=1/2 1/4 .5=2 or 0
|
||||||
|
char sci_pitch[4]; // scientific pitch
|
||||||
|
char dmark[6]; // dynamic mark (e.g. "pp","mf","fff")
|
||||||
|
unsigned dlevel; // dynamic level as an integer associated with dyn. mark
|
||||||
|
unsigned status; // MIDI status < type | channel > or 0
|
||||||
|
unsigned d0; // MIDI d0 or 0
|
||||||
|
unsigned d1; // MIDI d1 or 0
|
||||||
|
unsigned bpm; // tempo BPM or 0
|
||||||
|
char grace_mark[4]; // grace mark or 0
|
||||||
|
unsigned bar; // bar number or 0
|
||||||
|
unsigned section; // section number or 0
|
||||||
|
struct event_str* link; // list link
|
||||||
|
} event_t;
|
||||||
|
|
||||||
|
rc_t create( handle_t& hRef, const char* fn );
|
||||||
|
rc_t create( handle_t& hRef, const object_t* cfg );
|
||||||
|
rc_t destroy( handle_t& hRef );
|
||||||
|
|
||||||
|
unsigned event_count( handle_t h );
|
||||||
|
const event_t* base_event( handle_t h );
|
||||||
|
|
||||||
|
rc_t event_to_string( handle_t h, unsigned uid, char* buf, unsigned buf_byte_cnt );
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#endif
|
117
html/preset_sel/css/ui.css
Normal file
117
html/preset_sel/css/ui.css
Normal file
@ -0,0 +1,117 @@
|
|||||||
|
|
||||||
|
body {
|
||||||
|
background-color: LightCyan;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.title_disconnected {
|
||||||
|
color: red;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title_connected {
|
||||||
|
color: green;
|
||||||
|
}
|
||||||
|
|
||||||
|
input, label, button, select {
|
||||||
|
font-family: sans-serif;
|
||||||
|
font-size: 10px;
|
||||||
|
height: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
margin-right: 4px;
|
||||||
|
margin-left: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
div {
|
||||||
|
/*border: 1px solid black; */
|
||||||
|
background-color: LightSteelBlue;
|
||||||
|
}
|
||||||
|
|
||||||
|
div p {
|
||||||
|
font-family: sans-serif;
|
||||||
|
font-size: 12px;
|
||||||
|
margin-top: 2px;
|
||||||
|
margin-bottom: 2px;
|
||||||
|
background-color: LightSteelBlue;
|
||||||
|
}
|
||||||
|
|
||||||
|
label {
|
||||||
|
/*border: 1px solid red;*/
|
||||||
|
width: 50px;
|
||||||
|
padding-left: 10px;
|
||||||
|
margin-top: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
.uiCtlDiv {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
background-color: LightBlue;
|
||||||
|
}
|
||||||
|
|
||||||
|
.uiCtlDiv input {
|
||||||
|
background-color: PowderBlue;
|
||||||
|
}
|
||||||
|
|
||||||
|
.uiNumberDiv input {
|
||||||
|
width: 50px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.uiPanel {
|
||||||
|
background-color: LightBlue;
|
||||||
|
}
|
||||||
|
|
||||||
|
.uiRow {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
background-color: LightBlue;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
margin-top: 4px;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
.uiCol {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
background-color: LightBlue;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* outer log div - contains the log label and the log scroller */
|
||||||
|
.uiLogDiv {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
align-items: flex-start; /* left justify */
|
||||||
|
align-content: flex-stretch; /* fill horizontal space */
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
.uiLogDiv label {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* log scroller */
|
||||||
|
.uiLog {
|
||||||
|
display:flex;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
height: 150px;
|
||||||
|
overflow-x: hidden; /* 'hidden' to remove scroll bar */
|
||||||
|
overflow-y: auto;
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
background-color: PowderBlue;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/* The log text */
|
||||||
|
.uiLog pre {
|
||||||
|
|
||||||
|
}
|
25
html/preset_sel/index.html
Normal file
25
html/preset_sel/index.html
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<title>Preset Selection App</title>
|
||||||
|
<script type="text/javascript" src="js/ui.js"></script>
|
||||||
|
<link href="css/ui.css" rel="stylesheet">
|
||||||
|
|
||||||
|
<script text="text/javascript">
|
||||||
|
window.addEventListener("load",main, false )
|
||||||
|
</script>
|
||||||
|
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<div id="appTitleDiv" class="uiRow">
|
||||||
|
<p id="appTitleId">Preset Selection:</p>
|
||||||
|
<p id="connectTitleId">Disconnected</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="0" class="uiAppDiv">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
1046
html/preset_sel/js/ui.js
Normal file
1046
html/preset_sel/js/ui.js
Normal file
File diff suppressed because it is too large
Load Diff
55
html/preset_sel/ui.cfg
Normal file
55
html/preset_sel/ui.cfg
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
{
|
||||||
|
|
||||||
|
parent: "uiDivId"
|
||||||
|
|
||||||
|
panel: {
|
||||||
|
|
||||||
|
name: "panelDivId",
|
||||||
|
title: "",
|
||||||
|
|
||||||
|
row: {
|
||||||
|
button:{ name: quitBtnId, title:"Quit" },
|
||||||
|
button:{ name: ioReportBtnId, title:"IO Report" },
|
||||||
|
button:{ name: reportBtnId, title:"App Report" },
|
||||||
|
},
|
||||||
|
|
||||||
|
|
||||||
|
row: {
|
||||||
|
button:{ name: loadBtnId, title:"Load" },
|
||||||
|
button:{ name: startBtnId, title:"Start" },
|
||||||
|
button:{ name: stopBtnId, title:"Stop" },
|
||||||
|
number:{ name: locNumbId, title:"Loc", min:0, max:100000, step:1, decpl:0 },
|
||||||
|
},
|
||||||
|
|
||||||
|
row: {
|
||||||
|
check:{ name: midiThruCheckId, title:"MIDI Thru" },
|
||||||
|
numb_disp: { name: curMidiEvtCntId, title:"Current" },
|
||||||
|
numb_disp: { name: totalMidiEvtCntId, title:"Total" },
|
||||||
|
},
|
||||||
|
|
||||||
|
row: {
|
||||||
|
|
||||||
|
check:{ name: audioThroughCheckId, title:"Audio Thru" },
|
||||||
|
numb_disp: { name: curAudioSecsId, title:"Current:", decpl:1 },
|
||||||
|
numb_disp: { name: totalAudioSecsId, title:"Total:", decpl:1 },
|
||||||
|
},
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
row: {
|
||||||
|
string:{ name: filenameId, title:"File Name:", value:"record" },
|
||||||
|
button:{ name: saveBtnId, title:"Save" },
|
||||||
|
},
|
||||||
|
|
||||||
|
row: {
|
||||||
|
list:{ name: presetLocListId, title:"Preset Locations" },
|
||||||
|
},
|
||||||
|
|
||||||
|
row: {
|
||||||
|
|
||||||
|
log: { name: logId, title:"Log" }
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user