├── .gitignore ├── logo.png ├── screenshot.jpg ├── public ├── audio │ └── acoustic │ │ ├── kick1.mp3 │ │ ├── kick2.mp3 │ │ ├── kick3.mp3 │ │ ├── rim1.mp3 │ │ ├── hihat1.mp3 │ │ ├── hihat2.mp3 │ │ ├── snare1.mp3 │ │ ├── snare2.mp3 │ │ ├── snare3.mp3 │ │ ├── hihat_open1.mp3 │ │ ├── hihat_open2.mp3 │ │ └── hihat_open3.mp3 ├── index.css └── index.html ├── helpers ├── mtof.js ├── tuna.js ├── context.js ├── drumMap.js ├── List.js ├── BufferLoader.js ├── parseArguments.js └── FunctionCall.js ├── .eslintrc ├── functions ├── reverse.js ├── shuffle.js ├── FunctionCall.js ├── random.js ├── flatten.js ├── interpolate.js ├── index.js ├── repeat.js ├── chord.js └── transpose.js ├── webpack.config.js ├── classes ├── classMap.js ├── Gain.js ├── Pan.js ├── Delay.js ├── PolyBlock.js ├── Osc.js ├── Filter.js ├── Drums.js ├── Block.js ├── Scheduler.js ├── ADSR.js └── Sound.js ├── webpack.prod.js ├── backend ├── helpers.js ├── views │ └── patch.ejs └── server.js ├── LICENSE.md ├── package.json ├── runtime.js ├── notes.todo ├── slang.ohm ├── slang-grammar.js ├── editor.js ├── slang.js └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules/ 3 | build/ 4 | deploy.sh 5 | -------------------------------------------------------------------------------- /logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kylestetz/slang/HEAD/logo.png -------------------------------------------------------------------------------- /screenshot.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kylestetz/slang/HEAD/screenshot.jpg -------------------------------------------------------------------------------- /public/audio/acoustic/kick1.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kylestetz/slang/HEAD/public/audio/acoustic/kick1.mp3 -------------------------------------------------------------------------------- /public/audio/acoustic/kick2.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kylestetz/slang/HEAD/public/audio/acoustic/kick2.mp3 -------------------------------------------------------------------------------- /public/audio/acoustic/kick3.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kylestetz/slang/HEAD/public/audio/acoustic/kick3.mp3 -------------------------------------------------------------------------------- /public/audio/acoustic/rim1.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kylestetz/slang/HEAD/public/audio/acoustic/rim1.mp3 -------------------------------------------------------------------------------- /helpers/mtof.js: -------------------------------------------------------------------------------- 1 | export default function mtof(note) { 2 | return ( Math.pow(2, ( note-69 ) / 12) ) * 440.0; 3 | } 4 | -------------------------------------------------------------------------------- /public/audio/acoustic/hihat1.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kylestetz/slang/HEAD/public/audio/acoustic/hihat1.mp3 -------------------------------------------------------------------------------- /public/audio/acoustic/hihat2.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kylestetz/slang/HEAD/public/audio/acoustic/hihat2.mp3 -------------------------------------------------------------------------------- /public/audio/acoustic/snare1.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kylestetz/slang/HEAD/public/audio/acoustic/snare1.mp3 -------------------------------------------------------------------------------- /public/audio/acoustic/snare2.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kylestetz/slang/HEAD/public/audio/acoustic/snare2.mp3 -------------------------------------------------------------------------------- /public/audio/acoustic/snare3.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kylestetz/slang/HEAD/public/audio/acoustic/snare3.mp3 -------------------------------------------------------------------------------- /public/audio/acoustic/hihat_open1.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kylestetz/slang/HEAD/public/audio/acoustic/hihat_open1.mp3 -------------------------------------------------------------------------------- /public/audio/acoustic/hihat_open2.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kylestetz/slang/HEAD/public/audio/acoustic/hihat_open2.mp3 -------------------------------------------------------------------------------- /public/audio/acoustic/hihat_open3.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kylestetz/slang/HEAD/public/audio/acoustic/hihat_open3.mp3 -------------------------------------------------------------------------------- /helpers/tuna.js: -------------------------------------------------------------------------------- 1 | import Tuna from 'tunajs'; 2 | import context from './context'; 3 | 4 | const tuna = new Tuna(context); 5 | 6 | export default tuna; 7 | -------------------------------------------------------------------------------- /helpers/context.js: -------------------------------------------------------------------------------- 1 | let context; 2 | 3 | if (window.webkitAudioContext) { 4 | context = new webkitAudioContext(); 5 | } else { 6 | context = new AudioContext(); 7 | } 8 | 9 | export default context; 10 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parserOptions": { 3 | "ecmaFeatures": { 4 | "experimentalObjectRestSpread": true 5 | }, 6 | "sourceType": "module" 7 | }, 8 | "env": { 9 | "es6": true 10 | }, 11 | "rules": { 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /functions/reverse.js: -------------------------------------------------------------------------------- 1 | import { parseArgument } from '../helpers/parseArguments'; 2 | import FunctionCall from './FunctionCall'; 3 | import List from '../helpers/List'; 4 | 5 | export default class Reverse extends FunctionCall { 6 | constructor(functionObject) { 7 | super(functionObject); 8 | this.data = new List(this.arguments[0].toArray().reverse()); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | 3 | module.exports = { 4 | mode: 'development', 5 | entry: './editor.js', 6 | output: { 7 | filename: 'site.js', 8 | path: path.resolve(__dirname, 'public/build') 9 | }, 10 | module: { 11 | rules: [{ 12 | test: /\.css$/, 13 | use: [ 'style-loader', 'css-loader' ] 14 | }], 15 | }, 16 | }; 17 | -------------------------------------------------------------------------------- /functions/shuffle.js: -------------------------------------------------------------------------------- 1 | import shuffle from 'lodash/shuffle'; 2 | import { parseArgument } from '../helpers/parseArguments'; 3 | import FunctionCall from './FunctionCall'; 4 | import List from '../helpers/List'; 5 | 6 | export default class Shuffle extends FunctionCall { 7 | constructor(functionObject) { 8 | super(functionObject); 9 | this.data = new List(_.shuffle(this.arguments[0].toArray())); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /classes/classMap.js: -------------------------------------------------------------------------------- 1 | import Osc from './Osc'; 2 | import Drums from './Drums'; 3 | import Filter from './Filter'; 4 | import ADSR from './ADSR'; 5 | import Gain from './Gain'; 6 | import Pan from './Pan'; 7 | import Delay from './Delay'; 8 | 9 | const classMap = { 10 | 'osc': Osc, 11 | 'drums': Drums, 12 | 'filter': Filter, 13 | 'adsr': ADSR, 14 | 'gain': Gain, 15 | 'pan': Pan, 16 | 'delay': Delay, 17 | }; 18 | 19 | export default classMap; 20 | -------------------------------------------------------------------------------- /functions/FunctionCall.js: -------------------------------------------------------------------------------- 1 | import parseArguments from '../helpers/parseArguments'; 2 | 3 | class FunctionCall { 4 | constructor(functionObject) { 5 | this.type = functionObject.function; 6 | this.arguments = parseArguments(functionObject.arguments); 7 | } 8 | next() { 9 | if (!this.data) throw new Error(`Function ${this.type} forgot to set this.data`); 10 | return this.data.next(); 11 | } 12 | toArray() { 13 | return this.data.toArray(); 14 | } 15 | } 16 | 17 | export default FunctionCall; -------------------------------------------------------------------------------- /functions/random.js: -------------------------------------------------------------------------------- 1 | import FunctionCall from './FunctionCall'; 2 | import { parseArgument } from '../helpers/parseArguments'; 3 | 4 | export default class Random extends FunctionCall { 5 | constructor(functionObject) { 6 | super(functionObject); 7 | 8 | if (this.arguments[0] && this.arguments[0].toArray) { 9 | this.data = this.arguments[0].toArray(); 10 | } else { 11 | this.data = parseArgument(this.arguments[0]); 12 | } 13 | } 14 | next() { 15 | return this.data[ 16 | Math.floor(Math.random() * this.data.length) 17 | ].next(); 18 | } 19 | } -------------------------------------------------------------------------------- /functions/flatten.js: -------------------------------------------------------------------------------- 1 | import { flatMap } from 'lodash'; 2 | import FunctionCall from './FunctionCall'; 3 | import { parseArgument } from '../helpers/parseArguments'; 4 | import List from '../helpers/List'; 5 | 6 | export default class Flatten extends FunctionCall { 7 | constructor(functionObject) { 8 | super(functionObject); 9 | 10 | let data; 11 | 12 | // All arguments must be arrays. 13 | data = flatMap(this.arguments[0].toArray(), (arg) => { 14 | if (arg.toArray) return arg.toArray(); 15 | return arg; 16 | }); 17 | 18 | this.data = new List(data); 19 | } 20 | } -------------------------------------------------------------------------------- /webpack.prod.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | 3 | module.exports = { 4 | mode: 'production', 5 | entry: './editor.js', 6 | output: { 7 | filename: 'site.min.js', 8 | path: path.resolve(__dirname, 'public/build') 9 | }, 10 | module: { 11 | rules: [{ 12 | test: /\.css$/, 13 | use: [ 'style-loader', 'css-loader' ] 14 | }, 15 | { 16 | test: /\.m?js$/, 17 | exclude: /(node_modules|bower_components)/, 18 | use: { 19 | loader: 'babel-loader', 20 | options: { 21 | presets: ['@babel/preset-env'] 22 | } 23 | } 24 | }], 25 | }, 26 | }; 27 | -------------------------------------------------------------------------------- /backend/helpers.js: -------------------------------------------------------------------------------- 1 | // Returns a number between the min and max. 2 | function randomRange(min, max){ 3 | return Math.floor(Math.random() * (max - min + 1)) + min; 4 | } 5 | 6 | // Creates an alphanumeric string 6 characters long. 7 | // This is long enough to avoid random collisions without 8 | // being totally obnoxious to look at. 9 | function createHash() { 10 | var newHash = ''; 11 | 12 | for (var i = 0; i < 6; i++) { 13 | var digit; 14 | if (Math.random() < 0.5){ 15 | digit = String(randomRange(0,9)); 16 | } else { 17 | digit = String.fromCharCode(randomRange(97,122)); 18 | } 19 | newHash = newHash + digit; 20 | } 21 | return newHash; 22 | } 23 | 24 | module.exports = { 25 | createHash: createHash, 26 | randomRange: randomRange, 27 | }; 28 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | ## ISC License 2 | 3 | Copyright (c) 2018 Kyle Stetz (kylestetz.com) 4 | 5 | Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 8 | -------------------------------------------------------------------------------- /functions/interpolate.js: -------------------------------------------------------------------------------- 1 | import shuffle from 'lodash/shuffle'; 2 | import { parseArgument } from '../helpers/parseArguments'; 3 | import FunctionCall from './FunctionCall'; 4 | import List from '../helpers/List'; 5 | 6 | export default class Interpolate extends FunctionCall { 7 | constructor(functionObject) { 8 | super(functionObject); 9 | const fromValue = this.arguments[0].next(); 10 | const toValue = this.arguments[1].next(); 11 | const steps = this.arguments[2].next(); 12 | 13 | const stepInterval = (toValue - fromValue) / (steps - 2); 14 | 15 | let values = [fromValue]; 16 | for (let i = 1; i < steps; i++) { 17 | values.push(fromValue * (1 - (i / (steps - 1))) + toValue * (i / (steps - 1))); 18 | } 19 | 20 | this.data = new List(values); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /functions/index.js: -------------------------------------------------------------------------------- 1 | import Random from './random'; 2 | import Chord from './chord'; 3 | import Repeat from './repeat'; 4 | import Flatten from './flatten'; 5 | import Reverse from './reverse'; 6 | import Shuffle from './shuffle'; 7 | import Transpose from './transpose'; 8 | import Interpolate from './interpolate'; 9 | 10 | export const functionMap = { 11 | 'random': Random, 12 | 'chord': Chord, 13 | 'repeat': Repeat, 14 | 'flatten': Flatten, 15 | 'reverse': Reverse, 16 | 'shuffle': Shuffle, 17 | 'transpose': Transpose, 18 | 'lerp': Interpolate, 19 | }; 20 | 21 | export default function(functionObject) { 22 | if (functionMap[functionObject.function]) { 23 | return new functionMap[functionObject.function](functionObject); 24 | } 25 | 26 | throw new Error(`Function ${functionObject.function} does not exist`); 27 | } 28 | -------------------------------------------------------------------------------- /functions/repeat.js: -------------------------------------------------------------------------------- 1 | import FunctionCall from './FunctionCall'; 2 | import { parseArgument } from '../helpers/parseArguments'; 3 | import List from '../helpers/List'; 4 | 5 | export default class Random extends FunctionCall { 6 | constructor(functionObject) { 7 | super(functionObject); 8 | 9 | let data; 10 | this.data = []; 11 | 12 | // The second argument will be either an array or 13 | // a FunctionCall object. Since we can only repeat 14 | // JS arrays, call `toArray` on a FunctionCall object 15 | // to get something we can work with. 16 | if (this.arguments[1] && this.arguments[1].toArray) { 17 | data = this.arguments[1].toArray(); 18 | } else { 19 | data = this.arguments[1]; 20 | } 21 | 22 | const repeat = this.arguments[0].next(); 23 | 24 | for (let i = 0; i < repeat; i++) { 25 | this.data = this.data.concat(data); 26 | } 27 | 28 | this.data = new List(this.data); 29 | } 30 | } -------------------------------------------------------------------------------- /classes/Gain.js: -------------------------------------------------------------------------------- 1 | import Block from './Block'; 2 | import context from '../helpers/context'; 3 | import { parseArgument } from '../helpers/parseArguments'; 4 | 5 | class Gain extends Block { 6 | constructor(...args) { 7 | super(...args); 8 | 9 | this.level = this.arguments[0] || parseArgument(1); 10 | } 11 | 12 | instantiate() { 13 | if (this.getPolyMode()) return; 14 | 15 | this.gain = context.createGain(); 16 | this.gain.gain.setValueAtTime(1, context.currentTime, 0); 17 | 18 | this.getInput().connect(this.gain); 19 | this.gain.connect(this.getOutput()); 20 | } 21 | 22 | schedule(start) { 23 | if (!this.getPolyMode()) { 24 | this.gain.gain.setValueAtTime(this.level.next(), context.currentTime, 0); 25 | return; 26 | } 27 | 28 | const gain = context.createGain(); 29 | gain.gain.setValueAtTime(this.level.next(), start, 0); 30 | 31 | return { 32 | input: gain, 33 | output: gain, 34 | }; 35 | } 36 | } 37 | 38 | export default Gain; 39 | -------------------------------------------------------------------------------- /classes/Pan.js: -------------------------------------------------------------------------------- 1 | import StereoPannerNode from 'stereo-panner-node'; 2 | import Block from './Block'; 3 | import context from '../helpers/context'; 4 | import { parseArgument } from '../helpers/parseArguments'; 5 | 6 | StereoPannerNode.polyfill(); 7 | 8 | class Pan extends Block { 9 | constructor(...args) { 10 | super(...args); 11 | 12 | this.value = this.arguments[0] || parseArgument(0); 13 | } 14 | 15 | instantiate() { 16 | if (this.getPolyMode()) return; 17 | 18 | this.pan = context.createStereoPanner(); 19 | this.pan.pan.setValueAtTime(0, context.currentTime, 0); 20 | 21 | this.getInput().connect(this.pan); 22 | this.pan.connect(this.getOutput()); 23 | } 24 | 25 | schedule(start) { 26 | if (!this.getPolyMode()) { 27 | this.pan.pan.setValueAtTime(this.value.next(), context.currentTime, 0); 28 | return; 29 | } 30 | 31 | const pan = context.createStereoPanner(); 32 | pan.pan.setValueAtTime(this.value.next(), context.currentTime, 0); 33 | 34 | return { 35 | input: pan, 36 | output: pan, 37 | }; 38 | } 39 | } 40 | 41 | export default Pan; 42 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "slang", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "watch": "webpack -d --watch", 8 | "build": "webpack --config webpack.prod.js" 9 | }, 10 | "author": "", 11 | "license": "ISC", 12 | "dependencies": { 13 | "adsr-envelope": "^1.0.0", 14 | "body-parse": "^0.1.0", 15 | "codemirror": "^5.38.0", 16 | "ejs": "^2.6.1", 17 | "express": "^4.16.3", 18 | "lodash": "^4.17.10", 19 | "mongodb": "^2.0.30", 20 | "ohm-js": "^0.14.0", 21 | "stereo-panner-node": "^1.4.0", 22 | "tonal": "^1.1.3", 23 | "tonal-range": "^1.1.2", 24 | "tonal-scale": "^1.1.2", 25 | "tunajs": "^1.0.1" 26 | }, 27 | "devDependencies": { 28 | "@babel/core": "^7.1.2", 29 | "@babel/preset-env": "^7.1.0", 30 | "babel-loader": "^8.0.4", 31 | "css-loader": "^0.28.11", 32 | "eslint": "^4.19.1", 33 | "eslint-config-airbnb-base": "^12.1.0", 34 | "eslint-plugin-import": "^2.12.0", 35 | "style-loader": "^0.21.0", 36 | "webpack": "^4.10.1", 37 | "webpack-cli": "^2.1.4" 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /helpers/drumMap.js: -------------------------------------------------------------------------------- 1 | const drumMap = { 2 | 0: { 3 | file: '/audio/acoustic/kick1.mp3', 4 | label: 'acoustic kick 1' 5 | }, 6 | 1: { 7 | file: '/audio/acoustic/kick2.mp3', 8 | label: 'acoustic kick 2' 9 | }, 10 | 2: { 11 | file: '/audio/acoustic/kick3.mp3', 12 | label: 'acoustic kick 3' 13 | }, 14 | 3: { 15 | file: '/audio/acoustic/snare1.mp3', 16 | label: 'acoustic snare 1' 17 | }, 18 | 4: { 19 | file: '/audio/acoustic/snare2.mp3', 20 | label: 'acoustic snare 2' 21 | }, 22 | 5: { 23 | file: '/audio/acoustic/snare3.mp3', 24 | label: 'acoustic snare 3' 25 | }, 26 | 6: { 27 | file: '/audio/acoustic/hihat1.mp3', 28 | label: 'acoustic hat 1' 29 | }, 30 | 7: { 31 | file: '/audio/acoustic/hihat2.mp3', 32 | label: 'acoustic hat 2' 33 | }, 34 | 8: { 35 | file: '/audio/acoustic/hihat_open1.mp3', 36 | label: 'acoustic hat (open) 1' 37 | }, 38 | 9: { 39 | file: '/audio/acoustic/hihat_open2.mp3', 40 | label: 'acoustic hat (open) 2' 41 | }, 42 | 10: { 43 | file: '/audio/acoustic/hihat_open3.mp3', 44 | label: 'acoustic hat (open) 3' 45 | }, 46 | 11: { 47 | file: '/audio/acoustic/rim1.mp3', 48 | label: 'acoustic rim' 49 | }, 50 | }; 51 | 52 | export default drumMap; 53 | -------------------------------------------------------------------------------- /helpers/List.js: -------------------------------------------------------------------------------- 1 | import parseArguments from './parseArguments'; 2 | 3 | /* 4 | LIST class 5 | The list class takes care of what would otherwise 6 | be a huge pain: dealing with potentially recursive 7 | lists created by nesting functions. This class will 8 | take the data generated during the `.toAST` process 9 | and give our runtime two convenient methods: 10 | 11 | toArray - returns a flat JS array 12 | next - allows us to cycle through the list without 13 | caring about how long it is, what nested 14 | values are lurking inside of it, etc. 15 | */ 16 | 17 | class List { 18 | constructor(listObject) { 19 | // We want each value to be guaranteed to have a `next` method, 20 | // even if this is an array of static values. 21 | 22 | if (Array.isArray(listObject)) { 23 | this.values = parseArguments(listObject); 24 | } else if (typeof listObject === 'object' && listObject.arguments) { 25 | this.values = parseArguments(listObject.arguments); 26 | } else { 27 | throw new Error(`List got a weird value? ${listObject}`) 28 | } 29 | 30 | this._currentIndex = 0; 31 | } 32 | toArray() { 33 | return this.values; 34 | } 35 | next() { 36 | const value = this.values[this._currentIndex].next(); 37 | this._currentIndex = (this._currentIndex + 1) % this.values.length; 38 | return value; 39 | } 40 | } 41 | 42 | export default List; -------------------------------------------------------------------------------- /helpers/BufferLoader.js: -------------------------------------------------------------------------------- 1 | // an abstraction written by Boris Smus, 2 | // taken from http://www.html5rocks.com/en/tutorials/webaudio/intro/ 3 | // ... thanks Boris! 4 | 5 | export default function BufferLoader(context, urlList, callback) { 6 | this.context = context; 7 | this.urlList = urlList; 8 | this.onload = callback; 9 | this.bufferList = []; 10 | this.loadCount = 0; 11 | } 12 | 13 | BufferLoader.prototype.loadBuffer = function(url, index) { 14 | // Load buffer asynchronously 15 | var request = new XMLHttpRequest(); 16 | request.open("GET", url, true); 17 | request.responseType = "arraybuffer"; 18 | 19 | var loader = this; 20 | 21 | request.onload = function() { 22 | // Asynchronously decode the audio file data in request.response 23 | loader.context.decodeAudioData( 24 | request.response, 25 | function(buffer) { 26 | if (!buffer) { 27 | console.error('BufferLoader: error decoding file data from url: ' + url); 28 | return; 29 | } 30 | loader.bufferList[index] = buffer; 31 | if (++loader.loadCount == loader.urlList.length) 32 | loader.onload(loader.bufferList); 33 | }, 34 | function(error) { 35 | console.error('BufferLoader: decodeAudioData error', error); 36 | } 37 | ); 38 | }; 39 | 40 | request.onerror = function() { 41 | console.log('BufferLoader: error decoding', url); 42 | }; 43 | 44 | request.send(); 45 | }; 46 | 47 | BufferLoader.prototype.load = function() { 48 | for (var i = 0; i < this.urlList.length; ++i) 49 | this.loadBuffer(this.urlList[i], i); 50 | }; 51 | -------------------------------------------------------------------------------- /classes/Delay.js: -------------------------------------------------------------------------------- 1 | import Block from './Block'; 2 | import context from '../helpers/context'; 3 | import tuna from '../helpers/tuna'; 4 | import { parseArgument } from '../helpers/parseArguments'; 5 | 6 | class Delay extends Block { 7 | constructor(...args) { 8 | super(...args); 9 | 10 | // Arguments are time, fb, wet, dry, cutoff 11 | this.time = this.arguments[0] || parseArgument('8n'); 12 | this.feedback = this.arguments[1] || parseArgument(0.1); 13 | this.wet = this.arguments[2] || parseArgument(0.5); 14 | this.dry = this.arguments[3] || parseArgument(0.5); 15 | this.cutoff = this.arguments[4] || parseArgument(11025); 16 | 17 | this.delay = null; 18 | } 19 | 20 | instantiate() { 21 | if (this.getPolyMode()) return; 22 | 23 | this.delay = new tuna.Delay({ 24 | delayTime: this.time.next() * 1000, 25 | feedback: this.feedback.next(), 26 | wetLevel: this.wet.next(), 27 | dryLevel: this.dry.next(), 28 | cutoff: this.cutoff.next(), 29 | bypass: 0, 30 | }); 31 | 32 | this.getInput().connect(this.delay); 33 | this.delay.connect(this.getOutput()); 34 | } 35 | 36 | schedule(start) { 37 | if (!this.getPolyMode()) { 38 | // update values here 39 | return; 40 | } 41 | 42 | const delay = new tuna.Delay({ 43 | delayTime: this.time.next() * 1000, 44 | feedback: this.feedback.next(), 45 | wetLevel: this.wet.next(), 46 | dryLevel: this.dry.next(), 47 | cutoff: this.cutoff.next(), 48 | bypass: 0, 49 | }); 50 | 51 | return { 52 | input: delay, 53 | output: delay, 54 | }; 55 | } 56 | } 57 | 58 | export default Delay; 59 | -------------------------------------------------------------------------------- /helpers/parseArguments.js: -------------------------------------------------------------------------------- 1 | import List from './List'; 2 | import FunctionCall from '../functions'; 3 | 4 | const TEMPO = 120; 5 | const DIVISION = (1 / 24) / (TEMPO / 60); 6 | 7 | export const rhythmMap = { 8 | '64t': DIVISION, 9 | '64n': DIVISION * 1.5, 10 | '32t': DIVISION * 2, 11 | '32n': DIVISION * 3, 12 | '16t': DIVISION * 4, 13 | '16n': DIVISION * 6, 14 | '8t': DIVISION * 8, 15 | '8n': DIVISION * 12, 16 | '4t': DIVISION * 16, 17 | '4n':DIVISION * 24, 18 | '2t': DIVISION * 32, 19 | '2n': DIVISION * 48, 20 | '1n': DIVISION * 96, 21 | } 22 | 23 | /* 24 | ARGUMENTS 25 | Arguments within blocks and functions can be numbers, 26 | lists, notes, etc. Rather than doing a lot of type 27 | checking within our subclasses, let's turn every single 28 | argument into a consistent api, using `next()`. If it's 29 | a list, `next` will cycle through all of the values. 30 | If it's a static value, `next` will return that value. 31 | */ 32 | 33 | export default function(args) { 34 | return args.map((arg) => parseArgument(arg)); 35 | } 36 | 37 | export function parseArgument(arg) { 38 | // If this argument is already nextable, no need to do anything. 39 | if (arg && typeof arg.next === 'function') return arg; 40 | 41 | if ( 42 | typeof arg === 'number' 43 | || typeof arg === 'string' 44 | ) return createArgumentFromStaticValue(arg); 45 | 46 | if ( 47 | Array.isArray(arg) 48 | || arg.type === 'list' 49 | ) return new List(arg); 50 | 51 | if (arg.type === 'function') return new FunctionCall(arg); 52 | 53 | return null; 54 | } 55 | 56 | function createArgumentFromStaticValue(value) { 57 | // convert rhythms into numbers if we catch one 58 | return { 59 | next: () => rhythmMap[value] || value, 60 | }; 61 | } -------------------------------------------------------------------------------- /functions/chord.js: -------------------------------------------------------------------------------- 1 | import * as Scale from 'tonal-scale'; 2 | import take from 'lodash/take'; 3 | import flatMap from 'lodash/flatMap'; 4 | import FunctionCall from './FunctionCall'; 5 | import { parseArgument } from '../helpers/parseArguments'; 6 | 7 | // For now let's strip the spaces out of the chord names 8 | // to simplify the arguments to the (chord ...) function. 9 | const scaleNamesMap = Scale.names().reduce((ob, name) => { 10 | ob[name.replace(/( |\#)/g, '')] = name; 11 | return ob; 12 | }, {}); 13 | 14 | export default class Chord extends FunctionCall { 15 | constructor(functionObject) { 16 | super(functionObject); 17 | 18 | // Scale.notes takes the arguments in the other order, so the 19 | // note comes first and then the chord name. 20 | const key = this.arguments[1].next(); 21 | const scale = this.arguments[0].next(); 22 | const length = this.arguments[2] ? this.arguments[2].next() : null; 23 | if (!scaleNamesMap[scale]) throw new Error(`Chord: ${scale} is not a recognized scale!`); 24 | let notes = Scale.notes(key, scaleNamesMap[scale]); 25 | 26 | // If a length was provided, repeat the contents of the array 27 | // so that the return value matches the requested length. 28 | if (length) { 29 | if (length <= notes.length) { 30 | notes = take(notes, length); 31 | } else { 32 | // figure out how many times it repeats ... 33 | const repeat = Math.ceil(length / notes.length); 34 | // ... repeat it ... 35 | notes = flatMap(Array(repeat).fill(null), __ => notes); 36 | // ... now take the exact amount. 37 | notes = take(notes, length); 38 | } 39 | } 40 | 41 | // We don't need to implement our own `next` method because 42 | // the default for a FunctionCall is to return `this.data.next()`. 43 | this.data = parseArgument(notes); 44 | } 45 | } -------------------------------------------------------------------------------- /runtime.js: -------------------------------------------------------------------------------- 1 | import Sound from './classes/Sound'; 2 | import context from './helpers/context'; 3 | 4 | const model = { 5 | sounds: {}, 6 | }; 7 | 8 | function runScene(scene) { 9 | // a scene is a collection of lines that go together. 10 | 11 | // Stage 1: build the scene 12 | scene.forEach((operation) => { 13 | switch (operation.type) { 14 | case 'graph': 15 | parseGraph(operation); 16 | break; 17 | case 'play': 18 | parsePlay(operation); 19 | break; 20 | } 21 | }); 22 | 23 | const startTime = context.currentTime + 0.01; 24 | 25 | // Stage 2: Schedule the sound 26 | Object.keys(model.sounds).forEach((id) => { 27 | model.sounds[id].start(startTime); 28 | }); 29 | } 30 | 31 | function parseGraph(graph) { 32 | const { sound } = graph; 33 | 34 | // This particular line of code will either create 35 | // a new sound or modify an existing sound. We may 36 | // also hit a runtime error if we are trying to 37 | // access a property of a sound that hasn't been 38 | // instantiated yet. 39 | 40 | const soundExistsInModel = !!model.sounds[sound.name]; 41 | const accessingSoundProperty = !!sound.property; 42 | 43 | if (!soundExistsInModel && !accessingSoundProperty) { 44 | // We're instantiating a new sound. 45 | model.sounds[sound.name] = new Sound(graph); 46 | } else if (soundExistsInModel) { 47 | // We're adding new information to the same sound. 48 | model.sounds[sound.name].appendToGraph(graph); 49 | } else { 50 | throw new Error(`Tried to access ${sound.property} of non-existant sound ${sound.name}`); 51 | } 52 | }; 53 | 54 | function parsePlay(operation) { 55 | model.sounds[operation.sound.name].schedule(operation.patterns); 56 | } 57 | 58 | function clearScene() { 59 | Object.keys(model.sounds).forEach((id) => { 60 | model.sounds[id].destroy(); 61 | delete model.sounds[id]; 62 | }); 63 | } 64 | 65 | export default { 66 | runScene, 67 | clearScene, 68 | }; 69 | -------------------------------------------------------------------------------- /classes/PolyBlock.js: -------------------------------------------------------------------------------- 1 | import Block from './Block'; 2 | import classMap from './classMap'; 3 | 4 | class PolyBlock extends Block { 5 | constructor({ blocks }) { 6 | super(); 7 | this.blockDefinitions = blocks; 8 | this.blocks = []; 9 | } 10 | instantiate() { 11 | // Turn the block model objects into Block classes. 12 | this.blocks = this.blockDefinitions.map((block) => { 13 | if (classMap[block.function]) { 14 | // We're doing the same thing that the Sound 15 | // class is doing with the blocks here, but 16 | // in `schedule` we're going to do some tricks 17 | // to link together all of the sounds in a 18 | // polyphonic way. 19 | const b = new classMap[block.function](...block.arguments); 20 | // Tell this block it's in poly mode. 21 | b.setPolyMode(true); 22 | b.instantiate(); 23 | return b; 24 | } else { 25 | throw new Error(`PolyBlock: Block type "${block.function}" does not exist`); 26 | } 27 | }); 28 | } 29 | schedule(start, stop, note) { 30 | // Here's where we do the polyphonic magic. 31 | // All of our blocks have already been told 32 | // to act in poly mode, so the return value 33 | // of their `schedule` calls will be an 34 | // audio node. 35 | 36 | // First, map through the blocks and collect 37 | // the nodes they return. 38 | const connections = this.blocks.map(block => block.schedule(start, stop, note)); 39 | 40 | // Now loop through them and chain them together. 41 | for (let i = 0; i < connections.length; i++) { 42 | // If there is an adjacent block... 43 | if (connections[i] && connections[i + 1]) { 44 | // Connect them! 45 | connections[i].output.connect( 46 | connections[i + 1].input 47 | ); 48 | } else { 49 | // We're at the final block; connect 50 | // it to the output. 51 | connections[i].output.connect(this.getOutput()); 52 | } 53 | } 54 | } 55 | } 56 | 57 | export default PolyBlock; -------------------------------------------------------------------------------- /functions/transpose.js: -------------------------------------------------------------------------------- 1 | import { transpose as tonalTranspose } from 'tonal-distance'; 2 | import { fromSemitones } from "tonal-interval" 3 | import FunctionCall from './FunctionCall'; 4 | import { parseArgument, rhythmMap } from '../helpers/parseArguments'; 5 | 6 | window.tonalTranspose = tonalTranspose; 7 | window.fromSemitones = fromSemitones; 8 | 9 | export default class Transpose extends FunctionCall { 10 | constructor(functionObject) { 11 | super(functionObject); 12 | this.amount = parseArgument(this.arguments[0]); 13 | this.data = parseArgument(this.arguments[1]); 14 | 15 | this.hasWarned = false; 16 | } 17 | next(passedValue) { 18 | let nextValue = passedValue || this.data.next(); 19 | 20 | // Unfortunately transposing rhythms won't work 21 | // easily the way the rhythm strings are passed around 22 | // (specifically because rests aren't converted into 23 | // their number value until the scheduler looks at 24 | // the presence of the `r` in the string, which happens 25 | // too far downstream for us to do anything about it here.) 26 | 27 | // Let's start by detecting if this is a rhythm; if so 28 | // it's going to be a noop + a gentle console.warn. 29 | if ( 30 | typeof nextValue === 'string' 31 | && ( 32 | nextValue.charAt(0).toLowerCase() === 'r' 33 | || rhythmMap[nextValue] 34 | ) 35 | ) { 36 | if (!this.hasWarned) { 37 | console.warn('Warning: transpose doesn’t work with rhythm values.'); 38 | this.hasWarned = true; 39 | } 40 | return nextValue; 41 | } 42 | 43 | // Now if this is a string that means it's a note value. 44 | // The Scale library can help us transpose. 45 | if (typeof nextValue === 'string') { 46 | return tonalTranspose( 47 | nextValue, 48 | fromSemitones(Math.floor(this.amount.next())) 49 | ); 50 | } 51 | 52 | // Finally, if we've reached the end we have two numbers. 53 | return nextValue + this.amount.next(); 54 | } 55 | toArray() { 56 | // toArray is essentially "rendering" a static array, 57 | // which some functions require (like `shuffle`). 58 | return this.data.toArray().map(item => this.next(item.next())); 59 | } 60 | } -------------------------------------------------------------------------------- /classes/Osc.js: -------------------------------------------------------------------------------- 1 | import { Note } from 'tonal'; 2 | import Block from './Block'; 3 | import context from '../helpers/context'; 4 | import mtof from '../helpers/mtof'; 5 | import { parseArgument } from '../helpers/parseArguments'; 6 | 7 | const typeMap = { 8 | sin: 'sin', 9 | sine: 'sine', 10 | 11 | tri: 'triangle', 12 | triangle: 'triangle', 13 | 14 | saw: 'sawtooth', 15 | sawtooth: 'sawtooth', 16 | 17 | sq: 'square', 18 | square: 'square', 19 | } 20 | 21 | class Osc extends Block { 22 | constructor(...args) { 23 | super(...args); 24 | 25 | // We'll have this.arguments available to us now, 26 | // which did the work of parsing lists and functions 27 | // if they were passed in. 28 | this.type = this.arguments[0] || parseArgument('sine'); 29 | this.detune = this.arguments[1] || parseArgument(0); 30 | } 31 | 32 | schedule(start, stop, note, envelopeMode) { 33 | const osc = context.createOscillator(); 34 | 35 | osc.type = typeMap[this.type.next()]; 36 | 37 | let noteMidiValue = typeof note === 'string' ? Note.midi(note) : note; 38 | osc.frequency.setValueAtTime( 39 | Note.freq(Note.fromMidi(noteMidiValue + this.detune.next())), 40 | context.currentTime, 41 | 0 42 | ); 43 | 44 | osc.start(start); 45 | // Envelope mode is a flag that an ADSR envelope will pass 46 | // into Osc if it is controlling this Block. This is the 47 | // only reasonable way to solve the problem of the envelope 48 | // needing to control the stop time. 49 | if (!envelopeMode) osc.stop(stop); 50 | 51 | osc.onended = () => { 52 | osc.disconnect(); 53 | }; 54 | 55 | // Envelope mode returns the osc without setting stop, while 56 | // poly mode returns the consistent input/output interface. 57 | 58 | if (envelopeMode) { 59 | return { 60 | node: osc, 61 | property: osc, 62 | } 63 | } else if (this.getPolyMode()) { 64 | // An osc has no input! Not sure 65 | // what to do about that. 66 | return { 67 | output: osc, 68 | }; 69 | } 70 | 71 | // Finally, if we are in mono mode, just connect the osc to 72 | // the ouput. 73 | osc.connect(this.getOutput()); 74 | } 75 | } 76 | 77 | export default Osc; 78 | -------------------------------------------------------------------------------- /classes/Filter.js: -------------------------------------------------------------------------------- 1 | import Block from './Block'; 2 | import context from '../helpers/context'; 3 | import { parseArgument } from '../helpers/parseArguments'; 4 | 5 | const typeMap = { 6 | lp: 'lowpass', 7 | hp: 'highpass', 8 | bp: 'bandpass', 9 | n: 'notch', 10 | }; 11 | 12 | class Filter extends Block { 13 | constructor(...args) { 14 | super(...args); 15 | 16 | this.type = this.arguments[0] || parseArgument('lp'); 17 | this.amount = this.arguments[1] || parseArgument(100); 18 | this.Q = this.arguments[2] || parseArgument(1); 19 | } 20 | 21 | instantiate() { 22 | if (this.getPolyMode()) return; 23 | 24 | this.filter = context.createBiquadFilter(); 25 | // We're not calling `next()` on our parameters yet because 26 | // the schedule method will take care of that; otherwise we'll 27 | // end up with the second value in a cycle on the first 28 | // scheduled note. 29 | this.filter.type = typeMap['lp']; 30 | this.filter.frequency.setValueAtTime(11025, context.currentTime, 0); 31 | this.filter.Q.setValueAtTime(1, context.currentTime, 0); 32 | 33 | this.getInput().connect(this.filter); 34 | this.filter.connect(this.getOutput()); 35 | } 36 | 37 | schedule(start, stop, note, envelopeMode) { 38 | if (!this.getPolyMode()) { 39 | // If we're not in poly mode we still want to swap values 40 | // on the filter if our arguments are lists. 41 | this.filter.type = typeMap[this.type.next()]; 42 | this.filter.frequency.setValueAtTime((this.amount.next() / 127) * 11025, start, 10); 43 | this.filter.Q.setValueAtTime(this.Q.next(), start, 10); 44 | return; 45 | } 46 | 47 | const filter = context.createBiquadFilter(); 48 | filter.type = typeMap[this.type.next()]; 49 | filter.frequency.setValueAtTime((this.amount.next() / 127) * 11025, context.currentTime, 0); 50 | filter.Q.setValueAtTime(this.Q.next(), context.currentTime, 0); 51 | 52 | // TODO: envelope mode to return filter.frequency as property 53 | 54 | if (envelopeMode) { 55 | return { 56 | node: filter, 57 | property: filter.frequency, 58 | }; 59 | } 60 | 61 | return { 62 | input: filter, 63 | output: filter, 64 | }; 65 | } 66 | } 67 | 68 | export default Filter; 69 | -------------------------------------------------------------------------------- /classes/Drums.js: -------------------------------------------------------------------------------- 1 | import { Note } from 'tonal'; 2 | import Block from './Block'; 3 | import context from '../helpers/context'; 4 | import mtof from '../helpers/mtof'; 5 | import { parseArgument } from '../helpers/parseArguments'; 6 | import BufferLoader from '../helpers/BufferLoader'; 7 | import drumMap from '../helpers/drumMap'; 8 | 9 | // Taking the easy route here: let's store the drums in 10 | // global variables here so each instance of the `Drums` 11 | // class has access to them. 12 | let loadingDrums = false; 13 | let drumBuffers = []; 14 | 15 | class Drums extends Block { 16 | constructor(...args) { 17 | super(...args); 18 | 19 | // In the future we can support different drum sets 20 | // using a simple integer argument. 21 | this.type = this.arguments[0] || parseArgument(0); 22 | 23 | // Super basic lazy loading of drum sounds. 24 | // If the buffers don't already exist and we're 25 | // not trying to load them... do that. 26 | if (!drumBuffers.length && !loadingDrums) { 27 | this.loadDrumSounds(); 28 | } 29 | } 30 | 31 | schedule(start, stop, note, envelopeMode) { 32 | if (!drumBuffers.length || loadingDrums) return; 33 | // we only have 12 samples available but we shouldn't 34 | // burden the user with that knowledge so let's use 35 | // mod 12, which allows them to use chords, scales, 36 | // etc. without having to think about it. 37 | const drumSound = note % 12; 38 | 39 | const sample = context.createBufferSource(); 40 | sample.buffer = drumBuffers[drumSound]; 41 | 42 | sample.start(start); 43 | sample.stop(stop); 44 | 45 | sample.onended = () => { 46 | sample.disconnect(); 47 | }; 48 | 49 | if (this.getPolyMode()) { 50 | return { 51 | output: sample, 52 | }; 53 | } 54 | 55 | // Finally, if we are in mono mode, just connect the osc to 56 | // the ouput. 57 | sample.connect(this.getOutput()); 58 | } 59 | loadDrumSounds() { 60 | loadingDrums = true; 61 | // Get a list of files 62 | const files = Object.keys(drumMap).map(key => drumMap[key].file); 63 | // Load the files! 64 | const loader = new BufferLoader(context, files, list => { 65 | // set our global variable to the list of buffers. Done. 66 | drumBuffers = list; 67 | loadingDrums = false; 68 | }); 69 | loader.load(); 70 | } 71 | } 72 | 73 | export default Drums; 74 | -------------------------------------------------------------------------------- /helpers/FunctionCall.js: -------------------------------------------------------------------------------- 1 | import * as Scale from 'tonal-scale'; 2 | import parseArguments, { parseArgument } from './parseArguments'; 3 | import List from './List'; 4 | 5 | // For now let's strip the spaces out of the chord names 6 | // to simplify the arguments to the (chord ...) function. 7 | const scaleNamesMap = Scale.names().reduce((ob, name) => { 8 | ob[name.replace(/ /g, '')] = name; 9 | return ob; 10 | }, {}); 11 | 12 | class FunctionCall { 13 | constructor(functionObject) { 14 | this.type = functionObject.function; 15 | this.arguments = parseArguments(functionObject.arguments); 16 | 17 | // Some of the function calls will have to be prepared 18 | // as lists ahead of time so they can return cyclic values. 19 | if (this.type === 'chord') { 20 | // Scale.notes takes the arguments in the other order, so the 21 | // note comes first and then the chord name. 22 | const key = this.arguments[1].next(); 23 | const scale = this.arguments[0].next(); 24 | if (!scaleNamesMap[scale]) throw new Error(`Chord: ${scale} is not a recognized scale!`); 25 | const notes = Scale.notes(key, scaleNamesMap[scale]); 26 | this.chordList = parseArgument(notes); 27 | } 28 | 29 | if (this.type === 'random' && this.arguments[0].toArray) { 30 | console.log('Trying to make a randomList...', this.arguments[0]); 31 | this.randomList = this.arguments[0].toArray(); 32 | console.log(this.randomList); 33 | } 34 | } 35 | next() { 36 | switch (this.type) { 37 | case 'random': return this.random(); 38 | case 'chord': return this.chord(); 39 | default: 40 | throw new Error(`Function ${this.type} does not exist`); 41 | } 42 | } 43 | 44 | toArray() { 45 | if (this.type === 'chord') return this.chordList.toArray(); 46 | } 47 | 48 | // ============================================================ 49 | // FUNCTION TYPES 50 | // ============================================================ 51 | 52 | random() { 53 | // Returns a single value 54 | if (this.randomList) return this.randomList[Math.floor(Math.random() * this.randomList.length)].next(); 55 | return this.arguments[Math.floor(Math.random() * this.arguments.length)].next(); 56 | } 57 | 58 | chord() { 59 | // Arguments: chord, note 60 | // e.g. (chord major E3) 61 | return this.chordList.next(); 62 | } 63 | } 64 | 65 | export default FunctionCall; -------------------------------------------------------------------------------- /classes/Block.js: -------------------------------------------------------------------------------- 1 | import context from '../helpers/context'; 2 | import parseArguments from '../helpers/parseArguments'; 3 | 4 | /* 5 | BLOCK class 6 | This class provides a consistent interface 7 | for all blocks. Each block has input and 8 | output gain nodes which, while a bit 9 | extraneous in terms of the audio graph, 10 | give us a way to automatically connect 11 | blocks so specific block instances don't 12 | have to worry about it. 13 | */ 14 | 15 | class Block { 16 | constructor(...args) { 17 | // The Block class will parse its arguments, 18 | // which may contain function calls or lists, 19 | // so that subclasses don't need to worry 20 | // about interpreting them. Each subclass will 21 | // only need to call `arguments[i].next()` 22 | // to grab a parameter. 23 | this.arguments = parseArguments(args); 24 | 25 | // This input allows us to give each 26 | // block a consistent interface without 27 | // having to name it all particular. 28 | this._input = context.createGain(); 29 | this._output = context.createGain(); 30 | 31 | // Some blocks will want to implement 32 | // polyphony, which really just means 33 | // returning a specific audio node from 34 | // `schedule` so that it can be chained 35 | // to other nodes. Implementers just 36 | // need to call this.getPolyMode() to 37 | // find out which behavior to follow. 38 | this._polyMode = false; 39 | } 40 | 41 | instantiate() { 42 | // This is where each subclass will set up 43 | // its audio graph. The methods getInput() 44 | // and getOutput() give us an easy way to 45 | // connect everything together. 46 | } 47 | 48 | connect(block) { 49 | // The target block has `.getInput()`, 50 | // allowing us to connect our own internal 51 | // nodes to it. We could just connect the 52 | // class variables directly, but perhaps 53 | // specific blocks will have a reason to 54 | // override _input and _output. Who knows. 55 | this.getOutput().connect(block.getInput()); 56 | } 57 | 58 | schedule(start, stop, note) { 59 | // use the timestamp to schedule calls to 60 | // oscillators or what have you. 61 | } 62 | 63 | destroy() { 64 | this._output.disconnect(); 65 | } 66 | 67 | getInput() { 68 | return this._input; 69 | } 70 | 71 | getOutput() { 72 | return this._output; 73 | } 74 | 75 | setPolyMode(flag) { 76 | this._polyMode = flag; 77 | } 78 | 79 | getPolyMode() { 80 | return this._polyMode; 81 | } 82 | } 83 | 84 | export default Block; 85 | -------------------------------------------------------------------------------- /notes.todo: -------------------------------------------------------------------------------- 1 | Implement: 2 | ✔ E3-style notes @done (18-05-31 18:47) 3 | ☐ scene restarting at the next interval? 4 | ✘ randoms @cancelled (18-06-01 16:24) 5 | ✔ polyblocks @done (18-05-31 20:00) 6 | ✔ cool yeah that could be a class? @done (18-05-31 20:00) 7 | ✔ the schedule fn could link together the blocks for each note triggered @done (18-05-31 20:00) 8 | ✘ variables @cancelled (18-06-01 16:24) 9 | ✔ more blocks @done (18-09-29 11:49) 10 | ✔ pan @done (18-06-03 08:39) 11 | ✔ gain @done (18-06-03 08:40) 12 | ✔ sharp notes @done (18-06-02 11:41) 13 | ✔ Lists @done (18-06-02 11:41) 14 | ✔ step 1: discrete list as soundArgument @done (18-06-01 17:07) 15 | ✔ step 2: range as soundArgument @done (18-06-01 17:07) 16 | ✔ step 3: Function as sound argument @done (18-06-02 07:14) 17 | ✔ step 4: List/Function classes @done (18-06-02 11:41) 18 | ✔ parse functions to make scheduler patterns @done (18-06-02 08:12) 19 | ✔ envelope causes problems with the chain @done (18-06-04 16:41) 20 | ✔ osc needs to know what its end time will be, which the envelope defines via release @done (18-06-04 16:41) 21 | ✘ filter needs to set the peakLevel of the env (otherwise it would work) @cancelled (18-06-16 08:17) 22 | ✔ use a function call instead: `(envelope (osc [sin tri]) .1 0 1 .2) @done (18-06-04 16:41) 23 | ✔ split environment into a different file `editor.js` @done (18-06-16 09:18) 24 | ✔ save code in localstorage on run @done (18-06-16 09:18) 25 | ✘ start/stop button? @cancelled (18-06-16 09:18) 26 | 27 | Design: 28 | ✔ clojure-style parens @done (18-06-01 16:38) 29 | ✔ get rid of pattern definitions @done (18-06-02 07:40) 30 | ✔ lists and ranges @done (18-06-02 07:15) 31 | ✔ List class can hold a finite list or a range @done (18-06-02 07:14) 32 | ✔ lists won't be used outside of functions! I think @done (18-06-02 07:15) 33 | ✘ could LFOs be functions with forward-pointing arguments? @cancelled (18-09-29 11:49) 34 | ✘ @synth ~ (lfo osc.frequency 2 100) ~ (osc tri) + (filter lp 8) @cancelled (18-09-29 11:49) 35 | 36 | 37 | todos: 38 | ✔ get rid of commas as separators @done (18-06-02 07:15) 39 | ✔ expose errors @done (18-09-29 15:44) 40 | ✔ play/pause button @done (18-09-29 15:44) 41 | ✔ drums @done (18-09-29 11:48) 42 | ✔ @percussion (drums ) + (filter lp 10) @done (18-09-29 11:48) 43 | ✔ play @percussion (rhythm [8n]) (notes [0 1 2 3 4 5]) @done (18-09-29 11:48) 44 | ✔ lazy load when drum is first used? @done (18-09-29 11:48) 45 | ✔ 0 - 11 for now, maybe mod% so that any number hits a sound @done (18-09-29 11:48) 46 | 47 | 48 | 49 | -------------------------------------------------------------------------------- /classes/Scheduler.js: -------------------------------------------------------------------------------- 1 | import context from '../helpers/context'; 2 | import { parseArgument, rhythmMap } from '../helpers/parseArguments'; 3 | import List from '../helpers/List'; 4 | 5 | export default class Scheduler { 6 | constructor(patterns) { 7 | // Start with defaults for 8 | // each of the patterns. 9 | 10 | // a list of rhythm lengths 11 | this.rhythmPattern = parseArgument('8n'); 12 | // a List of notes 13 | this.notePattern = parseArgument(69); 14 | 15 | // Store a callback to the Sound here. 16 | this.tickCallback = null; 17 | // Stash a ref to the setInterval id 18 | this.interval = null; 19 | 20 | // CLOCK 21 | 22 | // currentTs will keep track of which 23 | // tick we've scheduled up to. 24 | this.currentTime = null; 25 | this.lookahead = .04; 26 | this.startTime = null; 27 | 28 | // Loop through whatever we got and overwrite 29 | // the default patterns. All three of these 30 | // functions only accept one argument, which 31 | // is why we're pulling arguments[0] out. 32 | patterns.forEach((pattern) => { 33 | switch (pattern.function) { 34 | case 'rhythm': 35 | // We have to special-case rhythm argument parsing 36 | // for now because the xoxoxo-style pattern is not 37 | // recognized by the parser as a List. 38 | this.rhythmPattern = parseArgument(pattern.arguments[0]) 39 | break; 40 | case 'notes': 41 | this.notePattern = parseArgument(pattern.arguments[0]); 42 | break; 43 | default: 44 | break; 45 | } 46 | }); 47 | } 48 | 49 | tick(callback) { 50 | this.tickCallback = callback; 51 | } 52 | 53 | start(timestamp) { 54 | this.startTime = timestamp; 55 | this.currentTime = timestamp; 56 | 57 | this.interval = setInterval(() => { 58 | while (this.currentTime < context.currentTime + this.lookahead) { 59 | // The tick length could be a number or a string that starts 60 | // with 'r', indicating a rest. 61 | let nextTickLength = this.rhythmPattern.next(); 62 | // Let's start by assuming it's not a rest. 63 | let rest = false; 64 | // if it's a string and it starts with R, it is a rest. 65 | if (typeof nextTickLength === 'string' && nextTickLength.charAt(0).toLowerCase() === 'r') { 66 | rest = true; 67 | // Convert it into the appropriate rhythm. 68 | nextTickLength = rhythmMap[nextTickLength.substr(1)]; 69 | } 70 | // We're only ticking on beats that aren't rests. 71 | if (!rest) { 72 | const nextNote = this.notePattern.next(); 73 | // schedule stuff! 74 | this.tickCallback( 75 | // start time 76 | this.currentTime, 77 | // stop time 78 | // this.currentTime + this.lengthPattern.next(), 79 | this.currentTime + nextTickLength, 80 | // note 81 | nextNote 82 | ); 83 | } 84 | // go to the next beat in the clock 85 | this.currentTime += nextTickLength; 86 | } 87 | }, 40); 88 | } 89 | 90 | stop() { 91 | this.interval = clearInterval(this.interval); 92 | } 93 | } -------------------------------------------------------------------------------- /public/index.css: -------------------------------------------------------------------------------- 1 | * { 2 | box-sizing: border-box; 3 | -webkit-font-smoothing: antialiased; 4 | -moz-osx-font-smoothing: grayscale; 5 | } 6 | 7 | body, html { 8 | height: 100%; 9 | margin: 0; 10 | } 11 | 12 | body { 13 | display: flex; 14 | flex-direction: column; 15 | padding: 24px; 16 | font-family: 'Roboto Mono', 'Andale Mono', monospace; 17 | } 18 | 19 | #editor { 20 | position: relative; 21 | flex: 1 0 auto; 22 | display: flex; 23 | flex-direction: column; 24 | } 25 | 26 | /* The top flex column that holds the SVG and the error box */ 27 | .banner { 28 | position: relative; 29 | } 30 | 31 | #svg-title { 32 | flex: 0 0 auto; 33 | display: block; 34 | width: 100%; 35 | max-width: 628px; 36 | max-height: 80px; 37 | height: auto; 38 | margin: 12px auto 24px auto; 39 | } 40 | 41 | #error { 42 | position: absolute; 43 | top: 0; 44 | left: 0; 45 | right: 0; 46 | padding: 12px; 47 | height: 100%; 48 | pointer-events: none; 49 | opacity: 0; 50 | transition: opacity 0.1s; 51 | background-color: white; 52 | border: 1px dashed #fc5e39; 53 | color: #fc5e39; 54 | white-space: pre; 55 | overflow-y: scroll; 56 | } 57 | 58 | #error.show { 59 | opacity: 1; 60 | pointer-events: auto; 61 | } 62 | 63 | #error-content { 64 | position: absolute; 65 | top: 12px; 66 | left: 12px; 67 | right: 24px; 68 | } 69 | 70 | #dismiss { 71 | position: absolute; 72 | top: 12px; 73 | right: 12px; 74 | line-height: 10px; 75 | font-size: 20px; 76 | cursor: pointer; 77 | } 78 | 79 | footer { 80 | position: relative; 81 | top: 10px; 82 | font-size: 12px; 83 | text-align: center; 84 | color: #28bed4; 85 | } 86 | 87 | footer a { 88 | text-decoration: none; 89 | color: #28bed4; 90 | } 91 | 92 | footer a:hover { 93 | text-decoration: underline; 94 | } 95 | 96 | /* 97 | --------------------------------- CONTROLS -------------------------------- 98 | */ 99 | 100 | #controls { 101 | display: flex; 102 | padding: 6px; 103 | background-color: #eee7dd; 104 | } 105 | 106 | #controls > * { 107 | flex: 0 0 33.33%; 108 | } 109 | 110 | .controls-center { 111 | text-align: center; 112 | color: rgba(0, 0, 0, 0.25); 113 | } 114 | 115 | .controls-right { 116 | text-align: right; 117 | } 118 | 119 | .button { 120 | display: inline-block; 121 | padding: 2px 6px; 122 | border-radius: 2px; 123 | line-height: 22px; 124 | background-color: #8dc154; 125 | color: white; 126 | cursor: pointer; 127 | text-decoration: none; 128 | } 129 | 130 | .button.red { 131 | background-color: #fb5e39; 132 | } 133 | 134 | .button.blue { 135 | background-color: #28bed3; 136 | } 137 | 138 | .button.gray { 139 | background-color: gray; 140 | } 141 | 142 | /* 143 | --------------------------------- EDITOR --------------------------------- 144 | */ 145 | 146 | .CodeMirror { 147 | flex: 1 0 auto; 148 | padding: 12px; 149 | font-size: 18px; 150 | font-family: 'Roboto Mono', 'Andale Mono', monospace !important; 151 | } 152 | 153 | .cm-note { 154 | color: #8BC34A; 155 | } 156 | .cm-variable { 157 | color: #FF5722 !important; 158 | } 159 | .cm-number { 160 | color: #00BCD4 !important; 161 | } 162 | .cm-pipe { 163 | color: #9c27b0; 164 | } 165 | .cm-comment { 166 | color: #d3ccbb !important; 167 | } 168 | .cm-beat { 169 | color: #ff9800; 170 | } 171 | .cm-rest { 172 | color: #ff980085; 173 | } -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 41 |
42 |
43 |
Run
44 |
Stop
45 |
46 |
47 |
Stopped
48 |
49 |
50 |
Create URL
51 | Docs 52 |
53 |
54 |
55 | 59 | 60 | 61 | 62 | -------------------------------------------------------------------------------- /backend/views/patch.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 41 |
42 |
43 |
Run
44 |
Stop
45 |
46 |
47 |
Stopped
48 |
49 |
50 |
Create URL
51 | Docs 52 |
53 |
54 |
55 | 59 | 60 | 61 | 62 | 63 | -------------------------------------------------------------------------------- /slang.ohm: -------------------------------------------------------------------------------- 1 | Sound { 2 | Line = Graph | Play | Comment 3 | 4 | /* 5 | A comment is any line that begins with # 6 | */ 7 | 8 | Comment = "#" any+ 9 | 10 | /* 11 | SOUND OBJECTS 12 | A sound object is a '@synth' variable. We 13 | can also access part of the sound's graph 14 | using dot notation, e.g. '@synth.osc1'. 15 | We'll only accept soundAccessors in a few 16 | places. 17 | */ 18 | 19 | sound = "@" alnum+ 20 | propertyAccessor = "." alnum+ 21 | soundAccessor = sound propertyAccessor? 22 | 23 | /* 24 | FUNCTIONS 25 | A function is anything in parentheses. These 26 | will power blocks and arbitrary tools that 27 | might spit out lists, numbers, etc. The syntax 28 | is inspired by Clojure. Notice that the first 29 | type of sound argument is... a function! This 30 | enables is to write nested functions. Imagine: 31 | (notes (random (chord major E3))) 32 | */ 33 | 34 | function = "(" listOf ")" 35 | soundArgument = function -- function 36 | | range -- range 37 | | list -- list 38 | | rhythm -- rhythm 39 | | float -- float 40 | | note -- note 41 | 42 | /* 43 | GRAPH LINES 44 | A graph is a sound declaration followed by 45 | one or more pipes that configure it. Graph 46 | declarations will be additive, e.g. two 47 | line with '@synth ~ (osc)' will create two 48 | oscillators. 49 | This definition is slightly longer than it 50 | needs to be so that we can make the first 51 | tilde optional. Either '@synth (osc tri)' 52 | or '@synth ~ (osc tri)' will be valid. 53 | */ 54 | 55 | Graph = soundAccessor "~"? PolySoundBlock Pipe? 56 | 57 | /* 58 | Sound blocks look like functions, e.g. 59 | '(osc sine 1)'. You can string several 60 | together using pipes, which will literally 61 | pipe the sounds together. 62 | */ 63 | 64 | PolySoundBlock = MonoSoundBlock ("+" MonoSoundBlock)* 65 | MonoSoundBlock = "(" listOf ")" name? 66 | name = ":" alnum+ 67 | 68 | Pipe = ("~" PolySoundBlock)+ 69 | 70 | /* 71 | PLAY LINES 72 | A play line is a play keyword (either 'play' 73 | or '>'), followed by the sound we want to play, 74 | followed by a pattern. Each pattern uses a 75 | different enclosing bracket. They could also 76 | use a SoundBlock-like definition I guess. 77 | */ 78 | 79 | Play = PlayKeyword sound Pattern 80 | PlayKeyword = "play" | ">" 81 | 82 | /* 83 | PATTERNS 84 | The play line is expecting one or more function 85 | calls that determine what the sound does. Those 86 | might be things like (rhythm xox), (notes E3 D3), 87 | and (times 0.2 0.3 0.5). Determining what tools 88 | are possible should be a *runtime* concern, not 89 | a grammar-level concern. 90 | */ 91 | 92 | Pattern = listOf 93 | 94 | /* 95 | PRIMITIVES 96 | Here are the primitive types we're working with. 97 | */ 98 | 99 | list = "[" listOf "]" 100 | range = "[" int ".." int "]" -- number 101 | | "[" note ".." note "]" -- note 102 | 103 | delimiter = " " 104 | 105 | float = "-"? digit* "." digit+ -- fullFloat 106 | | "-"? digit "." -- dot 107 | | "-"? digit+ -- int 108 | 109 | int = "-"? digit+ 110 | 111 | note = letter "#" digit+ -- sharp 112 | | letter "b" digit+ -- flat 113 | | alnum+ -- major 114 | 115 | rhythm = "r"? digit+ letter 116 | } -------------------------------------------------------------------------------- /slang-grammar.js: -------------------------------------------------------------------------------- 1 | module.exports = ` 2 | Sound { 3 | Line = Graph | Play | Comment 4 | 5 | /* 6 | A comment is any line that begins with # 7 | */ 8 | 9 | Comment = "#" any+ 10 | 11 | /* 12 | SOUND OBJECTS 13 | A sound object is a '@synth' variable. We 14 | can also access part of the sound's graph 15 | using dot notation, e.g. '@synth.osc1'. 16 | We'll only accept soundAccessors in a few 17 | places. 18 | */ 19 | 20 | sound = "@" alnum+ 21 | propertyAccessor = "." alnum+ 22 | soundAccessor = sound propertyAccessor? 23 | 24 | /* 25 | FUNCTIONS 26 | A function is anything in parentheses. These 27 | will power blocks and arbitrary tools that 28 | might spit out lists, numbers, etc. The syntax 29 | is inspired by Clojure. Notice that the first 30 | type of sound argument is... a function! This 31 | enables is to write nested functions. Imagine: 32 | (notes (random (chord major E3))) 33 | */ 34 | 35 | function = "(" listOf ")" 36 | soundArgument = function -- function 37 | | range -- range 38 | | list -- list 39 | | rhythm -- rhythm 40 | | float -- float 41 | | note -- note 42 | 43 | /* 44 | GRAPH LINES 45 | A graph is a sound declaration followed by 46 | one or more pipes that configure it. Graph 47 | declarations will be additive, e.g. two 48 | line with '@synth ~ (osc)' will create two 49 | oscillators. 50 | This definition is slightly longer than it 51 | needs to be so that we can make the first 52 | tilde optional. Either '@synth (osc tri)' 53 | or '@synth ~ (osc tri)' will be valid. 54 | */ 55 | 56 | Graph = soundAccessor "~"? PolySoundBlock Pipe? 57 | 58 | /* 59 | Sound blocks look like functions, e.g. 60 | '(osc sine 1)'. You can string several 61 | together using pipes, which will literally 62 | pipe the sounds together. 63 | */ 64 | 65 | PolySoundBlock = MonoSoundBlock ("+" MonoSoundBlock)* 66 | MonoSoundBlock = "(" listOf ")" name? 67 | name = ":" alnum+ 68 | 69 | Pipe = ("~" PolySoundBlock)+ 70 | 71 | /* 72 | PLAY LINES 73 | A play line is a play keyword (either 'play' 74 | or '>'), followed by the sound we want to play, 75 | followed by a pattern. Each pattern uses a 76 | different enclosing bracket. They could also 77 | use a SoundBlock-like definition I guess. 78 | */ 79 | 80 | Play = PlayKeyword sound Pattern 81 | PlayKeyword = "play" | ">" 82 | 83 | /* 84 | PATTERNS 85 | The play line is expecting one or more function 86 | calls that determine what the sound does. Those 87 | might be things like (rhythm xox), (notes E3 D3), 88 | and (times 0.2 0.3 0.5). Determining what tools 89 | are possible should be a *runtime* concern, not 90 | a grammar-level concern. 91 | */ 92 | 93 | Pattern = listOf 94 | 95 | /* 96 | PRIMITIVES 97 | Here are the primitive types we're working with. 98 | */ 99 | 100 | list = "[" listOf "]" 101 | range = "[" int ".." int "]" -- number 102 | | "[" note ".." note "]" -- note 103 | 104 | delimiter = " " 105 | 106 | float = "-"? digit* "." digit+ -- fullFloat 107 | | "-"? digit "." -- dot 108 | | "-"? digit+ -- int 109 | 110 | int = "-"? digit+ 111 | 112 | note = letter "#" digit+ -- sharp 113 | | letter "b" digit+ -- flat 114 | | alnum+ -- major 115 | 116 | rhythm = "r"? digit+ letter 117 | } 118 | `; 119 | -------------------------------------------------------------------------------- /classes/ADSR.js: -------------------------------------------------------------------------------- 1 | import ADSREnvelope from 'adsr-envelope'; 2 | import Block from './Block'; 3 | import classMap from './classMap'; 4 | import context from '../helpers/context'; 5 | import { parseArgument } from '../helpers/parseArguments'; 6 | 7 | /* 8 | ADSR Envelope 9 | The envelope is a special beast because it needs to have 10 | some knowledge and control over another Block. I was hoping 11 | I could make it work as an adjacent pipe, e.g. (osc) + (adsr), 12 | but that proved too weird and difficult to manage. Instead 13 | we're going to embrace the nested nature of our language and 14 | make the first argument a Block. This way we have full control 15 | over the block and can see what type it is to adjust our numbers 16 | accordingly. 17 | 18 | Usage: 19 | @synth (adsr (osc tri) .1 0 1 .2) 20 | */ 21 | 22 | class ADSR extends Block { 23 | constructor(block, ...args) { 24 | super(...args); 25 | 26 | if (!block.type || block.type !== 'function') { 27 | throw new Error('ADSR needs a block as its first argument, e.g. (osc sine)'); 28 | } 29 | 30 | this.block = new classMap[block.function](...block.arguments); 31 | this.block.setPolyMode(true); 32 | this.block.instantiate(); 33 | this.blockType = block.function; 34 | 35 | this.attack = this.arguments[0] || parseArgument(0.01); 36 | this.decay = this.arguments[1] || parseArgument(0); 37 | this.sustain = this.arguments[2] || parseArgument(1); 38 | this.release = this.arguments[3] || parseArgument(0.05); 39 | 40 | this.envelope = new ADSREnvelope({ 41 | attackTime: 0.05, 42 | decayTime: 0, 43 | peakLevel: 1, 44 | sustainLevel: 1, 45 | releaseTime: 0.05, 46 | gateTime: 0.25, 47 | releaseCurve: "exp", 48 | }); 49 | } 50 | 51 | schedule(start, stop, note) { 52 | // Create our envelope 53 | const env = this.envelope.clone(); 54 | env.attackTime = this.attack.next(); 55 | env.decayTime = this.decay.next(); 56 | env.sustainLevel = this.sustain.next(); 57 | env.releaseTime = this.release.next(); 58 | env.peakLevel = this.blockType === 'filter' ? 11025 : 1; 59 | env.gateTime = stop - start; 60 | 61 | 62 | // Schedule our block in "envelope mode", which will return 63 | // either an oscillator (for calling `stop` on) or a node 64 | // that we can connect to our gain. 65 | const scheduleResult = this.block.schedule(start, stop, note, true); 66 | 67 | // I don't love this logic, but there are two very separate 68 | // behaviors here depending on what the envelope applies to. 69 | // If it's an oscillator, the envelope's gain is part of the 70 | // audio signal and we need to control the stop time. 71 | // If it's any other block, we get a property passed back that 72 | // our gain node applies to. 73 | let gain; 74 | if (this.blockType === 'osc') { 75 | gain = context.createGain(); 76 | env.applyTo(gain.gain, start); 77 | scheduleResult.node.stop(start + env.duration); 78 | scheduleResult.node.connect(gain); 79 | } else { 80 | // connect the gain to the property that was passed back 81 | env.applyTo(scheduleResult.property); 82 | } 83 | 84 | if (this.getPolyMode()) { 85 | return { 86 | input: scheduleResult.node, 87 | output: this.blockType === 'osc' ? gain : scheduleResult.node, 88 | }; 89 | } 90 | 91 | if (this.blockType === 'osc') { 92 | gain.connect(this.getOutput()); 93 | } else { 94 | scheduleResult.node.connect(this.getOutput()); 95 | } 96 | 97 | // TODO: only create gain for osc, otherwise apply to existing property 98 | } 99 | } 100 | 101 | export default ADSR; 102 | -------------------------------------------------------------------------------- /backend/server.js: -------------------------------------------------------------------------------- 1 | /* 2 | DISCLAIMER: my vps is running an old version of node. 3 | Does that ever happen to you? I'm writing this mostly 4 | in old school ES5 to avoid having to upgrade right now. 5 | If you contribute anything to this file... please be 6 | kind. I'll upgrade eventually. 7 | ¯\_(ツ)_/¯ 8 | */ 9 | 10 | const express = require('express'); 11 | const bodyParser = require('body-parser'); 12 | const MongoClient = require('mongodb').MongoClient; 13 | const helpers = require('./helpers'); 14 | const path = require('path'); 15 | 16 | const PORT = process.env.PORT || 8000; 17 | 18 | const app = express(); 19 | 20 | // We're serving static assets out of /public 21 | app.use(express.static('public')); 22 | // We are going to send JSON blobs so let's have 23 | // bodyParser get the data ready for us. 24 | app.use(bodyParser.json()); 25 | // Using EJS to build the patch page. 26 | app.set('view engine', 'ejs'); 27 | // Point to the views folder 28 | app.set('views', path.join(__dirname, './views')); 29 | 30 | // Connect to the database, add our routes, then start the server. 31 | MongoClient.connect('mongodb://127.0.0.1:27017/slang', function(err, db) { 32 | if (err) { 33 | console.log('Oh no! The mongo database failed to start.'); 34 | console.error(err); 35 | return process.exit(1); 36 | } 37 | 38 | // Load a saved patch, if one exists. 39 | 40 | app.get('/:id', function (req, res) { 41 | const patches = db.collection('patches'); 42 | patches.find({ _id: req.params.id }).toArray(function (err, items) { 43 | if (err || !items.length) { 44 | // Let's be clever and present a "not found" error 45 | // inside of the text editor itself. 46 | return res.render('patch', { 47 | patch: { 48 | text: '# Whoops! We couldn’t a patch at this URL.\n' 49 | + '# You get the 404 womp womp patch instead.\n' 50 | + '\n' 51 | + '@notFoundLeft (adsr (osc tri)) + (pan -1)\n' 52 | + '@notFoundRight (adsr (osc tri)) + (pan 1)\n' 53 | + '\n' 54 | + 'play @notFoundLeft\n' 55 | + ' (rhythm [8n 8n 8n 8n 8n r1n r32n])\n' 56 | + ' (notes [c5 b4 a#4 a4 g#4])\n' 57 | + '\n' 58 | + 'play @notFoundRight\n' 59 | + ' (rhythm [r32n 8n 8n 8n 8n 8n r1n])\n' 60 | + ' (notes (transpose -5 [c5 b4 a#4 a4 g#4]))', 61 | }, 62 | }); 63 | } 64 | 65 | const patch = items[0]; 66 | // We're using a string literal to dump out the text, so 67 | // to avoid XSS let's remove any backticks present in the 68 | // string itself. 69 | patch.text = patch.text.replace(/\`/g, ''); 70 | res.render('patch', { patch: patch }); 71 | }); 72 | }); 73 | 74 | // Save a new patch. 75 | 76 | // There are lots of ways we could do this, but here's what we're 77 | // going to do and some rationale behind it: the client will send 78 | // a POST to the /save route with the text they want to save, we'll 79 | // respond by sending the ID as text, and the client will then 80 | // redirect itself to the new URL, hitting the `/:id` route above. 81 | 82 | // We could just as well take the response on the client and display 83 | // it as a URL that the user can copy, but because the URL represents 84 | // the text *at the moment it was saved*, I don't want them to 85 | // generate it, continue typing, and assume that their patch will 86 | // update in some way. Redirecting to the new URL reinforces the fact 87 | // that it's a one-time save. 88 | 89 | app.post('/save', function (req, res) { 90 | const text = req.body.text; 91 | 92 | // Need to set some limits so you don't blow up my database! 93 | if (!text || text.length > 10000) { 94 | res.statusCode = '400'; 95 | return res.send(':('); 96 | } 97 | 98 | const patches = db.collection('patches'); 99 | 100 | // In theory there can be ID collisions, and in practice that 101 | // does happen occasionally when you have a system like this 102 | // doing ~1000+ saves per day, but we're not going to worry 103 | // about that right now. 104 | const id = helpers.createHash(); 105 | patches.insert({ _id: id, text: text }, function (err, item) { 106 | if (err) { 107 | res.statusCode = 400; 108 | return res.send('error'); 109 | } 110 | // return the URL 111 | return res.send(id); 112 | }); 113 | }); 114 | 115 | // Start the server. 116 | app.listen(PORT, function () { 117 | console.log('Slang is running on port', (process.env.PORT || 8000), 'at', Date()); 118 | }); 119 | }); 120 | 121 | 122 | -------------------------------------------------------------------------------- /classes/Sound.js: -------------------------------------------------------------------------------- 1 | import Scheduler from './Scheduler'; 2 | import context from '../helpers/context'; 3 | import PolyBlock from './PolyBlock'; 4 | import classMap from './classMap'; 5 | 6 | class Sound { 7 | constructor(graph) { 8 | this.name = graph.sound.name; 9 | 10 | // We're going to store all of the block 11 | // instances as a flat object keyed by 12 | // the block's ID. 13 | this.model = {}; 14 | this.schedulers = []; 15 | 16 | // We're also going to store a map of 17 | // the connections between Blocks. These 18 | // will be arrays of strings keyed starting 19 | // at 0. 20 | this.connections = {}; 21 | 22 | // We'll increment this and append it to 23 | // block IDs to ensure uniqueness. 24 | this.idFactory = 0; 25 | 26 | // This is for debugging. 27 | this._graphs = []; 28 | 29 | // ======================================== 30 | // INSTANTIATE 31 | // ======================================== 32 | 33 | // Each sound has a final destination node 34 | // that all of the pipes end with. 35 | this.output = context.createGain(); 36 | this.output.gain.setValueAtTime(0.5, context.currentTime, 0); 37 | this.output.connect(context.destination); 38 | 39 | // Create the first graph 40 | this.appendToGraph(graph); 41 | } 42 | 43 | nextId() { 44 | return `--${++this.idFactory}`; 45 | } 46 | 47 | appendToGraph(graph) { 48 | // take additional graph info and append it 49 | // to the current sound. 50 | const nextConnectionKey = Object.keys(this.connections).length; 51 | this.createGraph(graph.pipe, nextConnectionKey); 52 | this.connectGraph(nextConnectionKey); 53 | 54 | // Add to the debug graphs array in case 55 | // we need to poke around. 56 | this._graphs.push(graph); 57 | } 58 | 59 | createGraph(pipe, index) { 60 | // Create a new set of connections. 61 | this.connections[index] = []; 62 | 63 | const model = pipe.reduce((model, block, i) => { 64 | // A block can either be a simple Block function 65 | // like `osc` & `filter`, OR it can be a polyblock. 66 | // We have to treat those two cases differently. 67 | 68 | if (block.type && block.type === 'polyblock') { 69 | // It seems like PolyBlocks aren't going to 70 | // be able to support name variables? Tbd. 71 | const thisId = `poly${this.nextId()}`; 72 | model[thisId] = new PolyBlock(block); 73 | model[thisId].instantiate(); 74 | 75 | this.connections[index].push(thisId); 76 | 77 | return model; 78 | } else { 79 | const thisId = block.name || `${block.function}${this.nextId()}`; 80 | if (classMap[block.function]) { 81 | // If the block was named, we'll stash 82 | // it by name in the model. Otherwise, 83 | // give it an internal ID that we can 84 | // use to reference it. 85 | model[thisId] = new classMap[block.function](...block.arguments); 86 | model[thisId].instantiate(); 87 | 88 | // Add this ID to the connection list. 89 | this.connections[index].push(thisId); 90 | 91 | return model; 92 | } else { 93 | throw new Error(`${this.name}: Block type "${block.function}" does not exist`); 94 | } 95 | } 96 | }, {}); 97 | 98 | // Append this all to our model 99 | this.model = { 100 | ...this.model, 101 | ...model, 102 | }; 103 | } 104 | 105 | connectGraph(index) { 106 | // We're going to loop through and connect each 107 | // Block to the next. The last one will connect 108 | // to this Sound's output. 109 | 110 | const connections = this.connections[index]; 111 | 112 | const length = this.connections[index].length; 113 | 114 | for (let i = 0; i < length; i++) { 115 | // If there is an adjacent block... 116 | if (connections[i] && connections[i + 1]) { 117 | // Connect them! 118 | this.model[connections[i]].connect( 119 | this.model[connections[i + 1]] 120 | ); 121 | } else { 122 | // We're at the final block; connect 123 | // it to the output. 124 | this.model[connections[i]] 125 | .getOutput() 126 | .connect(this.output); 127 | } 128 | } 129 | } 130 | 131 | schedule(patterns) { 132 | // This method is called once for each new set of 133 | // patterns to use. We'll create a scheduler for 134 | // each one. 135 | 136 | const scheduler = new Scheduler(patterns); 137 | 138 | scheduler.tick((start, stop, note) => { 139 | Object.keys(this.model).forEach((id) => { 140 | this.model[id].schedule(start, stop, note); 141 | }); 142 | }); 143 | 144 | this.schedulers.push(scheduler); 145 | } 146 | 147 | start(timestamp) { 148 | this.schedulers.forEach(scheduler => scheduler.start(timestamp)); 149 | } 150 | 151 | destroy() { 152 | this.schedulers.forEach(scheduler => scheduler.stop()); 153 | 154 | Object.keys(this.model).forEach((id) => { 155 | this.model[id].destroy(); 156 | }); 157 | 158 | this.output.disconnect(); 159 | } 160 | } 161 | 162 | export default Sound; 163 | -------------------------------------------------------------------------------- /editor.js: -------------------------------------------------------------------------------- 1 | import { runScene, clearScene } from './slang'; 2 | import context from './helpers/context'; 3 | 4 | import CodeMirror from 'codemirror'; 5 | import * as simpleMode from 'codemirror/addon/mode/simple'; 6 | import js from 'codemirror/mode/clojure/clojure'; 7 | import 'codemirror/lib/codemirror.css'; 8 | import 'codemirror/theme/duotone-light.css'; 9 | 10 | import classMap from './classes/classMap'; 11 | import { functionMap } from './functions'; 12 | 13 | // ------------------------------ EDITOR ------------------------------ 14 | 15 | const keywords = Object.keys(classMap).concat(Object.keys(functionMap), ['notes', 'rhythm']); 16 | const keywordRegex = new RegExp(`(?:${keywords.join('|')})\\b`); 17 | 18 | CodeMirror.defineSimpleMode("slang", { 19 | start: [ 20 | { 21 | regex: keywordRegex, 22 | token: "keyword" 23 | }, 24 | { 25 | regex: /[a-g](\#|b)?\d+/i, 26 | token: "note" 27 | }, 28 | { 29 | regex: /\d+(n|t)/i, 30 | token: "beat" 31 | }, 32 | { 33 | regex: /r\d+(n|t)/i, 34 | token: "rest" 35 | }, 36 | { 37 | regex: /0x[a-f\d]+|[-+]?(?:\.\d+|\d+\.?\d*)(?:e[-+]?\d+)?/i, 38 | token: "number" 39 | }, 40 | { 41 | regex: /(\+|\~)/, 42 | token: "pipe" 43 | }, 44 | { 45 | regex: /\#.+/, 46 | token: "comment" 47 | }, 48 | { 49 | regex: /\@[a-z$][\w$]*/, 50 | token: "variable" 51 | }, 52 | ], 53 | }); 54 | 55 | const existingCode = window.localStorage.getItem('code'); 56 | const defaultCode = `# Welcome to Slang! Here's an example to get you started. 57 | # Click the Run button above to start playing this code. 58 | 59 | # Make a sound called @synth with a triangle wave 60 | @synth (adsr (osc tri) 64n 8n 0.5 8n) 61 | 62 | # Play the @synth sound 63 | play @synth 64 | # play a quarter note and then an eighth note 65 | (rhythm [4n 8n]) 66 | # play a C major scale 67 | (notes [c3 d3 e3 f3 g3 a3 b3 c4]) 68 | `; 69 | 70 | const editor = CodeMirror(document.querySelector('#editor'), { 71 | value: window.slangPatch || existingCode || defaultCode, 72 | mode: 'slang', 73 | theme: 'duotone-light', 74 | indentWithTabs: true, 75 | }); 76 | 77 | function run() { 78 | context.resume(); 79 | const value = editor.getValue(); 80 | 81 | // Any error generated by running the scene should 82 | // be caught here (but not runtime errors like 83 | // a function not existing). 84 | try { 85 | runScene(value); 86 | clearError(); 87 | status('Running'); 88 | } catch (e) { 89 | console.error(e); 90 | displayError(e); 91 | status('Error'); 92 | } 93 | // save the scene to localStorage 94 | window.localStorage.setItem('code', value); 95 | } 96 | 97 | function stop() { 98 | clearScene(); 99 | status('Stopped'); 100 | } 101 | 102 | editor.on('keydown', (c, e) => { 103 | if (e.key === 'Enter' && e.metaKey && e.shiftKey) { 104 | stop(); 105 | } else if (e.key === 'Enter' && e.metaKey) { 106 | run(); 107 | } 108 | }); 109 | 110 | // ------------------------------ CONTROLS ------------------------------ 111 | 112 | const $run = document.querySelector('[data-run]'); 113 | const $stop = document.querySelector('[data-stop]'); 114 | const $status = document.querySelector('[data-status]'); 115 | const $url = document.querySelector('[data-url]'); 116 | 117 | $run.addEventListener('click', run); 118 | $stop.addEventListener('click', stop); 119 | $url.addEventListener('click', createUrl); 120 | 121 | function status(str) { 122 | $status.textContent = str; 123 | } 124 | 125 | function createUrl() { 126 | const value = editor.getValue(); 127 | 128 | // The /save route of our express server is 129 | // expecting a JSON blob containing a `text` field. 130 | fetch('/save', { 131 | method: 'POST', 132 | mode: 'cors', 133 | cache: 'default', 134 | body: JSON.stringify({ text: value }), 135 | headers: { 136 | 'Content-Type': 'application/json', 137 | 'Access-Control-Request-Method': 'post', 138 | 'Access-Control-Allow-Credentials': 'true', 139 | }, 140 | }) 141 | .then(response => response.text()) 142 | .then((text) => { 143 | // Redirect the browser to the newly created patch. 144 | window.location.pathname = `/${text}`; 145 | }) 146 | .catch((e) => { 147 | console.error(e); 148 | displayError('Oh no! There’s a problem with the server. Try again in a bit.') 149 | }); 150 | } 151 | 152 | // ------------------------------ ERROR HANDLING ------------------------------ 153 | 154 | // Stash a few references to elements that we'll use to present 155 | // errors to the user. 156 | const $error = document.querySelector('#error'); 157 | const $errorContent = document.querySelector('#error-content'); 158 | const $dismiss = document.querySelector('#dismiss'); 159 | 160 | $dismiss.addEventListener('click', () => { 161 | $error.classList.remove('show'); 162 | }); 163 | 164 | function displayError(message) { 165 | $error.classList.add('show'); 166 | $errorContent.textContent = String(message).trim(); 167 | } 168 | 169 | function clearError() { 170 | $error.classList.remove('show'); 171 | } 172 | 173 | -------------------------------------------------------------------------------- /slang.js: -------------------------------------------------------------------------------- 1 | import util from 'util'; 2 | import ohm from 'ohm-js'; 3 | import range from 'lodash/range'; 4 | import * as Range from 'tonal-range'; 5 | import grammarDefinition from './slang-grammar'; 6 | import runtime from './runtime'; 7 | 8 | const grammar = ohm.grammar(grammarDefinition); 9 | const semantics = grammar.createSemantics(); 10 | 11 | semantics.addOperation('toAST', { 12 | Comment(hash, text) { 13 | return { 14 | type: 'comment', 15 | }; 16 | }, 17 | Line: rule => rule.toAST(), 18 | Graph(soundAccessor, tilde, firstPolyBlock, pipe) { 19 | let pipeAST = pipe.toAST(); 20 | // This set of pipes might not exist at all, 21 | // in which case we want to default to an 22 | // empty array so nothing fails. 23 | pipeAST = (pipeAST && pipeAST[0]) || []; 24 | return { 25 | type: 'graph', 26 | sound: soundAccessor.toAST(), 27 | pipe: [firstPolyBlock.toAST(), ...pipeAST], 28 | }; 29 | }, 30 | Pipe: (char, soundBlock) => soundBlock.toAST(), 31 | 32 | function: (lp, soundArguments, rp) => { 33 | const [func, ...rest] = soundArguments.asIteration().toAST(); 34 | return { 35 | type: 'function', 36 | function: func, 37 | arguments: rest, 38 | }; 39 | }, 40 | 41 | PolySoundBlock(monoSB, plus, rest) { 42 | 43 | // Because of the way we wrote the parser, 44 | // normal non-polyphonic blocks will still 45 | // hit the PolySoundBlock definition. It's 46 | // easy to tell if it's really polyphonic 47 | // or not, though: just see if the `rest` 48 | // has a length. 49 | const polyblocks = rest.toAST(); 50 | if (!polyblocks.length) { 51 | return monoSB.toAST(); 52 | } 53 | 54 | // If we're here it really *is* polyphonic, 55 | // so let's return a structured polyblock 56 | // object with a list of Blocks. 57 | return { 58 | type: 'polyblock', 59 | blocks: [monoSB.toAST(), ...rest.toAST()], 60 | }; 61 | }, 62 | 63 | MonoSoundBlock(lp, list, rp, name) { 64 | const [func, ...rest] = list.asIteration().toAST(); 65 | return { 66 | type: 'block', 67 | // This is the name of the block function. 68 | function: func, 69 | // This is will be a list of soundArguments. 70 | arguments: rest, 71 | name: name.sourceString, 72 | } 73 | }, 74 | 75 | // soundArgument: s => s.sourceString, 76 | soundAccessor(sound, property) { 77 | return { 78 | name: sound.sourceString, 79 | property: property.sourceString, 80 | }; 81 | }, 82 | 83 | Play(kw, sound, pattern) { 84 | return { 85 | type: 'play', 86 | sound: { name: sound.sourceString }, 87 | patterns: pattern.asIteration().toAST(), 88 | }; 89 | }, 90 | 91 | list(lb, soundArguments, rb) { 92 | return { 93 | type: 'list', 94 | arguments: soundArguments.asIteration().toAST(), 95 | }; 96 | }, 97 | 98 | range_number(lb, arg1, __, arg2, rb) { 99 | return { 100 | type: 'list', 101 | arguments: range( 102 | parseInt(arg1.sourceString), 103 | parseInt(arg2.sourceString) 104 | ), 105 | }; 106 | }, 107 | 108 | range_note(lb, arg1, __, arg2, rb) { 109 | return { 110 | type: 'list', 111 | arguments: Range.chromatic( 112 | [arg1.sourceString, arg2.sourceString] 113 | ), 114 | }; 115 | }, 116 | 117 | int: (neg, i) => neg.sourceString ? parseInt(i.sourceString) * -1 : parseInt(i.sourceString), 118 | float: (f) => parseFloat(f.sourceString), 119 | note: n => isNaN(n.sourceString) ? n.sourceString : +n.sourceString, 120 | rhythm: (r, num, beat) => r.sourceString + num.sourceString + beat.sourceString, 121 | }); 122 | 123 | export function runScene(text) { 124 | // The parser can handle one line at a time 125 | // so we'll need to prepare an array with 126 | // "lines of code" that can be parsed individually. 127 | // Since we're going to support extending code 128 | // onto the next line when it starts with a tab 129 | // we'll have to do some extra work to figure out 130 | // what exactly a line means. 131 | 132 | const sceneLines = text 133 | // 1. split them by newline 134 | .split('\n') 135 | // 2. filter out empty lines 136 | .filter(l => !!l.trim()) 137 | // 3. reduce the current set 138 | // by appending tab-prefixed 139 | // lines onto their predecessor. 140 | .reduce((lines, thisLine, i) => { 141 | // If this line is only whitespace and a comment, 142 | // let's return early and ignore it here. This will 143 | // allow us to support multi-line calls with comments 144 | // interspersed. 145 | if (thisLine.trim().charAt(0) === '#') { 146 | return lines; 147 | } 148 | 149 | // If the line starts with a tab 150 | // and it's also not a comment 151 | // add the contents onto the last line. 152 | if (thisLine.startsWith('\t') && lines.length) { 153 | // Ohm doesn't consider tabs as whitespace, 154 | // so let's trim the edges and use a space instead. 155 | const padWithSpace = ( 156 | !lines[lines.length - 1].trim().endsWith('[') 157 | && !thisLine.trim().startsWith(']') 158 | ); 159 | lines[lines.length - 1] += (padWithSpace ? ' ' : '') + thisLine.trim(); 160 | } else { 161 | // This is a normal line. Add it to the array. 162 | lines.push(thisLine); 163 | } 164 | 165 | return lines; 166 | }, []); 167 | 168 | // Now that we have the definitive set of code lines, 169 | // let's parse them! 170 | const parsedScene = sceneLines 171 | .map((s, i) => { 172 | // First we call grammar.match, which 173 | // returns a structured Ohm MatchObject. 174 | const match = grammar.match(s); 175 | // This might fail, in which case it's 176 | // on us to define what the experience 177 | // of that failure is. This is a rabbit 178 | // hole; for now let's just throw it and 179 | // put it on the screen. 180 | if (!match.succeeded()) { 181 | // These errors are not going to have 182 | // the correct line numbers because 183 | // of the pre-processing step above 184 | // that trimmed and concatenated lines 185 | // that start with tabs + whitespace. 186 | // This is what source maps are for! 187 | // Those seem complicated so instead 188 | // let's be marginally helpful by 189 | // referencing which "command" it is. 190 | throw new Error( 191 | String(match.message) 192 | .replace('Line 1', `Command ${i}`) 193 | .replace('> 1 | ', '> ') 194 | ); 195 | } 196 | // Next we give that to the semantics tool 197 | // that we imbued with the `toAST` operation. 198 | // That will turn our parsed grammar into a 199 | // Concrete Syntax Tree, which is the blob 200 | // of data our interpreter needs to run the code. 201 | return semantics(match).toAST(); 202 | }) 203 | // The runtime doesn't care about comment lines 204 | // so let's throw them away. 205 | .filter(line => line.type !== 'comment'); 206 | 207 | // I'm sure there are better ways to approach this, 208 | // but for now let's preemptively clear the scene. 209 | runtime.clearScene(); 210 | 211 | console.log('%cRunning scene! Parsed syntax tree:', 'color: green;', parsedScene); 212 | 213 | // Start the show! 214 | runtime.runScene(parsedScene); 215 | } 216 | 217 | export function clearScene() { 218 | runtime.clearScene(); 219 | } 220 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | # Slang — An audio programming language built in JS 4 | 5 |

6 | Play with Slang 7 |

8 | 9 | - [How to write Slang](https://github.com/kylestetz/slang#how-to-write-slang) 10 | - [Reference](https://github.com/kylestetz/slang#reference) 11 | - [Examples](https://github.com/kylestetz/slang#examples) 12 | 13 | Slang was created to explore implementing a programming language entirely in the browser. Parsing is handled by [Ohm.js](https://github.com/harc/ohm) using a [custom grammar](./slang-grammar.js), the editor uses CodeMirror with a simple syntax definition, and the runtime itself is written in JS using the Web Audio API. 14 | 15 | 16 | 17 | ### Goals of this project 18 | 19 | I've always wanted to write a programming language from scratch, but as someone who didn't study computer science I find it incredibly intimidating. Discovering [Ohm.js](https://github.com/harc/ohm) changed my mind; its incredible editor and approachable JS API make it possible to experiment quickly with a lot of feedback. This project is my first pass at building a language and runtime environment from start to finish. 20 | 21 | This is not meant to be a great or comprehensive language, but I do hope this project can serve as a roadmap if you'd like to build your own! 22 | 23 | You'll notice a distinct lack of in-context error handling, inline docs, helpful UI, etc. Creating a great editor experience was not a goal of this project and it would take a lot of work to get there. I did my best to make it pleasant to use. 24 | 25 | # How to write Slang 26 | 27 | Slang consists of **sound lines** and **play lines**. Sound lines build up a synthesizer (or drum machine), then play lines tell those synthesizers or drum machines what to play. 28 | 29 | ``` 30 | @synth (adsr (osc tri) 64n 8n 0.5 8n) 31 | 32 | play @synth 33 | (rhythm [8n]) 34 | (notes [c3 d3 e3 f3 g3 a3 b3 c4]) 35 | ``` 36 | 37 | It turns out that explaining your own programming language is ridiculously hard, so I suggest skipping to the [**Examples**](https://github.com/kylestetz/slang#examples) section below and trying those out before reading all of these docs. 38 | 39 | ## Sound Lines 40 | 41 | A sound line establishes a variable (which always starts with `@`) that contains a **chain of sounds**. Sounds always start with either an `(osc)` or `(drums)` and can chain tools like `filter`, `pan`, and `gain` together using the `+` operator. 42 | 43 | Here we have a sine oscillator which gets piped into a lowpass filter and then gain. 44 | ``` 45 | @synth (osc sine) 46 | + (filter lp 100) 47 | + (gain 0.5) 48 | ``` 49 | 50 | You can add multiple sound lines for the same variable; when a note is played all of its corresponding chains of sound will trigger. 51 | 52 | Here's a sound that has two oscillators, the second one pitched up an octave. When the sound plays you'll hear both oscillators firing for each note. 53 | ``` 54 | @synth (osc sine) 55 | @synth (osc sine 12) 56 | 57 | play @synth (notes [e3]) 58 | ``` 59 | 60 | 💡 Try making multiple chains for your synth and panning them left and right to create stereo synths. 61 | 62 | ## Play Lines 63 | 64 | A play line starts with the word `play`, followed by the variable you want to play, and then declares a rhythm and notes to use. You can have multiple play lines referencing a single synth and they will all play independently (e.g. if you want to play polyphonic melodies). 65 | 66 | `rhythm` accepts a list of rhythm values or a function that returns rhythm values, while `notes` accepts a list of notes or a function that returns notes. Let's look at a simple example and then see how we can take advantage of the more advanced functions. 67 | 68 | A simple synth: 69 | ``` 70 | @synth (adsr (osc sine) 64n 8n 0 8n) 71 | play @synth (rhythm [8n]) (notes [e3 e4 e5]) 72 | ``` 73 | 74 | Now let's make a synth that plays a scale using the `(chord)` function. Chord takes a type as its second argument (e.g. `major`, `chromatic`, `phrygian`, etc.) and a root note as its third argument. 75 | ``` 76 | @synth (adsr (osc tri) 64n 8n 0 8n) 77 | play @synth (rhythm [8n]) (notes (chord lydian e3)) 78 | ``` 79 | 80 | Taking it one step further, let's put that `chord` function call within the `random` function, which will randomly pick one of the notes from the chord each time it's called. 81 | ``` 82 | @synth (adsr (osc tri) 64n 8n 0 8n) 83 | play @synth 84 | (rhythm [8n]) 85 | (notes (random (chord lydian e3))) 86 | ``` 87 | 88 | The `flatten` and `repeat` functions, when used inside of `notes`, are a powerful way to create repeating phrases. Since `notes` only takes a single list we use the `flatten` function to take a few different calls and flatten them down. The `repeat` function will take the list we give it and repeat it a number of times, saving us some copying & pasting. 89 | ``` 90 | @synth (adsr (osc sine) 64n 8n 0 8n) 91 | play @synth 92 | (rhythm [8n]) 93 | (notes (flatten [ 94 | (repeat 3 (chord lydian e4 4)) 95 | (chord lydian d4 4) 96 | ])) 97 | ``` 98 | 99 | 💡 Try making multiple play lines for the same sound to make polyphonic melodies and drum beats. 100 | 101 | ## Rhythm and Note Values 102 | 103 | Musical notes and rhythm values are a core concept of Slang, so let's look at them in a bit more detail. 104 | 105 | **Note values** may look familiar to you if you've ever learned an instrument. They contain both a note (like `c`, `f#`, or `a`) and a number that represents the _octave_. If you imagine a large piano, `c4` is the C key right smack in the middle of it. `c3` will be the C key one octave down from that, `c5` will be one octave up, etc. If you're unfamiliar with how the notes map to a keyboard, [here is a handy image](https://3.bp.blogspot.com/-X7bigGKA1ww/WdA-5CZ0e3I/AAAAAAAABWE/MlQ5xkgSEmICpi3HGpqnRn2gwKX7bdz1ACLcBGAs/s320/piano1.png). 106 | 107 | Note values can also be expressed as numbers. If you've worked with synthesizers or electronic instruments before you might be familiar with the MIDI protocol, which among other things represents all notes on a keyboard from `0` - `127`. `c4` is equivalent to the MIDI number `64`, and to move up or down an octave you can add or subtract `12`. 108 | 109 | Here are the notes in the fourth octave with their MIDI number equivalents: 110 | ``` 111 | Notes: c4 c#4 d4 d#4 e4 f4 f#4 g4 g#4 a4 a#4 b4 c5 112 | Numbers: 64 65 66 67 68 69 70 71 72 73 74 75 76 113 | ``` 114 | 115 | **Rhythm values** describe the _length_ of the note as a fraction of a _measure_. The longest possible note in Slang is `1n`, which in music notation would be referred to as a _whole note_. 116 | 117 | Here are all of the values and how they line up, from slowest to fastest: 118 | - `1n` - whole note (the longest note) 119 | - `2n` - half note (half of a whole note) 120 | - `2t` - half note triplet (3 of these is equal to `1n`) 121 | - `4n` - quarter note (a quarter of a whole note) 122 | - `4t` - quarter note triplet (3 of these is equal to `2n`) 123 | - `8n` - eighth note (1/8 of a whole note) 124 | - `8t` - eighth note triplet (3 of these is equal to `4n`) 125 | - `16n` - sixteenth note (1/16 of a whole note) 126 | - `16t` - sixteenth triplet (3 of these is equal to `8n`) 127 | - `32n` - thirty-second note (1/32 of a whole note) 128 | - `32t` - thirty-second triplet (3 of these is equal to `16n`) 129 | - `64n` - sixty-fourth note (1/64 of a whole note) 130 | - `64t` - sixty-fourth triplet (3 of these is equal to `32n`) 131 | 132 | When creating a rhythm in Slang you can freely mix and match these values. A good rule of thumb is that you should aim for all of your rhythm values to add up to `1n` or multiples of `1n`; for example `4n 4n 4n 4n`, `4n 8n 8n 2n`, and `4n 16t 16t 16t 8n 4t 4t 4t` all add up to `1n`. 133 | 134 | Sometimes you'll want to pause for a beat without playing a note. This is called a _rest_ in music terminology. Adding `r` in front of any rhythm value will turn it into a rest (and it will appear lighter in color within the Slang editor); for example `4n 4n 4n r4n` will play three quarter notes and then rest for the length of one quarter note. 135 | 136 | In addition to the rhythm notation you can also use **number values**, which correspond to _seconds_. Slang runs at a tempo of 120 beats per minute, which means that a whole note — `1n` — is exactly `2` seconds long. Writing `(rhythm [2])` and `(rhythm [1n])` produce exactly the same rhythm. This is useful in other functions like [`(adsr)`](https://github.com/kylestetz/slang#amp-envelope---adsr-osc-attack-001-decay-0-sustain-1-release-005) where the attack, decay, and release all accept a rhythm value. 137 | 138 | ## Syntax 139 | 140 | Functions are contained within parentheses, much like in Clojure. The first keyword in a function is the **function name**, which is followed by all of its arguments. Any argument can be a primitive value or a list (neat!); if it's a list, Slang will take one value at a time and loop back to the beginning when it reaches the end. Check out the Reference section for lots of usage examples. 141 | 142 | --- 143 | 144 | # Reference 145 | 146 | In Slang every argument can be either a static value (such as `8n`, `e3`, `1`, etc.) or a list of values. If you provide a list as an argument to a function it will take the next value in the list every time it is called, looping back around when it reaches the end. As an example, the oscillator can accept a list of types: `(osc [sine tri saw])`. Every time a note is hit, it will use the next type in the list. 147 | 148 | ## Sound Functions 149 | 150 | #### Oscillator - `(osc )` 151 | 152 | Creates an oscillator with an optional pitchOffset in semitones. Filters and effects can be chained off of the oscillator using the `+` sign. 153 | 154 | `type`: 155 | - `sine` 156 | - `saw` or `sawtooth` 157 | - `tri` or `triangle` 158 | - `square` 159 | 160 | `pitchOffset`: how many semitones to shift the pitch. 161 | 162 | Usage: 163 | ``` 164 | # Creates a synth with two sine oscillators, one pitched 7 semitones above the root note 165 | @synth (osc sine) 166 | @synth (osc sine 7) 167 | 168 | # Creates a synth that chooses a random oscillator for each note that is hit. 169 | @melody (osc (random [sine saw tri square])) 170 | ``` 171 | 172 | #### Drums - `(drums)` 173 | 174 | Creates a drum machine. It does not accept any arguments. 175 | 176 | When writing a play line, the notes 0 - 11 represent the 12 drum sounds. 177 | 178 | _Pro tip_: Any number above 11 will wrap around using modulus, so for example 25 will trigger sound 1 since `25 % 12 == 1`. This allows you to pass in note values (e.g. `e3`) as well since they correspond to number values. 179 | 180 | #### Amp Envelope - `(adsr )` 181 | 182 | Creates an amp envelope which contains an oscillator followed by ADSR values. If you're unfamiliar with the concept of an amp envelope, check out the _Typical Stages_ section of [this tutorial](https://theproaudiofiles.com/synthesis-101-envelope-parameters-uses/). Amp envelopes control the _volume_ of a sound over the course of a single note. 183 | 184 | Since amp envelopes contain oscillators they can kick off a chain of sound. 185 | 186 | `osc`: An oscillator function, e.g. `(osc tri)` 187 | 188 | `attack`: A rhythm value or number in seconds corresponding to how long the sound takes to fade in 189 | 190 | `decay`: A rhythm value or number in seconds corresponding to how long the sound takes to descend to the sustain value 191 | 192 | `sustain`: A value from `0` - `1` describing how loud the note should be while it is sustained. 193 | 194 | `release`: A rhythm value or number in seconds corresponding to how long the sound takes to fade from its sustain value down to `0`. 195 | 196 | Usage: 197 | ``` 198 | # Try each of these envelopes one at a time to get a feel for what ADSR does. 199 | @synth (adsr (osc sine) 8n 8n 1 4n) 200 | # @synth (adsr (osc sine) 0 0 1 0) 201 | # @synth (adsr (osc sine) 4n 0 1 2n) 202 | # @synth (adsr (osc sine) 16n 16n 0.2 8n) 203 | 204 | play @synth (rhythm [4n]) (notes [c4 d4 e4 f4 g4 a4 b4 c5]) 205 | ``` 206 | 207 | #### Filter - `+ (filter )` 208 | 209 | Creates a filter. This should be part of a sound chain. 210 | 211 | `type`: 212 | - `lp` (lowpass) 213 | - `hp` (highpass) 214 | - `bp` (bandpass) 215 | - `n` (notch) 216 | 217 | `frequency`: A value from 0 - 127 representing the frequencies 0 - 11,025. 218 | 219 | `resonance`: A number from 0 - 100 representing the amount of resonance (Q) to apply. 220 | 221 | Usage: 222 | ``` 223 | @synth (osc sine) + (filter lp 20) 224 | ``` 225 | ``` 226 | # Make a lowpass filter that loops through the 227 | # numbers 10 to 50 one at a time. 228 | @melody (osc saw) + (filter lp [10..50]) 229 | ``` 230 | 231 | #### Gain - `+ (gain )` 232 | 233 | Creates a gain (volume). This should be part of a sound chain. 234 | 235 | `value`: A number from 0 - 1. 236 | 237 | Usage: 238 | ``` 239 | @synth (osc sine) + (gain 0.5) 240 | @melody (osc sine) + (gain [0 0.25 0.5 0.75 1]) 241 | ``` 242 | 243 | #### Pan - `+ (pan )` 244 | 245 | Creates a stereo panner. This should be part of a sound chain. 246 | 247 | `value`: A number from -1 (left) to 0 (center) to 1 (right). 248 | 249 | Usage: 250 | ``` 251 | @synth (osc sine) + (pan -1) 252 | @synth (osc sine 12) + (pan 1) 253 | ``` 254 | 255 | #### Delay - `+ (delay )` 256 | 257 | Creates a delay effect. This should be part of a sound chain. 258 | 259 | _Warning: delay doesn't work very well right now and might cause some weird audio artifacts!_ 260 | 261 | `time`: A rhythm value or a number in seconds. 262 | 263 | `feedback`: A number from 0 - 1 representing the amount of feedback to apply. 264 | 265 | `wet`: A number from 0 - 1 representing the wet level. 266 | 267 | `dry`: A number from 0 - 1 representing the dry level. 268 | 269 | `cutoff`: A number from 0 - 11025 representing the frequency of a cutoff filter on the delay. 270 | 271 | Usage: 272 | ``` 273 | @synth (adsr (osc saw) 64n 8n 0 8n) + (delay 8t 0.4 1 1) 274 | ``` 275 | 276 | Creates 277 | 278 | ## Utility Functions 279 | 280 | #### Chord - `(chord )` 281 | 282 | Returns a list of notes belonging to a chord. 283 | 284 | `type`: A text value representing a chord type, e.g. `major`, `bebop`, `phrygian`. The list of possible chords is taken from [this library](https://github.com/danigb/tonal/blob/master/packages/dictionary/data/scales.json), but with spaces and `#` symbols removed (e.g. `minor #7M pentatonic` becomes `minor7Mpentatonic` in Slang). 285 | 286 | `root`: A note, e.g. `e3`. 287 | 288 | `length` (optional): a number representing exactly how many notes to return in the list. If unspecified, the length of the list will vary from chord to chord. 289 | 290 | Usage: 291 | ``` 292 | @synth (adsr (osc sine) 64n) 293 | play @synth (notes (chord phrygian e3)) 294 | ``` 295 | 296 | #### Random - `(random )` 297 | 298 | Selects a random item from the list each time it is called. The list can be a range such as `[1..10]` or the output of any other utility function, such as `chord` or `flatten`. 299 | 300 | To get repeating random values, check out `shuffle` below. 301 | 302 | Usage: 303 | ``` 304 | @synth (adsr (osc (random [saw tri])) 64n) 305 | + (filter lp [10..50]) 306 | play @synth 307 | (rhythm (random [8n 8t 4n])) 308 | (notes (random (chord phrygian e3))) 309 | ``` 310 | 311 | #### Flatten - `(flatten )` 312 | 313 | Takes a list of lists and flattens it. 314 | 315 | Usage: 316 | ``` 317 | @synth (adsr (osc sine) 64n) 318 | play @synth (notes (flatten [[e3..e4] [d#4..e#3]])) 319 | ``` 320 | ``` 321 | @synth (adsr (osc sine) 64n) 322 | play @synth (notes (flatten [ 323 | (repeat 2 [e3 e4 e5]) 324 | (repeat 2 [d3 d4 d5]) 325 | (repeat 2 [a3 a4 a5]) 326 | [g3 g4 g5] 327 | [f3 f4 f5] 328 | ])) 329 | ``` 330 | 331 | #### Repeat - `(repeat )` 332 | 333 | Takes a list and repeats it `amount` times. Useful when used inside of `flatten`. 334 | 335 | Usage: 336 | ``` 337 | @perc (drums) 338 | play @perc (notes (flatten [ 339 | (repeat 2 [0 6 3 6]) 340 | (repeat 2 [6 0 3 6]) 341 | ])) 342 | ``` 343 | 344 | #### Reverse - `(reverse )` 345 | 346 | Reverses the list. 347 | 348 | Usage: 349 | ``` 350 | @synth (adsr (osc sine) 64n) + (gain 0.5) 351 | play @synth (notes (reverse (chord lydian e4))) 352 | play @synth (notes (chord lydian e5)) 353 | ``` 354 | 355 | #### Shuffle - `(shuffle )` 356 | 357 | Does a one-time random shuffle of the list. Use this if you want a random but repeating sequence, and use `random` if you want a random value each time the function is triggered. 358 | 359 | Usage: 360 | ``` 361 | @bass (adsr (osc tri) 64n) 362 | 363 | play @bass (notes (shuffle (chord phrygian e3))) 364 | ``` 365 | 366 | #### Transpose - `(transpose )` 367 | 368 | Transpose a list of numbers or notes by an amount. 369 | 370 | `amount`: number 371 | 372 | Usage 373 | ``` 374 | @synth (adsr (osc tri) 64n) 375 | 376 | play @synth (notes (flatten [ 377 | (chord phrygian e3) 378 | (transpose 2 (chord phrygian e3)) 379 | ])) 380 | ``` 381 | 382 | #### Interpolate - `(lerp )` 383 | 384 | Generate a list that interpolates from the start to the end value over a number of steps. Useful for creating values that transition slowly over time, especially for tools like `pan` and `gain`. 385 | 386 | `start`: number 387 | 388 | `end`: number 389 | 390 | `steps`: number 391 | 392 | Usage: 393 | ``` 394 | @synth (adsr (osc tri) 64n) 395 | + (pan (lerp -1 1 16)) 396 | 397 | play @synth (notes (chord major d4 16)) 398 | ``` 399 | 400 | --- 401 | 402 | Primitive values: 403 | - **numbers** - integers and floats (`0`, `0.25`, `10000`, etc.) 404 | - **lists** (space-separated) - `[0 1 2 3 4 5 6]` 405 | - **notes** - `e3`, `d#4`, `f2`, etc. 406 | - **rhythm** - `32t`, `32n`, `16t`, `16n`, `8t`, `8n`, `4t`, `4n`, `2n`, `2t`, and `1n` 407 | - **rests** - `r32t`, `r32n`, `r16t`, `r16n`, `r8t`, `r8n`, `r4t`, `r4n`, `r2n`, and `r1n` 408 | - **special strings** - some functions take string arguments, such as `filter` and `osc` 409 | 410 | --- 411 | 412 | # Examples 413 | 414 | A simple synthesizer 415 | ``` 416 | # This is a sound line that establishes a synthesizer called @melody 417 | @melody (adsr (osc sine) 64n 8n 0) 418 | # This is a play line that plays @melody using a rhythm and a list of notes 419 | play @melody (rhythm [8n]) (notes [e3 d3 g3 f3]) 420 | ``` 421 | 422 | A drum machine 423 | ``` 424 | # Drums don't accept any arguments (at the moment!) 425 | @percussion (drums) 426 | 427 | # Hi-hats 428 | play @percussion (rhythm [16n r16n 16n 16n]) (notes [6 7 8]) 429 | # Kick and snare 430 | play @percussion (rhythm [8n]) (notes [0 3 11 0 3 0 3 11]) 431 | ``` 432 | 433 | A randomized synth & bassline with drums 434 | ``` 435 | @synth (adsr (osc saw) 64n) 436 | + (filter lp (random [5..30])) 437 | + (gain 0.2) 438 | + (pan -0.75) 439 | @synth (adsr (osc square 12) 64n) 440 | + (filter lp 15) 441 | + (gain 0.15) 442 | + (pan 0.75) 443 | 444 | @bass (adsr (osc tri) 64n 2n 0.4 4n) 445 | 446 | @drums (drums) + (gain 2) 447 | 448 | play @synth 449 | (rhythm [8t]) 450 | (notes (random (chord phrygian e5))) 451 | 452 | play @bass 453 | (rhythm [1n]) 454 | (notes (random (chord phrygian e2))) 455 | 456 | play @drums 457 | (rhythm [8t r8t 8t 8t 8t r8t]) 458 | (notes [6]) 459 | play @drums 460 | (rhythm [4n 4n 4n r8t 8t 8t]) 461 | (notes [0 3 0 11 11]) 462 | ``` 463 | 464 | Weird and complex little scene (I think this is in 18/8 + 17/8 ??) 465 | ``` 466 | @synth (adsr (osc tri) 0.01 8n 0.2 1n) 467 | + (filter lp (flatten [[5..30] [29..5]]) 10) 468 | @synth (adsr (osc square 12) 0.01 8t 0.2 4n) 469 | + (filter lp (flatten [[0..25] [24..0]])) 470 | @synth (adsr (osc square 7) 0 8n 0 0) 471 | + (filter lp (random [10..25])) 472 | + (gain 0.2) 473 | 474 | @pad (adsr (osc tri) 4n 4n 0.5 1n) 475 | + (filter lp 10) 476 | + (gain 0.5) 477 | @pad (adsr (osc square [7 5]) 4n 4n 0.5 1n) 478 | + (filter lp 5) 479 | + (gain 0.5) 480 | @pad (adsr (osc saw [7 5 7 9]) 1n 4n 0.5 1n) 481 | + (filter hp 10) 482 | + (filter lp 100) 483 | + (gain 0.1) 484 | 485 | @drums (drums) 486 | 487 | play @synth 488 | (rhythm [8t r8n 4n r8n 8t r8n 8t r8n]) 489 | (notes (flatten [ 490 | [[e2 g2] [d2 f2]] 491 | (repeat 3 (chord locrian e4 3)) 492 | (reverse (chord egyptian e4 3)) 493 | ])) 494 | 495 | play @pad 496 | (notes [d4]) 497 | (rhythm [1n r1n r1n r4n r4n]) 498 | 499 | play @drums 500 | (rhythm [8t 8n 4n 8n 8t 8n 8t 8n]) 501 | (notes [7 6]) 502 | play @drums 503 | (rhythm [8t 8n 4n r4n r8n r8t r8t]) 504 | (notes [4 3 1]) 505 | ``` 506 | --------------------------------------------------------------------------------