diff --git a/assets/css/style.css b/assets/css/style.css
index cdf12bd..6592aa6 100644
--- a/assets/css/style.css
+++ b/assets/css/style.css
@@ -1,6 +1,4 @@
-/* =============================================== */
-/* VÁRIAVEIS GLOBAIS (ROOT)
-/* =============================================== */
+/* MMPCreator - Folha de Estilos Principal */
:root {
--bg-body: #2d3035;
--bg-toolbar: #3b3f45;
@@ -17,90 +15,115 @@
}
/* =============================================== */
-/* ESTILOS GLOBAIS E LAYOUT PRINCIPAL (CORRIGIDO)
+/* LAYOUT E ESTRUTURA GLOBAL
/* =============================================== */
body {
margin: 0;
- font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
background-color: var(--bg-body);
color: var(--text-light);
- /* Retornamos ao layout com padding para compatibilidade */
- padding-left: 300px;
- padding-top: 50px; /* Adiciona espaço para a toolbar fixa */
- box-sizing: border-box;
- transition: padding-left .3s ease;
height: 100vh;
- display: flex; /* Usamos flex no body para o main-content crescer */
- flex-direction: column;
+ overflow: hidden;
+ display: flex;
}
-body.sidebar-hidden {
- padding-left: 0;
-}
+body.knob-dragging { cursor: ns-resize; }
+body.slice-tool-active .timeline-container { cursor: crosshair; }
-body.knob-dragging {
- cursor: ns-resize;
-}
-
-.main-content {
- padding: 1rem;
- flex-grow: 1; /* Faz o conteúdo principal ocupar o espaço restante */
+.app-container {
display: flex;
flex-direction: column;
- gap: 1rem;
- overflow: hidden; /* Evita que o conteúdo transborde */
- height: 100%; /* Garante que o flexbox interno funcione */
+ flex-grow: 1;
+ overflow: hidden;
+ position: relative;
}
/* =============================================== */
/* BARRA LATERAL (SAMPLE BROWSER)
/* =============================================== */
.sample-browser {
- position: fixed;
- top: 0;
- left: 0;
width: 300px;
- height: 100vh;
background-color: var(--bg-toolbar);
border-right: 2px solid var(--border-color);
- z-index: 1500;
+ z-index: 10;
display: flex;
flex-direction: column;
- transform: translateX(0);
- transition: transform .3s ease;
+ transition: min-width 0.3s ease, width 0.3s ease;
+ flex-shrink: 0;
}
-
body.sidebar-hidden .sample-browser {
- transform: translateX(-100%);
+ width: 0;
+ min-width: 0;
+ border-right: none;
+ overflow: hidden;
}
-
.browser-header {
padding: 15px;
background-color: #2a2c30;
border-bottom: 2px solid var(--border-color);
text-align: center;
font-weight: bold;
- color: var(--text-light);
+ white-space: nowrap;
}
-
.browser-content {
flex-grow: 1;
overflow-y: auto;
padding: 10px;
}
-
-.browser-content ul { list-style: none; padding-left: 15px; }
-.browser-content li { padding: 5px; cursor: pointer; border-radius: 3px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; user-select: none; }
-.browser-content li:hover { background-color: var(--bg-editor); }
-.browser-content li i { margin-right: 8px; width: 12px; color: var(--text-dark); transition: transform .2s; }
-.browser-content li.directory > ul { display: none; }
-.browser-content li.directory.open > ul { display: block; }
-.browser-content li.directory.open > .fa-folder { transform: rotate(90deg); }
-
+.browser-content ul {
+ padding-left: 15px;
+ list-style: none;
+}
+.folder-item > .file-list {
+ display: none;
+ padding-left: 20px;
+}
+.folder-item.open > .file-list {
+ display: block;
+}
+.folder-name {
+ cursor: pointer;
+ display: flex;
+ align-items: center;
+ padding: 5px;
+ border-radius: 3px;
+ user-select: none;
+}
+.folder-name:hover {
+ background-color: var(--bg-editor);
+}
+.folder-icon {
+ margin-right: 8px;
+ color: var(--text-dark);
+ transition: transform 0.2s ease-in-out;
+}
+.folder-name::before {
+ content: '\f0da';
+ font-family: "Font Awesome 6 Free";
+ font-weight: 900;
+ margin-right: 8px;
+ font-size: 0.9em;
+ transition: transform 0.2s ease-in-out;
+}
+.folder-item.open > .folder-name::before {
+ transform: rotate(90deg);
+}
+.folder-item.open > .folder-name > .folder-icon::before {
+ content: '\f07c';
+}
+.browser-content li.file-item {
+ padding: 5px;
+ cursor: pointer;
+ border-radius: 3px;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ user-select: none;
+}
#sidebar-toggle {
- position: fixed;
+ position: absolute;
top: 60px;
- left: 305px;
+ left: 5px;
z-index: 1400;
background-color: var(--bg-toolbar);
border: 1px solid var(--border-color);
@@ -108,49 +131,19 @@ body.sidebar-hidden .sample-browser {
width: 25px;
height: 40px;
cursor: pointer;
- border-top-right-radius: 4px;
- border-bottom-right-radius: 4px;
- transition: left .3s ease;
+ border-radius: 0 4px 4px 0;
display: flex;
align-items: center;
justify-content: center;
}
-body.sidebar-hidden #sidebar-toggle {
- left: 5px;
-}
-
/* =============================================== */
-/* BARRA DE FERRAMENTAS GLOBAL
+/* ÁREA DE CONTEÚDO E TOOLBARS
/* =============================================== */
-.global-toolbar {
- padding: 8px 15px;
- position: fixed;
- top: 0;
- left: 300px;
- right: 0;
- z-index: 1000;
- display: flex;
- align-items: center;
- gap: 20px;
- background-color: var(--bg-toolbar);
- border-bottom: 2px solid var(--border-color);
- transition: left .3s ease;
- height: 50px; /* Altura fixa para o cálculo do padding do body */
- box-sizing: border-box;
-}
-
-body.sidebar-hidden .global-toolbar {
- left: 0;
-}
-
-/* =============================================== */
-/* NOVO: EDITOR DE AMOSTRAS DE ÁUDIO (AUDIO EDITOR)
-/* =============================================== */
-
-/* O container principal que substitui o .future-panel */
-.audio-editor {
- height: 50%;
+.global-toolbar { display: flex; align-items: center; gap: 20px; padding: 8px 15px; background-color: var(--bg-toolbar); border-bottom: 2px solid var(--border-color); height: 50px; box-sizing: border-box; flex-shrink: 0; }
+.main-content { flex-grow: 1; padding: 1rem; display: flex; flex-direction: column; gap: 1rem; overflow: hidden; }
+.beat-editor {
+ flex: 1;
background-color: var(--bg-editor);
border: 1px solid var(--border-color);
border-radius: 8px;
@@ -159,178 +152,43 @@ body.sidebar-hidden .global-toolbar {
display: flex;
flex-direction: column;
}
-
-/* Container para as faixas de áudio, com scroll vertical */
-#audio-track-container {
- overflow-y: auto;
- flex-grow: 1;
-}
-
-/* Estilo para cada linha de faixa de áudio */
-.audio-track-lane {
- display: flex;
- align-items: center;
- padding: 8px 10px;
- background-color: var(--bg-editor);
- border-bottom: 1px solid var(--bg-toolbar);
- min-height: 40px; /* Altura mínima para cada faixa */
- box-sizing: border-box;
-}
+.editor-header { background-color: var(--bg-toolbar); padding: 4px 10px; font-size: .8rem; display: flex; justify-content: space-between; align-items: center; border-bottom: 1px solid var(--border-color); flex-shrink: 0; }
+.editor-toolbar { background-color: var(--bg-toolbar); padding: 5px 10px; border-bottom: 2px solid var(--border-color); flex-shrink: 0; display: flex; align-items: center; gap: 15px; }
/* =============================================== */
-/* ESTILOS DO EDITOR DE ÁUDIO (MARCADORES)
-/* =============================================== */
-
-/* Wrapper para a visualização do espectrograma, permite scroll horizontal */
-.spectrogram-view-wrapper {
- flex-grow: 1;
- overflow-x: auto;
- overflow-y: hidden;
- background-color: #2a2c30;
- border-radius: 3px;
-}
-
-/* Garante que o grid possa conter elementos posicionados de forma absoluta */
-.spectrogram-view-grid {
- position: relative;
- display: inline-block; /* Faz o contêiner se ajustar à largura do canvas */
- height: 100%;
-}
-
-/* Estilo para os números de compasso */
-.bar-marker {
- position: absolute;
- top: 0;
- transform: translateX(-50%); /* Centraliza o número sobre a linha */
- background-color: rgba(0, 0, 0, 0.5);
- color: var(--text-dark);
- padding: 1px 5px;
- font-size: 0.7rem;
- border-radius: 3px;
- user-select: none; /* Impede que o texto seja selecionado */
- z-index: 5; /* Garante que fique acima da forma de onda mas abaixo da agulha */
-}
-
-/* Mantém o canvas como block para evitar espaçamentos */
-.waveform-canvas {
- display: block;
-}
-
-/* =============================================== */
-/* TOOLBAR DO EDITOR
-/* =============================================== */
-.editor-header {
- background-color: var(--bg-toolbar);
- padding: 4px 10px;
- font-size: .8rem;
- display: flex;
- justify-content: space-between;
- align-items: center;
- border-bottom: 1px solid var(--border-color);
- flex-shrink: 0;
-}
-
-.window-controls i { margin-left: 12px; cursor: pointer; }
-
-.editor-toolbar, .editor-header .playback-controls {
- display: flex;
- align-items: center;
- gap: 15px;
-}
-
-.editor-toolbar {
- background-color: var(--bg-toolbar);
- padding: 5px 10px;
- border-bottom: 2px solid var(--border-color);
- flex-shrink: 0;
-}
-
-.editor-toolbar i, .editor-header .playback-controls i {
- cursor: pointer;
- padding: 5px;
- border-radius: 3px;
- font-size: 1rem;
-}
-
-.editor-toolbar i.enabled { background-color: var(--bg-body); box-shadow: inset 0 0 2px #000; }
-
-.pattern-manager { display: flex; align-items: center; gap: 10px; }
-.pattern-selector { background-color: var(--bg-body); color: var(--text-light); padding: 5px 10px; border: 1px solid var(--border-color); font-size: .9rem; border-radius: 2px; }
-.pattern-btn { background: var(--bg-body); border: 1px solid var(--border-color); color: var(--text-light); cursor: pointer; border-radius: 3px; width: 28px; height: 28px; font-size: 1.2rem; }
-
-/* =============================================== */
-/* FAIXAS (TRACK LANES) E SEQUENCIADOR
+/* EDITOR DE BASES (BEAT EDITOR / STEP SEQUENCER)
/* =============================================== */
#track-container {
overflow-y: auto;
+ overflow-x: hidden;
flex-grow: 1;
}
-
.track-lane {
display: flex;
- align-items: center;
- padding: 8px 10px;
+ align-items: stretch;
background-color: var(--bg-editor);
border-bottom: 1px solid var(--bg-toolbar);
- border-left: 2px solid transparent;
- border-right: 2px solid transparent;
- transition: border-color 0.2s, background-color 0.2s;
+ min-height: 72px;
+ box-sizing: border-box;
}
-
-.track-lane.active-track {
- background-color: #40454d;
+.track-lane.active-track { background-color: #40454d; }
+.track-lane.drag-over { border: 2px dashed var(--accent-green); }
+.track-lane .track-info {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ width: 180px;
+ flex-shrink: 0;
+ padding: 0 10px;
+ border-right: 1px solid var(--bg-toolbar);
}
-
-.track-lane.drag-over {
- border-color: var(--accent-green);
-}
-
-/* Localize a regra .track-mute e substitua por esta */
-.track-solo-btn {
- width: 25px;
- height: 12px;
- background-color: var(--accent-red); /* Cor padrão: vermelho */
- border-radius: 6px;
- cursor: pointer;
- border: 1px solid var(--border-color);
- box-shadow: inset 0 0 2px #000;
- transition: background-color 0.2s, opacity 0.2s;
-}
-
-.track-solo-btn:hover {
- opacity: 0.8;
-}
-
-/* Quando solado (ativo), o botão fica verde */
-.track-solo-btn.active {
- background-color: var(--accent-green);
- opacity: 1;
-}
-
-.track-info { display: flex; align-items: center; gap: 8px; width: 180px; flex-shrink: 0; }
-.track-info .fa-gear { font-size: 1.2rem; cursor: pointer; }
.track-mute { width: 25px; height: 12px; background-color: var(--accent-green); border-radius: 6px; cursor: pointer; border: 1px solid var(--border-color); box-shadow: inset 0 0 2px #000; transition: background-color 0.2s, opacity 0.2s; }
-.track-mute:hover {
- opacity: 0.8;
-}
-.track-mute.active {
- background-color: var(--text-dark);
- opacity: 0.7;
-}
-.track-name { color: var(--accent-red); font-weight: 700; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
-.track-controls { display: flex; gap: 5px; margin: 0 10px; padding-left: 10px; border-left: 1px solid var(--bg-toolbar); flex-shrink: 0; }
-.knob-container { text-align: center; font-size: .7rem; color: var(--text-dark); }
-.knob { width: 28px; height: 28px; background-color: var(--bg-toolbar); border-radius: 50%; border: 1px solid var(--border-color); margin-bottom: 2px; cursor: grab; box-shadow: inset 0 0 4px #222; position: relative; }
-.knob:active { cursor: grabbing; }
-.knob-indicator { width: 2px; height: 8px; background-color: var(--text-light); position: absolute; top: 2px; left: 50%; transform-origin: bottom center; transform: translateX(-50%) rotate(0deg); border-radius: 1px; }
-
-.step-sequencer-wrapper { flex-grow: 1; overflow-x: auto; overflow-y: hidden; padding-bottom: 8px; }
+.track-mute:hover { opacity: 0.8; }
+.track-mute.active { background-color: var(--text-dark); opacity: 0.7; }
+.track-lane .track-controls { display: flex; gap: 5px; margin: 0 10px; padding-left: 10px; border-left: 1px solid var(--bg-toolbar); flex-shrink: 0; }
+.step-sequencer-wrapper { flex-grow: 1; overflow-x: auto; overflow-y: hidden; padding-bottom: 8px; display: flex; align-items: center; }
.step-sequencer { display: flex; gap: 4px; }
-.step-sequencer-wrapper::-webkit-scrollbar { height: 8px; }
-.step-sequencer-wrapper::-webkit-scrollbar-track { background: var(--border-color); border-radius: 4px; }
-.step-sequencer-wrapper::-webkit-scrollbar-thumb { background: var(--bg-toolbar); border-radius: 4px; }
-.step-sequencer-wrapper::-webkit-scrollbar-thumb:hover { background: #555; }
.step-wrapper { position: relative; }
.step-marker { position: absolute; top: -16px; left: 1px; font-size: .6rem; color: var(--text-dark); user-select: none; }
.step { width: 28px; height: 28px; background-color: #2a2a2a; border: 1px solid #4a4a4a; border-radius: 2px; cursor: pointer; transition: background-color .1s, transform 0.1s; flex-shrink: 0; }
@@ -339,9 +197,161 @@ body.sidebar-hidden .global-toolbar {
.step.active { background-color: var(--accent-green); border: 1px solid #fff; box-shadow: 0 0 8px var(--accent-green); }
.step.playing { transform: scale(1.1); box-shadow: inset 0 0 8px rgba(255, 255, 255, 0.8); }
+/* =================================================================== */
+/* EDITOR DE ÁUDIO - LAYOUT PRINCIPAL
+/* =================================================================== */
+.audio-editor {
+ flex: 1;
+ background-color: var(--bg-editor);
+ border: 1px solid var(--border-color);
+ border-radius: 8px;
+ box-shadow: 0 5px 15px rgba(0, 0, 0, .3);
+ overflow: hidden;
+ display: flex;
+ flex-direction: column;
+ --track-info-width: 255px;
+}
+#audio-track-container {
+ flex-grow: 1; /* Ocupa o espaço restante */
+ overflow: auto; /* Habilita a rolagem horizontal e vertical */
+}
+.audio-track-lane {
+ display: flex;
+ flex-direction: row;
+ align-items: stretch;
+ background-color: var(--bg-editor);
+ border-bottom: 1px solid var(--bg-toolbar);
+ min-height: 90px;
+ box-sizing: border-box;
+}
+.audio-track-lane.drag-over { background-color: #40454d; }
+.audio-track-lane .track-info {
+ width: var(--track-info-width);
+ flex-shrink: 0;
+ display: flex;
+ flex-direction: column;
+ justify-content: space-between;
+ background-color: #383c42;
+ border-right: 2px solid var(--border-color);
+}
+.track-info-header { display: flex; align-items: center; gap: 8px; width: 100%; }
+.track-name { color: var(--accent-red); font-weight: 700; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
+.timeline-container {
+ flex-grow: 1;
+ position: relative;
+ overflow-x: hidden; /* A rolagem agora é controlada pelo #audio-track-container */
+ overflow-y: hidden;
+}
+.spectrogram-view-grid {
+ height: 100%;
+ position: relative;
+ display: block;
+ --step-width: 32px;
+ --beat-width: 128px;
+ --bar-width: 512px;
+ background-size: var(--bar-width) 100%, var(--beat-width) 100%, var(--step-width) 100%;
+ background-image:
+ repeating-linear-gradient(to right, #666 0, #666 1px, transparent 1px, transparent 100%),
+ repeating-linear-gradient(to right, #444 0, #444 1px, transparent 1px, transparent 100%),
+ repeating-linear-gradient(to right, #3a3e44 0, #3a3e44 1px, transparent 1px, transparent 100%);
+}
+
/* =============================================== */
-/* CONTROLES E INPUTS
+/* EDITOR DE ÁUDIO - RÉGUA E LAYOUT CORRIGIDO
/* =============================================== */
+.ruler-wrapper {
+ display: flex;
+ flex-shrink: 0;
+ background-color: #383c42;
+ border-bottom: 1px solid var(--border-color);
+}
+.ruler-spacer {
+ width: var(--track-info-width);
+ flex-shrink: 0;
+ border-right: 2px solid var(--border-color);
+}
+.timeline-ruler {
+ position: relative;
+ height: 25px;
+ flex-grow: 1;
+ overflow: hidden;
+ background-color: #2a2c30;
+}
+.ruler-marker {
+ position: absolute;
+ top: 0;
+ height: 100%;
+ color: var(--text-dark);
+ font-size: 0.75rem;
+ padding-left: 5px;
+ border-left: 1px solid #555;
+ user-select: none;
+ display: flex;
+ align-items: center;
+ box-sizing: border-box;
+}
+
+/* =============================================== */
+/* EDITOR DE ÁUDIO - CLIPS E CONTROLES
+/* =============================================== */
+.timeline-clip {
+ position: absolute;
+ top: 50%;
+ transform: translateY(-50%);
+ height: 55px;
+ background: linear-gradient(to bottom, #5c626b, #4a4f57);
+ border: 1px solid var(--border-color-dark);
+ border-radius: 4px;
+ box-shadow: 0 3px 8px rgba(0,0,0,0.5);
+ display: flex;
+ align-items: center;
+ padding: 0 8px;
+ overflow: hidden;
+ cursor: grab;
+ user-select: none;
+ color: var(--text-light);
+}
+.timeline-clip:active, .timeline-clip.dragging { cursor: grabbing; z-index: 1000; border-color: var(--accent-blue); opacity: 0.9; }
+.clip-name { position: absolute; top: 4px; left: 8px; font-size: 0.75rem; font-weight: bold; background-color: rgba(0,0,0,0.6); padding: 2px 6px; border-radius: 3px; pointer-events: none; }
+.waveform-canvas-clip { width: 100%; height: 100%; display: block; }
+.audio-track-lane .track-controls { display: flex; justify-content: flex-start; gap: 15px; border-left: none; padding-left: 0; margin: 0; }
+.clip-resize-handle { position: absolute; top: 0; bottom: 0; width: 8px; cursor: ew-resize; z-index: 10; }
+.clip-resize-handle.left { left: 0; }
+.clip-resize-handle.right { right: 0; }
+.playhead { position: absolute; top: 0; left: 0; width: 2px; height: 100%; background-color: var(--accent-red); z-index: 50; pointer-events: none; }
+#loop-region {
+ display: none; /* Começa escondido por padrão */
+ position: absolute;
+ top: 0;
+ height: 100%;
+ background-color: rgba(52, 152, 219, 0.2);
+ border-left: 1px solid var(--accent-blue);
+ border-right: 1px solid var(--accent-blue);
+ z-index: 15;
+ min-width: 16px;
+ cursor: grab;
+}
+#loop-region.visible {
+ display: block; /* ou 'flex', 'absolute', etc. */
+}
+
+/* Esta regra está correta, mas também deve usar o ID */
+#loop-region:active {
+ cursor: grabbing;
+}
+.loop-handle { position: absolute; top: 0; bottom: 0; width: 10px; cursor: ew-resize; }
+.loop-handle.left { left: -5px; }
+.loop-handle.right { right: -5px; }
+#slice-tool-btn.active { color: var(--accent-blue); }
+#audio-editor-loop-btn.active { color: var(--accent-green); }
+
+/* =============================================== */
+/* COMPONENTES GERAIS (KNOBS, BOTÕES, INPUTS)
+/* =============================================== */
+.knob-container { text-align: center; font-size: .7rem; color: var(--text-dark); }
+.knob { width: 28px; height: 28px; background-color: var(--bg-toolbar); border-radius: 50%; border: 1px solid var(--border-color); margin-bottom: 2px; cursor: grab; box-shadow: inset 0 0 4px #222; position: relative; }
+.knob:active { cursor: grabbing; }
+.knob-indicator { width: 2px; height: 8px; background-color: var(--text-light); position: absolute; top: 2px; left: 50%; transform-origin: bottom center; transform: translateX(-50%) rotate(0deg); border-radius: 1px; }
.interactive-input-container { display: flex; align-items: center; justify-content: center; gap: 4px; }
.compasso-group { display: flex; align-items: center; gap: 4px; }
.value-input { background: 0 0; border: 0; outline: 0; color: var(--accent-green); font-weight: 700; font-size: 1.4rem; font-family: Courier New, Courier, monospace; text-align: center; padding: 0; width: 55px; }
@@ -364,13 +374,15 @@ body.sidebar-hidden .global-toolbar {
#metronome-btn:hover { border-color: var(--text-light); background-color: var(--bg-editor); }
#metronome-btn.active { background-color: var(--accent-green); color: var(--bg-body); border-color: var(--accent-green); }
+/* =============================================== */
+/* MODAL E MENUS
+/* =============================================== */
.modal-overlay { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background-color: rgba(0, 0, 0, 0.7); z-index: 2000; display: flex; justify-content: center; align-items: center; padding: 1rem; visibility: hidden; opacity: 0; transition: visibility 0s 0.3s, opacity 0.3s; }
.modal-overlay.visible { visibility: visible; opacity: 1; transition: visibility 0s, opacity 0.3s; }
.modal-content { background-color: var(--bg-body); padding: 1.5rem 2rem; border-radius: 6px; border: 1px solid var(--border-color); box-shadow: 0 10px 30px rgba(0, 0, 0, 0.5); width: 100%; max-width: 500px; position: relative; display: flex; flex-direction: column; gap: 1.5rem; max-height: 90vh; }
.modal-close { position: absolute; top: 10px; right: 15px; font-size: 1.5rem; color: var(--text-dark); cursor: pointer; border: none; background: none; }
.modal-close:hover { color: var(--text-light); }
.modal-title { margin: 0; padding-bottom: 0.5rem; border-bottom: 1px solid var(--bg-toolbar); color: var(--text-light); text-align: center; flex-shrink: 0; }
-.modal-section { margin: 0; }
.modal-section h3 { margin-top: 0; margin-bottom: 0.8rem; font-size: 1rem; color: var(--text-light); }
#server-projects-list { max-height: 250px; overflow-y: auto; background-color: var(--bg-toolbar); border: 1px solid var(--border-color); border-radius: 4px; padding: 0.5rem; min-height: 50px; }
#server-projects-list .project-item { background-color: var(--bg-editor); padding: 10px 15px; border-radius: 4px; margin-bottom: 8px; cursor: pointer; transition: background-color 0.2s, color 0.2s; border: 1px solid transparent; }
@@ -378,124 +390,40 @@ body.sidebar-hidden .global-toolbar {
#server-projects-list .project-item:hover { background-color: var(--bg-body); color: #fff; border-color: var(--accent-green); }
.modal-button { background-color: var(--bg-toolbar); color: var(--text-light); border: 1px solid var(--border-color); padding: 0.8rem 1.5rem; border-radius: 4px; cursor: pointer; font-size: 1rem; transition: background-color 0.2s, border-color 0.2s; width: 100%; text-align: center; }
.modal-button:hover { background-color: #4a4f57; border-color: #333; }
-
-.file-menu-container { position: relative; }
-.toolbar-btn { background-color: var(--background-light); color: var(--text-light); border: 1px solid var(--border-color); border-radius: 3px; padding: 5px 10px; cursor: pointer; font-family: inherit; font-size: 0.8rem; }
-.toolbar-btn:hover { background-color: var(--background-lighter); }
-.file-menu-dropdown { position: absolute; top: 100%; left: 0; background-color: var(--background-lighter); border: 1px solid var(--border-color-dark); border-radius: 4px; box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2); min-width: 200px; z-index: 1000; overflow: hidden; display: flex; flex-direction: column; }
-.file-menu-dropdown.hidden { display: none; }
-.file-menu-dropdown a { color: var(--text-light); padding: 8px 12px; text-decoration: none; display: block; font-size: 0.9rem; }
-.file-menu-dropdown a:hover { background-color: var(--accent-blue); color: white; }
-.menu-divider { height: 1px; background-color: var(--border-color); margin: 4px 0; }
-
+#timeline-context-menu { position: fixed; display: none; background-color: var(--bg-toolbar); border: 1px solid var(--border-color-dark); border-radius: 4px; box-shadow: 0 4px 12px rgba(0,0,0,0.5); padding: 5px 0; z-index: 2000; font-size: 0.9rem; }
+#timeline-context-menu div { padding: 8px 15px; cursor: pointer; white-space: nowrap; }
+#timeline-context-menu div:hover { background-color: var(--accent-blue); color: white; }
/* =============================================== */
-/* ESTILOS RESPONSIVOS (MELHORADO)
+/* ESTILOS RESPONSIVOS
/* =============================================== */
@media (max-width: 1200px) {
- .info-display-group {
- gap: 2px;
- }
- .info-display {
- padding: 4px 6px;
- }
- .value-input {
- font-size: 1.2rem;
- width: 45px;
- }
- .compasso-input {
- width: 20px;
- }
+ .info-display-group { gap: 2px; }
+ .info-display { padding: 4px 6px; }
+ .value-input { font-size: 1.2rem; width: 45px; }
+ .compasso-input { width: 20px; }
}
-
@media (max-width: 992px) {
- .global-toolbar {
- gap: 10px;
- flex-wrap: wrap;
- height: auto; /* Permite que a toolbar cresça se o conteúdo quebrar linha */
- padding-bottom: 10px;
- }
- body {
- padding-top: 80px; /* Aumenta o espaço para a toolbar maior */
- }
- .info-display-group {
- order: 3; /* Move o grupo de informações para o final da toolbar */
- width: 100%;
- justify-content: space-around;
- }
- .spacer {
- display: none;
- }
+ .global-toolbar { gap: 10px; flex-wrap: wrap; height: auto; padding-bottom: 10px; }
+ .main-content { padding-top: 100px; }
+ .info-display-group { order: 3; width: 100%; justify-content: space-around; }
+ .spacer { display: none; }
}
-
@media (max-width: 768px) {
- body {
- padding-left: 0 !important;
- }
- .sample-browser {
- transform: translateX(-100%);
- position: fixed; /* Volta a ser fixo para deslizar por cima */
- width: 280px;
- }
- body:not(.sidebar-hidden) .sample-browser {
- transform: translateX(0);
- }
- #sidebar-toggle {
- left: 5px;
- transform: translateX(0);
- position: fixed; /* Garante que o botão fique visível */
- }
- .global-toolbar {
- left: 0;
- padding-left: 45px;
- }
- .main-content {
- padding: 10px;
- padding-top: 85px; /* Ajusta o padding para a toolbar fixa */
- }
- .track-lane, .audio-track-lane {
- flex-direction: column;
- align-items: stretch;
- gap: 15px;
- padding: 15px;
- }
- .track-info,
- .track-controls {
- width: 100%;
- }
- .track-controls {
- border-left: none;
- padding-left: 0;
- justify-content: space-around;
- }
- .step-sequencer-wrapper {
- width: 100%;
- }
+ .sample-browser { transform: translateX(-100%); position: fixed; width: 280px; }
+ body:not(.sidebar-hidden) .sample-browser { transform: translateX(0); }
+ #sidebar-toggle { left: 5px; transform: translateX(0); position: fixed; }
+ .global-toolbar { padding-left: 45px; }
+ .main-content { padding: 10px; }
+ .track-lane, .audio-track-lane { flex-direction: column; align-items: stretch; gap: 15px; padding: 15px; }
+ .track-lane .track-info, .audio-track-lane .track-info, .track-lane .track-controls, .audio-track-lane .track-controls { width: 100%; border: none; padding: 0; }
+ .track-lane .track-controls, .audio-track-lane .track-controls { justify-content: space-around; }
}
-.spectrogram-view-wrapper {
- position: relative; /* Essencial para o posicionamento absoluto do filho */
- overflow: hidden; /* Garante que a agulha não saia dos limites */
-}
-
-.playhead {
- position: absolute;
- top: 0;
- left: 0; /* A posição será atualizada via JavaScript */
- width: 2px;
- height: 100%;
- background-color: var(--accent-red, #e74c3c); /* Use uma cor de destaque */
- 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
+/* =============================================== */
+/* SCROLLBARS
+/* =============================================== */
+::-webkit-scrollbar { height: 10px; width: 10px; }
+::-webkit-scrollbar-track { background: var(--border-color); }
+::-webkit-scrollbar-thumb { background: var(--bg-toolbar); border-radius: 5px; }
+::-webkit-scrollbar-thumb:hover { background: #555; }
\ No newline at end of file
diff --git a/assets/js/creations/audio.js b/assets/js/creations/audio.js
index 7bbfde1..4dfed83 100644
--- a/assets/js/creations/audio.js
+++ b/assets/js/creations/audio.js
@@ -1,316 +1,34 @@
// js/audio.js
-import { appState } from "./state.js";
-import { highlightStep, updateAudioEditorUI, updatePlayheadVisual, resetPlayheadVisual } from "./ui.js";
-import { getTotalSteps } from "./utils.js";
-import { PIXELS_PER_STEP } from "./config.js";
-let audioContext;
-let mainGainNode;
-let masterPannerNode;
-
-const timerDisplay = document.getElementById('timer-display');
+// O contexto de áudio agora será gerenciado principalmente pelo Tone.js.
+// Esta função garante que ele seja iniciado por uma interação do usuário.
+export function initializeAudioContext() {
+ if (Tone.context.state !== 'running') {
+ Tone.start();
+ console.log("AudioContext iniciado com Tone.js");
+ }
+}
+// Funções de acesso ao contexto global do Tone.js
export function getAudioContext() {
- return audioContext;
+ return Tone.context;
}
export function getMainGainNode() {
- return mainGainNode;
-}
-
-export function initializeAudioContext() {
- if (!audioContext) {
- audioContext = new (window.AudioContext || window.webkitAudioContext)();
- mainGainNode = audioContext.createGain();
- masterPannerNode = audioContext.createStereoPanner();
-
- mainGainNode.connect(masterPannerNode);
- masterPannerNode.connect(audioContext.destination);
- }
- if (audioContext.state === "suspended") {
- audioContext.resume();
- }
+ return Tone.Destination;
}
+// Funções para controlar o volume e pan master
export function updateMasterVolume(volume) {
- if (mainGainNode) {
- mainGainNode.gain.setValueAtTime(volume, audioContext.currentTime);
+ // Tone.Destination.volume.value é em decibéis. Convertemos de linear (0-1.5) para dB.
+ if (volume === 0) {
+ Tone.Destination.volume.value = -Infinity;
+ } else {
+ Tone.Destination.volume.value = Tone.gainToDb(volume);
}
}
export function updateMasterPan(pan) {
- if (masterPannerNode) {
- masterPannerNode.pan.setValueAtTime(pan, audioContext.currentTime);
- }
-}
-
-function formatTime(milliseconds) {
- const totalSeconds = Math.floor(milliseconds / 1000);
- const minutes = Math.floor(totalSeconds / 60).toString().padStart(2, '0');
- const seconds = (totalSeconds % 60).toString().padStart(2, '0');
- const centiseconds = Math.floor((milliseconds % 1000) / 10).toString().padStart(2, '0');
- return `${minutes}:${seconds}:${centiseconds}`;
-}
-
-export function playMetronomeSound(isDownbeat) {
- initializeAudioContext();
- const oscillator = audioContext.createOscillator();
- const gainNode = audioContext.createGain();
- const frequency = isDownbeat ? 1000 : 800;
- oscillator.frequency.setValueAtTime(frequency, audioContext.currentTime);
- oscillator.type = "sine";
- gainNode.gain.setValueAtTime(1, audioContext.currentTime);
- gainNode.gain.exponentialRampToValueAtTime(
- 0.00001,
- audioContext.currentTime + 0.05
- );
- oscillator.connect(gainNode);
- gainNode.connect(mainGainNode);
- oscillator.start(audioContext.currentTime);
- oscillator.stop(audioContext.currentTime + 0.05);
-}
-
-export function playSample(filePath, trackId) {
- initializeAudioContext();
- if (!filePath) return;
-
- const track = trackId ? appState.tracks.find((t) => t.id == trackId) : null;
-
- if (!track || !track.audioBuffer) {
- const audio = new Audio(filePath);
- audio.play();
- return;
- }
-
- const source = audioContext.createBufferSource();
- source.buffer = track.audioBuffer;
-
- if (track.gainNode) {
- source.connect(track.gainNode);
- } else {
- source.connect(mainGainNode);
- }
-
- source.start(0);
-}
-
-function tick() {
- const totalSteps = getTotalSteps();
- if (totalSteps === 0 || !appState.isPlaying) {
- stopPlayback();
- return;
- }
-
- const lastStepIndex = appState.currentStep === 0 ? totalSteps - 1 : appState.currentStep - 1;
- highlightStep(lastStepIndex, false);
-
- const bpm = parseInt(document.getElementById("bpm-input").value, 10) || 120;
- const stepInterval = (60 * 1000) / (bpm * 4);
- const currentTime = appState.currentStep * stepInterval;
- if (timerDisplay) {
- timerDisplay.textContent = formatTime(currentTime);
- }
-
- if (appState.metronomeEnabled) {
- const noteValue = parseInt(document.getElementById("compasso-b-input").value, 10) || 4;
- const stepsPerBeat = 16 / noteValue;
- if (appState.currentStep % stepsPerBeat === 0) {
- playMetronomeSound(appState.currentStep % (stepsPerBeat * 4) === 0);
- }
- }
-
- appState.tracks.forEach((track) => {
- if (!track.patterns || track.patterns.length === 0) return;
-
- const activePattern = track.patterns[appState.activePatternIndex];
-
- if (activePattern && activePattern.steps[appState.currentStep] && track.samplePath) {
- playSample(track.samplePath, track.id);
- }
- });
-
- highlightStep(appState.currentStep, true);
- appState.currentStep = (appState.currentStep + 1) % totalSteps;
-}
-
-export function startPlayback() {
- if (appState.isPlaying || appState.tracks.length === 0) return;
- initializeAudioContext();
-
- if (appState.currentStep === 0) {
- rewindPlayback();
- }
-
- const bpm = parseInt(document.getElementById("bpm-input").value, 10) || 120;
- const stepInterval = (60 * 1000) / (bpm * 4);
-
- if (appState.playbackIntervalId) clearInterval(appState.playbackIntervalId);
-
- appState.isPlaying = true;
- document.getElementById("play-btn").classList.remove("fa-play");
- document.getElementById("play-btn").classList.add("fa-pause");
-
- tick();
- appState.playbackIntervalId = setInterval(tick, stepInterval);
-}
-
-export function stopPlayback() {
- if(appState.playbackIntervalId) {
- clearInterval(appState.playbackIntervalId);
- }
- appState.playbackIntervalId = null;
- appState.isPlaying = false;
-
- document.querySelectorAll('.step.playing').forEach(s => s.classList.remove('playing'));
-
- appState.currentStep = 0;
-
- if (timerDisplay) timerDisplay.textContent = '00:00:00';
-
- const playBtn = document.getElementById("play-btn");
- if (playBtn) {
- playBtn.classList.remove("fa-pause");
- playBtn.classList.add("fa-play");
- }
-}
-
-export function rewindPlayback() {
- const lastStep = appState.currentStep > 0 ? appState.currentStep - 1 : getTotalSteps() - 1;
- appState.currentStep = 0;
- if (!appState.isPlaying) {
- if (timerDisplay) timerDisplay.textContent = '00:00:00';
- highlightStep(lastStep, false);
- }
-}
-
-export function togglePlayback() {
- initializeAudioContext();
- if (appState.isPlaying) {
- stopPlayback();
- } else {
- appState.currentStep = 0;
- startPlayback();
- }
-}
-
-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;
-
- 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);
-}
-
-export function startAudioEditorPlayback() {
- if (appState.isAudioEditorPlaying || appState.audioTracks.length === 0) return;
- initializeAudioContext();
-
- appState.isAudioEditorPlaying = true;
- appState.activeAudioSources = [];
- updateAudioEditorUI();
-
- const startTime = audioContext.currentTime;
- appState.audioEditorStartTime = startTime;
-
- appState.audioTracks.forEach(track => {
- 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);
- }
- });
-
- if (appState.activeAudioSources.length > 0) {
- if (appState.audioEditorAnimationId) {
- cancelAnimationFrame(appState.audioEditorAnimationId);
- }
- animationLoop();
- } else {
- appState.isAudioEditorPlaying = false;
- updateAudioEditorUI();
- }
-}
-
-export function stopAudioEditorPlayback() {
- if (!appState.isAudioEditorPlaying) 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
- );
-
- // --- 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();
-}
-
-export function seekAudioEditor(newTime) {
- const wasPlaying = appState.isAudioEditorPlaying;
- if (wasPlaying) {
- stopAudioEditorPlayback();
- }
- appState.audioEditorPlaybackTime = newTime;
- const bpm = parseInt(document.getElementById("bpm-input").value, 10) || 120;
- const stepsPerSecond = (bpm / 60) * 4;
- const pixelsPerSecond = stepsPerSecond * PIXELS_PER_STEP;
- const newPositionPx = newTime * pixelsPerSecond;
- updatePlayheadVisual(newPositionPx);
- if (wasPlaying) {
- startAudioEditorPlayback();
- }
-}
-
-export function restartAudioEditorIfPlaying() {
- if (appState.isAudioEditorPlaying) {
- stopAudioEditorPlayback();
- startAudioEditorPlayback();
- }
+ // A panorimização master em Tone.js geralmente requer um nó Panner dedicado.
+ // Por enquanto, esta função servirá como um placeholder para futuras implementações.
+ console.log("Master Pan ainda não implementado com Tone.js");
}
\ No newline at end of file
diff --git a/assets/js/creations/audio/audio_audio.js b/assets/js/creations/audio/audio_audio.js
new file mode 100644
index 0000000..3b7fdcf
--- /dev/null
+++ b/assets/js/creations/audio/audio_audio.js
@@ -0,0 +1,154 @@
+// js/audio_audio.js
+import { appState } from "../state.js";
+import { updateAudioEditorUI, updatePlayheadVisual, resetPlayheadVisual } from "./audio_ui.js";
+import { PIXELS_PER_STEP } from "../config.js";
+import { initializeAudioContext } from "../audio.js";
+import { getPixelsPerSecond } from "../utils.js";
+
+function animationLoop() {
+ if (!appState.global.isAudioEditorPlaying) return;
+
+ const pixelsPerSecond = getPixelsPerSecond();
+ const totalElapsedTime = Tone.Transport.seconds;
+
+ let maxTime = 0;
+ appState.audio.clips.forEach(clip => {
+ const endTime = clip.startTime + clip.duration;
+ if (endTime > maxTime) maxTime = endTime;
+ });
+
+ if (!appState.global.isLoopActive && totalElapsedTime >= maxTime && maxTime > 0) {
+ stopAudioEditorPlayback();
+ resetPlayheadVisual();
+ return;
+ }
+
+ const newPositionPx = totalElapsedTime * pixelsPerSecond;
+ updatePlayheadVisual(newPositionPx);
+
+ // ##### CORREÇÃO 1 #####
+ // Salva o ID da animação para que o stop possa cancelá-lo
+ appState.audio.audioEditorAnimationId = requestAnimationFrame(animationLoop);
+}
+
+export function updateTransportLoop() {
+ Tone.Transport.loop = appState.global.isLoopActive;
+ Tone.Transport.loopStart = appState.global.loopStartTime;
+ Tone.Transport.loopEnd = appState.global.loopEndTime;
+}
+
+export function startAudioEditorPlayback() {
+ if (appState.global.isAudioEditorPlaying) return;
+ initializeAudioContext();
+ Tone.Transport.cancel(); // Limpa eventos agendados anteriormente
+
+ updateTransportLoop(); // Isso deve definir Tone.Transport.loop = true e Tone.Transport.loopEnd
+
+ // 1. Pegue a duração total do loop que a função acima definiu
+ const loopInterval = Tone.Transport.loopEnd;
+
+ // Se loopEnd não foi definido (ex: 0 ou undefined), o loop não funcionará.
+ if (!loopInterval || loopInterval === 0) {
+ console.error("LoopEnd não está definido no Tone.Transport! O áudio não repetirá.");
+ // Você pode querer definir um padrão aqui, mas o ideal é
+ // garantir que 'updateTransportLoop' esteja definindo 'loopEnd' corretamente.
+ // ex: const loopInterval = "1m"; (se for um compasso por padrão)
+ }
+
+ appState.audio.clips.forEach(clip => {
+ if (!clip.player || !clip.player.loaded) return;
+
+ // 2. CORREÇÃO: Use scheduleRepeat no lugar de scheduleOnce
+ Tone.Transport.scheduleRepeat((time) => {
+ // Sua lógica de parâmetros está correta
+ clip.gainNode.gain.value = Tone.gainToDb(clip.volume);
+ clip.pannerNode.pan.value = clip.pan;
+ clip.player.playbackRate = Math.pow(2, clip.pitch / 12);
+
+ // Inicia o player no tempo agendado
+ clip.player.start(time, clip.offset, clip.duration);
+
+ },
+ loopInterval, // <--- O intervalo de repetição (ex: "4m", "8m")
+ clip.startTime // <--- Onde o clip começa dentro da linha do tempo
+ );
+ });
+
+ // 3. ADIÇÃO CRÍTICA: Inicie o transporte e atualize o estado
+ Tone.Transport.start();
+ appState.global.isAudioEditorPlaying = true;
+
+ // 4. (CORRIGIDO) Atualize a UI do botão de play
+ const playBtn = document.getElementById("audio-editor-play-btn");
+ if (playBtn) {
+ playBtn.classList.add("active");
+ // Verifica se o ícone existe antes de tentar mudá-lo
+ const icon = playBtn.querySelector('i');
+ if (icon) {
+ icon.className = 'fa-solid fa-pause';
+ }
+ }
+
+ // ##### CORREÇÃO 2 #####
+ // Inicia o loop de animação da agulha
+ animationLoop();
+}
+
+export function stopAudioEditorPlayback() {
+ if (!appState.global.isAudioEditorPlaying) return;
+ Tone.Transport.stop();
+
+ appState.audio.clips.forEach(clip => {
+ if (clip.player && clip.player.state === 'started') {
+ clip.player.stop();
+ }
+ });
+
+ appState.audio.audioEditorPlaybackTime = Tone.Transport.seconds;
+
+ // Esta lógica agora funcionará corretamente graças à Correção 1
+ if (appState.audio.audioEditorAnimationId) {
+ cancelAnimationFrame(appState.audio.audioEditorAnimationId);
+ appState.audio.audioEditorAnimationId = null;
+ }
+
+ // (CORRIGIDO) Atualiza a UI do botão de play
+ const playBtn = document.getElementById("audio-editor-play-btn");
+ if (playBtn) {
+ playBtn.classList.remove("active");
+ // Verifica se o ícone existe antes de tentar mudá-lo
+ const icon = playBtn.querySelector('i');
+ if (icon) {
+ icon.className = 'fa-solid fa-play'; // Muda de volta para "play"
+ }
+ }
+
+ appState.global.isAudioEditorPlaying = false;
+ updateAudioEditorUI();
+}
+
+export function seekAudioEditor(newTime) {
+ const wasPlaying = appState.global.isAudioEditorPlaying;
+ if (wasPlaying) {
+ stopAudioEditorPlayback();
+ }
+
+ appState.audio.audioEditorPlaybackTime = newTime;
+ Tone.Transport.seconds = newTime;
+
+ const pixelsPerSecond = getPixelsPerSecond();
+ const newPositionPx = newTime * pixelsPerSecond;
+ updatePlayheadVisual(newPositionPx);
+
+ if (wasPlaying) {
+ startAudioEditorPlayback();
+ }
+}
+
+export function restartAudioEditorIfPlaying() {
+ if (appState.global.isAudioEditorPlaying) {
+ appState.audio.audioEditorPlaybackTime = Tone.Transport.seconds;
+ stopAudioEditorPlayback();
+ startAudioEditorPlayback();
+ }
+}
\ No newline at end of file
diff --git a/assets/js/creations/audio/audio_state.js b/assets/js/creations/audio/audio_state.js
new file mode 100644
index 0000000..f5f2cc2
--- /dev/null
+++ b/assets/js/creations/audio/audio_state.js
@@ -0,0 +1,136 @@
+// js/audio_state.js
+import { DEFAULT_VOLUME, DEFAULT_PAN } from "../config.js";
+import { renderAudioEditor } from "./audio_ui.js";
+import { getMainGainNode } from "../audio.js";
+
+const initialState = {
+ tracks: [],
+ clips: [],
+ audioEditorStartTime: 0,
+ audioEditorAnimationId: null,
+ audioEditorPlaybackTime: 0,
+ isAudioEditorLoopEnabled: false,
+};
+
+export let audioState = { ...initialState };
+
+export function initializeAudioState() {
+ audioState.clips.forEach(clip => {
+ if (clip.player) clip.player.dispose();
+ if (clip.pannerNode) clip.pannerNode.dispose();
+ if (clip.gainNode) clip.gainNode.dispose();
+ });
+ Object.assign(audioState, initialState, { tracks: [], clips: [] });
+}
+
+export async function loadAudioForClip(clip) {
+ if (!clip.sourcePath) return clip;
+ try {
+ // Cria o player e o conecta à cadeia de áudio do clipe
+ clip.player = new Tone.Player();
+ clip.player.chain(clip.gainNode, clip.pannerNode, getMainGainNode());
+
+ // Carrega o áudio e espera a conclusão
+ await clip.player.load(clip.sourcePath);
+
+ if (clip.duration === 0) {
+ clip.duration = clip.player.buffer.duration;
+ }
+ } catch (error) {
+ console.error(`Falha ao carregar áudio para o clipe ${clip.name}:`, error);
+ clip.player = null;
+ }
+ return clip;
+}
+
+export function addAudioClipToTimeline(samplePath, trackId = 1, startTime = 0) {
+ const newClip = {
+ id: Date.now() + Math.random(),
+ trackId: trackId,
+ sourcePath: samplePath,
+ name: samplePath.split('/').pop(),
+ player: null,
+ startTime: startTime,
+ offset: 0,
+ duration: 0,
+ pitch: 0,
+ volume: DEFAULT_VOLUME,
+ pan: DEFAULT_PAN,
+ isSoloed: true,
+ // --- ADICIONADO: Nós de áudio para cada clipe ---
+ gainNode: new Tone.Gain(Tone.gainToDb(DEFAULT_VOLUME)),
+ pannerNode: new Tone.Panner(DEFAULT_PAN),
+ };
+
+ audioState.clips.push(newClip);
+
+ loadAudioForClip(newClip).then(() => {
+ renderAudioEditor();
+ });
+}
+
+export function updateAudioClipProperties(clipId, properties) {
+ const clip = audioState.clips.find(c => c.id == clipId);
+ if (clip) {
+ Object.assign(clip, properties);
+ }
+}
+
+export function sliceAudioClip(clipId, sliceTimeInTimeline) {
+ const originalClip = audioState.clips.find(c => c.id == clipId);
+ if (!originalClip || sliceTimeInTimeline <= originalClip.startTime || sliceTimeInTimeline >= originalClip.startTime + originalClip.duration) {
+ return;
+ }
+
+ const cutPointInClip = sliceTimeInTimeline - originalClip.startTime;
+
+ const newClip = {
+ id: Date.now() + Math.random(),
+ trackId: originalClip.trackId,
+ sourcePath: originalClip.sourcePath,
+ name: originalClip.name,
+ player: originalClip.player,
+ startTime: sliceTimeInTimeline,
+ offset: originalClip.offset + cutPointInClip,
+ duration: originalClip.duration - cutPointInClip,
+ pitch: originalClip.pitch,
+ volume: originalClip.volume,
+ pan: originalClip.pan,
+ isSoloed: false,
+ gainNode: new Tone.Gain(Tone.gainToDb(originalClip.volume)),
+ pannerNode: new Tone.Panner(originalClip.pan),
+ };
+ newClip.player.chain(newClip.gainNode, newClip.pannerNode, getMainGainNode());
+
+
+ originalClip.duration = cutPointInClip;
+ audioState.clips.push(newClip);
+}
+
+export function updateClipVolume(clipId, volume) {
+ const clip = audioState.clips.find((c) => c.id == clipId);
+ if (clip) {
+ const clampedVolume = Math.max(0, Math.min(1.5, volume));
+ clip.volume = clampedVolume;
+ if (clip.gainNode) {
+ clip.gainNode.gain.value = Tone.gainToDb(clampedVolume);
+ }
+ }
+}
+
+export function updateClipPan(clipId, pan) {
+ const clip = audioState.clips.find((c) => c.id == clipId);
+ if (clip) {
+ const clampedPan = Math.max(-1, Math.min(1, pan));
+ clip.pan = clampedPan;
+ if (clip.pannerNode) {
+ clip.pannerNode.pan.value = clampedPan;
+ }
+ }
+}
+
+export function addAudioTrackLane() {
+ const newTrackName = `Pista de Áudio ${audioState.tracks.length + 1}`;
+ audioState.tracks.push({ id: Date.now(), name: newTrackName });
+ // A UI será re-renderizada a partir do main.js
+}
\ No newline at end of file
diff --git a/assets/js/creations/audio/audio_ui.js b/assets/js/creations/audio/audio_ui.js
new file mode 100644
index 0000000..a6594a1
--- /dev/null
+++ b/assets/js/creations/audio/audio_ui.js
@@ -0,0 +1,367 @@
+// js/audio/audio_ui.js
+import { appState } from "../state.js";
+import {
+ addAudioClipToTimeline,
+ updateAudioClipProperties,
+ sliceAudioClip,
+} from "./audio_state.js";
+import { seekAudioEditor, restartAudioEditorIfPlaying, updateTransportLoop } from "./audio_audio.js";
+import { drawWaveform } from "../waveform.js";
+import { PIXELS_PER_BAR, ZOOM_LEVELS } from "../config.js";
+import { getPixelsPerSecond } from "../utils.js";
+
+export function renderAudioEditor() {
+ const audioEditor = document.querySelector('.audio-editor');
+ const existingTrackContainer = document.getElementById('audio-track-container');
+ if (!audioEditor || !existingTrackContainer) return;
+
+ // --- CRIAÇÃO E RENDERIZAÇÃO DA RÉGUA (AGORA COM WRAPPER E SPACER) ---
+ let rulerWrapper = audioEditor.querySelector('.ruler-wrapper');
+ if (!rulerWrapper) {
+ rulerWrapper = document.createElement('div');
+ rulerWrapper.className = 'ruler-wrapper';
+ rulerWrapper.innerHTML = `
+
+
+ `;
+ audioEditor.insertBefore(rulerWrapper, existingTrackContainer);
+ }
+
+ const ruler = rulerWrapper.querySelector('.timeline-ruler');
+ ruler.innerHTML = ''; // Limpa a régua para redesenhar
+
+ const pixelsPerSecond = getPixelsPerSecond();
+
+ let maxTime = appState.global.loopEndTime;
+ appState.audio.clips.forEach(clip => {
+ const endTime = clip.startTime + clip.duration;
+ if (endTime > maxTime) maxTime = endTime;
+ });
+
+ const containerWidth = existingTrackContainer.offsetWidth;
+ const contentWidth = maxTime * pixelsPerSecond;
+ const totalWidth = Math.max(contentWidth, containerWidth, 2000); // Garante uma largura mínima
+
+ ruler.style.width = `${totalWidth}px`;
+
+ const zoomFactor = ZOOM_LEVELS[appState.global.zoomLevelIndex];
+ const scaledBarWidth = PIXELS_PER_BAR * zoomFactor;
+
+ if (scaledBarWidth > 0) {
+ const numberOfBars = Math.ceil(totalWidth / scaledBarWidth);
+ for (let i = 1; i <= numberOfBars; i++) {
+ const marker = document.createElement('div');
+ marker.className = 'ruler-marker';
+ marker.textContent = i;
+ marker.style.left = `${(i - 1) * scaledBarWidth}px`;
+ ruler.appendChild(marker);
+ }
+ }
+
+ const loopRegion = document.createElement('div');
+ loopRegion.id = 'loop-region';
+ loopRegion.style.left = `${appState.global.loopStartTime * pixelsPerSecond}px`;
+ loopRegion.style.width = `${(appState.global.loopEndTime - appState.global.loopStartTime) * pixelsPerSecond}px`;
+ loopRegion.innerHTML = ``;
+ loopRegion.classList.toggle("visible", appState.global.isLoopActive);
+ ruler.appendChild(loopRegion);
+
+ // --- LISTENER DA RÉGUA PARA INTERAÇÕES (LOOP E SEEK) ---
+ ruler.addEventListener('mousedown', (e) => {
+ const currentPixelsPerSecond = getPixelsPerSecond();
+ const loopHandle = e.target.closest('.loop-handle');
+ const loopRegionBody = e.target.closest('#loop-region:not(.loop-handle)');
+
+ if (loopHandle) {
+ e.preventDefault(); e.stopPropagation();
+ const handleType = loopHandle.classList.contains('left') ? 'left' : 'right';
+ const initialMouseX = e.clientX;
+ const initialStart = appState.global.loopStartTime;
+ const initialEnd = appState.global.loopEndTime;
+
+ const onMouseMove = (moveEvent) => {
+ const deltaX = moveEvent.clientX - initialMouseX;
+ const deltaTime = deltaX / currentPixelsPerSecond;
+ let newStart = appState.global.loopStartTime;
+ let newEnd = appState.global.loopEndTime;
+
+ if (handleType === 'left') {
+ newStart = Math.max(0, initialStart + deltaTime);
+ newStart = Math.min(newStart, appState.global.loopEndTime - 0.1); // Não deixa passar do fim
+ appState.global.loopStartTime = newStart;
+ } else {
+ newEnd = Math.max(appState.global.loopStartTime + 0.1, initialEnd + deltaTime); // Não deixa ser antes do início
+ appState.global.loopEndTime = newEnd;
+ }
+
+ updateTransportLoop();
+
+ // ### CORREÇÃO DE PERFORMANCE 1 ###
+ // Remove a chamada para renderAudioEditor()
+ // Em vez disso, atualiza o estilo do elemento 'loopRegion' diretamente
+ loopRegion.style.left = `${newStart * currentPixelsPerSecond}px`;
+ loopRegion.style.width = `${(newEnd - newStart) * currentPixelsPerSecond}px`;
+ // ### FIM DA CORREÇÃO 1 ###
+ };
+
+ const onMouseUp = () => {
+ document.removeEventListener('mousemove', onMouseMove);
+ document.removeEventListener('mouseup', onMouseUp);
+ // Opcional: chamar renderAudioEditor() UMA VEZ no final para garantir a sincronia
+ renderAudioEditor();
+ };
+ document.addEventListener('mousemove', onMouseMove);
+ document.addEventListener('mouseup', onMouseUp);
+ return;
+ }
+
+ if (loopRegionBody) {
+ e.preventDefault(); e.stopPropagation();
+ const initialMouseX = e.clientX;
+ const initialStart = appState.global.loopStartTime;
+ const initialEnd = appState.global.loopEndTime;
+ const initialDuration = initialEnd - initialStart;
+
+ const onMouseMove = (moveEvent) => {
+ const deltaX = moveEvent.clientX - initialMouseX;
+ const deltaTime = deltaX / currentPixelsPerSecond;
+ let newStart = Math.max(0, initialStart + deltaTime);
+ let newEnd = newStart + initialDuration;
+
+ appState.global.loopStartTime = newStart;
+ appState.global.loopEndTime = newEnd;
+
+ updateTransportLoop();
+
+ // ### CORREÇÃO DE PERFORMANCE 2 ###
+ // Remove a chamada para renderAudioEditor()
+ // Atualiza apenas a posição 'left' do elemento
+ loopRegion.style.left = `${newStart * currentPixelsPerSecond}px`;
+ // ### FIM DA CORREÇÃO 2 ###
+ };
+
+ const onMouseUp = () => {
+ document.removeEventListener('mousemove', onMouseMove);
+ document.removeEventListener('mouseup', onMouseUp);
+ // Opcional: chamar renderAudioEditor() UMA VEZ no final
+ renderAudioEditor();
+ };
+ document.addEventListener('mousemove', onMouseMove);
+ document.addEventListener('mouseup', onMouseUp);
+ return;
+ }
+
+ // Se o clique não foi em um handle ou no corpo do loop, faz o "seek"
+ e.preventDefault();
+ const handleSeek = (event) => {
+ const rect = ruler.getBoundingClientRect();
+ const scrollLeft = ruler.scrollLeft;
+ const clickX = event.clientX - rect.left;
+ const absoluteX = clickX + scrollLeft;
+ const newTime = absoluteX / currentPixelsPerSecond;
+ seekAudioEditor(newTime);
+ };
+ handleSeek(e);
+ const onMouseMoveSeek = (moveEvent) => handleSeek(moveEvent);
+ const onMouseUpSeek = () => { document.removeEventListener('mousemove', onMouseMoveSeek); document.removeEventListener('mouseup', onMouseUpSeek); };
+ document.addEventListener('mousemove', onMouseMoveSeek);
+ document.addEventListener('mouseup', onMouseUpSeek);
+ });
+
+ // --- RECRIAÇÃO DO CONTAINER DE PISTAS PARA EVITAR LISTENERS DUPLICADOS ---
+ const newTrackContainer = existingTrackContainer.cloneNode(false);
+ audioEditor.replaceChild(newTrackContainer, existingTrackContainer);
+
+ if (appState.audio.tracks.length === 0) {
+ appState.audio.tracks.push({ id: Date.now(), name: "Pista de Áudio 1" });
+ }
+
+ // --- RENDERIZAÇÃO DAS PISTAS INDIVIDUAIS ---
+ appState.audio.tracks.forEach(trackData => {
+ const audioTrackLane = document.createElement('div');
+ audioTrackLane.className = 'audio-track-lane';
+ audioTrackLane.dataset.trackId = trackData.id;
+ audioTrackLane.innerHTML = `
+
+
+ `;
+ newTrackContainer.appendChild(audioTrackLane);
+
+ const timelineContainer = audioTrackLane.querySelector('.timeline-container');
+
+ timelineContainer.addEventListener("dragover", (e) => { e.preventDefault(); audioTrackLane.classList.add('drag-over'); });
+ timelineContainer.addEventListener("dragleave", () => audioTrackLane.classList.remove('drag-over'));
+ timelineContainer.addEventListener("drop", (e) => {
+ e.preventDefault();
+ audioTrackLane.classList.remove('drag-over');
+ const filePath = e.dataTransfer.getData("text/plain");
+ if (!filePath) return;
+ const rect = timelineContainer.getBoundingClientRect();
+ const dropX = e.clientX - rect.left + timelineContainer.scrollLeft;
+ const startTimeInSeconds = dropX / pixelsPerSecond;
+ addAudioClipToTimeline(filePath, trackData.id, startTimeInSeconds);
+ });
+
+ const grid = timelineContainer.querySelector('.spectrogram-view-grid');
+ grid.style.setProperty('--bar-width', `${scaledBarWidth}px`);
+ grid.style.setProperty('--four-bar-width', `${scaledBarWidth * 4}px`);
+ });
+
+ // --- RENDERIZAÇÃO DOS CLIPS ---
+ appState.audio.clips.forEach(clip => {
+ const parentGrid = newTrackContainer.querySelector(`.audio-track-lane[data-track-id="${clip.trackId}"] .spectrogram-view-grid`);
+ if (!parentGrid) return;
+ const clipElement = document.createElement('div');
+ clipElement.className = 'timeline-clip';
+ clipElement.dataset.clipId = clip.id;
+ clipElement.style.left = `${clip.startTime * pixelsPerSecond}px`;
+ clipElement.style.width = `${clip.duration * pixelsPerSecond}px`;
+ let pitchStr = clip.pitch > 0 ? `+${clip.pitch}` : `${clip.pitch}`;
+ if (clip.pitch === 0) pitchStr = '';
+ clipElement.innerHTML = `
+
+ ${clip.name} ${pitchStr}
+
+
+ `;
+ parentGrid.appendChild(clipElement);
+ if (clip.player && clip.player.loaded) {
+ const canvas = clipElement.querySelector('.waveform-canvas-clip');
+ canvas.width = clip.duration * pixelsPerSecond;
+ canvas.height = 40;
+ const audioBuffer = clip.player.buffer.get();
+ drawWaveform(canvas, audioBuffer, 'var(--accent-green)', clip.offset, clip.duration);
+ }
+ clipElement.addEventListener('wheel', (e) => {
+ e.preventDefault();
+ const clipToUpdate = appState.audio.clips.find(c => c.id == clipElement.dataset.clipId);
+ if (!clipToUpdate) return;
+ const direction = e.deltaY < 0 ? 1 : -1;
+ let newPitch = clipToUpdate.pitch + direction;
+ newPitch = Math.max(-24, Math.min(24, newPitch));
+ updateAudioClipProperties(clipToUpdate.id, { pitch: newPitch });
+ renderAudioEditor();
+ restartAudioEditorIfPlaying();
+ });
+ });
+
+ // --- SINCRONIZAÇÃO DE SCROLL ENTRE A RÉGUA E AS PISTAS ---
+ newTrackContainer.addEventListener('scroll', () => {
+ const scrollPos = newTrackContainer.scrollLeft;
+ if (ruler.scrollLeft !== scrollPos) {
+ ruler.scrollLeft = scrollPos;
+ }
+ });
+
+ // --- EVENT LISTENER PRINCIPAL PARA INTERAÇÕES (MOVER, REDIMENSIONAR, ETC.) ---
+ newTrackContainer.addEventListener('mousedown', (e) => {
+ const currentPixelsPerSecond = getPixelsPerSecond();
+ const handle = e.target.closest('.clip-resize-handle');
+ const clipElement = e.target.closest('.timeline-clip');
+
+ if (appState.global.sliceToolActive && clipElement) { /* ... lógica de corte ... */ return; }
+ if (handle) { /* ... lógica de redimensionamento de clipe ... */ return; }
+
+ if (clipElement) {
+ e.preventDefault();
+ const clipId = clipElement.dataset.clipId;
+ const clickOffsetInClip = e.clientX - clipElement.getBoundingClientRect().left;
+ clipElement.classList.add('dragging');
+ let lastOverLane = clipElement.closest('.audio-track-lane');
+ const onMouseMove = (moveEvent) => {
+ const deltaX = moveEvent.clientX - e.clientX;
+ clipElement.style.transform = `translateX(${deltaX}px)`;
+ const overElement = document.elementFromPoint(moveEvent.clientX, moveEvent.clientY);
+ const overLane = overElement ? overElement.closest('.audio-track-lane') : null;
+ if (overLane && overLane !== lastOverLane) {
+ if(lastOverLane) lastOverLane.classList.remove('drag-over');
+ overLane.classList.add('drag-over');
+ lastOverLane = overLane;
+ }
+ };
+ const onMouseUp = (upEvent) => {
+ clipElement.classList.remove('dragging');
+ if (lastOverLane) lastOverLane.classList.remove('drag-over');
+ clipElement.style.transform = '';
+ document.removeEventListener('mousemove', onMouseMove);
+ document.removeEventListener('mouseup', onMouseUp);
+ const finalLane = lastOverLane;
+ if (!finalLane) return;
+ const newTrackId = finalLane.dataset.trackId;
+ const timelineContainer = finalLane.querySelector('.timeline-container');
+ const wrapperRect = timelineContainer.getBoundingClientRect();
+ const newLeftPx = (upEvent.clientX - wrapperRect.left) - clickOffsetInClip + timelineContainer.scrollLeft;
+
+ const constrainedLeftPx = Math.max(0, newLeftPx);
+ const newStartTime = constrainedLeftPx / currentPixelsPerSecond;
+
+ updateAudioClipProperties(clipId, { trackId: Number(newTrackId), startTime: newStartTime });
+ renderAudioEditor();
+ };
+ document.addEventListener('mousemove', onMouseMove);
+ document.addEventListener('mouseup', onMouseUp);
+ return;
+ }
+
+ const timelineContainer = e.target.closest('.timeline-container');
+ if (timelineContainer) {
+ e.preventDefault();
+ const handleSeek = (event) => {
+ const rect = timelineContainer.getBoundingClientRect();
+ const scrollLeft = timelineContainer.scrollLeft;
+ const clickX = event.clientX - rect.left;
+ const absoluteX = clickX + scrollLeft;
+ const newTime = absoluteX / currentPixelsPerSecond;
+ seekAudioEditor(newTime);
+ };
+ handleSeek(e);
+ const onMouseMoveSeek = (moveEvent) => handleSeek(moveEvent);
+ const onMouseUpSeek = () => { document.removeEventListener('mousemove', onMouseMoveSeek); document.removeEventListener('mouseup', onMouseUpSeek); };
+ document.addEventListener('mousemove', onMouseMoveSeek);
+ document.addEventListener('mouseup', onMouseUpSeek);
+ }
+ });
+}
+
+export function updateAudioEditorUI() {
+ const playBtn = document.getElementById('audio-editor-play-btn');
+ if (!playBtn) return;
+ if (appState.global.isAudioEditorPlaying) {
+ playBtn.classList.remove('fa-play');
+ playBtn.classList.add('fa-pause');
+ } else {
+ playBtn.classList.remove('fa-pause');
+ playBtn.classList.add('fa-play');
+ }
+}
+
+export function updatePlayheadVisual(pixels) {
+ document.querySelectorAll('.audio-track-lane .playhead').forEach(ph => {
+ ph.style.left = `${pixels}px`;
+ });
+}
+
+export function resetPlayheadVisual() {
+ document.querySelectorAll('.audio-track-lane .playhead').forEach(ph => {
+ ph.style.left = '0px';
+ });
+}
\ No newline at end of file
diff --git a/assets/js/creations/config.js b/assets/js/creations/config.js
index 69b9edd..f264d34 100644
--- a/assets/js/creations/config.js
+++ b/assets/js/creations/config.js
@@ -11,4 +11,7 @@ export const DEFAULT_PAN = 0.0;
// --- ADICIONADO ---
// Constantes para o layout do editor de áudio
export const PIXELS_PER_STEP = 32; // Cada step (1/16) terá 32px de largura
-export const PIXELS_PER_BAR = 512; // 16 steps * 32px/step = 512px por compasso (bar)
\ No newline at end of file
+export const PIXELS_PER_BAR = 512; // 16 steps * 32px/step = 512px por compasso (bar)
+
+// Níveis de zoom pré-definidos (fator de multiplicação)
+export const ZOOM_LEVELS = [0.25, 0.5, 1.0, 2.0, 4.0, 8.0];
\ No newline at end of file
diff --git a/assets/js/creations/file.js b/assets/js/creations/file.js
index dd21b72..2c41b63 100644
--- a/assets/js/creations/file.js
+++ b/assets/js/creations/file.js
@@ -1,13 +1,9 @@
// js/file.js
-import { appState, loadAudioForTrack } from "./state.js";
-import { getTotalSteps } from "./utils.js";
-import { renderApp, getSamplePathMap } from "./ui.js";
-import { DEFAULT_PAN, DEFAULT_VOLUME, NOTE_LENGTH, TICKS_PER_BAR } from "./config.js";
-import {
- initializeAudioContext,
- getAudioContext,
- getMainGainNode,
-} from "./audio.js";
+import { appState, resetProjectState } from "./state.js";
+import { loadAudioForTrack } from "./pattern/pattern_state.js";
+import { renderAll, getSamplePathMap } from "./ui.js";
+import { DEFAULT_PAN, DEFAULT_VOLUME, NOTE_LENGTH } from "./config.js";
+import { initializeAudioContext, getAudioContext, getMainGainNode } from "./audio.js";
export async function handleFileLoad(file) {
let xmlContent = "";
@@ -31,12 +27,31 @@ export async function handleFileLoad(file) {
}
}
+export async function loadProjectFromServer(fileName) {
+ try {
+ const response = await fetch(`mmp/${fileName}`);
+ if (!response.ok)
+ throw new Error(`Não foi possível carregar o arquivo ${fileName}`);
+
+ const xmlContent = await response.text();
+ await parseMmpContent(xmlContent);
+ return true;
+ } catch (error) {
+ console.error("Erro ao carregar projeto do servidor:", error);
+ console.error(error);
+ alert(`Erro ao carregar projeto: ${error.message}`);
+ return false;
+ }
+}
+
export async function parseMmpContent(xmlString) {
+ resetProjectState();
initializeAudioContext();
+
const parser = new DOMParser();
const xmlDoc = parser.parseFromString(xmlString, "application/xml");
- appState.originalXmlDoc = xmlDoc;
+ appState.global.originalXmlDoc = xmlDoc;
let newTracks = [];
const head = xmlDoc.querySelector("head");
@@ -48,25 +63,27 @@ export async function parseMmpContent(xmlString) {
const allBBTrackNodes = Array.from(xmlDoc.querySelectorAll('song > trackcontainer[type="song"] > track[type="1"]'));
if (allBBTrackNodes.length === 0) {
- appState.tracks = []; renderApp(); return;
+ appState.pattern.tracks = [];
+ renderAll();
+ return;
}
- // --- INÍCIO DA CORREÇÃO FINAL DE ORDENAÇÃO ---
- // A lista de NOMES é ordenada em ordem CRESCENTE (a ordem correta, cronológica).
const sortedBBTrackNameNodes = [...allBBTrackNodes].sort((a, b) => {
const bbtcoA = a.querySelector('bbtco');
const bbtcoB = b.querySelector('bbtco');
const posA = bbtcoA ? parseInt(bbtcoA.getAttribute('pos'), 10) : Infinity;
const posB = bbtcoB ? parseInt(bbtcoB.getAttribute('pos'), 10) : Infinity;
- return posA - posB; // Ordem crescente
+ return posA - posB;
});
const dataSourceTrack = allBBTrackNodes[0];
- appState.currentBeatBasslineName = dataSourceTrack.getAttribute("name") || "Beat/Bassline";
+ appState.global.currentBeatBasslineName = dataSourceTrack.getAttribute("name") || "Beat/Bassline";
const bbTrackContainer = dataSourceTrack.querySelector('bbtrack > trackcontainer');
if (!bbTrackContainer) {
- appState.tracks = []; renderApp(); return;
+ appState.pattern.tracks = [];
+ renderAll();
+ return;
}
const instrumentTracks = bbTrackContainer.querySelectorAll('track[type="0"]');
@@ -84,13 +101,11 @@ export async function parseMmpContent(xmlString) {
}
const allPatternsNodeList = trackNode.querySelectorAll("pattern");
- // A lista de CONTEÚDO dos patterns é ordenada de forma DECRESCENTE para corresponder.
const allPatternsArray = Array.from(allPatternsNodeList).sort((a, b) => {
const posA = parseInt(a.getAttribute('pos'), 10) || 0;
const posB = parseInt(b.getAttribute('pos'), 10) || 0;
- return posB - posA; // Ordem decrescente
+ return posB - posA;
});
- // --- FIM DA CORREÇÃO FINAL DE ORDENAÇÃO ---
const patterns = sortedBBTrackNameNodes.map((bbTrack, index) => {
const patternNode = allPatternsArray[index];
@@ -159,13 +174,15 @@ export async function parseMmpContent(xmlString) {
let isFirstTrackWithNotes = true;
newTracks.forEach(track => {
- const audioContext = getAudioContext();
- track.gainNode = audioContext.createGain();
- track.pannerNode = audioContext.createStereoPanner();
+ // --- INÍCIO DA CORREÇÃO ---
+ // Cria os nós de áudio usando os construtores do Tone.js
+ track.gainNode = new Tone.Gain(Tone.gainToDb(track.volume));
+ track.pannerNode = new Tone.Panner(track.pan);
+
+ // Conecta a cadeia de áudio: Gain -> Panner -> Saída Principal (Destination)
track.gainNode.connect(track.pannerNode);
track.pannerNode.connect(getMainGainNode());
- track.gainNode.gain.value = track.volume;
- track.pannerNode.pan.value = track.pan;
+ // --- FIM DA CORREÇÃO ---
if (isFirstTrackWithNotes) {
const activeIdx = track.activePatternIndex || 0;
@@ -187,13 +204,13 @@ export async function parseMmpContent(xmlString) {
console.error("Ocorreu um erro ao carregar os áudios do projeto:", error);
}
- appState.tracks = newTracks;
- appState.activeTrackId = appState.tracks[0]?.id || null;
- renderApp();
+ appState.pattern.tracks = newTracks;
+ appState.pattern.activeTrackId = appState.pattern.tracks[0]?.id || null;
+ renderAll();
}
export function generateMmpFile() {
- if (appState.originalXmlDoc) {
+ if (appState.global.originalXmlDoc) {
modifyAndSaveExistingMmp();
} else {
generateNewMmp();
@@ -202,11 +219,9 @@ export function generateMmpFile() {
function createTrackXml(track) {
if (track.patterns.length === 0) return "";
-
const ticksPerStep = 12;
const lmmsVolume = Math.round(track.volume * 100);
const lmmsPan = Math.round(track.pan * 100);
-
const patternsXml = track.patterns.map(pattern => {
const patternNotes = pattern.steps.map((isActive, index) => {
if (isActive) {
@@ -215,12 +230,10 @@ function createTrackXml(track) {
}
return "";
}).join("\n ");
-
return `
${patternNotes}
`;
}).join('\n ');
-
return `