![]() Server : Apache/2 System : Linux server-15-235-50-60 5.15.0-164-generic #174-Ubuntu SMP Fri Nov 14 20:25:16 UTC 2025 x86_64 User : gositeme ( 1004) PHP Version : 8.2.29 Disable Function : exec,system,passthru,shell_exec,proc_close,proc_open,dl,popen,show_source,posix_kill,posix_mkfifo,posix_getpwuid,posix_setpgid,posix_setsid,posix_setuid,posix_setgid,posix_seteuid,posix_setegid,posix_uname Directory : /home/gositeme/domains/soundstudiopro.com/private_html/ |
<?php
session_start();
// Check if user is logged in
if (!isset($_SESSION['user_id'])) {
header('Location: auth/login.php');
exit;
}
// Set page variables for header
$page_title = 'Studio Mixer - SoundStudioPro';
$page_description = 'Professional multi-track mixing console';
$current_page = 'studio';
// Include header
include 'includes/header.php';
// Get user data
require_once 'config/database.php';
$pdo = getDBConnection();
// Start with no tracks - user will load them manually
$tracks = [];
?>
<div class="mixer-page">
<div class="mixer-header">
<h1>🎚️ Studio Mixer</h1>
<p>Professional Multi-Track Mixing Console</p>
<button class="btn-load-track" id="loadTrackBtn">
<i class="fas fa-plus"></i>
Charger une piste
</button>
</div>
<!-- Track Loader Panel -->
<div class="track-loader-panel" id="trackLoaderPanel">
<div class="loader-header">
<h3>📚 Vos pistes disponibles</h3>
<button class="close-loader" id="closeLoader">
<i class="fas fa-times"></i>
</button>
</div>
<div class="loader-search">
<input type="text" id="trackSearch" placeholder="Rechercher une piste..." class="search-input">
</div>
<div class="track-list-container" id="trackListContainer">
<div class="loading-tracks">
<i class="fas fa-spinner fa-spin"></i>
<p>Chargement de vos pistes...</p>
</div>
</div>
</div>
<div class="mixer-container">
<!-- Transport Controls -->
<div class="transport-bar">
<div class="transport-left">
<button class="transport-btn play-btn" id="mixerPlay">
<i class="fas fa-play"></i>
</button>
<button class="transport-btn pause-btn" id="mixerPause">
<i class="fas fa-pause"></i>
</button>
<button class="transport-btn stop-btn" id="mixerStop">
<i class="fas fa-stop"></i>
</button>
</div>
<div class="transport-center">
<div class="time-display">
<span id="currentTime">00:00</span>
<span class="time-separator">/</span>
<span id="totalTime">00:00</span>
</div>
<div class="bpm-control">
<label>BPM</label>
<div class="bpm-input-group">
<button class="bpm-btn" id="bpmDown">-</button>
<input type="number" id="bpmValue" value="120" min="60" max="200" class="bpm-input">
<button class="bpm-btn" id="bpmUp">+</button>
</div>
</div>
</div>
<div class="transport-right">
<button class="transport-btn" id="mixerRecord" title="Record Mix">
<i class="fas fa-circle"></i>
</button>
<button class="transport-btn" id="mixerLoop" title="Loop">
<i class="fas fa-redo"></i>
</button>
</div>
</div>
<!-- Master Section -->
<div class="master-channel">
<div class="master-label-section">
<div class="master-label">MASTER</div>
<button class="master-mute-btn" id="masterMute" title="Mute Master">
<i class="fas fa-volume-up"></i>
</button>
</div>
<div class="master-fader-section">
<div class="fader-label">Volume</div>
<div class="fader-track">
<div class="fader-scale">
<span class="scale-mark">100</span>
<span class="scale-mark">75</span>
<span class="scale-mark">50</span>
<span class="scale-mark">25</span>
<span class="scale-mark">0</span>
</div>
<div class="fader-knob master-knob" id="masterFader" style="top: 0%;">
<div class="fader-grip"></div>
</div>
</div>
<div class="fader-value" id="masterValue">100</div>
</div>
<div class="master-meter-section">
<div class="meter-label">Level</div>
<div class="meter-bar">
<div class="meter-level" id="masterLevel"></div>
<div class="meter-peak" id="masterPeak"></div>
</div>
</div>
</div>
<!-- Mixer Channels -->
<div class="mixer-channels" id="mixerChannels">
<div class="empty-mixer" id="emptyMixer">
<i class="fas fa-music"></i>
<p>Chargement de pistes...</p>
<p style="font-size: 1rem; color: #94a3b8; margin-top: 0.5rem;">Cliquez sur "Charger une piste" pour commencer</p>
</div>
</div>
</div>
</div>
<style>
.mixer-page {
min-height: 100vh;
background: linear-gradient(135deg, #0a0a0a 0%, #1a1a2e 50%, #16213e 100%);
padding: 2rem 0 6rem;
color: #fff;
}
.mixer-header {
text-align: center;
margin-bottom: 3rem;
padding: 0 2rem;
}
.mixer-header h1 {
font-size: 3rem;
font-weight: 900;
margin-bottom: 0.5rem;
background: linear-gradient(135deg, #667eea, #764ba2);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
.mixer-header p {
font-size: 1.4rem;
color: #a0aec0;
margin-bottom: 1.5rem;
}
.btn-load-track {
display: inline-flex;
align-items: center;
gap: 0.8rem;
padding: 1rem 2rem;
background: linear-gradient(135deg, #667eea, #764ba2);
color: white;
border: none;
border-radius: 12px;
font-size: 1.1rem;
font-weight: 600;
cursor: pointer;
transition: all 0.3s ease;
box-shadow: 0 4px 15px rgba(102, 126, 234, 0.3);
}
.btn-load-track:hover {
transform: translateY(-2px);
box-shadow: 0 6px 20px rgba(102, 126, 234, 0.4);
}
.btn-load-track:active {
transform: translateY(0);
}
/* Track Loader Panel */
.track-loader-panel {
position: fixed;
top: 0;
right: -400px;
width: 400px;
height: 100vh;
background: linear-gradient(135deg, #1a1a2e, #16213e);
border-left: 2px solid rgba(102, 126, 234, 0.3);
box-shadow: -10px 0 40px rgba(0, 0, 0, 0.5);
z-index: 10000;
transition: right 0.4s cubic-bezier(0.4, 0, 0.2, 1);
display: flex;
flex-direction: column;
overflow: hidden;
}
.track-loader-panel.active {
right: 0;
}
.loader-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 1.5rem;
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
background: rgba(0, 0, 0, 0.3);
}
.loader-header h3 {
margin: 0;
font-size: 1.4rem;
color: #e2e8f0;
}
.close-loader {
width: 36px;
height: 36px;
border: none;
background: rgba(245, 101, 101, 0.2);
color: #f56565;
border-radius: 8px;
font-size: 1.2rem;
cursor: pointer;
transition: all 0.3s ease;
display: flex;
align-items: center;
justify-content: center;
}
.close-loader:hover {
background: rgba(245, 101, 101, 0.3);
transform: scale(1.1);
}
.loader-search {
padding: 1rem 1.5rem;
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
}
.search-input {
width: 100%;
padding: 0.9rem 1.2rem;
background: rgba(0, 0, 0, 0.4);
border: 2px solid rgba(102, 126, 234, 0.3);
border-radius: 10px;
color: white;
font-size: 1rem;
transition: all 0.3s ease;
}
.search-input:focus {
outline: none;
border-color: rgba(102, 126, 234, 0.6);
background: rgba(0, 0, 0, 0.5);
}
.search-input::placeholder {
color: #94a3b8;
}
.track-list-container {
flex: 1;
overflow-y: auto;
padding: 1rem;
}
.track-list-container::-webkit-scrollbar {
width: 8px;
}
.track-list-container::-webkit-scrollbar-track {
background: rgba(0, 0, 0, 0.2);
border-radius: 4px;
}
.track-list-container::-webkit-scrollbar-thumb {
background: rgba(102, 126, 234, 0.5);
border-radius: 4px;
}
.track-list-container::-webkit-scrollbar-thumb:hover {
background: rgba(102, 126, 234, 0.7);
}
.track-item-mixer {
background: rgba(0, 0, 0, 0.3);
border: 2px solid rgba(255, 255, 255, 0.1);
border-radius: 12px;
padding: 1rem 1.2rem;
margin-bottom: 0.8rem;
cursor: pointer;
transition: all 0.3s ease;
display: flex;
align-items: center;
justify-content: space-between;
gap: 1rem;
}
.track-item-mixer:hover {
background: rgba(102, 126, 234, 0.2);
border-color: rgba(102, 126, 234, 0.5);
transform: translateX(-4px);
}
.track-item-mixer.loaded {
opacity: 0.5;
cursor: not-allowed;
}
.track-item-mixer.loaded:hover {
transform: none;
}
.track-item-info {
flex: 1;
min-width: 0;
}
.track-item-title {
font-size: 1rem;
font-weight: 600;
color: #e2e8f0;
margin-bottom: 0.3rem;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.track-item-meta {
font-size: 0.85rem;
color: #94a3b8;
display: flex;
align-items: center;
gap: 0.5rem;
}
.track-item-action {
padding: 0.6rem 1rem;
background: linear-gradient(135deg, #667eea, #764ba2);
color: white;
border: none;
border-radius: 8px;
font-weight: 600;
font-size: 0.9rem;
cursor: pointer;
transition: all 0.3s ease;
white-space: nowrap;
}
.track-item-action:hover {
transform: scale(1.05);
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.4);
}
.track-item-action:disabled {
opacity: 0.5;
cursor: not-allowed;
transform: none;
}
.loading-tracks,
.empty-tracks {
text-align: center;
padding: 3rem 1rem;
color: #94a3b8;
}
.loading-tracks i,
.empty-tracks i {
font-size: 2.5rem;
margin-bottom: 1rem;
color: #667eea;
}
.empty-tracks p {
font-size: 1.1rem;
}
.mixer-container {
max-width: 1600px;
margin: 0 auto;
padding: 0 2rem;
}
/* Transport Bar */
.transport-bar {
display: flex;
justify-content: space-between;
align-items: center;
background: rgba(0, 0, 0, 0.4);
border: 2px solid rgba(102, 126, 234, 0.3);
border-radius: 16px;
padding: 1.5rem 2rem;
margin-bottom: 2rem;
backdrop-filter: blur(10px);
}
.transport-left,
.transport-right {
display: flex;
gap: 1rem;
}
.transport-center {
flex: 1;
text-align: center;
}
.time-display {
font-size: 1.8rem;
font-weight: 700;
color: #e2e8f0;
font-variant-numeric: tabular-nums;
}
.time-separator {
margin: 0 1rem;
color: #667eea;
}
/* BPM Control */
.bpm-control {
display: flex;
flex-direction: column;
align-items: center;
gap: 0.5rem;
margin-top: 1rem;
}
.bpm-control label {
font-size: 0.85rem;
color: #94a3b8;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 1px;
}
.bpm-input-group {
display: flex;
align-items: center;
gap: 0.5rem;
}
.bpm-btn {
width: 36px;
height: 36px;
border: 2px solid rgba(102, 126, 234, 0.4);
background: rgba(102, 126, 234, 0.1);
color: #667eea;
border-radius: 8px;
font-size: 1.2rem;
font-weight: 700;
cursor: pointer;
transition: all 0.2s ease;
display: flex;
align-items: center;
justify-content: center;
}
.bpm-btn:hover {
background: rgba(102, 126, 234, 0.3);
border-color: rgba(102, 126, 234, 0.6);
transform: scale(1.1);
}
.bpm-input {
width: 70px;
padding: 0.6rem;
background: rgba(0, 0, 0, 0.4);
border: 2px solid rgba(102, 126, 234, 0.3);
border-radius: 8px;
color: #e2e8f0;
font-size: 1.2rem;
font-weight: 700;
text-align: center;
font-variant-numeric: tabular-nums;
}
.bpm-input:focus {
outline: none;
border-color: rgba(102, 126, 234, 0.6);
background: rgba(0, 0, 0, 0.5);
}
.transport-btn {
width: 50px;
height: 50px;
border-radius: 12px;
border: 2px solid rgba(102, 126, 234, 0.4);
background: rgba(102, 126, 234, 0.1);
color: #667eea;
font-size: 1.4rem;
cursor: pointer;
transition: all 0.3s ease;
display: flex;
align-items: center;
justify-content: center;
}
.transport-btn:hover {
background: rgba(102, 126, 234, 0.3);
border-color: rgba(102, 126, 234, 0.6);
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.3);
}
.transport-btn.active {
background: linear-gradient(135deg, #667eea, #764ba2);
border-color: rgba(102, 126, 234, 0.8);
color: #fff;
box-shadow: 0 0 20px rgba(102, 126, 234, 0.5);
}
.play-btn.active {
background: linear-gradient(135deg, #48bb78, #38a169);
border-color: rgba(72, 187, 120, 0.8);
}
/* Mixer Channels */
.mixer-channels {
display: flex;
gap: 1.5rem;
overflow-x: auto;
padding: 1rem 0;
margin-bottom: 2rem;
scrollbar-width: thin;
scrollbar-color: rgba(102, 126, 234, 0.5) rgba(0, 0, 0, 0.2);
}
.mixer-channels::-webkit-scrollbar {
height: 8px;
}
.mixer-channels::-webkit-scrollbar-track {
background: rgba(0, 0, 0, 0.2);
border-radius: 4px;
}
.mixer-channels::-webkit-scrollbar-thumb {
background: rgba(102, 126, 234, 0.5);
border-radius: 4px;
}
.mixer-channels::-webkit-scrollbar-thumb:hover {
background: rgba(102, 126, 234, 0.7);
}
.mixer-channel {
min-width: 120px;
background: rgba(0, 0, 0, 0.5);
border: 2px solid rgba(102, 126, 234, 0.2);
border-radius: 16px;
padding: 1rem;
display: flex;
flex-direction: column;
gap: 1rem;
transition: all 0.3s ease;
}
.mixer-channel:hover {
border-color: rgba(102, 126, 234, 0.5);
background: rgba(0, 0, 0, 0.6);
transform: translateY(-4px);
box-shadow: 0 8px 24px rgba(102, 126, 234, 0.2);
}
/* Channel Header */
.channel-header {
display: flex;
align-items: center;
gap: 0.5rem;
padding-bottom: 0.5rem;
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
}
.channel-number {
width: 24px;
height: 24px;
background: linear-gradient(135deg, #667eea, #764ba2);
border-radius: 6px;
display: flex;
align-items: center;
justify-content: center;
font-weight: 700;
font-size: 0.85rem;
color: #fff;
}
.channel-name {
flex: 1;
font-size: 0.9rem;
font-weight: 600;
color: #e2e8f0;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
/* Track Controls */
.track-controls {
display: flex;
flex-direction: column;
gap: 0.8rem;
padding: 0.8rem;
background: rgba(0, 0, 0, 0.3);
border-radius: 10px;
margin-bottom: 0.5rem;
}
.track-play-btn,
.track-stop-btn {
width: 100%;
padding: 0.6rem;
border: 2px solid rgba(102, 126, 234, 0.4);
background: rgba(102, 126, 234, 0.1);
color: #667eea;
border-radius: 8px;
font-size: 0.9rem;
cursor: pointer;
transition: all 0.3s ease;
display: flex;
align-items: center;
justify-content: center;
gap: 0.5rem;
}
.track-play-btn:hover {
background: rgba(72, 187, 120, 0.2);
border-color: rgba(72, 187, 120, 0.6);
color: #48bb78;
}
.track-stop-btn:hover {
background: rgba(245, 101, 101, 0.2);
border-color: rgba(245, 101, 101, 0.6);
color: #f56565;
}
.track-play-btn.active {
background: linear-gradient(135deg, #48bb78, #38a169);
border-color: rgba(72, 187, 120, 0.8);
color: #fff;
}
.track-stop-btn.active {
background: linear-gradient(135deg, #f56565, #e53e3e);
border-color: rgba(245, 101, 101, 0.8);
color: #fff;
}
.track-bpm-control {
display: flex;
flex-direction: column;
gap: 0.4rem;
}
.track-bpm-control label {
font-size: 0.75rem;
color: #94a3b8;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.track-bpm-input-group {
display: flex;
align-items: center;
gap: 0.3rem;
}
.track-bpm-btn {
width: 28px;
height: 28px;
border: 1px solid rgba(102, 126, 234, 0.4);
background: rgba(102, 126, 234, 0.1);
color: #667eea;
border-radius: 6px;
font-size: 0.9rem;
font-weight: 700;
cursor: pointer;
transition: all 0.2s ease;
display: flex;
align-items: center;
justify-content: center;
}
.track-bpm-btn:hover {
background: rgba(102, 126, 234, 0.3);
border-color: rgba(102, 126, 234, 0.6);
}
.track-bpm-input {
flex: 1;
padding: 0.4rem 0.5rem;
background: rgba(0, 0, 0, 0.4);
border: 1px solid rgba(102, 126, 234, 0.3);
border-radius: 6px;
color: #e2e8f0;
font-size: 0.9rem;
font-weight: 700;
text-align: center;
font-variant-numeric: tabular-nums;
}
.track-bpm-input:focus {
outline: none;
border-color: rgba(102, 126, 234, 0.6);
background: rgba(0, 0, 0, 0.5);
}
overflow: hidden;
text-overflow: ellipsis;
}
.channel-controls {
display: flex;
gap: 0.3rem;
}
.channel-btn {
width: 28px;
height: 28px;
border: 1px solid rgba(255, 255, 255, 0.2);
background: rgba(255, 255, 255, 0.05);
border-radius: 6px;
color: #a0aec0;
font-size: 0.85rem;
cursor: pointer;
transition: all 0.2s ease;
display: flex;
align-items: center;
justify-content: center;
}
.channel-btn:hover {
background: rgba(102, 126, 234, 0.2);
border-color: rgba(102, 126, 234, 0.4);
color: #667eea;
}
.channel-btn.active {
background: linear-gradient(135deg, #f56565, #e53e3e);
border-color: rgba(245, 101, 101, 0.6);
color: #fff;
}
.solo-btn.active {
background: linear-gradient(135deg, #f59e0b, #d97706);
border-color: rgba(245, 158, 11, 0.6);
}
.remove-btn {
background: rgba(245, 101, 101, 0.1);
border-color: rgba(245, 101, 101, 0.3);
color: #f56565;
}
.remove-btn:hover {
background: rgba(245, 101, 101, 0.2);
border-color: rgba(245, 101, 101, 0.5);
color: #fff;
}
/* EQ Section */
.channel-eq {
display: flex;
justify-content: space-around;
gap: 0.5rem;
padding: 0.5rem 0;
}
.eq-band {
display: flex;
flex-direction: column;
align-items: center;
gap: 0.4rem;
}
.eq-band label {
font-size: 0.75rem;
color: #94a3b8;
font-weight: 700;
text-transform: uppercase;
}
.eq-knob-wrapper {
position: relative;
width: 45px;
height: 45px;
}
.eq-knob {
width: 45px;
height: 45px;
border-radius: 50%;
background: linear-gradient(135deg, rgba(102, 126, 234, 0.2), rgba(118, 75, 162, 0.2));
border: 2px solid rgba(102, 126, 234, 0.4);
position: relative;
cursor: pointer;
transition: all 0.2s ease;
display: flex;
align-items: center;
justify-content: center;
transform: rotate(0deg);
}
.eq-knob:hover {
border-color: rgba(102, 126, 234, 0.7);
background: linear-gradient(135deg, rgba(102, 126, 234, 0.3), rgba(118, 75, 162, 0.3));
transform: scale(1.1) rotate(0deg);
}
.eq-value {
font-size: 0.7rem;
color: #e2e8f0;
font-weight: 700;
pointer-events: none;
}
/* Volume Fader */
.channel-fader {
display: flex;
flex-direction: column;
align-items: center;
gap: 0.5rem;
flex: 1;
}
.fader-track {
width: 35px;
height: 220px;
background: linear-gradient(to top, rgba(0, 0, 0, 0.6), rgba(255, 255, 255, 0.1));
border: 2px solid rgba(255, 255, 255, 0.15);
border-radius: 18px;
position: relative;
box-shadow: inset 0 2px 10px rgba(0, 0, 0, 0.5);
}
.fader-scale {
position: absolute;
left: -30px;
top: 0;
bottom: 0;
display: flex;
flex-direction: column;
justify-content: space-between;
padding: 8px 0;
font-size: 0.65rem;
color: #94a3b8;
font-weight: 600;
font-variant-numeric: tabular-nums;
}
.scale-mark {
height: 1px;
width: 10px;
background: rgba(255, 255, 255, 0.3);
margin-left: auto;
}
.fader-knob {
position: absolute;
left: 50%;
top: 0;
transform: translateX(-50%);
width: 45px;
height: 18px;
background: linear-gradient(135deg, #667eea, #764ba2);
border: 2px solid rgba(255, 255, 255, 0.3);
border-radius: 9px;
cursor: grab;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.4);
z-index: 10;
user-select: none;
-webkit-user-select: none;
touch-action: none;
}
.fader-knob:active,
.fader-knob.dragging {
cursor: grabbing !important;
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.6);
transition: none;
}
.fader-grip {
width: 100%;
height: 100%;
background: repeating-linear-gradient(
90deg,
rgba(255, 255, 255, 0.2) 0px,
rgba(255, 255, 255, 0.2) 2px,
transparent 2px,
transparent 4px
);
border-radius: 7px;
}
.fader-value {
font-size: 0.9rem;
color: #e2e8f0;
font-weight: 700;
min-height: 1.2rem;
text-align: center;
font-variant-numeric: tabular-nums;
}
/* Pan Control */
.channel-pan {
display: flex;
flex-direction: column;
align-items: center;
gap: 0.5rem;
}
.pan-track {
width: 100%;
height: 32px;
background: rgba(0, 0, 0, 0.4);
border: 2px solid rgba(255, 255, 255, 0.15);
border-radius: 16px;
position: relative;
box-shadow: inset 0 2px 5px rgba(0, 0, 0, 0.5);
}
.pan-mark {
position: absolute;
top: 50%;
transform: translateY(-50%);
font-size: 0.7rem;
color: #94a3b8;
font-weight: 700;
pointer-events: none;
}
.pan-center {
left: 50%;
transform: translate(-50%, -50%);
}
.pan-left {
left: 6px;
}
.pan-right {
right: 6px;
}
.pan-knob {
position: absolute;
top: 50%;
transform: translate(-50%, -50%);
width: 26px;
height: 26px;
background: linear-gradient(135deg, #667eea, #764ba2);
border: 2px solid rgba(255, 255, 255, 0.3);
border-radius: 50%;
cursor: pointer;
transition: all 0.2s ease;
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.4);
z-index: 10;
}
.pan-knob:hover {
transform: translate(-50%, -50%) scale(1.15);
box-shadow: 0 3px 10px rgba(102, 126, 234, 0.5);
}
.pan-indicator {
position: absolute;
top: 50%;
left: 50%;
width: 3px;
height: 14px;
background: #fff;
transform: translate(-50%, -50%);
border-radius: 2px;
pointer-events: none;
}
.pan-value {
font-size: 0.8rem;
color: #e2e8f0;
font-weight: 600;
min-height: 1.2rem;
text-align: center;
}
/* Level Meter */
.channel-meter {
height: 220px;
width: 100%;
}
.meter-bar {
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.4);
border: 2px solid rgba(255, 255, 255, 0.15);
border-radius: 8px;
position: relative;
overflow: hidden;
box-shadow: inset 0 2px 5px rgba(0, 0, 0, 0.5);
}
.meter-level {
position: absolute;
bottom: 0;
left: 0;
right: 0;
width: 100%;
background: #667eea;
transition: height 0.05s linear;
height: 0%;
border-radius: 0 0 6px 6px;
}
.meter-peak {
position: absolute;
bottom: 0;
left: 0;
right: 0;
width: 2px;
background: #fff;
height: 0%;
margin: 0 auto;
opacity: 0;
transition: opacity 0.3s ease;
z-index: 5;
}
.meter-peak.active {
opacity: 1;
}
/* Master Channel */
.master-channel {
background: rgba(0, 0, 0, 0.5);
border: 2px solid rgba(102, 126, 234, 0.3);
border-radius: 12px;
padding: 1.2rem 1.5rem;
display: flex;
align-items: center;
justify-content: center;
gap: 2rem;
margin-bottom: 2rem;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3);
}
.master-label-section {
display: flex;
flex-direction: column;
align-items: center;
gap: 0.8rem;
min-width: 90px;
}
.master-label {
font-size: 0.85rem;
font-weight: 900;
color: #667eea;
letter-spacing: 1.5px;
text-transform: uppercase;
text-align: center;
}
.master-mute-btn {
width: 36px;
height: 36px;
border: 2px solid rgba(245, 101, 101, 0.4);
background: rgba(245, 101, 101, 0.1);
border-radius: 8px;
color: #f56565;
font-size: 1rem;
cursor: pointer;
transition: all 0.3s ease;
display: flex;
align-items: center;
justify-content: center;
}
.master-mute-btn:hover {
background: rgba(245, 101, 101, 0.2);
border-color: rgba(245, 101, 101, 0.6);
transform: scale(1.05);
}
.master-mute-btn.active {
background: linear-gradient(135deg, #f56565, #e53e3e);
border-color: rgba(245, 101, 101, 0.8);
color: #fff;
box-shadow: 0 0 15px rgba(245, 101, 101, 0.4);
}
.master-mute-btn:not(.active) {
background: rgba(72, 187, 120, 0.1);
border-color: rgba(72, 187, 120, 0.4);
color: #48bb78;
}
.master-mute-btn:not(.active):hover {
background: rgba(72, 187, 120, 0.2);
border-color: rgba(72, 187, 120, 0.6);
}
.master-fader-section {
display: flex;
flex-direction: column;
align-items: center;
gap: 0.5rem;
width: 80px;
flex-shrink: 0;
}
.master-fader-section .fader-label {
font-size: 0.75rem;
color: #94a3b8;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.master-fader-section .fader-track {
height: 200px;
width: 100%;
}
.master-fader-section .fader-value {
font-size: 0.9rem;
color: #e2e8f0;
font-weight: 700;
min-height: 1.2rem;
text-align: center;
}
.master-knob {
background: linear-gradient(135deg, #f59e0b, #d97706);
}
.master-meter-section {
display: flex;
flex-direction: column;
align-items: center;
gap: 0.5rem;
width: 60px;
flex-shrink: 0;
}
.master-meter-section .meter-label {
font-size: 0.75rem;
color: #94a3b8;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.master-meter-section .meter-bar {
height: 200px;
width: 100%;
}
.empty-mixer {
text-align: center;
padding: 4rem 2rem;
color: #a0aec0;
grid-column: 1 / -1;
}
.empty-mixer i {
font-size: 4rem;
margin-bottom: 1rem;
color: #667eea;
}
.empty-mixer p {
font-size: 1.4rem;
margin-bottom: 2rem;
}
/* Responsive Design */
@media (max-width: 768px) {
.mixer-header h1 {
font-size: 2rem;
}
.mixer-container {
padding: 0 1rem;
}
.transport-bar {
flex-direction: column;
gap: 1rem;
padding: 1rem;
}
.transport-left,
.transport-right {
width: 100%;
justify-content: center;
}
.mixer-channel {
min-width: 100px;
}
.fader-track {
height: 180px;
}
.channel-meter {
height: 180px;
}
.master-channel {
flex-direction: row;
flex-wrap: wrap;
justify-content: center;
gap: 1.5rem;
padding: 1rem;
}
.master-label-section {
width: auto;
min-width: 80px;
}
.master-fader-section,
.master-meter-section {
width: auto;
min-width: 60px;
}
.master-fader-section .fader-track,
.master-meter-section .meter-bar {
height: 150px;
}
}
</style>
<script>
// Studio Mixer Audio Engine
class StudioMixerEngine {
constructor() {
this.audioContext = null;
this.masterGain = null;
this.tracks = new Map();
this.isPlaying = false;
this.isRecording = false;
this.isLooping = false;
this.currentTime = 0;
this.startTime = 0;
this.totalTime = 0;
this.bpm = 120;
this.playbackRate = 1.0;
}
async initialize() {
try {
this.audioContext = new (window.AudioContext || window.webkitAudioContext)();
this.masterGain = this.audioContext.createGain();
this.masterGain.connect(this.audioContext.destination);
this.masterGain.gain.setValueAtTime(1.0, this.audioContext.currentTime); // Start at 100%
console.log('🎚️ Studio Mixer Engine initialized');
return true;
} catch (error) {
console.error('Failed to initialize mixer engine:', error);
return false;
}
}
async loadTrack(trackId, audioUrl) {
// Ensure audio context is initialized
if (!this.audioContext) {
console.error('Audio context not initialized, initializing now...');
await this.initialize();
}
try {
console.log(`📥 Loading track ${trackId} from ${audioUrl}`);
const response = await fetch(audioUrl);
if (!response.ok) {
throw new Error(`Failed to fetch audio: ${response.status} ${response.statusText}`);
}
const arrayBuffer = await response.arrayBuffer();
const audioBuffer = await this.audioContext.decodeAudioData(arrayBuffer);
console.log(`✅ Track ${trackId} decoded: ${audioBuffer.duration}s, ${audioBuffer.sampleRate}Hz`);
const track = {
id: trackId,
buffer: audioBuffer,
gain: this.audioContext.createGain(),
pan: this.audioContext.createStereoPanner(),
eq: {
high: this.audioContext.createBiquadFilter(),
mid: this.audioContext.createBiquadFilter(),
low: this.audioContext.createBiquadFilter()
},
source: null,
isMuted: false,
isSoloed: false,
volume: 1.0, // Start at 100% (100 on 0-100 scale)
panValue: 0,
eqValues: { high: 0, mid: 0, low: 0 },
bpm: 120 // Default BPM per track
};
// Set initial volume
track.gain.gain.setValueAtTime(1.0, this.audioContext.currentTime);
// Setup EQ filters
track.eq.high.type = 'highshelf';
track.eq.high.frequency.setValueAtTime(5000, this.audioContext.currentTime);
track.eq.mid.type = 'peaking';
track.eq.mid.frequency.setValueAtTime(1000, this.audioContext.currentTime);
track.eq.mid.Q.setValueAtTime(1, this.audioContext.currentTime);
track.eq.low.type = 'lowshelf';
track.eq.low.frequency.setValueAtTime(200, this.audioContext.currentTime);
// Connect: source -> gain -> eq -> pan -> master
track.gain.connect(track.eq.high);
track.eq.high.connect(track.eq.mid);
track.eq.mid.connect(track.eq.low);
track.eq.low.connect(track.pan);
track.pan.connect(this.masterGain);
// Verify master connection
if (!this.masterGain) {
console.error('Master gain not initialized!');
} else {
console.log(`🎵 Track ${trackId} loaded and connected to master`);
console.log('Audio chain: source -> gain -> eq.high -> eq.mid -> eq.low -> pan -> masterGain -> destination');
}
this.tracks.set(trackId, track);
// Update total time
if (audioBuffer.duration > this.totalTime) {
this.totalTime = audioBuffer.duration;
updateTimeDisplay(0, this.totalTime);
}
return track;
} catch (error) {
console.error(`Failed to load track ${trackId}:`, error);
return null;
}
}
async play() {
if (this.isPlaying) return;
if (this.tracks.size === 0) {
console.warn('No tracks loaded');
return;
}
// Ensure audio context is initialized and resumed
if (!this.audioContext) {
await this.initialize();
}
if (this.audioContext.state === 'suspended') {
try {
await this.audioContext.resume();
console.log('✅ Audio context resumed for global play');
} catch (error) {
console.error('❌ Failed to resume audio context:', error);
}
}
this.isPlaying = true;
this.startTime = this.audioContext.currentTime - this.currentTime;
// Play all tracks that aren't muted
const playPromises = [];
this.tracks.forEach((track, trackId) => {
if (track.buffer && !track.isMuted) {
playPromises.push(this.playTrack(trackId));
}
});
// Wait for all tracks to start
await Promise.all(playPromises);
this.updateMeters();
}
pause() {
this.isPlaying = false;
this.currentTime = this.audioContext.currentTime - this.startTime;
this.tracks.forEach((track) => {
if (track.source) {
track.source.stop();
track.source = null;
}
});
}
stop() {
this.isPlaying = false;
this.currentTime = 0;
this.startTime = 0;
this.tracks.forEach((track) => {
if (track.source) {
track.source.stop();
track.source = null;
}
});
updateTimeDisplay(0, this.totalTime);
}
async playTrack(trackId) {
// Ensure audio context is initialized
if (!this.audioContext) {
console.error('Audio context not initialized!');
await this.initialize();
}
const track = this.tracks.get(trackId);
if (!track) {
console.error(`Track ${trackId} not found in tracks map`);
return;
}
if (!track.buffer) {
console.error(`Track ${trackId} has no audio buffer`);
return;
}
// Resume audio context if suspended (required by browser autoplay policy)
if (this.audioContext.state === 'suspended') {
try {
await this.audioContext.resume();
console.log('✅ Audio context resumed');
} catch (error) {
console.error('❌ Failed to resume audio context:', error);
}
}
// Stop and disconnect existing source
if (track.source) {
try {
track.source.stop();
track.source.disconnect();
} catch (error) {
// Source might already be stopped
}
track.source = null;
}
// Create new source
track.source = this.audioContext.createBufferSource();
track.source.buffer = track.buffer;
const playbackRate = track.bpm ? (track.bpm / 120) : this.playbackRate;
track.source.playbackRate.setValueAtTime(playbackRate, this.audioContext.currentTime);
// Connect source to gain
// Audio routing chain: source -> gain -> eq.high -> eq.mid -> eq.low -> pan -> masterGain -> destination
// This chain is established in loadTrack(), we just connect the source here
track.source.connect(track.gain);
// Verify connections
console.log(`▶️ Playing track ${trackId} at ${playbackRate}x speed`);
console.log(`Master gain value: ${this.masterGain.gain.value}`);
console.log(`Track gain value: ${track.gain.gain.value}`);
console.log(`Audio context state: ${this.audioContext.state}`);
console.log(`Track buffer duration: ${track.buffer.duration}s`);
console.log(`Track buffer sample rate: ${track.buffer.sampleRate}Hz`);
// Ensure master is not muted
const masterMuteBtn = document.getElementById('masterMute');
if (masterMuteBtn && masterMuteBtn.classList.contains('active')) {
console.warn('⚠️ Master is muted! Unmuting...');
masterMuteBtn.classList.remove('active');
const icon = masterMuteBtn.querySelector('i');
if (icon) icon.className = 'fas fa-volume-up';
const currentVolume = parseInt(document.getElementById('masterValue')?.textContent || '50');
this.setMasterVolume(currentVolume);
}
try {
track.source.start(0);
console.log(`✅ Track ${trackId} started successfully`);
} catch (error) {
console.error(`❌ Error starting track ${trackId}:`, error);
track.source = null;
}
track.source.onended = () => {
console.log(`⏹️ Track ${trackId} finished playing`);
track.source = null;
};
track.source.onerror = (error) => {
console.error(`❌ Track ${trackId} playback error:`, error);
track.source = null;
};
}
stopTrack(trackId) {
const track = this.tracks.get(trackId);
if (!track) return;
if (track.source) {
track.source.stop();
track.source = null;
}
}
setTrackBPM(trackId, bpm) {
const track = this.tracks.get(trackId);
if (!track) return;
const clampedBPM = Math.max(60, Math.min(200, bpm));
track.bpm = clampedBPM;
// Update playback rate if track is currently playing
if (track.source && track.source.playbackRate) {
const playbackRate = clampedBPM / 120;
track.source.playbackRate.setValueAtTime(playbackRate, this.audioContext.currentTime);
}
// Update UI
const bpmInput = document.querySelector(`.track-bpm-input[data-track="${trackId}"]`);
if (bpmInput) {
bpmInput.value = clampedBPM;
}
}
setTrackVolume(trackId, volumePercent) {
const track = this.tracks.get(trackId);
if (!track) return;
// Convert 0-100 to 0-1 linear gain
const linearGain = Math.max(0, Math.min(1, volumePercent / 100));
track.volume = linearGain;
track.gain.gain.setValueAtTime(linearGain, this.audioContext.currentTime);
}
setTrackPan(trackId, panValue) {
const track = this.tracks.get(trackId);
if (!track) return;
track.panValue = Math.max(-1, Math.min(1, panValue));
track.pan.pan.setValueAtTime(track.panValue, this.audioContext.currentTime);
}
setTrackEQ(trackId, band, dbValue) {
const track = this.tracks.get(trackId);
if (!track || !track.eq[band]) return;
track.eqValues[band] = dbValue;
const gainValue = Math.pow(10, dbValue / 20);
track.eq[band].gain.setValueAtTime(gainValue, this.audioContext.currentTime);
}
muteTrack(trackId) {
const track = this.tracks.get(trackId);
if (!track) return;
track.isMuted = !track.isMuted;
track.gain.gain.setValueAtTime(track.isMuted ? 0 : track.volume, this.audioContext.currentTime);
}
soloTrack(trackId) {
const track = this.tracks.get(trackId);
if (!track) return;
track.isSoloed = !track.isSoloed;
// Mute all other tracks if this is soloed
this.tracks.forEach((otherTrack, otherId) => {
if (otherId !== trackId) {
const shouldMute = track.isSoloed;
otherTrack.gain.gain.setValueAtTime(
shouldMute ? 0 : (otherTrack.isMuted ? 0 : otherTrack.volume),
this.audioContext.currentTime
);
}
});
}
setMasterVolume(volumePercent) {
// Convert 0-100 to 0-1 linear gain
const linearGain = Math.max(0, Math.min(1, volumePercent / 100));
this.masterGain.gain.setValueAtTime(linearGain, this.audioContext.currentTime);
}
setBPM(bpm) {
this.bpm = Math.max(60, Math.min(200, bpm));
// Calculate playback rate based on BPM (120 BPM = 1.0x speed)
this.playbackRate = this.bpm / 120;
// Update all playing tracks
this.tracks.forEach((track, trackId) => {
if (track.source && track.source.playbackRate) {
track.source.playbackRate.setValueAtTime(this.playbackRate, this.audioContext.currentTime);
}
});
// Update UI
const bpmInput = document.getElementById('bpmValue');
if (bpmInput) {
bpmInput.value = this.bpm;
}
}
updateMeters() {
if (!this.isPlaying) return;
this.currentTime = this.audioContext.currentTime - this.startTime;
updateTimeDisplay(this.currentTime, this.totalTime);
// Simulate meter levels (replace with actual audio analysis)
this.tracks.forEach((track, trackId) => {
const level = Math.random() * 100;
updateMeter(trackId, level);
});
// Master meter
const masterLevel = Math.random() * 100;
updateMeter('master', masterLevel);
requestAnimationFrame(() => this.updateMeters());
}
}
// Initialize mixer engine
const mixerEngine = new StudioMixerEngine();
window.mixerEngine = mixerEngine;
// Track loader state
let availableTracks = [];
let loadedTrackIds = new Set();
// Initialize on page load
document.addEventListener('DOMContentLoaded', async () => {
await mixerEngine.initialize();
mixerEngine.setBPM(120); // Initialize BPM
initializeMixerControls();
initializeTrackLoader();
// Don't load tracks automatically - user will load them manually
});
function initializeTrackLoader() {
// Load Track Button
document.getElementById('loadTrackBtn')?.addEventListener('click', () => {
openTrackLoader();
});
// Close Loader
document.getElementById('closeLoader')?.addEventListener('click', () => {
closeTrackLoader();
});
// Close on background click
document.getElementById('trackLoaderPanel')?.addEventListener('click', (e) => {
if (e.target.id === 'trackLoaderPanel') {
closeTrackLoader();
}
});
// Search functionality
document.getElementById('trackSearch')?.addEventListener('input', (e) => {
filterTracks(e.target.value);
});
// Load available tracks
loadAvailableTracks();
}
async function loadAvailableTracks() {
try {
const response = await fetch('/api/get_user_tracks.php?type=all');
const data = await response.json();
if (data.success && data.tracks) {
availableTracks = data.tracks;
renderTrackList(availableTracks);
} else {
document.getElementById('trackListContainer').innerHTML = `
<div class="empty-tracks">
<i class="fas fa-music"></i>
<p>Aucune piste disponible</p>
</div>
`;
}
} catch (error) {
console.error('Error loading tracks:', error);
document.getElementById('trackListContainer').innerHTML = `
<div class="empty-tracks">
<i class="fas fa-exclamation-triangle"></i>
<p>Erreur lors du chargement</p>
</div>
`;
}
}
function renderTrackList(tracks) {
const container = document.getElementById('trackListContainer');
if (!container) return;
if (tracks.length === 0) {
container.innerHTML = `
<div class="empty-tracks">
<i class="fas fa-music"></i>
<p>Aucune piste trouvée</p>
</div>
`;
return;
}
container.innerHTML = tracks.map(track => {
const isLoaded = loadedTrackIds.has(track.id);
return `
<div class="track-item-mixer ${isLoaded ? 'loaded' : ''}" data-track-id="${track.id}">
<div class="track-item-info">
<div class="track-item-title" title="${escapeHtml(track.title)}">${escapeHtml(track.title)}</div>
<div class="track-item-meta">
<span>${escapeHtml(track.music_type || 'Track')}</span>
${track.duration ? `<span>· ${formatTime(track.duration)}</span>` : ''}
</div>
</div>
<button class="track-item-action"
data-track-id="${track.id}"
${isLoaded ? 'disabled' : ''}
onclick="loadTrackToMixer(${track.id}, '${escapeHtml(track.audio_url)}', '${escapeHtml(track.title)}')">
${isLoaded ? '<i class="fas fa-check"></i> Chargée' : '<i class="fas fa-plus"></i> Charger'}
</button>
</div>
`;
}).join('');
}
function filterTracks(query) {
const filtered = availableTracks.filter(track => {
const searchTerm = query.toLowerCase();
return (track.title || '').toLowerCase().includes(searchTerm) ||
(track.music_type || '').toLowerCase().includes(searchTerm);
});
renderTrackList(filtered);
}
function openTrackLoader() {
document.getElementById('trackLoaderPanel').classList.add('active');
document.body.style.overflow = 'hidden';
loadAvailableTracks();
}
function closeTrackLoader() {
document.getElementById('trackLoaderPanel').classList.remove('active');
document.body.style.overflow = '';
document.getElementById('trackSearch').value = '';
}
function loadTrackToMixer(trackId, audioUrl, trackTitle) {
// Check if already loaded
if (loadedTrackIds.has(trackId)) {
return;
}
// Add track to mixer
addTrackChannel(trackId, audioUrl, trackTitle);
// Load track in audio engine
mixerEngine.loadTrack(trackId, audioUrl).then(track => {
if (track) {
loadedTrackIds.add(trackId);
renderTrackList(availableTracks);
// Close the loader panel after loading
closeTrackLoader();
}
});
}
function addTrackChannel(trackId, audioUrl, trackTitle) {
const channelsContainer = document.getElementById('mixerChannels');
if (!channelsContainer) return;
// Hide empty message
const emptyMixer = document.getElementById('emptyMixer');
if (emptyMixer) {
emptyMixer.style.display = 'none';
}
// Get next channel number
const existingChannels = channelsContainer.querySelectorAll('.mixer-channel');
const channelNumber = existingChannels.length + 1;
const channelHTML = `
<div class="mixer-channel" data-track-id="${trackId}" data-audio-url="${escapeHtml(audioUrl)}">
<div class="channel-header">
<div class="channel-number">${channelNumber}</div>
<div class="channel-name" title="${escapeHtml(trackTitle)}">
${escapeHtml(trackTitle.length > 12 ? trackTitle.substring(0, 12) + '...' : trackTitle)}
</div>
<button class="channel-btn remove-btn" data-track="${trackId}" title="Retirer" onclick="removeTrackFromMixer(${trackId})">
<i class="fas fa-times"></i>
</button>
</div>
<!-- Track Controls -->
<div class="track-controls">
<button class="track-play-btn" data-track="${trackId}" title="Play">
<i class="fas fa-play"></i>
</button>
<button class="track-stop-btn" data-track="${trackId}" title="Stop">
<i class="fas fa-stop"></i>
</button>
<div class="track-bpm-control">
<label>BPM</label>
<div class="track-bpm-input-group">
<button class="track-bpm-btn" data-track="${trackId}" data-action="down">-</button>
<input type="number" class="track-bpm-input" data-track="${trackId}" value="120" min="60" max="200">
<button class="track-bpm-btn" data-track="${trackId}" data-action="up">+</button>
</div>
</div>
</div>
<div class="channel-eq">
<div class="eq-band" data-band="high">
<label>H</label>
<div class="eq-knob-wrapper">
<div class="eq-knob" data-track="${trackId}" data-band="high">
<div class="eq-value">0</div>
</div>
</div>
</div>
<div class="eq-band" data-band="mid">
<label>M</label>
<div class="eq-knob-wrapper">
<div class="eq-knob" data-track="${trackId}" data-band="mid">
<div class="eq-value">0</div>
</div>
</div>
</div>
<div class="eq-band" data-band="low">
<label>L</label>
<div class="eq-knob-wrapper">
<div class="eq-knob" data-track="${trackId}" data-band="low">
<div class="eq-value">0</div>
</div>
</div>
</div>
</div>
<div class="channel-fader">
<div class="fader-track">
<div class="fader-scale">
<span class="scale-mark">100</span>
<span class="scale-mark">75</span>
<span class="scale-mark">50</span>
<span class="scale-mark">25</span>
<span class="scale-mark">0</span>
</div>
<div class="fader-knob" data-track="${trackId}" style="top: 0%;">
<div class="fader-grip"></div>
</div>
</div>
<div class="fader-value" data-track="${trackId}">100</div>
</div>
<div class="channel-pan">
<div class="pan-track">
<div class="pan-mark pan-left">L</div>
<div class="pan-mark pan-center">C</div>
<div class="pan-mark pan-right">R</div>
<div class="pan-knob" data-track="${trackId}" style="left: 50%;">
<div class="pan-indicator"></div>
</div>
</div>
<div class="pan-value" data-track="${trackId}">Center</div>
</div>
<div class="channel-meter">
<div class="meter-bar">
<div class="meter-level" data-track="${trackId}"></div>
<div class="meter-peak" data-track="${trackId}"></div>
</div>
</div>
</div>
`;
channelsContainer.insertAdjacentHTML('beforeend', channelHTML);
// Initialize controls for new channel
const newChannel = channelsContainer.querySelector(`[data-track-id="${trackId}"]`);
if (!newChannel) return;
const fader = newChannel.querySelector('.fader-knob');
const panKnob = newChannel.querySelector('.pan-knob');
const eqKnobs = newChannel.querySelectorAll('.eq-knob');
if (fader) makeFaderDraggable(fader);
if (panKnob) makePanDraggable(panKnob);
eqKnobs.forEach(knob => makeEQDraggable(knob));
// Initialize track-specific controls
const playBtn = newChannel.querySelector('.track-play-btn');
const stopBtn = newChannel.querySelector('.track-stop-btn');
const bpmInput = newChannel.querySelector('.track-bpm-input');
const bpmBtns = newChannel.querySelectorAll('.track-bpm-btn');
if (playBtn) {
playBtn.addEventListener('click', async function() {
const trackId = this.getAttribute('data-track');
await mixerEngine.playTrack(trackId);
this.classList.add('active');
stopBtn?.classList.remove('active');
});
}
if (stopBtn) {
stopBtn.addEventListener('click', function() {
const trackId = this.getAttribute('data-track');
mixerEngine.stopTrack(trackId);
this.classList.add('active');
playBtn?.classList.remove('active');
});
}
if (bpmInput) {
bpmInput.addEventListener('change', function() {
const trackId = this.getAttribute('data-track');
const bpm = parseInt(this.value) || 120;
const clampedBPM = Math.max(60, Math.min(200, bpm));
this.value = clampedBPM;
mixerEngine.setTrackBPM(trackId, clampedBPM);
});
bpmInput.addEventListener('input', function() {
const trackId = this.getAttribute('data-track');
const bpm = parseInt(this.value) || 120;
const clampedBPM = Math.max(60, Math.min(200, bpm));
if (clampedBPM !== bpm) {
this.value = clampedBPM;
}
mixerEngine.setTrackBPM(trackId, clampedBPM);
});
}
bpmBtns.forEach(btn => {
btn.addEventListener('click', function() {
const trackId = this.getAttribute('data-track');
const action = this.getAttribute('data-action');
const input = newChannel.querySelector(`.track-bpm-input[data-track="${trackId}"]`);
if (input) {
let currentBPM = parseInt(input.value) || 120;
if (action === 'up') {
currentBPM = Math.min(200, currentBPM + 1);
} else {
currentBPM = Math.max(60, currentBPM - 1);
}
input.value = currentBPM;
mixerEngine.setTrackBPM(trackId, currentBPM);
}
});
});
}
function removeTrackFromMixer(trackId) {
if (confirm('Retirer cette piste du mixer?')) {
const channel = document.querySelector(`.mixer-channel[data-track-id="${trackId}"]`);
if (channel) {
channel.remove();
}
// Stop track in engine
mixerEngine.stopTrack(trackId);
mixerEngine.tracks.delete(trackId);
loadedTrackIds.delete(trackId);
// Update track list
renderTrackList(availableTracks);
// Show empty message if no tracks left
const channelsContainer = document.getElementById('mixerChannels');
const existingChannels = channelsContainer.querySelectorAll('.mixer-channel');
if (existingChannels.length === 0) {
const emptyMixer = document.getElementById('emptyMixer');
if (emptyMixer) {
emptyMixer.style.display = 'block';
}
}
}
}
function escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
function loadAllTracks() {
document.querySelectorAll('.mixer-channel').forEach(channel => {
const trackId = channel.getAttribute('data-track-id');
const audioUrl = channel.getAttribute('data-audio-url');
if (trackId && audioUrl) {
mixerEngine.loadTrack(trackId, audioUrl).then(track => {
if (track) {
loadedTrackIds.add(trackId);
}
});
}
});
}
function initializeMixerControls() {
// Transport controls
document.getElementById('mixerPlay')?.addEventListener('click', async () => {
if (mixerEngine.tracks.size === 0) {
alert('Veuillez charger au moins une piste avant de jouer');
return;
}
await mixerEngine.play();
document.getElementById('mixerPlay').classList.add('active');
document.getElementById('mixerPause').classList.remove('active');
});
document.getElementById('mixerPause')?.addEventListener('click', () => {
mixerEngine.pause();
document.getElementById('mixerPlay').classList.remove('active');
document.getElementById('mixerPause').classList.add('active');
});
document.getElementById('mixerStop')?.addEventListener('click', () => {
mixerEngine.stop();
document.getElementById('mixerPlay').classList.remove('active');
document.getElementById('mixerPause').classList.remove('active');
});
document.getElementById('mixerLoop')?.addEventListener('click', function() {
mixerEngine.isLooping = !mixerEngine.isLooping;
this.classList.toggle('active');
});
// BPM Controls
const bpmInput = document.getElementById('bpmValue');
const bpmUp = document.getElementById('bpmUp');
const bpmDown = document.getElementById('bpmDown');
if (bpmInput) {
bpmInput.addEventListener('change', function() {
const bpm = parseInt(this.value) || 120;
mixerEngine.setBPM(bpm);
});
bpmInput.addEventListener('input', function() {
const bpm = parseInt(this.value) || 120;
mixerEngine.setBPM(bpm);
});
}
if (bpmUp) {
bpmUp.addEventListener('click', () => {
const currentBPM = parseInt(bpmInput.value) || 120;
mixerEngine.setBPM(currentBPM + 1);
});
}
if (bpmDown) {
bpmDown.addEventListener('click', () => {
const currentBPM = parseInt(bpmInput.value) || 120;
mixerEngine.setBPM(currentBPM - 1);
});
}
// Fader controls
document.querySelectorAll('.fader-knob').forEach(fader => {
makeFaderDraggable(fader);
});
// Pan controls
document.querySelectorAll('.pan-knob').forEach(knob => {
makePanDraggable(knob);
});
// EQ controls
document.querySelectorAll('.eq-knob').forEach(knob => {
makeEQDraggable(knob);
});
// Mute/Solo buttons (for initial tracks)
document.querySelectorAll('.mute-btn').forEach(btn => {
btn.addEventListener('click', function() {
const trackId = this.getAttribute('data-track');
mixerEngine.muteTrack(trackId);
this.classList.toggle('active');
});
});
document.querySelectorAll('.solo-btn').forEach(btn => {
btn.addEventListener('click', function() {
const trackId = this.getAttribute('data-track');
mixerEngine.soloTrack(trackId);
this.classList.toggle('active');
});
});
// Master controls
document.getElementById('masterMute')?.addEventListener('click', function() {
const isMuted = this.classList.toggle('active');
const icon = this.querySelector('i');
if (isMuted) {
// Mute master
mixerEngine.masterGain.gain.setValueAtTime(0, mixerEngine.audioContext.currentTime);
if (icon) {
icon.className = 'fas fa-volume-mute';
}
} else {
// Unmute master - restore to current volume
const currentVolume = parseInt(document.getElementById('masterValue')?.textContent || '100');
mixerEngine.setMasterVolume(currentVolume);
if (icon) {
icon.className = 'fas fa-volume-up';
}
}
});
}
function makeFaderDraggable(fader) {
let isDragging = false;
let startY = 0;
let startTop = 0;
const faderTrack = fader.closest('.fader-track');
const isMaster = fader.classList.contains('master-knob');
fader.addEventListener('mousedown', function(e) {
isDragging = true;
startY = e.clientY;
const currentTop = parseFloat(getComputedStyle(fader).top) || 0;
startTop = isNaN(currentTop) ? 0 : currentTop;
fader.classList.add('dragging');
e.preventDefault();
e.stopPropagation();
});
const handleMove = (e) => {
if (!isDragging || !faderTrack) return;
const deltaY = startY - e.clientY; // Up = higher volume (move knob up)
const trackHeight = faderTrack.offsetHeight;
const knobHeight = fader.offsetHeight;
const maxTop = trackHeight - knobHeight;
// Calculate new top position (0 = top/100%, maxTop = bottom/0%)
let newTop = Math.max(0, Math.min(maxTop, startTop - deltaY));
fader.style.top = newTop + 'px';
// Convert to 0-100 scale (top=0 means volume=100%, top=max means volume=0%)
const percentage = 1 - (newTop / maxTop);
const volumeValue = Math.max(0, Math.min(100, Math.round(percentage * 100)));
const valueElement = isMaster
? document.getElementById('masterValue')
: fader.closest('.channel-fader').querySelector('.fader-value');
if (valueElement) {
valueElement.textContent = volumeValue;
}
const trackId = fader.getAttribute('data-track');
if (isMaster) {
mixerEngine.setMasterVolume(volumeValue);
} else if (trackId) {
mixerEngine.setTrackVolume(trackId, volumeValue);
}
};
const handleUp = () => {
if (isDragging) {
isDragging = false;
fader.classList.remove('dragging');
}
};
document.addEventListener('mousemove', handleMove);
document.addEventListener('mouseup', handleUp);
document.addEventListener('mouseleave', handleUp);
// Touch support
fader.addEventListener('touchstart', function(e) {
isDragging = true;
startY = e.touches[0].clientY;
const currentTop = parseFloat(getComputedStyle(fader).top) || 0;
startTop = isNaN(currentTop) ? 0 : currentTop;
fader.classList.add('dragging');
e.preventDefault();
});
document.addEventListener('touchmove', (e) => {
if (!isDragging || !faderTrack) return;
const touch = e.touches[0];
const deltaY = startY - touch.clientY;
const trackHeight = faderTrack.offsetHeight;
const knobHeight = fader.offsetHeight;
const maxTop = trackHeight - knobHeight;
let newTop = Math.max(0, Math.min(maxTop, startTop - deltaY));
fader.style.top = newTop + 'px';
const percentage = 1 - (newTop / maxTop);
const volumeValue = Math.max(0, Math.min(100, Math.round(percentage * 100)));
const valueElement = isMaster
? document.getElementById('masterValue')
: fader.closest('.channel-fader').querySelector('.fader-value');
if (valueElement) {
valueElement.textContent = volumeValue;
}
const trackId = fader.getAttribute('data-track');
if (isMaster) {
mixerEngine.setMasterVolume(volumeValue);
} else if (trackId) {
mixerEngine.setTrackVolume(trackId, volumeValue);
}
e.preventDefault();
});
document.addEventListener('touchend', handleUp);
}
function makePanDraggable(knob) {
let isDragging = false;
let startX = 0;
let startLeft = 0;
const panTrack = knob.closest('.pan-track');
knob.addEventListener('mousedown', function(e) {
isDragging = true;
startX = e.clientX;
startLeft = parseFloat(getComputedStyle(knob).left) || 50;
e.preventDefault();
});
const handleMove = (e) => {
if (!isDragging) return;
const deltaX = e.clientX - startX;
const trackWidth = panTrack.offsetWidth;
const knobWidth = knob.offsetWidth;
const maxLeft = trackWidth - knobWidth;
let newLeft = Math.max(0, Math.min(maxLeft, startLeft + (deltaX / trackWidth * 100)));
knob.style.left = newLeft + '%';
const percentage = (newLeft / maxLeft) * 2 - 1;
const panValue = Math.max(-1, Math.min(1, percentage));
const valueElement = knob.closest('.channel-pan').querySelector('.pan-value');
if (valueElement) {
if (Math.abs(panValue) < 0.05) {
valueElement.textContent = 'Center';
} else if (panValue > 0) {
valueElement.textContent = 'R' + Math.round(panValue * 100);
} else {
valueElement.textContent = 'L' + Math.round(Math.abs(panValue) * 100);
}
}
const trackId = knob.getAttribute('data-track');
if (trackId) {
mixerEngine.setTrackPan(trackId, panValue);
}
};
document.addEventListener('mousemove', handleMove);
document.addEventListener('mouseup', () => { isDragging = false; });
// Touch support
knob.addEventListener('touchstart', function(e) {
isDragging = true;
startX = e.touches[0].clientX;
startLeft = parseFloat(getComputedStyle(knob).left) || 50;
e.preventDefault();
});
document.addEventListener('touchmove', (e) => {
if (!isDragging) return;
handleMove(e.touches[0]);
e.preventDefault();
});
document.addEventListener('touchend', () => { isDragging = false; });
}
function makeEQDraggable(knob) {
let isDragging = false;
let startY = 0;
let startRotation = 0;
const currentRotation = { value: 0 };
knob.addEventListener('mousedown', function(e) {
isDragging = true;
startY = e.clientY;
startRotation = currentRotation.value;
e.preventDefault();
});
const handleMove = (e) => {
if (!isDragging) return;
const deltaY = startY - e.clientY;
const maxRotation = 270;
const rotation = Math.max(-maxRotation, Math.min(maxRotation, startRotation + deltaY * 2));
currentRotation.value = rotation;
knob.style.transform = `rotate(${rotation}deg)`;
const dbValue = (rotation / maxRotation) * 15;
const valueElement = knob.querySelector('.eq-value');
if (valueElement) {
valueElement.textContent = dbValue > 0 ? '+' + dbValue.toFixed(0) : dbValue.toFixed(0);
}
const trackId = knob.getAttribute('data-track');
const band = knob.getAttribute('data-band');
if (trackId && band) {
mixerEngine.setTrackEQ(trackId, band, dbValue);
}
};
document.addEventListener('mousemove', handleMove);
document.addEventListener('mouseup', () => { isDragging = false; });
// Touch support
knob.addEventListener('touchstart', function(e) {
isDragging = true;
startY = e.touches[0].clientY;
startRotation = currentRotation.value;
e.preventDefault();
});
document.addEventListener('touchmove', (e) => {
if (!isDragging) return;
handleMove(e.touches[0]);
e.preventDefault();
});
document.addEventListener('touchend', () => { isDragging = false; });
}
function updateTimeDisplay(current, total) {
const currentEl = document.getElementById('currentTime');
const totalEl = document.getElementById('totalTime');
if (currentEl) currentEl.textContent = formatTime(current);
if (totalEl) totalEl.textContent = formatTime(total);
}
function formatTime(seconds) {
const mins = Math.floor(seconds / 60);
const secs = Math.floor(seconds % 60);
return `${String(mins).padStart(2, '0')}:${String(secs).padStart(2, '0')}`;
}
function updateMeter(trackId, level) {
let meter, peak;
if (trackId === 'master') {
meter = document.getElementById('masterLevel');
peak = document.getElementById('masterPeak');
} else {
meter = document.querySelector(`.meter-level[data-track="${trackId}"]`);
peak = document.querySelector(`.meter-peak[data-track="${trackId}"]`);
}
if (meter) {
meter.style.height = Math.min(100, Math.max(0, level)) + '%';
}
if (peak && level > 90) {
peak.style.height = level + '%';
peak.classList.add('active');
setTimeout(() => peak.classList.remove('active'), 100);
}
}
</script>
<?php include 'includes/footer.php'; ?>