https://docs.cocos.com/creator/3.3/manual/zh/asset/audio.html
https://docs.cocos.com/creator/3.3/manual/zh/audio-system/overview.html
https://docs.cocos.com/creator/3.3/manual/zh/audio-system/audiosource.html
声音系统的接口主要面向两类需求,一类是长度较长,循环持续播放的 “音乐”,一类是长度较短,一次性播放的 “音效”。所有声音资源都会在编辑器内导入成 AudioClip 资源。
一、声音资源
1.支持的声音资源的格式
目前引擎的音频系统已经能够支持 web 原生支持的格式:
- .ogg
- .mp3
- .wav
- .mp4
- .m4a
2.关于 Web 平台声音资源的加载模式
Web 平台上的声音资源比较特别,因为 Web 标准支持以两种不同的方式加载声音资源,分别是:
- Web Audio: 提供相对更加现代化的声音控制接口,在引擎内是以一个 audio buffer 的形式缓存的。这种方式的优点是兼容性好,问题比较少。
- DOM Audio: 通过生成一个标准的 audio 元素来播放声音资源,缓存的就是这个 audio 元素。使用标准的 audio 元素播放声音资源的时候,在某些浏览器上可能会遇到一些兼容性问题。比如:iOS 上的浏览器不支持调整音量大小,所有 volume 相关属性将不会有效。
目前引擎默认会尝试以 Web Audio 的方式加载声音资源。如果检测到浏览器不支持加载 Web Audio,则会回滚到 DOM Audio 的方式。
如果项目需要强制使用 DOM Audio 的声音资源,请使用以下方式动态加载声音资源:
assetManager.loadRemote('http://example.com/background.mp3', {
audioLoadMode: AudioClip.AudioType.DOM_AUDIO,
}, callback);
二、AudioSource
- Clip 用来播放的声音资源对象
- Loop 是否循环播放
- PlayOnAwake 是否在组件激活后自动播放声音
- Volume 音量大小,范围在 0~1 之间
1.AudioSource 的播放
// AudioController.ts
@ccclass("AudioController")
export class AudioController extends Component {
@property(AudioSource)
public audioSource: AudioSource = null!;
play () {
this.audioSource.play();
}
pause () {
this.audioSource.pause();
}
}
2.音效播放
相较于长的音乐播放,音效播放具有以下特点:
- 播放时间短
- 同时播放的数量多
针对这样的播放需求,AudioSource 组件提供了 playOneShot 接口来播放音效。具体代码实现如下:
// AudioController.ts
@ccclass("AudioController")
export class AudioController extends Component {
@property(AudioClip)
public clip: AudioClip = null!;
@property(AudioSource)
public audioSource: AudioSource = null!;
playOneShot () {
this.audioSource.playOneShot(this.clip, 1);
}
}
注意:playOneShot 是一次性播放操作,播放后的声音没法暂停或停止播放,也没法监听播放结束的事件回调。
3.在 v3.3.0 支持了音频播放事件监听接口
@ccclass('AudioDemo')
export class AudioDemo extends Component {
@property(AudioSource)
audioSource: AudioSource = null!;
onEnable () {
// Register the started event callback
this.audioSource.node.on(AudioSource.EventType.STARTED, this.onAudioStarted, this);
// Register the ended event callback
this.audioSource.node.on(AudioSource.EventType.ENDED, this.onAudioEnded, this);
}
onDisable () {
this.audioSource.node.off(AudioSource.EventType.STARTED, this.onAudioStarted, this);
this.audioSource.node.off(AudioSource.EventType.ENDED, this.onAudioEnded, this);
}
onAudioStarted () {
// TODO...
}
onAudioEnded () {
// TODO...
}
}
4.Web 平台的播放限制
目前 Web 平台的声音播放需要遵守最新的 Audio Play Police,即使 AudioSource 组件设置了 playOnAwake
也会在第一次接收到用户输入时才开始播放。范例如下:
// AudioController.ts
@ccclass("AudioController")
export class AudioController extends Component {
@property(AudioSource)
public audioSource: AudioSource = null!;
start () {
let btnNode = find('BUTTON_NODE_NAME');
btnNode!.on(Node.EventType.TOUCH_START, this.playAudio, this);
}
playAudio () {
this.audioSource.play();
}
}
三、官方实例
https://github.com/cocos-creator/test-cases-3d
1.模拟弹琴那个
export class AudioController extends Component {
@property({type: [AudioClip]})
public clips: AudioClip[] = [];
@property({type: AudioSource})
public audioSource: AudioSource = null!;
@property({type: Label})
public nameLabel: Label = null!;
start () {
// Your initialization goes here.
}
// update (deltaTime: number) {
// // Your update function goes here.
// }
onButtonClicked(event: any, index: number) {
let clip: AudioClip = this.clips[index];
this.nameLabel.string = clip.name;
this.audioSource.playOneShot(clip);
}
onVolumeSliderChanged(eventTarget: Slider) {
this.audioSource.volume = eventTarget.progress;
}
}
可以理解为一个声音文件就对应一个AudioClip,点击时,通过索引切换不同的声音文件,交给audioSource去播放。
2.audioControl示例
预期:
1. 进入场景后 audioSource1 自动播放 (Web 端可能需要触摸屏幕)
2. audioSource1 和 audioSource2 独立播放,不互相影响
3. pause 之后点击 play 是继续播放
4. play 之后再次点击 play 是重新播放
5. stop 之后点击 play 是重新播放
6. playOneShot 可以同时播放多个音效播放
7. 进度条可以正确展示播放进度和设置进度,暂停状态下设置进度不会变成播放状态
8. 开始播放 和 播放结束时,进度条右侧会有相应的状态提示 (循环播放时不会触发 ENDED)
9. 如果没有循环播放,播放结束后,当前播放时间清零
10. playOneShot 会以当前的 audioSource 音量播放,播放后音量不可再调节
@ccclass('AudioControl')
export class AudioControl extends Component {
@property(AudioClip)
clip: AudioClip = null!;
@property(AudioSource)
source1: AudioSource = null!;
@property(Label)
currentTimeLabel1: Label = null!;
@property(Label)
durationLabel1: Label = null!;
@property(Slider)
progressSlider1: Slider = null!;
@property(Slider)
volumeSlider1: Slider = null!;
@property(Label)
eventLabel1: Label = null!;
@property(Toggle)
toggle1: Toggle = null!
@property(AudioSource)
source2: AudioSource = null!;
@property(Label)
currentTimeLabel2: Label = null!;
@property(Label)
durationLabel2: Label = null!;
@property(Slider)
progressSlider2: Slider = null!;
@property(Slider)
volumeSlider2: Slider = null!;
@property(Label)
eventLabel2: Label = null!;
@property(Toggle)
toggle2: Toggle = null!
onEnable () {
console.log('AudioSource1 loadMode: ', this.source1.clip?.loadMode);
console.log('AudioSource2 loadMode: ', this.source2.clip?.loadMode);
this.source1.loop = this.toggle1.isChecked;
this.source2.loop = this.toggle2.isChecked;
this.volumeSlider1.progress = this.source1.volume;
this.volumeSlider2.progress = this.source2.volume;
this.progressSlider1.node.on('slide', this.onSlide, this);
this.progressSlider2.node.on('slide', this.onSlide, this);
this.volumeSlider1.node.on('slide', this.onVolume, this);
this.volumeSlider2.node.on('slide', this.onVolume, this);
this.toggle1.node.on(Toggle.EventType.TOGGLE, this.onToggle, this);
this.toggle2.node.on(Toggle.EventType.TOGGLE, this.onToggle, this);
this.source1.node.on(AudioSource.EventType.STARTED, this.onStarted, this);
this.source1.node.on(AudioSource.EventType.ENDED, this.onEnded, this);
this.source2.node.on(AudioSource.EventType.STARTED, this.onStarted, this);
this.source2.node.on(AudioSource.EventType.ENDED, this.onEnded, this);
}
onDisable () {
this.progressSlider1.node.off('slide', this.onSlide, this);
this.progressSlider2.node.off('slide', this.onSlide, this);
this.volumeSlider1.node.off('slide', this.onVolume, this);
this.volumeSlider2.node.off('slide', this.onVolume, this);
this.toggle1.node.off(Toggle.EventType.TOGGLE, this.onToggle, this);
this.toggle2.node.off(Toggle.EventType.TOGGLE, this.onToggle, this);
this.source1.node.off(AudioSource.EventType.STARTED, this.onStarted, this);
this.source1.node.off(AudioSource.EventType.ENDED, this.onEnded, this);
this.source2.node.off(AudioSource.EventType.STARTED, this.onStarted, this);
this.source2.node.off(AudioSource.EventType.ENDED, this.onEnded, this);
}
playOneShot1 () {
this.source1.playOneShot(this.clip);
}
playOneShot2 () {
this.source2.playOneShot(this.clip);
}
update (dt: number) {
this.updateSlider(this.source1, this.progressSlider1, this.currentTimeLabel1, this.durationLabel1);
this.updateSlider(this.source2, this.progressSlider2, this.currentTimeLabel2, this.durationLabel2);
}
updateSlider (source: AudioSource, slider: Slider, currentTimeLabel: Label, durationLabel: Label) {
let currentTime = Number.parseFloat(source.currentTime.toFixed(2));
let duration = Number.parseFloat(source.duration.toFixed(2));
currentTimeLabel.string = currentTime.toString();
durationLabel.string = duration.toString();
slider.progress = currentTime / duration;
}
onSlide (slider: Slider) {
let source = slider === this.progressSlider1 ? this.source1 : this.source2;
let currentTime = slider.progress * source.duration;
source.currentTime = currentTime;
}
onVolume (slider: Slider) {
let source = slider === this.volumeSlider1 ? this.source1 : this.source2;
source.volume = slider.progress;
}
onToggle (toggle: Toggle) {
let source = toggle === this.toggle1 ? this.source1 : this.source2;
source.loop = toggle.isChecked;
}
onStarted (audioSource: AudioSource) {
let eventLabel = audioSource === this.source1 ? this.eventLabel1 : this.eventLabel2;
this.showEventLabel(eventLabel, 'STARTED', 1);
}
onEnded (audioSource: AudioSource) {
let eventLabel = audioSource === this.source1 ? this.eventLabel1 : this.eventLabel2;
this.showEventLabel(eventLabel, 'ENDED', 1);
}
showEventLabel (eventLabel: Label, text: string, timeInSeconds: number) {
eventLabel.string = text;
eventLabel.node.active = true;
this.scheduleOnce(() => {
eventLabel.node.active = false;
}, timeInSeconds);
}
}
- playOnAwake 是否启用自动播放。
- loop 是否循环播放音频
- currentTime 以秒为单位设置当前播放时间。以秒为单位获取当前播放时间。
- duration 获取以秒为单位的音频总时长。
play 就行了
play 的行为是,暂停状态,调用 play 就是 resume
如果 正在播放 或者 停止状态,play 就是重新播放
四、
1. creater3.1版本 audioEngine这个api改成什么了
A:取消了,用audiosource
Q:请问这些接口在3.1的是啥
cc.audioEngine.pauseAll();
cc.audioEngine.stopAll();
A:定义一个常驻节点~
2.audioEngine
已经被取消了 目前是推荐用声音组件变成常驻节点来播放声音。详细的案例可以查看官方快上车案例~
如果设置成常驻节点需要注意一个问题: 常驻节点在切场景时会暂停音乐,需要在 onEnable 继续播放
(之后需要在引擎侧解决这个问题)
@ccclass('GameRoot')
export class GameRoot extends Component {
@property(AudioSource)
private _audioSource: AudioSource = null!;
onLoad () {
const audioSource = this.getComponent(AudioSource)!;
assert(audioSource);
this._audioSource = audioSource;
game.addPersistRootNode(this.node);
// init AudioManager
AudioManager.init(audioSource);
}
}
import { assert, assetManager, AudioClip, AudioSource, log } from "cc";
export class AudioManager {
private static _audioSource?: AudioSource;
private static _cachedAudioClipMap: Record<string, AudioClip> = {};
// init AudioManager in GameRoot component.
public static init (audioSource: AudioSource) {
log('Init AudioManager !');
AudioManager._audioSource = audioSource;
}
public static playMusic () {
const audioSource = AudioManager._audioSource!;
assert(audioSource, 'AudioManager not inited!');
audioSource.play();
}
public static playSound(name: string) {
const audioSource = AudioManager._audioSource!;
assert(audioSource, 'AudioManager not inited!');
const path = `audio/sound/${name}`;
let cachedAudioClip = AudioManager._cachedAudioClipMap[path];
if (cachedAudioClip) {
audioSource.playOneShot(cachedAudioClip, 1);
} else {
assetManager.resources?.load(path, AudioClip, (err, clip) => {
if (err) {
console.warn(err);
return;
}
AudioManager._cachedAudioClipMap[path] = clip;
audioSource.playOneShot(clip, 1);
});
}
}
}
3. 请问Creator 3D playOneShot Bug
playOneShot 函数的设计就是 “播完就忘” 风格地快速播放大量简单的音效,unity 也有一样的设计 2,如果需要精确控制播放暂停,正常用 play 就好。
除了 playOneShot 接口,audioSource 本身是不支持音效叠加的,一个 audioSource 就是一个音源
如果游戏有需求要做到音效叠加又要随时的控制暂停播放
目前看来这个需求是比较少的,音效一般都是短音频,对控制的需求比较少
如果需要做到这样,就需要多个 audioSource 才行了
Q:常驻节点添加了AudioSource会在切换场景时导致正在播放的音频被暂停或停止,这个问题有处理办法么
A:这块我们之前也收到类似的反馈了,我们内部反馈下,看要不要从常驻节点的组件生命周期里去解决下这个问题,会在后续的版本里处理下这个问题。暂时可以先在组件 onEnable 的时候强制 AudioSource 继续播放
4.Cocos Creator v3.1 测试帖(已更新到最新版本)
Q:其实就是现在要播放声音必须要借助AudioSourceComponent嘛。。如果我想切换scene的时候声音不中断,那么这个AudioSourceComponent就必须挂载在持久化node中,如果我远程加载了一个mp3,也要先创建一个AudioSourceComponent才可以播放。你们不觉得对简单项目来说,远没有以前的2.x的利用audioEngine来的方便吗??? 从3.0刚出就开始说到现在3.1了
A:对,现在只能通过 AudioSourceComponent,从长远来看,之后支持 3D Audio ,这个也是必须的,AudioSourceComponent 可以获取到 node 的位置信息,这个是 audioEngine 不能满足的
其实 audioEngine 无非就是做了一些管理相关的工作,我们发现大部分项目其实会基于 audioEngine 又封装一次自己的 audioManager,说明 audioEngine 其实已经不能满足大部分项目的需求了,我们还是希望 audio 这块能提供给开发者稳定的基础播放接口,把更多业务相关的 audio 管理工作移交给开发者自己
5.Cocos Creator 3.0 正式版震撼来袭!
2.x里的audioEngine到底是彻底放弃了还是怎么样?现在想要动态播放音乐应该怎么办?
不同意。audioEngine仅在2D游戏且能保证严格管理audioEngine api使用的情况下好用。大型项目下每个人随意调用audioEngine,可以想象有多混乱。3D项目涉及音效距离衰减,audioEngine根本没法做。这是3D必须的改进,也没必要额外留一套2D专供API。Audiosource组件就是unity一样的,unity的设计总不会迷惑吧? 它也没有什么多余的全局API。要是有个“音频管理器”,那么用audioEngine还是Audiosource是没区别的,跨场景也没问题。
3D音效是3D引擎必须做的,早晚会补。两套API互存很久了,无论是规范问题、资源管理问题还是适用性问题,都该统一了。就像之前cc.vec2、cc.p各种乱七八糟的,早砍早痛快。AudioSource能满足2D/3D,就没必要留一个不符合资源管理、不符合组件思想的API了吧。比如position之类属性, 早晚也会默认成vec3,两者都能满足。至于怎么动态播放,真的需要看文档?
图是2.4的,加载clip跟你动态加载spriteframe不是一样吗
常驻节点,我用过,也知道,但我为了播放一个bgm,在各个场景之间连贯的bgm,就要去控制一个长柱节点么·······
目前引擎内部音频系统不完善,音频系统是一个复杂且专业的系统,目前我们在能力上确实存在短板,所以暂时推荐对音频系统要求比较高的开发者使用第三方库,Criware, Fmod, Wwise 都是很好的选择。