Overview
The background audio system provides a seamless spiritual atmosphere with auto-playing prayer music. It features volume fading, user consent management, and playback that persists across page navigation. The system complies with browser autoplay policies while maintaining a smooth user experience.
Audio Context Provider
The audio system is managed through a React Context that wraps the entire application:
contexts/audio-context.tsx
interface AudioContextType {
play : () => void
pause : () => void
fadeOut : ( duration ?: number ) => void
fadeToVolume : ( targetVolume : number , duration ?: number ) => void
toggle : () => void
isPlaying : boolean
isLoaded : boolean
hasUserConsent : boolean
grantConsent : () => void
}
const AudioContext = createContext < AudioContextType | null >( null )
export function AudioProvider ({ children } : { children : ReactNode }) {
const audioRef = useRef < HTMLAudioElement | null >( null )
const [ isPlaying , setIsPlaying ] = useState ( false )
const [ isLoaded , setIsLoaded ] = useState ( false )
const hasUserConsentRef = useRef ( false )
const [ hasUserConsent , setHasUserConsent ] = useState ( false )
const fadeIntervalRef = useRef < NodeJS . Timeout | null >( null )
const targetVolumeRef = useRef ( 1 )
const pathname = usePathname ()
useEffect (() => {
// Initialize audio element
audioRef . current = new Audio ( 'https://cdn.njrajatmahotsav.com/audio_files/prathna%2Banthem.mp3' )
audioRef . current . preload = 'auto'
// Start muted until explicit user interaction
audioRef . current . volume = 0
const audio = audioRef . current
const handleCanPlayThrough = () => setIsLoaded ( true )
const handleLoadedMetadata = () => setIsLoaded ( true )
const handleEnded = () => setIsPlaying ( false )
audio . addEventListener ( 'canplaythrough' , handleCanPlayThrough )
audio . addEventListener ( 'loadedmetadata' , handleLoadedMetadata )
audio . addEventListener ( 'ended' , handleEnded )
return () => {
if ( fadeIntervalRef . current ) clearInterval ( fadeIntervalRef . current )
audio . pause ()
audio . removeEventListener ( 'canplaythrough' , handleCanPlayThrough )
audio . removeEventListener ( 'loadedmetadata' , handleLoadedMetadata )
audio . removeEventListener ( 'ended' , handleEnded )
}
}, [])
// ... implementation details
}
export function useAudioContext () {
const context = useContext ( AudioContext )
if ( ! context ) throw new Error ( 'useAudioContext must be used within AudioProvider' )
return context
}
Audio File
The prayer audio is served from Cloudflare CDN:
const audioUrl = 'https://cdn.njrajatmahotsav.com/audio_files/prathna%2Banthem.mp3'
audioRef . current = new Audio ( audioUrl )
audioRef . current . preload = 'auto'
The audio file combines prayer (prathna) and anthem for a continuous spiritual experience throughout the event website.
User Consent Management
Browser autoplay policies require explicit user interaction before audio can play:
contexts/audio-context.tsx
const grantConsent = () => {
hasUserConsentRef . current = true
setHasUserConsent ( true )
}
const play = () => {
if ( audioRef . current && hasUserConsentRef . current ) {
audioRef . current . volume = targetVolumeRef . current
audioRef . current . play ()
setIsPlaying ( true )
}
}
const toggle = () => {
if ( isPlaying ) {
pause ()
} else {
grantConsent () // Grant consent on first interaction
if ( audioRef . current ) {
audioRef . current . volume = targetVolumeRef . current
audioRef . current . play ()
setIsPlaying ( true )
}
}
}
Fade Effects
The system includes smooth volume fading:
Fade Out
contexts/audio-context.tsx
const fadeOut = ( duration = 1000 ) => {
if ( ! audioRef . current || ! isPlaying || ! hasUserConsentRef . current ) return
if ( fadeIntervalRef . current ) clearInterval ( fadeIntervalRef . current )
const audio = audioRef . current
const startVolume = audio . volume
const steps = 50
const stepTime = duration / steps
const volumeStep = startVolume / steps
fadeIntervalRef . current = setInterval (() => {
if ( audio . volume > volumeStep ) {
audio . volume = Math . max ( 0 , audio . volume - volumeStep )
} else {
audio . volume = 0
audio . pause ()
setIsPlaying ( false )
if ( fadeIntervalRef . current ) clearInterval ( fadeIntervalRef . current )
}
}, stepTime )
}
Fade to Volume
contexts/audio-context.tsx
const fadeToVolume = ( targetVolume : number , duration = 1000 ) => {
if ( ! audioRef . current || ! hasUserConsentRef . current ) return
if ( fadeIntervalRef . current ) clearInterval ( fadeIntervalRef . current )
const audio = audioRef . current
const startVolume = audio . volume
const volumeDiff = targetVolume - startVolume
const steps = 50
const stepTime = duration / steps
const volumeStep = volumeDiff / steps
targetVolumeRef . current = targetVolume
fadeIntervalRef . current = setInterval (() => {
const newVolume = audio . volume + volumeStep
if (( volumeStep > 0 && newVolume < targetVolume ) || ( volumeStep < 0 && newVolume > targetVolume )) {
audio . volume = newVolume
} else {
audio . volume = targetVolume
if ( fadeIntervalRef . current ) clearInterval ( fadeIntervalRef . current )
}
}, stepTime )
}
Fade Out Gradually reduces volume to 0 over 1 second (default), then pauses playback
Fade To Volume Smoothly transitions to any volume level between 0 and 1
Audio Player Component
Floating audio control button with adaptive styling:
components/audio-player.tsx
export function AudioPlayer () {
const { toggle , isPlaying , isLoaded } = useAudioContext ()
const [ isDarkBackground , setIsDarkBackground ] = useState ( true )
const [ hasScrolled , setHasScrolled ] = useState ( false )
const rafRef = useRef < number >()
useEffect (() => {
let ticking = false
const handleScroll = () => {
if ( ! hasScrolled && window . scrollY > 0 ) {
setHasScrolled ( true )
}
if ( ! ticking ) {
requestAnimationFrame (() => {
if ( rafRef . current ) cancelAnimationFrame ( rafRef . current )
rafRef . current = requestAnimationFrame (() => {
const element = document . elementFromPoint ( 50 , window . innerHeight - 100 )
if ( element ) {
const styles = window . getComputedStyle ( element )
const bgColor = styles . backgroundColor
if ( bgColor && bgColor !== 'rgba(0, 0, 0, 0)' && bgColor !== 'transparent' ) {
const rgb = bgColor . match ( / \d + / g )
if ( rgb ) {
const brightness = ( parseInt ( rgb [ 0 ]) * 299 + parseInt ( rgb [ 1 ]) * 587 + parseInt ( rgb [ 2 ]) * 114 ) / 1000
setIsDarkBackground ( brightness < 128 )
}
}
}
})
ticking = false
})
ticking = true
}
}
handleScroll ()
window . addEventListener ( 'scroll' , handleScroll , { passive: true })
return () => {
window . removeEventListener ( 'scroll' , handleScroll )
if ( rafRef . current ) cancelAnimationFrame ( rafRef . current )
}
}, [ hasScrolled ])
return (
< div className = "fixed bottom-6 right-18 z-40" >
< FloatingButton
onClick = { toggle }
disabled = { ! isLoaded }
isDarkBackground = { isDarkBackground }
isVisible = { isLoaded }
className = { ! hasScrolled ? 'animate-float-attention' : undefined }
aria-label = { isPlaying ? 'Pause audio' : 'Play audio' }
>
{ isPlaying ? < Pause size = { 18 } /> : < Play size = { 18 } className = "ml-0.5" /> }
</ FloatingButton >
</ div >
)
}
Adaptive Background Detection
The audio button adapts its styling based on the background color:
components/audio-player.tsx
const element = document . elementFromPoint ( 50 , window . innerHeight - 100 )
if ( element ) {
const styles = window . getComputedStyle ( element )
const bgColor = styles . backgroundColor
if ( bgColor && bgColor !== 'rgba(0, 0, 0, 0)' && bgColor !== 'transparent' ) {
const rgb = bgColor . match ( / \d + / g )
if ( rgb ) {
// Calculate brightness using luminance formula
const brightness = ( parseInt ( rgb [ 0 ]) * 299 + parseInt ( rgb [ 1 ]) * 587 + parseInt ( rgb [ 2 ]) * 114 ) / 1000
setIsDarkBackground ( brightness < 128 )
}
}
}
The button uses the luminance formula (0.299R + 0.587G + 0.114B) to accurately determine if the background is dark or light, adjusting its appearance for optimal contrast.
Attention Animation
On initial load, the button pulses to draw user attention:
components/audio-player.tsx
< FloatingButton
onClick = { toggle }
disabled = { ! isLoaded }
isDarkBackground = { isDarkBackground }
isVisible = { isLoaded }
className = { ! hasScrolled ? 'animate-float-attention' : undefined } // Pulse until scroll
aria-label = { isPlaying ? 'Pause audio' : 'Play audio' }
>
{ isPlaying ? < Pause size = { 18 } /> : < Play size = { 18 } className = "ml-0.5" /> }
</ FloatingButton >
Persistent Playback
Audio continues playing across page navigation:
contexts/audio-context.tsx
// Commenting out automatic fadeout on navigation
// This allows audio to continue playing across pages
// useEffect(() => {
// if (pathname !== '/') {
// fadeOut(1500)
// }
// }, [pathname])
The audio will continue playing even when navigating away from the home page. Users can manually pause using the floating audio button available on all pages.
Integration in Layout
The AudioProvider wraps the entire app in the root layout:
export default function RootLayout ({ children } : { children : ReactNode }) {
return (
< html lang = "en" suppressHydrationWarning >
< body >
< ThemeProvider forcedTheme = "light" attribute = "class" >
< LoadingProvider >
< AudioProvider > { /* Audio context wraps everything */ }
< Navigation />
{ children }
< StickyFooter />
< ScrollToTop />
< FloatingMenuButton />
< AudioPlayer /> { /* Floating audio control */ }
</ AudioProvider >
</ LoadingProvider >
</ ThemeProvider >
</ body >
</ html >
)
}
Usage in Components
Access audio controls from any component:
import { useAudioContext } from '@/contexts/audio-context'
export function MyComponent () {
const { play , pause , fadeOut , fadeToVolume , toggle , isPlaying , isLoaded } = useAudioContext ()
return (
< button onClick = { () => fadeToVolume ( 0.5 , 2000 ) } >
Fade to 50% volume
</ button >
)
}
States & Properties
isPlaying Boolean indicating if audio is currently playing
isLoaded Boolean indicating if audio file has loaded
hasUserConsent Boolean indicating if user has granted playback permission
targetVolume Ref storing the target volume level (0-1)
Request Animation Frame
Background detection uses RAF to batch DOM reads during scroll
Passive Event Listeners
Scroll listener uses { passive: true } for better performance
Cleanup
All intervals and event listeners properly cleaned up on unmount
Preloading
Audio set to preload='auto' for instant playback when user consents
Browser Compatibility
The audio system handles browser autoplay policies:
Chrome/Edge : Requires user interaction before unmuted playback
Safari : Requires user gesture to initiate audio context
Firefox : More lenient but still respects autoplay settings
Due to browser autoplay policies, audio will NOT start automatically. Users must click the audio button to grant consent and begin playback.