├── .babelrc ├── .eslintrc.js ├── .gitignore ├── .npmignore ├── README.md ├── demo ├── index.html └── nanotunes.min.js ├── dist ├── nanotunes.js └── nanotunes.min.js ├── externs.js ├── lib └── nanotunes.js ├── package.json ├── tasks └── build.sh └── test ├── e2e.test.js └── unit.test.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": [ 3 | "transform-es2015-block-scoping", 4 | "transform-es2015-arrow-functions" 5 | ] 6 | } -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "env": { 3 | "browser": true, 4 | "commonjs": true, 5 | "es6": true 6 | }, 7 | "extends": "eslint:recommended", 8 | "rules": { 9 | "indent": [ 10 | "error", 11 | 4 12 | ], 13 | "linebreak-style": [ 14 | "error", 15 | "unix" 16 | ], 17 | "quotes": [ 18 | "error", 19 | "single" 20 | ], 21 | "semi": [ 22 | "error", 23 | "always" 24 | ] 25 | } 26 | }; -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .DS_Store -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | demo 2 | dist/nanotunes.min.js 3 | lib 4 | tasks 5 | test 6 | .babelrc 7 | eslintrc.js 8 | externs.js -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # NanoTunes 2 | 3 | [![npm version](https://img.shields.io/npm/v/nanotunes.svg)](https://www.npmjs.com/package/nanotunes) 4 | 5 | NanoTunes is a small schema for structuring music. This repository includes a JavaScript implementation, built upon [`OscillatorNode`](https://developer.mozilla.org/en-US/docs/Web/API/OscillatorNode). 6 | 7 | Developers must shape their own sounds; NanoTunes is simply a means of representing the notes that should be played, and for how long per bar. 8 | 9 | Try out the [demo](https://jamesseanwright.github.io/nanotunes/). 10 | 11 | ## The Schema 12 | 13 | The general structure is: 14 | 15 | `...` 16 | 17 | Where: 18 | 19 | * `` is a three-character string that refers to a previously defined instrument 20 | * `` is a string composed of three ordered parts: 21 | * The note's musical letter (A-G), and an optional # to sharpen the note by a semitone. `X0` can be used as a rest note 22 | * The octave (1-8) 23 | * The length of the note (1-16) 24 | 25 | 26 | ### A "Note" On Note Lengths 27 | 28 | As of this version, NanoTunes only supports semiquaver (1/16th) resolution (this will hopefully be increased in the next major release.) Essentially, this means: 29 | 30 | * A note length of `16` will last for a whole bar (semibreve) 31 | * A note length of `8` will last for half a bar (minim) 32 | * A note length of `4` will last for half a bar (crotchet) 33 | * A note length of `2` will last for half a bar (quaver) 34 | * A note length of `1` will last for half a bar (semiquaver) 35 | 36 | 37 | ### Example 38 | 39 | `VOXC44A#412` translates to: 40 | 41 | * Play this track with a user-defined instrument called "VOX"; 42 | * Play C4 for a quaver length; 43 | * Play A#4 for the remainder of the bar; 44 | 45 | 46 | ## JavaScript Implementation 47 | 48 | ### Example 49 | 50 | ```js 51 | 'use strict'; 52 | 53 | var instruments = { 54 | VOX: { 55 | wave: 'square', 56 | pan: -0.5, 57 | gain: 0.3 58 | }, 59 | 60 | BSS: { 61 | wave: 'triangle', 62 | pan: 0.5, 63 | gain: 0.7 64 | }, 65 | 66 | GTR: { 67 | wave: 'sawtooth', 68 | pan: 0.8, 69 | gain: 0.4 70 | }, 71 | }; 72 | 73 | var tracks = { 74 | title: { 75 | bpm: 180, 76 | isLooping: true, 77 | 78 | parts: [ 79 | 'VOXC44C44G44A44A#44A44G44E44', 80 | 'GTRC32C42C32C42E32E42E32E42G32G42G32G42C32C42C32C4', 81 | 'BSSC24C24G14G14A#14A#14B14B14', 82 | ] 83 | } 84 | }; 85 | 86 | var nanoTunes = new NT(instruments, tracks); 87 | 88 | nanoTunes.play('title'); 89 | ``` 90 | 91 | ### File Sizes: 92 | 93 | * Unminified - 4.3 KB 94 | * Minified - 1.6 KB 95 | * Minified and gzipped - 830 bytes 96 | 97 | 98 | ### Browser Compatibility 99 | 100 | * Chrome 101 | * Firefox 102 | * Edge 103 | * Safari - works, but somewhat buggy 104 | 105 | 106 | ### Setup 107 | 108 | #### npm 109 | 110 | Nanotunes is available as a CommonJS module, which is compatible with Browserify and Webpack. 111 | 112 | ``` 113 | npm i --save nanotunes 114 | ``` 115 | 116 | This module exposes the `NT` constructor directly: 117 | 118 | ```js 119 | 'use strict'; 120 | 121 | const NT = require('nanotunes'); 122 | const nanoTunes = new NT(instruments, tracks); 123 | ``` 124 | 125 | 126 | #### Standalone 127 | 128 | There are two scripts in the `dist` directory 129 | 130 | * An unminified version for those who minify everything 131 | * A minified version for those who bundle third-party dependencies separately 132 | 133 | Loading this script will attach the `NT` constructor to the `window` object. 134 | 135 | 136 | ### API 137 | 138 | #### `NT(instruments, tracks [, audioContext])` 139 | 140 | A constructor function to create a new instance of NanoTunes. The third parameter, which is optional, allows you to use a previously instantiated `AudioContext`. This is desirable if, for example, you're also generating sound effects with the Web Audio API. 141 | 142 | #### Defining Instruments 143 | 144 | The `instruments` parameter is an `Object` whose keys are three-letter instrument names. The below properties can be configured for each definition. most of which conform with the Web Audio API: 145 | 146 | * `wave` - the instrument's waveform. This can be any value assignable value to [`OscillatorNode.type`](https://developer.mozilla.org/en-US/docs/Web/API/OscillatorNode/typehttps://developer.mozilla.org/en-US/docs/Web/API/OscillatorNode/type). Unfortunately, custom waves aren't supported at this time, but will be available in the next major release 147 | 148 | * `pan` - controls the instrument's stereo panning. Like `StereoPannerNode`'s [`pan`](https://developer.mozilla.org/en-US/docs/Web/API/StereoPannerNode/pan) property, it can range from -1 for the left speaker, to 1 for the right speaker 149 | 150 | * `gain` - controls the instrument's pre-output gain. Like `GainNode`'s [`gain`](https://developer.mozilla.org/en-US/docs/Web/API/GainNode/gain) property, it can be a positive number 151 | 152 | 153 | #### Defining Tracks 154 | 155 | The `tracks` parameter is an `Object` whose keys are identifiable track names. For each, these properties can be specified: 156 | 157 | * `bpm`: - the song's beats per minute 158 | 159 | * `isLooping` - the song will loop infinitely if this is truthy, otherwise it will automatically stop after a single play 160 | 161 | * `parts` - an array of strings for each track. These will be played in parallel 162 | 163 | 164 | #### `NT.prototype.play(trackName)` 165 | 166 | Plays a track by name. 167 | 168 | 169 | #### `NT.prototype.stop()` 170 | 171 | Stops playing the current track. 172 | 173 | 174 | #### The `onStop` callback 175 | 176 | It's possible to attach a method to your NanoTunes instance called `onStop`. This will invoke whenever a non-looping track ends. This is useful for scheduling. 177 | 178 | ```js 179 | var nanoTunes = new NT(instruments, tracks); 180 | 181 | nanoTunes.onStop = function onStop() { 182 | console.log('Current track has ended.'); 183 | }; 184 | ``` 185 | 186 | 187 | ### Building Locally 188 | 189 | run `npm i` in the project's directory, which will install Babel. This is used to transpile ES2015 block variables and arrow functions to their ES5 counterparts, because Closure Compiler doesn't support this latest standard as a compilation target. I therefore thought that it would beneficial to align the languages of both the unminified and minified versions. Once Closure supports ES2015 traspilation, Babel will be removed. 190 | 191 | Additionally, the minified build is generated by [Closure Compiler](https://developers.google.com/closure/compiler/docs/gettingstarted_app). You'll need to download the compiler JAR and expose it to your environment as `closure-compiler`. You can achieve this by writing a [shell wrapper](https://gist.github.com/jamesseanwright/4b8e4c907c231a0f7ee71e01f5a33163) and placing this in one of your `$PATH`'s directories. 192 | 193 | 194 | #### Scripts 195 | 196 | * `npm run build` - builds both versions of the distributable, and copies the minified version to the `demo` directory 197 | 198 | 199 | ### Tests 200 | 201 | There are some unit tests which can be run with `npm test`. Increasing coverage is ongoing. Functional tests will also be available soon. -------------------------------------------------------------------------------- /demo/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | NanoTunes 5 | 6 | 7 | 8 | 13 | 14 | 15 | 16 |

