diff --git a/framework/audio/common/rpc/irpcchannel.h b/framework/audio/common/rpc/irpcchannel.h index dc392269c6..512883d5de 100644 --- a/framework/audio/common/rpc/irpcchannel.h +++ b/framework/audio/common/rpc/irpcchannel.h @@ -403,23 +403,27 @@ class IRpcChannel : MODULE_GLOBAL_INTERFACE, public IStreamRpcChannel virtual void process() = 0; virtual void send(const Msg& msg, const ResponseHandler& onResponse = nullptr) = 0; + virtual void cancelAllRequests(CtxId ctxId) = 0; virtual void onRequest(CtxId ctxId, MsgCode code, RequestHandler h) = 0; virtual void onNotification(CtxId ctxId, MsgCode code, NotificationHandler h) = 0; // stream (async/channel) template - StreamId addSendStream(StreamName name, const async::Channel& ch) + StreamId addSendStream(CtxId ctxId, StreamName name, const async::Channel& ch) { StreamId id = new_stream_id(); auto s = new RpcStream(this, name, id, StreamType::Send, ch, nullptr); + s->setCtxId(ctxId); addStream(std::shared_ptr(s)); return id; } template - void addReceiveStream(StreamName name, rpc::StreamId id, const async::Channel& ch, const RpcStreamExec& exec = nullptr) + void addReceiveStream(CtxId ctxId, StreamName name, rpc::StreamId id, const async::Channel& ch, + const RpcStreamExec& exec = nullptr) { auto s = new RpcStream(this, name, id, StreamType::Receive, ch, exec); + s->setCtxId(ctxId); addStream(std::shared_ptr(s)); } }; diff --git a/framework/audio/common/rpc/platform/general/generalrpcchannel.cpp b/framework/audio/common/rpc/platform/general/generalrpcchannel.cpp index 09dc0b91bb..b0deaeb4a8 100644 --- a/framework/audio/common/rpc/platform/general/generalrpcchannel.cpp +++ b/framework/audio/common/rpc/platform/general/generalrpcchannel.cpp @@ -79,19 +79,32 @@ void GeneralRpcChannel::send(const Msg& msg, const ResponseHandler& onResponse) if (s_isMainThread) { if (onResponse) { - m_mainRpcData.onResponses[msg.callId] = onResponse; + m_mainRpcData.onResponses[msg.callId] = { msg.ctxId, onResponse }; } m_msgQueue.port1()->send(msg); } else { if (onResponse) { - m_engineRpcData.onResponses[msg.callId] = onResponse; + m_engineRpcData.onResponses[msg.callId] = { msg.ctxId, onResponse }; } m_msgQueue.port2()->send(msg); } } +void GeneralRpcChannel::cancelAllRequests(CtxId ctxId) +{ + RpcData& data = s_isMainThread ? m_mainRpcData : m_engineRpcData; + + std::erase_if(data.onResponses, [ctxId](const auto& pair) { + return pair.second.ctxId == ctxId; + }); + + std::erase_if(data.streams, [ctxId](const auto& pair) { + return pair.second->ctxId() == ctxId; + }); +} + void GeneralRpcChannel::receive(RpcData& to, const Msg& m) { RPCLOG() << "ctxId: " << m.ctxId @@ -120,10 +133,11 @@ void GeneralRpcChannel::receive(RpcData& to, const Msg& m) case MsgType::Response: { auto it = to.onResponses.find(m.callId); if (it != to.onResponses.end()) { - if (it->second) { - it->second(m); - } + ResponseHandler handler = std::move(it->second.handler); to.onResponses.erase(it); + if (handler) { + handler(m); + } } } break; case MsgType::Notification: { diff --git a/framework/audio/common/rpc/platform/general/generalrpcchannel.h b/framework/audio/common/rpc/platform/general/generalrpcchannel.h index fd5d46ada1..b58d9d6c5d 100644 --- a/framework/audio/common/rpc/platform/general/generalrpcchannel.h +++ b/framework/audio/common/rpc/platform/general/generalrpcchannel.h @@ -39,6 +39,7 @@ class GeneralRpcChannel : public IRpcChannel // IRpcChannel // msgs void send(const Msg& msg, const ResponseHandler& onResponse = nullptr) override; + void cancelAllRequests(CtxId ctxId) override; void onRequest(CtxId ctxId, MsgCode code, RequestHandler h) override; void onNotification(CtxId ctxId, MsgCode code, NotificationHandler h) override; @@ -53,7 +54,12 @@ class GeneralRpcChannel : public IRpcChannel struct RpcData { // msgs std::map onRequests; - std::map onResponses; + struct PendingResponse { + CtxId ctxId = 0; + ResponseHandler handler; + }; + + std::map onResponses; std::map onNotifications; // stream diff --git a/framework/audio/common/rpc/platform/web/webrpcchannel.cpp b/framework/audio/common/rpc/platform/web/webrpcchannel.cpp index e662949b54..0579345482 100644 --- a/framework/audio/common/rpc/platform/web/webrpcchannel.cpp +++ b/framework/audio/common/rpc/platform/web/webrpcchannel.cpp @@ -117,12 +117,23 @@ void WebRpcChannel::send(const Msg& msg, const Handler& onResponse) } if (onResponse) { - m_data.onResponses[msg.callId] = onResponse; + m_data.onResponses[msg.callId] = { msg.ctxId, onResponse }; } rpcSend(&buffer[0], buffer.size()); } +void WebRpcChannel::cancelAllRequests(CtxId ctxId) +{ + std::erase_if(m_data.onResponses, [ctxId](const auto& pair) { + return pair.second.ctxId == ctxId; + }); + + std::erase_if(m_data.streams, [ctxId](const auto& pair) { + return pair.second->ctxId() == ctxId; + }); +} + void WebRpcChannel::receive(const ByteArray& data) { IF_ASSERT_FAILED(data.size() > 0) { @@ -174,10 +185,11 @@ void WebRpcChannel::receive(const Msg& msg) case MsgType::Response: { auto it = m_data.onResponses.find(msg.callId); if (it != m_data.onResponses.end()) { - if (it->second) { - it->second(msg); - } + Handler handler = std::move(it->second.handler); m_data.onResponses.erase(it); + if (handler) { + handler(msg); + } } } break; default: { diff --git a/framework/audio/common/rpc/platform/web/webrpcchannel.h b/framework/audio/common/rpc/platform/web/webrpcchannel.h index f91d069235..7c3720aec6 100644 --- a/framework/audio/common/rpc/platform/web/webrpcchannel.h +++ b/framework/audio/common/rpc/platform/web/webrpcchannel.h @@ -35,6 +35,7 @@ class WebRpcChannel : public IRpcChannel void process() override; void send(const Msg& msg, const Handler& onResponse = nullptr) override; + void cancelAllRequests(CtxId ctxId) override; void onRequest(MsgCode code, Handler h) override; void onNotification(MsgCode code, Handler h) override; void listenAll(Handler h) override; @@ -54,7 +55,12 @@ class WebRpcChannel : public IRpcChannel Handler listenerAll; std::map onRequests; std::map onNotifications; - std::map onResponses; + struct PendingResponse { + CtxId ctxId = 0; + Handler handler; + }; + + std::map onResponses; // stream std::map > streams; diff --git a/framework/audio/engine/internal/enginerpccontroller.cpp b/framework/audio/engine/internal/enginerpccontroller.cpp index 8b6c1be61f..875f0024b7 100644 --- a/framework/audio/engine/internal/enginerpccontroller.cpp +++ b/framework/audio/engine/internal/enginerpccontroller.cpp @@ -212,8 +212,8 @@ void EngineRpcController::init() m_execOperation->execOperation(OperationType::QuickOperation, func); }; - channel()->addReceiveStream(StreamName::PlaybackDataMainStream, mainStreamId, playbackData.mainStream, mainExec); - channel()->addReceiveStream(StreamName::PlaybackDataOffStream, offStreamId, playbackData.offStream, offExec); + channel()->addReceiveStream(msg.ctxId, StreamName::PlaybackDataMainStream, mainStreamId, playbackData.mainStream, mainExec); + channel()->addReceiveStream(msg.ctxId, StreamName::PlaybackDataOffStream, offStreamId, playbackData.offStream, offExec); auto addTrackAndSendResponse = [this](const Msg& msg, const TrackName& trackName, const mpe::PlaybackData& playbackData, const TrackParams& params) { @@ -349,6 +349,7 @@ void EngineRpcController::init() }); acontext->trackRemoved().onReceive(this, [this, ctxId](TrackId trackId) { + removeTrackStreams(ctxId, trackId); channel()->send(rpc::make_notification(ctxId, MsgCode::TrackRemoved, RpcPacker::pack(trackId))); }); @@ -505,7 +506,8 @@ void EngineRpcController::init() RetVal ret = actx->inputProcessingProgress(trackId); StreamId streamId = 0; if (ret.ret) { - streamId = channel()->addSendStream(StreamName::InputProcessingProgressStream, ret.val.processedChannel); + streamId = channel()->addSendStream(msg.ctxId, StreamName::InputProcessingProgressStream, ret.val.processedChannel); + addTrackStream(msg.ctxId, trackId, streamId); } return make_response(msg, RpcPacker::pack(ret.ret, ret.val.isStarted, streamId)); } else { @@ -570,7 +572,8 @@ void EngineRpcController::init() RetVal ret = audioContext(msg.ctxId)->signalChanges(trackId); StreamId streamId = 0; if (ret.ret) { - streamId = channel()->addSendStream(StreamName::AudioSignalStream, ret.val); + streamId = channel()->addSendStream(msg.ctxId, StreamName::AudioSignalStream, ret.val); + addTrackStream(msg.ctxId, trackId, streamId); } RetVal res; @@ -593,7 +596,8 @@ void EngineRpcController::init() RetVal ret = audioContext(msg.ctxId)->automatedControlParamsChanges(trackId); StreamId streamId = 0; if (ret.ret) { - streamId = channel()->addSendStream(StreamName::AutomatedControlParamsStream, ret.val); + streamId = channel()->addSendStream(msg.ctxId, StreamName::AutomatedControlParamsStream, ret.val); + addTrackStream(msg.ctxId, trackId, streamId); } RetVal res; @@ -727,7 +731,7 @@ void EngineRpcController::init() if (auto actx = audioContext(msg.ctxId)) { PlaybackStatus status = actx->playbackStatus(); async::Channel ch = actx->playbackStatusChanged(); - StreamId streamId = channel()->addSendStream(StreamName::PlaybackStatusStream, ch); + StreamId streamId = channel()->addSendStream(msg.ctxId, StreamName::PlaybackStatusStream, ch); return make_response(msg, RpcPacker::pack(make_ok(), status, streamId)); } else { return make_response(msg, RpcPacker::pack(make_ret(Err::InvalidContext), PlaybackStatus::Stopped, 0)); @@ -740,7 +744,7 @@ void EngineRpcController::init() if (auto actx = audioContext(msg.ctxId)) { secs_t pos = actx->playbackPosition(); async::Channel ch = actx->playbackPositionChanged(); - StreamId streamId = channel()->addSendStream(StreamName::PlaybackPositionStream, ch); + StreamId streamId = channel()->addSendStream(msg.ctxId, StreamName::PlaybackPositionStream, ch); return make_response(msg, RpcPacker::pack(make_ok(), pos, streamId)); } else { return make_response(msg, RpcPacker::pack(make_ret(Err::InvalidContext), 0, 0)); @@ -787,7 +791,7 @@ void EngineRpcController::init() }); if (m_saveSoundTrackProgressStreamId == 0) { - m_saveSoundTrackProgressStreamId = channel()->addSendStream(StreamName::SaveSoundTrackProgressStream, + m_saveSoundTrackProgressStreamId = channel()->addSendStream(GLOBAL_CTX_ID, StreamName::SaveSoundTrackProgressStream, m_saveSoundTrackProgressStream); } @@ -819,12 +823,39 @@ void EngineRpcController::init() audioContext->sourceParamsChanged().disconnect(this); audioContext->fxChainParamsChanged().disconnect(this); + channel()->cancelAllRequests(ctxId); + m_trackStreams.erase(ctxId); + audioEngine()->destroyContext(ctxId); return make_response_ret(msg, make_ok()); }); } +void EngineRpcController::addTrackStream(rpc::CtxId ctxId, TrackId trackId, rpc::StreamId streamId) +{ + m_trackStreams[ctxId][trackId].push_back(streamId); +} + +void EngineRpcController::removeTrackStreams(rpc::CtxId ctxId, TrackId trackId) +{ + auto ctxIt = m_trackStreams.find(ctxId); + if (ctxIt == m_trackStreams.end()) { + return; + } + + auto trackIt = ctxIt->second.find(trackId); + if (trackIt == ctxIt->second.end()) { + return; + } + + for (StreamId streamId : trackIt->second) { + channel()->removeStream(streamId); + } + + ctxIt->second.erase(trackIt); +} + void EngineRpcController::onLongRequest(rpc::CtxId ctxId, rpc::MsgCode code, const RequestHandler& h) { onRequest(OperationType::LongOperation, ctxId, code, h); diff --git a/framework/audio/engine/internal/enginerpccontroller.h b/framework/audio/engine/internal/enginerpccontroller.h index c7fd71ba72..ae606ac6d7 100644 --- a/framework/audio/engine/internal/enginerpccontroller.h +++ b/framework/audio/engine/internal/enginerpccontroller.h @@ -71,5 +71,11 @@ class EngineRpcController : public async::Asyncable async::Channel m_saveSoundTrackProgressStream; rpc::StreamId m_saveSoundTrackProgressStreamId = 0; + + //! NOTE The streams of the track are removed on the track removal, on the context deinit all streams of the context are removed by the channel + void addTrackStream(rpc::CtxId ctxId, TrackId trackId, rpc::StreamId streamId); + void removeTrackStreams(rpc::CtxId ctxId, TrackId trackId); + + std::map > > m_trackStreams; }; } diff --git a/framework/audio/main/internal/playback.cpp b/framework/audio/main/internal/playback.cpp index 92847705ec..658e92c7b8 100644 --- a/framework/audio/main/internal/playback.cpp +++ b/framework/audio/main/internal/playback.cpp @@ -35,6 +35,11 @@ using namespace muse::audio; using namespace muse::audio::rpc; using namespace muse::async; +Playback::~Playback() +{ + channel()->cancelAllRequests(ctxId()); +} + rpc::CtxId Playback::ctxId() const { return rpc::ctxId(iocContext()); @@ -60,6 +65,7 @@ async::Promise Playback::init() IF_ASSERT_FAILED(RpcPacker::unpack(msg.data, trackId)) { return; } + removeTrackStreams(trackId); m_trackRemoved.send(trackId); }); @@ -164,14 +170,42 @@ void Playback::deinit() channel()->onNotification(ctxId(), MsgCode::ControlParamsChanged, nullptr); channel()->onNotification(ctxId(), MsgCode::AuxSendsParamsChanged, nullptr); + channel()->cancelAllRequests(ctxId()); + m_saveSoundTrackProgressStream = SaveSoundTrackProgress(); m_saveSoundTrackProgressStreamInited = false; m_saveSoundTrackProgressStreamId = 0; + if (m_player) { + m_player->deinit(); + m_player.reset(); + } + + m_trackStreams.clear(); + channel()->send(rpc::make_request(rpc::GLOBAL_CTX_ID, MsgCode::ContextDeinit, RpcPacker::pack(ctxId()))); m_inited.set(false); } +void Playback::addTrackStream(TrackId trackId, rpc::StreamId streamId) const +{ + m_trackStreams[trackId].push_back(streamId); +} + +void Playback::removeTrackStreams(TrackId trackId) const +{ + auto it = m_trackStreams.find(trackId); + if (it == m_trackStreams.end()) { + return; + } + + for (StreamId streamId : it->second) { + channel()->removeStream(streamId); + } + + m_trackStreams.erase(it); +} + bool Playback::isInited() const { return m_inited.val; @@ -184,9 +218,17 @@ async::Channel Playback::initedChanged() const IPlayerPtr Playback::player() const { - std::shared_ptr p = std::make_shared(iocContext()); - p->init(); - return p; + //! NOTE The player requests the engine context, so it must exist + IF_ASSERT_FAILED(isInited()) { + return nullptr; + } + + if (!m_player) { + m_player = std::make_shared(iocContext()); + m_player->init(); + } + + return m_player; } template @@ -354,8 +396,8 @@ async::Promise Playback::addTrack(const TrackName& trackNa return async::make_promise([this, trackName, playbackData, params](auto resolve, auto reject) { ONLY_AUDIO_MAIN_THREAD; - rpc::StreamId mainStreamId = channel()->addSendStream(StreamName::PlaybackDataMainStream, playbackData.mainStream); - rpc::StreamId offStreamId = channel()->addSendStream(StreamName::PlaybackDataOffStream, playbackData.offStream); + rpc::StreamId mainStreamId = channel()->addSendStream(ctxId(), StreamName::PlaybackDataMainStream, playbackData.mainStream); + rpc::StreamId offStreamId = channel()->addSendStream(ctxId(), StreamName::PlaybackDataOffStream, playbackData.offStream); ByteArray data = RpcPacker::pack(trackName, playbackData, params, mainStreamId, offStreamId); @@ -546,7 +588,7 @@ muse::async::Promise Playback::inputProcessingProgress( return async::make_promise([this, trackId](auto resolve, auto reject) { ONLY_AUDIO_MAIN_THREAD; Msg msg = rpc::make_request(ctxId(), MsgCode::GetInputProcessingProgress, RpcPacker::pack(trackId)); - channel()->send(msg, [this, resolve, reject](const Msg& res) { + channel()->send(msg, [this, trackId, resolve, reject](const Msg& res) { ONLY_AUDIO_MAIN_THREAD; Ret ret; bool isStarted = false; @@ -559,7 +601,8 @@ muse::async::Promise Playback::inputProcessingProgress( if (ret) { InputProcessingProgress prog; prog.isStarted = isStarted; - channel()->addReceiveStream(StreamName::InputProcessingProgressStream, streamId, prog.processedChannel); + channel()->addReceiveStream(ctxId(), StreamName::InputProcessingProgressStream, streamId, prog.processedChannel); + addTrackStream(trackId, streamId); (void)resolve(prog); } else { doReject(MsgCode::GetInputProcessingProgress, reject, ret); @@ -604,7 +647,7 @@ async::Promise Playback::signalChanges(const TrackId trackId return async::make_promise([this, trackId](auto resolve, auto reject) { ONLY_AUDIO_MAIN_THREAD; Msg msg = rpc::make_request(ctxId(), MsgCode::GetSignalChanges, RpcPacker::pack(trackId)); - channel()->send(msg, [this, resolve, reject](const Msg& res) { + channel()->send(msg, [this, trackId, resolve, reject](const Msg& res) { ONLY_AUDIO_MAIN_THREAD; RetVal ret; IF_ASSERT_FAILED(RpcPacker::unpack(res.data, ret)) { @@ -614,7 +657,8 @@ async::Promise Playback::signalChanges(const TrackId trackId if (ret.ret) { AudioSignalChanges ch; - channel()->addReceiveStream(StreamName::AudioSignalStream, ret.val, ch); + channel()->addReceiveStream(ctxId(), StreamName::AudioSignalStream, ret.val, ch); + addTrackStream(trackId, ret.val); (void)resolve(ch); } else { doReject(MsgCode::GetSignalChanges, reject, ret.ret); @@ -635,7 +679,7 @@ async::Promise Playback::automatedControlParamsCh return async::make_promise([this, trackId](auto resolve, auto reject) { ONLY_AUDIO_MAIN_THREAD; Msg msg = rpc::make_request(ctxId(), MsgCode::GetAutomatedControlParamsChanges, RpcPacker::pack(trackId)); - channel()->send(msg, [this, resolve, reject](const Msg& res) { + channel()->send(msg, [this, trackId, resolve, reject](const Msg& res) { ONLY_AUDIO_MAIN_THREAD; RetVal ret; IF_ASSERT_FAILED(RpcPacker::unpack(res.data, ret)) { @@ -645,7 +689,8 @@ async::Promise Playback::automatedControlParamsCh if (ret.ret) { AutomatedControlParamsChanges ch; - channel()->addReceiveStream(StreamName::AutomatedControlParamsStream, ret.val, ch); + channel()->addReceiveStream(ctxId(), StreamName::AutomatedControlParamsStream, ret.val, ch); + addTrackStream(trackId, ret.val); (void)resolve(ch); } else { doReject(MsgCode::GetAutomatedControlParamsChanges, reject, ret.ret); @@ -704,7 +749,7 @@ SaveSoundTrackProgress Playback::saveSoundTrackProgressChanged() const if (m_saveSoundTrackProgressStreamId == 0) { m_saveSoundTrackProgressStreamId = ret.val; - channel()->addReceiveStream(StreamName::SaveSoundTrackProgressStream, + channel()->addReceiveStream(ctxId(), StreamName::SaveSoundTrackProgressStream, m_saveSoundTrackProgressStreamId, m_saveSoundTrackProgressStream); } diff --git a/framework/audio/main/internal/playback.h b/framework/audio/main/internal/playback.h index 5bfdbbbd4d..4dbed2ef66 100644 --- a/framework/audio/main/internal/playback.h +++ b/framework/audio/main/internal/playback.h @@ -26,12 +26,14 @@ #include "global/types/retval.h" #include "modularity/ioc.h" -#include "common/rpc/irpcchannel.h" +#include "audio/common/rpc/irpcchannel.h" #include "../istartaudiocontroller.h" #include "../iplayer.h" namespace muse::audio { +class Player; + //! NOTE It may not use contextual services, // but the playback itself is contextual, // because each context (window) has its own playback. @@ -43,6 +45,7 @@ class Playback : public IPlayback, public async::Asyncable, public Contextable public: Playback(const muse::modularity::ContextPtr& ctx) : Contextable(ctx) {} + ~Playback() override; // Init async::Promise init() override; @@ -123,6 +126,9 @@ class Playback : public IPlayback, public async::Asyncable, public Contextable rpc::CtxId ctxId() const; + void addTrackStream(TrackId trackId, rpc::StreamId streamId) const; + void removeTrackStreams(TrackId trackId) const; + ValCh m_inited; async::Channel m_trackAdded; @@ -138,5 +144,10 @@ class Playback : public IPlayback, public async::Asyncable, public Contextable mutable bool m_saveSoundTrackProgressStreamInited = false; async::Channel m_saveSoundTrackProgressStream; mutable rpc::StreamId m_saveSoundTrackProgressStreamId = 0; + + mutable std::shared_ptr m_player; + + //! NOTE The receive streams are removed on the track removal, on the deinit all streams of the context are removed by the channel + mutable std::map > m_trackStreams; }; } diff --git a/framework/audio/main/internal/player.cpp b/framework/audio/main/internal/player.cpp index afe9ebbe6a..ec6752674c 100644 --- a/framework/audio/main/internal/player.cpp +++ b/framework/audio/main/internal/player.cpp @@ -58,7 +58,7 @@ void Player::init() } if (ret) { - channel()->addReceiveStream(StreamName::PlaybackStatusStream, streamId, m_playbackStatusChanged); + channel()->addReceiveStream(ctxId(), StreamName::PlaybackStatusStream, streamId, m_playbackStatusChanged); //! NOTE Send initial state m_playbackStatusChanged.send(status); } else { @@ -83,7 +83,7 @@ void Player::init() } if (ret) { - channel()->addReceiveStream(StreamName::PlaybackPositionStream, streamId, m_playbackPositionChanged); + channel()->addReceiveStream(ctxId(), StreamName::PlaybackPositionStream, streamId, m_playbackPositionChanged); //! NOTE Send initial state m_playbackPositionChanged.send(pos); } else { @@ -93,6 +93,14 @@ void Player::init() } } +void Player::deinit() +{ + ONLY_AUDIO_MAIN_THREAD; + + m_playbackStatusChanged.disconnect(this); + m_playbackPositionChanged.disconnect(this); +} + async::Promise Player::prepareToPlay() { ONLY_AUDIO_MAIN_THREAD; diff --git a/framework/audio/main/internal/player.h b/framework/audio/main/internal/player.h index 0ae06a14d3..5a39dad632 100644 --- a/framework/audio/main/internal/player.h +++ b/framework/audio/main/internal/player.h @@ -40,6 +40,7 @@ class Player : public IPlayer, public Contextable, public async::Asyncable : Contextable(ctx) {} void init(); + void deinit(); async::Promise prepareToPlay() override; diff --git a/framework/audio/tests/CMakeLists.txt b/framework/audio/tests/CMakeLists.txt index dc35d14190..55658f7b59 100644 --- a/framework/audio/tests/CMakeLists.txt +++ b/framework/audio/tests/CMakeLists.txt @@ -22,13 +22,18 @@ set(MODULE_TEST muse_audio_tests) set(MODULE_TEST_SRC ${CMAKE_CURRENT_LIST_DIR}/mocks/audioconfigurationmock.h + ${CMAKE_CURRENT_LIST_DIR}/mocks/audiocontextmock.h ${CMAKE_CURRENT_LIST_DIR}/mocks/audiodriverfactorymock.h ${CMAKE_CURRENT_LIST_DIR}/mocks/audiodrivermock.h + ${CMAKE_CURRENT_LIST_DIR}/mocks/audioenginemock.h ${CMAKE_CURRENT_LIST_DIR}/mocks/rpcchannelmock.h + ${CMAKE_CURRENT_LIST_DIR}/mocks/startaudiocontrollermock.h ${CMAKE_CURRENT_LIST_DIR}/alignbuffer_tests.cpp ${CMAKE_CURRENT_LIST_DIR}/audioresourcetypes_tests.cpp ${CMAKE_CURRENT_LIST_DIR}/audiodrivercontroller_tests.cpp + ${CMAKE_CURRENT_LIST_DIR}/enginerpccontroller_tests.cpp + ${CMAKE_CURRENT_LIST_DIR}/playback_tests.cpp ) set(MODULE_TEST_LINK diff --git a/framework/audio/tests/enginerpccontroller_tests.cpp b/framework/audio/tests/enginerpccontroller_tests.cpp new file mode 100644 index 0000000000..16421f71aa --- /dev/null +++ b/framework/audio/tests/enginerpccontroller_tests.cpp @@ -0,0 +1,204 @@ +/* + * SPDX-License-Identifier: GPL-3.0-only + * MuseScore-CLA-applies + * + * MuseScore + * Music Composition & Notation + * + * Copyright (C) 2026 MuseScore Limited and others + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +#include +#include + +#include +#include + +#include "audio/common/rpc/rpcpacker.h" +#include "audio/engine/internal/enginerpccontroller.h" + +#include "mocks/audiocontextmock.h" +#include "mocks/audioenginemock.h" +#include "mocks/rpcchannelmock.h" + +using ::testing::_; +using ::testing::Invoke; +using ::testing::NiceMock; +using ::testing::Return; + +using namespace muse; +using namespace muse::audio; +using namespace muse::audio::engine; +using namespace muse::audio::rpc; + +namespace { +const CtxId CTX_ID = 1; +const TrackId TRACK_ID = 2; +const TrackId OTHER_TRACK_ID = 3; + +class ExecOperationNow : public IExecOperation +{ +public: + void execOperation(OperationType, const Operation& func) override + { + func(); + } +}; +} + +namespace muse::audio::engine { +class Audio_EngineRpcControllerTests : public ::testing::Test +{ +protected: + void SetUp() override + { + m_rpcChannel = std::make_shared >(); + m_audioEngine = std::make_shared >(); + m_audioContext = std::make_shared >(); + + modularity::globalIoc()->registerExport("utests", m_rpcChannel); + modularity::globalIoc()->registerExport("utests", m_audioEngine); + + ON_CALL(*m_rpcChannel, onRequest(_, _, _)) + .WillByDefault(Invoke([this](CtxId ctxId, MsgCode code, RequestHandler h) { + m_handlers[{ ctxId, code }] = h; + })); + + ON_CALL(*m_audioEngine, addAudioContext(_)) + .WillByDefault(Return(RetVal >::make_ok(m_audioContext))); + ON_CALL(*m_audioEngine, context(CTX_ID)).WillByDefault(Return(m_audioContext)); + + ON_CALL(*m_audioContext, trackRemoved()).WillByDefault(Return(m_trackRemoved)); + ON_CALL(*m_audioContext, signalChanges(_)) + .WillByDefault(Return(RetVal::make_ok(AudioSignalChanges()))); + ON_CALL(*m_audioContext, automatedControlParamsChanges(_)) + .WillByDefault(Return(RetVal::make_ok(AutomatedControlParamsChanges()))); + ON_CALL(*m_audioContext, inputProcessingProgress(_)) + .WillByDefault(Return(RetVal::make_ok(InputProcessingProgress()))); + + m_controller = std::make_shared(&m_execOperation); + m_controller->init(); + } + + void TearDown() override + { + m_controller->deinit(); + m_controller.reset(); + + modularity::globalIoc()->unregister("utests"); + modularity::globalIoc()->unregister("utests"); + } + + Msg request(CtxId ctxId, MsgCode code, const ByteArray& data = ByteArray()) + { + auto it = m_handlers.find({ ctxId, code }); + if (it == m_handlers.end() || !it->second) { + ADD_FAILURE() << "no handler: " << rpc::to_string(code); + return Msg(); + } + + return it->second(make_request(ctxId, code, data)); + } + + //! NOTE Returns the id of the stream added by the engine for the response + StreamId requestStream(MsgCode code, const ByteArray& data = ByteArray()) + { + StreamId streamId = 0; + EXPECT_CALL(*m_rpcChannel, addStream(_)) + .WillOnce(Invoke([&streamId](std::shared_ptr s) { + //! NOTE The stream must belong to the context to be removed with it + EXPECT_EQ(s->ctxId(), CTX_ID); + streamId = s->streamId(); + })); + + request(CTX_ID, code, data); + ::testing::Mock::VerifyAndClearExpectations(m_rpcChannel.get()); + + return streamId; + } + + ExecOperationNow m_execOperation; + + std::shared_ptr m_rpcChannel; + std::shared_ptr m_audioEngine; + std::shared_ptr m_audioContext; + std::shared_ptr m_controller; + + std::map, RequestHandler> m_handlers; + async::Channel m_trackRemoved; +}; + +TEST_F(Audio_EngineRpcControllerTests, ContextDeinit_CancelsAllRequestsOfContext) +{ + // [GIVEN] The context is inited + request(GLOBAL_CTX_ID, MsgCode::ContextInit, RpcPacker::pack(CTX_ID)); + + // [GIVEN] The player has requested the status and the position + requestStream(MsgCode::GetPlaybackStatus); + requestStream(MsgCode::GetPlaybackPosition); + + // [THEN] The pending responses and the streams of the context are removed by the channel + EXPECT_CALL(*m_rpcChannel, cancelAllRequests(CTX_ID)); + EXPECT_CALL(*m_rpcChannel, removeStream(_)).Times(0); + + // [WHEN] Deinit the context + request(GLOBAL_CTX_ID, MsgCode::ContextDeinit, RpcPacker::pack(CTX_ID)); +} + +TEST_F(Audio_EngineRpcControllerTests, ContextDeinit_ForgetsTrackStreams) +{ + // [GIVEN] The context is inited and the stream of the track is requested + request(GLOBAL_CTX_ID, MsgCode::ContextInit, RpcPacker::pack(CTX_ID)); + requestStream(MsgCode::GetSignalChanges, RpcPacker::pack(TRACK_ID)); + + // [GIVEN] The context is deinited and inited again + request(GLOBAL_CTX_ID, MsgCode::ContextDeinit, RpcPacker::pack(CTX_ID)); + request(GLOBAL_CTX_ID, MsgCode::ContextInit, RpcPacker::pack(CTX_ID)); + + // [THEN] The stream of the previous init is not removed again + EXPECT_CALL(*m_rpcChannel, removeStream(_)).Times(0); + + // [WHEN] The track is removed + m_trackRemoved.send(TRACK_ID); +} + +TEST_F(Audio_EngineRpcControllerTests, TrackRemoved_RemovesTrackStreams) +{ + // [GIVEN] The context is inited + request(GLOBAL_CTX_ID, MsgCode::ContextInit, RpcPacker::pack(CTX_ID)); + + // [GIVEN] The streams of two tracks are requested + const StreamId signalStreamId = requestStream(MsgCode::GetSignalChanges, RpcPacker::pack(TRACK_ID)); + const StreamId automationStreamId = requestStream(MsgCode::GetAutomatedControlParamsChanges, RpcPacker::pack(TRACK_ID)); + const StreamId progressStreamId = requestStream(MsgCode::GetInputProcessingProgress, RpcPacker::pack(TRACK_ID)); + const StreamId otherTrackStreamId = requestStream(MsgCode::GetSignalChanges, RpcPacker::pack(OTHER_TRACK_ID)); + + // [THEN] Only the streams of the removed track are removed + EXPECT_CALL(*m_rpcChannel, removeStream(signalStreamId)); + EXPECT_CALL(*m_rpcChannel, removeStream(automationStreamId)); + EXPECT_CALL(*m_rpcChannel, removeStream(progressStreamId)); + EXPECT_CALL(*m_rpcChannel, removeStream(otherTrackStreamId)).Times(0); + + // [WHEN] The track is removed + m_trackRemoved.send(TRACK_ID); + ::testing::Mock::VerifyAndClearExpectations(m_rpcChannel.get()); + + // [THEN] The streams of the other track are removed on the context deinit by the channel with all streams of the context + EXPECT_CALL(*m_rpcChannel, cancelAllRequests(CTX_ID)); + EXPECT_CALL(*m_rpcChannel, removeStream(_)).Times(0); + + // [WHEN] Deinit the context + request(GLOBAL_CTX_ID, MsgCode::ContextDeinit, RpcPacker::pack(CTX_ID)); +} +} diff --git a/framework/audio/tests/mocks/audiocontextmock.h b/framework/audio/tests/mocks/audiocontextmock.h new file mode 100644 index 0000000000..4574a8b257 --- /dev/null +++ b/framework/audio/tests/mocks/audiocontextmock.h @@ -0,0 +1,103 @@ +/* + * SPDX-License-Identifier: GPL-3.0-only + * MuseScore-CLA-applies + * + * MuseScore + * Music Composition & Notation + * + * Copyright (C) 2026 MuseScore Limited and others + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +#pragma once + +#include + +#include "audio/engine/internal/iaudiocontext.h" + +namespace muse::audio::engine { +class AudioContextMock : public IAudioContext +{ +public: + MOCK_METHOD(AudioCtxId, id, (), (const, override)); + + MOCK_METHOD(Ret, init, (), (override)); + MOCK_METHOD(void, deinit, (), (override)); + + MOCK_METHOD(AudioResourceMetaList, availableInputResources, (), (const, override)); + MOCK_METHOD(SoundPresetList, availableSoundPresets, (const AudioResourceMeta&), (const, override)); + MOCK_METHOD(AudioResourceMetaList, availableOutputResources, (), (const, override)); + + using AddTrackRet = RetVal2; + MOCK_METHOD(AddTrackRet, addTrack, (const TrackName&, io::IODevice*, const TrackParams&), (override)); + MOCK_METHOD(AddTrackRet, addTrack, (const TrackName&, const mpe::PlaybackData&, const TrackParams&), (override)); + MOCK_METHOD(AddTrackRet, addAuxTrack, (const TrackName&, const TrackParams&), (override)); + + MOCK_METHOD(void, removeTrack, (const TrackId), (override)); + MOCK_METHOD(void, removeAllTracks, (), (override)); + + MOCK_METHOD(async::Channel, trackAdded, (), (const, override)); + MOCK_METHOD(async::Channel, trackRemoved, (), (const, override)); + + MOCK_METHOD(RetVal, trackIdList, (), (const, override)); + MOCK_METHOD(RetVal, trackName, (const TrackId), (const, override)); + + MOCK_METHOD(RetVal, params, (const TrackId), (const, override)); + MOCK_METHOD(void, setSourceParams, (const TrackId, const AudioSourceParams&), (override)); + MOCK_METHOD(void, setControlParams, (const TrackId, const ControlParams&), (override)); + MOCK_METHOD(void, setFxChainParams, (const TrackId, const AudioFxChain&), (override)); + MOCK_METHOD(void, setAuxSendsParams, (const TrackId, const AuxSendsParams&), (override)); + + using SourceParamsChannel = async::Channel; + using ControlParamsChannel = async::Channel; + using FxChainChannel = async::Channel; + using AuxSendsParamsChannel = async::Channel; + MOCK_METHOD(SourceParamsChannel, sourceParamsChanged, (), (const, override)); + MOCK_METHOD(ControlParamsChannel, controlParamsChanged, (), (const, override)); + MOCK_METHOD(FxChainChannel, fxChainParamsChanged, (), (const, override)); + MOCK_METHOD(AuxSendsParamsChannel, auxSendsParamsChanged, (), (const, override)); + + MOCK_METHOD(void, processInput, (const TrackId), (const, override)); + MOCK_METHOD(RetVal, inputProcessingProgress, (const TrackId), (const, override)); + + MOCK_METHOD(void, clearCache, (const TrackId), (const, override)); + MOCK_METHOD(void, clearSources, (), (override)); + MOCK_METHOD(void, clearMasterOutputParams, (), (override)); + MOCK_METHOD(void, clearAllFx, (), (override)); + + MOCK_METHOD(RetVal, signalChanges, (const TrackId), (const, override)); + + MOCK_METHOD(RetVal, automatedControlParamsChanges, (const TrackId), (const, override)); + + MOCK_METHOD(async::Promise, prepareToPlay, (), (override)); + + MOCK_METHOD(void, play, (const secs_t), (override)); + MOCK_METHOD(void, seek, (const secs_t, const bool), (override)); + MOCK_METHOD(void, stop, (), (override)); + MOCK_METHOD(void, pause, (), (override)); + MOCK_METHOD(void, resume, (const secs_t), (override)); + + MOCK_METHOD(void, setDuration, (const secs_t), (override)); + MOCK_METHOD(Ret, setLoop, (const secs_t, const secs_t), (override)); + MOCK_METHOD(void, resetLoop, (), (override)); + + MOCK_METHOD(PlaybackStatus, playbackStatus, (), (const, override)); + MOCK_METHOD(async::Channel, playbackStatusChanged, (), (const, override)); + MOCK_METHOD(secs_t, playbackPosition, (), (const, override)); + MOCK_METHOD(async::Channel, playbackPositionChanged, (), (const, override)); + + MOCK_METHOD(async::Promise, saveSoundTrack, (io::IODevice&, const SoundTrackFormat&), (override)); + MOCK_METHOD(SaveSoundTrackProgress, saveSoundTrackProgressChanged, (), (const, override)); + MOCK_METHOD(void, abortSavingAllSoundTracks, (), (override)); +}; +} diff --git a/framework/audio/tests/mocks/audioenginemock.h b/framework/audio/tests/mocks/audioenginemock.h new file mode 100644 index 0000000000..258ffaa2e7 --- /dev/null +++ b/framework/audio/tests/mocks/audioenginemock.h @@ -0,0 +1,45 @@ +/* + * SPDX-License-Identifier: GPL-3.0-only + * MuseScore-CLA-applies + * + * MuseScore + * Music Composition & Notation + * + * Copyright (C) 2026 MuseScore Limited and others + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +#pragma once + +#include + +#include "audio/engine/internal/iaudioengine.h" + +namespace muse::audio::engine { +class AudioEngineMock : public IAudioEngine +{ +public: + MOCK_METHOD(Ret, init, (const OutputSpec&), (override)); + MOCK_METHOD(void, deinit, (), (override)); + + MOCK_METHOD(RetVal >, addAudioContext, (const AudioCtxId&), (override)); + MOCK_METHOD(std::shared_ptr, context, (const AudioCtxId&), (const, override)); + MOCK_METHOD(void, destroyContext, (const AudioCtxId&), (override)); + + MOCK_METHOD(void, setOutputSpec, (const OutputSpec&), (override)); + MOCK_METHOD(OutputSpec, outputSpec, (), (const, override)); + MOCK_METHOD(async::Channel, outputSpecChanged, (), (const, override)); + + MOCK_METHOD(samples_t, process, (float*, samples_t), (override)); +}; +} diff --git a/framework/audio/tests/mocks/rpcchannelmock.h b/framework/audio/tests/mocks/rpcchannelmock.h index 66d39b3855..07ed2f88d3 100644 --- a/framework/audio/tests/mocks/rpcchannelmock.h +++ b/framework/audio/tests/mocks/rpcchannelmock.h @@ -35,6 +35,7 @@ class RpcChannelMock : public IRpcChannel MOCK_METHOD(void, process, (), (override)); MOCK_METHOD(void, send, (const Msg&, const ResponseHandler&), (override)); + MOCK_METHOD(void, cancelAllRequests, (CtxId), (override)); MOCK_METHOD(void, onRequest, (CtxId, MsgCode, RequestHandler), (override)); MOCK_METHOD(void, onNotification, (CtxId, MsgCode, NotificationHandler), (override)); diff --git a/framework/audio/tests/mocks/startaudiocontrollermock.h b/framework/audio/tests/mocks/startaudiocontrollermock.h new file mode 100644 index 0000000000..bca37bb39f --- /dev/null +++ b/framework/audio/tests/mocks/startaudiocontrollermock.h @@ -0,0 +1,38 @@ +/* + * SPDX-License-Identifier: GPL-3.0-only + * MuseScore-CLA-applies + * + * MuseScore + * Music Composition & Notation + * + * Copyright (C) 2026 MuseScore Limited and others + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +#pragma once + +#include + +#include "audio/main/istartaudiocontroller.h" + +namespace muse::audio { +class StartAudioControllerMock : public IStartAudioController +{ +public: + MOCK_METHOD(void, startAudioProcessing, (const IApplication::RunMode&), (override)); + MOCK_METHOD(void, stopAudioProcessing, (), (override)); + + MOCK_METHOD(bool, isAudioStarted, (), (const, override)); + MOCK_METHOD(async::Channel, isAudioStartedChanged, (), (const, override)); +}; +} diff --git a/framework/audio/tests/playback_tests.cpp b/framework/audio/tests/playback_tests.cpp new file mode 100644 index 0000000000..3f47e34525 --- /dev/null +++ b/framework/audio/tests/playback_tests.cpp @@ -0,0 +1,274 @@ +/* + * SPDX-License-Identifier: GPL-3.0-only + * MuseScore-CLA-applies + * + * MuseScore + * Music Composition & Notation + * + * Copyright (C) 2026 MuseScore Limited and others + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +#include +#include + +#include +#include + +#include "async/processevents.h" + +#include "audio/common/rpc/rpcpacker.h" +#include "audio/main/internal/playback.h" + +#include "mocks/rpcchannelmock.h" +#include "mocks/startaudiocontrollermock.h" + +using ::testing::_; +using ::testing::Invoke; +using ::testing::NiceMock; +using ::testing::Return; + +using namespace muse; +using namespace muse::audio; +using namespace muse::audio::rpc; + +namespace { +const StreamId STATUS_STREAM_ID = 11; +const StreamId POSITION_STREAM_ID = 12; +const StreamId SIGNAL_STREAM_ID = 21; +const StreamId OTHER_SIGNAL_STREAM_ID = 22; + +const TrackId TRACK_ID = 2; +const TrackId OTHER_TRACK_ID = 3; + +MATCHER_P(StreamIdIs, id, "") +{ + return arg && arg->streamId() == id; +} +} + +namespace muse::audio { +class Audio_PlaybackTests : public ::testing::Test +{ +protected: + struct Request { + Msg msg; + ResponseHandler onResponse; + }; + + void SetUp() override + { + m_rpcChannel = std::make_shared >(); + m_startAudioController = std::make_shared >(); + + modularity::globalIoc()->registerExport("utests", m_rpcChannel); + modularity::globalIoc()->registerExport("utests", m_startAudioController); + + ON_CALL(*m_startAudioController, isAudioStarted()).WillByDefault(Return(true)); + + ON_CALL(*m_rpcChannel, send(_, _)) + .WillByDefault(Invoke([this](const Msg& msg, const ResponseHandler& onResponse) { + m_requests.push_back({ msg, onResponse }); + })); + + //! NOTE As the channel, the responses of the canceled requests are dropped + ON_CALL(*m_rpcChannel, cancelAllRequests(_)) + .WillByDefault(Invoke([this](CtxId ctxId) { + std::erase_if(m_requests, [ctxId](const Request& r) { + return r.msg.ctxId == ctxId; + }); + })); + + ON_CALL(*m_rpcChannel, onNotification(_, _, _)) + .WillByDefault(Invoke([this](CtxId, MsgCode code, NotificationHandler h) { + m_notifications[code] = h; + })); + + m_playback = std::make_shared(m_ctx); + } + + void TearDown() override + { + m_playback.reset(); + + modularity::globalIoc()->unregister("utests"); + modularity::globalIoc()->unregister("utests"); + } + + size_t requestCount(MsgCode code) const + { + size_t count = 0; + for (const Request& r : m_requests) { + if (r.msg.code == code) { + ++count; + } + } + return count; + } + + //! NOTE Responds to the first pending request with the given code + void respond(MsgCode code, const ByteArray& data) + { + for (auto it = m_requests.begin(); it != m_requests.end(); ++it) { + if (it->msg.code == code) { + Request r = *it; + m_requests.erase(it); + ASSERT_TRUE(r.onResponse); + r.onResponse(make_response(r.msg, data)); + return; + } + } + + FAIL() << "no pending request: " << rpc::to_string(code); + } + + void initContext() + { + m_playback->init(); + async::processMessages(); + + respond(MsgCode::ContextInit, RpcPacker::pack(make_ok())); + ASSERT_TRUE(m_playback->isInited()); + } + + void notify(MsgCode code, const ByteArray& data) + { + auto it = m_notifications.find(code); + ASSERT_TRUE(it != m_notifications.end() && it->second); + it->second(make_notification(rpc::ctxId(m_ctx), code, data)); + } + + void requestSignalChanges(TrackId trackId, StreamId streamId) + { + m_playback->signalChanges(trackId); + async::processMessages(); + + respond(MsgCode::GetSignalChanges, RpcPacker::pack(RetVal::make_ok(streamId))); + } + + void respondPlayerStreams() + { + respond(MsgCode::GetPlaybackStatus, RpcPacker::pack(make_ok(), PlaybackStatus::Running, STATUS_STREAM_ID)); + respond(MsgCode::GetPlaybackPosition, RpcPacker::pack(make_ok(), secs_t(2.0), POSITION_STREAM_ID)); + } + + modularity::ContextPtr m_ctx = std::make_shared(1); + + std::shared_ptr m_rpcChannel; + std::shared_ptr m_startAudioController; + std::shared_ptr m_playback; + + std::vector m_requests; + std::map m_notifications; +}; + +TEST_F(Audio_PlaybackTests, Player_IsCreatedOncePerContext) +{ + // [GIVEN] The context is inited + initContext(); + + // [WHEN] Get the player twice + IPlayerPtr player1 = m_playback->player(); + IPlayerPtr player2 = m_playback->player(); + + // [THEN] The same player is returned and it requests the state only once + ASSERT_TRUE(player1); + EXPECT_EQ(player1, player2); + EXPECT_EQ(requestCount(MsgCode::GetPlaybackStatus), 1); + EXPECT_EQ(requestCount(MsgCode::GetPlaybackPosition), 1); +} + +TEST_F(Audio_PlaybackTests, Player_SubscribesToStatusAndPosition) +{ + // [GIVEN] The context is inited + initContext(); + + // [THEN] The streams of the status and the position are added + EXPECT_CALL(*m_rpcChannel, addStream(StreamIdIs(STATUS_STREAM_ID))); + EXPECT_CALL(*m_rpcChannel, addStream(StreamIdIs(POSITION_STREAM_ID))); + + // [WHEN] Get the player and the engine responds + IPlayerPtr player = m_playback->player(); + respondPlayerStreams(); + + // [THEN] The player has the initial state + EXPECT_EQ(player->playbackStatus(), PlaybackStatus::Running); + EXPECT_EQ(player->playbackPosition(), secs_t(2.0)); +} + +TEST_F(Audio_PlaybackTests, Deinit_CancelsAllRequestsOfContext) +{ + // [GIVEN] The player is subscribed to the streams + initContext(); + IPlayerPtr player = m_playback->player(); + respondPlayerStreams(); + + // [THEN] The pending responses and the streams of the context are removed by the channel + EXPECT_CALL(*m_rpcChannel, cancelAllRequests(rpc::ctxId(m_ctx))); + + // [WHEN] Deinit the playback + m_playback->deinit(); + EXPECT_FALSE(m_playback->isInited()); + ::testing::Mock::VerifyAndClearExpectations(m_rpcChannel.get()); + + // [WHEN] Init it again + initContext(); + + // [THEN] A new player is created + IPlayerPtr newPlayer = m_playback->player(); + ASSERT_TRUE(newPlayer); + EXPECT_NE(newPlayer, player); +} + +TEST_F(Audio_PlaybackTests, Deinit_CancelsPlayerPendingRequests) +{ + // [GIVEN] The player has not received the responses yet + initContext(); + IPlayerPtr player = m_playback->player(); + + // [THEN] The responses are canceled + EXPECT_CALL(*m_rpcChannel, cancelAllRequests(rpc::ctxId(m_ctx))); + + // [WHEN] Deinit the playback + m_playback->deinit(); + ::testing::Mock::VerifyAndClearExpectations(m_rpcChannel.get()); + + // [THEN] No responses are expected anymore + EXPECT_EQ(requestCount(MsgCode::GetPlaybackStatus), 0); + EXPECT_EQ(requestCount(MsgCode::GetPlaybackPosition), 0); +} + +TEST_F(Audio_PlaybackTests, TrackRemoved_RemovesTrackStreams) +{ + // [GIVEN] The signal changes of two tracks are requested + initContext(); + requestSignalChanges(TRACK_ID, SIGNAL_STREAM_ID); + requestSignalChanges(OTHER_TRACK_ID, OTHER_SIGNAL_STREAM_ID); + + // [THEN] Only the stream of the removed track is removed + EXPECT_CALL(*m_rpcChannel, removeStream(SIGNAL_STREAM_ID)); + EXPECT_CALL(*m_rpcChannel, removeStream(OTHER_SIGNAL_STREAM_ID)).Times(0); + + // [WHEN] The engine notifies that the track is removed + notify(MsgCode::TrackRemoved, RpcPacker::pack(TRACK_ID)); + ::testing::Mock::VerifyAndClearExpectations(m_rpcChannel.get()); + + // [THEN] The stream of the other track is removed on deinit by the channel with all streams of the context + EXPECT_CALL(*m_rpcChannel, cancelAllRequests(rpc::ctxId(m_ctx))); + EXPECT_CALL(*m_rpcChannel, removeStream(_)).Times(0); + + // [WHEN] Deinit the playback + m_playback->deinit(); + ::testing::Mock::VerifyAndClearExpectations(m_rpcChannel.get()); +} +}