├── .npmignore ├── .travis.yml ├── LICENSE ├── README.md ├── example ├── compat.html ├── goldensun.mid ├── index.html ├── mario-death.mid ├── server.js └── sound.wav ├── img └── slide.png ├── index.js └── package.json /.npmignore: -------------------------------------------------------------------------------- 1 | .travis.yml 2 | example/ 3 | img/ 4 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - lts/* 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) Feross Aboukhadijeh 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | 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, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # bg-sound 2 | 3 | [![travis][travis-image]][travis-url] [![npm][npm-image]][npm-url] [![downloads][downloads-image]][downloads-url] [![javascript style guide][standard-image]][standard-url] 4 | 5 | [travis-image]: https://img.shields.io/travis/feross/bg-sound/master.svg 6 | [travis-url]: https://travis-ci.org/feross/bg-sound 7 | [npm-image]: https://img.shields.io/npm/v/bg-sound.svg 8 | [npm-url]: https://npmjs.org/package/bg-sound 9 | [downloads-image]: https://img.shields.io/npm/dm/bg-sound.svg 10 | [downloads-url]: https://npmjs.org/package/bg-sound 11 | [standard-image]: https://img.shields.io/badge/code_style-standard-brightgreen.svg 12 | [standard-url]: https://standardjs.com 13 | 14 | ### Web Component to emulate the old-school `` HTML element 15 | 16 | Play MIDI files in a browser with a simple Web Component, emulating 17 | [``, the Background Sound element](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/bgsound). 18 | 19 | ## Install 20 | 21 | ``` 22 | npm install bg-sound 23 | ``` 24 | 25 | This package works in the browser with [browserify](https://browserify.org). If you do not use a bundler, you can use the [standalone script](https://bundle.run/bg-sound) directly in a ` 33 | 34 | ``` 35 | 36 | Automatically make legacy `` and `` HTML tags work: 37 | 38 | ```html 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | ``` 50 | 51 | ## Talk 52 | 53 | I introduced this project in a talk at [JSConf Colombia 2018](https://jsconf.co/). 54 | 55 | [![The Lost Art of MIDI talk](img/slide.png)](https://speakerdeck.com/feross/the-lost-art-of-midi-bringing-back-to-the-web) 56 | 57 | - [Slides](https://speakerdeck.com/feross/the-lost-art-of-midi-bringing-back-to-the-web) 58 | - Talk video (coming soon!) 59 | 60 | ## FAQ 61 | 62 | ### Why is the tag called `` (with a dash)? 63 | 64 | The name of a custom HTML element must contain a dash (-). This is what the spec says, presumably because otherwise browsers could not introduce new HTML tags without web compatibility risk. 65 | 66 | ### Why is the script tag required? 67 | 68 | The script tag is needed to define the behavior of the `` HTML element. Without it, the browser would just treat the tag like a `
`. 69 | 70 | ### Where do the WebAssembly code and instrument sound files come from? 71 | 72 | By default, these files will load remotely from [BitMidi](https://bitmidi.com). This is nice for simple demos and quick hacks. However, it is recommended to host these files yourself. (I reserve the right to remove the CORS headers which allow this to work at any time if too much bandwidth is used.) 73 | 74 | ### What are the `timidity` and `freepats` packages? 75 | 76 | ```bash 77 | npm install timidity freepats 78 | ``` 79 | 80 | The `` custom element lazily loads a WebAssembly file and instrument 81 | sounds at runtime. 82 | 83 | The [`timidity`](https://github.com/feross/timidity) package provides the WebAssembly file (`libtimidity.wasm`). The 84 | `freepats` package provides the instrument sound files. 85 | 86 | It's important to ensure that the `timidity` and `freepats` folders in 87 | `node_modules` are being served to the public. For example, here is how to mount 88 | the necessary files at `/` with the `express` server: 89 | 90 | ```js 91 | const timidityPath = path.dirname(require.resolve('timidity')) 92 | app.use(express.static(timidityPath)) 93 | 94 | const freepatsPath = path.dirname(require.resolve('freepats')) 95 | app.use(express.static(freepatsPath)) 96 | ``` 97 | 98 | Optionally, provide a `baseUrl` attribute to customize where the player will 99 | look for the lazy-loaded WebAssembly file `libtimidity.wasm` and the 100 | [FreePats General MIDI soundset](https://www.npmjs.com/package/freepats) files. 101 | The default `baseUrl` is `https://bitmidi.com/timidity/`. 102 | 103 | ```js 104 | 105 | ``` 106 | 107 | ### How do I automatically make legacy `` and `` tags work? 108 | 109 | Include this code before any `` or `` HTML tags: 110 | 111 | ```html 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | ``` 123 | 124 | If you want to provide your own `baseUrl`, then simply pass that into the `BgSound.enableCompatMode()` function call as follows: 125 | 126 | ```js 127 | BgSound.enableCompatMode({ baseUrl: '/custom-path' }) 128 | ```` 129 | 130 | ## Demo 131 | 132 | If you like this, then check out [BitMidi.com](https://bitmidi.com), the wayback machine for old-school MIDI files! Check out some examples MIDIs here: 133 | 134 | - [Adele - Skyfall](https://bitmidi.com/adele-skyfall-mid) 135 | - [Beatles - Imagine](https://bitmidi.com/beatles-imagine-mid) 136 | - [Beyonce - Crazy in Love](https://bitmidi.com/beyonce-crazy-in-love-mid) 137 | - [CANYON.MID](https://bitmidi.com/canyon-mid) 138 | - [Cowboy Bepop - Space Lion](https://bitmidi.com/cowboy-bepop-space-lion-mid) 139 | - [Eiffel 65 - Blue](https://bitmidi.com/dj-ali-eiffel-blue-mid) 140 | - [Kingdom Hearts - Dearly Beloved](https://bitmidi.com/kingdom-hearts-dearly-beloved-mid) 141 | - [Mario Bros. - Super Mario Bros. Theme](https://bitmidi.com/mario-bros-super-mario-bros-theme-mid) 142 | - [Passenger - Let Her Go](https://bitmidi.com/passenger-let_her_go-mid) 143 | - [Portal - Still Alive](https://bitmidi.com/portal-still-alive-mid) 144 | - [Rick Astley - Never Gonna Give You Up](https://bitmidi.com/r-astley-never-gonna-give-you-up-k-mid) 145 | - [Simpsons Theme Song](https://bitmidi.com/simpsons-mid) 146 | - [Sonic the Hedgehog - Green Hill Zone](https://bitmidi.com/sonic-the-hedgehog-green-hill-zone-mid) 147 | - [TOTO - Africa](https://bitmidi.com/toto-africa-k-mid) 148 | 149 | ## License 150 | 151 | MIT. Copyright (c) [Feross Aboukhadijeh](https://feross.org). 152 | -------------------------------------------------------------------------------- /example/compat.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /example/goldensun.mid: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/feross/bg-sound/352536e55b32049bd6754f5edd0c267430e1ac0f/example/goldensun.mid -------------------------------------------------------------------------------- /example/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /example/mario-death.mid: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/feross/bg-sound/352536e55b32049bd6754f5edd0c267430e1ac0f/example/mario-death.mid -------------------------------------------------------------------------------- /example/server.js: -------------------------------------------------------------------------------- 1 | const express = require('express') 2 | const path = require('path') 3 | 4 | const PORT = 4000 5 | const ROOT_PATH = path.join(__dirname, '..') 6 | 7 | const app = express() 8 | 9 | app.use(express.static(path.join(ROOT_PATH, 'example'))) 10 | app.use(express.static(ROOT_PATH)) 11 | 12 | const timidityPath = path.dirname(require.resolve('timidity')) 13 | app.use(express.static(timidityPath)) 14 | 15 | const freepatsPath = path.dirname(require.resolve('freepats')) 16 | app.use(express.static(freepatsPath)) 17 | 18 | app.listen(PORT, () => console.log(`Listening on port ${PORT}`)) 19 | -------------------------------------------------------------------------------- /example/sound.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/feross/bg-sound/352536e55b32049bd6754f5edd0c267430e1ac0f/example/sound.wav -------------------------------------------------------------------------------- /img/slide.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/feross/bg-sound/352536e55b32049bd6754f5edd0c267430e1ac0f/img/slide.png -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | /* global HTMLElement */ 2 | 3 | const debug = require('debug')('bg-sound') 4 | const Timidity = require('timidity') 5 | const insertCss = require('insert-css') 6 | const whenDomReady = require('when-dom-ready') 7 | 8 | class BgSound extends HTMLElement { 9 | static get observedAttributes () { 10 | return ['src', 'baseUrl', 'loop'] 11 | } 12 | 13 | constructor () { 14 | super() 15 | 16 | this._playingFired = false 17 | this._onClick = this._onClick.bind(this) 18 | this._onPlaying = this._onPlaying.bind(this) 19 | this._onEnded = this._onEnded.bind(this) 20 | } 21 | 22 | connectedCallback () { 23 | document.addEventListener('click', this._onClick) 24 | 25 | if (!this.hasAttribute('baseUrl')) { 26 | this.setAttribute('baseUrl', 'https://bitmidi.com/timidity/') 27 | } 28 | 29 | if (!this.hasAttribute('loop')) { 30 | this.setAttribute('loop', 0) 31 | } 32 | 33 | this.playCount = 0 34 | this._initPlayer() 35 | } 36 | 37 | disconnectedCallback () { 38 | document.removeEventListener('click', this._onClick) 39 | this._destroyPlayer() 40 | } 41 | 42 | get src () { 43 | return this.getAttribute('src') 44 | } 45 | 46 | set src (val) { 47 | this.setAttribute('src', val) 48 | } 49 | 50 | get loop () { 51 | return this.getAttribute('loop') 52 | } 53 | 54 | set loop (val) { 55 | this.setAttribute('loop', val) 56 | } 57 | 58 | get baseUrl () { 59 | return this.getAttribute('baseUrl') 60 | } 61 | 62 | set baseUrl (val) { 63 | this.setAttribute('baseUrl', val) 64 | } 65 | 66 | attributeChangedCallback (name, oldValue, newValue) { 67 | debug(`${name} changed from ${oldValue} to ${newValue}`) 68 | if (name === 'src') { 69 | // TODO 70 | // if (oldValue != null) { 71 | // this.player.pause() 72 | // this.player.load(newValue) 73 | // this.player.play() 74 | // } 75 | } 76 | if (name === 'baseUrl') { 77 | // TODO 78 | } 79 | } 80 | 81 | _initPlayer () { 82 | this.player = new Timidity(this.baseUrl) 83 | this.player.once('playing', this._onPlaying) 84 | this.player.once('ended', this._onEnded) 85 | this.player.load(this.src) 86 | this.player.play() 87 | } 88 | 89 | _destroyPlayer () { 90 | this.player.pause() 91 | this.player.destroy() 92 | this.player.removeListener('playing', this._onPlaying) 93 | this.player.removeListener('ended', this._onEnded) 94 | this.player = null 95 | } 96 | 97 | _onPlaying () { 98 | this._playingFired = true 99 | } 100 | 101 | _onEnded () { 102 | this._destroyPlayer() 103 | this.playCount += 1 104 | 105 | const loop = String(this.loop).toLowerCase() 106 | if (loop === 'infinite' || loop === 'true' || loop === '-1' || 107 | Number(this.loop) > this.playCount) { 108 | this._initPlayer() 109 | } 110 | } 111 | 112 | _onClick () { 113 | if (!this._playingFired) this.player.play() 114 | } 115 | } 116 | 117 | window.customElements.define('bg-sound', BgSound) 118 | 119 | function enableCompatMode (opts = {}) { 120 | insertCss(` 121 | embed { 122 | display: none; 123 | } 124 | `) 125 | 126 | whenDomReady().then(() => { 127 | const embeds = [ 128 | ...document.querySelectorAll('embed'), 129 | ...document.querySelectorAll('bgsound') 130 | ] 131 | embeds.forEach(embed => { 132 | let src = embed.getAttribute('src') 133 | const loop = embed.getAttribute('loop') 134 | embed.remove() 135 | 136 | src = new URL(src, window.location.href).href 137 | console.log(src) 138 | 139 | if (!src) return 140 | 141 | if (src.endsWith('.mid') || src.endsWith('.midi')) { 142 | const bgSound = document.createElement('bg-sound') 143 | bgSound.setAttribute('src', src) 144 | if (loop) bgSound.setAttribute('loop', loop) 145 | if (opts.baseUrl) bgSound.setAttribute('baseUrl', opts.baseUrl) 146 | 147 | document.body.appendChild(bgSound) 148 | } 149 | 150 | if (src.endsWith('.wav')) { 151 | const audio = document.createElement('audio') 152 | audio.src = src 153 | audio.controls = false 154 | audio.autoplay = true 155 | if (loop && (loop.toLowerCase() === 'infinite' || loop === 'true' || 156 | loop === '-1' || Number(loop) >= 2)) { 157 | audio.loop = true 158 | } 159 | 160 | let playingFired = false 161 | audio.addEventListener('playing', () => { 162 | playingFired = true 163 | }) 164 | document.body.addEventListener('click', () => { 165 | if (!playingFired) audio.play() 166 | }) 167 | document.body.appendChild(audio) 168 | } 169 | }) 170 | }) 171 | } 172 | 173 | module.exports = BgSound 174 | module.exports.enableCompatMode = enableCompatMode 175 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "bg-sound", 3 | "description": "Web Component to emulate the old-school HTML element", 4 | "version": "2.0.0", 5 | "author": { 6 | "name": "Feross Aboukhadijeh", 7 | "email": "feross@feross.org", 8 | "url": "https://feross.org" 9 | }, 10 | "bugs": { 11 | "url": "https://github.com/feross/bg-sound/issues" 12 | }, 13 | "dependencies": { 14 | "debug": "^4.1.1", 15 | "insert-css": "^2.0.0", 16 | "timidity": "^1.1.1", 17 | "when-dom-ready": "^1.2.12" 18 | }, 19 | "devDependencies": { 20 | "babel-minify": "^0.5.1", 21 | "browserify": "^16.2.2", 22 | "express": "^4.16.4", 23 | "freepats": "^1.0.2", 24 | "standard": "*" 25 | }, 26 | "homepage": "https://bitmidi.com", 27 | "keywords": [ 28 | "background sound", 29 | "bg sound", 30 | "bg-sound", 31 | "bgsound", 32 | "browser", 33 | "mid file", 34 | "midi", 35 | "midi file", 36 | "midi player", 37 | "player", 38 | "web audio", 39 | "web component", 40 | "webcomponent" 41 | ], 42 | "license": "MIT", 43 | "main": "index.js", 44 | "repository": { 45 | "type": "git", 46 | "url": "git://github.com/feross/bg-sound.git" 47 | }, 48 | "scripts": { 49 | "size": "browserify -s BgSound -r . | minify | gzip | wc -c", 50 | "example": "node example/server.js", 51 | "test": "standard" 52 | } 53 | } 54 | --------------------------------------------------------------------------------