blob: 5283612f54b2dec8299f1370b790e864c57260ce [file] [log] [blame]
// Copyright 2021 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#ifndef SRC_MEDIA_AUDIO_LIB_ANALYSIS_DROPOUT_H_
#define SRC_MEDIA_AUDIO_LIB_ANALYSIS_DROPOUT_H_
#include <fuchsia/media/cpp/fidl.h>
#include <lib/syslog/cpp/macros.h>
#include <zircon/types.h>
#include <cmath>
#include <iomanip>
#include <optional>
#include <string>
#include <string_view>
#include "src/lib/fxl/strings/string_printf.h"
namespace media::audio {
// Below are three utility classes that can be used to detect dropouts.
// SilentPacketChecker checks each audio packet, returning _false_ if the packet contains at least
// one non-silent sample. For performance reasons, it stops when the first non-zero sample is found.
// This class works with all supported sample formats.
class SilentPacketChecker {
public:
static bool IsSilenceFloat32(const void* samples, int64_t sample_count) {
auto floats = static_cast<const float*>(samples);
for (int64_t idx = 0; idx < sample_count; ++idx) {
if (floats[idx] > std::numeric_limits<float>::epsilon() ||
floats[idx] < -std::numeric_limits<float>::epsilon()) {
return false;
}
}
return true;
}
static bool IsSilenceInt16(const void* samples, int64_t sample_count) {
auto ints = static_cast<const int16_t*>(samples);
for (int64_t idx = 0; idx < sample_count; ++idx) {
if (ints[idx]) {
return false;
}
}
return true;
}
static bool IsSilenceInt24In32(const void* samples, int64_t sample_count) {
auto ints = static_cast<const int32_t*>(samples);
for (int64_t idx = 0; idx < sample_count; ++idx) {
if (ints[idx] & 0xFFFFFF00) {
return false;
}
}
return true;
}
static bool IsSilenceUint8(const void* samples, int64_t sample_count) {
auto ints = static_cast<const uint8_t*>(samples);
for (int64_t idx = 0; idx < sample_count; ++idx) {
if (ints[idx] != 0x80) {
return false;
}
}
return true;
}
};
// PowerChecker verifies that a stream contains a signal of expected power.
// It calculates an audio section's power, returning false if this does not meet the expected val.
//
// This checker includes each sample in exactly one calculation (it does not use overlapping RMS
// windows). E.g., for a 512-sample window, the first RMS check is based on samples 0-511, and the
// second check based on samples 512-1023 (modulo any intervening Reset() calls).
//
// For simplicity, this class is currently limited to FLOAT data only.
class PowerChecker {
public:
PowerChecker(int64_t rms_window_in_frames, int32_t channels, double expected_min_power_rms,
const std::string_view& tag = "")
: rms_window_in_frames_(rms_window_in_frames),
channels_(channels),
expected_power_rms_(expected_min_power_rms),
tag_(tag.empty() ? tag : std::string_view(std::string(tag).append(": "))) {
Reset();
}
void Reset() {
running_window_frame_count_ = 0;
running_sum_squares_ = 0.0;
}
bool Check(const float* samples, int64_t frame_position, int64_t frame_count,
bool print = false) {
if (frame_position_ != frame_position) {
Reset();
}
frame_position_ = frame_position + frame_count;
bool pass = true;
// Ingest all the provided samples before leaving
while (frame_count) {
// Starting from any previously-retained running totals/counts, incorporate each additional
// sample until we have enough to analyze. Stop earlier if we run out of samples.
while (running_window_frame_count_ < rms_window_in_frames_ && frame_count) {
for (auto chan = 0; chan < channels_; ++chan) {
auto val = *samples;
running_sum_squares_ += (val * val);
samples++;
}
running_window_frame_count_++;
frame_count--;
}
// If we have enough to analyze, do so now and reset our running totals/counts to zero.
// Otherwise, just ingest these values and return true.
if (running_window_frame_count_ == rms_window_in_frames_) {
auto mean_squares =
running_sum_squares_ / static_cast<double>(rms_window_in_frames_ * channels_);
auto current_root_mean_squares = sqrt(mean_squares);
if (current_root_mean_squares + std::numeric_limits<float>::epsilon() <
expected_power_rms_) {
// We might have been provided enough samples for multiple windows (thus more than one
// success/fail calculation). If ANY of them fail, return false.
pass = false;
if (print) {
FX_LOGS(ERROR) << tag_ << "XXX Dropout detected. Across window of "
<< rms_window_in_frames_ << " frames, measured power " << std::fixed
<< std::setprecision(6) << std::setw(8) << current_root_mean_squares
<< " (expected " << std::fixed << std::setprecision(4) << std::setw(6)
<< expected_power_rms_ << ")";
}
} else {
if constexpr (kSuccessLogStride) {
if (print) {
if (success_log_count_ == 0) {
FX_LOGS(INFO) << tag_ << "XXX Across window of " << rms_window_in_frames_
<< " frames, successfully measured power " << std::fixed
<< std::setprecision(6) << std::setw(8) << current_root_mean_squares
<< " (expected " << std::fixed << std::setprecision(4) << std::setw(6)
<< expected_power_rms_ << ")";
}
success_log_count_ = ++success_log_count_ % kSuccessLogStride;
}
}
}
Reset();
}
}
return pass;
}
private:
const int64_t rms_window_in_frames_;
const int32_t channels_; // only used for display purposes
const double expected_power_rms_;
const std::string_view tag_;
int64_t frame_position_ = 0;
int64_t running_window_frame_count_;
double running_sum_squares_;
// To calibrate appropriate RMS limits for specific content, display the calculated RMS power even
// on success. A stride reduces log spam; making it PRIME (797 is appropriate) varies sampling
// across periodic signals. To disable this logging altogether, set kSuccessLogStride to 0.
static constexpr int64_t kSuccessLogStride = 0;
int64_t success_log_count_ = 0; // only used for display purposes
};
// SilenceChecker verifies that a stream does not contain a consecutive number of truly silent
// frames, with Check() returning false if this ever occurs. For simplicity, this class is currently
// limited to FLOAT data only.
class SilenceChecker {
static constexpr bool kOnlyLogFailureOnEnd = true;
public:
explicit SilenceChecker(int64_t max_count_silent_frames_allowed, int32_t channels,
const std::string_view& tag = "")
: max_silent_frames_allowed_(max_count_silent_frames_allowed),
channels_(channels),
tag_(tag.empty() ? tag : std::string_view(std::string(tag).append(": "))) {}
inline void Reset(int64_t frame_position = 0, bool print = false) {
if (running_silent_frame_count_) {
if constexpr (kOnlyLogFailureOnEnd) {
if (running_silent_frame_count_ > max_silent_frames_allowed_ && print) {
LogFailure(running_silent_frames_start_, running_silent_frame_count_);
}
}
if (print) {
FX_LOGS(INFO) << tag_ << "Clearing running silent frames (was "
<< running_silent_frame_count_ << ")";
}
running_silent_frame_count_ = 0;
}
frame_position_ = frame_position;
}
bool Check(const float* samples, int64_t frame_position, int64_t frame_count,
bool print = false) {
if (frame_position_ != frame_position) {
if (print) {
FX_LOGS(INFO) << tag_ << "Changing running frame position from " << frame_position_
<< " to " << frame_position;
}
Reset(frame_position);
}
bool pass = true;
int64_t max_silent_frames_detected = 0;
int64_t max_silent_frames_start = running_silent_frames_start_;
// Ingest all provided samples before leaving
while (frame_count--) {
// Starting from any previously-retained running count, incorporate additional silent frames.
bool is_frame_silent = true;
for (auto chan = 0; chan < channels_; ++chan) {
if (samples[chan] > std::numeric_limits<float>::epsilon() ||
samples[chan] < -std::numeric_limits<float>::epsilon()) {
is_frame_silent = false;
break;
}
}
if (is_frame_silent) {
if (!running_silent_frame_count_) {
running_silent_frames_start_ = frame_position_;
}
++running_silent_frame_count_;
if (running_silent_frame_count_ > max_silent_frames_allowed_) {
pass = false;
}
if (running_silent_frame_count_ > max_silent_frames_detected) {
max_silent_frames_start = running_silent_frames_start_;
max_silent_frames_detected = running_silent_frame_count_;
}
} else {
Reset(frame_position_, print);
}
samples += channels_;
frame_position_++;
}
if constexpr (!kOnlyLogFailureOnEnd) {
if (max_silent_frames_detected > max_silent_frames_allowed_ && print) {
LogFailure(max_silent_frames_start, max_silent_frames_detected);
}
}
return pass;
}
void LogFailure(int64_t max_silent_frames_start, int64_t max_silent_frames_detected) {
FX_LOGS(ERROR) << tag_ << "XXX Silence detected -- measured " << max_silent_frames_detected
<< " consecutive silent frames (max allowed: " << max_silent_frames_allowed_
<< ") starting at " << max_silent_frames_start;
}
private:
const int64_t max_silent_frames_allowed_;
const int32_t channels_;
const std::string_view tag_;
int64_t frame_position_ = 0;
int64_t running_silent_frame_count_ = 0;
int64_t running_silent_frames_start_;
};
// This rudimentary checker looks for overlaps/gaps in a sequence of PTS ranges (start and length).
//
// When given NO_TIMESTAMP, it subsequently will not trigger (i.e. it errs toward false negative).
// It also does not reason about continuity thresholds, nor whether a packet is submitted after the
// PTS. These aspects may be addressed in subsequent CL or in a different class altogether.
class BasicTimestampChecker {
public:
explicit BasicTimestampChecker(std::optional<int64_t> pts_start = std::nullopt,
const std::string_view& tag = "")
: last_pts_end_(pts_start),
tag_(tag.empty() ? tag : std::string_view(std::string(tag).append(": "))) {
Reset();
}
inline void Reset(std::optional<int64_t> pts_start = std::nullopt) { last_pts_end_ = pts_start; }
bool Check(int64_t pts_start, int64_t pts_len, bool print = false) {
if (pts_start == fuchsia::media::NO_TIMESTAMP) {
last_pts_end_ = fuchsia::media::NO_TIMESTAMP;
return true;
}
if (!last_pts_end_.has_value()) {
last_pts_end_ = pts_start + pts_len;
return true;
}
if (*last_pts_end_ == fuchsia::media::NO_TIMESTAMP) {
return true;
}
int64_t pts_delta = *last_pts_end_ - pts_start;
if (print) {
if (pts_delta > 0) {
FX_LOGS(ERROR) << tag_ << "XXX Packet overlap of " << separated_pts(pts_delta)
<< " detected -- prev pts_end " << separated_pts(*last_pts_end_)
<< ", this pts_start " << separated_pts(pts_start);
} else if (pts_delta < 0) {
FX_LOGS(ERROR) << tag_ << "XXX Gap of " << separated_pts(-pts_delta)
<< " detected between packets -- prev pts_end "
<< separated_pts(*last_pts_end_) << ", this pts_start "
<< separated_pts(pts_start);
}
}
last_pts_end_ = pts_start + pts_len;
return (pts_delta == 0);
}
private:
friend class DropoutBasicTimestampChecker;
static std::string separated_pts(int64_t pts) {
bool was_negative = (pts < 0);
if (was_negative) {
pts = -pts;
}
std::string pts_str = fxl::StringPrintf("%03zd", pts % 1000);
pts /= 1000;
while (pts > 0) {
pts_str = fxl::StringPrintf("%03zd", pts % 1000).append("'").append(pts_str);
pts /= 1000;
}
if (was_negative) {
pts_str = "-" + pts_str;
}
return pts_str;
}
std::optional<int64_t> last_pts_end_ = std::nullopt;
const std::string_view tag_;
};
} // namespace media::audio
#endif // SRC_MEDIA_AUDIO_LIB_ANALYSIS_DROPOUT_H_