~aleteoryx/muditaos

9a8ffff654596f42666fdcac137a43319b20b471 — Lefucjusz 1 year, 10 months ago da57acd
[BH-1863] Fix deleted file popup showing in Relaxation

* Fix of the issue that 'File has been
deleted' popup would show in Relaxation
app at the end of playback if the
playback was paused at least once,
even though the file wasn't actually
deleted.
* Added very basic audio decoder error
handling and propagation mechanism.
* Minor refactor around several
audio-related parts.
46 files changed, 515 insertions(+), 395 deletions(-)

M harmony_changelog.md
M module-apps/application-music-player/ApplicationMusicPlayer.cpp
M module-audio/Audio/Audio.cpp
M module-audio/Audio/Audio.hpp
M module-audio/Audio/AudioCommon.hpp
M module-audio/Audio/AudioDeviceFactory.hpp
M module-audio/Audio/AudioMux.cpp
M module-audio/Audio/AudioMux.hpp
M module-audio/Audio/Operation/Operation.hpp
M module-audio/Audio/Operation/PlaybackOperation.cpp
M module-audio/Audio/Operation/PlaybackOperation.hpp
M module-audio/Audio/StreamQueuedEventsListener.cpp
M module-audio/Audio/StreamQueuedEventsListener.hpp
M module-audio/Audio/decoder/Decoder.cpp
M module-audio/Audio/decoder/Decoder.hpp
A module-audio/Audio/decoder/DecoderCommon.hpp
R module-audio/Audio/decoder/{decoderFLAC => DecoderFLAC}.cpp
R module-audio/Audio/decoder/{decoderFLAC => DecoderFLAC}.hpp
R module-audio/Audio/decoder/{decoderMP3 => DecoderMP3}.cpp
R module-audio/Audio/decoder/{decoderMP3 => DecoderMP3}.hpp
A module-audio/Audio/decoder/DecoderWAV.cpp
R module-audio/Audio/decoder/{decoderWAV => DecoderWAV}.hpp
M module-audio/Audio/decoder/DecoderWorker.cpp
M module-audio/Audio/decoder/DecoderWorker.hpp
D module-audio/Audio/decoder/decoderCommon.hpp
D module-audio/Audio/decoder/decoderWAV.cpp
M module-audio/CMakeLists.txt
M module-services/service-audio/ServiceAudio.cpp
M module-services/service-audio/include/service-audio/AudioMessage.hpp
M module-services/service-audio/include/service-audio/ServiceAudio.hpp
M products/BellHybrid/apps/application-bell-relaxation/ApplicationBellRelaxation.cpp
M products/BellHybrid/apps/application-bell-relaxation/include/application-bell-relaxation/ApplicationBellRelaxation.hpp
M products/BellHybrid/apps/application-bell-relaxation/presenter/RelaxationRunningLoopPresenter.cpp
M products/BellHybrid/apps/application-bell-relaxation/presenter/RelaxationRunningProgressPresenter.cpp
M products/BellHybrid/apps/application-bell-relaxation/presenter/RelaxationRunningProgressPresenter.hpp
M products/BellHybrid/apps/application-bell-relaxation/widgets/RelaxationPlayer.cpp
M products/BellHybrid/apps/application-bell-relaxation/widgets/RelaxationPlayer.hpp
M products/BellHybrid/apps/common/include/common/models/AbstractAudioModel.hpp
M products/BellHybrid/apps/common/include/common/models/AudioModel.hpp
M products/BellHybrid/apps/common/include/common/widgets/BellConnectionStatus.hpp
M products/BellHybrid/apps/common/src/AudioModel.cpp
M products/BellHybrid/apps/common/src/widgets/BellConnectionStatus.cpp
M products/BellHybrid/services/audio/ServiceAudio.cpp
M products/BellHybrid/services/audio/include/audio/AudioMessage.hpp
M products/BellHybrid/services/audio/include/audio/ServiceAudio.hpp
M third-party/dr_libs/src
M harmony_changelog.md => harmony_changelog.md +1 -0
@@ 6,6 6,7 @@
* Fixed source clock frequency computation for PWM module
* Fixed initial watchdog configuration
* Fixed alarm rings when deactivated during snooze
* Fixed popup about file deletion showing in Relaxation app even if no file was deleted

### Added
* Added setting onboarding year to build date year

M module-apps/application-music-player/ApplicationMusicPlayer.cpp => module-apps/application-music-player/ApplicationMusicPlayer.cpp +6 -1
@@ 1,4 1,4 @@
// Copyright (c) 2017-2023, Mudita Sp. z.o.o. All rights reserved.
// Copyright (c) 2017-2024, Mudita Sp. z.o.o. All rights reserved.
// For licensing, see https://github.com/mudita/MuditaOS/LICENSE.md

#include <application-music-player/ApplicationMusicPlayer.hpp>


@@ 82,6 82,11 @@ namespace app
            music_player::AudioNotificationsHandler audioNotificationHandler{priv->songsPresenter};
            return audioNotificationHandler.handleAudioEofNotification(notification);
        });
        connect(typeid(AudioFileDeletedNotification), [&](sys::Message *msg) -> sys::MessagePointer {
            const auto notification = static_cast<AudioStopNotification *>(msg);
            music_player::AudioNotificationsHandler audioNotificationHandler{priv->songsPresenter};
            return audioNotificationHandler.handleAudioEofNotification(notification);
        });
        connect(typeid(AudioPausedNotification), [&](sys::Message *msg) -> sys::MessagePointer {
            const auto notification = static_cast<AudioPausedNotification *>(msg);
            music_player::AudioNotificationsHandler audioNotificationHandler{priv->songsPresenter};

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

#include "Audio.hpp"


@@ 9,10 9,8 @@

namespace audio
{

    Audio::Audio(AudioServiceMessage::Callback callback) : currentOperation(), serviceCallback(callback)
    {

        auto ret = Operation::Create(Operation::Type::Idle, "", audio::PlaybackType::None, callback);
        if (ret) {
            currentOperation = std::move(ret);

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

#pragma once


@@ 14,7 14,6 @@

namespace audio
{

    class Audio
    {
        enum class Muted : bool


@@ 131,5 130,4 @@ namespace audio

        AudioServiceMessage::Callback serviceCallback;
    };

} // namespace audio

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

#pragma once


@@ 308,6 308,20 @@ namespace AudioServiceMessage
        audio::Token token = audio::Token::MakeBadToken();
    };

    class FileDeleted : public sys::DataMessage
    {
      public:
        explicit FileDeleted(audio::Token &token) : token(token)
        {}
        const audio::Token &GetToken() const
        {
            return token;
        }

      private:
        audio::Token token = audio::Token::MakeBadToken();
    };

    class FileSystemNoSpace : public sys::DataMessage
    {
      public:

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

#pragma once


@@ 11,7 11,6 @@

namespace audio
{

    class AudioDeviceFactory
    {
      public:


@@ 22,6 21,7 @@ namespace audio
        };

        explicit AudioDeviceFactory(Observer *observer = nullptr);
        virtual ~AudioDeviceFactory() = default;

        void setObserver(Observer *observer) noexcept;
        std::shared_ptr<AudioDevice> CreateDevice(const Profile &profile);


@@ 33,5 33,4 @@ namespace audio
      private:
        Observer *_observer = nullptr;
    };

}; // namespace audio

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

#include "AudioMux.hpp"


@@ 25,7 25,7 @@ namespace audio
    {
        audioInputsCount = audioInputsCount > 0 ? audioInputsCount : 1;
        audioInputsInternal.reserve(audioInputsCount);
        for (size_t i = 0; i < audioInputsCount; i++) {
        for (std::size_t i = 0; i < audioInputsCount; i++) {
            audioInputsInternal.emplace_back(Input(std::make_unique<Audio>(callback), refToken.IncrementToken()));
        }
    }

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

#pragma once


@@ 111,5 111,4 @@ namespace audio
        std::vector<Input> audioInputsInternal;
        std::vector<Input> &audioInputs;
    };

} // namespace audio

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

#pragma once


@@ 162,5 162,4 @@ namespace audio
        std::shared_ptr<AudioDevice> CreateDevice(const Profile &profile);
        std::shared_ptr<AudioDevice> createCellularAudioDevice();
    };

} // namespace audio

M module-audio/Audio/Operation/PlaybackOperation.cpp => module-audio/Audio/Operation/PlaybackOperation.cpp +10 -7
@@ 1,4 1,4 @@
// Copyright (c) 2017-2023, Mudita Sp. z.o.o. All rights reserved.
// Copyright (c) 2017-2024, Mudita Sp. z.o.o. All rights reserved.
// For licensing, see https://github.com/mudita/MuditaOS/LICENSE.md

#include "PlaybackOperation.hpp"


@@ 13,7 13,6 @@

namespace audio
{

    using namespace AudioServiceMessage;

    PlaybackOperation::PlaybackOperation(const std::string &filePath,


@@ 28,9 27,14 @@ namespace audio

        endOfFileCallback = [this]() {
            state          = State::Idle;
            const auto req = AudioServiceMessage::EndOfFile(operationToken);
            serviceCallback(&req);
            return std::string();
            const auto msg = AudioServiceMessage::EndOfFile(operationToken);
            serviceCallback(&msg);
        };

        fileDeletedCallback = [this]() {
            state          = State::Idle;
            const auto msg = AudioServiceMessage::FileDeleted(operationToken);
            serviceCallback(&msg);
        };

        dec = Decoder::Create(filePath);


@@ 66,7 70,7 @@ namespace audio
        outputConnection = std::make_unique<StreamConnection>(dec.get(), audioDevice.get(), dataStreamOut.get());

        // decoder worker soft start - must be called after connection setup
        dec->startDecodingWorker(endOfFileCallback);
        dec->startDecodingWorker(endOfFileCallback, fileDeletedCallback);

        // start output device and enable audio connection
        auto ret = audioDevice->Start();


@@ 225,5 229,4 @@ namespace audio
    {
        Stop();
    }

} // namespace audio

M module-audio/Audio/Operation/PlaybackOperation.hpp => module-audio/Audio/Operation/PlaybackOperation.hpp +3 -8
@@ 1,4 1,4 @@
// Copyright (c) 2017-2022, Mudita Sp. z.o.o. All rights reserved.
// Copyright (c) 2017-2024, Mudita Sp. z.o.o. All rights reserved.
// For licensing, see https://github.com/mudita/MuditaOS/LICENSE.md

#pragma once


@@ 10,13 10,8 @@
#include "Audio/decoder/Decoder.hpp"

#include <chrono>
using namespace std::chrono_literals;

namespace audio::playbackDefaults
{
    constexpr audio::Volume defaultLoudspeakerVolume = 10;
    constexpr audio::Volume defaultHeadphonesVolume  = 2;
} // namespace audio::playbackDefaults
using namespace std::chrono_literals;

namespace audio
{


@@ 49,6 44,6 @@ namespace audio
        std::unique_ptr<StreamConnection> outputConnection;

        DecoderWorker::EndOfFileCallback endOfFileCallback;
        DecoderWorker::FileDeletedCallback fileDeletedCallback;
    };

} // namespace audio

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

#include "StreamQueuedEventsListener.hpp"


@@ 30,7 30,7 @@ void StreamQueuedEventsListener::onEvent(AbstractStream *stream, Stream::Event e
    }
}

