马黑黑 发表于 2023-2-18 17:35

原生lrc歌词同步(测试)

<style>
#papa {
        margin: auto;
        padding: 20px;
        width: 700px;
        height: 400px;
        box-shadow: 3px 3px 20px #000;
        position: relative;
}
#lrc {
        --motion: cover2;
        --tt: 2s;
        --state: paused;
        --bg: linear-gradient(180deg, hsla(60, 50%, 50%, .45), hsla(80, 70%, 50%, .65));
        position: absolute;
        font: bold 2em sans-serif;
        color: snow;
        white-space: pre;
        -webkit-background-clip: text;
        filter: drop-shadow(1px 1px 2px hsla(0, 0%, 0%, .95));
}
#lrc::before {
        position: absolute;
        content: attr(data-lrc);
        width: 20%;
        height: 100%;
        color: transparent;
        overflow: hidden;
        white-space: pre;
        background: var(--bg);
        filter: inherit;
        -webkit-background-clip: text;
        animation: var(--motion) var(--tt) linear forwards;
        animation-play-state: var(--state);
}
@keyframes cover1 { from { width: 0; } to { width: 100%; } }
@keyframes cover2 { from { width: 0; } to { width: 100%; } }
@keyframes rot { to { transform: rotate(1turn); } }
</style>

<div id="papa">
        <audio id="aud" src="https://music.163.com/song/media/outer/url?id=2021373243.mp3" controls loop autoplay></audio>
        <div id="lrc" data-lrc="HCPlayer">HCPlayer</div>
</div>

<script>

/*原始lrc歌词*/
let lrcStr = `Kirsty刘瑾睿 - 若把你\n总有一些话来不及说了\n总有一个人是心口的朱砂\n想起那些话那些傻眼泪落下\n只留一句你现在好吗\n如果爱忘了泪不想落下\n那些幸福啊让她替我到达\n如果爱懂了承诺的代价\n不能给我的请完整给她\n总有些牵挂旧的像伤疤\n越是不碰它越隐隐的痛在那\n想你的脸颊你的发我不害怕\n就让时间给我们回答\n如果爱忘了泪不想落下\n那些幸福啊让她替我到达\n如果爱懂了承诺的代价\n不能给我的请完整给她\n我说我忘了不痛了\n那是因为太爱太懂了\n笑了原谅了为你也值得\n用你的快乐告诉我\n现在放开双手是对的\n别管我多舍不得\n如果爱忘了就放他走吧\n那些幸福啊让她替我到达\n如果爱懂了承诺的代价\n不能给我的请完整给她`;

/*变量 :mKey - 当前歌词索引;mFlag :调用关键帧动画索引*/
let mKey = 0, mFlag = true;

/*函数 :获取每句歌词用时,歌词用时若超过平均值则取平均值,最后一句歌词则取平均值*/
let lrcTime = (ar) => {
        let tmpAr = [];
        for(j = 0; j <ar.length - 1; j ++) {
                if(j !== ar.length - 1) tmpAr = parseFloat((ar - ar).toFixed(1));
        }
        let aver = parseInt(tmpAr.reduce((a,b) => a + b) / (tmpAr.length - 1));
        tmpAr.push(aver);
        tmpAr.forEach((item,key) => {
                ar = item > aver ? aver : item;
        });
        return ar;
};

/*函数 :从原始lrc歌词获取信息并存入 n*2 数组*/
let getLrcAr = (str) => {
        let lines = [], lrcAr = [];
        let reg = /\[(\d{2,}:\d{2,}.\d{2,})\](.*)/g;
        if(!str.match(reg)) return;
        lines = str.replace(reg,'$1-{}-$2').split('\n');
        for(k = 0; k < lines.length; k ++) {
                lrcAr = [];
                for(j = 0; j < 3; j ++) {
                        let tmpAr = lines.split('-{}-');
                        lrcAr = j === 0 ? toSecs(tmpAr) : tmpAr;
                }
        }
        return lrcTime(lrcAr); /* 数组变为 n*3 */
};

/*函数 :原始lrc时间转为秒数*/
let toSecs = (lrcTime) => {
        let reg = /\d{2,}/g;
        let ar = lrcTime.match(reg);
        return parseInt(ar)*60 + parseInt(ar) + parseInt(ar)/1000;
};

