/ proof-of-concept / cymbalsim
Sign in
proof-of-concept / cymbalsim
cymbalsim / tests / felt.test.ts
65 lines · 3.2 KBBlameHistoryRaw
1// The viscoelastic felt: its chain of Maxwell arms has the loss factor it
2// was built for across the audio band, the storage stiffness it was
3// normalized to, and an exact time step.
4import { test } from "node:test";
5import assert from "node:assert/strict";
6import { feltChain, feltModulus, feltStepConstants, FELT_ARMS } from "../src/felt.ts";
8test("the loss factor of the chain is flat across the band and the storage is one at the reference", () => {
9 for (const eta of [0.05, 0.3, 0.6]) {
10 for (const fRef of [15, 96, 400]) {
11 const ch = feltChain(eta, fRef);
12 assert.equal(ch.tau.length, FELT_ARMS);
13 const ref = feltModulus(ch, 2 * Math.PI * fRef);
14 assert.ok(Math.abs(ref.storage - 1) < 1e-12);
15 assert.ok(Math.abs(ref.loss / ref.storage / eta - 1) < 0.02, `eta ${eta} at the reference ${fRef} Hz: ${ref.loss / ref.storage}`);
16 let worst = 0, prev = 0;
17 for (let f = 5; f <= 10000; f *= 1.1) {
18 const m = feltModulus(ch, 2 * Math.PI * f);
19 worst = Math.max(worst, Math.abs(m.loss / m.storage / eta - 1));
20 assert.ok(m.storage > prev, "the storage stiffness rises with frequency");
21 prev = m.storage;
22 }
23 const tol = eta > 0.5 ? 0.1 : 0.06;
24 assert.ok(worst < tol, `eta ${eta}, reference ${fRef} Hz: loss factor off by up to ${(100 * worst).toFixed(1)}% between 5 Hz and 10 kHz`);
25 // Relaxed and glassy stiffness bracket the reference.
26 const glassy = ch.kInf + ch.k.reduce((a, b) => a + b, 0);
27 assert.ok(ch.kInf < 1 && glassy > 1);
28 }
29 }
30 const none = feltChain(0, 100);
31 assert.equal(none.tau.length, 0);
32 assert.equal(none.kInf, 1);
33 assert.deepEqual(feltModulus(none, 1000), { storage: 1, loss: 0 });
34});
36test("the exact step of an arm reproduces its response to a ramp and to a sinusoid", () => {
37 const ch = feltChain(0.3, 50);
38 const a = 6; // an arm in the middle of the band
39 const tau = ch.tau[a];
40 // A ramp w = s t from a relaxed arm: p(t) = s tau (1 - exp(-t / tau)), whatever the step.
41 for (const dt of [tau / 50, tau / 3, 2 * tau]) {
42 const { decay, growth } = feltStepConstants(ch, dt);
43 const s = 1e-3;
44 let p = 0;
45 const steps = 20;
46 for (let i = 0; i < steps; i++) p = p * decay[a] + s * dt * growth[a];
47 const exact = s * tau * (1 - Math.exp(-(steps * dt) / tau));
48 assert.ok(Math.abs(p / exact - 1) < 1e-12, `dt = ${dt / tau} tau: ${p} vs ${exact}`);
49 }
50 // A sinusoid w = A sin(omega t), stepped finely: the work per cycle against
51 // the arm's force -k p is pi k A^2 omega tau / (1 + omega^2 tau^2).
52 const omega = 1 / tau, A = 1e-4, steps = 2000;
53 const dt = (2 * Math.PI) / omega / steps;
54 const { decay, growth } = feltStepConstants(ch, dt);
55 let p = 0, work = 0;
56 for (let i = 0; i < 6 * steps; i++) {
57 const w0 = A * Math.sin(omega * i * dt), w1 = A * Math.sin(omega * (i + 1) * dt);
58 const f0 = ch.k[a] * p;
59 p = p * decay[a] + (w1 - w0) * growth[a];
60 const f1 = ch.k[a] * p;
61 if (i >= 5 * steps) work += 0.5 * (f0 + f1) * (w1 - w0);
62 }
63 const expected = (Math.PI * ch.k[a] * A * A * (omega * tau)) / (1 + omega * omega * tau * tau);
64 assert.ok(Math.abs(work / expected - 1) < 1e-3, `work per cycle ${work} vs ${expected}`);
65});
↑↓ move↵openescclose