mirror of
https://github.com/AlexandreRouma/SDRPlusPlus.git
synced 2024-11-10 04:37:37 +01:00
final fixes
This commit is contained in:
parent
5c690c9753
commit
8771e4bf09
@ -7,6 +7,7 @@
|
||||
#include <dsp/sink/handler_sink.h>
|
||||
#include <dsp/routing/splitter.h>
|
||||
#include <dsp/audio/volume.h>
|
||||
#include <dsp/convert/stereo_to_mono.h>
|
||||
#include <thread>
|
||||
#include <ctime>
|
||||
#include <gui/gui.h>
|
||||
@ -41,15 +42,15 @@ public:
|
||||
root = (std::string)core::args["root"];
|
||||
|
||||
// Define option lists
|
||||
formats.define("WAV", wav::FORMAT_WAV);
|
||||
// formats.define("RF64", wav::FORMAT_RF64); // Disabled for now
|
||||
containers.define("WAV", wav::FORMAT_WAV);
|
||||
// containers.define("RF64", wav::FORMAT_RF64); // Disabled for now
|
||||
sampleTypes.define(wav::SAMP_TYPE_UINT8, "Uint8", wav::SAMP_TYPE_UINT8);
|
||||
sampleTypes.define(wav::SAMP_TYPE_INT16, "Int16", wav::SAMP_TYPE_INT16);
|
||||
sampleTypes.define(wav::SAMP_TYPE_INT32, "Int32", wav::SAMP_TYPE_INT32);
|
||||
sampleTypes.define(wav::SAMP_TYPE_FLOAT32, "Float32", wav::SAMP_TYPE_FLOAT32);
|
||||
|
||||
// Load default config for option lists
|
||||
formatId = formats.valueId(wav::FORMAT_WAV);
|
||||
containerId = containers.valueId(wav::FORMAT_WAV);
|
||||
sampleTypeId = sampleTypes.valueId(wav::SAMP_TYPE_INT16);
|
||||
|
||||
// Load config
|
||||
@ -60,8 +61,8 @@ public:
|
||||
if (config.conf[name].contains("recPath")) {
|
||||
folderSelect.setPath(config.conf[name]["recPath"]);
|
||||
}
|
||||
if (config.conf[name].contains("format") && formats.keyExists(config.conf[name]["format"])) {
|
||||
formatId = formats.keyId(config.conf[name]["format"]);
|
||||
if (config.conf[name].contains("format") && containers.keyExists(config.conf[name]["format"])) {
|
||||
containerId = containers.keyId(config.conf[name]["format"]);
|
||||
}
|
||||
if (config.conf[name].contains("sampleType") && sampleTypes.keyExists(config.conf[name]["sampleType"])) {
|
||||
sampleTypeId = sampleTypes.keyId(config.conf[name]["sampleType"]);
|
||||
@ -82,11 +83,12 @@ public:
|
||||
splitter.init(&volume.out);
|
||||
splitter.bindStream(&meterStream);
|
||||
meter.init(&meterStream);
|
||||
s2m.init(NULL);
|
||||
|
||||
// Init sinks
|
||||
basebandSink.init(NULL, complexHandler, this);
|
||||
stereoSink.init(NULL, stereoHandler, this);
|
||||
monoSink.init(NULL, monoHandler, this);
|
||||
monoSink.init(&s2m.out, monoHandler, this);
|
||||
|
||||
gui::menu.registerEntry(name, menuHandler, this);
|
||||
}
|
||||
@ -142,7 +144,7 @@ public:
|
||||
else {
|
||||
samplerate = sigpath::iqFrontEnd.getSampleRate();
|
||||
}
|
||||
writer.setFormat(formats[formatId]);
|
||||
writer.setFormat(containers[containerId]);
|
||||
writer.setChannels((recMode == RECORDER_MODE_AUDIO && !stereo) ? 1 : 2);
|
||||
writer.setSampleType(sampleTypes[sampleTypeId]);
|
||||
writer.setSamplerate(samplerate);
|
||||
@ -159,8 +161,15 @@ public:
|
||||
if (recMode == RECORDER_MODE_AUDIO) {
|
||||
// TODO: Select the stereo to mono converter if needed
|
||||
stereoStream = sigpath::sinkManager.bindStream(selectedStreamName);
|
||||
stereoSink.setInput(stereoStream);
|
||||
stereoSink.start();
|
||||
if (stereo) {
|
||||
stereoSink.setInput(stereoStream);
|
||||
stereoSink.start();
|
||||
}
|
||||
else {
|
||||
s2m.setInput(stereoStream);
|
||||
s2m.start();
|
||||
monoSink.start();
|
||||
}
|
||||
}
|
||||
else {
|
||||
// Create and bind IQ stream
|
||||
@ -179,8 +188,10 @@ public:
|
||||
|
||||
// Close audio stream or baseband
|
||||
if (recMode == RECORDER_MODE_AUDIO) {
|
||||
// TODO: HAS TO BE DONE PROPERLY
|
||||
// NOTE: Has to be done before the unbind since the stream is deleted...
|
||||
monoSink.stop();
|
||||
stereoSink.stop();
|
||||
s2m.stop();
|
||||
sigpath::sinkManager.unbindStream(selectedStreamName, stereoStream);
|
||||
}
|
||||
else {
|
||||
@ -231,11 +242,11 @@ private:
|
||||
}
|
||||
}
|
||||
|
||||
ImGui::LeftLabel("WAV Format");
|
||||
ImGui::LeftLabel("Container");
|
||||
ImGui::FillWidth();
|
||||
if (ImGui::Combo(CONCAT("##_recorder_wav_fmt_", _this->name), &_this->formatId, _this->formats.txt)) {
|
||||
if (ImGui::Combo(CONCAT("##_recorder_container_", _this->name), &_this->containerId, _this->containers.txt)) {
|
||||
config.acquire();
|
||||
config.conf[_this->name]["format"] = _this->formats.key(_this->formatId);
|
||||
config.conf[_this->name]["container"] = _this->containers.key(_this->containerId);
|
||||
config.release(true);
|
||||
}
|
||||
|
||||
@ -431,12 +442,12 @@ private:
|
||||
bool enabled = true;
|
||||
std::string root;
|
||||
|
||||
OptionList<std::string, wav::Format> formats;
|
||||
OptionList<std::string, wav::Format> containers;
|
||||
OptionList<int, wav::SampleType> sampleTypes;
|
||||
FolderSelect folderSelect;
|
||||
|
||||
int recMode = RECORDER_MODE_AUDIO;
|
||||
int formatId;
|
||||
int containerId;
|
||||
int sampleTypeId;
|
||||
bool stereo = true;
|
||||
std::string selectedStreamName = "";
|
||||
@ -460,6 +471,7 @@ private:
|
||||
dsp::routing::Splitter<dsp::stereo_t> splitter;
|
||||
dsp::stream<dsp::stereo_t> meterStream;
|
||||
dsp::bench::PeakLevelMeter<dsp::stereo_t> meter;
|
||||
dsp::convert::StereoToMono s2m;
|
||||
|
||||
uint64_t samplerate = 48000;
|
||||
|
||||
|
@ -2,28 +2,64 @@
|
||||
#include <stdexcept>
|
||||
|
||||
namespace riff {
|
||||
bool Writer::open(std::string path, char form[4]) {
|
||||
// TODO: Open file
|
||||
const char* RIFF_SIGNATURE = "RIFF";
|
||||
const char* LIST_SIGNATURE = "LIST";
|
||||
const size_t RIFF_LABEL_SIZE = 4;
|
||||
|
||||
bool Writer::open(std::string path, const char form[4]) {
|
||||
std::lock_guard<std::recursive_mutex> lck(mtx);
|
||||
|
||||
// Open file
|
||||
file = std::ofstream(path, std::ios::out | std::ios::binary);
|
||||
if (!file.is_open()) { return false; }
|
||||
|
||||
// Begin RIFF chunk
|
||||
beginRIFF(form);
|
||||
|
||||
return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool Writer::isOpen() {
|
||||
|
||||
return false;
|
||||
std::lock_guard<std::recursive_mutex> lck(mtx);
|
||||
return file.is_open();
|
||||
}
|
||||
|
||||
void Writer::close() {
|
||||
std::lock_guard<std::recursive_mutex> lck(mtx);
|
||||
|
||||
if (!isOpen()) { return; }
|
||||
|
||||
// Finalize RIFF chunk
|
||||
endRIFF();
|
||||
|
||||
// Close file
|
||||
file.close();
|
||||
}
|
||||
|
||||
void Writer::beginChunk(char id[4]) {
|
||||
void Writer::beginList(const char id[4]) {
|
||||
std::lock_guard<std::recursive_mutex> lck(mtx);
|
||||
|
||||
// Create chunk with the LIST ID and write id
|
||||
beginChunk(LIST_SIGNATURE);
|
||||
write((uint8_t*)id, RIFF_LABEL_SIZE);
|
||||
}
|
||||
|
||||
void Writer::endList() {
|
||||
std::lock_guard<std::recursive_mutex> lck(mtx);
|
||||
|
||||
if (chunks.empty()) {
|
||||
throw std::runtime_error("No chunk to end");
|
||||
}
|
||||
if (memcmp(chunks.top().hdr.id, LIST_SIGNATURE, RIFF_LABEL_SIZE)) {
|
||||
throw std::runtime_error("Top chunk not LIST chunk");
|
||||
}
|
||||
|
||||
endChunk();
|
||||
}
|
||||
|
||||
void Writer::beginChunk(const char id[4]) {
|
||||
std::lock_guard<std::recursive_mutex> lck(mtx);
|
||||
|
||||
// Create and write header
|
||||
ChunkDesc desc;
|
||||
desc.pos = file.tellp();
|
||||
@ -36,7 +72,9 @@ namespace riff {
|
||||
}
|
||||
|
||||
void Writer::endChunk() {
|
||||
if (!chunks.empty()) {
|
||||
std::lock_guard<std::recursive_mutex> lck(mtx);
|
||||
|
||||
if (chunks.empty()) {
|
||||
throw std::runtime_error("No chunk to end");
|
||||
}
|
||||
|
||||
@ -46,7 +84,9 @@ namespace riff {
|
||||
|
||||
// Write size
|
||||
auto pos = file.tellp();
|
||||
file.seekp(desc.pos + 4);
|
||||
auto npos = desc.pos;
|
||||
npos += 4;
|
||||
file.seekp(npos);
|
||||
file.write((char*)&desc.hdr.size, sizeof(desc.hdr.size));
|
||||
file.seekp(pos);
|
||||
|
||||
@ -56,31 +96,38 @@ namespace riff {
|
||||
}
|
||||
}
|
||||
|
||||
void Writer::write(void* data, size_t len) {
|
||||
if (!chunks.empty()) {
|
||||
void Writer::write(const uint8_t* data, size_t len) {
|
||||
std::lock_guard<std::recursive_mutex> lck(mtx);
|
||||
|
||||
if (chunks.empty()) {
|
||||
throw std::runtime_error("No chunk to write into");
|
||||
}
|
||||
file.write((char*)data, len);
|
||||
chunks.top().hdr.size += len;
|
||||
}
|
||||
|
||||
void Writer::beginRIFF(char form[4]) {
|
||||
void Writer::beginRIFF(const char form[4]) {
|
||||
std::lock_guard<std::recursive_mutex> lck(mtx);
|
||||
|
||||
if (!chunks.empty()) {
|
||||
throw std::runtime_error("Can't create RIFF chunk on an existing RIFF file");
|
||||
}
|
||||
|
||||
// Create chunk with RIFF ID and write form
|
||||
beginChunk("RIFF");
|
||||
write(form, sizeof(form));
|
||||
beginChunk(RIFF_SIGNATURE);
|
||||
write((uint8_t*)form, RIFF_LABEL_SIZE);
|
||||
}
|
||||
|
||||
void Writer::endRIFF() {
|
||||
if (!chunks.empty()) {
|
||||
std::lock_guard<std::recursive_mutex> lck(mtx);
|
||||
|
||||
if (chunks.empty()) {
|
||||
throw std::runtime_error("No chunk to end");
|
||||
}
|
||||
if (memcmp(chunks.top().hdr.id, "RIFF", 4)) {
|
||||
if (memcmp(chunks.top().hdr.id, RIFF_SIGNATURE, RIFF_LABEL_SIZE)) {
|
||||
throw std::runtime_error("Top chunk not RIFF chunk");
|
||||
}
|
||||
|
||||
endChunk();
|
||||
}
|
||||
}
|
@ -1,4 +1,5 @@
|
||||
#pragma once
|
||||
#include <mutex>
|
||||
#include <fstream>
|
||||
#include <string>
|
||||
#include <stack>
|
||||
@ -19,22 +20,23 @@ namespace riff {
|
||||
|
||||
class Writer {
|
||||
public:
|
||||
bool open(std::string path, char form[4]);
|
||||
bool open(std::string path, const char form[4]);
|
||||
bool isOpen();
|
||||
void close();
|
||||
|
||||
void beginList();
|
||||
void beginList(const char id[4]);
|
||||
void endList();
|
||||
|
||||
void beginChunk(char id[4]);
|
||||
void beginChunk(const char id[4]);
|
||||
void endChunk();
|
||||
|
||||
void write(void* data, size_t len);
|
||||
void write(const uint8_t* data, size_t len);
|
||||
|
||||
private:
|
||||
void beginRIFF(char form[4]);
|
||||
void beginRIFF(const char form[4]);
|
||||
void endRIFF();
|
||||
|
||||
std::recursive_mutex mtx;
|
||||
std::ofstream file;
|
||||
std::stack<ChunkDesc> chunks;
|
||||
};
|
||||
|
@ -6,7 +6,6 @@
|
||||
#include <map>
|
||||
|
||||
namespace wav {
|
||||
const char* RIFF_SIGNATURE = "RIFF";
|
||||
const char* WAVE_FILE_TYPE = "WAVE";
|
||||
const char* FORMAT_MARKER = "fmt ";
|
||||
const char* DATA_MARKER = "data";
|
||||
@ -30,13 +29,6 @@ namespace wav {
|
||||
_samplerate = samplerate;
|
||||
_format = format;
|
||||
_type = type;
|
||||
|
||||
// Initialize header with constants
|
||||
memcpy(hdr.signature, RIFF_SIGNATURE, 4);
|
||||
memcpy(hdr.fileType, WAVE_FILE_TYPE, 4);
|
||||
memcpy(hdr.formatMarker, FORMAT_MARKER, 4);
|
||||
hdr.formatHeaderLength = FORMAT_HEADER_LEN;
|
||||
memcpy(hdr.dataMarker, DATA_MARKER, 4);
|
||||
}
|
||||
|
||||
Writer::~Writer() { close(); }
|
||||
@ -44,14 +36,21 @@ namespace wav {
|
||||
bool Writer::open(std::string path) {
|
||||
std::lock_guard<std::recursive_mutex> lck(mtx);
|
||||
// Close previous file
|
||||
if (_isOpen) { close(); }
|
||||
if (rw.isOpen()) { close(); }
|
||||
|
||||
// Reset work values
|
||||
samplesWritten = 0;
|
||||
|
||||
// Fill header
|
||||
bytesPerSamp = (SAMP_BITS[_type] / 8) * _channels;
|
||||
hdr.codec = (_type == SAMP_TYPE_FLOAT32) ? CODEC_FLOAT : CODEC_PCM;
|
||||
hdr.channelCount = _channels;
|
||||
hdr.sampleRate = _samplerate;
|
||||
hdr.bitDepth = SAMP_BITS[_type];
|
||||
hdr.bytesPerSample = bytesPerSamp;
|
||||
hdr.bytesPerSecond = bytesPerSamp * _samplerate;
|
||||
|
||||
// Precompute sizes and allocate buffers
|
||||
// TODO: Get number of bits for each sample type
|
||||
bytesPerSamp = (SAMP_BITS[_type] / 8) * _channels; // THIS IS WRONG
|
||||
switch (_type) {
|
||||
case SAMP_TYPE_UINT8:
|
||||
bufU8 = dsp::buffer::alloc<uint8_t>(STREAM_BUFFER_SIZE * _channels);
|
||||
@ -69,36 +68,37 @@ namespace wav {
|
||||
break;
|
||||
}
|
||||
|
||||
// Open new file
|
||||
file.open(path, std::ios::out | std::ios::binary);
|
||||
if (!file.is_open()) { return false; }
|
||||
// Open file
|
||||
if (!rw.open(path, WAVE_FILE_TYPE)) { return false; }
|
||||
|
||||
// Skip header, it'll be written when finalizing the file
|
||||
uint8_t dummy[sizeof(Header)];
|
||||
memset(dummy, 0, sizeof(dummy));
|
||||
file.write((char*)dummy, sizeof(dummy));
|
||||
// Write format chunk
|
||||
rw.beginChunk(FORMAT_MARKER);
|
||||
rw.write((uint8_t*)&hdr, sizeof(FormatHeader));
|
||||
rw.endChunk();
|
||||
|
||||
// Begin data chunk
|
||||
rw.beginChunk(DATA_MARKER);
|
||||
|
||||
_isOpen = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool Writer::isOpen() {
|
||||
std::lock_guard<std::recursive_mutex> lck(mtx);
|
||||
return _isOpen;
|
||||
return rw.isOpen();
|
||||
}
|
||||
|
||||
void Writer::close() {
|
||||
std::lock_guard<std::recursive_mutex> lck(mtx);
|
||||
// Do nothing if the file is not open
|
||||
if (!_isOpen) { return; }
|
||||
if (!rw.isOpen()) { return; }
|
||||
|
||||
// Finilize wav
|
||||
finalize();
|
||||
// Finish data chunk
|
||||
rw.endChunk();
|
||||
|
||||
// Close the file
|
||||
file.close();
|
||||
rw.close();
|
||||
|
||||
// Destroy buffers
|
||||
// Free buffers
|
||||
if (bufU8) {
|
||||
dsp::buffer::free(bufU8);
|
||||
bufU8 = NULL;
|
||||
@ -111,15 +111,12 @@ namespace wav {
|
||||
dsp::buffer::free(bufI32);
|
||||
bufI32 = NULL;
|
||||
}
|
||||
|
||||
// Mark as closed
|
||||
_isOpen = false;
|
||||
}
|
||||
|
||||
void Writer::setChannels(int channels) {
|
||||
std::lock_guard<std::recursive_mutex> lck(mtx);
|
||||
// Do not allow settings to change while open
|
||||
if (_isOpen) { throw std::runtime_error("Cannot change parameters while file is open"); }
|
||||
if (rw.isOpen()) { throw std::runtime_error("Cannot change parameters while file is open"); }
|
||||
|
||||
// Validate channel count
|
||||
if (channels < 1) { throw std::runtime_error("Channel count must be greater or equal to 1"); }
|
||||
@ -129,7 +126,7 @@ namespace wav {
|
||||
void Writer::setSamplerate(uint64_t samplerate) {
|
||||
std::lock_guard<std::recursive_mutex> lck(mtx);
|
||||
// Do not allow settings to change while open
|
||||
if (_isOpen) { throw std::runtime_error("Cannot change parameters while file is open"); }
|
||||
if (rw.isOpen()) { throw std::runtime_error("Cannot change parameters while file is open"); }
|
||||
|
||||
// Validate samplerate
|
||||
if (!samplerate) { throw std::runtime_error("Samplerate must be non-zero"); }
|
||||
@ -138,42 +135,42 @@ namespace wav {
|
||||
void Writer::setFormat(Format format) {
|
||||
std::lock_guard<std::recursive_mutex> lck(mtx);
|
||||
// Do not allow settings to change while open
|
||||
if (_isOpen) { throw std::runtime_error("Cannot change parameters while file is open"); }
|
||||
if (rw.isOpen()) { throw std::runtime_error("Cannot change parameters while file is open"); }
|
||||
_format = format;
|
||||
}
|
||||
|
||||
void Writer::setSampleType(SampleType type) {
|
||||
std::lock_guard<std::recursive_mutex> lck(mtx);
|
||||
// Do not allow settings to change while open
|
||||
if (_isOpen) { throw std::runtime_error("Cannot change parameters while file is open"); }
|
||||
if (rw.isOpen()) { throw std::runtime_error("Cannot change parameters while file is open"); }
|
||||
_type = type;
|
||||
}
|
||||
|
||||
void Writer::write(float* samples, int count) {
|
||||
std::lock_guard<std::recursive_mutex> lck(mtx);
|
||||
if (!_isOpen) { return; }
|
||||
if (!rw.isOpen()) { return; }
|
||||
|
||||
// Select different writer function depending on the chose depth
|
||||
int tcount;
|
||||
int tcount = count * _channels;
|
||||
int tbytes = count * bytesPerSamp;
|
||||
switch (_type) {
|
||||
case SAMP_TYPE_UINT8:
|
||||
tcount = count * _channels;
|
||||
// Volk doesn't support unsigned ints :/
|
||||
// Volk doesn't support unsigned ints yet :/
|
||||
for (int i = 0; i < tcount; i++) {
|
||||
bufU8[i] = (samples[i] * 127.0f) + 128.0f;
|
||||
}
|
||||
file.write((char*)bufU8, count * bytesPerSamp);
|
||||
rw.write(bufU8, tbytes);
|
||||
break;
|
||||
case SAMP_TYPE_INT16:
|
||||
volk_32f_s32f_convert_16i(bufI16, samples, 32767.0f, count * _channels);
|
||||
file.write((char*)bufI16, count * bytesPerSamp);
|
||||
volk_32f_s32f_convert_16i(bufI16, samples, 32767.0f, tcount);
|
||||
rw.write((uint8_t*)bufI16, tbytes);
|
||||
break;
|
||||
case SAMP_TYPE_INT32:
|
||||
volk_32f_s32f_convert_32i(bufI32, samples, 2147483647.0f, count * _channels);
|
||||
file.write((char*)bufI32, count * bytesPerSamp);
|
||||
volk_32f_s32f_convert_32i(bufI32, samples, 2147483647.0f, tcount);
|
||||
rw.write((uint8_t*)bufI32, tbytes);
|
||||
break;
|
||||
case SAMP_TYPE_FLOAT32:
|
||||
file.write((char*)samples, count * bytesPerSamp);
|
||||
rw.write((uint8_t*)samples, tbytes);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
@ -182,17 +179,4 @@ namespace wav {
|
||||
// Increment sample counter
|
||||
samplesWritten += count;
|
||||
}
|
||||
|
||||
void Writer::finalize() {
|
||||
file.seekp(file.beg);
|
||||
hdr.codec = (_type == SAMP_TYPE_FLOAT32) ? CODEC_FLOAT : CODEC_PCM;
|
||||
hdr.channelCount = _channels;
|
||||
hdr.sampleRate = _samplerate;
|
||||
hdr.bitDepth = SAMP_BITS[_type];
|
||||
hdr.bytesPerSample = bytesPerSamp;
|
||||
hdr.bytesPerSecond = bytesPerSamp * _samplerate;
|
||||
hdr.dataSize = samplesWritten * bytesPerSamp;
|
||||
hdr.fileSize = hdr.dataSize + sizeof(hdr) - 8;
|
||||
file.write((char*)&hdr, sizeof(hdr));
|
||||
}
|
||||
}
|
@ -3,23 +3,17 @@
|
||||
#include <fstream>
|
||||
#include <stdint.h>
|
||||
#include <mutex>
|
||||
#include "riff.h"
|
||||
|
||||
namespace wav {
|
||||
#pragma pack(push, 1)
|
||||
struct Header {
|
||||
char signature[4]; // "RIFF"
|
||||
uint32_t fileSize; // data bytes + sizeof(WavHeader_t) - 8
|
||||
char fileType[4]; // "WAVE"
|
||||
char formatMarker[4]; // "fmt "
|
||||
uint32_t formatHeaderLength; // Always 16
|
||||
uint16_t codec; // PCM (1)
|
||||
struct FormatHeader {
|
||||
uint16_t codec;
|
||||
uint16_t channelCount;
|
||||
uint32_t sampleRate;
|
||||
uint32_t bytesPerSecond;
|
||||
uint16_t bytesPerSample;
|
||||
uint16_t bitDepth;
|
||||
char dataMarker[4]; // "data"
|
||||
uint32_t dataSize;
|
||||
};
|
||||
#pragma pack(pop)
|
||||
|
||||
@ -59,12 +53,9 @@ namespace wav {
|
||||
void write(float* samples, int count);
|
||||
|
||||
private:
|
||||
void finalize();
|
||||
|
||||
std::recursive_mutex mtx;
|
||||
std::ofstream file;
|
||||
Header hdr;
|
||||
bool _isOpen = false;
|
||||
FormatHeader hdr;
|
||||
riff::Writer rw;
|
||||
|
||||
int _channels;
|
||||
uint64_t _samplerate;
|
||||
|
Loading…
Reference in New Issue
Block a user