├── .eslintrc.json ├── .gitignore ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── app.yaml ├── assets ├── .gitkeep ├── audio │ └── drums │ │ ├── 36.mp3 │ │ ├── 38.mp3 │ │ ├── 42.mp3 │ │ ├── 45.mp3 │ │ ├── 46.mp3 │ │ ├── 48.mp3 │ │ ├── 49.mp3 │ │ ├── 50.mp3 │ │ └── 51.mp3 ├── drums_small │ ├── decoder_multi_rnn_cell_cell_0_lstm_cell_bias │ ├── decoder_multi_rnn_cell_cell_0_lstm_cell_kernel │ ├── decoder_multi_rnn_cell_cell_1_lstm_cell_bias │ ├── decoder_multi_rnn_cell_cell_1_lstm_cell_kernel │ ├── decoder_output_projection_bias │ ├── decoder_output_projection_kernel │ ├── decoder_z_to_initial_state_bias │ ├── decoder_z_to_initial_state_kernel │ ├── encoder_cell_0_bidirectional_rnn_bw_multi_rnn_cell_cell_0_lstm_cell_bias │ ├── encoder_cell_0_bidirectional_rnn_bw_multi_rnn_cell_cell_0_lstm_cell_kernel │ ├── encoder_cell_0_bidirectional_rnn_fw_multi_rnn_cell_cell_0_lstm_cell_bias │ ├── encoder_cell_0_bidirectional_rnn_fw_multi_rnn_cell_cell_0_lstm_cell_kernel │ ├── encoder_mu_bias │ ├── encoder_mu_kernel │ └── manifest.json ├── fonts │ └── FoundersGrotesk-Regular.otf └── images │ ├── ai-experiment.svg │ ├── beat-blender-social.jpeg │ ├── beat-blender-social.png │ ├── blend-palindrome-2.gif │ ├── edit.png │ ├── exit.svg │ ├── favicon-small.png │ ├── favicon.png │ ├── friends-at-google.svg │ ├── magenta-logo.png │ ├── pause-button.svg │ ├── play-button.svg │ ├── screenshot.jpg │ └── tempo.svg ├── index.html ├── js ├── canvas-utils.js ├── commands │ ├── on-draw-path.js │ ├── on-draw-release.js │ ├── on-puck-drag.js │ └── undo-manager.js ├── debug-mode.js ├── firebase.js ├── get-mouse-position.js ├── hub.js ├── index.js ├── midi-out.js ├── models │ ├── color.js │ ├── modals.js │ ├── play-modes.js │ └── responsive.js ├── samples.js ├── utils.js └── views │ ├── base-view.js │ ├── button.js │ ├── debug-layout-grid.js │ ├── grid.js │ ├── layout-containers.js │ ├── modal.js │ ├── preset-button-group.js │ ├── select-midi-out.js │ ├── sequence-player.js │ ├── sequencer-grid.js │ ├── sequencer-label.js │ ├── spinner.js │ └── tiles.js ├── json ├── preset-beats.json ├── response.json ├── response2.json ├── response3.json └── response4.json ├── less ├── _mixins.less ├── base-button.less ├── base-select.less ├── bpm.less ├── edit-sequencer-button.less ├── grid-view.less ├── layout.less ├── midi-out.less ├── modal.less ├── play-pause-button.less ├── preset-buttons.less ├── responsive.less ├── spinner.less ├── splash.less ├── style.less ├── tile.less └── toggle.less ├── main.py ├── package.json └── yarn.lock /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "commonjs": true, 5 | "es6": true 6 | }, 7 | "extends": "eslint:recommended", 8 | "parserOptions": { 9 | "sourceType": "module", 10 | "ecmaVersion": 2017 11 | }, 12 | "rules": { 13 | "no-extra-boolean-cast": 0, 14 | "no-console": 0, 15 | "indent": [ 16 | "error", 17 | 4 18 | ], 19 | "linebreak-style": [ 20 | "error", 21 | "unix" 22 | ], 23 | "quotes": [ 24 | "error", 25 | "single" 26 | ], 27 | "semi": [ 28 | "error", 29 | "always" 30 | ] 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | npm-debug.log 2 | yarn-error.log 3 | .idea 4 | *.pyc 5 | assets/*.js 6 | assets/*.css 7 | node_modules/ 8 | .sass-cache 9 | .DS_Store 10 | *.swp 11 | public/* 12 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # How to Contribute 2 | 3 | We'd love to accept your patches and contributions to this project. There are 4 | just a few small guidelines you need to follow. 5 | 6 | ## Contributor License Agreement 7 | 8 | Contributions to this project must be accompanied by a Contributor License 9 | Agreement. You (or your employer) retain the copyright to your contribution, 10 | this simply gives us permission to use and redistribute your contributions as 11 | part of the project. Head over to to see 12 | your current agreements on file or to sign a new one. 13 | 14 | You generally only need to submit a CLA once, so if you've already submitted one 15 | (even if it was for a different project), you probably don't need to do it 16 | again. 17 | 18 | ## Code reviews 19 | 20 | All submissions, including submissions by project members, require review. We 21 | use GitHub pull requests for this purpose. Consult 22 | [GitHub Help](https://help.github.com/articles/about-pull-requests/) for more 23 | information on using pull requests. 24 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Beat Blender 2 | ### Blend beats using machine learning to create music in a fun new way. [g.co/beatblender](https://g.co/beatblender) 3 | 4 | Built using [deeplearn.js](https://deeplearnjs.org) and [MusicVAE](https://github.com/tensorflow/magenta/tree/master/magenta/models/music_vae). 5 | 6 | ![Beat Blender](./assets/images/screenshot.jpg) 7 | 8 | 9 | 10 | ## Usage 11 | 12 | Beat Blender requires the [GCloud SDK](https://cloud.google.com/sdk/downloads) for running the server and [node + npm](http://nodejs.org) for javascript development. 13 | 14 | 15 | 1. Run `npm install` to install all of the dependencies of this project. 16 | 2. Run `npm start` to begin all file-watchers and to initialize the server on port `8080` 17 | 3. Open `http://localhost:8080` 18 | 19 | 20 | ## Easter eggs 21 | You have added a few query-strings that you can play with, you can load a different model for MusicVAE using `checkpoint=url` or a custom model for generating new beats with `samplerCheckpoint=url` 22 | 23 | 24 | ## Contributors 25 | Made by [Kyle Phillips](http://haptic-data.com) and [Torin Blankensmith](http://torinblankensmith.com) with friends at the Google Creative Lab in collaboration with [Adam Roberts](https://github.com/adarob) and the Magenta team. 26 | 27 | 28 | This is not an officially supported Google product. 29 | 30 | ## License 31 | Copyright 2017 Google Inc. 32 | Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at 33 | http://www.apache.org/licenses/LICENSE-2.0 34 | 35 | 36 | Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. 37 | 38 | ## Final Thoughts 39 | We encourage open sourcing projects as a way of learning from each other. Please respect our and other creators’ rights, including copyright and trademark rights when present, when sharing these works and creating derivative work. 40 | 41 | If you want more info on Google's policy, you can find that [here](https://www.google.com/policies/). 42 | -------------------------------------------------------------------------------- /app.yaml: -------------------------------------------------------------------------------- 1 | # Copyright 2018 Google Inc. 2 | 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | runtime: python27 16 | api_version: 1 17 | threadsafe: true 18 | 19 | handlers: 20 | 21 | - url: / 22 | script: main.app 23 | 24 | - url: /ai/beat-blender/view 25 | script: main.app 26 | 27 | - url: /ai/beat-blender/view/ 28 | static_files: index.html 29 | upload: index.html 30 | 31 | - url: /ai/beat-blender/view/main 32 | static_files: index.html 33 | upload: index.html 34 | 35 | - url: /ai/beat-blenader/view/share 36 | static_files: index.html 37 | upload: index.html 38 | 39 | - url: /ai/beat-blenader/view/share/ 40 | static_files: index.html 41 | upload: index.html 42 | 43 | - url: /ai/beat-blender/view/share/.* 44 | static_files: index.html 45 | upload: index.html 46 | 47 | - url: /ai/beat-blender/view/about 48 | static_files: index.html 49 | upload: index.html 50 | 51 | - url: /ai/beat-bleander/view/main 52 | static_files: index.html 53 | upload: index.html 54 | 55 | - url: /ai/beat-blender/view/assets 56 | static_dir: assets 57 | 58 | - url: /ai/beat-blender/view/third_party 59 | static_dir: third_party 60 | 61 | skip_files: 62 | - ^.git/.* 63 | - ^.*node_modules(/.*)? 64 | - ^node_modules/(.*/)? 65 | - ^js/(.*/)? 66 | - ^json/(.*/)? 67 | - ^less/(.*/)? 68 | - ^assets/big_melodies/(.*/)? 69 | - ^big_loopz_mel/(.*/)? 70 | - ^ml-loopz-demo-website/(.*/)? 71 | - ^big_loopz_mel/(.*/)? 72 | - ^assets/big_melodies/(.*/)? 73 | -------------------------------------------------------------------------------- /assets/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/googlecreativelab/beat-blender/af42ce5ce4fdcfc981b3dc7f84cc70ee7d9302b3/assets/.gitkeep -------------------------------------------------------------------------------- /assets/audio/drums/36.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/googlecreativelab/beat-blender/af42ce5ce4fdcfc981b3dc7f84cc70ee7d9302b3/assets/audio/drums/36.mp3 -------------------------------------------------------------------------------- /assets/audio/drums/38.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/googlecreativelab/beat-blender/af42ce5ce4fdcfc981b3dc7f84cc70ee7d9302b3/assets/audio/drums/38.mp3 -------------------------------------------------------------------------------- /assets/audio/drums/42.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/googlecreativelab/beat-blender/af42ce5ce4fdcfc981b3dc7f84cc70ee7d9302b3/assets/audio/drums/42.mp3 -------------------------------------------------------------------------------- /assets/audio/drums/45.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/googlecreativelab/beat-blender/af42ce5ce4fdcfc981b3dc7f84cc70ee7d9302b3/assets/audio/drums/45.mp3 -------------------------------------------------------------------------------- /assets/audio/drums/46.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/googlecreativelab/beat-blender/af42ce5ce4fdcfc981b3dc7f84cc70ee7d9302b3/assets/audio/drums/46.mp3 -------------------------------------------------------------------------------- /assets/audio/drums/48.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/googlecreativelab/beat-blender/af42ce5ce4fdcfc981b3dc7f84cc70ee7d9302b3/assets/audio/drums/48.mp3 -------------------------------------------------------------------------------- /assets/audio/drums/49.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/googlecreativelab/beat-blender/af42ce5ce4fdcfc981b3dc7f84cc70ee7d9302b3/assets/audio/drums/49.mp3 -------------------------------------------------------------------------------- /assets/audio/drums/50.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/googlecreativelab/beat-blender/af42ce5ce4fdcfc981b3dc7f84cc70ee7d9302b3/assets/audio/drums/50.mp3 -------------------------------------------------------------------------------- /assets/audio/drums/51.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/googlecreativelab/beat-blender/af42ce5ce4fdcfc981b3dc7f84cc70ee7d9302b3/assets/audio/drums/51.mp3 -------------------------------------------------------------------------------- /assets/drums_small/decoder_multi_rnn_cell_cell_0_lstm_cell_bias: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/googlecreativelab/beat-blender/af42ce5ce4fdcfc981b3dc7f84cc70ee7d9302b3/assets/drums_small/decoder_multi_rnn_cell_cell_0_lstm_cell_bias -------------------------------------------------------------------------------- /assets/drums_small/decoder_multi_rnn_cell_cell_0_lstm_cell_kernel: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/googlecreativelab/beat-blender/af42ce5ce4fdcfc981b3dc7f84cc70ee7d9302b3/assets/drums_small/decoder_multi_rnn_cell_cell_0_lstm_cell_kernel -------------------------------------------------------------------------------- /assets/drums_small/decoder_multi_rnn_cell_cell_1_lstm_cell_bias: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/googlecreativelab/beat-blender/af42ce5ce4fdcfc981b3dc7f84cc70ee7d9302b3/assets/drums_small/decoder_multi_rnn_cell_cell_1_lstm_cell_bias -------------------------------------------------------------------------------- /assets/drums_small/decoder_multi_rnn_cell_cell_1_lstm_cell_kernel: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/googlecreativelab/beat-blender/af42ce5ce4fdcfc981b3dc7f84cc70ee7d9302b3/assets/drums_small/decoder_multi_rnn_cell_cell_1_lstm_cell_kernel -------------------------------------------------------------------------------- /assets/drums_small/decoder_output_projection_bias: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/googlecreativelab/beat-blender/af42ce5ce4fdcfc981b3dc7f84cc70ee7d9302b3/assets/drums_small/decoder_output_projection_bias -------------------------------------------------------------------------------- /assets/drums_small/decoder_output_projection_kernel: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/googlecreativelab/beat-blender/af42ce5ce4fdcfc981b3dc7f84cc70ee7d9302b3/assets/drums_small/decoder_output_projection_kernel -------------------------------------------------------------------------------- /assets/drums_small/decoder_z_to_initial_state_bias: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/googlecreativelab/beat-blender/af42ce5ce4fdcfc981b3dc7f84cc70ee7d9302b3/assets/drums_small/decoder_z_to_initial_state_bias -------------------------------------------------------------------------------- /assets/drums_small/decoder_z_to_initial_state_kernel: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/googlecreativelab/beat-blender/af42ce5ce4fdcfc981b3dc7f84cc70ee7d9302b3/assets/drums_small/decoder_z_to_initial_state_kernel -------------------------------------------------------------------------------- /assets/drums_small/encoder_cell_0_bidirectional_rnn_bw_multi_rnn_cell_cell_0_lstm_cell_bias: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/googlecreativelab/beat-blender/af42ce5ce4fdcfc981b3dc7f84cc70ee7d9302b3/assets/drums_small/encoder_cell_0_bidirectional_rnn_bw_multi_rnn_cell_cell_0_lstm_cell_bias -------------------------------------------------------------------------------- /assets/drums_small/encoder_cell_0_bidirectional_rnn_bw_multi_rnn_cell_cell_0_lstm_cell_kernel: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/googlecreativelab/beat-blender/af42ce5ce4fdcfc981b3dc7f84cc70ee7d9302b3/assets/drums_small/encoder_cell_0_bidirectional_rnn_bw_multi_rnn_cell_cell_0_lstm_cell_kernel -------------------------------------------------------------------------------- /assets/drums_small/encoder_cell_0_bidirectional_rnn_fw_multi_rnn_cell_cell_0_lstm_cell_bias: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/googlecreativelab/beat-blender/af42ce5ce4fdcfc981b3dc7f84cc70ee7d9302b3/assets/drums_small/encoder_cell_0_bidirectional_rnn_fw_multi_rnn_cell_cell_0_lstm_cell_bias -------------------------------------------------------------------------------- /assets/drums_small/encoder_cell_0_bidirectional_rnn_fw_multi_rnn_cell_cell_0_lstm_cell_kernel: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/googlecreativelab/beat-blender/af42ce5ce4fdcfc981b3dc7f84cc70ee7d9302b3/assets/drums_small/encoder_cell_0_bidirectional_rnn_fw_multi_rnn_cell_cell_0_lstm_cell_kernel -------------------------------------------------------------------------------- /assets/drums_small/encoder_mu_bias: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/googlecreativelab/beat-blender/af42ce5ce4fdcfc981b3dc7f84cc70ee7d9302b3/assets/drums_small/encoder_mu_bias -------------------------------------------------------------------------------- /assets/drums_small/encoder_mu_kernel: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/googlecreativelab/beat-blender/af42ce5ce4fdcfc981b3dc7f84cc70ee7d9302b3/assets/drums_small/encoder_mu_kernel -------------------------------------------------------------------------------- /assets/drums_small/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "decoder/multi_rnn_cell/cell_0/lstm_cell/bias": { 3 | "filename": "decoder_multi_rnn_cell_cell_0_lstm_cell_bias", 4 | "shape": [ 5 | 1024 6 | ] 7 | }, 8 | "decoder/multi_rnn_cell/cell_0/lstm_cell/kernel": { 9 | "filename": "decoder_multi_rnn_cell_cell_0_lstm_cell_kernel", 10 | "shape": [ 11 | 1024, 12 | 1024 13 | ] 14 | }, 15 | "decoder/multi_rnn_cell/cell_1/lstm_cell/bias": { 16 | "filename": "decoder_multi_rnn_cell_cell_1_lstm_cell_bias", 17 | "shape": [ 18 | 1024 19 | ] 20 | }, 21 | "decoder/multi_rnn_cell/cell_1/lstm_cell/kernel": { 22 | "filename": "decoder_multi_rnn_cell_cell_1_lstm_cell_kernel", 23 | "shape": [ 24 | 512, 25 | 1024 26 | ] 27 | }, 28 | "decoder/output_projection/bias": { 29 | "filename": "decoder_output_projection_bias", 30 | "shape": [ 31 | 512 32 | ] 33 | }, 34 | "decoder/output_projection/kernel": { 35 | "filename": "decoder_output_projection_kernel", 36 | "shape": [ 37 | 256, 38 | 512 39 | ] 40 | }, 41 | "decoder/z_to_initial_state/bias": { 42 | "filename": "decoder_z_to_initial_state_bias", 43 | "shape": [ 44 | 1024 45 | ] 46 | }, 47 | "decoder/z_to_initial_state/kernel": { 48 | "filename": "decoder_z_to_initial_state_kernel", 49 | "shape": [ 50 | 256, 51 | 1024 52 | ] 53 | }, 54 | "encoder/cell_0/bidirectional_rnn/bw/multi_rnn_cell/cell_0/lstm_cell/bias": { 55 | "filename": "encoder_cell_0_bidirectional_rnn_bw_multi_rnn_cell_cell_0_lstm_cell_bias", 56 | "shape": [ 57 | 2048 58 | ] 59 | }, 60 | "encoder/cell_0/bidirectional_rnn/bw/multi_rnn_cell/cell_0/lstm_cell/kernel": { 61 | "filename": "encoder_cell_0_bidirectional_rnn_bw_multi_rnn_cell_cell_0_lstm_cell_kernel", 62 | "shape": [ 63 | 522, 64 | 2048 65 | ] 66 | }, 67 | "encoder/cell_0/bidirectional_rnn/fw/multi_rnn_cell/cell_0/lstm_cell/bias": { 68 | "filename": "encoder_cell_0_bidirectional_rnn_fw_multi_rnn_cell_cell_0_lstm_cell_bias", 69 | "shape": [ 70 | 2048 71 | ] 72 | }, 73 | "encoder/cell_0/bidirectional_rnn/fw/multi_rnn_cell/cell_0/lstm_cell/kernel": { 74 | "filename": "encoder_cell_0_bidirectional_rnn_fw_multi_rnn_cell_cell_0_lstm_cell_kernel", 75 | "shape": [ 76 | 522, 77 | 2048 78 | ] 79 | }, 80 | "encoder/mu/bias": { 81 | "filename": "encoder_mu_bias", 82 | "shape": [ 83 | 256 84 | ] 85 | }, 86 | "encoder/mu/kernel": { 87 | "filename": "encoder_mu_kernel", 88 | "shape": [ 89 | 1024, 90 | 256 91 | ] 92 | } 93 | } -------------------------------------------------------------------------------- /assets/fonts/FoundersGrotesk-Regular.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/googlecreativelab/beat-blender/af42ce5ce4fdcfc981b3dc7f84cc70ee7d9302b3/assets/fonts/FoundersGrotesk-Regular.otf -------------------------------------------------------------------------------- /assets/images/ai-experiment.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 8 | 10 | 12 | 13 | 15 | 17 | 19 | 22 | 25 | 27 | 29 | 32 | 35 | 37 | 39 | 41 | 43 | 45 | 48 | 50 | 53 | 56 | 58 | 59 | -------------------------------------------------------------------------------- /assets/images/beat-blender-social.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/googlecreativelab/beat-blender/af42ce5ce4fdcfc981b3dc7f84cc70ee7d9302b3/assets/images/beat-blender-social.jpeg -------------------------------------------------------------------------------- /assets/images/beat-blender-social.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/googlecreativelab/beat-blender/af42ce5ce4fdcfc981b3dc7f84cc70ee7d9302b3/assets/images/beat-blender-social.png -------------------------------------------------------------------------------- /assets/images/blend-palindrome-2.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/googlecreativelab/beat-blender/af42ce5ce4fdcfc981b3dc7f84cc70ee7d9302b3/assets/images/blend-palindrome-2.gif -------------------------------------------------------------------------------- /assets/images/edit.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/googlecreativelab/beat-blender/af42ce5ce4fdcfc981b3dc7f84cc70ee7d9302b3/assets/images/edit.png -------------------------------------------------------------------------------- /assets/images/exit.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Close 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /assets/images/favicon-small.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/googlecreativelab/beat-blender/af42ce5ce4fdcfc981b3dc7f84cc70ee7d9302b3/assets/images/favicon-small.png -------------------------------------------------------------------------------- /assets/images/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/googlecreativelab/beat-blender/af42ce5ce4fdcfc981b3dc7f84cc70ee7d9302b3/assets/images/favicon.png -------------------------------------------------------------------------------- /assets/images/friends-at-google.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 8 | 10 | 13 | 16 | 19 | 21 | 23 | 25 | 27 | 30 | 33 | 36 | 39 | 41 | 43 | 45 | 48 | 50 | 53 | 56 | 58 | 60 | 63 | 66 | 69 | 72 | 76 | 77 | 80 | 83 | 84 | -------------------------------------------------------------------------------- /assets/images/magenta-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/googlecreativelab/beat-blender/af42ce5ce4fdcfc981b3dc7f84cc70ee7d9302b3/assets/images/magenta-logo.png -------------------------------------------------------------------------------- /assets/images/pause-button.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Shape 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /assets/images/play-button.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Shape 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /assets/images/screenshot.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/googlecreativelab/beat-blender/af42ce5ce4fdcfc981b3dc7f84cc70ee7d9302b3/assets/images/screenshot.jpg -------------------------------------------------------------------------------- /assets/images/tempo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | tempo 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Beat Blender 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 | Beat Blender icon 32 |

