~aleteoryx/muditaos

6c52780fdb851fba6f28d8b6c3da7b58f3de65ef — Marcin Smoczyński 4 years ago c3e84f8
[EGD-6496] Add basic sample rate transcoding

Add basic interpolator and decimator to change sample rates by a factor
of a prime number. Interpolator repeats samples forming a stair signal,
while decimator drops excessive samples.

Templates have been used to utilize fast integer operations contrary to
byte-by-byte operations for number of bit width vs channel count
combinations.

Signed-off-by: Marcin Smoczyński <smoczynski.marcin@gmail.com>
M module-audio/Audio/test/unittest_transcode.cpp => module-audio/Audio/test/unittest_transcode.cpp +63 -0
@@ 13,6 13,8 @@
#include <Audio/transcode/Transform.hpp>
#include <Audio/transcode/MonoToStereo.hpp>
#include <Audio/transcode/TransformComposite.hpp>
#include <Audio/transcode/BasicInterpolator.hpp>
#include <Audio/transcode/BasicDecimator.hpp>

#include <cstdlib>



@@ 264,3 266,64 @@ TEST(Transform, Composite)
    auto outputBlockSize = composite.transformBlockSize(sizeof(inputBuffer));
    EXPECT_EQ(outputBlockSize, 2 * sizeof(inputBuffer));
}

TEST(Transform, BasicInterpolator)
{
    audio::transcode::BasicInterpolator<std::uint16_t, 2, 2> interp2;
    audio::transcode::BasicInterpolator<std::uint32_t, 1, 3> interp3;

    EXPECT_EQ(interp2.transformBlockSize(128), 256);
    EXPECT_EQ(interp3.transformBlockSize(100), 300);

    auto format        = audio::AudioFormat{8000, 16, 2};
    auto outputFormat2 = interp2.transformFormat(format);
    auto outputFormat3 = interp3.transformFormat(format);

    EXPECT_EQ(outputFormat2.getSampleRate(), 16000);
    EXPECT_EQ(outputFormat3.getSampleRate(), 24000);

    EXPECT_EQ(outputFormat2.getBitWidth(), 16);
    EXPECT_EQ(outputFormat3.getBitWidth(), 16);

    EXPECT_EQ(outputFormat2.getChannels(), 2);
    EXPECT_EQ(outputFormat3.getChannels(), 2);

    EXPECT_TRUE(interp2.validateInputFormat(format));
    EXPECT_FALSE(interp3.validateInputFormat(format));

    std::uint16_t inputBuffer[8]          = {1, 2, 3, 4, 0, 0, 0, 0};
    static const uint16_t expectBuffer[8] = {1, 2, 1, 2, 3, 4, 3, 4};
    auto inputSpan  = ::audio::AbstractStream::Span{.data     = reinterpret_cast<uint8_t *>(inputBuffer),
                                                   .dataSize = 4 * sizeof(std::uint16_t)};
    auto outputSpan = interp2.transform(inputSpan, inputSpan);

    EXPECT_EQ(outputSpan.dataSize, sizeof(uint16_t) * 8);
    EXPECT_EQ(memcmp(outputSpan.data, expectBuffer, outputSpan.dataSize), 0);
}

TEST(Transform, BasicDecimator)
{
    audio::transcode::BasicDecimator<std::uint16_t, 2, 2> decim2;

    EXPECT_EQ(decim2.transformBlockSize(128), 64);

    auto format        = audio::AudioFormat{16000, 16, 2};
    auto outputFormat2 = decim2.transformFormat(format);

    EXPECT_EQ(outputFormat2.getSampleRate(), 8000);
    EXPECT_EQ(outputFormat2.getBitWidth(), 16);
    EXPECT_EQ(outputFormat2.getChannels(), 2);

    auto invalidFormat = audio::AudioFormat{16000, 8, 2};
    EXPECT_TRUE(decim2.validateInputFormat(format));
    EXPECT_FALSE(decim2.validateInputFormat(invalidFormat));

    std::uint16_t inputBuffer[8]          = {1, 2, 1, 2, 3, 4, 3, 4};
    static const uint16_t expectBuffer[8] = {1, 2, 3, 4, 0, 0, 0, 0};
    auto inputSpan  = ::audio::AbstractStream::Span{.data     = reinterpret_cast<uint8_t *>(inputBuffer),
                                                   .dataSize = 8 * sizeof(std::uint16_t)};
    auto outputSpan = decim2.transform(inputSpan, inputSpan);

    EXPECT_EQ(outputSpan.dataSize, sizeof(uint16_t) * 4);
    EXPECT_EQ(memcmp(outputSpan.data, expectBuffer, outputSpan.dataSize), 0);
}

