-
Notifications
You must be signed in to change notification settings - Fork 15
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
98000e5
commit c71c9cb
Showing
5 changed files
with
554 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,162 @@ | ||
|
||
# Audio Plugin | ||
|
||
The Blits Audio Plugin allows developers to integrate audio playback into their Blits application. This plugin provides a simple API for preloading, playing, controlling, and managing audio tracks, including managing volume, playback rate (pitch), and other settings. | ||
|
||
## Registering the Plugin | ||
|
||
The Audio Plugin is not included by default and needs to be explicitly registered before usage. This makes the plugin _tree-shakable_, meaning if audio is not required, it won't be part of the final app bundle. | ||
|
||
To register the plugin, you should import and register it before calling the `Blits.Launch()` method, as shown in the example below: | ||
|
||
```js | ||
// index.js | ||
|
||
import Blits from '@lightningjs/blits' | ||
// import the audio plugin | ||
import { audio } from '@lightningjs/blits/plugins' | ||
|
||
import App from './App.js' | ||
|
||
// Register the audio plugin with optional preload settings | ||
Blits.Plugin(audio, { | ||
preload: { | ||
background: '/assets/audio/background.mp3', | ||
jump: '/assets/audio/jump.mp3', | ||
}, | ||
}) | ||
|
||
Blits.Launch(App, 'app', { | ||
// launch settings | ||
}) | ||
``` | ||
|
||
The Audio Plugin can accept an optional `preload` configuration, which allows you to preload audio files during initialization. These files are stored in an internal library for easy access during gameplay. | ||
|
||
## Playing Audio Tracks | ||
|
||
Once the plugin is registered, you can play audio tracks either from the preloaded library or from a URL. Here’s an example of how to use it inside a Blits Component: | ||
|
||
```js | ||
Blits.Component('MyComponent', { | ||
hooks: { | ||
ready() { | ||
// Play a preloaded track and get a track controller | ||
const bgMusic = this.$audio.playTrack('background', { volume: 0.5 }) | ||
|
||
// Play a track from URL and get its controller | ||
const effect = this.$audio.playUrl('/assets/audio/victory.mp3', { volume: 0.8 }, 'victory') | ||
}, | ||
}, | ||
}) | ||
``` | ||
|
||
The `playTrack()` method allows you to play an audio track from the preloaded library, while `playUrl()` allows you to play a track from a specified URL. Both methods return a track controller object. | ||
|
||
### Track Controller Methods: | ||
- `stop()`: Stops the track and removes it from the active list. | ||
- `setVolume(volume)`: Adjusts the playback volume for the track. | ||
|
||
### Example Usage of Track Controller: | ||
```js | ||
Blits.Component('MyComponent', { | ||
hooks: { | ||
ready() { | ||
const bgMusic = this.$audio.playTrack('background', { volume: 0.5 }, 'bg-music') | ||
|
||
// set volume on the track | ||
bgMusic.setVolume(0.8) | ||
// stop the track | ||
bgMusic.stop() | ||
}, | ||
}, | ||
}) | ||
``` | ||
|
||
## Removing Preloaded Audio Tracks | ||
|
||
In some cases, you might want to remove a preloaded audio track from the library, freeing up memory or resources. You can do this using the `removeTrack()` method: | ||
|
||
```js | ||
Blits.Component('MyComponent', { | ||
input: { | ||
removeJumpTrack() { | ||
// Remove the 'jump' track from the preloaded library | ||
this.$audio.removeTrack('jump') | ||
}, | ||
}, | ||
}) | ||
``` | ||
|
||
The `removeTrack(key)` method deletes the specified track from the internal `tracks` object, preventing further access to it. | ||
|
||
## Preloading Audio Files | ||
|
||
The most efficient way to manage audio in your app is to preload audio files. The Audio Plugin supports preloading via the `preloadTracks()` method. You can pass in an object where each key is the track name, and each value is the URL of the audio file. | ||
|
||
```js | ||
Blits.Component('MyComponent', { | ||
hooks: { | ||
init() { | ||
this.$audio.preload({ | ||
jump: '/assets/audio/jump.mp3', | ||
hit: '/assets/audio/hit.mp3', | ||
}) | ||
}, | ||
}, | ||
}) | ||
``` | ||
|
||
Preloaded audio files are stored in an internal library, which you can reference when calling `playTrack()`. | ||
|
||
## Error Handling | ||
|
||
In cases where the `AudioContext` cannot be instantiated (e.g., due to browser limitations or disabled audio features), the Audio Plugin will automatically disable itself, preventing errors. If the `AudioContext` fails to initialize, an error message will be logged, and audio-related methods will return early without throwing additional errors. | ||
|
||
You can check whether audio is available via the `audioEnabled` property: | ||
|
||
```js | ||
Blits.Component('MyComponent', { | ||
hooks: { | ||
ready() { | ||
if (!this.$audio.audioEnabled) { | ||
console.warn('Audio is disabled on this platform.') | ||
} | ||
}, | ||
}, | ||
}) | ||
``` | ||
|
||
This ensures that your app continues to function even if audio features are not supported or available. | ||
|
||
## Public API | ||
|
||
The Audio Plugin provides the following methods and properties: | ||
|
||
- `playTrack(key, { volume, pitch }, trackId)`: Plays a preloaded audio track and returns a track controller. | ||
- `playUrl(url, { volume, pitch }, trackId)`: Plays an audio track from a URL and returns a track controller. | ||
- `pause()`: Pauses the current audio context. | ||
- `resume()`: Resumes the current audio context. | ||
- `stop(trackId)`: Stops a specific audio track by its ID. | ||
- `stopAll()`: Stops all currently playing audio tracks. | ||
- `setVolume(trackId, volume)`: Sets the volume for a specific track by its ID. | ||
- `preload(tracks)`: Preloads a set of audio tracks into the internal library. | ||
- `removeTrack(key)`: Removes a preloaded track from the library. | ||
- `destroy()`: Destroys the audio context and stops all tracks. | ||
- `get activeTracks` : Return an Object of Active Track Controllers currently being played | ||
- `get audioEnabled`: Returns `true` if the `AudioContext` is available and audio is enabled. | ||
- `get tracks` : Return an Object of preloaded Tracks | ||
|
||
## Destroying the Plugin | ||
|
||
When you're done with the audio functionality, you can clean up the plugin and close the `AudioContext` by calling the `destroy()` method. This is especially useful when you no longer need audio in your application: | ||
|
||
```js | ||
Blits.Component('MyComponent', { | ||
hooks: { | ||
exit() { | ||
this.$audio.destroy() | ||
}, | ||
}, | ||
}) | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,201 @@ | ||
/* | ||
* Copyright 2024 Comcast Cable Communications Management, LLC | ||
* Licensed under the Apache License, Version 2.0 (the "License"); | ||
* you may not use this file except in compliance with the License. | ||
* You may obtain a copy of the License at | ||
* | ||
* http://www.apache.org/licenses/LICENSE-2.0 | ||
* | ||
* Unless required by applicable law or agreed to in writing, software | ||
* distributed under the License is distributed on an "AS IS" BASIS, | ||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
* See the License for the specific language governing permissions and | ||
* limitations under the License. | ||
* | ||
* SPDX-License-Identifier: Apache-2.0 | ||
*/ | ||
|
||
import { Log } from '../lib/log.js' | ||
|
||
export default { | ||
name: 'audio', | ||
plugin(options = {}) { | ||
let audioContext = undefined | ||
let audioEnabled = true | ||
let activeTracks = {} // Store all active track controllers | ||
|
||
try { | ||
audioContext = new AudioContext() | ||
} catch (e) { | ||
Log.error('AudioContext is not supported or failed to initialize. Audio will be disabled.') | ||
audioEnabled = false | ||
} | ||
|
||
let tracks = {} | ||
|
||
const loadAudioData = async (url) => { | ||
if (audioEnabled === false) return | ||
try { | ||
const response = await fetch(url) | ||
|
||
if (!response.ok) { | ||
throw Error(`${response.status} - ${response.statusText}`) | ||
} | ||
|
||
const arrayBuffer = await response.arrayBuffer() | ||
return audioContext.decodeAudioData(arrayBuffer) | ||
} catch (e) { | ||
Log.error(`Failed to load audio from ${url}: ${e}`) | ||
} | ||
} | ||
|
||
const preloadTracks = async (trackList) => { | ||
if (audioEnabled === false) return | ||
for (const [key, url] of Object.entries(trackList)) { | ||
const audioData = await loadAudioData(url) | ||
if (audioData) { | ||
tracks[key] = audioData | ||
} | ||
} | ||
} | ||
|
||
const createTrackController = (source, gainNode, trackId) => { | ||
return { | ||
stop() { | ||
try { | ||
source.stop() | ||
} catch (e) { | ||
Log.warn('Error stopping audio track', trackId) | ||
} | ||
|
||
delete activeTracks[trackId] | ||
}, | ||
setVolume(volume) { | ||
gainNode.gain.value = volume | ||
}, | ||
get source() { | ||
return source | ||
}, | ||
get gainNode() { | ||
return gainNode | ||
}, | ||
} | ||
} | ||
|
||
const playAudioBuffer = (buffer, trackId, { volume = 1, pitch = 1 } = {}) => { | ||
if (audioEnabled === false || audioContext === undefined) { | ||
Log.warn('AudioContext not available. Cannot play audio.') | ||
return | ||
} | ||
|
||
const source = audioContext.createBufferSource() | ||
source.buffer = buffer | ||
source.playbackRate.value = pitch | ||
|
||
const gainNode = audioContext.createGain() | ||
gainNode.gain.value = volume | ||
|
||
source.connect(gainNode) | ||
gainNode.connect(audioContext.destination) | ||
|
||
source.onended = () => { | ||
delete activeTracks[trackId] | ||
Log.info(`Track ${trackId} finished playing.`) | ||
} | ||
|
||
// Create and store the track controller | ||
const trackController = createTrackController(source, gainNode, trackId) | ||
activeTracks[trackId] = trackController | ||
|
||
source.start() | ||
|
||
return trackController | ||
} | ||
|
||
const playTrack = (key, options = {}, trackId = key) => { | ||
if (audioEnabled === false) { | ||
Log.warn('AudioContext not available. Cannot play track.') | ||
return | ||
} | ||
if (tracks[key] !== undefined) { | ||
return playAudioBuffer(tracks[key], trackId, options) | ||
} else { | ||
Log.warn(`Track ${key} not found in the library.`) | ||
} | ||
} | ||
|
||
const playUrl = async (url, options = {}, trackId = url) => { | ||
if (audioEnabled === false) return | ||
const audioData = await loadAudioData(url) | ||
if (audioData !== undefined) { | ||
return playAudioBuffer(audioData, trackId, options) | ||
} | ||
} | ||
|
||
const stop = (trackId) => { | ||
if (audioEnabled === false || activeTracks[trackId] === undefined) return | ||
activeTracks[trackId].stop() | ||
} | ||
|
||
const stopAll = () => { | ||
if (audioEnabled === false) return | ||
while (Object.keys(activeTracks).length > 0) { | ||
const trackId = Object.keys(activeTracks)[0] | ||
stop(trackId) | ||
} | ||
} | ||
|
||
const removeTrack = (key) => { | ||
if (tracks[key] !== undefined) { | ||
// stop if the track happens to be active as well | ||
if (activeTracks[key] !== undefined) { | ||
activeTracks[key].stop() | ||
} | ||
|
||
delete tracks[key] | ||
Log.info(`Track ${key} removed from the preloaded library.`) | ||
} else { | ||
Log.warn(`Track ${key} not found in the library.`) | ||
} | ||
} | ||
|
||
const destroy = () => { | ||
if (audioEnabled === false) return | ||
stopAll() // Stop all active tracks before destroying | ||
audioContext.close() | ||
} | ||
|
||
if (options.preload === true && audioEnabled === true) { | ||
preloadTracks(options.preload) | ||
} | ||
|
||
// Public API for the Audio Plugin | ||
return { | ||
get audioEnabled() { | ||
return audioEnabled | ||
}, | ||
get activeTracks() { | ||
return activeTracks | ||
}, | ||
get tracks() { | ||
return tracks | ||
}, | ||
get state() { | ||
return audioContext.state | ||
}, | ||
destroy, // Destroy the audio context and stop all tracks | ||
pause() { | ||
return audioContext.suspend() | ||
}, | ||
playTrack, // Play a preloaded track by its key and return the track controller | ||
playUrl, // Play a track directly from a URL and return the track controller | ||
preload: preloadTracks, // Preload a set of audio tracks | ||
resume() { | ||
return audioContext.resume() | ||
}, | ||
removeTrack, // Remove a track from the preloaded library | ||
stop, // Stop a specific track by its ID | ||
stopAll, // Stop all active tracks | ||
} | ||
}, | ||
} |
Oops, something went wrong.