TGideas-腾讯互动娱乐创意设计团队

SoundJs | Web-Audio-API实现多音频播放

因为HTML5 Audio不支持同时播放多首歌,今天我们主要介绍一下soundjs和Web-Audio-API实现多音频播放的方法,文末还总结了两种实现方式的demo,大家可以依据实际项目情况添加接口。

| 导语 再回首-“凤凰传奇邀你唱H5”

前言

需求背景:用户选择玲花或曾毅,参与合唱,合唱成功后上传用户录音数据,封装待播放的音频信息(这些部分都是由林雨哥[lennylin]完成的),到落版页请求歌曲进行播放。其中ios微信侧录制音频会屏蔽掉媒体本身的声音,所以ios播放用户合唱歌曲需要同时播放用户录制音频和背景音乐。

  (凤凰传奇邀你唱H5)  

因为HTML5 Audio不支持同时播放多首歌,今天我们主要介绍一下soundjs和Web-Audio-API实现多音频播放的方法,文末还总结了两种实现方式的demo,大家可以依据实际项目情况添加接口。                                           

目录

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); //针对每个注册音频加载完后的处理事件

 createjs.Sound.alternateExtensions = ["mp3"];
 createjs.Sound.on("fileload", this.loadHandler, this);
 createjs.Sound.registerSound("path/to/mySound.ogg", "sound");
 function loadHandler(event) {
     var instance = createjs.Sound.play("sound");  // 可使用ID、完整的源路径或event.src。
     instance.on("complete", this.handleComplete, this);
     instance.volume = 0.5;
 }

多音频播放:

sounds.forEach(function (sound) {
      createjs.Sound.alternateExtensions = ["mp3"];
createjs.Sound.registerSound(sound.src, sound.id); var promise = new Promise(function (resolve, reject) { createjs.Sound.on("fileload", function (event) { if (sound.id == event.id) { resolve(createjs.Sound.play(event.id)) } }) }) promises.push(promise) })
return Promise.all(promises)

值得注意的是,当sound.src是一个会重定向的音频地址,需要先获得重定向后的真实地址,再注册音频。回顾我们的需求,落版页听录制歌曲的时候,我们需要拿用户openid去请求服务器端的php文件,服务器会重定向到一个具体的mp3地址,代码片段如下:

createjs.Sound.registerSound('xxx.php?openid=xxx', 'userMp3'); ×
createjs.Sound.registerSound(request.responseURL, 'userMp3');  √

先看一下readyState几种状态

0

UNSENT (未打开)

open()方法还未被调用.

1

OPENED  (未发送)

open()方法已被调用.

2

HEADERS_RECEIVED (已获取响应头)

send()方法已经被调用, 响应头和响应状态已经返回.

3

LOADING (正在下载响应体)

响应体下载中

4

DONE (请求完成)

整个请求过程已经完毕.

当状态为HEADERS_RECEIVED时可以拿到多次重定向后的最终 URL 并终止请求
if (this.readyState == this.HEADERS_RECEIVED) {         
     createjs.Sound.registerSound(request.responseURL, sound.id);
     this.abort();                        
}

注意:如果在初始化音频的时候再去发request请求,会导致微信侧播放异常,所以提前获取音频真实地址,在回调里面再执行音频初始化。

当点击恢复音频播放时需要考虑是否所有音频都已经播放完成,如果是则调用play(),否则仅仅设置paused的状态为false。因为这个时候短音频可能已经播放完成,长音频未播放完成并处于暂停状态,如果不判断直接调用play(),短音频会重新开始播放。

playAll(){
        let allFinished = false;
        this.sounds.forEach(function (sound) {
            allFinished = sound.playState !== 'playFinished' ? false : true;
        });
        this.sounds.forEach(function(sound){
            if(allFinished){
                sound.play();
            }else{
                sound.paused = false;
            }
        });
    }

2. Web-Audio-API多音频播放

仔细分析soundjs源码,你会发现它对音频的操作其实是基于强大的Web-Audio-API,而且大多数浏览器都支持Web-Audio-API,我们完全可以用它实现一个简单的音频播放器,不再依赖任何接口文件。
 
2.1 常用的接口:
AudioContext

 音频上下文

控制其包含节点的创建、处理和解码,使用其他接口之前需创建一个音频上下文

AudioNode  音频节点
AudioBuffer

音频数据对象

可以通过AudioContext.createBuffer 来创建或者

通过 AudioContext.decodeAudioData成功解码音轨后获取.

AudioBufferSourceNode

含有音频源的音频节点,它是用AudioBuffer对象来播放音频数据

可以通过createBufferSource() 方法创建

方法:AudioBufferSourceNode.start()   AudioBufferSourceNode.stop()

事件:ended

ended

音频播放停止时触发

2.2 创建单音频播放
function playSound(buffer){    
    var context = new (window.AudioContext || window.webkitAudioContext)(),
    source = context.createBufferSource();
    source.buffer = buffer;//  告诉音频源 播放哪一段音频
    source.connect(context.destination);// 连接到输出源
    source.start(0);//开始播放
}

定义音频上下文需注意:webkit内核的浏览器需要带webkit前缀(webkitAudioContext)

