mirror of
https://github.com/hathach/tinyusb.git
synced 2025-01-31 05:52:55 +08:00
Merge pull request #2259 from HiFiPhile/uac_interl
Fix UAC encoding, update example.
This commit is contained in:
commit
118823c254
@ -28,6 +28,11 @@ target_include_directories(${PROJECT} PUBLIC
|
|||||||
${CMAKE_CURRENT_SOURCE_DIR}/src
|
${CMAKE_CURRENT_SOURCE_DIR}/src
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Add libm for GCC
|
||||||
|
if (CMAKE_C_COMPILER_ID STREQUAL "GNU")
|
||||||
|
target_link_libraries(${PROJECT} PUBLIC m)
|
||||||
|
endif()
|
||||||
|
|
||||||
# Configure compilation flags and libraries for the example without RTOS.
|
# Configure compilation flags and libraries for the example without RTOS.
|
||||||
# See the corresponding function in hw/bsp/FAMILY/family.cmake for details.
|
# See the corresponding function in hw/bsp/FAMILY/family.cmake for details.
|
||||||
family_configure_device_example(${PROJECT} noos)
|
family_configure_device_example(${PROJECT} noos)
|
||||||
|
@ -5,7 +5,10 @@ INC += \
|
|||||||
$(TOP)/hw \
|
$(TOP)/hw \
|
||||||
|
|
||||||
# Example source
|
# Example source
|
||||||
EXAMPLE_SOURCE += $(wildcard src/*.c)
|
EXAMPLE_SOURCE += \
|
||||||
|
src/main.c \
|
||||||
|
src/usb_descriptors.c \
|
||||||
|
|
||||||
SRC_C += $(addprefix $(CURRENT_PATH)/, $(EXAMPLE_SOURCE))
|
SRC_C += $(addprefix $(CURRENT_PATH)/, $(EXAMPLE_SOURCE))
|
||||||
|
|
||||||
include ../../rules.mk
|
include ../../rules.mk
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
mcu:SAMD11
|
mcu:SAMD11
|
||||||
mcu:SAME5X
|
mcu:SAME5X
|
||||||
mcu:SAMG
|
mcu:SAMG
|
||||||
|
family:broadcom_64bit
|
||||||
|
@ -34,17 +34,16 @@
|
|||||||
#include <stdlib.h>
|
#include <stdlib.h>
|
||||||
#include <stdio.h>
|
#include <stdio.h>
|
||||||
#include <string.h>
|
#include <string.h>
|
||||||
|
#include <math.h>
|
||||||
|
|
||||||
#include "bsp/board_api.h"
|
#include "bsp/board_api.h"
|
||||||
#include "tusb.h"
|
#include "tusb.h"
|
||||||
|
#include "tusb_config.h"
|
||||||
|
|
||||||
//--------------------------------------------------------------------+
|
//--------------------------------------------------------------------+
|
||||||
// MACRO CONSTANT TYPEDEF PROTYPES
|
// MACRO CONSTANT TYPEDEF PROTYPES
|
||||||
//--------------------------------------------------------------------+
|
//--------------------------------------------------------------------+
|
||||||
|
#define AUDIO_SAMPLE_RATE CFG_TUD_AUDIO_FUNC_1_SAMPLE_RATE
|
||||||
#ifndef AUDIO_SAMPLE_RATE
|
|
||||||
#define AUDIO_SAMPLE_RATE 48000
|
|
||||||
#endif
|
|
||||||
|
|
||||||
/* Blink pattern
|
/* Blink pattern
|
||||||
* - 250 ms : device not mounted
|
* - 250 ms : device not mounted
|
||||||
@ -70,7 +69,7 @@ uint8_t clkValid;
|
|||||||
audio_control_range_2_n_t(1) volumeRng[CFG_TUD_AUDIO_FUNC_1_N_CHANNELS_TX+1]; // Volume range state
|
audio_control_range_2_n_t(1) volumeRng[CFG_TUD_AUDIO_FUNC_1_N_CHANNELS_TX+1]; // Volume range state
|
||||||
audio_control_range_4_n_t(1) sampleFreqRng; // Sample frequency range state
|
audio_control_range_4_n_t(1) sampleFreqRng; // Sample frequency range state
|
||||||
|
|
||||||
// Audio test data
|
// Audio test data, each buffer contains 2 channels, buffer[0] for CH0-1, buffer[1] for CH1-2
|
||||||
uint16_t i2s_dummy_buffer[CFG_TUD_AUDIO_FUNC_1_N_TX_SUPP_SW_FIFO][CFG_TUD_AUDIO_FUNC_1_TX_SUPP_SW_FIFO_SZ/2]; // Ensure half word aligned
|
uint16_t i2s_dummy_buffer[CFG_TUD_AUDIO_FUNC_1_N_TX_SUPP_SW_FIFO][CFG_TUD_AUDIO_FUNC_1_TX_SUPP_SW_FIFO_SZ/2]; // Ensure half word aligned
|
||||||
|
|
||||||
void led_blinking_task(void);
|
void led_blinking_task(void);
|
||||||
@ -97,6 +96,27 @@ int main(void)
|
|||||||
sampleFreqRng.subrange[0].bMax = AUDIO_SAMPLE_RATE;
|
sampleFreqRng.subrange[0].bMax = AUDIO_SAMPLE_RATE;
|
||||||
sampleFreqRng.subrange[0].bRes = 0;
|
sampleFreqRng.subrange[0].bRes = 0;
|
||||||
|
|
||||||
|
// Generate dummy data
|
||||||
|
uint16_t * p_buff = i2s_dummy_buffer[0];
|
||||||
|
uint16_t dataVal = 1;
|
||||||
|
for (uint16_t cnt = 0; cnt < AUDIO_SAMPLE_RATE/1000; cnt++)
|
||||||
|
{
|
||||||
|
// CH0 saw wave
|
||||||
|
*p_buff++ = dataVal;
|
||||||
|
// CH1 inverted saw wave
|
||||||
|
*p_buff++ = 60 + AUDIO_SAMPLE_RATE/1000 - dataVal;
|
||||||
|
dataVal++;
|
||||||
|
}
|
||||||
|
p_buff = i2s_dummy_buffer[1];
|
||||||
|
for (uint16_t cnt = 0; cnt < AUDIO_SAMPLE_RATE/1000; cnt++)
|
||||||
|
{
|
||||||
|
// CH3 square wave
|
||||||
|
*p_buff++ = cnt < (AUDIO_SAMPLE_RATE/1000/2) ? 120:170;
|
||||||
|
// CH4 sinus wave
|
||||||
|
float t = 2*3.1415f * cnt / (AUDIO_SAMPLE_RATE/1000);
|
||||||
|
*p_buff++ = (uint16_t)(sinf(t) * 25) + 200;
|
||||||
|
}
|
||||||
|
|
||||||
while (1)
|
while (1)
|
||||||
{
|
{
|
||||||
tud_task(); // tinyusb device task
|
tud_task(); // tinyusb device task
|
||||||
@ -400,7 +420,17 @@ bool tud_audio_tx_done_pre_load_cb(uint8_t rhport, uint8_t itf, uint8_t ep_in, u
|
|||||||
(void) ep_in;
|
(void) ep_in;
|
||||||
(void) cur_alt_setting;
|
(void) cur_alt_setting;
|
||||||
|
|
||||||
for (uint8_t cnt=0; cnt < CFG_TUD_AUDIO_FUNC_1_N_TX_SUPP_SW_FIFO; cnt++)
|
|
||||||
|
// In read world application data flow is driven by I2S clock,
|
||||||
|
// both tud_audio_tx_done_pre_load_cb() & tud_audio_tx_done_post_load_cb() are hardly used.
|
||||||
|
// For example in your I2S receive callback:
|
||||||
|
// void I2S_Rx_Callback(int channel, const void* data, uint16_t samples)
|
||||||
|
// {
|
||||||
|
// tud_audio_write_support_ff(channel, data, samples * N_BYTES_PER_SAMPLE * N_CHANNEL_PER_FIFO);
|
||||||
|
// }
|
||||||
|
|
||||||
|
// Write I2S buffer into FIFO
|
||||||
|
for (uint8_t cnt=0; cnt < 2; cnt++)
|
||||||
{
|
{
|
||||||
tud_audio_write_support_ff(cnt, i2s_dummy_buffer[cnt], AUDIO_SAMPLE_RATE/1000 * CFG_TUD_AUDIO_FUNC_1_N_BYTES_PER_SAMPLE_TX * CFG_TUD_AUDIO_FUNC_1_CHANNEL_PER_FIFO_TX);
|
tud_audio_write_support_ff(cnt, i2s_dummy_buffer[cnt], AUDIO_SAMPLE_RATE/1000 * CFG_TUD_AUDIO_FUNC_1_N_BYTES_PER_SAMPLE_TX * CFG_TUD_AUDIO_FUNC_1_CHANNEL_PER_FIFO_TX);
|
||||||
}
|
}
|
||||||
@ -416,22 +446,6 @@ bool tud_audio_tx_done_post_load_cb(uint8_t rhport, uint16_t n_bytes_copied, uin
|
|||||||
(void) ep_in;
|
(void) ep_in;
|
||||||
(void) cur_alt_setting;
|
(void) cur_alt_setting;
|
||||||
|
|
||||||
uint16_t dataVal;
|
|
||||||
|
|
||||||
// Generate dummy data
|
|
||||||
for (uint16_t cnt = 0; cnt < CFG_TUD_AUDIO_FUNC_1_N_TX_SUPP_SW_FIFO; cnt++)
|
|
||||||
{
|
|
||||||
uint16_t * p_buff = i2s_dummy_buffer[cnt]; // 2 bytes per sample
|
|
||||||
dataVal = 1;
|
|
||||||
for (uint16_t cnt2 = 0; cnt2 < AUDIO_SAMPLE_RATE/1000; cnt2++)
|
|
||||||
{
|
|
||||||
for (uint8_t cnt3 = 0; cnt3 < CFG_TUD_AUDIO_FUNC_1_CHANNEL_PER_FIFO_TX; cnt3++)
|
|
||||||
{
|
|
||||||
*p_buff++ = dataVal;
|
|
||||||
}
|
|
||||||
dataVal++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -10,7 +10,7 @@ if __name__ == '__main__':
|
|||||||
# print(sd.query_devices())
|
# print(sd.query_devices())
|
||||||
|
|
||||||
fs = 48000 # Sample rate
|
fs = 48000 # Sample rate
|
||||||
duration = 100e-3 # Duration of recording
|
duration = 20e-3 # Duration of recording
|
||||||
|
|
||||||
if platform.system() == 'Windows':
|
if platform.system() == 'Windows':
|
||||||
# WDM-KS is needed since there are more than one MicNode device APIs (at least in Windows)
|
# WDM-KS is needed since there are more than one MicNode device APIs (at least in Windows)
|
||||||
@ -25,9 +25,14 @@ if __name__ == '__main__':
|
|||||||
sd.wait() # Wait until recording is finished
|
sd.wait() # Wait until recording is finished
|
||||||
print('Done!')
|
print('Done!')
|
||||||
|
|
||||||
|
|
||||||
time = np.arange(0, duration, 1 / fs) # time vector
|
time = np.arange(0, duration, 1 / fs) # time vector
|
||||||
|
# strip starting zero
|
||||||
|
myrecording = myrecording[100:]
|
||||||
|
time = time[100:]
|
||||||
plt.plot(time, myrecording)
|
plt.plot(time, myrecording)
|
||||||
plt.xlabel('Time [s]')
|
plt.xlabel('Time [s]')
|
||||||
plt.ylabel('Amplitude')
|
plt.ylabel('Amplitude')
|
||||||
plt.title('MicNode 4 Channel')
|
plt.title('MicNode 4 Channel')
|
||||||
|
plt.legend(['CH-1', 'CH-2', 'CH-3','CH-4'])
|
||||||
plt.show()
|
plt.show()
|
||||||
|
@ -103,6 +103,7 @@ extern "C" {
|
|||||||
//--------------------------------------------------------------------
|
//--------------------------------------------------------------------
|
||||||
|
|
||||||
// Have a look into audio_device.h for all configurations
|
// Have a look into audio_device.h for all configurations
|
||||||
|
#define CFG_TUD_AUDIO_FUNC_1_SAMPLE_RATE 48000
|
||||||
|
|
||||||
#define CFG_TUD_AUDIO_FUNC_1_DESC_LEN TUD_AUDIO_MIC_FOUR_CH_DESC_LEN
|
#define CFG_TUD_AUDIO_FUNC_1_DESC_LEN TUD_AUDIO_MIC_FOUR_CH_DESC_LEN
|
||||||
|
|
||||||
@ -112,7 +113,7 @@ extern "C" {
|
|||||||
#define CFG_TUD_AUDIO_ENABLE_EP_IN 1
|
#define CFG_TUD_AUDIO_ENABLE_EP_IN 1
|
||||||
#define CFG_TUD_AUDIO_FUNC_1_N_BYTES_PER_SAMPLE_TX 2 // This value is not required by the driver, it parses this information from the descriptor once the alternate interface is set by the host - we use it for the setup
|
#define CFG_TUD_AUDIO_FUNC_1_N_BYTES_PER_SAMPLE_TX 2 // This value is not required by the driver, it parses this information from the descriptor once the alternate interface is set by the host - we use it for the setup
|
||||||
#define CFG_TUD_AUDIO_FUNC_1_N_CHANNELS_TX 4 // This value is not required by the driver, it parses this information from the descriptor once the alternate interface is set by the host - we use it for the setup
|
#define CFG_TUD_AUDIO_FUNC_1_N_CHANNELS_TX 4 // This value is not required by the driver, it parses this information from the descriptor once the alternate interface is set by the host - we use it for the setup
|
||||||
#define CFG_TUD_AUDIO_EP_SZ_IN (48 + 1) * CFG_TUD_AUDIO_FUNC_1_N_BYTES_PER_SAMPLE_TX * CFG_TUD_AUDIO_FUNC_1_N_CHANNELS_TX // 48 Samples (48 kHz) x 2 Bytes/Sample x CFG_TUD_AUDIO_N_CHANNELS_TX Channels - the Windows driver always needs an extra sample per channel of space more, otherwise it complains... found by trial and error
|
#define CFG_TUD_AUDIO_EP_SZ_IN TUD_AUDIO_EP_SIZE(CFG_TUD_AUDIO_FUNC_1_SAMPLE_RATE, CFG_TUD_AUDIO_FUNC_1_N_BYTES_PER_SAMPLE_TX, CFG_TUD_AUDIO_FUNC_1_N_CHANNELS_TX)
|
||||||
#define CFG_TUD_AUDIO_FUNC_1_EP_IN_SZ_MAX CFG_TUD_AUDIO_EP_SZ_IN
|
#define CFG_TUD_AUDIO_FUNC_1_EP_IN_SZ_MAX CFG_TUD_AUDIO_EP_SZ_IN
|
||||||
#define CFG_TUD_AUDIO_FUNC_1_EP_IN_SW_BUF_SZ CFG_TUD_AUDIO_EP_SZ_IN
|
#define CFG_TUD_AUDIO_FUNC_1_EP_IN_SW_BUF_SZ CFG_TUD_AUDIO_EP_SZ_IN
|
||||||
|
|
||||||
|
@ -631,73 +631,55 @@ static bool audiod_rx_done_cb(uint8_t rhport, audiod_function_t* audio, uint16_t
|
|||||||
// Decoding according to 2.3.1.5 Audio Streams
|
// Decoding according to 2.3.1.5 Audio Streams
|
||||||
|
|
||||||
// Helper function
|
// Helper function
|
||||||
static inline uint8_t * audiod_interleaved_copy_bytes_fast_decode(uint16_t const nBytesToCopy, void * dst, uint8_t * dst_end, uint8_t * src, uint8_t const n_ff_used)
|
static inline void * audiod_interleaved_copy_bytes_fast_decode(uint16_t const nBytesPerSample, void * dst, const void * dst_end, void * src, uint8_t const n_ff_used)
|
||||||
{
|
{
|
||||||
|
// Due to one FIFO contains 2 channels, data always aligned to (nBytesPerSample * 2)
|
||||||
|
uint16_t * dst16 = dst;
|
||||||
|
uint16_t * src16 = src;
|
||||||
|
const uint16_t * dst_end16 = dst_end;
|
||||||
|
uint32_t * dst32 = dst;
|
||||||
|
uint32_t * src32 = src;
|
||||||
|
const uint32_t * dst_end32 = dst_end;
|
||||||
|
|
||||||
// This function is an optimized version of
|
if (nBytesPerSample == 1)
|
||||||
// while((uint8_t *)dst < dst_end)
|
|
||||||
// {
|
|
||||||
// memcpy(dst, src, nBytesToCopy);
|
|
||||||
// dst = (uint8_t *)dst + nBytesToCopy;
|
|
||||||
// src += nBytesToCopy * n_ff_used;
|
|
||||||
// }
|
|
||||||
|
|
||||||
// Optimize for fast half word copies
|
|
||||||
typedef struct{
|
|
||||||
uint16_t val;
|
|
||||||
} __attribute((__packed__)) unaligned_uint16_t;
|
|
||||||
|
|
||||||
// Optimize for fast word copies
|
|
||||||
typedef struct{
|
|
||||||
uint32_t val;
|
|
||||||
} __attribute((__packed__)) unaligned_uint32_t;
|
|
||||||
|
|
||||||
switch (nBytesToCopy)
|
|
||||||
{
|
{
|
||||||
case 1:
|
while(dst16 < dst_end16)
|
||||||
while((uint8_t *)dst < dst_end)
|
{
|
||||||
{
|
*dst16++ = *src16++;
|
||||||
*(uint8_t *)dst++ = *src;
|
src16 += n_ff_used - 1;
|
||||||
src += n_ff_used;
|
}
|
||||||
}
|
return src16;
|
||||||
break;
|
}
|
||||||
|
else if (nBytesPerSample == 2)
|
||||||
case 2:
|
{
|
||||||
while((uint8_t *)dst < dst_end)
|
while(dst32 < dst_end32)
|
||||||
{
|
{
|
||||||
*(unaligned_uint16_t*)dst = *(unaligned_uint16_t*)src;
|
*dst32++ = *src32++;
|
||||||
dst += 2;
|
src32 += n_ff_used - 1;
|
||||||
src += 2 * n_ff_used;
|
}
|
||||||
}
|
return src32;
|
||||||
break;
|
}
|
||||||
|
else if (nBytesPerSample == 3)
|
||||||
case 3:
|
{
|
||||||
while((uint8_t *)dst < dst_end)
|
while(dst16 < dst_end16)
|
||||||
{
|
{
|
||||||
// memcpy(dst, src, 3);
|
*dst16++ = *src16++;
|
||||||
// dst = (uint8_t *)dst + 3;
|
*dst16++ = *src16++;
|
||||||
// src += 3 * n_ff_used;
|
*dst16++ = *src16++;
|
||||||
|
src16 += 3 * (n_ff_used - 1);
|
||||||
// TODO: Is there a faster way to copy 3 bytes?
|
}
|
||||||
*(uint8_t *)dst++ = *src++;
|
return src16;
|
||||||
*(uint8_t *)dst++ = *src++;
|
}
|
||||||
*(uint8_t *)dst++ = *src++;
|
else // nBytesPerSample == 4
|
||||||
|
{
|
||||||
src += 3 * (n_ff_used - 1);
|
while(dst32 < dst_end32)
|
||||||
}
|
{
|
||||||
break;
|
*dst32++ = *src32++;
|
||||||
|
*dst32++ = *src32++;
|
||||||
case 4:
|
src32 += 2 * (n_ff_used - 1);
|
||||||
while((uint8_t *)dst < dst_end)
|
}
|
||||||
{
|
return src32;
|
||||||
*(unaligned_uint32_t*)dst = *(unaligned_uint32_t*)src;
|
|
||||||
dst += 4;
|
|
||||||
src += 4 * n_ff_used;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return src;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static bool audiod_decode_type_I_pcm(uint8_t rhport, audiod_function_t* audio, uint16_t n_bytes_received)
|
static bool audiod_decode_type_I_pcm(uint8_t rhport, audiod_function_t* audio, uint16_t n_bytes_received)
|
||||||
@ -944,64 +926,55 @@ range [-1, +1)
|
|||||||
* */
|
* */
|
||||||
|
|
||||||
// Helper function
|
// Helper function
|
||||||
static inline uint8_t * audiod_interleaved_copy_bytes_fast_encode(uint16_t const nBytesToCopy, uint8_t * src, uint8_t * src_end, uint8_t * dst, uint8_t const n_ff_used)
|
static inline void * audiod_interleaved_copy_bytes_fast_encode(uint16_t const nBytesPerSample, void * src, const void * src_end, void * dst, uint8_t const n_ff_used)
|
||||||
{
|
{
|
||||||
// Optimize for fast half word copies
|
// Due to one FIFO contains 2 channels, data always aligned to (nBytesPerSample * 2)
|
||||||
typedef struct{
|
uint16_t * dst16 = dst;
|
||||||
uint16_t val;
|
uint16_t * src16 = src;
|
||||||
} __attribute((__packed__)) unaligned_uint16_t;
|
const uint16_t * src_end16 = src_end;
|
||||||
|
uint32_t * dst32 = dst;
|
||||||
|
uint32_t * src32 = src;
|
||||||
|
const uint32_t * src_end32 = src_end;
|
||||||
|
|
||||||
// Optimize for fast word copies
|
if (nBytesPerSample == 1)
|
||||||
typedef struct{
|
|
||||||
uint32_t val;
|
|
||||||
} __attribute((__packed__)) unaligned_uint32_t;
|
|
||||||
|
|
||||||
switch (nBytesToCopy)
|
|
||||||
{
|
{
|
||||||
case 1:
|
while(src16 < src_end16)
|
||||||
while(src < src_end)
|
{
|
||||||
{
|
*dst16++ = *src16++;
|
||||||
*dst = *src++;
|
dst16 += n_ff_used - 1;
|
||||||
dst += n_ff_used;
|
}
|
||||||
}
|
return dst16;
|
||||||
break;
|
}
|
||||||
|
else if (nBytesPerSample == 2)
|
||||||
case 2:
|
{
|
||||||
while(src < src_end)
|
while(src32 < src_end32)
|
||||||
{
|
{
|
||||||
*(unaligned_uint16_t*)dst = *(unaligned_uint16_t*)src;
|
*dst32++ = *src32++;
|
||||||
src += 2;
|
dst32 += n_ff_used - 1;
|
||||||
dst += 2 * n_ff_used;
|
}
|
||||||
}
|
return dst32;
|
||||||
break;
|
}
|
||||||
|
else if (nBytesPerSample == 3)
|
||||||
case 3:
|
{
|
||||||
while(src < src_end)
|
while(src16 < src_end16)
|
||||||
{
|
{
|
||||||
// memcpy(dst, src, 3);
|
*dst16++ = *src16++;
|
||||||
// src = (uint8_t *)src + 3;
|
*dst16++ = *src16++;
|
||||||
// dst += 3 * n_ff_used;
|
*dst16++ = *src16++;
|
||||||
|
dst16 += 3 * (n_ff_used - 1);
|
||||||
// TODO: Is there a faster way to copy 3 bytes?
|
}
|
||||||
*dst++ = *src++;
|
return dst16;
|
||||||
*dst++ = *src++;
|
}
|
||||||
*dst++ = *src++;
|
else // nBytesPerSample == 4
|
||||||
|
{
|
||||||
dst += 3 * (n_ff_used - 1);
|
while(src32 < src_end32)
|
||||||
}
|
{
|
||||||
break;
|
*dst32++ = *src32++;
|
||||||
|
*dst32++ = *src32++;
|
||||||
case 4:
|
dst32 += 2 * (n_ff_used - 1);
|
||||||
while(src < src_end)
|
}
|
||||||
{
|
return dst32;
|
||||||
*(unaligned_uint32_t*)dst = *(unaligned_uint32_t*)src;
|
|
||||||
src += 4;
|
|
||||||
dst += 4 * n_ff_used;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return dst;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static uint16_t audiod_encode_type_I_pcm(uint8_t rhport, audiod_function_t* audio)
|
static uint16_t audiod_encode_type_I_pcm(uint8_t rhport, audiod_function_t* audio)
|
||||||
|
Loading…
x
Reference in New Issue
Block a user