用javaScript编写lrc歌词解析器

如果想要了解如何编写的请继续往下看,如果只需要代码,请点击这里Github

lrc歌词文件介绍

来先看一下以下歌词 Heart To Heart.lrc

[ti:Heart To Heart]
[ar:James Blunt]
[al:Heart To Heart]
[by:]
[offset:0]
[00:00.00]Heart To Heart (诚恳的) - James Blunt (詹姆仕·布朗特)
[00:13.72]There are times when
[00:14.57]I don't know where I stand
[00:16.73][00:22.40](oh  sometimes)
[00:18.49]You make me feel like
[00:19.68]I'm a boy and not a man

其中大致包含两类标签 标识标签时间标签
标识标签:主要标识歌曲的信息(作者,作词者…) 其格式为:[标识名:值]
[ar:歌手名]
[ti:歌曲名]
[al:专辑名]
[by:编辑者(指lrc歌词的制作人)]
[offset:时间补偿值]歌词整体的偏移量
时间标签:表示啥时候出现歌词 标准格式:[分钟:秒.毫秒] 歌词
除此之外还有其他格式:[分钟:秒] 歌词,[分钟:秒:毫秒] 歌词,[分钟:秒:毫秒][分钟:秒:毫秒] 歌词.


大致了解了lrc的格式就可以开始编写代码了

首先定个大致流程

  1. 先解析标识标签
  2. 再解析时间标签
  3. 封装两个解析部件(目标已经完成)
  4. 优化添加axios异步获取歌词

1. 先解析标识标签

标识标签格式已经得到那么可以用正则匹配就行了/\[[A-Za-z]+\:[^\[\]]*\]/g,
匹配完了之后当做对象返回 格式为{标签名:标签值}

function getFlagTags (text) {
        // 解析标记标签 text lrc文本 
        // return {标签名:标签值} 例如: {by:xiaohuohu}
        let res = {};
        let find = text.match(/\[[A-Za-z]+\:[^\[\]]*\]/g);// 匹标志标签
        for (let findKey in find) {
            let textArrayItem = find[findKey];
            let tagName = textArrayItem.substring(textArrayItem.indexOf('[') + 1, textArrayItem.indexOf(':'));
            let tagText = textArrayItem.substring(textArrayItem.indexOf(':') + 1, textArrayItem.indexOf(']'));
            res[tagName] = tagText;
        }
        return res;
    }

2. 再解析时间标签

这里工作就大了起来,我们再具体分析一下解析要求:

  1. 有多种标准:[分钟:秒.毫秒] 歌词,[分钟:秒] 歌词,[分钟:秒:毫秒] 歌词.需要兼容
  2. [offset:时间补偿值] 会影响时间
  3. [01:02.03][1:2.3] 无论前面是否添加0都是符合要求的
  4. [分钟:秒:毫秒][分钟:秒:毫秒]歌词可以多个时间对应一个歌词

然后我们就可以开始编写代码了

  1. 根据lrc一行一个标签的特点我们先新建lrc进行切分
var text='歌词源文件内容';
var textArray=text.split('\n');// 切割后的数组
  1. 我们依据要求1 编写一个筛选结构过滤掉标识标签以及一些不合规矩的文字,一样的正则走起/(?:\[\d+\:\d+(?:[.:]\d+)?\])+.*/
for (let textArrayKey in textArray) {
      let textArrayItem = textArray[textArrayKey];// 子项
      if (textArrayItem.match(/(?:\[\d+\:\d+(?:[.:]\d+)?\])+.*/)) {// 判断是否符合歌词的规则
        // 符合要求的
      }
}
  1. 由于要求4多个之间的问题,那我们不如先解决歌词
    其思路是将时间标签替换为 ‘空’ 剩下的就是歌词啦.
let findWord = textArrayItem.replace(/(?:\[\d+\:\d+(?:[.:]\d+)?\])+/g, '').trim();// 歌去除时间保留歌词,并去除两端多余空格
if (findWord) {// 去除歌词为空的项
    // ......
}
  1. 匹配完歌词剩下的就是时间
    先匹配多个时间块:/(?:\[\d+\:\d+(?:[.:]\d+)?\])/g歌词无视
    再对每一个时间快转为为毫秒 [m:s.ms] m * 60000 + s * 1000 + ms,
    为了拆解时方便计算我们将每一位的进率单独提出来timeRule = [60000, 1000, 1]
