2017-03-05 15:47:28 +01:00
|
|
|
/*************************************************************************/
|
|
|
|
/* reverb.cpp */
|
|
|
|
/*************************************************************************/
|
|
|
|
/* This file is part of: */
|
|
|
|
/* GODOT ENGINE */
|
2017-08-27 14:16:55 +02:00
|
|
|
/* https://godotengine.org */
|
2017-03-05 15:47:28 +01:00
|
|
|
/*************************************************************************/
|
2021-01-01 20:13:46 +01:00
|
|
|
/* Copyright (c) 2007-2021 Juan Linietsky, Ariel Manzur. */
|
|
|
|
/* Copyright (c) 2014-2021 Godot Engine contributors (cf. AUTHORS.md). */
|
2017-03-05 15:47:28 +01:00
|
|
|
/* */
|
|
|
|
/* Permission is hereby granted, free of charge, to any person obtaining */
|
|
|
|
/* a copy of this software and associated documentation files (the */
|
|
|
|
/* "Software"), to deal in the Software without restriction, including */
|
|
|
|
/* without limitation the rights to use, copy, modify, merge, publish, */
|
|
|
|
/* distribute, sublicense, and/or sell copies of the Software, and to */
|
|
|
|
/* permit persons to whom the Software is furnished to do so, subject to */
|
|
|
|
/* the following conditions: */
|
|
|
|
/* */
|
|
|
|
/* The above copyright notice and this permission notice shall be */
|
|
|
|
/* included in all copies or substantial portions of the Software. */
|
|
|
|
/* */
|
|
|
|
/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
|
|
|
|
/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
|
|
|
|
/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/
|
|
|
|
/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
|
|
|
|
/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
|
|
|
|
/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
|
|
|
|
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
|
|
|
|
/*************************************************************************/
|
|
|
|
|
2017-01-21 23:00:25 +01:00
|
|
|
// Author: Juan Linietsky <reduzio@gmail.com>, (C) 2006
|
|
|
|
|
|
|
|
#include "reverb.h"
|
2020-05-12 17:01:17 +02:00
|
|
|
|
2018-09-11 18:13:45 +02:00
|
|
|
#include "core/math/math_funcs.h"
|
2020-05-12 17:01:17 +02:00
|
|
|
|
2017-03-05 16:44:50 +01:00
|
|
|
#include <math.h>
|
2017-01-21 23:00:25 +01:00
|
|
|
|
2017-03-05 16:44:50 +01:00
|
|
|
const float Reverb::comb_tunings[MAX_COMBS] = {
|
2017-01-21 23:00:25 +01:00
|
|
|
//freeverb comb tunings
|
2018-10-04 15:38:52 +02:00
|
|
|
0.025306122448979593f,
|
|
|
|
0.026938775510204082f,
|
|
|
|
0.028956916099773241f,
|
|
|
|
0.03074829931972789f,
|
|
|
|
0.032244897959183672f,
|
|
|
|
0.03380952380952381f,
|
|
|
|
0.035306122448979592f,
|
|
|
|
0.036666666666666667f
|
2017-01-21 23:00:25 +01:00
|
|
|
};
|
|
|
|
|
2017-03-05 16:44:50 +01:00
|
|
|
const float Reverb::allpass_tunings[MAX_ALLPASS] = {
|
2017-01-21 23:00:25 +01:00
|
|
|
//freeverb allpass tunings
|
2018-10-04 15:38:52 +02:00
|
|
|
0.0051020408163265302f,
|
|
|
|
0.007732426303854875f,
|
|
|
|
0.01f,
|
|
|
|
0.012607709750566893f
|
2017-01-21 23:00:25 +01:00
|
|
|
};
|
|
|
|
|
2017-03-05 16:44:50 +01:00
|
|
|
void Reverb::process(float *p_src, float *p_dst, int p_frames) {
|
2020-05-14 16:41:43 +02:00
|
|
|
if (p_frames > INPUT_BUFFER_MAX_SIZE) {
|
2017-03-05 16:44:50 +01:00
|
|
|
p_frames = INPUT_BUFFER_MAX_SIZE;
|
2020-05-14 16:41:43 +02:00
|
|
|
}
|
2017-01-21 23:00:25 +01:00
|
|
|
|
2017-03-05 16:44:50 +01:00
|
|
|
int predelay_frames = lrint((params.predelay / 1000.0) * params.mix_rate);
|
2020-05-14 16:41:43 +02:00
|
|
|
if (predelay_frames < 10) {
|
2017-03-05 16:44:50 +01:00
|
|
|
predelay_frames = 10;
|
2020-05-14 16:41:43 +02:00
|
|
|
}
|
|
|
|
if (predelay_frames >= echo_buffer_size) {
|
2017-03-05 16:44:50 +01:00
|
|
|
predelay_frames = echo_buffer_size - 1;
|
2020-05-14 16:41:43 +02:00
|
|
|
}
|
2017-01-21 23:00:25 +01:00
|
|
|
|
2017-03-05 16:44:50 +01:00
|
|
|
for (int i = 0; i < p_frames; i++) {
|
2020-05-14 16:41:43 +02:00
|
|
|
if (echo_buffer_pos >= echo_buffer_size) {
|
2017-03-05 16:44:50 +01:00
|
|
|
echo_buffer_pos = 0;
|
2020-05-14 16:41:43 +02:00
|
|
|
}
|
2017-01-21 23:00:25 +01:00
|
|
|
|
2017-03-05 16:44:50 +01:00
|
|
|
int read_pos = echo_buffer_pos - predelay_frames;
|
2020-05-14 16:41:43 +02:00
|
|
|
while (read_pos < 0) {
|
2017-03-05 16:44:50 +01:00
|
|
|
read_pos += echo_buffer_size;
|
2020-05-14 16:41:43 +02:00
|
|
|
}
|
2017-01-21 23:00:25 +01:00
|
|
|
|
2017-03-05 16:44:50 +01:00
|
|
|
float in = undenormalise(echo_buffer[read_pos] * params.predelay_fb + p_src[i]);
|
2017-01-21 23:00:25 +01:00
|
|
|
|
2017-03-05 16:44:50 +01:00
|
|
|
echo_buffer[echo_buffer_pos] = in;
|
2017-01-21 23:00:25 +01:00
|
|
|
|
2017-03-05 16:44:50 +01:00
|
|
|
input_buffer[i] = in;
|
2017-01-21 23:00:25 +01:00
|
|
|
|
2017-03-05 16:44:50 +01:00
|
|
|
p_dst[i] = 0; //take the chance and clear this
|
2017-01-21 23:00:25 +01:00
|
|
|
|
|
|
|
echo_buffer_pos++;
|
|
|
|
}
|
|
|
|
|
2017-03-05 16:44:50 +01:00
|
|
|
if (params.hpf > 0) {
|
2020-04-03 11:50:40 +02:00
|
|
|
float hpaux = expf(-Math_TAU * params.hpf * 6000 / params.mix_rate);
|
2017-03-05 16:44:50 +01:00
|
|
|
float hp_a1 = (1.0 + hpaux) / 2.0;
|
|
|
|
float hp_a2 = -(1.0 + hpaux) / 2.0;
|
|
|
|
float hp_b1 = hpaux;
|
2017-01-21 23:00:25 +01:00
|
|
|
|
2017-03-05 16:44:50 +01:00
|
|
|
for (int i = 0; i < p_frames; i++) {
|
|
|
|
float in = input_buffer[i];
|
|
|
|
input_buffer[i] = in * hp_a1 + hpf_h1 * hp_a2 + hpf_h2 * hp_b1;
|
|
|
|
hpf_h2 = input_buffer[i];
|
|
|
|
hpf_h1 = in;
|
2017-01-21 23:00:25 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-03-05 16:44:50 +01:00
|
|
|
for (int i = 0; i < MAX_COMBS; i++) {
|
|
|
|
Comb &c = comb[i];
|
2017-01-21 23:00:25 +01:00
|
|
|
|
2017-03-05 16:44:50 +01:00
|
|
|
int size_limit = c.size - lrintf((float)c.extra_spread_frames * (1.0 - params.extra_spread));
|
|
|
|
for (int j = 0; j < p_frames; j++) {
|
2020-05-14 16:41:43 +02:00
|
|
|
if (c.pos >= size_limit) { //reset this now just in case
|
2017-03-05 16:44:50 +01:00
|
|
|
c.pos = 0;
|
2020-05-14 16:41:43 +02:00
|
|
|
}
|
2017-01-21 23:00:25 +01:00
|
|
|
|
2017-03-05 16:44:50 +01:00
|
|
|
float out = undenormalise(c.buffer[c.pos] * c.feedback);
|
|
|
|
out = out * (1.0 - c.damp) + c.damp_h * c.damp; //lowpass
|
|
|
|
c.damp_h = out;
|
|
|
|
c.buffer[c.pos] = input_buffer[j] + out;
|
|
|
|
p_dst[j] += out;
|
2017-01-21 23:00:25 +01:00
|
|
|
c.pos++;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-03-05 16:44:50 +01:00
|
|
|
static const float allpass_feedback = 0.7;
|
2017-01-21 23:00:25 +01:00
|
|
|
/* this one works, but the other version is just nicer....
|
|
|
|
int ap_size_limit[MAX_ALLPASS];
|
|
|
|
|
|
|
|
for (int i=0;i<MAX_ALLPASS;i++) {
|
|
|
|
AllPass &a=allpass[i];
|
|
|
|
ap_size_limit[i]=a.size-lrintf((float)a.extra_spread_frames*(1.0-params.extra_spread));
|
|
|
|
}
|
|
|
|
|
|
|
|
for (int i=0;i<p_frames;i++) {
|
|
|
|
float sample=p_dst[i];
|
|
|
|
float aux,in;
|
|
|
|
float AllPass*ap;
|
|
|
|
|
|
|
|
#define PROCESS_ALLPASS(m_ap) \
|
|
|
|
ap=&allpass[m_ap]; \
|
|
|
|
if (ap->pos>=ap_size_limit[m_ap]) \
|
|
|
|
ap->pos=0; \
|
|
|
|
aux=undenormalise(ap->buffer[ap->pos]); \
|
|
|
|
in=sample; \
|
|
|
|
sample=-in+aux; \
|
|
|
|
ap->pos++;
|
|
|
|
|
|
|
|
|
|
|
|
PROCESS_ALLPASS(0);
|
|
|
|
PROCESS_ALLPASS(1);
|
|
|
|
PROCESS_ALLPASS(2);
|
|
|
|
PROCESS_ALLPASS(3);
|
|
|
|
|
|
|
|
p_dst[i]=sample;
|
|
|
|
}
|
|
|
|
*/
|
|
|
|
|
2017-03-05 16:44:50 +01:00
|
|
|
for (int i = 0; i < MAX_ALLPASS; i++) {
|
|
|
|
AllPass &a = allpass[i];
|
|
|
|
int size_limit = a.size - lrintf((float)a.extra_spread_frames * (1.0 - params.extra_spread));
|
2017-01-21 23:00:25 +01:00
|
|
|
|
2017-03-05 16:44:50 +01:00
|
|
|
for (int j = 0; j < p_frames; j++) {
|
2020-05-14 16:41:43 +02:00
|
|
|
if (a.pos >= size_limit) {
|
2017-03-05 16:44:50 +01:00
|
|
|
a.pos = 0;
|
2020-05-14 16:41:43 +02:00
|
|
|
}
|
2017-01-21 23:00:25 +01:00
|
|
|
|
2017-03-05 16:44:50 +01:00
|
|
|
float aux = a.buffer[a.pos];
|
|
|
|
a.buffer[a.pos] = undenormalise(allpass_feedback * aux + p_dst[j]);
|
|
|
|
p_dst[j] = aux - allpass_feedback * a.buffer[a.pos];
|
2017-01-21 23:00:25 +01:00
|
|
|
a.pos++;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-03-05 16:44:50 +01:00
|
|
|
static const float wet_scale = 0.6;
|
2017-01-21 23:00:25 +01:00
|
|
|
|
2017-03-05 16:44:50 +01:00
|
|
|
for (int i = 0; i < p_frames; i++) {
|
|
|
|
p_dst[i] = p_dst[i] * params.wet * wet_scale + p_src[i] * params.dry;
|
2017-01-21 23:00:25 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
void Reverb::set_room_size(float p_size) {
|
2017-03-05 16:44:50 +01:00
|
|
|
params.room_size = p_size;
|
2017-01-21 23:00:25 +01:00
|
|
|
update_parameters();
|
|
|
|
}
|
2020-05-14 14:29:06 +02:00
|
|
|
|
2017-01-21 23:00:25 +01:00
|
|
|
void Reverb::set_damp(float p_damp) {
|
2017-03-05 16:44:50 +01:00
|
|
|
params.damp = p_damp;
|
2017-01-21 23:00:25 +01:00
|
|
|
update_parameters();
|
|
|
|
}
|
2020-05-14 14:29:06 +02:00
|
|
|
|
2017-01-21 23:00:25 +01:00
|
|
|
void Reverb::set_wet(float p_wet) {
|
2017-03-05 16:44:50 +01:00
|
|
|
params.wet = p_wet;
|
2017-01-21 23:00:25 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
void Reverb::set_dry(float p_dry) {
|
2017-03-05 16:44:50 +01:00
|
|
|
params.dry = p_dry;
|
2017-01-21 23:00:25 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
void Reverb::set_predelay(float p_predelay) {
|
2017-03-05 16:44:50 +01:00
|
|
|
params.predelay = p_predelay;
|
2017-01-21 23:00:25 +01:00
|
|
|
}
|
2020-05-14 14:29:06 +02:00
|
|
|
|
2017-01-21 23:00:25 +01:00
|
|
|
void Reverb::set_predelay_feedback(float p_predelay_fb) {
|
2017-03-05 16:44:50 +01:00
|
|
|
params.predelay_fb = p_predelay_fb;
|
2017-01-21 23:00:25 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
void Reverb::set_highpass(float p_frq) {
|
2020-05-14 16:41:43 +02:00
|
|
|
if (p_frq > 1) {
|
2017-03-05 16:44:50 +01:00
|
|
|
p_frq = 1;
|
2020-05-14 16:41:43 +02:00
|
|
|
}
|
|
|
|
if (p_frq < 0) {
|
2017-03-05 16:44:50 +01:00
|
|
|
p_frq = 0;
|
2020-05-14 16:41:43 +02:00
|
|
|
}
|
2017-03-05 16:44:50 +01:00
|
|
|
params.hpf = p_frq;
|
2017-01-21 23:00:25 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
void Reverb::set_extra_spread(float p_spread) {
|
2017-03-05 16:44:50 +01:00
|
|
|
params.extra_spread = p_spread;
|
2017-01-21 23:00:25 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
void Reverb::set_mix_rate(float p_mix_rate) {
|
2017-03-05 16:44:50 +01:00
|
|
|
params.mix_rate = p_mix_rate;
|
2017-01-21 23:00:25 +01:00
|
|
|
configure_buffers();
|
|
|
|
}
|
|
|
|
|
|
|
|
void Reverb::set_extra_spread_base(float p_sec) {
|
2017-03-05 16:44:50 +01:00
|
|
|
params.extra_spread_base = p_sec;
|
2017-01-21 23:00:25 +01:00
|
|
|
configure_buffers();
|
|
|
|
}
|
|
|
|
|
|
|
|
void Reverb::configure_buffers() {
|
2017-03-24 21:45:31 +01:00
|
|
|
clear_buffers(); //clear if necessary
|
2017-01-21 23:00:25 +01:00
|
|
|
|
2017-03-05 16:44:50 +01:00
|
|
|
for (int i = 0; i < MAX_COMBS; i++) {
|
|
|
|
Comb &c = comb[i];
|
2017-01-21 23:00:25 +01:00
|
|
|
|
2017-03-05 16:44:50 +01:00
|
|
|
c.extra_spread_frames = lrint(params.extra_spread_base * params.mix_rate);
|
2017-01-21 23:00:25 +01:00
|
|
|
|
2017-03-05 16:44:50 +01:00
|
|
|
int len = lrint(comb_tunings[i] * params.mix_rate) + c.extra_spread_frames;
|
2020-05-14 16:41:43 +02:00
|
|
|
if (len < 5) {
|
2017-03-05 16:44:50 +01:00
|
|
|
len = 5; //may this happen?
|
2020-05-14 16:41:43 +02:00
|
|
|
}
|
2017-01-21 23:00:25 +01:00
|
|
|
|
2017-03-05 16:44:50 +01:00
|
|
|
c.buffer = memnew_arr(float, len);
|
|
|
|
c.pos = 0;
|
2020-05-14 16:41:43 +02:00
|
|
|
for (int j = 0; j < len; j++) {
|
2017-03-05 16:44:50 +01:00
|
|
|
c.buffer[j] = 0;
|
2020-05-14 16:41:43 +02:00
|
|
|
}
|
2017-03-05 16:44:50 +01:00
|
|
|
c.size = len;
|
2017-01-21 23:00:25 +01:00
|
|
|
}
|
|
|
|
|
2017-03-05 16:44:50 +01:00
|
|
|
for (int i = 0; i < MAX_ALLPASS; i++) {
|
|
|
|
AllPass &a = allpass[i];
|
2017-01-21 23:00:25 +01:00
|
|
|
|
2017-03-05 16:44:50 +01:00
|
|
|
a.extra_spread_frames = lrint(params.extra_spread_base * params.mix_rate);
|
2017-01-21 23:00:25 +01:00
|
|
|
|
2017-03-05 16:44:50 +01:00
|
|
|
int len = lrint(allpass_tunings[i] * params.mix_rate) + a.extra_spread_frames;
|
2020-05-14 16:41:43 +02:00
|
|
|
if (len < 5) {
|
2017-03-05 16:44:50 +01:00
|
|
|
len = 5; //may this happen?
|
2020-05-14 16:41:43 +02:00
|
|
|
}
|
2017-01-21 23:00:25 +01:00
|
|
|
|
2017-03-05 16:44:50 +01:00
|
|
|
a.buffer = memnew_arr(float, len);
|
|
|
|
a.pos = 0;
|
2020-05-14 16:41:43 +02:00
|
|
|
for (int j = 0; j < len; j++) {
|
2017-03-05 16:44:50 +01:00
|
|
|
a.buffer[j] = 0;
|
2020-05-14 16:41:43 +02:00
|
|
|
}
|
2017-03-05 16:44:50 +01:00
|
|
|
a.size = len;
|
2017-01-21 23:00:25 +01:00
|
|
|
}
|
|
|
|
|
2017-03-05 16:44:50 +01:00
|
|
|
echo_buffer_size = (int)(((float)MAX_ECHO_MS / 1000.0) * params.mix_rate + 1.0);
|
|
|
|
echo_buffer = memnew_arr(float, echo_buffer_size);
|
|
|
|
for (int i = 0; i < echo_buffer_size; i++) {
|
|
|
|
echo_buffer[i] = 0;
|
2017-01-21 23:00:25 +01:00
|
|
|
}
|
|
|
|
|
2017-03-05 16:44:50 +01:00
|
|
|
echo_buffer_pos = 0;
|
2017-01-21 23:00:25 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
void Reverb::update_parameters() {
|
|
|
|
//more freeverb derived constants
|
|
|
|
static const float room_scale = 0.28f;
|
|
|
|
static const float room_offset = 0.7f;
|
|
|
|
|
2017-03-05 16:44:50 +01:00
|
|
|
for (int i = 0; i < MAX_COMBS; i++) {
|
|
|
|
Comb &c = comb[i];
|
|
|
|
c.feedback = room_offset + params.room_size * room_scale;
|
2020-05-14 16:41:43 +02:00
|
|
|
if (c.feedback < room_offset) {
|
2017-03-05 16:44:50 +01:00
|
|
|
c.feedback = room_offset;
|
2020-05-14 16:41:43 +02:00
|
|
|
} else if (c.feedback > (room_offset + room_scale)) {
|
2017-03-05 16:44:50 +01:00
|
|
|
c.feedback = (room_offset + room_scale);
|
2020-05-14 16:41:43 +02:00
|
|
|
}
|
2017-01-21 23:00:25 +01:00
|
|
|
|
2021-06-07 10:17:32 +02:00
|
|
|
float auxdmp = params.damp / 2.0 + 0.5; //only half the range (0.5 .. 1.0 is enough)
|
2017-03-05 16:44:50 +01:00
|
|
|
auxdmp *= auxdmp;
|
2017-01-21 23:00:25 +01:00
|
|
|
|
2020-04-03 11:50:40 +02:00
|
|
|
c.damp = expf(-Math_TAU * auxdmp * 10000 / params.mix_rate); // 0 .. 10khz
|
2017-01-21 23:00:25 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
void Reverb::clear_buffers() {
|
2020-05-14 16:41:43 +02:00
|
|
|
if (echo_buffer) {
|
2017-01-21 23:00:25 +01:00
|
|
|
memdelete_arr(echo_buffer);
|
2020-05-14 16:41:43 +02:00
|
|
|
}
|
2017-01-21 23:00:25 +01:00
|
|
|
|
2017-03-05 16:44:50 +01:00
|
|
|
for (int i = 0; i < MAX_COMBS; i++) {
|
2020-05-14 16:41:43 +02:00
|
|
|
if (comb[i].buffer) {
|
2017-01-21 23:00:25 +01:00
|
|
|
memdelete_arr(comb[i].buffer);
|
2020-05-14 16:41:43 +02:00
|
|
|
}
|
2017-01-21 23:00:25 +01:00
|
|
|
|
2020-04-02 01:20:12 +02:00
|
|
|
comb[i].buffer = nullptr;
|
2017-01-21 23:00:25 +01:00
|
|
|
}
|
|
|
|
|
2017-03-05 16:44:50 +01:00
|
|
|
for (int i = 0; i < MAX_ALLPASS; i++) {
|
2020-05-14 16:41:43 +02:00
|
|
|
if (allpass[i].buffer) {
|
2017-01-21 23:00:25 +01:00
|
|
|
memdelete_arr(allpass[i].buffer);
|
2020-05-14 16:41:43 +02:00
|
|
|
}
|
2017-01-21 23:00:25 +01:00
|
|
|
|
2020-04-02 01:20:12 +02:00
|
|
|
allpass[i].buffer = nullptr;
|
2017-01-21 23:00:25 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
Reverb::Reverb() {
|
2017-03-05 16:44:50 +01:00
|
|
|
params.room_size = 0.8;
|
|
|
|
params.damp = 0.5;
|
|
|
|
params.dry = 1.0;
|
|
|
|
params.wet = 0.0;
|
|
|
|
params.mix_rate = 44100;
|
|
|
|
params.extra_spread_base = 0;
|
|
|
|
params.extra_spread = 1.0;
|
|
|
|
params.predelay = 150;
|
|
|
|
params.predelay_fb = 0.4;
|
|
|
|
params.hpf = 0;
|
|
|
|
|
|
|
|
input_buffer = memnew_arr(float, INPUT_BUFFER_MAX_SIZE);
|
2017-01-21 23:00:25 +01:00
|
|
|
|
|
|
|
configure_buffers();
|
|
|
|
update_parameters();
|
|
|
|
}
|
|
|
|
|
|
|
|
Reverb::~Reverb() {
|
|
|
|
memdelete_arr(input_buffer);
|
|
|
|
clear_buffers();
|
|
|
|
}
|