|
| 1 | +/** |
| 2 | + * How long a chain keeps ringing after its input stops. |
| 3 | + * |
| 4 | + * The render used to end the offline context at the last input sample, so a |
| 5 | + * reverb or a delay was cut mid-tail — the one place the render did not match |
| 6 | + * preview. The length is not a guess: every tail-producing effect here has a |
| 7 | + * decay that follows from its own settings, so the render can ask for exactly |
| 8 | + * the room it needs. |
| 9 | + */ |
| 10 | + |
| 11 | +import { fxAutomationTarget, type HfAutomation } from "../audioAutomation.js"; |
| 12 | +import { normalizeAudioFxParams, type HfAudioFxChain, type HfAudioFxNode } from "../audioFx.js"; |
| 13 | + |
| 14 | +/** |
| 15 | + * Ceiling on the extension, in seconds. |
| 16 | + * |
| 17 | + * Delay is unbounded in principle: 5 s between repeats at 0.95 feedback decays |
| 18 | + * for eleven minutes, and the panel can dial exactly that. A tail that outruns |
| 19 | + * the composition costs render time and mixes into everything after it, so the |
| 20 | + * chain gets the room it asks for up to here and is cut beyond it. |
| 21 | + */ |
| 22 | +export const MAX_FX_TAIL_SECONDS = 5; |
| 23 | + |
| 24 | +/** |
| 25 | + * Where a tail stops counting as audible: -60 dB below the signal that fed it, |
| 26 | + * the usual convention for a reverb time. Anything quieter is under the noise |
| 27 | + * floor of every codec this renders to. |
| 28 | + */ |
| 29 | +const TAIL_FLOOR = 0.001; |
| 30 | + |
| 31 | +/** |
| 32 | + * The largest value a knob reaches, over the whole clip. |
| 33 | + * |
| 34 | + * A lane's `curve` is an exponent, so a segment is monotone between its two |
| 35 | + * points and cannot overshoot either — the maximum point value is the maximum |
| 36 | + * of the lane, no sampling needed. Room size has to be sized for the loudest |
| 37 | + * moment regardless of where in the clip it falls. |
| 38 | + */ |
| 39 | +function knobMax(node: HfAudioFxNode, key: string, automation?: HfAutomation): number { |
| 40 | + // Normalised, so a knob missing from the attribute reads as its default and |
| 41 | + // an out-of-range one is clamped the way the graph builder would clamp it. |
| 42 | + const fallback = Number(normalizeAudioFxParams(node.type, node.params)[key] ?? 0); |
| 43 | + if (!node.id || !automation) return Number.isFinite(fallback) ? fallback : 0; |
| 44 | + const lane = automation.lanes.find((l) => l.target === fxAutomationTarget(node.id ?? "", key)); |
| 45 | + if (!lane || lane.points.length === 0) return Number.isFinite(fallback) ? fallback : 0; |
| 46 | + return lane.points.reduce((max, p) => Math.max(max, p.v), -Infinity); |
| 47 | +} |
| 48 | + |
| 49 | +/** |
| 50 | + * Repeats until a feedback loop falls under the floor, times the gap between |
| 51 | + * them. `feedback` is capped below 1 by the registry, so this terminates. |
| 52 | + */ |
| 53 | +function delayTail(time: number, feedback: number): number { |
| 54 | + const gap = Math.min(5, time / 1000); |
| 55 | + if (gap <= 0) return 0; |
| 56 | + const fb = Math.max(0, Math.min(0.999, feedback)); |
| 57 | + if (fb <= 0) return gap; |
| 58 | + return Math.ceil(Math.log(TAIL_FLOOR) / Math.log(fb)) * gap; |
| 59 | +} |
| 60 | + |
| 61 | +/** One node's tail. Zero when it has none, or when it is mixed out entirely. */ |
| 62 | +function nodeTail(node: HfAudioFxNode, automation?: HfAutomation): number { |
| 63 | + if (node.enabled === false) return 0; |
| 64 | + switch (node.type) { |
| 65 | + case "reverb": |
| 66 | + // Exactly the generated impulse's length — see synthesizeReverbImpulse, |
| 67 | + // which is the same expression. A convolution is as long as its impulse. |
| 68 | + return knobMax(node, "wet", automation) > 0 |
| 69 | + ? 0.6 + Math.max(0, Math.min(1, knobMax(node, "size", automation))) * 2.6 |
| 70 | + : 0; |
| 71 | + case "delay": |
| 72 | + return knobMax(node, "mix", automation) > 0 |
| 73 | + ? delayTail(knobMax(node, "time", automation), knobMax(node, "feedback", automation)) |
| 74 | + : 0; |
| 75 | + case "chorus": |
| 76 | + // A single delay line, no feedback: it rings for one delay (≤100 ms). |
| 77 | + return knobMax(node, "mix", automation) > 0 ? knobMax(node, "delay", automation) / 1000 : 0; |
| 78 | + default: |
| 79 | + // Everything else settles with its input. The phaser is an all-pass chain |
| 80 | + // with no recirculation (group delay, not a tail); the dynamics nodes have |
| 81 | + // long releases but no signal to release — silence in, silence out; a |
| 82 | + // biquad rings for ~Q/f, which is microseconds. |
| 83 | + return 0; |
| 84 | + } |
| 85 | +} |
| 86 | + |
| 87 | +/** |
| 88 | + * The whole chain's tail, in seconds. |
| 89 | + * |
| 90 | + * Summed, not maxed: the chain is serial, so a delay in front of a reverb hands |
| 91 | + * each of its repeats to the room and the last one still gets a full tail. |
| 92 | + */ |
| 93 | +export function chainTailSeconds(chain: HfAudioFxChain, automation?: HfAutomation): number { |
| 94 | + const total = chain.nodes.reduce((sum, node) => sum + nodeTail(node, automation), 0); |
| 95 | + return Math.min(MAX_FX_TAIL_SECONDS, total); |
| 96 | +} |
0 commit comments