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); // 歌词高亮
});

完毕~

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/47398.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

RocketMQ基本概念与入门

文章目录 MQ基本结构依赖案例:productConsumer 核心概念1.nameserver2.broker3.主题队列4.queue队列5. 生产者6.消费者分组和生产者分组7.消费点位 MQ基本结构 message: 消息数据对象product: 程序代码,生成消息,发送消息到队列consumer: 程序代码,监听(绑定)队列,获取消息,执行…

分布式锁:Redis、Zookeeper

1.基于Redis实现分布式锁&#xfeff; Redis分布式锁原理如上图所示&#xff0c;当有多个Set命令发送到Redis时&#xff0c;Redis会串行处理&#xff0c;最终只有一个Set命令执行成功&#xff0c;从而只有一个线程加锁成功 2.SetNx命令加锁 利用Redis的setNx命令在Redis数据库…

数据结构【绪论】

数据结构入门级 第一章绪论 什么是数据结构&#xff1f;什么是数据类型&#xff1f; 程序数据结构算法 一、基本概念&#xff1a; 数据&#xff1a;指所有能被计算机处理的&#xff0c;无论图、文字、符号等。数据元素&#xff1a;数据的基本单位&#xff0c;通常作为整体考…

Unity TMP (TextMeshPro) 创建字体材质

1 TMP 简介 完整名称&#xff1a;Text Mesh Pro &#xff0c;unity新一代主流字体插件 1.1 组件变化 内置的Text组件以及与内置Text组件绑定的Button、DropDown、InputField均被替换为使用TextMeshPro的版本 内置的Text组件以及与内置Text组件绑定的Button、DropDown、Input…

tinymce插件tinymce-powerpaste-plugin——将word中内容(文字图片等)直接粘贴至tinymce编辑器中

TinyMCE是一款易用、且功能强大的所见即所得的富文本编辑器。同类程序有&#xff1a;UEditor、Kindeditor、Simditor、CKEditor、wangEditor、Suneditor、froala等等。 TinyMCE的优势&#xff1a; 开源可商用&#xff0c;基于LGPL2.1 插件丰富&#xff0c;自带插件基本涵盖日常…

【项目设计】基于负载均衡的在线oj平台

目录 一、项目介绍 二、开发环境以及技术 三、概要设计 四、关键算法 五、项目演示 六、代码实现 一、项目介绍 该项目是基于负载均衡的在线oj&#xff0c;模拟平时刷题网站&#xff08;leetcode和牛客&#xff09;写的一个在线判题系统 项目主要分为五个模块&#xff…

OpenAI重磅官宣ChatGPT安卓版本周发布,现已开启下载预约,附详细预约教程

7月22号&#xff0c;OpenAI 突然宣布&#xff0c;安卓版 ChatGPT 将在下周发布&#xff01;换句话说&#xff0c;本周安卓版 ChatGPT正式上线&#xff01; 最早&#xff0c;ChatGPT仅有网页版。 今年5月&#xff0c;iOS版ChatGPT正式发布&#xff0c;当时OpenAI表示Android版将…

Docker—— consul的容器服务更新与发现

Docker—— consul的容器服务更新与发现 一、Consul概述1.什么是服务注册与发现2.什么是consul 二、consul 部署1.consul服务器①. 建立 Consul 服务②. 查看集群信息③. 通过 http api 获取集群信息 2.registrator服务器①. 安装 Gliderlabs/Registrator②. 测试服务发现功能是…

智能小说文本字幕生成器

分享一个免费的&#xff0c;智能小说文本字幕生成器 智能分句。短词。 链接&#xff1a;https://pan.baidu.com/s/15xGlQg01LmbHHuGFZbgaiw?pwd0gjv 提取码&#xff1a;0gjv

分类评估指标

文章目录 1. 混淆矩阵2. Precision(精准率)3. Recall(召回率)4. F1-score5. ROC曲线和AUC指标5.1 ROC 曲线5.2 绘制 ROC 曲线5.3 AUC 值6. API介绍6.1 **分类评估报告api**6.2 **AUC计算API**练习-电信客户流失预测1. 数据集介绍2. 处理流程3. 案例实现4. 小结1. 混淆矩阵 …

