Spaces:
Running
Running
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 & 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">☁</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, '<') + '</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> |