pixospritz-core 0.10.1 → 1.0.1
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.
- package/README.md +36 -286
- package/dist/bundle.js +13 -3
- package/dist/bundle.js.map +1 -1
- package/dist/style.css +1 -0
- package/package.json +43 -44
- package/src/components/WebGLView.jsx +318 -0
- package/src/css/pixos.css +372 -0
- package/src/engine/actions/animate.js +41 -0
- package/src/engine/actions/changezone.js +135 -0
- package/src/engine/actions/chat.js +109 -0
- package/src/engine/actions/dialogue.js +90 -0
- package/src/engine/actions/face.js +22 -0
- package/src/engine/actions/greeting.js +28 -0
- package/src/engine/actions/interact.js +86 -0
- package/src/engine/actions/move.js +67 -0
- package/src/engine/actions/patrol.js +109 -0
- package/src/engine/actions/prompt.js +185 -0
- package/src/engine/actions/script.js +42 -0
- package/src/engine/core/audio/AudioSystem.js +543 -0
- package/src/engine/core/cutscene/PxcPlayer.js +956 -0
- package/src/engine/core/cutscene/manager.js +243 -0
- package/src/engine/core/database/index.js +75 -0
- package/src/engine/core/debug/index.js +371 -0
- package/src/engine/core/hud/index.js +765 -0
- package/src/engine/core/index.js +540 -0
- package/src/engine/core/input/gamepad/Controller.js +71 -0
- package/src/engine/core/input/gamepad/ControllerButtons.js +231 -0
- package/src/engine/core/input/gamepad/ControllerStick.js +173 -0
- package/src/engine/core/input/gamepad/index.js +592 -0
- package/src/engine/core/input/keyboard.js +196 -0
- package/src/engine/core/input/manager.js +485 -0
- package/src/engine/core/input/mouse.js +203 -0
- package/src/engine/core/input/touch.js +175 -0
- package/src/engine/core/mode/manager.js +199 -0
- package/src/engine/core/net/manager.js +535 -0
- package/src/engine/core/queue/action.js +83 -0
- package/src/engine/core/queue/event.js +82 -0
- package/src/engine/core/queue/index.js +44 -0
- package/src/engine/core/queue/loadable.js +33 -0
- package/src/engine/core/render/CameraEffects.js +494 -0
- package/src/engine/core/render/FrustumCuller.js +417 -0
- package/src/engine/core/render/LODManager.js +285 -0
- package/src/engine/core/render/ParticleManager.js +529 -0
- package/src/engine/core/render/TextureAtlas.js +465 -0
- package/src/engine/core/render/camera.js +338 -0
- package/src/engine/core/render/light.js +197 -0
- package/src/engine/core/render/manager.js +1079 -0
- package/src/engine/core/render/shaders.js +110 -0
- package/src/engine/core/render/skybox.js +342 -0
- package/src/engine/core/resource/manager.js +133 -0
- package/src/engine/core/resource/object.js +611 -0
- package/src/engine/core/resource/texture.js +103 -0
- package/src/engine/core/resource/tileset.js +177 -0
- package/src/engine/core/scene/avatar.js +215 -0
- package/src/engine/core/scene/speech.js +138 -0
- package/src/engine/core/scene/sprite.js +702 -0
- package/src/engine/core/scene/spritz.js +189 -0
- package/src/engine/core/scene/world.js +681 -0
- package/src/engine/core/scene/zone.js +1167 -0
- package/src/engine/core/store/index.js +110 -0
- package/src/engine/dynamic/animatedSprite.js +64 -0
- package/src/engine/dynamic/animatedTile.js +98 -0
- package/src/engine/dynamic/avatar.js +110 -0
- package/src/engine/dynamic/map.js +174 -0
- package/src/engine/dynamic/sprite.js +255 -0
- package/src/engine/dynamic/spritz.js +119 -0
- package/src/engine/events/EventSystem.js +609 -0
- package/src/engine/events/camera.js +142 -0
- package/src/engine/events/chat.js +75 -0
- package/src/engine/events/menu.js +186 -0
- package/src/engine/scripting/CallbackManager.js +514 -0
- package/src/engine/scripting/PixoScriptInterpreter.js +81 -0
- package/src/engine/scripting/PixoScriptLibrary.js +704 -0
- package/src/engine/shaders/effects/index.js +450 -0
- package/src/engine/shaders/fs.js +222 -0
- package/src/engine/shaders/particles/fs.js +41 -0
- package/src/engine/shaders/particles/vs.js +61 -0
- package/src/engine/shaders/picker/fs.js +34 -0
- package/src/engine/shaders/picker/init.js +62 -0
- package/src/engine/shaders/picker/vs.js +42 -0
- package/src/engine/shaders/pxsl/README.md +250 -0
- package/src/engine/shaders/pxsl/index.js +25 -0
- package/src/engine/shaders/pxsl/library.js +608 -0
- package/src/engine/shaders/pxsl/manager.js +338 -0
- package/src/engine/shaders/pxsl/specification.js +363 -0
- package/src/engine/shaders/pxsl/transpiler.js +753 -0
- package/src/engine/shaders/skybox/cosmic/fs.js +147 -0
- package/src/engine/shaders/skybox/cosmic/vs.js +23 -0
- package/src/engine/shaders/skybox/matrix/fs.js +127 -0
- package/src/engine/shaders/skybox/matrix/vs.js +23 -0
- package/src/engine/shaders/skybox/morning/fs.js +109 -0
- package/src/engine/shaders/skybox/morning/vs.js +23 -0
- package/src/engine/shaders/skybox/neon/fs.js +119 -0
- package/src/engine/shaders/skybox/neon/vs.js +23 -0
- package/src/engine/shaders/skybox/sky/fs.js +114 -0
- package/src/engine/shaders/skybox/sky/vs.js +23 -0
- package/src/engine/shaders/skybox/sunset/fs.js +101 -0
- package/src/engine/shaders/skybox/sunset/vs.js +23 -0
- package/src/engine/shaders/transition/blur/fs.js +42 -0
- package/src/engine/shaders/transition/blur/vs.js +26 -0
- package/src/engine/shaders/transition/cross/fs.js +36 -0
- package/src/engine/shaders/transition/cross/vs.js +26 -0
- package/src/engine/shaders/transition/crossBlur/fs.js +41 -0
- package/src/engine/shaders/transition/crossBlur/vs.js +25 -0
- package/src/engine/shaders/transition/dissolve/fs.js +78 -0
- package/src/engine/shaders/transition/dissolve/vs.js +24 -0
- package/src/engine/shaders/transition/fade/fs.js +31 -0
- package/src/engine/shaders/transition/fade/vs.js +27 -0
- package/src/engine/shaders/transition/iris/fs.js +52 -0
- package/src/engine/shaders/transition/iris/vs.js +24 -0
- package/src/engine/shaders/transition/pixelate/fs.js +44 -0
- package/src/engine/shaders/transition/pixelate/vs.js +24 -0
- package/src/engine/shaders/transition/slide/fs.js +53 -0
- package/src/engine/shaders/transition/slide/vs.js +24 -0
- package/src/engine/shaders/transition/swirl/fs.js +39 -0
- package/src/engine/shaders/transition/swirl/vs.js +26 -0
- package/src/engine/shaders/transition/wipe/fs.js +50 -0
- package/src/engine/shaders/transition/wipe/vs.js +24 -0
- package/src/engine/shaders/vs.js +60 -0
- package/src/engine/utils/CameraController.js +506 -0
- package/src/engine/utils/ObjHelper.js +551 -0
- package/src/engine/utils/debug-logger.js +110 -0
- package/src/engine/utils/enums.js +305 -0
- package/src/engine/utils/generator.js +156 -0
- package/src/engine/utils/index.js +21 -0
- package/src/engine/utils/loaders/ActionLoader.js +77 -0
- package/src/engine/utils/loaders/AudioLoader.js +157 -0
- package/src/engine/utils/loaders/EventLoader.js +66 -0
- package/src/engine/utils/loaders/ObjectLoader.js +67 -0
- package/src/engine/utils/loaders/SpriteLoader.js +77 -0
- package/src/engine/utils/loaders/TilesetLoader.js +103 -0
- package/src/engine/utils/loaders/index.js +21 -0
- package/src/engine/utils/math/matrix4.js +367 -0
- package/src/engine/utils/math/vector.js +458 -0
- package/src/engine/utils/obj/_old_js/index.js +46 -0
- package/src/engine/utils/obj/_old_js/layout.js +308 -0
- package/src/engine/utils/obj/_old_js/material.js +711 -0
- package/src/engine/utils/obj/_old_js/mesh.js +761 -0
- package/src/engine/utils/obj/_old_js/utils.js +647 -0
- package/src/engine/utils/obj/index.js +24 -0
- package/src/engine/utils/obj/js/index.js +277 -0
- package/src/engine/utils/obj/js/loader.js +232 -0
- package/src/engine/utils/obj/layout.js +246 -0
- package/src/engine/utils/obj/material.js +665 -0
- package/src/engine/utils/obj/mesh.js +657 -0
- package/src/engine/utils/obj/ts/index.ts +72 -0
- package/src/engine/utils/obj/ts/layout.ts +265 -0
- package/src/engine/utils/obj/ts/material.ts +760 -0
- package/src/engine/utils/obj/ts/mesh.ts +785 -0
- package/src/engine/utils/obj/ts/utils.ts +501 -0
- package/src/engine/utils/obj/utils.js +428 -0
- package/src/engine/utils/resources.js +18 -0
- package/src/index.jsx +55 -0
- package/src/spritz/player.js +18 -0
- package/src/spritz/readme.md +18 -0
- package/LICENSE +0 -437
- package/dist/bundle.js.LICENSE.txt +0 -31
|
@@ -0,0 +1,543 @@
|
|
|
1
|
+
/* *\
|
|
2
|
+
** ----------------------------------------------- **
|
|
3
|
+
** Calliope - Pixos Game Engine **
|
|
4
|
+
** ----------------------------------------------- **
|
|
5
|
+
** Copyright (c) 2020-2025 - Kyle Derby MacInnis **
|
|
6
|
+
** **
|
|
7
|
+
** Any unauthorized distribution or transfer **
|
|
8
|
+
** of this work is strictly prohibited. **
|
|
9
|
+
** **
|
|
10
|
+
** All Rights Reserved. **
|
|
11
|
+
** ----------------------------------------------- **
|
|
12
|
+
\* */
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* AudioSystem - Advanced audio management with mixing, effects, and spatial audio.
|
|
16
|
+
* Provides professional-quality audio experience for games.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* @typedef {object} AudioChannel
|
|
21
|
+
* @property {string} name - Channel name
|
|
22
|
+
* @property {GainNode} gainNode - Volume control node
|
|
23
|
+
* @property {number} volume - Current volume (0-1)
|
|
24
|
+
* @property {boolean} muted - Whether channel is muted
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* @typedef {object} SpatialAudioConfig
|
|
29
|
+
* @property {number} maxDistance - Maximum audible distance
|
|
30
|
+
* @property {number} refDistance - Reference distance for attenuation
|
|
31
|
+
* @property {number} rolloffFactor - How quickly sound attenuates
|
|
32
|
+
* @property {string} distanceModel - 'linear', 'inverse', or 'exponential'
|
|
33
|
+
*/
|
|
34
|
+
|
|
35
|
+
export default class AudioSystem {
|
|
36
|
+
/**
|
|
37
|
+
* Creates an instance of AudioSystem.
|
|
38
|
+
* @param {import('../index.js').default} engine - The game engine instance.
|
|
39
|
+
*/
|
|
40
|
+
constructor(engine) {
|
|
41
|
+
/** @type {import('../index.js').default} */
|
|
42
|
+
this.engine = engine;
|
|
43
|
+
|
|
44
|
+
/** @type {AudioContext|null} */
|
|
45
|
+
this.context = null;
|
|
46
|
+
|
|
47
|
+
/** @type {GainNode|null} */
|
|
48
|
+
this.masterGain = null;
|
|
49
|
+
|
|
50
|
+
/** @type {Map<string, AudioChannel>} */
|
|
51
|
+
this.channels = new Map();
|
|
52
|
+
|
|
53
|
+
/** @type {Map<string, AudioBuffer>} */
|
|
54
|
+
this.bufferCache = new Map();
|
|
55
|
+
|
|
56
|
+
/** @type {Map<string, AudioBufferSourceNode>} */
|
|
57
|
+
this.activeSources = new Map();
|
|
58
|
+
|
|
59
|
+
/** @type {DynamicsCompressorNode|null} */
|
|
60
|
+
this.compressor = null;
|
|
61
|
+
|
|
62
|
+
/** @type {ConvolverNode|null} */
|
|
63
|
+
this.reverb = null;
|
|
64
|
+
|
|
65
|
+
/** @type {GainNode|null} */
|
|
66
|
+
this.reverbGain = null;
|
|
67
|
+
|
|
68
|
+
/** @type {boolean} */
|
|
69
|
+
this.initialized = false;
|
|
70
|
+
|
|
71
|
+
/** @type {SpatialAudioConfig} */
|
|
72
|
+
this.spatialConfig = {
|
|
73
|
+
maxDistance: 100,
|
|
74
|
+
refDistance: 1,
|
|
75
|
+
rolloffFactor: 1,
|
|
76
|
+
distanceModel: 'inverse',
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
/** @type {{ x: number, y: number, z: number }} */
|
|
80
|
+
this.listenerPosition = { x: 0, y: 0, z: 0 };
|
|
81
|
+
|
|
82
|
+
// Crossfade state
|
|
83
|
+
/** @type {Map<string, { source: AudioBufferSourceNode, gain: GainNode }>} */
|
|
84
|
+
this.musicTracks = new Map();
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Initializes the audio system. Must be called after user interaction.
|
|
89
|
+
* @returns {Promise<void>}
|
|
90
|
+
*/
|
|
91
|
+
async init() {
|
|
92
|
+
if (this.initialized) return;
|
|
93
|
+
|
|
94
|
+
try {
|
|
95
|
+
this.context = new (window.AudioContext || window.webkitAudioContext)();
|
|
96
|
+
|
|
97
|
+
// Create master gain
|
|
98
|
+
this.masterGain = this.context.createGain();
|
|
99
|
+
this.masterGain.gain.value = 1.0;
|
|
100
|
+
|
|
101
|
+
// Create compressor to prevent clipping
|
|
102
|
+
this.compressor = this.context.createDynamicsCompressor();
|
|
103
|
+
this.compressor.threshold.value = -24;
|
|
104
|
+
this.compressor.knee.value = 30;
|
|
105
|
+
this.compressor.ratio.value = 12;
|
|
106
|
+
this.compressor.attack.value = 0.003;
|
|
107
|
+
this.compressor.release.value = 0.25;
|
|
108
|
+
|
|
109
|
+
// Create reverb (dry by default)
|
|
110
|
+
this.reverbGain = this.context.createGain();
|
|
111
|
+
this.reverbGain.gain.value = 0; // Reverb off by default
|
|
112
|
+
|
|
113
|
+
// Connect: master -> compressor -> destination
|
|
114
|
+
this.masterGain.connect(this.compressor);
|
|
115
|
+
this.compressor.connect(this.context.destination);
|
|
116
|
+
|
|
117
|
+
// Create default channels
|
|
118
|
+
this.createChannel('master', 1.0);
|
|
119
|
+
this.createChannel('music', 0.7);
|
|
120
|
+
this.createChannel('sfx', 1.0);
|
|
121
|
+
this.createChannel('dialogue', 1.0);
|
|
122
|
+
this.createChannel('ambient', 0.5);
|
|
123
|
+
|
|
124
|
+
this.initialized = true;
|
|
125
|
+
} catch (e) {
|
|
126
|
+
console.error('Failed to initialize AudioSystem:', e);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Creates an audio channel for mixing.
|
|
132
|
+
* @param {string} name - Channel name
|
|
133
|
+
* @param {number} [volume=1.0] - Initial volume
|
|
134
|
+
* @returns {AudioChannel}
|
|
135
|
+
*/
|
|
136
|
+
createChannel(name, volume = 1.0) {
|
|
137
|
+
if (!this.context) return null;
|
|
138
|
+
|
|
139
|
+
const gainNode = this.context.createGain();
|
|
140
|
+
gainNode.gain.value = volume;
|
|
141
|
+
gainNode.connect(this.masterGain);
|
|
142
|
+
|
|
143
|
+
const channel = {
|
|
144
|
+
name,
|
|
145
|
+
gainNode,
|
|
146
|
+
volume,
|
|
147
|
+
muted: false,
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
this.channels.set(name, channel);
|
|
151
|
+
return channel;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Sets volume for a channel.
|
|
156
|
+
* @param {string} channelName - Channel name
|
|
157
|
+
* @param {number} volume - Volume (0-1)
|
|
158
|
+
* @param {number} [fadeTime=0] - Fade time in seconds
|
|
159
|
+
*/
|
|
160
|
+
setChannelVolume(channelName, volume, fadeTime = 0) {
|
|
161
|
+
const channel = this.channels.get(channelName);
|
|
162
|
+
if (!channel) return;
|
|
163
|
+
|
|
164
|
+
const clampedVolume = Math.max(0, Math.min(1, volume));
|
|
165
|
+
channel.volume = clampedVolume;
|
|
166
|
+
|
|
167
|
+
if (fadeTime > 0 && this.context) {
|
|
168
|
+
channel.gainNode.gain.linearRampToValueAtTime(
|
|
169
|
+
channel.muted ? 0 : clampedVolume,
|
|
170
|
+
this.context.currentTime + fadeTime
|
|
171
|
+
);
|
|
172
|
+
} else {
|
|
173
|
+
channel.gainNode.gain.value = channel.muted ? 0 : clampedVolume;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Mutes or unmutes a channel.
|
|
179
|
+
* @param {string} channelName - Channel name
|
|
180
|
+
* @param {boolean} muted - Whether to mute
|
|
181
|
+
*/
|
|
182
|
+
setChannelMuted(channelName, muted) {
|
|
183
|
+
const channel = this.channels.get(channelName);
|
|
184
|
+
if (!channel) return;
|
|
185
|
+
|
|
186
|
+
channel.muted = muted;
|
|
187
|
+
channel.gainNode.gain.value = muted ? 0 : channel.volume;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Sets master volume.
|
|
192
|
+
* @param {number} volume - Volume (0-1)
|
|
193
|
+
*/
|
|
194
|
+
setMasterVolume(volume) {
|
|
195
|
+
if (!this.masterGain) return;
|
|
196
|
+
this.masterGain.gain.value = Math.max(0, Math.min(1, volume));
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Loads an audio buffer from URL.
|
|
201
|
+
* @param {string} url - Audio file URL
|
|
202
|
+
* @returns {Promise<AudioBuffer>}
|
|
203
|
+
*/
|
|
204
|
+
async loadBuffer(url) {
|
|
205
|
+
if (this.bufferCache.has(url)) {
|
|
206
|
+
return this.bufferCache.get(url);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
if (!this.context) await this.init();
|
|
210
|
+
|
|
211
|
+
const response = await fetch(url);
|
|
212
|
+
const arrayBuffer = await response.arrayBuffer();
|
|
213
|
+
const audioBuffer = await this.context.decodeAudioData(arrayBuffer);
|
|
214
|
+
|
|
215
|
+
this.bufferCache.set(url, audioBuffer);
|
|
216
|
+
return audioBuffer;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Plays a sound effect.
|
|
221
|
+
* @param {string|AudioBuffer} sound - URL or AudioBuffer
|
|
222
|
+
* @param {object} [options] - Playback options
|
|
223
|
+
* @param {string} [options.channel='sfx'] - Channel to play on
|
|
224
|
+
* @param {number} [options.volume=1] - Volume multiplier
|
|
225
|
+
* @param {number} [options.pitch=1] - Playback rate
|
|
226
|
+
* @param {boolean} [options.loop=false] - Whether to loop
|
|
227
|
+
* @param {number[]} [options.position] - 3D position [x, y, z] for spatial audio
|
|
228
|
+
* @returns {Promise<string>} Source ID for stopping
|
|
229
|
+
*/
|
|
230
|
+
async play(sound, options = {}) {
|
|
231
|
+
if (!this.context) await this.init();
|
|
232
|
+
|
|
233
|
+
const {
|
|
234
|
+
channel = 'sfx',
|
|
235
|
+
volume = 1,
|
|
236
|
+
pitch = 1,
|
|
237
|
+
loop = false,
|
|
238
|
+
position = null,
|
|
239
|
+
} = options;
|
|
240
|
+
|
|
241
|
+
const buffer = typeof sound === 'string' ? await this.loadBuffer(sound) : sound;
|
|
242
|
+
const channelObj = this.channels.get(channel);
|
|
243
|
+
if (!channelObj) return null;
|
|
244
|
+
|
|
245
|
+
const source = this.context.createBufferSource();
|
|
246
|
+
source.buffer = buffer;
|
|
247
|
+
source.loop = loop;
|
|
248
|
+
source.playbackRate.value = pitch;
|
|
249
|
+
|
|
250
|
+
// Create gain for this sound
|
|
251
|
+
const gainNode = this.context.createGain();
|
|
252
|
+
gainNode.gain.value = volume;
|
|
253
|
+
|
|
254
|
+
// Connect with optional spatial audio
|
|
255
|
+
if (position) {
|
|
256
|
+
const panner = this.context.createPanner();
|
|
257
|
+
this.configurePanner(panner, position);
|
|
258
|
+
source.connect(gainNode);
|
|
259
|
+
gainNode.connect(panner);
|
|
260
|
+
panner.connect(channelObj.gainNode);
|
|
261
|
+
} else {
|
|
262
|
+
source.connect(gainNode);
|
|
263
|
+
gainNode.connect(channelObj.gainNode);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// Generate unique ID
|
|
267
|
+
const id = `${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
|
268
|
+
this.activeSources.set(id, source);
|
|
269
|
+
|
|
270
|
+
source.onended = () => {
|
|
271
|
+
this.activeSources.delete(id);
|
|
272
|
+
};
|
|
273
|
+
|
|
274
|
+
source.start();
|
|
275
|
+
return id;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* Stops a playing sound.
|
|
280
|
+
* @param {string} id - Source ID from play()
|
|
281
|
+
*/
|
|
282
|
+
stop(id) {
|
|
283
|
+
const source = this.activeSources.get(id);
|
|
284
|
+
if (source) {
|
|
285
|
+
source.stop();
|
|
286
|
+
this.activeSources.delete(id);
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/**
|
|
291
|
+
* Stops all sounds on a channel.
|
|
292
|
+
* @param {string} channelName - Channel name
|
|
293
|
+
*/
|
|
294
|
+
stopChannel(channelName) {
|
|
295
|
+
// Note: This stops ALL sounds since we'd need to track by channel
|
|
296
|
+
// A more complete implementation would track sources per channel
|
|
297
|
+
this.activeSources.forEach((source, id) => {
|
|
298
|
+
try {
|
|
299
|
+
source.stop();
|
|
300
|
+
} catch (e) {
|
|
301
|
+
// Already stopped
|
|
302
|
+
}
|
|
303
|
+
});
|
|
304
|
+
this.activeSources.clear();
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
/**
|
|
308
|
+
* Plays music with optional crossfade from current track.
|
|
309
|
+
* @param {string|AudioBuffer} music - URL or AudioBuffer
|
|
310
|
+
* @param {object} [options] - Playback options
|
|
311
|
+
* @param {number} [options.fadeIn=2] - Fade in duration in seconds
|
|
312
|
+
* @param {number} [options.fadeOut=2] - Fade out duration for current track
|
|
313
|
+
* @param {boolean} [options.loop=true] - Whether to loop
|
|
314
|
+
* @returns {Promise<void>}
|
|
315
|
+
*/
|
|
316
|
+
async playMusic(music, options = {}) {
|
|
317
|
+
if (!this.context) await this.init();
|
|
318
|
+
|
|
319
|
+
const { fadeIn = 2, fadeOut = 2, loop = true } = options;
|
|
320
|
+
|
|
321
|
+
const buffer = typeof music === 'string' ? await this.loadBuffer(music) : music;
|
|
322
|
+
const channel = this.channels.get('music');
|
|
323
|
+
if (!channel) return;
|
|
324
|
+
|
|
325
|
+
// Fade out current music
|
|
326
|
+
if (this.musicTracks.size > 0) {
|
|
327
|
+
for (const [id, track] of this.musicTracks) {
|
|
328
|
+
track.gain.gain.linearRampToValueAtTime(0, this.context.currentTime + fadeOut);
|
|
329
|
+
setTimeout(() => {
|
|
330
|
+
try {
|
|
331
|
+
track.source.stop();
|
|
332
|
+
} catch (e) {}
|
|
333
|
+
this.musicTracks.delete(id);
|
|
334
|
+
}, fadeOut * 1000);
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
// Create new music source
|
|
339
|
+
const source = this.context.createBufferSource();
|
|
340
|
+
source.buffer = buffer;
|
|
341
|
+
source.loop = loop;
|
|
342
|
+
|
|
343
|
+
const gainNode = this.context.createGain();
|
|
344
|
+
gainNode.gain.value = 0;
|
|
345
|
+
|
|
346
|
+
source.connect(gainNode);
|
|
347
|
+
gainNode.connect(channel.gainNode);
|
|
348
|
+
|
|
349
|
+
const id = `music_${Date.now()}`;
|
|
350
|
+
this.musicTracks.set(id, { source, gain: gainNode });
|
|
351
|
+
|
|
352
|
+
source.start();
|
|
353
|
+
|
|
354
|
+
// Fade in
|
|
355
|
+
gainNode.gain.linearRampToValueAtTime(1, this.context.currentTime + fadeIn);
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
/**
|
|
359
|
+
* Stops all music with fade out.
|
|
360
|
+
* @param {number} [fadeOut=2] - Fade out duration
|
|
361
|
+
*/
|
|
362
|
+
stopMusic(fadeOut = 2) {
|
|
363
|
+
if (!this.context) return;
|
|
364
|
+
|
|
365
|
+
for (const [id, track] of this.musicTracks) {
|
|
366
|
+
track.gain.gain.linearRampToValueAtTime(0, this.context.currentTime + fadeOut);
|
|
367
|
+
setTimeout(() => {
|
|
368
|
+
try {
|
|
369
|
+
track.source.stop();
|
|
370
|
+
} catch (e) {}
|
|
371
|
+
this.musicTracks.delete(id);
|
|
372
|
+
}, fadeOut * 1000);
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
/**
|
|
377
|
+
* Configures a panner node for spatial audio.
|
|
378
|
+
* @param {PannerNode} panner - The panner node
|
|
379
|
+
* @param {number[]} position - [x, y, z] position
|
|
380
|
+
*/
|
|
381
|
+
configurePanner(panner, position) {
|
|
382
|
+
panner.panningModel = 'HRTF';
|
|
383
|
+
panner.distanceModel = this.spatialConfig.distanceModel;
|
|
384
|
+
panner.maxDistance = this.spatialConfig.maxDistance;
|
|
385
|
+
panner.refDistance = this.spatialConfig.refDistance;
|
|
386
|
+
panner.rolloffFactor = this.spatialConfig.rolloffFactor;
|
|
387
|
+
panner.setPosition(position[0], position[1], position[2]);
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
/**
|
|
391
|
+
* Updates the listener position for spatial audio.
|
|
392
|
+
* @param {number} x - X position
|
|
393
|
+
* @param {number} y - Y position
|
|
394
|
+
* @param {number} z - Z position
|
|
395
|
+
*/
|
|
396
|
+
setListenerPosition(x, y, z) {
|
|
397
|
+
if (!this.context) return;
|
|
398
|
+
|
|
399
|
+
this.listenerPosition = { x, y, z };
|
|
400
|
+
const listener = this.context.listener;
|
|
401
|
+
|
|
402
|
+
if (listener.positionX) {
|
|
403
|
+
// Modern API
|
|
404
|
+
listener.positionX.value = x;
|
|
405
|
+
listener.positionY.value = y;
|
|
406
|
+
listener.positionZ.value = z;
|
|
407
|
+
} else {
|
|
408
|
+
// Legacy API
|
|
409
|
+
listener.setPosition(x, y, z);
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
/**
|
|
414
|
+
* Sets the listener orientation.
|
|
415
|
+
* @param {number} fx - Forward X
|
|
416
|
+
* @param {number} fy - Forward Y
|
|
417
|
+
* @param {number} fz - Forward Z
|
|
418
|
+
* @param {number} ux - Up X
|
|
419
|
+
* @param {number} uy - Up Y
|
|
420
|
+
* @param {number} uz - Up Z
|
|
421
|
+
*/
|
|
422
|
+
setListenerOrientation(fx, fy, fz, ux = 0, uy = 1, uz = 0) {
|
|
423
|
+
if (!this.context) return;
|
|
424
|
+
|
|
425
|
+
const listener = this.context.listener;
|
|
426
|
+
|
|
427
|
+
if (listener.forwardX) {
|
|
428
|
+
listener.forwardX.value = fx;
|
|
429
|
+
listener.forwardY.value = fy;
|
|
430
|
+
listener.forwardZ.value = fz;
|
|
431
|
+
listener.upX.value = ux;
|
|
432
|
+
listener.upY.value = uy;
|
|
433
|
+
listener.upZ.value = uz;
|
|
434
|
+
} else {
|
|
435
|
+
listener.setOrientation(fx, fy, fz, ux, uy, uz);
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
/**
|
|
440
|
+
* Sets reverb amount (wet/dry mix).
|
|
441
|
+
* @param {number} amount - Reverb amount (0-1)
|
|
442
|
+
*/
|
|
443
|
+
setReverbAmount(amount) {
|
|
444
|
+
if (this.reverbGain) {
|
|
445
|
+
this.reverbGain.gain.value = Math.max(0, Math.min(1, amount));
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
/**
|
|
450
|
+
* Creates a simple impulse response for reverb.
|
|
451
|
+
* @param {number} [duration=2] - Reverb duration in seconds
|
|
452
|
+
* @param {number} [decay=2] - Decay rate
|
|
453
|
+
* @returns {AudioBuffer}
|
|
454
|
+
*/
|
|
455
|
+
createImpulseResponse(duration = 2, decay = 2) {
|
|
456
|
+
if (!this.context) return null;
|
|
457
|
+
|
|
458
|
+
const sampleRate = this.context.sampleRate;
|
|
459
|
+
const length = sampleRate * duration;
|
|
460
|
+
const impulse = this.context.createBuffer(2, length, sampleRate);
|
|
461
|
+
|
|
462
|
+
for (let channel = 0; channel < 2; channel++) {
|
|
463
|
+
const channelData = impulse.getChannelData(channel);
|
|
464
|
+
for (let i = 0; i < length; i++) {
|
|
465
|
+
channelData[i] = (Math.random() * 2 - 1) * Math.pow(1 - i / length, decay);
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
return impulse;
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
/**
|
|
473
|
+
* Enables reverb with custom impulse response.
|
|
474
|
+
* @param {AudioBuffer} [impulse] - Custom impulse response
|
|
475
|
+
*/
|
|
476
|
+
enableReverb(impulse = null) {
|
|
477
|
+
if (!this.context) return;
|
|
478
|
+
|
|
479
|
+
this.reverb = this.context.createConvolver();
|
|
480
|
+
this.reverb.buffer = impulse || this.createImpulseResponse();
|
|
481
|
+
|
|
482
|
+
// Insert reverb into chain
|
|
483
|
+
this.reverbGain.gain.value = 0.3;
|
|
484
|
+
this.masterGain.connect(this.reverbGain);
|
|
485
|
+
this.reverbGain.connect(this.reverb);
|
|
486
|
+
this.reverb.connect(this.compressor);
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
/**
|
|
490
|
+
* Disables reverb.
|
|
491
|
+
*/
|
|
492
|
+
disableReverb() {
|
|
493
|
+
if (this.reverbGain) {
|
|
494
|
+
this.reverbGain.gain.value = 0;
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
/**
|
|
499
|
+
* Gets volume levels for visualization.
|
|
500
|
+
* @returns {{ left: number, right: number }}
|
|
501
|
+
*/
|
|
502
|
+
getVolumeLevels() {
|
|
503
|
+
// This would require an AnalyserNode to implement properly
|
|
504
|
+
return { left: 0, right: 0 };
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
/**
|
|
508
|
+
* Resumes audio context if suspended.
|
|
509
|
+
* @returns {Promise<void>}
|
|
510
|
+
*/
|
|
511
|
+
async resume() {
|
|
512
|
+
if (this.context && this.context.state === 'suspended') {
|
|
513
|
+
await this.context.resume();
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
/**
|
|
518
|
+
* Suspends audio context.
|
|
519
|
+
* @returns {Promise<void>}
|
|
520
|
+
*/
|
|
521
|
+
async suspend() {
|
|
522
|
+
if (this.context && this.context.state === 'running') {
|
|
523
|
+
await this.context.suspend();
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
/**
|
|
528
|
+
* Cleans up all audio resources.
|
|
529
|
+
*/
|
|
530
|
+
dispose() {
|
|
531
|
+
this.stopChannel('all');
|
|
532
|
+
this.stopMusic(0);
|
|
533
|
+
this.bufferCache.clear();
|
|
534
|
+
this.channels.clear();
|
|
535
|
+
|
|
536
|
+
if (this.context) {
|
|
537
|
+
this.context.close();
|
|
538
|
+
this.context = null;
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
this.initialized = false;
|
|
542
|
+
}
|
|
543
|
+
}
|