Spaces:
Running
Running
Update index.html
Browse files- index.html +98 -94
index.html
CHANGED
@@ -1,7 +1,7 @@
|
|
1 |
<!DOCTYPE html>
|
2 |
<html>
|
3 |
<head>
|
4 |
-
<title>FFT
|
5 |
<style>
|
6 |
body { font-family: sans-serif; text-align: center; margin-top: 50px; }
|
7 |
button { padding: 10px 20px; font-size: 16px; cursor: pointer; margin: 5px; }
|
@@ -11,198 +11,201 @@
|
|
11 |
</head>
|
12 |
<body>
|
13 |
|
14 |
-
<h1>FFT
|
15 |
-
<p
|
16 |
|
17 |
-
<button id="startButton"
|
18 |
-
<button id="stopButton" disabled
|
19 |
-
<div id="status"
|
20 |
|
21 |
<script>
|
22 |
let audioContext;
|
23 |
let analyser;
|
24 |
-
let source
|
25 |
-
let
|
26 |
-
let animationFrameId = null; // requestAnimationFrame
|
27 |
-
let activeOscillators = []; //
|
28 |
|
29 |
-
// FFT
|
30 |
-
const fftSize = 2048;
|
31 |
-
//
|
32 |
-
const
|
33 |
|
34 |
-
//
|
35 |
-
let
|
36 |
|
37 |
-
//
|
|
|
38 |
const numReconstructedOscillators = 50;
|
39 |
|
|
|
40 |
const startButton = document.getElementById('startButton');
|
41 |
const stopButton = document.getElementById('stopButton');
|
42 |
const statusDiv = document.getElementById('status');
|
43 |
|
44 |
-
// ----------
|
45 |
const processAudio = () => {
|
46 |
-
if (!analyser) return; // analyser
|
47 |
|
48 |
-
// 1. FFT
|
49 |
-
analyser.getByteFrequencyData(
|
50 |
|
51 |
-
// 2.
|
52 |
activeOscillators.forEach(osc => {
|
53 |
try {
|
54 |
-
osc.stop(); //
|
55 |
-
osc.disconnect(); //
|
56 |
} catch (e) {
|
57 |
-
//
|
58 |
// console.warn("Error stopping/disconnecting oscillator:", e);
|
59 |
}
|
60 |
});
|
61 |
-
activeOscillators = []; //
|
62 |
|
63 |
-
// 3.
|
64 |
-
//
|
65 |
for (let i = 0; i < numReconstructedOscillators; i++) {
|
66 |
const oscillator = audioContext.createOscillator();
|
67 |
|
68 |
-
//
|
69 |
-
//
|
70 |
-
const frequencyIndex = Math.min(Math.floor(i * (
|
71 |
|
72 |
-
//
|
73 |
-
const magnitude =
|
74 |
|
75 |
-
//
|
76 |
-
if (magnitude > 10) { //
|
77 |
-
//
|
78 |
const frequency = (frequencyIndex * audioContext.sampleRate) / fftSize;
|
79 |
|
80 |
-
//
|
81 |
-
if (frequency > 20 && frequency < audioContext.sampleRate / 2) { //
|
82 |
-
oscillator.type = 'sine'; //
|
83 |
try {
|
84 |
-
// frequency
|
85 |
if (frequency > 0) {
|
86 |
oscillator.frequency.setValueAtTime(frequency, audioContext.currentTime);
|
87 |
} else {
|
88 |
-
continue; //
|
89 |
}
|
90 |
} catch (e) {
|
91 |
console.error("Error setting frequency:", e, "Frequency:", frequency);
|
92 |
-
continue; //
|
93 |
}
|
94 |
|
95 |
-
|
96 |
-
//
|
97 |
-
// magnitude(0~255)๋ฅผ 0~1 ๋ฒ์๋ก ์ ๊ทํํ๊ณ , ์ ์ฒด ๋ณผ๋ฅจ ๊ณ ๋ ค
|
98 |
const oscGainNode = audioContext.createGain();
|
99 |
-
// magnitude
|
100 |
-
oscGainNode.gain.setValueAtTime((magnitude / 255) ** 2 * 0.5, audioContext.currentTime); //
|
101 |
|
102 |
-
//
|
103 |
oscillator.connect(oscGainNode);
|
104 |
-
oscGainNode.connect(
|
105 |
|
106 |
-
//
|
107 |
oscillator.start(audioContext.currentTime);
|
108 |
|
109 |
-
//
|
110 |
activeOscillators.push(oscillator);
|
111 |
}
|
112 |
}
|
113 |
}
|
114 |
|
115 |
-
// 4.
|
116 |
animationFrameId = requestAnimationFrame(processAudio);
|
117 |
};
|
118 |
|
119 |
-
// ----------
|
120 |
startButton.addEventListener('click', async () => {
|
121 |
try {
|
122 |
-
if (audioContext) { //
|
123 |
-
console.log("AudioContext already active.");
|
124 |
return;
|
125 |
}
|
126 |
|
127 |
-
statusDiv.textContent = '
|
128 |
statusDiv.classList.remove('error');
|
129 |
|
130 |
-
// 1.
|
131 |
audioContext = new (window.AudioContext || window.webkitAudioContext)();
|
132 |
|
133 |
-
// 2.
|
134 |
const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
|
135 |
|
136 |
-
// 3.
|
137 |
-
|
138 |
-
analyser = audioContext.createAnalyser();
|
139 |
-
analyser.fftSize = fftSize;
|
140 |
-
|
141 |
-
|
142 |
|
143 |
-
//
|
144 |
-
|
145 |
-
//
|
146 |
-
// source
|
147 |
-
|
|
|
148 |
|
149 |
-
//
|
150 |
-
|
151 |
|
152 |
-
// 4.
|
153 |
processAudio();
|
154 |
|
155 |
-
// UI
|
156 |
startButton.disabled = true;
|
157 |
stopButton.disabled = false;
|
158 |
-
statusDiv.textContent = '
|
159 |
|
160 |
} catch (err) {
|
161 |
-
console.error('
|
162 |
-
statusDiv.textContent =
|
163 |
statusDiv.classList.add('error');
|
164 |
-
//
|
165 |
stopAudio();
|
166 |
}
|
167 |
});
|
168 |
|
169 |
-
// ----------
|
170 |
stopButton.addEventListener('click', () => {
|
171 |
stopAudio();
|
172 |
});
|
173 |
|
174 |
-
// ----------
|
175 |
const stopAudio = () => {
|
176 |
if (animationFrameId) {
|
177 |
-
cancelAnimationFrame(animationFrameId); //
|
178 |
animationFrameId = null;
|
179 |
}
|
180 |
|
181 |
-
//
|
182 |
activeOscillators.forEach(osc => {
|
183 |
try {
|
184 |
osc.stop();
|
185 |
osc.disconnect();
|
186 |
-
} catch (e) { /*
|
187 |
});
|
188 |
activeOscillators = [];
|
189 |
|
190 |
-
//
|
191 |
-
if (
|
192 |
-
|
193 |
-
|
194 |
-
|
|
|
195 |
}
|
196 |
if (analyser) {
|
197 |
analyser.disconnect();
|
198 |
analyser = null;
|
199 |
}
|
200 |
-
if (
|
201 |
-
|
202 |
-
|
203 |
}
|
204 |
|
205 |
-
//
|
206 |
if (audioContext && audioContext.state !== 'closed') {
|
207 |
audioContext.close().then(() => {
|
208 |
audioContext = null;
|
@@ -210,11 +213,12 @@
|
|
210 |
});
|
211 |
}
|
212 |
|
213 |
-
// UI
|
214 |
startButton.disabled = false;
|
215 |
stopButton.disabled = true;
|
216 |
-
statusDiv.textContent = '
|
217 |
-
|
|
|
218 |
};
|
219 |
|
220 |
</script>
|
|
|
1 |
<!DOCTYPE html>
|
2 |
<html>
|
3 |
<head>
|
4 |
+
<title>FFT Analysis and Sine/Cosine Wave Resynthesis (Corrected)</title>
|
5 |
<style>
|
6 |
body { font-family: sans-serif; text-align: center; margin-top: 50px; }
|
7 |
button { padding: 10px 20px; font-size: 16px; cursor: pointer; margin: 5px; }
|
|
|
11 |
</head>
|
12 |
<body>
|
13 |
|
14 |
+
<h1>FFT Analysis and Sine/Cosine Wave Resynthesis</h1>
|
15 |
+
<p>Takes microphone input, performs FFT analysis, and reconstructs the sound using frequency components.</p>
|
16 |
|
17 |
+
<button id="startButton">Start Microphone Input and Playback</button>
|
18 |
+
<button id="stopButton" disabled>Stop</button>
|
19 |
+
<div id="status">Waiting...</div>
|
20 |
|
21 |
<script>
|
22 |
let audioContext;
|
23 |
let analyser;
|
24 |
+
let microphoneSource; // Renamed from 'source' for clarity
|
25 |
+
let mainGainNode; // Renamed from 'gainNode' for clarity
|
26 |
+
let animationFrameId = null; // ID for requestAnimationFrame
|
27 |
+
let activeOscillators = []; // Array to store currently active oscillator nodes
|
28 |
|
29 |
+
// FFT analysis parameters
|
30 |
+
const fftSize = 2048; // Number of samples for FFT (power of 2)
|
31 |
+
// Size of the frequency data array (fftSize / 2)
|
32 |
+
const frequencyBinCount = fftSize / 2;
|
33 |
|
34 |
+
// Array to store FFT analysis results
|
35 |
+
let frequencyDataArray = new Uint8Array(frequencyBinCount); // Frequency magnitude (0-255)
|
36 |
|
37 |
+
// Number of sine/cosine waves to generate for resynthesis
|
38 |
+
// How many frequency components from the FFT result will be used to reconstruct the sound
|
39 |
const numReconstructedOscillators = 50;
|
40 |
|
41 |
+
// Get DOM elements
|
42 |
const startButton = document.getElementById('startButton');
|
43 |
const stopButton = document.getElementById('stopButton');
|
44 |
const statusDiv = document.getElementById('status');
|
45 |
|
46 |
+
// ---------- Audio Processing Function ----------
|
47 |
const processAudio = () => {
|
48 |
+
if (!analyser) return; // Exit if analyser is not ready
|
49 |
|
50 |
+
// 1. FFT Analysis: Get frequency data
|
51 |
+
analyser.getByteFrequencyData(frequencyDataArray);
|
52 |
|
53 |
+
// 2. Cleanup Previous Oscillators: Stop and disconnect all currently active oscillators
|
54 |
activeOscillators.forEach(osc => {
|
55 |
try {
|
56 |
+
osc.stop(); // Stop immediately
|
57 |
+
osc.disconnect(); // Disconnect from all connections
|
58 |
} catch (e) {
|
59 |
+
// Ignore errors if the oscillator was already stopped or disconnected
|
60 |
// console.warn("Error stopping/disconnecting oscillator:", e);
|
61 |
}
|
62 |
});
|
63 |
+
activeOscillators = []; // Clear the array
|
64 |
|
65 |
+
// 3. Create and Connect New Oscillators based on FFT results
|
66 |
+
// Analyze frequency components and create a fixed number of oscillators
|
67 |
for (let i = 0; i < numReconstructedOscillators; i++) {
|
68 |
const oscillator = audioContext.createOscillator();
|
69 |
|
70 |
+
// Calculate the corresponding frequency index in the data array
|
71 |
+
// Ensure index doesn't overlap if frequencyBinCount > numReconstructedOscillators
|
72 |
+
const frequencyIndex = Math.min(Math.floor(i * (frequencyBinCount / numReconstructedOscillators)), frequencyBinCount - 1);
|
73 |
|
74 |
+
// Get the magnitude (amplitude) of this frequency component
|
75 |
+
const magnitude = frequencyDataArray[frequencyIndex];
|
76 |
|
77 |
+
// Only create an oscillator if the magnitude is above a certain threshold (reduces noise)
|
78 |
+
if (magnitude > 10) { // Threshold value (adjustable)
|
79 |
+
// Calculate the actual frequency in Hz
|
80 |
const frequency = (frequencyIndex * audioContext.sampleRate) / fftSize;
|
81 |
|
82 |
+
// Optional: Limit the frequency range for playback
|
83 |
+
if (frequency > 20 && frequency < audioContext.sampleRate / 2) { // Within audible range
|
84 |
+
oscillator.type = 'sine'; // Waveform type: 'sine', 'square', 'sawtooth', 'triangle', or 'custom'
|
85 |
try {
|
86 |
+
// Ensure the frequency is valid (must be > 0)
|
87 |
if (frequency > 0) {
|
88 |
oscillator.frequency.setValueAtTime(frequency, audioContext.currentTime);
|
89 |
} else {
|
90 |
+
continue; // Skip if frequency is invalid
|
91 |
}
|
92 |
} catch (e) {
|
93 |
console.error("Error setting frequency:", e, "Frequency:", frequency);
|
94 |
+
continue; // Skip this oscillator if there's an error
|
95 |
}
|
96 |
|
97 |
+
// Create a gain node for this individual oscillator to control its volume
|
98 |
+
// Set the volume proportional to the magnitude of the frequency component
|
|
|
99 |
const oscGainNode = audioContext.createGain();
|
100 |
+
// Normalize magnitude (0-255) to a 0-1 range, apply non-linear scaling and overall volume adjustment
|
101 |
+
oscGainNode.gain.setValueAtTime((magnitude / 255) ** 2 * 0.5, audioContext.currentTime); // Volume control (0.5 is additional damping)
|
102 |
|
103 |
+
// Connect: Oscillator -> Individual Gain Node -> Main Gain Node -> Output
|
104 |
oscillator.connect(oscGainNode);
|
105 |
+
oscGainNode.connect(mainGainNode);
|
106 |
|
107 |
+
// Start the oscillator immediately
|
108 |
oscillator.start(audioContext.currentTime);
|
109 |
|
110 |
+
// Add the oscillator to the list of active oscillators
|
111 |
activeOscillators.push(oscillator);
|
112 |
}
|
113 |
}
|
114 |
}
|
115 |
|
116 |
+
// 4. Request the next frame for continuous processing
|
117 |
animationFrameId = requestAnimationFrame(processAudio);
|
118 |
};
|
119 |
|
120 |
+
// ---------- Start Button Click Event Handler ----------
|
121 |
startButton.addEventListener('click', async () => {
|
122 |
try {
|
123 |
+
if (audioContext) { // If already running, do nothing
|
124 |
+
console.log("AudioContext is already active.");
|
125 |
return;
|
126 |
}
|
127 |
|
128 |
+
statusDiv.textContent = 'Accessing microphone...';
|
129 |
statusDiv.classList.remove('error');
|
130 |
|
131 |
+
// 1. Create Audio Context
|
132 |
audioContext = new (window.AudioContext || window.webkitAudioContext)();
|
133 |
|
134 |
+
// 2. Get Microphone Input Stream
|
135 |
const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
|
136 |
|
137 |
+
// 3. Create and Connect Audio Nodes
|
138 |
+
microphoneSource = audioContext.createMediaStreamSource(stream); // Microphone source node
|
139 |
+
analyser = audioContext.createAnalyser(); // Analyser node
|
140 |
+
analyser.fftSize = fftSize; // Set FFT size
|
141 |
+
mainGainNode = audioContext.createGain(); // Main volume control node
|
142 |
+
mainGainNode.gain.setValueAtTime(0.5, audioContext.currentTime); // Set initial volume (0.5)
|
143 |
|
144 |
+
// Connect nodes: Microphone Source -> Analyser -> Main Gain -> Destination (Speakers)
|
145 |
+
microphoneSource.connect(analyser);
|
146 |
+
// Note: The analyser node itself doesn't output sound directly.
|
147 |
+
// Connect the source directly to gain if you want to hear the original mic input as well (uncomment below)
|
148 |
+
// microphoneSource.connect(mainGainNode);
|
149 |
+
mainGainNode.connect(audioContext.destination); // Connect main gain to speakers
|
150 |
|
151 |
+
// Initialize/Reset the frequency data array size based on the analyser
|
152 |
+
frequencyDataArray = new Uint8Array(analyser.frequencyBinCount);
|
153 |
|
154 |
+
// 4. Start Audio Processing Loop
|
155 |
processAudio();
|
156 |
|
157 |
+
// Update UI
|
158 |
startButton.disabled = true;
|
159 |
stopButton.disabled = false;
|
160 |
+
statusDiv.textContent = 'Microphone input active and playing back...';
|
161 |
|
162 |
} catch (err) {
|
163 |
+
console.error('Error starting audio:', err);
|
164 |
+
statusDiv.textContent = `Error: ${err.message}`;
|
165 |
statusDiv.classList.add('error');
|
166 |
+
// Clean up resources on error
|
167 |
stopAudio();
|
168 |
}
|
169 |
});
|
170 |
|
171 |
+
// ---------- Stop Button Click Event Handler ----------
|
172 |
stopButton.addEventListener('click', () => {
|
173 |
stopAudio();
|
174 |
});
|
175 |
|
176 |
+
// ---------- Function to Stop Audio and Clean Up Resources ----------
|
177 |
const stopAudio = () => {
|
178 |
if (animationFrameId) {
|
179 |
+
cancelAnimationFrame(animationFrameId); // Stop the animation loop
|
180 |
animationFrameId = null;
|
181 |
}
|
182 |
|
183 |
+
// Stop and disconnect all active oscillators
|
184 |
activeOscillators.forEach(osc => {
|
185 |
try {
|
186 |
osc.stop();
|
187 |
osc.disconnect();
|
188 |
+
} catch (e) { /* Ignore errors if already stopped/disconnected */ }
|
189 |
});
|
190 |
activeOscillators = [];
|
191 |
|
192 |
+
// Disconnect audio nodes
|
193 |
+
if (microphoneSource) {
|
194 |
+
microphoneSource.disconnect();
|
195 |
+
// Stop the microphone track(s) in the stream
|
196 |
+
microphoneSource.mediaStream.getTracks().forEach(track => track.stop());
|
197 |
+
microphoneSource = null;
|
198 |
}
|
199 |
if (analyser) {
|
200 |
analyser.disconnect();
|
201 |
analyser = null;
|
202 |
}
|
203 |
+
if (mainGainNode) {
|
204 |
+
mainGainNode.disconnect();
|
205 |
+
mainGainNode = null;
|
206 |
}
|
207 |
|
208 |
+
// Close the Audio Context to release system resources
|
209 |
if (audioContext && audioContext.state !== 'closed') {
|
210 |
audioContext.close().then(() => {
|
211 |
audioContext = null;
|
|
|
213 |
});
|
214 |
}
|
215 |
|
216 |
+
// Update UI
|
217 |
startButton.disabled = false;
|
218 |
stopButton.disabled = true;
|
219 |
+
statusDiv.textContent = 'Stopped.';
|
220 |
+
// Reset frequency data array
|
221 |
+
frequencyDataArray = new Uint8Array(frequencyBinCount);
|
222 |
};
|
223 |
|
224 |
</script>
|