Refine the music: fades, and a mute control on every public screen
CI / verify (push) Successful in 3m43s
CI / publish (push) Has been skipped

THE BUG. An anonymous visitor to / — the sign-in page, which is what the link
in an email opens — got music with ZERO mute controls. The provider moved above
the router so it covers the signed-out screens, but those render outside Shell
and therefore have no app header to host the toggle. Audio a visitor cannot
switch off is the worst version of this feature. Every unauthenticated screen
now carries the control pinned bottom-right; Learn keeps the one in its own
chrome rather than getting a second.

FADES. Volume ramps 0 -> 0.14 over 1.1s on start and back down over 0.42s on
mute, easeOutQuad so a mute feels prompt while a start feels like the room was
already there. Snapping to full volume on the first click reads as a glitch.
Measured: 0.076 at +0.4s, 0.14 at +2.2s, 0.025 at +0.25s after mute, paused by
+0.85s.

NO CHANGE TO THE TRACKS, and this reverses what I said earlier. I claimed
pig-tech would splice audibly every 28 seconds and offered to crossfade the
loop. That came from comparing 0.4-second mean levels, which measures musical
content rather than continuity. Measured properly — the wrap discontinuity
against each track's own 99.9th-percentile sample delta — all three already
loop cleanly (0.03-0.10x, i.e. quieter than their own ordinary transients), and
a folded crossfade made them WORSE (0.09-1.32x). The originals ship unchanged.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-08-13 19:59:45 -07:00
parent afce0dda28
commit 30171767f7
2 changed files with 87 additions and 16 deletions
+43 -12
View File
@@ -11,6 +11,7 @@ import { IdentityProvider, useIdentityQuery } from '@/lib/identity';
import { LayoutProvider } from '@/lib/layout';
import { PiggyContextProvider } from '@/lib/piggy-context';
import { PlatformAudioProvider } from '@/lib/audio';
import { AudioControl } from '@/components/AudioControl';
import { Shell } from '@/components/Shell';
import { SignIn } from '@/pages/SignIn';
import { CreateProfile } from '@/pages/CreateProfile';
@@ -106,6 +107,28 @@ export function App() {
);
}
/**
* The signed-out screens render outside Shell, so they get no app header — and
* therefore no way to stop the music. Audio a visitor cannot switch off is the
* worst version of this feature, so every unauthenticated screen gets the
* control pinned in a corner.
*
* Learn is not wrapped: it brings its own chrome and already hosts one.
*/
function SignedOut({ children }: { children: React.ReactNode }) {
return (
<>
{children}
<div
className="fixed right-3 z-50 rounded-full border border-border bg-surface/85 shadow-sm backdrop-blur"
style={{ bottom: 'calc(0.75rem + var(--safe-bottom))' }}
>
<AudioControl />
</div>
</>
);
}
/**
* Decides what to show based on *why* a request failed.
*
@@ -165,24 +188,32 @@ function AuthGate({ config }: { config: PublicConfig }) {
</RoutePage>
);
}
return showRegister ? (
<Register
config={config}
onBack={() => setShowRegister(false)}
onRegistered={() => {
setShowRegister(false);
void refetch();
}}
/>
) : (
<SignIn config={config} onCreateAccount={() => setShowRegister(true)} />
return (
<SignedOut>
{showRegister ? (
<Register
config={config}
onBack={() => setShowRegister(false)}
onRegistered={() => {
setShowRegister(false);
void refetch();
}}
/>
) : (
<SignIn config={config} onCreateAccount={() => setShowRegister(true)} />
)}
</SignedOut>
);
}
// Authenticated but not a member. This is a step in the flow, not an
// error — sending them back to a login screen they have already completed
// would be a loop with no exit.
if (error.needsProfile) {
return <CreateProfile config={config} onCreated={() => void refetch()} />;
return (
<SignedOut>
<CreateProfile config={config} onCreated={() => void refetch()} />
</SignedOut>
);
}
}