StreamQueuedEventsListener::queuedEvent StreamQueuedEventsListener::waitForEvent()
StreamQueuedEventsListener::QueuedEvent StreamQueuedEventsListener::waitForEvent()
{
    EventStorage queueStorage;
    if (queue->Dequeue(&queueStorage)) {


@@ 44,7 44,7 @@ std::size_t StreamQueuedEventsListener::getEventsCount() const
    return queue->NumItems();
}

StreamQueuedEventsListener::queuedEvent StreamQueuedEventsListener::getEvent()
StreamQueuedEventsListener::QueuedEvent StreamQueuedEventsListener::getEvent()
{
    EventStorage queueStorage;


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

#pragma once


@@ 23,21 23,20 @@ namespace audio
        };

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

        explicit StreamQueuedEventsListener(std::shared_ptr<cpp_freertos::Queue> eventsQueue);
        virtual ~StreamQueuedEventsListener() = default;

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

        queuedEvent waitForEvent();
        queuedEvent getEvent();
        QueuedEvent waitForEvent();
        QueuedEvent getEvent();

        std::size_t getEventsCount() const;

      private:
        std::shared_ptr<cpp_freertos::Queue> queue;
    };

}; // namespace audio

M module-audio/Audio/decoder/Decoder.cpp => module-audio/Audio/decoder/Decoder.cpp +17 -18
@@ 1,12 1,12 @@
// Copyright (c) 2017-2023, Mudita Sp. z.o.o. All rights reserved.
// Copyright (c) 2017-2024, Mudita Sp. z.o.o. All rights reserved.
// For licensing, see https://github.com/mudita/MuditaOS/LICENSE.md

#include <cstdio>
#include <Utils.hpp>
#include "Decoder.hpp"
#include "decoderMP3.hpp"
#include "decoderFLAC.hpp"
#include "decoderWAV.hpp"
#include "DecoderMP3.hpp"
#include "DecoderFLAC.hpp"
#include "DecoderWAV.hpp"

#include "tag.h"
#include <tags_fetcher/TagsFetcher.hpp>


