基于Howler.js 音频播放器开发 多段音频连续+背景音效合成播放

3,040 阅读5分钟

#一、写在前面的背景

先说下背景,最近项目设计了一个需求,需要实现多段音频连续+背景音效合成播放的需求。最开始考虑的方案是,由后台JAVA进行音频合成,进行了综合考虑,后台进行进行音频全成需要先从云存储上下载音频再进行处理,处理完后再上传云存储服务器。 此方案最终未被采用,原因有几点:

1.服务器流量成本太大,在大用户量的情况下,文件进行下载与上传会和产生大量的流量费用。

2.服务器开销增加,合成需要占用服务器的大量内存,在并发情况下开销会相当大。

3.用户体验感,在大并发情况下,服务需要进行排队等情况下,合成音频需要一定的时间。
  用户不能及时得到结果。

综合这些原因,此方案不是很合适。

实现思路: 既然后台合成需要这么大的成本,是不是考虑不合成,由前端进行处理,同时创建两个播器进行关联播放,一个播内容,一个播音效。播内容的播放器实现连续缝隙播放。理论上是可行,接下来就是进行技术选型和具体实现了。

#二技术选型-Howler.js 特点

开源的音频播放非常多,经过对比研究,howler.js 非常适合。

howler.js 是一个新的 JavaScript 库用于处理 Web 的音频,该库最初是为一个 HTML5 游戏引擎所开发,但也可用于其他的 Web 项目。 非常强大的一款声音引擎。 功能强大,性能不错。同时支持很多声音格式以兼容各种浏览器。MP3, MPEG, OPUS, OGG, OGA, WAV, AAC, CAF, M4A, MP4, WEBA, WEBM, DOLBY, FLAC.

然后最重要的一点就是循环点处理,我们自己手动写的声音循环在循环一次和下一次的衔接往往有些延迟,造成不连贯,Howler.js对于循环点的处理性能不错,延迟比较小,非常适合多段音频连续播放的需求,同时控制多个声音播放非常方便,效果也不错。

另外其他的特点

支持3D游戏
自动缓存
支持淡入淡出效果
轻量
纯JS
无第三方依赖
模块化

#三、实现过程

先来个目标效果图:

要点及难点:

1.多段音频总时长的获取。
2.多段音频连续播,一个进度条控制。
3.进度条的拖动控制,换算成第几段什么时间点。
4.背景音效的联动控制。

1.初始化播放器,为每段音频初始化一个播放器对象,并同时预加载出音频的真实时长,所有时长相加为进度条总时长,为了保证时长准确性,采用依次初始化; 采用VUE 组件中部分代码如下:

/**
 * 初始化播放列表
 * 根据url数组,依次初始化播放器,并预加载出时长。为了保证时长准确性,采用依次初始化;
 */
initPlayerList(urlList){
    if (typeof urlList == 'string'){
        urlList = [urlList]
    }
    if (urlList.length == 0 )return
    this.initSound(urlList,0);
},
/**
 * 初始化播放器
 * 每段音频初始化一个播放器,并放入播放列表对象中。
 */
initSound(urlList,_index){
    let _urlLen = urlList.length;
    let _url = urlList[_index];
    if (_urlLen > 0){
        let sound = new Howl({
            src: [_url],
            html5:true,
            volume: 1
        });
        //加载事件监听
        sound.once('load', ()=>{
            let _item = this.playerList[_index];
            _item.duration = sound.duration();
            //保存开始时间点,为前面所有音频的总时长,为了后面的进度条时间计算
            _item.startSeek = tempTotalTime;
            this.playerList.splice(_index,1,_item);
            //总时长=时长相加
            tempTotalTime += sound.duration();
            //最后一个,不是最后一个就继续初始化下一个
            if (_index == _urlLen - 1){
                this.totalTime = tempTotalTime;
            }else {
                this.initSound(urlList,_index+1)
            }
            
        });
        sound.on('play',()=>{
            this.audioStatus = 'play';
            requestAnimationFrame(this.step.bind(this));
        });
        sound.on('seek',()=>{
			if (this.sound.playing()) {
                requestAnimationFrame(this.step.bind(this));
			}
		});
		//播放完事件监听
        sound.on('end', ()=>{
            //最后一个停止播放,否则播放下一个
            if (this.playIndex == _urlLen - 1){
                this.stopAudio();
            }else {
                console.log("播放下一个")
                this.playIndex += 1;
                this.sound = this.playerList[this.playIndex].howl;
                this.playAudio();
            }
        });
        //放入播放列表中
        this.playerList.push({
            howl:sound
        });
        this.playIndex = 0;
        this.sound = this.playerList[this.playIndex].howl;
    }
},
/**
 * 初始化音效播放器
 */
