Line data Source code
1 : /*
2 : * SPDX-License-Identifier: GPL-3.0-only
3 : * MuseScore-Studio-CLA-applies
4 : *
5 : * MuseScore Studio
6 : * Music Composition & Notation
7 : *
8 : * Copyright (C) 2025 MuseScore Limited and others
9 : *
10 : * This program is free software: you can redistribute it and/or modify
11 : * it under the terms of the GNU General Public License version 3 as
12 : * published by the Free Software Foundation.
13 : *
14 : * This program is distributed in the hope that it will be useful,
15 : * but WITHOUT ANY WARRANTY; without even the implied warranty of
16 : * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17 : * GNU General Public License for more details.
18 : *
19 : * You should have received a copy of the GNU General Public License
20 : * along with this program. If not, see <https://www.gnu.org/licenses/>.
21 : */
22 :
23 : #include "onlinesoundscontroller.h"
24 :
25 : #include "audio/common/audioerrors.h"
26 :
27 : #include "modularity/ioc.h"
28 : #include "translation.h"
29 :
30 : #include "../playbackcommands.h"
31 :
32 : #include "log.h"
33 :
34 : using namespace muse;
35 : using namespace mu::playback;
36 : using namespace muse::audio;
37 :
38 0 : static Ret retFromProcessingStatus(const InputProcessingProgress::StatusInfo& status)
39 : {
40 0 : Ret ret(status.errorCode, status.errorText);
41 0 : for (const auto& pair : status.data) {
42 0 : ret.setData(pair.first, pair.second);
43 : }
44 :
45 0 : return ret;
46 0 : }
47 :
48 9 : OnlineSoundsController::OnlineSoundsController(const muse::modularity::ContextPtr& iocCtx)
49 9 : : muse::Contextable(iocCtx), m_onlineSoundsProcessingRet(make_ok())
50 : {
51 9 : }
52 :
53 9 : void OnlineSoundsController::regActions()
54 : {
55 9 : commandsDispatcher()->onRequest(this, PROCESS_ONLINESOUNDS_COMMAND, [this]() { return processOnlineSounds(); });
56 9 : commandsDispatcher()->onRequest(this, CLEAR_ONLINESOUNDS_CACHE_COMMAND, [this]() { return clearOnlineSoundsCache(); });
57 9 : }
58 :
59 0 : void OnlineSoundsController::reset()
60 : {
61 0 : const bool hadOnlineSounds = !m_onlineSounds.empty();
62 0 : m_onlineSounds.clear();
63 0 : m_onlineSoundsBeingProcessed.clear();
64 0 : m_onlineLibrariesWithExceededLimit.clear();
65 0 : m_onlineSoundsProcessingRet = make_ok();
66 :
67 0 : if (hadOnlineSounds) {
68 0 : m_onlineSoundsChanged.notify();
69 : }
70 0 : }
71 :
72 0 : const std::map<TrackId, AudioResourceMeta>& OnlineSoundsController::onlineSounds() const
73 : {
74 0 : return m_onlineSounds;
75 : }
76 :
77 0 : muse::async::Notification OnlineSoundsController::onlineSoundsChanged() const
78 : {
79 0 : return m_onlineSoundsChanged;
80 : }
81 :
82 0 : muse::Progress OnlineSoundsController::onlineSoundsProcessingProgress() const
83 : {
84 0 : return m_onlineSoundsProcessingProgress;
85 : }
86 :
87 0 : void OnlineSoundsController::addOnlineTrack(const TrackId trackId, const AudioResourceMeta& meta)
88 : {
89 0 : auto it = m_onlineSounds.find(trackId);
90 0 : if (it != m_onlineSounds.end() && it->second == meta) {
91 0 : return;
92 : }
93 :
94 0 : m_onlineSounds.insert_or_assign(trackId, meta);
95 0 : listenProcessingProgress(trackId);
96 0 : m_onlineSoundsChanged.notify();
97 : }
98 :
99 0 : void OnlineSoundsController::removeOnlineTrack(const TrackId trackId)
100 : {
101 0 : if (!muse::contains(m_onlineSounds, trackId)) {
102 0 : return;
103 : }
104 :
105 0 : muse::remove(m_onlineSounds, trackId);
106 0 : muse::remove(m_onlineSoundsBeingProcessed, trackId);
107 :
108 0 : if (m_onlineSoundsProcessingProgress.isStarted() && m_onlineSoundsBeingProcessed.empty()) {
109 0 : m_onlineSoundsProcessingProgress.finish(make_ret(Ret::Code::Cancel));
110 : }
111 :
112 0 : m_onlineSoundsChanged.notify();
113 : }
114 :
115 0 : void OnlineSoundsController::listenProcessingProgress(const TrackId trackId)
116 : {
117 0 : playback()->inputProcessingProgress(trackId)
118 0 : .onResolve(this, [this, trackId](InputProcessingProgress inputProgress) {
119 0 : inputProgress.processedChannel.onReceive(this, [this, trackId]
120 : (const InputProcessingProgress::StatusInfo& status,
121 : const InputProcessingProgress::ChunkInfoList& /*chunks*/,
122 : const InputProcessingProgress::ProgressInfo& progress)
123 : {
124 0 : switch (status.status) {
125 0 : case InputProcessingProgress::Undefined:
126 0 : break;
127 0 : case InputProcessingProgress::Started: {
128 0 : m_onlineSoundsBeingProcessed.insert(trackId);
129 :
130 0 : if (!m_onlineSoundsProcessingProgress.isStarted()) {
131 0 : m_onlineSoundsProcessingRet = make_ok();
132 0 : m_onlineSoundsProcessingProgress.start();
133 : }
134 0 : } break;
135 0 : case InputProcessingProgress::Processing: {
136 0 : if (m_onlineSoundsBeingProcessed.size() == 1) {
137 0 : m_onlineSoundsProcessingProgress.progress(progress.current, progress.total);
138 : }
139 0 : } break;
140 0 : case InputProcessingProgress::Finished: {
141 0 : muse::remove(m_onlineSoundsBeingProcessed, trackId);
142 :
143 0 : if (status.errorCode != 0 && status.errorCode != (int)Ret::Code::Cancel) {
144 0 : m_onlineSoundsProcessingRet = retFromProcessingStatus(status);
145 0 : LOGE() << "Error during online sounds processing: " << status.errorText << ", track: " << trackId;
146 :
147 0 : if (status.errorCode == (int)Err::OnlineSoundsLimitReached) {
148 0 : showLimitReachedErrorIfNeed(status);
149 : }
150 : }
151 :
152 0 : if (m_onlineSoundsBeingProcessed.empty()) {
153 0 : m_onlineSoundsProcessingProgress.finish(m_onlineSoundsProcessingRet);
154 : }
155 0 : } break;
156 : }
157 0 : });
158 0 : });
159 0 : }
160 :
161 0 : bool OnlineSoundsController::shouldShowOnlineSoundsProcessingError(bool isPlaying) const
162 : {
163 0 : if (m_onlineSounds.empty() || !configuration()->shouldShowOnlineSoundsProcessingError() || isPlaying) {
164 0 : return false;
165 : }
166 :
167 0 : if (!m_onlineSoundsProcessingRet) {
168 0 : const int code = m_onlineSoundsProcessingRet.code();
169 0 : return code != (int)Ret::Code::Cancel && code != (int)Err::OnlineSoundsLimitReached;
170 : }
171 :
172 0 : return false;
173 : }
174 :
175 0 : void OnlineSoundsController::showOnlineSoundsProcessingError(const std::function<void()>& onShown)
176 : {
177 0 : const std::string text = muse::qtrc("playback", "This may be due to a poor internet connection or server issue. "
178 : "Your score will still play, but some sounds may be missing. "
179 : "Please check your connection, and make sure MuseHub is running and you are logged in. "
180 : "<a href=\"%1\">Learn more here</a>.")
181 0 : .arg(configuration()->onlineSoundsHandbookUrl()).toStdString();
182 :
183 0 : auto promise = interactive()->warning(muse::trc("playback", "Some online sounds aren’t ready yet"), text,
184 : { IInteractive::Button::Ok }, IInteractive::Button::Ok,
185 0 : IInteractive::Option::WithIcon | IInteractive::Option::WithDontShowAgainCheckBox);
186 :
187 0 : promise.onResolve(this, [this, onShown](const IInteractive::Result& res) {
188 0 : if (!res.showAgain()) {
189 0 : configuration()->setShouldShowOnlineSoundsProcessingError(false);
190 : }
191 :
192 0 : if (onShown) {
193 0 : onShown();
194 : }
195 0 : });
196 0 : }
197 :
198 0 : void OnlineSoundsController::showLimitReachedErrorIfNeed(const InputProcessingProgress::StatusInfo& status)
199 : {
200 0 : IF_ASSERT_FAILED(status.errorCode == (int)audio::Err::OnlineSoundsLimitReached) {
201 0 : return;
202 : }
203 :
204 0 : const String libName = String::fromStdString(muse::value(status.data, "libraryName"));
205 0 : const String date = String::fromStdString(muse::value(status.data, "date"));
206 0 : const String url = String::fromStdString(muse::value(status.data, "url"));
207 :
208 0 : IF_ASSERT_FAILED(!libName.empty() && !date.empty() && !url.empty()) {
209 0 : return;
210 : }
211 :
212 0 : if (muse::contains(m_onlineLibrariesWithExceededLimit, libName)) {
213 0 : return;
214 : }
215 0 : m_onlineLibrariesWithExceededLimit.insert(libName);
216 :
217 0 : const std::string text = muse::qtrc("playback", "You’ve reached your current render limit for %1. "
218 : "You will be able to process online sounds again after your quota resets on %2. "
219 : "More info: <a href=\"%3\">%3</a>.")
220 0 : .arg(libName)
221 0 : .arg(date)
222 0 : .arg(url).toStdString();
223 :
224 0 : interactive()->warning(muse::trc("playback", "Unable to process online sounds"), text);
225 0 : }
226 :
227 0 : muse::Ret OnlineSoundsController::processOnlineSounds()
228 : {
229 0 : IF_ASSERT_FAILED(playback()) {
230 0 : return make_ret(Ret::Code::InternalError);
231 : }
232 :
233 0 : for (const auto& pair : m_onlineSounds) {
234 0 : playback()->processInput(pair.first);
235 : }
236 :
237 0 : return make_ok();
238 : }
239 :
240 0 : muse::Ret OnlineSoundsController::clearOnlineSoundsCache()
241 : {
242 0 : auto promise = interactive()->warning(
243 0 : muse::trc("playback", "Are you sure you want to clear online sounds cache?"),
244 0 : muse::trc("playback", "This will delete online sounds data stored on your computer for this score. "
245 : "Online sounds processing will try to restart immediately."),
246 : { IInteractive::Button::Ok, IInteractive::Button::Cancel },
247 0 : IInteractive::Button::Ok);
248 :
249 0 : promise.onResolve(this, [this](const IInteractive::Result& res) {
250 0 : if (!res.isButton(IInteractive::Button::Ok)) {
251 0 : return;
252 : }
253 :
254 0 : IF_ASSERT_FAILED(playback()) {
255 0 : return;
256 : }
257 :
258 0 : for (const auto& pair : m_onlineSounds) {
259 0 : playback()->clearCache(pair.first);
260 : }
261 : });
262 :
263 0 : return make_ok();
264 0 : }
|