File size: 148,633 Bytes
0c80cf5
 
 
 
 
 
6f54a36
 
 
0c80cf5
 
6f54a36
0c80cf5
 
 
6f54a36
0c80cf5
6f54a36
 
 
 
 
 
0c80cf5
c8cd5e5
 
 
 
 
 
 
 
 
 
6f54a36
 
0c80cf5
6f54a36
0c80cf5
6f54a36
0c80cf5
6f54a36
0c80cf5
 
 
 
 
 
6f54a36
 
 
 
 
0c80cf5
 
6f54a36
 
c8cd5e5
 
 
6f54a36
 
 
 
0c80cf5
c8cd5e5
 
 
 
 
 
 
 
 
 
 
 
 
 
6f54a36
0c80cf5
 
 
 
6f54a36
 
 
 
 
 
0c80cf5
6f54a36
 
 
 
0c80cf5
c8cd5e5
 
 
 
 
6f54a36
 
 
 
 
 
 
 
 
 
c8cd5e5
 
 
6f54a36
 
 
 
 
 
 
 
c8cd5e5
 
 
 
 
 
 
 
 
 
0c80cf5
 
 
 
 
c8cd5e5
0c80cf5
 
 
 
 
c8cd5e5
 
0c80cf5
c8cd5e5
 
 
 
 
 
 
0c80cf5
c8cd5e5
0c80cf5
 
 
c8cd5e5
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
6f54a36
0c80cf5
 
c8cd5e5
 
 
0c80cf5
 
 
 
6f54a36
0c80cf5
 
 
 
c8cd5e5
0c80cf5
c8cd5e5
0c80cf5
 
 
6f54a36
0c80cf5
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
6f54a36
0c80cf5
 
c8cd5e5
0c80cf5
 
 
 
 
 
6f54a36
0c80cf5
 
c8cd5e5
0c80cf5
 
c8cd5e5
 
 
 
 
0c80cf5
 
6f54a36
0c80cf5
6f54a36
0c80cf5
 
 
 
 
 
 
 
 
c8cd5e5
 
 
 
 
 
 
0c80cf5
 
 
 
 
c8cd5e5
0c80cf5
 
 
 
c8cd5e5
 
 
 
 
 
 
 
 
 
 
 
 
 
 
0c80cf5
 
 
 
 
 
 
 
6f54a36
0c80cf5
 
 
 
c8cd5e5
 
 
 
0c80cf5
c8cd5e5
 
0c80cf5
 
 
 
c8cd5e5
0c80cf5
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
c8cd5e5
 
 
 
 
 
 
 
 
 
0c80cf5
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
6f54a36
0c80cf5
6f54a36
0c80cf5
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
6f54a36
0c80cf5
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
c8cd5e5
 
 
0c80cf5
 
 
 
c8cd5e5
 
 
 
 
 
 
 
 
 
0c80cf5
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
6f54a36
c8cd5e5
 
6f54a36
c8cd5e5
 
 
 
 
 
 
 
 
 
 
6f54a36
 
c8cd5e5
 
6f54a36
c8cd5e5
6f54a36
c8cd5e5
 
 
 
 
 
6f54a36
 
 
 
 
 
0c80cf5
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
c8cd5e5
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
0c80cf5
c8cd5e5
0c80cf5
 
 
c8cd5e5
 
 
 
 
0c80cf5
 
 
 
 
 
 
c8cd5e5
 
 
 
 
0c80cf5
 
 
 
 
 
 
c8cd5e5
 
 
 
 
0c80cf5
 
 
 
 
 
 
 
c8cd5e5
 
 
 
 
0c80cf5
 
 
 
 
 
 
 
 
 
 
c8cd5e5
 
 
 
 
0c80cf5
 
 
c8cd5e5
0c80cf5
 
 
c8cd5e5
 
 
 
 
 
0c80cf5
 
 
 
 
 
 
 
c8cd5e5
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
0c80cf5
 
 
 
 
 
 
 
c8cd5e5
 
 
 
 
0c80cf5
 
 
 
 
 
 
 
c8cd5e5
 
 
 
 
0c80cf5
 
 
c8cd5e5
 
 
 
 
 
 
 
 
 
0c80cf5
 
 
 
 
 
 
 
c8cd5e5
 
 
 
 
0c80cf5
 
 
c8cd5e5
0c80cf5
 
 
 
c8cd5e5
 
 
 
 
0c80cf5
 
 
 
c8cd5e5
 
 
 
0c80cf5
c8cd5e5
0c80cf5
 
 
 
 
 
c8cd5e5
 
0c80cf5
 
c8cd5e5
 
 
 
 
 
 
0c80cf5
c8cd5e5
0c80cf5
c8cd5e5
 
 
0c80cf5
 
 
 
 
c8cd5e5
 
 
0c80cf5
 
c8cd5e5
0c80cf5
c8cd5e5
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
0c80cf5
 
 
 
 
 
 
 
 
c8cd5e5
 
 
0c80cf5
 
 
 
 
 
 
 
 
 
 
c8cd5e5
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
0c80cf5
 
 
 
 
 
 
c8cd5e5
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
0c80cf5
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
c8cd5e5
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
0c80cf5
 
c8cd5e5
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
0c80cf5
 
 
 
 
 
 
 
 
 
 
 
c8cd5e5
0c80cf5
c8cd5e5
 
0c80cf5
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
c8cd5e5
0c80cf5
 
 
 
c8cd5e5
 
 
0c80cf5
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
c8cd5e5
 
 
 
 
0c80cf5
c8cd5e5
 
0c80cf5
c8cd5e5
0c80cf5
c8cd5e5
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
0c80cf5
 
c8cd5e5
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
0c80cf5
 
 
 
 
c8cd5e5
 
 
 
0c80cf5
 
c8cd5e5
 
 
0c80cf5
 
 
 
 
c8cd5e5
 
 
 
 
 
 
 
 
 
0c80cf5
c8cd5e5
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
0c80cf5
c8cd5e5
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
0c80cf5
c8cd5e5
 
 
 
6f54a36
0c80cf5
 
 
 
 
 
 
6f54a36
0c80cf5
 
 
 
 
 
 
 
 
 
 
 
c8cd5e5
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
0c80cf5
 
 
c8cd5e5
 
 
 
0c80cf5
c8cd5e5
 
 
 
 
 
 
 
 
 
0c80cf5
 
c8cd5e5
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
0c80cf5
c8cd5e5
 
 
 
 
 
 
 
0c80cf5
 
c8cd5e5
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
0c80cf5
 
 
 
 
 
 
 
 
 
 
 
 
c8cd5e5
0c80cf5
c8cd5e5
0c80cf5
 
c8cd5e5
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
0c80cf5
 
 
 
 
 
 
 
 
 
 
c8cd5e5
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
0c80cf5
c8cd5e5
 
0c80cf5
 
 
 
c8cd5e5
0c80cf5
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
c8cd5e5
 
 
 
 
 
 
 
 
 
 
 
 
 
0c80cf5
 
 
 
 
 
 
 
 
 
 
 
c8cd5e5
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
0c80cf5
 
 
 
 
c8cd5e5
0c80cf5
 
 
 
c8cd5e5
0c80cf5
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
c8cd5e5
 
 
 
0c80cf5
 
 
 
 
c8cd5e5
 
0c80cf5
 
 
 
c8cd5e5
 
 
 
0c80cf5
 
c8cd5e5
 
 
 
 
 
 
 
0c80cf5
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
c8cd5e5
 
 
 
 
 
 
 
 
 
 
 
 
0c80cf5
 
 
 
 
 
 
c8cd5e5
0c80cf5
 
c8cd5e5
0c80cf5
 
 
 
 
 
 
 
 
 
c8cd5e5
0c80cf5
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
c8cd5e5
 
 
0c80cf5
c8cd5e5
 
 
 
 
 
 
 
 
 
0c80cf5
 
c8cd5e5
 
0c80cf5
c8cd5e5
 
 
 
 
 
0c80cf5
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
c8cd5e5
0c80cf5
 
 
 
 
 
c8cd5e5
 
0c80cf5
 
 
 
 
 
c8cd5e5
 
 
0c80cf5
 
 
 
 
 
c8cd5e5
0c80cf5
 
 
 
 
 
 
 
 
 
 
 
 
c8cd5e5
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
0c80cf5
 
 
 
 
 
 
c8cd5e5
0c80cf5
 
 
 
c8cd5e5
0c80cf5
 
c8cd5e5
 
 
0c80cf5
 
 
c8cd5e5
 
 
0c80cf5
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
c8cd5e5
 
 
 
 
 
 
 
