kimhyunwoo commited on
Commit
6574219
ยท
verified ยท
1 Parent(s): 34a689f

Update index.html

Browse files
Files changed (1) hide show
  1. index.html +98 -94
index.html CHANGED
@@ -1,7 +1,7 @@
1
  <!DOCTYPE html>
2
  <html>
3
  <head>
4
- <title>FFT ๋ถ„์„ ํ›„ ์‚ฌ์ธํŒŒ/์ฝ”์‚ฌ์ธํŒŒ ์žฌ์ƒ (์ˆ˜์ •)</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,198 +11,201 @@
11
  </head>
12
  <body>
13
 
14
- <h1>FFT ๋ถ„์„ ํ›„ ์‚ฌ์ธํŒŒ/์ฝ”์‚ฌ์ธํŒŒ ์žฌ์ƒ</h1>
15
- <p>๋งˆ์ดํฌ ์ž…๋ ฅ์„ ๋ฐ›์•„ FFT ๋ถ„์„ ํ›„ ์ฃผํŒŒ์ˆ˜ ์„ฑ๋ถ„์œผ๋กœ ์†Œ๋ฆฌ๋ฅผ ์žฌ๊ตฌ์„ฑํ•ฉ๋‹ˆ๋‹ค.</p>
16
 
17
- <button id="startButton">๋งˆ์ดํฌ ์ž…๋ ฅ ์‹œ์ž‘ ๋ฐ ์žฌ์ƒ</button>
18
- <button id="stopButton" disabled>์ค‘์ง€</button>
19
- <div id="status">๋Œ€๊ธฐ ์ค‘...</div>
20
 
21
  <script>
22
  let audioContext;
23
  let analyser;
24
- let source;
25
- let gainNode; // ์ „์ฒด ๋ณผ๋ฅจ ์กฐ์ ˆ ๋…ธ๋“œ
26
- let animationFrameId = null; // requestAnimationFrame ID
27
- let activeOscillators = []; // ํ˜„์žฌ ํ™œ์„ฑํ™”๋œ ์˜ค์‹ค๋ ˆ์ดํ„ฐ ๋…ธ๋“œ๋“ค์„ ์ €์žฅํ•˜๋Š” ๋ฐฐ์—ด
28
 
29
- // FFT ๋ถ„์„ํ•  ์ฃผํŒŒ์ˆ˜ ์„ฑ๋ถ„์˜ ๊ฐœ์ˆ˜ (2์˜ ๊ฑฐ๋“ญ์ œ๊ณฑ)
30
- const fftSize = 2048;
31
- // ์ฃผํŒŒ์ˆ˜ ๋ฐ์ดํ„ฐ ๋ฐฐ์—ด์˜ ํฌ๊ธฐ (fftSize / 2)
32
- const bufferLength = analyser ? analyser.frequencyBinCount : fftSize / 2;
33
 
34
- // FFT ๋ถ„์„ ๊ฒฐ๊ณผ๋ฅผ ์ €์žฅํ•  ๋ฐฐ์—ด
35
- let dataArray = new Uint8Array(bufferLength); // ์ฃผํŒŒ์ˆ˜ ํฌ๊ธฐ (0-255)
36
 
37
- // ์žฌ์ƒํ•  ์‚ฌ์ธํŒŒ/์ฝ”์‚ฌ์ธํŒŒ์˜ ๊ฐœ์ˆ˜ (FFT ๋ถ„์„ ๊ฒฐ๊ณผ๋ฅผ ๋ช‡ ๊ฐœ์˜ ํŒŒํ˜•์œผ๋กœ ์žฌ๊ตฌ์„ฑํ• ์ง€)
 
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(dataArray);
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
- // bufferLength๊ฐ€ numReconstructedOscillators๋ณด๋‹ค ํด ๋•Œ ์ธ๋ฑ์Šค๊ฐ€ ๊ฒน์น˜์ง€ ์•Š๋„๋ก ๊ณ„์‚ฐ
70
- const frequencyIndex = Math.min(Math.floor(i * (bufferLength / numReconstructedOscillators)), bufferLength - 1);
71
 
72
- // ํ•ด๋‹น ์ธ๋ฑ์Šค์˜ ์ฃผํŒŒ์ˆ˜ ํฌ๊ธฐ ๊ฐ€์ ธ์˜ค๊ธฐ
73
- const magnitude = dataArray[frequencyIndex];
74
 
