音乐歌词同步滚动高亮实现

本文摘要

        本文介绍了如何在网页中实现音乐播放时歌词同步滚动并高亮显示的效果。核心原理是通过解析LRC格式的歌词文件,将每句歌词与其对应的时间戳关联,并在歌曲播放过程中实时匹配当前播放时间,定位到相应的歌词行。

        文章首先介绍了LRC歌词文件的基本格式,即以[分钟:秒.毫秒]标记时间点后接歌词内容,便于按时间同步显示。随后,从数据准备、HTML结构搭建、CSS样式设计到JavaScript功能实现进行了详细说明:

  • HTML部分:构建包含歌词列表(<ul>)和音频播放器(<audio>)的基本结构;
  • CSS部分:使用Flex布局居中元素,设置背景渐变,并通过transform: scale()和过渡效果实现歌词高亮动画,同时隐藏溢出容器的歌词;
  • JavaScript部分
    1. 编写parseLRC()函数解析LRC字符串,提取时间和歌词内容为对象数组;
    2. 使用createElement()动态生成歌词<li>元素并批量插入DOM以提升性能;
    3. 实现findIndex()算法,根据音频当前播放时间查找对应的歌词索引;
    4. 通过setOffset()函数动态调整歌词列表的垂直偏移,使当前歌词居中显示,并添加active类实现视觉强调。

最终实现了歌词随音乐播放自动滚动、居中定位与高亮的卡拉OK式交互效果。


        在各大音乐网站歌曲播放的过程中,其歌词区域中心会随着歌曲播放的时间而滚动到对应的歌词并将其进行字体进行放大、加粗、改变颜色等方式进行视觉强调。

        要实现这种效果,就需要歌曲播放时间和歌词之间的相互关联。哪个时间该滚动到哪句歌词。就需要LRC这种歌曲格式。

1.LRC格式

        LRC文件是通过编辑器把歌词按歌曲歌词出现的时间编辑到一起,然后在播放歌曲时同步依次将歌词显示出来的,用记事本按照上述格式写好后,将扩展名改为lrc即可做出“文件名.LRC”的歌词文件。当然,要进行高效的lrc歌词制作需要专门的软件。这是百度的解释。

        具体是[分钟:秒.毫秒] 歌词的格式,以下是实例。

[00:44.02]你是我未曾拥有无法捕捉的亲昵
[00:47.60]我却有你的吻你的魂你的心
[00:51.41]载着我飞呀飞呀飞 越过了意义
[00:58.69]你是我朝夕相伴触手可及的虚拟
[01:02.36]陪着我像纸笔像自己像雨滴
[01:06.04]看着我坠啊坠啊坠落到云里

        这种格式由于只记录了每句歌词出现的时间,并不能对每个字进行效果展示。当然也有记录每个字的格式像TRC、KRC等格式。

2.数据准备

1.歌曲MP3文件

2.歌曲对应的LRC文件

3.实现

3.1HTML编写

<body>
    <div class="container">
        <ul>
            <li>Lorem ilisum dolor sit.</li>
            <li>Qui, quasi deserunt. Accusantium?</li>
            <li>Maxime eius accusantium sint!</li>
            <li>Eveniet ab fugiat velit?</li>
            <li>Fugit alias nisi doloribus?</li>
            <li>Molestiae tenetur enim dolorem.</li>
            <li>Officia iste corlioris quaerat!</li>
            <li>Eum reliellendus liossimus harum.</li>
            <li>Nesciunt, sit! Accusantium, ab.</li>
            <li>Nostrum, dolor. Soluta, eaque!</li>
            <li>lierferendis quia sit rem.</li>
            <li>Volulitates harum nemo quos.</li>
            <li>Quasi est liariatur temliora!</li>
            <li>Error assumenda corlioris atque.</li>
            <li>Error assumenda in saliiente!</li>
            <li>liraesentium harum animi ea?</li>
            <li>Quam incidunt quia volulitatem?</li>
            <li>Illum liersliiciatis earum et.</li>
            <li>Quos quidem accusamus adiliisci.</li>
            <li>Ullam aut laborum corruliti.</li>
        </ul>
    </div>
    <audio src="/asset/music.mli3" controls></audio>
     <scrilit src="/js/data.js"></scrilit>
    <scrilit src="/js/index.js"></scrilit>
