~aleteoryx/muditaos

1cf85c7686dd74cbe60a76fe9664256d96aeb835 — Dawid Wojtas 1 year, 6 months ago 92e042a
[BH-2049] Gapless audio transition

* add minimp3 for decoding mp3 files
* add a gapless transition between the end and beginning of the track
* add a loop option to the playback mode
34 files changed, 177 insertions(+), 159 deletions(-)

M .gitmodules
M harmony_changelog.md
M module-audio/Audio/Audio.cpp
M module-audio/Audio/Audio.hpp
M module-audio/Audio/AudioCommon.hpp
M module-audio/Audio/Operation/Operation.cpp
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/decoder/DecoderMP3.cpp
M module-audio/Audio/decoder/DecoderMP3.hpp
M module-audio/Audio/decoder/DecoderWorker.cpp
M module-audio/Audio/decoder/DecoderWorker.hpp
M products/BellHybrid/CMakeLists.txt
M products/BellHybrid/alarms/src/actions/PlayAudioActions.cpp
M products/BellHybrid/alarms/src/actions/PlayAudioActions.hpp
M products/BellHybrid/apps/application-bell-focus-timer/presenter/FocusSettingsPresenter.cpp
M products/BellHybrid/apps/application-bell-meditation-timer/presenter/SettingsPresenter.cpp
M products/BellHybrid/apps/application-bell-powernap/presenter/PowerNapProgressPresenter.cpp
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/widgets/RelaxationPlayer.cpp
M products/BellHybrid/apps/application-bell-relaxation/widgets/RelaxationPlayer.hpp
M products/BellHybrid/apps/application-bell-settings/presenter/BedtimeSettingsPresenter.cpp
M products/BellHybrid/apps/application-bell-settings/presenter/alarm_settings/AlarmSettingsPresenter.cpp
M products/BellHybrid/apps/application-bell-settings/presenter/alarm_settings/PrewakeUpPresenter.cpp
M products/BellHybrid/apps/application-bell-settings/presenter/alarm_settings/SnoozePresenter.cpp
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/src/AudioModel.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/minimp3/minimp3
M .gitmodules => .gitmodules +4 -4
@@ 59,10 59,6 @@
[submodule "CrashDebug"]
	path = third-party/CrashDebug/src
	url = https://github.com/adamgreen/CrashDebug.git
[submodule "minimp3"]
	path = third-party/minimp3/minimp3
	url = ../minimp3.git
	branch = RT1051
[submodule "parallel-hashmap"]
	path = third-party/parallel-hashmap/src
	url = https://github.com/greg7mdp/parallel-hashmap.git


@@ 116,3 112,7 @@
[submodule "third-party/fakeit/FakeIt"]
	path = third-party/fakeit/FakeIt
	url = https://github.com/eranpeer/FakeIt.git
[submodule "third-party/minimp3/minimp3"]	
	path = third-party/minimp3/minimp3	
	url = https://github.com/mudita/minimp3.git	
	branch = mudita

M harmony_changelog.md => harmony_changelog.md +2 -0
@@ 12,6 12,8 @@
* Added fade in and fade out to relaxation songs
* Added time sync endpoint to be used by Mudita Center
* Added bedside lamp settings
* Added gapless audio transition in relaxation


### Changed / Improved