A module-audio/Audio/transcode/BasicDecimator.hpp => module-audio/Audio/transcode/BasicDecimator.hpp +70 -0
@@ 0,0 1,70 @@
// Copyright (c) 2017-2021, Mudita Sp. z.o.o. All rights reserved.
// For licensing, see https://github.com/mudita/MuditaOS/LICENSE.md

#pragma once

#include "Transform.hpp"

#include <integer.hpp>

#include <type_traits>

#include <cstdint>

namespace audio::transcode
{
    /**
     * @brief Basic decimation transformation - for every Ratio samples it drops
     * Ratio - 1 samples. The transformation is performed using basic integer type
     * to allow compiler to perform loop optimizations. The transformation is performed
     * in-place.
     *
     * @tparam SampleType - type of a single PCM sample, e.g., std::uint16_t for LPCM16
     * @tparam Channels - number of channels; 1 for mono, 2 for stereo
     * @tparam Ratio - order of the decimator; e.g.: for Ratio = 4 drops 3 sample for each block of 4
     * reducing sample rate by the factor of 4.
     */
    template <typename SampleType, unsigned int Channels, unsigned int Ratio> class BasicDecimator : public Transform
    {
        static_assert(Channels == 1 || Channels == 2);
        static_assert(std::is_integral<SampleType>::value);
        static_assert(Ratio > 0);

        /**
         * @brief Integer type to be used to read and write data from/to a buffer.
         */
        using IntegerType = typename decltype(
            utils::integer::getIntegerType<sizeof(SampleType) * utils::integer::BitsInByte * Channels>())::type;

      public:
        auto transformBlockSize(std::size_t blockSize) const noexcept -> std::size_t override
        {
            return blockSize / Ratio;
        }

        auto transformFormat(const audio::AudioFormat &inputFormat) const noexcept -> audio::AudioFormat override
        {
            return audio::AudioFormat{
                inputFormat.getSampleRate() / Ratio, inputFormat.getBitWidth(), inputFormat.getChannels()};
        }

        auto validateInputFormat(const audio::AudioFormat &inputFormat) const noexcept -> bool override
        {
            return sizeof(SampleType) * utils::integer::BitsInByte == inputFormat.getBitWidth();
        }

        auto transform(const Span &inputSpan, const Span &transformSpace) const -> Span override
        {
            auto outputSpan     = Span{.data = transformSpace.data, .dataSize = transformBlockSize(inputSpan.dataSize)};
            IntegerType *input  = reinterpret_cast<IntegerType *>(inputSpan.data);
            IntegerType *output = reinterpret_cast<IntegerType *>(outputSpan.data);

            for (unsigned i = 0; i < inputSpan.dataSize / sizeof(IntegerType) / Ratio; i++) {
                output[i] = input[i * Ratio];
            }

            return outputSpan;
        }
    };

} // namespace audio::transcode

A module-audio/Audio/transcode/BasicInterpolator.hpp => module-audio/Audio/transcode/BasicInterpolator.hpp +72 -0
@@ 0,0 1,72 @@
// Copyright (c) 2017-2021, Mudita Sp. z.o.o. All rights reserved.
// For licensing, see https://github.com/mudita/MuditaOS/LICENSE.md

#pragma once

#include "Transform.hpp"

#include <integer.hpp>

#include <type_traits>

#include <cstdint>

