您的位置:首页 > 其它

web audio API实现可视化音乐盒

2020-04-22 01:27 351 查看

  最近给个人博客做了个音乐盒功能,能实现暂停,切换歌曲,拖拽时间,音频可视化的功能,效果:在个人博客界面右上角可以看到旋转的音乐图标,hover后可展示音乐盒。因为项目中用了vuecli框架,音乐盒以vue组件的形式编写,图标用到了阿里图标库,如果你想直接使用该组件需要替换下图标,css变量。

1 旋转音标

  通过animation重复动画实现,不是很难,值得注意的是rotate对inline标签不起作用,需要转化成block或者inline-block,直接上代码:

.musicBox .icon-music{
/* rotate对inline元素不起作用,转化成block */
display: block;
font-size: 1.8em;
color: var(--green1);
cursor: pointer;
animation: musicRotate 5s linear infinite;
}
@keyframes musicRotate{
from{
transform: rotate(0);
}
to{
transform: rotate(360deg);
}
}

2 拖拽条设置

  一个input做拖动条,一个div标签做填充。

<div class="progress"></div>
<input
type="range"
class="range"
:value='audio.value'
min="0" max="1" step="0.001"
@mousedown="setTime=true"
@touchstart="setTime=true"
@input="dragTime"
@change="changeTime"
>

js部分包含了audio API部分,可以先跳过看css,看完音频可视化再来看这部分js,拖拽过程实时更新进度条跟时间,但是不更新播放的音乐,当松手(input的值改变)时计算出时间然后跳转到指定时间音乐,audioCtx跟source属于Audio API。

dragTime(e){
const value = e.target.value
// 更新当前时间
const minutes = parseInt(value * source.buffer.duration / 60, 10)
const seconds = parseInt(value * source.buffer.duration % 60)
this.audio.currentMinutes = minutes
this.audio.currentSeconds = seconds
if(minutes < 10)
this.audio.currentMinutes = '0' + minutes
if(seconds < 10)
this.audio.currentSeconds = '0' + seconds
this.audio.value = value
document.querySelector('.progress').style.width = this.audio.value*95 + '%'
},
changeTime(e){
const value = e.target.value
offect = value * this.buffer.duration
source.stop()
audioCtx = new AudioContext()//重置audioCtx,会清空currentTime
this.setSource()
// 音频立即从offect秒开始播放
source.start(0,offect)
this.playing = true
this.setTime = false
},

修改默认range的样式,并让div标签与input标签重叠,将div标签覆盖在input上方,改变div的宽度实现背景已播放部分的背景填充。

/* 去掉默认样式 */
.range{
-webkit-appearance: none;
width: 100%;
border: none;
position: absolute;
left: 0;
}
.range:focus{
box-shadow: none;
}
/* 进度条样式 */
.range::-webkit-slider-runnable-track{
height: 5px;
border-radius: 20px;
background-color: var(--tint-gray);
z-index: 0;
}
/* 滑块样式 */
.range::-webkit-slider-thumb{
-webkit-appearance: none;
height: 13px;
width: 13px;
margin-top: -4px;
background-color: var(--green2);
border-radius: 50%;
cursor: pointer;
}
/* 填充背景 */
.progress{
background-color: var(--green1);
height: 5px;
width: 0;
border-radius: 20px;
position: absolute;
overflow: hidden;
z-index: 1;
cursor: pointer;
}
/* 解决覆盖在input上方的部分无法被点击问题 */
.progress:hover{
z-index: 0;
}

3 audio API使用

参考文档,主要用到了:

  • AudioContext
  • Analyser - 音频解析器
  • BufferSource - 音频播放器
var source,audioCtx,analyser,offect=0
loadMusic(){//部分内容
// 初始化audio API
let AudioContext = window.AudioContext || window.webkitAudioContext;
audioCtx = new AudioContext() //实例化AudioContext对象
var dataArray //存放解析后的数据

// 请求音频文件,以二进制格式返回
this.$axios.get(this.audio.url,{responseType:'arraybuffer'})
.then(res => {
// 解码二进制文件
audioCtx.decodeAudioData(res.data,(buffer) => {
this.buffer = buffer
this.setSource()
// 创建接受数组
dataArray = new Uint8Array(analyser.frequencyBinCount)
// 计算音频时长
this.audio.minutes = parseInt(buffer.duration / 60, 10)
this.audio.seconds = parseInt(buffer.duration % 60)
// 开始播放
if(!firstLoad){//第一次播放需要点击触发,在mounted部分定义
this.playing = true
source.start()
}
draw()
})
})
},
setSource(){
// 创建音频解析器
analyser = audioCtx.createAnalyser()
// 计算频域信号时使用的 FFT,值为需要可视化的数量的两倍
//例如项目中创建了32根线,这里就用64,需要是以2为底的数。
analyser.fftSize = 64
// 创建播放节点
source = audioCtx.createBufferSource()
// 填充音频buffer数据
source.buffer = this.buffer
// 连接节点
source.connect(analyser)
analyser.connect(audioCtx.destination)
// 播放结束事件
source.onended = () => {
if(audioCtx.currentTime > 0)
this.switchMusic(1)//切换下一曲
}
},

