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部分也比较简单包含两部分:
- 可视化部分用到了getByteFrequencyData方法来获取到傅里叶计算后的结果,根据结果计算线的高度。
- 更新当前播放时间,如果处于拖拽状态的话直接跳过。正常播放状态实时更新时间,前面提到了当前播放时间=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
- 收藏
- 分享
- 文章举报
相关文章推荐
- Web Audio API 实现音频可视化
- HTML5 Web Audio API实现【董小姐】播放
- 使用Web Audio API实现基于浏览器的Web端录音
- HTML5 ——web audio API 音乐可视化(二)
- Linux平台,使用JavaComm3 API及SMSLib项目实现在Web Application中发送手机短信的功能
- 初试konckout+webapi简单实现增删改查
- 通过jQuery和C#分别实现对.NET Core Web Api的访问以及文件上传
- WebApi Ajax 跨域请求解决方法(CORS实现)
- abp(net core)+easyui+efcore实现仓储管理系统——ABP WebAPI与EasyUI结合增删改查之五(三十一)
- HDFS-API调用-基于MVC框架实现WEB操作记录
- 使用delphi+intraweb进行微信开发5—准备实现微信API,先从获取AccessToken开始
- 【web audio】web简易、可视化、音频播放器 0 0
- 基于ASP.NET WebAPI OWIN实现Self-Host项目实战
- ASP.NET Core Web API下事件驱动型架构的实现(二):事件处理器中对象生命周期的管理
- WebAPI+SignalR实现实时日志监测(二)
- 平庸技术流,用 WebApi +AngularJS 实现网络爬虫
- spark08--kafka组件,面试题,常用命令,可视化,api,Streaming简介,DStream,nc服务,Streaming实现Wordcount
- Web语音处理 - Web Audio API & WebRTC
- ASP.NET Core Web API下事件驱动型架构的实现(一):一个简单的实现
- HTML5 WebAudioAPI(三)--绘制频谱图