// js/recording.js import { appState } from './state.js'; import { addAudioTrackLane, addAudioClipToTimeline } from './audio/audio_state.js'; import { renderAudioEditor } from './audio/audio_ui.js'; let userMedia = null; let recorder = null; let isRecordingInitialized = false; // --- NOVO: Variáveis para análise e desenho em tempo real --- 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). */ async function _initializeRecorder() { if (isRecordingInitialized) return true; try { userMedia = new Tone.UserMedia(); 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 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."); return false; } } // --- NOVO: Função para desenhar a onda ao vivo --- function _drawLiveWaveform() { // Continua o loop enquanto estivermos gravando 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 canvas = liveWaveformCanvas; const ctx = liveWaveformCtx; const width = canvas.width; const height = canvas.height; ctx.clearRect(0, 0, width, height); ctx.strokeStyle = 'var(--accent-red)'; // Cor vermelha para "gravando" 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]; // Valor entre -1.0 e 1.0 const y = (v * 0.5 + 0.5) * height; // Mapeia para 0..height 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.stroke(); } // ---------------------------------------------------- /** * Inicia a gravação de fato. */ async function _startRecording() { if (appState.global.isAudioEditorPlaying) { console.warn("A gravação foi iniciada, mas o playback do editor continua."); } const success = await _initializeRecorder(); if (!success) return; try { // --- MUDANÇA: Lógica movida para cá --- // 1. Cria a pista de áudio *antes* de gravar addAudioTrackLane(); // // 2. Pega o ID da pista recém-criada const newTrack = appState.audio.tracks[appState.audio.tracks.length - 1]; // if (!newTrack) { console.error("Falha ao criar a nova pista de áudio no estado."); return; } currentRecordingTrackId = newTrack.id; // 3. Renderiza o editor para a nova pista aparecer 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; } 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) await recorder.start(); appState.global.isRecording = true; // console.log("Gravação iniciada..."); _updateRecordButtonUI(true); // // 6. Inicia o loop de desenho _drawLiveWaveform(); } catch (err) { console.error("Erro ao iniciar a gravação:", err); appState.global.isRecording = false; // _updateRecordButtonUI(false); // } } /** * Para a gravação e processa o áudio resultante. */ async function _stopRecording() { if (!recorder || !userMedia) return; try { const recordingBlob = await recorder.stop(); // --- MUDANÇA: Limpa tudo --- userMedia.close(); if(analyser) analyser.dispose(); // Limpa o analisador analyser = null; isRecordingInitialized = 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); // _processRecording(recordingBlob); } catch (err) { console.error("Erro ao parar a gravação:", err); appState.global.isRecording = false; // _updateRecordButtonUI(false); // } } /** * Adiciona o áudio gravado (Blob) ao editor. */ function _processRecording(blob) { if (!blob || blob.size === 0) { console.warn("Blob de gravação está vazio. Nada a adicionar."); 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."); return; } currentRecordingTrackId = null; // Limpa o ID // ------------------------------------------------------------------ const blobUrl = URL.createObjectURL(blob); const clipName = `Rec_${new Date().toISOString().slice(11, 19).replace(/:/g, '-')}`; // Adiciona o clipe à pista que já criamos addAudioClipToTimeline(blobUrl, targetTrackId, 0, clipName); // // addAudioClipToTimeline já chama o render, mas como o estado mudou // (o clipe foi adicionado), renderizar de novo garante que o // waveform *final* (do blob) seja desenhado corretamente. renderAudioEditor(); // } /** * Atualiza o visual do botão de gravação. */ function _updateRecordButtonUI(isRecording) { 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) { // _stopRecording(); } else { _startRecording(); } }