// js/recording.js import { appState } from './state.js'; import { addAudioTrackLane } from './audio/audio_state.js'; import { renderAudioEditor } from './audio/audio_ui.js'; // šŸ‘‡ IMPORTANTE: Importar o disparador de aƧƵes do Socket import { sendAction } from './socket.js'; import * as Tone from "https://esm.sh/tone"; let userMedia = null; let recorder = null; let isRecordingInitialized = false; // --- VariĆ”veis para anĆ”lise visual --- let analyser = null; let liveWaveformCanvas = null; let liveWaveformCtx = null; let animationFrameId = null; let currentRecordingTrackId = null; /** * UtilitĆ”rio: Converte Blob para Base64 (Data URI) * NecessĆ”rio para enviar o Ć”udio via Socket para outros usuĆ”rios. */ function blobToBase64(blob) { return new Promise((resolve, reject) => { const reader = new FileReader(); reader.onloadend = () => resolve(reader.result); reader.onerror = reject; reader.readAsDataURL(blob); }); } /** * Inicializa microfone e analisador */ async function _initializeRecorder() { if (isRecordingInitialized) return true; try { userMedia = new Tone.UserMedia(); await userMedia.open(); recorder = new Tone.Recorder(); analyser = new Tone.Analyser('waveform', 1024); userMedia.connect(recorder); userMedia.connect(analyser); isRecordingInitialized = true; console.log("Sistema de gravação e anĆ”lise inicializado."); return true; } catch (err) { console.error("Erro ao inicializar gravação:", err); alert("Erro ao acessar microfone. Verifique permissƵes."); return false; } } /** * Desenha a onda em tempo real no Canvas */ function _drawLiveWaveform() { if (!appState.global.isRecording || !analyser || !liveWaveformCtx) return; animationFrameId = requestAnimationFrame(_drawLiveWaveform); const waveformData = analyser.getValue(); const canvas = liveWaveformCanvas; const ctx = liveWaveformCtx; const width = canvas.width; const height = canvas.height; ctx.clearRect(0, 0, width, height); ctx.strokeStyle = 'var(--accent-red)'; ctx.lineWidth = 1; ctx.beginPath(); const sliceWidth = width * 1.0 / waveformData.length; let x = 0; for (let i = 0; i < waveformData.length; i++) { const v = waveformData[i]; const y = (v * 0.5 + 0.5) * height; if (i === 0) ctx.moveTo(x, y); else ctx.lineTo(x, y); x += sliceWidth; } ctx.lineTo(width, height / 2); ctx.stroke(); } /** * Inicia a gravação */ async function _startRecording() { if (appState.global.isAudioEditorPlaying) { console.warn("Gravação iniciada durante playback."); } const success = await _initializeRecorder(); if (!success) return; try { // 1. Cria a pista e PEGA O RETORNO (lembre de ter aplicado a correção no audio_state.js) const newTrack = addAudioTrackLane(); if (!newTrack) { console.error("Erro: addAudioTrackLane nĆ£o retornou a nova pista."); return; } currentRecordingTrackId = newTrack.id; // 2. Renderiza para atualizar o DOM renderAudioEditor(); // 3. Injeta o Canvas na pista criada const trackInfoPanel = document.querySelector(`.audio-track-lane[data-track-id="${currentRecordingTrackId}"] .track-info`); if (trackInfoPanel) { liveWaveformCanvas = document.createElement('canvas'); liveWaveformCanvas.id = 'live-waveform-canvas'; liveWaveformCanvas.width = 180; // Ajustei largura para preencher melhor liveWaveformCanvas.height = 40; // Insere antes dos controles ou no final const controls = trackInfoPanel.querySelector('.track-controls'); if(controls) { trackInfoPanel.insertBefore(liveWaveformCanvas, controls); } else { trackInfoPanel.appendChild(liveWaveformCanvas); } liveWaveformCtx = liveWaveformCanvas.getContext('2d'); } // 4. Inicia gravação e visualização await recorder.start(); appState.global.isRecording = true; _updateRecordButtonUI(true); _drawLiveWaveform(); } catch (err) { console.error("Erro ao iniciar REC:", err); appState.global.isRecording = false; _updateRecordButtonUI(false); } } /** * Para a gravação e envia para o Socket */ async function _stopRecording() { if (!recorder || !userMedia) return; try { const recordingBlob = await recorder.stop(); // Limpeza userMedia.close(); if(analyser) analyser.dispose(); analyser = null; isRecordingInitialized = false; appState.global.isRecording = false; if (animationFrameId) { cancelAnimationFrame(animationFrameId); animationFrameId = null; } if (liveWaveformCanvas) { liveWaveformCanvas.remove(); liveWaveformCanvas = null; liveWaveformCtx = null; } _updateRecordButtonUI(false); // Processa o arquivo final await _processRecording(recordingBlob); } catch (err) { console.error("Erro ao parar REC:", err); appState.global.isRecording = false; _updateRecordButtonUI(false); } } /** * Converte o Ć”udio e envia via Socket (Broadcast) */ async function _processRecording(blob) { if (!blob || blob.size === 0) return; const targetTrackId = currentRecordingTrackId; if (!targetTrackId) { console.error("ID da pista de gravação perdido."); return; } currentRecordingTrackId = null; // 1. Converter para Base64 para poder enviar via rede // (Blobs locais nĆ£o funcionam para outros usuĆ”rios) const base64Audio = await blobToBase64(blob); const clipName = `Rec_${new Date().toLocaleTimeString().replace(/:/g, '-')}`; const clipId = `rec_${Date.now()}_${Math.random().toString(36).substr(2, 5)}`; // 2. USAR sendAction EM VEZ DE EDITAR O ESTADO DIRETAMENTE // Isso garante que: // a) O socket.js processe a ação (Broadcasting para a sala) // b) O socket.js chame handleActionBroadcast localmente (Atualizando sua tela) sendAction({ type: "ADD_AUDIO_CLIP", filePath: base64Audio, // Base64 funciona como URL no Tone.js/Browser trackId: targetTrackId, startTimeInSeconds: 0, clipId: clipId, name: clipName }); } function _updateRecordButtonUI(isRecording) { const recordBtn = document.getElementById("record-btn"); if (recordBtn) { recordBtn.classList.toggle("active", isRecording); recordBtn.classList.toggle("recording", isRecording); } } export function toggleRecording() { if (appState.global.isRecording) { _stopRecording(); } else { _startRecording(); } }