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
104 changes: 91 additions & 13 deletions src/libdmusic/core/lyricanalysis.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,22 @@

DCORE_USE_NAMESPACE

// 解析 LRC 时间戳,正确处理厘秒(百分之一秒)格式
static qint64 parseTimeStamp(const QString &timeStr)
{
QStringList timeParts = timeStr.split('.');
QTime t = QTime::fromString(timeParts[0], "mm:ss");
if (!t.isValid()) return -1;
qint64 time = t.msecsSinceStartOfDay();
if (timeParts.size() > 1) {
QString frac = timeParts[1];
if (frac.length() == 2) time += frac.toInt() * 10; // 厘秒转毫秒
else if (frac.length() == 3) time += frac.toInt(); // 已经是毫秒
}
return time;
}

static float codecConfidenceForData(const QTextCodec *codec, const QByteArray &data, const QLocale::Country &country)

Check warning on line 36 in src/libdmusic/core/lyricanalysis.cpp

View workflow job for this annotation

GitHub Actions / cppcheck

The function 'codecConfidenceForData' is never used.
{
qreal hep_count = 0;
int non_base_latin_count = 0;
Expand Down Expand Up @@ -103,7 +118,7 @@
return qMax(0.0f, c);
}

LyricAnalysis::LyricAnalysis(): m_offset(0.00)
LyricAnalysis::LyricAnalysis()
{
qCDebug(dmMusic) << "LyricAnalysis constructor called";
}
Expand Down Expand Up @@ -139,29 +154,80 @@
qCDebug(dmMusic) << "Parsing lyrics with length:" << str.length();
auto lines = str.split("\n");
QRegExp rx("\\[([^\\]]*)\\]\\s*(\\S.*\\S|\\S)\\s*$");
// 用于检测时间戳数量的正则(不含行尾锚点)
QRegExp timestampRx("\\[\\d{2}:\\d{2}\\.\\d{2,3}\\]");
// 匹配逐字歌词中单个 [时间]文本 片段
QRegExp wordRx("\\[([^\\]]+)\\]([^\\[]*)");
QVector<QPair<qint64, QString>> tmp;
QVector<QVector<LyricWord>> tmpWord;