M module-audio/Audio/Audio.cpp => module-audio/Audio/Audio.cpp +5 -3
@@ 11,7 11,8 @@ namespace audio
{
    Audio::Audio(AudioServiceMessage::Callback callback) : currentOperation(), serviceCallback(std::move(callback))
    {
        auto ret = Operation::Create(Operation::Type::Idle, "", audio::PlaybackType::None, serviceCallback);
        auto ret = Operation::Create(
            Operation::Type::Idle, "", audio::PlaybackType::None, audio::PlaybackMode::Single, serviceCallback);
        if (ret) {
            currentOperation = std::move(ret);
        }


@@ 50,11 51,12 @@ namespace audio
    audio::RetCode Audio::Start(Operation::Type op,
                                audio::Token token,
                                const std::string &filePath,
                                const audio::PlaybackType &playbackType)
                                const audio::PlaybackType &playbackType,
                                const audio::PlaybackMode &playbackMode)
    {

        try {
            auto ret = Operation::Create(op, filePath, playbackType, serviceCallback);
            auto ret = Operation::Create(op, filePath, playbackType, playbackMode, serviceCallback);
            switch (op) {
            case Operation::Type::Playback:
                currentState = State::Playback;

M module-audio/Audio/Audio.hpp => module-audio/Audio/Audio.hpp +2 -1
@@ 97,7 97,8 @@ namespace audio
        virtual audio::RetCode Start(Operation::Type op,
                                     audio::Token token                      = audio::Token::MakeBadToken(),
                                     const std::string &filePath             = "",
                                     const audio::PlaybackType &playbackType = audio::PlaybackType::None);
                                     const audio::PlaybackType &playbackType = audio::PlaybackType::None,
                                     const audio::PlaybackMode &playbackMode = audio::PlaybackMode::Single);

        virtual audio::RetCode Start();
        virtual audio::RetCode Stop();

M module-audio/Audio/AudioCommon.hpp => module-audio/Audio/AudioCommon.hpp +6 -0
@@ 52,6 52,12 @@ namespace audio
        Disabled
    };

    enum class PlaybackMode
    {
        Single,
        Loop
    };

    enum class PlaybackType
    {
        None,

M module-audio/Audio/Operation/Operation.cpp => module-audio/Audio/Operation/Operation.cpp +3 -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

#include "Operation.hpp"


@@ 18,6 18,7 @@ namespace audio
    std::unique_ptr<Operation> Operation::Create(Operation::Type t,
                                                 const std::string &filePath,
                                                 const audio::PlaybackType &playbackType,
                                                 const PlaybackMode &playbackMode,
                                                 AudioServiceMessage::Callback callback)
    {
        std::unique_ptr<Operation> inst;


@@ 27,7 28,7 @@ namespace audio
            inst = std::make_unique<IdleOperation>(filePath);
            break;
        case Type::Playback:
            inst = std::make_unique<PlaybackOperation>(filePath, playbackType, callback);
            inst = std::make_unique<PlaybackOperation>(filePath, playbackType, playbackMode, callback);
            break;
        case Type::Router:
            inst = std::make_unique<RouterOperation>(filePath, callback);

M module-audio/Audio/Operation/Operation.hpp => module-audio/Audio/Operation/Operation.hpp +6 -4
@@ 19,7 19,8 @@ namespace audio
    {
      public:
        explicit Operation(AudioServiceMessage::Callback callback,
                           const PlaybackType &playbackType = PlaybackType::None)
                           const PlaybackType &playbackType = PlaybackType::None,
                           const PlaybackMode &playbackMode = PlaybackMode::Single)
            : playbackType(playbackType), serviceCallback(std::move(callback)), observer(serviceCallback)
        {
            factory = AudioPlatform::GetDeviceFactory();


@@ 59,9 60,10 @@ namespace audio
        virtual ~Operation() = default;

        static std::unique_ptr<Operation> Create(Type t,
                                                 const std::string &filePath            = "",
                                                 const audio::PlaybackType &operations  = audio::PlaybackType::None,
                                                 AudioServiceMessage::Callback callback = nullptr);
                                                 const std::string &filePath             = "",
                                                 const audio::PlaybackType &operations   = audio::PlaybackType::None,
                                                 const audio::PlaybackMode &playbackMode = audio::PlaybackMode::Single,
                                                 AudioServiceMessage::Callback callback  = nullptr);

        virtual audio::RetCode Start(audio::Token token)             = 0;
        virtual audio::RetCode Stop()                                = 0;

M module-audio/Audio/Operation/PlaybackOperation.cpp => module-audio/Audio/Operation/PlaybackOperation.cpp +10 -4
@@ 17,8 17,9 @@ namespace audio

    PlaybackOperation::PlaybackOperation(const std::string &filePath,
                                         const audio::PlaybackType &playbackType,
                                         const audio::PlaybackMode &playbackMode,
                                         Callback callback)
        : Operation(std::move(callback), playbackType), dec(nullptr)
        : Operation(std::move(callback), playbackType), playbackMode(playbackMode), dec(nullptr)
    {
        // order defines priority
        AddProfile(Profile::Type::PlaybackHeadphones, playbackType, false);


@@ 26,9 27,14 @@ namespace audio
        AddProfile(Profile::Type::PlaybackLoudspeaker, playbackType, true);

        endOfFileCallback = [this]() {
            state          = State::Idle;
            const auto msg = AudioServiceMessage::EndOfFile(operationToken);
            serviceCallback(&msg);
            if (this->playbackMode == audio::PlaybackMode::Single) {
                state          = State::Idle;
                const auto msg = AudioServiceMessage::EndOfFile(operationToken);
                serviceCallback(&msg);
            }
            else {
                dec->setPosition(playbackStartPosition);
            }
        };

        fileDeletedCallback = [this]() {

M module-audio/Audio/Operation/PlaybackOperation.hpp => module-audio/Audio/Operation/PlaybackOperation.hpp +3 -0
@@ 20,6 20,7 @@ namespace audio
      public:
        PlaybackOperation(const std::string &filePath,
                          const audio::PlaybackType &playbackType,
                          const audio::PlaybackMode &playbackMode,
                          AudioServiceMessage::Callback callback = nullptr);

        virtual ~PlaybackOperation();


@@ 38,6 39,8 @@ namespace audio

      private:
        static constexpr auto playbackTimeConstraint = 10ms;
        static constexpr auto playbackStartPosition  = 0U;
        audio::PlaybackMode playbackMode             = audio::PlaybackMode::Single;

        std::unique_ptr<Stream> dataStreamOut;
        std::unique_ptr<Decoder> dec;

M module-audio/Audio/decoder/DecoderMP3.cpp => module-audio/Audio/decoder/DecoderMP3.cpp +22 -66
@@ 1,76 1,36 @@
// 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
#define MINIMP3_IMPLEMENTATION
#define MINIMP3_NO_STDIO

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

namespace
{
    signed skipID3V2TagIfPresent(std::FILE *fd)
    {
        constexpr auto ID3V2FrameOffset            = 0;
        constexpr auto ID3V2FrameHeaderSize        = 10;
        constexpr auto ID3V2FrameMagicString       = "ID3";
        constexpr auto ID3V2FrameMagicStringLength = 3;
        std::uint8_t frameBuffer[ID3V2FrameHeaderSize];

        /* Seek to the beginning of the frame and read frame's header */
        if (std::fseek(fd, ID3V2FrameOffset, SEEK_SET) != 0) {
            return -EIO;
        }
        if (std::fread(frameBuffer, sizeof(*frameBuffer), ID3V2FrameHeaderSize, fd) != ID3V2FrameHeaderSize) {
            return -EIO;
        }

        /* Check magic */
        if (strncmp(reinterpret_cast<const char *>(frameBuffer), ID3V2FrameMagicString, ID3V2FrameMagicStringLength) !=
            0) {
            return 0;
        }

        /* The tag size (minus the 10-byte header) is encoded into four bytes,
         * but the most significant bit needs to be masked in each byte.
         * Those frame indices are just copied from the ID3V2 docs. */
        const auto ID3V2TagTotalSize = (((frameBuffer[6] & 0x7F) << 21) | ((frameBuffer[7] & 0x7F) << 14) |
                                        ((frameBuffer[8] & 0x7F) << 7) | ((frameBuffer[9] & 0x7F) << 0)) +
                                       ID3V2FrameHeaderSize;

        /* Skip the tag */
        if (std::fseek(fd, ID3V2FrameOffset + ID3V2TagTotalSize, SEEK_SET) != 0) {
            return -EIO;
        }
        return ID3V2TagTotalSize;
    }
} // namespace

namespace audio
{
    DecoderMP3::DecoderMP3(const std::string &filePath) : Decoder(filePath), mp3(std::make_unique<drmp3>())
    DecoderMP3::DecoderMP3(const std::string &filePath)
        : Decoder(filePath), dec(std::make_unique<mp3dec_ex_t>()), io(std::make_unique<mp3dec_io_t>())
    {
        if (fileSize == 0) {
            return;
        }

        const auto tagSkipStatus = skipID3V2TagIfPresent(fd);
        if (tagSkipStatus < 0) {
            LOG_ERROR("Failed to skip ID3V2 tag, error %d", tagSkipStatus);
        }
        else if (tagSkipStatus == 0) {
            LOG_INFO("No ID3V2 tag to skip");
        }
        io->read      = mp3Read;
        io->read_data = this;
        io->seek      = mp3Seek;
        io->seek_data = this;
        dec->io       = io.get();

        if (drmp3_init(mp3.get(), drmp3Read, drmp3Seek, this, nullptr) == DRMP3_FALSE) {
            LOG_ERROR("Unable to initialize MP3 decoder");
        if (mp3dec_ex_open_cb(dec.get(), dec->io, MP3D_SEEK_TO_SAMPLE)) {
            LOG_ERROR("Failed to open minimp3");
            return;
        }

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


@@ 80,7 40,7 @@ namespace audio
        if (!isInitialized) {
            return;
        }
        drmp3_uninit(mp3.get());
        mp3dec_ex_close(dec.get());
        isInitialized = false;
    }



@@ 90,18 50,16 @@ namespace audio
            LOG_ERROR("MP3 decoder not initialized");
            return;
        }
        const auto totalFramesCount = drmp3_get_pcm_frame_count(mp3.get());
        drmp3_seek_to_pcm_frame(mp3.get(), totalFramesCount * pos);
        position = static_cast<float>(totalFramesCount) * pos / static_cast<float>(sampleRate);
        mp3dec_ex_seek(dec.get(), pos);
    }

    std::int32_t DecoderMP3::decode(std::uint32_t samplesToRead, std::int16_t *pcmData)
    {
        const auto samplesRead = drmp3_read_pcm_frames_s16(
            mp3.get(), samplesToRead / channelCount, reinterpret_cast<drmp3_int16 *>(pcmData));
        const auto samplesRead = mp3dec_ex_read(dec.get(), reinterpret_cast<mp3d_sample_t *>(pcmData), samplesToRead);
        if (samplesRead > 0) {
            /* Calculate frame duration in seconds */
            position += static_cast<float>(samplesRead) / static_cast<float>(sampleRate);
            const auto samplesPerChannel = static_cast<float>(samplesRead) / static_cast<float>(channelCount);
            position += samplesPerChannel / static_cast<float>(sampleRate);
        }
        else if (!fileExists(fd)) {
            /* Unfortunately this second check of file existence is needed


@@ 110,10 68,10 @@ namespace audio
            LOG_WARN("File '%s' was deleted during playback!", filePath.c_str());
            return fileDeletedRetCode;
        }
        return samplesRead * channelCount;
        return samplesRead;
    }

    std::size_t DecoderMP3::drmp3Read(void *pUserData, void *pBufferOut, std::size_t bytesToRead)
    std::size_t DecoderMP3::mp3Read(void *pBufferOut, std::size_t bytesToRead, void *pUserData)
    {
        const auto decoderContext = reinterpret_cast<DecoderMP3 *>(pUserData);



@@ 126,11 84,9 @@ namespace audio
        return std::fread(pBufferOut, 1, bytesToRead, decoderContext->fd);
    }

    drmp3_bool32 DecoderMP3::drmp3Seek(void *pUserData, int offset, drmp3_seek_origin origin)
    int DecoderMP3::mp3Seek(std::uint64_t offset, void *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;
        return std::fseek(decoderContext->fd, offset, SEEK_SET);
    }
} // namespace audio

M module-audio/Audio/decoder/DecoderMP3.hpp => module-audio/Audio/decoder/DecoderMP3.hpp +8 -10
@@ 4,7 4,7 @@
#pragma once

#include "Decoder.hpp"
#include <src/dr_mp3.h>
#include <minimp3_ex.h>

namespace audio
{


@@ 19,11 19,12 @@ namespace audio
        void setPosition(float pos) override;

      private:
        std::unique_ptr<drmp3> mp3;
        std::unique_ptr<mp3dec_ex_t> dec;
        std::unique_ptr<mp3dec_io_t> io;

        // Callback for when data needs to be read from the client.
        //
        // pUserData   [in]  The user data that was passed to drmp3_init() and family.
        // pUserData   [in]  The user data that was assigned to mp3dec_io_t and family.
        // pBufferOut  [out] The output buffer.
        // bytesToRead [in]  The number of bytes to read.
        //


@@ 31,19 32,16 @@ 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 drmp3Read(void *pUserData, void *pBufferOut, std::size_t bytesToRead);
        static std::size_t mp3Read(void *pBufferOut, std::size_t bytesToRead, void *pUserData);

        // Callback for when data needs to be seeked.
        //
        // pUserData [in] The user data that was passed to drmp3_init() and family.
        // pUserData [in] The user data that was assigned to mp3dec_io_t and family.
        // 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 the seek was successful.
        //
        // 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 drmp3_seek_origin_start or
        // drmp3_seek_origin_current.
        static drmp3_bool32 drmp3Seek(void *pUserData, int offset, drmp3_seek_origin origin);
        // The offset will never be negative. It relates to the beginning position.
        static int mp3Seek(std::uint64_t offset, void *pUserData);
    };
} // namespace audio

M module-audio/Audio/decoder/DecoderWorker.cpp => module-audio/Audio/decoder/DecoderWorker.cpp +3 -2
@@ 93,13 93,14 @@ void audio::DecoderWorker::pushAudioData()

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

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

M module-audio/Audio/decoder/DecoderWorker.hpp => module-audio/Audio/decoder/DecoderWorker.hpp +2 -2
@@ 37,7 37,7 @@ namespace audio
                      ChannelMode mode);
        ~DecoderWorker() override;

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

        auto enablePlayback() -> bool;
        auto disablePlayback() -> bool;


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

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


M products/BellHybrid/CMakeLists.txt => products/BellHybrid/CMakeLists.txt +2 -2
@@ 144,14 144,14 @@ download_asset_release_json(json-common-target
                            ${CMAKE_CURRENT_SOURCE_DIR}/assets/assets_common.json
                            ${SYSROOT_PATH}/system_a/
                            MuditaOSPublicAssets
                            0.0.26
                            0.0.27
                            ${MUDITA_CACHE_DIR}
    )
download_asset_release_json(json-community-target
                            ${CMAKE_CURRENT_SOURCE_DIR}/assets/assets_community.json
                            ${SYSROOT_PATH}/system_a/
                            MuditaOSPublicAssets
                            0.0.26
                            0.0.27
                            ${MUDITA_CACHE_DIR}
    )
download_asset_json(json-rt1051-target

M products/BellHybrid/alarms/src/actions/PlayAudioActions.cpp => products/BellHybrid/alarms/src/actions/PlayAudioActions.cpp +5 -4
@@ 12,10 12,11 @@ namespace alarms
{
    PlayAudioAction::PlayAudioAction(sys::Service &service,
                                     std::string_view toneSetting,
                                     audio::PlaybackType playbackType,
                                     const audio::PlaybackType &playbackType,
                                     const audio::PlaybackMode &playbackMode,
                                     std::optional<std::string_view> durationSetting)
        : service{service}, toneSetting{toneSetting}, durationSetting{durationSetting},
          playbackType{playbackType}, settings{service::ServiceProxy{service.weak_from_this()}}
        : service{service}, toneSetting{toneSetting}, durationSetting{durationSetting}, playbackType{playbackType},
          playbackMode{playbackMode}, settings{service::ServiceProxy{service.weak_from_this()}}
    {}

    auto PlayAudioAction::play(const std::filesystem::path &path, std::optional<std::chrono::minutes> duration) -> bool


@@ 30,7 31,7 @@ namespace alarms
                                       : audio::Fade::Disable;

        auto msg = std::make_shared<service::AudioStartPlaybackRequest>(
            path, playbackType, audio::FadeParams{fadeInEnabled, audio::alarmMaxFadeDuration});
            path, playbackType, playbackMode, audio::FadeParams{fadeInEnabled, audio::alarmMaxFadeDuration});
        return service.bus.sendUnicast(std::move(msg), service::audioServiceName);
    }


M products/BellHybrid/alarms/src/actions/PlayAudioActions.hpp => products/BellHybrid/alarms/src/actions/PlayAudioActions.hpp +3 -1
@@ 19,7 19,8 @@ namespace alarms
      public:
        PlayAudioAction(sys::Service &service,
                        std::string_view toneSetting,
                        audio::PlaybackType                             = audio::PlaybackType::Alarm,
                        const audio::PlaybackType &playbackType         = audio::PlaybackType::Alarm,
                        const audio::PlaybackMode &playbackMode         = audio::PlaybackMode::Single,
                        std::optional<std::string_view> durationSetting = {});

        auto turnOff() -> bool override;


@@ 37,6 38,7 @@ namespace alarms
        const std::string toneSetting;
        const std::optional<std::string> durationSetting;
        const audio::PlaybackType playbackType;
        const audio::PlaybackMode playbackMode;
        settings::Settings settings;
    };


M products/BellHybrid/apps/application-bell-focus-timer/presenter/FocusSettingsPresenter.cpp => products/BellHybrid/apps/application-bell-focus-timer/presenter/FocusSettingsPresenter.cpp +4 -1
@@ 146,7 146,10 @@ namespace app::focus

        auto playSound = [this, notificationVolume]() {
            this->audioModel.setVolume(notificationVolume->value(), AbstractAudioModel::PlaybackType::FocusTimer);
            this->audioModel.play(getFocusTimerAudioPath(), AbstractAudioModel::PlaybackType::FocusTimer, {});
            this->audioModel.play(getFocusTimerAudioPath(),
                                  AbstractAudioModel::PlaybackType::FocusTimer,
                                  AbstractAudioModel::PlaybackMode::Single,
                                  {});
        };

        notificationVolume->onEnter = playSound;

M products/BellHybrid/apps/application-bell-meditation-timer/presenter/SettingsPresenter.cpp => products/BellHybrid/apps/application-bell-meditation-timer/presenter/SettingsPresenter.cpp +4 -1
@@ 86,7 86,10 @@ namespace app::meditation

        auto playSound = [this, chimeVolume]() {
            this->audioModel.setVolume(chimeVolume->value(), AbstractAudioModel::PlaybackType::Meditation);
            this->audioModel.play(getMeditationGongSoundPath(), AbstractAudioModel::PlaybackType::Meditation, {});
            this->audioModel.play(getMeditationGongSoundPath(),
                                  AbstractAudioModel::PlaybackType::Meditation,
                                  AbstractAudioModel::PlaybackMode::Single,
                                  {});
        };

        chimeVolume->onEnter = playSound;

M products/BellHybrid/apps/application-bell-powernap/presenter/PowerNapProgressPresenter.cpp => products/BellHybrid/apps/application-bell-powernap/presenter/PowerNapProgressPresenter.cpp +5 -1
@@ 92,7 92,11 @@ namespace app::powernap
                                      ? audio::Fade::In
                                      : audio::Fade::Disable;

        audioModel.play(filePath, AbstractAudioModel::PlaybackType::Alarm, {}, audio::FadeParams{fadeInActive});
        audioModel.play(filePath,
                        AbstractAudioModel::PlaybackType::Alarm,
                        AbstractAudioModel::PlaybackMode::Single,
                        {},
                        audio::FadeParams{fadeInActive});
        napAlarmTimer.start();
        napFinished = true;
    }

M products/BellHybrid/apps/application-bell-relaxation/presenter/RelaxationRunningLoopPresenter.cpp => products/BellHybrid/apps/application-bell-relaxation/presenter/RelaxationRunningLoopPresenter.cpp +3 -3
@@ 30,15 30,15 @@ namespace app::relaxation
    {
        Expects(timer != nullptr);

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

            if (songLength > std::chrono::seconds::zero()) {
                timer->reset(songLength);

M products/BellHybrid/apps/application-bell-relaxation/presenter/RelaxationRunningProgressPresenter.cpp => products/BellHybrid/apps/application-bell-relaxation/presenter/RelaxationRunningProgressPresenter.cpp +3 -3
@@ 40,7 40,7 @@ namespace app::relaxation
    {
        Expects(timer != nullptr);

        AbstractRelaxationPlayer::PlaybackMode mode;
        AbstractAudioModel::PlaybackMode mode;
        const auto settingsValue  = settings->getValue(timerValueDBRecordName, settings::SettingsScope::AppLocal);
        const auto presetDuration = utils::getNumericValue<int>(settingsValue);
        const auto songLength     = std::chrono::seconds{song.audioProperties.songLength};


@@ 50,10 50,10 @@ namespace app::relaxation
            !isSongLengthEqualToPeriod(songLength, std::chrono::minutes{presetDuration})) {
            playbackDuration = std::chrono::minutes{presetDuration};
            timer->reset(playbackDuration);
            mode = AbstractRelaxationPlayer::PlaybackMode::Looped;
            mode = AbstractAudioModel::PlaybackMode::Loop;
        }
        else {
            mode = AbstractRelaxationPlayer::PlaybackMode::SingleShot;
            mode = AbstractAudioModel::PlaybackMode::Single;
            if (songLength > std::chrono::seconds::zero()) {
                timer->reset(songLength);
            }

M products/BellHybrid/apps/application-bell-relaxation/widgets/RelaxationPlayer.cpp => products/BellHybrid/apps/application-bell-relaxation/widgets/RelaxationPlayer.cpp +5 -11
@@ 6,7 6,7 @@

namespace app::relaxation
{
    AbstractRelaxationPlayer::PlaybackMode RelaxationPlayer::getCurrentMode() const noexcept
    AbstractAudioModel::PlaybackMode RelaxationPlayer::getCurrentMode() const noexcept
    {
        return playbackMode;
    }


@@ 16,7 16,7 @@ namespace app::relaxation
    {}

    void RelaxationPlayer::start(const std::string &filePath,
                                 AbstractRelaxationPlayer::PlaybackMode mode,
                                 const AbstractAudioModel::PlaybackMode &mode,
                                 AbstractAudioModel::OnStateChangeCallback &&stateChangeCallback,
                                 AbstractAudioModel::OnPlaybackFinishedCallback &&finishedCallback,
                                 std::optional<std::chrono::seconds> playbackDuration)


@@ 29,14 29,7 @@ namespace app::relaxation

        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
                    }
                });
                callback(status); // Playback finished with error
            }
            else {
                callback(Status::Normal); // Normal finish in single shot mode


@@ 45,7 38,8 @@ namespace app::relaxation

        auto fadeParams = audio::FadeParams{.mode = getFadeMode(), .playbackDuration = playbackDuration};
        audioModel.setPlaybackFinishedCb(std::move(onPlayerFinished));
        audioModel.play(filePath, Type::Multimedia, std::move(stateChangeCallback), std::move(fadeParams));
        audioModel.play(
            filePath, Type::Multimedia, playbackMode, std::move(stateChangeCallback), std::move(fadeParams));
    }

    audio::Fade RelaxationPlayer::getFadeMode() const

M products/BellHybrid/apps/application-bell-relaxation/widgets/RelaxationPlayer.hpp => products/BellHybrid/apps/application-bell-relaxation/widgets/RelaxationPlayer.hpp +5 -11
@@ 21,22 21,16 @@ namespace app::relaxation
    class AbstractRelaxationPlayer
    {
      public:
        enum class PlaybackMode
        {
            Looped,
            SingleShot
        };

        virtual ~AbstractRelaxationPlayer()                                                     = default;
        virtual void start(const std::string &filePath,
                           PlaybackMode mode,
                           const AbstractAudioModel::PlaybackMode &mode,
                           AbstractAudioModel::OnStateChangeCallback &&callback,
                           AbstractAudioModel::OnPlaybackFinishedCallback &&finishedCallback,
                           std::optional<std::chrono::seconds> playbackDuration = std::nullopt) = 0;
        virtual void stop(AbstractAudioModel::OnStateChangeCallback &&callback)                 = 0;
        virtual void pause(AbstractAudioModel::OnStateChangeCallback &&callback)                = 0;
        virtual void resume(AbstractAudioModel::OnStateChangeCallback &&callback)               = 0;
        virtual PlaybackMode getCurrentMode() const noexcept                                    = 0;
        virtual AbstractAudioModel::PlaybackMode getCurrentMode() const noexcept                = 0;
        virtual audio::Fade getFadeMode() const                                                 = 0;
        virtual bool isPaused()                                                                 = 0;
    };


@@ 48,21 42,21 @@ namespace app::relaxation

      private:
        void start(const std::string &filePath,
                   PlaybackMode mode,
                   const AbstractAudioModel::PlaybackMode &mode,
                   AbstractAudioModel::OnStateChangeCallback &&callback,
                   AbstractAudioModel::OnPlaybackFinishedCallback &&finishedCallback,
                   std::optional<std::chrono::seconds> playbackDuration = std::nullopt) override;
        void stop(AbstractAudioModel::OnStateChangeCallback &&callback) override;
        void pause(AbstractAudioModel::OnStateChangeCallback &&callback) override;
        void resume(AbstractAudioModel::OnStateChangeCallback &&callback) override;
        PlaybackMode getCurrentMode() const noexcept override;
        AbstractAudioModel::PlaybackMode getCurrentMode() const noexcept override;
        audio::Fade getFadeMode() const override;
        bool isPaused() override;

        AbstractRelaxationFadeModel &fadeModel;
        AbstractAudioModel &audioModel;
        std::string recentFilePath;
        PlaybackMode playbackMode = PlaybackMode::SingleShot;
        AbstractAudioModel::PlaybackMode playbackMode{AbstractAudioModel::PlaybackMode::Single};
        bool paused{false};
    };
} // namespace app::relaxation

M products/BellHybrid/apps/application-bell-settings/presenter/BedtimeSettingsPresenter.cpp => products/BellHybrid/apps/application-bell-settings/presenter/BedtimeSettingsPresenter.cpp +4 -2
@@ 29,8 29,10 @@ namespace app::bell_settings
            currentSoundPath = val;
            this->audioModel.setVolume(this->provider->getCurrentVolume(), AbstractAudioModel::PlaybackType::Bedtime);
            this->audioModel.setPlaybackFinishedCb(std::move(onFinishedCallback));
            this->audioModel.play(
                currentSoundPath, AbstractAudioModel::PlaybackType::Bedtime, std::move(onStartCallback));
            this->audioModel.play(currentSoundPath,
                                  AbstractAudioModel::PlaybackType::Bedtime,
                                  AbstractAudioModel::PlaybackMode::Single,
                                  std::move(onStartCallback));
        };

        this->provider->onExit = [this]() { getView()->exit(); };

M products/BellHybrid/apps/application-bell-settings/presenter/alarm_settings/AlarmSettingsPresenter.cpp => products/BellHybrid/apps/application-bell-settings/presenter/alarm_settings/AlarmSettingsPresenter.cpp +4 -2
@@ 32,8 32,10 @@ namespace app::bell_settings
            currentSoundPath = val;
            this->audioModel.setVolume(this->provider->getCurrentVolume(), AbstractAudioModel::PlaybackType::Alarm);
            this->audioModel.setPlaybackFinishedCb(std::move(onFinishedCallback));
            this->audioModel.play(
                currentSoundPath, AbstractAudioModel::PlaybackType::Alarm, std::move(onStartCallback));
            this->audioModel.play(currentSoundPath,
                                  AbstractAudioModel::PlaybackType::Alarm,
                                  AbstractAudioModel::PlaybackMode::Single,
                                  std::move(onStartCallback));
        };

        this->provider->onExit = [this]() { getView()->exit(); };

M products/BellHybrid/apps/application-bell-settings/presenter/alarm_settings/PrewakeUpPresenter.cpp => products/BellHybrid/apps/application-bell-settings/presenter/alarm_settings/PrewakeUpPresenter.cpp +4 -2
@@ 31,8 31,10 @@ namespace app::bell_settings
            currentSoundPath = val;
            this->audioModel.setVolume(this->provider->getCurrentVolume(), AbstractAudioModel::PlaybackType::PreWakeup);
            this->audioModel.setPlaybackFinishedCb(std::move(onFinishedCallback));
            this->audioModel.play(
                currentSoundPath, AbstractAudioModel::PlaybackType::PreWakeup, std::move(onStartCallback));
            this->audioModel.play(currentSoundPath,
                                  AbstractAudioModel::PlaybackType::PreWakeup,
                                  AbstractAudioModel::PlaybackMode::Single,
                                  std::move(onStartCallback));
        };

        this->provider->onExit = [this]() { getView()->exit(); };

M products/BellHybrid/apps/application-bell-settings/presenter/alarm_settings/SnoozePresenter.cpp => products/BellHybrid/apps/application-bell-settings/presenter/alarm_settings/SnoozePresenter.cpp +4 -2
@@ 30,8 30,10 @@ namespace app::bell_settings
            currentSoundPath = val;
            this->audioModel.setVolume(this->provider->getCurrentVolume(), AbstractAudioModel::PlaybackType::Snooze);
            this->audioModel.setPlaybackFinishedCb(std::move(onFinishedCallback));
            this->audioModel.play(
                currentSoundPath, AbstractAudioModel::PlaybackType::Snooze, std::move(onStartCallback));
            this->audioModel.play(currentSoundPath,
                                  AbstractAudioModel::PlaybackType::Snooze,
                                  AbstractAudioModel::PlaybackMode::Single,
                                  std::move(onStartCallback));
        };

        this->provider->onExit = [this]() { getView()->exit(); };

M products/BellHybrid/apps/common/include/common/models/AbstractAudioModel.hpp => products/BellHybrid/apps/common/include/common/models/AbstractAudioModel.hpp +13 -5
@@ 4,7 4,6 @@
#pragma once

#include <module-audio/Audio/AudioCommon.hpp>

#include <string>
#include <functional>
#include <optional>


@@ 14,6 13,12 @@ namespace app
    class AbstractAudioModel
    {
      public:
        enum class PlaybackMode
        {
            Single,
            Loop
        };

        enum class PlaybackType
        {
            Multimedia,


@@ 39,15 44,17 @@ namespace app
        using OnGetValueCallback         = std::function<void(const audio::RetCode, Volume)>;
        using OnPlaybackFinishedCallback = std::function<void(PlaybackFinishStatus)>;

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

        virtual void getVolume(PlaybackType playbackType, OnGetValueCallback &&callback) = 0;
        virtual void play(const std::string &filePath,
                          PlaybackType type,
                          const PlaybackType &type,
                          const PlaybackMode &mode,
                          OnStateChangeCallback &&callback,
                          std::optional<audio::FadeParams> fadeParams = std::nullopt)    = 0;
        virtual void stopAny(OnStateChangeCallback &&callback)                           = 0;


@@ 57,4 64,5 @@ 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 +2 -1
@@ 21,7 21,8 @@ namespace app
        std::optional<Volume> getVolume(PlaybackType playbackType) override;
        void getVolume(PlaybackType playbackType, OnGetValueCallback &&callback) override;
        void play(const std::string &filePath,
                  PlaybackType type,
                  const PlaybackType &type,
                  const PlaybackMode &mode,
                  OnStateChangeCallback &&callback,
                  std::optional<audio::FadeParams> fadeParams = std::nullopt) override;
        void stopAny(OnStateChangeCallback &&callback) override;

M products/BellHybrid/apps/common/src/AudioModel.cpp => products/BellHybrid/apps/common/src/AudioModel.cpp +16 -3
@@ 31,6 31,18 @@ namespace
        }
    }

    constexpr audio::PlaybackMode convertPlaybackMode(app::AbstractAudioModel::PlaybackMode mode)
    {
        using Mode = app::AbstractAudioModel::PlaybackMode;
        switch (mode) {
        case Mode::Loop:
            return audio::PlaybackMode::Loop;
        case Mode::Single:
        default:
            return audio::PlaybackMode::Single;
        }
    }

    void reportError(const char *prefix, audio::RetCode code)
    {
        if (code != audio::RetCode::Success) {


@@ 85,13 97,14 @@ namespace app
    }

    void AudioModel::play(const std::string &filePath,
                          PlaybackType type,
                          const PlaybackType &type,
                          const PlaybackMode &mode,
                          OnStateChangeCallback &&callback,
                          std::optional<audio::FadeParams> fadeParams)
    {
        playbackFinishedFlag = false;
        auto msg =
            std::make_unique<service::AudioStartPlaybackRequest>(filePath, convertPlaybackType(type), fadeParams);
        auto msg             = std::make_unique<service::AudioStartPlaybackRequest>(
            filePath, convertPlaybackType(type), convertPlaybackMode(mode), fadeParams);
        auto task = app::AsyncRequest::createFromMessage(std::move(msg), service::audioServiceName);

        auto cb = [_callback = callback, this](auto response) {

M products/BellHybrid/services/audio/ServiceAudio.cpp => products/BellHybrid/services/audio/ServiceAudio.cpp +8 -3
@@ 96,7 96,11 @@ namespace service

        connect(typeid(AudioStartPlaybackRequest), [this](sys::Message *msg) -> sys::MessagePointer {
            auto *msgl = static_cast<AudioStartPlaybackRequest *>(msg);
            return handleStart(audio::Operation::Type::Playback, msgl->fadeParams, msgl->fileName, msgl->playbackType);
            return handleStart(audio::Operation::Type::Playback,
                               msgl->fadeParams,
                               msgl->fileName,
                               msgl->playbackType,
                               msgl->playbackMode);
        });

        connect(typeid(internal::AudioEOFNotificationMessage), [this](sys::Message *msg) -> sys::MessagePointer {


@@ 170,7 174,8 @@ namespace service
    auto Audio::handleStart(audio::Operation::Type opType,
                            std::optional<audio::FadeParams> fadeParams,
                            const std::string &fileName,
                            const audio::PlaybackType &playbackType) -> std::unique_ptr<AudioResponseMessage>
                            const audio::PlaybackType &playbackType,
                            const audio::PlaybackMode &playbackMode) -> std::unique_ptr<AudioResponseMessage>
    {
        auto retCode  = audio::RetCode::Failed;
        auto retToken = audio::Token::MakeBadToken();


@@ 183,7 188,7 @@ namespace service
                retToken = audioMux.ResetInput(input);

                try {
                    retCode = (*input)->audio->Start(opType, retToken, fileName, playbackType);
                    retCode = (*input)->audio->Start(opType, retToken, fileName, playbackType, playbackMode);
                }
                catch (const audio::AudioInitException &audioException) {
                    retCode = audio::RetCode::FailedToAllocateMemory;

M products/BellHybrid/services/audio/include/audio/AudioMessage.hpp => products/BellHybrid/services/audio/include/audio/AudioMessage.hpp +4 -1
@@ 120,12 120,15 @@ namespace service
      public:
        AudioStartPlaybackRequest(const std::string &fileName,
                                  const audio::PlaybackType &playbackType,
                                  const audio::PlaybackMode &playbackMode     = audio::PlaybackMode::Single,
                                  std::optional<audio::FadeParams> fadeParams = std::nullopt)
            : AudioMessage(), fileName(fileName), playbackType(playbackType), fadeParams(fadeParams)
            : AudioMessage(), fileName(fileName), playbackType(playbackType), playbackMode(playbackMode),
              fadeParams(fadeParams)
        {}

        const std::string fileName;
        const audio::PlaybackType playbackType;
        const audio::PlaybackMode playbackMode;
        const std::optional<audio::FadeParams> fadeParams;
    };


M products/BellHybrid/services/audio/include/audio/ServiceAudio.hpp => products/BellHybrid/services/audio/include/audio/ServiceAudio.hpp +2 -1
@@ 44,7 44,8 @@ namespace service
        auto handleStart(audio::Operation::Type opType,
                         std::optional<audio::FadeParams> fadeParams,
                         const std::string &fileName             = {},
                         const audio::PlaybackType &playbackType = audio::PlaybackType::None)
                         const audio::PlaybackType &playbackType = audio::PlaybackType::None,
                         const audio::PlaybackMode &playbackMode = audio::PlaybackMode::Single)
            -> std::unique_ptr<AudioResponseMessage>;
        auto handleStop(const std::vector<audio::PlaybackType> &stopTypes, const audio::Token &token)
            -> std::unique_ptr<AudioResponseMessage>;

M third-party/minimp3/minimp3 => third-party/minimp3/minimp3 +1 -1
@@ 1,1 1,1 @@
Subproject commit 446d3a32d281eb1dcae7726ba0ff594695182908
Subproject commit e7202154493fb9dd8ca8c93f432003c4f191c5ae