├── .github └── workflows │ ├── build.yml │ └── release.yml ├── .gitignore ├── LICENSE ├── README.md ├── doc ├── midi-player.md └── midi-visualizer.md ├── index.html ├── jazz.mid ├── package-lock.json ├── package.json ├── rollup.config.js ├── src ├── assets │ ├── controls.scss │ ├── error.svg │ ├── imports.d.ts │ ├── index.ts │ ├── pause.svg │ ├── play.svg │ └── visualizer.scss ├── index.ts ├── player.ts ├── utils.ts └── visualizer.ts ├── tsconfig.json └── twinkle_twinkle.mid /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | jobs: 10 | build: 11 | runs-on: ubuntu-latest 12 | 13 | steps: 14 | - uses: actions/checkout@v4 15 | 16 | - name: Install 17 | run: | 18 | cd $GITHUB_WORKSPACE 19 | yarn install 20 | 21 | - name: Build 22 | run: yarn build 23 | 24 | - name: Upload 25 | uses: actions/upload-artifact@v4 26 | with: 27 | name: dist 28 | path: dist/ 29 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | release: 5 | types: 6 | - published 7 | 8 | jobs: 9 | dist: 10 | runs-on: ubuntu-latest 11 | 12 | steps: 13 | - uses: actions/checkout@v4 14 | 15 | - name: Install 16 | run: | 17 | cd $GITHUB_WORKSPACE 18 | yarn install 19 | - name: Build 20 | run: yarn build 21 | 22 | - name: Upload 23 | uses: actions/upload-release-asset@v1.0.2 24 | env: 25 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 26 | with: 27 | upload_url: ${{ github.event.release.upload_url }} 28 | asset_path: dist/midi-player.js 29 | asset_name: midi-player.js 30 | asset_content_type: application/javascript 31 | 32 | - name: Upload minified 33 | uses: actions/upload-release-asset@v1.0.2 34 | env: 35 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 36 | with: 37 | upload_url: ${{ github.event.release.upload_url }} 38 | asset_path: dist/midi-player.min.js 39 | asset_name: midi-player.min.js 40 | asset_content_type: application/javascript 41 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | *.log 3 | yarn-debug.log* 4 | yarn-error.log* 5 | .cache 6 | dist 7 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 2-Clause License 2 | 3 | Copyright (c) 2020, Ondřej Cífka 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | 1. Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | 2. Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 17 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 18 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 19 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 20 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 21 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 22 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 23 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 24 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 25 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 26 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # html-midi-player 2 | 3 | [![npm package](https://badge.fury.io/js/html-midi-player.svg)](https://badge.fury.io/js/html-midi-player) 4 | [![npm package daily downloads](https://badgen.net/npm/dm/html-midi-player)](https://npmjs.com/package/html-midi-player) 5 | [![jsDelivr](https://data.jsdelivr.com/v1/package/npm/html-midi-player/badge?style=rounded)](https://www.jsdelivr.com/package/npm/html-midi-player) 6 | [![Published on webcomponents.org](https://img.shields.io/badge/webcomponents.org-published-blue.svg)](https://www.webcomponents.org/element/html-midi-player) 7 | 8 | [``](https://github.com/cifkao/html-midi-player/blob/master/doc/midi-player.md#midi-player) 9 | and [``](https://github.com/cifkao/html-midi-player/blob/master/doc/midi-visualizer.md#midi-visualizer) 10 | HTML elements powered by [@magenta/music](https://github.com/magenta/magenta-js/tree/master/music/) (Magenta.js), fully stylable and scriptable. 11 | 12 | Works in Jupyter notebooks, Colab, and Weights & Biases thanks to the [midi-player](https://github.com/drscotthawley/midi-player) Python package by [@drscotthawley](https://github.com/drscotthawley). 13 | 14 | * [Simple demo](https://codepen.io/cifkao/pen/WNwpLzL) 15 | * [Advanced demo](https://codepen.io/cifkao/pen/GRZxqZN) 16 | * [Website](https://cifkao.github.io/html-midi-player/) [[source](https://github.com/cifkao/html-midi-player/tree/www)] with MIDI file upload 17 | 18 | Notable websites that use `html-midi-player` include [abcnotation.com](https://abcnotation.com/), [Musical Nexus](https://musicalnexus.net/) and demo websites for music generation models: [piano infilling](https://jackyhsiung.github.io/piano-infilling-demo/), [stochastic positional encoding](https://cifkao.github.io/spe/). 19 | 20 | If you use `html-midi-player` on your website, please consider linking back to [the repository](https://github.com/cifkao/html-midi-player/). 21 | 22 | ## Getting started 23 | 24 | 1. Add the necessary scripts to your page: 25 | 26 | ```html 27 | 28 | ``` 29 | 30 | 2. Add a player and a visualizer: 31 | 32 | 42 | ```html 43 | 46 | 47 | 48 | ``` 49 | 50 | That's it! 51 | 52 | Besides [jsDelivr](https://www.jsdelivr.com/package/npm/html-midi-player), the bundle is also available from [cdnjs](https://cdnjs.com/libraries/html-midi-player). 53 | 54 | ### Installing from NPM 55 | 56 | You can also add the package to your project from NPM, e.g. `npm install --save html-midi-player` or `yarn add html-midi-player`. Then you can either: 57 | - `import 'html-midi-player'` in your JavaScript code (as an ES Module), or 58 | - add the `node_modules/html-midi-player/dist/midi-player.min.js` bundle directly to your page, along with the dependencies (`node_modules/tone/build/Tone.js`, `node_modules/@magenta/music/es6/core.js`; note that these need to go *before* `html-midi-player`). 59 | 60 | In both cases, you should also add the [`focus-visible` polyfill](https://github.com/WICG/focus-visible) to enable outlines on keyboard focus. 61 | 62 | ## API basics 63 | 64 | The basic features of `html-midi-player` are explained below. Wherever both HTML and JavaScript examples are given, they are equivalent. In the JavaScript examples, `player` and `visualizer` refer to the corresponding custom element objects, which can be obtained using standard DOM methods like `document.getElementById('#myPlayer')` or `document.querySelectorAll('midi-player')`, for example. 65 | 66 | See also the API reference for both elements: 67 | [`midi-player`](https://github.com/cifkao/html-midi-player/blob/master/doc/midi-player.md#midi-player), 68 | [`midi-visualizer`](https://github.com/cifkao/html-midi-player/blob/master/doc/midi-visualizer.md#midi-visualizer). 69 | 70 | ### `src` and `noteSequence` 71 | Both `midi-player` and `midi-visualizer` support two different ways of specifying the input file: 72 | - By setting the `src` attribute to a MIDI file URL, e.g.: 73 | ```html 74 | 75 | ``` 76 | ```javascript 77 | player.src = "twinkle-twinkle.mid"; 78 | ``` 79 | - By assigning a Magenta [`NoteSequence`](https://hello-magenta.glitch.me/#playing-a-notesequence) to the `noteSequence` property, e.g.: 80 | ```javascript 81 | player.noteSequence = TWINKLE_TWINKLE; 82 | ``` 83 | To obtain a `NoteSequence`, you can use Magenta functions like [`urlToNoteSequence()`](https://magenta.github.io/magenta-js/music/modules/_core_.html#urltonotesequence) (see [FAQ](#how-do-i-create-and-manipulate-notesequences)). 84 | 85 | ### SoundFonts 86 | By default, the player will use a simple oscillator synth. To use a SoundFont, add the `sound-font` attribute: 87 | ```html 88 | 89 | 90 | ``` 91 | ```javascript 92 | player.soundFont = null; // no SoundFont 93 | player.soundFont = ''; // default SoundFont (same as below) 94 | player.soundFont = 'https://storage.googleapis.com/magentadata/js/soundfonts/sgm_plus'; 95 | ``` 96 | See the [Magenta.js docs](https://magenta.github.io/magenta-js/music/index.html#soundfonts) for a list of available SoundFonts. 97 | 98 | ### Looping 99 | To make the player loop, use the `loop` attribute: 100 | ```html 101 | 102 | ``` 103 | ```javascript 104 | player.loop = true; 105 | ``` 106 | 107 | ### Visualizer settings 108 | The visualizer type is specified via the `type` attribute. Three visualizer types are supported: `piano-roll`, `waterfall` and `staff`. 109 | 110 | Each visualizer type has a set of settings that can be specified using the `config` attribute (only from JavaScript), e.g.: 111 | ```javascript 112 | visualizer.config = { 113 | noteHeight: 4, 114 | pixelsPerTimeStep: 60, 115 | minPitch: 30 116 | }; 117 | ``` 118 | The settings are documented [in the Magenta.js docs](https://magenta.github.io/magenta-js/music/interfaces/_core_visualizer_.visualizerconfig.html). 119 | 120 | ### Binding visualizers 121 | A player supports binding one or more visualizers to it using the `visualizer` attribute (a selector) or the `addVisualizer` method: 122 | ```html 123 | 124 | ``` 125 | ```javascript 126 | player.addVisualizer(document.getElementById('myVisualizer')); 127 | player.addVisualizer(document.getElementById('myOtherVisualizer')); 128 | ``` 129 | The visualizer only gets updated while the player is playing, which allows a single visualizer to be bound to multiple players. 130 | 131 | ### Events 132 | The player supports listening to different kinds of events using the `player.addEventListener()` method. See the [API reference](https://github.com/cifkao/html-midi-player/blob/master/doc/midi-player.md#events) for the available event types. 133 | 134 | ## FAQ 135 | Here are some frequently asked questions about `html-midi-player`. Make sure to also check [discussions](https://github.com/cifkao/html-midi-player/discussions) and [issues](https://github.com/cifkao/html-midi-player/issues?q=is%3Aissue) to see if your question is answered there. 136 | 137 | ### Why is my MIDI file not loading? 138 | Please make sure that you provide a valid HTTP(S) URL, either absolute (beginning with `https://` or `http://`) or relative with respect to your HTML file (if hosted on the same server). 139 | 140 | Local files most likely **will not work**, as most browsers block requests to local files. To test the MIDI player locally, you will need to start an HTTP server to host your MIDI file; see for example [this tutorial](https://developer.mozilla.org/en-US/docs/Learn/Common_questions/Tools_and_setup/set_up_a_local_testing_server#running_a_simple_local_http_server) for easy ways to do that. 141 | 142 | To diagnose why the MIDI file is not loading, hover over the error icon to see the error; if the message is "Bad MIDI file. Expected 'MHdr'", it means either your file is not a valid MIDI file, or the server cannot find your file and is serving an error page instead. To see the file actually being served, open your browser's Developer Tools, go to the Network tab, reload the page, then find the name of your file in the list. 143 | 144 | ### How can I use custom samples? 145 | The player supports "SoundFonts" in a [special format](https://github.com/magenta/magenta-js/blob/d8a76682abb0979b985e4b80f6b68b5123b9f8d5/music/src/core/soundfont.ts#L381-L419) designed by Magenta. If you want to use a .sf2 file, it will not work out of the box, but it is possible to convert it with some effort. See [this discussion thread](https://github.com/cifkao/html-midi-player/discussions/43) and especially [this answer](https://github.com/cifkao/html-midi-player/discussions/43#discussioncomment-5439676), which proposes a conversion script. 146 | 147 | ### How do I create and manipulate `NoteSequence`s? 148 | The Magenta.js [`core`](https://magenta.github.io/magenta-js/music/modules/_core_.html) and [`core/sequences`](https://magenta.github.io/magenta-js/music/modules/_core_sequences_.html) modules define functions for loading and manipulating `NoteSequence`s. To load a MIDI file as a `NoteSequence`, use the `urlToNoteSequence()` function. Other useful functions are `clone()`, `trim()` and `concatenate()`. 149 | 150 | If you are using the provided bundle as suggested [above](#getting-started), then the `core` module will be available simply as `core`, so you will be able to call e.g. `core.urlToNoteSequence()` or `core.sequences.clone()` from your code. 151 | 152 | ### Can you implement feature X or fix issue Y? 153 | This library is a relatively thin wrapper around [Magenta.js](https://github.com/magenta/magenta-js/), which provides all of the MIDI loading, synthesis and visualization functionality. This means it inherits most of its limitations. If you found an issue, try to check if Magenta.js is also affected, e.g. using [this](https://magenta.github.io/magenta-js/music/demos/player.html) or [this](https://magenta.github.io/magenta-js/music/demos/visualizer.html) demo (click *Load MIDI File* to upload your own file). If the issue is still there, then this is most likely a Magenta.js issue and cannot be fixed here (although a workaround may be possible). Otherwise, feel free to open an issue (or even better, a pull request) here, but please check for existing [issues](https://github.com/cifkao/html-midi-player/issues?q=is%3Aissue) and [discussions](https://github.com/cifkao/html-midi-player/discussions) first. 154 | 155 | ## Limitations 156 | - Only one player can play at a time. Starting a player will stop any other player which is currently playing. ([#1](https://github.com/cifkao/html-midi-player/issues/1)) 157 | This can actually be a benefit in many cases. 158 | - Playback position only gets updated on note onsets. This may cause the player to appear stuck. 159 | -------------------------------------------------------------------------------- /doc/midi-player.md: -------------------------------------------------------------------------------- 1 | # midi-player 2 | 3 | MIDI player element. 4 | See also the [`@magenta/music/core/player` docs](https://magenta.github.io/magenta-js/music/modules/_core_player_.html). 5 | 6 | The element supports styling using the CSS [`::part` syntax](https://developer.mozilla.org/docs/Web/CSS/::part) 7 | (see the list of shadow parts [below](#css-shadow-parts)). For example: 8 | ```css 9 | midi-player::part(control-panel) { 10 | background: aquamarine; 11 | border-radius: 0px; 12 | } 13 | ``` 14 | 15 | ## Attributes 16 | 17 | | Attribute | Description | 18 | |--------------|--------------------------------------------------| 19 | | `visualizer` | A selector matching `midi-visualizer` elements to bind to this player | 20 | 21 | ## Properties 22 | 23 | | Property | Attribute | Type | Description | 24 | |----------------|--------------|-------------------------|--------------------------------------------------| 25 | | `currentTime` | | `number` | Current playback position in seconds | 26 | | `duration` | | `number` | Content duration in seconds | 27 | | `loop` | `loop` | `boolean` | Indicates whether the player should loop | 28 | | `noteSequence` | | `INoteSequence \| null` | Magenta note sequence object representing the currently loaded content | 29 | | `playing` | | `boolean` | Indicates whether the player is currently playing | 30 | | `soundFont` | `sound-font` | `string \| null` | Magenta SoundFont URL, an empty string to use the default SoundFont, or `null` to use a simple oscillator synth | 31 | | `src` | `src` | `string \| null` | MIDI file URL | 32 | 33 | ## Methods 34 | 35 | | Method | Type | 36 | |--------------------|-----------------------------------------| 37 | | `addVisualizer` | `(visualizer: VisualizerElement): void` | 38 | | `reload` | `(): void` | 39 | | `removeVisualizer` | `(visualizer: VisualizerElement): void` | 40 | | `start` | `(): void` | 41 | | `stop` | `(): void` | 42 | 43 | ## Events 44 | 45 | | Event | Type | Description | 46 | |---------|---------------------------------------|--------------------------------------------------| 47 | | `load` | | The content is loaded and ready to play | 48 | | `loop` | | The player has automatically restarted playback after reaching the end | 49 | | `note` | `CustomEvent<{ note: INote; }>` | A note starts | 50 | | `start` | | The player has started playing | 51 | | `stop` | `CustomEvent<{ finished: boolean; }>` | The player has stopped playing | 52 | 53 | ## CSS Shadow Parts 54 | 55 | | Part | Description | 56 | |-------------------|--------------------------------------------------| 57 | | `control-panel` | `
` containing all the controls | 58 | | `current-time` | Elapsed time | 59 | | `loading-overlay` | Overlay with shimmer animation | 60 | | `play-button` | Play button | 61 | | `seek-bar` | `` showing playback position | 62 | | `time` | Numeric time indicator | 63 | | `total-time` | Total duration | 64 | -------------------------------------------------------------------------------- /doc/midi-visualizer.md: -------------------------------------------------------------------------------- 1 | # midi-visualizer 2 | 3 | MIDI visualizer element. 4 | 5 | The visualizer is implemented via SVG elements which support styling as described 6 | [here](https://magenta.github.io/magenta-js/music/demos/visualizer.html). 7 | 8 | See also the 9 | [`@magenta/music/core/visualizer` docs](https://magenta.github.io/magenta-js/music/modules/_core_visualizer_.html). 10 | 11 | ## Properties 12 | 13 | | Property | Attribute | Type | Description | 14 | |----------------|-----------|------------------------------------------|--------------------------------------------------| 15 | | `config` | | `VisualizerConfig` | Magenta visualizer config object | 16 | | `noteSequence` | | `INoteSequence \| null` | Magenta note sequence object representing the currently displayed content | 17 | | `src` | `src` | `string \| null` | MIDI file URL | 18 | | `type` | `type` | `"piano-roll" \| "waterfall" \| "staff"` | Visualizer type | 19 | 20 | ## Methods 21 | 22 | | Method | Type | 23 | |--------------------|-------------------------------------------| 24 | | `clearActiveNotes` | `(): void` | 25 | | `redraw` | `(activeNote?: INote \| undefined): void` | 26 | | `reload` | `(): void` | 27 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | HTML MIDI Player 6 | 7 | 8 | 9 | 10 | 17 | 18 | 19 |

HTML MIDI Player

20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 45 | 46 | 47 | -------------------------------------------------------------------------------- /jazz.mid: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cifkao/html-midi-player/85544e6276ecd0e778835a1c7732910f4eb0cf6f/jazz.mid -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "html-midi-player", 3 | "description": "MIDI file player and visualizer web components", 4 | "version": "1.5.0", 5 | "author": "Ondřej Cífka", 6 | "license": "BSD-2-Clause", 7 | "homepage": "https://github.com/cifkao/html-midi-player#readme", 8 | "repository": { 9 | "type": "git", 10 | "url": "https://github.com/cifkao/html-midi-player.git" 11 | }, 12 | "bugs": { 13 | "url": "https://github.com/cifkao/html-midi-player/issues" 14 | }, 15 | "keywords": [ 16 | "midi", 17 | "midi-file", 18 | "player", 19 | "music", 20 | "html", 21 | "soundfont", 22 | "web-components", 23 | "webcomponents" 24 | ], 25 | "type": "module", 26 | "main": "dist/esm/index.js", 27 | "module": "dist/esm/index.js", 28 | "types": "dist/esm/index.d.ts", 29 | "jsdelivr": "dist/midi-player.js", 30 | "unpkg": "dist/midi-player.min.js", 31 | "files": [ 32 | "dist/**/*", 33 | "README.md", 34 | "LICENSE", 35 | "index.html" 36 | ], 37 | "scripts": { 38 | "prebuild": "rm -rf dist", 39 | "build": "rollup -c && tsc -d --emitDeclarationOnly --declarationDir dist/esm", 40 | "prestart": "rm -rf dist", 41 | "start": "rollup -c --watch --plugin dev", 42 | "build:doc": "wca src/index.ts --outFiles doc/{tagname}.md" 43 | }, 44 | "devDependencies": { 45 | "@babel/core": "^7.0.0", 46 | "@babel/plugin-transform-modules-umd": "^7.10.4", 47 | "@babel/preset-env": "^7.11.0", 48 | "@rollup/plugin-babel": "^5.2.0", 49 | "@rollup/plugin-typescript": "^5.0.2", 50 | "@types/node": "^17.0.45", 51 | "focus-visible": "^5.1.0", 52 | "rollup": "^2.26.4", 53 | "rollup-plugin-dev": "^1.1.2", 54 | "rollup-plugin-sass": "^1.2.2", 55 | "rollup-plugin-string": "^3.0.0", 56 | "rollup-plugin-terser": "^7.0.0", 57 | "sass": "^1.26.10", 58 | "tslib": "^2.0.1", 59 | "typescript": "^3.9.7", 60 | "web-component-analyzer": "^1.1.6" 61 | }, 62 | "dependencies": { 63 | "@magenta/music": "^1.22.1" 64 | }, 65 | "overrides": { 66 | "ndarray-resample": "npm:dry-uninstall@0.3.0" 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import sass from 'rollup-plugin-sass'; 2 | import typescript from '@rollup/plugin-typescript'; 3 | import { terser } from 'rollup-plugin-terser'; 4 | import { string } from 'rollup-plugin-string'; 5 | import babel from '@rollup/plugin-babel'; 6 | 7 | import pkg from './package.json'; 8 | 9 | const banner = 10 | `/** 11 | * ${pkg.name}@${pkg.version} 12 | * ${pkg.repository.url} 13 | * @author ${pkg.author} (@cifkao) 14 | * @license ${pkg.license} 15 | */ 16 | `; 17 | 18 | const commonPlugins = [ 19 | sass(), 20 | string({ 21 | include: ['src/assets/**/*.svg'] 22 | }), 23 | ]; 24 | 25 | const umdOutOptions = { 26 | format: 'umd', 27 | name: 'midiPlayer', 28 | globals: { 29 | '@magenta/music/esm/core.js': 'core' 30 | }, 31 | banner: banner 32 | }; 33 | 34 | export default [ 35 | { 36 | input: 'src/index.ts', 37 | output: { 38 | file: pkg.module, 39 | format: 'es', 40 | banner: banner, 41 | }, 42 | plugins: [ 43 | typescript({ 44 | target: 'es2017' 45 | }), 46 | ...commonPlugins 47 | ], 48 | external: [ 49 | '@magenta/music/esm/core.js', 50 | 'tslib' 51 | ] 52 | }, 53 | 54 | { 55 | input: 'src/index.ts', 56 | output: [ 57 | { 58 | file: 'dist/midi-player.js', 59 | ...umdOutOptions, 60 | }, 61 | { 62 | file: 'dist/midi-player.min.js', 63 | sourcemap: true, 64 | ...umdOutOptions, 65 | plugins: [ 66 | terser() 67 | ] 68 | }, 69 | ], 70 | plugins: [ 71 | typescript({ 72 | target: 'esnext' 73 | }), 74 | babel({ 75 | extensions: ['.ts', '.js'], 76 | presets: [ 77 | [ 78 | '@babel/preset-env', { 79 | targets: 'supports audio-api and supports custom-elementsv1 and supports shadowdomv1' 80 | } 81 | ], 82 | ], 83 | babelHelpers: 'bundled' 84 | }), 85 | ...commonPlugins 86 | ], 87 | external: [ 88 | '@magenta/music/esm/core.js' 89 | ] 90 | } 91 | ] 92 | -------------------------------------------------------------------------------- /src/assets/controls.scss: -------------------------------------------------------------------------------- 1 | :host { 2 | display: inline-block; 3 | width: 300px; 4 | margin: 3px; 5 | vertical-align: bottom; 6 | font-family: sans-serif; 7 | font-size: 14px; 8 | } 9 | 10 | :focus:not(.focus-visible) { 11 | outline: none; 12 | } 13 | 14 | .controls { 15 | width: inherit; 16 | height: inherit; 17 | box-sizing: border-box; 18 | display: flex; 19 | flex-direction: row; 20 | position: relative; 21 | overflow: hidden; 22 | 23 | align-items: center; 24 | border-radius: 100px; 25 | background: #f2f5f6; 26 | padding: 0 0.25em; 27 | user-select: none; 28 | 29 | &> * { 30 | margin: 0.8em 0.45em; 31 | } 32 | 33 | input, button { 34 | cursor: pointer; 35 | 36 | &:disabled { 37 | cursor: inherit; 38 | } 39 | } 40 | 41 | button { 42 | text-align: center; 43 | background: rgba(#ccc, 0); 44 | border: none; 45 | width: 32px; 46 | height: 32px; 47 | border-radius: 100%; 48 | transition: background-color 0.25s ease 0s; 49 | padding: 0; 50 | 51 | &:not(:disabled):hover { 52 | background: rgba(#ccc, 0.3); 53 | } 54 | 55 | &:not(:disabled):active { 56 | background: rgba(#ccc, 0.6); 57 | } 58 | 59 | .icon { 60 | display: none; 61 | 62 | &, svg { 63 | vertical-align: middle; 64 | } 65 | 66 | svg { 67 | fill: currentColor; 68 | } 69 | } 70 | } 71 | 72 | .seek-bar { 73 | flex: 1; 74 | min-width: 0; 75 | margin-right: 1.1em; 76 | 77 | background: transparent; 78 | 79 | &::-moz-range-track { 80 | // For some reason, the track is invisible in Firefox by default 81 | background-color: #555; 82 | } 83 | } 84 | 85 | &.stopped .play-icon, &.playing .stop-icon, &.error .error-icon { 86 | display: inherit; 87 | } 88 | 89 | &.frozen > div, &> button:disabled .icon { 90 | opacity: 0.5; 91 | } 92 | 93 | .overlay { 94 | z-index: 0; 95 | position: absolute; 96 | top: 0; 97 | left: 0; 98 | right: 0; 99 | bottom: 0; 100 | margin: 0; 101 | box-sizing: border-box; 102 | display: none; 103 | opacity: 1; 104 | } 105 | 106 | &.loading .loading-overlay { 107 | display: block; 108 | background: linear-gradient(110deg, #92929200 5%, #92929288 25%, #92929200 45%); 109 | background-size: 250% 100%; 110 | background-repeat: repeat-y; 111 | animation: shimmer 1.5s linear infinite; 112 | } 113 | } 114 | 115 | @keyframes shimmer { 116 | 0% { 117 | background-position: 125% 0; 118 | } 119 | 100% { 120 | background-position: -200% 0; 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /src/assets/error.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/assets/imports.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.svg' { 2 | const content: string; 3 | export default content; 4 | } 5 | declare module '*.scss' { 6 | const content: string; 7 | export default content; 8 | } 9 | -------------------------------------------------------------------------------- /src/assets/index.ts: -------------------------------------------------------------------------------- 1 | /// 2 | import playIcon from './play.svg'; 3 | import pauseIcon from './pause.svg'; 4 | import errorIcon from './error.svg'; 5 | import controlsCSS from './controls.scss'; 6 | import visualizerCSS from './visualizer.scss'; 7 | 8 | export const controlsTemplate = document.createElement('template'); 9 | controlsTemplate.innerHTML = ` 10 | 13 |
14 | 19 |
0:00 / 0:00
20 | 21 |
22 |
23 | `; 24 | 25 | export const visualizerTemplate = document.createElement('template'); 26 | visualizerTemplate.innerHTML = ` 27 | 30 | 31 | 32 | `; 33 | -------------------------------------------------------------------------------- /src/assets/pause.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/assets/play.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/assets/visualizer.scss: -------------------------------------------------------------------------------- 1 | :host { 2 | display: block; 3 | } 4 | 5 | ::slotted(.piano-roll-visualizer) { 6 | overflow-x: auto; 7 | } 8 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import {PlayerElement, NoteEvent} from './player'; 2 | import {VisualizerElement} from './visualizer'; 3 | 4 | export {PlayerElement, VisualizerElement, NoteEvent}; 5 | 6 | window.customElements.define('midi-player', PlayerElement); 7 | window.customElements.define('midi-visualizer', VisualizerElement); 8 | -------------------------------------------------------------------------------- /src/player.ts: -------------------------------------------------------------------------------- 1 | import * as mm from '@magenta/music/esm/core.js'; 2 | import {NoteSequence, INoteSequence} from '@magenta/music/esm/protobuf.js'; 3 | 4 | import {controlsTemplate} from './assets'; 5 | import * as utils from './utils'; 6 | import {VisualizerElement} from './visualizer'; 7 | 8 | 9 | export type NoteEvent = CustomEvent<{note: NoteSequence.INote}>; 10 | const VISUALIZER_EVENTS = ['start', 'stop', 'note'] as const; 11 | const DEFAULT_SOUNDFONT = 'https://storage.googleapis.com/magentadata/js/soundfonts/sgm_plus'; 12 | 13 | let playingPlayer: PlayerElement = null; 14 | 15 | 16 | /** 17 | * MIDI player element. 18 | * See also the [`@magenta/music/core/player` docs](https://magenta.github.io/magenta-js/music/modules/_core_player_.html). 19 | * 20 | * The element supports styling using the CSS [`::part` syntax](https://developer.mozilla.org/docs/Web/CSS/::part) 21 | * (see the list of shadow parts [below](#css-shadow-parts)). For example: 22 | * ```css 23 | * midi-player::part(control-panel) { 24 | * background: aquamarine; 25 | * border-radius: 0px; 26 | * } 27 | * ``` 28 | * 29 | * @prop src - MIDI file URL 30 | * @prop soundFont - Magenta SoundFont URL, an empty string to use the default SoundFont, or `null` to use a simple oscillator synth 31 | * @prop noteSequence - Magenta note sequence object representing the currently loaded content 32 | * @prop loop - Indicates whether the player should loop 33 | * @prop currentTime - Current playback position in seconds 34 | * @prop duration - Content duration in seconds 35 | * @prop playing - Indicates whether the player is currently playing 36 | * @attr visualizer - A selector matching `midi-visualizer` elements to bind to this player 37 | * 38 | * @fires load - The content is loaded and ready to play 39 | * @fires start - The player has started playing 40 | * @fires stop - The player has stopped playing 41 | * @fires loop - The player has automatically restarted playback after reaching the end 42 | * @fires note - A note starts 43 | * 44 | * @csspart control-panel - `
` containing all the controls 45 | * @csspart play-button - Play button 46 | * @csspart time - Numeric time indicator 47 | * @csspart current-time - Elapsed time 48 | * @csspart total-time - Total duration 49 | * @csspart seek-bar - `` showing playback position 50 | * @csspart loading-overlay - Overlay with shimmer animation 51 | */ 52 | export class PlayerElement extends HTMLElement { 53 | private domInitialized = false; 54 | private initTimeout: number; 55 | private needInitNs = false; 56 | 57 | protected player: mm.BasePlayer; 58 | protected controlPanel: HTMLElement; 59 | protected playButton: HTMLButtonElement; 60 | protected seekBar: HTMLInputElement; 61 | protected currentTimeLabel: HTMLInputElement; 62 | protected totalTimeLabel: HTMLInputElement; 63 | protected visualizerListeners = new Map(); 64 | 65 | protected ns: INoteSequence = null; 66 | protected _playing = false; 67 | protected seeking = false; 68 | 69 | static get observedAttributes() { return ['sound-font', 'src', 'visualizer']; } 70 | 71 | constructor() { 72 | super(); 73 | 74 | this.attachShadow({mode: 'open'}); 75 | this.shadowRoot.appendChild(controlsTemplate.content.cloneNode(true)); 76 | 77 | this.controlPanel = this.shadowRoot.querySelector('.controls'); 78 | this.playButton = this.controlPanel.querySelector('.play'); 79 | this.currentTimeLabel = this.controlPanel.querySelector('.current-time'); 80 | this.totalTimeLabel = this.controlPanel.querySelector('.total-time'); 81 | this.seekBar = this.controlPanel.querySelector('.seek-bar'); 82 | } 83 | 84 | connectedCallback() { 85 | if (this.domInitialized) { 86 | return; 87 | } 88 | this.domInitialized = true; 89 | 90 | const applyFocusVisiblePolyfill = 91 | (window as any).applyFocusVisiblePolyfill as (scope: Document | ShadowRoot) => void; 92 | if (applyFocusVisiblePolyfill != null) { 93 | applyFocusVisiblePolyfill(this.shadowRoot); 94 | } 95 | 96 | this.playButton.addEventListener('click', () => { 97 | if (this.player.isPlaying()) { 98 | this.stop(); 99 | } else { 100 | this.start(); 101 | } 102 | }); 103 | this.seekBar.addEventListener('input', () => { 104 | // Pause playback while the user is manipulating the control 105 | this.seeking = true; 106 | if (this.player && this.player.getPlayState() === 'started') { 107 | this.player.pause(); 108 | } 109 | }); 110 | this.seekBar.addEventListener('change', () => { 111 | const time = this.currentTime; // This returns the seek bar value as a number 112 | this.currentTimeLabel.textContent = utils.formatTime(time); 113 | if (this.player) { 114 | if (this.player.isPlaying()) { 115 | this.player.seekTo(time); 116 | if (this.player.getPlayState() === 'paused') { 117 | this.player.resume(); 118 | } 119 | } 120 | } 121 | this.seeking = false; 122 | }); 123 | 124 | this.initPlayerNow(); 125 | } 126 | 127 | attributeChangedCallback(name: string, _oldValue: string, newValue: string) { 128 | if (!this.hasAttribute(name)) { 129 | newValue = null; 130 | } 131 | 132 | if (name === 'sound-font' || name === 'src') { 133 | this.initPlayer(); 134 | } else if (name === 'visualizer') { 135 | const fn = () => { this.setVisualizerSelector(newValue); }; 136 | if (document.readyState === 'loading') { 137 | window.addEventListener('DOMContentLoaded', fn); 138 | } else { 139 | fn(); 140 | } 141 | } 142 | } 143 | 144 | protected initPlayer(initNs = true) { 145 | this.needInitNs = this.needInitNs || initNs; 146 | if (this.initTimeout == null) { 147 | this.stop(); 148 | this.setLoading(); 149 | this.initTimeout = window.setTimeout(() => this.initPlayerNow(this.needInitNs)); 150 | } 151 | } 152 | 153 | protected async initPlayerNow(initNs = true) { 154 | this.initTimeout = null; 155 | this.needInitNs = false; 156 | if (!this.domInitialized) { 157 | return; 158 | } 159 | 160 | try { 161 | let ns: INoteSequence = null; 162 | if (initNs) { 163 | if (this.src) { 164 | this.ns = null; 165 | this.ns = await mm.urlToNoteSequence(this.src); 166 | } 167 | this.currentTime = 0; 168 | if (!this.ns) { 169 | this.setError('No content loaded'); 170 | } 171 | } 172 | ns = this.ns; 173 | 174 | if (ns) { 175 | this.seekBar.max = String(ns.totalTime); 176 | this.totalTimeLabel.textContent = utils.formatTime(ns.totalTime); 177 | } else { 178 | this.seekBar.max = '0'; 179 | this.totalTimeLabel.textContent = utils.formatTime(0); 180 | return; 181 | } 182 | 183 | let soundFont = this.soundFont; 184 | const callbackObject = { 185 | // Call callbacks only if we are still playing the same note sequence. 186 | run: (n: NoteSequence.INote) => (this.ns === ns) && this.noteCallback(n), 187 | stop: () => {} 188 | }; 189 | if (soundFont === null) { 190 | this.player = new mm.Player(false, callbackObject); 191 | } else { 192 | if (soundFont === "") { 193 | soundFont = DEFAULT_SOUNDFONT; 194 | } 195 | this.player = new mm.SoundFontPlayer(soundFont, undefined, undefined, undefined, 196 | callbackObject); 197 | await (this.player as mm.SoundFontPlayer).loadSamples(ns); 198 | } 199 | 200 | if (this.ns !== ns) { 201 | // If we started loading a different sequence in the meantime... 202 | return; 203 | } 204 | 205 | this.setLoaded(); 206 | this.dispatchEvent(new CustomEvent('load')); 207 | } catch (error) { 208 | this.setError(String(error)); 209 | throw error; 210 | } 211 | } 212 | 213 | reload() { 214 | this.initPlayerNow(); 215 | } 216 | 217 | start() { 218 | this._start(); 219 | } 220 | 221 | protected _start(looped = false) { 222 | (async () => { 223 | if (this.player) { 224 | if (this.player.getPlayState() == 'stopped') { 225 | if (playingPlayer && playingPlayer.playing && !(playingPlayer == this && looped)) { 226 | playingPlayer.stop(); 227 | } 228 | playingPlayer = this; 229 | this._playing = true; 230 | 231 | let offset = this.currentTime; 232 | // Jump to the start if there are no notes left to play. 233 | if (this.ns.notes.filter((note) => note.startTime > offset).length == 0) { 234 | offset = 0; 235 | } 236 | this.currentTime = offset; 237 | 238 | this.controlPanel.classList.remove('stopped'); 239 | this.controlPanel.classList.add('playing'); 240 | try { 241 | // Force reload visualizers to prevent stuttering at playback start 242 | for (const visualizer of this.visualizerListeners.keys()) { 243 | if (visualizer.noteSequence != this.ns) { 244 | visualizer.noteSequence = this.ns; 245 | visualizer.reload(); 246 | } 247 | } 248 | 249 | const promise = this.player.start(this.ns, undefined, offset); 250 | if (!looped) { 251 | this.dispatchEvent(new CustomEvent('start')); 252 | } else { 253 | this.dispatchEvent(new CustomEvent('loop')); 254 | } 255 | await promise; 256 | this.handleStop(true); 257 | } catch (error) { 258 | this.handleStop(); 259 | throw error; 260 | } 261 | } else if (this.player.getPlayState() == 'paused') { 262 | // This normally should not happen, since we pause playback only when seeking. 263 | this.player.resume(); 264 | } 265 | } 266 | })(); 267 | } 268 | 269 | stop() { 270 | if (this.player && this.player.isPlaying()) { 271 | this.player.stop(); 272 | } 273 | this.handleStop(false); 274 | } 275 | 276 | addVisualizer(visualizer: VisualizerElement) { 277 | const listeners = { 278 | start: () => { visualizer.noteSequence = this.noteSequence; }, 279 | stop: () => { visualizer.clearActiveNotes(); }, 280 | note: (event: NoteEvent) => { visualizer.redraw(event.detail.note); }, 281 | } as const; 282 | for (const name of VISUALIZER_EVENTS) { 283 | this.addEventListener(name, listeners[name]); 284 | } 285 | this.visualizerListeners.set(visualizer, listeners); 286 | } 287 | 288 | removeVisualizer(visualizer: VisualizerElement) { 289 | const listeners = this.visualizerListeners.get(visualizer); 290 | for (const name of VISUALIZER_EVENTS) { 291 | this.removeEventListener(name, listeners[name]); 292 | } 293 | this.visualizerListeners.delete(visualizer); 294 | } 295 | 296 | protected noteCallback(note: NoteSequence.INote) { 297 | if (!this.playing) { 298 | return; 299 | } 300 | this.dispatchEvent(new CustomEvent('note', {detail: {note}})); 301 | if (this.seeking) { 302 | return; 303 | } 304 | this.seekBar.value = String(note.startTime); 305 | this.currentTimeLabel.textContent = utils.formatTime(note.startTime); 306 | } 307 | 308 | protected handleStop(finished = false) { 309 | if (finished) { 310 | if (this.loop) { 311 | this.currentTime = 0; 312 | this._start(true); 313 | return; 314 | } 315 | this.currentTime = this.duration; 316 | } 317 | this.controlPanel.classList.remove('playing'); 318 | this.controlPanel.classList.add('stopped'); 319 | if (this._playing) { 320 | this._playing = false; 321 | this.dispatchEvent(new CustomEvent('stop', {detail: {finished}})); 322 | } 323 | } 324 | 325 | protected setVisualizerSelector(selector: string) { 326 | // Remove old listeners 327 | for (const listeners of this.visualizerListeners.values()) { 328 | for (const name of VISUALIZER_EVENTS) { 329 | this.removeEventListener(name, listeners[name]); 330 | } 331 | } 332 | this.visualizerListeners.clear(); 333 | 334 | // Match visualizers and add them as listeners 335 | if (selector != null) { 336 | for (const element of document.querySelectorAll(selector)) { 337 | if (!(element instanceof VisualizerElement)) { 338 | console.warn(`Selector ${selector} matched non-visualizer element`, element); 339 | continue; 340 | } 341 | 342 | this.addVisualizer(element); 343 | } 344 | } 345 | } 346 | 347 | protected setLoading() { 348 | this.playButton.disabled = true; 349 | this.seekBar.disabled = true; 350 | this.controlPanel.classList.remove('error'); 351 | this.controlPanel.classList.add('loading', 'frozen'); 352 | this.controlPanel.removeAttribute('title'); 353 | } 354 | 355 | protected setLoaded() { 356 | this.controlPanel.classList.remove('loading', 'frozen'); 357 | this.playButton.disabled = false; 358 | this.seekBar.disabled = false; 359 | } 360 | 361 | protected setError(error: string) { 362 | this.playButton.disabled = true; 363 | this.seekBar.disabled = true; 364 | this.controlPanel.classList.remove('loading', 'stopped', 'playing'); 365 | this.controlPanel.classList.add('error', 'frozen'); 366 | this.controlPanel.title = error; 367 | } 368 | 369 | get noteSequence() { 370 | return this.ns; 371 | } 372 | 373 | set noteSequence(value: INoteSequence | null) { 374 | if (this.ns == value) { 375 | return; 376 | } 377 | this.ns = value; 378 | this.removeAttribute('src'); // Triggers initPlayer only if src was present. 379 | this.initPlayer(); 380 | } 381 | 382 | get src() { 383 | return this.getAttribute('src'); 384 | } 385 | 386 | set src(value: string | null) { 387 | this.ns = null; 388 | this.setOrRemoveAttribute('src', value); // Triggers initPlayer only if src was present. 389 | this.initPlayer(); 390 | } 391 | 392 | /** 393 | * @attr sound-font 394 | */ 395 | get soundFont() { 396 | return this.getAttribute('sound-font'); 397 | } 398 | 399 | set soundFont(value: string | null) { 400 | this.setOrRemoveAttribute('sound-font', value); 401 | } 402 | 403 | /** 404 | * @attr loop 405 | */ 406 | get loop() { 407 | return this.getAttribute('loop') != null; 408 | } 409 | 410 | set loop(value: boolean) { 411 | this.setOrRemoveAttribute('loop', value ? '' : null); 412 | } 413 | 414 | get currentTime() { 415 | return parseFloat(this.seekBar.value); 416 | } 417 | 418 | set currentTime(value: number) { 419 | this.seekBar.value = String(value); 420 | this.currentTimeLabel.textContent = utils.formatTime(this.currentTime); 421 | if (this.player && this.player.isPlaying()) { 422 | this.player.seekTo(value); 423 | } 424 | } 425 | 426 | get duration() { 427 | return parseFloat(this.seekBar.max); 428 | } 429 | 430 | get playing() { 431 | return this._playing; 432 | } 433 | 434 | protected setOrRemoveAttribute(name: string, value: string) { 435 | if (value == null) { 436 | this.removeAttribute(name); 437 | } else { 438 | this.setAttribute(name, value); 439 | } 440 | } 441 | } 442 | -------------------------------------------------------------------------------- /src/utils.ts: -------------------------------------------------------------------------------- 1 | export function formatTime(seconds: number) { 2 | const negative = (seconds < 0); 3 | seconds = Math.floor(Math.abs(seconds || 0)); 4 | const s = seconds % 60; 5 | const m = (seconds - s) / 60; 6 | const h = (seconds - s - 60 * m) / 3600; 7 | const sStr = (s > 9) ? `${s}` : `0${s}`; 8 | const mStr = (m > 9 || !h) ? `${m}:` : `0${m}:`; 9 | const hStr = h ? `${h}:` : ''; 10 | return (negative ? '-' : '') + hStr + mStr + sStr; 11 | } 12 | -------------------------------------------------------------------------------- /src/visualizer.ts: -------------------------------------------------------------------------------- 1 | import * as mm from '@magenta/music/esm/core.js'; 2 | import {NoteSequence, INoteSequence} from '@magenta/music/esm/protobuf.js'; 3 | 4 | import {visualizerTemplate} from './assets'; 5 | 6 | const VISUALIZER_TYPES = ['piano-roll', 'waterfall', 'staff'] as const; 7 | type VisualizerType = typeof VISUALIZER_TYPES[number]; 8 | type Visualizer = mm.PianoRollSVGVisualizer | mm.WaterfallSVGVisualizer | mm.StaffSVGVisualizer; 9 | 10 | 11 | /** 12 | * MIDI visualizer element. 13 | * 14 | * The visualizer is implemented via SVG elements which support styling as described 15 | * [here](https://magenta.github.io/magenta-js/music/demos/visualizer.html). 16 | * 17 | * See also the 18 | * [`@magenta/music/core/visualizer` docs](https://magenta.github.io/magenta-js/music/modules/_core_visualizer_.html). 19 | * 20 | * @prop src - MIDI file URL 21 | * @prop type - Visualizer type 22 | * @prop noteSequence - Magenta note sequence object representing the currently displayed content 23 | * @prop config - Magenta visualizer config object 24 | */ 25 | export class VisualizerElement extends HTMLElement { 26 | private domInitialized = false; 27 | private initTimeout: number; 28 | 29 | protected wrapper: HTMLDivElement; 30 | protected visualizer: Visualizer; 31 | 32 | protected ns: INoteSequence = null; 33 | protected _config: mm.VisualizerConfig = {}; 34 | 35 | static get observedAttributes() { return ['src', 'type']; } 36 | 37 | connectedCallback() { 38 | this.attachShadow({mode: 'open'}); 39 | this.shadowRoot.appendChild(visualizerTemplate.content.cloneNode(true)); 40 | 41 | if (this.domInitialized) { 42 | return; 43 | } 44 | this.domInitialized = true; 45 | 46 | this.wrapper = document.createElement('div'); 47 | this.appendChild(this.wrapper); 48 | 49 | this.initVisualizerNow(); 50 | } 51 | 52 | attributeChangedCallback(name: string, _oldValue: string, _newValue: string) { 53 | if (name === 'src' || name === 'type') { 54 | this.initVisualizer(); 55 | } 56 | } 57 | 58 | protected initVisualizer() { 59 | if (this.initTimeout == null) { 60 | this.initTimeout = window.setTimeout(() => this.initVisualizerNow()); 61 | } 62 | } 63 | 64 | protected async initVisualizerNow() { 65 | this.initTimeout = null; 66 | if (!this.domInitialized) { 67 | return; 68 | } 69 | if (this.src) { 70 | this.ns = null; 71 | this.ns = await mm.urlToNoteSequence(this.src); 72 | } 73 | 74 | this.wrapper.innerHTML = ''; 75 | 76 | if (!this.ns) { 77 | return; 78 | } 79 | 80 | if (this.type === 'piano-roll') { 81 | this.wrapper.classList.add('piano-roll-visualizer'); 82 | const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg'); 83 | this.wrapper.appendChild(svg); 84 | this.visualizer = new mm.PianoRollSVGVisualizer(this.ns, svg, this._config); 85 | } else if (this.type === 'waterfall') { 86 | this.wrapper.classList.add('waterfall-visualizer'); 87 | this.visualizer = new mm.WaterfallSVGVisualizer(this.ns, this.wrapper, this._config); 88 | } else if (this.type === 'staff') { 89 | this.wrapper.classList.add('staff-visualizer'); 90 | const div = document.createElement('div'); 91 | this.wrapper.appendChild(div); 92 | this.visualizer = new mm.StaffSVGVisualizer(this.ns, div, this._config); 93 | } 94 | } 95 | 96 | reload() { 97 | this.initVisualizerNow(); 98 | } 99 | 100 | redraw(activeNote?: NoteSequence.INote) { 101 | if (this.visualizer) { 102 | this.visualizer.redraw(activeNote, activeNote != null); 103 | } 104 | } 105 | 106 | clearActiveNotes() { 107 | if (this.visualizer) { 108 | this.visualizer.clearActiveNotes(); 109 | } 110 | } 111 | 112 | get noteSequence() { 113 | return this.ns; 114 | } 115 | 116 | set noteSequence(value: INoteSequence | null) { 117 | if (this.ns == value) { 118 | return; 119 | } 120 | this.ns = value; 121 | this.removeAttribute('src'); // Triggers initVisualizer only if src was present. 122 | this.initVisualizer(); 123 | } 124 | 125 | get src() { 126 | return this.getAttribute('src'); 127 | } 128 | 129 | set src(value: string | null) { 130 | this.ns = null; 131 | this.setOrRemoveAttribute('src', value); // Triggers initVisualizer only if src was present. 132 | this.initVisualizer(); 133 | } 134 | 135 | get type() { 136 | let value = this.getAttribute('type'); 137 | if ((VISUALIZER_TYPES as readonly string[]).indexOf(value) < 0) { 138 | value = 'piano-roll'; 139 | } 140 | return value as VisualizerType; 141 | } 142 | 143 | set type(value: VisualizerType) { 144 | if (value != null && VISUALIZER_TYPES.indexOf(value) < 0) { 145 | throw new Error( 146 | `Unknown visualizer type ${value}. Allowed values: ${VISUALIZER_TYPES.join(', ')}`); 147 | } 148 | this.setOrRemoveAttribute('type', value); 149 | } 150 | 151 | get config() { 152 | return this._config; 153 | } 154 | 155 | set config(value: mm.VisualizerConfig) { 156 | this._config = value; 157 | this.initVisualizer(); 158 | } 159 | 160 | protected setOrRemoveAttribute(name: string, value: string) { 161 | if (value == null) { 162 | this.removeAttribute(name); 163 | } else { 164 | this.setAttribute(name, value); 165 | } 166 | } 167 | } 168 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "alwaysStrict": true, 4 | "noImplicitAny": true, 5 | "noUnusedLocals": true, 6 | "noImplicitReturns": true, 7 | "noImplicitThis": true, 8 | "noUnusedParameters": true, 9 | "noFallthroughCasesInSwitch": true, 10 | "allowUnreachableCode": false, 11 | "pretty": true, 12 | "skipLibCheck": true, 13 | "target": "esnext", 14 | "module": "es6", 15 | "moduleResolution": "node" 16 | }, 17 | "include": [ 18 | "src/**/*" 19 | ], 20 | "exclude": [ 21 | "node_modules" 22 | ] 23 | } 24 | -------------------------------------------------------------------------------- /twinkle_twinkle.mid: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cifkao/html-midi-player/85544e6276ecd0e778835a1c7732910f4eb0cf6f/twinkle_twinkle.mid --------------------------------------------------------------------------------