Compare commits
1 Commits
mimo/code/
...
fix/1544
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1a0ae0aac4 |
@@ -395,6 +395,7 @@
|
|||||||
<div id="memory-connections-panel" class="memory-connections-panel" style="display:none;" aria-label="Memory Connections Panel"></div>
|
<div id="memory-connections-panel" class="memory-connections-panel" style="display:none;" aria-label="Memory Connections Panel"></div>
|
||||||
|
|
||||||
<script src="./boot.js"></script>
|
<script src="./boot.js"></script>
|
||||||
|
<script src="./js/spatial-audio-chat.js"></script>
|
||||||
<script src="./avatar-customization.js"></script>
|
<script src="./avatar-customization.js"></script>
|
||||||
<script src="./lod-system.js"></script>
|
<script src="./lod-system.js"></script>
|
||||||
<script>
|
<script>
|
||||||
|
|||||||
337
js/spatial-audio-chat.js
Normal file
337
js/spatial-audio-chat.js
Normal file
@@ -0,0 +1,337 @@
|
|||||||
|
/**
|
||||||
|
* 3D Audio Spatial Chat
|
||||||
|
* Issue #1544: feat: 3D audio spatial chat — volume based on distance
|
||||||
|
*
|
||||||
|
* Provides spatial audio awareness so near users are louder.
|
||||||
|
*/
|
||||||
|
|
||||||
|
class SpatialAudioChat {
|
||||||
|
constructor(options = {}) {
|
||||||
|
this.maxDistance = options.maxDistance || 50; // Maximum hearing distance in units
|
||||||
|
this.minVolume = options.minVolume || 0.1; // Minimum volume (10%)
|
||||||
|
this.maxVolume = options.maxVolume || 1.0; // Maximum volume (100%)
|
||||||
|
this.enabled = options.enabled !== undefined ? options.enabled : true;
|
||||||
|
|
||||||
|
// Audio context for spatial audio
|
||||||
|
this.audioContext = null;
|
||||||
|
this.listener = null;
|
||||||
|
|
||||||
|
// Track user positions
|
||||||
|
this.userPositions = new Map();
|
||||||
|
this.localUserId = options.localUserId || 'local';
|
||||||
|
|
||||||
|
// Initialize audio context
|
||||||
|
this.initAudioContext();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize Web Audio API context
|
||||||
|
*/
|
||||||
|
initAudioContext() {
|
||||||
|
try {
|
||||||
|
// Create audio context
|
||||||
|
this.audioContext = new (window.AudioContext || window.webkitAudioContext)();
|
||||||
|
|
||||||
|
// Create audio listener (represents the local user's ears)
|
||||||
|
this.listener = this.audioContext.listener;
|
||||||
|
|
||||||
|
// Set default listener position
|
||||||
|
if (this.listener.positionX) {
|
||||||
|
// Modern API
|
||||||
|
this.listener.positionX.value = 0;
|
||||||
|
this.listener.positionY.value = 0;
|
||||||
|
this.listener.positionZ.value = 0;
|
||||||
|
} else {
|
||||||
|
// Fallback for older browsers
|
||||||
|
this.listener.setPosition(0, 0, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('[SpatialAudio] Audio context initialized');
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[SpatialAudio] Failed to initialize audio context:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update local user position (the listener)
|
||||||
|
*/
|
||||||
|
updateLocalPosition(x, y, z) {
|
||||||
|
if (!this.listener) return;
|
||||||
|
|
||||||
|
if (this.listener.positionX) {
|
||||||
|
// Modern API
|
||||||
|
this.listener.positionX.value = x;
|
||||||
|
this.listener.positionY.value = y;
|
||||||
|
this.listener.positionZ.value = z;
|
||||||
|
} else {
|
||||||
|
// Fallback
|
||||||
|
this.listener.setPosition(x, y, z);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Store local position
|
||||||
|
this.userPositions.set(this.localUserId, { x, y, z });
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update remote user position
|
||||||
|
*/
|
||||||
|
updateRemoteUserPosition(userId, x, y, z) {
|
||||||
|
this.userPositions.set(userId, { x, y, z });
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculate distance between two 3D points
|
||||||
|
*/
|
||||||
|
calculateDistance(pos1, pos2) {
|
||||||
|
const dx = pos1.x - pos2.x;
|
||||||
|
const dy = pos1.y - pos2.y;
|
||||||
|
const dz = pos1.z - pos2.z;
|
||||||
|
return Math.sqrt(dx * dx + dy * dy + dz * dz);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculate volume based on distance
|
||||||
|
* Uses inverse square law for realistic falloff
|
||||||
|
*/
|
||||||
|
calculateVolume(distance) {
|
||||||
|
if (!this.enabled) return this.maxVolume;
|
||||||
|
|
||||||
|
if (distance >= this.maxDistance) {
|
||||||
|
return this.minVolume;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (distance <= 0) {
|
||||||
|
return this.maxVolume;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Inverse square law: volume = 1 / (distance^2)
|
||||||
|
// Normalize to [minVolume, maxVolume] range
|
||||||
|
const normalizedDistance = distance / this.maxDistance;
|
||||||
|
const volume = this.maxVolume / (1 + normalizedDistance * normalizedDistance * 10);
|
||||||
|
|
||||||
|
// Clamp to min/max
|
||||||
|
return Math.max(this.minVolume, Math.min(this.maxVolume, volume));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get volume for a specific user based on their distance
|
||||||
|
*/
|
||||||
|
getVolumeForUser(userId) {
|
||||||
|
const localPos = this.userPositions.get(this.localUserId);
|
||||||
|
const remotePos = this.userPositions.get(userId);
|
||||||
|
|
||||||
|
if (!localPos || !remotePos) {
|
||||||
|
return this.maxVolume; // Default to max if positions unknown
|
||||||
|
}
|
||||||
|
|
||||||
|
const distance = this.calculateDistance(localPos, remotePos);
|
||||||
|
return this.calculateVolume(distance);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get spatial audio parameters for a user
|
||||||
|
*/
|
||||||
|
getSpatialAudioParams(userId) {
|
||||||
|
const localPos = this.userPositions.get(this.localUserId);
|
||||||
|
const remotePos = this.userPositions.get(userId);
|
||||||
|
|
||||||
|
if (!localPos || !remotePos) {
|
||||||
|
return {
|
||||||
|
volume: this.maxVolume,
|
||||||
|
distance: 0,
|
||||||
|
pan: 0,
|
||||||
|
enabled: false
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const distance = this.calculateDistance(localPos, remotePos);
|
||||||
|
const volume = this.calculateVolume(distance);
|
||||||
|
|
||||||
|
// Calculate pan (left-right balance) based on relative X position
|
||||||
|
const dx = remotePos.x - localPos.x;
|
||||||
|
const pan = Math.max(-1, Math.min(1, dx / this.maxDistance));
|
||||||
|
|
||||||
|
return {
|
||||||
|
volume,
|
||||||
|
distance,
|
||||||
|
pan,
|
||||||
|
enabled: this.enabled && distance < this.maxDistance
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create spatial audio source for a user
|
||||||
|
*/
|
||||||
|
createSpatialAudioSource(userId, audioElement) {
|
||||||
|
if (!this.audioContext) {
|
||||||
|
console.warn('[SpatialAudio] Audio context not initialized');
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Create media element source
|
||||||
|
const source = this.audioContext.createMediaElementSource(audioElement);
|
||||||
|
|
||||||
|
// Create panner for 3D positioning
|
||||||
|
const panner = this.audioContext.createPanner();
|
||||||
|
panner.panningModel = 'HRTF'; // Head-Related Transfer Function for realistic 3D
|
||||||
|
panner.distanceModel = 'inverse';
|
||||||
|
panner.refDistance = 1;
|
||||||
|
panner.maxDistance = this.maxDistance;
|
||||||
|
panner.rolloffFactor = 1;
|
||||||
|
|
||||||
|
// Create gain node for volume control
|
||||||
|
const gainNode = this.audioContext.createGain();
|
||||||
|
|
||||||
|
// Connect: source -> gain -> panner -> destination
|
||||||
|
source.connect(gainNode);
|
||||||
|
gainNode.connect(panner);
|
||||||
|
panner.connect(this.audioContext.destination);
|
||||||
|
|
||||||
|
// Update position based on user data
|
||||||
|
this.updateAudioSourcePosition(userId, panner, gainNode);
|
||||||
|
|
||||||
|
return {
|
||||||
|
source,
|
||||||
|
panner,
|
||||||
|
gainNode,
|
||||||
|
updatePosition: () => this.updateAudioSourcePosition(userId, panner, gainNode)
|
||||||
|
};
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[SpatialAudio] Failed to create spatial audio source:', error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update audio source position based on user position
|
||||||
|
*/
|
||||||
|
updateAudioSourcePosition(userId, panner, gainNode) {
|
||||||
|
const params = this.getSpatialAudioParams(userId);
|
||||||
|
|
||||||
|
if (panner.positionX) {
|
||||||
|
// Modern API
|
||||||
|
const pos = this.userPositions.get(userId) || { x: 0, y: 0, z: 0 };
|
||||||
|
panner.positionX.value = pos.x;
|
||||||
|
panner.positionY.value = pos.y;
|
||||||
|
panner.positionZ.value = pos.z;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update volume
|
||||||
|
gainNode.gain.value = params.volume;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Play chat notification with spatial audio
|
||||||
|
*/
|
||||||
|
playChatNotification(userId, audioUrl) {
|
||||||
|
if (!this.enabled) {
|
||||||
|
// Just play normally
|
||||||
|
const audio = new Audio(audioUrl);
|
||||||
|
audio.volume = this.maxVolume;
|
||||||
|
audio.play();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const volume = this.getVolumeForUser(userId);
|
||||||
|
|
||||||
|
const audio = new Audio(audioUrl);
|
||||||
|
audio.volume = volume;
|
||||||
|
|
||||||
|
console.log(`[SpatialAudio] Playing notification for ${userId} at volume ${volume.toFixed(2)}`);
|
||||||
|
|
||||||
|
audio.play().catch(error => {
|
||||||
|
console.error('[SpatialAudio] Failed to play audio:', error);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get hearing distance for UI display
|
||||||
|
*/
|
||||||
|
getHearingDistance() {
|
||||||
|
return this.maxDistance;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set maximum hearing distance
|
||||||
|
*/
|
||||||
|
setHearingDistance(distance) {
|
||||||
|
if (typeof distance !== 'number' || distance < 1) {
|
||||||
|
console.warn('[SpatialAudio] Invalid hearing distance:', distance);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.maxDistance = distance;
|
||||||
|
console.log(`[SpatialAudio] Max hearing distance set to ${this.maxDistance}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Enable/disable spatial audio
|
||||||
|
*/
|
||||||
|
setEnabled(enabled) {
|
||||||
|
this.enabled = enabled;
|
||||||
|
console.log(`[SpatialAudio] ${enabled ? 'Enabled' : 'Disabled'}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get current status
|
||||||
|
*/
|
||||||
|
getStatus() {
|
||||||
|
return {
|
||||||
|
enabled: this.enabled,
|
||||||
|
maxDistance: this.maxDistance,
|
||||||
|
minVolume: this.minVolume,
|
||||||
|
maxVolume: this.maxVolume,
|
||||||
|
userCount: this.userPositions.size,
|
||||||
|
audioContextState: this.audioContext ? this.audioContext.state : 'not initialized'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clean up resources
|
||||||
|
*/
|
||||||
|
destroy() {
|
||||||
|
if (this.audioContext) {
|
||||||
|
this.audioContext.close();
|
||||||
|
this.audioContext = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.userPositions.clear();
|
||||||
|
console.log('[SpatialAudio] Destroyed');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Export for use in other modules
|
||||||
|
if (typeof module !== 'undefined' && module.exports) {
|
||||||
|
module.exports = SpatialAudioChat;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Global instance for browser use
|
||||||
|
if (typeof window !== 'undefined') {
|
||||||
|
window.SpatialAudioChat = SpatialAudioChat;
|
||||||
|
|
||||||
|
// Auto-initialize if chat panel exists
|
||||||
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
|
const chatPanel = document.getElementById('chat-panel');
|
||||||
|
if (chatPanel) {
|
||||||
|
const spatialAudio = new SpatialAudioChat({
|
||||||
|
maxDistance: 50,
|
||||||
|
enabled: true
|
||||||
|
});
|
||||||
|
|
||||||
|
// Store globally for access
|
||||||
|
window.spatialAudio = spatialAudio;
|
||||||
|
|
||||||
|
// Update local position when user moves
|
||||||
|
if (window.updateUserPosition) {
|
||||||
|
const originalUpdate = window.updateUserPosition;
|
||||||
|
window.updateUserPosition = function(x, y, z) {
|
||||||
|
originalUpdate(x, y, z);
|
||||||
|
spatialAudio.updateLocalPosition(x, y, z);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
248
tests/test_spatial_audio_chat.js
Normal file
248
tests/test_spatial_audio_chat.js
Normal file
@@ -0,0 +1,248 @@
|
|||||||
|
/**
|
||||||
|
* Tests for 3D Audio Spatial Chat
|
||||||
|
* Issue #1544: feat: 3D audio spatial chat — volume based on distance
|
||||||
|
*/
|
||||||
|
|
||||||
|
const test = require('node:test');
|
||||||
|
const assert = require('node:assert/strict');
|
||||||
|
const fs = require('node:fs');
|
||||||
|
const path = require('node:path');
|
||||||
|
|
||||||
|
const ROOT = path.resolve(__dirname, '..');
|
||||||
|
|
||||||
|
// Mock AudioContext
|
||||||
|
class MockAudioContext {
|
||||||
|
constructor() {
|
||||||
|
this.state = 'running';
|
||||||
|
this.listener = {
|
||||||
|
positionX: { value: 0 },
|
||||||
|
positionY: { value: 0 },
|
||||||
|
positionZ: { value: 0 },
|
||||||
|
setPosition: function(x, y, z) {
|
||||||
|
this.positionX.value = x;
|
||||||
|
this.positionY.value = y;
|
||||||
|
this.positionZ.value = z;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
createMediaElementSource() {
|
||||||
|
return {
|
||||||
|
connect: () => {}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
createPanner() {
|
||||||
|
return {
|
||||||
|
panningModel: 'HRTF',
|
||||||
|
distanceModel: 'inverse',
|
||||||
|
refDistance: 1,
|
||||||
|
maxDistance: 50,
|
||||||
|
rolloffFactor: 1,
|
||||||
|
positionX: { value: 0 },
|
||||||
|
positionY: { value: 0 },
|
||||||
|
positionZ: { value: 0 },
|
||||||
|
connect: () => {}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
createGain() {
|
||||||
|
return {
|
||||||
|
gain: { value: 1.0 },
|
||||||
|
connect: () => {}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
close() {
|
||||||
|
this.state = 'closed';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mock window.AudioContext
|
||||||
|
global.window = {
|
||||||
|
AudioContext: MockAudioContext,
|
||||||
|
webkitAudioContext: MockAudioContext
|
||||||
|
};
|
||||||
|
|
||||||
|
// Load spatial-audio-chat.js
|
||||||
|
const spatialAudioPath = path.join(ROOT, 'js', 'spatial-audio-chat.js');
|
||||||
|
const spatialAudioCode = fs.readFileSync(spatialAudioPath, 'utf8');
|
||||||
|
|
||||||
|
// Execute in context
|
||||||
|
const vm = require('node:vm');
|
||||||
|
const context = {
|
||||||
|
module: { exports: {} },
|
||||||
|
exports: {},
|
||||||
|
console,
|
||||||
|
window: global.window,
|
||||||
|
document: { addEventListener: () => {} },
|
||||||
|
Math: Math
|
||||||
|
};
|
||||||
|
|
||||||
|
vm.runInNewContext(spatialAudioCode, context);
|
||||||
|
|
||||||
|
// Get SpatialAudioChat
|
||||||
|
const SpatialAudioChat = context.window.SpatialAudioChat || context.module.exports;
|
||||||
|
|
||||||
|
test('SpatialAudioChat loads correctly', () => {
|
||||||
|
assert.ok(SpatialAudioChat, 'SpatialAudioChat should be defined');
|
||||||
|
assert.ok(typeof SpatialAudioChat === 'function', 'SpatialAudioChat should be a constructor');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('SpatialAudioChat can be instantiated', () => {
|
||||||
|
const spatialAudio = new SpatialAudioChat();
|
||||||
|
assert.ok(spatialAudio, 'SpatialAudioChat instance should be created');
|
||||||
|
assert.equal(spatialAudio.maxDistance, 50, 'Should have default max distance');
|
||||||
|
assert.equal(spatialAudio.minVolume, 0.1, 'Should have default min volume');
|
||||||
|
assert.equal(spatialAudio.maxVolume, 1.0, 'Should have default max volume');
|
||||||
|
assert.ok(spatialAudio.enabled, 'Should be enabled by default');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('SpatialAudioChat can update local position', () => {
|
||||||
|
const spatialAudio = new SpatialAudioChat();
|
||||||
|
|
||||||
|
spatialAudio.updateLocalPosition(10, 20, 30);
|
||||||
|
|
||||||
|
const localPos = spatialAudio.userPositions.get('local');
|
||||||
|
assert.ok(localPos, 'Should have local position');
|
||||||
|
assert.equal(localPos.x, 10, 'Should have correct x');
|
||||||
|
assert.equal(localPos.y, 20, 'Should have correct y');
|
||||||
|
assert.equal(localPos.z, 30, 'Should have correct z');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('SpatialAudioChat can update remote user position', () => {
|
||||||
|
const spatialAudio = new SpatialAudioChat();
|
||||||
|
|
||||||
|
spatialAudio.updateRemoteUserPosition('user1', 5, 10, 15);
|
||||||
|
|
||||||
|
const userPos = spatialAudio.userPositions.get('user1');
|
||||||
|
assert.ok(userPos, 'Should have user position');
|
||||||
|
assert.equal(userPos.x, 5, 'Should have correct x');
|
||||||
|
assert.equal(userPos.y, 10, 'Should have correct y');
|
||||||
|
assert.equal(userPos.z, 15, 'Should have correct z');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('SpatialAudioChat calculates distance correctly', () => {
|
||||||
|
const spatialAudio = new SpatialAudioChat();
|
||||||
|
|
||||||
|
const pos1 = { x: 0, y: 0, z: 0 };
|
||||||
|
const pos2 = { x: 3, y: 4, z: 0 };
|
||||||
|
|
||||||
|
const distance = spatialAudio.calculateDistance(pos1, pos2);
|
||||||
|
assert.equal(distance, 5, 'Should calculate 3-4-5 triangle correctly');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('SpatialAudioChat calculates volume based on distance', () => {
|
||||||
|
const spatialAudio = new SpatialAudioChat({
|
||||||
|
maxDistance: 10,
|
||||||
|
minVolume: 0.1,
|
||||||
|
maxVolume: 1.0
|
||||||
|
});
|
||||||
|
|
||||||
|
// At distance 0, should be max volume
|
||||||
|
const volume0 = spatialAudio.calculateVolume(0);
|
||||||
|
assert.equal(volume0, 1.0, 'Should be max volume at distance 0');
|
||||||
|
|
||||||
|
// At max distance, should be min volume
|
||||||
|
const volume10 = spatialAudio.calculateVolume(10);
|
||||||
|
assert.equal(volume10, 0.1, 'Should be min volume at max distance');
|
||||||
|
|
||||||
|
// At half distance, should be between min and max
|
||||||
|
const volume5 = spatialAudio.calculateVolume(5);
|
||||||
|
assert.ok(volume5 > 0.1 && volume5 < 1.0, 'Should be between min and max at half distance');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('SpatialAudioChat returns correct volume for user', () => {
|
||||||
|
const spatialAudio = new SpatialAudioChat({
|
||||||
|
maxDistance: 100,
|
||||||
|
minVolume: 0.1,
|
||||||
|
maxVolume: 1.0
|
||||||
|
});
|
||||||
|
|
||||||
|
// Set local position
|
||||||
|
spatialAudio.updateLocalPosition(0, 0, 0);
|
||||||
|
|
||||||
|
// Set remote user position
|
||||||
|
spatialAudio.updateRemoteUserPosition('user1', 10, 0, 0);
|
||||||
|
|
||||||
|
// Get volume for user
|
||||||
|
const volume = spatialAudio.getVolumeForUser('user1');
|
||||||
|
assert.ok(volume > 0.1 && volume <= 1.0, 'Volume should be between min and max');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('SpatialAudioChat returns spatial audio parameters', () => {
|
||||||
|
const spatialAudio = new SpatialAudioChat({
|
||||||
|
maxDistance: 100,
|
||||||
|
minVolume: 0.1,
|
||||||
|
maxVolume: 1.0
|
||||||
|
});
|
||||||
|
|
||||||
|
// Set local position
|
||||||
|
spatialAudio.updateLocalPosition(0, 0, 0);
|
||||||
|
|
||||||
|
// Set remote user position
|
||||||
|
spatialAudio.updateRemoteUserPosition('user1', 10, 5, 0);
|
||||||
|
|
||||||
|
// Get spatial audio params
|
||||||
|
const params = spatialAudio.getSpatialAudioParams('user1');
|
||||||
|
|
||||||
|
assert.ok(params, 'Should return params');
|
||||||
|
assert.ok(params.volume > 0, 'Should have volume');
|
||||||
|
assert.ok(params.distance > 0, 'Should have distance');
|
||||||
|
assert.ok(params.pan !== undefined, 'Should have pan');
|
||||||
|
assert.ok(params.enabled !== undefined, 'Should have enabled flag');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('SpatialAudioChat can enable/disable', () => {
|
||||||
|
const spatialAudio = new SpatialAudioChat();
|
||||||
|
|
||||||
|
assert.ok(spatialAudio.enabled, 'Should be enabled by default');
|
||||||
|
|
||||||
|
spatialAudio.setEnabled(false);
|
||||||
|
assert.ok(!spatialAudio.enabled, 'Should be disabled after setEnabled(false)');
|
||||||
|
|
||||||
|
spatialAudio.setEnabled(true);
|
||||||
|
assert.ok(spatialAudio.enabled, 'Should be enabled after setEnabled(true)');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('SpatialAudioChat can set hearing distance', () => {
|
||||||
|
const spatialAudio = new SpatialAudioChat();
|
||||||
|
|
||||||
|
assert.equal(spatialAudio.maxDistance, 50, 'Should have default max distance');
|
||||||
|
|
||||||
|
spatialAudio.setHearingDistance(100);
|
||||||
|
assert.equal(spatialAudio.maxDistance, 100, 'Should update max distance');
|
||||||
|
|
||||||
|
// Should reject invalid distance
|
||||||
|
spatialAudio.setHearingDistance(-5);
|
||||||
|
assert.equal(spatialAudio.maxDistance, 100, 'Should not update with invalid distance');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('SpatialAudioChat returns status', () => {
|
||||||
|
const spatialAudio = new SpatialAudioChat();
|
||||||
|
|
||||||
|
const status = spatialAudio.getStatus();
|
||||||
|
|
||||||
|
assert.ok(status, 'Should return status object');
|
||||||
|
assert.equal(status.enabled, true, 'Should be enabled');
|
||||||
|
assert.equal(status.maxDistance, 50, 'Should have correct max distance');
|
||||||
|
assert.equal(status.minVolume, 0.1, 'Should have correct min volume');
|
||||||
|
assert.equal(status.maxVolume, 1.0, 'Should have correct max volume');
|
||||||
|
assert.equal(status.userCount, 0, 'Should have 0 users initially');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('SpatialAudioChat can be destroyed', () => {
|
||||||
|
const spatialAudio = new SpatialAudioChat();
|
||||||
|
|
||||||
|
// Add some data
|
||||||
|
spatialAudio.updateLocalPosition(1, 2, 3);
|
||||||
|
spatialAudio.updateRemoteUserPosition('user1', 4, 5, 6);
|
||||||
|
|
||||||
|
// Destroy
|
||||||
|
spatialAudio.destroy();
|
||||||
|
|
||||||
|
assert.equal(spatialAudio.audioContext, null, 'Audio context should be null after destroy');
|
||||||
|
assert.equal(spatialAudio.userPositions.size, 0, 'User positions should be cleared');
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('All Spatial Audio Chat tests passed!');
|
||||||
Reference in New Issue
Block a user