Skip to content

Commit eedfcc3

Browse files
committed
Support for the improved lrc format has been added.
Priority has been given to the first stave and the first voices. Tests have been added.
1 parent 4963305 commit eedfcc3

31 files changed

+31382
-54
lines changed

src/importexport/lyricsexport/CMakeLists.txt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,3 +41,6 @@ endif()
4141

4242
setup_module()
4343

44+
if (MUE_BUILD_IMPORTEXPORT_TESTS)
45+
add_subdirectory(tests)
46+
endif()

src/importexport/lyricsexport/internal/iex_lyricsexport.cpp

Lines changed: 153 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -20,15 +20,18 @@
2020
* along with this program. If not, see <https://www.gnu.org/licenses/>.
2121
*/
2222

23-
#include "iex_lyricsexport.h"
23+
#include "io/file.h"
2424

2525
#include <QBuffer>
2626

2727
#include "engraving/dom/masterscore.h"
2828
#include "engraving/dom/repeatlist.h"
2929
#include "engraving/dom/lyrics.h"
3030

31+
#include "iex_lyricsexport.h"
32+
3133
using namespace mu::engraving;
34+
using namespace muse::io;
3235

3336
namespace mu::iex::lrcexport {
3437
// Interface implementation
@@ -37,43 +40,65 @@ std::vector<project::INotationWriter::UnitType> LRCWriter::supportedUnitTypes()
3740
return { UnitType::PER_PART };
3841
}
3942

43+
//
44+
// LRCWriter::supportsUnitType
45+
//
46+
4047
bool LRCWriter::supportsUnitType(UnitType ut) const { return ut == UnitType::PER_PART; }
4148

49+
//
50+
// LRCWriter::write
51+
//
52+
4253
muse::Ret LRCWriter::write(notation::INotationPtr notation, muse::io::IODevice& device, const Options&)
4354
{
4455
Score* score = notation->elements()->msScore();
45-
QByteArray data;
46-
QBuffer buffer(&data);
47-
bool enhancedFormat = configuration()->lrcUseEnhancedFormat();
56+
bool enhancedLrc = configuration()->lrcUseEnhancedFormat();
4857

49-
/***********
50-
*
51-
* PENDING....
52-
Is there any advantage to writing to a buffer first, and then writing that buffer to the device? It would seem more efficient to me to write to the device directly.
53-
54-
******/
58+
return write(score, &device, enhancedLrc);
59+
}
5560

56-
buffer.open(QIODevice::WriteOnly);
61+
//
62+
// LRCWriter::exportLrc
63+
//
5764

58-
writeMetadata(buffer, score);
65+
bool LRCWriter::exportLrc(mu::engraving::Score* score, muse::io::IODevice* device, bool enhancedLrc)
66+
{
67+
write(score, device, enhancedLrc);
68+
return true;
69+
}
5970

60-
const auto lyrics = collectLyrics(score);
71+
//
72+
// LRCWriter::writeScore
73+
//
6174

62-
// Write lyrics
63-
for (auto it = lyrics.constBegin(); it != lyrics.constEnd(); ++it) {
64-
buffer.write(QString("[%1]%2\n").arg(formatTimestamp(it.key()), it.value()).toUtf8());
75+
bool LRCWriter::writeScore(mu::engraving::Score* score, const muse::io::path_t& path, bool enhancedLrc)
76+
{
77+
File f(path);
78+
if (!f.open(IODevice::WriteOnly)) {
79+
return false;
6580
}
6681

67-
device.write(data);
68-
return muse::Ret(muse::Ret::Code::Ok);
82+
bool res = exportLrc(score, &f, enhancedLrc) && !f.hasError();
83+
f.close();
84+
85+
return res;
6986
}
7087

88+
//
89+
// LRCWriter::writeList
90+
//
91+
7192
muse::Ret LRCWriter::writeList(const notation::INotationPtrList&, muse::io::IODevice&, const Options&)
7293
{
7394
return muse::Ret(muse::Ret::Code::NotSupported);
7495
}
7596

76-
void LRCWriter::writeMetadata(QIODevice& device, const engraving::Score* score) const
97+
//
98+
// LRCWriter::writeMetadata
99+
//
100+
101+
void LRCWriter::writeMetadata(muse::io::IODevice* device, const engraving::Score* score) const
77102
{
78103
QString metadata;
79104

@@ -90,69 +115,80 @@ void LRCWriter::writeMetadata(QIODevice& device, const engraving::Score* score)
90115
}
91116

92117
if (!metadata.isEmpty()) {
93-
device.write(metadata.toUtf8());
118+
device->write(metadata.toUtf8());
94119
}
95120
}
96121

