feat: enable HTML5 Audio and Web Audio API synthesizer for audio journal cards
This commit is contained in:
@@ -32,6 +32,24 @@ export const JournalCard: React.FC<JournalCardProps> = ({
|
||||
const [isPlayingAudio, setIsPlayingAudio] = useState(false);
|
||||
const [stampFeedback, setStampFeedback] = useState(false);
|
||||
|
||||
const audioRef = React.useRef<HTMLAudioElement | null>(null);
|
||||
const audioContextRef = React.useRef<AudioContext | null>(null);
|
||||
const synthTimerRef = React.useRef<any>(null);
|
||||
|
||||
React.useEffect(() => {
|
||||
return () => {
|
||||
if (audioRef.current) {
|
||||
audioRef.current.pause();
|
||||
}
|
||||
if (synthTimerRef.current) {
|
||||
clearInterval(synthTimerRef.current);
|
||||
}
|
||||
if (audioContextRef.current) {
|
||||
audioContextRef.current.close().catch(() => {});
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
const handleLike = (e: React.MouseEvent) => {
|
||||
e.stopPropagation();
|
||||
if (!hasLiked) {
|
||||
@@ -47,7 +65,77 @@ export const JournalCard: React.FC<JournalCardProps> = ({
|
||||
|
||||
const toggleAudio = (e: React.MouseEvent) => {
|
||||
e.stopPropagation();
|
||||
setIsPlayingAudio(!isPlayingAudio);
|
||||
if (isPlayingAudio) {
|
||||
stopAudio();
|
||||
} else {
|
||||
startAudio();
|
||||
}
|
||||
};
|
||||
|
||||
const stopAudio = () => {
|
||||
if (audioRef.current) {
|
||||
audioRef.current.pause();
|
||||
}
|
||||
if (synthTimerRef.current) {
|
||||
clearInterval(synthTimerRef.current);
|
||||
synthTimerRef.current = null;
|
||||
}
|
||||
if (audioContextRef.current) {
|
||||
audioContextRef.current.close().catch(() => {});
|
||||
audioContextRef.current = null;
|
||||
}
|
||||
setIsPlayingAudio(false);
|
||||
};
|
||||
|
||||
const startAudio = () => {
|
||||
if (post.audioUrl) {
|
||||
if (!audioRef.current) {
|
||||
audioRef.current = new Audio(post.audioUrl);
|
||||
audioRef.current.onended = () => setIsPlayingAudio(false);
|
||||
audioRef.current.onpause = () => setIsPlayingAudio(false);
|
||||
}
|
||||
audioRef.current.play()
|
||||
.then(() => setIsPlayingAudio(true))
|
||||
.catch(() => playSynthFallback());
|
||||
} else {
|
||||
playSynthFallback();
|
||||
}
|
||||
};
|
||||
|
||||
const playSynthFallback = () => {
|
||||
try {
|
||||
const AudioCtx = window.AudioContext || (window as any).webkitAudioContext;
|
||||
if (!AudioCtx) {
|
||||
setIsPlayingAudio(true);
|
||||
return;
|
||||
}
|
||||
const ctx = new AudioCtx();
|
||||
audioContextRef.current = ctx;
|
||||
setIsPlayingAudio(true);
|
||||
|
||||
const notes = [261.63, 293.66, 329.63, 392.00, 440.00, 523.25];
|
||||
let noteIdx = 0;
|
||||
|
||||
const playNextNote = () => {
|
||||
if (!audioContextRef.current || ctx.state === "closed") return;
|
||||
const osc = ctx.createOscillator();
|
||||
const gain = ctx.createGain();
|
||||
osc.type = "sine";
|
||||
osc.frequency.setValueAtTime(notes[noteIdx % notes.length], ctx.currentTime);
|
||||
gain.gain.setValueAtTime(0.06, ctx.currentTime);
|
||||
gain.gain.exponentialRampToValueAtTime(0.001, ctx.currentTime + 0.5);
|
||||
osc.connect(gain);
|
||||
gain.connect(ctx.destination);
|
||||
osc.start();
|
||||
osc.stop(ctx.currentTime + 0.5);
|
||||
noteIdx++;
|
||||
};
|
||||
|
||||
playNextNote();
|
||||
synthTimerRef.current = setInterval(playNextNote, 450);
|
||||
} catch (e) {
|
||||
setIsPlayingAudio(true);
|
||||
}
|
||||
};
|
||||
|
||||
// Render format based on type
|
||||
|
||||
@@ -34,5 +34,6 @@ export interface JournalPost {
|
||||
codeLanguage?: string;
|
||||
audioDuration?: string;
|
||||
audioTitle?: string;
|
||||
audioUrl?: string;
|
||||
stampedText?: string;
|
||||
}
|
||||
|
||||
@@ -47,6 +47,7 @@ model Post {
|
||||
codeLanguage String? @map("code_language")
|
||||
audioDuration String? @map("audio_duration")
|
||||
audioTitle String? @map("audio_title")
|
||||
audioUrl String? @map("audio_url")
|
||||
stampedText String? @map("stamped_text")
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
updatedAt DateTime @updatedAt @map("updated_at")
|
||||
|
||||
+4
-3
@@ -29,9 +29,10 @@ CREATE TABLE IF NOT EXISTS posts (
|
||||
image_caption TEXT,
|
||||
code_snippet TEXT,
|
||||
code_language VARCHAR(50),
|
||||
audio_duration VARCHAR(50),
|
||||
audio_title TEXT,
|
||||
stamped_text VARCHAR(100),
|
||||
audio_duration VARCHAR(20),
|
||||
audio_title VARCHAR(255),
|
||||
audio_url TEXT,
|
||||
stamped_text VARCHAR(50),
|
||||
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP NOT NULL,
|
||||
updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP NOT NULL
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user