├── .babelrc ├── .eslintrc.js ├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── electron ├── glitch.icns ├── glitch.ico ├── index.js └── package.json ├── package.json ├── src ├── actions.js ├── audio.js ├── colors.js ├── examples.js ├── favicon.png ├── fonts │ └── robotomono │ │ └── v4 │ │ ├── N4duVc9C58uwPiY8_59Fz-jkDdvhIIFj_YMdgqpnSB0.woff2 │ │ ├── N4duVc9C58uwPiY8_59Fz2hQUTDJGru-0vvUpABgH8I.woff2 │ │ ├── N4duVc9C58uwPiY8_59Fz4lIZu-HDpmDIZMigmsroc4.woff2 │ │ ├── N4duVc9C58uwPiY8_59Fz56iIh_FvlUHQwED9Yt5Kbw.woff2 │ │ ├── N4duVc9C58uwPiY8_59FzwalQocB-__pDVGhF3uS2Ks.woff2 │ │ ├── N4duVc9C58uwPiY8_59FzyFaMxiho_5XQnyRZzQsrZs.woff2 │ │ ├── N4duVc9C58uwPiY8_59Fzy_vZmeiCMnoWNN9rHBYaTc.woff2 │ │ ├── hMqPNLsu_dywMa4C_DEpY0bcKLIaa1LC45dFaAfauRA.woff2 │ │ ├── hMqPNLsu_dywMa4C_DEpY2o_sUJ8uO4YLWRInS22T3Y.woff2 │ │ ├── hMqPNLsu_dywMa4C_DEpY44P5ICox8Kq3LLUNMylGO4.woff2 │ │ ├── hMqPNLsu_dywMa4C_DEpY76up8jxqWt8HVA3mDhkV_0.woff2 │ │ ├── hMqPNLsu_dywMa4C_DEpYyYE0-AqJ3nfInTTiDXDjU4.woff2 │ │ ├── hMqPNLsu_dywMa4C_DEpYzTOQ_MqJVwkKsUn0wKzc2I.woff2 │ │ └── hMqPNLsu_dywMa4C_DEpYzUj_cnvWIuuBMVgbX098Mw.woff2 ├── glitch.js ├── glitch180x180.png ├── glitch192x192.png ├── glitchFuncs.js ├── glitchSamples.js ├── index.html ├── index.js ├── jsx │ ├── App.js │ ├── Editor.js │ ├── Help.js │ ├── Layout.js │ ├── Library.js │ ├── Toolbar.js │ └── Visualizer.js ├── reducers.js ├── roboto.css ├── samples │ ├── bd.wav │ ├── cb.wav │ ├── cl.wav │ ├── hh.wav │ ├── mc.wav │ ├── mt.wav │ ├── oh.wav │ ├── rs.wav │ └── sn.wav ├── save.js └── styles.css ├── test ├── glitchFuncsTest.js └── mockAudio.js └── webpack.config.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["es2015"] 3 | } 4 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "extends": "airbnb", 3 | "plugins": [ 4 | "react" 5 | ] 6 | }; -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | 6 | # Runtime data 7 | pids 8 | *.pid 9 | *.seed 10 | 11 | # Directory for instrumented libs generated by jscoverage/JSCover 12 | lib-cov 13 | 14 | # Coverage directory used by tools like istanbul 15 | coverage 16 | 17 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 18 | .grunt 19 | 20 | # node-waf configuration 21 | .lock-wscript 22 | 23 | # Compiled binary addons (http://nodejs.org/api/addons.html) 24 | build/Release 25 | 26 | # Dependency directory 27 | node_modules 28 | 29 | # Optional npm cache directory 30 | .npm 31 | 32 | # Optional REPL history 33 | .node_repl_history 34 | 35 | # Build artifacts directory (bundle.js, index.html, ...) 36 | build/ 37 | electron/pkg 38 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "5" 4 | - "4" 5 | addons: 6 | apt: 7 | packages: 8 | - wine 9 | after_success: 10 | - | 11 | if test "$TRAVIS_TAG" ; then 12 | npm run build && 13 | cd electron && 14 | npm install && 15 | npm run build && 16 | npm run pkg && 17 | cd pkg && 18 | zip -q -r ../../glitch-linux.zip Glitch-linux-x64 && 19 | zip -q -r ../../glitch-windows.zip Glitch-win32-ia32 && 20 | zip -q -r ../../glitch-macosx.zip Glitch-darwin-x64 && 21 | cd ../.. 22 | ls -l 23 | fi 24 | deploy: 25 | provider: releases 26 | api_key: $GITHUB_ACCESS_TOKEN 27 | file: 28 | - glitch-linux.zip 29 | - glitch-windows.zip 30 | - glitch-macosx.zip 31 | skip_cleanup: true 32 | on: 33 | tags: true 34 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 NaiveSound 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # The Original Glitch 2 | 3 | This is the original Glitch, a minimal synthesizer and composer for algorithmic music. It's been fully written in ES6 + React, including the expression evaluation engine. The performance became a problem, on an average PC one can't code a song of more than ~20-30 lines of Gltich code. 4 | 5 | That's why Glitch has been rewritten. 6 | 7 | ## The latest Glitch 8 | 9 | The new (and the only active) Glitch is available at https://github.com/naivesound/glitch and a live web app is at http://naivesound.com/glitch 10 | 11 | Original web version of Glitch is still available at http://naivesound.com/glitch-orig if you want to compare. 12 | 13 | ## Reference 14 | 15 | Operators: 16 | 17 | - + - * / % \*\* ( ) 18 | - << >> | & ^ 19 | - < > <= >= == != && || 20 | - `=` 21 | - `,` 22 | 23 | Sequencers: 24 | 25 | - loop(bpm, ...) 26 | - seq(bpm, ...) 27 | - a(index, ...) 28 | 29 | Instruments: 30 | 31 | - sin(hz) 32 | - tri(hz) 33 | - saw(hz) 34 | - sqr(hz, [width]) 35 | - fm(hz, [m1, a1, m2, a2, m3, a3]) 36 | 37 | Effects: 38 | 39 | - env(releaseTime, x) 40 | - env(attackTime, [interval, gain]..., x) 41 | - lpf(x, frequency) 42 | - mix(...) 43 | 44 | Utils: 45 | 46 | - scale(index, mode) 47 | - hz(note) 48 | - r(max) 49 | - l(x) 50 | - s(phase) 51 | -------------------------------------------------------------------------------- /electron/glitch.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/naivesound/glitch-orig/a456917b36dc48d4dcfe37277f33bc6de1017a0d/electron/glitch.icns -------------------------------------------------------------------------------- /electron/glitch.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/naivesound/glitch-orig/a456917b36dc48d4dcfe37277f33bc6de1017a0d/electron/glitch.ico -------------------------------------------------------------------------------- /electron/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const electron = require('electron'); 4 | const path = require('path'); 5 | const fs = require('fs'); 6 | 7 | const app = electron.app; 8 | const BrowserWindow = electron.BrowserWindow; 9 | 10 | let mainWindow; 11 | 12 | function injectPlugins() { 13 | mainWindow.webContents.executeJavaScript('window.userFuncs = {};'); 14 | try { 15 | var plugins = fs.readdirSync('plugins').filter(function(name) { 16 | return name.endsWith('.js'); 17 | }).forEach(function(plugin) { 18 | var code = fs.readFileSync(path.join('plugins', plugin), 'utf8'); 19 | mainWindow.webContents.executeJavaScript(`;(function(){${code}})();`); 20 | }); 21 | } catch (e) {} 22 | } 23 | 24 | function injectSamples() { 25 | mainWindow.webContents.executeJavaScript('var sampleCache = {};'); 26 | mainWindow.webContents.executeJavaScript(loadSamples.toString()); 27 | try { 28 | fs.readdirSync('samples').forEach(function(sample) { 29 | var wavPath = path.join('samples', sample); 30 | if (sample.endsWith('.wav')) { 31 | var name = JSON.stringify(sample.replace(/\.wav$/, '')); 32 | mainWindow.webContents.executeJavaScript(`loadSamples(${name}, [${JSON.stringify(wavPath)}]);`); 33 | } else { 34 | var name = JSON.stringify(sample); 35 | var variants = fs.readdirSync(wavPath).map(function(f) { 36 | return path.join(wavPath, f); 37 | }); 38 | mainWindow.webContents.executeJavaScript(`loadSamples(${name}, ${JSON.stringify(variants)});`); 39 | } 40 | }); 41 | } catch (e) {} 42 | } 43 | 44 | function loadSamples(name, paths) { 45 | var fs = require('electron').remote.require('fs'); 46 | window.userFuncs[name] = function(args) { 47 | var index = (args[0] ? args[0]() : 0); 48 | var volume = (args[1] ? args[1]() : 1); 49 | let len = paths.length; 50 | if (!isNaN(index) && !isNaN(volume)) { 51 | let sample = paths[(((index|0)%len)+len)%len]; 52 | sampleCache[sample] = sampleCache[sample] || fs.readFileSync(sample); 53 | if (this.i * 2 + 0x80 + 1 < sampleCache[sample].length) { 54 | let v = sampleCache[sample].readInt16LE(0x80 + this.i * 2); 55 | let x = v / 0x7fff; 56 | this.i++; 57 | return x * volume * 127 + 128; 58 | } else { 59 | return NaN 60 | } 61 | } 62 | this.i = 0; 63 | return NaN 64 | }; 65 | } 66 | 67 | function createWindow () { 68 | let iconPath = __dirname + '/build/glitch192x192.png'; 69 | mainWindow = new BrowserWindow({ 70 | width: 640, 71 | height: 480, 72 | icon: iconPath, 73 | backgroundColor: '#333333', 74 | }); 75 | 76 | injectPlugins(); 77 | injectSamples(); 78 | mainWindow.loadURL('file://' + __dirname + '/build/index.html'); 79 | mainWindow.on('closed', function() { 80 | mainWindow = null; 81 | }); 82 | } 83 | 84 | app.on('ready', createWindow); 85 | 86 | app.on('window-all-closed', function () { 87 | if (process.platform !== 'darwin') { 88 | app.quit(); 89 | } 90 | }); 91 | 92 | app.on('activate', function () { 93 | if (mainWindow === null) { 94 | createWindow(); 95 | } 96 | }); 97 | -------------------------------------------------------------------------------- /electron/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "glitch", 3 | "productName": "Glitch", 4 | "version": "0.1.0", 5 | "description": "Algorithmic music synthesizer", 6 | "main": "index.js", 7 | "scripts": { 8 | "start": "npm run build && npm run launch", 9 | "launch": "electron index.js", 10 | "build": "NODE_ENV='production' webpack -p --config ../webpack.config.js --output-path=build", 11 | "pkg-linux": "electron-packager ./ --out=pkg --overwrite --platform=linux --arch=x64", 12 | "pkg-windows": "electron-packager ./ --out=pkg --icon=glitch.ico --overwrite --platform=win32 --arch=ia32", 13 | "pkg-darwin": "electron-packager ./ --out=pkg --icon=glitch.icns --overwrite --platform=darwin --arch=x64", 14 | "pkg": "npm run pkg-linux && npm run pkg-windows && npm run pkg-darwin" 15 | }, 16 | "author": "", 17 | "license": "MIT", 18 | "devDependencies": { 19 | "electron-packager": "^7.0.2", 20 | "electron-prebuilt": "^1.2.0", 21 | "webpack": "^1.13.1" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "scripts": { 3 | "watch": "webpack -w -d", 4 | "test": "mocha --compilers js:babel-core/register", 5 | "build": "webpack -p", 6 | "lint": "eslint src/**/*.js" 7 | }, 8 | "devDependencies": { 9 | "babel-core": "^6.9.1", 10 | "babel-loader": "^6.2.4", 11 | "babel-preset-es2015": "^6.9.0", 12 | "babel-preset-react": "^6.5.0", 13 | "base64-loader": "^1.0.0", 14 | "css-loader": "^0.23.1", 15 | "es6-promise": "^3.2.1", 16 | "eslint": "^2.11.1", 17 | "eslint-config-airbnb": "^9.0.1", 18 | "eslint-plugin-import": "^1.8.1", 19 | "eslint-plugin-jsx-a11y": "^1.3.0", 20 | "eslint-plugin-react": "^5.1.1", 21 | "file-loader": "^0.8.5", 22 | "mocha": "^2.5.3", 23 | "style-loader": "^0.13.1", 24 | "url-loader": "^0.5.7", 25 | "webpack": "^1.13.1" 26 | }, 27 | "dependencies": { 28 | "expr": "github:naivesound/expr-js#1.0.6", 29 | "font-awesome": "^4.6.3", 30 | "react": "^15.1.0", 31 | "react-dom": "^15.1.0", 32 | "react-redux": "^4.4.5", 33 | "redux": "^3.5.2" 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/actions.js: -------------------------------------------------------------------------------- 1 | // Action types 2 | export const SET_EXPR = 'SET_EXPR'; 3 | export const ERROR = 'ERROR'; 4 | export const PLAY = 'PLAY'; 5 | export const STOP = 'STOP'; 6 | export const EXPORT_WAV = 'EXPORT_WAV'; 7 | export const NAVIGATE = 'NAVIGATE'; 8 | 9 | function makeActionCreator(type, ...argNames) { 10 | return function actionCreator(...args) { 11 | const action = { type }; 12 | argNames.forEach((arg, index) => { 13 | action[argNames[index]] = args[index]; 14 | }); 15 | return action; 16 | }; 17 | } 18 | 19 | export const setExpr = makeActionCreator(SET_EXPR, 'expr'); 20 | export const error = makeActionCreator(ERROR, 'error'); 21 | export const play = makeActionCreator(PLAY); 22 | export const stop = makeActionCreator(STOP); 23 | export const exportWav = makeActionCreator(EXPORT_WAV); 24 | export const navigate = makeActionCreator(NAVIGATE, 'tab'); 25 | -------------------------------------------------------------------------------- /src/audio.js: -------------------------------------------------------------------------------- 1 | const audioContext = new AudioContext(); 2 | const AUDIO_BUFFER_SIZE = 8192; 3 | 4 | export const sampleRate = audioContext.sampleRate; 5 | 6 | const pcmNode = audioContext.createScriptProcessor(AUDIO_BUFFER_SIZE, 0, 1); 7 | 8 | export const analyser = audioContext.createAnalyser(); 9 | 10 | analyser.fftSize = 2048; 11 | analyser.smoothingTimeConstant = 0; 12 | analyser.connect(audioContext.destination); 13 | pcmNode.onaudioprocess = undefined; 14 | 15 | export function play(audioCallback) { 16 | if (!pcmNode.onaudioprocess) { 17 | pcmNode.connect(analyser); 18 | pcmNode.onaudioprocess = audioCallback; 19 | } 20 | } 21 | 22 | export function stop() { 23 | if (pcmNode.onaudioprocess) { 24 | pcmNode.disconnect(); 25 | pcmNode.onaudioprocess = undefined; 26 | } 27 | } 28 | 29 | -------------------------------------------------------------------------------- /src/colors.js: -------------------------------------------------------------------------------- 1 | export const YELLOW = '#ffc107'; 2 | export const GRAY = '#333333'; 3 | export const WHITE = '#ffffff'; 4 | export const PINK = '#e91e63'; 5 | export const GREEN = '#cddc39'; 6 | -------------------------------------------------------------------------------- /src/examples.js: -------------------------------------------------------------------------------- 1 | export default [ 2 | { f: 't', tags: [] }, 3 | { f: 't&t>>8', tags: [] }, 4 | { f: 't*(42&t>>10)', tags: [] }, 5 | { f: 't|t%255|t%257', tags: [] }, 6 | { f: 't*(t>>9|t>>13)&16', tags: [] }, 7 | { f: 't>>6&1&&t>>5||-t>>4', tags: [] }, 8 | { f: '(t&t>>12)*(t>>4|t>>8)', tags: [] }, 9 | { f: '(t*5&t>>7)|(t*3&t>>10)', tags: [] }, 10 | { f: '(t*(t>>5|t>>8))>>(t>>16)', tags: [] }, 11 | { f: '(t>>13|t%24)&(t>>7|t%19)', tags: [] }, 12 | { f: 't*((t>>12|t>>8)&63&t>>4)', tags: [] }, 13 | { f: 't*5&(t>>7)|t*3&(t*4>>10)', tags: [] }, 14 | { f: '(t*((t>>9|t>>13)&15))&129', tags: [] }, 15 | { f: '(t&t%255)-(t*3&t>>13&t>>6)', tags: [] }, 16 | { f: '(t&t>>12)*(t>>4|t>>8)^t>>6', tags: [] }, 17 | { f: 't*(((t>>9)^((t>>9)-1)^1)%13)', tags: [] }, 18 | { f: 't*(51864>>(t>>9&14)&15)|t>>8', tags: [] }, 19 | { f: '(^t/100|(t*3))^(t*3&(t>>5))&t', tags: [] }, 20 | { f: '(t/8)>>(t>>9)*t/((t>>14&3)+4)', tags: [] }, 21 | { f: 't&(s(t&t&3)*t>>5)/(t>>3&t>>6)', tags: [] }, 22 | { f: '((t>>1%128)+20)*3*t>>14*t>>18 ', tags: [] }, 23 | { f: '(t|(t>>9|t>>7))*t&(t>>11|t>>9)', tags: [] }, 24 | { f: '(t*(t>>8+t>>9)*100)+s(t)', tags: [] }, // TODO 25 | { f: '(t*9&t>>4|t*5&t>>7|t*3&t/1024)-1', tags: [] }, 26 | { f: 't*(((t>>9)|(t>>13))&(25&(t>>6)))', tags: [] }, 27 | { f: 't*(t^t+(t>>15|1)^(t-1280^t)>>10)', tags: [] }, 28 | { f: '(t>>7|t|t>>6)*10+4*(t&t>>13|t>>6)', tags: [] }, 29 | { f: 't*(((t>>11)&(t>>8))&(123&(t>>3)))', tags: [] }, 30 | { f: '(t>>6|t|t>>(t>>16))*10+((t>>11)&7)', tags: [] }, 31 | { f: 't*(t>>((t>>9)|(t>>8))&(63&(t>>4)))', tags: [] }, 32 | { f: '(t>>1)*(3134381729>>(t>>13)&3)|t>>5', tags: [] }, 33 | { f: '(t>>(t&7))|(t<<(t&42))|(t>>7)|(t<<5)', tags: [] }, 34 | { f: '(t>>4)*(13&(2291706249>>(t>>11&30)))', tags: [] }, 35 | { f: '(t>>7|t%45)&(t>>8|t%35)&(t>>11|t%20)', tags: [] }, 36 | { f: '(t>>6|t<<1)+(t>>5|t<<3|t>>3)|t>>2|t<<1', tags: [] }, 37 | { f: '((t*(t>>8|t>>9)&46&t>>8))^(t&t>>13|t>>6)', tags: [] }, 38 | { f: '(t>>5)|(t<<4)|((t&1023)^1981)|((t-67)>>4)', tags: [] }, 39 | { f: 't+(t&t^t>>6)-t*((t>>9)&(t%16&72||6)&t>>9)', tags: [] }, 40 | { f: 't>>4|t&(t>>5)/(t>>7-(t>>15)&-t>>7-(t>>15))', tags: [] }, 41 | // { f: 't*(1+"4451"[t>>13&3]/10)&t>>9+(t*0.003&3)', tags: [] }, 42 | { f: '((t>>5&t)-(t>>5)+(t>>5&t))+(t*((t>>14)&14))', tags: [] }, 43 | { f: 't*(t/256)-t*(t/255)+t*(t>>5|t>>6|t<<2&t>>1)', tags: [] }, 44 | { f: 't>>4|t&((t>>5)/(t>>7-(t>>15)&-t>>7-(t>>15)))', tags: [] }, 45 | { f: '(t*((3+(1^t>>10&5))*(5+(3&t>>14))))>>(t>>8&3)', tags: [] }, 46 | { f: '(^t>>2)*((127&t*(7&t>>10))<(245&t*(2+(5&t>>14))))', tags: [] }, 47 | { f: '(t+(t>>2)|(t>>5))+(t>>3)|((t>>13)|(t>>7)|(t>>11))', tags: [] }, 48 | { f: 't*(((t>>9)&10)|((t>>11)&24)^((t>>10)&15&(t>>15)))', tags: [] }, 49 | { f: 't*(t>>8*((t>>15)|(t>>8))&(20|(t>>19)*5>>t|(t>>3)))', tags: [] }, 50 | { f: '((t&((t>>23)))+(t|(t>>2)))&(t>>3)|(t>>5)&(t*(t>>7))', tags: [] }, 51 | { f: '(t>>4)|(t%10)|(((t%101)|(t>>14))&((t>>7)|(t*t%17)))', tags: [] }, 52 | { f: '((t&((t>>5)))+(t|((t>>7))))&(t>>6)|(t>>5)&(t*(t>>7))', tags: [] }, 53 | { f: '(((((t*((t>>9|t>>13)&15))&255/15)*9)%(1<<7))<<2)%6<<4', tags: [] }, 54 | { f: '((t%42)*(t>>4)|(357052961)-(t>>4))/(t>>16)^(t|(t>>4))', tags: [] }, 55 | { f: '(t/10000000*t*t+t)%127|t>>4|t>>5|t%127+(t>>16)|t', tags: [] }, // TODO? 56 | { f: 't*(t>>((t&4096)&&((t*t)/4096)||(t/4096)))|(t<<(t/256))|(t>>4)', tags: [] }, 57 | { f: 't*((3134974581>>((t>>12)&30)&3)*0.25*(372709>>((t>>16)&28)&3))', tags: [] }, 58 | { f: '((t&4096)&&((t*(t^t%255)|(t>>4))>>1)||(t>>3)|((t&8192)&&t<<2||t))', tags: [] }, 59 | { f: 't>>16|((t>>4)%16)|((t>>4)%192)|(t*t%64)|(t*t%96)|(t>>16)*(t|t>>5)', tags: [] }, 60 | { f: '((t&4096)&&((t*(t^t%255)|(t>>4))>>1)||((t>>3)|((t&8192)&&t<<2||t)))', tags: [] }, 61 | { f: '(t>>5)|(t>>4)|((t%42)*(t>>4)|(357052691)-(t>>4))/(t>>16)^(t|(t>>4))', tags: [] }, 62 | { f: '((-t&4095)*(255&t*(t&(t>>13)))>>12)+(127&t*(234&t>>8&t>>3)>>(3&t>>14))', tags: [] }, 63 | { f: '(t*t/256)&(t>>((t/1024)%16))^t%64*(828188282217>>(t>>9&30)&t%32)*t>>18', tags: [] }, 64 | { f: 't>>6^t&37|t+(t^t>>11)-t*((t%24&&2||6)&t>>11)^t<<1&(t&598&&t>>4||t>>10) ', tags: [] }, 65 | { f: '((t/2*(15&(591751328>>(t>>8&28))))|t/2>>(t>>11)^t>>12)+(t/16&t&24)', tags: [] }, // TODO 66 | { f: '(t%25-(t>>2|t*15|t%227)-t>>3)|((t>>5)&(t<<5)*1663|(t>>3)%1544)/(t%17|t%2048)', tags: [] }, 67 | { f: '((1-(((t+10)>>((t>>9)&((t>>14))))&(t>>4&-2)))*2)*' + 68 | '(((t>>10)^((t+((t>>6)&127))>>10))&1)*32+128', tags: [] }, 69 | { f: '((t>>4)*(13&(2291706249>>(t>>11&30)))&255)+' + 70 | '((((t>>9|(t>>2)|t>>8)*10+4*((t>>2)&t>>15|t>>8))&255)>>1)', tags: [] }, 71 | { f: '((t*(t>>12)&(201*t/100)&(199*t/100))&(t*(t>>14)&(t*301/100)&(t*399/100)))+' + 72 | '((t*(t>>16)&(t*202/100)&(t*198/100))-(t*(t>>17)&(t*302/100)&(t*298/100)))', tags: [] }, 73 | ]; 74 | 75 | -------------------------------------------------------------------------------- /src/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/naivesound/glitch-orig/a456917b36dc48d4dcfe37277f33bc6de1017a0d/src/favicon.png -------------------------------------------------------------------------------- /src/fonts/robotomono/v4/N4duVc9C58uwPiY8_59Fz-jkDdvhIIFj_YMdgqpnSB0.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/naivesound/glitch-orig/a456917b36dc48d4dcfe37277f33bc6de1017a0d/src/fonts/robotomono/v4/N4duVc9C58uwPiY8_59Fz-jkDdvhIIFj_YMdgqpnSB0.woff2 -------------------------------------------------------------------------------- /src/fonts/robotomono/v4/N4duVc9C58uwPiY8_59Fz2hQUTDJGru-0vvUpABgH8I.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/naivesound/glitch-orig/a456917b36dc48d4dcfe37277f33bc6de1017a0d/src/fonts/robotomono/v4/N4duVc9C58uwPiY8_59Fz2hQUTDJGru-0vvUpABgH8I.woff2 -------------------------------------------------------------------------------- /src/fonts/robotomono/v4/N4duVc9C58uwPiY8_59Fz4lIZu-HDpmDIZMigmsroc4.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/naivesound/glitch-orig/a456917b36dc48d4dcfe37277f33bc6de1017a0d/src/fonts/robotomono/v4/N4duVc9C58uwPiY8_59Fz4lIZu-HDpmDIZMigmsroc4.woff2 -------------------------------------------------------------------------------- /src/fonts/robotomono/v4/N4duVc9C58uwPiY8_59Fz56iIh_FvlUHQwED9Yt5Kbw.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/naivesound/glitch-orig/a456917b36dc48d4dcfe37277f33bc6de1017a0d/src/fonts/robotomono/v4/N4duVc9C58uwPiY8_59Fz56iIh_FvlUHQwED9Yt5Kbw.woff2 -------------------------------------------------------------------------------- /src/fonts/robotomono/v4/N4duVc9C58uwPiY8_59FzwalQocB-__pDVGhF3uS2Ks.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/naivesound/glitch-orig/a456917b36dc48d4dcfe37277f33bc6de1017a0d/src/fonts/robotomono/v4/N4duVc9C58uwPiY8_59FzwalQocB-__pDVGhF3uS2Ks.woff2 -------------------------------------------------------------------------------- /src/fonts/robotomono/v4/N4duVc9C58uwPiY8_59FzyFaMxiho_5XQnyRZzQsrZs.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/naivesound/glitch-orig/a456917b36dc48d4dcfe37277f33bc6de1017a0d/src/fonts/robotomono/v4/N4duVc9C58uwPiY8_59FzyFaMxiho_5XQnyRZzQsrZs.woff2 -------------------------------------------------------------------------------- /src/fonts/robotomono/v4/N4duVc9C58uwPiY8_59Fzy_vZmeiCMnoWNN9rHBYaTc.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/naivesound/glitch-orig/a456917b36dc48d4dcfe37277f33bc6de1017a0d/src/fonts/robotomono/v4/N4duVc9C58uwPiY8_59Fzy_vZmeiCMnoWNN9rHBYaTc.woff2 -------------------------------------------------------------------------------- /src/fonts/robotomono/v4/hMqPNLsu_dywMa4C_DEpY0bcKLIaa1LC45dFaAfauRA.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/naivesound/glitch-orig/a456917b36dc48d4dcfe37277f33bc6de1017a0d/src/fonts/robotomono/v4/hMqPNLsu_dywMa4C_DEpY0bcKLIaa1LC45dFaAfauRA.woff2 -------------------------------------------------------------------------------- /src/fonts/robotomono/v4/hMqPNLsu_dywMa4C_DEpY2o_sUJ8uO4YLWRInS22T3Y.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/naivesound/glitch-orig/a456917b36dc48d4dcfe37277f33bc6de1017a0d/src/fonts/robotomono/v4/hMqPNLsu_dywMa4C_DEpY2o_sUJ8uO4YLWRInS22T3Y.woff2 -------------------------------------------------------------------------------- /src/fonts/robotomono/v4/hMqPNLsu_dywMa4C_DEpY44P5ICox8Kq3LLUNMylGO4.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/naivesound/glitch-orig/a456917b36dc48d4dcfe37277f33bc6de1017a0d/src/fonts/robotomono/v4/hMqPNLsu_dywMa4C_DEpY44P5ICox8Kq3LLUNMylGO4.woff2 -------------------------------------------------------------------------------- /src/fonts/robotomono/v4/hMqPNLsu_dywMa4C_DEpY76up8jxqWt8HVA3mDhkV_0.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/naivesound/glitch-orig/a456917b36dc48d4dcfe37277f33bc6de1017a0d/src/fonts/robotomono/v4/hMqPNLsu_dywMa4C_DEpY76up8jxqWt8HVA3mDhkV_0.woff2 -------------------------------------------------------------------------------- /src/fonts/robotomono/v4/hMqPNLsu_dywMa4C_DEpYyYE0-AqJ3nfInTTiDXDjU4.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/naivesound/glitch-orig/a456917b36dc48d4dcfe37277f33bc6de1017a0d/src/fonts/robotomono/v4/hMqPNLsu_dywMa4C_DEpYyYE0-AqJ3nfInTTiDXDjU4.woff2 -------------------------------------------------------------------------------- /src/fonts/robotomono/v4/hMqPNLsu_dywMa4C_DEpYzTOQ_MqJVwkKsUn0wKzc2I.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/naivesound/glitch-orig/a456917b36dc48d4dcfe37277f33bc6de1017a0d/src/fonts/robotomono/v4/hMqPNLsu_dywMa4C_DEpYzTOQ_MqJVwkKsUn0wKzc2I.woff2 -------------------------------------------------------------------------------- /src/fonts/robotomono/v4/hMqPNLsu_dywMa4C_DEpYzUj_cnvWIuuBMVgbX098Mw.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/naivesound/glitch-orig/a456917b36dc48d4dcfe37277f33bc6de1017a0d/src/fonts/robotomono/v4/hMqPNLsu_dywMa4C_DEpYzUj_cnvWIuuBMVgbX098Mw.woff2 -------------------------------------------------------------------------------- /src/glitch.js: -------------------------------------------------------------------------------- 1 | import * as expr from 'expr'; 2 | import * as functions from './glitchFuncs'; 3 | import * as samples from './glitchSamples'; 4 | 5 | function funcs() { 6 | return Object.assign({}, functions, samples, window.userFuncs || {}); 7 | } 8 | 9 | export default class Glitch { 10 | constructor(sampleRate = 44100) { 11 | this.expr = () => 0; 12 | this.src = ''; 13 | this.sampleRate = sampleRate; 14 | this.vars = { 15 | t: expr.varExpr(0), 16 | x: expr.varExpr(0), 17 | y: expr.varExpr(0), 18 | }; 19 | this.reset(); 20 | } 21 | reset() { 22 | this.vars.t(0); 23 | this.frame = 0; 24 | this.measure = 0; 25 | } 26 | compile(e) { 27 | const f = expr.parse(e, this.vars, funcs()); 28 | if (f) { 29 | this.next = {src: e, expr: f}; 30 | return true; 31 | } 32 | return false; 33 | } 34 | nextSample() { 35 | if (this.next) { 36 | let applyNext = true; 37 | const bpm = (this.vars.bpm ? this.vars.bpm() : 0); 38 | if (bpm) { 39 | applyNext = false; 40 | this.measure++; 41 | if (this.measure > this.sampleRate * 60 / bpm) { 42 | this.measure = 0; 43 | applyNext = true; 44 | } 45 | } 46 | if (applyNext) { 47 | this.expr = this.next.expr; 48 | this.src = this.next.src; 49 | this.next = undefined; 50 | } 51 | } 52 | const v = this.expr(); 53 | if (!isNaN(v)) { 54 | this.lastSample = (((v % 256) + 256) % 256) / 128 - 1; 55 | } 56 | this.frame++; 57 | this.vars.t(Math.round(this.frame * 8000 / this.sampleRate)); 58 | return this.lastSample; 59 | } 60 | onaudioprocess(e) { 61 | const buffer = e.outputBuffer.getChannelData(0); 62 | for (let i = 0; i < buffer.length; i++) { 63 | buffer[i] = this.nextSample(); 64 | } 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/glitch180x180.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/naivesound/glitch-orig/a456917b36dc48d4dcfe37277f33bc6de1017a0d/src/glitch180x180.png -------------------------------------------------------------------------------- /src/glitch192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/naivesound/glitch-orig/a456917b36dc48d4dcfe37277f33bc6de1017a0d/src/glitch192x192.png -------------------------------------------------------------------------------- /src/glitchFuncs.js: -------------------------------------------------------------------------------- 1 | import { sampleRate } from './audio'; 2 | 3 | function denorm(x) { 4 | return x * 127 + 128; 5 | } 6 | 7 | function arg(x, defaultValue) { 8 | if (!x) { 9 | return defaultValue; 10 | } 11 | return x(); 12 | } 13 | 14 | // Returns sine value, argument is wave phase 0..255, result is in the range 0..255 15 | export function s(args) { 16 | return denorm(Math.sin(arg(args[0], 0) * Math.PI / 128)); 17 | } 18 | 19 | // Returns random number in the range 0..max 20 | export function r(args) { 21 | return Math.random() * arg(args[0], 255); 22 | } 23 | 24 | // Returns log2 from the argument 25 | export function l(args) { 26 | if (args[0]) { 27 | return Math.log2(args[0]()); 28 | } 29 | return 0; 30 | } 31 | 32 | // Returns agument by its index, e.g. a(2, 4, 5, 6) returns 5 (the 2nd argument) 33 | export function a(args) { 34 | if (args.length === 0) { 35 | return 0; 36 | } 37 | let i = args[0](); 38 | if (isNaN(i)) { 39 | return NaN; 40 | } 41 | const len = args.length - 1; 42 | if (len === 0) { 43 | return 0; 44 | } 45 | i = Math.floor(i + len) % len; 46 | i = (i + len) % len; 47 | return args[i + 1](); 48 | } 49 | 50 | // 51 | // Music theory helpers 52 | // 53 | const scales = [ 54 | // 0..6 - classical modes 55 | [0, 2, 4, 5, 7, 9, 11], // ionian, major scale 56 | [0, 2, 3, 5, 7, 9, 10], // dorian 57 | [0, 1, 3, 5, 7, 8, 10], // phrygian 58 | [0, 2, 4, 6, 7, 9, 11], // lydian 59 | [0, 2, 4, 5, 7, 9, 10], // mixolydian 60 | [0, 2, 3, 5, 7, 8, 10], // aeolian, natural minor scale 61 | [0, 1, 3, 5, 6, 8, 10], // locrian 62 | 63 | // 7..8 - common minor variants 64 | [0, 2, 3, 5, 7, 8, 11], // harmonic minor scale 65 | [0, 2, 3, 5, 7, 9, 11], // melodic minor scale 66 | 67 | // 9..13 - pentatonic and other scales 68 | [0, 2, 4, 7, 9], // major pentatonic 69 | [0, 3, 5, 7, 10], // minor pentatonic 70 | [0, 3, 5, 6, 7, 10], // blues 71 | [0, 2, 4, 6, 8, 10], // whole tone scale 72 | [0, 1, 3, 4, 6, 7, 9, 10], // octatonic 73 | 74 | // 14 and above - chromatic scale 75 | [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11], // chromatic is a fallback scale 76 | ]; 77 | 78 | // Returns note value from the given scale 79 | export function scale(args) { 80 | const i = Math.min(Math.floor(arg(args[1], 0)), scales.length - 1); 81 | if (isNaN(i)) { 82 | return NaN; 83 | } 84 | const len = scales[i].length; 85 | let j = arg(args[0], 0); 86 | const transpose = Math.floor(j / len) * 12; 87 | j = Math.floor(j + len) % len; 88 | j = (j + len) % len; 89 | return scales[i][j] + transpose; 90 | } 91 | 92 | // Returns frequency of the note 93 | export function hz(args) { 94 | return Math.pow(2, arg(args[0], 0) / 12) * 440; 95 | } 96 | 97 | // 98 | // Sound synthesis 99 | // 100 | function osc(oscillator, freq) { 101 | oscillator.nextfreq = arg(freq, NaN); 102 | if (!oscillator.freq) { 103 | oscillator.freq = oscillator.nextfreq; 104 | } 105 | let w = oscillator.w || 0; 106 | if (w > 1) { 107 | w = w - Math.floor(w); 108 | oscillator.freq = oscillator.nextfreq; 109 | } 110 | oscillator.w = w + oscillator.freq / sampleRate; 111 | if (isNaN(oscillator.nextfreq)) { 112 | return NaN; 113 | } 114 | return w; 115 | } 116 | 117 | export function sin(args) { 118 | return denorm(Math.sin(osc(this, args[0]) * 2 * Math.PI)); 119 | } 120 | 121 | export function saw(args) { 122 | const tau = osc(this, args[0]); 123 | return denorm(2 * (tau - Math.round(tau))); 124 | } 125 | 126 | export function tri(args) { 127 | const tau = osc(this, args[0]) + 0.25; 128 | return denorm(-1 + 4 * Math.abs(tau - Math.round(tau))); 129 | } 130 | 131 | export function sqr(args) { 132 | let tau = osc(this, args[0]); 133 | if (isNaN(tau)) { 134 | return NaN; 135 | } 136 | tau = tau - Math.floor(tau); 137 | return denorm(tau < arg(args[1], 0.5) ? 1 : -1); 138 | } 139 | 140 | // FM synthesizer, modulators 1 and 2 are chained, modulator 3 is parallel 141 | export function fm(args) { 142 | const freq = args[0]; 143 | const mf1 = args[1]; 144 | const mi1 = args[2]; 145 | const mf2 = args[3]; 146 | const mi2 = args[4]; 147 | const mf3 = args[5]; 148 | const mi3 = args[6]; 149 | this.nextfreq = arg(freq, NaN); 150 | if (!this.freq) { 151 | this.freq = this.nextfreq; 152 | } 153 | this.w = (this.w || 0); 154 | this.w += 1 / sampleRate; 155 | function modulate(tau, f) { 156 | const v3 = arg(mi3, 0) * Math.sin(tau * (f * arg(mf3, 0))); 157 | const v2 = arg(mi2, 0) * Math.sin(tau * (f * arg(mf2, 0) + v3)); 158 | const v1 = arg(mi1, 0) * Math.sin(tau * (f * arg(mf1, 0) + v3)); 159 | return Math.sin(tau * (f + v1 + v2)); 160 | } 161 | if (isNaN(this.nextfreq)) { 162 | return NaN; 163 | } 164 | const tau = this.w * 2 * Math.PI; 165 | if (modulate(tau, this.freq) * modulate(tau + 1 / sampleRate, this.freq) <= 0) { 166 | this.w = this.w - Math.floor(this.w); 167 | this.freq = this.nextfreq; 168 | } 169 | return denorm(modulate(tau, this.freq)); 170 | } 171 | 172 | 173 | // Switches values at give tempo, NaN is returned when the switch happens 174 | function next(args, seq, f) { 175 | const beatDuration = sampleRate / (arg(args[0], NaN) / 60) * (seq.mul || 1); 176 | if (isNaN(beatDuration)) { 177 | return NaN; 178 | } 179 | seq.t = (seq.t + 1) || beatDuration; 180 | if (seq.t >= beatDuration) { 181 | seq.t = 0; 182 | seq.beat = (seq.beat !== undefined ? seq.beat + 1 : 0); 183 | } 184 | const len = args.length - 1; 185 | if (len <= 0) { 186 | return (seq.t === 0 ? NaN : 0); 187 | } 188 | let i = (Math.floor(seq.beat) + len) % len; 189 | i = (i + len) % len; 190 | return f(args, i, seq.t / beatDuration); 191 | } 192 | 193 | // Switches values evaluating the current value on each call 194 | export function loop(args) { 195 | return next(args, this, (a, i, offset) => { 196 | let arg = a[i + 1]; 197 | this.mul = 1; 198 | if (arg.car) { 199 | this.mul = arg.car(); 200 | arg = arg.cdr; 201 | } 202 | const res = arg(); 203 | return offset === 0 ? NaN : res; 204 | }); 205 | } 206 | 207 | // Switches values evaluating them once per beat 208 | export function seq(args) { 209 | return next(args, this, (a, i, offset) => { 210 | if (offset === 0) { 211 | let arg = a[i + 1]; 212 | this.mul = 1; 213 | if (arg.car) { 214 | this.mul = arg.car(); 215 | arg = arg.cdr; 216 | if (arg.car) { 217 | const steps = []; 218 | while (arg.car) { 219 | steps.push(arg.car()); 220 | arg = arg.cdr; 221 | } 222 | steps.push(arg()); 223 | this.value = (delta) => { 224 | const n = steps.length - 1; 225 | const index = Math.floor(n * delta); 226 | const from = steps[index]; 227 | const to = steps[index + 1]; 228 | const k = (delta - index / n) * n; 229 | return from + (to - from) * k; 230 | }; 231 | return NaN; 232 | } 233 | } 234 | const val = arg(); 235 | this.value = () => val; 236 | return NaN; 237 | } 238 | return this.value(offset); 239 | }); 240 | } 241 | 242 | // env() -> 0 243 | // env(x) -> x 244 | // env(r, x) -> percussive 245 | // env(a, r, x) -> percussive 246 | // env(a, segmentDuration, segmentAmplitude, ..., x) 247 | export function env(args) { 248 | // Zero arguments = zero signal level 249 | // One argument = unmodied signal value 250 | if (args.length < 2) { 251 | return arg(args[0], 128); 252 | } 253 | // Last argument is signal value 254 | const v = arg(args[args.length - 1], NaN); 255 | // Update envelope 256 | this.e = this.e || []; 257 | this.d = this.d || []; 258 | this.segment = this.segment || 0; 259 | this.t = this.t || 0; 260 | if (args.length === 2) { 261 | this.d[0] = 0.0625 * sampleRate; 262 | this.e[0] = 1; 263 | this.d[1] = arg(args[0], NaN) * sampleRate; 264 | this.e[1] = 0; 265 | } else { 266 | this.d[0] = arg(args[0], NaN) * sampleRate; 267 | this.e[0] = 1; 268 | for (let i = 1; i < args.length - 1; i = i + 2) { 269 | this.d[(i - 1) / 2 + 1] = arg(args[i], NaN) * sampleRate; 270 | if (i + 1 < args.length - 1) { 271 | this.e[(i - 1) / 2 + 1] = arg(args[i + 1], NaN); 272 | } else { 273 | this.e[(i - 1) / 2 + 1] = 0; 274 | } 275 | } 276 | } 277 | if (isNaN(v)) { 278 | this.segment = 0; 279 | this.t = 0; 280 | return NaN; 281 | } 282 | this.t++; 283 | if (this.t > this.d[this.segment]) { 284 | this.t = 0; 285 | this.segment++; 286 | } 287 | if (this.segment >= this.e.length) { 288 | return 128; // end of envelope 289 | } 290 | const prevAmp = (this.segment === 0 ? 0 : this.e[this.segment - 1]); 291 | const amp = this.e[this.segment]; 292 | return (v - 128) * (prevAmp + (amp - prevAmp) * (this.t / this.d[this.segment])) + 128; 293 | } 294 | 295 | // mixes signals and cuts amplitude to avoid overflows 296 | export function mix(args) { 297 | let v = 0; 298 | this.lastSamples = this.lastSamples || {}; 299 | for (let i = 0; i < args.length; i++) { 300 | let arg = args[i]; 301 | let volume = 1; 302 | if (arg.car) { 303 | volume = arg.car(); 304 | arg = arg.cdr; 305 | } 306 | let sample = arg(); 307 | if (isNaN(sample)) { 308 | sample = this.lastSamples[i] || 0; 309 | } else { 310 | this.lastSamples[i] = sample; 311 | } 312 | v = v + volume * (sample - 128) / 127; 313 | } 314 | if (args.length > 0) { 315 | v = v / Math.sqrt(args.length); 316 | return denorm(Math.max(Math.min(v, 1), -1)); 317 | } 318 | return 128; 319 | } 320 | 321 | // Simple one pole IIR low-pass filter, can be used to construct high-pass and 322 | // all-pass filters as well (hpf=x-lpf(x), apf=hpf-lpf) 323 | export function lpf(args) { 324 | const x = args[0]; 325 | const fc = args[1]; 326 | const cutoff = arg(fc, 200); 327 | const value = arg(x, NaN); 328 | if (isNaN(value) || isNaN(cutoff)) { 329 | return NaN; 330 | } 331 | const wa = Math.tan(Math.PI * arg(fc, 200) / sampleRate); 332 | const a = wa / (1.0 + wa); 333 | this.lpf = this.lpf || 128; 334 | this.lpf = this.lpf + (value - this.lpf) * a; 335 | return this.lpf; 336 | } 337 | -------------------------------------------------------------------------------- /src/glitchSamples.js: -------------------------------------------------------------------------------- 1 | import b64bd from './samples/bd.wav'; 2 | import b64cb from './samples/cb.wav'; 3 | import b64cl from './samples/cl.wav'; 4 | import b64hh from './samples/hh.wav'; 5 | import b64mc from './samples/mc.wav'; 6 | import b64mt from './samples/mt.wav'; 7 | import b64oh from './samples/oh.wav'; 8 | import b64rs from './samples/rs.wav'; 9 | import b64sn from './samples/sn.wav'; 10 | 11 | const TR808Samples = [ 12 | atob(b64bd), 13 | atob(b64sn), 14 | atob(b64mt), 15 | atob(b64mc), 16 | atob(b64rs), 17 | atob(b64cl), 18 | atob(b64cb), 19 | atob(b64oh), 20 | atob(b64hh), 21 | ]; 22 | 23 | function arg(x, defaultValue) { 24 | if (!x) { 25 | return defaultValue; 26 | } 27 | return x(); 28 | } 29 | 30 | export function tr808(args) { 31 | this.i = this.i || 0; 32 | let drum = arg(args[0], NaN); 33 | let volume = arg(args[1], 1); 34 | let len = TR808Samples.length; 35 | if (!isNaN(drum) && !isNaN(volume)) { 36 | let sample = TR808Samples[(((drum|0)%len)+len)%len]; 37 | if (this.i * 2 + 0x80 + 1 < sample.length) { 38 | let hi = sample.charCodeAt(0x80 + this.i * 2+1); 39 | let lo = sample.charCodeAt(0x80 + this.i * 2); 40 | let sign = hi & (1 << 7); 41 | let v = (hi << 8) | lo; 42 | if (sign) { 43 | v = -v + 0x10000; 44 | } 45 | let x = v / 0x7fff; 46 | this.i++; 47 | return x * volume * 127 + 128; 48 | } else { 49 | return NaN 50 | } 51 | } 52 | this.i = 0; 53 | return NaN 54 | } 55 | 56 | -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | Glitch 10 | 11 | 12 |
13 | 14 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | // Importing assets: HTML, CSS and fonts 2 | import './index.html'; 3 | import './favicon.png'; 4 | import './glitch180x180.png'; 5 | import './glitch192x192.png'; 6 | import 'font-awesome/css/font-awesome.css'; 7 | import './styles.css'; 8 | import './roboto.css'; 9 | 10 | import React from 'react'; 11 | import ReactDOM from 'react-dom'; 12 | import { Provider } from 'react-redux'; 13 | import { createStore, combineReducers, applyMiddleware } from 'redux'; 14 | 15 | import Layout from './jsx/Layout'; 16 | import { expr, playback, navigation, playbackMode, tab } from './reducers'; 17 | 18 | import * as actions from './actions'; 19 | import * as audio from './audio'; 20 | import { saveFile, waveFile } from './save'; 21 | 22 | import Glitch from './glitch'; 23 | 24 | const glitch = new Glitch(audio.sampleRate); 25 | 26 | function exportWavFile(sampleRate, expression) { 27 | const glitchRenderer = new Glitch(sampleRate); 28 | glitchRenderer.compile(expression); 29 | saveFile('glitch.wav', 30 | waveFile(30, sampleRate, glitchRenderer.nextSample.bind(glitchRenderer)), 31 | 'audio/wav'); 32 | } 33 | 34 | // Calls audio service on PLAY and STOP actions 35 | const audioMiddleware = store => next => action => { 36 | switch (action.type) { 37 | case actions.PLAY: glitch.reset(); audio.play(glitch.onaudioprocess.bind(glitch)); break; 38 | case actions.STOP: audio.stop(); break; 39 | case actions.SET_EXPR: 40 | clearTimeout(audioMiddleware.errorTimeout); 41 | if (!glitch.compile(action.expr)) { 42 | audioMiddleware.errorTimeout = setTimeout(() => 43 | store.dispatch(actions.error('syntax error')), 500); 44 | } else { 45 | localStorage.setItem('expr', action.expr); 46 | store.dispatch(actions.error()); 47 | } 48 | break; 49 | case actions.EXPORT_WAV: 50 | exportWavFile(audio.sampleRate, store.getState().expr.expr); 51 | break; 52 | default: 53 | } 54 | return next(action); 55 | }; 56 | 57 | // Changes location URI hash whenever expression is changed 58 | const uriMiddleware = store => next => action => { 59 | const res = next(action); 60 | if (!store.getState().expr.error) { 61 | window.location.hash = encodeURIComponent(store.getState().expr.expr); 62 | } 63 | return res; 64 | }; 65 | 66 | const store = createStore(combineReducers({ 67 | expr, 68 | playback, 69 | navigation, 70 | }), applyMiddleware( 71 | audioMiddleware, 72 | uriMiddleware 73 | )); 74 | 75 | // Initialize glitch with default/current expression, optionally do autoplay 76 | if (window.location.hash) { 77 | store.dispatch(actions.setExpr(decodeURIComponent(window.location.hash.substring(1)))); 78 | } else if (localStorage.getItem('expr')) { 79 | store.dispatch(actions.setExpr(localStorage.getItem('expr'))); 80 | } else { 81 | store.dispatch(actions.setExpr(store.getState().expr.expr)); 82 | } 83 | if (window.location.search === '?play') { 84 | store.dispatch(actions.play()); 85 | } 86 | 87 | document.onmousemove = (e) => { 88 | glitch.vars.x(e.pageX / window.innerWidth); 89 | glitch.vars.y(e.pageY / window.innerHeight); 90 | }; 91 | 92 | document.onkeydown = (e) => { 93 | if (e.keyCode === 13 && e.ctrlKey) { 94 | if (store.getState().playback.mode === playbackMode.PLAYING) { 95 | store.dispatch(actions.stop()); 96 | } else { 97 | store.dispatch(actions.play()); 98 | } 99 | } 100 | }; 101 | 102 | window.onload = () => { 103 | ReactDOM.render(, 104 | document.getElementById('container')); 105 | }; 106 | -------------------------------------------------------------------------------- /src/jsx/App.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import { connect } from 'react-redux'; 4 | 5 | import { tab } from '../reducers'; 6 | 7 | import { YELLOW, GRAY, PINK } from '../colors'; 8 | 9 | import Visualizer from './Visualizer'; 10 | import Toolbar from './Toolbar'; 11 | import Editor from './Editor'; 12 | import Library from './Library'; 13 | import Help from './Help'; 14 | 15 | const optstring = React.PropTypes.string; 16 | const string = React.PropTypes.string.isRequired; 17 | 18 | const appContainerStyle = { 19 | display: 'flex', 20 | width: '100%', 21 | minHeight: '100%', 22 | maxHeight: '100vh', 23 | flex: 1, 24 | backgroundColor: GRAY, 25 | }; 26 | 27 | const headerStyle = { 28 | display: 'flex', 29 | height: '72px', 30 | lineHeight: '72px', 31 | fontSize: '18pt', 32 | flexShrink: '0', 33 | }; 34 | 35 | const titleStyle = { 36 | color: YELLOW, 37 | fontWeight: 600, 38 | }; 39 | 40 | const errorIconStyle = { 41 | color: PINK, 42 | fontSize: '20pt', 43 | height: '72px', 44 | width: '72px', 45 | lineHeight: '72px', 46 | textAlign: 'center', 47 | }; 48 | 49 | const mainSectionStyle = { 50 | display: 'flex', 51 | flexDirection: 'column', 52 | flex: 1, 53 | padding: '0 0 0 1em', 54 | }; 55 | 56 | function App(props) { 57 | return (
58 | 59 | 60 |
); 61 | } 62 | App.propTypes = { tab: string, error: optstring }; 63 | 64 | function MainSection(props) { 65 | let content; 66 | switch (props.tab) { 67 | case tab.EDITOR: content = ; break; 68 | case tab.LIBRARY: content = ; break; 69 | case tab.HELP: content = ; break; 70 | default: break; 71 | } 72 | return (
73 |
74 | {content} 75 |
); 76 | } 77 | MainSection.propTypes = { tab: string, error: optstring }; 78 | 79 | function Header({ error }) { 80 | return (
81 | 82 | <ErrorIcon error={error} /> 83 | <Visualizer /> 84 | </div>); 85 | } 86 | Header.propTypes = { error: optstring }; 87 | 88 | function Title() { 89 | return (<div style={titleStyle}> 90 | #glitch 91 | </div>); 92 | } 93 | 94 | function ErrorIcon({ error }) { 95 | const style = Object.assign({}, errorIconStyle, { visibility: (error ? 'visible' : 'hidden') }); 96 | return <i className="fa fa-exclamation-triangle" style={style}></i>; 97 | } 98 | ErrorIcon.propTypes = { error: optstring }; 99 | 100 | function mapStateToProps(state) { 101 | return { tab: state.navigation.tab, error: state.expr.error }; 102 | } 103 | 104 | export default connect(mapStateToProps)(App); 105 | -------------------------------------------------------------------------------- /src/jsx/Editor.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | 4 | import { connect } from 'react-redux'; 5 | 6 | import { GRAY, WHITE } from '../colors'; 7 | 8 | import { setExpr } from '../actions'; 9 | 10 | const editorStyle = { 11 | flex: '1', 12 | width: '100%', 13 | resize: 'none', 14 | fontSize: '18pt', 15 | border: 'none', 16 | outline: 'none', 17 | color: WHITE, 18 | backgroundColor: GRAY, 19 | fontFamily: 'Roboto Mono, monospace', 20 | }; 21 | 22 | class Editor extends React.Component { 23 | componentDidMount() { 24 | ReactDOM.findDOMNode(this.refs.editor).focus(); 25 | } 26 | handleTextChange(e) { 27 | this.props.dispatch(setExpr(e.target.value)); 28 | } 29 | render() { 30 | return (<textarea 31 | ref="editor" 32 | autoComplete="off" 33 | autoCorrect="off" 34 | autoCapitalize="off" 35 | spellCheck={false} 36 | value={this.props.expr} 37 | onChange={(e) => this.handleTextChange(e)} 38 | style={editorStyle} 39 | />); 40 | } 41 | } 42 | Editor.propTypes = { 43 | dispatch: React.PropTypes.func.isRequired, 44 | expr: React.PropTypes.string.isRequired, 45 | }; 46 | 47 | function mapStateToProps(state) { 48 | return { expr: state.expr.expr }; 49 | } 50 | 51 | export default connect(mapStateToProps)(Editor); 52 | 53 | -------------------------------------------------------------------------------- /src/jsx/Help.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { connect } from 'react-redux'; 3 | 4 | import { YELLOW } from '../colors'; 5 | 6 | import * as actions from '../actions'; 7 | 8 | import * as functions from '../glitchFuncs'; 9 | import * as samples from '../glitchSamples'; 10 | 11 | const helpWrapperStyle = { 12 | overflowY: 'auto', 13 | flex: 1, 14 | }; 15 | const helpStyle = { 16 | color: YELLOW, 17 | paddingTop: '1rem', 18 | }; 19 | 20 | const HELP = ` 21 | Glitch is an algorithmic synthesizer. It creates music with math. 22 | 23 | # INPUT AND OUTPUT 24 | 25 | Music is a function __f(t)__ where __t__ is increasing in time. 26 | 27 | Glitch increases __t__ at __8000/sec__ rate and it can be a real number if your 28 | hardware sample rate is higher. Expression result is expected to be in range 29 | __[0..255]__ otherwise overflow occurs. 30 | 31 | Example: [t*14](#) - sawtooth wave at 437 Hz. 32 | 33 | Music expression is evaluated once for each audio frame. You can use numbers, 34 | math operators, variables and functions to compose more complex expressions. 35 | 36 | # MATH 37 | 38 | Basic: __+__ __-__ __*__ __/__ __%__ _(modulo)_ __**__ _(power)_ 39 | 40 | Bitwise: __&__ __|__ __^__ _(xor or bitwise not)_ __<<__ __>>__ 41 | 42 | Compare: __== != < <= > >=__ _(return 1 or 0)_ 43 | 44 | Grouping: __( ) ,__ _(separates expressions or function arguments)_ 45 | 46 | Conditional: __&&__ __||__ _(short-circuit operators)_ 47 | 48 | Assignment: __=__ _(left side must be a variable)_ 49 | 50 | Bitwise operators truncate numbers to integer values. 51 | 52 | Example: [x=6,(y=x+1,x*y)](#) - returns 42 53 | 54 | Example: [t*5&(t>>7)|t*3&(t*4>>10)](#) - bytebeat music 55 | 56 | # FUNCTIONS 57 | 58 | __l(x)__: log2(x) 59 | 60 | __r(n)__: random number in the range [0..n] 61 | 62 | __s(i)__: sine wave amplitude [0..255] at phase i=[0..255] 63 | 64 | Example: [s(t*14)](#) - sine wave at 437Hz 65 | 66 | # SEQUENCERS 67 | 68 | Sequencers are used to describe melodies, rhythmic patterns or other parts of 69 | your song. 70 | 71 | __a(i, x0, x1, x2, ...)__ returns x[i] value for the given i 72 | 73 | Example: [t*a(t>>11,4,5,6)](#) 74 | 75 | __seq(bpm, x0, x1, x2, ...)__ returns x[i] value where i increases at given tempo. 76 | 77 | Values can be numeric constants, variables or expressions. Values are evaluated 78 | once per beat and the result is cached. 79 | 80 | Value can be a pair of numbers like (2,3) then the first number is relative 81 | step duration and the second one is actual value. This means value 3 will be 82 | returned for 2 beats. 83 | 84 | Value can be a group of more than 2 numbers. The the first number is relative 85 | step duration, and other values are gradually slided, e.g. (0.5,2,4,2) is a 86 | value changed from 2 to 4 back to 2 and the step duration is half of a beat. 87 | 88 | Example: [t*seq(120,4,5,6)](#) 89 | 90 | Example: [t*seq(120,(1,4,6,4),(1/2,5),(1/2,6))](#) 91 | 92 | __loop(bpm, x0, x1, x2, ...)__ evaluates x[i] increasing i at given tempo. 93 | Unlike seq, loop evaluates x[i] for every audio frame, so other functions can 94 | be used as loop values. 95 | 96 | seq is often used to change pitch or volume, loop is often used to schedule inner sequences/loops. 97 | 98 | Example: [t*loop(30,seq(240,4,5),seq(240,4,6))](#) 99 | 100 | seq and loop return NaN at the beginning of each step. NaN value is used by the 101 | instruments to detect the start of a new note. 102 | 103 | # INSTRUMENTS 104 | 105 | Oscillators are the building blocks of synthesizers. Oscillator phase is 106 | managed internally, only frequency must be provided (in Hz). 107 | 108 | __sin(freq)__ = sine wave 109 | 110 | __tri(freq)__ = triangular wave 111 | 112 | __saw(freq)__ = saw-tooth wave 113 | 114 | __sqr(freq, pwm)__ = square wave of given pulse width, default pwm=0.5 115 | 116 | Example: [(sin(220)+tri(440))/2](#) 117 | 118 | More advanced instruments: 119 | 120 | __fm(freq, mf1, ma1, mf2, ma2, mf3, ma3)__ is a 3-operator FM synthesizer, mf 121 | is operator frequency ratio, ma operator amplification. M2 and M1 are 122 | sequential, M3 is parallel. 123 | 124 | Example: [fm(seq(120,440,494),1,2,0.5,0.5)](#) 125 | 126 | __tr808(instr, volume)__ is TR808 drum kit. 0 = kick, 1 = snare, 2 = tom, 3 = 127 | crash, 4 = rimshot, 5 = clap, 6 = cowbell, 7 = open hat, 8 = closed hat. 128 | 129 | Example: [tr808(1,seq(240,1,0.2))](#) plays simple snare rhythm 130 | 131 | __env(r, x)__ wraps signal x with very short attack and given release time r 132 | 133 | __env(a, r, x)__ wraps signal x with given attack and release time 134 | 135 | __env(a, i1, a1, i2, a2, ..., x)__ wraps signal x with given attack time and 136 | amplitude values for each time interval i. 137 | 138 | Example: [env(0.001,0.1,sin(seq(480,440)))](#) 139 | 140 | # MELODY 141 | 142 | __hz(note)__ returns note frequency 143 | 144 | __scale(i, mode)__ returns node at position i in the given scale. 145 | 146 | Example: [tri(hz(scale(seq(480,r(5)))))](#) plays random notes from the major scale 147 | 148 | # POLYPHONY 149 | 150 | __mix(v1, v2, ...)__ mixes voices to avoid overflow. 151 | 152 | Voice can be a single value or a pair of (volume,voice) values. Volume must be in the range [0..1]. 153 | 154 | Example: [mix((0.1,sin(440)),(0.2,tri(220)))](#) 155 | 156 | # EFFECTS 157 | 158 | __lpf(signal, cutoff)__ low-pass filter 159 | 160 | # VARIABLES 161 | 162 | Any word can be a variable name if there is no function with such name. 163 | Variables keep their values between evaluations. 164 | 165 | __t__ is time, increased from 0 to infinity by 8000 for each second. 166 | 167 | __x__ and __y__ are current mouse cursor position in the range [0..1]. 168 | 169 | __bpm__ (if set) applies user input on the next beat to keep the tempo. 170 | 171 | # LIST OF ALL FUNCTIONS 172 | 173 | `; 174 | 175 | function allFuncs() { 176 | return Object.keys(Object.assign({}, functions, samples, window.userFuncs || {})) 177 | .sort().join(' '); 178 | } 179 | 180 | function unescape(s) { 181 | const e = document.createElement('div'); 182 | e.innerHTML = s; 183 | return e.childNodes.length === 0 ? '' : e.childNodes[0].nodeValue; 184 | } 185 | 186 | function mmd(src) { 187 | let h = ''; 188 | function escape(t) { return new Option(t).innerHTML; } 189 | function inlineEscape(s) { 190 | return escape(s) 191 | .replace(/\[([^\]]+)]\(([^(]+)\)/g, '$1'.link('$2')) 192 | .replace(/__([^_]*)__/g, '<strong>$1</strong>') 193 | .replace(/_([^_]+)_/g, '<em>$1</em>'); 194 | } 195 | 196 | src 197 | .replace(/^\s+|\r|\s+$/g, '') 198 | .replace(/\t/g, ' ') 199 | .split(/\n\n+/) 200 | .forEach((b) => { 201 | const c = b[0]; 202 | if (c === '#') { 203 | const i = b.indexOf(' '); 204 | h += `<h${i}>${inlineEscape(b.slice(i + 1))}</h${i}>`; 205 | } else if (c === '<') { 206 | h += b; 207 | } else { 208 | h += `<p>${inlineEscape(b)}</p>`; 209 | } 210 | }); 211 | return h; 212 | } 213 | 214 | export default connect()((props) => 215 | (<div 216 | className="help" 217 | style={helpWrapperStyle} 218 | ref={(el) => { 219 | if (el) { 220 | const links = el.querySelectorAll('a'); 221 | for (let i = 0; i < links.length; i++) { 222 | const a = links[i]; 223 | a.onclick = (e) => { 224 | e.preventDefault(); 225 | props.dispatch(actions.setExpr(unescape(a.innerHTML))); 226 | props.dispatch(actions.play()); 227 | }; 228 | } 229 | } 230 | }} 231 | > 232 | <div style={helpStyle} dangerouslySetInnerHTML={{ __html: mmd(HELP + allFuncs()) }} /> 233 | </div>)); 234 | -------------------------------------------------------------------------------- /src/jsx/Layout.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import { GREEN, GRAY, PINK } from '../colors'; 4 | 5 | import App from './App'; 6 | 7 | export function isElectron() { 8 | return window && window.process && window.process.type; 9 | } 10 | 11 | const layoutStyle = { 12 | backgroundColor: GREEN, 13 | fontFamily: 'Roboto Mono, monospace', 14 | height: '100vh', 15 | }; 16 | 17 | const flexColumStyle = { 18 | height: '100%', 19 | display: 'flex', 20 | flexDirection: 'column', 21 | flex: 1, 22 | justifyContent: 'space-around', 23 | alignItems: 'center', 24 | }; 25 | 26 | const appContainerStyle = { 27 | display: 'flex', 28 | width: '800px', 29 | minHeight: '500px', 30 | maxHeight: '500px', 31 | boxShadow: '8px 8px 0px 0px rgba(0,0,0,0.5)', 32 | }; 33 | 34 | const headerStyle = { 35 | display: 'flex', 36 | }; 37 | 38 | const copyrightStyle = { 39 | padding: '2em', 40 | color: GRAY, 41 | }; 42 | 43 | export default class Layout extends React.Component { 44 | constructor(props) { 45 | super(props); 46 | this.state = { width: window.innerWidth, height: window.innerHeight }; 47 | this.resize = this.resize.bind(this); 48 | } 49 | componentDidMount() { 50 | window.addEventListener('resize', this.resize); 51 | } 52 | componentWillUnmount() { 53 | window.removeEventListener('resize', this.resize); 54 | } 55 | resize() { 56 | this.setState({ width: window.innerWidth, height: window.innerHeight }); 57 | } 58 | render() { 59 | let app = <App />; 60 | if (!isElectron() && 61 | this.state.width > 800 && this.state.height > 500) { 62 | app = (<div style={flexColumStyle}> 63 | <SocialIcons /> 64 | <div style={appContainerStyle}>{app}</div> 65 | <Copyright /> 66 | </div>); 67 | } 68 | return <div style={layoutStyle}>{app}</div>; 69 | } 70 | } 71 | 72 | function SocialIcons() { 73 | return (<div style={headerStyle}> 74 | <a href="http://naivesound.com/">about</a> ~  75 | <a href="http://twitter.com/naive_sound">follow</a> ~  76 | <a href="https://github.com/naivesound/glitch">github</a> 77 | </div>); 78 | } 79 | 80 | function Copyright() { 81 | return (<div style={copyrightStyle}> 82 | Made with  83 | <i className="fa fa-heart" style={{ color: PINK }}></i> 84 |  at  85 | <a href="http://naivesound.com/">NaiveSound</a> 86 | </div>); 87 | } 88 | -------------------------------------------------------------------------------- /src/jsx/Library.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import { connect } from 'react-redux'; 4 | 5 | import { play, setExpr } from '../actions'; 6 | 7 | import { YELLOW, PINK } from '../colors'; 8 | 9 | import examples from '../examples'; 10 | 11 | const libraryStyle = { 12 | overflowY: 'auto', 13 | flex: 1, 14 | }; 15 | 16 | const linkStyle = { 17 | display: 'block', 18 | overflow: 'hidden', 19 | textOverflow: 'ellipsis', 20 | whiteSpace: 'nowrap', 21 | }; 22 | 23 | class Library extends React.Component { 24 | constructor() { 25 | super(); 26 | this.ellipsisWidth = 0; 27 | this.onresize = this.onresize.bind(this); 28 | } 29 | componentDidMount() { 30 | this.onresize(); 31 | window.addEventListener('resize', this.onresize); 32 | } 33 | componentWillUnmount() { 34 | window.removeEventListener('resize', this.onresize); 35 | } 36 | onresize() { 37 | this.width = this.refs.content.offsetWidth; 38 | this.height = this.refs.content.offsetHeight; 39 | this.ellipsisWidth = this.width * 0.9; 40 | this.forceUpdate(); 41 | } 42 | handleClick(expr) { 43 | this.props.dispatch(setExpr(expr)); 44 | this.props.dispatch(play()); 45 | } 46 | renderLink(example) { 47 | return (<Link 48 | key={example.f} 49 | expr={example.f} 50 | active={this.props.expr === example.f} 51 | color={YELLOW} 52 | width={this.ellipsisWidth} 53 | onClick={() => this.handleClick(example.f)} 54 | />); 55 | } 56 | render() { 57 | const links = examples.map((example) => this.renderLink(example)); 58 | return (<div ref="content" style={libraryStyle}> 59 | <div style={{ height: (this.ellipsisWidth === 0 ? 0 : `${this.height}px`) }}> 60 | {links} 61 | </div> 62 | </div>); 63 | } 64 | } 65 | Library.propTypes = { 66 | dispatch: React.PropTypes.func.isRequired, 67 | expr: React.PropTypes.string.isRequired, 68 | }; 69 | 70 | function Link(props) { 71 | let color = YELLOW; 72 | let mark = '\u00a0'; 73 | if (props.active) { 74 | color = PINK; 75 | mark = '\u25b6'; // right-pointing triangle 76 | } 77 | const style = Object.assign({}, linkStyle, { 78 | color, 79 | width: props.width, 80 | cursor: 'pointer', 81 | }); 82 | return (<a {...props} style={style}>{`${mark} ${props.expr}`}</a>); 83 | } 84 | Link.propTypes = { 85 | active: React.PropTypes.bool.isRequired, 86 | expr: React.PropTypes.string.isRequired, 87 | width: React.PropTypes.number.isRequired, 88 | }; 89 | 90 | export default connect(state => ({ expr: state.expr.expr }))(Library); 91 | -------------------------------------------------------------------------------- /src/jsx/Toolbar.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import { connect } from 'react-redux'; 4 | 5 | import { playbackMode, tab } from '../reducers'; 6 | import { navigate, play, stop, exportWav } from '../actions'; 7 | 8 | import { YELLOW } from '../colors'; 9 | 10 | const toolbarStyle = { 11 | display: 'flex', 12 | flexDirection: 'column', 13 | width: '72px', 14 | }; 15 | 16 | const iconButtonStyle = { 17 | color: YELLOW, 18 | height: '72px', 19 | lineHeight: '72px', 20 | textAlign: 'center', 21 | cursor: 'pointer', 22 | fontSize: '20pt', 23 | }; 24 | 25 | function Toolbar(props) { 26 | return (<div style={toolbarStyle}> 27 | <PlayButton mode={props.mode} dispatch={props.dispatch} /> 28 | <IconButton 29 | icon="fa-code" 30 | active={props.tab === tab.EDITOR} 31 | onClick={() => props.dispatch(navigate(tab.EDITOR))} 32 | /> 33 | <IconButton 34 | icon="fa-folder-open" 35 | active={props.tab === tab.LIBRARY} 36 | onClick={() => props.dispatch(navigate(tab.LIBRARY))} 37 | /> 38 | <IconButton 39 | icon="fa-question" 40 | active={props.tab === tab.HELP} 41 | onClick={() => props.dispatch(navigate(tab.HELP))} 42 | /> 43 | <div style={{ flex: 1 }}></div> 44 | <IconButton 45 | icon="fa-floppy-o" 46 | active 47 | onClick={() => props.dispatch(exportWav())} 48 | /> 49 | </div>); 50 | } 51 | Toolbar.propTypes = { 52 | dispatch: React.PropTypes.func.isRequired, 53 | tab: React.PropTypes.string.isRequired, 54 | mode: React.PropTypes.string.isRequired, 55 | }; 56 | 57 | function PlayButton(props) { 58 | if (props.mode === playbackMode.STOPPED) { 59 | return (<IconButton 60 | icon="fa-play" 61 | active 62 | onClick={() => props.dispatch(play())} 63 | />); 64 | } else if (props.mode === playbackMode.PLAYING) { 65 | return (<IconButton 66 | icon="fa-stop" 67 | active 68 | onClick={() => props.dispatch(stop())} 69 | />); 70 | } 71 | } 72 | PlayButton.propTypes = { 73 | dispatch: React.PropTypes.func.isRequired, 74 | mode: React.PropTypes.string.isRequired, 75 | }; 76 | 77 | function IconButton(props) { 78 | const opacity = props.active ? 1 : 0.4; 79 | const style = Object.assign({}, iconButtonStyle, { opacity }); 80 | return (<div {...props} style={style}> 81 | <i className={`fa ${props.icon}`} style={{ color: props.color }}></i> 82 | </div>); 83 | } 84 | IconButton.propTypes = { 85 | active: React.PropTypes.bool.isRequired, 86 | icon: React.PropTypes.string.isRequired, 87 | color: React.PropTypes.string, 88 | }; 89 | 90 | function mapStateToProps(state) { 91 | return { tab: state.navigation.tab, mode: state.playback.mode }; 92 | } 93 | 94 | export default connect(mapStateToProps)(Toolbar); 95 | 96 | -------------------------------------------------------------------------------- /src/jsx/Visualizer.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import { PINK, GRAY, GREEN } from '../colors'; 4 | 5 | import { analyser } from '../audio'; 6 | 7 | const wrapperStule = { 8 | flex: 1, 9 | }; 10 | 11 | export default class Visualizer extends React.Component { 12 | constructor() { 13 | super(); 14 | this.draw = this.draw.bind(this); 15 | this.onresize = this.onresize.bind(this); 16 | this.f = new Uint8Array(analyser.frequencyBinCount); 17 | this.t = new Uint8Array(analyser.frequencyBinCount); 18 | } 19 | componentDidMount() { 20 | this.context = this.refs.canvas.getContext('2d'); 21 | window.addEventListener('resize', this.onresize); 22 | this.onresize(); 23 | this.draw(); 24 | } 25 | componentWillUnmount() { 26 | window.removeEventListener('resize', this.onresize); 27 | } 28 | onresize() { 29 | this.width = this.refs.wrapper.offsetWidth; 30 | this.height = this.refs.wrapper.offsetHeight; 31 | this.forceUpdate(); 32 | } 33 | draw() { 34 | requestAnimationFrame(this.draw); 35 | this.context.fillStyle = GRAY; 36 | this.context.fillRect(0, 0, this.width, this.height); 37 | this.drawFFT(this.width, this.height); 38 | this.drawWaveForm(this.width, this.height); 39 | } 40 | drawFFT(width, height) { 41 | analyser.getByteFrequencyData(this.f); 42 | let x = 0; 43 | let v = 0; 44 | const sliceWidth = width / this.f.length; 45 | for (let i = 0; i < this.f.length; i++) { 46 | if (i % 10 === 0) { 47 | const y = (v * height * 0.45); 48 | this.context.fillStyle = PINK; 49 | this.context.fillRect(x, height / 2 - y / 20, 5 * sliceWidth, y / 10); 50 | v = 0; 51 | } 52 | v = v + this.f[i] / 256.0; 53 | x += sliceWidth; 54 | } 55 | } 56 | drawWaveForm(width, height) { 57 | analyser.getByteTimeDomainData(this.t); 58 | let x = 0; 59 | this.context.beginPath(); 60 | this.context.lineWidth = 2; 61 | this.context.strokeStyle = GREEN; 62 | const sliceWidth = width / this.t.length; 63 | for (let i = 0; i < this.t.length; i++) { 64 | const value = this.t[i] / 256; 65 | const y = height * 0.5 - (height * 0.45 * (value - 0.5)); 66 | if (i === 0) { 67 | this.context.moveTo(x, y); 68 | } else { 69 | this.context.lineTo(x, y); 70 | } 71 | x += sliceWidth; 72 | } 73 | this.context.stroke(); 74 | } 75 | render() { 76 | return (<div ref="wrapper" style={wrapperStule}> 77 | <canvas 78 | ref="canvas" 79 | width={this.width} 80 | height={this.height} 81 | style={{ width: '100%', height: this.height }} 82 | /> 83 | </div>); 84 | } 85 | } 86 | 87 | -------------------------------------------------------------------------------- /src/reducers.js: -------------------------------------------------------------------------------- 1 | import * as actions from './actions'; 2 | 3 | const defaultExpr = '(t*((3+(1^t>>10&5))*(5+(3&t>>14))))>>(t>>8&3)'; 4 | 5 | export const playbackMode = { 6 | STOPPED: 'stopped', 7 | PLAYING: 'playing', 8 | }; 9 | 10 | export const tab = { 11 | EDITOR: 'EDITOR', 12 | LIBRARY: 'LIBRARY', 13 | HELP: 'HELP', 14 | }; 15 | 16 | export function expr(state = { expr: defaultExpr, error: undefined }, action) { 17 | switch (action.type) { 18 | case actions.SET_EXPR: return Object.assign({}, state, { expr: action.expr }); 19 | case actions.ERROR: return Object.assign({}, state, { error: action.error }); 20 | default: return state; 21 | } 22 | } 23 | 24 | export function playback(state = { mode: playbackMode.STOPPED }, action) { 25 | switch (action.type) { 26 | case actions.PLAY: return Object.assign({}, state, { mode: playbackMode.PLAYING }); 27 | case actions.STOP: return Object.assign({}, state, { mode: playbackMode.STOPPED }); 28 | default: return state; 29 | } 30 | } 31 | 32 | export function navigation(state = { tab: tab.EDITOR }, action) { 33 | if (action.type === actions.NAVIGATE && tab[action.tab]) { 34 | return Object.assign({}, state, { tab: action.tab }); 35 | } 36 | return state; 37 | } 38 | 39 | -------------------------------------------------------------------------------- /src/roboto.css: -------------------------------------------------------------------------------- 1 | /* cyrillic-ext */ 2 | @font-face { 3 | font-family: 'Roboto Mono'; 4 | font-style: normal; 5 | font-weight: 400; 6 | src: local('Roboto Mono'), local('RobotoMono-Regular'), url(./fonts/robotomono/v4/hMqPNLsu_dywMa4C_DEpYzTOQ_MqJVwkKsUn0wKzc2I.woff2) format('woff2'); 7 | unicode-range: U+0460-052F, U+20B4, U+2DE0-2DFF, U+A640-A69F; 8 | } 9 | /* cyrillic */ 10 | @font-face { 11 | font-family: 'Roboto Mono'; 12 | font-style: normal; 13 | font-weight: 400; 14 | src: local('Roboto Mono'), local('RobotoMono-Regular'), url(./fonts/robotomono/v4/hMqPNLsu_dywMa4C_DEpYzUj_cnvWIuuBMVgbX098Mw.woff2) format('woff2'); 15 | unicode-range: U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116; 16 | } 17 | /* greek-ext */ 18 | @font-face { 19 | font-family: 'Roboto Mono'; 20 | font-style: normal; 21 | font-weight: 400; 22 | src: local('Roboto Mono'), local('RobotoMono-Regular'), url(./fonts/robotomono/v4/hMqPNLsu_dywMa4C_DEpY0bcKLIaa1LC45dFaAfauRA.woff2) format('woff2'); 23 | unicode-range: U+1F00-1FFF; 24 | } 25 | /* greek */ 26 | @font-face { 27 | font-family: 'Roboto Mono'; 28 | font-style: normal; 29 | font-weight: 400; 30 | src: local('Roboto Mono'), local('RobotoMono-Regular'), url(./fonts/robotomono/v4/hMqPNLsu_dywMa4C_DEpY2o_sUJ8uO4YLWRInS22T3Y.woff2) format('woff2'); 31 | unicode-range: U+0370-03FF; 32 | } 33 | /* vietnamese */ 34 | @font-face { 35 | font-family: 'Roboto Mono'; 36 | font-style: normal; 37 | font-weight: 400; 38 | src: local('Roboto Mono'), local('RobotoMono-Regular'), url(./fonts/robotomono/v4/hMqPNLsu_dywMa4C_DEpY76up8jxqWt8HVA3mDhkV_0.woff2) format('woff2'); 39 | unicode-range: U+0102-0103, U+1EA0-1EF1, U+20AB; 40 | } 41 | /* latin-ext */ 42 | @font-face { 43 | font-family: 'Roboto Mono'; 44 | font-style: normal; 45 | font-weight: 400; 46 | src: local('Roboto Mono'), local('RobotoMono-Regular'), url(./fonts/robotomono/v4/hMqPNLsu_dywMa4C_DEpYyYE0-AqJ3nfInTTiDXDjU4.woff2) format('woff2'); 47 | unicode-range: U+0100-024F, U+1E00-1EFF, U+20A0-20AB, U+20AD-20CF, U+2C60-2C7F, U+A720-A7FF; 48 | } 49 | /* latin */ 50 | @font-face { 51 | font-family: 'Roboto Mono'; 52 | font-style: normal; 53 | font-weight: 400; 54 | src: local('Roboto Mono'), local('RobotoMono-Regular'), url(./fonts/robotomono/v4/hMqPNLsu_dywMa4C_DEpY44P5ICox8Kq3LLUNMylGO4.woff2) format('woff2'); 55 | unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2212, U+2215, U+E0FF, U+EFFD, U+F000; 56 | } 57 | /* cyrillic-ext */ 58 | @font-face { 59 | font-family: 'Roboto Mono'; 60 | font-style: normal; 61 | font-weight: 700; 62 | src: local('Roboto Mono Bold'), local('RobotoMono-Bold'), url(./fonts/robotomono/v4/N4duVc9C58uwPiY8_59Fz56iIh_FvlUHQwED9Yt5Kbw.woff2) format('woff2'); 63 | unicode-range: U+0460-052F, U+20B4, U+2DE0-2DFF, U+A640-A69F; 64 | } 65 | /* cyrillic */ 66 | @font-face { 67 | font-family: 'Roboto Mono'; 68 | font-style: normal; 69 | font-weight: 700; 70 | src: local('Roboto Mono Bold'), local('RobotoMono-Bold'), url(./fonts/robotomono/v4/N4duVc9C58uwPiY8_59Fzy_vZmeiCMnoWNN9rHBYaTc.woff2) format('woff2'); 71 | unicode-range: U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116; 72 | } 73 | /* greek-ext */ 74 | @font-face { 75 | font-family: 'Roboto Mono'; 76 | font-style: normal; 77 | font-weight: 700; 78 | src: local('Roboto Mono Bold'), local('RobotoMono-Bold'), url(./fonts/robotomono/v4/N4duVc9C58uwPiY8_59FzyFaMxiho_5XQnyRZzQsrZs.woff2) format('woff2'); 79 | unicode-range: U+1F00-1FFF; 80 | } 81 | /* greek */ 82 | @font-face { 83 | font-family: 'Roboto Mono'; 84 | font-style: normal; 85 | font-weight: 700; 86 | src: local('Roboto Mono Bold'), local('RobotoMono-Bold'), url(./fonts/robotomono/v4/N4duVc9C58uwPiY8_59FzwalQocB-__pDVGhF3uS2Ks.woff2) format('woff2'); 87 | unicode-range: U+0370-03FF; 88 | } 89 | /* vietnamese */ 90 | @font-face { 91 | font-family: 'Roboto Mono'; 92 | font-style: normal; 93 | font-weight: 700; 94 | src: local('Roboto Mono Bold'), local('RobotoMono-Bold'), url(./fonts/robotomono/v4/N4duVc9C58uwPiY8_59Fz2hQUTDJGru-0vvUpABgH8I.woff2) format('woff2'); 95 | unicode-range: U+0102-0103, U+1EA0-1EF1, U+20AB; 96 | } 97 | /* latin-ext */ 98 | @font-face { 99 | font-family: 'Roboto Mono'; 100 | font-style: normal; 101 | font-weight: 700; 102 | src: local('Roboto Mono Bold'), local('RobotoMono-Bold'), url(./fonts/robotomono/v4/N4duVc9C58uwPiY8_59Fz-jkDdvhIIFj_YMdgqpnSB0.woff2) format('woff2'); 103 | unicode-range: U+0100-024F, U+1E00-1EFF, U+20A0-20AB, U+20AD-20CF, U+2C60-2C7F, U+A720-A7FF; 104 | } 105 | /* latin */ 106 | @font-face { 107 | font-family: 'Roboto Mono'; 108 | font-style: normal; 109 | font-weight: 700; 110 | src: local('Roboto Mono Bold'), local('RobotoMono-Bold'), url(./fonts/robotomono/v4/N4duVc9C58uwPiY8_59Fz4lIZu-HDpmDIZMigmsroc4.woff2) format('woff2'); 111 | unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2212, U+2215, U+E0FF, U+EFFD, U+F000; 112 | } 113 | -------------------------------------------------------------------------------- /src/samples/bd.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/naivesound/glitch-orig/a456917b36dc48d4dcfe37277f33bc6de1017a0d/src/samples/bd.wav -------------------------------------------------------------------------------- /src/samples/cb.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/naivesound/glitch-orig/a456917b36dc48d4dcfe37277f33bc6de1017a0d/src/samples/cb.wav -------------------------------------------------------------------------------- /src/samples/cl.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/naivesound/glitch-orig/a456917b36dc48d4dcfe37277f33bc6de1017a0d/src/samples/cl.wav -------------------------------------------------------------------------------- /src/samples/hh.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/naivesound/glitch-orig/a456917b36dc48d4dcfe37277f33bc6de1017a0d/src/samples/hh.wav -------------------------------------------------------------------------------- /src/samples/mc.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/naivesound/glitch-orig/a456917b36dc48d4dcfe37277f33bc6de1017a0d/src/samples/mc.wav -------------------------------------------------------------------------------- /src/samples/mt.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/naivesound/glitch-orig/a456917b36dc48d4dcfe37277f33bc6de1017a0d/src/samples/mt.wav -------------------------------------------------------------------------------- /src/samples/oh.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/naivesound/glitch-orig/a456917b36dc48d4dcfe37277f33bc6de1017a0d/src/samples/oh.wav -------------------------------------------------------------------------------- /src/samples/rs.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/naivesound/glitch-orig/a456917b36dc48d4dcfe37277f33bc6de1017a0d/src/samples/rs.wav -------------------------------------------------------------------------------- /src/samples/sn.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/naivesound/glitch-orig/a456917b36dc48d4dcfe37277f33bc6de1017a0d/src/samples/sn.wav -------------------------------------------------------------------------------- /src/save.js: -------------------------------------------------------------------------------- 1 | // Returns buffer filled with WAV samples of the given length produced by the 2 | // given function 3 | export function waveFile(sec, sampleRate, f) { 4 | const length = sec * sampleRate; 5 | const intBuffer = new Int16Array(length + 23); 6 | intBuffer[0] = 0x4952; // "RI" 7 | intBuffer[1] = 0x4646; // "FF" 8 | intBuffer[2] = (2 * length + 15) & 0x0000ffff; // RIFF size 9 | intBuffer[3] = ((2 * length + 15) & 0xffff0000) >> 16; // RIFF size 10 | intBuffer[4] = 0x4157; // "WA" 11 | intBuffer[5] = 0x4556; // "VE" 12 | intBuffer[6] = 0x6d66; // "fm" 13 | intBuffer[7] = 0x2074; // "t " 14 | intBuffer[8] = 0x0012; // fmt chunksize: 18 15 | intBuffer[9] = 0x0000; // 16 | intBuffer[10] = 0x0001; // format tag : 1 17 | intBuffer[11] = 1; // channels: 1 18 | intBuffer[12] = sampleRate & 0x0000ffff; // sample per sec 19 | intBuffer[13] = (sampleRate & 0xffff0000) >> 16; // sample per sec 20 | intBuffer[14] = (2 * sampleRate) & 0x0000ffff; // byte per sec 21 | intBuffer[15] = ((2 * sampleRate) & 0xffff0000) >> 16; // byte per sec 22 | intBuffer[16] = 0x0004; // block align 23 | intBuffer[17] = 0x0010; // bit per sample 24 | intBuffer[18] = 0x0000; // cb size 25 | intBuffer[19] = 0x6164; // "da" 26 | intBuffer[20] = 0x6174; // "ta" 27 | intBuffer[21] = (2 * length) & 0x0000ffff; // data size[byte] 28 | intBuffer[22] = ((2 * length) & 0xffff0000) >> 16; // data size[byte] 29 | 30 | for (let i = 0; i < length; i++) { 31 | intBuffer[i + 23] = Math.round(f(i) * (1 << 15)); 32 | } 33 | return intBuffer.buffer; 34 | } 35 | 36 | // Simulates clicking on a link to initiate file saving 37 | export function saveFile(name, data, contentType) { 38 | const blob = new Blob([data], { type: contentType }); 39 | const url = window.URL.createObjectURL(blob); 40 | const a = document.createElement('a'); 41 | document.body.appendChild(a); 42 | a.style = 'display: none'; 43 | a.href = url; 44 | a.download = name; 45 | a.click(); 46 | setTimeout(() => { 47 | document.body.removeChild(a); 48 | window.URL.revokeObjectURL(url); 49 | }, 100); 50 | } 51 | -------------------------------------------------------------------------------- /src/styles.css: -------------------------------------------------------------------------------- 1 | * { 2 | margin: 0; 3 | padding: 0; 4 | box-sizing: border-box; 5 | } 6 | a { 7 | color: #e91e63; 8 | text-decoration: none; 9 | } 10 | a:hover { 11 | color: #fff; 12 | } 13 | .help h1 { 14 | color: white; 15 | font-size: 1rem; 16 | line-height: 3rem; 17 | text-align: center; 18 | } 19 | .help p, .help ul { 20 | margin-bottom: 1rem; 21 | } 22 | .help em { 23 | color: grey; 24 | font-style: normal; 25 | } 26 | .help strong { 27 | color: white; 28 | font-style: normal; 29 | } 30 | ::-webkit-scrollbar { 31 | width: 4px; 32 | height: 4px; 33 | } 34 | ::-webkit-scrollbar-thumb { 35 | background: #ffc107; 36 | } 37 | ::-webkit-scrollbar-track { 38 | background: #444; 39 | } 40 | -------------------------------------------------------------------------------- /test/glitchFuncsTest.js: -------------------------------------------------------------------------------- 1 | import assert from 'assert'; 2 | 3 | import './mockAudio' 4 | import {sampleRate} from '../src/audio' 5 | import * as funcs from '../src/glitchFuncs' 6 | 7 | function n(x) { 8 | return function() { 9 | return x 10 | } 11 | } 12 | 13 | describe('Audio mock', function() { 14 | it('sample rate should be set to 44100', function() { 15 | assert.equal(sampleRate, 44100); 16 | }) 17 | }) 18 | 19 | describe('Glitch utils: s()', function() { 20 | it('s() returns denormalized sine wave', function() { 21 | assert.equal(funcs.s([n(0)]), 128) 22 | assert.equal(funcs.s([n(64)]), 255) 23 | assert.equal(Math.round(funcs.s([n(128)])), 128) 24 | assert.equal(Math.round(funcs.s([n(192)])), 1) 25 | }) 26 | it('s() is periodic', function() { 27 | assert.equal(Math.round(funcs.s([n(256)])), 128) 28 | assert.equal(Math.round(funcs.s([n(512)])), 128) 29 | assert.equal(Math.round(funcs.s([n(1024)])), 128) 30 | }) 31 | it('s() without args equals s(0)', function() { 32 | assert.equal(funcs.s([]), funcs.s([n(0)])) 33 | }) 34 | it('s(NaN) equals NaN', function() { 35 | assert(isNaN(funcs.s([n(NaN)]))) 36 | }) 37 | }) 38 | 39 | describe('Glitch utils: r()', function() { 40 | it('r() returns random numbers', function() { 41 | let a = funcs.r([]) 42 | let b = funcs.r([]) 43 | assert.notEqual(a, b) 44 | }) 45 | it('r() returns random numbers in the given range', function() { 46 | for (let i = 1; i < 1000; i++) { 47 | let r = funcs.r([n(i)]) 48 | assert(r >= 0, 'r >= 0') 49 | assert(r < i, 'r < i') 50 | } 51 | }) 52 | it('r(0) returns zero', function() { 53 | assert.equal(funcs.r([n(0)]), 0) 54 | }) 55 | it('r(NaN) returns NaN', function() { 56 | assert(isNaN(funcs.r([n(NaN)]))) 57 | }) 58 | it('r() without arguments returns random numbers in the range [0..255]', function() { 59 | let r = funcs.r([]) 60 | assert(r >= 0, 'r >= 0') 61 | assert(r < 255, 'r < 255') 62 | }) 63 | }) 64 | 65 | describe('Glitch utils: l()', function() { 66 | it('l() returns log2', function() { 67 | assert.equal(funcs.l([n(2)]), 1) 68 | assert.equal(funcs.l([n(4)]), 2) 69 | assert.equal(funcs.l([]), 0) 70 | assert(isNaN(funcs.l([n(NaN)]))) 71 | }) 72 | }) 73 | 74 | describe('Glitch utils: hz()', function() { 75 | it('hz() returns note frequency in hz', function() { 76 | assert.equal(funcs.hz([n(0)]), 440) 77 | assert.equal(Math.round(funcs.hz([n(1)])), 466) 78 | assert.equal(funcs.hz([n(12)]), 880) 79 | }) 80 | it('hz() without arguments returns 400 hz', function() { 81 | assert.equal(funcs.hz([]), 440) 82 | }) 83 | it('hz(NaN) returns NaN', function() { 84 | assert(isNaN(funcs.hz([n(NaN)]))) 85 | }) 86 | }) 87 | 88 | describe('Glitch utils: scale()', function() { 89 | // TODO: write tests after all basic scales are added 90 | }) 91 | 92 | describe('Glitch sequencer: a()', function() { 93 | it('a() returns element from the list of arguments at given index', function() { 94 | assert.equal(funcs.a([n(0), n(4), n(5), n(6)]), 4) 95 | assert.equal(funcs.a([n(1), n(4), n(5), n(6)]), 5) 96 | assert.equal(funcs.a([n(2), n(4), n(5), n(6)]), 6) 97 | }) 98 | it('a() positive index overflows', function() { 99 | assert.equal(funcs.a([n(3), n(4), n(5), n(6)]), 4) 100 | assert.equal(funcs.a([n(4), n(4), n(5), n(6)]), 5) 101 | assert.equal(funcs.a([n(5), n(4), n(5), n(6)]), 6) 102 | }) 103 | it('a() floors floating point index', function() { 104 | assert.equal(funcs.a([n(3.0), n(4), n(5), n(6)]), 4) 105 | assert.equal(funcs.a([n(3.1), n(4), n(5), n(6)]), 4) 106 | assert.equal(funcs.a([n(3.99), n(4), n(5), n(6)]), 4) 107 | }) 108 | it('a() negative index overflows', function() { 109 | assert.equal(funcs.a([n(-3), n(4), n(5), n(6)]), 4) 110 | assert.equal(funcs.a([n(-2), n(4), n(5), n(6)]), 5) 111 | assert.equal(funcs.a([n(-1), n(4), n(5), n(6)]), 6) 112 | assert.equal(funcs.a([n(-100), n(4), n(5), n(6)]), 6) 113 | }) 114 | it('a() without arguments returns zero', function() { 115 | assert.equal(funcs.a([]), 0) 116 | }) 117 | it('a() with no elements returns zero', function() { 118 | assert.equal(funcs.a([n(0)]), 0) 119 | assert.equal(funcs.a([n(1000)]), 0) 120 | assert.equal(funcs.a([n(-1000)]), 0) 121 | }) 122 | it('a() can return NaN', function() { 123 | let x = funcs.a([n(1), n(1), n(NaN), n(2)]) 124 | assert(isNaN(funcs.a([n(1), n(1), n(NaN), n(2)]))) 125 | }) 126 | it('a() returns NaN if index is NaN', function() { 127 | assert(isNaN(funcs.a([n(NaN), n(1), n(2), n(3)]))) 128 | assert(isNaN(funcs.a([n(NaN)]))) 129 | }) 130 | }) 131 | 132 | describe('Glitch sequencer: loop()', function() { 133 | it('produces NaN on each beat evaluating current step once per beat', function() { 134 | let loop = funcs.loop.bind({}) 135 | let i = 0 136 | let incr = function() { 137 | return i++ 138 | } 139 | let bpm = sampleRate * 60 / 4 140 | assert(isNaN(loop([n(bpm), incr, n(2)])), '1/4+0') 141 | assert.equal(loop([n(bpm), incr, n(2)]), 1, '2/4+0') 142 | assert.equal(loop([n(bpm), incr, n(2)]), 2, '3/4+0') 143 | assert.equal(loop([n(bpm), incr, n(2)]), 3, '4/4+0') 144 | assert(isNaN(loop([n(bpm), incr, n(2)])), '1/4+1') 145 | assert.equal(loop([n(bpm), incr, n(2)]), 2, '2/4+1') 146 | assert.equal(loop([n(bpm), incr, n(2)]), 2, '3/4+1') 147 | assert.equal(loop([n(bpm), incr, n(2)]), 2, '4/4+1') 148 | assert(isNaN(loop([n(bpm), incr, n(2)])), '1/4+2') 149 | assert.equal(loop([n(bpm), incr, n(2)]), 5, '2/4+2') 150 | assert.equal(loop([n(bpm), incr, n(2)]), 6, '3/4+2') 151 | assert.equal(loop([n(bpm), incr, n(2)]), 7, '4/4+2') 152 | assert(isNaN(loop([n(bpm), incr, n(2)])), '1/4+3') 153 | }) 154 | }) 155 | 156 | describe('Glitch sequencer: seq()', function() { 157 | it('produces NaN on each beat evaluating current step on each call', function() { 158 | let seq = funcs.seq.bind({}) 159 | let i = 0 160 | let incr = function() { 161 | return i++ 162 | } 163 | let bpm = sampleRate * 60 / 4 164 | assert(isNaN(seq([n(bpm), incr, n(2)])), '1/4+0') 165 | assert.equal(seq([n(bpm), incr, n(2)]), 0, '2/4+0') 166 | assert.equal(seq([n(bpm), incr, n(2)]), 0, '3/4+0') 167 | assert.equal(seq([n(bpm), incr, n(2)]), 0, '4/4+0') 168 | assert(isNaN(seq([n(bpm), incr, n(2)])), '1/4+1') 169 | assert.equal(seq([n(bpm), incr, n(2)]), 2, '2/4+1') 170 | assert.equal(seq([n(bpm), incr, n(2)]), 2, '3/4+1') 171 | assert.equal(seq([n(bpm), incr, n(2)]), 2, '4/4+1') 172 | assert(isNaN(seq([n(bpm), incr, n(2)])), '1/4+2') 173 | assert.equal(seq([n(bpm), incr, n(2)]), 1, '2/4+2') 174 | assert.equal(seq([n(bpm), incr, n(2)]), 1, '3/4+2') 175 | assert.equal(seq([n(bpm), incr, n(2)]), 1, '4/4+2') 176 | }) 177 | }) 178 | 179 | describe('Glitch instrument: sin()', function() { 180 | it('produces sine wave of the given frequency with amplitude 0..255', function() { 181 | let sin = funcs.sin.bind({}) 182 | let freq = n(sampleRate/4) 183 | let values = [128, 255, 128, 1, 128, 255, 128, 1] 184 | for (let gain of values) { 185 | let v = Math.round(sin([freq])) 186 | assert.equal(v, gain) 187 | } 188 | }) 189 | it('returns NaN when frequency is NaN', function() { 190 | let sin = funcs.sin.bind({}) 191 | assert(isNaN(sin([n(NaN)]))) 192 | }) 193 | it('applies frequency change on the next cycle', function() { 194 | // TODO 195 | }) 196 | }) 197 | 198 | describe('Glitch instrument: tri()', function() { 199 | it('produces triangular wave of the given frequency with amplitude 0..255', function() { 200 | let tri = funcs.tri.bind({}) 201 | let freq = n(sampleRate/4) 202 | let values = [128, 255, 128, 1, 128, 255, 128, 1] 203 | for (let gain of values) { 204 | let v = Math.round(tri([freq])) 205 | assert.equal(v, gain) 206 | } 207 | }) 208 | it('returns NaN when frequency is NaN', function() { 209 | let tri = funcs.tri.bind({}) 210 | assert(isNaN(tri([n(NaN)]))) 211 | }) 212 | it('applies frequency change on the next cycle', function() { 213 | // TODO 214 | }) 215 | }) 216 | 217 | describe('Glitch instrument: saw()', function() { 218 | it('produces sawtooth wave of the given frequency with amplitude 0..255', function() { 219 | let saw = funcs.saw.bind({}) 220 | let freq = n(sampleRate/4) 221 | let values = [128, 192, 1, 65, 128, 192, 1, 65] 222 | for (let gain of values) { 223 | let v = Math.round(saw([freq])) 224 | assert.equal(v, gain) 225 | } 226 | }) 227 | it('returns NaN when frequency is NaN', function() { 228 | let saw = funcs.saw.bind({}) 229 | assert(isNaN(saw([n(NaN)]))) 230 | }) 231 | it('applies frequency change on the next cycle', function() { 232 | // TODO 233 | }) 234 | }) 235 | 236 | describe('Glitch instrument: sqr()', function() { 237 | it('produces square wave of the given frequency with amplitude 0..255', function() { 238 | let sqr = funcs.sqr.bind({}) 239 | let freq = n(sampleRate/4) 240 | let values = [255, 255, 1, 1, 255, 255, 1, 1] 241 | for (let gain of values) { 242 | let v = Math.round(sqr([freq])) 243 | assert.equal(v, gain) 244 | } 245 | }) 246 | it('produces square wave of custom width', function() { 247 | let sqr = funcs.sqr.bind({}) 248 | let freq = n(sampleRate/4) 249 | let values = [255, 255, 255, 1, 255, 255, 255, 1] 250 | for (let gain of values) { 251 | let v = Math.round(sqr([freq, n(0.75)])) 252 | assert.equal(v, gain) 253 | } 254 | }) 255 | it('returns NaN when frequency is NaN', function() { 256 | let sqr = funcs.sqr.bind({}) 257 | assert(isNaN(sqr([n(NaN)]))) 258 | }) 259 | it('applies frequency change on the next cycle', function() { 260 | // TODO 261 | }) 262 | }) 263 | 264 | describe('Glitch instrument: fm()', function() { 265 | }) 266 | 267 | describe('Glitch effect: env()', function() { 268 | it('returns 128 if called without arguments', function() { 269 | let env = funcs.env.bind({}) 270 | assert.equal(env([]), 128) 271 | }) 272 | it('returns unmodified signal if no envelope is given', function() { 273 | let env = funcs.env.bind({}) 274 | assert.equal(env([n(213)]), 213) 275 | assert.equal(env([n(132)]), 132) 276 | }) 277 | it('default attack time is 0.0625', function() { 278 | let env1 = funcs.env.bind({}) 279 | let env2 = funcs.env.bind({}) 280 | for (let i = 0; i < 1000; i++) { 281 | assert.equal(env1([n(0.5), n(200)]), env2([n(0.0625), n(0.5), n(200)])) 282 | } 283 | }) 284 | it('handles odd number of variadic arguments', function() { 285 | let env1 = funcs.env.bind({}) 286 | let env2 = funcs.env.bind({}) 287 | for (let i = 0; i < 1000; i++) { 288 | let v1 = env1([n(10/sampleRate), n(100/sampleRate), n(200)]) 289 | let v2 = env2([n(10/sampleRate), n(100/sampleRate), n(0), n(200)]) 290 | if (isNaN(v1)) { 291 | assert(isNaN(v2)) 292 | console.log('NaN at', i) 293 | } else { 294 | assert.equal(v1, v2) 295 | } 296 | } 297 | }) 298 | it('handles custom envelope forms with variadic arguments', function() { 299 | let env = funcs.env.bind({}) 300 | function customEnv() { 301 | return env([n(100/sampleRate), 302 | n(100/sampleRate), n(0.1), 303 | n(100/sampleRate), n(0.8), 304 | n(100/sampleRate), n(0.5), 305 | n(200)]) 306 | } 307 | let v = 0 308 | for (let seg = 0; seg < 4; seg++) { 309 | for (let i = 0; i < 100; i++) { 310 | let u = customEnv() 311 | if (seg % 2 == 0) { 312 | assert(Math.ceil(u) >= Math.floor(v)) 313 | } else { 314 | assert(Math.floor(u) <= Math.ceil(v)) 315 | } 316 | v = u 317 | } 318 | } 319 | }) 320 | }) 321 | 322 | describe('Glitch effect: mix()', function() { 323 | it('sums up input values', function() { 324 | let mix = funcs.mix.bind({}) 325 | assert.equal(mix([]), 128) 326 | assert.equal(mix([n(4)]), 4) 327 | assert.equal(mix([n(128), n(128), n(128)]), 128) 328 | assert.equal(Math.round(mix([n(200), n(100), n(32)])), 98) 329 | }) 330 | it('cuts amplitude to avoid overflow', function() { 331 | let mix = funcs.mix.bind({}) 332 | assert.equal(Math.round(mix([n(1000), n(100000), n(10000)])), 255) 333 | assert.equal(Math.round(mix([n(-1000), n(-100000), n(-10000)])), 1) 334 | }) 335 | it('replaces NaN with last known sample value', function() { 336 | let mix = funcs.mix.bind({}) 337 | assert.equal(mix([n(4)]), 4) 338 | assert.equal(mix([n(NaN)]), 4) 339 | assert.equal(mix([n(5)]), 5) 340 | assert.equal(mix([n(NaN)]), 5) 341 | }) 342 | }) 343 | 344 | describe('Glitch effect: lpf()', function() { 345 | it('modifies signal', function() { 346 | let sin = funcs.sin.bind({}) 347 | let lpf = funcs.lpf.bind({}) 348 | let f = n(40) 349 | let cutoff = n(10000) 350 | let err = 0 351 | for (let i = 0; i < 1000; i++) { 352 | let signal = sin([f]) 353 | err = err + Math.abs(lpf([n(signal), cutoff]) - signal) 354 | } 355 | assert(err > 0) 356 | }) 357 | it('distorts signal depending on cutoff frequency', function() { 358 | let sin = funcs.sin.bind({}) 359 | let lpf1 = funcs.lpf.bind({}) 360 | let lpf2 = funcs.lpf.bind({}) 361 | let f = n(120) 362 | let cutoff1 = n(1000) 363 | let cutoff2 = n(100) 364 | let err1 = 0 365 | let err2 = 0 366 | for (let i = 0; i < 1000; i++) { 367 | let signal = sin([f]) 368 | err1 = err1 + Math.abs(lpf1([n(signal), cutoff1]) - signal) 369 | err2 = err2 + Math.abs(lpf2([n(signal), cutoff2]) - signal) 370 | } 371 | assert(err2 > err1) 372 | }) 373 | it('mutes signal when cutoff frequency is zero', function() { 374 | let sin = funcs.sin.bind({}) 375 | let lpf = funcs.lpf.bind({}) 376 | let f = n(440) 377 | let cutoff = n(0) 378 | for (let i = 0; i < 1000; i++) { 379 | assert.equal(lpf([n(sin([f])), cutoff]), 128) 380 | } 381 | }) 382 | }) 383 | 384 | -------------------------------------------------------------------------------- /test/mockAudio.js: -------------------------------------------------------------------------------- 1 | class AudioContext { 2 | constructor() { 3 | this.sampleRate = 44100 4 | } 5 | createAnalyser() { 6 | return new Analyser() 7 | } 8 | createScriptProcessor() { 9 | return {} 10 | } 11 | } 12 | 13 | class Analyser { 14 | connect() {} 15 | } 16 | 17 | global.AudioContext = AudioContext 18 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | var path = require('path'); 2 | var webpack = require('webpack'); 3 | 4 | require('es6-promise').polyfill(); 5 | 6 | var config = { 7 | entry: path.resolve(__dirname, 'src/index.js'), 8 | output: { 9 | path: path.resolve(__dirname, 'build'), 10 | filename: 'bundle.js' 11 | }, 12 | module: { 13 | loaders: [ 14 | {test: /\.html$/, loader: 'file?name=[name].[ext]'}, 15 | {test: /\.png$/, loader: 'file?name=[name].[ext]'}, 16 | {test: /\.jsx?$/, exclude: /node_modules/, loader: 'babel', query: {presets: ['react', 'es2015']}}, 17 | {test: /\.css$/, loader: 'style!css'}, 18 | {test: /\.woff(\?v=\d+\.\d+\.\d+)?$/, loader: "url?limit=10000&mimetype=application/font-woff"}, 19 | {test: /\.woff2(\?v=\d+\.\d+\.\d+)?$/, loader: "url?limit=10000&mimetype=application/font-woff"}, 20 | {test: /\.ttf(\?v=\d+\.\d+\.\d+)?$/, loader: "url?limit=10000&mimetype=application/octet-stream"}, 21 | {test: /\.eot(\?v=\d+\.\d+\.\d+)?$/, loader: "file"}, 22 | {test: /\.svg(\?v=\d+\.\d+\.\d+)?$/, loader: "url?limit=10000&mimetype=image/svg+xml"}, 23 | {test: /\.wav$/, loader: "base64"}, 24 | ] 25 | }, 26 | plugins: [ 27 | new webpack.DefinePlugin({ 28 | 'process.env': { 29 | 'NODE_ENV': JSON.stringify(process.env.NODE_ENV) 30 | } 31 | }) 32 | ] 33 | }; 34 | 35 | module.exports = config; 36 | --------------------------------------------------------------------------------