Skip to content

Commit

Permalink
refine: erase unneed audio operation & fix potential bug (#15884)
Browse files Browse the repository at this point in the history
* refine: erase unneeded audio operation & fix some potential bug
  • Loading branch information
bofeng-song authored Aug 10, 2023
1 parent e1caa28 commit 6b80095
Show file tree
Hide file tree
Showing 3 changed files with 106 additions and 22 deletions.
67 changes: 51 additions & 16 deletions cocos/audio/audio-source.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,18 @@ enum AudioSourceEventType {
ENDED = 'ended',
}

enum AudioOperationType {
PLAY = 'play',
STOP = 'stop',
PAUSE = 'pause',
SEEK = 'seek'
}

interface AudioOperationInfo {
op: AudioOperationType;
params: any[] | null;
}

/**
* @en
* A representation of a single audio source, <br>
Expand Down Expand Up @@ -68,10 +80,10 @@ export class AudioSource extends Component {
@serializable
protected _volume = 1;

private _cachedCurrentTime = 0;
private _cachedCurrentTime = -1;

// An operation queue to store the operations before loading the AudioPlayer.
private _operationsBeforeLoading: string[] = [];
private _operationsBeforeLoading: AudioOperationInfo[] = [];
private _isLoaded = false;

private _lastSetClip: AudioClip | null = null;
Expand Down Expand Up @@ -113,6 +125,7 @@ export class AudioSource extends Component {
return;
}
if (!clip._nativeAsset) {
// eslint-disable-next-line no-console
console.error('Invalid audio clip');
return;
}
Expand All @@ -138,6 +151,7 @@ export class AudioSource extends Component {
this._player = player;
this._syncStates();
this.node?.emit(_LOADED_EVENT);
// eslint-disable-next-line @typescript-eslint/no-empty-function
}).catch((e) => {});
}

Expand Down Expand Up @@ -178,7 +192,9 @@ export class AudioSource extends Component {
@tooltip('i18n:audio.loop')
set loop (val) {
this._loop = val;
this._player && (this._player.loop = val);
if (this._player) {
this._player.loop = val;
}
}
get loop (): boolean {
return this._loop;
Expand Down Expand Up @@ -214,6 +230,7 @@ export class AudioSource extends Component {
@range([0.0, 1.0])
@tooltip('i18n:audio.volume')
set volume (val) {
// eslint-disable-next-line no-console
if (Number.isNaN(val)) { console.warn('illegal audio volume!'); return; }
val = clamp(val, 0, 1);
if (this._player) {
Expand Down Expand Up @@ -275,6 +292,7 @@ export class AudioSource extends Component {
public getPCMData (channelIndex: number): Promise<AudioPCMDataView | undefined> {
return new Promise((resolve) => {
if (channelIndex !== 0 && channelIndex !== 1) {
// eslint-disable-next-line no-console
console.warn('Only support channel index 0 or 1 to get buffer');
resolve(undefined);
return;
Expand Down Expand Up @@ -345,13 +363,14 @@ export class AudioSource extends Component {
*/
public play (): void {
if (!this._isLoaded && this.clip) {
this._operationsBeforeLoading.push('play');
this._operationsBeforeLoading.push({ op: AudioOperationType.PLAY, params: null });
return;
}
this._registerListener();
audioManager.discardOnePlayingIfNeeded();
// Replay if the audio is playing
if (this.state === AudioState.PLAYING) {
// eslint-disable-next-line @typescript-eslint/no-empty-function
this._player?.stop().catch((e) => {});
}
const player = this._player;
Expand All @@ -373,9 +392,10 @@ export class AudioSource extends Component {
*/
public pause (): void {
if (!this._isLoaded && this.clip) {
this._operationsBeforeLoading.push('pause');
this._operationsBeforeLoading.push({ op: AudioOperationType.PAUSE, params: null });
return;
}
// eslint-disable-next-line @typescript-eslint/no-empty-function
this._player?.pause().catch((e) => {});
}

Expand All @@ -387,10 +407,11 @@ export class AudioSource extends Component {
*/
public stop (): void {
if (!this._isLoaded && this.clip) {
this._operationsBeforeLoading.push('stop');
this._operationsBeforeLoading.push({ op: AudioOperationType.STOP, params: null });
return;
}
if (this._player) {
// eslint-disable-next-line @typescript-eslint/no-empty-function
this._player.stop().catch((e) => {});
audioManager.removePlaying(this._player);
}
Expand All @@ -406,6 +427,7 @@ export class AudioSource extends Component {
*/
public playOneShot (clip: AudioClip, volumeScale = 1): void {
if (!clip._nativeAsset) {
// eslint-disable-next-line no-console
console.error('Invalid audio clip');
return;
}
Expand All @@ -428,15 +450,22 @@ export class AudioSource extends Component {
}

protected _syncStates (): void {
if (!this._player) { return; }
this._player.seek(this._cachedCurrentTime).then((): void => {
if (this._player) {
this._player.loop = this._loop;
this._player.volume = this._volume;
this._operationsBeforeLoading.forEach((opName): void => { this[opName]?.(); });
this._operationsBeforeLoading.length = 0;
}
}).catch((e): void => {});
if (this._player) {
this._player.loop = this._loop;
this._player.volume = this._volume;
this._operationsBeforeLoading.forEach((opInfo): void => {
if (opInfo.op === AudioOperationType.SEEK) {
this._cachedCurrentTime = (opInfo.params && opInfo.params[0]) as number;
if (this._player) {
// eslint-disable-next-line @typescript-eslint/no-empty-function
this._player.seek(this._cachedCurrentTime).catch((e): void => {});
}
} else {
this[opInfo.op]?.();
}
});
this._operationsBeforeLoading.length = 0;
}
}

/**
Expand All @@ -447,9 +476,15 @@ export class AudioSource extends Component {
* @param num playback time to jump to.
*/
set currentTime (num: number) {
// eslint-disable-next-line no-console
if (Number.isNaN(num)) { console.warn('illegal audio time!'); return; }
num = clamp(num, 0, this.duration);
if (!this._isLoaded && this.clip) {
this._operationsBeforeLoading.push({ op: AudioOperationType.SEEK, params: [num] });
return;
}
this._cachedCurrentTime = num;
// eslint-disable-next-line @typescript-eslint/no-empty-function
this._player?.seek(this._cachedCurrentTime).catch((e): void => {});
}

Expand All @@ -460,7 +495,7 @@ export class AudioSource extends Component {
* 以秒为单位获取当前播放时间。
*/
get currentTime (): number {
return this._player ? this._player.currentTime : this._cachedCurrentTime;
return this._player ? this._player.currentTime : (this._cachedCurrentTime < 0 ? 0 : this._cachedCurrentTime);
}

/**
Expand Down
25 changes: 20 additions & 5 deletions pal/audio/minigame/player-minigame.ts
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,7 @@ export class AudioPlayerMinigame implements OperationQueueable {
this._state = AudioState.PLAYING;
eventTarget.emit(AudioEvent.PLAYED);
if (this._needSeek) {
// eslint-disable-next-line @typescript-eslint/no-empty-function
this.seek(this._cacheTime).catch((e) => {});
}
};
Expand Down Expand Up @@ -168,6 +169,7 @@ export class AudioPlayerMinigame implements OperationQueueable {
if (this._needSeek) {
this._needSeek = false;
if (this._cacheTime.toFixed(3) !== this._innerAudioContext.currentTime.toFixed(3)) {
// eslint-disable-next-line @typescript-eslint/no-empty-function
this.seek(this._cacheTime).catch((e) => {});
} else {
this._needSeek = false;
Expand All @@ -189,7 +191,7 @@ export class AudioPlayerMinigame implements OperationQueueable {
['Play', 'Pause', 'Stop', 'Seeked', 'Ended'].forEach((event) => {
this._offEvent(event);
});
// NOTE: innewAudioContext might not stop the audio playing, have to call it explicitly.
// NOTE: innerAudioContext might not stop the audio playing, have to call it explicitly.
this._innerAudioContext.stop();
this._innerAudioContext.destroy();
// NOTE: Type 'null' is not assignable to type 'InnerAudioContext'
Expand All @@ -202,6 +204,7 @@ export class AudioPlayerMinigame implements OperationQueueable {
this._state = AudioState.INTERRUPTED;
this._readyToHandleOnShow = true;
this._eventTarget.emit(AudioEvent.INTERRUPTION_BEGIN);
// eslint-disable-next-line @typescript-eslint/no-empty-function
}).catch((e) => {});
}
}
Expand All @@ -214,6 +217,7 @@ export class AudioPlayerMinigame implements OperationQueueable {
if (this._state === AudioState.INTERRUPTED) {
this.play().then(() => {
this._eventTarget.emit(AudioEvent.INTERRUPTION_END);
// eslint-disable-next-line @typescript-eslint/no-empty-function
}).catch((e) => {});
}
this._readyToHandleOnShow = false;
Expand All @@ -235,6 +239,7 @@ export class AudioPlayerMinigame implements OperationQueueable {
return new Promise((resolve) => {
AudioPlayerMinigame.loadNative(url).then((innerAudioContext) => {
resolve(new AudioPlayerMinigame(innerAudioContext as InnerAudioContext));
// eslint-disable-next-line @typescript-eslint/no-empty-function
}).catch((e) => {});
});
}
Expand All @@ -259,6 +264,7 @@ export class AudioPlayerMinigame implements OperationQueueable {
clearTimeout(timer);
// eslint-disable-next-line no-console
console.error('failed to load innerAudioContext');
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
reject(new Error(err));
}
innerAudioContext.onCanplay(success);
Expand All @@ -270,6 +276,7 @@ export class AudioPlayerMinigame implements OperationQueueable {
return new Promise((resolve, reject) => {
AudioPlayerMinigame.loadNative(url).then((innerAudioContext) => {
// HACK: AudioPlayer should be a friend class in OneShotAudio
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
resolve(new (OneShotAudioMinigame as any)(innerAudioContext, volume));
}).catch(reject);
});
Expand Down Expand Up @@ -318,12 +325,15 @@ export class AudioPlayerMinigame implements OperationQueueable {
if (this._state === AudioState.PLAYING && !this._seeking) {
time = clamp(time, 0, this.duration);
this._seeking = true;
this._eventTarget.once(AudioEvent.SEEKED, resolve);
this._innerAudioContext.seek(time);
} else if (this._cacheTime !== time) { // Skip the invalid seek
this._cacheTime = time;
this._needSeek = true;
} else {
if (this._cacheTime !== time) { // Skip the invalid seek
this._cacheTime = time;
this._needSeek = true;
}
resolve();
}
resolve();
});
}

Expand All @@ -350,6 +360,11 @@ export class AudioPlayerMinigame implements OperationQueueable {
@enqueueOperation
stop (): Promise<void> {
return new Promise((resolve) => {
if (AudioState.INIT === this._state) {
this._resetSeekCache();
resolve();
return;
}
this._eventTarget.once(AudioEvent.STOPPED, resolve);
this._innerAudioContext.stop();
});
Expand Down
36 changes: 35 additions & 1 deletion pal/audio/operation-queue.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import { EventTarget } from '../../cocos/core';

type OperationMethod = (...args: any[]) => Promise<void>;
export interface OperationInfo {
op: string;
id: number;
func: OperationMethod;
args: any[],
Expand All @@ -37,18 +38,48 @@ export interface OperationQueueable {
_eventTarget: EventTarget;
}

function removeUnneededCalls (instance: OperationQueueable): void {
const size = instance._operationQueue.length;
const tmpQueue = instance._operationQueue.slice();
const reserveOps: OperationInfo[] = [];
let seekSearched = false;
for (let i = size - 1; i >= 0; i--) {
const opInfo = tmpQueue[i];
if (opInfo.op === 'stop') {
reserveOps.push(opInfo);
break;
} else if (opInfo.op === 'seek') {
if (!seekSearched) {
reserveOps.push(opInfo);
seekSearched = true;
}
} else if (seekSearched) {
reserveOps.push(opInfo);
break;
} else if (reserveOps.length === 0) {
reserveOps.push(opInfo);
}
}
instance._operationQueue = reserveOps.reverse();
}

let operationId = 0;
function _tryCallingRecursively<T extends OperationQueueable> (target: T, opInfo: OperationInfo): void {
if (opInfo.invoking) {
return;
}
opInfo.invoking = true;
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
opInfo.func.call(target, ...opInfo.args).then(() => {
opInfo.invoking = false;
target._operationQueue.shift();
target._eventTarget.emit(opInfo.id.toString());
removeUnneededCalls(target);
const nextOpInfo: OperationInfo = target._operationQueue[0];
nextOpInfo && _tryCallingRecursively(target, nextOpInfo);
if (nextOpInfo) {
_tryCallingRecursively(target, nextOpInfo);
}
// eslint-disable-next-line @typescript-eslint/no-empty-function
}).catch((e) => {});
}

Expand All @@ -64,14 +95,17 @@ function _tryCallingRecursively<T extends OperationQueueable> (target: T, opInfo
* It means that, for example, you can't call stop in the implementation of play operation,
* because that would cause the operation deadlock.
*/
// eslint-disable-next-line max-len
export function enqueueOperation<T extends OperationQueueable> (target: T, propertyKey: string, descriptor: TypedPropertyDescriptor<OperationMethod>): void {
const originalOperation = descriptor.value!;
// eslint-disable-next-line func-names
descriptor.value = function (...args: any[]): Promise<void> {
return new Promise((resolve) => {
const id = operationId++;
const instance = this as OperationQueueable;
// enqueue operation
instance._operationQueue.push({
op: propertyKey,
id,
func: originalOperation,
args,
Expand Down

0 comments on commit 6b80095

Please sign in to comment.