commit b0cb3950faaf8cf4af6ea60af53de1255c8e8249 Author: Eric Rosenbaum <eric.rosenbaum@gmail.com> Date: Thu Oct 13 14:54:07 2016 -0400 initial commit diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..e84613d --- /dev/null +++ b/.editorconfig @@ -0,0 +1,11 @@ +root = true + +[*] +end_of_line = lf +insert_final_newline = true +charset = utf-8 +indent_size = 4 +trim_trailing_whitespace = true + +[*.{js,html}] +indent_style = space diff --git a/.eslintignore b/.eslintignore new file mode 100644 index 0000000..8809c41 --- /dev/null +++ b/.eslintignore @@ -0,0 +1,2 @@ +node_modules/* +dist.js diff --git a/.eslintrc b/.eslintrc new file mode 100644 index 0000000..5be40e9 --- /dev/null +++ b/.eslintrc @@ -0,0 +1,22 @@ +{ + "parser": "babel-eslint", + "rules": { + "curly": [2, "multi-line"], + "eol-last": [2], + "indent": [2, 4], + "linebreak-style": [2, "unix"], + "max-len": [2, 120, 4], + "no-trailing-spaces": [2, { "skipBlankLines": true }], + "no-unused-vars": [2, {"args": "after-used", "varsIgnorePattern": "^_"}], + "quotes": [2, "single"], + "semi": [2, "always"], + "space-before-function-paren": [2, "always"], + "strict": [2, "never"] + }, + "env": { + "browser": true, + "es6": true, + "node": true + }, + "extends": ["eslint:recommended"] +} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..132627c --- /dev/null +++ b/.gitignore @@ -0,0 +1,12 @@ +# Mac OS +.DS_Store + +# NPM +/node_modules +npm-* + +# Testing +/.nyc_output +/coverage + +/dist.js diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..d305b94 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,7 @@ +language: node_js +node_js: +- '4.2' +- 'stable' +cache: + directories: + - node_modules diff --git a/package.json b/package.json new file mode 100644 index 0000000..e8a689a --- /dev/null +++ b/package.json @@ -0,0 +1,32 @@ +{ + "name": "scratch-audioengine", + "version": "0.1.0", + "description": "audio engine for scratch 3.0", + "main": "dist.js", + "scripts": { + "test": "npm run lint && npm run build", + "build": "webpack --bail", + "watch": "webpack --watch", + "lint": "eslint ." + }, + "repository": { + "type": "git", + "url": "git+https://github.com/LLK/scratch-audioengine.git" + }, + "author": "Massachusetts Institute of Technology", + "license": "BSD-3-Clause", + "bugs": { + "url": "https://github.com/LLK/scratch-audioengine/issues" + }, + "homepage": "https://github.com/LLK/scratch-audioengine#readme", + "devDependencies": { + "babel-core": "^6.17.0", + "babel-eslint": "^7.0.0", + "babel-loader": "^6.2.5", + "babel-preset-es2015": "^6.16.0", + "eslint": "^3.7.1", + "soundfont-player": "^0.10.5", + "tone": "^0.8.0", + "webpack": "^1.13.2" + } +} diff --git a/src/index.js b/src/index.js new file mode 100644 index 0000000..13b3106 --- /dev/null +++ b/src/index.js @@ -0,0 +1,159 @@ +var Tone = require('tone'); +var Soundfont = require('soundfont-player'); + +function AudioEngine () { + + // tone setup + + this.tone = new Tone(); + + // effects setup + + this.delay = new Tone.FeedbackDelay(0.25, 0.5); + this.panner = new Tone.Panner(); + this.reverb = new Tone.Freeverb(); + + this.clearEffects(); + + Tone.Master.chain(this.delay, this.panner, this.reverb); + + // drum sounds + + // var drumFileNames = ['high_conga', 'small_cowbell', + // 'snare_drum', 'splash cymbal']; + // this.drumSamplers = this._loadSoundFiles(drumFileNames); + + // sound urls - map each url to its tone.sampler + this.soundSamplers = []; + + // soundfont setup + + // instrument names used by Musyng Kite soundfont, in order to + // match scratch instruments + this.instrumentNames = ['acoustic_grand_piano', 'electric_piano_1', + 'drawbar_organ', 'acoustic_guitar_nylon', 'electric_guitar_clean', + 'acoustic_bass', 'pizzicato_strings', 'cello', 'trombone', 'clarinet']; + + Soundfont.instrument(Tone.context, this.instrumentNames[0]).then( + function (inst) { + this.instrument = inst; + this.instrument.connect(Tone.Master); + }.bind(this) + ); +} + +AudioEngine.prototype.playSound = function (soundNum) { + this.soundSamplers[soundNum].triggerAttack(); +}; + +AudioEngine.prototype.playSoundFromUrl = function (url) { + if (url) { + // if we've loaded it already, play it + if (this.soundSamplers[url]) { + // this.soundSamplers[url].triggerAttack(); + this.soundSamplers[url].player.start(); + } else { + // else load, play, and store it + // this results in a delay the first time you play the sound + var sampler = new Tone.Sampler(url, function () { + sampler.triggerAttack(); + this.soundSamplers[url] = sampler; + }.bind(this)).toMaster(); + } + } +}; + +AudioEngine.prototype.getSoundDuration = function (url) { + return this.soundSamplers[url].player.buffer.duration; +}; + +AudioEngine.prototype.playNoteForBeats = function (note, beats) { + this.instrument.play( + note, Tone.context.currentTime, {duration : Number(beats)} + ); +}; + +AudioEngine.prototype.playDrumForBeats = function (drumNum) { + this.drumSamplers[drumNum].triggerAttack(); +}; + +AudioEngine.prototype.stopAllSounds = function () { + // stop drum notes + // for (var i = 0; i<this.drumSamplers.length; i++) { + // this.drumSamplers[i].triggerRelease(); + // } + // stop sounds triggered with playSound (indexed by their urls) + for (var key in this.soundSamplers) { + this.soundSamplers[key].triggerRelease(); + } + // stop soundfont notes + this.instrument.stop(); +}; + +AudioEngine.prototype.setEffect = function (effect, value) { + switch (effect) { + case 'ECHO': + this.delay.wet.value = (value / 100) / 2; // max 50% wet + break; + case 'PAN': + this.panner.pan.value = value / 100; + break; + case 'REVERB': + this.reverb.wet.value = value / 100; + break; + case 'PITCH': + this._setPitchShift(value / 20); + break; + } +}; + +AudioEngine.prototype.changeEffect = function (effect, value) { + switch (effect) { + case 'ECHO': + this.delay.wet.value += (value / 100) / 2; // max 50% wet + this.delay.wet.value = this._clamp(this.delay.wet.value, 0, 0.5); + break; + case 'PAN': + this.panner.pan.value += value / 100; + this.panner.pan.value = this._clamp(this.panner.pan.value, -1, 1); + break; + case 'REVERB': + this.reverb.wet.value += value / 100; + this.reverb.wet.value = this._clamp(this.reverb.wet.value, 0, 1); + break; + case 'PITCH': + // this.pitchShift.pitch += value / 20; + break; + } +}; + +AudioEngine.prototype._setPitchShift = function (value) { + for (var i in this.soundSamplers) { + this.soundSamplers[i].player.playbackRate = 1 + value; + } +}; + +AudioEngine.prototype.clearEffects = function () { + this.delay.wet.value = 0; + this._setPitchShift(0); + this.panner.pan.value = 0; + this.reverb.wet.value = 0; +}; + +// AudioEngine.prototype.loadSoundFromUrl = function(url) { + +// }; + +AudioEngine.prototype.loadSounds = function (sounds) { + for (var sound in sounds) { + var sampler = new Tone.Sampler(sound.fileUrl).toMaster(); + this.soundSamplers[sound.fileUrl] = sampler; + } +}; + +AudioEngine.prototype._clamp = function (input, min, max) { + return Math.min(Math.max(input, min), max); +}; + +module.exports = AudioEngine; + diff --git a/webpack.config.js b/webpack.config.js new file mode 100644 index 0000000..4ad8cfe --- /dev/null +++ b/webpack.config.js @@ -0,0 +1,23 @@ +var path = require('path'); + +module.exports = { + entry: { + 'dist': './src/index.js' + }, + output: { + path: __dirname, + library: 'AudioEngine', + libraryTarget: 'commonjs2', + filename: '[name].js' + }, + module: { + loaders: [{ + test: /\.js$/, + loader: 'babel-loader', + include: path.resolve(__dirname, 'src'), + query: { + presets: ['es2015'] + } + }] + } +};