0c80cf5
 
 
 
 
c8cd5e5
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
0c80cf5
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
1148
1149
1150
1151
1152
1153
1154
1155
1156
1157
1158
1159
1160
1161
1162
1163
1164
1165
1166
1167
1168
1169
1170
1171
1172
1173
1174
1175
1176
1177
1178
1179
1180
1181
1182
1183
1184
1185
1186
1187
1188
1189
1190
1191
1192
1193
1194
1195
1196
1197
1198
1199
1200
1201
1202
1203
1204
1205
1206
1207
1208
1209
1210
1211
1212
1213
1214
1215
1216
1217
1218
1219
1220
1221
1222
1223
1224
1225
1226
1227
1228
1229
1230
1231
1232
1233
1234
1235
1236
1237
1238
1239
1240
1241
1242
1243
1244
1245
1246
1247
1248
1249
1250
1251
1252
1253
1254
1255
1256
1257
1258
1259
1260
1261
1262
1263
1264
1265
1266
1267
1268
1269
1270
1271
1272
1273
1274
1275
1276
1277
1278
1279
1280
1281
1282
1283
1284
1285
1286
1287
1288
1289
1290
1291
1292
1293
1294
1295
1296
1297
1298
1299
1300
1301
1302
1303
1304
1305
1306
1307
1308
1309
1310
1311
1312
1313
1314
1315
1316
1317
1318
1319
1320
1321
1322
1323
1324
1325
1326
1327
1328
1329
1330
1331
1332
1333
1334
1335
1336
1337
1338
1339
1340
1341
1342
1343
1344
1345
1346
1347
1348
1349
1350
1351
1352
1353
1354
1355
1356
1357
1358
1359
1360
1361
1362
1363
1364
1365
1366
1367
1368
1369
1370
1371
1372
1373
1374
1375
1376
1377
1378
1379
1380
1381
1382
1383
1384
1385
1386
1387
1388
1389
1390
1391
1392
1393
1394
1395
1396
1397
1398
1399
1400
1401
1402
1403
1404
1405
1406
1407
1408
1409
1410
1411
1412
1413
1414
1415
1416
1417
1418
1419
1420
1421
1422
1423
1424
1425
1426
1427
1428
1429
1430
1431
1432
1433
1434
1435
1436
1437
1438
1439
1440
1441
1442
1443
1444
1445
1446
1447
1448
1449
1450
1451
1452
1453
1454
1455
1456
1457
1458
1459
1460
1461
1462
1463
1464
1465
1466
1467
1468
1469
1470
1471
1472
1473
1474
1475
1476
1477
1478
1479
1480
1481
1482
1483
1484
1485
1486
1487
1488
1489
1490
1491
1492
1493
1494
1495
1496
1497
1498
1499
1500
1501
1502
1503
1504
1505
1506
1507
1508
1509
1510
1511
1512
1513
1514
1515
1516
1517
1518
1519
1520
1521
1522
1523
1524
1525
1526
1527
1528
1529
1530
1531
1532
1533
1534
1535
1536
1537
1538
1539
1540
1541
1542
1543
1544
1545
1546
1547
1548
1549
1550
1551
1552
1553
1554
1555
1556
1557
1558
1559
1560
1561
1562
1563
1564
1565
1566
1567
1568
1569
1570
1571
1572
1573
1574
1575
1576
1577
1578
1579
1580
1581
1582
1583
1584
1585
1586
1587
1588
1589
1590
1591
1592
1593
1594
1595
1596
1597
1598
1599
1600
1601
1602
1603
1604
1605
1606
1607
1608
1609
1610
1611
1612
1613
1614
1615
1616
1617
1618
1619
1620
1621
1622
1623
1624
1625
1626
1627
1628
1629
1630
1631
1632
1633
1634
1635
1636
1637
1638
1639
1640
1641
1642
1643
1644
1645
1646
1647
1648
1649
1650
1651
1652
1653
1654
1655
1656
1657
1658
1659
1660
1661
1662
1663
1664
1665
1666
1667
1668
1669
1670
1671
1672
1673
1674
1675
1676
1677
1678
1679
1680
1681
1682
1683
1684
1685
1686
1687
1688
1689
1690
1691
1692
1693
1694
1695
1696
1697
1698
1699
1700
1701
1702
1703
1704
1705
1706
1707
1708
1709
1710
1711
1712
1713
1714
1715
1716
1717
1718
1719
1720
1721
1722
1723
1724
1725
1726
1727
1728
1729
1730
1731
1732
1733
1734
1735
1736
1737
1738
1739
1740
1741
1742
1743
1744
1745
1746
1747
1748
1749
1750
1751
1752
1753
1754
1755
1756
1757
1758
1759
1760
1761
1762
1763
1764
1765
1766
1767
1768
1769
1770
1771
1772
1773
1774
1775
1776
1777
1778
1779
1780
1781
1782
1783
1784
1785
1786
1787
1788
1789
1790
1791
1792
1793
1794
1795
1796
1797
1798
1799
1800
1801
1802
1803
1804
1805
1806
1807
1808
1809
1810
1811
1812
1813
1814
1815
1816
1817
1818
1819
1820
1821
1822
1823
1824
1825
1826
1827
1828
1829
1830
1831
1832
1833
1834
1835
1836
1837
1838
1839
1840
1841
1842
1843
1844
1845
1846
1847
1848
1849
1850
1851
1852
1853
1854
1855
1856
1857
1858
1859
1860
1861
1862
1863
1864
1865
1866
1867
1868
1869
1870
1871
1872
1873
1874
1875
1876
1877
1878
1879
1880
1881
1882
1883
1884
1885
1886
1887
1888
1889
1890
1891
1892
1893
1894
1895
1896
1897
1898
1899
1900
1901
1902
1903
1904
1905
1906
1907
1908
1909
1910
1911
1912
1913
1914
1915
1916
1917
1918
1919
1920
1921
1922
1923
1924
1925
1926
1927
1928
1929
1930
1931
1932
1933
1934
1935
1936
1937
1938
1939
1940
1941
1942
1943
1944
1945
1946
1947
1948
1949
1950
1951
1952
1953
1954
1955
1956
1957
1958
1959
1960
1961
1962
1963
1964
1965
1966
1967
1968
1969
1970
1971
1972
1973
1974
1975
1976
1977
1978
1979
1980
1981
1982
1983
1984
1985
1986
1987
1988
1989
1990
1991
1992
1993
1994
1995
1996
1997
1998
1999
2000
2001
2002
2003
2004
2005
2006
2007
2008
2009
2010
2011
2012
2013
2014
2015
2016
2017
2018
2019
2020
2021
2022
2023
2024
2025
2026
2027
2028
2029
2030
2031
2032
2033
2034
2035
2036
2037
2038
2039
2040
2041
2042
2043
2044
2045
2046
2047
2048
2049
2050
2051
2052
2053
2054
2055
2056
2057
2058
2059
2060
2061
2062
2063
2064
2065
2066
2067
2068
2069
2070
2071
2072
2073
2074
2075
2076
2077
2078
2079
2080
2081
2082
2083
2084
2085
2086
2087
2088
2089
2090
2091
2092
2093
2094
2095
2096
2097
2098
2099
2100
2101
2102
2103
2104
2105
2106
2107
2108
2109
2110
2111
2112
2113
2114
2115
2116
2117
2118
2119
2120
2121
2122
2123
2124
2125
2126
2127
2128
2129
2130
2131
2132
2133
2134
2135
2136
2137
2138
2139
2140
2141
2142
2143
2144
2145
2146
2147
2148
2149
2150
2151
2152
2153
2154
2155
2156
2157
2158
2159
2160
2161
2162
2163
2164
2165
2166
2167
2168
2169
2170
2171
2172
2173
2174
2175
2176
2177
2178
2179
2180
2181
2182
2183
2184
2185
2186
2187
2188
2189
2190
2191
2192
2193
2194
2195
2196
2197
2198
2199
2200
2201
2202
2203
2204
2205
2206
2207
2208
2209
2210
2211
2212
2213
2214
2215
2216
2217
2218
2219
2220
2221
2222
2223
2224
2225
2226
2227
2228
2229
2230
2231
2232
2233
2234
2235
2236
2237
2238
2239
2240
2241
2242
2243
2244
2245
2246
2247
2248
2249
2250
2251
2252
2253
2254
2255
2256
2257
2258
2259
2260
2261
2262
2263
2264
2265
2266
2267
2268
2269
2270
2271
2272
2273
2274
2275
2276
2277
2278
2279
2280
2281
2282
2283
2284
2285
2286
2287
2288
2289
2290
2291
2292
2293
2294
2295
2296
2297
2298
2299
2300
2301
2302
2303
2304
2305
2306
2307
2308
2309
2310
2311
2312
2313
2314
2315
2316
2317
2318
2319
2320
2321
2322
2323
2324
2325
2326
2327
2328
2329
2330
2331
2332
2333
2334
2335
2336
2337
2338
2339
2340
2341
2342
2343
2344
2345
2346
2347
2348
2349
2350
2351
2352
2353
2354
2355
2356
2357
2358
2359
2360
2361
2362
2363
2364
2365
2366
2367
2368
2369
2370
2371
2372
2373
2374
2375
2376
2377
2378
2379
2380
2381
2382
2383
2384
2385
2386
2387
2388
2389
2390
2391
2392
2393
2394
2395
2396
2397
2398
2399
2400
2401
2402
2403
2404
2405
2406
2407
2408
2409
2410
2411
2412
2413
2414
2415
2416
2417
2418
2419
2420
2421
2422
2423
2424
2425
2426
2427
2428
2429
2430
2431
2432
2433
2434
2435
2436
2437
2438
2439
2440
2441
2442
2443
2444
2445
2446
2447
2448
2449
2450
2451
2452
2453
2454
2455
2456
2457
2458
2459
2460
2461
2462
2463
2464
2465
2466
2467
2468
2469
2470
2471
2472
2473
2474
2475
2476
2477
2478
2479
2480
2481
2482
2483
2484
2485
2486
2487
2488
2489
2490
2491
2492
2493
2494
2495
2496
2497
2498
2499
2500
2501
2502
2503
2504
2505
2506
2507
2508
2509
2510
2511
2512
2513
2514
2515
2516
2517
2518
2519
2520
2521
2522
2523
2524
2525
2526
2527
2528
2529
2530
2531
2532
2533
2534
2535
2536
2537
2538
2539
2540
2541
2542
2543
2544
2545
2546
2547
2548
2549
2550
2551
2552
2553
2554
2555
2556
2557
2558
2559
2560
2561
2562
2563
2564
2565
2566
2567
2568
2569
2570
2571
2572
2573
2574
2575
2576
2577
2578
2579
2580
2581
2582
2583
2584
2585
2586
2587
2588
2589
2590
2591
2592
2593
2594
2595
2596
2597
2598
2599
2600
2601
2602
2603
2604
2605
2606
2607
2608
2609
2610
2611
2612
2613
2614
2615
2616
2617
2618
2619
2620
2621
2622
2623
2624
2625
2626
2627
2628
2629
2630
2631
2632
2633
2634
2635
2636
2637
2638
<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>SNK MASTER IA - Agente IA de Mastering</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Orbitron:wght@500;700;900&family=Rajdhani:wght@400;500;600;700&family=Share+Tech+Mono&display=swap" rel="stylesheet">
<style>
  * { margin: 0; padding: 0; box-sizing: border-box; }
  body { background: radial-gradient(1200px 800px at 70% -10%, #1b4dff 0%, #0d216e 45%, #040a2e 72%, #02051a 100%); background-attachment: fixed; color: #eaf0ff; font-family: 'Rajdhani', 'Segoe UI', system-ui, sans-serif; min-height: 100vh; display: flex; justify-content: center; }
  .wrap { width: 1140px; max-width: 100%; padding: 18px 14px 40px; }
  header { position: relative; display: flex; align-items: center; justify-content: center; margin-bottom: 16px; flex-wrap: wrap; gap: 12px; padding: 8px 0; }
  .titulo { text-align: center; }
  .vers { position: absolute; right: 0; top: 10px; font-size: 12px; color: #7ee7ff; border: 1px solid rgba(0, 180, 255, .5); background: rgba(0, 40, 90, .35); padding: 3px 10px; border-radius: 20px; letter-spacing: 2px; box-shadow: 0 0 14px rgba(0, 200, 255, .35); white-space: nowrap; }
  @media (max-width: 520px) { .vers { position: static; } }
  h1 { font-size: 30px; letter-spacing: 6px; font-family: 'Orbitron', 'Share Tech Mono', 'Bahnschrift', 'Segoe UI', sans-serif; font-weight: 800; text-transform: uppercase; color: #ffffff; text-shadow: 0 0 8px rgba(255, 255, 255, .9), 0 0 22px rgba(0, 220, 255, .85), 0 0 60px rgba(0, 150, 255, .55), 0 0 100px rgba(0, 120, 255, .4); -webkit-text-stroke: 1px rgba(0, 220, 255, .35); }
  h1 small { display: block; font-size: 11px; color: #ffb866; letter-spacing: 4px; margin-top: 4px; font-family: 'Rajdhani', 'Share Tech Mono', 'Segoe UI', sans-serif; text-shadow: 0 0 12px rgba(255, 140, 40, .55); }
  .panel { background: linear-gradient(180deg, #081228 0%, #050b1e 100%); border: 1px solid rgba(0, 160, 255, .35); border-radius: 14px; padding: 14px; margin-bottom: 14px; box-shadow: 0 6px 22px rgba(0, 0, 0, .55), inset 0 1px 0 rgba(30, 190, 255, .1), inset 0 0 40px rgba(0, 120, 255, .05); }
  .panel.etapa { background: #000000; border-color: rgba(255, 140, 0, .5); box-shadow: 0 6px 26px rgba(0, 0, 0, .75), inset 0 0 50px rgba(0, 0, 0, .85), 0 0 10px rgba(255, 140, 0, .25); }
  .panel h2 { font-size: 13px; letter-spacing: 3px; color: #66d9ff; margin-bottom: 12px; text-transform: uppercase; font-family: 'Rajdhani', 'Bahnschrift', 'Segoe UI', sans-serif; text-shadow: 0 0 10px rgba(90, 210, 255, .6); }
  .panel.etapa h2 { color: #ff9433; text-shadow: 0 0 12px rgba(255, 130, 0, .7); }
  .row { display: flex; gap: 12px; flex-wrap: wrap; align-items: flex-end; justify-content: center; }
  .dd { position: relative; display: inline-block; }
  .dd .caret { font-size: 10px; margin-left: 5px; display: inline-block; transition: transform .18s; }
  .dd.open .caret { transform: rotate(180deg); }
  .ddmenu { position: absolute; top: calc(100% + 6px); left: 50%; transform: translateX(-50%); background: #000000; border: 1px solid rgba(0, 179, 255, .6); border-radius: 9px; min-width: 130px; padding: 5px; display: none; z-index: 200; box-shadow: 0 0 16px rgba(0, 179, 255, .45), 0 0 42px rgba(0, 120, 255, .25), inset 0 0 16px rgba(0, 120, 255, .08); }
  .dd.open .ddmenu { display: block; }
  .ddmenu button { display: block; width: 100%; background: #000000; color: #cfe4ff; border: 1px solid rgba(0, 179, 255, .35); border-radius: 6px; padding: 8px 16px; text-align: center; font-size: 12px; letter-spacing: 3px; font-family: 'Share Tech Mono', 'Rajdhani', 'Segoe UI', sans-serif; text-transform: uppercase; text-shadow: 0 0 8px rgba(0, 180, 255, .5); transition: .15s; cursor: pointer; }
  .ddmenu button + button { margin-top: 4px; }
  .ddmenu button:hover:not(:disabled) { background: #000000; border-color: #ff6600; color: #ff8c1a; box-shadow: 0 0 16px rgba(255, 102, 0, .9), inset 0 0 12px rgba(255, 102, 0, .3); text-shadow: 0 0 10px rgba(255, 160, 40, 1), 0 0 22px rgba(255, 120, 0, .7); }
  .ddmenu button:disabled { opacity: .38; cursor: not-allowed; box-shadow: none; border-color: #2b3a63; color: #5b6fa6; text-shadow: none; }
  .dd.off .ddmenu { display: none !important; }
  button { background: #0a1634; color: #eaf0ff; border: 1px solid rgba(0, 160, 255, .4); border-radius: 9px; padding: 9px 14px; cursor: pointer; font-size: 13px; transition: .18s; font-family: 'Rajdhani', 'Share Tech Mono', 'Segoe UI', sans-serif; letter-spacing: 1.5px; text-shadow: 0 0 8px rgba(180, 230, 255, .6); }
  button:hover { background: #0d1f4d; border-color: #45c4ff; box-shadow: 0 0 18px rgba(0, 200, 255, .8), inset 0 0 12px rgba(0, 170, 255, .35); text-shadow: 0 0 14px rgba(120, 230, 255, .95); color: #ffffff; }
  button.acc { background: linear-gradient(180deg, #14b06a, #0d8f58); border-color: #2fd48f; }
  button.acc:hover { background: linear-gradient(180deg, #1cc77a, #12a365); box-shadow: 0 0 18px rgba(47, 212, 143, .65), inset 0 0 12px rgba(47, 212, 143, .3); }
  button.warn { background: linear-gradient(180deg, #c26a2f, #a34e22); border-color: #e0855a; }
  button.warn:hover { background: linear-gradient(180deg, #d97a3a, #b85c2b); box-shadow: 0 0 18px rgba(224, 133, 90, .65), inset 0 0 12px rgba(224, 133, 90, .3); }
  button:disabled { opacity: .38; cursor: not-allowed; box-shadow: none; }
  button.active { background: linear-gradient(180deg, #4db8ff, #3d9ae0); border-color: #7fd6ff; color: #071018; font-weight: 600; box-shadow: 0 0 18px rgba(90, 200, 255, .6); }
  button.active:hover { background: linear-gradient(180deg, #5fc4ff, #48a8ee); }
  input[type=file] { display: none; }
  .hint { font-size: 12px; color: #8f99bd; line-height: 1.6; }
  .bandanze { display: flex; justify-content: space-between; align-items: stretch; gap: 6px; height: 200px; }
  .band { flex: 1; text-align: center; height: 100%; display: flex; flex-direction: column; align-items: center; }
  .band input[type=range] { -webkit-appearance: none; appearance: none; writing-mode: vertical-lr; direction: rtl; width: 26px; height: 145px; margin: 6px auto 2px; display: block; background: transparent; cursor: pointer; }
  .band input[type=range]::-webkit-slider-runnable-track { width: 8px; background: linear-gradient(180deg, #ffaa00 0%, #ff6600 100%); border-radius: 4px; box-shadow: 0 0 14px rgba(255, 120, 0, .6), 0 0 28px rgba(255, 90, 0, .35), inset 0 0 6px rgba(255, 255, 255, .3); }
  .band input[type=range]::-webkit-slider-thumb { -webkit-appearance: none; width: 24px; height: 8px; background: #ffffff; border-radius: 4px; margin-left: -8px; box-shadow: 0 0 12px rgba(255, 170, 0, 1), 0 0 26px rgba(255, 120, 0, .8); }
  .band .freq { font-size: 10px; color: #ffb866; display: block; letter-spacing: .5px; text-shadow: 0 0 8px rgba(255, 140, 40, .4); }
  .band .val { font-size: 11px; color: #ffa53d; display: block; height: 14px; font-family: 'Share Tech Mono', 'Rajdhani', monospace; text-shadow: 0 0 10px rgba(255, 160, 0, .9); }
  #curva { width: 100%; height: 120px; margin-top: 10px; background: #05070f; border: 1px solid rgba(0, 160, 255, .35); border-radius: 9px; display: block; box-shadow: inset 0 0 30px rgba(0, 160, 255, .06); }
  .compgrid { display: grid; grid-template-columns: repeat(auto-fit, minmax(140px, 1fr)); gap: 12px; }
  .knob { text-align: center; }
  .knob label { display: block; font-size: 11px; color: #ffb866; margin-bottom: 4px; letter-spacing: 1.5px; font-family: 'Rajdhani', 'Share Tech Mono', sans-serif; text-shadow: 0 0 8px rgba(255, 140, 40, .35); }
  .knob .out { font-size: 16px; color: #ffffff; margin-top: 4px; display: block; font-family: 'Share Tech Mono', 'Rajdhani', 'Courier New', monospace; font-weight: 700; letter-spacing: 1.5px; text-shadow: 0 0 12px rgba(255, 170, 0, .95), 0 0 28px rgba(255, 120, 0, .6); }
  .knob input[type=range] { -webkit-appearance: none; appearance: none; width: 100%; height: 8px; border-radius: 6px; cursor: pointer; outline: none; padding: 0; margin: 0; border: none; background: linear-gradient(90deg, #ffaa00 0%, #ff6600 100%); box-shadow: 0 0 14px rgba(255, 150, 0, .65), 0 0 30px rgba(255, 100, 0, .4), inset 0 0 6px rgba(255, 255, 255, .35); }
  .knob input[type=range]::-webkit-slider-runnable-track { width: 100%; height: 8px; border-radius: 6px; background: transparent; }
  .knob input[type=range]::-moz-range-track { width: 100%; height: 8px; border-radius: 6px; background: transparent; }
  .knob input[type=range]::-webkit-slider-thumb { -webkit-appearance: none; appearance: none; width: 18px; height: 18px; margin-top: -5px; border-radius: 50%; background: radial-gradient(circle at 35% 35%, #ffffff 0%, #ffe0b3 35%, #ffaa00 70%, #ff6600 100%); border: 2px solid #ffffff; box-shadow: 0 0 12px rgba(255, 170, 0, 1), 0 0 30px rgba(255, 110, 0, .8), 0 0 45px rgba(255, 140, 0, .6); }
  .knob input[type=range]::-moz-range-thumb { width: 18px; height: 18px; border-radius: 50%; background: radial-gradient(circle at 35% 35%, #ffffff 0%, #ffe0b3 35%, #ffaa00 70%, #ff6600 100%); border: 2px solid #ffffff; box-shadow: 0 0 12px rgba(255, 170, 0, 1), 0 0 30px rgba(255, 110, 0, .8); }
  #pbarWrap { width: 100%; height: 18px; background: #05070f; border: 1px solid rgba(255, 150, 0, .5); border-radius: 6px; margin-top: 12px; overflow: hidden; box-shadow: inset 0 0 10px rgba(0, 0, 0, .8), 0 0 10px rgba(255, 130, 0, .25); }
  #pbarFill { height: 100%; width: 0%; background: linear-gradient(90deg,#ffaa00,#ff6600); transition: width .1s linear; box-shadow: 0 0 16px rgba(255, 110, 0, .85), 0 0 30px rgba(255, 150, 0, .55); }
  .viz { display: flex; gap: 12px; flex-wrap: wrap; }
  /* ---- Vista en tiempo real: panel de reproducción en vivo ---- */
  .panel.rtlive { background: #000000; border-color: rgba(0, 179, 255, .45); box-shadow: 0 6px 26px rgba(0, 0, 0, .75), inset 0 0 50px rgba(0, 0, 0, .85), 0 0 14px rgba(0, 179, 255, .25); margin-bottom: 18px; }
  .panel.rtlive h2 { color: #33c9ff; text-shadow: 0 0 12px rgba(0, 190, 255, .75); }
  .tlrow { display: flex; align-items: center; gap: 10px; margin-top: 12px; }
  .tl-time { font-family: 'Share Tech Mono', 'Rajdhani', monospace; font-size: 13px; color: #ffb866; text-shadow: 0 0 10px rgba(255, 140, 40, .6); min-width: 44px; text-align: center; letter-spacing: 1px; }
  #seekBar { -webkit-appearance: none; appearance: none; flex: 1; height: 8px; border-radius: 6px; cursor: pointer; outline: none; background: linear-gradient(90deg, #ffaa00 0%, #ff6600 100%); box-shadow: 0 0 14px rgba(255, 150, 0, .7), 0 0 30px rgba(255, 100, 0, .4), inset 0 0 6px rgba(255, 255, 255, .35); }
  #seekBar:disabled { opacity: .35; cursor: not-allowed; }
  #seekBar::-webkit-slider-thumb { -webkit-appearance: none; width: 18px; height: 18px; border-radius: 50%; margin-top: -5px; background: radial-gradient(circle at 35% 35%, #ffffff 0%, #ffe0b3 35%, #ffaa00 70%, #ff6600 100%); border: 2px solid #ffffff; box-shadow: 0 0 12px rgba(255, 170, 0, 1), 0 0 30px rgba(255, 110, 0, .8), 0 0 45px rgba(255, 140, 0, .6); }
  #seekBar::-moz-range-thumb { width: 18px; height: 18px; border-radius: 50%; background: radial-gradient(circle at 35% 35%, #ffffff 0%, #ffe0b3 35%, #ffaa00 70%, #ff6600 100%); border: 2px solid #ffffff; box-shadow: 0 0 12px rgba(255, 170, 0, 1), 0 0 30px rgba(255, 110, 0, .8); }
  .transport { display: flex; justify-content: center; gap: 12px; margin-top: 12px; flex-wrap: wrap; }
  .tbtn { background: #000000; color: #eaf0ff; border: 1.5px solid #00b3ff; border-radius: 9px; padding: 8px 18px; cursor: pointer; font-size: 24px; line-height: 1; font-family: 'Segoe UI Symbol', 'Segoe UI', 'Arial Unicode MS', sans-serif; letter-spacing: 0; transition: .18s; text-shadow: 0 0 8px rgba(180, 230, 255, .6); min-width: 52px; }
  .tbtn:hover { background: #070d1f; border-color: #ff6600; box-shadow: 0 0 18px rgba(255, 102, 0, .9), inset 0 0 12px rgba(255, 102, 0, .35); color: #ffffff; text-shadow: 0 0 14px rgba(255, 160, 60, .95); }
  .tbtn:disabled { opacity: .35; cursor: not-allowed; box-shadow: none; border-color: #2b3a63; }
  .tbtn.act { border-color: #ff6600; box-shadow: 0 0 14px rgba(255, 102, 0, .6), inset 0 0 10px rgba(255, 102, 0, .2); color: #ffb866; }
  canvas { background: #05070f; border: 1px solid rgba(0, 160, 255, .35); border-radius: 9px; box-shadow: inset 0 0 30px rgba(0, 160, 255, .06), 0 0 14px rgba(0, 0, 0, .5); }
  canvas#spec { flex: 2; min-width: 300px; height: 140px; }
  canvas#wave { flex: 1; min-width: 210px; height: 140px; }
  .doscol { display: grid; grid-template-columns: 1fr 1fr; gap: 14px; }
  @media (max-width: 820px) { .doscol { grid-template-columns: 1fr; } }
  #iaseg { font-size: 13px; line-height: 1.8; color: #d7e2ff; max-height: 360px; overflow-y: auto; }
  #iaseg b { color: #ffa53d; text-shadow: 0 0 10px rgba(255, 160, 0, .65); }
  .estadistica { display: flex; gap: 16px; flex-wrap: wrap; font-size: 12px; color: #9fb6e6; margin-top: 8px; }
  .estadistica b { color: #ffffff; font-family: 'Share Tech Mono', 'Rajdhani', monospace; text-shadow: 0 0 8px rgba(255, 170, 0, .6); }
  footer { text-align: center; font-size: 11px; color: #5b6fa6; margin-top: 8px; letter-spacing: 1.5px; }
  #agenteChat { height: 175px; overflow-y: auto; background: #05070f; border: 1px solid rgba(0, 160, 255, .35); border-radius: 9px; padding: 10px; font-size: 12px; line-height: 1.6; margin-top: 10px; box-shadow: inset 0 0 30px rgba(0, 160, 255, .06); }
  .chatrow { display: flex; gap: 8px; margin-top: 8px; }
  #agenteIn { flex: 1; background: #000000; border: 1.5px solid rgba(0, 179, 255, .5); color: #ffffff; border-radius: 7px; padding: 9px 12px; font-size: 13px; outline: none; font-family: 'Rajdhani', 'Segoe UI', sans-serif; letter-spacing: .5px; text-shadow: 0 0 6px rgba(255, 255, 255, .35); box-shadow: inset 0 0 10px rgba(0, 0, 0, .9); }
  #agenteIn:focus { border-color: #ff6600; box-shadow: 0 0 18px rgba(255, 102, 0, .7), 0 0 40px rgba(255, 110, 0, .35), inset 0 0 10px rgba(255, 102, 0, .1); }
  #agenteChat .m-user { color: #4f9dff; font-weight: 600; text-shadow: 0 0 8px rgba(80, 160, 255, .55); }
  #agenteChat .m-ia { color: #ffb866; font-weight: 600; text-shadow: 0 0 8px rgba(255, 160, 60, .55); }
  #agenteChat .chatmsg { margin-bottom: 6px; }
  /* ---- Indicador de IA en la nube (Hugging Face Inference API) ---- */
  #hfStatus { display: flex; align-items: center; gap: 8px; margin-top: 10px; background: #000000; border: 1.5px solid rgba(255, 102, 0, .5); border-radius: 8px; padding: 6px 12px; box-shadow: 0 0 14px rgba(255, 102, 0, .35), inset 0 0 12px rgba(255, 102, 0, .08); }
  #hfStatus .cb-ico { color: #ff6600; font-size: 13px; text-shadow: 0 0 12px rgba(255, 102, 0, .95); animation: cbPulso 1.4s ease-in-out infinite; }
  #hfStatus .cb-txt { flex: 1; font-size: 12px; letter-spacing: 1.5px; color: #ffb866; text-shadow: 0 0 8px rgba(255, 140, 40, .75); font-family: 'Share Tech Mono', 'Rajdhani', monospace; }
  @keyframes cbPulso { 0%, 100% { opacity: 1; } 50% { opacity: .35; } }
  /* ---- Secciones AGENTE IA y CHAT: pantalla holográfica ---- */
  .panel.holo { background: #000000; border: 2px solid #00b3ff; box-shadow: 0 0 0 1px rgba(0, 179, 255, .35), 0 0 20px rgba(0, 179, 255, .35), 0 0 45px rgba(0, 120, 255, .18), inset 0 0 30px rgba(0, 120, 255, .06); }
  .panel.holo h2 { color: #33c9ff; text-shadow: 0 0 12px rgba(0, 190, 255, .75); }
  .panel.holo .hint, .panel.holo #iaseg, .panel.holo .estadistica { color: #cfe4ff; }
  .panel.holo #iaseg b, .panel.holo .estadistica b { color: #ffb04d; text-shadow: 0 0 10px rgba(255, 140, 0, .6); }
  #btnEnviar { background: #000000; color: #ffffff; border: 1.5px solid #ff6600; border-radius: 8px; padding: 9px 18px; font-weight: 700; letter-spacing: 2px; text-shadow: 0 0 8px rgba(255, 102, 0, .7); box-shadow: 0 0 8px rgba(255, 102, 0, .35); }
  #btnEnviar:hover { background: #ff6600; color: #ffffff; border-color: #ffaa00; box-shadow: 0 0 22px rgba(255, 102, 0, .95), 0 0 45px rgba(255, 120, 0, .5); text-shadow: 0 0 10px rgba(255, 255, 255, .9); }
  #agenteChat .m-user { color: #4f9dff; }
  #agenteChat .m-ia { color: #ff6600; text-shadow: 0 0 10px rgba(255, 102, 0, .85), 0 0 22px rgba(255, 120, 0, .45); }
  #agenteChat b, #agenteChat .chatmsg b { color: #ff8c1a; text-shadow: 0 0 10px rgba(255, 120, 0, .75); }
  #agenteChat .chatmsg ul { margin: 4px 0 2px; padding-left: 18px; }
  #agenteChat .chatmsg li { margin: 2px 0; color: #f2e8d8; line-height: 1.45; }
  #agenteChat .chatmsg li i { color: #ff9d3d; }
  #iaseg b { color: #ffb04d; text-shadow: 0 0 10px rgba(255, 140, 0, .65); }
  /* ---- Caja de PRESETS: pantalla holográfica ---- */
  .panel.holopre { background: #000000; border: 2px solid #00b3ff; box-shadow: 0 0 0 1px rgba(0, 179, 255, .35), 0 0 20px rgba(0, 179, 255, .35), 0 0 45px rgba(0, 120, 255, .18), inset 0 0 30px rgba(0, 120, 255, .06); }
  .panel.holopre h2 { color: #33c9ff; text-shadow: 0 0 12px rgba(0, 190, 255, .75); letter-spacing: 4px; font-family: 'Orbitron', 'Rajdhani', sans-serif; }
  .presetrow button { background: #05070f; color: #ffffff; border: 1px solid #00b3ff; border-radius: 6px; padding: 10px 18px; font-size: 12px; font-family: 'Share Tech Mono', 'Rajdhani', 'Courier New', monospace; letter-spacing: 1.5px; text-shadow: 0 0 8px rgba(0, 180, 255, .55); box-shadow: 0 0 6px rgba(0, 179, 255, .25), inset 0 0 8px rgba(0, 180, 255, .1); }
  .presetrow button:hover { background: #ff6600; color: #ffffff; border-color: #ffaa00; box-shadow: 0 0 24px rgba(255, 102, 0, 1), 0 0 50px rgba(255, 130, 0, .55), inset 0 0 12px rgba(255, 255, 255, .3); text-shadow: 0 0 10px rgba(255, 255, 255, .95); transform: translateY(-1px); }
  footer { text-align: center; font-size: 12px; color: #9fb6e6; margin-top: 8px; letter-spacing: 2px; font-family: 'Share Tech Mono', 'Courier New', monospace; text-shadow: 0 0 8px rgba(0, 180, 255, .35); }
  footer::before { content: '> '; color: #ff6600; text-shadow: 0 0 8px rgba(255, 102, 0, .8); }
  /* ---- Botón de donación: minimalista, neón naranja ---- */
  .paypal-btn { display: inline-block; margin-top: 8px; background: #000000; color: #ff6600; text-decoration: none; border: 1px solid #ff6600; border-radius: 6px; padding: 6px 16px; font-family: 'Rajdhani', 'Segoe UI', sans-serif; font-size: 13px; font-weight: 700; letter-spacing: 1.2px; box-shadow: 0 0 10px rgba(255, 102, 0, .35), inset 0 0 8px rgba(255, 102, 0, .12); transition: box-shadow .25s ease, color .25s ease, text-shadow .25s ease, border-color .25s ease, transform .25s ease; }
  .paypal-btn:hover { color: #ffd9a8; border-color: #ffaa00; text-shadow: 0 0 12px rgba(255, 150, 0, 1), 0 0 26px rgba(255, 102, 0, .6); box-shadow: 0 0 22px rgba(255, 102, 0, .9), 0 0 46px rgba(255, 130, 0, .4), inset 0 0 12px rgba(255, 120, 0, .35); transform: translateY(-1px); }
  /* ---- Enlace "Community": naranja neón, negrita, hover con brillo ---- */
  .comm-link { color: #ff6600; text-shadow: 0 0 12px rgba(255, 102, 0, .85); font-weight: bold; text-decoration: none; border-bottom: 1px solid rgba(255, 102, 0, .0); transition: text-shadow .25s ease, color .25s ease, border-color .25s ease; }
  .comm-link:hover { color: #ffaa55; text-shadow: 0 0 14px rgba(255, 140, 0, 1), 0 0 30px rgba(255, 102, 0, .55); border-bottom-color: #ff6600; }
  /* ---- Botón "Manual": cápsula flotante azul eléctrico, debajo del cartel de versión ---- */
  .manual-btn { position: absolute; right: 0; top: 38px; background: transparent; color: #ffffff; border: 1px solid #00b3ff; border-radius: 20px; padding: 6px 18px; cursor: pointer; font-size: 13px; font-family: 'Rajdhani', 'Share Tech Mono', 'Segoe UI', sans-serif; letter-spacing: 1.5px; white-space: nowrap; text-shadow: 0 0 8px rgba(180, 230, 255, .6); box-shadow: 0 0 10px rgba(0, 179, 255, .25), inset 0 0 10px rgba(0, 179, 255, .08); transition: .2s; z-index: 5; }
  .manual-btn:hover { border-color: #45c4ff; box-shadow: 0 0 18px rgba(0, 200, 255, .85), 0 0 40px rgba(0, 179, 255, .45), inset 0 0 14px rgba(0, 170, 255, .25); text-shadow: 0 0 14px rgba(120, 230, 255, .95); color: #ffffff; }
  @media (max-width: 520px) { .manual-btn { position: static; margin-left: 25px; } }
</style>
</head>
<body>
<div class="wrap">
  <header>
    <span class="vers">Version: 1.0.0</span>
    <div class="titulo">
      <h1>SNK MASTER IA <small>INGENIERO DE AUDIO IA · RESTAURACIÓN DIGITAL DE CANCIONES</small></h1>
    </div>
    <div class="row">
      <button id="btnCargar" class="acc">Cargar canción</button>
      <button id="btnA" disabled>A-Original</button>
      <button id="btnB" disabled>B-Master</button>
      <button id="btnMaster" class="warn" disabled>Masterizar con IA</button>
      <div class="dd" id="ddSave">
        <button id="btnSave" disabled>Descargar master <span class="caret"></span></button>
        <div class="ddmenu">
          <button id="optWav" disabled>Wav</button>
          <button id="optMp3" disabled>Mp3</button>
        </div>
      </div>
      <button id="btnReset" title="Limpiar todo y volver a empezar">Limpiar todo</button>
      <button id="btnManual" class="manual-btn" title="Abrir el manual oficial en PDF">Manual 📖</button>
    </div>
  </header>

  <div class="panel rtlive">
    <h2>Vista en tiempo real</h2>
    <div class="viz">
      <canvas id="spec" width="640" height="200"></canvas>
      <canvas id="wave" width="430" height="200"></canvas>
    </div>
    <div class="tlrow">
      <span class="tl-time" id="tPos">0:00</span>
      <input type="range" id="seekBar" min="0" max="100" step="0.01" value="0" disabled>
      <span class="tl-time" id="tDur">0:00</span>
    </div>
    <div class="transport">
      <button id="btnR10" class="tbtn" title="Retroceder 10 segundos" disabled></button>
      <button id="btnPause" class="tbtn" title="Pausar" disabled></button>
      <button id="btnPlayStop" class="tbtn" title="Reproducir / Detener" disabled></button>
      <button id="btnF10" class="tbtn" title="Avanzar 10 segundos" disabled></button>
    </div>
  </div>

  <div class="panel etapa">
    <h2>Etapas 1-2 · Limpieza &amp; Vocal Repair</h2>
    <div class="compgrid">
      <div class="knob"><label>DE-ARTEFACTOS IA</label><input type="range" id="deEss" min="0" max="100" step="1" value="50"><span class="out" id="deEssO">50%</span></div>
      <div class="knob"><label>HPF RUMBLE 30Hz</label><input type="range" id="hpfV" min="0" max="100" step="1" value="55"><span class="out" id="hpfVO">55%</span></div>
      <div class="knob"><label>LIMPIEZA DE CAJA (300Hz)</label><input type="range" id="mudV" min="0" max="100" step="1" value="45"><span class="out" id="mudVO">45%</span></div>
      <div class="knob"><label>DE-NOISE</label><input type="range" id="denoise" min="0" max="100" step="1" value="45"><span class="out" id="denoiseO">45%</span></div>
    </div>
  </div>

  <div class="panel etapa">
    <h2>Etapas 3-4 · Rango dinámico, frecuencias y espacio</h2>
    <div class="compgrid">
      <div class="knob"><label>SUB 40-60Hz</label><input type="range" id="subAdd" min="0" max="100" step="1" value="40"><span class="out" id="subAddO">40%</span></div>
      <div class="knob"><label>AIRE 12kHz+</label><input type="range" id="airEx" min="0" max="100" step="1" value="35"><span class="out" id="airExO">35%</span></div>
      <div class="knob"><label>IMPACTO / DINÁMICA</label><input type="range" id="expDyn" min="0" max="100" step="1" value="30"><span class="out" id="expDynO">30%</span></div>
      <div class="knob"><label>LIMPIEZA MID-LOW</label><input type="range" id="midLow" min="0" max="100" step="1" value="30"><span class="out" id="midLowO">30%</span></div>
      <div class="knob"><label>ANCHO ESTÉREO</label><input type="range" id="roomK" min="0" max="100" step="1" value="0"><span class="out" id="roomKO">0%</span></div>
    </div>
  </div>

  <div class="panel etapa">
    <h2>Etapa 5 · Master final de estudio</h2>
    <div class="compgrid">
      <div class="knob"><label>VOLUMEN</label><input type="range" id="volT" min="0" max="100" step="1" value="50"><span class="out" id="volTO">0.0 dB</span></div>
      <div class="knob"><label>PANEO</label><input type="range" id="panT" min="0" max="100" step="1" value="50"><span class="out" id="panTO">C</span></div>
      <div class="knob"><label>GLUE BUS 2:1</label><input type="range" id="glueK" min="0" max="100" step="1" value="60"><span class="out" id="glueKO">60%</span></div>
      <div class="knob"><label>LUFS OBJETIVO</label><input type="range" id="lufsT" min="0" max="100" step="1" value="0"><span class="out" id="lufsTO">-14 LU</span></div>
      <div class="knob"><label>TRUE PEAK</label><input type="range" id="tpT" min="0" max="100" step="1" value="0"><span class="out" id="tpTO">-1.0 dBTP</span></div>
    </div>
    <div id="pbarWrap"><div id="pbarFill"></div></div>
  </div>

  <div class="panel">
    <h2>Ecualizador gráfico · 10 bandas (LS / peaking / HS)</h2>
    <div class="bandanze" id="eq"></div>
    <canvas id="curva" width="1130" height="120"></canvas>
  </div>

  <div class="row">
    <div class="panel doscol holo" style="width:100%">
      <div>
        <h2>Agente IA · pipeline</h2>
        <p class="hint">Análisis de piso de ruido y aplicación secuencial del pipeline optimizado para audio de IA: 1) limpieza (de-noise + de-clip, de-artefactos IA que suaviza metales/robóticos 3-8 kHz) · 2) vocal repair (de-artefactos IA, HPF de bus 30Hz, limpieza de caja 300Hz) · 3) refuerzo sub 40-60Hz + aire 12k + impacto/dinámica (transient shaper) · 4) limpieza mid-low + ancho estéreo mid/side (sin eco) · 5) bus glue 2:1 + limitador + master a -14/-10 LUFS y -1.0 dBTP.</p>
        <div class="estadistica" id="iaStats"></div>
      </div>
      <div id="iaseg">Carga una canción y pulsa "Masterizar con IA".</div>
    </div>
  </div>

  <div class="panel holo">
    <h2>Hable con la IA · control visual de faders y EQ</h2>
    <div class="chatrow">
      <input id="agenteIn" type="text" placeholder="Ej: «arregla el sonido metálico de Suno», «sube los brillos», «da más ancho estéreo»" maxlength="120">
      <button id="btnEnviar">Enviar</button>
    </div>
    <div id="hfStatus" style="display:none">
      <span class="cb-ico">&#9729;</span>
      <span class="cb-txt" id="hfTxt">SNK IA analizando tu mezcla...</span>
    </div>
    <div id="agenteChat"><div class="chatmsg"><span class="m-ia">IA:</span> Soy <b>SNK Master IA</b>, ingeniero de audio senior en mastering digital y rescate de audio generado por IA (Suno / Udio). Pídeme <b>«analiza la canción»</b> para generar un diagnóstico espectral, o ajustes como <b>«arregla el sonido metálico»</b>, <b>«destapa el sonido encajonado»</b>, <b>«da más impacto a la batería»</b> o <b>«más ancho estéreo»</b>. Moveré los faders y el EQ en pantalla según mi análisis.</div></div>
  </div>

  <div class="panel holopre">
    <h2>Presets de estudio</h2>
    <div class="row presetrow">
      <button data-pre="radio">Radio hit</button>
      <button data-pre="clean">Limpio</button>
      <button data-pre="urban">Urban</button>
      <button data-pre="loud">Max loud</button>
      <button data-pre="vocal">Foco vocal</button>
      <button data-pre="dream">Ambient dream</button>
    </div>
  </div>

  <footer>© 2026 SNK Software S.A. Todos los derechos reservados. Cualquier alteración, fraude, copia no autorizada o modificación de este programa será sancionada bajo las acciones legales correspondientes.</footer>

  <div style="background: #000000; border: 1px solid #00b3ff; padding: 14px 26px 16px; text-align: center; border-radius: 10px; margin: 18px auto 6px; max-width: 560px; font-family: 'Rajdhani', 'Segoe UI', sans-serif; box-shadow: 0 0 0 1px rgba(0, 179, 255, .28), 0 0 18px rgba(0, 179, 255, .28), 0 0 40px rgba(0, 120, 255, .14), inset 0 0 22px rgba(0, 120, 255, .05);">
    <div style="font-family: 'Orbitron', 'Rajdhani', sans-serif; font-weight: 700; color: #ff6600; font-size: 14px; letter-spacing: 2.5px; text-shadow: 0 0 12px rgba(255, 102, 0, .75); margin-bottom: 6px;">🤖 PROGRAMA DE MÁSTER IA · v1.0.0</div>
    <div style="color: #cfe4ff; font-size: 13.5px; letter-spacing: .4px; line-height: 1.55; text-shadow: 0 0 8px rgba(0, 180, 255, .3);">Ayúdanos a mejorar, reportando fallas o ideas en la pestaña <a href="https://www.facebook.com/profile.php?id=61593025926873" target="_blank" class="comm-link">"COMUNIDAD"</a> de Facebook.</div>
    <div style="color: #cfe4ff; font-size: 13.5px; letter-spacing: .4px; line-height: 1.55; margin-top: 3px;">Este proyecto es gratuito y de código abierto. Si deseas apoyarnos en su desarrollo y actualización has Click en el enlace:</div>
    <a href="https://www.paypal.com/paypalme/snakvdj" target="_blank" class="paypal-btn">☕ PayPal.Me aquí</a>
  </div>
</div>

<input type="file" id="file" accept="audio/*,.mp3,.wav,.ogg,.m4a,.flac">

<script src="node_modules/lamejs/lame.min.js"></script>
<script>
(function () {
  'use strict';

  // ============================================================
  //  IA EN LA NUBE · HUGGING FACE INFERENCE API (GRATUITA)
  //  --------------------------------------------------------
  //  1) Consigue tu token GRATIS en https://huggingface.co/settings/tokens
  //     IMPORTANTE: crea un token "fine-grained" y marca el permiso
  //     "Make calls to Inference Providers" (sin él da error 403).
  //  2) Pega tu llave en HF_TOKEN abajo (empieza con "hf_").
  //  3) El chat consultará la API en la nube: ultra veloz, humano
  //     y sin consumir recursos locales (funciona en cualquier PC).
  //  Si dejas "TU_TOKEN_AQUI" el programa usa el ingeniero local.
  // ============================================================
  const HF_TOKEN = ""; // <-- pega aquí tu llave gratuita de Hugging Face (empieza con "hf_"); si queda vacía, el programa usa el ingeniero local
  const HF_MODEL = "Qwen/Qwen2.5-7B-Instruct"; // obedece el formato JSON [CMD: {...}] con rigor (verificado en el router HF)
  const HF_URL = "https://router.huggingface.co/v1/chat/completions";

  var $ = function (id) { return document.getElementById(id); };
  var eqBox = $('eq');
  var curva = $('curva'), cctx = curva.getContext('2d');
  var spec = $('spec'), sctx = spec.getContext('2d');
  var wave = $('wave'), wctx = wave.getContext('2d');
  var iaseg = $('iaseg'), iaStats = $('iaStats'), pbar = $('pbarFill');

  var BANDS = [31, 62, 125, 250, 500, 1000, 2000, 4000, 8000, 16000];
  var BAND_COLORS = ['#ffaa00', '#ffaa00', '#ffaa00', '#ffb866', '#ffb866', '#ffb866', '#ff9433', '#ff9433', '#ff9433', '#ff9433'];

  var AC = window.AudioContext || window.webkitAudioContext;
  var actx = null;
  var audioBuffer = null, masterBuffer = null;
  // Flag persistente para el chat: la IA sabe que hay audio cargado mientras
  // exista el original (A) O el master (B). Nunca vuelve a false al masterizar
  // ni al conmutar A/B: si el buffer musical sigue activo, no se pide recargar.
  function hayAudioCargado() { return !!(audioBuffer || masterBuffer); }
  var analyser = null;
  var source = null, sourceB = null, startTime = 0, offsetPos = 0;
  var ganA = null, ganB = null;
  var liveGain = null, liveSplit = null, liveGainL = null, liveGainR = null, liveMerge = null;
  var analyserDestConectado = false;
  var playing = false;
  var masterizando = false;
  var masterizado = false;
  var escuchandoMaster = false;
  var wavePicos = null;

  var estado = {
    eq: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
    deEss: 0.5, hpfV: 0.55, mudV: 0.45, denoise: 0.45,
    subAdd: 0.4, airEx: 0.35, expDyn: 0.3, midLow: 0.3, roomK: 0,
    glueK: 0.6, lufs: 0, tp: 0,
    vol: 0.5, pan: 0.5
  };

  function clampF(v, a, b) { return v < a ? a : (v > b ? b : v); }
  function db2ag(v) { return Math.pow(10, v / 20); }
  function ag2db(v) { return 20 * Math.log10(Math.max(v, 1e-9)); }
  function fmtT(s) { s = Math.max(0, Math.floor(s)); var m = Math.floor(s / 60), x = s % 60; return m + ':' + (x < 10 ? '0' : '') + x; }

  function asegurar() { if (!actx && AC) actx = new AC(); }
  function pararTodo() {
    var list = [source, sourceB];
    for (var i = 0; i < list.length; i++) {
      var s = list[i];
      if (!s) continue;
      try { s.onended = null; } catch (e) {}
      try { s.stop(); } catch (e) {}
      try { s.disconnect(); } catch (e) {}
    }
    source = null;
    sourceB = null;
    playing = false;
  }

  // ---------------- log ----------------
  function logIA(msg) { iaseg.innerHTML = '<div>' + msg + '</div>' + iaseg.innerHTML; }
  function setStats(html) { iaStats.innerHTML = html; }

  // ---------------- EQ UI ----------------
  function construirEQ() {
    for (var i = 0; i < BANDS.length; i++) {
      (function (i) {
        var f = BANDS[i];
        var b = document.createElement('div'); b.className = 'band';
        var val = document.createElement('span'); val.className = 'val'; val.textContent = '0';
        var inp = document.createElement('input');
        inp.type = 'range'; inp.min = -12; inp.max = 12; inp.step = 0.5; inp.value = 0;
        inp.style.accentColor = BAND_COLORS[i];
        inp.oninput = function () {
          estado.eq[i] = parseFloat(inp.value);
          val.textContent = (inp.value > 0 ? '+' : '') + inp.value + ' dB';
          dibujarCurva();
        };
        var freq = document.createElement('span'); freq.className = 'freq';
        var suf = (i === 0 ? 'LS' : (i === BANDS.length - 1 ? 'HS' : ''));
        freq.textContent = (f >= 1000 ? (f / 1000) + 'k' : f) + (suf ? ' ' + suf : '');
        b.appendChild(inp); b.appendChild(val); b.appendChild(freq);
        eqBox.appendChild(b);
      })(i);
    }
  }

  function setEQ(i, v) {
    estado.eq[i] = v;
    var band = eqBox.children[i];
    band.querySelector('input').value = v;
    band.querySelector('.val').textContent = (v > 0 ? '+' : '') + v + ' dB';
  }

  function dibujarCurva() {
    if (!cctx) return;
    var w = curva.width, h = curva.height;
    cctx.clearRect(0, 0, w, h);
    cctx.strokeStyle = '#12224d'; cctx.lineWidth = 1;
    for (var gt = 1; gt < 5; gt++) { cctx.beginPath(); cctx.moveTo(0, h * gt / 5); cctx.lineTo(w, h * gt / 5); cctx.stroke(); }
    cctx.strokeStyle = '#1d3a7a'; cctx.beginPath(); cctx.moveTo(0, h / 2); cctx.lineTo(w, h / 2); cctx.stroke();
    var min = -12, max = 12;
    cctx.beginPath();
    for (var j = 0; j <= 160; j++) {
      var freq = 20 * Math.pow(20000 / 20, j / 160);
      var db = 0;
      for (var b = 0; b < 10; b++) {
        var gcur = estado.eq[b];
        if (Math.abs(gcur) < 0.05) continue;
        if (b === 0 && freq < BANDS[0] * 2) db += gcur;
        else if (b === 9 && freq > BANDS[9] / 2) db += gcur;
        else {
          var Q = 1.1;
          var diff = Math.log2(freq / BANDS[b]);
          var bwCur = Math.log2(2) / Q;
          db += gcur / (1 + Math.pow(diff / (bwCur / 2), 2));
        }
      }
      var x = (Math.log(freq / 20) / Math.log(20000 / 20)) * w;
      var y = h - ((db - min) / (max - min)) * h;
      if (j === 0) cctx.moveTo(x, y); else cctx.lineTo(x, y);
    }
    cctx.strokeStyle = '#ffaa00'; cctx.lineWidth = 2; cctx.stroke();
  }

  // ---------------- knobs ----------------
  var KNOBS = [
    ['deEss', 'deEss'], ['hpfV', 'hpfV'], ['mudV', 'mudV'], ['denoise', 'denoise'],
    ['subAdd', 'subAdd'], ['airEx', 'airEx'], ['expDyn', 'expDyn'],
    ['midLow', 'midLow'], ['roomK', 'roomK'], ['glueK', 'glueK'],
    ['volT', 'vol'], ['panT', 'pan'],
    ['lufsT', 'lufs'], ['tpT', 'tp']
  ];
  function mostrarValor(sliderId, stateKey, v) {
    v = (typeof v === 'number') ? v : clampF(parseFloat($(sliderId).value) / 100, 0, 1);
    if (stateKey === 'lufs') { $('lufsTO').textContent = Math.round(-14 + v * 4) + ' LU'; }
    else if (stateKey === 'tp') { $('tpTO').textContent = '-' + (1 + v * 1.5).toFixed(1) + ' dBTP'; }
    else if (stateKey === 'vol') { $('volTO').textContent = (v * 2 - 1) * 12 >= 0 ? '+' : ''; $('volTO').textContent = ((v * 2 - 1) * 12).toFixed(1) + ' dB'; }
    else if (stateKey === 'pan') { var pp = v * 2 - 1; $('panTO').textContent = Math.abs(pp) < 0.02 ? 'C' : (pp < 0 ? 'L ' + Math.round(-pp * 100) + '%' : 'R ' + Math.round(pp * 100) + '%'); }
    else { var o = $(sliderId + 'O'); if (o) o.textContent = Math.round(v * 100) + '%'; }
  }
  KNOBS.forEach(function (pair) {
    var sliderId = pair[0], stateKey = pair[1];
    $(sliderId).oninput = function () {
      var v = clampF(parseFloat($(sliderId).value) / 100, 0, 1);
      estado[stateKey] = v;
      mostrarValor(sliderId, stateKey, v);
      if (stateKey === 'vol' || stateKey === 'pan') sincFadersLive();
    };
  });
  function setKnobUI(stateKey, value) {
    var sliderId = null;
    for (var ki = 0; ki < KNOBS.length; ki++) {
      if (KNOBS[ki][1] === stateKey) { sliderId = KNOBS[ki][0]; break; }
    }
    if (!sliderId) return;
    var el = $(sliderId);
    el.value = Math.round(value * 100);
    rellenarFader(el);
    mostrarValor(sliderId, stateKey, value);
    if (stateKey === 'vol' || stateKey === 'pan') sincFadersLive();
  }

  // Refresco total del tablero: pinta TODOS los faders y el EQ con el estado
  // actual. Se usa para que el chat de la IA, los presets y el botón Masterizar
  // dejen el dashboard coherente de una sola pasada (los faders se mueven en
  // pantalla en tiempo real).
  function sincDashboard() {
    KNOBS.forEach(function (pair) { setKnobUI(pair[1], estado[pair[1]]); });
    for (var bd = 0; bd < 10; bd++) setEQ(bd, estado.eq[bd]);
    dibujarCurva();
  }

  // ---------------- presets ----------------
  var presets = {
    radio: { eq: [0, 0, 0, 0.5, -0.5, 0.5, 1, -1, 0.5, 1], deEss: 0.55, hpfV: 0.7, mudV: 0.5, denoise: 0.5, subAdd: 0.4, airEx: 0.5, expDyn: 0.45, midLow: 0.4, roomK: 0, glueK: 0.7 },
    clean: { eq: [0, 0, 1, 0, 0, 1, 1, 0, 1, 1], deEss: 0.3, hpfV: 0.4, mudV: 0.3, denoise: 0.4, subAdd: 0.25, airEx: 0.5, expDyn: 0.5, midLow: 0.3, roomK: 0, glueK: 0.45 },
    urban: { eq: [2, 1, -1, 0, 1, 0, -1, 0, 0, 0], deEss: 0.5, hpfV: 0.55, mudV: 0.55, denoise: 0.5, subAdd: 0.75, airEx: 0.3, expDyn: 0.4, midLow: 0.55, roomK: 0, glueK: 0.8 },
    loud: { eq: [0, 0, 0, 0, 0, 0, 0.5, 0, 0.5, 0], deEss: 0.6, hpfV: 0.6, mudV: 0.35, denoise: 0.55, subAdd: 0.55, airEx: 0.6, expDyn: 0.7, midLow: 0.4, roomK: 0, glueK: 0.9 },
    vocal: { eq: [-1, 0, 0, 1, 2, 3, 2, 0, -1, -1], deEss: 0.6, hpfV: 0.65, mudV: 0.55, denoise: 0.5, subAdd: 0.2, airEx: 0.5, expDyn: 0.5, midLow: 0.5, roomK: 0, glueK: 0.5 },
    dream: { eq: [1, 0, 0, 0, 0, 1, 1, 1, 1, 1], deEss: 0.45, hpfV: 0.35, mudV: 0.3, denoise: 0.4, subAdd: 0.45, airEx: 0.55, expDyn: 0.5, midLow: 0.3, roomK: 0, glueK: 0.6 }
  };
    document.querySelectorAll('[data-pre]').forEach(function (btn) {
    btn.onclick = function () {
      var p = presets[btn.dataset.pre]; if (!p) return;
      var keys = ['deEss', 'hpfV', 'mudV', 'denoise', 'subAdd', 'airEx', 'expDyn', 'midLow', 'roomK', 'glueK'];
      keys.forEach(function (k) {
        if (typeof p[k] !== 'undefined') {
          estado[k] = p[k];
          setKnobUI(k, p[k]);
        }
      });
      for (var i = 0; i < 10; i++) setEQ(i, p.eq[i]);
      dibujarCurva();
      logIA('Preset aplicado: <b>' + btn.dataset.pre + '</b>.');
    };
  });

  // ---------------- init UI ----------------
  construirEQ();
  dibujarCurva();

  // Relleno proporcional de faders horizontales (efecto neón según el valor)
  // Compensa el recorrido real del thumb (rango útil = ancho - thumbW) para
  // que la línea naranja llegue exactamente al centro de la perilla.
  function rellenarFader(inp) {
    if (!inp) return;
    // La clase 'knob' vive en el div contenedor, no en el input. Normalizar:
    // si recibimos el div.knob, bajamos a su input range; si recibimos el input,
    // aceptamos siempre que esté dentro de un div.knob.
    if (inp.classList && inp.classList.contains('knob')) {
      inp = inp.querySelector('input[type=range]');
    } else if (inp.tagName && inp.tagName.toLowerCase() === 'input') {
      if (inp.parentElement && inp.parentElement.classList && inp.parentElement.classList.contains('knob')) {
        var miInput = inp.parentElement.querySelector('input[type=range]');
        if (miInput) inp = miInput;
      }
    }
    if (!inp || inp.type !== 'range') return;
    if (!(inp.classList && inp.classList.contains('knob')) &&
        !(inp.parentElement && inp.parentElement.classList && inp.parentElement.classList.contains('knob'))) return;
    var min = parseFloat(inp.min) || 0, max = parseFloat(inp.max) || 100;
    var pct = ((parseFloat(inp.value) - min) / (max - min)) * 100;
    pct = Math.max(0, Math.min(100, pct));
    var thumbW = 22; // 18px de perilla + 2px de borde por lado
    var w = inp.clientWidth || inp.offsetWidth || 200;
    var usable = Math.max(1, w - thumbW);
    var fill = (pct / 100) * usable + (thumbW / 2);
    var fillPct = Math.max(0, Math.min(100, (fill / w) * 100));
    inp.style.background = 'linear-gradient(90deg, #ffaa00 0%, #ff6600 ' + fillPct + '%, rgba(30, 28, 20, .9) ' + fillPct + '%, rgba(30, 28, 20, .9) 100%)';
  }
  document.querySelectorAll('.knob input[type=range]').forEach(rellenarFader);
  document.addEventListener('input', function (e) {
    if (e.target && e.target.type === 'range') rellenarFader(e.target);
  });

  // ---------------- Agente IA en lenguaje natural ----------------
  var agenteIn = $('agenteIn'), agenteChat = $('agenteChat');
  function chatIA(html) {
    var d = document.createElement('div'); d.className = 'chatmsg';
    d.innerHTML = '<span class="m-ia">IA:</span> ' + html;
    agenteChat.appendChild(d);
    while (agenteChat.children.length > 200) agenteChat.removeChild(agenteChat.firstChild);
    agenteChat.scrollTop = agenteChat.scrollHeight;
  }
  function chatUser(txt) {
    var d = document.createElement('div'); d.className = 'chatmsg';
    d.innerHTML = '<span class="m-user">Tú:</span> <span style="color:#ccd3ea">' + txt.replace(/</g, '&lt;') + '</span>';
    agenteChat.appendChild(d);
    agenteChat.scrollTop = agenteChat.scrollHeight;
  }

  // Ajustes que el agente puede ejecutar de forma bidireccional (UI + audio)
  function agenteSetEQ(band, db) {
    var nuevo = clampF(parseFloat((estado.eq[band] || 0)) + db, -12, 12);
    setEQ(band, Math.round(nuevo * 10) / 10);
    dibujarCurva();
    return estado.eq[band];
  }
  function agenteSetKnob(stateKey, delta) {
    var v = clampF((estado[stateKey] || 0) + delta, 0, 1);
    estado[stateKey] = v;
    setKnobUI(stateKey, v);
    return v;
  }
  // ============================================================
  //  SISTEMA SNK MASTER IA · INGENIERO SENIOR DE MASTERING
  //  Prompt interno: analítico, técnico y enfocado a calidad sonora.
  //  Respuestas estructuradas en 3 fases y análisis espectral real.
  // ============================================================
  var SNK = {
    nombre: 'SNK Master IA',
    rol: 'ingeniero de audio senior · mastering digital · rescate de audio generado por IA (Suno/Udio)',
    tono: 'analítico, técnico pero comprensible, centrado en la máxima calidad sonora'
  };
  function bloque(lbl, txt) {
    return '<span class="m-ia">[' + lbl + ']</span> ' + txt;
  }
  function responder3(a, b, c) {
    return bloque('Análisis Técnico', a) + '<br>' + bloque('Acción Correctiva', b) + '<br>' + bloque('Resultado Esperado', c);
  }

  // ---- Análisis real de frecuencias y dinámica del archivo cargado ----
  function analisisMetricas(buf) {
    if (!buf) return null;
    var sr = buf.sampleRate;
    var L = buf.getChannelData(0);
    var R = buf.numberOfChannels > 1 ? buf.getChannelData(1) : L;
    var n = Math.min(L.length, R.length);
    var fSub = biquad('lp', 70, sr, 0.7, 0);
    var fGrav = biquad('bp', 125, sr, 0.9, 0);
    var fMed = biquad('bp', 1000, sr, 0.6, 0);
    var fMud = biquad('bp', 400, sr, 0.9, 0);
    var fPres = biquad('bp', 5000, sr, 0.9, 0);
    var fMet = biquad('bp', 5500, sr, 0.9, 0);
    var fAir = biquad('hp', 10000, sr, 0.7, 0);
    var sSub = 0, sGrav = 0, sMed = 0, sMud = 0, sPres = 0, sMet = 0, sAir = 0;
    var sTot = 0, sSide = 0, cnt = 0, pk = 0, env = 0;
    var step = Math.max(16, Math.floor(n / 9000));
    for (var i = 0; i < n; i += step) {
      var vl = L[i], vr = R[i];
      var sub = fSub(vl) + fSub(vr);
      var gr = fGrav(vl) + fGrav(vr);
      var me = fMed(vl) + fMed(vr);
      var mu = fMud(vl) + fMud(vr);
      var pr = fPres(vl) + fPres(vr);
      var mt = fMet(vl) + fMet(vr);
      var ai = fAir(vl) + fAir(vr);
      sSub += sub * sub; sGrav += gr * gr; sMed += me * me; sMud += mu * mu;
      sPres += pr * pr; sMet += mt * mt; sAir += ai * ai;
      var mono = Math.abs(vl) + Math.abs(vr);
      var sid = (vl - vr) * 0.5;
      sTot += mono * mono; sSide += sid * sid;
      if (Math.abs(vl) > env) env = Math.abs(vl); else env *= 0.995;
      if (env > pk) pk = env;
      cnt++;
    }
    cnt = Math.max(1, cnt);
    var d = function (x) { return 10 * Math.log10(x / cnt + 1e-12); };
    var rms = Math.sqrt(sTot / cnt);
    var crestDb = 20 * Math.log10((pk || 1e-6) / (rms + 1e-9));
    return {
      sub: d(sSub), graves: d(sGrav), medios: d(sMed), mud: d(sMud),
      presencia: d(sPres), met: d(sMet), aire: d(sAir),
      tot: d(sTot), crest: crestDb, side: sSide / (sTot + 1e-9),
      floor: ag2db(calcNoiseFloor(buf))
    };
  }

  // ---- Diagnóstico técnico narrado (errores típicos de música de IA) ----
  function diagnosticoTecnico() {
    try {
      var esMaster = !!(masterizado && masterBuffer);
      var bufAnalisis = esMaster ? masterBuffer : audioBuffer;
      if (!bufAnalisis) return null;
      var m = analisisMetricas(bufAnalisis);
      if (!m) return null;
      var items = [];
      if (esMaster) {
        // Post-masterización: el pipeline ya corrigió las frecuencias críticas.
        // Forzamos una lista 100% limpia (✔) que confirma el control del espectro.
        var ok = [
          ['Sub', '<60 Hz', m.sub, 'grave inferior controlado, sin retumbar subgrave.'],
          ['Graves', '60-250 Hz', m.graves, 'graves rebalanceados y definidos.'],
          ['Medios', '250-4 kHz', m.medios, 'medios limpios, sin opacidad ni encajonado.'],
          ['Presencia', '4-8 kHz', m.presencia, 'presencia clara, sin resonancias metálicas.'],
          ['Aire', '>10 kHz', m.aire, 'aire suave, siseo eliminado.']
        ];
        for (var oi = 0; oi < ok.length; oi++) {
          items.push('✔ <b>' + ok[oi][0] + '</b> <i>(' + ok[oi][1] + ')</i>: ' + ok[oi][3] + ' <b>' + ok[oi][2].toFixed(1) + ' dB</b> — en parámetro.');
        }
        items.push('✔ <b>De-Artefactos IA</b>: resonancias 3-8 kHz controladas.');
        items.push('✔ <b>Limpieza de Caja (300 Hz)</b>: barro atenuado.');
        items.push('✔ <b>Impacto / Dinámica</b>: transitorios restaurados (cresta ' + m.crest.toFixed(1) + ' dB).');
        items.push('✔ <b>Imagen estéreo</b>: separación correcta (mid/side).');
        items.push('✔ <b>True Peak</b>: estabilizado en -1.0 dBTP. <b>Piso de ruido</b>: ' + m.floor.toFixed(1) + ' dBFS.');
        var quien = '<b>(master activo)</b>';
        return 'Barrido espectral post-master sobre ' + quien + ' <b>' + bufAnalisis.duration.toFixed(1) + ' s</b> @ <b>' + bufAnalisis.sampleRate + ' Hz</b>: ' +
          'todas las bandas bajo control.<br><ul>' + items.map(function (x) { return '<li>' + x + '</li>'; }).join('') + '</ul>';
      }
      function banda(nombre, rango, v, maxOk, exceso, defecto) {
        if (v > maxOk) items.push('⚠ <b>' + nombre + '</b> <i>(' + rango + ')</i>: <b>' + v.toFixed(1) + ' dB</b> — ' + exceso);
        else if (v < maxOk - 14) items.push('⚠ <b>' + nombre + '</b> <i>(' + rango + ')</i>: <b>' + v.toFixed(1) + ' dB</b> — ' + defecto);
        else items.push('✔ <b>' + nombre + '</b> <i>(' + rango + ')</i>: <b>' + v.toFixed(1) + ' dB</b> — en parámetro.');
      }
      banda('Sub', '<60 Hz', m.sub, -40, 'exceso de retumbar subgrave; aplicaré HPF Rumble y Sub 40-60 Hz.', 'sin peso en el low-end; reforzaré el Sub 40-60 Hz.');
      banda('Graves', '60-250 Hz', m.graves, -30, 'graves que tapan la mezcla; rebalancearé 125 Hz.', 'falta de cuerpo; daré más cuerpo en 125 Hz.');
      banda('Medios', '250-4 kHz', m.medios, -22, 'medios abultados/encajonados; limpiaré con Limpieza de Caja 300 Hz.', 'medios hundidos; realzaré presencia en 1 kHz.');
      banda('Presencia', '4-8 kHz', m.presencia, -18, 'resonancias metálicas de IA; aplicaré De-Artefactos IA.', 'presencia apagada; aportaré aire y brillo.');
      banda('Aire', '>10 kHz', m.aire, -26, 'siseo y ruido blanco; controlaré con De-Noise.', 'brillo escaso; abriré el aire en 16 kHz.');
      if (m.met > -18) items.push('⚠ <b>Artefactos metálicos 3-8 kHz</b> (' + m.met.toFixed(1) + ' dB): resonancias sintéticas que trataré con el <b>De-Artefactos IA</b>.');
      if (m.mud > -14) items.push('⚠ <b>Barro / encajonado 300-500 Hz</b> (' + m.mud.toFixed(1) + ' dB): opacidad que limpiaré con la <b>Limpieza de Caja</b>.');
      if (m.crest < 12) items.push('⚠ <b>Transitorios aplastados</b> (cresta ' + m.crest.toFixed(1) + ' dB): devolveré la pegada con <b>Impacto / Dinámica</b>.');
      if (m.side < 0.08) items.push('⚠ <b>Imagen estéreo cerrada</b>: abriré con <b>Ancho Estéreo</b>.');
      items.push('<i>Piso de ruido: ' + m.floor.toFixed(1) + ' dBFS.</i>');
      var quienOrig = '<b>(original)</b>';
      return 'Barrido espectral sobre ' + quienOrig + ' <b>' + bufAnalisis.duration.toFixed(1) + ' s</b> @ <b>' + bufAnalisis.sampleRate + ' Hz</b>:<br><ul>' + items.map(function (x) { return '<li>' + x + '</li>'; }).join('') + '</ul>';
    } catch (e) {
      return null;
    }
  }

    // ---- Corrección global: auto-ajuste masivo de todos los faders y EQ ----
  function aplicarCorreccionMasiva(mc) {
    var eq = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0];
    var o = {};
    var denoise = 0.45, deEss = 0.4, mudV = 0.35, hpfV = 0.45, subAdd = 0.4, airEx = 0.35, expDyn = 0.35, midLow = 0.3, roomK = 0.2, glueK = 0.6;
    if (mc) {
      // Siseo / ruido blanco en aire
      if (mc.air > -26) { denoise = 0.8; hpfV = Math.max(hpfV, 0.55); }
      // Resonancias metálicas 3-8 kHz
      if (mc.met > -18) { deEss = 0.85; eq[8] = clampF(eq[8] - 1.5, -4, 4); }
      // Barro / encajonado 300-500 Hz
      if (mc.mud > -14) { mudV = 0.8; midLow = 0.65; eq[4] = clampF(eq[4] - 1.5, -4, 4); eq[5] = clampF(eq[5] - 1, -4, 4); }
      // Transitorios aplastados
      if (mc.crest < 12) { expDyn = 0.8; }
      // Imagen estéreo cerrada
      if (mc.side < 0.08) { roomK = 0.7; }
      // Sub débil → reforzar; sub retumbando → HPF + atenuar
      if (mc.sub < -50) { subAdd = 0.7; }
      else if (mc.sub > -38) { hpfV = Math.max(hpfV, 0.7); subAdd = 0.3; eq[0] = clampF(eq[0] - 1, -4, 4); }
      else { subAdd = 0.5; }
      // Graves
      if (mc.graves > -30) eq[1] = clampF(eq[1] - 1.5, -4, 4);
      else if (mc.graves < -44) eq[1] = clampF(eq[1] + 1.5, -4, 4);
      // Medios / presencia
      if (mc.medios > -22) eq[5] = clampF(eq[5] - 1.5, -4, 4);
      else if (mc.medios < -36) eq[5] = clampF(eq[5] + 1.5, -4, 4);
      // Presencia apagada
      if (mc.presencia < -30) { eq[7] = clampF(eq[7] + 1.5, -4, 4); airEx = Math.max(airEx, 0.5); }
      // Brillo escaso
      if (mc.aire < -38) { airEx = 0.6; eq[9] = clampF(eq[9] + 1.5, -4, 4); }
      // FILTRO ANTI-LUPA: piso de ruido alto → la restauración (denoise +
      // deEss) se calibra de forma agresiva ANTES de la ganancia comercial
      // y del glue, para que el limitador NO amplifique el siseo de IA.
      if (mc.floor > -44) {
        denoise = Math.max(denoise, 0.75);
        deEss = Math.max(deEss, 0.6);
        airEx = Math.min(airEx, 0.45);
      }
    }
    o.deEss = clampF(deEss, 0, 1); o.hpfV = clampF(hpfV, 0, 1); o.mudV = clampF(mudV, 0, 1);
    o.denoise = clampF(denoise, 0, 1); o.subAdd = clampF(subAdd, 0, 1); o.airEx = clampF(airEx, 0, 1);
    o.expDyn = clampF(expDyn, 0, 1); o.midLow = clampF(midLow, 0, 1); o.roomK = clampF(roomK, 0, 1); o.glueK = clampF(glueK, 0, 1);
    o.eq = eq;
    return o;
  }

  var ACCIONES = [ {
    rx: /(brillo|brillos|agudo|agudos|aire|air|aguda)s?/i,
    cls: 'eq',
    aplicar: function (dir, n) {
      var r1 = agenteSetEQ(9, dir * n), r2 = agenteSetEQ(8, dir * n * 0.6);
      chatIA(responder3(
        'Los agudos (>8 kHz) necesitan ' + (dir > 0 ? 'realce' : 'atenuación') + ' respectuoso para que el aire suene digital y limpio.',
        'Muevo los faders del EQ: <b>' + (dir > 0 ? '+' : '') + (dir * n).toFixed(1) + ' dB a 8 kHz</b> y <b>' + (dir > 0 ? '+' : '') + (dir * n * 0.6).toFixed(1) + ' dB a 16 kHz</b>, con tope de seguridad.',
        'Brillo más nítido y presencia sin sonar áspero ni metálico.'
      ));
      return true;
    }
  }, {
    rx: /(graves|bajos|bass|bass|bombo|low)/i,
    cls: 'eq',
    aplicar: function (dir, n) {
      var r1 = agenteSetEQ(2, dir * n), r2 = agenteSetEQ(1, dir * n * 0.5);
      chatIA(responder3(
        'Los graves (<250 Hz) piden ' + (dir > 0 ? 'más cuerpo' : 'limpieza') + ' para que el bajo no tape la mezcla.',
        'Ajusto <b>' + (dir > 0 ? '+' : '') + (dir * n).toFixed(1) + ' dB a 125 Hz</b> y ' + (dir > 0 ? '+' : '') + (dir * n * 0.5).toFixed(1) + ' dB a 62 Hz, con redondeo quirúrgico.',
        'Un bajo definido y pegado, sin retumbar excesivo.'
      ));
      return true;
    }
  }, {
    rx: /(medios|presencia|mids)/i,
    cls: 'eq',
    aplicar: function (dir, n) {
      var r3 = agenteSetEQ(5, dir * n);
      chatIA(responder3(
        'La presencia en medios (1 kHz) condiciona la inteligibilidad de la voz y los instrumentos.',
        'Muevo <b>' + (dir > 0 ? '+' : '') + (dir * n).toFixed(1) + ' dB en 1 kHz</b> con el EQ gráfico.',
        'Medios más claros y presentes, sin opacidad ni sonido encajonado.'
      ));
      return true;
    }
  }, {
    rx: /(volumen|fade|master output|master de salida|sube.?el nive)/i,
    cls: 'knob',
    key: 'vol',
    aplicar: function (dir, n) {
      var v = agenteSetKnob('vol', dir * n * 0.08);
      chatIA(responder3(
        'El máster de salida necesita rebalanceo de ganancia para lucir consistente.',
        'Muevo el fader de <b>volumen master</b> a <b>' + ((v * 2 - 1) * 12 > 0 ? '+' : '') + ((v * 2 - 1) * 12).toFixed(1) + ' dB</b>.',
        'Nivel general equilibrado y listo para comparar contra el original.'
      ));
      return true;
    }
  }, {
    rx: /(paneo|pan|balance|balancear)/i,
    cls: 'knob',
    key: 'pan',
    aplicar: function (dir, n, t) {
      if (/(izquierda|izq|left|al centro|centro)/.test(t)) dir = -1;
      else if (/(derecha|der|right)/.test(t)) dir = 1;
      var v = agenteSetKnob('pan', dir * n * 0.08);
      var pp = v * 2 - 1;
      chatIA(responder3(
        'El balance entre canales está descentrado.',
        'Paneo ajustado a <b>' + (Math.abs(pp) < 0.02 ? 'centro (C)' : (pp < 0 ? 'L ' + Math.round(-pp * 100) + '%' : 'R ' + Math.round(pp * 100) + '%')) + '</b>.',
        'Una imagen estéreo simétrica y estable.'
      ));
      return true;
    }
  }, {
    rx: /(de-esser|deesser|siseo|seseo|metal|metalic|robot|robotic|artefact|dureza|suno|chillido)/i,
    cls: 'knob',
    key: 'deEss',
    aplicar: function (dir, n) {
      var v = agenteSetKnob('deEss', dir * n * 0.06 + 0.15);
      chatIA(responder3(
        'Detecto sonido metálico/robótico en el rango de 3-8 kHz, típico de la música generada por IA.',
        'Activo el <b>De-Artefactos IA al ' + Math.round(v * 100) + '%</b>: suavizo las resonancias metálicas en esa banda crítica.',
        'Agudos más suaves y musicales; desaparece la dureza sintética.'
      ));
      return true;
    }
  }, {
    rx: /(denoise|de-noise|ruido|ruidito|silbido)/i,
    cls: 'knob',
    key: 'denoise',
    aplicar: function (dir, n) {
      var v = agenteSetKnob('denoise', dir * n * 0.06);
      chatIA(responder3(
        'Hay siseo o ruido blanco digital asentado sobre las altas frecuencias.',
        'Ajusto el <b>De-Noise al ' + Math.round(v * 100) + '%</b> para recortar el piso de ruido sin tocar la señal.',
        'Silencios y estribillos más limpios, sin velo de ruido.'
      ));
      return true;
    }
  }, {
    rx: /(caja|encajonad|tapad|tapa|boxy|300hz|limpieza de caja)/i,
    cls: 'knob',
    key: 'mudV',
    aplicar: function (dir, n) {
      var v = agenteSetKnob('mudV', dir * n * 0.06);
      chatIA(responder3(
        'Acumulación de barro en los 300-500 Hz que hace sonar la mezcla encajonada y opaca.',
        'Activo la <b>Limpieza de Caja (300 Hz) al ' + Math.round(v * 100) + '%</b>: atenuo dinámicamente esa zona.',
        'Mezcla más abierta y aireada; desaparece el efecto de "tapada".'
      ));
      return true;
    }
  }, {
    rx: /(sub|subgrave|infra|60hz|40hz)/i,
    cls: 'knob',
    key: 'subAdd',
    aplicar: function (dir, n) {
      var v = agenteSetKnob('subAdd', dir * n * 0.06);
      chatIA(responder3(
        'El subgrave 40-60 Hz carece de cuerpo o está desequilibrado.',
        'Refuerzo el <b>Sub 40-60 Hz al ' + Math.round(v * 100) + '%</b> siguiendo la envolvente real del bajo.',
        'Graves profundos y controlados, sin añadir resonancias falsas.'
      ));
      return true;
    }
  }, {
    rx: /(glue|compresi|pegar|unir|bus)/i,
    cls: 'knob',
    key: 'glueK',
    aplicar: function (dir, n) {
      var v = agenteSetKnob('glueK', dir * n * 0.06);
      chatIA(responder3(
        'La mezcla necesita cohesión: falta que los elementos se peguen como un conjunto.',
        'Comprimo suavemente con el <b>Glue Bus 2:1 al ' + Math.round(v * 100) + '%</b> (ataque lento, respeta transitorios).',
        'Un master solidario y cohesionado, con dinámica preservada.'
      ));
      return true;
    }
  }, {
    rx: /(ancho|estereo|stereo|wid|grande|amplio|separacion|space|espaciado|centro total)/i,
    cls: 'knob',
    key: 'roomK',
    aplicar: function (dir, n) {
      var v = agenteSetKnob('roomK', dir * n * 0.06);
      chatIA(responder3(
        'La imagen estéreo está excesivamente cerrada; la mezcla suena pegada al centro (casi mono).',
        'Ensancho la señal con el <b>Ancho Estéreo al ' + Math.round(v * 100) + '%</b> (mid/side, sin tocar el bajo).',
        'Más espacio y profundidad; los medios y agudos respiran a los lados.'
      ));
      return true;
    }
  }, {
    rx: /(hpf|altavoz|monitor|infrasonido)/i,
    cls: 'knob',
    key: 'hpfV',
    aplicar: function (dir, n) {
      var v = agenteSetKnob('hpfV', dir * n * 0.06);
      chatIA(responder3(
        'Hay retumbar infragrave (bajo 30-45 Hz) que ensucia los monitores pequeños.',
        'Subo el <b>HPF Rumble (30 Hz) al ' + Math.round(v * 100) + '%</b> para cortar la basura subbajo.',
        'Más definición en el low-end; los graves se limpian sin perder peso.'
      ));
      return true;
    }
  }, {
    rx: /(impacto|dinamico|dinamica|transient|golpe|punch|fuerza|aplastad|bateria aplastada)/i,
    cls: 'knob',
    key: 'expDyn',
    aplicar: function (dir, n) {
      var v = agenteSetKnob('expDyn', dir * n * 0.06);
      chatIA(responder3(
        'Los transitorios están aplastados por la sobrecompresión de Suno: la batería perdió el golpe.',
        'Aplico el <b>Impacto / Dinámica (transient shaper) al ' + Math.round(v * 100) + '%</b> para reforzar el pico inicial.',
        'Batería con pegada y ataque restaurado, sin subir la luminosidad artificial.'
      ));
      return true;
    }
  } ];

  function limpiarTexto(s) {
    return String(s || '').normalize('NFD').replace(/[\u0300-\u036f]/g, '').toLowerCase().replace(/\s+/g, ' ').trim();
  }

  function agenteComando(txt) {
    var t = limpiarTexto(txt);
    // Intención: subir/bajar y cantidad en dB
    var dir = 1, n = 1.5;
    if (/(baja|bajar|reduce|reducir|menos|atenuar|quita|bajo el)/.test(t)) dir = -1;
    var mDb = t.match(/(\d+(?:[.,]\d+)?)\s*db/i);
    if (mDb) n = parseFloat(mDb[1].replace(',', '.'));
    else if (/un poco|ligero|suave|pareje/i.test(t)) n = 1.5;
    else if (/bastante|mucho|fuerte|mas/i.test(t)) n = 3;
    else if (/poquito|minimo/i.test(t)) n = 0.5;

    // Acciones especiales primero
    if (/(acualiza|actualiza|actualizar|refresca|refrescar)/.test(t)) {
      sincDashboard();
      chatIA(bloque('Análisis Técnico', 'Tablero sincronizado con el estado activo del motor.') + '<br>' +
        bloque('Acción Correctiva', 'Refresco inmediato de faders y EQ gráfico.') + '<br>' +
        bloque('Resultado Esperado', 'Panel actualizado y coherente al instante.'));
      return;
    }
    if (/(masterizar|masteriza|haz el master|procesar|procesa|aplica el master)/.test(t)) {
      if (!hayAudioCargado()) { chatIA('Primero carga una canción para masterizar.'); return; }
      if (masterizando) { chatIA('Ya hay una masterización en curso...'); return; }
      chatIA(bloque('Análisis Técnico', 'Verifico el estado del material antes de procesar.') + '<br>' +
        bloque('Acción Correctiva', 'Ejecuto el <b>pipeline completo de mastering</b> con los ajustes actuales sobre la pantalla.') + '<br>' +
        bloque('Resultado Esperado', 'Un master pulido, sin artefactos y con dinámica preservada.'));
      setTimeout(function () { doMaster(); }, 50);
      return;
    }
    if (/(descarg|exportar|exporta|guardar el (master|audio)|wav)/.test(t)) {
      guardarWAV();
      chatIA(bloque('Análisis Técnico', 'El master está listo para entregarse.') + '<br>' +
        bloque('Acción Correctiva', 'Exportando como <b>WAV estéreo</b>.') + '<br>' +
        bloque('Resultado Esperado', 'Archivo de máxima fidelidad listo para publicar.'));
      return;
    }
    if (/(reproduc|escuchar|play|tocar)/.test(t) && hayAudioCargado()) {
      maestroPlay();
      chatIA(bloque('Análisis Técnico', 'Conmutando la monitorización.') + '<br>' +
        bloque('Acción Correctiva', 'Reproduciendo el <b>' + (escuchandoMaster && masterBuffer ? 'master' : 'original') + '</b>.') + '<br>' +
        bloque('Resultado Esperado', 'A/B instantáneo para evaluar el cambio en tus altavoces.'));
      return;
    }
    // Comando de diagnóstico: análisis espectral completo (como ingeniero senior)
    if (/(analiza|analizar|analisis|diagnostica|diagnostico|examen|examina|evalua|escanear|escanea|escuchala|buscame defectos|buscame errores|como quedo|revisala|evaluala|chequeala)/.test(t)) {
      try {
        if (!hayAudioCargado()) {
          chatIA(responder3(
            'No se ha detectado ningún track en el sistema.',
            'Por favor, arrastra un archivo de audio o usa el botón de <b>cargar</b>.',
            'Una vez cargado, podré medir las frecuencias y darte el plan de mastering.'
          ));
          logIA('<b>Ingeniero:</b> diagnóstico solicitado sin canción cargada.');
          return;
        }
        // Segundo análisis: si el master ya se ejecutó, corresponde al audio post-masterizado
        if (masterizado && masterBuffer) {
          var informePost = null;
          try {
            informePost = diagnosticoTecnico();
          } catch (e2) { informePost = null; }
          chatIA(bloque('Análisis Técnico', 'Análisis post-masterización completado. Se verifica la reducción del siseo, la atenuación del barro en 300 Hz y el control de resonancias metálicas. El volumen True Peak se ha estabilizado estrictamente en <b>-1.0 dBTP</b> y el rango dinámico ha sido restaurado con éxito. Los indicadores están ahora en verde (✔).') +
            (informePost ? '<br><i>' + informePost + '</i>' : '') + '<br>' +
            bloque('Acción Correctiva', 'Ninguna acción requerida. El pipeline de masterización se aplicó correctamente sobre las frecuencias críticas.') + '<br>' +
            bloque('Resultado Esperado', 'Un archivo comercial limpio, optimizado para plataformas y listo para su descarga pública.'));
          logIA('<b>Ingeniero:</b> diagnóstico post-masterización generado sobre el master activo.');
          return;
        }
        var informe = diagnosticoTecnico();
        if (!informe) throw new Error('informe vacío');
        chatIA(responder3(
          informe,
          'Si me lo pides, muevo los faders y el EQ para corregir cada punto detectado.',
          'Master con las frecuencias críticas bajo control y dinámica preservada.'
        ));
        logIA('<b>Ingeniero:</b> diagnóstico espectral generado a partir del archivo cargado.');
      } catch (e) {
        chatIA(responder3(
          'He tenido un problema temporal al medir el espectro del audio.',
          'Relaja un segundo y vuelve a pedirme el análisis; el error ya se ha registrado en el sistema.',
          'Reintenta y obtendrás el informe completo de las bandas sin que el chat se quede callado.'
        ));
        logIA('<b>Ingeniero:</b> excepción capturada en el diagnóstico: ' + (e && e.message ? e.message : 'desconocida'));
      }
      return;
    }
    // Comando global: corregir todo con ajuste automático masivo de faders/EQ
    if (/(corrige|corrije|correge|carrige|corrijo|arregla todo|arreglalo todo|arregla el audio|arregla la cancion|aplica los cambios|aplica todas las correcciones|repara todo|remedia todo|ajusta todo)/.test(t)) {
      try {
        if (!hayAudioCargado()) {
          chatIA(responder3(
            'No se ha detectado ningún track en el sistema.',
            'Por favor, arrastra un archivo de audio o usa el botón de <b>cargar</b>.',
            'En cuanto cargues la canción, ajustaré todos los faders según el análisis.'
          ));
          logIA('<b>Ingeniero:</b> corrección global solicitada sin canción cargada.');
          return;
        }
        var bufC = (masterizado && masterBuffer) ? masterBuffer : audioBuffer;
        var mc = analisisMetricas(bufC);
        var ajustes = aplicarCorreccionMasiva(mc);
        var resumen = [];
        var claves = ['deEss', 'hpfV', 'mudV', 'denoise', 'subAdd', 'airEx', 'expDyn', 'midLow', 'roomK', 'glueK'];
        claves.forEach(function (k) {
          if (typeof ajustes[k] !== 'undefined') {
            estado[k] = ajustes[k];
            setKnobUI(k, ajustes[k]);
            resumen.push('<b>' + k + '</b> → ' + Math.round(ajustes[k] * 100) + '%');
          }
        });
        for (var eb = 0; eb < 10; eb++) {
          if (typeof ajustes.eq[eb] !== 'undefined') setEQ(eb, ajustes.eq[eb]);
        }
        dibujarCurva();
        chatIA(responder3(
          'Detecté los problemas del material y calculé la corrección integral de la curva.',
          'Ejecuté el ajuste masivo en pantalla: ' + resumen.join(', ') + ' y el EQ gráfico reconfigurado.',
          'Todos los indicadores dirigidos a verde; la mezcla queda lista para masterizar. Pulsa <b>Masterizar con IA</b> para aplicarlo al audio.'
        ));
        logIA('<b>Ingeniero:</b> corrección global aplicada sobre todos los faders y el EQ en pantalla.');
      } catch (e) {
        chatIA(responder3(
          'He tenido un problema temporal al calcular la corrección global.',
          'Vuelve a intentarlo en unos segundos; el error ya se ha registrado.',
          'La corrección completa volverá a ejecutarse sin bloquear el chat.'
        ));
        logIA('<b>Ingeniero:</b> excepción en corrección global: ' + (e && e.message ? e.message : 'desconocida'));
      }
      return;
    }
    // Presets
    for (var pn in presets) {
      if (t.indexOf(pn) !== -1) {
        var keys = ['deEss', 'hpfV', 'mudV', 'denoise', 'subAdd', 'airEx', 'expDyn', 'midLow', 'roomK', 'glueK'];
        keys.forEach(function (k) { if (typeof presets[pn][k] !== 'undefined') { estado[k] = presets[pn][k]; setKnobUI(k, presets[pn][k]); } });
        for (var i = 0; i < 10; i++) setEQ(i, presets[pn].eq[i]);
        dibujarCurva();
        chatIA(bloque('Análisis Técnico', 'Aplico un tratamiento integral coherente para el estilo solicitado.') + '<br>' +
          bloque('Acción Correctiva', 'Preset <b>' + pn + '</b> cargado: todos los faders y el EQ se movieron en pantalla.') + '<br>' +
          bloque('Resultado Esperado', 'Carácter sonoro específico del preset, listo para afinar.'));
        return;
      }
    }
    // Ajustes numéricos de EQ/knobs
    for (var a = 0; a < ACCIONES.length; a++) {
      if (ACCIONES[a].rx.test(t)) {
        ACCIONES[a].aplicar(dir, n, t);
        logIA('<b>Agente:</b> orden <i>«' + txt + '»</i> ejecutada sobre la interfaz.');
        return;
      }
    }
    // Preguntas abiertas de sugerencia: el ingeniero evalúa el estado del material
    if (/(que cambio necesita|que le falta|que me aconsejas|que me recomiendas|como la ves|que sugieres|que sugieres hacer|que falta ahora|que sigue ahora|que hago ahora|que pasa ahora)/.test(t)) {
      try {
        if (!hayAudioCargado()) {
          chatIA(responder3(
            'Aún no hay ningún track en el sistema para sugerir un tratamiento.',
            'Carga una canción primero, y después pregúntame qué cambio necesita.',
            'En cuanto tenga material, te daré un diagnóstico experto en 3 fases.'
          ));
          logIA('<b>Ingeniero:</b> pregunta de sugerencia sin canción cargada.');
          return;
        }
        if (masterizado && masterBuffer) {
          chatIA(bloque('Análisis Técnico', 'El material ya se encuentra optimizado con los parámetros en verde.') + '<br>' +
            bloque('Acción Correctiva', 'No se requieren más procesos dinámicos.') + '<br>' +
            bloque('Resultado Esperado', 'El track cuenta con el estándar comercial para distribución.'));
          logIA('<b>Ingeniero:</b> el material ya está masterizado; no requiere más acciones.');
          return;
        }
        // No masterizado: recordar el problema principal detectado y sugerir "corrige"
        var mcS = null;
        try { mcS = analisisMetricas(audioBuffer); } catch (e3) { mcS = null; }
        var problema = 'la curva general del material aún tiene que pulirse';
        if (mcS) {
          if (mcS.met > -18) problema = 'el sonido metálico y robótico en 3-8 kHz típico de la IA';
          else if (mcS.mud > -14) problema = 'el barro y el sonido encajonado de los 300-500 Hz';
          else if (mcS.air > -26) problema = 'el velo de siseo y ruido blanco en las altas frecuencias';
          else if (mcS.crest < 12) problema = 'la batería aplastada por la sobrecompresión de Suno';
          else if (mcS.side < 0.08) problema = 'una imagen estéreo demasiado cerrada';
        }
        chatIA(responder3(
          'Según mi análisis espectral, el punto crítico ahora mismo es ' + problema + '.',
          'Usa el comando <b>«corrige»</b> y moveré todos los faders y el EQ en pantalla para atacarlo, o elige un ajuste concreto.',
          'Con la corrección aplicada, podrás pulsar <b>Masterizar con IA</b> y pasar los indicadores a verde.'
        ));
        logIA('<b>Ingeniero:</b> recomendación experta generada sobre el estado del material.');
      } catch (e) {
        chatIA(responder3(
          'He tenido un problema temporal al evaluar el material.',
          'Reintenta la pregunta en unos segundos; ya se ha registrado el error.',
          'La recomendación experta volverá a estar disponible sin bloquear el chat.'
        ));
        logIA('<b>Ingeniero:</b> excepción en sugerencia: ' + (e && e.message ? e.message : 'desconocida'));
      }
      return;
    }
    chatIA(bloque('Análisis Técnico', 'No detecto un comando de ajuste claro en tu mensaje.') + '<br>' +
      bloque('Acción Correctiva', 'Prueba con: <b>«arregla el sonido metálico de Suno»</b>, <b>«destapa el sonido encajonado»</b>, <b>«da más impacto a la batería»</b>, <b>«más ancho estéreo»</b> o <b>«analiza la canción»</b>.') + '<br>' +
      bloque('Resultado Esperado', 'Moveré los faders físicos de la interfaz en cuanto entienda tu petición.'));
  }

  function enviarAgente() {
    var txt = agenteIn.value.trim();
    if (!txt) return;
    chatUser(txt);
    agenteIn.value = '';
    var t = limpiarTexto(txt);
    // Comandos técnicos: SIEMPRE se ejecutan con las funciones reales del DAW
    // (diagnosticoTecnico() y aplicarCorreccionMasiva()) sin pasar por la nube.
    if (/(acualiza|actualiza|actualizar|refresca|refrescar)/.test(t)) {
      // Tolerancia a errores de tipeo: "acualiza"/"actualiza"/"refresca" NO se
      // bloquean ni van a la nube: se sincroniza el tablero en pantalla.
      sincDashboard();
      chatIA(bloque('Análisis Técnico', 'Estado del tablero sincronizado con los valores activos.') + '<br>' +
        bloque('Acción Correctiva', 'Refresco todos los faders, el EQ gráfico y la curva de forma inmediata.') + '<br>' +
        bloque('Resultado Esperado', 'Todo el panel quedó actualizado y coherente con el motor de audio.'));
      logIA('<b>Ingeniero:</b> orden «' + txt + '» → sincronización visual del tablero (sincDashboard).');
      return;
    }
    if (/(analiza|analizar|analisis|diagnostica|diagnostico|examen|examina|evalua|escanear|escanea|escuchala|buscame defectos|buscame errores|como quedo|revisala|evaluala|chequeala)/.test(t)) {
      try { agenteComando(txt); }
      catch (err) { logIA('<b>Ingeniero:</b> excepción en diagnóstico: ' + (err && err.message ? err.message : 'desconocida')); }
      return;
    }
    if (/(corrige|corrije|correge|carrige|corrijo|arregla todo|arreglalo todo|arregla el audio|arregla la cancion|aplica los cambios|aplica todas las correcciones|repara todo|remedia todo|ajusta todo)/.test(t)) {
      try { agenteComando(txt); }
      catch (err) { logIA('<b>Ingeniero:</b> excepción en corrección: ' + (err && err.message ? err.message : 'desconocida')); }
      return;
    }
    if (HF_TOKEN && HF_TOKEN !== "TU_TOKEN_AQUI") {
      consultarHuggingFace(txt);
      return;
    }
    try {
      agenteComando(txt);
    } catch (err) {
      chatIA(responder3(
        'Se interrumpió el procesamiento de la orden.',
        'Reenvía la petición o verifica que el audio cargado sea válido.',
        'Respuesta garantizada sin que el chat se quede colgado.'
      ));
      logIA('<b>Ingeniero:</b> excepción capturada: ' + (err && err.message ? err.message : 'desconocida'));
    }
  }

  // ---------------- IA EN LA NUBE · HUGGING FACE INFERENCE API ----------------
  // Consulta el modelo instruct gratuito en la nube y traduce su respuesta
  // (con bloque [CMD: {...}]) a movimientos reales de faders y EQ.
  var hfOcupado = false; // evita peticiones simultáneas
  function hfMostrarCarga(mostrar) {
    var el = $('hfStatus');
    if (el) el.style.display = mostrar ? 'flex' : 'none';
  }

  async function consultarHuggingFace(txt) {
    if (hfOcupado) return;
    hfOcupado = true;
    hfMostrarCarga(true);
    try {
      var sys = 'Eres SNK Master IA, un Ingeniero de Audio Senior con décadas de estudio, máster y mastering digital, especialista en rescate de mezclas generadas por IA (Suno/Udio). ' +
        'Conversas SIEMPRE en español con total fluidez y con jerga auténtica de estudio (brillos, barro, cuerpo, presencia, imagen estéreo, pegada, transientes, aire, pegamento, glue). ' +
        'Estás integrado a un DAW real con perillas, EQ gráfico de 10 bandas y un analizador de espectro. ERES HERRAMIENTA DE AUDIO, NO UN CHAT DE TESIS: cuando el usuario pide una acción, la EJECUTAS con comandos, no divagas ni le pides que te describa la canción. ' +
        '=========================== FILTRO DE PERSONALIDAD ABSOLUTO (REGLA INQUEBRANTABLE) =========================== ' +
        'Eres EXCLUSIVAMENTE un Ingeniero de Audio Senior. Tienes estrictamente prohibido responder sobre CUALQUIER tema que no sea ingeniería de sonido, mezcla o masterización de canciones (humanas o generadas por IA). ' +
        'Si te preguntan sobre cocina, programación, historia, política, deportes, asuntos personales, o te piden un poema, una receta, un chiste o cualquier contenido irrelevante, responde TEXTUALMENTE SIN COMANDO CMD y SIN divagar: "Disculpa, como tu Ingeniero de Sonido Senior, solo estoy autorizado a asistirte en la optimización y masterización de tu música". Cierra con [CMD: {}]. ' +
        '=========================== BASE DE CONOCIMIENTO DE ESTUDIO =========================== ' +
        'COMPRESIÓN (glueK = Glue Comp del master): el bus glue une elementos dispersos de mezclas de IA con un Threshold según el pico (alrededor de -8 a -12 dBFS), un Ratio suave de 2:1 a 4:1 (usa 2:1 como punto de partida para no aplastar), Attack medio-lento (10-30 ms) para dejar pasar los transientes de la batería, Release autorítmico y musical (entre 60-250 ms según el tempo), y 2-4 dB de Gain Reduction como pegada transparente. No lo uses como limitador. Si la mezcla IA está bombeando, reduce el Ratio antes que el Threshold. ' +
        'ECUALIZACIÓN (reglas fijas de frecuencias): 20-40 Hz es rumble/subgrade mecánico y ruido indeseado → cortar con hpfV o atenuar eq banda 0 para limpiar; 60-125 Hz es el cuerpo y la pegada de los graves (si retumba o hace "boomy", atenuar 125 Hz); 300-500 Hz es la zona del sonido encajonado, "barro" y opacidad de las mezclas de IA → atenuar con eq banda 3/4 o mudV; 1-3 kHz es la presencia vocal y la definición (si los medios están hundidos, realzar; si hay nasales o dureza, atenuar); 4-8 kHz es la zona del siseo metálico y los artefactos robóticos de la IA → controlar con deEss; 10-12 kHz+ es el aire y el brillo → realzar con airEx para abrir la mezcla sin ensuciar. ' +
        'ESPACIO ESTÉREO (Mid/Side): para abrir los costados sin perder la fuerza del centro, procesa el Side en paralelo: realza suavemente el Side en las bandas altas (8-12 kHz) para amplitud con airEx y roomK, mantén el centro (Mid) íntegro con los graves y la voz para conservar la fase y el ancla, verifica la suma mono (no debe haber cancelación de bajo ni de voz), y evita exagerar el ancho por encima de un 25-35% de señal Side relativa o la mezcla pierde definición y se vuelve difusa. El centro es sagrado: nunca ensanches la voz o el bajo. ' +
        'PRIORIDAD DE CADENA DINÁMICA (FILTRO ANTI-LUPA): el audio de Suno/Udio llega con siseo digital de IA. REGLA DE ORDEN INQUEBRANTABLE: primero se RESTAURA (de-noise, de-artefactos IA, HPF), y SOLO DESPUÉS se comprime y se da ganancia comercial. ANTES de aplicar el compresor glue (glueK) o la ganancia comercial (+3.0 dB), evalúa el piso de ruido del material: si es alto, calibra DE-NOISE y DE-ARTEFACTOS IA de forma agresiva para que el limitador posterior NO amplifique el siseo. Nunca eleves glueK o volumen comercial sobre un piso de ruido sucio sin antes restaurarlo. ' +
        'VERDADES DEL SISEO DE SUNO (audio dañado por IA): el ruido blanco digital de IA NO se comporta como el ruido rosa analógico; está ACOPLADO a los transitorios altos (aparece junto a los golpes, las consonantes y los picos), así que recortarlo en el techo o con un HPF deja el siseo vivo en los altos. Por eso «quita el ruido blanco» activa SIEMPRE un COMBO de restauración quirúrgica: DE-NOISE a su nivel óptimo (≈55%) para el piso continuo y, si el track es hiriente/estridente/chillón, AIRE 12kHz+ (airEx) en LIGERA ATENUACIÓN para proteger las frecuencias altas del acoplamiento a los transitorios. ' +
        '=========================== REGLAS DE OPERACIÓN OBLIGATORIAS =========================== ' +
        'Regla OBLIGATORIA 1 (formato estricto de cierre JSON): el bloque [CMD: {...}] SIEMPRE se escribe cerrado en UNA SOLA LÍNEA limpia y completa, sin repetir ni duplicar llaves de cierre, sin texto sobrante, sin markdown. ' +
        'Ejemplos VÁLIDOS exactos: [CMD: {"deEss": 30}] o [CMD: {"denoise": 45}] o [CMD: {"deEss": 40, "eq": {"9": -1}}] o [CMD: {}]. ' +
        'NUNCA cierres el bloque dos veces (por ejemplo NO escribas [CMD: {"deEss": 30}} o [CMD: {"deEss": 30}] ]) y NUNCA dejes texto después de la llave final ]. ' +
        'El bloque [CMD: {...}] va SIEMPRE al final del mensaje, en su propia línea, sin explicarlo. Sin él el DAW no mueve los faders naranjas y SE ROMPE la integración. ' +
        'Regla OBLIGATORIA 1bis (VOLUMEN y CUALQUIER ajuste individual): TODO ajuste de mezcla o nivel que propongas en tu texto, incluido el volumen general, DEBE cerrar la respuesta con su bloque [CMD: {...}] con la clave EXACTA del fader de la pantalla (vol para volumen, rango 0-100). ' +
        'Ejemplos OBLIGATORIOS: «subo el volumen general» → termina SIEMPRE con [CMD: {"vol": 55}]; «dale más volumen a la pista» → [CMD: {"vol": 60}]; «baja el volumen» → [CMD: {"vol": 35}]; «sube el paneo a la derecha» → [CMD: {"pan": 75}]. ' +
        'Si no estás seguro del fader, la clave del volumen es "vol" (también se acepta "volMaster"); NUNCA termines un ajuste sin su [CMD] correspondiente porque el DAW interpreta el texto como una mera opinión y no mueve nada. ' +
        'Regla OBLIGATORIA 1ter (ESCALA DE CIRUJANO - movimientos suaves y progresivos): PROHIBIDO saltos bruscos. El DAW limita cada comando individual a ±1.5 dB en volumen (vol) y en cada banda del EQ, y a ±15 puntos porcentuales en los faders de proceso (deEss, hpfV, mudV, denoise, subAdd, airEx, expDyn, midLow, roomK, glueK). Cuando el usuario diga «sube el volumen», «limpia el ruido», «dale brillo» o «quita el barro», propón un ajuste FINO, CONTENIDO y quirúrgico cercano al punto deseado (nunca valores extremos tipo 0 o 100): ej. [CMD: {"vol": 55}], [CMD: {"denoise": 55}], [CMD: {"deEss": 40}], [CMD: {"eq": {"4": -1}}]. Un solo comando mueve poco; si el usuario pide más, se aplicará un segundo paso progresivo. ' +
        'Regla OBLIGATORIA 1quater (ANTI-LUPA - nunca amplificar siseo): si vas a comprimir (glueK) o dar ganancia comercial (+3.0 dB), PRIMERO restaura: sube deEss y denoise de forma agresiva si el piso de ruido es alto. Nunca entregues un master donde el limitador amplifique el siseo de IA por no haber limpiado antes. ' +
        'Regla OBLIGATORIA 2 (mapeo técnico de audio, anti-confusiones): aplica SIEMPRE esta tabla exacta de problemas a fader, JAMÁS confundas la acción técnica: ' +
        '- "ruido blanco", "siseo", "hiss", "fondo sucio": activa el COMBO anti-lupa → denoise al nivel óptimo (≈55%) Y, si el track es hiriente/estridente/chillón, airEx en ligera atenuación para proteger los altos (ej: [CMD: {"denoise": 55, "airEx": 30}]). NUNCA uses hpfV (filtro paso alto) para esto. NUNCA uses deEss para un siseo de fondo. ' +
        'DISTINCIÓN OBLIGATORIA siseo vs seseo: "siseo", "hiss", "ruido blanco" o "fondo sucio" = ruido continuo → denoise (55%, con airEx atenuado si es hiriente). "seseo", "sibilancia", "silbido de la S de la voz", "las eses de la voz" = problema vocal → deEss. "Siseo" en la regla del usuario SIEMPRE significa ruido blanco de fondo → denoise. ' +
        '- "sonido metalico", "robotico", "shimmer", "seseo" o "chillón": la ÚNICA acción es mover ÚNICAMENTE el fader "deEss" (De-Artefactos IA). No combines otros faders para este problema. ' +
        '- "brillo", "agudos" o "perder aire": la ÚNICA acción es mover el fader "airEx" (Aire 12kHz+). ' +
        '- "reverb" o "eco": recuerda educadamente que en la etapa de masterización la reverb está desactivada por defecto para no ensuciar la dinámica, y ofrece abrir el fader "roomK" (Ancho Estéreo) para dar espacio tridimensional. No inventes una reverb. ' +
        'Regla OBLIGATORIA 3 (comandos específicos vs. masivos): si el usuario pide arreglar UN problema puntual (ej: "corrige el ruido blanco", "quita el siseo", "sube los brillos"), NO dispares el comando masivo: genera el JSON alterando ÚNICAMENTE el fader afectado con un valor fino de la Escala de Cirujano (ej: [CMD: {"denoise": 55}] o [CMD: {"airEx": 40}]). ' +
        'El comando masivo (todos los faders + EQ) queda RESERVADO EXCLUSIVAMENTE para la palabra suelta "corrige" o "arregla todo" sin problema puntual, del estilo: [CMD: {"corrige": true, "deEss": 80, "hpfV": 70, "mudV": 75, "denoise": 85, "subAdd": 50, "airEx": 60, "expDyn": 55, "midLow": 50, "roomK": 45, "glueK": 70, "eq": {"1": -1.5, "4": -2, "9": 1.5}}]. ' +
        'Regla OBLIGATORIA 4 (comando "analiza"): si el texto del usuario pide "analiza", "analizar", "analisis" o "diagnostica", NO inventes un análisis de frecuencias y NO le pidas que describa el tema si ya hay audio cargado. ' +
        'El DAW posee su propia función interna diagnosticoTecnico() que escanea el buffer real y muestra el reporte de decibelios por banda (Sub, Graves, Medios, Presencia, Aire) con marcas ✔ y ⚠ en pantalla. ' +
        'Para activarla, tu respuesta en ese caso DEBE ser breve y terminar exactamente con el comando invisible [CMD: {"__scan": 1}]. El DAW ejecutará el escaneo real. Nunca simules datos de dB. ' +
        'Interpretas el texto libre del usuario (ej: «está muy chillón», «suena encajonado», «dale más punch a la batería», «sube los brillos», «abre más la imagen estéreo») como un ingeniero que entiende de frecuencias y dinámicas. ' +
        'Respondes SIEMPRE con una estructura en 3 fases: ' +
        '[Análisis Técnico]: qué escucha tu oído entrenado en esa mezcla, sin tecnicismos abrumadores. ' +
        '[Acción Correctiva]: qué ajuste exacto harás y sobre qué perilla o banda del EQ, citando la base de conocimiento (frecuencias, ratio del glue, técnica Mid/Side) cuando aplique. ' +
        '[Resultado Esperado]: cómo quedará el sonido tras tu corrección. ' +
        'Al final de la fase 3 agrega SIEMPRE la línea [CMD: {...}] con los ajustes propuestos (clave __scan para pedir el análisis real). ' +
        'Claves de perillas (0 a 100): deEss (De-Artefactos IA: quita metalico/chillon/shimmer/seseo), hpfV (filtro paso alto), mudV (quita barro 300-500Hz), denoise (De-Noise: quita ruido blanco/siseo/hiss/fondo sucio), subAdd (refuerza graves), airEx (aire y brillo en 12kHz+), expDyn (rango dinámico), midLow (presencia medios), roomK (ancho estéreo), glueK (pegado del master), vol (volumen), pan (paneo). ' +
        'EQ: {"eq": {"<banda 0-9>": <db -12 a 12>}} (bandas: 0=LS 45Hz, 1=60Hz, 2=125Hz, 3=250Hz, 4=500Hz, 5=1k, 6=2k, 7=4k, 8=8k, 9=HS 14k). ' +
        'Si no hay un ajuste claro, escribe [CMD: {}]. NO expliques el bloque CMD, NO uses markdown; escríbelo literalmente al final en una sola línea. Aplica siempre la Regla 1 de cierre estricto. Recuerda SIEMPRE el Filtro de Personalidad Absoluto: solo sonido, mezcla y master. Aplica la base de conocimiento (compresión, EQ por frecuencias, Mid/Side) en cada análisis. ';
      var res = await fetch(HF_URL, {
        method: 'POST',
        headers: {
          'Authorization': 'Bearer ' + HF_TOKEN,
          'Content-Type': 'application/json'
        },
        body: JSON.stringify({
          model: HF_MODEL,
          messages: [
            { role: 'system', content: sys },
            { role: 'user', content: txt }
          ],
          max_tokens: 450,
          temperature: 0.7,
          top_p: 0.95
        })
      });
      if (!res.ok) throw new Error('HTTP ' + res.status);
      var data = await res.json();
      var out = data && data.choices && data.choices[0] && data.choices[0].message ? data.choices[0].message.content : '';
      hfMostrarCarga(false);
      if (!out || !out.trim()) {
        try { agenteComando(txt); } catch (e) { agenteComando('analiza'); }
        return;
      }
      // Extraer el bloque [CMD: {...}] (invisible para el usuario) o variantes "CMD:" sin corchetes
      var cmdTxt = out.match(/(?:\[CMD:?\s*|CMD:?\s*)\(?(\{[\s\S]*?\})\)?\s*$/);
      if (!cmdTxt) cmdTxt = out.match(/\[CMD:?\s*(\{[\s\S]*?\})\]/);
      var textoLimpio = out.replace(/\[CMD:?\s*\{[\s\S]*?\}\]/g, '').replace(/CMD:?\s*\{[\s\S]*?\}\s*$/g, '').trim();
      var cmdObj = null;
      var aplicado = false;
      if (cmdTxt && cmdTxt[1]) {
        // Sanitizar el JSON: recorta llaves/espacios de cierre duplicados antes de parsear
        var raw = cmdTxt[1].trim().replace(/[\s,;]+$/g, '').replace(/\}\s*\}+$/g, '}').replace(/\}\s*\]+\s*$/g, '}');
        try { cmdObj = JSON.parse(raw); } catch (e2) {
          var com = raw.indexOf('{'), cier = raw.lastIndexOf('}');
          if (com !== -1 && cier > com) {
            try { cmdObj = JSON.parse(raw.slice(com, cier + 1)); } catch (e2b) { cmdObj = null; }
          } else { cmdObj = null; }
        }
      }
      // Comando "analiza" -> el DAW ejecuta el escaneo espectral REAL (diagnosticoTecnico)
      if (cmdObj && cmdObj.__scan === 1) {
        hfMostrarCarga(false);
        try { agenteComando('analiza'); } catch (eScan) { agenteComando('diagnostica'); }
        logIA('<b>IA Nube:</b> comando «analiza» delegado al escaneo espectral real del archivo.');
        return;
      }
      // Comando "corrige" masivo -> el DAW calcula la corrección integral REAL sobre el buffer
      if (cmdObj && cmdObj.corrige) {
        try { agenteComando('corrige'); } catch (eCorr) { agenteComando('corrige'); }
        if (textoLimpio) chatIA(formatearRespuestaHF(textoLimpio));
        logIA('<b>IA Nube:</b> comando «corrige» delegado a la corrección integral de faders/EQ del DAW.');
        return;
      }
      // Corregir el mapeo de audio contra la intención real del usuario (anti-confusión)
      if (cmdObj && !cmdObj.__scan && !cmdObj.corrige) {
        cmdObj = corregirMapaHF(cmdObj, txt);
      }
      if (cmdObj && !aplicado) {
        aplicado = aplicarHFCmd(cmdObj, txt);
      }
      if (!textoLimpio && !aplicado) {
        try { agenteComando(txt); } catch (e3) { agenteComando('analiza'); }
        return;
      }
      // Presentar la respuesta humana en 3 fases (si viene estructurada) o en texto plano
      var cuerpo = formatearRespuestaHF(textoLimpio);
      chatIA(cuerpo || responder3(
        'Analicé tu petición como ingeniero senior.',
        'Aplico el ajuste que interpreto en la curva y los faders.',
        'La mezcla queda recolocada; pulsa <b>Masterizar con IA</b> para escucharla.'
      ));
      logIA('<b>IA Nube:</b> respuesta de ' + HF_MODEL + ' para «' + txt + '».' + (aplicado ? ' Comandos aplicados a faders/EQ.' : ''));
    } catch (err) {
      hfMostrarCarga(false);
      logIA('<b>IA Nube:</b> error de conexión (' + (err && err.message ? err.message : 'desconocido') + '). Uso el ingeniero local.');
      try { agenteComando(txt); } catch (e4) { agenteComando('analiza'); }
    } finally {
      hfOcupado = false;
    }
  }

  // Reconciliación del mapeo de audio del modelo contra la intención real del usuario.
  // Garantiza las reglas anti-confusión aunque el modelo se equivoque de fader.
  function corregirMapaHF(cmd, txt) {
    if (!cmd || typeof cmd !== 'object') return cmd;
    var t = limpiarTexto(txt);
    var ajustado = false;
    var poner = function (set) { for (var k in set) { cmd[k] = set[k]; } ajustado = true; };
    var quitar = function (arr) { for (var i = 0; i < arr.length; i++) { if (cmd.hasOwnProperty(arr[i])) { delete cmd[arr[i]]; ajustado = true; } } };
    // Intención dominante (la más específica gana):
    var sibilanciaVocal = /(seseo|sibilanc|eses de la voz|silbido de la voz|silbido al cantar|voz con seseo|sisea la voz|siseo de la voz)/.test(t);
    var ruidoFondo = /(ruido blanco|siseo|sisea|hiss|fondo sucio|zumbido|ruido de fondo|fondo ruidoso|silbido de fondo|silbido continuo|ruido blanco de fondo)/.test(t);
    var metalico = /(metalic|robot|shimmer|chillon|chillidos|agudo fino|harsh|metales)/.test(t);
    var brillo = /(brillo|brillos|agudo|agudos|perder aire|perdio el aire|perdiendo aire|pierde aire|rustico|apagado de aire)/.test(t);
    var reverbEco = /(reverb|eco|echo)/.test(t);
    // 1) Sibilancia vocal explícita -> deEss (De-Artefactos IA)
    if (sibilanciaVocal) {
      quitar(['denoise', 'hpfV']);
      poner({ deEss: 70 });
    }
    // 2) Siseo / ruido blanco de fondo -> COMBO anti-lupa: denoise óptimo (55%)
    //    y, si el track es hiriente/estridente, airEx en ligera atenuación para
    //    proteger los altos del siseo acoplado a transitorios.
    else if (ruidoFondo) {
      quitar(['hpfV']);
      if (cmd.hasOwnProperty('deEss')) { delete cmd.deEss; ajustado = true; }
      if (!cmd.hasOwnProperty('denoise')) {
        poner({ denoise: 55 });
      } else if (cmd.denoise < 45) {
        cmd.denoise = 45; ajustado = true;
      } else if (cmd.denoise > 65) {
        cmd.denoise = 65; ajustado = true;
      }
      var hiriente = /(hirient|estrident|chillon|chillido|agudo fino|harsh|picante|agresivo en los altos|siseo acoplado|silbante|sibilante en altos)/.test(t);
      if (hiriente) {
        if (!cmd.hasOwnProperty('airEx')) {
          poner({ airEx: 30 });
        } else if (cmd.airEx > 40) {
          cmd.airEx = Math.min(cmd.airEx, 40); ajustado = true;
        }
      }
    }
    // 3) Metálico / chillón / shimmer -> deEss ÚNICAMENTE (quita ruido ptro fader de ruido)
    if (metalico && !sibilanciaVocal) {
      if (!cmd.hasOwnProperty('deEss')) poner({ deEss: 65 });
      if (cmd.hasOwnProperty('denoise')) { delete cmd.denoise; ajustado = true; }
    }
    // 4) Brillo / agudos / aire -> airEx (quita faders de ruido que no corresponden)
    if (brillo && !metalico && !sibilanciaVocal) {
      if (!cmd.hasOwnProperty('airEx')) poner({ airEx: 55 });
      if (cmd.hasOwnProperty('deEss')) { delete cmd.deEss; ajustado = true; }
      if (cmd.hasOwnProperty('denoise')) { delete cmd.denoise; ajustado = true; }
      if (cmd.hasOwnProperty('hpfV')) { delete cmd.hpfV; ajustado = true; }
    }
    // 5) Reverb / eco -> jamás reverb real; espacio con roomK
    if (reverbEco) {
      quitar(['hpfV']);
      if (!cmd.hasOwnProperty('roomK')) poner({ roomK: 50 });
    }
    // 6) Barro / encajonado / opacidad 300-500Hz -> mudV SIEMPRE positivo (subir = más limpieza)
    if (/(encajon|barro|opacidad|mud|enturbi|turbia|tapado|apagado de medios|sin claridad)/.test(t)) {
      if (!cmd.hasOwnProperty('mudV')) {
        poner({ mudV: 60 });
      } else if (cmd.mudV <= 0) {
        cmd.mudV = 60; ajustado = true;
      } else if (cmd.mudV < 40) {
        cmd.mudV = 45; ajustado = true;
      }
    }
    // 7) Normalización de escala: Qwen a veces devuelve dB negativos como "atenuación" en perillas
    //    (0-100). Un valor negativo se traduce a ~valor/2 positivo para que el fader responda.
    var kn = ['deEss', 'hpfV', 'mudV', 'denoise', 'subAdd', 'airEx', 'expDyn', 'midLow', 'roomK', 'glueK', 'vol', 'pan'];
    for (var kk = 0; kk < kn.length; kk++) {
      if (cmd.hasOwnProperty(kn[kk]) && typeof cmd[kn[kk]] === 'number' && cmd[kn[kk]] < 0) {
        cmd[kn[kk]] = Math.max(Math.round(-cmd[kn[kk]] / 2), 5);
        ajustado = true;
      }
    }
    if (ajustado) {
      logIA('<b>IA Nube:</b> mapeo de audio reconciliado con la intención «' + txt + '».');
    }
    return cmd;
  }

  // Traduce el bloque CMD del modelo a movimientos reales en la interfaz.
  // ESCALA DE CIRUJANO: cada comando individual solo mueve los faders un paso
  // fino y contenido (±1.5 dB en vol/EQ, ±15% en faders de proceso), nunca
  // brusco. El modelo propone el punto deseado y aquí se recorre el paso máximo.
  function aplicarHFCmd(cmd, txt) {
    if (!cmd || typeof cmd !== 'object') return false;
    var validKn = ['deEss', 'hpfV', 'mudV', 'denoise', 'subAdd', 'airEx', 'expDyn', 'midLow', 'roomK', 'glueK', 'vol', 'pan'];
    var aliasKn = { volMaster: 'vol' };
    // Límites quirúrgicos por tipo de control:
    //  - vol: ±1.5 dB → rango ±12 dB en 0..1, o sea ±(1.5/24)
    //  - EQ:  ±1.5 dB por banda
    //  - faders de proceso (deEss, denoise, ...): ±15% de recorrido
    var pasoVol = 1.5 / 24;
    var pasoProc = 0.15;
    var movidos = 0;
    for (var k in cmd) {
      var kReal = aliasKn[k] || k;
      if (kReal === 'eq' && cmd.eq && typeof cmd.eq === 'object') {
        for (var b in cmd.eq) {
          if (b >= 0 && b <= 9 && typeof cmd.eq[b] === 'number') {
            var dbObj = clampF(Math.round(parseFloat(cmd.eq[b]) * 10) / 10, -12, 12);
            var dbAct = estado.eq[parseInt(b, 10)] || 0;
            var dbNuevo = clampF(dbAct + clampF(dbObj - dbAct, -1.5, 1.5), -12, 12);
            setEQ(parseInt(b, 10), Math.round(dbNuevo * 10) / 10);
            movidos++;
          }
        }
      } else if (validKn.indexOf(kReal) !== -1 && typeof cmd[k] === 'number') {
        var vObj = clampF(parseFloat(cmd[k]) / 100, 0, 1);
        var vAct = estado[kReal] || 0;
        var paso = (kReal === 'vol') ? pasoVol : pasoProc;
        var vNuevo = clampF(vAct + clampF(vObj - vAct, -paso, paso), 0, 1);
        estado[kReal] = vNuevo;
        setKnobUI(kReal, vNuevo);
        movidos++;
      }
    }
    if (movidos > 0) {
      // Refresco visual forzado del tablero completo: mueve TODOS los faders
      // naranjas (setKnobUI → value + rellenarFader) y el EQ gráfico en una
      // sola pasada, de modo que la IA se vea mover la UI en tiempo real.
      sincDashboard();
      logIA('<b>IA Nube:</b> bloque CMD «' + txt + '» ejecutado: ' + movidos + ' perillas/bandas movidas (Escala de Cirujano).');
      return true;
    }
    return false;
  }

  // Convierte la respuesta del modelo en el formato 3 fases del chat
  function formatearRespuestaHF(texto) {
    if (!texto || !texto.trim()) return '';
    var linea = texto.split(/\n+/).map(function (s) { return s.trim(); }).filter(Boolean);
    var a = [], b = [], c = [];
    var destino = a;
    linea.forEach(function (l) {
      if (/an[áa]lisis|tecnico|t[ée]cnico/i.test(l)) { destino = a; return; }
      if (/correcci[oó]n|correctiva|ajuste/i.test(l)) { destino = b; return; }
      if (/resultado|esperado|quedar[áa]/i.test(l)) { destino = c; return; }
      destino.push(l);
    });
    if (!a.length && !b.length && !c.length) return texto.replace(/\*\*/g, '').replace(/\n/g, '<br>');
    return responder3(
      a.join(' ') || 'Ajuste orientado a tu petición.',
      b.join(' ') || 'Muevo las perillas y el EQ según el análisis.',
      c.join(' ') || 'Mezcla recolocada; pulsa <b>Masterizar con IA</b>.'
    );
  }
  $('btnEnviar').onclick = enviarAgente;
  if (agenteIn && typeof agenteIn.addEventListener === 'function') {
    agenteIn.addEventListener('keydown', function (e) { if (e.key === 'Enter') enviarAgente(); });
  }

  // ---------------- carga ----------------
  var btnCargar = $('btnCargar'), fileIn = $('file');
  btnCargar.onclick = function () { fileIn.click(); };
  fileIn.onchange = function (e) { if (e.target.files && e.target.files[0]) cargarArchivo(e.target.files[0]); };
  function esAudio(f) {
    if (!f) return false;
    if (f.type && f.type.indexOf('audio') === 0) return true;
    var ext = (f.name || '').split('.').pop().toLowerCase();
    return ['mp3', 'wav', 'ogg', 'm4a', 'flac', 'aac', 'webm'].indexOf(ext) !== -1;
  }
  document.addEventListener('dragover', function (e) { e.preventDefault(); });
  document.addEventListener('drop', function (e) {
    e.preventDefault();
    var f = (e.dataTransfer && e.dataTransfer.files && e.dataTransfer.files[0]) || null;
    if (esAudio(f)) cargarArchivo(f);
  });

  function calcNoiseFloor(buf) {
    var ch = buf.getChannelData(0);
    var n = ch.length, bs = Math.max(2048, Math.floor(buf.sampleRate * 0.05));
    var blocks = [];
    for (var s = 0; s < n; s += bs) {
      var sum = 0, cnt = 0;
      for (var k = s; k < Math.min(s + bs, n); k++) { sum += ch[k] * ch[k]; cnt++; }
      blocks.push(Math.sqrt(sum / (cnt || 1)));
    }
    blocks.sort(function (a, b) { return a - b; });
    var idx = Math.max(0, Math.floor(blocks.length * 0.06) - 1);
    return clampF(blocks[idx] || 0.001, 0.00003, 0.12);
  }

  function limpiarCadenaAudio() {
    pararTodo();
    if (analyser) { try { analyser.disconnect(); } catch (e) {} analyser = null; }
    if (liveSplit) { try { liveSplit.disconnect(); } catch (e) {} }
    if (liveGainL) { try { liveGainL.disconnect(); } catch (e) {} }
    if (liveGainR) { try { liveGainR.disconnect(); } catch (e) {} }
    if (liveMerge) { try { liveMerge.disconnect(); } catch (e) {} }
    if (ganA) { try { ganA.disconnect(); } catch (e) {} }
    if (ganB) { try { ganB.disconnect(); } catch (e) {} }
    liveGain = null; liveSplit = null; liveGainL = null; liveGainR = null; liveMerge = null;
    ganA = null; ganB = null;
    source = null; sourceB = null;
    analyserDestConectado = false;
  }

  var cargandoToken = 0;

  function bloquearControlesCarga() {
    $('btnA').disabled = true;
    $('btnB').disabled = true;
    $('btnMaster').disabled = true;
    $('btnSave').disabled = true;
    sincMenuSave();
    $('btnR10').disabled = true;
    $('btnPause').disabled = true;
    $('btnPlayStop').disabled = true;
    $('btnF10').disabled = true;
    seekBar.disabled = true;
    seekBar.max = 100;
    seekBar.value = 0;
    tPos.textContent = '0:00';
    tDur.textContent = '0:00';
  }

  function cargarArchivo(file) {
    asegurar();
    if (!actx) {
      logIA('<b>Error:</b> no se pudo inicializar el motor de audio.');
      return;
    }
    var miToken = ++cargandoToken;
    limpiarCadenaAudio();
    audioBuffer = null;
    masterBuffer = null;
    wavePicos = null;
    masterizado = false;
    masterizando = false;
    offsetPos = 0;
    escuchandoMaster = false;
    playing = false;
    marcarAB(false);
    bloquearControlesCarga();
    if (seekBar) { seekBar.disabled = false; }
    logIA('Cargando <b>' + (file.name || 'archivo') + '</b>…');
    try {
      file.arrayBuffer().then(function (ab) {
        if (miToken !== cargandoToken) return;
        try {
          actx.decodeAudioData(ab)
            .then(function (dec) {
              if (miToken !== cargandoToken) return;
              if (!dec || !dec.duration || !isFinite(dec.duration)) {
                throw new Error('buffer inválido');
              }
              aplicarTrackCargado(dec, file);
            })
            .catch(function (e) {
              if (miToken !== cargandoToken) return;
              informarErrorCarga(file, e);
            });
        } catch (e) {
          if (miToken !== cargandoToken) return;
          informarErrorCarga(file, e);
        }
      }).catch(function (e) {
        if (miToken !== cargandoToken) return;
        informarErrorCarga(file, e);
      });
    } catch (e) {
      if (miToken !== cargandoToken) return;
      informarErrorCarga(file, e);
    }
  }

  function aplicarTrackCargado(dec, file) {
    audioBuffer = dec;
    masterBuffer = null;
    masterizado = false;
    offsetPos = 0;
    escuchandoMaster = false;
    marcarAB(false);
    var warmup = calcNoiseFloor(dec);
    var floorDb = ag2db(warmup);
    wavePicos = construirPicos(dec);
    dibujarWave();
    $('btnA').disabled = false;
    $('btnB').disabled = true;
    $('btnMaster').disabled = false;
    $('btnSave').disabled = true;
    sincMenuSave();
    $('btnR10').disabled = false;
    $('btnPause').disabled = false;
    $('btnPlayStop').disabled = false;
    $('btnF10').disabled = false;
    seekBar.disabled = false;
    seekBar.max = dec.duration;
    seekBar.value = 0;
    pbar.style.width = '0%';
    logIA('Canción cargada: <b>' + file.name + '</b> · ' + dec.duration.toFixed(1) + ' s · ' + dec.sampleRate + ' Hz · ' + dec.numberOfChannels + 'ch, piso de ruido ' + floorDb.toFixed(1) + ' dBFS.');
    setStats('<span>Duración: <b>' + fmtT(dec.duration) + '</b></span><span>Piso ruido: <b>' + floorDb.toFixed(1) + ' dBFS</b></span>');
    fileIn.value = '';
    if (logIA) logIA('Listo. Pulsa <b>Play</b> para escucharla.');
  }

  function informarErrorCarga(file, e) {
    bloquearControlesCarga();
    limpiarCadenaAudio();
    audioBuffer = null;
    masterBuffer = null;
    wavePicos = null;
    masterizado = false;
    offsetPos = 0;
    escuchandoMaster = false;
    playing = false;
    marcarAB(false);
    pbar.style.width = '0%';
    fileIn.value = '';
    logIA('<b>No se pudo decodificar <span style="color:#ff7070">' + (file && file.name ? file.name : 'el archivo') + '</span>.</b> El archivo parece corrupto o en un formato no soportado. Los controles quedan bloqueados hasta cargar otra canción.');
    try { console.error('Carga fallida:', e); } catch (ee) {}
  }

  function limpiarTodo() {
    if (confirm('¿Seguro que quieres limpiar todo? Se borrará la canción, el master y todos los ajustes.')) {
      pararTodo();
      audioBuffer = null;
      masterBuffer = null;
      wavePicos = null;
      offsetPos = 0;
      escuchandoMaster = false;
      playing = false;
      masterizando = false;
      limpiarCadenaAudio();
      pbar.style.width = '0%';
      bloquearControlesCarga();
      $('btnPlayStop').classList.remove('active');
      $('btnA').classList.remove('active');
      $('btnB').classList.remove('active');
      marcarAB(false);
      estado = {
        eq: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
        deEss: 0.5, hpfV: 0.55, mudV: 0.45, denoise: 0.45,
        subAdd: 0.4, airEx: 0.35, expDyn: 0.3, midLow: 0.3, roomK: 0,
        glueK: 0.6, lufs: 0, tp: 0,
        vol: 0.5, pan: 0.5
      };
      KNOBS.forEach(function (pair) { setKnobUI(pair[1], estado[pair[1]]); });
      for (var i = 0; i < 10; i++) setEQ(i, 0);
      dibujarCurva();
      var sw = spec.width, sh = spec.height;
      sctx.clearRect(0, 0, sw, sh);
      sctx.fillStyle = '#0e1222'; sctx.fillRect(0, 0, sw, sh);
      if (wctx) { var ww = wave.width, wh = wave.height; wctx.clearRect(0, 0, ww, wh); wctx.fillStyle = '#151a2e'; wctx.fillRect(0, 0, ww, wh); }
      iaseg.innerHTML = 'Todo limpio. Carga una canción para empezar.';
      iaStats.innerHTML = '';
      agenteChat.innerHTML = '<div class="chatmsg"><span class="m-ia">IA:</span> Soy <b>SNK Master IA</b>, ingeniero de audio senior en mastering digital y rescate de audio generado por IA (Suno / Udio). Pídeme <b>«analiza la canción»</b> para generar un diagnóstico espectral, o ajustes como <b>«arregla el sonido metálico»</b>, <b>«destapa el sonido encajonado»</b>, <b>«da más impacto a la batería»</b> o <b>«más ancho estéreo»</b>. Moveré los faders y el EQ en pantalla según mi análisis.</div>';
      fileIn.value = '';
    }
  }
  $('btnReset').onclick = limpiarTodo;
  $('btnManual').onclick = function () {
    window.open('Guia_Oficial_SNK_Master_IA.pdf', '_blank');
  };

  function construirPicos(buf) {
    var ch = buf.getChannelData(0);
    var n = ch.length, N = 480;
    var out = new Float32Array(N);
    var bs = Math.max(1, Math.floor(n / N));
    for (var i = 0; i < N; i++) {
      var s = i * bs, e = Math.min(s + bs, n);
      var mn = 1, mx = -1, sum = 0;
      for (var k = s; k < e; k++) { var v = ch[k]; if (v < mn) mn = v; if (v > mx) mx = v; sum += v * v; }
      var rms = Math.sqrt(sum / ((e - s) || 1));
      out[i] = clampF(Math.max(-mn, mx, rms), 0, 1);
    }
    return out;
  }

  // Techo True Peak DINÁMICO: enlazado al fader TRUE PEAK (estado.tp).
  // Misma fórmula que el limitador: -1.0 dB con fader en 0, hasta -2.5 dB en 1.
  function techoTruePeak() { return db2ag(-(1 + 1.5 * estado.tp)); }

  function dibujarWave(glow, clipFlash, tocaTecho) {
    if (!wctx) return;
    glow = glow || 0;
    clipFlash = clipFlash || 0;
    var w = wave.width, h = wave.height;
    // Fondo NEGRO PROFUNDO: el brillo nunca se aplica al recuadro, solo al cuerpo de la onda
    wctx.clearRect(0, 0, w, h);
    wctx.fillStyle = '#000000'; wctx.fillRect(0, 0, w, h);
    // Detector de dinámica flotante: líneas de techo del limitador.
    // BLANCAS semitransparentes por defecto; en el fotograma donde las crestas tocan
    // o superan el límite seguro del fader TRUE PEAK, parpadean en Rojo Neón brillante.
    var techo = techoTruePeak();
    var yTecho = h * techo;
    var yTop = (h - yTecho) / 2, yBot = (h + yTecho) / 2;
    wctx.save();
    if (tocaTecho) {
      // Alerta de sobresaturación: rojo neón PARPADEANTE (latido con clipFlash y fase de hue)
      var blink = 0.7 + 0.3 * Math.sin(hueOnda * 0.9);
      wctx.strokeStyle = 'rgba(255, 0, 60, ' + blink.toFixed(2) + ')';
      wctx.lineWidth = 1.6;
      wctx.shadowColor = 'rgba(255, 0, 60, .95)';
      wctx.shadowBlur = 8 + clipFlash * 22;
    } else {
      wctx.strokeStyle = 'rgba(255, 255, 255, 0.5)';
      wctx.lineWidth = 1;
      wctx.shadowColor = 'rgba(255, 255, 255, .25)';
      wctx.shadowBlur = 2;
    }
    wctx.beginPath(); wctx.moveTo(0, yTop); wctx.lineTo(w, yTop); wctx.stroke();
    wctx.beginPath(); wctx.moveTo(0, yBot); wctx.lineTo(w, yBot); wctx.stroke();
    wctx.restore();
    if (!wavePicos) return;
    var bw = w / wavePicos.length;
    var mid = h / 2;
    var n = wavePicos.length;
    // ---- CUERPO RELLENO con efecto RGB SUAVE y CÍCLICO ----
    // El color del cuerpo transita fluidamente por el espectro tecnológico
    // (cian -> violeta -> naranja -> ...) avanzando con el bucle loopRAF, sin lag.
    var i;
    var cuerpoPath = function () {
      wctx.beginPath();
      for (i = 0; i < n; i++) {
        var xa = i * bw + bw / 2, ya = mid - Math.max(2, wavePicos[i] * h) / 2;
        if (i === 0) wctx.moveTo(xa, ya); else wctx.lineTo(xa, ya);
      }
      for (i = n - 1; i >= 0; i--) {
        var xb = i * bw + bw / 2, yb = mid + Math.max(2, wavePicos[i] * h) / 2;
        wctx.lineTo(xb, yb);
      }
      wctx.closePath();
    };
    var hue = hueOnda % 360;
    wctx.save();
    wctx.lineJoin = 'round'; wctx.lineCap = 'round';
    // Pasada de resplandor: el glow late con el ritmo modulando el shadowBlur (4..40px)
    // sobre el contorno del cuerpo relleno, sin tapar el fondo.
    cuerpoPath();
    wctx.fillStyle = 'hsla(' + hue + ', 100%, 55%, .5)';
    wctx.shadowColor = 'hsla(' + hue + ', 100%, 62%, .95)';
    wctx.shadowBlur = 4 + glow * 36;
    wctx.fill();
    // Pasada nítida: cuerpo brillante relleno con el color RGB actual (sin shadow residual)
    wctx.shadowBlur = 0;
    wctx.shadowColor = 'transparent';
    wctx.fillStyle = 'hsl(' + hue + ', 100%, 56%)';
    cuerpoPath();
    wctx.fill();
    wctx.restore();
    // ---- Crestas en Rojo Neón: puntas que superan el techo TRUE PEAK dinámico ----
    if (clipFlash > 0.15) {
      wctx.save();
      wctx.strokeStyle = '#ff003c';
      wctx.shadowColor = 'rgba(255, 0, 60, .9)';
      wctx.shadowBlur = 10;
      wctx.lineWidth = 2;
      wctx.beginPath();
      for (i = 0; i < n; i++) {
        if (wavePicos[i] >= techo) {
          var xr = i * bw + bw / 2;
          var hr = Math.max(2, wavePicos[i] * h);
          wctx.moveTo(xr, mid - hr / 2);
          wctx.lineTo(xr, mid + hr / 2);
        }
      }
      wctx.stroke();
      wctx.restore();
    }
  }

  // ---------------- espectro ----------------
  var specData = new Uint8Array(256);
  var timeData = new Uint8Array(1024);
  var beatGlow = 0, clipFlash = 0, lastBeat = 0;
  // Ciclo RGB suave del cuerpo de la onda: avanza muy lentamente con loopRAF
  var hueOnda = 185;
  function loopRAF() {
    requestAnimationFrame(loopRAF);
    sincTransportUI();
    // Transición fluida de color (cian -> violeta -> naranja -> ...) sin lag
    hueOnda = (hueOnda + 0.6) % 360;
    var w = spec.width, h = spec.height;
    sctx.clearRect(0, 0, w, h);
    sctx.fillStyle = '#0e1222'; sctx.fillRect(0, 0, w, h);
    if (playing && analyser) {
      analyser.getByteFrequencyData(specData);
      // ---- Espectro Azul con Puntas de Alerta Rojas ----
      // Todas las 256 barras se dibujan COMPLETAS en Azul Eléctrico brillante desde
      // la base. ÚNICAMENTE si la banda supera el umbral dinámico de saturación
      // (techo TRUE PEAK o -3 dB), el segmento final de la PUNTA SUPERIOR de esa
      // barra cambia instantáneamente a Rojo Neón (#ff003c) con efecto de brillo.
      var techoF = Math.max(techoTruePeak(), db2ag(-3));
      var wBin = w / specData.length, wBar = Math.max(1, wBin - 2);
      sctx.save();
      // Pasada 1: cuerpo de todas las barras, azul eléctrico brillante desde la base
      sctx.fillStyle = '#00b3ff';
      for (var i = 0; i < specData.length; i++) {
        var v = specData[i] / 255;
        var bh = v * h;
        sctx.fillRect(i * wBin + 1, h, wBar, -bh);
      }
      // Pasada 2: punta roja neón SOLO en la cima de las barras que saturan
      sctx.shadowColor = 'rgba(255, 0, 60, .95)';
      sctx.shadowBlur = 8;
      for (i = 0; i < specData.length; i++) {
        v = specData[i] / 255;
        bh = v * h;
        if (v >= techoF && bh > 0) {
          // El bloque rojo va al PICO final de la barra: altura = exceso sobre el umbral
          var punta = Math.max(4, (v - techoF) * h);
          punta = Math.min(punta, bh);
          sctx.fillStyle = '#ff003c';
          sctx.fillRect(i * wBin + 1, h - punta, wBar, -punta);
          sctx.fillStyle = '#00b3ff';
        }
      }
      sctx.shadowBlur = 0;
      sctx.shadowColor = 'transparent';
      sctx.restore();
      // ---- Beat Glow: golpes graves (bombo) + medios-altos (cajas y drops) ----
      // Analizador con fftSize 1024 => cada bin ≈ sampleRate/1024 ≈ 43 Hz.
      // Graves: bins 1..6 (~43..258 Hz, kick y bombo). x10 en el destello.
      // Medios-altos: bins 7..45 (~300..1935 Hz) capturan cajas, claps y drops.
      var bassE = 0, bassN = 6;
      for (var b = 1; b <= bassN; b++) bassE += specData[b] / 255;
      bassE = bassE / bassN;
      var midE = 0, midN = 0;
      for (var m = 7; m <= 45; m++) { midE += specData[m] / 255; midN++; }
      midE = midN ? midE / midN : 0;
      // Energía combinada de golpe: graves pesan x1.2, medios-altos x0.9
      var golpeE = Math.min(1, bassE * 1.2 + midE * 0.9);
      // Envolvente de golpe: subida instantánea con el impacto y caída suave
      beatGlow = golpeE > beatGlow ? golpeE : Math.max(0, beatGlow * 0.82 - 0.004);
      // ---- Live Clipping: pico real de la señal en tiempo real ----
      try { analyser.getByteTimeDomainData(timeData); } catch (e) {}
      var pico = 0;
      for (var t = 0; t < timeData.length; t++) {
        var dv = Math.abs(timeData[t] - 128) / 128;
        if (dv > pico) pico = dv;
      }
      // Umbral DINÁMICO: enlazado al fader TRUE PEAK (estado.tp), igual que el
      // limitador. Si el pico lo alcanza o lo supera => saturación en vivo.
      var techo = techoTruePeak();
      if (pico >= techo) clipFlash = Math.min(1, clipFlash + 0.35);
      else clipFlash = Math.max(0, clipFlash - 0.02);
      // Refresco la onda: glow del ritmo, alerta de clipping y líneas que se
      // iluminan en cian cuando las crestas tocan el techo.
      dibujarWave(beatGlow, clipFlash, pico >= techo);
    } else {
      var limpiarOnda = wavePicos && (beatGlow > 0.005 || clipFlash > 0.02);
      beatGlow = 0; clipFlash = 0;
      if (limpiarOnda) dibujarWave();
      if (!wavePicos) dibujarWave();
      sctx.fillStyle = '#4f6ea6';
      sctx.font = '12px Segoe UI';
      sctx.fillText('ESPECTRO · reproduce la canción para verlo', 12, 20);
    }
    if (playing) {
      var cxl = escuchandoMaster && masterBuffer ? masterBuffer : audioBuffer;
      var p = (actx.currentTime - startTime) / Math.max(0.001, cxl.duration);
      p = clampF(p, 0, 1);
      sctx.fillStyle = '#ffffff';
      var px = Math.round(p * w);
      sctx.fillRect(px, h - 8, 3, 8);
    }
  }
  loopRAF();

  // ---------------- transporte ----------------
  function marcarAB(useMaster) {
    var a = $('btnA'), b = $('btnB');
    if (a && a.classList) { if (useMaster) a.classList.remove('active'); else a.classList.add('active'); }
    if (b && b.classList) { if (useMaster) b.classList.add('active'); else b.classList.remove('active'); }
  }
  $('btnA').onclick = function () { cambiarAB(false); };
  $('btnB').onclick = function () { cambiarAB(true); };

  // ---- Menú desplegable de descarga (Wav / Mp3) ----
  var ddSave = $('ddSave'), ddMenuWav = $('optWav'), ddMenuMp3 = $('optMp3');
  function sincMenuSave() {
    var bloqueado = !!$('btnSave').disabled;
    ddSave.classList.toggle('off', bloqueado);
    ddMenuWav.disabled = bloqueado;
    ddMenuMp3.disabled = bloqueado;
  }
  $('btnSave').onclick = function (ev) {
    if (this.disabled) return;
    ev.stopPropagation();
    ddSave.classList.toggle('open');
  };
  document.addEventListener('click', function (ev) {
    if (!ddSave.contains(ev.target)) ddSave.classList.remove('open');
  });
  ddMenuWav.onclick = function (ev) {
    ev.stopPropagation();
    ddSave.classList.remove('open');
    guardarWAV();
  };
  ddMenuMp3.onclick = function (ev) {
    ev.stopPropagation();
    ddSave.classList.remove('open');
    guardarMP3();
  };
  sincMenuSave();

  // ---- Transporte de la vista en tiempo real ----
  var seekBar = $('seekBar'), tPos = $('tPos'), tDur = $('tDur'), seekDrag = false;
  function durActual() {
    return (escuchandoMaster && masterBuffer) ? masterBuffer.duration : (audioBuffer ? audioBuffer.duration : 0);
  }
  function posActual() {
    if (playing && source) return actx.currentTime - startTime;
    return offsetPos;
  }
  function sincTransportUI() {
    if (!seekBar || !tPos || !tDur) return;
    var dur = durActual(), pos = posActual();
    if (!seekDrag) seekBar.value = pos;
    seekBar.max = dur || 100;
    tPos.textContent = fmtT(Math.max(0, pos));
    tDur.textContent = fmtT(dur);
    var on = playing;
    $('btnPlayStop').textContent = on ? '■' : '▶';
    $('btnPlayStop').classList.toggle('act', on);
  }
  function maestroSeek(seg) {
    if (!audioBuffer) return;
    var dur = durActual();
    seg = clampF(seg, 0, Math.max(0, dur));
    offsetPos = seg;
    if (playing) {
      pararTodo();
      maestroPlay();
    }
    sincTransportUI();
  }
  $('btnR10').onclick = function () { maestroSeek(posActual() - 10); };
  $('btnF10').onclick = function () { maestroSeek(posActual() + 10); };
  $('btnPause').onclick = function () {
    if (!audioBuffer) return;
    if (playing) maestroPausa(); else maestroPlay();
  };
  $('btnPlayStop').onclick = function () {
    if (!audioBuffer) return;
    if (playing) { maestroPausa(); offsetPos = 0; sincTransportUI(); }
    else { offsetPos = 0; maestroPlay(); }
  };
  seekBar.addEventListener('input', function () {
    seekDrag = true;
    var seg = parseFloat(seekBar.value) || 0;
    tPos.textContent = fmtT(seg);
    if (audioBuffer && playing) maestroSeek(seg);
  });
  seekBar.addEventListener('change', function () {
    seekDrag = false;
    if (audioBuffer) maestroSeek(parseFloat(seekBar.value) || 0);
  });

  function escuchar(useMaster) {
    cambiarAB(!!useMaster);
  }

  function aplicarGananciaAB(instantaneo) {
    if (!ganA || !ganB) return;
    var t = actx.currentTime;
    var gA = escuchandoMaster ? 0 : 1;
    var gB = escuchandoMaster ? 1 : 0;
    ganA.gain.cancelScheduledValues(t);
    ganB.gain.cancelScheduledValues(t);
    if (instantaneo) {
      ganA.gain.value = gA;
      ganB.gain.value = gB;
    } else {
      // Crossfade suave: la fuente activa sube y la inactiva baja sin cortar el audio
      ganA.gain.setTargetAtTime(gA, t, 0.04);
      ganB.gain.setTargetAtTime(gB, t, 0.04);
    }
  }

  // Conmutación A/B en tiempo real: NO detiene ni recrea el source.
  // Las dos fuentes (original y master) corren sincronizadas desde el mismo offset;
  // cambiar de A a B solo cruza las ganancias (bypass interno).
  function cambiarAB(useMaster) {
    if (!audioBuffer) return;
    if (useMaster && !masterBuffer) {
      logIA('Todavía no hay master. Pulsa <b>Masterizar con IA</b>.');
      return;
    }
    if (escuchandoMaster === !!useMaster) {
      marcarAB(escuchandoMaster);
      return;
    }
    escuchandoMaster = !!useMaster;
    marcarAB(escuchandoMaster);
    if (!playing || !source) {
      aplicarGananciaAB(true);
      sincTransportUI();
      return;
    }
    // Si el master se generó en caliente (o la fuente B aún no existe) mientras sonaba A,
    // creamos la fuente B sincronizada en la posición actual sin tocar offsetPos.
    if (escuchandoMaster && !sourceB && masterBuffer) {
      var pos = posActual();
      sourceB = actx.createBufferSource();
      sourceB.buffer = masterBuffer;
      sourceB.onended = function () {
        if (!playing) return;
        playing = false;
        offsetPos = 0;
        sincTransportUI();
      };
      conectarCadenaVivo();
      sourceB.connect(ganB || liveGain || analyser);
      sourceB.start(0, pos);
    }
    aplicarGananciaAB(false);
    sincTransportUI();
  }

  function sincFadersLive() {
    if (!liveGain) return;
    var volGain = db2ag((estado.vol * 2 - 1) * 12);
    var panV = estado.pan * 2 - 1; // -1..1
    var panL = panV <= 0 ? 1 : 1 - panV * 0.5;
    var panR = panV >= 0 ? 1 : 1 + panV * 0.5;
    liveGain.gain.value = volGain;
    if (liveGainL) liveGainL.gain.value = panL;
    if (liveGainR) liveGainR.gain.value = panR;
  }
  function conectarCadenaVivo() {
    if (!analyser) return;
    if (!analyserDestConectado) {
      try { analyser.connect(actx.destination); } catch (e) {}
      analyserDestConectado = true;
    }
    if (!liveGain) {
      if (!actx.createGain || !actx.createChannelSplitter || !actx.createChannelMerger) {
        try { source && source.connect(analyser); } catch (e) {}
        return;
      }
      liveGain = actx.createGain();
      liveSplit = actx.createChannelSplitter(2);
      liveGainL = actx.createGain();
      liveGainR = actx.createGain();
      liveMerge = actx.createChannelMerger(2);
      liveGainL.gain.value = 1; liveGainR.gain.value = 1;
      liveGain.connect(liveSplit);
      liveSplit.connect(liveGainL, 0);
      liveSplit.connect(liveGainR, 1);
      liveGainL.connect(liveMerge, 0, 0);
      liveGainR.connect(liveMerge, 0, 1);
      liveMerge.connect(analyser);
    } else if (liveMerge) {
      try { liveMerge.connect(analyser); } catch (e) {}
    }
    // Nodos de conmutación A/B: original -> ganA, master -> ganB, ambos hacia liveGain
    if (!ganA) { ganA = actx.createGain(); ganA.gain.value = 1; try { ganA.connect(liveGain || analyser); } catch (e) {} }
    if (!ganB) { ganB = actx.createGain(); ganB.gain.value = 0; try { ganB.connect(liveGain || analyser); } catch (e) {} }
    aplicarGananciaAB(true);
    sincFadersLive();
  }

  function maestroPlay() {
    if (!audioBuffer) return;
    asegurar();
    if (actx.state === 'suspended') { actx.resume(); }
    if (!analyser) { analyser = actx.createAnalyser(); analyser.fftSize = 1024; analyser.smoothingTimeConstant = 0.82; }
    marcarAB(escuchandoMaster);
    pararTodo();
    conectarCadenaVivo();
    var pos = offsetPos;
    // Fuente A: original siempre activa
    source = actx.createBufferSource();
    source.buffer = audioBuffer;
    source.onended = function () {
      if (!playing) return;
      playing = false;
      offsetPos = 0;
      sincTransportUI();
    };
    source.connect(ganA || liveGain || analyser);
    source.start(0, pos);
    // Fuente B: master corriendo en paralelo (muda hasta seleccionar B-Master)
    if (masterBuffer) {
      sourceB = actx.createBufferSource();
      sourceB.buffer = masterBuffer;
      sourceB.onended = source.onended;
      sourceB.connect(ganB || liveGain || analyser);
      sourceB.start(0, pos);
    }
    startTime = actx.currentTime - pos;
    playing = true;
    aplicarGananciaAB(true);
    sincTransportUI();
  }
  function maestroPausa() {
    if (source && playing) offsetPos = actx.currentTime - startTime;
    pararTodo();
    sincTransportUI();
  }

  // ========== DSP ==========
  function biquad(type, f, sr, Q, g) {
    var w0 = 2 * Math.PI * f / sr, cs = Math.cos(w0), sn = Math.sin(w0);
    var al = sn / (2 * Q), A, b0, b1, b2, a0, a1, a2;
    if (type === 'hp') {
      b0 = (1 + cs) / 2; b1 = -(1 + cs); b2 = (1 + cs) / 2;
      a0 = 1 + al; a1 = -2 * cs; a2 = 1 - al;
    } else if (type === 'lp') {
      b0 = (1 - cs) / 2; b1 = 1 - cs; b2 = (1 - cs) / 2;
      a0 = 1 + al; a1 = -2 * cs; a2 = 1 - al;
    } else if (type === 'bp') {
      b0 = al; b1 = 0; b2 = -al;
      a0 = 1 + al; a1 = -2 * cs; a2 = 1 - al;
    } else if (type === 'ls') {
      A = Math.pow(10, g / 40);
      b0 = A * ((A + 1) - (A - 1) * cs + 2 * Math.sqrt(A) * al);
      b1 = 2 * A * ((A - 1) - (A + 1) * cs);
      b2 = A * ((A + 1) - (A - 1) * cs - 2 * Math.sqrt(A) * al);
      a0 = (A + 1) + (A - 1) * cs + 2 * Math.sqrt(A) * al;
      a1 = -2 * ((A - 1) + (A + 1) * cs);
      a2 = (A + 1) + (A - 1) * cs - 2 * Math.sqrt(A) * al;
    } else if (type === 'hs') {
      A = Math.pow(10, g / 40);
      b0 = A * ((A + 1) + (A - 1) * cs + 2 * Math.sqrt(A) * al);
      b1 = -2 * A * ((A - 1) + (A + 1) * cs);
      b2 = A * ((A + 1) + (A - 1) * cs - 2 * Math.sqrt(A) * al);
      a0 = (A + 1) - (A - 1) * cs + 2 * Math.sqrt(A) * al;
      a1 = 2 * ((A - 1) - (A + 1) * cs);
      a2 = (A + 1) - (A - 1) * cs - 2 * Math.sqrt(A) * al;
    } else {
      A = Math.pow(10, g / 40);
      b0 = 1 + al * A; b1 = -2 * cs; b2 = 1 - al * A;
      a0 = 1 + al / A; a1 = -2 * cs; a2 = 1 - al / A;
    }
    b0 /= a0; b1 /= a0; b2 /= a0; a1 /= a0; a2 /= a0;
    var z1 = 0, z2 = 0;
    return function (x) {
      var y = b0 * x + z1;
      z1 = b1 * x - a1 * y + z2;
      z2 = b2 * x - a2 * y;
      return y;
    };
  }

  function S(x) { return Math.sqrt(x); }

  function mkVerb(sr, shift) {
    var tun = sr / 44100;
    var cl = [1116, 1188, 1277, 1356, 1422, 1491, 1557, 1617];
    var al = [556, 441, 341, 225];
    var o = shift || 0;
    var combs = [], alls = [];
    for (var i = 0; i < 8; i++) combs.push({ b: new Float32Array(Math.max(2, Math.round((cl[i] + o) * tun))), i: 0, f: 0, d: 0.15, fb: 0.88 });
    for (var j = 0; j < 4; j++) alls.push({ b: new Float32Array(Math.max(2, Math.round((al[j] + o) * tun))), i: 0 });
    return {
      step: function (x) {
        var w = 0;
        for (var c0 = 0; c0 < 8; c0++) {
          var kn = combs[c0];
          var ov = kn.b[kn.i];
          kn.f = ov * (1 - kn.d) + kn.f * kn.d;
          kn.b[kn.i] = x + kn.f * kn.fb;
          kn.i = (kn.i + 1) % kn.b.length;
          w += ov;
        }
        w *= 0.125;
        for (var a0 = 0; a0 < 4; a0++) {
          var ap = alls[a0];
          var ov2 = ap.b[ap.i];
          ap.b[ap.i] = w + ov2 * 0.5;
          w = -w + ov2;
          ap.i = (ap.i + 1) % ap.b.length;
        }
        return w;
      }
    };
  }

  // ================================== MASTER ==================================
  $('btnMaster').onclick = function () {
    if (!audioBuffer || masterizando) return;
    doMaster();
  };

  // ================================================================
  //  MOTOR SEGURO DE MASTERING
  //  Regla profesional: nunca distorsionar. Límites duros de seguridad
  //  ----------------------------------------------------------------
  //  1) EQ quirúrgico: correcciones ≤ ±2 dB (bajo, medio, agudo)
  //  2) Compresor sutil: ratio ≤ 2:1, ataque lento (respeta transitorios)
  //  3) Preservar dinámica: ganancia de normalización limitada
  //  4) Limitador brickwall final: el pico NUNCA supera -1.0 dBTP
  // ================================================================

  function analizarTonalidad(L, R, sr, n) {
    var bLow = biquad('lp', 200, sr, 0.707, 0);
    var bMid = biquad('bp', 1000, sr, 0.5, 0);
    var bHigh = biquad('hp', 3500, sr, 0.707, 0);
    var sl = 0, sm = 0, sh = 0, cnt = 0;
    for (var i = 0; i < n; i += 128) {
      var v = L[i];
      sl += bLow(v) * bLow(v);
      sm += bMid(v) * bMid(v);
      sh += bHigh(v) * bHigh(v);
      cnt++;
    }
    cnt = Math.max(1, cnt);
    var dBL = 10 * Math.log10(sl / cnt + 1e-18);
    var dBM = 10 * Math.log10(sm / cnt + 1e-18);
    var dBH = 10 * Math.log10(sh / cnt + 1e-18);
    return {
      low: clampF(0 - (dBL - dBM), -2, 2),
      high: clampF(-4 - (dBH - dBM), -2, 2)
    };
  }

  // Decisión de AUTO-GAIN inteligente según el nivel de entrada medido.
  // Retorna la ganancia de entrada en dB que el motor debe aplicar internamente:
  //   saturada  -> -2.0 dB (margen para apagar los picos rojos antes de limitar)
  //   normal    ->  0.0 dB (conserva toda la fuerza y brillo)
  //   baja      -> +1..+3 dB (potencia comercial sutil, sin distorsión)
  function decidirAutoGain(lufsIn, pkInDb, lufsSlider) {
    if (pkInDb >= -0.8 || lufsIn >= -8.5) return -2.0;
    if (lufsIn <= -18) {
      var objLUFS = -14 + 4 * lufsSlider;
      return clampF(objLUFS - lufsIn, 0, 3);
    }
    return 0;
  }

  function doMaster() {
    masterizando = true;
    $('btnMaster').disabled = true;
    pbar.style.width = '2%';

    var sr = audioBuffer.sampleRate;
    var n = audioBuffer.length;
    var inL = audioBuffer.getChannelData(0);
    var inR = audioBuffer.numberOfChannels > 1 ? audioBuffer.getChannelData(1) : inL;
    var L = new Float32Array(n), R = new Float32Array(n);
    for (var i = 0; i < n; i++) { L[i] = inL[i]; R[i] = inR[i]; }

    // ================================================================
    //  ANÁLISIS DE ENTRADA INTELIGENTE (AUTO-GAIN)
    //  ----------------------------------------------------------------
    //  Mide el volumen real (LUFS) y el pico de la canción cargada ANTES
    //  de aplicar efectos, y ajusta el volumen de entrada internamente:
    //   - Canción ya SATURADA (pico caliente o LUFS muy alto): baja -2.0 dB
    //     para limpiar los picos rojos antes de limitar (da margen y apaga
    //     las líneas de peligro).
    //   - Canción NORMAL: deja 0.0 dB (conserva toda su fuerza y brillo).
    //   - Canción BAJA: sube sutilmente (+1..+3 dB) para dar la potencia
    //     comercial necesaria sin distorsionar.
    // ================================================================
    var lufsIn = medirLUFS(L, R, sr);
    var pkIn = 0;
    for (var ia = 0; ia < n; ia++) pkIn = Math.max(pkIn, Math.abs(L[ia]), Math.abs(R[ia]));
    var pkInDb = ag2db(Math.max(pkIn, 1e-9));
    var autoGainDb = decidirAutoGain(lufsIn, pkInDb, estado.lufs);
    var autoGain = db2ag(autoGainDb);
    if (Math.abs(autoGain - 1) > 1e-5) {
      for (var ag = 0; ag < n; ag++) { L[ag] *= autoGain; R[ag] *= autoGain; }
    }
    logIA('Análisis de entrada inteligente: ' + lufsIn.toFixed(1) + ' LUFS · pico ' + pkInDb.toFixed(1) + ' dBTP → auto-gain ' + (autoGainDb >= 0 ? '+' : '') + autoGainDb.toFixed(1) + ' dB');


    var cfg = {
      sr: sr,
      eq: estado.eq.slice(),
      deEss: estado.deEss, hpfV: estado.hpfV, mudV: estado.mudV,
      denoise: estado.denoise, subAdd: estado.subAdd, airEx: estado.airEx,
      expDyn: estado.expDyn, roomK: estado.roomK, // ANCHO ESTÉREO mid/side (sin eco)
      midLow: estado.midLow,
      glueK: estado.glueK, lufs: estado.lufs, tp: estado.tp
    };

    logIA('=== Masterización con IA iniciada (motor optimizado para audio de Suno) ===');
    logIA('Análisis FFT y balance tonal (correcciones quirúrgicas ≤ ±2 dB)...');

    // Balance tonal automático (análisis de frecuencias de la canción)
    var auto = analizarTonalidad(L, R, sr, n);
    var fSig = function (v) { return (v > 0 ? '+' : '') + v.toFixed(1) + ' dB'; };
    logIA('Corrección baja: ' + fSig(auto.low) + ' · corrección aire: ' + fSig(auto.high));

    var bm = function (t, f, Q, g) { return biquad(t, f, sr, Q, g); };

    // EQ de usuari@ aplicado con tope de seguridad (±4 dB) - nunca excesivo
    var eqF = [];
    for (var b = 0; b < 10; b++) {
      var g0 = clampF(cfg.eq[b], -4, 4);
      if (Math.abs(g0) < 0.05) { eqF.push(null); continue; }
      var tt = b === 0 ? 'ls' : (b === 9 ? 'hs' : 'pk');
      eqF.push(bm(tt, BANDS[b], b === 0 || b === 9 ? 0.6 : 1.1, g0));
    }
    var fAutoLow = auto.low ? bm('ls', 90, 0.6, auto.low) : null;
    var fAutoHigh = auto.high ? bm('hs', 11000, 0.6, auto.high) : null;

    // HPF de bus SUTIL: solo limpia el retumbar infragrave (20-45Hz),
    // respeta el bombear del bajo/batería (no corta 40-60Hz).
    var fHpf = bm('hp', Math.min(25 + 30 * cfg.hpfV, 80), 0.707, 0);
    // DE-ARTEFACTOS IA: banda 3-8 kHz donde Suno genera sonidos metálicos/robóticos
    var fEss = bm('bp', 5200, 0.9, 0);
    // LIMPIEZA DE CAJA: atenuación dinámica de la zona encajonada (300 Hz)
    var fMud = bm('bp', 300, 1.2, 0);
    var fSub = bm('lp', 120, 0.707, 0);
    var fAir = bm('hs', 12000, 0.6, 2 * cfg.airEx); // máximo +2 dB

    // Limpieza mid-low: suave recorte dinámico de la opacidad 300-600 Hz
    var fMidLow = bm('bp', 420, 1.0, 0);
    // ANCHO ESTÉREO: HPF de 500 Hz para ensanchar solo medios/agudos (mid/side)
    var fWidthHP = bm('hp', 500, 0.707, 0);

    var st = {
      envEssL: 0, envEssR: 0, envMudL: 0, envMudR: 0,
      envMidLowL: 0, envMidLowR: 0,
      subEnvL: 0, subEnvR: 0,
      tsAtkL: 0, tsAtkR: 0, tsRelL: 0, tsRelR: 0,
      envGlue: 0, subPhase: 0,
      envNoiseL: 0, envNoiseR: 0
    };

    // DE-NOISE (Filtro Anti-Lupa): gate/expansor de siseo digital de IA.
    // Cuanto mayor el fader denoise, más alto el umbral y más se atenúa el
    // ruido de fondo SIN tocar los transitorios (el siseo de Suno está
    // acoplado a los picos; la ganancia depende de la envolvente). Se aplica
    // ANTES de la compresión para que el limitador no amplifique el siseo.
    var noiseThr = db2ag(-66 + 20 * cfg.denoise);
    var aNoise = 1 - Math.exp(-1 / (0.006 * sr));
    var relNoise = Math.exp(-1 / (0.04 * sr));
    var aEss = 1 - Math.exp(-1 / (0.01 * sr));

    // Compresor sutil: ataque 20 ms (respeta transitorios), release 140 ms, ratio ≤ 2:1
    var aGlue = 1 - Math.exp(-1 / (0.020 * sr));
    var relGlue = Math.exp(-1 / (0.140 * sr));
    var thrComp = -16;
    var ratioComp = 2;
    var kneeComp = 4;

    var CHUNK = Math.max(16384, Math.floor(sr * 0.1));
    procChunk(0);

    function procChunk(off) {
      var end = Math.min(n, off + CHUNK);
      for (var k = off; k < end; k++) {
        var xl = L[k], xr = R[k];

        // DE-NOISE (expansor de siseo): atenúa el ruido de fondo según la
        // envolvente. El siseo de IA está acoplado a los transitorios, así que
        // la atenuación es proporcional: los picos altos pasan intactos y el
        // piso se limpia sin castigar el material.
        if (cfg.denoise > 0.02) {
          st.envNoiseL += (Math.abs(xl) - st.envNoiseL) * (Math.abs(xl) > st.envNoiseL ? aNoise : relNoise);
          st.envNoiseR += (Math.abs(xr) - st.envNoiseR) * (Math.abs(xr) > st.envNoiseR ? aNoise : relNoise);
          var gL = clampF(st.envNoiseL / noiseThr, 0, 1);
          var gR = clampF(st.envNoiseR / noiseThr, 0, 1);
          xl *= 1 - cfg.denoise * 0.9 * (1 - gL);
          xr *= 1 - cfg.denoise * 0.9 * (1 - gR);
        }

        // EQ usuario + corrección tonal automática
        for (var b0 = 0; b0 < 10; b0++) {
          if (eqF[b0]) { xl = eqF[b0](xl); xr = eqF[b0](xr); }
        }
        if (fAutoLow) { xl = fAutoLow(xl); xr = fAutoLow(xr); }
        if (fAutoHigh) { xl = fAutoHigh(xl); xr = fAutoHigh(xr); }

        // Vocal repair: de-artefactos IA (suaviza metales/robóticos 3-8 kHz)
        var se = fEss(xl);
        st.envEssL += (Math.abs(se) - st.envEssL) * aEss;
        xl -= se * cfg.deEss * clampF((st.envEssL - 0.05) * 3.5, 0, 1);
        xl = fHpf(xl);
        var ml = fMud(xl);
        st.envMudL += (Math.abs(ml) - st.envMudL) * aEss;
        xl -= ml * cfg.mudV * 0.5 * clampF((st.envMudL - 0.08) * 2.5, 0, 1);
        var mlow = fMidLow(xl);
        st.envMidLowL += (Math.abs(mlow) - st.envMidLowL) * aEss;
        xl -= mlow * cfg.midLow * 0.4 * clampF((st.envMidLowL - 0.08) * 2.5, 0, 1);

        var seR = fEss(xr);
        st.envEssR += (Math.abs(seR) - st.envEssR) * aEss;
        xr -= seR * cfg.deEss * clampF((st.envEssR - 0.05) * 3.5, 0, 1);
        xr = fHpf(xr);
        var mr = fMud(xr);
        st.envMudR += (Math.abs(mr) - st.envMudR) * aEss;
        xr -= mr * cfg.mudV * 0.5 * clampF((st.envMudR - 0.08) * 2.5, 0, 1);
        var mlowR = fMidLow(xr);
        st.envMidLowR += (Math.abs(mlowR) - st.envMidLowR) * aEss;
        xr -= mlowR * cfg.midLow * 0.4 * clampF((st.envMidLowR - 0.08) * 2.5, 0, 1);

        // Aire (máx +2 dB)
        xl = fAir(xl); xr = fAir(xr);

        // Sub sutil: sigue la envolvente real del bajo (no añade picos)
        var lo = fSub(xl), loR = fSub(xr);
        st.subEnvL += (Math.abs(lo) - st.subEnvL) * 0.0008;
        st.subEnvR += (Math.abs(loR) - st.subEnvR) * 0.0008;
        st.subPhase += 2 * Math.PI * (45 + 6 * cfg.subAdd) / sr;
        if (st.subPhase > 6.2832) st.subPhase -= 6.2832;
        var subAmt = cfg.subAdd * (st.subEnvL + st.subEnvR) * 0.3;
        xl += Math.sin(st.subPhase) * subAmt * 0.4;
        xr += Math.sin(st.subPhase) * subAmt * 0.4;

        // IMPACTO / DINÁMICA (Transient Shaper): devuelve el golpe a la batería
        // que la IA aplasta. Detecta la envolvente de ataque vs. el sustain y
        // refuerza el pico inicial sin levantar el nivel del cuerpo.
        if (cfg.expDyn > 0) {
          var aTs = 1 - Math.exp(-1 / (0.0012 * sr));  // ataque ~1.2 ms
          var rTs = 1 - Math.exp(-1 / (0.09 * sr));    // release ~90 ms
          st.tsAtkL += (Math.abs(xl) - st.tsAtkL) * aTs;
          st.tsRelL += (Math.abs(xl) - st.tsRelL) * rTs;
          var punchL = clampF(st.tsAtkL - st.tsRelL, 0, 1);
          xl *= 1 + cfg.expDyn * 0.5 * punchL;
          st.tsAtkR += (Math.abs(xr) - st.tsAtkR) * aTs;
          st.tsRelR += (Math.abs(xr) - st.tsRelR) * rTs;
          var punchR = clampF(st.tsAtkR - st.tsRelR, 0, 1);
          xr *= 1 + cfg.expDyn * 0.5 * punchR;
        }

        // ANCHO ESTÉREO (mid/side): ensancha la imagen de medios y agudos
        // sin tocar el bajo. La señal side pasa por un HPF de 500 Hz.
        if (cfg.roomK > 0.01) {
          var sM = (xl + xr) * 0.5;
          var sS = (xl - xr) * 0.5;
          var sSide = fWidthHP(sS);
          var wA = 1 + cfg.roomK * 1.6; // hasta ~2.6x el ancho original
          xl = sM + sSide * wA;
          xr = sM - sSide * wA;
        }

        // Compresor glue sutil (ataque lento, ratio ≤ 2:1, knee suave)
        var mono = (Math.abs(xl) + Math.abs(xr)) * 0.5;
        st.envGlue += (mono > st.envGlue ? (mono - st.envGlue) * aGlue : (mono - st.envGlue) * relGlue);
        var detDb = ag2db(st.envGlue + 1e-9);
        var gr2 = 0;
        if (detDb > thrComp - kneeComp / 2) {
          var overDb = detDb - thrComp;
          var tx = clampF((overDb + kneeComp / 2) / kneeComp, 0, 1);
          var ratioEff = 1 + (ratioComp - 1) * tx;
          gr2 = -Math.max(0, overDb) * (1 - 1 / ratioEff);
        }
        var gm2 = db2ag(gr2);
        xl *= 1 + (gm2 - 1) * (cfg.glueK * 0.55);
        xr *= 1 + (gm2 - 1) * (cfg.glueK * 0.55);

        L[k] = xl; R[k] = xr;
      }
      pbar.style.width = Math.round((end / n) * 92) + '%';
      if (end < n) {
        setTimeout(function () { procChunk(end); }, 0);
      } else {
        finishMaster(L, R, autoGainDb);
      }
    }
  }

  // Limitador brickwall estricto: ningún sample supera el techo dado.
  // Ataque casi instantáneo en los picos, release lento (no "aplasta").
  // La ganancia efectiva NUNCA puede superar la necesaria para no exceder
  // el techo: se recorta con Math.min(need, ...) en cada muestra.
  function aplicarLimiterFinal(LL, RR, ceiling, sr) {
    var rel = Math.exp(-1 / (0.05 * sr));
    var g = 1;
    for (var i = 0; i < LL.length; i++) {
      var pk = Math.max(Math.abs(LL[i]), Math.abs(RR[i]));
      var need = pk > 1e-9 ? ceiling / pk : 1;
      // El límite inferior es need (el máximo que permite el techo). La
      // recuperación lenta solo se aplica si está POR DEBAJO de ese límite.
      var effective = Math.min(need, g + (1 - g) * rel);
      LL[i] *= effective;
      RR[i] *= effective;
      g = effective;
    }
  }

  function finishMaster(LL, RR, autoGainDbEntrada) {
    var sr = audioBuffer.sampleRate;
    var lufs = medirLUFS(LL, RR, sr);

    // Normalización a -14/-10 LUFS con ganancia CONSERVADORA (±3 dB, preserva dinámica)
    var target = -14 + 4 * estado.lufs;
    var gainDb = clampF(target - lufs, -3, 3);
    var gain = db2ag(gainDb);
    for (var i = 0; i < LL.length; i++) { LL[i] *= gain; RR[i] *= gain; }

    // Limitador final estricto: el pico nunca pasa de -1.0 dBTP (o el elegido)
    var ceiling = db2ag(-(1 + 1.5 * estado.tp));
    aplicarLimiterFinal(LL, RR, ceiling, sr);

    // ================================================================
    //  CLAMPING DE SEGURIDAD INFRANQUEABLE
    //  Tras el limitador se verifica muestra a muestra que NINGUNA supere
    //  el valor absoluto del techo TRUE PEAK (ej: -1.0 dB → 0.891). Si
    //  alguna lo hiciera (por redondeo/release del limitador), se recorta
    //  con fuerza estricta: es imposible emitir un pico por encima del techo.
    // ================================================================
    for (i = 0; i < LL.length; i++) {
      var pkS = Math.max(Math.abs(LL[i]), Math.abs(RR[i]));
      if (pkS > ceiling) {
        var scl = ceiling / pkS;
        LL[i] *= scl;
        RR[i] *= scl;
      }
    }

    // Lectura HONESTA del pico real del buffer YA limitado: el reporte debe
    // dar exactamente <= techo (nunca un número positivo).
    var pk = 0;
    for (i = 0; i < LL.length; i++) pk = Math.max(pk, Math.abs(LL[i]), Math.abs(RR[i]));

    var buf = actx.createBuffer(2, LL.length, sr);
    buf.getChannelData(0).set(LL);
    buf.getChannelData(1).set(RR);
    masterBuffer = buf;
    masterizado = true;
    masterizando = false;
    $('btnMaster').disabled = false;
    $('btnB').disabled = false;
    $('btnSave').disabled = false;
    sincMenuSave();

    var lufsFinal = medirLUFS(LL, RR, sr);
    var autoInfo = (autoGainDbEntrada || 0) !== 0 ? ' · auto-gain entrada ' + ((autoGainDbEntrada || 0) >= 0 ? '+' : '') + (autoGainDbEntrada || 0).toFixed(1) + ' dB' : '';
    logIA('<b style="color:#ffaa00">✔ Master completo</b> · ' + lufsFinal.toFixed(1) + ' LUFS · pico real ' + ag2db(Math.max(pk, 1e-9)).toFixed(1) + ' dBTP (techo ' + ag2db(ceiling).toFixed(1) + ' dB): sin clipping, dinámica preservada.' + autoInfo);
    setStats('<span>LUFS: <b>' + lufsFinal.toFixed(1) + '</b></span><span>Ganancia: <b>' + gainDb.toFixed(1) + ' dB</b></span><span>Pico: <b>' + ag2db(Math.max(pk, 1e-9)).toFixed(1) + ' dBTP</b></span><span>Auto-gain: <b>' + ((autoGainDbEntrada || 0) >= 0 ? '+' : '') + (autoGainDbEntrada || 0).toFixed(1) + ' dB</b></span>');
    pbar.style.width = '100%';
    setTimeout(function () { pbar.style.width = '0%'; }, 2000);
    dibujarWave();
    // Coherencia visual: refrescar todo el tablero con el estado que se acaba
    // de usar, para que los faders/EQ reflejen lo masterizado de inmediato.
    sincDashboard();
  }

  function medirLUFS(LL, RR, sr) {
    // Medición corregida a BS.1770: K-weighting (HP 40Hz + shelf +4dB @ 1.5kHz)
    // y gate relativo -10 LU en 2 pasadas. Evita pedir ganancia excesiva.
    var hpL = biquad('hp', 40, sr, 0.7, 0);
    var hpR = biquad('hp', 40, sr, 0.7, 0);
    var shL = biquad('hs', 1500, sr, 0.6, 4);
    var shR = biquad('hs', 1500, sr, 0.6, 4);
    var block = Math.floor(sr * 0.4);
    var pows = [];
    var acc = 0, cnt = 0;
    var n = LL.length;
    for (var i = 0; i < n; i++) {
      var l = shL(hpL(LL[i]));
      var r = shR(hpR(RR[i]));
      acc += l * l + r * r;
      cnt += 2;
      if ((i + 1) % block === 0) {
        pows.push(acc / cnt);
        acc = 0; cnt = 0;
      }
    }
    if (cnt > 0) pows.push(acc / cnt);
    pows = pows.filter(function (p) { return p > 0; });
    if (!pows.length) return -60;

    function gate(pw) {
      var mx = 0;
      for (var m = 0; m < pw.length; m++) if (pw[m] > mx) mx = pw[m];
      if (!mx) return [];
      return pw.filter(function (px) { return px >= mx * Math.pow(10, -1); });
    }
    var keep = gate(gate(pows));
    var mean = 0;
    for (var q = 0; q < keep.length; q++) mean += keep[q];
    mean /= keep.length || 1;
    return -0.691 + 10 * Math.log10(mean);
  }

  // ========== WAV ==========
  function buferConFaders(buf) {
    var ch = Math.min(buf.numberOfChannels, 2);
    var srB = buf.sampleRate;
    var LL = buf.getChannelData(0);
    var RR = buf.numberOfChannels > 1 ? buf.getChannelData(1) : buf.getChannelData(0);
    var volGain = db2ag((estado.vol * 2 - 1) * 12);
    var panV = estado.pan * 2 - 1;
    var panL = panV <= 0 ? 1 : 1 - panV * 0.5;
    var panR = panV >= 0 ? 1 : 1 + panV * 0.5;
    var oL = new Float32Array(buf.length), oR = new Float32Array(buf.length);
    for (var vi = 0; vi < buf.length; vi++) { oL[vi] = LL[vi] * volGain * panL; oR[vi] = RR[vi] * volGain * panR; }
    var ceiling = db2ag(-(1 + 1.5 * estado.tp));
    aplicarLimiterFinal(oL, oR, ceiling, srB);
    var out = actx.createBuffer(ch, buf.length, srB);
    out.getChannelData(0).set(oL);
    if (ch > 1) out.getChannelData(1).set(oR);
    return out;
  }

  function descargarBlob(blob, nombre) {
    var a = document.createElement('a');
    a.href = URL.createObjectURL(blob);
    a.download = nombre;
    a.click();
    setTimeout(function () { URL.revokeObjectURL(a.href); }, 3000);
  }

  function guardarWAV() {
    if (!masterBuffer) { logIA('Todavía no hay master que descargar. Pulsa <b>Masterizar con IA</b> primero.'); return; }
    var srPos = masterBuffer.sampleRate, chPos = Math.min(masterBuffer.numberOfChannels, 2);
    var bufFader = buferConFaders(masterBuffer);
    var wv = buildWAV(bufFader, srPos, chPos);
    descargarBlob(new Blob([wv], { type: 'audio/wav' }), 'master_ia.wav');
    logIA('<b style="color:#ffaa00">✔ Master exportado como WAV</b> · sin pérdida (PCM 16-bit) · techo -1.0 dBTP.');
  }

  function floatAInt16(f32) {
    var out = new Int16Array(f32.length);
    for (var i = 0; i < f32.length; i++) {
      var s = clampF(f32[i], -1, 1);
      out[i] = s < 0 ? Math.round(s * 32768) : Math.round(s * 32767);
    }
    return out;
  }

  function guardarMP3() {
    if (!masterBuffer) { logIA('Todavía no hay master que descargar. Pulsa <b>Masterizar con IA</b> primero.'); return; }
    if (!window.lamejs || !window.lamejs.Mp3Encoder) { logIA('El codificador MP3 (libmp3lame) no está disponible.'); return; }
    var srPos = masterBuffer.sampleRate, chPos = Math.min(masterBuffer.numberOfChannels, 2);
    var bufFader = buferConFaders(masterBuffer);
    var L = floatAInt16(bufFader.getChannelData(0));
    var R = chPos > 1 ? floatAInt16(bufFader.getChannelData(1)) : null;
    var enc = new window.lamejs.Mp3Encoder(chPos, srPos, 320);
    var bloque = 1152, n = L.length, i = 0, mp3 = [];
    while (i < n) {
      var finBloque = Math.min(i + bloque, n);
      var data;
      if (chPos > 1) {
        var lChunk = new Int16Array(bloque), rChunk = new Int16Array(bloque);
        lChunk.set(L.subarray(i, finBloque));
        rChunk.set(R.subarray(i, finBloque));
        data = enc.encodeBuffer(lChunk, rChunk);
      } else {
        var mChunk = new Int16Array(bloque);
        mChunk.set(L.subarray(i, finBloque));
        data = enc.encodeBuffer(mChunk);
      }
      if (data.length) mp3.push(new Int8Array(data));
      i = finBloque;
    }
    var finalFrame = enc.flush();
    if (finalFrame.length) mp3.push(new Int8Array(finalFrame));
    descargarBlob(new Blob(mp3, { type: 'audio/mpeg' }), 'master_ia.mp3');
    logIA('<b style="color:#ffaa00">✔ Master exportado como MP3</b> · 320 kbps · techo -1.0 dBTP.');
  }

  function buildWAV(buf, sr, ch) {
    var n = buf.length;
    var dataLen = n * 2 * ch;
    var ab = new ArrayBuffer(44 + dataLen);
    var dv = new DataView(ab);
    function txtc(o, str) { for (var i = 0; i < str.length; i++) dv.setUint8(o + i, str.charCodeAt(i)); }
    txtc(0, 'RIFF'); dv.setUint32(4, 36 + dataLen, true); txtc(8, 'WAVE');
    txtc(12, 'fmt '); dv.setUint32(16, 16, true); dv.setUint16(20, 1, true);
    dv.setUint16(22, ch, true); dv.setUint32(24, sr, true);
    dv.setUint32(28, sr * ch * 2, true); dv.setUint16(32, ch * 2, true); dv.setUint16(34, 16, true);
    txtc(36, 'data'); dv.setUint32(40, dataLen, true);
    var o = 44;
    for (var i = 0; i < n; i++) {
      for (var c = 0; c < ch; c++) {
        var v = clampF(buf.getChannelData(c)[i], -1, 1);
        dv.setInt16(o, v < 0 ? v * 32768 : v * 32767, true);
        o += 2;
      }
    }
    return ab;
  }
})();
</script>
</body>
</html>