From b5a24fec0067a2254db42c93ada5b8760b5e153c Mon Sep 17 00:00:00 2001 From: PP_Pro Date: Thu, 20 Aug 2020 18:22:26 +0800 Subject: [PATCH 01/15] try set web audio volume immediately --- cocos/audio/assets/player-web.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/cocos/audio/assets/player-web.ts b/cocos/audio/assets/player-web.ts index 07abab668b6..7fd21379e45 100644 --- a/cocos/audio/assets/player-web.ts +++ b/cocos/audio/assets/player-web.ts @@ -129,7 +129,13 @@ export class AudioPlayerWeb extends AudioPlayer { public setVolume (val: number, immediate: boolean) { this._volume = val; if (!immediate && this._gainNode.gain.setTargetAtTime) { - this._gainNode.gain.setTargetAtTime(val, this._context.currentTime, 0.01); + try { + this._gainNode.gain.setTargetAtTime(val, this._context.currentTime, 0); + } + catch (e) { + // Some other unknown browsers may crash if TIME_CONSTANT is 0 + this._gainNode.gain.setTargetAtTime(val, this._context.currentTime, 0.01); + } } else { this._gainNode.gain.value = val; } From 671d12edd97dc7059f750e483948875cd6c45697 Mon Sep 17 00:00:00 2001 From: PP_Pro Date: Wed, 14 Oct 2020 16:46:22 +0800 Subject: [PATCH 02/15] fix web audio can't play after unpluging earphones update update --- cocos/audio/assets/clip.ts | 13 ++++++++++++- cocos/audio/assets/player-web.ts | 3 ++- cocos/audio/audio-source.ts | 3 +-- 3 files changed, 15 insertions(+), 4 deletions(-) diff --git a/cocos/audio/assets/clip.ts b/cocos/audio/assets/clip.ts index ebcd8e0b2a1..a585d0c1201 100644 --- a/cocos/audio/assets/clip.ts +++ b/cocos/audio/assets/clip.ts @@ -108,7 +108,18 @@ export class AudioClip extends Asset { return this._player ? this._player.getState() : PlayingState.INITIALIZING; } - public play () { if (this._player) { this._player.play(); } } + public play () { + if (this._player) { + if (this.state === PlayingState.PLAYING) { + /* sometimes there is no way to update the playing state + especially when player unplug earphones and the audio automatically stops + so we need to force updating the playing state by pausing audio */ + this._player.pause(); + this._player.setCurrentTime(0); + } + this._player.play(); + } + } public pause () { if (this._player) { this._player.pause(); } } public stop () { if (this._player) { this._player.stop(); } } public playOneShot (volume: number) { if (this._player) { this._player.playOneShot(volume); } } diff --git a/cocos/audio/assets/player-web.ts b/cocos/audio/assets/player-web.ts index 7fd21379e45..9ead7a3a447 100644 --- a/cocos/audio/assets/player-web.ts +++ b/cocos/audio/assets/player-web.ts @@ -76,7 +76,8 @@ export class AudioPlayerWeb extends AudioPlayer { if (!this._audio || this._state === PlayingState.PLAYING) { return; } if (this._blocking || this._context.state !== 'running') { this._interrupted = true; - if (this._context.state as string === 'interrupted' && this._context.resume) { + if (('interrupted' === this._context.state as string || 'suspended' === this._context.state as string) + && this._context.resume) { this._onGesture(); } return; diff --git a/cocos/audio/audio-source.ts b/cocos/audio/audio-source.ts index 44ce3bb2470..082f5728116 100644 --- a/cocos/audio/audio-source.ts +++ b/cocos/audio/audio-source.ts @@ -155,8 +155,7 @@ export class AudioSource extends Component { */ public play () { if (!this._clip) { return; } - if (this.playing) { this.currentTime = 0; } - else { this._clip.play(); } + this._clip.play(); } /** From 041744c8885a6b3bae6e7ca1fd96a3056360d286 Mon Sep 17 00:00:00 2001 From: PP_Pro Date: Tue, 13 Oct 2020 11:27:08 +0800 Subject: [PATCH 03/15] support audio clone destroy clip on ended when playing one shot rename var audio clone --- cocos/audio/assets/clip.ts | 23 ++++++--- cocos/audio/assets/player-dom.ts | 87 ++++++++++++++------------------ cocos/audio/assets/player-web.ts | 39 +++++++------- cocos/audio/assets/player.ts | 21 ++++++-- cocos/audio/audio-downloader.ts | 71 ++++++++++++++------------ 5 files changed, 128 insertions(+), 113 deletions(-) diff --git a/cocos/audio/assets/clip.ts b/cocos/audio/assets/clip.ts index a585d0c1201..84a688c09ff 100644 --- a/cocos/audio/assets/clip.ts +++ b/cocos/audio/assets/clip.ts @@ -61,6 +61,7 @@ export class AudioClip extends Asset { public static PlayingState = PlayingState; public static AudioType = AudioType; public static preventDeferredLoadDependents = true; + public isOneShot = false; @serializable protected _duration = 0; // we serialize this because it's unavailable at runtime on some platforms @@ -68,7 +69,7 @@ export class AudioClip extends Asset { @type(AudioType) protected _loadMode = AudioType.UNKNOWN_AUDIO; - protected _audio: any = null; + protected _nativeAudio: any = null; protected _player: AudioPlayer | null = null; constructor () { @@ -81,11 +82,11 @@ export class AudioClip extends Asset { return super.destroy(); } - set _nativeAsset (clip: any) { - this._audio = clip; - if (clip) { - const ctor = this._getPlayer(clip); - this._player = new ctor({ clip, duration: this._duration, eventTarget: this }); + set _nativeAsset (nativeAudio: any) { + this._nativeAudio = nativeAudio; + if (nativeAudio) { + const ctor = this._getPlayer(nativeAudio); + this._player = new ctor({ nativeAudio, duration: this._duration, audioClip: this }); this.loaded = true; this.emit('load'); } else { @@ -97,7 +98,7 @@ export class AudioClip extends Asset { } get _nativeAsset () { - return this._audio; + return this._nativeAudio; } get loadMode () { @@ -130,6 +131,14 @@ export class AudioClip extends Asset { public getVolume () { if (this._player) { return this._player.getVolume(); } return 1; } public setLoop (val: boolean) { if (this._player) { this._player.setLoop(val); } } public getLoop () { if (this._player) { return this._player.getLoop(); } return false; } + public clone (): Promise { + return new Promise((resolve, reject) => { + if (!this._player) { + return reject('player not exists'); + } + this._player.clone().then(clip => resolve(clip), reject); + }); + } private _getPlayer (clip: any) { let ctor: any; diff --git a/cocos/audio/assets/player-dom.ts b/cocos/audio/assets/player-dom.ts index 59a491ff344..ddbc8ef169c 100644 --- a/cocos/audio/assets/player-dom.ts +++ b/cocos/audio/assets/player-dom.ts @@ -31,12 +31,12 @@ import { clamp } from '../../core/math/utils'; import { AudioPlayer, IAudioInfo, PlayingState } from './player'; import { legacyCC } from '../../core/global-exports'; +import { AudioClip } from './clip'; export class AudioPlayerDOM extends AudioPlayer { protected _volume = 1; protected _loop = false; - protected _oneShotOngoing = false; - protected _audio: HTMLAudioElement; + protected _nativeAudio: HTMLAudioElement; protected _cbRegistered = false; private _remove_cb: () => void; @@ -46,7 +46,7 @@ export class AudioPlayerDOM extends AudioPlayer { constructor (info: IAudioInfo) { super(info); - this._audio = info.clip; + this._nativeAudio = info.nativeAudio; this._remove_cb = () => { if (!this._cbRegistered) { return; } @@ -57,18 +57,18 @@ export class AudioPlayerDOM extends AudioPlayer { this._post_play = () => { this._state = PlayingState.PLAYING; - this._eventTarget.emit('started'); + this._clip.emit('started'); this._remove_cb(); // should remove callbacks after any success play }; this._post_gesture = () => { if (this._interrupted) { this._post_play(); this._interrupted = false; } - else { this._audio!.pause(); this._audio!.currentTime = 0; } + else { this._nativeAudio!.pause(); this._nativeAudio!.currentTime = 0; } }; this._on_gesture = () => { - if (!this._audio) { return; } - const promise = this._audio.play(); + if (!this._nativeAudio) { return; } + const promise = this._nativeAudio.play(); if (!promise) { // Chrome50/Firefox53 below // delay eval here to yield uniform behavior with other platforms this._state = PlayingState.PLAYING; @@ -79,14 +79,13 @@ export class AudioPlayerDOM extends AudioPlayer { this._remove_cb(); }; - this._audio.volume = this._volume; - this._audio.loop = this._loop; + this._nativeAudio.volume = this._volume; + this._nativeAudio.loop = this._loop; // callback on audio ended - this._audio.addEventListener('ended', () => { - if (this._oneShotOngoing) { return; } + this._nativeAudio.addEventListener('ended', () => { this._state = PlayingState.STOPPED; - this._audio!.currentTime = 0; - this._eventTarget.emit('ended'); + this._nativeAudio!.currentTime = 0; + this._clip.emit('ended'); }); /* play & stop immediately after receiving a gesture so that we can freely invoke play() outside event listeners later */ @@ -96,9 +95,9 @@ export class AudioPlayerDOM extends AudioPlayer { } public play () { - if (!this._audio || this._state === PlayingState.PLAYING) { return; } + if (!this._nativeAudio || this._state === PlayingState.PLAYING) { return; } if (this._blocking) { this._interrupted = true; return; } - const promise = this._audio.play(); + const promise = this._nativeAudio.play(); if (!promise) { // delay eval here to yield uniform behavior with other platforms this._state = PlayingState.PLAYING; @@ -109,75 +108,65 @@ export class AudioPlayerDOM extends AudioPlayer { } public pause () { - if (!this._audio) { return; } + if (!this._nativeAudio) { return; } this._interrupted = false; if (this._state !== PlayingState.PLAYING) { return; } - this._audio.pause(); + this._nativeAudio.pause(); this._state = PlayingState.STOPPED; - this._oneShotOngoing = false; } public stop () { - if (!this._audio) { return; } - this._audio.currentTime = 0; this._interrupted = false; + if (!this._nativeAudio) { return; } + this._nativeAudio.currentTime = 0; this._interrupted = false; if (this._state !== PlayingState.PLAYING) { return; } - this._audio.pause(); + this._nativeAudio.pause(); this._state = PlayingState.STOPPED; - this._oneShotOngoing = false; - } - - public playOneShot (volume = 1) { - /* HTMLMediaElement doesn't support multiple playback at the - same time so here we fall back to re-start style approach */ - const clip = this._audio; - if (!clip) { return; } - clip.currentTime = 0; - clip.volume = volume; - if (this._oneShotOngoing) { return; } - clip.loop = false; - this._oneShotOngoing = true; - clip.play().then(() => { - clip.addEventListener('ended', () => { - clip.currentTime = 0; - clip.volume = this._volume; - clip.loop = this._loop; - this._oneShotOngoing = false; - }, { once: true }); - }).catch(() => { this._oneShotOngoing = false; }); } public setCurrentTime (val: number) { - if (!this._audio) { return; } - this._audio.currentTime = clamp(val, 0, this._duration); + if (!this._nativeAudio) { return; } + this._nativeAudio.currentTime = clamp(val, 0, this._duration); } public getCurrentTime () { - return this._audio ? this._audio.currentTime : 0; + return this._nativeAudio ? this._nativeAudio.currentTime : 0; } public setVolume (val: number, immediate: boolean) { this._volume = val; /* note this won't work for ios devices, for there is just no way to set HTMLMediaElement's volume */ - if (this._audio) { this._audio.volume = val; } + if (this._nativeAudio) { this._nativeAudio.volume = val; } } public getVolume () { - if (this._audio) { return this._audio.volume; } + if (this._nativeAudio) { return this._nativeAudio.volume; } return this._volume; } public setLoop (val: boolean) { this._loop = val; - if (this._audio) { this._audio.loop = val; } + if (this._nativeAudio) { this._nativeAudio.loop = val; } } public getLoop () { return this._loop; } + public clone (): Promise { + return new Promise((resolve, reject) => { + createDomAudio(this._nativeAudio.src).then(dom => { + let clip = new AudioClip(); + clip._nativeAsset = dom; + resolve(clip); + }, errMsg => { + log(errMsg); + }); + }); + } + public destroy () { - if (this._audio) { this._audio.src = ''; } + if (this._nativeAudio) { this._nativeAudio.src = ''; } super.destroy(); } } diff --git a/cocos/audio/assets/player-web.ts b/cocos/audio/assets/player-web.ts index 9ead7a3a447..5dc73c6f42e 100644 --- a/cocos/audio/assets/player-web.ts +++ b/cocos/audio/assets/player-web.ts @@ -32,6 +32,7 @@ import { clamp } from '../../core/math/utils'; import { sys } from '../../core/platform/sys'; import { AudioPlayer, IAudioInfo, PlayingState } from './player'; import { legacyCC } from '../../core/global-exports'; +import { AudioClip } from './clip'; const audioSupport = sys.__audioSupport; @@ -41,7 +42,7 @@ export class AudioPlayerWeb extends AudioPlayer { protected _volume = 1; protected _loop = false; protected _currentTimer = 0; - protected _audio: AudioBuffer; + protected _nativeAudio: AudioBuffer; private _context: AudioContext; private _sourceNode: AudioBufferSourceNode; @@ -54,7 +55,7 @@ export class AudioPlayerWeb extends AudioPlayer { constructor (info: IAudioInfo) { super(info); - this._audio = info.clip; + this._nativeAudio = info.nativeAudio; this._context = audioSupport.context; this._sourceNode = this._context.createBufferSource(); @@ -73,7 +74,7 @@ export class AudioPlayerWeb extends AudioPlayer { } public play () { - if (!this._audio || this._state === PlayingState.PLAYING) { return; } + if (!this._nativeAudio || this._state === PlayingState.PLAYING) { return; } if (this._blocking || this._context.state !== 'running') { this._interrupted = true; if (('interrupted' === this._context.state as string || 'suspended' === this._context.state as string) @@ -102,22 +103,10 @@ export class AudioPlayerWeb extends AudioPlayer { clearInterval(this._currentTimer); } - public playOneShot (volume = 1) { - if (!this._audio) { return; } - const gainNode = this._context.createGain(); - gainNode.connect(this._context.destination); - gainNode.gain.value = volume; - const sourceNode = this._context.createBufferSource(); - sourceNode.buffer = this._audio; - sourceNode.loop = false; - sourceNode.connect(gainNode); - sourceNode.start(); - } - public setCurrentTime (val: number) { // throws InvalidState Error on some device if we don't do the clamp here // the serialized duration may not be accurate, use the actual duration first - this._offset = clamp(val, 0, this._audio && this._audio.duration || this._duration); + this._offset = clamp(val, 0, this._nativeAudio && this._nativeAudio.duration || this._duration); if (this._state !== PlayingState.PLAYING) { return; } this._doStop(); this._doPlay(); } @@ -155,12 +144,20 @@ export class AudioPlayerWeb extends AudioPlayer { return this._loop; } + public clone (): Promise { + return new Promise(resolve => { + let clip = new AudioClip(); + clip._nativeAsset = this._nativeAudio; + resolve(clip); + }); + } + public destroy () { super.destroy(); } private _doPlay () { this._state = PlayingState.PLAYING; this._sourceNode = this._context.createBufferSource(); - this._sourceNode.buffer = this._audio; + this._sourceNode.buffer = this._nativeAudio; this._sourceNode.loop = this._loop; this._sourceNode.connect(this._gainNode); this._startTime = this._context.currentTime; @@ -175,9 +172,9 @@ export class AudioPlayerWeb extends AudioPlayer { this._onEnded(); clearInterval(this._currentTimer); if (this._sourceNode.loop) { - this._currentTimer = window.setInterval(this._onEndedCB, this._audio.duration * 1000); + this._currentTimer = window.setInterval(this._onEndedCB, this._nativeAudio.duration * 1000); } - }, (this._audio.duration - this._offset) * 1000); + }, (this._nativeAudio.duration - this._offset) * 1000); } private _doStop () { @@ -188,7 +185,7 @@ export class AudioPlayerWeb extends AudioPlayer { private _playAndEmit () { this._sourceNode.start(0, this._offset); - this._eventTarget.emit('started'); + this._clip.emit('started'); this._startInvoked = true; } @@ -196,7 +193,7 @@ export class AudioPlayerWeb extends AudioPlayer { this._offset = 0; this._startTime = this._context.currentTime; if (this._sourceNode.loop) { return; } - this._eventTarget.emit('ended'); + this._clip.emit('ended'); this._state = PlayingState.STOPPED; } diff --git a/cocos/audio/assets/player.ts b/cocos/audio/assets/player.ts index dff360b0573..a97d5129071 100644 --- a/cocos/audio/assets/player.ts +++ b/cocos/audio/assets/player.ts @@ -28,6 +28,7 @@ */ import { legacyCC } from '../../core/global-exports'; +import { AudioClip } from './clip'; export const PlayingState = { INITIALIZING: 0, @@ -36,24 +37,24 @@ export const PlayingState = { }; export interface IAudioInfo { - clip: any; + nativeAudio: any; duration: number; - eventTarget: any; + audioClip: any; } export abstract class AudioPlayer { protected _state = PlayingState.STOPPED; protected _duration = 0; - protected _eventTarget: any; protected _onHide: Function; protected _onShow: Function; protected _interrupted = false; protected _blocking = false; + protected _clip: AudioClip; constructor (info: IAudioInfo) { + this._clip = info.audioClip; this._duration = info.duration; - this._eventTarget = info.eventTarget; this._onHide = () => { this._blocking = true; if (this._state !== PlayingState.PLAYING) { return; } @@ -72,7 +73,16 @@ export abstract class AudioPlayer { public abstract play (): void; public abstract pause (): void; public abstract stop (): void; - public abstract playOneShot (volume: number): void; + public playOneShot (volume = 1) { + this._clip.clone().then(clonedClip => { + clonedClip.isOneShot = true; + clonedClip.setVolume(volume); + clonedClip.once('ended', () => { + clonedClip.destroy(); + }); + clonedClip.play(); + }); + } public abstract setCurrentTime (val: number): void; public abstract getCurrentTime (): number; public abstract setVolume (val: number, immediate: boolean): void; @@ -81,6 +91,7 @@ export abstract class AudioPlayer { public abstract getLoop (): boolean; public getState () { return this._state; } public getDuration () { return this._duration; } + public abstract clone (): Promise; public destroy () { legacyCC.game.off(legacyCC.Game.EVENT_HIDE, this._onHide); legacyCC.game.off(legacyCC.Game.EVENT_SHOW, this._onShow); diff --git a/cocos/audio/audio-downloader.ts b/cocos/audio/audio-downloader.ts index b835d86e568..f0d0af60e0f 100644 --- a/cocos/audio/audio-downloader.ts +++ b/cocos/audio/audio-downloader.ts @@ -36,40 +36,49 @@ import { legacyCC } from '../core/global-exports'; const __audioSupport = sys.__audioSupport; const formatSupport = __audioSupport.format; -function loadDomAudio (item, callback) { - const dom = document.createElement('audio'); - dom.src = item.url; - - const clearEvent = () => { - clearTimeout(timer); - dom.removeEventListener('canplaythrough', success, false); - dom.removeEventListener('error', failure, false); +export function createDomAudio (url): Promise { + return new Promise((resolve, reject) => { + const dom = document.createElement('audio'); + dom.src = url; + + const clearEvent = () => { + clearTimeout(timer); + dom.removeEventListener('canplaythrough', success, false); + dom.removeEventListener('error', failure, false); + if (__audioSupport.USE_LOADER_EVENT) { + dom.removeEventListener(__audioSupport.USE_LOADER_EVENT, success, false); + } + }; + const timer = setTimeout(() => { + if (dom.readyState === 0) { + failure(); + } else { + success(); + } + }, 8000); + const success = () => { + clearEvent(); + resolve(dom); + }; + const failure = () => { + clearEvent(); + const message = 'load audio failure - ' + url; + reject(message); + }; + dom.addEventListener('canplaythrough', success, false); + dom.addEventListener('error', failure, false); if (__audioSupport.USE_LOADER_EVENT) { - dom.removeEventListener(__audioSupport.USE_LOADER_EVENT, success, false); - } - }; - const timer = setTimeout(() => { - if (dom.readyState === 0) { - failure(); - } else { - success(); + dom.addEventListener(__audioSupport.USE_LOADER_EVENT, success, false); } - }, 8000); - const success = () => { - clearEvent(); + }); +} + +function loadDomAudio (item, callback) { + createDomAudio(item.url).then(dom => { callback(null, dom); - }; - const failure = () => { - clearEvent(); - const message = 'load audio failure - ' + item.url; - log(message); - callback(message); - }; - dom.addEventListener('canplaythrough', success, false); - dom.addEventListener('error', failure, false); - if (__audioSupport.USE_LOADER_EVENT) { - dom.addEventListener(__audioSupport.USE_LOADER_EVENT, success, false); - } + }, errMsg => { + log(errMsg); + }); } function loadWebAudio (item, callback) { From b21e3405cefc07729fb7c913ecac50402cf9c857 Mon Sep 17 00:00:00 2001 From: PP_Pro Date: Wed, 14 Oct 2020 17:24:28 +0800 Subject: [PATCH 04/15] support audio manager --- cocos/audio/assets/player-dom.ts | 7 ++++ cocos/audio/assets/player-web.ts | 4 ++ cocos/audio/audio-manager.ts | 71 ++++++++++++++++++++++++++++++++ 3 files changed, 82 insertions(+) create mode 100644 cocos/audio/audio-manager.ts diff --git a/cocos/audio/assets/player-dom.ts b/cocos/audio/assets/player-dom.ts index ddbc8ef169c..1ec083b03a0 100644 --- a/cocos/audio/assets/player-dom.ts +++ b/cocos/audio/assets/player-dom.ts @@ -32,6 +32,9 @@ import { clamp } from '../../core/math/utils'; import { AudioPlayer, IAudioInfo, PlayingState } from './player'; import { legacyCC } from '../../core/global-exports'; import { AudioClip } from './clip'; +import audioManager from '../audio-manager'; +import { createDomAudio } from '../audio-downloader'; +import { log } from '../../core/platform/debug'; export class AudioPlayerDOM extends AudioPlayer { protected _volume = 1; @@ -59,6 +62,7 @@ export class AudioPlayerDOM extends AudioPlayer { this._state = PlayingState.PLAYING; this._clip.emit('started'); this._remove_cb(); // should remove callbacks after any success play + audioManager.addPlaying(this._clip); }; this._post_gesture = () => { @@ -97,6 +101,7 @@ export class AudioPlayerDOM extends AudioPlayer { public play () { if (!this._nativeAudio || this._state === PlayingState.PLAYING) { return; } if (this._blocking) { this._interrupted = true; return; } + audioManager.discardOnePlayingIfNeeded(); const promise = this._nativeAudio.play(); if (!promise) { // delay eval here to yield uniform behavior with other platforms @@ -113,6 +118,7 @@ export class AudioPlayerDOM extends AudioPlayer { if (this._state !== PlayingState.PLAYING) { return; } this._nativeAudio.pause(); this._state = PlayingState.STOPPED; + audioManager.removePlaying(this._clip); } public stop () { @@ -121,6 +127,7 @@ export class AudioPlayerDOM extends AudioPlayer { if (this._state !== PlayingState.PLAYING) { return; } this._nativeAudio.pause(); this._state = PlayingState.STOPPED; + audioManager.removePlaying(this._clip); } public setCurrentTime (val: number) { diff --git a/cocos/audio/assets/player-web.ts b/cocos/audio/assets/player-web.ts index 5dc73c6f42e..47806276821 100644 --- a/cocos/audio/assets/player-web.ts +++ b/cocos/audio/assets/player-web.ts @@ -33,6 +33,7 @@ import { sys } from '../../core/platform/sys'; import { AudioPlayer, IAudioInfo, PlayingState } from './player'; import { legacyCC } from '../../core/global-exports'; import { AudioClip } from './clip'; +import audioManager from '../audio-manager'; const audioSupport = sys.__audioSupport; @@ -155,6 +156,7 @@ export class AudioPlayerWeb extends AudioPlayer { public destroy () { super.destroy(); } private _doPlay () { + audioManager.discardOnePlayingIfNeeded(); this._state = PlayingState.PLAYING; this._sourceNode = this._context.createBufferSource(); this._sourceNode.buffer = this._nativeAudio; @@ -181,12 +183,14 @@ export class AudioPlayerWeb extends AudioPlayer { // stop can only be called after play if (this._startInvoked) { this._sourceNode.stop(); } else { legacyCC.director.off(legacyCC.Director.EVENT_AFTER_UPDATE, this._playAndEmit, this); } + audioManager.removePlaying(this._clip); } private _playAndEmit () { this._sourceNode.start(0, this._offset); this._clip.emit('started'); this._startInvoked = true; + audioManager.addPlaying(this._clip); } private _onEnded () { diff --git a/cocos/audio/audio-manager.ts b/cocos/audio/audio-manager.ts new file mode 100644 index 00000000000..322b9df153a --- /dev/null +++ b/cocos/audio/audio-manager.ts @@ -0,0 +1,71 @@ +import { AudioClip } from './assets/clip'; +import { legacyCC } from '../core/global-exports'; + +enum DiscardStrategy { + OLDEST, + NEWEST, + QUIETEST, +} + +class AudioManager { + private _playingClips: Array; + + public static DiscardStrategy = DiscardStrategy; + public discardStrategy: DiscardStrategy; + public static readonly maxAudioChannel: number = 24; + + constructor () { + this._playingClips = []; + this.discardStrategy = DiscardStrategy.OLDEST; + } + + public addPlaying (clip: AudioClip) { + clip.once('ended', () => { + this.removePlaying(clip); + }); + this._playingClips.push(clip); + } + + public removePlaying (clip: AudioClip) { + let index = this._playingClips.indexOf(clip); + if (index > -1) { + this._playingClips.splice(index, 1); + } + } + + public discardOnePlayingIfNeeded () { + if (this._playingClips.length < AudioManager.maxAudioChannel) { + return; + } + let clip: AudioClip | undefined; + switch (this.discardStrategy) { + case DiscardStrategy.OLDEST: + clip = this._playingClips.shift(); + break; + case DiscardStrategy.NEWEST: + clip = this._playingClips.pop(); + break; + case DiscardStrategy.QUIETEST: + let minVolume = 9999; + this._playingClips.forEach(playingClip => { + let currentVolume = playingClip.getVolume(); + if (minVolume > currentVolume) { + minVolume = currentVolume; + clip = playingClip; + } + }); + break; + } + if (clip) { + if (clip.state === AudioClip.PlayingState.PLAYING) { + clip.stop(); + } + // one shot audio clip should be destroyed + if (clip.isOneShot) { + clip.destroy(); + } + } + } +} + +export default legacyCC.internal.audioManager = new AudioManager(); \ No newline at end of file From 68b1245974d1f904f2698a50c2f33e438e101b23 Mon Sep 17 00:00:00 2001 From: PP_Pro Date: Wed, 14 Oct 2020 17:51:14 +0800 Subject: [PATCH 05/15] update details --- cocos/audio/assets/player-dom.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/cocos/audio/assets/player-dom.ts b/cocos/audio/assets/player-dom.ts index 1ec083b03a0..e6e4d000dd7 100644 --- a/cocos/audio/assets/player-dom.ts +++ b/cocos/audio/assets/player-dom.ts @@ -34,7 +34,6 @@ import { legacyCC } from '../../core/global-exports'; import { AudioClip } from './clip'; import audioManager from '../audio-manager'; import { createDomAudio } from '../audio-downloader'; -import { log } from '../../core/platform/debug'; export class AudioPlayerDOM extends AudioPlayer { protected _volume = 1; @@ -167,7 +166,7 @@ export class AudioPlayerDOM extends AudioPlayer { clip._nativeAsset = dom; resolve(clip); }, errMsg => { - log(errMsg); + reject(errMsg); }); }); } From 1da8c34ff142c14038858f97d88ed2bd5ffccebf Mon Sep 17 00:00:00 2001 From: PP_Pro Date: Thu, 15 Oct 2020 17:09:36 +0800 Subject: [PATCH 06/15] Revert "update details" This reverts commit 68b1245974d1f904f2698a50c2f33e438e101b23. --- cocos/audio/assets/player-dom.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/cocos/audio/assets/player-dom.ts b/cocos/audio/assets/player-dom.ts index e6e4d000dd7..1ec083b03a0 100644 --- a/cocos/audio/assets/player-dom.ts +++ b/cocos/audio/assets/player-dom.ts @@ -34,6 +34,7 @@ import { legacyCC } from '../../core/global-exports'; import { AudioClip } from './clip'; import audioManager from '../audio-manager'; import { createDomAudio } from '../audio-downloader'; +import { log } from '../../core/platform/debug'; export class AudioPlayerDOM extends AudioPlayer { protected _volume = 1; @@ -166,7 +167,7 @@ export class AudioPlayerDOM extends AudioPlayer { clip._nativeAsset = dom; resolve(clip); }, errMsg => { - reject(errMsg); + log(errMsg); }); }); } From d1aac4a7a08d949ff8e4055ed22e9f69503bf162 Mon Sep 17 00:00:00 2001 From: PP_Pro Date: Thu, 15 Oct 2020 17:09:40 +0800 Subject: [PATCH 07/15] Revert "support audio manager" This reverts commit b21e3405cefc07729fb7c913ecac50402cf9c857. --- cocos/audio/assets/player-dom.ts | 7 ---- cocos/audio/assets/player-web.ts | 4 -- cocos/audio/audio-manager.ts | 71 -------------------------------- 3 files changed, 82 deletions(-) delete mode 100644 cocos/audio/audio-manager.ts diff --git a/cocos/audio/assets/player-dom.ts b/cocos/audio/assets/player-dom.ts index 1ec083b03a0..ddbc8ef169c 100644 --- a/cocos/audio/assets/player-dom.ts +++ b/cocos/audio/assets/player-dom.ts @@ -32,9 +32,6 @@ import { clamp } from '../../core/math/utils'; import { AudioPlayer, IAudioInfo, PlayingState } from './player'; import { legacyCC } from '../../core/global-exports'; import { AudioClip } from './clip'; -import audioManager from '../audio-manager'; -import { createDomAudio } from '../audio-downloader'; -import { log } from '../../core/platform/debug'; export class AudioPlayerDOM extends AudioPlayer { protected _volume = 1; @@ -62,7 +59,6 @@ export class AudioPlayerDOM extends AudioPlayer { this._state = PlayingState.PLAYING; this._clip.emit('started'); this._remove_cb(); // should remove callbacks after any success play - audioManager.addPlaying(this._clip); }; this._post_gesture = () => { @@ -101,7 +97,6 @@ export class AudioPlayerDOM extends AudioPlayer { public play () { if (!this._nativeAudio || this._state === PlayingState.PLAYING) { return; } if (this._blocking) { this._interrupted = true; return; } - audioManager.discardOnePlayingIfNeeded(); const promise = this._nativeAudio.play(); if (!promise) { // delay eval here to yield uniform behavior with other platforms @@ -118,7 +113,6 @@ export class AudioPlayerDOM extends AudioPlayer { if (this._state !== PlayingState.PLAYING) { return; } this._nativeAudio.pause(); this._state = PlayingState.STOPPED; - audioManager.removePlaying(this._clip); } public stop () { @@ -127,7 +121,6 @@ export class AudioPlayerDOM extends AudioPlayer { if (this._state !== PlayingState.PLAYING) { return; } this._nativeAudio.pause(); this._state = PlayingState.STOPPED; - audioManager.removePlaying(this._clip); } public setCurrentTime (val: number) { diff --git a/cocos/audio/assets/player-web.ts b/cocos/audio/assets/player-web.ts index 47806276821..5dc73c6f42e 100644 --- a/cocos/audio/assets/player-web.ts +++ b/cocos/audio/assets/player-web.ts @@ -33,7 +33,6 @@ import { sys } from '../../core/platform/sys'; import { AudioPlayer, IAudioInfo, PlayingState } from './player'; import { legacyCC } from '../../core/global-exports'; import { AudioClip } from './clip'; -import audioManager from '../audio-manager'; const audioSupport = sys.__audioSupport; @@ -156,7 +155,6 @@ export class AudioPlayerWeb extends AudioPlayer { public destroy () { super.destroy(); } private _doPlay () { - audioManager.discardOnePlayingIfNeeded(); this._state = PlayingState.PLAYING; this._sourceNode = this._context.createBufferSource(); this._sourceNode.buffer = this._nativeAudio; @@ -183,14 +181,12 @@ export class AudioPlayerWeb extends AudioPlayer { // stop can only be called after play if (this._startInvoked) { this._sourceNode.stop(); } else { legacyCC.director.off(legacyCC.Director.EVENT_AFTER_UPDATE, this._playAndEmit, this); } - audioManager.removePlaying(this._clip); } private _playAndEmit () { this._sourceNode.start(0, this._offset); this._clip.emit('started'); this._startInvoked = true; - audioManager.addPlaying(this._clip); } private _onEnded () { diff --git a/cocos/audio/audio-manager.ts b/cocos/audio/audio-manager.ts deleted file mode 100644 index 322b9df153a..00000000000 --- a/cocos/audio/audio-manager.ts +++ /dev/null @@ -1,71 +0,0 @@ -import { AudioClip } from './assets/clip'; -import { legacyCC } from '../core/global-exports'; - -enum DiscardStrategy { - OLDEST, - NEWEST, - QUIETEST, -} - -class AudioManager { - private _playingClips: Array; - - public static DiscardStrategy = DiscardStrategy; - public discardStrategy: DiscardStrategy; - public static readonly maxAudioChannel: number = 24; - - constructor () { - this._playingClips = []; - this.discardStrategy = DiscardStrategy.OLDEST; - } - - public addPlaying (clip: AudioClip) { - clip.once('ended', () => { - this.removePlaying(clip); - }); - this._playingClips.push(clip); - } - - public removePlaying (clip: AudioClip) { - let index = this._playingClips.indexOf(clip); - if (index > -1) { - this._playingClips.splice(index, 1); - } - } - - public discardOnePlayingIfNeeded () { - if (this._playingClips.length < AudioManager.maxAudioChannel) { - return; - } - let clip: AudioClip | undefined; - switch (this.discardStrategy) { - case DiscardStrategy.OLDEST: - clip = this._playingClips.shift(); - break; - case DiscardStrategy.NEWEST: - clip = this._playingClips.pop(); - break; - case DiscardStrategy.QUIETEST: - let minVolume = 9999; - this._playingClips.forEach(playingClip => { - let currentVolume = playingClip.getVolume(); - if (minVolume > currentVolume) { - minVolume = currentVolume; - clip = playingClip; - } - }); - break; - } - if (clip) { - if (clip.state === AudioClip.PlayingState.PLAYING) { - clip.stop(); - } - // one shot audio clip should be destroyed - if (clip.isOneShot) { - clip.destroy(); - } - } - } -} - -export default legacyCC.internal.audioManager = new AudioManager(); \ No newline at end of file From 7de6f7b11110f3b23529e0456efaf3f53aafdd12 Mon Sep 17 00:00:00 2001 From: PP_Pro Date: Thu, 15 Oct 2020 17:09:44 +0800 Subject: [PATCH 08/15] Revert "support audio clone" This reverts commit 041744c8885a6b3bae6e7ca1fd96a3056360d286. --- cocos/audio/assets/clip.ts | 23 +++------ cocos/audio/assets/player-dom.ts | 87 ++++++++++++++++++-------------- cocos/audio/assets/player-web.ts | 39 +++++++------- cocos/audio/assets/player.ts | 21 ++------ cocos/audio/audio-downloader.ts | 71 ++++++++++++-------------- 5 files changed, 113 insertions(+), 128 deletions(-) diff --git a/cocos/audio/assets/clip.ts b/cocos/audio/assets/clip.ts index 84a688c09ff..a585d0c1201 100644 --- a/cocos/audio/assets/clip.ts +++ b/cocos/audio/assets/clip.ts @@ -61,7 +61,6 @@ export class AudioClip extends Asset { public static PlayingState = PlayingState; public static AudioType = AudioType; public static preventDeferredLoadDependents = true; - public isOneShot = false; @serializable protected _duration = 0; // we serialize this because it's unavailable at runtime on some platforms @@ -69,7 +68,7 @@ export class AudioClip extends Asset { @type(AudioType) protected _loadMode = AudioType.UNKNOWN_AUDIO; - protected _nativeAudio: any = null; + protected _audio: any = null; protected _player: AudioPlayer | null = null; constructor () { @@ -82,11 +81,11 @@ export class AudioClip extends Asset { return super.destroy(); } - set _nativeAsset (nativeAudio: any) { - this._nativeAudio = nativeAudio; - if (nativeAudio) { - const ctor = this._getPlayer(nativeAudio); - this._player = new ctor({ nativeAudio, duration: this._duration, audioClip: this }); + set _nativeAsset (clip: any) { + this._audio = clip; + if (clip) { + const ctor = this._getPlayer(clip); + this._player = new ctor({ clip, duration: this._duration, eventTarget: this }); this.loaded = true; this.emit('load'); } else { @@ -98,7 +97,7 @@ export class AudioClip extends Asset { } get _nativeAsset () { - return this._nativeAudio; + return this._audio; } get loadMode () { @@ -131,14 +130,6 @@ export class AudioClip extends Asset { public getVolume () { if (this._player) { return this._player.getVolume(); } return 1; } public setLoop (val: boolean) { if (this._player) { this._player.setLoop(val); } } public getLoop () { if (this._player) { return this._player.getLoop(); } return false; } - public clone (): Promise { - return new Promise((resolve, reject) => { - if (!this._player) { - return reject('player not exists'); - } - this._player.clone().then(clip => resolve(clip), reject); - }); - } private _getPlayer (clip: any) { let ctor: any; diff --git a/cocos/audio/assets/player-dom.ts b/cocos/audio/assets/player-dom.ts index ddbc8ef169c..59a491ff344 100644 --- a/cocos/audio/assets/player-dom.ts +++ b/cocos/audio/assets/player-dom.ts @@ -31,12 +31,12 @@ import { clamp } from '../../core/math/utils'; import { AudioPlayer, IAudioInfo, PlayingState } from './player'; import { legacyCC } from '../../core/global-exports'; -import { AudioClip } from './clip'; export class AudioPlayerDOM extends AudioPlayer { protected _volume = 1; protected _loop = false; - protected _nativeAudio: HTMLAudioElement; + protected _oneShotOngoing = false; + protected _audio: HTMLAudioElement; protected _cbRegistered = false; private _remove_cb: () => void; @@ -46,7 +46,7 @@ export class AudioPlayerDOM extends AudioPlayer { constructor (info: IAudioInfo) { super(info); - this._nativeAudio = info.nativeAudio; + this._audio = info.clip; this._remove_cb = () => { if (!this._cbRegistered) { return; } @@ -57,18 +57,18 @@ export class AudioPlayerDOM extends AudioPlayer { this._post_play = () => { this._state = PlayingState.PLAYING; - this._clip.emit('started'); + this._eventTarget.emit('started'); this._remove_cb(); // should remove callbacks after any success play }; this._post_gesture = () => { if (this._interrupted) { this._post_play(); this._interrupted = false; } - else { this._nativeAudio!.pause(); this._nativeAudio!.currentTime = 0; } + else { this._audio!.pause(); this._audio!.currentTime = 0; } }; this._on_gesture = () => { - if (!this._nativeAudio) { return; } - const promise = this._nativeAudio.play(); + if (!this._audio) { return; } + const promise = this._audio.play(); if (!promise) { // Chrome50/Firefox53 below // delay eval here to yield uniform behavior with other platforms this._state = PlayingState.PLAYING; @@ -79,13 +79,14 @@ export class AudioPlayerDOM extends AudioPlayer { this._remove_cb(); }; - this._nativeAudio.volume = this._volume; - this._nativeAudio.loop = this._loop; + this._audio.volume = this._volume; + this._audio.loop = this._loop; // callback on audio ended - this._nativeAudio.addEventListener('ended', () => { + this._audio.addEventListener('ended', () => { + if (this._oneShotOngoing) { return; } this._state = PlayingState.STOPPED; - this._nativeAudio!.currentTime = 0; - this._clip.emit('ended'); + this._audio!.currentTime = 0; + this._eventTarget.emit('ended'); }); /* play & stop immediately after receiving a gesture so that we can freely invoke play() outside event listeners later */ @@ -95,9 +96,9 @@ export class AudioPlayerDOM extends AudioPlayer { } public play () { - if (!this._nativeAudio || this._state === PlayingState.PLAYING) { return; } + if (!this._audio || this._state === PlayingState.PLAYING) { return; } if (this._blocking) { this._interrupted = true; return; } - const promise = this._nativeAudio.play(); + const promise = this._audio.play(); if (!promise) { // delay eval here to yield uniform behavior with other platforms this._state = PlayingState.PLAYING; @@ -108,65 +109,75 @@ export class AudioPlayerDOM extends AudioPlayer { } public pause () { - if (!this._nativeAudio) { return; } + if (!this._audio) { return; } this._interrupted = false; if (this._state !== PlayingState.PLAYING) { return; } - this._nativeAudio.pause(); + this._audio.pause(); this._state = PlayingState.STOPPED; + this._oneShotOngoing = false; } public stop () { - if (!this._nativeAudio) { return; } - this._nativeAudio.currentTime = 0; this._interrupted = false; + if (!this._audio) { return; } + this._audio.currentTime = 0; this._interrupted = false; if (this._state !== PlayingState.PLAYING) { return; } - this._nativeAudio.pause(); + this._audio.pause(); this._state = PlayingState.STOPPED; + this._oneShotOngoing = false; + } + + public playOneShot (volume = 1) { + /* HTMLMediaElement doesn't support multiple playback at the + same time so here we fall back to re-start style approach */ + const clip = this._audio; + if (!clip) { return; } + clip.currentTime = 0; + clip.volume = volume; + if (this._oneShotOngoing) { return; } + clip.loop = false; + this._oneShotOngoing = true; + clip.play().then(() => { + clip.addEventListener('ended', () => { + clip.currentTime = 0; + clip.volume = this._volume; + clip.loop = this._loop; + this._oneShotOngoing = false; + }, { once: true }); + }).catch(() => { this._oneShotOngoing = false; }); } public setCurrentTime (val: number) { - if (!this._nativeAudio) { return; } - this._nativeAudio.currentTime = clamp(val, 0, this._duration); + if (!this._audio) { return; } + this._audio.currentTime = clamp(val, 0, this._duration); } public getCurrentTime () { - return this._nativeAudio ? this._nativeAudio.currentTime : 0; + return this._audio ? this._audio.currentTime : 0; } public setVolume (val: number, immediate: boolean) { this._volume = val; /* note this won't work for ios devices, for there is just no way to set HTMLMediaElement's volume */ - if (this._nativeAudio) { this._nativeAudio.volume = val; } + if (this._audio) { this._audio.volume = val; } } public getVolume () { - if (this._nativeAudio) { return this._nativeAudio.volume; } + if (this._audio) { return this._audio.volume; } return this._volume; } public setLoop (val: boolean) { this._loop = val; - if (this._nativeAudio) { this._nativeAudio.loop = val; } + if (this._audio) { this._audio.loop = val; } } public getLoop () { return this._loop; } - public clone (): Promise { - return new Promise((resolve, reject) => { - createDomAudio(this._nativeAudio.src).then(dom => { - let clip = new AudioClip(); - clip._nativeAsset = dom; - resolve(clip); - }, errMsg => { - log(errMsg); - }); - }); - } - public destroy () { - if (this._nativeAudio) { this._nativeAudio.src = ''; } + if (this._audio) { this._audio.src = ''; } super.destroy(); } } diff --git a/cocos/audio/assets/player-web.ts b/cocos/audio/assets/player-web.ts index 5dc73c6f42e..9ead7a3a447 100644 --- a/cocos/audio/assets/player-web.ts +++ b/cocos/audio/assets/player-web.ts @@ -32,7 +32,6 @@ import { clamp } from '../../core/math/utils'; import { sys } from '../../core/platform/sys'; import { AudioPlayer, IAudioInfo, PlayingState } from './player'; import { legacyCC } from '../../core/global-exports'; -import { AudioClip } from './clip'; const audioSupport = sys.__audioSupport; @@ -42,7 +41,7 @@ export class AudioPlayerWeb extends AudioPlayer { protected _volume = 1; protected _loop = false; protected _currentTimer = 0; - protected _nativeAudio: AudioBuffer; + protected _audio: AudioBuffer; private _context: AudioContext; private _sourceNode: AudioBufferSourceNode; @@ -55,7 +54,7 @@ export class AudioPlayerWeb extends AudioPlayer { constructor (info: IAudioInfo) { super(info); - this._nativeAudio = info.nativeAudio; + this._audio = info.clip; this._context = audioSupport.context; this._sourceNode = this._context.createBufferSource(); @@ -74,7 +73,7 @@ export class AudioPlayerWeb extends AudioPlayer { } public play () { - if (!this._nativeAudio || this._state === PlayingState.PLAYING) { return; } + if (!this._audio || this._state === PlayingState.PLAYING) { return; } if (this._blocking || this._context.state !== 'running') { this._interrupted = true; if (('interrupted' === this._context.state as string || 'suspended' === this._context.state as string) @@ -103,10 +102,22 @@ export class AudioPlayerWeb extends AudioPlayer { clearInterval(this._currentTimer); } + public playOneShot (volume = 1) { + if (!this._audio) { return; } + const gainNode = this._context.createGain(); + gainNode.connect(this._context.destination); + gainNode.gain.value = volume; + const sourceNode = this._context.createBufferSource(); + sourceNode.buffer = this._audio; + sourceNode.loop = false; + sourceNode.connect(gainNode); + sourceNode.start(); + } + public setCurrentTime (val: number) { // throws InvalidState Error on some device if we don't do the clamp here // the serialized duration may not be accurate, use the actual duration first - this._offset = clamp(val, 0, this._nativeAudio && this._nativeAudio.duration || this._duration); + this._offset = clamp(val, 0, this._audio && this._audio.duration || this._duration); if (this._state !== PlayingState.PLAYING) { return; } this._doStop(); this._doPlay(); } @@ -144,20 +155,12 @@ export class AudioPlayerWeb extends AudioPlayer { return this._loop; } - public clone (): Promise { - return new Promise(resolve => { - let clip = new AudioClip(); - clip._nativeAsset = this._nativeAudio; - resolve(clip); - }); - } - public destroy () { super.destroy(); } private _doPlay () { this._state = PlayingState.PLAYING; this._sourceNode = this._context.createBufferSource(); - this._sourceNode.buffer = this._nativeAudio; + this._sourceNode.buffer = this._audio; this._sourceNode.loop = this._loop; this._sourceNode.connect(this._gainNode); this._startTime = this._context.currentTime; @@ -172,9 +175,9 @@ export class AudioPlayerWeb extends AudioPlayer { this._onEnded(); clearInterval(this._currentTimer); if (this._sourceNode.loop) { - this._currentTimer = window.setInterval(this._onEndedCB, this._nativeAudio.duration * 1000); + this._currentTimer = window.setInterval(this._onEndedCB, this._audio.duration * 1000); } - }, (this._nativeAudio.duration - this._offset) * 1000); + }, (this._audio.duration - this._offset) * 1000); } private _doStop () { @@ -185,7 +188,7 @@ export class AudioPlayerWeb extends AudioPlayer { private _playAndEmit () { this._sourceNode.start(0, this._offset); - this._clip.emit('started'); + this._eventTarget.emit('started'); this._startInvoked = true; } @@ -193,7 +196,7 @@ export class AudioPlayerWeb extends AudioPlayer { this._offset = 0; this._startTime = this._context.currentTime; if (this._sourceNode.loop) { return; } - this._clip.emit('ended'); + this._eventTarget.emit('ended'); this._state = PlayingState.STOPPED; } diff --git a/cocos/audio/assets/player.ts b/cocos/audio/assets/player.ts index a97d5129071..dff360b0573 100644 --- a/cocos/audio/assets/player.ts +++ b/cocos/audio/assets/player.ts @@ -28,7 +28,6 @@ */ import { legacyCC } from '../../core/global-exports'; -import { AudioClip } from './clip'; export const PlayingState = { INITIALIZING: 0, @@ -37,24 +36,24 @@ export const PlayingState = { }; export interface IAudioInfo { - nativeAudio: any; + clip: any; duration: number; - audioClip: any; + eventTarget: any; } export abstract class AudioPlayer { protected _state = PlayingState.STOPPED; protected _duration = 0; + protected _eventTarget: any; protected _onHide: Function; protected _onShow: Function; protected _interrupted = false; protected _blocking = false; - protected _clip: AudioClip; constructor (info: IAudioInfo) { - this._clip = info.audioClip; this._duration = info.duration; + this._eventTarget = info.eventTarget; this._onHide = () => { this._blocking = true; if (this._state !== PlayingState.PLAYING) { return; } @@ -73,16 +72,7 @@ export abstract class AudioPlayer { public abstract play (): void; public abstract pause (): void; public abstract stop (): void; - public playOneShot (volume = 1) { - this._clip.clone().then(clonedClip => { - clonedClip.isOneShot = true; - clonedClip.setVolume(volume); - clonedClip.once('ended', () => { - clonedClip.destroy(); - }); - clonedClip.play(); - }); - } + public abstract playOneShot (volume: number): void; public abstract setCurrentTime (val: number): void; public abstract getCurrentTime (): number; public abstract setVolume (val: number, immediate: boolean): void; @@ -91,7 +81,6 @@ export abstract class AudioPlayer { public abstract getLoop (): boolean; public getState () { return this._state; } public getDuration () { return this._duration; } - public abstract clone (): Promise; public destroy () { legacyCC.game.off(legacyCC.Game.EVENT_HIDE, this._onHide); legacyCC.game.off(legacyCC.Game.EVENT_SHOW, this._onShow); diff --git a/cocos/audio/audio-downloader.ts b/cocos/audio/audio-downloader.ts index f0d0af60e0f..b835d86e568 100644 --- a/cocos/audio/audio-downloader.ts +++ b/cocos/audio/audio-downloader.ts @@ -36,49 +36,40 @@ import { legacyCC } from '../core/global-exports'; const __audioSupport = sys.__audioSupport; const formatSupport = __audioSupport.format; -export function createDomAudio (url): Promise { - return new Promise((resolve, reject) => { - const dom = document.createElement('audio'); - dom.src = url; - - const clearEvent = () => { - clearTimeout(timer); - dom.removeEventListener('canplaythrough', success, false); - dom.removeEventListener('error', failure, false); - if (__audioSupport.USE_LOADER_EVENT) { - dom.removeEventListener(__audioSupport.USE_LOADER_EVENT, success, false); - } - }; - const timer = setTimeout(() => { - if (dom.readyState === 0) { - failure(); - } else { - success(); - } - }, 8000); - const success = () => { - clearEvent(); - resolve(dom); - }; - const failure = () => { - clearEvent(); - const message = 'load audio failure - ' + url; - reject(message); - }; - dom.addEventListener('canplaythrough', success, false); - dom.addEventListener('error', failure, false); +function loadDomAudio (item, callback) { + const dom = document.createElement('audio'); + dom.src = item.url; + + const clearEvent = () => { + clearTimeout(timer); + dom.removeEventListener('canplaythrough', success, false); + dom.removeEventListener('error', failure, false); if (__audioSupport.USE_LOADER_EVENT) { - dom.addEventListener(__audioSupport.USE_LOADER_EVENT, success, false); + dom.removeEventListener(__audioSupport.USE_LOADER_EVENT, success, false); } - }); -} - -function loadDomAudio (item, callback) { - createDomAudio(item.url).then(dom => { + }; + const timer = setTimeout(() => { + if (dom.readyState === 0) { + failure(); + } else { + success(); + } + }, 8000); + const success = () => { + clearEvent(); callback(null, dom); - }, errMsg => { - log(errMsg); - }); + }; + const failure = () => { + clearEvent(); + const message = 'load audio failure - ' + item.url; + log(message); + callback(message); + }; + dom.addEventListener('canplaythrough', success, false); + dom.addEventListener('error', failure, false); + if (__audioSupport.USE_LOADER_EVENT) { + dom.addEventListener(__audioSupport.USE_LOADER_EVENT, success, false); + } } function loadWebAudio (item, callback) { From 7207c5509f8e7195ad815795773d434acd9ab63f Mon Sep 17 00:00:00 2001 From: PP_Pro Date: Thu, 15 Oct 2020 17:21:56 +0800 Subject: [PATCH 09/15] rename var rename --- cocos/audio/assets/clip.ts | 14 ++++----- cocos/audio/assets/player-dom.ts | 50 ++++++++++++++++---------------- cocos/audio/assets/player-web.ts | 22 +++++++------- cocos/audio/assets/player.ts | 9 +++--- 4 files changed, 48 insertions(+), 47 deletions(-) diff --git a/cocos/audio/assets/clip.ts b/cocos/audio/assets/clip.ts index a585d0c1201..14f404a9c71 100644 --- a/cocos/audio/assets/clip.ts +++ b/cocos/audio/assets/clip.ts @@ -68,7 +68,7 @@ export class AudioClip extends Asset { @type(AudioType) protected _loadMode = AudioType.UNKNOWN_AUDIO; - protected _audio: any = null; + protected _nativeAudio: any = null; protected _player: AudioPlayer | null = null; constructor () { @@ -81,11 +81,11 @@ export class AudioClip extends Asset { return super.destroy(); } - set _nativeAsset (clip: any) { - this._audio = clip; - if (clip) { - const ctor = this._getPlayer(clip); - this._player = new ctor({ clip, duration: this._duration, eventTarget: this }); + set _nativeAsset (nativeAudio: any) { + this._nativeAudio = nativeAudio; + if (nativeAudio) { + const ctor = this._getPlayer(nativeAudio); + this._player = new ctor({ nativeAudio, duration: this._duration, audioClip: this }); this.loaded = true; this.emit('load'); } else { @@ -97,7 +97,7 @@ export class AudioClip extends Asset { } get _nativeAsset () { - return this._audio; + return this._nativeAudio; } get loadMode () { diff --git a/cocos/audio/assets/player-dom.ts b/cocos/audio/assets/player-dom.ts index 59a491ff344..6c54e221145 100644 --- a/cocos/audio/assets/player-dom.ts +++ b/cocos/audio/assets/player-dom.ts @@ -36,7 +36,7 @@ export class AudioPlayerDOM extends AudioPlayer { protected _volume = 1; protected _loop = false; protected _oneShotOngoing = false; - protected _audio: HTMLAudioElement; + protected _nativeAudio: HTMLAudioElement; protected _cbRegistered = false; private _remove_cb: () => void; @@ -46,7 +46,7 @@ export class AudioPlayerDOM extends AudioPlayer { constructor (info: IAudioInfo) { super(info); - this._audio = info.clip; + this._nativeAudio = info.nativeAudio; this._remove_cb = () => { if (!this._cbRegistered) { return; } @@ -57,18 +57,18 @@ export class AudioPlayerDOM extends AudioPlayer { this._post_play = () => { this._state = PlayingState.PLAYING; - this._eventTarget.emit('started'); + this._clip.emit('started'); this._remove_cb(); // should remove callbacks after any success play }; this._post_gesture = () => { if (this._interrupted) { this._post_play(); this._interrupted = false; } - else { this._audio!.pause(); this._audio!.currentTime = 0; } + else { this._nativeAudio!.pause(); this._nativeAudio!.currentTime = 0; } }; this._on_gesture = () => { - if (!this._audio) { return; } - const promise = this._audio.play(); + if (!this._nativeAudio) { return; } + const promise = this._nativeAudio.play(); if (!promise) { // Chrome50/Firefox53 below // delay eval here to yield uniform behavior with other platforms this._state = PlayingState.PLAYING; @@ -79,14 +79,14 @@ export class AudioPlayerDOM extends AudioPlayer { this._remove_cb(); }; - this._audio.volume = this._volume; - this._audio.loop = this._loop; + this._nativeAudio.volume = this._volume; + this._nativeAudio.loop = this._loop; // callback on audio ended - this._audio.addEventListener('ended', () => { + this._nativeAudio.addEventListener('ended', () => { if (this._oneShotOngoing) { return; } this._state = PlayingState.STOPPED; - this._audio!.currentTime = 0; - this._eventTarget.emit('ended'); + this._nativeAudio!.currentTime = 0; + this._clip.emit('ended'); }); /* play & stop immediately after receiving a gesture so that we can freely invoke play() outside event listeners later */ @@ -96,9 +96,9 @@ export class AudioPlayerDOM extends AudioPlayer { } public play () { - if (!this._audio || this._state === PlayingState.PLAYING) { return; } + if (!this._nativeAudio || this._state === PlayingState.PLAYING) { return; } if (this._blocking) { this._interrupted = true; return; } - const promise = this._audio.play(); + const promise = this._nativeAudio.play(); if (!promise) { // delay eval here to yield uniform behavior with other platforms this._state = PlayingState.PLAYING; @@ -109,19 +109,19 @@ export class AudioPlayerDOM extends AudioPlayer { } public pause () { - if (!this._audio) { return; } + if (!this._nativeAudio) { return; } this._interrupted = false; if (this._state !== PlayingState.PLAYING) { return; } - this._audio.pause(); + this._nativeAudio.pause(); this._state = PlayingState.STOPPED; this._oneShotOngoing = false; } public stop () { - if (!this._audio) { return; } - this._audio.currentTime = 0; this._interrupted = false; + if (!this._nativeAudio) { return; } + this._nativeAudio.currentTime = 0; this._interrupted = false; if (this._state !== PlayingState.PLAYING) { return; } - this._audio.pause(); + this._nativeAudio.pause(); this._state = PlayingState.STOPPED; this._oneShotOngoing = false; } @@ -147,29 +147,29 @@ export class AudioPlayerDOM extends AudioPlayer { } public setCurrentTime (val: number) { - if (!this._audio) { return; } - this._audio.currentTime = clamp(val, 0, this._duration); + if (!this._nativeAudio) { return; } + this._nativeAudio.currentTime = clamp(val, 0, this._duration); } public getCurrentTime () { - return this._audio ? this._audio.currentTime : 0; + return this._nativeAudio ? this._nativeAudio.currentTime : 0; } public setVolume (val: number, immediate: boolean) { this._volume = val; /* note this won't work for ios devices, for there is just no way to set HTMLMediaElement's volume */ - if (this._audio) { this._audio.volume = val; } + if (this._nativeAudio) { this._nativeAudio.volume = val; } } public getVolume () { - if (this._audio) { return this._audio.volume; } + if (this._nativeAudio) { return this._nativeAudio.volume; } return this._volume; } public setLoop (val: boolean) { this._loop = val; - if (this._audio) { this._audio.loop = val; } + if (this._nativeAudio) { this._nativeAudio.loop = val; } } public getLoop () { @@ -177,7 +177,7 @@ export class AudioPlayerDOM extends AudioPlayer { } public destroy () { - if (this._audio) { this._audio.src = ''; } + if (this._nativeAudio) { this._nativeAudio.src = ''; } super.destroy(); } } diff --git a/cocos/audio/assets/player-web.ts b/cocos/audio/assets/player-web.ts index 9ead7a3a447..80bc1e376bc 100644 --- a/cocos/audio/assets/player-web.ts +++ b/cocos/audio/assets/player-web.ts @@ -41,7 +41,7 @@ export class AudioPlayerWeb extends AudioPlayer { protected _volume = 1; protected _loop = false; protected _currentTimer = 0; - protected _audio: AudioBuffer; + protected _nativeAudio: AudioBuffer; private _context: AudioContext; private _sourceNode: AudioBufferSourceNode; @@ -54,7 +54,7 @@ export class AudioPlayerWeb extends AudioPlayer { constructor (info: IAudioInfo) { super(info); - this._audio = info.clip; + this._nativeAudio = info.nativeAudio; this._context = audioSupport.context; this._sourceNode = this._context.createBufferSource(); @@ -73,7 +73,7 @@ export class AudioPlayerWeb extends AudioPlayer { } public play () { - if (!this._audio || this._state === PlayingState.PLAYING) { return; } + if (!this._nativeAudio || this._state === PlayingState.PLAYING) { return; } if (this._blocking || this._context.state !== 'running') { this._interrupted = true; if (('interrupted' === this._context.state as string || 'suspended' === this._context.state as string) @@ -103,12 +103,12 @@ export class AudioPlayerWeb extends AudioPlayer { } public playOneShot (volume = 1) { - if (!this._audio) { return; } + if (!this._nativeAudio) { return; } const gainNode = this._context.createGain(); gainNode.connect(this._context.destination); gainNode.gain.value = volume; const sourceNode = this._context.createBufferSource(); - sourceNode.buffer = this._audio; + sourceNode.buffer = this._nativeAudio; sourceNode.loop = false; sourceNode.connect(gainNode); sourceNode.start(); @@ -117,7 +117,7 @@ export class AudioPlayerWeb extends AudioPlayer { public setCurrentTime (val: number) { // throws InvalidState Error on some device if we don't do the clamp here // the serialized duration may not be accurate, use the actual duration first - this._offset = clamp(val, 0, this._audio && this._audio.duration || this._duration); + this._offset = clamp(val, 0, this._nativeAudio && this._nativeAudio.duration || this._duration); if (this._state !== PlayingState.PLAYING) { return; } this._doStop(); this._doPlay(); } @@ -160,7 +160,7 @@ export class AudioPlayerWeb extends AudioPlayer { private _doPlay () { this._state = PlayingState.PLAYING; this._sourceNode = this._context.createBufferSource(); - this._sourceNode.buffer = this._audio; + this._sourceNode.buffer = this._nativeAudio; this._sourceNode.loop = this._loop; this._sourceNode.connect(this._gainNode); this._startTime = this._context.currentTime; @@ -175,9 +175,9 @@ export class AudioPlayerWeb extends AudioPlayer { this._onEnded(); clearInterval(this._currentTimer); if (this._sourceNode.loop) { - this._currentTimer = window.setInterval(this._onEndedCB, this._audio.duration * 1000); + this._currentTimer = window.setInterval(this._onEndedCB, this._nativeAudio.duration * 1000); } - }, (this._audio.duration - this._offset) * 1000); + }, (this._nativeAudio.duration - this._offset) * 1000); } private _doStop () { @@ -188,7 +188,7 @@ export class AudioPlayerWeb extends AudioPlayer { private _playAndEmit () { this._sourceNode.start(0, this._offset); - this._eventTarget.emit('started'); + this._clip.emit('started'); this._startInvoked = true; } @@ -196,7 +196,7 @@ export class AudioPlayerWeb extends AudioPlayer { this._offset = 0; this._startTime = this._context.currentTime; if (this._sourceNode.loop) { return; } - this._eventTarget.emit('ended'); + this._clip.emit('ended'); this._state = PlayingState.STOPPED; } diff --git a/cocos/audio/assets/player.ts b/cocos/audio/assets/player.ts index dff360b0573..565b9982584 100644 --- a/cocos/audio/assets/player.ts +++ b/cocos/audio/assets/player.ts @@ -28,6 +28,7 @@ */ import { legacyCC } from '../../core/global-exports'; +import { AudioClip } from './clip'; export const PlayingState = { INITIALIZING: 0, @@ -36,15 +37,15 @@ export const PlayingState = { }; export interface IAudioInfo { - clip: any; + nativeAudio: any; duration: number; - eventTarget: any; + audioClip: AudioClip; } export abstract class AudioPlayer { protected _state = PlayingState.STOPPED; protected _duration = 0; - protected _eventTarget: any; + protected _clip: AudioClip; protected _onHide: Function; protected _onShow: Function; @@ -53,7 +54,7 @@ export abstract class AudioPlayer { constructor (info: IAudioInfo) { this._duration = info.duration; - this._eventTarget = info.eventTarget; + this._clip = info.audioClip; this._onHide = () => { this._blocking = true; if (this._state !== PlayingState.PLAYING) { return; } From bb2789e0e997e0a847ee45599ec9ec64aba8091f Mon Sep 17 00:00:00 2001 From: PP_Pro Date: Fri, 16 Oct 2020 15:33:32 +0800 Subject: [PATCH 10/15] support multiple playing for Dom Audio --- cocos/audio/assets/player-dom.ts | 28 ++++--------- cocos/audio/audio-downloader.ts | 71 ++++++++++++++++++-------------- 2 files changed, 47 insertions(+), 52 deletions(-) diff --git a/cocos/audio/assets/player-dom.ts b/cocos/audio/assets/player-dom.ts index 6c54e221145..c57fdeec30e 100644 --- a/cocos/audio/assets/player-dom.ts +++ b/cocos/audio/assets/player-dom.ts @@ -31,11 +31,11 @@ import { clamp } from '../../core/math/utils'; import { AudioPlayer, IAudioInfo, PlayingState } from './player'; import { legacyCC } from '../../core/global-exports'; +import { createDomAudio } from '../audio-downloader'; export class AudioPlayerDOM extends AudioPlayer { protected _volume = 1; protected _loop = false; - protected _oneShotOngoing = false; protected _nativeAudio: HTMLAudioElement; protected _cbRegistered = false; @@ -83,7 +83,6 @@ export class AudioPlayerDOM extends AudioPlayer { this._nativeAudio.loop = this._loop; // callback on audio ended this._nativeAudio.addEventListener('ended', () => { - if (this._oneShotOngoing) { return; } this._state = PlayingState.STOPPED; this._nativeAudio!.currentTime = 0; this._clip.emit('ended'); @@ -114,7 +113,6 @@ export class AudioPlayerDOM extends AudioPlayer { if (this._state !== PlayingState.PLAYING) { return; } this._nativeAudio.pause(); this._state = PlayingState.STOPPED; - this._oneShotOngoing = false; } public stop () { @@ -123,27 +121,15 @@ export class AudioPlayerDOM extends AudioPlayer { if (this._state !== PlayingState.PLAYING) { return; } this._nativeAudio.pause(); this._state = PlayingState.STOPPED; - this._oneShotOngoing = false; } public playOneShot (volume = 1) { - /* HTMLMediaElement doesn't support multiple playback at the - same time so here we fall back to re-start style approach */ - const clip = this._audio; - if (!clip) { return; } - clip.currentTime = 0; - clip.volume = volume; - if (this._oneShotOngoing) { return; } - clip.loop = false; - this._oneShotOngoing = true; - clip.play().then(() => { - clip.addEventListener('ended', () => { - clip.currentTime = 0; - clip.volume = this._volume; - clip.loop = this._loop; - this._oneShotOngoing = false; - }, { once: true }); - }).catch(() => { this._oneShotOngoing = false; }); + createDomAudio(this._nativeAudio.src).then(dom => { + dom.volume = volume; + dom.play(); + }, errMsg => { + console.error(errMsg); + }); } public setCurrentTime (val: number) { diff --git a/cocos/audio/audio-downloader.ts b/cocos/audio/audio-downloader.ts index b835d86e568..f0d0af60e0f 100644 --- a/cocos/audio/audio-downloader.ts +++ b/cocos/audio/audio-downloader.ts @@ -36,40 +36,49 @@ import { legacyCC } from '../core/global-exports'; const __audioSupport = sys.__audioSupport; const formatSupport = __audioSupport.format; -function loadDomAudio (item, callback) { - const dom = document.createElement('audio'); - dom.src = item.url; - - const clearEvent = () => { - clearTimeout(timer); - dom.removeEventListener('canplaythrough', success, false); - dom.removeEventListener('error', failure, false); +export function createDomAudio (url): Promise { + return new Promise((resolve, reject) => { + const dom = document.createElement('audio'); + dom.src = url; + + const clearEvent = () => { + clearTimeout(timer); + dom.removeEventListener('canplaythrough', success, false); + dom.removeEventListener('error', failure, false); + if (__audioSupport.USE_LOADER_EVENT) { + dom.removeEventListener(__audioSupport.USE_LOADER_EVENT, success, false); + } + }; + const timer = setTimeout(() => { + if (dom.readyState === 0) { + failure(); + } else { + success(); + } + }, 8000); + const success = () => { + clearEvent(); + resolve(dom); + }; + const failure = () => { + clearEvent(); + const message = 'load audio failure - ' + url; + reject(message); + }; + dom.addEventListener('canplaythrough', success, false); + dom.addEventListener('error', failure, false); if (__audioSupport.USE_LOADER_EVENT) { - dom.removeEventListener(__audioSupport.USE_LOADER_EVENT, success, false); - } - }; - const timer = setTimeout(() => { - if (dom.readyState === 0) { - failure(); - } else { - success(); + dom.addEventListener(__audioSupport.USE_LOADER_EVENT, success, false); } - }, 8000); - const success = () => { - clearEvent(); + }); +} + +function loadDomAudio (item, callback) { + createDomAudio(item.url).then(dom => { callback(null, dom); - }; - const failure = () => { - clearEvent(); - const message = 'load audio failure - ' + item.url; - log(message); - callback(message); - }; - dom.addEventListener('canplaythrough', success, false); - dom.addEventListener('error', failure, false); - if (__audioSupport.USE_LOADER_EVENT) { - dom.addEventListener(__audioSupport.USE_LOADER_EVENT, success, false); - } + }, errMsg => { + log(errMsg); + }); } function loadWebAudio (item, callback) { From e84837b61135045b4728f2e63c1b76433b4dad0b Mon Sep 17 00:00:00 2001 From: PP_Pro Date: Fri, 16 Oct 2020 15:35:15 +0800 Subject: [PATCH 11/15] support multiple backend Audio Manager --- cocos/audio/assets/audio-manager.ts | 25 +++++++++++++++++++++ cocos/audio/assets/player-dom.ts | 34 ++++++++++++++++++++++++++++ cocos/audio/assets/player-web.ts | 35 +++++++++++++++++++++++++++++ 3 files changed, 94 insertions(+) create mode 100644 cocos/audio/assets/audio-manager.ts diff --git a/cocos/audio/assets/audio-manager.ts b/cocos/audio/assets/audio-manager.ts new file mode 100644 index 00000000000..429dfd290e3 --- /dev/null +++ b/cocos/audio/assets/audio-manager.ts @@ -0,0 +1,25 @@ +import { legacyCC } from "cocos/core/global-exports"; + +export abstract class AudioManager { + protected _playingAudios: Array; + public static readonly maxAudioChannel: number = 3; + + constructor () { + this._playingAudios = []; + } + + public addPlaying (audio: AudioType) { + this._playingAudios.push(audio); + } + + public removePlaying (audio: AudioType) { + let index = this._playingAudios.indexOf(audio); + if (index > -1) { + this._playingAudios.splice(index, 1); + } + } + + public abstract discardOnePlayingIfNeeded (); +} + +legacyCC.internal.AudioManager = AudioManager; \ No newline at end of file diff --git a/cocos/audio/assets/player-dom.ts b/cocos/audio/assets/player-dom.ts index c57fdeec30e..c3c52cef8b6 100644 --- a/cocos/audio/assets/player-dom.ts +++ b/cocos/audio/assets/player-dom.ts @@ -32,8 +32,32 @@ import { clamp } from '../../core/math/utils'; import { AudioPlayer, IAudioInfo, PlayingState } from './player'; import { legacyCC } from '../../core/global-exports'; import { createDomAudio } from '../audio-downloader'; +import { AudioManager } from './audio-manager'; +type ManagedAudio = AudioPlayerDOM | HTMLAudioElement; +class AudioManagerDom extends AudioManager { + public discardOnePlayingIfNeeded() { + if (this._playingAudios.length < AudioManager.maxAudioChannel) { + return; + } + + // a played audio has a higher priority than a played shot + let audioToDiscard: ManagedAudio | undefined; + let oldestOneShotIndex = this._playingAudios.findIndex(audio => audio instanceof HTMLAudioElement); + if (oldestOneShotIndex > -1) { + audioToDiscard = this._playingAudios[oldestOneShotIndex] as HTMLAudioElement; + this._playingAudios.splice(oldestOneShotIndex, 1); + audioToDiscard.pause(); + audioToDiscard.src = ''; + } + else { + audioToDiscard = this._playingAudios.shift(); + (audioToDiscard).stop(); + } + } +} export class AudioPlayerDOM extends AudioPlayer { + protected static _manager: AudioManagerDom = new AudioManagerDom(); protected _volume = 1; protected _loop = false; protected _nativeAudio: HTMLAudioElement; @@ -59,6 +83,7 @@ export class AudioPlayerDOM extends AudioPlayer { this._state = PlayingState.PLAYING; this._clip.emit('started'); this._remove_cb(); // should remove callbacks after any success play + AudioPlayerDOM._manager.addPlaying(this); }; this._post_gesture = () => { @@ -86,6 +111,7 @@ export class AudioPlayerDOM extends AudioPlayer { this._state = PlayingState.STOPPED; this._nativeAudio!.currentTime = 0; this._clip.emit('ended'); + AudioPlayerDOM._manager.removePlaying(this); }); /* play & stop immediately after receiving a gesture so that we can freely invoke play() outside event listeners later */ @@ -97,6 +123,7 @@ export class AudioPlayerDOM extends AudioPlayer { public play () { if (!this._nativeAudio || this._state === PlayingState.PLAYING) { return; } if (this._blocking) { this._interrupted = true; return; } + AudioPlayerDOM._manager.discardOnePlayingIfNeeded(); const promise = this._nativeAudio.play(); if (!promise) { // delay eval here to yield uniform behavior with other platforms @@ -113,6 +140,7 @@ export class AudioPlayerDOM extends AudioPlayer { if (this._state !== PlayingState.PLAYING) { return; } this._nativeAudio.pause(); this._state = PlayingState.STOPPED; + AudioPlayerDOM._manager.removePlaying(this); } public stop () { @@ -121,12 +149,18 @@ export class AudioPlayerDOM extends AudioPlayer { if (this._state !== PlayingState.PLAYING) { return; } this._nativeAudio.pause(); this._state = PlayingState.STOPPED; + AudioPlayerDOM._manager.removePlaying(this); } public playOneShot (volume = 1) { createDomAudio(this._nativeAudio.src).then(dom => { + AudioPlayerDOM._manager.discardOnePlayingIfNeeded(); dom.volume = volume; dom.play(); + AudioPlayerDOM._manager.addPlaying(dom); + dom.addEventListener('ended', () => { + AudioPlayerDOM._manager.removePlaying(dom); + }); }, errMsg => { console.error(errMsg); }); diff --git a/cocos/audio/assets/player-web.ts b/cocos/audio/assets/player-web.ts index 80bc1e376bc..129398825ed 100644 --- a/cocos/audio/assets/player-web.ts +++ b/cocos/audio/assets/player-web.ts @@ -32,10 +32,36 @@ import { clamp } from '../../core/math/utils'; import { sys } from '../../core/platform/sys'; import { AudioPlayer, IAudioInfo, PlayingState } from './player'; import { legacyCC } from '../../core/global-exports'; +import { AudioManager } from './audio-manager'; const audioSupport = sys.__audioSupport; +type ManagedAudio = AudioPlayerWeb | AudioBufferSourceNode; + +class AudioManagerWeb extends AudioManager { + public discardOnePlayingIfNeeded() { + if (this._playingAudios.length < AudioManager.maxAudioChannel) { + return; + } + + // a played audio has a higher priority than a played shot + let audioToDiscard: ManagedAudio | undefined; + let oldestOneShotIndex = this._playingAudios.findIndex(audio => audio instanceof AudioBufferSourceNode); + if (oldestOneShotIndex > -1) { + audioToDiscard = this._playingAudios[oldestOneShotIndex]; + this._playingAudios.splice(oldestOneShotIndex, 1); + } + else { + audioToDiscard = this._playingAudios.shift(); + } + if (audioToDiscard) { + audioToDiscard.stop(); + } + } +} + export class AudioPlayerWeb extends AudioPlayer { + protected static _manager: AudioManagerWeb = new AudioManagerWeb; protected _startTime = 0; protected _offset = 0; protected _volume = 1; @@ -104,6 +130,7 @@ export class AudioPlayerWeb extends AudioPlayer { public playOneShot (volume = 1) { if (!this._nativeAudio) { return; } + AudioPlayerWeb._manager.discardOnePlayingIfNeeded(); const gainNode = this._context.createGain(); gainNode.connect(this._context.destination); gainNode.gain.value = volume; @@ -112,6 +139,10 @@ export class AudioPlayerWeb extends AudioPlayer { sourceNode.loop = false; sourceNode.connect(gainNode); sourceNode.start(); + AudioPlayerWeb._manager.addPlaying(sourceNode); + sourceNode.onended = () => { + AudioPlayerWeb._manager.removePlaying(sourceNode); + } } public setCurrentTime (val: number) { @@ -158,6 +189,7 @@ export class AudioPlayerWeb extends AudioPlayer { public destroy () { super.destroy(); } private _doPlay () { + AudioPlayerWeb._manager.discardOnePlayingIfNeeded(); this._state = PlayingState.PLAYING; this._sourceNode = this._context.createBufferSource(); this._sourceNode.buffer = this._nativeAudio; @@ -184,12 +216,14 @@ export class AudioPlayerWeb extends AudioPlayer { // stop can only be called after play if (this._startInvoked) { this._sourceNode.stop(); } else { legacyCC.director.off(legacyCC.Director.EVENT_AFTER_UPDATE, this._playAndEmit, this); } + AudioPlayerWeb._manager.removePlaying(this); } private _playAndEmit () { this._sourceNode.start(0, this._offset); this._clip.emit('started'); this._startInvoked = true; + AudioPlayerWeb._manager.addPlaying(this); } private _onEnded () { @@ -198,6 +232,7 @@ export class AudioPlayerWeb extends AudioPlayer { if (this._sourceNode.loop) { return; } this._clip.emit('ended'); this._state = PlayingState.STOPPED; + AudioPlayerWeb._manager.removePlaying(this); } private _onGestureProceed () { From 756f3b0c4f601dfa67097fdcebbb0711a40cd3f5 Mon Sep 17 00:00:00 2001 From: PP_Pro Date: Fri, 16 Oct 2020 17:42:54 +0800 Subject: [PATCH 12/15] update --- cocos/audio/assets/audio-manager.ts | 2 +- cocos/audio/assets/player-dom.ts | 2 +- cocos/audio/assets/player-web.ts | 4 +--- 3 files changed, 3 insertions(+), 5 deletions(-) diff --git a/cocos/audio/assets/audio-manager.ts b/cocos/audio/assets/audio-manager.ts index 429dfd290e3..233eae76354 100644 --- a/cocos/audio/assets/audio-manager.ts +++ b/cocos/audio/assets/audio-manager.ts @@ -1,4 +1,4 @@ -import { legacyCC } from "cocos/core/global-exports"; +import { legacyCC } from "../../core/global-exports"; export abstract class AudioManager { protected _playingAudios: Array; diff --git a/cocos/audio/assets/player-dom.ts b/cocos/audio/assets/player-dom.ts index c3c52cef8b6..506285e7e9f 100644 --- a/cocos/audio/assets/player-dom.ts +++ b/cocos/audio/assets/player-dom.ts @@ -52,7 +52,7 @@ class AudioManagerDom extends AudioManager { } else { audioToDiscard = this._playingAudios.shift(); - (audioToDiscard).stop(); + (audioToDiscard)?.stop(); } } } diff --git a/cocos/audio/assets/player-web.ts b/cocos/audio/assets/player-web.ts index 129398825ed..05fcbf0899b 100644 --- a/cocos/audio/assets/player-web.ts +++ b/cocos/audio/assets/player-web.ts @@ -54,9 +54,7 @@ class AudioManagerWeb extends AudioManager { else { audioToDiscard = this._playingAudios.shift(); } - if (audioToDiscard) { - audioToDiscard.stop(); - } + audioToDiscard?.stop(); } } From cff046f8efc49053e6b06fcb3e53cd085dd5738c Mon Sep 17 00:00:00 2001 From: PP_Pro Date: Fri, 16 Oct 2020 17:50:59 +0800 Subject: [PATCH 13/15] move restart playing logic into player --- cocos/audio/assets/clip.ts | 13 +------------ cocos/audio/assets/player-dom.ts | 10 +++++++++- cocos/audio/assets/player-web.ts | 10 +++++++++- 3 files changed, 19 insertions(+), 14 deletions(-) diff --git a/cocos/audio/assets/clip.ts b/cocos/audio/assets/clip.ts index 14f404a9c71..6f0a8b8ba16 100644 --- a/cocos/audio/assets/clip.ts +++ b/cocos/audio/assets/clip.ts @@ -108,18 +108,7 @@ export class AudioClip extends Asset { return this._player ? this._player.getState() : PlayingState.INITIALIZING; } - public play () { - if (this._player) { - if (this.state === PlayingState.PLAYING) { - /* sometimes there is no way to update the playing state - especially when player unplug earphones and the audio automatically stops - so we need to force updating the playing state by pausing audio */ - this._player.pause(); - this._player.setCurrentTime(0); - } - this._player.play(); - } - } + public play () { if (this._player) { this._player.play(); } } public pause () { if (this._player) { this._player.pause(); } } public stop () { if (this._player) { this._player.stop(); } } public playOneShot (volume: number) { if (this._player) { this._player.playOneShot(volume); } } diff --git a/cocos/audio/assets/player-dom.ts b/cocos/audio/assets/player-dom.ts index 506285e7e9f..2b37a594184 100644 --- a/cocos/audio/assets/player-dom.ts +++ b/cocos/audio/assets/player-dom.ts @@ -121,8 +121,16 @@ export class AudioPlayerDOM extends AudioPlayer { } public play () { - if (!this._nativeAudio || this._state === PlayingState.PLAYING) { return; } + if (!this._nativeAudio) { return; } if (this._blocking) { this._interrupted = true; return; } + if (this._state === PlayingState.PLAYING) { + /* sometimes there is no way to update the playing state + especially when player unplug earphones and the audio automatically stops + so we need to force updating the playing state by pausing audio */ + this.pause(); + // restart if already playing + this.setCurrentTime(0); + } AudioPlayerDOM._manager.discardOnePlayingIfNeeded(); const promise = this._nativeAudio.play(); if (!promise) { diff --git a/cocos/audio/assets/player-web.ts b/cocos/audio/assets/player-web.ts index 05fcbf0899b..9b261d25ebf 100644 --- a/cocos/audio/assets/player-web.ts +++ b/cocos/audio/assets/player-web.ts @@ -97,7 +97,15 @@ export class AudioPlayerWeb extends AudioPlayer { } public play () { - if (!this._nativeAudio || this._state === PlayingState.PLAYING) { return; } + if (!this._nativeAudio) { return; } + if (this._state === PlayingState.PLAYING) { + /* sometimes there is no way to update the playing state + especially when player unplug earphones and the audio automatically stops + so we need to force updating the playing state by pausing audio */ + this.pause(); + // restart if already playing + this.setCurrentTime(0); + } if (this._blocking || this._context.state !== 'running') { this._interrupted = true; if (('interrupted' === this._context.state as string || 'suspended' === this._context.state as string) From ffb2f9fde527bc0cf0adc3a431d35829b5399e33 Mon Sep 17 00:00:00 2001 From: PP_Pro Date: Fri, 16 Oct 2020 17:55:40 +0800 Subject: [PATCH 14/15] update maxAudioChannel --- cocos/audio/assets/audio-manager.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cocos/audio/assets/audio-manager.ts b/cocos/audio/assets/audio-manager.ts index 233eae76354..1b16f1dc444 100644 --- a/cocos/audio/assets/audio-manager.ts +++ b/cocos/audio/assets/audio-manager.ts @@ -2,7 +2,7 @@ import { legacyCC } from "../../core/global-exports"; export abstract class AudioManager { protected _playingAudios: Array; - public static readonly maxAudioChannel: number = 3; + public static readonly maxAudioChannel: number = 24; constructor () { this._playingAudios = []; From cc5f8c3ec6a7435de34707c9b568e203841be2be Mon Sep 17 00:00:00 2001 From: PP_Pro Date: Fri, 16 Oct 2020 18:00:09 +0800 Subject: [PATCH 15/15] details --- cocos/audio/assets/player-web.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cocos/audio/assets/player-web.ts b/cocos/audio/assets/player-web.ts index 9b261d25ebf..e3d6e845df2 100644 --- a/cocos/audio/assets/player-web.ts +++ b/cocos/audio/assets/player-web.ts @@ -59,7 +59,7 @@ class AudioManagerWeb extends AudioManager { } export class AudioPlayerWeb extends AudioPlayer { - protected static _manager: AudioManagerWeb = new AudioManagerWeb; + protected static _manager: AudioManagerWeb = new AudioManagerWeb(); protected _startTime = 0; protected _offset = 0; protected _volume = 1;