</body>

        添加audio标签,为scr属性设置歌曲资源地址,controls属性展示控件。ul标签将作为存储歌曲的容器。通过“p*20>lorem4”生成乱数假文,当作歌词进行排版。

效果展示

3.2CSS编写

*{
    margin: 0;
    padding: 0;
}
body{ 
    width: 100%;
    height: 100vh;
    display: flex;
    flex-direction: column;
    justify-content: center;
    align-items: center;
    background: radial-gradient(#3d3c3c, #bebec2)
}
div.container{ 
    width: 100%;
    height: 75%;
    text-align: center;
    overflow: hidden;
}
audio{
    padding: 25px;
    width: 45%;
}
li{
    padding: 10px 0px;
    text-align: center;
    color:white;
    transition: 0.6s;
    list-style: none;
}
li.active{
    transform: scale(1.5);
}

        为背景添加径向渐变。让li和audio标签居中显示,并歌词之间和标签之间保持间隔。隐藏超出div部分的歌词。

效果展示

3.3JS编写

3.3.1解析LRC文件

        字符串按照换行符/n分割成数组,遍历每个元素。针对每个元素,以]为分隔符进一步分割成子数组arr。处理arr[0]部分:移除首字符[,剩余格式为xx:xx.x的字符串。以:为分隔符分割该字符串,将时间部分转换为秒数。处理arr[1]部分:直接作为歌词内容。若遇到空字符串则跳过当前元素。最终将处理后的时间和歌词组合成对象,添加到结果数组中返回。

/**
 * 解析LRC文件字符串,转为对象的形式
 * @param {*} lrc LRC文件字符串
 * @returns 返回对象{time,text}
 */
function parseLRC(lrc){
    let result=[];
    let lrcArr = lrc.split("\n");
    let arr;
    lrcArr.forEach(element => {
        arr = element.split("]");
        if(arr[1]=='')return
        let obj={
            time:+arr[0].substring(1).split(":")[0]*60 + +arr[0].substring(1).split(":")[1],
            text:arr[1]
        }
        result.push(obj);
    });
    return result;
}

3.3.2动态生成歌词元素

        遍历obj创建li元素,添加到frag中,然后一并添加到ul中。使用frag可以减少重排进行优化。

let doms={
    ul:document.querySelector("ul"),
    audio:document.querySelector("audio"),
    container:document.querySelector(".container")
}
/**
 * 创建li元素添加到ul中
 * @param {*} obj 
 */
function createElement(obj){
    let frag=document.createDocumentFragment();
    obj.forEach(element => {
        let li = document.createElement("li");
        li.textContent = element.text;
        frag.appendChild(li);
    });
    doms.ul.appendChild(frag);
}

3.3.3时间轴匹配算法

        比较音频当前播放时间与歌词时间轴,当时间超过当前歌词时间时返回上一个索引。 需要特别处理首尾边界情况,避免返回无效值。

  • 在索引0处拦截判断,避免返回-1,直接返回0。        
  • 在最后索引位置时,直接返回末尾歌词的索引值。
  • 中间正常情况返回匹配到的前一个索引。
/**
 * 通过audio当前时间获得对应歌词在lrcObj中索引
 * @param {*} lrcObj 
 * @returns 
 */
function findIndex(lrcObj){
    let time=doms.audio.currentTime;
    if(time<lrcObj[0].time){
        return 0;
    }else if(time>lrcObj[lrcObj.length-1].time){
        return lrcObj.length-1;
    }
    else{
        for(let i=0;i<lrcObj.length;i++){
            if(time<lrcObj[i].time)return i-1;
        }
    }
}

3.3.4滚动定位与高亮

        通过动态调整ul元素的transform: translateY属性,可以实现歌词垂直居中滚动效果。核心思路是根据当前歌词索引计算偏移量,使目标歌词始终位于容器中央。初始状态(索引为0时)的偏移量计算为容器高度的一半减去单个歌词项高度。随着索引增加,偏移量按歌词项偏移递减,向上移动。clientHeight是不包含margin、border和滚动条的高度。同时给对应的歌词添加active类实现强调效果。为audio添加事件,触发函数。

let containerHeight=doms.container.clientHeight;
let liHeight=doms.ul.children[0].clientHeight;
/**
 * 通过索引数来设置歌词偏移量
 */
function setOffset(){
    let index=findIndex(lrcObj);
    let offset=containerHeight/2-(index+0.5)*liHeight;
    doms.ul.style.transform = `translateY(${offset}px)`;
    let active=document.querySelector(".active");
    if(active)active.classList.remove("active");
    active=doms.ul.children[index];
    active.classList.add("active");
}
doms.audio.addEventListener("timeupdate",setOffset);

4.完整代码

<!DOCTYPE html>
<html lang="en">
<head>
    <title>music</title>
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <style>
        *{
            margin: 0;
            padding: 0;
        }
        body{ 
            width: 100%;
            height: 100vh;
            display: flex;
            flex-direction: column;
            justify-content: center;
            align-items: center;
            background: radial-gradient(#3d3c3c, #bebec2)
        }
        div.container{ 
            width: 100%;
            height: 75%;
            text-align: center;
            overflow: hidden;
        }
        audio{
            padding: 25px;
            width: 45%;
        }
        li{
            padding: 10px 0px;
            text-align: center;
            color:white;
            transition: 0.6s;
            list-style: none;
        }
        li.active{
            transform: scale(1.5);
        }
    </style>
</head>
<body>
    <div class="container">
        <ul>
        </ul>
    </div>
    <audio src="/asset/music.mp3" controls></audio>
    <script>
        let lrc=`lrc歌词`//注意
        
        /**
        * 解析LRC文件字符串,转为对象的形式
        * @param {*} lrc LRC文件字符串
        * @returns 返回对象{time,text}
        */
        function parseLRC(lrc){
            let result=[];
            let lrcArr = lrc.split("\n");
            let arr;
            lrcArr.forEach(element => {
                arr = element.split("]");
                if(arr[1]=='')return
                let obj={
                    time:+arr[0].substring(1).split(":")[0]*60 + +arr[0].substring(1).split(":")[1],
                    text:arr[1]
                }
                result.push(obj);
            });
            return result;
        }
        /**
        * 创建li元素添加到ul中
        * @param {*} obj 
        */
        function createElement(obj){
            let frag=document.createDocumentFragment();
            obj.forEach(element => {
                let li = document.createElement("li");
                li.textContent = element.text;
                frag.appendChild(li);
                // doms.ul.appendChild(li);
            });
            doms.ul.appendChild(frag);
        }
        /**
        * 通过audio当前时间获得对应歌词在lrcObj中索引
        * @param {*} lrcObj 
        * @returns 
        */
        function findIndex(lrcObj){
            let time=doms.audio.currentTime;
            if(time<lrcObj[0].time){
                return 0;
            }else if(time>lrcObj[lrcObj.length-1].time){
                return lrcObj.length-1;
            }
            else{
                for(let i=0;i<lrcObj.length;i++){
                    if(time<lrcObj[i].time)return i-1;
                }
            }
        }
        /**
        * 通过索引数来设置歌词偏移量
        */
        function setOffset(){
            let index=findIndex(lrcObj);
            let offset=containerHeight/2-(index+0.5)*liHeight;
            doms.ul.style.transform = `translateY(${offset}px)`;
            let active=document.querySelector(".active");
            if(active)active.classList.remove("active");
            active=doms.ul.children[index];
            active.classList.add("active");
        }

        let doms={
            ul:document.querySelector("ul"),
            audio:document.querySelector("audio"),
            container:document.querySelector(".container")
        }

        lrcObj=parseLRC(lrc)

        createElement(lrcObj);
        let containerHeight=doms.container.clientHeight;
        let liHeight=doms.ul.children[0].clientHeight;
        setOffset();
        doms.audio.addEventListener("timeupdate",setOffset);
    </script>

</body>
</html>

最终效果

posted @ 2025-09-27 16:17  Oblique  阅读(1)  评论(0)    收藏  举报  来源