diff --git a/assets/css/style.css b/assets/css/style.css index a3489e4..cdf12bd 100644 --- a/assets/css/style.css +++ b/assets/css/style.css @@ -488,4 +488,14 @@ body.sidebar-hidden .global-toolbar { z-index: 10; pointer-events: none; /* Impede que a agulha intercepte cliques do mouse */ transition: background-color 0.3s; /* Efeito suave ao parar */ +} + +/* Estilo para o botão de loop na barra de ferramentas do editor de áudio */ +#audio-editor-loop-btn { + color: var(--text-dark); /* Cor padrão quando está desligado */ + transition: color 0.2s; +} + +#audio-editor-loop-btn.active { + color: var(--accent-green); /* Cor de destaque quando está ligado */ } \ No newline at end of file diff --git a/assets/js/creations/audio.js b/assets/js/creations/audio.js index ce17269..7bbfde1 100644 --- a/assets/js/creations/audio.js +++ b/assets/js/creations/audio.js @@ -31,7 +31,6 @@ export function initializeAudioContext() { } } -// ... (funções de master volume/pan, formatTime, metronome, sample player, tick, etc. permanecem iguais)... export function updateMasterVolume(volume) { if (mainGainNode) { mainGainNode.gain.setValueAtTime(volume, audioContext.currentTime); @@ -195,23 +194,33 @@ export function togglePlayback() { function animationLoop() { if (!appState.isAudioEditorPlaying || !audioContext) return; + const bpm = parseInt(document.getElementById("bpm-input").value, 10) || 120; const stepsPerSecond = (bpm / 60) * 4; const pixelsPerSecond = stepsPerSecond * PIXELS_PER_STEP; - const totalElapsedTime = (audioContext.currentTime - appState.audioEditorStartTime) + appState.audioEditorPlaybackTime; - const newPositionPx = totalElapsedTime * pixelsPerSecond; - const maxDuration = appState.audioTracks.reduce((max, track) => (track.audioBuffer && track.audioBuffer.duration > max) ? track.audioBuffer.duration : max, 0); - if (totalElapsedTime >= maxDuration && maxDuration > 0) { - stopAudioEditorPlayback(); - appState.audioEditorPlaybackTime = 0; - resetPlayheadVisual(); - return; + + let totalElapsedTime = (audioContext.currentTime - appState.audioEditorStartTime) + appState.audioEditorPlaybackTime; + + const maxDuration = appState.audioTracks.reduce((max, track) => + (track.audioBuffer && track.audioBuffer.duration > max) ? track.audioBuffer.duration : max, 0 + ); + + if (appState.isAudioEditorLoopEnabled && maxDuration > 0) { + totalElapsedTime = totalElapsedTime % maxDuration; + } else { + if (totalElapsedTime >= maxDuration && maxDuration > 0) { + stopAudioEditorPlayback(); + appState.audioEditorPlaybackTime = 0; + resetPlayheadVisual(); + return; + } } + + const newPositionPx = totalElapsedTime * pixelsPerSecond; updatePlayheadVisual(newPositionPx); appState.audioEditorAnimationId = requestAnimationFrame(animationLoop); } -// --- LÓGICA DE REPRODUÇÃO ATUALIZADA --- export function startAudioEditorPlayback() { if (appState.isAudioEditorPlaying || appState.audioTracks.length === 0) return; initializeAudioContext(); @@ -223,25 +232,13 @@ export function startAudioEditorPlayback() { const startTime = audioContext.currentTime; appState.audioEditorStartTime = startTime; - // Verifica se existe alguma faixa no modo "solo" - const isAnyTrackSoloed = appState.audioTracks.some(t => t.isSoloed); - appState.audioTracks.forEach(track => { - // Condições para tocar: - // 1. A faixa deve ter um buffer de áudio. - // 2. A faixa não pode estar mutada. - const canPlay = track.audioBuffer && !track.isMuted; - - // 3. Lógica de solo: - // - Se houver alguma faixa solada, esta faixa TAMBÉM deve estar solada. - // - Se NENHUMA faixa estiver solada, todas podem tocar. - const shouldPlay = isAnyTrackSoloed ? track.isSoloed : true; - - if (canPlay && shouldPlay) { + if (track.audioBuffer && !track.isMuted && track.isSoloed) { if (appState.audioEditorPlaybackTime >= track.audioBuffer.duration) return; const source = audioContext.createBufferSource(); source.buffer = track.audioBuffer; + source.loop = appState.isAudioEditorLoopEnabled; source.connect(track.gainNode); source.start(startTime, appState.audioEditorPlaybackTime); appState.activeAudioSources.push(source); @@ -261,17 +258,35 @@ export function startAudioEditorPlayback() { export function stopAudioEditorPlayback() { if (!appState.isAudioEditorPlaying) return; - const elapsedTime = (audioContext.currentTime - appState.audioEditorStartTime); - appState.audioEditorPlaybackTime += elapsedTime; + + let totalElapsedTime = (audioContext.currentTime - appState.audioEditorStartTime) + appState.audioEditorPlaybackTime; + + const maxDuration = appState.audioTracks.reduce((max, track) => + (track.audioBuffer && track.audioBuffer.duration > max) ? track.audioBuffer.duration : max, 0 + ); + + // --- CORREÇÃO FINAL E ROBUSTA --- + // Sempre aplica o módulo ao salvar o tempo. + // Se não estava em loop, totalElapsedTime < maxDuration, e o módulo não faz nada. + // Se estava em loop, ele corrige o valor para a posição visual correta. + if (maxDuration > 0) { + appState.audioEditorPlaybackTime = totalElapsedTime % maxDuration; + } else { + appState.audioEditorPlaybackTime = totalElapsedTime; + } + // --- FIM DA CORREÇÃO --- + if (appState.audioEditorAnimationId) { cancelAnimationFrame(appState.audioEditorAnimationId); appState.audioEditorAnimationId = null; } + appState.activeAudioSources.forEach(source => { try { source.stop(0); } catch (e) { /* Ignora erros */ } }); + appState.activeAudioSources = []; appState.isAudioEditorPlaying = false; updateAudioEditorUI(); @@ -291,4 +306,11 @@ export function seekAudioEditor(newTime) { if (wasPlaying) { startAudioEditorPlayback(); } +} + +export function restartAudioEditorIfPlaying() { + if (appState.isAudioEditorPlaying) { + stopAudioEditorPlayback(); + startAudioEditorPlayback(); + } } \ No newline at end of file diff --git a/assets/js/creations/main.js b/assets/js/creations/main.js index fb7b049..0395c23 100644 --- a/assets/js/creations/main.js +++ b/assets/js/creations/main.js @@ -13,6 +13,7 @@ import { updateMasterPan, startAudioEditorPlayback, stopAudioEditorPlayback, + restartAudioEditorIfPlaying, } from "./audio.js"; import { handleFileLoad, generateMmpFile } from "./file.js"; import { @@ -21,7 +22,7 @@ import { loadAndRenderSampleBrowser, showOpenProjectModal, closeOpenProjectModal, - handleSampleUpload, // Importa handleSampleUpload, embora não seja mais usado diretamente + handleSampleUpload, } from "./ui.js"; import { adjustValue, enforceNumericInput } from "./utils.js"; import { DEFAULT_PAN, DEFAULT_VOLUME } from "./config.js"; @@ -37,6 +38,7 @@ document.addEventListener("DOMContentLoaded", () => { const stopBtn = document.getElementById("stop-btn"); const audioEditorPlayBtn = document.getElementById("audio-editor-play-btn"); const audioEditorStopBtn = document.getElementById("audio-editor-stop-btn"); + const audioEditorLoopBtn = document.getElementById("audio-editor-loop-btn"); const rewindBtn = document.getElementById("rewind-btn"); const metronomeBtn = document.getElementById("metronome-btn"); const mmpFileInput = document.getElementById("mmp-file-input"); @@ -57,6 +59,7 @@ document.addEventListener("DOMContentLoaded", () => { return; Object.assign(appState, { tracks: [], + audioTracks: [], activeTrackId: null, isPlaying: false, playbackIntervalId: null, @@ -166,8 +169,6 @@ document.addEventListener("DOMContentLoaded", () => { uploadSampleBtn.addEventListener("click", () => sampleFileInput.click()); - // --- INÍCIO DA CORREÇÃO --- - // Lógica de upload de sample para o servidor Flask sampleFileInput.addEventListener("change", async (event) => { const file = event.target.files[0]; if (!file) return; @@ -176,7 +177,6 @@ document.addEventListener("DOMContentLoaded", () => { formData.append("sampleFile", file); try { - // ATENÇÃO: Verifique se a URL e a porta estão corretas para o seu servidor Flask const response = await fetch('http://localhost:5000/upload-sample', { method: 'POST', body: formData, @@ -186,7 +186,6 @@ document.addEventListener("DOMContentLoaded", () => { if (response.ok) { alert("Sample enviado com sucesso!"); - // Recarrega a lista de samples para exibir o novo arquivo await loadAndRenderSampleBrowser(); } else { throw new Error(result.error || "Erro desconhecido no servidor."); @@ -197,9 +196,8 @@ document.addEventListener("DOMContentLoaded", () => { alert(`Falha no upload: ${error.message}`); } - event.target.value = null; // Limpa o input para permitir o mesmo arquivo de novo + event.target.value = null; }); - // --- FIM DA CORREÇÃO --- saveMmpBtn.addEventListener("click", generateMmpFile); addInstrumentBtn.addEventListener("click", addTrackToState); @@ -252,7 +250,6 @@ document.addEventListener("DOMContentLoaded", () => { }); }); - // Listeners para os controles do editor de áudio audioEditorPlayBtn.addEventListener("click", () => { if (appState.isAudioEditorPlaying) { stopAudioEditorPlayback(); @@ -263,6 +260,12 @@ document.addEventListener("DOMContentLoaded", () => { audioEditorStopBtn.addEventListener("click", stopAudioEditorPlayback); + audioEditorLoopBtn.addEventListener("click", () => { + appState.isAudioEditorLoopEnabled = !appState.isAudioEditorLoopEnabled; + audioEditorLoopBtn.classList.toggle("active", appState.isAudioEditorLoopEnabled); + restartAudioEditorIfPlaying(); + }); + loadAndRenderSampleBrowser(); renderApp(); setupMasterKnobs(); diff --git a/assets/js/creations/state.js b/assets/js/creations/state.js index 1418dcc..b2abe24 100644 --- a/assets/js/creations/state.js +++ b/assets/js/creations/state.js @@ -18,7 +18,8 @@ export let appState = { activeAudioSources: [], audioEditorStartTime: 0, audioEditorAnimationId: null, - audioEditorPlaybackTime: 0, + audioEditorPlaybackTime: 0, + isAudioEditorLoopEnabled: false, // <-- ADICIONADO: Estado para controlar o loop playbackIntervalId: null, currentStep: 0, metronomeEnabled: false, diff --git a/creation.html b/creation.html index a2d7824..a9dbea9 100644 --- a/creation.html +++ b/creation.html @@ -125,10 +125,11 @@
+
- -
-
+
+
+
diff --git a/readme.md b/readme.md index 9e9bc11..06741b2 100644 --- a/readme.md +++ b/readme.md @@ -1,69 +1,100 @@ -# ----------------------- // -------------------------- +# mmpCreator - Documentação Técnica -# Área de Desenvolvimento -O site está sendo desenvolvido em um venv, para executá-lo de forma correta, é necessário acessar através do comando: +--- + +## 🛠️ Área de Desenvolvimento + +O site está sendo desenvolvido em um ambiente virtual Python (`venv`). Para executá-lo corretamente, ative o ambiente com o comando: + +```bash source venv/bin/activate -Isso ativará o ambiente de desenvolvimento. +``` -# ----------------------- // -------------------------- +Isso garante que todas as dependências do projeto sejam utilizadas corretamente. -# Serviço Watchdog para verificar alterações nas pastas de samples -Foi criado um serviço (/etc/systemd/system/mmpCreator-upload-server.service) com nome "mmpCreator-upload-server.service" para verificar alterações na pasta src/samples se há alguma mudança. Caso tenha, ele fará um novo build do site para que seja atualizado em tempo real todas as alterações. (tempo de 5 em 5 segundos) -A atualização no site é feita a partir de build do site feito pelo script. +--- -# TO DO -- Verificar caminho do build, pois está sendo feito apenas na parte de testes +## 👁️ Serviço de Monitoramento (Watchdog) -# ----------------------- // -------------------------- +Um serviço systemd foi criado para monitorar alterações na pasta `src/samples`: -# Servidor de Upload de samples -O servidor "upload_server.py" é responsável por receber o upload dos arquivos de áudio (samples) para a plataforma. -É um servidor Flask, utilizando a biblioteca watchdog do Python para monitorar as modificações nas pastas de samples. -Após qualquer alteração identificada, o script fará a atualização dos caminhos de arquivos de áudio através dos json (samples-manifest.json e mmp-manifest.json) para que todos os arquivos tenham sua exibição correta, assim como sua execução. +- **Serviço:** `/etc/systemd/system/mmpCreator-upload-server.service` +- **Nome do serviço:** `mmpCreator-upload-server.service` +- **Frequência:** A cada 5 segundos -http://127.0.0.1:5000 -É necessário verificar o quão seguro isso é, para que seja validado como ferramenta oficial ou não do projeto. -Inicialmente, está em produção para fazer testes. +### Funcionalidade: -# ----------------------- // -------------------------- +Sempre que houver mudanças na pasta de samples, o serviço irá: -# TODO -- Verificar projeto MMP antes de aceitar o upload - - Verificar se são samples nativos; - - Corrigir caminhos dos samples para que não haja problemas durante o download e execução na plataforma. -- Envio de samples gravados com o microfone do dispositivo - - Possibilitar a execução e recorte do sample antes de salvá-lo de fato no servidor (evitar muito lixo salvo) - - Possibilita a gravação de músicas na plataforma, mesmo que sejam sem mixagem. -- Ativar e desativar samples nas patterns, para executar apenas samples específicos ou alternar entre ativo e inativo -- Retirar botões inúteis do controle de patterns -- Deixar o código mais bem documentado, facilitando o entendimento de externos ao desenvolvimento -- Possibilitar a alteração dos nomes das patterns (tanto em novos projetos, quanto em projetos que foram baixados/carregados) -- Corrigir a parte de notas do projeto, deixando rastros da plataforma MMPCreator, informações de autor e etc -- Adicionar atalhos, espaço pra play/pause, + e - para alternar entre as patterns, etc -- Possibilitar a alteração do nome do sample (?) (não sei se é tão válido, porque pode gerar inúmeros samples repetidos) -- Verificar se já há algum sample com o mesmo nome, para não ficar duplicando samples -- Verificar se já há algum projeto com o mesmo nome, para não ficar duplicando projetos -- Resetar valores dos botões utilizando o scroll do mouse -- Na hora de salvar projeto, verificar se quer baixar o projeto (possibilitando escolher o nome) ou salvar no servidor +1. Detectar alterações via `watchdog`. +2. Executar um novo build do site. +3. Atualizar automaticamente o conteúdo do site em tempo real. -# ----------------------- // -------------------------- +> **Nota:** O build atual está sendo feito apenas na parte de testes. Verificar e ajustar o caminho correto do build. -# O que já é possível fazer -- Criar projeto; -- Carregar projeto do PC ou do Servidor; -- Salvar projeto de forma externa (no servidor ainda não tá salvando); -- Editar projetos já existentes (não alterando o original); -- Usar o metrônomo; -- Criar patterns; -- Excluir patterns; -- Alternar entre as patterns em tempo real; -- Aumentar/diminuir volume das patterns; -- Alterar pan das patterns; -- Aumentar/diminuir volume dos instrumentos; -- Alterar pan dos instrumentos; -- Aumentar/diminuir volume global; -- Alterar pan global; -- Alterar a quantidade de compassos da música; -- Alterar o formato do compasso da música; -- Alterar o BPM; -- Ver o tempo de duração da pattern através do timmer. +--- + +## 📤 Servidor de Upload de Samples + +O servidor `upload_server.py` é responsável por receber arquivos de áudio (samples) para a plataforma. Ele é construído com: + +- **Framework:** Flask +- **Monitoramento:** Biblioteca `watchdog` + +### Funcionamento: + +1. Após qualquer modificação nas pastas de samples: + - O script atualiza os arquivos `samples-manifest.json` e `mmp-manifest.json`. + - Os caminhos dos arquivos são ajustados para garantir exibição e execução corretas. + +### Acesso: + +- `http://127.0.0.1:5000` + +> ⚠️ **Importante:** Avaliar a segurança do servidor antes de validar como ferramenta oficial do projeto. Atualmente está sendo usado apenas para testes. + +--- + +## ✅ Funcionalidades Já Implementadas + +- [x] Criar projetos +- [x] Carregar projetos (do PC ou do servidor) +- [x] Salvar projetos (externamente) +- [x] Editar projetos existentes (sem alterar o original) +- [x] Utilizar metrônomo +- [x] Criar e excluir patterns +- [x] Alternar entre patterns em tempo real +- [x] Controle de volume e pan por pattern e por instrumento +- [x] Controle de volume e pan global +- [x] Alterar quantidade e formato de compassos +- [x] Alterar BPM +- [x] Exibir duração da pattern via timer + +--- + +## 📋 TODO (Próximos Passos) + +### 🛠️ Funcionalidades + +- [ ] Validar projetos MMP antes de aceitar uploads: + - [ ] Verificar se os samples são nativos + - [ ] Corrigir caminhos dos samples (evitar problemas de execução) +- [ ] Permitir envio de samples gravados com microfone: + - [ ] Executar e recortar antes de salvar no servidor + - [ ] Gravar músicas diretamente na plataforma, mesmo sem mixagem +- [ ] Ativar/desativar samples em patterns +- [ ] Remover botões inúteis no controle de patterns +- [ ] Melhorar documentação do código +- [ ] Permitir alteração dos nomes das patterns (novos e existentes projetos) +- [ ] Corrigir sistema de notas do projeto: + - [ ] Incluir rastros da plataforma MMPCreator + - [ ] Informações de autor e metadados +- [ ] Criar atalhos de teclado: + - [ ] Barra de espaço para play/pause + - [ ] `+` e `-` para alternar entre patterns +- [ ] Permitir alteração de nome de samples (⚠️ pode gerar duplicatas) +- [ ] Verificar duplicidade de samples e projetos por nome +- [ ] Resetar valores de botões via scroll do mouse +- [ ] Ao salvar projeto: + - [ ] Permitir escolha entre download ou salvar no servidor + - [ ] Escolher nome do projeto