/*函数 :模拟显示同步歌词*/
let showLrc = (time) => {
        let name = mFlag ? 'cover1' : 'cover2';
        lrc.innerHTML = lrcAr;
        lrc.dataset.lrc = lrcAr;
        lrc.style.setProperty('--motion', name);
        lrc.style.setProperty('--tt', time + 's');
        lrc.style.setProperty('--state', 'running');
        mKey += 1;
        mFlag = !mFlag;
};

/*函数 :处理当前歌词索引 mKey*/
let calcKey = () => {
        for (j = 0; j < lrcAr.length; j++) {
                if (aud.currentTime <= lrcAr) {
                        mKey = j - 1;
                        break;
                }
        }
        if (mKey < 0) mKey = 0;
        if (mKey > lrcAr.length - 1) mKey = lrcAr.length - 1;
        let time = lrcAr - (aud.currentTime - lrcAr);
        showLrc(time);
};

/*格式化时间信息*/
let toMin = (val) => {
        if (!val) return '00:00';
        val = Math.floor(val);
        let min = parseInt(val / 60),
        sec = parseFloat(val % 60);
        if (min < 10) min = '0' + min;
        if (sec < 10) sec = '0' + sec;
        return min + ':' + sec;
}

/*函数 :歌词同步状态切换*/
let mState = () => lrc.style.setProperty('--state', aud.paused ? 'paused' : 'running');

/*监听播放进度*/
aud.addEventListener('timeupdate', () => {
        for (j = 0; j < lrcAr.length; j++) {
                if (aud.currentTime >= lrcAr) {
                        cKey = j;
                        if (mKey === j) showLrc(lrcAr);
                        else continue;
                }
        }
});

aud.addEventListener('pause', () => mState());/*监听暂停事件*/
aud.addEventListener('play', () => mState());/*监听播放事件*/
aud.addEventListener('seeked', () => calcKey());/*监听查询事件*/

let lrcAr = getLrcAr(lrcStr); /*获得歌词数组*/

</script>

马黑黑 发表于 2023-2-18 17:36

原始代码
<style>
#papa {
        margin: auto;
        padding: 20px;
        width: 700px;
        height: 400px;
        box-shadow: 3px 3px 20px #000;
        position: relative;
}
#lrc {
        --motion: cover2;
        --tt: 2s;
        --state: paused;
        --bg: linear-gradient(180deg, hsla(60, 50%, 50%, .45), hsla(80, 70%, 50%, .65));
        position: absolute;
        font: bold 2em sans-serif;
        color: snow;
        white-space: pre;
        -webkit-background-clip: text;
        filter: drop-shadow(1px 1px 2px hsla(0, 0%, 0%, .95));
}
#lrc::before {
        position: absolute;
        content: attr(data-lrc);
        width: 20%;
        height: 100%;
        color: transparent;
        overflow: hidden;
        white-space: pre;
        background: var(--bg);
        filter: inherit;
        -webkit-background-clip: text;
        animation: var(--motion) var(--tt) linear forwards;
        animation-play-state: var(--state);
}
@keyframes cover1 { from { width: 0; } to { width: 100%; } }
@keyframes cover2 { from { width: 0; } to { width: 100%; } }
@keyframes rot { to { transform: rotate(1turn); } }
</style>

<div id="papa">
        <audio id="aud" src="https://music.163.com/song/media/outer/url?id=2021373243.mp3" controls loop autoplay></audio>
        <div id="lrc" data-lrc="HCPlayer">HCPlayer</div>
</div>

<script>

/*原始lrc歌词*/
let lrcStr = `Kirsty刘瑾睿 - 若把你\n总有一些话来不及说了\n总有一个人是心口的朱砂\n想起那些话那些傻眼泪落下\n只留一句你现在好吗\n如果爱忘了泪不想落下\n那些幸福啊让她替我到达\n如果爱懂了承诺的代价\n不能给我的请完整给她\n总有些牵挂旧的像伤疤\n越是不碰它越隐隐的痛在那\n想你的脸颊你的发我不害怕\n就让时间给我们回答\n如果爱忘了泪不想落下\n那些幸福啊让她替我到达\n如果爱懂了承诺的代价\n不能给我的请完整给她\n我说我忘了不痛了\n那是因为太爱太懂了\n笑了原谅了为你也值得\n用你的快乐告诉我\n现在放开双手是对的\n别管我多舍不得\n如果爱忘了就放他走吧\n那些幸福啊让她替我到达\n如果爱懂了承诺的代价\n不能给我的请完整给她`;

/*变量 :mKey - 当前歌词索引;mFlag :调用关键帧动画索引*/
let mKey = 0, mFlag = true;

