feat(midsummer): WebAudio Sma grodorna jingle, plays once on first gesture
This commit is contained in:
parent
e896ef1f21
commit
3cd2165c15
3 changed files with 68 additions and 2 deletions
|
|
@ -1,5 +1,6 @@
|
||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
import { useLiveData } from '../hooks/useLiveData';
|
import { useLiveData } from '../hooks/useLiveData';
|
||||||
|
import { useMidsummerSound } from '../hooks/useMidsummerSound';
|
||||||
import { PlayerDashboardContent } from './windows/PlayerDashboardWindow';
|
import { PlayerDashboardContent } from './windows/PlayerDashboardWindow';
|
||||||
import { MidsummerBanner } from './midsummer/MidsummerBanner';
|
import { MidsummerBanner } from './midsummer/MidsummerBanner';
|
||||||
|
|
||||||
|
|
@ -15,6 +16,7 @@ import { MidsummerBanner } from './midsummer/MidsummerBanner';
|
||||||
*/
|
*/
|
||||||
export const PlayerDashboardFullPage: React.FC = () => {
|
export const PlayerDashboardFullPage: React.FC = () => {
|
||||||
const data = useLiveData();
|
const data = useLiveData();
|
||||||
|
useMidsummerSound();
|
||||||
const [version, setVersion] = useState('');
|
const [version, setVersion] = useState('');
|
||||||
|
|
||||||
// Set tab title.
|
// Set tab title.
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ import { WindowRenderer } from '../windows/WindowRenderer';
|
||||||
import { RareNotification } from '../effects/RareNotification';
|
import { RareNotification } from '../effects/RareNotification';
|
||||||
import { DeathNotification } from '../effects/DeathNotification';
|
import { DeathNotification } from '../effects/DeathNotification';
|
||||||
import { usePlayerColors } from '../../hooks/usePlayerColors';
|
import { usePlayerColors } from '../../hooks/usePlayerColors';
|
||||||
|
import { useMidsummerSound } from '../../hooks/useMidsummerSound';
|
||||||
import { MidsummerBanner } from '../midsummer/MidsummerBanner';
|
import { MidsummerBanner } from '../midsummer/MidsummerBanner';
|
||||||
import type { DashboardState } from '../../hooks/useLiveData';
|
import type { DashboardState } from '../../hooks/useLiveData';
|
||||||
|
|
||||||
|
|
@ -16,6 +17,7 @@ interface Props {
|
||||||
|
|
||||||
export const MapLayout: React.FC<Props> = ({ data }) => {
|
export const MapLayout: React.FC<Props> = ({ data }) => {
|
||||||
const getColor = usePlayerColors();
|
const getColor = usePlayerColors();
|
||||||
|
useMidsummerSound();
|
||||||
const [showHeatmap, setShowHeatmap] = useState(false);
|
const [showHeatmap, setShowHeatmap] = useState(false);
|
||||||
const [showPortals, setShowPortals] = useState(false);
|
const [showPortals, setShowPortals] = useState(false);
|
||||||
const [selectedPlayer, setSelectedPlayer] = useState<string | null>(null);
|
const [selectedPlayer, setSelectedPlayer] = useState<string | null>(null);
|
||||||
|
|
|
||||||
|
|
@ -1,2 +1,64 @@
|
||||||
// Stub — replaced with the full WebAudio synth in Task 8.
|
import { useEffect } from 'react';
|
||||||
export function playSmaGrodorna(): void {}
|
import { useMidsummer } from './useMidsummer';
|
||||||
|
|
||||||
|
const JINGLE_FLAG = 'mo-midsummer-jingle';
|
||||||
|
|
||||||
|
let ctx: AudioContext | null = null;
|
||||||
|
|
||||||
|
// Public-domain "Små grodorna" opening phrase (approximation), as
|
||||||
|
// [frequencyHz, durationSeconds]. Cheerful major-key triangle tones.
|
||||||
|
const MELODY: [number, number][] = [
|
||||||
|
[392, 0.22], [392, 0.22], [392, 0.22], [440, 0.22], [494, 0.42],
|
||||||
|
[494, 0.22], [440, 0.22], [494, 0.22], [523, 0.22], [587, 0.5],
|
||||||
|
];
|
||||||
|
|
||||||
|
/** Play the jingle once. Safe to call from any user gesture. No-op if
|
||||||
|
* WebAudio is unavailable. Reuses a single AudioContext (no leak). */
|
||||||
|
export function playSmaGrodorna(): void {
|
||||||
|
try {
|
||||||
|
const AC = window.AudioContext || (window as unknown as { webkitAudioContext: typeof AudioContext }).webkitAudioContext;
|
||||||
|
if (!AC) return;
|
||||||
|
if (!ctx) ctx = new AC();
|
||||||
|
if (ctx.state === 'suspended') void ctx.resume();
|
||||||
|
let t = ctx.currentTime + 0.05;
|
||||||
|
for (const [freq, dur] of MELODY) {
|
||||||
|
const osc = ctx.createOscillator();
|
||||||
|
const gain = ctx.createGain();
|
||||||
|
osc.type = 'triangle';
|
||||||
|
osc.frequency.value = freq;
|
||||||
|
gain.gain.setValueAtTime(0.0001, t);
|
||||||
|
gain.gain.exponentialRampToValueAtTime(0.18, t + 0.02);
|
||||||
|
gain.gain.exponentialRampToValueAtTime(0.0001, t + dur);
|
||||||
|
osc.connect(gain).connect(ctx.destination);
|
||||||
|
osc.start(t);
|
||||||
|
osc.stop(t + dur);
|
||||||
|
t += dur;
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
/* audio not available — ignore */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Plays the jingle once per session, on the first user gesture, when the
|
||||||
|
* theme and sound are both on. Browsers block audio before a gesture, so
|
||||||
|
* we wait for the first pointerdown/keydown. */
|
||||||
|
export function useMidsummerSound(): void {
|
||||||
|
const { enabled, soundOn } = useMidsummer();
|
||||||
|
useEffect(() => {
|
||||||
|
if (!enabled || !soundOn) return;
|
||||||
|
if (sessionStorage.getItem(JINGLE_FLAG)) return;
|
||||||
|
|
||||||
|
const fire = () => {
|
||||||
|
sessionStorage.setItem(JINGLE_FLAG, '1');
|
||||||
|
playSmaGrodorna();
|
||||||
|
cleanup();
|
||||||
|
};
|
||||||
|
const cleanup = () => {
|
||||||
|
window.removeEventListener('pointerdown', fire);
|
||||||
|
window.removeEventListener('keydown', fire);
|
||||||
|
};
|
||||||
|
window.addEventListener('pointerdown', fire);
|
||||||
|
window.addEventListener('keydown', fire);
|
||||||
|
return cleanup;
|
||||||
|
}, [enabled, soundOn]);
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue