├── .eslintrc.js ├── .gitattributes ├── .github └── workflows │ ├── gather-songs.yml │ └── test.yml ├── .gitignore ├── .vscode └── settings.json ├── LICENSE.md ├── README.md ├── build └── update-songs.js ├── dist ├── 1.x │ ├── ByteBeat.js │ └── ByteBeat.module.js └── 2.x │ ├── ByteBeat.js │ └── ByteBeat.module.js ├── editor ├── base64.js ├── compressor.js ├── elem.js ├── index.js ├── songList.js ├── songs.json ├── utils.js ├── visualizers │ ├── CanvasVisualizer.js │ ├── NullVisualizer.js │ ├── Visualizer.js │ ├── WebGLVisualizer.js │ └── effects │ │ ├── DataEffect.js │ │ ├── FFTEffect.js │ │ ├── SampleEffect.js │ │ ├── VSAEffect.js │ │ ├── WaveEffect.js │ │ └── effect-utils.js └── vsa.json ├── examples ├── esm.html ├── npm │ ├── .gitignore │ ├── README.md │ ├── index.html │ ├── package.json │ ├── rollup.config.js │ └── src │ │ └── index.js └── umd.html ├── html5bytebeat.html ├── html5bytebeat.png ├── icon.png ├── index.html ├── js ├── lzma.js ├── lzma_worker.js ├── scrollbars.css ├── scrollbars.js ├── twgl-full.module.js └── wavmaker.js ├── package-lock.json ├── package.json ├── rollup.config.js ├── src ├── ByteBeatCompiler.js ├── ByteBeatNode.js ├── ByteBeatProcessor.js └── WrappingStack.js └── test ├── comments-from-github-pass-to-update-songs.json ├── gen-links.js ├── serve.js ├── test-comment.md ├── test-songs.html └── test.html /.eslintrc.js: -------------------------------------------------------------------------------- 1 | /* global module */ 2 | module.exports = { 3 | 'env': { 4 | 'browser': true, 5 | 'es2022': true, 6 | }, 7 | 'parserOptions': { 8 | 'sourceType': 'module', 9 | 'ecmaVersion': 'latest', 10 | }, 11 | 'plugins': [ 12 | 'eslint-plugin-html', 13 | 'eslint-plugin-optional-comma-spacing', 14 | 'eslint-plugin-one-variable-per-var', 15 | 'eslint-plugin-require-trailing-comma', 16 | ], 17 | 'extends': 'eslint:recommended', 18 | 'rules': { 19 | 'no-alert': 2, 20 | 'no-array-constructor': 2, 21 | 'no-caller': 2, 22 | 'no-catch-shadow': 2, 23 | 'no-const-assign': 2, 24 | 'no-eval': 2, 25 | 'no-extend-native': 2, 26 | 'no-extra-bind': 2, 27 | 'no-implied-eval': 2, 28 | 'no-inner-declarations': 0, 29 | 'no-iterator': 2, 30 | 'no-label-var': 2, 31 | 'no-labels': 2, 32 | 'no-lone-blocks': 0, 33 | 'no-multi-str': 2, 34 | 'no-native-reassign': 2, 35 | 'no-new': 2, 36 | 'no-new-func': 2, 37 | 'no-new-object': 2, 38 | 'no-new-wrappers': 2, 39 | 'no-octal-escape': 2, 40 | 'no-process-exit': 2, 41 | 'no-proto': 2, 42 | 'no-return-assign': 2, 43 | 'no-script-url': 2, 44 | 'no-sequences': 2, 45 | 'no-shadow-restricted-names': 2, 46 | 'no-spaced-func': 2, 47 | 'no-trailing-spaces': 2, 48 | 'no-undef-init': 2, 49 | 'no-unused-expressions': 2, 50 | 'no-use-before-define': 0, 51 | 'no-var': 2, 52 | 'no-with': 2, 53 | 'prefer-const': 2, 54 | 'consistent-return': 2, 55 | 'curly': [2, 'all'], 56 | 'no-extra-parens': [2, 'functions'], 57 | 'eqeqeq': 2, 58 | 'new-cap': 2, 59 | 'new-parens': 2, 60 | 'semi-spacing': [2, {'before': false, 'after': true}], 61 | 'space-infix-ops': 2, 62 | 'space-unary-ops': [2, { 'words': true, 'nonwords': false }], 63 | 'yoda': [2, 'never'], 64 | 65 | 'brace-style': [2, '1tbs', { 'allowSingleLine': false }], 66 | 'camelcase': [0], 67 | 'comma-spacing': 0, 68 | 'comma-dangle': 0, 69 | 'comma-style': [2, 'last'], 70 | 'optional-comma-spacing/optional-comma-spacing': [2, {'after': true}], 71 | 'dot-notation': 0, 72 | 'eol-last': [0], 73 | 'global-strict': [0], 74 | 'key-spacing': [0], 75 | 'no-comma-dangle': [0], 76 | 'no-irregular-whitespace': 2, 77 | 'no-multi-spaces': [0], 78 | 'no-loop-func': 0, 79 | 'no-obj-calls': 2, 80 | 'no-redeclare': [0], 81 | 'no-shadow': [0], 82 | 'no-undef': [2], 83 | 'no-unreachable': 2, 84 | 'one-variable-per-var/one-variable-per-var': [2], 85 | 'quotes': [2, 'single'], 86 | 'require-atomic-updates': 0, 87 | 'require-trailing-comma/require-trailing-comma': [2], 88 | 'require-yield': 0, 89 | 'semi': [2, 'always'], 90 | 'strict': [2, 'global'], 91 | 'space-before-function-paren': [2, 'never'], 92 | 'keyword-spacing': [1, {'before': true, 'after': true, 'overrides': {}} ], 93 | }, 94 | }; 95 | 96 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text eol=lf 2 | 3 | *.md text 4 | *.css text 5 | *.js text 6 | *.html text 7 | 8 | *.jpeg binary 9 | *.dds binary 10 | *.wasm binary 11 | *.tga binary 12 | *.glb binary 13 | *.bin binary 14 | *.psd binary 15 | *.afdesign binary 16 | *.png binary 17 | *.jpg binary 18 | *.gif binary 19 | *.eot binary 20 | *.ttf binary 21 | *.woff binary 22 | *.zip binary 23 | -------------------------------------------------------------------------------- /.github/workflows/gather-songs.yml: -------------------------------------------------------------------------------- 1 | on: issue_comment 2 | 3 | jobs: 4 | issue_commented: 5 | name: gather-songs 6 | if: ${{ !github.event.issue.pull_request && github.event.issue.number == '17' }} 7 | runs-on: ubuntu-latest 8 | steps: 9 | - name: Checkout 🍔🍟🥤 10 | uses: actions/checkout@v3.0.2 11 | 12 | - name: Use Node.js 😂 13 | uses: actions/setup-node@v3 14 | with: 15 | node-version: 16 16 | 17 | - name: Get Songs 🎶 18 | run: | 19 | npm ci 20 | npm run update-songs 21 | env: 22 | NUMBER: ${{ github.event.issue.number }} 23 | GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} 24 | 25 | - name: Commit Songs 💮 26 | uses: stefanzweifel/git-auto-commit-action@v4.14.1 27 | with: 28 | commit_message: Update Songs 29 | branch: master 30 | file_pattern: editor/songs.json 31 | commit_user_name: update-songs-bot 32 | commit_user_email: update-song-bot@github.org 33 | commit_author: update-songs-bot 34 | 35 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | on: 3 | push: 4 | branches: 5 | - master 6 | pull_request: 7 | jobs: 8 | test: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - name: Checkout 🍔🍟🥤 12 | uses: actions/checkout@v3.0.2 13 | with: 14 | persist-credentials: false 15 | 16 | - name: Use Node.js 😂 17 | uses: actions/setup-node@v3 18 | with: 19 | node-version: 16 20 | 21 | - name: Test 🧪 22 | run: | 23 | npm ci 24 | npm run check-ci 25 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | .DS_Store 3 | node_modules 4 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "cSpell.words": [ 3 | "buttonstyle", 4 | "lzma", 5 | "nonwords", 6 | "savedialog" 7 | ] 8 | } -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2021 Gregg Tavares 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | HTML5 Bytebeat 2 | ============== 3 | 4 | Bytebeat is the name of type of music made from math. 5 | 6 | You provide a function who's only input is time *t* and from that write some code to generate a sound. 7 | 8 | In this particular case *t* is an 8000hz timer that counts up. For example 9 | 10 | sin(t) * 127 + 127 11 | 12 | You can choose traditional bytebeat where the output of your function is expected to be 0 to 255 13 | or you can choose floatbeat where the output is expected to be -1 to +1. 14 | 15 | Functions are just plain JavaScript though sin, cos, tan, floor, ceil and int will automatically be converted 16 | to Math.sin, Math.cos, Math.tan, Math.floor, Math.ceil, and Math.floor respectively. 17 | 18 | [Click here to try your hand at Bytebeat](http://greggman.com/downloads/examples/html5bytebeat/html5bytebeat.html). 19 | 20 | # Instructions 21 | 22 | ### Modes 23 | 24 | There 2 modes 25 | 26 | * bytebeat: Your expression is expected to generate byte values from 0 to 255 27 | * floatbeat: Your expression is expected to generate float values from -1.0 to 1.0 28 | 29 | ### Expression Types 30 | * Infix: Standard expressions eg. "`(t * 2) / 4`" 31 | * Postfix(rpn): Reverse Polish Notation eg "`t 2 * 4 /`" 32 | * glitch: glitch format or glitch urls. 33 | * function: Return a function. eg. "`return t => (t * 2) / 4`" 34 | 35 | **Infix** is standard JavaScript so all Math functions are available. 36 | Most math functions you can drop the "Math." part. In other words 37 | 38 | sin(t) 39 | 40 | is the same as 41 | 42 | Math.sin(t) 43 | 44 | **Postfix** requires that each element have at least one space between it. 45 | 46 | t2* // BAD! 47 | t 2 * // Good! 48 | 49 | If you're unfamiliar with postfix [see below](#postfix) 50 | 51 | **Glitch** is a format used by glitch machine for sharing. Examples 52 | 53 | * 42_forever!a13880fa400he!a5kma6kn40g!aCk28!a12k1ld!2fladm!43n 54 | * pipe_symphony!aEk5h5f!a11k2h!a9k3hdf!aDk4hg!ad4e!p5fm!a11k2h1rg!a5kdm 55 | 56 | These can be prefixed with "glitch://". For example 57 | 58 | * glitch://sadglitch!4.4.9.8.9.6.4.2!aoCk8hq!ad2d!aFk3h1fe!p5d3em!a63hm!a7kFFlp80slf 59 | 60 | There's 61 | a bunch more here. I have a feeling there's a bug or 2 left for full glitch support 62 | 63 | ### Function 64 | 65 | Expects a function body vs infix which expects an expression 66 | 67 | infix: `sin(t)` 68 | 69 | function: `return t => sin(t)` 70 | 71 | Note thought that "function" receives `t` in seconds, not samples. 72 | 73 | [See below](#Function) 74 | 75 | ### Postfix 76 | 77 | Postfix in this case I guess can be described as [forth](http://en.wikipedia.org/wiki/Forth_(programming_language)) 78 | like. It works with a stack. Each command either adds things to the stack or uses what's on the stack to do something. For example 79 | 80 | 123 // pushes 123 on the stack stack = 123 81 | 456 // pushes 456 on the stack stack = 123, 456 82 | + // pop the stop 2 things on the stack 83 | // adds them, puts the result on the 84 | // stack stack = 569 85 | 86 | Note the stack is only 256 elements deep. If you push 257 elements it wraps around. Similarly if you use `pick` 87 | with a large value your pick will wrap around. The stack is neither cleared nor reset on each iteration 88 | of your function. Some postfix based bytebeat songs take advantage of this where each iteration leaves 89 | things on the stack for the next iteration. 90 | 91 | #### operators 92 | 93 | The postfix operators are 94 | 95 | `>`, `<` ,`=` 96 | 97 | These take the top two things from the stack, do the comparision, then push 0xFFFFFFFF if the result 98 | is true or 0x0 if the result is false. Think of it has follows: If the TOP thing on the stack is `>`, `<`, or `=` to 99 | the next thing on the stack then 0xFFFFFFFF else 0x0 100 | 101 | `drop` 102 | 103 | removes the top thing from the stack 104 | 105 | `dup` 106 | 107 | duplicates the top thing on the stack. 108 | 109 | `swap` 110 | 111 | swaps the top 2 things on the stack 112 | 113 | `pick` 114 | 115 | pops the top thing from the stack and duplicates one item that many items back. In other words 116 | if the stack is `1,2,3,4,5,6,7,3` then `pick` pops the top thing `3` and duplicates 117 | the 3rd thing back counting from 0, which is no `4`. The stack is then `1,2,3,4,5,6,7,4`. 118 | 119 | Another way to look at it is `dup` is the same as `0 pick`. 120 | 121 | `put` 122 | 123 | sets the n'th element from the top of the stack to the current top. In other words if the stack is 124 | `1,2,3,4,5,6,7,3,100` then put will pull the top `100` and then set the `3` element back. The stack 125 | will then be `1,2,3,4,100,6,7,3`. 126 | 127 | `abs`, `sqrt`, `round`, `tan`, `log`, `exp`, `sin`, `cos`, `tan`, `floor`, `ceil`, `int` 128 | `min`, `max`, `pow` 129 | 130 | These operators all pop the top value from the stack, apply the operator, then push the result on 131 | the stack 132 | 133 | `/`, `+`, `-`, `*`, `%`, `>>`, `<<`, `|`, `&`, `^`, `&&`, `||`: 134 | 135 | These operators pop the top 2 values from the stack, apply the operator, then push the result. The 136 | order is as follows 137 | 138 | b = pop 139 | a = pop 140 | push(a op b) 141 | 142 | In other words `4 2 /` is 4 divided by 2. 143 | 144 | `~` 145 | 146 | Pops the top of the stack, applies the binary negate to it, pushes the result. 147 | 148 | ### Function 149 | 150 | See [Rant](#rant). 151 | 152 | "function" means you could write code that returns a function. The simplest example 153 | might be 154 | 155 | ```js 156 | return function(t) { 157 | sin(t); 158 | } 159 | ``` 160 | 161 | or shorter 162 | 163 | ```js 164 | return t => sin(t); 165 | ``` 166 | 167 | The point being you can write more generic JavaScript. For example 168 | 169 | ```js 170 | const notes = [261.62, 329.628, 391.995, 523.25, 391.995, 329.628, 261.62, 261.62, 1, 1]; 171 | 172 | function getNote(t) { 173 | const ndx = (t * 4 | 0) % notes.length; 174 | return note = notes[ndx]; 175 | } 176 | 177 | return function(t) { 178 | const note = getNote(t); 179 | return sin(t * 10 * note); 180 | } 181 | ``` 182 | 183 | [Example](https://greggman.com/downloads/examples/html5bytebeat/html5bytebeat.html#t=1&e=3&s=8000&bb=5d00000100080100000000000000319bca19c63617c05f852678809434c0245718cc973d0784216c18766f89f97417838f624d415ce254f1adfce8a1aa0aca94da7cd23a110b8cb7c5c13d29497b74893a02e812083f504edc25c42ead4ebd79647b868b71508dbcdd834b7a07446239af435da82980ba5f7108aab703421b6143d96834ac872176794e2ba01d8152a4bae0e9e1932f4a3a4a009dd00f27902217477670ef2c0ac8cd96ddf4fe13a4c0) 184 | 185 | But see [Rant](#rant) why this is seems kind of missing the point. 186 | 187 | ### Stereo 188 | 189 | You can emit an array with 2 values for left and right channels. Eg. 190 | 191 | ```js 192 | [sin(t), sin(t / 2)] 193 | ``` 194 | 195 | ### Extra 196 | 197 | Comments can be both // or /* */ style and I'd personally suggest 198 | you use comments for your name, the song's name, etc... 199 | 200 | There are several extra inputs available: 201 | 202 | The mouse position is available as `mouseX` and `mouseY` 203 | 204 | ```js 205 | sin(t * mouseX * 0.001) + cos(t * mouseY * 0.003) 206 | ``` 207 | 208 | The size of the window is available `width` and `height` 209 | 210 | Also note, using the comma operator you can write fairly arbitrary code. [See this example](https://greggman.com/downloads/examples/html5bytebeat/html5bytebeat.html#t=1&e=0&s=22000&bb=5d0000010058010000000000000017e07ce86fbd1ca9dedaaaf283d5ff76502fd7dadb76e5d882697d441ca3af61153f2f1380cbf89731ae302303c50ef1ebed677ad146c1f124dcf3cc109dd31ddd363d9d15d0d6a631f5f755297df9d98d614a051e4ed8cad8dae98b3b60d98a87f3ef147227e075cf005fc063cb9e4afe0ef1418c10607d6e7748e5c4477a20901c00ef5379b618214e7e2a2c8a538fec32de37b565c288aa49e52f2bcae7c1c9c474fcf1eb149f734180cccc153d360cb13e758ccf5d1eb9bebee221421a05b2a991f07c0b2ee2ed8ffa2ff5fc). 211 | 212 | Putting a comment in form of 213 | 214 | ``` 215 | // vsa: 216 | ``` 217 | 218 | Will apply a [vertexshaderart](https://vertexshaderart.com) piece. [Example](https://greggman.com/downloads/examples/html5bytebeat/html5bytebeat.html#t=0&e=0&s=8000&bb=5d00000100b9000000000000000017e07c86411ba2a517dacbc183b1477d3c17bd909f859f6ac588a82b934d189a40e82441616f52b3c7192116ea6be66102b438fc3d5e7ca2be7e768c34a949ce70e384f65243670976039bc9ed417d3b4d307c7506468aa5a052be90c656f55857c76626ba542034fe7d4cc6b435dee121ec730c2b0ebd730287a180702ee24e5cfb642a79cf1835917ef095b197cc235e5bee9e023e0f55f263acd5d95412fff91dde60) 219 | 220 | Rant 221 | ---- 222 | 223 | The original bytebeat, or at least the one I saw, was fairly simple. 8bits, stack based, 224 | few options. When I built a live JavaScript version I just thought 225 | "you get an expression that takes time and returns a value". **The end**. 226 | 227 | A few obvious additions, at least to me, were floatbeat, because the Web Audio API itself 228 | takes floats and IIRC some original C based thing that took a function expected floats. 229 | In fact I wrote that first. I just fed an expression that returns floats 230 | into the Web Audio API. I then manually converted a few beatbyte expressions by just 231 | putting `(original-bytebeat-expression) / 127 - 1`. 232 | 233 | The reason I didn't just stick with floatbeat is bytebeat expressions already 234 | existed and wanted people to be able to use them without having to understand 235 | how to convert, even though it's trivial. 236 | 237 | But now I see people have added *signed bytebeat*. What is the point? Any signed 238 | bytebeat can be turned in to regular bytebeat by just putting `+ 0x80` at the 239 | end of your expression. The entire point of bytebeat is to be self sufficient, 240 | to put what you need in the expression itself. 241 | 242 | I then found a `funcbeat` in which instead of an expression you pass it a 243 | function body. AFAICT the advantage is you can write code and declare 244 | other functions and data vs having to squeeze everything into an expression with 245 | commas. For example: 246 | 247 | ```js 248 | const notes = [261.62, 329.628, 391.995, 523.25, 391.995, 329.628, 261.62, 261.62, 1, 1]; 249 | 250 | function getNote(t) { 251 | const ndx = (t * 4 | 0) % notes.length; 252 | return note = notes[ndx]; 253 | } 254 | 255 | return function(t) { 256 | const note = getNote(t); 257 | return sin(t * 10 * note); 258 | } 259 | ``` 260 | 261 | But again, What is the point? If you're going to write real code with no limits 262 | then why do it this way all? [Just write code](https://jsgist.org/?src=5c0429e50839b546a38ce9dbb66c2ab3), 263 | no need to try to cram it into a bytebeat player? 264 | 265 | Then I found that some people had added a time divisor. For example, instead of 266 | `t` counting in samples it can count in seconds (fractional values). But again, 267 | what is the point? Why does this option need to exist when you can just divide 268 | `t` by the sample rate in the expression itself? 269 | 270 | It'd be like if someone added various options for time. `t = sample`, 271 | `t = sample / sampleRate`, `t = sin(sammple)`, `t = sin(sample / sampleRate)`, 272 | etc... The whole point is to **PUT THE MATH IN YOUR EXPRESSION!!!**. There is 273 | no need to add these options 😤 274 | 275 | </rant> 😛 276 | 277 | For more info 278 | ------------- 279 | Check out and be sure follow the many links. 280 | 281 | 282 | Special thanks to: 283 | ------------------ 284 | 285 | * Paul Pridham for his [Glitch Machine iOS Bytebeat program](http://madgarden.net/apps/glitch-machine/). 286 | * Mr.Doob for his [GLSL Sandbox](http://mrdoob.com/projects/glsl_sandbox/) where much of this code was cribbed. 287 | * Nathan Rugg for his [LZMA-JS library](https://github.com/nmrugg/LZMA-JS). 288 | * Darius Bacon for his [bytebeat program](https://github.com/darius/bytebeat) and for tips and examples to test it. 289 | * All the people making awesome bytebeats! 290 | 291 | # Library 292 | 293 | You can use this as a library. The library provides a `ByteBeatNode` which is a WebAudio [`AudioNode`](https://developer.mozilla.org/en-US/docs/Web/API/AudioNode). 294 | 295 | ## Example usage: 296 | 297 | ```js 298 | import ByteBeatNode from 'https://greggman.github.io/html5byteabeat/dist/1.x/ByteBeat.module.js'; 299 | 300 | async function start() { 301 | const context = new AudioContext(); 302 | context.resume(); // needed for safari 303 | await ByteBeatNode.setup(context); 304 | byteBeatNode = new ByteBeatNode(context); 305 | byteBeatNode.setType(ByteBeatNode.Type.byteBeat); 306 | byteBeatNode.setExpressionType(ByteBeatNode.ExpressionType.infix); 307 | byteBeatNode.setDesiredSampleRate(8000); 308 | await byteBeatNode.setExpressions(['((t >> 10) & 42) * t']); 309 | byteBeatNode.connect(context.destination); 310 | } 311 | ``` 312 | 313 | ## Live examples: 314 | 315 | * [Minimal ESM](https://jsgist.org/?src=668ef93e49417bf0bfafc088edf6b826) 316 | * [Minimal UMD](https://jsgist.org/?src=b63d9187f8ad3b7ff57831bd6ccd23b3) 317 | * [Simple visualizer](https://jsgist.org/?src=d1fb987c85cc1cff03f405ed210f04f6) 318 | * [Visualized based on AnalyserNode](https://jsgist.org/?src=36a61aa554a6da096540c3b7fcce7c78) 319 | * [npm](examples/npm/README.md) 320 | 321 | ## API 322 | 323 | There's just one class `ByteBeatNode`. You must call the async function `ByteBeatNode.setup` before using the library. 324 | 325 | * `reset()` 326 | 327 | re-starts the time to 0 328 | 329 | * `isRunning(): bool` 330 | 331 | true or false if running. The node is considered running if it's connected. 332 | 333 | * `async setExpressions(expressions: string[], resetToZero: bool)` 334 | 335 | Pass in array of 1 or 2 expressions. 336 | If 2 expressions it is assumed each expression is for a different channel. 337 | If a single expression returns an array of 338 | 2 values that is also also assumed to 339 | be 2 channels. Otherwise, it's 1 channel and will be output to both left and right channels. 340 | 341 | Note: this function is async. You can catch expression errors with `try`/`catch`. 342 | 343 | * `setDesiredSampleRate(rate: number)` 344 | 345 | Sets the sample rate for the expression 346 | (eg 8000, 11000, 22050, 44100, 48000) 347 | 348 | * `getDesiredSampleRate(): number` 349 | 350 | Returns the previously set sample rate 351 | 352 | * `setExpressionType(expressionType: number)` 353 | 354 | Sets the expression type. Valid expression types 355 | 356 | ``` 357 | ByteBeatNode.ExpressionType.infix // sin(t / 50) 358 | ByteBeatNode.ExpressionType.postfix // t 50 / sin 359 | ByteBeatNode.ExpressionType.glitch // see docs 360 | ByteBeatNode.ExpressionType.function // return function() { sin(t / 50); } 361 | ``` 362 | 363 | * `getExpressionType(): number` 364 | 365 | Gets the expression type 366 | 367 | * `setType(type: number)` 368 | 369 | Sets the output type. 370 | 371 | Valid types 372 | 373 | ``` 374 | ByteBeatNode.Type.byteBeat // 0 <-> 255 375 | ByteBeatNode.Type.floatBeat // -1.0 <-> +1.0 376 | ByteBeatNode.Type.signedByteBeat // -128 <-> 127 377 | ``` 378 | * `getType(): number` 379 | 380 | Gets the type 381 | 382 | * `getNumChannels(): number` 383 | 384 | Returns the number of channels output 385 | by the current expression. 386 | 387 | * `async getSamplesForTimeRange(start: number, end: number: numSamples: number, context, stack, channel: number)` 388 | 389 | Gets a -1 to +1 from the current expression for the given time (time is the `t` value in your expression) 390 | 391 | This function is useful for visualizers. 392 | 393 | To make a stack call `byteBeat.createStack()`. To create a context call 394 | `byteBeat.createContext`. 395 | A stack is used for postfix expressions. 396 | [See docs on postfix](#postfix). The context 397 | is used for keeping expressions state for 398 | expressions that try hacks to keep state around like if they build a large note table and assign it to `window`. It won't 399 | actually be assigned to `window`, it will 400 | be assigned to the context (in theory) 401 | 402 | ## Development / running locally 403 | 404 | ``` 405 | git clone https://github.com/greggman/html5bytebeat.git 406 | cd html5bytebeat 407 | npm i 408 | npm start 409 | ``` 410 | 411 | The instructions above assume you have node.js installed. If not, if you're 412 | on windows use [nvm-windows](https://github.com/coreybutler/nvm-windows), 413 | or if you're on mac/linux use [nvm](https://github.com/nvm-sh/nvm). 414 | 415 | Or you can just use the installers at [nodejs.org](https://nodejs.org) though 416 | I'd recommend nvm and nvm-windows personally as once you get into node dev 417 | you'll likely need different versions for different projects. 418 | 419 | # License 420 | 421 | [MIT](LICENSE.md) 422 | -------------------------------------------------------------------------------- /build/update-songs.js: -------------------------------------------------------------------------------- 1 | /* global require, process, __dirname */ 2 | const { Octokit } = require('octokit'); 3 | const fs = require('fs'); 4 | const path = require('path'); 5 | const lzma = require('lzma'); 6 | 7 | async function getComments() { 8 | const filename = process.argv[2]; 9 | if (filename) { 10 | const content = fs.readFileSync(process.argv[2], {encoding: 'utf-8'}); 11 | if (filename.endsWith('.json')) { 12 | return JSON.parse(content); 13 | } 14 | return [ 15 | { 16 | body: content, 17 | reactions: {}, 18 | user: { 19 | id: 123, 20 | login: 'gman', 21 | }, 22 | }, 23 | ]; 24 | } 25 | 26 | const token = process.env.GH_TOKEN; 27 | if (!token) { 28 | console.error('must set GH_TOKEN'); 29 | // eslint-disable-next-line no-process-exit 30 | process.exit(1); 31 | } 32 | 33 | 34 | const octokit = new Octokit({ 35 | auth: token, 36 | }); 37 | 38 | const comments = await octokit.paginate('GET /repos/{owner}/{repo}/issues/{issue_number}/comments', { 39 | owner: 'greggman', 40 | repo: 'html5bytebeat', 41 | issue_number: '17', 42 | headers: { 43 | accept: 'application/vnd.github.v3.raw+json', 44 | }, 45 | }); 46 | return comments; 47 | } 48 | 49 | function convertHexToBytes(text) { 50 | const array = []; 51 | for (let i = 0; i < text.length; i += 2) { 52 | const tmpHex = text.substring(i, i + 2); 53 | array.push(parseInt(tmpHex, 16)); 54 | } 55 | return array; 56 | } 57 | 58 | function readURL(hash) { 59 | const args = hash.split('&'); 60 | const data = {}; 61 | for (let i = 0; i < args.length; ++i) { 62 | const parts = args[i].split('='); 63 | data[parts[0]] = parts[1]; 64 | } 65 | const t = data.t !== undefined ? parseFloat(data.t) : 1; 66 | const e = data.e !== undefined ? parseFloat(data.e) : 0; 67 | const s = data.s !== undefined ? parseFloat(data.s) : 8000; 68 | const bytes = convertHexToBytes(data.bb); 69 | const code = lzma.decompress(bytes); 70 | return {t, e, s, code}; 71 | } 72 | 73 | function removeCommentsAndLineBreaks(x) { 74 | // remove comments (hacky) 75 | x = x.replace(/\/\/.*/g, ' '); 76 | x = x.replace(/\n/g, ' '); 77 | x = x.replace(/\/\*.*?\*\//g, ' '); 78 | return x; 79 | } 80 | 81 | function minimize(code) { 82 | return removeCommentsAndLineBreaks(code).trim().replace(/\s\s+/g, ' '); 83 | } 84 | 85 | async function main() { 86 | const comments = await getComments(); 87 | 88 | const linkRE = /\[(.*?)\]\((https:\/\/greggman\.com\/downloads\/examples\/html5bytebeat\/html5bytebeat\.html#.*?)\)/g; 89 | const songs = []; 90 | for (const {body, reactions, user} of comments) { 91 | const results = [...body.matchAll(linkRE)]; 92 | const {login, id} = user; 93 | songs.push(...results.map(([, title, link]) => { 94 | const {code} = readURL(link.substring(link.indexOf('#') + 1)); 95 | const size = minimize(code).length; 96 | // fix title -- this seems hacked to me 97 | title = title.replaceAll(/\\(.)/g, '$1'); 98 | return {title, size, link, reactions, groupSize: results.length, user: {login, id}}; 99 | })); 100 | } 101 | 102 | const filename = path.join(__dirname, '..', 'editor', 'songs.json'); 103 | console.log(`writing ${songs.length} song to ${filename}`); 104 | const extraArgs = process.argv[2] ? [null, 2] : []; 105 | fs.writeFileSync(filename, JSON.stringify(songs, ...extraArgs)); 106 | } 107 | 108 | main(); 109 | -------------------------------------------------------------------------------- /editor/base64.js: -------------------------------------------------------------------------------- 1 | export const decode = s => Uint8Array.from(atob(s), c => c.charCodeAt(0)); 2 | export const encode = b => btoa(String.fromCharCode(...new Uint8Array(b))); 3 | export const decodeToString = s => new TextDecoder().decode(decode(s)); 4 | export const encodeString = s => encode(new TextEncoder().encode(s)); 5 | -------------------------------------------------------------------------------- /editor/compressor.js: -------------------------------------------------------------------------------- 1 | /* global LZMA */ 2 | 3 | const compressor = new LZMA( 'js/lzma_worker.js' ); 4 | export default compressor; -------------------------------------------------------------------------------- /editor/elem.js: -------------------------------------------------------------------------------- 1 | export function setElemProps(elem, attrs, children) { 2 | for (const [key, value] of Object.entries(attrs)) { 3 | if (typeof value === 'function' && key.startsWith('on')) { 4 | const eventName = key.substring(2).toLowerCase(); 5 | elem.addEventListener(eventName, value, {passive: false}); 6 | } else if (typeof value === 'object') { 7 | for (const [k, v] of Object.entries(value)) { 8 | elem[key][k] = v; 9 | } 10 | } else if (elem[key] === undefined) { 11 | elem.setAttribute(key, value); 12 | } else { 13 | elem[key] = value; 14 | } 15 | } 16 | for (const child of children) { 17 | elem.appendChild(child); 18 | } 19 | return elem; 20 | } 21 | 22 | export function createElem(tag, attrs = {}, children = []) { 23 | const elem = document.createElement(tag); 24 | setElemProps(elem, attrs, children); 25 | return elem; 26 | } 27 | 28 | export function addElem(tag, parent, attrs = {}, children = []) { 29 | const elem = createElem(tag, attrs, children); 30 | parent.appendChild(elem); 31 | return elem; 32 | } -------------------------------------------------------------------------------- /editor/index.js: -------------------------------------------------------------------------------- 1 | /* global WavMaker */ 2 | import '../js/scrollbars.js'; 3 | import * as twgl from '../js/twgl-full.module.js'; 4 | import compressor from './compressor.js'; 5 | import { createElem as el } from './elem.js'; 6 | 7 | import ByteBeatNode from '../src/ByteBeatNode.js'; 8 | import WebGLVisualizer from './visualizers/WebGLVisualizer.js'; 9 | import CanvasVisualizer from './visualizers/CanvasVisualizer.js'; 10 | import NullVisualizer from './visualizers/NullVisualizer.js'; 11 | 12 | import DataEffect from './visualizers/effects/DataEffect.js'; 13 | import FFTEffect from './visualizers/effects/FFTEffect.js'; 14 | // import SampleEffect from './visualizers/effects/SampleEffect.js'; 15 | import VSAEffect from './visualizers/effects/VSAEffect.js'; 16 | import WaveEffect from './visualizers/effects/WaveEffect.js'; 17 | 18 | import songList from './songList.js'; 19 | 20 | import { 21 | convertBytesToHex, 22 | convertHexToBytes, 23 | makeExposedPromise, 24 | splitBySections, 25 | s_beatTypes, 26 | s_expressionTypes, 27 | } from './utils.js'; 28 | 29 | function $(id) { 30 | return document.getElementById(id); 31 | } 32 | 33 | function strip(s) { 34 | return s.replace(/^\s+/, '').replace(/\s+$/, ''); 35 | } 36 | 37 | let g_context; 38 | let g_gainNode; 39 | let g_byteBeat; 40 | let g_filter; 41 | let g_localSettings; 42 | const g_analyzers = []; 43 | let g_splitter; 44 | let g_merger; 45 | let g_visualizers; 46 | let g_visualizer; 47 | let g_captureFn; 48 | let g_screenshot; 49 | let g_saving = false; 50 | let g_saveDialogInitialized = false; 51 | let g_screenshotCanvas; 52 | let g_screenshotContext; 53 | let g_debugElem; 54 | let g_ignoreHashChange; 55 | let g_vsaVisualizer; 56 | let g_vsaEffect; 57 | let g_vsaIndex; 58 | let playing = false; 59 | let codeElem; 60 | let helpElem; 61 | let timeElem; 62 | let playElem; 63 | let beatTypeElem; 64 | let expressionTypeElem; 65 | let sampleRateElem; 66 | let visualTypeElem; 67 | let saveElem; 68 | let compileStatusElem; 69 | let canvas; 70 | let controls; 71 | let doNotSetURL = true; 72 | const g_slow = false; 73 | 74 | 75 | 76 | /* 77 | ByteBeatNode--->Splitter--->analyser---->merger---->context 78 | \ / 79 | \----->analyser--/ 80 | */ 81 | function connectFor2Channels() { 82 | g_byteBeat.disconnect(); 83 | g_byteBeat.connect(g_splitter); 84 | g_splitter.connect(g_analyzers[0], 0); 85 | g_splitter.connect(g_analyzers[1], 1); 86 | g_analyzers[0].connect(g_merger, 0, 0); 87 | g_analyzers[1].connect(g_merger, 0, 1); 88 | return g_merger; 89 | } 90 | 91 | function reconnect() { 92 | const lastNode = connectFor2Channels(); 93 | if (g_filter) { 94 | lastNode.connect(g_filter); 95 | g_filter.connect(g_gainNode); 96 | g_gainNode.connect(g_context.destination); 97 | } else { 98 | lastNode.connect(g_gainNode); 99 | g_gainNode.connect(g_context.destination); 100 | } 101 | g_context.resume(); 102 | } 103 | 104 | function play() { 105 | if (!playing) { 106 | playing = true; 107 | reconnect(); 108 | } 109 | } 110 | 111 | function pause() { 112 | if (playing) { 113 | playing = false; 114 | g_byteBeat.disconnect(); 115 | } 116 | } 117 | 118 | function setSelected(element, selected) { 119 | if (element) { 120 | element.selected = selected; 121 | } 122 | } 123 | function setSelectOption(select, selectedIndex) { 124 | setSelected(select.options[select.selectedIndex], false); 125 | setSelected(select.options[selectedIndex], true); 126 | } 127 | 128 | 129 | const setVisualizer = ndx => { 130 | const {visualizer, fn} = g_visualizers[Math.min(ndx, g_visualizers.length - 1)]; 131 | g_visualizer = visualizer; 132 | if (fn) { 133 | fn(); 134 | } 135 | setSelectOption(visualTypeElem, ndx); 136 | }; 137 | 138 | try { 139 | g_localSettings = JSON.parse(localStorage.getItem('localSettings')); 140 | } catch { 141 | } 142 | 143 | { 144 | if (!g_localSettings || typeof g_localSettings !== 'object') { 145 | g_localSettings = {}; 146 | } 147 | const defaultSettings = { 148 | volume: 100, 149 | }; 150 | for (const [key, value] of Object.entries(defaultSettings)) { 151 | if (typeof g_localSettings[key] != typeof value) { 152 | g_localSettings[key] = value; 153 | } 154 | } 155 | } 156 | 157 | function saveSettings() { 158 | localStorage.setItem('localSettings', JSON.stringify(g_localSettings)); 159 | } 160 | 161 | async function main() { 162 | canvas = $('visualization'); 163 | controls = $('controls'); 164 | 165 | g_context = new AudioContext(); 166 | g_context.resume(); // needed for safari 167 | g_gainNode = new GainNode(g_context, { gain: g_localSettings.volume / 100 }); 168 | await ByteBeatNode.setup(g_context); 169 | g_byteBeat = new ByteBeatNode(g_context); 170 | 171 | g_analyzers.push(g_context.createAnalyser(), g_context.createAnalyser()); 172 | g_analyzers.forEach(a => { 173 | a.maxDecibels = -1; 174 | }); 175 | 176 | g_splitter = g_context.createChannelSplitter(2); 177 | g_merger = g_context.createChannelMerger(2); 178 | 179 | // g_filter = g_context.createBiquadFilter(); 180 | // g_filter.type = 'lowpass'; 181 | // g_filter.frequency.value = 4000; 182 | 183 | g_screenshotCanvas = el('canvas', { 184 | width: 400, 185 | height: 100, 186 | }); 187 | g_screenshotContext = g_screenshotCanvas.getContext('2d'); 188 | 189 | function resetToZero() { 190 | g_byteBeat.reset(); 191 | g_visualizer.reset(); 192 | g_visualizer.render(g_byteBeat, g_analyzers); 193 | updateTimeDisplay(); 194 | } 195 | 196 | helpElem = el('a', { 197 | href: 'https://github.com/greggman/html5bytebeat', 198 | textContent: '?', 199 | className: 'buttonstyle', 200 | }); 201 | controls.appendChild(helpElem); 202 | 203 | timeElem = el('button', {onClick: resetToZero}); 204 | controls.appendChild(timeElem); 205 | 206 | function playPause() { 207 | if (!playing) { 208 | playElem.classList.remove('play'); 209 | playElem.classList.add('pause'); 210 | play(); 211 | } else { 212 | playElem.classList.remove('pause'); 213 | playElem.classList.add('play'); 214 | pause(); 215 | updateTimeDisplay(); 216 | } 217 | } 218 | playElem = el('button', { className: 'play', onClick: playPause }); 219 | controls.appendChild(playElem); 220 | 221 | function addOption(textContent, selected) { 222 | return el('option', { 223 | textContent, 224 | ...(selected && {selected}), 225 | }); 226 | } 227 | 228 | function addSelection(options, selectedIndex, props = {}) { 229 | const select = el('select', props, options.map((option, i) => { 230 | return addOption(option, i === selectedIndex); 231 | })); 232 | return select; 233 | } 234 | 235 | function addVerticalRange(options, props) { 236 | const fn = props.onChange; 237 | const valueElem = el('div', { textContent: options.value ?? 0 }); 238 | return el('div', {className: 'vertical-range', tabIndex: 0}, [ 239 | valueElem, 240 | el('div', {className: 'vertical-range-holder'}, [ 241 | el('input', { ...options, type: 'range', onInput: (e) => { 242 | valueElem.textContent = e.target.value; 243 | if (fn) { 244 | fn(e); 245 | } 246 | },}), 247 | ]), 248 | ]) 249 | } 250 | 251 | beatTypeElem = addSelection(s_beatTypes, 0, { 252 | onChange(event) { 253 | g_byteBeat.setType(event.target.selectedIndex); 254 | setURL(); 255 | }, 256 | }); 257 | controls.appendChild(beatTypeElem); 258 | 259 | expressionTypeElem = addSelection(s_expressionTypes, 0, { 260 | onChange(event) { 261 | g_byteBeat.setExpressionType(event.target.selectedIndex); 262 | setExpressions(g_byteBeat.getExpressions()); 263 | }, 264 | }); 265 | controls.appendChild(expressionTypeElem); 266 | 267 | const sampleRates = [8000, 11000, 22000, 32000, 44100, 48000]; 268 | sampleRateElem = addSelection(['8kHz', '11kHz', '22kHz', '32kHz', '44kHz', '48kHz'], 0, { 269 | onChange(event) { 270 | g_byteBeat.setDesiredSampleRate(sampleRates[event.target.selectedIndex]); 271 | setURL(); 272 | }, 273 | }); 274 | controls.appendChild(sampleRateElem); 275 | 276 | const volumeElem = addVerticalRange({min: 0, max: 100, step: 1, value: g_localSettings.volume }, { 277 | onChange(event) { 278 | g_gainNode.gain.value = Math.pow(event.target.value / 100, 2); 279 | g_localSettings.volume = parseInt(event.target.value); 280 | saveSettings(); 281 | }, 282 | }); 283 | controls.appendChild(volumeElem); 284 | 285 | if (g_slow) { 286 | g_visualizers = [ 287 | {name: 'none', visualizer: new NullVisualizer() }, 288 | ]; 289 | } else { 290 | const gl = canvas.getContext('webgl', { 291 | alpha: false, 292 | antialias: false, 293 | preserveDrawingBuffer: true, 294 | }); 295 | if (gl) { 296 | twgl.addExtensionsToContext(gl); 297 | } 298 | if (gl) { 299 | g_vsaEffect = new VSAEffect(gl); 300 | g_vsaEffect.setURL('editor/vsa.json'); 301 | 302 | g_vsaVisualizer = new WebGLVisualizer(gl, [g_vsaEffect]); 303 | 304 | const effects = [ 305 | new DataEffect(gl), 306 | // ...(showSample ? [new SampleEffect(gl)] : []), 307 | new WaveEffect(gl), 308 | new FFTEffect(gl), 309 | ]; 310 | g_visualizers = [ 311 | { name: 'none', visualizer: new NullVisualizer(), }, 312 | { name: 'wave', visualizer: new WebGLVisualizer(gl, effects), }, 313 | ]; 314 | g_vsaIndex = g_visualizers.length; 315 | g_visualizers.push({ name: 'vsa', visualizer: g_vsaVisualizer,}); 316 | const vsaUrls = [ 317 | { url: 'https://www.vertexshaderart.com/art/R2FYLbHWTcCWh5PiE', name: 'blorp', }, 318 | { url: 'https://www.vertexshaderart.com/art/Xr5DemAP52ZcKLRbQ', name: 'seaqyuk', }, 319 | { url: 'https://www.vertexshaderart.com/art/hffRc9FH8TMNKECkJ', name: 'bhatsu', }, 320 | { url: 'https://www.vertexshaderart.com/art/a75Aou3fJGMJjXG5r', name: 'discinos', }, 321 | { url: 'https://www.vertexshaderart.com/art/QCxSnbduPERK5rQni', name: '?dot-line', }, 322 | { url: 'https://www.vertexshaderart.com/art/RnwjSt42YXLcGjsgT', name: 'morp', }, 323 | { url: 'https://www.vertexshaderart.com/art/7YgXgotM2u7EazE58', name: 'add-em-up', }, 324 | { url: 'https://www.vertexshaderart.com/art/TYoTaksHA6DWsP4aD', name: 'grid', }, 325 | { url: 'https://www.vertexshaderart.com/art/ctdaXFjXNjTiss8Kh', name: 'circles', }, 326 | { url: 'https://www.vertexshaderart.com/art/auo92EWvwwyBRak2c', name: 'widr', }, 327 | { url: 'https://www.vertexshaderart.com/art/xvg4vyvfWjCvKZQfW', name: 'fuzeball', }, 328 | { url: 'https://www.vertexshaderart.com/art/wFtvqKAQ3wB8Hho3p', name: 'undul', }, 329 | { url: 'https://www.vertexshaderart.com/art/PFHJfQrt3knT8K8sQ', name: 'flwr', }, 330 | { url: 'https://www.vertexshaderart.com/art/fmmQsNyrdyjA3226x', name: 'radonut', }, 331 | { url: 'https://www.vertexshaderart.com/art/yKbsMohpXxZXWLHSm', name: 'vu-w/max', }, 332 | { url: 'https://www.vertexshaderart.com/art/GxbSZ33B9swmxAmdT', name: 'notmizu', }, 333 | { url: 'https://www.vertexshaderart.com/art/mNBny7JXpBGwQnMwG', name: 'pulsedn', }, 334 | { url: 'https://www.vertexshaderart.com/art/YRrZ7fHmFhtoKpyrq', name: 'bebubebup', }, 335 | { url: 'https://www.vertexshaderart.com/art/qZCxqkkWDsfd8gqGS', name: 'dncrs', }, 336 | { url: 'https://www.vertexshaderart.com/art/yX9SGHv6RPPqcsXvh', name: 'discus', }, 337 | { url: 'https://www.vertexshaderart.com/art/Q4dpCbhvWMYfDz5Nb', name: 'smutz', }, 338 | { url: 'https://www.vertexshaderart.com/art/79HqSrQH4meL63aAo', name: 'ball-o?3', }, 339 | { url: 'https://www.vertexshaderart.com/art/SHEuL7KCpNnj28Rmn', name: 'incId', }, 340 | { url: 'https://www.vertexshaderart.com/art/sHdHwHQ9GTSaJ9j99', name: 'headrush', }, 341 | { url: 'https://www.vertexshaderart.com/art/zd2E5vCZduc5JeoFz', name: 'cubespace', }, 342 | { url: 'https://www.vertexshaderart.com/art/PHWvovEcpp6R6yT8K', name: 's.o.i.', }, 343 | { url: 'https://www.vertexshaderart.com/art/s7zehgnGsLh5aHkM8', name: 'volum', }, 344 | { url: 'https://www.vertexshaderart.com/art/NR42qFZjAfmdmw6oR', name: 'iblot', }, 345 | { url: 'https://www.vertexshaderart.com/art/YQhEmHqKTgrDSD3AM', name: 'circlepower', }, 346 | { url: 'https://www.vertexshaderart.com/art/gX32iAvezAbinbMJz', name: 'c-pump', }, 347 | { url: 'https://www.vertexshaderart.com/art/p9pecgaEBJ3kz5r7g', name: 'red ring', }, 348 | { url: 'https://www.vertexshaderart.com/art/g2PZWgGp6YYe9CWwE', name: 'cybr', }, 349 | { url: 'https://www.vertexshaderart.com/art/ysh84kFrt5dxksGM9', name: 'ball', }, 350 | { url: 'https://www.vertexshaderart.com/art/MefAhfbtS5ZbYifPi', name: 'qyube', }, 351 | { url: 'https://www.vertexshaderart.com/art/uuHumiKPEiAKNPkEA', name: 'hexalicious', }, 352 | ]; 353 | for (const {url, name} of vsaUrls) { 354 | g_visualizers.push({name, visualizer: g_vsaVisualizer, fn: () => { 355 | g_vsaEffect.setURL(url); 356 | }, 357 | }); 358 | } 359 | } else { 360 | g_visualizers = [ 361 | { name: 'none', visualizer: new NullVisualizer(), }, 362 | { name: 'simple', visualizer: new CanvasVisualizer(canvas), }, 363 | ]; 364 | } 365 | } 366 | 367 | { 368 | const names = g_visualizers.map(({name}) => name); 369 | const ndx = Math.min(names.length - 1, 1); 370 | visualTypeElem = addSelection(names, ndx, { 371 | onChange(event) { 372 | setVisualizer(event.target.selectedIndex); 373 | setURL(); 374 | }, 375 | }); 376 | controls.appendChild(visualTypeElem); 377 | setVisualizer(ndx); 378 | } 379 | 380 | function getChildElemIndexByContent(parent, content) { 381 | for (let i = 0; i < parent.children.length; ++i) { 382 | if (parent.children[i].textContent === content) { 383 | return i; 384 | } 385 | } 386 | return -1; 387 | } 388 | 389 | function setVisualizerByName(name) { 390 | const ndx = getChildElemIndexByContent(visualTypeElem, name); 391 | if (ndx >= 0) { 392 | setVisualizer(ndx); 393 | } 394 | } 395 | 396 | saveElem = el('button', { 397 | textContent: 'save', 398 | onClick: startSave, 399 | }); 400 | controls.appendChild(saveElem); 401 | 402 | compileStatusElem = el('button', { 403 | className: 'status', 404 | textContent: '---', 405 | }); 406 | controls.appendChild(compileStatusElem); 407 | 408 | codeElem = $('code'); 409 | codeElem.addEventListener('input', () => { 410 | compile(codeElem.value); 411 | }); 412 | 413 | codeElem.addEventListener('keydown', function(event) { 414 | if (event.code === 'Tab') { 415 | // Fake TAB 416 | event.preventDefault(); 417 | 418 | const start = codeElem.selectionStart; 419 | const end = codeElem.selectionEnd; 420 | 421 | codeElem.value = codeElem.value.substring(0, start) + '\t' + codeElem.value.substring(end, codeElem.value.length); 422 | 423 | codeElem.selectionStart = codeElem.selectionEnd = start + 1; 424 | codeElem.focus(); 425 | } 426 | }, false); 427 | 428 | window.addEventListener('keydown', function(event){ 429 | if ((event.ctrlKey || event.metaKey) && event.code === 'KeyS') { 430 | event.preventDefault(); 431 | startSave(); 432 | } 433 | }); 434 | 435 | window.addEventListener('hashchange', function() { 436 | if (g_ignoreHashChange) { 437 | g_ignoreHashChange = false; 438 | return; 439 | } 440 | const hash = window.location.hash.substr(1); 441 | readURL(hash); 442 | }); 443 | 444 | if (window.location.hash) { 445 | const hash = window.location.hash.substr(1); 446 | readURL(hash); 447 | } else { 448 | readURL('t=0&e=0&s=8000&bb=5d000001001400000000000000001461cc403ebd1b3df4f78ee66fe76abfec87b7777fd27ffff85bd000'); 449 | } 450 | 451 | { 452 | const observer = new ResizeObserver(onWindowResize); 453 | observer.observe(canvas); 454 | } 455 | playPause(); 456 | 457 | function render() { 458 | // request the next one because we want to try again 459 | // even if one of the functions below fails (given we're 460 | // running user code) 461 | requestAnimationFrame(render, canvas); 462 | if (playing || g_captureFn) { 463 | updateTimeDisplay(); 464 | g_visualizer.render(g_byteBeat, g_analyzers); 465 | if (g_captureFn) { 466 | g_captureFn(); 467 | } 468 | } 469 | } 470 | render(); 471 | 472 | function readURL(hash) { 473 | const data = Object.fromEntries(new URLSearchParams(hash).entries()); 474 | const t = data.t !== undefined ? parseFloat(data.t) : 1; 475 | const e = data.e !== undefined ? parseFloat(data.e) : 0; 476 | const s = data.s !== undefined ? parseFloat(data.s) : 8000; 477 | let rateNdx = sampleRates.indexOf(s); 478 | if (rateNdx < 0) { 479 | rateNdx = sampleRates.length; 480 | sampleRateElem.appendChild(addOption(s)); 481 | sampleRates.push(s); 482 | } 483 | setSelectOption(sampleRateElem, rateNdx); 484 | setSelectOption(beatTypeElem, t); 485 | setSelectOption(expressionTypeElem, e); 486 | g_byteBeat.setType(parseInt(t)); 487 | g_byteBeat.setExpressionType(parseInt(e)); 488 | g_byteBeat.setDesiredSampleRate(parseInt(s)); 489 | g_byteBeat.reset(); 490 | if (data.v) { 491 | setVisualizerByName(data.v); 492 | } 493 | const bytes = convertHexToBytes(data.bb); 494 | compressor.decompress(bytes, function(text) { 495 | doNotSetURL = true; 496 | codeElem.value = text; 497 | compile(text, true); 498 | }, 499 | dummyFunction); 500 | if (data.debug) { 501 | g_debugElem = document.querySelector('#debug'); 502 | g_debugElem.style.display = ''; 503 | } 504 | } 505 | 506 | function onWindowResize(/*event*/) { 507 | g_byteBeat.resize(canvas.clientWidth, canvas.clientHeight); 508 | g_visualizer.resize(canvas.clientWidth, canvas.clientHeight); 509 | } 510 | } 511 | // var dataURL = captureScreenshot(400, 100, firstLine); 512 | 513 | function captureScreenshot(ctx, canvas, text) { 514 | const width = ctx.canvas.width; 515 | const height = ctx.canvas.height; 516 | ctx.fillStyle = '#008'; 517 | ctx.fillRect(0, 0, width, height); 518 | ctx.drawImage(canvas, 0, 0, width, height); 519 | ctx.font = 'bold 20px monospace'; 520 | const infos = [ 521 | {x: 2, y: 2, color: '#000'}, 522 | {x: 0, y: 1, color: '#000'}, 523 | {x: 1, y: 0, color: '#000'}, 524 | {x: 0, y: 0, color: '#FFF'}, 525 | ]; 526 | for (let i = 0; i < infos.length; ++i) { 527 | const info = infos[i]; 528 | ctx.fillStyle = info.color; 529 | ctx.fillText(text, 20 + info.x, height - 20 + info.y, width - 40); 530 | } 531 | return g_screenshotCanvas.toDataURL(); 532 | } 533 | 534 | function startSave() { 535 | if (!g_saving) { 536 | g_saving = true; 537 | showSaveDialog(); 538 | } 539 | } 540 | 541 | async function showSaveDialog() { 542 | function closeSave() { 543 | $('savedialog').style.display = 'none'; 544 | window.removeEventListener('keypress', handleKeyPress); 545 | g_saving = false; 546 | g_screenshot = ''; // just cuz. 547 | } 548 | function handleKeyPress(event) { 549 | if (event.code === 'Escape') { 550 | closeSave(); 551 | } 552 | } 553 | const saveData = (function() { 554 | const a = el('a', {style: {display: 'none'}}); 555 | document.body.appendChild(a); 556 | return function saveData(blob, fileName) { 557 | const url = window.URL.createObjectURL(blob); 558 | a.href = url; 559 | a.download = fileName; 560 | a.click(); 561 | }; 562 | }()); 563 | 564 | function wait(ms = 0) { 565 | return new Promise((resolve) => { 566 | setTimeout(resolve, ms); 567 | }); 568 | } 569 | 570 | function save() { 571 | async function realSave() { 572 | const numSeconds = parseFloat($('seconds').value); 573 | if (numSeconds > 0) { 574 | const wasPlaying = playing; 575 | if (playing) { 576 | pause(); 577 | } 578 | // there are issues where. The stack should be 579 | // reset if nothing else. 580 | const sampleRate = g_byteBeat.getDesiredSampleRate(); 581 | const numSamplesNeeded = sampleRate * numSeconds | 0; 582 | const numChannels = 2; 583 | const wavMaker = new WavMaker(sampleRate, numChannels); 584 | const context = await g_byteBeat.createContext(); 585 | const stack = await g_byteBeat.createStack(); 586 | for (let i = 0; i < numSamplesNeeded; i += sampleRate) { 587 | const start = i; 588 | const end = Math.min(i + sampleRate, numSamplesNeeded); 589 | const dataP = []; 590 | for (let channel = 0; channel < numChannels; ++channel) { 591 | dataP.push(g_byteBeat.getSamplesForTimeRange(start, end, end - start, context, stack, channel)); 592 | } 593 | const data = await Promise.all(dataP); 594 | wavMaker.addData(data); 595 | await wait(); 596 | } 597 | const blob = wavMaker.getWavBlob(); 598 | saveData(blob, 'html5bytebeat.wav'); 599 | if (wasPlaying) { 600 | play(); 601 | } 602 | } 603 | closeSave(); 604 | } 605 | realSave(); 606 | } 607 | 608 | const firstLine = strip(strip(codeElem.value.split('\n')[0]).replace(/^\/\//, '')); 609 | const p = makeExposedPromise(); 610 | g_captureFn = () => { 611 | g_captureFn = undefined; 612 | p.resolve(captureScreenshot(g_screenshotContext, canvas, firstLine)); 613 | }; 614 | g_screenshot = await p.promise; 615 | 616 | window.addEventListener('keypress', handleKeyPress); 617 | if (!g_saveDialogInitialized) { 618 | g_saveDialogInitialized = true; 619 | $('save').addEventListener('click', save); 620 | $('cancel').addEventListener('click', closeSave); 621 | } 622 | const saveDialogElem = $('savedialog'); 623 | const screenshotElem = $('screenshot'); 624 | saveDialogElem.style.display = 'table'; 625 | screenshotElem.src = g_screenshot; 626 | } 627 | 628 | function dummyFunction() {} 629 | 630 | function updateTimeDisplay() { 631 | timeElem.innerHTML = g_byteBeat.getTime(); 632 | } 633 | 634 | async function setExpressions(expressions, resetToZero) { 635 | let error; 636 | try { 637 | await g_byteBeat.setExpressions(expressions, resetToZero); 638 | 639 | } catch (e) { 640 | error = e; 641 | } 642 | 643 | compileStatusElem.textContent = error ? error : '*'; 644 | compileStatusElem.classList.toggle('error', error); 645 | setURL(); 646 | } 647 | 648 | async function compile(text, resetToZero) { 649 | const sections = splitBySections(text); 650 | if (sections.default || sections.channel1) { 651 | const expressions = [sections.default?.body || sections.channel1?.body]; 652 | if (sections.channel2) { 653 | expressions.push(sections.channel2.body); 654 | } 655 | if (resetToZero) { 656 | g_visualizer.reset(); 657 | } 658 | await setExpressions(expressions, resetToZero); 659 | if (resetToZero) { 660 | g_visualizer.reset(); 661 | } 662 | } 663 | if (sections.vsa) { 664 | g_vsaEffect.setURL(sections.vsa.argString); 665 | setVisualizer(g_vsaIndex); 666 | } 667 | } 668 | 669 | function setURL() { 670 | if (doNotSetURL) { 671 | doNotSetURL = false; 672 | return; 673 | } 674 | compressor.compress(codeElem.value, 1, function(bytes) { 675 | const hex = convertBytesToHex(bytes); 676 | g_ignoreHashChange = true; 677 | const vNdx = visualTypeElem.selectedIndex; 678 | const params = new URLSearchParams({ 679 | t: g_byteBeat.getType(), 680 | e: g_byteBeat.getExpressionType(), 681 | s: g_byteBeat.getDesiredSampleRate(), 682 | ...(vNdx > 2 && {v: visualTypeElem.children[vNdx].textContent}), 683 | bb: hex, 684 | }); 685 | window.location.replace(`#${params.toString()}`); 686 | }, 687 | dummyFunction); 688 | } 689 | 690 | { 691 | songList(); 692 | $('loadingContainer').style.display = 'none'; 693 | const s = $('startContainer'); 694 | s.style.display = ''; 695 | 696 | const mayBeChrome122 = (navigator.userAgentData?.brands || []).findIndex(e => e.version === '122') >= 0; 697 | if (mayBeChrome122) { 698 | const chrome122Issue = $('chrome122issue'); 699 | chrome122Issue.style.display = ''; 700 | } 701 | 702 | s.addEventListener('click', function() { 703 | s.style.display = 'none'; 704 | main(); 705 | }, false); 706 | } 707 | -------------------------------------------------------------------------------- /editor/songList.js: -------------------------------------------------------------------------------- 1 | import {createElem as el} from './elem.js'; 2 | import { 3 | typeParamToTypeName, 4 | expressionTypeParamToExpressionName, 5 | } from './utils.js'; 6 | 7 | function sampleRateParamToSampleRate(s) { 8 | const sampleRate = parseInt(s); 9 | return `${sampleRate / 1000 | 0}k`; 10 | } 11 | 12 | function valueOrDefault(v, defaultV) { 13 | return v === undefined ? defaultV : v; 14 | } 15 | 16 | const thePTB = -1;//234804; 17 | function score({user, reactions, groupSize/*, link*/}) { 18 | //const lenHack = 10000000; 19 | return (user.id === thePTB ? 1000000 : 0) + 20 | (reactions['+1'] + 21 | reactions['laugh'] + 22 | reactions['heart'] + 23 | reactions['hooray'] - 24 | reactions['-1']) / groupSize + 25 | //lenHack / link.length / lenHack + 26 | 0; 27 | } 28 | 29 | /* 30 | const reactionMap = new Map([ 31 | ['+1', '👍'], 32 | ['laugh', '🤣'], 33 | ['heart', '❤️'], 34 | ['hooray', '🎉'], 35 | ]); 36 | function makeReactions(reactions, groupSize, user) { 37 | const scores = []; 38 | for (const [reaction, count] of Object.entries(reactions)) { 39 | const emoji = reactionMap.get(reaction); 40 | if (emoji) { 41 | const score = user === thePTB ? count : count / groupSize; 42 | if (score >= 1) { 43 | scores.push(`${emoji}(${score | 0})`); 44 | } 45 | } 46 | } 47 | return scores.join(''); 48 | } 49 | */ 50 | function makeReactions() { 51 | return ''; 52 | } 53 | 54 | export default async function loadSongs() { 55 | const showSongsElem = document.querySelector('#showSongs'); 56 | try { 57 | //const url = 'editor/songs.json'; 58 | const url = 'https://greggman.github.io/html5bytebeat/editor/songs.json'; 59 | 60 | const res = await fetch(url); 61 | const songs = await res.json(); 62 | const localBase = `${window.location.origin}${window.location.pathname}`; 63 | const origBase = 'https://greggman.com/downloads/examples/html5bytebeat/html5bytebeat.html'; 64 | const songsElem = document.querySelector('#songs'); 65 | const songListElem = songsElem.querySelector('#song-list'); 66 | 67 | const sortedSongs = songs.slice().sort((a, b) => { 68 | const scoreA = score(a); 69 | const scoreB = score(b); 70 | return Math.sign(scoreB - scoreA); 71 | }); 72 | 73 | const sizeToBin = size => { 74 | if (size < 256) { 75 | return 0; 76 | } else if (size < 1024) { 77 | return 1; 78 | } else { 79 | return 2; 80 | } 81 | }; 82 | 83 | const sizeBinLabels = ['small(≤256b)', 'medium(≤1k)', 'large(>1k)']; 84 | const sizeBinToString = bin => sizeBinLabels[bin]; 85 | 86 | const categories = {}; 87 | for (const {title, size, link, reactions, groupSize, user} of sortedSongs) { 88 | const url = new URL(link); 89 | const q = Object.fromEntries(new URLSearchParams(url.hash.substring(1)).entries()); 90 | const type = typeParamToTypeName(valueOrDefault(q.t, 1)); 91 | const expressionType = expressionTypeParamToExpressionName(valueOrDefault(q.e, 0)); 92 | const sampleRate = sampleRateParamToSampleRate(valueOrDefault(q.s, 8000)); 93 | const subCategory = categories[type] || {}; 94 | categories[type] = subCategory; 95 | const subSongs = subCategory[expressionType] || []; 96 | subCategory[expressionType] = subSongs; 97 | const bin = sizeToBin(size); 98 | const sizeBin = subSongs[bin] || []; 99 | subSongs[bin] = sizeBin; 100 | const reaction = makeReactions(reactions, groupSize, user); 101 | sizeBin.push({title: `${reaction}${title}`, link, sampleRate}); 102 | } 103 | 104 | function makeSubTree(className, name, children) { 105 | return el('details', {className, open: true}, [ 106 | el('summary', {textContent: name}), 107 | el('div', {}, children), 108 | ]); 109 | } 110 | 111 | const currentHref = window.location.href.replace(origBase, localBase); 112 | 113 | const makeSongElements = (songs) => { 114 | return songs.map(({title, link, sampleRate}) => { 115 | const href = link.replace(origBase, localBase); 116 | return el('a', { 117 | href, 118 | textContent: `${title} (${sampleRate})`, 119 | onClick: highlightLink, 120 | ...(href === currentHref && {classList: 'highlight'}), 121 | }); 122 | }); 123 | }; 124 | 125 | const makeSizeBins = (sizeBins) => { 126 | return sizeBins.filter(b => !!b).map((songs, bin) => 127 | makeSubTree('size-bin', sizeBinToString(bin), makeSongElements(songs)) 128 | ); 129 | }; 130 | 131 | const makeSubCategories = (subCategories) => { 132 | return [...Object.entries(subCategories)].map(([subCategory, sizeBins]) => 133 | makeSubTree('sub-category', subCategory, makeSizeBins(sizeBins)), 134 | ); 135 | }; 136 | 137 | for (const [category, subCategories] of Object.entries(categories)) { 138 | const details = makeSubTree('category', category, makeSubCategories(subCategories)); 139 | songListElem.appendChild(details); 140 | } 141 | 142 | { 143 | const elem = document.querySelector('.highlight'); 144 | if (elem) { 145 | // If a match was found scroll it into view in the song list 146 | // We have to make the song list visible for scrollInfoView to 147 | // work. 148 | songsElem.style.display = ''; 149 | elem.scrollIntoView(); 150 | songsElem.style.display = 'none'; 151 | } 152 | } 153 | 154 | function highlightLink() { 155 | const links = songsElem.querySelectorAll('a'); 156 | for (const link of links) { 157 | link.classList.toggle('highlight', link === this); 158 | } 159 | } 160 | 161 | const searchElem = document.querySelector('#search'); 162 | function search() { 163 | const str = searchElem.value.toLowerCase(); 164 | const links = songsElem.querySelectorAll('a'); 165 | 166 | links.forEach(function(link, ndx){ 167 | const text = link.textContent.toLowerCase(); 168 | if (str.length && !text.includes(str)) { 169 | link.classList.add('hide'); 170 | } else { 171 | link.classList.remove('hide'); 172 | } 173 | link.classList.toggle('odd', ndx & 1); 174 | }); 175 | } 176 | search(); // sets odd class on full list 177 | 178 | searchElem.addEventListener('keyup', search); 179 | 180 | showSongsElem.addEventListener('click', () => { 181 | const show = !!songsElem.style.display; 182 | songsElem.style.display = show ? '' : 'none'; 183 | showSongsElem.textContent = show ? '▼ beats' : '▶ beats'; 184 | }); 185 | } catch (e) { 186 | console.error(`could not load songs.json: ${e}`); 187 | showSongsElem.style.display = 'none'; 188 | } 189 | } 190 | -------------------------------------------------------------------------------- /editor/utils.js: -------------------------------------------------------------------------------- 1 | export const s_beatTypes = ['bytebeat', 'floatbeat', 'signed bytebeat']; 2 | export function typeParamToTypeName(s) { 3 | return s_beatTypes[parseInt(s)]; 4 | } 5 | 6 | export const s_expressionTypes = ['infix', 'postfix(rpn)', 'glitch', 'function']; 7 | export function expressionTypeParamToExpressionName(s) { 8 | return s_expressionTypes[parseInt(s)]; 9 | } 10 | 11 | export function convertHexToBytes(text) { 12 | const array = []; 13 | for (let i = 0; i < text.length; i += 2) { 14 | const tmpHex = text.substring(i, i + 2); 15 | array.push(parseInt(tmpHex, 16)); 16 | } 17 | return array; 18 | } 19 | 20 | export function convertBytesToHex(byteArray) { 21 | let hex = ''; 22 | const il = byteArray.length; 23 | for (let i = 0; i < il; i++) { 24 | if (byteArray[i] < 0) { 25 | byteArray[i] = byteArray[i] + 256; 26 | } 27 | let tmpHex = byteArray[i].toString(16); 28 | // add leading zero 29 | if (tmpHex.length === 1) { 30 | tmpHex = '0' + tmpHex; 31 | } 32 | hex += tmpHex; 33 | } 34 | return hex; 35 | } 36 | 37 | // Splits a string, looking for //:name 38 | const g_splitRE = new RegExp(/\/\/:([a-zA-Z0-9_-]+)(.*)/); 39 | const g_headerRE = new RegExp(/^\/\/ ([a-zA-Z0-9_-]+): (\S.*?)$/gm); 40 | 41 | export function splitBySections(str) { 42 | const sections = {}; 43 | 44 | { 45 | const matches = str.matchAll(g_headerRE); 46 | for (const m of matches) { 47 | sections[m[1]] = { argString: m[2].trim(), body: '' }; 48 | } 49 | } 50 | 51 | function getNextSection(str) { 52 | const pos = str.search(g_splitRE); 53 | if (pos < 0) { 54 | return str; 55 | } 56 | const m = str.match(g_splitRE); 57 | const sectionName = m[1]; 58 | const newStr = getNextSection(str.substring(pos + 3 + sectionName.length)); 59 | sections[sectionName] = { 60 | argString: m[2].trim(), 61 | body: newStr, 62 | }; 63 | return str.substring(0, pos); 64 | } 65 | str = getNextSection(str); 66 | if (str.length) { 67 | sections.default = { 68 | body: str, 69 | }; 70 | } 71 | return sections; 72 | } 73 | 74 | export function makeExposedPromise() { 75 | const p = {}; 76 | p.promise = new Promise((resolve, reject) => { 77 | p.resolve = resolve; 78 | p.reject = reject; 79 | }); 80 | return p; 81 | } -------------------------------------------------------------------------------- /editor/visualizers/CanvasVisualizer.js: -------------------------------------------------------------------------------- 1 | import Visualizer from './Visualizer.js'; 2 | 3 | export default class CanvasVisualizer extends Visualizer { 4 | constructor(canvas) { 5 | super(canvas); 6 | this.ctx = canvas.getContext('2d'); 7 | this.temp = new Float32Array(1); 8 | this.resize(512, 512); 9 | this.type = 1; 10 | } 11 | 12 | resize(width, height) { 13 | const canvas = this.canvas; 14 | canvas.width = canvas.clientWidth; 15 | canvas.height = canvas.clientHeight; 16 | this.positions = new Float32Array(width); 17 | this.oldPositions = new Float32Array(width); 18 | this.width = width; 19 | this.height = height; 20 | this.position = 0; 21 | this.drawPosition = 0; 22 | this.drawCount = 0; 23 | } 24 | 25 | reset() { 26 | this.position = 0; 27 | this.drawPosition = 0; 28 | this.drawCount = 0; 29 | const canvas = this.canvas; 30 | this.ctx.clearRect(0, 0, canvas.width, canvas.height); 31 | } 32 | 33 | update(buffer, length) { 34 | // Yes I know this is dumb. I should just do the last 2 at most. 35 | let s = 0; 36 | let p = this.position; 37 | const ps = this.positions; 38 | while (length) { 39 | const max = Math.min(length, this.width - p); 40 | for (let i = 0; i < max; ++i) { 41 | ps[p++] = buffer[s++]; 42 | } 43 | p = p % this.width; 44 | this.drawCount += max; 45 | length -= max; 46 | } 47 | this.position = p; 48 | } 49 | 50 | render() { 51 | let count = Math.min(this.drawCount, this.width); 52 | let dp = this.drawPosition; 53 | const ctx = this.ctx; 54 | const old = this.oldPositions; 55 | const ps = this.positions; 56 | const halfHeight = this.height / 2; 57 | ctx.fillStyle = 'rgb(255,0,0)'; 58 | /* horizontal */ 59 | while (count) { 60 | ctx.clearRect(dp, old[dp], 1, 1); 61 | const newPos = Math.floor(-ps[dp] * halfHeight + halfHeight); 62 | ctx.fillRect(dp, newPos, 1, 1); 63 | old[dp] = newPos; 64 | dp = (dp + 1) % this.width; 65 | --count; 66 | } 67 | 68 | /* vertical hack (drawing the wave vertically should be faster */ 69 | /* 70 | var w = this.width; 71 | var h = this.height; 72 | var hw = Math.floor(w * 0.5); 73 | while (count) { 74 | var y = Math.floor(dp * h / w); 75 | var oldX = Math.floor(old[dp] * w / h * 0.3); 76 | ctx.clearRect(hw - oldX, y, oldX * 2, 1); 77 | var newPos = Math.floor(-ps[dp] * halfHeight + halfHeight); 78 | var x = Math.floor(newPos * w / h * 0.3); 79 | ctx.fillRect(hw - x, y, x * 2, 1, 1); 80 | old[dp] = newPos; 81 | dp = (dp + 1) % this.width; 82 | --count; 83 | } 84 | */ 85 | this.drawCount = 0; 86 | this.drawPosition = dp; 87 | } 88 | } -------------------------------------------------------------------------------- /editor/visualizers/NullVisualizer.js: -------------------------------------------------------------------------------- 1 | import Visualizer from './Visualizer.js'; 2 | 3 | export default class NullVisualizer extends Visualizer { 4 | constructor(canvas) { 5 | super(canvas); 6 | } 7 | } -------------------------------------------------------------------------------- /editor/visualizers/Visualizer.js: -------------------------------------------------------------------------------- 1 | export default class Visualizer { 2 | constructor(canvas) { 3 | this.canvas = canvas; 4 | this.type = 1; 5 | } 6 | 7 | // called when the window resizes 8 | resize(/*width, height*/) { 9 | // extend and override 10 | } 11 | 12 | // called the the time is reset to 0 13 | reset() { 14 | // extend and override 15 | } 16 | 17 | // called with audio data 18 | // If buffer0 === buffer1 then it's mono 19 | // else it's stereo. 20 | update(/*buffer0, buffer1, length*/) { 21 | // extend and override 22 | } 23 | 24 | // Called on requestAnimationFrame 25 | render(/*byteBeat*/) { 26 | // extend and override 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /editor/visualizers/WebGLVisualizer.js: -------------------------------------------------------------------------------- 1 | // import 'https://greggman.github.io/webgl-lint/webgl-lint.js'; 2 | import * as twgl from '../../js/twgl-full.module.js'; 3 | import Visualizer from './Visualizer.js'; 4 | 5 | export default class WebGLVisualizer extends Visualizer { 6 | constructor(gl, effects) { 7 | super(gl.canvas); 8 | this.gl = gl; 9 | this.temp = new Float32Array(1); 10 | this.resolution = new Float32Array(2); 11 | this.commonUniforms = { 12 | time: 0, 13 | resolution: this.resolution, 14 | }; 15 | 16 | this.effects = effects; 17 | this.resize(512, 512); 18 | } 19 | 20 | resize(width, height) { 21 | const gl = this.gl; 22 | const canvas = this.canvas; 23 | canvas.width = canvas.clientWidth; 24 | canvas.height = canvas.clientHeight; 25 | gl.viewport(0, 0, gl.drawingBufferWidth, gl.drawingBufferHeight); 26 | 27 | for (const effect of this.effects) { 28 | effect.resize(gl); 29 | } 30 | 31 | this.width = width; 32 | this.height = height; 33 | this.then = performance.now() * 0.001; 34 | } 35 | 36 | reset() { 37 | const gl = this.gl; 38 | this.then = performance.now() * 0.001; 39 | this.position = 0; 40 | 41 | for (const effect of this.effects) { 42 | effect.reset(gl); 43 | } 44 | } 45 | 46 | render(byteBeat, analyzers) { 47 | const gl = this.gl; 48 | twgl.bindFramebufferInfo(gl); 49 | gl.clearColor(0, 0, 0.3, 1); 50 | gl.clear(gl.COLOR_BUFFER_BIT); 51 | 52 | this.resolution[0] = gl.drawingBufferWidth; 53 | this.resolution[1] = gl.drawingBufferHeight; 54 | const now = performance.now(); 55 | this.commonUniforms.time = (now - this.then) * 0.001; 56 | 57 | for (const effect of this.effects) { 58 | effect.render(gl, this.commonUniforms, byteBeat, analyzers); 59 | } 60 | } 61 | } 62 | 63 | -------------------------------------------------------------------------------- /editor/visualizers/effects/DataEffect.js: -------------------------------------------------------------------------------- 1 | import * as twgl from '../../../js/twgl-full.module.js'; 2 | import { drawEffect } from './effect-utils.js'; 3 | 4 | const colorBlue = new Float32Array([0, 0, 1, 1]); 5 | const colorGray = new Float32Array([0.25, 0.25, 0.25, 1]); 6 | 7 | const kChunkSize = 1024; 8 | 9 | export default class DataEffect { 10 | constructor(gl) { 11 | this.programInfo = twgl.createProgramInfo(gl, [ 12 | ` 13 | attribute vec2 position; 14 | uniform float offset; 15 | varying vec2 v_texCoord; 16 | void main() { 17 | gl_Position = vec4(position, 0, 1); 18 | v_texCoord = vec2(position * 0.5 + 0.5) + vec2(offset, 0); 19 | } 20 | `, 21 | ` 22 | precision mediump float; 23 | varying vec2 v_texCoord; 24 | uniform sampler2D tex; 25 | uniform vec4 color; 26 | void main() { 27 | int c = int(texture2D(tex, fract(v_texCoord)).r * 255.0); 28 | int y = int(v_texCoord.y * 8.0); 29 | int p = int(pow(2.0, float(y))); 30 | c = c / p; 31 | float m = mod(float(c), 2.0); 32 | float line = mod(gl_FragCoord.y, 3.0) / 2.0; 33 | gl_FragColor = color * m; 34 | } 35 | `, 36 | ]); 37 | this.uniforms = { 38 | offset: 0, 39 | color: new Float32Array([0, 0, 1, 1]), 40 | }; 41 | this.bufferInfo = twgl.primitives.createXYQuadBufferInfo(gl); 42 | this.dataTex = [ 43 | twgl.createTexture(gl, { 44 | src: [0], 45 | format: gl.LUMINANCE, 46 | minMag: gl.NEAREST, 47 | wrap: gl.CLAMP_TO_EDGE, 48 | }), 49 | twgl.createTexture(gl, { 50 | src: [0], 51 | format: gl.LUMINANCE, 52 | minMag: gl.NEAREST, 53 | wrap: gl.CLAMP_TO_EDGE, 54 | }), 55 | ]; 56 | this.dataCursor = 0; 57 | this.data = []; 58 | } 59 | reset(gl) { 60 | for (let i = 0; i < this.dataWidth; ++i) { 61 | this.dataBuf[i] = 0; 62 | } 63 | this.resize(gl); 64 | } 65 | 66 | async resize(gl) { 67 | this.dataWidth = gl.drawingBufferWidth; 68 | const dataBuf = new Uint8Array(this.dataWidth); 69 | this.dataPos = 0; 70 | this.dataPixel = new Uint8Array(1); 71 | for (const tex of this.dataTex) { 72 | gl.bindTexture(gl.TEXTURE_2D, tex); 73 | gl.texImage2D( 74 | gl.TEXTURE_2D, 0, gl.LUMINANCE, this.dataWidth, 1, 0, 75 | gl.LUMINANCE, gl.UNSIGNED_BYTE, dataBuf); 76 | } 77 | this.dataBuf = dataBuf; 78 | this.dataTime = 0; 79 | this.oldDataTime = 0; 80 | this.data = new Map(); 81 | this.state = 'init'; 82 | } 83 | 84 | async #getData(byteBeat) { 85 | this.updating = true; 86 | const start = Math.ceil(this.dataTime / kChunkSize) * kChunkSize; 87 | const numChannels = byteBeat.getNumChannels(); 88 | const dataP = []; 89 | for (let channel = 0; channel < numChannels; ++channel) { 90 | dataP.push(byteBeat.getSamplesForTimeRange(start, start + kChunkSize, kChunkSize, this.dataContext, this.dataStack, channel)); 91 | } 92 | const data = await Promise.all(dataP); 93 | const chunkId = start / kChunkSize; 94 | this.data.set(chunkId, data); 95 | this.updating = false; 96 | } 97 | 98 | #update(byteBeat) { 99 | const noData = this.data.length === 0; 100 | const passingHalfWayPoint = (this.oldDataTime % kChunkSize) < kChunkSize / 2 && (this.dataTime % kChunkSize) >= kChunkSize / 2; 101 | const passingChunk = (this.oldDataTime % kChunkSize) === kChunkSize - 1 && this.dataTime % kChunkSize === 0; 102 | const oldChunkId = this.oldDataTime / kChunkSize | 0; 103 | this.oldDataTime = this.dataTime; 104 | if (passingChunk) { 105 | this.data.delete(oldChunkId); 106 | } 107 | if (!this.updating && (noData || passingHalfWayPoint)) { 108 | this.#getData(byteBeat); 109 | } 110 | } 111 | 112 | async #init(byteBeat) { 113 | if (this.dataContext) { 114 | byteBeat.destroyContext(this.dataContext); 115 | byteBeat.destroyStack(this.dataStack); 116 | } 117 | this.dataContext = await byteBeat.createContext(); 118 | this.dataStack = await byteBeat.createStack(); 119 | await this.#getData(byteBeat); 120 | this.state = 'running'; 121 | } 122 | 123 | render(gl, commonUniforms, byteBeat) { 124 | if (this.state === 'init') { 125 | this.state = 'initializing'; 126 | this.#init(byteBeat); 127 | } 128 | if (this.state !== 'running') { 129 | return; 130 | } 131 | this.#update(byteBeat); 132 | const numChannels = byteBeat.getNumChannels(); 133 | 134 | const {uniforms, programInfo, bufferInfo} = this; 135 | 136 | const chunkId = this.dataTime / kChunkSize | 0; 137 | const chunk = this.data.get(chunkId); 138 | const ndx = this.dataTime % kChunkSize; 139 | for (let channel = 0; channel < numChannels; ++channel) { 140 | try { 141 | const ch = chunk[channel]; 142 | const sample = ch[ndx]; 143 | this.dataPixel[0] = Math.round(sample * 127) + 127; 144 | } catch { 145 | // 146 | } 147 | gl.bindTexture(gl.TEXTURE_2D, this.dataTex[channel]); 148 | gl.texSubImage2D(gl.TEXTURE_2D, 0, this.dataPos, 0, 1, 1, gl.LUMINANCE, gl.UNSIGNED_BYTE, this.dataPixel); 149 | this.dataPos = (this.dataPos + 1) % this.dataWidth; 150 | 151 | uniforms.color = channel ? colorGray : colorBlue; 152 | uniforms.tex = this.dataTex[channel]; 153 | uniforms.offset = this.dataPos / this.dataWidth; 154 | if (channel) { 155 | gl.enable(gl.BLEND); 156 | gl.blendFunc(gl.ONE, gl.ONE); 157 | } 158 | drawEffect(gl, programInfo, bufferInfo, uniforms, commonUniforms, gl.TRIANGLES); 159 | if (channel) { 160 | gl.disable(gl.BLEND); 161 | } 162 | } 163 | ++this.dataTime; 164 | } 165 | } -------------------------------------------------------------------------------- /editor/visualizers/effects/FFTEffect.js: -------------------------------------------------------------------------------- 1 | import * as twgl from '../../../js/twgl-full.module.js'; 2 | import { drawEffect } from './effect-utils.js'; 3 | 4 | const colorDarkCyan = [0, 0.7, 0.7, 1]; 5 | const colorDarkYellow = [0.7, 0.7, 0, 1]; 6 | 7 | export default class FFTEffect { 8 | constructor(gl) { 9 | if (gl.getParameter(gl.MAX_VERTEX_TEXTURE_IMAGE_UNITS) < 1) { 10 | console.warn('vertex texture units not supported'); 11 | const noop = _ => _; 12 | this.reset = noop; 13 | this.resize = noop; 14 | this.render = noop; 15 | return; 16 | } 17 | this.programInfo = twgl.createProgramInfo(gl, [ 18 | ` 19 | attribute float column; 20 | uniform sampler2D heightTex; 21 | uniform float heightTexWidth; 22 | uniform float position; 23 | uniform vec2 offset; 24 | uniform vec2 resolution; 25 | void main() { 26 | float width = resolution[0]; 27 | float height = resolution[1]; 28 | 29 | float oneVerticalPixel = 2.0 / height; 30 | float oneHorizontalTexel = 1.0 / heightTexWidth; 31 | 32 | float odd = mod(column, 2.0); 33 | 34 | float c = floor(column / 2.0) / width; 35 | float u = c + oneHorizontalTexel * odd; 36 | float h = texture2D(heightTex, vec2(u, 0.5)).r * 2.0 - 1.0 37 | + oneVerticalPixel * odd; 38 | gl_Position = vec4(c * 2.0 - 1.0, h, 0, 1) + vec4(offset, 0, 0); 39 | } 40 | `, 41 | ` 42 | precision mediump float; 43 | uniform vec4 color; 44 | void main() { 45 | gl_FragColor = color; 46 | } 47 | `, 48 | ]); 49 | 50 | this.heightTex = twgl.createTexture(gl, { 51 | format: gl.LUMINANCE, 52 | src: [0], 53 | minMag: gl.NEAREST, 54 | wrap: gl.CLAMP_TO_EDGE, 55 | }); 56 | 57 | this.uniforms = { 58 | heightTex: this.heightTex, 59 | heightTexWidth: 0, 60 | position: 0, 61 | resolution: [0, 0], 62 | offset: [0, 0], 63 | color: new Float32Array([0, 0.5, 0.5, 1]), 64 | }; 65 | } 66 | reset(/*gl*/) { 67 | } 68 | resize(gl) { 69 | const width = gl.drawingBufferWidth; 70 | const height = gl.drawingBufferHeight; 71 | const column = new Float32Array(width * 2); 72 | 73 | this.width = width; 74 | this.position = 0; 75 | this.uniforms.resolution[0] = width; 76 | this.uniforms.resolution[1] = height; 77 | 78 | this.maxWidth = gl.getParameter(gl.MAX_TEXTURE_SIZE); 79 | 80 | for (let ii = 0; ii < column.length; ++ii) { 81 | column[ii] = ii; 82 | } 83 | const arrays = { 84 | column: { numComponents: 1, data: column, }, 85 | }; 86 | 87 | if (!this.bufferInfo) { 88 | this.bufferInfo = twgl.createBufferInfoFromArrays(gl, arrays); 89 | } else { 90 | twgl.setAttribInfoBufferFromArray(gl, this.bufferInfo.attribs.column, arrays.column); 91 | this.bufferInfo.numElements = width * 2; 92 | } 93 | } 94 | render(gl, commonUniforms, byteBeat, analyzers) { 95 | if (!this.data) { 96 | this.data = new Uint8Array(analyzers[0].frequencyBinCount); 97 | } 98 | const data = this.data; 99 | const numChannels = byteBeat.getNumChannels(); 100 | for (let ch = 0; ch < numChannels; ++ch) { 101 | analyzers[ch].getByteFrequencyData(data); 102 | gl.bindTexture(gl.TEXTURE_2D, this.heightTex); 103 | const texWidth = Math.min(this.maxWidth, data.length); 104 | gl.texImage2D(gl.TEXTURE_2D, 0, gl.LUMINANCE, texWidth, 1, 0, gl.LUMINANCE, gl.UNSIGNED_BYTE, data); 105 | 106 | const {uniforms, programInfo, bufferInfo} = this; 107 | 108 | uniforms.width = this.width; 109 | uniforms.heightTexWidth = texWidth; 110 | uniforms.position = this.position / this.width; 111 | //uniforms.offset[0] = ch / gl.drawingBufferWidth; 112 | uniforms.color = ch ? colorDarkYellow : colorDarkCyan; 113 | gl.enable(gl.BLEND); 114 | gl.blendFunc(gl.ONE, gl.ONE); 115 | drawEffect(gl, programInfo, bufferInfo, uniforms, commonUniforms, gl.LINES); 116 | gl.disable(gl.BLEND); 117 | } 118 | } 119 | } -------------------------------------------------------------------------------- /editor/visualizers/effects/SampleEffect.js: -------------------------------------------------------------------------------- 1 | import * as twgl from '../../../js/twgl-full.module.js'; 2 | import { drawEffect } from './effect-utils.js'; 3 | 4 | const kChunkSize = 1024; 5 | 6 | export default class SampleEffect { 7 | constructor(gl) { 8 | this.programInfo = twgl.createProgramInfo(gl, [ 9 | ` 10 | attribute vec2 position; 11 | uniform float offset; 12 | varying vec2 v_texCoord; 13 | void main() { 14 | gl_Position = vec4(position, 0, 1); 15 | v_texCoord = vec2(position * 0.5 + 0.5) + vec2(offset, 0); 16 | } 17 | `, 18 | ` 19 | precision mediump float; 20 | varying vec2 v_texCoord; 21 | uniform sampler2D tex; 22 | uniform vec4 color; 23 | void main() { 24 | float height = texture2D(tex, fract(v_texCoord)).r * 0.5; 25 | float m = v_texCoord.y > height ? 0.0 : 1.0; 26 | gl_FragColor = color * m; 27 | } 28 | `, 29 | ]); 30 | this.uniforms = { 31 | offset: 0, 32 | color: new Float32Array([0, 0.3, 0, 1]), 33 | }; 34 | this.bufferInfo = twgl.primitives.createXYQuadBufferInfo(gl); 35 | const tex = twgl.createTexture(gl, { 36 | format: gl.LUMINANCE, 37 | src: [0], 38 | minMag: gl.NEAREST, 39 | wrap: gl.CLAMP_TO_EDGE, 40 | }); 41 | this.uniforms.tex = tex; 42 | this.sampleTex = tex; 43 | } 44 | reset(gl) { 45 | this.sampleTime = 0; 46 | this.samplePos = 0; 47 | for (let i = 0; i < this.sampleWidth; ++i) { 48 | this.sampleBuf[i] = 0; 49 | } 50 | gl.bindTexture(gl.TEXTURE_2D, this.sampleTex); 51 | gl.texImage2D( 52 | gl.TEXTURE_2D, 0, gl.LUMINANCE, this.sampleWidth, 1, 0, 53 | gl.LUMINANCE, gl.UNSIGNED_BYTE, this.sampleBuf); 54 | } 55 | resize(gl) { 56 | this.sampleWidth = gl.drawingBufferWidth; 57 | const sampleBuf = new Uint8Array(this.sampleWidth); 58 | this.samplePos = 0; 59 | this.samplePixel = new Uint8Array(1); 60 | gl.bindTexture(gl.TEXTURE_2D, this.sampleTex); 61 | gl.texImage2D( 62 | gl.TEXTURE_2D, 0, gl.LUMINANCE, this.sampleWidth, 1, 0, 63 | gl.LUMINANCE, gl.UNSIGNED_BYTE, sampleBuf); 64 | this.sampleBuf = sampleBuf; 65 | this.sampleTime = 0; 66 | this.data = new Map(); 67 | this.state = 'init'; 68 | } 69 | 70 | async #getData(byteBeat) { 71 | this.updating = true; 72 | const start = Math.ceil(this.sampleTime / kChunkSize) * kChunkSize; 73 | const numChannels = byteBeat.getNumChannels(); 74 | const dataP = []; 75 | for (let channel = 0; channel < numChannels; ++channel) { 76 | dataP.push(byteBeat.getSamplesForTimeRange(start, start + kChunkSize, kChunkSize, this.sampleContext, this.sampleStack, channel)); 77 | } 78 | const data = await Promise.all(dataP); 79 | const chunkId = start / kChunkSize; 80 | this.data.set(chunkId, data); 81 | this.updating = false; 82 | } 83 | 84 | #update(byteBeat) { 85 | const noData = this.data.length === 0; 86 | const passingHalfWayPoint = (this.oldSampleTime % kChunkSize) < kChunkSize / 2 && (this.sampleTime % kChunkSize) >= kChunkSize / 2; 87 | const passingChunk = (this.oldSampleTime % kChunkSize) >= kChunkSize - 2 && this.sampleTime % kChunkSize === 0; 88 | const oldChunkId = this.oldSampleTime / kChunkSize | 0; 89 | this.oldSampleTime = this.sampleTime; 90 | if (passingChunk) { 91 | this.data.delete(oldChunkId); 92 | } 93 | if (!this.updating && (noData || passingHalfWayPoint)) { 94 | this.#getData(byteBeat); 95 | } 96 | } 97 | 98 | async #init(byteBeat) { 99 | if (this.sampleContext) { 100 | byteBeat.destroyContext(this.sampleContext); 101 | byteBeat.destroyStack(this.sampleStack); 102 | } 103 | this.sampleContext = await byteBeat.createContext(); 104 | this.sampleStack = await byteBeat.createStack(); 105 | await this.#getData(byteBeat); 106 | this.state = 'running'; 107 | } 108 | 109 | render(gl, commonUniforms, byteBeat) { 110 | const {uniforms, programInfo, bufferInfo} = this; 111 | 112 | if (this.state === 'init') { 113 | this.state = 'initializing'; 114 | this.#init(byteBeat); 115 | } 116 | if (this.state !== 'running') { 117 | return; 118 | } 119 | this.#update(byteBeat); 120 | 121 | gl.bindTexture(gl.TEXTURE_2D, this.sampleTex); 122 | for (let ii = 0; ii < 2; ++ii) { 123 | const chunkId = this.sampleTime++ / kChunkSize | 0; 124 | const chunk = this.data.get(chunkId); 125 | const ndx = this.sampleTime % kChunkSize; 126 | try { 127 | const ch = chunk[0]; 128 | const sample = ch[ndx]; 129 | this.samplePixel[0] = Math.round(sample * 127) + 127; 130 | } catch { 131 | // 132 | } 133 | gl.texSubImage2D(gl.TEXTURE_2D, 0, this.samplePos, 0, 1, 1, gl.LUMINANCE, gl.UNSIGNED_BYTE, this.samplePixel); 134 | this.samplePos = (this.samplePos + 1) % this.sampleWidth; 135 | } 136 | 137 | gl.enable(gl.BLEND); 138 | gl.blendFunc(gl.ONE, gl.ONE); 139 | uniforms.offset = this.samplePos / this.sampleWidth; 140 | drawEffect(gl, programInfo, bufferInfo, uniforms, commonUniforms, gl.TRIANGLES); 141 | gl.disable(gl.BLEND); 142 | } 143 | } -------------------------------------------------------------------------------- /editor/visualizers/effects/VSAEffect.js: -------------------------------------------------------------------------------- 1 | 2 | import * as twgl from '../../../js/twgl-full.module.js'; 3 | import { 4 | decode, 5 | } from '../../base64.js'; 6 | import compressor from '../../compressor.js'; 7 | 8 | const m4 = twgl.m4; 9 | 10 | const kMaxCount = 100000; 11 | 12 | const s_vsHeader = ` 13 | attribute float vertexId; 14 | 15 | uniform vec2 mouse; 16 | uniform vec2 resolution; 17 | uniform vec4 background; 18 | uniform float time; 19 | uniform float vertexCount; 20 | uniform sampler2D volume; 21 | uniform sampler2D sound; 22 | uniform sampler2D floatSound; 23 | uniform sampler2D touch; 24 | uniform vec2 soundRes; 25 | uniform float _dontUseDirectly_pointSize; 26 | 27 | varying vec4 v_color; 28 | `; 29 | 30 | const s_fs = ` 31 | precision mediump float; 32 | 33 | varying vec4 v_color; 34 | 35 | void main() { 36 | gl_FragColor = v_color; 37 | } 38 | `; 39 | 40 | 41 | const s_historyVS = ` 42 | attribute vec4 position; 43 | attribute vec2 texcoord; 44 | uniform mat4 u_matrix; 45 | varying vec2 v_texcoord; 46 | 47 | void main() { 48 | gl_Position = u_matrix * position; 49 | v_texcoord = texcoord; 50 | } 51 | `; 52 | 53 | const s_historyFS = ` 54 | precision mediump float; 55 | 56 | uniform sampler2D u_texture; 57 | uniform float u_mix; 58 | uniform float u_mult; 59 | varying vec2 v_texcoord; 60 | 61 | void main() { 62 | vec4 color = texture2D(u_texture, v_texcoord); 63 | gl_FragColor = mix(color.aaaa, color.rgba, u_mix) * u_mult; 64 | } 65 | `; 66 | 67 | const s_rectVS = ` 68 | attribute vec4 position; 69 | uniform mat4 u_matrix; 70 | 71 | void main() { 72 | gl_Position = u_matrix * position; 73 | } 74 | `; 75 | 76 | const s_rectFS = ` 77 | precision mediump float; 78 | 79 | uniform vec4 u_color; 80 | 81 | void main() { 82 | gl_FragColor = u_color; 83 | } 84 | `; 85 | 86 | class HistoryTexture { 87 | constructor(gl, options) { 88 | this.gl = gl; 89 | const _width = options.width; 90 | const type = options.type || gl.UNSIGNED_BYTE; 91 | const format = options.format || gl.RGBA; 92 | const Ctor = twgl.getTypedArrayTypeForGLType(type); 93 | const numComponents = twgl.getNumComponentsForFormat(format); 94 | const size = _width * numComponents; 95 | const _buffer = new Ctor(size); 96 | const _texSpec = { 97 | src: _buffer, 98 | height: 1, 99 | min: options.min || gl.LINEAR, 100 | mag: options.mag || gl.LINEAR, 101 | wrap: gl.CLAMP_TO_EDGE, 102 | format: format, 103 | auto: false, // don't set tex params or call genmipmap 104 | }; 105 | const _tex = twgl.createTexture(gl, _texSpec); 106 | 107 | const _length = options.length; 108 | const _historyAttachments = [ 109 | { 110 | format: options.historyFormat || gl.RGBA, 111 | type: type, 112 | mag: options.mag || gl.LINEAR, 113 | min: options.min || gl.LINEAR, 114 | wrap: gl.CLAMP_TO_EDGE, 115 | }, 116 | ]; 117 | 118 | let _srcFBI = twgl.createFramebufferInfo(gl, _historyAttachments, _width, _length); 119 | let _dstFBI = twgl.createFramebufferInfo(gl, _historyAttachments, _width, _length); 120 | 121 | const _historyUniforms = { 122 | u_mix: 0, 123 | u_mult: 1, 124 | u_matrix: m4.identity(), 125 | u_texture: undefined, 126 | }; 127 | 128 | this.buffer = _buffer; 129 | 130 | this.update = (gl, historyProgramInfo, quadBufferInfo) => { 131 | const temp = _srcFBI; 132 | _srcFBI = _dstFBI; 133 | _dstFBI = temp; 134 | 135 | twgl.setTextureFromArray(gl, _tex, _texSpec.src, _texSpec); 136 | 137 | gl.useProgram(historyProgramInfo.program); 138 | twgl.bindFramebufferInfo(gl, _dstFBI); 139 | 140 | // copy from historySrc to historyDst one pixel down 141 | m4.translation([0, 2 / _length, 0], _historyUniforms.u_matrix); 142 | _historyUniforms.u_mix = 1; 143 | _historyUniforms.u_texture = _srcFBI.attachments[0]; 144 | 145 | twgl.setUniforms(historyProgramInfo, _historyUniforms); 146 | twgl.drawBufferInfo(gl, quadBufferInfo); 147 | 148 | // copy audio data into top row of historyDst 149 | _historyUniforms.u_mix = format === gl.ALPHA ? 0 : 1; 150 | _historyUniforms.u_texture = _tex; 151 | m4.translation( 152 | [0, -(_length - 0.5) / _length, 0], 153 | _historyUniforms.u_matrix); 154 | m4.scale( 155 | _historyUniforms.u_matrix, 156 | [1, 1 / _length, 1], 157 | _historyUniforms.u_matrix); 158 | 159 | twgl.setUniforms(historyProgramInfo, _historyUniforms); 160 | twgl.drawBufferInfo(gl, quadBufferInfo); 161 | }; 162 | 163 | this.getTexture = () => { 164 | return _dstFBI.attachments[0]; 165 | }; 166 | } 167 | } 168 | 169 | class CPUHistoryTexture { 170 | constructor(gl, options) { 171 | const _width = options.width; 172 | const type = options.type || gl.UNSIGNED_BYTE; 173 | const format = options.format || gl.RGBA; 174 | const Ctor = twgl.getTypedArrayTypeForGLType(type); 175 | const numComponents = twgl.getNumComponentsForFormat(format); 176 | const _length = options.length; 177 | const _rowSize = _width * numComponents; 178 | const _size = _rowSize * _length; 179 | const _buffer = new Ctor(_size); 180 | const _texSpec = { 181 | src: _buffer, 182 | height: _length, 183 | min: options.min || gl.LINEAR, 184 | mag: options.mag || gl.LINEAR, 185 | wrap: gl.CLAMP_TO_EDGE, 186 | format: format, 187 | auto: false, // don't set tex params or call genmipmap 188 | }; 189 | const _tex = twgl.createTexture(gl, _texSpec); 190 | 191 | this.buffer = _buffer; 192 | 193 | this.update = function update() { 194 | // Upload the latest 195 | twgl.setTextureFromArray(gl, _tex, _texSpec.src, _texSpec); 196 | 197 | // scroll the data 198 | _buffer.copyWithin(_rowSize, 0, _size - _rowSize); 199 | }; 200 | 201 | this.getTexture = function getTexture() { 202 | return _tex; 203 | }; 204 | } 205 | } 206 | 207 | const mainRE = /(void[ \t\n\r]+main[ \t\n\r]*\([ \t\n\r]*\)[ \t\n\r]\{)/g; 208 | function applyTemplateToShader(src) { 209 | let vSrc = s_vsHeader + src; 210 | vSrc = vSrc.replace(mainRE, function(m) { 211 | return `${m}gl_PointSize=1.0;`; 212 | }); 213 | const lastBraceNdx = vSrc.lastIndexOf('}'); 214 | if (lastBraceNdx >= 0) { 215 | const before = vSrc.substr(0, lastBraceNdx); 216 | const after = vSrc.substr(lastBraceNdx); 217 | vSrc = `${before};gl_PointSize = max(0., gl_PointSize*_dontUseDirectly_pointSize);${after}`; 218 | } 219 | return vSrc; 220 | } 221 | 222 | export default class VSAEffect { 223 | constructor(gl) { 224 | this.gl = gl; 225 | this.rectProgramInfo = twgl.createProgramInfo(gl, [s_rectVS, s_rectFS]); 226 | this.historyProgramInfo = twgl.createProgramInfo(gl, [s_historyVS, s_historyFS]); 227 | } 228 | #init(gl, analyser) { 229 | if (this.init) { 230 | return; 231 | } 232 | this.init = true; 233 | const maxTextureSize = gl.getParameter(gl.MAX_TEXTURE_SIZE); 234 | this.numSoundSamples = Math.min(maxTextureSize, analyser.frequencyBinCount); 235 | this.numHistorySamples = 60 * 4; // 4 seconds; 236 | 237 | this.volumeHistory = new HistoryTexture(gl, { 238 | width: 4, 239 | length: this.numHistorySamples, 240 | format: gl.ALPHA, 241 | }); 242 | 243 | this.soundHistory = new HistoryTexture(gl, { 244 | width: this.numSoundSamples, 245 | length: this.numHistorySamples, 246 | format: gl.ALPHA, 247 | }); 248 | 249 | this.touchColumns = 32; 250 | this.touchHistory = new (this.canRenderToFloat ? HistoryTexture : CPUHistoryTexture)(gl, { 251 | width: this.touchColumns, 252 | length: this.numHistorySamples, 253 | type: this.canUseFloat ? gl.FLOAT : gl.UNSIGNED_BYTE, 254 | min: gl.NEAREST, 255 | mag: gl.NEAREST, 256 | }); 257 | 258 | const count = new Float32Array(kMaxCount); 259 | for (let ii = 0; ii < count.length; ++ii) { 260 | count[ii] = ii; 261 | } 262 | const arrays = { 263 | vertexId: { data: count, numComponents: 1 }, 264 | }; 265 | this.countBufferInfo = twgl.createBufferInfoFromArrays(gl, arrays); 266 | this.quadBufferInfo = twgl.createBufferInfoFromArrays(gl, { 267 | position: { numComponents: 2, data: [-1, -1, 1, -1, -1, 1, 1, 1] }, 268 | texcoord: [0, 0, 1, 0, 0, 1, 1, 1], 269 | indices: [0, 1, 2, 2, 1, 3], 270 | }); 271 | 272 | this.uniforms = { 273 | time: 0, 274 | vertexCount: 0, 275 | resolution: [1, 1], 276 | background: [0, 0, 0, 1], 277 | mouse: [0, 0], 278 | sound: undefined, 279 | floatSound: undefined, 280 | soundRes: [this.numSoundSamples, this.numHistorySamples], 281 | _dontUseDirectly_pointSize: 1, 282 | }; 283 | 284 | this.historyUniforms = { 285 | u_mix: 0, 286 | u_matrix: m4.identity(), 287 | u_texture: undefined, 288 | }; 289 | } 290 | async setURL(url) { 291 | try { 292 | const u = new URL(url, window.location.href); 293 | if (u.hostname !== window.location.hostname && u.hostname !== 'www.vertexshaderart.com' && u.hostname !== 'vertexshaderart.com') { 294 | return; 295 | } 296 | if (url === this.currentUrl) { 297 | // It's the current URL 298 | return; 299 | } 300 | if (url === this.pendingUrl) { 301 | // It's the pending Url 302 | return; 303 | } 304 | this.pendingUrl = url; 305 | if (this.compiling) { 306 | return; 307 | } 308 | // It doesn't matter if the URL is bad, we don't want to try again 309 | this.currentUrl = this.pendingUrl; 310 | this.pendingUrl = undefined; 311 | this.compiling = true; 312 | let vsa; 313 | if (u.hash.includes('s=')) { 314 | const q = new URLSearchParams(u.hash.substring(1)); 315 | const bytes = decode(q.get('s')); 316 | const text = await new Promise((resolve, reject) => compressor.decompress(bytes, resolve, () => {}, reject)); 317 | vsa = JSON.parse(text); 318 | } else { 319 | const mungedUrl = url.includes('vertexshaderart.com') 320 | ? `${url}/art.json` 321 | : url; 322 | const req = await fetch(mungedUrl); 323 | vsa = await req.json(); 324 | } 325 | const gl = this.gl; 326 | const vs = applyTemplateToShader(vsa.settings.shader); 327 | const programInfo = await twgl.createProgramInfoAsync(gl, [vs, s_fs]); 328 | this.programInfo = programInfo; 329 | this.vsa = vsa; 330 | } catch (e) { 331 | console.error(e); 332 | } 333 | this.compiling = false; 334 | if (this.pendingUrl) { 335 | const nextUrl = this.pendingUrl; 336 | this.pendingUrl = undefined; 337 | this.setURL(nextUrl); 338 | } 339 | } 340 | reset(/*gl*/) { 341 | } 342 | resize() { 343 | } 344 | 345 | #updateSoundAndTouchHistory(gl, analysers, time) { 346 | // Copy audio data to Nx1 texture 347 | analysers[0].getByteFrequencyData(this.soundHistory.buffer); 348 | 349 | // should we do this in a shader? 350 | { 351 | const buf = this.soundHistory.buffer; 352 | const len = buf.length; 353 | let max = 0; 354 | for (let ii = 0; ii < len; ++ii) { 355 | const v = buf[ii]; 356 | if (v > max) { 357 | max = v; 358 | } 359 | } 360 | this.volumeHistory.buffer[3] = max; 361 | } 362 | this.volumeHistory.buffer[0] = Math.abs(this.maxSample) * 255; 363 | this.volumeHistory.buffer[1] = this.sum * 255; 364 | this.volumeHistory.buffer[2] = this.maxDif * 127; 365 | 366 | if (this.floatSoundHistory) { 367 | this.analyser.getFloatFrequencyData(this.floatSoundHistory.buffer); 368 | } 369 | 370 | // Update time 371 | for (let ii = 0; ii < this.touchColumns; ++ii) { 372 | const offset = ii * 4; 373 | this.touchHistory.buffer[offset + 3] = time; 374 | } 375 | 376 | gl.disable(gl.DEPTH_TEST); 377 | gl.disable(gl.BLEND); 378 | 379 | twgl.setBuffersAndAttributes(gl, this.historyProgramInfo, this.quadBufferInfo); 380 | 381 | this.volumeHistory.update(gl, this.historyProgramInfo, this.quadBufferInfo); 382 | this.soundHistory.update(gl, this.historyProgramInfo, this.quadBufferInfo); 383 | if (this.floatSoundHistory) { 384 | this.floatSoundHistory.update(gl, this.historyProgramInfo, this.quadBufferInfo); 385 | } 386 | this.touchHistory.update(gl, this.historyProgramInfo, this.quadBufferInfo); 387 | } 388 | 389 | #renderScene(gl, volumeHistoryTex, touchHistoryTex, soundHistoryTex, floatSoundHistoryTex, time) { 390 | twgl.bindFramebufferInfo(gl); 391 | const settings = this.vsa.settings; 392 | 393 | const programInfo = this.programInfo; 394 | if (!programInfo) { 395 | return; 396 | } 397 | 398 | gl.enable(gl.DEPTH_TEST); 399 | gl.enable(gl.BLEND); 400 | gl.blendFunc(gl.ONE, gl.ONE_MINUS_SRC_ALPHA); 401 | gl.clearColor(...settings.backgroundColor); 402 | gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT); 403 | 404 | const num = settings.num; 405 | const mode = gl[settings.mode]; 406 | const uniforms = this.uniforms; 407 | uniforms.time = time; 408 | uniforms.vertexCount = num; 409 | uniforms.resolution[0] = gl.drawingBufferWidth; 410 | uniforms.resolution[1] = gl.drawingBufferHeight; 411 | uniforms.background[0] = settings.backgroundColor[0]; 412 | uniforms.background[1] = settings.backgroundColor[1]; 413 | uniforms.background[2] = settings.backgroundColor[2]; 414 | uniforms.background[3] = settings.backgroundColor[3]; 415 | // uniforms.mouse[0] = mouse[0]; 416 | // uniforms.mouse[1] = mouse[1]; 417 | uniforms._dontUseDirectly_pointSize = 1; 418 | uniforms.volume = volumeHistoryTex; 419 | uniforms.sound = soundHistoryTex; 420 | uniforms.floatSound = floatSoundHistoryTex; 421 | uniforms.touch = touchHistoryTex; 422 | 423 | gl.useProgram(programInfo.program); 424 | twgl.setBuffersAndAttributes(gl, programInfo, this.countBufferInfo); 425 | twgl.setUniforms(programInfo, uniforms); 426 | twgl.drawBufferInfo(gl, this.countBufferInfo, mode, num); 427 | 428 | gl.disable(gl.DEPTH_TEST); 429 | gl.disable(gl.BLEND); 430 | } 431 | 432 | render(gl, commonUniforms, byteBeat, analyzers) { 433 | if (!this.vsa || !this.programInfo) { 434 | return; 435 | } 436 | this.#init(gl, analyzers[0]); 437 | const time = byteBeat.getTime() / byteBeat.getDesiredSampleRate(); 438 | this.#updateSoundAndTouchHistory(gl, analyzers, time); 439 | 440 | const volumeHistoryTex = this.volumeHistory.getTexture(); 441 | const touchHistoryTex = this.touchHistory.getTexture(); 442 | const historyTex = this.soundHistory.getTexture(); 443 | const floatHistoryTex = this.floatSoundHistory ? this.floatSoundHistory.getTexture() : historyTex; 444 | this.#renderScene(gl, volumeHistoryTex, touchHistoryTex, historyTex, floatHistoryTex, time); 445 | } 446 | } -------------------------------------------------------------------------------- /editor/visualizers/effects/WaveEffect.js: -------------------------------------------------------------------------------- 1 | import * as twgl from '../../../js/twgl-full.module.js'; 2 | import { drawEffect } from './effect-utils.js'; 3 | 4 | const colorRed = new Float32Array([1, 0, 0, 1]); 5 | const colorMagenta = new Float32Array([1, 0, 1, 1]); 6 | const colorGreen = new Float32Array([0, 1, 0, 1]); 7 | 8 | export default class WaveEffect { 9 | constructor(gl) { 10 | this.programInfo = twgl.createProgramInfo(gl, [ 11 | ` 12 | attribute float column; 13 | attribute float height; 14 | uniform float position; 15 | uniform vec2 offset; 16 | void main() { 17 | gl_Position = vec4(mod(column - position, 1.0) * 2.0 - 1.0, height, 0, 1) + vec4(offset, 0, 0); 18 | } 19 | `, 20 | ` 21 | precision mediump float; 22 | uniform vec4 color; 23 | void main() { 24 | gl_FragColor = color; 25 | } 26 | `, 27 | ]); 28 | this.uniforms = { 29 | position: 0, 30 | offset: [0, 0], 31 | color: new Float32Array([1, 0, 0, 1]), 32 | }; 33 | this.frameCount = 0; 34 | } 35 | reset(gl) { 36 | for (let i = 0; i < this.lineHeightL.length; ++i) { 37 | this.lineHeightL[i] = 0; 38 | this.lineHeightR[i] = 0; 39 | } 40 | twgl.setAttribInfoBufferFromArray(gl, this.bufferInfoL.attribs.height, this.lineHeightL); 41 | twgl.setAttribInfoBufferFromArray(gl, this.bufferInfoR.attribs.height, this.lineHeightR); 42 | this.then = performance.now(); 43 | this.position = 0; 44 | } 45 | resize(gl) { 46 | const width = gl.drawingBufferWidth; 47 | const lineHeight = new Float32Array(width); 48 | const column = new Float32Array(width); 49 | 50 | this.state = 'init'; 51 | this.width = width; 52 | this.position = 0; 53 | this.then = performance.now(); 54 | 55 | this.oneVerticalPixel = 2 / gl.drawingBufferHeight; 56 | 57 | for (let ii = 0; ii < width; ++ii) { 58 | lineHeight[ii] = Math.sin(ii / width * Math.PI * 2); 59 | column[ii] = ii / width; 60 | } 61 | this.lineHeightL = lineHeight; 62 | this.lineHeightR = lineHeight.slice(); 63 | const arrays = { 64 | height: { numComponents: 1, data: lineHeight, }, 65 | column: { numComponents: 1, data: column, }, 66 | }; 67 | 68 | if (!this.bufferInfoL) { 69 | this.bufferInfoL = twgl.createBufferInfoFromArrays(gl, arrays); 70 | this.bufferInfoR = twgl.createBufferInfoFromArrays(gl, arrays); 71 | } else { 72 | twgl.setAttribInfoBufferFromArray(gl, this.bufferInfoL.attribs.height, arrays.height); 73 | twgl.setAttribInfoBufferFromArray(gl, this.bufferInfoL.attribs.column, arrays.column); 74 | twgl.setAttribInfoBufferFromArray(gl, this.bufferInfoR.attribs.height, arrays.height); 75 | twgl.setAttribInfoBufferFromArray(gl, this.bufferInfoR.attribs.column, arrays.column); 76 | this.bufferInfoL.numElements = width; 77 | this.bufferInfoR.numElements = width; 78 | } 79 | } 80 | async #update(gl, byteBeat, elapsedTimeMS) { 81 | if (this.state === 'init') { 82 | this.state = 'initializing'; 83 | if (this.beatContext) { 84 | byteBeat.destroyContext(this.beatContext); 85 | byteBeat.destroyStack(this.beatStack); 86 | } 87 | this.beatContext = await byteBeat.createContext(); 88 | this.beatStack = await byteBeat.createStack(); 89 | this.state = 'running'; 90 | } 91 | if (this.state === 'running') { 92 | this.state = 'waiting'; 93 | const {bufferInfoL, bufferInfoR, beatContext: context, beatStack: stack} = this; 94 | const numChannels = byteBeat.getNumChannels(); 95 | const startTime = this.position; 96 | const endTime = startTime + elapsedTimeMS * 0.001 * byteBeat.getDesiredSampleRate() | 0; 97 | this.position = endTime; 98 | const dataP = []; 99 | for (let channel = 0; channel < numChannels; ++channel) { 100 | dataP.push(byteBeat.getSamplesForTimeRange( 101 | startTime, 102 | endTime, 103 | this.lineHeightL.length, 104 | context, 105 | stack, 106 | channel, 107 | )); 108 | } 109 | const data = await Promise.all(dataP); 110 | for (let channel = 0; channel < numChannels; ++channel) { 111 | const bufferInfo = channel ? bufferInfoR : bufferInfoL; 112 | gl.bindBuffer(gl.ARRAY_BUFFER, bufferInfo.attribs.height.buffer); 113 | gl.bufferSubData(gl.ARRAY_BUFFER, 0, data[channel].subarray(0, this.lineHeightL.length)); 114 | } 115 | this.state = 'running'; 116 | } 117 | } 118 | render(gl, commonUniforms, byteBeat) { 119 | const {uniforms, programInfo, bufferInfoL, bufferInfoR} = this; 120 | const numChannels = byteBeat.getNumChannels(); 121 | 122 | const targetTimeMS = 1000 / (48000 / 4096); 123 | const now = performance.now(); 124 | const elapsedTimeMS = now - this.then; 125 | const run = elapsedTimeMS >= targetTimeMS; 126 | if (run) { 127 | this.then = now; 128 | if (byteBeat.isRunning()) { 129 | this.#update(gl, byteBeat, elapsedTimeMS); 130 | } 131 | } 132 | 133 | for (let channel = 0; channel < numChannels; ++channel) { 134 | const bufferInfo = channel ? bufferInfoR : bufferInfoL; 135 | uniforms.color = channel 136 | ? colorGreen 137 | : (numChannels === 2 ? colorMagenta : colorRed); 138 | //uniforms.position = this.position / this.width; 139 | uniforms.offset[0] = channel / gl.drawingBufferWidth; 140 | uniforms.offset[1] = 0; 141 | if (channel) { 142 | gl.enable(gl.BLEND); 143 | gl.blendFunc(gl.ONE, gl.ONE); 144 | } 145 | drawEffect(gl, programInfo, bufferInfo, uniforms, commonUniforms, gl.LINE_STRIP); 146 | uniforms.offset[0] += 1 / gl.drawingBufferWidth; 147 | uniforms.offset[1] += 1 / gl.drawingBufferHeight; 148 | drawEffect(gl, programInfo, bufferInfo, uniforms, commonUniforms, gl.LINE_STRIP); 149 | if (channel) { 150 | gl.disable(gl.BLEND); 151 | } 152 | } 153 | } 154 | } -------------------------------------------------------------------------------- /editor/visualizers/effects/effect-utils.js: -------------------------------------------------------------------------------- 1 | import * as twgl from '../../../js/twgl-full.module.js'; 2 | 3 | export function drawEffect(gl, programInfo, bufferInfo, uniforms, commonUniforms, primitive) { 4 | gl.useProgram(programInfo.program); 5 | twgl.setBuffersAndAttributes(gl, programInfo, bufferInfo); 6 | twgl.setUniforms(programInfo, commonUniforms, uniforms); 7 | twgl.drawBufferInfo(gl, bufferInfo, primitive); 8 | } -------------------------------------------------------------------------------- /editor/vsa.json: -------------------------------------------------------------------------------- 1 | {"_id":"A8Zc7NFQdTdeKQimv","createdAt":"2017-09-30T17:14:27.900Z","modifiedAt":"2017-09-30T17:14:27.900Z","origId":"7TrYkuK4aHzLqvZ7r","name":"pookymelon","username":"gman","avatarUrl":"https://secure.gravatar.com/avatar/dcc0309895c3d6db087631813efaa9d1?default=retro&size=200","settings":{"num":97200,"mode":"TRIANGLES","sound":"https://soundcloud.com/mixmag-1/premiere-michael-klein-pan-pot-haze-effect","lineSize":"NATIVE","backgroundColor":[0,0,0,1],"shader":"/*\n\n┬ ┬┌─┐┬─┐┌┬┐┌─┐─┐ ┬┌─┐┬ ┬┌─┐┌┬┐┌─┐┬─┐┌─┐┬─┐┌┬┐\n└┐┌┘├┤ ├┬┘ │ ├┤ ┌┴┬┘└─┐├─┤├─┤ ││├┤ ├┬┘├─┤├┬┘ │ \n └┘ └─┘┴└─ ┴ └─┘┴ └─└─┘┴ ┴┴ ┴─┴┘└─┘┴└─┴ ┴┴└─ ┴ \n\n*/\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n#define PI radians(180.0)\n\nvec3 hsv2rgb(vec3 c) {\n c = vec3(c.x, clamp(c.yz, 0.0, 1.0));\n vec4 K = vec4(1.0, 2.0 / 3.0, 1.0 / 3.0, 3.0);\n vec3 p = abs(fract(c.xxx + K.xyz) * 6.0 - K.www);\n return c.z * mix(K.xxx, clamp(p - K.xxx, 0.0, 1.0), c.y);\n}\n\nmat4 rotX(float angleInRadians) {\n float s = sin(angleInRadians);\n float c = cos(angleInRadians);\n \t\n return mat4( \n 1, 0, 0, 0,\n 0, c, s, 0,\n 0, -s, c, 0,\n 0, 0, 0, 1); \n}\n\nmat4 rotY(float angleInRadians) {\n float s = sin(angleInRadians);\n float c = cos(angleInRadians);\n \t\n return mat4( \n c, 0,-s, 0,\n 0, 1, 0, 0,\n s, 0, c, 0,\n 0, 0, 0, 1); \n}\n\nmat4 rotZ(float angleInRadians) {\n float s = sin(angleInRadians);\n float c = cos(angleInRadians);\n \t\n return mat4( \n c,-s, 0, 0, \n s, c, 0, 0,\n 0, 0, 1, 0,\n 0, 0, 0, 1); \n}\n\nmat4 trans(vec3 trans) {\n return mat4(\n 1, 0, 0, 0,\n 0, 1, 0, 0,\n 0, 0, 1, 0,\n trans, 1);\n}\n\nmat4 ident() {\n return mat4(\n 1, 0, 0, 0,\n 0, 1, 0, 0,\n 0, 0, 1, 0,\n 0, 0, 0, 1);\n}\n\nmat4 scale(vec3 s) {\n return mat4(\n s[0], 0, 0, 0,\n 0, s[1], 0, 0,\n 0, 0, s[2], 0,\n 0, 0, 0, 1);\n}\n\nmat4 uniformScale(float s) {\n return mat4(\n s, 0, 0, 0,\n 0, s, 0, 0,\n 0, 0, s, 0,\n 0, 0, 0, 1);\n}\n\nmat4 persp(float fov, float aspect, float zNear, float zFar) {\n float f = tan(PI * 0.5 - 0.5 * fov);\n float rangeInv = 1.0 / (zNear - zFar);\n\n return mat4(\n f / aspect, 0, 0, 0,\n 0, f, 0, 0,\n 0, 0, (zNear + zFar) * rangeInv, -1,\n 0, 0, zNear * zFar * rangeInv * 2., 0);\n}\n\nmat4 trInv(mat4 m) {\n mat3 i = mat3(\n m[0][0], m[1][0], m[2][0], \n m[0][1], m[1][1], m[2][1], \n m[0][2], m[1][2], m[2][2]);\n vec3 t = -i * m[3].xyz;\n \n return mat4(\n i[0], t[0], \n i[1], t[1],\n i[2], t[2],\n 0, 0, 0, 1);\n}\n\nmat4 transpose(mat4 m) {\n return mat4(\n m[0][0], m[1][0], m[2][0], m[3][0], \n m[0][1], m[1][1], m[2][1], m[3][1],\n m[0][2], m[1][2], m[2][2], m[3][2],\n m[0][3], m[1][3], m[2][3], m[3][3]);\n}\n\nmat4 lookAt(vec3 eye, vec3 target, vec3 up) {\n vec3 zAxis = normalize(eye - target);\n vec3 xAxis = normalize(cross(up, zAxis));\n vec3 yAxis = cross(zAxis, xAxis);\n\n return mat4(\n xAxis, 0,\n yAxis, 0,\n zAxis, 0,\n eye, 1);\n}\n\nmat4 inverse(mat4 m) {\n float\n a00 = m[0][0], a01 = m[0][1], a02 = m[0][2], a03 = m[0][3],\n a10 = m[1][0], a11 = m[1][1], a12 = m[1][2], a13 = m[1][3],\n a20 = m[2][0], a21 = m[2][1], a22 = m[2][2], a23 = m[2][3],\n a30 = m[3][0], a31 = m[3][1], a32 = m[3][2], a33 = m[3][3],\n\n b00 = a00 * a11 - a01 * a10,\n b01 = a00 * a12 - a02 * a10,\n b02 = a00 * a13 - a03 * a10,\n b03 = a01 * a12 - a02 * a11,\n b04 = a01 * a13 - a03 * a11,\n b05 = a02 * a13 - a03 * a12,\n b06 = a20 * a31 - a21 * a30,\n b07 = a20 * a32 - a22 * a30,\n b08 = a20 * a33 - a23 * a30,\n b09 = a21 * a32 - a22 * a31,\n b10 = a21 * a33 - a23 * a31,\n b11 = a22 * a33 - a23 * a32,\n\n det = b00 * b11 - b01 * b10 + b02 * b09 + b03 * b08 - b04 * b07 + b05 * b06;\n\n return mat4(\n a11 * b11 - a12 * b10 + a13 * b09,\n a02 * b10 - a01 * b11 - a03 * b09,\n a31 * b05 - a32 * b04 + a33 * b03,\n a22 * b04 - a21 * b05 - a23 * b03,\n a12 * b08 - a10 * b11 - a13 * b07,\n a00 * b11 - a02 * b08 + a03 * b07,\n a32 * b02 - a30 * b05 - a33 * b01,\n a20 * b05 - a22 * b02 + a23 * b01,\n a10 * b10 - a11 * b08 + a13 * b06,\n a01 * b08 - a00 * b10 - a03 * b06,\n a30 * b04 - a31 * b02 + a33 * b00,\n a21 * b02 - a20 * b04 - a23 * b00,\n a11 * b07 - a10 * b09 - a12 * b06,\n a00 * b09 - a01 * b07 + a02 * b06,\n a31 * b01 - a30 * b03 - a32 * b00,\n a20 * b03 - a21 * b01 + a22 * b00) / det;\n}\n\nmat4 cameraLookAt(vec3 eye, vec3 target, vec3 up) {\n #if 1\n return inverse(lookAt(eye, target, up));\n #else\n vec3 zAxis = normalize(target - eye);\n vec3 xAxis = normalize(cross(up, zAxis));\n vec3 yAxis = cross(zAxis, xAxis);\n\n return mat4(\n xAxis, 0,\n yAxis, 0,\n zAxis, 0,\n -dot(xAxis, eye), -dot(yAxis, eye), -dot(zAxis, eye), 1); \n #endif\n \n}\n\n\n\n// hash function from https://www.shadertoy.com/view/4djSRW\nfloat hash(float p) {\n\tvec2 p2 = fract(vec2(p * 5.3983, p * 5.4427));\n p2 += dot(p2.yx, p2.xy + vec2(21.5351, 14.3137));\n\treturn fract(p2.x * p2.y * 95.4337);\n}\n\n// times 2 minus 1\nfloat t2m1(float v) {\n return v * 2. - 1.;\n}\n\n// times .5 plus .5\nfloat t5p5(float v) {\n return v * 0.5 + 0.5;\n}\n\nfloat inv(float v) {\n return 1. - v;\n}\n\n\n#define NUM_EDGE_POINTS_PER_CIRCLE 100.\n#define NUM_POINTS_PER_DIVISION (NUM_EDGE_POINTS_PER_CIRCLE * 6.)\n#define NUM_POINTS_PER_CIRCLE (NUM_SUBDIVISIONS_PER_CIRCLE * NUM_POINTS_PER_DIVISION) \nvoid getCirclePoint(const float id, const float inner, const float start, const float end, out vec3 pos, out vec4 uvf, out float snd) {\n float NUM_SUBDIVISIONS_PER_CIRCLE = floor(vertexCount / NUM_POINTS_PER_DIVISION);\n float edgeId = mod(id, NUM_POINTS_PER_DIVISION);\n float ux = floor(edgeId / 6.) + mod(edgeId, 2.);\n float vy = mod(floor(id / 2.) + floor(id / 3.), 2.); // change that 3. for cool fx\n float sub = floor(id / NUM_POINTS_PER_DIVISION);\n float subV = sub / (NUM_SUBDIVISIONS_PER_CIRCLE - 1.);\n float level = subV + vy / (NUM_SUBDIVISIONS_PER_CIRCLE - 1.);\n float u = ux / NUM_EDGE_POINTS_PER_CIRCLE;\n float v = 1.;//mix(inner, 1., level);\n float ringId = sub + vy;\n float ringV = ringId / NUM_SUBDIVISIONS_PER_CIRCLE;\n float numRings = vertexCount / NUM_SUBDIVISIONS_PER_CIRCLE;\n float a = mix(start, end, u) * PI * 2. + PI * 0.0;\n float skew = 1. - step(0.5, mod(ringId - 2., 3.));\n float su = fract(abs(u * 2. - 1.) + time * 0.1);\n \n a += 1. / NUM_EDGE_POINTS_PER_CIRCLE * PI * 2.;// * 20. * sin(time * 1.) + snd * 1.5;\n float s = sin(a);\n float c = cos(a);\n float z = mix(inner, 2., level) - vy / NUM_SUBDIVISIONS_PER_CIRCLE * 0.;\n float x = c * v * z;\n float y = s * v * z;\n pos = vec3(x, y, 0.); \n uvf = vec4(floor(edgeId / 6.) / NUM_EDGE_POINTS_PER_CIRCLE, subV, floor(id / 6.), sub);\n}\n\nfloat goop(float t) {\n return sin(t) + sin(t * 0.27) + sin(t * 0.13) + sin(t * 0.73);\n}\n\nfloat modStep(float count, float steps) {\n return mod(count, steps) / steps;\n}\n\n\nvoid main() {\n float numQuads = floor(vertexCount / 6.);\n float around = 180.;\n float down = numQuads / around;\n float quadId = floor(vertexId / 6.);\n \n float qx = mod(quadId, around);\n float qy = floor(quadId / around);\n \n // 0--1 3\n // | / /|\n // |/ / |\n // 2 4--5\n //\n // 0 1 0 1 0 1\n // 0 0 1 0 1 1\n \n float edgeId = mod(vertexId, 6.);\n float ux = mod(edgeId, 2.);\n float vy = mod(floor(edgeId / 2.) + floor(edgeId / 3.), 2.); \n \n float qu = (qx + ux) / around;\n float qv = (qy + vy) / down;\n \n float r = sin(qv * PI);\n float x = cos(qu * PI * 2.) * r;\n float z = sin(qu * PI * 2.) * r;\n \n vec3 pos = vec3(x, cos(qv * PI), z);\n vec3 nrm = vec3(\n cos((qx + .5) / around * PI * 2.),\n cos((qy + .5) / down * PI),\n sin((qx + .5) / around * PI * 2.)\n );\n \n float tm = time * 1.1;\n float rd = mix(2., 3.5, t5p5(sin(time * 0.11)));\n mat4 mat = persp(PI * 0.25, resolution.x / resolution.y, 0.1, 100.);\n vec3 eye = vec3(cos(tm) * rd, sin(tm * 0.9) * .0 + 0., sin(tm) * rd);\n vec3 target = vec3(0);\n vec3 up = vec3(0,sin(tm),cos(tm));\n \n float s = texture2D(sound, vec2(mix(0.1, .25, abs(qu * 2. - 1.)), mix(0., .12, qv))).a;\n \n mat *= cameraLookAt(eye, target, up); \n mat *= uniformScale(mix(0.5, 2.5, pow(s + .15, 5.)));\n \n gl_Position = mat * vec4(pos, 1);\n gl_PointSize = 4.;\n\n float odd = mod(floor(quadId / 2.), 2.);\n float hue = time * .1 +s * .15;\n float sat = mix(0., 3., pow(s, 5.));\n float val = mix(0.1, 1., pow(s + .4, 15.));\n v_color = vec4(hsv2rgb(vec3(hue, sat, val)), 1.);\n \n \n v_color.rgb *= v_color.a;\n \n \n \n}"},"revisionId":"8TbPNzy7gAKpNs7Hj","revisionUrl":"https://www.vertexshaderart.com/art/A8Zc7NFQdTdeKQimv/revision/8TbPNzy7gAKpNs7Hj","artUrl":"https://www.vertexshaderart.com/art/undefined","origUrl":"https://www.vertexshaderart.com/art/7TrYkuK4aHzLqvZ7r"} -------------------------------------------------------------------------------- /examples/esm.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /examples/npm/.gitignore: -------------------------------------------------------------------------------- 1 | index-build.js 2 | package-lock.json 3 | -------------------------------------------------------------------------------- /examples/npm/README.md: -------------------------------------------------------------------------------- 1 | # html5bytebeat via npm 2 | 3 | Normally you'd add bytebeat.js to your project 4 | 5 | ```sh 6 | npm install bytebeat.js 7 | ``` 8 | 9 | and then setup your builder (webpack, rollup, parcel, etc..) 10 | so you should be able to import the library with 11 | 12 | ```js 13 | import ByteBeatNode from 'bytebeat.js'; 14 | ``` 15 | 16 | ## Working example 17 | 18 | Steps 19 | 20 | ```sh 21 | git clone https://github.com/greggman/html5bytebeat.git 22 | cd html5bytebeat/examples/npm 23 | npm i 24 | npm run build 25 | ``` 26 | 27 | This should build `index-build.js` via rollup. 28 | 29 | Then do 30 | 31 | ```sh 32 | npx servez 33 | ``` 34 | 35 | Then to go to [`http://localhost:8080`](http://localhost:8080) in your browser. 36 | 37 | -------------------------------------------------------------------------------- /examples/npm/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | HTML5ByteBeat via npm 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /examples/npm/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "html5bytebeat-via-npm", 3 | "version": "1.0.0", 4 | "description": "", 5 | "type": "module", 6 | "scripts": { 7 | "build": "rollup -c" 8 | }, 9 | "dependencies": { 10 | "bytebeat.js": "^2.0.0" 11 | }, 12 | "devDependencies": { 13 | "@rollup/plugin-node-resolve": "^15.3.0", 14 | "rollup": "^4.24.0" 15 | }, 16 | "keywords": [], 17 | "author": "", 18 | "license": "ISC" 19 | } 20 | -------------------------------------------------------------------------------- /examples/npm/rollup.config.js: -------------------------------------------------------------------------------- 1 | import resolve from '@rollup/plugin-node-resolve'; 2 | 3 | export default [ 4 | { 5 | input: 'src/index.js', 6 | plugins: [ 7 | resolve({ 8 | modulesOnly: true, 9 | }), 10 | ], 11 | output: [ 12 | { 13 | format: 'iife', 14 | file: 'index-build.js', 15 | }, 16 | ], 17 | }, 18 | ]; 19 | -------------------------------------------------------------------------------- /examples/npm/src/index.js: -------------------------------------------------------------------------------- 1 | import ByteBeatNode from 'bytebeat.js'; 2 | 3 | document.querySelector('button').addEventListener('click', playPause); 4 | 5 | let g_context; 6 | let g_byteBeatNode; 7 | let g_playing = false; 8 | 9 | async function init() { 10 | g_context = new AudioContext(); 11 | g_context.resume(); // needed for safari 12 | await ByteBeatNode.setup(g_context); 13 | g_byteBeatNode = new ByteBeatNode(g_context); 14 | g_byteBeatNode.setType(ByteBeatNode.Type.byteBeat); 15 | g_byteBeatNode.setExpressionType(ByteBeatNode.ExpressionType.infix); 16 | g_byteBeatNode.setDesiredSampleRate(8000); 17 | await g_byteBeatNode.setExpressions(['((t >> 10) & 42) * t']); 18 | } 19 | 20 | async function playPause() { 21 | if (!g_context) { 22 | await init(); 23 | } 24 | if (!g_playing) { 25 | g_playing = true; 26 | g_byteBeatNode.connect(g_context.destination); 27 | } else { 28 | g_playing = false; 29 | g_byteBeatNode.disconnect(); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /examples/umd.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /html5bytebeat.html: -------------------------------------------------------------------------------- 1 |  31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | HTML5 Bytebeat 57 | 58 | 59 | 364 | 365 | 366 |
367 |
368 |
369 | 370 |
371 |
372 |
373 | 374 | 375 |
376 |
377 | 381 |
382 | 383 |
384 |
385 |
386 |
387 | 388 |
389 |
390 |
seconds:
391 |
392 |
393 |
394 |
395 |
396 | 402 |
403 |
loading...
404 |
405 | 406 | 407 | 408 | -------------------------------------------------------------------------------- /html5bytebeat.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/greggman/html5bytebeat/a266d2f6f212cb7709ccfec2dbdd273d6d6ac6c2/html5bytebeat.png -------------------------------------------------------------------------------- /icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/greggman/html5bytebeat/a266d2f6f212cb7709ccfec2dbdd273d6d6ac6c2/icon.png -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | HTML5 Bytebeat - About 6 | 9 | 10 | 11 | HTML5 ByteBeat 12 | 13 | 14 | -------------------------------------------------------------------------------- /js/lzma.js: -------------------------------------------------------------------------------- 1 | /// This code is licensed under the MIT License. See LICENSE for more details. 2 | 3 | /// Does the environment support web workers? If not, let's fake it. 4 | if (!Worker) { 5 | ///NOTE: IE8 needs onmessage to be created first, IE9 cannot, IE7- do not care. 6 | /*@cc_on 7 | /// Is this IE8-? 8 | @if (@_jscript_version < 9) 9 | var onmessage = function () {}; 10 | @end 11 | @*/ 12 | 13 | /// If this were a regular function statement, IE9 would run it first and therefore make the Worker variable truthy because of hoisting. 14 | var Worker = function(script) { 15 | var global_var, 16 | return_object = {}; 17 | 18 | /// Determine the global variable (it's called "window" in browsers, "global" in Node.js). 19 | if (typeof window !== "undefined") { 20 | global_var = window; 21 | } else if (global) { 22 | global_var = global; 23 | } 24 | 25 | /// Is the environment is browser? 26 | /// If not, create a require() function, if it doesn't have one. 27 | if (global_var.document && !global_var.require) { 28 | global_var.require = function (path) { 29 | var script_tag = document.createElement("script"); 30 | script_tag.type ="text/javascript"; 31 | script_tag.src = path; 32 | document.getElementsByTagName('head')[0].appendChild(script_tag); 33 | }; 34 | } 35 | 36 | /// Dummy onmessage() function. 37 | return_object.onmessage = function () {}; 38 | 39 | /// This is the function that the main script calls to post a message to the "worker." 40 | return_object.postMessage = function (message) { 41 | /// Delay the call just in case the "worker" script has not had time to load. 42 | setTimeout(function () { 43 | /// Call the global onmessage() created by the "worker." 44 | ///NOTE: Wrap the message in an object. 45 | global_var.onmessage({data: message}); 46 | }, 10); 47 | }; 48 | 49 | /// Create a global postMessage() function for the "worker" to call. 50 | global_var.postMessage = function (e) { 51 | ///NOTE: Wrap the message in an object. 52 | ///TODO: Add more properties. 53 | return_object.onmessage({data: e, type: "message"}); 54 | }; 55 | 56 | require(script); 57 | 58 | return return_object; 59 | }; 60 | } 61 | 62 | 63 | ///NOTE: The "this" keyword is the global context ("window" variable) if loaded via a 9 | 85 | -------------------------------------------------------------------------------- /test/test.html: -------------------------------------------------------------------------------- 1 | 2 | 33 | 34 | --------------------------------------------------------------------------------