forked from computertech/techdj
Add listener spectrum visualizer
This commit is contained in:
@@ -413,6 +413,8 @@
|
||||
<div class="listener-content">
|
||||
<div class="now-playing" id="listener-now-playing">Waiting for stream...</div>
|
||||
|
||||
<canvas id="viz-listener" width="400" height="100"></canvas>
|
||||
|
||||
<!-- Enable Audio Button (shown when autoplay is blocked) -->
|
||||
<button class="enable-audio-btn" id="enable-audio-btn" style="display: none;"
|
||||
onclick="enableListenerAudio()">
|
||||
|
||||
87
script.js
87
script.js
@@ -1545,7 +1545,67 @@ let isBroadcasting = false;
|
||||
let autoStartStream = false;
|
||||
let listenerAudioContext = null;
|
||||
let listenerGainNode = null;
|
||||
let listenerAnalyserNode = null;
|
||||
let listenerMediaElementSourceNode = null;
|
||||
let listenerVuMeterRunning = false;
|
||||
let listenerChunksReceived = 0;
|
||||
|
||||
function startListenerVUMeter() {
|
||||
if (listenerVuMeterRunning) return;
|
||||
listenerVuMeterRunning = true;
|
||||
|
||||
const draw = () => {
|
||||
if (!listenerVuMeterRunning) return;
|
||||
requestAnimationFrame(draw);
|
||||
|
||||
const canvas = document.getElementById('viz-listener');
|
||||
if (!canvas || !listenerAnalyserNode) return;
|
||||
|
||||
const ctx = canvas.getContext('2d');
|
||||
if (!ctx) return;
|
||||
|
||||
// Keep canvas sized correctly for DPI
|
||||
const dpr = window.devicePixelRatio || 1;
|
||||
const rect = canvas.getBoundingClientRect();
|
||||
const targetW = Math.max(1, Math.floor(rect.width * dpr));
|
||||
const targetH = Math.max(1, Math.floor(rect.height * dpr));
|
||||
if (canvas.width !== targetW || canvas.height !== targetH) {
|
||||
canvas.width = targetW;
|
||||
canvas.height = targetH;
|
||||
}
|
||||
|
||||
const analyser = listenerAnalyserNode;
|
||||
const bufferLength = analyser.frequencyBinCount;
|
||||
const dataArray = new Uint8Array(bufferLength);
|
||||
analyser.getByteFrequencyData(dataArray);
|
||||
|
||||
const width = canvas.width;
|
||||
const height = canvas.height;
|
||||
const barCount = 32;
|
||||
const barWidth = width / barCount;
|
||||
|
||||
ctx.fillStyle = '#0a0a12';
|
||||
ctx.fillRect(0, 0, width, height);
|
||||
|
||||
// Listener uses the magenta hue (matches Deck B styling)
|
||||
const hue = 280;
|
||||
for (let i = 0; i < barCount; i++) {
|
||||
const freqIndex = Math.floor(Math.pow(i / barCount, 1.5) * bufferLength);
|
||||
const value = (dataArray[freqIndex] || 0) / 255;
|
||||
const barHeight = value * height;
|
||||
|
||||
const lightness = 30 + (value * 50);
|
||||
const gradient = ctx.createLinearGradient(0, height, 0, height - barHeight);
|
||||
gradient.addColorStop(0, `hsl(${hue}, 100%, ${lightness}%)`);
|
||||
gradient.addColorStop(1, `hsl(${hue}, 100%, ${Math.min(lightness + 20, 80)}%)`);
|
||||
|
||||
ctx.fillStyle = gradient;
|
||||
ctx.fillRect(i * barWidth, height - barHeight, barWidth - 2, barHeight);
|
||||
}
|
||||
};
|
||||
|
||||
draw();
|
||||
}
|
||||
let currentStreamMimeType = null;
|
||||
|
||||
function getMp3FallbackUrl() {
|
||||
@@ -2351,17 +2411,36 @@ async function enableListenerAudio() {
|
||||
}
|
||||
|
||||
// 3. Bridge Audio Element to AudioContext if not already connected
|
||||
if (window.listenerAudio && !window.listenerAudio._connectedToContext) {
|
||||
if (window.listenerAudio) {
|
||||
try {
|
||||
const sourceNode = listenerAudioContext.createMediaElementSource(window.listenerAudio);
|
||||
if (!listenerGainNode) {
|
||||
listenerGainNode = listenerAudioContext.createGain();
|
||||
listenerGainNode.gain.value = 0.8;
|
||||
listenerGainNode.connect(listenerAudioContext.destination);
|
||||
}
|
||||
sourceNode.connect(listenerGainNode);
|
||||
|
||||
if (!listenerAnalyserNode) {
|
||||
listenerAnalyserNode = listenerAudioContext.createAnalyser();
|
||||
listenerAnalyserNode.fftSize = 256;
|
||||
}
|
||||
|
||||
if (!listenerMediaElementSourceNode) {
|
||||
listenerMediaElementSourceNode = listenerAudioContext.createMediaElementSource(window.listenerAudio);
|
||||
}
|
||||
|
||||
// Ensure a clean, single connection chain:
|
||||
// media element -> analyser -> gain -> destination
|
||||
try { listenerMediaElementSourceNode.disconnect(); } catch (_) { }
|
||||
try { listenerAnalyserNode.disconnect(); } catch (_) { }
|
||||
|
||||
listenerMediaElementSourceNode.connect(listenerAnalyserNode);
|
||||
listenerAnalyserNode.connect(listenerGainNode);
|
||||
|
||||
window.listenerAudio._connectedToContext = true;
|
||||
console.log('🔗 Connected audio element to AudioContext');
|
||||
console.log('🔗 Connected audio element to AudioContext (with analyser)');
|
||||
|
||||
// Start visualizer after the graph exists
|
||||
startListenerVUMeter();
|
||||
} catch (e) {
|
||||
console.warn('⚠️ Could not connect to AudioContext:', e.message);
|
||||
}
|
||||
|
||||
14
style.css
14
style.css
@@ -1380,7 +1380,8 @@ input[type=range] {
|
||||
}
|
||||
|
||||
#viz-A,
|
||||
#viz-B {
|
||||
#viz-B,
|
||||
#viz-listener {
|
||||
height: 80px !important;
|
||||
}
|
||||
|
||||
@@ -2413,6 +2414,12 @@ input[type=range] {
|
||||
backdrop-filter: blur(10px);
|
||||
}
|
||||
|
||||
#viz-listener {
|
||||
width: 100%;
|
||||
display: block;
|
||||
margin: 20px 0;
|
||||
}
|
||||
|
||||
.now-playing {
|
||||
text-align: center;
|
||||
font-family: 'Orbitron', sans-serif;
|
||||
@@ -2559,6 +2566,11 @@ input[type=range] {
|
||||
.volume-control label {
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
#viz-listener {
|
||||
height: 60px !important;
|
||||
margin: 15px 0;
|
||||
}
|
||||
}
|
||||
|
||||
/* Hide landscape prompt globally when listening-active class is present */
|
||||
|
||||
Reference in New Issue
Block a user