JavaScript 练手小技巧:音乐播放器的歌词显示

暑假了,还是不能让自己闲着,学点自己感兴趣的知识,写点自己喜欢的代码。

=========================

今天写了一个播放器的雏形,带歌词显示。

没去自定义播放器,主要是写歌词显示效果。效果图如下:

首先当然是要准备一个 mp3 文件。 

一、HTML 结构

很简单:

<audio  id="myPlayer" class="myPlayer"  controls src="music/music_zndsb.mp3"></audio>
<div class="container" id="container">
    <ul class="lrcList" id="lrcList">
            <li>歌词歌词歌词歌词1</li>
            <li class="current">歌词歌词歌词歌词2</li>
            <li>歌词歌词歌词歌词3</li>
            <li>歌词歌词歌词歌词4</li>
            <li>歌词歌词歌词歌词5</li>
    </ul>
</div>
<img class="img" src="images/bg1.jpg" alt="">

播放器就用  audio 标签。

主要是歌词部分。一个div嵌套一个 ul;每句歌词都是一个 li;当前歌词有个高亮的类 current。

最后使用一张自己喜欢的图作为背景图。

二、CSS

*{
    margin: 0;
    padding: 0;
}

ul,li,ol{
    list-style: none;
}

.myPlayer{
    display: block;
    width: 600px;
    margin-top: 50px;
    margin-left: auto;
    margin-right: auto;
    opacity: 0.5;
    transition:all 0.2s;
}
.myPlayer:hover{
    opacity: 1;
}
.img{
    position: absolute;
    z-index: -1;  /* 让图片在页面的内容下方 */
    top: 0;
    width: 100%;
    height: 100%;
    object-fit: cover;    /* 调整图片内容不变形 */
}

.container{
    width: 600px;
    margin-left: auto;
    margin-right: auto;
    margin-top: 30px; 
    height: 180px;
    overflow: hidden;
    position: relative;
}


.lrcList{
    font-size: 16px;
    line-height: 30px;
    color: #869cd3;
    text-align: center;
    transition:all 0.2s;  /* 过渡动画。实现歌词上下移动的动画 */
}
.lrcList li{
    transition:all 0.2s;
    height: 30px;
    opacity: 0.5;
}
.lrcList .current{
    transform: scale(1.4);
    color: #fff;
    opacity: 1;
}

三、歌词

这个案例因为重点在歌词的显示,所以就把歌词写在里一个变量里。

歌词很长,因此用了 ES6 里的模板字符串。

let  lrc = `[00:00.00]在你的身边 - 盛哲
[00:02.25]词:盛哲
[00:04.51]曲:盛哲
[00:06.77]安静地又说分开
[00:09.49]
[00:10.80]没有依赖却是太多依赖
[00:15.70]
[00:18.66]寂寞的广场中央
[00:21.35]
[00:22.57]是谁的对白追赶我的空白
[00:29.46]
[00:30.55]爱就爱了不怕没来过
[00:33.58]恨就恨了我从没想过
[00:36.33]
[00:37.85]是怕独念一个人太深刻
[00:41.40]
[00:42.34]爱就爱了不怕没来过
[00:45.48]恨就恨了我从没想过
[00:48.02]
[00:48.84]到过的地方熟悉曾经的模样
[00:54.20]我以为忘了想念
[00:56.88]
[00:58.08]而面对夕阳希望你回到今天
[01:04.43]
[01:06.01]我记得捧你的脸
[01:09.88]在双手之间安静地看你的眼
[01:16.43]像秋天落叶温柔整个世界
[01:22.65]
[01:23.91]我想在你的身边
[01:27.58]
[01:29.95]忘了这路有多长
[01:32.63]
[01:33.64]想和你去看季节慢慢变换
[01:39.12]
[01:41.65]又来到这座广场
[01:44.39]
[01:45.45]听风随落叶已是最后一片
[01:52.53]
[01:53.53]爱就爱了不怕没来过
[01:56.39]恨就恨了我从没想过
[01:59.15]
[02:00.89]是怕独念一个人太深刻
[02:04.54]
[02:05.30]爱就爱了不怕没来过
[02:08.37]恨就恨了我从没想过
[02:11.47]
[02:12.31]到过的地方熟悉曾经的模样
[02:17.16]我以为忘了想念
[02:20.72]而面对夕阳希望你回到今天
[02:27.38]
[02:29.02]我记得捧你的脸
[02:32.77]在双手之间安静地看你的眼
[02:39.41]像秋天落叶温柔整个世界
[02:45.58]
[02:46.98]我想在你的身边
[02:50.51]
[02:52.86]我想在你的身边
[02:56.43]
[02:58.76]我想在你的身边
[03:03.04]
[03:04.66]就让那往事随风
[03:08.39]让它带走伤带走痛
[03:10.83]带回那日暮的梦
[03:14.93]花开落
[03:17.56]
[03:18.20]云会走
[03:19.62]
[03:21.12]铺满天
[03:22.81]
[03:24.00]而你笑着在我的身边
[03:28.29]我以为忘了想念
[03:31.01]
[03:31.85]而面对夕阳希望你回到今天
[03:38.37]
[03:40.08]我记得捧你的脸
[03:43.71]在双手之间安静地看你的眼
[03:50.45]像秋天落叶温柔整个世界
[03:56.47]
[03:58.07]我想在你的身边
[04:01.82]
[04:03.88]我想在你的身边
[04:07.20]
[04:09.88]我想在你的身边
[04:13.63]在你的身边`;

