Skip to content

Commit 06d6db0

Browse files
committed
feat: v1 frequency display
1 parent b95284a commit 06d6db0

2 files changed

Lines changed: 306 additions & 16 deletions

File tree

src/audio.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ export let analyser;
77
/** @type {AudioNode} */
88
let sourceNode;
99

10-
/** @type {Uint8Array} */
10+
/** @type {Uint8Array|undefined} */
1111
export let dataArray;
1212

1313
/** @type {MediaStream} */

src/draw.js

Lines changed: 305 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,144 @@
11
import { analyser, dataArray } from "./audio.js";
22
import { canvas, ctx } from "./canvas.js";
33

4+
/**
5+
* Sets up the canvas for high DPI displays with enhanced smoothing
6+
* Ensures proper scaling and sizing for optimal visual quality
7+
*/
8+
function setupHighDPICanvas() {
9+
// Get the container dimensions
10+
const container = canvas.parentElement;
11+
const containerWidth = container ? container.clientWidth : window.innerWidth;
12+
const containerHeight = container
13+
? container.clientHeight
14+
: window.innerHeight;
15+
16+
// Use device pixel ratio for better quality, but don't scale too much
17+
const dpr = Math.min(window.devicePixelRatio || 1, 2);
18+
19+
// Set the canvas CSS size to match container
20+
canvas.style.width = containerWidth + "px";
21+
canvas.style.height = containerHeight + "px";
22+
23+
// Set the canvas drawing buffer size with DPI scaling for higher resolution
24+
canvas.width = containerWidth * dpr;
25+
canvas.height = containerHeight * dpr;
26+
27+
// Scale all drawing operations by the DPI scaling factor
28+
ctx.scale(dpr, dpr);
29+
30+
// Enable image smoothing for anti-aliased lines
31+
ctx.imageSmoothingEnabled = true;
32+
ctx.imageSmoothingQuality = "high";
33+
34+
// Set line join and cap for smoother lines
35+
ctx.lineJoin = "round";
36+
ctx.lineCap = "round";
37+
}
38+
39+
// Call this function once at startup
40+
setupHighDPICanvas();
41+
42+
// Listen for resize events to maintain high DPI
43+
window.addEventListener("resize", setupHighDPICanvas);
44+
445
/** @type {number} */
5-
const defaultHeight = 1;
46+
const defaultHeight = 20; // Increased default height for better visibility
47+
48+
/**
49+
* Frequency bands configuration
50+
* Each band represents a range of frequencies in the audio spectrum
51+
* @typedef {Object} FrequencyBand
52+
* @property {string} name - Name of the frequency band
53+
* @property {string} color - Main color for the band visualization
54+
* @property {string} glowColor - Glow effect color
55+
* @property {number[]} range - Array with start and end indices in the frequency data array
56+
*/
57+
58+
/** @type {FrequencyBand[]} */
59+
const BANDS = [
60+
{
61+
name: "high",
62+
color: "#00FFFF",
63+
glowColor: "rgba(0, 255, 255, 0.9)",
64+
range: [23, 31],
65+
},
66+
{
67+
name: "mid-high",
68+
color: "#00BFFF",
69+
glowColor: "rgba(0, 191, 255, 0.9)",
70+
range: [16, 22],
71+
},
72+
{
73+
name: "mid",
74+
color: "#FF1493",
75+
glowColor: "rgba(255, 20, 147, 0.9)",
76+
range: [9, 15],
77+
},
78+
{
79+
name: "mid-low",
80+
color: "#FF00FF",
81+
glowColor: "rgba(255, 0, 255, 0.9)",
82+
range: [4, 8],
83+
},
84+
{
85+
name: "low",
86+
color: "#8A2BE2",
87+
glowColor: "rgba(137, 43, 226, 0.9)",
88+
range: [0, 3],
89+
},
90+
];
91+
92+
// Animation time for wave movement
93+
let time = 0;
694

