Changed ramping functionality of the AudioMixer.
BUG=webrtc:6346 Review-Url: https://codereview.webrtc.org/2398083005 Cr-Commit-Position: refs/heads/master@{#14607}
This commit is contained in:
parent
3ec3da6fc2
commit
4b8bfb8ed3
@ -336,6 +336,7 @@ if (rtc_include_tests) {
|
||||
"audio_coding/neteq/tools/packet_unittest.cc",
|
||||
"audio_conference_mixer/test/audio_conference_mixer_unittest.cc",
|
||||
"audio_device/fine_audio_buffer_unittest.cc",
|
||||
"audio_mixer/audio_frame_manipulator_unittest.cc",
|
||||
"audio_mixer/test/audio_mixer_unittest.cc",
|
||||
"audio_processing/aec/echo_cancellation_unittest.cc",
|
||||
"audio_processing/aec/system_delay_unittest.cc",
|
||||
|
||||
@ -15,22 +15,6 @@
|
||||
#include "webrtc/typedefs.h"
|
||||
|
||||
namespace webrtc {
|
||||
namespace {
|
||||
// Linear ramping over 80 samples.
|
||||
// TODO(hellner): ramp using fix point?
|
||||
const float kRampArray[] = {
|
||||
0.0000f, 0.0127f, 0.0253f, 0.0380f, 0.0506f, 0.0633f, 0.0759f, 0.0886f,
|
||||
0.1013f, 0.1139f, 0.1266f, 0.1392f, 0.1519f, 0.1646f, 0.1772f, 0.1899f,
|
||||
0.2025f, 0.2152f, 0.2278f, 0.2405f, 0.2532f, 0.2658f, 0.2785f, 0.2911f,
|
||||
0.3038f, 0.3165f, 0.3291f, 0.3418f, 0.3544f, 0.3671f, 0.3797f, 0.3924f,
|
||||
0.4051f, 0.4177f, 0.4304f, 0.4430f, 0.4557f, 0.4684f, 0.4810f, 0.4937f,
|
||||
0.5063f, 0.5190f, 0.5316f, 0.5443f, 0.5570f, 0.5696f, 0.5823f, 0.5949f,
|
||||
0.6076f, 0.6203f, 0.6329f, 0.6456f, 0.6582f, 0.6709f, 0.6835f, 0.6962f,
|
||||
0.7089f, 0.7215f, 0.7342f, 0.7468f, 0.7595f, 0.7722f, 0.7848f, 0.7975f,
|
||||
0.8101f, 0.8228f, 0.8354f, 0.8481f, 0.8608f, 0.8734f, 0.8861f, 0.8987f,
|
||||
0.9114f, 0.9241f, 0.9367f, 0.9494f, 0.9620f, 0.9747f, 0.9873f, 1.0000f};
|
||||
const size_t kRampSize = sizeof(kRampArray) / sizeof(kRampArray[0]);
|
||||
} // namespace
|
||||
|
||||
uint32_t AudioMixerCalculateEnergy(const AudioFrame& audio_frame) {
|
||||
uint32_t energy = 0;
|
||||
@ -42,34 +26,32 @@ uint32_t AudioMixerCalculateEnergy(const AudioFrame& audio_frame) {
|
||||
return energy;
|
||||
}
|
||||
|
||||
void NewMixerRampIn(AudioFrame* audio_frame) {
|
||||
assert(kRampSize <= audio_frame->samples_per_channel_);
|
||||
for (size_t i = 0; i < kRampSize; i++) {
|
||||
audio_frame->data_[i] =
|
||||
static_cast<int16_t>(kRampArray[i] * audio_frame->data_[i]);
|
||||
}
|
||||
}
|
||||
void Ramp(float start_gain, float target_gain, AudioFrame* audio_frame) {
|
||||
RTC_DCHECK(audio_frame);
|
||||
RTC_DCHECK_GE(start_gain, 0.0f);
|
||||
RTC_DCHECK_GE(target_gain, 0.0f);
|
||||
|
||||
void NewMixerRampOut(AudioFrame* audio_frame) {
|
||||
assert(kRampSize <= audio_frame->samples_per_channel_);
|
||||
for (size_t i = 0; i < kRampSize; i++) {
|
||||
const size_t kRampPos = kRampSize - 1 - i;
|
||||
audio_frame->data_[i] =
|
||||
static_cast<int16_t>(kRampArray[kRampPos] * audio_frame->data_[i]);
|
||||
size_t samples = audio_frame->samples_per_channel_;
|
||||
RTC_DCHECK_LT(0u, samples);
|
||||
float increment = (target_gain - start_gain) / samples;
|
||||
float gain = start_gain;
|
||||
for (size_t i = 0; i < samples; ++i) {
|
||||
// If the audio is interleaved of several channels, we want to
|
||||
// apply the same gain change to the ith sample of every channel.
|
||||
for (size_t ch = 0; ch < audio_frame->num_channels_; ++ch) {
|
||||
audio_frame->data_[audio_frame->num_channels_ * i + ch] *= gain;
|
||||
}
|
||||
gain += increment;
|
||||
}
|
||||
memset(&audio_frame->data_[kRampSize], 0,
|
||||
(audio_frame->samples_per_channel_ - kRampSize) *
|
||||
sizeof(audio_frame->data_[0]));
|
||||
}
|
||||
|
||||
void RemixFrame(size_t target_number_of_channels, AudioFrame* frame) {
|
||||
RTC_DCHECK_GE(target_number_of_channels, static_cast<size_t>(1));
|
||||
RTC_DCHECK_LE(target_number_of_channels, static_cast<size_t>(2));
|
||||
RTC_DCHECK_GE(target_number_of_channels, 1u);
|
||||
RTC_DCHECK_LE(target_number_of_channels, 2u);
|
||||
if (frame->num_channels_ == 1 && target_number_of_channels == 2) {
|
||||
AudioFrameOperations::MonoToStereo(frame);
|
||||
} else if (frame->num_channels_ == 2 && target_number_of_channels == 1) {
|
||||
AudioFrameOperations::StereoToMono(frame);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace webrtc
|
||||
|
||||
@ -19,9 +19,9 @@ namespace webrtc {
|
||||
// Updates the audioFrame's energy (based on its samples).
|
||||
uint32_t AudioMixerCalculateEnergy(const AudioFrame& audio_frame);
|
||||
|
||||
// Apply linear step function that ramps in/out the audio samples in audio_frame
|
||||
void NewMixerRampIn(AudioFrame* audio_frame);
|
||||
void NewMixerRampOut(AudioFrame* audio_frame);
|
||||
// Ramps up or down the provided audio frame. Ramp(0, 1, frame) will
|
||||
// linearly increase the samples in the frame from 0 to full volume.
|
||||
void Ramp(float start_gain, float target_gain, AudioFrame* audio_frame);
|
||||
|
||||
// Downmixes or upmixes a frame between stereo and mono.
|
||||
void RemixFrame(size_t target_number_of_channels, AudioFrame* frame);
|
||||
|
||||
@ -0,0 +1,63 @@
|
||||
/*
|
||||
* Copyright (c) 2016 The WebRTC project authors. All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by a BSD-style license
|
||||
* that can be found in the LICENSE file in the root of the source
|
||||
* tree. An additional intellectual property rights grant can be found
|
||||
* in the file PATENTS. All contributing project authors may
|
||||
* be found in the AUTHORS file in the root of the source tree.
|
||||
*/
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
#include "webrtc/modules/audio_mixer/audio_frame_manipulator.h"
|
||||
#include "webrtc/modules/include/module_common_types.h"
|
||||
#include "webrtc/test/gtest.h"
|
||||
|
||||
namespace webrtc {
|
||||
namespace {
|
||||
|
||||
void FillFrameWithConstants(size_t samples_per_channel,
|
||||
size_t number_of_channels,
|
||||
int16_t value,
|
||||
AudioFrame* frame) {
|
||||
frame->num_channels_ = number_of_channels;
|
||||
frame->samples_per_channel_ = samples_per_channel;
|
||||
std::fill(frame->data_,
|
||||
frame->data_ + samples_per_channel * number_of_channels, value);
|
||||
}
|
||||
} // namespace
|
||||
|
||||
TEST(AudioFrameManipulator, CompareForwardRampWithExpectedResultStereo) {
|
||||
constexpr int kSamplesPerChannel = 5;
|
||||
constexpr int kNumberOfChannels = 2;
|
||||
|
||||
// Create a frame with values 5, 5, 5, ... and channels & samples as above.
|
||||
AudioFrame frame;
|
||||
FillFrameWithConstants(kSamplesPerChannel, kNumberOfChannels, 5, &frame);
|
||||
|
||||
Ramp(0.0f, 1.0f, &frame);
|
||||
|
||||
const int total_samples = kSamplesPerChannel * kNumberOfChannels;
|
||||
const int16_t expected_result[total_samples] = {0, 0, 1, 1, 2, 2, 3, 3, 4, 4};
|
||||
EXPECT_TRUE(
|
||||
std::equal(frame.data_, frame.data_ + total_samples, expected_result));
|
||||
}
|
||||
|
||||
TEST(AudioFrameManipulator, CompareBackwardRampWithExpectedResultMono) {
|
||||
constexpr int kSamplesPerChannel = 5;
|
||||
constexpr int kNumberOfChannels = 1;
|
||||
|
||||
// Create a frame with values 5, 5, 5, ... and channels & samples as above.
|
||||
AudioFrame frame;
|
||||
FillFrameWithConstants(kSamplesPerChannel, kNumberOfChannels, 5, &frame);
|
||||
|
||||
Ramp(1.0f, 0.0f, &frame);
|
||||
|
||||
const int total_samples = kSamplesPerChannel * kNumberOfChannels;
|
||||
const int16_t expected_result[total_samples] = {5, 4, 3, 2, 1};
|
||||
EXPECT_TRUE(
|
||||
std::equal(frame.data_, frame.data_ + total_samples, expected_result));
|
||||
}
|
||||
|
||||
} // namespace webrtc
|
||||
@ -22,61 +22,59 @@
|
||||
namespace webrtc {
|
||||
namespace {
|
||||
|
||||
class SourceFrame {
|
||||
public:
|
||||
SourceFrame(AudioSourceWithMixStatus* audio_source,
|
||||
struct SourceFrame {
|
||||
SourceFrame(AudioMixerImpl::SourceStatus* source_status,
|
||||
AudioFrame* audio_frame,
|
||||
bool muted)
|
||||
: audio_source_(audio_source), audio_frame_(audio_frame), muted_(muted) {
|
||||
if (!muted_) {
|
||||
energy_ = AudioMixerCalculateEnergy(*audio_frame);
|
||||
: source_status(source_status), audio_frame(audio_frame), muted(muted) {
|
||||
RTC_DCHECK(source_status);
|
||||
RTC_DCHECK(audio_frame);
|
||||
if (!muted) {
|
||||
energy = AudioMixerCalculateEnergy(*audio_frame);
|
||||
}
|
||||
}
|
||||
|
||||
SourceFrame(AudioSourceWithMixStatus* audio_source,
|
||||
SourceFrame(AudioMixerImpl::SourceStatus* source_status,
|
||||
AudioFrame* audio_frame,
|
||||
bool muted,
|
||||
uint32_t energy)
|
||||
: audio_source_(audio_source),
|
||||
audio_frame_(audio_frame),
|
||||
muted_(muted),
|
||||
energy_(energy) {}
|
||||
|
||||
// a.ShouldMixBefore(b) is used to select mixer sources.
|
||||
bool ShouldMixBefore(const SourceFrame& other) const {
|
||||
if (muted_ != other.muted_) {
|
||||
return other.muted_;
|
||||
}
|
||||
|
||||
const auto our_activity = audio_frame_->vad_activity_;
|
||||
const auto other_activity = other.audio_frame_->vad_activity_;
|
||||
|
||||
if (our_activity != other_activity) {
|
||||
return our_activity == AudioFrame::kVadActive;
|
||||
}
|
||||
|
||||
return energy_ > other.energy_;
|
||||
: source_status(source_status),
|
||||
audio_frame(audio_frame),
|
||||
muted(muted),
|
||||
energy(energy) {
|
||||
RTC_DCHECK(source_status);
|
||||
RTC_DCHECK(audio_frame);
|
||||
}
|
||||
|
||||
AudioSourceWithMixStatus* audio_source_ = nullptr;
|
||||
AudioFrame* audio_frame_ = nullptr;
|
||||
bool muted_ = true;
|
||||
uint32_t energy_ = 0;
|
||||
AudioMixerImpl::SourceStatus* source_status = nullptr;
|
||||
AudioFrame* audio_frame = nullptr;
|
||||
bool muted = true;
|
||||
uint32_t energy = 0;
|
||||
};
|
||||
|
||||
// ShouldMixBefore(a, b) is used to select mixer sources.
|
||||
bool ShouldMixBefore(const SourceFrame& a, const SourceFrame& b) {
|
||||
if (a.muted != b.muted) {
|
||||
return b.muted;
|
||||
}
|
||||
|
||||
void Ramp(const std::vector<SourceFrame>& mixed_sources_and_frames) {
|
||||
const auto a_activity = a.audio_frame->vad_activity_;
|
||||
const auto b_activity = b.audio_frame->vad_activity_;
|
||||
|
||||
if (a_activity != b_activity) {
|
||||
return a_activity == AudioFrame::kVadActive;
|
||||
}
|
||||
|
||||
return a.energy > b.energy;
|
||||
}
|
||||
|
||||
void RampAndUpdateGain(
|
||||
const std::vector<SourceFrame>& mixed_sources_and_frames) {
|
||||
for (const auto& source_frame : mixed_sources_and_frames) {
|
||||
// Ramp in previously unmixed.
|
||||
if (!source_frame.audio_source_->WasMixed()) {
|
||||
NewMixerRampIn(source_frame.audio_frame_);
|
||||
}
|
||||
|
||||
const bool is_mixed = source_frame.audio_source_->IsMixed();
|
||||
// Ramp out currently unmixed.
|
||||
if (source_frame.audio_source_->WasMixed() && !is_mixed) {
|
||||
NewMixerRampOut(source_frame.audio_frame_);
|
||||
}
|
||||
float target_gain = source_frame.source_status->is_mixed ? 1.0f : 0.0f;
|
||||
Ramp(source_frame.source_status->gain, target_gain,
|
||||
source_frame.audio_frame);
|
||||
source_frame.source_status->gain = target_gain;
|
||||
}
|
||||
}
|
||||
|
||||
@ -121,21 +119,21 @@ int32_t MixFromList(AudioFrame* mixed_audio,
|
||||
return 0;
|
||||
}
|
||||
|
||||
MixerAudioSourceList::const_iterator FindSourceInList(
|
||||
AudioMixerImpl::SourceStatusList::const_iterator FindSourceInList(
|
||||
AudioMixerImpl::Source const* audio_source,
|
||||
MixerAudioSourceList const* audio_source_list) {
|
||||
AudioMixerImpl::SourceStatusList const* audio_source_list) {
|
||||
return std::find_if(audio_source_list->begin(), audio_source_list->end(),
|
||||
[audio_source](const AudioSourceWithMixStatus& p) {
|
||||
return p.audio_source() == audio_source;
|
||||
[audio_source](const AudioMixerImpl::SourceStatus& p) {
|
||||
return p.audio_source == audio_source;
|
||||
});
|
||||
}
|
||||
|
||||
MixerAudioSourceList::iterator FindSourceInList(
|
||||
AudioMixerImpl::SourceStatusList::iterator FindSourceInList(
|
||||
AudioMixerImpl::Source const* audio_source,
|
||||
MixerAudioSourceList* audio_source_list) {
|
||||
AudioMixerImpl::SourceStatusList* audio_source_list) {
|
||||
return std::find_if(audio_source_list->begin(), audio_source_list->end(),
|
||||
[audio_source](const AudioSourceWithMixStatus& p) {
|
||||
return p.audio_source() == audio_source;
|
||||
[audio_source](const AudioMixerImpl::SourceStatus& p) {
|
||||
return p.audio_source == audio_source;
|
||||
});
|
||||
}
|
||||
|
||||
@ -362,7 +360,7 @@ AudioFrameList AudioMixerImpl::GetNonAnonymousAudio() {
|
||||
// Get audio source audio and put it in the struct vector.
|
||||
for (auto& source_and_status : audio_source_list_) {
|
||||
auto audio_frame_with_info =
|
||||
source_and_status.audio_source()->GetAudioFrameWithInfo(
|
||||
source_and_status.audio_source->GetAudioFrameWithInfo(
|
||||
id_, static_cast<int>(OutputFrequency()));
|
||||
|
||||
const auto audio_frame_info = audio_frame_with_info.audio_frame_info;
|
||||
@ -380,16 +378,15 @@ AudioFrameList AudioMixerImpl::GetNonAnonymousAudio() {
|
||||
|
||||
// Sort frames by sorting function.
|
||||
std::sort(audio_source_mixing_data_list.begin(),
|
||||
audio_source_mixing_data_list.end(),
|
||||
std::mem_fn(&SourceFrame::ShouldMixBefore));
|
||||
audio_source_mixing_data_list.end(), ShouldMixBefore);
|
||||
|
||||
int max_audio_frame_counter = kMaximumAmountOfMixedAudioSources;
|
||||
|
||||
// Go through list in order and put unmuted frames in result list.
|
||||
for (const auto& p : audio_source_mixing_data_list) {
|
||||
// Filter muted.
|
||||
if (p.muted_) {
|
||||
p.audio_source_->SetIsMixed(false);
|
||||
if (p.muted) {
|
||||
p.source_status->is_mixed = false;
|
||||
continue;
|
||||
}
|
||||
|
||||
@ -397,13 +394,13 @@ AudioFrameList AudioMixerImpl::GetNonAnonymousAudio() {
|
||||
bool is_mixed = false;
|
||||
if (max_audio_frame_counter > 0) {
|
||||
--max_audio_frame_counter;
|
||||
result.push_back(p.audio_frame_);
|
||||
ramp_list.emplace_back(p.audio_source_, p.audio_frame_, false, -1);
|
||||
result.push_back(p.audio_frame);
|
||||
ramp_list.emplace_back(p.source_status, p.audio_frame, false, -1);
|
||||
is_mixed = true;
|
||||
}
|
||||
p.audio_source_->SetIsMixed(is_mixed);
|
||||
p.source_status->is_mixed = is_mixed;
|
||||
}
|
||||
Ramp(ramp_list);
|
||||
RampAndUpdateGain(ramp_list);
|
||||
return result;
|
||||
}
|
||||
|
||||
@ -415,7 +412,7 @@ AudioFrameList AudioMixerImpl::GetAnonymousAudio() {
|
||||
AudioFrameList result;
|
||||
for (auto& source_and_status : additional_audio_source_list_) {
|
||||
const auto audio_frame_with_info =
|
||||
source_and_status.audio_source()->GetAudioFrameWithInfo(
|
||||
source_and_status.audio_source->GetAudioFrameWithInfo(
|
||||
id_, OutputFrequency());
|
||||
const auto ret = audio_frame_with_info.audio_frame_info;
|
||||
AudioFrame* audio_frame = audio_frame_with_info.audio_frame;
|
||||
@ -427,25 +424,25 @@ AudioFrameList AudioMixerImpl::GetAnonymousAudio() {
|
||||
if (ret != Source::AudioFrameInfo::kMuted) {
|
||||
result.push_back(audio_frame);
|
||||
ramp_list.emplace_back(&source_and_status, audio_frame, false, 0);
|
||||
source_and_status.SetIsMixed(true);
|
||||
source_and_status.is_mixed = true;
|
||||
}
|
||||
}
|
||||
Ramp(ramp_list);
|
||||
RampAndUpdateGain(ramp_list);
|
||||
return result;
|
||||
}
|
||||
|
||||
bool AudioMixerImpl::AddAudioSourceToList(
|
||||
Source* audio_source,
|
||||
MixerAudioSourceList* audio_source_list) const {
|
||||
SourceStatusList* audio_source_list) const {
|
||||
WEBRTC_TRACE(kTraceStream, kTraceAudioMixerServer, id_,
|
||||
"AddAudioSourceToList(audio_source, audio_source_list)");
|
||||
audio_source_list->emplace_back(audio_source);
|
||||
audio_source_list->emplace_back(audio_source, false, 0);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool AudioMixerImpl::RemoveAudioSourceFromList(
|
||||
Source* audio_source,
|
||||
MixerAudioSourceList* audio_source_list) const {
|
||||
SourceStatusList* audio_source_list) const {
|
||||
WEBRTC_TRACE(kTraceStream, kTraceAudioMixerServer, id_,
|
||||
"RemoveAudioSourceFromList(audio_source, audio_source_list)");
|
||||
const auto iter = FindSourceInList(audio_source, audio_source_list);
|
||||
@ -511,13 +508,13 @@ bool AudioMixerImpl::GetAudioSourceMixabilityStatusForTest(
|
||||
const auto non_anonymous_iter =
|
||||
FindSourceInList(audio_source, &audio_source_list_);
|
||||
if (non_anonymous_iter != audio_source_list_.end()) {
|
||||
return non_anonymous_iter->IsMixed();
|
||||
return non_anonymous_iter->is_mixed;
|
||||
}
|
||||
|
||||
const auto anonymous_iter =
|
||||
FindSourceInList(audio_source, &additional_audio_source_list_);
|
||||
if (anonymous_iter != audio_source_list_.end()) {
|
||||
return anonymous_iter->IsMixed();
|
||||
return anonymous_iter->is_mixed;
|
||||
}
|
||||
|
||||
LOG(LS_ERROR) << "Audio source unknown";
|
||||
|
||||
@ -18,7 +18,6 @@
|
||||
#include "webrtc/base/thread_annotations.h"
|
||||
#include "webrtc/base/thread_checker.h"
|
||||
#include "webrtc/modules/audio_mixer/audio_mixer.h"
|
||||
#include "webrtc/modules/audio_mixer/audio_source_with_mix_status.h"
|
||||
#include "webrtc/modules/audio_processing/include/audio_processing.h"
|
||||
#include "webrtc/modules/include/module_common_types.h"
|
||||
#include "webrtc/system_wrappers/include/critical_section_wrapper.h"
|
||||
@ -28,10 +27,19 @@
|
||||
namespace webrtc {
|
||||
|
||||
typedef std::vector<AudioFrame*> AudioFrameList;
|
||||
typedef std::vector<AudioSourceWithMixStatus> MixerAudioSourceList;
|
||||
|
||||
class AudioMixerImpl : public AudioMixer {
|
||||
public:
|
||||
struct SourceStatus {
|
||||
SourceStatus(Source* audio_source, bool is_mixed, float gain)
|
||||
: audio_source(audio_source), is_mixed(is_mixed), gain(gain) {}
|
||||
Source* audio_source = nullptr;
|
||||
bool is_mixed = false;
|
||||
float gain = 0.0f;
|
||||
};
|
||||
|
||||
typedef std::vector<SourceStatus> SourceStatusList;
|
||||
|
||||
// AudioProcessing only accepts 10 ms frames.
|
||||
static const int kFrameDurationInMs = 10;
|
||||
|
||||
@ -73,9 +81,9 @@ class AudioMixerImpl : public AudioMixer {
|
||||
// Add/remove the MixerAudioSource to the specified
|
||||
// MixerAudioSource list.
|
||||
bool AddAudioSourceToList(Source* audio_source,
|
||||
MixerAudioSourceList* audio_source_list) const;
|
||||
SourceStatusList* audio_source_list) const;
|
||||
bool RemoveAudioSourceFromList(Source* remove_audio_source,
|
||||
MixerAudioSourceList* audio_source_list) const;
|
||||
SourceStatusList* audio_source_list) const;
|
||||
|
||||
bool LimitMixedAudio(AudioFrame* mixed_audio) const;
|
||||
|
||||
@ -93,10 +101,10 @@ class AudioMixerImpl : public AudioMixer {
|
||||
size_t sample_size_ ACCESS_ON(&thread_checker_);
|
||||
|
||||
// List of all audio sources. Note all lists are disjunct
|
||||
MixerAudioSourceList audio_source_list_ GUARDED_BY(crit_); // May be mixed.
|
||||
SourceStatusList audio_source_list_ GUARDED_BY(crit_); // May be mixed.
|
||||
|
||||
// Always mixed, anonymously.
|
||||
MixerAudioSourceList additional_audio_source_list_ GUARDED_BY(crit_);
|
||||
SourceStatusList additional_audio_source_list_ GUARDED_BY(crit_);
|
||||
|
||||
size_t num_mixed_audio_sources_ GUARDED_BY(crit_);
|
||||
// Determines if we will use a limiter for clipping protection during
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user