LCOV - code coverage report
Current view: top level - src/importexport/videoexport/internal - videowriter.cpp Coverage Total Hit
Test: filtered_coverage.info Lines: 0.0 % 377 0
Test Date: 2026-11-05 04:07:02

            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 : }
        

Generated by: LCOV version 2.0-1