114 lines
3.8 KiB
TypeScript
114 lines
3.8 KiB
TypeScript
"use client";
|
|
|
|
import React, { useState, useEffect, useRef } from "react";
|
|
import { Volume2, VolumeX } from "lucide-react";
|
|
|
|
export default function CosmicAudioToggle() {
|
|
const [isPlaying, setIsPlaying] = useState(false);
|
|
const audioCtxRef = useRef<AudioContext | null>(null);
|
|
const masterGainRef = useRef<GainNode | null>(null);
|
|
const osc1Ref = useRef<OscillatorNode | null>(null);
|
|
const osc2Ref = useRef<OscillatorNode | null>(null);
|
|
|
|
const toggleSound = () => {
|
|
if (isPlaying) {
|
|
if (masterGainRef.current && audioCtxRef.current) {
|
|
masterGainRef.current.gain.setTargetAtTime(
|
|
0,
|
|
audioCtxRef.current.currentTime,
|
|
0.5
|
|
);
|
|
setTimeout(() => {
|
|
setIsPlaying(false);
|
|
}, 500);
|
|
}
|
|
} else {
|
|
try {
|
|
const AudioCtx =
|
|
window.AudioContext ||
|
|
(window as unknown as { webkitAudioContext: typeof AudioContext })
|
|
.webkitAudioContext;
|
|
const ctx = audioCtxRef.current || new AudioCtx();
|
|
audioCtxRef.current = ctx;
|
|
|
|
if (ctx.state === "suspended") {
|
|
ctx.resume();
|
|
}
|
|
|
|
const masterGain = ctx.createGain();
|
|
masterGain.gain.setValueAtTime(0, ctx.currentTime);
|
|
masterGain.gain.linearRampToValueAtTime(0.08, ctx.currentTime + 1.5);
|
|
masterGain.connect(ctx.destination);
|
|
masterGainRef.current = masterGain;
|
|
|
|
// Lowpass filter for deep cosmic drone
|
|
const filter = ctx.createBiquadFilter();
|
|
filter.type = "lowpass";
|
|
filter.frequency.setValueAtTime(140, ctx.currentTime);
|
|
filter.Q.setValueAtTime(2, ctx.currentTime);
|
|
filter.connect(masterGain);
|
|
|
|
// Oscillator 1: 55Hz sub drone
|
|
const osc1 = ctx.createOscillator();
|
|
osc1.type = "sine";
|
|
osc1.frequency.setValueAtTime(55, ctx.currentTime);
|
|
osc1.connect(filter);
|
|
osc1.start();
|
|
osc1Ref.current = osc1;
|
|
|
|
// Oscillator 2: 110.2Hz warm harmonic with slight detune
|
|
const osc2 = ctx.createOscillator();
|
|
osc2.type = "triangle";
|
|
osc2.frequency.setValueAtTime(110.3, ctx.currentTime);
|
|
const osc2Gain = ctx.createGain();
|
|
osc2Gain.gain.setValueAtTime(0.03, ctx.currentTime);
|
|
osc2.connect(osc2Gain);
|
|
osc2Gain.connect(filter);
|
|
osc2.start();
|
|
osc2Ref.current = osc2;
|
|
|
|
setIsPlaying(true);
|
|
} catch {
|
|
// Fallback gracefully if Web Audio is blocked
|
|
}
|
|
}
|
|
};
|
|
|
|
useEffect(() => {
|
|
return () => {
|
|
if (audioCtxRef.current && audioCtxRef.current.state !== "closed") {
|
|
try {
|
|
audioCtxRef.current.close();
|
|
} catch {
|
|
// Ignore
|
|
}
|
|
}
|
|
};
|
|
}, []);
|
|
|
|
return (
|
|
<button
|
|
onClick={toggleSound}
|
|
title={isPlaying ? "Mute Deep Space Ambience" : "Enable Deep Space Ambience"}
|
|
className="flex items-center gap-2 px-3 py-1.5 rounded-full glass-panel hover:border-indigo-500/40 text-xs font-mono tracking-wider text-slate-300 transition-all duration-300 hover:text-white group"
|
|
aria-label="Toggle Cosmic Soundscape"
|
|
>
|
|
{isPlaying ? (
|
|
<>
|
|
<span className="relative flex h-2 w-2">
|
|
<span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-cyan-400 opacity-75"></span>
|
|
<span className="relative inline-flex rounded-full h-2 w-2 bg-cyan-500"></span>
|
|
</span>
|
|
<Volume2 className="w-3.5 h-3.5 text-cyan-400 group-hover:scale-110 transition-transform" />
|
|
<span className="hidden sm:inline text-[11px] text-cyan-300">AUDIO: RESONANCE ON</span>
|
|
</>
|
|
) : (
|
|
<>
|
|
<VolumeX className="w-3.5 h-3.5 text-slate-400 group-hover:scale-110 transition-transform" />
|
|
<span className="hidden sm:inline text-[11px] text-slate-400">AUDIO: VOID</span>
|
|
</>
|
|
)}
|
|
</button>
|
|
);
|
|
}
|