├── .editorconfig ├── .github ├── CONTRIBUTING.md ├── ISSUE_TEMPLATE.md └── PULL_REQUEST_TEMPLATE.md ├── .gitignore ├── LICENSE ├── README.md ├── css └── style.css ├── img ├── app-image.jpg ├── demo-lightpad.gif ├── demo-seaboard.gif ├── lightpad.png ├── roli.png └── seaboard.png ├── index.html ├── js └── script.js └── lib └── mpe.min.js /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig helps developers define and maintain consistent 2 | # coding styles between different editors and IDEs 3 | # editorconfig.org 4 | 5 | root = true 6 | 7 | 8 | [*] 9 | 10 | # Change these settings to your own preference 11 | indent_style = space 12 | indent_size = 2 13 | 14 | # We recommend you to keep these unchanged 15 | end_of_line = lf 16 | charset = utf-8 17 | trim_trailing_whitespace = true 18 | insert_final_newline = true 19 | 20 | [*.md] 21 | trim_trailing_whitespace = false -------------------------------------------------------------------------------- /.github/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | Thank you for Contributing 2 | === 3 | 4 | We want to make it as easy as possible to contribute changes. 5 | 6 | Follow the requirements below for __[Creating Issues](https://github.com/CivilServiceUSA/api/issues/new)__ and __[Pull Requests](https://github.com/CivilServiceUSA/api/pull/new)__, to keep everything simple for everyone :) 7 | 8 | ![daftpunktocat](https://octodex.github.com/images/daftpunktocat-thomas.gif "daftpunktocat") 9 | 10 | 11 | Creating an Issue 12 | --- 13 | 14 | Use the Template that we provide. Issues reported that do not use the Issue Template will likely be rejected. 15 | 16 | 17 | Creating a Pull Request 18 | --- 19 | 20 | Before you can submit a PR, you will need to: 21 | 22 | 1. Clone this repo 23 | 2. Make a New Branch ( ideally you will name your branch for the issue you are fixing, e,g, `issue-3-updating-docs` ) 24 | 3. Commit & Push the New Branch 25 | 26 | Now you can submit a new PR using the Template that we provide. PR's submitted that do not use the PR Template will likely be rejected. 27 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | #### Overview: 2 | 3 | _( a detailed overview of the problem this ticket will solve and the targeted audience )_ 4 | 5 | #### Acceptance Criteria: 6 | 7 | _( issue will not be considered complete unless this list of criteria is met, e.g. intended usage, expected functionality, specific metrics met, etc. )_ 8 | 9 | #### Steps to Duplicate _( required for bug reports )_: 10 | 11 | _( a step by step guide written for a person who might be looking at this for the first time )_ 12 | 13 | #### System Info _( required for bug reports )_: 14 | 15 | _( e.g. "iPhone 6 iOS 10.3" or "OSX 10.10 and Google Chrome Version 59.0.3071.115 (64-bit)" )_ 16 | 17 | #### Relevant Documentation _( optional )_ 18 | 19 | _( e.g. Screenshots, Github Issues, etc )_ 20 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | #### What's this PR do? 2 | 3 | _[write_something]_ 4 | 5 | #### Where should the reviewer start? 6 | 7 | _[write_something]_ 8 | 9 | #### How should this be manually tested? 10 | 11 | _[write_something]_ 12 | 13 | #### Any background context you want to provide? 14 | 15 | _[write_something]_ 16 | 17 | #### What are the relevant github issue? 18 | 19 | _[write_something]_ 20 | 21 | #### Screenshots (if appropriate) 22 | 23 | _[drag_and_drop_here]_ 24 | 25 | #### What gif best describes this PR or how it makes you feel? 26 | 27 | _[drag_and_drop_something_fun_here]_ 28 | 29 | #### Definition of Done: 30 | 31 | - [ ] You have actually run this locally and can verify it works 32 | - [ ] You have added code comments to all code being submitted 33 | - [ ] You have updated the README file (if appropriate) 34 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | !.gitignore 2 | .DS_Store 3 | Thumbs.db 4 | .idea 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Briosum 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. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | MPE.js Player v1.0.0 2 | === 3 | 4 | MPE Player using [mpe.js](http://mpe.js.org/) Library. 5 | 6 | This demo app was built to play with Browser Based Audio Oscillators using MPE devices ( such as ROLI Lightpad & Seaboard BLOCKS ). This _should_ work with any Modern Browser that supports [`AudioContext`](https://caniuse.com/#search=AudioContext). 7 | 8 | ### [♫ Use MPE Player ♫](https://briosum.com/lab/mpe-player/) 9 | 10 | Seaboard BLOCK 11 | --- 12 | ![seaboard](img/demo-seaboard.gif "seaboard") 13 | 14 | This demo uses the following Seaboard BLOCK config settings via the BLOCKS Dashboard. 15 | 16 | - [x] Note Start channel: `2` 17 | - [x] Note End channel: `16` 18 | - [x] Use MPE: `Checked` 19 | - [x] Pitch Bend Range: `48` 20 | 21 | 22 | Lightpad BLOCK 23 | --- 24 | ![lightpad](img/demo-lightpad.gif "lightpad") 25 | 26 | This demo uses the following Lightpad BLOCK config settings via the BLOCKS Dashboard. 27 | 28 | - [x] Setting: `4x4 MPE Mode` 29 | - [x] MIDI Mode: `MPE` 30 | - [x] Note channel first: `2` 31 | - [x] Note channel last: `16` 32 | - [x] Base note: `C3` 33 | - [x] Grid size: `4` 34 | - [x] Send pitch bend: `unchecked` 35 | 36 | 37 | Instructions 38 | --- 39 | 40 | Connect your MPE device to your Web Browser and tinker away. 41 | 42 | If you want to tweak some stuff, `MpePlayer` has a few config options. `waveShape` is probably the one you might enjoy the most as it sets up the oscillator sound that the MPE device uses. 43 | 44 | ``` 45 | 52 | ``` 53 | 54 | Legal Stuff 55 | --- 56 | Briosum is not affiliated with ROLI. All Product Names & Images are Copyright [ROLI Ltd](https://roli.com/) 57 | -------------------------------------------------------------------------------- /css/style.css: -------------------------------------------------------------------------------- 1 | html, body, div, span, applet, object, iframe, h1, h2, h3, h4, h5, h6, p, blockquote, pre, a, abbr, acronym, address, big, cite, code, del, dfn, em, img, ins, kbd, q, s, samp, small, strike, strong, sub, sup, tt, var, b, u, i, center, dl, dt, dd, ol, ul, li, fieldset, form, label, legend, table, caption, tbody, tfoot, thead, tr, th, td, article, aside, canvas, details, embed, figure, figcaption, footer, header, hgroup, menu, nav, output, ruby, section, summary, time, mark, audio, video { 2 | margin: 0; 3 | padding: 0; 4 | border: 0; 5 | font-size: 100%; 6 | font: inherit; 7 | vertical-align: baseline; 8 | } 9 | html { 10 | box-sizing: border-box; 11 | } 12 | *, *:before, *:after { 13 | box-sizing: inherit; 14 | } 15 | body { 16 | background-color: rgb(25, 25, 25); 17 | font-family: 'Open Sans', sans-serif; 18 | color: #fff; 19 | } 20 | 21 | #wrapper { 22 | display: flex; 23 | height: 100vh; 24 | width: 100vw; 25 | align-items: center; 26 | justify-content: center; 27 | } 28 | 29 | #connect-device, #not-supported, #error { 30 | opacity: 0; 31 | height: 100vh; 32 | width: 100vw; 33 | display: none; 34 | align-items: center; 35 | justify-content: center; 36 | position: absolute; 37 | left: 0; 38 | top: 0; 39 | transition: 0.25s ease-in-out; 40 | z-index: 100; 41 | font-weight: 300; 42 | font-size: 24px; 43 | } 44 | #connect-device a, #not-supported a { 45 | margin: 0 8px; 46 | color: #FFF; 47 | text-decoration: none; 48 | transition: 0.25s ease-in-out; 49 | border-bottom: 1px solid rgba(255, 255, 255, 0.15); 50 | } 51 | #connect-device a:hover, #not-supported a:hover { 52 | border-bottom: 1px solid rgba(255, 255, 255, 1); 53 | } 54 | 55 | #roli { 56 | position: absolute; 57 | display: block; 58 | bottom: 20px; 59 | left: 20px; 60 | width: 30px; 61 | height: 30px; 62 | background: url(../img/roli.png) center center no-repeat; 63 | background-size: cover; 64 | z-index: 110; 65 | opacity: 0.75; 66 | transition: opacity 0.25s ease-in-out; 67 | cursor: pointer; 68 | } 69 | #roli:hover { 70 | opacity: 1; 71 | } 72 | 73 | #debug { 74 | transition: opacity 0.5s ease-in; 75 | position: absolute; 76 | bottom: 0; 77 | right: 0; 78 | height: 100vh; 79 | width: 250px; 80 | align-items: center; 81 | justify-content: left; 82 | overflow: hidden; 83 | color: #CCC; 84 | font-family: sans-serif; 85 | font-weight: 300; 86 | font-size: 12px; 87 | line-height: 16px; 88 | z-index: 100; 89 | background-color: rgba(25, 25, 25, 0.5); 90 | opacity: 0; 91 | display: flex; 92 | } 93 | 94 | #seaboard { 95 | transition: opacity 0.25s ease-in-out; 96 | width: 800px; 97 | height: 400px; 98 | background: url(../img/seaboard.png) center center no-repeat; 99 | background-size: cover; 100 | position: relative; 101 | border-radius: 16px; 102 | box-shadow: 0 4px 16px rgba(0, 0, 0, 0.65); 103 | z-index: 90; 104 | opacity: 0; 105 | } 106 | 107 | #lightpad { 108 | transition: opacity 0.25s ease-in-out; 109 | width: 400px; 110 | height: 400px; 111 | background: url(../img/lightpad.png) center center no-repeat; 112 | background-size: cover; 113 | position: relative; 114 | border-radius: 16px; 115 | box-shadow: 0 4px 16px rgba(0, 0, 0, 0.65); 116 | z-index: 90; 117 | opacity: 0; 118 | } 119 | 120 | .title { 121 | position: absolute; 122 | top: -30px; 123 | text-align: center; 124 | display: block; 125 | width: 100%; 126 | font-weight: 300; 127 | color: #999; 128 | } 129 | 130 | .note { 131 | position: absolute; 132 | opacity: 0.75; 133 | width: 30px; 134 | height: 30px; 135 | border-radius: 50%; 136 | left: 0; 137 | top: calc(50% - 15px); 138 | z-index: 95; 139 | transform-origin: 50% 50%; 140 | } 141 | 142 | .note-square { 143 | position: absolute; 144 | opacity: 0.35; 145 | width: 75px; 146 | height: 75px; 147 | left: 11px; 148 | bottom: 12px; 149 | z-index: 95; 150 | border-radius: 8px; 151 | } 152 | 153 | .color:nth-child(1) { 154 | background-color: #2196f3; 155 | } 156 | .color:nth-child(2) { 157 | background-color: #ff9801; 158 | } 159 | .color:nth-child(3) { 160 | background-color: #e91d62; 161 | } 162 | .color:nth-child(4) { 163 | background-color: #9c28b1; 164 | } 165 | .color:nth-child(5) { 166 | background-color: #4cb050; 167 | } 168 | .color:nth-child(6) { 169 | background-color: #03a9f5; 170 | } 171 | .color:nth-child(7) { 172 | background-color: #673bb7; 173 | } 174 | .color:nth-child(8) { 175 | background-color: #ffeb3c; 176 | } 177 | .color:nth-child(94) { 178 | background-color: #8bc24a; 179 | } 180 | .color:nth-child(10) { 181 | background-color: #00bbd4; 182 | } 183 | 184 | .github-corner { 185 | z-index: 1000; 186 | opacity: 0.25; 187 | transition: opacity 0.25s ease-in-out; 188 | position: absolute; 189 | top: 0; 190 | left: 0; 191 | } 192 | .github-corner:hover { 193 | opacity: 1; 194 | } 195 | .github-corner:hover .octo-arm { 196 | animation:octocat-wave 560ms ease-in-out 197 | } 198 | @keyframes octocat-wave { 199 | 0%,100% { 200 | transform:rotate(0) 201 | } 202 | 20%,60% { 203 | transform:rotate(-25deg) 204 | } 205 | 40%,80% { 206 | transform:rotate(10deg) 207 | } 208 | } 209 | @media (max-width:500px) { 210 | .github-corner:hover .octo-arm { 211 | animation:none 212 | } 213 | .github-corner .octo-arm { 214 | animation:octocat-wave 560ms ease-in-out 215 | } 216 | } 217 | -------------------------------------------------------------------------------- /img/app-image.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/briosum/mpe-player/dde014fe2c23383aaa42d81202b6f501e1e0fcfb/img/app-image.jpg -------------------------------------------------------------------------------- /img/demo-lightpad.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/briosum/mpe-player/dde014fe2c23383aaa42d81202b6f501e1e0fcfb/img/demo-lightpad.gif -------------------------------------------------------------------------------- /img/demo-seaboard.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/briosum/mpe-player/dde014fe2c23383aaa42d81202b6f501e1e0fcfb/img/demo-seaboard.gif -------------------------------------------------------------------------------- /img/lightpad.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/briosum/mpe-player/dde014fe2c23383aaa42d81202b6f501e1e0fcfb/img/lightpad.png -------------------------------------------------------------------------------- /img/roli.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/briosum/mpe-player/dde014fe2c23383aaa42d81202b6f501e1e0fcfb/img/roli.png -------------------------------------------------------------------------------- /img/seaboard.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/briosum/mpe-player/dde014fe2c23383aaa42d81202b6f501e1e0fcfb/img/seaboard.png -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | MPE.js Player 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 |
47 |

