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) 2021 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 : #include "videowriter.h"
23 :
24 : #include <cmath>
25 :
26 : #include <QPainter>
27 : #include <QThread>
28 :
29 : #include "global/concurrency/concurrent.h"
30 : #include "global/io/filestream.h"
31 :
32 : #include "draw/fontmetrics.h"
33 : #include "draw/painter.h"
34 : #include "draw/types/drawtypes.h"
35 :
36 : #include "engraving/dom/masterscore.h"
37 : #include "engraving/dom/page.h"
38 : #include "engraving/dom/repeatlist.h"
39 :
40 : #include "notation/imasternotation.h"
41 : #include "notation/inotation.h"
42 : #include "notation/inotationelements.h" // IWYU pragma: keep
43 : #include "notation/inotationpainting.h"
44 : #include "notation/inotationplayback.h"
45 : #include "notation/notationtypes.h"
46 :
47 : #include "notationscene/qml/MuseScore/NotationScene/playbackcursor.h"
48 :
49 : #include "defer.h"
50 : #include "log.h"
51 :
52 : using namespace mu::iex::videoexport;
53 : using namespace mu::project;
54 : using namespace mu::notation;
55 : using namespace muse::draw;
56 : using namespace muse::midi;
57 :
58 0 : static muse::String notationTitle(const INotationPtr notation)
59 : {
60 0 : muse::String title;
61 0 : mu::engraving::Score* score = notation->elements()->msScore();
62 0 : mu::engraving::MasterScore* masterScore = notation->masterNotation()->masterScore();
63 :
64 0 : if (const mu::engraving::Text* text = score->getText(mu::engraving::TextStyleType::TITLE)) {
65 0 : title = text->plainText();
66 : }
67 :
68 0 : if (title.isEmpty()) {
69 0 : if (const mu::engraving::Text* text = masterScore->getText(mu::engraving::TextStyleType::TITLE)) {
70 0 : title = text->plainText();
71 : }
72 : }
73 :
74 0 : if (title.isEmpty()) {
75 0 : title = masterScore->metaTag(u"workTitle");
76 : }
77 :
78 0 : return title;
79 0 : }
80 :
81 0 : static muse::String notationSubtitle(const INotationPtr notation)
82 : {
83 0 : if (notation->isMaster()) {
84 0 : return muse::String();
85 : }
86 :
87 0 : return notation->name();
88 : }
89 :
90 0 : std::vector<INotationWriter::UnitType> VideoWriter::supportedUnitTypes() const
91 : {
92 0 : return { UnitType::PER_PART };
93 : }
94 :
95 0 : bool VideoWriter::supportsUnitType(UnitType unitType) const
96 : {
97 0 : std::vector<UnitType> unitTypes = supportedUnitTypes();
98 0 : return std::find(unitTypes.cbegin(), unitTypes.cend(), unitType) != unitTypes.cend();
99 0 : }
100 :
101 0 : muse::Ret VideoWriter::write(INotationPtr notation, muse::io::IODevice& device, const Options& options)
102 : {
103 0 : std::string filePath = device.meta("file_path");
104 0 : IF_ASSERT_FAILED(!filePath.empty()) {
105 0 : return make_ret(muse::Ret::Code::InternalError);
106 : }
107 :
108 0 : auto encoder = videoEncodeResolver()->currentVideoEncoder();
109 0 : if (!encoder) {
110 0 : LOGE() << "FFmpeg not found, video export is not available. "
111 0 : << "Install FFmpeg or specify the path to its libraries in the preferences";
112 0 : return make_ret(muse::Ret::Code::NotSupported);
113 : }
114 :
115 0 : bool withAudio = muse::value(options, OptionKey::WITH_AUDIO, muse::Val(true)).toBool();
116 :
117 0 : Config cfg = makeConfig();
118 :
119 0 : muse::io::path_t finalPath(filePath);
120 0 : muse::io::path_t tempAudioPath = finalPath + ".tmp_audio.aac";
121 :
122 0 : muse::media::IVideoEncoder::Options encoderOptions;
123 0 : encoderOptions.format = "mp4";
124 0 : encoderOptions.width = cfg.width;
125 0 : encoderOptions.height = cfg.height;
126 0 : encoderOptions.bitrate = cfg.bitrate;
127 0 : encoderOptions.gop = cfg.fps / 2;
128 0 : encoderOptions.fps = cfg.fps;
129 :
130 0 : if (!encoder->open(finalPath, encoderOptions)) {
131 0 : LOGE() << "failed to open video encoder";
132 0 : return make_ret(muse::Ret::Code::UnknownError);
133 : }
134 :
135 0 : m_isCompleted = false;
136 0 : m_audioCompleted = false;
137 0 : m_abort = false;
138 0 : m_writeRet = muse::Ret();
139 0 : m_audioRet = muse::Ret();
140 :
141 0 : startVideoExport(encoder, notation, cfg);
142 :
143 0 : if (withAudio) {
144 0 : startAudioExport(notation, tempAudioPath, cfg);
145 : } else {
146 0 : m_audioCompleted = true;
147 0 : m_audioRet = muse::make_ok();
148 : }
149 :
150 0 : while (!m_isCompleted || !m_audioCompleted) {
151 0 : application()->processEvents();
152 0 : QThread::yieldCurrentThread();
153 : }
154 :
155 0 : if (m_audioWriter) {
156 0 : m_audioWriter->progress()->finished().disconnect(this);
157 0 : m_audioWriter = nullptr;
158 : }
159 :
160 0 : if (m_audioFile) {
161 0 : m_audioFile->close();
162 0 : m_audioFile.reset();
163 : }
164 :
165 0 : muse::Ret result = m_writeRet;
166 :
167 0 : encoder->finishEncode();
168 :
169 : // Release the device's file handle before add audio replaces the file
170 0 : device.close();
171 :
172 0 : if (withAudio) {
173 0 : if (result && m_audioRet) {
174 0 : if (!encoder->addAudio(tempAudioPath)) {
175 0 : result = make_ret(muse::Ret::Code::UnknownError);
176 : }
177 0 : } else if (!result) {
178 : // keep video error
179 : } else {
180 0 : result = m_audioRet;
181 : }
182 :
183 0 : fileSystem()->remove(tempAudioPath);
184 : }
185 :
186 0 : encoder->close();
187 :
188 0 : return result;
189 0 : }
190 :
191 0 : VideoWriter::Config VideoWriter::makeConfig() const
192 : {
193 0 : Config cfg;
194 :
195 0 : cfg.fps = configuration()->fps();
196 :
197 0 : std::string resolution = configuration()->resolution();
198 0 : if (resolution == "2160p") {
199 0 : cfg.width = 3840;
200 0 : cfg.height = 2160;
201 0 : } else if (resolution == "1440p") {
202 0 : cfg.width = 2560;
203 0 : cfg.height = 1440;
204 0 : } else if (resolution == "1080p") {
205 0 : cfg.width = 1920;
206 0 : cfg.height = 1080;
207 0 : } else if (resolution == "720p") {
208 0 : cfg.width = 1280;
209 0 : cfg.height = 720;
210 0 : } else if (resolution == "480p") {
211 0 : cfg.width = 854;
212 0 : cfg.height = 480;
213 0 : } else if (resolution == "360p") {
214 0 : cfg.width = 640;
215 0 : cfg.height = 360;
216 : } else {
217 0 : cfg.width = 1920;
218 0 : cfg.height = 1080;
219 : }
220 :
221 : // compute bitrate according to Google recommended settings
222 : // https://support.google.com/youtube/answer/1722171?hl=en
223 0 : float br = 8;
224 0 : if (cfg.height == 2160) {
225 0 : br = cfg.fps < 35 ? 40 : 60;
226 0 : } else if (cfg.height == 1440) {
227 0 : br = cfg.fps < 35 ? 16 : 24;
228 0 : } else if (cfg.height == 1080) {
229 0 : br = cfg.fps < 35 ? 10 : 15;
230 0 : } else if (cfg.height == 720) {
231 0 : br = cfg.fps < 35 ? 5 : 7.5;
232 0 : } else if (cfg.height == 480) {
233 0 : br = cfg.fps < 35 ? 2.5 : 4;
234 0 : } else if (cfg.height == 360) {
235 0 : br = cfg.fps < 35 ? 1 : 1.5;
236 : }
237 0 : cfg.bitrate = int(br * 1000000);
238 :
239 0 : cfg.leadingSec = configuration()->leadingSec();
240 0 : cfg.trailingSec = configuration()->trailingSec();
241 :
242 0 : cfg.viewMode = configuration()->viewMode();
243 :
244 0 : return cfg;
245 0 : }
246 :
247 0 : void VideoWriter::startVideoExport(muse::media::IVideoEncoderPtr encoder, INotationPtr notation, const Config& cfg)
248 : {
249 0 : muse::Concurrent::run([this, encoder, notation, cfg]() {
250 0 : doGenerate(encoder, notation, cfg);
251 0 : });
252 0 : }
253 :
254 0 : void VideoWriter::startAudioExport(INotationPtr notation, const muse::io::path_t& audioPath, const Config& cfg)
255 : {
256 0 : m_audioWriter = writers()->writer("aac");
257 0 : if (!m_audioWriter) {
258 0 : LOGE() << "aac writer not found";
259 0 : m_audioRet = make_ret(muse::Ret::Code::InternalError);
260 0 : m_audioCompleted = true;
261 0 : return;
262 : }
263 :
264 0 : m_audioWriter->progress()->finished().onReceive(this, [this](const muse::ProgressResult& res) {
265 0 : m_audioRet = res.ret;
266 0 : m_audioCompleted = true;
267 0 : });
268 :
269 0 : Options audioOpts;
270 0 : audioOpts[OptionKey::WAIT_FOR_COMPLETION] = muse::Val(false);
271 0 : audioOpts[OptionKey::LEADING_SILENCE_SEC] = muse::Val(static_cast<double>(cfg.leadingSec));
272 0 : audioOpts[OptionKey::TRAILING_SILENCE_SEC] = muse::Val(static_cast<double>(cfg.trailingSec));
273 :
274 0 : m_audioFile = std::make_unique<muse::io::FileStream>(audioPath);
275 0 : m_audioFile->setMeta("file_path", audioPath.toStdString());
276 0 : m_audioFile->open(muse::io::IODevice::WriteOnly);
277 :
278 0 : m_audioWriter->write(notation, *m_audioFile, audioOpts);
279 0 : }
280 :
281 0 : muse::Ret VideoWriter::writeList(const INotationPtrList&, muse::io::IODevice&, const Options&)
282 : {
283 0 : NOT_SUPPORTED;
284 0 : return make_ret(muse::Ret::Code::NotSupported);
285 : }
286 :
287 0 : muse::Progress* VideoWriter::progress()
288 : {
289 0 : return &m_progress;
290 : }
291 :
292 0 : void VideoWriter::abort()
293 : {
294 0 : m_abort = true;
295 :
296 0 : if (m_audioWriter) {
297 0 : m_audioWriter->abort();
298 : }
299 :
300 : // Wait abort completion
301 0 : while (!m_isCompleted || !m_audioCompleted) {
302 0 : application()->processEvents();
303 0 : QThread::yieldCurrentThread();
304 : }
305 0 : }
306 :
307 0 : std::optional<VideoWriter::ScoreRestoreData> VideoWriter::prepareScore(INotationPtr notation, Config& config)
308 : {
309 0 : ScoreRestoreData result;
310 0 : engraving::Score* score = notation->elements()->msScore();
311 :
312 0 : result.style = score->style();
313 :
314 0 : result.layoutMode = score->layoutMode();
315 :
316 0 : result.showFrames = score->showFrames();
317 0 : result.showInstrumentNames = score->showInstrumentNames();
318 0 : result.showInvisible = score->isShowInvisible();
319 0 : result.showPageborders = score->showPageborders();
320 0 : result.showUnprintable = score->showUnprintable();
321 0 : result.showVBox = score->layoutOptions().isShowVBox;
322 :
323 0 : score->setLayoutMode(engraving::LayoutMode::PAGE);
324 :
325 0 : score->setShowFrames(false);
326 0 : score->setShowInvisible(false);
327 0 : score->setShowPageborders(false);
328 0 : score->setShowUnprintable(false);
329 :
330 0 : if (config.viewMode == ViewMode::Flexible) {
331 0 : score->setShowInstrumentNames(false);
332 0 : score->setShowVBox(false);
333 : }
334 :
335 0 : score->doLayout();
336 :
337 0 : PageList pages = notation->elements()->pages();
338 0 : if (pages.empty()) {
339 0 : LOGE() << "No pages";
340 0 : restoreScore(notation, result);
341 0 : return std::nullopt;
342 : }
343 :
344 0 : const Page* page = pages.front();
345 :
346 0 : if (config.viewMode == ViewMode::PageFull) {
347 0 : double scaleX = config.width / page->width();
348 0 : double scaleY = config.height / page->height();
349 0 : double scale = std::min(scaleX, scaleY);
350 0 : config.canvasDpi = scale * engraving::DPI;
351 0 : config.moveToCenter = muse::PointF(0.5 * (config.width / scale - page->width()), 0.5 * (config.height / scale - page->height()));
352 0 : return result;
353 : }
354 :
355 0 : if (score->staves().size() > 3) {
356 : //! NOTE: Calculate the dpi to display all page elements
357 0 : double originalPageHeight = page->height() - score->style().styleD(engraving::Sid::pageOddTopMargin)
358 0 : - score->style().styleD(engraving::Sid::pageOddBottomMargin);
359 0 : double margin = 100.0;
360 0 : double ttboxHeight = originalPageHeight + margin * 2;
361 0 : double scale = config.height / ttboxHeight;
362 0 : config.canvasDpi = scale * engraving::DPI;
363 : }
364 :
365 0 : score->style().set(engraving::Sid::pageHeight, config.height / config.canvasDpi);
366 0 : score->style().set(engraving::Sid::pageWidth, config.width / config.canvasDpi);
367 0 : score->style().set(engraving::Sid::pagePrintableWidth, score->style().styleD(engraving::Sid::pageWidth)
368 0 : - score->style().styleD(engraving::Sid::pageOddLeftMargin)
369 0 : - score->style().styleD(engraving::Sid::pageEvenLeftMargin));
370 :
371 0 : score->style().set(engraving::Sid::pageEvenTopMargin, 0.0);
372 0 : score->style().set(engraving::Sid::pageEvenBottomMargin, 0.0);
373 0 : score->style().set(engraving::Sid::pageOddTopMargin, 0.0);
374 0 : score->style().set(engraving::Sid::pageOddBottomMargin, 0.0);
375 0 : score->style().set(engraving::Sid::pageTwosided, false);
376 0 : score->style().set(engraving::Sid::showHeader, false);
377 0 : score->style().set(engraving::Sid::showFooter, false);
378 :
379 0 : score->style().set(engraving::Sid::minSystemDistance, engraving::Spatium(10));
380 0 : score->style().set(engraving::Sid::maxSystemDistance, engraving::Spatium(10));
381 0 : score->style().set(engraving::Sid::staffLowerBorder, engraving::Spatium(5));
382 0 : score->style().set(engraving::Sid::staffUpperBorder, engraving::Spatium(7));
383 :
384 0 : score->setLayoutAll();
385 0 : score->doLayout();
386 :
387 0 : return result;
388 0 : }
389 :
390 0 : void VideoWriter::restoreScore(INotationPtr notation, const ScoreRestoreData& data)
391 : {
392 0 : engraving::Score* score = notation->elements()->msScore();
393 :
394 0 : score->style() = data.style;
395 :
396 0 : score->setShowFrames(data.showFrames);
397 0 : score->setShowInstrumentNames(data.showInstrumentNames);
398 0 : score->setShowInvisible(data.showInvisible);
399 0 : score->setShowPageborders(data.showPageborders);
400 0 : score->setShowUnprintable(data.showUnprintable);
401 0 : score->setShowVBox(data.showVBox);
402 :
403 0 : score->setLayoutMode(data.layoutMode);
404 :
405 0 : score->setLayoutAll();
406 0 : score->update();
407 0 : }
408 :
409 0 : void VideoWriter::doGenerate(muse::media::IVideoEncoderPtr encoder, INotationPtr notation, const Config& config)
410 : {
411 0 : Config actualConfig = config;
412 0 : auto restoreData = prepareScore(notation, actualConfig);
413 0 : if (!restoreData) {
414 0 : m_writeRet = make_ret(muse::Ret::Code::UnknownError);
415 0 : m_isCompleted = true;
416 0 : return;
417 : }
418 :
419 0 : DEFER {
420 0 : restoreScore(notation, restoreData.value());
421 0 : m_isCompleted = true;
422 0 : };
423 :
424 : // Setup painting
425 0 : QImage frame(actualConfig.width, actualConfig.height, QImage::Format_RGB32);
426 0 : frame.setDotsPerMeterX(std::lrint((actualConfig.canvasDpi * 1000) / engraving::INCH));
427 0 : frame.setDotsPerMeterY(std::lrint((actualConfig.canvasDpi * 1000) / engraving::INCH));
428 :
429 0 : QPainter qp(&frame);
430 0 : qp.setRenderHint(QPainter::Antialiasing, true);
431 0 : qp.setRenderHint(QPainter::TextAntialiasing, true);
432 :
433 0 : Painter painter(&qp, "video_writer");
434 :
435 : // Setup duration
436 0 : INotationPlaybackPtr playback = notation->masterNotation()->playback();
437 0 : float totalPlayTimeSec = playback->totalPlayTime();
438 :
439 0 : int leadingFrameCount = static_cast<int>(actualConfig.leadingSec * actualConfig.fps);
440 0 : int scoreFrameCount = static_cast<int>(totalPlayTimeSec * actualConfig.fps);
441 0 : int trailingFrameCount = static_cast<int>(actualConfig.trailingSec * actualConfig.fps);
442 0 : int totalFrameCount = leadingFrameCount + scoreFrameCount + trailingFrameCount;
443 0 : LOGI() << "totalPlayTime: " << totalPlayTimeSec << " sec" << " frame count " << totalFrameCount;
444 :
445 0 : m_progress.start();
446 :
447 : // Add score title
448 0 : if (!generateLeadingFrames(encoder, notation, painter, frame, actualConfig, totalFrameCount)) {
449 0 : return;
450 : }
451 :
452 : // Add score frames
453 0 : if (!generateScoreFrames(encoder, notation, painter, frame, actualConfig, totalPlayTimeSec, leadingFrameCount, totalFrameCount)) {
454 0 : return;
455 : }
456 :
457 : // Add "Made with MuseScore"
458 0 : if (!generateTrailingFrames(encoder, actualConfig)) {
459 0 : return;
460 : }
461 :
462 0 : m_writeRet = muse::make_ok();
463 0 : m_progress.finish(muse::make_ok());
464 0 : }
465 :
466 0 : bool VideoWriter::generateLeadingFrames(muse::media::IVideoEncoderPtr encoder, INotationPtr notation,
467 : Painter& painter, QImage& frame,
468 : const Config& config, int totalFrameCount)
469 : {
470 0 : int leadingFrameCount = static_cast<int>(config.leadingSec * config.fps);
471 0 : if (leadingFrameCount <= 0) {
472 0 : return true;
473 : }
474 :
475 0 : muse::String title = notationTitle(notation);
476 0 : muse::String subtitle = notationSubtitle(notation);
477 :
478 0 : auto scaledFontPointSize = [&config](double basePixelSize) {
479 0 : double pixelSize = basePixelSize * config.height / 1080.0;
480 0 : return pixelSize * 72.0 / engraving::DPI;
481 0 : };
482 :
483 0 : Font titleFont(Font::FontFamily(u"Muse Sans"), Font::Type::Text);
484 0 : titleFont.setPointSizeF(scaledFontPointSize(128.0));
485 0 : titleFont.setWeight(Font::Weight::Medium);
486 :
487 0 : Font subtitleFont(titleFont);
488 0 : subtitleFont.setPointSizeF(scaledFontPointSize(48.0));
489 :
490 0 : muse::RectF frameRect = muse::RectF::fromQRectF(QRectF(frame.rect()));
491 :
492 0 : const double maxTextWidth = frameRect.width() * 0.9;
493 0 : const double textLeft = (frameRect.width() - maxTextWidth) / 2.0;
494 :
495 0 : auto lineCount = [&](const FontMetrics& fm, const muse::String& text) {
496 0 : if (text.isEmpty()) {
497 0 : return 0;
498 : }
499 0 : double textWidth = fm.horizontalAdvance(text);
500 0 : return std::max(1, static_cast<int>(std::ceil(textWidth / maxTextWidth)));
501 0 : };
502 :
503 0 : FontMetrics titleFontMetrics(titleFont);
504 0 : const int titleLines = lineCount(titleFontMetrics, title);
505 0 : const double titleHeight = titleLines * titleFontMetrics.lineSpacing();
506 :
507 0 : FontMetrics subtitleFontMetrics(subtitleFont);
508 0 : const int subtitleLines = lineCount(subtitleFontMetrics, subtitle);
509 0 : const double subtitleHeight = subtitleLines * subtitleFontMetrics.lineSpacing();
510 :
511 0 : double centerY = frameRect.center().y();
512 0 : double titleTop = centerY - titleHeight / 2.0;
513 0 : muse::RectF titleRect(textLeft, titleTop, maxTextWidth, titleHeight);
514 :
515 0 : const double subtitleOffset = config.height / 20.0;
516 0 : double subtitleTop = titleRect.bottom() + subtitleOffset;
517 0 : muse::RectF subtitleRect(textLeft, subtitleTop, maxTextWidth, subtitleHeight);
518 :
519 0 : for (int f = 0; f < leadingFrameCount; f++) {
520 0 : if (m_abort) {
521 0 : m_writeRet = make_ret(muse::Ret::Code::Cancel);
522 0 : m_progress.finish(m_writeRet);
523 0 : return false;
524 : }
525 :
526 0 : m_progress.progress(f, totalFrameCount);
527 :
528 0 : painter.fillRect(frameRect, Color::BLACK);
529 0 : painter.setPen(Color::WHITE);
530 0 : painter.setFont(titleFont);
531 0 : painter.drawText(titleRect, AlignCenter, TextWordWrap, title);
532 :
533 0 : if (!subtitle.isEmpty()) {
534 0 : painter.setFont(subtitleFont);
535 0 : painter.drawText(subtitleRect, AlignCenter, TextWordWrap, subtitle);
536 : }
537 :
538 0 : encoder->encodeImage(frame);
539 : }
540 :
541 0 : return true;
542 0 : }
543 :
544 0 : bool VideoWriter::generateTrailingFrames(muse::media::IVideoEncoderPtr encoder, const Config& config)
545 : {
546 0 : int trailingFrameCount = static_cast<int>(config.trailingSec * config.fps);
547 0 : if (trailingFrameCount <= 0) {
548 0 : return true;
549 : }
550 :
551 0 : static const muse::io::path_t RESOURCE_PATH = ":/videoexport/internal/resources/video_made_with.mp4";
552 0 : muse::ByteArray videoData = fileSystem()->readFile(RESOURCE_PATH).val;
553 :
554 0 : encoder->encodeVideo(videoData, trailingFrameCount);
555 :
556 0 : return true;
557 0 : }
558 :
559 0 : bool VideoWriter::generateScoreFrames(muse::media::IVideoEncoderPtr encoder, INotationPtr notation,
560 : Painter& painter, QImage& frame,
561 : const Config& config, float totalPlayTimeSec,
562 : int leadingFrameCount, int totalFrameCount)
563 : {
564 0 : int scoreFrameCount = static_cast<int>(totalPlayTimeSec * config.fps);
565 0 : if (scoreFrameCount <= 0) {
566 0 : return true;
567 : }
568 :
569 0 : PageList pages = notation->elements()->pages();
570 0 : auto painting = notation->painting();
571 0 : INotationPlaybackPtr playback = notation->masterNotation()->playback();
572 0 : muse::RectF frameRect = muse::RectF::fromQRectF(QRectF(frame.rect()));
573 :
574 0 : auto pageByTick = [](const PageList& pages, tick_t tick) -> const Page* {
575 0 : for (const Page* p : pages) {
576 0 : if (tick < static_cast<tick_t>(p->endTick().ticks())) {
577 0 : return p;
578 : }
579 : }
580 0 : return nullptr;
581 : };
582 :
583 0 : const Color CURSOR_COLOR = Color(2, 109, 203, 127);
584 :
585 0 : PlaybackCursor cursor(iocContext());
586 0 : cursor.setNotation(notation);
587 :
588 0 : for (int f = 0; f < scoreFrameCount; f++) {
589 0 : if (m_abort) {
590 0 : m_writeRet = make_ret(muse::Ret::Code::Cancel);
591 0 : m_progress.finish(m_writeRet);
592 0 : return false;
593 : }
594 :
595 0 : m_progress.progress(leadingFrameCount + f, totalFrameCount);
596 :
597 0 : float currentTimeSec = static_cast<float>(f) / config.fps;
598 0 : if (currentTimeSec > totalPlayTimeSec) {
599 0 : currentTimeSec = totalPlayTimeSec;
600 : }
601 :
602 0 : tick_t tick = playback->secToTick(currentTimeSec);
603 :
604 0 : const Page* page = pageByTick(pages, tick);
605 0 : if (!page) {
606 0 : break;
607 : }
608 :
609 0 : if (!page->firstMeasure()) {
610 : // Skip pages with no notation
611 0 : continue;
612 : }
613 :
614 0 : INotationPainting::Options opt;
615 0 : opt.fromPage = static_cast<int>(page->pageNumber());
616 0 : opt.toPage = opt.fromPage;
617 0 : opt.deviceDpi = config.canvasDpi;
618 :
619 0 : painter.fillRect(frameRect, Color::BLACK);
620 :
621 0 : painter.save();
622 0 : painter.translate(config.moveToCenter);
623 :
624 0 : painting->paintPrint(&painter, opt);
625 :
626 0 : cursor.move(tick);
627 :
628 0 : muse::RectF cursorRect = cursor.rect();
629 0 : muse::PointF pagePos = page->pos();
630 0 : muse::RectF cursorAbsRect = cursorRect.translated(-pagePos);
631 :
632 0 : painter.fillRect(cursorAbsRect, CURSOR_COLOR);
633 :
634 0 : painter.restore();
635 :
636 0 : encoder->encodeImage(frame);
637 0 : }
638 :
639 0 : return true;
640 0 : }
|