lively 0.12.0 → 0.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,87 @@
1
+ // Base Audio Output Node - handles routing to analysis and/or audio device
2
+
3
+ export class Output {
4
+ #audioContext = null;
5
+ #gainNode = null;
6
+ #analysisNode = null;
7
+ #destination = null;
8
+
9
+ constructor(audioContext, initialGain = 1.0) {
10
+ if (!audioContext || typeof audioContext.createGain !== 'function') {
11
+ throw new Error('Output requires a valid AudioContext');
12
+ }
13
+
14
+ this.#audioContext = audioContext;
15
+
16
+ // Initialize audio nodes
17
+ this.#gainNode = this.#audioContext.createGain();
18
+ this.#destination = this.#audioContext.destination;
19
+
20
+ // Default volume is 1.0
21
+ this.#gainNode.gain.value = initialGain;
22
+
23
+ // Connect directly to destination by default
24
+ this.#gainNode.connect(this.#destination);
25
+ }
26
+
27
+ // Synchronous getter for audioContext
28
+ get audioContext() {
29
+ return this.#audioContext;
30
+ }
31
+
32
+ // Connect an analysis node for visualization
33
+ connectAnalysis(analysisNode) {
34
+ if (this.#analysisNode) {
35
+ this.#gainNode.disconnect(this.#analysisNode.input);
36
+ }
37
+ this.#analysisNode = analysisNode;
38
+ this.#gainNode.disconnect(this.#destination);
39
+ this.#gainNode.connect(this.#analysisNode.input);
40
+ this.#analysisNode.connect(this.#destination);
41
+ }
42
+
43
+ // Remove analysis and connect directly to destination
44
+ disconnectAnalysis() {
45
+ if (this.#analysisNode) {
46
+ this.#gainNode.disconnect(this.#analysisNode.input);
47
+ this.#analysisNode.disconnect(this.#destination);
48
+ this.#analysisNode = null;
49
+ }
50
+ this.#gainNode.connect(this.#destination);
51
+ }
52
+
53
+ get input() {
54
+ return this.#gainNode;
55
+ }
56
+
57
+ // Public getter for gain node (for testing)
58
+ get gainNode() {
59
+ return this.#gainNode;
60
+ }
61
+
62
+ // Get current volume from the gain node
63
+ get volume() {
64
+ return this.#gainNode.gain.value;
65
+ }
66
+
67
+ // Apply volume to the gain node (called by Controller)
68
+ setVolume(volume) {
69
+ this.#gainNode.gain.value = volume;
70
+ }
71
+
72
+ // Clean up resources
73
+ dispose() {
74
+ if (this.#gainNode) {
75
+ this.#gainNode.disconnect();
76
+ this.#gainNode = null;
77
+ }
78
+
79
+ if (this.#analysisNode) {
80
+ this.#analysisNode.disconnect();
81
+ this.#analysisNode = null;
82
+ }
83
+
84
+ this.#audioContext = null;
85
+ this.#destination = null;
86
+ }
87
+ }
@@ -0,0 +1,34 @@
1
+ // Base Sound Class
2
+ export class Sound {
3
+ createEnvelope(audioContext, gainNode, attack, decay, sustain, release, duration) {
4
+ const now = audioContext.currentTime;
5
+ const initialGain = gainNode.gain.value;
6
+
7
+ gainNode.gain.setValueAtTime(0, now);
8
+ gainNode.gain.linearRampToValueAtTime(initialGain, now + attack);
9
+ gainNode.gain.linearRampToValueAtTime(initialGain * sustain, now + attack + decay);
10
+ if (duration - release > attack + decay) {
11
+ gainNode.gain.setValueAtTime(initialGain * sustain, now + duration - release);
12
+ }
13
+ gainNode.gain.linearRampToValueAtTime(0, now + duration);
14
+ }
15
+
16
+ // Public interface - takes output and extracts what's needed.
17
+ play(output) {
18
+ // Return early if volume is zero (muted) - subclasses can override this behavior:
19
+ if (output.volume <= 0) return;
20
+
21
+ this.start(output);
22
+ }
23
+
24
+ // Internal method to be implemented by subclasses.
25
+ start(output) {
26
+ throw new Error('start() method must be implemented by subclass');
27
+ }
28
+
29
+ // Default stop implementation (no-op for most sounds).
30
+ stop() {
31
+ // Most sounds are fire-and-forget, so this is a no-op by default.
32
+ // Subclasses can override this for continuous sounds like music.
33
+ }
34
+ }
@@ -0,0 +1,265 @@
1
+ // Analysis Node - handles waveform visualization and audio quality monitoring
2
+ export class Visualizer {
3
+ constructor(audioContext, waveformCanvas = null, alertCanvas = null) {
4
+ this.audioContext = audioContext;
5
+ this.analyser = audioContext.createAnalyser();
6
+ this.analyser.fftSize = 2048;
7
+ this.analyser.smoothingTimeConstant = 0.8;
8
+
9
+ this.dataArray = new Uint8Array(this.analyser.frequencyBinCount);
10
+ this.canvas = waveformCanvas;
11
+ this.canvasContext = null;
12
+ this.alertCanvas = alertCanvas;
13
+ this.alertContext = null;
14
+ this.animationId = null;
15
+
16
+ // Analysis state
17
+ this.clipDetected = false;
18
+ this.popDetected = false;
19
+ this.clipThreshold = 0.95;
20
+ this.popThreshold = 0.1;
21
+ this.clipCounter = 0;
22
+ this.popCounter = 0;
23
+
24
+ // Rolling peak detection
25
+ this.peakHistory = new Array(60).fill(0);
26
+ this.peakHistoryIndex = 0;
27
+ this.rollingPeak = 0;
28
+
29
+ // Only set up canvas and start visualization if canvases are provided
30
+ if (this.canvas || this.alertCanvas) {
31
+ try {
32
+ this.setupCanvas();
33
+ this.startVisualization();
34
+ } catch (error) {
35
+ console.warn('Visualizer canvas setup failed:', error.message);
36
+ this.canvasContext = null;
37
+ this.alertContext = null;
38
+ }
39
+ }
40
+ }
41
+
42
+ get input() {
43
+ return this.analyser;
44
+ }
45
+
46
+ connect(destination) {
47
+ this.analyser.connect(destination);
48
+ }
49
+
50
+ disconnect(destination) {
51
+ this.analyser.disconnect(destination);
52
+ }
53
+
54
+ setupCanvas() {
55
+ // Use provided canvases or fall back to finding/creating them
56
+ if (!this.canvas) {
57
+ this.canvas = document.getElementById('waveform-canvas');
58
+ if (!this.canvas) {
59
+ this.canvas = document.createElement('canvas');
60
+ this.canvas.id = 'waveform-canvas';
61
+ this.canvas.width = 800;
62
+ this.canvas.height = 200;
63
+ this.canvas.style.border = '2px solid #333';
64
+ this.canvas.style.borderRadius = '8px';
65
+ this.canvas.style.background = '#000';
66
+ document.body.appendChild(this.canvas);
67
+ }
68
+ }
69
+
70
+ if (this.canvas) {
71
+ this.canvasContext = this.canvas.getContext('2d');
72
+ }
73
+
74
+ if (!this.alertCanvas) {
75
+ this.alertCanvas = document.getElementById('alert-canvas');
76
+ if (!this.alertCanvas) {
77
+ this.alertCanvas = document.createElement('canvas');
78
+ this.alertCanvas.id = 'alert-canvas';
79
+ this.alertCanvas.width = 800;
80
+ this.alertCanvas.height = 100;
81
+ this.alertCanvas.style.border = '2px solid #333';
82
+ this.alertCanvas.style.borderRadius = '8px';
83
+ this.alertCanvas.style.background = '#111';
84
+ this.alertCanvas.style.marginTop = '10px';
85
+ if (this.canvas && this.canvas.parentNode) {
86
+ this.canvas.parentNode.insertBefore(this.alertCanvas, this.canvas.nextSibling);
87
+ } else {
88
+ document.body.appendChild(this.alertCanvas);
89
+ }
90
+ }
91
+ }
92
+
93
+ if (this.alertCanvas) {
94
+ this.alertContext = this.alertCanvas.getContext('2d');
95
+ }
96
+ }
97
+
98
+ startVisualization() {
99
+ // Don't start visualization if canvas contexts are not available
100
+ if (!this.canvasContext || !this.alertContext) {
101
+ return;
102
+ }
103
+
104
+ const draw = () => {
105
+ this.animationId = requestAnimationFrame(draw);
106
+ this.analyser.getByteTimeDomainData(this.dataArray);
107
+ this.analyzeAudioQuality();
108
+ this.drawWaveform();
109
+ this.drawQualityIndicators();
110
+ };
111
+ draw();
112
+ }
113
+
114
+ analyzeAudioQuality() {
115
+ this.clipDetected = false;
116
+ this.popDetected = false;
117
+ let currentPeak = 0;
118
+
119
+ for (let i = 0; i < this.dataArray.length; i++) {
120
+ const sample = (this.dataArray[i] - 128) / 128.0;
121
+ const sampleAbs = Math.abs(sample);
122
+ currentPeak = Math.max(currentPeak, sampleAbs);
123
+
124
+ if (sampleAbs > this.clipThreshold) {
125
+ this.clipDetected = true;
126
+ this.clipCounter = Math.min(this.clipCounter + 1, 60);
127
+ }
128
+
129
+ if (i > 0) {
130
+ const previousSample = (this.dataArray[i-1] - 128) / 128.0;
131
+ const amplitudeChange = Math.abs(sample - previousSample);
132
+ if (amplitudeChange > this.popThreshold) {
133
+ this.popDetected = true;
134
+ this.popCounter = Math.min(this.popCounter + 1, 60);
135
+ }
136
+ }
137
+ }
138
+
139
+ this.peakHistory[this.peakHistoryIndex] = currentPeak;
140
+ this.peakHistoryIndex = (this.peakHistoryIndex + 1) % this.peakHistory.length;
141
+ this.rollingPeak = Math.max(...this.peakHistory);
142
+
143
+ if (!this.clipDetected) this.clipCounter = Math.max(this.clipCounter - 1, 0);
144
+ if (!this.popDetected) this.popCounter = Math.max(this.popCounter - 1, 0);
145
+ }
146
+
147
+ drawWaveform() {
148
+ if (!this.canvasContext || !this.canvas) return;
149
+
150
+ this.canvasContext.fillStyle = '#000';
151
+ this.canvasContext.fillRect(0, 0, this.canvas.width, this.canvas.height);
152
+
153
+ this.canvasContext.lineWidth = 2;
154
+ this.canvasContext.strokeStyle = this.clipCounter > 0 ? '#ff0000' :
155
+ this.popCounter > 0 ? '#ff8800' : '#00ff41';
156
+
157
+ this.canvasContext.beginPath();
158
+ const sliceWidth = this.canvas.width / this.dataArray.length;
159
+ let x = 0;
160
+
161
+ for (let i = 0; i < this.dataArray.length; i++) {
162
+ const v = this.dataArray[i] / 128.0;
163
+ const y = v * this.canvas.height / 2;
164
+
165
+ if (Math.abs((this.dataArray[i] - 128) / 128.0) > this.clipThreshold) {
166
+ this.canvasContext.fillStyle = '#ff0000';
167
+ this.canvasContext.fillRect(x - 1, y - 2, 3, 4);
168
+ }
169
+
170
+ if (i === 0) {
171
+ this.canvasContext.moveTo(x, y);
172
+ } else {
173
+ this.canvasContext.lineTo(x, y);
174
+ }
175
+ x += sliceWidth;
176
+ }
177
+
178
+ this.canvasContext.stroke();
179
+ this.drawThresholdLines();
180
+ }
181
+
182
+ drawThresholdLines() {
183
+ const clipLine = this.canvas.height / 2 * (1 - this.clipThreshold);
184
+ const clipLineBottom = this.canvas.height / 2 * (1 + this.clipThreshold);
185
+
186
+ this.canvasContext.strokeStyle = '#444';
187
+ this.canvasContext.lineWidth = 1;
188
+ this.canvasContext.setLineDash([5, 5]);
189
+
190
+ this.canvasContext.beginPath();
191
+ this.canvasContext.moveTo(0, clipLine);
192
+ this.canvasContext.lineTo(this.canvas.width, clipLine);
193
+ this.canvasContext.moveTo(0, clipLineBottom);
194
+ this.canvasContext.lineTo(this.canvas.width, clipLineBottom);
195
+ this.canvasContext.stroke();
196
+ this.canvasContext.setLineDash([]);
197
+ }
198
+
199
+ drawQualityIndicators() {
200
+ if (!this.alertContext || !this.alertCanvas) return;
201
+
202
+ this.alertContext.fillStyle = '#111';
203
+ this.alertContext.fillRect(0, 0, this.alertCanvas.width, this.alertCanvas.height);
204
+
205
+ const centerY = this.alertCanvas.height / 2;
206
+ this.alertContext.font = '16px monospace';
207
+ this.alertContext.textAlign = 'left';
208
+
209
+ if (this.clipCounter > 0) {
210
+ this.alertContext.fillStyle = '#ff0000';
211
+ this.alertContext.fillText('⚠️ CLIPPING DETECTED', 20, centerY - 10);
212
+ this.alertContext.fillText(`Clip Level: ${Math.round(this.clipCounter / 60 * 100)}%`, 20, centerY + 10);
213
+ } else if (this.popCounter > 0) {
214
+ this.alertContext.fillStyle = '#ff8800';
215
+ this.alertContext.fillText('⚠️ AUDIO POPS DETECTED', 20, centerY - 10);
216
+ this.alertContext.fillText(`Pop Level: ${Math.round(this.popCounter / 60 * 100)}%`, 20, centerY + 10);
217
+ } else {
218
+ this.alertContext.fillStyle = '#00ff41';
219
+ this.alertContext.fillText('✅ AUDIO QUALITY: GOOD', 20, centerY - 10);
220
+ this.alertContext.fillText('No clipping or pops detected', 20, centerY + 10);
221
+ }
222
+
223
+ // Peak level meter
224
+ const maxSample = Math.max(...Array.from(this.dataArray).map(x => Math.abs((x - 128) / 128.0)));
225
+ const meterWidth = 200;
226
+ const meterHeight = 20;
227
+ const meterX = this.alertCanvas.width - meterWidth - 20;
228
+ const meterY = centerY - meterHeight / 2;
229
+
230
+ this.alertContext.fillStyle = '#333';
231
+ this.alertContext.fillRect(meterX, meterY, meterWidth, meterHeight);
232
+
233
+ const currentWidth = maxSample * meterWidth;
234
+ this.alertContext.fillStyle = '#555';
235
+ this.alertContext.fillRect(meterX, meterY, currentWidth, meterHeight);
236
+
237
+ const peakWidth = this.rollingPeak * meterWidth;
238
+ this.alertContext.fillStyle = this.rollingPeak > this.clipThreshold ? '#ff0000' :
239
+ this.rollingPeak > 0.7 ? '#ff8800' : '#00ff41';
240
+ this.alertContext.fillRect(meterX, meterY, peakWidth, meterHeight);
241
+
242
+ this.alertContext.strokeStyle = '#666';
243
+ this.alertContext.lineWidth = 1;
244
+ this.alertContext.strokeRect(meterX, meterY, meterWidth, meterHeight);
245
+
246
+ this.alertContext.fillStyle = '#ccc';
247
+ this.alertContext.font = '12px monospace';
248
+ this.alertContext.textAlign = 'center';
249
+ this.alertContext.fillText('PEAK LEVEL (1s)', meterX + meterWidth / 2, meterY - 5);
250
+ this.alertContext.fillText(`${Math.round(this.rollingPeak * 100)}%`, meterX + meterWidth / 2, meterY + meterHeight + 15);
251
+
252
+ const clipMarkerX = meterX + this.clipThreshold * meterWidth;
253
+ this.alertContext.strokeStyle = '#ff0000';
254
+ this.alertContext.lineWidth = 2;
255
+ this.alertContext.beginPath();
256
+ this.alertContext.moveTo(clipMarkerX, meterY - 2);
257
+ this.alertContext.lineTo(clipMarkerX, meterY + meterHeight + 2);
258
+ this.alertContext.stroke();
259
+ }
260
+
261
+ setDetectionSensitivity(clipThreshold = 0.95, popThreshold = 0.1) {
262
+ this.clipThreshold = clipThreshold;
263
+ this.popThreshold = popThreshold;
264
+ }
265
+ }
@@ -0,0 +1,24 @@
1
+ // Live Audio Library
2
+ // Web Audio API-based collection for game sounds and background music
3
+
4
+ import { Controller } from './Audio/Controller.js';
5
+
6
+ // Export the essential classes for users
7
+ export { Controller } from './Audio/Controller.js';
8
+ export { Sound } from './Audio/Sound.js';
9
+ export { Visualizer } from './Audio/Visualizer.js';
10
+ export { Output } from './Audio/Output.js';
11
+
12
+ // Export all sound library classes under Library namespace
13
+ export * as Library from './Audio/Library.js';
14
+
15
+ // Main Audio namespace with Live.js pattern
16
+ export const Audio = {
17
+ start(options = {}) {
18
+ const window = options.window || globalThis;
19
+ return new Controller(window, options);
20
+ },
21
+
22
+ // Direct access to Controller for advanced usage
23
+ Controller
24
+ };
@@ -0,0 +1,35 @@
1
+ {
2
+ "name": "@socketry/live-audio",
3
+ "type": "module",
4
+ "version": "0.4.0",
5
+ "description": "Web Audio API-based game audio synthesis and background music library for Ruby Live applications.",
6
+ "main": "Live/Audio.js",
7
+ "files": [
8
+ "Live/"
9
+ ],
10
+ "repository": {
11
+ "type": "git",
12
+ "url": "git+https://github.com/socketry/live-audio-js.git"
13
+ },
14
+ "scripts": {
15
+ "test": "node --test"
16
+ },
17
+ "devDependencies": {
18
+ "jsdom": "^24.1.3"
19
+ },
20
+ "keywords": [
21
+ "audio",
22
+ "webaudio",
23
+ "game",
24
+ "sound",
25
+ "synthesis",
26
+ "music",
27
+ "live"
28
+ ],
29
+ "author": "Samuel Williams <samuel.williams@oriontransfer.co.nz> (http://www.codeotaku.com/)",
30
+ "license": "MIT",
31
+ "bugs": {
32
+ "url": "https://github.com/socketry/live-audio-js/issues"
33
+ },
34
+ "homepage": "https://github.com/socketry/live-audio-js#readme"
35
+ }
@@ -0,0 +1,250 @@
1
+ # Live Audio.js
2
+
3
+ A Web Audio API-based game audio synthesis and background music library for Ruby Live applications.
4
+
5
+ [![Development Status](https://github.com/socketry/live-audio-js/workflows/Test/badge.svg)](https://github.com/socketry/live-audio-js/actions?workflow=Test)
6
+
7
+ ## Features
8
+
9
+ - **Synthesized Sound Effects**: Classic game sounds including jump, coin, power-up, death, explosion, laser, and animal sounds.
10
+ - **Background Music**: MP3 playback with loop points and volume control.
11
+ - **Audio Visualization**: Real-time waveform display with quality monitoring.
12
+ - **Anti-Clipping Protection**: Built-in gain management to prevent audio distortion.
13
+ - **Modular Architecture**: Clean separation between sound synthesis, output routing, and visualization.
14
+ - **Live.js Pattern Compliance**: Follows established patterns from the Live.js ecosystem.
15
+
16
+ ## Usage
17
+
18
+ ### Installation
19
+
20
+ ```bash
21
+ npm install @socketry/live-audio
22
+ ```
23
+
24
+ ### Basic Setup
25
+
26
+ ```javascript
27
+ import { Audio } from '@socketry/live-audio';
28
+ import { MeowSound, ExplosionSound, BackgroundMusicSound } from '@socketry/live-audio/Live/Audio/Library.js';
29
+
30
+ // Audio.start() pattern - follows Live.js conventions
31
+ window.liveAudio = Audio.start();
32
+
33
+ // Add sounds using the controller
34
+ const meow = new MeowSound();
35
+ const explosion = new ExplosionSound();
36
+ const music = new BackgroundMusicSound('/assets/music.mp3', 10.0, 45.0);
37
+
38
+ window.liveAudio.addSound('meow', meow);
39
+ window.liveAudio.addSound('explosion', explosion);
40
+ window.liveAudio.addSound('music', music);
41
+ window.liveAudio.setVolume(0.8);
42
+
43
+ // Play sounds anywhere in your app
44
+ window.liveAudio.playSound('meow');
45
+ ```
46
+
47
+ ### Alternative: Direct Controller Usage
48
+
49
+ ```javascript
50
+ import { Audio } from '@socketry/live-audio';
51
+ import { CoinSound, LaserSound } from '@socketry/live-audio/Live/Audio/Library.js';
52
+
53
+ const controller = Audio.start();
54
+
55
+ // Add and play sounds
56
+ const coin = new CoinSound();
57
+ const laser = new LaserSound();
58
+ controller.addSound('coin', coin);
59
+ controller.addSound('laser', laser);
60
+ controller.playSound('coin');
61
+ controller.setVolume(0.8);
62
+ ```
63
+
64
+ ## API Reference
65
+
66
+ ### Audio (Main Namespace)
67
+
68
+ The primary entry point following Live.js conventions.
69
+
70
+ #### Static Methods
71
+ - `Audio.start(options)` - Create a new controller instance (recommended)
72
+ - `options.window` - The window object to use (defaults to globalThis)
73
+ - Returns the controller instance
74
+ - `Audio.Controller` - Direct access to Controller class for advanced usage
75
+
76
+ ### Controller
77
+
78
+ The main audio controller class that manages all sound playback and audio context.
79
+
80
+ #### Instance Methods
81
+ - `addSound(name, soundInstance)` - Add a sound instance to the controller
82
+ - `playSound(name)` - Play a sound by name
83
+ - `stopSound(name)` - Stop a sound by name
84
+ - `stopAllSounds()` - Stop all sounds
85
+ - `listSounds()` - Get array of available sound names
86
+ - `removeSound(name)` - Remove a sound from the controller
87
+ - `setVolume(volume)` - Set master volume (0.0 to 1.0)
88
+ - `getSound(name)` - Get direct access to a sound instance
89
+
90
+ ### Sound
91
+
92
+ Base class for creating custom sound effects. Extend this class to create your own synthesized sounds.
93
+
94
+ ```javascript
95
+ import { Sound } from '@socketry/live-audio';
96
+
97
+ class CustomSound extends Sound {
98
+ start(output) {
99
+ const audioContext = output.audioContext;
100
+ const oscillator = audioContext.createOscillator();
101
+ const gainNode = audioContext.createGain();
102
+
103
+ oscillator.type = 'sine';
104
+ oscillator.frequency.value = 440;
105
+
106
+ this.createEnvelope(audioContext, gainNode, 0.01, 0.1, 0.5, 0.2, 0.5);
107
+
108
+ oscillator.connect(gainNode);
109
+ gainNode.connect(output.input);
110
+
111
+ oscillator.start();
112
+ oscillator.stop(audioContext.currentTime + 0.5);
113
+ }
114
+ }
115
+ ```
116
+
117
+ ### Visualizer
118
+
119
+ Audio analysis and visualization component that provides real-time waveform display and audio quality monitoring.
120
+
121
+ - Clipping detection and visualization
122
+ - Audio pop/click detection
123
+ - Rolling peak level monitoring
124
+ - Real-time waveform display
125
+
126
+ ## Built-in Sound Library
127
+
128
+ The library includes a comprehensive collection of pre-built sound classes in `Library.js`:
129
+
130
+ ### Game Sound Effects
131
+ - `JumpSound` - Classic platform game jump sound
132
+ - `CoinSound` - Collectible pickup sound
133
+ - `PowerUpSound` - Power-up acquisition sound
134
+ - `DeathSound` - Game over sound
135
+ - `ExplosionSound` - Explosive sound with multiple rumble layers
136
+ - `LaserSound` - Sci-fi laser sound
137
+ - `BeepSound` - Simple notification beep
138
+ - `BlipSound` - Short UI interaction sound
139
+
140
+ ### Animal Sounds
141
+ - `MeowSound` - Cat meow with frequency modulation
142
+ - `BarkSound` - Dog bark with formant filtering
143
+ - `RoarSound` - Lion roar with noise texture
144
+ - `ChirpSound` - Bird chirp sound
145
+ - `HowlSound` - Wolf howl with harmonic sweep
146
+ - `DuckSound` - Duck quack with FM synthesis
147
+ - `AlienSound` - Alien sound with ring modulation
148
+
149
+ ### Background Music
150
+ - `BackgroundMusicSound(url, loopStart, loopEnd)` - MP3 background music with required URL and loop points
151
+
152
+ ### Usage Example
153
+
154
+ ```javascript
155
+ import { Audio } from '@socketry/live-audio';
156
+ import { MeowSound, ExplosionSound, BackgroundMusicSound } from '@socketry/live-audio/Live/Audio/Library.js';
157
+
158
+ const controller = Audio.start();
159
+
160
+ // Add sounds from the library
161
+ const meow = new MeowSound();
162
+ const explosion = new ExplosionSound();
163
+ const music = new BackgroundMusicSound('/assets/background.mp3', 10.5, 45.2);
164
+
165
+ controller.addSound('meow', meow);
166
+ controller.addSound('explosion', explosion);
167
+ controller.addSound('music', music);
168
+
169
+ // Play them
170
+ controller.playSound('meow');
171
+ controller.playSound('explosion');
172
+ controller.playSound('music');
173
+ ```
174
+
175
+ ## Project Structure
176
+
177
+ The library follows Live.js ecosystem patterns with a clean modular architecture:
178
+
179
+ ```
180
+ @socketry/live-audio/
181
+ ├── Live/
182
+ │ ├── Audio.js # Main module - exports Controller, Sound, Visualizer
183
+ │ └── Audio/
184
+ │ ├── Controller.js # Audio controller with window-keyed shared instances
185
+ │ ├── Sound.js # Base Sound class for custom sounds
186
+ │ ├── Output.js # Audio routing and master volume control
187
+ │ ├── Visualizer.js # Real-time waveform visualization
188
+ │ └── Library.js # Collection of pre-built game sounds
189
+ └── test/
190
+ └── LiveAudio.js # Comprehensive test suite
191
+ ```
192
+
193
+ ### Import Patterns
194
+
195
+ ```javascript
196
+ // Main Audio namespace (recommended)
197
+ import { Audio } from '@socketry/live-audio';
198
+
199
+ // Essential classes for advanced usage
200
+ import { Controller, Sound } from '@socketry/live-audio';
201
+
202
+ // Full access including visualization
203
+ import { Controller, Sound, Visualizer, Output } from '@socketry/live-audio';
204
+
205
+ // Pre-built sound library - individual imports (recommended)
206
+ import { MeowSound, ExplosionSound, BackgroundMusicSound } from '@socketry/live-audio/Live/Audio/Library.js';
207
+
208
+ // Pre-built sound library - namespace import
209
+ import * as Library from '@socketry/live-audio/Live/Audio/Library.js';
210
+ ```
211
+
212
+ ## Audio Context Management
213
+
214
+ The library automatically manages a shared AudioContext to avoid browser limitations and ensure optimal performance:
215
+
216
+ - Automatic context creation and resumption
217
+ - Safari compatibility with proper latency handling
218
+ - Shared instance pattern to prevent multiple contexts
219
+ - Graceful degradation when audio is unavailable
220
+
221
+ ## Browser Compatibility
222
+
223
+ - Modern browsers with Web Audio API support
224
+ - Handles browser autoplay policies
225
+ - Safari-specific optimizations for reduced latency
226
+ - Fallback behavior when audio context is unavailable
227
+
228
+ ## Contributing
229
+
230
+ We welcome contributions to this project.
231
+
232
+ 1. Fork it.
233
+ 2. Create your feature branch (`git checkout -b my-new-feature`).
234
+ 3. Commit your changes (`git commit -am 'Add some feature'`).
235
+ 4. Push to the branch (`git push origin my-new-feature`).
236
+ 5. Create new Pull Request.
237
+
238
+ ### Developer Certificate of Origin
239
+
240
+ In order to protect users of this project, we require all contributors to comply with the [Developer Certificate of Origin](https://developercertificate.org/). This ensures that all contributions are properly licensed and attributed.
241
+
242
+ ### Community Guidelines
243
+
244
+ This project is best served by a collaborative and respectful environment. Treat each other professionally, respect differing viewpoints, and engage constructively. Harassment, discrimination, or harmful behavior is not tolerated. Communicate clearly, listen actively, and support one another. If any issues arise, please inform the project maintainers.
245
+
246
+ ## See Also
247
+
248
+ - [lively](https://github.com/socketry/lively) — Ruby framework for building interactive web applications.
249
+ - [live](https://github.com/socketry/live) — Provides client-server communication using websockets.
250
+ - [live-js](https://github.com/socketry/live-js) — JavaScript client library for Live framework.
@@ -0,0 +1,4 @@
1
+ import { Live } from 'live';
2
+
3
+ // Start Live.js with default configuration:
4
+ const live = Live.start();