Beat Blender

33 |
34 |

Blend beats using machine learning to create music in a fun new way.

35 | 36 |

Built with Deeplearn.js
37 | Learn how it works 38 |

39 |
40 | 43 |
44 |
45 | 46 | 47 |
48 | 49 |
50 | 51 | 52 |
Built using Magenta 53 |
54 |
55 | 56 | 57 |
58 |
59 | Privacy & 60 | Terms 61 |
62 | 63 | 64 |
65 | 66 | 67 |
68 |
69 |
70 |
71 | 72 |
73 |

74 |
75 | 76 |
77 |
78 |
79 |

PRESET BEATS

80 |
81 | 82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 | 91 |
92 |
93 | 101 |
102 |
103 | 104 | 105 | 106 | 115 | 116 | 117 | 118 | -------------------------------------------------------------------------------- /js/canvas-utils.js: -------------------------------------------------------------------------------- 1 | // Copyright 2018 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // https://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | import * as grid2d from 'grid2d'; 16 | import { toCSSString } from './models/color'; 17 | 18 | 19 | /** 20 | * this module is for functions that assist in rendering common application 21 | * data to a canvas, for example a grid 22 | */ 23 | 24 | 25 | export const dpr = ()=> 2; //window.devicePixelRatio || 1; 26 | 27 | 28 | export const setCanvasSize = (canvas, width, height)=>{ 29 | canvas.width = width * dpr(); 30 | canvas.height = height * dpr(); 31 | // scaling to dpr should be covered in css, not in this function 32 | // Object.assign(canvas.style, { 33 | // width: (canvas.width / dpr()) + 'px', 34 | // height: (canvas.height / dpr()) + 'px' 35 | // }); 36 | return canvas; 37 | }; 38 | 39 | 40 | 41 | export const renderToNewCanvas = ({ width, height }, fn)=>{ 42 | const el = document.createElement('canvas'); 43 | el.width = width; 44 | el.height = height; 45 | const ctx = el.getContext('2d'); 46 | fn(ctx); 47 | return el; 48 | }; 49 | 50 | 51 | export const renderGridStrokes = (ctx, grid, color, lineWidth=2, widthScale=1, heightScale=1)=>{ 52 | ctx.strokeStyle = toCSSString(color); 53 | ctx.lineWidth = lineWidth * dpr(); 54 | ctx.beginPath(); 55 | const h = 2; 56 | for(let col=0; coltrue), rounding=Math.ceil)=>{ 84 | 85 | const cw = rounding(grid2d.cellWidth(grid) * dpr() * widthScale); 86 | const ch = rounding(grid2d.cellHeight(grid) * dpr() * heightScale); 87 | 88 | 89 | for(let x=0; x{ 109 | ctx.strokeStyle = 'white'; 110 | ctx.lineWidth = 10 * dpr(); 111 | ctx.beginPath(); 112 | for(let i=0; i{ 120 | ctx.fillStyle = color; 121 | for(let i=0; i 0.01){ 29 | path.push(pt); 30 | } 31 | 32 | set({ 33 | pathLerp: 0, 34 | path 35 | }); 36 | } 37 | -------------------------------------------------------------------------------- /js/commands/on-draw-release.js: -------------------------------------------------------------------------------- 1 | // Copyright 2018 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // https://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | 16 | import { lerpPath, pathLength, times } from '../utils'; 17 | import * as grid2d from 'grid2d'; 18 | 19 | export default function({ state, set }){ 20 | if(!state.path.length){ 21 | return; 22 | } 23 | 24 | const n = pathLength(state.path) * 30; 25 | const equids = times(n, (i, n)=> lerpPath(state.path, i/(n-1))); 26 | 27 | 28 | const cells = []; 29 | 30 | const points = []; 31 | let lastCell = {}; 32 | for(let i=0; i this.max && safety < 1000){ 81 | this.removeOldest(index); 82 | safety++; 83 | } 84 | this.app.setDeep(`${this.key}.${index}`, hist); 85 | } 86 | 87 | /** 88 | * remove the newest item from history 89 | * @param {number} index for history 90 | * @returns {any} newest item 91 | */ 92 | popHistory(index){ 93 | assert(this.size(index) > 0, 'history is empty'); 94 | const hist = this.get(index); 95 | const last = hist.shift(); 96 | this.app.setDeep(`${this.key}.${index}`, hist); 97 | return last; 98 | } 99 | 100 | removeOldest(index){ 101 | assert(this.size(index) > 0, 'history is empty'); 102 | const hist = this.get(index); 103 | const oldest = hist.pop(); 104 | this.app.setDeep(`${this.key}.${index}`, hist); 105 | return oldest; 106 | } 107 | 108 | size(index){ 109 | return this.app.state[this.key][index].length; 110 | } 111 | } 112 | 113 | -------------------------------------------------------------------------------- /js/debug-mode.js: -------------------------------------------------------------------------------- 1 | // Copyright 2018 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // https://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | import { save, get } from './firebase'; 16 | import DAT from 'dat-gui'; 17 | 18 | 19 | let _initialized = false; 20 | 21 | 22 | export const isInitialized = ()=>_initialized; 23 | 24 | 25 | export const initialize = function(state, page){ 26 | if(_initialized){ 27 | return; 28 | } 29 | _initialized = true; 30 | const gui = new DAT.GUI(); 31 | 32 | get().then(snapshot=>{ 33 | 34 | const shareState = { 35 | save: ()=> save(state).then((snapshot)=> console.log(snapshot)), 36 | selected: null 37 | }; 38 | 39 | const shares = snapshot.val(); 40 | 41 | gui.add(shareState, 'selected', Object.keys(shares)).onChange(key=>{ 42 | page(`/share/${key}`); 43 | }); 44 | 45 | gui.add(shareState, 'save'); 46 | }); 47 | }; 48 | -------------------------------------------------------------------------------- /js/firebase.js: -------------------------------------------------------------------------------- 1 | // Copyright 2018 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // https://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | /*global: firebase */ 16 | import { generate as uuid } from 'shortid'; 17 | const { firebase } = window; 18 | 19 | // Initialize Firebase 20 | const config = { 21 | //put your firebase config here 22 | }; 23 | 24 | 25 | //uncomment here to use database 26 | //firebase.initializeApp(config); 27 | 28 | 29 | //the keys in state that we care about storing 30 | const keys = [ 31 | 'encodedSequences', 32 | 'sequenceUUID', 33 | 'selectedIndex', 34 | 'sequence', 35 | 'bpm', 36 | 'playMode', 37 | 'path', 38 | 'pathIntersections', 39 | 'pathLerp', 40 | 'puck', 41 | 'gradient', 42 | 'grid' 43 | ]; 44 | 45 | const parseState = (state)=> 46 | keys.reduce((mem, key)=>{ 47 | mem[key] = state[key]; 48 | return mem; 49 | }, {}); 50 | 51 | 52 | export const save = (state)=>{ 53 | const id = uuid(); 54 | return firebase.database().ref(`shares/${id}`) 55 | .set(parseState(state)) 56 | .then(function(){ console.log(arguments); return id; }); 57 | }; 58 | 59 | export const get = (uid='')=> 60 | firebase.database().ref(`shares/${uid}`).once('value'); 61 | 62 | 63 | 64 | -------------------------------------------------------------------------------- /js/get-mouse-position.js: -------------------------------------------------------------------------------- 1 | // Copyright 2018 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // https://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | /** 16 | * get-mouse-position is a reliable way to get the current mouse 17 | * position relative to the element provided, considering all offest 18 | * @example getMousePosition(domElement, event): {x, y} 19 | */ 20 | 21 | //window has a circular window.window reference 22 | var isWindow = function( elem ){ 23 | return elem !== null && elem === elem.window; 24 | }; 25 | 26 | var getWindow = function( elem ){ 27 | return isWindow(elem) ? elem : elem.nodeType === 9 ? elem.defaultView || elem.parentWindow : false; 28 | }; 29 | 30 | //calculate the page-offset of the element 31 | /** 32 | * calculate the offset of an element 33 | * @param {HTMLElement} element 34 | * @return { x:Number, y: Number } 35 | */ 36 | export function offset( elem, ignoreWindowOffset ){ 37 | //true if not explicitly false 38 | ignoreWindowOffset = (ignoreWindowOffset === true); 39 | var docElem, 40 | win, 41 | box = { 42 | top: 0, 43 | left: 0 44 | }, 45 | doc = elem && elem.ownerDocument; 46 | 47 | box.x = box.left; 48 | box.y = box.top; 49 | 50 | if (!doc) { 51 | return box; 52 | } 53 | 54 | docElem = doc.documentElement; 55 | 56 | // Make sure it's not a disconnected DOM node 57 | if (!document.body.contains(elem)) { 58 | return box; 59 | } 60 | 61 | // If we don't have gBCR, just use 0,0 rather than error 62 | // BlackBerry 5, iOS 3 (original iPhone) 63 | if (typeof elem.getBoundingClientRect !== 'undefined') { 64 | box = elem.getBoundingClientRect(); 65 | box.x = box.left; 66 | box.y = box.top; 67 | } 68 | win = getWindow(doc); 69 | var page = { x: 0, y: 0 }; 70 | if( !ignoreWindowOffset ){ 71 | page.y = win.pageYOffset || docElem.scrollTop; 72 | page.x = win.pageXOffset || docElem.scrollLeft; 73 | } 74 | 75 | const box2 = {}; 76 | Object.assign(box2, box); 77 | 78 | box2.top = box.y = box.top + page.y - (docElem.clientTop || 0); 79 | box2.left = box.x = box.left + page.x - (docElem.clientLeft || 0); 80 | /*box.top = box.y = box.top + win.pageYOffset - docElem.clientTop; 81 | box.left = box.x = box.left + win.pageXOffset - docElem.clientLeft;*/ 82 | 83 | return box2; 84 | } 85 | 86 | 87 | /** 88 | * get the mouse position within the provided HTMLElement 89 | * @param {HTMLElement} element 90 | * @param {Event} event this requires to an event object 91 | * @param {Boolean} [ignoreWindowOffset] optionally ignore the window.pageYOffset 92 | * @return { x:Number, y:Number, toString:Function } 93 | */ 94 | export default function getMousePosition(element, event, ignoreWindowOffset, point={} ){ 95 | const off = offset(element, ignoreWindowOffset); 96 | if( event.touches && event.touches.length === 1 ){ 97 | event = event.touches[0]; 98 | } 99 | 100 | point.x = event.clientX - off.left; 101 | point.y = event.clientY - off.top; 102 | return point; 103 | } 104 | -------------------------------------------------------------------------------- /js/hub.js: -------------------------------------------------------------------------------- 1 | // Copyright 2018 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // https://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | import eventMap from 'event-map'; 16 | import assert from 'assert'; 17 | import { generate as uuid } from 'shortid'; 18 | import deepMixin from 'mixin-deep'; 19 | import deepExtend from 'deep-extend'; 20 | import { set as setObjectPath } from 'object-path'; 21 | 22 | Object.freeze = Object.freeze || (()=>{}); 23 | 24 | 25 | /** 26 | * this module is a home-rolled minimal mvc manager 27 | */ 28 | export default function app({ options, state, computed }){ 29 | 30 | options = options || {}; 31 | computed = computed || {}; 32 | 33 | const DEBUG = !!options.debug; 34 | const IMMUTABLE = !!options.immutable; 35 | 36 | const removeEventsMap = {}; 37 | const uuidMap = {}; 38 | 39 | const findUuid = (view)=>{ 40 | for(let key in uuidMap){ 41 | if(uuidMap[key] === view){ 42 | return key; 43 | } 44 | } 45 | }; 46 | 47 | const getNextState = (changes)=> 48 | applyComputed((IMMUTABLE ? deepExtend : deepMixin)({}, app.state, changes)); 49 | 50 | //Object.freeze(state); 51 | 52 | //start dirty, first render should be (state, {}) 53 | let stateIsDirty = true; 54 | 55 | const views = []; 56 | 57 | //this is what gets exported 58 | const app = { 59 | lastState: {}, 60 | state: {}, 61 | views, 62 | set, 63 | setDeep, 64 | onStart, 65 | onStop, 66 | addView, 67 | removeView, 68 | render, 69 | toJSON 70 | }; 71 | 72 | 73 | app.set(state); 74 | 75 | function applyComputed(state){ 76 | for(let prop in computed){ 77 | setObjectPath(state, prop, computed[prop](state, app.lastState)); 78 | } 79 | return state; 80 | } 81 | 82 | /** 83 | * provide a pattern to a single deep property 84 | * @example setDeep('gradient.0.3', 1) //will set state.gradient[0][3] = 1 85 | * @param pattern 86 | * @param value 87 | * @param cb 88 | */ 89 | function setDeep(pattern, value, cb=(()=>{})){ 90 | let before, after; 91 | if(DEBUG) { 92 | before = performance.now(); 93 | } 94 | const nextState = getNextState(); 95 | setObjectPath(nextState, pattern, value); 96 | app.state = nextState; 97 | stateIsDirty = true; 98 | if(DEBUG) { 99 | after = performance.now(); 100 | console.log(`set deep took ${after - before}ms`); 101 | } 102 | cb(app.state); 103 | } 104 | 105 | /** 106 | * provide an object of changes to merge into the current state, overwriting any existing 107 | * @param changes 108 | * @param cb 109 | */ 110 | function set(changes, cb=(()=>{})){ 111 | //go through and deep-merge changes 112 | let before, after; 113 | if(DEBUG) { 114 | before = performance.now(); 115 | } 116 | const nextState = getNextState(changes); 117 | //const stateDiff = diff(state, nextState); 118 | if(DEBUG){ 119 | after = performance.now(); 120 | console.log(`${after-before} ms elapsed`); 121 | } 122 | app.state = nextState; 123 | //app.state = Object.freeze(nextState); 124 | stateIsDirty = true; 125 | cb(app.state); 126 | } 127 | 128 | /** 129 | * add a view, take its event map and listen for it, providing it with state 130 | * whenever the event occurs 131 | * @param view 132 | * @returns {{ 133 | * lastState:{}, 134 | * state:{}, 135 | * views:Array, 136 | * set:Function, 137 | * addView:Function, 138 | * removeView:Function, 139 | * render:Function 140 | * }} 141 | */ 142 | function addView(view){ 143 | if(Array.isArray(view)){ 144 | //accept receiving multiple views as Array 145 | view.forEach(addView); 146 | return app; 147 | } 148 | 149 | assert.equal(views.indexOf(view), -1, `View ${view} already exists`); 150 | 151 | const id = uuid(); 152 | 153 | uuidMap[id] = view; 154 | 155 | const events = {}; 156 | for(let prop in view.events){ 157 | events[prop] = (event)=> view.events[prop](event, app.state, app.lastState); 158 | } 159 | 160 | if(Object.keys(events).length) { 161 | //every view has a uuid and that will be used for registering the event map 162 | removeEventsMap[id] = view.domElement ? eventMap(view.domElement, events) : eventMap(events); 163 | } 164 | 165 | views.push(view); 166 | return app; 167 | } 168 | 169 | /** 170 | * remove a view, clean up its listeners as well 171 | * @param view 172 | * @returns {{ 173 | * lastState:{}, 174 | * state:{}, 175 | * views:Array, 176 | * set:Function, 177 | * addView:Function, 178 | * removeView:Function, 179 | * render:Function 180 | * }} 181 | */ 182 | function removeView(view){ 183 | const index = views.indexOf(view); 184 | const id = findUuid(view); 185 | assert.notEqual(index, -1, `View ${view} does not exist`); 186 | assert(!!id, 'View uuid could not be found'); 187 | const removeEvents = removeEventsMap[id]; 188 | removeEvents && removeEvents(); 189 | view.remove && view.remove(); 190 | views.splice(index, 1); 191 | return app; 192 | } 193 | 194 | function onStart(){ 195 | for(let view of views){ 196 | view.onStart && view.onStart(app.state, app.lastState); 197 | } 198 | } 199 | 200 | function onStop(){ 201 | for(let view of views){ 202 | view.onStop && view.onStop(app.state, app.lastState); 203 | } 204 | } 205 | 206 | 207 | function render(){ 208 | if(!stateIsDirty){ 209 | return; 210 | } 211 | for(let i=0; i { 28 | assert.equal(typeof onChange, 'function', `requires a function to report changes to, received ${onChange}`); 29 | 30 | const updateMidiOutState = () => { 31 | onChange(null, midiDevices); 32 | }; 33 | 34 | if(typeof navigator.requestMIDIAccess !== 'function'){ 35 | onChange(new Error('navigator.requestMIDIAccess does not exist')); 36 | return; 37 | } 38 | 39 | navigator.requestMIDIAccess().then((midi) => { 40 | 41 | midi.outputs.forEach((output) => { 42 | /*console.log(` 43 | Output midi device [type: '${output.type}'] 44 | id: ${output.id} 45 | manufacturer: ${output.manufacturer} 46 | name:${output.name} 47 | version: ${output.version}`);*/ 48 | midiDevices[output.name] = output; 49 | }); 50 | 51 | midi.onstatechange = (e) => { 52 | if(e.port.state == 'disconnected') { 53 | delete midiDevices[e.port.name]; 54 | updateMidiOutState(); 55 | } else if(e.port.state == 'connected') { 56 | if(!(e.port.name in midiDevices)) { 57 | midi.outputs.forEach((output) => { 58 | if(output.name == e.port.name){ 59 | midiDevices[e.port.name] = output; 60 | updateMidiOutState(); 61 | } 62 | }); 63 | } 64 | } 65 | }; 66 | updateMidiOutState(); 67 | }, onChange); 68 | }; 69 | 70 | /** 71 | * send a MIDI Note to a MIDI Device 72 | * @param {String} outputDeviceName the device to use 73 | * @param {Number} outnoteNum the note to send 74 | * @param {Boolean} shouldTurnNoteOn are we turning this note on or off? 75 | * @param {Number} currVelocity velocity to send note at 76 | * @param {Number} channel 77 | */ 78 | export const sendMIDIToDevice = (outputDeviceName, outnoteNum, shouldTurnNoteOn, currVelocity, channel) => { 79 | const outputDevice = midiDevices[outputDeviceName]; 80 | assert(outputDevice, `MIDI Device ${outputDeviceName} does not exist`); 81 | const eventToSend = shouldTurnNoteOn ? MIDI_EVENT_ON : MIDI_EVENT_OFF; 82 | outputDevice.send([eventToSend + channel - 1, outnoteNum, 0x03]); 83 | }; 84 | 85 | -------------------------------------------------------------------------------- /js/models/color.js: -------------------------------------------------------------------------------- 1 | // Copyright 2018 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // https://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | 16 | import { lerpArray, times } from '../utils'; 17 | import assert from 'assert'; 18 | 19 | /** 20 | * Generate a gradient that interopolates across 2 dimensions 21 | * @param {Array} tl top-left color 22 | * @param {Array} tr top-right color 23 | * @param {Array} bl bottom-left color 24 | * @param {Array} br bottom-right color 25 | * @param {Number} columns 26 | * @param {Number} rows 27 | */ 28 | export const generate4PointGradient = (tl, tr, bl, br, columns, rows)=>{ 29 | return times(columns, (x,columns)=>{ 30 | const cp = x / (columns-1); 31 | const topColor = lerpArray(tl, tr, cp); 32 | const bottomColor = lerpArray(bl, br, cp); 33 | 34 | return times(rows, (y, rows)=>{ 35 | const rp = y/ (rows-1); 36 | return lerpArray(topColor, bottomColor, rp); 37 | }); 38 | }); 39 | }; 40 | 41 | 42 | // reuse these arrays always to avoid extra garbage collection 43 | const lerpTmpTop = []; 44 | const lerpTmpBot = []; 45 | 46 | const shrinkToLength = (arr, length)=>{ 47 | let attempts = 0; 48 | const maxAttempts = 10000; 49 | while(arr.length > length && attempts < maxAttempts){ 50 | arr.pop(); 51 | attempts++; 52 | } 53 | assert(attempts < maxAttempts, 'maxAttempts reached, Wow, somehow that array got really full'); 54 | return arr; 55 | }; 56 | 57 | /** 58 | * generate a 4-point gradient for one specified cell 59 | * @param {Array} tl top-left color 60 | * @param {Array} tr top-right color 61 | * @param {Array} bl bottom-left color 62 | * @param {Array} br bottom-right color 63 | * @param {Number} percentX percent between 0-1 64 | * @param {Number} percentY perecent between 0-1 65 | * @returns {Array} 66 | */ 67 | export const generate4PointGradientAt = (tl, tr, bl, br, percentX, percentY, result=[])=>{ 68 | //empty any extras (this shouldn't ever actually happen) 69 | shrinkToLength(lerpTmpTop, tl.length); 70 | shrinkToLength(lerpTmpBot, tl.length); 71 | const topColor = lerpArray(tl, tr, percentX, lerpTmpTop); 72 | const bottomColor = lerpArray(bl, br, percentX, lerpTmpBot); 73 | return lerpArray(topColor, bottomColor, percentY, result); 74 | }; 75 | 76 | 77 | /** 78 | * Generate a gradient that 79 | * @param {Array} tl top-left color 80 | * @param {Array} tr top-right color 81 | * @param {Array} bl bottom-left color 82 | * @param {Array} br bottom-right color 83 | * @param {Array} center center color 84 | * @param {Number} columns 85 | * @param {Number} rows 86 | */ 87 | export const generate5PointGradient = (tl, tr, bl, br, center, columns, rows)=>{ 88 | return times(columns, (x,columns)=>{ 89 | const cp = x / (columns-1); 90 | const topColor = lerpArray(tl, tr, cp); 91 | const bottomColor = lerpArray(bl, br, cp); 92 | 93 | return times(rows, (y, rows)=>{ 94 | const rp = y/ (rows-1); 95 | const color = lerpArray(topColor, bottomColor, rp); 96 | const dc = (x - ~~(columns/2)) / columns; 97 | const dr = (y - ~~(rows/2)) / rows; 98 | const dist = Math.sqrt(dc*dc + dr*dr); 99 | 100 | return lerpArray(color, center, 1.0 - dist*2); 101 | }); 102 | }); 103 | }; 104 | 105 | 106 | const numberComparator = (f1,f2)=>{ 107 | if(f1 == f2) return 0; 108 | if(f1 < f2) return -1; 109 | if(f1 > f2) return 1; 110 | }; 111 | 112 | const INV60DEGREES = 60.0 / 360; 113 | 114 | export const rgbToHSV = ([r, g, b])=>{ 115 | const hsv = []; 116 | var h = 0, 117 | s = 0, 118 | v = Math.max(r, g, b), 119 | d = v - Math.min(r, g, b); 120 | 121 | if (v !== 0) { 122 | s = d / v; 123 | } 124 | 125 | if (s !== 0) { 126 | if( numberComparator( r, v ) === 0 ){ 127 | h = (g - b) / d; 128 | } else if ( numberComparator( g, v ) === 0 ) { 129 | h = 2 + (b - r) / d; 130 | } else { 131 | h = 4 + (r - g) / d; 132 | } 133 | } 134 | h *= INV60DEGREES; 135 | if (h < 0) { 136 | h += 1.0; 137 | } 138 | hsv[0] = h; 139 | hsv[1] = s; 140 | hsv[2] = v; 141 | 142 | return hsv; 143 | }; 144 | 145 | 146 | 147 | /** 148 | * Converts HSV values into RGB array. 149 | * @param h 150 | * @param s 151 | * @param v 152 | * @return rgb array 153 | */ 154 | export const hsvToRGB = ([h, s, v])=>{ 155 | const rgb = []; 156 | if(s === 0.0){ 157 | rgb[0] = rgb[1] = rgb[2] = v; 158 | } else { 159 | h /= INV60DEGREES; 160 | var i = parseInt(h,10), 161 | f = h - i, 162 | p = v * (1 - s), 163 | q = v * (1 - s * f), 164 | t = v * (1 - s * (1 - f)); 165 | 166 | if (i === 0) { 167 | rgb[0] = v; 168 | rgb[1] = t; 169 | rgb[2] = p; 170 | } else if (i == 1) { 171 | rgb[0] = q; 172 | rgb[1] = v; 173 | rgb[2] = p; 174 | } else if (i == 2) { 175 | rgb[0] = p; 176 | rgb[1] = v; 177 | rgb[2] = t; 178 | } else if (i == 3) { 179 | rgb[0] = p; 180 | rgb[1] = q; 181 | rgb[2] = v; 182 | } else if (i == 4) { 183 | rgb[0] = t; 184 | rgb[1] = p; 185 | rgb[2] = v; 186 | } else { 187 | rgb[0] = v; 188 | rgb[1] = p; 189 | rgb[2] = q; 190 | } 191 | } 192 | return rgb; 193 | }; 194 | 195 | 196 | export const toCSSString = (color)=> 197 | `rgba(${~~color[0]},${~~color[1]},${~~color[2]}, ${(color[3]||1)})`; 198 | -------------------------------------------------------------------------------- /js/models/modals.js: -------------------------------------------------------------------------------- 1 | // Copyright 2018 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // https://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | 16 | export const NONE = ''; 17 | export const ABOUT = 'about'; 18 | export const SHARE = 'share'; 19 | -------------------------------------------------------------------------------- /js/models/play-modes.js: -------------------------------------------------------------------------------- 1 | // Copyright 2018 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // https://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | //the user is dragging the puck around 1-cell at a time 16 | export const DRAG = 'drag'; 17 | //the user has a path or is creating a paththat is being played in sequence 18 | export const PATH = 'path'; 19 | -------------------------------------------------------------------------------- /js/models/responsive.js: -------------------------------------------------------------------------------- 1 | // Copyright 2018 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // https://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | /** 16 | * this modules responsibility is to set breakpoints for the view and apply a css-style 17 | * for its appropriate dimensions 18 | */ 19 | 20 | 21 | export const DESKTOP = 'desktop'; 22 | export const MOBILE_PORTRAIT = 'mobile-portrait'; 23 | export const MOBILE_LANDSCAPE = 'mobile-landscape'; 24 | export const TABLET = 'tablet'; 25 | 26 | 27 | const queries = [ 28 | ['(max-width: 479px)', MOBILE_PORTRAIT], 29 | ['(orientation: landscape) and (max-height: 440px)', MOBILE_LANDSCAPE], 30 | ['(max-width: 991px)', TABLET] 31 | ]; 32 | 33 | export function is(layout){ 34 | return get() === layout; 35 | } 36 | 37 | export function get(){ 38 | if(!window.matchMedia){ 39 | //possibly a really old browser, if so its desktop 40 | return DESKTOP; 41 | } 42 | 43 | for(let i=0; i} 38 | */ 39 | export const urls = notesBySample.map(notes=> `/ai/beat-blender/view/assets/audio/drums/${notes[0]}.mp3`); 40 | 41 | /** 42 | * this is a map of every unique note to the array-index it is found in 43 | * so if its value is "3" then play `urls[3]` 44 | */ 45 | const noteFlatMap = notesBySample.reduce((mem, notes, i)=>{ 46 | notes.forEach(note=> mem[note] = i); 47 | return mem; 48 | }, {}); 49 | 50 | 51 | /** 52 | * get the index in the urls array for the given note 53 | * @return {Number} 54 | */ 55 | export const getURLIndexForNote = (note)=> noteFlatMap[note]; 56 | 57 | const map = { 58 | 36: 0, 59 | 38: 1, 60 | 42: 2, 61 | 46: 3, 62 | 45: 4, 63 | 48: 5, 64 | 50: 6, 65 | 49: 7, 66 | 51: 8 67 | }; 68 | 69 | const revMap = { 70 | 0: 36, 71 | 1: 38, 72 | 2: 42, 73 | 3: 46, 74 | 4: 45, 75 | 5: 48, 76 | 6: 50, 77 | 7: 49, 78 | 8: 51 79 | }; 80 | 81 | export const getDrumRowFromNotePitch = (pitch)=> map[pitch]; 82 | export const getMidiNoteFromDrumRow = (row)=> revMap[row]; 83 | 84 | 85 | /** 86 | * calculate the sequencers matrix values (0|1) for each column and row 87 | * @param {Array<{startTime:Number, pitch:Number}>} notes 88 | * @param {Number} columns 89 | * @param {Number} rows 90 | * @param {Array>} [matrix] optionally provide a matrix to reuse 91 | * @returns {Array>} 92 | */ 93 | export const notesToMatrix = (notes, columns, rows, matrix)=>{ 94 | assert(Array.isArray(notes), `notes should be array, received ${notes}`); 95 | assert.equal(typeof columns, 'number', `invalid columns ${columns}`); 96 | assert.equal(typeof rows, 'number', `invalid rows ${columns}`); 97 | //2d array of all 0s matching our current matrix size 98 | matrix = matrix || times(columns, ()=>times(rows,()=>0)); 99 | 100 | notes.forEach((note, column) => { 101 | const notes = getLabelsFromEncoding(note, columns); 102 | notes.forEach((row) => { 103 | matrix[column][row] = 1; 104 | }); 105 | }); 106 | return matrix; 107 | }; 108 | 109 | export const iNoteSequenceToEncodedNotes = (sequences, columns)=>{ 110 | const encodedSequences = times(sequences.length, ()=>times(columns,()=>0)); 111 | sequences.forEach((sequence, sequenceIndex) => { 112 | sequence.notes.forEach(note => { 113 | encodedSequences[sequenceIndex][note.quantizedStartStep] += Math.pow(2, getDrumRowFromNotePitch(note.pitch)); 114 | }); 115 | }); 116 | return encodedSequences; 117 | }; 118 | 119 | export const encodedNotesToINoteSequence = (notes, columns)=>{ 120 | assert(Array.isArray(notes), `notes should be array, received ${notes}`); 121 | //2d array of all 0s matching our current matrix size 122 | let sequenceNotes = []; 123 | notes.forEach((note, column) => { 124 | const notes = getLabelsFromEncoding(note, columns); 125 | notes.forEach((row) => { 126 | sequenceNotes.push({pitch: getMidiNoteFromDrumRow(row), quantizedStartStep: column}); 127 | // matrix[column][row] = 1; 128 | }); 129 | }); 130 | return {notes: sequenceNotes}; 131 | }; 132 | 133 | /** 134 | * expands a given value to retrive the lables 135 | * @param {Number} value the encoded value to expand 136 | * @param {Number} numRows height of the initially encoded column 137 | * @returns {Array} decoded lables 138 | */ 139 | export const getLabelsFromEncoding = (value, numRows) => { 140 | let labels = []; 141 | for (var i = 0; i < numRows; i++) { 142 | if(((value >> i) & 1) == 1) { 143 | labels.push(i); 144 | } 145 | } 146 | return labels; 147 | }; 148 | 149 | -------------------------------------------------------------------------------- /js/utils.js: -------------------------------------------------------------------------------- 1 | // Copyright 2018 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // https://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | 16 | export const copyToClipboard = (target)=>{ 17 | var currentFocus = document.activeElement; 18 | target.select(); 19 | target.setSelectionRange(0, target.value.length); 20 | 21 | // copy the selection 22 | var succeed; 23 | try { 24 | succeed = document.execCommand('copy'); 25 | console.log(succeed); 26 | } catch(e) { 27 | succeed = false; 28 | console.log(e); 29 | } 30 | // restore original focus 31 | if (currentFocus && typeof currentFocus.focus === 'function') { 32 | currentFocus.focus(); 33 | } 34 | 35 | return succeed; 36 | }; 37 | 38 | /** 39 | * true if these objects have the same shallow value for the key provided 40 | * @param {Object} a 41 | * @param {Object} b 42 | * @param {string} key 43 | * @return {boolean} true if key is equal to each other 44 | */ 45 | export const keyEqual = (a, b, key)=> a[key] === b[key]; 46 | 47 | 48 | /** 49 | * true if these objects have a different shallow value for the key provided 50 | * @param {Object} a 51 | * @param {Object} b 52 | * @param {string} key 53 | * @return {boolean} true if key is not equal to each other 54 | */ 55 | export const keyNotEqual = (a, b, key)=> !keyEqual(a, b, key); 56 | 57 | 58 | export const allKeysEqual = (a, b, keyArray)=>{ 59 | for(let i=0; i 77 | !!(keyArray || Object.keys(a)).filter(k=> keyNotEqual(a, b, k)).length; 78 | 79 | 80 | export const clamp = (val, min, max)=> Math.max(Math.min(val, max), min); 81 | 82 | 83 | /** 84 | * linear interpolation 85 | * @param {Number} start 86 | * @param {Number} end 87 | * @param {Number} amt 88 | * @returns {Number} 89 | */ 90 | export const lerp = (start, end, amt) => 91 | (1 - amt) * start + amt * end; 92 | 93 | /** 94 | * Linearly interpolate a 2D vector 95 | * @param {{x:Number, y:Number}} start 96 | * @param {{x:Number, y:Number}} end 97 | * @param {Number} amt 98 | * @returns {{x:Number, y:Number}} 99 | */ 100 | export const lerpPoint = (start, end, amt) => 101 | ({ 102 | x: (1 - amt) * start.x + amt * end.x, 103 | y: (1 - amt) * start.y + amt * end.y 104 | }); 105 | 106 | 107 | /** 108 | * lerp 2 arrays 109 | * example: lerpArray([255, 128, 64], [0, 0, 0], 0.5) -> [128, 64, 32] 110 | * @param {Array} arrA 111 | * @param {Array} arrB 112 | * @param {Number} amt 113 | * @param {Array} [arrC] optionally provide the result array 114 | */ 115 | export const lerpArray = (arrA, arrB, amt, arrC=[])=>{ 116 | const len = Math.min(arrA.length, arrB.length); 117 | for(let i=0; i { 130 | const dx = a.x - b.x; 131 | const dy = a.y - b.y; 132 | return Math.sqrt(dx * dx + dy * dy); 133 | }; 134 | 135 | 136 | /** 137 | * Given `points` find the closest to `pt` 138 | * @param {Array<{x:Number, y:Number}>} points 139 | * @param {{x:Number, y:Number}} pt 140 | * @returns {x:Number, y:Number}} 141 | */ 142 | export const closestPoint = (points, pt) => { 143 | let closestPoint = null; 144 | let closestDistance = Number.MAX_VALUE; 145 | points.forEach((currPoint) => { 146 | const dist = distance(pt, currPoint); 147 | if (dist < closestDistance) { 148 | closestPoint = currPoint; 149 | closestDistance = dist; 150 | } 151 | }); 152 | return closestPoint; 153 | }; 154 | 155 | 156 | export const hash = (x) => { 157 | var nx = x * 1.380251; 158 | var n = Math.floor(nx); 159 | var f = nx - n; 160 | var h = 355.347391 * f + n * 5.3794610581 + 41.53823; 161 | h = h * h + f * h + f * f * 37.3921539 + 0.3861203; 162 | var nf = Math.floor(h); 163 | return h - nf; 164 | }; 165 | 166 | export const smoothNoise = (x) => { 167 | var n = Math.floor(x); 168 | var f = x - n; 169 | var n2 = n + 1; 170 | var h1 = hash(n); 171 | var h2 = hash(n2); 172 | var smooth = f * f * (3 - 2 * f); 173 | return h1 * (1 - smooth) + h2 * smooth; 174 | }; 175 | 176 | /** 177 | * generate fractal noise 178 | * @param {Number} x 179 | * @returns {number} 180 | */ 181 | export const fractalNoise = (x) => { 182 | var p = x + 11.3951031; 183 | var amp = 0.7; 184 | var scale = 10.0; 185 | var result = 0.0; 186 | for (var i = 0; i < 6; i++) { 187 | result += amp * smoothNoise(p * scale); 188 | amp *= 0.5; 189 | scale *= 2.0; 190 | } 191 | 192 | return result; 193 | }; 194 | 195 | 196 | /** 197 | * Used to generate an array of numbers between `start` and `stop` 198 | * i.e. range(1, 5) = [1,2,3,4] 199 | * @param {Number} start 200 | * @param {Number} stop 201 | * @returns {Array} 202 | */ 203 | export const range = function(start, stop){ 204 | //if only one argument is provided it was the stop 205 | if (arguments.length === 1) { 206 | stop = start; 207 | start = 0; 208 | } 209 | const arr = []; 210 | for (; start < stop; start++) { 211 | arr.push(start); 212 | } 213 | return arr; 214 | }; 215 | 216 | /** 217 | * invoke `fn`, `n` times and collect the result 218 | * @param {Number} n 219 | * @param {Function} fn 220 | * @returns {Array} 221 | */ 222 | export const times = (n, fn)=>{ 223 | const result = []; 224 | for(let i=0; i{ 232 | out.x = lerp(a.x, b.x, t); 233 | out.y = lerp(a.y, b.y, t); 234 | return out; 235 | }; 236 | /** 237 | * map a value from one range of numbers to another, 238 | * i.e. scalemap(0.5, 0, 2, 10, 20) = 15 239 | * @param value 240 | * @param start1 241 | * @param stop1 242 | * @param start2 243 | * @param stop2 244 | * @returns {*} 245 | */ 246 | export const scalemap = ( value, start1, stop1, start2, stop2 )=> 247 | start2 + (stop2 - start2) * ((value - start1) / (stop1 - start1)); 248 | 249 | export const pathLength = (path)=> 250 | path.reduce((sum, pt, i, arr)=> 251 | i===0 ? sum : sum + distance(arr[i-1], arr[i]), 0); 252 | 253 | 254 | /** 255 | * find the point at `t` along the `path` points 256 | * @param {Array<{x:Number, y:Number}>} path 257 | * @param {Number} t 258 | * @param {Object} [out] optionally provide an object to mutate 259 | * @returns {Object} 260 | */ 261 | export const lerpPath = (path, t, out={})=>{ 262 | 263 | t = clamp(t, 0, 1); 264 | 265 | if(t === 0 || path.length < 2){ 266 | return path[0]; 267 | } 268 | if(t === 1){ 269 | return path[path.length-1]; 270 | } 271 | 272 | const totalLength = pathLength(path); 273 | const wantedLength = totalLength * t; 274 | 275 | let lastLength = 0; 276 | let currLength = 0; 277 | let a, b; 278 | 279 | for(let i=1; i= wantedLength){ 285 | break; 286 | } 287 | lastLength = currLength; 288 | } 289 | 290 | const relativeLerp = scalemap(wantedLength, lastLength, currLength, 0, 1); 291 | 292 | return lerpBetweenPoints(a, b, relativeLerp, out); 293 | }; 294 | 295 | -------------------------------------------------------------------------------- /js/views/base-view.js: -------------------------------------------------------------------------------- 1 | // Copyright 2018 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // https://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | import { EventEmitter } from 'events'; 16 | import assert from 'assert'; 17 | 18 | 19 | export const getDOM = (dom)=> 20 | typeof dom === 'string' ? 21 | document.querySelector(dom) : 22 | dom; 23 | 24 | 25 | 26 | export class View extends EventEmitter { 27 | 28 | constructor(domElement){ 29 | super(); 30 | this.domElement = getDOM(domElement); 31 | assert(!!this.domElement, `Unable to resolve domElement from ${domElement}`); 32 | } 33 | 34 | set visible(val){ 35 | if(val !== this.visible){ 36 | this.domElement.classList[val ? 'remove' : 'add']('hidden'); 37 | } 38 | } 39 | 40 | get visible(){ 41 | return !this.domElement.classList.contains('hidden'); 42 | } 43 | 44 | 45 | _setEventMap(map){ 46 | this.events = map; 47 | //this.__removeEventListeners = eventMap(this.domElement, map); 48 | } 49 | 50 | shouldComponentUpdate(){ 51 | return true; 52 | } 53 | 54 | render(){ 55 | return this; 56 | } 57 | 58 | removeEventListeners(){ 59 | this.__removeEventListeners && this.__removeEventListeners(); 60 | } 61 | 62 | remove(){ 63 | this.removeEventListeners(); 64 | this.domElement.parentElement && this.domElement.parentElement.removeChild(this.domElement); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /js/views/button.js: -------------------------------------------------------------------------------- 1 | // Copyright 2018 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // https://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | import debounce from 'debounce'; 16 | 17 | import { View } from './base-view'; 18 | import throttle from 'throttleit'; 19 | import * as playModes from '../models/play-modes'; 20 | import { anyKeyNotEqual } from '../utils'; 21 | 22 | const editTemplate = ()=>` 23 | 24 | `; 25 | 26 | 27 | export class EditModeGroup extends View { 28 | 29 | constructor(domElement){ 30 | super(domElement); 31 | this._setEventMap({ 32 | 'click .save-edit': ()=> this.emit('save', this), 33 | 'click .undo-edit': ()=>this.emit('undo', this), 34 | 'click .clear-edit': ()=>this.emit('clear', this) 35 | }); 36 | } 37 | 38 | shouldComponentUpdate(state, last) { 39 | const sci = state.selectedCornerIndex; 40 | const lsci = last.selectedCornerIndex; 41 | return sci != lsci;// || (state.editHistory[sci] && (state.editHistory[sci][0] !== last.editHistory[sci][0])); 42 | } 43 | 44 | render(state) { 45 | 46 | if(state.selectedCornerIndex > -1) { 47 | this.domElement.classList.add('active'); 48 | } else { 49 | this.domElement.classList.remove('active'); 50 | } 51 | 52 | this.domElement.innerHTML = editTemplate(state); 53 | } 54 | } 55 | 56 | 57 | export class ToggleButton extends View { 58 | constructor(domElement, shouldBeActive){ 59 | super(domElement); 60 | this._shouldBeActive = shouldBeActive; 61 | this.__active = false; 62 | 63 | this._setEventMap({ 64 | 'click': ()=> this.emit('click', this) 65 | }); 66 | } 67 | 68 | set active(val){ 69 | const c = 'active'; 70 | if(val !== this.domElement.classList.contains(c)){ 71 | this.domElement.classList[val ? 'add' : 'remove'](c); 72 | } 73 | } 74 | 75 | get active(){ 76 | return this.domElement.classList.contains('active'); 77 | } 78 | 79 | render(state, last){ 80 | this.active = this._shouldBeActive(state, last); 81 | return this; 82 | } 83 | } 84 | 85 | 86 | 87 | 88 | 89 | export class CustomizeBeatsButton extends ToggleButton { 90 | 91 | constructor(domElement, shouldBeActive){ 92 | super(domElement, shouldBeActive); 93 | // this.domElement.innerHTML = bpmSliderTemplate(); 94 | this._setEventMap({ 95 | //dont let this be repeatedly clicked quickly 96 | 'click' : debounce((event, state) => { 97 | if(state.selectedCornerIndex >=0) { 98 | this.emit('done', this); 99 | } else { 100 | this.emit('edit', this); 101 | } 102 | 103 | }, 120, true) 104 | }); 105 | } 106 | 107 | shouldComponentUpdate(state, last) { 108 | return state.selectedCornerIndex !== last.selectedCornerIndex || 109 | state.gridAnimationLerp !== last.gridAnimationLerp || 110 | state.interpolating !== last.interpolating; 111 | } 112 | 113 | render(state) { 114 | 115 | const isBusy = state.interpolating || state.gridAnimationLerp > 0 && state.gridAnimationLerp < 1; 116 | //if the grid is animating, dont let the button be clicked 117 | this.domElement.classList[ isBusy ? 'add' : 'remove' ]('disabled'); 118 | 119 | // if(state.selectedCornerIndex === last.selectedCornerIndex){ 120 | // //already up to date 121 | // return; 122 | // } 123 | if(isBusy) { 124 | this.domElement.innerHTML = 'LOADING...'; 125 | } else if(state.selectedCornerIndex >=0) { 126 | this.domElement.innerHTML = 'DONE'; 127 | this.domElement.classList.add('active'); 128 | } else { 129 | this.domElement.innerHTML = 'EDIT CORNERS'; 130 | this.domElement.classList.remove('active'); 131 | } 132 | } 133 | } 134 | 135 | 136 | const dragDrawTemplate = (labelA, labelB)=>` 137 | ${labelA} 138 |
139 |
140 |
141 | ${labelB}`; 142 | 143 | 144 | 145 | export class DragDrawToggle extends View { 146 | constructor(domElement){ 147 | super(domElement); 148 | this.domElement.innerHTML = dragDrawTemplate('DRAG', 'DRAW'); 149 | this._setEventMap({ 150 | 'click': ()=> this.emit('click', this) 151 | }); 152 | } 153 | 154 | 155 | shouldComponentUpdate(state, last){ 156 | return anyKeyNotEqual(state, last, ['selectedCornerIndex', 'playMode']); 157 | } 158 | 159 | render({ playMode, selectedCornerIndex }){ 160 | //const { selectedCornerIndex } = state; 161 | this.domElement.classList[ playMode === playModes.PATH ? 'add' : 'remove' ]('right-selected'); 162 | this.domElement.classList[ selectedCornerIndex === -1 ? 'add' : 'remove']('active'); 163 | /*const isOn = selectedCornerIndex > -1; 164 | Object.assign(this.domElement.style, { 165 | pointerEvents: isOn ? 'all' : 'none', 166 | opacity: isOn ? 0.5 : 1, 167 | cursor: isOn ? 'pointer' : 'inherit' 168 | });*/ 169 | 170 | return this; 171 | } 172 | } 173 | 174 | 175 | const bpmSliderTemplate = ()=>` 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 |
185 | 186 | 120 187 |
`; 188 | 189 | 190 | export class BPMSlider extends ToggleButton { 191 | 192 | constructor(domElement, shouldBeActive){ 193 | super(domElement, shouldBeActive); 194 | this.domElement.innerHTML = bpmSliderTemplate(); 195 | this.pendulum = document.getElementById('pendulumWrapper'); 196 | this.__label = document.querySelector('.bpm-slider-label'); 197 | this._setEventMap({ 198 | 'click .bpm-slider-button': ()=> this.emit('click', this), 199 | //when the bpm slider moves 200 | 'input input[type="range"]': throttle((event)=> { 201 | let bpmValue = parseInt(event.target.value, 10); 202 | this.emit('change', this, bpmValue); 203 | }, 1000 / 15) 204 | }); 205 | } 206 | 207 | // shouldComponentUpdate(state, last) { 208 | // if(last.sequence === undefined) return false; 209 | // return state.sequence.activeColumn !== last.sequence.activeColumn; 210 | // } 211 | 212 | render(state, last){ 213 | if(state.bpm !== last.bpm) { 214 | this.__label.innerHTML = state.bpm; 215 | } 216 | 217 | //percent through the sequence * PI 218 | const perc = state.sequence.activeColumn / state.sequence.columns * Math.PI; 219 | // there are 4 beats per measure 220 | const beats = 4; 221 | //how many degrees should this fluctuate (on each side of the cneter) 222 | const amp = 45; 223 | 224 | //add the amp at the end so its based on the center 225 | const angle = (Math.sin(perc * beats) ) * amp + amp; 226 | 227 | this.pendulum.style.transform = `rotate(${angle}deg)`; 228 | return super.render(state, last); 229 | } 230 | } 231 | -------------------------------------------------------------------------------- /js/views/debug-layout-grid.js: -------------------------------------------------------------------------------- 1 | // Copyright 2018 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // https://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | import * as grid2d from 'grid2d'; 16 | import { View } from './base-view'; 17 | 18 | /** 19 | * render the `state.layoutGrid` on top of the page, 20 | * useful for debugging positioning at different resolutions 21 | * this module is mainly just for fun tbh :) 22 | */ 23 | 24 | 25 | export class DebugLayoutGrid extends View { 26 | 27 | constructor(color='rgba(255,128,128,1)'){ 28 | super(document.createElement('canvas')); 29 | this.ctx = this.domElement.getContext('2d'); 30 | this.color = color; 31 | } 32 | 33 | shouldComponentUpdate(state, last){ 34 | const g = state.layoutGrid; 35 | //this will be undefined the first time 36 | const pg = last.layoutGrid; 37 | return state.showDebugLayoutGrid !== last.showDebugLayoutGrid || 38 | g !== pg || 39 | g.column != pg.column || g.row !== pg.row || 40 | state.innerWidth !== last.innerWidth || 41 | state.innerHeight !== last.innerHeight; 42 | } 43 | 44 | render(state){ 45 | 46 | this.ctx.lineWidth = 1; 47 | if(!this.domElement.parentElement){ 48 | //inject it if its not in the active DOM 49 | document.body.appendChild(this.domElement); 50 | } 51 | 52 | if(!state.showDebugLayoutGrid){ 53 | //if its in the dom, take it out 54 | this.domElement.parentElement && this.domElement.parentElement.removeChild(this.domElement); 55 | return; 56 | } 57 | 58 | Object.assign(this.domElement.style, { 59 | pointerEvents: 'none', 60 | zIndex: 10, 61 | position: 'fixed', 62 | top: 0, 63 | left: 0, 64 | boxSizing: 'border-box' 65 | }); 66 | 67 | this.domElement.width = state.innerWidth; 68 | this.domElement.height = state.innerHeight; 69 | const grid = Object.assign({}, state.layoutGrid); 70 | grid.width = state.innerWidth; 71 | grid.height = state.innerHeight; 72 | const cells = grid2d.createCells(grid); 73 | this.ctx.clearRect(0, 0, this.domElement.width, this.domElement.height); 74 | 75 | this.ctx.strokeStyle = this.color; 76 | cells.forEach(cell=> 77 | this.ctx.strokeRect(cell.x, cell.y, cell.width, cell.height) 78 | ); 79 | 80 | this.ctx.lineWidth = 2; 81 | Object.keys(state.layoutItems).forEach(key=>{ 82 | 83 | const [a, b] = state.layoutItems[key]; 84 | 85 | const tl = { 86 | x: grid2d.xForColumn(grid, a.column), 87 | y: grid2d.yForRow(grid, a.row) 88 | }; 89 | 90 | const br = { 91 | x: grid2d.xForColumn(grid, b.column), 92 | y: grid2d.yForRow(grid, b.row) 93 | }; 94 | 95 | this.ctx.beginPath(); 96 | this.ctx.moveTo(tl.x, tl.y); 97 | this.ctx.lineTo(br.x, br.y); 98 | this.ctx.stroke(); 99 | 100 | }); 101 | 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /js/views/layout-containers.js: -------------------------------------------------------------------------------- 1 | // Copyright 2018 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // https://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | import * as grid2d from 'grid2d'; 16 | import { View } from './base-view'; 17 | import { MOBILE_PORTRAIT } from '../models/responsive'; 18 | 19 | /** 20 | * These layout containers are responsible for watching 21 | * the `state.layoutGrid` and adapting their dimensions to their item 22 | */ 23 | 24 | const li = (s, l, item, i)=> 25 | s.layoutItems[item][i].column !== !l.layoutItems[item][i].column || 26 | s.layoutItems[item][i].row !== l.layoutItems[item][i].row; 27 | 28 | const changed = (s, l, item)=> 29 | s.innerWidth !== l.innerWidth || 30 | s.innerHeight !== l.innerHeight || 31 | s.layoutItems && !l.layoutItems || 32 | li(s, l, item, 0) || 33 | li(s, l, item, 1); 34 | 35 | 36 | /** 37 | * GridContainer is the element containing the gradient grid (grid.js) 38 | * and 4 editable corners (tiles.js) 39 | */ 40 | export class GridContainer extends View { 41 | 42 | constructor(domElement){ 43 | super(domElement); 44 | } 45 | 46 | shouldComponentUpdate(state, last){ 47 | return changed(state, last, 'gridContainer'); 48 | } 49 | 50 | render(state){ 51 | 52 | if(state.layout === MOBILE_PORTRAIT){ 53 | Object.assign(this.domElement.style, { 54 | position: 'relative', 55 | top: 0, 56 | left: 0, 57 | width: '100%', 58 | height: 'auto' 59 | }); 60 | 61 | return; 62 | } 63 | 64 | const cw = grid2d.cellWidth(state.layoutGrid); 65 | const ch = grid2d.cellHeight(state.layoutGrid); 66 | const li = state.layoutItems.gridContainer; 67 | 68 | const width = `${cw * (li[1].column - li[0].column) * state.innerWidth}px`; 69 | 70 | Object.assign(this.domElement.style, { 71 | position: 'absolute', 72 | left: `${cw * li[0].column * state.innerWidth}px`, 73 | top: `${ch * li[0].row * state.innerHeight}px`, 74 | width, 75 | height: width 76 | }); 77 | } 78 | } 79 | 80 | 81 | 82 | /** 83 | * SequencerContainer is the container holding 84 | * the editable sequencer, the presets and the button 85 | */ 86 | export class SequencerContainer extends View { 87 | 88 | constructor(domElement){ 89 | super(domElement); 90 | } 91 | 92 | shouldComponentUpdate(state, last){ 93 | return changed(state, last, 'sequencerContainer'); 94 | } 95 | 96 | render(state){ 97 | 98 | if(state.layout === MOBILE_PORTRAIT){ 99 | Object.assign(this.domElement.style, { 100 | position: 'relative', 101 | top: 0, 102 | left: 0, 103 | width: '100%', 104 | height: `${state.innerHeight}px` 105 | }); 106 | 107 | return; 108 | } 109 | 110 | const cw = grid2d.cellWidth(state.layoutGrid); 111 | const ch = grid2d.cellHeight(state.layoutGrid); 112 | const li = state.layoutItems.sequencerContainer; 113 | 114 | Object.assign(this.domElement.style, { 115 | position: 'absolute', 116 | left: `${cw * li[0].column * state.innerWidth}px`, 117 | top: `${ch * li[0].row * state.innerHeight}px`, 118 | width: `${cw * (li[1].column - li[0].column) * state.innerWidth}px`, 119 | height: `${ch * (li[1].row - li[0].row) * state.innerHeight}px` 120 | }); 121 | 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /js/views/modal.js: -------------------------------------------------------------------------------- 1 | // Copyright 2018 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // https://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | import { View } from './base-view'; 16 | import * as modals from '../models/modals'; 17 | import { copyToClipboard } from '../utils'; 18 | 19 | 20 | const open = (url)=> 21 | window.open(url, 'fbShareWindow', 'height=450, width=550, top=' + (window.innerHeight / 2 - 275) + ', left=' + (window.innerWidth / 2 - 225) + ', toolbar=0, location=0, menubar=0, directories=0, scrollbars=0'); 22 | 23 | 24 | const facebookId = '224976884733659'; 25 | const description = 'Check out these beats I made using machine learning with Beat Blender'; 26 | const twitterTxt = (url)=>`${description} → ${url} #beatblender`; 27 | 28 | const twitter = ({shareURL }) => 29 | 'https://twitter.com/intent/tweet?text=' + window.encodeURIComponent(twitterTxt(shareURL)); 30 | 31 | const base = 'https://experiments.withgoogle.com/ai/beat-blender/view/'; 32 | 33 | const facebook = ({ shareURL }) => 34 | [`https://www.facebook.com/dialog/feed?app_id=${facebookId}`, 35 | '&display=popup', 36 | `&link=${shareURL}`, 37 | '&name=Beat Blender', 38 | `&caption=${base}`, 39 | `&description=${description}`, 40 | `&picture=${base + 'assets/share.png'}?type=png` 41 | ].join(''); 42 | 43 | // the html to repeat for every preset 44 | const shareTemplate = (state)=>` 45 | 46 |