diff --git a/client/src/components/FourZero/NoFearShow.tsx b/client/src/components/FourZero/NoFearShow.tsx
index 6bf5605b8..1e1c029e5 100644
--- a/client/src/components/FourZero/NoFearShow.tsx
+++ b/client/src/components/FourZero/NoFearShow.tsx
@@ -18,7 +18,9 @@ interface Cue {
act?: NoFearAct
line?: LineKey | null
hard?: boolean
+ soft?: boolean
impact?: number
+ swell?: boolean
}
// The script. Times in seconds; the scene ramps below share the same clock.
@@ -28,7 +30,7 @@ const CUES: Cue[] = [
{ at: 12.0, act: 'dread', line: 'fearTool' },
{ at: 18.5, line: 'hateTrade' },
{ at: 26.0, act: 'silence', line: null },
- { at: 28.5, line: 'butYouTraveled', impact: 0.5 },
+ { at: 28.5, line: 'butYouTraveled', soft: true, swell: true },
{ at: 34.0, act: 'hope', line: 'tables' },
{ at: 42.0, line: 'face' },
{ at: 49.0, line: 'fences' },
@@ -188,6 +190,7 @@ export default function NoFearShow({ onClose }: { onClose: () => void }) {
const cue = CUES[idx]
if (cue.act) audioRef.current?.setAct(cue.act)
if (cue.impact) audioRef.current?.impact(cue.impact)
+ if (cue.swell) audioRef.current?.swell()
setCueIdx(idx)
if (t >= ANTHEM_AT) setAnthem(true)
if (t >= CREDITS_AT) setCredits(true)
@@ -242,7 +245,7 @@ export default function NoFearShow({ onClose }: { onClose: () => void }) {
{/* The lines. */}
{!anthem && lineText && flashIdx < 0 && (
-
{lineText}
+
{lineText}
)}
diff --git a/client/src/components/FourZero/fourzero.css b/client/src/components/FourZero/fourzero.css
index 03fd0e2d2..1fd617295 100644
--- a/client/src/components/FourZero/fourzero.css
+++ b/client/src/components/FourZero/fourzero.css
@@ -66,6 +66,11 @@
letter-spacing: 0.01em;
}
+/* The pivot line out of the silence breathes in slowly instead of arriving. */
+.fz-line-soft {
+ animation-duration: 3.4s;
+}
+
@keyframes fzLineIn {
from { opacity: 0; transform: translateY(14px); filter: blur(6px); }
to { opacity: 1; transform: translateY(0); filter: blur(0); }
diff --git a/client/src/components/FourZero/noFearAudio.ts b/client/src/components/FourZero/noFearAudio.ts
index 906b34cb2..b6f7bf463 100644
--- a/client/src/components/FourZero/noFearAudio.ts
+++ b/client/src/components/FourZero/noFearAudio.ts
@@ -125,6 +125,22 @@ export class NoFearAudio {
else void this.ctx.resume().catch(() => {})
}
+ /** A soft warm swell out of the silence — the first calm breath, not a hit. */
+ swell(): void {
+ if (!this.ctx || !this.master) return
+ const t = this.ctx.currentTime + 0.05
+ const o = this.ctx.createOscillator()
+ o.type = 'sine'
+ o.frequency.value = 55 // A1 — the root the hope act will grow from
+ const g = this.ctx.createGain()
+ g.gain.setValueAtTime(0.0001, t)
+ g.gain.exponentialRampToValueAtTime(0.22, t + 1.6)
+ g.gain.exponentialRampToValueAtTime(0.0001, t + 5)
+ o.connect(g)
+ this.out(g, 0.6)
+ o.start(t); o.stop(t + 5.2)
+ }
+
/** A cue-driven cinema hit: sub boom + filtered noise burst, tail blooming in the hall. */
impact(strength = 1): void {
if (!this.ctx || !this.master) return
@@ -161,7 +177,9 @@ export class NoFearAudio {
if (!this.ctx || !this.master || this.act === act) return
this.act = act
const t = this.ctx.currentTime
- this.releaseVoices(act === 'silence' ? 0.1 : 1.6)
+ // Into the silence: a fast pull-away rather than a hard kill — the fear's
+ // hall tail is left ringing out on its own, like a room falling quiet.
+ this.releaseVoices(act === 'silence' ? 0.6 : 1.6)
this.stopChords()
switch (act) {
case 'fear':
@@ -202,6 +220,9 @@ export class NoFearAudio {
this.heartbeatTimer = null
this.stopChords()
this.releaseVoices(0.1)
+ // Pull the master down fast BEFORE the deferred close — closing mid-release
+ // (voices still ~30% hot plus the hall tail) would pop.
+ if (this.master && this.ctx) this.master.gain.setTargetAtTime(0, this.ctx.currentTime, 0.05)
const ctx = this.ctx
this.ctx = null
this.master = null
@@ -303,13 +324,18 @@ export class NoFearAudio {
const g = this.ctx.createGain()
g.gain.setValueAtTime(0.0001, t)
g.gain.exponentialRampToValueAtTime(level, t + 4)
+ // The swell LFO modulates a SERIES tremolo stage, not g itself — an input
+ // on g.gain would ADD to the release automation and the wind could never
+ // be silenced for the hard cut into the silence act.
+ const trem = this.ctx.createGain()
+ trem.gain.value = 0.75
const lfo = this.ctx.createOscillator()
lfo.frequency.value = 0.07
const lfoGain = this.ctx.createGain()
- lfoGain.gain.value = level * 0.55
- lfo.connect(lfoGain); lfoGain.connect(g.gain)
+ lfoGain.gain.value = 0.4
+ lfo.connect(lfoGain); lfoGain.connect(trem.gain)
lfo.start(t)
- src.connect(bp); bp.connect(g)
+ src.connect(bp); bp.connect(trem); trem.connect(g)
this.out(g, 0.55)
src.start(t)
this.hold(g, [src, lfo])
@@ -490,15 +516,15 @@ export class NoFearAudio {
const n = this.ctx.createBufferSource()
n.buffer = this.noiseBuf
const hp = this.ctx.createBiquadFilter()
- hp.type = 'lowpass'
- hp.frequency.value = 340
+ hp.type = 'highpass' // the CLICK of the valve — the sine above is the body
+ hp.frequency.value = 1700
const ng = this.ctx.createGain()
ng.gain.setValueAtTime(0.0001, at)
- ng.gain.exponentialRampToValueAtTime(level * 0.5, at + 0.006)
- ng.gain.exponentialRampToValueAtTime(0.0001, at + 0.05)
+ ng.gain.exponentialRampToValueAtTime(level * 0.3, at + 0.005)
+ ng.gain.exponentialRampToValueAtTime(0.0001, at + 0.04)
n.connect(hp); hp.connect(ng); ng.connect(this.master)
n.start(at)
- n.stop(at + 0.08)
+ n.stop(at + 0.07)
}
}
}