Cocos 3.x 声音

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
image.png
  • 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.模拟弹琴那个
image.png
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去播放。


image.png
2.audioControl示例
image.png
预期:
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 获取以秒为单位的音频总时长。

AudioSource组件连resume的方法都没有的吗?

play 就行了
play 的行为是,暂停状态,调用 play 就是 resume
如果 正在播放 或者 停止状态,play 就是重新播放

四、
1. creater3.1版本 audioEngine这个api改成什么了
image.png

A:取消了,用audiosource
Q:请问这些接口在3.1的是啥
cc.audioEngine.pauseAll();
cc.audioEngine.stopAll();
A:定义一个常驻节点~

2.audioEngine

已经被取消了 目前是推荐用声音组件变成常驻节点来播放声音。详细的案例可以查看官方快上车案例~
如果设置成常驻节点需要注意一个问题: 常驻节点在切场景时会暂停音乐,需要在 onEnable 继续播放
(之后需要在引擎侧解决这个问题)


image.png
@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,两者都能满足。至于怎么动态播放,真的需要看文档?


image.png

图是2.4的,加载clip跟你动态加载spriteframe不是一样吗

常驻节点,我用过,也知道,但我为了播放一个bgm,在各个场景之间连贯的bgm,就要去控制一个长柱节点么·······

目前引擎内部音频系统不完善,音频系统是一个复杂且专业的系统,目前我们在能力上确实存在短板,所以暂时推荐对音频系统要求比较高的开发者使用第三方库,Criware, Fmod, Wwise 都是很好的选择。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,772评论 6 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,458评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,610评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,640评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,657评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,590评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,962评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,631评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,870评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,611评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,704评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,386评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,969评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,944评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,179评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,742评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,440评论 2 342

推荐阅读更多精彩内容