48 | 
49 |       
50 | 
51 |       
52 | 
53 |       
54 |
ROLI Seaboard BLOCK
55 |
56 | 57 |
58 |
ROLI Lightpad BLOCK
59 |
60 | 61 |
62 | Please Connect a ROLI Lightpad or Seaboard BLOCK 63 |
64 | 65 |
66 | Your Browser Does Not Support MIDI, Try Chrome or Opera 67 |
68 | 69 |
70 |
71 | 72 | 73 | 74 | 75 | 82 | 83 | 84 | -------------------------------------------------------------------------------- /js/script.js: -------------------------------------------------------------------------------- 1 | /** 2 | * MPE Player 3 | * Connect an MPE device to your browser and play some music. 4 | * 5 | * This is just for demo purposes to show how you can connect your MPE device 6 | * and use it in a browser window. Feel free to do whatever you want with this 7 | * code, and hit me up with any questions of comments. 8 | * 9 | * @license MIT 10 | * @version 1.0.0 11 | * @author Peter Schmalfeldt 12 | * @link https://github.com/briosum/mpe-player 13 | */ 14 | var MpePlayer = { 15 | /** 16 | * MPE Instrument reference which will be populated on MpePlayer.init() with `mpe.min.js` 17 | */ 18 | instrument: null, 19 | 20 | /** 21 | * Store Information about connected device 22 | */ 23 | port: {}, 24 | 25 | /** 26 | * MPE Player DOM Elements 27 | */ 28 | dom: { 29 | debug: document.getElementById('debug'), 30 | seaboard: document.getElementById('seaboard'), 31 | lightpad: document.getElementById('lightpad'), 32 | connectDevice: document.getElementById('connect-device'), 33 | notSupported: document.getElementById('not-supported'), 34 | error: document.getElementById('error') 35 | }, 36 | 37 | /** 38 | * MPE Player Configuration Options 39 | */ 40 | options: { 41 | debug: false, 42 | debugHTML: true, 43 | waveShape: 'sine' 44 | }, 45 | 46 | /** 47 | * MPE Player Audio Engine 48 | */ 49 | audio: { 50 | /** 51 | * Audio Context based on Browser Support 52 | */ 53 | context: new (typeof AudioContext !== "undefined" && AudioContext !== null ? AudioContext : webkitAudioContext), 54 | 55 | /** 56 | * Audio Engine Oscillators 57 | */ 58 | oscillators: {}, 59 | 60 | /** 61 | * Audio Engine Envelopes 62 | */ 63 | envelopes: {}, 64 | 65 | /** 66 | * Audio Engine Note Timeouts 67 | */ 68 | timeouts: {}, 69 | 70 | /** 71 | * Convert MIDI note to Frequency 72 | * 73 | * @param note - MIDI Note 74 | * @param pitchBend - MPEs `pitchBend` parameter ( used here with x 12 to mimic octave bend ) 75 | * @returns {number} 76 | */ 77 | frequencyFromNoteNumber: function (note, pitchBend) { 78 | return (440 * Math.pow(2, (note-69) / 12)) + (pitchBend * 12); 79 | }, 80 | 81 | /** 82 | * Make sure output does not go above or below limits 83 | * @param output 84 | * @returns {number} 85 | */ 86 | limiter: function (output) { 87 | if (output < 0) { 88 | output = 0; 89 | } 90 | if (output > 1) { 91 | output = 1; 92 | } 93 | 94 | return output; 95 | }, 96 | 97 | /** 98 | * Play Note 99 | * Use the browsers ability to create oscillators and apply some fun MPE features 100 | * such as pitch bend and after touch. You could probably create an oscillator filter 101 | * here and use the `timbre` MPE param to make gliding on the keys apply an effect. 102 | * 103 | * @param note - This is the MPE Note object being passed in from MPEs `subscribe` handler 104 | */ 105 | playNote: function (note) { 106 | 107 | // Setup Note Defaults 108 | var index = 'note_' + note.noteNumber; 109 | var now = MpePlayer.audio.context.currentTime; 110 | var frequency = MpePlayer.audio.frequencyFromNoteNumber(note.noteNumber, note.pitchBend); 111 | 112 | // Check if we are already playing this note, if not, create it 113 | if (MpePlayer.audio.oscillators[index] === undefined) { 114 | 115 | // Create oscillator for this note 116 | MpePlayer.audio.oscillators[index] = MpePlayer.audio.context.createOscillator(); 117 | MpePlayer.audio.oscillators[index].type = MpePlayer.options.waveShape; 118 | MpePlayer.audio.oscillators[index].frequency.setValueAtTime(110, 0); 119 | 120 | // Create envelope for this note 121 | MpePlayer.audio.envelopes[index] = MpePlayer.audio.context.createGain(); 122 | MpePlayer.audio.envelopes[index].gain.value = 0.0; 123 | 124 | // Connect oscillator & envelope 125 | MpePlayer.audio.envelopes[index].connect(MpePlayer.audio.context.destination); 126 | MpePlayer.audio.oscillators[index].connect(MpePlayer.audio.envelopes[index]); 127 | 128 | // Start oscillator 129 | MpePlayer.audio.oscillators[index].start(now); 130 | } 131 | 132 | // Create some cached params for referencing oscillator & envelope 133 | var oscillator = MpePlayer.audio.oscillators[index]; 134 | var envelope = MpePlayer.audio.envelopes[index]; 135 | 136 | // Control oscillator for this note 137 | oscillator.frequency.setValueAtTime(frequency, now); 138 | oscillator.frequency.cancelScheduledValues(0); 139 | oscillator.frequency.setTargetAtTime(frequency, 0, 0); 140 | oscillator.frequency.linearRampToValueAtTime(1, now + 5); 141 | 142 | // Control envelope for this note 143 | envelope.gain.cancelScheduledValues(now); 144 | envelope.gain.setValueAtTime(MpePlayer.audio.limiter(note.pressure), now); 145 | envelope.gain.setTargetAtTime(MpePlayer.audio.limiter(note.pressure), 0, 0); 146 | envelope.gain.linearRampToValueAtTime(1, now + 5); 147 | 148 | // Clear Timeouts for Currently Playing Note 149 | clearTimeout(MpePlayer.audio.timeouts[index]); 150 | 151 | // Setup to auto stop and remove it from our 152 | MpePlayer.audio.timeouts[index] = setTimeout(function () { 153 | 154 | // Stop oscillator 155 | MpePlayer.audio.oscillators[index].stop(0); 156 | 157 | // Remove oscillator & envelope from memory 158 | delete MpePlayer.audio.oscillators[index]; 159 | delete MpePlayer.audio.envelopes[index]; 160 | }, 100); 161 | } 162 | }, 163 | 164 | /** 165 | * Render Notes Being Played on Device 166 | */ 167 | render: { 168 | /** 169 | * Render Note Timeouts 170 | */ 171 | timeouts: {}, 172 | 173 | /** 174 | * Handle Detecting which MPE Device is Connected 175 | */ 176 | init: function () { 177 | MpePlayer.dom.seaboard.style.opacity = 0; 178 | MpePlayer.dom.lightpad.style.opacity = 0; 179 | MpePlayer.dom.connectDevice.style.opacity = 0; 180 | 181 | MpePlayer.dom.seaboard.style.display = 'none'; 182 | MpePlayer.dom.lightpad.style.display = 'none'; 183 | MpePlayer.dom.connectDevice.style.display = 'none'; 184 | MpePlayer.dom.notSupported.style.display = 'none'; 185 | MpePlayer.dom.error.style.display = 'none'; 186 | 187 | if (MpePlayer.port.state !== 'connected') { 188 | MpePlayer.dom.connectDevice.style.display = 'flex'; 189 | MpePlayer.dom.connectDevice.style.opacity = 1; 190 | } 191 | else if (MpePlayer.port.state === 'connected' && MpePlayer.port.connection !== 'open') { 192 | MpePlayer.dom.error.innerHTML = MpePlayer.port.name + ' detected, but closed our connection. Try refreshing the page.'; 193 | MpePlayer.dom.error.style.opacity = 1; 194 | MpePlayer.dom.error.style.display = 'flex'; 195 | } 196 | else { 197 | if (MpePlayer.port.name.trim() === 'Seaboard BLOCK') { 198 | MpePlayer.dom.seaboard.style.display = 'block'; 199 | MpePlayer.dom.seaboard.style.opacity = 1; 200 | } 201 | else if (MpePlayer.port.name.trim() === 'Lightpad BLOCK') { 202 | MpePlayer.dom.lightpad.style.display = 'block'; 203 | MpePlayer.dom.lightpad.style.opacity = 1; 204 | } 205 | } 206 | }, 207 | 208 | /** 209 | * Render Note Being Played 210 | * rendering it based on its device name 211 | * 212 | * @param note 213 | */ 214 | note: function (note) { 215 | if (MpePlayer.port.name.trim() === 'Seaboard BLOCK') { 216 | MpePlayer.render.seaboard(note); 217 | } 218 | 219 | if (MpePlayer.port.name.trim() === 'Lightpad BLOCK') { 220 | MpePlayer.render.lightpad(note); 221 | } 222 | }, 223 | 224 | /** 225 | * Render ROLI Seaboard Block 226 | * 227 | * Seaboard Settings: 228 | * 229 | * - Note Start channel: 2 230 | * - Note End channel: 16 231 | * - Use MPE: Checked 232 | * - Pitch Bend Range: 48 233 | * 234 | * @param note 235 | */ 236 | seaboard: function (note) { 237 | var index = 'note_' + note.noteNumber; 238 | var elm = document.getElementById(index); 239 | 240 | // Check if we are already have not on screen 241 | if (!elm) { 242 | // Create note to append to Instrument 243 | elm = document.createElement('div'); 244 | elm.className = 'note color note-' + note.noteNumber; 245 | elm.id = 'note_' + note.noteNumber; 246 | 247 | // Setup Positioning 248 | var position = (note.noteNumber % 24); 249 | var offset = (note.noteNumber % 24); 250 | 251 | // Handle the Gaps in the Seaboard that are not really unique keys 252 | if (position > 4 && position <= 11) { 253 | offset += 1; 254 | } 255 | else if (position > 11 && position <= 16) { 256 | offset += 2; 257 | } 258 | else if (position > 16) { 259 | offset += 3; 260 | } 261 | 262 | // Apply Initial Style 263 | elm.style.left = (13 + (offset * 30) * 0.955) + 'px'; 264 | 265 | // Append to Instrument 266 | MpePlayer.dom.seaboard.appendChild(elm); 267 | } 268 | 269 | // Convert MPE note into CSS Styles 270 | var pitchBend = (800/50) * note.pitchBend; 271 | var scale = 'scale(' + ( 1 + note.pressure ) + ')'; 272 | var translate = 'translate(' + pitchBend + 'px, 0)'; 273 | 274 | // Apply Live Styles 275 | elm.style.top = (400 - (400 * note.timbre) - (15 * ( 1 + note.pressure ))) + 'px'; 276 | elm.style.filter = 'blur('+ (1 * note.pressure) + 'px)'; 277 | elm.style.transform = scale + ' ' + translate; 278 | elm.style.webkitTransform = scale + ' ' + translate; 279 | 280 | // Automatically Remove Note from DOM 281 | clearTimeout(MpePlayer.render.timeouts[index]); 282 | MpePlayer.render.timeouts[index] = setTimeout(function () { 283 | elm.remove(); 284 | }, 100); 285 | }, 286 | 287 | /** 288 | * Render ROLI Lightpad Block 289 | * 290 | * Lightpad Settings: 291 | * 292 | * - Setting: 4x4 MPE Mode 293 | * - MIDI Mode: MPE 294 | * - Note channel first: 2 295 | * - Note channel last: 16 296 | * - Base note: C3 297 | * - Grid size: 4 298 | * - Send pitch bend: unchecked 299 | * 300 | * @param note 301 | */ 302 | lightpad: function (note) { 303 | var index = 'note_' + note.noteNumber; 304 | var elm = document.getElementById(index); 305 | 306 | // Check if we are already have not on screen 307 | if (!elm) { 308 | // Create note to append to Instrument 309 | elm = document.createElement('div'); 310 | elm.className = 'note-square color note-' + note.noteNumber; 311 | elm.id = 'note_' + note.noteNumber; 312 | 313 | // Setup Positioning 314 | var vOffset = 0; 315 | var hOffset = (note.noteNumber % 4); 316 | var position = (note.noteNumber % 60); 317 | 318 | if (position > 3 && position <= 7) { 319 | vOffset += 1; 320 | } 321 | else if (position > 7 && position <= 11) { 322 | vOffset += 2; 323 | } 324 | else if (position > 11) { 325 | vOffset += 3; 326 | } 327 | 328 | var vGutter = (vOffset * 26); 329 | var hGutter = (hOffset * 26); 330 | 331 | // Apply Initial Style 332 | elm.style.left = (12 + (75 * hOffset)) + hGutter + 'px'; 333 | elm.style.bottom = (12 + (75 * vOffset)) + vGutter + 'px'; 334 | 335 | // Append to Instrument 336 | MpePlayer.dom.lightpad.appendChild(elm); 337 | } 338 | 339 | // Convert MPE note into CSS Styles 340 | elm.style.opacity = 0.35 + ((1 * note.pressure) * 0.65); 341 | elm.style.filter = 'blur('+ (5 * note.pressure) + 'px)'; 342 | 343 | // Automatically Remove Note from DOM 344 | clearTimeout(MpePlayer.render.timeouts[index]); 345 | MpePlayer.render.timeouts[index] = setTimeout(function () { 346 | elm.remove(); 347 | }, 100); 348 | } 349 | }, 350 | 351 | /** 352 | * Initialize MPE Player 353 | * @param options - JSON Object to Customize Player 354 | */ 355 | init: function (options) { 356 | 357 | // Overload Default Option with init(options) 358 | MpePlayer.options = Object.assign(MpePlayer.options, options); 359 | 360 | // Configure `MpePlayer.instrument` to use global `mpe` from `mpe.min.js` 361 | MpePlayer.instrument = mpe({ 362 | log: MpePlayer.options.debug 363 | }); 364 | 365 | // Subscribe to Active Note Changes 366 | MpePlayer.instrument.subscribe(function (notes) { 367 | if (MpePlayer.port.state === 'connected' && MpePlayer.port.connection === 'open') { 368 | // Send Individual Notes to Audio Engine 369 | for (var i = 0; i < notes.length; i++) { 370 | MpePlayer.audio.playNote(notes[i]); 371 | MpePlayer.render.note(notes[i]); 372 | } 373 | 374 | // Send Debug of Notes to Debug HTML Node 375 | if (MpePlayer.options.debugHTML) { 376 | var output = JSON.stringify(notes, null, 2); 377 | MpePlayer.dom.debug.innerText = (output.length > 2) ? output : ''; 378 | MpePlayer.dom.debug.style.display = (output.length > 2) ? 'flex' : 'none'; 379 | MpePlayer.dom.debug.style.opacity = (output.length > 2) ? 1 : 0; 380 | } 381 | } 382 | }); 383 | 384 | // Check first that we have can have MIDI access 385 | if (navigator.requestMIDIAccess) { 386 | 387 | // Request Midi Access 388 | navigator.requestMIDIAccess().then(function (access) { 389 | 390 | // Handle Device 391 | access.onstatechange = function(e) { 392 | MpePlayer.port = { 393 | connection: e.port.connection, 394 | id: e.port.id, 395 | manufacturer: e.port.manufacturer, 396 | name: e.port.name, 397 | state: e.port.state, 398 | type: e.port.type, 399 | version: e.port.version 400 | }; 401 | 402 | // Update State Change for MPE Device 403 | MpePlayer.render.init(); 404 | }; 405 | 406 | // Handle Initial Request for MIDI 407 | MpePlayer.render.init(); 408 | 409 | // Capture Input from MIDI Device 410 | var inputs = access.inputs.values(); 411 | 412 | // Loop through inputs to process MIDI Messages 413 | for (var input = inputs.next(); input && !input.done; input = inputs.next()) { 414 | // Hand off MIDI Message to MPEs Midi Message Processor 415 | input.value.onmidimessage = function (message) { 416 | MpePlayer.instrument.processMidiMessage(message.data); 417 | }; 418 | } 419 | }, function (e) { 420 | MpePlayer.dom.error.innerHTML = 'No access to your midi devices. ' + e; 421 | MpePlayer.dom.error.style.opacity = 1; 422 | MpePlayer.dom.error.style.display = 'flex'; 423 | }); 424 | } else { 425 | MpePlayer.dom.notSupported.style.opacity = 1; 426 | MpePlayer.dom.notSupported.style.display = 'flex'; 427 | } 428 | } 429 | }; 430 | -------------------------------------------------------------------------------- /lib/mpe.min.js: -------------------------------------------------------------------------------- 1 | var mpe=function(e){function t(r){if(n[r])return n[r].exports;var o=n[r]={exports:{},id:r,loaded:!1};return e[r].call(o.exports,o,o.exports,t),o.loaded=!0,o.exports}var n={};return t.m=e,t.c=n,t.p="",t(0)}([function(e,t,n){"use strict";function r(e){return e&&e.__esModule?e:{"default":e}}var o=n(1),i=r(o);e.exports=i["default"]},function(e,t,n){"use strict";Object.defineProperty(t,"__esModule",{value:!0});var r=n(2);Object.defineProperty(t,"default",{enumerable:!0,get:function(){return r.mpeInstrument}})},function(e,t,n){"use strict";function r(e){return e&&e.__esModule?e:{"default":e}}function o(e){if(Array.isArray(e)){for(var t=0,n=Array(e.length);t127)throw new RangeError("scale7To14Bit takes a 7-bit integer.\n"+("scale7To14Bit("+e+") is invalid."));return e<=64?e<<7:e/127*16383};t.dataBytesToUint14=function(e){var t=e.map(function(e){return 127&e});switch(e.length){case 1:return n(t[0]);case 2:return(t[0]<<7)+t[1]}throw new Error("midiDataToMpeValue takes one or two 8-bit integers.\n"+("midiDataToMpeValue("+e+") is invalid."))},t.int7ToUnsignedFloat=function(e){return e<=64?.5*e/64:.5+.5*(e-64)/63},t.int14ToUnsignedFloat=function(e){return e<=8192?.5*e/8192:.5+.5*(e-8192)/8191},t.int14ToSignedFloat=function(e){return e<=8192?e/8192-1:(e-8192)/8191}},function(e,t){"use strict";Object.defineProperty(t,"__esModule",{value:!0});var n=void 0;t.logger=function(e){return function(t){return function(r){return function(o){var i=r(o),u=n;return n=t.getState().activeNotes,n!==u&&console.log("active notes:",e(n)),i}}}}},function(e,t,n){"use strict";function r(e){if(Array.isArray(e)){for(var t=0,n=Array(e.length);t1&&void 0!==arguments[1]?arguments[1]:{},n=Object.keys(t).reduce(function(n,r){return"undefined"!=typeof e[r]&&(n[r]=t[r](e[r])),n},{});return Object.assign({},e,n)}},function(e,t){"use strict";Object.defineProperty(t,"__esModule",{value:!0});var n={0:"C",1:"C#",2:"D",3:"Eb",4:"E",5:"F",6:"F#",7:"G",8:"Ab",9:"A",10:"Bb",11:"B"},r=t.toPitchClassNumber=function(e){return Math.floor(e%12)},o=t.toOctaveNumber=function(e){return Math.floor(e/12)-1},i=t.toPitchClassName=function(e){return n[r(e)]},u=t.toHelmholtzCommas=function(e){var t=Math.max(-1*o(e)+2,0);return new Array(t).fill(",").join("")},c=t.toHelmholtzApostrophes=function(e){var t=Math.max(o(e)-3,0);return new Array(t).fill("'").join("")},a=t.toHelmholtzPitchName=function(e){return e>=48?i(e).toLowerCase():i(e)};t.toHelmholtzPitch=function(e){return""+a(e)+u(e)+c(e)},t.toScientificPitch=function(e){return""+i(e)+o(e)}},function(e,t,n){"use strict";function r(e){return e&&e.__esModule?e:{"default":e}}Object.defineProperty(t,"__esModule",{value:!0});var o=n(3),i=n(29),u=r(i),c=n(30),a=r(c);t["default"]=(0,o.combineReducers)({channelScopes:a["default"],activeNotes:u["default"]})},function(e,t,n){"use strict";function r(e){if(e&&e.__esModule)return e;var t={};if(null!=e)for(var n in e)Object.prototype.hasOwnProperty.call(e,n)&&(t[n]=e[n]);return t["default"]=e,t}function o(e){if(Array.isArray(e)){for(var t=0,n=Array(e.length);t0&&void 0!==arguments[0]?arguments[0]:[],t=arguments[1];if(!u[t.type])return e;switch(t.type){case u.NOTE_ON:return[].concat(o(e),[p({},t)]);case u.NOTE_OFF:var n=(0,l.findActiveNoteIndex)(e,t);return n>=0?[].concat(o(e.slice(0,n)),[p(e[n],t)],o(e.slice(n+1))):e;case u.PITCH_BEND:case u.CHANNEL_PRESSURE:case u.TIMBRE:var r=(0,l.findActiveNoteIndexesByChannel)(e,t);return r.forEach(function(n){e=[].concat(o(e.slice(0,n)),[p(e[n],t)],o(e.slice(n+1)))}),e;case u.NOTE_RELEASED:return e.length?e.filter(function(e){return e.noteState!==s.OFF}):e;case u.ALL_NOTES_OFF:return[]}return e},p=function(){var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:a.ACTIVE_NOTE,t=arguments[1],n=t.noteNumber,r=t.channel,o=t.channelScope,i=t.noteOnVelocity,c=t.noteOffVelocity,f=t.pitch,l=t.pitchBend,d=t.pressure,p=t.timbre;switch(t.type){case u.NOTE_ON:return Object.assign({},e,{noteNumber:n,channel:r,noteOnVelocity:i},f&&{pitch:f},o);case u.NOTE_OFF:return Object.assign({},e,{noteOffVelocity:c,noteState:s.OFF});case u.PITCH_BEND:return Object.assign({},e,{pitchBend:l});case u.CHANNEL_PRESSURE:return Object.assign({},e,{pressure:d});case u.TIMBRE:return Object.assign({},e,{timbre:p})}return e};t["default"]=d},function(e,t,n){"use strict";function r(e){if(e&&e.__esModule)return e;var t={};if(null!=e)for(var n in e)Object.prototype.hasOwnProperty.call(e,n)&&(t[n]=e[n]);return t["default"]=e,t}function o(e,t,n){return t in e?Object.defineProperty(e,t,{value:n,enumerable:!0,configurable:!0,writable:!0}):e[t]=n,e}Object.defineProperty(t,"__esModule",{value:!0});var i=n(21),u=r(i),c=n(19),a=r(c),f=function(){var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:a.CHANNEL_SCOPES,t=arguments[1];if(!u[t.type])return e;var n=t.channel;return Object.assign({},e,o({},n,s(e[n],t)))},s=function(e,t){switch(t.type){case u.PITCH_BEND:return Object.assign({},e,{pitchBend:t.pitchBend});case u.CHANNEL_PRESSURE:return Object.assign({},e,{pressure:t.pressure});case u.TIMBRE:return Object.assign({},e,{timbre:t.timbre});case u.NOTE_ON:case u.NOTE_OFF:return a.CHANNEL_SCOPE}return e};t["default"]=f}]); --------------------------------------------------------------------------------