2.3 多音频播
opts = [{ id: '1mp3', url: 'mp3/select_1_1.mp3' },{ id: '2mp3', url: 'mp3/select_0_1.mp3' } ]
opts.forEach(opt => { var request = new XMLHttpRequest(); var context = new (window.AudioContext || window.webkitAudioContext)(); self.sounds[opt.id] = new Sound(opt.id, opt.url, context); request.open('GET', opt.url, true); request.responseType = 'arraybuffer'; var p = new Promise(function (resolve, reject) { //下面就是对音频文件的异步解析 request.onload = function () { context.decodeAudioData(request.response, function (buffer) { self.sounds[opt.id].setBuffer(buffer); self.sounds[opt.id].setEnd(0); resolve(self.sounds[opt.id]); }, function (err) { reject(err); }); }; }); request.send(); promises.push(p); });

 一个 AudioBufferSourceNode 只能被播放一次,每次调用 start() 之后,如果还想再播放一遍同样的声音,那么就需要再创建一个 AudioBufferSourceNode,如果想要多次播放声音,需要保留音频播放上下文和音频数据,这里可以定义一个class,初始化每个音频的id,url地址,buffer数据以及音频上下文等等。

class Sound {
//音频id,音频url,音频播放上下文 constructor(id, url, context) { this.id = id; this.url = url; this.context = context; this.endState = 0; }
//设置音频数据 setBuffer(buf) { this.buffer = buf; }
//定义音频播放完成事件 setEnd(end) { this.endState = end; } on(event, callback) { this.events[event] = callback; }
//音频播放事件 play() { }
//音频暂停事件 pause() { } }

 2.4 音频暂停、播放

if ( context.state === 'running') {               
          context.suspend().then(function () {
                 console.log('Resume context');
           });           
 } else if (context.state === 'suspended') {           
         context.resume().then(function () {
                console.log('Suspend context');
          });            
 }

 2.5 音频播放结束

绑定音频播放结束事件

sound.on('ended', function () {
       sound.setEnd(1);
 })

综上所述,当对音频点击操作时,需要先判断音频播放状态,如果正在播放则直接暂停;如果已经暂停并且没有播放结束,那么执行恢复操作resume();如果音频已经播放完成,则需要再创建一个 AudioBufferSourceNode,重新绑定context、buffer等信息

if(this.context.state === 'running' && this.endState === 0){
      this.context.suspend();
}
else if (this.context.state === 'suspended' && this.endState === 0) {
      this.context.resume();
}
else{
      this.source = this.context.createBufferSource();
      if (this.events.ended) {
          this.source.onended = this.events.ended;
      }
      this.source.buffer = this.buffer;//  告诉音频源 播放哪一段音频
      this.source.connect(this.context.destination);// 连接到输出源
      this.source.start(0);//开始播放
      this.endState = 0;
}

 3. 音频播放进度条实现

svg的stroke-dasharray属性:stroke-dasharray="0,10000",其中第一个属性是虚线宽度,第二个是虚线之间间隔,保证第二个属性大于圆形周长即可,然后动态改变第一个属性值,即可实现音乐播放进度条,其中第一个属性值是圆周长*idx / 100
function draw() {
        if (idx < 101) {
            inside.attr("stroke-dasharray", "" + circleLength * idx / 100 + ",10000");
            idx++;
        } else {
            if (inter)
            clearInterval(inter);
inter = null; idx = 1; btnEnd.show(); btnStop.hide(); btnStart.hide(); } }

值得注意的是,如果是多首歌曲同时播放,需要考虑歌曲的最大时长来设置进度条速度,代码片段如下:

allSound.forEach(function (sound, index) {
       durations.push(sound.duration);
       if (sound.duration > duration) {
              duration = sound.duration;
              i = index;
       }
 });
space = Math.max(...durations) / 100;

总结

1. soundjs实现的音频播放

调用方法:

var mSrc = [{
    id:"firstMp3",src:"//game.gtimg.cn/images/hx/act/a20180305song/assets/select_1_1.mp3"},{id:"secondMp3",src:"//game.gtimg.cn/images/hx/act/a20180305song/assets/select_0_1.mp3"
}];
playSound.init(mSrc);//初始化
$('.pop_disk').on("click", function () {
    if (playSound.sound.longest.playState !== 'playFinished' && playSound.sound.longest.paused == false) {
        playSound.stopDraw();
        playSound.sound.pauseAll();
        btnStart.hide();
        btnStop.show();
    } else {
        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调用方法:

var index = 0,idValue = 0, duration = 0, durations = [];
AwesomeSound.load([
    { id: '1mp3', url: '//game.gtimg.cn/images/hx/act/a20180305song/assets/select_1_1.mp3' },
    { id: '2mp3', url: '//game.gtimg.cn/images/hx/act/a20180305song/assets/select_0_1.mp3' }
]).then(function (sounds) {
    sounds.forEach(function (sound, i) {
        durations.push(sound.buffer.duration);
        if (sound.buffer.duration > duration) {
            duration = sound.buffer.duration;
            index = i;
            idValue = sound.id;
        }
        sounds[index].on('ended', function () {
            sound.setEnd(1);
            playmp3.html("点击再次播放");
        })
        sound.play();
    });
});
playmp3.click(function () {
    var playState = AwesomeSound.getState(idValue);            
    if (playState === 'running') {
        playmp3.html("暂停播放");
    }
    else {
        playmp3.html("正在播放");
    }
    AwesomeSound.playALL();
});

 

求经验
关闭

分享

返回顶部