for (auto line : lines) {
if (rx.indexIn(line) != -1) {
auto timeStr = rx.capturedTexts()[1];
auto lyricStr = rx.capturedTexts()[2];
QTime t = QTime::fromString(timeStr, "mm:ss.z");
qint64 time = t.msecsSinceStartOfDay();
if (t.isValid()) {
tmp.push_back({time, lyricStr});
qCDebug(dmMusic) << "Parsed lyric - Time:" << timeStr << "Text:" << lyricStr;
// 检测是否包含多个时间戳(逐字歌词格式)
int timestampCount = 0;
int pos = 0;
while ((pos = timestampRx.indexIn(line, pos)) != -1) {
timestampCount++;
pos += timestampRx.matchedLength();
}

if (timestampCount > 1) {
// 逐字歌词格式:[mm:ss.xx]字[mm:ss.xx]字...
QVector<LyricWord> words;
qint64 firstTime = -1;
QString fullText;
pos = 0;
while ((pos = wordRx.indexIn(line, pos)) != -1) {
auto timeStr = wordRx.capturedTexts()[1];
auto textStr = wordRx.capturedTexts()[2];
qint64 time = parseTimeStamp(timeStr);
if (time >= 0) {
if (firstTime < 0) firstTime = time;
words.push_back({time, textStr});
fullText += textStr;
qCDebug(dmMusic) << "Parsed word - Time:" << timeStr << "ms:" << time << "Text:" << textStr;
}
pos += wordRx.matchedLength();
}
if (!words.isEmpty() && firstTime >= 0) {
tmp.push_back({firstTime, fullText});
tmpWord.push_back(words);
qCDebug(dmMusic) << "Parsed lyric line with" << words.size() << "words - Time:" << firstTime << "Text:" << fullText;
} else {
qCWarning(dmMusic) << "Invalid multi-timestamp lyric line:" << line;
}
} else if (timestampCount == 1) {
// 普通歌词格式:[mm:ss.xx]歌词文本
if (rx.indexIn(line) != -1) {
auto timeStr = rx.capturedTexts()[1];
auto lyricStr = rx.capturedTexts()[2];
qint64 time = parseTimeStamp(timeStr);
if (time >= 0) {
tmp.push_back({time, lyricStr});
tmpWord.push_back(QVector<LyricWord>()); // 空表示无逐字时间轴
qCDebug(dmMusic) << "Parsed lyric - Time:" << timeStr << "Text:" << lyricStr;
} else {
qCWarning(dmMusic) << "Invalid time format in lyric line:" << line;
}
} else {
qCWarning(dmMusic) << "Invalid time format in lyric line:" << line;
qCDebug(dmMusic) << "Skipping non-matching line:" << line;
}
} else {
qCDebug(dmMusic) << "Skipping non-matching line:" << line;
}
}

std::sort(tmp.begin(), tmp.end());
// 按时间排序(同时保持 word 和 lyric 的对应关系)
QVector<int> indices(tmp.size());
for (int i = 0; i < indices.size(); i++) indices[i] = i;
std::sort(indices.begin(), indices.end(), [&](int a, int b) {
return tmp[a].first < tmp[b].first;
});

m_allLyrics.clear();
for (auto item : tmp) {
m_allLyrics.push_back(item);
m_wordLyrics.clear();
for (int i : indices) {
m_allLyrics.push_back(tmp[i]);
m_wordLyrics.push_back(tmpWord[i]);
}
}

Expand Down Expand Up @@ -269,3 +335,15 @@
qCDebug(dmMusic) << "Getting lyrics count:" << m_allLyrics.size();
return m_allLyrics.count();
}

bool LyricAnalysis::hasWordTiming(int index) const
{
if (index < 0 || index >= m_wordLyrics.size()) return false;
return !m_wordLyrics[index].isEmpty();
}

QVector<LyricWord> LyricAnalysis::getWordTiming(int index) const
{
if (index < 0 || index >= m_wordLyrics.size()) return QVector<LyricWord>();
return m_wordLyrics[index];
}
14 changes: 11 additions & 3 deletions src/libdmusic/core/lyricanalysis.h
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
// Copyright (C) 2020 ~ 2020 Deepin Technology Co., Ltd.
// SPDX-FileCopyrightText: 2023 UnionTech Software Technology Co., Ltd.
// SPDX-FileCopyrightText: 2023 - 2026 UnionTech Software Technology Co., Ltd.
//
// SPDX-License-Identifier: GPL-3.0-or-later

Expand All @@ -7,9 +6,14 @@
#define LYRICANALYSIS_H

#include <QVector>
#include <QByteArray>

Check warning on line 9 in src/libdmusic/core/lyricanalysis.h

View workflow job for this annotation

GitHub Actions / cppcheck

Include file: <QByteArray> not found. Please note: Cppcheck does not need standard library headers to get proper results.
#include <QPair>

Check warning on line 10 in src/libdmusic/core/lyricanalysis.h

View workflow job for this annotation

GitHub Actions / cppcheck

Include file: <QPair> not found. Please note: Cppcheck does not need standard library headers to get proper results.

struct LyricWord {
qint64 time; // 时间戳(毫秒)
QString text; // 该时间点后的文本片段
};

class LyricAnalysis
{
public:
Expand All @@ -22,14 +26,18 @@
int getIndex(qint64 pos);
qint64 getPostion(int index);

// 逐字歌词支持
bool hasWordTiming(int index) const;
QVector<LyricWord> getWordTiming(int index) const;

private:
void parseLyric(const QString &str);
QString getFileCodec();

private:
QString m_filePath;
double m_offset;
QVector<QPair<qint64, QString> > m_allLyrics;
QVector<QVector<LyricWord>> m_wordLyrics; // 逐字歌词时间轴
};

#endif
25 changes: 20 additions & 5 deletions src/libdmusic/presenter.cpp
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
// Copyright (C) 2020 ~ 2026 Uniontech Software Technology Co., Ltd.
// SPDX-FileCopyrightText: 2023 UnionTech Software Technology Co., Ltd.
// SPDX-FileCopyrightText: 2023 - 2026 UnionTech Software Technology Co., Ltd.
//
// SPDX-License-Identifier: GPL-3.0-or-later

Expand Down Expand Up @@ -328,10 +327,26 @@ QVariantList Presenter::getLyrics()
}
m_data->m_lyricAnalysis.setFromFile(lrcPath);
QVector<QPair<qint64, QString> > allLyrics = m_data->m_lyricAnalysis.allLyrics();
for (QPair<qint64, QString> lyric : allLyrics) {
for (int i = 0; i < allLyrics.size(); i++) {
QVariantMap curData;
curData.insert("time", lyric.first);
curData.insert("lyric", lyric.second);
curData.insert("time", allLyrics[i].first);
curData.insert("lyric", allLyrics[i].second);

// 逐字歌词时间轴
bool hasWords = m_data->m_lyricAnalysis.hasWordTiming(i);
curData.insert("hasWordTiming", hasWords);
if (hasWords) {
QVariantList wordsList;
QVector<LyricWord> words = m_data->m_lyricAnalysis.getWordTiming(i);
for (const LyricWord &w : words) {
QVariantMap wordMap;
wordMap.insert("time", w.time);
wordMap.insert("text", w.text);
wordsList.append(wordMap);
}
curData.insert("words", wordsList);
}

lyrics.append(curData);
}
}
Expand Down
21 changes: 19 additions & 2 deletions src/music-player/lyric/LyricPage.qml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// SPDX-FileCopyrightText: 2023 UnionTech Software Technology Co., Ltd.
// SPDX-FileCopyrightText: 2023 - 2026 UnionTech Software Technology Co., Ltd.
//
// SPDX-License-Identifier: GPL-3.0-or-later

