..

Qt实现LRC歌词的解析

网易云音乐的歌词格式共分三种:不含时间标签的纯文本格式、每一句歌词带有一个时间标签的格式,以及每一个字都带有一个时间标签的卡拉OK格式。对于第一种格式,只需把源文本按照换行符\n分割开来即可,而第三种格式自己写的程序里用不到所以先不管它,所以这里只讨论第二种格式。

按照维基百科的说明,标准的LRC格式歌词,每一行开头都有一个形如[mm:ss.xx]形式的时间标签,实际上有的歌词每一行开头可能有多个时间标签,表示这一句可能会在不同时间点重复多次。以此为标准,实现解析的思路如下:

  1. 查找第一个形如[mm:ss.xx]的时间标签。如果找不到,就把文本按照换行符简单分割,完成解析;找到的话就记录标签的位置并执行步骤2。
  2. 查找下一个时间标签的位置。如果找不到,就把最后一个时间标签之后的全部文本作为最后一句歌词,保存到结果中,执行步骤4;找到的话就记录标签的位置并执行步骤3。
  3. 比较当前标签和上一个标签的位置。如果是紧挨的话,表示这两个标签代表同一句歌词,重复步骤2;如果不是紧挨的,就把两个标签之间的文本作为歌词保存到结果中,然后重复步骤2。
  4. 把得到的结果按照时间顺序重新排列,完成解析。

每一行歌词都可以抽象为一个包含时间和文本的结构体,解析的结果使用QList维护,最终代码如下:

class LyricLine
{
public:
    LyricLine(int time, QString text):time(time), text(text){}

    int time;
    QString text;
};

QList<LyricLine*> mLines;
bool mHasTimer;

bool lyricTimeLessThan(const LyricLine* line1, const LyricLine* line2)
{
    return line1->time < line2->time;
}

bool LyricLoader::processContent(const QString &content)
{
    if (!mLines.isEmpty()) {
        qDeleteAll(mLines);
        mLines.clear();
        mHasTimer = false;
        emit lyricChanged();
    }

    const QRegExp rx("\\[(\\d+):(\\d+(\\.\\d+)?)\\]"); //用来查找时间标签的正则表达式

    // 步骤1
    int pos = rx.indexIn(content);
    if (pos == -1) {
        QStringList list = content.split('\n', QString::SkipEmptyParts);
        foreach (QString line, list)
            mLines.append(new LyricLine(0, line));

        mHasTimer = false;
    }
    else {
        int lastPos;
        QList<int> timeLabels;
        do {
            // 步骤2
            timeLabels << (rx.cap(1).toInt() * 60 + rx.cap(2).toDouble()) * 1000;
            lastPos = pos + rx.matchedLength();
            pos = rx.indexIn(content, lastPos);
            if (pos == -1) {
                QString text = content.mid(lastPos).trimmed();
                foreach (const int& time, timeLabels)
                    mLines.append(new LyricLine(time, text));
                break;
            }
            // 步骤3
            QString text = content.mid(lastPos, pos - lastPos);
            if (!text.isEmpty()) {
                foreach (const int& time, timeLabels)
                    mLines.append(new LyricLine(time, text.trimmed()));
                timeLabels.clear();
            }
        }
        while (true);
        // 步骤4
        qStableSort(mLines.begin(), mLines.end(), lyricTimeLessThan);
        mHasTimer = true;
    }

    if (!mLines.isEmpty()) {
        emit lyricChanged();
        return true;
    }

    return false;
}

播放音乐的时候,需要监听播放进度的变化,根据当前的时间点找出对应的歌词显示出来,代码如下:

int LyricLoader::getLineByPosition(const int &millisec, const int &startPos) const
{
    if (!mHasTimer || mLines.isEmpty())
        return -1;

    int result = qBound(0, startPos, mLines.size());
    while (result < mLines.size()) {
        if (mLines.at(result)->time > millisec)
            break;

        result++;
    }
    return result - 1;
}

这里startPos是上一行歌词的行号,从它开始查找是为了提高效率。具体显示歌词的话就很简单了,按照具体需求使用ListView或者QLabel都可以。