75
- // ํฌ๊ธฐ๊ฐ€ ์ผ์ • ๊ฐ’ ์ด์ƒ์ธ ๊ฒฝ์šฐ์—๋งŒ ์˜ค์‹ค๋ ˆ์ดํ„ฐ ์ƒ์„ฑ (๋…ธ์ด์ฆˆ ๊ฐ์†Œ ํšจ๊ณผ)
76
- if (magnitude > 10) { // ์ž„๊ณ„๊ฐ’ (์กฐ์ ˆ ๊ฐ€๋Šฅ)
77
- // ์‹ค์ œ ์ฃผํŒŒ์ˆ˜ ๊ณ„์‚ฐ (Hz)
78
  const frequency = (frequencyIndex * audioContext.sampleRate) / fftSize;
79
 
80
- // ์žฌ์ƒํ•  ์ฃผํŒŒ์ˆ˜ ๋ฒ”์œ„ ์ œํ•œ (์„ ํƒ ์‚ฌํ•ญ)
81
- if (frequency > 20 && frequency < audioContext.sampleRate / 2) { // ๊ฐ€์ฒญ ์ฃผํŒŒ์ˆ˜ ๋ฒ”์œ„ ๋‚ด
82
- oscillator.type = 'sine'; // ํŒŒํ˜• ํƒ€์ž…: 'sine', 'square', 'sawtooth', 'triangle' ๋˜๋Š” 'custom'
83
  try {
84
- // frequency ๊ฐ’์ด ์œ ํšจํ•œ์ง€ ํ™•์ธ (0๋ณด๋‹ค ์ปค์•ผ ํ•จ)
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); // ๋ณผ๋ฅจ ์กฐ์ ˆ (0.5๋Š” ์ถ”๊ฐ€ ๊ฐ์‡ )
101
 
102
- // ์˜ค์‹ค๋ ˆ์ดํ„ฐ -> ๊ฐœ๋ณ„ ๊ฒŒ์ธ ๋…ธ๋“œ -> ์ „์ฒด ๊ฒŒ์ธ ๋…ธ๋“œ -> ์ถœ๋ ฅ
103
  oscillator.connect(oscGainNode);
104
- oscGainNode.connect(gainNode);
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
- source = audioContext.createMediaStreamSource(stream); // ๋งˆ์ดํฌ ์†Œ์Šค
138
- analyser = audioContext.createAnalyser(); // ๋ถ„์„๊ธฐ
139
- analyser.fftSize = fftSize; // FFT ์‚ฌ์ด์ฆˆ ์„ค์ •
140
- gainNode = audioContext.createGain(); // ์ „์ฒด ๋ณผ๋ฅจ ์กฐ์ ˆ ๋…ธ๋“œ
141
- gainNode.gain.setValueAtTime(0.5, audioContext.currentTime); // ์ดˆ๊ธฐ ๋ณผ๋ฅจ ์„ค์ • (0.5)
142
 
143
- // ์—ฐ๊ฒฐ: ๋งˆ์ดํฌ ์†Œ์Šค -> ๋ถ„์„๊ธฐ -> ์ „์ฒด ๊ฒŒ์ธ -> ์ถœ๋ ฅ
144
- source.connect(analyser);
145
- // ์ฃผ์˜: ๋ถ„์„๊ธฐ ๋…ธ๋“œ์—์„œ ์ง์ ‘ ์ถœ๋ ฅ์œผ๋กœ ์†Œ๋ฆฌ๊ฐ€ ๋‚˜๊ฐ€์ง€ ์•Š๋„๋ก gainNode์—๋งŒ ์—ฐ๊ฒฐ
146
- // source.connect(gainNode); // ํ•„์š”์— ๋”ฐ๋ผ ์›๋ณธ ์†Œ๋ฆฌ๋„ ์ถœ๋ ฅํ•˜๋ ค๋ฉด ์—ฐ๊ฒฐ
147
- gainNode.connect(audioContext.destination); // ์ตœ์ข… ์Šคํ”ผ์ปค ์ถœ๋ ฅ
 
148
 
149
- // dataArray ํฌ๊ธฐ ์žฌ์„ค์ • (analyser ์ƒ์„ฑ ํ›„)
150
- dataArray = new Uint8Array(analyser.frequencyBinCount);
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('์˜ค๋””์˜ค ์‹œ์ž‘ ์˜ค๋ฅ˜:', err);
162
- statusDiv.textContent = `์˜ค๋ฅ˜: ${err.message}`;
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 (source) {
192
- source.disconnect();
193
- source.mediaStream.getTracks().forEach(track => track.stop()); // ๋งˆ์ดํฌ ์ŠคํŠธ๋ฆผ ํŠธ๋ž™ ์ค‘์ง€
194
- source = null;
 
195
  }
196
  if (analyser) {
197
  analyser.disconnect();
198
  analyser = null;
199
  }
200
- if (gainNode) {
201
- gainNode.disconnect();
202
- gainNode = null;
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
- dataArray = new Uint8Array(fftSize / 2); // dataArray ์ดˆ๊ธฐํ™”
 
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>