Skip to content

Commit 05587e8

Browse files
committed
feat: some features
1 parent 34f2d42 commit 05587e8

27 files changed

+1172
-791
lines changed

packages/audiodocs/src/audio/Equalizer.ts

Lines changed: 13 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,21 @@
1-
export const initialFrequencies = [32, 64, 125, 250, 500, 1000, 2000, 4000, 8000, 16000];
2-
3-
export const vocalPreset = [-6, -4, -2, -2, 0, 2, 3, 2, 3, 2];
1+
export const frequencies: Array<{ frequency: number; type: BiquadFilterType }> = [
2+
{ frequency: 60, type: 'lowshelf'},
3+
{ frequency: 250, type: 'peaking' },
4+
{ frequency: 1000, type: 'peaking' },
5+
{ frequency: 4000, type: 'peaking' },
6+
{ frequency: 12000, type: 'highshelf' },
7+
];
48

59
export default class Equalizer {
6-
private audioContext: AudioContext;
7-
private bands: BiquadFilterNode[];
8-
9-
constructor(audioContext: AudioContext, size: number = 10) {
10-
this.audioContext = audioContext;
10+
bands: BiquadFilterNode[];
1111

12+
constructor(audioContext: AudioContext, size: number = frequencies.length) {
1213
this.bands = Array.from({ length: size }, (_, i) => {
1314
const filter = audioContext.createBiquadFilter();
14-
filter.type = "peaking";
15-
filter.frequency.value = initialFrequencies[i] || 1000;
16-
filter.Q.value = 1;
17-
filter.gain.value = vocalPreset[i] || 0;
15+
filter.type = frequencies[i].type;
16+
filter.frequency.value = frequencies[i].frequency;
17+
// filter.Q.value = 1;
18+
filter.gain.value = 0;
1819
return filter;
1920
});
2021

Lines changed: 221 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,221 @@
1+
import { useCallback, useEffect, useState, useRef } from "react";
2+
import AudioManager from "./AudioManager";
3+
import { frequencies } from "./Equalizer";
4+
5+
export interface EqualizerControl {
6+
frequency: number;
7+
gain: number;
8+
type: BiquadFilterType;
9+
}
10+
11+
interface DragState {
12+
isDragging: boolean;
13+
dragIndex: number;
14+
startY: number;
15+
startGain: number;
16+
}
17+
18+
const minFreq = frequencies[0].frequency;
19+
const maxFreq = frequencies[frequencies.length - 1].frequency;
20+
const logMin = Math.log10(minFreq);
21+
const logMax = Math.log10(maxFreq);
22+
23+
const initialValues: EqualizerControl[] = frequencies.map(freq => ({
24+
frequency: freq.frequency,
25+
type: freq.type,
26+
gain: 0,
27+
}));
28+
29+
export default function useEqualizerControls(canvasRef?: React.RefObject<HTMLCanvasElement>) {
30+
const [equalizerBands, setEqualizerBands] = useState<EqualizerControl[]>(initialValues);
31+
const dragState = useRef<DragState>({
32+
isDragging: false,
33+
dragIndex: -1,
34+
startY: 0,
35+
startGain: 0,
36+
});
37+
const canvasRect = useRef<DOMRect | null>(null);
38+
39+
const updateGain = useCallback((index: number, gain: number) => {
40+
// Clamp gain between -12 and +12 dB
41+
const clampedGain = Math.max(-12, Math.min(12, gain));
42+
43+
setEqualizerBands(prev => {
44+
const newBands = [...prev];
45+
newBands[index] = { ...newBands[index], gain: clampedGain };
46+
return newBands;
47+
});
48+
}, []);
49+
50+
const getControlPointPosition = useCallback((frequency: number, gain: number, canvasRect: DOMRect, index: number) => {
51+
// Use the same drawing bounds logic as the drawing function
52+
const labelBoxSize = 32;
53+
const drawingWidth = canvasRect.width - labelBoxSize;
54+
const drawingHeight = canvasRect.height - labelBoxSize;
55+
const offsetX = 0; // drawing starts at x=0
56+
const offsetY = 0; // drawing starts at y=0
57+
58+
const logFreq = Math.log10(frequency);
59+
const normalizedPos = (logFreq - logMin) / (logMax - logMin);
60+
let x = offsetX + (normalizedPos * drawingWidth);
61+
62+
// Apply custom positioning - exactly like the drawing function:
63+
// Points 1,2,5 keep their current positions, point 3 goes to center + 96px, point 4 reflects point 2
64+
if (index === 0) {
65+
x += 96; // Point 1 (index 0) - keep current position
66+
} else if (index === 1) {
67+
x += 91; // Point 2 (index 1) - keep current position
68+
} else if (index === 2) {
69+
x = offsetX + drawingWidth / 2 + 59; // Point 3 (index 2) - center + 59px to the right
70+
} else if (index === 3) {
71+
x -= 18; // Point 4 (index 3) - reflect point 2's position
72+
} else if (index === 4) {
73+
x -= 96; // Point 5 (index 4) - keep current position
74+
}
75+
76+
const normalizedGain = gain / 12; // -1 to 1 range
77+
const y = offsetY + drawingHeight / 2 - (normalizedGain * drawingHeight / 2);
78+
79+
return { x, y };
80+
}, []);
81+
82+
const findControlPointAtPosition = useCallback((x: number, y: number, rect: DOMRect) => {
83+
const controlPointRadius = 24; // 48px diameter = 24px radius
84+
85+
for (let i = 0; i < equalizerBands.length; i++) {
86+
const band = equalizerBands[i];
87+
const pos = getControlPointPosition(band.frequency, band.gain, rect, i);
88+
89+
const distance = Math.sqrt((x - pos.x) ** 2 + (y - pos.y) ** 2);
90+
if (distance <= controlPointRadius) {
91+
return i;
92+
}
93+
}
94+
return -1;
95+
}, [equalizerBands, getControlPointPosition]); const handleStart = useCallback((clientX: number, clientY: number) => {
96+
if (!canvasRef?.current) {
97+
return;
98+
}
99+
100+
const rect = canvasRef.current.getBoundingClientRect();
101+
canvasRect.current = rect;
102+
103+
const x = clientX - rect.left;
104+
const y = clientY - rect.top;
105+
106+
const index = findControlPointAtPosition(x, y, rect);
107+
if (index !== -1) {
108+
dragState.current = {
109+
isDragging: true,
110+
dragIndex: index,
111+
startY: clientY,
112+
startGain: equalizerBands[index].gain,
113+
};
114+
}
115+
}, [canvasRef, findControlPointAtPosition, equalizerBands]);
116+
117+
const handleMove = useCallback((clientY: number) => {
118+
if (!dragState.current.isDragging || !canvasRect.current) {
119+
return;
120+
}
121+
122+
const deltaY = dragState.current.startY - clientY;
123+
const labelBoxSize = 32;
124+
const drawingHeight = canvasRect.current.height - labelBoxSize;
125+
const gainChange = (deltaY / drawingHeight) * 24; // 24dB range using drawing height
126+
let newGain = dragState.current.startGain + gainChange;
127+
128+
// Clamp gain to prevent control points from going outside the canvas boundaries
129+
// The control points have a 24px radius, so we need to account for that
130+
const controlPointRadius = 24;
131+
const maxGainForBounds = 12 * (1 - (2 * controlPointRadius) / drawingHeight);
132+
const minGainForBounds = -maxGainForBounds;
133+
134+
// Apply stricter bounds to keep circles fully visible
135+
newGain = Math.max(minGainForBounds, Math.min(maxGainForBounds, newGain));
136+
137+
updateGain(dragState.current.dragIndex, newGain);
138+
}, [updateGain]);
139+
140+
const handleEnd = useCallback(() => {
141+
dragState.current.isDragging = false;
142+
dragState.current.dragIndex = -1;
143+
canvasRect.current = null;
144+
}, []);
145+
146+
// Mouse event handlers
147+
const handleMouseDown = useCallback((e: MouseEvent) => {
148+
handleStart(e.clientX, e.clientY);
149+
}, [handleStart]);
150+
151+
const handleMouseMove = useCallback((e: MouseEvent) => {
152+
handleMove(e.clientY);
153+
}, [handleMove]);
154+
155+
const handleMouseUp = useCallback(() => {
156+
handleEnd();
157+
}, [handleEnd]);
158+
159+
// Touch event handlers
160+
const handleTouchStart = useCallback((e: TouchEvent) => {
161+
if (e.touches.length === 1) {
162+
const touch = e.touches[0];
163+
handleStart(touch.clientX, touch.clientY);
164+
}
165+
}, [handleStart]);
166+
167+
const handleTouchMove = useCallback((e: TouchEvent) => {
168+
if (e.touches.length === 1 && dragState.current.isDragging) {
169+
e.preventDefault(); // Prevent scrolling
170+
const touch = e.touches[0];
171+
handleMove(touch.clientY);
172+
}
173+
}, [handleMove]);
174+
175+
const handleTouchEnd = useCallback(() => {
176+
handleEnd();
177+
}, [handleEnd]);
178+
179+
// Setup global event listeners
180+
useEffect(() => {
181+
if (!canvasRef?.current) {
182+
return;
183+
}
184+
185+
const canvas = canvasRef.current;
186+
187+
// Add canvas event listeners
188+
canvas.addEventListener('mousedown', handleMouseDown);
189+
canvas.addEventListener('touchstart', handleTouchStart, { passive: false });
190+
191+
// Add global event listeners
192+
document.addEventListener('mousemove', handleMouseMove);
193+
document.addEventListener('mouseup', handleMouseUp);
194+
document.addEventListener('touchmove', handleTouchMove, { passive: false });
195+
document.addEventListener('touchend', handleTouchEnd);
196+
document.addEventListener('touchcancel', handleTouchEnd);
197+
198+
return () => {
199+
// Remove canvas event listeners
200+
canvas.removeEventListener('mousedown', handleMouseDown);
201+
canvas.removeEventListener('touchstart', handleTouchStart);
202+
203+
// Remove global event listeners
204+
document.removeEventListener('mousemove', handleMouseMove);
205+
document.removeEventListener('mouseup', handleMouseUp);
206+
document.removeEventListener('touchmove', handleTouchMove);
207+
document.removeEventListener('touchend', handleTouchEnd);
208+
document.removeEventListener('touchcancel', handleTouchEnd);
209+
};
210+
}, [canvasRef, handleMouseDown, handleMouseMove, handleMouseUp, handleTouchStart, handleTouchMove, handleTouchEnd]);
211+
212+
useEffect(() => {
213+
AudioManager.equalizer.setFrequencies(equalizerBands.map(band => band.frequency));
214+
AudioManager.equalizer.setGains(equalizerBands.map(band => band.gain));
215+
}, [equalizerBands]);
216+
217+
return {
218+
equalizerBands,
219+
updateGain,
220+
};
221+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import React, { useEffect, useState } from 'react';
2+
3+
import AudioManager from './AudioManager';
4+
5+
export default function useIsPlaying() {
6+
const [isPlaying, setIsPlaying] = useState(AudioManager.isPlaying);
7+
8+
useEffect(() => {
9+
AudioManager.addEventListener('playing', () => {
10+
setIsPlaying(true);
11+
});
12+
13+
AudioManager.addEventListener('stopped', () => {
14+
setIsPlaying(false);
15+
});
16+
17+
return () => {
18+
AudioManager.removeEventListener('playing', () => setIsPlaying(true));
19+
AudioManager.removeEventListener('stopped', () => setIsPlaying(false));
20+
};
21+
}, []);
22+
23+
return isPlaying;
24+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
2+
export default function createGradient(ctx: CanvasRenderingContext2D, y: number, maxHeight: number, rgb: [number, number, number], stopAt: number = 0.95): CanvasGradient {
3+
const gradient = ctx.createLinearGradient(0, y, 0, y + maxHeight);
4+
5+
gradient.addColorStop(0, `rgba(${rgb[0]}, ${rgb[1]}, ${rgb[2]}, 0.3)`);
6+
gradient.addColorStop(stopAt, `rgba(${rgb[0]}, ${rgb[1]}, ${rgb[2]}, 0.0)`);
7+
8+
return gradient;
9+
}
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
import { frequencies } from "../audio/Equalizer";
2+
import AudioManager from "../audio/AudioManager";
3+
4+
const minFreq = frequencies[0].frequency;
5+
const maxFreq = frequencies[frequencies.length - 1].frequency;
6+
const logMin = Math.log10(minFreq);
7+
const logMax = Math.log10(maxFreq);
8+
9+
export default function drawEQControlPoints(
10+
canvas: HTMLCanvasElement,
11+
ctx: CanvasRenderingContext2D,
12+
x: number,
13+
y: number,
14+
width: number,
15+
height: number,
16+
fillColor: string
17+
) {
18+
const controlPointSize = 48;
19+
const halfSize = controlPointSize / 2;
20+
21+
// Draw control points for each equalizer band
22+
frequencies.forEach((freqConfig, index) => {
23+
if (AudioManager.equalizer && AudioManager.equalizer.bands[index]) {
24+
const band = AudioManager.equalizer.bands[index];
25+
const frequency = freqConfig.frequency;
26+
27+
// Get current gain value in dB
28+
const gainValue = band.gain.value;
29+
30+
// Calculate X position based on frequency (logarithmic scale)
31+
const logFreq = Math.log10(frequency);
32+
const normalizedPos = (logFreq - logMin) / (logMax - logMin);
33+
let posX = x + (normalizedPos * width);
34+
35+
// Apply custom positioning:
36+
// Points 1,2,5 keep their current positions, point 3 goes to center + 96px, point 4 reflects point 2
37+
if (index === 0) {
38+
posX += 96; // Point 1 (index 0) - keep current position
39+
} else if (index === 1) {
40+
posX += 91; // Point 2 (index 1) - keep current position
41+
} else if (index === 2) {
42+
posX = x + width / 2 + 59; // Point 3 (index 2) - center + 86px to the right
43+
} else if (index === 3) {
44+
// Point 4 (index 3) - reflect point 2's position
45+
posX -= 18; // Reflect point 2's position
46+
} else {
47+
posX -= 96; // Point 5 (index 4) - keep current position
48+
}
49+
50+
// Calculate Y position based on gain value
51+
// Assuming ±12dB range for display
52+
const normalizedGain = gainValue / 12; // -1 to 1 range
53+
const posY = y + height / 2 - (normalizedGain * height / 2);
54+
55+
// Draw outer circle (border)
56+
ctx.beginPath();
57+
ctx.arc(posX, posY, halfSize, 0, Math.PI * 2);
58+
ctx.fillStyle = fillColor;
59+
ctx.fill();
60+
ctx.strokeStyle = fillColor;
61+
ctx.lineWidth = 2;
62+
ctx.stroke();
63+
64+
// Draw frequency label
65+
ctx.fillStyle = '#FFFFFF';
66+
ctx.font = '16px Aeonik';
67+
ctx.textAlign = 'center';
68+
ctx.textBaseline = 'middle';
69+
70+
// Format frequency display
71+
const freqLabel = (index + 1).toString();
72+
73+
ctx.fillText(freqLabel, posX, posY);
74+
}
75+
});
76+
}

0 commit comments

Comments
 (0)