注释都挺完整的了,应该都能理解。可能播放事件的条件比较疑惑,这里需要考虑到changeTime里的事件

  • audioCtx.currentTIme理解为source.start()后播放的时间,单纯的改变source是不能够修改audioCtx.currentTIme的值得,所以利用重置audioCtx清零currentTime
  • source.stop()会触发播放结束事件
  • 通过拖拽触发的播放结束事件将不被触发,只有真正的播放结束才会触发
  • 使用BufferSource来播放音乐是无法直接改变currentTime的(报错只可读,暂未找到其他解决方法)
  • 通过调整开始播放的时间来实现切换播放时间的效果
  • 设置offect来填充跳过的音频,当前播放时间=audioCtx.currentTIme + offect
// 获取到音频已播放的时长
audioCtx.currentTime

changeTime(){
……
// 停止播放
source.stop()
audioCtx = new AudioContext()//重置audioCtx,会清空currentTime
this.setSource()
// 音频立即从offect秒开始播放,offect的值参考完整的changeTime函数
source.start(0,offect)
……
}

4 音频可视化

也可以用canvas画图的方式实现

<div class="animate">
<div
class="item"
v-for="item in 32"
:key="item"
>
</div>
</div>
.musicBox .box .animate{
width: 100%;
height: 80px;
display: flex;
justify-content: space-between;
align-items: flex-end;
overflow: hidden;
}
.musicBox .box .animate .item{
width: 2px;
height: 30px;
background-color: var(--green1);
border-radius: 4px;
}
loadMusic(){
……
const doms = document.querySelectorAll('.animate .item')
const draw = () => {
requestAnimationFrame(draw)
// 判断是否是正在播放状态
if(!this.playing) return
analyser.getByteFrequencyData(dataArray)//傅里叶计算

doms.forEach((item,i) => {//修改线长
item.style.height = `${dataArray[i]/8 + 30}px`
})

// 判断是否处于拖拽状态
if(this.setTime) return
// 获取当前播放的时间
const currentTime = audioCtx.currentTime + offect
const minutes = parseInt(currentTime / 60, 10)
const seconds = parseInt(currentTime % 60)
this.audio.currentMinutes = minutes
this.audio.currentSeconds = seconds
if(minutes < 10)
this.audio.currentMinutes = '0' + minutes
if(seconds < 10)
this.audio.currentSeconds = '0' + seconds
this.audio.value = currentTime / source.buffer.duration
document.querySelector('.progress').style.width = this.audio.value*94 + '%'
}
draw()
……
}

js部分也比较简单包含两部分:

  1. 可视化部分用到了getByteFrequencyData方法来获取到傅里叶计算后的结果,根据结果计算线的高度。
  2. 更新当前播放时间,如果处于拖拽状态的话直接跳过。正常播放状态实时更新时间,前面提到了当前播放时间=audioCtx.currentTime + offect,这里的offect很关键,因为audioCtx.currentTime仅代表已播放的时间,前面已经被重置过了,所以不准确需要用offect来补充。

5 完整代码

  可以在我的博客项目地址中找到,github: src -> components -> musicBox.vue
  项目中音频是写死的,可以自行通过请求来获取,还有字幕,播放列表,音效等功能待完善。

<template>
<div class="musicBox">
<i
class="iconfont icon-music"
@mouseenter="showBox=true"
>
</i>
<div class="box" v-show="showBox" @mouseleave="showBox=false">
<div class="animate">
<div
class="item"
v-for="item in 32"
:key="item"
>
</div>
</div><header>
<p class="name">{{audio.name}}</p>
<p class="time">{{audio.currentMinutes}}:{{audio.currentSeconds}}/{{audio.minutes}}:{{audio.seconds}}</p>
<div class="progress"></div>
<input
type="range"
class="range"
:value='audio.value'
min="0" max="1" step="0.001"
@mousedown="setTime=true"
@touchstart="setTime=true"
@input="dragTime"
@change="changeTime"
></header>

