Skip to content

歌词跟随歌曲播放效果

作者头像作者: 佚名 更新: 3/23/2025 字数: 0 字 时长: 0 分钟

序言

先说明一下,本篇这个歌词跟随播放效果并不难,我个人给它定位是 初级+ 的水平。大概花了几个小时吧就写完了,时间大部分耗在 corner case 上。

在做一个功能前,我们首先最需要克服的是恐惧心理,不要总感觉 "这该怎么办?" 或者急急忙忙的去网上翻找类似组件然后磕磕盼盼的粗糙完成。这很难得到真正的成长,也不利于养成程序员思维。
---- 致年轻时候的自己

正文

废话不多说,先看效果:

首先对这个功能进行列举拆分,由大化小。主要有:

  • 一个播放器控件,负责播放。当然播放的歌曲、歌词资源也要自己去找一个
  • 一个面板容器,负责展示歌词
  • 歌词面板从顶部到中间有一个半透明到完全显示的过渡效果
  • 播放歌曲的时候,歌词会滚动到对应行且高亮该行

先看播放器,这个是最简单的,直接用浏览器自带的

html
<audio id="audio" controls 
    src="./song/assets/我记得你眼里的依恋.mp3" preload="metadata">
</audio>

INFO

注意:audio 控件本身是不显示,页面上显示的是其 controls

容器面板,我们设置一个定高满宽内容居中就行了。 那歌词是如何显示呢?歌词的原始数据是这样的:

txt
[00:26.67]走在红尘俗世间
[00:30.39]谁的呼唤飘在耳边
[00:34.89]那么熟悉却又遥远
[00:39.57]为什么痴心两处总难相见
[00:46.53]徘徊在起风的午夜
[00:50.19]谁的叹息飘在风间
[00:54.69]那么无奈却又无悔
[00:59.13]多少前世残梦留待今生缘
[01:04.02]就算换了时空变了容颜
[01:10.08]我依然记得你眼里的依恋
[01:14.73]纵然聚散由命也要用心感动天
[01:22.80]就算换了时空变了容颜
[01:28.77]我依然记得你眼里的依恋
[01:33.45]纵然难续前世也要再结今生缘
[02:02.34]走在红尘俗世间
// ... 更多歌词

我们把它转化一下,形成 “秒数:行内容” 格式的对象/Map

javascript
// 歌词解析
function parseLrc(lrc) {
    const lines = lrc.split('\n');
    const result = {};
    const timeRegex = /^\[(\d{2}):(\d{2})\.(\d{2})\]\s*(.+)/;

    lines.forEach(line => {
        line = line.trim();
        const match = line.match(timeRegex);
        if (match) {
            // Convert time to seconds
            const minutes = parseInt(match[1]);
            const seconds = parseInt(match[2]);
            const centiseconds = parseInt(match[3]);
            const timeInSeconds = minutes * 60 + seconds + centiseconds / 100;

            const text = match[4].trim();
            result[timeInSeconds] = text;
        }
    });

    return result;
}
json
{
    "26.67": "走在红尘俗世间",
    "30.39": "谁的呼唤飘在耳边",
    "34.89": "那么熟悉却又遥远",
    "39.57": "为什么痴心两处总难相见",
    "46.53": "徘徊在起风的午夜",
    "50.19": "谁的叹息飘在风间",
    "54.69": "那么无奈却又无悔",
    "59.13": "多少前世残梦留待今生缘",
    "64.02": "就算换了时空变了容颜",
    "70.08": "我依然记得你眼里的依恋",
    "74.73": "纵然聚散由命也要用心感动天",
    "82.8": "就算换了时空变了容颜",
    "88.77": "我依然记得你眼里的依恋",
    "93.45": "纵然难续前世也要再结今生缘",
    "122.34": "走在红尘俗世间",
    "126.03": "谁的呼唤飘在耳边",
    "130.59": "那么熟悉却又遥远",
    "135.18": "为什么痴心两处总难相见",
    "142.11": "徘徊在起风的午夜",
    "145.86": "谁的叹息飘在风间",
    "150.33": "那么无奈却又无悔",
    "154.71": "多少前世残梦留待今生缘",
    "159.63": "就算换了时空变了容颜",
    "165.75": "我依然记得你眼里的依恋",
    "170.37": "纵然聚散由命也要用心感动天",
    "178.47": "就算换了时空变了容颜",
    "184.41": "我依然记得你眼里的依恋",
    "189.03": "纵然难续前世也要再结今生缘",
    "206.91": "就算换了时空变了容颜",
    "212.73": "我依然记得你眼里的依恋",
    "217.98": "就算换了时空你变了容颜",
    "223.74": "我依然记得你眼里的依恋"
}

然后,我们将歌词逐行对应一个 li 元素显示到面板上。

至于半透明的效果,直接用 mask-image 蒙版来完成,当然你也可以用为元素来套上一层渐变透明效果。

css
 mask-image: linear-gradient(to bottom, 
    rgba(0, 0, 0, 0.3) 0%, rgba(0, 0, 0, 1) 50%);
mask-mode: alpha;

最后,最难点是我要将歌曲播放时间点和歌词关联起来。我们可以通过监听 audio 的 timeupdate 事件来达到这一点

index.js
javascript
// 监听歌曲播放
const audio = document.querySelector('#audio');
audio.addEventListener('timeupdate', (event) => {
    const currentTime = event.target.currentTime;
    console.log("🚀 ~ audio.addEventListener ~ currentTime:", currentTime)

    // 找到当前时间对应的歌词下一行
    const currentInx = sortedTimes.findIndex(time => currentTime < time);
    // 移除所有高亮歌词
    Array.from(ul.children).forEach(li => {
        li.classList.remove('active');
    });

    if (currentInx > 0) {
        // 高亮歌词
        const currentLi = ul.children[currentInx - 1];
        if (!currentLi.classList.contains('active')) {
            currentLi.classList.add('active');
        }

        // 滚动歌词
        ul.style.transform = `translateY(-${(currentInx - 1) * 34}px)`;
    }
});
点我查看

timeupdate 事件函数在实际开发建议加上防抖处理

提示

这里我们用 translateY 来做滚动,而不是 scrollTop , 想一想为什么呢?

好了代码大致解释到这里了。你可以进入 CodeSandbox 查看完整代码,快快动手试一试吧!