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