这个案例目前是把歌词写死了。后期可以做成 lrc 文件,通过 AJAX 异步加载,并解析。

四、关键 JavaScript 解析

首先是解析歌词。

歌词目前是一个很长的字符串,但是每句歌词都独立成行。所以,为了便于独立歌词出来,要把歌词分解为数组。每个元素就是每一句歌词。

考虑到歌词分为时间和文本部分,所以歌词数组里的元素是一个 Object。其属性有 time 和 word 两部分。

// 把歌词字符串处理为 Object 对象
/**
 *   解析歌词字符串,的到歌词对象数组
 *   每个歌词对象:
 *   {
        time:开始时间,
        word:歌词
 *   }
*/
function parseLRC(LRC){
    let lines = LRC.split('\n'); // 把歌词转为数组
    let LRCArr = [];  // 歌词数组
    // 遍历数组
    lines.forEach(item => {
        // item数据: [00:06.77]安静地又说分开
        // 切割字符
        let parts = item.split("]"); //  [00:06.77 , 安静地又说分开
        let timer =  parts[0].slice(1).trim();  // 00:06.77
        let obj = {
            time: parseTime(timer),
            word: parts[1].trim()==""?"":parts[1]  // 安静地又说分开
        }
        // console.info( obj );
        LRCArr.push(obj);    
    });
    return LRCArr;
}

把时间要转为对应的 x 秒,写一个时间处理函数。

/*
    把时间字符串转为时间数字
    eg:
    01:06.77  => 66.77
*/
function parseTime(timer){
   let t = timer.split(":");
   let result = Number(t[0])*60 + Number(t[1]);
   return result ; 
}

找到当前播放进度的歌词,也就是歌词数组的索引。


/*
* 计算出,再当前播放器播放到第几秒的情况
* LRCData 应该高亮显示的歌词下标。
* 高亮歌词是: 比当前时间数【第一次大】的上一句。
* 如果没有任何歌词显示,就为 -1 。
* 返回值:当前歌词对应的索引
*/
function findIndex(){
    // 播放器当前时间
    let index = -1;
    let curTime = doms.audio.currentTime;
    for(let i=0; i<=LRCData.length-1 ; i++){
        if( curTime < LRCData[i].time ){
            index = i - 1;
            return  index;
        }
    }
    // 找遍了,都没有歌词,说明播放完毕里,显示最后一句歌词。
    index = LRCData.length-1
    return index;
}

歌词显示。为了方便歌词的显示,歌词结构用的是 ul 和 li。因此,要遍历歌词数组,生成对应的 li。

因为,createElement 会多次操作DOM,造成 DOM 重绘。为了提高效率,是用 Fragment 同一收集 li 后,集体绘制,减少 DOM 重绘次数。