<div class="btns">
<i class="iconfont icon-last" @click="switchMusic(-1)"></i>
<i class="iconfont icon-start" v-if="!playing" @click="musicPlay()"></i>
<i class="iconfont icon-pause" v-else @click="musicPlay()"></i>
<i class="iconfont icon-next" @click="switchMusic(1)"></i>
</div>
</div>
</div>
</template>

<script>
var source,audioCtx,analyser,offect=0
var firstLoad = true
var urls = [
{
name : "司南 - 冬眠",
url : 'http://blogs.jinlongyuchitang.cn/music/%E5%8F%B8%E5%8D%97%20-%20%E5%86%AC%E7%9C%A0.ogg'
},
{
name : "G.E.M. 邓紫棋 - 很久以后",
url : 'http://blogs.jinlongyuchitang.cn/music/G.E.M.%20%E9%82%93%E7%B4%AB%E6%A3%8B%20-%20%E5%BE%88%E4%B9%85%E4%BB%A5%E5%90%8E.ogg'
},
{
name : "程响 - 世界这么大还是遇见你",
url : 'http://blogs.jinlongyuchitang.cn/music/%E7%A8%8B%E5%93%8D%20-%20%E4%B8%96%E7%95%8C%E8%BF%99%E4%B9%88%E5%A4%A7%E8%BF%98%E6%98%AF%E9%81%87%E8%A7%81%E4%BD%A0.ogg'
},
{
name : "阿冗 - 你的答案",
url : 'http://blogs.jinlongyuchitang.cn/music/%E9%98%BF%E5%86%97%20-%20%E4%BD%A0%E7%9A%84%E7%AD%94%E6%A1%88.ogg'
},
]
var currentUrl = 0
export default{
data(){
return{
showBox : false,
currentUrl : 0,
audio : {},
playing : false,
setTime : false,
buffer : ''
}
},
methods:{
initAudio(){
document.querySelector('.progress').style.width = 0
this.audio = {
url : urls[currentUrl].url,
name : urls[currentUrl].name,
minutes : '00',
seconds : '00',
currentMinutes : '00',
currentSeconds : '00',
value : 0
}
offect = 0
this.setTime = false
this.playing = false
},
loadMusic(){
this.initAudio()
// 初始化audio API
let AudioContext = window.AudioContext || window.webkitAudioContext;
audioCtx = new AudioContext() //实例化AudioContext对象
var dataArray

// 请求音频文件,以二进制格式返回
this.$axios.get(this.audio.url,{responseType:'arraybuffer'})
.then(res => {
// 解码二进制文件
audioCtx.decodeAudioData(res.data,(buffer) => {
this.buffer = buffer
this.setSource()
// 创建接受数组
dataArray = new Uint8Array(analyser.frequencyBinCount)
// 获取音频时长
this.audio.minutes = parseInt(buffer.duration / 60, 10)
this.audio.seconds = parseInt(buffer.duration % 60)
// 开始播放
if(!firstLoad){
this.playing = true
source.start()
}
draw()
})
})

const doms = document.querySelectorAll('.animate .item')
const draw = () => {
requestAnimationFrame(draw)
if(!this.playing) return
analyser.getByteFrequencyData(dataArray)//傅里叶计算

doms.forEach((item,i) => {
item.style.height = `${dataArray[i]/8 + 30}px`
})

// 更新进度
if(this.setTime) return
const currentTime = audioCtx.currentTime + offect
const minutes = parseInt(currentTime / 60, 10)
const seconds = parseInt(currentTime % 60)
this.audio.currentMinutes = minutes
this.audio.currentSeconds = seconds
if(minutes < 10)
this.audio.currentMinutes = '0' + minutes
if(seconds < 10)
this.audio.currentSeconds = '0' + seconds
this.audio.value = currentTime / source.buffer.duration
document.querySelector('.progress').style.width = this.audio.value*94 + '%'
}
draw()
},
setSource(){
// 创建音频解析器
analyser = audioCtx.createAnalyser()
// 计算频域信号时使用的 FFT,值为需要可视化的数量的两倍
analyser.fftSize = 64
// 创建播放节点
source = audioCtx.createBufferSource()
// 填充音频buffer数据
source.buffer = this.buffer
// 连接节点
source.connect(analyser)
analyser.connect(audioCtx.destination)
// 播放结束事件
source.onended = () => {
if(audioCtx.currentTime > 0)
this.switchMusic(1)
}
},
switchMusic(e){
audioCtx.close()
this.playing = false
if(e === 1 &&  currentUrl === urls.length-1)
currentUrl = 0
else if(e === -1 && currentUrl === 0)
currentUrl = urls.length-1
else
currentUrl += e
this.loadMusic()
},
dragTime(e){
const value = e.target.value
// 更新当前时间
const minutes = parseInt(value * source.buffer.duration / 60, 10)
const seconds = parseInt(value * source.buffer.duration % 60)
this.audio.currentMinutes = minutes
this.audio.currentSeconds = seconds
if(minutes < 10)
this.audio.currentMinutes = '0' + minutes
if(seconds < 10)
this.audio.currentSeconds = '0' + seconds
this.audio.value = value
document.querySelector('.progress').style.width = this.audio.value*95 + '%'
},
changeTime(e){
const value = e.target.value
offect = value * this.buffer.duration
source.stop()
audioCtx = new AudioContext()
this.setSource()
source.start(0,offect)
this.playing = true
this.setTime = false
},
musicPlay(){
if(this.playing)
audioCtx.suspend()
else
audioCtx.resume()
this.playing = !this.playing
},
},
mounted() {
urls.sort(() => {
return 0.5-Math.random()
})
this.loadMusic()
window.onclick = (e) => {
if(firstLoad){
window.onclick = ""
firstLoad = false
this.playing = true
source.start()
}
}
},
beforeDestroy() {
this.initAudio()
audioCtx.close()
}
}
</script>

