This commit is contained in:
Arthur Barre 2025-03-13 21:26:24 +01:00
parent de70929a62
commit 20b9b17493
4 changed files with 384 additions and 45 deletions

View File

@ -1,72 +1,72 @@
// useAudioRecorder.ts
import { useState, useCallback, useEffect } from 'react'
import vmsg from 'vmsg'
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 [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)
setIsLoading(true);
try {
await recorder.initAudio()
await recorder.initWorker()
recorder.startRecording()
setIsRecording(true)
} catch (e) {
console.error('Failed to start recording:', e)
// Nécessaire sur mobile : initAudio DOIT être dans un handler utilisateur (tap/click)
await recorder.initAudio();
await recorder.initWorker();
await recorder.startRecording();
setIsRecording(true);
} catch (error) {
console.error("Erreur lors du démarrage de l'enregistrement :", error);
throw error;
} finally {
setIsLoading(false)
setIsLoading(false);
}
}, [])
}, []);
const stopRecording = useCallback(async () => {
if (!isRecording) return
if (!isRecording) return;
setIsLoading(true)
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)
const blob = await recorder.stopRecording();
const url = URL.createObjectURL(blob);
setRecordings(prev => [...prev, url]);
setCurrentRecording(url);
return { blob, url };
} catch (error) {
console.error("Erreur lors de l'arrêt de l'enregistrement :", error);
throw error;
} finally {
setIsRecording(false)
setIsLoading(false)
setIsRecording(false);
setIsLoading(false);
}
}, [isRecording])
}, [isRecording]);
const toggleRecording = useCallback(async () => {
if (isRecording) {
return await stopRecording()
return await stopRecording();
} else {
await startRecording()
return null
return await startRecording();
}
}, [isRecording, startRecording, stopRecording])
}, [isRecording, startRecording, stopRecording]);
const clearRecordings = useCallback(() => {
// Revoke object URLs to prevent memory leaks
recordings.forEach(url => URL.revokeObjectURL(url))
setRecordings([])
setCurrentRecording(null)
}, [recordings])
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])
recordings.forEach(url => URL.revokeObjectURL(url));
};
}, [recordings]);
return {
isLoading,
@ -77,5 +77,5 @@ export function useAudioRecorder() {
stopRecording,
toggleRecording,
clearRecordings
}
};
}

22
recorder/package-lock.json generated Normal file
View File

@ -0,0 +1,22 @@
{
"name": "recorder",
"version": "1.0.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "recorder",
"version": "1.0.0",
"license": "ISC",
"dependencies": {
"vmsg": "^0.4.0"
}
},
"node_modules/vmsg": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/vmsg/-/vmsg-0.4.0.tgz",
"integrity": "sha512-46BBqRSfqdFGUpO2j+Hpz8T9YE5uWG0/PWal1PT+R1o8NEthtjG/XWl4HzbB8hIHpg/UtmKvsxL2OKQBrIYcHQ==",
"license": "CC0-1.0"
}
}
}

15
recorder/package.json Normal file
View File

@ -0,0 +1,15 @@
{
"name": "recorder",
"version": "1.0.0",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC",
"description": "",
"dependencies": {
"vmsg": "^0.4.0"
}
}

302
recorder/recorder.html Normal file
View File