let timeRule = [60000, 1000, 1];// // 对应位数的毫秒数
let res={};// 结果对象 {时间:歌词}
let findTime = textArrayItem.match(/(?:\[\d+\:\d+(?:[.:]\d+)?\])/g);// 匹配多个时间 例如 [1:2.3][4:2.4]hello world
for (let findTimeKey in findTime) {
     let findTimeItem = findTime[findTimeKey].match(/\d+/g);// 切割每一个时间的m s ms部分
     let nowTime = 0;// 初始为0;
     for (let x = 0; x < findTimeItem.length; x++) {
            nowTime += parseInt(findTimeItem[x]) * timeRule[x];// 分钟,秒,毫秒转换为转毫秒之后累加
     }
     res[nowTime] = findWord;// 添加结果 {时间:歌词}
}
  1. 解决要求2[offset:0] 前面步骤2不是在过滤符合要求的歌词?那我们就在他下面再过滤[offset:0] /^\[offset\:\-?[1-9]\d+\]$/
let addTime = 0;// 偏移量
let textArrayItem = textArray[textArrayKey];
if (textArrayItem.match(/(?:\[\d+\:\d+(?:[.:]\d+)?\])+.*/)) {// 判断是否符合歌词的规则
        // 歌词
} else if (textArrayItem.match(/^\[offset\:\-?[1-9]\d+\]$/)) {// 匹配偏移时间 解决[offset:-232]操作
      // [offset:-232]正值表示整体提前,负值相反
      addTime -= parseInt(textArrayItem.substring(textArrayItem.indexOf(':') + 1, textArrayItem.length));
}
  1. addTime应用上去
let nowTime = addTime;// 初始由0变为addTime;
for (let x = 0; x < findTimeItem.length; x++) {
    nowTime += parseInt(findTimeItem[x]) * timeRule[x];// 分钟,秒,毫秒转换为转毫秒之后累加
}
  1. 最后整理一下
function getTimeTags (text) {
    // 解析歌词标签 text lrc全部文本
    // return {time:word} 例如{12300:'hello world'}
    // [m:s.ms] or [m:s:ms] or [m:s], [1:2.3] or [01:02.03] or [01:02] ...
    let res = {};// 结果
    if (typeof (text) != 'string') { return res; }// 检查参数
    let textArray = text.split('\n');// 歌词拆分
    let timeRule = [60000, 1000, 1];// 对应位数的毫秒数
    let addTime = 0;// 时间补偿
    for (let textArrayKey in textArray) {
        let textArrayItem = textArray[textArrayKey];
        if (textArrayItem.match(/(?:\[\d+\:\d+(?:[.:]\d+)?\])+.*/)) {// 判断是否符合歌词的规则
            let findWord = textArrayItem.replace(/(?:\[\d+\:\d+(?:[.:]\d+)?\])+/g, '').trim();// 歌去除时间保留歌词,并去除两端多余空格
            if (findWord) {// 去除歌词为空的项
                let findTime = textArrayItem.match(/(?:\[\d+\:\d+(?:[.:]\d+)?\])/g);// 匹配多个时间 例如 [1:2.3][4:2.4]hello world
                for (let findTimeKey in findTime) {
                    let findTimeItem = findTime[findTimeKey].match(/\d+/g);// 切割每一个时间的m s ms部分
                    let nowTime = addTime;// 初始为偏移时间 解决[offset:-232]操作
                    for (let x = 0; x < findTimeItem.length; x++) {
                        nowTime += parseInt(findTimeItem[x]) * timeRule[x];// 分钟,秒,毫秒转换为转毫秒之后累加
                    }
                    res[nowTime > 0 ? nowTime : 0] = findWord;// 限制下线时间 0
                }
            }
        } else if (textArrayItem.match(/^\[offset\:\-?[1-9]\d+\]$/)) {// 匹配偏移时间 解决[offset:-232]操作
            // [offset:-232]正值表示整体提前,负值相反
            addTime -= parseInt(textArrayItem.substring(textArrayItem.indexOf(':') + 1, textArrayItem.length));
        }
    }
    return res;
}

3. 到此为止大部分功能已经完成

接下来就是添加axios然后再封装一下就大功告成(以下代码就可以直接使用,记得添加axios哟)