97-
// Core lyric collection (simplified)
98-
QMap<qreal, QString> LRCWriter::collectLyrics(const Score* score) const
122+
//
123+
// LRCWriter::write
124+
//
125+
126+
muse::Ret LRCWriter::write(mu::engraving::Score* score, muse::io::IODevice* device, bool enhancedLrc)
99127
{
100-
QMap<qreal, QString> lyrics;
101-
const RepeatList& repeats = score->repeatList();
128+
writeMetadata(device, score);
102129

103-
LOGI() << "tpacebes ";
130+
const auto lyrics = collectLyrics(score);
104131

105-
int paabRepeatSegment = 0;
132+
// Write lyrics
133+
for (auto it = lyrics.constBegin(); it != lyrics.constEnd(); ++it) {
134+
if (enhancedLrc) {
135+
// As there should only be words we replace spaces by "-"
136+
QString lyricsText = it.value();
137+
lyricsText.replace(QRegularExpression("\\s"), "-");
138+
lyricsText.replace(QChar(0x00A0), QChar('-'));
139+
140+
device->write(QString("[%1] <%1> %2\n").arg(formatTimestamp(it.key()), lyricsText).toUtf8());
141+
} else {
142+
device->write(QString("[%1]%2\n").arg(formatTimestamp(it.key()), it.value()).toUtf8());
143+
}
144+
}
106145

107-
for (const RepeatSegment* rs : repeats) {
108-
const int tickOffset = rs->utick - rs->tick;
146+
return muse::Ret(muse::Ret::Code::Ok);
147+
}
109148

110-
++paabRepeatSegment;
149+
//
150+
// LRCWriter::collectLyrics
151+
//
111152

112-
LOGI() << "tpacebes repeat segment " << paabRepeatSegment;
153+
QMap<qreal, QString> LRCWriter::collectLyrics(const mu::engraving::Score* score)
154+
{
155+
QMap<qreal, QString> lyrics;
156+
const RepeatList& repeats = score->repeatList();
113157

114-
int paabMeasureBase = 0;
158+
staff_idx_t lyricsStaff;
159+
voice_idx_t lyricsVoice;
160+
int lyricNumber;
115161

116-
for (const MeasureBase* mb = rs->firstMeasure(); mb; mb = mb->next()) {
162+
findStaffVoiceAndLyricToExport(score, lyricsStaff, lyricsVoice, lyricNumber);
117163

118-
++paabMeasureBase;
164+
for (const RepeatSegment* rs : repeats) {
165+
const int tickOffset = rs->utick - rs->tick;
119166

167+
for (const MeasureBase* mb = rs->firstMeasure(); mb; mb = mb->next()) {
120168
if (!mb->isMeasure()) {
121169
continue;
122170
}
123171

124-
LOGI() << "tpacebes measure base " << paabMeasureBase;
125-
126-
int paabSegment = 0;
127172
for (Segment* seg = toMeasure(mb)->first(); seg; seg = seg->next()) {
128-
++paabSegment;
129173
if (!seg->isChordRestType()) {
130174
continue;
131175
}
132176

133-
LOGI() << "tpacebes paabSegment " << paabSegment;
134-
135-
int paabEngravingItem = 0;
136177
for (EngravingItem* e : seg->elist()) {
137-
++paabEngravingItem;
138178
if (!e || !e->isChordRest()) {
139179
continue;
140180
}
141181

142-
LOGI() << "tpacebes paabEngravingItem " << paabEngravingItem;
143-
144-
int paabLyrics = 0;
145182
for (Lyrics* l : toChordRest(e)->lyrics()) {
146-
++paabLyrics;
147183
// if (l->text().empty())
148184
if (l->plainText().isEmpty()) {
149185
continue;
150186
}
151-
LOGI() << "tpacebes paabLyrics " << paabLyrics;
152187

153-
const qreal time = score->utick2utime(l->tick().ticks() + tickOffset) * 1000;
154-
lyrics.insert(time, l->plainText());
155-
LOGI() << "tpacebes insertamos Time " << time << "==>" << l->plainText() << "<==";
188+
if ((lyricsStaff == e->staffIdx()) && (lyricsVoice == e->voice()) && (lyricNumber == l->subtype())) {
189+
const qreal time = score->utick2utime(l->tick().ticks() + tickOffset) * 1000;
190+
lyrics.insert(time, l->plainText());
191+
}
156192
}
157193
}
158194
}
@@ -161,6 +197,10 @@ QMap<qreal, QString> LRCWriter::collectLyrics(const Score* score) const
161197
return lyrics;
162198
}
163199

200+
//
201+
// LRCWriter::formatTimestamp
202+
//
203+
164204
QString LRCWriter::formatTimestamp(qreal ms) const
165205
{
166206
const int totalSec = static_cast<int>(ms / 1000);
@@ -169,4 +209,68 @@ QString LRCWriter::formatTimestamp(qreal ms) const
169209
.arg(totalSec % 60, 2, 10, QLatin1Char('0'))
170210
.arg(static_cast<int>(ms) % 1000 / 10, 2, 10, QLatin1Char('0'));
171211
}
212+
213+
//
214+
// LRCWriter::findStaffVoiceAndLyricToExport
215+
//
216+
217+
void LRCWriter::findStaffVoiceAndLyricToExport(const mu::engraving::Score* score, mu::engraving::staff_idx_t& staff,
218+
mu::engraving::voice_idx_t& voice, int& lyricNumber)
219+
{
220+
bool lyricsFound = false;
221+
staff = 0;
222+
voice = 0;
223+
lyricNumber = 0;
224+
225+
const RepeatList& repeats = score->repeatList();
226+
227+
for (const RepeatSegment* rs : repeats) {
228+
const int tickOffset = rs->utick - rs->tick;
229+
230+
for (const MeasureBase* mb = rs->firstMeasure(); mb; mb = mb->next()) {
231+
if (!mb->isMeasure()) {
232+
continue;
233+
}
234+
235+
for (Segment* seg = toMeasure(mb)->first(); seg; seg = seg->next()) {
236+
if (!seg->isChordRestType()) {
237+
continue;
238+
}
239+
240+
for (EngravingItem* e : seg->elist()) {
241+
if (!e || !e->isChordRest()) {
242+
continue;
243+
}
244+
245+
for (Lyrics* l : toChordRest(e)->lyrics()) {
246+
// if (l->text().empty())
247+
if (l->plainText().isEmpty()) {
248+
continue;
249+
} else {
250+
if (!lyricsFound) {
251+
lyricsFound = true;
252+
staff = e->staffIdx();
253+
voice = e->voice();
254+
lyricNumber = l->subtype();
255+
} else {
256+
if (staff > e->staffIdx()) {
257+
staff = e->staffIdx();
258+
voice = e->voice();
259+
lyricNumber = l->subtype();
260+
} else if (staff == e->staffIdx()) {
261+
if (voice > e->voice()) {
262+
voice = e->voice();
263+
lyricNumber = l->subtype();
264+
} else if (voice == e->voice()) {
265+
lyricNumber = min(lyricNumber, l->subtype());
266+
}
267+
}
268+
}
269+
}
270+
}
271+
}
272+
}
273+
}
274+
}
275+
}
172276
} // namespace mu::iex::lrcexport

src/importexport/lyricsexport/internal/iex_lyricsexport.h

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,9 @@
2525
#include <QIODevice>
2626

2727
#include "project/inotationwriter.h"
28-
#include "../ilyricsexportconfiguration.h"
28+
#include "engraving/engravingerrors.h"
29+
30+
#include "importexport/lyricsexport/ilyricsexportconfiguration.h"
2931

3032
namespace mu::engraving {
3133
class Score;
@@ -35,19 +37,23 @@ namespace mu::iex::lrcexport {
3537
class LRCWriter : public project::INotationWriter
3638
{
3739
public:
38-
INJECT_STATIC(mu::iex::lrcexport::LyricsExportConfiguration, configuration)
40+
INJECT_STATIC(mu::iex::lrcexport::ILyricsExportConfiguration, configuration)
3941

4042
public:
4143
// Interface implementation
4244
std::vector<UnitType> supportedUnitTypes() const override;
4345
bool supportsUnitType(UnitType) const override;
4446
muse::Ret write(notation::INotationPtr, muse::io::IODevice&, const Options&) override;
45-
void writeMetadata(QIODevice& device, const engraving::Score* score) const;
47+
void writeMetadata(muse::io::IODevice* device, const engraving::Score* score) const;
4648
muse::Ret writeList(const notation::INotationPtrList&, muse::io::IODevice&, const Options&) override;
49+
bool writeScore(mu::engraving::Score* score, const muse::io::path_t& path, bool enhancedLrc);
4750

4851
private:
52+
muse::Ret write(mu::engraving::Score* score, muse::io::IODevice*, bool);
4953
// Core lyric functionality
50-
QMap<qreal, QString> collectLyrics(const engraving::Score*) const;
54+
QMap<qreal, QString> collectLyrics(const mu::engraving::Score*);
5155
QString formatTimestamp(qreal ms) const;
56+
bool exportLrc(mu::engraving::Score*, muse::io::IODevice*, bool);
57+
void findStaffVoiceAndLyricToExport(const mu::engraving::Score*, mu::engraving::staff_idx_t&, mu::engraving::voice_idx_t&, int&);
5258
};
5359
} // namespace mu::iex::lrcexport

src/importexport/lyricsexport/internal/lyricsexportconfiguration.cpp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ static const Settings::Key LRC_USE_ENHANCED_FORMAT_KEY(module_name, "export/lrc/
3535

3636
void LyricsExportConfiguration::init()
3737
{
38-
settings()->setDefaultValue(LRC_USE_ENHANCED_FORMAT_KEY, Val(false));
38+
settings()->setDefaultValue(LRC_USE_ENHANCED_FORMAT_KEY, Val(true));
3939
}
4040

4141
async::Channel<bool> LyricsExportConfiguration::lrcUseEnhancedFormatChanged() const
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
# SPDX-License-Identifier: GPL-3.0-only
2+
# MuseScore-Studio-CLA-applies
3+
#
4+
# MuseScore Studio
5+
# Music Composition & Notation
6+
#
7+
# Copyright (C) 2021 MuseScore Limited
8+
#
9+
# This program is free software: you can redistribute it and/or modify
10+
# it under the terms of the GNU General Public License version 3 as
11+
# published by the Free Software Foundation.
12+
#
13+
# This program is distributed in the hope that it will be useful,
14+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
15+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16+
# GNU General Public License for more details.
17+
#
18+
# You should have received a copy of the GNU General Public License
19+
# along with this program. If not, see <https://www.gnu.org/licenses/>.
20+
21+
set(MODULE_TEST iex_lyricsexport_tests)
22+
23+
set(MODULE_TEST_SRC
24+
${PROJECT_SOURCE_DIR}/src/engraving/tests/utils/scorerw.cpp
25+
${PROJECT_SOURCE_DIR}/src/engraving/tests/utils/scorerw.h
26+
${PROJECT_SOURCE_DIR}/src/engraving/tests/utils/scorecomp.cpp
27+
${PROJECT_SOURCE_DIR}/src/engraving/tests/utils/scorecomp.h
28+
29+
${CMAKE_CURRENT_LIST_DIR}/environment.cpp
30+
${CMAKE_CURRENT_LIST_DIR}/lrc_tests.cpp
31+
)
32+
33+
set(MODULE_TEST_LINK
34+
engraving
35+
iex_lyricsexport
36+
)
37+
38+
set(MODULE_TEST_DATA_ROOT ${CMAKE_CURRENT_LIST_DIR})
39+
40+
include(SetupGTest)
41+

0 commit comments

Comments
 (0)