@ -0,0 +1,302 @@
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Enregistreur Audio</title>
<style>
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
max-width: 800px;
margin: 0 auto;
padding: 20px;
background-color: #f9f9f9;
color: #333;
}
h1 {
text-align: center;
color: #e67e22;
}
.container {
background-color: white;
border-radius: 8px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
padding: 20px;
margin-top: 20px;
}
.recorder-controls {
display: flex;
justify-content: center;
margin: 20px 0;
}
button {
background-color: #e67e22;
color: white;
border: none;
border-radius: 4px;
padding: 10px 20px;
font-size: 16px;
cursor: pointer;
transition: background-color 0.3s;
display: flex;
align-items: center;
gap: 8px;
}
button:hover {
background-color: #d35400;
}
button:disabled {
background-color: #ccc;
cursor: not-allowed;
}
.recordings {
margin-top: 20px;
}
.recording-item {
background-color: #f5f5f5;
border-radius: 4px;
padding: 15px;
margin-bottom: 10px;
}
.recording-info {
margin-bottom: 10px;
font-size: 14px;
color: #666;
}
audio {
width: 100%;
}
.status {
text-align: center;
margin: 10px 0;
font-style: italic;
color: #666;
}
.pulse {
animation: pulse 1.5s infinite;
}
@keyframes pulse {
0% {
transform: scale(1);
}
50% {
transform: scale(1.05);
}
100% {
transform: scale(1);
}
}
.mic-icon {
width: 20px;
height: 20px;
}
</style>
<!-- Charger le script vmsg avant notre script principal -->
<script src="https://unpkg.com/vmsg@0.3.0/vmsg.js"></script>
</head>
<body>
<h1>Enregistreur Audio</h1>
<div class="container">
<p>Enregistrez votre voix et écoutez le résultat. Les enregistrements sont stockés localement dans votre navigateur.
</p>
<div id="status" class="status">Prêt à enregistrer</div>
<div class="recorder-controls">
<button id="recordButton">
<svg class="mic-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
stroke-linecap="round" stroke-linejoin="round">
<path d="M12 1a3 3 0 0 0-3 3v8a3 3 0 0 0 6 0V4a3 3 0 0 0-3-3z"></path>
<path d="M19 10v2a7 7 0 0 1-14 0v-2"></path>
<line x1="12" y1="19" x2="12" y2="23"></line>
<line x1="8" y1="23" x2="16" y2="23"></line>
</svg>
Commencer l'enregistrement
</button>
</div>
<div id="recordings" class="recordings">
<h3>Vos enregistrements</h3>
<div id="recordingsList"></div>
</div>
</div>
<script>
// Attendre que vmsg soit complètement chargé
window.onload = function () {
// Vérifier si vmsg est disponible
if (typeof vmsg === 'undefined') {
console.error("La bibliothèque vmsg n'a pas été chargée correctement");
document.getElementById('status').textContent = "Erreur: Impossible de charger l'enregistreur audio";
document.getElementById('recordButton').disabled = true;
return;
}
// Initialiser le recorder
const recorder = new vmsg.Recorder({
wasmURL: "https://unpkg.com/vmsg@0.3.0/vmsg.wasm"
});
const recordButton = document.getElementById('recordButton');
const statusElement = document.getElementById('status');
const recordingsList = document.getElementById('recordingsList');
let isLoading = false;
let isRecording = false;
let recordings = [];
// Fonction pour mettre à jour l'interface utilisateur
function updateUI() {
if (isLoading) {
recordButton.disabled = true;
statusElement.textContent = isRecording
? "Arrêt de l'enregistrement..."
: "Initialisation de l'enregistrement...";
} else if (isRecording) {
recordButton.innerHTML = `
<svg class="mic-icon pulse" viewBox="0 0 24 24" fill="none" stroke="red" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M12 1a3 3 0 0 0-3 3v8a3 3 0 0 0 6 0V4a3 3 0 0 0-3-3z"></path>
<path d="M19 10v2a7 7 0 0 1-14 0v-2"></path>
<line x1="12" y1="19" x2="12" y2="23"></line>
<line x1="8" y1="23" x2="16" y2="23"></line>
</svg>
Arrêter l'enregistrement
`;
recordButton.style.backgroundColor = '#e74c3c';
statusElement.textContent = "Enregistrement en cours...";
} else {
recordButton.disabled = false;
recordButton.innerHTML = `
<svg class="mic-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M12 1a3 3 0 0 0-3 3v8a3 3 0 0 0 6 0V4a3 3 0 0 0-3-3z"></path>
<path d="M19 10v2a7 7 0 0 1-14 0v-2"></path>
<line x1="12" y1="19" x2="12" y2="23"></line>
<line x1="8" y1="23" x2="16" y2="23"></line>
</svg>
Commencer l'enregistrement
`;
recordButton.style.backgroundColor = '#e67e22';
statusElement.textContent = "Prêt à enregistrer";
}
}
// Fonction pour gérer l'enregistrement
async function toggleRecording() {
isLoading = true;
updateUI();
if (isRecording) {
try {
const blob = await recorder.stopRecording();
const url = URL.createObjectURL(blob);
const timestamp = new Date().toLocaleString();
const size = (blob.size / 1024).toFixed(2);
recordings.push({ url, timestamp, size });
renderRecordings();
isRecording = false;
} catch (e) {
console.error("Erreur lors de l'arrêt de l'enregistrement:", e);
alert("Une erreur est survenue lors de l'arrêt de l'enregistrement.");
}
} else {
try {
await recorder.initAudio();
await recorder.initWorker();
recorder.startRecording();
isRecording = true;
} catch (e) {
console.error("Erreur lors du démarrage de l'enregistrement:", e);
alert("Impossible d'accéder au microphone. Vérifiez les permissions.");
}
}
isLoading = false;
updateUI();
}
// Fonction pour afficher les enregistrements
function renderRecordings() {
recordingsList.innerHTML = '';
if (recordings.length === 0) {
recordingsList.innerHTML = '<p>Aucun enregistrement pour le moment.</p>';
return;
}
recordings.forEach((recording, index) => {
const recordingItem = document.createElement('div');
recordingItem.className = 'recording-item';
recordingItem.innerHTML = `
<div class="recording-info">
<strong>Enregistrement #${index + 1}</strong> - ${recording.timestamp} (${recording.size} KB)
</div>
<audio controls src="${recording.url}"></audio>
<div style="margin-top: 10px;">
<button class="download-btn" data-index="${index}" style="background-color: #3498db;">
Télécharger
</button>
<button class="delete-btn" data-index="${index}" style="background-color: #e74c3c; margin-left: 10px;">
Supprimer
</button>
</div>
`;
recordingsList.appendChild(recordingItem);
});
// Ajouter les écouteurs d'événements pour les boutons
document.querySelectorAll('.download-btn').forEach(btn => {
btn.addEventListener('click', function () {
const index = parseInt(this.dataset.index);
const recording = recordings[index];
const a = document.createElement('a');
a.href = recording.url;
a.download = `enregistrement-${index + 1}.mp3`;
a.click();
});
});
document.querySelectorAll('.delete-btn').forEach(btn => {
btn.addEventListener('click', function () {
const index = parseInt(this.dataset.index);
URL.revokeObjectURL(recordings[index].url);
recordings.splice(index, 1);
renderRecordings();
});
});
}
// Initialiser l'interface
renderRecordings();
updateUI();
// Ajouter l'écouteur d'événement pour le bouton d'enregistrement
recordButton.addEventListener('click', toggleRecording);
};
</script>
</body>
</html>