diff --git a/assets/audio/dotty-floor-english.json b/assets/audio/dotty-floor-english.json new file mode 100644 index 0000000..f1186be --- /dev/null +++ b/assets/audio/dotty-floor-english.json @@ -0,0 +1,348 @@ +{ + "1": [0, 654], + "2": [654, 498], + "3": [1152, 560], + "4": [1712, 544], + "5": [2256, 564], + "6": [2820, 663], + "7": [3483, 591], + "8": [4074, 545], + "9": [4619, 560], + "10": [5179, 524], + "11": [5703, 645], + "12": [6348, 581], + "13": [6929, 847], + "14": [7776, 733], + "15": [8509, 873], + "16": [9382, 1020], + "17": [10402, 964], + "18": [11366, 847], + "19": [12213, 930], + "20": [13143, 729], + "21": [13872, 964], + "22": [14836, 950], + "23": [15786, 914], + "24": [16700, 901], + "25": [17601, 986], + "26": [18587, 981], + "27": [19568, 959], + "28": [20527, 923], + "29": [21450, 1047], + "30": [22497, 688], + "31": [23185, 970], + "32": [24155, 858], + "33": [25013, 1050], + "34": [26063, 942], + "35": [27005, 924], + "36": [27929, 972], + "37": [28901, 977], + "38": [29878, 844], + "39": [30722, 918], + "40": [31640, 667], + "41": [32307, 845], + "42": [33152, 928], + "43": [34080, 936], + "44": [35016, 964], + "45": [35980, 992], + "46": [36972, 964], + "47": [37936, 882], + "48": [38818, 863], + "49": [39681, 919], + "50": [40600, 678], + "51": [41278, 912], + "52": [42190, 870], + "53": [43060, 940], + "54": [44000, 883], + "55": [44883, 950], + "56": [45833, 1000], + "57": [46833, 992], + "58": [47825, 906], + "59": [48731, 954], + "60": [49685, 795], + "61": [50480, 993], + "62": [51473, 982], + "63": [52455, 989], + "64": [53444, 1003], + "65": [54447, 1009], + "66": [55456, 1119], + "67": [56575, 1076], + "68": [57651, 1133], + "69": [58784, 1077], + "70": [59861, 874], + "71": [60735, 990], + "72": [61725, 973], + "73": [62698, 1099], + "74": [63797, 1101], + "75": [64898, 1015], + "76": [65913, 1095], + "77": [67008, 1112], + "78": [68120, 1064], + "79": [69184, 1070], + "80": [70254, 661], + "81": [70915, 787], + "82": [71702, 800], + "83": [72502, 930], + "84": [73432, 938], + "85": [74370, 943], + "86": [75313, 957], + "87": [76270, 859], + "88": [77129, 834], + "89": [77963, 876], + "90": [78839, 790], + "91": [79629, 942], + "92": [80571, 973], + "93": [81544, 1017], + "94": [82561, 1019], + "95": [83580, 1003], + "96": [84583, 1113], + "97": [85696, 949], + "98": [86645, 1010], + "99": [87655, 1068], + "100": [88723, 917], + "101": [89640, 1371], + "102": [91011, 1295], + "103": [92306, 1382], + "104": [93688, 1273], + "105": [94961, 1360], + "106": [96321, 1368], + "107": [97689, 1289], + "108": [98978, 1297], + "109": [100275, 1353], + "110": [101628, 1256], + "111": [102884, 1437], + "112": [104321, 1484], + "113": [105805, 1453], + "114": [107258, 1501], + "115": [108759, 1595], + "116": [110354, 1534], + "117": [111888, 1680], + "118": [113568, 1566], + "119": [115134, 1506], + "120": [116640, 1373], + "121": [118013, 1704], + "122": [119717, 1711], + "123": [121428, 1685], + "124": [123113, 1741], + "125": [124854, 1909], + "126": [126763, 1750], + "127": [128513, 1877], + "128": [130390, 1839], + "129": [132229, 1758], + "130": [133987, 1704], + "131": [135691, 1787], + "132": [137478, 1721], + "133": [139199, 1793], + "134": [140992, 1829], + "135": [142821, 1846], + "136": [144667, 1805], + "137": [146472, 1728], + "138": [148200, 1817], + "139": [150017, 1879], + "140": [151896, 1468], + "141": [153364, 1624], + "142": [154988, 1754], + "143": [156742, 1711], + "144": [158453, 1845], + "145": [160298, 1754], + "146": [162052, 1832], + "147": [163884, 1746], + "148": [165630, 1761], + "149": [167391, 1745], + "150": [169136, 1497], + "151": [170633, 1664], + "152": [172297, 1704], + "153": [174001, 1825], + "154": [175826, 1813], + "155": [177639, 1818], + "156": [179457, 1902], + "157": [181359, 1780], + "158": [183139, 1738], + "159": [184877, 1793], + "160": [186670, 1667], + "161": [188337, 1683], + "162": [190020, 1769], + "164": [191789, 1745], + "165": [193534, 1822], + "167": [195356, 1816], + "168": [197172, 1768], + "170": [198940, 1529], + "171": [200469, 1709], + "174": [202178, 1723], + "177": [203901, 1830], + "180": [205731, 1454], + "require_1": [207185, 1659], + "require_2": [208844, 1408], + "require_3": [210252, 1474], + "require_4": [211726, 1549], + "require_5": [213274, 1569], + "require_6": [214843, 1549], + "require_7": [216392, 1505], + "require_8": [217897, 1533], + "require_9": [219430, 1474], + "require_10": [220904, 1444], + "require_11": [222348, 1633], + "require_12": [223981, 1404], + "require_13": [225385, 1761], + "require_14": [227145, 1652], + "require_15": [228797, 1861], + "require_16": [230658, 2008], + "require_17": [232666, 1883], + "require_18": [234549, 1757], + "require_19": [236306, 1844], + "require_20": [238150, 1717], + "require_21": [239867, 1952], + "require_22": [241818, 1870], + "require_23": [243688, 1990], + "require_24": [245678, 1815], + "require_25": [247493, 1991], + "require_26": [249484, 1900], + "require_27": [251384, 1799], + "require_28": [253183, 1746], + "require_29": [254929, 1933], + "require_30": [256861, 1602], + "require_31": [258463, 1975], + "require_32": [260438, 1698], + "require_33": [262136, 2055], + "require_34": [264191, 1862], + "require_35": [266053, 1747], + "require_36": [267800, 1882], + "require_37": [269682, 1896], + "require_38": [271577, 1754], + "require_39": [273331, 1923], + "require_40": [275254, 1672], + "require_41": [276926, 1921], + "require_42": [278847, 1847], + "require_43": [280694, 1924], + "require_44": [282618, 1969], + "require_45": [284587, 2068], + "require_46": [286655, 2040], + "require_47": [288695, 1705], + "require_48": [290400, 1686], + "require_49": [292085, 1838], + "require_50": [293923, 1598], + "require_51": [295521, 1735], + "require_52": [297256, 1710], + "require_53": [298966, 1945], + "require_54": [300911, 1797], + "require_55": [302708, 2026], + "require_56": [304734, 2005], + "require_57": [306739, 1906], + "require_58": [308644, 1894], + "require_59": [310538, 1959], + "require_60": [312497, 1618], + "require_61": [314115, 1903], + "require_62": [316018, 2058], + "require_63": [318076, 1899], + "require_64": [319975, 1923], + "require_65": [321898, 1832], + "require_66": [323729, 2124], + "require_67": [325853, 1962], + "require_68": [327815, 2043], + "require_69": [329858, 1987], + "require_70": [331845, 1784], + "require_71": [333629, 1876], + "require_72": [335505, 2049], + "require_73": [337554, 1939], + "require_74": [339492, 2106], + "require_75": [341598, 1855], + "require_76": [343453, 2171], + "require_77": [345624, 2022], + "require_78": [347646, 1904], + "require_79": [349550, 1980], + "require_80": [351530, 1575], + "require_81": [353105, 1697], + "require_82": [354801, 1805], + "require_83": [356606, 1850], + "require_84": [358456, 1858], + "require_85": [360314, 1862], + "require_86": [362176, 1843], + "require_87": [364019, 1935], + "require_88": [365954, 1720], + "require_89": [367674, 1790], + "require_90": [369463, 1709], + "require_91": [371172, 1765], + "require_92": [372937, 1813], + "require_93": [374750, 1927], + "require_94": [376677, 1939], + "require_95": [378616, 2079], + "require_96": [380695, 2023], + "require_97": [382718, 1835], + "require_98": [384552, 1998], + "require_99": [386550, 1982], + "require_100": [388532, 1922], + "require_101": [390454, 2447], + "require_102": [392901, 2283], + "require_103": [395184, 2302], + "require_104": [397486, 2159], + "require_105": [399645, 2436], + "require_106": [402080, 2208], + "require_107": [404288, 2203], + "require_108": [406491, 2285], + "require_109": [408776, 2272], + "require_110": [411048, 2079], + "require_111": [413127, 2351], + "require_112": [415478, 2489], + "require_113": [417967, 2373], + "require_114": [420339, 2324], + "require_115": [422663, 2418], + "require_116": [425081, 2420], + "require_117": [427501, 2520], + "require_118": [430021, 2554], + "require_119": [432575, 2511], + "require_120": [435086, 2361], + "require_121": [437447, 2614], + "require_122": [440060, 2631], + "require_123": [442691, 2571], + "require_124": [445262, 2564], + "require_125": [447826, 2823], + "require_126": [450649, 2738], + "require_127": [453387, 2882], + "require_128": [456269, 2662], + "require_129": [458931, 2763], + "require_130": [461693, 2692], + "require_131": [464385, 2673], + "require_132": [467058, 2561], + "require_133": [469619, 2798], + "require_134": [472417, 2743], + "require_135": [475160, 2922], + "require_136": [478082, 2881], + "require_137": [480963, 2638], + "require_138": [483600, 2822], + "require_139": [486422, 2702], + "require_140": [489124, 2456], + "require_141": [491580, 2544], + "require_142": [494124, 2664], + "require_143": [496788, 2625], + "require_144": [499413, 2833], + "require_145": [502246, 2668], + "require_146": [504914, 2655], + "require_147": [507568, 2666], + "require_148": [510234, 2601], + "require_149": [512835, 2750], + "require_150": [515585, 2485], + "require_151": [518070, 2652], + "require_152": [520722, 2590], + "require_153": [523312, 2830], + "require_154": [526142, 2733], + "require_155": [528874, 2641], + "require_156": [531515, 2890], + "require_157": [534405, 2699], + "require_158": [537104, 2578], + "require_159": [539682, 2869], + "require_160": [542551, 2581], + "require_161": [545132, 2602], + "require_162": [547734, 2689], + "require_164": [550423, 2585], + "require_165": [553008, 2732], + "require_167": [555739, 2821], + "require_168": [558560, 2756], + "require_170": [561316, 2415], + "require_171": [563731, 2785], + "require_174": [566516, 2633], + "require_177": [569149, 2835], + "require_180": [571984, 2442], + "game_on": [574426, 904], + "game_shot": [575330, 887], + "game_shot_match": [576217, 1724], + "0": [577941, 903] +} diff --git a/assets/audio/dotty-floor-english.mp3 b/assets/audio/dotty-floor-english.mp3 new file mode 100644 index 0000000..5f3403d Binary files /dev/null and b/assets/audio/dotty-floor-english.mp3 differ diff --git a/assets/audio/dotty-floor-german.json b/assets/audio/dotty-floor-german.json new file mode 100644 index 0000000..2f39937 --- /dev/null +++ b/assets/audio/dotty-floor-german.json @@ -0,0 +1 @@ +{"1": [0, 698], "2": [698, 577], "3": [1275, 598], "4": [1873, 459], "5": [2332, 515], "6": [2847, 648], "7": [3495, 696], "8": [4191, 626], "9": [4817, 514], "10": [5331, 585], "11": [5916, 524], "12": [6440, 750], "13": [7190, 837], "14": [8027, 708], "15": [8735, 864], "16": [9599, 850], "17": [10449, 1008], "18": [11457, 789], "19": [12246, 879], "20": [13125, 846], "21": [13971, 1204], "22": [15175, 1086], "23": [16261, 1263], "24": [17524, 1049], "25": [18573, 1176], "26": [19749, 1190], "27": [20939, 1190], "28": [22129, 1159], "29": [23288, 1225], "30": [24513, 722], "31": [25235, 999], "32": [26234, 1026], "33": [27260, 1089], "34": [28349, 1032], "35": [29381, 1101], "36": [30482, 1247], "37": [31729, 1273], "38": [33002, 1082], "39": [34084, 1032], "40": [35116, 728], "41": [35844, 1017], "42": [36861, 1092], "43": [37953, 1070], "44": [39023, 981], "45": [40004, 1094], "46": [41098, 1108], "47": [42206, 1170], "48": [43376, 1097], "49": [44473, 1116], "50": [45589, 802], "51": [46391, 1037], "52": [47428, 1122], "53": [48550, 1090], "54": [49640, 1023], "55": [50663, 1160], "56": [51823, 1282], "57": [53105, 1259], "58": [54364, 1307], "59": [55671, 1223], "60": [56894, 903], "61": [57797, 1078], "62": [58875, 1158], "63": [60033, 1069], "64": [61102, 1035], "65": [62137, 1063], "66": [63200, 1180], "67": [64380, 1323], "68": [65703, 1300], "69": [67003, 1186], "70": [68189, 837], "71": [69026, 1092], "72": [70118, 1206], "73": [71324, 1156], "74": [72480, 1053], "75": [73533, 1065], "76": [74598, 1233], "77": [75831, 1208], "78": [77039, 1142], "79": [78181, 1095], "80": [79276, 848], "81": [80124, 1172], "82": [81296, 1208], "83": [82504, 1124], "84": [83628, 1081], "85": [84709, 1170], "86": [85879, 1119], "87": [86998, 1222], "88": [88220, 1101], "89": [89321, 1087], "90": [90408, 771], "91": [91179, 1012], "92": [92191, 1013], "93": [93204, 1026], "94": [94230, 1001], "95": [95231, 1039], "96": [96270, 1172], "97": [97442, 1182], "98": [98624, 1092], "99": [99716, 1090], "100": [100806, 919], "101": [101725, 1198], "102": [102923, 1095], "103": [104018, 1047], "104": [105065, 1074], "105": [106139, 1051], "106": [107190, 1226], "107": [108416, 1159], "108": [109575, 1021], "109": [110596, 1118], "110": [111714, 1059], "111": [112773, 1058], "112": [113831, 1126], "113": [114957, 1207], "114": [116164, 1143], "115": [117307, 1305], "116": [118612, 1165], "117": [119777, 1185], "118": [120962, 1220], "119": [122182, 1234], "120": [123416, 1323], "121": [124739, 1482], "122": [126221, 1559], "123": [127780, 1484], "124": [129264, 1511], "125": [130775, 1634], "126": [132409, 1604], "127": [134013, 1628], "128": [135641, 1629], "129": [137270, 1565], "130": [138835, 1257], "131": [140092, 1506], "132": [141598, 1565], "133": [143163, 1487], "134": [144650, 1492], "135": [146142, 1538], "136": [147680, 1561], "137": [149241, 1756], "138": [150997, 1530], "139": [152527, 1499], "140": [154026, 1417], "141": [155443, 1417], "142": [156860, 1532], "143": [158392, 1582], "144": [159974, 1471], "145": [161445, 1520], "146": [162965, 1581], "147": [164546, 1539], "148": [166085, 1480], "149": [167565, 1527], "150": [169092, 1459], "151": [170551, 1549], "152": [172100, 1674], "153": [173774, 1550], "154": [175324, 1578], "155": [176902, 1480], "156": [178382, 1530], "157": [179912, 1667], "158": [181579, 1737], "159": [183316, 1619], "160": [184935, 1494], "161": [186429, 1537], "162": [187966, 1584], "164": [189550, 1580], "165": [191130, 1607], "167": [192737, 1675], "168": [194412, 1548], "170": [195960, 1516], "171": [197476, 1693], "174": [199169, 1626], "177": [200795, 1909], "180": [202704, 1392], "require_1": [204096, 1604], "require_2": [205700, 1477], "require_3": [207177, 1627], "require_4": [208804, 1365], "require_5": [210168, 1421], "require_6": [211589, 1473], "require_7": [213062, 1725], "require_8": [214787, 1535], "require_9": [216322, 1430], "require_10": [217752, 1614], "require_11": [219366, 1579], "require_12": [220945, 1659], "require_13": [222604, 1684], "require_14": [224287, 1624], "require_15": [225911, 1893], "require_16": [227804, 1692], "require_17": [229496, 1917], "require_18": [231413, 1705], "require_19": [233118, 1788], "require_20": [234906, 1688], "require_21": [236594, 2104], "require_22": [238697, 2002], "require_23": [240699, 2292], "require_24": [242991, 1958], "require_25": [244949, 2085], "require_26": [247034, 2245], "require_27": [249279, 2106], "require_28": [251385, 2060], "require_29": [253445, 2254], "require_30": [255698, 1628], "require_31": [257326, 1841], "require_32": [259167, 1909], "require_33": [261076, 1936], "require_34": [263012, 1857], "require_35": [264869, 2130], "require_36": [266999, 2163], "require_37": [269162, 2179], "require_38": [271340, 1965], "require_39": [273305, 1938], "require_40": [275243, 1783], "require_41": [277026, 1864], "require_42": [278890, 1992], "require_43": [280882, 1912], "require_44": [282794, 1897], "require_45": [284691, 1994], "require_46": [286685, 2014], "require_47": [288698, 2076], "require_48": [290774, 1998], "require_49": [292772, 1958], "require_50": [294730, 1711], "require_51": [296441, 1953], "require_52": [298394, 2028], "require_53": [300422, 1991], "require_54": [302413, 1939], "require_55": [304351, 2061], "require_56": [306412, 2198], "require_57": [308610, 2106], "require_58": [310716, 2154], "require_59": [312870, 2065], "require_60": [314935, 1958], "require_61": [316893, 1925], "require_62": [318818, 2041], "require_63": [320859, 1975], "require_64": [322834, 2064], "require_65": [324897, 1964], "require_66": [326861, 2086], "require_67": [328947, 2352], "require_68": [331299, 2209], "require_69": [333508, 2092], "require_70": [335600, 1684], "require_71": [337284, 1992], "require_72": [339276, 2107], "require_73": [341383, 2056], "require_74": [343438, 1969], "require_75": [345407, 2120], "require_76": [347527, 2080], "require_77": [349607, 2114], "require_78": [351721, 1967], "require_79": [353688, 1995], "require_80": [355683, 1748], "require_81": [357431, 2072], "require_82": [359502, 2033], "require_83": [361535, 2040], "require_84": [363575, 1982], "require_85": [365557, 2053], "require_86": [367610, 2025], "require_87": [369635, 2105], "require_88": [371740, 2002], "require_89": [373742, 1987], "require_90": [375728, 1654], "require_91": [377382, 1912], "require_92": [379294, 1929], "require_93": [381223, 2055], "require_94": [383278, 1901], "require_95": [385179, 1945], "require_96": [387124, 2088], "require_97": [389212, 2237], "require_98": [391449, 2147], "require_99": [393595, 1932], "require_100": [395527, 1744], "require_101": [397271, 2099], "require_102": [399370, 1920], "require_103": [401290, 2076], "require_104": [403366, 1916], "require_105": [405282, 1898], "require_106": [407180, 2109], "require_107": [409288, 2059], "require_108": [411347, 2076], "require_109": [413423, 2019], "require_110": [415442, 1959], "require_111": [417401, 1883], "require_112": [419284, 2035], "require_113": [421319, 2236], "require_114": [423555, 2049], "require_115": [425603, 2206], "require_116": [427809, 2074], "require_117": [429883, 2240], "require_118": [432123, 2045], "require_119": [434168, 2135], "require_120": [436303, 2239], "require_121": [438542, 2365], "require_122": [440907, 2588], "require_123": [443494, 2393], "require_124": [445887, 2353], "require_125": [448240, 2481], "require_126": [450721, 2633], "require_127": [453354, 2544], "require_128": [455898, 2545], "require_129": [458443, 2481], "require_130": [460924, 2163], "require_131": [463087, 2561], "require_132": [465648, 2412], "require_133": [468059, 2334], "require_134": [470393, 2317], "require_135": [472710, 2421], "require_136": [475131, 2470], "require_137": [477601, 2662], "require_138": [480263, 2439], "require_139": [482702, 2399], "require_140": [485101, 2472], "require_141": [487572, 2242], "require_142": [489814, 2379], "require_143": [492193, 2611], "require_144": [494804, 2380], "require_145": [497184, 2426], "require_146": [499610, 2428], "require_147": [502038, 2445], "require_148": [504483, 2389], "require_149": [506871, 2352], "require_150": [509223, 2342], "require_151": [511565, 2455], "require_152": [514020, 2590], "require_153": [516610, 2579], "require_154": [519189, 2479], "require_155": [521668, 2381], "require_156": [524049, 2430], "require_157": [526478, 2573], "require_158": [529051, 2584], "require_159": [531635, 2535], "require_160": [534170, 2403], "require_161": [536573, 2592], "require_162": [539165, 2484], "require_164": [541649, 2609], "require_165": [544258, 2516], "require_167": [546773, 2704], "require_168": [549477, 2395], "require_170": [551872, 2571], "require_171": [554443, 2576], "require_174": [557019, 2532], "require_177": [559551, 2818], "require_180": [562369, 2308], "game_on": [564677, 904], "game_shot": [565581, 887], "game_shot_match": [566468, 1803], "0": [568271, 949]} \ No newline at end of file diff --git a/assets/audio/dotty-floor-german.mp3 b/assets/audio/dotty-floor-german.mp3 new file mode 100644 index 0000000..a726955 Binary files /dev/null and b/assets/audio/dotty-floor-german.mp3 differ diff --git a/assets/css/src/style.scss b/assets/css/src/style.scss index 34dd50d..4fce115 100644 --- a/assets/css/src/style.scss +++ b/assets/css/src/style.scss @@ -427,6 +427,13 @@ body.xoi{ margin-bottom: 1.3em; } } + .sidebyside{ + display: flex; + & > * { + flex-grow: 1; + flex-basis: 1; + } + } } .playerselect{ & > h2{ @@ -447,7 +454,12 @@ body.xoi{ "navi navi navi navi navi" ; background-color: black; - + &.Throw{ + box-shadow: 0px 0px 2rem 2rem green; + } + &.Takeout{ + box-shadow: 0px 0px 2rem 2rem orange; + } .bigToGo{ margin: $line; background-color: white; diff --git a/assets/css/style.min.css b/assets/css/style.min.css index bc7ee9e..449b6dc 100644 --- a/assets/css/style.min.css +++ b/assets/css/style.min.css @@ -1 +1 @@ -html,body,div,span,applet,object,iframe,h1,h2,h3,h4,h5,h6,p,blockquote,pre,a,abbr,acronym,address,big,cite,code,del,dfn,em,img,ins,kbd,q,s,samp,small,strike,strong,sub,sup,tt,var,b,u,i,center,dl,dt,dd,ol,ul,li,fieldset,form,label,legend,table,caption,tbody,tfoot,thead,tr,th,td,article,aside,canvas,details,embed,figure,figcaption,footer,header,hgroup,menu,nav,output,ruby,section,summary,time,mark,audio,video{margin:0;padding:0;border:0;font-size:100%;font:inherit;vertical-align:baseline}article,aside,details,figcaption,figure,footer,header,hgroup,menu,nav,section{display:block}ol,ul{list-style:none}blockquote,q{quotes:none}blockquote:before,blockquote:after,q:before,q:after{content:"";content:none}table{border-collapse:collapse;border-spacing:0}@font-face{font-display:swap;font-family:"Open Sans";font-style:normal;font-weight:300;src:url("../fonts/opensans/open-sans-v40-latin-300.woff2") format("woff2")}@font-face{font-display:swap;font-family:"Open Sans";font-style:italic;font-weight:300;src:url("../fonts/opensans/open-sans-v40-latin-300italic.woff2") format("woff2")}@font-face{font-display:swap;font-family:"Open Sans";font-style:normal;font-weight:400;src:url("../fonts/opensans/open-sans-v40-latin-regular.woff2") format("woff2")}@font-face{font-display:swap;font-family:"Open Sans";font-style:italic;font-weight:400;src:url("../fonts/opensans/open-sans-v40-latin-italic.woff2") format("woff2")}@font-face{font-display:swap;font-family:"Open Sans";font-style:normal;font-weight:500;src:url("../fonts/opensans/open-sans-v40-latin-500.woff2") format("woff2")}@font-face{font-display:swap;font-family:"Open Sans";font-style:italic;font-weight:500;src:url("../fonts/opensans/open-sans-v40-latin-500italic.woff2") format("woff2")}@font-face{font-display:swap;font-family:"Open Sans";font-style:normal;font-weight:600;src:url("../fonts/opensans/open-sans-v40-latin-600.woff2") format("woff2")}@font-face{font-display:swap;font-family:"Open Sans";font-style:italic;font-weight:600;src:url("../fonts/opensans/open-sans-v40-latin-600italic.woff2") format("woff2")}@font-face{font-display:swap;font-family:"Open Sans";font-style:normal;font-weight:700;src:url("../fonts/opensans/open-sans-v40-latin-700.woff2") format("woff2")}@font-face{font-display:swap;font-family:"Open Sans";font-style:italic;font-weight:700;src:url("../fonts/opensans/open-sans-v40-latin-700italic.woff2") format("woff2")}@font-face{font-display:swap;font-family:"Open Sans";font-style:normal;font-weight:800;src:url("../fonts/opensans/open-sans-v40-latin-800.woff2") format("woff2")}@font-face{font-display:swap;font-family:"Open Sans";font-style:italic;font-weight:800;src:url("../fonts/opensans/open-sans-v40-latin-800italic.woff2") format("woff2")}*{box-sizing:border-box;margin:0;padding:0}:root{font-size:min(1.111111111vw,1.481481481vh)}body{margin:0;font-family:"Open Sans";background-color:#000;display:flex;justify-content:center;align-items:center;height:100vh;color:#fff;display:flex;flex-direction:column;overflow:hidden}input{font-family:"Open Sans"}main{border-left:.2rem solid #000;border-right:.2rem solid #000;height:67.5rem;width:90rem;background-color:#2f6f2a;display:flex;flex-direction:column}h1,h2,h3,h4,h5,h6{font-weight:bold}.game .body{height:48rem}.center{display:flex;justify-content:center}.hidden{display:none !important}.label{font-size:1.8em;padding:.3em 0;font-weight:bold;display:block}.overlay{border-radius:.5rem;border:.3rem #000 solid;position:absolute;top:50%;left:50%;z-index:1000;transform:translate(-50%, -50%);width:45rem;max-height:60rem;overflow-y:auto;background-color:#2f6f2a;padding:2em 1.7em}.overlay::-webkit-scrollbar{width:0}.dialog{flex-grow:1;display:flex;flex-direction:column;justify-content:center;align-items:center;width:auto}.dialog h2{font-size:2.4em;margin-bottom:.5em;text-shadow:.2rem .2rem .4rem #000}.dialog p{font-size:1.4em;margin-bottom:1em;text-shadow:.2rem .2rem .4rem #000}nav.list{display:flex;justify-content:center;flex-grow:1;width:100%}nav.list.horizontal{flex-direction:row;justify-content:space-evenly}nav.list.horizontal>*+*{margin-left:1em}nav.list.vertical{flex-direction:column}nav.list.vertical>*+*{margin-top:1em}.element{border-radius:.5rem;border:.3rem #000 solid;border-width:.3rem;background-color:#214d1d;cursor:pointer;box-shadow:.25rem .25rem 1rem rgba(0,0,0,.3)}.element:focus,.element:hover{outline:none;border-color:orange}.element.input{font-size:2em;padding:.3em;background-color:#fff;color:#000;display:block;width:100%}.element.player{display:flex;justify-content:flex-start;align-items:center;padding:.7em;height:4.5em}.element.player h2{font-size:1.3em}.element.player>*{margin-right:1em}.element.player img{height:3em;aspect-ratio:1/1;object-position:top;object-fit:cover}.element.scorer{color:#fff;font-weight:bold;padding:.3em .5em;text-align:center;display:flex;align-items:flex-end;text-shadow:.2rem .2rem .4rem #000;font-family:"Open Sans";display:flex;justify-content:flex-start;align-items:center;padding:.7em;height:4.5em}.element.scorer .player{display:flex;justify-content:flex-start;align-items:center;min-width:0;flex-basis:0;flex-grow:1;text-align:left}.element.scorer .player h2{font-size:1.3em}.element.scorer .player>*{margin-right:1em}.element.scorer .player img{height:3em;aspect-ratio:1/1;object-position:top;object-fit:cover}.element.plain{color:#fff;font-weight:bold;padding:.3em .5em;text-align:center;display:flex;align-items:flex-end;text-shadow:.2rem .2rem .4rem #000;font-family:"Open Sans";padding:1rem 2rem;justify-content:center;align-items:center;font-size:1.6em}.element.plain:hover,.element.plain:focus{background-color:rgba(0,0,0,.2);border-color:orange;outline:none}.element.plain.back{background-color:#5e716a}.element.plain.new{background-color:#193a16}.element.square{font-size:1rem;width:20em;height:20em;padding:2.5em 1.25em;display:flex;flex-direction:column;justify-content:center;align-items:center;background-color:#214d1d;box-shadow:.25em .25em 1em rgba(0,0,0,.3);cursor:pointer}.element.square h2{color:#fff;font-weight:bold;padding:.3em .5em;text-align:center;display:flex;align-items:flex-end;text-shadow:.2rem .2rem .4rem #000;font-family:"Open Sans";font-size:2em;padding:.6em 0 0 0}.element.square:hover,.element.square:focus{background-color:rgba(0,0,0,.2);border-color:orange;outline:none}.element.square .icon{width:65%;height:auto}.element.square .icon svg{width:100%;aspect-ratio:1/1;object-fit:cover;fill:#fff;filter:drop-shadow(0.2rem 0.2rem 0.4rem #000)}.element.square .icon img{width:100%;aspect-ratio:1/1;object-fit:cover;object-position:top;filter:drop-shadow(0.2rem 0.2rem 0.4rem #000)}body.home header{display:flex;justify-content:center;align-items:center;height:42%}body.home header>img{display:block;width:50%;height:auto}body.home h1{text-align:center;color:#fff;font-size:2.6rem;text-shadow:.2rem .2rem .4rem #000}body.home .menu{flex-grow:1;display:flex;align-items:center;padding-bottom:7.5rem}body.home .menu .mainmenu{width:60%;margin:0 auto;display:flex;align-items:center;flex-direction:column;justify-content:center}body.home .menu .mainmenu h1{margin-bottom:1em}body.xoi main{display:flex;justify-content:center}body.xoi main .gamesetup{width:65%;margin:0 auto}body.xoi main .gamesetup h1{font-size:3em;text-align:center;margin-bottom:.5em}body.xoi main .gamesetup .menu>*{margin-bottom:1.3em}body.xoi main .playerselect>h2{font-size:1.8em;margin-bottom:.7em;text-align:center}body.xoi main .xoi{display:grid;height:100%;grid-template-columns:3fr 3fr 2fr 3fr 3fr;grid-template-rows:11.5rem 52rem 4rem;grid-template-areas:"scorer1 toGo1 score toGo2 scorer2" "scorer1 game game game scorer2" "navi navi navi navi navi";background-color:#000}body.xoi main .xoi .bigToGo{margin:.2rem;background-color:#fff;border:.7rem solid #000;color:#000;font-weight:bold;font-size:7.5em;display:flex;justify-content:center;align-items:center}body.xoi main .xoi .bigToGo.active{border-color:orange}body.xoi main .xoi .bigToGo.one{grid-area:toGo1}body.xoi main .xoi .bigToGo.two{grid-area:toGo2}body.xoi main .xoi .score{margin:.2rem 0;grid-area:score;color:#fff;text-shadow:.1rem .1rem .2rem rgba(0,0,0,.8);background-color:#2f6f2a;display:flex;flex-direction:column;justify-content:space-evenly}body.xoi main .xoi .score>div{display:flex;justify-content:space-evenly;align-items:center}body.xoi main .xoi .score>div h2,body.xoi main .xoi .score>div h3{margin:0;text-align:center;font-size:1.3rem}body.xoi main .xoi .score>div h3{font-size:1.1rem}body.xoi main .xoi .score>div>h2{font-size:3.2rem}body.xoi main .xoi .score>div h2:nth-child(2){order:10}body.xoi main .xoi .player{background-color:#2f6f2a;margin-bottom:.1rem}body.xoi main .xoi .player.team .images{position:relative;left:0;top:0;width:100%;padding-bottom:128.5714286%;background-color:#737373}body.xoi main .xoi .player.team .images img{transform:translate(24%, 0) scale(0.42);position:absolute;left:0;top:0;transition:transform .3s linear,box-shadow .3s linear,z-index .3s linear;z-index:10;box-shadow:.6rem .6rem .6rem rgba(0,0,0,.4)}body.xoi main .xoi .player.team .images img:nth-child(1){transform:translate(-24%, 0) scale(0.42)}body.xoi main .xoi .player.team .images img:nth-child(1).inactive{transform:translate(-25%, 25%) scale(0.35)}body.xoi main .xoi .player.team .images img.inactive{transform:translate(25%, 25%) scale(0.35)}body.xoi main .xoi .player.team .images img.active{box-shadow:0rem 0rem 0rem #000;transform:scale(1);z-index:1}body.xoi main .xoi .player.team .titles{display:grid}body.xoi main .xoi .player.team .titles>*{grid-column:1;grid-row:1}body.xoi main .xoi .player.team .titles .title{display:flex;flex-direction:column;justify-content:center}body.xoi main .xoi .player.team .titles .title h2,body.xoi main .xoi .player.team .titles .title h3{opacity:0}body.xoi main .xoi .player.team .titles .title.active h2,body.xoi main .xoi .player.team .titles .title.active h3{transition:opacity .3s linear;opacity:1}body.xoi main .xoi .player.scorer1{grid-area:scorer1}body.xoi main .xoi .player.scorer2{grid-area:scorer2}body.xoi main .xoi .player img{width:100%;aspect-ratio:35/45;object-fit:cover}body.xoi main .xoi .player h2,body.xoi main .xoi .player h3{text-align:center;margin:.2em;text-shadow:.1rem .1rem .2rem rgba(0,0,0,.8)}body.xoi main .xoi .player h2{font-size:1.8em}body.xoi main .xoi .player h3{font-size:1.2em}body.xoi main .xoi .player .stats{display:grid;grid-template-columns:auto 1fr 1fr 1fr;grid-auto-rows:auto;margin:1em .6em;text-shadow:.1rem .1rem .2rem rgba(0,0,0,.8)}body.xoi main .xoi .player .stats .row{display:contents;font-size:1.4em}body.xoi main .xoi .player .stats .row.header{font-weight:bold}body.xoi main .xoi .player .stats .row :nth-child(2),body.xoi main .xoi .player .stats .row :nth-child(3),body.xoi main .xoi .player .stats .row :nth-child(4){text-align:center}body.xoi main .xoi .game{grid-area:game;display:grid;grid-template-columns:1fr;grid-template-rows:4.72727272rem 47.272727273rem}body.xoi main .xoi .game>div{display:grid;grid-template-columns:4fr 4fr 2fr 4fr 4fr;grid-auto-rows:4.72727272rem;overflow-y:scroll;grid-auto-flow:dense;height:100%}body.xoi main .xoi .game>div::-webkit-scrollbar{width:0}body.xoi main .xoi .game>div>div{font-size:2rem;text-align:center;background:#fff;color:#000;margin:0 0 .2rem .2rem;border:.15em rgba(0,0,0,0) solid;display:flex;justify-content:center;align-items:center}body.xoi main .xoi .game>div>div.headding{background-color:#2f6f2a;color:#fff;text-shadow:.1rem .1rem .2rem rgba(0,0,0,.8)}body.xoi main .xoi .game>div>div.rounds{grid-column:3;background-color:#2f6f2a;color:#fff;text-shadow:.1rem .1rem .2rem rgba(0,0,0,.8)}body.xoi main .xoi .game>div>div.scorer1.points{grid-column:1}body.xoi main .xoi .game>div>div.scorer1.toGo{grid-column:2;font-size:1.6rem}body.xoi main .xoi .game>div>div.scorer2.points{grid-column:4}body.xoi main .xoi .game>div>div.scorer2.toGo{grid-column:5;margin-right:.2rem;font-size:1.6rem}body.xoi main .xoi .game>div>div.input{padding:0;border:.15em orange solid}body.xoi main .xoi .game>div>div.input input{text-align:center;box-sizing:border-box;font-family:inherit;padding:.1em;border:none;width:100%;height:100%;font-size:1em}body.xoi main .xoi .game>div>div.input input:focus{outline:none}body.xoi main .xoi .game>div>div.headding.rounds{font-size:1.4rem}body.xoi main .xoi .nav{grid-area:navi;background-color:#2f6f2a;margin-top:.1rem;display:flex;justify-content:space-evenly;align-items:center}body.xoi main .xoi .nav span{font-size:2em} \ No newline at end of file +html,body,div,span,applet,object,iframe,h1,h2,h3,h4,h5,h6,p,blockquote,pre,a,abbr,acronym,address,big,cite,code,del,dfn,em,img,ins,kbd,q,s,samp,small,strike,strong,sub,sup,tt,var,b,u,i,center,dl,dt,dd,ol,ul,li,fieldset,form,label,legend,table,caption,tbody,tfoot,thead,tr,th,td,article,aside,canvas,details,embed,figure,figcaption,footer,header,hgroup,menu,nav,output,ruby,section,summary,time,mark,audio,video{margin:0;padding:0;border:0;font-size:100%;font:inherit;vertical-align:baseline}article,aside,details,figcaption,figure,footer,header,hgroup,menu,nav,section{display:block}ol,ul{list-style:none}blockquote,q{quotes:none}blockquote:before,blockquote:after,q:before,q:after{content:"";content:none}table{border-collapse:collapse;border-spacing:0}@font-face{font-display:swap;font-family:"Open Sans";font-style:normal;font-weight:300;src:url("../fonts/opensans/open-sans-v40-latin-300.woff2") format("woff2")}@font-face{font-display:swap;font-family:"Open Sans";font-style:italic;font-weight:300;src:url("../fonts/opensans/open-sans-v40-latin-300italic.woff2") format("woff2")}@font-face{font-display:swap;font-family:"Open Sans";font-style:normal;font-weight:400;src:url("../fonts/opensans/open-sans-v40-latin-regular.woff2") format("woff2")}@font-face{font-display:swap;font-family:"Open Sans";font-style:italic;font-weight:400;src:url("../fonts/opensans/open-sans-v40-latin-italic.woff2") format("woff2")}@font-face{font-display:swap;font-family:"Open Sans";font-style:normal;font-weight:500;src:url("../fonts/opensans/open-sans-v40-latin-500.woff2") format("woff2")}@font-face{font-display:swap;font-family:"Open Sans";font-style:italic;font-weight:500;src:url("../fonts/opensans/open-sans-v40-latin-500italic.woff2") format("woff2")}@font-face{font-display:swap;font-family:"Open Sans";font-style:normal;font-weight:600;src:url("../fonts/opensans/open-sans-v40-latin-600.woff2") format("woff2")}@font-face{font-display:swap;font-family:"Open Sans";font-style:italic;font-weight:600;src:url("../fonts/opensans/open-sans-v40-latin-600italic.woff2") format("woff2")}@font-face{font-display:swap;font-family:"Open Sans";font-style:normal;font-weight:700;src:url("../fonts/opensans/open-sans-v40-latin-700.woff2") format("woff2")}@font-face{font-display:swap;font-family:"Open Sans";font-style:italic;font-weight:700;src:url("../fonts/opensans/open-sans-v40-latin-700italic.woff2") format("woff2")}@font-face{font-display:swap;font-family:"Open Sans";font-style:normal;font-weight:800;src:url("../fonts/opensans/open-sans-v40-latin-800.woff2") format("woff2")}@font-face{font-display:swap;font-family:"Open Sans";font-style:italic;font-weight:800;src:url("../fonts/opensans/open-sans-v40-latin-800italic.woff2") format("woff2")}*{box-sizing:border-box;margin:0;padding:0}:root{font-size:min(1.111111111vw,1.481481481vh)}body{margin:0;font-family:"Open Sans";background-color:#000;display:flex;justify-content:center;align-items:center;height:100vh;color:#fff;display:flex;flex-direction:column;overflow:hidden}input{font-family:"Open Sans"}main{border-left:.2rem solid #000;border-right:.2rem solid #000;height:67.5rem;width:90rem;background-color:#2f6f2a;display:flex;flex-direction:column}h1,h2,h3,h4,h5,h6{font-weight:bold}.game .body{height:48rem}.center{display:flex;justify-content:center}.hidden{display:none !important}.label{font-size:1.8em;padding:.3em 0;font-weight:bold;display:block}.overlay{border-radius:.5rem;border:.3rem #000 solid;position:absolute;top:50%;left:50%;z-index:1000;transform:translate(-50%, -50%);width:45rem;max-height:60rem;overflow-y:auto;background-color:#2f6f2a;padding:2em 1.7em}.overlay::-webkit-scrollbar{width:0}.dialog{flex-grow:1;display:flex;flex-direction:column;justify-content:center;align-items:center;width:auto}.dialog h2{font-size:2.4em;margin-bottom:.5em;text-shadow:.2rem .2rem .4rem #000}.dialog p{font-size:1.4em;margin-bottom:1em;text-shadow:.2rem .2rem .4rem #000}nav.list{display:flex;justify-content:center;flex-grow:1;width:100%}nav.list.horizontal{flex-direction:row;justify-content:space-evenly}nav.list.horizontal>*+*{margin-left:1em}nav.list.vertical{flex-direction:column}nav.list.vertical>*+*{margin-top:1em}.element{border-radius:.5rem;border:.3rem #000 solid;border-width:.3rem;background-color:#214d1d;cursor:pointer;box-shadow:.25rem .25rem 1rem rgba(0,0,0,.3)}.element:focus,.element:hover{outline:none;border-color:orange}.element.input{font-size:2em;padding:.3em;background-color:#fff;color:#000;display:block;width:100%}.element.player{display:flex;justify-content:flex-start;align-items:center;padding:.7em;height:4.5em}.element.player h2{font-size:1.3em}.element.player>*{margin-right:1em}.element.player img{height:3em;aspect-ratio:1/1;object-position:top;object-fit:cover}.element.scorer{color:#fff;font-weight:bold;padding:.3em .5em;text-align:center;display:flex;align-items:flex-end;text-shadow:.2rem .2rem .4rem #000;font-family:"Open Sans";display:flex;justify-content:flex-start;align-items:center;padding:.7em;height:4.5em}.element.scorer .player{display:flex;justify-content:flex-start;align-items:center;min-width:0;flex-basis:0;flex-grow:1;text-align:left}.element.scorer .player h2{font-size:1.3em}.element.scorer .player>*{margin-right:1em}.element.scorer .player img{height:3em;aspect-ratio:1/1;object-position:top;object-fit:cover}.element.plain{color:#fff;font-weight:bold;padding:.3em .5em;text-align:center;display:flex;align-items:flex-end;text-shadow:.2rem .2rem .4rem #000;font-family:"Open Sans";padding:1rem 2rem;justify-content:center;align-items:center;font-size:1.6em}.element.plain:hover,.element.plain:focus{background-color:rgba(0,0,0,.2);border-color:orange;outline:none}.element.plain.back{background-color:#5e716a}.element.plain.new{background-color:#193a16}.element.square{font-size:1rem;width:20em;height:20em;padding:2.5em 1.25em;display:flex;flex-direction:column;justify-content:center;align-items:center;background-color:#214d1d;box-shadow:.25em .25em 1em rgba(0,0,0,.3);cursor:pointer}.element.square h2{color:#fff;font-weight:bold;padding:.3em .5em;text-align:center;display:flex;align-items:flex-end;text-shadow:.2rem .2rem .4rem #000;font-family:"Open Sans";font-size:2em;padding:.6em 0 0 0}.element.square:hover,.element.square:focus{background-color:rgba(0,0,0,.2);border-color:orange;outline:none}.element.square .icon{width:65%;height:auto}.element.square .icon svg{width:100%;aspect-ratio:1/1;object-fit:cover;fill:#fff;filter:drop-shadow(0.2rem 0.2rem 0.4rem #000)}.element.square .icon img{width:100%;aspect-ratio:1/1;object-fit:cover;object-position:top;filter:drop-shadow(0.2rem 0.2rem 0.4rem #000)}body.home header{display:flex;justify-content:center;align-items:center;height:42%}body.home header>img{display:block;width:50%;height:auto}body.home h1{text-align:center;color:#fff;font-size:2.6rem;text-shadow:.2rem .2rem .4rem #000}body.home .menu{flex-grow:1;display:flex;align-items:center;padding-bottom:7.5rem}body.home .menu .mainmenu{width:60%;margin:0 auto;display:flex;align-items:center;flex-direction:column;justify-content:center}body.home .menu .mainmenu h1{margin-bottom:1em}body.xoi main{display:flex;justify-content:center}body.xoi main .gamesetup{width:65%;margin:0 auto}body.xoi main .gamesetup h1{font-size:3em;text-align:center;margin-bottom:.5em}body.xoi main .gamesetup .menu>*{margin-bottom:1.3em}body.xoi main .gamesetup .sidebyside{display:flex}body.xoi main .gamesetup .sidebyside>*{flex-grow:1;flex-basis:1}body.xoi main .playerselect>h2{font-size:1.8em;margin-bottom:.7em;text-align:center}body.xoi main .xoi{display:grid;height:100%;grid-template-columns:3fr 3fr 2fr 3fr 3fr;grid-template-rows:11.5rem 52rem 4rem;grid-template-areas:"scorer1 toGo1 score toGo2 scorer2" "scorer1 game game game scorer2" "navi navi navi navi navi";background-color:#000}body.xoi main .xoi.Throw{box-shadow:0px 0px 2rem 2rem green}body.xoi main .xoi.Takeout{box-shadow:0px 0px 2rem 2rem orange}body.xoi main .xoi .bigToGo{margin:.2rem;background-color:#fff;border:.7rem solid #000;color:#000;font-weight:bold;font-size:7.5em;display:flex;justify-content:center;align-items:center}body.xoi main .xoi .bigToGo.active{border-color:orange}body.xoi main .xoi .bigToGo.one{grid-area:toGo1}body.xoi main .xoi .bigToGo.two{grid-area:toGo2}body.xoi main .xoi .score{margin:.2rem 0;grid-area:score;color:#fff;text-shadow:.1rem .1rem .2rem rgba(0,0,0,.8);background-color:#2f6f2a;display:flex;flex-direction:column;justify-content:space-evenly}body.xoi main .xoi .score>div{display:flex;justify-content:space-evenly;align-items:center}body.xoi main .xoi .score>div h2,body.xoi main .xoi .score>div h3{margin:0;text-align:center;font-size:1.3rem}body.xoi main .xoi .score>div h3{font-size:1.1rem}body.xoi main .xoi .score>div>h2{font-size:3.2rem}body.xoi main .xoi .score>div h2:nth-child(2){order:10}body.xoi main .xoi .player{background-color:#2f6f2a;margin-bottom:.1rem}body.xoi main .xoi .player.team .images{position:relative;left:0;top:0;width:100%;padding-bottom:128.5714286%;background-color:#737373}body.xoi main .xoi .player.team .images img{transform:translate(24%, 0) scale(0.42);position:absolute;left:0;top:0;transition:transform .3s linear,box-shadow .3s linear,z-index .3s linear;z-index:10;box-shadow:.6rem .6rem .6rem rgba(0,0,0,.4)}body.xoi main .xoi .player.team .images img:nth-child(1){transform:translate(-24%, 0) scale(0.42)}body.xoi main .xoi .player.team .images img:nth-child(1).inactive{transform:translate(-25%, 25%) scale(0.35)}body.xoi main .xoi .player.team .images img.inactive{transform:translate(25%, 25%) scale(0.35)}body.xoi main .xoi .player.team .images img.active{box-shadow:0rem 0rem 0rem #000;transform:scale(1);z-index:1}body.xoi main .xoi .player.team .titles{display:grid}body.xoi main .xoi .player.team .titles>*{grid-column:1;grid-row:1}body.xoi main .xoi .player.team .titles .title{display:flex;flex-direction:column;justify-content:center}body.xoi main .xoi .player.team .titles .title h2,body.xoi main .xoi .player.team .titles .title h3{opacity:0}body.xoi main .xoi .player.team .titles .title.active h2,body.xoi main .xoi .player.team .titles .title.active h3{transition:opacity .3s linear;opacity:1}body.xoi main .xoi .player.scorer1{grid-area:scorer1}body.xoi main .xoi .player.scorer2{grid-area:scorer2}body.xoi main .xoi .player img{width:100%;aspect-ratio:35/45;object-fit:cover}body.xoi main .xoi .player h2,body.xoi main .xoi .player h3{text-align:center;margin:.2em;text-shadow:.1rem .1rem .2rem rgba(0,0,0,.8)}body.xoi main .xoi .player h2{font-size:1.8em}body.xoi main .xoi .player h3{font-size:1.2em}body.xoi main .xoi .player .stats{display:grid;grid-template-columns:auto 1fr 1fr 1fr;grid-auto-rows:auto;margin:1em .6em;text-shadow:.1rem .1rem .2rem rgba(0,0,0,.8)}body.xoi main .xoi .player .stats .row{display:contents;font-size:1.4em}body.xoi main .xoi .player .stats .row.header{font-weight:bold}body.xoi main .xoi .player .stats .row :nth-child(2),body.xoi main .xoi .player .stats .row :nth-child(3),body.xoi main .xoi .player .stats .row :nth-child(4){text-align:center}body.xoi main .xoi .game{grid-area:game;display:grid;grid-template-columns:1fr;grid-template-rows:4.72727272rem 47.272727273rem}body.xoi main .xoi .game>div{display:grid;grid-template-columns:4fr 4fr 2fr 4fr 4fr;grid-auto-rows:4.72727272rem;overflow-y:scroll;grid-auto-flow:dense;height:100%}body.xoi main .xoi .game>div::-webkit-scrollbar{width:0}body.xoi main .xoi .game>div>div{font-size:2rem;text-align:center;background:#fff;color:#000;margin:0 0 .2rem .2rem;border:.15em rgba(0,0,0,0) solid;display:flex;justify-content:center;align-items:center}body.xoi main .xoi .game>div>div.headding{background-color:#2f6f2a;color:#fff;text-shadow:.1rem .1rem .2rem rgba(0,0,0,.8)}body.xoi main .xoi .game>div>div.rounds{grid-column:3;background-color:#2f6f2a;color:#fff;text-shadow:.1rem .1rem .2rem rgba(0,0,0,.8)}body.xoi main .xoi .game>div>div.scorer1.points{grid-column:1}body.xoi main .xoi .game>div>div.scorer1.toGo{grid-column:2;font-size:1.6rem}body.xoi main .xoi .game>div>div.scorer2.points{grid-column:4}body.xoi main .xoi .game>div>div.scorer2.toGo{grid-column:5;margin-right:.2rem;font-size:1.6rem}body.xoi main .xoi .game>div>div.input{padding:0;border:.15em orange solid}body.xoi main .xoi .game>div>div.input input{text-align:center;box-sizing:border-box;font-family:inherit;padding:.1em;border:none;width:100%;height:100%;font-size:1em}body.xoi main .xoi .game>div>div.input input:focus{outline:none}body.xoi main .xoi .game>div>div.headding.rounds{font-size:1.4rem}body.xoi main .xoi .nav{grid-area:navi;background-color:#2f6f2a;margin-top:.1rem;display:flex;justify-content:space-evenly;align-items:center}body.xoi main .xoi .nav span{font-size:2em} \ No newline at end of file diff --git a/assets/js/howler/howler.core.min.js b/assets/js/howler/howler.core.min.js new file mode 100644 index 0000000..3901ac4 --- /dev/null +++ b/assets/js/howler/howler.core.min.js @@ -0,0 +1,2 @@ +/*! howler.js v2.2.4 | (c) 2013-2020, James Simpson of GoldFire Studios | MIT License | howlerjs.com */ +!function(){"use strict";var e=function(){this.init()};e.prototype={init:function(){var e=this||n;return e._counter=1e3,e._html5AudioPool=[],e.html5PoolSize=10,e._codecs={},e._howls=[],e._muted=!1,e._volume=1,e._canPlayEvent="canplaythrough",e._navigator="undefined"!=typeof window&&window.navigator?window.navigator:null,e.masterGain=null,e.noAudio=!1,e.usingWebAudio=!0,e.autoSuspend=!0,e.ctx=null,e.autoUnlock=!0,e._setup(),e},volume:function(e){var o=this||n;if(e=parseFloat(e),o.ctx||_(),void 0!==e&&e>=0&&e<=1){if(o._volume=e,o._muted)return o;o.usingWebAudio&&o.masterGain.gain.setValueAtTime(e,n.ctx.currentTime);for(var t=0;t=0;o--)e._howls[o].unload();return e.usingWebAudio&&e.ctx&&void 0!==e.ctx.close&&(e.ctx.close(),e.ctx=null,_()),e},codecs:function(e){return(this||n)._codecs[e.replace(/^x-/,"")]},_setup:function(){var e=this||n;if(e.state=e.ctx?e.ctx.state||"suspended":"suspended",e._autoSuspend(),!e.usingWebAudio)if("undefined"!=typeof Audio)try{var o=new Audio;void 0===o.oncanplaythrough&&(e._canPlayEvent="canplay")}catch(n){e.noAudio=!0}else e.noAudio=!0;try{var o=new Audio;o.muted&&(e.noAudio=!0)}catch(e){}return e.noAudio||e._setupCodecs(),e},_setupCodecs:function(){var e=this||n,o=null;try{o="undefined"!=typeof Audio?new Audio:null}catch(n){return e}if(!o||"function"!=typeof o.canPlayType)return e;var t=o.canPlayType("audio/mpeg;").replace(/^no$/,""),r=e._navigator?e._navigator.userAgent:"",a=r.match(/OPR\/(\d+)/g),u=a&&parseInt(a[0].split("/")[1],10)<33,d=-1!==r.indexOf("Safari")&&-1===r.indexOf("Chrome"),i=r.match(/Version\/(.*?) /),_=d&&i&&parseInt(i[1],10)<15;return e._codecs={mp3:!(u||!t&&!o.canPlayType("audio/mp3;").replace(/^no$/,"")),mpeg:!!t,opus:!!o.canPlayType('audio/ogg; codecs="opus"').replace(/^no$/,""),ogg:!!o.canPlayType('audio/ogg; codecs="vorbis"').replace(/^no$/,""),oga:!!o.canPlayType('audio/ogg; codecs="vorbis"').replace(/^no$/,""),wav:!!(o.canPlayType('audio/wav; codecs="1"')||o.canPlayType("audio/wav")).replace(/^no$/,""),aac:!!o.canPlayType("audio/aac;").replace(/^no$/,""),caf:!!o.canPlayType("audio/x-caf;").replace(/^no$/,""),m4a:!!(o.canPlayType("audio/x-m4a;")||o.canPlayType("audio/m4a;")||o.canPlayType("audio/aac;")).replace(/^no$/,""),m4b:!!(o.canPlayType("audio/x-m4b;")||o.canPlayType("audio/m4b;")||o.canPlayType("audio/aac;")).replace(/^no$/,""),mp4:!!(o.canPlayType("audio/x-mp4;")||o.canPlayType("audio/mp4;")||o.canPlayType("audio/aac;")).replace(/^no$/,""),weba:!(_||!o.canPlayType('audio/webm; codecs="vorbis"').replace(/^no$/,"")),webm:!(_||!o.canPlayType('audio/webm; codecs="vorbis"').replace(/^no$/,"")),dolby:!!o.canPlayType('audio/mp4; codecs="ec-3"').replace(/^no$/,""),flac:!!(o.canPlayType("audio/x-flac;")||o.canPlayType("audio/flac;")).replace(/^no$/,"")},e},_unlockAudio:function(){var e=this||n;if(!e._audioUnlocked&&e.ctx){e._audioUnlocked=!1,e.autoUnlock=!1,e._mobileUnloaded||44100===e.ctx.sampleRate||(e._mobileUnloaded=!0,e.unload()),e._scratchBuffer=e.ctx.createBuffer(1,1,22050);var o=function(n){for(;e._html5AudioPool.length0?d._seek:t._sprite[e][0]/1e3),s=Math.max(0,(t._sprite[e][0]+t._sprite[e][1])/1e3-_),l=1e3*s/Math.abs(d._rate),c=t._sprite[e][0]/1e3,f=(t._sprite[e][0]+t._sprite[e][1])/1e3;d._sprite=e,d._ended=!1;var p=function(){d._paused=!1,d._seek=_,d._start=c,d._stop=f,d._loop=!(!d._loop&&!t._sprite[e][2])};if(_>=f)return void t._ended(d);var m=d._node;if(t._webAudio){var v=function(){t._playLock=!1,p(),t._refreshBuffer(d);var e=d._muted||t._muted?0:d._volume;m.gain.setValueAtTime(e,n.ctx.currentTime),d._playStart=n.ctx.currentTime,void 0===m.bufferSource.start?d._loop?m.bufferSource.noteGrainOn(0,_,86400):m.bufferSource.noteGrainOn(0,_,s):d._loop?m.bufferSource.start(0,_,86400):m.bufferSource.start(0,_,s),l!==1/0&&(t._endTimers[d._id]=setTimeout(t._ended.bind(t,d),l)),o||setTimeout(function(){t._emit("play",d._id),t._loadQueue()},0)};"running"===n.state&&"interrupted"!==n.ctx.state?v():(t._playLock=!0,t.once("resume",v),t._clearTimer(d._id))}else{var h=function(){m.currentTime=_,m.muted=d._muted||t._muted||n._muted||m.muted,m.volume=d._volume*n.volume(),m.playbackRate=d._rate;try{var r=m.play();if(r&&"undefined"!=typeof Promise&&(r instanceof Promise||"function"==typeof r.then)?(t._playLock=!0,p(),r.then(function(){t._playLock=!1,m._unlocked=!0,o?t._loadQueue():t._emit("play",d._id)}).catch(function(){t._playLock=!1,t._emit("playerror",d._id,"Playback was unable to start. This is most commonly an issue on mobile devices and Chrome where playback was not within a user interaction."),d._ended=!0,d._paused=!0})):o||(t._playLock=!1,p(),t._emit("play",d._id)),m.playbackRate=d._rate,m.paused)return void t._emit("playerror",d._id,"Playback was unable to start. This is most commonly an issue on mobile devices and Chrome where playback was not within a user interaction.");"__default"!==e||d._loop?t._endTimers[d._id]=setTimeout(t._ended.bind(t,d),l):(t._endTimers[d._id]=function(){t._ended(d),m.removeEventListener("ended",t._endTimers[d._id],!1)},m.addEventListener("ended",t._endTimers[d._id],!1))}catch(e){t._emit("playerror",d._id,e)}};"data:audio/wav;base64,UklGRigAAABXQVZFZm10IBIAAAABAAEARKwAAIhYAQACABAAAABkYXRhAgAAAAEA"===m.src&&(m.src=t._src,m.load());var y=window&&window.ejecta||!m.readyState&&n._navigator.isCocoonJS;if(m.readyState>=3||y)h();else{t._playLock=!0,t._state="loading";var g=function(){t._state="loaded",h(),m.removeEventListener(n._canPlayEvent,g,!1)};m.addEventListener(n._canPlayEvent,g,!1),t._clearTimer(d._id)}}return d._id},pause:function(e){var n=this;if("loaded"!==n._state||n._playLock)return n._queue.push({event:"pause",action:function(){n.pause(e)}}),n;for(var o=n._getSoundIds(e),t=0;t=0?o=parseInt(r[0],10):e=parseFloat(r[0])}else r.length>=2&&(e=parseFloat(r[0]),o=parseInt(r[1],10));var a;if(!(void 0!==e&&e>=0&&e<=1))return a=o?t._soundById(o):t._sounds[0],a?a._volume:0;if("loaded"!==t._state||t._playLock)return t._queue.push({event:"volume",action:function(){t.volume.apply(t,r)}}),t;void 0===o&&(t._volume=e),o=t._getSoundIds(o);for(var u=0;u0?t/_:t),l=Date.now();e._fadeTo=o,e._interval=setInterval(function(){var r=(Date.now()-l)/t;l=Date.now(),d+=i*r,d=Math.round(100*d)/100,d=i<0?Math.max(o,d):Math.min(o,d),u._webAudio?e._volume=d:u.volume(d,e._id,!0),a&&(u._volume=d),(on&&d>=o)&&(clearInterval(e._interval),e._interval=null,e._fadeTo=null,u.volume(o,e._id),u._emit("fade",e._id))},s)},_stopFade:function(e){var o=this,t=o._soundById(e);return t&&t._interval&&(o._webAudio&&t._node.gain.cancelScheduledValues(n.ctx.currentTime),clearInterval(t._interval),t._interval=null,o.volume(t._fadeTo,e),t._fadeTo=null,o._emit("fade",e)),o},loop:function(){var e,n,o,t=this,r=arguments;if(0===r.length)return t._loop;if(1===r.length){if("boolean"!=typeof r[0])return!!(o=t._soundById(parseInt(r[0],10)))&&o._loop;e=r[0],t._loop=e}else 2===r.length&&(e=r[0],n=parseInt(r[1],10));for(var a=t._getSoundIds(n),u=0;u=0?o=parseInt(r[0],10):e=parseFloat(r[0])}else 2===r.length&&(e=parseFloat(r[0]),o=parseInt(r[1],10));var d;if("number"!=typeof e)return d=t._soundById(o),d?d._rate:t._rate;if("loaded"!==t._state||t._playLock)return t._queue.push({event:"rate",action:function(){t.rate.apply(t,r)}}),t;void 0===o&&(t._rate=e),o=t._getSoundIds(o);for(var i=0;i=0?o=parseInt(r[0],10):t._sounds.length&&(o=t._sounds[0]._id,e=parseFloat(r[0]))}else 2===r.length&&(e=parseFloat(r[0]),o=parseInt(r[1],10));if(void 0===o)return 0;if("number"==typeof e&&("loaded"!==t._state||t._playLock))return t._queue.push({event:"seek",action:function(){t.seek.apply(t,r)}}),t;var d=t._soundById(o);if(d){if(!("number"==typeof e&&e>=0)){if(t._webAudio){var i=t.playing(o)?n.ctx.currentTime-d._playStart:0,_=d._rateSeek?d._rateSeek-d._seek:0;return d._seek+(_+i*Math.abs(d._rate))}return d._node.currentTime}var s=t.playing(o);s&&t.pause(o,!0),d._seek=e,d._ended=!1,t._clearTimer(o),t._webAudio||!d._node||isNaN(d._node.duration)||(d._node.currentTime=e);var l=function(){s&&t.play(o,!0),t._emit("seek",o)};if(s&&!t._webAudio){var c=function(){t._playLock?setTimeout(c,0):l()};setTimeout(c,0)}else l()}return t},playing:function(e){var n=this;if("number"==typeof e){var o=n._soundById(e);return!!o&&!o._paused}for(var t=0;t=0&&n._howls.splice(a,1);var u=!0;for(t=0;t=0){u=!1;break}return r&&u&&delete r[e._src],n.noAudio=!1,e._state="unloaded",e._sounds=[],e=null,null},on:function(e,n,o,t){var r=this,a=r["_on"+e];return"function"==typeof n&&a.push(t?{id:o,fn:n,once:t}:{id:o,fn:n}),r},off:function(e,n,o){var t=this,r=t["_on"+e],a=0;if("number"==typeof n&&(o=n,n=null),n||o)for(a=0;a=0;a--)r[a].id&&r[a].id!==n&&"load"!==e||(setTimeout(function(e){e.call(this,n,o)}.bind(t,r[a].fn),0),r[a].once&&t.off(e,r[a].fn,r[a].id));return t._loadQueue(e),t},_loadQueue:function(e){var n=this;if(n._queue.length>0){var o=n._queue[0];o.event===e&&(n._queue.shift(),n._loadQueue()),e||o.action()}return n},_ended:function(e){var o=this,t=e._sprite;if(!o._webAudio&&e._node&&!e._node.paused&&!e._node.ended&&e._node.currentTime=0;t--){if(o<=n)return;e._sounds[t]._ended&&(e._webAudio&&e._sounds[t]._node&&e._sounds[t]._node.disconnect(0),e._sounds.splice(t,1),o--)}}},_getSoundIds:function(e){var n=this;if(void 0===e){for(var o=[],t=0;t=0;if(!e.bufferSource)return o;if(n._scratchBuffer&&e.bufferSource&&(e.bufferSource.onended=null,e.bufferSource.disconnect(0),t))try{e.bufferSource.buffer=n._scratchBuffer}catch(e){}return e.bufferSource=null,o},_clearSound:function(e){/MSIE |Trident\//.test(n._navigator&&n._navigator.userAgent)||(e.src="data:audio/wav;base64,UklGRigAAABXQVZFZm10IBIAAAABAAEARKwAAIhYAQACABAAAABkYXRhAgAAAAEA")}};var t=function(e){this._parent=e,this.init()};t.prototype={init:function(){var e=this,o=e._parent;return e._muted=o._muted,e._loop=o._loop,e._volume=o._volume,e._rate=o._rate,e._seek=0,e._paused=!0,e._ended=!0,e._sprite="__default",e._id=++n._counter,o._sounds.push(e),e.create(),e},create:function(){var e=this,o=e._parent,t=n._muted||e._muted||e._parent._muted?0:e._volume;return o._webAudio?(e._node=void 0===n.ctx.createGain?n.ctx.createGainNode():n.ctx.createGain(),e._node.gain.setValueAtTime(t,n.ctx.currentTime),e._node.paused=!0,e._node.connect(n.masterGain)):n.noAudio||(e._node=n._obtainHtml5Audio(),e._errorFn=e._errorListener.bind(e),e._node.addEventListener("error",e._errorFn,!1),e._loadFn=e._loadListener.bind(e),e._node.addEventListener(n._canPlayEvent,e._loadFn,!1),e._endFn=e._endListener.bind(e),e._node.addEventListener("ended",e._endFn,!1),e._node.src=o._src,e._node.preload=!0===o._preload?"auto":o._preload,e._node.volume=t*n.volume(),e._node.load()),e},reset:function(){var e=this,o=e._parent;return e._muted=o._muted,e._loop=o._loop,e._volume=o._volume,e._rate=o._rate,e._seek=0,e._rateSeek=0,e._paused=!0,e._ended=!0,e._sprite="__default",e._id=++n._counter,e},_errorListener:function(){var e=this;e._parent._emit("loaderror",e._id,e._node.error?e._node.error.code:0),e._node.removeEventListener("error",e._errorFn,!1)},_loadListener:function(){var e=this,o=e._parent;o._duration=Math.ceil(10*e._node.duration)/10,0===Object.keys(o._sprite).length&&(o._sprite={__default:[0,1e3*o._duration]}),"loaded"!==o._state&&(o._state="loaded",o._emit("load"),o._loadQueue()),e._node.removeEventListener(n._canPlayEvent,e._loadFn,!1)},_endListener:function(){var e=this,n=e._parent;n._duration===1/0&&(n._duration=Math.ceil(10*e._node.duration)/10,n._sprite.__default[1]===1/0&&(n._sprite.__default[1]=1e3*n._duration),n._ended(e)),e._node.removeEventListener("ended",e._endFn,!1)}};var r={},a=function(e){var n=e._src;if(r[n])return e._duration=r[n].duration,void i(e);if(/^data:[^;]+;base64,/.test(n)){for(var o=atob(n.split(",")[1]),t=new Uint8Array(o.length),a=0;a0?(r[o._src]=e,i(o,e)):t()};"undefined"!=typeof Promise&&1===n.ctx.decodeAudioData.length?n.ctx.decodeAudioData(e).then(a).catch(t):n.ctx.decodeAudioData(e,a,t)},i=function(e,n){n&&!e._duration&&(e._duration=n.duration),0===Object.keys(e._sprite).length&&(e._sprite={__default:[0,1e3*e._duration]}),"loaded"!==e._state&&(e._state="loaded",e._emit("load"),e._loadQueue())},_=function(){if(n.usingWebAudio){try{"undefined"!=typeof AudioContext?n.ctx=new AudioContext:"undefined"!=typeof webkitAudioContext?n.ctx=new webkitAudioContext:n.usingWebAudio=!1}catch(e){n.usingWebAudio=!1}n.ctx||(n.usingWebAudio=!1);var e=/iP(hone|od|ad)/.test(n._navigator&&n._navigator.platform),o=n._navigator&&n._navigator.appVersion.match(/OS (\d+)_(\d+)_?(\d+)?/),t=o?parseInt(o[1],10):null;if(e&&t&&t<9){var r=/safari/.test(n._navigator&&n._navigator.userAgent.toLowerCase());n._navigator&&!r&&(n.usingWebAudio=!1)}n.usingWebAudio&&(n.masterGain=void 0===n.ctx.createGain?n.ctx.createGainNode():n.ctx.createGain(),n.masterGain.gain.setValueAtTime(n._muted?0:n._volume,n.ctx.currentTime),n.masterGain.connect(n.ctx.destination)),n._setup()}};"function"==typeof define&&define.amd&&define([],function(){return{Howler:n,Howl:o}}),"undefined"!=typeof exports&&(exports.Howler=n,exports.Howl=o),"undefined"!=typeof global?(global.HowlerGlobal=e,global.Howler=n,global.Howl=o,global.Sound=t):"undefined"!=typeof window&&(window.HowlerGlobal=e,window.Howler=n,window.Howl=o,window.Sound=t)}(); \ No newline at end of file diff --git a/assets/js/howler/howler.es6.js b/assets/js/howler/howler.es6.js new file mode 100644 index 0000000..0a0821d --- /dev/null +++ b/assets/js/howler/howler.es6.js @@ -0,0 +1,3252 @@ +/*! + * howler.js v2.2.4 + * howlerjs.com + * + * (c) 2013-2020, James Simpson of GoldFire Studios + * goldfirestudios.com + * + * MIT License + */ + +(function() { + + 'use strict'; + + /** Global Methods **/ + /***************************************************************************/ + + /** + * Create the global controller. All contained methods and properties apply + * to all sounds that are currently playing or will be in the future. + */ + var HowlerGlobal = function() { + this.init(); + }; + HowlerGlobal.prototype = { + /** + * Initialize the global Howler object. + * @return {Howler} + */ + init: function() { + var self = this || Howler; + + // Create a global ID counter. + self._counter = 1000; + + // Pool of unlocked HTML5 Audio objects. + self._html5AudioPool = []; + self.html5PoolSize = 10; + + // Internal properties. + self._codecs = {}; + self._howls = []; + self._muted = false; + self._volume = 1; + self._canPlayEvent = 'canplaythrough'; + self._navigator = (typeof window !== 'undefined' && window.navigator) ? window.navigator : null; + + // Public properties. + self.masterGain = null; + self.noAudio = false; + self.usingWebAudio = true; + self.autoSuspend = true; + self.ctx = null; + + // Set to false to disable the auto audio unlocker. + self.autoUnlock = true; + + // Setup the various state values for global tracking. + self._setup(); + + return self; + }, + + /** + * Get/set the global volume for all sounds. + * @param {Float} vol Volume from 0.0 to 1.0. + * @return {Howler/Float} Returns self or current volume. + */ + volume: function(vol) { + var self = this || Howler; + vol = parseFloat(vol); + + // If we don't have an AudioContext created yet, run the setup. + if (!self.ctx) { + setupAudioContext(); + } + + if (typeof vol !== 'undefined' && vol >= 0 && vol <= 1) { + self._volume = vol; + + // Don't update any of the nodes if we are muted. + if (self._muted) { + return self; + } + + // When using Web Audio, we just need to adjust the master gain. + if (self.usingWebAudio) { + self.masterGain.gain.setValueAtTime(vol, Howler.ctx.currentTime); + } + + // Loop through and change volume for all HTML5 audio nodes. + for (var i=0; i=0; i--) { + self._howls[i].unload(); + } + + // Create a new AudioContext to make sure it is fully reset. + if (self.usingWebAudio && self.ctx && typeof self.ctx.close !== 'undefined') { + self.ctx.close(); + self.ctx = null; + setupAudioContext(); + } + + return self; + }, + + /** + * Check for codec support of specific extension. + * @param {String} ext Audio file extention. + * @return {Boolean} + */ + codecs: function(ext) { + return (this || Howler)._codecs[ext.replace(/^x-/, '')]; + }, + + /** + * Setup various state values for global tracking. + * @return {Howler} + */ + _setup: function() { + var self = this || Howler; + + // Keeps track of the suspend/resume state of the AudioContext. + self.state = self.ctx ? self.ctx.state || 'suspended' : 'suspended'; + + // Automatically begin the 30-second suspend process + self._autoSuspend(); + + // Check if audio is available. + if (!self.usingWebAudio) { + // No audio is available on this system if noAudio is set to true. + if (typeof Audio !== 'undefined') { + try { + var test = new Audio(); + + // Check if the canplaythrough event is available. + if (typeof test.oncanplaythrough === 'undefined') { + self._canPlayEvent = 'canplay'; + } + } catch(e) { + self.noAudio = true; + } + } else { + self.noAudio = true; + } + } + + // Test to make sure audio isn't disabled in Internet Explorer. + try { + var test = new Audio(); + if (test.muted) { + self.noAudio = true; + } + } catch (e) {} + + // Check for supported codecs. + if (!self.noAudio) { + self._setupCodecs(); + } + + return self; + }, + + /** + * Check for browser support for various codecs and cache the results. + * @return {Howler} + */ + _setupCodecs: function() { + var self = this || Howler; + var audioTest = null; + + // Must wrap in a try/catch because IE11 in server mode throws an error. + try { + audioTest = (typeof Audio !== 'undefined') ? new Audio() : null; + } catch (err) { + return self; + } + + if (!audioTest || typeof audioTest.canPlayType !== 'function') { + return self; + } + + var mpegTest = audioTest.canPlayType('audio/mpeg;').replace(/^no$/, ''); + + // Opera version <33 has mixed MP3 support, so we need to check for and block it. + var ua = self._navigator ? self._navigator.userAgent : ''; + var checkOpera = ua.match(/OPR\/(\d+)/g); + var isOldOpera = (checkOpera && parseInt(checkOpera[0].split('/')[1], 10) < 33); + var checkSafari = ua.indexOf('Safari') !== -1 && ua.indexOf('Chrome') === -1; + var safariVersion = ua.match(/Version\/(.*?) /); + var isOldSafari = (checkSafari && safariVersion && parseInt(safariVersion[1], 10) < 15); + + self._codecs = { + mp3: !!(!isOldOpera && (mpegTest || audioTest.canPlayType('audio/mp3;').replace(/^no$/, ''))), + mpeg: !!mpegTest, + opus: !!audioTest.canPlayType('audio/ogg; codecs="opus"').replace(/^no$/, ''), + ogg: !!audioTest.canPlayType('audio/ogg; codecs="vorbis"').replace(/^no$/, ''), + oga: !!audioTest.canPlayType('audio/ogg; codecs="vorbis"').replace(/^no$/, ''), + wav: !!(audioTest.canPlayType('audio/wav; codecs="1"') || audioTest.canPlayType('audio/wav')).replace(/^no$/, ''), + aac: !!audioTest.canPlayType('audio/aac;').replace(/^no$/, ''), + caf: !!audioTest.canPlayType('audio/x-caf;').replace(/^no$/, ''), + m4a: !!(audioTest.canPlayType('audio/x-m4a;') || audioTest.canPlayType('audio/m4a;') || audioTest.canPlayType('audio/aac;')).replace(/^no$/, ''), + m4b: !!(audioTest.canPlayType('audio/x-m4b;') || audioTest.canPlayType('audio/m4b;') || audioTest.canPlayType('audio/aac;')).replace(/^no$/, ''), + mp4: !!(audioTest.canPlayType('audio/x-mp4;') || audioTest.canPlayType('audio/mp4;') || audioTest.canPlayType('audio/aac;')).replace(/^no$/, ''), + weba: !!(!isOldSafari && audioTest.canPlayType('audio/webm; codecs="vorbis"').replace(/^no$/, '')), + webm: !!(!isOldSafari && audioTest.canPlayType('audio/webm; codecs="vorbis"').replace(/^no$/, '')), + dolby: !!audioTest.canPlayType('audio/mp4; codecs="ec-3"').replace(/^no$/, ''), + flac: !!(audioTest.canPlayType('audio/x-flac;') || audioTest.canPlayType('audio/flac;')).replace(/^no$/, '') + }; + + return self; + }, + + /** + * Some browsers/devices will only allow audio to be played after a user interaction. + * Attempt to automatically unlock audio on the first user interaction. + * Concept from: http://paulbakaus.com/tutorials/html5/web-audio-on-ios/ + * @return {Howler} + */ + _unlockAudio: function() { + var self = this || Howler; + + // Only run this if Web Audio is supported and it hasn't already been unlocked. + if (self._audioUnlocked || !self.ctx) { + return; + } + + self._audioUnlocked = false; + self.autoUnlock = false; + + // Some mobile devices/platforms have distortion issues when opening/closing tabs and/or web views. + // Bugs in the browser (especially Mobile Safari) can cause the sampleRate to change from 44100 to 48000. + // By calling Howler.unload(), we create a new AudioContext with the correct sampleRate. + if (!self._mobileUnloaded && self.ctx.sampleRate !== 44100) { + self._mobileUnloaded = true; + self.unload(); + } + + // Scratch buffer for enabling iOS to dispose of web audio buffers correctly, as per: + // http://stackoverflow.com/questions/24119684 + self._scratchBuffer = self.ctx.createBuffer(1, 1, 22050); + + // Call this method on touch start to create and play a buffer, + // then check if the audio actually played to determine if + // audio has now been unlocked on iOS, Android, etc. + var unlock = function(e) { + // Create a pool of unlocked HTML5 Audio objects that can + // be used for playing sounds without user interaction. HTML5 + // Audio objects must be individually unlocked, as opposed + // to the WebAudio API which only needs a single activation. + // This must occur before WebAudio setup or the source.onended + // event will not fire. + while (self._html5AudioPool.length < self.html5PoolSize) { + try { + var audioNode = new Audio(); + + // Mark this Audio object as unlocked to ensure it can get returned + // to the unlocked pool when released. + audioNode._unlocked = true; + + // Add the audio node to the pool. + self._releaseHtml5Audio(audioNode); + } catch (e) { + self.noAudio = true; + break; + } + } + + // Loop through any assigned audio nodes and unlock them. + for (var i=0; i= 55. + if (typeof self.ctx.resume === 'function') { + self.ctx.resume(); + } + + // Setup a timeout to check that we are unlocked on the next event loop. + source.onended = function() { + source.disconnect(0); + + // Update the unlocked state and prevent this check from happening again. + self._audioUnlocked = true; + + // Remove the touch start listener. + document.removeEventListener('touchstart', unlock, true); + document.removeEventListener('touchend', unlock, true); + document.removeEventListener('click', unlock, true); + document.removeEventListener('keydown', unlock, true); + + // Let all sounds know that audio has been unlocked. + for (var i=0; i 0 ? sound._seek : self._sprite[sprite][0] / 1000); + var duration = Math.max(0, ((self._sprite[sprite][0] + self._sprite[sprite][1]) / 1000) - seek); + var timeout = (duration * 1000) / Math.abs(sound._rate); + var start = self._sprite[sprite][0] / 1000; + var stop = (self._sprite[sprite][0] + self._sprite[sprite][1]) / 1000; + sound._sprite = sprite; + + // Mark the sound as ended instantly so that this async playback + // doesn't get grabbed by another call to play while this one waits to start. + sound._ended = false; + + // Update the parameters of the sound. + var setParams = function() { + sound._paused = false; + sound._seek = seek; + sound._start = start; + sound._stop = stop; + sound._loop = !!(sound._loop || self._sprite[sprite][2]); + }; + + // End the sound instantly if seek is at the end. + if (seek >= stop) { + self._ended(sound); + return; + } + + // Begin the actual playback. + var node = sound._node; + if (self._webAudio) { + // Fire this when the sound is ready to play to begin Web Audio playback. + var playWebAudio = function() { + self._playLock = false; + setParams(); + self._refreshBuffer(sound); + + // Setup the playback params. + var vol = (sound._muted || self._muted) ? 0 : sound._volume; + node.gain.setValueAtTime(vol, Howler.ctx.currentTime); + sound._playStart = Howler.ctx.currentTime; + + // Play the sound using the supported method. + if (typeof node.bufferSource.start === 'undefined') { + sound._loop ? node.bufferSource.noteGrainOn(0, seek, 86400) : node.bufferSource.noteGrainOn(0, seek, duration); + } else { + sound._loop ? node.bufferSource.start(0, seek, 86400) : node.bufferSource.start(0, seek, duration); + } + + // Start a new timer if none is present. + if (timeout !== Infinity) { + self._endTimers[sound._id] = setTimeout(self._ended.bind(self, sound), timeout); + } + + if (!internal) { + setTimeout(function() { + self._emit('play', sound._id); + self._loadQueue(); + }, 0); + } + }; + + if (Howler.state === 'running' && Howler.ctx.state !== 'interrupted') { + playWebAudio(); + } else { + self._playLock = true; + + // Wait for the audio context to resume before playing. + self.once('resume', playWebAudio); + + // Cancel the end timer. + self._clearTimer(sound._id); + } + } else { + // Fire this when the sound is ready to play to begin HTML5 Audio playback. + var playHtml5 = function() { + node.currentTime = seek; + node.muted = sound._muted || self._muted || Howler._muted || node.muted; + node.volume = sound._volume * Howler.volume(); + node.playbackRate = sound._rate; + + // Some browsers will throw an error if this is called without user interaction. + try { + var play = node.play(); + + // Support older browsers that don't support promises, and thus don't have this issue. + if (play && typeof Promise !== 'undefined' && (play instanceof Promise || typeof play.then === 'function')) { + // Implements a lock to prevent DOMException: The play() request was interrupted by a call to pause(). + self._playLock = true; + + // Set param values immediately. + setParams(); + + // Releases the lock and executes queued actions. + play + .then(function() { + self._playLock = false; + node._unlocked = true; + if (!internal) { + self._emit('play', sound._id); + } else { + self._loadQueue(); + } + }) + .catch(function() { + self._playLock = false; + self._emit('playerror', sound._id, 'Playback was unable to start. This is most commonly an issue ' + + 'on mobile devices and Chrome where playback was not within a user interaction.'); + + // Reset the ended and paused values. + sound._ended = true; + sound._paused = true; + }); + } else if (!internal) { + self._playLock = false; + setParams(); + self._emit('play', sound._id); + } + + // Setting rate before playing won't work in IE, so we set it again here. + node.playbackRate = sound._rate; + + // If the node is still paused, then we can assume there was a playback issue. + if (node.paused) { + self._emit('playerror', sound._id, 'Playback was unable to start. This is most commonly an issue ' + + 'on mobile devices and Chrome where playback was not within a user interaction.'); + return; + } + + // Setup the end timer on sprites or listen for the ended event. + if (sprite !== '__default' || sound._loop) { + self._endTimers[sound._id] = setTimeout(self._ended.bind(self, sound), timeout); + } else { + self._endTimers[sound._id] = function() { + // Fire ended on this audio node. + self._ended(sound); + + // Clear this listener. + node.removeEventListener('ended', self._endTimers[sound._id], false); + }; + node.addEventListener('ended', self._endTimers[sound._id], false); + } + } catch (err) { + self._emit('playerror', sound._id, err); + } + }; + + // If this is streaming audio, make sure the src is set and load again. + if (node.src === 'data:audio/wav;base64,UklGRigAAABXQVZFZm10IBIAAAABAAEARKwAAIhYAQACABAAAABkYXRhAgAAAAEA') { + node.src = self._src; + node.load(); + } + + // Play immediately if ready, or wait for the 'canplaythrough'e vent. + var loadedNoReadyState = (window && window.ejecta) || (!node.readyState && Howler._navigator.isCocoonJS); + if (node.readyState >= 3 || loadedNoReadyState) { + playHtml5(); + } else { + self._playLock = true; + self._state = 'loading'; + + var listener = function() { + self._state = 'loaded'; + + // Begin playback. + playHtml5(); + + // Clear this listener. + node.removeEventListener(Howler._canPlayEvent, listener, false); + }; + node.addEventListener(Howler._canPlayEvent, listener, false); + + // Cancel the end timer. + self._clearTimer(sound._id); + } + } + + return sound._id; + }, + + /** + * Pause playback and save current position. + * @param {Number} id The sound ID (empty to pause all in group). + * @return {Howl} + */ + pause: function(id) { + var self = this; + + // If the sound hasn't loaded or a play() promise is pending, add it to the load queue to pause when capable. + if (self._state !== 'loaded' || self._playLock) { + self._queue.push({ + event: 'pause', + action: function() { + self.pause(id); + } + }); + + return self; + } + + // If no id is passed, get all ID's to be paused. + var ids = self._getSoundIds(id); + + for (var i=0; i Returns the group's volume value. + * volume(id) -> Returns the sound id's current volume. + * volume(vol) -> Sets the volume of all sounds in this Howl group. + * volume(vol, id) -> Sets the volume of passed sound id. + * @return {Howl/Number} Returns self or current volume. + */ + volume: function() { + var self = this; + var args = arguments; + var vol, id; + + // Determine the values based on arguments. + if (args.length === 0) { + // Return the value of the groups' volume. + return self._volume; + } else if (args.length === 1 || args.length === 2 && typeof args[1] === 'undefined') { + // First check if this is an ID, and if not, assume it is a new volume. + var ids = self._getSoundIds(); + var index = ids.indexOf(args[0]); + if (index >= 0) { + id = parseInt(args[0], 10); + } else { + vol = parseFloat(args[0]); + } + } else if (args.length >= 2) { + vol = parseFloat(args[0]); + id = parseInt(args[1], 10); + } + + // Update the volume or return the current volume. + var sound; + if (typeof vol !== 'undefined' && vol >= 0 && vol <= 1) { + // If the sound hasn't loaded, add it to the load queue to change volume when capable. + if (self._state !== 'loaded'|| self._playLock) { + self._queue.push({ + event: 'volume', + action: function() { + self.volume.apply(self, args); + } + }); + + return self; + } + + // Set the group volume. + if (typeof id === 'undefined') { + self._volume = vol; + } + + // Update one or all volumes. + id = self._getSoundIds(id); + for (var i=0; i 0) ? len / steps : len); + var lastTick = Date.now(); + + // Store the value being faded to. + sound._fadeTo = to; + + // Update the volume value on each interval tick. + sound._interval = setInterval(function() { + // Update the volume based on the time since the last tick. + var tick = (Date.now() - lastTick) / len; + lastTick = Date.now(); + vol += diff * tick; + + // Round to within 2 decimal points. + vol = Math.round(vol * 100) / 100; + + // Make sure the volume is in the right bounds. + if (diff < 0) { + vol = Math.max(to, vol); + } else { + vol = Math.min(to, vol); + } + + // Change the volume. + if (self._webAudio) { + sound._volume = vol; + } else { + self.volume(vol, sound._id, true); + } + + // Set the group's volume. + if (isGroup) { + self._volume = vol; + } + + // When the fade is complete, stop it and fire event. + if ((to < from && vol <= to) || (to > from && vol >= to)) { + clearInterval(sound._interval); + sound._interval = null; + sound._fadeTo = null; + self.volume(to, sound._id); + self._emit('fade', sound._id); + } + }, stepLen); + }, + + /** + * Internal method that stops the currently playing fade when + * a new fade starts, volume is changed or the sound is stopped. + * @param {Number} id The sound id. + * @return {Howl} + */ + _stopFade: function(id) { + var self = this; + var sound = self._soundById(id); + + if (sound && sound._interval) { + if (self._webAudio) { + sound._node.gain.cancelScheduledValues(Howler.ctx.currentTime); + } + + clearInterval(sound._interval); + sound._interval = null; + self.volume(sound._fadeTo, id); + sound._fadeTo = null; + self._emit('fade', id); + } + + return self; + }, + + /** + * Get/set the loop parameter on a sound. This method can optionally take 0, 1 or 2 arguments. + * loop() -> Returns the group's loop value. + * loop(id) -> Returns the sound id's loop value. + * loop(loop) -> Sets the loop value for all sounds in this Howl group. + * loop(loop, id) -> Sets the loop value of passed sound id. + * @return {Howl/Boolean} Returns self or current loop value. + */ + loop: function() { + var self = this; + var args = arguments; + var loop, id, sound; + + // Determine the values for loop and id. + if (args.length === 0) { + // Return the grou's loop value. + return self._loop; + } else if (args.length === 1) { + if (typeof args[0] === 'boolean') { + loop = args[0]; + self._loop = loop; + } else { + // Return this sound's loop value. + sound = self._soundById(parseInt(args[0], 10)); + return sound ? sound._loop : false; + } + } else if (args.length === 2) { + loop = args[0]; + id = parseInt(args[1], 10); + } + + // If no id is passed, get all ID's to be looped. + var ids = self._getSoundIds(id); + for (var i=0; i Returns the first sound node's current playback rate. + * rate(id) -> Returns the sound id's current playback rate. + * rate(rate) -> Sets the playback rate of all sounds in this Howl group. + * rate(rate, id) -> Sets the playback rate of passed sound id. + * @return {Howl/Number} Returns self or the current playback rate. + */ + rate: function() { + var self = this; + var args = arguments; + var rate, id; + + // Determine the values based on arguments. + if (args.length === 0) { + // We will simply return the current rate of the first node. + id = self._sounds[0]._id; + } else if (args.length === 1) { + // First check if this is an ID, and if not, assume it is a new rate value. + var ids = self._getSoundIds(); + var index = ids.indexOf(args[0]); + if (index >= 0) { + id = parseInt(args[0], 10); + } else { + rate = parseFloat(args[0]); + } + } else if (args.length === 2) { + rate = parseFloat(args[0]); + id = parseInt(args[1], 10); + } + + // Update the playback rate or return the current value. + var sound; + if (typeof rate === 'number') { + // If the sound hasn't loaded, add it to the load queue to change playback rate when capable. + if (self._state !== 'loaded' || self._playLock) { + self._queue.push({ + event: 'rate', + action: function() { + self.rate.apply(self, args); + } + }); + + return self; + } + + // Set the group rate. + if (typeof id === 'undefined') { + self._rate = rate; + } + + // Update one or all volumes. + id = self._getSoundIds(id); + for (var i=0; i Returns the first sound node's current seek position. + * seek(id) -> Returns the sound id's current seek position. + * seek(seek) -> Sets the seek position of the first sound node. + * seek(seek, id) -> Sets the seek position of passed sound id. + * @return {Howl/Number} Returns self or the current seek position. + */ + seek: function() { + var self = this; + var args = arguments; + var seek, id; + + // Determine the values based on arguments. + if (args.length === 0) { + // We will simply return the current position of the first node. + if (self._sounds.length) { + id = self._sounds[0]._id; + } + } else if (args.length === 1) { + // First check if this is an ID, and if not, assume it is a new seek position. + var ids = self._getSoundIds(); + var index = ids.indexOf(args[0]); + if (index >= 0) { + id = parseInt(args[0], 10); + } else if (self._sounds.length) { + id = self._sounds[0]._id; + seek = parseFloat(args[0]); + } + } else if (args.length === 2) { + seek = parseFloat(args[0]); + id = parseInt(args[1], 10); + } + + // If there is no ID, bail out. + if (typeof id === 'undefined') { + return 0; + } + + // If the sound hasn't loaded, add it to the load queue to seek when capable. + if (typeof seek === 'number' && (self._state !== 'loaded' || self._playLock)) { + self._queue.push({ + event: 'seek', + action: function() { + self.seek.apply(self, args); + } + }); + + return self; + } + + // Get the sound. + var sound = self._soundById(id); + + if (sound) { + if (typeof seek === 'number' && seek >= 0) { + // Pause the sound and update position for restarting playback. + var playing = self.playing(id); + if (playing) { + self.pause(id, true); + } + + // Move the position of the track and cancel timer. + sound._seek = seek; + sound._ended = false; + self._clearTimer(id); + + // Update the seek position for HTML5 Audio. + if (!self._webAudio && sound._node && !isNaN(sound._node.duration)) { + sound._node.currentTime = seek; + } + + // Seek and emit when ready. + var seekAndEmit = function() { + // Restart the playback if the sound was playing. + if (playing) { + self.play(id, true); + } + + self._emit('seek', id); + }; + + // Wait for the play lock to be unset before emitting (HTML5 Audio). + if (playing && !self._webAudio) { + var emitSeek = function() { + if (!self._playLock) { + seekAndEmit(); + } else { + setTimeout(emitSeek, 0); + } + }; + setTimeout(emitSeek, 0); + } else { + seekAndEmit(); + } + } else { + if (self._webAudio) { + var realTime = self.playing(id) ? Howler.ctx.currentTime - sound._playStart : 0; + var rateSeek = sound._rateSeek ? sound._rateSeek - sound._seek : 0; + return sound._seek + (rateSeek + realTime * Math.abs(sound._rate)); + } else { + return sound._node.currentTime; + } + } + } + + return self; + }, + + /** + * Check if a specific sound is currently playing or not (if id is provided), or check if at least one of the sounds in the group is playing or not. + * @param {Number} id The sound id to check. If none is passed, the whole sound group is checked. + * @return {Boolean} True if playing and false if not. + */ + playing: function(id) { + var self = this; + + // Check the passed sound ID (if any). + if (typeof id === 'number') { + var sound = self._soundById(id); + return sound ? !sound._paused : false; + } + + // Otherwise, loop through all sounds and check if any are playing. + for (var i=0; i= 0) { + Howler._howls.splice(index, 1); + } + + // Delete this sound from the cache (if no other Howl is using it). + var remCache = true; + for (i=0; i= 0) { + remCache = false; + break; + } + } + + if (cache && remCache) { + delete cache[self._src]; + } + + // Clear global errors. + Howler.noAudio = false; + + // Clear out `self`. + self._state = 'unloaded'; + self._sounds = []; + self = null; + + return null; + }, + + /** + * Listen to a custom event. + * @param {String} event Event name. + * @param {Function} fn Listener to call. + * @param {Number} id (optional) Only listen to events for this sound. + * @param {Number} once (INTERNAL) Marks event to fire only once. + * @return {Howl} + */ + on: function(event, fn, id, once) { + var self = this; + var events = self['_on' + event]; + + if (typeof fn === 'function') { + events.push(once ? {id: id, fn: fn, once: once} : {id: id, fn: fn}); + } + + return self; + }, + + /** + * Remove a custom event. Call without parameters to remove all events. + * @param {String} event Event name. + * @param {Function} fn Listener to remove. Leave empty to remove all. + * @param {Number} id (optional) Only remove events for this sound. + * @return {Howl} + */ + off: function(event, fn, id) { + var self = this; + var events = self['_on' + event]; + var i = 0; + + // Allow passing just an event and ID. + if (typeof fn === 'number') { + id = fn; + fn = null; + } + + if (fn || id) { + // Loop through event store and remove the passed function. + for (i=0; i=0; i--) { + // Only fire the listener if the correct ID is used. + if (!events[i].id || events[i].id === id || event === 'load') { + setTimeout(function(fn) { + fn.call(this, id, msg); + }.bind(self, events[i].fn), 0); + + // If this event was setup with `once`, remove it. + if (events[i].once) { + self.off(event, events[i].fn, events[i].id); + } + } + } + + // Pass the event type into load queue so that it can continue stepping. + self._loadQueue(event); + + return self; + }, + + /** + * Queue of actions initiated before the sound has loaded. + * These will be called in sequence, with the next only firing + * after the previous has finished executing (even if async like play). + * @return {Howl} + */ + _loadQueue: function(event) { + var self = this; + + if (self._queue.length > 0) { + var task = self._queue[0]; + + // Remove this task if a matching event was passed. + if (task.event === event) { + self._queue.shift(); + self._loadQueue(); + } + + // Run the task if no event type is passed. + if (!event) { + task.action(); + } + } + + return self; + }, + + /** + * Fired when playback ends at the end of the duration. + * @param {Sound} sound The sound object to work with. + * @return {Howl} + */ + _ended: function(sound) { + var self = this; + var sprite = sound._sprite; + + // If we are using IE and there was network latency we may be clipping + // audio before it completes playing. Lets check the node to make sure it + // believes it has completed, before ending the playback. + if (!self._webAudio && sound._node && !sound._node.paused && !sound._node.ended && sound._node.currentTime < sound._stop) { + setTimeout(self._ended.bind(self, sound), 100); + return self; + } + + // Should this sound loop? + var loop = !!(sound._loop || self._sprite[sprite][2]); + + // Fire the ended event. + self._emit('end', sound._id); + + // Restart the playback for HTML5 Audio loop. + if (!self._webAudio && loop) { + self.stop(sound._id, true).play(sound._id); + } + + // Restart this timer if on a Web Audio loop. + if (self._webAudio && loop) { + self._emit('play', sound._id); + sound._seek = sound._start || 0; + sound._rateSeek = 0; + sound._playStart = Howler.ctx.currentTime; + + var timeout = ((sound._stop - sound._start) * 1000) / Math.abs(sound._rate); + self._endTimers[sound._id] = setTimeout(self._ended.bind(self, sound), timeout); + } + + // Mark the node as paused. + if (self._webAudio && !loop) { + sound._paused = true; + sound._ended = true; + sound._seek = sound._start || 0; + sound._rateSeek = 0; + self._clearTimer(sound._id); + + // Clean up the buffer source. + self._cleanBuffer(sound._node); + + // Attempt to auto-suspend AudioContext if no sounds are still playing. + Howler._autoSuspend(); + } + + // When using a sprite, end the track. + if (!self._webAudio && !loop) { + self.stop(sound._id, true); + } + + return self; + }, + + /** + * Clear the end timer for a sound playback. + * @param {Number} id The sound ID. + * @return {Howl} + */ + _clearTimer: function(id) { + var self = this; + + if (self._endTimers[id]) { + // Clear the timeout or remove the ended listener. + if (typeof self._endTimers[id] !== 'function') { + clearTimeout(self._endTimers[id]); + } else { + var sound = self._soundById(id); + if (sound && sound._node) { + sound._node.removeEventListener('ended', self._endTimers[id], false); + } + } + + delete self._endTimers[id]; + } + + return self; + }, + + /** + * Return the sound identified by this ID, or return null. + * @param {Number} id Sound ID + * @return {Object} Sound object or null. + */ + _soundById: function(id) { + var self = this; + + // Loop through all sounds and find the one with this ID. + for (var i=0; i=0; i--) { + if (cnt <= limit) { + return; + } + + if (self._sounds[i]._ended) { + // Disconnect the audio source when using Web Audio. + if (self._webAudio && self._sounds[i]._node) { + self._sounds[i]._node.disconnect(0); + } + + // Remove sounds until we have the pool size. + self._sounds.splice(i, 1); + cnt--; + } + } + }, + + /** + * Get all ID's from the sounds pool. + * @param {Number} id Only return one ID if one is passed. + * @return {Array} Array of IDs. + */ + _getSoundIds: function(id) { + var self = this; + + if (typeof id === 'undefined') { + var ids = []; + for (var i=0; i= 0; + + if (!node.bufferSource) { + return self; + } + + if (Howler._scratchBuffer && node.bufferSource) { + node.bufferSource.onended = null; + node.bufferSource.disconnect(0); + if (isIOS) { + try { node.bufferSource.buffer = Howler._scratchBuffer; } catch(e) {} + } + } + node.bufferSource = null; + + return self; + }, + + /** + * Set the source to a 0-second silence to stop any downloading (except in IE). + * @param {Object} node Audio node to clear. + */ + _clearSound: function(node) { + var checkIE = /MSIE |Trident\//.test(Howler._navigator && Howler._navigator.userAgent); + if (!checkIE) { + node.src = 'data:audio/wav;base64,UklGRigAAABXQVZFZm10IBIAAAABAAEARKwAAIhYAQACABAAAABkYXRhAgAAAAEA'; + } + } + }; + + /** Single Sound Methods **/ + /***************************************************************************/ + + /** + * Setup the sound object, which each node attached to a Howl group is contained in. + * @param {Object} howl The Howl parent group. + */ + var Sound = function(howl) { + this._parent = howl; + this.init(); + }; + Sound.prototype = { + /** + * Initialize a new Sound object. + * @return {Sound} + */ + init: function() { + var self = this; + var parent = self._parent; + + // Setup the default parameters. + self._muted = parent._muted; + self._loop = parent._loop; + self._volume = parent._volume; + self._rate = parent._rate; + self._seek = 0; + self._paused = true; + self._ended = true; + self._sprite = '__default'; + + // Generate a unique ID for this sound. + self._id = ++Howler._counter; + + // Add itself to the parent's pool. + parent._sounds.push(self); + + // Create the new node. + self.create(); + + return self; + }, + + /** + * Create and setup a new sound object, whether HTML5 Audio or Web Audio. + * @return {Sound} + */ + create: function() { + var self = this; + var parent = self._parent; + var volume = (Howler._muted || self._muted || self._parent._muted) ? 0 : self._volume; + + if (parent._webAudio) { + // Create the gain node for controlling volume (the source will connect to this). + self._node = (typeof Howler.ctx.createGain === 'undefined') ? Howler.ctx.createGainNode() : Howler.ctx.createGain(); + self._node.gain.setValueAtTime(volume, Howler.ctx.currentTime); + self._node.paused = true; + self._node.connect(Howler.masterGain); + } else if (!Howler.noAudio) { + // Get an unlocked Audio object from the pool. + self._node = Howler._obtainHtml5Audio(); + + // Listen for errors (http://dev.w3.org/html5/spec-author-view/spec.html#mediaerror). + self._errorFn = self._errorListener.bind(self); + self._node.addEventListener('error', self._errorFn, false); + + // Listen for 'canplaythrough' event to let us know the sound is ready. + self._loadFn = self._loadListener.bind(self); + self._node.addEventListener(Howler._canPlayEvent, self._loadFn, false); + + // Listen for the 'ended' event on the sound to account for edge-case where + // a finite sound has a duration of Infinity. + self._endFn = self._endListener.bind(self); + self._node.addEventListener('ended', self._endFn, false); + + // Setup the new audio node. + self._node.src = parent._src; + self._node.preload = parent._preload === true ? 'auto' : parent._preload; + self._node.volume = volume * Howler.volume(); + + // Begin loading the source. + self._node.load(); + } + + return self; + }, + + /** + * Reset the parameters of this sound to the original state (for recycle). + * @return {Sound} + */ + reset: function() { + var self = this; + var parent = self._parent; + + // Reset all of the parameters of this sound. + self._muted = parent._muted; + self._loop = parent._loop; + self._volume = parent._volume; + self._rate = parent._rate; + self._seek = 0; + self._rateSeek = 0; + self._paused = true; + self._ended = true; + self._sprite = '__default'; + + // Generate a new ID so that it isn't confused with the previous sound. + self._id = ++Howler._counter; + + return self; + }, + + /** + * HTML5 Audio error listener callback. + */ + _errorListener: function() { + var self = this; + + // Fire an error event and pass back the code. + self._parent._emit('loaderror', self._id, self._node.error ? self._node.error.code : 0); + + // Clear the event listener. + self._node.removeEventListener('error', self._errorFn, false); + }, + + /** + * HTML5 Audio canplaythrough listener callback. + */ + _loadListener: function() { + var self = this; + var parent = self._parent; + + // Round up the duration to account for the lower precision in HTML5 Audio. + parent._duration = Math.ceil(self._node.duration * 10) / 10; + + // Setup a sprite if none is defined. + if (Object.keys(parent._sprite).length === 0) { + parent._sprite = {__default: [0, parent._duration * 1000]}; + } + + if (parent._state !== 'loaded') { + parent._state = 'loaded'; + parent._emit('load'); + parent._loadQueue(); + } + + // Clear the event listener. + self._node.removeEventListener(Howler._canPlayEvent, self._loadFn, false); + }, + + /** + * HTML5 Audio ended listener callback. + */ + _endListener: function() { + var self = this; + var parent = self._parent; + + // Only handle the `ended`` event if the duration is Infinity. + if (parent._duration === Infinity) { + // Update the parent duration to match the real audio duration. + // Round up the duration to account for the lower precision in HTML5 Audio. + parent._duration = Math.ceil(self._node.duration * 10) / 10; + + // Update the sprite that corresponds to the real duration. + if (parent._sprite.__default[1] === Infinity) { + parent._sprite.__default[1] = parent._duration * 1000; + } + + // Run the regular ended method. + parent._ended(self); + } + + // Clear the event listener since the duration is now correct. + self._node.removeEventListener('ended', self._endFn, false); + } + }; + + /** Helper Methods **/ + /***************************************************************************/ + + var cache = {}; + + /** + * Buffer a sound from URL, Data URI or cache and decode to audio source (Web Audio API). + * @param {Howl} self + */ + var loadBuffer = function(self) { + var url = self._src; + + // Check if the buffer has already been cached and use it instead. + if (cache[url]) { + // Set the duration from the cache. + self._duration = cache[url].duration; + + // Load the sound into this Howl. + loadSound(self); + + return; + } + + if (/^data:[^;]+;base64,/.test(url)) { + // Decode the base64 data URI without XHR, since some browsers don't support it. + var data = atob(url.split(',')[1]); + var dataView = new Uint8Array(data.length); + for (var i=0; i 0) { + cache[self._src] = buffer; + loadSound(self, buffer); + } else { + error(); + } + }; + + // Decode the buffer into an audio source. + if (typeof Promise !== 'undefined' && Howler.ctx.decodeAudioData.length === 1) { + Howler.ctx.decodeAudioData(arraybuffer).then(success).catch(error); + } else { + Howler.ctx.decodeAudioData(arraybuffer, success, error); + } + } + + /** + * Sound is now loaded, so finish setting everything up and fire the loaded event. + * @param {Howl} self + * @param {Object} buffer The decoded buffer sound source. + */ + var loadSound = function(self, buffer) { + // Set the duration. + if (buffer && !self._duration) { + self._duration = buffer.duration; + } + + // Setup a sprite if none is defined. + if (Object.keys(self._sprite).length === 0) { + self._sprite = {__default: [0, self._duration * 1000]}; + } + + // Fire the loaded event. + if (self._state !== 'loaded') { + self._state = 'loaded'; + self._emit('load'); + self._loadQueue(); + } + }; + + /** + * Setup the audio context when available, or switch to HTML5 Audio mode. + */ + var setupAudioContext = function() { + // If we have already detected that Web Audio isn't supported, don't run this step again. + if (!Howler.usingWebAudio) { + return; + } + + // Check if we are using Web Audio and setup the AudioContext if we are. + try { + if (typeof AudioContext !== 'undefined') { + Howler.ctx = new AudioContext(); + } else if (typeof webkitAudioContext !== 'undefined') { + Howler.ctx = new webkitAudioContext(); + } else { + Howler.usingWebAudio = false; + } + } catch(e) { + Howler.usingWebAudio = false; + } + + // If the audio context creation still failed, set using web audio to false. + if (!Howler.ctx) { + Howler.usingWebAudio = false; + } + + // Check if a webview is being used on iOS8 or earlier (rather than the browser). + // If it is, disable Web Audio as it causes crashing. + var iOS = (/iP(hone|od|ad)/.test(Howler._navigator && Howler._navigator.platform)); + var appVersion = Howler._navigator && Howler._navigator.appVersion.match(/OS (\d+)_(\d+)_?(\d+)?/); + var version = appVersion ? parseInt(appVersion[1], 10) : null; + if (iOS && version && version < 9) { + var safari = /safari/.test(Howler._navigator && Howler._navigator.userAgent.toLowerCase()); + if (Howler._navigator && !safari) { + Howler.usingWebAudio = false; + } + } + + // Create and expose the master GainNode when using Web Audio (useful for plugins or advanced usage). + if (Howler.usingWebAudio) { + Howler.masterGain = (typeof Howler.ctx.createGain === 'undefined') ? Howler.ctx.createGainNode() : Howler.ctx.createGain(); + Howler.masterGain.gain.setValueAtTime(Howler._muted ? 0 : Howler._volume, Howler.ctx.currentTime); + Howler.masterGain.connect(Howler.ctx.destination); + } + + // Re-run the setup on Howler. + Howler._setup(); + }; + + // Add support for AMD (Asynchronous Module Definition) libraries such as require.js. + if (typeof define === 'function' && define.amd) { + define([], function() { + return { + Howler: Howler, + Howl: Howl + }; + }); + } + + // Add support for CommonJS libraries such as browserify. + if (typeof exports !== 'undefined') { + exports.Howler = Howler; + exports.Howl = Howl; + } + + // Add to global in Node.js (for testing, etc). + if (typeof global !== 'undefined') { + global.HowlerGlobal = HowlerGlobal; + global.Howler = Howler; + global.Howl = Howl; + global.Sound = Sound; + } else if (typeof window !== 'undefined') { // Define globally in case AMD is not available or unused. + window.HowlerGlobal = HowlerGlobal; + window.Howler = Howler; + window.Howl = Howl; + window.Sound = Sound; + } +})(); + + +/*! + * Spatial Plugin - Adds support for stereo and 3D audio where Web Audio is supported. + * + * howler.js v2.2.4 + * howlerjs.com + * + * (c) 2013-2020, James Simpson of GoldFire Studios + * goldfirestudios.com + * + * MIT License + */ + +(function() { + + 'use strict'; + + // Setup default properties. + HowlerGlobal.prototype._pos = [0, 0, 0]; + HowlerGlobal.prototype._orientation = [0, 0, -1, 0, 1, 0]; + + /** Global Methods **/ + /***************************************************************************/ + + /** + * Helper method to update the stereo panning position of all current Howls. + * Future Howls will not use this value unless explicitly set. + * @param {Number} pan A value of -1.0 is all the way left and 1.0 is all the way right. + * @return {Howler/Number} Self or current stereo panning value. + */ + HowlerGlobal.prototype.stereo = function(pan) { + var self = this; + + // Stop right here if not using Web Audio. + if (!self.ctx || !self.ctx.listener) { + return self; + } + + // Loop through all Howls and update their stereo panning. + for (var i=self._howls.length-1; i>=0; i--) { + self._howls[i].stereo(pan); + } + + return self; + }; + + /** + * Get/set the position of the listener in 3D cartesian space. Sounds using + * 3D position will be relative to the listener's position. + * @param {Number} x The x-position of the listener. + * @param {Number} y The y-position of the listener. + * @param {Number} z The z-position of the listener. + * @return {Howler/Array} Self or current listener position. + */ + HowlerGlobal.prototype.pos = function(x, y, z) { + var self = this; + + // Stop right here if not using Web Audio. + if (!self.ctx || !self.ctx.listener) { + return self; + } + + // Set the defaults for optional 'y' & 'z'. + y = (typeof y !== 'number') ? self._pos[1] : y; + z = (typeof z !== 'number') ? self._pos[2] : z; + + if (typeof x === 'number') { + self._pos = [x, y, z]; + + if (typeof self.ctx.listener.positionX !== 'undefined') { + self.ctx.listener.positionX.setTargetAtTime(self._pos[0], Howler.ctx.currentTime, 0.1); + self.ctx.listener.positionY.setTargetAtTime(self._pos[1], Howler.ctx.currentTime, 0.1); + self.ctx.listener.positionZ.setTargetAtTime(self._pos[2], Howler.ctx.currentTime, 0.1); + } else { + self.ctx.listener.setPosition(self._pos[0], self._pos[1], self._pos[2]); + } + } else { + return self._pos; + } + + return self; + }; + + /** + * Get/set the direction the listener is pointing in the 3D cartesian space. + * A front and up vector must be provided. The front is the direction the + * face of the listener is pointing, and up is the direction the top of the + * listener is pointing. Thus, these values are expected to be at right angles + * from each other. + * @param {Number} x The x-orientation of the listener. + * @param {Number} y The y-orientation of the listener. + * @param {Number} z The z-orientation of the listener. + * @param {Number} xUp The x-orientation of the top of the listener. + * @param {Number} yUp The y-orientation of the top of the listener. + * @param {Number} zUp The z-orientation of the top of the listener. + * @return {Howler/Array} Returns self or the current orientation vectors. + */ + HowlerGlobal.prototype.orientation = function(x, y, z, xUp, yUp, zUp) { + var self = this; + + // Stop right here if not using Web Audio. + if (!self.ctx || !self.ctx.listener) { + return self; + } + + // Set the defaults for optional 'y' & 'z'. + var or = self._orientation; + y = (typeof y !== 'number') ? or[1] : y; + z = (typeof z !== 'number') ? or[2] : z; + xUp = (typeof xUp !== 'number') ? or[3] : xUp; + yUp = (typeof yUp !== 'number') ? or[4] : yUp; + zUp = (typeof zUp !== 'number') ? or[5] : zUp; + + if (typeof x === 'number') { + self._orientation = [x, y, z, xUp, yUp, zUp]; + + if (typeof self.ctx.listener.forwardX !== 'undefined') { + self.ctx.listener.forwardX.setTargetAtTime(x, Howler.ctx.currentTime, 0.1); + self.ctx.listener.forwardY.setTargetAtTime(y, Howler.ctx.currentTime, 0.1); + self.ctx.listener.forwardZ.setTargetAtTime(z, Howler.ctx.currentTime, 0.1); + self.ctx.listener.upX.setTargetAtTime(xUp, Howler.ctx.currentTime, 0.1); + self.ctx.listener.upY.setTargetAtTime(yUp, Howler.ctx.currentTime, 0.1); + self.ctx.listener.upZ.setTargetAtTime(zUp, Howler.ctx.currentTime, 0.1); + } else { + self.ctx.listener.setOrientation(x, y, z, xUp, yUp, zUp); + } + } else { + return or; + } + + return self; + }; + + /** Group Methods **/ + /***************************************************************************/ + + /** + * Add new properties to the core init. + * @param {Function} _super Core init method. + * @return {Howl} + */ + Howl.prototype.init = (function(_super) { + return function(o) { + var self = this; + + // Setup user-defined default properties. + self._orientation = o.orientation || [1, 0, 0]; + self._stereo = o.stereo || null; + self._pos = o.pos || null; + self._pannerAttr = { + coneInnerAngle: typeof o.coneInnerAngle !== 'undefined' ? o.coneInnerAngle : 360, + coneOuterAngle: typeof o.coneOuterAngle !== 'undefined' ? o.coneOuterAngle : 360, + coneOuterGain: typeof o.coneOuterGain !== 'undefined' ? o.coneOuterGain : 0, + distanceModel: typeof o.distanceModel !== 'undefined' ? o.distanceModel : 'inverse', + maxDistance: typeof o.maxDistance !== 'undefined' ? o.maxDistance : 10000, + panningModel: typeof o.panningModel !== 'undefined' ? o.panningModel : 'HRTF', + refDistance: typeof o.refDistance !== 'undefined' ? o.refDistance : 1, + rolloffFactor: typeof o.rolloffFactor !== 'undefined' ? o.rolloffFactor : 1 + }; + + // Setup event listeners. + self._onstereo = o.onstereo ? [{fn: o.onstereo}] : []; + self._onpos = o.onpos ? [{fn: o.onpos}] : []; + self._onorientation = o.onorientation ? [{fn: o.onorientation}] : []; + + // Complete initilization with howler.js core's init function. + return _super.call(this, o); + }; + })(Howl.prototype.init); + + /** + * Get/set the stereo panning of the audio source for this sound or all in the group. + * @param {Number} pan A value of -1.0 is all the way left and 1.0 is all the way right. + * @param {Number} id (optional) The sound ID. If none is passed, all in group will be updated. + * @return {Howl/Number} Returns self or the current stereo panning value. + */ + Howl.prototype.stereo = function(pan, id) { + var self = this; + + // Stop right here if not using Web Audio. + if (!self._webAudio) { + return self; + } + + // If the sound hasn't loaded, add it to the load queue to change stereo pan when capable. + if (self._state !== 'loaded') { + self._queue.push({ + event: 'stereo', + action: function() { + self.stereo(pan, id); + } + }); + + return self; + } + + // Check for PannerStereoNode support and fallback to PannerNode if it doesn't exist. + var pannerType = (typeof Howler.ctx.createStereoPanner === 'undefined') ? 'spatial' : 'stereo'; + + // Setup the group's stereo panning if no ID is passed. + if (typeof id === 'undefined') { + // Return the group's stereo panning if no parameters are passed. + if (typeof pan === 'number') { + self._stereo = pan; + self._pos = [pan, 0, 0]; + } else { + return self._stereo; + } + } + + // Change the streo panning of one or all sounds in group. + var ids = self._getSoundIds(id); + for (var i=0; i Returns the group's values. + * pannerAttr(id) -> Returns the sound id's values. + * pannerAttr(o) -> Set's the values of all sounds in this Howl group. + * pannerAttr(o, id) -> Set's the values of passed sound id. + * + * Attributes: + * coneInnerAngle - (360 by default) A parameter for directional audio sources, this is an angle, in degrees, + * inside of which there will be no volume reduction. + * coneOuterAngle - (360 by default) A parameter for directional audio sources, this is an angle, in degrees, + * outside of which the volume will be reduced to a constant value of `coneOuterGain`. + * coneOuterGain - (0 by default) A parameter for directional audio sources, this is the gain outside of the + * `coneOuterAngle`. It is a linear value in the range `[0, 1]`. + * distanceModel - ('inverse' by default) Determines algorithm used to reduce volume as audio moves away from + * listener. Can be `linear`, `inverse` or `exponential. + * maxDistance - (10000 by default) The maximum distance between source and listener, after which the volume + * will not be reduced any further. + * refDistance - (1 by default) A reference distance for reducing volume as source moves further from the listener. + * This is simply a variable of the distance model and has a different effect depending on which model + * is used and the scale of your coordinates. Generally, volume will be equal to 1 at this distance. + * rolloffFactor - (1 by default) How quickly the volume reduces as source moves from listener. This is simply a + * variable of the distance model and can be in the range of `[0, 1]` with `linear` and `[0, ∞]` + * with `inverse` and `exponential`. + * panningModel - ('HRTF' by default) Determines which spatialization algorithm is used to position audio. + * Can be `HRTF` or `equalpower`. + * + * @return {Howl/Object} Returns self or current panner attributes. + */ + Howl.prototype.pannerAttr = function() { + var self = this; + var args = arguments; + var o, id, sound; + + // Stop right here if not using Web Audio. + if (!self._webAudio) { + return self; + } + + // Determine the values based on arguments. + if (args.length === 0) { + // Return the group's panner attribute values. + return self._pannerAttr; + } else if (args.length === 1) { + if (typeof args[0] === 'object') { + o = args[0]; + + // Set the grou's panner attribute values. + if (typeof id === 'undefined') { + if (!o.pannerAttr) { + o.pannerAttr = { + coneInnerAngle: o.coneInnerAngle, + coneOuterAngle: o.coneOuterAngle, + coneOuterGain: o.coneOuterGain, + distanceModel: o.distanceModel, + maxDistance: o.maxDistance, + refDistance: o.refDistance, + rolloffFactor: o.rolloffFactor, + panningModel: o.panningModel + }; + } + + self._pannerAttr = { + coneInnerAngle: typeof o.pannerAttr.coneInnerAngle !== 'undefined' ? o.pannerAttr.coneInnerAngle : self._coneInnerAngle, + coneOuterAngle: typeof o.pannerAttr.coneOuterAngle !== 'undefined' ? o.pannerAttr.coneOuterAngle : self._coneOuterAngle, + coneOuterGain: typeof o.pannerAttr.coneOuterGain !== 'undefined' ? o.pannerAttr.coneOuterGain : self._coneOuterGain, + distanceModel: typeof o.pannerAttr.distanceModel !== 'undefined' ? o.pannerAttr.distanceModel : self._distanceModel, + maxDistance: typeof o.pannerAttr.maxDistance !== 'undefined' ? o.pannerAttr.maxDistance : self._maxDistance, + refDistance: typeof o.pannerAttr.refDistance !== 'undefined' ? o.pannerAttr.refDistance : self._refDistance, + rolloffFactor: typeof o.pannerAttr.rolloffFactor !== 'undefined' ? o.pannerAttr.rolloffFactor : self._rolloffFactor, + panningModel: typeof o.pannerAttr.panningModel !== 'undefined' ? o.pannerAttr.panningModel : self._panningModel + }; + } + } else { + // Return this sound's panner attribute values. + sound = self._soundById(parseInt(args[0], 10)); + return sound ? sound._pannerAttr : self._pannerAttr; + } + } else if (args.length === 2) { + o = args[0]; + id = parseInt(args[1], 10); + } + + // Update the values of the specified sounds. + var ids = self._getSoundIds(id); + for (var i=0; i= 0 && vol <= 1) { + self._volume = vol; + + // Don't update any of the nodes if we are muted. + if (self._muted) { + return self; + } + + // When using Web Audio, we just need to adjust the master gain. + if (self.usingWebAudio) { + self.masterGain.gain.setValueAtTime(vol, Howler.ctx.currentTime); + } + + // Loop through and change volume for all HTML5 audio nodes. + for (var i=0; i=0; i--) { + self._howls[i].unload(); + } + + // Create a new AudioContext to make sure it is fully reset. + if (self.usingWebAudio && self.ctx && typeof self.ctx.close !== 'undefined') { + self.ctx.close(); + self.ctx = null; + setupAudioContext(); + } + + return self; + }, + + /** + * Check for codec support of specific extension. + * @param {String} ext Audio file extention. + * @return {Boolean} + */ + codecs: function(ext) { + return (this || Howler)._codecs[ext.replace(/^x-/, '')]; + }, + + /** + * Setup various state values for global tracking. + * @return {Howler} + */ + _setup: function() { + var self = this || Howler; + + // Keeps track of the suspend/resume state of the AudioContext. + self.state = self.ctx ? self.ctx.state || 'suspended' : 'suspended'; + + // Automatically begin the 30-second suspend process + self._autoSuspend(); + + // Check if audio is available. + if (!self.usingWebAudio) { + // No audio is available on this system if noAudio is set to true. + if (typeof Audio !== 'undefined') { + try { + var test = new Audio(); + + // Check if the canplaythrough event is available. + if (typeof test.oncanplaythrough === 'undefined') { + self._canPlayEvent = 'canplay'; + } + } catch(e) { + self.noAudio = true; + } + } else { + self.noAudio = true; + } + } + + // Test to make sure audio isn't disabled in Internet Explorer. + try { + var test = new Audio(); + if (test.muted) { + self.noAudio = true; + } + } catch (e) {} + + // Check for supported codecs. + if (!self.noAudio) { + self._setupCodecs(); + } + + return self; + }, + + /** + * Check for browser support for various codecs and cache the results. + * @return {Howler} + */ + _setupCodecs: function() { + var self = this || Howler; + var audioTest = null; + + // Must wrap in a try/catch because IE11 in server mode throws an error. + try { + audioTest = (typeof Audio !== 'undefined') ? new Audio() : null; + } catch (err) { + return self; + } + + if (!audioTest || typeof audioTest.canPlayType !== 'function') { + return self; + } + + var mpegTest = audioTest.canPlayType('audio/mpeg;').replace(/^no$/, ''); + + // Opera version <33 has mixed MP3 support, so we need to check for and block it. + var ua = self._navigator ? self._navigator.userAgent : ''; + var checkOpera = ua.match(/OPR\/(\d+)/g); + var isOldOpera = (checkOpera && parseInt(checkOpera[0].split('/')[1], 10) < 33); + var checkSafari = ua.indexOf('Safari') !== -1 && ua.indexOf('Chrome') === -1; + var safariVersion = ua.match(/Version\/(.*?) /); + var isOldSafari = (checkSafari && safariVersion && parseInt(safariVersion[1], 10) < 15); + + self._codecs = { + mp3: !!(!isOldOpera && (mpegTest || audioTest.canPlayType('audio/mp3;').replace(/^no$/, ''))), + mpeg: !!mpegTest, + opus: !!audioTest.canPlayType('audio/ogg; codecs="opus"').replace(/^no$/, ''), + ogg: !!audioTest.canPlayType('audio/ogg; codecs="vorbis"').replace(/^no$/, ''), + oga: !!audioTest.canPlayType('audio/ogg; codecs="vorbis"').replace(/^no$/, ''), + wav: !!(audioTest.canPlayType('audio/wav; codecs="1"') || audioTest.canPlayType('audio/wav')).replace(/^no$/, ''), + aac: !!audioTest.canPlayType('audio/aac;').replace(/^no$/, ''), + caf: !!audioTest.canPlayType('audio/x-caf;').replace(/^no$/, ''), + m4a: !!(audioTest.canPlayType('audio/x-m4a;') || audioTest.canPlayType('audio/m4a;') || audioTest.canPlayType('audio/aac;')).replace(/^no$/, ''), + m4b: !!(audioTest.canPlayType('audio/x-m4b;') || audioTest.canPlayType('audio/m4b;') || audioTest.canPlayType('audio/aac;')).replace(/^no$/, ''), + mp4: !!(audioTest.canPlayType('audio/x-mp4;') || audioTest.canPlayType('audio/mp4;') || audioTest.canPlayType('audio/aac;')).replace(/^no$/, ''), + weba: !!(!isOldSafari && audioTest.canPlayType('audio/webm; codecs="vorbis"').replace(/^no$/, '')), + webm: !!(!isOldSafari && audioTest.canPlayType('audio/webm; codecs="vorbis"').replace(/^no$/, '')), + dolby: !!audioTest.canPlayType('audio/mp4; codecs="ec-3"').replace(/^no$/, ''), + flac: !!(audioTest.canPlayType('audio/x-flac;') || audioTest.canPlayType('audio/flac;')).replace(/^no$/, '') + }; + + return self; + }, + + /** + * Some browsers/devices will only allow audio to be played after a user interaction. + * Attempt to automatically unlock audio on the first user interaction. + * Concept from: http://paulbakaus.com/tutorials/html5/web-audio-on-ios/ + * @return {Howler} + */ + _unlockAudio: function() { + var self = this || Howler; + + // Only run this if Web Audio is supported and it hasn't already been unlocked. + if (self._audioUnlocked || !self.ctx) { + return; + } + + self._audioUnlocked = false; + self.autoUnlock = false; + + // Some mobile devices/platforms have distortion issues when opening/closing tabs and/or web views. + // Bugs in the browser (especially Mobile Safari) can cause the sampleRate to change from 44100 to 48000. + // By calling Howler.unload(), we create a new AudioContext with the correct sampleRate. + if (!self._mobileUnloaded && self.ctx.sampleRate !== 44100) { + self._mobileUnloaded = true; + self.unload(); + } + + // Scratch buffer for enabling iOS to dispose of web audio buffers correctly, as per: + // http://stackoverflow.com/questions/24119684 + self._scratchBuffer = self.ctx.createBuffer(1, 1, 22050); + + // Call this method on touch start to create and play a buffer, + // then check if the audio actually played to determine if + // audio has now been unlocked on iOS, Android, etc. + var unlock = function(e) { + // Create a pool of unlocked HTML5 Audio objects that can + // be used for playing sounds without user interaction. HTML5 + // Audio objects must be individually unlocked, as opposed + // to the WebAudio API which only needs a single activation. + // This must occur before WebAudio setup or the source.onended + // event will not fire. + while (self._html5AudioPool.length < self.html5PoolSize) { + try { + var audioNode = new Audio(); + + // Mark this Audio object as unlocked to ensure it can get returned + // to the unlocked pool when released. + audioNode._unlocked = true; + + // Add the audio node to the pool. + self._releaseHtml5Audio(audioNode); + } catch (e) { + self.noAudio = true; + break; + } + } + + // Loop through any assigned audio nodes and unlock them. + for (var i=0; i= 55. + if (typeof self.ctx.resume === 'function') { + self.ctx.resume(); + } + + // Setup a timeout to check that we are unlocked on the next event loop. + source.onended = function() { + source.disconnect(0); + + // Update the unlocked state and prevent this check from happening again. + self._audioUnlocked = true; + + // Remove the touch start listener. + document.removeEventListener('touchstart', unlock, true); + document.removeEventListener('touchend', unlock, true); + document.removeEventListener('click', unlock, true); + document.removeEventListener('keydown', unlock, true); + + // Let all sounds know that audio has been unlocked. + for (var i=0; i 0 ? sound._seek : self._sprite[sprite][0] / 1000); + var duration = Math.max(0, ((self._sprite[sprite][0] + self._sprite[sprite][1]) / 1000) - seek); + var timeout = (duration * 1000) / Math.abs(sound._rate); + var start = self._sprite[sprite][0] / 1000; + var stop = (self._sprite[sprite][0] + self._sprite[sprite][1]) / 1000; + sound._sprite = sprite; + + // Mark the sound as ended instantly so that this async playback + // doesn't get grabbed by another call to play while this one waits to start. + sound._ended = false; + + // Update the parameters of the sound. + var setParams = function() { + sound._paused = false; + sound._seek = seek; + sound._start = start; + sound._stop = stop; + sound._loop = !!(sound._loop || self._sprite[sprite][2]); + }; + + // End the sound instantly if seek is at the end. + if (seek >= stop) { + self._ended(sound); + return; + } + + // Begin the actual playback. + var node = sound._node; + if (self._webAudio) { + // Fire this when the sound is ready to play to begin Web Audio playback. + var playWebAudio = function() { + self._playLock = false; + setParams(); + self._refreshBuffer(sound); + + // Setup the playback params. + var vol = (sound._muted || self._muted) ? 0 : sound._volume; + node.gain.setValueAtTime(vol, Howler.ctx.currentTime); + sound._playStart = Howler.ctx.currentTime; + + // Play the sound using the supported method. + if (typeof node.bufferSource.start === 'undefined') { + sound._loop ? node.bufferSource.noteGrainOn(0, seek, 86400) : node.bufferSource.noteGrainOn(0, seek, duration); + } else { + sound._loop ? node.bufferSource.start(0, seek, 86400) : node.bufferSource.start(0, seek, duration); + } + + // Start a new timer if none is present. + if (timeout !== Infinity) { + self._endTimers[sound._id] = setTimeout(self._ended.bind(self, sound), timeout); + } + + if (!internal) { + setTimeout(function() { + self._emit('play', sound._id); + self._loadQueue(); + }, 0); + } + }; + + if (Howler.state === 'running' && Howler.ctx.state !== 'interrupted') { + playWebAudio(); + } else { + self._playLock = true; + + // Wait for the audio context to resume before playing. + self.once('resume', playWebAudio); + + // Cancel the end timer. + self._clearTimer(sound._id); + } + } else { + // Fire this when the sound is ready to play to begin HTML5 Audio playback. + var playHtml5 = function() { + node.currentTime = seek; + node.muted = sound._muted || self._muted || Howler._muted || node.muted; + node.volume = sound._volume * Howler.volume(); + node.playbackRate = sound._rate; + + // Some browsers will throw an error if this is called without user interaction. + try { + var play = node.play(); + + // Support older browsers that don't support promises, and thus don't have this issue. + if (play && typeof Promise !== 'undefined' && (play instanceof Promise || typeof play.then === 'function')) { + // Implements a lock to prevent DOMException: The play() request was interrupted by a call to pause(). + self._playLock = true; + + // Set param values immediately. + setParams(); + + // Releases the lock and executes queued actions. + play + .then(function() { + self._playLock = false; + node._unlocked = true; + if (!internal) { + self._emit('play', sound._id); + } else { + self._loadQueue(); + } + }) + .catch(function() { + self._playLock = false; + self._emit('playerror', sound._id, 'Playback was unable to start. This is most commonly an issue ' + + 'on mobile devices and Chrome where playback was not within a user interaction.'); + + // Reset the ended and paused values. + sound._ended = true; + sound._paused = true; + }); + } else if (!internal) { + self._playLock = false; + setParams(); + self._emit('play', sound._id); + } + + // Setting rate before playing won't work in IE, so we set it again here. + node.playbackRate = sound._rate; + + // If the node is still paused, then we can assume there was a playback issue. + if (node.paused) { + self._emit('playerror', sound._id, 'Playback was unable to start. This is most commonly an issue ' + + 'on mobile devices and Chrome where playback was not within a user interaction.'); + return; + } + + // Setup the end timer on sprites or listen for the ended event. + if (sprite !== '__default' || sound._loop) { + self._endTimers[sound._id] = setTimeout(self._ended.bind(self, sound), timeout); + } else { + self._endTimers[sound._id] = function() { + // Fire ended on this audio node. + self._ended(sound); + + // Clear this listener. + node.removeEventListener('ended', self._endTimers[sound._id], false); + }; + node.addEventListener('ended', self._endTimers[sound._id], false); + } + } catch (err) { + self._emit('playerror', sound._id, err); + } + }; + + // If this is streaming audio, make sure the src is set and load again. + if (node.src === 'data:audio/wav;base64,UklGRigAAABXQVZFZm10IBIAAAABAAEARKwAAIhYAQACABAAAABkYXRhAgAAAAEA') { + node.src = self._src; + node.load(); + } + + // Play immediately if ready, or wait for the 'canplaythrough'e vent. + var loadedNoReadyState = (window && window.ejecta) || (!node.readyState && Howler._navigator.isCocoonJS); + if (node.readyState >= 3 || loadedNoReadyState) { + playHtml5(); + } else { + self._playLock = true; + self._state = 'loading'; + + var listener = function() { + self._state = 'loaded'; + + // Begin playback. + playHtml5(); + + // Clear this listener. + node.removeEventListener(Howler._canPlayEvent, listener, false); + }; + node.addEventListener(Howler._canPlayEvent, listener, false); + + // Cancel the end timer. + self._clearTimer(sound._id); + } + } + + return sound._id; + }, + + /** + * Pause playback and save current position. + * @param {Number} id The sound ID (empty to pause all in group). + * @return {Howl} + */ + pause: function(id) { + var self = this; + + // If the sound hasn't loaded or a play() promise is pending, add it to the load queue to pause when capable. + if (self._state !== 'loaded' || self._playLock) { + self._queue.push({ + event: 'pause', + action: function() { + self.pause(id); + } + }); + + return self; + } + + // If no id is passed, get all ID's to be paused. + var ids = self._getSoundIds(id); + + for (var i=0; i Returns the group's volume value. + * volume(id) -> Returns the sound id's current volume. + * volume(vol) -> Sets the volume of all sounds in this Howl group. + * volume(vol, id) -> Sets the volume of passed sound id. + * @return {Howl/Number} Returns self or current volume. + */ + volume: function() { + var self = this; + var args = arguments; + var vol, id; + + // Determine the values based on arguments. + if (args.length === 0) { + // Return the value of the groups' volume. + return self._volume; + } else if (args.length === 1 || args.length === 2 && typeof args[1] === 'undefined') { + // First check if this is an ID, and if not, assume it is a new volume. + var ids = self._getSoundIds(); + var index = ids.indexOf(args[0]); + if (index >= 0) { + id = parseInt(args[0], 10); + } else { + vol = parseFloat(args[0]); + } + } else if (args.length >= 2) { + vol = parseFloat(args[0]); + id = parseInt(args[1], 10); + } + + // Update the volume or return the current volume. + var sound; + if (typeof vol !== 'undefined' && vol >= 0 && vol <= 1) { + // If the sound hasn't loaded, add it to the load queue to change volume when capable. + if (self._state !== 'loaded'|| self._playLock) { + self._queue.push({ + event: 'volume', + action: function() { + self.volume.apply(self, args); + } + }); + + return self; + } + + // Set the group volume. + if (typeof id === 'undefined') { + self._volume = vol; + } + + // Update one or all volumes. + id = self._getSoundIds(id); + for (var i=0; i 0) ? len / steps : len); + var lastTick = Date.now(); + + // Store the value being faded to. + sound._fadeTo = to; + + // Update the volume value on each interval tick. + sound._interval = setInterval(function() { + // Update the volume based on the time since the last tick. + var tick = (Date.now() - lastTick) / len; + lastTick = Date.now(); + vol += diff * tick; + + // Round to within 2 decimal points. + vol = Math.round(vol * 100) / 100; + + // Make sure the volume is in the right bounds. + if (diff < 0) { + vol = Math.max(to, vol); + } else { + vol = Math.min(to, vol); + } + + // Change the volume. + if (self._webAudio) { + sound._volume = vol; + } else { + self.volume(vol, sound._id, true); + } + + // Set the group's volume. + if (isGroup) { + self._volume = vol; + } + + // When the fade is complete, stop it and fire event. + if ((to < from && vol <= to) || (to > from && vol >= to)) { + clearInterval(sound._interval); + sound._interval = null; + sound._fadeTo = null; + self.volume(to, sound._id); + self._emit('fade', sound._id); + } + }, stepLen); + }, + + /** + * Internal method that stops the currently playing fade when + * a new fade starts, volume is changed or the sound is stopped. + * @param {Number} id The sound id. + * @return {Howl} + */ + _stopFade: function(id) { + var self = this; + var sound = self._soundById(id); + + if (sound && sound._interval) { + if (self._webAudio) { + sound._node.gain.cancelScheduledValues(Howler.ctx.currentTime); + } + + clearInterval(sound._interval); + sound._interval = null; + self.volume(sound._fadeTo, id); + sound._fadeTo = null; + self._emit('fade', id); + } + + return self; + }, + + /** + * Get/set the loop parameter on a sound. This method can optionally take 0, 1 or 2 arguments. + * loop() -> Returns the group's loop value. + * loop(id) -> Returns the sound id's loop value. + * loop(loop) -> Sets the loop value for all sounds in this Howl group. + * loop(loop, id) -> Sets the loop value of passed sound id. + * @return {Howl/Boolean} Returns self or current loop value. + */ + loop: function() { + var self = this; + var args = arguments; + var loop, id, sound; + + // Determine the values for loop and id. + if (args.length === 0) { + // Return the grou's loop value. + return self._loop; + } else if (args.length === 1) { + if (typeof args[0] === 'boolean') { + loop = args[0]; + self._loop = loop; + } else { + // Return this sound's loop value. + sound = self._soundById(parseInt(args[0], 10)); + return sound ? sound._loop : false; + } + } else if (args.length === 2) { + loop = args[0]; + id = parseInt(args[1], 10); + } + + // If no id is passed, get all ID's to be looped. + var ids = self._getSoundIds(id); + for (var i=0; i Returns the first sound node's current playback rate. + * rate(id) -> Returns the sound id's current playback rate. + * rate(rate) -> Sets the playback rate of all sounds in this Howl group. + * rate(rate, id) -> Sets the playback rate of passed sound id. + * @return {Howl/Number} Returns self or the current playback rate. + */ + rate: function() { + var self = this; + var args = arguments; + var rate, id; + + // Determine the values based on arguments. + if (args.length === 0) { + // We will simply return the current rate of the first node. + id = self._sounds[0]._id; + } else if (args.length === 1) { + // First check if this is an ID, and if not, assume it is a new rate value. + var ids = self._getSoundIds(); + var index = ids.indexOf(args[0]); + if (index >= 0) { + id = parseInt(args[0], 10); + } else { + rate = parseFloat(args[0]); + } + } else if (args.length === 2) { + rate = parseFloat(args[0]); + id = parseInt(args[1], 10); + } + + // Update the playback rate or return the current value. + var sound; + if (typeof rate === 'number') { + // If the sound hasn't loaded, add it to the load queue to change playback rate when capable. + if (self._state !== 'loaded' || self._playLock) { + self._queue.push({ + event: 'rate', + action: function() { + self.rate.apply(self, args); + } + }); + + return self; + } + + // Set the group rate. + if (typeof id === 'undefined') { + self._rate = rate; + } + + // Update one or all volumes. + id = self._getSoundIds(id); + for (var i=0; i Returns the first sound node's current seek position. + * seek(id) -> Returns the sound id's current seek position. + * seek(seek) -> Sets the seek position of the first sound node. + * seek(seek, id) -> Sets the seek position of passed sound id. + * @return {Howl/Number} Returns self or the current seek position. + */ + seek: function() { + var self = this; + var args = arguments; + var seek, id; + + // Determine the values based on arguments. + if (args.length === 0) { + // We will simply return the current position of the first node. + if (self._sounds.length) { + id = self._sounds[0]._id; + } + } else if (args.length === 1) { + // First check if this is an ID, and if not, assume it is a new seek position. + var ids = self._getSoundIds(); + var index = ids.indexOf(args[0]); + if (index >= 0) { + id = parseInt(args[0], 10); + } else if (self._sounds.length) { + id = self._sounds[0]._id; + seek = parseFloat(args[0]); + } + } else if (args.length === 2) { + seek = parseFloat(args[0]); + id = parseInt(args[1], 10); + } + + // If there is no ID, bail out. + if (typeof id === 'undefined') { + return 0; + } + + // If the sound hasn't loaded, add it to the load queue to seek when capable. + if (typeof seek === 'number' && (self._state !== 'loaded' || self._playLock)) { + self._queue.push({ + event: 'seek', + action: function() { + self.seek.apply(self, args); + } + }); + + return self; + } + + // Get the sound. + var sound = self._soundById(id); + + if (sound) { + if (typeof seek === 'number' && seek >= 0) { + // Pause the sound and update position for restarting playback. + var playing = self.playing(id); + if (playing) { + self.pause(id, true); + } + + // Move the position of the track and cancel timer. + sound._seek = seek; + sound._ended = false; + self._clearTimer(id); + + // Update the seek position for HTML5 Audio. + if (!self._webAudio && sound._node && !isNaN(sound._node.duration)) { + sound._node.currentTime = seek; + } + + // Seek and emit when ready. + var seekAndEmit = function() { + // Restart the playback if the sound was playing. + if (playing) { + self.play(id, true); + } + + self._emit('seek', id); + }; + + // Wait for the play lock to be unset before emitting (HTML5 Audio). + if (playing && !self._webAudio) { + var emitSeek = function() { + if (!self._playLock) { + seekAndEmit(); + } else { + setTimeout(emitSeek, 0); + } + }; + setTimeout(emitSeek, 0); + } else { + seekAndEmit(); + } + } else { + if (self._webAudio) { + var realTime = self.playing(id) ? Howler.ctx.currentTime - sound._playStart : 0; + var rateSeek = sound._rateSeek ? sound._rateSeek - sound._seek : 0; + return sound._seek + (rateSeek + realTime * Math.abs(sound._rate)); + } else { + return sound._node.currentTime; + } + } + } + + return self; + }, + + /** + * Check if a specific sound is currently playing or not (if id is provided), or check if at least one of the sounds in the group is playing or not. + * @param {Number} id The sound id to check. If none is passed, the whole sound group is checked. + * @return {Boolean} True if playing and false if not. + */ + playing: function(id) { + var self = this; + + // Check the passed sound ID (if any). + if (typeof id === 'number') { + var sound = self._soundById(id); + return sound ? !sound._paused : false; + } + + // Otherwise, loop through all sounds and check if any are playing. + for (var i=0; i= 0) { + Howler._howls.splice(index, 1); + } + + // Delete this sound from the cache (if no other Howl is using it). + var remCache = true; + for (i=0; i= 0) { + remCache = false; + break; + } + } + + if (cache && remCache) { + delete cache[self._src]; + } + + // Clear global errors. + Howler.noAudio = false; + + // Clear out `self`. + self._state = 'unloaded'; + self._sounds = []; + self = null; + + return null; + }, + + /** + * Listen to a custom event. + * @param {String} event Event name. + * @param {Function} fn Listener to call. + * @param {Number} id (optional) Only listen to events for this sound. + * @param {Number} once (INTERNAL) Marks event to fire only once. + * @return {Howl} + */ + on: function(event, fn, id, once) { + var self = this; + var events = self['_on' + event]; + + if (typeof fn === 'function') { + events.push(once ? {id: id, fn: fn, once: once} : {id: id, fn: fn}); + } + + return self; + }, + + /** + * Remove a custom event. Call without parameters to remove all events. + * @param {String} event Event name. + * @param {Function} fn Listener to remove. Leave empty to remove all. + * @param {Number} id (optional) Only remove events for this sound. + * @return {Howl} + */ + off: function(event, fn, id) { + var self = this; + var events = self['_on' + event]; + var i = 0; + + // Allow passing just an event and ID. + if (typeof fn === 'number') { + id = fn; + fn = null; + } + + if (fn || id) { + // Loop through event store and remove the passed function. + for (i=0; i=0; i--) { + // Only fire the listener if the correct ID is used. + if (!events[i].id || events[i].id === id || event === 'load') { + setTimeout(function(fn) { + fn.call(this, id, msg); + }.bind(self, events[i].fn), 0); + + // If this event was setup with `once`, remove it. + if (events[i].once) { + self.off(event, events[i].fn, events[i].id); + } + } + } + + // Pass the event type into load queue so that it can continue stepping. + self._loadQueue(event); + + return self; + }, + + /** + * Queue of actions initiated before the sound has loaded. + * These will be called in sequence, with the next only firing + * after the previous has finished executing (even if async like play). + * @return {Howl} + */ + _loadQueue: function(event) { + var self = this; + + if (self._queue.length > 0) { + var task = self._queue[0]; + + // Remove this task if a matching event was passed. + if (task.event === event) { + self._queue.shift(); + self._loadQueue(); + } + + // Run the task if no event type is passed. + if (!event) { + task.action(); + } + } + + return self; + }, + + /** + * Fired when playback ends at the end of the duration. + * @param {Sound} sound The sound object to work with. + * @return {Howl} + */ + _ended: function(sound) { + var self = this; + var sprite = sound._sprite; + + // If we are using IE and there was network latency we may be clipping + // audio before it completes playing. Lets check the node to make sure it + // believes it has completed, before ending the playback. + if (!self._webAudio && sound._node && !sound._node.paused && !sound._node.ended && sound._node.currentTime < sound._stop) { + setTimeout(self._ended.bind(self, sound), 100); + return self; + } + + // Should this sound loop? + var loop = !!(sound._loop || self._sprite[sprite][2]); + + // Fire the ended event. + self._emit('end', sound._id); + + // Restart the playback for HTML5 Audio loop. + if (!self._webAudio && loop) { + self.stop(sound._id, true).play(sound._id); + } + + // Restart this timer if on a Web Audio loop. + if (self._webAudio && loop) { + self._emit('play', sound._id); + sound._seek = sound._start || 0; + sound._rateSeek = 0; + sound._playStart = Howler.ctx.currentTime; + + var timeout = ((sound._stop - sound._start) * 1000) / Math.abs(sound._rate); + self._endTimers[sound._id] = setTimeout(self._ended.bind(self, sound), timeout); + } + + // Mark the node as paused. + if (self._webAudio && !loop) { + sound._paused = true; + sound._ended = true; + sound._seek = sound._start || 0; + sound._rateSeek = 0; + self._clearTimer(sound._id); + + // Clean up the buffer source. + self._cleanBuffer(sound._node); + + // Attempt to auto-suspend AudioContext if no sounds are still playing. + Howler._autoSuspend(); + } + + // When using a sprite, end the track. + if (!self._webAudio && !loop) { + self.stop(sound._id, true); + } + + return self; + }, + + /** + * Clear the end timer for a sound playback. + * @param {Number} id The sound ID. + * @return {Howl} + */ + _clearTimer: function(id) { + var self = this; + + if (self._endTimers[id]) { + // Clear the timeout or remove the ended listener. + if (typeof self._endTimers[id] !== 'function') { + clearTimeout(self._endTimers[id]); + } else { + var sound = self._soundById(id); + if (sound && sound._node) { + sound._node.removeEventListener('ended', self._endTimers[id], false); + } + } + + delete self._endTimers[id]; + } + + return self; + }, + + /** + * Return the sound identified by this ID, or return null. + * @param {Number} id Sound ID + * @return {Object} Sound object or null. + */ + _soundById: function(id) { + var self = this; + + // Loop through all sounds and find the one with this ID. + for (var i=0; i=0; i--) { + if (cnt <= limit) { + return; + } + + if (self._sounds[i]._ended) { + // Disconnect the audio source when using Web Audio. + if (self._webAudio && self._sounds[i]._node) { + self._sounds[i]._node.disconnect(0); + } + + // Remove sounds until we have the pool size. + self._sounds.splice(i, 1); + cnt--; + } + } + }, + + /** + * Get all ID's from the sounds pool. + * @param {Number} id Only return one ID if one is passed. + * @return {Array} Array of IDs. + */ + _getSoundIds: function(id) { + var self = this; + + if (typeof id === 'undefined') { + var ids = []; + for (var i=0; i= 0; + + if (!node.bufferSource) { + return self; + } + + if (Howler._scratchBuffer && node.bufferSource) { + node.bufferSource.onended = null; + node.bufferSource.disconnect(0); + if (isIOS) { + try { node.bufferSource.buffer = Howler._scratchBuffer; } catch(e) {} + } + } + node.bufferSource = null; + + return self; + }, + + /** + * Set the source to a 0-second silence to stop any downloading (except in IE). + * @param {Object} node Audio node to clear. + */ + _clearSound: function(node) { + var checkIE = /MSIE |Trident\//.test(Howler._navigator && Howler._navigator.userAgent); + if (!checkIE) { + node.src = 'data:audio/wav;base64,UklGRigAAABXQVZFZm10IBIAAAABAAEARKwAAIhYAQACABAAAABkYXRhAgAAAAEA'; + } + } + }; + + /** Single Sound Methods **/ + /***************************************************************************/ + + /** + * Setup the sound object, which each node attached to a Howl group is contained in. + * @param {Object} howl The Howl parent group. + */ + var Sound = function(howl) { + this._parent = howl; + this.init(); + }; + Sound.prototype = { + /** + * Initialize a new Sound object. + * @return {Sound} + */ + init: function() { + var self = this; + var parent = self._parent; + + // Setup the default parameters. + self._muted = parent._muted; + self._loop = parent._loop; + self._volume = parent._volume; + self._rate = parent._rate; + self._seek = 0; + self._paused = true; + self._ended = true; + self._sprite = '__default'; + + // Generate a unique ID for this sound. + self._id = ++Howler._counter; + + // Add itself to the parent's pool. + parent._sounds.push(self); + + // Create the new node. + self.create(); + + return self; + }, + + /** + * Create and setup a new sound object, whether HTML5 Audio or Web Audio. + * @return {Sound} + */ + create: function() { + var self = this; + var parent = self._parent; + var volume = (Howler._muted || self._muted || self._parent._muted) ? 0 : self._volume; + + if (parent._webAudio) { + // Create the gain node for controlling volume (the source will connect to this). + self._node = (typeof Howler.ctx.createGain === 'undefined') ? Howler.ctx.createGainNode() : Howler.ctx.createGain(); + self._node.gain.setValueAtTime(volume, Howler.ctx.currentTime); + self._node.paused = true; + self._node.connect(Howler.masterGain); + } else if (!Howler.noAudio) { + // Get an unlocked Audio object from the pool. + self._node = Howler._obtainHtml5Audio(); + + // Listen for errors (http://dev.w3.org/html5/spec-author-view/spec.html#mediaerror). + self._errorFn = self._errorListener.bind(self); + self._node.addEventListener('error', self._errorFn, false); + + // Listen for 'canplaythrough' event to let us know the sound is ready. + self._loadFn = self._loadListener.bind(self); + self._node.addEventListener(Howler._canPlayEvent, self._loadFn, false); + + // Listen for the 'ended' event on the sound to account for edge-case where + // a finite sound has a duration of Infinity. + self._endFn = self._endListener.bind(self); + self._node.addEventListener('ended', self._endFn, false); + + // Setup the new audio node. + self._node.src = parent._src; + self._node.preload = parent._preload === true ? 'auto' : parent._preload; + self._node.volume = volume * Howler.volume(); + + // Begin loading the source. + self._node.load(); + } + + return self; + }, + + /** + * Reset the parameters of this sound to the original state (for recycle). + * @return {Sound} + */ + reset: function() { + var self = this; + var parent = self._parent; + + // Reset all of the parameters of this sound. + self._muted = parent._muted; + self._loop = parent._loop; + self._volume = parent._volume; + self._rate = parent._rate; + self._seek = 0; + self._rateSeek = 0; + self._paused = true; + self._ended = true; + self._sprite = '__default'; + + // Generate a new ID so that it isn't confused with the previous sound. + self._id = ++Howler._counter; + + return self; + }, + + /** + * HTML5 Audio error listener callback. + */ + _errorListener: function() { + var self = this; + + // Fire an error event and pass back the code. + self._parent._emit('loaderror', self._id, self._node.error ? self._node.error.code : 0); + + // Clear the event listener. + self._node.removeEventListener('error', self._errorFn, false); + }, + + /** + * HTML5 Audio canplaythrough listener callback. + */ + _loadListener: function() { + var self = this; + var parent = self._parent; + + // Round up the duration to account for the lower precision in HTML5 Audio. + parent._duration = Math.ceil(self._node.duration * 10) / 10; + + // Setup a sprite if none is defined. + if (Object.keys(parent._sprite).length === 0) { + parent._sprite = {__default: [0, parent._duration * 1000]}; + } + + if (parent._state !== 'loaded') { + parent._state = 'loaded'; + parent._emit('load'); + parent._loadQueue(); + } + + // Clear the event listener. + self._node.removeEventListener(Howler._canPlayEvent, self._loadFn, false); + }, + + /** + * HTML5 Audio ended listener callback. + */ + _endListener: function() { + var self = this; + var parent = self._parent; + + // Only handle the `ended`` event if the duration is Infinity. + if (parent._duration === Infinity) { + // Update the parent duration to match the real audio duration. + // Round up the duration to account for the lower precision in HTML5 Audio. + parent._duration = Math.ceil(self._node.duration * 10) / 10; + + // Update the sprite that corresponds to the real duration. + if (parent._sprite.__default[1] === Infinity) { + parent._sprite.__default[1] = parent._duration * 1000; + } + + // Run the regular ended method. + parent._ended(self); + } + + // Clear the event listener since the duration is now correct. + self._node.removeEventListener('ended', self._endFn, false); + } + }; + + /** Helper Methods **/ + /***************************************************************************/ + + var cache = {}; + + /** + * Buffer a sound from URL, Data URI or cache and decode to audio source (Web Audio API). + * @param {Howl} self + */ + var loadBuffer = function(self) { + var url = self._src; + + // Check if the buffer has already been cached and use it instead. + if (cache[url]) { + // Set the duration from the cache. + self._duration = cache[url].duration; + + // Load the sound into this Howl. + loadSound(self); + + return; + } + + if (/^data:[^;]+;base64,/.test(url)) { + // Decode the base64 data URI without XHR, since some browsers don't support it. + var data = atob(url.split(',')[1]); + var dataView = new Uint8Array(data.length); + for (var i=0; i 0) { + cache[self._src] = buffer; + loadSound(self, buffer); + } else { + error(); + } + }; + + // Decode the buffer into an audio source. + if (typeof Promise !== 'undefined' && Howler.ctx.decodeAudioData.length === 1) { + Howler.ctx.decodeAudioData(arraybuffer).then(success).catch(error); + } else { + Howler.ctx.decodeAudioData(arraybuffer, success, error); + } + } + + /** + * Sound is now loaded, so finish setting everything up and fire the loaded event. + * @param {Howl} self + * @param {Object} buffer The decoded buffer sound source. + */ + var loadSound = function(self, buffer) { + // Set the duration. + if (buffer && !self._duration) { + self._duration = buffer.duration; + } + + // Setup a sprite if none is defined. + if (Object.keys(self._sprite).length === 0) { + self._sprite = {__default: [0, self._duration * 1000]}; + } + + // Fire the loaded event. + if (self._state !== 'loaded') { + self._state = 'loaded'; + self._emit('load'); + self._loadQueue(); + } + }; + + /** + * Setup the audio context when available, or switch to HTML5 Audio mode. + */ + var setupAudioContext = function() { + // If we have already detected that Web Audio isn't supported, don't run this step again. + if (!Howler.usingWebAudio) { + return; + } + + // Check if we are using Web Audio and setup the AudioContext if we are. + try { + if (typeof AudioContext !== 'undefined') { + Howler.ctx = new AudioContext(); + } else if (typeof webkitAudioContext !== 'undefined') { + Howler.ctx = new webkitAudioContext(); + } else { + Howler.usingWebAudio = false; + } + } catch(e) { + Howler.usingWebAudio = false; + } + + // If the audio context creation still failed, set using web audio to false. + if (!Howler.ctx) { + Howler.usingWebAudio = false; + } + + // Check if a webview is being used on iOS8 or earlier (rather than the browser). + // If it is, disable Web Audio as it causes crashing. + var iOS = (/iP(hone|od|ad)/.test(Howler._navigator && Howler._navigator.platform)); + var appVersion = Howler._navigator && Howler._navigator.appVersion.match(/OS (\d+)_(\d+)_?(\d+)?/); + var version = appVersion ? parseInt(appVersion[1], 10) : null; + if (iOS && version && version < 9) { + var safari = /safari/.test(Howler._navigator && Howler._navigator.userAgent.toLowerCase()); + if (Howler._navigator && !safari) { + Howler.usingWebAudio = false; + } + } + + // Create and expose the master GainNode when using Web Audio (useful for plugins or advanced usage). + if (Howler.usingWebAudio) { + Howler.masterGain = (typeof Howler.ctx.createGain === 'undefined') ? Howler.ctx.createGainNode() : Howler.ctx.createGain(); + Howler.masterGain.gain.setValueAtTime(Howler._muted ? 0 : Howler._volume, Howler.ctx.currentTime); + Howler.masterGain.connect(Howler.ctx.destination); + } + + // Re-run the setup on Howler. + Howler._setup(); + }; + + // Add support for AMD (Asynchronous Module Definition) libraries such as require.js. + if (typeof define === 'function' && define.amd) { + define([], function() { + return { + Howler: Howler, + Howl: Howl + }; + }); + } + + // Add support for CommonJS libraries such as browserify. + if (typeof exports !== 'undefined') { + exports.Howler = Howler; + exports.Howl = Howl; + } + + // Add to global in Node.js (for testing, etc). + if (typeof global !== 'undefined') { + global.HowlerGlobal = HowlerGlobal; + global.Howler = Howler; + global.Howl = Howl; + global.Sound = Sound; + } else if (typeof window !== 'undefined') { // Define globally in case AMD is not available or unused. + window.HowlerGlobal = HowlerGlobal; + window.Howler = Howler; + window.Howl = Howl; + window.Sound = Sound; + } +})(); + + +/*! + * Spatial Plugin - Adds support for stereo and 3D audio where Web Audio is supported. + * + * howler.js v2.2.4 + * howlerjs.com + * + * (c) 2013-2020, James Simpson of GoldFire Studios + * goldfirestudios.com + * + * MIT License + */ + +(function() { + + 'use strict'; + + // Setup default properties. + HowlerGlobal.prototype._pos = [0, 0, 0]; + HowlerGlobal.prototype._orientation = [0, 0, -1, 0, 1, 0]; + + /** Global Methods **/ + /***************************************************************************/ + + /** + * Helper method to update the stereo panning position of all current Howls. + * Future Howls will not use this value unless explicitly set. + * @param {Number} pan A value of -1.0 is all the way left and 1.0 is all the way right. + * @return {Howler/Number} Self or current stereo panning value. + */ + HowlerGlobal.prototype.stereo = function(pan) { + var self = this; + + // Stop right here if not using Web Audio. + if (!self.ctx || !self.ctx.listener) { + return self; + } + + // Loop through all Howls and update their stereo panning. + for (var i=self._howls.length-1; i>=0; i--) { + self._howls[i].stereo(pan); + } + + return self; + }; + + /** + * Get/set the position of the listener in 3D cartesian space. Sounds using + * 3D position will be relative to the listener's position. + * @param {Number} x The x-position of the listener. + * @param {Number} y The y-position of the listener. + * @param {Number} z The z-position of the listener. + * @return {Howler/Array} Self or current listener position. + */ + HowlerGlobal.prototype.pos = function(x, y, z) { + var self = this; + + // Stop right here if not using Web Audio. + if (!self.ctx || !self.ctx.listener) { + return self; + } + + // Set the defaults for optional 'y' & 'z'. + y = (typeof y !== 'number') ? self._pos[1] : y; + z = (typeof z !== 'number') ? self._pos[2] : z; + + if (typeof x === 'number') { + self._pos = [x, y, z]; + + if (typeof self.ctx.listener.positionX !== 'undefined') { + self.ctx.listener.positionX.setTargetAtTime(self._pos[0], Howler.ctx.currentTime, 0.1); + self.ctx.listener.positionY.setTargetAtTime(self._pos[1], Howler.ctx.currentTime, 0.1); + self.ctx.listener.positionZ.setTargetAtTime(self._pos[2], Howler.ctx.currentTime, 0.1); + } else { + self.ctx.listener.setPosition(self._pos[0], self._pos[1], self._pos[2]); + } + } else { + return self._pos; + } + + return self; + }; + + /** + * Get/set the direction the listener is pointing in the 3D cartesian space. + * A front and up vector must be provided. The front is the direction the + * face of the listener is pointing, and up is the direction the top of the + * listener is pointing. Thus, these values are expected to be at right angles + * from each other. + * @param {Number} x The x-orientation of the listener. + * @param {Number} y The y-orientation of the listener. + * @param {Number} z The z-orientation of the listener. + * @param {Number} xUp The x-orientation of the top of the listener. + * @param {Number} yUp The y-orientation of the top of the listener. + * @param {Number} zUp The z-orientation of the top of the listener. + * @return {Howler/Array} Returns self or the current orientation vectors. + */ + HowlerGlobal.prototype.orientation = function(x, y, z, xUp, yUp, zUp) { + var self = this; + + // Stop right here if not using Web Audio. + if (!self.ctx || !self.ctx.listener) { + return self; + } + + // Set the defaults for optional 'y' & 'z'. + var or = self._orientation; + y = (typeof y !== 'number') ? or[1] : y; + z = (typeof z !== 'number') ? or[2] : z; + xUp = (typeof xUp !== 'number') ? or[3] : xUp; + yUp = (typeof yUp !== 'number') ? or[4] : yUp; + zUp = (typeof zUp !== 'number') ? or[5] : zUp; + + if (typeof x === 'number') { + self._orientation = [x, y, z, xUp, yUp, zUp]; + + if (typeof self.ctx.listener.forwardX !== 'undefined') { + self.ctx.listener.forwardX.setTargetAtTime(x, Howler.ctx.currentTime, 0.1); + self.ctx.listener.forwardY.setTargetAtTime(y, Howler.ctx.currentTime, 0.1); + self.ctx.listener.forwardZ.setTargetAtTime(z, Howler.ctx.currentTime, 0.1); + self.ctx.listener.upX.setTargetAtTime(xUp, Howler.ctx.currentTime, 0.1); + self.ctx.listener.upY.setTargetAtTime(yUp, Howler.ctx.currentTime, 0.1); + self.ctx.listener.upZ.setTargetAtTime(zUp, Howler.ctx.currentTime, 0.1); + } else { + self.ctx.listener.setOrientation(x, y, z, xUp, yUp, zUp); + } + } else { + return or; + } + + return self; + }; + + /** Group Methods **/ + /***************************************************************************/ + + /** + * Add new properties to the core init. + * @param {Function} _super Core init method. + * @return {Howl} + */ + Howl.prototype.init = (function(_super) { + return function(o) { + var self = this; + + // Setup user-defined default properties. + self._orientation = o.orientation || [1, 0, 0]; + self._stereo = o.stereo || null; + self._pos = o.pos || null; + self._pannerAttr = { + coneInnerAngle: typeof o.coneInnerAngle !== 'undefined' ? o.coneInnerAngle : 360, + coneOuterAngle: typeof o.coneOuterAngle !== 'undefined' ? o.coneOuterAngle : 360, + coneOuterGain: typeof o.coneOuterGain !== 'undefined' ? o.coneOuterGain : 0, + distanceModel: typeof o.distanceModel !== 'undefined' ? o.distanceModel : 'inverse', + maxDistance: typeof o.maxDistance !== 'undefined' ? o.maxDistance : 10000, + panningModel: typeof o.panningModel !== 'undefined' ? o.panningModel : 'HRTF', + refDistance: typeof o.refDistance !== 'undefined' ? o.refDistance : 1, + rolloffFactor: typeof o.rolloffFactor !== 'undefined' ? o.rolloffFactor : 1 + }; + + // Setup event listeners. + self._onstereo = o.onstereo ? [{fn: o.onstereo}] : []; + self._onpos = o.onpos ? [{fn: o.onpos}] : []; + self._onorientation = o.onorientation ? [{fn: o.onorientation}] : []; + + // Complete initilization with howler.js core's init function. + return _super.call(this, o); + }; + })(Howl.prototype.init); + + /** + * Get/set the stereo panning of the audio source for this sound or all in the group. + * @param {Number} pan A value of -1.0 is all the way left and 1.0 is all the way right. + * @param {Number} id (optional) The sound ID. If none is passed, all in group will be updated. + * @return {Howl/Number} Returns self or the current stereo panning value. + */ + Howl.prototype.stereo = function(pan, id) { + var self = this; + + // Stop right here if not using Web Audio. + if (!self._webAudio) { + return self; + } + + // If the sound hasn't loaded, add it to the load queue to change stereo pan when capable. + if (self._state !== 'loaded') { + self._queue.push({ + event: 'stereo', + action: function() { + self.stereo(pan, id); + } + }); + + return self; + } + + // Check for PannerStereoNode support and fallback to PannerNode if it doesn't exist. + var pannerType = (typeof Howler.ctx.createStereoPanner === 'undefined') ? 'spatial' : 'stereo'; + + // Setup the group's stereo panning if no ID is passed. + if (typeof id === 'undefined') { + // Return the group's stereo panning if no parameters are passed. + if (typeof pan === 'number') { + self._stereo = pan; + self._pos = [pan, 0, 0]; + } else { + return self._stereo; + } + } + + // Change the streo panning of one or all sounds in group. + var ids = self._getSoundIds(id); + for (var i=0; i Returns the group's values. + * pannerAttr(id) -> Returns the sound id's values. + * pannerAttr(o) -> Set's the values of all sounds in this Howl group. + * pannerAttr(o, id) -> Set's the values of passed sound id. + * + * Attributes: + * coneInnerAngle - (360 by default) A parameter for directional audio sources, this is an angle, in degrees, + * inside of which there will be no volume reduction. + * coneOuterAngle - (360 by default) A parameter for directional audio sources, this is an angle, in degrees, + * outside of which the volume will be reduced to a constant value of `coneOuterGain`. + * coneOuterGain - (0 by default) A parameter for directional audio sources, this is the gain outside of the + * `coneOuterAngle`. It is a linear value in the range `[0, 1]`. + * distanceModel - ('inverse' by default) Determines algorithm used to reduce volume as audio moves away from + * listener. Can be `linear`, `inverse` or `exponential. + * maxDistance - (10000 by default) The maximum distance between source and listener, after which the volume + * will not be reduced any further. + * refDistance - (1 by default) A reference distance for reducing volume as source moves further from the listener. + * This is simply a variable of the distance model and has a different effect depending on which model + * is used and the scale of your coordinates. Generally, volume will be equal to 1 at this distance. + * rolloffFactor - (1 by default) How quickly the volume reduces as source moves from listener. This is simply a + * variable of the distance model and can be in the range of `[0, 1]` with `linear` and `[0, ∞]` + * with `inverse` and `exponential`. + * panningModel - ('HRTF' by default) Determines which spatialization algorithm is used to position audio. + * Can be `HRTF` or `equalpower`. + * + * @return {Howl/Object} Returns self or current panner attributes. + */ + Howl.prototype.pannerAttr = function() { + var self = this; + var args = arguments; + var o, id, sound; + + // Stop right here if not using Web Audio. + if (!self._webAudio) { + return self; + } + + // Determine the values based on arguments. + if (args.length === 0) { + // Return the group's panner attribute values. + return self._pannerAttr; + } else if (args.length === 1) { + if (typeof args[0] === 'object') { + o = args[0]; + + // Set the grou's panner attribute values. + if (typeof id === 'undefined') { + if (!o.pannerAttr) { + o.pannerAttr = { + coneInnerAngle: o.coneInnerAngle, + coneOuterAngle: o.coneOuterAngle, + coneOuterGain: o.coneOuterGain, + distanceModel: o.distanceModel, + maxDistance: o.maxDistance, + refDistance: o.refDistance, + rolloffFactor: o.rolloffFactor, + panningModel: o.panningModel + }; + } + + self._pannerAttr = { + coneInnerAngle: typeof o.pannerAttr.coneInnerAngle !== 'undefined' ? o.pannerAttr.coneInnerAngle : self._coneInnerAngle, + coneOuterAngle: typeof o.pannerAttr.coneOuterAngle !== 'undefined' ? o.pannerAttr.coneOuterAngle : self._coneOuterAngle, + coneOuterGain: typeof o.pannerAttr.coneOuterGain !== 'undefined' ? o.pannerAttr.coneOuterGain : self._coneOuterGain, + distanceModel: typeof o.pannerAttr.distanceModel !== 'undefined' ? o.pannerAttr.distanceModel : self._distanceModel, + maxDistance: typeof o.pannerAttr.maxDistance !== 'undefined' ? o.pannerAttr.maxDistance : self._maxDistance, + refDistance: typeof o.pannerAttr.refDistance !== 'undefined' ? o.pannerAttr.refDistance : self._refDistance, + rolloffFactor: typeof o.pannerAttr.rolloffFactor !== 'undefined' ? o.pannerAttr.rolloffFactor : self._rolloffFactor, + panningModel: typeof o.pannerAttr.panningModel !== 'undefined' ? o.pannerAttr.panningModel : self._panningModel + }; + } + } else { + // Return this sound's panner attribute values. + sound = self._soundById(parseInt(args[0], 10)); + return sound ? sound._pannerAttr : self._pannerAttr; + } + } else if (args.length === 2) { + o = args[0]; + id = parseInt(args[1], 10); + } + + // Update the values of the specified sounds. + var ids = self._getSoundIds(id); + for (var i=0; i=0&&e<=1){if(o._volume=e,o._muted)return o;o.usingWebAudio&&o.masterGain.gain.setValueAtTime(e,n.ctx.currentTime);for(var t=0;t=0;o--)e._howls[o].unload();return e.usingWebAudio&&e.ctx&&void 0!==e.ctx.close&&(e.ctx.close(),e.ctx=null,_()),e},codecs:function(e){return(this||n)._codecs[e.replace(/^x-/,"")]},_setup:function(){var e=this||n;if(e.state=e.ctx?e.ctx.state||"suspended":"suspended",e._autoSuspend(),!e.usingWebAudio)if("undefined"!=typeof Audio)try{var o=new Audio;void 0===o.oncanplaythrough&&(e._canPlayEvent="canplay")}catch(n){e.noAudio=!0}else e.noAudio=!0;try{var o=new Audio;o.muted&&(e.noAudio=!0)}catch(e){}return e.noAudio||e._setupCodecs(),e},_setupCodecs:function(){var e=this||n,o=null;try{o="undefined"!=typeof Audio?new Audio:null}catch(n){return e}if(!o||"function"!=typeof o.canPlayType)return e;var t=o.canPlayType("audio/mpeg;").replace(/^no$/,""),r=e._navigator?e._navigator.userAgent:"",a=r.match(/OPR\/(\d+)/g),u=a&&parseInt(a[0].split("/")[1],10)<33,d=-1!==r.indexOf("Safari")&&-1===r.indexOf("Chrome"),i=r.match(/Version\/(.*?) /),_=d&&i&&parseInt(i[1],10)<15;return e._codecs={mp3:!(u||!t&&!o.canPlayType("audio/mp3;").replace(/^no$/,"")),mpeg:!!t,opus:!!o.canPlayType('audio/ogg; codecs="opus"').replace(/^no$/,""),ogg:!!o.canPlayType('audio/ogg; codecs="vorbis"').replace(/^no$/,""),oga:!!o.canPlayType('audio/ogg; codecs="vorbis"').replace(/^no$/,""),wav:!!(o.canPlayType('audio/wav; codecs="1"')||o.canPlayType("audio/wav")).replace(/^no$/,""),aac:!!o.canPlayType("audio/aac;").replace(/^no$/,""),caf:!!o.canPlayType("audio/x-caf;").replace(/^no$/,""),m4a:!!(o.canPlayType("audio/x-m4a;")||o.canPlayType("audio/m4a;")||o.canPlayType("audio/aac;")).replace(/^no$/,""),m4b:!!(o.canPlayType("audio/x-m4b;")||o.canPlayType("audio/m4b;")||o.canPlayType("audio/aac;")).replace(/^no$/,""),mp4:!!(o.canPlayType("audio/x-mp4;")||o.canPlayType("audio/mp4;")||o.canPlayType("audio/aac;")).replace(/^no$/,""),weba:!(_||!o.canPlayType('audio/webm; codecs="vorbis"').replace(/^no$/,"")),webm:!(_||!o.canPlayType('audio/webm; codecs="vorbis"').replace(/^no$/,"")),dolby:!!o.canPlayType('audio/mp4; codecs="ec-3"').replace(/^no$/,""),flac:!!(o.canPlayType("audio/x-flac;")||o.canPlayType("audio/flac;")).replace(/^no$/,"")},e},_unlockAudio:function(){var e=this||n;if(!e._audioUnlocked&&e.ctx){e._audioUnlocked=!1,e.autoUnlock=!1,e._mobileUnloaded||44100===e.ctx.sampleRate||(e._mobileUnloaded=!0,e.unload()),e._scratchBuffer=e.ctx.createBuffer(1,1,22050);var o=function(n){for(;e._html5AudioPool.length0?d._seek:t._sprite[e][0]/1e3),s=Math.max(0,(t._sprite[e][0]+t._sprite[e][1])/1e3-_),l=1e3*s/Math.abs(d._rate),c=t._sprite[e][0]/1e3,f=(t._sprite[e][0]+t._sprite[e][1])/1e3;d._sprite=e,d._ended=!1;var p=function(){d._paused=!1,d._seek=_,d._start=c,d._stop=f,d._loop=!(!d._loop&&!t._sprite[e][2])};if(_>=f)return void t._ended(d);var m=d._node;if(t._webAudio){var v=function(){t._playLock=!1,p(),t._refreshBuffer(d);var e=d._muted||t._muted?0:d._volume;m.gain.setValueAtTime(e,n.ctx.currentTime),d._playStart=n.ctx.currentTime,void 0===m.bufferSource.start?d._loop?m.bufferSource.noteGrainOn(0,_,86400):m.bufferSource.noteGrainOn(0,_,s):d._loop?m.bufferSource.start(0,_,86400):m.bufferSource.start(0,_,s),l!==1/0&&(t._endTimers[d._id]=setTimeout(t._ended.bind(t,d),l)),o||setTimeout(function(){t._emit("play",d._id),t._loadQueue()},0)};"running"===n.state&&"interrupted"!==n.ctx.state?v():(t._playLock=!0,t.once("resume",v),t._clearTimer(d._id))}else{var h=function(){m.currentTime=_,m.muted=d._muted||t._muted||n._muted||m.muted,m.volume=d._volume*n.volume(),m.playbackRate=d._rate;try{var r=m.play();if(r&&"undefined"!=typeof Promise&&(r instanceof Promise||"function"==typeof r.then)?(t._playLock=!0,p(),r.then(function(){t._playLock=!1,m._unlocked=!0,o?t._loadQueue():t._emit("play",d._id)}).catch(function(){t._playLock=!1,t._emit("playerror",d._id,"Playback was unable to start. This is most commonly an issue on mobile devices and Chrome where playback was not within a user interaction."),d._ended=!0,d._paused=!0})):o||(t._playLock=!1,p(),t._emit("play",d._id)),m.playbackRate=d._rate,m.paused)return void t._emit("playerror",d._id,"Playback was unable to start. This is most commonly an issue on mobile devices and Chrome where playback was not within a user interaction.");"__default"!==e||d._loop?t._endTimers[d._id]=setTimeout(t._ended.bind(t,d),l):(t._endTimers[d._id]=function(){t._ended(d),m.removeEventListener("ended",t._endTimers[d._id],!1)},m.addEventListener("ended",t._endTimers[d._id],!1))}catch(e){t._emit("playerror",d._id,e)}};"data:audio/wav;base64,UklGRigAAABXQVZFZm10IBIAAAABAAEARKwAAIhYAQACABAAAABkYXRhAgAAAAEA"===m.src&&(m.src=t._src,m.load());var y=window&&window.ejecta||!m.readyState&&n._navigator.isCocoonJS;if(m.readyState>=3||y)h();else{t._playLock=!0,t._state="loading";var g=function(){t._state="loaded",h(),m.removeEventListener(n._canPlayEvent,g,!1)};m.addEventListener(n._canPlayEvent,g,!1),t._clearTimer(d._id)}}return d._id},pause:function(e){var n=this;if("loaded"!==n._state||n._playLock)return n._queue.push({event:"pause",action:function(){n.pause(e)}}),n;for(var o=n._getSoundIds(e),t=0;t=0?o=parseInt(r[0],10):e=parseFloat(r[0])}else r.length>=2&&(e=parseFloat(r[0]),o=parseInt(r[1],10));var a;if(!(void 0!==e&&e>=0&&e<=1))return a=o?t._soundById(o):t._sounds[0],a?a._volume:0;if("loaded"!==t._state||t._playLock)return t._queue.push({event:"volume",action:function(){t.volume.apply(t,r)}}),t;void 0===o&&(t._volume=e),o=t._getSoundIds(o);for(var u=0;u0?t/_:t),l=Date.now();e._fadeTo=o,e._interval=setInterval(function(){var r=(Date.now()-l)/t;l=Date.now(),d+=i*r,d=Math.round(100*d)/100,d=i<0?Math.max(o,d):Math.min(o,d),u._webAudio?e._volume=d:u.volume(d,e._id,!0),a&&(u._volume=d),(on&&d>=o)&&(clearInterval(e._interval),e._interval=null,e._fadeTo=null,u.volume(o,e._id),u._emit("fade",e._id))},s)},_stopFade:function(e){var o=this,t=o._soundById(e);return t&&t._interval&&(o._webAudio&&t._node.gain.cancelScheduledValues(n.ctx.currentTime),clearInterval(t._interval),t._interval=null,o.volume(t._fadeTo,e),t._fadeTo=null,o._emit("fade",e)),o},loop:function(){var e,n,o,t=this,r=arguments;if(0===r.length)return t._loop;if(1===r.length){if("boolean"!=typeof r[0])return!!(o=t._soundById(parseInt(r[0],10)))&&o._loop;e=r[0],t._loop=e}else 2===r.length&&(e=r[0],n=parseInt(r[1],10));for(var a=t._getSoundIds(n),u=0;u=0?o=parseInt(r[0],10):e=parseFloat(r[0])}else 2===r.length&&(e=parseFloat(r[0]),o=parseInt(r[1],10));var d;if("number"!=typeof e)return d=t._soundById(o),d?d._rate:t._rate;if("loaded"!==t._state||t._playLock)return t._queue.push({event:"rate",action:function(){t.rate.apply(t,r)}}),t;void 0===o&&(t._rate=e),o=t._getSoundIds(o);for(var i=0;i=0?o=parseInt(r[0],10):t._sounds.length&&(o=t._sounds[0]._id,e=parseFloat(r[0]))}else 2===r.length&&(e=parseFloat(r[0]),o=parseInt(r[1],10));if(void 0===o)return 0;if("number"==typeof e&&("loaded"!==t._state||t._playLock))return t._queue.push({event:"seek",action:function(){t.seek.apply(t,r)}}),t;var d=t._soundById(o);if(d){if(!("number"==typeof e&&e>=0)){if(t._webAudio){var i=t.playing(o)?n.ctx.currentTime-d._playStart:0,_=d._rateSeek?d._rateSeek-d._seek:0;return d._seek+(_+i*Math.abs(d._rate))}return d._node.currentTime}var s=t.playing(o);s&&t.pause(o,!0),d._seek=e,d._ended=!1,t._clearTimer(o),t._webAudio||!d._node||isNaN(d._node.duration)||(d._node.currentTime=e);var l=function(){s&&t.play(o,!0),t._emit("seek",o)};if(s&&!t._webAudio){var c=function(){t._playLock?setTimeout(c,0):l()};setTimeout(c,0)}else l()}return t},playing:function(e){var n=this;if("number"==typeof e){var o=n._soundById(e);return!!o&&!o._paused}for(var t=0;t=0&&n._howls.splice(a,1);var u=!0;for(t=0;t=0){u=!1;break}return r&&u&&delete r[e._src],n.noAudio=!1,e._state="unloaded",e._sounds=[],e=null,null},on:function(e,n,o,t){var r=this,a=r["_on"+e];return"function"==typeof n&&a.push(t?{id:o,fn:n,once:t}:{id:o,fn:n}),r},off:function(e,n,o){var t=this,r=t["_on"+e],a=0;if("number"==typeof n&&(o=n,n=null),n||o)for(a=0;a=0;a--)r[a].id&&r[a].id!==n&&"load"!==e||(setTimeout(function(e){e.call(this,n,o)}.bind(t,r[a].fn),0),r[a].once&&t.off(e,r[a].fn,r[a].id));return t._loadQueue(e),t},_loadQueue:function(e){var n=this;if(n._queue.length>0){var o=n._queue[0];o.event===e&&(n._queue.shift(),n._loadQueue()),e||o.action()}return n},_ended:function(e){var o=this,t=e._sprite;if(!o._webAudio&&e._node&&!e._node.paused&&!e._node.ended&&e._node.currentTime=0;t--){if(o<=n)return;e._sounds[t]._ended&&(e._webAudio&&e._sounds[t]._node&&e._sounds[t]._node.disconnect(0),e._sounds.splice(t,1),o--)}}},_getSoundIds:function(e){var n=this;if(void 0===e){for(var o=[],t=0;t=0;if(!e.bufferSource)return o;if(n._scratchBuffer&&e.bufferSource&&(e.bufferSource.onended=null,e.bufferSource.disconnect(0),t))try{e.bufferSource.buffer=n._scratchBuffer}catch(e){}return e.bufferSource=null,o},_clearSound:function(e){/MSIE |Trident\//.test(n._navigator&&n._navigator.userAgent)||(e.src="data:audio/wav;base64,UklGRigAAABXQVZFZm10IBIAAAABAAEARKwAAIhYAQACABAAAABkYXRhAgAAAAEA")}};var t=function(e){this._parent=e,this.init()};t.prototype={init:function(){var e=this,o=e._parent;return e._muted=o._muted,e._loop=o._loop,e._volume=o._volume,e._rate=o._rate,e._seek=0,e._paused=!0,e._ended=!0,e._sprite="__default",e._id=++n._counter,o._sounds.push(e),e.create(),e},create:function(){var e=this,o=e._parent,t=n._muted||e._muted||e._parent._muted?0:e._volume;return o._webAudio?(e._node=void 0===n.ctx.createGain?n.ctx.createGainNode():n.ctx.createGain(),e._node.gain.setValueAtTime(t,n.ctx.currentTime),e._node.paused=!0,e._node.connect(n.masterGain)):n.noAudio||(e._node=n._obtainHtml5Audio(),e._errorFn=e._errorListener.bind(e),e._node.addEventListener("error",e._errorFn,!1),e._loadFn=e._loadListener.bind(e),e._node.addEventListener(n._canPlayEvent,e._loadFn,!1),e._endFn=e._endListener.bind(e),e._node.addEventListener("ended",e._endFn,!1),e._node.src=o._src,e._node.preload=!0===o._preload?"auto":o._preload,e._node.volume=t*n.volume(),e._node.load()),e},reset:function(){var e=this,o=e._parent;return e._muted=o._muted,e._loop=o._loop,e._volume=o._volume,e._rate=o._rate,e._seek=0,e._rateSeek=0,e._paused=!0,e._ended=!0,e._sprite="__default",e._id=++n._counter,e},_errorListener:function(){var e=this;e._parent._emit("loaderror",e._id,e._node.error?e._node.error.code:0),e._node.removeEventListener("error",e._errorFn,!1)},_loadListener:function(){var e=this,o=e._parent;o._duration=Math.ceil(10*e._node.duration)/10,0===Object.keys(o._sprite).length&&(o._sprite={__default:[0,1e3*o._duration]}),"loaded"!==o._state&&(o._state="loaded",o._emit("load"),o._loadQueue()),e._node.removeEventListener(n._canPlayEvent,e._loadFn,!1)},_endListener:function(){var e=this,n=e._parent;n._duration===1/0&&(n._duration=Math.ceil(10*e._node.duration)/10,n._sprite.__default[1]===1/0&&(n._sprite.__default[1]=1e3*n._duration),n._ended(e)),e._node.removeEventListener("ended",e._endFn,!1)}};var r={},a=function(e){var n=e._src;if(r[n])return e._duration=r[n].duration,void i(e);if(/^data:[^;]+;base64,/.test(n)){for(var o=atob(n.split(",")[1]),t=new Uint8Array(o.length),a=0;a0?(r[o._src]=e,i(o,e)):t()};"undefined"!=typeof Promise&&1===n.ctx.decodeAudioData.length?n.ctx.decodeAudioData(e).then(a).catch(t):n.ctx.decodeAudioData(e,a,t)},i=function(e,n){n&&!e._duration&&(e._duration=n.duration),0===Object.keys(e._sprite).length&&(e._sprite={__default:[0,1e3*e._duration]}),"loaded"!==e._state&&(e._state="loaded",e._emit("load"),e._loadQueue())},_=function(){if(n.usingWebAudio){try{"undefined"!=typeof AudioContext?n.ctx=new AudioContext:"undefined"!=typeof webkitAudioContext?n.ctx=new webkitAudioContext:n.usingWebAudio=!1}catch(e){n.usingWebAudio=!1}n.ctx||(n.usingWebAudio=!1);var e=/iP(hone|od|ad)/.test(n._navigator&&n._navigator.platform),o=n._navigator&&n._navigator.appVersion.match(/OS (\d+)_(\d+)_?(\d+)?/),t=o?parseInt(o[1],10):null;if(e&&t&&t<9){var r=/safari/.test(n._navigator&&n._navigator.userAgent.toLowerCase());n._navigator&&!r&&(n.usingWebAudio=!1)}n.usingWebAudio&&(n.masterGain=void 0===n.ctx.createGain?n.ctx.createGainNode():n.ctx.createGain(),n.masterGain.gain.setValueAtTime(n._muted?0:n._volume,n.ctx.currentTime),n.masterGain.connect(n.ctx.destination)),n._setup()}};"function"==typeof define&&define.amd&&define([],function(){return{Howler:n,Howl:o}}),"undefined"!=typeof exports&&(exports.Howler=n,exports.Howl=o),"undefined"!=typeof global?(global.HowlerGlobal=e,global.Howler=n,global.Howl=o,global.Sound=t):"undefined"!=typeof window&&(window.HowlerGlobal=e,window.Howler=n,window.Howl=o,window.Sound=t)}(); +/*! Spatial Plugin */ +!function(){"use strict";HowlerGlobal.prototype._pos=[0,0,0],HowlerGlobal.prototype._orientation=[0,0,-1,0,1,0],HowlerGlobal.prototype.stereo=function(e){var n=this;if(!n.ctx||!n.ctx.listener)return n;for(var t=n._howls.length-1;t>=0;t--)n._howls[t].stereo(e);return n},HowlerGlobal.prototype.pos=function(e,n,t){var r=this;return r.ctx&&r.ctx.listener?(n="number"!=typeof n?r._pos[1]:n,t="number"!=typeof t?r._pos[2]:t,"number"!=typeof e?r._pos:(r._pos=[e,n,t],void 0!==r.ctx.listener.positionX?(r.ctx.listener.positionX.setTargetAtTime(r._pos[0],Howler.ctx.currentTime,.1),r.ctx.listener.positionY.setTargetAtTime(r._pos[1],Howler.ctx.currentTime,.1),r.ctx.listener.positionZ.setTargetAtTime(r._pos[2],Howler.ctx.currentTime,.1)):r.ctx.listener.setPosition(r._pos[0],r._pos[1],r._pos[2]),r)):r},HowlerGlobal.prototype.orientation=function(e,n,t,r,o,i){var a=this;if(!a.ctx||!a.ctx.listener)return a;var p=a._orientation;return n="number"!=typeof n?p[1]:n,t="number"!=typeof t?p[2]:t,r="number"!=typeof r?p[3]:r,o="number"!=typeof o?p[4]:o,i="number"!=typeof i?p[5]:i,"number"!=typeof e?p:(a._orientation=[e,n,t,r,o,i],void 0!==a.ctx.listener.forwardX?(a.ctx.listener.forwardX.setTargetAtTime(e,Howler.ctx.currentTime,.1),a.ctx.listener.forwardY.setTargetAtTime(n,Howler.ctx.currentTime,.1),a.ctx.listener.forwardZ.setTargetAtTime(t,Howler.ctx.currentTime,.1),a.ctx.listener.upX.setTargetAtTime(r,Howler.ctx.currentTime,.1),a.ctx.listener.upY.setTargetAtTime(o,Howler.ctx.currentTime,.1),a.ctx.listener.upZ.setTargetAtTime(i,Howler.ctx.currentTime,.1)):a.ctx.listener.setOrientation(e,n,t,r,o,i),a)},Howl.prototype.init=function(e){return function(n){var t=this;return t._orientation=n.orientation||[1,0,0],t._stereo=n.stereo||null,t._pos=n.pos||null,t._pannerAttr={coneInnerAngle:void 0!==n.coneInnerAngle?n.coneInnerAngle:360,coneOuterAngle:void 0!==n.coneOuterAngle?n.coneOuterAngle:360,coneOuterGain:void 0!==n.coneOuterGain?n.coneOuterGain:0,distanceModel:void 0!==n.distanceModel?n.distanceModel:"inverse",maxDistance:void 0!==n.maxDistance?n.maxDistance:1e4,panningModel:void 0!==n.panningModel?n.panningModel:"HRTF",refDistance:void 0!==n.refDistance?n.refDistance:1,rolloffFactor:void 0!==n.rolloffFactor?n.rolloffFactor:1},t._onstereo=n.onstereo?[{fn:n.onstereo}]:[],t._onpos=n.onpos?[{fn:n.onpos}]:[],t._onorientation=n.onorientation?[{fn:n.onorientation}]:[],e.call(this,n)}}(Howl.prototype.init),Howl.prototype.stereo=function(n,t){var r=this;if(!r._webAudio)return r;if("loaded"!==r._state)return r._queue.push({event:"stereo",action:function(){r.stereo(n,t)}}),r;var o=void 0===Howler.ctx.createStereoPanner?"spatial":"stereo";if(void 0===t){if("number"!=typeof n)return r._stereo;r._stereo=n,r._pos=[n,0,0]}for(var i=r._getSoundIds(t),a=0;a=0;t--)n._howls[t].stereo(e);return n},HowlerGlobal.prototype.pos=function(e,n,t){var r=this;return r.ctx&&r.ctx.listener?(n="number"!=typeof n?r._pos[1]:n,t="number"!=typeof t?r._pos[2]:t,"number"!=typeof e?r._pos:(r._pos=[e,n,t],void 0!==r.ctx.listener.positionX?(r.ctx.listener.positionX.setTargetAtTime(r._pos[0],Howler.ctx.currentTime,.1),r.ctx.listener.positionY.setTargetAtTime(r._pos[1],Howler.ctx.currentTime,.1),r.ctx.listener.positionZ.setTargetAtTime(r._pos[2],Howler.ctx.currentTime,.1)):r.ctx.listener.setPosition(r._pos[0],r._pos[1],r._pos[2]),r)):r},HowlerGlobal.prototype.orientation=function(e,n,t,r,o,i){var a=this;if(!a.ctx||!a.ctx.listener)return a;var p=a._orientation;return n="number"!=typeof n?p[1]:n,t="number"!=typeof t?p[2]:t,r="number"!=typeof r?p[3]:r,o="number"!=typeof o?p[4]:o,i="number"!=typeof i?p[5]:i,"number"!=typeof e?p:(a._orientation=[e,n,t,r,o,i],void 0!==a.ctx.listener.forwardX?(a.ctx.listener.forwardX.setTargetAtTime(e,Howler.ctx.currentTime,.1),a.ctx.listener.forwardY.setTargetAtTime(n,Howler.ctx.currentTime,.1),a.ctx.listener.forwardZ.setTargetAtTime(t,Howler.ctx.currentTime,.1),a.ctx.listener.upX.setTargetAtTime(r,Howler.ctx.currentTime,.1),a.ctx.listener.upY.setTargetAtTime(o,Howler.ctx.currentTime,.1),a.ctx.listener.upZ.setTargetAtTime(i,Howler.ctx.currentTime,.1)):a.ctx.listener.setOrientation(e,n,t,r,o,i),a)},Howl.prototype.init=function(e){return function(n){var t=this;return t._orientation=n.orientation||[1,0,0],t._stereo=n.stereo||null,t._pos=n.pos||null,t._pannerAttr={coneInnerAngle:void 0!==n.coneInnerAngle?n.coneInnerAngle:360,coneOuterAngle:void 0!==n.coneOuterAngle?n.coneOuterAngle:360,coneOuterGain:void 0!==n.coneOuterGain?n.coneOuterGain:0,distanceModel:void 0!==n.distanceModel?n.distanceModel:"inverse",maxDistance:void 0!==n.maxDistance?n.maxDistance:1e4,panningModel:void 0!==n.panningModel?n.panningModel:"HRTF",refDistance:void 0!==n.refDistance?n.refDistance:1,rolloffFactor:void 0!==n.rolloffFactor?n.rolloffFactor:1},t._onstereo=n.onstereo?[{fn:n.onstereo}]:[],t._onpos=n.onpos?[{fn:n.onpos}]:[],t._onorientation=n.onorientation?[{fn:n.onorientation}]:[],e.call(this,n)}}(Howl.prototype.init),Howl.prototype.stereo=function(n,t){var r=this;if(!r._webAudio)return r;if("loaded"!==r._state)return r._queue.push({event:"stereo",action:function(){r.stereo(n,t)}}),r;var o=void 0===Howler.ctx.createStereoPanner?"spatial":"stereo";if(void 0===t){if("number"!=typeof n)return r._stereo;r._stereo=n,r._pos=[n,0,0]}for(var i=r._getSoundIds(t),a=0;a= 1) { + const elem = ToDo.shift(); + caller.play(elem) + } else { + looperActive = false; + } +} +export function playSound(sound) { + ToDo.push(sound); + if (!looperActive){ + looper(); + } +} +export const caller = new Howl({ + src: ['/assets/audio/dotty-floor-english.mp3'], + onend: function() { + looper() + }, + sprite: { + "1": [0, 654], + "2": [654, 498], + "3": [1152, 560], + "4": [1712, 544], + "5": [2256, 564], + "6": [2820, 663], + "7": [3483, 591], + "8": [4074, 545], + "9": [4619, 560], + "10": [5179, 524], + "11": [5703, 645], + "12": [6348, 581], + "13": [6929, 847], + "14": [7776, 733], + "15": [8509, 873], + "16": [9382, 1020], + "17": [10402, 964], + "18": [11366, 847], + "19": [12213, 930], + "20": [13143, 729], + "21": [13872, 964], + "22": [14836, 950], + "23": [15786, 914], + "24": [16700, 901], + "25": [17601, 986], + "26": [18587, 981], + "27": [19568, 959], + "28": [20527, 923], + "29": [21450, 1047], + "30": [22497, 688], + "31": [23185, 970], + "32": [24155, 858], + "33": [25013, 1050], + "34": [26063, 942], + "35": [27005, 924], + "36": [27929, 972], + "37": [28901, 977], + "38": [29878, 844], + "39": [30722, 918], + "40": [31640, 667], + "41": [32307, 845], + "42": [33152, 928], + "43": [34080, 936], + "44": [35016, 964], + "45": [35980, 992], + "46": [36972, 964], + "47": [37936, 882], + "48": [38818, 863], + "49": [39681, 919], + "50": [40600, 678], + "51": [41278, 912], + "52": [42190, 870], + "53": [43060, 940], + "54": [44000, 883], + "55": [44883, 950], + "56": [45833, 1000], + "57": [46833, 992], + "58": [47825, 906], + "59": [48731, 954], + "60": [49685, 795], + "61": [50480, 993], + "62": [51473, 982], + "63": [52455, 989], + "64": [53444, 1003], + "65": [54447, 1009], + "66": [55456, 1119], + "67": [56575, 1076], + "68": [57651, 1133], + "69": [58784, 1077], + "70": [59861, 874], + "71": [60735, 990], + "72": [61725, 973], + "73": [62698, 1099], + "74": [63797, 1101], + "75": [64898, 1015], + "76": [65913, 1095], + "77": [67008, 1112], + "78": [68120, 1064], + "79": [69184, 1070], + "80": [70254, 661], + "81": [70915, 787], + "82": [71702, 800], + "83": [72502, 930], + "84": [73432, 938], + "85": [74370, 943], + "86": [75313, 957], + "87": [76270, 859], + "88": [77129, 834], + "89": [77963, 876], + "90": [78839, 790], + "91": [79629, 942], + "92": [80571, 973], + "93": [81544, 1017], + "94": [82561, 1019], + "95": [83580, 1003], + "96": [84583, 1113], + "97": [85696, 949], + "98": [86645, 1010], + "99": [87655, 1068], + "100": [88723, 917], + "101": [89640, 1371], + "102": [91011, 1295], + "103": [92306, 1382], + "104": [93688, 1273], + "105": [94961, 1360], + "106": [96321, 1368], + "107": [97689, 1289], + "108": [98978, 1297], + "109": [100275, 1353], + "110": [101628, 1256], + "111": [102884, 1437], + "112": [104321, 1484], + "113": [105805, 1453], + "114": [107258, 1501], + "115": [108759, 1595], + "116": [110354, 1534], + "117": [111888, 1680], + "118": [113568, 1566], + "119": [115134, 1506], + "120": [116640, 1373], + "121": [118013, 1704], + "122": [119717, 1711], + "123": [121428, 1685], + "124": [123113, 1741], + "125": [124854, 1909], + "126": [126763, 1750], + "127": [128513, 1877], + "128": [130390, 1839], + "129": [132229, 1758], + "130": [133987, 1704], + "131": [135691, 1787], + "132": [137478, 1721], + "133": [139199, 1793], + "134": [140992, 1829], + "135": [142821, 1846], + "136": [144667, 1805], + "137": [146472, 1728], + "138": [148200, 1817], + "139": [150017, 1879], + "140": [151896, 1468], + "141": [153364, 1624], + "142": [154988, 1754], + "143": [156742, 1711], + "144": [158453, 1845], + "145": [160298, 1754], + "146": [162052, 1832], + "147": [163884, 1746], + "148": [165630, 1761], + "149": [167391, 1745], + "150": [169136, 1497], + "151": [170633, 1664], + "152": [172297, 1704], + "153": [174001, 1825], + "154": [175826, 1813], + "155": [177639, 1818], + "156": [179457, 1902], + "157": [181359, 1780], + "158": [183139, 1738], + "159": [184877, 1793], + "160": [186670, 1667], + "161": [188337, 1683], + "162": [190020, 1769], + "164": [191789, 1745], + "165": [193534, 1822], + "167": [195356, 1816], + "168": [197172, 1768], + "170": [198940, 1529], + "171": [200469, 1709], + "174": [202178, 1723], + "177": [203901, 1830], + "180": [205731, 1454], + "require_1": [207185, 1659], + "require_2": [208844, 1408], + "require_3": [210252, 1474], + "require_4": [211726, 1549], + "require_5": [213274, 1569], + "require_6": [214843, 1549], + "require_7": [216392, 1505], + "require_8": [217897, 1533], + "require_9": [219430, 1474], + "require_10": [220904, 1444], + "require_11": [222348, 1633], + "require_12": [223981, 1404], + "require_13": [225385, 1761], + "require_14": [227145, 1652], + "require_15": [228797, 1861], + "require_16": [230658, 2008], + "require_17": [232666, 1883], + "require_18": [234549, 1757], + "require_19": [236306, 1844], + "require_20": [238150, 1717], + "require_21": [239867, 1952], + "require_22": [241818, 1870], + "require_23": [243688, 1990], + "require_24": [245678, 1815], + "require_25": [247493, 1991], + "require_26": [249484, 1900], + "require_27": [251384, 1799], + "require_28": [253183, 1746], + "require_29": [254929, 1933], + "require_30": [256861, 1602], + "require_31": [258463, 1975], + "require_32": [260438, 1698], + "require_33": [262136, 2055], + "require_34": [264191, 1862], + "require_35": [266053, 1747], + "require_36": [267800, 1882], + "require_37": [269682, 1896], + "require_38": [271577, 1754], + "require_39": [273331, 1923], + "require_40": [275254, 1672], + "require_41": [276926, 1921], + "require_42": [278847, 1847], + "require_43": [280694, 1924], + "require_44": [282618, 1969], + "require_45": [284587, 2068], + "require_46": [286655, 2040], + "require_47": [288695, 1705], + "require_48": [290400, 1686], + "require_49": [292085, 1838], + "require_50": [293923, 1598], + "require_51": [295521, 1735], + "require_52": [297256, 1710], + "require_53": [298966, 1945], + "require_54": [300911, 1797], + "require_55": [302708, 2026], + "require_56": [304734, 2005], + "require_57": [306739, 1906], + "require_58": [308644, 1894], + "require_59": [310538, 1959], + "require_60": [312497, 1618], + "require_61": [314115, 1903], + "require_62": [316018, 2058], + "require_63": [318076, 1899], + "require_64": [319975, 1923], + "require_65": [321898, 1832], + "require_66": [323729, 2124], + "require_67": [325853, 1962], + "require_68": [327815, 2043], + "require_69": [329858, 1987], + "require_70": [331845, 1784], + "require_71": [333629, 1876], + "require_72": [335505, 2049], + "require_73": [337554, 1939], + "require_74": [339492, 2106], + "require_75": [341598, 1855], + "require_76": [343453, 2171], + "require_77": [345624, 2022], + "require_78": [347646, 1904], + "require_79": [349550, 1980], + "require_80": [351530, 1575], + "require_81": [353105, 1697], + "require_82": [354801, 1805], + "require_83": [356606, 1850], + "require_84": [358456, 1858], + "require_85": [360314, 1862], + "require_86": [362176, 1843], + "require_87": [364019, 1935], + "require_88": [365954, 1720], + "require_89": [367674, 1790], + "require_90": [369463, 1709], + "require_91": [371172, 1765], + "require_92": [372937, 1813], + "require_93": [374750, 1927], + "require_94": [376677, 1939], + "require_95": [378616, 2079], + "require_96": [380695, 2023], + "require_97": [382718, 1835], + "require_98": [384552, 1998], + "require_99": [386550, 1982], + "require_100": [388532, 1922], + "require_101": [390454, 2447], + "require_102": [392901, 2283], + "require_103": [395184, 2302], + "require_104": [397486, 2159], + "require_105": [399645, 2436], + "require_106": [402080, 2208], + "require_107": [404288, 2203], + "require_108": [406491, 2285], + "require_109": [408776, 2272], + "require_110": [411048, 2079], + "require_111": [413127, 2351], + "require_112": [415478, 2489], + "require_113": [417967, 2373], + "require_114": [420339, 2324], + "require_115": [422663, 2418], + "require_116": [425081, 2420], + "require_117": [427501, 2520], + "require_118": [430021, 2554], + "require_119": [432575, 2511], + "require_120": [435086, 2361], + "require_121": [437447, 2614], + "require_122": [440060, 2631], + "require_123": [442691, 2571], + "require_124": [445262, 2564], + "require_125": [447826, 2823], + "require_126": [450649, 2738], + "require_127": [453387, 2882], + "require_128": [456269, 2662], + "require_129": [458931, 2763], + "require_130": [461693, 2692], + "require_131": [464385, 2673], + "require_132": [467058, 2561], + "require_133": [469619, 2798], + "require_134": [472417, 2743], + "require_135": [475160, 2922], + "require_136": [478082, 2881], + "require_137": [480963, 2638], + "require_138": [483600, 2822], + "require_139": [486422, 2702], + "require_140": [489124, 2456], + "require_141": [491580, 2544], + "require_142": [494124, 2664], + "require_143": [496788, 2625], + "require_144": [499413, 2833], + "require_145": [502246, 2668], + "require_146": [504914, 2655], + "require_147": [507568, 2666], + "require_148": [510234, 2601], + "require_149": [512835, 2750], + "require_150": [515585, 2485], + "require_151": [518070, 2652], + "require_152": [520722, 2590], + "require_153": [523312, 2830], + "require_154": [526142, 2733], + "require_155": [528874, 2641], + "require_156": [531515, 2890], + "require_157": [534405, 2699], + "require_158": [537104, 2578], + "require_159": [539682, 2869], + "require_160": [542551, 2581], + "require_161": [545132, 2602], + "require_162": [547734, 2689], + "require_164": [550423, 2585], + "require_165": [553008, 2732], + "require_167": [555739, 2821], + "require_168": [558560, 2756], + "require_170": [561316, 2415], + "require_171": [563731, 2785], + "require_174": [566516, 2633], + "require_177": [569149, 2835], + "require_180": [571984, 2442], + "game_on": [574426, 904], + "game_shot": [575330, 887], + "game_shot_match": [576217, 1724], + "0": [577941, 903] + } +}); + \ No newline at end of file diff --git a/assets/js/views/xoi/logic.js b/assets/js/views/xoi/logic.js index d4d0950..b2f548c 100644 --- a/assets/js/views/xoi/logic.js +++ b/assets/js/views/xoi/logic.js @@ -6,31 +6,34 @@ import { computed } from "vue"; function setPoints(page, set){ let points = [0, 0]; // need to get updated for more player - let ret; - const m = page.game.sets.length-1; + for (const i in page.game.sets) { - const set = page.game.sets[i]; - const points = legPoints(page, set, set.legs[set.legs.length-1]); - const winner = getWinner(points, page.legs); + const s = page.game.sets[i]; + const p = legPoints(s, s.legs[s.legs.length-1]); + + const winner = getWinner(p, page.legs); + if (winner > -1) points[winner] += 1; - else + else if (winner == -2) { + points[0] += 0.5; + points[1] += 0.5; + } else return points; - if (set == set){ + if (s == set){ return points; } } return points; } -function legPoints(page, set, leg){ +function legPoints(set, leg){ let points = [0, 0]; // need to get updated for more player - let ret; - const m = set.legs.length-1; for (const i in set.legs) { const l = set.legs[i]; - if (l.visits[l.visits.length-1].toGo === undefined) { + + if (l.visits[l.visits.length-1].toGo === undefined) { return points; } if (l.visits[l.visits.length-1].toGo[0] == 0) @@ -61,8 +64,9 @@ function newPlayerStats(player) { } function addNewSetStats(stats, scorers) { - stats["sets"].push(newStats(scorers)); - stats["sets"][0]["legs"] = []; + const s = newStats(scorers) + s["legs"] = []; + stats["sets"].push(s); return stats; } @@ -142,7 +146,6 @@ function addNewLeg(page){ const set = page.game.sets[page.game.sets.length-1]; const leg = set.legs[set.legs.length-1]; const k = player2scorer(page)[leg.visits[0].player]; - console.log(k, k+1); const p = page.scorers[(k+1)%2].member[0].uuid; page.game.sets[page.game.sets.length-1].legs.push({ @@ -187,6 +190,7 @@ export function initGame(page) { function updateStats(page, visit){ const playerUUIDs = allPlayers(page).map((p) => p.uuid); + const p = player2scorer(page)[visit.player]; const k = playerUUIDs.indexOf(visit.player); const todos = [page.stats["stats"][k]]; @@ -200,7 +204,7 @@ function updateStats(page, visit){ todos[i]["first9"][0] += visit["sum"]; todos[i]["first9"][1] += visit["numDarts"]; } - if (visit["toGo"][k] == 0) { + if (visit["toGo"][p] == 0) { todos[i]["checkouts"][0] += 1; todos[i]["checkoutPoints"].push(visit["sum"]); } @@ -277,6 +281,7 @@ export function recalcStats(page){ }); if (j == lastleg && i != lastset) { addNewSetStats(page.stats, page.scorers); + addNewLegStats(page.stats, page.scorers); } }); }); @@ -345,8 +350,11 @@ export function storeVisit(page, throws, sum, numDarts, outTries, inTries=0) { } else { // rest == 0 leg finished - const newlegp = legPoints(page, set, leg); + + + const newlegp = legPoints(set, leg); let winner = getWinner(newlegp, page.legs); + console.log(winner); if (winner == -1) { // new Leg addNewLeg(page); @@ -355,6 +363,7 @@ export function storeVisit(page, throws, sum, numDarts, outTries, inTries=0) { // new set? const newsetp = setPoints(page, set); winner = getWinner(newsetp, page.sets); + console.log("Set", winner, newsetp, page.sets); if (winner == -1) { // new Set addNewSet(page); @@ -387,7 +396,6 @@ export function endGame(page){ export function extension(page){ const set = page.game.sets[page.game.sets.length-1]; - const leg = set.legs[set.legs.length-1]; page.enddate = undefined; // leg["points"] = newlegp; // new Leg @@ -396,6 +404,34 @@ export function extension(page){ } +export const getVal = (tr) => { + const val = tr.trim().toLowerCase(); + if (val == "") { + return 0; + } + if (val == "sb") { + return 25; + } + if (val == "db" || val == "bull") { + return 50; + } + if (val[0] == "s" || val[0] == "o" || val[0] == "i") { + return parseFloat(val.substring(1)); + } + if (val[0] == "d") { + return 2 * parseFloat(val.substring(1)); + } + if (val[0] == "t") { + return 3 * parseFloat(val.substring(1)); + } + if (val[0] == "m") { + return 0; + } else { + // TODO: Check for Na + return parseFloat(val); + } +} + ///////////////////////////////////////// // GameProps ///////////////////////////////////////// @@ -448,7 +484,7 @@ export function getGameProps(page, current_set, current_leg) { const current_leg_points = computed(() => { if (current_set.value != undefined || current_leg.value != undefined) { - return legPoints(page, current_set.value, current_leg.value); + return legPoints(current_set.value, current_leg.value); } return undefined; }); @@ -482,33 +518,7 @@ export function getGameProps(page, current_set, current_leg) { }); ret.breaks = breaks; - const getVal = (tr) => { - const val = tr.trim(); - if (val == "") { - return 0; - } - if (val == "SB") { - return 25; - } - if (val == "DB") { - return 50; - } - if (val[0] == "S" || val[0] == "O" || val[0] == "I") { - return parseFloat(val.substring(1)); - } - if (val[0] == "D") { - return 2 * parseFloat(val.substring(1)); - } - if (val[0] == "T") { - return 3 * parseFloat(val.substring(1)); - } - if (val[0] == "M") { - return 0; - } else { - // TODO: Check for Na - return parseFloat(val); - } - } + const verifySum = (sum) => { if (sum > 180 || [179, 178, 176, 175, 173, 172, 169, 166, 163].indexOf(sum) > -1) { return -1; @@ -528,16 +538,28 @@ export function getGameProps(page, current_set, current_leg) { return 2; } - const checkVisit = (throws) => { + const checkVisit = (throws) => { const tr = throws.split(","); let sum = 0; + let numDarts = 0; + let fifty = current_toGo.value[current_scorer_idx.value] == 50; + let tries = 0; tr.forEach((t, i) => { - sum += getVal(t); + const val = getVal(t); + let rest = current_toGo.value[current_scorer_idx.value] - sum; + if ((rest <= 40 && rest % 2 == 0) || (fifty && val != 18 && val != 10)){ + tries += 1 + fifty = false + } else if (rest == 50) { + fifty = true; + } + sum += val; + numDarts += 1 }); const res = verifySum(sum); - return [res, sum]; + return [res, sum, [numDarts, tries]]; } ret.checkVisit = checkVisit; return ret; -} \ No newline at end of file +} diff --git a/assets/js/views/xoi/main.js b/assets/js/views/xoi/main.js index 03ca796..3a5a7cc 100644 --- a/assets/js/views/xoi/main.js +++ b/assets/js/views/xoi/main.js @@ -1,9 +1,10 @@ -import { reactive, ref, computed, onMounted, toRaw } from "vue"; +import { reactive, ref, computed, onMounted, onUnmounted, toRaw } from "vue"; import { getQuery, setKirby } from "../../kirby.js"; import { handleActive, ArrowVerticalKeyHandler, ArrowHorizontalKeyHandler, NumberKeyHandler } from "../../handlers.js"; import { overlayAndGet, powerStateMachine, overlayAndPop, popLastElem } from "../../componentPromise.js"; - -import { getGameProps, initGame, initStats, storeVisit, formatDate, removeLastVisit, extension, getFinalWinner, playerFromUUID } from "./logic.js"; +import { autodartsState } from '../../websocket.js' +import { getGameProps, initGame, initStats, storeVisit, formatDate, removeLastVisit, extension, getFinalWinner, getVal } from "./logic.js"; +import { playSound } from '../../sound.js'; const html = (v) => { return v[0] }; @@ -69,9 +70,9 @@ const pregame = {
Scorer 1:
Scorer 2:
-
X01:
-
In:
- + + +
Best of Sets:
Best of Legs (per Set):
@@ -358,7 +359,7 @@ const game = { } const gameinput = { - props: ['input','active', 'stack'], + props: ['input','active', 'stack', 'toGo'], setup(props, context) { handleActive(props, []); const check_remove = (event) => { @@ -367,6 +368,55 @@ const gameinput = { context.emit('reject', -2); } } + let inputtext = ref(props.input || "") + + // if (websocket){ + let throwdetect = (event) => { + if (event.detail.data.event == "Takeout started"){ + return + } + if (event.detail.data.event == "Takeout finished" && inputtext.value != "" && inputtext.value != undefined && inputtext.value != NaN){ + context.emit('resolve', inputtext.value); + return + } + let throws = event.detail.data.throws; + if (!throws){ + return + } + const scored = throws.at(-1).segment.name + if (throws.length == 1) { + inputtext.value = scored; + } else { + inputtext.value += "," + scored + } + const tr = inputtext.value.split(","); + let sum = 0; + tr.forEach((t, i) => { + sum += getVal(t); + }); + + if (props.toGo - sum == 0 && scored.startsWith("D")) { + playSound("game_shot"); + return + } + if (props.toGo - sum <= 1) { + playSound("0"); + return + } + if (throws.length == 3) { + playSound(""+sum); + } + }; + + onMounted(() => { + document.body.addEventListener("autodarts", throwdetect); + if (props.toGo < 170 && [169, 168, 166, 165, 163, 162, 159].indexOf(props.toGo) == -1) { + playSound("require_"+ props.toGo); + } + }) + + onUnmounted(() => document.body.removeEventListener("autodarts", throwdetect)) + // } const keyhandler = (e) => { if (e.key == "F1" || e.keyCode == 112) { e.preventDefault(); @@ -384,12 +434,11 @@ const gameinput = { e.preventDefault(); context.emit('resolve', -1*parseFloat(e.target.value)); } - } - return { check_remove, keyhandler } + return { check_remove, keyhandler, inputtext } }, template: html` - + ` } @@ -409,6 +458,7 @@ const xoi = { const leg_id = ref(props.page.game.sets[set_id.value].legs.length - 1); let current_stat; if (props.inspect) { + handleActive(props, [ArrowHorizontalKeyHandler]); current_set = computed(() => props.page.game.sets[set_id.value]); current_leg = computed(() => current_set.value.legs[leg_id.value]); current_stat = computed(() => props.page.stats?.sets[set_id.value].legs[leg_id.value]); @@ -423,7 +473,9 @@ const xoi = { const computedProps = getGameProps(props.page, current_set, current_leg); if (!props.inspect) { - current_stat = computed( () => props.page.stats?.sets[props.page.stats.sets.length-1].legs[computedProps.current_set.value.legs.length-1]); + current_stat = computed( () => { + return props.page.stats?.sets[props.page.stats.sets.length-1].legs[computedProps.current_set.value.legs.length-1]; + }); } const mounted = onMounted(async () => { @@ -437,10 +489,10 @@ const xoi = { } } }) - return { ...computedProps, set_id, leg_id, gamestack, inspectstack, current_stat } + return { ...computedProps, set_id, leg_id, gamestack, inspectstack, current_stat, autodartsState} }, template: html` -
+
{{ current_toGo[0] }}
{{ current_toGo[1] }}
@@ -450,10 +502,10 @@ const xoi = {
` @@ -704,10 +756,11 @@ const gameStateMachine = (gamestack, stack, page, computedProps) => { 1: async (input, reGet) => { // Normal Game Loop // Get Game Input - let [visit, error] = await overlayAndPop("d-gameinput", { input: input }, gamestack); + const toGo = computedProps.current_toGo.value[computedProps.current_scorer_idx.value * 1] + let [visit, error] = await overlayAndPop("d-gameinput", { input: input, toGo: toGo}, gamestack); if (parseFloat(visit) < 0 ){ - visit = ""+(computedProps.current_toGo.value[computedProps.current_scorer_idx.value * 1] - parseFloat(visit)*-1); + visit = ""+(computedProps.current_toGo.value[computedProps.current_scorer_idx.value * 1] + parseFloat(visit)); } // back/delete last throw if (error != undefined) { @@ -722,15 +775,21 @@ const gameStateMachine = (gamestack, stack, page, computedProps) => { // Validate throw let numDarts, tries; - const [ret, sum] = computedProps.checkVisit(visit); + const [ret, sum, extra] = computedProps.checkVisit(visit); + [numDarts, tries] = extra; + const manual = !(visit.split(",").length > 1) && !visit.toLowerCase().startsWith("s") && !visit.toLowerCase().startsWith("d") && !visit.toLowerCase().startsWith("t") && !visit.toLowerCase().startsWith("b") + console.log(manual); + if (ret == -1) { // Impossible const [_, error] = await overlayAndPop("d-dialog", impossibleDialog(sum), stack); return [1, visit]; } else if (ret == -2) { // Bust TODO - const [_, error] = await overlayAndPop("d-dialog", impossibleDialog(sum), stack); - return [1, visit]; + // const [_, error] = await overlayAndPop("d-dialog", impossibleDialog(sum), stack); + storeVisit(page, visit.split(","), 0, 3, 0); + saveGame(page); + return [1, undefined]; } else if (ret == 3) { // Checkins const [ret, error] = await overlayAndPop("d-dialog", numCheckinTriesDialog(3), stack); @@ -745,10 +804,10 @@ const gameStateMachine = (gamestack, stack, page, computedProps) => { storeVisit(page, visit.split(","), sum, 3, 0); saveGame(page); return [1, undefined] - } else if (ret == 0) { + } else if (ret == 0 && manual) { // Checkout: Ask for num Darts [numDarts, tries] = await powerStateMachine(checkoutPipeline(stack), stack, /*initState=*/0, /*initInput=*/sum); - } else if (ret == 1) { + } else if (ret == 1 && manual) { // <=50: Ask for checkout tries [numDarts, tries] = await powerStateMachine(checkoutPipeline(stack), stack, /*initState=*/1, /*initInput=*/[3,0]); } @@ -758,9 +817,12 @@ const gameStateMachine = (gamestack, stack, page, computedProps) => { return [1, visit]; } else { const points = storeVisit(page, visit.split(","), sum, numDarts, tries); + console.log(points); + if (points){ // Game Over const winner = getFinalWinner(page); + let name = "DRAW" if (winner != -2){ name = page.scorers[winner].member.map((p) => p.forename).join(" + "); @@ -877,6 +939,7 @@ const stateMachine = (stack, page) => { const ret = await savePregame(page); return [2, page]; } + await reloadGame(page); return [4, result]; }, 4: async (winnerUUID, reGet) => { diff --git a/assets/js/websocket.js b/assets/js/websocket.js index ee7c456..7291521 100644 --- a/assets/js/websocket.js +++ b/assets/js/websocket.js @@ -1,12 +1,11 @@ +import { reactive, ref } from "vue"; + export function connect(endpoint, callback){ - const socket = new WebSocket("ws://localhost:4040/subscribe"); + const socket = new WebSocket(endpoint); // Connection opened socket.addEventListener("open", (event) => { - socket.send(JSON.stringify({ - "action": "subscribe", - "value": endpoint - })); + console.log("Connection open"); }); // Listen for messages @@ -16,3 +15,7 @@ export function connect(endpoint, callback){ socket.close(); }; } + +export let autodartsState = reactive({ + status: null +}) \ No newline at end of file diff --git a/site/templates/xoi.php b/site/templates/xoi.php index 14c9fba..51f6dbe 100644 --- a/site/templates/xoi.php +++ b/site/templates/xoi.php @@ -26,11 +26,54 @@ app.component("d-renderer", renderer); app.mount('main') - + document.addEventListener("keydown", (event) => { + if (event.key == "F6"){ + event.preventDefault(); + fetch('http://localhost:3180/api/reset', { + method: 'POST', + headers: { + 'Accept': 'application/json', + 'Content-Type': 'application/json' + }, + }) + } + if (event.key == "F7"){ + event.preventDefault(); + fetch('http://localhost:3180/api/start', { + method: 'PUT', + headers: { + 'Accept': 'application/json', + 'Content-Type': 'application/json' + }, + }) + } + if (event.key == "F8"){ + event.preventDefault(); + fetch('http://localhost:3180/api/stop', { + method: 'PUT', + headers: { + 'Accept': 'application/json', + 'Content-Type': 'application/json' + }, + }) + } + if (event.key == "u"){ + event.preventDefault(); + connect("ws://localhost:3180/api/events", (event) => { + let data = JSON.parse(event.data); + if(data.type == "state"){ + autodartsState.status = data.data.status; + const evt = new CustomEvent("autodarts", {detail: data}); + document.body.dispatchEvent(evt); + console.log("Message from server ", data); + console.log(autodartsState.status); + } + }); + } + }); +