Files
radio-with-a-view/src/classes/VolumeMeterVisualizer.ts
Alexander Zinn 1fd455d29b Add initial project structure with Docker setup, SOLID principles, and video player implementation
- Created .gitignore to exclude logs and build artifacts.
- Added Dockerfile and docker-compose.yml for containerized deployment.
- Implemented a video player following SOLID principles with classes for video source, audio control, and volume meter.
- Introduced interfaces for audio, video source, and volume meter to ensure adherence to Interface Segregation Principle.
- Developed main entry point and HTML structure for the video player application.
- Included TypeScript configuration and package.json for project dependencies.
2025-12-17 22:33:35 -05:00

118 lines
3.6 KiB
TypeScript

import type { IVolumeMeter } from '../interfaces/IVolumeMeter';
/**
* VolumeMeterVisualizer class
* Follows Single Responsibility Principle (SRP) - only responsible for visualizing volume data
*/
export class VolumeMeterVisualizer {
private container: HTMLElement;
private volumeMeter: IVolumeMeter;
private bars: HTMLElement[] = [];
private barCount: number;
private animationFrameId: number | null = null;
constructor(container: HTMLElement, volumeMeter: IVolumeMeter, barCount: number = 20) {
this.container = container;
this.volumeMeter = volumeMeter;
this.barCount = barCount;
this.createBars();
}
/**
* Creates the visual bar elements
*/
private createBars(): void {
this.container.innerHTML = '';
this.bars = [];
for (let i = 0; i < this.barCount; i++) {
const bar = document.createElement('div');
bar.className = 'volume-bar';
this.container.appendChild(bar);
this.bars.push(bar);
}
}
/**
* Starts the visualization animation
*/
start(): void {
if (this.animationFrameId !== null) {
return;
}
const update = () => {
if (!this.volumeMeter.isActive()) {
// Keep running even if not active, but show minimal activity so bars are visible
this.bars.forEach(bar => {
bar.style.height = '10%';
bar.style.opacity = '0.4';
});
this.animationFrameId = requestAnimationFrame(update);
return;
}
// Get volume level
const volume = this.volumeMeter.getVolumeLevel();
// Update each bar based on volume and frequency
// Use the correct size based on analyser's frequencyBinCount (fftSize / 2)
const frequencyArray = new Uint8Array(256);
this.volumeMeter.getFrequencyData(frequencyArray);
// Check if we're getting any actual audio data (CORS check)
const hasAudioData = frequencyArray.some(value => value > 0) || volume > 0.001;
if (!hasAudioData) {
// If no audio data (likely CORS issue), show a subtle pulsing animation
const time = Date.now() / 1000;
this.bars.forEach((bar, index) => {
const pulse = Math.sin(time * 2 + index * 0.3) * 0.3 + 0.5;
bar.style.height = `${10 + pulse * 15}%`;
bar.style.opacity = `${0.3 + pulse * 0.2}`;
});
this.animationFrameId = requestAnimationFrame(update);
return;
}
this.bars.forEach((bar, index) => {
// Map bar index to frequency bin with some overlap for smoother visualization
const frequencyIndex = Math.floor((index / this.barCount) * frequencyArray.length);
const frequencyValue = frequencyArray[frequencyIndex] || 0;
// Calculate height with better scaling - ensure minimum visibility
const normalizedFreq = frequencyValue / 255;
// Scale more aggressively for better visibility
const height = Math.max(normalizedFreq * 100, volume * 30, 8); // At least 8% when active
bar.style.height = `${Math.min(height, 100)}%`;
// Add color and opacity based on volume level - more dynamic
const intensity = Math.max(normalizedFreq, volume);
bar.style.opacity = `${0.5 + intensity * 0.5}`;
});
this.animationFrameId = requestAnimationFrame(update);
};
update();
}
/**
* Stops the visualization animation
*/
stop(): void {
if (this.animationFrameId !== null) {
cancelAnimationFrame(this.animationFrameId);
this.animationFrameId = null;
}
// Reset all bars
this.bars.forEach(bar => {
bar.style.height = '0%';
bar.style.opacity = '0.3';
});
}
}