Compare commits

..

1 Commits

Author SHA1 Message Date
Alexander Whitestone
9c3d9952d7 fix: closes #674
Some checks failed
CI / test (pull_request) Failing after 9s
CI / validate (pull_request) Failing after 13s
Review Approval Gate / verify-review (pull_request) Failing after 2s
2026-04-12 12:26:17 -04:00
3 changed files with 56 additions and 249 deletions

View File

@@ -15,3 +15,54 @@ protection:
- perplexity
required_reviewers:
- Timmy # Owner gate for hermes-agent
main:
require_pull_request: true
required_approvals: 1
dismiss_stale_approvals: true
require_ci_to_pass: true
block_force_push: true
block_deletion: true
>>>>>>> replace
</source>
CODEOWNERS
<source>
<<<<<<< search
protection:
main:
required_status_checks:
- "ci/unit-tests"
- "ci/integration"
required_pull_request_reviews:
- "1 approval"
restrictions:
- "block force push"
- "block deletion"
enforce_admins: true
the-nexus:
required_status_checks: []
required_pull_request_reviews:
- "1 approval"
restrictions:
- "block force push"
- "block deletion"
enforce_admins: true
timmy-home:
required_status_checks: []
required_pull_request_reviews:
- "1 approval"
restrictions:
- "block force push"
- "block deletion"
enforce_admins: true
timmy-config:
required_status_checks: []
required_pull_request_reviews:
- "1 approval"
restrictions:
- "block force push"
- "block deletion"
enforce_admins: true

12
app.js
View File