Windows上安装Docker Desktop

运行环境 Windows 10Docker Desktop 4.21.1 安装步骤 步骤1&#xff1a; 勾掉"Use WSL 2 instead of Hyper-V(recommended)"&#xff08;原因见小插曲2章节&#xff09; 步骤2&#xff1a; 安装完成 步骤3&#xff1a; 运行Docker Desktop 步骤4&#xff1a; …

【MATLAB】ILOSpsi制导率的代码解析

ILOSpsi制导率的代码解析 这里记录一下关于fossen的MMS工具箱中&#xff0c;关于ILOSpsi制导率的代码解析内容&#xff0c;结合fossen的marine carft hydrodynamics and motion control这本书来参考看 文章目录 ILOSpsi制导率的代码解析前言一、代码全文二、内容解析1.persist…

opencv-27 阈值处理 cv2.threshold()

怎么理解阈值处理? 阈值处理&#xff08;Thresholding&#xff09;是一种常用的图像处理技术&#xff0c;在机器学习和计算机视觉中经常被用于二值化图像或二分类任务。它基于设定一个阈值来将像素值进行分类&#xff0c;将像素值大于或小于阈值的部分分为两个不同的类别&…

Redis持久化机制 RDB、AOF、混合持久化详解!如何选择?| JavaGuide

本文已经收录进 JavaGuide(「Java学习+面试指南」一份涵盖大部分 Java 程序员所需要掌握的核心知识。) Redis 持久化机制属于后端面试超高频的面试知识点,老生常谈了,需要重点花时间掌握。即使不是准备面试,日常开发也是需要经常用到的。 最近抽空对之前写的 Redis 持久化…

【ES】---ES的聚合(aggregations)

目录 一、前言1、聚合分类2、聚合的实现方式二、RestAPI--bucket聚合案例11、按照类型分bucket2、按照(String)时间分bucket三、RestAPI-- metric聚合案例11、metric指标统计四、RestAPI-- pipeline聚合案例1一、前言 聚合是对文档数据的统计、分析、计算。 注意:参与聚合的字…

Java中I/O流是什么?输入/输出流又是什么?

在 Java中所有数据都是使用流读写的。流是一组有序的数据序列&#xff0c;将数据从一个地方带到另一个地方。根据数据流向的不同&#xff0c;可以分为输入&#xff08;Input&#xff09;流和输出&#xff08;Output&#xff09;流两种。 在学习输入和输出流之前&#xff0c;我们…

PDF怎么转成Excel?4个方法非常实用!

如何使用记灵在线工具将PDF转成Excel&#xff1f;在日常工作中&#xff0c;我们经常需要转换PDF文件为Excel文件以方便我们处理数据。虽然PDF格式对于文本和图片的可视化效果效果不错&#xff0c;但是在处理数据时&#xff0c;Excel表格更加便捷。当我们将PDF文件转换成Excel文…

JDBC的的使用

首先导入jar包。 https://downloads.mysql.com/archives/c-j/ package com.test.sql;import java.sql.*;public class StudySql {public static void init() throws SQLException {Statement stmt null;Connection conn null;ResultSet res null;PreparedStatement pstm…

LeetCode Top100 Liked 题单(序号1~17)

01Two Sum - LeetCode 我自己写的代码【193ms】 因为不知道怎么加cmp函数&#xff0c;就只能pair的first设为值了&#xff0c;但其实这也是瞎做&#xff0c;应该也是O(n&#xff09;吧 class Solution { public:vector<int> twoSum(vector<int>& nums, int …

【渗透测试】PNG图片隐藏部分恢复

1、图片原尺寸还原方法一 缺点就是有点慢&#xff0c;毕竟遍历的次数比较多 import binascii import struct import sysfilename sys.argv[1] crcbp open(filename, "rb").read() # 打开图片 crc32frombp int(crcbp[29:33].hex(), 16) # 读取图片中的CRC校验值 …