NanoTunes

17 | 18 |

NanoTunes is a small schema for structuring music, along with a JavaScript implementation for the browser, built upon OscillatorNode. When minified and gzipped, the player script file is 830 bytes.

19 | 20 |

To get started, consult the README.

21 | 22 |

Demo

23 | 24 | 29 | 30 | 31 | 32 | 33 |
 34 |         
35 | 36 | 37 | 38 | 101 | 102 | 108 | 109 | -------------------------------------------------------------------------------- /demo/nanotunes.min.js: -------------------------------------------------------------------------------- 1 | (function(e){function b(a,c,d){this.a=d||new (e.AudioContext||webkitAudioContext);this.u=a;this.v=c;this.b=[]}var h=/^([A-Z]{3})/,k=/([A-GX]#?)([1-8])([1-9]{1,2})/g,l=new Map([["C",16.35],["C#",17.32],["D",18.35],["D#",19.45],["E",20.6],["F",21.83],["F#",23.12],["G",24.5],["G#",25.96],["A",27.5],["A#",29.14],["B",30.87],["X",0]]);b.prototype.play=function(a){this.stop();a=this.v[a];for(var c=Array(a.parts.length),d=0;d { 99 | if (!isLooping) { 100 | this.stop(); 101 | this.onStop && this.onStop(); 102 | return; 103 | } 104 | 105 | this._enqueueFreqs(oscillator, frequencies, isLooping); 106 | }, Math.round(nextTime) * 1000); 107 | }; 108 | 109 | NT.prototype._applyGain = function _applyGain(node, gain) { 110 | return this._applyEffect(node, gain, 'createGain', 'gain'); 111 | }; 112 | 113 | NT.prototype._applyPan = function _applyPan(node, pan) { 114 | return this._applyEffect(node, pan, 'createStereoPanner', 'pan'); 115 | }; 116 | 117 | NT.prototype._applyEffect = function _applyEffect(node, val, method, prop) { 118 | if (!val || !this.audioContext[method]) { 119 | return node; 120 | } 121 | 122 | const nextNode = this.audioContext[method](); 123 | nextNode[prop].value = val; 124 | node.connect(nextNode); 125 | 126 | return nextNode; 127 | }; 128 | 129 | root.NT = NT; 130 | 131 | if (typeof module !== 'undefined') { 132 | module.exports = NT; 133 | } 134 | }(typeof global !== 'undefined' ? global : window)); -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nanotunes", 3 | "version": "0.4.1", 4 | "description": "A small music format and an accompanying implementation using OscillatorNode", 5 | "main": "dist/nanotunes.js", 6 | "scripts": { 7 | "test": "eslint src lib && mocha", 8 | "build": "./tasks/build.sh" 9 | }, 10 | "repository": { 11 | "type": "git", 12 | "url": "git+https://github.com/jamesseanwright/nanotunes.git" 13 | }, 14 | "keywords": [ 15 | "nanotunes", 16 | "music", 17 | "synthesis", 18 | "oscillator", 19 | "oscillatornode", 20 | "chiptune", 21 | "js13kgames" 22 | ], 23 | "author": "James Wright ", 24 | "license": "ISC", 25 | "bugs": { 26 | "url": "https://github.com/jamesseanwright/nanotunes/issues" 27 | }, 28 | "homepage": "https://github.com/jamesseanwright/nanotunes#readme", 29 | "devDependencies": { 30 | "babel-cli": "6.11.4", 31 | "babel-plugin-transform-es2015-arrow-functions": "6.8.0", 32 | "babel-plugin-transform-es2015-block-scoping": "6.10.1", 33 | "chai": "3.5.0", 34 | "eslint": "3.19.0", 35 | "mocha": "3.1.2", 36 | "sinon": "1.17.6" 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /tasks/build.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | if [ ! -e dist ] 4 | then 5 | mkdir dist 6 | fi 7 | 8 | node_modules/.bin/babel lib/nanotunes.js --out-file dist/nanotunes.js 9 | 10 | closure-compiler \ 11 | --js dist/nanotunes.js \ 12 | --js_output_file dist/nanotunes.min.js \ 13 | --compilation_level ADVANCED_OPTIMIZATIONS \ 14 | --externs externs.js 15 | 16 | cp dist/nanotunes.min.js demo/ -------------------------------------------------------------------------------- /test/e2e.test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | -------------------------------------------------------------------------------- /test/unit.test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const expect = require('chai').expect; 4 | const sinon = require('sinon'); 5 | const NT = require('../lib/nanotunes'); 6 | 7 | describe('NanoTunes', function () { 8 | stubAudioContext(); 9 | 10 | describe('the _parseInstrument method', function () { 11 | it('should parse the three-letter instrument name from an individual track part', function () { 12 | const trackPart = 'GTRA44B44'; 13 | const expectedHeader = 'GTR'; 14 | const actualHeader = NT.prototype._parseInstrument(trackPart); 15 | 16 | expect(actualHeader).to.equal(expectedHeader); 17 | }); 18 | }); 19 | 20 | describe('the _getFreqLength method', function () { 21 | it('should return a length, in seconds, for which a note\'s duration should be played', function () { 22 | const bpm = 120; 23 | const length = 4; // a crotchet 24 | const expectedLength = 0.5; 25 | const actualLength = NT.prototype._getFreqLength(length, bpm); 26 | 27 | expect(actualLength).to.equal(expectedLength); 28 | }); 29 | 30 | it('should support the longest note duration that is currently supported', function () { 31 | const bpm = 240; 32 | const length = 16 // a crotchet 33 | const expectedLength = 1; 34 | const actualLength = NT.prototype._getFreqLength(length, bpm); 35 | 36 | expect(actualLength).to.equal(expectedLength); 37 | }); 38 | }); 39 | 40 | /* I'll eventually write some functional/regression tests to ensure this 41 | * produces the expected values for a range of notes */ 42 | describe('the _convertToFrequencyMethod', function () { 43 | it('should convert a note to Hz', function () { 44 | const name = 'F'; 45 | const octave = 5; 46 | const expectedFrequency = 698.5599999883178; 47 | const actualFrequency = NT.prototype._convertToFrequency(name, octave); 48 | 49 | expect(actualFrequency).to.equal(expectedFrequency); 50 | }); 51 | 52 | it('should support sharp notes', function () { 53 | const name = 'D#'; 54 | const octave = 2; 55 | const expectedFrequency = 77.79999999947958; 56 | const actualFrequency = NT.prototype._convertToFrequency(name, octave); 57 | 58 | expect(actualFrequency).to.equal(expectedFrequency); 59 | }); 60 | 61 | it('should convert any rest note to 0 Hz', function () { 62 | const name = 'X'; 63 | const octave = 9; 64 | const expectedFrequency = 0; 65 | const actualFrequency = NT.prototype._convertToFrequency(name, octave); 66 | 67 | expect(actualFrequency).to.equal(expectedFrequency); 68 | }); 69 | }); 70 | }); 71 | 72 | function stubAudioContext() { 73 | const Ctx = global.AudioContext = function StubAudioContext() { 74 | this.currentTime = 0; 75 | this.destination = createStubAudioNode(); 76 | }; 77 | 78 | Ctx.prototype.createOscillator = function createOscillator() {}; 79 | Ctx.prototype.createGain = function createGain() {}; 80 | Ctx.prototype.createStereoPanner = function createStereoPanner() {}; 81 | } 82 | 83 | function createStubAudioNode(...audioParams) { 84 | const props = { 85 | connect() {} 86 | }; 87 | 88 | for (let param of audioParams) { 89 | props[param] = createStubAudioNode(); 90 | } 91 | 92 | return props; 93 | } 94 | 95 | function createStubAudioParam() { 96 | return { 97 | value: 0, 98 | setValueAtTime() {} 99 | }; 100 | } --------------------------------------------------------------------------------