Skip to content
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

Open
wants to merge 21 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions packages/core/src/asset/AssetType.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,8 @@ export enum AssetType {
Font = "Font",
/** Source Font, include ttf、 otf and woff. */
SourceFont = "SourceFont",
/** AudioClip, inclue ogg, wav and mp3 */
Audio = "Audio",
/** Project asset. */
Project = "project"
}
67 changes: 67 additions & 0 deletions packages/core/src/audio/AudioClip.ts
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;
}
}
50 changes: 50 additions & 0 deletions packages/core/src/audio/AudioManager.ts
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);
Copy link
Member

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?

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;
}
});
}
}
264 changes: 264 additions & 0 deletions packages/core/src/audio/AudioSource.ts
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) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Your comments use speed, API use rate,try to use the unified concept

Copy link
Member Author

Choose a reason for hiding this comment

The 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 {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is the unique value that is different from pause from the perspective of usage? It's call play form 0 next time?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes

Copy link
Member Author

Choose a reason for hiding this comment

The 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;
}
}
Copy link
Member

@GuoLei1990 GuoLei1990 Jun 17, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is the unique value that is different from play from the perspective of usage


/** @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;
}
}
2 changes: 2 additions & 0 deletions packages/core/src/audio/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export { AudioClip } from "./AudioClip";
export { AudioSource } from "./AudioSource";
Loading