SoundJs | Web-Audio-API实现多音频播放
因为HTML5 Audio不支持同时播放多首歌,今天我们主要介绍一下soundjs和Web-Audio-API实现多音频播放的方法,文末还总结了两种实现方式的demo,大家可以依据实际项目情况添加接口。
| 导语 再回首-“凤凰传奇邀你唱H5”
前言
需求背景:用户选择玲花或曾毅,参与合唱,合唱成功后上传用户录音数据,封装待播放的音频信息(这些部分都是由林雨哥[lennylin]完成的),到落版页请求歌曲进行播放。其中ios微信侧录制音频会屏蔽掉媒体本身的声音,所以ios播放用户合唱歌曲需要同时播放用户录制音频和背景音乐。
(凤凰传奇邀你唱H5)
目录
1. soundjs多音频播放
常用属性:
duration |
音频时长 |
volume |
音量 |
playState |
音频播放状态:playFinished | playSucceeded |
paused |
s.paused = true 设置音频暂停 |
startTime |
音频开始播放时间点 |
loop |
音频循环次数 |
常用方法:
play() | 音频播放 |
stop() | 音频暂停播放,重置播放位置为0,如果想保留播放位,可以使用sound.paused = true |
常用事件:
complete | 音频播放完成 |
succeeded | 回放成功时触发 |
首先我们看下单个音频播放:
createjs.Sound.alternateExtensions //设置声音后缀名
createjs.Sound.registerSound //注册音频,sound播放音乐之前需先注册
createjs.Sound.on("fileload", this.loadHandler, this); //针对每个注册音频加载完后的处理事件
1 2 3 4 5 6 7 8 |
createjs.Sound.alternateExtensions = [<span class="hljs-string">"mp3"</span>]; createjs.Sound.on(<span class="hljs-string">"fileload"</span>, <span class="hljs-keyword">this</span>.loadHandler, <span class="hljs-keyword">this</span>); createjs.Sound.registerSound(<span class="hljs-string">"path/to/mySound.ogg"</span>, <span class="hljs-string">"sound"</span>); function loadHandler(event) { <span class="hljs-keyword">var</span> instance = createjs.Sound.play(<span class="hljs-string">"sound"</span>); <span class="hljs-comment">// 可使用ID、完整的源路径或event.src。</span> instance.on(<span class="hljs-string">"complete"</span>, <span class="hljs-keyword">this</span>.handleComplete, <span class="hljs-keyword">this</span>); instance.volume = <span class="hljs-number">0.5</span>; } |
多音频播放:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
sounds.forEach(<span class="hljs-function"><span class="hljs-keyword">function</span></span><span class="hljs-function"> (</span><span class="hljs-function"><span class="hljs-params">sound</span></span><span class="hljs-function">) </span>{ createjs.Sound.alternateExtensions = [<span class="hljs-string">"mp3"</span>]; createjs.Sound.registerSound(sound.src, sound.id); <span class="hljs-keyword">var</span> promise = <span class="hljs-keyword">new</span> <span class="hljs-built_in">Promise</span>(<span class="hljs-function"><span class="hljs-keyword">function</span></span><span class="hljs-function"> (</span><span class="hljs-function"><span class="hljs-params">resolve, reject</span></span><span class="hljs-function">) </span>{ createjs.Sound.on(<span class="hljs-string">"fileload"</span>, <span class="hljs-function"><span class="hljs-keyword">function</span></span><span class="hljs-function"> (</span><span class="hljs-function"><span class="hljs-params">event</span></span><span class="hljs-function">) </span>{ <span class="hljs-keyword">if</span> (sound.id == event.id) { resolve(createjs.Sound.play(event.id)) } }) }) promises.push(promise) }) <span class="hljs-keyword">return</span> <span class="hljs-built_in">Promise</span>.all(promises) |
值得注意的是,当sound.src是一个会重定向的音频地址,需要先获得重定向后的真实地址,再注册音频。回顾我们的需求,落版页听录制歌曲的时候,我们需要拿用户openid去请求服务器端的php文件,服务器会重定向到一个具体的mp3地址,代码片段如下:
1 2 |
<span class="hljs-selector-tag">createjs</span><span class="hljs-selector-class">.Sound</span><span class="hljs-selector-class">.registerSound</span>(<span class="hljs-string">'xxx.php?openid=xxx'</span>, <span class="hljs-string">'userMp3'</span>); × <span class="hljs-selector-tag">createjs</span><span class="hljs-selector-class">.Sound</span><span class="hljs-selector-class">.registerSound</span>(request.responseURL, <span class="hljs-string">'userMp3'</span>); √ |
先看一下readyState几种状态
0 |
|
|
1 |
|
|
2 |
HEADERS_RECEIVED (已获取响应头) |
|
3 |
LOADING (正在下载响应体) |
响应体下载中 |
4 |
DONE (请求完成) |
整个请求过程已经完毕. |
1 2 3 4 |
</code><code class="hljs kotlin"><span class="hljs-keyword">if</span></code><code class="hljs kotlin"> (<span class="hljs-keyword">this</span>.readyState == <span class="hljs-keyword">this</span>.HEADERS_RECEIVED) { createjs.Sound.registerSound(request.responseURL, sound.id); <span class="hljs-keyword">this</span>.abort(); } |
注意:如果在初始化音频的时候再去发request请求,会导致微信侧播放异常,所以提前获取音频真实地址,在回调里面再执行音频初始化。
当点击恢复音频播放时需要考虑是否所有音频都已经播放完成,如果是则调用play(),否则仅仅设置paused的状态为false。因为这个时候短音频可能已经播放完成,长音频未播放完成并处于暂停状态,如果不判断直接调用play(),短音频会重新开始播放。
1 2 3 4 5 6 7 8 9 10 11 12 13 |
playAll(){ <span class="hljs-keyword">let</span> allFinished = <span class="hljs-literal">false</span>; <span class="hljs-keyword">this</span>.sounds.forEach(<span class="hljs-function"><span class="hljs-keyword">function</span></span><span class="hljs-function"> (</span><span class="hljs-function"><span class="hljs-params">sound</span></span><span class="hljs-function">) </span>{ allFinished = sound.playState !== <span class="hljs-string">'playFinished'</span> ? <span class="hljs-literal">false</span> : <span class="hljs-literal">true</span>; }); <span class="hljs-keyword">this</span>.sounds.forEach(<span class="hljs-function"><span class="hljs-keyword">function</span></span><span class="hljs-function">(</span><span class="hljs-function"><span class="hljs-params">sound</span></span><span class="hljs-function">)</span>{ <span class="hljs-keyword">if</span>(allFinished){ sound.play(); }<span class="hljs-keyword">else</span>{ sound.paused = <span class="hljs-literal">false</span>; } }); } |
2. Web-Audio-API多音频播放
AudioContext |
音频上下文 控制其包含节点的创建、处理和解码,使用其他接口之前需创建一个音频上下文 |
AudioNode | 音频节点 |
AudioBuffer |
音频数据对象 可以通过AudioContext.createBuffer 来创建或者 通过 AudioContext.decodeAudioData成功解码音轨后获取. |
AudioBufferSourceNode |
方法:AudioBufferSourceNode.start() AudioBufferSourceNode.stop() 事件:ended |
ended |
音频播放停止时触发 |
1 2 3 4 5 6 7 |
</code><code class="hljs javascript"><span class="hljs-function"><span class="hljs-keyword">function</span></span></code><code class="hljs javascript"> <span class="hljs-function"><span class="hljs-title">playSound</span></span><span class="hljs-function">(</span><span class="hljs-function"><span class="hljs-params">buffer</span></span><span class="hljs-function">)</span>{ <span class="hljs-keyword">var</span> context = <span class="hljs-keyword">new</span> (<span class="hljs-built_in">window</span>.AudioContext || <span class="hljs-built_in">window</span>.webkitAudioContext)(), source = context.createBufferSource(); source.buffer = buffer;<span class="hljs-comment">// 告诉音频源 播放哪一段音频</span> source.connect(context.destination);<span class="hljs-comment">// 连接到输出源</span> source.start(<span class="hljs-number">0</span>);<span class="hljs-comment">//开始播放</span> } |
定义音频上下文需注意:webkit内核的浏览器需要带webkit前缀(webkitAudioContext)
2.3 多音频播
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
opts = [{ id: <span class="hljs-string">'1mp3'</span>, url: <span class="hljs-string">'mp3/select_1_1.mp3'</span> },{ id: <span class="hljs-string">'2mp3'</span>, url: <span class="hljs-string">'mp3/select_0_1.mp3'</span> } ] opts.<span class="hljs-keyword">forEach</span>(opt => { <span class="hljs-keyword">var</span> request = <span class="hljs-keyword">new</span> XMLHttpRequest(); <span class="hljs-keyword">var</span> context = <span class="hljs-keyword">new</span> (window.AudioContext || window.webkitAudioContext)(); <span class="hljs-keyword">self</span>.sounds[opt.id] = <span class="hljs-keyword">new</span> Sound(opt.id, opt.url, context); request.open(<span class="hljs-string">'GET'</span>, opt.url, <span class="hljs-keyword">true</span>); request.responseType = <span class="hljs-string">'arraybuffer'</span>; <span class="hljs-keyword">var</span> p = <span class="hljs-keyword">new</span> Promise(<span class="hljs-function"><span class="hljs-keyword">function</span></span><span class="hljs-function"> <span class="hljs-params">(</span></span><span class="hljs-function"><span class="hljs-params">resolve, reject</span></span><span class="hljs-function"><span class="hljs-params">)</span> </span>{ <span class="hljs-comment">//下面就是对音频文件的异步解析</span> request.onload = <span class="hljs-function"><span class="hljs-keyword">function</span></span><span class="hljs-function"> <span class="hljs-params">(</span></span><span class="hljs-function"><span class="hljs-params">)</span> </span>{ context.decodeAudioData(request.response, <span class="hljs-function"><span class="hljs-keyword">function</span></span><span class="hljs-function"> <span class="hljs-params">(</span></span><span class="hljs-function"><span class="hljs-params">buffer</span></span><span class="hljs-function"><span class="hljs-params">)</span> </span>{ <span class="hljs-keyword">self</span>.sounds[opt.id].setBuffer(buffer); <span class="hljs-keyword">self</span>.sounds[opt.id].setEnd(<span class="hljs-number">0</span>); resolve(<span class="hljs-keyword">self</span>.sounds[opt.id]); }, <span class="hljs-function"><span class="hljs-keyword">function</span></span><span class="hljs-function"> <span class="hljs-params">(</span></span><span class="hljs-function"><span class="hljs-params">err</span></span><span class="hljs-function"><span class="hljs-params">)</span> </span>{ reject(err); }); }; }); request.send(); promises.push(p); }); |
一个 AudioBufferSourceNode 只能被播放一次,每次调用 start() 之后,如果还想再播放一遍同样的声音,那么就需要再创建一个 AudioBufferSourceNode,如果想要多次播放声音,需要保留音频播放上下文和音频数据,这里可以定义一个class,初始化每个音频的id,url地址,buffer数据以及音频上下文等等。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
</code><code class="hljs kotlin"><span class="hljs-class"><span class="hljs-keyword">class</span></span></code><code class="hljs kotlin"> <span class="hljs-class"><span class="hljs-title">Sound</span></span> { <span class="hljs-comment">//音频id,音频url,音频播放上下文</span> <span class="hljs-keyword">constructor</span>(id, url, context) { <span class="hljs-keyword">this</span>.id = id; <span class="hljs-keyword">this</span>.url = url; <span class="hljs-keyword">this</span>.context = context; <span class="hljs-keyword">this</span>.endState = <span class="hljs-number">0</span>; } <span class="hljs-comment">//设置音频数据</span> setBuffer(buf) { <span class="hljs-keyword">this</span>.buffer = buf; } <span class="hljs-comment">//定义音频播放完成事件</span> setEnd(end) { <span class="hljs-keyword">this</span>.endState = end; } on(event, callback) { <span class="hljs-keyword">this</span>.events[event] = callback; } <span class="hljs-comment">//音频播放事件</span> play() { } <span class="hljs-comment">//音频暂停事件</span> pause() { } } |
2.4 音频暂停、播放
1 2 3 4 5 6 7 8 9 |
<span class="hljs-keyword">if</span> ( context.state === <span class="hljs-string">'running'</span>) { context.suspend().<span class="hljs-keyword">then</span>(<span class="hljs-function"><span class="hljs-keyword">function</span></span><span class="hljs-function"> <span class="hljs-params">(</span></span><span class="hljs-function"><span class="hljs-params">)</span></span> { console.log(<span class="hljs-string">'Resume context'</span>); }); } <span class="hljs-keyword">else</span> <span class="hljs-keyword">if</span> (context.state === <span class="hljs-string">'suspended'</span>) { context.<span class="hljs-built_in">resume</span>().<span class="hljs-keyword">then</span>(<span class="hljs-function"><span class="hljs-keyword">function</span></span><span class="hljs-function"> <span class="hljs-params">(</span></span><span class="hljs-function"><span class="hljs-params">)</span></span> { console.log(<span class="hljs-string">'Suspend context'</span>); }); } |
2.5 音频播放结束
绑定音频播放结束事件
1 2 3 |
sound.on(<span class="hljs-string">'ended'</span>, <span class="hljs-function"><span class="hljs-keyword">function</span></span><span class="hljs-function"> <span class="hljs-params">(</span></span><span class="hljs-function"><span class="hljs-params">)</span></span> { sound.setEnd(<span class="hljs-number">1</span>); }) |
综上所述,当对音频点击操作时,需要先判断音频播放状态,如果正在播放则直接暂停;如果已经暂停并且没有播放结束,那么执行恢复操作resume();如果音频已经播放完成,则
需要再创建一个 AudioBufferSourceNode,重新绑定context、buffer等信息
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
<span class="hljs-keyword">if</span>(<span class="hljs-keyword">this</span>.context.state === <span class="hljs-string">'running'</span> && <span class="hljs-keyword">this</span>.endState === <span class="hljs-number">0</span>){ <span class="hljs-keyword">this</span>.context.suspend(); } <span class="hljs-keyword">else</span> <span class="hljs-keyword">if</span> (<span class="hljs-keyword">this</span>.context.state === <span class="hljs-string">'suspended'</span> && <span class="hljs-keyword">this</span>.endState === <span class="hljs-number">0</span>) { <span class="hljs-keyword">this</span>.context.resume(); } <span class="hljs-keyword">else</span>{ <span class="hljs-keyword">this</span>.source = <span class="hljs-keyword">this</span>.context.createBufferSource(); <span class="hljs-keyword">if</span> (<span class="hljs-keyword">this</span>.events.ended) { <span class="hljs-keyword">this</span>.source.onended = <span class="hljs-keyword">this</span>.events.ended; } <span class="hljs-keyword">this</span>.source.buffer = <span class="hljs-keyword">this</span>.buffer;<span class="hljs-comment">// 告诉音频源 播放哪一段音频</span> <span class="hljs-keyword">this</span>.source.connect(<span class="hljs-keyword">this</span>.context.destination);<span class="hljs-comment">// 连接到输出源</span> <span class="hljs-keyword">this</span>.source.start(<span class="hljs-number">0</span>);<span class="hljs-comment">//开始播放</span> <span class="hljs-keyword">this</span>.endState = <span class="hljs-number">0</span>; } |
3. 音频播放进度条实现
svg的stroke-dasharray属性:stroke-dasharray=”0,10000″,其中第一个属性是虚线宽度,第二个是虚线之间间隔,保证第二个属性大于圆形周长即可,然后动态改变第一个属性值,即可实现音乐播放进度条,其中第一个属性值是圆周长*idx / 100
。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
<span class="hljs-function"><span class="hljs-keyword">function</span></span> <span class="hljs-function"><span class="hljs-title">draw</span></span><span class="hljs-function"><span class="hljs-params">(</span></span><span class="hljs-function"><span class="hljs-params">)</span> </span>{ <span class="hljs-keyword">if</span> (idx < <span class="hljs-number">101</span>) { inside.attr(<span class="hljs-string">"stroke-dasharray"</span>, <span class="hljs-string">""</span> + circleLength * idx / <span class="hljs-number">100</span> + <span class="hljs-string">",10000"</span>); idx++; } <span class="hljs-keyword">else</span> { <span class="hljs-keyword">if</span> (inter) clearInterval(inter); inter = <span class="hljs-literal">null</span>; idx = <span class="hljs-number">1</span>; btnEnd.show(); btnStop.hide(); btnStart.hide(); } } |
值得注意的是,如果是多首歌曲同时播放,需要考虑歌曲的最大时长来设置进度条速度,代码片段如下:
1 2 3 4 5 6 7 8 |
allSound.forEach(<span class="hljs-function"><span class="hljs-keyword">function</span></span> (sound, <span class="hljs-built_in">index</span>) { durations.push(sound.duration); <span class="hljs-keyword">if</span> (sound.duration > duration) { duration = sound.duration; i = <span class="hljs-built_in">index</span>; } }); space = Math.<span class="hljs-built_in">max</span>(...durations) / <span class="hljs-number">100</span>; |
总结
1. soundjs实现的音频播放
调用方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
<span class="hljs-keyword">var</span> mSrc = [{ <span class="hljs-attr">id</span>:<span class="hljs-string">"firstMp3"</span>,<span class="hljs-attr">src</span>:<span class="hljs-string">"//game.gtimg.cn/images/hx/act/a20180305song/assets/select_1_1.mp3"</span>},{<span class="hljs-attr">id</span>:<span class="hljs-string">"secondMp3"</span>,<span class="hljs-attr">src</span>:<span class="hljs-string">"//game.gtimg.cn/images/hx/act/a20180305song/assets/select_0_1.mp3"</span> }]; playSound.init(mSrc);<span class="hljs-comment">//初始化</span> $(<span class="hljs-string">'.pop_disk'</span>).on(<span class="hljs-string">"click"</span>, <span class="hljs-function"><span class="hljs-keyword">function</span></span><span class="hljs-function"> (</span><span class="hljs-function">) </span>{ <span class="hljs-keyword">if</span> (playSound.sound.longest.playState !== <span class="hljs-string">'playFinished'</span> && playSound.sound.longest.paused == <span class="hljs-literal">false</span>) { playSound.stopDraw(); playSound.sound.pauseAll(); btnStart.hide(); btnStop.show(); } <span class="hljs-keyword">else</span> { playSound.startDraw(); playSound.sound.playAll(); btnStart.show(); btnStop.hide(); } btnEnd.hide(); }) |
2. Web-Audio-API实现的音频播放
http://hx.qq.com/act/a20180305song/test.html
(AudioContext)
Web-Audio-API调用方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
<span class="hljs-keyword">var</span> index = <span class="hljs-number">0</span>,idValue = <span class="hljs-number">0</span>, duration = <span class="hljs-number">0</span>, durations = []; AwesomeSound.load([ { id: <span class="hljs-string">'1mp3'</span>, url: <span class="hljs-string">'//game.gtimg.cn/images/hx/act/a20180305song/assets/select_1_1.mp3'</span> }, { id: <span class="hljs-string">'2mp3'</span>, url: <span class="hljs-string">'//game.gtimg.cn/images/hx/act/a20180305song/assets/select_0_1.mp3'</span> } ]).then(<span class="hljs-function"><span class="hljs-keyword">function</span></span><span class="hljs-function"> <span class="hljs-params">(</span></span><span class="hljs-function"><span class="hljs-params">sounds</span></span><span class="hljs-function"><span class="hljs-params">)</span> </span>{ sounds.<span class="hljs-keyword">forEach</span>(<span class="hljs-function"><span class="hljs-keyword">function</span></span><span class="hljs-function"> <span class="hljs-params">(</span></span><span class="hljs-function"><span class="hljs-params">sound, i</span></span><span class="hljs-function"><span class="hljs-params">)</span> </span>{ durations.push(sound.buffer.duration); <span class="hljs-keyword">if</span> (sound.buffer.duration > duration) { duration = sound.buffer.duration; index = i; idValue = sound.id; } sounds[index].on(<span class="hljs-string">'ended'</span>, <span class="hljs-function"><span class="hljs-keyword">function</span></span><span class="hljs-function"> <span class="hljs-params">(</span></span><span class="hljs-function"><span class="hljs-params">)</span> </span>{ sound.setEnd(<span class="hljs-number">1</span>); playmp3.html(<span class="hljs-string">"点击再次播放"</span>); }) sound.play(); }); }); playmp3.click(<span class="hljs-function"><span class="hljs-keyword">function</span></span><span class="hljs-function"> <span class="hljs-params">(</span></span><span class="hljs-function"><span class="hljs-params">)</span> </span>{ <span class="hljs-keyword">var</span> playState = AwesomeSound.getState(idValue); <span class="hljs-keyword">if</span> (playState === <span class="hljs-string">'running'</span>) { playmp3.html(<span class="hljs-string">"暂停播放"</span>); } <span class="hljs-keyword">else</span> { playmp3.html(<span class="hljs-string">"正在播放"</span>); } AwesomeSound.playALL(); }); |