eq.html 6.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132
  1. <!DOCTYPE html><html><head>
  2. <meta charset='UTF-8'>
  3. <meta name='viewport' content='width=device-width,initial-scale=1'>
  4. <title>EQ - AirPlay Receiver</title>
  5. <style>
  6. *{box-sizing:border-box;margin:0;padding:0}
  7. body{font-family:-apple-system,system-ui,sans-serif;background:linear-gradient(135deg,#1a1a2e 0%,#16213e 100%);min-height:100vh;padding:20px;color:#fff}
  8. .wrap{max-width:720px;margin:0 auto}
  9. .header{text-align:center;padding:20px 0}
  10. .header h1{font-size:24px;font-weight:600;margin-bottom:4px}
  11. .header p{color:#888;font-size:13px}
  12. .nav{text-align:center;margin-bottom:16px}
  13. .nav a{color:#e94560;text-decoration:none;font-size:13px}
  14. .card{background:rgba(255,255,255,0.05);backdrop-filter:blur(10px);border-radius:16px;padding:20px;margin-bottom:16px;border:1px solid rgba(255,255,255,0.1)}
  15. .card h2{font-size:16px;font-weight:600;margin-bottom:16px;color:#e94560}
  16. .eq-container{display:flex;gap:4px;justify-content:center;align-items:flex-end;height:280px;padding:10px 0}
  17. .eq-band{display:flex;flex-direction:column;align-items:center;flex:1;min-width:0}
  18. .eq-band .val{font-size:11px;color:#e94560;font-weight:600;margin-bottom:4px;min-height:16px;text-align:center}
  19. .eq-band input[type=range]{-webkit-appearance:none;appearance:none;writing-mode:vertical-lr;direction:rtl;width:28px;height:200px;background:transparent;cursor:pointer}
  20. .eq-band input[type=range]::-webkit-slider-runnable-track{width:4px;background:rgba(255,255,255,0.15);border-radius:2px}
  21. .eq-band input[type=range]::-webkit-slider-thumb{-webkit-appearance:none;width:16px;height:16px;border-radius:50%;background:#e94560;margin-left:-6px;cursor:pointer}
  22. .eq-band input[type=range]::-moz-range-track{width:4px;background:rgba(255,255,255,0.15);border-radius:2px}
  23. .eq-band input[type=range]::-moz-range-thumb{width:14px;height:14px;border-radius:50%;background:#e94560;border:none;cursor:pointer}
  24. .eq-band .freq{font-size:10px;color:#888;margin-top:6px;text-align:center;white-space:nowrap}
  25. .eq-scale{display:flex;flex-direction:column;justify-content:space-between;margin:48px 6px 25px 0;padding:0}
  26. .eq-scale span{font-size:10px;color:#555;text-align:right;min-width:28px}
  27. .eq-actions{display:flex;gap:10px;margin-top:16px;flex-wrap:wrap}
  28. .btn{display:inline-block;padding:10px 16px;border:none;border-radius:8px;font-size:13px;font-weight:500;cursor:pointer;transition:all 0.2s}
  29. .btn-primary{background:#e94560;color:#fff}
  30. .btn-primary:hover{background:#d63850}
  31. .btn-secondary{background:rgba(255,255,255,0.1);color:#fff}
  32. .btn-secondary:hover{background:rgba(255,255,255,0.2)}
  33. .btn:disabled{opacity:0.5;cursor:not-allowed}
  34. .presets{margin-top:12px}
  35. .presets select{background:rgba(0,0,0,0.3);border:1px solid rgba(255,255,255,0.1);border-radius:8px;color:#fff;padding:8px 12px;font-size:13px;cursor:pointer;min-width:160px}
  36. .presets select option{background:#16213e}
  37. .msg{padding:8px;border-radius:6px;margin-top:10px;font-size:13px}
  38. .msg.ok{background:rgba(27,67,50,0.5)}
  39. .msg.err{background:rgba(92,26,26,0.5)}
  40. .msg.info{background:rgba(15,52,96,0.5)}
  41. </style></head><body>
  42. <div class='wrap'>
  43. <div class='header'><h1>Equalizer</h1><p>15-Band Parametric EQ</p></div>
  44. <div class='nav'><a href='/'>&#8592; Back to Settings</a></div>
  45. <div class='card'><h2>EQ Bands</h2>
  46. <div style='display:flex'>
  47. <div class='eq-scale'><span>+15</span><span>+7</span><span>0</span><span>-7</span><span>-15</span></div>
  48. <div class='eq-container' id='eq-sliders'></div>
  49. </div>
  50. <div class='eq-actions'>
  51. <button class='btn btn-primary' id='apply-btn' onclick='applyEQ()'>Apply</button>
  52. <button class='btn btn-secondary' onclick='flatEQ()'>Flat (Reset)</button>
  53. <div class='presets'>
  54. <select id='preset-sel' onchange='loadPreset(this.value)'>
  55. <option value=''>-- Presets --</option>
  56. <option value='bass_boost'>Bass Boost</option>
  57. <option value='treble_boost'>Treble Boost</option>
  58. <option value='v_shape'>V-Shape</option>
  59. <option value='vocal'>Vocal Presence</option>
  60. <option value='loudness'>Loudness</option>
  61. </select></div>
  62. </div>
  63. <div id='eq-msg'></div>
  64. </div></div>
  65. <script>
  66. var BANDS=15;
  67. var FREQS=[20,31.5,50,80,125,200,315,500,800,'1.25k','2k','3.15k','5k','8k','16k'];
  68. var gains=new Array(BANDS).fill(0);
  69. var dirty=false;
  70. function fmtFreq(f){return typeof f==='number'?f+'':f;}
  71. function fmtGain(v){return (v>0?'+':'')+v;}
  72. function buildSliders(){
  73. var c=document.getElementById('eq-sliders');c.innerHTML='';
  74. for(var i=0;i<BANDS;i++){
  75. var d=document.createElement('div');d.className='eq-band';
  76. var v=document.createElement('div');v.className='val';v.id='val-'+i;v.textContent=fmtGain(gains[i]);
  77. var s=document.createElement('input');s.type='range';s.min=-15;s.max=15;s.step=1;s.value=gains[i];s.dataset.band=i;
  78. s.addEventListener('input',function(e){
  79. var b=parseInt(e.target.dataset.band);gains[b]=parseFloat(e.target.value);
  80. document.getElementById('val-'+b).textContent=fmtGain(gains[b]);
  81. dirty=true;document.getElementById('apply-btn').disabled=false;
  82. });
  83. var f=document.createElement('div');f.className='freq';f.textContent=fmtFreq(FREQS[i]);
  84. d.appendChild(v);d.appendChild(s);d.appendChild(f);c.appendChild(d);
  85. }
  86. }
  87. function updateSliders(){
  88. for(var i=0;i<BANDS;i++){
  89. var s=document.querySelector('[data-band="'+i+'"]');
  90. if(s){s.value=gains[i];}
  91. var v=document.getElementById('val-'+i);
  92. if(v){v.textContent=fmtGain(gains[i]);}
  93. }
  94. dirty=false;document.getElementById('apply-btn').disabled=true;
  95. }
  96. function msg(txt,type){
  97. var e=document.getElementById('eq-msg');
  98. e.innerHTML='<div class="msg '+type+'">'+txt+'</div>';
  99. setTimeout(function(){e.innerHTML='';},3000);
  100. }
  101. async function loadEQ(){
  102. try{var r=await fetch('/api/eq');var d=await r.json();
  103. if(d.success&&d.gains){gains=d.gains;updateSliders();}}
  104. catch(e){msg('Failed to load EQ','err');}
  105. }
  106. async function applyEQ(){
  107. document.getElementById('apply-btn').disabled=true;
  108. try{var r=await fetch('/api/eq',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({gains:gains})});
  109. var d=await r.json();
  110. if(d.success){msg('EQ applied','ok');dirty=false;}
  111. else{msg(d.error||'Failed','err');document.getElementById('apply-btn').disabled=false;}}
  112. catch(e){msg('Error applying EQ','err');document.getElementById('apply-btn').disabled=false;}
  113. }
  114. function flatEQ(){
  115. gains=new Array(BANDS).fill(0);updateSliders();
  116. dirty=true;document.getElementById('apply-btn').disabled=false;
  117. document.getElementById('preset-sel').value='';
  118. }
  119. var PRESETS={
  120. bass_boost:[8,6,5,4,2,0,0,0,0,0,0,0,0,0,0],
  121. treble_boost:[0,0,0,0,0,0,0,0,0,1,3,4,6,7,7],
  122. v_shape:[5,4,2,1,0,-1,-2,-3,-2,0,1,3,4,5,5],
  123. vocal:[0,0,0,0,0,1,3,4,4,3,1,0,0,0,0],
  124. loudness:[7,5,3,0,-1,-2,-3,-2,0,1,3,4,6,7,5]
  125. };
  126. function loadPreset(name){
  127. if(!name||!PRESETS[name])return;
  128. gains=PRESETS[name].slice();updateSliders();
  129. dirty=true;document.getElementById('apply-btn').disabled=false;
  130. }
  131. window.onload=function(){buildSliders();loadEQ();};
  132. </script></body></html>