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 [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
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
@@ -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
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user