use new audio recorder

This commit is contained in:
Arthur Barre 2025-03-13 21:18:21 +01:00
parent 20c345f05a
commit de70929a62
4 changed files with 124 additions and 35 deletions

View File

@ -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",

View File

@ -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:

View File

@ -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<string[]>([])
const [currentRecording, setCurrentRecording] = useState<string | null>(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
}
}

View File

@ -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<File | null>(null)
const [isRecording, setIsRecording] = useState(false)
const [mediaRecorder, setMediaRecorder] = useState<MediaRecorder | null>(null)
const [recordingStatus, setRecordingStatus] = useState<"idle" | "recording" | "processing">("idle")
const [loading, setLoading] = useState(false)
const [error, setError] = useState("")
// Démarrer l'enregistrement audio
const startRecording = 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" })
// 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])
recorder.start()
setIsRecording(true)
// Handle recording start
const handleStartRecording = async () => {
try {
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() {
<Button
variant="default"
className="cursor-pointer"
onClick={startRecording}
onClick={handleStartRecording}
>
<Mic className="mr-2 h-4 w-4" />
Commencer l'enregistrement
@ -190,7 +189,7 @@ export default function RecipeForm() {
<Button
variant="destructive"
className="cursor-pointer"
onClick={stopRecording}
onClick={handleStopRecording}
>
<motion.div
className="flex items-center"