versão 2.0
Deploy / Deploy (push) Successful in 40s
Details
Deploy / Deploy (push) Successful in 40s
Details
This commit is contained in:
parent
d77fe91df1
commit
531fb7b36a
|
|
@ -1,6 +1,4 @@
|
||||||
/* =============================================== */
|
/* MMPCreator - Folha de Estilos Principal */
|
||||||
/* VÁRIAVEIS GLOBAIS (ROOT)
|
|
||||||
/* =============================================== */
|
|
||||||
:root {
|
:root {
|
||||||
--bg-body: #2d3035;
|
--bg-body: #2d3035;
|
||||||
--bg-toolbar: #3b3f45;
|
--bg-toolbar: #3b3f45;
|
||||||
|
|
@ -17,90 +15,115 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
/* =============================================== */
|
/* =============================================== */
|
||||||
/* ESTILOS GLOBAIS E LAYOUT PRINCIPAL (CORRIGIDO)
|
/* LAYOUT E ESTRUTURA GLOBAL
|
||||||
/* =============================================== */
|
/* =============================================== */
|
||||||
body {
|
body {
|
||||||
margin: 0;
|
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);
|
background-color: var(--bg-body);
|
||||||
color: var(--text-light);
|
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;
|
height: 100vh;
|
||||||
display: flex; /* Usamos flex no body para o main-content crescer */
|
overflow: hidden;
|
||||||
flex-direction: column;
|
display: flex;
|
||||||
}
|
}
|
||||||
|
|
||||||
body.sidebar-hidden {
|
body.knob-dragging { cursor: ns-resize; }
|
||||||
padding-left: 0;
|
body.slice-tool-active .timeline-container { cursor: crosshair; }
|
||||||
}
|
|
||||||
|
|
||||||
body.knob-dragging {
|
.app-container {
|
||||||
cursor: ns-resize;
|
|
||||||
}
|
|
||||||
|
|
||||||
.main-content {
|
|
||||||
padding: 1rem;
|
|
||||||
flex-grow: 1; /* Faz o conteúdo principal ocupar o espaço restante */
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 1rem;
|
flex-grow: 1;
|
||||||
overflow: hidden; /* Evita que o conteúdo transborde */
|
overflow: hidden;
|
||||||
height: 100%; /* Garante que o flexbox interno funcione */
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* =============================================== */
|
/* =============================================== */
|
||||||
/* BARRA LATERAL (SAMPLE BROWSER)
|
/* BARRA LATERAL (SAMPLE BROWSER)
|
||||||
/* =============================================== */
|
/* =============================================== */
|
||||||
.sample-browser {
|
.sample-browser {
|
||||||
position: fixed;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
width: 300px;
|
width: 300px;
|
||||||
height: 100vh;
|
|
||||||
background-color: var(--bg-toolbar);
|
background-color: var(--bg-toolbar);
|
||||||
border-right: 2px solid var(--border-color);
|
border-right: 2px solid var(--border-color);
|
||||||
z-index: 1500;
|
z-index: 10;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
transform: translateX(0);
|
transition: min-width 0.3s ease, width 0.3s ease;
|
||||||
transition: transform .3s ease;
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
body.sidebar-hidden .sample-browser {
|
body.sidebar-hidden .sample-browser {
|
||||||
transform: translateX(-100%);
|
width: 0;
|
||||||
|
min-width: 0;
|
||||||
|
border-right: none;
|
||||||
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.browser-header {
|
.browser-header {
|
||||||
padding: 15px;
|
padding: 15px;
|
||||||
background-color: #2a2c30;
|
background-color: #2a2c30;
|
||||||
border-bottom: 2px solid var(--border-color);
|
border-bottom: 2px solid var(--border-color);
|
||||||
text-align: center;
|
text-align: center;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
color: var(--text-light);
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.browser-content {
|
.browser-content {
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
}
|
}
|
||||||
|
.browser-content ul {
|
||||||
.browser-content ul { list-style: none; padding-left: 15px; }
|
padding-left: 15px;
|
||||||
.browser-content li { padding: 5px; cursor: pointer; border-radius: 3px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; user-select: none; }
|
list-style: 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; }
|
.folder-item > .file-list {
|
||||||
.browser-content li.directory > ul { display: none; }
|
display: none;
|
||||||
.browser-content li.directory.open > ul { display: block; }
|
padding-left: 20px;
|
||||||
.browser-content li.directory.open > .fa-folder { transform: rotate(90deg); }
|
}
|
||||||
|
.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 {
|
#sidebar-toggle {
|
||||||
position: fixed;
|
position: absolute;
|
||||||
top: 60px;
|
top: 60px;
|
||||||
left: 305px;
|
left: 5px;
|
||||||
z-index: 1400;
|
z-index: 1400;
|
||||||
background-color: var(--bg-toolbar);
|
background-color: var(--bg-toolbar);
|
||||||
border: 1px solid var(--border-color);
|
border: 1px solid var(--border-color);
|
||||||
|
|
@ -108,49 +131,19 @@ body.sidebar-hidden .sample-browser {
|
||||||
width: 25px;
|
width: 25px;
|
||||||
height: 40px;
|
height: 40px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
border-top-right-radius: 4px;
|
border-radius: 0 4px 4px 0;
|
||||||
border-bottom-right-radius: 4px;
|
|
||||||
transition: left .3s ease;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
body.sidebar-hidden #sidebar-toggle {
|
|
||||||
left: 5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* =============================================== */
|
/* =============================================== */
|
||||||
/* BARRA DE FERRAMENTAS GLOBAL
|
/* ÁREA DE CONTEÚDO E TOOLBARS
|
||||||
/* =============================================== */
|
/* =============================================== */
|
||||||
.global-toolbar {
|
.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; }
|
||||||
padding: 8px 15px;
|
.main-content { flex-grow: 1; padding: 1rem; display: flex; flex-direction: column; gap: 1rem; overflow: hidden; }
|
||||||
position: fixed;
|
.beat-editor {
|
||||||
top: 0;
|
flex: 1;
|
||||||
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%;
|
|
||||||
background-color: var(--bg-editor);
|
background-color: var(--bg-editor);
|
||||||
border: 1px solid var(--border-color);
|
border: 1px solid var(--border-color);
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
|
|
@ -159,178 +152,43 @@ body.sidebar-hidden .global-toolbar {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
|
.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; }
|
||||||
/* Container para as faixas de áudio, com scroll vertical */
|
.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; }
|
||||||
#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;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/* =============================================== */
|
/* =============================================== */
|
||||||
/* ESTILOS DO EDITOR DE ÁUDIO (MARCADORES)
|
/* EDITOR DE BASES (BEAT EDITOR / STEP SEQUENCER)
|
||||||
/* =============================================== */
|
|
||||||
|
|
||||||
/* 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
|
|
||||||
/* =============================================== */
|
/* =============================================== */
|
||||||
#track-container {
|
#track-container {
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
|
overflow-x: hidden;
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.track-lane {
|
.track-lane {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: stretch;
|
||||||
padding: 8px 10px;
|
|
||||||
background-color: var(--bg-editor);
|
background-color: var(--bg-editor);
|
||||||
border-bottom: 1px solid var(--bg-toolbar);
|
border-bottom: 1px solid var(--bg-toolbar);
|
||||||
border-left: 2px solid transparent;
|
min-height: 72px;
|
||||||
border-right: 2px solid transparent;
|
box-sizing: border-box;
|
||||||
transition: border-color 0.2s, background-color 0.2s;
|
|
||||||
}
|
}
|
||||||
|
.track-lane.active-track { background-color: #40454d; }
|
||||||
.track-lane.active-track {
|
.track-lane.drag-over { border: 2px dashed var(--accent-green); }
|
||||||
background-color: #40454d;
|
.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 { 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 {
|
.track-mute:hover { opacity: 0.8; }
|
||||||
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; }
|
||||||
.track-mute.active {
|
.step-sequencer-wrapper { flex-grow: 1; overflow-x: auto; overflow-y: hidden; padding-bottom: 8px; display: flex; align-items: center; }
|
||||||
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; }
|
|
||||||
.step-sequencer { display: flex; gap: 4px; }
|
.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-wrapper { position: relative; }
|
||||||
.step-marker { position: absolute; top: -16px; left: 1px; font-size: .6rem; color: var(--text-dark); user-select: none; }
|
.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; }
|
.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.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); }
|
.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; }
|
.interactive-input-container { display: flex; align-items: center; justify-content: center; gap: 4px; }
|
||||||
.compasso-group { display: flex; align-items: 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; }
|
.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: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); }
|
#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 { 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-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-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 { 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-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-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); }
|
.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 { 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; }
|
#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); }
|
#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 { 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; }
|
.modal-button:hover { background-color: #4a4f57; border-color: #333; }
|
||||||
|
#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; }
|
||||||
.file-menu-container { position: relative; }
|
#timeline-context-menu div { padding: 8px 15px; cursor: pointer; white-space: nowrap; }
|
||||||
.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; }
|
#timeline-context-menu div:hover { background-color: var(--accent-blue); color: white; }
|
||||||
.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; }
|
|
||||||
|
|
||||||
|
|
||||||
/* =============================================== */
|
/* =============================================== */
|
||||||
/* ESTILOS RESPONSIVOS (MELHORADO)
|
/* ESTILOS RESPONSIVOS
|
||||||
/* =============================================== */
|
/* =============================================== */
|
||||||
@media (max-width: 1200px) {
|
@media (max-width: 1200px) {
|
||||||
.info-display-group {
|
.info-display-group { gap: 2px; }
|
||||||
gap: 2px;
|
.info-display { padding: 4px 6px; }
|
||||||
}
|
.value-input { font-size: 1.2rem; width: 45px; }
|
||||||
.info-display {
|
.compasso-input { width: 20px; }
|
||||||
padding: 4px 6px;
|
|
||||||
}
|
|
||||||
.value-input {
|
|
||||||
font-size: 1.2rem;
|
|
||||||
width: 45px;
|
|
||||||
}
|
|
||||||
.compasso-input {
|
|
||||||
width: 20px;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 992px) {
|
@media (max-width: 992px) {
|
||||||
.global-toolbar {
|
.global-toolbar { gap: 10px; flex-wrap: wrap; height: auto; padding-bottom: 10px; }
|
||||||
gap: 10px;
|
.main-content { padding-top: 100px; }
|
||||||
flex-wrap: wrap;
|
.info-display-group { order: 3; width: 100%; justify-content: space-around; }
|
||||||
height: auto; /* Permite que a toolbar cresça se o conteúdo quebrar linha */
|
.spacer { display: none; }
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 768px) {
|
@media (max-width: 768px) {
|
||||||
body {
|
.sample-browser { transform: translateX(-100%); position: fixed; width: 280px; }
|
||||||
padding-left: 0 !important;
|
body:not(.sidebar-hidden) .sample-browser { transform: translateX(0); }
|
||||||
}
|
#sidebar-toggle { left: 5px; transform: translateX(0); position: fixed; }
|
||||||
.sample-browser {
|
.global-toolbar { padding-left: 45px; }
|
||||||
transform: translateX(-100%);
|
.main-content { padding: 10px; }
|
||||||
position: fixed; /* Volta a ser fixo para deslizar por cima */
|
.track-lane, .audio-track-lane { flex-direction: column; align-items: stretch; gap: 15px; padding: 15px; }
|
||||||
width: 280px;
|
.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; }
|
||||||
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%;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.spectrogram-view-wrapper {
|
/* =============================================== */
|
||||||
position: relative; /* Essencial para o posicionamento absoluto do filho */
|
/* SCROLLBARS
|
||||||
overflow: hidden; /* Garante que a agulha não saia dos limites */
|
/* =============================================== */
|
||||||
}
|
::-webkit-scrollbar { height: 10px; width: 10px; }
|
||||||
|
::-webkit-scrollbar-track { background: var(--border-color); }
|
||||||
.playhead {
|
::-webkit-scrollbar-thumb { background: var(--bg-toolbar); border-radius: 5px; }
|
||||||
position: absolute;
|
::-webkit-scrollbar-thumb:hover { background: #555; }
|
||||||
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 */
|
|
||||||
}
|
|
||||||
|
|
@ -1,316 +1,34 @@
|
||||||
// js/audio.js
|
// 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;
|
// O contexto de áudio agora será gerenciado principalmente pelo Tone.js.
|
||||||
let mainGainNode;
|
// Esta função garante que ele seja iniciado por uma interação do usuário.
|
||||||
let masterPannerNode;
|
export function initializeAudioContext() {
|
||||||
|
if (Tone.context.state !== 'running') {
|
||||||
const timerDisplay = document.getElementById('timer-display');
|
Tone.start();
|
||||||
|
console.log("AudioContext iniciado com Tone.js");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Funções de acesso ao contexto global do Tone.js
|
||||||
export function getAudioContext() {
|
export function getAudioContext() {
|
||||||
return audioContext;
|
return Tone.context;
|
||||||
}
|
}
|
||||||
export function getMainGainNode() {
|
export function getMainGainNode() {
|
||||||
return mainGainNode;
|
return Tone.Destination;
|
||||||
}
|
|
||||||
|
|
||||||
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();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Funções para controlar o volume e pan master
|
||||||
export function updateMasterVolume(volume) {
|
export function updateMasterVolume(volume) {
|
||||||
if (mainGainNode) {
|
// Tone.Destination.volume.value é em decibéis. Convertemos de linear (0-1.5) para dB.
|
||||||
mainGainNode.gain.setValueAtTime(volume, audioContext.currentTime);
|
if (volume === 0) {
|
||||||
|
Tone.Destination.volume.value = -Infinity;
|
||||||
|
} else {
|
||||||
|
Tone.Destination.volume.value = Tone.gainToDb(volume);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function updateMasterPan(pan) {
|
export function updateMasterPan(pan) {
|
||||||
if (masterPannerNode) {
|
// A panorimização master em Tone.js geralmente requer um nó Panner dedicado.
|
||||||
masterPannerNode.pan.setValueAtTime(pan, audioContext.currentTime);
|
// Por enquanto, esta função servirá como um placeholder para futuras implementações.
|
||||||
}
|
console.log("Master Pan ainda não implementado com Tone.js");
|
||||||
}
|
|
||||||
|
|
||||||
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();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
@ -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 = `
|
||||||
|
<div class="ruler-spacer"></div>
|
||||||
|
<div class="timeline-ruler"></div>
|
||||||
|
`;
|
||||||
|
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 = `<div class="loop-handle left"></div><div class="loop-handle right"></div>`;
|
||||||
|
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 = `
|
||||||
|
<div class="track-info">
|
||||||
|
<div class="track-info-header">
|
||||||
|
<i class="fa-solid fa-gear"></i>
|
||||||
|
<span class="track-name">${trackData.name}</span>
|
||||||
|
<div class="track-mute"></div>
|
||||||
|
</div>
|
||||||
|
<div class="track-controls">
|
||||||
|
<div class="knob-container">
|
||||||
|
<div class="knob" data-control="volume"><div class="knob-indicator"></div></div>
|
||||||
|
<span>VOL</span>
|
||||||
|
</div>
|
||||||
|
<div class="knob-container">
|
||||||
|
<div class="knob" data-control="pan"><div class="knob-indicator"></div></div>
|
||||||
|
<span>PAN</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="timeline-container">
|
||||||
|
<div class="spectrogram-view-grid" style="width: ${totalWidth}px;"></div>
|
||||||
|
<div class="playhead"></div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
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 = `
|
||||||
|
<div class="clip-resize-handle left"></div>
|
||||||
|
<span class="clip-name">${clip.name} ${pitchStr}</span>
|
||||||
|
<canvas class="waveform-canvas-clip"></canvas>
|
||||||
|
<div class="clip-resize-handle right"></div>
|
||||||
|
`;
|
||||||
|
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';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
@ -11,4 +11,7 @@ export const DEFAULT_PAN = 0.0;
|
||||||
// --- ADICIONADO ---
|
// --- ADICIONADO ---
|
||||||
// Constantes para o layout do editor de áudio
|
// 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_STEP = 32; // Cada step (1/16) terá 32px de largura
|
||||||
export const PIXELS_PER_BAR = 512; // 16 steps * 32px/step = 512px por compasso (bar)
|
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];
|
||||||
|
|
@ -1,13 +1,9 @@
|
||||||
// js/file.js
|
// js/file.js
|
||||||
import { appState, loadAudioForTrack } from "./state.js";
|
import { appState, resetProjectState } from "./state.js";
|
||||||
import { getTotalSteps } from "./utils.js";
|
import { loadAudioForTrack } from "./pattern/pattern_state.js";
|
||||||
import { renderApp, getSamplePathMap } from "./ui.js";
|
import { renderAll, getSamplePathMap } from "./ui.js";
|
||||||
import { DEFAULT_PAN, DEFAULT_VOLUME, NOTE_LENGTH, TICKS_PER_BAR } from "./config.js";
|
import { DEFAULT_PAN, DEFAULT_VOLUME, NOTE_LENGTH } from "./config.js";
|
||||||
import {
|
import { initializeAudioContext, getAudioContext, getMainGainNode } from "./audio.js";
|
||||||
initializeAudioContext,
|
|
||||||
getAudioContext,
|
|
||||||
getMainGainNode,
|
|
||||||
} from "./audio.js";
|
|
||||||
|
|
||||||
export async function handleFileLoad(file) {
|
export async function handleFileLoad(file) {
|
||||||
let xmlContent = "";
|
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) {
|
export async function parseMmpContent(xmlString) {
|
||||||
|
resetProjectState();
|
||||||
initializeAudioContext();
|
initializeAudioContext();
|
||||||
|
|
||||||
const parser = new DOMParser();
|
const parser = new DOMParser();
|
||||||
const xmlDoc = parser.parseFromString(xmlString, "application/xml");
|
const xmlDoc = parser.parseFromString(xmlString, "application/xml");
|
||||||
|
|
||||||
appState.originalXmlDoc = xmlDoc;
|
appState.global.originalXmlDoc = xmlDoc;
|
||||||
let newTracks = [];
|
let newTracks = [];
|
||||||
|
|
||||||
const head = xmlDoc.querySelector("head");
|
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"]'));
|
const allBBTrackNodes = Array.from(xmlDoc.querySelectorAll('song > trackcontainer[type="song"] > track[type="1"]'));
|
||||||
if (allBBTrackNodes.length === 0) {
|
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 sortedBBTrackNameNodes = [...allBBTrackNodes].sort((a, b) => {
|
||||||
const bbtcoA = a.querySelector('bbtco');
|
const bbtcoA = a.querySelector('bbtco');
|
||||||
const bbtcoB = b.querySelector('bbtco');
|
const bbtcoB = b.querySelector('bbtco');
|
||||||
const posA = bbtcoA ? parseInt(bbtcoA.getAttribute('pos'), 10) : Infinity;
|
const posA = bbtcoA ? parseInt(bbtcoA.getAttribute('pos'), 10) : Infinity;
|
||||||
const posB = bbtcoB ? parseInt(bbtcoB.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];
|
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');
|
const bbTrackContainer = dataSourceTrack.querySelector('bbtrack > trackcontainer');
|
||||||
if (!bbTrackContainer) {
|
if (!bbTrackContainer) {
|
||||||
appState.tracks = []; renderApp(); return;
|
appState.pattern.tracks = [];
|
||||||
|
renderAll();
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const instrumentTracks = bbTrackContainer.querySelectorAll('track[type="0"]');
|
const instrumentTracks = bbTrackContainer.querySelectorAll('track[type="0"]');
|
||||||
|
|
@ -84,13 +101,11 @@ export async function parseMmpContent(xmlString) {
|
||||||
}
|
}
|
||||||
|
|
||||||
const allPatternsNodeList = trackNode.querySelectorAll("pattern");
|
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 allPatternsArray = Array.from(allPatternsNodeList).sort((a, b) => {
|
||||||
const posA = parseInt(a.getAttribute('pos'), 10) || 0;
|
const posA = parseInt(a.getAttribute('pos'), 10) || 0;
|
||||||
const posB = parseInt(b.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 patterns = sortedBBTrackNameNodes.map((bbTrack, index) => {
|
||||||
const patternNode = allPatternsArray[index];
|
const patternNode = allPatternsArray[index];
|
||||||
|
|
@ -159,13 +174,15 @@ export async function parseMmpContent(xmlString) {
|
||||||
|
|
||||||
let isFirstTrackWithNotes = true;
|
let isFirstTrackWithNotes = true;
|
||||||
newTracks.forEach(track => {
|
newTracks.forEach(track => {
|
||||||
const audioContext = getAudioContext();
|
// --- INÍCIO DA CORREÇÃO ---
|
||||||
track.gainNode = audioContext.createGain();
|
// Cria os nós de áudio usando os construtores do Tone.js
|
||||||
track.pannerNode = audioContext.createStereoPanner();
|
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.gainNode.connect(track.pannerNode);
|
||||||
track.pannerNode.connect(getMainGainNode());
|
track.pannerNode.connect(getMainGainNode());
|
||||||
track.gainNode.gain.value = track.volume;
|
// --- FIM DA CORREÇÃO ---
|
||||||
track.pannerNode.pan.value = track.pan;
|
|
||||||
|
|
||||||
if (isFirstTrackWithNotes) {
|
if (isFirstTrackWithNotes) {
|
||||||
const activeIdx = track.activePatternIndex || 0;
|
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);
|
console.error("Ocorreu um erro ao carregar os áudios do projeto:", error);
|
||||||
}
|
}
|
||||||
|
|
||||||
appState.tracks = newTracks;
|
appState.pattern.tracks = newTracks;
|
||||||
appState.activeTrackId = appState.tracks[0]?.id || null;
|
appState.pattern.activeTrackId = appState.pattern.tracks[0]?.id || null;
|
||||||
renderApp();
|
renderAll();
|
||||||
}
|
}
|
||||||
|
|
||||||
export function generateMmpFile() {
|
export function generateMmpFile() {
|
||||||
if (appState.originalXmlDoc) {
|
if (appState.global.originalXmlDoc) {
|
||||||
modifyAndSaveExistingMmp();
|
modifyAndSaveExistingMmp();
|
||||||
} else {
|
} else {
|
||||||
generateNewMmp();
|
generateNewMmp();
|
||||||
|
|
@ -202,11 +219,9 @@ export function generateMmpFile() {
|
||||||
|
|
||||||
function createTrackXml(track) {
|
function createTrackXml(track) {
|
||||||
if (track.patterns.length === 0) return "";
|
if (track.patterns.length === 0) return "";
|
||||||
|
|
||||||
const ticksPerStep = 12;
|
const ticksPerStep = 12;
|
||||||
const lmmsVolume = Math.round(track.volume * 100);
|
const lmmsVolume = Math.round(track.volume * 100);
|
||||||
const lmmsPan = Math.round(track.pan * 100);
|
const lmmsPan = Math.round(track.pan * 100);
|
||||||
|
|
||||||
const patternsXml = track.patterns.map(pattern => {
|
const patternsXml = track.patterns.map(pattern => {
|
||||||
const patternNotes = pattern.steps.map((isActive, index) => {
|
const patternNotes = pattern.steps.map((isActive, index) => {
|
||||||
if (isActive) {
|
if (isActive) {
|
||||||
|
|
@ -215,12 +230,10 @@ function createTrackXml(track) {
|
||||||
}
|
}
|
||||||
return "";
|
return "";
|
||||||
}).join("\n ");
|
}).join("\n ");
|
||||||
|
|
||||||
return `<pattern type="0" pos="${pattern.pos}" muted="0" steps="${pattern.steps.length}" name="${pattern.name}">
|
return `<pattern type="0" pos="${pattern.pos}" muted="0" steps="${pattern.steps.length}" name="${pattern.name}">
|
||||||
${patternNotes}
|
${patternNotes}
|
||||||
</pattern>`;
|
</pattern>`;
|
||||||
}).join('\n ');
|
}).join('\n ');
|
||||||
|
|
||||||
return `
|
return `
|
||||||
<track type="0" solo="0" muted="0" name="${track.name}">
|
<track type="0" solo="0" muted="0" name="${track.name}">
|
||||||
<instrumenttrack vol="${lmmsVolume}" pitch="0" fxch="0" pitchrange="1" basenote="57" usemasterpitch="1" pan="${lmmsPan}">
|
<instrumenttrack vol="${lmmsVolume}" pitch="0" fxch="0" pitchrange="1" basenote="57" usemasterpitch="1" pan="${lmmsPan}">
|
||||||
|
|
@ -235,39 +248,23 @@ function createTrackXml(track) {
|
||||||
|
|
||||||
function modifyAndSaveExistingMmp() {
|
function modifyAndSaveExistingMmp() {
|
||||||
console.log("Modificando arquivo .mmp existente...");
|
console.log("Modificando arquivo .mmp existente...");
|
||||||
const xmlDoc = appState.originalXmlDoc.cloneNode(true);
|
const xmlDoc = appState.global.originalXmlDoc.cloneNode(true);
|
||||||
const head = xmlDoc.querySelector("head");
|
const head = xmlDoc.querySelector("head");
|
||||||
if (head) {
|
if (head) {
|
||||||
head.setAttribute("bpm", document.getElementById("bpm-input").value);
|
head.setAttribute("bpm", document.getElementById("bpm-input").value);
|
||||||
head.setAttribute("num_bars", document.getElementById("bars-input").value);
|
head.setAttribute("num_bars", document.getElementById("bars-input").value);
|
||||||
head.setAttribute(
|
head.setAttribute("timesig_numerator", document.getElementById("compasso-a-input").value);
|
||||||
"timesig_numerator",
|
head.setAttribute("timesig_denominator", document.getElementById("compasso-b-input").value);
|
||||||
document.getElementById("compasso-a-input").value
|
|
||||||
);
|
|
||||||
head.setAttribute(
|
|
||||||
"timesig_denominator",
|
|
||||||
document.getElementById("compasso-b-input").value
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const bbTrackContainer = xmlDoc.querySelector('track[type="1"] > bbtrack > trackcontainer');
|
const bbTrackContainer = xmlDoc.querySelector('track[type="1"] > bbtrack > trackcontainer');
|
||||||
|
|
||||||
if (bbTrackContainer) {
|
if (bbTrackContainer) {
|
||||||
bbTrackContainer.querySelectorAll('track[type="0"]').forEach(node => node.remove());
|
bbTrackContainer.querySelectorAll('track[type="0"]').forEach(node => node.remove());
|
||||||
|
const tracksXml = appState.pattern.tracks.map((track) => createTrackXml(track)).join("");
|
||||||
const tracksXml = appState.tracks
|
const tempDoc = new DOMParser().parseFromString(`<root>${tracksXml}</root>`, "application/xml");
|
||||||
.map((track) => createTrackXml(track))
|
|
||||||
.join("");
|
|
||||||
|
|
||||||
const tempDoc = new DOMParser().parseFromString(
|
|
||||||
`<root>${tracksXml}</root>`,
|
|
||||||
"application/xml"
|
|
||||||
);
|
|
||||||
Array.from(tempDoc.documentElement.children).forEach((newTrackNode) => {
|
Array.from(tempDoc.documentElement.children).forEach((newTrackNode) => {
|
||||||
bbTrackContainer.appendChild(newTrackNode);
|
bbTrackContainer.appendChild(newTrackNode);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const serializer = new XMLSerializer();
|
const serializer = new XMLSerializer();
|
||||||
const mmpContent = serializer.serializeToString(xmlDoc);
|
const mmpContent = serializer.serializeToString(xmlDoc);
|
||||||
downloadFile(mmpContent, "projeto_editado.mmp");
|
downloadFile(mmpContent, "projeto_editado.mmp");
|
||||||
|
|
@ -278,10 +275,7 @@ function generateNewMmp() {
|
||||||
const sig_num = document.getElementById("compasso-a-input").value;
|
const sig_num = document.getElementById("compasso-a-input").value;
|
||||||
const sig_den = document.getElementById("compasso-b-input").value;
|
const sig_den = document.getElementById("compasso-b-input").value;
|
||||||
const num_bars = document.getElementById("bars-input").value;
|
const num_bars = document.getElementById("bars-input").value;
|
||||||
const tracksXml = appState.tracks
|
const tracksXml = appState.pattern.tracks.map((track) => createTrackXml(track)).join("");
|
||||||
.map((track) => createTrackXml(track))
|
|
||||||
.join("");
|
|
||||||
|
|
||||||
const mmpContent = `<?xml version="1.0"?>
|
const mmpContent = `<?xml version="1.0"?>
|
||||||
<!DOCTYPE lmms-project>
|
<!DOCTYPE lmms-project>
|
||||||
<lmms-project version="1.0" type="song" creator="MMPCreator" creatorversion="1.0">
|
<lmms-project version="1.0" type="song" creator="MMPCreator" creatorversion="1.0">
|
||||||
|
|
@ -320,19 +314,4 @@ function downloadFile(content, fileName) {
|
||||||
a.click();
|
a.click();
|
||||||
document.body.removeChild(a);
|
document.body.removeChild(a);
|
||||||
URL.revokeObjectURL(url);
|
URL.revokeObjectURL(url);
|
||||||
}
|
|
||||||
|
|
||||||
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);
|
|
||||||
alert(`Erro ao carregar projeto: ${error.message}`);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
@ -1,31 +1,24 @@
|
||||||
// js/main.js
|
// js/main.js
|
||||||
import {
|
import { appState, resetProjectState } from "./state.js";
|
||||||
appState,
|
import { addTrackToState, removeLastTrackFromState } from "./pattern/pattern_state.js";
|
||||||
addTrackToState,
|
import { addAudioTrackLane } from "./audio/audio_state.js";
|
||||||
removeLastTrackFromState,
|
import { updateTransportLoop } from "./audio/audio_audio.js";
|
||||||
} from "./state.js";
|
|
||||||
import {
|
import {
|
||||||
togglePlayback,
|
togglePlayback,
|
||||||
stopPlayback,
|
stopPlayback,
|
||||||
rewindPlayback,
|
rewindPlayback,
|
||||||
initializeAudioContext,
|
} from "./pattern/pattern_audio.js";
|
||||||
updateMasterVolume,
|
import {
|
||||||
updateMasterPan,
|
|
||||||
startAudioEditorPlayback,
|
startAudioEditorPlayback,
|
||||||
stopAudioEditorPlayback,
|
stopAudioEditorPlayback,
|
||||||
restartAudioEditorIfPlaying,
|
restartAudioEditorIfPlaying,
|
||||||
} from "./audio.js";
|
} from "./audio/audio_audio.js";
|
||||||
|
import { initializeAudioContext } from "./audio.js";
|
||||||
import { handleFileLoad, generateMmpFile } from "./file.js";
|
import { handleFileLoad, generateMmpFile } from "./file.js";
|
||||||
import {
|
import { renderAll, loadAndRenderSampleBrowser, showOpenProjectModal, closeOpenProjectModal } from "./ui.js";
|
||||||
renderApp,
|
import { renderAudioEditor } from "./audio/audio_ui.js";
|
||||||
redrawSequencer,
|
|
||||||
loadAndRenderSampleBrowser,
|
|
||||||
showOpenProjectModal,
|
|
||||||
closeOpenProjectModal,
|
|
||||||
handleSampleUpload,
|
|
||||||
} from "./ui.js";
|
|
||||||
import { adjustValue, enforceNumericInput } from "./utils.js";
|
import { adjustValue, enforceNumericInput } from "./utils.js";
|
||||||
import { DEFAULT_PAN, DEFAULT_VOLUME } from "./config.js";
|
import { ZOOM_LEVELS } from "./config.js";
|
||||||
|
|
||||||
document.addEventListener("DOMContentLoaded", () => {
|
document.addEventListener("DOMContentLoaded", () => {
|
||||||
const newProjectBtn = document.getElementById("new-project-btn");
|
const newProjectBtn = document.getElementById("new-project-btn");
|
||||||
|
|
@ -39,8 +32,10 @@ document.addEventListener("DOMContentLoaded", () => {
|
||||||
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 audioEditorLoopBtn = document.getElementById("audio-editor-loop-btn");
|
||||||
|
const addAudioTrackBtn = document.getElementById("add-audio-track-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 sliceToolBtn = document.getElementById("slice-tool-btn");
|
||||||
const mmpFileInput = document.getElementById("mmp-file-input");
|
const mmpFileInput = document.getElementById("mmp-file-input");
|
||||||
const sampleFileInput = document.getElementById("sample-file-input");
|
const sampleFileInput = document.getElementById("sample-file-input");
|
||||||
const openProjectModal = document.getElementById("open-project-modal");
|
const openProjectModal = document.getElementById("open-project-modal");
|
||||||
|
|
@ -48,225 +43,134 @@ document.addEventListener("DOMContentLoaded", () => {
|
||||||
const loadFromComputerBtn = document.getElementById("load-from-computer-btn");
|
const loadFromComputerBtn = document.getElementById("load-from-computer-btn");
|
||||||
const sidebarToggle = document.getElementById("sidebar-toggle");
|
const sidebarToggle = document.getElementById("sidebar-toggle");
|
||||||
const addBarBtn = document.getElementById("add-bar-btn");
|
const addBarBtn = document.getElementById("add-bar-btn");
|
||||||
const masterVolumeKnob = document.getElementById("master-volume-knob");
|
const zoomInBtn = document.getElementById("zoom-in-btn");
|
||||||
const masterPanKnob = document.getElementById("master-pan-knob");
|
const zoomOutBtn = document.getElementById("zoom-out-btn");
|
||||||
|
|
||||||
newProjectBtn.addEventListener("click", () => {
|
newProjectBtn.addEventListener("click", () => {
|
||||||
if (
|
if ((appState.pattern.tracks.length > 0 || appState.audio.clips.length > 0) && !confirm("Você tem certeza? Alterações não salvas serão perdidas.")) return;
|
||||||
appState.tracks.length > 0 &&
|
resetProjectState();
|
||||||
!confirm("Você tem certeza? Alterações não salvas serão perdidas.")
|
|
||||||
)
|
|
||||||
return;
|
|
||||||
Object.assign(appState, {
|
|
||||||
tracks: [],
|
|
||||||
audioTracks: [],
|
|
||||||
activeTrackId: null,
|
|
||||||
isPlaying: false,
|
|
||||||
playbackIntervalId: null,
|
|
||||||
currentStep: 0,
|
|
||||||
metronomeEnabled: false,
|
|
||||||
originalXmlDoc: null,
|
|
||||||
currentBeatBasslineName: 'Novo Projeto',
|
|
||||||
masterVolume: DEFAULT_VOLUME,
|
|
||||||
masterPan: DEFAULT_PAN
|
|
||||||
});
|
|
||||||
document.getElementById('bpm-input').value = 140;
|
document.getElementById('bpm-input').value = 140;
|
||||||
document.getElementById('bars-input').value = 1;
|
document.getElementById('bars-input').value = 1;
|
||||||
document.getElementById('compasso-a-input').value = 4;
|
document.getElementById('compasso-a-input').value = 4;
|
||||||
document.getElementById('compasso-b-input').value = 4;
|
document.getElementById('compasso-b-input').value = 4;
|
||||||
const titleElement = document.getElementById('beat-bassline-title');
|
const titleElement = document.getElementById('beat-bassline-title');
|
||||||
if(titleElement) titleElement.textContent = 'Novo Projeto';
|
if(titleElement) titleElement.textContent = 'Novo Projeto';
|
||||||
renderApp();
|
renderAll();
|
||||||
setupMasterKnobs();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
addBarBtn.addEventListener("click", () => {
|
addBarBtn.addEventListener("click", () => {
|
||||||
const barsInput = document.getElementById("bars-input");
|
const barsInput = document.getElementById("bars-input");
|
||||||
if (barsInput) adjustValue(barsInput, 1);
|
if (barsInput) adjustValue(barsInput, 1);
|
||||||
});
|
});
|
||||||
|
|
||||||
function setupMasterKnobs() {
|
|
||||||
function updateMasterKnobVisual(knobElement, controlType) {
|
|
||||||
const indicator = knobElement.querySelector(".knob-indicator");
|
|
||||||
if (!indicator) return;
|
|
||||||
const minAngle = -135;
|
|
||||||
const maxAngle = 135;
|
|
||||||
let percentage = 0.5;
|
|
||||||
let title = "";
|
|
||||||
if (controlType === "volume") {
|
|
||||||
const value = appState.masterVolume;
|
|
||||||
percentage = value / 1.5;
|
|
||||||
title = `Volume Master: ${Math.round(value * 100)}%`;
|
|
||||||
} else {
|
|
||||||
const value = appState.masterPan;
|
|
||||||
percentage = (value + 1) / 2;
|
|
||||||
const panDisplay = Math.round(value * 100);
|
|
||||||
title = `Pan Master: ${ panDisplay === 0 ? "Centro" : panDisplay < 0 ? `${-panDisplay} L` : `${panDisplay} R` }`;
|
|
||||||
}
|
|
||||||
const angle = minAngle + percentage * (maxAngle - minAngle);
|
|
||||||
indicator.style.transform = `translateX(-50%) rotate(${angle}deg)`;
|
|
||||||
knobElement.title = title;
|
|
||||||
}
|
|
||||||
function addMasterKnobInteraction(knobElement, controlType) {
|
|
||||||
knobElement.addEventListener("wheel", (e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
const step = 0.05;
|
|
||||||
const direction = e.deltaY < 0 ? 1 : -1;
|
|
||||||
if (controlType === "volume") {
|
|
||||||
const newValue = appState.masterVolume + direction * step;
|
|
||||||
appState.masterVolume = Math.max(0, Math.min(1.5, newValue));
|
|
||||||
updateMasterVolume(appState.masterVolume);
|
|
||||||
} else {
|
|
||||||
const newValue = appState.masterPan + direction * step;
|
|
||||||
appState.masterPan = Math.max(-1, Math.min(1, newValue));
|
|
||||||
updateMasterPan(appState.masterPan);
|
|
||||||
}
|
|
||||||
updateMasterKnobVisual(knobElement, controlType);
|
|
||||||
});
|
|
||||||
knobElement.addEventListener("mousedown", (e) => {
|
|
||||||
if (e.button !== 0) return;
|
|
||||||
e.preventDefault();
|
|
||||||
const startY = e.clientY;
|
|
||||||
const startValue = controlType === "volume" ? appState.masterVolume : appState.masterPan;
|
|
||||||
document.body.classList.add("knob-dragging");
|
|
||||||
function onMouseMove(moveEvent) {
|
|
||||||
const deltaY = startY - moveEvent.clientY;
|
|
||||||
const sensitivity = controlType === "volume" ? 150 : 200;
|
|
||||||
const newValue = startValue + deltaY / sensitivity;
|
|
||||||
if (controlType === "volume") {
|
|
||||||
appState.masterVolume = Math.max(0, Math.min(1.5, newValue));
|
|
||||||
updateMasterVolume(appState.masterVolume);
|
|
||||||
} else {
|
|
||||||
appState.masterPan = Math.max(-1, Math.min(1, newValue));
|
|
||||||
updateMasterPan(appState.masterPan);
|
|
||||||
}
|
|
||||||
updateMasterKnobVisual(knobElement, controlType);
|
|
||||||
}
|
|
||||||
function onMouseUp() {
|
|
||||||
document.body.classList.remove("knob-dragging");
|
|
||||||
document.removeEventListener("mousemove", onMouseMove);
|
|
||||||
document.removeEventListener("mouseup", onMouseUp);
|
|
||||||
}
|
|
||||||
document.addEventListener("mousemove", onMouseMove);
|
|
||||||
document.addEventListener("mouseup", onMouseUp);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
addMasterKnobInteraction(masterVolumeKnob, "volume");
|
|
||||||
updateMasterKnobVisual(masterVolumeKnob, "volume");
|
|
||||||
addMasterKnobInteraction(masterPanKnob, "pan");
|
|
||||||
updateMasterKnobVisual(masterPanKnob, "pan");
|
|
||||||
}
|
|
||||||
|
|
||||||
openMmpBtn.addEventListener("click", showOpenProjectModal);
|
openMmpBtn.addEventListener("click", showOpenProjectModal);
|
||||||
loadFromComputerBtn.addEventListener("click", () => mmpFileInput.click());
|
loadFromComputerBtn.addEventListener("click", () => mmpFileInput.click());
|
||||||
mmpFileInput.addEventListener("change", async (event) => {
|
mmpFileInput.addEventListener("change", (event) => { const file = event.target.files[0]; if (file) { handleFileLoad(file).then(() => closeOpenProjectModal()); } });
|
||||||
const file = event.target.files[0];
|
|
||||||
if (file) {
|
|
||||||
await handleFileLoad(file);
|
|
||||||
closeOpenProjectModal();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
uploadSampleBtn.addEventListener("click", () => sampleFileInput.click());
|
uploadSampleBtn.addEventListener("click", () => sampleFileInput.click());
|
||||||
|
|
||||||
sampleFileInput.addEventListener("change", async (event) => {
|
|
||||||
const file = event.target.files[0];
|
|
||||||
if (!file) return;
|
|
||||||
|
|
||||||
const formData = new FormData();
|
|
||||||
formData.append("sampleFile", file);
|
|
||||||
|
|
||||||
try {
|
|
||||||
const response = await fetch('http://localhost:5000/upload-sample', {
|
|
||||||
method: 'POST',
|
|
||||||
body: formData,
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = await response.json();
|
|
||||||
|
|
||||||
if (response.ok) {
|
|
||||||
alert("Sample enviado com sucesso!");
|
|
||||||
await loadAndRenderSampleBrowser();
|
|
||||||
} else {
|
|
||||||
throw new Error(result.error || "Erro desconhecido no servidor.");
|
|
||||||
}
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Erro ao enviar o sample:", error);
|
|
||||||
alert(`Falha no upload: ${error.message}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
event.target.value = null;
|
|
||||||
});
|
|
||||||
|
|
||||||
saveMmpBtn.addEventListener("click", generateMmpFile);
|
saveMmpBtn.addEventListener("click", generateMmpFile);
|
||||||
addInstrumentBtn.addEventListener("click", addTrackToState);
|
addInstrumentBtn.addEventListener("click", addTrackToState);
|
||||||
removeInstrumentBtn.addEventListener("click", removeLastTrackFromState);
|
removeInstrumentBtn.addEventListener("click", removeLastTrackFromState);
|
||||||
playBtn.addEventListener("click", togglePlayback);
|
playBtn.addEventListener("click", togglePlayback);
|
||||||
stopBtn.addEventListener("click", stopPlayback);
|
stopBtn.addEventListener("click", stopPlayback);
|
||||||
rewindBtn.addEventListener("click", rewindPlayback);
|
rewindBtn.addEventListener("click", rewindPlayback);
|
||||||
metronomeBtn.addEventListener("click", () => {
|
metronomeBtn.addEventListener("click", () => { initializeAudioContext(); appState.global.metronomeEnabled = !appState.global.metronomeEnabled; metronomeBtn.classList.toggle("active", appState.global.metronomeEnabled); });
|
||||||
initializeAudioContext();
|
if(sliceToolBtn) { sliceToolBtn.addEventListener("click", () => { appState.global.sliceToolActive = !appState.global.sliceToolActive; sliceToolBtn.classList.toggle("active", appState.global.sliceToolActive); document.body.classList.toggle("slice-tool-active", appState.global.sliceToolActive); }); }
|
||||||
appState.metronomeEnabled = !appState.metronomeEnabled;
|
|
||||||
metronomeBtn.classList.toggle("active", appState.metronomeEnabled);
|
|
||||||
});
|
|
||||||
openModalCloseBtn.addEventListener("click", closeOpenProjectModal);
|
openModalCloseBtn.addEventListener("click", closeOpenProjectModal);
|
||||||
openProjectModal.addEventListener("click", (e) => {
|
|
||||||
if (e.target === openProjectModal) closeOpenProjectModal();
|
// ### CORREÇÃO 2: Adicionada verificação 'if (icon)' ###
|
||||||
});
|
sidebarToggle.addEventListener("click", () => {
|
||||||
sidebarToggle.addEventListener("click", () => {
|
document.body.classList.toggle("sidebar-hidden");
|
||||||
document.body.classList.toggle("sidebar-hidden");
|
const icon = sidebarToggle.querySelector("i");
|
||||||
const icon = sidebarToggle.querySelector("i");
|
if (icon) {
|
||||||
icon.className = document.body.classList.contains("sidebar-hidden")
|
icon.className = document.body.classList.contains("sidebar-hidden") ? "fa-solid fa-caret-right" : "fa-solid fa-caret-left";
|
||||||
? "fa-solid fa-caret-right"
|
}
|
||||||
: "fa-solid fa-caret-left";
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const inputs = document.querySelectorAll(".value-input");
|
const inputs = document.querySelectorAll(".value-input");
|
||||||
inputs.forEach((input) => {
|
inputs.forEach((input) => {
|
||||||
input.addEventListener("input", (event) => {
|
input.addEventListener("input", (event) => {
|
||||||
enforceNumericInput(event);
|
enforceNumericInput(event);
|
||||||
if (appState.isPlaying && (event.target.id.startsWith("compasso-") || event.target.id === 'bars-input')) {
|
if (appState.global.isPlaying && (event.target.id.startsWith("compasso-") || event.target.id === 'bars-input')) { stopPlayback(); }
|
||||||
stopPlayback();
|
if (event.target.id.startsWith("compasso-") || event.target.id === 'bars-input' || event.target.id === 'bpm-input') { renderAll(); }
|
||||||
}
|
|
||||||
if (event.target.id.startsWith("compasso-") || event.target.id === 'bars-input') {
|
|
||||||
redrawSequencer();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
input.addEventListener("wheel", (event) => {
|
|
||||||
event.preventDefault();
|
|
||||||
const step = event.deltaY < 0 ? 1 : -1;
|
|
||||||
adjustValue(event.target, step);
|
|
||||||
});
|
});
|
||||||
|
input.addEventListener("wheel", (event) => { event.preventDefault(); const step = event.deltaY < 0 ? 1 : -1; adjustValue(event.target, step); });
|
||||||
});
|
});
|
||||||
|
|
||||||
const buttons = document.querySelectorAll(".adjust-btn");
|
const buttons = document.querySelectorAll(".adjust-btn");
|
||||||
buttons.forEach((button) => {
|
buttons.forEach((button) => { button.addEventListener("click", () => { const targetId = button.dataset.target + "-input"; const targetInput = document.getElementById(targetId); const step = parseInt(button.dataset.step, 10) || 1; if (targetInput) { adjustValue(targetInput, step); } }); });
|
||||||
button.addEventListener("click", () => {
|
|
||||||
const targetId = button.dataset.target + "-input";
|
if (zoomInBtn) {
|
||||||
const targetInput = document.getElementById(targetId);
|
zoomInBtn.addEventListener("click", () => {
|
||||||
const step = parseInt(button.dataset.step, 10) || 1;
|
if (appState.global.zoomLevelIndex < ZOOM_LEVELS.length - 1) {
|
||||||
if (targetInput) {
|
appState.global.zoomLevelIndex++;
|
||||||
adjustValue(targetInput, step);
|
renderAll();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
}
|
||||||
|
if (zoomOutBtn) {
|
||||||
audioEditorPlayBtn.addEventListener("click", () => {
|
zoomOutBtn.addEventListener("click", () => {
|
||||||
if (appState.isAudioEditorPlaying) {
|
if (appState.global.zoomLevelIndex > 0) {
|
||||||
stopAudioEditorPlayback();
|
appState.global.zoomLevelIndex--;
|
||||||
} else {
|
renderAll();
|
||||||
startAudioEditorPlayback();
|
}
|
||||||
}
|
});
|
||||||
});
|
}
|
||||||
|
|
||||||
|
audioEditorPlayBtn.addEventListener("click", () => { if (appState.global.isAudioEditorPlaying) { stopAudioEditorPlayback(); } else { startAudioEditorPlayback(); } });
|
||||||
audioEditorStopBtn.addEventListener("click", stopAudioEditorPlayback);
|
audioEditorStopBtn.addEventListener("click", stopAudioEditorPlayback);
|
||||||
|
|
||||||
|
// ### CORREÇÃO 1: Listeners duplicados combinados em um só ###
|
||||||
|
// No main.js
|
||||||
audioEditorLoopBtn.addEventListener("click", () => {
|
audioEditorLoopBtn.addEventListener("click", () => {
|
||||||
appState.isAudioEditorLoopEnabled = !appState.isAudioEditorLoopEnabled;
|
console.log("--- Botão de Loop Clicado ---"); // DEBUG 1
|
||||||
audioEditorLoopBtn.classList.toggle("active", appState.isAudioEditorLoopEnabled);
|
|
||||||
|
// 1. Altera o estado global de loop
|
||||||
|
appState.global.isLoopActive = !appState.global.isLoopActive;
|
||||||
|
console.log("Estado appState.global.isLoopActive:", appState.global.isLoopActive); // DEBUG 2
|
||||||
|
|
||||||
|
// 2. Sincroniza o estado do loop do editor
|
||||||
|
appState.audio.isAudioEditorLoopEnabled = appState.global.isLoopActive;
|
||||||
|
|
||||||
|
// 3. Atualiza a aparência do botão
|
||||||
|
audioEditorLoopBtn.classList.toggle("active", appState.global.isLoopActive);
|
||||||
|
|
||||||
|
// 4. Sincroniza o Tone.Transport
|
||||||
|
updateTransportLoop();
|
||||||
|
|
||||||
|
// 5. Mostra/esconde a área de loop
|
||||||
|
const loopArea = document.getElementById("loop-region");
|
||||||
|
|
||||||
|
// ESTE É O TESTE MAIS IMPORTANTE:
|
||||||
|
if (loopArea) {
|
||||||
|
console.log("Elemento #loop-region ENCONTRADO. Alterando classe 'visible'."); // DEBUG 3
|
||||||
|
loopArea.classList.toggle("visible", appState.global.isLoopActive);
|
||||||
|
} else {
|
||||||
|
console.error("ERRO GRAVE: Elemento #loop-region NÃO FOI ENCONTRADO!"); // DEBUG 4
|
||||||
|
}
|
||||||
|
|
||||||
|
// 6. Reinicia o playback se estiver tocando
|
||||||
restartAudioEditorIfPlaying();
|
restartAudioEditorIfPlaying();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (addAudioTrackBtn) { addAudioTrackBtn.addEventListener("click", () => { addAudioTrackLane(); renderAudioEditor(); }); }
|
||||||
|
|
||||||
|
// ### CORREÇÃO 3: Ordem de execução corrigida ###
|
||||||
|
|
||||||
|
// 1. Carrega o conteúdo do navegador de samples
|
||||||
loadAndRenderSampleBrowser();
|
loadAndRenderSampleBrowser();
|
||||||
renderApp();
|
|
||||||
setupMasterKnobs();
|
// 2. Adiciona o listener DEPOIS que o conteúdo supostamente existe
|
||||||
|
const browserContent = document.getElementById('browser-content');
|
||||||
|
if (browserContent) {
|
||||||
|
browserContent.addEventListener('click', function(event) {
|
||||||
|
const folderName = event.target.closest('.folder-name');
|
||||||
|
if (folderName) {
|
||||||
|
const folderItem = folderName.parentElement;
|
||||||
|
folderItem.classList.toggle('open');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Renderiza o resto
|
||||||
|
renderAll();
|
||||||
});
|
});
|
||||||
|
|
@ -0,0 +1,141 @@
|
||||||
|
// js/pattern_audio.js
|
||||||
|
import { appState } from "../state.js";
|
||||||
|
import { highlightStep } from "./pattern_ui.js";
|
||||||
|
import { getTotalSteps } from "../utils.js";
|
||||||
|
import { initializeAudioContext } from "../audio.js";
|
||||||
|
|
||||||
|
const timerDisplay = document.getElementById('timer-display');
|
||||||
|
|
||||||
|
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 synth = new Tone.Synth().toDestination();
|
||||||
|
const freq = isDownbeat ? 1000 : 800;
|
||||||
|
synth.triggerAttackRelease(freq, "8n", Tone.now());
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- FUNÇÃO CORRIGIDA E EFICIENTE ---
|
||||||
|
export function playSample(filePath, trackId) {
|
||||||
|
initializeAudioContext();
|
||||||
|
const track = trackId ? appState.pattern.tracks.find((t) => t.id == trackId) : null;
|
||||||
|
|
||||||
|
// Se a faixa existe e tem um player pré-carregado, apenas o dispara.
|
||||||
|
if (track && track.player) {
|
||||||
|
// Atualiza o volume/pan caso tenham sido alterados
|
||||||
|
track.gainNode.gain.value = Tone.gainToDb(track.volume);
|
||||||
|
track.pannerNode.pan.value = track.pan;
|
||||||
|
|
||||||
|
// Dispara o som imediatamente. Esta operação é instantânea.
|
||||||
|
track.player.start(Tone.now());
|
||||||
|
}
|
||||||
|
// Fallback para preview de samples no navegador (sem trackId)
|
||||||
|
else if (!trackId && filePath) {
|
||||||
|
const previewPlayer = new Tone.Player(filePath).toDestination();
|
||||||
|
previewPlayer.autostart = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function tick() {
|
||||||
|
if (!appState.global.isPlaying) {
|
||||||
|
stopPlayback();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const totalSteps = getTotalSteps();
|
||||||
|
const lastStepIndex = appState.global.currentStep === 0 ? totalSteps - 1 : appState.global.currentStep - 1;
|
||||||
|
highlightStep(lastStepIndex, false);
|
||||||
|
|
||||||
|
const bpm = parseInt(document.getElementById("bpm-input").value, 10) || 120;
|
||||||
|
const stepInterval = (60 * 1000) / (bpm * 4);
|
||||||
|
const currentTime = appState.global.currentStep * stepInterval;
|
||||||
|
if (timerDisplay) {
|
||||||
|
timerDisplay.textContent = formatTime(currentTime);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (appState.global.metronomeEnabled) {
|
||||||
|
const noteValue = parseInt(document.getElementById("compasso-b-input").value, 10) || 4;
|
||||||
|
const stepsPerBeat = 16 / noteValue;
|
||||||
|
if (appState.global.currentStep % stepsPerBeat === 0) {
|
||||||
|
playMetronomeSound(appState.global.currentStep % (stepsPerBeat * 4) === 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
appState.pattern.tracks.forEach((track) => {
|
||||||
|
if (!track.patterns || track.patterns.length === 0) return;
|
||||||
|
|
||||||
|
const activePattern = track.patterns[appState.pattern.activePatternIndex];
|
||||||
|
|
||||||
|
if (activePattern && activePattern.steps[appState.global.currentStep] && track.samplePath) {
|
||||||
|
playSample(track.samplePath, track.id);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
highlightStep(appState.global.currentStep, true);
|
||||||
|
appState.global.currentStep = (appState.global.currentStep + 1) % totalSteps;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function startPlayback() {
|
||||||
|
if (appState.global.isPlaying || appState.pattern.tracks.length === 0) return;
|
||||||
|
initializeAudioContext();
|
||||||
|
|
||||||
|
if (appState.global.currentStep === 0) {
|
||||||
|
rewindPlayback();
|
||||||
|
}
|
||||||
|
|
||||||
|
const bpm = parseInt(document.getElementById("bpm-input").value, 10) || 120;
|
||||||
|
Tone.Transport.bpm.value = bpm;
|
||||||
|
const stepInterval = (60 * 1000) / (bpm * 4);
|
||||||
|
|
||||||
|
if (appState.global.playbackIntervalId) clearInterval(appState.global.playbackIntervalId);
|
||||||
|
|
||||||
|
appState.global.isPlaying = true;
|
||||||
|
document.getElementById("play-btn").classList.remove("fa-play");
|
||||||
|
document.getElementById("play-btn").classList.add("fa-pause");
|
||||||
|
|
||||||
|
tick();
|
||||||
|
appState.global.playbackIntervalId = setInterval(tick, stepInterval);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function stopPlayback() {
|
||||||
|
if(appState.global.playbackIntervalId) {
|
||||||
|
clearInterval(appState.global.playbackIntervalId);
|
||||||
|
}
|
||||||
|
appState.global.playbackIntervalId = null;
|
||||||
|
appState.global.isPlaying = false;
|
||||||
|
|
||||||
|
document.querySelectorAll('.step.playing').forEach(s => s.classList.remove('playing'));
|
||||||
|
appState.global.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.global.currentStep > 0 ? appState.global.currentStep - 1 : getTotalSteps() - 1;
|
||||||
|
appState.global.currentStep = 0;
|
||||||
|
if (!appState.global.isPlaying) {
|
||||||
|
if (timerDisplay) timerDisplay.textContent = '00:00:00';
|
||||||
|
highlightStep(lastStep, false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function togglePlayback() {
|
||||||
|
initializeAudioContext();
|
||||||
|
if (appState.global.isPlaying) {
|
||||||
|
stopPlayback();
|
||||||
|
} else {
|
||||||
|
appState.global.currentStep = 0;
|
||||||
|
startPlayback();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,97 @@
|
||||||
|
// js/pattern_state.js
|
||||||
|
import { DEFAULT_VOLUME, DEFAULT_PAN } from "../config.js";
|
||||||
|
import { getAudioContext, getMainGainNode } from "../audio.js";
|
||||||
|
import { renderPatternEditor } from "./pattern_ui.js";
|
||||||
|
import { getTotalSteps } from "../utils.js";
|
||||||
|
|
||||||
|
const initialState = {
|
||||||
|
tracks: [],
|
||||||
|
activeTrackId: null,
|
||||||
|
activePatternIndex: 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
export let patternState = { ...initialState };
|
||||||
|
|
||||||
|
export function initializePatternState() {
|
||||||
|
Object.assign(patternState, initialState, { tracks: [] });
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- FUNÇÃO CORRIGIDA ---
|
||||||
|
// Agora, esta função cria e pré-carrega um Tone.Player para a faixa.
|
||||||
|
export async function loadAudioForTrack(track) {
|
||||||
|
if (!track.samplePath) return track;
|
||||||
|
try {
|
||||||
|
// Se já existir um player antigo, o descartamos para liberar memória.
|
||||||
|
if (track.player) {
|
||||||
|
track.player.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cria um novo Tone.Player e o conecta à cadeia de áudio da faixa.
|
||||||
|
// O 'await' garante que o áudio seja totalmente carregado antes de prosseguirmos.
|
||||||
|
track.player = await new Tone.Player(track.samplePath).toDestination();
|
||||||
|
track.player.chain(track.gainNode, track.pannerNode, getMainGainNode());
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Falha ao carregar áudio para a trilha ${track.name}:`, error);
|
||||||
|
track.player = null;
|
||||||
|
}
|
||||||
|
return track;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function addTrackToState() {
|
||||||
|
const mainGainNode = getMainGainNode();
|
||||||
|
const totalSteps = getTotalSteps();
|
||||||
|
const referenceTrack = patternState.tracks[0];
|
||||||
|
|
||||||
|
const newTrack = {
|
||||||
|
id: Date.now(),
|
||||||
|
name: "novo instrumento",
|
||||||
|
samplePath: null,
|
||||||
|
player: null, // <-- ADICIONADO: O player começará como nulo
|
||||||
|
patterns: referenceTrack
|
||||||
|
? referenceTrack.patterns.map(p => ({ name: p.name, steps: new Array(p.steps.length).fill(false), pos: p.pos }))
|
||||||
|
: [{ name: "Pattern 1", steps: new Array(totalSteps).fill(false), pos: 0 }],
|
||||||
|
activePatternIndex: 0,
|
||||||
|
volume: DEFAULT_VOLUME,
|
||||||
|
pan: DEFAULT_PAN,
|
||||||
|
gainNode: new Tone.Gain(Tone.gainToDb(DEFAULT_VOLUME)),
|
||||||
|
pannerNode: new Tone.Panner(DEFAULT_PAN),
|
||||||
|
};
|
||||||
|
|
||||||
|
newTrack.gainNode.chain(newTrack.pannerNode, mainGainNode);
|
||||||
|
|
||||||
|
patternState.tracks.push(newTrack);
|
||||||
|
renderPatternEditor();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function removeLastTrackFromState() {
|
||||||
|
if (patternState.tracks.length > 0) {
|
||||||
|
const trackToRemove = patternState.tracks[patternState.tracks.length - 1];
|
||||||
|
if (trackToRemove.player) trackToRemove.player.dispose();
|
||||||
|
if (trackToRemove.pannerNode) trackToRemove.pannerNode.dispose();
|
||||||
|
if (trackToRemove.gainNode) trackToRemove.gainNode.dispose();
|
||||||
|
|
||||||
|
patternState.tracks.pop();
|
||||||
|
renderPatternEditor();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function updateTrackSample(trackId, samplePath) {
|
||||||
|
const track = patternState.tracks.find((t) => t.id == trackId);
|
||||||
|
if (track) {
|
||||||
|
track.samplePath = samplePath;
|
||||||
|
track.name = samplePath.split("/").pop();
|
||||||
|
await loadAudioForTrack(track); // Carrega o novo player
|
||||||
|
renderPatternEditor();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function toggleStepState(trackId, stepIndex) {
|
||||||
|
const track = patternState.tracks.find((t) => t.id == trackId);
|
||||||
|
if (track && track.patterns && track.patterns.length > 0) {
|
||||||
|
const activePattern = track.patterns[track.activePatternIndex];
|
||||||
|
if (activePattern && activePattern.steps.length > stepIndex) {
|
||||||
|
activePattern.steps[stepIndex] = !activePattern.steps[stepIndex];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,171 @@
|
||||||
|
// js/pattern_ui.js
|
||||||
|
import { appState } from "../state.js";
|
||||||
|
import {
|
||||||
|
toggleStepState,
|
||||||
|
updateTrackSample
|
||||||
|
} from "./pattern_state.js";
|
||||||
|
import { playSample, stopPlayback } from "./pattern_audio.js"; // Será criado no próximo passo
|
||||||
|
import { getTotalSteps } from "../utils.js";
|
||||||
|
|
||||||
|
// Função principal de renderização para o editor de patterns
|
||||||
|
export function renderPatternEditor() {
|
||||||
|
const trackContainer = document.getElementById("track-container");
|
||||||
|
trackContainer.innerHTML = "";
|
||||||
|
|
||||||
|
appState.pattern.tracks.forEach((trackData) => {
|
||||||
|
const trackLane = document.createElement("div");
|
||||||
|
trackLane.className = "track-lane";
|
||||||
|
trackLane.dataset.trackId = trackData.id;
|
||||||
|
|
||||||
|
if (trackData.id === appState.pattern.activeTrackId) {
|
||||||
|
trackLane.classList.add('active-track');
|
||||||
|
}
|
||||||
|
|
||||||
|
trackLane.innerHTML = `
|
||||||
|
<div class="track-info">
|
||||||
|
<i class="fa-solid fa-gear"></i>
|
||||||
|
<div class="track-mute"></div>
|
||||||
|
<span class="track-name">${trackData.name}</span>
|
||||||
|
</div>
|
||||||
|
<div class="track-controls">
|
||||||
|
<div class="knob-container">
|
||||||
|
<div class="knob" data-control="volume" data-track-id="${trackData.id}"><div class="knob-indicator"></div></div>
|
||||||
|
<span>VOL</span>
|
||||||
|
</div>
|
||||||
|
<div class="knob-container">
|
||||||
|
<div class="knob" data-control="pan" data-track-id="${trackData.id}"><div class="knob-indicator"></div></div>
|
||||||
|
<span>PAN</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="step-sequencer-wrapper"></div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
trackLane.addEventListener('click', () => {
|
||||||
|
if (appState.pattern.activeTrackId === trackData.id) return;
|
||||||
|
stopPlayback();
|
||||||
|
appState.pattern.activeTrackId = trackData.id;
|
||||||
|
document.querySelectorAll('.track-lane').forEach(lane => lane.classList.remove('active-track'));
|
||||||
|
trackLane.classList.add('active-track');
|
||||||
|
updateGlobalPatternSelector();
|
||||||
|
redrawSequencer();
|
||||||
|
});
|
||||||
|
|
||||||
|
trackLane.addEventListener("dragover", (e) => { e.preventDefault(); trackLane.classList.add("drag-over"); });
|
||||||
|
trackLane.addEventListener("dragleave", () => trackLane.classList.remove("drag-over"));
|
||||||
|
trackLane.addEventListener("drop", (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
trackLane.classList.remove("drag-over");
|
||||||
|
const filePath = e.dataTransfer.getData("text/plain");
|
||||||
|
if (filePath) {
|
||||||
|
updateTrackSample(trackData.id, filePath);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
trackContainer.appendChild(trackLane);
|
||||||
|
// A lógica dos knobs precisará ser reimplementada ou movida para um arquivo de componentes
|
||||||
|
});
|
||||||
|
|
||||||
|
updateGlobalPatternSelector();
|
||||||
|
redrawSequencer();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function redrawSequencer() {
|
||||||
|
const totalGridSteps = getTotalSteps();
|
||||||
|
document.querySelectorAll(".step-sequencer-wrapper").forEach((wrapper) => {
|
||||||
|
let sequencerContainer = wrapper.querySelector(".step-sequencer");
|
||||||
|
if (!sequencerContainer) {
|
||||||
|
sequencerContainer = document.createElement("div");
|
||||||
|
sequencerContainer.className = "step-sequencer";
|
||||||
|
wrapper.appendChild(sequencerContainer);
|
||||||
|
}
|
||||||
|
|
||||||
|
const parentTrackElement = wrapper.closest(".track-lane");
|
||||||
|
const trackId = parentTrackElement.dataset.trackId;
|
||||||
|
const trackData = appState.pattern.tracks.find((t) => t.id == trackId);
|
||||||
|
|
||||||
|
if (!trackData || !trackData.patterns || trackData.patterns.length === 0) {
|
||||||
|
sequencerContainer.innerHTML = ""; return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const activePattern = trackData.patterns[appState.pattern.activePatternIndex];
|
||||||
|
if (!activePattern) {
|
||||||
|
sequencerContainer.innerHTML = ""; return;
|
||||||
|
}
|
||||||
|
const patternSteps = activePattern.steps;
|
||||||
|
|
||||||
|
sequencerContainer.innerHTML = "";
|
||||||
|
for (let i = 0; i < totalGridSteps; i++) {
|
||||||
|
const stepWrapper = document.createElement("div");
|
||||||
|
stepWrapper.className = "step-wrapper";
|
||||||
|
const stepElement = document.createElement("div");
|
||||||
|
stepElement.className = "step";
|
||||||
|
|
||||||
|
if (patternSteps[i] === true) {
|
||||||
|
stepElement.classList.add("active");
|
||||||
|
}
|
||||||
|
|
||||||
|
stepElement.addEventListener("click", () => {
|
||||||
|
toggleStepState(trackData.id, i);
|
||||||
|
stepElement.classList.toggle("active");
|
||||||
|
if (trackData && trackData.samplePath) {
|
||||||
|
playSample(trackData.samplePath, trackData.id);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const beatsPerBar = parseInt(document.getElementById("compasso-a-input").value, 10) || 4;
|
||||||
|
const groupIndex = Math.floor(i / beatsPerBar);
|
||||||
|
if (groupIndex % 2 === 0) {
|
||||||
|
stepElement.classList.add("step-dark");
|
||||||
|
}
|
||||||
|
|
||||||
|
const stepsPerBar = 16;
|
||||||
|
if (i > 0 && i % stepsPerBar === 0) {
|
||||||
|
const marker = document.createElement("div");
|
||||||
|
marker.className = "step-marker";
|
||||||
|
marker.textContent = Math.floor(i / stepsPerBar) + 1;
|
||||||
|
stepWrapper.appendChild(marker);
|
||||||
|
}
|
||||||
|
|
||||||
|
stepWrapper.appendChild(stepElement);
|
||||||
|
sequencerContainer.appendChild(stepWrapper);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function updateGlobalPatternSelector() {
|
||||||
|
const globalPatternSelector = document.getElementById('global-pattern-selector');
|
||||||
|
if (!globalPatternSelector) return;
|
||||||
|
|
||||||
|
const referenceTrack = appState.pattern.tracks[0];
|
||||||
|
globalPatternSelector.innerHTML = '';
|
||||||
|
if (referenceTrack && referenceTrack.patterns.length > 0) {
|
||||||
|
referenceTrack.patterns.forEach((pattern, index) => {
|
||||||
|
const option = document.createElement('option');
|
||||||
|
option.value = index;
|
||||||
|
option.textContent = pattern.name;
|
||||||
|
globalPatternSelector.appendChild(option);
|
||||||
|
});
|
||||||
|
globalPatternSelector.selectedIndex = appState.pattern.activePatternIndex;
|
||||||
|
globalPatternSelector.disabled = false;
|
||||||
|
} else {
|
||||||
|
const option = document.createElement('option');
|
||||||
|
option.textContent = 'Sem patterns';
|
||||||
|
globalPatternSelector.appendChild(option);
|
||||||
|
globalPatternSelector.disabled = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function highlightStep(stepIndex, isActive) {
|
||||||
|
if (stepIndex < 0) return;
|
||||||
|
document.querySelectorAll(".track-lane").forEach((track) => {
|
||||||
|
const stepWrapper = track.querySelector(
|
||||||
|
`.step-sequencer .step-wrapper:nth-child(${stepIndex + 1})`
|
||||||
|
);
|
||||||
|
if (stepWrapper) {
|
||||||
|
const stepElement = stepWrapper.querySelector(".step");
|
||||||
|
if (stepElement) {
|
||||||
|
stepElement.classList.toggle("playing", isActive);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
@ -1,25 +1,13 @@
|
||||||
// js/state.js
|
// js/state.js
|
||||||
|
import { patternState, initializePatternState } from './pattern/pattern_state.js';
|
||||||
|
import { audioState, initializeAudioState } from './audio/audio_state.js';
|
||||||
import { DEFAULT_VOLUME, DEFAULT_PAN } from "./config.js";
|
import { DEFAULT_VOLUME, DEFAULT_PAN } from "./config.js";
|
||||||
import {
|
|
||||||
initializeAudioContext,
|
|
||||||
getAudioContext,
|
|
||||||
getMainGainNode,
|
|
||||||
} from "./audio.js";
|
|
||||||
import { renderApp, renderAudioEditor } from "./ui.js";
|
|
||||||
import { getTotalSteps } from "./utils.js";
|
|
||||||
|
|
||||||
export let appState = {
|
// Estado global da aplicação
|
||||||
tracks: [],
|
const globalState = {
|
||||||
audioTracks: [],
|
sliceToolActive: false,
|
||||||
activeTrackId: null,
|
|
||||||
activePatternIndex: 0,
|
|
||||||
isPlaying: false,
|
isPlaying: false,
|
||||||
isAudioEditorPlaying: false,
|
isAudioEditorPlaying: false,
|
||||||
activeAudioSources: [],
|
|
||||||
audioEditorStartTime: 0,
|
|
||||||
audioEditorAnimationId: null,
|
|
||||||
audioEditorPlaybackTime: 0,
|
|
||||||
isAudioEditorLoopEnabled: false, // <-- ADICIONADO: Estado para controlar o loop
|
|
||||||
playbackIntervalId: null,
|
playbackIntervalId: null,
|
||||||
currentStep: 0,
|
currentStep: 0,
|
||||||
metronomeEnabled: false,
|
metronomeEnabled: false,
|
||||||
|
|
@ -27,148 +15,40 @@ export let appState = {
|
||||||
currentBeatBasslineName: 'Novo Projeto',
|
currentBeatBasslineName: 'Novo Projeto',
|
||||||
masterVolume: DEFAULT_VOLUME,
|
masterVolume: DEFAULT_VOLUME,
|
||||||
masterPan: DEFAULT_PAN,
|
masterPan: DEFAULT_PAN,
|
||||||
|
zoomLevelIndex: 2,
|
||||||
|
|
||||||
|
// --- ADICIONADO PARA A ÁREA DE LOOP ---
|
||||||
|
isLoopActive: false, // O botão de loop principal agora controla este estado
|
||||||
|
loopStartTime: 0, // Início do loop em segundos
|
||||||
|
loopEndTime: 8, // Fim do loop em segundos (padrão de 4 compassos a 120BPM)
|
||||||
};
|
};
|
||||||
|
|
||||||
export async function loadAudioForTrack(track) {
|
// Combina todos os estados em um único objeto namespaced
|
||||||
if (!track.samplePath) return track;
|
export let appState = {
|
||||||
try {
|
global: globalState,
|
||||||
const audioContext = getAudioContext();
|
pattern: patternState,
|
||||||
if (!audioContext) initializeAudioContext();
|
audio: audioState,
|
||||||
const response = await fetch(track.samplePath);
|
};
|
||||||
if (!response.ok) throw new Error(`Erro ao buscar o sample: ${response.statusText}`);
|
|
||||||
const arrayBuffer = await response.arrayBuffer();
|
|
||||||
track.audioBuffer = await audioContext.decodeAudioData(arrayBuffer);
|
|
||||||
} catch (error) {
|
|
||||||
console.error(`Falha ao carregar áudio para a trilha ${track.name}:`, error);
|
|
||||||
track.audioBuffer = null;
|
|
||||||
}
|
|
||||||
return track;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function addAudioTrack(samplePath) {
|
// Função para resetar o projeto para o estado inicial
|
||||||
initializeAudioContext();
|
export function resetProjectState() {
|
||||||
const audioContext = getAudioContext();
|
initializePatternState();
|
||||||
const mainGainNode = getMainGainNode();
|
initializeAudioState();
|
||||||
|
|
||||||
const newAudioTrack = {
|
Object.assign(globalState, {
|
||||||
id: Date.now() + Math.random(),
|
sliceToolActive: false,
|
||||||
name: samplePath.split('/').pop(),
|
isPlaying: false,
|
||||||
samplePath: samplePath,
|
isAudioEditorPlaying: false,
|
||||||
audioBuffer: null,
|
playbackIntervalId: null,
|
||||||
volume: DEFAULT_VOLUME,
|
currentStep: 0,
|
||||||
pan: DEFAULT_PAN,
|
metronomeEnabled: false,
|
||||||
isMuted: false,
|
originalXmlDoc: null,
|
||||||
isSoloed: false, // <-- ADICIONADO: Começa como não-solada
|
currentBeatBasslineName: 'Novo Projeto',
|
||||||
gainNode: audioContext.createGain(),
|
masterVolume: DEFAULT_VOLUME,
|
||||||
pannerNode: audioContext.createStereoPanner(),
|
masterPan: DEFAULT_PAN,
|
||||||
};
|
zoomLevelIndex: 2,
|
||||||
|
isLoopActive: false,
|
||||||
newAudioTrack.gainNode.connect(newAudioTrack.pannerNode);
|
loopStartTime: 0,
|
||||||
newAudioTrack.pannerNode.connect(mainGainNode);
|
loopEndTime: 8,
|
||||||
newAudioTrack.gainNode.gain.value = newAudioTrack.volume;
|
|
||||||
newAudioTrack.pannerNode.pan.value = newAudioTrack.pan;
|
|
||||||
|
|
||||||
appState.audioTracks.push(newAudioTrack);
|
|
||||||
|
|
||||||
loadAudioForTrack(newAudioTrack).then(() => {
|
|
||||||
renderAudioEditor();
|
|
||||||
});
|
});
|
||||||
}
|
|
||||||
|
|
||||||
// A função de mute agora será a de solo.
|
|
||||||
export function toggleAudioTrackSolo(trackId) {
|
|
||||||
const track = appState.audioTracks.find(t => t.id == trackId);
|
|
||||||
if (track) {
|
|
||||||
track.isSoloed = !track.isSoloed;
|
|
||||||
renderAudioEditor(); // Re-renderiza para mostrar a nova cor
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Mantemos a função de mute caso precise no futuro, mas ela não está conectada ao botão.
|
|
||||||
export function toggleAudioTrackMute(trackId) {
|
|
||||||
const track = appState.audioTracks.find(t => t.id == trackId);
|
|
||||||
if (track) {
|
|
||||||
track.isMuted = !track.isMuted;
|
|
||||||
renderAudioEditor();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function addTrackToState() {
|
|
||||||
initializeAudioContext();
|
|
||||||
const audioContext = getAudioContext();
|
|
||||||
const mainGainNode = getMainGainNode();
|
|
||||||
const totalSteps = getTotalSteps();
|
|
||||||
const referenceTrack = appState.tracks[0];
|
|
||||||
|
|
||||||
const newTrack = {
|
|
||||||
id: Date.now(),
|
|
||||||
name: "novo instrumento",
|
|
||||||
samplePath: null,
|
|
||||||
audioBuffer: null,
|
|
||||||
patterns: referenceTrack
|
|
||||||
? referenceTrack.patterns.map(p => ({ name: p.name, steps: new Array(p.steps.length).fill(false), pos: p.pos }))
|
|
||||||
: [{ name: "Pattern 1", steps: new Array(totalSteps).fill(false), pos: 0 }],
|
|
||||||
activePatternIndex: 0,
|
|
||||||
volume: DEFAULT_VOLUME,
|
|
||||||
pan: DEFAULT_PAN,
|
|
||||||
gainNode: audioContext.createGain(),
|
|
||||||
pannerNode: audioContext.createStereoPanner(),
|
|
||||||
};
|
|
||||||
newTrack.gainNode.connect(newTrack.pannerNode);
|
|
||||||
newTrack.pannerNode.connect(mainGainNode);
|
|
||||||
newTrack.gainNode.gain.value = newTrack.volume;
|
|
||||||
newTrack.pannerNode.pan.value = newTrack.pan;
|
|
||||||
|
|
||||||
appState.tracks.push(newTrack);
|
|
||||||
renderApp();
|
|
||||||
}
|
|
||||||
|
|
||||||
export function removeLastTrackFromState() {
|
|
||||||
if (appState.tracks.length > 0) {
|
|
||||||
appState.tracks.pop();
|
|
||||||
renderApp();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function updateTrackSample(trackId, samplePath) {
|
|
||||||
const track = appState.tracks.find((t) => t.id == trackId);
|
|
||||||
if (track) {
|
|
||||||
track.samplePath = samplePath;
|
|
||||||
track.name = samplePath.split("/").pop();
|
|
||||||
track.audioBuffer = null;
|
|
||||||
await loadAudioForTrack(track);
|
|
||||||
renderApp();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function toggleStepState(trackId, stepIndex) {
|
|
||||||
const track = appState.tracks.find((t) => t.id == trackId);
|
|
||||||
if (track && track.patterns && track.patterns.length > 0) {
|
|
||||||
const activePattern = track.patterns[track.activePatternIndex];
|
|
||||||
if (activePattern && activePattern.steps.length > stepIndex) {
|
|
||||||
activePattern.steps[stepIndex] = !activePattern.steps[stepIndex];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function updateTrackVolume(trackId, volume) {
|
|
||||||
const track = appState.tracks.find((t) => t.id == trackId) || appState.audioTracks.find((t) => t.id == trackId);
|
|
||||||
if (track) {
|
|
||||||
const clampedVolume = Math.max(0, Math.min(1.5, volume));
|
|
||||||
track.volume = clampedVolume;
|
|
||||||
if (track.gainNode) {
|
|
||||||
track.gainNode.gain.setValueAtTime(clampedVolume, getAudioContext().currentTime);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function updateTrackPan(trackId, pan) {
|
|
||||||
const track = appState.tracks.find((t) => t.id == trackId) || appState.audioTracks.find((t) => t.id == trackId);
|
|
||||||
if (track) {
|
|
||||||
const clampedPan = Math.max(-1, Math.min(1, pan));
|
|
||||||
track.pan = clampedPan;
|
|
||||||
if (track.pannerNode) {
|
|
||||||
track.pannerNode.pan.setValueAtTime(clampedPan, getAudioContext().currentTime);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
@ -1,110 +1,22 @@
|
||||||
// js/ui.js
|
// js/ui.js
|
||||||
import {
|
import { playSample } from "./pattern/pattern_audio.js";
|
||||||
appState,
|
import { renderPatternEditor } from "./pattern/pattern_ui.js";
|
||||||
toggleStepState,
|
import { renderAudioEditor } from "./audio/audio_ui.js";
|
||||||
updateTrackSample,
|
|
||||||
updateTrackVolume,
|
|
||||||
updateTrackPan,
|
|
||||||
addAudioTrack,
|
|
||||||
toggleAudioTrackSolo,
|
|
||||||
} from "./state.js";
|
|
||||||
import { playSample, stopPlayback, seekAudioEditor } from "./audio.js";
|
|
||||||
import { getTotalSteps } from "./utils.js";
|
|
||||||
import { loadProjectFromServer } from "./file.js";
|
import { loadProjectFromServer } from "./file.js";
|
||||||
import { drawWaveform } from "./waveform.js";
|
|
||||||
import { PIXELS_PER_STEP, PIXELS_PER_BAR } from "./config.js";
|
|
||||||
|
|
||||||
export function updateAudioEditorUI() {
|
|
||||||
const playBtn = document.getElementById('audio-editor-play-btn');
|
|
||||||
if (playBtn) {
|
|
||||||
if (appState.isAudioEditorPlaying) {
|
|
||||||
playBtn.classList.remove('fa-play');
|
|
||||||
playBtn.classList.add('fa-pause');
|
|
||||||
} else {
|
|
||||||
playBtn.classList.remove('fa-pause');
|
|
||||||
playBtn.classList.add('fa-play');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let samplePathMap = {};
|
let samplePathMap = {};
|
||||||
const globalPatternSelector = document.getElementById('global-pattern-selector');
|
|
||||||
|
|
||||||
if (globalPatternSelector) {
|
export function renderAll() {
|
||||||
globalPatternSelector.addEventListener('change', () => {
|
renderPatternEditor();
|
||||||
stopPlayback();
|
renderAudioEditor();
|
||||||
appState.activePatternIndex = parseInt(globalPatternSelector.value, 10);
|
const loopArea = document.getElementById("loop-region");
|
||||||
|
|
||||||
const firstTrack = appState.tracks[0];
|
|
||||||
if (firstTrack) {
|
|
||||||
const activePattern = firstTrack.patterns[appState.activePatternIndex];
|
|
||||||
if (activePattern) {
|
|
||||||
const stepsPerBar = 16;
|
|
||||||
const requiredBars = Math.ceil(activePattern.steps.length / stepsPerBar);
|
|
||||||
document.getElementById("bars-input").value = requiredBars > 0 ? requiredBars : 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
redrawSequencer();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export function updateGlobalPatternSelector() {
|
if (loopArea) {
|
||||||
if (!globalPatternSelector) return;
|
// Sincroniza a visibilidade da área de loop com o estado atual
|
||||||
const referenceTrack = appState.tracks[0];
|
loopArea.classList.toggle("visible", appState.global.isLoopActive);
|
||||||
globalPatternSelector.innerHTML = '';
|
|
||||||
if (referenceTrack && referenceTrack.patterns.length > 0) {
|
|
||||||
referenceTrack.patterns.forEach((pattern, index) => {
|
|
||||||
const option = document.createElement('option');
|
|
||||||
option.value = index;
|
|
||||||
option.textContent = pattern.name;
|
|
||||||
globalPatternSelector.appendChild(option);
|
|
||||||
});
|
|
||||||
globalPatternSelector.selectedIndex = appState.activePatternIndex;
|
|
||||||
globalPatternSelector.disabled = false;
|
|
||||||
} else {
|
|
||||||
const option = document.createElement('option');
|
|
||||||
option.textContent = 'Sem patterns';
|
|
||||||
globalPatternSelector.appendChild(option);
|
|
||||||
globalPatternSelector.disabled = true;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function handleSampleUpload(file) {
|
|
||||||
const validExtensions = ['.wav', '.flac', '.ogg', '.mp3'];
|
|
||||||
const fileExtension = '.' + file.name.split('.').pop().toLowerCase();
|
|
||||||
|
|
||||||
if (!validExtensions.includes(fileExtension)) {
|
|
||||||
alert("Formato de arquivo inválido. Por favor, envie .wav, .flac, .ogg, ou .mp3.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const reader = new FileReader();
|
|
||||||
reader.onload = (e) => {
|
|
||||||
const dataURL = e.target.result;
|
|
||||||
const browserContent = document.getElementById("browser-content");
|
|
||||||
const list = browserContent.querySelector("ul");
|
|
||||||
|
|
||||||
if (list) {
|
|
||||||
const li = document.createElement("li");
|
|
||||||
li.innerHTML = `<i class="fa-solid fa-file-audio"></i> ${file.name}`;
|
|
||||||
li.setAttribute("draggable", true);
|
|
||||||
|
|
||||||
li.addEventListener("click", (event) => {
|
|
||||||
event.stopPropagation();
|
|
||||||
playSample(dataURL, null);
|
|
||||||
});
|
|
||||||
|
|
||||||
li.addEventListener("dragstart", (event) => {
|
|
||||||
event.dataTransfer.setData("text/plain", dataURL);
|
|
||||||
event.dataTransfer.effectAllowed = "copy";
|
|
||||||
});
|
|
||||||
|
|
||||||
list.prepend(li);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
reader.readAsDataURL(file);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getSamplePathMap() {
|
export function getSamplePathMap() {
|
||||||
return samplePathMap;
|
return samplePathMap;
|
||||||
}
|
}
|
||||||
|
|
@ -122,383 +34,11 @@ function buildSamplePathMap(tree, currentPath) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function renderAudioEditor() {
|
|
||||||
const audioEditor = document.querySelector('.audio-editor');
|
|
||||||
const audioTrackContainer = document.getElementById('audio-track-container');
|
|
||||||
if (!audioEditor || !audioTrackContainer) return;
|
|
||||||
|
|
||||||
audioEditor.ondragover = (e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
audioEditor.classList.add("drag-over");
|
|
||||||
};
|
|
||||||
audioEditor.ondragleave = () => {
|
|
||||||
audioEditor.classList.remove("drag-over");
|
|
||||||
};
|
|
||||||
audioEditor.ondrop = (e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
audioEditor.classList.remove("drag-over");
|
|
||||||
const filePath = e.dataTransfer.getData("text/plain");
|
|
||||||
if (filePath) {
|
|
||||||
addAudioTrack(filePath);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
audioTrackContainer.innerHTML = '';
|
|
||||||
|
|
||||||
appState.audioTracks.forEach(trackData => {
|
|
||||||
const audioTrackLane = document.createElement('div');
|
|
||||||
audioTrackLane.className = 'audio-track-lane';
|
|
||||||
audioTrackLane.dataset.trackId = trackData.id;
|
|
||||||
|
|
||||||
audioTrackLane.innerHTML = `
|
|
||||||
<div class="track-info">
|
|
||||||
<i class="fa-solid fa-gear"></i>
|
|
||||||
<div class="track-solo-btn"></div>
|
|
||||||
<span class="track-name">${trackData.name}</span>
|
|
||||||
</div>
|
|
||||||
<div class="track-controls">
|
|
||||||
<div class="knob-container">
|
|
||||||
<div class="knob" data-control="volume" data-track-id="${trackData.id}"><div class="knob-indicator"></div></div>
|
|
||||||
<span>VOL</span>
|
|
||||||
</div>
|
|
||||||
<div class="knob-container">
|
|
||||||
<div class="knob" data-control="pan" data-track-id="${trackData.id}"><div class="knob-indicator"></div></div>
|
|
||||||
<span>PAN</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="spectrogram-view-wrapper">
|
|
||||||
<div class="spectrogram-view-grid">
|
|
||||||
<div class="playhead"></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
|
|
||||||
const grid = audioTrackLane.querySelector('.spectrogram-view-grid');
|
|
||||||
const canvas = document.createElement('canvas');
|
|
||||||
canvas.className = 'waveform-canvas';
|
|
||||||
canvas.height = 60;
|
|
||||||
grid.prepend(canvas);
|
|
||||||
|
|
||||||
audioTrackContainer.appendChild(audioTrackLane);
|
|
||||||
|
|
||||||
if (trackData.audioBuffer) {
|
|
||||||
const bpm = parseInt(document.getElementById("bpm-input").value, 10) || 120;
|
|
||||||
const sampleDuration = trackData.audioBuffer.duration;
|
|
||||||
|
|
||||||
const stepsPerSecond = (bpm / 60) * 4;
|
|
||||||
const totalSteps = sampleDuration * stepsPerSecond;
|
|
||||||
|
|
||||||
const canvasWidth = totalSteps * PIXELS_PER_STEP;
|
|
||||||
canvas.width = canvasWidth;
|
|
||||||
|
|
||||||
drawWaveform(canvas, trackData.audioBuffer, 'var(--accent-green)');
|
|
||||||
|
|
||||||
const numberOfBars = Math.ceil(canvasWidth / PIXELS_PER_BAR);
|
|
||||||
for (let i = 0; i < numberOfBars; i++) {
|
|
||||||
if (i === 0) continue;
|
|
||||||
const marker = document.createElement('div');
|
|
||||||
marker.className = 'bar-marker';
|
|
||||||
marker.textContent = i + 1;
|
|
||||||
marker.style.left = `${i * PIXELS_PER_BAR}px`;
|
|
||||||
grid.appendChild(marker);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const soloButton = audioTrackLane.querySelector('.track-solo-btn');
|
|
||||||
if (soloButton) {
|
|
||||||
if (trackData.isSoloed) {
|
|
||||||
soloButton.classList.add('active');
|
|
||||||
}
|
|
||||||
soloButton.addEventListener('click', (e) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
toggleAudioTrackSolo(trackData.id);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const volumeKnob = audioTrackLane.querySelector('.knob[data-control="volume"]');
|
|
||||||
addKnobInteraction(volumeKnob);
|
|
||||||
updateKnobVisual(volumeKnob, "volume");
|
|
||||||
|
|
||||||
const panKnob = audioTrackLane.querySelector('.knob[data-control="pan"]');
|
|
||||||
addKnobInteraction(panKnob);
|
|
||||||
updateKnobVisual(panKnob, "pan");
|
|
||||||
|
|
||||||
const waveformWrapper = audioTrackLane.querySelector('.spectrogram-view-wrapper');
|
|
||||||
const handleSeek = (event) => {
|
|
||||||
const bpm = parseInt(document.getElementById("bpm-input").value, 10) || 120;
|
|
||||||
const stepsPerSecond = (bpm / 60) * 4;
|
|
||||||
const pixelsPerSecond = stepsPerSecond * PIXELS_PER_STEP;
|
|
||||||
|
|
||||||
const rect = waveformWrapper.getBoundingClientRect();
|
|
||||||
const clickX = event.clientX - rect.left;
|
|
||||||
const scrollLeft = waveformWrapper.scrollLeft;
|
|
||||||
const absoluteX = clickX + scrollLeft;
|
|
||||||
|
|
||||||
const newTime = absoluteX / pixelsPerSecond;
|
|
||||||
|
|
||||||
seekAudioEditor(newTime);
|
|
||||||
};
|
|
||||||
|
|
||||||
waveformWrapper.addEventListener('mousedown', (e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
handleSeek(e);
|
|
||||||
const onMouseMove = (moveEvent) => handleSeek(moveEvent);
|
|
||||||
const onMouseUp = () => {
|
|
||||||
document.removeEventListener('mousemove', onMouseMove);
|
|
||||||
document.removeEventListener('mouseup', onMouseUp);
|
|
||||||
};
|
|
||||||
document.addEventListener('mousemove', onMouseMove);
|
|
||||||
document.addEventListener('mouseup', onMouseUp);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export function renderApp() {
|
|
||||||
const trackContainer = document.getElementById("track-container");
|
|
||||||
trackContainer.innerHTML = "";
|
|
||||||
|
|
||||||
appState.tracks.forEach((trackData) => {
|
|
||||||
const trackLane = document.createElement("div");
|
|
||||||
trackLane.className = "track-lane";
|
|
||||||
trackLane.dataset.trackId = trackData.id;
|
|
||||||
|
|
||||||
if (trackData.id === appState.activeTrackId) {
|
|
||||||
trackLane.classList.add('active-track');
|
|
||||||
}
|
|
||||||
|
|
||||||
trackLane.innerHTML = `
|
|
||||||
<div class="track-info">
|
|
||||||
<i class="fa-solid fa-gear"></i>
|
|
||||||
<div class="track-mute"></div>
|
|
||||||
<span class="track-name">${trackData.name}</span>
|
|
||||||
</div>
|
|
||||||
<div class="track-controls">
|
|
||||||
<div class="knob-container">
|
|
||||||
<div class="knob" data-control="volume" data-track-id="${trackData.id}"><div class="knob-indicator"></div></div>
|
|
||||||
<span>VOL</span>
|
|
||||||
</div>
|
|
||||||
<div class="knob-container">
|
|
||||||
<div class="knob" data-control="pan" data-track-id="${trackData.id}"><div class="knob-indicator"></div></div>
|
|
||||||
<span>PAN</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="step-sequencer-wrapper"></div>
|
|
||||||
`;
|
|
||||||
|
|
||||||
trackLane.addEventListener('click', () => {
|
|
||||||
if (appState.activeTrackId === trackData.id) return;
|
|
||||||
stopPlayback();
|
|
||||||
appState.activeTrackId = trackData.id;
|
|
||||||
document.querySelectorAll('.track-lane').forEach(lane => lane.classList.remove('active-track'));
|
|
||||||
trackLane.classList.add('active-track');
|
|
||||||
updateGlobalPatternSelector();
|
|
||||||
redrawSequencer();
|
|
||||||
});
|
|
||||||
|
|
||||||
trackLane.addEventListener("dragover", (e) => { e.preventDefault(); trackLane.classList.add("drag-over"); });
|
|
||||||
trackLane.addEventListener("dragleave", () => trackLane.classList.remove("drag-over"));
|
|
||||||
trackLane.addEventListener("drop", (e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
trackLane.classList.remove("drag-over");
|
|
||||||
const filePath = e.dataTransfer.getData("text/plain");
|
|
||||||
if (filePath) {
|
|
||||||
updateTrackSample(trackData.id, filePath);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
trackContainer.appendChild(trackLane);
|
|
||||||
const volumeKnob = trackLane.querySelector(".knob[data-control='volume']");
|
|
||||||
addKnobInteraction(volumeKnob);
|
|
||||||
updateKnobVisual(volumeKnob, "volume");
|
|
||||||
const panKnob = trackLane.querySelector(".knob[data-control='pan']");
|
|
||||||
addKnobInteraction(panKnob);
|
|
||||||
updateKnobVisual(panKnob, "pan");
|
|
||||||
});
|
|
||||||
|
|
||||||
updateGlobalPatternSelector();
|
|
||||||
redrawSequencer();
|
|
||||||
renderAudioEditor();
|
|
||||||
}
|
|
||||||
|
|
||||||
export function redrawSequencer() {
|
|
||||||
const totalGridSteps = getTotalSteps();
|
|
||||||
document.querySelectorAll(".step-sequencer-wrapper").forEach((wrapper) => {
|
|
||||||
let sequencerContainer = wrapper.querySelector(".step-sequencer");
|
|
||||||
if (!sequencerContainer) {
|
|
||||||
sequencerContainer = document.createElement("div");
|
|
||||||
sequencerContainer.className = "step-sequencer";
|
|
||||||
wrapper.appendChild(sequencerContainer);
|
|
||||||
}
|
|
||||||
|
|
||||||
const parentTrackElement = wrapper.closest(".track-lane");
|
|
||||||
const trackId = parentTrackElement.dataset.trackId;
|
|
||||||
const trackData = appState.tracks.find((t) => t.id == trackId);
|
|
||||||
|
|
||||||
if (!trackData || !trackData.patterns || trackData.patterns.length === 0) {
|
|
||||||
sequencerContainer.innerHTML = ""; return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const activePattern = trackData.patterns[appState.activePatternIndex];
|
|
||||||
if (!activePattern) {
|
|
||||||
sequencerContainer.innerHTML = ""; return;
|
|
||||||
}
|
|
||||||
const patternSteps = activePattern.steps;
|
|
||||||
|
|
||||||
sequencerContainer.innerHTML = "";
|
|
||||||
for (let i = 0; i < totalGridSteps; i++) {
|
|
||||||
const stepWrapper = document.createElement("div");
|
|
||||||
stepWrapper.className = "step-wrapper";
|
|
||||||
const stepElement = document.createElement("div");
|
|
||||||
stepElement.className = "step";
|
|
||||||
|
|
||||||
if (patternSteps[i] === true) {
|
|
||||||
stepElement.classList.add("active");
|
|
||||||
}
|
|
||||||
|
|
||||||
stepElement.addEventListener("click", () => {
|
|
||||||
toggleStepState(trackData.id, i);
|
|
||||||
stepElement.classList.toggle("active");
|
|
||||||
if (trackData && trackData.samplePath) {
|
|
||||||
playSample(trackData.samplePath, trackData.id);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const beatsPerBar = parseInt(document.getElementById("compasso-a-input").value, 10) || 4;
|
|
||||||
const groupIndex = Math.floor(i / beatsPerBar);
|
|
||||||
if (groupIndex % 2 === 0) {
|
|
||||||
stepElement.classList.add("step-dark");
|
|
||||||
}
|
|
||||||
|
|
||||||
const stepsPerBar = 16;
|
|
||||||
if (i > 0 && i % stepsPerBar === 0) {
|
|
||||||
const marker = document.createElement("div");
|
|
||||||
marker.className = "step-marker";
|
|
||||||
marker.textContent = Math.floor(i / stepsPerBar) + 1;
|
|
||||||
stepWrapper.appendChild(marker);
|
|
||||||
}
|
|
||||||
|
|
||||||
stepWrapper.appendChild(stepElement);
|
|
||||||
sequencerContainer.appendChild(stepWrapper);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function addKnobInteraction(knobElement) {
|
|
||||||
const controlType = knobElement.dataset.control;
|
|
||||||
knobElement.addEventListener("mousedown", (e) => {
|
|
||||||
if (e.button === 1) {
|
|
||||||
e.preventDefault();
|
|
||||||
const trackId = knobElement.dataset.trackId;
|
|
||||||
const defaultValue = controlType === "volume" ? 0.8 : 0.0;
|
|
||||||
if (controlType === "volume") {
|
|
||||||
updateTrackVolume(trackId, defaultValue);
|
|
||||||
} else {
|
|
||||||
updateTrackPan(trackId, defaultValue);
|
|
||||||
}
|
|
||||||
updateKnobVisual(knobElement, controlType);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
knobElement.addEventListener("mousedown", (e) => {
|
|
||||||
if (e.button !== 0) return;
|
|
||||||
e.preventDefault();
|
|
||||||
const trackId = knobElement.dataset.trackId;
|
|
||||||
const track = appState.tracks.find((t) => t.id == trackId) || appState.audioTracks.find((t) => t.id == trackId);
|
|
||||||
if (!track) return;
|
|
||||||
const startY = e.clientY;
|
|
||||||
const startValue = controlType === "volume" ? track.volume : track.pan;
|
|
||||||
document.body.classList.add("knob-dragging");
|
|
||||||
function onMouseMove(moveEvent) {
|
|
||||||
const deltaY = startY - moveEvent.clientY;
|
|
||||||
const sensitivity = controlType === "volume" ? 150 : 200;
|
|
||||||
const newValue = startValue + deltaY / sensitivity;
|
|
||||||
if (controlType === "volume") {
|
|
||||||
updateTrackVolume(trackId, newValue);
|
|
||||||
} else {
|
|
||||||
updateTrackPan(trackId, newValue);
|
|
||||||
}
|
|
||||||
updateKnobVisual(knobElement, controlType);
|
|
||||||
}
|
|
||||||
function onMouseUp() {
|
|
||||||
document.body.classList.remove("knob-dragging");
|
|
||||||
document.removeEventListener("mousemove", onMouseMove);
|
|
||||||
document.removeEventListener("mouseup", onMouseUp);
|
|
||||||
}
|
|
||||||
document.addEventListener("mousemove", onMouseMove);
|
|
||||||
document.addEventListener("mouseup", onMouseUp);
|
|
||||||
});
|
|
||||||
knobElement.addEventListener("wheel", (e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
const trackId = knobElement.dataset.trackId;
|
|
||||||
const track = appState.tracks.find((t) => t.id == trackId) || appState.audioTracks.find((t) => t.id == trackId);
|
|
||||||
if (!track) return;
|
|
||||||
const step = 0.05;
|
|
||||||
const direction = e.deltaY < 0 ? 1 : -1;
|
|
||||||
if (controlType === "volume") {
|
|
||||||
const newValue = track.volume + direction * step;
|
|
||||||
updateTrackVolume(trackId, newValue);
|
|
||||||
} else {
|
|
||||||
const newValue = track.pan + direction * step;
|
|
||||||
updateTrackPan(trackId, newValue);
|
|
||||||
}
|
|
||||||
updateKnobVisual(knobElement, controlType);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function updateKnobVisual(knobElement, controlType) {
|
|
||||||
const trackId = knobElement.dataset.trackId;
|
|
||||||
const track = appState.tracks.find((t) => t.id == trackId) || appState.audioTracks.find((t) => t.id == trackId);
|
|
||||||
if (!track) return;
|
|
||||||
const indicator = knobElement.querySelector(".knob-indicator");
|
|
||||||
if (!indicator) return;
|
|
||||||
const minAngle = -135;
|
|
||||||
const maxAngle = 135;
|
|
||||||
let percentage = 0.5;
|
|
||||||
let title = "";
|
|
||||||
if (controlType === "volume") {
|
|
||||||
const value = track.volume;
|
|
||||||
const clampedValue = Math.max(0, Math.min(1.5, value));
|
|
||||||
percentage = clampedValue / 1.5;
|
|
||||||
title = `Volume: ${Math.round(clampedValue * 100)}%`;
|
|
||||||
} else {
|
|
||||||
const value = track.pan;
|
|
||||||
const clampedValue = Math.max(-1, Math.min(1, value));
|
|
||||||
percentage = (clampedValue + 1) / 2;
|
|
||||||
const panDisplay = Math.round(clampedValue * 100);
|
|
||||||
title = `Pan: ${
|
|
||||||
panDisplay === 0
|
|
||||||
? "Centro"
|
|
||||||
: panDisplay < 0
|
|
||||||
? `${-panDisplay} L`
|
|
||||||
: `${panDisplay} R`
|
|
||||||
}`;
|
|
||||||
}
|
|
||||||
const angle = minAngle + percentage * (maxAngle - minAngle);
|
|
||||||
indicator.style.transform = `translateX(-50%) rotate(${angle}deg)`;
|
|
||||||
knobElement.title = title;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function highlightStep(stepIndex, isActive) {
|
|
||||||
if (stepIndex < 0) return;
|
|
||||||
document.querySelectorAll(".track-lane").forEach((track) => {
|
|
||||||
const stepWrapper = track.querySelector(
|
|
||||||
`.step-sequencer .step-wrapper:nth-child(${stepIndex + 1})`
|
|
||||||
);
|
|
||||||
if (stepWrapper) {
|
|
||||||
const stepElement = stepWrapper.querySelector(".step");
|
|
||||||
if (stepElement) {
|
|
||||||
stepElement.classList.toggle("playing", isActive);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function loadAndRenderSampleBrowser() {
|
export async function loadAndRenderSampleBrowser() {
|
||||||
const browserContent = document.getElementById("browser-content");
|
const browserContent = document.getElementById("browser-content");
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`metadata/samples-manifest.json?v=${Date.now()}`);
|
const response = await fetch(`metadata/samples-manifest.json?v=${Date.now()}`);
|
||||||
if (!response.ok) {
|
if (!response.ok) throw new Error("Arquivo samples-manifest.json não encontrado.");
|
||||||
throw new Error("Arquivo samples-manifest.json não encontrado.");
|
|
||||||
}
|
|
||||||
const fileTree = await response.json();
|
const fileTree = await response.json();
|
||||||
|
|
||||||
samplePathMap = {};
|
samplePathMap = {};
|
||||||
|
|
@ -511,42 +51,65 @@ export async function loadAndRenderSampleBrowser() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Em ui.js, substitua a função antiga por esta:
|
||||||
|
|
||||||
function renderFileTree(tree, parentElement, currentPath) {
|
function renderFileTree(tree, parentElement, currentPath) {
|
||||||
parentElement.innerHTML = "";
|
parentElement.innerHTML = ""; // Limpa o conteúdo anterior
|
||||||
const ul = document.createElement("ul");
|
const ul = document.createElement("ul");
|
||||||
|
|
||||||
|
// Ordena para que as pastas sempre apareçam antes dos arquivos
|
||||||
const sortedKeys = Object.keys(tree).sort((a, b) => {
|
const sortedKeys = Object.keys(tree).sort((a, b) => {
|
||||||
const aIsFile = tree[a]._isFile;
|
const aIsFile = tree[a]._isFile;
|
||||||
const bIsFile = tree[b]._isFile;
|
const bIsFile = tree[b]._isFile;
|
||||||
if (aIsFile === bIsFile) return a.localeCompare(b);
|
if (aIsFile === bIsFile) return a.localeCompare(b); // Ordena alfabeticamente se ambos forem do mesmo tipo
|
||||||
return aIsFile ? 1 : -1;
|
return aIsFile ? 1 : -1; // Pastas (-1) vêm antes de arquivos (1)
|
||||||
});
|
});
|
||||||
|
|
||||||
for (const key of sortedKeys) {
|
for (const key of sortedKeys) {
|
||||||
if (key === '_isFile') continue;
|
if (key === '_isFile') continue; // Pula a propriedade de metadados
|
||||||
|
|
||||||
const node = tree[key];
|
const node = tree[key];
|
||||||
const li = document.createElement("li");
|
const li = document.createElement("li");
|
||||||
const newPath = `${currentPath}/${key}`;
|
const newPath = `${currentPath}/${key}`;
|
||||||
|
|
||||||
if (node._isFile) {
|
if (node._isFile) {
|
||||||
li.innerHTML = `<i class="fa-solid fa-file-audio"></i> ${key}`;
|
// --- LÓGICA PARA ARQUIVOS ---
|
||||||
|
li.className = "file-item draggable-sample"; // CORREÇÃO: Adiciona classe para consistência
|
||||||
|
li.innerHTML = `<i class="fa-solid fa-volume-high"></i> ${key}`; // Ícone mais apropriado
|
||||||
li.setAttribute("draggable", true);
|
li.setAttribute("draggable", true);
|
||||||
|
li.dataset.path = newPath; // Guarda o caminho para o drag-and-drop
|
||||||
|
|
||||||
li.addEventListener("click", (e) => {
|
li.addEventListener("click", (e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
playSample(newPath, null);
|
playSample(newPath, null);
|
||||||
});
|
});
|
||||||
|
|
||||||
li.addEventListener("dragstart", (e) => {
|
li.addEventListener("dragstart", (e) => {
|
||||||
e.dataTransfer.setData("text/plain", newPath);
|
e.dataTransfer.setData("text/plain", newPath);
|
||||||
e.dataTransfer.effectAllowed = "copy";
|
e.dataTransfer.effectAllowed = "copy";
|
||||||
});
|
});
|
||||||
|
|
||||||
ul.appendChild(li);
|
ul.appendChild(li);
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
li.className = "directory";
|
// --- LÓGICA CORRIGIDA PARA PASTAS ---
|
||||||
li.innerHTML = `<i class="fa-solid fa-folder"></i> ${key}`;
|
li.className = "folder-item"; // CORREÇÃO 1: Usa a classe CSS correta
|
||||||
|
|
||||||
|
// CORREÇÃO 2: Cria o <span> clicável para o nome da pasta, que o CSS e o main.js esperam
|
||||||
|
const folderNameSpan = document.createElement("span");
|
||||||
|
folderNameSpan.className = "folder-name";
|
||||||
|
folderNameSpan.innerHTML = `<i class="folder-icon fa-solid fa-folder"></i> ${key}`;
|
||||||
|
li.appendChild(folderNameSpan);
|
||||||
|
|
||||||
const nestedUl = document.createElement("ul");
|
const nestedUl = document.createElement("ul");
|
||||||
|
nestedUl.className = "file-list"; // CORREÇÃO: Adiciona classe para o CSS
|
||||||
|
|
||||||
|
// Chama a função recursivamente para os conteúdos da pasta
|
||||||
renderFileTree(node, nestedUl, newPath);
|
renderFileTree(node, nestedUl, newPath);
|
||||||
li.appendChild(nestedUl);
|
li.appendChild(nestedUl);
|
||||||
li.addEventListener("click", (e) => {
|
|
||||||
e.stopPropagation();
|
// CORREÇÃO 3: Remove o addEventListener de clique daqui. O main.js já cuida disso.
|
||||||
li.classList.toggle("open");
|
|
||||||
});
|
|
||||||
ul.appendChild(li);
|
ul.appendChild(li);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -560,14 +123,12 @@ export async function showOpenProjectModal() {
|
||||||
openProjectModal.classList.add("visible");
|
openProjectModal.classList.add("visible");
|
||||||
try {
|
try {
|
||||||
const response = await fetch("metadata/mmp-manifest.json");
|
const response = await fetch("metadata/mmp-manifest.json");
|
||||||
if (!response.ok)
|
if (!response.ok) throw new Error("Arquivo mmp-manifest.json não encontrado.");
|
||||||
throw new Error("Arquivo mmp-manifest.json não encontrado.");
|
|
||||||
const projects = await response.json();
|
const projects = await response.json();
|
||||||
|
|
||||||
serverProjectsList.innerHTML = "";
|
serverProjectsList.innerHTML = "";
|
||||||
if (projects.length === 0) {
|
if (projects.length === 0) {
|
||||||
serverProjectsList.innerHTML =
|
serverProjectsList.innerHTML = '<p style="color:var(--text-dark);">Nenhum projeto encontrado no servidor.</p>';
|
||||||
'<p style="color:var(--text-dark);">Nenhum projeto encontrado no servidor.</p>';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
projects.forEach((projectName) => {
|
projects.forEach((projectName) => {
|
||||||
|
|
@ -591,16 +152,4 @@ export async function showOpenProjectModal() {
|
||||||
export function closeOpenProjectModal() {
|
export function closeOpenProjectModal() {
|
||||||
const openProjectModal = document.getElementById("open-project-modal");
|
const openProjectModal = document.getElementById("open-project-modal");
|
||||||
openProjectModal.classList.remove("visible");
|
openProjectModal.classList.remove("visible");
|
||||||
}
|
|
||||||
|
|
||||||
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';
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
@ -1,5 +1,23 @@
|
||||||
// js/utils.js
|
// js/utils.js
|
||||||
|
import { appState } from './state.js';
|
||||||
|
import { PIXELS_PER_STEP, ZOOM_LEVELS } from './config.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calcula a quantidade de pixels que representa um segundo na timeline,
|
||||||
|
* levando em conta o BPM e o nível de zoom atual.
|
||||||
|
* @returns {number} A quantidade de pixels por segundo.
|
||||||
|
*/
|
||||||
|
export function getPixelsPerSecond() {
|
||||||
|
const bpm = parseInt(document.getElementById("bpm-input").value, 10) || 120;
|
||||||
|
const stepsPerSecond = (bpm / 60) * 4;
|
||||||
|
const zoomFactor = ZOOM_LEVELS[appState.global.zoomLevelIndex];
|
||||||
|
return stepsPerSecond * PIXELS_PER_STEP * zoomFactor;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calcula o número total de steps no sequenciador de patterns.
|
||||||
|
* @returns {number} O número total de steps.
|
||||||
|
*/
|
||||||
export function getTotalSteps() {
|
export function getTotalSteps() {
|
||||||
const barsInput = document.getElementById("bars-input");
|
const barsInput = document.getElementById("bars-input");
|
||||||
const compassoAInput = document.getElementById("compasso-a-input");
|
const compassoAInput = document.getElementById("compasso-a-input");
|
||||||
|
|
@ -13,19 +31,29 @@ export function getTotalSteps() {
|
||||||
return numberOfBars * beatsPerBar * subdivisions;
|
return numberOfBars * beatsPerBar * subdivisions;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Garante que apenas números sejam inseridos em um campo de input.
|
||||||
|
* @param {Event} event - O evento de input.
|
||||||
|
*/
|
||||||
export function enforceNumericInput(event) {
|
export function enforceNumericInput(event) {
|
||||||
event.target.value = event.target.value.replace(/[^0-9]/g, "");
|
event.target.value = event.target.value.replace(/[^0-9]/g, "");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ajusta o valor de um elemento de input com base em um passo (step),
|
||||||
|
* respeitando os limites de min/max definidos no elemento.
|
||||||
|
* @param {HTMLInputElement} inputElement - O elemento de input a ser ajustado.
|
||||||
|
* @param {number} step - O valor a ser adicionado (pode ser negativo).
|
||||||
|
*/
|
||||||
export function adjustValue(inputElement, step) {
|
export function adjustValue(inputElement, step) {
|
||||||
let currentValue = parseInt(inputElement.value, 10) || 0;
|
let currentValue = parseInt(inputElement.value, 10) || 0;
|
||||||
let min = parseInt(inputElement.dataset.min, 10);
|
let min = parseInt(inputElement.dataset.min, 10);
|
||||||
let max = parseInt(inputElement.dataset.max, 10);
|
let max = parseInt(inputElement.dataset.max, 10);
|
||||||
let newValue = currentValue + step;
|
let newValue = currentValue + step;
|
||||||
|
|
||||||
if (!isNaN(min) && newValue < min) newValue = min;
|
if (!isNaN(min) && newValue < min) newValue = min;
|
||||||
if (!isNaN(max) && newValue > max) newValue = max;
|
if (!isNaN(max) && newValue > max) newValue = max;
|
||||||
inputElement.value = newValue;
|
|
||||||
|
|
||||||
// Dispara um evento 'input' para que outros listeners (como o que redesenha o sequenciador) sejam acionados.
|
inputElement.value = newValue;
|
||||||
inputElement.dispatchEvent(new Event("input", { bubbles: true }));
|
inputElement.dispatchEvent(new Event("input", { bubbles: true }));
|
||||||
}
|
}
|
||||||
|
|
@ -2,44 +2,61 @@
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Desenha a forma de onda de um AudioBuffer em um elemento Canvas.
|
* Desenha a forma de onda de um AudioBuffer em um elemento Canvas.
|
||||||
|
* Pode desenhar apenas um segmento específico do buffer.
|
||||||
* @param {HTMLCanvasElement} canvas - O elemento canvas onde o desenho será feito.
|
* @param {HTMLCanvasElement} canvas - O elemento canvas onde o desenho será feito.
|
||||||
* @param {AudioBuffer} audioBuffer - O buffer de áudio decodificado da faixa.
|
* @param {AudioBuffer} audioBuffer - O buffer de áudio decodificado da faixa.
|
||||||
* @param {string} color - A cor da forma de onda (ex: '#2ecc71').
|
* @param {string} color - A cor da forma de onda (ex: '#2ecc71').
|
||||||
|
* @param {number} [offset=0] - O tempo em segundos de onde começar a desenhar no AudioBuffer.
|
||||||
|
* @param {number} [duration] - A duração em segundos do segmento a ser desenhado.
|
||||||
*/
|
*/
|
||||||
export function drawWaveform(canvas, audioBuffer, color) {
|
export function drawWaveform(canvas, audioBuffer, color, offset = 0, duration) {
|
||||||
if (!canvas || !audioBuffer) return;
|
if (!canvas || !audioBuffer) return;
|
||||||
|
|
||||||
const ctx = canvas.getContext('2d');
|
const ctx = canvas.getContext('2d');
|
||||||
const width = canvas.width;
|
const width = canvas.width;
|
||||||
const height = canvas.height;
|
const height = canvas.height;
|
||||||
const channelData = audioBuffer.getChannelData(0); // Pega os dados do primeiro canal
|
const channelData = audioBuffer.getChannelData(0);
|
||||||
const step = Math.ceil(channelData.length / width);
|
const sampleRate = audioBuffer.sampleRate;
|
||||||
const amp = height / 2; // Amplitude máxima do desenho
|
|
||||||
|
|
||||||
ctx.clearRect(0, 0, width, height); // Limpa o canvas
|
// Se a duração não for fornecida, usa a duração total a partir do offset
|
||||||
|
const finalDuration = duration || (audioBuffer.duration - offset);
|
||||||
|
|
||||||
|
// Calcula os índices de início e fim no array de dados do áudio
|
||||||
|
const startIndex = Math.floor(offset * sampleRate);
|
||||||
|
const endIndex = Math.floor((offset + finalDuration) * sampleRate);
|
||||||
|
const totalSamplesInSegment = endIndex - startIndex;
|
||||||
|
|
||||||
|
const step = Math.ceil(totalSamplesInSegment / width);
|
||||||
|
const amp = height / 2;
|
||||||
|
|
||||||
|
ctx.clearRect(0, 0, width, height);
|
||||||
ctx.strokeStyle = color;
|
ctx.strokeStyle = color;
|
||||||
ctx.lineWidth = 2;
|
ctx.lineWidth = 1;
|
||||||
ctx.beginPath();
|
ctx.beginPath();
|
||||||
|
|
||||||
|
// Desenha a linha do meio (zero amplitude)
|
||||||
|
ctx.moveTo(0, amp);
|
||||||
|
ctx.lineTo(width, amp);
|
||||||
|
|
||||||
for (let i = 0; i < width; i++) {
|
for (let i = 0; i < width; i++) {
|
||||||
let min = 1.0;
|
let min = 1.0;
|
||||||
let max = -1.0;
|
let max = -1.0;
|
||||||
|
|
||||||
// Encontra o valor mínimo e máximo para um bloco de amostras
|
|
||||||
for (let j = 0; j < step; j++) {
|
for (let j = 0; j < step; j++) {
|
||||||
const datum = channelData[(i * step) + j];
|
// --- CORREÇÃO CRÍTICA AQUI ---
|
||||||
if (datum < min) {
|
// Calcula o índice da amostra considerando o startIndex do segmento
|
||||||
min = datum;
|
const sampleIndex = startIndex + (i * step) + j;
|
||||||
}
|
if (sampleIndex < channelData.length) {
|
||||||
if (datum > max) {
|
const datum = channelData[sampleIndex];
|
||||||
max = datum;
|
if (datum < min) min = datum;
|
||||||
|
if (datum > max) max = datum;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Desenha a linha vertical para aquele ponto no tempo
|
|
||||||
const x = i;
|
const x = i;
|
||||||
const y_max = (1 + max) * amp;
|
// Ajusta o desenho para ser centrado verticalmente
|
||||||
const y_min = (1 + min) * amp;
|
const y_max = (1 - max) * amp;
|
||||||
|
const y_min = (1 - min) * amp;
|
||||||
|
|
||||||
ctx.moveTo(x, y_max);
|
ctx.moveTo(x, y_max);
|
||||||
ctx.lineTo(x, y_min);
|
ctx.lineTo(x, y_min);
|
||||||
|
|
|
||||||
|
|
@ -99,7 +99,7 @@
|
||||||
<div class="editor-toolbar">
|
<div class="editor-toolbar">
|
||||||
<div class="playback-controls">
|
<div class="playback-controls">
|
||||||
<i class="fa-solid fa-play" id="play-btn" title="Play/Pause"></i>
|
<i class="fa-solid fa-play" id="play-btn" title="Play/Pause"></i>
|
||||||
<i class="fa-solid fa-stop" id="stop-btn" title="Stop"></i>
|
<i class="fa-solid fa-stop" id="stop-btn" title="Stop"></i>
|
||||||
</div>
|
</div>
|
||||||
<div class="pattern-manager">
|
<div class="pattern-manager">
|
||||||
<h2 id="beat-bassline-title"></h2>
|
<h2 id="beat-bassline-title"></h2>
|
||||||
|
|
@ -113,7 +113,7 @@
|
||||||
<i class="fa-solid fa-table-cells"></i><i class="fa-solid fa-bars-staggered"></i><i class="fa-solid fa-wave-square enabled"></i><i class="fa-solid fa-plus" id="add-bar-btn" title="Adicionar 1 Compasso"></i>
|
<i class="fa-solid fa-table-cells"></i><i class="fa-solid fa-bars-staggered"></i><i class="fa-solid fa-wave-square enabled"></i><i class="fa-solid fa-plus" id="add-bar-btn" title="Adicionar 1 Compasso"></i>
|
||||||
</div>
|
</div>
|
||||||
<div class="zoom-controls">
|
<div class="zoom-controls">
|
||||||
<i class="fa-solid fa-minus" id="remove-instrument-btn"></i><i class="fa-solid fa-plus" id="add-instrument-btn"></i>
|
<i class="fa-solid fa-minus" id="remove-instrument-btn"></i><i class="fa-solid fa-plus" id="add-instrument-btn"></i>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div id="track-container"></div>
|
<div id="track-container"></div>
|
||||||
|
|
@ -123,42 +123,72 @@
|
||||||
<span>Editor de Amostras de Áudio</span>
|
<span>Editor de Amostras de Áudio</span>
|
||||||
|
|
||||||
<div class="playback-controls">
|
<div class="playback-controls">
|
||||||
|
<i class="fa-solid fa-search-minus" id="zoom-out-btn" title="Zoom Out"></i>
|
||||||
|
<i class="fa-solid fa-search-plus" id="zoom-in-btn" title="Zoom In"></i>
|
||||||
|
<i class="fa-solid fa-scissors" id="slice-tool-btn" title="Ferramenta de Corte"></i>
|
||||||
<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>
|
<i class="fa-solid fa-repeat" id="audio-editor-loop-btn" title="Ativar/Desativar Loop"></i>
|
||||||
|
<i class="fa-solid fa-plus" id="add-audio-track-btn" title="Adicionar Pista de Áudio"></i>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div id="audio-track-container">
|
<div id="audio-track-container">
|
||||||
<div class="audio-track-lane">
|
<div class="audio-track-lane">
|
||||||
<div class="track-info">
|
<div class="track-info">
|
||||||
|
<div class="track-info-header">
|
||||||
|
<i class="fa-solid fa-gear"></i>
|
||||||
|
<span class="track-name">Pista de Áudio 1</span>
|
||||||
|
<div class="track-mute"></div>
|
||||||
|
</div>
|
||||||
|
<div class="track-controls">
|
||||||
|
<div class="knob-container">
|
||||||
|
<div class="knob" data-control="volume"><div class="knob-indicator"></div></div>
|
||||||
|
<span>VOL</span>
|
||||||
|
</div>
|
||||||
|
<div class="knob-container">
|
||||||
|
<div class="knob" data-control="pan"><div class="knob-indicator"></div></div>
|
||||||
|
<span>PAN</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="timeline-container">
|
||||||
|
<div class="spectrogram-view-grid" style="width: 4000px;"> <div class="timeline-clip" style="left: 100px; width: 400px;"></div>
|
||||||
|
<div class="playhead"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="audio-track-lane">
|
||||||
|
<div class="track-info">
|
||||||
|
<div class="track-info-header">
|
||||||
<i class="fa-solid fa-gear"></i>
|
<i class="fa-solid fa-gear"></i>
|
||||||
|
<span class="track-name">Pista de Áudio 2</span>
|
||||||
<div class="track-mute"></div>
|
<div class="track-mute"></div>
|
||||||
<span class="track-name">bassslap02.ogg</span>
|
|
||||||
</div>
|
|
||||||
<div class="track-controls">
|
|
||||||
<div class="knob-container">
|
|
||||||
<div class="knob" data-control="volume">
|
|
||||||
<div class="knob-indicator"></div>
|
|
||||||
</div>
|
|
||||||
<span>VOL</span>
|
|
||||||
</div>
|
|
||||||
<div class="knob-container">
|
|
||||||
<div class="knob" data-control="pan">
|
|
||||||
<div class="knob-indicator"></div>
|
|
||||||
</div>
|
|
||||||
<span>PAN</span>
|
|
||||||
</div>
|
|
||||||
<div class="spectrogram-view-wrapper">
|
|
||||||
<div class="spectrogram-view-grid"></div>
|
|
||||||
</div>
|
|
||||||
<div class="spectrogram-view-wrapper">
|
|
||||||
<canvas id="spectrogram-canvas-1" width="800" height="100"></canvas>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
<div class="track-controls">
|
||||||
|
<div class="knob-container">
|
||||||
|
<div class="knob" data-control="volume"><div class="knob-indicator"></div></div>
|
||||||
|
<span>VOL</span>
|
||||||
|
</div>
|
||||||
|
<div class="knob-container">
|
||||||
|
<div class="knob" data-control="pan"><div class="knob-indicator"></div></div>
|
||||||
|
<span>PAN</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="timeline-container">
|
||||||
|
<div id="loop-region" class="loop-region">
|
||||||
|
<div class="spectrogram-view-grid" style="width: 4000px;">
|
||||||
|
|
||||||
|
<div class="timeline-clip" style="left: 50px; width: 600px;">
|
||||||
|
<div class="clip-name">jungle01.ogg</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="playhead"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
<input type="file" id="mmp-file-input" accept=".mmp, .mmpz" style="display: none"/>
|
<input type="file" id="mmp-file-input" accept=".mmp, .mmpz" style="display: none"/>
|
||||||
|
|
@ -181,8 +211,15 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/jszip/3.10.1/jszip.min.js"></script>
|
|
||||||
|
<div id="timeline-context-menu">
|
||||||
|
<div id="set-loop-start">Definir Início do Loop</div>
|
||||||
|
<div id="set-loop-end">Definir Fim do Loop</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/jszip/3.10.1/jszip.min.js"></script>
|
||||||
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/tone/14.7.77/Tone.js"></script>
|
||||||
<script src="assets/js/creations/main.js" type="module"></script>
|
<script src="assets/js/creations/main.js" type="module"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
Loading…
Reference in New Issue