Merge branch 'master' of gitea.larke.org:kevin/libcw
This commit is contained in:
commit
f6ddb9f97f
@ -35,9 +35,11 @@ namespace cw
|
|||||||
kMidiThruCheckId,
|
kMidiThruCheckId,
|
||||||
kCurMidiEvtCntId,
|
kCurMidiEvtCntId,
|
||||||
kTotalMidiEvtCntId,
|
kTotalMidiEvtCntId,
|
||||||
|
kMidiMuteCheckId,
|
||||||
|
|
||||||
kCurAudioSecsId,
|
kCurAudioSecsId,
|
||||||
kTotalAudioSecsId,
|
kTotalAudioSecsId,
|
||||||
|
kAudioMuteCheckId,
|
||||||
|
|
||||||
kSaveBtnId,
|
kSaveBtnId,
|
||||||
kOpenBtnId,
|
kOpenBtnId,
|
||||||
@ -66,10 +68,11 @@ namespace cw
|
|||||||
{ kPanelDivId, kMidiThruCheckId, "midiThruCheckId" },
|
{ kPanelDivId, kMidiThruCheckId, "midiThruCheckId" },
|
||||||
{ kPanelDivId, kCurMidiEvtCntId, "curMidiEvtCntId" },
|
{ kPanelDivId, kCurMidiEvtCntId, "curMidiEvtCntId" },
|
||||||
{ kPanelDivId, kTotalMidiEvtCntId, "totalMidiEvtCntId" },
|
{ kPanelDivId, kTotalMidiEvtCntId, "totalMidiEvtCntId" },
|
||||||
|
{ kPanelDivId, kMidiMuteCheckId, "midiMuteCheckId" },
|
||||||
|
|
||||||
{ kPanelDivId, kCurAudioSecsId, "curAudioSecsId" },
|
{ kPanelDivId, kCurAudioSecsId, "curAudioSecsId" },
|
||||||
{ kPanelDivId, kTotalAudioSecsId, "totalAudioSecsId" },
|
{ kPanelDivId, kTotalAudioSecsId, "totalAudioSecsId" },
|
||||||
|
{ kPanelDivId, kAudioMuteCheckId, "audioMuteCheckId" },
|
||||||
|
|
||||||
{ kPanelDivId, kSaveBtnId, "saveBtnId" },
|
{ kPanelDivId, kSaveBtnId, "saveBtnId" },
|
||||||
{ kPanelDivId, kOpenBtnId, "openBtnId" },
|
{ kPanelDivId, kOpenBtnId, "openBtnId" },
|
||||||
@ -388,6 +391,14 @@ namespace cw
|
|||||||
cwLogInfo("MIDI thru:%i",m.value->u.b);
|
cwLogInfo("MIDI thru:%i",m.value->u.b);
|
||||||
_set_midi_thru_state(app, m.value->u.b);
|
_set_midi_thru_state(app, m.value->u.b);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
case kMidiMuteCheckId:
|
||||||
|
midi_record_play::set_mute_state(app->mrpH,m.value->u.b);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case kAudioMuteCheckId:
|
||||||
|
audio_record_play::set_mute_state(app->arpH,m.value->u.b);
|
||||||
|
break;
|
||||||
|
|
||||||
case kStartBtnId:
|
case kStartBtnId:
|
||||||
_on_ui_start(app);
|
_on_ui_start(app);
|
||||||
|
@ -39,6 +39,8 @@ namespace cw
|
|||||||
unsigned curFrameIdx;
|
unsigned curFrameIdx;
|
||||||
bool recordFl;
|
bool recordFl;
|
||||||
bool startedFl;
|
bool startedFl;
|
||||||
|
|
||||||
|
bool mute_fl;
|
||||||
|
|
||||||
unsigned* audioInChMapA;
|
unsigned* audioInChMapA;
|
||||||
unsigned audioInChMapN;
|
unsigned audioInChMapN;
|
||||||
@ -169,8 +171,11 @@ namespace cw
|
|||||||
for(unsigned chIdx=0; chIdx<chCnt; ++chIdx)
|
for(unsigned chIdx=0; chIdx<chCnt; ++chIdx)
|
||||||
{
|
{
|
||||||
unsigned srcChIdx = p->audioInChMapA == nullptr ? chIdx : p->audioInChMapA[chIdx];
|
unsigned srcChIdx = p->audioInChMapA == nullptr ? chIdx : p->audioInChMapA[chIdx];
|
||||||
|
|
||||||
memcpy(a->audioBuf + chIdx*asrc.dspFrameCnt, asrc.iBufArray[ srcChIdx ], asrc.dspFrameCnt * sizeof(sample_t));
|
if( srcChIdx >= asrc.iBufChCnt )
|
||||||
|
cwLogError(kInvalidArgRC,"Invalid input channel map index:%i >= %i.",srcChIdx,asrc.iBufChCnt);
|
||||||
|
else
|
||||||
|
memcpy(a->audioBuf + chIdx*asrc.dspFrameCnt, asrc.iBufArray[ srcChIdx ], asrc.dspFrameCnt * sizeof(sample_t));
|
||||||
}
|
}
|
||||||
|
|
||||||
a->chCnt = chCnt;
|
a->chCnt = chCnt;
|
||||||
@ -197,31 +202,34 @@ namespace cw
|
|||||||
void _audio_play( audio_record_play_t* p, io::audio_msg_t& adst )
|
void _audio_play( audio_record_play_t* p, io::audio_msg_t& adst )
|
||||||
{
|
{
|
||||||
unsigned adst_idx = 0;
|
unsigned adst_idx = 0;
|
||||||
|
|
||||||
while(adst_idx < adst.dspFrameCnt)
|
if( !p->mute_fl )
|
||||||
{
|
{
|
||||||
am_audio_t* a;
|
while(adst_idx < adst.dspFrameCnt)
|
||||||
unsigned sample_offs = 0;
|
|
||||||
if((a = _am_audio_from_sample_index(p, p->curFrameIdx, sample_offs )) == nullptr )
|
|
||||||
break;
|
|
||||||
|
|
||||||
unsigned n = std::min(a->dspFrameCnt - sample_offs, adst.dspFrameCnt );
|
|
||||||
|
|
||||||
|
|
||||||
// TODO: Verify that this is correct - it looks like sample_offs should have to be incremented
|
|
||||||
|
|
||||||
for(unsigned i=0; i<a->chCnt; ++i)
|
|
||||||
{
|
{
|
||||||
unsigned dstChIdx = p->audioOutChMapA != nullptr && i < p->audioOutChMapN ? p->audioOutChMapA[i] : i;
|
am_audio_t* a;
|
||||||
|
unsigned sample_offs = 0;
|
||||||
|
if((a = _am_audio_from_sample_index(p, p->curFrameIdx, sample_offs )) == nullptr )
|
||||||
|
break;
|
||||||
|
|
||||||
if( dstChIdx < adst.oBufChCnt )
|
unsigned n = std::min(a->dspFrameCnt - sample_offs, adst.dspFrameCnt );
|
||||||
memcpy( adst.oBufArray[ dstChIdx ] + adst_idx, a->audioBuf + sample_offs, n * sizeof(sample_t));
|
|
||||||
|
|
||||||
|
// TODO: Verify that this is correct - it looks like sample_offs should have to be incremented
|
||||||
|
|
||||||
|
for(unsigned i=0; i<a->chCnt; ++i)
|
||||||
|
{
|
||||||
|
unsigned dstChIdx = p->audioOutChMapA != nullptr && i < p->audioOutChMapN ? p->audioOutChMapA[i] : i;
|
||||||
|
|
||||||
|
if( dstChIdx < adst.oBufChCnt )
|
||||||
|
memcpy( adst.oBufArray[ dstChIdx ] + adst_idx, a->audioBuf + sample_offs, n * sizeof(sample_t));
|
||||||
|
}
|
||||||
|
|
||||||
|
p->curFrameIdx += n;
|
||||||
|
adst_idx += n;
|
||||||
}
|
}
|
||||||
|
|
||||||
p->curFrameIdx += n;
|
|
||||||
adst_idx += n;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: zero unused channels
|
// TODO: zero unused channels
|
||||||
|
|
||||||
if( adst_idx < adst.dspFrameCnt )
|
if( adst_idx < adst.dspFrameCnt )
|
||||||
@ -415,9 +423,16 @@ namespace cw
|
|||||||
else
|
else
|
||||||
{
|
{
|
||||||
if( m.oBufChCnt > 0 )
|
if( m.oBufChCnt > 0 )
|
||||||
|
{
|
||||||
_audio_play(p,m);
|
_audio_play(p,m);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
for(unsigned i=0; i<m.oBufChCnt; ++i)
|
||||||
|
memset( m.oBufArray[i], 0, m.dspFrameCnt * sizeof(sample_t));
|
||||||
|
}
|
||||||
|
|
||||||
return rc;
|
return rc;
|
||||||
}
|
}
|
||||||
@ -522,6 +537,22 @@ bool cw::audio_record_play::record_state( handle_t h )
|
|||||||
return rc;
|
return rc;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
cw::rc_t cw::audio_record_play::set_mute_state( handle_t h, bool mute_fl )
|
||||||
|
{
|
||||||
|
rc_t rc = kOkRC;
|
||||||
|
audio_record_play_t* p = _handleToPtr(h);
|
||||||
|
p->mute_fl = true;
|
||||||
|
return rc;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool cw::audio_record_play::mute_state( handle_t h )
|
||||||
|
{
|
||||||
|
rc_t rc = kOkRC;
|
||||||
|
audio_record_play_t* p = _handleToPtr(h);
|
||||||
|
return p->mute_fl;
|
||||||
|
return rc;
|
||||||
|
}
|
||||||
|
|
||||||
cw::rc_t cw::audio_record_play::save( handle_t h, const char* fn )
|
cw::rc_t cw::audio_record_play::save( handle_t h, const char* fn )
|
||||||
{
|
{
|
||||||
audio_record_play_t* p = _handleToPtr(h);
|
audio_record_play_t* p = _handleToPtr(h);
|
||||||
|
@ -17,6 +17,8 @@ namespace cw
|
|||||||
rc_t clear( handle_t h );
|
rc_t clear( handle_t h );
|
||||||
rc_t set_record_state( handle_t h, bool record_fl );
|
rc_t set_record_state( handle_t h, bool record_fl );
|
||||||
bool record_state( handle_t h );
|
bool record_state( handle_t h );
|
||||||
|
rc_t set_mute_state( handle_t h, bool mute_fl );
|
||||||
|
bool mute_state( handle_t h );
|
||||||
rc_t save( handle_t h, const char* fn );
|
rc_t save( handle_t h, const char* fn );
|
||||||
rc_t open( handle_t h, const char* fn );
|
rc_t open( handle_t h, const char* fn );
|
||||||
double duration_seconds( handle_t h );
|
double duration_seconds( handle_t h );
|
||||||
|
@ -60,6 +60,10 @@ namespace cw
|
|||||||
bool force_damper_down_fl;
|
bool force_damper_down_fl;
|
||||||
unsigned force_damper_down_threshold;
|
unsigned force_damper_down_threshold;
|
||||||
unsigned force_damper_down_velocity;
|
unsigned force_damper_down_velocity;
|
||||||
|
|
||||||
|
bool damper_dead_band_enable_fl;
|
||||||
|
unsigned damper_dead_band_min_value;
|
||||||
|
unsigned damper_dead_band_max_value;
|
||||||
|
|
||||||
} midi_device_t;
|
} midi_device_t;
|
||||||
|
|
||||||
@ -95,6 +99,7 @@ namespace cw
|
|||||||
|
|
||||||
bool startedFl;
|
bool startedFl;
|
||||||
bool recordFl;
|
bool recordFl;
|
||||||
|
bool muteFl;
|
||||||
bool thruFl;
|
bool thruFl;
|
||||||
bool logInFl; // log incoming message when not in 'record' mode.
|
bool logInFl; // log incoming message when not in 'record' mode.
|
||||||
bool logOutFl; // log outgoing messages
|
bool logOutFl; // log outgoing messages
|
||||||
@ -198,20 +203,27 @@ namespace cw
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
if((rc = ele->getv_opt( "vel_table", velTable,
|
if((rc = ele->getv_opt(
|
||||||
"pedal", pedalRecd,
|
"vel_table", velTable,
|
||||||
"force_damper_down_fl",p->midiDevA[i].force_damper_down_fl,
|
"pedal", pedalRecd,
|
||||||
"force_damper_down_threshold",p->midiDevA[i].force_damper_down_threshold,
|
"force_damper_down_fl",p->midiDevA[i].force_damper_down_fl,
|
||||||
"force_damper_down_velocity", p->midiDevA[i].force_damper_down_velocity)) != kOkRC )
|
"force_damper_down_threshold",p->midiDevA[i].force_damper_down_threshold,
|
||||||
|
"force_damper_down_velocity", p->midiDevA[i].force_damper_down_velocity,
|
||||||
|
"damper_dead_band_enable_fl", p->midiDevA[i].damper_dead_band_enable_fl,
|
||||||
|
"damper_dead_band_min_value", p->midiDevA[i].damper_dead_band_min_value,
|
||||||
|
"damper_dead_band_max_value", p->midiDevA[i].damper_dead_band_max_value)) != kOkRC )
|
||||||
{
|
{
|
||||||
rc = cwLogError(kSyntaxErrorRC,"MIDI record play device optional argument parsing failed.");
|
rc = cwLogError(kSyntaxErrorRC,"MIDI record play device optional argument parsing failed.");
|
||||||
goto errLabel;
|
goto errLabel;
|
||||||
}
|
}
|
||||||
|
|
||||||
printf("Force Pedal: enabled%i thresh:%i veloc:%i\n",
|
cwLogInfo("Force Pedal: enabled:%i thresh:%i veloc:%i dead band: enable:%i min:%i max:%i",
|
||||||
p->midiDevA[i].force_damper_down_fl,
|
p->midiDevA[i].force_damper_down_fl,
|
||||||
p->midiDevA[i].force_damper_down_threshold,
|
p->midiDevA[i].force_damper_down_threshold,
|
||||||
p->midiDevA[i].force_damper_down_velocity);
|
p->midiDevA[i].force_damper_down_velocity,
|
||||||
|
p->midiDevA[i].damper_dead_band_enable_fl,
|
||||||
|
p->midiDevA[i].damper_dead_band_min_value,
|
||||||
|
p->midiDevA[i].damper_dead_band_max_value );
|
||||||
|
|
||||||
p->midiDevA[i].midiOutDevLabel = mem::duplStr( midiOutDevLabel);
|
p->midiDevA[i].midiOutDevLabel = mem::duplStr( midiOutDevLabel);
|
||||||
p->midiDevA[i].midiOutPortLabel = mem::duplStr( midiOutPortLabel);
|
p->midiDevA[i].midiOutPortLabel = mem::duplStr( midiOutPortLabel);
|
||||||
@ -306,7 +318,8 @@ namespace cw
|
|||||||
|
|
||||||
bool is_note_on_fl = status==midi::kNoteOnMdId and d1 != 0;
|
bool is_note_on_fl = status==midi::kNoteOnMdId and d1 != 0;
|
||||||
bool is_damper_fl = status==midi::kCtlMdId and d0==midi::kSustainCtlMdId;
|
bool is_damper_fl = status==midi::kCtlMdId and d0==midi::kSustainCtlMdId;
|
||||||
bool supress_fl = is_note_on_fl && after_stop_time_fl;
|
bool supress_fl = (is_note_on_fl && after_stop_time_fl) || p->muteFl;
|
||||||
|
bool is_pedal_fl = midi::isPedal( status, d0 );
|
||||||
|
|
||||||
if( after_all_off_fl )
|
if( after_all_off_fl )
|
||||||
{
|
{
|
||||||
@ -339,12 +352,19 @@ namespace cw
|
|||||||
out_d1 = p->midiDevA[i].velTableArray[ d1 ];
|
out_d1 = p->midiDevA[i].velTableArray[ d1 ];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// store the note-on velocity histogram data
|
||||||
if( p->velHistogramEnableFl && is_note_on_fl && out_d1 < midi::kMidiVelCnt )
|
if( p->velHistogramEnableFl && is_note_on_fl && out_d1 < midi::kMidiVelCnt )
|
||||||
p->midiDevA[i].velHistogram[ out_d1 ] += 1;
|
p->midiDevA[i].velHistogram[ out_d1 ] += 1;
|
||||||
|
|
||||||
|
// if the damper pedal velocity is in the dead band then don't send it
|
||||||
|
if( p->midiDevA[i].damper_dead_band_enable_fl && is_pedal_fl && p->midiDevA[i].damper_dead_band_min_value <= out_d1 && out_d1 <= p->midiDevA[i].damper_dead_band_max_value )
|
||||||
|
out_d1 = 0;
|
||||||
|
|
||||||
|
// if the damper pedal velocity is over the 'forcing' threshold then force the damper down
|
||||||
if( p->midiDevA[i].force_damper_down_fl && is_damper_fl && out_d1>p->midiDevA[i].force_damper_down_threshold )
|
if( p->midiDevA[i].force_damper_down_fl && is_damper_fl && out_d1>p->midiDevA[i].force_damper_down_threshold )
|
||||||
out_d1 = p->midiDevA[i].force_damper_down_velocity;
|
out_d1 = p->midiDevA[i].force_damper_down_velocity;
|
||||||
|
|
||||||
|
|
||||||
// map the pedal down velocity
|
// map the pedal down velocity
|
||||||
if( status==midi::kCtlMdId && d0 == midi::kSustainCtlMdId && p->midiDevA[i].pedalMapEnableFl )
|
if( status==midi::kCtlMdId && d0 == midi::kSustainCtlMdId && p->midiDevA[i].pedalMapEnableFl )
|
||||||
{
|
{
|
||||||
@ -363,8 +383,10 @@ namespace cw
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if( !supress_fl )
|
if( !supress_fl )
|
||||||
|
{
|
||||||
io::midiDeviceSend( p->ioH, p->midiDevA[i].midiOutDevIdx, p->midiDevA[i].midiOutPortIdx, status + ch, d0, out_d1 );
|
io::midiDeviceSend( p->ioH, p->midiDevA[i].midiOutDevIdx, p->midiDevA[i].midiOutPortIdx, status + ch, d0, out_d1 );
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if( !after_stop_time_fl and p->cb )
|
if( !after_stop_time_fl and p->cb )
|
||||||
@ -1211,6 +1233,21 @@ bool cw::midi_record_play::record_state( handle_t h )
|
|||||||
return p->recordFl;
|
return p->recordFl;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
cw::rc_t cw::midi_record_play::set_mute_state( handle_t h, bool mute_fl )
|
||||||
|
{
|
||||||
|
rc_t rc = kOkRC;
|
||||||
|
midi_record_play_t* p = _handleToPtr(h);
|
||||||
|
p->muteFl = mute_fl;
|
||||||
|
return rc;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool cw::midi_record_play::mute_state( handle_t h )
|
||||||
|
{
|
||||||
|
midi_record_play_t* p = _handleToPtr(h);
|
||||||
|
return p->muteFl;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
cw::rc_t cw::midi_record_play::set_thru_state( handle_t h, bool thru_fl )
|
cw::rc_t cw::midi_record_play::set_thru_state( handle_t h, bool thru_fl )
|
||||||
{
|
{
|
||||||
rc_t rc = kOkRC;
|
rc_t rc = kOkRC;
|
||||||
|
@ -41,6 +41,9 @@ namespace cw
|
|||||||
rc_t set_record_state( handle_t h, bool record_fl );
|
rc_t set_record_state( handle_t h, bool record_fl );
|
||||||
bool record_state( handle_t h );
|
bool record_state( handle_t h );
|
||||||
|
|
||||||
|
rc_t set_mute_state( handle_t h, bool record_fl );
|
||||||
|
bool mute_state( handle_t h );
|
||||||
|
|
||||||
rc_t set_thru_state( handle_t h, bool record_thru );
|
rc_t set_thru_state( handle_t h, bool record_thru );
|
||||||
bool thru_state( handle_t h );
|
bool thru_state( handle_t h );
|
||||||
|
|
||||||
|
4
cwMidi.h
4
cwMidi.h
@ -99,6 +99,10 @@ namespace cw
|
|||||||
template< typename T> bool isSostenutoPedalDown( T s, T d0, T d1) { return ( isSostenutoPedal(s,d0) && (d1)>=64 ); }
|
template< typename T> bool isSostenutoPedalDown( T s, T d0, T d1) { return ( isSostenutoPedal(s,d0) && (d1)>=64 ); }
|
||||||
template< typename T> bool isSostenutoPedalUp( T s, T d0, T d1) { return ( isSostenutoPedal(s,d0) && (d1)<64 ); }
|
template< typename T> bool isSostenutoPedalUp( T s, T d0, T d1) { return ( isSostenutoPedal(s,d0) && (d1)<64 ); }
|
||||||
|
|
||||||
|
template< typename T> bool isSoftPedal( T s, T d0 ) { return ( kCtlMdId <= (s) && (s) <= (kCtlMdId + kMidiChCnt) && (d0)== kSoftPedalCtlMdId ); }
|
||||||
|
template< typename T> bool isSoftPedalDown( T s, T d0, T d1) { return ( isSoftPedal(s,d0) && (d1)>=64 ); }
|
||||||
|
template< typename T> bool isSoftPedalUp( T s, T d0, T d1) { return ( isSoftPedal(s,d0) && (d1)<64 ); }
|
||||||
|
|
||||||
template< typename T> bool isPedal( T s, T d0 ) { return ( kCtlMdId <= (s) && (s) <= (kCtlMdId + kMidiChCnt) && (d0)>=kSustainCtlMdId && (d0)<=kLegatoCtlMdId ); }
|
template< typename T> bool isPedal( T s, T d0 ) { return ( kCtlMdId <= (s) && (s) <= (kCtlMdId + kMidiChCnt) && (d0)>=kSustainCtlMdId && (d0)<=kLegatoCtlMdId ); }
|
||||||
template< typename T> bool isPedalDown( T s, T d0, T d1 ) { return ( isPedal(s,d0) && (d1)>=64 ); }
|
template< typename T> bool isPedalDown( T s, T d0, T d1 ) { return ( isPedal(s,d0) && (d1)>=64 ); }
|
||||||
template< typename T> bool isPedalUp( T s, T d0, T d1 ) { return ( isPedal(s,d0) && (d1)<64 ); }
|
template< typename T> bool isPedalUp( T s, T d0, T d1 ) { return ( isPedal(s,d0) && (d1)<64 ); }
|
||||||
|
@ -26,6 +26,7 @@
|
|||||||
check:{ name: midiThruCheckId, title:"MIDI Thru" },
|
check:{ name: midiThruCheckId, title:"MIDI Thru" },
|
||||||
numb_disp: { name: curMidiEvtCntId, title:"Current" },
|
numb_disp: { name: curMidiEvtCntId, title:"Current" },
|
||||||
numb_disp: { name: totalMidiEvtCntId, title:"Total" },
|
numb_disp: { name: totalMidiEvtCntId, title:"Total" },
|
||||||
|
check:{ name: midiMuteCheckId, title:"Mute" },
|
||||||
},
|
},
|
||||||
|
|
||||||
row: {
|
row: {
|
||||||
@ -33,6 +34,7 @@
|
|||||||
check:{ name: audioThroughCheckId, title:"Audio Thru" },
|
check:{ name: audioThroughCheckId, title:"Audio Thru" },
|
||||||
numb_disp: { name: curAudioSecsId, title:"Current:", decpl:1 },
|
numb_disp: { name: curAudioSecsId, title:"Current:", decpl:1 },
|
||||||
numb_disp: { name: totalAudioSecsId, title:"Total:", decpl:1 },
|
numb_disp: { name: totalAudioSecsId, title:"Total:", decpl:1 },
|
||||||
|
check: { name: audioMuteCheckId, title:"Mute:" },
|
||||||
},
|
},
|
||||||
|
|
||||||
row: {
|
row: {
|
||||||
|
Loading…
Reference in New Issue
Block a user