<style>
.musicBox{
position: absolute;
z-index: 99;
right: 20px;
top: 10px;
}
.musicBox .icon-music{
/* rotate对inline元素不起作用,转化成block */
display: block;
font-size: 1.8em;
color: var(--green1);
cursor: pointer;
animation: musicRotate 5s linear infinite;
}
@keyframes musicRotate{
from{
transform: rotate(0);
}
to{
transform: rotate(360deg);
}
}
.musicBox .box{
position: absolute;
min-width: 250px;
left: -220px;
top: 0;
width: 200px;
padding: 5px;
background-color: #FFFFFF;
box-shadow: var(--box-shadow1);
border-radius: 5px;
}

.musicBox .box .animate{
width: 100%;
height: 80px;
display: flex;
justify-content: space-between;
align-items: flex-end;
overflow: hidden;
}
.musicBox .box .animate .item{
width: 2px;
height: 30px;
background-color: var(--green1);
border-radius: 4px;
}
.musicBox .box header{
text-align: center;
}
.musicBox .box header .time{
margin-bottom: 5px;
font-size: 0.9em;
color: var(--green2);
}

.musicBox .box .btns{
margin-top: 15px;
display: flex;
justify-content: center;
}
.musicBox .box .btns i{
margin: 0 15px;
font-size: 1.5em;
cursor: pointer;
}
.musicBox .box .btns i.icon-last,
.musicBox .box .btns i.icon-next{
color: var(--font-dark-remark);
}
.musicBox .box .btns i.icon-pause,
.musicBox .box .btns i.icon-start{
color: var(--green2);
}

/* 去掉默认样式 */
.range{
-webkit-appearance: none;
width: 100%;
border: none;
position: absolute;
left: 0;
}
.range:focus{
box-shadow: none;
}
/* 进度条样式 */
.range::-webkit-slider-runnable-track{
height: 5px;
border-radius: 20px;
background-color: var(--tint-gray);
z-index: 0;
}
/* 滑块样式 */
.range::-webkit-slider-thumb{
-webkit-appearance: none;
height: 13px;
width: 13px;
margin-top: -4px;
background-color: var(--green2);
border-radius: 50%;
cursor: pointer;
}
/* 填充背景 */
.progress{
background-color: var(--green1);
height: 5px;
width: 0;
border-radius: 20px;
position: absolute;
overflow: hidden;
z-index: 1;
cursor: pointer;
}
.progress:hover{
z-index: 0;
}
</style>

  • 点赞 2
  • 收藏
  • 分享
  • 文章举报
璡龙鱼 发布了4 篇原创文章 · 获赞 4 · 访问量 225 私信 关注
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: