Skip to content

Commit

Permalink
audio-manager+README: More review changes
Browse files Browse the repository at this point in the history
  • Loading branch information
timothyhale committed Feb 26, 2024
1 parent aac296f commit 05390e5
Show file tree
Hide file tree
Showing 2 changed files with 91 additions and 60 deletions.
54 changes: 31 additions & 23 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,19 +45,44 @@ If `spatial` is set to `none`, all settings below are ignored.

### AudioManager

1. Instantiate a playable audio node by invoking the `load()` method of the `AudioManager` class. This method returns a promise that resolves to an audio node upon successful loading of the audio resource.
```js
// Load your audio on start(), so it is ready when you need it.
start() {
this.audio = globalAudioManager.load('path_to_audiofile')
.then((playableNode) => {
return playableNode;
});
}

// Play the file when you need it.
onClick() {
// Play's the audio without panning
this.audio.play();

const position: Float32Array = new Float32Array(3);
this.object.getPositionWorld(position);
// Play's from specified position relative to audio-listener
this.audio.play(position);
}

// Free up the resources, if audio is not needed anymore.
this.audio.destroy();
```

1. Instantiate a playable audio node by invoking the `load()` method of the `AudioManager` class. This method
returns a promise that resolves to an audio node upon successful loading of the audio resource. You can create
your own `AudioManager`, per scene for example, but `wonderland-spatial-audio` also provides a `globalAudioManager`.

2. The `play()` method initiates the playback of the audio node. For spatialized audio playback, supply the `play()` method with a position argument in the form `play(pos)`.

3. Each audio node instance exposes several properties for advanced configuration:

```js
/* The 'volume' property controls the amplitude of the audio output.
* It accepts a float value between 0.0 (silence) and 1.0 (maximum volume). */
/* The 'volume' property controls the amplitude of the audio output. */
this.audio.volume = 0.5;

/* The 'HRTF' property, when set to true, enables full HRTF (Head-Related Transfer Function) spatialization,
* as opposed to standard panning. */
* as opposed to standard panning, for better immersion. */
this.audio.HRTF = true;

/* The 'loop' property, when set to true, causes the audio to repeat indefinitely. */
Expand All @@ -66,25 +91,8 @@ this.audio.loop = true;

4. The `destroy()` method deallocates the resources associated with the audio node, effectively removing it from memory.

Here is an example of how to use the `AudioManager` class:

```js
// Load your audio on start(), so it is ready when you need it.
async start() {
this.audio = AudioManager.load('path_to_audiofile')
.then((playableNode) => {
return playableNode;
});
}

// Play the file when you need it.
onClick() {
this.audio.play();
}

// Free up the resources, if audio is not needed anymore.
this.audio.destroy();
```
:warning: As these AudioNodes only play from the specified position, use `audio-source` for any moving objects that
emmit sound.

## Considerations

Expand Down
97 changes: 60 additions & 37 deletions src/audio-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -139,28 +139,31 @@ class PlayableNode {
*
* @param posVec - An optional parameter representing the 3D spatial position of the audio source.
* If provided, the audio will be spatialized using a PannerNode based on the given position vector.
* @param pannerOptions - An optional parameter that is used to configure a WebAudio PannerNode. This is only for
* advanced configuration, beyond the spatial positioning.
* @returns A Promise that resolves once the audio playback starts.
* @throws If there is an error during the playback process, a warning is logged to the console.
* @throws If there is an error during the playback process.
*/
async play(posVec?: Float32Array, pannerOptions?: PannerOptions): Promise<void> {
async play(posVec?: Float32Array): Promise<void> {
if (this._destroy) {
throw 'playable-node: play() was called on destroyed node!';
}
if (this.isPlaying) {
this.stop();
} else if (_audioContext.state === 'suspended') {
await _unlockAudioContext();
}
try {
if (this.isPlaying) {
this.stop();
}
if (_audioContext.state === 'suspended') {
await _unlockAudioContext();
}
this._audioNode = new AudioBufferSourceNode(_audioContext, {
buffer: this._audioBuffer,
loop: this.loop,
});
if (pannerOptions !== undefined) {
this._pannerNode = new PannerNode(_audioContext, pannerOptions);
this._audioNode.connect(this._pannerNode).connect(this._gainNode);
}
else if (posVec !== undefined) {
} catch (e) {
throw (
'playable-node: Error occurred while creating the AudioBufferSourceNode: ' +
e
);
}
if (posVec !== undefined) {
try {
this._pannerNode = new PannerNode(_audioContext, {
coneInnerAngle: 360,
coneOuterAngle: 0,
Expand All @@ -170,24 +173,55 @@ class PlayableNode {
refDistance: 1.0,
rolloffFactor: 1.0,
panningModel: this.HRTF ? 'HRTF' : 'equalpower',
positionX: posVec[0],
positionY: posVec[2],
positionZ: -posVec[1],
positionX: posVec![0],
positionY: posVec![2],
positionZ: -posVec![1],
orientationX: 0,
orientationY: 0,
orientationZ: 1,
});
this._audioNode.connect(this._pannerNode).connect(this._gainNode);
}
else {
this._audioNode.connect(this._gainNode);
} catch (e) {
throw 'playable-node: Error occurred while creating the PannerNode: ' + e;
}
this._audioNode.addEventListener('ended', this.stop);
this._audioNode.start();
this._isPlaying = true;
this._audioNode.connect(this._pannerNode!).connect(this._gainNode);
} else {
this._audioNode.connect(this._gainNode);
}
this._audioNode.addEventListener('ended', this.stop);
this._audioNode.start();
this._isPlaying = true;
}

/**
* This is an alternative to the regular `play()` function, with advanced customization options for the distance
* model and directional fall-off.
*
* @param pannerOptions Sets the options for the WebAudio PannerNode.
* @returns A Promise that resolves once the audio playback starts.
* @throws If there is an error during the playback process.
*/
async playWithAdvancedConfig(pannerOptions: PannerOptions): Promise<void> {
if (this._destroy) {
throw 'playable-node: play() was called on destroyed node!';
}
if (this.isPlaying) {
this.stop();
} else if (_audioContext.state === 'suspended') {
await _unlockAudioContext();
}
try {
this._audioNode = new AudioBufferSourceNode(_audioContext, {
buffer: this._audioBuffer,
loop: this.loop,
});
this._pannerNode = new PannerNode(_audioContext, pannerOptions);
} catch (e) {
console.warn(e);
throw 'playable-node: Error occurred while creating the WebAudio nodes: ' + e;
}
this._audioNode.connect(this._pannerNode!).connect(this._gainNode);
this._audioNode.addEventListener('ended', this.stop);
this._audioNode.start();
this._isPlaying = true;
}

/**
Expand Down Expand Up @@ -228,13 +262,6 @@ class PlayableNode {

/**
* Free's up the audio resources after Node stopped playing.
*
* @example
* ```js
* this.audio.play() // plays entire audio file
* this.destroy() // frees resources
* this.audio.play() // does nothing
* ```
*/
destroy() {
if (this._isPlaying) {
Expand All @@ -244,12 +271,8 @@ class PlayableNode {
this._gainNode.disconnect();
}

/* Remove ability to re-trigger the sound */
this.play = this._removePlay.bind(this);
this.destroy = () => {};
}

private async _removePlay(): Promise<void> {}
}

export const globalAudioManager = new AudioManager();

0 comments on commit 05390e5

Please sign in to comment.