~aleteoryx/muditaos

9cf397d1926ad80f09f755c0fba812abf9c63257 — Marcin Smoczyński 4 years ago b39dd15
[EGD-6497] Add audio transcoding framework

Add a mechanism to apply a data transform on the stream's input. An
example mono to stereo transform is provided.

Signed-off-by: Marcin Smoczyński <smoczynski.marcin@gmail.com>
A module-audio/Audio/AbstractStream.hpp => module-audio/Audio/AbstractStream.hpp +235 -0
@@ 0,0 1,235 @@
// 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 "AudioFormat.hpp"

#include <cstdint>

namespace audio
{
    /**
     * @brief An abstract interface for classes implementing data connection
     * between two endpoints. The data is stored in blocks and accessed just
     * like in a regular FIFO. It provides an API to implement a zero-copy
     * mechanism.
     *
     */
    class AbstractStream
    {
      public:
        /**
         * @brief Events that are broadcasted to listeners by an AbstractStream
         *
         */
        enum class Event
        {
            NoEvent,
            StreamFull,
            StreamHalfUsed,
            StreamEmpty,
            StreamOverflow,
            StreamUnderFlow
        };

        /**
         * @brief An abstract interface for AbstractStream's event listeners
         *
         */
        class EventListener
        {
          public:
            /**
             * @brief Called on event by AbstractStream
             *
             * @param stream - stream reporting the event
             * @param event - type of event
             */
            virtual void onEvent(AbstractStream *stream, Event event) = 0;
        };

        /**
         * @brief A structure describing a range of raw data using a pair of
         * pointer and the size of data.
         */
        struct Span
        {
            /**
             * @brief Pointer to raw data.
             */
            std::uint8_t *data = nullptr;

            /**
             * @brief Size of data
             */
            std::size_t dataSize = 0;

            /**
             * @brief Calculate pointer to the end of the raw buffer
             *
             * @return pointer to the end of data.
             */
            constexpr std::uint8_t *dataEnd() const noexcept
            {
                return data + dataSize;
            }

            /**
             * @brief Resets the stored value to Null
             *
             */
            constexpr void reset() noexcept
            {
                data     = nullptr;
                dataSize = 0;
            }

            inline bool operator==(const Span &other) const noexcept
            {
                return data == other.data && dataSize == other.dataSize;
            }

            inline bool operator!=(const Span &other) const noexcept
            {
                return !operator==(other);
            }
        };

        /**
         * @brief A structure describing characteristics of an AbstractStream's
         * endpoint.
         *
         */
        struct Traits
        {
            /**
             * @brief the size of data to read/write in a single operation
             */
            std::size_t blockSize = 0;

            /**
             * @brief the format of data stored in the AbstractStream
             */
            AudioFormat format = nullFormat;
        };

        virtual ~AbstractStream() = default;

        /**
         * @brief Registers events listener
         *
         * @param listener to register
         */
        virtual void registerListener(EventListener *listener) = 0;

        /**
         * @brief Unregisters events listener
         *
         * @param listener to unregister
         */
        virtual void unregisterListeners(EventListener *listener) = 0;

        /**
         * @brief Fills single block with data provided
         *
         * @param data - a pointer to raw data to be copied from
         * @param dataSize - size of the data
         * @return true if operation succeded, false otherwise
         * @return false
         */
        virtual bool push(void *data, std::size_t dataSize) = 0;

        /**
         * @brief Fills single block with data provided
         *
         * @param span describing block of data to be copied
         *
         * @return true if operation succeded, false otherwise
         */
        virtual bool push(const Span &span) = 0;

        /**
         * @brief Fills single block with empty data
         *
         * @return true if operation succeded, false otherwise
         */
        virtual bool push() = 0;

        /**
         * @brief Copies one block of data to a buffer provided as an argument
         *
         * @param span - a place to copy stream's data to
         *
         * @return true if operation succeded, false otherwise
         */
        virtual bool pop(Span &span) = 0;

        /**
         * @brief Reserves a block in a stream for writing
         *
         * @param span - a space reserved within the stream
         * @return true if operation succeded, false otherwise
         */
        virtual bool reserve(Span &span) = 0;

        /**
         * @brief Marks reserved block as ready and writes it to the stream.
         */
        virtual void commit() = 0;

        /**
         * @brief Discards data written to the reserved block
         */
        virtual void release() = 0;

        /**
         * @brief Gets pointer and size of data ready to read without copying.
         *
         * @param span
         *
         * @return true if operation succeded, false otherwise
         */
        virtual bool peek(Span &span) = 0;

        /**
         * @brief Marks peeked data as read.
         */
        virtual void consume() = 0;

        /**
         * @brief Reset peek state without consuming it.
         */
        virtual void unpeek() = 0;

        /**
         * @brief Resets the state of a stream; discards all data
         */
        virtual void reset() = 0;

        /**
         * @brief Get the traits of the stream's input.
         *
         * @return Traits
         */
        [[nodiscard]] virtual auto getInputTraits() const noexcept -> Traits = 0;

        /**
         * @brief Get the traits of the stream's input.
         *
         * @return Traits
         */
        [[nodiscard]] virtual auto getOutputTraits() const noexcept -> Traits = 0;

        /**
         * @brief Checks if stream is empty.
         */
        [[nodiscard]] virtual bool isEmpty() const noexcept = 0;

        /**
         * @brief Checks if stream is full.
         */
        [[nodiscard]] virtual bool isFull() const noexcept = 0;
    };

}; // namespace audio

M module-audio/Audio/Operation/RouterOperation.hpp => module-audio/Audio/Operation/RouterOperation.hpp +1 -0
@@ 10,6 10,7 @@
#include <Audio/AudioCommon.hpp>
#include <Audio/Profiles/Profile.hpp>
#include <Audio/Endpoint.hpp>
#include <Audio/Stream.hpp>

#include <mutex.hpp>


M module-audio/Audio/Stream.cpp => module-audio/Audio/Stream.cpp +19 -18
@@ 1,17 1,15 @@
// Copyright (c) 2017-2020, Mudita Sp. z.o.o. All rights reserved.
// Copyright (c) 2017-2021, Mudita Sp. z.o.o. All rights reserved.
// For licensing, see https://github.com/mudita/MuditaOS/LICENSE.md

#include "Stream.hpp"

#include <macros.h>

#include <algorithm>
#include <iterator>

using namespace audio;

Stream::Stream(Allocator &allocator, std::size_t blockSize, unsigned int bufferingSize)
    : _allocator(allocator), _blockSize(blockSize), _blockCount(bufferingSize),
Stream::Stream(AudioFormat format, Allocator &allocator, std::size_t blockSize, unsigned int bufferingSize)
    : _allocator(allocator), _blockSize(blockSize), _blockCount(bufferingSize), _format(format),
      _buffer(_allocator.allocate(_blockSize * _blockCount)), _emptyBuffer(_allocator.allocate(_blockSize)),
      _dataStart(_buffer.get(), _blockSize * _blockCount, _buffer.get(), _blockSize), _dataEnd(_dataStart),
      _peekPosition(_dataStart), _writeReservationPosition(_dataStart)


@@ 169,21 167,31 @@ void Stream::release()
    _writeReservationPosition = _dataEnd;
}

std::size_t Stream::getBlockSize() const noexcept
auto Stream::getInputTraits() const noexcept -> Traits
{
    LockGuard lock;
    return getIOTraits();
}

    return _blockSize;
auto Stream::getOutputTraits() const noexcept -> Traits
{
    LockGuard lock;
    return getIOTraits();
}

void Stream::registerListener(EventListener *listener)
auto Stream::getIOTraits() const noexcept -> Traits
{
    return Traits{.blockSize = _blockSize, .format = _format};
}

void Stream::registerListener(AbstractStream::EventListener *listener)
{
    LockGuard lock;

    listeners.push_back(std::ref(listener));
}

void Stream::unregisterListeners(Stream::EventListener *listener)
void Stream::unregisterListeners(AbstractStream::EventListener *listener)
{
    LockGuard lock;



@@ 195,10 203,8 @@ void Stream::unregisterListeners(Stream::EventListener *listener)

void Stream::broadcastEvent(Event event)
{
    auto eventMode = isIRQ() ? EventSourceMode::ISR : EventSourceMode::Thread;

    for (auto listener : listeners) {
        listener->onEvent(this, event, eventMode);
        listener->onEvent(this, event);
    }
}



@@ 251,7 257,7 @@ bool Stream::isFull() const noexcept

bool Stream::blocksAvailable() const noexcept
{
    return !isEmpty();
    return !isFull();
}

void Stream::reset()


@@ 338,11 344,6 @@ Stream::Span Stream::RawBlockIterator::operator*()
    return Stream::Span{.data = _curPos, .dataSize = _stepSize};
}

