From de70929a624f210ea86c195d45e238c69c670f3c Mon Sep 17 00:00:00 2001 From: Arthur Barre Date: Thu, 13 Mar 2025 21:18:21 +0100 Subject: [PATCH] use new audio recorder --- frontend/package.json | 3 +- frontend/pnpm-lock.yaml | 8 +++ frontend/src/hooks/useAudioRecorder.ts | 81 +++++++++++++++++++++++ frontend/src/pages/Recipes/RecipeForm.tsx | 67 +++++++++---------- 4 files changed, 124 insertions(+), 35 deletions(-) create mode 100644 frontend/src/hooks/useAudioRecorder.ts diff --git a/frontend/package.json b/frontend/package.json index 667f8c4..65af04d 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -34,7 +34,8 @@ "tailwind-merge": "^3.0.2", "tailwindcss": "^4.0.12", "tailwindcss-animate": "^1.0.7", - "vite-plugin-pwa": "^0.21.1" + "vite-plugin-pwa": "^0.21.1", + "vmsg": "^0.4.0" }, "devDependencies": { "@eslint/js": "^9.21.0", diff --git a/frontend/pnpm-lock.yaml b/frontend/pnpm-lock.yaml index 5252fa9..985bebf 100644 --- a/frontend/pnpm-lock.yaml +++ b/frontend/pnpm-lock.yaml @@ -80,6 +80,9 @@ importers: vite-plugin-pwa: specifier: ^0.21.1 version: 0.21.1(vite@6.2.1(@types/node@22.13.9)(jiti@2.4.2)(lightningcss@1.29.2)(terser@5.39.0))(workbox-build@7.3.0(@types/babel__core@7.20.5))(workbox-window@7.3.0) + vmsg: + specifier: ^0.4.0 + version: 0.4.0 devDependencies: '@eslint/js': specifier: ^9.21.0 @@ -2976,6 +2979,9 @@ packages: yaml: optional: true + vmsg@0.4.0: + resolution: {integrity: sha512-46BBqRSfqdFGUpO2j+Hpz8T9YE5uWG0/PWal1PT+R1o8NEthtjG/XWl4HzbB8hIHpg/UtmKvsxL2OKQBrIYcHQ==} + webidl-conversions@4.0.2: resolution: {integrity: sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg==} @@ -6036,6 +6042,8 @@ snapshots: lightningcss: 1.29.2 terser: 5.39.0 + vmsg@0.4.0: {} + webidl-conversions@4.0.2: {} whatwg-url@7.1.0: diff --git a/frontend/src/hooks/useAudioRecorder.ts b/frontend/src/hooks/useAudioRecorder.ts new file mode 100644 index 0000000..e60d4d1 --- /dev/null +++ b/frontend/src/hooks/useAudioRecorder.ts @@ -0,0 +1,81 @@ +// useAudioRecorder.ts +import { useState, useCallback, useEffect } from 'react' +import vmsg from 'vmsg' + +// Initialize the recorder once +const recorder = new vmsg.Recorder({ + wasmURL: "https://unpkg.com/vmsg@0.3.0/vmsg.wasm" +}) + +export function useAudioRecorder() { + const [isLoading, setIsLoading] = useState(false) + const [isRecording, setIsRecording] = useState(false) + const [recordings, setRecordings] = useState([]) + const [currentRecording, setCurrentRecording] = useState(null) + + const startRecording = useCallback(async () => { + setIsLoading(true) + try { + await recorder.initAudio() + await recorder.initWorker() + recorder.startRecording() + setIsRecording(true) + } catch (e) { + console.error('Failed to start recording:', e) + } finally { + setIsLoading(false) + } + }, []) + + const stopRecording = useCallback(async () => { + if (!isRecording) return + + setIsLoading(true) + try { + const blob = await recorder.stopRecording() + const url = URL.createObjectURL(blob) + setRecordings(prev => [...prev, url]) + setCurrentRecording(url) + return { blob, url } + } catch (e) { + console.error('Failed to stop recording:', e) + } finally { + setIsRecording(false) + setIsLoading(false) + } + }, [isRecording]) + + const toggleRecording = useCallback(async () => { + if (isRecording) { + return await stopRecording() + } else { + await startRecording() + return null + } + }, [isRecording, startRecording, stopRecording]) + + const clearRecordings = useCallback(() => { + // Revoke object URLs to prevent memory leaks + recordings.forEach(url => URL.revokeObjectURL(url)) + setRecordings([]) + setCurrentRecording(null) + }, [recordings]) + + // Clean up object URLs when component unmounts + useEffect(() => { + return () => { + recordings.forEach(url => URL.revokeObjectURL(url)) + } + }, [recordings]) + + return { + isLoading, + isRecording, + recordings, + currentRecording, + startRecording, + stopRecording, + toggleRecording, + clearRecordings + } +} \ No newline at end of file diff --git a/frontend/src/pages/Recipes/RecipeForm.tsx b/frontend/src/pages/Recipes/RecipeForm.tsx index 75d4c0e..4d4c86e 100644 --- a/frontend/src/pages/Recipes/RecipeForm.tsx +++ b/frontend/src/pages/Recipes/RecipeForm.tsx @@ -2,7 +2,7 @@ import type React from "react" -import { useState } from "react" +import { useState, useEffect } from "react" import { useNavigate } from "react-router-dom" import { recipeService } from "@/api/recipe" import { Button } from "@/components/ui/button" @@ -11,40 +11,42 @@ import { Mic, Upload, ArrowLeft, Loader2 } from "lucide-react" import { motion } from "framer-motion" import { KitchenIllustration } from "@/components/illustrations/KitchenIllustration" import { CookingLoader } from "@/components/illustrations/CookingLoader" +import { useAudioRecorder } from "@/hooks/useAudioRecorder" export default function RecipeForm() { const navigate = useNavigate() + const { + isLoading: isRecorderLoading, + isRecording, + currentRecording, + startRecording, + stopRecording + } = useAudioRecorder() + const [audioFile, setAudioFile] = useState(null) - const [isRecording, setIsRecording] = useState(false) - const [mediaRecorder, setMediaRecorder] = useState(null) const [recordingStatus, setRecordingStatus] = useState<"idle" | "recording" | "processing">("idle") const [loading, setLoading] = useState(false) const [error, setError] = useState("") + // Update audioFile when recording is available + useEffect(() => { + if (currentRecording) { + fetch(currentRecording) + .then(res => res.blob()) + .then(blob => { + const file = new File([blob], "recording.mp3", { type: "audio/mp3" }) + setAudioFile(file) + setRecordingStatus("idle") + setError("") + }) + } + }, [currentRecording]) - // Démarrer l'enregistrement audio - const startRecording = async () => { + // Handle recording start + const handleStartRecording = async () => { try { - const stream = await navigator.mediaDevices.getUserMedia({ audio: true }) - const recorder = new MediaRecorder(stream) - setMediaRecorder(recorder) - - const chunks: BlobPart[] = [] - recorder.ondataavailable = (e) => { - chunks.push(e.data) - } - - recorder.onstop = () => { - const blob = new Blob(chunks, { type: "audio/webm" }) - const file = new File([blob], "recording.webm", { type: "audio/webm" }) - setAudioFile(file) - setRecordingStatus("idle") - setError("") - } - - recorder.start() - setIsRecording(true) + await startRecording() setRecordingStatus("recording") } catch (err) { console.error("Erreur lors de l'accès au microphone:", err) @@ -52,15 +54,12 @@ export default function RecipeForm() { } } - // Arrêter l'enregistrement audio - const stopRecording = () => { - if (mediaRecorder && isRecording) { - mediaRecorder.stop() - setIsRecording(false) + // Handle recording stop + const handleStopRecording = async () => { + if (isRecording) { + await stopRecording() setRecordingStatus("processing") - - // Arrêter toutes les pistes audio - mediaRecorder.stream.getTracks().forEach((track) => track.stop()) + // Processing will be updated to "idle" when the audioFile is set } } @@ -179,7 +178,7 @@ export default function RecipeForm() {