chromium/chromecast/starboard/media/media/starboard_resampler.cc

// Copyright 2024 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "starboard_resampler.h"

#include <cmath>

#include "base/check_op.h"
#include "base/logging.h"
#include "chromecast/starboard/chromecast/starboard_cast_api/cast_starboard_api_types.h"

namespace chromecast {
namespace media {

namespace {

// A function type for converting an input sample to a double.
using ToDoubleFn = double (*)(const uint8_t* in_iter);

// A function type for converting the double specified by `in` to an output
// sample, written to `out_iter`.
using ToOutputFn = void (*)(double in, uint8_t* out_iter);

// Converts an unsigned 8-bit audio sample to a double.
double U8ToDouble(const uint8_t* in) {
  constexpr int16_t kUnsignedInt8Offset = 0x80;
  // After being shifted to signed 8 bit,the values are in [-128, 127].
  const double max_value = *in > kUnsignedInt8Offset ? 0x7F : 0x80;
  return (*in - kUnsignedInt8Offset) / max_value;
}

// Converts a signed 16-bit audio sample to a double.
double S16ToDouble(const uint8_t* in) {
  const int16_t in_s16 = *reinterpret_cast<const int16_t*>(in);
  const double max_value = in_s16 > 0 ? 0x7FFF : 0x8000;
  return in_s16 / max_value;
}

// Converts a Big Endian signed 16-bit audio sample to a double.
double S16BEToDouble(const uint8_t* in) {
  // Reverse the bytes.
  uint8_t data[2];
  data[0] = in[1];
  data[1] = in[0];

  // Now `data` holds an S16 in little endian byte order, so we can reuse the
  // logic of S16ToDouble.
  return S16ToDouble(data);
}

// Converts a signed 24-bit audio sample to a double.
double S24ToDouble(const uint8_t* in) {
  // Note: we avoid bit shifting here, since bit shifting signed types is
  // undefined behavior, and converting from signed to unsigned types can be
  // implementation-defined.
  uint8_t data[4];
  data[0] = in[0];
  data[1] = in[1];
  data[2] = in[2];
  // The highest byte of the int32_t is filled with the same value as the
  // highest bit of the int24, to properly account for the sign.
  data[3] = ((in[2] & 0x80) == 0) ? 0x00 : ~0x00;
  const int32_t in_s24 = *reinterpret_cast<int32_t*>(data);
  const double max_value = in_s24 > 0 ? 0x7FFFFF : 0x800000;
  return in_s24 / max_value;
}

// Converts a signed 32-bit audio sample to a double.
double S32ToDouble(const uint8_t* in) {
  const int32_t in_s32 = *reinterpret_cast<const int32_t*>(in);
  const double max_value = in_s32 > 0 ? 0x7FFFFFFF : 0x80000000;
  return in_s32 / max_value;
}

// Converts a float audio sample to a double.
double FloatToDouble(const uint8_t* in) {
  return *reinterpret_cast<const float*>(in);
}

// Converts a double audio sample to S16, writing it to `out`.
void WriteDoubleToS16(double in, uint8_t* out) {
  const int32_t max_value = in > 0 ? 0x7FFF : 0x8000;
  *reinterpret_cast<int16_t*>(out) = std::round(in * max_value);
}

// Converts a double audio sample to S32, writing it to `out`.
void WriteDoubleToS32(double in, uint8_t* out) {
  const int64_t max_value = in > 0 ? 0x7FFFFFFF : 0x80000000;
  *reinterpret_cast<int32_t*>(out) =
      std::round(static_cast<double>(in) * max_value);
}

// Converts a double audio sample to float, writing it to `out`.
void WriteDoubleToFloat(double in, uint8_t* out) {
  *reinterpret_cast<float*>(out) = in;
}

// Returns the number of bytes per channel for `format`. `format` must not be
// unknown.
//
// Note that this is different from the implementation of
// ::media::SampleFormatToBitsPerChannel(). That one maps S24 to 4 bytes instead
// of 3.
int BytesPerChannel(SampleFormat format) {
  switch (format) {
    case chromecast::media::kSampleFormatU8: {
      return 1;
    }
    case chromecast::media::kSampleFormatPlanarS16:
    case chromecast::media::kSampleFormatS16: {
      return 2;
    }
    case chromecast::media::kSampleFormatS24: {
      return 3;
    }
    case chromecast::media::kSampleFormatPlanarS32:
    case chromecast::media::kSampleFormatS32:
    case chromecast::media::kSampleFormatF32:
    case chromecast::media::kSampleFormatPlanarF32: {
      return 4;
    }
    default: {
      LOG(FATAL) << "Unsupported sample format: " << format;
    }
  }
}

// Returns the number of bytes per channel for `format`.
int BytesPerChannel(StarboardPcmSampleFormat format) {
  switch (format) {
    case kStarboardPcmSampleFormatS16:
      return 2;
    case kStarboardPcmSampleFormatS32:
    case kStarboardPcmSampleFormatF32:
      return 4;
  }
}

// Returns the conversion function needed to convert `in_sample_format` to
// double. `in_audio_codec` is needed because Big Endian S16 data is treated as
// its own codec (kCodecPCM_S16BE).
//
// This function crashes if an invalid in_sample_format is passed to it.
ToDoubleFn GetInputConversionFunction(SampleFormat in_sample_format,
                                      AudioCodec in_audio_codec) {
  switch (in_sample_format) {
    case chromecast::media::kSampleFormatU8: {
      return &U8ToDouble;
    }
    case chromecast::media::kSampleFormatPlanarS16:
    case chromecast::media::kSampleFormatS16: {
      return (in_audio_codec == kCodecPCM_S16BE) ? &S16BEToDouble
                                                 : &S16ToDouble;
    }
    case chromecast::media::kSampleFormatS24: {
      return &S24ToDouble;
    }
    case chromecast::media::kSampleFormatPlanarS32:
    case chromecast::media::kSampleFormatS32: {
      return &S32ToDouble;
    }
    case chromecast::media::kSampleFormatF32:
    case chromecast::media::kSampleFormatPlanarF32: {
      return &FloatToDouble;
    }
    default: {
      LOG(FATAL) << "Unsupported input format: " << in_sample_format;
    }
  }
}

// Returns the conversion function needed to convert double values to
// `out_sample_format`.
ToOutputFn GetOutputConversionFunction(
    StarboardPcmSampleFormat out_sample_format) {
  switch (out_sample_format) {
    case kStarboardPcmSampleFormatS16: {
      return &WriteDoubleToS16;
    }
    case kStarboardPcmSampleFormatS32: {
      return &WriteDoubleToS32;
    }
    case kStarboardPcmSampleFormatF32: {
      return &WriteDoubleToFloat;
    }
  }
}

// Resamples the PCM data in `buffer` from `in_sample_format` to
// `out_sample_format`. `out_size` is the size of the returned buffer, in bytes.
// `in_audio_codec` is needed because S16BE is treated as its own codec.
std::unique_ptr<uint8_t[]> ResamplePCM(
    const CastDecoderBuffer& buffer,
    int num_channels,
    SampleFormat in_sample_format,
    StarboardPcmSampleFormat out_sample_format,
    AudioCodec in_audio_codec,
    size_t& out_size) {
  DCHECK_GT(num_channels, 0);

  const int in_bytes_per_channel = BytesPerChannel(in_sample_format);
  const int out_bytes_per_channel = BytesPerChannel(out_sample_format);

  const uint8_t* in_data = buffer.data();
  out_size =
      (buffer.data_size() * out_bytes_per_channel) / in_bytes_per_channel;
  auto out_data = std::make_unique<uint8_t[]>(out_size);

  // These two functions are used to convert any input type to any output type.
  // The conversion goes:
  //   input -> double -> output
  // to reduce the number of code paths (2n vs (n choose 2)) and simplify the
  // conversion logic (since converting to/from double is straightforward).
  const ToDoubleFn convert_to_double =
      GetInputConversionFunction(in_sample_format, in_audio_codec);
  const ToOutputFn write_output_type =
      GetOutputConversionFunction(out_sample_format);

  const bool is_input_planar =
      in_sample_format == chromecast::media::kSampleFormatPlanarS16 ||
      in_sample_format == chromecast::media::kSampleFormatPlanarS32 ||
      in_sample_format == chromecast::media::kSampleFormatPlanarF32;

  const int num_planes = is_input_planar ? num_channels : 1;

  std::vector<int> plane_offsets;
  for (int plane = 0; plane < num_planes; ++plane) {
    plane_offsets.push_back(plane * (buffer.data_size() / num_planes));
  }

  // For each input sample, this loop converts it to double and then to the
  // final output format. It also handles converting from planar to interleaved,
  // if necessary.
  //
  // Planar data is grouped by channel; interleaved data is not. For example,
  // consider 2-channel data where A represents data from the first channel, and
  // B represents data from the second channel. 4 samples of planar data would
  // be:
  //   AAAABBBB
  // whereas 4 samples of interleaved data would be:
  //   ABABABAB
  for (int i = 0;
       i * static_cast<size_t>(in_bytes_per_channel) < buffer.data_size();
       ++i) {
    const int plane = i % num_planes;
    const int index_by_plane = i / num_planes;

    write_output_type(convert_to_double(in_data + plane_offsets[plane] +
                                        index_by_plane * in_bytes_per_channel),
                      &out_data[i * out_bytes_per_channel]);
  }

  return out_data;
}

// Returns true if the two formats do not match, meaning resampling is
// necessary. Returns false otherwise.
bool RequiresResampling(StarboardPcmSampleFormat format_to_decode_to,
                        SampleFormat format_to_decode_from) {
  const bool same_format =
      (format_to_decode_to == kStarboardPcmSampleFormatS16 &&
       format_to_decode_from == kSampleFormatS16) ||
      (format_to_decode_to == kStarboardPcmSampleFormatS32 &&
       format_to_decode_from == kSampleFormatS32) ||
      (format_to_decode_to == kStarboardPcmSampleFormatF32 &&
       format_to_decode_from == kSampleFormatF32);
  return !same_format;
}
}  // namespace

std::unique_ptr<uint8_t[]> ResamplePCMAudioDataForStarboard(
    StarboardPcmSampleFormat format_to_decode_to,
    SampleFormat format_to_decode_from,
    AudioCodec audio_codec,
    int audio_channels,
    const CastDecoderBuffer& buffer,
    size_t& buffer_out_size) {
  // The check for kCodecPCM is necessary because there is a separate codec,
  // kCodecPCM_S16BE, which is in Big Endian format instead of Little Endian
  // like all the other formats. In that case, at minimum we need to change the
  // samples to Little Endian, since we do not support Big Endian output
  // formats.
  if (audio_codec == AudioCodec::kCodecPCM &&
      !RequiresResampling(format_to_decode_to, format_to_decode_from)) {
    // No resampling is necessary; return a copy of the data.
    const size_t size_of_buffer = buffer.data_size();
    buffer_out_size = size_of_buffer;
    auto data_copy = std::make_unique<uint8_t[]>(size_of_buffer);
    memcpy(data_copy.get(), buffer.data(), size_of_buffer);
    return data_copy;
  }

  return ResamplePCM(buffer, audio_channels, format_to_decode_from,
                     format_to_decode_to, audio_codec, buffer_out_size);
}

}  // namespace media
}  // namespace chromecast