std::uint8_t *Stream::Span::dataEnd() const noexcept
{
    return data + dataSize;
}

Stream::Span Stream::getNullSpan() const noexcept
{
    return Span{.data = _emptyBuffer.get(), .dataSize = _blockSize};

M module-audio/Audio/Stream.hpp => module-audio/Audio/Stream.hpp +32 -51
@@ 1,8 1,11 @@
// Copyright (c) 2017-2020, Mudita Sp. z.o.o. All rights reserved.
// 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 "AbstractStream.hpp"
#include "AudioFormat.hpp"

#include <memory/NonCachedMemAllocator.hpp>
#include <CriticalSectionGuard.hpp>



@@ 15,18 18,11 @@

namespace audio
{
    class Stream
    class Stream : public AbstractStream
    {
      public:
        using UniqueStreamBuffer = std::unique_ptr<std::uint8_t[], std::function<void(uint8_t[])>>;

        struct Span
        {
            std::uint8_t *data   = nullptr;
            std::size_t dataSize = 0;

            std::uint8_t *dataEnd() const noexcept;
        };
        using AbstractStream::Span;

        class RawBlockIterator
        {


@@ 54,72 50,56 @@ namespace audio
            virtual UniqueStreamBuffer allocate(std::size_t size) = 0;
        };

        enum class Event
        {
            NoEvent,
            StreamFull,
            StreamHalfUsed,
            StreamEmpty,
            StreamOverflow,
            StreamUnderFlow
        };

        enum class EventSourceMode
        {
            ISR,
            Thread
        };

        class EventListener
        {
          public:
            virtual void onEvent(Stream *stream, Event event, EventSourceMode source) = 0;
        };

        static constexpr auto defaultBufferingSize = 4U;

        Stream(Allocator &allocator, std::size_t blockSize, unsigned int bufferingSize = defaultBufferingSize);
        Stream(AudioFormat format,
               Allocator &allocator,
               std::size_t blockSize,
               unsigned int bufferingSize = defaultBufferingSize);

        void registerListener(AbstractStream::EventListener *listener) override;
        void unregisterListeners(AbstractStream::EventListener *listener) override;

        /// push
        bool push(void *data, std::size_t dataSize);
        bool push(const Span &span);
        bool push();
        bool push(void *data, std::size_t dataSize) override;
        bool push(const Span &span) override;
        bool push() override;

        /// pop
        bool pop(Span &span);
        bool pop(Span &span) override;

        /// zero copy write
        bool reserve(Span &span);
        void commit();
        void release();
        bool reserve(Span &span) override;
        void commit() override;
        void release() override;

        /// zero copy read
        bool peek(Span &span);
        void consume();
        void unpeek();
        bool peek(Span &span) override;
        void consume() override;
        void unpeek() override;

        void reset() override;

        /// get empty data span
        Span getNullSpan() const noexcept;

        void reset();
        [[nodiscard]] auto getInputTraits() const noexcept -> Traits override;
        [[nodiscard]] auto getOutputTraits() const noexcept -> Traits override;
        [[nodiscard]] bool isEmpty() const noexcept override;
        [[nodiscard]] bool isFull() const noexcept override;

        [[nodiscard]] std::size_t getBlockSize() const noexcept;
        [[nodiscard]] std::size_t getBlockCount() const noexcept;
        [[nodiscard]] std::size_t getUsedBlockCount() const noexcept;
        [[nodiscard]] std::size_t getPeekedCount() const noexcept;
        [[nodiscard]] std::size_t getReservedCount() const noexcept;
        [[nodiscard]] bool isEmpty() const noexcept;
        [[nodiscard]] bool isFull() const noexcept;
        [[nodiscard]] bool blocksAvailable() const noexcept;

        void registerListener(EventListener *listener);
        void unregisterListeners(EventListener *listener);

      private:
        using LockGuard = cpp_freertos::CriticalSectionGuard;

        void broadcastEvent(Event event);
        void broadcastStateEvents();
        auto getIOTraits() const noexcept -> Traits;

        Allocator &_allocator;
        std::size_t _blockSize    = 0;


@@ 127,9 107,10 @@ namespace audio
        std::size_t _blocksUsed   = 0;
        std::size_t _peekCount    = 0;
        std::size_t _reserveCount = 0;
        AudioFormat _format       = nullFormat;
        UniqueStreamBuffer _buffer;
        UniqueStreamBuffer _emptyBuffer;
        std::list<EventListener *> listeners;
        std::list<AbstractStream::EventListener *> listeners;

        RawBlockIterator _dataStart;
        RawBlockIterator _dataEnd;

M module-audio/Audio/StreamFactory.cpp => module-audio/Audio/StreamFactory.cpp +4 -2
@@ 18,11 18,13 @@ StreamFactory::StreamFactory(Endpoint::Capabilities factoryCaps, unsigned int bu
    : caps(std::move(factoryCaps)), bufferingSize(bufferingSize)
{}

auto StreamFactory::makeStream(const Source &source, const Sink &sink) -> std::unique_ptr<Stream>
auto StreamFactory::makeStream(Source &source, Sink &sink) -> std::unique_ptr<Stream>
{
    auto negotiatedCaps = negotiateCaps({source, sink});
    auto format         = source.getSourceFormat();

    return std::make_unique<Stream>(getAllocator(negotiatedCaps.usesDMA), negotiatedCaps.maxBlockSize, bufferingSize);
    return std::make_unique<Stream>(
        format, getAllocator(negotiatedCaps.usesDMA), negotiatedCaps.maxBlockSize, bufferingSize);
}

auto StreamFactory::negotiateCaps(std::vector<std::reference_wrapper<const Endpoint>> v) -> Endpoint::Capabilities

M module-audio/Audio/StreamFactory.hpp => module-audio/Audio/StreamFactory.hpp +2 -1
@@ 4,6 4,7 @@
#pragma once

#include "Endpoint.hpp"
#include "Stream.hpp"

#include <memory>
#include <vector>


@@ 16,7 17,7 @@ namespace audio
      public:
        explicit StreamFactory(Endpoint::Capabilities factoryCaps,
                               unsigned int bufferingSize = Stream::defaultBufferingSize);
        auto makeStream(const Source &source, const Sink &sink) -> std::unique_ptr<Stream>;
        auto makeStream(Source &source, Sink &sink) -> std::unique_ptr<Stream>;

      private:
        auto negotiateCaps(std::vector<std::reference_wrapper<const Endpoint>> v) -> Endpoint::Capabilities;

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

#include "StreamProxy.hpp"
#include "AbstractStream.hpp"

using audio::StreamProxy;

StreamProxy::StreamProxy(AbstractStream &wrappedStream) noexcept : wrappedStream(wrappedStream)
{}

bool StreamProxy::push(void *data, std::size_t dataSize)
{
    return wrappedStream.push(data, dataSize);
}

bool StreamProxy::push(const Span &span)
{
    return wrappedStream.push(span);
}

bool StreamProxy::push()
{
    return wrappedStream.push();
}

bool StreamProxy::pop(Span &span)
{
    return wrappedStream.pop(span);
}

bool StreamProxy::reserve(Span &span)
{
    return wrappedStream.reserve(span);
}

void StreamProxy::commit()
{
    wrappedStream.commit();
}

void StreamProxy::release()
{
    wrappedStream.release();
}

bool StreamProxy::peek(Span &span)
{
    return wrappedStream.peek(span);
}

void StreamProxy::consume()
{
    wrappedStream.consume();
}

void StreamProxy::unpeek()
{
    wrappedStream.unpeek();
}

void StreamProxy::reset()
{
    wrappedStream.reset();
}

void StreamProxy::registerListener(EventListener *listener)
{
    wrappedStream.registerListener(listener);
}

void StreamProxy::unregisterListeners(EventListener *listener)
{
    wrappedStream.unregisterListeners(listener);
}

auto StreamProxy::getOutputTraits() const noexcept -> Traits
{
    return wrappedStream.getOutputTraits();
}

auto StreamProxy::getInputTraits() const noexcept -> Traits
{
    return wrappedStream.getInputTraits();
}

bool StreamProxy::isEmpty() const noexcept
{
    return wrappedStream.isEmpty();
}

bool StreamProxy::isFull() const noexcept
{
    return wrappedStream.isFull();
}

auto StreamProxy::getWrappedStream() -> AbstractStream &
{
    return wrappedStream;
}

A module-audio/Audio/StreamProxy.hpp => module-audio/Audio/StreamProxy.hpp +61 -0
@@ 0,0 1,61 @@
// 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 "AbstractStream.hpp"

namespace audio
{

    /**
     * @brief Proxy for the AbstractStream class allowing to wrap its methods
     * with custom logic.
     */
    class StreamProxy : public AbstractStream
    {
      public:
        /**
         * @brief Construct a new Stream Proxy object
         *
         * @param wrappedStream - stream to wrap.
         */
        explicit StreamProxy(AbstractStream &wrappedStream) noexcept;

        // listener registration
        void registerListener(EventListener *listener) override;
        void unregisterListeners(EventListener *listener) override;

        /// push
        bool push(void *data, std::size_t dataSize) override;
        bool push(const Span &span) override;
        bool push() override;

        /// pop
        bool pop(Span &span) override;

        /// zero copy write
        bool reserve(Span &span) override;
        void commit() override;
        void release() override;

        /// zero copy read
        bool peek(Span &span) override;
        void consume() override;
        void unpeek() override;

        void reset() override;

        [[nodiscard]] auto getInputTraits() const noexcept -> Traits override;
        [[nodiscard]] auto getOutputTraits() const noexcept -> Traits override;
        [[nodiscard]] bool isEmpty() const noexcept override;
        [[nodiscard]] bool isFull() const noexcept override;

      protected:
        auto getWrappedStream() -> AbstractStream &;

      private:
        AbstractStream &wrappedStream;
    };

} // namespace audio

M module-audio/Audio/StreamQueuedEventsListener.cpp => module-audio/Audio/StreamQueuedEventsListener.cpp +8 -4
@@ 1,20 1,24 @@
// Copyright (c) 2017-2020, Mudita Sp. z.o.o. All rights reserved.
// Copyright (c) 2017-2021, Mudita Sp. z.o.o. All rights reserved.
// For licensing, see https://github.com/mudita/MuditaOS/LICENSE.md

#include "StreamQueuedEventsListener.hpp"

using namespace audio;
#include <macros.h>

#include <utility>

using audio::StreamQueuedEventsListener;

StreamQueuedEventsListener::StreamQueuedEventsListener(std::shared_ptr<cpp_freertos::Queue> eventsQueue)
    : queue(eventsQueue)
{}

void StreamQueuedEventsListener::onEvent(Stream *stream, Stream::Event event, Stream::EventSourceMode source)
void StreamQueuedEventsListener::onEvent(AbstractStream *stream, Stream::Event event)
{
    portBASE_TYPE xHigherPriorityTaskWoken = pdFALSE;
    EventStorage newStorage                = {stream, event};

    if (source == Stream::EventSourceMode::ISR) {
    if (isIRQ()) {
        queue->EnqueueFromISR(&newStorage, &xHigherPriorityTaskWoken);
        if (xHigherPriorityTaskWoken) {
            taskYIELD();

M module-audio/Audio/StreamQueuedEventsListener.hpp => module-audio/Audio/StreamQueuedEventsListener.hpp +6 -6
@@ 1,4 1,4 @@
// Copyright (c) 2017-2020, Mudita Sp. z.o.o. All rights reserved.
// Copyright (c) 2017-2021, Mudita Sp. z.o.o. All rights reserved.
// For licensing, see https://github.com/mudita/MuditaOS/LICENSE.md

#pragma once


@@ 18,18 18,18 @@ namespace audio
      private:
        struct EventStorage
        {
            Stream *stream      = nullptr;
            Stream::Event event = Stream::Event::NoEvent;
            AbstractStream *stream      = nullptr;
            AbstractStream::Event event = AbstractStream::Event::NoEvent;
        };

      public:
        using queueInfo                           = std::pair<QueueHandle_t, std::string>;
        using queuedEvent                         = std::pair<Stream *, Stream::Event>;
        using queuedEvent                         = std::pair<AbstractStream *, AbstractStream::Event>;
        static constexpr auto listenerElementSize = sizeof(EventStorage);

        StreamQueuedEventsListener(std::shared_ptr<cpp_freertos::Queue> eventsQueue);
        explicit StreamQueuedEventsListener(std::shared_ptr<cpp_freertos::Queue> eventsQueue);

        void onEvent(Stream *stream, Stream::Event event, Stream::EventSourceMode source);
        void onEvent(AbstractStream *stream, Stream::Event event) override;

        queuedEvent waitForEvent();
        queuedEvent getEvent();

M module-audio/Audio/decoder/DecoderWorker.cpp => module-audio/Audio/decoder/DecoderWorker.cpp +5 -4
@@ 1,16 1,17 @@
// Copyright (c) 2017-2020, Mudita Sp. z.o.o. All rights reserved.
// Copyright (c) 2017-2021, Mudita Sp. z.o.o. All rights reserved.
// For licensing, see https://github.com/mudita/MuditaOS/LICENSE.md

#include "DecoderWorker.hpp"
#include "Audio/decoder/Decoder.hpp"
#include <Audio/AbstractStream.hpp>
#include <Audio/decoder/Decoder.hpp>

audio::DecoderWorker::DecoderWorker(Stream *audioStreamOut,
audio::DecoderWorker::DecoderWorker(audio::AbstractStream *audioStreamOut,
                                    Decoder *decoder,
                                    EndOfFileCallback endOfFileCallback,
                                    ChannelMode mode)
    : sys::Worker(DecoderWorker::workerName, DecoderWorker::workerPriority, stackDepth), audioStreamOut(audioStreamOut),
      decoder(decoder), endOfFileCallback(endOfFileCallback),
      bufferSize(audioStreamOut->getBlockSize() / sizeof(BufferInternalType)), channelMode(mode)
      bufferSize(audioStreamOut->getInputTraits().blockSize / sizeof(BufferInternalType)), channelMode(mode)
{}

audio::DecoderWorker::~DecoderWorker()

M module-audio/Audio/decoder/DecoderWorker.hpp => module-audio/Audio/decoder/DecoderWorker.hpp +9 -5
@@ 1,9 1,10 @@
// Copyright (c) 2017-2020, Mudita Sp. z.o.o. All rights reserved.
// 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 "Audio/StreamQueuedEventsListener.hpp"
#include <Audio/StreamQueuedEventsListener.hpp>
#include <Audio/AbstractStream.hpp>

#include <Service/Worker.hpp>
#include <semaphore.hpp>


@@ 27,7 28,10 @@ namespace audio
            ForceStereo
        };

        DecoderWorker(Stream *audioStreamOut, Decoder *decoder, EndOfFileCallback endOfFileCallback, ChannelMode mode);
        DecoderWorker(AbstractStream *audioStreamOut,
                      Decoder *decoder,
                      EndOfFileCallback endOfFileCallback,
                      ChannelMode mode);
        ~DecoderWorker() override;

        virtual auto init(std::list<sys::WorkerQueueInfo> queues = std::list<sys::WorkerQueueInfo>()) -> bool override;


@@ 49,8 53,8 @@ namespace audio
        static constexpr auto listenerQueueName     = "DecoderWorkerQueue";
        static constexpr auto listenerQueueCapacity = 1024;

        Stream *audioStreamOut = nullptr;
        Decoder *decoder       = nullptr;
        AbstractStream *audioStreamOut = nullptr;
        Decoder *decoder               = nullptr;
        EndOfFileCallback endOfFileCallback;
        std::unique_ptr<StreamQueuedEventsListener> queueListener;
        bool playbackEnabled = false;

M module-audio/Audio/test/CMakeLists.txt => module-audio/Audio/test/CMakeLists.txt +2 -0
@@ 25,7 25,9 @@ add_gtest_executable(
    SRCS
        unittest.cpp
        unittest_format.cpp
        unittest_transcode.cpp
        TestEndpoint.cpp
        TestStream.cpp
    LIBS
        module-audio
)

A module-audio/Audio/test/MockStream.hpp => module-audio/Audio/test/MockStream.hpp +44 -0
@@ 0,0 1,44 @@
// 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 <Audio/AbstractStream.hpp>

#include <gmock/gmock.h>

namespace testing::audio
{

    class MockStream : public ::audio::AbstractStream
    {
      public:
        MOCK_METHOD(bool, push, (void *data, std::size_t dataSize), (override));
        MOCK_METHOD(bool, push, (const Span &span), (override));
        MOCK_METHOD(bool, push, (), (override));
        MOCK_METHOD(bool, pop, (Span & span), (override));
        MOCK_METHOD(bool, reserve, (Span & span), (override));
        MOCK_METHOD(void, commit, (), (override));
        MOCK_METHOD(void, release, (), (override));
        MOCK_METHOD(bool, peek, (Span & span), (override));
        MOCK_METHOD(void, consume, (), (override));
        MOCK_METHOD(void, unpeek, (), (override));
        MOCK_METHOD(void, reset, (), (override));
        MOCK_METHOD(void, registerListener, (EventListener * listener), (override));
        MOCK_METHOD(void, unregisterListeners, (EventListener * listener), (override));
        MOCK_METHOD(bool, isEmpty, (), (const, noexcept, override));
        MOCK_METHOD(bool, isFull, (), (const, noexcept, override));
        MOCK_METHOD(Traits, getInputTraits, (), (const, noexcept, override));
        MOCK_METHOD(Traits, getOutputTraits, (), (const, noexcept, override));
    };

    class MockStreamEventListener : public ::audio::AbstractStream::EventListener
    {
      public:
        MOCK_METHOD(void,
                    onEvent,
                    (::audio::AbstractStream * stream, ::audio::AbstractStream::Event event),
                    (override));
    };

} // namespace testing::audio

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

#include "TestStream.hpp"

#include <Audio/AbstractStream.hpp>

#include <memory>
#include <utility>

using ::audio::AbstractStream;
using testing::audio::TestStream;

TestStream::TestStream(std::size_t blockSize) : bufSize(blockSize)
{
    data = std::make_unique<std::uint8_t[]>(bufSize);
}

const AbstractStream::Span TestStream::getDataSpan() const
{
    return AbstractStream::Span{data.get(), bufSize};
}

bool TestStream::push(void *data, std::size_t dataSize)
{
    return true;
}

bool TestStream::push(const AbstractStream::Span &span)
{
    if (span.dataSize != bufSize) {
        return false;
    }

    for (std::size_t i = 0; i < span.dataSize; i++) {
        data.get()[i] = span.data[i];
    }

    return true;
}

bool TestStream::push()
{
    return true;
}

bool TestStream::pop(AbstractStream::Span &span)
{
    return true;
}

bool TestStream::reserve(AbstractStream::Span &span)
{
    return true;
}

void TestStream::commit()
{}

void TestStream::release()
{}

bool TestStream::peek(AbstractStream::Span &span)
{
    span = Span{.data = data.get(), .dataSize = bufSize};
    return true;
}

void TestStream::consume()
{}

void TestStream::unpeek()
{}

void TestStream::reset()
{}

void TestStream::setData(uint8_t value)
{
    for (std::size_t i = 0; i < bufSize; i++) {
        data.get()[i] = value;
    }
}

bool TestStream::checkData(const Span &span)
{
    if (span.dataSize != bufSize) {
        return false;
    }

    for (std::size_t i = 0; i < bufSize; i++) {
        if (data.get()[i] != span.data[i]) {
            return false;
        }
    }

    return true;
}

void TestStream::registerListener(EventListener *listener)
{}

void TestStream::unregisterListeners(EventListener *listener)
{}

auto TestStream::getInputTraits() const noexcept -> Traits
{
    return Traits{.blockSize = bufSize, .format = ::audio::nullFormat};
}

auto TestStream::getOutputTraits() const noexcept -> Traits
{
    return Traits{.blockSize = bufSize, .format = ::audio::nullFormat};
}

bool TestStream::isEmpty() const noexcept
{
    return false;
}

bool TestStream::isFull() const noexcept
{
    return false;
}

A module-audio/Audio/test/TestStream.hpp => module-audio/Audio/test/TestStream.hpp +57 -0
@@ 0,0 1,57 @@
// 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 <Audio/AbstractStream.hpp>

#include <memory>

namespace testing::audio
{

    class TestStream : public ::audio::AbstractStream
    {
      public:
        explicit TestStream(std::size_t blockSize);

        // listener registration
        void registerListener(EventListener *listener) override;
        void unregisterListeners(EventListener *listener) override;

        /// push
        bool push(void *data, std::size_t dataSize) override;
        bool push(const Span &span) override;
        bool push() override;

        /// pop
        bool pop(Span &span) override;

        /// zero copy write
        bool reserve(Span &span) override;
        void commit() override;
        void release() override;

        /// zero copy read
        bool peek(Span &span) override;
        void consume() override;
        void unpeek() override;

        void reset() override;

        [[nodiscard]] auto getInputTraits() const noexcept -> Traits override;
        [[nodiscard]] auto getOutputTraits() const noexcept -> Traits override;
        [[nodiscard]] bool isEmpty() const noexcept override;
        [[nodiscard]] bool isFull() const noexcept override;

        const Span getDataSpan() const;

        void setData(uint8_t value);
        bool checkData(const Span &span);

      private:
        std::unique_ptr<std::uint8_t[]> data;
        std::size_t bufSize;
    };

}; // namespace testing::audio

M module-audio/Audio/test/unittest_stream.cpp => module-audio/Audio/test/unittest_stream.cpp +212 -12
@@ 1,23 1,32 @@
// Copyright (c) 2017-2020, Mudita Sp. z.o.o. All rights reserved.
// Copyright (c) 2017-2021, Mudita Sp. z.o.o. All rights reserved.
// For licensing, see https://github.com/mudita/MuditaOS/LICENSE.md

#include <gtest/gtest.h>
#include <gmock/gmock.h>

#include <Audio/Stream.hpp>
#include <Audio/AudioFormat.hpp>
#include <Audio/StreamProxy.hpp>

#include "MockStream.hpp"

#include <cstdint>
#include <cstring>

using audio::NonCacheableStreamAllocator;
using audio::StandardStreamAllocator;
using audio::Stream;
using testing::Return;
using testing::audio::MockStream;
using testing::audio::MockStreamEventListener;

constexpr std::size_t defaultBlockSize = 64U;
constexpr std::size_t defaultBuffering = 4U;

using namespace audio;
constexpr audio::AudioFormat format    = audio::AudioFormat(44100, 16, 2);

static std::uint8_t testData[defaultBuffering][defaultBlockSize];
static std::uint8_t emptyBlock[defaultBlockSize];

#include <iostream>

static void initTestData()
{
    auto fillbuf = [](std::uint8_t *b, std::size_t s, unsigned step) {


@@ 52,17 61,68 @@ TEST(Stream, Init)
{
    StandardStreamAllocator a;
    constexpr auto bufferingSize = 2U;
    Stream s(a, defaultBlockSize, bufferingSize);
    Stream s(format, a, defaultBlockSize, bufferingSize);

    EXPECT_EQ(s.getBlockCount(), bufferingSize);
    EXPECT_EQ(s.getBlockSize(), defaultBlockSize);
    EXPECT_EQ(s.getInputTraits().blockSize, defaultBlockSize);
    EXPECT_EQ(s.getOutputTraits().blockSize, defaultBlockSize);
    EXPECT_EQ(s.getUsedBlockCount(), 0);
}

TEST(Stream, EmptyFull)
{
    StandardStreamAllocator a;
    constexpr auto bufferingSize = 2U;
    Stream s(format, a, defaultBlockSize, bufferingSize);

    EXPECT_TRUE(s.isEmpty());
    EXPECT_TRUE(s.blocksAvailable());
    EXPECT_FALSE(s.isFull());

    s.push();

    EXPECT_FALSE(s.isEmpty());
    EXPECT_TRUE(s.blocksAvailable());
    EXPECT_FALSE(s.isFull());

    s.push();

    EXPECT_FALSE(s.isEmpty());
    EXPECT_FALSE(s.blocksAvailable());
    EXPECT_TRUE(s.isFull());
}

TEST(Stream, Allocator)
{
    NonCacheableStreamAllocator a;
    EXPECT_NO_THROW(Stream(format, a, defaultBlockSize));
}

TEST(Stream, Listeners)
{
    testing::audio::MockStreamEventListener listener;
    StandardStreamAllocator a;
    Stream s(format, a, defaultBlockSize);
    Stream::Span span;

    s.registerListener(&listener);

    // does not take effect
    s.unregisterListeners(nullptr);

    EXPECT_CALL(listener, onEvent(&s, ::audio::AbstractStream::Event::StreamUnderFlow)).Times(1);
    s.peek(span);

    s.unregisterListeners(&listener);

    // no second onEvent call - listener is unregistered
    s.peek(span);
}

TEST(Stream, Push)
{
    StandardStreamAllocator a;
    Stream s(a, defaultBlockSize);
    Stream s(format, a, defaultBlockSize);
    auto block = testData[0];

    EXPECT_TRUE(s.push(block, defaultBlockSize));


@@ 73,10 133,36 @@ TEST(Stream, Push)
    EXPECT_FALSE(s.push(block, defaultBlockSize));
}

TEST(Stream, InvalidPushPop)
{
    StandardStreamAllocator a;
    std::uint8_t buf[defaultBlockSize];
    Stream s(format, a, defaultBlockSize);
    Stream::Span span{.data = buf, .dataSize = 63};

    EXPECT_FALSE(s.push(span));
    span.dataSize = defaultBlockSize;

    // push and peek null data
    EXPECT_TRUE(s.push());
    EXPECT_TRUE(s.peek(span));
    memset(buf, 0, sizeof(buf));

    ASSERT_EQ(span.dataSize, defaultBlockSize);
    EXPECT_EQ(memcmp(span.data, buf, span.dataSize), 0);

    // peek in progress - push is not allowed
    EXPECT_FALSE(s.pop(span));

    // push is not allowed when using zero copy write
    EXPECT_TRUE(s.reserve(span));
    EXPECT_FALSE(s.push(span));
}

TEST(Stream, PushPop)
{
    StandardStreamAllocator a;
    Stream s(a, defaultBlockSize);
    Stream s(format, a, defaultBlockSize);

    initTestData();



@@ 122,7 208,7 @@ TEST(Stream, PushPop)
TEST(Stream, Peek)
{
    StandardStreamAllocator a;
    Stream s(a, defaultBlockSize);
    Stream s(format, a, defaultBlockSize);

    initTestData();



@@ 157,7 243,7 @@ TEST(Stream, Peek)
TEST(Stream, GreedyPeek)
{
    StandardStreamAllocator a;
    Stream s(a, defaultBlockSize);
    Stream s(format, a, defaultBlockSize);
    Stream::Span span;

    initTestData();


@@ 182,7 268,7 @@ TEST(Stream, GreedyPeek)
TEST(Stream, Reserve)
{
    StandardStreamAllocator a;
    Stream s(a, defaultBlockSize);
    Stream s(format, a, defaultBlockSize);
    Stream::Span span;

    EXPECT_EQ(s.getReservedCount(), 0);


@@ 206,6 292,120 @@ TEST(Stream, Reserve)
    EXPECT_EQ(s.getUsedBlockCount(), 1);
}

TEST(Stream, Iterator)
{
    std::uint8_t buf[defaultBlockSize * 2];

    Stream::RawBlockIterator it(buf, sizeof(buf), buf, 64);

    it++;
    auto data = *it;
    EXPECT_EQ(data.data, &buf[defaultBlockSize]);
    EXPECT_EQ(data.dataSize, defaultBlockSize);

    it++;
    data = *it;
    EXPECT_EQ(data.data, buf);
    EXPECT_EQ(data.dataSize, defaultBlockSize);

    it--;
    data = *it;
    EXPECT_EQ(data.data, &buf[defaultBlockSize]);
    EXPECT_EQ(data.dataSize, defaultBlockSize);

    it--;
    data = *it;
    EXPECT_EQ(data.data, buf);
    EXPECT_EQ(data.dataSize, defaultBlockSize);
}

TEST(Stream, Reset)
{
    StandardStreamAllocator a;
    Stream s(format, a, defaultBlockSize);

    s.push();
    ASSERT_FALSE(s.isEmpty());

    s.reset();
    EXPECT_TRUE(s.isEmpty());
}

TEST(Stream, spanEquality)
{
    std::uint8_t buf[defaultBlockSize];
    auto span  = Stream::Span{.data = buf, .dataSize = defaultBlockSize};
    auto span2 = span;
    EXPECT_TRUE(span == span2);
    EXPECT_FALSE(span != span2);

    span2.data = &buf[1];
    EXPECT_FALSE(span == span2);
    EXPECT_TRUE(span != span2);

    span2          = span;
    span2.dataSize = defaultBlockSize - 1;
    EXPECT_FALSE(span == span2);
    EXPECT_TRUE(span != span2);
}

TEST(Proxy, Write)
{
    MockStream mock;
    auto proxy = ::audio::StreamProxy(mock);
    ::audio::AbstractStream::Span span{.data = reinterpret_cast<uint8_t *>(0xdeadbeef), .dataSize = 0xc00f33b4d};

    EXPECT_CALL(mock, push(span)).Times(1).WillOnce(Return(true));
    EXPECT_CALL(mock, push(nullptr, 128)).WillOnce(Return(false));
    EXPECT_CALL(mock, push()).WillOnce(Return(true));

    EXPECT_TRUE(proxy.push(span));
    EXPECT_FALSE(proxy.push(nullptr, 128));
    EXPECT_TRUE(proxy.push());

    EXPECT_CALL(mock, reserve).Times(1);
    EXPECT_CALL(mock, commit).Times(1);
    EXPECT_CALL(mock, release).Times(1);

    proxy.reserve(span);
    proxy.commit();
    proxy.release();
}

TEST(Proxy, Read)
{
    MockStream mock;
    auto proxy = ::audio::StreamProxy(mock);
    ::audio::AbstractStream::Span span{.data = reinterpret_cast<uint8_t *>(0xdeadbeef), .dataSize = 0xc00f33b4d};

    EXPECT_CALL(mock, pop(span)).Times(1).WillOnce(Return(true));
    EXPECT_TRUE(proxy.pop(span));

    EXPECT_CALL(mock, peek(span)).Times(2).WillOnce(Return(true)).WillOnce(Return(false));
    EXPECT_CALL(mock, consume());
    EXPECT_CALL(mock, unpeek());

    EXPECT_TRUE(proxy.peek(span));
    EXPECT_FALSE(proxy.peek(span));

    proxy.consume();
    proxy.unpeek();
}

TEST(Proxy, Misc)
{
    MockStream mock;
    auto proxy = ::audio::StreamProxy(mock);

    EXPECT_CALL(mock, reset).Times(1);
    EXPECT_CALL(mock, isEmpty).Times(1).WillOnce(Return(true));
    EXPECT_CALL(mock, isFull).Times(1).WillOnce(Return(false));

    proxy.reset();
    EXPECT_TRUE(proxy.isEmpty());
    EXPECT_FALSE(proxy.isFull());
}

int main(int argc, char **argv)
{
    ::testing::InitGoogleTest(&argc, argv);

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

#include <gtest/gtest.h>
#include <gmock/gmock.h>

#include "TestStream.hpp"
#include "MockStream.hpp"

#include <Audio/AbstractStream.hpp>
#include <Audio/AudioFormat.hpp>
#include <Audio/transcode/InputTranscodeProxy.hpp>
#include <Audio/transcode/Transform.hpp>
#include <Audio/transcode/MonoToStereo.hpp>
#include <Audio/transcode/TransformComposite.hpp>

#include <cstdlib>

using ::testing::Return;
using ::testing::audio::MockStream;
using ::testing::audio::MockStreamEventListener;

constexpr std::size_t testStreamSize = 8;

class InverseTransform : public audio::transcode::Transform
{
  public:
    auto transform(const Span &span, const Span &transformSpace) const -> Span override
    {
        for (std::size_t i = 0; i < span.dataSize; i++) {
            span.data[i] = ~span.data[i];
        }
        return span;
    }

    auto transformBlockSize(std::size_t inputBufferSize) const noexcept -> std::size_t override
    {
        return inputBufferSize;
    }

    auto validateInputFormat(const audio::AudioFormat &inputFormat) const noexcept -> bool override
    {
        return true;
    }

    auto transformFormat(const audio::AudioFormat &inputFormat) const noexcept -> audio::AudioFormat override
    {
        return inputFormat;
    }
};

class NullTransform : public audio::transcode::Transform
{
  public:
    auto transform(const Span &span, const Span &transformSpace) const -> Span override
    {
        return span;
    }

    auto validateInputFormat(const audio::AudioFormat &inputFormat) const noexcept -> bool override
    {
        return true;
    }

    auto transformFormat(const audio::AudioFormat &inputFormat) const noexcept -> audio::AudioFormat override
    {
        return inputFormat;
    }

    auto transformBlockSize(std::size_t inputBufferSize) const noexcept -> std::size_t override
    {
        return inputBufferSize;
    }
};

TEST(Transcode, Reset)
{
    MockStream mock;
    NullTransform nullTransform;

    EXPECT_CALL(mock, getInputTraits);
    audio::transcode::InputTranscodeProxy proxy(mock, nullTransform);

    EXPECT_CALL(mock, reset()).Times(1);

    proxy.reset();
}

TEST(Transcode, Push)
{
    static std::uint8_t testData[testStreamSize]     = {0, 1, 2, 3, 4, 5, 6, 7};
    static std::uint8_t invertedData[testStreamSize] = {255, 254, 253, 252, 251, 250, 249, 248};
    InverseTransform inv;
    ::testing::audio::TestStream stream(testStreamSize);
    ::audio::transcode::InputTranscodeProxy transform(stream, inv);

    transform.push(::audio::AbstractStream::Span{testData, testStreamSize});
    EXPECT_TRUE(stream.checkData(::audio::AbstractStream::Span{invertedData, testStreamSize}));
}

TEST(Transcode, FailedPeek)
{
    NullTransform nullTransform;
    MockStream mockStream;

    EXPECT_CALL(mockStream, getInputTraits);
    ::audio::transcode::InputTranscodeProxy transform(mockStream, nullTransform);
    ::audio::AbstractStream::Span dataSpan;

    EXPECT_CALL(mockStream, peek).Times(1).WillOnce(Return(false));
    EXPECT_EQ(transform.peek(dataSpan), false);
}

TEST(Transcode, Commit)
{
    static std::uint8_t invertedData[testStreamSize] = {255, 254, 253, 252, 251, 250, 249, 248};
    InverseTransform inv;
    ::testing::audio::TestStream stream(testStreamSize);
    ::audio::transcode::InputTranscodeProxy transform(stream, inv);
    ::audio::AbstractStream::Span span;

    stream.setData(0);

    ASSERT_TRUE(transform.peek(span));
    ASSERT_TRUE(span.data != nullptr);
    ASSERT_TRUE(span.dataSize == testStreamSize);

    for (std::size_t i = 0; i < span.dataSize; ++i) {
        span.data[i] = i;
    }

    transform.commit();
    EXPECT_TRUE(stream.checkData(::audio::AbstractStream::Span{invertedData, testStreamSize}));

    // no conversion on commit with no peek
    stream.commit();
    EXPECT_TRUE(stream.checkData(::audio::AbstractStream::Span{invertedData, testStreamSize}));
}

TEST(Transcode, Traits)
{
    auto testFormat = ::audio::AudioFormat(44100, 16, 1);
    ::audio::transcode::MonoToStereo m2s;
    MockStream mockStream;

    EXPECT_CALL(mockStream, getInputTraits)
        .Times(1)
        .WillOnce(Return(::audio::AbstractStream::Traits{.blockSize = 128, .format = testFormat}));

    ::audio::transcode::InputTranscodeProxy proxy(mockStream, m2s);

    EXPECT_CALL(mockStream, getInputTraits)
        .Times(1)
        .WillOnce(Return(::audio::AbstractStream::Traits{.blockSize = 128, .format = testFormat}));

    EXPECT_CALL(mockStream, getOutputTraits)
        .Times(1)
        .WillOnce(Return(::audio::AbstractStream::Traits{.blockSize = 128, .format = testFormat}));

    auto proxyInputTraits = proxy.getInputTraits();

    EXPECT_EQ(proxyInputTraits.blockSize, 256);
    EXPECT_EQ(proxyInputTraits.format.getChannels(), 2);
    EXPECT_EQ(proxyInputTraits.format.getSampleRate(), 44100);
    EXPECT_EQ(proxyInputTraits.format.getBitWidth(), 16);

    auto proxyOutputTraits = proxy.getOutputTraits();

    EXPECT_EQ(proxyOutputTraits.blockSize, 128);
    EXPECT_EQ(proxyOutputTraits.format.getChannels(), 1);
    EXPECT_EQ(proxyOutputTraits.format.getSampleRate(), 44100);
    EXPECT_EQ(proxyOutputTraits.format.getBitWidth(), 16);
}

TEST(Transcode, ListenersWrap)
{
    MockStream mock;
    MockStreamEventListener listener;
    NullTransform nullTransform;

    EXPECT_CALL(mock, getInputTraits);
    audio::transcode::InputTranscodeProxy proxy(mock, nullTransform);

    EXPECT_CALL(mock, registerListener(&listener)).Times(1);

    proxy.registerListener(&listener);

    EXPECT_CALL(mock, unregisterListeners(&listener)).Times(1);

    proxy.unregisterListeners(&listener);
}

TEST(Transform, MonoToStereo)
{
    audio::transcode::MonoToStereo m2s;
    static std::uint16_t inputBuffer[8] = {0, 1, 2, 3, 4, 5, 6, 7};
    static std::uint16_t outputBuffer[16];
    auto input  = ::audio::AbstractStream::Span{.data     = reinterpret_cast<std::uint8_t *>(&inputBuffer[0]),
                                               .dataSize = sizeof(inputBuffer)};
    auto output = ::audio::AbstractStream::Span{.data     = reinterpret_cast<std::uint8_t *>(&outputBuffer[0]),
                                                .dataSize = sizeof(outputBuffer)};

    ASSERT_EQ(input.dataSize, 16);
    ASSERT_EQ(output.dataSize, 32);

    EXPECT_TRUE(m2s.validateInputFormat(audio::AudioFormat(44100, 16, 1)));
    EXPECT_FALSE(m2s.validateInputFormat(audio::AudioFormat(44100, 16, 0)));
    EXPECT_FALSE(m2s.validateInputFormat(audio::AudioFormat(44100, 16, 2)));

    auto outputFormat = m2s.transformFormat(audio::AudioFormat(44100, 16, 1));
    EXPECT_EQ(outputFormat, audio::AudioFormat(44100, 16, 2));

    EXPECT_EQ(m2s.getTransformSize(4), 8);
    EXPECT_EQ(m2s.getTransformSize(128), 256);
    EXPECT_EQ(m2s.getTransformSize(0), 0);

    static std::uint16_t expectedResult[16] = {0, 0, 1, 1, 2, 2, 3, 3, 4, 4, 5, 5, 6, 6, 7, 7};
    ASSERT_EQ(m2s.transform(input, output), output);
    ASSERT_EQ(memcmp(output.data, expectedResult, 32), 0);
}

TEST(Transform, Composite)
{
    audio::transcode::MonoToStereo m2s;
    InverseTransform inv;
    audio::transcode::TransformComposite composite{&inv, &m2s};
    static std::uint16_t inputBuffer[8] = {0, 1, 2, 3, 4, 5, 6, 7};
    static std::uint16_t outputBuffer[16];
    static std::uint16_t expectedResult[] = {
        0xFFFF,
        0xFFFF,
        0xFFFE,
        0xFFFE,
        0xFFFD,
        0xFFFD,
        0xFFFC,
        0xFFFC,
        0xFFFB,
        0xFFFB,
        0xFFFA,
        0xFFFA,
        0xFFF9,
        0xFFF9,
        0xFFF8,
        0xFFF8,
    };

    auto input  = ::audio::AbstractStream::Span{.data     = reinterpret_cast<std::uint8_t *>(&inputBuffer[0]),
                                               .dataSize = sizeof(inputBuffer)};
    auto output = ::audio::AbstractStream::Span{.data     = reinterpret_cast<std::uint8_t *>(&outputBuffer[0]),
                                                .dataSize = sizeof(outputBuffer)};

    ASSERT_EQ(output.dataSize, composite.getTransformSize(sizeof(inputBuffer)));
    auto result = composite.transform(input, output);
    ASSERT_EQ(result.dataSize, sizeof(expectedResult));
    ASSERT_EQ(memcmp(result.data, expectedResult, sizeof(expectedResult)), 0);

    EXPECT_TRUE(composite.validateInputFormat(audio::AudioFormat(44100, 16, 1)));
    EXPECT_FALSE(composite.validateInputFormat(audio::AudioFormat(44100, 16, 0)));
    EXPECT_FALSE(composite.validateInputFormat(audio::AudioFormat(44100, 16, 2)));

    auto outputFormat = composite.transformFormat(audio::AudioFormat(44100, 16, 1));
    EXPECT_EQ(outputFormat, audio::AudioFormat(44100, 16, 2));
    auto outputBlockSize = composite.transformBlockSize(sizeof(inputBuffer));
    EXPECT_EQ(outputBlockSize, 2 * sizeof(inputBuffer));
}

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

#include "InputTranscodeProxy.hpp"
#include "Transform.hpp"

#include <utility>

using audio::transcode::InputTranscodeProxy;

InputTranscodeProxy::InputTranscodeProxy(AbstractStream &wrappedStream, Transform &transform) noexcept
    : StreamProxy(wrappedStream), transform(transform),
      transcodingSpaceSize(transform.getTransformSize(wrappedStream.getInputTraits().blockSize)),
      transcodingSpace(std::make_unique<std::uint8_t[]>(transcodingSpaceSize)), transcodingSpaceSpan{
                                                                                    .data     = transcodingSpace.get(),
                                                                                    .dataSize = transcodingSpaceSize}
{}

bool InputTranscodeProxy::push(const Span &span)
{
    return getWrappedStream().push(transform.transform(span, transcodingSpaceSpan));
}

void InputTranscodeProxy::commit()
{
    transform.transform(peekedSpan, transcodingSpaceSpan);
    getWrappedStream().commit();
    peekedSpan.reset();
}

bool InputTranscodeProxy::peek(Span &span)
{
    auto result = getWrappedStream().peek(span);

    if (result) {
        peekedSpan = span;
    }

    return result;
}

auto InputTranscodeProxy::getInputTraits() const noexcept -> Traits
{
    auto originalTraits      = StreamProxy::getInputTraits();
    auto transcodedFormat    = transform.transformFormat(originalTraits.format);
    auto transcodedBlockSize = transform.transformBlockSize(originalTraits.blockSize);

    return Traits{.blockSize = transcodedBlockSize, .format = transcodedFormat};
}

A module-audio/Audio/transcode/InputTranscodeProxy.hpp => module-audio/Audio/transcode/InputTranscodeProxy.hpp +41 -0
@@ 0,0 1,41 @@
// 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 <Audio/StreamProxy.hpp>

#include "Transform.hpp"

#include <memory>

namespace audio::transcode
{
    /**
     * @brief An input transcoding proxy for an AbstractStream.
     */
    class InputTranscodeProxy : public audio::StreamProxy
    {
      public:
        /**
         * @brief Construct a new Input Transcode Proxy object
         *
         * @param wrappedStream
         * @param transform to apply on the input
         */
        explicit InputTranscodeProxy(AbstractStream &wrappedStream, Transform &transform) noexcept;

        bool push(const Span &span) override;
        void commit() override;
        bool peek(Span &span) override;
        [[nodiscard]] auto getInputTraits() const noexcept -> Traits override;

      private:
        Transform &transform;
        Span peekedSpan;
        std::size_t transcodingSpaceSize;
        std::unique_ptr<std::uint8_t[]> transcodingSpace;
        Span transcodingSpaceSpan;
    };

}; // namespace audio::transcode

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

#include "MonoToStereo.hpp"

#include <Audio/AudioFormat.hpp>

using audio::transcode::MonoToStereo;

auto MonoToStereo::transform(const Span &span, const Span &transformSpace) const -> Span
{
    auto outputSpan   = Span{.data = transformSpace.data, .dataSize = getTransformSize(span.dataSize)};
    auto outputBuffer = reinterpret_cast<std::uint16_t *>(transformSpace.data);
    auto inputBuffer  = reinterpret_cast<std::uint16_t *>(span.data);

    for (std::size_t i = span.dataSize / sizeof(std::uint16_t); i > 0; i--) {
        outputBuffer[i * 2 - 1] = outputBuffer[i * 2 - 2] = inputBuffer[i - 1];
    }

    return outputSpan;
}

auto MonoToStereo::transformBlockSize(std::size_t inputBufferSize) const noexcept -> std::size_t
{
    return 2 * inputBufferSize;
}

auto MonoToStereo::validateInputFormat(const audio::AudioFormat &inputFormat) const noexcept -> bool
{
    return inputFormat.getChannels() == 1;
}

auto MonoToStereo::transformFormat(const audio::AudioFormat &inputFormat) const noexcept -> audio::AudioFormat
{
    return audio::AudioFormat(inputFormat.getSampleRate(), inputFormat.getBitWidth(), 2);
}

A module-audio/Audio/transcode/MonoToStereo.hpp => module-audio/Audio/transcode/MonoToStereo.hpp +22 -0
@@ 0,0 1,22 @@
// 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"

namespace audio::transcode
{
    /**
     * @brief Mono to stereo PCM16 data transform
     */
    class MonoToStereo : public Transform
    {
      public:
        auto transform(const Span &span, const Span &transformSpace) const -> Span override;
        auto validateInputFormat(const audio::AudioFormat &inputFormat) const noexcept -> bool override;
        auto transformFormat(const audio::AudioFormat &inputFormat) const noexcept -> audio::AudioFormat override;
        auto transformBlockSize(std::size_t blockSize) const noexcept -> std::size_t override;
    };

} // namespace audio::transcode

A module-audio/Audio/transcode/Transform.hpp => module-audio/Audio/transcode/Transform.hpp +90 -0
@@ 0,0 1,90 @@
// 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 <Audio/AudioFormat.hpp>
#include <Audio/AbstractStream.hpp>

namespace audio::transcode
{

    /**
     * @brief Data transform which can be applied on a data range.
     */
    class Transform
    {
      public:
        using Span = audio::AbstractStream::Span;

        virtual ~Transform() = default;

        /**
         * @brief Transforms data within range
         *
         * @param inputSpan - input data to transform
         * @param transformSpace - overall space available to the transform
         * @return Output data (contained within )
         */
        virtual auto transform(const Span &inputSpan, const Span &transformSpace) const -> Span = 0;

        /**
         * @brief Checks if audio format is suitable for transformation.
         *
         * @param inputFormat - format to be checked
         * @return true if inputFormat is suitable for transformation, false otherwise
         */
        virtual auto validateInputFormat(const audio::AudioFormat &inputFormat) const noexcept -> bool = 0;

        /**
         * @brief Calculates the audio format on transform output based on the
         * input format.
         *
         * @param inputFormat - format of the data on input
         * @return format of the data on output.
         */
        virtual auto transformFormat(const audio::AudioFormat &inputFormat) const noexcept -> audio::AudioFormat = 0;

        /**
         * @brief Calculates the size of an output data after the transform.
         *
         * @param blockSize - size of an input block
         * @return size of an output block
         */
        virtual auto transformBlockSize(std::size_t blockSize) const noexcept -> std::size_t = 0;

        /**
         * @brief Get the overall space size required for transformation. Some transforms
         * may need to use more space than the size of an output data, e.g. resampling
         * where there is a signal interpolation before the final decimation.
         *
         * @param inputBufferSize
         * @return std::size_t
         */
        virtual auto getTransformSize(std::size_t inputBufferSize) const noexcept -> std::size_t
        {
            return transformBlockSize(inputBufferSize);
        }

        /**
         * @brief A convenience transform operator.
         */
        inline auto operator()(const Span &span, const Span &transformSpace)
        {
            return transform(span, transformSpace);
        }

        /**
         * @brief A convenience wrapper for transforms which has transcoding size
         * same as output size and a transcoding operation is performed in-place.
         *
         * @param span to transform
         * @return transformed span
         */
        inline auto transform(const Span &span)
        {
            return transform(span, span);
        }
    };

}; // namespace audio::transcode

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

#include "TransformComposite.hpp"
#include "Transform.hpp"

#include <Audio/AudioFormat.hpp>

#include <algorithm>
#include <initializer_list>

using audio::transcode::Transform;
using audio::transcode::TransformComposite;

TransformComposite::TransformComposite(std::initializer_list<Transform *> transforms) : children(transforms)
{}

auto TransformComposite::transform(const Span &input, const Span &conversionSpace) const -> Span
{
    auto output = input;

    for (auto t : children) {
        output = t->transform(output, conversionSpace);
    }
    return output;
}

auto TransformComposite::getTransformSize(std::size_t inputBufferSize) const noexcept -> std::size_t
{
    auto nextBlockSize  = inputBufferSize;
    auto inputBlockSize = inputBufferSize;

    for (auto t : children) {
        nextBlockSize  = t->getTransformSize(nextBlockSize);
        inputBlockSize = std::max(nextBlockSize, inputBlockSize);
    }

    return inputBlockSize;
}

auto TransformComposite::validateInputFormat(const audio::AudioFormat &inputFormat) const noexcept -> bool
{
    auto checkFormat = inputFormat;

    for (auto t : children) {
        if (!t->validateInputFormat(checkFormat)) {
            return false;
        }
        checkFormat = t->transformFormat(checkFormat);
    }

    return true;
}

auto TransformComposite::transformFormat(const audio::AudioFormat &inputFormat) const noexcept -> audio::AudioFormat
{
    auto outputFormat = inputFormat;

    for (auto t : children) {
        outputFormat = t->transformFormat(outputFormat);
    }

    return outputFormat;
}

auto TransformComposite::transformBlockSize(std::size_t blockSize) const noexcept -> std::size_t
{
    std::size_t transformedBlockSize = blockSize;

    for (auto t : children) {
        transformedBlockSize = t->transformBlockSize(transformedBlockSize);
    }

    return transformedBlockSize;
}

A module-audio/Audio/transcode/TransformComposite.hpp => module-audio/Audio/transcode/TransformComposite.hpp +39 -0
@@ 0,0 1,39 @@
// 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 <Audio/AudioFormat.hpp>

#include <initializer_list>
#include <vector>

namespace audio::transcode
{

    /**
     * @brief Allows to execute a set of transforms to be performed in a chain.
     */
    class TransformComposite : public Transform
    {
      public:
        /**
         * @brief Construct a new Transform Composite object
         *
         * @param children - transforms to be executed on each transform call.
         */
        TransformComposite(std::initializer_list<Transform *> children);

        auto transform(const Span &span, const Span &transformSpace) const -> Span override;
        auto getTransformSize(std::size_t inputBufferSize) const noexcept -> std::size_t override;
        auto validateInputFormat(const audio::AudioFormat &inputFormat) const noexcept -> bool override;
        auto transformFormat(const audio::AudioFormat &inputFormat) const noexcept -> audio::AudioFormat override;
        auto transformBlockSize(std::size_t blockSize) const noexcept -> std::size_t override;

      private:
        std::vector<Transform *> children;
    };

} // namespace audio::transcode

M module-audio/CMakeLists.txt => module-audio/CMakeLists.txt +37 -30
@@ 8,40 8,45 @@ project(module-audio VERSION 1.0

module_is_test_entity()

set(SOURCES
        "${CMAKE_CURRENT_SOURCE_DIR}/Audio/Audio.cpp"
        "${CMAKE_CURRENT_SOURCE_DIR}/Audio/AudioCommon.cpp"
        "${CMAKE_CURRENT_SOURCE_DIR}/Audio/AudioDeviceFactory.cpp"
        "${CMAKE_CURRENT_SOURCE_DIR}/Audio/AudioFormat.cpp"
        "${CMAKE_CURRENT_SOURCE_DIR}/Audio/AudioMux.cpp"
        "${CMAKE_CURRENT_SOURCE_DIR}/Audio/VolumeScaler.cpp"
        "${CMAKE_CURRENT_SOURCE_DIR}/Audio/decoder/Decoder.cpp"
        "${CMAKE_CURRENT_SOURCE_DIR}/Audio/decoder/decoderFLAC.cpp"
        "${CMAKE_CURRENT_SOURCE_DIR}/Audio/decoder/decoderMP3.cpp"
        "${CMAKE_CURRENT_SOURCE_DIR}/Audio/decoder/decoderWAV.cpp"
        "${CMAKE_CURRENT_SOURCE_DIR}/Audio/decoder/DecoderWorker.cpp"
        "${CMAKE_CURRENT_SOURCE_DIR}/Audio/decoder/xing_header.c"
        "${CMAKE_CURRENT_SOURCE_DIR}/Audio/encoder/Encoder.cpp"
        "${CMAKE_CURRENT_SOURCE_DIR}/Audio/encoder/EncoderWAV.cpp"
        "${CMAKE_CURRENT_SOURCE_DIR}/Audio/Endpoint.cpp"
        "${CMAKE_CURRENT_SOURCE_DIR}/Audio/Operation/IdleOperation.cpp"
        "${CMAKE_CURRENT_SOURCE_DIR}/Audio/Operation/Operation.cpp"
        "${CMAKE_CURRENT_SOURCE_DIR}/Audio/Operation/PlaybackOperation.cpp"
        "${CMAKE_CURRENT_SOURCE_DIR}/Audio/Operation/RecorderOperation.cpp"
        "${CMAKE_CURRENT_SOURCE_DIR}/Audio/Operation/RouterOperation.cpp"
        "${CMAKE_CURRENT_SOURCE_DIR}/Audio/Profiles/Profile.cpp"
        "${CMAKE_CURRENT_SOURCE_DIR}/Audio/ServiceObserver.cpp"
        "${CMAKE_CURRENT_SOURCE_DIR}/Audio/Stream.cpp"
        "${CMAKE_CURRENT_SOURCE_DIR}/Audio/StreamFactory.cpp"
        "${CMAKE_CURRENT_SOURCE_DIR}/Audio/StreamQueuedEventsListener.cpp"
)

# include target specific rules
set(AUDIO_BOARD_LIBRARY audio-${PROJECT_TARGET_NAME})
add_subdirectory(board/${PROJECT_TARGET_NAME})
target_include_directories(${AUDIO_BOARD_LIBRARY} PRIVATE ${CMAKE_CURRENT_SOURCE_DIR})

add_library(${PROJECT_NAME} STATIC ${SOURCES})
add_library(${PROJECT_NAME} STATIC)
target_sources(
                ${PROJECT_NAME}
        PRIVATE
                ${CMAKE_CURRENT_SOURCE_DIR}/Audio/Audio.cpp
                ${CMAKE_CURRENT_SOURCE_DIR}/Audio/AudioCommon.cpp
                ${CMAKE_CURRENT_SOURCE_DIR}/Audio/AudioDeviceFactory.cpp
                ${CMAKE_CURRENT_SOURCE_DIR}/Audio/AudioFormat.cpp
                ${CMAKE_CURRENT_SOURCE_DIR}/Audio/AudioMux.cpp
                ${CMAKE_CURRENT_SOURCE_DIR}/Audio/decoder/Decoder.cpp
                ${CMAKE_CURRENT_SOURCE_DIR}/Audio/decoder/decoderFLAC.cpp
                ${CMAKE_CURRENT_SOURCE_DIR}/Audio/decoder/decoderMP3.cpp
                ${CMAKE_CURRENT_SOURCE_DIR}/Audio/decoder/decoderWAV.cpp
                ${CMAKE_CURRENT_SOURCE_DIR}/Audio/decoder/DecoderWorker.cpp
                ${CMAKE_CURRENT_SOURCE_DIR}/Audio/decoder/xing_header.c
                ${CMAKE_CURRENT_SOURCE_DIR}/Audio/encoder/Encoder.cpp
                ${CMAKE_CURRENT_SOURCE_DIR}/Audio/encoder/EncoderWAV.cpp
                ${CMAKE_CURRENT_SOURCE_DIR}/Audio/Endpoint.cpp
                ${CMAKE_CURRENT_SOURCE_DIR}/Audio/Operation/IdleOperation.cpp
                ${CMAKE_CURRENT_SOURCE_DIR}/Audio/Operation/Operation.cpp
                ${CMAKE_CURRENT_SOURCE_DIR}/Audio/Operation/PlaybackOperation.cpp
                ${CMAKE_CURRENT_SOURCE_DIR}/Audio/Operation/RecorderOperation.cpp
                ${CMAKE_CURRENT_SOURCE_DIR}/Audio/Operation/RouterOperation.cpp
                ${CMAKE_CURRENT_SOURCE_DIR}/Audio/Profiles/Profile.cpp
                ${CMAKE_CURRENT_SOURCE_DIR}/Audio/ServiceObserver.cpp
                ${CMAKE_CURRENT_SOURCE_DIR}/Audio/Stream.cpp
                ${CMAKE_CURRENT_SOURCE_DIR}/Audio/StreamFactory.cpp
                ${CMAKE_CURRENT_SOURCE_DIR}/Audio/StreamProxy.cpp
                ${CMAKE_CURRENT_SOURCE_DIR}/Audio/StreamQueuedEventsListener.cpp
                ${CMAKE_CURRENT_SOURCE_DIR}/Audio/transcode/InputTranscodeProxy.cpp
                ${CMAKE_CURRENT_SOURCE_DIR}/Audio/transcode/MonoToStereo.cpp
                ${CMAKE_CURRENT_SOURCE_DIR}/Audio/transcode/TransformComposite.cpp
                ${CMAKE_CURRENT_SOURCE_DIR}/Audio/VolumeScaler.cpp
)
target_include_directories(${PROJECT_NAME} PRIVATE ${TAGLIB_INCLUDE_DIRS})
target_compile_definitions(${PROJECT_NAME} PUBLIC ${PROJECT_CONFIG_DEFINITIONS})
target_compile_definitions(${PROJECT_NAME} PUBLIC ${PROJECT_TARGET})


@@ 51,9 56,11 @@ target_compile_features(${PROJECT_NAME} PUBLIC ${TARGET_COMPILE_FEATURES})
target_link_options(${PROJECT_NAME} PUBLIC ${TARGET_LINK_OPTIONS})
target_include_directories(${PROJECT_NAME} INTERFACE ${CMAKE_CURRENT_SOURCE_DIR})

target_compile_options(${PROJECT_NAME} PRIVATE -O0 -g)

# supress warning for flac decoder
set_source_files_properties(
        "${CMAKE_CURRENT_SOURCE_DIR}/Audio/decoder/decoderFLAC.cpp"
        ${CMAKE_CURRENT_SOURCE_DIR}/Audio/decoder/decoderFLAC.cpp
        PROPERTIES COMPILE_FLAGS
	"-Wno-implicit-fallthrough -Wno-error=maybe-uninitialized"
)

M module-audio/board/rt1051/SAIAudioDevice.cpp => module-audio/board/rt1051/SAIAudioDevice.cpp +6 -2
@@ 3,6 3,8 @@

#include "SAIAudioDevice.hpp"

#include <Audio/Stream.hpp>

using namespace audio;

SAIAudioDevice::SAIAudioDevice(I2S_Type *base, sai_edma_handle_t *rxHandle, sai_edma_handle_t *txHandle)


@@ 20,8 22,10 @@ void SAIAudioDevice::initiateRxTransfer()

void SAIAudioDevice::initiateTxTransfer()
{
    auto nullSpan = Sink::_stream->getNullSpan();
    auto xfer     = sai_transfer_t{.data = nullSpan.data, .dataSize = nullSpan.dataSize};
    audio::Stream::Span dataToSend;
    Sink::_stream->peek(dataToSend);

    auto xfer = sai_transfer_t{.data = dataToSend.data, .dataSize = dataToSend.dataSize};
    SAI_TransferSendEDMA(_base, tx, &xfer);
}