/*函数 :获取每句歌词用时,歌词用时若超过平均值则取平均值,最后一句歌词则取平均值*/
let lrcTime = (ar) => {
        let tmpAr = [];
        for(j = 0; j <ar.length - 1; j ++) {
                if(j !== ar.length - 1) tmpAr = parseFloat((ar - ar).toFixed(1));
        }
        let aver = parseInt(tmpAr.reduce((a,b) => a + b) / (tmpAr.length - 1));
        tmpAr.push(aver);
        tmpAr.forEach((item,key) => {
                ar = item > aver ? aver : item;
        });
        return ar;
};

/*函数 :从原始lrc歌词获取信息并存入 n*2 数组*/
let getLrcAr = (str) => {
        let lines = [], lrcAr = [];
        let reg = /\[(\d{2,}:\d{2,}.\d{2,})\](.*)/g;
        if(!str.match(reg)) return;
        lines = str.replace(reg,'$1-{}-$2').split('\n');
        for(k = 0; k < lines.length; k ++) {
                lrcAr = [];
                for(j = 0; j < 3; j ++) {
                        let tmpAr = lines.split('-{}-');
                        lrcAr = j === 0 ? toSecs(tmpAr) : tmpAr;
                }
        }
        return lrcTime(lrcAr); /* 数组变为 n*3 */
};

/*函数 :原始lrc时间转为秒数*/
let toSecs = (lrcTime) => {
        let reg = /\d{2,}/g;
        let ar = lrcTime.match(reg);
        return parseInt(ar)*60 + parseInt(ar) + parseInt(ar)/1000;
};

/*函数 :模拟显示同步歌词*/
let showLrc = (time) => {
        let name = mFlag ? 'cover1' : 'cover2';
        lrc.innerHTML = lrcAr;
        lrc.dataset.lrc = lrcAr;
        lrc.style.setProperty('--motion', name);
        lrc.style.setProperty('--tt', time + 's');
        lrc.style.setProperty('--state', 'running');
        mKey += 1;
        mFlag = !mFlag;
};

/*函数 :处理当前歌词索引 mKey*/
let calcKey = () => {
        for (j = 0; j < lrcAr.length; j++) {
                if (aud.currentTime <= lrcAr) {
                        mKey = j - 1;
                        break;
                }
        }
        if (mKey < 0) mKey = 0;
        if (mKey > lrcAr.length - 1) mKey = lrcAr.length - 1;
        let time = lrcAr - (aud.currentTime - lrcAr);
        showLrc(time);
};

/*格式化时间信息*/
let toMin = (val) => {
        if (!val) return '00:00';
        val = Math.floor(val);
        let min = parseInt(val / 60),
        sec = parseFloat(val % 60);
        if (min < 10) min = '0' + min;
        if (sec < 10) sec = '0' + sec;
        return min + ':' + sec;
}

/*函数 :歌词同步状态切换*/
let mState = () => lrc.style.setProperty('--state', aud.paused ? 'paused' : 'running');

/*监听播放进度*/
aud.addEventListener('timeupdate', () => {
        for (j = 0; j < lrcAr.length; j++) {
                if (aud.currentTime >= lrcAr) {
                        cKey = j;
                        if (mKey === j) showLrc(lrcAr);
                        else continue;
                }
        }
});

aud.addEventListener('pause', () => mState());/*监听暂停事件*/
aud.addEventListener('play', () => mState());/*监听播放事件*/
aud.addEventListener('seeked', () => calcKey());/*监听查询事件*/

let lrcAr = getLrcAr(lrcStr); /*获得歌词数组*/

</script>


马黑黑 发表于 2023-2-18 17:47

本帖最后由 马黑黑 于 2023-2-18 20:04 编辑

lrc歌词使用 lrcStr 变量装载,支持多行的书写格式:

let lrcStr = `歌词一
歌词二
歌词N`;

上述变量的书写格式,要求每句歌词信息写一行,等号之后有反引号 ` ,歌词结束时冒号之前也有反引号 ` ,亦即,歌词是用反引号包裹起来的,另外注意,每句歌词最好是顶格书写。还必须特别注意的是,由于没有做特别处理,头反引号之后必须马上跟歌词信息、尾反引号紧跟在歌词之后。
另一种写法,单行写法:

let lrcStr = `歌词一\n歌词二\n歌词N`;


单行写法,也最好使用反引号将歌词包裹起来,当然使用小角双引号或单引号也可以。不论何种写法,若歌词中存在单引号或双引号,请将其转义,方式是在小角单或双引号之前加符号 \ ,例如 : I‘m a man 要写成 I\'m a man

