From 4613a787590d14d823231d18884eec8de9abc363 Mon Sep 17 00:00:00 2001 From: JujieX Date: Sat, 3 Jun 2023 19:44:01 +0800 Subject: [PATCH 01/21] feat: setup audio --- packages/core/src/asset/AssetType.ts | 4 +- packages/core/src/audio/AudioClip.ts | 35 ++++ packages/core/src/audio/AudioListener.ts | 23 +++ packages/core/src/audio/AudioManager.ts | 25 +++ packages/core/src/audio/AudioSource.ts | 207 +++++++++++++++++++++++ packages/core/src/audio/index.ts | 4 + packages/core/src/index.ts | 1 + packages/loader/src/AudioLoader.ts | 14 ++ packages/loader/src/index.ts | 1 + 9 files changed, 313 insertions(+), 1 deletion(-) create mode 100644 packages/core/src/audio/AudioClip.ts create mode 100644 packages/core/src/audio/AudioListener.ts create mode 100644 packages/core/src/audio/AudioManager.ts create mode 100644 packages/core/src/audio/AudioSource.ts create mode 100644 packages/core/src/audio/index.ts create mode 100644 packages/loader/src/AudioLoader.ts diff --git a/packages/core/src/asset/AssetType.ts b/packages/core/src/asset/AssetType.ts index 0112491e10..a09f6aed8b 100644 --- a/packages/core/src/asset/AssetType.ts +++ b/packages/core/src/asset/AssetType.ts @@ -48,5 +48,7 @@ export enum AssetType { /** Font. */ Font = "Font", /** Source Font, include ttf、 otf and woff. */ - SourceFont = "SourceFont" + SourceFont = "SourceFont", + /** AudioClip, inclue ogg, wav and mp3 */ + Audio = "Audio" } diff --git a/packages/core/src/audio/AudioClip.ts b/packages/core/src/audio/AudioClip.ts new file mode 100644 index 0000000000..a326c26277 --- /dev/null +++ b/packages/core/src/audio/AudioClip.ts @@ -0,0 +1,35 @@ +/** + * Audio Clip + */ +export class AudioClip { + /** the name of clip */ + name: string; + private _audioBuffer: AudioBuffer; + + /** the number of discrete audio channels */ + get channels(): Readonly { + return this._audioBuffer.numberOfChannels; + } + + /** the sample rate, in samples per second */ + get sampleRate(): Readonly { + return this._audioBuffer.sampleRate; + } + + /** the duration, in seconds */ + get duration(): Readonly { + return this._audioBuffer.duration; + } + + constructor(name?: string) { + this.name = name; + } + /** get the clip's audio buffer */ + public getData(): AudioBuffer { + return this._audioBuffer; + } + /** set audio buffer for the clip */ + public setData(value: AudioBuffer) { + this._audioBuffer = value; + } +} diff --git a/packages/core/src/audio/AudioListener.ts b/packages/core/src/audio/AudioListener.ts new file mode 100644 index 0000000000..1d5be9607a --- /dev/null +++ b/packages/core/src/audio/AudioListener.ts @@ -0,0 +1,23 @@ +import { Component } from "../Component"; +import { Entity } from "../Entity"; +import { AudioManager } from "./AudioManager"; + +/** + * Audio Listener + * only one per scene + */ +export class AudioListener extends Component { + /** + * @internal + */ + constructor(entity: Entity) { + super(entity); + const gain = AudioManager.context.createGain(); + gain.connect(AudioManager.context.destination); + AudioManager.listener = gain; + } + + protected override _onDestroy(): void { + AudioManager.listener = null + } +} diff --git a/packages/core/src/audio/AudioManager.ts b/packages/core/src/audio/AudioManager.ts new file mode 100644 index 0000000000..eca67e7d2a --- /dev/null +++ b/packages/core/src/audio/AudioManager.ts @@ -0,0 +1,25 @@ +/** + * @internal + * Audio Manager + */ +export class AudioManager { + /** @internal */ + private static _context: AudioContext; + /** @internal */ + private static _listener: GainNode; + + static get context():AudioContext { + if (!AudioManager._context) { + AudioManager._context = new window.AudioContext(); + } + return AudioManager._context; + } + + static get listener(): GainNode { + return AudioManager._listener; + } + + static set listener(value: GainNode) { + AudioManager._listener = value; + } +} diff --git a/packages/core/src/audio/AudioSource.ts b/packages/core/src/audio/AudioSource.ts new file mode 100644 index 0000000000..50780eece8 --- /dev/null +++ b/packages/core/src/audio/AudioSource.ts @@ -0,0 +1,207 @@ +import { MathUtil } from "@galacean/engine-math"; +import { Component } from "../Component"; +import { Entity } from "../Entity"; +import { AudioClip } from "./AudioClip"; +import { AudioManager } from "./AudioManager"; + +/** + * Audio Source Component + */ +export class AudioSource extends Component { + /** Whether the sound is playing or not */ + isPlaying: Readonly; + /** whether the sound must be replayed when the end is reached. Default false */ + loop: boolean = false; + /** Fired when the sound has stopped playing, either because it's reached a predetermined stop time, the full duration of the audio has been performed, or because the entire audio has been played. */ + onPlayEnd: () => any; + + private _clip: AudioClip; + private _context: AudioContext; + private _gainNode: GainNode; + private _sourceNode: AudioBufferSourceNode; + + private _startTime: number = 0; + private _pausedTime: number = null; + private _endTime: number = null; + private _duration: number = null; + private _absoluteStartTime: number; + + private _currRepeatTimes: number = 1; + private _repeatTimes: number = 1; + private _volume: number = 1; + private _mute: boolean = false; + private _playbackRate: number = 1; + + /** The audio asset to be played */ + get clip(): AudioClip { + return this._clip; + } + + set clip(value: AudioClip) { + this._clip = value; + } + + /** the volume, should be positive. Default 1*/ + get volume(): number { + return this._volume; + } + + set volume(value: number) { + this._volume = value; + if (this.isPlaying) { + this._gainNode.gain.setValueAtTime(value, this._context.currentTime); + } + } + + /** Speed factor at which the sound will be played. Default 1 */ + get playbackRate(): number { + return this._playbackRate; + } + + set playbackRate(value: number) { + this._playbackRate = value; + if (this.isPlaying) { + this._sourceNode.playbackRate.value = this._playbackRate; + } + } + + /** whether is muted or not */ + get mute(): boolean { + return this._mute; + } + + set mute(value: boolean) { + this._mute = value; + if (value) { + this.volume = 0; + } + } + + /** repeat times, default 1, should be positive integer */ + get repeatTimes(): number { + return this._repeatTimes; + } + + set repeatTimes(value: number) { + this._repeatTimes = MathUtil.clamp(Math.abs(Math.ceil(value)), 1, Infinity); + this._currRepeatTimes = this._repeatTimes; + } + + /** The time, in seconds, at which the sound should begin to play. Default 0 */ + get startTime(): number { + return this._startTime; + } + + set startTime(value: number) { + this._startTime = value; + } + + /** The time, in seconds, at which the sound should stop to play. */ + get endTime(): number { + return this._endTime; + } + + set endTime(value: number) { + this._endTime = value; + this._duration = this._endTime - this._startTime; + } + + /** Current playback progress, in seconds */ + get position(): number { + if (this.isPlaying) { + return this._pausedTime + ? this.engine.time.elapsedTime - this._absoluteStartTime + this._pausedTime + : this.engine.time.elapsedTime - this._absoluteStartTime + this.startTime; + } + return 0; + } + + /** @internal */ + constructor(entity: Entity) { + super(entity); + this._onPlayEnd = this._onPlayEnd.bind(this); + + this._context = AudioManager.context; + this._gainNode = AudioManager.context.createGain(); + this._gainNode.connect(AudioManager.listener); + } + + /** play the sound from the very beginning */ + public play() { + if (!this._clip || !this.clip.duration || this.isPlaying) return; + if (this.startTime > this._clip.duration || this.startTime < 0) return; + if (this._duration && this._duration < 0) return; + + this._pausedTime = null; + this._play(this.startTime, this._duration); + } + + /** stop play the sound */ + public stop() { + if (this._sourceNode && this.isPlaying) { + this._sourceNode.stop(); + this._currRepeatTimes = 1; + } + } + /** pause playing */ + public pause() { + if (this._sourceNode && this.isPlaying) { + this._pausedTime = this.position; + + this.isPlaying = false; + + this._sourceNode.disconnect(); + this._sourceNode.onended = null; + this._sourceNode = null; + } + } + + /** resume playing, if is paused */ + public resume() { + if (!this.isPlaying && this._pausedTime) { + const duration = this.endTime ? this.endTime - this._pausedTime : null; + this._play(this._pausedTime, duration); + } + } + + private _play(startTime: number, duration: number | null) { + const source = this._context.createBufferSource(); + source.buffer = this._clip.getData(); + source.onended = this._onPlayEnd; + source.playbackRate.value = this._playbackRate; + + if (this.loop) { + source.loop = true; + source.loopStart = startTime; + if (this.endTime) { + source.loopEnd = this.endTime; + } + } + + this._gainNode.gain.setValueAtTime(this._volume, 0); + source.connect(this._gainNode); + + duration ? source.start(0, startTime, duration) : source.start(0, startTime); + + this._absoluteStartTime = this.engine.time.elapsedTime; + this._sourceNode = source; + this.isPlaying = true; + } + + private _onPlayEnd() { + if (!this.isPlaying) return; + this.isPlaying = false; + if (this._currRepeatTimes === 1) { + this._currRepeatTimes = this._repeatTimes; + this._sourceNode.disconnect(); + this._sourceNode = null; + this._pausedTime = null; + this.onPlayEnd && this.onPlayEnd(); + return; + } + if (this._currRepeatTimes > 1) { + this._currRepeatTimes--; + this.play(); + } + } +} diff --git a/packages/core/src/audio/index.ts b/packages/core/src/audio/index.ts new file mode 100644 index 0000000000..b00c40d3a8 --- /dev/null +++ b/packages/core/src/audio/index.ts @@ -0,0 +1,4 @@ +export { AudioManager } from "./AudioManager"; +export { AudioClip } from "./AudioClip"; +export { AudioListener } from "./AudioListener"; +export { AudioSource } from "./AudioSource"; diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 35f051cbc8..dfa59313b9 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -55,6 +55,7 @@ export * from "./clone/CloneManager"; export * from "./renderingHardwareInterface/index"; export * from "./physics/index"; export * from "./Utils"; +export * from './audio' // Export for CanvasRenderer plugin. export { Basic2DBatcher } from "./RenderPipeline/Basic2DBatcher"; diff --git a/packages/loader/src/AudioLoader.ts b/packages/loader/src/AudioLoader.ts new file mode 100644 index 0000000000..abc6a96287 --- /dev/null +++ b/packages/loader/src/AudioLoader.ts @@ -0,0 +1,14 @@ +import { resourceLoader, Loader, AssetPromise, AssetType, LoadItem, AudioManager } from "@galacean/engine-core"; + +@resourceLoader(AssetType.Audio, ["mp3", "ogg", "wav"], false) +class AudioLoader extends Loader { + load(item: LoadItem): AssetPromise { + return new AssetPromise((resolve) => { + this.request(item.url, { type: "arraybuffer" }).then((arrayBuffer) => { + AudioManager.context.decodeAudioData(arrayBuffer).then((result) => { + resolve(result); + }); + }); + }); + } +} diff --git a/packages/loader/src/index.ts b/packages/loader/src/index.ts index 5e593ad254..bace872f94 100644 --- a/packages/loader/src/index.ts +++ b/packages/loader/src/index.ts @@ -15,6 +15,7 @@ import "./SpriteLoader"; import "./Texture2DLoader"; import "./TextureCubeLoader"; import "./AnimationClipLoader"; +import "./AudioLoader" export { parseSingleKTX } from "./compressed-texture"; export type { GLTFParams } from "./GLTFLoader"; From 4a94c9d8557140aa1f3077f7eeeb00cafacf6398 Mon Sep 17 00:00:00 2001 From: JujieX Date: Wed, 7 Jun 2023 18:17:24 +0800 Subject: [PATCH 02/21] fix: update code --- packages/core/src/audio/AudioClip.ts | 39 +++-- packages/core/src/audio/AudioListener.ts | 12 +- packages/core/src/audio/AudioManager.ts | 39 ++++- packages/core/src/audio/AudioSource.ts | 178 ++++++++++++++--------- packages/loader/src/AudioLoader.ts | 13 +- 5 files changed, 193 insertions(+), 88 deletions(-) diff --git a/packages/core/src/audio/AudioClip.ts b/packages/core/src/audio/AudioClip.ts index a326c26277..1ffb83a6ab 100644 --- a/packages/core/src/audio/AudioClip.ts +++ b/packages/core/src/audio/AudioClip.ts @@ -1,35 +1,52 @@ +import { Engine } from "../Engine"; +import { ReferResource } from "../asset/ReferResource"; + /** * Audio Clip */ -export class AudioClip { +export class AudioClip extends ReferResource { /** the name of clip */ name: string; + private _audioBuffer: AudioBuffer; - /** the number of discrete audio channels */ + /** + * the number of discrete audio channels + */ get channels(): Readonly { return this._audioBuffer.numberOfChannels; } - /** the sample rate, in samples per second */ + /** + * the sample rate, in samples per second + */ get sampleRate(): Readonly { return this._audioBuffer.sampleRate; } - /** the duration, in seconds */ + /** + * the duration, in seconds + */ get duration(): Readonly { return this._audioBuffer.duration; } - constructor(name?: string) { - this.name = name; - } - /** get the clip's audio buffer */ - public getData(): AudioBuffer { + /** + * get the clip's audio buffer + */ + getData(): AudioBuffer { return this._audioBuffer; } - /** set audio buffer for the clip */ - public setData(value: AudioBuffer) { + + /** + * set audio buffer for the clip + */ + setData(value: AudioBuffer): void { this._audioBuffer = value; } + + constructor(engine: Engine, name: string = null) { + super(engine); + this.name = name; + } } diff --git a/packages/core/src/audio/AudioListener.ts b/packages/core/src/audio/AudioListener.ts index 1d5be9607a..8471d9ebe5 100644 --- a/packages/core/src/audio/AudioListener.ts +++ b/packages/core/src/audio/AudioListener.ts @@ -4,13 +4,13 @@ import { AudioManager } from "./AudioManager"; /** * Audio Listener - * only one per scene + * Can only have one in a scene. */ export class AudioListener extends Component { - /** - * @internal - */ - constructor(entity: Entity) { + /** + * @internal + */ + constructor(entity: Entity) { super(entity); const gain = AudioManager.context.createGain(); gain.connect(AudioManager.context.destination); @@ -18,6 +18,6 @@ export class AudioListener extends Component { } protected override _onDestroy(): void { - AudioManager.listener = null + AudioManager.listener = null; } } diff --git a/packages/core/src/audio/AudioManager.ts b/packages/core/src/audio/AudioManager.ts index eca67e7d2a..29ae424575 100644 --- a/packages/core/src/audio/AudioManager.ts +++ b/packages/core/src/audio/AudioManager.ts @@ -8,13 +8,27 @@ export class AudioManager { /** @internal */ private static _listener: GainNode; - static get context():AudioContext { + private static _unlocked: boolean = true; + + /** + * Audio context + */ + static get context(): AudioContext { if (!AudioManager._context) { AudioManager._context = new window.AudioContext(); } + if (AudioManager._context.state != "running") { + AudioManager._unlock(); + window.document.addEventListener("mousedown", AudioManager._unlock, true); + window.document.addEventListener("touchend", AudioManager._unlock, true); + window.document.addEventListener("touchstart", AudioManager._unlock, true); + } return AudioManager._context; } + /** + * Audio Listener. Can only have one listener in a Scene. + */ static get listener(): GainNode { return AudioManager._listener; } @@ -22,4 +36,27 @@ export class AudioManager { static set listener(value: GainNode) { AudioManager._listener = value; } + + private static _unlock(): void { + if (AudioManager._unlocked) { + return; + } + AudioManager._playEmptySound(); + if (AudioManager._context.state == "running") { + window.document.removeEventListener("mousedown", AudioManager._unlock, true); + window.document.removeEventListener("touchend", AudioManager._unlock, true); + window.document.removeEventListener("touchstart", AudioManager._unlock, true); + AudioManager._unlocked = true; + } + } + + private static _playEmptySound(): void { + if (!AudioManager._context) { + return; + } + const source = AudioManager.context.createBufferSource(); + source.buffer = AudioManager.context.createBuffer(1, 1, 22050); + source.connect(AudioManager.context.destination); + source.start(0, 0, 0); + } } diff --git a/packages/core/src/audio/AudioSource.ts b/packages/core/src/audio/AudioSource.ts index 50780eece8..996e0af9ee 100644 --- a/packages/core/src/audio/AudioSource.ts +++ b/packages/core/src/audio/AudioSource.ts @@ -1,47 +1,66 @@ -import { MathUtil } from "@galacean/engine-math"; import { Component } from "../Component"; import { Entity } from "../Entity"; import { AudioClip } from "./AudioClip"; import { AudioManager } from "./AudioManager"; +import { assignmentClone, deepClone, ignoreClone } from "../clone/CloneManager"; /** * Audio Source Component */ export class AudioSource extends Component { - /** Whether the sound is playing or not */ + @ignoreClone + /** Whether the clip playing right now */ isPlaying: Readonly; - /** whether the sound must be replayed when the end is reached. Default false */ + @deepClone + /** Whether the audio clip looping. Default false */ loop: boolean = false; - /** Fired when the sound has stopped playing, either because it's reached a predetermined stop time, the full duration of the audio has been performed, or because the entire audio has been played. */ - onPlayEnd: () => any; + @deepClone + /** If set to true, the audio source will automatically start playing on awake. */ + playOnAwake: boolean = false; + @ignoreClone private _clip: AudioClip; - private _context: AudioContext; + @deepClone private _gainNode: GainNode; + @ignoreClone private _sourceNode: AudioBufferSourceNode; + @deepClone private _startTime: number = 0; + @deepClone private _pausedTime: number = null; + @deepClone private _endTime: number = null; + @deepClone private _duration: number = null; + @ignoreClone private _absoluteStartTime: number; - private _currRepeatTimes: number = 1; - private _repeatTimes: number = 1; + @deepClone private _volume: number = 1; - private _mute: boolean = false; + @deepClone + private _lastVolume: number = 1; + @deepClone private _playbackRate: number = 1; - /** The audio asset to be played */ + /** + * The audio cilp to play + */ get clip(): AudioClip { return this._clip; } set clip(value: AudioClip) { - this._clip = value; + const lastClip = this._clip; + if (lastClip !== value) { + lastClip && lastClip._addReferCount(-1); + this._clip = value; + } } - /** the volume, should be positive. Default 1*/ + /** + * The volume of the audio source. 1.0 is origin volume. + */ get volume(): number { return this._volume; } @@ -49,11 +68,13 @@ export class AudioSource extends Component { set volume(value: number) { this._volume = value; if (this.isPlaying) { - this._gainNode.gain.setValueAtTime(value, this._context.currentTime); + this._gainNode.gain.setValueAtTime(value, AudioManager.context.currentTime); } } - /** Speed factor at which the sound will be played. Default 1 */ + /** + * The playback speed of the audio source, 1.0 is normal playback speed. + */ get playbackRate(): number { return this._playbackRate; } @@ -65,29 +86,26 @@ export class AudioSource extends Component { } } - /** whether is muted or not */ + /** + * Mutes / Unmutes the AudioSource. + * Mute sets the volume = 0, Un-Mute restore the original volume. + */ get mute(): boolean { - return this._mute; + return this.volume === 0; } set mute(value: boolean) { - this._mute = value; if (value) { + this._lastVolume = this.volume; this.volume = 0; + } else { + this.volume = this._lastVolume; } } - /** repeat times, default 1, should be positive integer */ - get repeatTimes(): number { - return this._repeatTimes; - } - - set repeatTimes(value: number) { - this._repeatTimes = MathUtil.clamp(Math.abs(Math.ceil(value)), 1, Infinity); - this._currRepeatTimes = this._repeatTimes; - } - - /** The time, in seconds, at which the sound should begin to play. Default 0 */ + /** + * The time, in seconds, at which the sound should begin to play. Default 0. + */ get startTime(): number { return this._startTime; } @@ -96,7 +114,9 @@ export class AudioSource extends Component { this._startTime = value; } - /** The time, in seconds, at which the sound should stop to play. */ + /** + * The time, in seconds, at which the sound should stop to play. + */ get endTime(): number { return this._endTime; } @@ -106,8 +126,10 @@ export class AudioSource extends Component { this._duration = this._endTime - this._startTime; } - /** Current playback progress, in seconds */ - get position(): number { + /** + * Playback position in seconds. + */ + get time(): number { if (this.isPlaying) { return this._pausedTime ? this.engine.time.elapsedTime - this._absoluteStartTime + this._pausedTime @@ -116,18 +138,10 @@ export class AudioSource extends Component { return 0; } - /** @internal */ - constructor(entity: Entity) { - super(entity); - this._onPlayEnd = this._onPlayEnd.bind(this); - - this._context = AudioManager.context; - this._gainNode = AudioManager.context.createGain(); - this._gainNode.connect(AudioManager.listener); - } - - /** play the sound from the very beginning */ - public play() { + /** + * Plays the clip. + */ + play(): void { if (!this._clip || !this.clip.duration || this.isPlaying) return; if (this.startTime > this._clip.duration || this.startTime < 0) return; if (this._duration && this._duration < 0) return; @@ -136,17 +150,21 @@ export class AudioSource extends Component { this._play(this.startTime, this._duration); } - /** stop play the sound */ - public stop() { + /** + * Stops playing the clip. + */ + stop(): void { if (this._sourceNode && this.isPlaying) { this._sourceNode.stop(); - this._currRepeatTimes = 1; } } - /** pause playing */ - public pause() { + + /** + * Pauses playing the clip. + */ + pause(): void { if (this._sourceNode && this.isPlaying) { - this._pausedTime = this.position; + this._pausedTime = this.time; this.isPlaying = false; @@ -156,16 +174,56 @@ export class AudioSource extends Component { } } - /** resume playing, if is paused */ - public resume() { + /** + * Unpause the paused playback of this AudioSource. + */ + unPause(): void { if (!this.isPlaying && this._pausedTime) { const duration = this.endTime ? this.endTime - this._pausedTime : null; this._play(this._pausedTime, duration); } } - private _play(startTime: number, duration: number | null) { - const source = this._context.createBufferSource(); + /** @internal */ + constructor(entity: Entity) { + super(entity); + this._onPlayEnd = this._onPlayEnd.bind(this); + + this._gainNode = AudioManager.context.createGain(); + this._gainNode.connect(AudioManager.listener); + } + + override _onAwake(): void { + this.playOnAwake && this._clip && this.play(); + } + + /** + * @internal + */ + override _onEnable(): void { + this._clip && this.unPause(); + } + + /** + * @internal + */ + override _onDisable(): void { + this._clip && this.pause(); + } + + /** + * @internal + */ + protected override _onDestroy(): void { + super._onDestroy(); + if (this._clip) { + this._clip._addReferCount(-1); + this._clip = null; + } + } + + private _play(startTime: number, duration: number | null): void { + const source = AudioManager.context.createBufferSource(); source.buffer = this._clip.getData(); source.onended = this._onPlayEnd; source.playbackRate.value = this._playbackRate; @@ -188,20 +246,8 @@ export class AudioSource extends Component { this.isPlaying = true; } - private _onPlayEnd() { + private _onPlayEnd(): void { if (!this.isPlaying) return; this.isPlaying = false; - if (this._currRepeatTimes === 1) { - this._currRepeatTimes = this._repeatTimes; - this._sourceNode.disconnect(); - this._sourceNode = null; - this._pausedTime = null; - this.onPlayEnd && this.onPlayEnd(); - return; - } - if (this._currRepeatTimes > 1) { - this._currRepeatTimes--; - this.play(); - } } } diff --git a/packages/loader/src/AudioLoader.ts b/packages/loader/src/AudioLoader.ts index abc6a96287..df048c5fa5 100644 --- a/packages/loader/src/AudioLoader.ts +++ b/packages/loader/src/AudioLoader.ts @@ -3,11 +3,16 @@ import { resourceLoader, Loader, AssetPromise, AssetType, LoadItem, AudioManager @resourceLoader(AssetType.Audio, ["mp3", "ogg", "wav"], false) class AudioLoader extends Loader { load(item: LoadItem): AssetPromise { - return new AssetPromise((resolve) => { + return new AssetPromise((resolve, reject) => { this.request(item.url, { type: "arraybuffer" }).then((arrayBuffer) => { - AudioManager.context.decodeAudioData(arrayBuffer).then((result) => { - resolve(result); - }); + AudioManager.context + .decodeAudioData(arrayBuffer) + .then((result) => { + resolve(result); + }) + .catch((e) => { + reject(e); + }); }); }); } From 2c0d994c611bda04f1f0dcb2ea5d075cedd4f525 Mon Sep 17 00:00:00 2001 From: JujieX Date: Thu, 8 Jun 2023 09:41:32 +0800 Subject: [PATCH 03/21] fix: lint error --- packages/core/src/index.ts | 2 +- packages/loader/src/index.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index dfa59313b9..00abce26f5 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -55,7 +55,7 @@ export * from "./clone/CloneManager"; export * from "./renderingHardwareInterface/index"; export * from "./physics/index"; export * from "./Utils"; -export * from './audio' +export * from "./audio"; // Export for CanvasRenderer plugin. export { Basic2DBatcher } from "./RenderPipeline/Basic2DBatcher"; diff --git a/packages/loader/src/index.ts b/packages/loader/src/index.ts index bace872f94..7628188f94 100644 --- a/packages/loader/src/index.ts +++ b/packages/loader/src/index.ts @@ -15,7 +15,7 @@ import "./SpriteLoader"; import "./Texture2DLoader"; import "./TextureCubeLoader"; import "./AnimationClipLoader"; -import "./AudioLoader" +import "./AudioLoader"; export { parseSingleKTX } from "./compressed-texture"; export type { GLTFParams } from "./GLTFLoader"; From 69eeb8f176f6d084778bcdc2e265e8c86df90bee Mon Sep 17 00:00:00 2001 From: JujieX Date: Tue, 13 Jun 2023 15:17:01 +0800 Subject: [PATCH 04/21] fix: auto play after context resume --- packages/core/src/audio/AudioManager.ts | 32 +++++++------------------ 1 file changed, 9 insertions(+), 23 deletions(-) diff --git a/packages/core/src/audio/AudioManager.ts b/packages/core/src/audio/AudioManager.ts index 29ae424575..a248bf4e4f 100644 --- a/packages/core/src/audio/AudioManager.ts +++ b/packages/core/src/audio/AudioManager.ts @@ -8,7 +8,7 @@ export class AudioManager { /** @internal */ private static _listener: GainNode; - private static _unlocked: boolean = true; + private static _unlocked: boolean = false; /** * Audio context @@ -17,11 +17,8 @@ export class AudioManager { if (!AudioManager._context) { AudioManager._context = new window.AudioContext(); } - if (AudioManager._context.state != "running") { - AudioManager._unlock(); - window.document.addEventListener("mousedown", AudioManager._unlock, true); - window.document.addEventListener("touchend", AudioManager._unlock, true); - window.document.addEventListener("touchstart", AudioManager._unlock, true); + if (AudioManager._context.state !== "running") { + window.document.addEventListener("pointerdown", AudioManager._unlock, true); } return AudioManager._context; } @@ -41,22 +38,11 @@ export class AudioManager { if (AudioManager._unlocked) { return; } - AudioManager._playEmptySound(); - if (AudioManager._context.state == "running") { - window.document.removeEventListener("mousedown", AudioManager._unlock, true); - window.document.removeEventListener("touchend", AudioManager._unlock, true); - window.document.removeEventListener("touchstart", AudioManager._unlock, true); - AudioManager._unlocked = true; - } - } - - private static _playEmptySound(): void { - if (!AudioManager._context) { - return; - } - const source = AudioManager.context.createBufferSource(); - source.buffer = AudioManager.context.createBuffer(1, 1, 22050); - source.connect(AudioManager.context.destination); - source.start(0, 0, 0); + AudioManager._context.resume().then(() => { + if (AudioManager._context.state === "running") { + window.document.removeEventListener("pointerdown", AudioManager._unlock, true); + AudioManager._unlocked = true; + } + }); } } From 69b0fab62671628117b549a9cac07f1af8f01391 Mon Sep 17 00:00:00 2001 From: JujieX Date: Tue, 13 Jun 2023 15:24:47 +0800 Subject: [PATCH 05/21] fix: loop take effect error --- packages/core/src/audio/AudioSource.ts | 49 ++++++++++++++++---------- 1 file changed, 31 insertions(+), 18 deletions(-) diff --git a/packages/core/src/audio/AudioSource.ts b/packages/core/src/audio/AudioSource.ts index 996e0af9ee..229eb6d963 100644 --- a/packages/core/src/audio/AudioSource.ts +++ b/packages/core/src/audio/AudioSource.ts @@ -2,7 +2,7 @@ import { Component } from "../Component"; import { Entity } from "../Entity"; import { AudioClip } from "./AudioClip"; import { AudioManager } from "./AudioManager"; -import { assignmentClone, deepClone, ignoreClone } from "../clone/CloneManager"; +import { deepClone, ignoreClone } from "../clone/CloneManager"; /** * Audio Source Component @@ -11,12 +11,6 @@ export class AudioSource extends Component { @ignoreClone /** Whether the clip playing right now */ isPlaying: Readonly; - @deepClone - /** Whether the audio clip looping. Default false */ - loop: boolean = false; - @deepClone - /** If set to true, the audio source will automatically start playing on awake. */ - playOnAwake: boolean = false; @ignoreClone private _clip: AudioClip; @@ -42,6 +36,8 @@ export class AudioSource extends Component { private _lastVolume: number = 1; @deepClone private _playbackRate: number = 1; + @deepClone + private _loop: boolean = false; /** * The audio cilp to play @@ -103,6 +99,23 @@ export class AudioSource extends Component { } } + /** + * Whether the audio clip looping. Default false. + */ + get loop(): boolean { + return this._loop; + } + + set loop(value: boolean) { + if (value !== this._loop) { + this._loop = value; + + if (this.isPlaying) { + this._setSourceNodeLoop(this._sourceNode); + } + } + } + /** * The time, in seconds, at which the sound should begin to play. Default 0. */ @@ -193,10 +206,6 @@ export class AudioSource extends Component { this._gainNode.connect(AudioManager.listener); } - override _onAwake(): void { - this.playOnAwake && this._clip && this.play(); - } - /** * @internal */ @@ -228,13 +237,7 @@ export class AudioSource extends Component { source.onended = this._onPlayEnd; source.playbackRate.value = this._playbackRate; - if (this.loop) { - source.loop = true; - source.loopStart = startTime; - if (this.endTime) { - source.loopEnd = this.endTime; - } - } + this._setSourceNodeLoop(source); this._gainNode.gain.setValueAtTime(this._volume, 0); source.connect(this._gainNode); @@ -250,4 +253,14 @@ export class AudioSource extends Component { if (!this.isPlaying) return; this.isPlaying = false; } + + private _setSourceNodeLoop(sourceNode: AudioBufferSourceNode) { + sourceNode.loop = this._loop; + if (this._loop) { + sourceNode.loopStart = this.startTime; + if (this.endTime) { + sourceNode.loopEnd = this.endTime; + } + } + } } From f4e2474c17d6213bd6dff58d13a8c41fe75a5b2d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=BB=B4=E5=8B=92?= Date: Mon, 19 Jun 2023 10:49:06 +0800 Subject: [PATCH 06/21] fix: set initial time as -1 --- packages/core/src/audio/AudioSource.ts | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/packages/core/src/audio/AudioSource.ts b/packages/core/src/audio/AudioSource.ts index 229eb6d963..a8c2ccd820 100644 --- a/packages/core/src/audio/AudioSource.ts +++ b/packages/core/src/audio/AudioSource.ts @@ -22,9 +22,9 @@ export class AudioSource extends Component { @deepClone private _startTime: number = 0; @deepClone - private _pausedTime: number = null; + private _pausedTime: number = -1; @deepClone - private _endTime: number = null; + private _endTime: number = -1; @deepClone private _duration: number = null; @ignoreClone @@ -144,7 +144,7 @@ export class AudioSource extends Component { */ get time(): number { if (this.isPlaying) { - return this._pausedTime + return this._pausedTime > 0 ? this.engine.time.elapsedTime - this._absoluteStartTime + this._pausedTime : this.engine.time.elapsedTime - this._absoluteStartTime + this.startTime; } @@ -159,7 +159,7 @@ export class AudioSource extends Component { if (this.startTime > this._clip.duration || this.startTime < 0) return; if (this._duration && this._duration < 0) return; - this._pausedTime = null; + this._pausedTime = -1; this._play(this.startTime, this._duration); } @@ -191,8 +191,8 @@ export class AudioSource extends Component { * Unpause the paused playback of this AudioSource. */ unPause(): void { - if (!this.isPlaying && this._pausedTime) { - const duration = this.endTime ? this.endTime - this._pausedTime : null; + if (!this.isPlaying && this._pausedTime > 0) { + const duration = this.endTime > 0 ? this.endTime - this._pausedTime : null; this._play(this._pausedTime, duration); } } @@ -258,7 +258,7 @@ export class AudioSource extends Component { sourceNode.loop = this._loop; if (this._loop) { sourceNode.loopStart = this.startTime; - if (this.endTime) { + if (this.endTime > 0) { sourceNode.loopEnd = this.endTime; } } From fe318a4f0b163c7235695a46f300b5e7ba464dae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=BB=B4=E5=8B=92?= Date: Thu, 14 Dec 2023 13:50:26 +0800 Subject: [PATCH 07/21] refactor: basic audio --- packages/core/src/audio/AudioClip.ts | 18 +-- packages/core/src/audio/AudioListener.ts | 22 +++- packages/core/src/audio/AudioManager.ts | 10 +- packages/core/src/audio/AudioSource.ts | 140 +++++++++-------------- 4 files changed, 83 insertions(+), 107 deletions(-) diff --git a/packages/core/src/audio/AudioClip.ts b/packages/core/src/audio/AudioClip.ts index 1ffb83a6ab..4ebcf4c08b 100644 --- a/packages/core/src/audio/AudioClip.ts +++ b/packages/core/src/audio/AudioClip.ts @@ -5,41 +5,43 @@ import { ReferResource } from "../asset/ReferResource"; * Audio Clip */ export class AudioClip extends ReferResource { - /** the name of clip */ + /** + * Name of clip. + */ name: string; private _audioBuffer: AudioBuffer; /** - * the number of discrete audio channels + * Number of discrete audio channels. */ - get channels(): Readonly { + get channels(): number { return this._audioBuffer.numberOfChannels; } /** - * the sample rate, in samples per second + * Sample rate, in samples per second. */ - get sampleRate(): Readonly { + get sampleRate(): number { return this._audioBuffer.sampleRate; } /** - * the duration, in seconds + * Duration, in seconds. */ get duration(): Readonly { return this._audioBuffer.duration; } /** - * get the clip's audio buffer + * Get the clip's audio buffer. */ getData(): AudioBuffer { return this._audioBuffer; } /** - * set audio buffer for the clip + * Set audio buffer for the clip. */ setData(value: AudioBuffer): void { this._audioBuffer = value; diff --git a/packages/core/src/audio/AudioListener.ts b/packages/core/src/audio/AudioListener.ts index 8471d9ebe5..cb492eae32 100644 --- a/packages/core/src/audio/AudioListener.ts +++ b/packages/core/src/audio/AudioListener.ts @@ -7,17 +7,31 @@ import { AudioManager } from "./AudioManager"; * Can only have one in a scene. */ export class AudioListener extends Component { + private static instance: AudioListener | null = null; /** * @internal */ constructor(entity: Entity) { super(entity); - const gain = AudioManager.context.createGain(); - gain.connect(AudioManager.context.destination); - AudioManager.listener = gain; + if (AudioListener.instance) { + throw new Error("There can only be one AudioListener in a scene."); + } + AudioListener.instance = this; + if (!AudioManager.listener) { + const gain = AudioManager.context.createGain(); + gain.connect(AudioManager.context.destination); + AudioManager.listener = gain; + } } protected override _onDestroy(): void { - AudioManager.listener = null; + if (AudioListener.instance === this) { + AudioListener.instance = null; + } + + if (AudioManager.listener) { + AudioManager.listener.disconnect(); + AudioManager.listener = null; + } } } diff --git a/packages/core/src/audio/AudioManager.ts b/packages/core/src/audio/AudioManager.ts index a248bf4e4f..2ce5356e71 100644 --- a/packages/core/src/audio/AudioManager.ts +++ b/packages/core/src/audio/AudioManager.ts @@ -3,11 +3,8 @@ * Audio Manager */ export class AudioManager { - /** @internal */ private static _context: AudioContext; - /** @internal */ private static _listener: GainNode; - private static _unlocked: boolean = false; /** @@ -15,10 +12,9 @@ export class AudioManager { */ static get context(): AudioContext { if (!AudioManager._context) { - AudioManager._context = new window.AudioContext(); - } - if (AudioManager._context.state !== "running") { - window.document.addEventListener("pointerdown", AudioManager._unlock, true); + AudioManager._context = new window.AudioContext();} + if (AudioManager._context.state !== "running") { + window.document.addEventListener("pointerdown", AudioManager._unlock, true); } return AudioManager._context; } diff --git a/packages/core/src/audio/AudioSource.ts b/packages/core/src/audio/AudioSource.ts index a8c2ccd820..ee6da65ab8 100644 --- a/packages/core/src/audio/AudioSource.ts +++ b/packages/core/src/audio/AudioSource.ts @@ -9,26 +9,19 @@ import { deepClone, ignoreClone } from "../clone/CloneManager"; */ export class AudioSource extends Component { @ignoreClone - /** Whether the clip playing right now */ - isPlaying: Readonly; + private _isPlaying: boolean = false @ignoreClone private _clip: AudioClip; @deepClone private _gainNode: GainNode; @ignoreClone - private _sourceNode: AudioBufferSourceNode; + private _sourceNode: AudioBufferSourceNode | null = null; - @deepClone - private _startTime: number = 0; @deepClone private _pausedTime: number = -1; - @deepClone - private _endTime: number = -1; - @deepClone - private _duration: number = null; @ignoreClone - private _absoluteStartTime: number; + private _absoluteStartTime: number = -1 @deepClone private _volume: number = 1; @@ -40,7 +33,7 @@ export class AudioSource extends Component { private _loop: boolean = false; /** - * The audio cilp to play + * The audio cilp to play. */ get clip(): AudioClip { return this._clip; @@ -54,6 +47,13 @@ export class AudioSource extends Component { } } + /** + * Whether the clip playing right now (Read Only). + */ + get isPlaying():boolean{ + return this._isPlaying + } + /** * The volume of the audio source. 1.0 is origin volume. */ @@ -63,13 +63,13 @@ export class AudioSource extends Component { set volume(value: number) { this._volume = value; - if (this.isPlaying) { + if (this._isPlaying) { this._gainNode.gain.setValueAtTime(value, AudioManager.context.currentTime); } } /** - * The playback speed of the audio source, 1.0 is normal playback speed. + * The playback rate of the audio source, 1.0 is normal playback speed. */ get playbackRate(): number { return this._playbackRate; @@ -77,14 +77,14 @@ export class AudioSource extends Component { set playbackRate(value: number) { this._playbackRate = value; - if (this.isPlaying) { + if (this._isPlaying) { this._sourceNode.playbackRate.value = this._playbackRate; } } /** * Mutes / Unmutes the AudioSource. - * Mute sets the volume = 0, Un-Mute restore the original volume. + * Mute sets volume as 0, Un-Mute restore volume. */ get mute(): boolean { return this.volume === 0; @@ -111,63 +111,37 @@ export class AudioSource extends Component { this._loop = value; if (this.isPlaying) { - this._setSourceNodeLoop(this._sourceNode); + this._sourceNode.loop = this._loop; } } } - /** - * The time, in seconds, at which the sound should begin to play. Default 0. - */ - get startTime(): number { - return this._startTime; - } - - set startTime(value: number) { - this._startTime = value; - } - - /** - * The time, in seconds, at which the sound should stop to play. - */ - get endTime(): number { - return this._endTime; - } - - set endTime(value: number) { - this._endTime = value; - this._duration = this._endTime - this._startTime; - } - /** * Playback position in seconds. */ get time(): number { - if (this.isPlaying) { - return this._pausedTime > 0 - ? this.engine.time.elapsedTime - this._absoluteStartTime + this._pausedTime - : this.engine.time.elapsedTime - this._absoluteStartTime + this.startTime; + if (this._isPlaying) { + return this.engine.time.elapsedTime - this._absoluteStartTime } - return 0; + return this._pausedTime >= 0 ? this._pausedTime : 0; } /** * Plays the clip. */ play(): void { - if (!this._clip || !this.clip.duration || this.isPlaying) return; - if (this.startTime > this._clip.duration || this.startTime < 0) return; - if (this._duration && this._duration < 0) return; - + if (!this._isValidClip() || this._isPlaying) return; + this._initSourceNode(); + console.log(this._pausedTime) + this._startPlayback(this._pausedTime >= 0 ? this._pausedTime : 0); this._pausedTime = -1; - this._play(this.startTime, this._duration); } /** * Stops playing the clip. */ stop(): void { - if (this._sourceNode && this.isPlaying) { + if (this._sourceNode && this._isPlaying) { this._sourceNode.stop(); } } @@ -176,24 +150,16 @@ export class AudioSource extends Component { * Pauses playing the clip. */ pause(): void { - if (this._sourceNode && this.isPlaying) { + if (this._sourceNode && this._isPlaying) { + this._pausedTime = this.time; - this.isPlaying = false; + this._isPlaying = false; this._sourceNode.disconnect(); this._sourceNode.onended = null; this._sourceNode = null; - } - } - - /** - * Unpause the paused playback of this AudioSource. - */ - unPause(): void { - if (!this.isPlaying && this._pausedTime > 0) { - const duration = this.endTime > 0 ? this.endTime - this._pausedTime : null; - this._play(this._pausedTime, duration); + console.log(this._pausedTime) } } @@ -210,14 +176,14 @@ export class AudioSource extends Component { * @internal */ override _onEnable(): void { - this._clip && this.unPause(); + this.play(); } /** * @internal */ override _onDisable(): void { - this._clip && this.pause(); + this._isValidClip() && this.pause(); } /** @@ -231,36 +197,34 @@ export class AudioSource extends Component { } } - private _play(startTime: number, duration: number | null): void { - const source = AudioManager.context.createBufferSource(); - source.buffer = this._clip.getData(); - source.onended = this._onPlayEnd; - source.playbackRate.value = this._playbackRate; - - this._setSourceNodeLoop(source); + private _onPlayEnd(): void { + if (!this.isPlaying) return; + this._isPlaying = false; + } - this._gainNode.gain.setValueAtTime(this._volume, 0); - source.connect(this._gainNode); + private _initSourceNode(): void { + if (this._sourceNode) { + this._sourceNode.disconnect(); + } + this._sourceNode = AudioManager.context.createBufferSource(); - duration ? source.start(0, startTime, duration) : source.start(0, startTime); + const {_sourceNode : sourceNode} = this + sourceNode.buffer = this._clip.getData(); + sourceNode.onended = this._onPlayEnd.bind(this); + sourceNode.playbackRate.value = this._playbackRate; - this._absoluteStartTime = this.engine.time.elapsedTime; - this._sourceNode = source; - this.isPlaying = true; + sourceNode.loop = this._loop; + this._gainNode.gain.setValueAtTime(this._volume, AudioManager.context.currentTime); + sourceNode.connect(this._gainNode); } - private _onPlayEnd(): void { - if (!this.isPlaying) return; - this.isPlaying = false; + private _startPlayback(startTime: number): void { + this._sourceNode.start(0, startTime); + this._absoluteStartTime = AudioManager.context.currentTime - startTime; + this._isPlaying = true; } - private _setSourceNodeLoop(sourceNode: AudioBufferSourceNode) { - sourceNode.loop = this._loop; - if (this._loop) { - sourceNode.loopStart = this.startTime; - if (this.endTime > 0) { - sourceNode.loopEnd = this.endTime; - } - } + private _isValidClip(): boolean { + return this._clip && this._clip.duration > 0; } } From 98d8fd7751dcd2782d6a73cbe3484f0af30dd149 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=BB=B4=E5=8B=92?= Date: Thu, 14 Dec 2023 15:01:26 +0800 Subject: [PATCH 08/21] fix: opt code --- packages/core/src/audio/AudioSource.ts | 3 +-- packages/loader/src/AudioLoader.ts | 13 +++++++------ 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/packages/core/src/audio/AudioSource.ts b/packages/core/src/audio/AudioSource.ts index ee6da65ab8..0077af2622 100644 --- a/packages/core/src/audio/AudioSource.ts +++ b/packages/core/src/audio/AudioSource.ts @@ -132,7 +132,6 @@ export class AudioSource extends Component { play(): void { if (!this._isValidClip() || this._isPlaying) return; this._initSourceNode(); - console.log(this._pausedTime) this._startPlayback(this._pausedTime >= 0 ? this._pausedTime : 0); this._pausedTime = -1; } @@ -143,6 +142,7 @@ export class AudioSource extends Component { stop(): void { if (this._sourceNode && this._isPlaying) { this._sourceNode.stop(); + this._pausedTime = -1 } } @@ -159,7 +159,6 @@ export class AudioSource extends Component { this._sourceNode.disconnect(); this._sourceNode.onended = null; this._sourceNode = null; - console.log(this._pausedTime) } } diff --git a/packages/loader/src/AudioLoader.ts b/packages/loader/src/AudioLoader.ts index df048c5fa5..bdb7ae6230 100644 --- a/packages/loader/src/AudioLoader.ts +++ b/packages/loader/src/AudioLoader.ts @@ -1,14 +1,15 @@ -import { resourceLoader, Loader, AssetPromise, AssetType, LoadItem, AudioManager } from "@galacean/engine-core"; - +import { resourceLoader, Loader, AssetPromise, AssetType, LoadItem, AudioManager, AudioClip, ResourceManager } from "@galacean/engine-core"; @resourceLoader(AssetType.Audio, ["mp3", "ogg", "wav"], false) -class AudioLoader extends Loader { - load(item: LoadItem): AssetPromise { +class AudioLoader extends Loader { + load(item: LoadItem,resourceManager: ResourceManager): AssetPromise { return new AssetPromise((resolve, reject) => { this.request(item.url, { type: "arraybuffer" }).then((arrayBuffer) => { AudioManager.context .decodeAudioData(arrayBuffer) - .then((result) => { - resolve(result); + .then((result:AudioBuffer) => { + const audioClip = new AudioClip(resourceManager.engine) + audioClip.setData(result) + resolve(audioClip); }) .catch((e) => { reject(e); From aad876d7f457b37180be73551abda6f3542f9a26 Mon Sep 17 00:00:00 2001 From: JujieX Date: Thu, 14 Dec 2023 22:37:45 +0800 Subject: [PATCH 09/21] feat: add audio test --- tests/src/core/audio/AudioSource.test.ts | 51 ++++++++++++++++++++++++ tests/src/core/model/sound.ts | 2 + 2 files changed, 53 insertions(+) create mode 100644 tests/src/core/audio/AudioSource.test.ts create mode 100644 tests/src/core/model/sound.ts diff --git a/tests/src/core/audio/AudioSource.test.ts b/tests/src/core/audio/AudioSource.test.ts new file mode 100644 index 0000000000..c7a8ab2173 --- /dev/null +++ b/tests/src/core/audio/AudioSource.test.ts @@ -0,0 +1,51 @@ +import { AssetType, AudioClip, AudioSource, Engine,AudioListener} from "@galacean/engine-core"; +import { WebGLEngine } from "@galacean/engine-rhi-webgl"; +import { expect } from "chai"; +import { sound } from "../model/sound"; + +describe("AudioSource", () => { + const canvas = document.createElement("canvas"); + + let engine: Engine; + let url:string + let clip:AudioClip + let audioSource: AudioSource + + before(async function () { + engine = await WebGLEngine.create({ canvas: canvas }); + const blob = await fetch(sound).then((res) => res.blob()); + url = URL.createObjectURL(blob) + "#.ogg"; + + engine.run(); + }); + + + it('load', async () => { + clip = await engine.resourceManager.load( + { + url: url, + type: AssetType.Audio, + } + ); + + expect(clip.duration).to.be.above(0); + }); + + + it('start play', async () => { + const scene = engine.sceneManager.activeScene; + const rootEntity = scene.createRootEntity(); + const audioEntity = rootEntity.createChild() + const listenEntity = rootEntity.createChild("listen"); + + listenEntity.addComponent(AudioListener); + audioSource = audioEntity.addComponent(AudioSource) + audioSource.clip = clip + + audioSource.stop(); + audioSource.play(); + expect(audioSource.isPlaying).to.be.true; + expect(audioSource.time).to.be.equal(0) + }); + +}) \ No newline at end of file diff --git a/tests/src/core/model/sound.ts b/tests/src/core/model/sound.ts new file mode 100644 index 0000000000..d8e0b6020f --- /dev/null +++ b/tests/src/core/model/sound.ts @@ -0,0 +1,2 @@ +export const sound = +"data:audio/ogg;base64,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" \ No newline at end of file From 319a125826026b3935f9c2563dee3f22f809455e Mon Sep 17 00:00:00 2001 From: JujieX Date: Fri, 15 Dec 2023 00:11:12 +0800 Subject: [PATCH 10/21] fix: opt code --- packages/core/src/audio/AudioListener.ts | 37 ----------------------- packages/core/src/audio/AudioManager.ts | 20 +++++++------ packages/core/src/audio/AudioSource.ts | 38 +++++++++++++++++++----- packages/core/src/audio/index.ts | 1 - tests/src/core/audio/AudioSource.test.ts | 4 +-- 5 files changed, 43 insertions(+), 57 deletions(-) delete mode 100644 packages/core/src/audio/AudioListener.ts diff --git a/packages/core/src/audio/AudioListener.ts b/packages/core/src/audio/AudioListener.ts deleted file mode 100644 index cb492eae32..0000000000 --- a/packages/core/src/audio/AudioListener.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { Component } from "../Component"; -import { Entity } from "../Entity"; -import { AudioManager } from "./AudioManager"; - -/** - * Audio Listener - * Can only have one in a scene. - */ -export class AudioListener extends Component { - private static instance: AudioListener | null = null; - /** - * @internal - */ - constructor(entity: Entity) { - super(entity); - if (AudioListener.instance) { - throw new Error("There can only be one AudioListener in a scene."); - } - AudioListener.instance = this; - if (!AudioManager.listener) { - const gain = AudioManager.context.createGain(); - gain.connect(AudioManager.context.destination); - AudioManager.listener = gain; - } - } - - protected override _onDestroy(): void { - if (AudioListener.instance === this) { - AudioListener.instance = null; - } - - if (AudioManager.listener) { - AudioManager.listener.disconnect(); - AudioManager.listener = null; - } - } -} diff --git a/packages/core/src/audio/AudioManager.ts b/packages/core/src/audio/AudioManager.ts index 2ce5356e71..657d5b2c74 100644 --- a/packages/core/src/audio/AudioManager.ts +++ b/packages/core/src/audio/AudioManager.ts @@ -4,7 +4,7 @@ */ export class AudioManager { private static _context: AudioContext; - private static _listener: GainNode; + private static _gainNode: GainNode; private static _unlocked: boolean = false; /** @@ -12,7 +12,8 @@ export class AudioManager { */ static get context(): AudioContext { if (!AudioManager._context) { - AudioManager._context = new window.AudioContext();} + AudioManager._context = new window.AudioContext(); + } if (AudioManager._context.state !== "running") { window.document.addEventListener("pointerdown", AudioManager._unlock, true); } @@ -20,14 +21,15 @@ export class AudioManager { } /** - * Audio Listener. Can only have one listener in a Scene. + * Audio GainNode. */ - static get listener(): GainNode { - return AudioManager._listener; - } - - static set listener(value: GainNode) { - AudioManager._listener = value; + static get gainNode(): GainNode { + if(!AudioManager._gainNode){ + const gain = AudioManager.context.createGain(); + gain.connect(AudioManager.context.destination); + AudioManager._gainNode = gain; + } + return AudioManager._gainNode; } private static _unlock(): void { diff --git a/packages/core/src/audio/AudioSource.ts b/packages/core/src/audio/AudioSource.ts index 0077af2622..4e7305653a 100644 --- a/packages/core/src/audio/AudioSource.ts +++ b/packages/core/src/audio/AudioSource.ts @@ -10,6 +10,8 @@ import { deepClone, ignoreClone } from "../clone/CloneManager"; export class AudioSource extends Component { @ignoreClone private _isPlaying: boolean = false + @ignoreClone + private _isPlayOnAwake: boolean = false @ignoreClone private _clip: AudioClip; @@ -54,6 +56,17 @@ export class AudioSource extends Component { return this._isPlaying } + /** + * Whether the clip playing on Awake. + */ + get playOnAwake(): boolean { + return this._isPlayOnAwake; + } + + set playOnAwake(value: boolean) { + this._isPlayOnAwake = value; + } + /** * The volume of the audio source. 1.0 is origin volume. */ @@ -121,9 +134,12 @@ export class AudioSource extends Component { */ get time(): number { if (this._isPlaying) { - return this.engine.time.elapsedTime - this._absoluteStartTime + return this._pausedTime > 0 + ? this.engine.time.elapsedTime - this._absoluteStartTime + this._pausedTime + : this.engine.time.elapsedTime - this._absoluteStartTime + }else{ + return this._pausedTime > 0 ? this._pausedTime : 0; } - return this._pausedTime >= 0 ? this._pausedTime : 0; } /** @@ -132,7 +148,7 @@ export class AudioSource extends Component { play(): void { if (!this._isValidClip() || this._isPlaying) return; this._initSourceNode(); - this._startPlayback(this._pausedTime >= 0 ? this._pausedTime : 0); + this._startPlayback(this._pausedTime > 0 ? this._pausedTime : 0); this._pausedTime = -1; } @@ -151,9 +167,7 @@ export class AudioSource extends Component { */ pause(): void { if (this._sourceNode && this._isPlaying) { - this._pausedTime = this.time; - this._isPlaying = false; this._sourceNode.disconnect(); @@ -168,7 +182,7 @@ export class AudioSource extends Component { this._onPlayEnd = this._onPlayEnd.bind(this); this._gainNode = AudioManager.context.createGain(); - this._gainNode.connect(AudioManager.listener); + this._gainNode.connect(AudioManager.gainNode); } /** @@ -185,6 +199,14 @@ export class AudioSource extends Component { this._isValidClip() && this.pause(); } + /** + * @internal + */ + override _onAwake(): void { + this._isPlayOnAwake && this.play(); + } + + /** * @internal */ @@ -199,6 +221,8 @@ export class AudioSource extends Component { private _onPlayEnd(): void { if (!this.isPlaying) return; this._isPlaying = false; + this._absoluteStartTime = -1 + this._pausedTime = -1 } private _initSourceNode(): void { @@ -219,7 +243,7 @@ export class AudioSource extends Component { private _startPlayback(startTime: number): void { this._sourceNode.start(0, startTime); - this._absoluteStartTime = AudioManager.context.currentTime - startTime; + this._absoluteStartTime = this._absoluteStartTime > 0 ? this.engine.time.elapsedTime - startTime:this.engine.time.elapsedTime; this._isPlaying = true; } diff --git a/packages/core/src/audio/index.ts b/packages/core/src/audio/index.ts index b00c40d3a8..8622b1525d 100644 --- a/packages/core/src/audio/index.ts +++ b/packages/core/src/audio/index.ts @@ -1,4 +1,3 @@ export { AudioManager } from "./AudioManager"; export { AudioClip } from "./AudioClip"; -export { AudioListener } from "./AudioListener"; export { AudioSource } from "./AudioSource"; diff --git a/tests/src/core/audio/AudioSource.test.ts b/tests/src/core/audio/AudioSource.test.ts index c7a8ab2173..2858eef682 100644 --- a/tests/src/core/audio/AudioSource.test.ts +++ b/tests/src/core/audio/AudioSource.test.ts @@ -1,4 +1,4 @@ -import { AssetType, AudioClip, AudioSource, Engine,AudioListener} from "@galacean/engine-core"; +import { AssetType, AudioClip, AudioSource, Engine} from "@galacean/engine-core"; import { WebGLEngine } from "@galacean/engine-rhi-webgl"; import { expect } from "chai"; import { sound } from "../model/sound"; @@ -36,9 +36,7 @@ describe("AudioSource", () => { const scene = engine.sceneManager.activeScene; const rootEntity = scene.createRootEntity(); const audioEntity = rootEntity.createChild() - const listenEntity = rootEntity.createChild("listen"); - listenEntity.addComponent(AudioListener); audioSource = audioEntity.addComponent(AudioSource) audioSource.clip = clip From 062a4c4e469a8ebd036882769d7f30b628de6d26 Mon Sep 17 00:00:00 2001 From: JujieX Date: Fri, 15 Dec 2023 00:18:26 +0800 Subject: [PATCH 11/21] feat: opt code --- packages/core/src/audio/AudioClip.ts | 2 +- packages/core/src/audio/AudioManager.ts | 6 +- packages/core/src/audio/AudioSource.ts | 36 ++++++------ packages/loader/src/AudioLoader.ts | 19 ++++-- tests/src/core/audio/AudioSource.test.ts | 75 +++++++++++------------- 5 files changed, 71 insertions(+), 67 deletions(-) diff --git a/packages/core/src/audio/AudioClip.ts b/packages/core/src/audio/AudioClip.ts index 4ebcf4c08b..ebb890aba2 100644 --- a/packages/core/src/audio/AudioClip.ts +++ b/packages/core/src/audio/AudioClip.ts @@ -5,7 +5,7 @@ import { ReferResource } from "../asset/ReferResource"; * Audio Clip */ export class AudioClip extends ReferResource { - /** + /** * Name of clip. */ name: string; diff --git a/packages/core/src/audio/AudioManager.ts b/packages/core/src/audio/AudioManager.ts index 657d5b2c74..b4f9f8719c 100644 --- a/packages/core/src/audio/AudioManager.ts +++ b/packages/core/src/audio/AudioManager.ts @@ -14,8 +14,8 @@ export class AudioManager { if (!AudioManager._context) { AudioManager._context = new window.AudioContext(); } - if (AudioManager._context.state !== "running") { - window.document.addEventListener("pointerdown", AudioManager._unlock, true); + if (AudioManager._context.state !== "running") { + window.document.addEventListener("pointerdown", AudioManager._unlock, true); } return AudioManager._context; } @@ -24,7 +24,7 @@ export class AudioManager { * Audio GainNode. */ static get gainNode(): GainNode { - if(!AudioManager._gainNode){ + if (!AudioManager._gainNode) { const gain = AudioManager.context.createGain(); gain.connect(AudioManager.context.destination); AudioManager._gainNode = gain; diff --git a/packages/core/src/audio/AudioSource.ts b/packages/core/src/audio/AudioSource.ts index 4e7305653a..166db80c04 100644 --- a/packages/core/src/audio/AudioSource.ts +++ b/packages/core/src/audio/AudioSource.ts @@ -9,21 +9,21 @@ import { deepClone, ignoreClone } from "../clone/CloneManager"; */ export class AudioSource extends Component { @ignoreClone - private _isPlaying: boolean = false + private _isPlaying: boolean = false; @ignoreClone - private _isPlayOnAwake: boolean = false + private _isPlayOnAwake: boolean = false; @ignoreClone private _clip: AudioClip; @deepClone private _gainNode: GainNode; @ignoreClone - private _sourceNode: AudioBufferSourceNode | null = null; + private _sourceNode: AudioBufferSourceNode | null = null; @deepClone private _pausedTime: number = -1; @ignoreClone - private _absoluteStartTime: number = -1 + private _absoluteStartTime: number = -1; @deepClone private _volume: number = 1; @@ -52,8 +52,8 @@ export class AudioSource extends Component { /** * Whether the clip playing right now (Read Only). */ - get isPlaying():boolean{ - return this._isPlaying + get isPlaying(): boolean { + return this._isPlaying; } /** @@ -135,9 +135,9 @@ export class AudioSource extends Component { get time(): number { if (this._isPlaying) { return this._pausedTime > 0 - ? this.engine.time.elapsedTime - this._absoluteStartTime + this._pausedTime - : this.engine.time.elapsedTime - this._absoluteStartTime - }else{ + ? this.engine.time.elapsedTime - this._absoluteStartTime + this._pausedTime + : this.engine.time.elapsedTime - this._absoluteStartTime; + } else { return this._pausedTime > 0 ? this._pausedTime : 0; } } @@ -158,7 +158,7 @@ export class AudioSource extends Component { stop(): void { if (this._sourceNode && this._isPlaying) { this._sourceNode.stop(); - this._pausedTime = -1 + this._pausedTime = -1; } } @@ -189,7 +189,7 @@ export class AudioSource extends Component { * @internal */ override _onEnable(): void { - this.play(); + this.play(); } /** @@ -202,11 +202,10 @@ export class AudioSource extends Component { /** * @internal */ - override _onAwake(): void { - this._isPlayOnAwake && this.play(); + override _onAwake(): void { + this._isPlayOnAwake && this.play(); } - /** * @internal */ @@ -221,8 +220,8 @@ export class AudioSource extends Component { private _onPlayEnd(): void { if (!this.isPlaying) return; this._isPlaying = false; - this._absoluteStartTime = -1 - this._pausedTime = -1 + this._absoluteStartTime = -1; + this._pausedTime = -1; } private _initSourceNode(): void { @@ -231,7 +230,7 @@ export class AudioSource extends Component { } this._sourceNode = AudioManager.context.createBufferSource(); - const {_sourceNode : sourceNode} = this + const { _sourceNode: sourceNode } = this; sourceNode.buffer = this._clip.getData(); sourceNode.onended = this._onPlayEnd.bind(this); sourceNode.playbackRate.value = this._playbackRate; @@ -243,7 +242,8 @@ export class AudioSource extends Component { private _startPlayback(startTime: number): void { this._sourceNode.start(0, startTime); - this._absoluteStartTime = this._absoluteStartTime > 0 ? this.engine.time.elapsedTime - startTime:this.engine.time.elapsedTime; + this._absoluteStartTime = + this._absoluteStartTime > 0 ? this.engine.time.elapsedTime - startTime : this.engine.time.elapsedTime; this._isPlaying = true; } diff --git a/packages/loader/src/AudioLoader.ts b/packages/loader/src/AudioLoader.ts index bdb7ae6230..0cac564734 100644 --- a/packages/loader/src/AudioLoader.ts +++ b/packages/loader/src/AudioLoader.ts @@ -1,14 +1,23 @@ -import { resourceLoader, Loader, AssetPromise, AssetType, LoadItem, AudioManager, AudioClip, ResourceManager } from "@galacean/engine-core"; +import { + resourceLoader, + Loader, + AssetPromise, + AssetType, + LoadItem, + AudioManager, + AudioClip, + ResourceManager +} from "@galacean/engine-core"; @resourceLoader(AssetType.Audio, ["mp3", "ogg", "wav"], false) class AudioLoader extends Loader { - load(item: LoadItem,resourceManager: ResourceManager): AssetPromise { + load(item: LoadItem, resourceManager: ResourceManager): AssetPromise { return new AssetPromise((resolve, reject) => { this.request(item.url, { type: "arraybuffer" }).then((arrayBuffer) => { AudioManager.context .decodeAudioData(arrayBuffer) - .then((result:AudioBuffer) => { - const audioClip = new AudioClip(resourceManager.engine) - audioClip.setData(result) + .then((result: AudioBuffer) => { + const audioClip = new AudioClip(resourceManager.engine); + audioClip.setData(result); resolve(audioClip); }) .catch((e) => { diff --git a/tests/src/core/audio/AudioSource.test.ts b/tests/src/core/audio/AudioSource.test.ts index 2858eef682..46eddf03ab 100644 --- a/tests/src/core/audio/AudioSource.test.ts +++ b/tests/src/core/audio/AudioSource.test.ts @@ -1,49 +1,44 @@ -import { AssetType, AudioClip, AudioSource, Engine} from "@galacean/engine-core"; +import { AssetType, AudioClip, AudioSource, Engine } from "@galacean/engine-core"; import { WebGLEngine } from "@galacean/engine-rhi-webgl"; import { expect } from "chai"; import { sound } from "../model/sound"; describe("AudioSource", () => { - const canvas = document.createElement("canvas"); - - let engine: Engine; - let url:string - let clip:AudioClip - let audioSource: AudioSource - - before(async function () { - engine = await WebGLEngine.create({ canvas: canvas }); - const blob = await fetch(sound).then((res) => res.blob()); - url = URL.createObjectURL(blob) + "#.ogg"; - - engine.run(); - }); + const canvas = document.createElement("canvas"); + let engine: Engine; + let url: string; + let clip: AudioClip; + let audioSource: AudioSource; - it('load', async () => { - clip = await engine.resourceManager.load( - { - url: url, - type: AssetType.Audio, - } - ); + before(async function () { + engine = await WebGLEngine.create({ canvas: canvas }); + const blob = await fetch(sound).then((res) => res.blob()); + url = URL.createObjectURL(blob) + "#.ogg"; - expect(clip.duration).to.be.above(0); - }); - - - it('start play', async () => { - const scene = engine.sceneManager.activeScene; - const rootEntity = scene.createRootEntity(); - const audioEntity = rootEntity.createChild() - - audioSource = audioEntity.addComponent(AudioSource) - audioSource.clip = clip - - audioSource.stop(); - audioSource.play(); - expect(audioSource.isPlaying).to.be.true; - expect(audioSource.time).to.be.equal(0) + engine.run(); + }); + + it("load", async () => { + clip = await engine.resourceManager.load({ + url: url, + type: AssetType.Audio }); - -}) \ No newline at end of file + + expect(clip.duration).to.be.above(0); + }); + + it("start play", async () => { + const scene = engine.sceneManager.activeScene; + const rootEntity = scene.createRootEntity(); + const audioEntity = rootEntity.createChild(); + + audioSource = audioEntity.addComponent(AudioSource); + audioSource.clip = clip; + + audioSource.stop(); + audioSource.play(); + expect(audioSource.isPlaying).to.be.true; + expect(audioSource.time).to.be.equal(0); + }); +}); From 08f957c2cefd3d69afa8bed0ffffd2919806f115 Mon Sep 17 00:00:00 2001 From: JujieX Date: Fri, 15 Dec 2023 00:23:06 +0800 Subject: [PATCH 12/21] feat: make audioManager internal --- packages/core/src/audio/index.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/core/src/audio/index.ts b/packages/core/src/audio/index.ts index 8622b1525d..36be051655 100644 --- a/packages/core/src/audio/index.ts +++ b/packages/core/src/audio/index.ts @@ -1,3 +1,2 @@ -export { AudioManager } from "./AudioManager"; export { AudioClip } from "./AudioClip"; export { AudioSource } from "./AudioSource"; From 010b4e386cddf9f8427a5b70719bf40d29ad2374 Mon Sep 17 00:00:00 2001 From: JujieX Date: Mon, 18 Dec 2023 12:00:56 +0800 Subject: [PATCH 13/21] feat: make audioManager internal --- packages/core/src/audio/AudioClip.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/core/src/audio/AudioClip.ts b/packages/core/src/audio/AudioClip.ts index ebb890aba2..5d5c31651b 100644 --- a/packages/core/src/audio/AudioClip.ts +++ b/packages/core/src/audio/AudioClip.ts @@ -1,10 +1,13 @@ import { Engine } from "../Engine"; import { ReferResource } from "../asset/ReferResource"; +import { AudioManager } from "./AudioManager"; /** * Audio Clip */ export class AudioClip extends ReferResource { + /** @internal */ + _context: AudioContext; /** * Name of clip. */ @@ -50,5 +53,6 @@ export class AudioClip extends ReferResource { constructor(engine: Engine, name: string = null) { super(engine); this.name = name; + this._context = AudioManager.context; } } From 843ce263e764149f05ef10351f2afb5f5cea4222 Mon Sep 17 00:00:00 2001 From: JujieX Date: Mon, 18 Dec 2023 12:01:11 +0800 Subject: [PATCH 14/21] fix: add more start gesture --- packages/core/src/audio/AudioManager.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/core/src/audio/AudioManager.ts b/packages/core/src/audio/AudioManager.ts index b4f9f8719c..4b485373af 100644 --- a/packages/core/src/audio/AudioManager.ts +++ b/packages/core/src/audio/AudioManager.ts @@ -16,6 +16,8 @@ export class AudioManager { } if (AudioManager._context.state !== "running") { window.document.addEventListener("pointerdown", AudioManager._unlock, true); + window.document.addEventListener("touchend", AudioManager._unlock, true); + window.document.addEventListener("touchstart", AudioManager._unlock, true); } return AudioManager._context; } @@ -39,6 +41,8 @@ export class AudioManager { AudioManager._context.resume().then(() => { if (AudioManager._context.state === "running") { window.document.removeEventListener("pointerdown", AudioManager._unlock, true); + window.document.removeEventListener("touchend", AudioManager._unlock, true); + window.document.removeEventListener("touchstart", AudioManager._unlock, true); AudioManager._unlocked = true; } }); From e74db44c4ba910307f48938c8888e960ce6dcb50 Mon Sep 17 00:00:00 2001 From: JujieX Date: Mon, 18 Dec 2023 12:01:28 +0800 Subject: [PATCH 15/21] feat: add audio contentRestorer --- packages/loader/src/AudioContentRestorer.ts | 27 +++++++++++++++++++++ packages/loader/src/AudioLoader.ts | 23 +++++++++++++++--- 2 files changed, 46 insertions(+), 4 deletions(-) create mode 100644 packages/loader/src/AudioContentRestorer.ts diff --git a/packages/loader/src/AudioContentRestorer.ts b/packages/loader/src/AudioContentRestorer.ts new file mode 100644 index 0000000000..364d039456 --- /dev/null +++ b/packages/loader/src/AudioContentRestorer.ts @@ -0,0 +1,27 @@ +import { AssetPromise, ContentRestorer, request, AudioClip, TextureCubeFace } from "@galacean/engine-core"; +import { RequestConfig } from "@galacean/engine-core/types/asset/request"; +/** + * @internal + */ +export class AudioContentRestorer extends ContentRestorer { + constructor( + resource: AudioClip, + public url: string, + public requestConfig: RequestConfig + ) { + super(resource); + } + + override restoreContent(): AssetPromise { + return request(this.url, this.requestConfig) + .then((arrayBuffer) => { + // @ts-ignore + return resource._context.decodeAudioData(arrayBuffer); + }) + .then((audioBuffer) => { + const resource = this.resource; + resource.setData(audioBuffer); + return resource; + }); + } +} diff --git a/packages/loader/src/AudioLoader.ts b/packages/loader/src/AudioLoader.ts index 0cac564734..f64c92710c 100644 --- a/packages/loader/src/AudioLoader.ts +++ b/packages/loader/src/AudioLoader.ts @@ -4,20 +4,35 @@ import { AssetPromise, AssetType, LoadItem, - AudioManager, AudioClip, ResourceManager } from "@galacean/engine-core"; +import { RequestConfig } from "@galacean/engine-core/types/asset/request"; +import { AudioContentRestorer } from "./AudioContentRestorer"; @resourceLoader(AssetType.Audio, ["mp3", "ogg", "wav"], false) class AudioLoader extends Loader { load(item: LoadItem, resourceManager: ResourceManager): AssetPromise { return new AssetPromise((resolve, reject) => { - this.request(item.url, { type: "arraybuffer" }).then((arrayBuffer) => { - AudioManager.context + const url = item.url; + const requestConfig = { + ...item, + type: "arraybuffer" + }; + + this.request(url, requestConfig).then((arrayBuffer) => { + const audioClip = new AudioClip(resourceManager.engine); + // @ts-ignore + audioClip._context .decodeAudioData(arrayBuffer) .then((result: AudioBuffer) => { - const audioClip = new AudioClip(resourceManager.engine); audioClip.setData(result); + + if (url.indexOf("data:") !== 0) { + const index = url.lastIndexOf("/"); + audioClip.name = url.substring(index + 1); + } + + resourceManager.addContentRestorer(new AudioContentRestorer(audioClip, url, requestConfig)); resolve(audioClip); }) .catch((e) => { From 0ffdf1b43c3f3b58b812f03ae5745315570ed856 Mon Sep 17 00:00:00 2001 From: JujieX Date: Mon, 18 Dec 2023 14:31:10 +0800 Subject: [PATCH 16/21] fix: rename setData --- packages/core/src/audio/AudioClip.ts | 19 ++++++++++++++----- packages/core/src/audio/AudioSource.ts | 3 ++- packages/loader/src/AudioLoader.ts | 2 +- 3 files changed, 17 insertions(+), 7 deletions(-) diff --git a/packages/core/src/audio/AudioClip.ts b/packages/core/src/audio/AudioClip.ts index 5d5c31651b..fa5967bfea 100644 --- a/packages/core/src/audio/AudioClip.ts +++ b/packages/core/src/audio/AudioClip.ts @@ -8,13 +8,13 @@ import { AudioManager } from "./AudioManager"; export class AudioClip extends ReferResource { /** @internal */ _context: AudioContext; + private _audioBuffer: AudioBuffer | null = null; + /** * Name of clip. */ name: string; - private _audioBuffer: AudioBuffer; - /** * Number of discrete audio channels. */ @@ -39,20 +39,29 @@ export class AudioClip extends ReferResource { /** * Get the clip's audio buffer. */ - getData(): AudioBuffer { + getAudioSource(): AudioBuffer { return this._audioBuffer; } /** * Set audio buffer for the clip. */ - setData(value: AudioBuffer): void { + setAudioSource(value: AudioBuffer): void { this._audioBuffer = value; } - constructor(engine: Engine, name: string = null) { + constructor(engine: Engine, name: string = "") { super(engine); this.name = name; this._context = AudioManager.context; } + + /** + * @internal + */ + protected override _onDestroy(): void { + super._onDestroy(); + this._audioBuffer = null; + this.name = null; + } } diff --git a/packages/core/src/audio/AudioSource.ts b/packages/core/src/audio/AudioSource.ts index 166db80c04..cd32f8afcc 100644 --- a/packages/core/src/audio/AudioSource.ts +++ b/packages/core/src/audio/AudioSource.ts @@ -158,6 +158,7 @@ export class AudioSource extends Component { stop(): void { if (this._sourceNode && this._isPlaying) { this._sourceNode.stop(); + this._sourceNode.onended = this._sourceNode.disconnect; this._pausedTime = -1; } } @@ -231,7 +232,7 @@ export class AudioSource extends Component { this._sourceNode = AudioManager.context.createBufferSource(); const { _sourceNode: sourceNode } = this; - sourceNode.buffer = this._clip.getData(); + sourceNode.buffer = this._clip.getAudioSource(); sourceNode.onended = this._onPlayEnd.bind(this); sourceNode.playbackRate.value = this._playbackRate; diff --git a/packages/loader/src/AudioLoader.ts b/packages/loader/src/AudioLoader.ts index f64c92710c..27f919557d 100644 --- a/packages/loader/src/AudioLoader.ts +++ b/packages/loader/src/AudioLoader.ts @@ -25,7 +25,7 @@ class AudioLoader extends Loader { audioClip._context .decodeAudioData(arrayBuffer) .then((result: AudioBuffer) => { - audioClip.setData(result); + audioClip.setAudioSource(result); if (url.indexOf("data:") !== 0) { const index = url.lastIndexOf("/"); From 3e549b388c13f45a3eccef28688bd271ada69424 Mon Sep 17 00:00:00 2001 From: JujieX Date: Mon, 18 Dec 2023 16:23:16 +0800 Subject: [PATCH 17/21] fix: opt codes --- packages/core/src/audio/AudioSource.ts | 91 ++++++++++++++------------ 1 file changed, 48 insertions(+), 43 deletions(-) diff --git a/packages/core/src/audio/AudioSource.ts b/packages/core/src/audio/AudioSource.ts index cd32f8afcc..e7c7e76318 100644 --- a/packages/core/src/audio/AudioSource.ts +++ b/packages/core/src/audio/AudioSource.ts @@ -10,8 +10,6 @@ import { deepClone, ignoreClone } from "../clone/CloneManager"; export class AudioSource extends Component { @ignoreClone private _isPlaying: boolean = false; - @ignoreClone - private _isPlayOnAwake: boolean = false; @ignoreClone private _clip: AudioClip; @@ -56,17 +54,6 @@ export class AudioSource extends Component { return this._isPlaying; } - /** - * Whether the clip playing on Awake. - */ - get playOnAwake(): boolean { - return this._isPlayOnAwake; - } - - set playOnAwake(value: boolean) { - this._isPlayOnAwake = value; - } - /** * The volume of the audio source. 1.0 is origin volume. */ @@ -123,7 +110,7 @@ export class AudioSource extends Component { if (value !== this._loop) { this._loop = value; - if (this.isPlaying) { + if (this._isPlaying) { this._sourceNode.loop = this._loop; } } @@ -146,20 +133,25 @@ export class AudioSource extends Component { * Plays the clip. */ play(): void { - if (!this._isValidClip() || this._isPlaying) return; + if (!this._canPlay()) return; + if (this._isPlaying) return; this._initSourceNode(); this._startPlayback(this._pausedTime > 0 ? this._pausedTime : 0); + this._pausedTime = -1; + this._isPlaying = true; } /** * Stops playing the clip. */ stop(): void { - if (this._sourceNode && this._isPlaying) { - this._sourceNode.stop(); - this._sourceNode.onended = this._sourceNode.disconnect; + if (this._isPlaying) { + this._clearSourceNode(); + + this._isPlaying = false; this._pausedTime = -1; + this._absoluteStartTime = -1; } } @@ -167,13 +159,11 @@ export class AudioSource extends Component { * Pauses playing the clip. */ pause(): void { - if (this._sourceNode && this._isPlaying) { - this._pausedTime = this.time; - this._isPlaying = false; + if (this._isPlaying) { + this._clearSourceNode(); - this._sourceNode.disconnect(); - this._sourceNode.onended = null; - this._sourceNode = null; + this._isPlaying = false; + this._pausedTime = this.time; } } @@ -190,6 +180,7 @@ export class AudioSource extends Component { * @internal */ override _onEnable(): void { + if (!this._canPlay()) return; this.play(); } @@ -197,14 +188,8 @@ export class AudioSource extends Component { * @internal */ override _onDisable(): void { - this._isValidClip() && this.pause(); - } - - /** - * @internal - */ - override _onAwake(): void { - this._isPlayOnAwake && this.play(); + if (!this._canPlay()) return; + this.pause(); } /** @@ -219,36 +204,56 @@ export class AudioSource extends Component { } private _onPlayEnd(): void { - if (!this.isPlaying) return; - this._isPlaying = false; - this._absoluteStartTime = -1; - this._pausedTime = -1; + this.stop(); } private _initSourceNode(): void { - if (this._sourceNode) { - this._sourceNode.disconnect(); - } + this._clearSourceNode(); this._sourceNode = AudioManager.context.createBufferSource(); const { _sourceNode: sourceNode } = this; sourceNode.buffer = this._clip.getAudioSource(); - sourceNode.onended = this._onPlayEnd.bind(this); + sourceNode.onended = this._onPlayEnd; sourceNode.playbackRate.value = this._playbackRate; - sourceNode.loop = this._loop; + this._gainNode.gain.setValueAtTime(this._volume, AudioManager.context.currentTime); sourceNode.connect(this._gainNode); } + private _clearSourceNode(): void { + if (!this._sourceNode) return; + + this._sourceNode.stop(); + this._sourceNode.disconnect(); + this._sourceNode.onended = null; + this._sourceNode = null; + } + private _startPlayback(startTime: number): void { this._sourceNode.start(0, startTime); this._absoluteStartTime = this._absoluteStartTime > 0 ? this.engine.time.elapsedTime - startTime : this.engine.time.elapsedTime; - this._isPlaying = true; + } + + private _canPlay(): boolean { + return this._isValidClip() && this._isAudioContextRunning(); } private _isValidClip(): boolean { - return this._clip && this._clip.duration > 0; + if (!this._clip || this._clip.duration <= 0) { + return false; + } + + return true; + } + + private _isAudioContextRunning(): boolean { + if (AudioManager.context.state !== "running") { + console.warn("AudioContext is not running. User interaction required."); + return false; + } + + return true; } } From 36ba9de73dd3c7a41a156608f6a96880f7acf02b Mon Sep 17 00:00:00 2001 From: JujieX Date: Mon, 18 Dec 2023 16:44:50 +0800 Subject: [PATCH 18/21] feat: add playOnEnabled --- packages/core/src/audio/AudioSource.ts | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/packages/core/src/audio/AudioSource.ts b/packages/core/src/audio/AudioSource.ts index e7c7e76318..c1f8b99d2a 100644 --- a/packages/core/src/audio/AudioSource.ts +++ b/packages/core/src/audio/AudioSource.ts @@ -32,6 +32,9 @@ export class AudioSource extends Component { @deepClone private _loop: boolean = false; + /** If set to true, the audio component automatically begins to play on startup. */ + playOnEnabled = true; + /** * The audio cilp to play. */ @@ -44,6 +47,10 @@ export class AudioSource extends Component { if (lastClip !== value) { lastClip && lastClip._addReferCount(-1); this._clip = value; + + if (this.playOnEnabled && this.enabled) { + this.play(); + } } } @@ -180,15 +187,13 @@ export class AudioSource extends Component { * @internal */ override _onEnable(): void { - if (!this._canPlay()) return; - this.play(); + this.playOnEnabled && this.play(); } /** * @internal */ override _onDisable(): void { - if (!this._canPlay()) return; this.pause(); } From 03f5f3c8cde68e15caee174cee22c7584533c2c3 Mon Sep 17 00:00:00 2001 From: JujieX Date: Mon, 18 Dec 2023 16:56:25 +0800 Subject: [PATCH 19/21] fix: opt code --- packages/core/src/audio/AudioSource.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/core/src/audio/AudioSource.ts b/packages/core/src/audio/AudioSource.ts index c1f8b99d2a..3788a6444f 100644 --- a/packages/core/src/audio/AudioSource.ts +++ b/packages/core/src/audio/AudioSource.ts @@ -33,7 +33,7 @@ export class AudioSource extends Component { private _loop: boolean = false; /** If set to true, the audio component automatically begins to play on startup. */ - playOnEnabled = true; + playOnEnabled: boolean = true; /** * The audio cilp to play. From d841d41a079e9e739c97ea82b55432e058732b90 Mon Sep 17 00:00:00 2001 From: JujieX Date: Mon, 18 Dec 2023 16:58:52 +0800 Subject: [PATCH 20/21] fix: opt code --- packages/loader/src/AudioContentRestorer.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/loader/src/AudioContentRestorer.ts b/packages/loader/src/AudioContentRestorer.ts index 364d039456..ddb21b031c 100644 --- a/packages/loader/src/AudioContentRestorer.ts +++ b/packages/loader/src/AudioContentRestorer.ts @@ -20,7 +20,7 @@ export class AudioContentRestorer extends ContentRestorer { }) .then((audioBuffer) => { const resource = this.resource; - resource.setData(audioBuffer); + resource.setAudioSource(audioBuffer); return resource; }); } From 05e4014f4b2fa5a2c8ba155a317e7424ed967c69 Mon Sep 17 00:00:00 2001 From: GuoLei1990 Date: Tue, 3 Dec 2024 17:13:15 +0800 Subject: [PATCH 21/21] feat: test --- packages/core/src/asset/AssetType.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/core/src/asset/AssetType.ts b/packages/core/src/asset/AssetType.ts index 75ba517ecc..7bac4d02f3 100644 --- a/packages/core/src/asset/AssetType.ts +++ b/packages/core/src/asset/AssetType.ts @@ -56,3 +56,5 @@ export enum AssetType { /** Project asset. */ Project = "project" } + +