function Lrc(requestUrl) {
    // lrc歌词解析器 依赖axios库
    this.flagTags = {};// 解析的标志标签 [by:xiaohuohu]
    this.timeTags = {};// 解析的歌词 [3:20.3]hello world
    this.requestText = '';// 请求的lrc内容
    this.canPlay = 0;// 当前状态 0 未加载 1 获取失败 2 解析失败 3 解析成功
    this.canPlayInf = ['外星人正在搜寻歌词,请稍后', '外星人未找到歌词', '歌词外星人无法解析'];// 与canPlay 对应提示 除3之外
    this.lastWord = '';// 上一次歌词
    this.getLrc = function () {
        // 异步获取歌词
        let _this = this;
        _this.canPlay = 0;
        axios.get(requestUrl).then(function (response) {
            let data = response.data;// 获取数据
            _this.requestText = data;
            _this.flagTags = _this.getFlagTags(data);// 解析标志
            _this.timeTags = _this.getTimeTags(data);// 解析歌词
            // 判断是否成功解析,不为空.
            _this.canPlay = (Object.keys(_this.timeTags).length == 0) ? 2 : 3;// 设置当前状态
        }).catch(function (error) {
            _this.canPlay = 1;// 设置错误状态
            console.log(error);
        });
    }
    this.getRequestText = function () {
        // 获取lrc源文件
        return this.requestText;
    }
    this.getFlagTags = function (text) {
        // 解析标记标签 text lrc文本 
        // return {标签名:标签值} 例如: {by:xiaohuohu}
        let res = {};
        if (typeof (text) !== 'string') { return res; }// 检查参数
        let find = text.match(/\[[A-Za-z]+\:[^\[\]]*\]/g);// 匹标志标签
        for (let findKey in find) {
            let textArrayItem = find[findKey];
            let tagName = textArrayItem.substring(textArrayItem.indexOf('[') + 1, textArrayItem.indexOf(':'));
            let tagText = textArrayItem.substring(textArrayItem.indexOf(':') + 1, textArrayItem.indexOf(']'));
            res[tagName] = tagText;
        }
        return res;
    }
    this.getWord = function (time, flag = true, timeDeviation = 50) {
        // 获取解析歌词 time: 毫秒 flag:是否模糊匹配 若为 false 且找不到时返回 '' 
        // timeDeviation:模糊时间 (time - timeDeviation< time < time + timeDeviation),flag 为true 时生效
        if (this.canPlay == 3) {// 解析成功
            if (flag) {// 模糊匹配
                for (let key in this.timeTags) {
                    // 获取大概区间的第一个歌词
                    if ((key >= time - timeDeviation) && (key <= time + timeDeviation)) {
                        this.lastWord = this.timeTags[key];
                        return this.timeTags[key];
                    } else if (key > time + timeDeviation) {// 未找到返回上一次的歌词
                        return this.lastWord;
                    }
                }
            } else {// 精确匹配
                let res = this.timeTags[time];
                return res ? res : '';
            }
        } else {// 其它状态
            return this.canPlayInf[this.canPlay];
        }
    }
    this.getTag = function () {
        // 获取歌曲信息
        return this.flagTags;
    }
    this.getTimeTags = function (text) {
        // 解析歌词标签 text lrc全部文本
        // return {time:word} 例如{12300:'hello world'}
        // [m:s.ms] or [m:s:ms] or [m:s], [1:2.3] or [01:02.03] or [01:02] ...
        let res = {};// 结果
        if (typeof (text) != 'string') { return res; }// 检查参数
        let textArray = text.split('\n');// 歌词拆分
        let timeRule = [60000, 1000, 1];// 对应位数的毫秒数
        let addTime = 0;// 时间补偿
        for (let textArrayKey in textArray) {
            let textArrayItem = textArray[textArrayKey];
            if (textArrayItem.match(/(?:\[\d+\:\d+(?:[.:]\d+)?\])+.*/)) {// 判断是否符合歌词的规则
                let findWord = textArrayItem.replace(/(?:\[\d+\:\d+(?:[.:]\d+)?\])+/g, '').trim();// 歌去除时间保留歌词,并去除两端多余空格
                if (findWord) {// 去除歌词为空的项
                    let findTime = textArrayItem.match(/(?:\[\d+\:\d+(?:[.:]\d+)?\])/g);// 匹配多个时间 例如 [1:2.3][4:2.4]hello world
                    for (let findTimeKey in findTime) {
                        let findTimeItem = findTime[findTimeKey].match(/\d+/g);// 切割每一个时间的m s ms部分
                        let nowTime = addTime;// 初始为偏移时间 解决[offset:-232]操作
                        for (let x = 0; x < findTimeItem.length; x++) {
                            nowTime += parseInt(findTimeItem[x]) * timeRule[x];// 分钟,秒,毫秒转换为转毫秒之后累加
                        }
                        res[nowTime > 0 ? nowTime : 0] = findWord;// 限制下线时间 0
                    }
                }
            } else if (textArrayItem.match(/^\[offset\:\-?[1-9]\d+\]$/)) {// 匹配偏移时间 解决[offset:-232]操作
                // [offset:-232]正值表示整体提前,负值相反
                addTime -= parseInt(textArrayItem.substring(textArrayItem.indexOf(':') + 1, textArrayItem.length));
            }
        }
        return res;
    }
}

4.其它

为了节省内存,可以将Lrc的方法改进Lrc.prototype.functionName=function(){...}

5. 使用

let lrcob = new Lrc('./Counting Stars.lrc');
lrcob.getLrc();// 加载lrc歌词文件
let time = 0;
setInterval(() => {// 模仿audio,video时间进度条
    lrcob.getWord(time,true,50);// 模糊获取歌词 50为模糊值
    // lrcob.getWord(time,false); // 精确获取歌词
    time += 100;
}, 100)

其中的 getWord,getRequestText,getTag方法请自行了解

6.最后

资源奉上Github

  • 17
    点赞
  • 26
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值