// 界面部分
/*
  生成歌词 li
*/
function createLrcList(lrc){
    // 避免多次操作 DOM。创建一个 DOM 片段,它不会显示,但是可以集中处理 DOM。
    let frag = document.createDocumentFragment();
    doms.lrcList.innerHTML = "";
    lrc.forEach(item=>{
        let li = document.createElement("li");
        li.innerHTML = item.word ;
        frag.appendChild(li);
    });
    doms.lrcList.appendChild(frag);
}

五、完整 JS 代码 

// 把歌词字符串处理为 Object 对象
/**
 *   解析歌词字符串,的到歌词对象数组
 *   每个歌词对象:
 *   {
        time:开始时间,
        word:歌词
 *   }
*/
function parseLRC(LRC){
    let lines = LRC.split('\n'); // 把歌词转为数组
    let LRCArr = [];  // 歌词数组
    // 遍历数组
    lines.forEach(item => {
        // item数据: [00:06.77]安静地又说分开
        // 切割字符
        let parts = item.split("]"); //  [00:06.77 , 安静地又说分开
        let timer =  parts[0].slice(1).trim();  // 00:06.77
        let obj = {
            time: parseTime(timer),
            word: parts[1].trim()==""?"":parts[1]  // 安静地又说分开
        }
        // console.info( obj );
        LRCArr.push(obj);    
    });
    return LRCArr;
}
/*
    把时间字符串转为时间数字
    eg:
    01:06.77  => 66.77
*/
function parseTime(timer){
   let t = timer.split(":");
   let result = Number(t[0])*60 + Number(t[1]);
   return result ; 
}


/*
* 计算出,再当前播放器播放到第几秒的情况
* LRCData 应该高亮显示的歌词下标。
* 高亮歌词是: 比当前时间数【第一次大】的上一句。
* 如果没有任何歌词显示,就为 -1 。
* 返回值:当前歌词对应的索引
*/
function findIndex(){
    // 播放器当前时间
    let index = -1;
    let curTime = doms.audio.currentTime;
    for(let i=0; i<=LRCData.length-1 ; i++){
        if( curTime < LRCData[i].time ){
            index = i - 1;
            return  index;
        }
    }
    // 找遍了,都没有歌词,说明播放完毕里,显示最后一句歌词。
    index = LRCData.length-1
    return index;
}

// 界面部分
/*
  生成歌词 li
*/
function createLrcList(lrc){
    // 避免多次操作 DOM。创建一个 DOM 片段,它不会显示,但是可以集中处理 DOM。
    let frag = document.createDocumentFragment();
    doms.lrcList.innerHTML = "";
    lrc.forEach(item=>{
        let li = document.createElement("li");
        li.innerHTML = item.word ;
        frag.appendChild(li);
    });
    doms.lrcList.appendChild(frag);
}

/*
  设置歌词 ul 的偏移量
*/
function setOffset(index){
    let dis =-1*( index * liH + liH/2  - conH/2 );  // 位移距离
    doms.lrcList.style.transform = `translateY(${dis}px)`;
    console.info( dis );
}
/*
  设置歌词高亮
*/
function setLight(index){
    let ul = doms.lrcList;
    let lis = ul.children;
    let cur = document.querySelector(".current");
    if( cur ){ // 如果存在
        cur.classList.remove("current");
    }
    lis[index].classList.add("current");
}


let doms = {
    audio:document.querySelector("audio"),
    lrcList:document.querySelector("#lrcList"),
    container:document.querySelector("#container")
}
let LRCData = parseLRC(lrc);
createLrcList(LRCData);  // 创造歌词 li
let conH = doms.container.clientHeight;  // 容器高度
let liH = doms.lrcList.children[0].clientHeight;  // li 高度
// 初始化歌词位置,让第一句歌词在歌词区中间
doms.lrcList.style.transform = `translateY(${-1*( liH/2  - conH/2)}px)`;  

doms.audio.addEventListener("timeupdate",function(){
    let index = findIndex();
    setLight(index);  // 歌词位移
    setOffset(index); // 歌词高亮
});

完毕~

猜你喜欢

转载自blog.csdn.net/weixin_42703239/article/details/131969848