ttg73ml commited on
Commit
d3b6483
Β·
verified Β·
1 Parent(s): 23719bb

Upload fake_audio_cutter_demo.html

Browse files
Files changed (1) hide show
  1. fake_audio_cutter_demo.html +1577 -0
fake_audio_cutter_demo.html ADDED
@@ -0,0 +1,1577 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en" data-theme="dark">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover">
6
+ <title>Audio Waveform Studio - Pro Trimmer & Recorder</title>
7
+ <style>
8
+ /* ═══════════════════════════════════════════════════════════════
9
+ CSS CUSTOM PROPERTIES (SMOOTH DARK & LIGHT MODE)
10
+ ═══════════════════════════════════════════════════════════════ */
11
+ :root {
12
+ /* DARK THEME (DEFAULT) */
13
+ --bg-base: #090d16;
14
+ --bg-gradient: radial-gradient(circle at 50% 0%, #1e1b4b 0%, #090d16 100%);
15
+ --card-bg: rgba(18, 24, 38, 0.92);
16
+ --card-border: rgba(255, 255, 255, 0.08);
17
+ --card-shadow: 0 24px 60px rgba(0, 0, 0, 0.6);
18
+ --text-main: #f8fafc;
19
+ --text-sub: #cbd5e1;
20
+ --text-muted: #94a3b8;
21
+ --panel-bg: rgba(11, 15, 25, 0.55);
22
+ --panel-border: rgba(255, 255, 255, 0.06);
23
+ --dropzone-bg: rgba(15, 23, 42, 0.4);
24
+ --dropzone-border: rgba(99, 102, 241, 0.4);
25
+ --track-bg: #0b0f19;
26
+ --track-border: rgba(255, 255, 255, 0.08);
27
+ --wave-bar: #818cf8;
28
+ --wave-baseline: rgba(255, 255, 255, 0.12);
29
+ --overlay-bg: rgba(99, 102, 241, 0.25);
30
+ --overlay-border: #a855f7;
31
+ --handle-bar: #ffffff;
32
+ --playhead-color: #38bdf8;
33
+ --btn-bg: rgba(255, 255, 255, 0.06);
34
+ --btn-border: rgba(255, 255, 255, 0.1);
35
+ --btn-hover: rgba(255, 255, 255, 0.16);
36
+ --btn-text: #f1f5f9;
37
+ --chip-bg: rgba(99, 102, 241, 0.15);
38
+ --chip-border: rgba(99, 102, 241, 0.35);
39
+ --chip-text: #c7d2fe;
40
+ --log-bg: rgba(11, 15, 25, 0.7);
41
+ --log-border: rgba(255, 255, 255, 0.05);
42
+ --danger-glow: rgba(239, 68, 68, 0.45);
43
+ }
44
+
45
+ [data-theme="light"] {
46
+ /* 100% BULLETPROOF LIGHT THEME (ALL TEXT IS SHARP BLACK / SLATE) */
47
+ --bg-base: #f1f5f9;
48
+ --bg-gradient: radial-gradient(circle at 50% 0%, #e0e7ff 0%, #f1f5f9 100%);
49
+ --card-bg: #ffffff;
50
+ --card-border: #cbd5e1;
51
+ --card-shadow: 0 20px 45px rgba(99, 102, 241, 0.12);
52
+ --text-main: #0f172a; /* Deep solid black/slate */
53
+ --text-sub: #1e293b; /* Crisp dark slate */
54
+ --text-muted: #475569; /* Medium dark slate */
55
+ --panel-bg: #f8fafc;
56
+ --panel-border: #e2e8f0;
57
+ --dropzone-bg: #f8fafc;
58
+ --dropzone-border: #818cf8;
59
+ --track-bg: #f8fafc;
60
+ --track-border: #cbd5e1;
61
+ --wave-bar: #6366f1;
62
+ --wave-baseline: rgba(100, 116, 139, 0.35);
63
+ --overlay-bg: rgba(99, 102, 241, 0.2);
64
+ --overlay-border: #4f46e5;
65
+ --handle-bar: #1e1b4b;
66
+ --playhead-color: #0284c7;
67
+ --btn-bg: #f8fafc;
68
+ --btn-border: #cbd5e1;
69
+ --btn-hover: #e2e8f0;
70
+ --btn-text: #0f172a;
71
+ --chip-bg: #eef2ff;
72
+ --chip-border: #c7d2fe;
73
+ --chip-text: #3730a3;
74
+ --log-bg: #f8fafc;
75
+ --log-border: #e2e8f0;
76
+ --danger-glow: rgba(239, 68, 68, 0.25);
77
+ }
78
+
79
+ /* ─── RESET & GLOBAL BASE ─── */
80
+ * {
81
+ box-sizing: border-box;
82
+ margin: 0;
83
+ padding: 0;
84
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
85
+ -webkit-tap-highlight-color: transparent;
86
+ user-select: none;
87
+ }
88
+
89
+ body {
90
+ background: var(--bg-base);
91
+ background-image: var(--bg-gradient);
92
+ min-height: 100vh;
93
+ min-height: -webkit-fill-available;
94
+ display: flex;
95
+ justify-content: center;
96
+ align-items: center;
97
+ padding: 24px 16px;
98
+ color: var(--text-main);
99
+ transition: background 0.3s ease, color 0.3s ease;
100
+ }
101
+
102
+ /* ─── VECTOR SVG ICON STYLING (100% VECTOR, ZERO EMOJIS) ─── */
103
+ .svg-icon {
104
+ width: 18px;
105
+ height: 18px;
106
+ stroke-width: 2.2;
107
+ stroke: currentColor;
108
+ fill: none;
109
+ stroke-linecap: round;
110
+ stroke-linejoin: round;
111
+ display: inline-block;
112
+ vertical-align: middle;
113
+ flex-shrink: 0;
114
+ transition: transform 0.2s cubic-bezier(0.4, 0, 0.2, 1);
115
+ }
116
+
117
+ .svg-icon.icon-lg {
118
+ width: 24px;
119
+ height: 24px;
120
+ }
121
+
122
+ .svg-icon.icon-xl {
123
+ width: 38px;
124
+ height: 38px;
125
+ }
126
+
127
+ /* ─── MAIN APP CONTAINER ─── */
128
+ .app-container {
129
+ width: 100%;
130
+ max-width: 880px;
131
+ background: var(--card-bg);
132
+ border: 1px solid var(--card-border);
133
+ border-radius: 24px;
134
+ box-shadow: var(--card-shadow);
135
+ padding: 32px;
136
+ display: flex;
137
+ flex-direction: column;
138
+ gap: 22px;
139
+ backdrop-filter: blur(20px);
140
+ -webkit-backdrop-filter: blur(20px);
141
+ transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
142
+ position: relative;
143
+ overflow: hidden;
144
+ }
145
+
146
+ /* ─── HEADER ROW & THEME TOGGLE ─── */
147
+ .header-row {
148
+ display: flex;
149
+ align-items: center;
150
+ justify-content: space-between;
151
+ border-bottom: 1px solid var(--panel-border);
152
+ padding-bottom: 18px;
153
+ }
154
+
155
+ .title-meta h1 {
156
+ font-size: 1.55rem;
157
+ font-weight: 800;
158
+ background: linear-gradient(135deg, #a855f7 0%, #6366f1 100%);
159
+ -webkit-background-clip: text;
160
+ -webkit-text-fill-color: transparent;
161
+ letter-spacing: -0.5px;
162
+ }
163
+
164
+ .title-meta p {
165
+ font-size: 0.88rem;
166
+ color: var(--text-muted);
167
+ margin-top: 3px;
168
+ font-weight: 500;
169
+ }
170
+
171
+ .theme-toggle-btn {
172
+ background: var(--btn-bg);
173
+ border: 1px solid var(--btn-border);
174
+ color: var(--text-main);
175
+ width: 44px;
176
+ height: 44px;
177
+ border-radius: 12px;
178
+ display: flex;
179
+ align-items: center;
180
+ justify-content: center;
181
+ cursor: pointer;
182
+ transition: all 0.25s ease;
183
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
184
+ }
185
+
186
+ .theme-toggle-btn:hover {
187
+ background: var(--btn-hover);
188
+ transform: translateY(-2px);
189
+ }
190
+
191
+ .theme-toggle-btn:active {
192
+ transform: scale(0.94);
193
+ }
194
+
195
+ /* ─── TABS COMPONENT ─── */
196
+ .tab-switcher {
197
+ display: flex;
198
+ gap: 8px;
199
+ background: var(--panel-bg);
200
+ padding: 6px;
201
+ border-radius: 14px;
202
+ border: 1px solid var(--panel-border);
203
+ }
204
+
205
+ .tab-btn {
206
+ flex: 1;
207
+ padding: 11px 18px;
208
+ border: none;
209
+ background: transparent;
210
+ color: var(--text-muted);
211
+ font-size: 0.92rem;
212
+ font-weight: 600;
213
+ border-radius: 10px;
214
+ cursor: pointer;
215
+ transition: all 0.25s ease;
216
+ display: flex;
217
+ align-items: center;
218
+ justify-content: center;
219
+ gap: 10px;
220
+ min-height: 44px;
221
+ }
222
+
223
+ .tab-btn:hover {
224
+ color: var(--text-main);
225
+ }
226
+
227
+ .tab-btn.active {
228
+ background: linear-gradient(135deg, #6366f1, #4f46e5);
229
+ color: #ffffff;
230
+ box-shadow: 0 4px 16px rgba(99, 102, 241, 0.35);
231
+ }
232
+
233
+ /* ─── UPLOAD PANEL ─── */
234
+ .upload-panel {
235
+ display: flex;
236
+ flex-direction: column;
237
+ gap: 14px;
238
+ }
239
+
240
+ .dropzone-box {
241
+ border: 2px dashed var(--dropzone-border);
242
+ background: var(--dropzone-bg);
243
+ border-radius: 18px;
244
+ padding: 38px 24px;
245
+ text-align: center;
246
+ cursor: pointer;
247
+ transition: all 0.25s cubic-bezier(0.4, 0, 0.2, 1);
248
+ display: flex;
249
+ flex-direction: column;
250
+ align-items: center;
251
+ gap: 12px;
252
+ }
253
+
254
+ .dropzone-box:hover, .dropzone-box.dragover {
255
+ border-color: #a855f7;
256
+ background: rgba(99, 102, 241, 0.08);
257
+ transform: translateY(-2px);
258
+ }
259
+
260
+ .dropzone-box .icon-wrap {
261
+ width: 60px;
262
+ height: 60px;
263
+ border-radius: 16px;
264
+ background: rgba(99, 102, 241, 0.12);
265
+ color: #6366f1;
266
+ display: flex;
267
+ align-items: center;
268
+ justify-content: center;
269
+ transition: transform 0.25s ease;
270
+ }
271
+
272
+ .dropzone-box:hover .icon-wrap {
273
+ transform: scale(1.08);
274
+ }
275
+
276
+ .dropzone-box h3 {
277
+ font-size: 1.05rem;
278
+ font-weight: 700;
279
+ color: var(--text-main);
280
+ }
281
+
282
+ .dropzone-box span {
283
+ font-size: 0.82rem;
284
+ color: var(--text-muted);
285
+ font-weight: 500;
286
+ }
287
+
288
+ /* ─── LIVE RECORDING PANEL ─── */
289
+ .record-panel {
290
+ display: none;
291
+ flex-direction: column;
292
+ align-items: center;
293
+ justify-content: center;
294
+ background: var(--panel-bg);
295
+ border-radius: 18px;
296
+ border: 1px solid var(--panel-border);
297
+ padding: 36px 20px;
298
+ gap: 18px;
299
+ }
300
+
301
+ .rec-timer-display {
302
+ font-size: 2.2rem;
303
+ font-weight: 800;
304
+ color: #ef4444;
305
+ font-variant-numeric: tabular-nums;
306
+ letter-spacing: 2px;
307
+ }
308
+
309
+ .rec-action-btn {
310
+ width: 76px;
311
+ height: 76px;
312
+ border-radius: 50%;
313
+ border: none;
314
+ background: linear-gradient(135deg, #ef4444, #dc2626);
315
+ color: #ffffff;
316
+ display: flex;
317
+ align-items: center;
318
+ justify-content: center;
319
+ cursor: pointer;
320
+ box-shadow: 0 0 25px var(--danger-glow);
321
+ transition: all 0.25s cubic-bezier(0.4, 0, 0.2, 1);
322
+ }
323
+
324
+ .rec-action-btn:hover {
325
+ transform: scale(1.08);
326
+ box-shadow: 0 0 35px rgba(239, 68, 68, 0.7);
327
+ }
328
+
329
+ .rec-action-btn:active {
330
+ transform: scale(0.95);
331
+ }
332
+
333
+ .rec-action-btn.recording {
334
+ animation: pulse-ring 1.3s infinite ease-in-out;
335
+ background: #b91c1c;
336
+ }
337
+
338
+ @keyframes pulse-ring {
339
+ 0% { transform: scale(1); box-shadow: 0 0 16px rgba(239, 68, 68, 0.5); }
340
+ 50% { transform: scale(1.1); box-shadow: 0 0 36px rgba(239, 68, 68, 0.85); }
341
+ 100% { transform: scale(1); box-shadow: 0 0 16px rgba(239, 68, 68, 0.5); }
342
+ }
343
+
344
+ .rec-helper-text {
345
+ font-size: 0.88rem;
346
+ color: var(--text-muted);
347
+ font-weight: 600;
348
+ }
349
+
350
+ /* ─── WAVEFORM CARD (EXACT ULTRA DESIGN) ─── */
351
+ .waveform-card {
352
+ display: none;
353
+ background: var(--track-bg);
354
+ border: 1px solid var(--track-border);
355
+ border-radius: 18px;
356
+ padding: 20px 22px;
357
+ box-shadow: 0 12px 36px rgba(0, 0, 0, 0.25);
358
+ flex-direction: column;
359
+ gap: 14px;
360
+ transition: all 0.3s ease;
361
+ }
362
+
363
+ .wf-badge-row {
364
+ display: flex;
365
+ align-items: center;
366
+ justify-content: space-between;
367
+ gap: 10px;
368
+ }
369
+
370
+ .wf-chip {
371
+ display: inline-flex;
372
+ align-items: center;
373
+ gap: 10px;
374
+ background: var(--chip-bg);
375
+ border: 1px solid var(--chip-border);
376
+ color: var(--chip-text);
377
+ font-size: 0.84rem;
378
+ padding: 5px 14px;
379
+ border-radius: 20px;
380
+ font-weight: 600;
381
+ max-width: 75%;
382
+ overflow: hidden;
383
+ text-overflow: ellipsis;
384
+ white-space: nowrap;
385
+ }
386
+
387
+ .wf-chip-close {
388
+ cursor: pointer;
389
+ color: var(--text-muted);
390
+ display: flex;
391
+ align-items: center;
392
+ justify-content: center;
393
+ transition: color 0.15s, transform 0.15s;
394
+ }
395
+
396
+ .wf-chip-close:hover {
397
+ color: #ef4444;
398
+ transform: scale(1.15);
399
+ }
400
+
401
+ .wf-format-tag {
402
+ font-size: 0.78rem;
403
+ color: var(--text-muted);
404
+ font-weight: 600;
405
+ letter-spacing: 0.5px;
406
+ }
407
+
408
+ /* ─── WAVEFORM TRACK & INTERACTIVE CANVAS ─── */
409
+ .wf-track-wrap {
410
+ position: relative;
411
+ width: 100%;
412
+ height: 88px;
413
+ background: var(--panel-bg);
414
+ border-radius: 12px;
415
+ overflow: hidden;
416
+ cursor: pointer;
417
+ border: 1px solid var(--track-border);
418
+ touch-action: none;
419
+ }
420
+
421
+ .wf-canvas {
422
+ width: 100%;
423
+ height: 100%;
424
+ display: block;
425
+ }
426
+
427
+ .wf-playhead {
428
+ position: absolute;
429
+ top: 0;
430
+ bottom: 0;
431
+ width: 2px;
432
+ background: var(--playhead-color);
433
+ pointer-events: none;
434
+ left: 0;
435
+ display: none;
436
+ box-shadow: 0 0 10px var(--playhead-color);
437
+ z-index: 5;
438
+ }
439
+
440
+ /* ─── SELECTION OVERLAY & DRAGGABLE HANDLES ─── */
441
+ .wf-selection-overlay {
442
+ position: absolute;
443
+ top: 0;
444
+ bottom: 0;
445
+ left: 0;
446
+ width: 100%;
447
+ background: var(--overlay-bg);
448
+ border-left: 2px solid var(--overlay-border);
449
+ border-right: 2px solid var(--overlay-border);
450
+ display: none;
451
+ cursor: grab;
452
+ z-index: 4;
453
+ box-sizing: border-box;
454
+ touch-action: none;
455
+ }
456
+
457
+ .wf-selection-overlay:active {
458
+ cursor: grabbing;
459
+ }
460
+
461
+ .wf-handle {
462
+ position: absolute;
463
+ top: 0;
464
+ bottom: 0;
465
+ width: 24px;
466
+ cursor: ew-resize;
467
+ display: flex;
468
+ align-items: center;
469
+ justify-content: center;
470
+ z-index: 6;
471
+ touch-action: none;
472
+ }
473
+
474
+ .wf-handle.start { left: -12px; }
475
+ .wf-handle.end { right: -12px; }
476
+
477
+ .wf-handle-bar {
478
+ width: 5px;
479
+ height: 42px;
480
+ background: var(--handle-bar);
481
+ border-radius: 4px;
482
+ box-shadow: 0 0 8px rgba(0, 0, 0, 0.45);
483
+ transition: transform 0.15s ease;
484
+ }
485
+
486
+ .wf-handle:hover .wf-handle-bar, .wf-handle:active .wf-handle-bar {
487
+ transform: scaleY(1.15);
488
+ background: #a855f7;
489
+ }
490
+
491
+ /* ─── TIME DISPLAY ROW ─── */
492
+ .wf-time-row {
493
+ display: flex;
494
+ justify-content: space-between;
495
+ align-items: center;
496
+ font-size: 0.84rem;
497
+ color: var(--text-muted);
498
+ font-variant-numeric: tabular-nums;
499
+ font-weight: 600;
500
+ }
501
+
502
+ .crop-duration-pill {
503
+ background: rgba(239, 68, 68, 0.15);
504
+ border: 1px solid rgba(239, 68, 68, 0.35);
505
+ color: #ef4444;
506
+ padding: 3px 10px;
507
+ border-radius: 8px;
508
+ font-weight: 700;
509
+ display: none;
510
+ margin-right: 8px;
511
+ align-items: center;
512
+ gap: 6px;
513
+ }
514
+
515
+ /* ─── CONTROLS ROW ─── */
516
+ .wf-controls-row {
517
+ display: flex;
518
+ align-items: center;
519
+ justify-content: space-between;
520
+ padding-top: 6px;
521
+ flex-wrap: wrap;
522
+ gap: 12px;
523
+ }
524
+
525
+ .ctrl-group {
526
+ display: flex;
527
+ align-items: center;
528
+ gap: 10px;
529
+ }
530
+
531
+ /* ─── ULTRA-MODERN ANIMATED BUTTONS ─── */
532
+ .ctrl-btn {
533
+ background: var(--btn-bg);
534
+ border: 1px solid var(--btn-border);
535
+ color: var(--btn-text);
536
+ width: 42px;
537
+ height: 42px;
538
+ border-radius: 50%;
539
+ display: inline-flex;
540
+ align-items: center;
541
+ justify-content: center;
542
+ cursor: pointer;
543
+ transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
544
+ box-shadow: 0 2px 6px rgba(0, 0, 0, 0.06);
545
+ flex-shrink: 0;
546
+ }
547
+
548
+ .ctrl-btn:hover {
549
+ background: var(--btn-hover);
550
+ transform: translateY(-2px);
551
+ box-shadow: 0 6px 14px rgba(0, 0, 0, 0.12);
552
+ }
553
+
554
+ .ctrl-btn:active {
555
+ transform: scale(0.92);
556
+ }
557
+
558
+ .ctrl-btn.play-btn {
559
+ width: 48px;
560
+ height: 48px;
561
+ background: linear-gradient(135deg, #6366f1, #4f46e5);
562
+ border: none;
563
+ color: #ffffff;
564
+ box-shadow: 0 6px 20px rgba(99, 102, 241, 0.45);
565
+ }
566
+
567
+ .ctrl-btn.play-btn:hover {
568
+ transform: scale(1.08) translateY(-1px);
569
+ box-shadow: 0 8px 24px rgba(99, 102, 241, 0.6);
570
+ }
571
+
572
+ .ctrl-btn.play-btn:active {
573
+ transform: scale(0.94);
574
+ }
575
+
576
+ .speed-btn {
577
+ background: var(--btn-bg);
578
+ border: 1px solid var(--btn-border);
579
+ color: var(--text-muted);
580
+ padding: 6px 12px;
581
+ border-radius: 14px;
582
+ font-size: 0.8rem;
583
+ font-weight: 700;
584
+ cursor: pointer;
585
+ transition: all 0.2s;
586
+ min-height: 38px;
587
+ display: flex;
588
+ align-items: center;
589
+ justify-content: center;
590
+ }
591
+
592
+ .speed-btn:hover {
593
+ color: var(--text-main);
594
+ background: var(--btn-hover);
595
+ transform: translateY(-1px);
596
+ }
597
+
598
+ .speed-btn:active {
599
+ transform: scale(0.94);
600
+ }
601
+
602
+ /* ─── SCISSORS (CUT) & UNDO SPECIAL BUTTONS ─── */
603
+ .cut-btn {
604
+ color: #ef4444;
605
+ border-color: rgba(239, 68, 68, 0.35);
606
+ background: rgba(239, 68, 68, 0.1);
607
+ }
608
+
609
+ .cut-btn:hover {
610
+ background: rgba(239, 68, 68, 0.22);
611
+ border-color: #ef4444;
612
+ box-shadow: 0 4px 14px rgba(239, 68, 68, 0.3);
613
+ }
614
+
615
+ .undo-btn {
616
+ color: #f59e0b;
617
+ border-color: rgba(245, 158, 11, 0.35);
618
+ background: rgba(245, 158, 11, 0.1);
619
+ display: none;
620
+ }
621
+
622
+ .undo-btn:hover {
623
+ background: rgba(245, 158, 11, 0.22);
624
+ border-color: #f59e0b;
625
+ box-shadow: 0 4px 14px rgba(245, 158, 11, 0.3);
626
+ }
627
+
628
+ .crop-action-group {
629
+ display: none;
630
+ align-items: center;
631
+ gap: 10px;
632
+ }
633
+
634
+ .pill-btn {
635
+ padding: 8px 18px;
636
+ border-radius: 24px;
637
+ font-size: 0.85rem;
638
+ font-weight: 700;
639
+ cursor: pointer;
640
+ border: none;
641
+ transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
642
+ display: inline-flex;
643
+ align-items: center;
644
+ gap: 6px;
645
+ min-height: 40px;
646
+ }
647
+
648
+ .pill-btn.primary {
649
+ background: linear-gradient(135deg, #10b981, #059669);
650
+ color: #ffffff;
651
+ box-shadow: 0 4px 14px rgba(16, 185, 129, 0.35);
652
+ }
653
+
654
+ .pill-btn.primary:hover {
655
+ transform: translateY(-2px);
656
+ box-shadow: 0 6px 18px rgba(16, 185, 129, 0.5);
657
+ }
658
+
659
+ .pill-btn.primary:active {
660
+ transform: scale(0.95);
661
+ }
662
+
663
+ .pill-btn.secondary {
664
+ background: var(--btn-bg);
665
+ border: 1px solid var(--btn-border);
666
+ color: var(--text-main);
667
+ }
668
+
669
+ .pill-btn.secondary:hover {
670
+ background: var(--btn-hover);
671
+ }
672
+
673
+ .pill-btn.secondary:active {
674
+ transform: scale(0.95);
675
+ }
676
+
677
+ /* ─── STATUS / CONSOLE LOG BAR ─── */
678
+ .status-box {
679
+ background: var(--log-bg);
680
+ border: 1px solid var(--log-border);
681
+ border-radius: 14px;
682
+ padding: 14px 18px;
683
+ font-size: 0.84rem;
684
+ color: var(--text-muted);
685
+ display: flex;
686
+ align-items: center;
687
+ justify-content: space-between;
688
+ gap: 12px;
689
+ flex-wrap: wrap;
690
+ }
691
+
692
+ .status-badge {
693
+ display: inline-flex;
694
+ align-items: center;
695
+ gap: 8px;
696
+ color: #38bdf8;
697
+ font-weight: 600;
698
+ }
699
+
700
+ [data-theme="light"] .status-badge {
701
+ color: #0284c7;
702
+ }
703
+
704
+ /* ═══════════════════════════════════════════════════════════════
705
+ RESPONSIVE & MOBILE / SIMULATOR / ROTATION ADAPTATIONS
706
+ ═══════════════════════════════════════════════════════════════ */
707
+ @media (max-width: 680px) {
708
+ body { padding: 12px 8px; }
709
+ .app-container { padding: 20px 16px; border-radius: 18px; gap: 16px; }
710
+ .title-meta h1 { font-size: 1.25rem; }
711
+ .title-meta p { font-size: 0.78rem; }
712
+ .wf-controls-row { justify-content: center; }
713
+ .ctrl-group { justify-content: center; width: 100%; }
714
+ .tab-btn { font-size: 0.84rem; padding: 9px 12px; }
715
+ .dropzone-box { padding: 26px 14px; }
716
+ .wf-track-wrap { height: 74px; }
717
+ }
718
+
719
+ @media (max-width: 440px) {
720
+ .ctrl-btn { width: 38px; height: 38px; }
721
+ .ctrl-btn.play-btn { width: 44px; height: 44px; }
722
+ .pill-btn { padding: 7px 14px; font-size: 0.78rem; }
723
+ }
724
+
725
+ /* Landscape Mobile & Simulator mode */
726
+ @media (orientation: landscape) and (max-height: 540px) {
727
+ body { padding: 8px; }
728
+ .app-container { padding: 16px; gap: 12px; }
729
+ .dropzone-box { padding: 18px; }
730
+ .wf-track-wrap { height: 64px; }
731
+ }
732
+ </style>
733
+ </head>
734
+ <body>
735
+
736
+ <div class="app-container">
737
+ <!-- HEADER & THEME TOGGLE -->
738
+ <div class="header-row">
739
+ <div class="title-meta">
740
+ <h1>Audio Waveform Studio</h1>
741
+ <p>High-Precision Waveform Trimmer, Visualizer & Recorder</p>
742
+ </div>
743
+ <button class="theme-toggle-btn" id="themeToggleBtn" onclick="toggleTheme()" title="Toggle Dark/Light Mode">
744
+ <!-- Sun Icon (Shown in Dark Mode) -->
745
+ <svg class="svg-icon" id="sunIcon" viewBox="0 0 24 24">
746
+ <circle cx="12" cy="12" r="5"></circle>
747
+ <line x1="12" y1="1" x2="12" y2="3"></line>
748
+ <line x1="12" y1="21" x2="12" y2="23"></line>
749
+ <line x1="4.22" y1="4.22" x2="5.64" y2="5.64"></line>
750
+ <line x1="18.36" y1="18.36" x2="19.78" y2="19.78"></line>
751
+ <line x1="1" y1="12" x2="3" y2="12"></line>
752
+ <line x1="21" y1="12" x2="23" y2="12"></line>
753
+ <line x1="4.22" y1="19.78" x2="5.64" y2="18.36"></line>
754
+ <line x1="18.36" y1="5.64" x2="19.78" y2="4.22"></line>
755
+ </svg>
756
+ <!-- Moon Icon (Shown in Light Mode) -->
757
+ <svg class="svg-icon" id="moonIcon" viewBox="0 0 24 24" style="display:none;">
758
+ <path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"></path>
759
+ </svg>
760
+ </button>
761
+ </div>
762
+
763
+ <!-- TABS -->
764
+ <div class="tab-switcher">
765
+ <button class="tab-btn active" id="tabUpload" onclick="switchTab('upload')">
766
+ <svg class="svg-icon" viewBox="0 0 24 24">
767
+ <path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"></path>
768
+ <line x1="12" y1="11" x2="12" y2="17"></line>
769
+ <polyline points="9 14 12 11 15 14"></polyline>
770
+ </svg>
771
+ Upload File
772
+ </button>
773
+ <button class="tab-btn" id="tabRecord" onclick="switchTab('record')">
774
+ <svg class="svg-icon" viewBox="0 0 24 24">
775
+ <path d="M12 1a3 3 0 0 0-3 3v8a3 3 0 0 0 6 0V4a3 3 0 0 0-3-3z"></path>
776
+ <path d="M19 10v2a7 7 0 0 1-14 0v-2"></path>
777
+ <line x1="12" y1="19" x2="12" y2="23"></line>
778
+ <line x1="8" y1="23" x2="16" y2="23"></line>
779
+ </svg>
780
+ Record Audio
781
+ </button>
782
+ </div>
783
+
784
+ <!-- UPLOAD PANEL -->
785
+ <div class="upload-panel" id="panelUpload">
786
+ <div class="dropzone-box" id="dropZone" onclick="document.getElementById('fileInput').click()">
787
+ <div class="icon-wrap">
788
+ <svg class="svg-icon icon-xl" viewBox="0 0 24 24">
789
+ <polyline points="16 16 12 12 8 16"></polyline>
790
+ <line x1="12" y1="12" x2="12" y2="21"></line>
791
+ <path d="M20.39 18.39A5 5 0 0 0 18 9h-1.26A8 8 0 1 0 3 16.3"></path>
792
+ <polyline points="16 16 12 12 8 16"></polyline>
793
+ </svg>
794
+ </div>
795
+ <h3>Click to Browse or Drag Audio / Video</h3>
796
+ <span>Supports MP3, WAV, AAC, M4A, OGG, MP4, WebM, MKV, MOV</span>
797
+ </div>
798
+ <input type="file" id="fileInput" accept="audio/*,video/*" style="display:none;">
799
+ </div>
800
+
801
+ <!-- LIVE MICROPHONE RECORDING PANEL -->
802
+ <div class="record-panel" id="panelRecord">
803
+ <div class="rec-timer-display" id="recTimer">0:00</div>
804
+ <button class="rec-action-btn" id="recBtn" onclick="toggleRecording()" title="Click to Record / Stop">
805
+ <svg class="svg-icon icon-lg" id="recMicIcon" viewBox="0 0 24 24">
806
+ <path d="M12 1a3 3 0 0 0-3 3v8a3 3 0 0 0 6 0V4a3 3 0 0 0-3-3z"></path>
807
+ <path d="M19 10v2a7 7 0 0 1-14 0v-2"></path>
808
+ <line x1="12" y1="19" x2="12" y2="23"></line>
809
+ <line x1="8" y1="23" x2="16" y2="23"></line>
810
+ </svg>
811
+ <svg class="svg-icon icon-lg" id="recStopIcon" viewBox="0 0 24 24" style="display:none;">
812
+ <rect x="4" y="4" width="16" height="16" rx="2"></rect>
813
+ </svg>
814
+ </button>
815
+ <span class="rec-helper-text" id="recStatusText">Click microphone button to start recording</span>
816
+ </div>
817
+
818
+ <!-- WAVEFORM CARD (MATCHING USER SCREENSHOT SPECIFICATIONS) -->
819
+ <div class="waveform-card" id="waveformCard">
820
+ <!-- Title Badge Row -->
821
+ <div class="wf-badge-row">
822
+ <div class="wf-chip">
823
+ <span id="wfFileName">audio-sample.wav</span>
824
+ <span class="wf-chip-close" onclick="closeWaveform()" title="Remove file">
825
+ <svg class="svg-icon" style="width:14px; height:14px;" viewBox="0 0 24 24">
826
+ <line x1="18" y1="6" x2="6" y2="18"></line>
827
+ <line x1="6" y1="6" x2="18" y2="18"></line>
828
+ </svg>
829
+ </span>
830
+ </div>
831
+ <span class="wf-format-tag" id="wfFormatBadge">24kHz Mono WAV</span>
832
+ </div>
833
+
834
+ <!-- Waveform Track Wrap -->
835
+ <div class="wf-track-wrap" id="trackWrap">
836
+ <canvas class="wf-canvas" id="wfCanvas"></canvas>
837
+
838
+ <!-- Draggable Crop Overlay -->
839
+ <div class="wf-selection-overlay" id="selectionOverlay">
840
+ <div class="wf-handle start" id="handleStart"><div class="wf-handle-bar"></div></div>
841
+ <div class="wf-handle end" id="handleEnd"><div class="wf-handle-bar"></div></div>
842
+ </div>
843
+
844
+ <!-- Current Playhead -->
845
+ <div class="wf-playhead" id="playhead"></div>
846
+ </div>
847
+
848
+ <!-- Time Row -->
849
+ <div class="wf-time-row">
850
+ <span id="currentTime">0:00</span>
851
+ <div style="display:flex; align-items:center;">
852
+ <span class="crop-duration-pill" id="cropDurationPill">
853
+ <svg class="svg-icon" style="width:14px; height:14px;" viewBox="0 0 24 24">
854
+ <circle cx="6" cy="6" r="3"></circle>
855
+ <circle cx="6" cy="18" r="3"></circle>
856
+ <line x1="20" y1="4" x2="8.12" y2="15.88"></line>
857
+ <line x1="14.47" y1="14.48" x2="20" y2="20"></line>
858
+ <line x1="8.12" y1="8.12" x2="12" y2="12"></line>
859
+ </svg>
860
+ <span id="cropDurationText">0:00</span>
861
+ </span>
862
+ <span id="totalDuration">0:00</span>
863
+ </div>
864
+ </div>
865
+
866
+ <!-- Controls Row -->
867
+ <div class="wf-controls-row">
868
+ <!-- Left Group: Volume & Speed -->
869
+ <div class="ctrl-group">
870
+ <button class="ctrl-btn" id="volBtn" onclick="toggleMute()" title="Mute / Unmute">
871
+ <!-- Volume High -->
872
+ <svg class="svg-icon" id="volHighIcon" viewBox="0 0 24 24">
873
+ <polygon points="11 5 6 9 2 9 2 15 6 15 11 19 11 5"></polygon>
874
+ <path d="M15.54 8.46a5 5 0 0 1 0 7.07"></path>
875
+ <path d="M19.07 4.93a10 10 0 0 1 0 14.14"></path>
876
+ </svg>
877
+ <!-- Volume Mute -->
878
+ <svg class="svg-icon" id="volMuteIcon" viewBox="0 0 24 24" style="display:none;">
879
+ <polygon points="11 5 6 9 2 9 2 15 6 15 11 19 11 5"></polygon>
880
+ <line x1="23" y1="9" x2="17" y2="15"></line>
881
+ <line x1="17" y1="9" x2="23" y2="15"></line>
882
+ </svg>
883
+ </button>
884
+ <button class="speed-btn" id="speedBtn" onclick="cycleSpeed()" title="Playback Speed">1x</button>
885
+ </div>
886
+
887
+ <!-- Center Group: Rewind, Play/Pause, Forward -->
888
+ <div class="ctrl-group">
889
+ <button class="ctrl-btn" onclick="skip(-5)" title="Rewind 5s">
890
+ <svg class="svg-icon" viewBox="0 0 24 24">
891
+ <polygon points="11 19 2 12 11 5 11 19"></polygon>
892
+ <polygon points="22 19 13 12 22 5 22 19"></polygon>
893
+ </svg>
894
+ </button>
895
+ <button class="ctrl-btn play-btn" id="playBtn" onclick="togglePlay()" title="Play / Pause">
896
+ <!-- Play Icon -->
897
+ <svg class="svg-icon icon-lg" id="playIcon" viewBox="0 0 24 24" style="margin-left: 2px;">
898
+ <polygon points="5 3 19 12 5 21 5 3"></polygon>
899
+ </svg>
900
+ <!-- Pause Icon -->
901
+ <svg class="svg-icon icon-lg" id="pauseIcon" viewBox="0 0 24 24" style="display:none;">
902
+ <rect x="6" y="4" width="4" height="16"></rect>
903
+ <rect x="14" y="4" width="4" height="16"></rect>
904
+ </svg>
905
+ </button>
906
+ <button class="ctrl-btn" onclick="skip(5)" title="Forward 5s">
907
+ <svg class="svg-icon" viewBox="0 0 24 24">
908
+ <polygon points="13 19 22 12 13 5 13 19"></polygon>
909
+ <polygon points="2 19 11 12 2 5 2 19"></polygon>
910
+ </svg>
911
+ </button>
912
+ </div>
913
+
914
+ <!-- Right Group: Undo, Scissors Cut, Download, Action Pills -->
915
+ <div class="ctrl-group">
916
+ <!-- Standard Actions -->
917
+ <div id="normalControls" style="display:flex; align-items:center; gap:8px;">
918
+ <!-- Undo / Revert Button -->
919
+ <button class="ctrl-btn undo-btn" id="undoBtn" onclick="undoTrim()" title="Revert / Undo Crop">
920
+ <svg class="svg-icon" viewBox="0 0 24 24">
921
+ <polyline points="1 4 1 10 7 10"></polyline>
922
+ <path d="M3.51 15a9 9 0 1 0 2.13-9.36L1 10"></path>
923
+ </svg>
924
+ </button>
925
+ <!-- Scissors / Cutter Button -->
926
+ <button class="ctrl-btn cut-btn" id="cutBtn" onclick="toggleCropMode()" title="Crop / Trim Audio">
927
+ <svg class="svg-icon" viewBox="0 0 24 24">
928
+ <circle cx="6" cy="6" r="3"></circle>
929
+ <circle cx="6" cy="18" r="3"></circle>
930
+ <line x1="20" y1="4" x2="8.12" y2="15.88"></line>
931
+ <line x1="14.47" y1="14.48" x2="20" y2="20"></line>
932
+ <line x1="8.12" y1="8.12" x2="12" y2="12"></line>
933
+ </svg>
934
+ </button>
935
+ <!-- Download Button -->
936
+ <a class="ctrl-btn" id="downloadBtn" download="trimmed_audio.wav" title="Download WAV File" style="text-decoration:none;">
937
+ <svg class="svg-icon" viewBox="0 0 24 24">
938
+ <path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path>
939
+ <polyline points="7 10 12 15 17 10"></polyline>
940
+ <line x1="12" y1="15" x2="12" y2="3"></line>
941
+ </svg>
942
+ </a>
943
+ </div>
944
+
945
+ <!-- Crop Mode Active Pills -->
946
+ <div class="crop-action-group" id="cropControls">
947
+ <button class="pill-btn primary" onclick="applyTrim()">
948
+ <svg class="svg-icon" style="width:16px; height:16px;" viewBox="0 0 24 24">
949
+ <polyline points="20 6 9 17 4 12"></polyline>
950
+ </svg>
951
+ Trim Selection
952
+ </button>
953
+ <button class="pill-btn secondary" onclick="cancelCrop()">Cancel</button>
954
+ </div>
955
+ </div>
956
+ </div>
957
+ </div>
958
+
959
+ <!-- STATUS & ACTIVITY BAR -->
960
+ <div class="status-box">
961
+ <span class="status-badge" id="statusMessage">
962
+ <svg class="svg-icon" viewBox="0 0 24 24">
963
+ <circle cx="12" cy="12" r="10"></circle>
964
+ <line x1="12" y1="8" x2="12" y2="12"></line>
965
+ <line x1="12" y1="16" x2="12.01" y2="16"></line>
966
+ </svg>
967
+ System ready. Select or record audio to begin.
968
+ </span>
969
+ <span style="font-size: 0.75rem; color: var(--text-muted); font-weight:600;">100% Client-Side Engine</span>
970
+ </div>
971
+ </div>
972
+
973
+ <script>
974
+ /* ═══════════════════════════════════════════════════════════════
975
+ BULLETPROOF AUDIO ENGINE & WAVEFORM CONTROLLER
976
+ INFINITE CLICK STRESS TOLERANCE & ZERO-GLITCH STATE SYSTEM
977
+ ═══════════════════════════════════════════════════════════════ */
978
+
979
+ let audioCtx = null;
980
+ let audioBuffer = null;
981
+ let originalBlob = null;
982
+ let currentBlob = null;
983
+ const audioEl = new Audio();
984
+
985
+ let duration = 0;
986
+ let isCropMode = false;
987
+ let cropStart = 0;
988
+ let cropEnd = 0;
989
+ let hasTrimmed = false;
990
+ let isMuted = false;
991
+ const speeds = [1, 1.25, 1.5, 2, 0.75];
992
+ let speedIdx = 0;
993
+
994
+ // Draggable Selection State
995
+ let isDragging = false;
996
+ let dragType = null;
997
+ let dragStartX = 0;
998
+ let dragStartCrop = 0;
999
+ let dragEndCrop = 0;
1000
+
1001
+ // Stress Tolerance Locks
1002
+ let isProcessing = false;
1003
+
1004
+ // DOM Elements
1005
+ const waveformCard = document.getElementById('waveformCard');
1006
+ const wfCanvas = document.getElementById('wfCanvas');
1007
+ const trackWrap = document.getElementById('trackWrap');
1008
+ const selectionOverlay = document.getElementById('selectionOverlay');
1009
+ const handleStart = document.getElementById('handleStart');
1010
+ const handleEnd = document.getElementById('handleEnd');
1011
+ const playhead = document.getElementById('playhead');
1012
+ const currentTimeEl = document.getElementById('currentTime');
1013
+ const totalDurationEl = document.getElementById('totalDuration');
1014
+ const cropDurationPill = document.getElementById('cropDurationPill');
1015
+ const cropDurationText = document.getElementById('cropDurationText');
1016
+ const playIcon = document.getElementById('playIcon');
1017
+ const pauseIcon = document.getElementById('pauseIcon');
1018
+ const volHighIcon = document.getElementById('volHighIcon');
1019
+ const volMuteIcon = document.getElementById('volMuteIcon');
1020
+ const speedBtn = document.getElementById('speedBtn');
1021
+ const undoBtn = document.getElementById('undoBtn');
1022
+ const normalControls = document.getElementById('normalControls');
1023
+ const cropControls = document.getElementById('cropControls');
1024
+ const wfFileName = document.getElementById('wfFileName');
1025
+ const downloadBtn = document.getElementById('downloadBtn');
1026
+ const statusMessage = document.getElementById('statusMessage');
1027
+ const sunIcon = document.getElementById('sunIcon');
1028
+ const moonIcon = document.getElementById('moonIcon');
1029
+
1030
+ // ─── THEME TOGGLE (DARK / LIGHT) ───
1031
+ function toggleTheme() {
1032
+ const html = document.documentElement;
1033
+ const currentTheme = html.getAttribute('data-theme') || 'dark';
1034
+ const nextTheme = currentTheme === 'dark' ? 'light' : 'dark';
1035
+ html.setAttribute('data-theme', nextTheme);
1036
+ localStorage.setItem('audio_studio_theme', nextTheme);
1037
+
1038
+ if (nextTheme === 'light') {
1039
+ sunIcon.style.display = 'none';
1040
+ moonIcon.style.display = 'block';
1041
+ } else {
1042
+ sunIcon.style.display = 'block';
1043
+ moonIcon.style.display = 'none';
1044
+ }
1045
+
1046
+ // Redraw waveform with theme-matching colors
1047
+ if (waveformCard.style.display !== 'none' && audioBuffer) {
1048
+ requestAnimationFrame(() => drawWaveform());
1049
+ }
1050
+ }
1051
+
1052
+ // Initialize saved theme
1053
+ (function initTheme() {
1054
+ const saved = localStorage.getItem('audio_studio_theme');
1055
+ if (saved) {
1056
+ document.documentElement.setAttribute('data-theme', saved);
1057
+ if (saved === 'light') {
1058
+ sunIcon.style.display = 'none';
1059
+ moonIcon.style.display = 'block';
1060
+ }
1061
+ }
1062
+ })();
1063
+
1064
+ // ─── TAB SWITCHING ───
1065
+ function switchTab(tab) {
1066
+ document.getElementById('tabUpload').classList.toggle('active', tab === 'upload');
1067
+ document.getElementById('tabRecord').classList.toggle('active', tab === 'record');
1068
+ document.getElementById('panelUpload').style.display = tab === 'upload' ? 'flex' : 'none';
1069
+ document.getElementById('panelRecord').style.display = tab === 'record' ? 'flex' : 'none';
1070
+ }
1071
+
1072
+ function setStatus(msg, isSuccess = true) {
1073
+ statusMessage.innerHTML = `
1074
+ <svg class="svg-icon" viewBox="0 0 24 24">
1075
+ ${isSuccess ? '<polyline points="20 6 9 17 4 12"></polyline>' : '<circle cx="12" cy="12" r="10"></circle><line x1="12" y1="8" x2="12" y2="12"></line><line x1="12" y1="16" x2="12.01" y2="16"></line>'}
1076
+ </svg>
1077
+ ${msg}
1078
+ `;
1079
+ statusMessage.style.color = isSuccess ? (document.documentElement.getAttribute('data-theme') === 'light' ? '#0284c7' : '#38bdf8') : '#ef4444';
1080
+ }
1081
+
1082
+ function formatTime(sec) {
1083
+ if (isNaN(sec) || sec < 0) sec = 0;
1084
+ const m = Math.floor(sec / 60);
1085
+ const s = Math.floor(sec % 60);
1086
+ return `${m}:${s < 10 ? '0' : ''}${s}`;
1087
+ }
1088
+
1089
+ // ─── AUDIO PLAYBACK EVENT BINDINGS ───
1090
+ audioEl.addEventListener('timeupdate', () => updatePlayhead());
1091
+ audioEl.addEventListener('play', () => {
1092
+ playIcon.style.display = 'none';
1093
+ pauseIcon.style.display = 'block';
1094
+ });
1095
+ audioEl.addEventListener('pause', () => {
1096
+ playIcon.style.display = 'block';
1097
+ pauseIcon.style.display = 'none';
1098
+ });
1099
+ audioEl.addEventListener('ended', () => {
1100
+ audioEl.currentTime = isCropMode ? cropStart : 0;
1101
+ updatePlayhead();
1102
+ playIcon.style.display = 'block';
1103
+ pauseIcon.style.display = 'none';
1104
+ });
1105
+
1106
+ // ─── LOAD AUDIO BLOB ───
1107
+ async function loadAudioBlob(blob, name = 'audio.wav', isInitial = true) {
1108
+ if (isProcessing) return;
1109
+ isProcessing = true;
1110
+ try {
1111
+ audioEl.pause();
1112
+ if (audioEl.src && audioEl.src.startsWith('blob:')) {
1113
+ try { URL.revokeObjectURL(audioEl.src); } catch (_) {}
1114
+ }
1115
+
1116
+ const AudioCtxClass = window.AudioContext || window.webkitAudioContext;
1117
+ if (!audioCtx) audioCtx = new AudioCtxClass();
1118
+ if (audioCtx.state === 'suspended') await audioCtx.resume();
1119
+
1120
+ const arrayBuf = await blob.slice(0).arrayBuffer();
1121
+ audioBuffer = await audioCtx.decodeAudioData(arrayBuf);
1122
+
1123
+ duration = audioBuffer.duration;
1124
+ currentBlob = blob;
1125
+ audioEl.src = URL.createObjectURL(blob);
1126
+ audioEl.playbackRate = speeds[speedIdx];
1127
+ audioEl.muted = isMuted;
1128
+
1129
+ if (isInitial) {
1130
+ originalBlob = blob;
1131
+ hasTrimmed = false;
1132
+ undoBtn.style.display = 'none';
1133
+ } else {
1134
+ if (hasTrimmed) undoBtn.style.display = 'inline-flex';
1135
+ }
1136
+
1137
+ cropStart = 0;
1138
+ cropEnd = duration;
1139
+
1140
+ wfFileName.textContent = name;
1141
+ totalDurationEl.textContent = formatTime(duration);
1142
+ currentTimeEl.textContent = '0:00';
1143
+ downloadBtn.href = audioEl.src;
1144
+ downloadBtn.download = name.replace(/\.[^/.]+$/, '') + '_trimmed.wav';
1145
+
1146
+ waveformCard.style.display = 'flex';
1147
+ requestAnimationFrame(() => {
1148
+ drawWaveform();
1149
+ updateSelection();
1150
+ updatePlayhead();
1151
+ });
1152
+
1153
+ setStatus(`Loaded audio: "${name}" (${formatTime(duration)})`);
1154
+ } catch (err) {
1155
+ console.error('Audio decode error:', err);
1156
+ setStatus('Error decoding audio. File might be corrupted or format unsupported.', false);
1157
+ } finally {
1158
+ isProcessing = false;
1159
+ }
1160
+ }
1161
+
1162
+ // ─── ULTRA-CRISP PURPLE WAVEFORM RENDERER ───
1163
+ function drawWaveform() {
1164
+ if (!wfCanvas || !trackWrap || !audioBuffer) return;
1165
+ const rect = trackWrap.getBoundingClientRect();
1166
+ const width = Math.max(80, Math.floor(rect.width));
1167
+ const height = Math.max(50, Math.floor(rect.height || 88));
1168
+ const dpr = Math.max(1, Math.min(2.5, window.devicePixelRatio || 1));
1169
+
1170
+ wfCanvas.width = Math.floor(width * dpr);
1171
+ wfCanvas.height = Math.floor(height * dpr);
1172
+ const ctx = wfCanvas.getContext('2d');
1173
+ ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
1174
+ ctx.clearRect(0, 0, width, height);
1175
+
1176
+ const isLight = document.documentElement.getAttribute('data-theme') === 'light';
1177
+ const barColor = isLight ? '#6366f1' : '#818cf8';
1178
+ const baselineColor = isLight ? 'rgba(100, 116, 139, 0.35)' : 'rgba(255, 255, 255, 0.12)';
1179
+
1180
+ // Baseline
1181
+ const centerY = height / 2;
1182
+ ctx.strokeStyle = baselineColor;
1183
+ ctx.lineWidth = 1;
1184
+ ctx.beginPath();
1185
+ ctx.moveTo(0, centerY + 0.5);
1186
+ ctx.lineTo(width, centerY + 0.5);
1187
+ ctx.stroke();
1188
+
1189
+ // Waveform Bars Calculation
1190
+ const channelData = audioBuffer.getChannelData(0);
1191
+ const barCount = Math.max(60, Math.min(260, Math.floor(width / 3.6)));
1192
+ const barWidth = Math.max(1.8, (width / barCount) * 0.72);
1193
+ const samplesPerBar = Math.max(1, Math.floor(channelData.length / barCount));
1194
+ const maxAmp = height * 0.44;
1195
+
1196
+ ctx.fillStyle = barColor;
1197
+ for (let i = 0; i < barCount; i++) {
1198
+ const start = i * samplesPerBar;
1199
+ const end = Math.min(channelData.length, start + samplesPerBar);
1200
+ let peak = 0;
1201
+ let sum = 0;
1202
+ let count = 0;
1203
+ for (let s = start; s < end; s += Math.max(1, Math.floor(samplesPerBar / 20))) {
1204
+ const val = Math.abs(channelData[s]);
1205
+ if (val > peak) peak = val;
1206
+ sum += val;
1207
+ count++;
1208
+ }
1209
+ const rms = count ? sum / count : 0;
1210
+ const barH = Math.max(2, (peak * 0.74 + rms * 0.26) * maxAmp);
1211
+ const x = i * (width / barCount) + (width / barCount - barWidth) / 2;
1212
+
1213
+ if (ctx.roundRect) {
1214
+ ctx.beginPath();
1215
+ ctx.roundRect(x, centerY - barH, barWidth, barH * 2, [barWidth / 2]);
1216
+ ctx.fill();
1217
+ } else {
1218
+ ctx.fillRect(x, centerY - barH, barWidth, barH * 2);
1219
+ }
1220
+ }
1221
+ }
1222
+
1223
+ // ─── PLAYHEAD & SEEKING ───
1224
+ function updatePlayhead() {
1225
+ if (!playhead || !duration) return;
1226
+ const cur = audioEl.currentTime || 0;
1227
+ const pct = Math.max(0, Math.min(100, (cur / duration) * 100));
1228
+ playhead.style.left = pct + '%';
1229
+ playhead.style.display = 'block';
1230
+ currentTimeEl.textContent = formatTime(cur);
1231
+
1232
+ if (isCropMode && !audioEl.paused && cur >= cropEnd) {
1233
+ audioEl.pause();
1234
+ audioEl.currentTime = cropStart;
1235
+ updatePlayhead();
1236
+ }
1237
+ }
1238
+
1239
+ trackWrap.addEventListener('pointerdown', (e) => {
1240
+ if (e.target.closest('.wf-handle') || (isCropMode && e.target.closest('.wf-selection-overlay'))) return;
1241
+ const rect = trackWrap.getBoundingClientRect();
1242
+ const pct = Math.max(0, Math.min(1, (e.clientX - rect.left) / Math.max(1, rect.width)));
1243
+ const newTime = pct * duration;
1244
+ if (isCropMode) {
1245
+ audioEl.currentTime = Math.max(cropStart, Math.min(cropEnd, newTime));
1246
+ } else {
1247
+ audioEl.currentTime = newTime;
1248
+ }
1249
+ updatePlayhead();
1250
+ });
1251
+
1252
+ // ─── CROP OVERLAY & SELECTION HANDLERS ───
1253
+ function updateSelection() {
1254
+ if (!selectionOverlay || !duration) return;
1255
+ if (!isCropMode) {
1256
+ selectionOverlay.style.display = 'none';
1257
+ cropDurationPill.style.display = 'none';
1258
+ } else {
1259
+ selectionOverlay.style.display = 'block';
1260
+ const leftPct = (cropStart / duration) * 100;
1261
+ const rightPct = (cropEnd / duration) * 100;
1262
+ selectionOverlay.style.left = leftPct + '%';
1263
+ selectionOverlay.style.width = Math.max(0, rightPct - leftPct) + '%';
1264
+
1265
+ cropDurationText.textContent = formatTime(Math.max(0, cropEnd - cropStart));
1266
+ cropDurationPill.style.display = 'inline-flex';
1267
+ }
1268
+ }
1269
+
1270
+ const startDrag = (type, e) => {
1271
+ e.preventDefault();
1272
+ e.stopPropagation();
1273
+ isDragging = true;
1274
+ dragType = type;
1275
+ dragStartX = e.clientX;
1276
+ dragStartCrop = cropStart;
1277
+ dragEndCrop = cropEnd;
1278
+ try { (e.target || trackWrap).setPointerCapture(e.pointerId); } catch (_) {}
1279
+ };
1280
+
1281
+ handleStart.addEventListener('pointerdown', (e) => startDrag('start', e));
1282
+ handleEnd.addEventListener('pointerdown', (e) => startDrag('end', e));
1283
+ selectionOverlay.addEventListener('pointerdown', (e) => {
1284
+ if (e.target.closest('.wf-handle')) return;
1285
+ startDrag('overlay', e);
1286
+ });
1287
+
1288
+ window.addEventListener('pointermove', (e) => {
1289
+ if (!isDragging || !trackWrap || !duration) return;
1290
+ const rect = trackWrap.getBoundingClientRect();
1291
+ const deltaRatio = (e.clientX - dragStartX) / Math.max(1, rect.width);
1292
+ const deltaTime = deltaRatio * duration;
1293
+ const minSpan = Math.min(0.2, duration / 20);
1294
+
1295
+ if (dragType === 'start') {
1296
+ cropStart = Math.max(0, Math.min(cropEnd - minSpan, dragStartCrop + deltaTime));
1297
+ } else if (dragType === 'end') {
1298
+ cropEnd = Math.min(duration, Math.max(cropStart + minSpan, dragEndCrop + deltaTime));
1299
+ } else if (dragType === 'overlay') {
1300
+ const span = dragEndCrop - dragStartCrop;
1301
+ let newStart = dragStartCrop + deltaTime;
1302
+ let newEnd = newStart + span;
1303
+ if (newStart < 0) {
1304
+ newStart = 0;
1305
+ newEnd = span;
1306
+ } else if (newEnd > duration) {
1307
+ newEnd = duration;
1308
+ newStart = duration - span;
1309
+ }
1310
+ cropStart = Math.max(0, newStart);
1311
+ cropEnd = Math.min(duration, newEnd);
1312
+ }
1313
+ updateSelection();
1314
+ });
1315
+
1316
+ window.addEventListener('pointerup', () => {
1317
+ isDragging = false;
1318
+ dragType = null;
1319
+ });
1320
+
1321
+ // ─── ROTATION & RESIZE AUTO-ADAPTATION (NO OVERLAPS OR GLITCHES) ───
1322
+ let resizeTimer = null;
1323
+ function handleResize() {
1324
+ if (waveformCard.style.display !== 'none' && audioBuffer) {
1325
+ clearTimeout(resizeTimer);
1326
+ resizeTimer = setTimeout(() => {
1327
+ drawWaveform();
1328
+ updateSelection();
1329
+ updatePlayhead();
1330
+ }, 60);
1331
+ }
1332
+ }
1333
+ window.addEventListener('resize', handleResize);
1334
+ window.addEventListener('orientationchange', handleResize);
1335
+
1336
+ // ─── SCISSORS CROP MODE TOGGLE ───
1337
+ function toggleCropMode() {
1338
+ isCropMode = !isCropMode;
1339
+ if (isCropMode) {
1340
+ cropStart = 0;
1341
+ cropEnd = duration;
1342
+ normalControls.style.display = 'none';
1343
+ cropControls.style.display = 'flex';
1344
+ setStatus('Trimmer active: Drag handles to select range, then click "Trim Selection"');
1345
+ } else {
1346
+ normalControls.style.display = 'flex';
1347
+ cropControls.style.display = 'none';
1348
+ }
1349
+ updateSelection();
1350
+ }
1351
+
1352
+ function cancelCrop() {
1353
+ isCropMode = false;
1354
+ normalControls.style.display = 'flex';
1355
+ cropControls.style.display = 'none';
1356
+ updateSelection();
1357
+ setStatus('Trimming cancelled.');
1358
+ }
1359
+
1360
+ // ─── APPLY TRIM (IN-MEMORY 16-BIT PCM WAV) ───
1361
+ async function applyTrim() {
1362
+ if (!audioBuffer || isProcessing) return;
1363
+ const trimmedBlob = audioBufferToWavBlob(audioBuffer, cropStart, cropEnd);
1364
+ hasTrimmed = true;
1365
+ isCropMode = false;
1366
+ normalControls.style.display = 'flex';
1367
+ cropControls.style.display = 'none';
1368
+
1369
+ await loadAudioBlob(trimmedBlob, wfFileName.textContent, false);
1370
+ undoBtn.style.display = 'inline-flex';
1371
+ setStatus(`Trimmed successfully to ${formatTime(duration)}! Revert anytime using Undo.`);
1372
+ }
1373
+
1374
+ // ─── UNDO TRIM (RESTORE UNTRIMMED ORIGINAL) ───
1375
+ async function undoTrim() {
1376
+ if (!originalBlob || isProcessing) return;
1377
+ hasTrimmed = false;
1378
+ isCropMode = false;
1379
+ normalControls.style.display = 'flex';
1380
+ cropControls.style.display = 'none';
1381
+ undoBtn.style.display = 'none';
1382
+
1383
+ await loadAudioBlob(originalBlob, wfFileName.textContent, false);
1384
+ setStatus('Audio restored to original untrimmed state.');
1385
+ }
1386
+
1387
+ // ─── PLAYBACK CONTROLS ───
1388
+ async function togglePlay() {
1389
+ if (!audioEl.src) return;
1390
+ if (audioEl.paused) {
1391
+ if (isCropMode && (audioEl.currentTime < cropStart || audioEl.currentTime >= cropEnd)) {
1392
+ audioEl.currentTime = cropStart;
1393
+ }
1394
+ try { await audioEl.play(); } catch (_) {}
1395
+ } else {
1396
+ audioEl.pause();
1397
+ }
1398
+ }
1399
+
1400
+ function skip(sec) {
1401
+ if (!audioEl.src || !duration) return;
1402
+ let target = (audioEl.currentTime || 0) + sec;
1403
+ if (isCropMode) {
1404
+ target = Math.max(cropStart, Math.min(cropEnd, target));
1405
+ } else {
1406
+ target = Math.max(0, Math.min(duration, target));
1407
+ }
1408
+ audioEl.currentTime = target;
1409
+ updatePlayhead();
1410
+ }
1411
+
1412
+ function toggleMute() {
1413
+ isMuted = !isMuted;
1414
+ audioEl.muted = isMuted;
1415
+ volHighIcon.style.display = isMuted ? 'none' : 'block';
1416
+ volMuteIcon.style.display = isMuted ? 'block' : 'none';
1417
+ }
1418
+
1419
+ function cycleSpeed() {
1420
+ speedIdx = (speedIdx + 1) % speeds.length;
1421
+ const spd = speeds[speedIdx];
1422
+ audioEl.playbackRate = spd;
1423
+ speedBtn.textContent = spd + 'x';
1424
+ }
1425
+
1426
+ function closeWaveform() {
1427
+ audioEl.pause();
1428
+ waveformCard.style.display = 'none';
1429
+ setStatus('Audio unloaded.');
1430
+ }
1431
+
1432
+ // ─── FILE UPLOAD HANDLERS ───
1433
+ const fileInput = document.getElementById('fileInput');
1434
+ const dropZone = document.getElementById('dropZone');
1435
+
1436
+ fileInput.addEventListener('change', (e) => {
1437
+ const file = e.target.files && e.target.files[0];
1438
+ if (file) handleFile(file);
1439
+ });
1440
+
1441
+ dropZone.addEventListener('dragover', (e) => {
1442
+ e.preventDefault();
1443
+ dropZone.classList.add('dragover');
1444
+ });
1445
+
1446
+ dropZone.addEventListener('dragleave', () => dropZone.classList.remove('dragover'));
1447
+
1448
+ dropZone.addEventListener('drop', (e) => {
1449
+ e.preventDefault();
1450
+ dropZone.classList.remove('dragover');
1451
+ const file = e.dataTransfer.files && e.dataTransfer.files[0];
1452
+ if (file) handleFile(file);
1453
+ });
1454
+
1455
+ function handleFile(file) {
1456
+ setStatus(`Loading "${file.name}"...`);
1457
+ loadAudioBlob(file, file.name, true);
1458
+ }
1459
+
1460
+ // ─── LIVE MICROPHONE RECORDING ───
1461
+ let mediaRecorder = null;
1462
+ let recordedChunks = [];
1463
+ let recInterval = null;
1464
+ let recSeconds = 0;
1465
+ const recTimer = document.getElementById('recTimer');
1466
+ const recBtn = document.getElementById('recBtn');
1467
+ const recMicIcon = document.getElementById('recMicIcon');
1468
+ const recStopIcon = document.getElementById('recStopIcon');
1469
+ const recStatusText = document.getElementById('recStatusText');
1470
+
1471
+ async function toggleRecording() {
1472
+ if (mediaRecorder && mediaRecorder.state === 'recording') {
1473
+ // Stop recording
1474
+ mediaRecorder.stop();
1475
+ clearInterval(recInterval);
1476
+ recBtn.classList.remove('recording');
1477
+ recMicIcon.style.display = 'block';
1478
+ recStopIcon.style.display = 'none';
1479
+ recStatusText.textContent = 'Processing live recording...';
1480
+ } else {
1481
+ // Start recording
1482
+ try {
1483
+ const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
1484
+ recordedChunks = [];
1485
+ recSeconds = 0;
1486
+ recTimer.textContent = '0:00';
1487
+
1488
+ mediaRecorder = new MediaRecorder(stream);
1489
+ mediaRecorder.ondataavailable = (e) => {
1490
+ if (e.data.size > 0) recordedChunks.push(e.data);
1491
+ };
1492
+
1493
+ mediaRecorder.onstop = async () => {
1494
+ stream.getTracks().forEach(t => t.stop());
1495
+ const blob = new Blob(recordedChunks, { type: 'audio/webm' });
1496
+ await loadAudioBlob(blob, `Mic_Recording_${new Date().toLocaleTimeString().replace(/[: ]/g, '-')}.wav`, true);
1497
+ recStatusText.textContent = 'Recording ready! Loaded in waveform below.';
1498
+ switchTab('upload');
1499
+ };
1500
+
1501
+ mediaRecorder.start();
1502
+ recBtn.classList.add('recording');
1503
+ recMicIcon.style.display = 'none';
1504
+ recStopIcon.style.display = 'block';
1505
+ recStatusText.textContent = 'Recording live... Click to stop!';
1506
+
1507
+ recInterval = setInterval(() => {
1508
+ recSeconds++;
1509
+ recTimer.textContent = formatTime(recSeconds);
1510
+ }, 1000);
1511
+ } catch (err) {
1512
+ console.error('Microphone error:', err);
1513
+ setStatus('Could not access microphone: ' + err.message, false);
1514
+ }
1515
+ }
1516
+ }
1517
+
1518
+ // ─── CLIENT-SIDE IN-MEMORY 16-BIT PCM WAV ENCODER ───
1519
+ function audioBufferToWavBlob(buf, startSec = 0, endSec = null) {
1520
+ endSec = endSec === null ? buf.duration : Math.min(buf.duration, endSec);
1521
+ startSec = Math.max(0, startSec);
1522
+ const sampleRate = buf.sampleRate;
1523
+ const numChannels = buf.numberOfChannels;
1524
+ const startOffset = Math.floor(startSec * sampleRate);
1525
+ const endOffset = Math.floor(endSec * sampleRate);
1526
+ const frameCount = Math.max(1, endOffset - startOffset);
1527
+
1528
+ const bytesPerSample = 2; // 16-bit PCM
1529
+ const blockAlign = numChannels * bytesPerSample;
1530
+ const byteRate = sampleRate * blockAlign;
1531
+ const dataLength = frameCount * blockAlign;
1532
+ const bufferLength = 44 + dataLength;
1533
+
1534
+ const arrayBuffer = new ArrayBuffer(bufferLength);
1535
+ const view = new DataView(arrayBuffer);
1536
+
1537
+ function writeString(offset, str) {
1538
+ for (let i = 0; i < str.length; i++) {
1539
+ view.setUint8(offset + i, str.charCodeAt(i));
1540
+ }
1541
+ }
1542
+
1543
+ writeString(0, 'RIFF');
1544
+ view.setUint32(4, 36 + dataLength, true);
1545
+ writeString(8, 'WAVE');
1546
+ writeString(12, 'fmt ');
1547
+ view.setUint32(16, 16, true);
1548
+ view.setUint16(20, 1, true);
1549
+ view.setUint16(22, numChannels, true);
1550
+ view.setUint32(24, sampleRate, true);
1551
+ view.setUint32(28, byteRate, true);
1552
+ view.setUint16(32, blockAlign, true);
1553
+ view.setUint16(34, 16, true);
1554
+ writeString(36, 'data');
1555
+ view.setUint32(40, dataLength, true);
1556
+
1557
+ let offset = 44;
1558
+ const channels = [];
1559
+ for (let ch = 0; ch < numChannels; ch++) {
1560
+ channels.push(buf.getChannelData(ch));
1561
+ }
1562
+
1563
+ for (let i = 0; i < frameCount; i++) {
1564
+ for (let ch = 0; ch < numChannels; ch++) {
1565
+ const sample = Math.max(-1, Math.min(1, channels[ch][startOffset + i] || 0));
1566
+ const intSample = sample < 0 ? sample * 0x8000 : sample * 0x7FFF;
1567
+ view.setInt16(offset, intSample, true);
1568
+ offset += 2;
1569
+ }
1570
+ }
1571
+
1572
+ return new Blob([view], { type: 'audio/wav' });
1573
+ }
1574
+ </script>
1575
+
1576
+ </body>
1577
+ </html>