diff --git a/assets/js/creations/recording.js b/assets/js/creations/recording.js index 722ff553..7792715f 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(); diff --git a/assets/js/creations/server/data/teste.json b/assets/js/creations/server/data/teste.json new file mode 100644 index 00000000..f357bcfc --- /dev/null +++ b/assets/js/creations/server/data/teste.json @@ -0,0 +1 @@ +{"projectXml":"\n\n\n \n \n \n \n \n \n \n","audio":{"tracks":[{"id":1761493568567,"name":"Pista de Áudio 1"},{"id":1761493568703,"name":"Pista de Áudio 2"},{"id":1761493649939,"name":"Pista de Áudio 3"},{"id":1761493650115,"name":"Pista de Áudio 4"},{"id":1761493650258,"name":"Pista de Áudio 5"},{"id":1761493172201,"name":"Pista de Áudio 6"},{"id":1761494647801,"name":"Pista de Áudio 7"},{"id":"track_1761494658269_ugophvk","name":"Pista de Áudio 8"},{"id":"track_1761494663244_d02eymw","name":"Pista de Áudio 9"},{"id":1761494644211,"name":"Pista de Áudio 10"},{"id":1761494703529,"name":"Pista de Áudio 11"},{"id":"track_1761494760329_r251hkm","name":"Pista de Áudio 12"},{"id":1761495115257,"name":"Pista de Áudio 13"},{"id":"track_1761495200078_tutmj39","name":"Pista de Áudio 14"},{"id":1761495668600,"name":"Pista de Áudio 15"},{"id":"track_1761495680437_5b0cy18","name":"Pista de Áudio 16"},{"id":"track_1761496472045_bk9iq61","name":"Pista de Áudio 17"},{"id":"track_1761496473349_gzy9jf5","name":"Pista de Áudio 18"},{"id":"track_1761496507683_5ki5kwa","name":"Pista de Áudio 19"},{"id":"track_1761499407995_5maq15z","name":"Pista de Áudio 20"},{"id":"track_1761500741617_rpz0rna","name":"Pista de Áudio 21"},{"id":"track_1761500742384_ifk23ix","name":"Pista de Áudio 22"},{"id":"track_1761500743176_q3xiqem","name":"Pista de Áudio 23"},{"id":"track_1761516174436_k148qeg","name":"Pista de Áudio 24"},{"id":"track_1761595035504_e5nbwb0","name":"Pista de Áudio 25"},{"id":"track_1762285083478_7hrl01q","name":"Pista de Áudio 26"},{"id":"track_1762285084094_qll6vxd","name":"Pista de Áudio 27"},{"id":"track_1762302178952_wcttlam","name":"Pista de Áudio 28"},{"id":"track_1762987134721_r3wuym4","name":"Pista de Áudio 29"},{"id":"track_1763215292203_3deprfo","name":"Pista de Áudio 30"},{"id":"track_1763216139380_3wqct5l","name":"Pista de Áudio 31"}],"clips":[{"id":"ede18e8f-d896-4bf8-8942-988212bc039a","trackId":1761493172201,"name":"break01.ogg","sourcePath":"src/samples/beats/break01.ogg","startTimeInSeconds":0.16666666666666666,"durationInSeconds":0,"offset":0,"pitch":0,"volume":1,"pan":0,"originalDuration":0},{"id":"58fe84a6-7e84-4d89-8971-6777376fbdbd","trackId":null,"name":"909beat01.ogg","sourcePath":"src/samples/beats/909beat01.ogg","startTimeInSeconds":0,"durationInSeconds":0,"offset":0,"pitch":0,"volume":1,"pan":0,"originalDuration":0},{"id":"01514ea7-ede5-4df8-81f5-01a469e1be1c","trackId":null,"name":"909beat01.ogg","sourcePath":"src/samples/beats/909beat01.ogg","startTimeInSeconds":1.3333333333333333,"durationInSeconds":0,"offset":0,"pitch":0,"volume":1,"pan":0,"originalDuration":0},{"id":"34487014-50f7-40c1-9c8d-30381e1ea54b","trackId":1761494644211,"name":"break02.ogg","sourcePath":"src/samples/beats/break02.ogg","startTimeInSeconds":0.5,"durationInSeconds":0,"offset":0,"pitch":0,"volume":1,"pan":0,"originalDuration":0},{"id":"e40762ee-3672-42f1-bf83-2efaae656777","trackId":1761494703529,"name":"909beat01.ogg","sourcePath":"src/samples/beats/909beat01.ogg","startTimeInSeconds":0.6428571428571428,"durationInSeconds":0,"offset":0,"pitch":0,"volume":1,"pan":0,"originalDuration":0},{"id":"db4ef81a-6ee8-4cbb-8448-d2b5d0a50c7b","trackId":null,"name":"break02.ogg","sourcePath":"src/samples/beats/break02.ogg","startTimeInSeconds":0.75,"durationInSeconds":0,"offset":0,"pitch":0,"volume":1,"pan":0,"originalDuration":0},{"id":"8d588036-4474-4eb7-9ea7-d88868478e55","trackId":1761495115257,"name":"909beat01.ogg","sourcePath":"src/samples/beats/909beat01.ogg","startTimeInSeconds":0,"durationInSeconds":0,"offset":0,"pitch":0,"volume":1,"pan":0,"originalDuration":0},{"id":"5de971f3-ae8b-41b5-a68b-24008783844e","trackId":1761495668600,"name":"909beat01.ogg","sourcePath":"src/samples/beats/909beat01.ogg","startTimeInSeconds":1,"durationInSeconds":0,"offset":0,"pitch":0,"volume":1,"pan":0,"originalDuration":0},{"id":"c9506c85-ae97-4fc6-bbfb-abd193dd187e","trackId":"track_1761496472045_bk9iq61","name":"909beat01.ogg","sourcePath":"src/samples/beats/909beat01.ogg","startTimeInSeconds":0,"durationInSeconds":0,"offset":0,"pitch":0,"volume":1,"pan":0,"originalDuration":0,"__operation":"slice","sliceTimeInTimeline":1.8333333333333333},{"id":"6dd35010-93de-4e4b-9f56-2e02b95f82dc","trackId":"track_1761496507683_5ki5kwa","name":"break01.ogg","sourcePath":"src/samples/beats/break01.ogg","startTimeInSeconds":0,"durationInSeconds":0,"offset":0,"pitch":0,"volume":1,"pan":0,"originalDuration":0},{"id":"1a5cae7a-c54d-4d88-ba4c-6b8d5997ca11","trackId":"track_1761499407995_5maq15z","name":"909beat01.ogg","sourcePath":"src/samples/beats/909beat01.ogg","startTimeInSeconds":0,"durationInSeconds":0,"offset":0,"pitch":0,"volume":1,"pan":0,"originalDuration":0},{"id":"bb7c8b59-6d99-4ae3-adac-2cd473dfd4c8","trackId":"track_1762285083478_7hrl01q","name":"909beat01.ogg","sourcePath":"src/samples/beats/909beat01.ogg","startTimeInSeconds":0.3333333333333333,"durationInSeconds":4,"offset":0,"pitch":0,"volume":1,"pan":0,"originalDuration":0},{"id":"caf9918b-26c8-4265-9154-7ba84c70a828","trackId":"track_1762987134721_r3wuym4","name":"bassdrum01.ogg","sourcePath":"src/samples/drums/bassdrum01.ogg","startTimeInSeconds":0.5,"durationInSeconds":0,"offset":0,"pitch":0,"volume":1,"pan":0,"originalDuration":0},{"id":"e13b5dc8-d746-41f1-9809-3b90caacdd44","trackId":"track_1762987134721_r3wuym4","name":"hihat_opened01.ogg","sourcePath":"src/samples/drums/hihat_opened01.ogg","startTimeInSeconds":3.5,"durationInSeconds":0,"offset":0,"pitch":0,"volume":1,"pan":0,"originalDuration":0},{"id":"6f3b52cd-d97e-4d3e-bc27-2bdceab6e14e","trackId":"track_1763215344149_sm4fldd","name":"hihat_closed04.ogg","sourcePath":"src/samples/drums/hihat_closed04.ogg","startTimeInSeconds":2.357142857142857,"durationInSeconds":0,"offset":0,"pitch":0,"volume":1,"pan":0,"originalDuration":0},{"id":"bounced_1763216137738","trackId":"track_1763216139380_3wqct5l","name":"Pattern 1 (Bounced).wav","sourcePath":"blob:https://alice.ufsj.edu.br/dd985346-551b-4851-9715-cc57dffec2bb","startTimeInSeconds":0,"durationInSeconds":0,"offset":0,"pitch":0,"volume":1,"pan":0,"originalDuration":0}]},"seq":80} \ No newline at end of file