Skip to content

Commit c71c9cb

Browse files
committed
Add Audio plugin
1 parent 98000e5 commit c71c9cb

File tree

5 files changed

+554
-0
lines changed

5 files changed

+554
-0
lines changed

docs/_sidebar.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,3 +28,4 @@
2828
- [Language](/plugins/language.md)
2929
- [Theme](/plugins/theme.md)
3030
- [Global App State](/plugins/global_app_state.md)
31+
- [Audio](/plugins/audio.md)

docs/plugins/audio.md

Lines changed: 162 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,162 @@
1+
2+
# Audio Plugin
3+
4+
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.
5+
6+
## Registering the Plugin
7+
8+
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.
9+
10+
To register the plugin, you should import and register it before calling the `Blits.Launch()` method, as shown in the example below:
11+
12+
```js
13+
// index.js
14+
15+
import Blits from '@lightningjs/blits'
16+
// import the audio plugin
17+
import { audio } from '@lightningjs/blits/plugins'
18+
19+
import App from './App.js'
20+
21+
// Register the audio plugin with optional preload settings
22+
Blits.Plugin(audio, {
23+
preload: {
24+
background: '/assets/audio/background.mp3',
25+
jump: '/assets/audio/jump.mp3',
26+
},
27+
})
28+
29+
Blits.Launch(App, 'app', {
30+
// launch settings
31+
})
32+
```
33+
34+
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.
35+
36+
## Playing Audio Tracks
37+
38+
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:
39+
40+
```js
41+
Blits.Component('MyComponent', {
42+
hooks: {
43+
ready() {
44+
// Play a preloaded track and get a track controller
45+
const bgMusic = this.$audio.playTrack('background', { volume: 0.5 })
46+
47+
// Play a track from URL and get its controller
48+
const effect = this.$audio.playUrl('/assets/audio/victory.mp3', { volume: 0.8 }, 'victory')
49+
},
50+
},
51+
})
52+
```
53+
54+
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.
55+
56+
### Track Controller Methods:
57+
- `stop()`: Stops the track and removes it from the active list.
58+
- `setVolume(volume)`: Adjusts the playback volume for the track.
59+
60+
### Example Usage of Track Controller:
61+
```js
62+
Blits.Component('MyComponent', {
63+
hooks: {
64+
ready() {
65+
const bgMusic = this.$audio.playTrack('background', { volume: 0.5 }, 'bg-music')
66+
67+
// set volume on the track
68+
bgMusic.setVolume(0.8)
69+
// stop the track
70+
bgMusic.stop()
71+
},
72+
},
73+
})
74+
```
75+
76+
## Removing Preloaded Audio Tracks
77+
78+
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:
79+
80+
```js
81+
Blits.Component('MyComponent', {
82+
input: {
83+
removeJumpTrack() {
84+
// Remove the 'jump' track from the preloaded library
85+
this.$audio.removeTrack('jump')
86+
},
87+
},
88+
})
89+
```
90+
91+
The `removeTrack(key)` method deletes the specified track from the internal `tracks` object, preventing further access to it.
92+
93+
## Preloading Audio Files
94+
95+
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.
96+
97+
```js
98+
Blits.Component('MyComponent', {
99+
hooks: {
100+
init() {
101+
this.$audio.preload({
102+
jump: '/assets/audio/jump.mp3',
103+
hit: '/assets/audio/hit.mp3',
104+
})
105+
},
106+
},
107+
})
108+
```
109+
110+
Preloaded audio files are stored in an internal library, which you can reference when calling `playTrack()`.
111+
112+
## Error Handling
113+
114+
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.
115+
116+
You can check whether audio is available via the `audioEnabled` property:
117+
118+
```js
119+
Blits.Component('MyComponent', {
120+
hooks: {
121+
ready() {
122+
if (!this.$audio.audioEnabled) {
123+
console.warn('Audio is disabled on this platform.')
124+
}
125+
},
126+
},
127+
})
128+
```
129+
130+
This ensures that your app continues to function even if audio features are not supported or available.
131+
132+
## Public API
133+
134+
The Audio Plugin provides the following methods and properties:
135+
136+
- `playTrack(key, { volume, pitch }, trackId)`: Plays a preloaded audio track and returns a track controller.
137+
- `playUrl(url, { volume, pitch }, trackId)`: Plays an audio track from a URL and returns a track controller.
138+
- `pause()`: Pauses the current audio context.
139+
- `resume()`: Resumes the current audio context.
140+
- `stop(trackId)`: Stops a specific audio track by its ID.
141+
- `stopAll()`: Stops all currently playing audio tracks.
142+
- `setVolume(trackId, volume)`: Sets the volume for a specific track by its ID.
143+
- `preload(tracks)`: Preloads a set of audio tracks into the internal library.
144+
- `removeTrack(key)`: Removes a preloaded track from the library.
145+
- `destroy()`: Destroys the audio context and stops all tracks.
146+
- `get activeTracks` : Return an Object of Active Track Controllers currently being played
147+
- `get audioEnabled`: Returns `true` if the `AudioContext` is available and audio is enabled.
148+
- `get tracks` : Return an Object of preloaded Tracks
149+
150+
## Destroying the Plugin
151+
152+
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:
153+
154+
```js
155+
Blits.Component('MyComponent', {
156+
hooks: {
157+
exit() {
158+
this.$audio.destroy()
159+
},
160+
},
161+
})
162+
```

