C 语言解析 lrc 歌词文件

Author Avatar
Equim 2017年2月18日
  • 在其它设备中阅读本文章
Read: 4 minWords: 826Last Updated: 17-12-25Written in: MarkdownLicense: CC-BY-NC-4.0

迁移自 CSDN 博客,大一的时候写的,稍作改动


LRC文件如上图所示。
格式为 [mm:ss.ms]歌词
但是也有单行多个时间轴的情况,即 [mm:ss.ms][mm:ss.ms]歌词

对于解析来说,比较困难的正是单行多个时间轴的情况。我的解决方法是:
(1) 对单独一行检索]后没有[的位置,即该行最后一个[]框,将指针指向下一元素,就是这行歌词的第一个字。
(2) 编写函数将最后一个[]里面mm:ss.ms格式的时间轴化为 long 的 ms 。
(3) 删除最后一个[]及里面的内容,将后面的歌词并上来重新构成该行(整个算法的核心就在这里)
(4) 重新检索该行最右边的[],直到没有[]为止
(5) 开始检索下一行,重复(1)的操作,直到下一行也没有[]为止。
(6) 整个歌词用链表储存,获取所有时间轴和歌词之后,按时间轴对链表进行排序。

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include <windows.h>
#include <mmsystem.h>
#pragma comment(lib, "WINMM.LIB")
#define LEN sizeof(lyric)

typedef struct _lyric
{
    long timeLine;
    char verse[256];
    struct _lyric* next;
} lyric;

lyric* Head = NULL;

long ms(char origin[9])                                     //将mm:ss.ms化为毫秒
{
    long result = 0;
    result = atoi(origin) * 60 * 1000 + atoi(origin + 3) * 1000 + atoi(origin + 6) * 10;
    return result;
}

// 输出模块
void OutputLyrics()
{
    lyric *p;
    for (p = Head; p != NULL; p = p->next)
        printf("%ld >> %s\n", p->timeLine, p->verse);
}

// 播放模块
void Play()
{
    clock_t e, start;
    lyric *p;
    system("mode con cols=150 lines=3");
    mciSendString("play D:\\C\\Jukebox\\Release\\audio\\ebbandflow.mp3", NULL, 0, 0);
    mciSendString("setaudio D:\\C\\Jukebox\\Release\\audio\\ebbandflow.mp3 volume to 150", NULL, 0, 0);
    start = clock();
    while (1)
    {
        Sleep(200);
        e = clock() - start;
        printf("%ld\t", e);
        p = Head;
        while (!(p->next == NULL || (p->next)->timeLine >= e)) //检索当前应显示的歌词,要么是最后一句,要么下一句的时间轴在当前时间之后
            p = p->next;
        printf("%ld >> %-130.130s\r", p->timeLine, p->verse);
    }
}

int main()
{
    FILE *lrc = fopen("D:\\C\\Jukebox\\Release\\audio\\ebbandflow.lrc", "r");
    char linePointer[256];
    int i;
    lyric *p, *q, temp, *tempMin;

    // 让linePointer指向第一个歌词串
    do
    {
        fgets(linePointer, 256, lrc);
    }
    while (!(linePointer[2] >= '0' && linePointer[2] <= '9'));

    // 对每一行歌词进行操作
    do
    {
        if (linePointer[strlen(linePointer) - 1] == '\n')
            linePointer[strlen(linePointer) - 1] = '\0';    //删除末尾的回车
        do
        {
            i = 0;
            do
            {
                i++;
            } while (!(linePointer[i - 1] == ']' && linePointer[i] != '[')); //让i指向最右边的[]右侧]的右边,即指向该行歌词的第一个字
            p = (lyric*)malloc(LEN);
            p->timeLine = ms(linePointer + i - 9);
            strcpy(p->verse, linePointer + i);
            if (Head == NULL)
                Head = p;
            else
                q->next = p;
            q = p;
            linePointer[i - 10] = '\0';                 //删除最后的括号
            strcat(linePointer, q->verse);               //巧妙地接回去便于继续解析
        } while (linePointer[0] == '[');                 //解析到没有括号为止
    } while (!(fgets(linePointer, 256, lrc) == NULL || linePointer[0] != '[')); //到最后一行
    q->next = NULL;
    fclose(lrc);

    // 按时间轴排序
    for (p = Head; p != NULL; p = p->next)               //比较笨拙的选择排序,按时间轴从小到大
    {
        tempMin = p;
        for (q = p->next; q != NULL; q = q->next)
            if (tempMin->timeLine > q->timeLine)       //找最小的
                tempMin = q;                            //寻找比最小还要小的,用临时指针标记
        if (tempMin != p)                               //如果确实找到了更小的
        {
            temp = *tempMin;
            *tempMin = *p;
            *p = temp;
            temp.next = tempMin->next;
            tempMin->next = p->next;
            p->next = temp.next;
        }
    }

    // 播放测试或打印到屏幕
    Play();
//  OutputLyrics();
    return 0;
}

在我这边的环境下目前仅支持 ANSI 编码的 lrc 文件(Unicode 和 UTF-8 会变乱码)

同步播放的效果

按条导出的效果

知识共享许可协议
本文采用知识共享署名-非商业性使用 4.0 国际许可协议进行许可。

本文链接:https://ekyu.moe/article/analyze-lrc-file-using-c/