Expand All @@ -19,8 +19,10 @@ Rectangle{
property string bgImgPath: "qrc:/dsg/img/music.svg"
property bool withLrcs: lrcModel.count == 0 ? false : true
property int centerAreaWidth: 899
property real currentPosition: 0
// property int curIndex: 0
property ListModel lrcModel: ListModel{}
property var wordLyricsData: []

signal currentIndexChanged(int index)

Expand Down Expand Up @@ -156,6 +158,8 @@ Rectangle{
anchors.horizontalCenter: parent.horizontalCenter
LyricRect {
id: lyricRect
currentPosition: lrcRectItem.currentPosition
wordLyricsData: lrcRectItem.wordLyricsData
}
}
Rectangle {
Expand Down Expand Up @@ -221,6 +225,7 @@ Rectangle{

function metaChange(){
lrcModel.clear()
wordLyricsData = []

var meta = Presenter.getActivateMeta()
titleStr = meta["title"]
Expand All @@ -234,16 +239,28 @@ Rectangle{
}

var lyricList = Presenter.getLyrics();
var tempWordData = [];
for (var i = 0; i < lyricList.length; i++) {
lrcModel.append(lyricList[i])
var item = lyricList[i];
// 将 words 数据存储到单独的 JS 数组中
var words = item["words"] || [];
tempWordData.push(words);
// 创建不包含 words 的新对象,因为 ListModel 无法正确存储嵌套数据
lrcModel.append({
"time": item["time"],
"lyric": item["lyric"],
"hasWordTiming": item["hasWordTiming"]
});
}
wordLyricsData = tempWordData;

//切换shader
switchShader();
}

function positionChange(position, length) {
position = position + 500
currentPosition = position
//二分法查找位置
var lt,rt
lt = 0
Expand Down
Loading
Loading