diff --git a/assets/js/creations/recording.js b/assets/js/creations/recording.js index 722ff55..7792715 100644 --- a/assets/js/creations/recording.js +++ b/assets/js/creations/recording.js @@ -1,22 +1,37 @@ // js/recording.js import { appState } from './state.js'; -import { addAudioTrackLane, addAudioClipToTimeline } from './audio/audio_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; -// --- NOVO: Variáveis para análise e desenho em tempo real --- +// --- Variáveis para análise visual --- let analyser = null; let liveWaveformCanvas = null; let liveWaveformCtx = null; let animationFrameId = null; let currentRecordingTrackId = null; -// ----------------------------------------------------------- /** - * Pede permissão e prepara o microfone (Tone.UserMedia) e o gravador (Tone.Recorder). + * 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; @@ -26,38 +41,30 @@ async function _initializeRecorder() { await userMedia.open(); recorder = new Tone.Recorder(); - - // --- NOVO: Inicializa o Analisador --- - // 'waveform' nos dá os dados de amplitude ao longo do tempo. 1024 é um bom tamanho de buffer. analyser = new Tone.Analyser('waveform', 1024); - // 3. Conecta o microfone a *ambos*: o gravador E o analisador userMedia.connect(recorder); - userMedia.connect(analyser); // "Fan-out" para o analisador + userMedia.connect(analyser); isRecordingInitialized = true; console.log("Sistema de gravação e análise inicializado."); return true; } catch (err) { - console.error("Erro ao inicializar a gravação (permissão negada?):", err); - alert("Erro ao acessar o microfone. Verifique as permissões do navegador."); + console.error("Erro ao inicializar gravação:", err); + alert("Erro ao acessar microfone. Verifique permissões."); return false; } } -// --- NOVO: Função para desenhar a onda ao vivo --- +/** + * Desenha a onda em tempo real no Canvas + */ function _drawLiveWaveform() { - // Continua o loop enquanto estivermos gravando - if (!appState.global.isRecording || !analyser || !liveWaveformCtx) { // - return; - } + if (!appState.global.isRecording || !analyser || !liveWaveformCtx) return; - // Pede o próximo quadro de animação animationFrameId = requestAnimationFrame(_drawLiveWaveform); - - // Pega os dados da forma de onda do analisador - const waveformData = analyser.getValue(); // Retorna um Float32Array + const waveformData = analyser.getValue(); const canvas = liveWaveformCanvas; const ctx = liveWaveformCtx; @@ -65,7 +72,7 @@ function _drawLiveWaveform() { const height = canvas.height; ctx.clearRect(0, 0, width, height); - ctx.strokeStyle = 'var(--accent-red)'; // Cor vermelha para "gravando" + ctx.strokeStyle = 'var(--accent-red)'; ctx.lineWidth = 1; ctx.beginPath(); @@ -73,80 +80,77 @@ function _drawLiveWaveform() { let x = 0; for (let i = 0; i < waveformData.length; i++) { - const v = waveformData[i]; // Valor entre -1.0 e 1.0 - const y = (v * 0.5 + 0.5) * height; // Mapeia para 0..height + const v = waveformData[i]; + const y = (v * 0.5 + 0.5) * height; - if (i === 0) { - ctx.moveTo(x, y); - } else { - ctx.lineTo(x, y); - } + if (i === 0) ctx.moveTo(x, y); + else ctx.lineTo(x, y); x += sliceWidth; } - - ctx.lineTo(width, height / 2); // Linha final até o meio + ctx.lineTo(width, height / 2); ctx.stroke(); } -// ---------------------------------------------------- - /** - * Inicia a gravação de fato. + * Inicia a gravação */ async function _startRecording() { if (appState.global.isAudioEditorPlaying) { - console.warn("A gravação foi iniciada, mas o playback do editor continua."); + console.warn("Gravação iniciada durante playback."); } const success = await _initializeRecorder(); if (!success) return; try { - const newTrack = addAudioTrackLane(); // Agora isso vai funcionar! - currentRecordingTrackId = newTrack.id; + // 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("Falha ao criar a nova pista de áudio no estado."); + console.error("Erro: addAudioTrackLane não retornou a nova pista."); return; } + currentRecordingTrackId = newTrack.id; - // 3. Renderiza o editor para a nova pista aparecer - renderAudioEditor(); // + // 2. Renderiza para atualizar o DOM + renderAudioEditor(); - // 4. Encontra o painel de info da nova pista e injeta o canvas - const trackInfoPanel = document.querySelector(`.audio-track-lane[data-track-id="${currentRecordingTrackId}"] .track-info`); // - if (!trackInfoPanel) { - console.error("Não foi possível encontrar o painel da nova pista para o canvas."); - return; + // 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'); } - liveWaveformCanvas = document.createElement('canvas'); - liveWaveformCanvas.id = 'live-waveform-canvas'; - // Ajuste a largura e altura como preferir para caber no painel - liveWaveformCanvas.width = 100; - liveWaveformCanvas.height = 40; - trackInfoPanel.appendChild(liveWaveformCanvas); - liveWaveformCtx = liveWaveformCanvas.getContext('2d'); - // --- Fim da lógica movida --- - - // 5. Inicia a gravação (Tone.Recorder) + // 4. Inicia gravação e visualização await recorder.start(); - appState.global.isRecording = true; // - console.log("Gravação iniciada..."); - _updateRecordButtonUI(true); // - - // 6. Inicia o loop de desenho + appState.global.isRecording = true; + _updateRecordButtonUI(true); _drawLiveWaveform(); } catch (err) { - console.error("Erro ao iniciar a gravação:", err); - appState.global.isRecording = false; // + console.error("Erro ao iniciar REC:", err); + appState.global.isRecording = false; _updateRecordButtonUI(false); } } /** - * Para a gravação e processa o áudio resultante. + * Para a gravação e envia para o Socket */ async function _stopRecording() { if (!recorder || !userMedia) return; @@ -154,84 +158,80 @@ async function _stopRecording() { try { const recordingBlob = await recorder.stop(); - // --- MUDANÇA: Limpa tudo --- + // Limpeza userMedia.close(); - if(analyser) analyser.dispose(); // Limpa o analisador + if(analyser) analyser.dispose(); analyser = null; isRecordingInitialized = false; + appState.global.isRecording = false; - appState.global.isRecording = false; // - - // Para o loop de animação if (animationFrameId) { cancelAnimationFrame(animationFrameId); animationFrameId = null; } - // Remove o canvas temporário if (liveWaveformCanvas) { liveWaveformCanvas.remove(); liveWaveformCanvas = null; liveWaveformCtx = null; } - // --- Fim da limpeza --- - console.log("Gravação parada."); - _updateRecordButtonUI(false); // + _updateRecordButtonUI(false); - _processRecording(recordingBlob); + // Processa o arquivo final + await _processRecording(recordingBlob); } catch (err) { - console.error("Erro ao parar a gravação:", err); - appState.global.isRecording = false; // + console.error("Erro ao parar REC:", err); + appState.global.isRecording = false; _updateRecordButtonUI(false); } } /** - * Adiciona o áudio gravado (Blob) ao editor. + * Converte o áudio e envia via Socket (Broadcast) */ -function _processRecording(blob) { - if (!blob || blob.size === 0) { - console.warn("Blob de gravação está vazio. Nada a adicionar."); - return; - } +async function _processRecording(blob) { + if (!blob || blob.size === 0) return; - // --- MUDANÇA: Não criamos mais a pista aqui, apenas pegamos o ID --- - // const newTrackId = newTrack.id; // [removido] const targetTrackId = currentRecordingTrackId; if (!targetTrackId) { - console.error("ID da pista de gravação não encontrado."); + console.error("ID da pista de gravação perdido."); return; } - currentRecordingTrackId = null; // Limpa o ID - // ------------------------------------------------------------------ + currentRecordingTrackId = null; - const blobUrl = URL.createObjectURL(blob); - const clipName = `Rec_${new Date().toISOString().slice(11, 19).replace(/:/g, '-')}`; + // 1. Converter para Base64 para poder enviar via rede + // (Blobs locais não funcionam para outros usuários) + const base64Audio = await blobToBase64(blob); - // Adiciona o clipe à pista que já criamos - addAudioClipToTimeline(blobUrl, targetTrackId, 0, clipName, null); // - - // addAudioClipToTimeline já chama o render + 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 + }); } -/** - * Atualiza o visual do botão de gravação. - */ function _updateRecordButtonUI(isRecording) { - const recordBtn = document.getElementById("record-btn"); // + const recordBtn = document.getElementById("record-btn"); if (recordBtn) { recordBtn.classList.toggle("active", isRecording); recordBtn.classList.toggle("recording", isRecording); } } -/** - * Função pública que será chamada pelo botão em main.js - */ export function toggleRecording() { - if (appState.global.isRecording) { // + if (appState.global.isRecording) { _stopRecording(); } else { _startRecording();