├── .eslintrc.js
├── .gitignore
├── .travis.yml
├── LICENSE
├── README.md
├── examples
├── basic.html
├── context.html
├── events.html
└── tone.html
├── images
├── volume-off.svg
└── volume-on.svg
├── package-lock.json
├── package.json
├── scripts
└── increment_version.js
├── src
├── AudioContext.js
├── AudioElement.js
├── ScriptElement.js
├── Toggle.js
├── Unmute.js
└── unmute.scss
├── test
├── autoadd.html
├── blank.html
├── build-test.js
├── events.html
├── indirect_click.html
├── oldtone.html
├── referenceImage.png
├── test.js
├── tone.html
└── tone_latest.html
└── webpack.config.js
/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | "env": {
3 | "browser": true,
4 | "node" : true,
5 | "mocha" : true,
6 | "es6": true,
7 | "amd" : false,
8 | },
9 | "globals": {
10 | "UnmuteButton": true,
11 | "Tone": true
12 | },
13 | "parserOptions": {
14 | "ecmaVersion": 2017,
15 | "ecmaFeatures": {
16 | "jsx": true
17 | },
18 | "sourceType": "module"
19 | },
20 | "extends": ["eslint:recommended"],
21 | "rules": {
22 | "dot-location" : [ "error", "property" ],
23 | "linebreak-style": [ "error", "unix" ],
24 | "eqeqeq" : [ "error" ],
25 | "curly" : [ "error", "all" ],
26 | "dot-notation" : [ "error" ],
27 | "no-throw-literal" : [ "error" ],
28 | "no-useless-call" : [ "error" ],
29 | "no-unmodified-loop-condition": [ "error" ],
30 | "quote-props" : [ "error", "as-needed" /*"as-needed"*/ ],
31 | "quotes": [ "error","single" ],
32 | "no-lonely-if" : [ "error" ],
33 | "semi": [ "error", "never" ],
34 | //STYLE
35 | "indent": [ "error", "tab", { "SwitchCase": 1 } ],
36 | "no-multi-spaces" : [ "error" ],
37 | "array-bracket-spacing" : [ "error" , "never" ],
38 | "block-spacing": [ "error", "always" ],
39 | "func-call-spacing" : [ "error", "never" ],
40 | "key-spacing" : [ "error", {"beforeColon" : true, "afterColon" : true} ],
41 | "brace-style": [ "error", "1tbs" ],
42 | "space-in-parens": [ "error", "never" ],
43 | "eol-last": [ "error", "always" ],
44 | "lines-between-class-members": [ "error", "always" ],
45 | "no-multiple-empty-lines": [ "error", { "max": 1, "maxEOF": 1, "maxBOF": 0} ],
46 | "no-unneeded-ternary": [ "error" ],
47 | "object-curly-spacing": [ "error" , "always" ],
48 | "space-unary-ops": [ "error" , { "words" : true, "nonwords" : false } ],
49 | "block-spacing" : ["error", "always"],
50 | "keyword-spacing" : ["error", { "before": true }],
51 | "space-before-function-paren": ["error", {"anonymous": "never", "named": "never", "asyncArrow": "always"}],
52 | "comma-spacing": ["error", { "before": false, "after": true }],
53 | "space-before-blocks": ["error", { "functions": "never", "keywords": "never", "classes": "always" }]
54 | }
55 | };
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules/
2 | .DS_Store
3 | *.sublime-project
4 | *sublime-workspace
5 | test/testCapture.png
6 | build
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | sudo: required
2 | dist: trusty
3 | language: node_js
4 | node_js:
5 | - '9'
6 | before_deploy: npm run increment
7 | deploy:
8 | - provider: npm
9 | skip_cleanup: true
10 | email: yotammann@gmail.com
11 | api_key: $NPM_TOKEN
12 | on:
13 | # don't publish on cron or PRs
14 | condition: $TRAVIS_EVENT_TYPE != cron && $TRAVIS_EVENT_TYPE != pull_request
15 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2018 Yotam Mann
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | [](https://travis-ci.org/Tonejs/unmute)
2 |
3 | Unmute adds a mute/unmute button to the top right corner of your page.
4 |
5 | This button implements many browsers' requirements that the AudioContext is started by a user action before it can play any sound. If the AudioContext is not running when the page is loaded, the button will initially be muted until a user clicks to unmute the button.
6 |
7 | [example](https://tonejs.github.io/unmute/examples/basic.html)
8 |
9 | ## INSTALLATION
10 |
11 | `npm install unmute`
12 |
13 | ## USAGE
14 |
15 | The mute button can be added to the page like so:
16 |
17 | ```javascript
18 | UnmuteButton()
19 | ```
20 |
21 | ### es6
22 |
23 | ```javascript
24 | import UnmuteButton from 'unmute'
25 |
26 | UnmuteButton()
27 | ```
28 |
29 | ### HTML
30 |
31 | If your code uses Tone.js, you can simply add the following code to your `
` and it'll add an UnmuteButton to the page and bind itself to Tone.js' AudioContext. Tone.js must be included on the page.
32 |
33 | ```html
34 |
35 | ```
36 |
37 | ## API
38 |
39 |
40 | ### Parameter
41 |
42 | UnmuteButton takes an optional object as a parameter.
43 |
44 | ```javascript
45 | UnmuteButton({
46 | //the parent element of the mute button
47 | //can pass in "none" to create the element, but not add it to the DOM
48 | container : document.querySelector('#container'),
49 | //the title which appears on the iOS lock screen
50 | title : 'Web Audio',
51 | //force it to start muted, even when the AudioContext is running
52 | mute : false
53 | //AudioContext
54 | context : new AudioContext(),
55 | })
56 | ```
57 | #### `container`
58 |
59 | The HTMLElement which the button will be added to
60 |
61 | #### `title`
62 |
63 | UnmuteButton also unmutes the browser tab on iOS even when the mute toggle rocker switch is toggled on. This causes a title to appear on the phone's lock screen. The default title says "Web Audio"
64 |
65 | #### `mute`
66 |
67 | This will force the initial state of the button to be muted. Though, you _cannot_ force it to be 'unmuted' by passing in `{'mute' : false}` because the default state of the button is also determined by the state of the AudioContext.
68 |
69 | #### `context`
70 |
71 | If a context is passed in, it will be wrapped and available as a property of the returned object. If no context was passed in, one will be created. You can access the created context as a property.
72 |
73 | ```javascript
74 | const { context } = UnmuteButton()
75 | ```
76 |
77 | ### Events
78 |
79 | UnmuteButton returns an event emitting object.
80 |
81 | #### 'start'
82 |
83 | Emitted when the AudioContext is started for the first time.
84 |
85 | ```javascript
86 | UnmuteButton().on('start', () => {
87 | //AudioContext.state is 'running'
88 | })
89 | ```
90 |
91 | #### 'mute'
92 |
93 | Emitted when the AudioContext is muted.
94 |
95 | #### 'unmute'
96 |
97 | Emitted when the AudioContext is unmuted.
98 |
99 | ## Methods
100 |
101 | ### `remove()`
102 |
103 | Removes the button element from its container
104 |
105 | ```javascript
106 | const unmute = UnmuteButton()
107 | //remove the element
108 | unmute.remove()
109 | ```
110 |
111 | ## Style
112 |
113 | The UnmuteButton's default styling can be overwritten with css. The UnmuteButton is a `` element with id `#unmute-button`. When in a muted state, a class `.muted` is added to the element.
114 |
115 | ## iOS
116 |
117 | Additionally this button plays a silent sound through an `` element when the button is clicked which enables sound on iOS even when the mute rocker switch is toggled on. [[reference](https://stackoverflow.com/questions/21122418/ios-webaudio-only-works-on-headphones/46839941#46839941)]
118 |
119 | ## Earlier versions of Tone.js (before tone@13.2.3)
120 |
121 | If using an older version of Tone with a global reference to Tone.js, it should work as with the above examples. The one exception is if you're using it with a build system which does not create a reference to `Tone` on the window.
122 |
123 | This has been tested with Tone.js (>=tone@0.7.0)
124 |
125 | ```javascript
126 | import Tone from 'tone'
127 |
128 | UnmuteButton({ tone : Tone })
129 | ```
130 |
131 | ## Without Tone.js
132 |
133 | To use it without Tone.js, check out [this example](examples/context.html). Be sure to use the wrapped and shimmed AudioContext instance which is a property of the UnmuteButton instance. Automatically adding the button to the body (using `data-add-button="true"`) will not work.
--------------------------------------------------------------------------------
/examples/basic.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
18 |
19 |
20 |
--------------------------------------------------------------------------------
/examples/context.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
16 |
17 |
18 |
--------------------------------------------------------------------------------
/examples/events.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
24 |
25 |
26 |
--------------------------------------------------------------------------------
/examples/tone.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
18 |
19 |
20 |
--------------------------------------------------------------------------------
/images/volume-off.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/images/volume-on.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "unmute",
3 | "version": "0.1.0",
4 | "description": "start/stop web audio",
5 | "main": "build/unmute.js",
6 | "scripts": {
7 | "watch": "webpack -w",
8 | "build": "webpack -p --env.production",
9 | "build:test": "npm run build && webpack -p --env.test",
10 | "increment": "node scripts/increment_version.js",
11 | "lint": "eslint src/*.js",
12 | "test": "npm run lint && npm run build:test && mocha test/test.js --timeout 30000"
13 | },
14 | "files": [
15 | "README.md",
16 | "LICENSE",
17 | "build/unmute.js.map",
18 | "build/unmute.js",
19 | "src"
20 | ],
21 | "author": "Yotam Mann",
22 | "license": "MIT",
23 | "devDependencies": {
24 | "babel-core": "^6.26.3",
25 | "babel-loader": "^7.1.4",
26 | "chai": "^4.1.2",
27 | "concurrently": "^3.6.1",
28 | "css-loader": "^0.28.11",
29 | "eslint": "^5.2.0",
30 | "html-webpack-plugin": "^3.2.0",
31 | "http-server": "^0.11.1",
32 | "looks-same": "^3.3.0",
33 | "mocha": "^5.2.0",
34 | "node-sass": "^4.9.2",
35 | "puppeteer": "^1.6.1",
36 | "raw-loader": "^0.5.1",
37 | "sass-loader": "^7.0.1",
38 | "semver": "^5.5.0",
39 | "style-loader": "^0.21.0",
40 | "tone": "^13.3.6",
41 | "url-loader": "^1.0.1",
42 | "webpack": "^4.8.3",
43 | "webpack-cli": "^2.1.3"
44 | },
45 | "peerDependencies": {
46 | "tone": ">=13.3.2"
47 | },
48 | "keywords": [
49 | "Web Audio",
50 | "Web Audio API",
51 | "Tone.js",
52 | "Autoplay",
53 | "Mute"
54 | ]
55 | }
56 |
--------------------------------------------------------------------------------
/scripts/increment_version.js:
--------------------------------------------------------------------------------
1 | const fs = require('fs')
2 | const semver = require('semver')
3 | const { resolve } = require('path')
4 | const child_process = require('child_process')
5 |
6 | const publishedVersion = child_process.execSync('npm show unmute version').toString()
7 |
8 | //increment the patch
9 | let version = semver.inc(publishedVersion, 'patch')
10 |
11 | //write it to the package.json
12 | const packageFile = resolve(__dirname, '../package.json')
13 | const packageObj = JSON.parse(fs.readFileSync(packageFile, 'utf-8'))
14 |
15 | //if the package version if the latest, go with that one
16 | if (semver.gt(packageObj.version, version)){
17 | version = packageObj.version
18 | }
19 |
20 | console.log(`incrementing to version ${version}`)
21 | packageObj.version = version
22 | //only if it's travis, update the package.json
23 | if (process.env.TRAVIS){
24 | fs.writeFileSync(packageFile, JSON.stringify(packageObj, undefined, ' '))
25 | }
26 |
--------------------------------------------------------------------------------
/src/AudioContext.js:
--------------------------------------------------------------------------------
1 | import { EventEmitter } from 'events'
2 | import Tone from 'tone/Tone/core/Tone'
3 | import 'tone/Tone/shim/StereoPannerNode'
4 | import 'tone/Tone/core/Context'
5 | import 'tone/Tone/core/Master'
6 |
7 | /**
8 | * Wraps tone and handles mute/unmute and events
9 | */
10 | export class Context extends EventEmitter {
11 | constructor(context, mute, tone){
12 |
13 | super()
14 |
15 | if (tone && tone.context !== Tone.context){
16 | Tone.context = tone.context
17 | } else if (context && context !== Tone.context){
18 | Tone.context = context
19 | }
20 |
21 | /**
22 | * Reference to the wrapper context
23 | * @type {Tone.Context}
24 | */
25 | this.context = Tone.context
26 |
27 | /**
28 | * Reference to the master output.
29 | * @type {Tone.Master}
30 | */
31 | if (tone && tone.Master !== Tone.Master){
32 | this.master = tone.Master
33 | } else {
34 | this.master = this.context.master
35 | }
36 |
37 | //add listeners
38 | this.context.addEventListener('statechange', e => {
39 | this.emit('statechange', e)
40 | })
41 |
42 | //set the initial muted state
43 | this.master.mute = mute
44 |
45 | let currentmute = this.mute
46 | //watch for if it's muted itself
47 | const loop = () => {
48 | requestAnimationFrame(loop)
49 | if (this.mute !== currentmute){
50 | currentmute = this.mute
51 | this.emit('mute', this.mute)
52 | }
53 | }
54 | loop()
55 | }
56 |
57 | get state(){
58 | return this.context.state
59 | }
60 |
61 | get mute(){
62 | return this.master.mute || this.state !== 'running'
63 | }
64 |
65 | set mute(m){
66 | if (this.state === 'running'){
67 | this.master.mute = m
68 | }
69 | }
70 |
71 | resume(){
72 | if (Tone.supported && this.state !== 'running'){
73 | return this.context.resume()
74 | } else {
75 | return Promise.resolve()
76 | }
77 | }
78 |
79 | //promise which resolved when the context is started
80 | started(){
81 | if (this.state === 'running'){
82 | return Promise.resolve()
83 | } else {
84 | return new Promise(done => {
85 | this.on('statechange', () => {
86 | if (this.state === 'running'){
87 | done()
88 | }
89 | })
90 | })
91 | }
92 | }
93 |
94 | toggleMute(){
95 | this.mute = !this.mute
96 | }
97 | }
98 |
--------------------------------------------------------------------------------
/src/AudioElement.js:
--------------------------------------------------------------------------------
1 | const silentAudio = 'data:audio/mp3;base64,//MkxAAHiAICWABElBeKPL/RANb2w+yiT1g/gTok//lP/W/l3h8QO/OCdCqCW2Cw//MkxAQHkAIWUAhEmAQXWUOFW2dxPu//9mr60ElY5sseQ+xxesmHKtZr7bsqqX2L//MkxAgFwAYiQAhEAC2hq22d3///9FTV6tA36JdgBJoOGgc+7qvqej5Zu7/7uI9l//MkxBQHAAYi8AhEAO193vt9KGOq+6qcT7hhfN5FTInmwk8RkqKImTM55pRQHQSq//MkxBsGkgoIAABHhTACIJLf99nVI///yuW1uBqWfEu7CgNPWGpUadBmZ////4sL//MkxCMHMAH9iABEmAsKioqKigsLCwtVTEFNRTMuOTkuNVVVVVVVVVVVVVVVVVVV//MkxCkECAUYCAAAAFVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV'
2 |
3 | /**
4 | * Plays a silent HTMLAudioElement which causes iOS to unmute even if the mute toggle is on
5 | */
6 | export class AudioElement {
7 | constructor(title){
8 |
9 | this.element = document.createElement('audio')
10 | this.element.controls = false
11 | this.element.preload = 'auto'
12 | this.element.loop = false
13 | this.element.src = silentAudio
14 |
15 | //set the title that appears on iOS lock screen
16 | this.element.title = title
17 | }
18 |
19 | click(){
20 | this.element.play()
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/src/ScriptElement.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Handles case where the script is added to the top of the
3 | */
4 |
5 | //this is the script which loaded the UnmuteButton
6 | const currentScript = document.currentScript
7 |
8 | export function addButton(UnmuteButton){
9 | const addButtonAttr = currentScript.getAttribute('data-add-button')
10 | if (currentScript && addButtonAttr === 'true'){
11 | //add it once the window is loaded
12 | const mute = currentScript.getAttribute('data-mute') === 'true'
13 |
14 | //check if the document is already loaded
15 | if (document.readyState === 'complete'){
16 | UnmuteButton({ mute })
17 | } else {
18 | //otherwise add an event listener
19 | window.addEventListener('load', () => UnmuteButton({ mute }))
20 | }
21 | }
22 | }
23 |
24 |
--------------------------------------------------------------------------------
/src/Toggle.js:
--------------------------------------------------------------------------------
1 | import { EventEmitter } from 'events'
2 | import volumeOn from '../images/volume-on.svg'
3 | import volumeOff from '../images/volume-off.svg'
4 |
5 | export class Toggle extends EventEmitter {
6 | constructor(container){
7 | super()
8 |
9 | //create the element
10 | this.element = document.createElement('button')
11 | this.element.id = 'unmute-button'
12 | this.element.setAttribute('aria-pressed', false)
13 | this.element.setAttribute('aria-label', 'mute')
14 |
15 | //add it to the container
16 | if (container !== 'none'){
17 | container.appendChild(this.element)
18 | }
19 |
20 | //forward the events
21 | this.element.addEventListener('click', e => {
22 | this.emit('click', e)
23 | })
24 |
25 | //set it to initially be muted
26 | this.mute = true
27 | }
28 |
29 | get mute(){
30 | return this.element.classList.contains('muted')
31 | }
32 |
33 | set mute(m){
34 | this.element.setAttribute('aria-pressed', m)
35 | if (m){
36 | this.element.classList.add('muted')
37 | this.element.innerHTML = volumeOff
38 | } else {
39 | this.element.classList.remove('muted')
40 | this.element.innerHTML = volumeOn
41 | }
42 | }
43 |
44 | click(){
45 | this.element.click()
46 | }
47 |
48 | /**
49 | * Remove the element from the container
50 | */
51 | remove(){
52 | this.element.remove()
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/src/Unmute.js:
--------------------------------------------------------------------------------
1 | import { Toggle } from './Toggle'
2 | import { Context } from './AudioContext'
3 | import { EventEmitter } from 'events'
4 | import { AudioElement } from './AudioElement'
5 | import { addButton } from './ScriptElement'
6 | import './unmute.scss'
7 |
8 | /**
9 | * @returns EventEmitter
10 | */
11 | class Unmute extends EventEmitter {
12 | constructor({ container=document.body, tone=window.Tone, context=(tone ? tone.context : null), title='Web Audio', mute=false } = {}){
13 | super()
14 |
15 | /**
16 | * The HTML element
17 | * @type {Toggle}
18 | */
19 | this._button = new Toggle(container)
20 |
21 | /**
22 | * Controls the AudioContext
23 | * @type {Context}
24 | */
25 | this._context = new Context(context, mute, tone)
26 |
27 | /**
28 | * AudioElement used to unsilence iOS
29 | * @type {AudioElement}
30 | */
31 | this._audioElement = new AudioElement(title)
32 |
33 | //fwd events from the context
34 | this._context.on('mute', m => {
35 | this._button.mute = m
36 | this.emit(m ? 'mute' : 'unmute')
37 | })
38 |
39 | //listen for click events
40 | this._button.on('click', () => {
41 | if (this._context.state !== 'running'){
42 | this.start()
43 | this.emit('click')
44 | } else {
45 | this._context.toggleMute()
46 | }
47 | })
48 |
49 | //listen for started change
50 | this._context.started().then(() => {
51 | this.emit('start')
52 | })
53 |
54 | //start out in the contexts current state
55 | this._button.mute = this._context.mute
56 | }
57 |
58 | /**
59 | * the mute state of the button
60 | * @type {Boolean}
61 | */
62 | get mute(){
63 | return this._context.mute
64 | }
65 |
66 | set mute(m){
67 | this._button.mute = m
68 | this._context.mute = m
69 | }
70 |
71 | /**
72 | * The HTML element
73 | * @type {HTMLElement}
74 | * @readOnly
75 | */
76 | get element(){
77 | return this._button.element
78 | }
79 |
80 | /**
81 | * The AudioContext reference
82 | * @type {Tone.Context}
83 | * @readOnly
84 | */
85 | get context(){
86 | return this._context.context
87 | }
88 |
89 | /**
90 | * remove the element from the container
91 | */
92 | remove(){
93 | this._button.remove()
94 | }
95 |
96 | /**
97 | * Click on the element. Must come from a trusted MouseEvent to actually unmute the context
98 | */
99 | click(){
100 | this._button.click()
101 | }
102 |
103 | /**
104 | * Start the AudioContext. Must come from a trusted MouseEvent or keyboard event to actually unmute the context.
105 | */
106 | start(){
107 | if (this._context.state !== 'running'){
108 | this._context.resume()
109 | this._audioElement.click()
110 | }
111 | }
112 | }
113 |
114 | export function UnmuteButton(...args){
115 | return new Unmute(...args)
116 | }
117 |
118 | //maybe the button automatically
119 | addButton(UnmuteButton)
120 |
--------------------------------------------------------------------------------
/src/unmute.scss:
--------------------------------------------------------------------------------
1 |
2 | #unmute-button {
3 | --unmute-margin: 8px;
4 | --unmute-size : 24px;
5 |
6 | position: fixed;
7 | right: var(--unmute-margin);
8 | top: var(--unmute-margin);
9 | border-radius: 50%;
10 | width: var(--unmute-size);
11 | height: var(--unmute-size);
12 | background-color: transparent;
13 | border: none;
14 | cursor: pointer;
15 |
16 | svg {
17 | position: absolute;
18 | left: 0px;
19 | top: 0px;
20 | width: 100%;
21 | height: 100%;
22 | }
23 | }
--------------------------------------------------------------------------------
/test/autoadd.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | AUTO ADD TEST
5 |
6 |
7 |
8 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/test/blank.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | TEST
5 |
6 |
7 |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/test/build-test.js:
--------------------------------------------------------------------------------
1 | import UnmuteButton from '../'
2 | import Tone, { Synth, Transport } from 'tone'
3 |
4 | const unmute = UnmuteButton().on('start', () => {
5 | const synth = new Synth().toMaster()
6 |
7 | Transport.scheduleRepeat(time => {
8 | synth.triggerAttackRelease('C4', '8n', time)
9 | }, 0.5)
10 |
11 | Transport.start()
12 | })
13 |
14 | window.SAME_CONTEXT = Tone.context === unmute.context
15 |
--------------------------------------------------------------------------------
/test/events.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | TEST
5 |
6 |
7 |
8 |
27 |
28 |
29 |
--------------------------------------------------------------------------------
/test/indirect_click.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | TEST
5 |
6 |
7 |
8 |
9 |
16 |
17 |
--------------------------------------------------------------------------------
/test/oldtone.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | TEST
5 |
6 |
7 |
8 |
9 |
16 |
17 |
--------------------------------------------------------------------------------
/test/referenceImage.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Tonejs/unmute/cfa012b62af7e48cc906f44d51de687cb9cde19d/test/referenceImage.png
--------------------------------------------------------------------------------
/test/test.js:
--------------------------------------------------------------------------------
1 | const puppeteer = require('puppeteer')
2 | const { resolve } = require('path')
3 | const looksSame = require('looks-same')
4 | const { expect } = require('chai')
5 |
6 | describe('Unmute', () => {
7 |
8 | async function loadPage(url){
9 | /*const browser = await puppeteer.launch({
10 | args : ['--disable-features=MediaEngagementBypassAutoplayPolicies', '--user-gesture-required'],
11 | })*/
12 | const browser = await puppeteer.launch()
13 | const page = await browser.newPage()
14 | await page.goto(`file://${resolve(__dirname, url)}`)
15 | return { page, browser }
16 | }
17 |
18 | it('loads on a page', async () => {
19 | const { browser } = await loadPage('blank.html')
20 | await browser.close()
21 | })
22 |
23 | it('adds a button to the page', async () => {
24 | const { browser, page } = await loadPage('blank.html')
25 | const hasButton = await page.evaluate(() => {
26 | const context = new AudioContext()
27 | UnmuteButton({ context })
28 | return Boolean(document.querySelector('#unmute-button'))
29 | })
30 | expect(hasButton).to.be.true
31 | await page.screenshot({ path : resolve(__dirname, './testCapture.png') })
32 | const similar = await new Promise((done, error) => {
33 | looksSame(resolve(__dirname, './referenceImage.png'), resolve(__dirname, './testCapture.png'), (e, equal) => {
34 | if (e){
35 | error(e)
36 | } else {
37 | done(equal)
38 | }
39 | })
40 | })
41 | expect(similar).to.be.true
42 |
43 | await browser.close()
44 | })
45 |
46 | it('can specify the container', async () => {
47 | const { browser, page } = await loadPage('blank.html')
48 | const correctParent = await page.evaluate(() => {
49 | const context = new AudioContext()
50 | const container = document.createElement('div')
51 | document.body.appendChild(container)
52 | UnmuteButton({ context, container })
53 | return Boolean(document.querySelector('#unmute-button').parentNode === container)
54 | })
55 | expect(correctParent).to.be.true
56 | await browser.close()
57 | })
58 |
59 | it('should initially be muted', async () => {
60 |
61 | const { browser, page } = await loadPage('blank.html')
62 | const context = await page.evaluate(async () => {
63 | const context = new AudioContext()
64 | await context.suspend()
65 | const unmute = UnmuteButton({ context })
66 | const className = unmute.element.classList[0]
67 | return { state : context.state, mute : unmute.mute, className }
68 | })
69 | expect(context.state).to.equal('suspended')
70 | expect(context.className).to.equal('muted')
71 | expect(context.mute).to.be.true
72 | await browser.close()
73 | })
74 |
75 | it('unmutes when clicked on', async () => {
76 | const { browser, page } = await loadPage('blank.html')
77 | //before being clicked
78 | const context = await page.evaluate(async () => {
79 | const context = new AudioContext()
80 | await context.suspend()
81 | window.unmute = UnmuteButton({ context })
82 | return { state : context.state, mute : window.unmute.mute }
83 | })
84 | expect(context.state).to.equal('suspended')
85 | expect(context.mute).to.be.true
86 |
87 | //click and wait a split second
88 | await page.click('#unmute-button')
89 | await page.waitFor(100)
90 |
91 | //check context and state again
92 | const afterClick = await page.evaluate(() => {
93 | return { state : window.unmute.context.state, mute : window.unmute.mute }
94 | })
95 | expect(afterClick.mute).to.be.false
96 | expect(afterClick.state).to.equal('running')
97 | await browser.close()
98 | })
99 |
100 | it('mutes if clicked on again', async () => {
101 | const { browser, page } = await loadPage('blank.html')
102 | await page.evaluate(async () => {
103 | const context = new AudioContext()
104 | await context.suspend()
105 | window.unmute = UnmuteButton({ context })
106 | })
107 |
108 | //unmute
109 | await page.click('#unmute-button')
110 | await page.waitFor(100)
111 | //remute it
112 | await page.click('#unmute-button')
113 | await page.waitFor(100)
114 |
115 | //check context and state again
116 | const afterClick = await page.evaluate(() => {
117 | return { state : window.unmute.context.state, mute : window.unmute.mute }
118 | })
119 | expect(afterClick.mute).to.be.true
120 | expect(afterClick.state).to.equal('running')
121 | await browser.close()
122 | })
123 |
124 | it('can remove the element', async () => {
125 | const { browser, page } = await loadPage('blank.html')
126 | //before being clicked
127 | const button = await page.evaluate(async () => {
128 | const context = new AudioContext()
129 | await context.suspend()
130 | const unmute = UnmuteButton({ context })
131 | unmute.remove()
132 | return Boolean(document.querySelector('#unmute-button'))
133 | })
134 | expect(button).to.be.false
135 | await browser.close()
136 | })
137 |
138 | it('creates an AudioContext if none was passed in', async () => {
139 | const { browser, page } = await loadPage('blank.html')
140 | //before being clicked
141 | const hasContext = await page.evaluate(async () => {
142 | const unmute = UnmuteButton()
143 | return Boolean(unmute.context)
144 | })
145 | expect(hasContext).to.be.true
146 | await browser.close()
147 | })
148 |
149 | it('can be created with no args if tone is on the page', async () => {
150 | const { browser, page } = await loadPage('tone.html')
151 | const sameContext = await page.evaluate(() => {
152 | const unmute = UnmuteButton()
153 | return unmute.context === Tone.context
154 | })
155 | expect(sameContext).to.be.true
156 | await browser.close()
157 | })
158 |
159 | it('works with the latest version of Tone.js with no arguments', async () => {
160 | const { browser, page } = await loadPage('tone_latest.html')
161 | const sameContext = await page.evaluate(() => {
162 | const unmute = UnmuteButton()
163 | return unmute.context === Tone.context
164 | })
165 | expect(sameContext).to.be.true
166 | await browser.close()
167 | })
168 |
169 | it('can be used with an earlier version of Tone.js', async () => {
170 | const { browser, page } = await loadPage('oldtone.html')
171 | //mute the context
172 | const isMuted = await page.evaluate(async () => {
173 | window.unmute.mute = true
174 | await wait(100)
175 | return Tone.Master.mute
176 | })
177 | expect(isMuted).to.be.true
178 |
179 | await browser.close()
180 | })
181 |
182 | it('can start out muted', async () => {
183 | const { browser, page } = await loadPage('tone.html')
184 | const muted = await page.evaluate(() => {
185 | UnmuteButton({ mute : true })
186 | return Tone.Master.mute
187 | })
188 | expect(muted).to.be.true
189 | await browser.close()
190 | })
191 |
192 | it('can add itself to the page', async () => {
193 | const { browser, page } = await loadPage('autoadd.html')
194 | const exists = await page.evaluate(() => {
195 | return Boolean(document.querySelector('#unmute-button'))
196 | })
197 | expect(exists).to.be.true
198 | await browser.close()
199 | })
200 |
201 | it('can mute it from Tone.js or by clicking', async () => {
202 | const { browser, page } = await loadPage('tone.html')
203 | const beforeClicked = await page.evaluate(async () => {
204 | window.unmute = UnmuteButton()
205 | Tone.Master.mute = true
206 | await wait(100)
207 | return window.unmute.mute
208 | })
209 |
210 | expect(beforeClicked).to.be.true
211 |
212 | //click it
213 | await page.click('#unmute-button')
214 | await page.waitFor(100)
215 |
216 | //should be in the opposite state now
217 | const afterClicked = await page.evaluate(() => {
218 | return window.unmute.mute
219 | })
220 | expect(afterClicked).to.be.false
221 |
222 | await browser.close()
223 | })
224 |
225 | it('invokes start, mute, unmute events', async () => {
226 | const { browser, page } = await loadPage('events.html')
227 |
228 | //click it
229 | await page.click('#unmute-button')
230 | await page.waitFor(100)
231 |
232 | //click it again
233 | await page.click('#unmute-button')
234 | await page.waitFor(100)
235 |
236 | const { started, muted, unmuted } = await page.evaluate(() => events)
237 | expect(started).to.be.true
238 | expect(muted).to.be.true
239 | expect(unmuted).to.be.true
240 |
241 | await browser.close()
242 | })
243 |
244 | it('works with a build', async () => {
245 | const { browser, page } = await loadPage('../build/index.html')
246 |
247 | //click it
248 | await page.click('#unmute-button')
249 | await page.waitFor(100)
250 |
251 | const sameContext = await page.evaluate(() => window.SAME_CONTEXT)
252 | expect(sameContext).to.be.true
253 | await browser.close()
254 | })
255 |
256 | it('can click indirectly', async () => {
257 | const { browser, page } = await loadPage('indirect_click.html')
258 |
259 | //click it
260 | await page.click('#indirection')
261 | await page.waitFor(100)
262 |
263 | const isMuted = await page.evaluate(() => window.unmute.mute)
264 | expect(isMuted).to.be.false
265 | await browser.close()
266 | })
267 |
268 | /**
269 | * Helper function to load a test html file and report any errors
270 | */
271 | async function testExample(url){
272 | return await new Promise(async (done, error) => {
273 | const examplePrefix = `file://${resolve(__dirname, '../examples')}`
274 | const browser = await puppeteer.launch()
275 | const page = await browser.newPage()
276 | page.on('pageerror', e => error(e))
277 | await page.goto(`${examplePrefix}/${url}`, { waitFor : 'networkidle0' })
278 | await browser.close()
279 | done()
280 | })
281 | }
282 |
283 | /*it('can run all the examples', async () => {
284 | await testExample('basic.html')
285 | await testExample('context.html')
286 | await testExample('events.html')
287 | await testExample('tone.html')
288 | })*/
289 | })
290 |
--------------------------------------------------------------------------------
/test/tone.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | TEST
5 |
6 |
7 |
12 |
13 |
14 |
15 |
16 |
--------------------------------------------------------------------------------
/test/tone_latest.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | TEST
5 |
6 |
7 |
8 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/webpack.config.js:
--------------------------------------------------------------------------------
1 | const path = require('path')
2 | const HtmlWebpackPlugin = require('html-webpack-plugin')
3 |
4 | const commonConfig = {
5 | mode : 'development',
6 | context : __dirname,
7 | resolve : {
8 | modules : [
9 | 'node_modules',
10 | path.resolve(__dirname, '.'),
11 | ],
12 | /*alias : {
13 | Tone : 'node_modules/tone/Tone'
14 | },*/
15 | },
16 | module : {
17 | rules : [
18 | {
19 | test : /\.js$/,
20 | exclude : /node_modules/,
21 | loader : 'babel-loader'
22 | },
23 | {
24 | test : /\.scss$/,
25 | loader : 'style-loader!css-loader!sass-loader'
26 | },
27 | {
28 | test : /\.(svg)$/,
29 | loader : 'raw-loader'
30 | }
31 | ]
32 | },
33 | devtool : 'source-map'
34 | }
35 |
36 | const libConfig = Object.assign({}, commonConfig, {
37 | entry : {
38 | unmute : './src/Unmute.js'
39 | },
40 | output : {
41 | path : path.resolve(__dirname, 'build'),
42 | filename : '[name].js',
43 | library : 'UnmuteButton',
44 | libraryTarget : 'umd',
45 | libraryExport : 'UnmuteButton'
46 | },
47 | })
48 |
49 | const testConfig = Object.assign({}, commonConfig, {
50 | entry : {
51 | test : './test/build-test.js'
52 | },
53 | output : {
54 | path : path.resolve(__dirname, 'build'),
55 | filename : '[name].js',
56 | },
57 | plugins : [
58 | new HtmlWebpackPlugin()
59 | ]
60 | })
61 |
62 | module.exports = env => {
63 | if (env && env.test){
64 | return testConfig
65 | } else {
66 | return libConfig
67 | }
68 | }
69 |
--------------------------------------------------------------------------------