mirror of
https://github.com/dborth/snes9xgx.git
synced 2024-12-25 02:31:50 +01:00
Backport and adjusted the Refactored APU Resampler (#821)
This commit is contained in:
parent
6e1a197edd
commit
087ac22029
@ -10,7 +10,7 @@
|
|||||||
#include "../msu1.h"
|
#include "../msu1.h"
|
||||||
#include "../snapshot.h"
|
#include "../snapshot.h"
|
||||||
#include "../display.h"
|
#include "../display.h"
|
||||||
#include "hermite_resampler.h"
|
#include "resampler.h"
|
||||||
|
|
||||||
#define APU_DEFAULT_INPUT_RATE 32040
|
#define APU_DEFAULT_INPUT_RATE 32040
|
||||||
#define APU_MINIMUM_SAMPLE_COUNT 512
|
#define APU_MINIMUM_SAMPLE_COUNT 512
|
||||||
@ -62,7 +62,7 @@ namespace spc
|
|||||||
static uint32 ratio_denominator = APU_DENOMINATOR_NTSC;
|
static uint32 ratio_denominator = APU_DENOMINATOR_NTSC;
|
||||||
|
|
||||||
static double dynamic_rate_multiplier = 1.0;
|
static double dynamic_rate_multiplier = 1.0;
|
||||||
}
|
} // namespace spc
|
||||||
|
|
||||||
namespace msu
|
namespace msu
|
||||||
{
|
{
|
||||||
@ -71,7 +71,7 @@ namespace msu
|
|||||||
static Resampler *resampler = NULL;
|
static Resampler *resampler = NULL;
|
||||||
static int resample_buffer_size = -1;
|
static int resample_buffer_size = -1;
|
||||||
static uint8 *resample_buffer = NULL;
|
static uint8 *resample_buffer = NULL;
|
||||||
}
|
} // namespace msu
|
||||||
|
|
||||||
static void EightBitize (uint8 *, int);
|
static void EightBitize (uint8 *, int);
|
||||||
static void DeStereo (uint8 *, int);
|
static void DeStereo (uint8 *, int);
|
||||||
@ -359,7 +359,7 @@ bool8 S9xInitSound (int buffer_ms, int lag_ms)
|
|||||||
arguments. Use 2x in the resampler for buffer leveling with SoundSync */
|
arguments. Use 2x in the resampler for buffer leveling with SoundSync */
|
||||||
if (!spc::resampler)
|
if (!spc::resampler)
|
||||||
{
|
{
|
||||||
spc::resampler = new HermiteResampler(spc::buffer_size >> (Settings.SoundSync ? 0 : 1));
|
spc::resampler = new Resampler(spc::buffer_size >> (Settings.SoundSync ? 0 : 1));
|
||||||
if (!spc::resampler)
|
if (!spc::resampler)
|
||||||
{
|
{
|
||||||
delete[] spc::landing_buffer;
|
delete[] spc::landing_buffer;
|
||||||
@ -372,7 +372,7 @@ bool8 S9xInitSound (int buffer_ms, int lag_ms)
|
|||||||
|
|
||||||
if (!msu::resampler)
|
if (!msu::resampler)
|
||||||
{
|
{
|
||||||
msu::resampler = new HermiteResampler(msu::buffer_size >> (Settings.SoundSync ? 0 : 1));
|
msu::resampler = new Resampler(msu::buffer_size >> (Settings.SoundSync ? 0 : 1));
|
||||||
if (!msu::resampler)
|
if (!msu::resampler)
|
||||||
{
|
{
|
||||||
delete[] msu::landing_buffer;
|
delete[] msu::landing_buffer;
|
||||||
|
@ -1,143 +0,0 @@
|
|||||||
/*****************************************************************************\
|
|
||||||
Snes9x - Portable Super Nintendo Entertainment System (TM) emulator.
|
|
||||||
This file is licensed under the Snes9x License.
|
|
||||||
For further information, consult the LICENSE file in the root directory.
|
|
||||||
\*****************************************************************************/
|
|
||||||
|
|
||||||
#ifndef __HERMITE_RESAMPLER_H
|
|
||||||
#define __HERMITE_RESAMPLER_H
|
|
||||||
|
|
||||||
#include "resampler.h"
|
|
||||||
|
|
||||||
#undef CLAMP
|
|
||||||
#undef SHORT_CLAMP
|
|
||||||
#define CLAMP(x, low, high) (((x) > (high)) ? (high) : (((x) < (low)) ? (low) : (x)))
|
|
||||||
#define SHORT_CLAMP(n) ((short) CLAMP((n), -32768, 32767))
|
|
||||||
|
|
||||||
class HermiteResampler : public Resampler
|
|
||||||
{
|
|
||||||
protected:
|
|
||||||
|
|
||||||
float r_step;
|
|
||||||
float r_frac;
|
|
||||||
int r_left[4], r_right[4];
|
|
||||||
|
|
||||||
static inline float
|
|
||||||
hermite (float mu1, float a, float b, float c, float d)
|
|
||||||
{
|
|
||||||
float mu2, mu3, m0, m1, a0, a1, a2, a3;
|
|
||||||
|
|
||||||
mu2 = mu1 * mu1;
|
|
||||||
mu3 = mu2 * mu1;
|
|
||||||
|
|
||||||
m0 = (c - a) * 0.5;
|
|
||||||
m1 = (d - b) * 0.5;
|
|
||||||
|
|
||||||
a0 = +2 * mu3 - 3 * mu2 + 1;
|
|
||||||
a1 = mu3 - 2 * mu2 + mu1;
|
|
||||||
a2 = mu3 - mu2;
|
|
||||||
a3 = -2 * mu3 + 3 * mu2;
|
|
||||||
|
|
||||||
return (a0 * b) + (a1 * m0) + (a2 * m1) + (a3 * c);
|
|
||||||
}
|
|
||||||
|
|
||||||
public:
|
|
||||||
HermiteResampler (int num_samples) : Resampler (num_samples)
|
|
||||||
{
|
|
||||||
clear ();
|
|
||||||
}
|
|
||||||
|
|
||||||
~HermiteResampler ()
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
void
|
|
||||||
time_ratio (double ratio)
|
|
||||||
{
|
|
||||||
r_step = ratio;
|
|
||||||
}
|
|
||||||
|
|
||||||
void
|
|
||||||
clear (void)
|
|
||||||
{
|
|
||||||
ring_buffer::clear ();
|
|
||||||
r_frac = 1.0;
|
|
||||||
r_left [0] = r_left [1] = r_left [2] = r_left [3] = 0;
|
|
||||||
r_right[0] = r_right[1] = r_right[2] = r_right[3] = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
void
|
|
||||||
read (short *data, int num_samples)
|
|
||||||
{
|
|
||||||
//If we are outputting the exact same ratio as the input, pull directly from the input buffer
|
|
||||||
if (r_step == 1.0)
|
|
||||||
{
|
|
||||||
ring_buffer::pull((unsigned char*)data, num_samples * sizeof(short));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
int i_position = start >> 1;
|
|
||||||
int max_samples = buffer_size >> 1;
|
|
||||||
short *internal_buffer = (short *) buffer;
|
|
||||||
int o_position = 0;
|
|
||||||
int consumed = 0;
|
|
||||||
|
|
||||||
while (o_position < num_samples && consumed < buffer_size)
|
|
||||||
{
|
|
||||||
int s_left = internal_buffer[i_position];
|
|
||||||
int s_right = internal_buffer[i_position + 1];
|
|
||||||
float hermite_val[2];
|
|
||||||
|
|
||||||
while (r_frac <= 1.0 && o_position < num_samples)
|
|
||||||
{
|
|
||||||
hermite_val[0] = hermite (r_frac, r_left [0], r_left [1], r_left [2], r_left [3]);
|
|
||||||
hermite_val[1] = hermite (r_frac, r_right[0], r_right[1], r_right[2], r_right[3]);
|
|
||||||
data[o_position] = SHORT_CLAMP (hermite_val[0]);
|
|
||||||
data[o_position + 1] = SHORT_CLAMP (hermite_val[1]);
|
|
||||||
|
|
||||||
o_position += 2;
|
|
||||||
|
|
||||||
r_frac += r_step;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (r_frac > 1.0)
|
|
||||||
{
|
|
||||||
r_left [0] = r_left [1];
|
|
||||||
r_left [1] = r_left [2];
|
|
||||||
r_left [2] = r_left [3];
|
|
||||||
r_left [3] = s_left;
|
|
||||||
|
|
||||||
r_right[0] = r_right[1];
|
|
||||||
r_right[1] = r_right[2];
|
|
||||||
r_right[2] = r_right[3];
|
|
||||||
r_right[3] = s_right;
|
|
||||||
|
|
||||||
r_frac -= 1.0;
|
|
||||||
|
|
||||||
i_position += 2;
|
|
||||||
if (i_position >= max_samples)
|
|
||||||
i_position -= max_samples;
|
|
||||||
consumed += 2;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
size -= consumed << 1;
|
|
||||||
start += consumed << 1;
|
|
||||||
if (start >= buffer_size)
|
|
||||||
start -= buffer_size;
|
|
||||||
}
|
|
||||||
|
|
||||||
inline int
|
|
||||||
avail (void)
|
|
||||||
{
|
|
||||||
//If we are outputting the exact same ratio as the input, find out directly from the input buffer
|
|
||||||
if (r_step == 1.0)
|
|
||||||
{
|
|
||||||
return (ring_buffer::space_filled() + sizeof(short) - 1) / sizeof(short);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (int) floor (((size >> 2) - r_frac) / r_step) * 2;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
#endif /* __HERMITE_RESAMPLER_H */
|
|
@ -4,61 +4,216 @@
|
|||||||
For further information, consult the LICENSE file in the root directory.
|
For further information, consult the LICENSE file in the root directory.
|
||||||
\*****************************************************************************/
|
\*****************************************************************************/
|
||||||
|
|
||||||
#ifndef __RESAMPLER_H
|
#ifndef __NEW_RESAMPLER_H
|
||||||
#define __RESAMPLER_H
|
#define __NEW_RESAMPLER_H
|
||||||
|
|
||||||
#include "ring_buffer.h"
|
#include <cstring>
|
||||||
|
#include <cassert>
|
||||||
|
#if __cplusplus >= 201103L
|
||||||
|
#include <cstdint>
|
||||||
|
#else
|
||||||
|
#include <stdint.h>
|
||||||
|
#endif
|
||||||
|
#include <cmath>
|
||||||
|
|
||||||
class Resampler : public ring_buffer
|
class Resampler
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
virtual void clear (void) = 0;
|
int size;
|
||||||
virtual void time_ratio (double) = 0;
|
int buffer_size;
|
||||||
virtual void read (short *, int) = 0;
|
int start;
|
||||||
virtual int avail (void) = 0;
|
int16_t *buffer;
|
||||||
|
|
||||||
Resampler (int num_samples) : ring_buffer (num_samples << 1)
|
float r_step;
|
||||||
|
float r_frac;
|
||||||
|
int r_left[4], r_right[4];
|
||||||
|
|
||||||
|
static inline int16_t short_clamp(int n)
|
||||||
{
|
{
|
||||||
|
return (int16_t)(((int16_t)n != n) ? (n >> 31) ^ 0x7fff : n);
|
||||||
}
|
}
|
||||||
|
|
||||||
virtual ~Resampler ()
|
static inline int min(int a, int b)
|
||||||
{
|
{
|
||||||
|
return ((a) < (b) ? (a) : (b));
|
||||||
}
|
}
|
||||||
|
|
||||||
inline bool
|
static inline float hermite(float mu1, float a, float b, float c, float d)
|
||||||
push (short *src, int num_samples)
|
|
||||||
{
|
{
|
||||||
if (max_write () < num_samples)
|
float mu2, mu3, m0, m1, a0, a1, a2, a3;
|
||||||
|
|
||||||
|
mu2 = mu1 * mu1;
|
||||||
|
mu3 = mu2 * mu1;
|
||||||
|
|
||||||
|
m0 = (c - a) * 0.5;
|
||||||
|
m1 = (d - b) * 0.5;
|
||||||
|
|
||||||
|
a0 = +2 * mu3 - 3 * mu2 + 1;
|
||||||
|
a1 = mu3 - 2 * mu2 + mu1;
|
||||||
|
a2 = mu3 - mu2;
|
||||||
|
a3 = -2 * mu3 + 3 * mu2;
|
||||||
|
|
||||||
|
return (a0 * b) + (a1 * m0) + (a2 * m1) + (a3 * c);
|
||||||
|
}
|
||||||
|
|
||||||
|
Resampler(int num_samples)
|
||||||
|
{
|
||||||
|
this->buffer_size = num_samples;
|
||||||
|
buffer = new int16_t[this->buffer_size];
|
||||||
|
r_step = 1.0;
|
||||||
|
clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
~Resampler()
|
||||||
|
{
|
||||||
|
delete[] buffer;
|
||||||
|
buffer = NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
inline void time_ratio(double ratio)
|
||||||
|
{
|
||||||
|
r_step = ratio;
|
||||||
|
}
|
||||||
|
|
||||||
|
inline void clear(void)
|
||||||
|
{
|
||||||
|
start = 0;
|
||||||
|
size = 0;
|
||||||
|
memset(buffer, 0, buffer_size * 2);
|
||||||
|
|
||||||
|
r_frac = 0.0;
|
||||||
|
r_left[0] = r_left[1] = r_left[2] = r_left[3] = 0;
|
||||||
|
r_right[0] = r_right[1] = r_right[2] = r_right[3] = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
inline bool pull(int16_t *dst, int num_samples)
|
||||||
|
{
|
||||||
|
if (space_filled() < num_samples)
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
!num_samples || ring_buffer::push ((unsigned char *) src, num_samples << 1);
|
memcpy(dst, buffer + start, min(num_samples, buffer_size - start) * 2);
|
||||||
|
|
||||||
|
if (num_samples > (buffer_size - start))
|
||||||
|
memcpy(dst + (buffer_size - start), buffer, (num_samples - (buffer_size - start)) * 2);
|
||||||
|
|
||||||
|
start = (start + num_samples) % buffer_size;
|
||||||
|
size -= num_samples;
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
inline int
|
inline void push_sample(int16_t l, int16_t r)
|
||||||
space_empty (void) const
|
{
|
||||||
|
if (space_empty() >= 2)
|
||||||
|
{
|
||||||
|
int end = start + size;
|
||||||
|
if (end >= buffer_size)
|
||||||
|
end -= buffer_size;
|
||||||
|
buffer[end] = l;
|
||||||
|
buffer[end + 1] = r;
|
||||||
|
size += 2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
inline bool push(int16_t *src, int num_samples)
|
||||||
|
{
|
||||||
|
if (space_empty() < num_samples)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
int end = start + size;
|
||||||
|
if (end > buffer_size)
|
||||||
|
end -= buffer_size;
|
||||||
|
int first_write_size = min(num_samples, buffer_size - end);
|
||||||
|
|
||||||
|
memcpy(buffer + end, src, first_write_size * 2);
|
||||||
|
|
||||||
|
if (num_samples > first_write_size)
|
||||||
|
memcpy(buffer, src + first_write_size, (num_samples - first_write_size) * 2);
|
||||||
|
|
||||||
|
size += num_samples;
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void read(int16_t *data, int num_samples)
|
||||||
|
{
|
||||||
|
//If we are outputting the exact same ratio as the input, pull directly from the input buffer
|
||||||
|
if (r_step == 1.0)
|
||||||
|
{
|
||||||
|
pull(data, num_samples);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
assert((num_samples & 1) == 0); // resampler always processes both stereo samples
|
||||||
|
int o_position = 0;
|
||||||
|
|
||||||
|
while (o_position < num_samples && size > 0)
|
||||||
|
{
|
||||||
|
int s_left = buffer[start];
|
||||||
|
int s_right = buffer[start + 1];
|
||||||
|
int hermite_val[2];
|
||||||
|
|
||||||
|
while (r_frac <= 1.0 && o_position < num_samples)
|
||||||
|
{
|
||||||
|
hermite_val[0] = (int)hermite(r_frac, r_left[0], r_left[1], r_left[2], r_left[3]);
|
||||||
|
hermite_val[1] = (int)hermite(r_frac, r_right[0], r_right[1], r_right[2], r_right[3]);
|
||||||
|
data[o_position] = short_clamp(hermite_val[0]);
|
||||||
|
data[o_position + 1] = short_clamp(hermite_val[1]);
|
||||||
|
|
||||||
|
o_position += 2;
|
||||||
|
|
||||||
|
r_frac += r_step;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (r_frac > 1.0)
|
||||||
|
{
|
||||||
|
r_left[0] = r_left[1];
|
||||||
|
r_left[1] = r_left[2];
|
||||||
|
r_left[2] = r_left[3];
|
||||||
|
r_left[3] = s_left;
|
||||||
|
|
||||||
|
r_right[0] = r_right[1];
|
||||||
|
r_right[1] = r_right[2];
|
||||||
|
r_right[2] = r_right[3];
|
||||||
|
r_right[3] = s_right;
|
||||||
|
|
||||||
|
r_frac -= 1.0;
|
||||||
|
|
||||||
|
start += 2;
|
||||||
|
if (start >= buffer_size)
|
||||||
|
start -= buffer_size;
|
||||||
|
size -= 2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
inline int space_empty(void) const
|
||||||
{
|
{
|
||||||
return buffer_size - size;
|
return buffer_size - size;
|
||||||
}
|
}
|
||||||
|
|
||||||
inline int
|
inline int space_filled(void) const
|
||||||
space_filled (void) const
|
|
||||||
{
|
{
|
||||||
return size;
|
return size;
|
||||||
}
|
}
|
||||||
|
|
||||||
inline int
|
inline int avail(void)
|
||||||
max_write (void) const
|
|
||||||
{
|
{
|
||||||
return space_empty () >> 1;
|
//If we are outputting the exact same ratio as the input, find out directly from the input buffer
|
||||||
|
if (r_step == 1.0)
|
||||||
|
return size;
|
||||||
|
|
||||||
|
return (int)trunc(((size >> 1) - r_frac) / r_step) * 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
inline void
|
void resize(int num_samples)
|
||||||
resize (int num_samples)
|
|
||||||
{
|
{
|
||||||
ring_buffer::resize (num_samples << 1);
|
if (buffer)
|
||||||
|
delete[] buffer;
|
||||||
|
buffer_size = num_samples;
|
||||||
|
buffer = new int16_t[buffer_size];
|
||||||
|
clear();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
#endif /* __RESAMPLER_H */
|
#endif /* __NEW_RESAMPLER_H */
|
@ -1,115 +0,0 @@
|
|||||||
/*****************************************************************************\
|
|
||||||
Snes9x - Portable Super Nintendo Entertainment System (TM) emulator.
|
|
||||||
This file is licensed under the Snes9x License.
|
|
||||||
For further information, consult the LICENSE file in the root directory.
|
|
||||||
\*****************************************************************************/
|
|
||||||
|
|
||||||
#ifndef __RING_BUFFER_H
|
|
||||||
#define __RING_BUFFER_H
|
|
||||||
|
|
||||||
#include <string.h>
|
|
||||||
|
|
||||||
#undef MIN
|
|
||||||
#define MIN(a, b) ((a) < (b) ? (a) : (b))
|
|
||||||
|
|
||||||
class ring_buffer
|
|
||||||
{
|
|
||||||
protected:
|
|
||||||
int size;
|
|
||||||
int buffer_size;
|
|
||||||
int start;
|
|
||||||
unsigned char *buffer;
|
|
||||||
|
|
||||||
public:
|
|
||||||
ring_buffer (int buffer_size)
|
|
||||||
{
|
|
||||||
this->buffer_size = buffer_size;
|
|
||||||
buffer = new unsigned char[this->buffer_size];
|
|
||||||
memset (buffer, 0, this->buffer_size);
|
|
||||||
|
|
||||||
size = 0;
|
|
||||||
start = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
~ring_buffer (void)
|
|
||||||
{
|
|
||||||
delete[] buffer;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool
|
|
||||||
push (unsigned char *src, int bytes)
|
|
||||||
{
|
|
||||||
if (space_empty () < bytes)
|
|
||||||
return false;
|
|
||||||
|
|
||||||
int end = (start + size) % buffer_size;
|
|
||||||
int first_write_size = MIN (bytes, buffer_size - end);
|
|
||||||
|
|
||||||
memcpy (buffer + end, src, first_write_size);
|
|
||||||
|
|
||||||
if (bytes > first_write_size)
|
|
||||||
memcpy (buffer, src + first_write_size, bytes - first_write_size);
|
|
||||||
|
|
||||||
size += bytes;
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool
|
|
||||||
pull (unsigned char *dst, int bytes)
|
|
||||||
{
|
|
||||||
if (space_filled () < bytes)
|
|
||||||
return false;
|
|
||||||
|
|
||||||
memcpy (dst, buffer + start, MIN (bytes, buffer_size - start));
|
|
||||||
|
|
||||||
if (bytes > (buffer_size - start))
|
|
||||||
memcpy (dst + (buffer_size - start), buffer, bytes - (buffer_size - start));
|
|
||||||
|
|
||||||
start = (start + bytes) % buffer_size;
|
|
||||||
size -= bytes;
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
inline int
|
|
||||||
space_empty (void) const
|
|
||||||
{
|
|
||||||
return buffer_size - size;
|
|
||||||
}
|
|
||||||
|
|
||||||
inline int
|
|
||||||
space_filled (void) const
|
|
||||||
{
|
|
||||||
return size;
|
|
||||||
}
|
|
||||||
|
|
||||||
void
|
|
||||||
clear (void)
|
|
||||||
{
|
|
||||||
start = 0;
|
|
||||||
size = 0;
|
|
||||||
memset (buffer, 0, buffer_size);
|
|
||||||
}
|
|
||||||
|
|
||||||
void
|
|
||||||
resize (int size)
|
|
||||||
{
|
|
||||||
delete[] buffer;
|
|
||||||
buffer_size = size;
|
|
||||||
buffer = new unsigned char[buffer_size];
|
|
||||||
memset (buffer, 0, this->buffer_size);
|
|
||||||
|
|
||||||
this->size = 0;
|
|
||||||
start = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
inline void
|
|
||||||
cache_silence (void)
|
|
||||||
{
|
|
||||||
clear ();
|
|
||||||
size = buffer_size;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
#endif
|
|
@ -9,7 +9,7 @@
|
|||||||
#define _SNES9X_H_
|
#define _SNES9X_H_
|
||||||
|
|
||||||
#ifndef VERSION
|
#ifndef VERSION
|
||||||
#define VERSION "1.59"
|
#define VERSION "1.59.2"
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
#include "port.h"
|
#include "port.h"
|
||||||
|
Loading…
Reference in New Issue
Block a user