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 "notationregionsbeingprocessedmodel.h"
24 :
25 : #include <QTransform>
26 : #include <QVariant>
27 :
28 : #include "engraving/dom/measure.h"
29 : #include "engraving/dom/score.h"
30 : #include "engraving/dom/segment.h"
31 : #include "engraving/dom/system.h"
32 :
33 : #include "notation/imasternotation.h"
34 : #include "notation/inotation.h"
35 : #include "notation/inotationparts.h" // IWYU pragma: keep
36 : #include "notation/inotationplayback.h" // IWYU pragma: keep
37 :
38 : using namespace muse::audio;
39 : using namespace mu::playback;
40 : using namespace mu::engraving;
41 : using namespace mu::notation;
42 :
43 0 : static const Segment* findSegmentFrom(const Score* score, const System* system,
44 : const int tickFrom, const staff_idx_t staffIdx)
45 : {
46 0 : const Segment* segment = score->tick2leftSegment(Fraction::fromTicks(tickFrom), /*MM*/ true, SegmentType::ChordRest);
47 0 : if (!segment || segment->system() != system) {
48 0 : return nullptr;
49 : }
50 :
51 0 : const track_idx_t trackIdx = staff2track(staffIdx);
52 0 : const EngravingItem* item = segment->element(trackIdx);
53 0 : if (!item) {
54 0 : return nullptr;
55 : }
56 :
57 0 : if (!item->isRest()) {
58 0 : return segment;
59 : }
60 :
61 0 : while (segment && item && item->isRest()) {
62 0 : segment = segment->next1(SegmentType::ChordRest);
63 0 : item = segment ? segment->element(trackIdx) : nullptr;
64 : }
65 :
66 0 : return segment;
67 : }
68 :
69 0 : static const Segment* findSegmentTo(const Score* score, const System* system, const Segment* segmentFrom,
70 : const int tickTo, const staff_idx_t staffIdx)
71 : {
72 0 : const Segment* segment = score->tick2leftSegment(Fraction::fromTicks(tickTo), /*MM*/ true, SegmentType::ChordRest);
73 0 : if (!segment || segment->system() != system) {
74 0 : return nullptr;
75 : }
76 :
77 0 : if (segment == segmentFrom) {
78 0 : return segment;
79 : }
80 :
81 0 : const track_idx_t trackIdx = staff2track(staffIdx);
82 0 : const EngravingItem* item = segment->element(trackIdx);
83 0 : if (!item) {
84 0 : return nullptr;
85 : }
86 :
87 0 : if (!item->isRest()) {
88 0 : return segment;
89 : }
90 :
91 0 : while (segment && item && item->isRest()) {
92 0 : segment = segment->prev1(SegmentType::ChordRest);
93 0 : item = segment ? segment->element(trackIdx) : nullptr;
94 : }
95 :
96 0 : return segment;
97 : }
98 :
99 0 : NotationRegionsBeingProcessedModel::NotationRegionsBeingProcessedModel(QObject* parent)
100 0 : : QAbstractListModel(parent), muse::Contextable(muse::iocCtxForQmlObject(this))
101 : {
102 0 : }
103 :
104 0 : QVariant NotationRegionsBeingProcessedModel::data(const QModelIndex& index, int role) const
105 : {
106 0 : if (!index.isValid() || index.row() >= rowCount()) {
107 0 : return QVariant();
108 : }
109 :
110 0 : const RegionInfo& region = m_regions.at(index.row());
111 :
112 0 : switch (role) {
113 0 : case RectRole: return region.viewRect;
114 0 : case ProgressRole: {
115 0 : auto it = m_tracksBeingProcessed.find(region.trackId);
116 0 : return it != m_tracksBeingProcessed.end() ? it->second.progress : 0;
117 : }
118 : }
119 :
120 0 : return QVariant();
121 : }
122 :
123 0 : int NotationRegionsBeingProcessedModel::rowCount(const QModelIndex&) const
124 : {
125 0 : return m_regions.size();
126 : }
127 :
128 0 : QHash<int, QByteArray> NotationRegionsBeingProcessedModel::roleNames() const
129 : {
130 : static const QHash<int, QByteArray> roles {
131 0 : { RectRole, "rect" },
132 0 : { ProgressRole, "progress" },
133 0 : };
134 :
135 0 : return roles;
136 0 : }
137 :
138 0 : void NotationRegionsBeingProcessedModel::load()
139 : {
140 0 : clear();
141 0 : projectSession()->masterNotationChanged().onNotify(this, [this]() {
142 0 : clear();
143 0 : });
144 :
145 0 : onOnlineSoundsChanged();
146 0 : playbackController()->onlineSoundsChanged().onNotify(this, [this]() {
147 0 : onOnlineSoundsChanged();
148 0 : });
149 :
150 0 : listenViewModeChanges();
151 :
152 0 : projectSession()->currentNotationChanged().onNotify(this, [this]() {
153 0 : listenViewModeChanges();
154 0 : setRegions(calculateRegions(m_tracksBeingProcessed));
155 0 : });
156 :
157 0 : playbackController()->isPlayingChanged().onReceive(this, [this](bool) {
158 0 : onIsPlayingChanged();
159 0 : });
160 :
161 0 : configuration()->onlineSoundsShowProgressBarModeChanged().onNotify(this, [this]() {
162 0 : initShouldShowRegions();
163 0 : setRegions(calculateRegions(m_tracksBeingProcessed));
164 0 : });
165 0 : }
166 :
167 0 : QVariant NotationRegionsBeingProcessedModel::notationViewMatrix() const
168 : {
169 0 : return m_notationViewMatrix;
170 : }
171 :
172 0 : bool NotationRegionsBeingProcessedModel::isEmpty() const
173 : {
174 0 : return m_regions.empty();
175 : }
176 :
177 0 : QColor NotationRegionsBeingProcessedModel::progressBackgroundColor() const
178 : {
179 0 : return notationConfiguration()->selectionColor(VOICES); // "all voices" color
180 : }
181 :
182 0 : QColor NotationRegionsBeingProcessedModel::progressTextColor() const
183 : {
184 0 : return notationConfiguration()->notationColor();
185 : }
186 :
187 0 : void NotationRegionsBeingProcessedModel::setNotationViewMatrix(const QVariant& matrix)
188 : {
189 0 : if (m_notationViewMatrix == matrix) {
190 0 : return;
191 : }
192 :
193 0 : m_notationViewMatrix = matrix.value<QTransform>();
194 0 : emit notationViewMatrixChanged();
195 :
196 0 : onViewMatrixChanged();
197 : }
198 :
199 0 : bool NotationRegionsBeingProcessedModel::isPlaying() const
200 : {
201 0 : return playbackController()->isPlaying();
202 : }
203 :
204 0 : void NotationRegionsBeingProcessedModel::clear()
205 : {
206 0 : m_onlineSounds.clear();
207 0 : m_tracksBeingProcessed.clear();
208 0 : initShouldShowRegions();
209 0 : setRegions({});
210 0 : }
211 :
212 0 : void NotationRegionsBeingProcessedModel::setRegions(const QList<RegionInfo>& regions)
213 : {
214 0 : if (m_regions == regions) {
215 0 : return;
216 : }
217 :
218 0 : const bool wasEmpty = m_regions.empty();
219 0 : beginResetModel();
220 0 : m_regions = regions;
221 0 : endResetModel();
222 :
223 0 : if (wasEmpty != m_regions.empty()) {
224 0 : emit isEmptyChanged();
225 : }
226 : }
227 :
228 0 : void NotationRegionsBeingProcessedModel::onOnlineSoundsChanged()
229 : {
230 0 : const std::map<TrackId, AudioResourceMeta>& newSounds = playbackController()->onlineSounds();
231 :
232 0 : for (const auto& pair : newSounds) {
233 0 : auto it = m_onlineSounds.find(pair.first);
234 0 : if (it == m_onlineSounds.end() || it->second != pair.second) {
235 0 : startListeningToProgress(pair.first);
236 : }
237 : }
238 :
239 0 : for (const auto& pair : m_onlineSounds) {
240 0 : if (!muse::contains(newSounds, pair.first)) {
241 0 : stopListeningToProgress(pair.first);
242 : }
243 : }
244 :
245 0 : m_onlineSounds = newSounds;
246 0 : }
247 :
248 0 : void NotationRegionsBeingProcessedModel::onIsPlayingChanged()
249 : {
250 0 : const OnlineSoundsShowProgressBarMode mode = configuration()->onlineSoundsShowProgressBarMode();
251 0 : if (mode != OnlineSoundsShowProgressBarMode::DuringPlayback) {
252 0 : return;
253 : }
254 :
255 0 : if (!isPlaying()) {
256 0 : m_shouldShowRegions = !m_tracksBeingProcessed.empty();
257 0 : return;
258 : }
259 :
260 0 : m_shouldShowRegions = true;
261 0 : setRegions(calculateRegions(m_tracksBeingProcessed));
262 : }
263 :
264 0 : void NotationRegionsBeingProcessedModel::listenViewModeChanges()
265 : {
266 0 : INotationPtr notation = projectSession()->currentNotation();
267 0 : if (!notation) {
268 0 : return;
269 : }
270 :
271 0 : notation->viewModeChanged().onNotify(this, [this]() {
272 0 : setRegions(calculateRegions(m_tracksBeingProcessed));
273 0 : }, Asyncable::Mode::SetReplace /* FIXME */);
274 0 : }
275 :
276 0 : void NotationRegionsBeingProcessedModel::startListeningToProgress(const TrackId trackId)
277 : {
278 0 : const IPlaybackController::InstrumentTrackIdMap& instrumentTrackIdMap = playbackController()->instrumentTrackIdMap();
279 0 : const InstrumentTrackId instrumentTrackId = muse::key(instrumentTrackIdMap, trackId);
280 0 : if (!instrumentTrackId.isValid()) {
281 0 : return;
282 : }
283 :
284 0 : playback()->inputProcessingProgress(trackId)
285 0 : .onResolve(this, [this, instrumentTrackId](InputProcessingProgress inputProgress) {
286 0 : if (inputProgress.isStarted) {
287 0 : onProgressStarted(instrumentTrackId);
288 : }
289 :
290 0 : inputProgress.processedChannel.onReceive(this, [this, instrumentTrackId]
291 : (const InputProcessingProgress::StatusInfo& status,
292 : const InputProcessingProgress::ChunkInfoList& chunks,
293 : const InputProcessingProgress::ProgressInfo& progress)
294 : {
295 0 : switch (status.status) {
296 0 : case InputProcessingProgress::Undefined:
297 0 : break;
298 0 : case InputProcessingProgress::Started: {
299 0 : onProgressStarted(instrumentTrackId);
300 0 : } break;
301 0 : case InputProcessingProgress::Processing: {
302 0 : if (!chunks.empty()) {
303 0 : onChunksReceived(instrumentTrackId, chunks);
304 : }
305 0 : onProgressChanged(instrumentTrackId, progress.current);
306 0 : } break;
307 0 : case InputProcessingProgress::Finished: {
308 0 : onProgressFinished(instrumentTrackId);
309 0 : } break;
310 : }
311 0 : });
312 0 : });
313 0 : }
314 :
315 0 : void NotationRegionsBeingProcessedModel::stopListeningToProgress(const muse::audio::TrackId trackId)
316 : {
317 0 : const IPlaybackController::InstrumentTrackIdMap& instrumentTrackIdMap = playbackController()->instrumentTrackIdMap();
318 0 : const InstrumentTrackId instrumentTrackId = muse::key(instrumentTrackIdMap, trackId);
319 0 : if (!instrumentTrackId.isValid()) {
320 0 : return;
321 : }
322 :
323 0 : onProgressFinished(instrumentTrackId);
324 0 : }
325 :
326 0 : void NotationRegionsBeingProcessedModel::onProgressStarted(const InstrumentTrackId& instrumentTrackId)
327 : {
328 0 : if (!muse::contains(m_tracksBeingProcessed, instrumentTrackId)) {
329 0 : m_tracksBeingProcessed[instrumentTrackId] = TrackInfo();
330 : }
331 0 : }
332 :
333 0 : void NotationRegionsBeingProcessedModel::onChunksReceived(const InstrumentTrackId& instrumentTrackId, const ChunkInfoList& chunks)
334 : {
335 0 : const notation::IMasterNotationPtr master = projectSession()->masterNotation();
336 0 : if (!master) {
337 0 : return;
338 : }
339 :
340 0 : auto it = m_tracksBeingProcessed.find(instrumentTrackId);
341 0 : if (it == m_tracksBeingProcessed.end()) {
342 0 : return;
343 : }
344 :
345 0 : TrackInfo& info = it->second;
346 0 : info.ranges.clear();
347 :
348 0 : bool shouldUpdate = false;
349 :
350 0 : for (const InputProcessingProgress::ChunkInfo& chunk : chunks) {
351 0 : TickRange range;
352 0 : range.tickFrom = master->playback()->secToTick(chunk.start);
353 0 : range.tickTo = master->playback()->secToTick(chunk.end);
354 :
355 0 : if (!muse::contains(info.ranges, range)) {
356 0 : info.ranges.push_back(range);
357 0 : shouldUpdate = true;
358 : }
359 : }
360 :
361 0 : if (!shouldUpdate) {
362 0 : return;
363 : }
364 :
365 0 : QList<RegionInfo> newRegions = m_regions;
366 0 : newRegions.removeIf([&instrumentTrackId](const RegionInfo& r) {
367 0 : return r.trackId == instrumentTrackId;
368 : });
369 :
370 0 : newRegions.append(calculateRegions({ { instrumentTrackId, info } }));
371 0 : setRegions(newRegions);
372 0 : }
373 :
374 0 : void NotationRegionsBeingProcessedModel::onProgressChanged(const InstrumentTrackId& instrumentTrackId, int progress)
375 : {
376 0 : auto trackIt = m_tracksBeingProcessed.find(instrumentTrackId);
377 0 : if (trackIt == m_tracksBeingProcessed.end()) {
378 0 : return;
379 : }
380 :
381 0 : if (trackIt->second.progress == progress) {
382 0 : return;
383 : }
384 :
385 0 : trackIt->second.progress = progress;
386 :
387 0 : for (int i = 0; i < m_regions.size(); ++i) {
388 0 : const RegionInfo& region = m_regions.at(i);
389 0 : if (region.trackId != instrumentTrackId) {
390 0 : continue;
391 : }
392 :
393 0 : QModelIndex modelIdx = index(i);
394 0 : emit dataChanged(modelIdx, modelIdx, { ProgressRole });
395 : }
396 : }
397 :
398 0 : void NotationRegionsBeingProcessedModel::onProgressFinished(const InstrumentTrackId& instrumentTrackId)
399 : {
400 0 : muse::remove(m_tracksBeingProcessed, instrumentTrackId);
401 :
402 0 : const QList<RegionInfo> regionsCopy = m_regions;
403 0 : const bool wasEmpty = m_regions.empty();
404 :
405 0 : for (const RegionInfo& region : regionsCopy) {
406 0 : if (region.trackId != instrumentTrackId) {
407 0 : continue;
408 : }
409 :
410 0 : const int idx = m_regions.indexOf(region);
411 0 : if (idx < 0) {
412 0 : continue;
413 : }
414 :
415 0 : beginRemoveRows(QModelIndex(), idx, idx);
416 0 : m_regions.removeAt(idx);
417 0 : endRemoveRows();
418 : }
419 :
420 0 : if (wasEmpty != m_regions.empty()) {
421 0 : initShouldShowRegions();
422 0 : emit isEmptyChanged();
423 : }
424 0 : }
425 :
426 0 : void NotationRegionsBeingProcessedModel::onViewMatrixChanged()
427 : {
428 0 : if (m_regions.empty()) {
429 0 : return;
430 : }
431 :
432 0 : for (RegionInfo& region : m_regions) {
433 0 : region.viewRect = m_notationViewMatrix.mapRect(region.logicRect);
434 : }
435 :
436 0 : emit dataChanged(index(0), index(m_regions.size() - 1), { RectRole });
437 : }
438 :
439 0 : void NotationRegionsBeingProcessedModel::initShouldShowRegions()
440 : {
441 0 : switch (configuration()->onlineSoundsShowProgressBarMode()) {
442 0 : case OnlineSoundsShowProgressBarMode::Always:
443 0 : m_shouldShowRegions = true;
444 0 : break;
445 0 : case OnlineSoundsShowProgressBarMode::Never:
446 0 : m_shouldShowRegions = false;
447 0 : break;
448 0 : case OnlineSoundsShowProgressBarMode::DuringPlayback:
449 0 : m_shouldShowRegions = isPlaying();
450 0 : break;
451 : }
452 0 : }
453 :
454 0 : QList<NotationRegionsBeingProcessedModel::RegionInfo> NotationRegionsBeingProcessedModel::calculateRegions(
455 : const TracksBeingProcessed& tracks) const
456 : {
457 0 : if (!m_shouldShowRegions || tracks.empty()) {
458 0 : return {};
459 : }
460 :
461 0 : const notation::INotationPtr notation = projectSession()->currentNotation();
462 0 : if (!notation) {
463 0 : return {};
464 : }
465 :
466 0 : QList<RegionInfo> regions;
467 :
468 0 : for (const auto& pair : tracks) {
469 0 : const Part* part = notation->parts()->part(pair.first.partId);
470 0 : if (!part) {
471 0 : continue;
472 : }
473 :
474 0 : const std::vector<QRectF> newRects = calculateRects(part, pair.second.ranges);
475 0 : regions.reserve(regions.size() + newRects.size());
476 :
477 0 : for (const QRectF& rect : newRects) {
478 0 : RegionInfo region;
479 0 : region.trackId = pair.first;
480 0 : region.logicRect = rect;
481 0 : region.viewRect = m_notationViewMatrix.mapRect(rect);
482 0 : regions.push_back(region);
483 0 : }
484 0 : }
485 :
486 0 : return regions;
487 0 : }
488 :
489 0 : std::vector<QRectF> NotationRegionsBeingProcessedModel::calculateRects(const Part* part, const std::vector<TickRange>& ranges) const
490 : {
491 0 : std::vector<QRectF> result;
492 :
493 0 : for (const System* system : part->score()->systems()) {
494 0 : if (system->measures().empty() || system->staves().empty()) {
495 0 : continue;
496 : }
497 :
498 0 : std::vector<QRectF> systemRects = calculateRects(part, system, ranges);
499 0 : result.insert(result.end(), std::make_move_iterator(systemRects.begin()),
500 : std::make_move_iterator(systemRects.end()));
501 0 : }
502 :
503 0 : return result;
504 0 : }
505 :
506 0 : std::vector<QRectF> NotationRegionsBeingProcessedModel::calculateRects(const Part* part, const System* system,
507 : const std::vector<TickRange>& ranges) const
508 : {
509 0 : const staff_idx_t staffIdx = system->firstVisibleSysStaffOfPart(part);
510 0 : const SysStaff* sysStaff = system->staff(staffIdx);
511 0 : if (!sysStaff) {
512 0 : return {};
513 : }
514 :
515 0 : const Score* score = system->score();
516 0 : const Measure* lastMeasure = score->lastMeasureMM();
517 0 : const double lastMeasureEndX = lastMeasure ? lastMeasure->canvasPos().x() + lastMeasure->width() : 0.0;
518 :
519 0 : std::vector<QRectF> result;
520 :
521 0 : for (const TickRange& range : ranges) {
522 0 : if (system->last()->tick().ticks() < range.tickFrom) {
523 0 : continue;
524 : }
525 :
526 0 : if (system->first()->tick().ticks() > range.tickTo) {
527 0 : continue;
528 : }
529 :
530 0 : const muse::PointF systemPos = system->canvasPos();
531 0 : QRectF logicRect = sysStaff->bbox().translated(systemPos).toQRectF();
532 :
533 0 : const Segment* segmentFrom = findSegmentFrom(score, system, range.tickFrom, staffIdx);
534 0 : if (segmentFrom) {
535 0 : const double segmentFromStartX = segmentFrom->canvasPos().x();
536 0 : const double startX = std::max(segmentFromStartX, logicRect.x());
537 0 : logicRect.setLeft(startX);
538 : } else {
539 0 : const double firstNoteRestX = systemPos.x() + system->firstNoteRestSegmentX();
540 0 : logicRect.setLeft(firstNoteRestX);
541 : }
542 :
543 0 : const Segment* segmentTo = findSegmentTo(score, system, segmentFrom, range.tickTo, staffIdx);
544 0 : if (segmentTo) {
545 0 : const double segmentToEndX = segmentTo->canvasPos().x() + segmentTo->width();
546 0 : const double endX = std::min(systemPos.x() + system->width(), segmentToEndX);
547 0 : logicRect.setRight(endX);
548 0 : } else if (lastMeasure) {
549 0 : const double width = std::min(logicRect.width(), lastMeasureEndX - logicRect.x());
550 0 : logicRect.setWidth(width);
551 : }
552 :
553 : // Merge with overlapping rects
554 0 : for (auto it = result.begin(); it != result.end();) {
555 0 : if (logicRect.intersects(*it)) {
556 0 : logicRect = logicRect.united(*it);
557 0 : it = result.erase(it);
558 : } else {
559 0 : ++it;
560 : }
561 : }
562 :
563 0 : result.push_back(logicRect);
564 : }
565 :
566 0 : return result;
567 0 : }
|