Files
sonora/tests/constellation.test.js
marcuspaico ab748bfb9d Sonora: local 3D audio visualizer (initial import)
Three.js + Web Audio FFT visualizer with Constellation, Texture, Frequency
and Modulation views. Vite build, node --test suite.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JABXBRLs9T6nqHKUVwoTHJ
2026-09-08 15:57:51 +10:00

92 lines
5.1 KiB
JavaScript

import test from 'node:test';
import assert from 'node:assert/strict';
import { makeNodes, connectNodes, ConstellationModel } from '../src/constellation-data.js';
const frame = (time, frequency = 1500) => ({time, rms: 0.2, entropy: 0.4, spread: 900, flatness: 0.03, peaks: [{frequency, amplitude: 0.2}]});
const settings = {trail:12, sensitivity:1};
test('constellation uses measured peak data and excludes expired or silent frames', () => {
const nodes = makeNodes([frame(-20), {...frame(0), rms:0}, frame(1)], 2, settings);
assert.equal(nodes.length, 1);
assert.equal(nodes[0].frequency, 1500); assert.equal(nodes[0].amplitude, 0.2); assert.equal(nodes[0].time, 1);
});
test('node positions are stable as they age and respond to changed frequency', () => {
const node = makeNodes([frame(1)], 2, settings)[0];
assert.deepEqual(node.position, makeNodes([frame(1)], 3, settings)[0].position);
assert.notDeepEqual(node.position, makeNodes([frame(1, 4000)], 2, settings)[0].position);
});
test('dense tracks have a bounded point count and finite positions', () => {
const frames = Array.from({length:3000}, (_, i) => ({...frame(i / 100), peaks:Array.from({length:12}, (_, j) => ({frequency:200+j*400, amplitude:0.1}))}));
const nodes = makeNodes(frames, 30, {...settings, trail:30});
assert.ok(nodes.length <= 2400); assert.ok(nodes.length > 100);
for (const node of nodes) assert.ok(node.position.every(Number.isFinite));
});
test('network connections are local, bounded and never self-connect', () => {
const nodes = [0, 1, 2, 50].map(x => ({position:[x,0,0]}));
const links = connectNodes(nodes);
assert.ok(links.length > 0);
for (const [a,b] of links) { assert.notEqual(a,b); assert.ok(Math.abs(nodes[a].position[0]-nodes[b].position[0]) < 3); }
assert.ok(links.length <= nodes.length * 3);
});
test('rolling history retains the identity, position and parents of surviving points', () => {
const model = new ConstellationModel();
const history = Array.from({length:60}, (_,i) => frame(i/30,1500+i*2));
const first = model.update(history,2,{...settings,trail:3});
const survivor = first.nodes.find(n=>n.time>1);
const saved = {id:survivor.id,position:[...survivor.position],parents:[...survivor.parents]};
history.splice(0,20); history.push(frame(2.1,1630));
const next = model.update(history,2.1,{...settings,trail:3});
const same = next.nodes.find(n=>n.id===saved.id);
assert.deepEqual(same.position,saved.position); assert.deepEqual(same.parents,saved.parents);
});
test('frequency tracks survive peak loudness reordering and create continuous links', () => {
const model = new ConstellationModel(), history=[];
history.push({...frame(0),peaks:[{frequency:1000,amplitude:.3},{frequency:2000,amplitude:.2}]});
const first = model.update(history,0,settings);
const tone = first.nodes.find(n=>n.frequency===1000);
history.push({...frame(.05),peaks:[{frequency:2000,amplitude:.4},{frequency:1005,amplitude:.1}]});
const next = model.update(history,.05,settings);
const continuation = next.nodes.find(n=>n.frequency===1005);
assert.equal(continuation.trackId,tone.trackId);
assert.ok(continuation.parents.includes(tone.id));
});
test('unchanged audio does not rebuild geometry or connections', () => {
const model = new ConstellationModel(), history=[frame(1)];
const first=model.update(history,1,settings);
const next=model.update(history,1.01,settings);
assert.equal(next.revision,first.revision);
assert.equal(next.nodes,first.nodes); assert.equal(next.links,first.links);
});
test('a new source resets tracks and never links back to the previous song', () => {
const model=new ConstellationModel(); model.update([frame(1)],1,settings);
const next=model.update([frame(2)],2,settings);
assert.equal(next.nodes.length,1); assert.deepEqual(next.nodes[0].parents,[]);
});
test('new samples are smoothly connected through sudden amplitude changes', () => {
const model=new ConstellationModel(), history=[{...frame(0),peaks:[{frequency:1500,amplitude:.01}]}];
const start=model.update(history,0,settings).nodes[0];
history.push({...frame(.05),peaks:[{frequency:1500,amplitude:.8}]});
const next=model.update(history,.05,settings).nodes.at(-1);
const raw=makeNodes([history[1]],.05,settings)[0];
const distance = (a,b) => Math.hypot(...a.position.map((v,i)=>v-b.position[i]));
assert.ok(distance(start,next)<distance(start,raw)*.5);
});
test('rapid pitch changes still connect consecutive sound samples', () => {
const model=new ConstellationModel(), history=[frame(0,700)];
const first=model.update(history,0,settings).nodes[0];
history.push(frame(.1,6000));
const next=model.update(history,.1,settings);
assert.ok(next.nodes.at(-1).parents.includes(first.id));
assert.ok(next.links.some(([, , trail])=>trail));
});
test('sound paths break across silence and all points and links expire', () => {
const model=new ConstellationModel(), history=[frame(0,700),frame(1,6000)];
const result=model.update(history,1,settings);
assert.equal(result.nodes.at(-1).trailParent,null);
const expired=model.update(history,14,settings);
assert.equal(expired.nodes.length,0);
assert.equal(expired.links.length,0);
});