feat: enable HTML5 Audio and Web Audio API synthesizer for audio journal cards

This commit is contained in:
mstfyldz
2026-08-16 17:49:43 +03:00
parent b23ea43392
commit 89f9605557
4 changed files with 95 additions and 4 deletions
+89 -1
View File
@@ -32,6 +32,24 @@ export const JournalCard: React.FC<JournalCardProps> = ({
const [isPlayingAudio, setIsPlayingAudio] = useState(false); const [isPlayingAudio, setIsPlayingAudio] = useState(false);
const [stampFeedback, setStampFeedback] = 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) => { const handleLike = (e: React.MouseEvent) => {
e.stopPropagation(); e.stopPropagation();
if (!hasLiked) { if (!hasLiked) {
@@ -47,7 +65,77 @@ export const JournalCard: React.FC<JournalCardProps> = ({
const toggleAudio = (e: React.MouseEvent) => { const toggleAudio = (e: React.MouseEvent) => {
e.stopPropagation(); 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 // Render format based on type
+1
View File
@@ -34,5 +34,6 @@ export interface JournalPost {
codeLanguage?: string; codeLanguage?: string;
audioDuration?: string; audioDuration?: string;
audioTitle?: string; audioTitle?: string;
audioUrl?: string;
stampedText?: string; stampedText?: string;
} }
+1
View File
@@ -47,6 +47,7 @@ model Post {
codeLanguage String? @map("code_language") codeLanguage String? @map("code_language")
audioDuration String? @map("audio_duration") audioDuration String? @map("audio_duration")
audioTitle String? @map("audio_title") audioTitle String? @map("audio_title")
audioUrl String? @map("audio_url")
stampedText String? @map("stamped_text") stampedText String? @map("stamped_text")
createdAt DateTime @default(now()) @map("created_at") createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at") updatedAt DateTime @updatedAt @map("updated_at")
+4 -3
View File
@@ -29,9 +29,10 @@ CREATE TABLE IF NOT EXISTS posts (
image_caption TEXT, image_caption TEXT,
code_snippet TEXT, code_snippet TEXT,
code_language VARCHAR(50), code_language VARCHAR(50),
audio_duration VARCHAR(50), audio_duration VARCHAR(20),
audio_title TEXT, audio_title VARCHAR(255),
stamped_text VARCHAR(100), audio_url TEXT,
stamped_text VARCHAR(50),
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP NOT NULL, created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP NOT NULL,
updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP NOT NULL updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP NOT NULL
); );