前言 在数字化时代,我们正在见证音乐和视觉艺术的深度融合。流行音乐图像化,即将音乐视觉化,成为了一种独特的艺术表达方式。它跨越了时间和空间,用视觉元素诠释音乐的情感和氛围,为听众带来全新的感官体验。 今天分享的是一个基于WebAudio、canvas、three.js实现的简易音乐播放器。由于篇幅过长,所以分为三篇文章进行讲解,该篇文章主要叙述音乐播放的实现过程。
音乐播放功能实现请看上一篇文章 基于canvas和three.js实现音频可视化二
three.js实现可视化请看基于canvas和three.js实现音频可视化三
先来看下一最终效果:
实现简介
该音乐播放器基于vue、vant搭建,主要功能通过Web Audio、canvas、three.js实现。 接下来先看一下音乐播放的实现: 功能实现分为以下几个步骤:
- 首先要完成的是页面布局及样式
- 实现歌曲的播放和暂停
- 实现歌曲总时长、歌曲播放倒计时时间展示功能
- 拖动进度条实现歌曲的快进、后退
1、页面结构及样式
贴出的代码是最终的页面结构,包含了绑定的方法和变量,具体怎么实现的,看一下代码就明白了,这里就不多赘述。
<template>
<div>
<img :src="picUrl" alt="" class="bgimg" />
<!-- 轮播图区域显示 -->
<div class="detaiBlock">
<van-swipe class="my-swipe" :loop="false" indicator-color="white">
<van-swipe-item>
<div class="detailContent">
<img src="@/assets/cd.png" alt="" class="img_cd" />
<img :src="picUrl" alt="" class="img_ar" :class="{ img_ar_active: !isbtnShow, img_ar_pauesd: isbtnShow }" />
</div>
</van-swipe-item>
</van-swipe>
</div>
<!-- 底部音乐播放控制 -->
<div class="detailFooter">
<div class="footerContent">
<van-row justify="space-around" align="center">
<van-col span="3" class="musicDown musicTime">{{ countDownTime }}</van-col>
<van-col span="16">
<input
type="range"
class="range"
min="0"
:max="duration"
:value="currentTime"
@input="changeRangeFn"
step="0.05"
/>
</van-col>
<van-col span="3" class="musicTime"
><span>{{ musicTime(duration) }}</span></van-col
>
</van-row>
</div>
<div class="footer">
<!-- <svg class="icon" aria-hidden="true">
<use xlink:href="#icon-xunhuan"></use>
</svg> -->
<svg class="icon" aria-hidden="true">
<use xlink:href="#icon-shangyishoushangyige"></use>
</svg>
<svg class="icon bofang" aria-hidden="true" v-if="isbtnShow" @click="e => play()">
<use xlink:href="#icon-bofang1"></use>
</svg>
<svg class="icon bofang" aria-hidden="true" v-else @click="e => play()">
<use xlink:href="#icon-zanting"></use>
</svg>
<svg class="icon" aria-hidden="true">
<use xlink:href="#icon-xiayigexiayishou"></use>
</svg>
<!-- <svg class="icon" aria-hidden="true">
<use xlink:href="#icon-zu"></use>
</svg> -->
</div>
</div>
<audio
ref="audio"
@loadedmetadata="e => onloadedmetadata(e)"
:src="require('@/utils/music/yilushenghua.mp3')"
></audio>
</div>
</template>
<style lang="less" scoped>
.bgimg {
width: 100%;
height: 100%;
position: absolute;
z-index: -1;
filter: blur(70px);
transition: all 1s;
}
.detailTop {
width: 100%;
height: 1rem;
display: flex;
padding: 0.2rem;
justify-content: space-between;
align-items: center;
fill: #fff;
.detailTopLeft {
display: flex;
align-items: center;
.leftMarquee {
width: 3rem;
height: 100%;
margin-left: 0.4rem;
span {
color: #82b8d1;
}
.icon {
width: 0.3rem;
height: 0.3rem;
vertical-align: middle;
fill: #82b8d1;
margin-left: 0.1rem;
}
}
}
}
.detaiBlock {
// position: relative;
}
.detailContent {
width: 100%;
height: 9rem;
display: flex;
flex-direction: column;
align-items: center;
position: relative;
.img_needle {
width: 2rem;
height: 3rem;
position: absolute;
left: 46%;
transform-origin: 0 0;
transform: rotate(-13deg);
transition: all 2s;
}
.img_needle_active {
width: 2rem;
height: 3rem;
position: absolute;
left: 46%;
transform-origin: 0 0;
transform: rotate(0deg);
transition: all 2s;
}
.my_canvas {
position: absolute;
top: 50%;
transform: translateY(-50%);
z-index: -1;
}
.img_cd {
width: 4rem;
height: 4rem;
position: absolute;
// bottom: 2.3rem;
top: 50%;
transform: translateY(-50%);
z-index: -1;
}
.img_ar {
width: 2.5rem;
height: 2.5rem;
border-radius: 50%;
position: absolute;
top: 50%;
transform: translateY(-50%);
// bottom: 3.14rem;
animation: rotate_ar 10s linear infinite;
}
.img_ar_active {
animation-play-state: running;
}
.img_ar_pauesd {
animation-play-state: paused;
}
@keyframes rotate_ar {
0% {
transform: translateY(-50%) rotateZ(0deg);
}
100% {
transform: translateY(-50%) rotateZ(360deg);
}
}
}
.musicDown {
text-align: right;
}
.musicTime {
font-size: 0.2rem;
color: #fff;
margin-top: 0.13rem;
}
.threeContainer {
width: 100%;
height: 400px;
// min-height: 400px;
}
.detailFooter {
width: 100%;
height: 3rem;
position: absolute;
bottom: 0.2rem;
display: flex;
flex-direction: column;
justify-content: space-between;
.footerTop {
width: 100%;
height: 1rem;
display: flex;
justify-content: space-around;
align-items: center;
.icon {
width: 0.36rem;
height: 0.36rem;
fill: rgb(245, 234, 234);
}
.icon {
width: 0.6rem;
height: 0.6rem;
}
}
.range {
width: 100%;
height: 0.06rem;
}
.footer {
width: 100%;
height: 1rem;
display: flex;
justify-content: space-around;
align-items: center;
.icon {
fill: rgb(245, 234, 234);
}
.bofang {
width: 1rem;
height: 1rem;
}
}
}
</style>
2、实现歌曲的播放和暂停
首先是音乐播放,当点击播放按钮之后,我们先通过ref获取到音频对象,播放过程声音需要渐渐变大,我们给volume赋值为0,设置定时器渐变为1,大于等于1的时候清除定时器。 音乐暂停刚好相反, 我们给volume赋值为1,设置定时器渐变为0,小于等于0的时候清除定时器。
这里有个问题,暂停动作触发之后歌曲声音实际上并没有立即暂停,this.$refs.audio.paused获取到的值是false,所以点击暂停需要设置一个标识isToPause,代表当前状态在暂停中。 注意:在切换播放、暂停时要先清除定时器。
export default {
data() {
return {
playTimer: null,
pauseTimer: null,
countTimer: null, // 倒计时定时器
isToPause: false, // 暂停动作标识
currentTime: 0, //当前时间
duration: 0, //歌曲总时长
countDownTime: 0, // 倒计时时间
picUrl: 'https://p1.music.126.net/6y-UleORITEDbvrOLV0Q8A==/5639395138885805.jpg'
}
},
methods: {
// 播放或暂停
async play() {
// 在切换播放、暂停时先清除定时器
clearInterval(this.playTimer)
clearInterval(this.pauseTimer)
// 判断音乐是否播放
// 暂停动作触发之后歌曲声音实际上并没有立即暂停,而是渐渐减小,
// 这里设置一个标识isToPause用来表示当前是暂停状态
if (this.$refs.audio.paused || this.isToPause) {
this.isToPause = false
this.handlerPlay()
} else {
this.handlerPause()
}
},
// 播放(声音缓慢变大)
handlerPlay(e) {
const musicRef = this.$refs.audio
if (!musicRef) {
return
}
musicRef.play()
musicRef.volume = 0
let v = 0
this.playTimer = setInterval(() => {
v = v + 0.1
if (v <= 1) {
musicRef.volume = v
} else {
clearInterval(this.playTimer)
}
}, 200)
},
// 暂停(声音渐渐减小)
handlerPause() {
this.isToPause = true
const musicRef = this.$refs.audio
if (!musicRef) {
return
}
let v = 1
this.pauseTimer = setInterval(() => {
v -= 0.1
if (v > 0) {
musicRef.volume = v
} else {
clearInterval(this.pauseTimer)
this.isToPause = false
musicRef.pause()
}
}, 200)
},
}
}
3、实现歌曲播放倒计时功能
添加onloadedmetadata事件,通过歌曲dom对象duration属性获取歌曲时长,获取到的时间为秒(s)。 通过歌曲dom对象currentTime属性获取歌曲当前播放时长秒](s),总时长减去当前播放的时间点就是歌曲剩余播放时长,然后设置定时器即可。具体代码如下:
公共方法
function formatTime(time) {
const min = parseInt(time / 60)
const sec = parseInt(time % 60)
return `${min < 10 ? '0' + min : min} : ${sec < 10 ? '0' + sec : sec}`
}
computed
computed: {
// 歌曲总时长格式化
musicTime() {
return time => formatTime(time)
}
}
以下添加到methods
// 获取音乐时间
onloadedmetadata(e) {
this.duration = e.target.duration
},
// 设置倒计时
setCountDown() {
const duration = this.$refs.audio.duration
const curTime = this.$refs.audio.currentTime || 0
let target = duration - curTime
if (isNaN(target)) {
clearInterval(this.countTimer)
return
}
if (this.countTimer) {
clearInterval(this.countTimer)
}
this.countTimer = setInterval(() => {
const time = formatTime(--target)
this.countDownTime = time
this.currentTime = duration - target
if (target <= 0) {
clearInterval(this.countTimer)
}
}, 1000)
},
修改play方法
// 播放或暂停
async play() {
// 在切换播放、暂停时先清除定时器
clearInterval(this.playTimer)
clearInterval(this.pauseTimer)
clearInterval(this.countTimer)
// 判断音乐是否播放
// 暂停动作触发之后歌曲声音实际上并没有立即暂停,而是渐渐减小,
// 这里设置一个标识isToPause用来表示当前是暂停状态
// 判断音乐是否播放
if (this.$refs.audio.paused || this.isToPause) {
this.isToPause = false
this.handlerPlay()
setTimeout(() => {
this.setCountDown()
}, 1000)
} else {
this.handlerPause()
}
},
4、拖动进度条实现歌曲的快进、后退
首先添加input标签type类型为range,绑定最大值duration(歌曲总时长)、最小值为0,绑定value值等于currentTime(当前播放时间),代码如下:
<input
type="range"
class="range"
min="0"
:max="duration"
:value="currentTime"
@input="changeRangeFn"
step="0.05"
/>
这里拖动进度条要设置防抖,随后获取当前值,赋值给歌曲对象属性currentTime,然后需要重新设置歌曲倒计时,代码如下:
公共方法
function debounce(fn, time) {
var timer
return function () {
var _this = this
var args = arguments
if (timer) {
clearTimeout(timer)
}
timer = setTimeout(function () {
fn.apply(_this, args)
}, time)
}
}
添加生命周期方法
mounted() {
// this.initMusicOrigin();
this.changeRangeFn = debounce(this.changeRange, 200)
},
beforeDestroy () {
// 方法使用了闭包,这里释放一下内存
this.changeRangeFn = null
}
methods添加方法
// 更改音乐播放时间
changeRange(e) {
// 在音频播放器中指定播放时间
this.$refs.audio.currentTime = e.target.value
this.setCountDown()
},
最终代码
<script>
function debounce(fn, time) {
var timer
return function () {
var _this = this
var args = arguments
if (timer) {
clearTimeout(timer)
}
timer = setTimeout(function () {
fn.apply(_this, args)
}, time)
}
}
function formatTime(time) {
const min = parseInt(time / 60)
const sec = parseInt(time % 60)
return `${min < 10 ? '0' + min : min} : ${sec < 10 ? '0' + sec : sec}`
}
export default {
data() {
return {
playTimer: null,
pauseTimer: null,
countTimer: null, // 倒计时定时器
isToPause: false, // 暂停动作标识
currentTime: 0, //当前时间
duration: 0, //歌曲总时长
countDownTime: 0, // 倒计时时间
picUrl: 'https://p1.music.126.net/6y-UleORITEDbvrOLV0Q8A==/5639395138885805.jpg'
}
},
computed: {
// 设置时间格式
musicTime() {
return time => formatTime(time)
}
},
mounted() {
// this.initMusicOrigin();
this.changeRangeFn = debounce(this.changeRange, 200)
},
beforeDestroy () {
// 方法使用了闭包,这里释放一下内存
this.changeRangeFn = null
},
methods: {
// 播放或暂停
async play() {
// 在切换播放、暂停时先清除定时器
clearInterval(this.playTimer)
clearInterval(this.pauseTimer)
clearInterval(this.countTimer)
// 判断音乐是否播放
// 暂停动作触发之后歌曲声音实际上并没有立即暂停,而是渐渐减小,
// 这里设置一个标识isToPause用来表示当前是暂停状态
if (this.$refs.audio.paused || this.isToPause) {
this.isToPause = false
this.handlerPlay()
setTimeout(() => {
this.setCountDown()
}, 1000)
} else {
this.handlerPause()
}
},
// 播放(声音缓慢变大)
handlerPlay(e) {
const musicRef = this.$refs.audio
if (!musicRef) {
return
}
musicRef.play()
musicRef.volume = 0
let v = 0
this.playTimer = setInterval(() => {
v = v + 0.1
if (v <= 1) {
musicRef.volume = v
} else {
clearInterval(this.playTimer)
}
}, 200)
},
// 暂停(声音渐渐减小)
handlerPause() {
this.isToPause = true
const musicRef = this.$refs.audio
if (!musicRef) {
return
}
let v = 1
this.pauseTimer = setInterval(() => {
v -= 0.1
if (v > 0) {
musicRef.volume = v
} else {
clearInterval(this.pauseTimer)
this.isToPause = false
musicRef.pause()
}
}, 200)
},
// 获取音乐时间
onloadedmetadata(e) {
this.duration = e.target.duration
},
// 设置倒计时
setCountDown() {
const duration = this.$refs.audio.duration
const curTime = this.$refs.audio.currentTime || 0
let target = duration - curTime
if (isNaN(target)) {
clearInterval(this.countTimer)
return
}
if (this.countTimer) {
clearInterval(this.countTimer)
}
this.countTimer = setInterval(() => {
const time = formatTime(--target)
this.countDownTime = time
this.currentTime = duration - target
if (target <= 0) {
clearInterval(this.countTimer)
}
}, 1000)
},
// 更改音乐播放时间
changeRange(e) {
// 在音频播放器中指定播放时间
this.$refs.audio.currentTime = e.target.value
this.setCountDown()
},
}
}
</script>