import { AudioPipe } from "./audio.js"; /** * AudioPipeVisualizer * * Class to handle streaming audio data to the browser's audio context * Also draws the audio waveform on a canvas if provided */ export class AudioPipeVisualizer extends AudioPipe { /** * Constructor * @param {Object} options - Options object * @param {HTMLCanvasElement} options.canvas - Canvas element to draw the waveform * @param {Number} options.lineWidth - Line width for the waveform * @param {Number} options.fftSize - FFT size for the waveform * @param {String} options.fillStyle - Fill style for the waveform * @param {String} options.strokeStyle - Stroke style for the waveform */ constructor(options = {}) { super(options); this.canvas = options.canvas || null; this.lineWidth = options.lineWidth || 1; this.fftSize = options.fftSize || 2048; this.fillStyle = options.fillStyle || "#FFFFFF"; this.strokeStyle = options.strokeStyle || "#000000"; this.waveformNoiseLevel = options.waveformNoiseLevel || 0; // Only for waveform, a visual distortion for style this.fftBuffer = new Uint8Array(this.fftSize); this.startDrawing(); } /** * Get the waveform styles for the canvas. * We let the user pass in multiple styles (colors) or widths to draw the waveform. * This allows for stacking or glowing effects, if desired. * @returns {Array} - Array of styles for the waveform */ waveformStyles() { let strokeStyle = this.strokeStyle; let lineWidth = this.lineWidth; if (!Array.isArray(strokeStyle)) { strokeStyle = [strokeStyle]; } if (!Array.isArray(lineWidth)) { lineWidth = [lineWidth]; } let numStrokes = Math.max(strokeStyle.length, lineWidth.length); let styles = []; for (let i = 0; i < numStrokes; i++) { styles.push({ strokeStyle: strokeStyle[Math.min(i, strokeStyle.length - 1)], lineWidth: lineWidth[Math.min(i, lineWidth.length - 1)] }); } return styles; } push(data, sampleRate = 44100) { let audioBuffer = super.push(data, sampleRate); audioBuffer.connect(this.analyser); return audioBuffer; } /** * Initialize the analyzer to draw the waveform on the canvas. */ initialize() { super.initialize(); // Create analyser this.analyser = this.audioContext.createAnalyser(); this.analyser.fftSize = this.fftSize; this.analyser.connect(this.audioContext.destination); } /** * Starts drawing the waveform on the canvas. * If the audio context is not initialized, will draw as if there was silence. */ startDrawing() { const context = this.canvas.getContext("2d"); const sliceWidth = this.canvas.width / this.fftSize; context.fillStyle = this.fillStyle; const draw = () => { // Update data this.analyser !== undefined && this.analyser.getByteTimeDomainData(this.fftBuffer); // Clear the canvas context.fillRect(0, 0, this.canvas.width, this.canvas.height); // Start drawing the waveform let x = 0; context.beginPath(); for (let i = 0; i < this.fftSize; i++) { let v = this.initialized ? this.fftBuffer[i] / 128.0 : 1; // Scale v's distance from 1.0 by the volume let distance = Math.abs(v - 1.0); v = 1.0 + (distance * this.volume * Math.sign(v - 1.0)); if (this.waveformNoiseLevel > 0) { v += (Math.random() - 0.5) * this.waveformNoiseLevel * 2 * Math.sin(i / this.fftSize * Math.PI) * this.volume; } const y = v * this.canvas.height / 2; if (i === 0) { context.moveTo(x, y); } else { context.lineTo(x, y); } x += sliceWidth; } // Final line to the right context.lineTo(this.canvas.width, this.canvas.height / 2); // Stroke the path using all strokes for (let style of this.waveformStyles()) { context.strokeStyle = style.strokeStyle; context.lineWidth = style.lineWidth; context.stroke(); } // Schedule next frame requestAnimationFrame(draw); }; // Start drawing requestAnimationFrame(draw); } };