马黑黑 发表于 2023-2-18 17:58

本帖最后由 马黑黑 于 2023-2-18 18:02 编辑

脚本程序测试中,不一定理想。这里简单说说实现原理:

每句歌词演唱用时这样获取:

首先获取 歌词总数减一 的所有歌词的每一句的用时,方法是用下一句的开始时间减去当前句的开始时间,然后计算其平均值,最后一句歌词取平均值。

然后,检测每一句歌词业已获得的用时,如果超出平均值则取平均值。

这个实现机制不一定靠谱,可能需要使用者观察一下原始歌词,必要时手动调整一下。比方讲,假如几乎每一句的用时是5秒,但由于歌词分上下两部分或更多,这些部分之间的间隔长达20秒,则可以在两部分之间加入一句空歌词,令前一部分最末一句的用时取5秒,这样可能可以解决逐字模拟不准确的问题。

马黑黑 发表于 2023-2-18 18:04

格式化时间信息的函数 toMin 是预留函数,本帖示例未用上

马黑黑 发表于 2023-2-18 18:05

本帖示例的原始lrc歌词来源于网络,除去掉头尾,其余内容几乎未做改动

红影 发表于 2023-2-18 18:19

用搜来的歌词也能做歌词逐字同步了,这个好,小辣椒肯定会觉得很省力{:4_173:}

马黑黑 发表于 2023-2-18 19:34

红影 发表于 2023-2-18 18:19
用搜来的歌词也能做歌词逐字同步了,这个好,小辣椒肯定会觉得很省力
这个呢,有一个比较难处理的问题,就是每一句歌词的用时不好确定

红影 发表于 2023-2-18 21:12

马黑黑 发表于 2023-2-18 19:34
这个呢,有一个比较难处理的问题,就是每一句歌词的用时不好确定

是的,只有开始时间,没有结束时间。对于间隔长的过门,可以用加入一句空歌词来调整,当中就只能随它去了。

醉美水芙蓉 发表于 2023-2-18 21:36

马黑黑 发表于 2023-2-18 23:11

醉美水芙蓉 发表于 2023-2-18 21:36
这个很实用!很多人喜欢简单化!

主要是不用制作歌词,不过也有缺点,比如不好确定每一句歌词演唱用时

马黑黑 发表于 2023-2-18 23:12

红影 发表于 2023-2-18 21:12
是的,只有开始时间,没有结束时间。对于间隔长的过门,可以用加入一句空歌词来调整,当中就只能随它去了 ...

据我观察,有不少歌词有空歌词信息,它其实就是前一句歌词演唱结束时间

红影 发表于 2023-2-19 18:49

马黑黑 发表于 2023-2-18 23:12
据我观察,有不少歌词有空歌词信息,它其实就是前一句歌词演唱结束时间

遇上这样原本就带着空歌词信息的,就更方便了。

马黑黑 发表于 2023-2-19 19:15

红影 发表于 2023-2-19 18:49
遇上这样原本就带着空歌词信息的,就更方便了。

对。我现在纠结的是平均值的使用是否合理。@小辣椒 的新帖有两句明显偏快,这是平均值机制使然;不过对于大多数的歌曲,它是合适的。

红影 发表于 2023-2-19 20:41

马黑黑 发表于 2023-2-19 19:15
对。我现在纠结的是平均值的使用是否合理。@小辣椒 的新帖有两句明显偏快,这是平均值机制使然;不过对于 ...

对于个别调整可以手动的啊,尤其熟悉的歌曲。可以把平均值那句修改一下的。

马黑黑 发表于 2023-2-19 20:44

红影 发表于 2023-2-19 20:41
对于个别调整可以手动的啊,尤其熟悉的歌曲。可以把平均值那句修改一下的。

是的

小辣椒 发表于 2023-2-20 20:52

马黑黑 发表于 2023-2-19 19:15
对。我现在纠结的是平均值的使用是否合理。@小辣椒 的新帖有两句明显偏快,这是平均值机制使然;不过对于 ...

黑黑,我也是自己对比了几个以前的歌词同步,我感觉有几句还是要自己修改一下

马黑黑 发表于 2023-2-20 22:13

小辣椒 发表于 2023-2-20 20:52
黑黑,我也是自己对比了几个以前的歌词同步,我感觉有几句还是要自己修改一下

抒情类的歌曲尤甚
页: [1]
查看完整版本: 原生lrc歌词同步(测试)