-
-
Notifications
You must be signed in to change notification settings - Fork 299
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Feat: setup audio component #1587
base: main
Are you sure you want to change the base?
Changes from all commits
4613a78
4a94c9d
2c0d994
69eeb8f
69b0fab
f4e2474
524f93c
fe318a4
98d8fd7
aad876d
319a125
062a4c4
08f957c
010b4e3
843ce26
e74db44
0ffdf1b
3e549b3
36ba9de
03f5f3c
d841d41
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,67 @@ | ||
import { Engine } from "../Engine"; | ||
import { ReferResource } from "../asset/ReferResource"; | ||
import { AudioManager } from "./AudioManager"; | ||
|
||
/** | ||
* Audio Clip | ||
*/ | ||
export class AudioClip extends ReferResource { | ||
/** @internal */ | ||
_context: AudioContext; | ||
private _audioBuffer: AudioBuffer | null = null; | ||
|
||
/** | ||
* Name of clip. | ||
*/ | ||
name: string; | ||
|
||
/** | ||
* Number of discrete audio channels. | ||
*/ | ||
get channels(): number { | ||
return this._audioBuffer.numberOfChannels; | ||
} | ||
|
||
/** | ||
* Sample rate, in samples per second. | ||
*/ | ||
get sampleRate(): number { | ||
return this._audioBuffer.sampleRate; | ||
} | ||
|
||
/** | ||
* Duration, in seconds. | ||
*/ | ||
get duration(): Readonly<number> { | ||
return this._audioBuffer.duration; | ||
} | ||
|
||
/** | ||
* Get the clip's audio buffer. | ||
*/ | ||
getAudioSource(): AudioBuffer { | ||
return this._audioBuffer; | ||
} | ||
|
||
/** | ||
* Set audio buffer for the clip. | ||
*/ | ||
setAudioSource(value: AudioBuffer): void { | ||
this._audioBuffer = value; | ||
} | ||
|
||
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; | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,50 @@ | ||
/** | ||
* @internal | ||
* Audio Manager | ||
*/ | ||
export class AudioManager { | ||
private static _context: AudioContext; | ||
private static _gainNode: GainNode; | ||
private static _unlocked: boolean = false; | ||
|
||
/** | ||
* Audio context | ||
*/ | ||
static get context(): AudioContext { | ||
if (!AudioManager._context) { | ||
AudioManager._context = new window.AudioContext(); | ||
} | ||
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; | ||
} | ||
|
||
/** | ||
* Audio GainNode. | ||
*/ | ||
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 { | ||
if (AudioManager._unlocked) { | ||
return; | ||
} | ||
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; | ||
} | ||
}); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,264 @@ | ||
import { Component } from "../Component"; | ||
import { Entity } from "../Entity"; | ||
import { AudioClip } from "./AudioClip"; | ||
import { AudioManager } from "./AudioManager"; | ||
import { deepClone, ignoreClone } from "../clone/CloneManager"; | ||
|
||
/** | ||
* Audio Source Component | ||
*/ | ||
export class AudioSource extends Component { | ||
GuoLei1990 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
@ignoreClone | ||
private _isPlaying: boolean = false; | ||
|
||
@ignoreClone | ||
private _clip: AudioClip; | ||
@deepClone | ||
private _gainNode: GainNode; | ||
@ignoreClone | ||
private _sourceNode: AudioBufferSourceNode | null = null; | ||
|
||
@deepClone | ||
private _pausedTime: number = -1; | ||
@ignoreClone | ||
private _absoluteStartTime: number = -1; | ||
|
||
@deepClone | ||
private _volume: number = 1; | ||
@deepClone | ||
private _lastVolume: number = 1; | ||
@deepClone | ||
private _playbackRate: number = 1; | ||
@deepClone | ||
private _loop: boolean = false; | ||
|
||
/** If set to true, the audio component automatically begins to play on startup. */ | ||
playOnEnabled: boolean = true; | ||
|
||
/** | ||
* The audio cilp to play. | ||
*/ | ||
get clip(): AudioClip { | ||
return this._clip; | ||
} | ||
|
||
set clip(value: AudioClip) { | ||
const lastClip = this._clip; | ||
if (lastClip !== value) { | ||
lastClip && lastClip._addReferCount(-1); | ||
this._clip = value; | ||
|
||
if (this.playOnEnabled && this.enabled) { | ||
this.play(); | ||
} | ||
} | ||
} | ||
|
||
/** | ||
* 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. | ||
*/ | ||
get volume(): number { | ||
return this._volume; | ||
} | ||
|
||
set volume(value: number) { | ||
this._volume = value; | ||
if (this._isPlaying) { | ||
this._gainNode.gain.setValueAtTime(value, AudioManager.context.currentTime); | ||
} | ||
} | ||
|
||
/** | ||
* The playback rate of the audio source, 1.0 is normal playback speed. | ||
*/ | ||
get playbackRate(): number { | ||
return this._playbackRate; | ||
} | ||
|
||
set playbackRate(value: number) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Your comments use There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. use rate |
||
this._playbackRate = value; | ||
if (this._isPlaying) { | ||
this._sourceNode.playbackRate.value = this._playbackRate; | ||
} | ||
} | ||
|
||
/** | ||
* Mutes / Unmutes the AudioSource. | ||
* Mute sets volume as 0, Un-Mute restore volume. | ||
*/ | ||
get mute(): boolean { | ||
return this.volume === 0; | ||
} | ||
|
||
set mute(value: boolean) { | ||
if (value) { | ||
this._lastVolume = this.volume; | ||
this.volume = 0; | ||
} else { | ||
this.volume = this._lastVolume; | ||
} | ||
} | ||
|
||
/** | ||
* 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._sourceNode.loop = this._loop; | ||
} | ||
} | ||
} | ||
|
||
/** | ||
* 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; | ||
} else { | ||
return this._pausedTime > 0 ? this._pausedTime : 0; | ||
} | ||
} | ||
|
||
/** | ||
* Plays the clip. | ||
*/ | ||
play(): void { | ||
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 { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What is the unique value that is different from There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. yes There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. now only has play stop and pause, removed unpause |
||
if (this._isPlaying) { | ||
this._clearSourceNode(); | ||
|
||
this._isPlaying = false; | ||
this._pausedTime = -1; | ||
this._absoluteStartTime = -1; | ||
} | ||
} | ||
|
||
/** | ||
* Pauses playing the clip. | ||
*/ | ||
pause(): void { | ||
if (this._isPlaying) { | ||
this._clearSourceNode(); | ||
|
||
this._isPlaying = false; | ||
this._pausedTime = this.time; | ||
} | ||
} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What is the unique value that is different from |
||
|
||
/** @internal */ | ||
constructor(entity: Entity) { | ||
super(entity); | ||
this._onPlayEnd = this._onPlayEnd.bind(this); | ||
|
||
this._gainNode = AudioManager.context.createGain(); | ||
this._gainNode.connect(AudioManager.gainNode); | ||
} | ||
|
||
/** | ||
* @internal | ||
*/ | ||
override _onEnable(): void { | ||
this.playOnEnabled && this.play(); | ||
} | ||
|
||
/** | ||
* @internal | ||
*/ | ||
override _onDisable(): void { | ||
this.pause(); | ||
} | ||
|
||
/** | ||
* @internal | ||
*/ | ||
protected override _onDestroy(): void { | ||
super._onDestroy(); | ||
if (this._clip) { | ||
this._clip._addReferCount(-1); | ||
this._clip = null; | ||
} | ||
} | ||
|
||
private _onPlayEnd(): void { | ||
this.stop(); | ||
} | ||
|
||
private _initSourceNode(): void { | ||
this._clearSourceNode(); | ||
this._sourceNode = AudioManager.context.createBufferSource(); | ||
|
||
const { _sourceNode: sourceNode } = this; | ||
sourceNode.buffer = this._clip.getAudioSource(); | ||
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; | ||
} | ||
|
||
private _canPlay(): boolean { | ||
return this._isValidClip() && this._isAudioContextRunning(); | ||
} | ||
|
||
private _isValidClip(): boolean { | ||
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; | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
export { AudioClip } from "./AudioClip"; | ||
export { AudioSource } from "./AudioSource"; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should do this when call
context
every time?