@@ 15,12 15,12 @@ namespace audio
{
    Decoder::Decoder(const std::string &path) : filePath(path)
    {
        fd = std::fopen(path.c_str(), "r");
        fd = std::fopen(path.c_str(), "rb");
        if (fd == nullptr) {
            return;
        }

        constexpr size_t streamBufferSize = 16 * 1024;
        constexpr std::size_t streamBufferSize = 16 * 1024;
        streamBuffer                      = std::make_unique<char[]>(streamBufferSize);
        setvbuf(fd, streamBuffer.get(), _IOFBF, streamBufferSize);



@@ 55,13 55,13 @@ namespace audio
        std::unique_ptr<Decoder> dec;

        if (extensionLowercase == ".wav") {
            dec = std::make_unique<decoderWAV>(filePath);
            dec = std::make_unique<DecoderWAV>(filePath);
        }
        else if (extensionLowercase == ".mp3") {
            dec = std::make_unique<decoderMP3>(filePath);
            dec = std::make_unique<DecoderMP3>(filePath);
        }
        else if (extensionLowercase == ".flac") {
            dec = std::make_unique<decoderFLAC>(filePath);
            dec = std::make_unique<DecoderFLAC>(filePath);
        }
        else {
            return nullptr;


@@ 74,16 74,16 @@ namespace audio
        return dec;
    }

    void Decoder::startDecodingWorker(const DecoderWorker::EndOfFileCallback &endOfFileCallback)
    void Decoder::startDecodingWorker(const DecoderWorker::EndOfFileCallback &endOfFileCallback,
                                      const DecoderWorker::FileDeletedCallback &fileDeletedCallback)
    {
        assert(_stream != nullptr);
        if (!audioWorker) {
        if (audioWorker == nullptr) {
            const auto channelMode = (tags->num_channel == 1) ? DecoderWorker::ChannelMode::ForceStereo
                                                              : DecoderWorker::ChannelMode::NoConversion;

            audioWorker =
                std::make_unique<DecoderWorker>(_stream,
                                                this,
                                                endOfFileCallback,
                                                tags->num_channel == 1 ? DecoderWorker::ChannelMode::ForceStereo
                                                                       : DecoderWorker::ChannelMode::NoConversion);
                std::make_unique<DecoderWorker>(_stream, this, endOfFileCallback, fileDeletedCallback, channelMode);
            audioWorker->init();
            audioWorker->run();
        }


@@ 120,7 120,7 @@ namespace audio
        auto bitWidth = getBitWidth();
        // this is a decoder mono to stereo hack, will be removed when proper
        // transcoding implementation is added
        auto channels = tags->num_channel == 1 ? 2U : tags->num_channel;
        auto channels = (tags->num_channel == 1) ? 2U : tags->num_channel;

        return AudioFormat{tags->sample_rate, bitWidth, channels};
    }


@@ 134,5 134,4 @@ namespace audio
    {
        return Endpoint::Traits{};
    }

} // namespace audio

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

#pragma once


@@ 16,19 16,19 @@ namespace audio
{
    namespace channel
    {
        constexpr inline auto monoSound   = 1;
        constexpr inline auto stereoSound = 2;
        inline constexpr auto monoSound   = 1;
        inline constexpr auto stereoSound = 2;
    } // namespace channel

    class Decoder : public Source
    {

      public:
        Decoder(const std::string &path);
        static constexpr auto fileDeletedRetCode = -1;

        explicit Decoder(const std::string &path);
        virtual ~Decoder();

        virtual std::uint32_t decode(std::uint32_t samplesToRead, std::int16_t *pcmData) = 0;
        virtual std::int32_t decode(std::uint32_t samplesToRead, std::int16_t *pcmData) = 0;

        // Range 0 - 1
        virtual void setPosition(float pos) = 0;


@@ 38,9 38,9 @@ namespace audio
            return sampleRate;
        }

        std::uint32_t getChannelNumber()
        std::uint32_t getChannelCount()
        {
            return chanNumber;
            return channelCount;
        }

        float getCurrentPosition()


@@ 57,7 57,8 @@ namespace audio

        auto getTraits() const -> Endpoint::Traits override;

        void startDecodingWorker(const DecoderWorker::EndOfFileCallback &endOfFileCallback);
        void startDecodingWorker(const DecoderWorker::EndOfFileCallback &endOfFileCallback,
                                 const DecoderWorker::FileDeletedCallback &fileDeletedCallback);
        void stopDecodingWorker();

        // Factory method


@@ 68,12 69,13 @@ namespace audio
        {
            return bitsPerSample;
        }

        virtual std::unique_ptr<tags::fetcher::Tags> fetchTags();

        static constexpr Endpoint::Traits decoderCaps = {.usesDMA = false};

        std::uint32_t sampleRate = 0;
        std::uint32_t chanNumber = 0;
        std::uint32_t channelCount = 0;
        std::uint32_t bitsPerSample;
        float position = 0;
        std::FILE *fd  = nullptr;


@@ 84,9 86,7 @@ namespace audio
        std::unique_ptr<tags::fetcher::Tags> tags;
        bool isInitialized = false;

        // decoding worker
        // Decoding worker
        std::unique_ptr<DecoderWorker> audioWorker;
        DecoderWorker::EndOfFileCallback _endOfFileCallback;
    };

} // namespace audio

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

#pragma once

#include <sys/stat.h>
#include <cstdio>

namespace audio
{
    inline bool fileExists(FILE *fd)
    {
        struct stat fdStat
        {};
        constexpr auto statFailed = -1;
        return (fstat(fileno(fd), &fdStat) != statFailed);
    }
} // namespace audio

R module-audio/Audio/decoder/decoderFLAC.cpp => module-audio/Audio/decoder/DecoderFLAC.cpp +30 -31
@@ 1,10 1,9 @@
// Copyright (c) 2017-2023, Mudita Sp. z.o.o. All rights reserved.
// Copyright (c) 2017-2024, Mudita Sp. z.o.o. All rights reserved.
// For licensing, see https://github.com/mudita/MuditaOS/LICENSE.md

#include <Utils.hpp>
#include "decoderFLAC.hpp"
#include "DecoderFLAC.hpp"
#include "DecoderCommon.hpp"
#include "flac/flacfile.h"
#include "decoderCommon.hpp"

#define DR_FLAC_IMPLEMENTATION
#define DR_FLAC_NO_STDIO


@@ 15,44 14,47 @@

namespace audio
{

    decoderFLAC::decoderFLAC(const std::string &filePath) : Decoder(filePath)
    DecoderFLAC::DecoderFLAC(const std::string &filePath) : Decoder(filePath)
    {
        if (fileSize == 0) {
            return;
        }

        flac = drflac_open(drflac_read, drflac_seek, this, nullptr);
        flac = drflac_open(drflacRead, drflacSeek, this, nullptr);
        if (flac == nullptr) {
            LOG_ERROR("Unable to initialize FLAC decoder");
            return;
        }

        chanNumber = flac->channels;
        sampleRate = flac->sampleRate;
        // NOTE: Always convert to S16LE as internal format
        /* NOTE: Always convert to S16LE as an internal format */
        channelCount  = flac->channels;
        sampleRate    = flac->sampleRate;
        bitsPerSample = 16;
        isInitialized = true;
    }

    decoderFLAC::~decoderFLAC()
    DecoderFLAC::~DecoderFLAC()
    {
        drflac_close(flac);
    }

    std::uint32_t decoderFLAC::decode(std::uint32_t samplesToRead, int16_t *pcmData)
    std::int32_t DecoderFLAC::decode(std::uint32_t samplesToRead, int16_t *pcmData)
    {
        std::uint32_t samples_read =
            drflac_read_pcm_frames_s16(flac, samplesToRead / chanNumber, reinterpret_cast<drflac_int16 *>(pcmData));
        if (samples_read > 0) {
            /* Calculate frame duration in seconds */
            position += static_cast<float>(samples_read) / static_cast<float>(sampleRate);
        if (!fileExists(fd)) {
            LOG_WARN("File '%s' was deleted during playback!", filePath.c_str());
            return fileDeletedRetCode;
        }

        return samples_read * chanNumber;
        const auto samplesRead =
            drflac_read_pcm_frames_s16(flac, samplesToRead / channelCount, reinterpret_cast<drflac_int16 *>(pcmData));
        if (samplesRead > 0) {
            /* Calculate frame duration in seconds */
            position += static_cast<float>(samplesRead) / static_cast<float>(sampleRate);
        }
        return samplesRead * channelCount;
    }

    void decoderFLAC::setPosition(float pos)
    void DecoderFLAC::setPosition(float pos)
    {
        if (!isInitialized) {
            LOG_ERROR("FLAC decoder not initialized");


@@ 65,29 67,26 @@ namespace audio
    }

    /* Data encoded in UTF-8 */
    void decoderFLAC::flac_parse_text(
    void DecoderFLAC::parseText(
        std::uint8_t *in, std::uint32_t taglen, std::uint32_t datalen, std::uint8_t *out, std::uint32_t outlen)
    {
        /* Little Endian here. */
        std::uint32_t size = ((datalen - taglen) > (outlen - 1)) ? (outlen - 1) : (datalen - taglen);
        /* Little Endian here */
        const auto size = std::min(datalen - taglen, outlen - 1);
        memcpy(out, in + taglen, size);
        out[size] = '\0';
    }

    size_t decoderFLAC::drflac_read(void *pUserData, void *pBufferOut, size_t bytesToRead)
    std::size_t DecoderFLAC::drflacRead(void *pUserData, void *pBufferOut, std::size_t bytesToRead)
    {
        const auto decoderContext = reinterpret_cast<decoderFLAC *>(pUserData);
        return !statFd(decoderContext->fd, "FLAC audio file deleted by user!")
                   ? 0
                   : std::fread(pBufferOut, 1, bytesToRead, decoderContext->fd);
        const auto decoderContext = reinterpret_cast<DecoderFLAC *>(pUserData);
        return std::fread(pBufferOut, 1, bytesToRead, decoderContext->fd);
    }

    drflac_bool32 decoderFLAC::drflac_seek(void *pUserData, int offset, drflac_seek_origin origin)
    drflac_bool32 DecoderFLAC::drflacSeek(void *pUserData, int offset, drflac_seek_origin origin)
    {
        const auto decoderContext = reinterpret_cast<decoderFLAC *>(pUserData);
        const int seekError =
        const auto decoderContext = reinterpret_cast<DecoderFLAC *>(pUserData);
        const auto seekError =
            std::fseek(decoderContext->fd, offset, origin == drflac_seek_origin_start ? SEEK_SET : SEEK_CUR);
        return (seekError == 0) ? DRFLAC_TRUE : DRFLAC_FALSE;
    }

} // namespace audio

R module-audio/Audio/decoder/decoderFLAC.hpp => module-audio/Audio/decoder/DecoderFLAC.hpp +11 -14
@@ 1,4 1,4 @@
// Copyright (c) 2017-2022, Mudita Sp. z.o.o. All rights reserved.
// Copyright (c) 2017-2024, Mudita Sp. z.o.o. All rights reserved.
// For licensing, see https://github.com/mudita/MuditaOS/LICENSE.md

#pragma once


@@ 8,16 8,13 @@

namespace audio
{

    class decoderFLAC : public Decoder
    class DecoderFLAC : public Decoder
    {

      public:
        explicit decoderFLAC(const std::string &filePath);
        explicit DecoderFLAC(const std::string &filePath);
        ~DecoderFLAC();

        ~decoderFLAC();

        uint32_t decode(uint32_t samplesToRead, int16_t *pcmData) override;
        std::int32_t decode(std::uint32_t samplesToRead, std::int16_t *pcmData) override;

        void setPosition(float pos) override;



@@ 25,7 22,8 @@ namespace audio
        drflac *flac = nullptr;

        /* Data encoded in UTF-8 */
        void flac_parse_text(uint8_t *in, uint32_t taglen, uint32_t datalen, uint8_t *out, uint32_t outlen);
        void parseText(
            std::uint8_t *in, std::uint32_t taglen, std::uint32_t datalen, std::uint8_t *out, std::uint32_t outlen);

        // Callback for when data needs to be read from the client.
        //


@@ 37,7 35,7 @@ namespace audio
        //
        // A return value of less than bytesToRead indicates the end of the stream. Do _not_ return from this callback
        // until either the entire bytesToRead is filled or you have reached the end of the stream.
        static size_t drflac_read(void *pUserData, void *pBufferOut, size_t bytesToRead);
        static std::size_t drflacRead(void *pUserData, void *pBufferOut, std::size_t bytesToRead);

        // Callback for when data needs to be seeked.
        //


@@ 45,12 43,11 @@ namespace audio
        // offset    [in] The number of bytes to move, relative to the origin. Will never be negative.
        // origin    [in] The origin of the seek - the current position or the start of the stream.
        //
        // Returns whether or not the seek was successful.
        // Returns whether the seek was successful.
        //
        // The offset will never be negative. Whether or not it is relative to the beginning or current position is
        // The offset will never be negative. Whether it is relative to the beginning or current position is
        // determined by the "origin" parameter which will be either drflac_seek_origin_start or
        // drflac_seek_origin_current.
        static drflac_bool32 drflac_seek(void *pUserData, int offset, drflac_seek_origin origin);
        static drflac_bool32 drflacSeek(void *pUserData, int offset, drflac_seek_origin origin);
    };

} // namespace audio

R module-audio/Audio/decoder/decoderMP3.cpp => module-audio/Audio/decoder/DecoderMP3.cpp +25 -22
@@ 1,11 1,11 @@
// Copyright (c) 2017-2023, Mudita Sp. z.o.o. All rights reserved.
// Copyright (c) 2017-2024, Mudita Sp. z.o.o. All rights reserved.
// For licensing, see https://github.com/mudita/MuditaOS/LICENSE.md

#define DR_MP3_IMPLEMENTATION
#define DR_MP3_NO_STDIO

#include "decoderCommon.hpp"
#include "decoderMP3.hpp"
#include "DecoderCommon.hpp"
#include "DecoderMP3.hpp"
#include <cstdio>

namespace


@@ 49,7 49,7 @@ namespace

namespace audio
{
    decoderMP3::decoderMP3(const std::string &filePath) : Decoder(filePath)
    DecoderMP3::DecoderMP3(const std::string &filePath) : Decoder(filePath)
    {
        if (fileSize == 0) {
            return;


@@ 65,25 65,25 @@ namespace audio

        mp3 = std::make_unique<drmp3>();

        drmp3_init(mp3.get(), drmp3_read, drmp3_seek, this, nullptr);
        drmp3_init(mp3.get(), drmp3Read, drmp3Seek, this, nullptr);
        if (mp3 == nullptr) {
            LOG_ERROR("Unable to initialize MP3 decoder");
            return;
        }

        chanNumber = mp3->channels;
        sampleRate = mp3->sampleRate;
        // NOTE: Always convert to S16LE as internal format
        /* NOTE: Always convert to S16LE as an internal format */
        channelCount  = mp3->channels;
        sampleRate    = mp3->sampleRate;
        bitsPerSample = 16;
        isInitialized = true;
    }

    decoderMP3::~decoderMP3()
    DecoderMP3::~DecoderMP3()
    {
        drmp3_uninit(mp3.get());
    }

    void decoderMP3::setPosition(float pos)
    void DecoderMP3::setPosition(float pos)
    {
        if (!isInitialized) {
            LOG_ERROR("MP3 decoder not initialized");


@@ 94,28 94,31 @@ namespace audio
        position = static_cast<float>(totalFramesCount) * pos / static_cast<float>(sampleRate);
    }

    std::uint32_t decoderMP3::decode(std::uint32_t samplesToRead, std::int16_t *pcmData)
    std::int32_t DecoderMP3::decode(std::uint32_t samplesToRead, std::int16_t *pcmData)
    {
        const auto samplesRead =
            drmp3_read_pcm_frames_s16(mp3.get(), samplesToRead / chanNumber, reinterpret_cast<drmp3_int16 *>(pcmData));
        if (!fileExists(fd)) {
            LOG_WARN("File '%s' was deleted during playback!", filePath.c_str());
            return fileDeletedRetCode;
        }

        const auto samplesRead = drmp3_read_pcm_frames_s16(
            mp3.get(), samplesToRead / channelCount, reinterpret_cast<drmp3_int16 *>(pcmData));
        if (samplesRead > 0) {
            /* Calculate frame duration in seconds */
            position += static_cast<float>(samplesRead) / static_cast<float>(sampleRate);
        }

        return samplesRead * chanNumber;
        return samplesRead * channelCount;
    }

    std::size_t decoderMP3::drmp3_read(void *pUserData, void *pBufferOut, std::size_t bytesToRead)
    std::size_t DecoderMP3::drmp3Read(void *pUserData, void *pBufferOut, std::size_t bytesToRead)
    {
        const auto decoderContext = reinterpret_cast<decoderMP3 *>(pUserData);
        return !statFd(decoderContext->fd, "MP3 audio file deleted by user!")
                   ? 0
                   : std::fread(pBufferOut, 1, bytesToRead, decoderContext->fd);
        const auto decoderContext = reinterpret_cast<DecoderMP3 *>(pUserData);
        return std::fread(pBufferOut, 1, bytesToRead, decoderContext->fd);
    }

    drmp3_bool32 decoderMP3::drmp3_seek(void *pUserData, int offset, drmp3_seek_origin origin)
    drmp3_bool32 DecoderMP3::drmp3Seek(void *pUserData, int offset, drmp3_seek_origin origin)
    {
        const auto decoderContext = reinterpret_cast<decoderMP3 *>(pUserData);
        const auto decoderContext = reinterpret_cast<DecoderMP3 *>(pUserData);
        const auto seekError =
            std::fseek(decoderContext->fd, offset, origin == drmp3_seek_origin_start ? SEEK_SET : SEEK_CUR);
        return (seekError == 0) ? DRMP3_TRUE : DRMP3_FALSE;

R module-audio/Audio/decoder/decoderMP3.hpp => module-audio/Audio/decoder/DecoderMP3.hpp +9 -11
@@ 1,4 1,4 @@
// Copyright (c) 2017-2023, Mudita Sp. z.o.o. All rights reserved.
// Copyright (c) 2017-2024, Mudita Sp. z.o.o. All rights reserved.
// For licensing, see https://github.com/mudita/MuditaOS/LICENSE.md

#pragma once


@@ 8,15 8,13 @@

namespace audio
{
    class decoderMP3 : public Decoder
    class DecoderMP3 : public Decoder
    {

      public:
        explicit decoderMP3(const std::string &filePath);

        ~decoderMP3();
        explicit DecoderMP3(const std::string &filePath);
        ~DecoderMP3();

        std::uint32_t decode(std::uint32_t samplesToRead, std::int16_t *pcmData) override;
        std::int32_t decode(std::uint32_t samplesToRead, std::int16_t *pcmData) override;

        void setPosition(float pos) override;



@@ 33,7 31,7 @@ namespace audio
        //
        // A return value of less than bytesToRead indicates the end of the stream. Do _not_ return from this callback
        // until either the entire bytesToRead is filled or you have reached the end of the stream.
        static std::size_t drmp3_read(void *pUserData, void *pBufferOut, std::size_t bytesToRead);
        static std::size_t drmp3Read(void *pUserData, void *pBufferOut, std::size_t bytesToRead);

        // Callback for when data needs to be seeked.
        //


@@ 41,11 39,11 @@ namespace audio
        // offset    [in] The number of bytes to move, relative to the origin. Will never be negative.
        // origin    [in] The origin of the seek - the current position or the start of the stream.
        //
        // Returns whether or not the seek was successful.
        // Returns whether the seek was successful.
        //
        // The offset will never be negative. Whether or not it is relative to the beginning or current position is
        // The offset will never be negative. Whether it is relative to the beginning or current position is
        // determined by the "origin" parameter which will be either drflac_seek_origin_start or
        // drflac_seek_origin_current.
        static drmp3_bool32 drmp3_seek(void *pUserData, int offset, drmp3_seek_origin origin);
        static drmp3_bool32 drmp3Seek(void *pUserData, int offset, drmp3_seek_origin origin);
    };
} // namespace audio

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

#include "DecoderWAV.hpp"
#include "DecoderCommon.hpp"
#include <log/log.hpp>
#include <memory>

#define DR_WAV_IMPLEMENTATION
#include <src/dr_wav.h>

namespace audio
{
    DecoderWAV::DecoderWAV(const std::string &filePath) : Decoder(filePath), wav(std::make_unique<drwav>())
    {
        if (drwav_init_file(wav.get(), filePath.c_str(), nullptr) == DRWAV_FALSE) {
            LOG_ERROR("Unable to init WAV decoder");
            return;
        }

        /* NOTE: Always convert to S16LE as an internal format */
        channelCount  = wav->channels;
        sampleRate    = wav->sampleRate;
        bitsPerSample = 16;
        isInitialized = true;
    }

    DecoderWAV::~DecoderWAV()
    {
        if (isInitialized) {
            drwav_uninit(wav.get());
        }
    }

    std::int32_t DecoderWAV::decode(std::uint32_t samplesToRead, std::int16_t *pcmData)
    {
        if (!isInitialized) {
            LOG_ERROR("WAV decoder not initialized");
            return 0;
        }

        if (!fileExists(fd)) {
            LOG_WARN("File '%s' was deleted during playback!", filePath.c_str());
            return fileDeletedRetCode;
        }

        const auto samplesRead = drwav_read_pcm_frames_s16(wav.get(), samplesToRead / channelCount, pcmData);
        if (samplesRead > 0) {
            /* Calculate frame duration in seconds */
            position += static_cast<float>(samplesRead) / static_cast<float>(sampleRate);
        }
        return samplesRead * channelCount;
    }

    void DecoderWAV::setPosition(float pos)
    {
        if (!isInitialized) {
            LOG_ERROR("WAV decoder not initialized");
            return;
        }
        drwav_seek_to_pcm_frame(wav.get(), wav->totalPCMFrameCount * pos);

        /* Calculate new position */
        position = static_cast<float>(wav->totalPCMFrameCount) * pos / static_cast<float>(sampleRate);
    }
} // namespace audio

R module-audio/Audio/decoder/decoderWAV.hpp => module-audio/Audio/decoder/DecoderWAV.hpp +7 -11
@@ 1,28 1,24 @@
// Copyright (c) 2017-2022, Mudita Sp. z.o.o. All rights reserved.
// Copyright (c) 2017-2024, Mudita Sp. z.o.o. All rights reserved.
// For licensing, see https://github.com/mudita/MuditaOS/LICENSE.md

#pragma once

#include "Decoder.hpp"
#include <src/dr_wav.h>

namespace audio
{
    namespace internal
    {
        struct wavContext;
    }
    class decoderWAV : public Decoder
    class DecoderWAV : public Decoder
    {
      public:
        explicit decoderWAV(const std::string &filePath);
        virtual ~decoderWAV();
        explicit DecoderWAV(const std::string &filePath);
        ~DecoderWAV();

        std::uint32_t decode(std::uint32_t samplesToRead, std::int16_t *pcmData) override;
        std::int32_t decode(std::uint32_t samplesToRead, std::int16_t *pcmData) override;

        void setPosition(float pos) override;

      private:
        std::unique_ptr<internal::wavContext> decoderContext;
        std::unique_ptr<drwav> wav;
    };

} // namespace audio

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

#include "DecoderWorker.hpp"


@@ 7,11 7,12 @@

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

audio::DecoderWorker::~DecoderWorker()


@@ 41,7 42,7 @@ auto audio::DecoderWorker::init(std::list<sys::WorkerQueueInfo> queues) -> bool
    return isSuccessful;
}

bool audio::DecoderWorker::handleMessage(uint32_t queueID)
bool audio::DecoderWorker::handleMessage(std::uint32_t queueID)
{
    auto queue      = queues[queueID];
    auto &queueName = queue->GetQueueName();


@@ 58,7 59,6 @@ bool audio::DecoderWorker::handleMessage(uint32_t queueID)
        case Stream::Event::StreamFull:
            break;
        case Stream::Event::StreamHalfUsed:
            [[fallthrough]];
        case Stream::Event::StreamEmpty:
            pushAudioData();
        }


@@ 88,13 88,17 @@ bool audio::DecoderWorker::handleMessage(uint32_t queueID)

void audio::DecoderWorker::pushAudioData()
{
    auto samplesRead             = 0;
    const unsigned int readScale = channelMode == ChannelMode::ForceStereo ? 2 : 1;
    const auto readScale     = (channelMode == ChannelMode::ForceStereo) ? channel::stereoSound : channel::monoSound;
    std::int32_t samplesRead = 0;

    while (!audioStreamOut->isFull() && playbackEnabled) {
        auto buffer = decoderBuffer.get();
        samplesRead = decoder->decode(bufferSize / readScale, buffer);

        if (samplesRead == Decoder::fileDeletedRetCode) {
            fileDeletedCallback();
            break;
        }
        if (samplesRead == 0) {
            endOfFileCallback();
            break;


@@ 102,13 106,13 @@ void audio::DecoderWorker::pushAudioData()

        // pcm mono to stereo force conversion
        if (channelMode == ChannelMode::ForceStereo) {
            for (unsigned int i = bufferSize / 2; i > 0; i--) {
            for (auto i = bufferSize / 2; i > 0; i--) {
                buffer[i * 2 - 1] = buffer[i * 2 - 2] = buffer[i - 1];
            }
        }

        if (!audioStreamOut->push(decoderBuffer.get(), samplesRead * sizeof(BufferInternalType) * readScale)) {
            LOG_FATAL("Decoder failed to push to stream.");
            LOG_ERROR("Decoder failed to push to stream");
            break;
        }
    }


@@ 116,13 120,13 @@ void audio::DecoderWorker::pushAudioData()

bool audio::DecoderWorker::enablePlayback()
{
    return sendCommand({.command = static_cast<uint32_t>(Command::EnablePlayback), .data = nullptr}) &&
    return sendCommand({.command = static_cast<std::uint32_t>(Command::EnablePlayback), .data = nullptr}) &&
           stateChangeWait();
}

bool audio::DecoderWorker::disablePlayback()
{
    return sendCommand({.command = static_cast<uint32_t>(Command::DisablePlayback), .data = nullptr}) &&
    return sendCommand({.command = static_cast<std::uint32_t>(Command::DisablePlayback), .data = nullptr}) &&
           stateChangeWait();
}


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

#pragma once


@@ 16,6 16,8 @@ namespace audio
    {
      public:
        using EndOfFileCallback = std::function<void()>;
        using FileDeletedCallback = std::function<void()>;

        enum class Command
        {
            EnablePlayback,


@@ 30,7 32,8 @@ namespace audio

        DecoderWorker(AbstractStream *audioStreamOut,
                      Decoder *decoder,
                      EndOfFileCallback endOfFileCallback,
                      const EndOfFileCallback &endOfFileCallback,
                      const FileDeletedCallback &fileDeletedCallback,
                      ChannelMode mode);
        ~DecoderWorker() override;



@@ 42,11 45,11 @@ namespace audio
      private:
        static constexpr std::size_t stackDepth = 12 * 1024;

        virtual auto handleMessage(uint32_t queueID) -> bool override;
        virtual auto handleMessage(std::uint32_t queueID) -> bool override;
        void pushAudioData();
        bool stateChangeWait();

        using BufferInternalType = int16_t;
        using BufferInternalType = std::int16_t;

        static constexpr auto workerName            = "DecoderWorker";
        static constexpr auto workerPriority        = static_cast<UBaseType_t>(sys::ServicePriority::Idle);


@@ 55,7 58,6 @@ namespace audio

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


@@ 63,5 65,8 @@ namespace audio
        const int bufferSize;
        std::unique_ptr<BufferInternalType[]> decoderBuffer;
        ChannelMode channelMode = ChannelMode::NoConversion;

        EndOfFileCallback endOfFileCallback;
        FileDeletedCallback fileDeletedCallback;
    };
} // namespace audio

D module-audio/Audio/decoder/decoderCommon.hpp => module-audio/Audio/decoder/decoderCommon.hpp +0 -26
@@ 1,26 0,0 @@
// Copyright (c) 2017-2023, Mudita Sp. z.o.o. All rights reserved.
// For licensing, see https://github.com/mudita/MuditaOS/LICENSE.md

#pragma once

#include <log/log.hpp>
#include <sys/stat.h>
#include <cerrno>
#include <cstdio>

namespace
{
    inline bool statFd(FILE *fd, char const *logMessage)
    {
        struct stat fdStats;
        constexpr int statFailed = -1;
        if (fstat(fileno(fd), &fdStats) != statFailed) {
            return true;
        }

        auto originalErrno = errno;
        LOG_WARN("%s", logMessage);
        errno = originalErrno;
        return false;
    }
} // namespace

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

#include "decoderWAV.hpp"
#include <log/log.hpp>
#include <memory>

#define DR_WAV_IMPLEMENTATION
#include <src/dr_wav.h>

namespace audio
{
    namespace internal
    {
        struct wavContext
        {
            drwav wav;
        };
    } // namespace internal

    decoderWAV::decoderWAV(const std::string &filePath)
        : Decoder(filePath), decoderContext(std::make_unique<internal::wavContext>())
    {
        auto dwav = &decoderContext->wav;
        if (drwav_init_file(dwav, filePath.c_str(), nullptr) == DRWAV_FALSE) {
            LOG_ERROR("Unable to init wav decoder");
            return;
        }
        sampleRate = dwav->sampleRate;
        // NOTE: Always convert to S16LE as internal format
        bitsPerSample = 16;
        // Number of channels
        chanNumber    = dwav->channels;
        isInitialized = true;
    }

    decoderWAV::~decoderWAV()
    {
        if (isInitialized) {
            auto dwav = &decoderContext->wav;
            drwav_uninit(dwav);
        }
    }

    std::uint32_t decoderWAV::decode(std::uint32_t samplesToRead, std::int16_t *pcmData)
    {
        if (!isInitialized) {
            LOG_ERROR("Wav decoder not initialized");
            return 0;
        }
        auto dwav               = &decoderContext->wav;
        const auto samples_read = drwav_read_pcm_frames_s16(dwav, samplesToRead / chanNumber, pcmData);
        if (samples_read) {
            /* Calculate frame duration in seconds */
            position += static_cast<float>(samplesToRead) / static_cast<float>(sampleRate);
        }
        return samples_read * chanNumber;
    }

    void decoderWAV::setPosition(float pos)
    {
        if (!isInitialized) {
            LOG_ERROR("Wav decoder not initialized");
            return;
        }
        auto dwav = &decoderContext->wav;
        drwav_seek_to_pcm_frame(dwav, dwav->totalPCMFrameCount * pos);

        // Calculate new position
        position = static_cast<float>(dwav->totalPCMFrameCount) * pos / static_cast<float>(sampleRate);
    }
} // namespace audio

M module-audio/CMakeLists.txt => module-audio/CMakeLists.txt +5 -5
@@ 21,9 21,9 @@ target_sources(
                ${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/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/encoder/Encoder.cpp
                ${CMAKE_CURRENT_SOURCE_DIR}/Audio/encoder/EncoderWAV.cpp


@@ 54,11 54,11 @@ target_compile_definitions(${PROJECT_NAME} PUBLIC ${PROJECT_TARGET})
target_include_directories(${PROJECT_NAME} PUBLIC ${PROJECT_INCLUDES})
target_include_directories(${PROJECT_NAME} PUBLIC ${CMAKE_CURRENT_SOURCE_DIR})

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

target_link_libraries(${PROJECT_NAME}

M module-services/service-audio/ServiceAudio.cpp => module-services/service-audio/ServiceAudio.cpp +35 -14
@@ 1,4 1,4 @@
// Copyright (c) 2017-2023, Mudita Sp. z.o.o. All rights reserved.
// Copyright (c) 2017-2024, Mudita Sp. z.o.o. All rights reserved.
// For licensing, see https://github.com/mudita/MuditaOS/LICENSE.md

#include <AudioMessage.hpp>


@@ 171,15 171,19 @@ void ServiceAudio::ProcessCloseReason(sys::CloseReason closeReason)

std::optional<std::string> ServiceAudio::AudioServicesCallback(const sys::Message *msg)
{
    if (const auto *eof = dynamic_cast<const AudioServiceMessage::EndOfFile *>(msg); eof) {
        bus.sendUnicast(std::make_shared<AudioInternalEOFNotificationMessage>(eof->GetToken()), service::name::audio);
    if (const auto eofMsg = dynamic_cast<const AudioServiceMessage::EndOfFile *>(msg); eofMsg) {
        bus.sendUnicast(std::make_shared<AudioInternalEOFNotificationMessage>(eofMsg->GetToken()),
                        service::name::audio);
    }
    else if (const auto *dbReq = dynamic_cast<const AudioServiceMessage::DbRequest *>(msg); dbReq) {

        const auto selectedPlayback = generatePlayback(dbReq->playback, dbReq->setting);
        const auto selectedProfile  = generateProfile(dbReq->profile, dbReq->playback);
    else if (const auto fileDeletedMsg = dynamic_cast<const AudioServiceMessage::FileDeleted *>(msg); fileDeletedMsg) {
        bus.sendUnicast(std::make_shared<AudioInternalFileDeletedNotificationMessage>(fileDeletedMsg->GetToken()),
                        service::name::audio);
    }
    else if (const auto dbRequestMsg = dynamic_cast<const AudioServiceMessage::DbRequest *>(msg); dbRequestMsg) {
        const auto selectedPlayback = generatePlayback(dbRequestMsg->playback, dbRequestMsg->setting);
        const auto selectedProfile  = generateProfile(dbRequestMsg->profile, dbRequestMsg->playback);

        const auto &path = dbPath(dbReq->setting, selectedPlayback, selectedProfile);
        const auto &path = dbPath(dbRequestMsg->setting, selectedPlayback, selectedProfile);
        LOG_DEBUG("ServiceAudio::DBbCallback(%s)", path.c_str());
        const auto settings_it = settingsCache.find(path);



@@ 207,7 211,7 @@ std::optional<std::string> ServiceAudio::AudioServicesCallback(const sys::Messag
        }
    }
    else {
        LOG_DEBUG("Message received but not handled - no effect.");
        LOG_DEBUG("Message received but not handled - no effect");
    }

    return std::nullopt;


@@ 553,19 557,25 @@ auto ServiceAudio::StopInput(audio::AudioMux::Input *input, StopReason stopReaso
    if (input->audio->GetCurrentState() == Audio::State::Idle) {
        return audio::RetCode::Success;
    }
    const auto rCode = input->audio->Stop();
    const auto retCode = input->audio->Stop();

    // Send notification that audio file was stopped
    std::shared_ptr<AudioNotificationMessage> msg;
    if (stopReason == StopReason::Eof) {
    switch (stopReason) {
    case StopReason::Eof:
        msg = std::make_shared<AudioEOFNotification>(input->token);
    }
    else {
        break;
    case StopReason::FileDeleted:
        msg = std::make_shared<AudioFileDeletedNotification>(input->token);
        break;
    default:
        msg = std::make_shared<AudioStopNotification>(input->token);
        break;
    }
    bus.sendMulticast(std::move(msg), sys::BusChannel::ServiceAudioNotifications);
    audioMux.ResetInput(input);
    VibrationUpdate();
    return rCode;
    return retCode;
}

void ServiceAudio::HandleEOF(const Token &token)


@@ 583,6 593,13 @@ void ServiceAudio::HandleEOF(const Token &token)
    }
}

void ServiceAudio::HandleFileDeleted(const audio::Token &token)
{
    if (const auto input = audioMux.GetInput(token); input) {
        StopInput(*input, StopReason::FileDeleted);
    }
}

auto ServiceAudio::HandleKeyPressed(const int step) -> sys::MessagePointer
{
    auto context = getCurrentContext();


@@ 651,6 668,10 @@ sys::MessagePointer ServiceAudio::DataReceivedHandler(sys::DataMessage *msgl, sy
        auto *msg = static_cast<AudioInternalEOFNotificationMessage *>(msgl);
        HandleEOF(msg->token);
    }
    else if (msgType == typeid(AudioInternalFileDeletedNotificationMessage)) {
        auto *msg = static_cast<AudioInternalEOFNotificationMessage *>(msgl);
        HandleFileDeleted(msg->token);
    }
    else if (msgType == typeid(AudioGetSetting)) {
        auto *msg   = static_cast<AudioGetSetting *>(msgl);
        auto value  = getSetting(msg->setting, Profile::Type::Idle, msg->playbackType);

M module-services/service-audio/include/service-audio/AudioMessage.hpp => module-services/service-audio/include/service-audio/AudioMessage.hpp +18 -2
@@ 1,4 1,4 @@
// Copyright (c) 2017-2023, Mudita Sp. z.o.o. All rights reserved.
// Copyright (c) 2017-2024, Mudita Sp. z.o.o. All rights reserved.
// For licensing, see https://github.com/mudita/MuditaOS/LICENSE.md

#pragma once


@@ 35,7 35,16 @@ class AudioResponseMessage : public sys::ResponseMessage
class AudioInternalEOFNotificationMessage : public AudioMessage
{
  public:
    explicit AudioInternalEOFNotificationMessage(audio::Token token) : token(token)
    explicit AudioInternalEOFNotificationMessage(audio::Token token) : token{token}
    {}

    const audio::Token token;
};

class AudioInternalFileDeletedNotificationMessage : public AudioMessage
{
  public:
    explicit AudioInternalFileDeletedNotificationMessage(audio::Token token) : token{token}
    {}

    const audio::Token token;


@@ 64,6 73,13 @@ class AudioEOFNotification : public AudioNotificationMessage
    {}
};

class AudioFileDeletedNotification : public AudioNotificationMessage
{
  public:
    explicit AudioFileDeletedNotification(audio::Token token) : AudioNotificationMessage{token}
    {}
};

class AudioPausedNotification : public AudioNotificationMessage
{
  public:

M module-services/service-audio/include/service-audio/ServiceAudio.hpp => module-services/service-audio/include/service-audio/ServiceAudio.hpp +3 -1
@@ 1,4 1,4 @@
// Copyright (c) 2017-2023, Mudita Sp. z.o.o. All rights reserved.
// Copyright (c) 2017-2024, Mudita Sp. z.o.o. All rights reserved.
// For licensing, see https://github.com/mudita/MuditaOS/LICENSE.md

#pragma once


@@ 80,6 80,7 @@ class ServiceAudio : public sys::Service
    enum class StopReason
    {
        Eof,
        FileDeleted,
        Other
    };



@@ 89,6 90,7 @@ class ServiceAudio : public sys::Service
    auto HandlePause(std::optional<audio::AudioMux::Input *> input) -> std::unique_ptr<AudioResponseMessage>;
    auto HandleResume(const audio::Token &token) -> std::unique_ptr<AudioResponseMessage>;
    void HandleEOF(const audio::Token &token);
    void HandleFileDeleted(const audio::Token &token);
    auto HandleKeyPressed(const int step) -> sys::MessagePointer;
    void MuteCurrentOperation();
    void VibrationUpdate(const audio::PlaybackType &type               = audio::PlaybackType::None,

M products/BellHybrid/apps/application-bell-relaxation/ApplicationBellRelaxation.cpp => products/BellHybrid/apps/application-bell-relaxation/ApplicationBellRelaxation.cpp +1 -4
@@ 26,12 26,9 @@
#include <common/models/BatteryModel.hpp>
#include <common/models/AudioModel.hpp>
#include <common/windows/AppsBatteryStatusWindow.hpp>
#include <audio/AudioMessage.hpp>
#include <service-db/DBNotificationMessage.hpp>
#include <system/messages/SentinelRegistrationMessage.hpp>

#include <log/log.hpp>

namespace
{
    constexpr auto relaxationRebuildTimer         = "RelaxationRebuildTimer";


@@ 44,7 41,7 @@ namespace app
                                                         std::string parent,
                                                         StatusIndicators statusIndicators,
                                                         StartInBackground startInBackground,
                                                         uint32_t stackDepth)
                                                         std::uint32_t stackDepth)
        : Application(std::move(name), std::move(parent), statusIndicators, startInBackground, stackDepth),
          audioModel{std::make_unique<AudioModel>(this)}
    {

M products/BellHybrid/apps/application-bell-relaxation/include/application-bell-relaxation/ApplicationBellRelaxation.hpp => products/BellHybrid/apps/application-bell-relaxation/include/application-bell-relaxation/ApplicationBellRelaxation.hpp +2 -2
@@ 18,7 18,6 @@ namespace gui::window::name
    inline constexpr auto relaxationEnded           = "RelaxationEndedWindow";
    inline constexpr auto relaxationLowBattery      = "RelaxationLowBatteryWindow";
    inline constexpr auto relaxationError           = "RelaxationError";

} // namespace gui::window::name
namespace app
{


@@ 26,6 25,7 @@ namespace app
    {
        class RelaxationPlayer;
    }

    inline constexpr auto applicationBellRelaxationName = "ApplicationBellRelaxation";

    class ApplicationBellRelaxation : public Application


@@ 46,7 46,7 @@ namespace app
                                  std::string parent                  = "",
                                  StatusIndicators statusIndicators   = StatusIndicators{},
                                  StartInBackground startInBackground = {false},
                                  uint32_t stackDepth                 = 4096 * 2);
                                  std::uint32_t stackDepth            = 1024 * 8);
        ~ApplicationBellRelaxation();
        sys::ReturnCodes InitHandler() override;


M products/BellHybrid/apps/application-bell-relaxation/presenter/RelaxationRunningLoopPresenter.cpp => products/BellHybrid/apps/application-bell-relaxation/presenter/RelaxationRunningLoopPresenter.cpp +9 -3
@@ 29,6 29,7 @@ namespace app::relaxation
    void RelaxationRunningLoopPresenter::activate(const db::multimedia_files::MultimediaFilesRecord &song)
    {
        Expects(timer != nullptr);

        AbstractRelaxationPlayer::PlaybackMode mode;
        const auto value = settings->getValue(timerValueDBRecordName, settings::SettingsScope::AppLocal);
        if (utils::is_number(value) && utils::getNumericValue<int>(value) != 0) {


@@ 54,12 55,17 @@ namespace app::relaxation
            }
            else {
                getView()->handleError();
                return;
            }
        };
        auto onErrorCallback = [this]() { getView()->handleDeletedFile(); };

        player.start(song.fileInfo.path, mode, std::move(onStartCallback), std::move(onErrorCallback));
        auto onFinishedCallback = [this](AbstractAudioModel::PlaybackFinishStatus status) {
            if (status == AbstractAudioModel::PlaybackFinishStatus::Error) {
                timer->stop();
                getView()->handleDeletedFile(); // Deleted file is currently the only error handled by player
            }
        };

        player.start(song.fileInfo.path, mode, std::move(onStartCallback), std::move(onFinishedCallback));
    }

    void RelaxationRunningLoopPresenter::stop()

M products/BellHybrid/apps/application-bell-relaxation/presenter/RelaxationRunningProgressPresenter.cpp => products/BellHybrid/apps/application-bell-relaxation/presenter/RelaxationRunningProgressPresenter.cpp +16 -9
@@ 1,4 1,4 @@
// Copyright (c) 2017-2023, Mudita Sp. z.o.o. All rights reserved.
// Copyright (c) 2017-2024, Mudita Sp. z.o.o. All rights reserved.
// For licensing, see https://github.com/mudita/MuditaOS/LICENSE.md

#include "RelaxationRunningProgressPresenter.hpp"


@@ 10,9 10,9 @@

namespace
{
    bool songLengthEqualsToSelectedPeriod(std::chrono::minutes period, std::chrono::seconds songLength)
    bool isSongLengthEqualToPeriod(std::chrono::seconds songLength, std::chrono::minutes period)
    {
        auto periodInSeconds = std::chrono::duration_cast<std::chrono::seconds>(period);
        const auto periodInSeconds = std::chrono::duration_cast<std::chrono::seconds>(period);
        return periodInSeconds.count() == songLength.count();
    }
} // namespace


@@ 39,11 39,12 @@ namespace app::relaxation
    void RelaxationRunningProgressPresenter::activate(const db::multimedia_files::MultimediaFilesRecord &song)
    {
        Expects(timer != nullptr);

        AbstractRelaxationPlayer::PlaybackMode mode;
        const auto value      = settings->getValue(timerValueDBRecordName, settings::SettingsScope::AppLocal);
        const auto songLength = std::chrono::seconds{song.audioProperties.songLength};
        if (utils::is_number(value) && utils::getNumericValue<int>(value) != 0 &&
            !songLengthEqualsToSelectedPeriod(std::chrono::minutes{utils::getNumericValue<int>(value)}, songLength)) {
        if (utils::is_number(value) && (utils::getNumericValue<int>(value) != 0) &&
            !isSongLengthEqualToPeriod(songLength, std::chrono::minutes{utils::getNumericValue<int>(value)})) {
            const auto playbackTimeInMinutes = std::chrono::minutes{utils::getNumericValue<int>(value)};
            timer->reset(playbackTimeInMinutes);
            mode = AbstractRelaxationPlayer::PlaybackMode::Looped;


@@ 58,18 59,24 @@ namespace app::relaxation
                return;
            }
        }

        auto onStartCallback = [this](audio::RetCode retCode) {
            if (retCode == audio::RetCode::Success) {
                timer->start();
            }
            else {
                getView()->handleError();
                return;
            }
        };

        auto onErrorCallback = [this]() { getView()->handleDeletedFile(); };
        player.start(song.fileInfo.path, mode, std::move(onStartCallback), std::move(onErrorCallback));
        auto onFinishedCallback = [this](AbstractAudioModel::PlaybackFinishStatus status) {
            if (status == AbstractAudioModel::PlaybackFinishStatus::Error) {
                timer->stop();
                getView()->handleDeletedFile(); // Deleted file is currently the only error handled by player
            }
        };

        player.start(song.fileInfo.path, mode, std::move(onStartCallback), std::move(onFinishedCallback));
    }

    void RelaxationRunningProgressPresenter::stop()


@@ 90,7 97,7 @@ namespace app::relaxation

    void RelaxationRunningProgressPresenter::pause()
    {
        if (not timer->isStopped()) {
        if (!timer->isStopped()) {
            auto onPauseCallback = [this](audio::RetCode retCode) {
                if (retCode == audio::RetCode::Success) {
                    timer->stop();

M products/BellHybrid/apps/application-bell-relaxation/presenter/RelaxationRunningProgressPresenter.hpp => products/BellHybrid/apps/application-bell-relaxation/presenter/RelaxationRunningProgressPresenter.hpp +3 -1
@@ 1,4 1,4 @@
// Copyright (c) 2017-2023, Mudita Sp. z.o.o. All rights reserved.
// Copyright (c) 2017-2024, Mudita Sp. z.o.o. All rights reserved.
// For licensing, see https://github.com/mudita/MuditaOS/LICENSE.md

#pragma once


@@ 16,10 16,12 @@ namespace app
    class AbstractBatteryModel;
    class ApplicationCommon;
} // namespace app

namespace gui
{
    class Item;
} // namespace gui

namespace settings
{
    class Settings;

M products/BellHybrid/apps/application-bell-relaxation/widgets/RelaxationPlayer.cpp => products/BellHybrid/apps/application-bell-relaxation/widgets/RelaxationPlayer.cpp +25 -14
@@ 1,4 1,4 @@
// Copyright (c) 2017-2023, Mudita Sp. z.o.o. All rights reserved.
// Copyright (c) 2017-2024, Mudita Sp. z.o.o. All rights reserved.
// For licensing, see https://github.com/mudita/MuditaOS/LICENSE.md

#include "RelaxationPlayer.hpp"


@@ 10,48 10,59 @@ namespace app::relaxation
    {
        return playbackMode;
    }

    RelaxationPlayer::RelaxationPlayer(AbstractAudioModel &audioModel) : audioModel{audioModel}
    {}

    void RelaxationPlayer::start(const std::string &filePath,
                                 AbstractRelaxationPlayer::PlaybackMode mode,
                                 AbstractAudioModel::OnStateChangeCallback &&callback,
                                 AbstractAudioModel::OnStateChangeCallback &&stateChangeCallback,
                                 AbstractAudioModel::OnPlaybackFinishedCallback &&finishedCallback)
    {
        using Status = AbstractAudioModel::PlaybackFinishStatus;
        using Type   = AbstractAudioModel::PlaybackType;

        recentFilePath = filePath;
        playbackMode   = mode;
        audioModel.play(filePath, AbstractAudioModel::PlaybackType::Multimedia, std::move(callback));

        auto finishedCb = [_callback = finishedCallback, this]() {
            auto cb = [&](audio::RetCode retCode) {
                if (retCode != audio::RetCode::Success) {
                    _callback();
                }
            };
            if (playbackMode == PlaybackMode::Looped) {
                audioModel.play(recentFilePath, AbstractAudioModel::PlaybackType::Multimedia, std::move(cb));

        auto onPlayerFinished = [callback = finishedCallback, this](Status status) {
            if (status == Status::Error) {
                callback(status); // First playback finished with error
            }
            else if (playbackMode == PlaybackMode::Looped) {
                audioModel.play(recentFilePath, Type::Multimedia, [&](audio::RetCode retCode) { // Replay in loop mode
                    if (retCode != audio::RetCode::Success) {
                        callback(Status::Error); // Replay fail in looped mode
                    }
                });
            }
            else {
                _callback();
                callback(Status::Normal); // Normal finish in single shot mode
            }
        };

        audioModel.setPlaybackFinishedCb(std::move(finishedCb));
        audioModel.setPlaybackFinishedCb(std::move(onPlayerFinished));
        audioModel.play(filePath, Type::Multimedia, std::move(stateChangeCallback));
    }

    void RelaxationPlayer::stop(AbstractAudioModel::OnStateChangeCallback &&callback)
    {
        paused = false;
        audioModel.stopPlayedByThis(std::move(callback));
    }

    void RelaxationPlayer::pause(AbstractAudioModel::OnStateChangeCallback &&callback)
    {
        paused = true;
        audioModel.pause(std::move(callback));
    }

    void RelaxationPlayer::resume(AbstractAudioModel::OnStateChangeCallback &&callback)
    {
        paused = false;
        audioModel.resume(std::move(callback));
    }

    bool RelaxationPlayer::isPaused()
    {
        return paused;

M products/BellHybrid/apps/application-bell-relaxation/widgets/RelaxationPlayer.hpp => products/BellHybrid/apps/application-bell-relaxation/widgets/RelaxationPlayer.hpp +2 -1
@@ 1,4 1,4 @@
// Copyright (c) 2017-2023, Mudita Sp. z.o.o. All rights reserved.
// Copyright (c) 2017-2024, Mudita Sp. z.o.o. All rights reserved.
// For licensing, see https://github.com/mudita/MuditaOS/LICENSE.md

#pragma once


@@ 9,6 9,7 @@ namespace app
{
    class ApplicationCommon;
}

namespace service
{
    class AudioEOFNotification;

M products/BellHybrid/apps/common/include/common/models/AbstractAudioModel.hpp => products/BellHybrid/apps/common/include/common/models/AbstractAudioModel.hpp +15 -10
@@ 1,4 1,4 @@
// Copyright (c) 2017-2023, Mudita Sp. z.o.o. All rights reserved.
// Copyright (c) 2017-2024, Mudita Sp. z.o.o. All rights reserved.
// For licensing, see https://github.com/mudita/MuditaOS/LICENSE.md

#pragma once


@@ 14,14 14,6 @@ namespace app
    class AbstractAudioModel
    {
      public:
        /// 0-15 range
        static constexpr auto minVolume  = 1;
        static constexpr auto maxVolume  = 15;
        using Volume                     = std::uint32_t;
        using OnStateChangeCallback      = std::function<void(const audio::RetCode code)>;
        using OnGetValueCallback         = std::function<void(const audio::RetCode, Volume)>;
        using OnPlaybackFinishedCallback = std::function<void()>;

        enum class PlaybackType
        {
            Multimedia,


@@ 32,6 24,20 @@ namespace app
            Meditation
        };

        enum class PlaybackFinishStatus
        {
            Normal,
            Error
        };

        /// 0-15 range
        static constexpr auto minVolume  = 1;
        static constexpr auto maxVolume  = 15;
        using Volume                     = std::uint32_t;
        using OnStateChangeCallback      = std::function<void(const audio::RetCode code)>;
        using OnGetValueCallback         = std::function<void(const audio::RetCode, Volume)>;
        using OnPlaybackFinishedCallback = std::function<void(PlaybackFinishStatus)>;

        virtual ~AbstractAudioModel() noexcept                                                              = default;
        virtual void setVolume(Volume volume, PlaybackType playbackType, OnStateChangeCallback &&callback)  = 0;
        virtual std::optional<Volume> getVolume(PlaybackType playbackType)                                  = 0;


@@ 47,5 53,4 @@ namespace app
        virtual void setPlaybackFinishedCb(OnPlaybackFinishedCallback &&callback)                           = 0;
        virtual bool hasPlaybackFinished()                                                                  = 0;
    };

} // namespace app

M products/BellHybrid/apps/common/include/common/models/AudioModel.hpp => products/BellHybrid/apps/common/include/common/models/AudioModel.hpp +5 -3
@@ 1,4 1,4 @@
// Copyright (c) 2017-2023, Mudita Sp. z.o.o. All rights reserved.
// Copyright (c) 2017-2024, Mudita Sp. z.o.o. All rights reserved.
// For licensing, see https://github.com/mudita/MuditaOS/LICENSE.md

#pragma once


@@ 30,9 30,11 @@ namespace app

      private:
        audio::Token lastPlayedToken = audio::Token::MakeBadToken();
        void stop(audio::Token token, OnStateChangeCallback &&callback);
        std::function<void()> playbackFinishedCallback{nullptr};
        bool playbackFinishedFlag{false};
        ApplicationCommon *app{};

        OnPlaybackFinishedCallback playbackFinishedCallback{nullptr};

        void stop(audio::Token token, OnStateChangeCallback &&callback);
    };
} // namespace app

M products/BellHybrid/apps/common/include/common/widgets/BellConnectionStatus.hpp => products/BellHybrid/apps/common/include/common/widgets/BellConnectionStatus.hpp +1 -2
@@ 1,4 1,4 @@
// Copyright (c) 2017-2023, Mudita Sp. z.o.o. All rights reserved.
// Copyright (c) 2017-2024, Mudita Sp. z.o.o. All rights reserved.
// For licensing, see https://github.com/mudita/MuditaOS/LICENSE.md

#pragma once


@@ 9,7 9,6 @@

namespace gui
{

    class BellConnectionStatus : public gui::HBox
    {
      public:

M products/BellHybrid/apps/common/src/AudioModel.cpp => products/BellHybrid/apps/common/src/AudioModel.cpp +23 -12
@@ 1,4 1,4 @@
// Copyright (c) 2017-2023, Mudita Sp. z.o.o. All rights reserved.
// Copyright (c) 2017-2024, Mudita Sp. z.o.o. All rights reserved.
// For licensing, see https://github.com/mudita/MuditaOS/LICENSE.md

#include "models/AudioModel.hpp"


@@ 50,27 50,35 @@ namespace
        LOG_ERROR("Command %d Failed with %d error", msgType, static_cast<int>(ret.first));
        return std::make_shared<service::AudioResponseMessage>(audio::RetCode::Failed);
    }

} // namespace

namespace app
{

    AudioModel::AudioModel(ApplicationCommon *app) : app::AsyncCallbackReceiver{app}, app{app}
    {
        app->connect(typeid(service::AudioEOFNotification), [&](sys::Message *msg) -> sys::MessagePointer {
            playbackFinishedFlag = true;
            if (playbackFinishedCallback) {
                playbackFinishedCallback();
            }

            return sys::msgHandled();
        });
        app->connect(typeid(service::AudioEOFNotification),
                     [&]([[maybe_unused]] sys::Message *msg) -> sys::MessagePointer {
                         playbackFinishedFlag = true;
                         if (playbackFinishedCallback) {
                             playbackFinishedCallback(AbstractAudioModel::PlaybackFinishStatus::Normal);
                         }
                         return sys::msgHandled();
                     });

        app->connect(typeid(service::AudioFileDeletedNotification),
                     [&]([[maybe_unused]] sys::Message *msg) -> sys::MessagePointer {
                         playbackFinishedFlag = true;
                         if (playbackFinishedCallback) {
                             playbackFinishedCallback(AbstractAudioModel::PlaybackFinishStatus::Error);
                         }
                         return sys::msgHandled();
                     });
    }

    AudioModel::~AudioModel()
    {
        app->disconnect(typeid(service::AudioEOFNotification));
        app->disconnect(typeid(service::AudioFileDeletedNotification));
    }

    void AudioModel::play(const std::string &filePath,


@@ 146,6 154,7 @@ namespace app
        };
        task->execute(app, this, std::move(cb));
    }

    void AudioModel::pause(OnStateChangeCallback &&callback)
    {
        auto msg  = std::make_unique<service::AudioPauseRequest>();


@@ 164,6 173,7 @@ namespace app
        };
        task->execute(app, this, std::move(cb));
    }

    void AudioModel::resume(OnStateChangeCallback &&callback)
    {
        auto msg  = std::make_unique<service::AudioResumeRequest>();


@@ 181,6 191,7 @@ namespace app
        };
        task->execute(app, this, std::move(cb));
    }

    void AudioModel::getVolume(AbstractAudioModel::PlaybackType playbackType,
                               AbstractAudioModel::OnGetValueCallback &&callback)
    {


@@ 200,6 211,7 @@ namespace app
        };
        task->execute(app, this, std::move(cb));
    }

    std::optional<AbstractAudioModel::Volume> AudioModel::getVolume(AbstractAudioModel::PlaybackType playbackType)
    {
        auto msg       = std::make_shared<service::AudioGetVolume>(convertPlaybackType(playbackType));


@@ 220,5 232,4 @@ namespace app
    {
        return playbackFinishedFlag;
    }

} // namespace app

M products/BellHybrid/apps/common/src/widgets/BellConnectionStatus.cpp => products/BellHybrid/apps/common/src/widgets/BellConnectionStatus.cpp +3 -5
@@ 1,4 1,4 @@
// Copyright (c) 2017-2023, Mudita Sp. z.o.o. All rights reserved.
// Copyright (c) 2017-2024, Mudita Sp. z.o.o. All rights reserved.
// For licensing, see https://github.com/mudita/MuditaOS/LICENSE.md

#include <EventStore.hpp>


@@ 9,9 9,8 @@
namespace
{
    constexpr auto usb_connected_status     = "app_bellmain_usb_status_connected";
    constexpr inline auto status_text_max_w = 350U;
    constexpr inline auto status_text_max_h = 102U;

    constexpr auto status_text_max_w        = 350U;
    constexpr auto status_text_max_h        = 102U;
} // namespace

namespace gui


@@ 46,5 45,4 @@ namespace gui
    {
        statusText->setVisible(true);
    }

} // namespace gui

M products/BellHybrid/services/audio/ServiceAudio.cpp => products/BellHybrid/services/audio/ServiceAudio.cpp +64 -28
@@ 1,4 1,4 @@
// Copyright (c) 2017-2023, Mudita Sp. z.o.o. All rights reserved.
// Copyright (c) 2017-2024, Mudita Sp. z.o.o. All rights reserved.
// For licensing, see https://github.com/mudita/MuditaOS/LICENSE.md

#include "ServiceAudio.hpp"


@@ 12,7 12,7 @@ namespace
{
    // 4kB is too small because internally drflac_open() uses cache which by default has 4kB.
    // Alternatively smaller DR_FLAC_BUFFER_SIZE could be defined.
    constexpr auto stackSize            = 1024 * 8;
    constexpr auto serviceAudioStackSize = 1024 * 8;
    constexpr auto defaultVolume        = "11";
    constexpr auto defaultSnoozeVolume  = "10";
    constexpr auto defaultBedtimeVolume = "12";


@@ 26,18 26,18 @@ namespace
        using namespace audio;
        // clang-format off
        constexpr std::initializer_list<std::pair<audio::DbPathElement, const char *>> values{
            {DbPathElement{Setting::Volume, PlaybackType::Meditation, Profile::Type::PlaybackLoudspeaker},defaultVolume},
            {DbPathElement{Setting::Volume, PlaybackType::Multimedia, Profile::Type::PlaybackLoudspeaker},defaultVolume},
            {DbPathElement{Setting::Volume, PlaybackType::Meditation, Profile::Type::PlaybackLoudspeaker}, defaultVolume},
            {DbPathElement{Setting::Volume, PlaybackType::Multimedia, Profile::Type::PlaybackLoudspeaker}, defaultVolume},
            {DbPathElement{Setting::Volume, PlaybackType::Alarm, Profile::Type::PlaybackLoudspeaker}, defaultVolume},
            {DbPathElement{Setting::Volume, PlaybackType::Bedtime, Profile::Type::PlaybackLoudspeaker}, defaultBedtimeVolume},
            {DbPathElement{Setting::Volume, PlaybackType::PreWakeUp, Profile::Type::PlaybackLoudspeaker},defaultSnoozeVolume},
            {DbPathElement{Setting::Volume, PlaybackType::PreWakeUp, Profile::Type::PlaybackLoudspeaker}, defaultSnoozeVolume},
            {DbPathElement{Setting::Volume, PlaybackType::Snooze, Profile::Type::PlaybackLoudspeaker}, defaultSnoozeVolume},

            /// Profiles below are not used but unfortunately, must exist in order to satisfy audio module requirements
            {DbPathElement{Setting::Volume, PlaybackType::Meditation, Profile::Type::PlaybackHeadphones},defaultVolume},
            {DbPathElement{Setting::Volume, PlaybackType::Meditation, Profile::Type::PlaybackBluetoothA2DP},defaultVolume},
            {DbPathElement{Setting::Volume, PlaybackType::Multimedia, Profile::Type::PlaybackHeadphones},defaultVolume},
            {DbPathElement{Setting::Volume, PlaybackType::Multimedia, Profile::Type::PlaybackBluetoothA2DP},defaultVolume},
            {DbPathElement{Setting::Volume, PlaybackType::Meditation, Profile::Type::PlaybackHeadphones}, defaultVolume},
            {DbPathElement{Setting::Volume, PlaybackType::Meditation, Profile::Type::PlaybackBluetoothA2DP}, defaultVolume},
            {DbPathElement{Setting::Volume, PlaybackType::Multimedia, Profile::Type::PlaybackHeadphones}, defaultVolume},
            {DbPathElement{Setting::Volume, PlaybackType::Multimedia, Profile::Type::PlaybackBluetoothA2DP}, defaultVolume},
            {DbPathElement{Setting::Volume, PlaybackType::Alarm, Profile::Type::PlaybackHeadphones}, defaultVolume},
            {DbPathElement{Setting::Volume, PlaybackType::Alarm, Profile::Type::PlaybackBluetoothA2DP}, defaultVolume},
            {DbPathElement{Setting::Volume, PlaybackType::Bedtime, Profile::Type::PlaybackHeadphones}, defaultVolume},


@@ 50,19 50,29 @@ namespace
        // clang-format on
    } // namespace initializer

    class AudioInternalEOFNotificationMessage : public service::AudioNotificationMessage
    namespace internal
    {
      public:
        explicit AudioInternalEOFNotificationMessage(audio::Token token) : AudioNotificationMessage(token)
        {}
    };
        class AudioEOFNotificationMessage : public service::AudioNotificationMessage
        {
          public:
            explicit AudioEOFNotificationMessage(audio::Token token) : AudioNotificationMessage(token)
            {}
        };

        class AudioFileDeletedNotificationMessage : public service::AudioNotificationMessage
        {
          public:
            explicit AudioFileDeletedNotificationMessage(audio::Token token) : AudioNotificationMessage(token)
            {}
        };
    } // namespace internal
} // namespace

namespace service
{
    Audio::Audio()
        : sys::Service(audioServiceName, "", stackSize, sys::ServicePriority::Idle),
          audioMux([this](auto... params) { return this->AudioServicesCallback(params...); }),
        : sys::Service(audioServiceName, "", serviceAudioStackSize, sys::ServicePriority::Idle),
          audioMux([this](auto... params) { return AudioServicesCallback(params...); }),
          cpuSentinel(std::make_shared<sys::CpuSentinel>(audioServiceName, this)),
          settingsProvider(std::make_unique<settings::Settings>())
    {


@@ 86,12 96,19 @@ namespace service
            return handleStart(audio::Operation::Type::Playback, msgl->fadeIn, msgl->fileName, msgl->playbackType);
        });

        connect(typeid(AudioInternalEOFNotificationMessage), [this](sys::Message *msg) -> sys::MessagePointer {
            auto *msgl = static_cast<AudioInternalEOFNotificationMessage *>(msg);
        connect(typeid(internal::AudioEOFNotificationMessage), [this](sys::Message *msg) -> sys::MessagePointer {
            auto *msgl = static_cast<internal::AudioEOFNotificationMessage *>(msg);
            handleEOF(msgl->token);
            return sys::msgHandled();
        });

        connect(typeid(internal::AudioFileDeletedNotificationMessage),
                [this](sys::Message *msg) -> sys::MessagePointer {
                    auto *msgl = static_cast<internal::AudioEOFNotificationMessage *>(msg);
                    handleFileDeleted(msgl->token);
                    return sys::msgHandled();
                });

        connect(typeid(AudioStopRequest), [this](sys::Message *msg) -> sys::MessagePointer {
            volumeFadeIn->Stop();
            auto *msgl = static_cast<AudioStopRequest *>(msg);


@@ 211,13 228,19 @@ namespace service
            return audio::RetCode::Success;
        }
        const auto retCode = input->audio->Stop();

        // Send notification that audio file was stopped
        std::shared_ptr<AudioNotificationMessage> msg;
        if (stopReason == StopReason::Eof) {
        switch (stopReason) {
        case StopReason::Eof:
            msg = std::make_shared<AudioEOFNotification>(input->token);
        }
        else {
            break;
        case StopReason::FileDeleted:
            msg = std::make_shared<AudioFileDeletedNotification>(input->token);
            break;
        default:
            msg = std::make_shared<AudioStopNotification>(input->token);
            break;
        }
        bus.sendMulticast(std::move(msg), sys::BusChannel::ServiceAudioNotifications);
        audioMux.ResetInput(input);


@@ 257,23 280,36 @@ namespace service
        }
    }

    void Audio::handleFileDeleted(const audio::Token &token)
    {
        if (const auto input = audioMux.GetInput(token); input) {
            stopInput(*input, StopReason::FileDeleted);
        }
    }

    auto Audio::AudioServicesCallback(const sys::Message *msg) -> std::optional<std::string>
    {
        std::optional<std::string> ret;
        if (const auto *eof = dynamic_cast<const AudioServiceMessage::EndOfFile *>(msg); eof) {
            bus.sendUnicast(std::make_shared<AudioInternalEOFNotificationMessage>(eof->GetToken()), audioServiceName);
        if (const auto eofMsg = dynamic_cast<const AudioServiceMessage::EndOfFile *>(msg); eofMsg) {
            bus.sendUnicast(std::make_shared<internal::AudioEOFNotificationMessage>(eofMsg->GetToken()),
                            audioServiceName);
        }
        else if (const auto fileDeletedMsg = dynamic_cast<const AudioServiceMessage::FileDeleted *>(msg);
                 fileDeletedMsg) {
            bus.sendUnicast(std::make_shared<internal::AudioFileDeletedNotificationMessage>(fileDeletedMsg->GetToken()),
                            audioServiceName);
        }
        else if (const auto *dbReq = dynamic_cast<const AudioServiceMessage::DbRequest *>(msg); dbReq) {
            auto selectedPlayback = dbReq->playback;
            auto selectedProfile  = dbReq->profile;
        else if (const auto dbRequestMsg = dynamic_cast<const AudioServiceMessage::DbRequest *>(msg); dbRequestMsg) {
            auto selectedPlayback = dbRequestMsg->playback;
            auto selectedProfile  = dbRequestMsg->profile;
            if (const auto result =
                    settingsProvider->getValue(dbPath(dbReq->setting, selectedPlayback, selectedProfile));
                    settingsProvider->getValue(dbPath(dbRequestMsg->setting, selectedPlayback, selectedProfile));
                not result.empty()) {
                ret.emplace(result);
            }
        }
        else {
            LOG_DEBUG("Message received but not handled - no effect.");
            LOG_DEBUG("Message received but not handled - no effect");
        }

        return ret;

M products/BellHybrid/services/audio/include/audio/AudioMessage.hpp => products/BellHybrid/services/audio/include/audio/AudioMessage.hpp +8 -1
@@ 1,4 1,4 @@
// Copyright (c) 2017-2023, Mudita Sp. z.o.o. All rights reserved.
// Copyright (c) 2017-2024, Mudita Sp. z.o.o. All rights reserved.
// For licensing, see https://github.com/mudita/MuditaOS/LICENSE.md

#pragma once


@@ 55,6 55,13 @@ namespace service
        {}
    };

    class AudioFileDeletedNotification : public AudioNotificationMessage
    {
      public:
        explicit AudioFileDeletedNotification(audio::Token token) : AudioNotificationMessage{token}
        {}
    };

    class AudioSettingsMessage : public AudioMessage
    {
      public:

M products/BellHybrid/services/audio/include/audio/ServiceAudio.hpp => products/BellHybrid/services/audio/include/audio/ServiceAudio.hpp +3 -1
@@ 1,4 1,4 @@
// Copyright (c) 2017-2023, Mudita Sp. z.o.o. All rights reserved.
// Copyright (c) 2017-2024, Mudita Sp. z.o.o. All rights reserved.
// For licensing, see https://github.com/mudita/MuditaOS/LICENSE.md

#pragma once


@@ 37,6 37,7 @@ namespace service
        enum class StopReason
        {
            Eof,
            FileDeleted,
            Other
        };



@@ 57,6 58,7 @@ namespace service
        auto handleResume() -> std::unique_ptr<AudioResponseMessage>;

        void handleEOF(const audio::Token &token);
        void handleFileDeleted(const audio::Token &token);

        auto AudioServicesCallback(const sys::Message *msg) -> std::optional<std::string>;


M third-party/dr_libs/src => third-party/dr_libs/src +1 -1
@@ 1,1 1,1 @@
Subproject commit 4d577ffbba6979fcf198a5c42825ad33f542e5a7
Subproject commit 190a6a50ca40879ffddcd054c417d64b7dd6ff99