LCOV - code coverage report
Current view: top level - src/importexport/lyricsexport/internal - lrcwriter.cpp Coverage Total Hit
Test: filtered_coverage.info Lines: 87.0 % 100 87
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) 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 "lrcwriter.h"
      24              : 
      25              : #include <QBuffer>
      26              : 
      27              : #include "global/io/buffer.h"
      28              : #include "global/io/file.h"
      29              : #include "global/types/ret.h"
      30              : 
      31              : #include "engraving/dom/lyrics.h"
      32              : #include "engraving/dom/masterscore.h"
      33              : #include "engraving/dom/repeatlist.h"
      34              : 
      35              : #include "notation/inotation.h"
      36              : #include "notation/inotationelements.h" // IWYU pragma: keep
      37              : 
      38              : using namespace Qt::Literals;
      39              : 
      40              : using namespace muse;
      41              : using namespace muse::io;
      42              : using namespace mu::engraving;
      43              : using namespace mu::project;
      44              : using namespace mu::iex::lrcexport;
      45              : 
      46            0 : std::vector<INotationWriter::UnitType> LRCWriter::supportedUnitTypes() const
      47              : {
      48            0 :     return { UnitType::PER_PART };
      49              : }
      50              : 
      51            0 : bool LRCWriter::supportsUnitType(UnitType ut) const { return ut == UnitType::PER_PART; }
      52              : 
      53            0 : muse::Ret LRCWriter::write(notation::INotationPtr notation, muse::io::IODevice& device, const Options&)
      54              : {
      55            0 :     Score* score = notation->elements()->msScore();
      56            0 :     bool enhancedLrc = configuration()->lrcUseEnhancedFormat();
      57              : 
      58            0 :     return doWrite(score, &device, enhancedLrc);
      59              : }
      60              : 
      61           12 : bool LRCWriter::writeScore(mu::engraving::Score* score, const muse::io::path_t& path, bool enhancedLrc)
      62              : {
      63           12 :     auto outBuf = Buffer::opened(IODevice::WriteOnly);
      64           12 :     bool res = doWrite(score, &outBuf, enhancedLrc) && !outBuf.hasError();
      65           12 :     if (!res) {
      66            0 :         return res;
      67              :     }
      68           12 :     outBuf.close();
      69              : 
      70           12 :     return File::writeFile(path, outBuf.data());
      71           12 : }
      72              : 
      73            0 : muse::Ret LRCWriter::writeList(const notation::INotationPtrList&, muse::io::IODevice&, const Options&)
      74              : {
      75            0 :     return make_ret(Ret::Code::NotSupported);
      76              : }
      77              : 
      78           12 : void LRCWriter::writeMetadata(muse::io::IODevice* device, const engraving::Score* score) const
      79              : {
      80           12 :     QString metadata;
      81              : 
      82              :     // Title
      83           12 :     const QString title = score->metaTag(u"workTitle").toQString();
      84           12 :     if (!title.isEmpty()) {
      85           24 :         metadata += u"[ti:%1]\n"_s.arg(title);
      86              :     }
      87              : 
      88              :     // Composer/Artist
      89           12 :     const QString artist = score->metaTag(u"composer").toQString();
      90           12 :     if (!artist.isEmpty()) {
      91           24 :         metadata += u"[ar:%1]\n"_s.arg(artist);
      92              :     }
      93              : 
      94           12 :     if (!metadata.isEmpty()) {
      95           12 :         device->write(metadata.toUtf8());
      96              :     }
      97           12 : }
      98              : 
      99           12 : muse::Ret LRCWriter::doWrite(mu::engraving::Score* score, muse::io::IODevice* device, bool enhancedLrc)
     100              : {
     101           12 :     writeMetadata(device, score);
     102              : 
     103           12 :     const auto lyrics = collectLyrics(score);
     104              : 
     105              :     // Write lyrics
     106          836 :     for (const auto& [timestamp, text] : lyrics) {
     107          824 :         if (enhancedLrc) {
     108              :             // As there should only be words we replace spaces by "-"
     109          412 :             QString lyricsText = text;
     110          824 :             lyricsText.replace(QRegularExpression("\\s"), "-");
     111          412 :             lyricsText.replace(u'\u00A0', u'-');
     112              : 
     113          412 :             device->write(QString("[%1] <%1> %2\n").arg(formatTimestamp(timestamp), lyricsText).toUtf8());
     114          412 :         } else {
     115          412 :             device->write(QString("[%1]%2\n").arg(formatTimestamp(timestamp), text).toUtf8());
     116              :         }
     117              :     }
     118              : 
     119           24 :     return make_ok();
     120           12 : }
     121              : 
     122           12 : std::map<double, QString> LRCWriter::collectLyrics(const mu::engraving::Score* score)
     123              : {
     124           12 :     std::map<double, QString> lyrics;
     125           12 :     const RepeatList& repeats = score->repeatList();
     126              : 
     127              :     track_idx_t trackNumber;
     128              :     int lyricNumber;
     129              : 
     130           12 :     findTrackAndLyricToExport(score, trackNumber, lyricNumber);
     131              : 
     132           72 :     for (const RepeatSegment* rs : repeats) {
     133           60 :         const int tickOffset = rs->utick - rs->tick;
     134              : 
     135          396 :         for (const MeasureBase* mb : rs->measureList()) {
     136          336 :             if (!mb->isMeasure()) {
     137            0 :                 continue;
     138              :             }
     139              : 
     140         2838 :             for (const Segment* seg = toMeasure(mb)->first(); seg; seg = seg->next()) {
     141         2502 :                 if (!seg->isChordRestType()) {
     142         1158 :                     continue;
     143              :                 }
     144              : 
     145         1344 :                 EngravingItem* e = seg->element(trackNumber);
     146         1344 :                 if (!e || !e->isChordRest()) {
     147          212 :                     continue;
     148              :                 }
     149              : 
     150         1972 :                 for (const Lyrics* l : toChordRest(e)->lyrics()) {
     151          840 :                     if (l->plainText().isEmpty()) {
     152           16 :                         continue;
     153              :                     }
     154              : 
     155          824 :                     if (lyricNumber == l->subtype()) {
     156          824 :                         const double time = score->utick2utime(l->tick().ticks() + tickOffset) * 1000;
     157          824 :                         lyrics.insert_or_assign(time, l->plainText());
     158              :                     }
     159              :                 }
     160              :             }
     161              :         }
     162              :     }
     163           12 :     return lyrics;
     164            0 : }
     165              : 
     166          824 : QString LRCWriter::formatTimestamp(double ms) const
     167              : {
     168          824 :     const int totalSec = static_cast<int>(ms / 1000);
     169          824 :     return u"%1:%2.%3"_s
     170         1648 :            .arg(totalSec / 60, 2, 10, QLatin1Char('0'))
     171         2472 :            .arg(totalSec % 60, 2, 10, QLatin1Char('0'))
     172         1648 :            .arg(static_cast<int>(ms) % 1000 / 10, 2, 10, QLatin1Char('0'));
     173              : }
     174              : 
     175           12 : void LRCWriter::findTrackAndLyricToExport(const engraving::Score* score, mu::engraving::track_idx_t& trackNumber, int& lyricNumber)
     176              : {
     177           12 :     bool lyricsFound = false;
     178           12 :     trackNumber = 0;
     179           12 :     lyricNumber = 0;
     180              : 
     181           12 :     const RepeatList& repeats = score->repeatList();
     182              : 
     183           52 :     for (const RepeatSegment* rs : repeats) {
     184          276 :         for (const MeasureBase* mb : rs->measureList()) {
     185          236 :             if (!mb->isMeasure()) {
     186            0 :                 continue;
     187              :             }
     188              : 
     189         1904 :             for (const Segment* seg = toMeasure(mb)->first(); seg; seg = seg->next()) {
     190         1672 :                 if (!seg->isChordRestType()) {
     191          740 :                     continue;
     192              :                 }
     193              : 
     194        12068 :                 for (const EngravingItem* e : seg->elist()) {
     195        11140 :                     if (!e || !e->isChordRest()) {
     196         8190 :                         continue;
     197              :                     }
     198              : 
     199         3734 :                     for (const Lyrics* l : toChordRest(e)->lyrics()) {
     200              :                         // if (l->text().empty())
     201          784 :                         if (l->plainText().isEmpty()) {
     202           12 :                             continue;
     203              :                         }
     204              : 
     205          772 :                         if (!lyricsFound) {
     206           10 :                             lyricsFound = true;
     207           10 :                             trackNumber = e->track();
     208           10 :                             lyricNumber = l->subtype();
     209           10 :                             continue;
     210              :                         }
     211              : 
     212              :                         // We check if we have a better option
     213          762 :                         if (trackNumber > e->track()) {
     214            6 :                             trackNumber = e->track();
     215            6 :                             lyricNumber = l->subtype();
     216          756 :                         } else if (trackNumber == e->track()) {
     217          466 :                             lyricNumber = std::min(lyricNumber, l->subtype());
     218              :                         }
     219              :                     }
     220              :                     // If we have already chosen the lowest/prioritized option we can return (no better option available)
     221         2950 :                     if (lyricsFound && (trackNumber == 0) && (lyricNumber == 0)) {
     222            4 :                         return;
     223              :                     }
     224              :                 }
     225              :             }
     226              :         }
     227              :     }
     228              : }
        

Generated by: LCOV version 2.0-1