idgmatrix commited on
Commit
8f656f4
·
verified ·
1 Parent(s): ff5768f

Upload folder using huggingface_hub

Browse files
Files changed (1) hide show
  1. index.html +854 -19
index.html CHANGED
@@ -1,19 +1,854 @@
1
- <!doctype html>
2
- <html>
3
- <head>
4
- <meta charset="utf-8" />
5
- <meta name="viewport" content="width=device-width" />
6
- <title>My static Space</title>
7
- <link rel="stylesheet" href="style.css" />
8
- </head>
9
- <body>
10
- <div class="card">
11
- <h1>Welcome to your static Space!</h1>
12
- <p>You can modify this app directly by editing <i>index.html</i> in the Files and versions tab.</p>
13
- <p>
14
- Also don't forget to check the
15
- <a href="https://huggingface.co/docs/hub/spaces" target="_blank">Spaces documentation</a>.
16
- </p>
17
- </div>
18
- </body>
19
- </html>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="ko">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Passive Sonar Audio Synthesizer</title>
7
+ <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
8
+ <style>
9
+ :root {
10
+ --sonar-primary: #00ffaa;
11
+ --sonar-secondary: #008f60;
12
+ --sonar-bg: #021016;
13
+ --panel-bg: rgba(2, 25, 35, 0.85);
14
+ --text-muted: #6fa8a1;
15
+ --accent-alert: #ff4d4d;
16
+ --font-main: 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
17
+ }
18
+
19
+ * {
20
+ box-sizing: border-box;
21
+ margin: 0;
22
+ padding: 0;
23
+ }
24
+
25
+ body {
26
+ background-color: var(--sonar-bg);
27
+ color: var(--sonar-primary);
28
+ font-family: var(--font-main);
29
+ height: 100vh;
30
+ overflow: hidden;
31
+ display: flex;
32
+ flex-direction: column;
33
+ background-image:
34
+ radial-gradient(circle at 50% 50%, rgba(0, 255, 170, 0.05) 0%, transparent 60%),
35
+ linear-gradient(0deg, rgba(0,0,0,0.8) 0%, transparent 100%);
36
+ }
37
+
38
+ /* Header */
39
+ header {
40
+ padding: 1rem 2rem;
41
+ display: flex;
42
+ justify-content: space-between;
43
+ align-items: center;
44
+ border-bottom: 1px solid rgba(0, 255, 170, 0.3);
45
+ background: rgba(0, 0, 0, 0.5);
46
+ backdrop-filter: blur(5px);
47
+ z-index: 10;
48
+ }
49
+
50
+ .brand {
51
+ font-size: 1.2rem;
52
+ font-weight: 700;
53
+ letter-spacing: 2px;
54
+ text-transform: uppercase;
55
+ display: flex;
56
+ align-items: center;
57
+ gap: 10px;
58
+ }
59
+
60
+ .brand i {
61
+ animation: pulse 2s infinite;
62
+ }
63
+
64
+ .anycoder-link {
65
+ color: var(--text-muted);
66
+ text-decoration: none;
67
+ font-size: 0.9rem;
68
+ transition: color 0.3s;
69
+ border: 1px solid rgba(111, 168, 161, 0.3);
70
+ padding: 5px 12px;
71
+ border-radius: 20px;
72
+ }
73
+
74
+ .anycoder-link:hover {
75
+ color: var(--sonar-primary);
76
+ border-color: var(--sonar-primary);
77
+ background: rgba(0, 255, 170, 0.1);
78
+ }
79
+
80
+ /* Main Layout */
81
+ main {
82
+ flex: 1;
83
+ display: grid;
84
+ grid-template-columns: 300px 1fr 300px;
85
+ gap: 1rem;
86
+ padding: 1rem;
87
+ overflow: hidden;
88
+ }
89
+
90
+ /* Panels */
91
+ .panel {
92
+ background: var(--panel-bg);
93
+ border: 1px solid rgba(0, 255, 170, 0.2);
94
+ border-radius: 8px;
95
+ padding: 1.5rem;
96
+ display: flex;
97
+ flex-direction: column;
98
+ gap: 1.5rem;
99
+ overflow-y: auto;
100
+ box-shadow: 0 4px 15px rgba(0,0,0,0.5);
101
+ backdrop-filter: blur(10px);
102
+ }
103
+
104
+ .panel-title {
105
+ font-size: 1rem;
106
+ text-transform: uppercase;
107
+ border-bottom: 1px solid var(--sonar-secondary);
108
+ padding-bottom: 0.5rem;
109
+ margin-bottom: 0.5rem;
110
+ display: flex;
111
+ align-items: center;
112
+ gap: 10px;
113
+ }
114
+
115
+ /* Visualizer Area */
116
+ .visualizer-container {
117
+ grid-column: 2;
118
+ display: flex;
119
+ flex-direction: column;
120
+ position: relative;
121
+ border: 1px solid var(--sonar-secondary);
122
+ border-radius: 8px;
123
+ background: #000;
124
+ overflow: hidden;
125
+ }
126
+
127
+ canvas {
128
+ width: 100%;
129
+ height: 100%;
130
+ display: block;
131
+ }
132
+
133
+ .sonar-overlay {
134
+ position: absolute;
135
+ top: 50%;
136
+ left: 50%;
137
+ transform: translate(-50%, -50%);
138
+ width: 100%;
139
+ height: 100%;
140
+ background: conic-gradient(from 0deg, transparent 0deg, transparent 280deg, rgba(0, 255, 170, 0.1) 360deg);
141
+ border-radius: 50%;
142
+ animation: spin 4s linear infinite;
143
+ pointer-events: none;
144
+ opacity: 0.3;
145
+ mix-blend-mode: screen;
146
+ }
147
+
148
+ /* Controls */
149
+ .control-group {
150
+ margin-bottom: 1rem;
151
+ }
152
+
153
+ .control-label {
154
+ display: flex;
155
+ justify-content: space-between;
156
+ font-size: 0.8rem;
157
+ color: var(--text-muted);
158
+ margin-bottom: 0.3rem;
159
+ }
160
+
161
+ input[type="range"] {
162
+ -webkit-appearance: none;
163
+ width: 100%;
164
+ height: 6px;
165
+ background: rgba(0, 255, 170, 0.2);
166
+ border-radius: 3px;
167
+ outline: none;
168
+ }
169
+
170
+ input[type="range"]::-webkit-slider-thumb {
171
+ -webkit-appearance: none;
172
+ width: 16px;
173
+ height: 16px;
174
+ border-radius: 50%;
175
+ background: var(--sonar-primary);
176
+ cursor: pointer;
177
+ box-shadow: 0 0 10px var(--sonar-primary);
178
+ margin-top: -5px;
179
+ }
180
+
181
+ input[type="range"]::-webkit-slider-runnable-track {
182
+ width: 100%;
183
+ height: 6px;
184
+ cursor: pointer;
185
+ }
186
+
187
+ /* Buttons */
188
+ .btn {
189
+ background: transparent;
190
+ border: 1px solid var(--sonar-primary);
191
+ color: var(--sonar-primary);
192
+ padding: 10px 20px;
193
+ font-family: inherit;
194
+ text-transform: uppercase;
195
+ cursor: pointer;
196
+ transition: all 0.3s ease;
197
+ border-radius: 4px;
198
+ font-weight: bold;
199
+ width: 100%;
200
+ display: flex;
201
+ align-items: center;
202
+ justify-content: center;
203
+ gap: 8px;
204
+ }
205
+
206
+ .btn:hover {
207
+ background: rgba(0, 255, 170, 0.2);
208
+ box-shadow: 0 0 15px rgba(0, 255, 170, 0.4);
209
+ }
210
+
211
+ .btn.active {
212
+ background: var(--sonar-primary);
213
+ color: #000;
214
+ }
215
+
216
+ .btn-trigger {
217
+ background: rgba(0, 255, 170, 0.1);
218
+ margin-top: 5px;
219
+ }
220
+
221
+ /* Start Overlay */
222
+ #start-overlay {
223
+ position: fixed;
224
+ top: 0;
225
+ left: 0;
226
+ width: 100%;
227
+ height: 100%;
228
+ background: rgba(2, 16, 22, 0.95);
229
+ display: flex;
230
+ flex-direction: column;
231
+ justify-content: center;
232
+ align-items: center;
233
+ z-index: 100;
234
+ backdrop-filter: blur(10px);
235
+ }
236
+
237
+ #start-overlay h1 {
238
+ font-size: 3rem;
239
+ margin-bottom: 1rem;
240
+ text-shadow: 0 0 20px var(--sonar-primary);
241
+ }
242
+
243
+ #start-overlay p {
244
+ color: var(--text-muted);
245
+ margin-bottom: 2rem;
246
+ max-width: 600px;
247
+ text-align: center;
248
+ line-height: 1.6;
249
+ }
250
+
251
+ #start-btn {
252
+ font-size: 1.5rem;
253
+ padding: 15px 40px;
254
+ border: 2px solid var(--sonar-primary);
255
+ }
256
+
257
+ /* Responsive */
258
+ @media (max-width: 1024px) {
259
+ main {
260
+ grid-template-columns: 1fr 1fr;
261
+ grid-template-rows: auto 1fr;
262
+ }
263
+ .visualizer-container {
264
+ grid-column: 1 / -1;
265
+ grid-row: 1;
266
+ height: 300px;
267
+ }
268
+ }
269
+
270
+ @media (max-width: 768px) {
271
+ main {
272
+ grid-template-columns: 1fr;
273
+ overflow-y: auto;
274
+ }
275
+ .visualizer-container {
276
+ height: 250px;
277
+ }
278
+ body {
279
+ overflow: auto;
280
+ }
281
+ }
282
+
283
+ /* Animations */
284
+ @keyframes spin {
285
+ from { transform: translate(-50%, -50%) rotate(0deg); }
286
+ to { transform: translate(-50%, -50%) rotate(360deg); }
287
+ }
288
+
289
+ @keyframes pulse {
290
+ 0% { opacity: 0.6; text-shadow: 0 0 5px var(--sonar-primary); }
291
+ 50% { opacity: 1; text-shadow: 0 0 20px var(--sonar-primary); }
292
+ 100% { opacity: 0.6; text-shadow: 0 0 5px var(--sonar-primary); }
293
+ }
294
+
295
+ .status-led {
296
+ width: 10px;
297
+ height: 10px;
298
+ border-radius: 50%;
299
+ background-color: #333;
300
+ display: inline-block;
301
+ margin-right: 8px;
302
+ box-shadow: inset 0 0 2px rgba(0,0,0,0.5);
303
+ }
304
+
305
+ .status-led.on {
306
+ background-color: var(--sonar-primary);
307
+ box-shadow: 0 0 8px var(--sonar-primary);
308
+ }
309
+ </style>
310
+ </head>
311
+ <body>
312
+
313
+ <!-- Audio Context Start Overlay -->
314
+ <div id="start-overlay">
315
+ <h1><i class="fa-solid fa-water"></i> PASSIVE SONAR SYNTH</h1>
316
+ <p>
317
+ 패시브 소나 시뮬레이션을 위한 고품질 수중 음원 합성기입니다.<br>
318
+ Web Audio API를 사용하여 고래 소리, 선박 프로펠러 소음, 바다 배경음을 실시간으로 생성합니다.
319
+ </p>
320
+ <button id="start-btn" class="btn">
321
+ <i class="fa-solid fa-power-off"></i> 시스템 가동 (Start System)
322
+ </button>
323
+ </div>
324
+
325
+ <header>
326
+ <div class="brand">
327
+ <i class="fa-solid fa-radar"></i>
328
+ <span>Sonar Audio Lab</span>
329
+ </div>
330
+ <a href="https://huggingface.co/spaces/akhaliq/anycoder" target="_blank" class="anycoder-link">Built with anycoder</a>
331
+ </header>
332
+
333
+ <main>
334
+ <!-- Left Panel: Environment & Biologic -->
335
+ <div class="panel">
336
+ <div class="panel-title">
337
+ <i class="fa-solid fa-globe-americas"></i> 환경 (Environment)
338
+ </div>
339
+
340
+ <!-- Ocean Ambience -->
341
+ <div class="control-group">
342
+ <div class="control-label">
343
+ <span><span class="status-led" id="led-ocean"></span>해저 배경음 (Ambience)</span>
344
+ <span id="val-ocean-vol">0%</span>
345
+ </div>
346
+ <input type="range" id="ocean-vol" min="0" max="1" step="0.01" value="0">
347
+ <button class="btn btn-trigger" id="toggle-ocean" style="margin-top:5px; font-size: 0.8rem;">
348
+ ON / OFF
349
+ </button>
350
+ </div>
351
+
352
+ <div class="panel-title" style="margin-top: 1rem;">
353
+ <i class="fa-solid fa-fish"></i> 생물 (Biologic)
354
+ </div>
355
+
356
+ <!-- Whale Synth -->
357
+ <div class="control-group">
358
+ <div class="control-label">
359
+ <span>고래 소리 크기 (Whale Volume)</span>
360
+ <span id="val-whale-vol">80%</span>
361
+ </div>
362
+ <input type="range" id="whale-vol" min="0" max="1" step="0.01" value="0.8">
363
+ </div>
364
+
365
+ <div class="control-group">
366
+ <div class="control-label">
367
+ <span>호출 빈도 (Call Pitch)</span>
368
+ <span id="val-whale-pitch">Low</span>
369
+ </div>
370
+ <input type="range" id="whale-pitch" min="100" max="600" step="10" value="300">
371
+ </div>
372
+
373
+ <button class="btn" id="btn-whale-call">
374
+ <i class="fa-solid fa-bullhorn"></i> 고래 호출 (Trigger Call)
375
+ </button>
376
+
377
+ <div class="control-group" style="margin-top:10px;">
378
+ <div class="control-label">
379
+ <span>클릭음 (Clicks)</span>
380
+ </div>
381
+ <button class="btn btn-trigger" id="btn-whale-click">
382
+ <i class="fa-regular fa-circle-dot"></i> 클릭음 발생
383
+ </button>
384
+ </div>
385
+ </div>
386
+
387
+ <!-- Center: Visualizer -->
388
+ <div class="visualizer-container">
389
+ <canvas id="scope"></canvas>
390
+ <div class="sonar-overlay"></div>
391
+ <div style="position: absolute; bottom: 10px; left: 10px; font-size: 0.8rem; color: var(--sonar-primary);">
392
+ LOFARGRAM PREVIEW
393
+ </div>
394
+ </div>
395
+
396
+ <!-- Right Panel: Mechanical Target -->
397
+ <div class="panel">
398
+ <div class="panel-title">
399
+ <i class="fa-solid fa-ship"></i> 표적 소음 (Target Noise)
400
+ </div>
401
+
402
+ <div class="control-group">
403
+ <button class="btn" id="toggle-ship">
404
+ <i class="fa-solid fa-power-off"></i> 엔진 시동 (Engine Start)
405
+ </button>
406
+ </div>
407
+
408
+ <!-- Propeller RPM -->
409
+ <div class="control-group">
410
+ <div class="control-label">
411
+ <span>프로펠러 속도 (RPM)</span>
412
+ <span id="val-rpm">Stopped</span>
413
+ </div>
414
+ <input type="range" id="ship-rpm" min="0" max="15" step="0.1" value="0">
415
+ </div>
416
+
417
+ <!-- Engine Tone -->
418
+ <div class="control-group">
419
+ <div class="control-label">
420
+ <span>엔진 톤 (Engine Tone)</span>
421
+ <span id="val-engine-tone">Low</span>
422
+ </div>
423
+ <input type="range" id="engine-tone" min="50" max="200" step="1" value="80">
424
+ </div>
425
+
426
+ <!-- Cavitation -->
427
+ <div class="control-group">
428
+ <div class="control-label">
429
+ <span>공동 현상 (Cavitation Noise)</span>
430
+ <span id="val-cavitation">50%</span>
431
+ </div>
432
+ <input type="range" id="cavitation-vol" min="0" max="1" step="0.01" value="0.5">
433
+ </div>
434
+
435
+ <!-- Distance Filter -->
436
+ <div class="control-group" style="margin-top: 1rem; border-top: 1px solid var(--sonar-secondary); padding-top: 1rem;">
437
+ <div class="control-label">
438
+ <span>거리 감쇠 (Distance Filter)</span>
439
+ <span id="val-distance">Close</span>
440
+ </div>
441
+ <input type="range" id="distance-filter" min="100" max="2000" step="10" value="2000">
442
+ <p style="font-size: 0.7rem; color: var(--text-muted); margin-top: 5px;">
443
+ 낮을수록 멀리 있는 소리 (Low Pass)
444
+ </p>
445
+ </div>
446
+ </div>
447
+ </main>
448
+
449
+ <script>
450
+ /**
451
+ * Sonar Audio Synthesizer Logic
452
+ * Uses Web Audio API to generate sounds procedurally.
453
+ */
454
+
455
+ // Audio Context
456
+ let audioCtx;
457
+ let masterGain;
458
+ let analyser;
459
+ let isSystemOn = false;
460
+
461
+ // Visualization
462
+ const canvas = document.getElementById('scope');
463
+ const canvasCtx = canvas.getContext('2d');
464
+
465
+ // State
466
+ let oceanNode = null;
467
+ let shipNodes = null;
468
+
469
+ // --- Initialization ---
470
+ const initAudio = () => {
471
+ if (audioCtx) return;
472
+
473
+ const AudioContext = window.AudioContext || window.webkitAudioContext;
474
+ audioCtx = new AudioContext();
475
+
476
+ // Master Gain (Volume)
477
+ masterGain = audioCtx.createGain();
478
+ masterGain.gain.value = 0.8;
479
+
480
+ // Analyser for Visualization
481
+ analyser = audioCtx.createAnalyser();
482
+ analyser.fftSize = 2048;
483
+
484
+ masterGain.connect(analyser);
485
+ analyser.connect(audioCtx.destination);
486
+
487
+ drawVisualizer();
488
+ isSystemOn = true;
489
+ };
490
+
491
+ document.getElementById('start-btn').addEventListener('click', () => {
492
+ initAudio();
493
+ if(audioCtx.state === 'suspended') audioCtx.resume();
494
+ document.getElementById('start-overlay').style.opacity = '0';
495
+ setTimeout(() => {
496
+ document.getElementById('start-overlay').style.display = 'none';
497
+ }, 500);
498
+ });
499
+
500
+ // --- Helper: Create Noise Buffer ---
501
+ function createNoiseBuffer() {
502
+ const bufferSize = audioCtx.sampleRate * 2; // 2 seconds
503
+ const buffer = audioCtx.createBuffer(1, bufferSize, audioCtx.sampleRate);
504
+ const data = buffer.getChannelData(0);
505
+ for (let i = 0; i < bufferSize; i++) {
506
+ data[i] = Math.random() * 2 - 1; // White noise
507
+ }
508
+ return buffer;
509
+ }
510
+
511
+ // --- Sound Module: Ocean Ambience ---
512
+ // Pinkish noise with heavy lowpass filter
513
+ let isOceanOn = false;
514
+
515
+ function toggleOcean() {
516
+ if (!audioCtx) return;
517
+
518
+ if (isOceanOn) {
519
+ // Stop
520
+ if (oceanNode) {
521
+ oceanNode.source.stop();
522
+ oceanNode.source.disconnect();
523
+ oceanNode = null;
524
+ }
525
+ isOceanOn = false;
526
+ document.getElementById('led-ocean').classList.remove('on');
527
+ } else {
528
+ // Start
529
+ const buffer = createNoiseBuffer();
530
+ const source = audioCtx.createBufferSource();
531
+ source.buffer = buffer;
532
+ source.loop = true;
533
+
534
+ // Filter to make it sound "underwater" (Pink/Brown noise approx)
535
+ const filter = audioCtx.createBiquadFilter();
536
+ filter.type = 'lowpass';
537
+ filter.frequency.value = 400;
538
+
539
+ const gain = audioCtx.createGain();
540
+ gain.gain.value = document.getElementById('ocean-vol').value;
541
+
542
+ source.connect(filter);
543
+ filter.connect(gain);
544
+ gain.connect(masterGain);
545
+
546
+ oceanNode = { source, gain, filter };
547
+ source.start();
548
+ isOceanOn = true;
549
+ document.getElementById('led-ocean').classList.add('on');
550
+ }
551
+ }
552
+
553
+ document.getElementById('toggle-ocean').addEventListener('click', toggleOcean);
554
+ document.getElementById('ocean-vol').addEventListener('input', (e) => {
555
+ document.getElementById('val-ocean-vol').innerText = Math.round(e.target.value * 100) + '%';
556
+ if (oceanNode) oceanNode.gain.gain.setTargetAtTime(e.target.value, audioCtx.currentTime, 0.1);
557
+ });
558
+
559
+ // --- Sound Module: Whale Synth ---
560
+ // Sine waves with pitch bending and reverb/delay
561
+
562
+ function playWhaleCall() {
563
+ if (!audioCtx) return;
564
+
565
+ const t = audioCtx.currentTime;
566
+ const basePitch = parseInt(document.getElementById('whale-pitch').value);
567
+ const volume = parseFloat(document.getElementById('whale-vol').value);
568
+
569
+ const osc = audioCtx.createOscillator();
570
+ const gain = audioCtx.createGain();
571
+ const filter = audioCtx.createBiquadFilter(); // Smooth out edges
572
+
573
+ // Simple Reverb using Delay
574
+ const delay = audioCtx.createDelay();
575
+ const delayGain = audioCtx.createGain();
576
+
577
+ osc.type = 'sine';
578
+
579
+ // Pitch Envelope (The "Moan")
580
+ osc.frequency.setValueAtTime(basePitch, t);
581
+ osc.frequency.exponentialRampToValueAtTime(basePitch * 1.5, t + 1.5); // Pitch up
582
+ osc.frequency.exponentialRampToValueAtTime(basePitch * 0.8, t + 3.0); // Pitch down
583
+
584
+ // Amplitude Envelope
585
+ gain.gain.setValueAtTime(0, t);
586
+ gain.gain.linearRampToValueAtTime(volume, t + 0.5); // Attack
587
+ gain.gain.linearRampToValueAtTime(volume * 0.8, t + 2.0); // Sustain-ish
588
+ gain.gain.exponentialRampToValueAtTime(0.01, t + 4.0); // Release
589
+
590
+ // Filter
591
+ filter.type = 'lowpass';
592
+ filter.frequency.value = 1500;
593
+
594
+ // Delay settings (Echo)
595
+ delay.delayTime.value = 0.4;
596
+ delayGain.gain.value = 0.4;
597
+
598
+ // Connections
599
+ osc.connect(filter);
600
+ filter.connect(gain);
601
+
602
+ // Dry path
603
+ gain.connect(masterGain);
604
+
605
+ // Wet path (Echo)
606
+ gain.connect(delay);
607
+ delay.connect(delayGain);
608
+ delayGain.connect(delay); // Feedback loop
609
+ delayGain.connect(masterGain);
610
+
611
+ osc.start(t);
612
+ osc.stop(t + 5.0);
613
+ }
614
+
615
+ function playWhaleClick() {
616
+ if (!audioCtx) return;
617
+ const t = audioCtx.currentTime;
618
+ const volume = parseFloat(document.getElementById('whale-vol').value);
619
+
620
+ // Create a burst of high frequency noise/sine
621
+ const osc = audioCtx.createOscillator();
622
+ const gain = audioCtx.createGain();
623
+
624
+ osc.type = 'triangle';
625
+ osc.frequency.setValueAtTime(3000, t);
626
+ osc.frequency.exponentialRampToValueAtTime(100, t + 0.05);
627
+
628
+ gain.gain.setValueAtTime(volume, t);
629
+ gain.gain.exponentialRampToValueAtTime(0.01, t + 0.05);
630
+
631
+ osc.connect(gain);
632
+ gain.connect(masterGain);
633
+
634
+ osc.start(t);
635
+ osc.stop(t + 0.1);
636
+ }
637
+
638
+ document.getElementById('btn-whale-call').addEventListener('click', playWhaleCall);
639
+ document.getElementById('btn-whale-click').addEventListener('click', () => {
640
+ // Play a sequence of clicks
641
+ let count = 0;
642
+ const interval = setInterval(() => {
643
+ playWhaleClick();
644
+ count++;
645
+ if(count > 5) clearInterval(interval);
646
+ }, 150); // rapid clicks
647
+ });
648
+
649
+ document.getElementById('whale-pitch').addEventListener('input', (e) => {
650
+ document.getElementById('val-whale-pitch').innerText = e.target.value + 'Hz';
651
+ });
652
+ document.getElementById('whale-vol').addEventListener('input', (e) => {
653
+ document.getElementById('val-whale-vol').innerText = Math.round(e.target.value * 100) + '%';
654
+ });
655
+
656
+
657
+ // --- Sound Module: Ship Propeller ---
658
+ // Complex: Engine drone (Oscillator) + Cavitation (Modulated Noise)
659
+
660
+ let isShipOn = false;
661
+
662
+ function updateShipParams() {
663
+ if (!shipNodes) return;
664
+
665
+ const rpm = parseFloat(document.getElementById('ship-rpm').value);
666
+ const tone = parseFloat(document.getElementById('engine-tone').value);
667
+ const cavVol = parseFloat(document.getElementById('cavitation-vol').value);
668
+ const distFreq = parseFloat(document.getElementById('distance-filter').value);
669
+
670
+ // Update Engine Tone
671
+ shipNodes.engineOsc.frequency.setTargetAtTime(tone, audioCtx.currentTime, 0.2);
672
+
673
+ // Update RPM (LFO speed)
674
+ // If RPM is 0, stop sound effectively
675
+ const lfoFreq = rpm;
676
+ shipNodes.lfo.frequency.setTargetAtTime(lfoFreq, audioCtx.currentTime, 0.5);
677
+
678
+ // Update Volumes based on RPM (faster = louder generally)
679
+ const engineBaseVol = rpm > 0 ? 0.3 : 0;
680
+ shipNodes.engineGain.gain.setTargetAtTime(engineBaseVol, audioCtx.currentTime, 0.5);
681
+
682
+ const cavBaseVol = rpm > 0 ? cavVol : 0;
683
+ shipNodes.cavitationMasterGain.gain.setTargetAtTime(cavBaseVol, audioCtx.currentTime, 0.5);
684
+
685
+ // Update Distance Filter
686
+ shipNodes.masterFilter.frequency.setTargetAtTime(distFreq, audioCtx.currentTime, 0.2);
687
+ }
688
+
689
+ function toggleShip() {
690
+ if (!audioCtx) return;
691
+ const btn = document.getElementById('toggle-ship');
692
+
693
+ if (isShipOn) {
694
+ // Stop
695
+ shipNodes.engineOsc.stop();
696
+ shipNodes.lfo.stop();
697
+ shipNodes.noiseSource.stop();
698
+
699
+ // Clean disconnect
700
+ shipNodes.masterFilter.disconnect();
701
+
702
+ shipNodes = null;
703
+ isShipOn = false;
704
+ btn.classList.remove('active');
705
+ btn.innerHTML = '<i class="fa-solid fa-power-off"></i> 엔진 시동 (Engine Start)';
706
+ } else {
707
+ // Start
708
+ const t = audioCtx.currentTime;
709
+
710
+ // 1. Master Filter (Distance simulation)
711
+ const masterFilter = audioCtx.createBiquadFilter();
712
+ masterFilter.type = 'lowpass';
713
+ masterFilter.frequency.value = 2000;
714
+ masterFilter.connect(masterGain);
715
+
716
+ // 2. LFO (The RPM Rhythm)
717
+ const lfo = audioCtx.createOscillator();
718
+ lfo.type = 'sine';
719
+ lfo.frequency.value = 0; // Start at 0
720
+
721
+ // 3. Engine Drone (Low Sawtooth)
722
+ const engineOsc = audioCtx.createOscillator();
723
+ engineOsc.type = 'sawtooth';
724
+ engineOsc.frequency.value = 100;
725
+
726
+ const engineGain = audioCtx.createGain();
727
+ engineGain.gain.value = 0;
728
+
729
+ // Engine LFO modulation (Engine throbs slightly)
730
+ const engineLfoGain = audioCtx.createGain();
731
+ engineLfoGain.gain.value = 0.3; // Depth of modulation
732
+
733
+ lfo.connect(engineLfoGain);
734
+ engineLfoGain.connect(engineGain.gain); // AM Synthesis
735
+
736
+ engineOsc.connect(engineGain);
737
+ engineGain.connect(masterFilter);
738
+
739
+ // 4. Cavitation (White Noise modulated by LFO)
740
+ const noiseBuffer = createNoiseBuffer();
741
+ const noiseSource = audioCtx.createBufferSource();
742
+ noiseSource.buffer = noiseBuffer;
743
+ noiseSource.loop = true;
744
+
745
+ const noiseFilter = audioCtx.createBiquadFilter();
746
+ noiseFilter.type = 'bandpass';
747
+ noiseFilter.frequency.value = 800;
748
+ noiseFilter.Q.value = 1;
749
+
750
+ const cavitationGain = audioCtx.createGain();
751
+ cavitationGain.gain.value = 0; // Controlled by LFO
752
+
753
+ // To map LFO (-1 to 1) to Gain (0 to 1), we need a bias
754
+ // But simple AM is fine here. We want the swoosh-swoosh.
755
+ const cavLfoGain = audioCtx.createGain();
756
+ cavLfoGain.gain.value = 0.8; // Depth
757
+
758
+ const cavitationMasterGain = audioCtx.createGain();
759
+ cavitationMasterGain.gain.value = 0; // Overall volume
760
+
761
+ lfo.connect(cavLfoGain);
762
+ cavLfoGain.connect(cavitationGain.gain);
763
+
764
+ noiseSource.connect(noiseFilter);
765
+ noiseFilter.connect(cavitationGain);
766
+ cavitationGain.connect(cavitationMasterGain);
767
+ cavitationMasterGain.connect(masterFilter);
768
+
769
+ // Start everything
770
+ lfo.start(t);
771
+ engineOsc.start(t);
772
+ noiseSource.start(t);
773
+
774
+ shipNodes = {
775
+ masterFilter, lfo, engineOsc, engineGain,
776
+ noiseSource, cavitationMasterGain
777
+ };
778
+
779
+ isShipOn = true;
780
+ btn.classList.add('active');
781
+ btn.innerHTML = '<i class="fa-solid fa-stop"></i> 엔진 정지 (Engine Stop)';
782
+
783
+ // Apply initial slider values
784
+ updateShipParams();
785
+ }
786
+ }
787
+
788
+ document.getElementById('toggle-ship').addEventListener('click', toggleShip);
789
+
790
+ // Ship Controls Listeners
791
+ ['ship-rpm', 'engine-tone', 'cavitation-vol', 'distance-filter'].forEach(id => {
792
+ document.getElementById(id).addEventListener('input', (e) => {
793
+ // Update UI labels
794
+ if(id === 'ship-rpm') document.getElementById('val-rpm').innerText = e.target.value + ' Hz';
795
+ if(id === 'engine-tone') document.getElementById('val-engine-tone').innerText = e.target.value + ' Hz';
796
+ if(id === 'cavitation-vol') document.getElementById('val-cavitation').innerText = Math.round(e.target.value*100) + '%';
797
+ if(id === 'distance-filter') document.getElementById('val-distance').innerText = e.target.value + ' Hz';
798
+
799
+ updateShipParams();
800
+ });
801
+ });
802
+
803
+
804
+ // --- Visualizer Logic ---
805
+ function drawVisualizer() {
806
+ requestAnimationFrame(drawVisualizer);
807
+
808
+ const bufferLength = analyser.frequencyBinCount;
809
+ const dataArray = new Uint8Array(bufferLength);
810
+
811
+ analyser.getByteTimeDomainData(dataArray);
812
+
813
+ canvasCtx.fillStyle = 'rgba(0, 15, 20, 0.2)'; // Trail effect
814
+ canvasCtx.fillRect(0, 0, canvas.width, canvas.height);
815
+
816
+ canvasCtx.lineWidth = 2;
817
+ canvasCtx.strokeStyle = '#00ffaa';
818
+ canvasCtx.shadowBlur = 5;
819
+ canvasCtx.shadowColor = '#00ffaa';
820
+
821
+ canvasCtx.beginPath();
822
+
823
+ const sliceWidth = canvas.width * 1.0 / bufferLength;
824
+ let x = 0;
825
+
826
+ for (let i = 0; i < bufferLength; i++) {
827
+ const v = dataArray[i] / 128.0;
828
+ const y = v * canvas.height / 2;
829
+
830
+ if (i === 0) {
831
+ canvasCtx.moveTo(x, y);
832
+ } else {
833
+ canvasCtx.lineTo(x, y);
834
+ }
835
+
836
+ x += sliceWidth;
837
+ }
838
+
839
+ canvasCtx.lineTo(canvas.width, canvas.height / 2);
840
+ canvasCtx.stroke();
841
+ }
842
+
843
+ // Resize Canvas
844
+ function resizeCanvas() {
845
+ const container = document.querySelector('.visualizer-container');
846
+ canvas.width = container.clientWidth;
847
+ canvas.height = container.clientHeight;
848
+ }
849
+ window.addEventListener('resize', resizeCanvas);
850
+ resizeCanvas();
851
+
852
+ </script>
853
+ </body>
854
+ </html>