Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ option(MUE_BUILD_IMPEXP_GUITARPRO_MODULE "Build importexport guitarpro module" O
option(MUE_BUILD_IMPEXP_MEI_MODULE "Build importexport mei module" ON)
option(MUE_BUILD_IMPEXP_VIDEOEXPORT_MODULE "Build importexport videoexport module" OFF)
option(MUE_BUILD_IMPEXP_TABLEDIT_MODULE "Build importexport tabledit module" ON)
option(MUE_BUILD_IMPEXP_LYRICS_MODULE "Build importexport lyrics module" ON)

option(MUE_BUILD_IMPORTEXPORT_TESTS "Build importexport tests" ON)
option(MUE_BUILD_INSPECTOR_MODULE "Build inspector module" ON)
Expand Down
2 changes: 2 additions & 0 deletions SetupConfigure.cmake
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,7 @@ if(BUILD_CONFIGURATION STREQUAL "APP-WEB")
set(MUE_BUILD_IMPEXP_MEI_MODULE OFF)
set(MUE_BUILD_IMPEXP_VIDEOEXPORT_MODULE OFF)
set(MUE_BUILD_IMPEXP_TABLEDIT_MODULE OFF)
set(MUE_BUILD_IMPEXP_LYRICS_MODULE OFF)

set(MUE_INSTALL_SOUNDFONT OFF)

Expand Down Expand Up @@ -246,6 +247,7 @@ if(BUILD_CONFIGURATION STREQUAL "VTEST")
set(MUE_BUILD_IMPEXP_MEI_MODULE OFF)
set(MUE_BUILD_IMPEXP_VIDEOEXPORT_MODULE OFF)
set(MUE_BUILD_IMPEXP_TABLEDIT_MODULE OFF)
set(MUE_BUILD_IMPEXP_LYRICS_MODULE OFF)

set(MUE_INSTALL_SOUNDFONT OFF)

Expand Down
3 changes: 3 additions & 0 deletions src/app/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,9 @@ endif ()
if (MUE_BUILD_IMPEXP_TABLEDIT_MODULE)
list(APPEND LINK_LIB iex_tabledit)
endif ()
if (MUE_BUILD_IMPEXP_LYRICS_MODULE)
list(APPEND LINK_LIB iex_lyricsexport)
endif ()

set (MSCORE_APPEND_SRC)

Expand Down
1 change: 1 addition & 0 deletions src/app/app_config.h.in
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@
#cmakedefine MUE_BUILD_IMPEXP_MEI_MODULE ${MUE_BUILD_IMPEXP_MEI_MODULE})
#cmakedefine MUE_BUILD_IMPEXP_VIDEOEXPORT_MODULE ${MUE_BUILD_IMPEXP_VIDEOEXPORT_MODULE})
#cmakedefine MUE_BUILD_IMPEXP_TABLEDIT_MODULE ${MUE_BUILD_IMPEXP_TABLEDIT_MODULE})
#cmakedefine MUE_BUILD_IMPEXP_LYRICS_MODULE ${MUE_BUILD_IMPEXP_LYRICS_MODULE})

/* ============================================== */
/* Functions */
Expand Down
9 changes: 9 additions & 0 deletions src/app/appfactory.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,9 @@
#ifdef MUE_BUILD_IMPEXP_TABLEDIT_MODULE
#include "importexport/tabledit/tableditmodule.h"
#endif
#ifdef MUE_BUILD_IMPEXP_LYRICS_MODULE
#include "importexport/lyricsexport/lyricsexportmodule.h"
#endif

#include "inspector/inspectormodule.h"

Expand Down Expand Up @@ -355,6 +358,9 @@ std::shared_ptr<muse::IApplication> AppFactory::newGuiApp(const CmdOptions& opti
#ifdef MUE_BUILD_IMPEXP_TABLEDIT_MODULE
app->addModule(new mu::iex::tabledit::TablEditModule());
#endif
#ifdef MUE_BUILD_IMPEXP_LYRICS_MODULE
app->addModule(new mu::iex::lrcexport::LyricsExportModule());
#endif

app->addModule(new mu::inspector::InspectorModule());
app->addModule(new mu::instrumentsscene::InstrumentsSceneModule());
Expand Down Expand Up @@ -499,6 +505,9 @@ std::shared_ptr<muse::IApplication> AppFactory::newConsoleApp(const CmdOptions&
#ifdef MUE_BUILD_IMPEXP_TABLEDIT_MODULE
app->addModule(new mu::iex::tabledit::TablEditModule());
#endif
#ifdef MUE_BUILD_IMPEXP_LYRICS_MODULE
app->addModule(new mu::iex::lrcexport::LyricsExportModule());
#endif

app->addModule(new mu::inspector::InspectorModule());
app->addModule(new mu::instrumentsscene::InstrumentsSceneModule());
Expand Down
3 changes: 3 additions & 0 deletions src/importexport/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -57,3 +57,6 @@ endif()
if (MUE_BUILD_IMPEXP_TABLEDIT_MODULE)
add_subdirectory(tabledit)
endif()
if (MUE_BUILD_IMPEXP_LYRICS_MODULE)
add_subdirectory(lyricsexport)
endif()
38 changes: 38 additions & 0 deletions src/importexport/lyricsexport/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
# SPDX-License-Identifier: GPL-3.0-only
# MuseScore-Studio-CLA-applies
#
# MuseScore Studio
# Music Composition & Notation
#
# Copyright (C) 2021 MuseScore Limited
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License version 3 as
# published by the Free Software Foundation.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.

muse_create_module(iex_lyricsexport)

target_sources(iex_lyricsexport PRIVATE
lyricsexportmodule.cpp
lyricsexportmodule.h
ilyricsexportconfiguration.h

internal/lyricsexportconfiguration.cpp
internal/lyricsexportconfiguration.h
internal/lrcwriter.cpp
internal/lrcwriter.h
)

target_link_libraries(iex_lyricsexport PRIVATE engraving)

if (MUE_BUILD_IMPORTEXPORT_TESTS)
add_subdirectory(tests)
endif()
41 changes: 41 additions & 0 deletions src/importexport/lyricsexport/ilyricsexportconfiguration.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
/*
* SPDX-License-Identifier: GPL-3.0-only
* MuseScore-Studio-CLA-applies
*
* MuseScore Studio
* Music Composition & Notation
*
* Copyright (C) 2021 MuseScore Limited
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License version 3 as
* published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/

#pragma once

#include "async/channel.h"

#include "modularity/imoduleinterface.h"

namespace mu::iex::lrcexport {
class ILyricsExportConfiguration : MODULE_EXPORT_INTERFACE
{
INTERFACE_ID(ILyricsExportConfiguration)

public:
virtual ~ILyricsExportConfiguration() = default;

virtual bool lrcUseEnhancedFormat() const = 0;
virtual void setLrcUseEnhancedFormat(bool value) = 0;
virtual muse::async::Channel<bool> lrcUseEnhancedFormatChanged() const = 0;
};
}
227 changes: 227 additions & 0 deletions src/importexport/lyricsexport/internal/lrcwriter.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,227 @@
/*
* SPDX-License-Identifier: GPL-3.0-only
* MuseScore-Studio-CLA-applies
*
* MuseScore Studio
* Music Composition & Notation
*
* Copyright (C) 2025 MuseScore Limited
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License version 3 as
* published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/

#include "lrcwriter.h"

#include <QBuffer>

#include "io/file.h"
#include "types/ret.h"

#include "engraving/dom/lyrics.h"
#include "engraving/dom/masterscore.h"
#include "engraving/dom/repeatlist.h"

using namespace Qt::Literals;

using namespace muse;
using namespace muse::io;
using namespace mu::engraving;
using namespace mu::project;
using namespace mu::iex::lrcexport;

std::vector<INotationWriter::UnitType> LRCWriter::supportedUnitTypes() const
{
return { UnitType::PER_PART };
}

bool LRCWriter::supportsUnitType(UnitType ut) const { return ut == UnitType::PER_PART; }

muse::Ret LRCWriter::write(notation::INotationPtr notation, muse::io::IODevice& device, const Options&)
{
Score* score = notation->elements()->msScore();
bool enhancedLrc = configuration()->lrcUseEnhancedFormat();

return doWrite(score, &device, enhancedLrc);
}

bool LRCWriter::writeScore(mu::engraving::Score* score, const muse::io::path_t& path, bool enhancedLrc)
{
File f(path);
if (!f.open(IODevice::WriteOnly)) {
return false;
}

bool res = doWrite(score, &f, enhancedLrc) && !f.hasError();
f.close();

return res;
}

muse::Ret LRCWriter::writeList(const notation::INotationPtrList&, muse::io::IODevice&, const Options&)
{
return make_ret(Ret::Code::NotSupported);
}

void LRCWriter::writeMetadata(muse::io::IODevice* device, const engraving::Score* score) const
{
QString metadata;

// Title
const QString title = score->metaTag(u"workTitle").toQString();
if (!title.isEmpty()) {
metadata += u"[ti:%1]\n"_s.arg(title);
}

// Composer/Artist
const QString artist = score->metaTag(u"composer").toQString();
if (!artist.isEmpty()) {
metadata += u"[ar:%1]\n"_s.arg(artist);
}

if (!metadata.isEmpty()) {
device->write(metadata.toUtf8());
}
}

muse::Ret LRCWriter::doWrite(mu::engraving::Score* score, muse::io::IODevice* device, bool enhancedLrc)
{
writeMetadata(device, score);

const auto lyrics = collectLyrics(score);

// Write lyrics
for (const auto& [timestamp, text] : lyrics) {
if (enhancedLrc) {
// As there should only be words we replace spaces by "-"
QString lyricsText = text;
lyricsText.replace(QRegularExpression("\\s"), "-");
lyricsText.replace(u'\u00A0', u'-');

device->write(QString("[%1] <%1> %2\n").arg(formatTimestamp(timestamp), lyricsText).toUtf8());
} else {
device->write(QString("[%1]%2\n").arg(formatTimestamp(timestamp), text).toUtf8());
}
}

return make_ok();
}

std::map<double, QString> LRCWriter::collectLyrics(const mu::engraving::Score* score)
{
std::map<double, QString> lyrics;
const RepeatList& repeats = score->repeatList();

track_idx_t trackNumber;
int lyricNumber;

findTrackAndLyricToExport(score, trackNumber, lyricNumber);

for (const RepeatSegment* rs : repeats) {
const int tickOffset = rs->utick - rs->tick;

for (const MeasureBase* mb : rs->measureList()) {
if (!mb->isMeasure()) {
continue;
}

for (const Segment* seg = toMeasure(mb)->first(); seg; seg = seg->next()) {
if (!seg->isChordRestType()) {
continue;
}

for (const EngravingItem* e : seg->elist()) {
if (!e || !e->isChordRest()) {
continue;
}

for (const Lyrics* l : toChordRest(e)->lyrics()) {
// if (l->text().empty())
if (l->plainText().isEmpty()) {
continue;
}

if ((trackNumber == e->track()) && (lyricNumber == l->subtype())) {
const double time = score->utick2utime(l->tick().ticks() + tickOffset) * 1000;
lyrics.insert_or_assign(time, l->plainText());
}
}
}
}
}
}
return lyrics;
}

QString LRCWriter::formatTimestamp(double ms) const
{
const int totalSec = static_cast<int>(ms / 1000);
return u"%1:%2.%3"_s
.arg(totalSec / 60, 2, 10, QLatin1Char('0'))
.arg(totalSec % 60, 2, 10, QLatin1Char('0'))
.arg(static_cast<int>(ms) % 1000 / 10, 2, 10, QLatin1Char('0'));
}

void LRCWriter::findTrackAndLyricToExport(const engraving::Score* score, mu::engraving::track_idx_t& trackNumber, int& lyricNumber)
{
bool lyricsFound = false;
trackNumber = 0;
lyricNumber = 0;

const RepeatList& repeats = score->repeatList();

for (const RepeatSegment* rs : repeats) {
for (const MeasureBase* mb : rs->measureList()) {
if (!mb->isMeasure()) {
continue;
}

for (const Segment* seg = toMeasure(mb)->first(); seg; seg = seg->next()) {
if (!seg->isChordRestType()) {
continue;
}

for (const EngravingItem* e : seg->elist()) {
if (!e || !e->isChordRest()) {
continue;
}

for (const Lyrics* l : toChordRest(e)->lyrics()) {
// if (l->text().empty())
if (l->plainText().isEmpty()) {
continue;
}

if (!lyricsFound) {
lyricsFound = true;
trackNumber = e->track();
lyricNumber = l->subtype();
continue;
}

// We check if we have a better option
if (trackNumber > e->track()) {
trackNumber = e->track();
lyricNumber = l->subtype();
} else if (trackNumber == e->track()) {
lyricNumber = std::min(lyricNumber, l->subtype());
}
}
// If we have already chosen the lowest/prioritized option we can return (no better option available)
if (lyricsFound && (trackNumber == 0) && (lyricNumber == 0)) {
return;
}
}
}
}
}
}
Loading
Loading