src/plugins/audio.js

Lines changed: 201 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,201 @@
1+
/*
2+
* Copyright 2024 Comcast Cable Communications Management, LLC
3+
* Licensed under the Apache License, Version 2.0 (the "License");
4+
* you may not use this file except in compliance with the License.
5+
* You may obtain a copy of the License at
6+
*
7+
* http://www.apache.org/licenses/LICENSE-2.0
8+
*
9+
* Unless required by applicable law or agreed to in writing, software
10+
* distributed under the License is distributed on an "AS IS" BASIS,
11+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
* See the License for the specific language governing permissions and
13+
* limitations under the License.
14+
*
15+
* SPDX-License-Identifier: Apache-2.0
16+
*/
17+
18+
import { Log } from '../lib/log.js'
19+
20+
export default {
21+
name: 'audio',
22+
plugin(options = {}) {
23+
let audioContext = undefined
24+
let audioEnabled = true
25+
let activeTracks = {} // Store all active track controllers
26+
27+
try {
28+
audioContext = new AudioContext()
29+
} catch (e) {
30+
Log.error('AudioContext is not supported or failed to initialize. Audio will be disabled.')
31+
audioEnabled = false
32+
}
33+
34+
let tracks = {}
35+
36+
const loadAudioData = async (url) => {
37+
if (audioEnabled === false) return
38+
try {
39+
const response = await fetch(url)
40+
41+
if (!response.ok) {
42+
throw Error(`${response.status} - ${response.statusText}`)
43+
}
44+
45+
const arrayBuffer = await response.arrayBuffer()
46+
return audioContext.decodeAudioData(arrayBuffer)
47+
} catch (e) {
48+
Log.error(`Failed to load audio from ${url}: ${e}`)
49+
}
50+
}
51+
52+
const preloadTracks = async (trackList) => {
53+
if (audioEnabled === false) return
54+
for (const [key, url] of Object.entries(trackList)) {
55+
const audioData = await loadAudioData(url)
56+
if (audioData) {
57+
tracks[key] = audioData
58+
}
59+
}
60+
}
61+
62+
const createTrackController = (source, gainNode, trackId) => {
63+
return {
64+
stop() {
65+
try {
66+
source.stop()
67+
} catch (e) {
68+
Log.warn('Error stopping audio track', trackId)
69+
}
70+
71+
delete activeTracks[trackId]
72+
},
73+
setVolume(volume) {
74+
gainNode.gain.value = volume
75+
},
76+
get source() {
77+
return source
78+
},
79+
get gainNode() {
80+
return gainNode
81+
},
82+
}
83+
}
84+
85+
const playAudioBuffer = (buffer, trackId, { volume = 1, pitch = 1 } = {}) => {
86+
if (audioEnabled === false || audioContext === undefined) {
87+
Log.warn('AudioContext not available. Cannot play audio.')
88+
return
89+
}
90+
91+
const source = audioContext.createBufferSource()
92+
source.buffer = buffer
93+
source.playbackRate.value = pitch
94+
95+
const gainNode = audioContext.createGain()
96+
gainNode.gain.value = volume
97+
98+
source.connect(gainNode)
99+
gainNode.connect(audioContext.destination)
100+
101+
source.onended = () => {
102+
delete activeTracks[trackId]
103+
Log.info(`Track ${trackId} finished playing.`)
104+
}
105+
106+
// Create and store the track controller
107+
const trackController = createTrackController(source, gainNode, trackId)
108+
activeTracks[trackId] = trackController
109+
110+
source.start()
111+
112+
return trackController
113+
}
114+
115+
const playTrack = (key, options = {}, trackId = key) => {
116+
if (audioEnabled === false) {
117+
Log.warn('AudioContext not available. Cannot play track.')
118+
return
119+
}
120+
if (tracks[key] !== undefined) {
121+
return playAudioBuffer(tracks[key], trackId, options)
122+
} else {
123+
Log.warn(`Track ${key} not found in the library.`)
124+
}
125+
}
126+
127+
const playUrl = async (url, options = {}, trackId = url) => {
128+
if (audioEnabled === false) return
129+
const audioData = await loadAudioData(url)
130+
if (audioData !== undefined) {
131+
return playAudioBuffer(audioData, trackId, options)
132+
}
133+
}
134+
135+
const stop = (trackId) => {
136+
if (audioEnabled === false || activeTracks[trackId] === undefined) return
137+
activeTracks[trackId].stop()
138+
}
139+
140+
const stopAll = () => {
141+
if (audioEnabled === false) return
142+
while (Object.keys(activeTracks).length > 0) {
143+
const trackId = Object.keys(activeTracks)[0]
144+
stop(trackId)
145+
}
146+
}
147+
148+
const removeTrack = (key) => {
149+
if (tracks[key] !== undefined) {
150+
// stop if the track happens to be active as well
151+
if (activeTracks[key] !== undefined) {
152+
activeTracks[key].stop()
153+
}
154+
155+
delete tracks[key]
156+
Log.info(`Track ${key} removed from the preloaded library.`)
157+
} else {
158+
Log.warn(`Track ${key} not found in the library.`)
159+
}
160+
}
161+
162+
const destroy = () => {
163+
if (audioEnabled === false) return
164+
stopAll() // Stop all active tracks before destroying
165+
audioContext.close()
166+
}
167+
168+
if (options.preload === true && audioEnabled === true) {
169+
preloadTracks(options.preload)
170+
}
171+
172+
// Public API for the Audio Plugin
173+
return {
174+
get audioEnabled() {
175+
return audioEnabled
176+
},
177+
get activeTracks() {
178+
return activeTracks
179+
},
180+
get tracks() {
181+
return tracks
182+
},
183+
get state() {
184+
return audioContext.state
185+
},
186+
destroy, // Destroy the audio context and stop all tracks
187+
pause() {
188+
return audioContext.suspend()
189+
},
190+
playTrack, // Play a preloaded track by its key and return the track controller
191+
playUrl, // Play a track directly from a URL and return the track controller
192+
preload: preloadTracks, // Preload a set of audio tracks
193+
resume() {
194+
return audioContext.resume()
195+
},
196+
removeTrack, // Remove a track from the preloaded library
197+
stop, // Stop a specific track by its ID
198+
stopAll, // Stop all active tracks
199+
}
200+
},
201+
}

0 commit comments

Comments
 (0)