Mixer/Spatializer_Module:
Remove distance related frequency effects. Add control to disable delay effects. Add angle control with frequency and reflection effects. Add advanced options for controlling early and late reverb send amounts.
This commit is contained in:
parent
e927781ee0
commit
b3a9d0be1d
|
@ -48,8 +48,6 @@ class Module : public Fl_Group, public Loggable {
|
||||||
bool _is_default;
|
bool _is_default;
|
||||||
// nframes_t _latency;
|
// nframes_t _latency;
|
||||||
|
|
||||||
Module_Parameter_Editor *_editor;
|
|
||||||
|
|
||||||
static nframes_t _sample_rate;
|
static nframes_t _sample_rate;
|
||||||
static Module *_copied_module_empty;
|
static Module *_copied_module_empty;
|
||||||
static char *_copied_module_settings;
|
static char *_copied_module_settings;
|
||||||
|
@ -68,6 +66,8 @@ class Module : public Fl_Group, public Loggable {
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
|
|
||||||
|
Module_Parameter_Editor *_editor;
|
||||||
|
|
||||||
volatile bool _bypass;
|
volatile bool _bypass;
|
||||||
|
|
||||||
public:
|
public:
|
||||||
|
|
|
@ -469,6 +469,13 @@ Module_Parameter_Editor::handle_control_changed ( Module::Port *p )
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void
|
||||||
|
Module_Parameter_Editor::reload ( void )
|
||||||
|
{
|
||||||
|
make_controls();
|
||||||
|
redraw();
|
||||||
|
}
|
||||||
|
|
||||||
void
|
void
|
||||||
Module_Parameter_Editor::set_value (int i, float value )
|
Module_Parameter_Editor::set_value (int i, float value )
|
||||||
{
|
{
|
||||||
|
|
|
@ -90,6 +90,7 @@ class Module_Parameter_Editor : public Fl_Double_Window
|
||||||
|
|
||||||
public:
|
public:
|
||||||
|
|
||||||
|
void reload ( void );
|
||||||
void handle_control_changed ( Module::Port *p );
|
void handle_control_changed ( Module::Port *p );
|
||||||
|
|
||||||
Module_Parameter_Editor ( Module *module );
|
Module_Parameter_Editor ( Module *module );
|
||||||
|
|
|
@ -21,11 +21,9 @@
|
||||||
#include <FL/Fl_Box.H>
|
#include <FL/Fl_Box.H>
|
||||||
#include "Spatializer_Module.H"
|
#include "Spatializer_Module.H"
|
||||||
#include "dsp.h"
|
#include "dsp.h"
|
||||||
|
#include "Module_Parameter_Editor.H"
|
||||||
|
|
||||||
static const float max_distance = 15.0f;
|
static const float max_distance = 15.0f;
|
||||||
static const float HIGHPASS_FREQ = 200.0f;
|
|
||||||
//static const float LOWPASS_FREQ = 70000.0f;
|
|
||||||
static const float LOWPASS_FREQ = 22000.0f;
|
|
||||||
|
|
||||||
#include <math.h>
|
#include <math.h>
|
||||||
|
|
||||||
|
@ -381,6 +379,7 @@ Spatializer_Module::Spatializer_Module ( ) : JACK_Module ( false )
|
||||||
is_default( false );
|
is_default( false );
|
||||||
|
|
||||||
_panner = 0;
|
_panner = 0;
|
||||||
|
_early_panner = 0;
|
||||||
|
|
||||||
{
|
{
|
||||||
Port p( this, Port::INPUT, Port::CONTROL, "Azimuth" );
|
Port p( this, Port::INPUT, Port::CONTROL, "Azimuth" );
|
||||||
|
@ -424,7 +423,6 @@ Spatializer_Module::Spatializer_Module ( ) : JACK_Module ( false )
|
||||||
add_port( p );
|
add_port( p );
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
{
|
{
|
||||||
Port p( this, Port::INPUT, Port::CONTROL, "Highpass (Hz)" );
|
Port p( this, Port::INPUT, Port::CONTROL, "Highpass (Hz)" );
|
||||||
p.hints.type = Port::Hints::LINEAR;
|
p.hints.type = Port::Hints::LINEAR;
|
||||||
|
@ -432,6 +430,7 @@ Spatializer_Module::Spatializer_Module ( ) : JACK_Module ( false )
|
||||||
p.hints.minimum = 0.0f;
|
p.hints.minimum = 0.0f;
|
||||||
p.hints.maximum = 600.0f;
|
p.hints.maximum = 600.0f;
|
||||||
p.hints.default_value = 0.0f;
|
p.hints.default_value = 0.0f;
|
||||||
|
p.hints.visible = false;
|
||||||
|
|
||||||
p.connect_to( new float );
|
p.connect_to( new float );
|
||||||
p.control_value( p.hints.default_value );
|
p.control_value( p.hints.default_value );
|
||||||
|
@ -446,6 +445,73 @@ Spatializer_Module::Spatializer_Module ( ) : JACK_Module ( false )
|
||||||
p.hints.minimum = -90.0f;
|
p.hints.minimum = -90.0f;
|
||||||
p.hints.maximum = 90.0f;
|
p.hints.maximum = 90.0f;
|
||||||
p.hints.default_value = 90.0f;
|
p.hints.default_value = 90.0f;
|
||||||
|
p.connect_to( new float );
|
||||||
|
p.control_value( p.hints.default_value );
|
||||||
|
|
||||||
|
add_port( p );
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
Port p( this, Port::INPUT, Port::CONTROL, "Angle" );
|
||||||
|
p.hints.type = Port::Hints::LINEAR;
|
||||||
|
p.hints.ranged = true;
|
||||||
|
p.hints.minimum = -180.0f;
|
||||||
|
p.hints.maximum = +180.0f;
|
||||||
|
p.hints.default_value = 0.0f;
|
||||||
|
p.connect_to( new float );
|
||||||
|
p.control_value( p.hints.default_value );
|
||||||
|
|
||||||
|
add_port( p );
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
Port p( this, Port::INPUT, Port::CONTROL, "Advanced Options" );
|
||||||
|
p.hints.type = Port::Hints::BOOLEAN;
|
||||||
|
p.hints.ranged = true;
|
||||||
|
p.hints.minimum = 0.0f;
|
||||||
|
p.hints.maximum = 1.0f;
|
||||||
|
p.hints.default_value = 0.0f;
|
||||||
|
p.connect_to( new float );
|
||||||
|
p.control_value( p.hints.default_value );
|
||||||
|
|
||||||
|
add_port( p );
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
Port p( this, Port::INPUT, Port::CONTROL, "Speed of Sound" );
|
||||||
|
p.hints.type = Port::Hints::BOOLEAN;
|
||||||
|
p.hints.ranged = true;
|
||||||
|
p.hints.minimum = 0.0f;
|
||||||
|
p.hints.maximum = 1.0f;
|
||||||
|
p.hints.default_value = 1.0f;
|
||||||
|
p.hints.visible = false;
|
||||||
|
p.connect_to( new float );
|
||||||
|
p.control_value( p.hints.default_value );
|
||||||
|
|
||||||
|
add_port( p );
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
Port p( this, Port::INPUT, Port::CONTROL, "Late Gain (dB)" );
|
||||||
|
p.hints.type = Port::Hints::LOGARITHMIC;
|
||||||
|
p.hints.ranged = true;
|
||||||
|
p.hints.minimum = -70.0f;
|
||||||
|
p.hints.maximum = 6.0f;
|
||||||
|
p.hints.default_value = 0.0f;
|
||||||
|
p.hints.visible = false;
|
||||||
|
p.connect_to( new float );
|
||||||
|
p.control_value( p.hints.default_value );
|
||||||
|
|
||||||
|
add_port( p );
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
Port p( this, Port::INPUT, Port::CONTROL, "Early Gain (dB)" );
|
||||||
|
p.hints.type = Port::Hints::LOGARITHMIC;
|
||||||
|
p.hints.ranged = true;
|
||||||
|
p.hints.minimum = -70.0f;
|
||||||
|
p.hints.maximum = 6.0f;
|
||||||
|
p.hints.default_value = 0.0f;
|
||||||
p.hints.visible = false;
|
p.hints.visible = false;
|
||||||
p.connect_to( new float );
|
p.connect_to( new float );
|
||||||
p.control_value( p.hints.default_value );
|
p.control_value( p.hints.default_value );
|
||||||
|
@ -456,6 +522,7 @@ Spatializer_Module::Spatializer_Module ( ) : JACK_Module ( false )
|
||||||
log_create();
|
log_create();
|
||||||
|
|
||||||
_panner = new ambisonic_panner();
|
_panner = new ambisonic_panner();
|
||||||
|
_early_panner = new ambisonic_panner();
|
||||||
|
|
||||||
labelsize(9);
|
labelsize(9);
|
||||||
|
|
||||||
|
@ -465,6 +532,8 @@ Spatializer_Module::Spatializer_Module ( ) : JACK_Module ( false )
|
||||||
align(FL_ALIGN_LEFT|FL_ALIGN_TOP|FL_ALIGN_INSIDE);
|
align(FL_ALIGN_LEFT|FL_ALIGN_TOP|FL_ALIGN_INSIDE);
|
||||||
|
|
||||||
gain_smoothing.sample_rate( sample_rate() );
|
gain_smoothing.sample_rate( sample_rate() );
|
||||||
|
late_gain_smoothing.sample_rate( sample_rate() );
|
||||||
|
early_gain_smoothing.sample_rate( sample_rate() );
|
||||||
delay_smoothing.cutoff( 0.5f );
|
delay_smoothing.cutoff( 0.5f );
|
||||||
delay_smoothing.sample_rate( sample_rate() );
|
delay_smoothing.sample_rate( sample_rate() );
|
||||||
}
|
}
|
||||||
|
@ -472,12 +541,10 @@ Spatializer_Module::Spatializer_Module ( ) : JACK_Module ( false )
|
||||||
Spatializer_Module::~Spatializer_Module ( )
|
Spatializer_Module::~Spatializer_Module ( )
|
||||||
{
|
{
|
||||||
configure_inputs(0);
|
configure_inputs(0);
|
||||||
|
delete _early_panner;
|
||||||
delete _panner;
|
delete _panner;
|
||||||
delete (float*)control_input[0].buffer();
|
for ( unsigned int i = 0; i < control_input.size(); i++ )
|
||||||
delete (float*)control_input[1].buffer();
|
delete (float*)control_input[i].buffer();
|
||||||
delete (float*)control_input[2].buffer();
|
|
||||||
delete (float*)control_input[3].buffer();
|
|
||||||
delete (float*)control_input[4].buffer();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -488,6 +555,8 @@ Spatializer_Module::handle_sample_rate_change ( nframes_t n )
|
||||||
{
|
{
|
||||||
gain_smoothing.sample_rate( n );
|
gain_smoothing.sample_rate( n );
|
||||||
delay_smoothing.sample_rate( n );
|
delay_smoothing.sample_rate( n );
|
||||||
|
early_gain_smoothing.sample_rate( n );
|
||||||
|
late_gain_smoothing.sample_rate( n );
|
||||||
|
|
||||||
for ( unsigned int i = 0; i < audio_input.size(); i++ )
|
for ( unsigned int i = 0; i < audio_input.size(); i++ )
|
||||||
{
|
{
|
||||||
|
@ -533,10 +602,17 @@ Spatializer_Module::process ( nframes_t nframes )
|
||||||
float radius = control_input[2].control_value();
|
float radius = control_input[2].control_value();
|
||||||
float highpass_freq = control_input[3].control_value();
|
float highpass_freq = control_input[3].control_value();
|
||||||
float width = control_input[4].control_value();
|
float width = control_input[4].control_value();
|
||||||
|
float angle = control_input[5].control_value();
|
||||||
|
// bool more_options = control_input[6].control_value();
|
||||||
|
bool speed_of_sound = control_input[7].control_value() > 0.5f;
|
||||||
|
float late_gain = DB_CO( control_input[8].control_value() );
|
||||||
|
float early_gain = DB_CO( control_input[9].control_value() );
|
||||||
|
|
||||||
|
control_input[3].hints.visible = highpass_freq != 0.0f;
|
||||||
|
|
||||||
float delay_seconds = 0.0f;
|
float delay_seconds = 0.0f;
|
||||||
|
|
||||||
if ( radius > 1.0f )
|
if ( speed_of_sound && radius > 1.0f )
|
||||||
delay_seconds = ( radius - 1.0f ) / 340.29f;
|
delay_seconds = ( radius - 1.0f ) / 340.29f;
|
||||||
|
|
||||||
/* direct sound follows inverse square law */
|
/* direct sound follows inverse square law */
|
||||||
|
@ -548,12 +624,12 @@ Spatializer_Module::process ( nframes_t nframes )
|
||||||
|
|
||||||
float gain = 1.0f / radius;
|
float gain = 1.0f / radius;
|
||||||
|
|
||||||
float cutoff_frequency = gain * LOWPASS_FREQ;
|
/* float cutoff_frequency = gain * LOWPASS_FREQ; */
|
||||||
|
|
||||||
sample_t gainbuf[nframes];
|
sample_t gainbuf[nframes];
|
||||||
sample_t delaybuf[nframes];
|
sample_t delaybuf[nframes];
|
||||||
|
|
||||||
bool use_gainbuf = gain_smoothing.apply( gainbuf, nframes, gain );
|
bool use_gainbuf = false;
|
||||||
bool use_delaybuf = delay_smoothing.apply( delaybuf, nframes, delay_seconds );
|
bool use_delaybuf = delay_smoothing.apply( delaybuf, nframes, delay_seconds );
|
||||||
|
|
||||||
for ( unsigned int i = 0; i < audio_input.size(); i++ )
|
for ( unsigned int i = 0; i < audio_input.size(); i++ )
|
||||||
|
@ -562,7 +638,6 @@ Spatializer_Module::process ( nframes_t nframes )
|
||||||
|
|
||||||
/* frequency effects */
|
/* frequency effects */
|
||||||
_highpass[i]->run_highpass( buf, highpass_freq, nframes );
|
_highpass[i]->run_highpass( buf, highpass_freq, nframes );
|
||||||
_lowpass[i]->run_lowpass( buf, cutoff_frequency, nframes );
|
|
||||||
|
|
||||||
/* send to late reverb */
|
/* send to late reverb */
|
||||||
if ( i == 0 )
|
if ( i == 0 )
|
||||||
|
@ -570,15 +645,94 @@ Spatializer_Module::process ( nframes_t nframes )
|
||||||
else
|
else
|
||||||
buffer_mix( (sample_t*)aux_audio_output[0].jack_port()->buffer(nframes), buf, nframes );
|
buffer_mix( (sample_t*)aux_audio_output[0].jack_port()->buffer(nframes), buf, nframes );
|
||||||
|
|
||||||
/* /\* FIXME: use smoothed value... *\/ */
|
|
||||||
/* buffer_apply_gain( (sample_t*)jack_output[0].buffer(nframes), nframes, 1.0f / sqrt(D) ); */
|
|
||||||
|
|
||||||
if ( use_delaybuf )
|
|
||||||
_delay[i]->run( buf, delaybuf, 0, nframes );
|
|
||||||
else
|
|
||||||
_delay[i]->run( buf, 0, delay_seconds, nframes );
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
use_gainbuf = late_gain_smoothing.apply( gainbuf, nframes, late_gain );
|
||||||
|
|
||||||
|
/* gain effects */
|
||||||
|
if ( use_gainbuf )
|
||||||
|
buffer_apply_gain_buffer( (sample_t*)aux_audio_output[0].jack_port()->buffer(nframes), gainbuf, nframes );
|
||||||
|
else
|
||||||
|
buffer_apply_gain( (sample_t*)aux_audio_output[0].jack_port()->buffer(nframes), nframes, late_gain );
|
||||||
|
}
|
||||||
|
|
||||||
|
float early_angle = azimuth - angle;
|
||||||
|
if ( early_angle > 180.0f )
|
||||||
|
early_angle = -180 - ( early_angle - 180 );
|
||||||
|
else if ( early_angle < -180.0f )
|
||||||
|
early_angle = 180 - ( early_angle + 180 );
|
||||||
|
|
||||||
|
/* send to early reverb */
|
||||||
|
if ( audio_input.size() == 1 )
|
||||||
|
{
|
||||||
|
_early_panner->run_mono( (sample_t*)audio_input[0].buffer(),
|
||||||
|
(sample_t*)aux_audio_output[1].jack_port()->buffer(nframes),
|
||||||
|
(sample_t*)aux_audio_output[2].jack_port()->buffer(nframes),
|
||||||
|
(sample_t*)aux_audio_output[3].jack_port()->buffer(nframes),
|
||||||
|
(sample_t*)aux_audio_output[4].jack_port()->buffer(nframes),
|
||||||
|
azimuth + angle,
|
||||||
|
elevation,
|
||||||
|
nframes );
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_early_panner->run_stereo( (sample_t*)audio_input[0].buffer(),
|
||||||
|
(sample_t*)audio_input[1].buffer(),
|
||||||
|
(sample_t*)aux_audio_output[1].jack_port()->buffer(nframes),
|
||||||
|
(sample_t*)aux_audio_output[2].jack_port()->buffer(nframes),
|
||||||
|
(sample_t*)aux_audio_output[3].jack_port()->buffer(nframes),
|
||||||
|
(sample_t*)aux_audio_output[4].jack_port()->buffer(nframes),
|
||||||
|
azimuth + angle,
|
||||||
|
elevation,
|
||||||
|
width,
|
||||||
|
nframes );
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
use_gainbuf = early_gain_smoothing.apply( gainbuf, nframes, early_gain );
|
||||||
|
|
||||||
|
for ( int i = 1; i < 5; i++ )
|
||||||
|
{
|
||||||
|
/* gain effects */
|
||||||
|
if ( use_gainbuf )
|
||||||
|
buffer_apply_gain_buffer( (sample_t*)aux_audio_output[i].jack_port()->buffer(nframes), gainbuf, nframes );
|
||||||
|
else
|
||||||
|
buffer_apply_gain( (sample_t*)aux_audio_output[i].jack_port()->buffer(nframes), nframes, early_gain );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
float corrected_angle = fabs( angle ) - (fabs( width ) * 0.5f);
|
||||||
|
|
||||||
|
if ( corrected_angle < 0.0f )
|
||||||
|
corrected_angle = 0.0f;
|
||||||
|
|
||||||
|
float cutoff_frequency = ( 1.0f / ( 1.0f + corrected_angle ) ) * 300000.0f;
|
||||||
|
|
||||||
|
use_gainbuf = gain_smoothing.apply( gainbuf, nframes, gain );
|
||||||
|
|
||||||
|
for ( unsigned int i = 0; i < audio_input.size(); i++ )
|
||||||
|
{
|
||||||
|
/* gain effects */
|
||||||
|
if ( use_gainbuf )
|
||||||
|
buffer_apply_gain_buffer( (sample_t*)audio_input[i].buffer(), gainbuf, nframes );
|
||||||
|
else
|
||||||
|
buffer_apply_gain( (sample_t*)audio_input[i].buffer(), nframes, gain );
|
||||||
|
|
||||||
|
/* frequency effects */
|
||||||
|
_lowpass[i]->run_lowpass( (sample_t*)audio_input[i].buffer(), cutoff_frequency, nframes );
|
||||||
|
|
||||||
|
/* delay effects */
|
||||||
|
if ( speed_of_sound )
|
||||||
|
{
|
||||||
|
if ( use_delaybuf )
|
||||||
|
_delay[i]->run( (sample_t*)audio_input[i].buffer(), delaybuf, 0, nframes );
|
||||||
|
else
|
||||||
|
_delay[i]->run( (sample_t*)audio_input[i].buffer(), 0, delay_seconds, nframes );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* now do direct outputs */
|
||||||
if ( audio_input.size() == 1 )
|
if ( audio_input.size() == 1 )
|
||||||
{
|
{
|
||||||
_panner->run_mono( (sample_t*)audio_input[0].buffer(),
|
_panner->run_mono( (sample_t*)audio_input[0].buffer(),
|
||||||
|
@ -603,24 +757,23 @@ Spatializer_Module::process ( nframes_t nframes )
|
||||||
width,
|
width,
|
||||||
nframes );
|
nframes );
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/* send to early reverb */
|
void
|
||||||
for ( int i = 4; i--; )
|
Spatializer_Module::handle_control_changed ( Port *p )
|
||||||
buffer_copy( (sample_t*)aux_audio_output[1 + i].jack_port()->buffer(nframes),
|
{
|
||||||
(sample_t*)audio_output[0 + i].buffer(),
|
if ( p == &control_input[6] )
|
||||||
nframes );
|
{
|
||||||
|
bool v = p->control_value();
|
||||||
|
|
||||||
/* gain effects */
|
control_input[7].hints.visible = v;
|
||||||
if ( use_gainbuf )
|
control_input[8].hints.visible = v;
|
||||||
{
|
control_input[9].hints.visible = v;
|
||||||
for ( int i = 4; i--; )
|
|
||||||
buffer_apply_gain_buffer( (sample_t*)audio_output[i].buffer(), gainbuf, nframes );
|
DMESSAGE( "reloading" );
|
||||||
}
|
if ( _editor )
|
||||||
else
|
_editor->reload();
|
||||||
{
|
|
||||||
for ( int i = 4; i--; )
|
|
||||||
buffer_apply_gain( (sample_t*)audio_output[i].buffer(), nframes, gain );
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -673,8 +826,9 @@ Spatializer_Module::configure_inputs ( int n )
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
control_input[4].hints.visible = audio_input.size() == 2;
|
// control_input[4].hints.visible = audio_input.size() == 2;
|
||||||
|
|
||||||
|
control_input[4].hints.default_value = audio_input.size() == 2 ? 90.0f : 0.0f;
|
||||||
|
|
||||||
if ( n == 0 )
|
if ( n == 0 )
|
||||||
{
|
{
|
||||||
|
|
|
@ -30,12 +30,15 @@ class Spatializer_Module : public JACK_Module
|
||||||
{
|
{
|
||||||
Value_Smoothing_Filter gain_smoothing;
|
Value_Smoothing_Filter gain_smoothing;
|
||||||
Value_Smoothing_Filter delay_smoothing;
|
Value_Smoothing_Filter delay_smoothing;
|
||||||
|
Value_Smoothing_Filter late_gain_smoothing;
|
||||||
|
Value_Smoothing_Filter early_gain_smoothing;
|
||||||
|
|
||||||
std::vector<filter*> _lowpass;
|
std::vector<filter*> _lowpass;
|
||||||
std::vector<filter*> _highpass;
|
std::vector<filter*> _highpass;
|
||||||
std::vector<delay*> _delay;
|
std::vector<delay*> _delay;
|
||||||
|
|
||||||
ambisonic_panner *_panner;
|
ambisonic_panner *_panner;
|
||||||
|
ambisonic_panner *_early_panner;
|
||||||
|
|
||||||
public:
|
public:
|
||||||
|
|
||||||
|
@ -53,7 +56,7 @@ public:
|
||||||
MODULE_CLONE_FUNC(Spatializer_Module);
|
MODULE_CLONE_FUNC(Spatializer_Module);
|
||||||
|
|
||||||
virtual void handle_sample_rate_change ( nframes_t n );
|
virtual void handle_sample_rate_change ( nframes_t n );
|
||||||
|
virtual void handle_control_changed ( Port *p );
|
||||||
virtual void draw ( void );
|
virtual void draw ( void );
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
|
|
Loading…
Reference in New Issue