Repository navigation
Expand file tree
/
Copy pathaudioFxTail.test.ts
More file actions
103 lines (95 loc) · 3.77 KB
/
Copy pathaudioFxTail.test.ts
File metadata and controls
103 lines (95 loc) · 3.77 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
import { describe, expect, it } from "vitest";
import { chainTailSeconds, MAX_FX_TAIL_SECONDS } from "./audioFxTail.js";
import { synthesizeReverbImpulse } from "./audioFxGraph.js";
import type { HfAudioFxChain } from "../audioFx.js";
import type { HfAutomation } from "../audioAutomation.js";
const chain = (nodes: HfAudioFxChain["nodes"]): HfAudioFxChain => ({ version: 1, nodes });
describe("chainTailSeconds", () => {
it("is zero for a chain that settles with its input", () => {
expect(
chainTailSeconds(
chain([
{ type: "peaking", id: "n1", params: { frequency: 900, gain: -6, q: 1 } },
{ type: "compressor", id: "n2", params: { threshold: -18, ratio: 4, release: 9000 } },
// A 9-second release still holds no tail: with no input there is no
// signal to release, so the output is silence either way.
{ type: "phaser", id: "n3", params: { delay: 3, decay: 0.4, speed: 0.5 } },
]),
),
).toBe(0);
});
it("matches the reverb impulse it has to make room for", () => {
// The whole point: too short and the render cuts the tail the impulse
// generates. Measured against the generator rather than restating 0.6+2.6.
for (const size of [0.05, 0.4, 0.7, 1]) {
const impulse = synthesizeReverbImpulse(48000, size, 0.5);
const tail = chainTailSeconds(
chain([{ type: "reverb", id: "r", params: { size, damping: 0.5, wet: 0.35, dry: 0.7 } }]),
);
expect(tail).toBeCloseTo(impulse.length / 48000, 4);
}
});
it("counts a delay's repeats down to -60 dB", () => {
// 250 ms at 0.35 feedback: 0.35^7 = 6.4e-4, the first repeat under the floor.
expect(
chainTailSeconds(
chain([{ type: "delay", id: "d", params: { time: 250, feedback: 0.35, mix: 0.5 } }]),
),
).toBeCloseTo(1.75, 5);
});
it("sums a serial chain instead of taking the longest", () => {
// The delay hands each repeat to the room, so the last repeat still gets a
// full tail — taking the max would cut the end of it.
const both = chainTailSeconds(
chain([
{ type: "delay", id: "d", params: { time: 250, feedback: 0.35, mix: 0.5 } },
{ type: "reverb", id: "r", params: { size: 0.3, damping: 0.5, wet: 0.35, dry: 0.7 } },
]),
);
expect(both).toBeCloseTo(1.75 + (0.6 + 0.3 * 2.6), 5);
});
it("caps a tail the panel can dial but nobody wants to render", () => {
// 5 s between repeats at 0.95 feedback decays for eleven minutes.
expect(
chainTailSeconds(
chain([{ type: "delay", id: "d", params: { time: 5000, feedback: 0.95, mix: 1 } }]),
),
).toBe(MAX_FX_TAIL_SECONDS);
});
it("ignores an effect that is bypassed or mixed out", () => {
const bypassed = chain([
{
type: "reverb",
id: "r",
enabled: false,
params: { size: 1, damping: 0.5, wet: 1, dry: 0 },
},
]);
expect(chainTailSeconds(bypassed)).toBe(0);
const silent = chain([
{ type: "reverb", id: "r", params: { size: 1, damping: 0.5, wet: 0, dry: 1 } },
]);
expect(chainTailSeconds(silent)).toBe(0);
});
it("sizes the room for the loudest moment a lane reaches", () => {
// Static wet is 0 — read alone it would say "no tail" and cut the swell the
// lane brings in halfway through the clip.
const automation: HfAutomation = {
version: 1,
lanes: [
{
target: "fx.r.wet",
points: [
{ t: 0, v: 0 },
{ t: 2, v: 0.8 },
],
},
],
};
const withLane = chain([
{ type: "reverb", id: "r", params: { size: 0.5, damping: 0.5, wet: 0, dry: 1 } },
]);
expect(chainTailSeconds(withLane)).toBe(0);
expect(chainTailSeconds(withLane, automation)).toBeCloseTo(0.6 + 0.5 * 2.6, 5);
});
});