@@ -4,7 +4,6 @@ import { RenderPass } from 'three/addons/postprocessing/RenderPass.js';
import { UnrealBloomPass } from 'three/addons/postprocessing/UnrealBloomPass.js';
import { SMAAPass } from 'three/addons/postprocessing/SMAAPass.js';
import { SpatialMemory } from './nexus/components/spatial-memory.js';
import { SpatialAudio } from './nexus/components/spatial-audio.js';
import { MemoryBirth } from './nexus/components/memory-birth.js';
import { MemoryOptimizer } from './nexus/components/memory-optimizer.js';
import { MemoryInspect } from './nexus/components/memory-inspect.js';
@@ -716,8 +715,6 @@ async function init() {
MemoryBirth.init(scene);
MemoryBirth.wrapSpatialMemory(SpatialMemory);
SpatialMemory.setCamera(camera);
SpatialAudio.init(camera, scene);
SpatialAudio.bindSpatialMemory(SpatialMemory);
MemoryInspect.init({ onNavigate: _navigateToMemory });
MemoryPulse.init(SpatialMemory);
updateLoad(90);
@@ -1922,10 +1919,12 @@ function setupControls() {
);
const raycaster = new THREE.Raycaster();
raycaster.setFromCamera(mouse, camera);
const intersects = raycaster.intersectObjects(portals.map(p => p.ring));
// Raycast against both ring and swirl for a larger click target
const portalMeshes = portals.flatMap(p => [p.ring, p.swirl]);
const intersects = raycaster.intersectObjects(portalMeshes);
if (intersects.length > 0) {
const clickedRing = intersects[0].object;
const portal = portals.find(p => p.ring === clickedRing);
const hitObj = intersects[0].object;
const portal = portals.find(p => p.ring === hitObj || p.swirl === hitObj);
if (portal) activatePortal(portal);
}
}
@@ -2929,7 +2928,6 @@ function gameLoop() {
// Project Mnemosyne - Memory Orb Animation
if (typeof animateMemoryOrbs === 'function') {
SpatialMemory.update(delta);
SpatialAudio.update(delta);
MemoryBirth.update(delta);
MemoryPulse.update();
animateMemoryOrbs(delta);

View File

@@ -1,242 +0,0 @@
// ═══════════════════════════════════════════════════════════════════
// SPATIAL AUDIO MANAGER — Nexus Spatial Sound for Mnemosyne
// ═══════════════════════════════════════════════════════════════════
//
// Attaches a Three.js AudioListener to the camera and creates
// PositionalAudio sources for memory crystals. Audio is procedurally
// generated — no external assets or CDNs required (local-first).
//
// Each region gets a distinct tone. Proximity controls volume and
// panning. Designed to layer on top of SpatialMemory without
// modifying it.
//
// Usage from app.js:
// SpatialAudio.init(camera, scene);
// SpatialAudio.bindSpatialMemory(SpatialMemory);
// SpatialAudio.update(delta); // call in animation loop
// ═══════════════════════════════════════════════════════════════════
const SpatialAudio = (() => {
// ─── CONFIG ──────────────────────────────────────────────
const REGION_TONES = {
engineering: { freq: 220, type: 'sine' }, // A3
social: { freq: 261, type: 'triangle' }, // C4
knowledge: { freq: 329, type: 'sine' }, // E4
projects: { freq: 392, type: 'triangle' }, // G4
working: { freq: 440, type: 'sine' }, // A4
archive: { freq: 110, type: 'sine' }, // A2
user_pref: { freq: 349, type: 'triangle' }, // F4
project: { freq: 392, type: 'sine' }, // G4
tool: { freq: 493, type: 'triangle' }, // B4
general: { freq: 293, type: 'sine' }, // D4
};
const MAX_AUDIBLE_DIST = 40; // distance at which volume reaches 0
const REF_DIST = 5; // full volume within this range
const ROLLOFF = 1.5;
const BASE_VOLUME = 0.12; // master volume cap per source
const AMBIENT_VOLUME = 0.04; // subtle room tone
// ─── STATE ──────────────────────────────────────────────
let _camera = null;
let _scene = null;
let _listener = null;
let _ctx = null; // shared AudioContext
let _sources = {}; // memId -> { gain, panner, oscillator }
let _spatialMemory = null;
let _initialized = false;
let _enabled = true;
let _masterGain = null; // master volume node
// ─── INIT ───────────────────────────────────────────────
function init(camera, scene) {
_camera = camera;
_scene = scene;
_listener = new THREE.AudioListener();
camera.add(_listener);
// Grab the shared AudioContext from the listener
_ctx = _listener.context;
_masterGain = _ctx.createGain();
_masterGain.gain.value = 1.0;
_masterGain.connect(_ctx.destination);
_initialized = true;
console.info('[SpatialAudio] Initialized — AudioContext state:', _ctx.state);
// Browsers require a user gesture to resume audio context
if (_ctx.state === 'suspended') {
const resume = () => {
_ctx.resume().then(() => {
console.info('[SpatialAudio] AudioContext resumed');
document.removeEventListener('click', resume);
document.removeEventListener('keydown', resume);
});
};
document.addEventListener('click', resume);
document.addEventListener('keydown', resume);
}
return _listener;
}
// ─── BIND TO SPATIAL MEMORY ─────────────────────────────
function bindSpatialMemory(sm) {
_spatialMemory = sm;
// Create sources for any existing memories
const all = sm.getAllMemories();
all.forEach(mem => _ensureSource(mem));
console.info('[SpatialAudio] Bound to SpatialMemory —', Object.keys(_sources).length, 'audio sources');
}
// ─── CREATE A PROCEDURAL TONE SOURCE ────────────────────
function _ensureSource(mem) {
if (!_ctx || !_enabled || _sources[mem.id]) return;
const regionKey = mem.category || 'working';
const tone = REGION_TONES[regionKey] || REGION_TONES.working;
// Procedural oscillator
const osc = _ctx.createOscillator();
osc.type = tone.type;
osc.frequency.value = tone.freq + _hashOffset(mem.id); // slight per-crystal detune
const gain = _ctx.createGain();
gain.gain.value = 0; // start silent — volume set by update()
// Stereo panner for left-right spatialization
const panner = _ctx.createStereoPanner();
panner.pan.value = 0;
osc.connect(gain);
gain.connect(panner);
panner.connect(_masterGain);
osc.start();
_sources[mem.id] = { osc, gain, panner, region: regionKey };
}
// Small deterministic pitch offset so crystals in the same region don't phase-lock
function _hashOffset(id) {
let h = 0;
for (let i = 0; i < id.length; i++) {
h = ((h << 5) - h) + id.charCodeAt(i);
h |= 0;
}
return (Math.abs(h) % 40) - 20; // ±20 Hz
}
// ─── PER-FRAME UPDATE ───────────────────────────────────
function update() {
if (!_initialized || !_enabled || !_spatialMemory || !_camera) return;
const camPos = _camera.position;
const memories = _spatialMemory.getAllMemories();
// Ensure sources for newly placed memories
memories.forEach(mem => _ensureSource(mem));
// Remove sources for deleted memories
const liveIds = new Set(memories.map(m => m.id));
Object.keys(_sources).forEach(id => {
if (!liveIds.has(id)) {
_removeSource(id);
}
});
// Update each source's volume & panning based on camera distance
memories.forEach(mem => {
const src = _sources[mem.id];
if (!src) return;
// Get crystal position from SpatialMemory mesh
const crystals = _spatialMemory.getCrystalMeshes();
let meshPos = null;
for (const mesh of crystals) {
if (mesh.userData.memId === mem.id) {
meshPos = mesh.position;
break;
}
}
if (!meshPos) return;
const dx = meshPos.x - camPos.x;
const dy = meshPos.y - camPos.y;
const dz = meshPos.z - camPos.z;
const dist = Math.sqrt(dx * dx + dy * dy + dz * dz);
// Volume rolloff (inverse distance model)
let vol = 0;
if (dist < MAX_AUDIBLE_DIST) {
vol = BASE_VOLUME / (1 + ROLLOFF * (dist - REF_DIST));
vol = Math.max(0, Math.min(BASE_VOLUME, vol));
}
src.gain.gain.setTargetAtTime(vol, _ctx.currentTime, 0.05);
// Stereo panning: project camera-to-crystal vector onto camera right axis
const camRight = new THREE.Vector3();
_camera.getWorldDirection(camRight);
camRight.cross(_camera.up).normalize();
const toCrystal = new THREE.Vector3(dx, 0, dz).normalize();
const pan = THREE.MathUtils.clamp(toCrystal.dot(camRight), -1, 1);
src.panner.pan.setTargetAtTime(pan, _ctx.currentTime, 0.05);
});
}
function _removeSource(id) {
const src = _sources[id];
if (!src) return;
try {
src.osc.stop();
src.osc.disconnect();
src.gain.disconnect();
src.panner.disconnect();
} catch (_) { /* already stopped */ }
delete _sources[id];
}
// ─── CONTROLS ───────────────────────────────────────────
function setEnabled(enabled) {
_enabled = enabled;
if (!_enabled) {
// Silence all sources
Object.values(_sources).forEach(src => {
src.gain.gain.setTargetAtTime(0, _ctx.currentTime, 0.05);
});
}
console.info('[SpatialAudio]', enabled ? 'Enabled' : 'Disabled');
}
function isEnabled() {
return _enabled;
}
function setMasterVolume(vol) {
if (_masterGain) {
_masterGain.gain.setTargetAtTime(
THREE.MathUtils.clamp(vol, 0, 1),
_ctx.currentTime,
0.05
);
}
}
function getActiveSourceCount() {
return Object.keys(_sources).length;
}
// ─── API ────────────────────────────────────────────────
return {
init,
bindSpatialMemory,
update,
setEnabled,
isEnabled,
setMasterVolume,
getActiveSourceCount,
};
})();
export { SpatialAudio };