From 8713d99950e7f3145e22cb682e94cbd3f0abf2a4 Mon Sep 17 00:00:00 2001 From: Samuel Plumppu <6125097+Greenheart@users.noreply.github.com> Date: Sat, 25 Sep 2021 15:36:12 +0200 Subject: [PATCH 01/19] Create initial setup and verify that tests work with the new setup --- .gitignore | 1 + .prettierrc | 4 + bower.json | 35 --- package-lock.json | 689 +++++++++++++++++++++++++++++++++++++++++++ package.json | 9 +- tests/css/styles.css | 6 +- vite.config.js | 5 + 7 files changed, 709 insertions(+), 40 deletions(-) create mode 100644 .gitignore create mode 100644 .prettierrc delete mode 100644 bower.json create mode 100644 package-lock.json create mode 100644 vite.config.js diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..b512c09d --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +node_modules \ No newline at end of file diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 00000000..6e778b4f --- /dev/null +++ b/.prettierrc @@ -0,0 +1,4 @@ +{ + "trailingComma": "all", + "singleQuote": true +} diff --git a/bower.json b/bower.json deleted file mode 100644 index cebe94d4..00000000 --- a/bower.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "howler.js", - "description": "Javascript audio library for the modern web.", - "homepage": "https://howlerjs.com", - "keywords": [ - "howler", - "howler.js", - "audio", - "sound", - "web audio", - "webaudio", - "html5", - "html5 audio", - "audio sprite", - "audiosprite" - ], - "authors": [ - "James Simpson (http://goldfirestudios.com)" - ], - "repository": { - "type": "git", - "url": "git://github.com/goldfire/howler.js.git" - }, - "main": "dist/howler.js", - "license": "MIT", - "moduleType": [ - "amd", - "globals", - "node" - ], - "ignore": [ - "tests", - "examples" - ] -} diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 00000000..9080a2f1 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,689 @@ +{ + "name": "howler", + "version": "3.0.0@alpha.0", + "lockfileVersion": 2, + "requires": true, + "packages": { + "": { + "version": "3.0.0@alpha.0", + "license": "MIT", + "devDependencies": { + "esbuild": "^0.13.2", + "prettier": "^2.4.1", + "typescript": "^4.4.3", + "vite": "^2.5.10" + } + }, + "node_modules/esbuild": { + "version": "0.13.2", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.13.2.tgz", + "integrity": "sha512-/tpIqo45hyRREGqh7hsIut8GwY1X2n9IhKbIwRIXUO6IohzG3/RarSGX7dT2eNvYzIbQmelpX+ZyuIphE5u+Bw==", + "dev": true, + "hasInstallScript": true, + "bin": { + "esbuild": "bin/esbuild" + }, + "optionalDependencies": { + "esbuild-android-arm64": "0.13.2", + "esbuild-darwin-64": "0.13.2", + "esbuild-darwin-arm64": "0.13.2", + "esbuild-freebsd-64": "0.13.2", + "esbuild-freebsd-arm64": "0.13.2", + "esbuild-linux-32": "0.13.2", + "esbuild-linux-64": "0.13.2", + "esbuild-linux-arm": "0.13.2", + "esbuild-linux-arm64": "0.13.2", + "esbuild-linux-mips64le": "0.13.2", + "esbuild-linux-ppc64le": "0.13.2", + "esbuild-openbsd-64": "0.13.2", + "esbuild-sunos-64": "0.13.2", + "esbuild-windows-32": "0.13.2", + "esbuild-windows-64": "0.13.2", + "esbuild-windows-arm64": "0.13.2" + } + }, + "node_modules/esbuild-android-arm64": { + "version": "0.13.2", + "resolved": "https://registry.npmjs.org/esbuild-android-arm64/-/esbuild-android-arm64-0.13.2.tgz", + "integrity": "sha512-Eh2paXUWYqf5JgikdkC0LnhtjSC8tGAz/L2kJRlMC0o3DzOBIxcmT2fdzBerdhW4roY0bOExfcO6deI1qsxI/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/esbuild-darwin-64": { + "version": "0.13.2", + "resolved": "https://registry.npmjs.org/esbuild-darwin-64/-/esbuild-darwin-64-0.13.2.tgz", + "integrity": "sha512-jqp6uXHIIAWZ8kxRqFjxyMmIE1cuSbINellwwigOgk44eLg74ls82oqjY72MbDAowPivQkOU/fF7tsyaGQf5Zg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/esbuild-darwin-arm64": { + "version": "0.13.2", + "resolved": "https://registry.npmjs.org/esbuild-darwin-arm64/-/esbuild-darwin-arm64-0.13.2.tgz", + "integrity": "sha512-bD4oAyPZdzOWEA/JoX0sAitOhjJRwhomhWMeyRyowtlVQhQleG2ijRUKTvkq4CAvSobrW5EnZxjvHNKJ5L7zJg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/esbuild-freebsd-64": { + "version": "0.13.2", + "resolved": "https://registry.npmjs.org/esbuild-freebsd-64/-/esbuild-freebsd-64-0.13.2.tgz", + "integrity": "sha512-fFJ0yc3lZyfwca+F5OPN/s+izozWryUQpN8aUMIdUkOa7UKX0h3xXrKnkDgdOo8vy3d1A6zHH0/4f2VJfEzLCg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/esbuild-freebsd-arm64": { + "version": "0.13.2", + "resolved": "https://registry.npmjs.org/esbuild-freebsd-arm64/-/esbuild-freebsd-arm64-0.13.2.tgz", + "integrity": "sha512-DWBZauEfjmqdfWxIacI+KBEim3ulOjtvK+WVm1bX67XlfyUVIkD915OIfT2EBhQUWmv+Z0tZZwskSMNj5DKojw==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/esbuild-linux-32": { + "version": "0.13.2", + "resolved": "https://registry.npmjs.org/esbuild-linux-32/-/esbuild-linux-32-0.13.2.tgz", + "integrity": "sha512-Gt2rNqqRCRh1QQC2d83KP0iWIXWQYpns7l2+41a1n0PQxXkQ5AarpjjL9mUzdXtcZauNXbUvWwBKAuBTCW+XQg==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/esbuild-linux-64": { + "version": "0.13.2", + "resolved": "https://registry.npmjs.org/esbuild-linux-64/-/esbuild-linux-64-0.13.2.tgz", + "integrity": "sha512-yT0D5Xly8oGHuqq975k1XUyULHzk3fN/ZlTY+awlU+nCFsYPZ43NE5msGpxlNQu8i6KOXQEke5GXN3y5d+Zd4g==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/esbuild-linux-arm": { + "version": "0.13.2", + "resolved": "https://registry.npmjs.org/esbuild-linux-arm/-/esbuild-linux-arm-0.13.2.tgz", + "integrity": "sha512-KXeyotqj9jbvCjbSpwnxDE8E8jKoBgrgbJpOvvY5Zz7Pp2fAwu/94vWQtE/jPEJndY4C4MSs+ryJLFWzmLOa4w==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/esbuild-linux-arm64": { + "version": "0.13.2", + "resolved": "https://registry.npmjs.org/esbuild-linux-arm64/-/esbuild-linux-arm64-0.13.2.tgz", + "integrity": "sha512-qwXL+3NDCWiC8RMKBBETpuOWdC+pUAUS+pwg9jJmapYblLdVKkyRtwF/ogj06TdYs6riSSNikW8HK/Xs0HHbbQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/esbuild-linux-mips64le": { + "version": "0.13.2", + "resolved": "https://registry.npmjs.org/esbuild-linux-mips64le/-/esbuild-linux-mips64le-0.13.2.tgz", + "integrity": "sha512-sx8eheRX2XC2ppNAsbQm8/VUcU8XPYGpJK0BEyRefqHONz6u5Ib2guUdOz2Wh4YlbA7oOd482lHjprXSTwUcrQ==", + "cpu": [ + "mips64el" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/esbuild-linux-ppc64le": { + "version": "0.13.2", + "resolved": "https://registry.npmjs.org/esbuild-linux-ppc64le/-/esbuild-linux-ppc64le-0.13.2.tgz", + "integrity": "sha512-y8iZ3qy2TIAKKsZ6xSopCztHOtGW9oiYDl22vQ0UIoVWjnfRKrbSzX7Y2F94y32hSvRWle6OhAIC+UpS5nQmIA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/esbuild-openbsd-64": { + "version": "0.13.2", + "resolved": "https://registry.npmjs.org/esbuild-openbsd-64/-/esbuild-openbsd-64-0.13.2.tgz", + "integrity": "sha512-g6AYrjBeV9OK624bw0KQ1TjHJQSW+X1Yicyd1NvDWqSFpMqKAjw7EUX4tA87mOFqv8BflPGr4f43ySgNvSVzIw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/esbuild-sunos-64": { + "version": "0.13.2", + "resolved": "https://registry.npmjs.org/esbuild-sunos-64/-/esbuild-sunos-64-0.13.2.tgz", + "integrity": "sha512-hIXvFIyrqwFd6v62XSra0ctCUXDS9Tu5D6QYbvnbhEoBmvD/TmEJRYRH48/+xmRifKJLzu6aegcrjAsDmaww7g==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "sunos" + ] + }, + "node_modules/esbuild-windows-32": { + "version": "0.13.2", + "resolved": "https://registry.npmjs.org/esbuild-windows-32/-/esbuild-windows-32-0.13.2.tgz", + "integrity": "sha512-Y767LG0NFkw0sPoDVOTKC5gaj4vURjjWfSCCDV5awpXXxBKOF2zsIp3aia4KvVoivoSSeRPk3emDd0OPHuPrKg==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/esbuild-windows-64": { + "version": "0.13.2", + "resolved": "https://registry.npmjs.org/esbuild-windows-64/-/esbuild-windows-64-0.13.2.tgz", + "integrity": "sha512-01b59kVJUMasctn6lzswC0drchr7zO75QtF22o5w0nlOw0Zorw0loY/8i5choFuWc30gXJId9qBSc1zPvt7uEw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/esbuild-windows-arm64": { + "version": "0.13.2", + "resolved": "https://registry.npmjs.org/esbuild-windows-arm64/-/esbuild-windows-arm64-0.13.2.tgz", + "integrity": "sha512-HxyY604ytmh8NkPYyS1TdIB/bFS7DWd1hP90e8Ovo/elEdN5I13h0tyIatDYZkXKS0Ztk+9T/3h6K0fI1a/4tQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", + "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", + "dev": true + }, + "node_modules/has": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", + "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", + "dev": true, + "dependencies": { + "function-bind": "^1.1.1" + }, + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/is-core-module": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.6.0.tgz", + "integrity": "sha512-wShG8vs60jKfPWpF2KZRaAtvt3a20OAn7+IJ6hLPECpSABLcKtFKTTI4ZtH5QcBruBHlq+WsdHWyz0BCZW7svQ==", + "dev": true, + "dependencies": { + "has": "^1.0.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/nanocolors": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/nanocolors/-/nanocolors-0.2.3.tgz", + "integrity": "sha512-RxGTOApG8prHMA08UBMOT6qYzcBBW2EMBv7SRBqoXg/Dqp6G3yT7kLy1tpFuYLO+5h7eajmdiIHJA8oewN58XQ==", + "dev": true + }, + "node_modules/nanoid": { + "version": "3.1.25", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.1.25.tgz", + "integrity": "sha512-rdwtIXaXCLFAQbnfqDRnI6jaRHp9fTcYBjtFKE8eezcZ7LuLjhUaQGNeMXf1HmRoCH32CLz6XwX0TtxEOS/A3Q==", + "dev": true, + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true + }, + "node_modules/postcss": { + "version": "8.3.8", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.3.8.tgz", + "integrity": "sha512-GT5bTjjZnwDifajzczOC+r3FI3Cu+PgPvrsjhQdRqa2kTJ4968/X9CUce9xttIB0xOs5c6xf0TCWZo/y9lF6bA==", + "dev": true, + "dependencies": { + "nanocolors": "^0.2.2", + "nanoid": "^3.1.25", + "source-map-js": "^0.6.2" + }, + "engines": { + "node": "^10 || ^12 || >=14" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + } + }, + "node_modules/prettier": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.4.1.tgz", + "integrity": "sha512-9fbDAXSBcc6Bs1mZrDYb3XKzDLm4EXXL9sC1LqKP5rZkT6KRr/rf9amVUcODVXgguK/isJz0d0hP72WeaKWsvA==", + "dev": true, + "bin": { + "prettier": "bin-prettier.js" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/resolve": { + "version": "1.20.0", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.20.0.tgz", + "integrity": "sha512-wENBPt4ySzg4ybFQW2TT1zMQucPK95HSh/nq2CFTZVOGut2+pQvSsgtda4d26YrYcr067wjbmzOG8byDPBX63A==", + "dev": true, + "dependencies": { + "is-core-module": "^2.2.0", + "path-parse": "^1.0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/rollup": { + "version": "2.57.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-2.57.0.tgz", + "integrity": "sha512-bKQIh1rWKofRee6mv8SrF2HdP6pea5QkwBZSMImJysFj39gQuiV8MEPBjXOCpzk3wSYp63M2v2wkWBmFC8O/rg==", + "dev": true, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=10.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/source-map-js": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-0.6.2.tgz", + "integrity": "sha512-/3GptzWzu0+0MBQFrDKzw/DvvMTUORvgY6k6jd/VS6iCR4RDTKWH6v6WPwQoUO8667uQEf9Oe38DxAYWY5F/Ug==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/typescript": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.4.3.tgz", + "integrity": "sha512-4xfscpisVgqqDfPaJo5vkd+Qd/ItkoagnHpufr+i2QCHBsNYp+G7UAoyFl8aPtx879u38wPV65rZ8qbGZijalA==", + "dev": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=4.2.0" + } + }, + "node_modules/vite": { + "version": "2.5.10", + "resolved": "https://registry.npmjs.org/vite/-/vite-2.5.10.tgz", + "integrity": "sha512-0ObiHTi5AHyXdJcvZ67HMsDgVpjT5RehvVKv6+Q0jFZ7zDI28PF5zK9mYz2avxdA+4iJMdwCz6wnGNnn4WX5Gg==", + "dev": true, + "dependencies": { + "esbuild": "^0.12.17", + "postcss": "^8.3.6", + "resolve": "^1.20.0", + "rollup": "^2.38.5" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": ">=12.2.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/vite/node_modules/esbuild": { + "version": "0.12.29", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.12.29.tgz", + "integrity": "sha512-w/XuoBCSwepyiZtIRsKsetiLDUVGPVw1E/R3VTFSecIy8UR7Cq3SOtwKHJMFoVqqVG36aGkzh4e8BvpO1Fdc7g==", + "dev": true, + "hasInstallScript": true, + "bin": { + "esbuild": "bin/esbuild" + } + } + }, + "dependencies": { + "esbuild": { + "version": "0.13.2", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.13.2.tgz", + "integrity": "sha512-/tpIqo45hyRREGqh7hsIut8GwY1X2n9IhKbIwRIXUO6IohzG3/RarSGX7dT2eNvYzIbQmelpX+ZyuIphE5u+Bw==", + "dev": true, + "requires": { + "esbuild-android-arm64": "0.13.2", + "esbuild-darwin-64": "0.13.2", + "esbuild-darwin-arm64": "0.13.2", + "esbuild-freebsd-64": "0.13.2", + "esbuild-freebsd-arm64": "0.13.2", + "esbuild-linux-32": "0.13.2", + "esbuild-linux-64": "0.13.2", + "esbuild-linux-arm": "0.13.2", + "esbuild-linux-arm64": "0.13.2", + "esbuild-linux-mips64le": "0.13.2", + "esbuild-linux-ppc64le": "0.13.2", + "esbuild-openbsd-64": "0.13.2", + "esbuild-sunos-64": "0.13.2", + "esbuild-windows-32": "0.13.2", + "esbuild-windows-64": "0.13.2", + "esbuild-windows-arm64": "0.13.2" + } + }, + "esbuild-android-arm64": { + "version": "0.13.2", + "resolved": "https://registry.npmjs.org/esbuild-android-arm64/-/esbuild-android-arm64-0.13.2.tgz", + "integrity": "sha512-Eh2paXUWYqf5JgikdkC0LnhtjSC8tGAz/L2kJRlMC0o3DzOBIxcmT2fdzBerdhW4roY0bOExfcO6deI1qsxI/A==", + "dev": true, + "optional": true + }, + "esbuild-darwin-64": { + "version": "0.13.2", + "resolved": "https://registry.npmjs.org/esbuild-darwin-64/-/esbuild-darwin-64-0.13.2.tgz", + "integrity": "sha512-jqp6uXHIIAWZ8kxRqFjxyMmIE1cuSbINellwwigOgk44eLg74ls82oqjY72MbDAowPivQkOU/fF7tsyaGQf5Zg==", + "dev": true, + "optional": true + }, + "esbuild-darwin-arm64": { + "version": "0.13.2", + "resolved": "https://registry.npmjs.org/esbuild-darwin-arm64/-/esbuild-darwin-arm64-0.13.2.tgz", + "integrity": "sha512-bD4oAyPZdzOWEA/JoX0sAitOhjJRwhomhWMeyRyowtlVQhQleG2ijRUKTvkq4CAvSobrW5EnZxjvHNKJ5L7zJg==", + "dev": true, + "optional": true + }, + "esbuild-freebsd-64": { + "version": "0.13.2", + "resolved": "https://registry.npmjs.org/esbuild-freebsd-64/-/esbuild-freebsd-64-0.13.2.tgz", + "integrity": "sha512-fFJ0yc3lZyfwca+F5OPN/s+izozWryUQpN8aUMIdUkOa7UKX0h3xXrKnkDgdOo8vy3d1A6zHH0/4f2VJfEzLCg==", + "dev": true, + "optional": true + }, + "esbuild-freebsd-arm64": { + "version": "0.13.2", + "resolved": "https://registry.npmjs.org/esbuild-freebsd-arm64/-/esbuild-freebsd-arm64-0.13.2.tgz", + "integrity": "sha512-DWBZauEfjmqdfWxIacI+KBEim3ulOjtvK+WVm1bX67XlfyUVIkD915OIfT2EBhQUWmv+Z0tZZwskSMNj5DKojw==", + "dev": true, + "optional": true + }, + "esbuild-linux-32": { + "version": "0.13.2", + "resolved": "https://registry.npmjs.org/esbuild-linux-32/-/esbuild-linux-32-0.13.2.tgz", + "integrity": "sha512-Gt2rNqqRCRh1QQC2d83KP0iWIXWQYpns7l2+41a1n0PQxXkQ5AarpjjL9mUzdXtcZauNXbUvWwBKAuBTCW+XQg==", + "dev": true, + "optional": true + }, + "esbuild-linux-64": { + "version": "0.13.2", + "resolved": "https://registry.npmjs.org/esbuild-linux-64/-/esbuild-linux-64-0.13.2.tgz", + "integrity": "sha512-yT0D5Xly8oGHuqq975k1XUyULHzk3fN/ZlTY+awlU+nCFsYPZ43NE5msGpxlNQu8i6KOXQEke5GXN3y5d+Zd4g==", + "dev": true, + "optional": true + }, + "esbuild-linux-arm": { + "version": "0.13.2", + "resolved": "https://registry.npmjs.org/esbuild-linux-arm/-/esbuild-linux-arm-0.13.2.tgz", + "integrity": "sha512-KXeyotqj9jbvCjbSpwnxDE8E8jKoBgrgbJpOvvY5Zz7Pp2fAwu/94vWQtE/jPEJndY4C4MSs+ryJLFWzmLOa4w==", + "dev": true, + "optional": true + }, + "esbuild-linux-arm64": { + "version": "0.13.2", + "resolved": "https://registry.npmjs.org/esbuild-linux-arm64/-/esbuild-linux-arm64-0.13.2.tgz", + "integrity": "sha512-qwXL+3NDCWiC8RMKBBETpuOWdC+pUAUS+pwg9jJmapYblLdVKkyRtwF/ogj06TdYs6riSSNikW8HK/Xs0HHbbQ==", + "dev": true, + "optional": true + }, + "esbuild-linux-mips64le": { + "version": "0.13.2", + "resolved": "https://registry.npmjs.org/esbuild-linux-mips64le/-/esbuild-linux-mips64le-0.13.2.tgz", + "integrity": "sha512-sx8eheRX2XC2ppNAsbQm8/VUcU8XPYGpJK0BEyRefqHONz6u5Ib2guUdOz2Wh4YlbA7oOd482lHjprXSTwUcrQ==", + "dev": true, + "optional": true + }, + "esbuild-linux-ppc64le": { + "version": "0.13.2", + "resolved": "https://registry.npmjs.org/esbuild-linux-ppc64le/-/esbuild-linux-ppc64le-0.13.2.tgz", + "integrity": "sha512-y8iZ3qy2TIAKKsZ6xSopCztHOtGW9oiYDl22vQ0UIoVWjnfRKrbSzX7Y2F94y32hSvRWle6OhAIC+UpS5nQmIA==", + "dev": true, + "optional": true + }, + "esbuild-openbsd-64": { + "version": "0.13.2", + "resolved": "https://registry.npmjs.org/esbuild-openbsd-64/-/esbuild-openbsd-64-0.13.2.tgz", + "integrity": "sha512-g6AYrjBeV9OK624bw0KQ1TjHJQSW+X1Yicyd1NvDWqSFpMqKAjw7EUX4tA87mOFqv8BflPGr4f43ySgNvSVzIw==", + "dev": true, + "optional": true + }, + "esbuild-sunos-64": { + "version": "0.13.2", + "resolved": "https://registry.npmjs.org/esbuild-sunos-64/-/esbuild-sunos-64-0.13.2.tgz", + "integrity": "sha512-hIXvFIyrqwFd6v62XSra0ctCUXDS9Tu5D6QYbvnbhEoBmvD/TmEJRYRH48/+xmRifKJLzu6aegcrjAsDmaww7g==", + "dev": true, + "optional": true + }, + "esbuild-windows-32": { + "version": "0.13.2", + "resolved": "https://registry.npmjs.org/esbuild-windows-32/-/esbuild-windows-32-0.13.2.tgz", + "integrity": "sha512-Y767LG0NFkw0sPoDVOTKC5gaj4vURjjWfSCCDV5awpXXxBKOF2zsIp3aia4KvVoivoSSeRPk3emDd0OPHuPrKg==", + "dev": true, + "optional": true + }, + "esbuild-windows-64": { + "version": "0.13.2", + "resolved": "https://registry.npmjs.org/esbuild-windows-64/-/esbuild-windows-64-0.13.2.tgz", + "integrity": "sha512-01b59kVJUMasctn6lzswC0drchr7zO75QtF22o5w0nlOw0Zorw0loY/8i5choFuWc30gXJId9qBSc1zPvt7uEw==", + "dev": true, + "optional": true + }, + "esbuild-windows-arm64": { + "version": "0.13.2", + "resolved": "https://registry.npmjs.org/esbuild-windows-arm64/-/esbuild-windows-arm64-0.13.2.tgz", + "integrity": "sha512-HxyY604ytmh8NkPYyS1TdIB/bFS7DWd1hP90e8Ovo/elEdN5I13h0tyIatDYZkXKS0Ztk+9T/3h6K0fI1a/4tQ==", + "dev": true, + "optional": true + }, + "fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "optional": true + }, + "function-bind": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", + "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", + "dev": true + }, + "has": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", + "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", + "dev": true, + "requires": { + "function-bind": "^1.1.1" + } + }, + "is-core-module": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.6.0.tgz", + "integrity": "sha512-wShG8vs60jKfPWpF2KZRaAtvt3a20OAn7+IJ6hLPECpSABLcKtFKTTI4ZtH5QcBruBHlq+WsdHWyz0BCZW7svQ==", + "dev": true, + "requires": { + "has": "^1.0.3" + } + }, + "nanocolors": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/nanocolors/-/nanocolors-0.2.3.tgz", + "integrity": "sha512-RxGTOApG8prHMA08UBMOT6qYzcBBW2EMBv7SRBqoXg/Dqp6G3yT7kLy1tpFuYLO+5h7eajmdiIHJA8oewN58XQ==", + "dev": true + }, + "nanoid": { + "version": "3.1.25", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.1.25.tgz", + "integrity": "sha512-rdwtIXaXCLFAQbnfqDRnI6jaRHp9fTcYBjtFKE8eezcZ7LuLjhUaQGNeMXf1HmRoCH32CLz6XwX0TtxEOS/A3Q==", + "dev": true + }, + "path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true + }, + "postcss": { + "version": "8.3.8", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.3.8.tgz", + "integrity": "sha512-GT5bTjjZnwDifajzczOC+r3FI3Cu+PgPvrsjhQdRqa2kTJ4968/X9CUce9xttIB0xOs5c6xf0TCWZo/y9lF6bA==", + "dev": true, + "requires": { + "nanocolors": "^0.2.2", + "nanoid": "^3.1.25", + "source-map-js": "^0.6.2" + } + }, + "prettier": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.4.1.tgz", + "integrity": "sha512-9fbDAXSBcc6Bs1mZrDYb3XKzDLm4EXXL9sC1LqKP5rZkT6KRr/rf9amVUcODVXgguK/isJz0d0hP72WeaKWsvA==", + "dev": true + }, + "resolve": { + "version": "1.20.0", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.20.0.tgz", + "integrity": "sha512-wENBPt4ySzg4ybFQW2TT1zMQucPK95HSh/nq2CFTZVOGut2+pQvSsgtda4d26YrYcr067wjbmzOG8byDPBX63A==", + "dev": true, + "requires": { + "is-core-module": "^2.2.0", + "path-parse": "^1.0.6" + } + }, + "rollup": { + "version": "2.57.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-2.57.0.tgz", + "integrity": "sha512-bKQIh1rWKofRee6mv8SrF2HdP6pea5QkwBZSMImJysFj39gQuiV8MEPBjXOCpzk3wSYp63M2v2wkWBmFC8O/rg==", + "dev": true, + "requires": { + "fsevents": "~2.3.2" + } + }, + "source-map-js": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-0.6.2.tgz", + "integrity": "sha512-/3GptzWzu0+0MBQFrDKzw/DvvMTUORvgY6k6jd/VS6iCR4RDTKWH6v6WPwQoUO8667uQEf9Oe38DxAYWY5F/Ug==", + "dev": true + }, + "typescript": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.4.3.tgz", + "integrity": "sha512-4xfscpisVgqqDfPaJo5vkd+Qd/ItkoagnHpufr+i2QCHBsNYp+G7UAoyFl8aPtx879u38wPV65rZ8qbGZijalA==", + "dev": true + }, + "vite": { + "version": "2.5.10", + "resolved": "https://registry.npmjs.org/vite/-/vite-2.5.10.tgz", + "integrity": "sha512-0ObiHTi5AHyXdJcvZ67HMsDgVpjT5RehvVKv6+Q0jFZ7zDI28PF5zK9mYz2avxdA+4iJMdwCz6wnGNnn4WX5Gg==", + "dev": true, + "requires": { + "esbuild": "^0.12.17", + "fsevents": "~2.3.2", + "postcss": "^8.3.6", + "resolve": "^1.20.0", + "rollup": "^2.38.5" + }, + "dependencies": { + "esbuild": { + "version": "0.12.29", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.12.29.tgz", + "integrity": "sha512-w/XuoBCSwepyiZtIRsKsetiLDUVGPVw1E/R3VTFSecIy8UR7Cq3SOtwKHJMFoVqqVG36aGkzh4e8BvpO1Fdc7g==", + "dev": true + } + } + } + } +} diff --git a/package.json b/package.json index e3c222d1..a97c3e15 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "howler", - "version": "2.2.3", + "version": "3.0.0@alpha.0", "description": "Javascript audio library for the modern web.", "homepage": "https://howlerjs.com", "keywords": [ @@ -22,11 +22,16 @@ "url": "git://github.com/goldfire/howler.js.git" }, "scripts": { + "test": "vite", + "test:network": "vite --host", "build": "VERSION=`printf 'v' && node -e 'console.log(require(\"./package.json\").version)'` && sed -i '' '2s/.*/ * howler.js '\"$VERSION\"'/' src/howler.core.js && sed -i '' '4s/.*/ * howler.js '\"$VERSION\"'/' src/plugins/howler.spatial.js && uglifyjs --preamble \"/*! howler.js $VERSION | (c) 2013-2020, James Simpson of GoldFire Studios | MIT License | howlerjs.com */\" src/howler.core.js -c -m --screw-ie8 -o dist/howler.core.min.js && uglifyjs --preamble \"/*! howler.js $VERSION | Spatial Plugin | (c) 2013-2020, James Simpson of GoldFire Studios | MIT License | howlerjs.com */\" src/plugins/howler.spatial.js -c -m --screw-ie8 -o dist/howler.spatial.min.js && awk 'FNR==1{echo \"\"}1' dist/howler.core.min.js dist/howler.spatial.min.js | sed '3s~.*~/*! Spatial Plugin */~' | perl -pe 'chomp if eof' > dist/howler.min.js && awk '(NR>1 && FNR==1){printf (\"\\n\\n\")};1' src/howler.core.js src/plugins/howler.spatial.js > dist/howler.js", "release": "VERSION=`printf 'v' && node -e 'console.log(require(\"./package.json\").version)'` && git tag $VERSION && git push && git push origin $VERSION && npm publish" }, "devDependencies": { - "uglify-js": "2.x" + "esbuild": "^0.13.2", + "typescript": "^4.4.3", + "vite": "^2.5.10", + "prettier": "^2.4.1" }, "main": "dist/howler.js", "license": "MIT", diff --git a/tests/css/styles.css b/tests/css/styles.css index 501b568e..de1af0a5 100644 --- a/tests/css/styles.css +++ b/tests/css/styles.css @@ -1,6 +1,6 @@ html { width: 100%; - height: 100%; + height: 100%; overflow: hidden; padding: 0; margin: 0; @@ -36,7 +36,7 @@ body { height: 100%; text-align: center; } -.button{ +.button { background: #a2998c; border: none; outline: 0; @@ -93,4 +93,4 @@ body { .button { padding: 5px; } -} \ No newline at end of file +} diff --git a/vite.config.js b/vite.config.js new file mode 100644 index 00000000..38c20e5a --- /dev/null +++ b/vite.config.js @@ -0,0 +1,5 @@ +import { defineConfig } from "vite"; + +export default defineConfig({ + root: "tests", +}); From f79ceb3b2b24ebf1878081c1a18d2d4396770c73 Mon Sep 17 00:00:00 2001 From: Samuel Plumppu <6125097+Greenheart@users.noreply.github.com> Date: Sat, 25 Sep 2021 17:06:15 +0200 Subject: [PATCH 02/19] Initial refactoring to ES2015 classes and modules --- package.json | 4 +- src/core.ts | 11 + src/helpers.ts | 202 ++++++ src/howl.ts | 1723 ++++++++++++++++++++++++++++++++++++++++++++++++ src/howler.ts | 578 ++++++++++++++++ src/sound.ts | 183 +++++ tsconfig.json | 17 + 7 files changed, 2716 insertions(+), 2 deletions(-) create mode 100644 src/core.ts create mode 100644 src/helpers.ts create mode 100644 src/howl.ts create mode 100644 src/howler.ts create mode 100644 src/sound.ts create mode 100644 tsconfig.json diff --git a/package.json b/package.json index a97c3e15..47df509e 100644 --- a/package.json +++ b/package.json @@ -29,9 +29,9 @@ }, "devDependencies": { "esbuild": "^0.13.2", + "prettier": "^2.4.1", "typescript": "^4.4.3", - "vite": "^2.5.10", - "prettier": "^2.4.1" + "vite": "^2.5.10" }, "main": "dist/howler.js", "license": "MIT", diff --git a/src/core.ts b/src/core.ts new file mode 100644 index 00000000..99c700af --- /dev/null +++ b/src/core.ts @@ -0,0 +1,11 @@ +/*! + * howler.js v2.2.3 + * howlerjs.com + * + * (c) 2013-2020, James Simpson of GoldFire Studios + * goldfirestudios.com + * + * MIT License + */ +export Howler from './howler' +export Howl from './howl' \ No newline at end of file diff --git a/src/helpers.ts b/src/helpers.ts new file mode 100644 index 00000000..7b167157 --- /dev/null +++ b/src/helpers.ts @@ -0,0 +1,202 @@ +import Howler from './howler'; + +const cache = {}; + +/** + * Buffer a sound from URL, Data URI or cache and decode to audio source (Web Audio API). + * @param {Howl} self + */ +export function loadBuffer(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 < data.length; ++i) { + dataView[i] = data.charCodeAt(i); + } + + decodeAudioData(dataView.buffer, self); + } else { + // Load the buffer from the URL. + var xhr = new XMLHttpRequest(); + xhr.open(self._xhr.method, url, true); + xhr.withCredentials = self._xhr.withCredentials; + xhr.responseType = 'arraybuffer'; + + // Apply any custom headers to the request. + if (self._xhr.headers) { + Object.keys(self._xhr.headers).forEach(function (key) { + xhr.setRequestHeader(key, self._xhr.headers[key]); + }); + } + + xhr.onload = function () { + // Make sure we get a successful response back. + var code = (xhr.status + '')[0]; + if (code !== '0' && code !== '2' && code !== '3') { + self._emit( + 'loaderror', + null, + 'Failed loading audio file with status: ' + xhr.status + '.', + ); + return; + } + + decodeAudioData(xhr.response, self); + }; + xhr.onerror = function () { + // If there is an error, switch to HTML5 Audio. + if (self._webAudio) { + self._html5 = true; + self._webAudio = false; + self._sounds = []; + delete cache[url]; + self.load(); + } + }; + safeXhrSend(xhr); + } +} + +/** + * Send the XHR request wrapped in a try/catch. + * @param {Object} xhr XHR to send. + */ +function safeXhrSend(xhr) { + try { + xhr.send(); + } catch (e) { + xhr.onerror(); + } +} + +/** + * Decode audio data from an array buffer. + * @param {ArrayBuffer} arraybuffer The audio data. + * @param {Howl} self + */ +function decodeAudioData(arraybuffer, self) { + // Fire a load error if something broke. + var error = function () { + self._emit('loaderror', null, 'Decoding audio data failed.'); + }; + + // Load the sound on success. + var success = function (buffer) { + if (buffer && self._sounds.length > 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. + */ +function loadSound(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. + */ +export function setupAudioContext() { + // 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(); +} diff --git a/src/howl.ts b/src/howl.ts new file mode 100644 index 00000000..36007938 --- /dev/null +++ b/src/howl.ts @@ -0,0 +1,1723 @@ +import Howler from './howler'; +import { loadBuffer, setupAudioContext } from './helpers'; +import Sound from './sound'; + +class Howl { + /** + * Create an audio group controller. + * @param {Object} o Passed in properties for this group. + */ + constructor(o) { + // Throw an error if no source is provided. + if (!o.src || o.src.length === 0) { + console.error( + 'An array of source files must be passed with any new Howl.', + ); + return; + } + + this.init(o); + } + + /** + * Initialize a new Howl group object. + * @param {Object} o Passed in properties for this group. + * @return {Howl} + */ + init(o) { + // If we don't have an AudioContext created yet, run the setup. + if (!Howler.ctx) { + setupAudioContext(); + } + + // Setup user-defined default properties. + this._autoplay = o.autoplay || false; + this._format = typeof o.format !== 'string' ? o.format : [o.format]; + this._html5 = o.html5 || false; + this._muted = o.mute || false; + this._loop = o.loop || false; + this._pool = o.pool || 5; + this._preload = + typeof o.preload === 'boolean' || o.preload === 'metadata' + ? o.preload + : true; + this._rate = o.rate || 1; + this._sprite = o.sprite || {}; + this._src = typeof o.src !== 'string' ? o.src : [o.src]; + this._volume = o.volume !== undefined ? o.volume : 1; + this._xhr = { + method: o.xhr && o.xhr.method ? o.xhr.method : 'GET', + headers: o.xhr && o.xhr.headers ? o.xhr.headers : null, + withCredentials: + o.xhr && o.xhr.withCredentials ? o.xhr.withCredentials : false, + }; + + // Setup all other default properties. + this._duration = 0; + this._state = 'unloaded'; + this._sounds = []; + this._endTimers = {}; + this._queue = []; + this._playLock = false; + + // Setup event listeners. + this._onend = o.onend ? [{ fn: o.onend }] : []; + this._onfade = o.onfade ? [{ fn: o.onfade }] : []; + this._onload = o.onload ? [{ fn: o.onload }] : []; + this._onloaderror = o.onloaderror ? [{ fn: o.onloaderror }] : []; + this._onplayerror = o.onplayerror ? [{ fn: o.onplayerror }] : []; + this._onpause = o.onpause ? [{ fn: o.onpause }] : []; + this._onplay = o.onplay ? [{ fn: o.onplay }] : []; + this._onstop = o.onstop ? [{ fn: o.onstop }] : []; + this._onmute = o.onmute ? [{ fn: o.onmute }] : []; + this._onvolume = o.onvolume ? [{ fn: o.onvolume }] : []; + this._onrate = o.onrate ? [{ fn: o.onrate }] : []; + this._onseek = o.onseek ? [{ fn: o.onseek }] : []; + this._onunlock = o.onunlock ? [{ fn: o.onunlock }] : []; + this._onresume = []; + + // Web Audio or HTML5 Audio? + this._webAudio = Howler.usingWebAudio && !this._html5; + + // Automatically try to enable audio. + if (typeof Howler.ctx !== 'undefined' && Howler.ctx && Howler.autoUnlock) { + Howler._unlockAudio(); + } + + // Keep track of this Howl group in the global controller. + Howler._howls.push(this); + + // If they selected autoplay, add a play event to the load queue. + if (this._autoplay) { + this._queue.push({ + event: 'play', + action() { + this.play(); + }, + }); + } + + // Load the source file unless otherwise specified. + if (this._preload && this._preload !== 'none') { + this.load(); + } + + return this; + } + + /** + * Load the audio file. + * @return {Howler} + */ + load() { + var url = null; + + // If no audio is available, quit immediately. + if (Howler.noAudio) { + this._emit('loaderror', null, 'No audio support.'); + return; + } + + // Make sure our source is in an array. + if (typeof this._src === 'string') { + this._src = [this._src]; + } + + // Loop through the sources and pick the first one that is compatible. + for (var i = 0; i < this._src.length; i++) { + var ext, str; + + if (this._format && this._format[i]) { + // If an extension was specified, use that instead. + ext = this._format[i]; + } else { + // Make sure the source is a string. + str = this._src[i]; + if (typeof str !== 'string') { + this._emit( + 'loaderror', + null, + 'Non-string found in selected audio sources - ignoring.', + ); + continue; + } + + // Extract the file extension from the URL or base64 data URI. + ext = /^data:audio\/([^;,]+);/i.exec(str); + if (!ext) { + ext = /\.([^.]+)$/.exec(str.split('?', 1)[0]); + } + + if (ext) { + ext = ext[1].toLowerCase(); + } + } + + // Log a warning if no extension was found. + if (!ext) { + console.warn( + 'No file extension was found. Consider using the "format" property or specify an extension.', + ); + } + + // Check if this extension is available. + if (ext && Howler.codecs(ext)) { + url = this._src[i]; + break; + } + } + + if (!url) { + this._emit( + 'loaderror', + null, + 'No codec support for selected audio sources.', + ); + return; + } + + this._src = url; + this._state = 'loading'; + + // If the hosting page is HTTPS and the source isn't, + // drop down to HTML5 Audio to avoid Mixed Content errors. + if (window.location.protocol === 'https:' && url.slice(0, 5) === 'http:') { + this._html5 = true; + this._webAudio = false; + } + + // Create a new sound object and add it to the pool. + new Sound(this); + + // Load and decode the audio data for playback. + if (this._webAudio) { + loadBuffer(this); + } + + return this; + } + + /** + * Play a sound or resume previous playback. + * @param {String/Number} sprite Sprite name for sprite playback or sound id to continue previous. + * @param {Boolean} internal Internal Use: true prevents event firing. + * @return {Number} Sound ID. + */ + play(sprite, internal) { + var id = null; + + // Determine if a sprite, sound id or nothing was passed + if (typeof sprite === 'number') { + id = sprite; + sprite = null; + } else if ( + typeof sprite === 'string' && + this._state === 'loaded' && + !this._sprite[sprite] + ) { + // If the passed sprite doesn't exist, do nothing. + return null; + } else if (typeof sprite === 'undefined') { + // Use the default sound sprite (plays the full audio length). + sprite = '__default'; + + // Check if there is a single paused sound that isn't ended. + // If there is, play that sound. If not, continue as usual. + if (!this._playLock) { + var num = 0; + for (var i = 0; i < this._sounds.length; i++) { + if (this._sounds[i]._paused && !this._sounds[i]._ended) { + num++; + id = this._sounds[i]._id; + } + } + + if (num === 1) { + sprite = null; + } else { + id = null; + } + } + } + + // Get the selected node, or get one from the pool. + var sound = id ? this._soundById(id) : this._inactiveSound(); + + // If the sound doesn't exist, do nothing. + if (!sound) { + return null; + } + + // Select the sprite definition. + if (id && !sprite) { + sprite = sound._sprite || '__default'; + } + + // If the sound hasn't loaded, we must wait to get the audio's duration. + // We also need to wait to make sure we don't run into race conditions with + // the order of function calls. + if (this._state !== 'loaded') { + // Set the sprite value on this sound. + sound._sprite = sprite; + + // Mark this sound as not ended in case another sound is played before this one loads. + sound._ended = false; + + // Add the sound to the queue to be played on load. + var soundId = sound._id; + this._queue.push({ + event: 'play', + action() { + this.play(soundId); + }, + }); + + return soundId; + } + + // Don't play the sound if an id was passed and it is already playing. + if (id && !sound._paused) { + // Trigger the play event, in order to keep iterating through queue. + if (!internal) { + this._loadQueue('play'); + } + + return sound._id; + } + + // Make sure the AudioContext isn't suspended, and resume it if it is. + if (this._webAudio) { + Howler._autoResume(); + } + + // Determine how long to play for and where to start playing. + var seek = Math.max( + 0, + sound._seek > 0 ? sound._seek : this._sprite[sprite][0] / 1000, + ); + var duration = Math.max( + 0, + (this._sprite[sprite][0] + this._sprite[sprite][1]) / 1000 - seek, + ); + var timeout = (duration * 1000) / Math.abs(sound._rate); + var start = this._sprite[sprite][0] / 1000; + var stop = (this._sprite[sprite][0] + this._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 || this._sprite[sprite][2]); + }; + + // End the sound instantly if seek is at the end. + if (seek >= stop) { + this._ended(sound); + return; + } + + // Begin the actual playback. + var node = sound._node; + if (this._webAudio) { + // Fire this when the sound is ready to play to begin Web Audio playback. + var playWebAudio = function () { + this._playLock = false; + setParams(); + this._refreshBuffer(sound); + + // Setup the playback params. + var vol = sound._muted || this._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) { + this._endTimers[sound._id] = setTimeout( + this._ended.bind(this, sound), + timeout, + ); + } + + if (!internal) { + setTimeout(function () { + this._emit('play', sound._id); + this._loadQueue(); + }, 0); + } + }; + + if (Howler.state === 'running' && Howler.ctx.state !== 'interrupted') { + playWebAudio(); + } else { + this._playLock = true; + + // Wait for the audio context to resume before playing. + this.once('resume', playWebAudio); + + // Cancel the end timer. + this._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 || this._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(). + this._playLock = true; + + // Set param values immediately. + setParams(); + + // Releases the lock and executes queued actions. + play + .then(function () { + this._playLock = false; + node._unlocked = true; + if (!internal) { + this._emit('play', sound._id); + } else { + this._loadQueue(); + } + }) + .catch(function () { + this._playLock = false; + this._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) { + this._playLock = false; + setParams(); + this._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) { + this._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) { + this._endTimers[sound._id] = setTimeout( + this._ended.bind(this, sound), + timeout, + ); + } else { + this._endTimers[sound._id] = function () { + // Fire ended on this audio node. + this._ended(sound); + + // Clear this listener. + node.removeEventListener( + 'ended', + this._endTimers[sound._id], + false, + ); + }; + node.addEventListener('ended', this._endTimers[sound._id], false); + } + } catch (err) { + this._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 = this._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 { + this._playLock = true; + this._state = 'loading'; + + var listener = function () { + this._state = 'loaded'; + + // Begin playback. + playHtml5(); + + // Clear this listener. + node.removeEventListener(Howler._canPlayEvent, listener, false); + }; + node.addEventListener(Howler._canPlayEvent, listener, false); + + // Cancel the end timer. + this._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(id) { + // If the sound hasn't loaded or a play() promise is pending, add it to the load queue to pause when capable. + if (this._state !== 'loaded' || this._playLock) { + this._queue.push({ + event: 'pause', + action() { + this.pause(id); + }, + }); + + return this; + } + + // If no id is passed, get all ID's to be paused. + var ids = this._getSoundIds(id); + + for (var i = 0; i < ids.length; i++) { + // Clear the end timer. + this._clearTimer(ids[i]); + + // Get the sound. + var sound = this._soundById(ids[i]); + + if (sound && !sound._paused) { + // Reset the seek position. + sound._seek = this.seek(ids[i]); + sound._rateSeek = 0; + sound._paused = true; + + // Stop currently running fades. + this._stopFade(ids[i]); + + if (sound._node) { + if (this._webAudio) { + // Make sure the sound has been created. + if (!sound._node.bufferSource) { + continue; + } + + if (typeof sound._node.bufferSource.stop === 'undefined') { + sound._node.bufferSource.noteOff(0); + } else { + sound._node.bufferSource.stop(0); + } + + // Clean up the buffer source. + this._cleanBuffer(sound._node); + } else if ( + !isNaN(sound._node.duration) || + sound._node.duration === Infinity + ) { + sound._node.pause(); + } + } + } + + // Fire the pause event, unless `true` is passed as the 2nd argument. + if (!arguments[1]) { + this._emit('pause', sound ? sound._id : null); + } + } + + return this; + } + + /** + * Stop playback and reset to start. + * @param {Number} id The sound ID (empty to stop all in group). + * @param {Boolean} internal Internal Use: true prevents event firing. + * @return {Howl} + */ + stop(id, internal) { + // If the sound hasn't loaded, add it to the load queue to stop when capable. + if (this._state !== 'loaded' || this._playLock) { + this._queue.push({ + event: 'stop', + action() { + this.stop(id); + }, + }); + + return this; + } + + // If no id is passed, get all ID's to be stopped. + var ids = this._getSoundIds(id); + + for (var i = 0; i < ids.length; i++) { + // Clear the end timer. + this._clearTimer(ids[i]); + + // Get the sound. + var sound = this._soundById(ids[i]); + + if (sound) { + // Reset the seek position. + sound._seek = sound._start || 0; + sound._rateSeek = 0; + sound._paused = true; + sound._ended = true; + + // Stop currently running fades. + this._stopFade(ids[i]); + + if (sound._node) { + if (this._webAudio) { + // Make sure the sound's AudioBufferSourceNode has been created. + if (sound._node.bufferSource) { + if (typeof sound._node.bufferSource.stop === 'undefined') { + sound._node.bufferSource.noteOff(0); + } else { + sound._node.bufferSource.stop(0); + } + + // Clean up the buffer source. + this._cleanBuffer(sound._node); + } + } else if ( + !isNaN(sound._node.duration) || + sound._node.duration === Infinity + ) { + sound._node.currentTime = sound._start || 0; + sound._node.pause(); + + // If this is a live stream, stop download once the audio is stopped. + if (sound._node.duration === Infinity) { + this._clearSound(sound._node); + } + } + } + + if (!internal) { + this._emit('stop', sound._id); + } + } + } + + return this; + } + + /** + * Mute/unmute a single sound or all sounds in this Howl group. + * @param {Boolean} muted Set to true to mute and false to unmute. + * @param {Number} id The sound ID to update (omit to mute/unmute all). + * @return {Howl} + */ + mute(muted, id) { + // If the sound hasn't loaded, add it to the load queue to mute when capable. + if (this._state !== 'loaded' || this._playLock) { + this._queue.push({ + event: 'mute', + action() { + this.mute(muted, id); + }, + }); + + return this; + } + + // If applying mute/unmute to all sounds, update the group's value. + if (typeof id === 'undefined') { + if (typeof muted === 'boolean') { + this._muted = muted; + } else { + return this._muted; + } + } + + // If no id is passed, get all ID's to be muted. + var ids = this._getSoundIds(id); + + for (var i = 0; i < ids.length; i++) { + // Get the sound. + var sound = this._soundById(ids[i]); + + if (sound) { + sound._muted = muted; + + // Cancel active fade and set the volume to the end value. + if (sound._interval) { + this._stopFade(sound._id); + } + + if (this._webAudio && sound._node) { + sound._node.gain.setValueAtTime( + muted ? 0 : sound._volume, + Howler.ctx.currentTime, + ); + } else if (sound._node) { + sound._node.muted = Howler._muted ? true : muted; + } + + this._emit('mute', sound._id); + } + } + + return this; + } + + /** + * Get/set the volume of this sound or of the Howl group. This method can optionally take 0, 1 or 2 arguments. + * volume() -> 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 this or current volume. + */ + volume() { + var args = arguments; + var vol, id; + + // Determine the values based on arguments. + if (args.length === 0) { + // Return the value of the groups' volume. + return this._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 = this._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 (this._state !== 'loaded' || this._playLock) { + this._queue.push({ + event: 'volume', + action() { + this.volume.apply(this, args); + }, + }); + + return this; + } + + // Set the group volume. + if (typeof id === 'undefined') { + this._volume = vol; + } + + // Update one or all volumes. + id = this._getSoundIds(id); + for (var i = 0; i < id.length; i++) { + // Get the sound. + sound = this._soundById(id[i]); + + if (sound) { + sound._volume = vol; + + // Stop currently running fades. + if (!args[2]) { + this._stopFade(id[i]); + } + + if (this._webAudio && sound._node && !sound._muted) { + sound._node.gain.setValueAtTime(vol, Howler.ctx.currentTime); + } else if (sound._node && !sound._muted) { + sound._node.volume = vol * Howler.volume(); + } + + this._emit('volume', sound._id); + } + } + } else { + sound = id ? this._soundById(id) : this._sounds[0]; + return sound ? sound._volume : 0; + } + + return this; + } + + /** + * Fade a currently playing sound between two volumes (if no id is passed, all sounds will fade). + * @param {Number} from The value to fade from (0.0 to 1.0). + * @param {Number} to The volume to fade to (0.0 to 1.0). + * @param {Number} len Time in milliseconds to fade. + * @param {Number} id The sound id (omit to fade all sounds). + * @return {Howl} + */ + fade(from, to, len, id) { + // If the sound hasn't loaded, add it to the load queue to fade when capable. + if (this._state !== 'loaded' || this._playLock) { + this._queue.push({ + event: 'fade', + action() { + this.fade(from, to, len, id); + }, + }); + + return this; + } + + // Make sure the to/from/len values are numbers. + from = Math.min(Math.max(0, parseFloat(from)), 1); + to = Math.min(Math.max(0, parseFloat(to)), 1); + len = parseFloat(len); + + // Set the volume to the start position. + this.volume(from, id); + + // Fade the volume of one or all sounds. + var ids = this._getSoundIds(id); + for (var i = 0; i < ids.length; i++) { + // Get the sound. + var sound = this._soundById(ids[i]); + + // Create a linear fade or fall back to timeouts with HTML5 Audio. + if (sound) { + // Stop the previous fade if no sprite is being used (otherwise, volume handles this). + if (!id) { + this._stopFade(ids[i]); + } + + // If we are using Web Audio, let the native methods do the actual fade. + if (this._webAudio && !sound._muted) { + var currentTime = Howler.ctx.currentTime; + var end = currentTime + len / 1000; + sound._volume = from; + sound._node.gain.setValueAtTime(from, currentTime); + sound._node.gain.linearRampToValueAtTime(to, end); + } + + this._startFadeInterval( + sound, + from, + to, + len, + ids[i], + typeof id === 'undefined', + ); + } + } + + return this; + } + + /** + * Starts the internal interval to fade a sound. + * @param {Object} sound Reference to sound to fade. + * @param {Number} from The value to fade from (0.0 to 1.0). + * @param {Number} to The volume to fade to (0.0 to 1.0). + * @param {Number} len Time in milliseconds to fade. + * @param {Number} id The sound id to fade. + * @param {Boolean} isGroup If true, set the volume on the group. + */ + _startFadeInterval(sound, from, to, len, id, isGroup) { + var vol = from; + var diff = to - from; + var steps = Math.abs(diff / 0.01); + var stepLen = Math.max(4, steps > 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 (this._webAudio) { + sound._volume = vol; + } else { + this.volume(vol, sound._id, true); + } + + // Set the group's volume. + if (isGroup) { + this._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; + this.volume(to, sound._id); + this._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(id) { + var sound = this._soundById(id); + + if (sound && sound._interval) { + if (this._webAudio) { + sound._node.gain.cancelScheduledValues(Howler.ctx.currentTime); + } + + clearInterval(sound._interval); + sound._interval = null; + this.volume(sound._fadeTo, id); + sound._fadeTo = null; + this._emit('fade', id); + } + + return this; + } + + /** + * 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 this or current loop value. + */ + loop() { + 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 this._loop; + } else if (args.length === 1) { + if (typeof args[0] === 'boolean') { + loop = args[0]; + this._loop = loop; + } else { + // Return this sound's loop value. + sound = this._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 = this._getSoundIds(id); + for (var i = 0; i < ids.length; i++) { + sound = this._soundById(ids[i]); + + if (sound) { + sound._loop = loop; + if (this._webAudio && sound._node && sound._node.bufferSource) { + sound._node.bufferSource.loop = loop; + if (loop) { + sound._node.bufferSource.loopStart = sound._start || 0; + sound._node.bufferSource.loopEnd = sound._stop; + + // If playing, restart playback to ensure looping updates. + if (this.playing(ids[i])) { + this.pause(ids[i], true); + this.play(ids[i], true); + } + } + } + } + } + + return this; + } + + /** + * Get/set the playback rate of a sound. This method can optionally take 0, 1 or 2 arguments. + * rate() -> 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 this or the current playback rate. + */ + rate() { + 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 = this._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 = this._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 (this._state !== 'loaded' || this._playLock) { + this._queue.push({ + event: 'rate', + action() { + this.rate.apply(this, args); + }, + }); + + return this; + } + + // Set the group rate. + if (typeof id === 'undefined') { + this._rate = rate; + } + + // Update one or all volumes. + id = this._getSoundIds(id); + for (var i = 0; i < id.length; i++) { + // Get the sound. + sound = this._soundById(id[i]); + + if (sound) { + // Keep track of our position when the rate changed and update the playback + // start position so we can properly adjust the seek position for time elapsed. + if (this.playing(id[i])) { + sound._rateSeek = this.seek(id[i]); + sound._playStart = this._webAudio + ? Howler.ctx.currentTime + : sound._playStart; + } + sound._rate = rate; + + // Change the playback rate. + if (this._webAudio && sound._node && sound._node.bufferSource) { + sound._node.bufferSource.playbackRate.setValueAtTime( + rate, + Howler.ctx.currentTime, + ); + } else if (sound._node) { + sound._node.playbackRate = rate; + } + + // Reset the timers. + var seek = this.seek(id[i]); + var duration = + (this._sprite[sound._sprite][0] + this._sprite[sound._sprite][1]) / + 1000 - + seek; + var timeout = (duration * 1000) / Math.abs(sound._rate); + + // Start a new end timer if sound is already playing. + if (this._endTimers[id[i]] || !sound._paused) { + this._clearTimer(id[i]); + this._endTimers[id[i]] = setTimeout( + this._ended.bind(this, sound), + timeout, + ); + } + + this._emit('rate', sound._id); + } + } + } else { + sound = this._soundById(id); + return sound ? sound._rate : this._rate; + } + + return this; + } + + /** + * Get/set the seek position of a sound. This method can optionally take 0, 1 or 2 arguments. + * seek() -> 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 this or the current seek position. + */ + seek() { + 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 (this._sounds.length) { + id = this._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 = this._getSoundIds(); + var index = ids.indexOf(args[0]); + if (index >= 0) { + id = parseInt(args[0], 10); + } else if (this._sounds.length) { + id = this._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' && + (this._state !== 'loaded' || this._playLock) + ) { + this._queue.push({ + event: 'seek', + action() { + this.seek.apply(this, args); + }, + }); + + return this; + } + + // Get the sound. + var sound = this._soundById(id); + + if (sound) { + if (typeof seek === 'number' && seek >= 0) { + // Pause the sound and update position for restarting playback. + var playing = this.playing(id); + if (playing) { + this.pause(id, true); + } + + // Move the position of the track and cancel timer. + sound._seek = seek; + sound._ended = false; + this._clearTimer(id); + + // Update the seek position for HTML5 Audio. + if (!this._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) { + this.play(id, true); + } + + this._emit('seek', id); + }; + + // Wait for the play lock to be unset before emitting (HTML5 Audio). + if (playing && !this._webAudio) { + var emitSeek = function () { + if (!this._playLock) { + seekAndEmit(); + } else { + setTimeout(emitSeek, 0); + } + }; + setTimeout(emitSeek, 0); + } else { + seekAndEmit(); + } + } else { + if (this._webAudio) { + var realTime = this.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 this; + } + + /** + * 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(id) { + // Check the passed sound ID (if any). + if (typeof id === 'number') { + var sound = this._soundById(id); + return sound ? !sound._paused : false; + } + + // Otherwise, loop through all sounds and check if any are playing. + for (var i = 0; i < this._sounds.length; i++) { + if (!this._sounds[i]._paused) { + return true; + } + } + + return false; + } + + /** + * Get the duration of this sound. Passing a sound id will return the sprite duration. + * @param {Number} id The sound id to check. If none is passed, return full source duration. + * @return {Number} Audio duration in seconds. + */ + duration(id) { + var duration = this._duration; + + // If we pass an ID, get the sound and return the sprite length. + var sound = this._soundById(id); + if (sound) { + duration = this._sprite[sound._sprite][1] / 1000; + } + + return duration; + } + + /** + * Returns the current loaded state of this Howl. + * @return {String} 'unloaded', 'loading', 'loaded' + */ + state() { + return this._state; + } + + /** + * Unload and destroy the current Howl object. + * This will immediately stop all sound instances attached to this group. + */ + unload() { + // Stop playing any active sounds. + var sounds = this._sounds; + for (var i = 0; i < sounds.length; i++) { + // Stop the sound if it is currently playing. + if (!sounds[i]._paused) { + this.stop(sounds[i]._id); + } + + // Remove the source or disconnect. + if (!this._webAudio) { + // Set the source to 0-second silence to stop any downloading (except in IE). + this._clearSound(sounds[i]._node); + + // Remove any event listeners. + sounds[i]._node.removeEventListener('error', sounds[i]._errorFn, false); + sounds[i]._node.removeEventListener( + Howler._canPlayEvent, + sounds[i]._loadFn, + false, + ); + sounds[i]._node.removeEventListener('ended', sounds[i]._endFn, false); + + // Release the Audio object back to the pool. + Howler._releaseHtml5Audio(sounds[i]._node); + } + + // Empty out all of the nodes. + delete sounds[i]._node; + + // Make sure all timers are cleared out. + this._clearTimer(sounds[i]._id); + } + + // Remove the references in the global Howler object. + var index = Howler._howls.indexOf(this); + if (index >= 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 < Howler._howls.length; i++) { + if ( + Howler._howls[i]._src === this._src || + this._src.indexOf(Howler._howls[i]._src) >= 0 + ) { + remCache = false; + break; + } + } + + if (cache && remCache) { + delete cache[this._src]; + } + + // Clear global errors. + Howler.noAudio = false; + + // Clear out `this`. + this._state = 'unloaded'; + this._sounds = []; + this = 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(event, fn, id, once) { + var events = this['_on' + event]; + + if (typeof fn === 'function') { + events.push(once ? { id: id, fn: fn, once: once } : { id: id, fn: fn }); + } + + return this; + } + + /** + * 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(event, fn, id) { + var events = this['_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 < events.length; i++) { + var isId = id === events[i].id; + if ((fn === events[i].fn && isId) || (!fn && isId)) { + events.splice(i, 1); + break; + } + } + } else if (event) { + // Clear out all events of this type. + this['_on' + event] = []; + } else { + // Clear out all events of every type. + var keys = Object.keys(this); + for (i = 0; i < keys.length; i++) { + if (keys[i].indexOf('_on') === 0 && Array.isArray(this[keys[i]])) { + this[keys[i]] = []; + } + } + } + + return this; + } + + /** + * Listen to a custom event and remove it once fired. + * @param {String} event Event name. + * @param {Function} fn Listener to call. + * @param {Number} id (optional) Only listen to events for this sound. + * @return {Howl} + */ + once(event, fn, id) { + // Setup the event listener. + this.on(event, fn, id, 1); + + return this; + } + + /** + * Emit all events of a specific type and pass the sound id. + * @param {String} event Event name. + * @param {Number} id Sound ID. + * @param {Number} msg Message to go with event. + * @return {Howl} + */ + _emit(event, id, msg) { + var events = this['_on' + event]; + + // Loop through event store and fire all functions. + for (var i = events.length - 1; 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(this, events[i].fn), + 0, + ); + + // If this event was setup with `once`, remove it. + if (events[i].once) { + this.off(event, events[i].fn, events[i].id); + } + } + } + + // Pass the event type into load queue so that it can continue stepping. + this._loadQueue(event); + + return this; + } + + /** + * 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(event) { + if (this._queue.length > 0) { + var task = this._queue[0]; + + // Remove this task if a matching event was passed. + if (task.event === event) { + this._queue.shift(); + this._loadQueue(); + } + + // Run the task if no event type is passed. + if (!event) { + task.action(); + } + } + + return this; + } + + /** + * Fired when playback ends at the end of the duration. + * @param {Sound} sound The sound object to work with. + * @return {Howl} + */ + _ended(sound) { + 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 ( + !this._webAudio && + sound._node && + !sound._node.paused && + !sound._node.ended && + sound._node.currentTime < sound._stop + ) { + setTimeout(this._ended.bind(this, sound), 100); + return this; + } + + // Should this sound loop? + var loop = !!(sound._loop || this._sprite[sprite][2]); + + // Fire the ended event. + this._emit('end', sound._id); + + // Restart the playback for HTML5 Audio loop. + if (!this._webAudio && loop) { + this.stop(sound._id, true).play(sound._id); + } + + // Restart this timer if on a Web Audio loop. + if (this._webAudio && loop) { + this._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); + this._endTimers[sound._id] = setTimeout( + this._ended.bind(this, sound), + timeout, + ); + } + + // Mark the node as paused. + if (this._webAudio && !loop) { + sound._paused = true; + sound._ended = true; + sound._seek = sound._start || 0; + sound._rateSeek = 0; + this._clearTimer(sound._id); + + // Clean up the buffer source. + this._cleanBuffer(sound._node); + + // Attempt to auto-suspend AudioContext if no sounds are still playing. + Howler._autoSuspend(); + } + + // When using a sprite, end the track. + if (!this._webAudio && !loop) { + this.stop(sound._id, true); + } + + return this; + } + + /** + * Clear the end timer for a sound playback. + * @param {Number} id The sound ID. + * @return {Howl} + */ + _clearTimer(id) { + if (this._endTimers[id]) { + // Clear the timeout or remove the ended listener. + if (typeof this._endTimers[id] !== 'function') { + clearTimeout(this._endTimers[id]); + } else { + var sound = this._soundById(id); + if (sound && sound._node) { + sound._node.removeEventListener('ended', this._endTimers[id], false); + } + } + + delete this._endTimers[id]; + } + + return this; + } + /** + * Return the sound identified by this ID, or return null. + * @param {Number} id Sound ID + * @return {Object} Sound object or null. + */ + _soundById(id) { + // Loop through all sounds and find the one with this ID. + for (var i = 0; i < this._sounds.length; i++) { + if (id === this._sounds[i]._id) { + return this._sounds[i]; + } + } + + return null; + } + + /** + * Return an inactive sound from the pool or create a new one. + * @return {Sound} Sound playback object. + */ + _inactiveSound() { + this._drain(); + + // Find the first inactive node to recycle. + for (var i = 0; i < this._sounds.length; i++) { + if (this._sounds[i]._ended) { + return this._sounds[i].reset(); + } + } + + // If no inactive node was found, create a new one. + return new Sound(this); + } + + /** + * Drain excess inactive sounds from the pool. + */ + _drain() { + var limit = this._pool; + var cnt = 0; + var i = 0; + + // If there are less sounds than the max pool size, we are done. + if (this._sounds.length < limit) { + return; + } + + // Count the number of inactive sounds. + for (i = 0; i < this._sounds.length; i++) { + if (this._sounds[i]._ended) { + cnt++; + } + } + + // Remove excess inactive sounds, going in reverse order. + for (i = this._sounds.length - 1; i >= 0; i--) { + if (cnt <= limit) { + return; + } + + if (this._sounds[i]._ended) { + // Disconnect the audio source when using Web Audio. + if (this._webAudio && this._sounds[i]._node) { + this._sounds[i]._node.disconnect(0); + } + + // Remove sounds until we have the pool size. + this._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(id) { + if (typeof id === 'undefined') { + var ids = []; + for (var i = 0; i < this._sounds.length; i++) { + ids.push(this._sounds[i]._id); + } + + return ids; + } else { + return [id]; + } + } + + /** + * Load the sound back into the buffer source. + * @param {Sound} sound The sound object to work with. + * @return {Howl} + */ + _refreshBuffer(sound) { + // Setup the buffer source for playback. + sound._node.bufferSource = Howler.ctx.createBufferSource(); + sound._node.bufferSource.buffer = cache[this._src]; + + // Connect to the correct node. + if (sound._panner) { + sound._node.bufferSource.connect(sound._panner); + } else { + sound._node.bufferSource.connect(sound._node); + } + + // Setup looping and playback rate. + sound._node.bufferSource.loop = sound._loop; + if (sound._loop) { + sound._node.bufferSource.loopStart = sound._start || 0; + sound._node.bufferSource.loopEnd = sound._stop || 0; + } + sound._node.bufferSource.playbackRate.setValueAtTime( + sound._rate, + Howler.ctx.currentTime, + ); + + return this; + } + + /** + * Prevent memory leaks by cleaning up the buffer source after playback. + * @param {Object} node Sound's audio node containing the buffer source. + * @return {Howl} + */ + _cleanBuffer(node) { + var isIOS = + Howler._navigator && Howler._navigator.vendor.indexOf('Apple') >= 0; + + 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 this; + } + + /** + * Set the source to a 0-second silence to stop any downloading (except in IE). + * @param {Object} node Audio node to clear. + */ + _clearSound(node) { + var checkIE = /MSIE |Trident\//.test( + Howler._navigator && Howler._navigator.userAgent, + ); + if (!checkIE) { + node.src = + 'data:audio/wav;base64,UklGRigAAABXQVZFZm10IBIAAAABAAEARKwAAIhYAQACABAAAABkYXRhAgAAAAEA'; + } + } +} + +export default Howl; diff --git a/src/howler.ts b/src/howler.ts new file mode 100644 index 00000000..c7dc7bcc --- /dev/null +++ b/src/howler.ts @@ -0,0 +1,578 @@ +import { setupAudioContext } from './helpers'; + +class Howler { + /** + * Create the global controller. All contained methods and properties apply + * to all sounds that are currently playing or will be in the future. + */ + constructor() { + this.init(); + } + + /** + * Initialize the global Howler object. + * @return {Howler} + */ + init() { + // Create a global ID counter. + this._counter = 1000; + + // Pool of unlocked HTML5 Audio objects. + this._html5AudioPool = []; + this.html5PoolSize = 10; + + // Internal properties. + this._codecs = {}; + this._howls = []; + this._muted = false; + this._volume = 1; + this._canPlayEvent = 'canplaythrough'; + this._navigator = + typeof window !== 'undefined' && window.navigator + ? window.navigator + : null; + + // Public properties. + this.masterGain = null; + this.noAudio = false; + this.usingWebAudio = true; + this.autoSuspend = true; + this.ctx = null; + + // Set to false to disable the auto audio unlocker. + this.autoUnlock = true; + + // Setup the various state values for global tracking. + this._setup(); + } + + /** + * 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(vol) { + vol = parseFloat(vol); + + // If we don't have an AudioContext created yet, run the setup. + if (!this.ctx) { + setupAudioContext(); + } + + if (typeof vol !== 'undefined' && vol >= 0 && vol <= 1) { + this._volume = vol; + + // Don't update any of the nodes if we are muted. + if (this._muted) { + return this; + } + + // When using Web Audio, we just need to adjust the master gain. + if (this.usingWebAudio) { + this.masterGain.gain.setValueAtTime(vol, Howler.ctx.currentTime); + } + + // Loop through and change volume for all HTML5 audio nodes. + for (var i = 0; i < this._howls.length; i++) { + if (!this._howls[i]._webAudio) { + // Get all of the sounds in this Howl group. + var ids = this._howls[i]._getSoundIds(); + + // Loop through all sounds and change the volumes. + for (var j = 0; j < ids.length; j++) { + var sound = this._howls[i]._soundById(ids[j]); + + if (sound && sound._node) { + sound._node.volume = sound._volume * vol; + } + } + } + } + + return this; + } + + return this._volume; + } + + /** + * Handle muting and unmuting globally. + * @param {Boolean} muted Is muted or not. + */ + mute(muted) { + // If we don't have an AudioContext created yet, run the setup. + if (!this.ctx) { + setupAudioContext(); + } + + this._muted = muted; + + // With Web Audio, we just need to mute the master gain. + if (this.usingWebAudio) { + this.masterGain.gain.setValueAtTime( + muted ? 0 : this._volume, + Howler.ctx.currentTime, + ); + } + + // Loop through and mute all HTML5 Audio nodes. + for (var i = 0; i < this._howls.length; i++) { + if (!this._howls[i]._webAudio) { + // Get all of the sounds in this Howl group. + var ids = this._howls[i]._getSoundIds(); + + // Loop through all sounds and mark the audio node as muted. + for (var j = 0; j < ids.length; j++) { + var sound = this._howls[i]._soundById(ids[j]); + + if (sound && sound._node) { + sound._node.muted = muted ? true : sound._muted; + } + } + } + } + + return this; + } + + /** + * Handle stopping all sounds globally. + */ + stop() { + // Loop through all Howls and stop them. + for (var i = 0; i < this._howls.length; i++) { + this._howls[i].stop(); + } + + return this; + } + + /** + * Unload and destroy all currently loaded Howl objects. + * @return {Howler} + */ + unload() { + for (var i = this._howls.length - 1; i >= 0; i--) { + this._howls[i].unload(); + } + + // Create a new AudioContext to make sure it is fully reset. + if ( + this.usingWebAudio && + this.ctx && + typeof this.ctx.close !== 'undefined' + ) { + this.ctx.close(); + this.ctx = null; + setupAudioContext(); + } + + return this; + } + + /** + * Check for codec support of specific extension. + * @param {String} ext Audio file extention. + * @return {Boolean} + */ + codecs(ext) { + return this._codecs[ext.replace(/^x-/, '')]; + } + + /** + * Setup various state values for global tracking. + * @return {Howler} + */ + _setup() { + // Keeps track of the suspend/resume state of the AudioContext. + this.state = this.ctx ? this.ctx.state || 'suspended' : 'suspended'; + + // Automatically begin the 30-second suspend process + this._autoSuspend(); + + // Check if audio is available. + if (!this.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') { + this._canPlayEvent = 'canplay'; + } + } catch (e) { + this.noAudio = true; + } + } else { + this.noAudio = true; + } + } + + // Test to make sure audio isn't disabled in Internet Explorer. + try { + var test = new Audio(); + if (test.muted) { + this.noAudio = true; + } + } catch (e) {} + + // Check for supported codecs. + if (!this.noAudio) { + this._setupCodecs(); + } + + return this; + } + + /** + * Check for browser support for various codecs and cache the results. + * @return {Howler} + */ + _setupCodecs() { + 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 this; + } + + if (!audioTest || typeof audioTest.canPlayType !== 'function') { + return this; + } + + 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 = this._navigator ? this._navigator.userAgent : ''; + var checkOpera = ua.match(/OPR\/([0-6].)/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; + + this._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 this; + } + + /** + * 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() { + // Only run this if Web Audio is supported and it hasn't already been unlocked. + if (this._audioUnlocked || !this.ctx) { + return; + } + + this._audioUnlocked = false; + this.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 (!this._mobileUnloaded && this.ctx.sampleRate !== 44100) { + this._mobileUnloaded = true; + this.unload(); + } + + // Scratch buffer for enabling iOS to dispose of web audio buffers correctly, as per: + // http://stackoverflow.com/questions/24119684 + this._scratchBuffer = this.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 (this._html5AudioPool.length < this.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. + this._releaseHtml5Audio(audioNode); + } catch (e) { + this.noAudio = true; + break; + } + } + + // Loop through any assigned audio nodes and unlock them. + for (var i = 0; i < this._howls.length; i++) { + if (!this._howls[i]._webAudio) { + // Get all of the sounds in this Howl group. + var ids = this._howls[i]._getSoundIds(); + + // Loop through all sounds and unlock the audio nodes. + for (var j = 0; j < ids.length; j++) { + var sound = this._howls[i]._soundById(ids[j]); + + if (sound && sound._node && !sound._node._unlocked) { + sound._node._unlocked = true; + sound._node.load(); + } + } + } + } + + // Fix Android can not play in suspend state. + this._autoResume(); + + // Create an empty buffer. + var source = this.ctx.createBufferSource(); + source.buffer = this._scratchBuffer; + source.connect(this.ctx.destination); + + // Play the empty buffer. + if (typeof source.start === 'undefined') { + source.noteOn(0); + } else { + source.start(0); + } + + // Calling resume() on a stack initiated by user gesture is what actually unlocks the audio on Android Chrome >= 55. + if (typeof this.ctx.resume === 'function') { + this.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. + this._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 < this._howls.length; i++) { + this._howls[i]._emit('unlock'); + } + }; + }; + + // Setup a touch start listener to attempt an unlock in. + document.addEventListener('touchstart', unlock, true); + document.addEventListener('touchend', unlock, true); + document.addEventListener('click', unlock, true); + document.addEventListener('keydown', unlock, true); + + return this; + } + + /** + * Get an unlocked HTML5 Audio object from the pool. If none are left, + * return a new Audio object and throw a warning. + * @return {Audio} HTML5 Audio object. + */ + _obtainHtml5Audio() { + // Return the next object from the pool if one exists. + if (this._html5AudioPool.length) { + return this._html5AudioPool.pop(); + } + + //.Check if the audio is locked and throw a warning. + var testPlay = new Audio().play(); + if ( + testPlay && + typeof Promise !== 'undefined' && + (testPlay instanceof Promise || typeof testPlay.then === 'function') + ) { + testPlay.catch(function () { + console.warn( + 'HTML5 Audio pool exhausted, returning potentially locked audio object.', + ); + }); + } + + return new Audio(); + } + + /** + * Return an activated HTML5 Audio object to the pool. + * @return {Howler} + */ + _releaseHtml5Audio(audio) { + // Don't add audio to the pool if we don't know if it has been unlocked. + if (audio._unlocked) { + this._html5AudioPool.push(audio); + } + + return this; + } + + /** + * Automatically suspend the Web Audio AudioContext after no sound has played for 30 seconds. + * This saves processing/energy and fixes various browser-specific bugs with audio getting stuck. + * @return {Howler} + */ + _autoSuspend() { + if ( + !this.autoSuspend || + !this.ctx || + typeof this.ctx.suspend === 'undefined' || + !Howler.usingWebAudio + ) { + return; + } + + // Check if any sounds are playing. + for (var i = 0; i < this._howls.length; i++) { + if (this._howls[i]._webAudio) { + for (var j = 0; j < this._howls[i]._sounds.length; j++) { + if (!this._howls[i]._sounds[j]._paused) { + return this; + } + } + } + } + + if (this._suspendTimer) { + clearTimeout(this._suspendTimer); + } + + // If no sound has played after 30 seconds, suspend the context. + this._suspendTimer = setTimeout(function () { + if (!this.autoSuspend) { + return; + } + + this._suspendTimer = null; + this.state = 'suspending'; + + // Handle updating the state of the audio context after suspending. + var handleSuspension = function () { + this.state = 'suspended'; + + if (this._resumeAfterSuspend) { + delete this._resumeAfterSuspend; + this._autoResume(); + } + }; + + // Either the state gets suspended or it is interrupted. + // Either way, we need to update the state to suspended. + this.ctx.suspend().then(handleSuspension, handleSuspension); + }, 30000); + + return this; + } + + /** + * Automatically resume the Web Audio AudioContext when a new sound is played. + * @return {Howler} + */ + _autoResume() { + if ( + !this.ctx || + typeof this.ctx.resume === 'undefined' || + !Howler.usingWebAudio + ) { + return; + } + + if ( + this.state === 'running' && + this.ctx.state !== 'interrupted' && + this._suspendTimer + ) { + clearTimeout(this._suspendTimer); + this._suspendTimer = null; + } else if ( + this.state === 'suspended' || + (this.state === 'running' && this.ctx.state === 'interrupted') + ) { + this.ctx.resume().then(function () { + this.state = 'running'; + + // Emit to all Howls that the audio has resumed. + for (var i = 0; i < this._howls.length; i++) { + this._howls[i]._emit('resume'); + } + }); + + if (this._suspendTimer) { + clearTimeout(this._suspendTimer); + this._suspendTimer = null; + } + } else if (this.state === 'suspending') { + this._resumeAfterSuspend = true; + } + + return this; + } +} + +export default new Howler(); diff --git a/src/sound.ts b/src/sound.ts new file mode 100644 index 00000000..bbc0023b --- /dev/null +++ b/src/sound.ts @@ -0,0 +1,183 @@ +/** + * TODO: pass the howler instance reference to each sound that is created instead of using a global variable + * TODO: update the sound id generator to be common across all modules. + * + * IDEA: Maybe use ES private properties, as they can be compiled away with esbuild + TS. + */ + +import Howler from './howler'; + +class Sound { + /** + * Setup the sound object, which each node attached to a Howl group is contained in. + * @param {Object} howl The Howl parent group. + */ + constructor(howl) { + this._parent = howl; + this.init(); + } + + /** + * Initialize a new Sound object. + * @return {Sound} + */ + init() { + var parent = this._parent; + + // Setup the default parameters. + this._muted = parent._muted; + this._loop = parent._loop; + this._volume = parent._volume; + this._rate = parent._rate; + this._seek = 0; + this._paused = true; + this._ended = true; + this._sprite = '__default'; + + // Generate a unique ID for this sound. + this._id = ++Howler._counter; + + // Add itself to the parent's pool. + parent._sounds.push(this); + + // Create the new node. + this.create(); + } + + /** + * Create and setup a new sound object, whether HTML5 Audio or Web Audio. + * @return {Sound} + */ + create() { + var parent = this._parent; + var volume = + Howler._muted || this._muted || this._parent._muted ? 0 : this._volume; + + if (parent._webAudio) { + // Create the gain node for controlling volume (the source will connect to this). + this._node = + typeof Howler.ctx.createGain === 'undefined' + ? Howler.ctx.createGainNode() + : Howler.ctx.createGain(); + this._node.gain.setValueAtTime(volume, Howler.ctx.currentTime); + this._node.paused = true; + this._node.connect(Howler.masterGain); + } else if (!Howler.noAudio) { + // Get an unlocked Audio object from the pool. + this._node = Howler._obtainHtml5Audio(); + + // Listen for errors (http://dev.w3.org/html5/spec-author-view/spec.html#mediaerror). + this._errorFn = this._errorListener.bind(this); + this._node.addEventListener('error', this._errorFn, false); + + // Listen for 'canplaythrough' event to let us know the sound is ready. + this._loadFn = this._loadListener.bind(this); + this._node.addEventListener(Howler._canPlayEvent, this._loadFn, false); + + // Listen for the 'ended' event on the sound to account for edge-case where + // a finite sound has a duration of Infinity. + this._endFn = this._endListener.bind(this); + this._node.addEventListener('ended', this._endFn, false); + + // Setup the new audio node. + this._node.src = parent._src; + this._node.preload = parent._preload === true ? 'auto' : parent._preload; + this._node.volume = volume * Howler.volume(); + + // Begin loading the source. + this._node.load(); + } + + return this; + } + + /** + * Reset the parameters of this sound to the original state (for recycle). + * @return {Sound} + */ + reset() { + var parent = this._parent; + + // Reset all of the parameters of this sound. + this._muted = parent._muted; + this._loop = parent._loop; + this._volume = parent._volume; + this._rate = parent._rate; + this._seek = 0; + this._rateSeek = 0; + this._paused = true; + this._ended = true; + this._sprite = '__default'; + + // Generate a new ID so that it isn't confused with the previous sound. + this._id = ++Howler._counter; + + return this; + } + + /** + * HTML5 Audio error listener callback. + */ + _errorListener() { + // Fire an error event and pass back the code. + this._parent._emit( + 'loaderror', + this._id, + this._node.error ? this._node.error.code : 0, + ); + + // Clear the event listener. + this._node.removeEventListener('error', this._errorFn, false); + } + + /** + * HTML5 Audio canplaythrough listener callback. + */ + _loadListener() { + var parent = this._parent; + + // Round up the duration to account for the lower precision in HTML5 Audio. + parent._duration = Math.ceil(this._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. + this._node.removeEventListener(Howler._canPlayEvent, this._loadFn, false); + } + + /** + * HTML5 Audio ended listener callback. + */ + _endListener() { + var parent = this._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(this._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(this); + } + + // Clear the event listener since the duration is now correct. + this._node.removeEventListener('ended', this._endFn, false); + } +} + +export default Sound; diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 00000000..ab52595b --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,17 @@ +{ + "compilerOptions": { + "declaration": true, + "target": "es2018", + "lib": ["es2018", "dom"], + "strict": true, + "noImplicitAny": false, + "esModuleInterop": true, + "allowJs": true, + "moduleResolution": "node", + "outDir": "dist", + "module": "esnext", + "resolveJsonModule": true, + "emitDeclarationOnly": true + }, + "include": ["src/*.ts"] +} From fa61399f3e7d2302989cd5dfb6038e8ec512e85f Mon Sep 17 00:00:00 2001 From: Samuel Plumppu <6125097+Greenheart@users.noreply.github.com> Date: Sat, 25 Sep 2021 18:18:17 +0200 Subject: [PATCH 03/19] WIP refactor --- src/helpers.ts | 62 ---------------- src/howl.ts | 4 +- src/howler.ts | 190 +++++++++++++++++++++++++++++++++++-------------- src/sound.ts | 35 +++++---- 4 files changed, 157 insertions(+), 134 deletions(-) diff --git a/src/helpers.ts b/src/helpers.ts index 7b167157..03cad36f 100644 --- a/src/helpers.ts +++ b/src/helpers.ts @@ -138,65 +138,3 @@ function loadSound(self, buffer) { self._loadQueue(); } } - -/** - * Setup the audio context when available, or switch to HTML5 Audio mode. - */ -export function setupAudioContext() { - // 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(); -} diff --git a/src/howl.ts b/src/howl.ts index 36007938..0233da21 100644 --- a/src/howl.ts +++ b/src/howl.ts @@ -1,5 +1,5 @@ import Howler from './howler'; -import { loadBuffer, setupAudioContext } from './helpers'; +import { loadBuffer } from './helpers'; import Sound from './sound'; class Howl { @@ -27,7 +27,7 @@ class Howl { init(o) { // If we don't have an AudioContext created yet, run the setup. if (!Howler.ctx) { - setupAudioContext(); + Howler._setupAudioContext(); } // Setup user-defined default properties. diff --git a/src/howler.ts b/src/howler.ts index c7dc7bcc..723bf554 100644 --- a/src/howler.ts +++ b/src/howler.ts @@ -1,47 +1,72 @@ -import { setupAudioContext } from './helpers'; +import Howl from './Howl'; + +interface HowlerInstance { + mute(muted: boolean): this; + stop(): this; + volume(): number; + volume(volume: number): this; + codecs(ext: string): boolean; + unload(): this; + usingWebAudio: boolean; + html5PoolSize: number; + noAudio: boolean; + autoUnlock: boolean; + autoSuspend: boolean; + ctx: AudioContext; + masterGain: GainNode; + + stereo(pan: number): this; + pos(x: number, y: number, z: number): this | void; + orientation( + x: number, + y: number, + z: number, + xUp: number, + yUp: number, + zUp: number, + ): this | void; +} + +interface HowlerAudioElement extends HTMLAudioElement { + _unlocked: boolean; +} + +// IDEA: Maybe use TS private properties to create clearer contexts. class Howler { + // Public properties. + masterGain: GainNode | null = null; + noAudio = false; + usingWebAudio = true; + autoSuspend = true; + ctx: AudioContext | null = null; + + // Set to false to disable the auto audio unlocker. + autoUnlock = true; + + // Create a global ID counter. + _counter: number = 1000; + + // Pool of unlocked HTML5 Audio objects. + _html5AudioPool: Array = []; + html5PoolSize: number = 10; + + // Internal properties + _codecs = {}; + _howls: Array = []; + _muted = false; + _volume = 1; + _canPlayEvent = 'canplaythrough'; + _navigator: Navigator | null = + typeof window !== 'undefined' && window.navigator ? window.navigator : null; + _audioUnlocked: boolean = false; + _mobileUnloaded: boolean = false; + /** * Create the global controller. All contained methods and properties apply * to all sounds that are currently playing or will be in the future. */ constructor() { - this.init(); - } - - /** - * Initialize the global Howler object. - * @return {Howler} - */ - init() { - // Create a global ID counter. - this._counter = 1000; - - // Pool of unlocked HTML5 Audio objects. - this._html5AudioPool = []; - this.html5PoolSize = 10; - - // Internal properties. - this._codecs = {}; - this._howls = []; - this._muted = false; - this._volume = 1; - this._canPlayEvent = 'canplaythrough'; - this._navigator = - typeof window !== 'undefined' && window.navigator - ? window.navigator - : null; - - // Public properties. - this.masterGain = null; - this.noAudio = false; - this.usingWebAudio = true; - this.autoSuspend = true; - this.ctx = null; - - // Set to false to disable the auto audio unlocker. - this.autoUnlock = true; - // Setup the various state values for global tracking. this._setup(); } @@ -51,16 +76,16 @@ class Howler { * @param {Float} vol Volume from 0.0 to 1.0. * @return {Howler/Float} Returns self or current volume. */ - volume(vol) { - vol = parseFloat(vol); + volume(vol: string) { + const volume = parseFloat(vol); // If we don't have an AudioContext created yet, run the setup. if (!this.ctx) { - setupAudioContext(); + this._setupAudioContext(); } - if (typeof vol !== 'undefined' && vol >= 0 && vol <= 1) { - this._volume = vol; + if (typeof volume !== 'undefined' && volume >= 0 && volume <= 1) { + this._volume = volume; // Don't update any of the nodes if we are muted. if (this._muted) { @@ -69,7 +94,7 @@ class Howler { // When using Web Audio, we just need to adjust the master gain. if (this.usingWebAudio) { - this.masterGain.gain.setValueAtTime(vol, Howler.ctx.currentTime); + this.masterGain.gain.setValueAtTime(volume, this.ctx.currentTime); } // Loop through and change volume for all HTML5 audio nodes. @@ -225,12 +250,74 @@ class Howler { return this; } + /** + * Setup the audio context when available, or switch to HTML5 Audio mode. + */ + _setupAudioContext() { + // If we have already detected that Web Audio isn't supported, don't run this step again. + if (!this.usingWebAudio) { + return; + } + + // Check if we are using Web Audio and setup the AudioContext if we are. + try { + if (typeof AudioContext !== 'undefined') { + this.ctx = new AudioContext(); + } else if (typeof webkitAudioContext !== 'undefined') { + this.ctx = new webkitAudioContext(); + } else { + this.usingWebAudio = false; + } + } catch (e) { + this.usingWebAudio = false; + } + + // If the audio context creation still failed, set using web audio to false. + if (!this.ctx) { + this.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( + this._navigator && this._navigator.platform, + ); + var appVersion = + this._navigator && + this._navigator.appVersion.match(/OS (\d+)_(\d+)_?(\d+)?/); + var version = appVersion ? parseInt(appVersion[1], 10) : null; + if (iOS && version && version < 9) { + var safari = /safari/.test( + this._navigator && this._navigator.userAgent.toLowerCase(), + ); + if (this._navigator && !safari) { + this.usingWebAudio = false; + } + } + + // Create and expose the master GainNode when using Web Audio (useful for plugins or advanced usage). + if (this.usingWebAudio) { + this.masterGain = + typeof this.ctx.createGain === 'undefined' + ? this.ctx.createGainNode() + : this.ctx.createGain(); + this.masterGain.gain.setValueAtTime( + this._muted ? 0 : this._volume, + this.ctx.currentTime, + ); + this.masterGain.connect(this.ctx.destination); + } + + // Re-run the setup on Howler. + this._setup(); + } + /** * Check for browser support for various codecs and cache the results. * @return {Howler} */ _setupCodecs() { - var audioTest = null; + var audioTest: HTMLAudioElement | null = null; // Must wrap in a try/catch because IE11 in server mode throws an error. try { @@ -324,7 +411,6 @@ class Howler { return; } - this._audioUnlocked = false; this.autoUnlock = false; // Some mobile devices/platforms have distortion issues when opening/closing tabs and/or web views. @@ -337,12 +423,12 @@ class Howler { // Scratch buffer for enabling iOS to dispose of web audio buffers correctly, as per: // http://stackoverflow.com/questions/24119684 - this._scratchBuffer = this.ctx.createBuffer(1, 1, 22050); + const scratchBuffer = this.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) { + const unlock = (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 @@ -351,7 +437,7 @@ class Howler { // event will not fire. while (this._html5AudioPool.length < this.html5PoolSize) { try { - var audioNode = new Audio(); + var audioNode = new Audio() as HowlerAudioElement; // Mark this Audio object as unlocked to ensure it can get returned // to the unlocked pool when released. @@ -388,7 +474,7 @@ class Howler { // Create an empty buffer. var source = this.ctx.createBufferSource(); - source.buffer = this._scratchBuffer; + source.buffer = scratchBuffer; source.connect(this.ctx.destination); // Play the empty buffer. @@ -443,7 +529,7 @@ class Howler { return this._html5AudioPool.pop(); } - //.Check if the audio is locked and throw a warning. + // Check if the audio is locked and throw a warning. var testPlay = new Audio().play(); if ( testPlay && @@ -464,7 +550,7 @@ class Howler { * Return an activated HTML5 Audio object to the pool. * @return {Howler} */ - _releaseHtml5Audio(audio) { + _releaseHtml5Audio(audio: HowlerAudioElement) { // Don't add audio to the pool if we don't know if it has been unlocked. if (audio._unlocked) { this._html5AudioPool.push(audio); @@ -483,7 +569,7 @@ class Howler { !this.autoSuspend || !this.ctx || typeof this.ctx.suspend === 'undefined' || - !Howler.usingWebAudio + !this.usingWebAudio ) { return; } diff --git a/src/sound.ts b/src/sound.ts index bbc0023b..b5af4da8 100644 --- a/src/sound.ts +++ b/src/sound.ts @@ -6,33 +6,32 @@ */ import Howler from './howler'; +import Howl from './howl'; class Sound { + _parent: Howl; + _muted: boolean; + _loop: boolean; + _volume: number; + _rate: number; + _seek: number = 0; + _paused: boolean = true; + _ended: boolean = true; + _sprite: string = '__default'; + _id: number; + /** * Setup the sound object, which each node attached to a Howl group is contained in. * @param {Object} howl The Howl parent group. */ - constructor(howl) { + constructor(howl: Howl) { this._parent = howl; - this.init(); - } - - /** - * Initialize a new Sound object. - * @return {Sound} - */ - init() { - var parent = this._parent; // Setup the default parameters. - this._muted = parent._muted; - this._loop = parent._loop; - this._volume = parent._volume; - this._rate = parent._rate; - this._seek = 0; - this._paused = true; - this._ended = true; - this._sprite = '__default'; + this._muted = howl._muted; + this._loop = howl._loop; + this._volume = howl._volume; + this._rate = howl._rate; // Generate a unique ID for this sound. this._id = ++Howler._counter; From 9b560b3b81f2830783dfc795e03e40a4bed993dd Mon Sep 17 00:00:00 2001 From: Samuel Plumppu <6125097+Greenheart@users.noreply.github.com> Date: Sat, 25 Sep 2021 21:43:57 +0200 Subject: [PATCH 04/19] WIP refactoring --- src/helpers.ts | 13 ++- src/howl.ts | 310 ++++++++++++++++++++++++++++++++++++++++++------- src/howler.ts | 44 ++++--- 3 files changed, 304 insertions(+), 63 deletions(-) diff --git a/src/helpers.ts b/src/helpers.ts index 03cad36f..a8c4be51 100644 --- a/src/helpers.ts +++ b/src/helpers.ts @@ -1,3 +1,4 @@ +import Howl from './Howl'; import Howler from './howler'; const cache = {}; @@ -88,21 +89,21 @@ function safeXhrSend(xhr) { * @param {ArrayBuffer} arraybuffer The audio data. * @param {Howl} self */ -function decodeAudioData(arraybuffer, self) { +function decodeAudioData(arraybuffer: ArrayBuffer, self: Howl) { // Fire a load error if something broke. - var error = function () { + function error() { self._emit('loaderror', null, 'Decoding audio data failed.'); - }; + } // Load the sound on success. - var success = function (buffer) { + function success(buffer: AudioBuffer) { if (buffer && self._sounds.length > 0) { cache[self._src] = buffer; loadSound(self, buffer); } else { error(); } - }; + } // Decode the buffer into an audio source. if ( @@ -120,7 +121,7 @@ function decodeAudioData(arraybuffer, self) { * @param {Howl} self * @param {Object} buffer The decoded buffer sound source. */ -function loadSound(self, buffer) { +function loadSound(self: Howl, buffer?: AudioBuffer) { // Set the duration. if (buffer && !self._duration) { self._duration = buffer.duration; diff --git a/src/howl.ts b/src/howl.ts index 0233da21..e264a1cf 100644 --- a/src/howl.ts +++ b/src/howl.ts @@ -2,12 +2,253 @@ import Howler from './howler'; import { loadBuffer } from './helpers'; import Sound from './sound'; +export type HowlCallback = (soundId: number) => void; +export type HowlErrorCallback = (soundId: number, error: unknown) => void; + +export interface SoundSpriteDefinitions { + [name: string]: [number, number] | [number, number, boolean]; +} + +export interface HowlListeners { + /** + * Fires when the sound has been stopped. The first parameter is the ID of the sound. + */ + onstop?: HowlCallback; + + /** + * Fires when the sound has been paused. The first parameter is the ID of the sound. + */ + onpause?: HowlCallback; + + /** + * Fires when the sound is loaded. + */ + onload?: HowlCallback; + + /** + * Fires when the sound has been muted/unmuted. The first parameter is the ID of the sound. + */ + onmute?: HowlCallback; + + /** + * Fires when the sound's volume has changed. The first parameter is the ID of the sound. + */ + onvolume?: HowlCallback; + + /** + * Fires when the sound's playback rate has changed. The first parameter is the ID of the sound. + */ + onrate?: HowlCallback; + + /** + * Fires when the sound has been seeked. The first parameter is the ID of the sound. + */ + onseek?: HowlCallback; + + /** + * Fires when the current sound finishes fading in/out. The first parameter is the ID of the sound. + */ + onfade?: HowlCallback; + + /** + * Fires when audio has been automatically unlocked through a touch/click event. + */ + onunlock?: HowlCallback; + + /** + * Fires when the sound finishes playing (if it is looping, it'll fire at the end of each loop). + * The first parameter is the ID of the sound. + */ + onend?: HowlCallback; + + /** + * Fires when the sound begins playing. The first parameter is the ID of the sound. + */ + onplay?: HowlCallback; + + /** + * Fires when the sound is unable to load. The first parameter is the ID of the sound (if it exists) and the second is the error message/code. + */ + onloaderror?: HowlErrorCallback; + + /** + * Fires when the sound is unable to play. The first parameter is the ID of the sound and the second is the error message/code. + */ + onplayerror?: HowlErrorCallback; +} + +export interface HowlOptions extends HowlListeners { + /** + * The sources to the track(s) to be loaded for the sound (URLs or base64 data URIs). These should + * be in order of preference, howler.js will automatically load the first one that is compatible + * with the current browser. If your files have no extensions, you will need to explicitly specify + * the extension using the format property. + * + * @default `[]` + */ + src?: string | string[]; + + /** + * The volume of the specific track, from 0.0 to 1.0. + * + * @default `1.0` + */ + volume?: number; + + /** + * Set to true to force HTML5 Audio. This should be used for large audio files so that you don't + * have to wait for the full file to be downloaded and decoded before playing. + * + * @default `false` + */ + html5?: boolean; + + /** + * Set to true to automatically loop the sound forever. + * + * @default `false` + */ + loop?: boolean; + + /** + * Automatically begin downloading the audio file when the Howl is defined. If using HTML5 Audio, + * you can set this to 'metadata' to only preload the file's metadata (to get its duration without + * download the entire file, for example). + * + * @default `true` + */ + preload?: boolean | 'metadata'; + + /** + * Set to true to automatically start playback when sound is loaded. + * + * @default `false` + */ + autoplay?: boolean; + + /** + * Set to true to load the audio muted. + * + * @default `false` + */ + mute?: boolean; + + /** + * Define a sound sprite for the sound. The offset and duration are defined in milliseconds. A + * third (optional) parameter is available to set a sprite as looping. An easy way to generate + * compatible sound sprites is with audiosprite. + * + * @default `{}` + */ + sprite?: { + [name: string]: [number, number] | [number, number, boolean]; + }; + + /** + * The rate of playback. 0.5 to 4.0, with 1.0 being normal speed. + * + * @default `1.0` + */ + rate?: number; + + /** + * The size of the inactive sounds pool. Once sounds are stopped or finish playing, they are marked + * as ended and ready for cleanup. We keep a pool of these to recycle for improved performance. + * Generally this doesn't need to be changed. It is important to keep in mind that when a sound is + * paused, it won't be removed from the pool and will still be considered active so that it can be + * resumed later. + * + * @default `5` + */ + pool?: number; + + /** + * howler.js automatically detects your file format from the extension, but you may also specify a + * format in situations where extraction won't work (such as with a SoundCloud stream). + * + * @default `[]` + */ + format?: string[]; + + /** + * When using Web Audio, howler.js uses an XHR request to load the audio files. If you need to send + * custom headers, set the HTTP method or enable withCredentials (see reference), include them with + * this parameter. Each is optional (method defaults to GET, headers default to undefined and + * withCredentials defaults to false). + */ + xhr?: { + method?: string; + headers?: Record; + withCredentials?: boolean; + }; +} + +type HowlCallbacks = Array<{ fn: HowlCallback }>; +type HowlErrorCallbacks = Array<{ fn: HowlErrorCallback }>; + +type HowlEvent = + | 'play' + | 'end' + | 'pause' + | 'stop' + | 'mute' + | 'volume' + | 'rate' + | 'seek' + | 'fade' + | 'unlock'; + +interface HowlEventHandler { + event: HowlEvent; + action: () => void; +} + class Howl { + // User defined properties + _autoplay: boolean = false; + _format: HowlOptions['format']; + _html5: HowlOptions['html5']; + _muted: HowlOptions['mute']; + _loop: HowlOptions['loop']; + _pool: HowlOptions['pool']; + _preload: HowlOptions['preload']; + _rate: HowlOptions['rate']; + _sprite: HowlOptions['sprite']; + _src: HowlOptions['src']; + _volume: HowlOptions['volume']; + _xhr: HowlOptions['xhr']; + + // Other default properties. + _duration = 0; + _state = 'unloaded'; + _sounds: Sound[] = []; + _endTimers = {}; + _queue: HowlEventHandler[] = []; + _playLock = false; + + _onend: HowlCallbacks = []; + _onfade: HowlCallbacks = []; + _onload: HowlCallbacks = []; + _onloaderror: HowlErrorCallbacks = []; + _onplayerror: HowlErrorCallbacks = []; + _onpause: HowlCallbacks = []; + _onplay: HowlCallbacks = []; + _onstop: HowlCallbacks = []; + _onmute: HowlCallbacks = []; + _onvolume: HowlCallbacks = []; + _onrate: HowlCallbacks = []; + _onseek: HowlCallbacks = []; + _onunlock: HowlCallbacks = []; + _onresume: HowlCallbacks = []; + + // @ts-expect-error Not definitely assigned in constructor, likely due to using a module. + _webAudio: boolean; + /** * Create an audio group controller. * @param {Object} o Passed in properties for this group. */ - constructor(o) { + constructor(o: HowlOptions) { // Throw an error if no source is provided. if (!o.src || o.src.length === 0) { console.error( @@ -16,27 +257,18 @@ class Howl { return; } - this.init(o); - } - - /** - * Initialize a new Howl group object. - * @param {Object} o Passed in properties for this group. - * @return {Howl} - */ - init(o) { // If we don't have an AudioContext created yet, run the setup. if (!Howler.ctx) { Howler._setupAudioContext(); } // Setup user-defined default properties. - this._autoplay = o.autoplay || false; this._format = typeof o.format !== 'string' ? o.format : [o.format]; this._html5 = o.html5 || false; this._muted = o.mute || false; this._loop = o.loop || false; this._pool = o.pool || 5; + this._preload = typeof o.preload === 'boolean' || o.preload === 'metadata' ? o.preload @@ -47,19 +279,11 @@ class Howl { this._volume = o.volume !== undefined ? o.volume : 1; this._xhr = { method: o.xhr && o.xhr.method ? o.xhr.method : 'GET', - headers: o.xhr && o.xhr.headers ? o.xhr.headers : null, + headers: o.xhr && o.xhr.headers ? o.xhr.headers : undefined, withCredentials: o.xhr && o.xhr.withCredentials ? o.xhr.withCredentials : false, }; - // Setup all other default properties. - this._duration = 0; - this._state = 'unloaded'; - this._sounds = []; - this._endTimers = {}; - this._queue = []; - this._playLock = false; - // Setup event listeners. this._onend = o.onend ? [{ fn: o.onend }] : []; this._onfade = o.onfade ? [{ fn: o.onfade }] : []; @@ -91,7 +315,7 @@ class Howl { if (this._autoplay) { this._queue.push({ event: 'play', - action() { + action: () => { this.play(); }, }); @@ -101,8 +325,6 @@ class Howl { if (this._preload && this._preload !== 'none') { this.load(); } - - return this; } /** @@ -203,7 +425,7 @@ class Howl { * @param {Boolean} internal Internal Use: true prevents event firing. * @return {Number} Sound ID. */ - play(sprite, internal) { + play(sprite: string | number, internal?: boolean) { var id = null; // Determine if a sprite, sound id or nothing was passed @@ -267,7 +489,7 @@ class Howl { var soundId = sound._id; this._queue.push({ event: 'play', - action() { + action: () => { this.play(soundId); }, }); @@ -377,7 +599,7 @@ class Howl { } } else { // Fire this when the sound is ready to play to begin HTML5 Audio playback. - var playHtml5 = function () { + const playHtml5 = () => { node.currentTime = seek; node.muted = sound._muted || this._muted || Howler._muted || node.muted; node.volume = sound._volume * Howler.volume(); @@ -401,7 +623,7 @@ class Howl { // Releases the lock and executes queued actions. play - .then(function () { + .then(() => { this._playLock = false; node._unlocked = true; if (!internal) { @@ -410,7 +632,7 @@ class Howl { this._loadQueue(); } }) - .catch(function () { + .catch(() => { this._playLock = false; this._emit( 'playerror', @@ -477,9 +699,11 @@ class Howl { node.load(); } - // Play immediately if ready, or wait for the 'canplaythrough'e vent. + // Play immediately if ready, or wait for the 'canplaythrough'event. var loadedNoReadyState = + // @ts-expect-error Support old browsers (window && window.ejecta) || + // @ts-expect-error Support old browsers (!node.readyState && Howler._navigator.isCocoonJS); if (node.readyState >= 3 || loadedNoReadyState) { playHtml5(); @@ -487,7 +711,7 @@ class Howl { this._playLock = true; this._state = 'loading'; - var listener = function () { + const listener = () => { this._state = 'loaded'; // Begin playback. @@ -516,7 +740,7 @@ class Howl { if (this._state !== 'loaded' || this._playLock) { this._queue.push({ event: 'pause', - action() { + action: () => { this.pause(id); }, }); @@ -587,7 +811,7 @@ class Howl { if (this._state !== 'loaded' || this._playLock) { this._queue.push({ event: 'stop', - action() { + action: () => { this.stop(id); }, }); @@ -662,7 +886,7 @@ class Howl { if (this._state !== 'loaded' || this._playLock) { this._queue.push({ event: 'mute', - action() { + action: () => { this.mute(muted, id); }, }); @@ -750,7 +974,7 @@ class Howl { if (this._state !== 'loaded' || this._playLock) { this._queue.push({ event: 'volume', - action() { + action: () => { this.volume.apply(this, args); }, }); @@ -807,7 +1031,7 @@ class Howl { if (this._state !== 'loaded' || this._playLock) { this._queue.push({ event: 'fade', - action() { + action: () => { this.fade(from, to, len, id); }, }); @@ -1035,7 +1259,7 @@ class Howl { if (this._state !== 'loaded' || this._playLock) { this._queue.push({ event: 'rate', - action() { + action: () => { this.rate.apply(this, args); }, }); @@ -1148,7 +1372,7 @@ class Howl { ) { this._queue.push({ event: 'seek', - action() { + action: () => { this.seek.apply(this, args); }, }); @@ -1401,7 +1625,7 @@ class Howl { * @param {Number} id (optional) Only listen to events for this sound. * @return {Howl} */ - once(event, fn, id) { + once(event: string, fn: Function, id?: number) { // Setup the event listener. this.on(event, fn, id, 1); @@ -1415,7 +1639,7 @@ class Howl { * @param {Number} msg Message to go with event. * @return {Howl} */ - _emit(event, id, msg) { + _emit(event: string, id?: number, msg?: string) { var events = this['_on' + event]; // Loop through event store and fire all functions. @@ -1423,9 +1647,9 @@ class Howl { // Only fire the listener if the correct ID is used. if (!events[i].id || events[i].id === id || event === 'load') { setTimeout( - function (fn) { + ((fn) => { fn.call(this, id, msg); - }.bind(this, events[i].fn), + }).bind(this, events[i].fn), 0, ); @@ -1448,7 +1672,7 @@ class Howl { * after the previous has finished executing (even if async like play). * @return {Howl} */ - _loadQueue(event) { + _loadQueue(event: string) { if (this._queue.length > 0) { var task = this._queue[0]; @@ -1472,7 +1696,7 @@ class Howl { * @param {Sound} sound The sound object to work with. * @return {Howl} */ - _ended(sound) { + _ended(sound: Sound) { var sprite = sound._sprite; // If we are using IE and there was network latency we may be clipping diff --git a/src/howler.ts b/src/howler.ts index 723bf554..6b671fc8 100644 --- a/src/howler.ts +++ b/src/howler.ts @@ -31,6 +31,14 @@ interface HowlerAudioElement extends HTMLAudioElement { _unlocked: boolean; } +type HowlerAudioContextState = AudioContextState | 'suspending' | 'closed'; + +type HowlerAudioContext = Omit & { + // In iOS Safari, the state can also be set to 'interrupted' + // https://developer.mozilla.org/en-US/docs/Web/API/BaseAudioContext/state#resuming_interrupted_play_states_in_ios_safari + state: AudioContextState | 'interrupted'; +}; + // IDEA: Maybe use TS private properties to create clearer contexts. class Howler { @@ -39,7 +47,7 @@ class Howler { noAudio = false; usingWebAudio = true; autoSuspend = true; - ctx: AudioContext | null = null; + ctx: HowlerAudioContext | null = null; // Set to false to disable the auto audio unlocker. autoUnlock = true; @@ -57,10 +65,13 @@ class Howler { _muted = false; _volume = 1; _canPlayEvent = 'canplaythrough'; - _navigator: Navigator | null = - typeof window !== 'undefined' && window.navigator ? window.navigator : null; + _navigator = window.navigator; _audioUnlocked: boolean = false; _mobileUnloaded: boolean = false; + // IDEA: Maybe rename to `_state` to indicate that this is an internal property? + state: HowlerAudioContextState = 'suspended'; + _suspendTimer: number | null = null; + _resumeAfterSuspend?: boolean; /** * Create the global controller. All contained methods and properties apply @@ -108,13 +119,13 @@ class Howler { var sound = this._howls[i]._soundById(ids[j]); if (sound && sound._node) { - sound._node.volume = sound._volume * vol; + sound._node.volume = sound._volume * volume; } } } } - return this; + return volume; } return this._volume; @@ -127,7 +138,7 @@ class Howler { mute(muted) { // If we don't have an AudioContext created yet, run the setup. if (!this.ctx) { - setupAudioContext(); + this._setupAudioContext(); } this._muted = muted; @@ -189,7 +200,7 @@ class Howler { ) { this.ctx.close(); this.ctx = null; - setupAudioContext(); + this._setupAudioContext(); } return this; @@ -263,7 +274,9 @@ class Howler { try { if (typeof AudioContext !== 'undefined') { this.ctx = new AudioContext(); + // @ts-expect-error Safari backwards compatibility } else if (typeof webkitAudioContext !== 'undefined') { + // @ts-expect-error Safari backwards compatibility this.ctx = new webkitAudioContext(); } else { this.usingWebAudio = false; @@ -299,7 +312,8 @@ class Howler { if (this.usingWebAudio) { this.masterGain = typeof this.ctx.createGain === 'undefined' - ? this.ctx.createGainNode() + ? // @ts-expect-error Support old browsers + this.ctx.createGainNode() : this.ctx.createGain(); this.masterGain.gain.setValueAtTime( this._muted ? 0 : this._volume, @@ -428,7 +442,7 @@ class Howler { // 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. - const unlock = (e) => { + const unlock = () => { // 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 @@ -479,6 +493,7 @@ class Howler { // Play the empty buffer. if (typeof source.start === 'undefined') { + // @ts-expect-error .noteOn() only exists in old browsers. source.noteOn(0); } else { source.start(0); @@ -490,7 +505,7 @@ class Howler { } // Setup a timeout to check that we are unlocked on the next event loop. - source.onended = function () { + source.onended = () => { source.disconnect(0); // Update the unlocked state and prevent this check from happening again. @@ -534,6 +549,7 @@ class Howler { if ( testPlay && typeof Promise !== 'undefined' && + // @ts-expect-error (testPlay instanceof Promise || typeof testPlay.then === 'function') ) { testPlay.catch(function () { @@ -590,7 +606,7 @@ class Howler { } // If no sound has played after 30 seconds, suspend the context. - this._suspendTimer = setTimeout(function () { + this._suspendTimer = setTimeout(() => { if (!this.autoSuspend) { return; } @@ -599,7 +615,7 @@ class Howler { this.state = 'suspending'; // Handle updating the state of the audio context after suspending. - var handleSuspension = function () { + const handleSuspension = () => { this.state = 'suspended'; if (this._resumeAfterSuspend) { @@ -624,7 +640,7 @@ class Howler { if ( !this.ctx || typeof this.ctx.resume === 'undefined' || - !Howler.usingWebAudio + !this.usingWebAudio ) { return; } @@ -640,7 +656,7 @@ class Howler { this.state === 'suspended' || (this.state === 'running' && this.ctx.state === 'interrupted') ) { - this.ctx.resume().then(function () { + this.ctx.resume().then(() => { this.state = 'running'; // Emit to all Howls that the audio has resumed. From 5cfd871feb9ebb199e980c4a86e0660a17c0e00e Mon Sep 17 00:00:00 2001 From: Samuel Plumppu <6125097+Greenheart@users.noreply.github.com> Date: Sat, 25 Sep 2021 22:10:15 +0200 Subject: [PATCH 05/19] Fix type error with preloading --- src/howl.ts | 27 +++++++++++++++------------ src/sound.ts | 7 ++++--- 2 files changed, 19 insertions(+), 15 deletions(-) diff --git a/src/howl.ts b/src/howl.ts index e264a1cf..5de95e82 100644 --- a/src/howl.ts +++ b/src/howl.ts @@ -196,7 +196,10 @@ type HowlEvent = | 'rate' | 'seek' | 'fade' - | 'unlock'; + | 'unlock' + | 'load' + | 'loaderror' + | 'playerror'; interface HowlEventHandler { event: HowlEvent; @@ -207,15 +210,15 @@ class Howl { // User defined properties _autoplay: boolean = false; _format: HowlOptions['format']; - _html5: HowlOptions['html5']; - _muted: HowlOptions['mute']; - _loop: HowlOptions['loop']; - _pool: HowlOptions['pool']; - _preload: HowlOptions['preload']; - _rate: HowlOptions['rate']; + _html5: boolean = false; + _muted: boolean = false; + _loop: boolean = false; + _pool: number = 5; + _preload: boolean | 'metadata' = true; + _rate: number = 1; _sprite: HowlOptions['sprite']; _src: HowlOptions['src']; - _volume: HowlOptions['volume']; + _volume: number = 1; _xhr: HowlOptions['xhr']; // Other default properties. @@ -322,7 +325,7 @@ class Howl { } // Load the source file unless otherwise specified. - if (this._preload && this._preload !== 'none') { + if (this._preload) { this.load(); } } @@ -425,8 +428,8 @@ class Howl { * @param {Boolean} internal Internal Use: true prevents event firing. * @return {Number} Sound ID. */ - play(sprite: string | number, internal?: boolean) { - var id = null; + play(sprite?: string | number, internal?: boolean) { + var id: number | null = null; // Determine if a sprite, sound id or nothing was passed if (typeof sprite === 'number') { @@ -1672,7 +1675,7 @@ class Howl { * after the previous has finished executing (even if async like play). * @return {Howl} */ - _loadQueue(event: string) { + _loadQueue(event?: string) { if (this._queue.length > 0) { var task = this._queue[0]; diff --git a/src/sound.ts b/src/sound.ts index b5af4da8..5f441a74 100644 --- a/src/sound.ts +++ b/src/sound.ts @@ -28,8 +28,8 @@ class Sound { this._parent = howl; // Setup the default parameters. - this._muted = howl._muted; - this._loop = howl._loop; + this._muted = Boolean(howl._muted); + this._loop = Boolean(howl._loop); this._volume = howl._volume; this._rate = howl._rate; @@ -56,7 +56,8 @@ class Sound { // Create the gain node for controlling volume (the source will connect to this). this._node = typeof Howler.ctx.createGain === 'undefined' - ? Howler.ctx.createGainNode() + ? // @ts-expect-error Support old browsers + Howler.ctx.createGainNode() : Howler.ctx.createGain(); this._node.gain.setValueAtTime(volume, Howler.ctx.currentTime); this._node.paused = true; From 44aac28140e65602e7da7706b21fb63e21cdd871 Mon Sep 17 00:00:00 2001 From: Samuel Plumppu <6125097+Greenheart@users.noreply.github.com> Date: Sat, 25 Sep 2021 22:48:47 +0200 Subject: [PATCH 06/19] WIP refactor --- src/howl.ts | 39 ++++++++++++++++++++++----------------- src/howler.ts | 4 ++-- src/sound.ts | 23 ++++++++++++++++------- 3 files changed, 40 insertions(+), 26 deletions(-) diff --git a/src/howl.ts b/src/howl.ts index 5de95e82..c866de63 100644 --- a/src/howl.ts +++ b/src/howl.ts @@ -140,9 +140,7 @@ export interface HowlOptions extends HowlListeners { * * @default `{}` */ - sprite?: { - [name: string]: [number, number] | [number, number, boolean]; - }; + sprite?: SoundSpriteDefinitions; /** * The rate of playback. 0.5 to 4.0, with 1.0 being normal speed. @@ -209,15 +207,15 @@ interface HowlEventHandler { class Howl { // User defined properties _autoplay: boolean = false; - _format: HowlOptions['format']; + _format: string[] = []; _html5: boolean = false; _muted: boolean = false; _loop: boolean = false; _pool: number = 5; _preload: boolean | 'metadata' = true; _rate: number = 1; - _sprite: HowlOptions['sprite']; - _src: HowlOptions['src']; + _sprite: SoundSpriteDefinitions = {}; + _src: string | string[] = []; _volume: number = 1; _xhr: HowlOptions['xhr']; @@ -266,7 +264,12 @@ class Howl { } // Setup user-defined default properties. - this._format = typeof o.format !== 'string' ? o.format : [o.format]; + this._format = + o.format === undefined + ? [] + : typeof o.format !== 'string' + ? o.format + : [o.format]; this._html5 = o.html5 || false; this._muted = o.mute || false; this._loop = o.loop || false; @@ -335,7 +338,7 @@ class Howl { * @return {Howler} */ load() { - var url = null; + var url: string | null = null; // If no audio is available, quit immediately. if (Howler.noAudio) { @@ -434,6 +437,7 @@ class Howl { // Determine if a sprite, sound id or nothing was passed if (typeof sprite === 'number') { id = sprite; + // @ts-expect-error Not sure how to handle this with TypeScript. sprite = null; } else if ( typeof sprite === 'string' && @@ -458,6 +462,7 @@ class Howl { } if (num === 1) { + // @ts-expect-error Not sure how to handle this with TypeScript. sprite = null; } else { id = null; @@ -534,7 +539,7 @@ class Howl { sound._ended = false; // Update the parameters of the sound. - var setParams = function () { + const setParams = () => { sound._paused = false; sound._seek = seek; sound._start = start; @@ -552,7 +557,7 @@ class Howl { var node = sound._node; if (this._webAudio) { // Fire this when the sound is ready to play to begin Web Audio playback. - var playWebAudio = function () { + var playWebAudio = () => { this._playLock = false; setParams(); this._refreshBuffer(sound); @@ -582,7 +587,7 @@ class Howl { } if (!internal) { - setTimeout(function () { + setTimeout(() => { this._emit('play', sound._id); this._loadQueue(); }, 0); @@ -675,7 +680,7 @@ class Howl { timeout, ); } else { - this._endTimers[sound._id] = function () { + this._endTimers[sound._id] = () => { // Fire ended on this audio node. this._ended(sound); @@ -1106,7 +1111,7 @@ class Howl { sound._fadeTo = to; // Update the volume value on each interval tick. - sound._interval = setInterval(function () { + sound._interval = setInterval(() => { // Update the volume based on the time since the last tick. var tick = (Date.now() - lastTick) / len; lastTick = Date.now(); @@ -1405,7 +1410,7 @@ class Howl { } // Seek and emit when ready. - var seekAndEmit = function () { + const seekAndEmit = () => { // Restart the playback if the sound was playing. if (playing) { this.play(id, true); @@ -1416,7 +1421,7 @@ class Howl { // Wait for the play lock to be unset before emitting (HTML5 Audio). if (playing && !this._webAudio) { - var emitSeek = function () { + const emitSeek = () => { if (!this._playLock) { seekAndEmit(); } else { @@ -1642,7 +1647,7 @@ class Howl { * @param {Number} msg Message to go with event. * @return {Howl} */ - _emit(event: string, id?: number, msg?: string) { + _emit(event: string, id?: number | null, msg?: string) { var events = this['_on' + event]; // Loop through event store and fire all functions. @@ -1865,7 +1870,7 @@ class Howl { * @param {Number} id Only return one ID if one is passed. * @return {Array} Array of IDs. */ - _getSoundIds(id) { + _getSoundIds(id: number) { if (typeof id === 'undefined') { var ids = []; for (var i = 0; i < this._sounds.length; i++) { diff --git a/src/howler.ts b/src/howler.ts index 6b671fc8..28cc19d5 100644 --- a/src/howler.ts +++ b/src/howler.ts @@ -87,8 +87,8 @@ class Howler { * @param {Float} vol Volume from 0.0 to 1.0. * @return {Howler/Float} Returns self or current volume. */ - volume(vol: string) { - const volume = parseFloat(vol); + volume(vol?: string) { + const volume = typeof vol === 'string' ? parseFloat(vol) : undefined; // If we don't have an AudioContext created yet, run the setup. if (!this.ctx) { diff --git a/src/sound.ts b/src/sound.ts index 5f441a74..89632bff 100644 --- a/src/sound.ts +++ b/src/sound.ts @@ -20,6 +20,13 @@ class Sound { _sprite: string = '__default'; _id: number; + _node: GainNode | HTMLAudioElement; + _errorFn?: EventListener; + _loadFn?: EventListener; + _endFn?: EventListener; + + _rateSeek?: number; + /** * Setup the sound object, which each node attached to a Howl group is contained in. * @param {Object} howl The Howl parent group. @@ -37,7 +44,7 @@ class Sound { this._id = ++Howler._counter; // Add itself to the parent's pool. - parent._sounds.push(this); + this._parent._sounds.push(this); // Create the new node. this.create(); @@ -54,17 +61,18 @@ class Sound { if (parent._webAudio) { // Create the gain node for controlling volume (the source will connect to this). - this._node = + this._node = ( typeof Howler.ctx.createGain === 'undefined' ? // @ts-expect-error Support old browsers Howler.ctx.createGainNode() - : Howler.ctx.createGain(); + : Howler.ctx.createGain() + ) as GainNode; this._node.gain.setValueAtTime(volume, Howler.ctx.currentTime); this._node.paused = true; this._node.connect(Howler.masterGain); } else if (!Howler.noAudio) { // Get an unlocked Audio object from the pool. - this._node = Howler._obtainHtml5Audio(); + this._node = Howler._obtainHtml5Audio() as HTMLAudioElement; // Listen for errors (http://dev.w3.org/html5/spec-author-view/spec.html#mediaerror). this._errorFn = this._errorListener.bind(this); @@ -80,9 +88,10 @@ class Sound { this._node.addEventListener('ended', this._endFn, false); // Setup the new audio node. - this._node.src = parent._src; - this._node.preload = parent._preload === true ? 'auto' : parent._preload; - this._node.volume = volume * Howler.volume(); + this._node.src = parent._src as string; + this._node.preload = + parent._preload === true ? 'auto' : (parent._preload as string); + this._node.volume = volume * (Howler.volume() as number); // Begin loading the source. this._node.load(); From a921ae90706bb999f70698c9bb1a34580f566533 Mon Sep 17 00:00:00 2001 From: Samuel Plumppu <6125097+Greenheart@users.noreply.github.com> Date: Sat, 25 Sep 2021 23:14:50 +0200 Subject: [PATCH 07/19] WIP refactor --- src/helpers.ts | 8 +++++++- src/howl.ts | 8 ++++---- src/howler.ts | 2 +- src/sound.ts | 20 ++++++++++++++------ 4 files changed, 26 insertions(+), 12 deletions(-) diff --git a/src/helpers.ts b/src/helpers.ts index a8c4be51..09c0b038 100644 --- a/src/helpers.ts +++ b/src/helpers.ts @@ -1,7 +1,7 @@ import Howl from './Howl'; import Howler from './howler'; -const cache = {}; +export const cache = {}; /** * Buffer a sound from URL, Data URI or cache and decode to audio source (Web Audio API). @@ -139,3 +139,9 @@ function loadSound(self: Howl, buffer?: AudioBuffer) { self._loadQueue(); } } + +export const isHTMLAudioElement = (node: any): node is HTMLAudioElement => + (node as HTMLAudioElement).playbackRate !== undefined; + +export const isGainNode = (node: any): node is GainNode => + (node as GainNode).connect !== undefined; diff --git a/src/howl.ts b/src/howl.ts index c866de63..eafb7f00 100644 --- a/src/howl.ts +++ b/src/howl.ts @@ -1,5 +1,5 @@ import Howler from './howler'; -import { loadBuffer } from './helpers'; +import { loadBuffer, cache } from './helpers'; import Sound from './sound'; export type HowlCallback = (soundId: number) => void; @@ -610,7 +610,7 @@ class Howl { const playHtml5 = () => { node.currentTime = seek; node.muted = sound._muted || this._muted || Howler._muted || node.muted; - node.volume = sound._volume * Howler.volume(); + node.volume = sound._volume * (Howler.volume() as number); node.playbackRate = sound._rate; // Some browsers will throw an error if this is called without user interaction. @@ -1872,7 +1872,7 @@ class Howl { */ _getSoundIds(id: number) { if (typeof id === 'undefined') { - var ids = []; + var ids: number[] = []; for (var i = 0; i < this._sounds.length; i++) { ids.push(this._sounds[i]._id); } @@ -1888,7 +1888,7 @@ class Howl { * @param {Sound} sound The sound object to work with. * @return {Howl} */ - _refreshBuffer(sound) { + _refreshBuffer(sound: Sound) { // Setup the buffer source for playback. sound._node.bufferSource = Howler.ctx.createBufferSource(); sound._node.bufferSource.buffer = cache[this._src]; diff --git a/src/howler.ts b/src/howler.ts index 28cc19d5..c3776ed7 100644 --- a/src/howler.ts +++ b/src/howler.ts @@ -47,7 +47,7 @@ class Howler { noAudio = false; usingWebAudio = true; autoSuspend = true; - ctx: HowlerAudioContext | null = null; + ctx: HowlerAudioContext; // Set to false to disable the auto audio unlocker. autoUnlock = true; diff --git a/src/sound.ts b/src/sound.ts index 89632bff..179a7c58 100644 --- a/src/sound.ts +++ b/src/sound.ts @@ -8,6 +8,12 @@ import Howler from './howler'; import Howl from './howl'; +interface HowlGainNode extends GainNode { + bufferSource: AudioBufferSourceNode | null; + paused: boolean; + volume: number; +} + class Sound { _parent: Howl; _muted: boolean; @@ -20,10 +26,12 @@ class Sound { _sprite: string = '__default'; _id: number; - _node: GainNode | HTMLAudioElement; - _errorFn?: EventListener; - _loadFn?: EventListener; - _endFn?: EventListener; + _node: HowlGainNode | HTMLAudioElement; + _errorFn: EventListener; + _loadFn: EventListener; + _endFn: EventListener; + // TODO: Add better type when adding the spatial audio plugin. + _panner: unknown; _rateSeek?: number; @@ -66,10 +74,10 @@ class Sound { ? // @ts-expect-error Support old browsers Howler.ctx.createGainNode() : Howler.ctx.createGain() - ) as GainNode; + ) as HowlGainNode; this._node.gain.setValueAtTime(volume, Howler.ctx.currentTime); this._node.paused = true; - this._node.connect(Howler.masterGain); + this._node.connect(Howler.masterGain as GainNode); } else if (!Howler.noAudio) { // Get an unlocked Audio object from the pool. this._node = Howler._obtainHtml5Audio() as HTMLAudioElement; From 2371db2c0031c033aecfad5d083fd10816d04a0c Mon Sep 17 00:00:00 2001 From: Samuel Plumppu <6125097+Greenheart@users.noreply.github.com> Date: Sat, 25 Sep 2021 23:23:44 +0200 Subject: [PATCH 08/19] wip refactor --- src/howl.ts | 2 +- src/sound.ts | 54 +++++++++++++++++++++++++++++++--------------------- 2 files changed, 33 insertions(+), 23 deletions(-) diff --git a/src/howl.ts b/src/howl.ts index eafb7f00..88070260 100644 --- a/src/howl.ts +++ b/src/howl.ts @@ -1647,7 +1647,7 @@ class Howl { * @param {Number} msg Message to go with event. * @return {Howl} */ - _emit(event: string, id?: number | null, msg?: string) { + _emit(event: string, id?: number | null, msg?: string | number) { var events = this['_on' + event]; // Loop through event store and fire all functions. diff --git a/src/sound.ts b/src/sound.ts index 179a7c58..c24eb99d 100644 --- a/src/sound.ts +++ b/src/sound.ts @@ -27,9 +27,9 @@ class Sound { _id: number; _node: HowlGainNode | HTMLAudioElement; - _errorFn: EventListener; - _loadFn: EventListener; - _endFn: EventListener; + _errorFn: EventListener = () => {}; + _loadFn: EventListener = () => {}; + _endFn: EventListener = () => {}; // TODO: Add better type when adding the spatial audio plugin. _panner: unknown; @@ -54,6 +54,19 @@ class Sound { // Add itself to the parent's pool. this._parent._sounds.push(this); + if (this._parent._webAudio) { + // Create the gain node for controlling volume (the source will connect to this). + this._node = ( + typeof Howler.ctx.createGain === 'undefined' + ? // @ts-expect-error Support old browsers + Howler.ctx.createGainNode() + : Howler.ctx.createGain() + ) as HowlGainNode; + } else { + // Get an unlocked Audio object from the pool. + this._node = Howler._obtainHtml5Audio() as HTMLAudioElement; + } + // Create the new node. this.create(); } @@ -68,20 +81,13 @@ class Sound { Howler._muted || this._muted || this._parent._muted ? 0 : this._volume; if (parent._webAudio) { - // Create the gain node for controlling volume (the source will connect to this). - this._node = ( - typeof Howler.ctx.createGain === 'undefined' - ? // @ts-expect-error Support old browsers - Howler.ctx.createGainNode() - : Howler.ctx.createGain() - ) as HowlGainNode; - this._node.gain.setValueAtTime(volume, Howler.ctx.currentTime); - this._node.paused = true; - this._node.connect(Howler.masterGain as GainNode); + (this._node as HowlGainNode).gain.setValueAtTime( + volume, + Howler.ctx.currentTime, + ); + (this._node as HowlGainNode).paused = true; + (this._node as HowlGainNode).connect(Howler.masterGain as GainNode); } else if (!Howler.noAudio) { - // Get an unlocked Audio object from the pool. - this._node = Howler._obtainHtml5Audio() as HTMLAudioElement; - // Listen for errors (http://dev.w3.org/html5/spec-author-view/spec.html#mediaerror). this._errorFn = this._errorListener.bind(this); this._node.addEventListener('error', this._errorFn, false); @@ -96,13 +102,13 @@ class Sound { this._node.addEventListener('ended', this._endFn, false); // Setup the new audio node. - this._node.src = parent._src as string; - this._node.preload = + (this._node as HTMLAudioElement).src = parent._src as string; + (this._node as HTMLAudioElement).preload = parent._preload === true ? 'auto' : (parent._preload as string); this._node.volume = volume * (Howler.volume() as number); // Begin loading the source. - this._node.load(); + (this._node as HTMLAudioElement).load(); } return this; @@ -140,7 +146,9 @@ class Sound { this._parent._emit( 'loaderror', this._id, - this._node.error ? this._node.error.code : 0, + (this._node as HTMLAudioElement).error instanceof MediaError + ? ((this._node as HTMLAudioElement).error as MediaError).code + : 0, ); // Clear the event listener. @@ -154,7 +162,8 @@ class Sound { var parent = this._parent; // Round up the duration to account for the lower precision in HTML5 Audio. - parent._duration = Math.ceil(this._node.duration * 10) / 10; + parent._duration = + Math.ceil((this._node as HTMLAudioElement).duration * 10) / 10; // Setup a sprite if none is defined. if (Object.keys(parent._sprite).length === 0) { @@ -181,7 +190,8 @@ class Sound { 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(this._node.duration * 10) / 10; + parent._duration = + Math.ceil((this._node as HTMLAudioElement).duration * 10) / 10; // Update the sprite that corresponds to the real duration. if (parent._sprite.__default[1] === Infinity) { From 740e19372731c3059624935f76ea0357acba78ec Mon Sep 17 00:00:00 2001 From: Samuel Plumppu <6125097+Greenheart@users.noreply.github.com> Date: Sat, 25 Sep 2021 23:35:15 +0200 Subject: [PATCH 09/19] WIP refactor --- src/helpers.ts | 39 ++++++++++++++++++++------------------- src/howl.ts | 12 +++++++----- src/sound.ts | 8 +++----- 3 files changed, 30 insertions(+), 29 deletions(-) diff --git a/src/helpers.ts b/src/helpers.ts index 09c0b038..11f42d4f 100644 --- a/src/helpers.ts +++ b/src/helpers.ts @@ -1,14 +1,13 @@ -import Howl from './Howl'; +import Howl, { HowlXHROptions } from './Howl'; import Howler from './howler'; export const cache = {}; /** * Buffer a sound from URL, Data URI or cache and decode to audio source (Web Audio API). - * @param {Howl} self */ -export function loadBuffer(self) { - var url = self._src; +export function loadBuffer(self: Howl) { + var url = self._src as string; // Check if the buffer has already been cached and use it instead. if (cache[url]) { @@ -33,18 +32,19 @@ export function loadBuffer(self) { } else { // Load the buffer from the URL. var xhr = new XMLHttpRequest(); - xhr.open(self._xhr.method, url, true); - xhr.withCredentials = self._xhr.withCredentials; + xhr.open((self._xhr as HowlXHROptions).method as string, url, true); + xhr.withCredentials = (self._xhr as HowlXHROptions) + .withCredentials as boolean; xhr.responseType = 'arraybuffer'; // Apply any custom headers to the request. - if (self._xhr.headers) { - Object.keys(self._xhr.headers).forEach(function (key) { - xhr.setRequestHeader(key, self._xhr.headers[key]); + if (self._xhr as HowlXHROptions) { + Object.keys(self._xhr as HowlXHROptions).forEach(function (key) { + xhr.setRequestHeader(key, (self._xhr as HowlXHROptions)[key]); }); } - xhr.onload = function () { + xhr.onload = () => { // Make sure we get a successful response back. var code = (xhr.status + '')[0]; if (code !== '0' && code !== '2' && code !== '3') { @@ -58,7 +58,7 @@ export function loadBuffer(self) { decodeAudioData(xhr.response, self); }; - xhr.onerror = function () { + xhr.onerror = () => { // If there is an error, switch to HTML5 Audio. if (self._webAudio) { self._html5 = true; @@ -74,20 +74,20 @@ export function loadBuffer(self) { /** * Send the XHR request wrapped in a try/catch. - * @param {Object} xhr XHR to send. + * @param xhr XHR to send. */ -function safeXhrSend(xhr) { +function safeXhrSend(xhr: XMLHttpRequest) { try { xhr.send(); } catch (e) { - xhr.onerror(); + console.error('XHR Request failed: ', e); } } /** * Decode audio data from an array buffer. - * @param {ArrayBuffer} arraybuffer The audio data. - * @param {Howl} self + * @param arraybuffer The audio data. + * @param self */ function decodeAudioData(arraybuffer: ArrayBuffer, self: Howl) { // Fire a load error if something broke. @@ -98,7 +98,7 @@ function decodeAudioData(arraybuffer: ArrayBuffer, self: Howl) { // Load the sound on success. function success(buffer: AudioBuffer) { if (buffer && self._sounds.length > 0) { - cache[self._src] = buffer; + cache[self._src as string] = buffer; loadSound(self, buffer); } else { error(); @@ -118,8 +118,8 @@ function decodeAudioData(arraybuffer: ArrayBuffer, self: Howl) { /** * 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. + * @param self + * @param buffer The decoded buffer sound source. */ function loadSound(self: Howl, buffer?: AudioBuffer) { // Set the duration. @@ -140,6 +140,7 @@ function loadSound(self: Howl, buffer?: AudioBuffer) { } } +// NOTE: Maybe remove these export const isHTMLAudioElement = (node: any): node is HTMLAudioElement => (node as HTMLAudioElement).playbackRate !== undefined; diff --git a/src/howl.ts b/src/howl.ts index 88070260..e52677d3 100644 --- a/src/howl.ts +++ b/src/howl.ts @@ -9,6 +9,12 @@ export interface SoundSpriteDefinitions { [name: string]: [number, number] | [number, number, boolean]; } +export interface HowlXHROptions { + method?: string; + headers?: Record; + withCredentials?: boolean; +} + export interface HowlListeners { /** * Fires when the sound has been stopped. The first parameter is the ID of the sound. @@ -174,11 +180,7 @@ export interface HowlOptions extends HowlListeners { * this parameter. Each is optional (method defaults to GET, headers default to undefined and * withCredentials defaults to false). */ - xhr?: { - method?: string; - headers?: Record; - withCredentials?: boolean; - }; + xhr?: HowlXHROptions; } type HowlCallbacks = Array<{ fn: HowlCallback }>; diff --git a/src/sound.ts b/src/sound.ts index c24eb99d..66aefbe1 100644 --- a/src/sound.ts +++ b/src/sound.ts @@ -37,7 +37,7 @@ class Sound { /** * Setup the sound object, which each node attached to a Howl group is contained in. - * @param {Object} howl The Howl parent group. + * @param howl The Howl parent group. */ constructor(howl: Howl) { this._parent = howl; @@ -73,7 +73,6 @@ class Sound { /** * Create and setup a new sound object, whether HTML5 Audio or Web Audio. - * @return {Sound} */ create() { var parent = this._parent; @@ -116,7 +115,6 @@ class Sound { /** * Reset the parameters of this sound to the original state (for recycle). - * @return {Sound} */ reset() { var parent = this._parent; @@ -159,7 +157,7 @@ class Sound { * HTML5 Audio canplaythrough listener callback. */ _loadListener() { - var parent = this._parent; + const parent = this._parent; // Round up the duration to account for the lower precision in HTML5 Audio. parent._duration = @@ -184,7 +182,7 @@ class Sound { * HTML5 Audio ended listener callback. */ _endListener() { - var parent = this._parent; + const parent = this._parent; // Only handle the `ended`` event if the duration is Infinity. if (parent._duration === Infinity) { From 4375cf458482dc864853cb1bd2453b156a981503 Mon Sep 17 00:00:00 2001 From: Samuel Plumppu <6125097+Greenheart@users.noreply.github.com> Date: Sun, 26 Sep 2021 00:28:22 +0200 Subject: [PATCH 10/19] wip refactor --- src/helpers.ts | 5 + src/howl.ts | 290 ++++++++++++++++++++++++++++++------------------- src/sound.ts | 8 +- 3 files changed, 188 insertions(+), 115 deletions(-) diff --git a/src/helpers.ts b/src/helpers.ts index 11f42d4f..aa669792 100644 --- a/src/helpers.ts +++ b/src/helpers.ts @@ -1,5 +1,6 @@ import Howl, { HowlXHROptions } from './Howl'; import Howler from './howler'; +import { HowlGainNode } from './sound'; export const cache = {}; @@ -146,3 +147,7 @@ export const isHTMLAudioElement = (node: any): node is HTMLAudioElement => export const isGainNode = (node: any): node is GainNode => (node as GainNode).connect !== undefined; + +export const isAudioBufferSourceNode = ( + node: any, +): node is AudioBufferSourceNode => node instanceof AudioBufferSourceNode; diff --git a/src/howl.ts b/src/howl.ts index e52677d3..a2abc92d 100644 --- a/src/howl.ts +++ b/src/howl.ts @@ -1,6 +1,6 @@ import Howler from './howler'; -import { loadBuffer, cache } from './helpers'; -import Sound from './sound'; +import { loadBuffer, cache, isAudioBufferSourceNode } from './helpers'; +import Sound, { HowlGainNode } from './sound'; export type HowlCallback = (soundId: number) => void; export type HowlErrorCallback = (soundId: number, error: unknown) => void; @@ -223,7 +223,7 @@ class Howl { // Other default properties. _duration = 0; - _state = 'unloaded'; + _state: 'unloaded' | 'loading' | 'loaded' = 'unloaded'; _sounds: Sound[] = []; _endTimers = {}; _queue: HowlEventHandler[] = []; @@ -249,7 +249,7 @@ class Howl { /** * Create an audio group controller. - * @param {Object} o Passed in properties for this group. + * @param o Passed in properties for this group. */ constructor(o: HowlOptions) { // Throw an error if no source is provided. @@ -337,7 +337,6 @@ class Howl { /** * Load the audio file. - * @return {Howler} */ load() { var url: string | null = null; @@ -345,7 +344,7 @@ class Howl { // If no audio is available, quit immediately. if (Howler.noAudio) { this._emit('loaderror', null, 'No audio support.'); - return; + return this; } // Make sure our source is in an array. @@ -403,7 +402,7 @@ class Howl { null, 'No codec support for selected audio sources.', ); - return; + return this; } this._src = url; @@ -429,9 +428,9 @@ class Howl { /** * Play a sound or resume previous playback. - * @param {String/Number} sprite Sprite name for sprite playback or sound id to continue previous. - * @param {Boolean} internal Internal Use: true prevents event firing. - * @return {Number} Sound ID. + * @param sprite Sprite name for sprite playback or sound id to continue previous. + * @param internal Internal Use: true prevents event firing. + * @return Sound ID. */ play(sprite?: string | number, internal?: boolean) { var id: number | null = null; @@ -473,7 +472,7 @@ class Howl { } // Get the selected node, or get one from the pool. - var sound = id ? this._soundById(id) : this._inactiveSound(); + const sound = id ? this._soundById(id) : this._inactiveSound(); // If the sound doesn't exist, do nothing. if (!sound) { @@ -490,7 +489,7 @@ class Howl { // the order of function calls. if (this._state !== 'loaded') { // Set the sprite value on this sound. - sound._sprite = sprite; + sound._sprite = sprite as string; // Mark this sound as not ended in case another sound is played before this one loads. sound._ended = false; @@ -523,18 +522,22 @@ class Howl { } // Determine how long to play for and where to start playing. - var seek = Math.max( + const seek = Math.max( 0, - sound._seek > 0 ? sound._seek : this._sprite[sprite][0] / 1000, + sound._seek > 0 ? sound._seek : this._sprite[sprite as string][0] / 1000, ); - var duration = Math.max( + const duration = Math.max( 0, - (this._sprite[sprite][0] + this._sprite[sprite][1]) / 1000 - seek, + (this._sprite[sprite as string][0] + this._sprite[sprite as string][1]) / + 1000 - + seek, ); - var timeout = (duration * 1000) / Math.abs(sound._rate); - var start = this._sprite[sprite][0] / 1000; - var stop = (this._sprite[sprite][0] + this._sprite[sprite][1]) / 1000; - sound._sprite = sprite; + const timeout = (duration * 1000) / Math.abs(sound._rate); + const start = this._sprite[sprite as string][0] / 1000; + const stop = + (this._sprite[sprite as string][0] + this._sprite[sprite as string][1]) / + 1000; + sound._sprite = sprite as string; // 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. @@ -556,7 +559,7 @@ class Howl { } // Begin the actual playback. - var node = sound._node; + const node = sound._node; if (this._webAudio) { // Fire this when the sound is ready to play to begin Web Audio playback. var playWebAudio = () => { @@ -566,18 +569,29 @@ class Howl { // Setup the playback params. var vol = sound._muted || this._muted ? 0 : sound._volume; - node.gain.setValueAtTime(vol, Howler.ctx.currentTime); + (node as HowlGainNode).gain.setValueAtTime(vol, Howler.ctx.currentTime); sound._playStart = Howler.ctx.currentTime; // Play the sound using the supported method. - if (typeof node.bufferSource.start === 'undefined') { + if ( + typeof ((node as HowlGainNode).bufferSource as AudioBufferSourceNode) + .start === 'undefined' + ) { sound._loop - ? node.bufferSource.noteGrainOn(0, seek, 86400) - : node.bufferSource.noteGrainOn(0, seek, duration); + ? ((node as HowlGainNode).bufferSource as AudioBufferSourceNode) + // @ts-expect-error Support older browsers. + .noteGrainOn(0, seek, 86400) + : ((node as HowlGainNode).bufferSource as AudioBufferSourceNode) + // @ts-expect-error Support older browsers. + .noteGrainOn(0, seek, duration); } else { sound._loop - ? node.bufferSource.start(0, seek, 86400) - : node.bufferSource.start(0, seek, duration); + ? ( + (node as HowlGainNode).bufferSource as AudioBufferSourceNode + ).start(0, seek, 86400) + : ( + (node as HowlGainNode).bufferSource as AudioBufferSourceNode + ).start(0, seek, duration); } // Start a new timer if none is present. @@ -611,18 +625,23 @@ class Howl { // Fire this when the sound is ready to play to begin HTML5 Audio playback. const playHtml5 = () => { node.currentTime = seek; - node.muted = sound._muted || this._muted || Howler._muted || node.muted; + (node as HTMLAudioElement).muted = + sound._muted || + this._muted || + Howler._muted || + (node as HTMLAudioElement).muted; node.volume = sound._volume * (Howler.volume() as number); - node.playbackRate = sound._rate; + (node as HTMLAudioElement).playbackRate = sound._rate; // Some browsers will throw an error if this is called without user interaction. try { - var play = node.play(); + const play = (node as HTMLAudioElement).play(); // Support older browsers that don't support promises, and thus don't have this issue. if ( play && typeof Promise !== 'undefined' && + // @ts-expect-error (play instanceof Promise || typeof play.then === 'function') ) { // Implements a lock to prevent DOMException: The play() request was interrupted by a call to pause(). @@ -742,10 +761,9 @@ class Howl { /** * Pause playback and save current position. - * @param {Number} id The sound ID (empty to pause all in group). - * @return {Howl} + * @param id The sound ID (empty to pause all in group). */ - pause(id) { + pause(id: number) { // If the sound hasn't loaded or a play() promise is pending, add it to the load queue to pause when capable. if (this._state !== 'loaded' || this._playLock) { this._queue.push({ @@ -770,7 +788,7 @@ class Howl { if (sound && !sound._paused) { // Reset the seek position. - sound._seek = this.seek(ids[i]); + sound._seek = this.seek(ids[i]) as number; sound._rateSeek = 0; sound._paused = true; @@ -780,23 +798,36 @@ class Howl { if (sound._node) { if (this._webAudio) { // Make sure the sound has been created. - if (!sound._node.bufferSource) { + if (!(sound._node as HowlGainNode).bufferSource) { continue; } - if (typeof sound._node.bufferSource.stop === 'undefined') { - sound._node.bufferSource.noteOff(0); + if ( + typeof ( + (sound._node as HowlGainNode) + .bufferSource as AudioBufferSourceNode + ).stop === 'undefined' + ) { + ( + (sound._node as HowlGainNode) + .bufferSource as AudioBufferSourceNode + ) + // @ts-expect-error Support older browsers. + .noteOff(0); } else { - sound._node.bufferSource.stop(0); + ( + (sound._node as HowlGainNode) + .bufferSource as AudioBufferSourceNode + ).stop(0); } // Clean up the buffer source. this._cleanBuffer(sound._node); } else if ( - !isNaN(sound._node.duration) || - sound._node.duration === Infinity + !isNaN((sound._node as HTMLAudioElement).duration) || + (sound._node as HTMLAudioElement).duration === Infinity ) { - sound._node.pause(); + (sound._node as HTMLAudioElement).pause(); } } } @@ -812,11 +843,10 @@ class Howl { /** * Stop playback and reset to start. - * @param {Number} id The sound ID (empty to stop all in group). - * @param {Boolean} internal Internal Use: true prevents event firing. - * @return {Howl} + * @param id The sound ID (empty to stop all in group). + * @param internal Internal Use: true prevents event firing. */ - stop(id, internal) { + stop(id: number, internal?: boolean) { // If the sound hasn't loaded, add it to the load queue to stop when capable. if (this._state !== 'loaded' || this._playLock) { this._queue.push({ @@ -852,25 +882,38 @@ class Howl { if (sound._node) { if (this._webAudio) { // Make sure the sound's AudioBufferSourceNode has been created. - if (sound._node.bufferSource) { - if (typeof sound._node.bufferSource.stop === 'undefined') { - sound._node.bufferSource.noteOff(0); + if ((sound._node as HowlGainNode).bufferSource) { + if ( + typeof ( + (sound._node as HowlGainNode) + .bufferSource as AudioBufferSourceNode + ).stop === 'undefined' + ) { + ( + (sound._node as HowlGainNode) + .bufferSource as AudioBufferSourceNode + ) + // @ts-expect-error Support older browsers + .noteOff(0); } else { - sound._node.bufferSource.stop(0); + ( + (sound._node as HowlGainNode) + .bufferSource as AudioBufferSourceNode + ).stop(0); } // Clean up the buffer source. this._cleanBuffer(sound._node); } } else if ( - !isNaN(sound._node.duration) || - sound._node.duration === Infinity + !isNaN((sound._node as HTMLAudioElement).duration) || + (sound._node as HTMLAudioElement).duration === Infinity ) { sound._node.currentTime = sound._start || 0; - sound._node.pause(); + (sound._node as HTMLAudioElement).pause(); // If this is a live stream, stop download once the audio is stopped. - if (sound._node.duration === Infinity) { + if ((sound._node as HTMLAudioElement).duration === Infinity) { this._clearSound(sound._node); } } @@ -887,11 +930,10 @@ class Howl { /** * Mute/unmute a single sound or all sounds in this Howl group. - * @param {Boolean} muted Set to true to mute and false to unmute. - * @param {Number} id The sound ID to update (omit to mute/unmute all). - * @return {Howl} + * @param muted Set to true to mute and false to unmute. + * @param id The sound ID to update (omit to mute/unmute all). */ - mute(muted, id) { + mute(muted: boolean, id: number) { // If the sound hasn't loaded, add it to the load queue to mute when capable. if (this._state !== 'loaded' || this._playLock) { this._queue.push({ @@ -929,12 +971,14 @@ class Howl { } if (this._webAudio && sound._node) { - sound._node.gain.setValueAtTime( + (sound._node as HowlGainNode).gain.setValueAtTime( muted ? 0 : sound._volume, Howler.ctx.currentTime, ); } else if (sound._node) { - sound._node.muted = Howler._muted ? true : muted; + (sound._node as HTMLAudioElement).muted = Howler._muted + ? true + : muted; } this._emit('mute', sound._id); @@ -950,11 +994,10 @@ class Howl { * 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 this or current volume. + * @return Returns this or current volume. */ - volume() { - var args = arguments; - var vol, id; + volume(...args) { + let vol, id; // Determine the values based on arguments. if (args.length === 0) { @@ -978,7 +1021,7 @@ class Howl { } // Update the volume or return the current volume. - var sound; + let sound: Sound | null; 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 (this._state !== 'loaded' || this._playLock) { @@ -1012,9 +1055,12 @@ class Howl { } if (this._webAudio && sound._node && !sound._muted) { - sound._node.gain.setValueAtTime(vol, Howler.ctx.currentTime); + (sound._node as HowlGainNode).gain.setValueAtTime( + vol, + Howler.ctx.currentTime, + ); } else if (sound._node && !sound._muted) { - sound._node.volume = vol * Howler.volume(); + sound._node.volume = vol * (Howler.volume() as number); } this._emit('volume', sound._id); @@ -1030,13 +1076,17 @@ class Howl { /** * Fade a currently playing sound between two volumes (if no id is passed, all sounds will fade). - * @param {Number} from The value to fade from (0.0 to 1.0). - * @param {Number} to The volume to fade to (0.0 to 1.0). - * @param {Number} len Time in milliseconds to fade. - * @param {Number} id The sound id (omit to fade all sounds). - * @return {Howl} + * @param from The value to fade from (0.0 to 1.0). + * @param to The volume to fade to (0.0 to 1.0). + * @param len Time in milliseconds to fade. + * @param id The sound id (omit to fade all sounds). */ - fade(from, to, len, id) { + fade( + from: number | string, + to: number | string, + len: number | string, + id: number, + ) { // If the sound hasn't loaded, add it to the load queue to fade when capable. if (this._state !== 'loaded' || this._playLock) { this._queue.push({ @@ -1050,9 +1100,9 @@ class Howl { } // Make sure the to/from/len values are numbers. - from = Math.min(Math.max(0, parseFloat(from)), 1); - to = Math.min(Math.max(0, parseFloat(to)), 1); - len = parseFloat(len); + from = Math.min(Math.max(0, parseFloat(from as string)), 1); + to = Math.min(Math.max(0, parseFloat(to as string)), 1); + len = parseFloat(len as string); // Set the volume to the start position. this.volume(from, id); @@ -1075,8 +1125,8 @@ class Howl { var currentTime = Howler.ctx.currentTime; var end = currentTime + len / 1000; sound._volume = from; - sound._node.gain.setValueAtTime(from, currentTime); - sound._node.gain.linearRampToValueAtTime(to, end); + (sound._node as HowlGainNode).gain.setValueAtTime(from, currentTime); + (sound._node as HowlGainNode).gain.linearRampToValueAtTime(to, end); } this._startFadeInterval( @@ -1095,14 +1145,21 @@ class Howl { /** * Starts the internal interval to fade a sound. - * @param {Object} sound Reference to sound to fade. - * @param {Number} from The value to fade from (0.0 to 1.0). - * @param {Number} to The volume to fade to (0.0 to 1.0). - * @param {Number} len Time in milliseconds to fade. - * @param {Number} id The sound id to fade. - * @param {Boolean} isGroup If true, set the volume on the group. + * @param sound Reference to sound to fade. + * @param from The value to fade from (0.0 to 1.0). + * @param to The volume to fade to (0.0 to 1.0). + * @param len Time in milliseconds to fade. + * @param id The sound id to fade. + * @param isGroup If true, set the volume on the group. */ - _startFadeInterval(sound, from, to, len, id, isGroup) { + _startFadeInterval( + sound: Sound, + from: number, + to: number, + len: number, + id: number, + isGroup: boolean, + ) { var vol = from; var diff = to - from; var steps = Math.abs(diff / 0.01); @@ -1143,7 +1200,9 @@ class Howl { // When the fade is complete, stop it and fire event. if ((to < from && vol <= to) || (to > from && vol >= to)) { - clearInterval(sound._interval); + if (typeof sound._interval === 'number') { + clearInterval(sound._interval); + } sound._interval = null; sound._fadeTo = null; this.volume(to, sound._id); @@ -1163,7 +1222,9 @@ class Howl { if (sound && sound._interval) { if (this._webAudio) { - sound._node.gain.cancelScheduledValues(Howler.ctx.currentTime); + (sound._node as HowlGainNode).gain.cancelScheduledValues( + Howler.ctx.currentTime, + ); } clearInterval(sound._interval); @@ -1182,11 +1243,10 @@ class Howl { * 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 this or current loop value. + * @return Returns this or current loop value. */ - loop() { - var args = arguments; - var loop, id, sound; + loop(...args) { + let loop, id, sound; // Determine the values for loop and id. if (args.length === 0) { @@ -1343,11 +1403,11 @@ class Howl { * 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 this or the current seek position. + * @return Returns this or the current seek position. */ - seek() { - var args = arguments; - var seek, id; + seek(...args) { + let seek: number | undefined = undefined, + id: number | undefined = undefined; // Determine the values based on arguments. if (args.length === 0) { @@ -1452,10 +1512,10 @@ class Howl { /** * 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. + * @param id The sound id to check. If none is passed, the whole sound group is checked. + * @return True if playing and false if not. */ - playing(id) { + playing(id: number) { // Check the passed sound ID (if any). if (typeof id === 'number') { var sound = this._soundById(id); @@ -1474,10 +1534,10 @@ class Howl { /** * Get the duration of this sound. Passing a sound id will return the sprite duration. - * @param {Number} id The sound id to check. If none is passed, return full source duration. - * @return {Number} Audio duration in seconds. + * @param id The sound id to check. If none is passed, return full source duration. + * @return Audio duration in seconds. */ - duration(id) { + duration(id: number) { var duration = this._duration; // If we pass an ID, get the sound and return the sprite length. @@ -1491,7 +1551,7 @@ class Howl { /** * Returns the current loaded state of this Howl. - * @return {String} 'unloaded', 'loading', 'loaded' + * @return 'unloaded', 'loading', 'loaded' */ state() { return this._state; @@ -1869,10 +1929,10 @@ class Howl { /** * Get all ID's from the sounds pool. - * @param {Number} id Only return one ID if one is passed. - * @return {Array} Array of IDs. + * @param id Only return one ID if one is passed. + * @return Array of IDs. */ - _getSoundIds(id: number) { + _getSoundIds(id?: number) { if (typeof id === 'undefined') { var ids: number[] = []; for (var i = 0; i < this._sounds.length; i++) { @@ -1887,28 +1947,30 @@ class Howl { /** * Load the sound back into the buffer source. - * @param {Sound} sound The sound object to work with. - * @return {Howl} + * @param sound The sound object to work with. */ _refreshBuffer(sound: Sound) { // Setup the buffer source for playback. - sound._node.bufferSource = Howler.ctx.createBufferSource(); - sound._node.bufferSource.buffer = cache[this._src]; + (sound._node as HowlGainNode).bufferSource = + Howler.ctx.createBufferSource(); + (sound._node as HowlGainNode).bufferSource.buffer = cache[this._src]; // Connect to the correct node. if (sound._panner) { - sound._node.bufferSource.connect(sound._panner); + (sound._node as HowlGainNode).bufferSource.connect(sound._panner); } else { - sound._node.bufferSource.connect(sound._node); + (sound._node as HowlGainNode).bufferSource.connect( + sound._node as HowlGainNode, + ); } // Setup looping and playback rate. - sound._node.bufferSource.loop = sound._loop; + (sound._node as HowlGainNode).bufferSource.loop = sound._loop; if (sound._loop) { - sound._node.bufferSource.loopStart = sound._start || 0; - sound._node.bufferSource.loopEnd = sound._stop || 0; + (sound._node as HowlGainNode).bufferSource.loopStart = sound._start || 0; + (sound._node as HowlGainNode).bufferSource.loopEnd = sound._stop || 0; } - sound._node.bufferSource.playbackRate.setValueAtTime( + (sound._node as HowlGainNode).bufferSource.playbackRate.setValueAtTime( sound._rate, Howler.ctx.currentTime, ); diff --git a/src/sound.ts b/src/sound.ts index 66aefbe1..548b6a63 100644 --- a/src/sound.ts +++ b/src/sound.ts @@ -8,10 +8,11 @@ import Howler from './howler'; import Howl from './howl'; -interface HowlGainNode extends GainNode { +export interface HowlGainNode extends GainNode { bufferSource: AudioBufferSourceNode | null; paused: boolean; volume: number; + currentTime: number; } class Sound { @@ -34,6 +35,11 @@ class Sound { _panner: unknown; _rateSeek?: number; + _playStart: number = 0; + _start: number = 0; + _stop: number = 0; + _fadeTo: number | null = null; + _interval: number | null = null; /** * Setup the sound object, which each node attached to a Howl group is contained in. From 7f3b00ca266f691ea01af555cc83ac26e65638e7 Mon Sep 17 00:00:00 2001 From: Samuel Plumppu <6125097+Greenheart@users.noreply.github.com> Date: Sun, 26 Sep 2021 00:36:57 +0200 Subject: [PATCH 11/19] wip refactor --- src/howl.ts | 25 ++++++++++++------------- 1 file changed, 12 insertions(+), 13 deletions(-) diff --git a/src/howl.ts b/src/howl.ts index a2abc92d..dac02b33 100644 --- a/src/howl.ts +++ b/src/howl.ts @@ -762,8 +762,9 @@ class Howl { /** * Pause playback and save current position. * @param id The sound ID (empty to pause all in group). + * @param skipEmit If true, the `pause` event won't be emitted. */ - pause(id: number) { + pause(id: number, skipEmit?: boolean) { // If the sound hasn't loaded or a play() promise is pending, add it to the load queue to pause when capable. if (this._state !== 'loaded' || this._playLock) { this._queue.push({ @@ -832,8 +833,8 @@ class Howl { } } - // Fire the pause event, unless `true` is passed as the 2nd argument. - if (!arguments[1]) { + // Fire the pause event, unless skipEmit is `true` + if (!skipEmit) { this._emit('pause', sound ? sound._id : null); } } @@ -1298,11 +1299,10 @@ class Howl { * 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 this or the current playback rate. + * @return Returns this or the current playback rate. */ - rate() { - var args = arguments; - var rate, id; + rate(...args) { + let rate, id; // Determine the values based on arguments. if (args.length === 0) { @@ -1323,7 +1323,7 @@ class Howl { } // Update the playback rate or return the current value. - var sound; + let sound; if (typeof rate === 'number') { // If the sound hasn't loaded, add it to the load queue to change playback rate when capable. if (this._state !== 'loaded' || this._playLock) { @@ -1370,12 +1370,12 @@ class Howl { } // Reset the timers. - var seek = this.seek(id[i]); - var duration = + const seek = this.seek(id[i]) as number; + const duration = (this._sprite[sound._sprite][0] + this._sprite[sound._sprite][1]) / 1000 - seek; - var timeout = (duration * 1000) / Math.abs(sound._rate); + const timeout = (duration * 1000) / Math.abs(sound._rate); // Start a new end timer if sound is already playing. if (this._endTimers[id[i]] || !sound._paused) { @@ -1406,8 +1406,7 @@ class Howl { * @return Returns this or the current seek position. */ seek(...args) { - let seek: number | undefined = undefined, - id: number | undefined = undefined; + let seek, id; // Determine the values based on arguments. if (args.length === 0) { From af97db32414b6be1eda9e139e477e35ad9bf1105 Mon Sep 17 00:00:00 2001 From: Samuel Plumppu <6125097+Greenheart@users.noreply.github.com> Date: Sun, 26 Sep 2021 01:18:01 +0200 Subject: [PATCH 12/19] Phew! No type errors! --- src/core.ts | 6 +- src/helpers.ts | 16 +++-- src/howl.ts | 175 ++++++++++++++++++++++++++++--------------------- src/howler.ts | 111 ++++++++++++++----------------- src/sound.ts | 12 ++-- 5 files changed, 169 insertions(+), 151 deletions(-) diff --git a/src/core.ts b/src/core.ts index 99c700af..34aa53d3 100644 --- a/src/core.ts +++ b/src/core.ts @@ -7,5 +7,7 @@ * * MIT License */ -export Howler from './howler' -export Howl from './howl' \ No newline at end of file +import Howler from './Howler'; +import Howl from './Howl'; + +export { Howler, Howl }; diff --git a/src/helpers.ts b/src/helpers.ts index aa669792..709aa41f 100644 --- a/src/helpers.ts +++ b/src/helpers.ts @@ -1,6 +1,5 @@ import Howl, { HowlXHROptions } from './Howl'; -import Howler from './howler'; -import { HowlGainNode } from './sound'; +import Howler, { HowlerAudioContext } from './howler'; export const cache = {}; @@ -109,11 +108,18 @@ function decodeAudioData(arraybuffer: ArrayBuffer, self: Howl) { // Decode the buffer into an audio source. if ( typeof Promise !== 'undefined' && - Howler.ctx.decodeAudioData.length === 1 + (Howler.ctx as HowlerAudioContext).decodeAudioData.length === 1 ) { - Howler.ctx.decodeAudioData(arraybuffer).then(success).catch(error); + (Howler.ctx as HowlerAudioContext) + .decodeAudioData(arraybuffer) + .then(success) + .catch(error); } else { - Howler.ctx.decodeAudioData(arraybuffer, success, error); + (Howler.ctx as HowlerAudioContext).decodeAudioData( + arraybuffer, + success, + error, + ); } } diff --git a/src/howl.ts b/src/howl.ts index dac02b33..ad2837ac 100644 --- a/src/howl.ts +++ b/src/howl.ts @@ -1,5 +1,5 @@ -import Howler from './howler'; -import { loadBuffer, cache, isAudioBufferSourceNode } from './helpers'; +import Howler, { HowlerAudioContext, HowlerAudioElement } from './howler'; +import { loadBuffer, cache } from './helpers'; import Sound, { HowlGainNode } from './sound'; export type HowlCallback = (soundId: number) => void; @@ -549,7 +549,7 @@ class Howl { sound._seek = seek; sound._start = start; sound._stop = stop; - sound._loop = !!(sound._loop || this._sprite[sprite][2]); + sound._loop = !!(sound._loop || this._sprite[sprite as string][2]); }; // End the sound instantly if seek is at the end. @@ -562,15 +562,18 @@ class Howl { const node = sound._node; if (this._webAudio) { // Fire this when the sound is ready to play to begin Web Audio playback. - var playWebAudio = () => { + const playWebAudio = () => { this._playLock = false; setParams(); this._refreshBuffer(sound); // Setup the playback params. - var vol = sound._muted || this._muted ? 0 : sound._volume; - (node as HowlGainNode).gain.setValueAtTime(vol, Howler.ctx.currentTime); - sound._playStart = Howler.ctx.currentTime; + const vol = sound._muted || this._muted ? 0 : sound._volume; + (node as HowlGainNode).gain.setValueAtTime( + vol, + (Howler.ctx as HowlerAudioContext).currentTime, + ); + sound._playStart = (Howler.ctx as HowlerAudioContext).currentTime; // Play the sound using the supported method. if ( @@ -610,7 +613,10 @@ class Howl { } }; - if (Howler.state === 'running' && Howler.ctx.state !== 'interrupted') { + if ( + Howler.state === 'running' && + (Howler.ctx as HowlerAudioContext).state !== 'interrupted' + ) { playWebAudio(); } else { this._playLock = true; @@ -654,7 +660,7 @@ class Howl { play .then(() => { this._playLock = false; - node._unlocked = true; + (node as HowlerAudioElement)._unlocked = true; if (!internal) { this._emit('play', sound._id); } else { @@ -681,7 +687,7 @@ class Howl { } // Setting rate before playing won't work in IE, so we set it again here. - node.playbackRate = sound._rate; + (node as HTMLAudioElement).playbackRate = sound._rate; // If the node is still paused, then we can assume there was a playback issue. if (node.paused) { @@ -715,26 +721,26 @@ class Howl { node.addEventListener('ended', this._endTimers[sound._id], false); } } catch (err) { - this._emit('playerror', sound._id, err); + this._emit('playerror', sound._id, err as any); } }; // If this is streaming audio, make sure the src is set and load again. if ( - node.src === + (node as HTMLAudioElement).src === 'data:audio/wav;base64,UklGRigAAABXQVZFZm10IBIAAAABAAEARKwAAIhYAQACABAAAABkYXRhAgAAAAEA' ) { - node.src = this._src; - node.load(); + (node as HTMLAudioElement).src = this._src as string; + (node as HTMLAudioElement).load(); } // Play immediately if ready, or wait for the 'canplaythrough'event. - var loadedNoReadyState = + const loadedNoReadyState = // @ts-expect-error Support old browsers (window && window.ejecta) || // @ts-expect-error Support old browsers (!node.readyState && Howler._navigator.isCocoonJS); - if (node.readyState >= 3 || loadedNoReadyState) { + if ((node as HTMLAudioElement).readyState >= 3 || loadedNoReadyState) { playHtml5(); } else { this._playLock = true; @@ -847,7 +853,7 @@ class Howl { * @param id The sound ID (empty to stop all in group). * @param internal Internal Use: true prevents event firing. */ - stop(id: number, internal?: boolean) { + stop(id?: number, internal?: boolean) { // If the sound hasn't loaded, add it to the load queue to stop when capable. if (this._state !== 'loaded' || this._playLock) { this._queue.push({ @@ -971,7 +977,7 @@ class Howl { this._stopFade(sound._id); } - if (this._webAudio && sound._node) { + if (this._webAudio && sound._node && Howler.ctx) { (sound._node as HowlGainNode).gain.setValueAtTime( muted ? 0 : sound._volume, Howler.ctx.currentTime, @@ -1055,7 +1061,7 @@ class Howl { this._stopFade(id[i]); } - if (this._webAudio && sound._node && !sound._muted) { + if (this._webAudio && sound._node && !sound._muted && Howler.ctx) { (sound._node as HowlGainNode).gain.setValueAtTime( vol, Howler.ctx.currentTime, @@ -1122,7 +1128,7 @@ class Howl { } // If we are using Web Audio, let the native methods do the actual fade. - if (this._webAudio && !sound._muted) { + if (this._webAudio && !sound._muted && Howler.ctx) { var currentTime = Howler.ctx.currentTime; var end = currentTime + len / 1000; sound._volume = from; @@ -1222,7 +1228,7 @@ class Howl { var sound = this._soundById(id); if (sound && sound._interval) { - if (this._webAudio) { + if (this._webAudio && Howler.ctx) { (sound._node as HowlGainNode).gain.cancelScheduledValues( Howler.ctx.currentTime, ); @@ -1348,7 +1354,7 @@ class Howl { // Get the sound. sound = this._soundById(id[i]); - if (sound) { + if (sound && Howler.ctx) { // Keep track of our position when the rate changed and update the playback // start position so we can properly adjust the seek position for time elapsed. if (this.playing(id[i])) { @@ -1466,7 +1472,11 @@ class Howl { this._clearTimer(id); // Update the seek position for HTML5 Audio. - if (!this._webAudio && sound._node && !isNaN(sound._node.duration)) { + if ( + !this._webAudio && + sound._node && + !isNaN((sound._node as HTMLAudioElement).duration) + ) { sound._node.currentTime = seek; } @@ -1494,11 +1504,11 @@ class Howl { seekAndEmit(); } } else { - if (this._webAudio) { - var realTime = this.playing(id) + if (this._webAudio && Howler.ctx) { + const realTime = this.playing(id) ? Howler.ctx.currentTime - sound._playStart : 0; - var rateSeek = sound._rateSeek ? sound._rateSeek - sound._seek : 0; + const rateSeek = sound._rateSeek ? sound._rateSeek - sound._seek : 0; return sound._seek + (rateSeek + realTime * Math.abs(sound._rate)); } else { return sound._node.currentTime; @@ -1563,7 +1573,7 @@ class Howl { unload() { // Stop playing any active sounds. var sounds = this._sounds; - for (var i = 0; i < sounds.length; i++) { + for (let i = 0; i < sounds.length; i++) { // Stop the sound if it is currently playing. if (!sounds[i]._paused) { this.stop(sounds[i]._id); @@ -1584,10 +1594,11 @@ class Howl { sounds[i]._node.removeEventListener('ended', sounds[i]._endFn, false); // Release the Audio object back to the pool. - Howler._releaseHtml5Audio(sounds[i]._node); + Howler._releaseHtml5Audio(sounds[i]._node as HowlerAudioElement); } // Empty out all of the nodes. + // @ts-expect-error Disable type checking to avoid dynamic edge case. delete sounds[i]._node; // Make sure all timers are cleared out. @@ -1602,10 +1613,10 @@ class Howl { // Delete this sound from the cache (if no other Howl is using it). var remCache = true; - for (i = 0; i < Howler._howls.length; i++) { + for (let i = 0; i < Howler._howls.length; i++) { if ( Howler._howls[i]._src === this._src || - this._src.indexOf(Howler._howls[i]._src) >= 0 + this._src.indexOf(Howler._howls[i]._src as string) >= 0 ) { remCache = false; break; @@ -1613,7 +1624,7 @@ class Howl { } if (cache && remCache) { - delete cache[this._src]; + delete cache[this._src as string]; } // Clear global errors. @@ -1622,6 +1633,7 @@ class Howl { // Clear out `this`. this._state = 'unloaded'; this._sounds = []; + // @ts-expect-error Temporarily ignore strict type checking to allow dynamic JS this = null; return null; @@ -1633,9 +1645,8 @@ class Howl { * @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(event, fn, id, once) { + on(event: string, fn: Function, id?: number, once?: number) { var events = this['_on' + event]; if (typeof fn === 'function') { @@ -1689,10 +1700,9 @@ class Howl { /** * Listen to a custom event and remove it once fired. - * @param {String} event Event name. - * @param {Function} fn Listener to call. - * @param {Number} id (optional) Only listen to events for this sound. - * @return {Howl} + * @param event Event name. + * @param fn Listener to call. + * @param id (optional) Only listen to events for this sound. */ once(event: string, fn: Function, id?: number) { // Setup the event listener. @@ -1703,10 +1713,9 @@ class Howl { /** * Emit all events of a specific type and pass the sound id. - * @param {String} event Event name. - * @param {Number} id Sound ID. - * @param {Number} msg Message to go with event. - * @return {Howl} + * @param event Event name. + * @param id Sound ID. + * @param msg Message to go with event. */ _emit(event: string, id?: number | null, msg?: string | number) { var events = this['_on' + event]; @@ -1739,7 +1748,6 @@ class Howl { * 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(event?: string) { if (this._queue.length > 0) { @@ -1762,8 +1770,7 @@ class Howl { /** * Fired when playback ends at the end of the duration. - * @param {Sound} sound The sound object to work with. - * @return {Howl} + * @param sound The sound object to work with. */ _ended(sound: Sound) { var sprite = sound._sprite; @@ -1775,7 +1782,7 @@ class Howl { !this._webAudio && sound._node && !sound._node.paused && - !sound._node.ended && + !(sound._node as HowlerAudioElement).ended && sound._node.currentTime < sound._stop ) { setTimeout(this._ended.bind(this, sound), 100); @@ -1794,7 +1801,7 @@ class Howl { } // Restart this timer if on a Web Audio loop. - if (this._webAudio && loop) { + if (this._webAudio && loop && Howler.ctx) { this._emit('play', sound._id); sound._seek = sound._start || 0; sound._rateSeek = 0; @@ -1834,9 +1841,8 @@ class Howl { /** * Clear the end timer for a sound playback. * @param {Number} id The sound ID. - * @return {Howl} */ - _clearTimer(id) { + _clearTimer(id: number) { if (this._endTimers[id]) { // Clear the timeout or remove the ended listener. if (typeof this._endTimers[id] !== 'function') { @@ -1855,10 +1861,10 @@ class Howl { } /** * Return the sound identified by this ID, or return null. - * @param {Number} id Sound ID - * @return {Object} Sound object or null. + * @param id Sound ID + * @return Sound object or null. */ - _soundById(id) { + _soundById(id: number) { // Loop through all sounds and find the one with this ID. for (var i = 0; i < this._sounds.length; i++) { if (id === this._sounds[i]._id) { @@ -1871,13 +1877,13 @@ class Howl { /** * Return an inactive sound from the pool or create a new one. - * @return {Sound} Sound playback object. + * @return Sound playback object. */ _inactiveSound() { this._drain(); // Find the first inactive node to recycle. - for (var i = 0; i < this._sounds.length; i++) { + for (let i = 0; i < this._sounds.length; i++) { if (this._sounds[i]._ended) { return this._sounds[i].reset(); } @@ -1891,9 +1897,9 @@ class Howl { * Drain excess inactive sounds from the pool. */ _drain() { - var limit = this._pool; - var cnt = 0; - var i = 0; + const limit = this._pool; + let cnt = 0; + let i = 0; // If there are less sounds than the max pool size, we are done. if (this._sounds.length < limit) { @@ -1916,7 +1922,7 @@ class Howl { if (this._sounds[i]._ended) { // Disconnect the audio source when using Web Audio. if (this._webAudio && this._sounds[i]._node) { - this._sounds[i]._node.disconnect(0); + (this._sounds[i]._node as HowlGainNode).disconnect(0); } // Remove sounds until we have the pool size. @@ -1950,28 +1956,40 @@ class Howl { */ _refreshBuffer(sound: Sound) { // Setup the buffer source for playback. - (sound._node as HowlGainNode).bufferSource = - Howler.ctx.createBufferSource(); - (sound._node as HowlGainNode).bufferSource.buffer = cache[this._src]; + (sound._node as HowlGainNode).bufferSource = ( + Howler.ctx as HowlerAudioContext + ).createBufferSource(); + ( + (sound._node as HowlGainNode).bufferSource as AudioBufferSourceNode + ).buffer = cache[this._src as string]; // Connect to the correct node. if (sound._panner) { - (sound._node as HowlGainNode).bufferSource.connect(sound._panner); + ( + (sound._node as HowlGainNode).bufferSource as AudioBufferSourceNode + ).connect(sound._panner); } else { - (sound._node as HowlGainNode).bufferSource.connect( - sound._node as HowlGainNode, - ); + ( + (sound._node as HowlGainNode).bufferSource as AudioBufferSourceNode + ).connect(sound._node as HowlGainNode); } // Setup looping and playback rate. - (sound._node as HowlGainNode).bufferSource.loop = sound._loop; + ((sound._node as HowlGainNode).bufferSource as AudioBufferSourceNode).loop = + sound._loop; if (sound._loop) { - (sound._node as HowlGainNode).bufferSource.loopStart = sound._start || 0; - (sound._node as HowlGainNode).bufferSource.loopEnd = sound._stop || 0; - } - (sound._node as HowlGainNode).bufferSource.playbackRate.setValueAtTime( + ( + (sound._node as HowlGainNode).bufferSource as AudioBufferSourceNode + ).loopStart = sound._start || 0; + ( + (sound._node as HowlGainNode).bufferSource as AudioBufferSourceNode + ).loopEnd = sound._stop || 0; + } + ( + (sound._node as HowlGainNode).bufferSource as AudioBufferSourceNode + ).playbackRate.setValueAtTime( sound._rate, - Howler.ctx.currentTime, + (Howler.ctx as HowlerAudioContext).currentTime, ); return this; @@ -1982,20 +2000,25 @@ class Howl { * @param {Object} node Sound's audio node containing the buffer source. * @return {Howl} */ - _cleanBuffer(node) { + _cleanBuffer(node: Sound['_node']) { var isIOS = Howler._navigator && Howler._navigator.vendor.indexOf('Apple') >= 0; - if (Howler._scratchBuffer && node.bufferSource) { - node.bufferSource.onended = null; - node.bufferSource.disconnect(0); + if (Howler._scratchBuffer && (node as HowlGainNode).bufferSource) { + ((node as HowlGainNode).bufferSource as AudioBufferSourceNode).onended = + null; + ((node as HowlGainNode).bufferSource as AudioBufferSourceNode).disconnect( + 0, + ); if (isIOS) { try { - node.bufferSource.buffer = Howler._scratchBuffer; + ( + (node as HowlGainNode).bufferSource as AudioBufferSourceNode + ).buffer = Howler._scratchBuffer; } catch (e) {} } } - node.bufferSource = null; + (node as HowlGainNode).bufferSource = null; return this; } diff --git a/src/howler.ts b/src/howler.ts index c3776ed7..8b78a3f3 100644 --- a/src/howler.ts +++ b/src/howler.ts @@ -1,39 +1,16 @@ import Howl from './Howl'; -interface HowlerInstance { - mute(muted: boolean): this; - stop(): this; - volume(): number; - volume(volume: number): this; - codecs(ext: string): boolean; - unload(): this; - usingWebAudio: boolean; - html5PoolSize: number; - noAudio: boolean; - autoUnlock: boolean; - autoSuspend: boolean; - ctx: AudioContext; - masterGain: GainNode; - - stereo(pan: number): this; - pos(x: number, y: number, z: number): this | void; - orientation( - x: number, - y: number, - z: number, - xUp: number, - yUp: number, - zUp: number, - ): this | void; -} - -interface HowlerAudioElement extends HTMLAudioElement { +export interface HowlerAudioElement extends HTMLAudioElement { _unlocked: boolean; } -type HowlerAudioContextState = AudioContextState | 'suspending' | 'closed'; +type HowlerAudioContextState = + | AudioContextState + | 'suspending' + | 'closed' + | 'interrupted'; -type HowlerAudioContext = Omit & { +export type HowlerAudioContext = Omit & { // In iOS Safari, the state can also be set to 'interrupted' // https://developer.mozilla.org/en-US/docs/Web/API/BaseAudioContext/state#resuming_interrupted_play_states_in_ios_safari state: AudioContextState | 'interrupted'; @@ -47,7 +24,7 @@ class Howler { noAudio = false; usingWebAudio = true; autoSuspend = true; - ctx: HowlerAudioContext; + ctx: HowlerAudioContext | null = null; // Set to false to disable the auto audio unlocker. autoUnlock = true; @@ -73,6 +50,8 @@ class Howler { _suspendTimer: number | null = null; _resumeAfterSuspend?: boolean; + _scratchBuffer: any; + /** * Create the global controller. All contained methods and properties apply * to all sounds that are currently playing or will be in the future. @@ -104,7 +83,7 @@ class Howler { } // When using Web Audio, we just need to adjust the master gain. - if (this.usingWebAudio) { + if (this.usingWebAudio && this.masterGain && this.ctx) { this.masterGain.gain.setValueAtTime(volume, this.ctx.currentTime); } @@ -133,9 +112,9 @@ class Howler { /** * Handle muting and unmuting globally. - * @param {Boolean} muted Is muted or not. + * @param muted Is muted or not. */ - mute(muted) { + mute(muted: boolean) { // If we don't have an AudioContext created yet, run the setup. if (!this.ctx) { this._setupAudioContext(); @@ -144,10 +123,10 @@ class Howler { this._muted = muted; // With Web Audio, we just need to mute the master gain. - if (this.usingWebAudio) { + if (this.usingWebAudio && this.masterGain && this.ctx) { this.masterGain.gain.setValueAtTime( muted ? 0 : this._volume, - Howler.ctx.currentTime, + this.ctx.currentTime, ); } @@ -162,7 +141,9 @@ class Howler { var sound = this._howls[i]._soundById(ids[j]); if (sound && sound._node) { - sound._node.muted = muted ? true : sound._muted; + (sound._node as HowlerAudioElement).muted = muted + ? true + : sound._muted; } } } @@ -209,9 +190,8 @@ class Howler { /** * Check for codec support of specific extension. * @param {String} ext Audio file extention. - * @return {Boolean} */ - codecs(ext) { + codecs(ext: string) { return this._codecs[ext.replace(/^x-/, '')]; } @@ -309,17 +289,17 @@ class Howler { } // Create and expose the master GainNode when using Web Audio (useful for plugins or advanced usage). - if (this.usingWebAudio) { + if (this.usingWebAudio && this.masterGain && this.ctx) { this.masterGain = typeof this.ctx.createGain === 'undefined' ? // @ts-expect-error Support old browsers this.ctx.createGainNode() : this.ctx.createGain(); - this.masterGain.gain.setValueAtTime( + (this.masterGain as GainNode).gain.setValueAtTime( this._muted ? 0 : this._volume, this.ctx.currentTime, ); - this.masterGain.connect(this.ctx.destination); + (this.masterGain as GainNode).connect(this.ctx.destination); } // Re-run the setup on Howler. @@ -331,7 +311,7 @@ class Howler { * @return {Howler} */ _setupCodecs() { - var audioTest: HTMLAudioElement | null = null; + let audioTest: HTMLAudioElement | null = null; // Must wrap in a try/catch because IE11 in server mode throws an error. try { @@ -344,17 +324,17 @@ class Howler { return this; } - var mpegTest = audioTest.canPlayType('audio/mpeg;').replace(/^no$/, ''); + const 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 = this._navigator ? this._navigator.userAgent : ''; - var checkOpera = ua.match(/OPR\/([0-6].)/g); - var isOldOpera = + const ua = this._navigator ? this._navigator.userAgent : ''; + const checkOpera = ua.match(/OPR\/([0-6].)/g); + const isOldOpera = checkOpera && parseInt(checkOpera[0].split('/')[1], 10) < 33; - var checkSafari = + const checkSafari = ua.indexOf('Safari') !== -1 && ua.indexOf('Chrome') === -1; - var safariVersion = ua.match(/Version\/(.*?) /); - var isOldSafari = + const safariVersion = ua.match(/Version\/(.*?) /); + const isOldSafari = checkSafari && safariVersion && parseInt(safariVersion[1], 10) < 15; this._codecs = { @@ -417,12 +397,11 @@ class Howler { * 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() { // Only run this if Web Audio is supported and it hasn't already been unlocked. if (this._audioUnlocked || !this.ctx) { - return; + return this; } this.autoUnlock = false; @@ -437,7 +416,7 @@ class Howler { // Scratch buffer for enabling iOS to dispose of web audio buffers correctly, as per: // http://stackoverflow.com/questions/24119684 - const scratchBuffer = this.ctx.createBuffer(1, 1, 22050); + this._scratchBuffer = this.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 @@ -475,9 +454,13 @@ class Howler { for (var j = 0; j < ids.length; j++) { var sound = this._howls[i]._soundById(ids[j]); - if (sound && sound._node && !sound._node._unlocked) { - sound._node._unlocked = true; - sound._node.load(); + if ( + sound && + sound._node && + !(sound._node as HowlerAudioElement)._unlocked + ) { + (sound._node as HowlerAudioElement)._unlocked = true; + (sound._node as HTMLAudioElement).load(); } } } @@ -487,9 +470,9 @@ class Howler { this._autoResume(); // Create an empty buffer. - var source = this.ctx.createBufferSource(); - source.buffer = scratchBuffer; - source.connect(this.ctx.destination); + const source = (this.ctx as HowlerAudioContext).createBufferSource(); + source.buffer = this._scratchBuffer; + source.connect((this.ctx as HowlerAudioContext).destination); // Play the empty buffer. if (typeof source.start === 'undefined') { @@ -500,7 +483,7 @@ class Howler { } // Calling resume() on a stack initiated by user gesture is what actually unlocks the audio on Android Chrome >= 55. - if (typeof this.ctx.resume === 'function') { + if (this.ctx && typeof this.ctx.resume === 'function') { this.ctx.resume(); } @@ -626,7 +609,9 @@ class Howler { // Either the state gets suspended or it is interrupted. // Either way, we need to update the state to suspended. - this.ctx.suspend().then(handleSuspension, handleSuspension); + (this.ctx as HowlerAudioContext) + .suspend() + .then(handleSuspension, handleSuspension); }, 30000); return this; @@ -677,4 +662,6 @@ class Howler { } } -export default new Howler(); +const HowlerSingleton = new Howler(); + +export default HowlerSingleton; diff --git a/src/sound.ts b/src/sound.ts index 548b6a63..cb7e51d6 100644 --- a/src/sound.ts +++ b/src/sound.ts @@ -5,7 +5,7 @@ * IDEA: Maybe use ES private properties, as they can be compiled away with esbuild + TS. */ -import Howler from './howler'; +import Howler, { HowlerAudioElement } from './howler'; import Howl from './howl'; export interface HowlGainNode extends GainNode { @@ -27,12 +27,12 @@ class Sound { _sprite: string = '__default'; _id: number; - _node: HowlGainNode | HTMLAudioElement; + _node: HowlGainNode | HowlerAudioElement; _errorFn: EventListener = () => {}; _loadFn: EventListener = () => {}; _endFn: EventListener = () => {}; // TODO: Add better type when adding the spatial audio plugin. - _panner: unknown; + _panner?: AudioParam; _rateSeek?: number; _playStart: number = 0; @@ -60,7 +60,7 @@ class Sound { // Add itself to the parent's pool. this._parent._sounds.push(this); - if (this._parent._webAudio) { + if (this._parent._webAudio && Howler.ctx) { // Create the gain node for controlling volume (the source will connect to this). this._node = ( typeof Howler.ctx.createGain === 'undefined' @@ -70,7 +70,7 @@ class Sound { ) as HowlGainNode; } else { // Get an unlocked Audio object from the pool. - this._node = Howler._obtainHtml5Audio() as HTMLAudioElement; + this._node = Howler._obtainHtml5Audio() as HowlerAudioElement; } // Create the new node. @@ -85,7 +85,7 @@ class Sound { var volume = Howler._muted || this._muted || this._parent._muted ? 0 : this._volume; - if (parent._webAudio) { + if (parent._webAudio && Howler.ctx) { (this._node as HowlGainNode).gain.setValueAtTime( volume, Howler.ctx.currentTime, From 899d9b0d75a5e60fd695009b1e1494255c99e860 Mon Sep 17 00:00:00 2001 From: Samuel Plumppu <6125097+Greenheart@users.noreply.github.com> Date: Sun, 26 Sep 2021 01:21:23 +0200 Subject: [PATCH 13/19] fixes --- src/howler.ts | 2 -- src/sound.ts | 7 ------- 2 files changed, 9 deletions(-) diff --git a/src/howler.ts b/src/howler.ts index 8b78a3f3..188937bb 100644 --- a/src/howler.ts +++ b/src/howler.ts @@ -16,8 +16,6 @@ export type HowlerAudioContext = Omit & { state: AudioContextState | 'interrupted'; }; -// IDEA: Maybe use TS private properties to create clearer contexts. - class Howler { // Public properties. masterGain: GainNode | null = null; diff --git a/src/sound.ts b/src/sound.ts index cb7e51d6..39d604e3 100644 --- a/src/sound.ts +++ b/src/sound.ts @@ -1,10 +1,3 @@ -/** - * TODO: pass the howler instance reference to each sound that is created instead of using a global variable - * TODO: update the sound id generator to be common across all modules. - * - * IDEA: Maybe use ES private properties, as they can be compiled away with esbuild + TS. - */ - import Howler, { HowlerAudioElement } from './howler'; import Howl from './howl'; From 7feaaa9fd1c4f2069805bfe741f6b8f274d04bf1 Mon Sep 17 00:00:00 2001 From: Samuel Plumppu <6125097+Greenheart@users.noreply.github.com> Date: Sun, 26 Sep 2021 01:34:06 +0200 Subject: [PATCH 14/19] WIP build --- esbuild.js | 60 +++++++++++++++++++++++++++++++++++++++++ package-lock.json | 69 +++++++++++++++++++++++++++++++++++++++++++++++ package.json | 9 ++++--- src/core.ts | 4 +-- src/helpers.ts | 2 +- src/howler.ts | 2 +- 6 files changed, 139 insertions(+), 7 deletions(-) create mode 100644 esbuild.js diff --git a/esbuild.js b/esbuild.js new file mode 100644 index 00000000..3f8cf01f --- /dev/null +++ b/esbuild.js @@ -0,0 +1,60 @@ +import esbuild from 'esbuild'; +import fse from 'fs-extra'; +import { execSync } from 'child_process'; +import { resolve } from 'path'; +import { performance } from 'perf_hooks'; + +const { emptyDir, copy, readJson, writeJson } = fse; + +const startTime = performance.now(); +const pkg = await readJson('./package.json'); +const tsConfig = await readJson('./tsconfig.json'); + +console.log(`⚡ Building howler.js v${pkg.version}...`); + +const outDir = './dist'; +const distDir = resolve(outDir); + +await emptyDir(distDir); + +esbuild + .build({ + entryPoints: ['src/core.ts'], + outdir: outDir, + bundle: true, + sourcemap: false, + minify: false, + splitting: false, + format: 'esm', + target: [tsConfig.compilerOptions.target], + platform: 'browser', + external: [], + }) + .then(async () => { + // Build declaration files with TSC since they aren't built by esbuild. + execSync('npx tsc'); + + const declarationsDir = resolve(distDir, 'src'); + + // Move all declaration files to the root dist folder. Also remove unwanted files and folder. + // await remove(resolve(declarationsDir, 'cli.d.ts')); + await copy(declarationsDir, distDir); + // await remove(declarationsDir); + + await writeJson(resolve(distDir, 'package.json'), distPackage, { + spaces: 2, + }); + + const buildTime = ((performance.now() - startTime) / 1000).toLocaleString( + 'en-US', + { + minimumFractionDigits: 3, + maximumFractionDigits: 3, + }, + ); + console.log(`✅ Finished in ${buildTime} s\n`); + }) + .catch((e) => { + console.error(e); + process.exit(1); + }); diff --git a/package-lock.json b/package-lock.json index 9080a2f1..0c3442a9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7,6 +7,9 @@ "": { "version": "3.0.0@alpha.0", "license": "MIT", + "dependencies": { + "fs-extra": "^10.0.0" + }, "devDependencies": { "esbuild": "^0.13.2", "prettier": "^2.4.1", @@ -250,6 +253,19 @@ "win32" ] }, + "node_modules/fs-extra": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.0.0.tgz", + "integrity": "sha512-C5owb14u9eJwizKGdchcDUQeFtlSHHthBk8pbX9Vc1PFZrLombudjDnNns88aYslCyF6IY5SUw3Roz6xShcEIQ==", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/fsevents": { "version": "2.3.2", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", @@ -270,6 +286,11 @@ "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", "dev": true }, + "node_modules/graceful-fs": { + "version": "4.2.8", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.8.tgz", + "integrity": "sha512-qkIilPUYcNhJpd33n0GBXTB1MMPp14TxEsEs0pTrsSVucApsYzW5V+Q8Qxhik6KU3evy+qkAAowTByymK0avdg==" + }, "node_modules/has": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", @@ -294,6 +315,17 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/jsonfile": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", + "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, "node_modules/nanocolors": { "version": "0.2.3", "resolved": "https://registry.npmjs.org/nanocolors/-/nanocolors-0.2.3.tgz", @@ -398,6 +430,14 @@ "node": ">=4.2.0" } }, + "node_modules/universalify": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz", + "integrity": "sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==", + "engines": { + "node": ">= 10.0.0" + } + }, "node_modules/vite": { "version": "2.5.10", "resolved": "https://registry.npmjs.org/vite/-/vite-2.5.10.tgz", @@ -567,6 +607,16 @@ "dev": true, "optional": true }, + "fs-extra": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.0.0.tgz", + "integrity": "sha512-C5owb14u9eJwizKGdchcDUQeFtlSHHthBk8pbX9Vc1PFZrLombudjDnNns88aYslCyF6IY5SUw3Roz6xShcEIQ==", + "requires": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + } + }, "fsevents": { "version": "2.3.2", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", @@ -580,6 +630,11 @@ "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", "dev": true }, + "graceful-fs": { + "version": "4.2.8", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.8.tgz", + "integrity": "sha512-qkIilPUYcNhJpd33n0GBXTB1MMPp14TxEsEs0pTrsSVucApsYzW5V+Q8Qxhik6KU3evy+qkAAowTByymK0avdg==" + }, "has": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", @@ -598,6 +653,15 @@ "has": "^1.0.3" } }, + "jsonfile": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", + "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", + "requires": { + "graceful-fs": "^4.1.6", + "universalify": "^2.0.0" + } + }, "nanocolors": { "version": "0.2.3", "resolved": "https://registry.npmjs.org/nanocolors/-/nanocolors-0.2.3.tgz", @@ -664,6 +728,11 @@ "integrity": "sha512-4xfscpisVgqqDfPaJo5vkd+Qd/ItkoagnHpufr+i2QCHBsNYp+G7UAoyFl8aPtx879u38wPV65rZ8qbGZijalA==", "dev": true }, + "universalify": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz", + "integrity": "sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==" + }, "vite": { "version": "2.5.10", "resolved": "https://registry.npmjs.org/vite/-/vite-2.5.10.tgz", diff --git a/package.json b/package.json index 47df509e..50b3d9f1 100644 --- a/package.json +++ b/package.json @@ -24,7 +24,7 @@ "scripts": { "test": "vite", "test:network": "vite --host", - "build": "VERSION=`printf 'v' && node -e 'console.log(require(\"./package.json\").version)'` && sed -i '' '2s/.*/ * howler.js '\"$VERSION\"'/' src/howler.core.js && sed -i '' '4s/.*/ * howler.js '\"$VERSION\"'/' src/plugins/howler.spatial.js && uglifyjs --preamble \"/*! howler.js $VERSION | (c) 2013-2020, James Simpson of GoldFire Studios | MIT License | howlerjs.com */\" src/howler.core.js -c -m --screw-ie8 -o dist/howler.core.min.js && uglifyjs --preamble \"/*! howler.js $VERSION | Spatial Plugin | (c) 2013-2020, James Simpson of GoldFire Studios | MIT License | howlerjs.com */\" src/plugins/howler.spatial.js -c -m --screw-ie8 -o dist/howler.spatial.min.js && awk 'FNR==1{echo \"\"}1' dist/howler.core.min.js dist/howler.spatial.min.js | sed '3s~.*~/*! Spatial Plugin */~' | perl -pe 'chomp if eof' > dist/howler.min.js && awk '(NR>1 && FNR==1){printf (\"\\n\\n\")};1' src/howler.core.js src/plugins/howler.spatial.js > dist/howler.js", + "build": "node esbuild.js", "release": "VERSION=`printf 'v' && node -e 'console.log(require(\"./package.json\").version)'` && git tag $VERSION && git push && git push origin $VERSION && npm publish" }, "devDependencies": { @@ -33,14 +33,17 @@ "typescript": "^4.4.3", "vite": "^2.5.10" }, + "type": "module", "main": "dist/howler.js", "license": "MIT", "files": [ - "src", "dist/howler.js", "dist/howler.min.js", "dist/howler.core.min.js", "dist/howler.spatial.min.js", "LICENSE.md" - ] + ], + "dependencies": { + "fs-extra": "^10.0.0" + } } diff --git a/src/core.ts b/src/core.ts index 34aa53d3..ae91fddc 100644 --- a/src/core.ts +++ b/src/core.ts @@ -7,7 +7,7 @@ * * MIT License */ -import Howler from './Howler'; -import Howl from './Howl'; +import Howler from './howler'; +import Howl from './howl'; export { Howler, Howl }; diff --git a/src/helpers.ts b/src/helpers.ts index 709aa41f..af388f8b 100644 --- a/src/helpers.ts +++ b/src/helpers.ts @@ -1,4 +1,4 @@ -import Howl, { HowlXHROptions } from './Howl'; +import Howl, { HowlXHROptions } from './howl'; import Howler, { HowlerAudioContext } from './howler'; export const cache = {}; diff --git a/src/howler.ts b/src/howler.ts index 188937bb..b32ed07a 100644 --- a/src/howler.ts +++ b/src/howler.ts @@ -1,4 +1,4 @@ -import Howl from './Howl'; +import Howl from './howl'; export interface HowlerAudioElement extends HTMLAudioElement { _unlocked: boolean; From 2e3e97def3cc103ffd565ab4c4cba4e7b67de18b Mon Sep 17 00:00:00 2001 From: Samuel Plumppu <6125097+Greenheart@users.noreply.github.com> Date: Sun, 26 Sep 2021 01:36:52 +0200 Subject: [PATCH 15/19] improvements --- src/howler.ts | 18 ++++++------------ 1 file changed, 6 insertions(+), 12 deletions(-) diff --git a/src/howler.ts b/src/howler.ts index b32ed07a..9ad2ecd2 100644 --- a/src/howler.ts +++ b/src/howler.ts @@ -61,11 +61,11 @@ class Howler { /** * 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. + * @param vol Volume from 0.0 to 1.0. + * @return Returns self or current volume. */ - volume(vol?: string) { - const volume = typeof vol === 'string' ? parseFloat(vol) : undefined; + volume(vol?: number | string) { + const volume = parseFloat(vol as string); // If we don't have an AudioContext created yet, run the setup. if (!this.ctx) { @@ -164,7 +164,6 @@ class Howler { /** * Unload and destroy all currently loaded Howl objects. - * @return {Howler} */ unload() { for (var i = this._howls.length - 1; i >= 0; i--) { @@ -187,7 +186,7 @@ class Howler { /** * Check for codec support of specific extension. - * @param {String} ext Audio file extention. + * @param ext Audio file extention. */ codecs(ext: string) { return this._codecs[ext.replace(/^x-/, '')]; @@ -195,7 +194,6 @@ class Howler { /** * Setup various state values for global tracking. - * @return {Howler} */ _setup() { // Keeps track of the suspend/resume state of the AudioContext. @@ -306,7 +304,6 @@ class Howler { /** * Check for browser support for various codecs and cache the results. - * @return {Howler} */ _setupCodecs() { let audioTest: HTMLAudioElement | null = null; @@ -517,7 +514,7 @@ class Howler { /** * Get an unlocked HTML5 Audio object from the pool. If none are left, * return a new Audio object and throw a warning. - * @return {Audio} HTML5 Audio object. + * @return HTML5 Audio object. */ _obtainHtml5Audio() { // Return the next object from the pool if one exists. @@ -545,7 +542,6 @@ class Howler { /** * Return an activated HTML5 Audio object to the pool. - * @return {Howler} */ _releaseHtml5Audio(audio: HowlerAudioElement) { // Don't add audio to the pool if we don't know if it has been unlocked. @@ -559,7 +555,6 @@ class Howler { /** * Automatically suspend the Web Audio AudioContext after no sound has played for 30 seconds. * This saves processing/energy and fixes various browser-specific bugs with audio getting stuck. - * @return {Howler} */ _autoSuspend() { if ( @@ -617,7 +612,6 @@ class Howler { /** * Automatically resume the Web Audio AudioContext when a new sound is played. - * @return {Howler} */ _autoResume() { if ( From 450e7986a05aa4fe9070d5b779b6cc359131f03f Mon Sep 17 00:00:00 2001 From: Samuel Plumppu <6125097+Greenheart@users.noreply.github.com> Date: Sun, 26 Sep 2021 01:49:22 +0200 Subject: [PATCH 16/19] improve build --- esbuild.js | 13 +------------ src/howl.ts | 4 ++-- 2 files changed, 3 insertions(+), 14 deletions(-) diff --git a/esbuild.js b/esbuild.js index 3f8cf01f..1314ca12 100644 --- a/esbuild.js +++ b/esbuild.js @@ -4,7 +4,7 @@ import { execSync } from 'child_process'; import { resolve } from 'path'; import { performance } from 'perf_hooks'; -const { emptyDir, copy, readJson, writeJson } = fse; +const { emptyDir, readJson } = fse; const startTime = performance.now(); const pkg = await readJson('./package.json'); @@ -34,17 +34,6 @@ esbuild // Build declaration files with TSC since they aren't built by esbuild. execSync('npx tsc'); - const declarationsDir = resolve(distDir, 'src'); - - // Move all declaration files to the root dist folder. Also remove unwanted files and folder. - // await remove(resolve(declarationsDir, 'cli.d.ts')); - await copy(declarationsDir, distDir); - // await remove(declarationsDir); - - await writeJson(resolve(distDir, 'package.json'), distPackage, { - spaces: 2, - }); - const buildTime = ((performance.now() - startTime) / 1000).toLocaleString( 'en-US', { diff --git a/src/howl.ts b/src/howl.ts index ad2837ac..4cc631cc 100644 --- a/src/howl.ts +++ b/src/howl.ts @@ -1633,8 +1633,8 @@ class Howl { // Clear out `this`. this._state = 'unloaded'; this._sounds = []; - // @ts-expect-error Temporarily ignore strict type checking to allow dynamic JS - this = null; + // NOTE: This is operation is not allowed in modern TS + JS. Don't know how to replace it though. + // this = null; return null; } From f378dfdee1f4a1ac52ac6ef0d6f95edc7ce8440c Mon Sep 17 00:00:00 2001 From: Samuel Plumppu <6125097+Greenheart@users.noreply.github.com> Date: Sun, 26 Sep 2021 01:57:17 +0200 Subject: [PATCH 17/19] Start updating tests --- tests/core.webaudio.html | 29 ++++---- tests/index.html | 50 ++++++------- tests/js/core.webaudio.js | 143 +++++++++++++++++++++----------------- 3 files changed, 119 insertions(+), 103 deletions(-) diff --git a/tests/core.webaudio.html b/tests/core.webaudio.html index 7c16bae6..c6c6d258 100644 --- a/tests/core.webaudio.html +++ b/tests/core.webaudio.html @@ -1,16 +1,15 @@ - + - - - Howler.js Core Web Audio Tests - - - -
-
- -
- - - - \ No newline at end of file + + + Howler.js Core Web Audio Tests + + + +
+
+ +
+ + + diff --git a/tests/index.html b/tests/index.html index a30b0201..f47a8225 100644 --- a/tests/index.html +++ b/tests/index.html @@ -1,29 +1,29 @@ - + - - - Howler.js Tests - - - -
- - - -
+ + + Howler.js Tests + + + +
+ + + +
- - - \ No newline at end of file + document.getElementById('spatial').onclick = function () { + window.location = 'spatial.html'; + }; + + + diff --git a/tests/js/core.webaudio.js b/tests/js/core.webaudio.js index f6690310..ae9388ee 100644 --- a/tests/js/core.webaudio.js +++ b/tests/js/core.webaudio.js @@ -1,13 +1,15 @@ +import { Howl, Howler } from '../../dist/core.js'; + // Cache the label for later use. -var label = document.getElementById('label'); -var start = document.getElementById('start'); +const label = document.getElementById('label'); +const start = document.getElementById('start'); // Setup the sounds to be used. -var sound1 = new Howl({ - src: ['audio/sound1.webm', 'audio/sound1.mp3'] +const sound1 = new Howl({ + src: ['audio/sound1.webm', 'audio/sound1.mp3'], }); -var sound2 = new Howl({ +const sound2 = new Howl({ src: ['audio/sound2.webm', 'audio/sound2.mp3'], sprite: { one: [0, 450], @@ -15,237 +17,248 @@ var sound2 = new Howl({ three: [4000, 350], four: [6000, 380], five: [8000, 340], - beat: [10000, 11163] - } + beat: [10000, 11163], + }, }); // Enable the start button when the sounds have loaded. -sound1.once('load', function() { +sound1.once('load', () => { start.removeAttribute('disabled'); start.innerHTML = 'BEGIN CORE TESTS'; }); // Define the tests to run. -var id; -var tests = [ - function(fn) { - sound1.once('play', function() { +let id; +const tests = [ + (fn) => { + sound1.once('play', () => { label.innerHTML = 'PLAYING'; setTimeout(fn, 2000); }); - + id = sound1.play(); }, - function(fn) { + (fn) => { sound1.pause(id); label.innerHTML = 'PAUSED'; setTimeout(fn, 1500); }, - function(fn) { + (fn) => { sound1.play(id); label.innerHTML = 'RESUMING'; setTimeout(fn, 2000); }, - function(fn) { + (fn) => { sound1.stop(id); label.innerHTML = 'STOPPED'; setTimeout(fn, 1500); }, - function(fn) { + (fn) => { sound1.play(id); label.innerHTML = 'PLAY FROM START'; setTimeout(fn, 2000); }, - function(fn) { + (fn) => { sound1.rate(1.5, id); label.innerHTML = 'SPEED UP'; setTimeout(fn, 2000); }, - function(fn) { + (fn) => { sound1.rate(1, id); label.innerHTML = 'SLOW DOWN'; setTimeout(fn, 2000); }, - function(fn) { + (fn) => { sound1.fade(1, 0, 2000, id); label.innerHTML = 'FADE OUT'; - sound1.once('fade', function() { - fn(); - }, id); + sound1.once( + 'fade', + () => { + fn(); + }, + id, + ); }, - function(fn) { + (fn) => { sound1.fade(0, 1, 2000, id); label.innerHTML = 'FADE IN'; - sound1.once('fade', function() { - fn(); - }, id); + sound1.once( + 'fade', + () => { + fn(); + }, + id, + ); }, - function(fn) { + (fn) => { sound1.mute(true, id); label.innerHTML = 'MUTE'; setTimeout(fn, 1500); }, - function(fn) { + (fn) => { sound1.mute(false, id); label.innerHTML = 'UNMUTE'; setTimeout(fn, 2000); }, - function(fn) { + (fn) => { sound1.volume(0.5, id); label.innerHTML = 'HALF VOLUME'; setTimeout(fn, 2000); }, - function(fn) { + (fn) => { sound1.volume(1, id); label.innerHTML = 'FULL VOLUME'; setTimeout(fn, 2000); }, - function(fn) { + (fn) => { sound1.seek(0, id); label.innerHTML = 'SEEK TO START'; setTimeout(fn, 2000); }, - function(fn) { + (fn) => { id = sound1.play(); label.innerHTML = 'PLAY 2ND'; setTimeout(fn, 2000); }, - function(fn) { + (fn) => { sound1.mute(true); label.innerHTML = 'MUTE GROUP'; setTimeout(fn, 1500); }, - function(fn) { + (fn) => { sound1.mute(false); label.innerHTML = 'UNMUTE GROUP'; setTimeout(fn, 2000); }, - function(fn) { + (fn) => { sound1.volume(0.5); label.innerHTML = 'HALF VOLUME GROUP'; setTimeout(fn, 2000); }, - function(fn) { + (fn) => { sound1.fade(0.5, 0, 2000); label.innerHTML = 'FADE OUT GROUP'; - sound1.once('fade', function() { + sound1.once('fade', () => { if (sound1._onfade.length === 0) { fn(); } }); }, - function(fn) { + (fn) => { sound1.fade(0, 1, 2000); label.innerHTML = 'FADE IN GROUP'; - sound1.once('fade', function() { + sound1.once('fade', () => { if (sound1._onfade.length === 0) { fn(); } }); }, - function(fn) { + (fn) => { sound1.stop(); label.innerHTML = 'STOP GROUP'; setTimeout(fn, 1500); }, - function(fn) { + (fn) => { id = sound2.play('beat'); label.innerHTML = 'PLAY SPRITE'; setTimeout(fn, 2000); }, - function(fn) { + (fn) => { sound2.pause(id); label.innerHTML = 'PAUSE SPRITE'; setTimeout(fn, 1000); }, - function(fn) { + (fn) => { sound2.play(id); label.innerHTML = 'RESUME SPRITE'; setTimeout(fn, 1500); }, - function(fn) { - var sounds = ['one', 'two', 'three', 'four', 'five']; - for (var i=0; i { + const sounds = ['one', 'two', 'three', 'four', 'five']; + for (let i = 0; i < sounds.length; i++) { + setTimeout( + ((i) => { + sound2.play(sounds[i]); + }).bind(null, i), + i * 500, + ); } label.innerHTML = 'MULTIPLE SPRITES'; setTimeout(fn, 3000); }, - function(fn) { + (fn) => { var sprite = sound2.play('one'); sound2.loop(true, sprite); label.innerHTML = 'LOOP SPRITE'; - setTimeout(function() { + setTimeout(() => { sound2.loop(false, sprite); fn(); }, 3000); }, - function(fn) { + (fn) => { sound2.fade(1, 0, 2000, id); label.innerHTML = 'FADE OUT SPRITE'; - sound2.once('fade', function() { + sound2.once('fade', () => { fn(); }); - } + }, ]; // Create a method that will call the next in the series. -var chain = function(i) { - return function() { +const chain = (i) => { + return () => { if (tests[i]) { tests[i](chain(++i)); } else { @@ -253,7 +266,7 @@ var chain = function(i) { label.style.color = '#74b074'; // Wait for 5 seconds and then go back to the tests index. - setTimeout(function() { + setTimeout(() => { window.location = './'; }, 5000); } @@ -263,10 +276,14 @@ var chain = function(i) { // If Web Audio isn't available, send them to hTML5 test. if (Howler.usingWebAudio) { // Listen to a click on the button to being the tests. - start.addEventListener('click', function() { - tests[0](chain(1)); - start.style.display = 'none'; - }, false); + start.addEventListener( + 'click', + () => { + tests[0](chain(1)); + start.style.display = 'none'; + }, + false, + ); } else { window.location = 'core.html5audio.html'; -} \ No newline at end of file +} From b5a1c3c0eb62910e8fec3c336a81999b3a7761b8 Mon Sep 17 00:00:00 2001 From: Samuel Plumppu <6125097+Greenheart@users.noreply.github.com> Date: Sun, 26 Sep 2021 02:06:37 +0200 Subject: [PATCH 18/19] YES! Literally the second time running the test suite, it completed all tests! TypeScript makes large refactors like this one so much easier! --- esbuild.js | 2 ++ src/howler.ts | 14 ++++++++------ 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/esbuild.js b/esbuild.js index 1314ca12..9a99322c 100644 --- a/esbuild.js +++ b/esbuild.js @@ -17,6 +17,8 @@ const distDir = resolve(outDir); await emptyDir(distDir); +// TODO: setup watch mode script for development + esbuild .build({ entryPoints: ['src/core.ts'], diff --git a/src/howler.ts b/src/howler.ts index 9ad2ecd2..c9c6fa67 100644 --- a/src/howler.ts +++ b/src/howler.ts @@ -285,17 +285,19 @@ class Howler { } // Create and expose the master GainNode when using Web Audio (useful for plugins or advanced usage). - if (this.usingWebAudio && this.masterGain && this.ctx) { + if (this.usingWebAudio) { this.masterGain = - typeof this.ctx.createGain === 'undefined' + typeof (this.ctx as HowlerAudioContext).createGain === 'undefined' ? // @ts-expect-error Support old browsers - this.ctx.createGainNode() - : this.ctx.createGain(); + (this.ctx as HowlerAudioContext).createGainNode() + : (this.ctx as HowlerAudioContext).createGain(); (this.masterGain as GainNode).gain.setValueAtTime( this._muted ? 0 : this._volume, - this.ctx.currentTime, + (this.ctx as HowlerAudioContext).currentTime, + ); + (this.masterGain as GainNode).connect( + (this.ctx as HowlerAudioContext).destination, ); - (this.masterGain as GainNode).connect(this.ctx.destination); } // Re-run the setup on Howler. From c7db5b2d7d453f9743a5b77a3c976b227a7ebea0 Mon Sep 17 00:00:00 2001 From: Samuel Plumppu <6125097+Greenheart@users.noreply.github.com> Date: Sun, 26 Sep 2021 02:17:13 +0200 Subject: [PATCH 19/19] Update tests - Tests are fully functional for both HTML5 and WebAudio! Needs thorough testing for other browsers, but this is a promising start! --- dist/core.d.ts | 12 + dist/core.js | 1526 +++++++++++++++++ dist/helpers.d.ts | 9 + dist/howl.d.ts | 401 +++++ dist/howler.core.min.js | 2 - dist/howler.d.ts | 99 ++ dist/howler.js | 3242 ----------------------------------- dist/howler.min.js | 4 - dist/howler.spatial.min.js | 2 - dist/sound.d.ts | 57 + esbuild.js | 5 + tests/core.html5audio.html | 31 +- tests/js/core.html5audio.js | 123 +- tests/js/core.webaudio.js | 2 +- 14 files changed, 2191 insertions(+), 3324 deletions(-) create mode 100644 dist/core.d.ts create mode 100644 dist/core.js create mode 100644 dist/helpers.d.ts create mode 100644 dist/howl.d.ts delete mode 100644 dist/howler.core.min.js create mode 100644 dist/howler.d.ts delete mode 100644 dist/howler.js delete mode 100644 dist/howler.min.js delete mode 100644 dist/howler.spatial.min.js create mode 100644 dist/sound.d.ts diff --git a/dist/core.d.ts b/dist/core.d.ts new file mode 100644 index 00000000..e7d2342b --- /dev/null +++ b/dist/core.d.ts @@ -0,0 +1,12 @@ +/*! + * howler.js v2.2.3 + * howlerjs.com + * + * (c) 2013-2020, James Simpson of GoldFire Studios + * goldfirestudios.com + * + * MIT License + */ +import Howler from './howler'; +import Howl from './howl'; +export { Howler, Howl }; diff --git a/dist/core.js b/dist/core.js new file mode 100644 index 00000000..d8bd4cb8 --- /dev/null +++ b/dist/core.js @@ -0,0 +1,1526 @@ +// src/howler.ts +var Howler = class { + constructor() { + this.masterGain = null; + this.noAudio = false; + this.usingWebAudio = true; + this.autoSuspend = true; + this.ctx = null; + this.autoUnlock = true; + this._counter = 1e3; + this._html5AudioPool = []; + this.html5PoolSize = 10; + this._codecs = {}; + this._howls = []; + this._muted = false; + this._volume = 1; + this._canPlayEvent = "canplaythrough"; + this._navigator = window.navigator; + this._audioUnlocked = false; + this._mobileUnloaded = false; + this.state = "suspended"; + this._suspendTimer = null; + this._setup(); + } + volume(vol) { + const volume = parseFloat(vol); + if (!this.ctx) { + this._setupAudioContext(); + } + if (typeof volume !== "undefined" && volume >= 0 && volume <= 1) { + this._volume = volume; + if (this._muted) { + return this; + } + if (this.usingWebAudio && this.masterGain && this.ctx) { + this.masterGain.gain.setValueAtTime(volume, this.ctx.currentTime); + } + for (var i = 0; i < this._howls.length; i++) { + if (!this._howls[i]._webAudio) { + var ids = this._howls[i]._getSoundIds(); + for (var j = 0; j < ids.length; j++) { + var sound = this._howls[i]._soundById(ids[j]); + if (sound && sound._node) { + sound._node.volume = sound._volume * volume; + } + } + } + } + return volume; + } + return this._volume; + } + mute(muted) { + if (!this.ctx) { + this._setupAudioContext(); + } + this._muted = muted; + if (this.usingWebAudio && this.masterGain && this.ctx) { + this.masterGain.gain.setValueAtTime(muted ? 0 : this._volume, this.ctx.currentTime); + } + for (var i = 0; i < this._howls.length; i++) { + if (!this._howls[i]._webAudio) { + var ids = this._howls[i]._getSoundIds(); + for (var j = 0; j < ids.length; j++) { + var sound = this._howls[i]._soundById(ids[j]); + if (sound && sound._node) { + sound._node.muted = muted ? true : sound._muted; + } + } + } + } + return this; + } + stop() { + for (var i = 0; i < this._howls.length; i++) { + this._howls[i].stop(); + } + return this; + } + unload() { + for (var i = this._howls.length - 1; i >= 0; i--) { + this._howls[i].unload(); + } + if (this.usingWebAudio && this.ctx && typeof this.ctx.close !== "undefined") { + this.ctx.close(); + this.ctx = null; + this._setupAudioContext(); + } + return this; + } + codecs(ext) { + return this._codecs[ext.replace(/^x-/, "")]; + } + _setup() { + this.state = this.ctx ? this.ctx.state || "suspended" : "suspended"; + this._autoSuspend(); + if (!this.usingWebAudio) { + if (typeof Audio !== "undefined") { + try { + var test = new Audio(); + if (typeof test.oncanplaythrough === "undefined") { + this._canPlayEvent = "canplay"; + } + } catch (e) { + this.noAudio = true; + } + } else { + this.noAudio = true; + } + } + try { + var test = new Audio(); + if (test.muted) { + this.noAudio = true; + } + } catch (e) { + } + if (!this.noAudio) { + this._setupCodecs(); + } + return this; + } + _setupAudioContext() { + if (!this.usingWebAudio) { + return; + } + try { + if (typeof AudioContext !== "undefined") { + this.ctx = new AudioContext(); + } else if (typeof webkitAudioContext !== "undefined") { + this.ctx = new webkitAudioContext(); + } else { + this.usingWebAudio = false; + } + } catch (e) { + this.usingWebAudio = false; + } + if (!this.ctx) { + this.usingWebAudio = false; + } + var iOS = /iP(hone|od|ad)/.test(this._navigator && this._navigator.platform); + var appVersion = this._navigator && this._navigator.appVersion.match(/OS (\d+)_(\d+)_?(\d+)?/); + var version = appVersion ? parseInt(appVersion[1], 10) : null; + if (iOS && version && version < 9) { + var safari = /safari/.test(this._navigator && this._navigator.userAgent.toLowerCase()); + if (this._navigator && !safari) { + this.usingWebAudio = false; + } + } + if (this.usingWebAudio) { + this.masterGain = typeof this.ctx.createGain === "undefined" ? this.ctx.createGainNode() : this.ctx.createGain(); + this.masterGain.gain.setValueAtTime(this._muted ? 0 : this._volume, this.ctx.currentTime); + this.masterGain.connect(this.ctx.destination); + } + this._setup(); + } + _setupCodecs() { + let audioTest = null; + try { + audioTest = typeof Audio !== "undefined" ? new Audio() : null; + } catch (err) { + return this; + } + if (!audioTest || typeof audioTest.canPlayType !== "function") { + return this; + } + const mpegTest = audioTest.canPlayType("audio/mpeg;").replace(/^no$/, ""); + const ua = this._navigator ? this._navigator.userAgent : ""; + const checkOpera = ua.match(/OPR\/([0-6].)/g); + const isOldOpera = checkOpera && parseInt(checkOpera[0].split("/")[1], 10) < 33; + const checkSafari = ua.indexOf("Safari") !== -1 && ua.indexOf("Chrome") === -1; + const safariVersion = ua.match(/Version\/(.*?) /); + const isOldSafari = checkSafari && safariVersion && parseInt(safariVersion[1], 10) < 15; + this._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 this; + } + _unlockAudio() { + if (this._audioUnlocked || !this.ctx) { + return this; + } + this.autoUnlock = false; + if (!this._mobileUnloaded && this.ctx.sampleRate !== 44100) { + this._mobileUnloaded = true; + this.unload(); + } + this._scratchBuffer = this.ctx.createBuffer(1, 1, 22050); + const unlock = () => { + while (this._html5AudioPool.length < this.html5PoolSize) { + try { + var audioNode = new Audio(); + audioNode._unlocked = true; + this._releaseHtml5Audio(audioNode); + } catch (e) { + this.noAudio = true; + break; + } + } + for (var i = 0; i < this._howls.length; i++) { + if (!this._howls[i]._webAudio) { + var ids = this._howls[i]._getSoundIds(); + for (var j = 0; j < ids.length; j++) { + var sound = this._howls[i]._soundById(ids[j]); + if (sound && sound._node && !sound._node._unlocked) { + sound._node._unlocked = true; + sound._node.load(); + } + } + } + } + this._autoResume(); + const source = this.ctx.createBufferSource(); + source.buffer = this._scratchBuffer; + source.connect(this.ctx.destination); + if (typeof source.start === "undefined") { + source.noteOn(0); + } else { + source.start(0); + } + if (this.ctx && typeof this.ctx.resume === "function") { + this.ctx.resume(); + } + source.onended = () => { + source.disconnect(0); + this._audioUnlocked = true; + document.removeEventListener("touchstart", unlock, true); + document.removeEventListener("touchend", unlock, true); + document.removeEventListener("click", unlock, true); + document.removeEventListener("keydown", unlock, true); + for (var i2 = 0; i2 < this._howls.length; i2++) { + this._howls[i2]._emit("unlock"); + } + }; + }; + document.addEventListener("touchstart", unlock, true); + document.addEventListener("touchend", unlock, true); + document.addEventListener("click", unlock, true); + document.addEventListener("keydown", unlock, true); + return this; + } + _obtainHtml5Audio() { + if (this._html5AudioPool.length) { + return this._html5AudioPool.pop(); + } + var testPlay = new Audio().play(); + if (testPlay && typeof Promise !== "undefined" && (testPlay instanceof Promise || typeof testPlay.then === "function")) { + testPlay.catch(function() { + console.warn("HTML5 Audio pool exhausted, returning potentially locked audio object."); + }); + } + return new Audio(); + } + _releaseHtml5Audio(audio) { + if (audio._unlocked) { + this._html5AudioPool.push(audio); + } + return this; + } + _autoSuspend() { + if (!this.autoSuspend || !this.ctx || typeof this.ctx.suspend === "undefined" || !this.usingWebAudio) { + return; + } + for (var i = 0; i < this._howls.length; i++) { + if (this._howls[i]._webAudio) { + for (var j = 0; j < this._howls[i]._sounds.length; j++) { + if (!this._howls[i]._sounds[j]._paused) { + return this; + } + } + } + } + if (this._suspendTimer) { + clearTimeout(this._suspendTimer); + } + this._suspendTimer = setTimeout(() => { + if (!this.autoSuspend) { + return; + } + this._suspendTimer = null; + this.state = "suspending"; + const handleSuspension = () => { + this.state = "suspended"; + if (this._resumeAfterSuspend) { + delete this._resumeAfterSuspend; + this._autoResume(); + } + }; + this.ctx.suspend().then(handleSuspension, handleSuspension); + }, 3e4); + return this; + } + _autoResume() { + if (!this.ctx || typeof this.ctx.resume === "undefined" || !this.usingWebAudio) { + return; + } + if (this.state === "running" && this.ctx.state !== "interrupted" && this._suspendTimer) { + clearTimeout(this._suspendTimer); + this._suspendTimer = null; + } else if (this.state === "suspended" || this.state === "running" && this.ctx.state === "interrupted") { + this.ctx.resume().then(() => { + this.state = "running"; + for (var i = 0; i < this._howls.length; i++) { + this._howls[i]._emit("resume"); + } + }); + if (this._suspendTimer) { + clearTimeout(this._suspendTimer); + this._suspendTimer = null; + } + } else if (this.state === "suspending") { + this._resumeAfterSuspend = true; + } + return this; + } +}; +var HowlerSingleton = new Howler(); +var howler_default = HowlerSingleton; + +// src/helpers.ts +var cache = {}; +function loadBuffer(self) { + var url = self._src; + if (cache[url]) { + self._duration = cache[url].duration; + loadSound(self); + return; + } + if (/^data:[^;]+;base64,/.test(url)) { + var data = atob(url.split(",")[1]); + var dataView = new Uint8Array(data.length); + for (var i = 0; i < data.length; ++i) { + dataView[i] = data.charCodeAt(i); + } + decodeAudioData(dataView.buffer, self); + } else { + var xhr = new XMLHttpRequest(); + xhr.open(self._xhr.method, url, true); + xhr.withCredentials = self._xhr.withCredentials; + xhr.responseType = "arraybuffer"; + if (self._xhr) { + Object.keys(self._xhr).forEach(function(key) { + xhr.setRequestHeader(key, self._xhr[key]); + }); + } + xhr.onload = () => { + var code = (xhr.status + "")[0]; + if (code !== "0" && code !== "2" && code !== "3") { + self._emit("loaderror", null, "Failed loading audio file with status: " + xhr.status + "."); + return; + } + decodeAudioData(xhr.response, self); + }; + xhr.onerror = () => { + if (self._webAudio) { + self._html5 = true; + self._webAudio = false; + self._sounds = []; + delete cache[url]; + self.load(); + } + }; + safeXhrSend(xhr); + } +} +function safeXhrSend(xhr) { + try { + xhr.send(); + } catch (e) { + console.error("XHR Request failed: ", e); + } +} +function decodeAudioData(arraybuffer, self) { + function error() { + self._emit("loaderror", null, "Decoding audio data failed."); + } + function success(buffer) { + if (buffer && self._sounds.length > 0) { + cache[self._src] = buffer; + loadSound(self, buffer); + } else { + error(); + } + } + if (typeof Promise !== "undefined" && howler_default.ctx.decodeAudioData.length === 1) { + howler_default.ctx.decodeAudioData(arraybuffer).then(success).catch(error); + } else { + howler_default.ctx.decodeAudioData(arraybuffer, success, error); + } +} +function loadSound(self, buffer) { + if (buffer && !self._duration) { + self._duration = buffer.duration; + } + if (Object.keys(self._sprite).length === 0) { + self._sprite = { __default: [0, self._duration * 1e3] }; + } + if (self._state !== "loaded") { + self._state = "loaded"; + self._emit("load"); + self._loadQueue(); + } +} + +// src/sound.ts +var Sound = class { + constructor(howl) { + this._seek = 0; + this._paused = true; + this._ended = true; + this._sprite = "__default"; + this._errorFn = () => { + }; + this._loadFn = () => { + }; + this._endFn = () => { + }; + this._playStart = 0; + this._start = 0; + this._stop = 0; + this._fadeTo = null; + this._interval = null; + this._parent = howl; + this._muted = Boolean(howl._muted); + this._loop = Boolean(howl._loop); + this._volume = howl._volume; + this._rate = howl._rate; + this._id = ++howler_default._counter; + this._parent._sounds.push(this); + if (this._parent._webAudio && howler_default.ctx) { + this._node = typeof howler_default.ctx.createGain === "undefined" ? howler_default.ctx.createGainNode() : howler_default.ctx.createGain(); + } else { + this._node = howler_default._obtainHtml5Audio(); + } + this.create(); + } + create() { + var parent = this._parent; + var volume = howler_default._muted || this._muted || this._parent._muted ? 0 : this._volume; + if (parent._webAudio && howler_default.ctx) { + this._node.gain.setValueAtTime(volume, howler_default.ctx.currentTime); + this._node.paused = true; + this._node.connect(howler_default.masterGain); + } else if (!howler_default.noAudio) { + this._errorFn = this._errorListener.bind(this); + this._node.addEventListener("error", this._errorFn, false); + this._loadFn = this._loadListener.bind(this); + this._node.addEventListener(howler_default._canPlayEvent, this._loadFn, false); + this._endFn = this._endListener.bind(this); + this._node.addEventListener("ended", this._endFn, false); + this._node.src = parent._src; + this._node.preload = parent._preload === true ? "auto" : parent._preload; + this._node.volume = volume * howler_default.volume(); + this._node.load(); + } + return this; + } + reset() { + var parent = this._parent; + this._muted = parent._muted; + this._loop = parent._loop; + this._volume = parent._volume; + this._rate = parent._rate; + this._seek = 0; + this._rateSeek = 0; + this._paused = true; + this._ended = true; + this._sprite = "__default"; + this._id = ++howler_default._counter; + return this; + } + _errorListener() { + this._parent._emit("loaderror", this._id, this._node.error instanceof MediaError ? this._node.error.code : 0); + this._node.removeEventListener("error", this._errorFn, false); + } + _loadListener() { + const parent = this._parent; + parent._duration = Math.ceil(this._node.duration * 10) / 10; + if (Object.keys(parent._sprite).length === 0) { + parent._sprite = { __default: [0, parent._duration * 1e3] }; + } + if (parent._state !== "loaded") { + parent._state = "loaded"; + parent._emit("load"); + parent._loadQueue(); + } + this._node.removeEventListener(howler_default._canPlayEvent, this._loadFn, false); + } + _endListener() { + const parent = this._parent; + if (parent._duration === Infinity) { + parent._duration = Math.ceil(this._node.duration * 10) / 10; + if (parent._sprite.__default[1] === Infinity) { + parent._sprite.__default[1] = parent._duration * 1e3; + } + parent._ended(this); + } + this._node.removeEventListener("ended", this._endFn, false); + } +}; +var sound_default = Sound; + +// src/howl.ts +var Howl = class { + constructor(o) { + this._autoplay = false; + this._format = []; + this._html5 = false; + this._muted = false; + this._loop = false; + this._pool = 5; + this._preload = true; + this._rate = 1; + this._sprite = {}; + this._src = []; + this._volume = 1; + this._duration = 0; + this._state = "unloaded"; + this._sounds = []; + this._endTimers = {}; + this._queue = []; + this._playLock = false; + this._onend = []; + this._onfade = []; + this._onload = []; + this._onloaderror = []; + this._onplayerror = []; + this._onpause = []; + this._onplay = []; + this._onstop = []; + this._onmute = []; + this._onvolume = []; + this._onrate = []; + this._onseek = []; + this._onunlock = []; + this._onresume = []; + if (!o.src || o.src.length === 0) { + console.error("An array of source files must be passed with any new Howl."); + return; + } + if (!howler_default.ctx) { + howler_default._setupAudioContext(); + } + this._format = o.format === void 0 ? [] : typeof o.format !== "string" ? o.format : [o.format]; + this._html5 = o.html5 || false; + this._muted = o.mute || false; + this._loop = o.loop || false; + this._pool = o.pool || 5; + this._preload = typeof o.preload === "boolean" || o.preload === "metadata" ? o.preload : true; + this._rate = o.rate || 1; + this._sprite = o.sprite || {}; + this._src = typeof o.src !== "string" ? o.src : [o.src]; + this._volume = o.volume !== void 0 ? o.volume : 1; + this._xhr = { + method: o.xhr && o.xhr.method ? o.xhr.method : "GET", + headers: o.xhr && o.xhr.headers ? o.xhr.headers : void 0, + withCredentials: o.xhr && o.xhr.withCredentials ? o.xhr.withCredentials : false + }; + this._onend = o.onend ? [{ fn: o.onend }] : []; + this._onfade = o.onfade ? [{ fn: o.onfade }] : []; + this._onload = o.onload ? [{ fn: o.onload }] : []; + this._onloaderror = o.onloaderror ? [{ fn: o.onloaderror }] : []; + this._onplayerror = o.onplayerror ? [{ fn: o.onplayerror }] : []; + this._onpause = o.onpause ? [{ fn: o.onpause }] : []; + this._onplay = o.onplay ? [{ fn: o.onplay }] : []; + this._onstop = o.onstop ? [{ fn: o.onstop }] : []; + this._onmute = o.onmute ? [{ fn: o.onmute }] : []; + this._onvolume = o.onvolume ? [{ fn: o.onvolume }] : []; + this._onrate = o.onrate ? [{ fn: o.onrate }] : []; + this._onseek = o.onseek ? [{ fn: o.onseek }] : []; + this._onunlock = o.onunlock ? [{ fn: o.onunlock }] : []; + this._onresume = []; + this._webAudio = howler_default.usingWebAudio && !this._html5; + if (typeof howler_default.ctx !== "undefined" && howler_default.ctx && howler_default.autoUnlock) { + howler_default._unlockAudio(); + } + howler_default._howls.push(this); + if (this._autoplay) { + this._queue.push({ + event: "play", + action: () => { + this.play(); + } + }); + } + if (this._preload) { + this.load(); + } + } + load() { + var url = null; + if (howler_default.noAudio) { + this._emit("loaderror", null, "No audio support."); + return this; + } + if (typeof this._src === "string") { + this._src = [this._src]; + } + for (var i = 0; i < this._src.length; i++) { + var ext, str; + if (this._format && this._format[i]) { + ext = this._format[i]; + } else { + str = this._src[i]; + if (typeof str !== "string") { + this._emit("loaderror", null, "Non-string found in selected audio sources - ignoring."); + continue; + } + ext = /^data:audio\/([^;,]+);/i.exec(str); + if (!ext) { + ext = /\.([^.]+)$/.exec(str.split("?", 1)[0]); + } + if (ext) { + ext = ext[1].toLowerCase(); + } + } + if (!ext) { + console.warn('No file extension was found. Consider using the "format" property or specify an extension.'); + } + if (ext && howler_default.codecs(ext)) { + url = this._src[i]; + break; + } + } + if (!url) { + this._emit("loaderror", null, "No codec support for selected audio sources."); + return this; + } + this._src = url; + this._state = "loading"; + if (window.location.protocol === "https:" && url.slice(0, 5) === "http:") { + this._html5 = true; + this._webAudio = false; + } + new sound_default(this); + if (this._webAudio) { + loadBuffer(this); + } + return this; + } + play(sprite, internal) { + var id = null; + if (typeof sprite === "number") { + id = sprite; + sprite = null; + } else if (typeof sprite === "string" && this._state === "loaded" && !this._sprite[sprite]) { + return null; + } else if (typeof sprite === "undefined") { + sprite = "__default"; + if (!this._playLock) { + var num = 0; + for (var i = 0; i < this._sounds.length; i++) { + if (this._sounds[i]._paused && !this._sounds[i]._ended) { + num++; + id = this._sounds[i]._id; + } + } + if (num === 1) { + sprite = null; + } else { + id = null; + } + } + } + const sound = id ? this._soundById(id) : this._inactiveSound(); + if (!sound) { + return null; + } + if (id && !sprite) { + sprite = sound._sprite || "__default"; + } + if (this._state !== "loaded") { + sound._sprite = sprite; + sound._ended = false; + var soundId = sound._id; + this._queue.push({ + event: "play", + action: () => { + this.play(soundId); + } + }); + return soundId; + } + if (id && !sound._paused) { + if (!internal) { + this._loadQueue("play"); + } + return sound._id; + } + if (this._webAudio) { + howler_default._autoResume(); + } + const seek = Math.max(0, sound._seek > 0 ? sound._seek : this._sprite[sprite][0] / 1e3); + const duration = Math.max(0, (this._sprite[sprite][0] + this._sprite[sprite][1]) / 1e3 - seek); + const timeout = duration * 1e3 / Math.abs(sound._rate); + const start = this._sprite[sprite][0] / 1e3; + const stop = (this._sprite[sprite][0] + this._sprite[sprite][1]) / 1e3; + sound._sprite = sprite; + sound._ended = false; + const setParams = () => { + sound._paused = false; + sound._seek = seek; + sound._start = start; + sound._stop = stop; + sound._loop = !!(sound._loop || this._sprite[sprite][2]); + }; + if (seek >= stop) { + this._ended(sound); + return; + } + const node = sound._node; + if (this._webAudio) { + const playWebAudio = () => { + this._playLock = false; + setParams(); + this._refreshBuffer(sound); + const vol = sound._muted || this._muted ? 0 : sound._volume; + node.gain.setValueAtTime(vol, howler_default.ctx.currentTime); + sound._playStart = howler_default.ctx.currentTime; + 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); + } + if (timeout !== Infinity) { + this._endTimers[sound._id] = setTimeout(this._ended.bind(this, sound), timeout); + } + if (!internal) { + setTimeout(() => { + this._emit("play", sound._id); + this._loadQueue(); + }, 0); + } + }; + if (howler_default.state === "running" && howler_default.ctx.state !== "interrupted") { + playWebAudio(); + } else { + this._playLock = true; + this.once("resume", playWebAudio); + this._clearTimer(sound._id); + } + } else { + const playHtml5 = () => { + node.currentTime = seek; + node.muted = sound._muted || this._muted || howler_default._muted || node.muted; + node.volume = sound._volume * howler_default.volume(); + node.playbackRate = sound._rate; + try { + const play = node.play(); + if (play && typeof Promise !== "undefined" && (play instanceof Promise || typeof play.then === "function")) { + this._playLock = true; + setParams(); + play.then(() => { + this._playLock = false; + node._unlocked = true; + if (!internal) { + this._emit("play", sound._id); + } else { + this._loadQueue(); + } + }).catch(() => { + this._playLock = false; + this._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."); + sound._ended = true; + sound._paused = true; + }); + } else if (!internal) { + this._playLock = false; + setParams(); + this._emit("play", sound._id); + } + node.playbackRate = sound._rate; + if (node.paused) { + this._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; + } + if (sprite !== "__default" || sound._loop) { + this._endTimers[sound._id] = setTimeout(this._ended.bind(this, sound), timeout); + } else { + this._endTimers[sound._id] = () => { + this._ended(sound); + node.removeEventListener("ended", this._endTimers[sound._id], false); + }; + node.addEventListener("ended", this._endTimers[sound._id], false); + } + } catch (err) { + this._emit("playerror", sound._id, err); + } + }; + if (node.src === "data:audio/wav;base64,UklGRigAAABXQVZFZm10IBIAAAABAAEARKwAAIhYAQACABAAAABkYXRhAgAAAAEA") { + node.src = this._src; + node.load(); + } + const loadedNoReadyState = window && window.ejecta || !node.readyState && howler_default._navigator.isCocoonJS; + if (node.readyState >= 3 || loadedNoReadyState) { + playHtml5(); + } else { + this._playLock = true; + this._state = "loading"; + const listener = () => { + this._state = "loaded"; + playHtml5(); + node.removeEventListener(howler_default._canPlayEvent, listener, false); + }; + node.addEventListener(howler_default._canPlayEvent, listener, false); + this._clearTimer(sound._id); + } + } + return sound._id; + } + pause(id, skipEmit) { + if (this._state !== "loaded" || this._playLock) { + this._queue.push({ + event: "pause", + action: () => { + this.pause(id); + } + }); + return this; + } + var ids = this._getSoundIds(id); + for (var i = 0; i < ids.length; i++) { + this._clearTimer(ids[i]); + var sound = this._soundById(ids[i]); + if (sound && !sound._paused) { + sound._seek = this.seek(ids[i]); + sound._rateSeek = 0; + sound._paused = true; + this._stopFade(ids[i]); + if (sound._node) { + if (this._webAudio) { + if (!sound._node.bufferSource) { + continue; + } + if (typeof sound._node.bufferSource.stop === "undefined") { + sound._node.bufferSource.noteOff(0); + } else { + sound._node.bufferSource.stop(0); + } + this._cleanBuffer(sound._node); + } else if (!isNaN(sound._node.duration) || sound._node.duration === Infinity) { + sound._node.pause(); + } + } + } + if (!skipEmit) { + this._emit("pause", sound ? sound._id : null); + } + } + return this; + } + stop(id, internal) { + if (this._state !== "loaded" || this._playLock) { + this._queue.push({ + event: "stop", + action: () => { + this.stop(id); + } + }); + return this; + } + var ids = this._getSoundIds(id); + for (var i = 0; i < ids.length; i++) { + this._clearTimer(ids[i]); + var sound = this._soundById(ids[i]); + if (sound) { + sound._seek = sound._start || 0; + sound._rateSeek = 0; + sound._paused = true; + sound._ended = true; + this._stopFade(ids[i]); + if (sound._node) { + if (this._webAudio) { + if (sound._node.bufferSource) { + if (typeof sound._node.bufferSource.stop === "undefined") { + sound._node.bufferSource.noteOff(0); + } else { + sound._node.bufferSource.stop(0); + } + this._cleanBuffer(sound._node); + } + } else if (!isNaN(sound._node.duration) || sound._node.duration === Infinity) { + sound._node.currentTime = sound._start || 0; + sound._node.pause(); + if (sound._node.duration === Infinity) { + this._clearSound(sound._node); + } + } + } + if (!internal) { + this._emit("stop", sound._id); + } + } + } + return this; + } + mute(muted, id) { + if (this._state !== "loaded" || this._playLock) { + this._queue.push({ + event: "mute", + action: () => { + this.mute(muted, id); + } + }); + return this; + } + if (typeof id === "undefined") { + if (typeof muted === "boolean") { + this._muted = muted; + } else { + return this._muted; + } + } + var ids = this._getSoundIds(id); + for (var i = 0; i < ids.length; i++) { + var sound = this._soundById(ids[i]); + if (sound) { + sound._muted = muted; + if (sound._interval) { + this._stopFade(sound._id); + } + if (this._webAudio && sound._node && howler_default.ctx) { + sound._node.gain.setValueAtTime(muted ? 0 : sound._volume, howler_default.ctx.currentTime); + } else if (sound._node) { + sound._node.muted = howler_default._muted ? true : muted; + } + this._emit("mute", sound._id); + } + } + return this; + } + volume(...args) { + let vol, id; + if (args.length === 0) { + return this._volume; + } else if (args.length === 1 || args.length === 2 && typeof args[1] === "undefined") { + var ids = this._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); + } + let sound; + if (typeof vol !== "undefined" && vol >= 0 && vol <= 1) { + if (this._state !== "loaded" || this._playLock) { + this._queue.push({ + event: "volume", + action: () => { + this.volume.apply(this, args); + } + }); + return this; + } + if (typeof id === "undefined") { + this._volume = vol; + } + id = this._getSoundIds(id); + for (var i = 0; i < id.length; i++) { + sound = this._soundById(id[i]); + if (sound) { + sound._volume = vol; + if (!args[2]) { + this._stopFade(id[i]); + } + if (this._webAudio && sound._node && !sound._muted && howler_default.ctx) { + sound._node.gain.setValueAtTime(vol, howler_default.ctx.currentTime); + } else if (sound._node && !sound._muted) { + sound._node.volume = vol * howler_default.volume(); + } + this._emit("volume", sound._id); + } + } + } else { + sound = id ? this._soundById(id) : this._sounds[0]; + return sound ? sound._volume : 0; + } + return this; + } + fade(from, to, len, id) { + if (this._state !== "loaded" || this._playLock) { + this._queue.push({ + event: "fade", + action: () => { + this.fade(from, to, len, id); + } + }); + return this; + } + from = Math.min(Math.max(0, parseFloat(from)), 1); + to = Math.min(Math.max(0, parseFloat(to)), 1); + len = parseFloat(len); + this.volume(from, id); + var ids = this._getSoundIds(id); + for (var i = 0; i < ids.length; i++) { + var sound = this._soundById(ids[i]); + if (sound) { + if (!id) { + this._stopFade(ids[i]); + } + if (this._webAudio && !sound._muted && howler_default.ctx) { + var currentTime = howler_default.ctx.currentTime; + var end = currentTime + len / 1e3; + sound._volume = from; + sound._node.gain.setValueAtTime(from, currentTime); + sound._node.gain.linearRampToValueAtTime(to, end); + } + this._startFadeInterval(sound, from, to, len, ids[i], typeof id === "undefined"); + } + } + return this; + } + _startFadeInterval(sound, from, to, len, id, isGroup) { + var vol = from; + var diff = to - from; + var steps = Math.abs(diff / 0.01); + var stepLen = Math.max(4, steps > 0 ? len / steps : len); + var lastTick = Date.now(); + sound._fadeTo = to; + sound._interval = setInterval(() => { + var tick = (Date.now() - lastTick) / len; + lastTick = Date.now(); + vol += diff * tick; + vol = Math.round(vol * 100) / 100; + if (diff < 0) { + vol = Math.max(to, vol); + } else { + vol = Math.min(to, vol); + } + if (this._webAudio) { + sound._volume = vol; + } else { + this.volume(vol, sound._id, true); + } + if (isGroup) { + this._volume = vol; + } + if (to < from && vol <= to || to > from && vol >= to) { + if (typeof sound._interval === "number") { + clearInterval(sound._interval); + } + sound._interval = null; + sound._fadeTo = null; + this.volume(to, sound._id); + this._emit("fade", sound._id); + } + }, stepLen); + } + _stopFade(id) { + var sound = this._soundById(id); + if (sound && sound._interval) { + if (this._webAudio && howler_default.ctx) { + sound._node.gain.cancelScheduledValues(howler_default.ctx.currentTime); + } + clearInterval(sound._interval); + sound._interval = null; + this.volume(sound._fadeTo, id); + sound._fadeTo = null; + this._emit("fade", id); + } + return this; + } + loop(...args) { + let loop, id, sound; + if (args.length === 0) { + return this._loop; + } else if (args.length === 1) { + if (typeof args[0] === "boolean") { + loop = args[0]; + this._loop = loop; + } else { + sound = this._soundById(parseInt(args[0], 10)); + return sound ? sound._loop : false; + } + } else if (args.length === 2) { + loop = args[0]; + id = parseInt(args[1], 10); + } + var ids = this._getSoundIds(id); + for (var i = 0; i < ids.length; i++) { + sound = this._soundById(ids[i]); + if (sound) { + sound._loop = loop; + if (this._webAudio && sound._node && sound._node.bufferSource) { + sound._node.bufferSource.loop = loop; + if (loop) { + sound._node.bufferSource.loopStart = sound._start || 0; + sound._node.bufferSource.loopEnd = sound._stop; + if (this.playing(ids[i])) { + this.pause(ids[i], true); + this.play(ids[i], true); + } + } + } + } + } + return this; + } + rate(...args) { + let rate, id; + if (args.length === 0) { + id = this._sounds[0]._id; + } else if (args.length === 1) { + var ids = this._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); + } + let sound; + if (typeof rate === "number") { + if (this._state !== "loaded" || this._playLock) { + this._queue.push({ + event: "rate", + action: () => { + this.rate.apply(this, args); + } + }); + return this; + } + if (typeof id === "undefined") { + this._rate = rate; + } + id = this._getSoundIds(id); + for (var i = 0; i < id.length; i++) { + sound = this._soundById(id[i]); + if (sound && howler_default.ctx) { + if (this.playing(id[i])) { + sound._rateSeek = this.seek(id[i]); + sound._playStart = this._webAudio ? howler_default.ctx.currentTime : sound._playStart; + } + sound._rate = rate; + if (this._webAudio && sound._node && sound._node.bufferSource) { + sound._node.bufferSource.playbackRate.setValueAtTime(rate, howler_default.ctx.currentTime); + } else if (sound._node) { + sound._node.playbackRate = rate; + } + const seek = this.seek(id[i]); + const duration = (this._sprite[sound._sprite][0] + this._sprite[sound._sprite][1]) / 1e3 - seek; + const timeout = duration * 1e3 / Math.abs(sound._rate); + if (this._endTimers[id[i]] || !sound._paused) { + this._clearTimer(id[i]); + this._endTimers[id[i]] = setTimeout(this._ended.bind(this, sound), timeout); + } + this._emit("rate", sound._id); + } + } + } else { + sound = this._soundById(id); + return sound ? sound._rate : this._rate; + } + return this; + } + seek(...args) { + let seek, id; + if (args.length === 0) { + if (this._sounds.length) { + id = this._sounds[0]._id; + } + } else if (args.length === 1) { + var ids = this._getSoundIds(); + var index = ids.indexOf(args[0]); + if (index >= 0) { + id = parseInt(args[0], 10); + } else if (this._sounds.length) { + id = this._sounds[0]._id; + seek = parseFloat(args[0]); + } + } else if (args.length === 2) { + seek = parseFloat(args[0]); + id = parseInt(args[1], 10); + } + if (typeof id === "undefined") { + return 0; + } + if (typeof seek === "number" && (this._state !== "loaded" || this._playLock)) { + this._queue.push({ + event: "seek", + action: () => { + this.seek.apply(this, args); + } + }); + return this; + } + var sound = this._soundById(id); + if (sound) { + if (typeof seek === "number" && seek >= 0) { + var playing = this.playing(id); + if (playing) { + this.pause(id, true); + } + sound._seek = seek; + sound._ended = false; + this._clearTimer(id); + if (!this._webAudio && sound._node && !isNaN(sound._node.duration)) { + sound._node.currentTime = seek; + } + const seekAndEmit = () => { + if (playing) { + this.play(id, true); + } + this._emit("seek", id); + }; + if (playing && !this._webAudio) { + const emitSeek = () => { + if (!this._playLock) { + seekAndEmit(); + } else { + setTimeout(emitSeek, 0); + } + }; + setTimeout(emitSeek, 0); + } else { + seekAndEmit(); + } + } else { + if (this._webAudio && howler_default.ctx) { + const realTime = this.playing(id) ? howler_default.ctx.currentTime - sound._playStart : 0; + const rateSeek = sound._rateSeek ? sound._rateSeek - sound._seek : 0; + return sound._seek + (rateSeek + realTime * Math.abs(sound._rate)); + } else { + return sound._node.currentTime; + } + } + } + return this; + } + playing(id) { + if (typeof id === "number") { + var sound = this._soundById(id); + return sound ? !sound._paused : false; + } + for (var i = 0; i < this._sounds.length; i++) { + if (!this._sounds[i]._paused) { + return true; + } + } + return false; + } + duration(id) { + var duration = this._duration; + var sound = this._soundById(id); + if (sound) { + duration = this._sprite[sound._sprite][1] / 1e3; + } + return duration; + } + state() { + return this._state; + } + unload() { + var sounds = this._sounds; + for (let i = 0; i < sounds.length; i++) { + if (!sounds[i]._paused) { + this.stop(sounds[i]._id); + } + if (!this._webAudio) { + this._clearSound(sounds[i]._node); + sounds[i]._node.removeEventListener("error", sounds[i]._errorFn, false); + sounds[i]._node.removeEventListener(howler_default._canPlayEvent, sounds[i]._loadFn, false); + sounds[i]._node.removeEventListener("ended", sounds[i]._endFn, false); + howler_default._releaseHtml5Audio(sounds[i]._node); + } + delete sounds[i]._node; + this._clearTimer(sounds[i]._id); + } + var index = howler_default._howls.indexOf(this); + if (index >= 0) { + howler_default._howls.splice(index, 1); + } + var remCache = true; + for (let i = 0; i < howler_default._howls.length; i++) { + if (howler_default._howls[i]._src === this._src || this._src.indexOf(howler_default._howls[i]._src) >= 0) { + remCache = false; + break; + } + } + if (cache && remCache) { + delete cache[this._src]; + } + howler_default.noAudio = false; + this._state = "unloaded"; + this._sounds = []; + return null; + } + on(event, fn, id, once) { + var events = this["_on" + event]; + if (typeof fn === "function") { + events.push(once ? { id, fn, once } : { id, fn }); + } + return this; + } + off(event, fn, id) { + var events = this["_on" + event]; + var i = 0; + if (typeof fn === "number") { + id = fn; + fn = null; + } + if (fn || id) { + for (i = 0; i < events.length; i++) { + var isId = id === events[i].id; + if (fn === events[i].fn && isId || !fn && isId) { + events.splice(i, 1); + break; + } + } + } else if (event) { + this["_on" + event] = []; + } else { + var keys = Object.keys(this); + for (i = 0; i < keys.length; i++) { + if (keys[i].indexOf("_on") === 0 && Array.isArray(this[keys[i]])) { + this[keys[i]] = []; + } + } + } + return this; + } + once(event, fn, id) { + this.on(event, fn, id, 1); + return this; + } + _emit(event, id, msg) { + var events = this["_on" + event]; + for (var i = events.length - 1; i >= 0; i--) { + if (!events[i].id || events[i].id === id || event === "load") { + setTimeout(((fn) => { + fn.call(this, id, msg); + }).bind(this, events[i].fn), 0); + if (events[i].once) { + this.off(event, events[i].fn, events[i].id); + } + } + } + this._loadQueue(event); + return this; + } + _loadQueue(event) { + if (this._queue.length > 0) { + var task = this._queue[0]; + if (task.event === event) { + this._queue.shift(); + this._loadQueue(); + } + if (!event) { + task.action(); + } + } + return this; + } + _ended(sound) { + var sprite = sound._sprite; + if (!this._webAudio && sound._node && !sound._node.paused && !sound._node.ended && sound._node.currentTime < sound._stop) { + setTimeout(this._ended.bind(this, sound), 100); + return this; + } + var loop = !!(sound._loop || this._sprite[sprite][2]); + this._emit("end", sound._id); + if (!this._webAudio && loop) { + this.stop(sound._id, true).play(sound._id); + } + if (this._webAudio && loop && howler_default.ctx) { + this._emit("play", sound._id); + sound._seek = sound._start || 0; + sound._rateSeek = 0; + sound._playStart = howler_default.ctx.currentTime; + var timeout = (sound._stop - sound._start) * 1e3 / Math.abs(sound._rate); + this._endTimers[sound._id] = setTimeout(this._ended.bind(this, sound), timeout); + } + if (this._webAudio && !loop) { + sound._paused = true; + sound._ended = true; + sound._seek = sound._start || 0; + sound._rateSeek = 0; + this._clearTimer(sound._id); + this._cleanBuffer(sound._node); + howler_default._autoSuspend(); + } + if (!this._webAudio && !loop) { + this.stop(sound._id, true); + } + return this; + } + _clearTimer(id) { + if (this._endTimers[id]) { + if (typeof this._endTimers[id] !== "function") { + clearTimeout(this._endTimers[id]); + } else { + var sound = this._soundById(id); + if (sound && sound._node) { + sound._node.removeEventListener("ended", this._endTimers[id], false); + } + } + delete this._endTimers[id]; + } + return this; + } + _soundById(id) { + for (var i = 0; i < this._sounds.length; i++) { + if (id === this._sounds[i]._id) { + return this._sounds[i]; + } + } + return null; + } + _inactiveSound() { + this._drain(); + for (let i = 0; i < this._sounds.length; i++) { + if (this._sounds[i]._ended) { + return this._sounds[i].reset(); + } + } + return new sound_default(this); + } + _drain() { + const limit = this._pool; + let cnt = 0; + let i = 0; + if (this._sounds.length < limit) { + return; + } + for (i = 0; i < this._sounds.length; i++) { + if (this._sounds[i]._ended) { + cnt++; + } + } + for (i = this._sounds.length - 1; i >= 0; i--) { + if (cnt <= limit) { + return; + } + if (this._sounds[i]._ended) { + if (this._webAudio && this._sounds[i]._node) { + this._sounds[i]._node.disconnect(0); + } + this._sounds.splice(i, 1); + cnt--; + } + } + } + _getSoundIds(id) { + if (typeof id === "undefined") { + var ids = []; + for (var i = 0; i < this._sounds.length; i++) { + ids.push(this._sounds[i]._id); + } + return ids; + } else { + return [id]; + } + } + _refreshBuffer(sound) { + sound._node.bufferSource = howler_default.ctx.createBufferSource(); + sound._node.bufferSource.buffer = cache[this._src]; + if (sound._panner) { + sound._node.bufferSource.connect(sound._panner); + } else { + sound._node.bufferSource.connect(sound._node); + } + sound._node.bufferSource.loop = sound._loop; + if (sound._loop) { + sound._node.bufferSource.loopStart = sound._start || 0; + sound._node.bufferSource.loopEnd = sound._stop || 0; + } + sound._node.bufferSource.playbackRate.setValueAtTime(sound._rate, howler_default.ctx.currentTime); + return this; + } + _cleanBuffer(node) { + var isIOS = howler_default._navigator && howler_default._navigator.vendor.indexOf("Apple") >= 0; + if (howler_default._scratchBuffer && node.bufferSource) { + node.bufferSource.onended = null; + node.bufferSource.disconnect(0); + if (isIOS) { + try { + node.bufferSource.buffer = howler_default._scratchBuffer; + } catch (e) { + } + } + } + node.bufferSource = null; + return this; + } + _clearSound(node) { + var checkIE = /MSIE |Trident\//.test(howler_default._navigator && howler_default._navigator.userAgent); + if (!checkIE) { + node.src = "data:audio/wav;base64,UklGRigAAABXQVZFZm10IBIAAAABAAEARKwAAIhYAQACABAAAABkYXRhAgAAAAEA"; + } + } +}; +var howl_default = Howl; +export { + howl_default as Howl, + howler_default as Howler +}; +/*! + * howler.js v2.2.3 + * howlerjs.com + * + * (c) 2013-2020, James Simpson of GoldFire Studios + * goldfirestudios.com + * + * MIT License + */ diff --git a/dist/helpers.d.ts b/dist/helpers.d.ts new file mode 100644 index 00000000..ef53ed7c --- /dev/null +++ b/dist/helpers.d.ts @@ -0,0 +1,9 @@ +import Howl from './howl'; +export declare const cache: {}; +/** + * Buffer a sound from URL, Data URI or cache and decode to audio source (Web Audio API). + */ +export declare function loadBuffer(self: Howl): void; +export declare const isHTMLAudioElement: (node: any) => node is HTMLAudioElement; +export declare const isGainNode: (node: any) => node is GainNode; +export declare const isAudioBufferSourceNode: (node: any) => node is AudioBufferSourceNode; diff --git a/dist/howl.d.ts b/dist/howl.d.ts new file mode 100644 index 00000000..0315314f --- /dev/null +++ b/dist/howl.d.ts @@ -0,0 +1,401 @@ +import Sound from './sound'; +export declare type HowlCallback = (soundId: number) => void; +export declare type HowlErrorCallback = (soundId: number, error: unknown) => void; +export interface SoundSpriteDefinitions { + [name: string]: [number, number] | [number, number, boolean]; +} +export interface HowlXHROptions { + method?: string; + headers?: Record; + withCredentials?: boolean; +} +export interface HowlListeners { + /** + * Fires when the sound has been stopped. The first parameter is the ID of the sound. + */ + onstop?: HowlCallback; + /** + * Fires when the sound has been paused. The first parameter is the ID of the sound. + */ + onpause?: HowlCallback; + /** + * Fires when the sound is loaded. + */ + onload?: HowlCallback; + /** + * Fires when the sound has been muted/unmuted. The first parameter is the ID of the sound. + */ + onmute?: HowlCallback; + /** + * Fires when the sound's volume has changed. The first parameter is the ID of the sound. + */ + onvolume?: HowlCallback; + /** + * Fires when the sound's playback rate has changed. The first parameter is the ID of the sound. + */ + onrate?: HowlCallback; + /** + * Fires when the sound has been seeked. The first parameter is the ID of the sound. + */ + onseek?: HowlCallback; + /** + * Fires when the current sound finishes fading in/out. The first parameter is the ID of the sound. + */ + onfade?: HowlCallback; + /** + * Fires when audio has been automatically unlocked through a touch/click event. + */ + onunlock?: HowlCallback; + /** + * Fires when the sound finishes playing (if it is looping, it'll fire at the end of each loop). + * The first parameter is the ID of the sound. + */ + onend?: HowlCallback; + /** + * Fires when the sound begins playing. The first parameter is the ID of the sound. + */ + onplay?: HowlCallback; + /** + * Fires when the sound is unable to load. The first parameter is the ID of the sound (if it exists) and the second is the error message/code. + */ + onloaderror?: HowlErrorCallback; + /** + * Fires when the sound is unable to play. The first parameter is the ID of the sound and the second is the error message/code. + */ + onplayerror?: HowlErrorCallback; +} +export interface HowlOptions extends HowlListeners { + /** + * The sources to the track(s) to be loaded for the sound (URLs or base64 data URIs). These should + * be in order of preference, howler.js will automatically load the first one that is compatible + * with the current browser. If your files have no extensions, you will need to explicitly specify + * the extension using the format property. + * + * @default `[]` + */ + src?: string | string[]; + /** + * The volume of the specific track, from 0.0 to 1.0. + * + * @default `1.0` + */ + volume?: number; + /** + * Set to true to force HTML5 Audio. This should be used for large audio files so that you don't + * have to wait for the full file to be downloaded and decoded before playing. + * + * @default `false` + */ + html5?: boolean; + /** + * Set to true to automatically loop the sound forever. + * + * @default `false` + */ + loop?: boolean; + /** + * Automatically begin downloading the audio file when the Howl is defined. If using HTML5 Audio, + * you can set this to 'metadata' to only preload the file's metadata (to get its duration without + * download the entire file, for example). + * + * @default `true` + */ + preload?: boolean | 'metadata'; + /** + * Set to true to automatically start playback when sound is loaded. + * + * @default `false` + */ + autoplay?: boolean; + /** + * Set to true to load the audio muted. + * + * @default `false` + */ + mute?: boolean; + /** + * Define a sound sprite for the sound. The offset and duration are defined in milliseconds. A + * third (optional) parameter is available to set a sprite as looping. An easy way to generate + * compatible sound sprites is with audiosprite. + * + * @default `{}` + */ + sprite?: SoundSpriteDefinitions; + /** + * The rate of playback. 0.5 to 4.0, with 1.0 being normal speed. + * + * @default `1.0` + */ + rate?: number; + /** + * The size of the inactive sounds pool. Once sounds are stopped or finish playing, they are marked + * as ended and ready for cleanup. We keep a pool of these to recycle for improved performance. + * Generally this doesn't need to be changed. It is important to keep in mind that when a sound is + * paused, it won't be removed from the pool and will still be considered active so that it can be + * resumed later. + * + * @default `5` + */ + pool?: number; + /** + * howler.js automatically detects your file format from the extension, but you may also specify a + * format in situations where extraction won't work (such as with a SoundCloud stream). + * + * @default `[]` + */ + format?: string[]; + /** + * When using Web Audio, howler.js uses an XHR request to load the audio files. If you need to send + * custom headers, set the HTTP method or enable withCredentials (see reference), include them with + * this parameter. Each is optional (method defaults to GET, headers default to undefined and + * withCredentials defaults to false). + */ + xhr?: HowlXHROptions; +} +declare type HowlCallbacks = Array<{ + fn: HowlCallback; +}>; +declare type HowlErrorCallbacks = Array<{ + fn: HowlErrorCallback; +}>; +declare type HowlEvent = 'play' | 'end' | 'pause' | 'stop' | 'mute' | 'volume' | 'rate' | 'seek' | 'fade' | 'unlock' | 'load' | 'loaderror' | 'playerror'; +interface HowlEventHandler { + event: HowlEvent; + action: () => void; +} +declare class Howl { + _autoplay: boolean; + _format: string[]; + _html5: boolean; + _muted: boolean; + _loop: boolean; + _pool: number; + _preload: boolean | 'metadata'; + _rate: number; + _sprite: SoundSpriteDefinitions; + _src: string | string[]; + _volume: number; + _xhr: HowlOptions['xhr']; + _duration: number; + _state: 'unloaded' | 'loading' | 'loaded'; + _sounds: Sound[]; + _endTimers: {}; + _queue: HowlEventHandler[]; + _playLock: boolean; + _onend: HowlCallbacks; + _onfade: HowlCallbacks; + _onload: HowlCallbacks; + _onloaderror: HowlErrorCallbacks; + _onplayerror: HowlErrorCallbacks; + _onpause: HowlCallbacks; + _onplay: HowlCallbacks; + _onstop: HowlCallbacks; + _onmute: HowlCallbacks; + _onvolume: HowlCallbacks; + _onrate: HowlCallbacks; + _onseek: HowlCallbacks; + _onunlock: HowlCallbacks; + _onresume: HowlCallbacks; + _webAudio: boolean; + /** + * Create an audio group controller. + * @param o Passed in properties for this group. + */ + constructor(o: HowlOptions); + /** + * Load the audio file. + */ + load(): this; + /** + * Play a sound or resume previous playback. + * @param sprite Sprite name for sprite playback or sound id to continue previous. + * @param internal Internal Use: true prevents event firing. + * @return Sound ID. + */ + play(sprite?: string | number, internal?: boolean): number | null | undefined; + /** + * Pause playback and save current position. + * @param id The sound ID (empty to pause all in group). + * @param skipEmit If true, the `pause` event won't be emitted. + */ + pause(id: number, skipEmit?: boolean): this; + /** + * Stop playback and reset to start. + * @param id The sound ID (empty to stop all in group). + * @param internal Internal Use: true prevents event firing. + */ + stop(id?: number, internal?: boolean): this; + /** + * Mute/unmute a single sound or all sounds in this Howl group. + * @param muted Set to true to mute and false to unmute. + * @param id The sound ID to update (omit to mute/unmute all). + */ + mute(muted: boolean, id: number): boolean | this; + /** + * Get/set the volume of this sound or of the Howl group. This method can optionally take 0, 1 or 2 arguments. + * volume() -> 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 Returns this or current volume. + */ + volume(...args: any[]): number | this; + /** + * Fade a currently playing sound between two volumes (if no id is passed, all sounds will fade). + * @param from The value to fade from (0.0 to 1.0). + * @param to The volume to fade to (0.0 to 1.0). + * @param len Time in milliseconds to fade. + * @param id The sound id (omit to fade all sounds). + */ + fade(from: number | string, to: number | string, len: number | string, id: number): this; + /** + * Starts the internal interval to fade a sound. + * @param sound Reference to sound to fade. + * @param from The value to fade from (0.0 to 1.0). + * @param to The volume to fade to (0.0 to 1.0). + * @param len Time in milliseconds to fade. + * @param id The sound id to fade. + * @param isGroup If true, set the volume on the group. + */ + _startFadeInterval(sound: Sound, from: number, to: number, len: number, id: number, isGroup: boolean): void; + /** + * 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(id: any): this; + /** + * 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 Returns this or current loop value. + */ + loop(...args: any[]): any; + /** + * Get/set the playback rate of a sound. This method can optionally take 0, 1 or 2 arguments. + * rate() -> 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 Returns this or the current playback rate. + */ + rate(...args: any[]): any; + /** + * Get/set the seek position of a sound. This method can optionally take 0, 1 or 2 arguments. + * seek() -> 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 Returns this or the current seek position. + */ + seek(...args: any[]): number | this; + /** + * 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 id The sound id to check. If none is passed, the whole sound group is checked. + * @return True if playing and false if not. + */ + playing(id: number): boolean; + /** + * Get the duration of this sound. Passing a sound id will return the sprite duration. + * @param id The sound id to check. If none is passed, return full source duration. + * @return Audio duration in seconds. + */ + duration(id: number): number; + /** + * Returns the current loaded state of this Howl. + * @return 'unloaded', 'loading', 'loaded' + */ + state(): "loaded" | "loading" | "unloaded"; + /** + * Unload and destroy the current Howl object. + * This will immediately stop all sound instances attached to this group. + */ + unload(): 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. + */ + on(event: string, fn: Function, id?: number, once?: number): this; + /** + * 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(event: any, fn: any, id: any): this; + /** + * Listen to a custom event and remove it once fired. + * @param event Event name. + * @param fn Listener to call. + * @param id (optional) Only listen to events for this sound. + */ + once(event: string, fn: Function, id?: number): this; + /** + * Emit all events of a specific type and pass the sound id. + * @param event Event name. + * @param id Sound ID. + * @param msg Message to go with event. + */ + _emit(event: string, id?: number | null, msg?: string | number): this; + /** + * 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). + */ + _loadQueue(event?: string): this; + /** + * Fired when playback ends at the end of the duration. + * @param sound The sound object to work with. + */ + _ended(sound: Sound): this; + /** + * Clear the end timer for a sound playback. + * @param {Number} id The sound ID. + */ + _clearTimer(id: number): this; + /** + * Return the sound identified by this ID, or return null. + * @param id Sound ID + * @return Sound object or null. + */ + _soundById(id: number): Sound | null; + /** + * Return an inactive sound from the pool or create a new one. + * @return Sound playback object. + */ + _inactiveSound(): Sound; + /** + * Drain excess inactive sounds from the pool. + */ + _drain(): void; + /** + * Get all ID's from the sounds pool. + * @param id Only return one ID if one is passed. + * @return Array of IDs. + */ + _getSoundIds(id?: number): number[]; + /** + * Load the sound back into the buffer source. + * @param sound The sound object to work with. + */ + _refreshBuffer(sound: Sound): this; + /** + * Prevent memory leaks by cleaning up the buffer source after playback. + * @param {Object} node Sound's audio node containing the buffer source. + * @return {Howl} + */ + _cleanBuffer(node: Sound['_node']): this; + /** + * Set the source to a 0-second silence to stop any downloading (except in IE). + * @param {Object} node Audio node to clear. + */ + _clearSound(node: any): void; +} +export default Howl; diff --git a/dist/howler.core.min.js b/dist/howler.core.min.js deleted file mode 100644 index c260dc37..00000000 --- a/dist/howler.core.min.js +++ /dev/null @@ -1,2 +0,0 @@ -/*! howler.js v2.2.3 | (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\/([0-6].)/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(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/dist/howler.d.ts b/dist/howler.d.ts new file mode 100644 index 00000000..63e68a25 --- /dev/null +++ b/dist/howler.d.ts @@ -0,0 +1,99 @@ +import Howl from './howl'; +export interface HowlerAudioElement extends HTMLAudioElement { + _unlocked: boolean; +} +declare type HowlerAudioContextState = AudioContextState | 'suspending' | 'closed' | 'interrupted'; +export declare type HowlerAudioContext = Omit & { + state: AudioContextState | 'interrupted'; +}; +declare class Howler { + masterGain: GainNode | null; + noAudio: boolean; + usingWebAudio: boolean; + autoSuspend: boolean; + ctx: HowlerAudioContext | null; + autoUnlock: boolean; + _counter: number; + _html5AudioPool: Array; + html5PoolSize: number; + _codecs: {}; + _howls: Array; + _muted: boolean; + _volume: number; + _canPlayEvent: string; + _navigator: Navigator; + _audioUnlocked: boolean; + _mobileUnloaded: boolean; + state: HowlerAudioContextState; + _suspendTimer: number | null; + _resumeAfterSuspend?: boolean; + _scratchBuffer: any; + /** + * Create the global controller. All contained methods and properties apply + * to all sounds that are currently playing or will be in the future. + */ + constructor(); + /** + * Get/set the global volume for all sounds. + * @param vol Volume from 0.0 to 1.0. + * @return Returns self or current volume. + */ + volume(vol?: number | string): number | this; + /** + * Handle muting and unmuting globally. + * @param muted Is muted or not. + */ + mute(muted: boolean): this; + /** + * Handle stopping all sounds globally. + */ + stop(): this; + /** + * Unload and destroy all currently loaded Howl objects. + */ + unload(): this; + /** + * Check for codec support of specific extension. + * @param ext Audio file extention. + */ + codecs(ext: string): any; + /** + * Setup various state values for global tracking. + */ + _setup(): this; + /** + * Setup the audio context when available, or switch to HTML5 Audio mode. + */ + _setupAudioContext(): void; + /** + * Check for browser support for various codecs and cache the results. + */ + _setupCodecs(): this; + /** + * 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/ + */ + _unlockAudio(): this; + /** + * Get an unlocked HTML5 Audio object from the pool. If none are left, + * return a new Audio object and throw a warning. + * @return HTML5 Audio object. + */ + _obtainHtml5Audio(): HTMLAudioElement | undefined; + /** + * Return an activated HTML5 Audio object to the pool. + */ + _releaseHtml5Audio(audio: HowlerAudioElement): this; + /** + * Automatically suspend the Web Audio AudioContext after no sound has played for 30 seconds. + * This saves processing/energy and fixes various browser-specific bugs with audio getting stuck. + */ + _autoSuspend(): this | undefined; + /** + * Automatically resume the Web Audio AudioContext when a new sound is played. + */ + _autoResume(): this | undefined; +} +declare const HowlerSingleton: Howler; +export default HowlerSingleton; diff --git a/dist/howler.js b/dist/howler.js deleted file mode 100644 index a2758c70..00000000 --- a/dist/howler.js +++ /dev/null @@ -1,3242 +0,0 @@ -/*! - * howler.js v2.2.3 - * 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\/([0-6].)/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 (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.3 - * 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\/([0-6].)/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(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 s=a._orientation;return n="number"!=typeof n?s[1]:n,t="number"!=typeof t?s[2]:t,r="number"!=typeof r?s[3]:r,o="number"!=typeof o?s[4]:o,i="number"!=typeof i?s[5]:i,"number"!=typeof e?s:(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 s=a._orientation;return n="number"!=typeof n?s[1]:n,t="number"!=typeof t?s[2]:t,r="number"!=typeof r?s[3]:r,o="number"!=typeof o?s[4]:o,i="number"!=typeof i?s[5]:i,"number"!=typeof e?s:(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 + - - - Howler.js Core HTML5 Audio Tests - - - -
-
- - -
- - - - \ No newline at end of file + + + Howler.js Core HTML5 Audio Tests + + + +
+
+ + +
+ + + diff --git a/tests/js/core.html5audio.js b/tests/js/core.html5audio.js index cd546ef7..3c63e68a 100644 --- a/tests/js/core.html5audio.js +++ b/tests/js/core.html5audio.js @@ -1,14 +1,16 @@ +import { Howl } from '../../dist/core'; + // Cache the label for later use. -var label = document.getElementById('label'); -var start = document.getElementById('start'); +const label = document.getElementById('label'); +const start = document.getElementById('start'); // Setup the sounds to be used. -var sound1 = new Howl({ +const sound1 = new Howl({ src: ['audio/sound1.webm', 'audio/sound1.mp3'], - html5: true + html5: true, }); -var sound2 = new Howl({ +const sound2 = new Howl({ src: ['audio/sound2.webm', 'audio/sound2.mp3'], html5: true, sprite: { @@ -17,235 +19,238 @@ var sound2 = new Howl({ three: [4000, 350], four: [6000, 380], five: [8000, 340], - beat: [10000, 11163] - } + beat: [10000, 11163], + }, }); // Enable the start button when the sounds have loaded. -sound1.once('load', function() { +sound1.once('load', () => { start.removeAttribute('disabled'); start.innerHTML = 'BEGIN CORE TESTS'; }); // Define the tests to run. -var id; -var tests = [ - function(fn) { +let id; +const tests = [ + (fn) => { id = sound1.play(); label.innerHTML = 'PLAYING'; setTimeout(fn, 2000); }, - function(fn) { + (fn) => { sound1.pause(id); label.innerHTML = 'PAUSED'; setTimeout(fn, 1500); }, - function(fn) { + (fn) => { sound1.play(id); label.innerHTML = 'RESUMING'; setTimeout(fn, 2000); }, - function(fn) { + (fn) => { sound1.stop(id); label.innerHTML = 'STOPPED'; setTimeout(fn, 1500); }, - function(fn) { + (fn) => { sound1.play(id); label.innerHTML = 'PLAY FROM START'; setTimeout(fn, 2000); }, - function(fn) { + (fn) => { sound1.rate(1.5, id); label.innerHTML = 'SPEED UP'; setTimeout(fn, 2000); }, - function(fn) { + (fn) => { sound1.rate(1, id); label.innerHTML = 'SLOW DOWN'; setTimeout(fn, 2000); }, - function(fn) { + (fn) => { sound1.fade(1, 0, 2000, id); label.innerHTML = 'FADE OUT'; - sound1.once('fade', function() { + sound1.once('fade', () => { fn(); }); }, - function(fn) { + (fn) => { sound1.fade(0, 1, 2000, id); label.innerHTML = 'FADE IN'; - sound1.once('fade', function() { + sound1.once('fade', () => { fn(); }); }, - function(fn) { + (fn) => { sound1.mute(true, id); label.innerHTML = 'MUTE'; setTimeout(fn, 1500); }, - function(fn) { + (fn) => { sound1.mute(false, id); label.innerHTML = 'UNMUTE'; setTimeout(fn, 2000); }, - function(fn) { + (fn) => { sound1.volume(0.5, id); label.innerHTML = 'HALF VOLUME'; setTimeout(fn, 2000); }, - function(fn) { + (fn) => { sound1.volume(1, id); label.innerHTML = 'FULL VOLUME'; setTimeout(fn, 2000); }, - function(fn) { + (fn) => { sound1.seek(0, id); label.innerHTML = 'SEEK TO START'; setTimeout(fn, 2000); }, - function(fn) { + (fn) => { id = sound1.play(); label.innerHTML = 'PLAY 2ND'; setTimeout(fn, 2000); }, - function(fn) { + (fn) => { sound1.mute(true); label.innerHTML = 'MUTE GROUP'; setTimeout(fn, 1500); }, - function(fn) { + (fn) => { sound1.mute(false); label.innerHTML = 'UNMUTE GROUP'; setTimeout(fn, 2000); }, - function(fn) { + (fn) => { sound1.volume(0.5); label.innerHTML = 'HALF VOLUME GROUP'; setTimeout(fn, 2000); }, - function(fn) { + (fn) => { sound1.fade(0.5, 0, 2000); label.innerHTML = 'FADE OUT GROUP'; - sound1.once('fade', function() { + sound1.once('fade', () => { if (sound1._onfade.length === 0) { fn(); } }); }, - function(fn) { + (fn) => { sound1.fade(0, 1, 2000); label.innerHTML = 'FADE IN GROUP'; - sound1.once('fade', function() { + sound1.once('fade', () => { if (sound1._onfade.length === 0) { fn(); } }); }, - function(fn) { + (fn) => { sound1.stop(); label.innerHTML = 'STOP GROUP'; setTimeout(fn, 1500); }, - function(fn) { + (fn) => { id = sound2.play('beat'); label.innerHTML = 'PLAY SPRITE'; setTimeout(fn, 2000); }, - function(fn) { + (fn) => { sound2.pause(id); label.innerHTML = 'PAUSE SPRITE'; setTimeout(fn, 1000); }, - function(fn) { + (fn) => { sound2.play(id); label.innerHTML = 'RESUME SPRITE'; setTimeout(fn, 1500); }, - function(fn) { - var sounds = ['one', 'two', 'three', 'four', 'five']; - for (var i=0; i { + const sounds = ['one', 'two', 'three', 'four', 'five']; + for (let i = 0; i < sounds.length; i++) { + setTimeout( + ((i) => { + sound2.play(sounds[i]); + }).bind(null, i), + i * 500, + ); } label.innerHTML = 'MULTIPLE SPRITES'; setTimeout(fn, 3000); }, - function(fn) { - var sprite = sound2.play('one'); + (fn) => { + const sprite = sound2.play('one'); sound2.loop(true, sprite); label.innerHTML = 'LOOP SPRITE'; - setTimeout(function() { + setTimeout(() => { sound2.loop(false, sprite); fn(); }, 3000); }, - function(fn) { + (fn) => { sound2.fade(1, 0, 2000, id); label.innerHTML = 'FADE OUT SPRITE'; - sound2.once('fade', function() { + sound2.once('fade', () => { fn(); }); - } + }, ]; // Create a method that will call the next in the series. -var chain = function(i) { - return function() { +const chain = (i) => { + return () => { if (tests[i]) { tests[i](chain(++i)); } else { @@ -254,7 +259,7 @@ var chain = function(i) { label.style.color = '#74b074'; // Wait for 5 seconds and then go back to the tests index. - setTimeout(function() { + setTimeout(() => { window.location = './'; }, 5000); } @@ -262,7 +267,11 @@ var chain = function(i) { }; // Listen to a click on the button to being the tests. -start.addEventListener('click', function() { - tests[0](chain(1)); - start.style.display = 'none'; -}, false); \ No newline at end of file +start.addEventListener( + 'click', + () => { + tests[0](chain(1)); + start.style.display = 'none'; + }, + false, +); diff --git a/tests/js/core.webaudio.js b/tests/js/core.webaudio.js index ae9388ee..e7a2e959 100644 --- a/tests/js/core.webaudio.js +++ b/tests/js/core.webaudio.js @@ -236,7 +236,7 @@ const tests = [ }, (fn) => { - var sprite = sound2.play('one'); + const sprite = sound2.play('one'); sound2.loop(true, sprite); label.innerHTML = 'LOOP SPRITE';