namespace audio::transcode
{
    /**
     * @brief Basic interpolation transformation - for every Ratio samples it repeats
     * Ratio - 1 samples. The transformation is performed using basic integer type
     * to allow compiler to perform loop optimizations. The transformation is performed
     * in-place. The transformed signal is not filtered with a low-pass filter.
     *
     * @tparam SampleType - type of a single PCM sample, e.g., std::uint16_t for LPCM16
     * @tparam Channels - number of channels; 1 for mono, 2 for stereo
     * @tparam Ratio - order of the interpolator; e.g.: for Ratio = 4 repeats first sample 3
     * times for each block of 4 increasing sample rate by the factor of 4.
     */
    template <typename SampleType, unsigned int Channels, unsigned int Ratio> class BasicInterpolator : public Transform
    {
        static_assert(Channels == 1 || Channels == 2);
        static_assert(std::is_integral<SampleType>::value);
        static_assert(Ratio > 0);

        /**
         * @brief Integer type to be used to read and write data from/to a buffer.
         */
        using IntegerType = typename decltype(
            utils::integer::getIntegerType<sizeof(SampleType) * utils::integer::BitsInByte * Channels>())::type;

      public:
        auto transformBlockSize(std::size_t blockSize) const noexcept -> std::size_t override
        {
            return blockSize * Ratio;
        }

        auto transformFormat(const audio::AudioFormat &inputFormat) const noexcept -> audio::AudioFormat override
        {
            return audio::AudioFormat{
                inputFormat.getSampleRate() * Ratio, inputFormat.getBitWidth(), inputFormat.getChannels()};
        }

        auto validateInputFormat(const audio::AudioFormat &inputFormat) const noexcept -> bool override
        {
            return sizeof(SampleType) * utils::integer::BitsInByte == inputFormat.getBitWidth();
        }

        auto transform(const Span &inputSpan, const Span &transformSpace) const -> Span override
        {
            auto outputSpan     = Span{.data = transformSpace.data, .dataSize = transformBlockSize(inputSpan.dataSize)};
            IntegerType *input  = reinterpret_cast<IntegerType *>(inputSpan.data);
            IntegerType *output = reinterpret_cast<IntegerType *>(outputSpan.data);

            for (unsigned i = inputSpan.dataSize / sizeof(IntegerType); i > 0; i--) {
                for (unsigned j = 1; j <= Ratio; j++) {
                    output[i * Ratio - j] = input[i - 1];
                }
            }

            return outputSpan;
        }
    };

} // namespace audio::transcode

A module-utils/integer.hpp => module-utils/integer.hpp +53 -0
@@ 0,0 1,53 @@
// Copyright (c) 2017-2021, Mudita Sp. z.o.o. All rights reserved.
// For licensing, see https://github.com/mudita/MuditaOS/LICENSE.md

#pragma once

#include <cstdint>

namespace utils::integer
{

    /**
     * @brief number bits in bytes constant for greater clarity
     */
    constexpr inline auto BitsInByte = 8U;

    /**
     * @brief Allows to pass type as a return value with no overhead; used by
     * getIntegerType.
     *
     * @tparam T - type to return
     */
    template <typename T> struct TypeHolder
    {
        using type = T;
    };

    /**
     * @brief Discovers the type of integer which is best suited to hold
     * N bits of information, e.g.: returns std::uint8_t for 5 bits,
     * std::uint32_t for 18.
     *
     * @tparam Bits - number of bits which must an integer be able to hold.
     * @return integer type best suited to hold value Bits bits long.
     */
    template <unsigned int Bits> auto getIntegerType()
    {
        static_assert(Bits <= 64);

        if constexpr (Bits <= 8) {
            return TypeHolder<std::uint8_t>();
        }
        else if constexpr (Bits <= 16) {
            return TypeHolder<std::uint16_t>();
        }
        else if constexpr (Bits <= 32) {
            return TypeHolder<std::uint32_t>();
        }
        else {
            return TypeHolder<std::uint64_t>();
        }
    }

} // namespace utils::integer