795
/**
896
* Main drawing function that renders the audio visualization
97+
* This is called repeatedly by requestAnimationFrame
998
*/
1099
export function draw() {
11-
// Clear canvas
12-
ctx.clearRect(0, 0, canvas.width, canvas.height);
100+
// Update animation time for wave movement
101+
time += 0.009;
102+
103+
// Clear and prepare the canvas
104+
clearCanvas();
105+
13106
// Show instructions
14107
drawInstructions();
15-
drawLine("green");
108+
109+
// Draw audio visualization
110+
drawVisualization();
111+
112+
// Schedule the next frame
16113
requestAnimationFrame(draw);
17114
}
18115

116+
/**
117+
* Clears the canvas and fills with background color
118+
*/
119+
function clearCanvas() {
120+
ctx.clearRect(0, 0, canvas.width, canvas.height);
121+
ctx.fillStyle = "#000000"; // Pure black background
122+
ctx.fillRect(0, 0, canvas.width, canvas.height);
123+
}
124+
125+
/**
126+
* Draws the audio visualization based on current state
127+
*/
128+
function drawVisualization() {
129+
if (analyser) {
130+
// Get frequency data from audio analyzer
131+
analyser.getByteFrequencyData(dataArray);
132+
drawFrequencyBands();
133+
} else {
134+
// Draw default waves when no audio is playing
135+
drawDefaultBands();
136+
}
137+
}
138+
19139
function drawInstructions() {
20140
ctx.textAlign = "center";
141+
ctx.fillStyle = "white";
21142
ctx.fillText(
22143
"F: File Audio | V: Microphone | ESC: Stop Mic",
23144
canvas.width / 2,
@@ -26,18 +147,187 @@ function drawInstructions() {
26147
}
27148

28149
/**
29-
* @param {string} color
150+
* Draws all frequency bands based on audio data
151+
* Each band is visualized as a sine wave with amplitude based on its frequency range
30152
*/
31-
function drawLine(color) {
32-
// Draw visualization if audio is active
33-
ctx.fillStyle = color;
153+
function drawFrequencyBands() {
154+
// Check if dataArray is initialized and has data
155+
if (!dataArray || dataArray.length === 0) {
156+
// Draw default waves if no audio data is available
157+
drawDefaultBands();
158+
return;
159+
}
34160

35-
if (analyser) {
36-
analyser.getByteFrequencyData(dataArray);
37-
const avg = dataArray.reduce((a, b) => a + b, 0) / dataArray.length;
38-
const height = defaultHeight + (avg / 255) * 100;
39-
ctx.fillRect(0, canvas.height / 2 - height / 2, canvas.width, height);
40-
} else {
41-
ctx.fillRect(0, canvas.height / 2, canvas.width, defaultHeight);
161+
// Draw each frequency band
162+
BANDS.forEach((band, index) => {
163+
// Get the frequency data for this band
164+
const bandData = getBandData(band, index);
165+
166+
// Draw the line for this band
167+
drawLine(
168+
band.color,
169+
band.glowColor,
170+
index,
171+
bandData.frequency,
172+
bandData.amplitude,
173+
time,
174+
);
175+
});
176+
}
177+
178+
/**
179+
* Draws default frequency bands when no audio data is available
180+
*/
181+
function drawDefaultBands() {
182+
BANDS.forEach((band, index) => {
183+
const defaultFrequency = 2 + index * 0.5;
184+
const defaultAmplitude = 20 + index * 5;
185+
drawLine(
186+
band.color,
187+
band.glowColor,
188+
index,
189+
defaultFrequency,
190+
defaultAmplitude,
191+
time,
192+
);
193+
});
194+
}
195+
196+
/**
197+
* Calculates the frequency data for a specific band
198+
* @param {Object} band - The frequency band object
199+
* @param {number} index - The index of the band
200+
* @returns {Object} - Object containing frequency and amplitude data
201+
*/
202+
function getBandData(band, index) {
203+
const [start, end] = band.range;
204+
205+
// Ensure indices are within bounds
206+
const safeStart = Math.min(start, dataArray.length - 1);
207+
const safeEnd = Math.min(end, dataArray.length - 1);
208+
209+
// Calculate average frequency value for this band
210+
let sum = 0;
211+
for (let i = safeStart; i <= safeEnd; i++) {
212+
sum += dataArray[i];
213+
}
214+
const avgFrequency = sum / (safeEnd - safeStart + 1);
215+
216+
// Calculate amplitude based on average frequency
217+
const amplitude = defaultHeight + (avgFrequency / 255) * 100;
218+
219+
// Calculate frequency (number of waves) based on the band
220+
// Higher frequency bands get more waves
221+
const frequency = 2 + (band.range[0] / dataArray.length) * 10;
222+
223+
// Add a small random factor to make it more dynamic
224+
const randomFactor = Math.sin(time * (index + 1)) * 5;
225+
226+
return {
227+
frequency,
228+
amplitude: amplitude + randomFactor,
229+
};
230+
}
231+
232+
/**
233+
* Draws a single frequency band as a sine wave
234+
* @param {string} color - Color of the line
235+
* @param {string} glowColor - Color for the glow effect
236+
* @param {number} index - Band index for vertical positioning
237+
* @param {number} frequency - Number of waves to draw
238+
* @param {number} amplitude - Height of the waves
239+
* @param {number} time - Current animation time
240+
*/
241+
function drawLine(color, glowColor, index, frequency, amplitude, time) {
242+
// Set up drawing context with enhanced visual effects
243+
setupLineStyle(color, glowColor);
244+
245+
// Draw the sine wave
246+
drawSineWave(index, frequency, amplitude, time);
247+
248+
// Reset shadow for next drawing
249+
ctx.shadowBlur = 0;
250+
}
251+
252+
/**
253+
* Sets up the line style with glow effect
254+
* @param {string} color - Main color of the line
255+
* @param {string} glowColor - Color for the glow effect
256+
*/
257+
function setupLineStyle(color, glowColor) {
258+
ctx.shadowBlur = 15;
259+
ctx.shadowColor = glowColor;
260+
ctx.strokeStyle = color;
261+
ctx.lineWidth = 6; // Thicker lines for smoother appearance
262+
}
263+
264+
/**
265+
* Draws a sine wave with an envelope function
266+
* @param {number} index - Band index for phase calculation
267+
* @param {number} frequency - Number of waves to draw
268+
* @param {number} amplitude - Height of the waves
269+
* @param {number} time - Current animation time
270+
*/
271+
function drawSineWave(index, frequency, amplitude, time) {
272+
ctx.beginPath();
273+
274+
// Common vertical center for all lines
275+
const verticalCenter = canvas.height / 2;
276+
277+
// Start at the left edge - all lines start at the same point
278+
ctx.moveTo(0, verticalCenter);
279+
280+
// Draw the sine wave points
281+
for (let x = 0; x < canvas.width; x++) {
282+
// Calculate the y position for this point
283+
const y = calculateWavePoint(
284+
x,
285+
verticalCenter,
286+
index,
287+
frequency,
288+
amplitude,
289+
time,
290+
);
291+
ctx.lineTo(x, y);
42292
}
293+
294+
// Stroke with glow effect
295+
ctx.stroke();
296+
}
297+
298+
/**
299+
* Calculates a single point on the sine wave
300+
* @param {number} x - X coordinate
301+
* @param {number} verticalCenter - Vertical center position
302+
* @param {number} index - Band index for phase calculation
303+
* @param {number} frequency - Number of waves
304+
* @param {number} amplitude - Height of the waves
305+
* @param {number} time - Current animation time
306+
* @returns {number} - Y coordinate
307+
*/
308+
function calculateWavePoint(
309+
x,
310+
verticalCenter,
311+
index,
312+
frequency,
313+
amplitude,
314+
time,
315+
) {
316+
// Calculate position relative to center (0 to 1, where 0.5 is center)
317+
const relativePos = x / canvas.width;
318+
319+
// Create an envelope that peaks in the center and is flat at the edges
320+
// Using a modified bell curve (Gaussian function)
321+
const envelope = Math.exp(-Math.pow((relativePos - 0.5) * 5, 2));
322+
323+
// Calculate phase shift based on time and band index
324+
const phase = time + (index * Math.PI) / 5;
325+
326+
// Calculate the sine wave with the envelope
327+
return (
328+
verticalCenter +
329+
Math.sin((x * (Math.PI * 2 * frequency)) / canvas.width + phase) *
330+
amplitude *
331+
envelope
332+
);
43333
}

0 commit comments

Comments
 (0)