feat(static): client-side alteration engine (port of player.alteration/audio)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,52 @@
|
||||
// Client-side port of player/alteration.py + player/audio.py (the alteration
|
||||
// engine). Replaces the live POST /api/alteration in the static build — pure math,
|
||||
// unity calibration (the client never sends a calibration, so DEFAULT_CALIBRATION
|
||||
// = all gains 1.0 always applies). UMD so the browser gets `HEFAlteration` and
|
||||
// `node --test` can require() it. Keep IN SYNC with the Python — the numeric
|
||||
// contract is guarded by tests/test_alteration_js_parity.py.
|
||||
(function (root, factory) {
|
||||
const api = factory();
|
||||
if (typeof module !== "undefined" && module.exports) module.exports = api;
|
||||
else root.HEFAlteration = api;
|
||||
})(typeof self !== "undefined" ? self : this, function () {
|
||||
"use strict";
|
||||
const KNOB_MAX = 4; // player/alteration.py:25
|
||||
const clamp = (x, lo, hi) => Math.max(lo, Math.min(hi, x));
|
||||
|
||||
// Mirror of player/alteration.py::plan_alteration with DEFAULT_CALIBRATION (unity).
|
||||
function plan(c) {
|
||||
const tone = clamp((c.light - c.dark) / KNOB_MAX, -1, 1);
|
||||
const overlayIntensity = clamp(c.left / KNOB_MAX, 0, 1);
|
||||
const dreamIntensity = clamp(c.right / KNOB_MAX, 0, 1);
|
||||
const affectIntensity = clamp(c.right / KNOB_MAX, 0, 1); // affect uses overlay_gain too
|
||||
const is_identity = tone === 0 && c.left === 0 && c.right === 0;
|
||||
return {
|
||||
grade: { tone },
|
||||
overlay: { level: c.left, intensity: overlayIntensity },
|
||||
affect: { strength: c.right, intensity: affectIntensity },
|
||||
dream: { strength: c.right, intensity: dreamIntensity },
|
||||
is_identity,
|
||||
};
|
||||
}
|
||||
|
||||
// Mirror of player/audio.py::resolve_audio. `scaleAudio` is the current scale's
|
||||
// `audio` filename (relative to <mediaBase>audio/); off → silence.
|
||||
function renderAudio(source, scaleAudio, mediaBase) {
|
||||
if (source === "off") return { source: "off", url: null, altitude_coupled: false };
|
||||
const url = scaleAudio ? mediaBase + "audio/" + scaleAudio : null;
|
||||
return { source: "soundtrack", url, altitude_coupled: true };
|
||||
}
|
||||
|
||||
// The full /api/alteration response shape, computed locally.
|
||||
function alteration(controls, scaleAudio, mediaBase) {
|
||||
return {
|
||||
plan: plan(controls),
|
||||
render: {
|
||||
video: { shown: controls.visual === "on" }, // player/audio.py::resolve_visual
|
||||
audio: renderAudio(controls.audio, scaleAudio, mediaBase),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
return { plan, renderAudio, alteration, KNOB_MAX };
|
||||
});
|
||||
@@ -0,0 +1,51 @@
|
||||
"use strict";
|
||||
const test = require("node:test");
|
||||
const assert = require("node:assert/strict");
|
||||
const A = require("../static/alteration.js");
|
||||
|
||||
const c = (o) => ({ visual: "on", audio: "off", left: 0, right: 0, dark: 0, light: 0, ...o });
|
||||
|
||||
test("identity at zero knobs", () => {
|
||||
const p = A.plan(c({}));
|
||||
assert.equal(p.grade.tone, 0);
|
||||
assert.equal(p.overlay.level, 0);
|
||||
assert.equal(p.dream.strength, 0);
|
||||
assert.equal(p.is_identity, true);
|
||||
});
|
||||
|
||||
test("mood tone is signed (light - dark)/KNOB_MAX, clamped", () => {
|
||||
assert.equal(A.plan(c({ light: 4 })).grade.tone, 1); // full light
|
||||
assert.equal(A.plan(c({ dark: 4 })).grade.tone, -1); // full dark
|
||||
assert.equal(A.plan(c({ light: 2 })).grade.tone, 0.5);
|
||||
});
|
||||
|
||||
test("left drives overlay level + intensity; right drives dream + affect", () => {
|
||||
const p = A.plan(c({ left: 2, right: 4 }));
|
||||
assert.equal(p.overlay.level, 2);
|
||||
assert.equal(p.overlay.intensity, 0.5);
|
||||
assert.equal(p.dream.strength, 4);
|
||||
assert.equal(p.dream.intensity, 1);
|
||||
assert.equal(p.affect.strength, 4);
|
||||
assert.equal(p.affect.intensity, 1);
|
||||
assert.equal(p.is_identity, false);
|
||||
});
|
||||
|
||||
test("renderAudio: off → null, no coupling", () => {
|
||||
assert.deepEqual(A.renderAudio("off", "cosmos.mp3", "https://x/"),
|
||||
{ source: "off", url: null, altitude_coupled: false });
|
||||
});
|
||||
|
||||
test("renderAudio: soundtrack → mediaBase audio url, coupled", () => {
|
||||
assert.deepEqual(A.renderAudio("soundtrack", "cosmos.mp3", "https://x/"),
|
||||
{ source: "soundtrack", url: "https://x/audio/cosmos.mp3", altitude_coupled: true });
|
||||
// no scale audio → null url but still coupled
|
||||
assert.deepEqual(A.renderAudio("soundtrack", "", "https://x/"),
|
||||
{ source: "soundtrack", url: null, altitude_coupled: true });
|
||||
});
|
||||
|
||||
test("alteration() composes plan + render with video.shown from visual", () => {
|
||||
const r = A.alteration(c({ visual: "off", audio: "soundtrack", left: 1 }), "reef.mp3", "https://x/");
|
||||
assert.equal(r.render.video.shown, false);
|
||||
assert.equal(r.render.audio.url, "https://x/audio/reef.mp3");
|
||||
assert.equal(r.plan.overlay.level, 1);
|
||||
});
|
||||
@@ -0,0 +1,21 @@
|
||||
"""The static build reimplements plan_alteration/resolve_audio in JS
|
||||
(simulator/static/alteration.js). This pins the numeric contract so the Python
|
||||
and JS cannot silently diverge — if you change the engine, change both (the
|
||||
mirrored JS cases live in simulator/unit/alteration.test.js)."""
|
||||
from hef.selection import Coordinate
|
||||
from player.alteration import plan_alteration, render_plan_to_dict
|
||||
from player.audio import resolve_audio, resolve_visual
|
||||
|
||||
|
||||
def test_engine_contract_matches_js_expectations():
|
||||
# mirrors the cases asserted in simulator/unit/alteration.test.js
|
||||
p = render_plan_to_dict(plan_alteration(Coordinate(left=2, right=4, dark=0, light=0)))
|
||||
assert p["overlay"] == {"level": 2, "intensity": 0.5}
|
||||
assert p["dream"] == {"strength": 4, "intensity": 1.0}
|
||||
assert p["affect"] == {"strength": 4, "intensity": 1.0}
|
||||
assert render_plan_to_dict(plan_alteration(Coordinate(0, 0, 0, 4)))["grade"]["tone"] == 1.0
|
||||
assert render_plan_to_dict(plan_alteration(Coordinate(0, 0, 4, 0)))["grade"]["tone"] == -1.0
|
||||
assert render_plan_to_dict(plan_alteration(Coordinate(0, 0, 0, 0)))["is_identity"] is True
|
||||
assert resolve_visual("off") is False
|
||||
a = resolve_audio("soundtrack", scale_audio="cosmos.mp3")
|
||||
assert a.url == "/media/audio/cosmos.mp3" and a.altitude_coupled is True
|
||||
Reference in New Issue
Block a user