initEffectSound(url){
    this.effectSound = new Howl({
        src: [url],
        html5:true,
        loop: true,
        volume: this.bgVolume
    });
    this.effectSound.once('load', ()=>{
        // let _effectTime = this.effectSound.duration();
    });
},

2.多段音频连续播,一个进度条控制。进度条播放时间点为前面所有几段音频的总时长加当前播放器的播放时长。 部分代码如下:

/**
 * 进度条滚动
 */
step(){
    //不在拖动状态时,进度条进行滚动
    if (this.isDrag == false){
        let seek = this.sound.seek() || 0;
        //进度条时间点为 开始时间+当时播放器时间。
        if (this.playIndex > 0){
            seek += this.playerList[this.playIndex].startSeek;
        }
        this.curretnTime = seek;
        let currentPress = (((seek / this.totalTime) * 100) || 0);
        this.currentPos = Math.round(currentPress);
    }
    if (this.sound.playing()) {
        requestAnimationFrame(this.step.bind(this));
    }
},

3.进度条的拖动控制,换算成第几段什么时间点。 循环计算 当前拖动点位于播放列表第几个,什么点。 此处拖地没有对音效播放器进行相应处理,有需要的朋友可以自己添加。

/**
 * 进度条拖动改变事件
 */
sliderChange(value){
    let changeTime = this.totalTime * value / 100 ;
    //循环计算 当前拖动点位于播放列表第几个,什么点。
    for (let i=this.playerList.length-1;i>=0;i--){
        let item  = this.playerList[i];
        //此时加0.5秒,防止当时播放时间太短。
        if (changeTime + 0.5 > item.startSeek){
            let seek = changeTime - item.startSeek;
            seek = seek > 0 ? seek : 0;
            this.skip(i,seek);
            break;
        }
    }
},
/**
 * 跳跃播放
 */
skip(index,seek){
    console.log(index+"^*****"+seek)
    if (this.playIndex !== index){
        this.sound.stop();
    }
    this.sound = this.playerList[index].howl;
    this.sound.seek(seek);
    if (this.playIndex !== index && this.audioStatus == 'play'){
        this.sound.play();
    }
    this.playIndex = index;
    
},

4.背景音效的联动控制。 背景音效加做了播放、暂停、停止等关联处理。

//音频播放
playAudio(){
    if (this.sound){
        this.sound.play();
    }
    if (this.effectSound){
        this.effectSound.play();
    }
},
//音频暂停播放
pauseAudio(){
    if (this.sound){
        this.sound.pause();
        this.audioStatus = 'pause';
    }
    if (this.effectSound){
        this.effectSound.pause();
    }
},
//音频停止播放
stopAudio(){
    if (this.sound){
        this.sound.stop();
        this.audioStatus = 'stop';
        this.playIndex = 0;
        this.currentPos = 0;
    }
    if (this.effectSound){
        this.effectSound.stop();
    }
}

需求基本都实现了,效果还可以,还有许多可优化的地方。有不正确或建议的请大家指正。文案功底有限,希望对大家有帮助。

夜已深,正是码农出没时。。。。