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
This commit is contained in:
91
tests/constellation.test.js
Normal file
91
tests/constellation.test.js
Normal file
@@ -0,0 +1,91 @@
|
||||
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);
|
||||
});
|
||||
Reference in New Issue
Block a user