主要是对于歌词部分的描述
gitee项目仓库地址
https://gitee.com/manster1231/master-cloud-music
(点个star哦!)
1.总体思路
- 先在加载页面时异步获取歌词,根据 musicId 我们可以获取到该歌曲的歌词
- 对歌词进行切分并以对象的形式放入数组中,将每个时间段获得的歌词存起来方便页面渲染
- 判定该显示那句歌词。将歌词数组进行遍历,如果当前歌曲播放时间等于歌词数组中歌词的时间,就将当前歌词换为这一句;这样当改到下一句时就会等到上一句完全唱完再进行切换
直接看效果
2.详细分析
先在加载页面时异步获取歌词,根据 musicId 我们可以获取到该歌曲的歌词
//获取歌词
async getLyric(musicId){
let lyricData = await request("/lyric", {id: musicId});
let lyric = this.formatLyric(lyricData.lrc.lyric);
},
我可以得到的歌词样式
http://localhost:3000/lyric?id=1815684465
{
"lrc": {
"version": 7,
"lyric": "[00:00.000] 作词 : TetraCalyx\n[00:01.000] 作曲 : 蔡近翰Zoe(HOYO-MiX)\n[00:02.000] 编曲 : 宫奇Gon(HOYO-MiX)/杨启翔Frex(HOYO-MiX)\n[00:03.08]Life blooms like a flower\n[00:07.00]far away or by the road\n[00:10.15]waiting for the one\n[00:13.22]to find the way back home\n[00:17.15]Rain falls a thousand times\n[00:21.02]No footprints of come-and-go\n[00:24.14]You who once went by\n[00:28.05]where will you belong\n[00:30.10]I feel your sigh and breath\n[00:33.24]In the last blow of wind\n[00:38.08]Not yet for the story on the last page\n[00:42.18]It's not the end\n[00:45.17]Life blooms like a flower\n[00:49.07]far away or by the road\n[00:52.23]waiting for the one\n[00:56.04]to find the way back home\n[00:59.22]Time flows across the world\n[01:03.09]There is always a longer way to go\n[01:07.21]Till I reach your arms\n[01:10.07]a Madder there for you\n[01:13.24]Up against the stream\n[01:17.15]waterways will join as one\n[01:21.00]Tracing to the source\n[01:24.17]No more strayed or lost\n[01:26.22]You will see petals fly\n[01:30.10]when lament becomes carol\n[01:34.19]Could you please hear my voice\n[01:37.11]that hungers for a duo\n[01:43.69]Life blooms like a flower\n[01:45.18]far away or by the road\n[01:49.08]waiting for the one\n[01:52.16]to find the way back home\n[01:56.09]Time flows across the world\n[01:59.21]There is always a longer way to go\n[02:04.08]Till I reach your arms\n[02:06.19]a Madder there for you\n[02:37.00]Life blooms like a flower\n[02:40.11]far away or by the road\n[02:44.01]waiting for the one\n[02:47.08]to find the way back home\n[02:51.00]Time flows across the world\n[02:54.15]There is always a longer way to go\n[02:59.01]Till I reach your arms\n[03:01.11]a Madder there for you\n[03:03.720] 人声录音 Recording:徐威Aaron Xu\n[03:06.429] 混音/母带 Mixing&Mastering Engineer:宫奇Gon(HOYO-MiX)\n[03:09.138] 制作人 Producer:蔡近翰Zoe(HOYO-MiX)\n[03:11.847] 特别鸣谢 Special Thanks:周深工作室\n[03:14.556] 出品 Produced by:HOYO-MiX\n"
}
}
但是歌词只是文本,我们需要将其进行切割,并以对象的形式放入数组中,将每个时间段获得的歌词存起来方便页面渲染
-
首先我们以换行符进行切分
\n
,切分后数组元素为
[00:00.000] 作词 : TetraCalyx
-
然后我们再以
]
进行切分,我们可以得到
[00:00.000
和
作词 : TetraCalyx
,对这个数组使用
pop()
方法可以得到
作词 : TetraCalyx
这样的歌词了 -
然后我们就需要获取歌词对应出现的时间了
-
通过
element.substr(1, element.length - 1)
我们可以得到
00:00.000
-
然后我们再使用
split(":")
就可以得到分钟和秒的时间(即
00
和
00.000
) -
然后我们在使其转换为以秒作单位的时间,
parseInt(time_arr[0]) * 60 + Math.ceil(time_arr[1]);
,为了方便计算我们就不计秒的小数点以后的时间了,直接使用整秒 - 最后我们将歌词与秒当做一个对象,传到我们的歌词对象数组中
-
通过
//传入初始歌词文本text
formatLyric(text) {
let result = [];
let arr = text.split("\n"); //原歌词文本已经换好行了方便很多,我们直接通过换行符“\n”进行切割
let row = arr.length; //获取歌词行数
for (let i = 0; i < row; i++) {
let temp_row = arr[i]; //现在每一行格式大概就是这样"[00:04.302][02:10.00]hello world";
let temp_arr = temp_row.split("]");//我们可以通过“]”对时间和文本进行分离
let text = temp_arr.pop(); //把歌词文本从数组中剔除出来,获取到歌词文本了!
//再对剩下的歌词时间进行处理
temp_arr.forEach(element => {
let obj = {};
let time_arr = element.substr(1, element.length - 1).split(":");//先把多余的“[”去掉,再分离出分、秒
let s = parseInt(time_arr[0]) * 60 + Math.ceil(time_arr[1]); //把时间转换成与currentTime相同的类型,方便待会实现滚动效果
obj.time = s;
obj.text = text;
result.push(obj); //每一行歌词对象存到组件的lyric歌词属性里
});
}
result.sort(this.sortRule) //由于不同时间的相同歌词我们给排到一起了,所以这里要以时间顺序重新排列一下
this.setData({
lyric: result
})
},
其中我们需要按照歌词出现的时间来进行排序
sortRule(a, b) { //设置一下排序规则
return a.time - b.time;
},
现在我们的数组中的数据就变成了一个一个的对象
lyric[
{
text: " 作词 : TetraCalyx",
time: 0
},
{
text: " 作曲 : 蔡近翰Zoe(HOYO-MiX)",
time: 1
},
{
text: " 编曲 : 宫奇Gon(HOYO-MiX)/杨启翔Frex(HOYO-MiX)",
time: 2
},
{
text: "Life blooms like a flower",
time: 4
},
{
text: "far away or by the road",
time: 7
},
{
text: "waiting for the one",
time: 11
},{
text: "to find the way back home",
time: 14
},
...
]
- 判定该显示哪句歌词。将歌词数组进行遍历,如果当前歌曲播放时间等于歌词数组中歌词的时间,就将当前歌词换为这一句;这样当该到下一句时就会等到上一句完全唱完再进行切换
//控制歌词播放
getCurrentLyric(){
let j;
for(j=0; j<this.data.lyric.length-1; j++){
if(this.data.lyricTime == this.data.lyric[j].time){
this.setData({
currentLyric : this.data.lyric[j].text
})
}
}
},
songDetail.wxml
<view class="scrollLrc">
<text>{{currentLyric}}</text>
</view>
songDetail.wxss
/* 歌词显示 */
.scrollLrc {
position: absolute;
bottom: 280rpx;
width: 640rpx;
height: 120rpx;
line-height: 120rpx;
text-align: center;
}
songDetail.js
首先增加了
lyric: []
用来存放所有的歌词对象(以
{time:0, text:'歌词'}
的形式)
然后增加
lyricTime
来对歌曲进行与歌词一样样式的时间来方便进行判断,单位为秒
然后每次对
currentLyric
进行操作,放边 wxml 渲染歌词
data: {
isPlay: false,//标识播放状态
song: {},//歌曲详情对象
musicId: '',//歌曲Id
currentTime: '00:00',//当前时长
durationTime:'00:00',//总时长
currentWidth: 0,//实时进度条宽度
lyric: [],//歌词
lyricTime: 0,//歌词对应的时间
currentLyric: "",//当前歌词对象
},
onLoad: function (options) {
this.getLyric(musicId);
//音乐播放自然结束
this.backgroundAudioManager.onEnded(()=>{
//切歌
PubSub.publish('switchMusic','next');
this.setData({
currentWidth: 0,
currentTime: '00:00',
lyric: 0,
lyricTime: 0,
})
})
//监听音乐实时播放的进度
this.backgroundAudioManager.onTimeUpdate(() => {
let lyricTime = Math.ceil(this.backgroundAudioManager.currentTime);//获取歌词对应时间
let currentTime = moment(this.backgroundAudioManager.currentTime * 1000).format('mm:ss');
let currentWidth = (this.backgroundAudioManager.currentTime/this.backgroundAudioManager.duration) * 450;
this.setData({
lyricTime,
currentTime,
currentWidth
})
//跟随歌曲播放进度来进行歌词的调控
this.getCurrentLyric();
})
},
//获取歌词
async getLyric(musicId){
let lyricData = await request("/lyric", {id: musicId});
let lyric = this.formatLyric(lyricData.lrc.lyric);//存储整理好的歌词对象
},
//传入初始歌词文本text对歌词进行处理
formatLyric(text) {
let result = [];
let arr = text.split("\n"); //原歌词文本已经换好行了方便很多,我们直接通过换行符“\n”进行切割
let row = arr.length; //获取歌词行数
for (let i = 0; i < row; i++) {
let temp_row = arr[i]; //现在每一行格式大概就是这样"[00:04.302][02:10.00]hello world";
let temp_arr = temp_row.split("]");//我们可以通过“]”对时间和文本进行分离
let text = temp_arr.pop(); //把歌词文本从数组中剔除出来,获取到歌词文本了!
//再对剩下的歌词时间进行处理
temp_arr.forEach(element => {
let obj = {};
let time_arr = element.substr(1, element.length - 1).split(":");//先把多余的“[”去掉,再分离出分、秒
let s = parseInt(time_arr[0]) * 60 + Math.ceil(time_arr[1]); //把时间转换成与currentTime相同的类型,方便待会实现滚动效果
obj.time = s;
obj.text = text;
result.push(obj); //每一行歌词对象存到组件的lyric歌词属性里
});
}
result.sort(this.sortRule) //由于不同时间的相同歌词我们给排到一起了,所以这里要以时间顺序重新排列一下
this.setData({
lyric: result
})
},
sortRule(a, b) { //设置一下排序规则
return a.time - b.time;
},
//控制歌词播放
getCurrentLyric(){
let j;
for(j=0; j<this.data.lyric.length-1; j++){
if(this.data.lyricTime == this.data.lyric[j].time){
this.setData({
currentLyric : this.data.lyric[j].text
})
}
}
},
3.所有代码
songDetail.wxml
<!--pages/songDetail/songDetail.wxml-->
<view class="songDetailContainer">
<view class="musicAuthor">{{song.ar[0].name}}</view>
<view class="circle"></view>
<!-- 摇杆 -->
<image class="needle {{isPlay && 'needleRotate'}}" src="/static/images/song/needle.png"></image>
<!-- 磁盘 -->
<view class="discContainer {{isPlay && 'discAnimation'}}">
<image class="disc" src="/static/images/song/disc.png"></image>
<!-- 歌曲封面图 -->
<image class="musicImg" src="{{song.al.picUrl}}"></image>
</view>
<!-- 歌词 -->
<view class="scrollLrc">
<text>{{currentLyric}}</text>
</view>
<!-- 进度条控制 -->
<view class="progressControl">
<text>{{currentTime}}</text>
<!-- 总进度条 -->
<view class="barControl">
<!-- 实时进度条 -->
<view class="audio-currentTime-Bar" style="width: {{currentWidth + 'rpx'}}">
<!-- 小圆球 -->
<view class="audio-circle"></view>
</view>
</view>
<text>{{durationTime}}</text>
</view>
<!-- 歌曲播放控制 -->
<view class="musicControl">
<text class="iconfont icon-random"></text>
<text class="iconfont icon-diyigeshipin" id="pre" bindtap="handleSwitch"></text>
<text class="iconfont {{isPlay ? 'icon-zanting' : 'icon-kaishi'}} big" bindtap="handleMusicPlay"></text>
<text class="iconfont icon-zuihouyigeshipin" id="next" bindtap="handleSwitch"></text>
<text class="iconfont icon-liebiao"></text>
</view>
</view>
songDetail.wxss
/* pages/songDetail/songDetail.wxss */
.songDetailContainer {
height: 100%;
background: rgba(0,0,0,0.5);
display: flex;
flex-flow: column;
align-items: center;
}
/* 底座 */
.circle {
position: relative;
z-index: 100;
width: 60rpx;
height: 60rpx;
border-radius: 50%;
background: #fff;
margin: 10rpx 0;
}
/* 摇杆 */
.needle {
position: relative;
z-index: 99;
top: -40rpx;
left: 56rpx;
width: 192rpx;
height: 274rpx;
transform-origin: 40rpx 0;
transform: rotate(-20deg);
transition: transform 1s;
}
/* 摇杆落下 */
.needleRotate {
transform: rotate(0deg);
}
.discContainer {
position: relative;
top: -170rpx;
width: 598rpx;
height: 598rpx;
}
.discAnimation {
animation: disc 20s linear infinite;
animation-delay: 1s;
}
/*设置动画帧 1.from to(只有起始帧和结束帧) 2.百分比(不止两帧)*/
@keyframes disc{
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
/* 磁盘 */
.disc {
width: 100%;
height: 100%;
}
/* 歌曲封面 */
.musicImg {
position: absolute;
top: 0;
left: 0;
bottom: 0;
right: 0;
margin: auto;
width: 370rpx;
height: 370rpx;
border-radius: 50%;
}
/* 歌词显示 */
.scrollLrc {
position: absolute;
bottom: 280rpx;
width: 640rpx;
height: 120rpx;
line-height: 120rpx;
text-align: center;
}
/* 底部控制器 */
.musicControl {
position: absolute;
bottom: 40rpx;
left: 0;
border-top: 1rpx solid #fff;
width: 100%;
display: flex;
}
.musicControl text {
width: 20%;
height: 120rpx;
line-height: 120rpx;
text-align: center;
color: #fff;
font-size: 50rpx;
}
.musicControl text.big {
font-size: 80rpx;
}
/* 进度条控制 */
.progressControl {
position: absolute;
bottom: 200rpx;
width: 640rpx;
height: 80rpx;
line-height: 80rpx;
display: flex;
}
.barControl {
position: relative;
width: 450rpx;
height: 4rpx;
background: rgba(0,0,0,0.4);
margin: auto;
}
.audio-currentTime-Bar {
position: absolute;
top: 0;
left: 0;
z-index: 1;
height: 4rpx;
background: red;
}
/* 小圆球 */
.audio-circle {
position: absolute;
right: -12rpx;
top: -4rpx;
width: 12rpx;
height: 12rpx;
border-radius: 50%;
background: #fff;
}
songDetail.js
// pages/songDetail/songDetail.js
import PubSub from 'pubsub-js';
import moment from 'moment';
import request from '../../utils/request';
//获取全局实例
const appInstance = getApp();
Page({
/**
* 页面的初始数据
*/
data: {
isPlay: false,//标识播放状态
song: {},//歌曲详情对象
musicId: '',//歌曲Id
currentTime: '00:00',//当前时长
durationTime:'00:00',//总时长
currentWidth: 0,//实时进度条宽度
lyric: [],//歌词
lyricTime: 0,//歌词对应的时间
currentLyric: "",//当前歌词对象
},
/**
* 生命周期函数--监听页面加载
*/
onLoad: function (options) {
//options路由跳转参数
let musicId = options.song;
this.setData({
musicId: musicId
})
this.getMusicInfo(musicId);
this.getLyric(musicId);
//判断当前页面音乐是否在播放
if(appInstance.globalData.isMusicPlay && appInstance.globalData.musicId === musicId){
//修改当前页面音乐播放状态
this.setData({
isPlay: true
})
}
//创建控制音乐播放实例对象
this.backgroundAudioManager = wx.getBackgroundAudioManager();
//监视音乐播放与暂停
this.backgroundAudioManager.onPlay(()=>{
//修改音乐播放状态
this.changePlayState(true);
appInstance.globalData.musicId = musicId;
});
this.backgroundAudioManager.onPause(()=>{
this.changePlayState(false);
});
this.backgroundAudioManager.onStop(()=>{
this.changePlayState(false);
});
//音乐播放自然结束
this.backgroundAudioManager.onEnded(()=>{
//切歌
PubSub.publish('switchMusic','next');
//重置所有数据
this.setData({
currentWidth: 0,
currentTime: '00:00',
lyric: [],
lyricTime: 0,
isPlay: false,
currentLyric: ""
})
//获取歌曲
this.getMusicInfo(musicId);
//获得歌词
this.getLyric(musicId);
//自动播放当前音乐
this.musicControl(true,musicId);
})
//监听音乐实时播放的进度
this.backgroundAudioManager.onTimeUpdate(() => {
this.musicPlayTime()
})
},
//观察音乐播放进度
musicPlayTime(){
//获取歌词对应时间
let lyricTime = Math.ceil(this.backgroundAudioManager.currentTime);
let currentTime = moment(this.backgroundAudioManager.currentTime * 1000).format('mm:ss');
let currentWidth = (this.backgroundAudioManager.currentTime/this.backgroundAudioManager.duration) * 450;
this.setData({
lyricTime,
currentTime,
currentWidth
})
//获取当前歌词
this.getCurrentLyric();
},
//修改播放状态
changePlayState(isPlay){
this.setData({
isPlay: isPlay
})
//修改全局播放状态
appInstance.globalData.isMusicPlay = isPlay;
},
//点击暂停/播放的回调
handleMusicPlay(){
//修改是否播放的状态
let isPlay = !this.data.isPlay;
// this.setData({
// isPlay: isPlay
// })
let {musicId} = this.data;
this.musicControl(isPlay,musicId);
},
//请求歌曲信息
async getMusicInfo(musicId){
let songData = await request('/song/detail',{ids: musicId});
let durationTime = moment(songData.songs[0].dt).format('mm:ss');
this.setData({
song: songData.songs[0],
durationTime: durationTime
})
//动态修改窗口标题
wx.setNavigationBarTitle({
title: this.data.song.name
})
},
//歌曲播放控制功能
async musicControl(isPlay,musicId){
if(isPlay){//音乐播放
//获取音频资源
let musicLinkData = await request('/song/url',{id: musicId})
console.log(musicLinkData)
let musicLink = musicLinkData.data[0].url;
if(musicLink === null){
wx.showToast({
title: '由于版权或会员问题暂获取不到此资源',
icon: 'none'
})
return;
}
this.setData({
isPlay: isPlay
})
//歌曲播放
this.backgroundAudioManager.src = musicLink;
this.backgroundAudioManager.title = this.data.song.name;
}else{//音乐暂停
this.backgroundAudioManager.pause();
}
},
//歌曲切换
handleSwitch(event){
//切换类型
let type = event.currentTarget.id;
//关闭当前播放音乐
this.backgroundAudioManager.stop();
//订阅来自recommendSong页面
PubSub.subscribe('musicId',(msg,musicId) => {
//获取歌曲
this.getMusicInfo(musicId);
//获得歌词
this.getLyric(musicId);
//自动播放当前音乐
this.musicControl(true,musicId);
//取消订阅
PubSub.unsubscribe('musicId');
})
//发布消息数据给recommendSong页面
PubSub.publish('switchMusic',type);
},
//获取歌词
async getLyric(musicId){
let lyricData = await request("/lyric", {id: musicId});
let lyric = this.formatLyric(lyricData.lrc.lyric);
},
//传入初始歌词文本text
formatLyric(text) {
let result = [];
let arr = text.split("\n"); //原歌词文本已经换好行了方便很多,我们直接通过换行符“\n”进行切割
let row = arr.length; //获取歌词行数
for (let i = 0; i < row; i++) {
let temp_row = arr[i]; //现在每一行格式大概就是这样"[00:04.302][02:10.00]hello world";
let temp_arr = temp_row.split("]");//我们可以通过“]”对时间和文本进行分离
let text = temp_arr.pop(); //把歌词文本从数组中剔除出来,获取到歌词文本了!
//再对剩下的歌词时间进行处理
temp_arr.forEach(element => {
let obj = {};
let time_arr = element.substr(1, element.length - 1).split(":");//先把多余的“[”去掉,再分离出分、秒
let s = parseInt(time_arr[0]) * 60 + Math.ceil(time_arr[1]); //把时间转换成与currentTime相同的类型,方便待会实现滚动效果
obj.time = s;
obj.text = text;
result.push(obj); //每一行歌词对象存到组件的lyric歌词属性里
});
}
result.sort(this.sortRule) //由于不同时间的相同歌词我们给排到一起了,所以这里要以时间顺序重新排列一下
this.setData({
lyric: result
})
},
sortRule(a, b) { //设置一下排序规则
return a.time - b.time;
},
//控制歌词播放
getCurrentLyric(){
let j;
for(j=0; j<this.data.lyric.length-1; j++){
if(this.data.lyricTime == this.data.lyric[j].time){
this.setData({
currentLyric : this.data.lyric[j].text
})
}
}
},
/**
* 生命周期函数--监听页面初次渲染完成
*/
onReady: function () {
},
/**
* 生命周期函数--监听页面显示
*/
onShow: function () {
},
/**
* 生命周期函数--监听页面隐藏
*/
onHide: function () {
},
/**
* 生命周期函数--监听页面卸载
*/
onUnload: function () {
},
/**
* 页面相关事件处理函数--监听用户下拉动作
*/
onPullDownRefresh: function () {
},
/**
* 页面上拉触底事件的处理函数
*/
onReachBottom: function () {
},
/**
* 用户点击右上角分享
*/
onShareAppMessage: function () {
}
})
版权声明:本文为qq_45803593原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。