Amostras de áudio funcionais até o momento. Agulha de tempo, mute/solo e ajustado com a velocidade de reprodução, loop funcional.
Deploy / Deploy (push) Successful in 48s Details

This commit is contained in:
JotaChina 2025-10-12 10:42:27 -03:00
parent facc329b03
commit d77fe91df1
6 changed files with 163 additions and 95 deletions

View File

@ -489,3 +489,13 @@ body.sidebar-hidden .global-toolbar {
pointer-events: none; /* Impede que a agulha intercepte cliques do mouse */ pointer-events: none; /* Impede que a agulha intercepte cliques do mouse */
transition: background-color 0.3s; /* Efeito suave ao parar */ 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 */
}

View File

@ -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) { export function updateMasterVolume(volume) {
if (mainGainNode) { if (mainGainNode) {
mainGainNode.gain.setValueAtTime(volume, audioContext.currentTime); mainGainNode.gain.setValueAtTime(volume, audioContext.currentTime);
@ -195,23 +194,33 @@ export function togglePlayback() {
function animationLoop() { function animationLoop() {
if (!appState.isAudioEditorPlaying || !audioContext) return; if (!appState.isAudioEditorPlaying || !audioContext) return;
const bpm = parseInt(document.getElementById("bpm-input").value, 10) || 120; const bpm = parseInt(document.getElementById("bpm-input").value, 10) || 120;
const stepsPerSecond = (bpm / 60) * 4; const stepsPerSecond = (bpm / 60) * 4;
const pixelsPerSecond = stepsPerSecond * PIXELS_PER_STEP; const pixelsPerSecond = stepsPerSecond * PIXELS_PER_STEP;
const totalElapsedTime = (audioContext.currentTime - appState.audioEditorStartTime) + appState.audioEditorPlaybackTime;
const newPositionPx = totalElapsedTime * pixelsPerSecond; 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);
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) { if (totalElapsedTime >= maxDuration && maxDuration > 0) {
stopAudioEditorPlayback(); stopAudioEditorPlayback();
appState.audioEditorPlaybackTime = 0; appState.audioEditorPlaybackTime = 0;
resetPlayheadVisual(); resetPlayheadVisual();
return; return;
} }
}
const newPositionPx = totalElapsedTime * pixelsPerSecond;
updatePlayheadVisual(newPositionPx); updatePlayheadVisual(newPositionPx);
appState.audioEditorAnimationId = requestAnimationFrame(animationLoop); appState.audioEditorAnimationId = requestAnimationFrame(animationLoop);
} }
// --- LÓGICA DE REPRODUÇÃO ATUALIZADA ---
export function startAudioEditorPlayback() { export function startAudioEditorPlayback() {
if (appState.isAudioEditorPlaying || appState.audioTracks.length === 0) return; if (appState.isAudioEditorPlaying || appState.audioTracks.length === 0) return;
initializeAudioContext(); initializeAudioContext();
@ -223,25 +232,13 @@ export function startAudioEditorPlayback() {
const startTime = audioContext.currentTime; const startTime = audioContext.currentTime;
appState.audioEditorStartTime = startTime; appState.audioEditorStartTime = startTime;
// Verifica se existe alguma faixa no modo "solo"
const isAnyTrackSoloed = appState.audioTracks.some(t => t.isSoloed);
appState.audioTracks.forEach(track => { appState.audioTracks.forEach(track => {
// Condições para tocar: if (track.audioBuffer && !track.isMuted && track.isSoloed) {
// 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 (appState.audioEditorPlaybackTime >= track.audioBuffer.duration) return; if (appState.audioEditorPlaybackTime >= track.audioBuffer.duration) return;
const source = audioContext.createBufferSource(); const source = audioContext.createBufferSource();
source.buffer = track.audioBuffer; source.buffer = track.audioBuffer;
source.loop = appState.isAudioEditorLoopEnabled;
source.connect(track.gainNode); source.connect(track.gainNode);
source.start(startTime, appState.audioEditorPlaybackTime); source.start(startTime, appState.audioEditorPlaybackTime);
appState.activeAudioSources.push(source); appState.activeAudioSources.push(source);
@ -261,17 +258,35 @@ export function startAudioEditorPlayback() {
export function stopAudioEditorPlayback() { export function stopAudioEditorPlayback() {
if (!appState.isAudioEditorPlaying) return; 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) { if (appState.audioEditorAnimationId) {
cancelAnimationFrame(appState.audioEditorAnimationId); cancelAnimationFrame(appState.audioEditorAnimationId);
appState.audioEditorAnimationId = null; appState.audioEditorAnimationId = null;
} }
appState.activeAudioSources.forEach(source => { appState.activeAudioSources.forEach(source => {
try { try {
source.stop(0); source.stop(0);
} catch (e) { /* Ignora erros */ } } catch (e) { /* Ignora erros */ }
}); });
appState.activeAudioSources = []; appState.activeAudioSources = [];
appState.isAudioEditorPlaying = false; appState.isAudioEditorPlaying = false;
updateAudioEditorUI(); updateAudioEditorUI();
@ -292,3 +307,10 @@ export function seekAudioEditor(newTime) {
startAudioEditorPlayback(); startAudioEditorPlayback();
} }
} }
export function restartAudioEditorIfPlaying() {
if (appState.isAudioEditorPlaying) {
stopAudioEditorPlayback();
startAudioEditorPlayback();
}
}

View File

@ -13,6 +13,7 @@ import {
updateMasterPan, updateMasterPan,
startAudioEditorPlayback, startAudioEditorPlayback,
stopAudioEditorPlayback, stopAudioEditorPlayback,
restartAudioEditorIfPlaying,
} from "./audio.js"; } from "./audio.js";
import { handleFileLoad, generateMmpFile } from "./file.js"; import { handleFileLoad, generateMmpFile } from "./file.js";
import { import {
@ -21,7 +22,7 @@ import {
loadAndRenderSampleBrowser, loadAndRenderSampleBrowser,
showOpenProjectModal, showOpenProjectModal,
closeOpenProjectModal, closeOpenProjectModal,
handleSampleUpload, // Importa handleSampleUpload, embora não seja mais usado diretamente handleSampleUpload,
} from "./ui.js"; } from "./ui.js";
import { adjustValue, enforceNumericInput } from "./utils.js"; import { adjustValue, enforceNumericInput } from "./utils.js";
import { DEFAULT_PAN, DEFAULT_VOLUME } from "./config.js"; import { DEFAULT_PAN, DEFAULT_VOLUME } from "./config.js";
@ -37,6 +38,7 @@ document.addEventListener("DOMContentLoaded", () => {
const stopBtn = document.getElementById("stop-btn"); const stopBtn = document.getElementById("stop-btn");
const audioEditorPlayBtn = document.getElementById("audio-editor-play-btn"); const audioEditorPlayBtn = document.getElementById("audio-editor-play-btn");
const audioEditorStopBtn = document.getElementById("audio-editor-stop-btn"); const audioEditorStopBtn = document.getElementById("audio-editor-stop-btn");
const audioEditorLoopBtn = document.getElementById("audio-editor-loop-btn");
const rewindBtn = document.getElementById("rewind-btn"); const rewindBtn = document.getElementById("rewind-btn");
const metronomeBtn = document.getElementById("metronome-btn"); const metronomeBtn = document.getElementById("metronome-btn");
const mmpFileInput = document.getElementById("mmp-file-input"); const mmpFileInput = document.getElementById("mmp-file-input");
@ -57,6 +59,7 @@ document.addEventListener("DOMContentLoaded", () => {
return; return;
Object.assign(appState, { Object.assign(appState, {
tracks: [], tracks: [],
audioTracks: [],
activeTrackId: null, activeTrackId: null,
isPlaying: false, isPlaying: false,
playbackIntervalId: null, playbackIntervalId: null,
@ -166,8 +169,6 @@ document.addEventListener("DOMContentLoaded", () => {
uploadSampleBtn.addEventListener("click", () => sampleFileInput.click()); 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) => { sampleFileInput.addEventListener("change", async (event) => {
const file = event.target.files[0]; const file = event.target.files[0];
if (!file) return; if (!file) return;
@ -176,7 +177,6 @@ document.addEventListener("DOMContentLoaded", () => {
formData.append("sampleFile", file); formData.append("sampleFile", file);
try { 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', { const response = await fetch('http://localhost:5000/upload-sample', {
method: 'POST', method: 'POST',
body: formData, body: formData,
@ -186,7 +186,6 @@ document.addEventListener("DOMContentLoaded", () => {
if (response.ok) { if (response.ok) {
alert("Sample enviado com sucesso!"); alert("Sample enviado com sucesso!");
// Recarrega a lista de samples para exibir o novo arquivo
await loadAndRenderSampleBrowser(); await loadAndRenderSampleBrowser();
} else { } else {
throw new Error(result.error || "Erro desconhecido no servidor."); throw new Error(result.error || "Erro desconhecido no servidor.");
@ -197,9 +196,8 @@ document.addEventListener("DOMContentLoaded", () => {
alert(`Falha no upload: ${error.message}`); 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); saveMmpBtn.addEventListener("click", generateMmpFile);
addInstrumentBtn.addEventListener("click", addTrackToState); addInstrumentBtn.addEventListener("click", addTrackToState);
@ -252,7 +250,6 @@ document.addEventListener("DOMContentLoaded", () => {
}); });
}); });
// Listeners para os controles do editor de áudio
audioEditorPlayBtn.addEventListener("click", () => { audioEditorPlayBtn.addEventListener("click", () => {
if (appState.isAudioEditorPlaying) { if (appState.isAudioEditorPlaying) {
stopAudioEditorPlayback(); stopAudioEditorPlayback();
@ -263,6 +260,12 @@ document.addEventListener("DOMContentLoaded", () => {
audioEditorStopBtn.addEventListener("click", stopAudioEditorPlayback); audioEditorStopBtn.addEventListener("click", stopAudioEditorPlayback);
audioEditorLoopBtn.addEventListener("click", () => {
appState.isAudioEditorLoopEnabled = !appState.isAudioEditorLoopEnabled;
audioEditorLoopBtn.classList.toggle("active", appState.isAudioEditorLoopEnabled);
restartAudioEditorIfPlaying();
});
loadAndRenderSampleBrowser(); loadAndRenderSampleBrowser();
renderApp(); renderApp();
setupMasterKnobs(); setupMasterKnobs();

View File

@ -19,6 +19,7 @@ export let appState = {
audioEditorStartTime: 0, audioEditorStartTime: 0,
audioEditorAnimationId: null, audioEditorAnimationId: null,
audioEditorPlaybackTime: 0, audioEditorPlaybackTime: 0,
isAudioEditorLoopEnabled: false, // <-- ADICIONADO: Estado para controlar o loop
playbackIntervalId: null, playbackIntervalId: null,
currentStep: 0, currentStep: 0,
metronomeEnabled: false, metronomeEnabled: false,

View File

@ -125,6 +125,7 @@
<div class="playback-controls"> <div class="playback-controls">
<i class="fa-solid fa-play" id="audio-editor-play-btn" title="Play/Pause"></i> <i class="fa-solid fa-play" id="audio-editor-play-btn" title="Play/Pause"></i>
<i class="fa-solid fa-stop" id="audio-editor-stop-btn" title="Stop"></i> <i class="fa-solid fa-stop" id="audio-editor-stop-btn" title="Stop"></i>
<i class="fa-solid fa-repeat" id="audio-editor-loop-btn" title="Ativar/Desativar Loop"></i>
</div> </div>
</div> </div>
<div id="audio-track-container"> <div id="audio-track-container">

145
readme.md
View File

@ -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 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 ## 👁️ Serviço de Monitoramento (Watchdog)
- Verificar caminho do build, pois está sendo feito apenas na parte de testes
# ----------------------- // -------------------------- Um serviço systemd foi criado para monitorar alterações na pasta `src/samples`:
# Servidor de Upload de samples - **Serviço:** `/etc/systemd/system/mmpCreator-upload-server.service`
O servidor "upload_server.py" é responsável por receber o upload dos arquivos de áudio (samples) para a plataforma. - **Nome do serviço:** `mmpCreator-upload-server.service`
É um servidor Flask, utilizando a biblioteca watchdog do Python para monitorar as modificações nas pastas de samples. - **Frequência:** A cada 5 segundos
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.
http://127.0.0.1:5000 ### Funcionalidade:
É 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.
# ----------------------- // -------------------------- Sempre que houver mudanças na pasta de samples, o serviço irá:
# TODO 1. Detectar alterações via `watchdog`.
- Verificar projeto MMP antes de aceitar o upload 2. Executar um novo build do site.
- Verificar se são samples nativos; 3. Atualizar automaticamente o conteúdo do site em tempo real.
- 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
# ----------------------- // -------------------------- > **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; ## 📤 Servidor de Upload de Samples
- Salvar projeto de forma externa (no servidor ainda não tá salvando);
- Editar projetos já existentes (não alterando o original); O servidor `upload_server.py` é responsável por receber arquivos de áudio (samples) para a plataforma. Ele é construído com:
- Usar o metrônomo;
- Criar patterns; - **Framework:** Flask
- Excluir patterns; - **Monitoramento:** Biblioteca `watchdog`
- Alternar entre as patterns em tempo real;
- Aumentar/diminuir volume das patterns; ### Funcionamento:
- Alterar pan das patterns;
- Aumentar/diminuir volume dos instrumentos; 1. Após qualquer modificação nas pastas de samples:
- Alterar pan dos instrumentos; - O script atualiza os arquivos `samples-manifest.json` e `mmp-manifest.json`.
- Aumentar/diminuir volume global; - Os caminhos dos arquivos são ajustados para garantir exibição e execução corretas.
- Alterar pan global;
- Alterar a quantidade de compassos da música; ### Acesso:
- Alterar o formato do compasso da música;
- Alterar o BPM; - `http://127.0.0.1:5000`
- Ver o tempo de duração da pattern através do timmer.
> ⚠️ **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