微信小程序播放音乐并同步一次显示一行歌词

2023-11-14

主要是对于歌词部分的描述
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(":") 就可以得到分钟和秒的时间(即 0000.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 () {

  }
})
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

微信小程序播放音乐并同步一次显示一行歌词 的相关文章