blob: 990465d5654cd6562f559f5ad0cf60c6d94166ca [file] [log] [blame]
// Copyright 2022 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.
#include "src/media/audio/lib/processing/point_sampler.h"
#include <lib/trace/event.h>
#include <algorithm>
#include <cstdint>
#include <memory>
#include "fidl/fuchsia.mediastreams/cpp/wire_types.h"
#include "lib/syslog/cpp/macros.h"
#include "src/media/audio/lib/format2/channel_mapper.h"
#include "src/media/audio/lib/format2/fixed.h"
#include "src/media/audio/lib/format2/format.h"
#include "src/media/audio/lib/processing/gain.h"
namespace media_audio {
namespace {
// `PointSampler` is only used for 1:1 frame rate conversions. In such unity conversion cases,
// there may be situations where samples would continuously arrive at exactly halfway between two
// source frames. To resolve these into integral destination frames without introducing any
// latency, by preserving zero-phase, we would have to continuously average those two neighbouring
// source frames. However, this could potentially lead to a reduced output response at higher
// frequencies in a typical implementation, since we would compute each output frame by a linear
// interpolation of those two neighbouring frames. To avoid this issue, we always snap to the
// forward nearest neighbor sample directly without interpolation, i.e. choosing the newer frame
// position when the fractional sampling position is exactly in the middle between two positions.
constexpr int64_t kFracPositiveFilterLength = kFracHalfFrame + 1;
constexpr int64_t kFracNegativeFilterLength = kFracHalfFrame;
template <typename SourceSampleType, size_t SourceChannelCount, size_t DestChannelCount>
class PointSamplerImpl : public PointSampler {
public:
PointSamplerImpl()
: PointSampler(Fixed::FromRaw(kFracPositiveFilterLength),
Fixed::FromRaw(kFracNegativeFilterLength)) {}
void EagerlyPrepare() final {}
void Process(Source source, Dest dest, Gain gain, bool accumulate) final {
TRACE_DURATION("audio", "PointSampler::Process");
switch (gain.type) {
case GainType::kSilent:
if (accumulate) {
ProcessWith<GainType::kSilent, true>(source, dest, gain);
} else {
ProcessWith<GainType::kSilent, false>(source, dest, gain);
}
break;
case GainType::kNonUnity:
if (accumulate) {
ProcessWith<GainType::kNonUnity, true>(source, dest, gain);
} else {
ProcessWith<GainType::kNonUnity, false>(source, dest, gain);
}
break;
case GainType::kUnity:
if (accumulate) {
ProcessWith<GainType::kUnity, true>(source, dest, gain);
} else {
ProcessWith<GainType::kUnity, false>(source, dest, gain);
}
break;
case GainType::kRamping:
if (accumulate) {
ProcessWith<GainType::kRamping, true>(source, dest, gain);
} else {
ProcessWith<GainType::kRamping, false>(source, dest, gain);
}
break;
default:
break;
}
}
private:
// Processes `source` into `dest` with gain `Type`.
template <GainType Type, bool Accumulate>
static void ProcessWith(const Source& source, const Dest& dest, const Gain& gain) {
auto& dest_frame_offset = *dest.frame_offset_ptr;
if (dest_frame_offset >= dest.frame_count) {
// Nothing to process.
return;
}
// `source_frac_end` is the first subframe for which this call cannot produce output, since
// processing output centered on this position (or beyond) requires data that we don't have yet.
auto& source_frame_offset = *source.frame_offset_ptr;
const auto source_frac_offset = source_frame_offset.raw_value();
const int64_t source_frac_end =
(source.frame_count << Fixed::Format::FractionalBits) - kFracPositiveFilterLength + 1;
if (source_frac_offset >= source_frac_end) {
return;
}
// Process destination frames.
const int64_t frames_to_process =
std::min(Sampler::Ceiling(source_frac_end - source_frac_offset),
dest.frame_count - dest_frame_offset);
if constexpr (Type != GainType::kSilent) {
const auto* source_frame = &static_cast<const SourceSampleType*>(
source.samples)[Sampler::Floor(source_frac_offset + kFracPositiveFilterLength - 1) *
SourceChannelCount];
float* dest_frame = &dest.samples[dest_frame_offset * DestChannelCount];
float scale = gain.scale;
for (int64_t frame = 0; frame < frames_to_process; ++frame) {
if constexpr (Type == GainType::kRamping) {
scale = gain.scale_ramp[frame];
}
for (size_t dest_channel = 0; dest_channel < DestChannelCount; ++dest_channel) {
MixSample<Type, Accumulate>(mapper_.Map(source_frame, dest_channel),
&dest_frame[dest_channel], scale);
}
source_frame += SourceChannelCount;
dest_frame += DestChannelCount;
}
} else if constexpr (!Accumulate) {
// Zero fill destination frames.
std::fill_n(&dest.samples[dest_frame_offset * DestChannelCount],
frames_to_process * DestChannelCount, 0.0f);
}
// Advance the source and destination frame offsets by `frames_to_process`.
source_frame_offset =
Fixed::FromRaw(source_frac_offset + (frames_to_process << Fixed::Format::FractionalBits));
dest_frame_offset += frames_to_process;
}
static inline ChannelMapper<SourceSampleType, SourceChannelCount, DestChannelCount> mapper_;
};
// Helper functions to expand the combinations of possible `PointSamplerImpl` configurations.
template <typename SourceSampleType, size_t SourceChannelCount, size_t DestChannelCount>
std::shared_ptr<Sampler> CreateWith() {
return std::make_shared<
PointSamplerImpl<SourceSampleType, SourceChannelCount, DestChannelCount>>();
}
template <typename SourceSampleType, size_t SourceChannelCount>
std::shared_ptr<Sampler> CreateWith(int64_t dest_channel_count) {
switch (dest_channel_count) {
case 1:
return CreateWith<SourceSampleType, SourceChannelCount, 1>();
case 2:
return CreateWith<SourceSampleType, SourceChannelCount, 2>();
case 3:
if constexpr (SourceChannelCount <= 3) {
return CreateWith<SourceSampleType, SourceChannelCount, 3>();
}
break;
case 4:
if constexpr (SourceChannelCount != 3) {
return CreateWith<SourceSampleType, SourceChannelCount, 4>();
}
break;
default:
break;
}
FX_LOGS(WARNING) << "PointSampler does not support this channelization: " << SourceChannelCount
<< " -> " << dest_channel_count;
return nullptr;
}
template <typename SourceSampleType>
std::shared_ptr<Sampler> CreateWith(int64_t source_channel_count, int64_t dest_channel_count) {
// N -> N channel configuration.
if (source_channel_count == dest_channel_count) {
switch (source_channel_count) {
case 1:
return CreateWith<SourceSampleType, 1, 1>();
case 2:
return CreateWith<SourceSampleType, 2, 2>();
case 3:
return CreateWith<SourceSampleType, 3, 3>();
case 4:
return CreateWith<SourceSampleType, 4, 4>();
case 5:
return CreateWith<SourceSampleType, 5, 5>();
case 6:
return CreateWith<SourceSampleType, 6, 6>();
case 7:
return CreateWith<SourceSampleType, 7, 7>();
case 8:
return CreateWith<SourceSampleType, 8, 8>();
default:
FX_LOGS(WARNING) << "PointSampler does not support this channelization: "
<< source_channel_count << " -> " << dest_channel_count;
return nullptr;
}
}
// M -> N channel configuration.
switch (source_channel_count) {
case 1:
return CreateWith<SourceSampleType, 1>(dest_channel_count);
case 2:
return CreateWith<SourceSampleType, 2>(dest_channel_count);
case 3:
return CreateWith<SourceSampleType, 3>(dest_channel_count);
case 4:
return CreateWith<SourceSampleType, 4>(dest_channel_count);
default:
FX_LOGS(WARNING) << "PointSampler does not support this channelization: "
<< source_channel_count << " -> " << dest_channel_count;
return nullptr;
}
}
} // namespace
std::shared_ptr<Sampler> PointSampler::Create(const Format& source_format,
const Format& dest_format) {
TRACE_DURATION("audio", "PointSampler::Create");
if (source_format.frames_per_second() != dest_format.frames_per_second()) {
FX_LOGS(WARNING) << "PointSampler source frame rate " << source_format.frames_per_second()
<< " must be equal to dest frame rate " << dest_format.frames_per_second();
return nullptr;
}
if (dest_format.sample_format() != fuchsia_mediastreams::wire::AudioSampleFormat::kFloat) {
FX_LOGS(WARNING) << "PointSampler does not support this dest sample format: "
<< static_cast<uint32_t>(dest_format.sample_format());
return nullptr;
}
const int64_t source_channel_count = source_format.channels();
const int64_t dest_channel_count = dest_format.channels();
switch (source_format.sample_format()) {
case fuchsia_mediastreams::wire::AudioSampleFormat::kUnsigned8:
return CreateWith<uint8_t>(source_channel_count, dest_channel_count);
case fuchsia_mediastreams::wire::AudioSampleFormat::kSigned16:
return CreateWith<int16_t>(source_channel_count, dest_channel_count);
case fuchsia_mediastreams::wire::AudioSampleFormat::kSigned24In32:
return CreateWith<int32_t>(source_channel_count, dest_channel_count);
case fuchsia_mediastreams::wire::AudioSampleFormat::kFloat:
return CreateWith<float>(source_channel_count, dest_channel_count);
default:
FX_LOGS(WARNING) << "PointSampler does not support this source sample format: "
<< static_cast<uint32_t>(source_format.sample_format());
return nullptr;
}
}
} // namespace media_audio