├── src ├── math │ ├── logical-operators │ │ ├── NOR.es6 │ │ ├── NAND.es6 │ │ ├── XOR.es6 │ │ ├── LogicalOperator.es6 │ │ ├── AND.es6 │ │ ├── NOT.es6 │ │ └── OR.es6 │ ├── trigonometry │ │ ├── DegToRad.es6 │ │ ├── RadToDeg.es6 │ │ ├── Tan.es6 │ │ ├── Cos.es6 │ │ └── Sin.es6 │ ├── Negate.es6 │ ├── relational-operators │ │ ├── GreaterThanZero.es6 │ │ ├── IfElse.es6 │ │ ├── LessThanZero.es6 │ │ ├── GreaterThan.es6 │ │ ├── LessThan.es6 │ │ ├── EqualToZero.es6 │ │ └── EqualTo.es6 │ ├── Square.es6 │ ├── Round.es6 │ ├── Exp.es6 │ ├── Multiply.es6 │ ├── Sign.es6 │ ├── Add.es6 │ ├── Subtract.es6 │ ├── Wrap.es6 │ ├── Floor.es6 │ ├── Divide.es6 │ ├── Clamp.es6 │ ├── SampleDelay.es6 │ ├── Min.es6 │ ├── Max.es6 │ ├── Switch.es6 │ ├── Abs.es6 │ ├── Pow.es6 │ ├── Lerp.es6 │ ├── Reciprocal.es6 │ ├── Constant.es6 │ ├── Sqrt.es6 │ └── Scale.es6 ├── mixins │ ├── noteRegExp.es6 │ ├── noteStrings.es6 │ ├── setIO.es6 │ ├── notes.es6 │ ├── cleaners.es6 │ ├── connections.es6 │ ├── scales.es6 │ ├── Pool.es6 │ └── chords.es6 ├── core │ ├── config.es6 │ ├── overrides.es6 │ ├── WaveShaper.es6 │ └── AudioIO.es6 ├── fx │ ├── SineShaper.es6 │ ├── Delay.es6 │ ├── BitReduction.es6 │ ├── PingPongDelay.es6.js │ ├── StereoRotation.es6 │ └── StereoWidth.es6 ├── graphs │ ├── PhaseOffset.es6 │ ├── Crossfader.es6 │ ├── EQShelf.es6 │ ├── Counter.es6 │ └── DryWetNode.es6 ├── oscillators │ ├── OscillatorBank.es6 │ ├── SineBank.es6 │ └── FMOscillator.es6 ├── main.es6 └── envelopes │ └── ASDREnvelope.es6 ├── .gitignore ├── tests ├── node-sketches │ ├── B185.wav │ ├── stereo.wav │ ├── stinger1.wav │ ├── tom-high.wav │ ├── stereo.wav.asd │ ├── Kit03-150A-13.wav │ ├── K01-BrsSnrOff-07.wav │ ├── findingParams.html │ ├── MIDI.html │ ├── Factorial.html │ ├── BitReduction.html │ ├── Counter.html │ ├── NoiseOscillator.html │ ├── Follower.html │ ├── lerp.html │ ├── lerp3.html │ ├── SineBank.html │ ├── OscillatorBank.html │ ├── WaveformDirection.html │ ├── PinkNoise.html │ ├── PhaseOffset.html │ ├── PeakDetector.html │ ├── FM.html │ ├── PMOscillator.html │ ├── GeneratorPlayer.html │ ├── parabolicSaturation.html │ ├── StereoWidth.html │ ├── lerp2.html │ ├── ParabolicSaturation2.html │ ├── StereoRotation.html │ └── Sketch.html └── jasmine │ ├── lib │ ├── jasmine-2.2.0 │ │ └── jasmine_favicon.png │ └── OfflineAudioTest.js │ ├── NodeTests.html │ ├── MIT.LICENSE │ ├── spec │ ├── Wrap2PI.js │ ├── DegToRad.js │ ├── RadToDeg.js │ ├── Cos.js │ ├── Sin.js │ ├── Tan.js │ ├── NOT.js │ ├── Square.js │ └── Constant.js │ ├── nodeSpecs │ ├── Note.js │ └── Param.js │ └── MathTests.html ├── examples └── basic-synth │ ├── package.json │ ├── gruntfile.js │ └── res │ ├── css │ ├── keyboard.css.map │ ├── main.css.map │ ├── keyboard.css │ ├── knob.css.map │ └── main.css │ └── sass │ └── main.scss ├── package.json ├── README.md └── GruntFile.js /src/math/logical-operators/NOR.es6: -------------------------------------------------------------------------------- 1 | // OR -> NOT -> out -------------------------------------------------------------------------------- /src/math/logical-operators/NAND.es6: -------------------------------------------------------------------------------- 1 | // AND -> NOT -> out -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | .idea/ 3 | .DS_STORE 4 | .sass-cache/ -------------------------------------------------------------------------------- /src/math/logical-operators/XOR.es6: -------------------------------------------------------------------------------- 1 | // a equalTo( b ) -> NOT -> out -------------------------------------------------------------------------------- /src/mixins/noteRegExp.es6: -------------------------------------------------------------------------------- 1 | export default /^([A|B|C|D|E|F|G]{1})([#bx]{0,2})([\-\+]?\d+)?([\+|\-]{1}\d*.\d*)?/; -------------------------------------------------------------------------------- /src/mixins/noteStrings.es6: -------------------------------------------------------------------------------- 1 | export default [ 'C', 'C#', 'D', 'D#', 'E', 'F', 'F#', 'G', 'G#', 'A', 'A#', 'B' ]; -------------------------------------------------------------------------------- /tests/node-sketches/B185.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/squarefeet/AudioIO/HEAD/tests/node-sketches/B185.wav -------------------------------------------------------------------------------- /tests/node-sketches/stereo.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/squarefeet/AudioIO/HEAD/tests/node-sketches/stereo.wav -------------------------------------------------------------------------------- /tests/node-sketches/stinger1.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/squarefeet/AudioIO/HEAD/tests/node-sketches/stinger1.wav -------------------------------------------------------------------------------- /tests/node-sketches/tom-high.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/squarefeet/AudioIO/HEAD/tests/node-sketches/tom-high.wav -------------------------------------------------------------------------------- /src/mixins/setIO.es6: -------------------------------------------------------------------------------- 1 | export default function _setIO( io ) { 2 | this.io = io; 3 | this.context = io.context; 4 | }; -------------------------------------------------------------------------------- /tests/node-sketches/stereo.wav.asd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/squarefeet/AudioIO/HEAD/tests/node-sketches/stereo.wav.asd -------------------------------------------------------------------------------- /tests/node-sketches/Kit03-150A-13.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/squarefeet/AudioIO/HEAD/tests/node-sketches/Kit03-150A-13.wav -------------------------------------------------------------------------------- /tests/node-sketches/K01-BrsSnrOff-07.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/squarefeet/AudioIO/HEAD/tests/node-sketches/K01-BrsSnrOff-07.wav -------------------------------------------------------------------------------- /tests/jasmine/lib/jasmine-2.2.0/jasmine_favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/squarefeet/AudioIO/HEAD/tests/jasmine/lib/jasmine-2.2.0/jasmine_favicon.png -------------------------------------------------------------------------------- /src/math/trigonometry/DegToRad.es6: -------------------------------------------------------------------------------- 1 | import "../../core/AudioIO.es6"; 2 | import Node from "../../core/Node.es6"; 3 | 4 | class DegToRad extends Node { 5 | constructor( io ) { 6 | super( io, 0, 0 ); 7 | this.inputs[ 0 ] = this.outputs[ 0 ] = this.io.createMultiply( Math.PI / 180 ); 8 | } 9 | } 10 | 11 | AudioIO.prototype.createDegToRad = function( deg ) { 12 | return new DegToRad( this, deg ); 13 | }; -------------------------------------------------------------------------------- /src/math/trigonometry/RadToDeg.es6: -------------------------------------------------------------------------------- 1 | import "../../core/AudioIO.es6"; 2 | import Node from "../../core/Node.es6"; 3 | 4 | class RadToDeg extends Node { 5 | constructor( io ) { 6 | super( io, 0, 0 ); 7 | this.inputs[ 0 ] = this.outputs[ 0 ] = this.io.createMultiply( 180 / Math.PI ); 8 | } 9 | } 10 | 11 | AudioIO.prototype.createRadToDeg = function( deg ) { 12 | return new RadToDeg( this, deg ); 13 | }; -------------------------------------------------------------------------------- /examples/basic-synth/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "audio-io-basic-synth", 3 | "version": "1.0.0", 4 | "description": "Basic synth built with AudioIO", 5 | "main": "gruntfile.js", 6 | "author": "Luke Moody ", 7 | "license": "MIT", 8 | "devDependencies": { 9 | "grunt": "^0.4.5", 10 | "grunt-contrib-sass": "^0.9.2", 11 | "grunt-contrib-uglify": "^0.11.1", 12 | "grunt-contrib-watch": "^0.6.1" 13 | } 14 | } -------------------------------------------------------------------------------- /src/math/Negate.es6: -------------------------------------------------------------------------------- 1 | import "../core/AudioIO.es6"; 2 | import Node from "../core/Node.es6"; 3 | 4 | /** 5 | * Negates the incoming audio signal. 6 | * @param {Object} io Instance of AudioIO. 7 | */ 8 | class Negate extends Node { 9 | constructor( io ) { 10 | super( io, 1, 0 ); 11 | 12 | this.inputs[ 0 ].gain.value = -1; 13 | this.outputs = this.inputs; 14 | } 15 | } 16 | 17 | 18 | AudioIO.prototype.createNegate = function() { 19 | return new Negate( this ); 20 | }; -------------------------------------------------------------------------------- /tests/node-sketches/findingParams.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 18 | 19 | -------------------------------------------------------------------------------- /tests/jasmine/lib/OfflineAudioTest.js: -------------------------------------------------------------------------------- 1 | function offlineAudioTest( options ) { 2 | var io = new AudioIO( new OfflineAudioContext( 1, options.cycles || 5, 44100 ) ); 3 | 4 | options.onSetup( io ); 5 | 6 | io.context.oncomplete = function( e ) { 7 | var buffer = e.renderedBuffer.getChannelData( 0 ), 8 | size = buffer.length, 9 | i = 0; 10 | 11 | for ( i; i < size; i++ ) { 12 | options.onCompare( buffer[ i ] ); 13 | } 14 | 15 | options.onComplete(); 16 | }; 17 | 18 | io.context.startRendering(); 19 | } -------------------------------------------------------------------------------- /src/mixins/notes.es6: -------------------------------------------------------------------------------- 1 | export default { 2 | 'C': 0, 'Dbb': 0, 'B#': 0, 3 | 'C#': 1, 'Db': 1, 'B##': 1, 'Bx': 1, 4 | 'D': 2, 'Ebb': 2, 'C##': 2, 'Cx': 2, 5 | 'D#': 3, 'Eb': 3, 'Fbb': 3, 6 | 'E': 4, 'Fb': 4, 'D##': 4, 'Dx': 4, 7 | 'F': 5, 'Gbb': 5, 'E#': 5, 8 | 'F#': 6, 'Gb': 6, 'E##': 6, 'Ex': 6, 9 | 'G': 7, 'Abb': 7, 'F##': 7, 'Fx': 7, 10 | 'G#': 8, 'Ab': 8, 11 | 'A': 9, 'Bbb': 9, 'G##': 9, 'Gx': 9, 12 | 'A#': 10, 'Bb': 10, 'Cbb': 10, 13 | 'B': 11, 'Cb': 11, 'A##': 11, 'Ax': 11 14 | }; -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "audio-io", 3 | "version": "0.0.2", 4 | "description": "", 5 | "main": "index.js", 6 | "dependencies": { 7 | "grunt": "^0.4.5" 8 | }, 9 | "devDependencies": { 10 | "babel-plugin-closure-elimination": "0.0.1", 11 | "babelify": "^6.1.0", 12 | "grunt-browserify": "^3.8.0", 13 | "grunt-contrib-watch": "^0.6.1", 14 | "grunt-contrib-uglify": "~0.9.1" 15 | }, 16 | "scripts": { 17 | "test": "echo \"Error: no test specified\" && exit 1" 18 | }, 19 | "author": "", 20 | "license": "ISC" 21 | } -------------------------------------------------------------------------------- /src/math/logical-operators/LogicalOperator.es6: -------------------------------------------------------------------------------- 1 | import "../../core/AudioIO.es6"; 2 | import Node from "../../core/Node.es6"; 3 | 4 | 5 | class LogicalOperator extends Node { 6 | 7 | /** 8 | * @param {Object} io Instance of AudioIO. 9 | */ 10 | constructor( io ) { 11 | super( io, 0, 1 ); 12 | 13 | var graph = this.getGraph(); 14 | 15 | graph.clamp = this.io.createClamp( 0, 1 ); 16 | this.inputs[ 0 ] = graph.clamp; 17 | 18 | this.setGraph( graph ); 19 | } 20 | } 21 | 22 | export default LogicalOperator; 23 | 24 | AudioIO.prototype.createLogicalOperator = function() { 25 | return new LogicalOperator( this ); 26 | }; -------------------------------------------------------------------------------- /src/math/relational-operators/GreaterThanZero.es6: -------------------------------------------------------------------------------- 1 | import "../../core/AudioIO.es6"; 2 | import Node from "../../core/Node.es6"; 3 | 4 | class GreaterThanZero extends Node { 5 | constructor( io ) { 6 | super( io, 1, 1 ); 7 | 8 | var graph = this.getGraph(); 9 | 10 | this.inputs[ 0 ].gain.value = 100000; 11 | graph.shaper = this.io.createWaveShaper( this.io.curves.GreaterThanZero ); 12 | this.inputs[ 0 ].connect( graph.shaper ); 13 | graph.shaper.connect( this.outputs[ 0 ] ); 14 | 15 | this.setGraph( graph ); 16 | } 17 | } 18 | 19 | AudioIO.prototype.createGreaterThanZero = function() { 20 | return new GreaterThanZero( this ); 21 | }; -------------------------------------------------------------------------------- /tests/node-sketches/MIDI.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 25 | 26 | -------------------------------------------------------------------------------- /src/math/Square.es6: -------------------------------------------------------------------------------- 1 | import AudioIO from "../core/AudioIO.es6"; 2 | import Node from "../core/Node.es6"; 3 | 4 | 5 | class Square extends Node { 6 | 7 | /** 8 | * @param {Object} io Instance of AudioIO. 9 | */ 10 | constructor( io ) { 11 | super( io, 1, 1 ); 12 | 13 | var graph = this.getGraph(); 14 | 15 | graph.multiply = this.io.createMultiply(); 16 | this.inputs[ 0 ].connect( graph.multiply, 0, 0 ); 17 | this.inputs[ 0 ].connect( graph.multiply, 0, 1 ); 18 | graph.multiply.connect( this.outputs[ 0 ] ); 19 | 20 | this.setGraph( graph ); 21 | } 22 | } 23 | 24 | AudioIO.prototype.createSquare = function() { 25 | return new Square( this ); 26 | }; -------------------------------------------------------------------------------- /src/core/config.es6: -------------------------------------------------------------------------------- 1 | export default { 2 | curveResolution: 4096, // Must be an even number. 3 | 4 | // Used when converting note strings (eg. 'A#4') to MIDI values. 5 | // It's the octave number of the lowest C note (MIDI note 0). 6 | // Change this if you're used to a DAW that doesn't use -2 as the 7 | // lowest octave. 8 | lowestOctave: -2, 9 | 10 | // Lowest allowed number. Used by some Math 11 | // functions, especially when converting between 12 | // number formats (ie. hz -> MIDI, note -> MIDI, etc. ) 13 | // 14 | // Also used in calls to AudioParam.exponentialRampToValueAtTime 15 | // so there's no ramping to 0 (which throws an error). 16 | epsilon: 0.001, 17 | 18 | midiNotePoolSize: 500 19 | }; -------------------------------------------------------------------------------- /src/math/Round.es6: -------------------------------------------------------------------------------- 1 | import "../core/AudioIO.es6"; 2 | import Node from "../core/Node.es6"; 3 | 4 | // NOTE: 5 | // Only accepts values >= -1 && <= 1. Values outside 6 | // this range are clamped to this range. 7 | class Round extends Node { 8 | constructor( io ) { 9 | super( io, 1, 1 ); 10 | 11 | var graph = this.getGraph(); 12 | 13 | graph.floor = this.io.createFloor(); 14 | graph.add = this.io.createAdd( 0.5 ); 15 | this.inputs[0].connect( graph.add ); 16 | graph.add.connect( graph.floor ); 17 | graph.floor.connect( this.outputs[0] ); 18 | 19 | this.setGraph( graph ); 20 | } 21 | } 22 | 23 | AudioIO.prototype.createRound = function() { 24 | return new Round( this ); 25 | }; -------------------------------------------------------------------------------- /tests/node-sketches/Factorial.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 25 | 26 | -------------------------------------------------------------------------------- /src/math/relational-operators/IfElse.es6: -------------------------------------------------------------------------------- 1 | import "../../core/AudioIO.es6"; 2 | import Node from "../../core/Node.es6"; 3 | 4 | class IfElse extends Node { 5 | constructor( io ) { 6 | super( io, 0, 0 ); 7 | 8 | var graph = this.getGraph(); 9 | 10 | graph.switch = this.io.createSwitch( 2, 0 ); 11 | 12 | this.if = this.io.createEqualToZero(); 13 | this.if.connect( graph.switch.control ); 14 | this.then = graph.switch.inputs[ 0 ]; 15 | this.else = graph.switch.inputs[ 1 ]; 16 | 17 | this.inputs = graph.switch.inputs; 18 | this.outputs = graph.switch.outputs; 19 | 20 | this.setGraph( graph ); 21 | } 22 | } 23 | 24 | AudioIO.prototype.createIfElse = function() { 25 | return new IfElse( this ); 26 | }; -------------------------------------------------------------------------------- /src/math/relational-operators/LessThanZero.es6: -------------------------------------------------------------------------------- 1 | import "../../core/AudioIO.es6"; 2 | import Node from "../../core/Node.es6"; 3 | 4 | class LessThanZero extends Node { 5 | constructor( io ) { 6 | super( io, 1, 1 ); 7 | 8 | var graph = this.getGraph(); 9 | 10 | graph.booster = this.context.createGain(); 11 | graph.booster.gain.value = -100000; 12 | this.inputs[ 0 ].connect( graph.booster ); 13 | 14 | graph.shaper = this.io.createWaveShaper( this.io.curves.GreaterThanZero ); 15 | 16 | graph.booster.connect( graph.shaper ); 17 | graph.shaper.connect( this.outputs[ 0 ] ); 18 | 19 | this.setGraph( graph ); 20 | } 21 | } 22 | 23 | AudioIO.prototype.createLessThanZero = function() { 24 | return new LessThanZero( this ); 25 | }; -------------------------------------------------------------------------------- /src/math/logical-operators/AND.es6: -------------------------------------------------------------------------------- 1 | import "../../core/AudioIO.es6"; 2 | import LogicalOperator from "./LogicalOperator.es6"; 3 | 4 | 5 | class AND extends LogicalOperator { 6 | 7 | /** 8 | * @param {Object} io Instance of AudioIO. 9 | */ 10 | constructor( io ) { 11 | super( io ); 12 | 13 | var graph = this.getGraph(); 14 | 15 | graph.multiply = this.io.createMultiply(); 16 | this.inputs[ 1 ] = this.io.createClamp( 0, 1 ); 17 | 18 | this.inputs[ 0 ].connect( graph.multiply, 0, 0 ); 19 | this.inputs[ 1 ].connect( graph.multiply, 0, 1 ); 20 | 21 | graph.multiply.connect( this.outputs[ 0 ] ); 22 | 23 | this.setGraph( graph ); 24 | } 25 | } 26 | 27 | export default AND; 28 | 29 | AudioIO.prototype.createAND = function() { 30 | return new AND( this ); 31 | }; -------------------------------------------------------------------------------- /src/math/Exp.es6: -------------------------------------------------------------------------------- 1 | import "../core/AudioIO.es6"; 2 | import Node from "../core/Node.es6"; 3 | 4 | // Emulates Math.exp: 5 | // 6 | // Math.pow( Math.E, value ); 7 | // 8 | // 9 | // BROKEN!!! 10 | // Having floats, or dynamic exponents to AudioIO's Pow 11 | // class DOES NOT WORK. It's just not do-able ;( 12 | class Exp extends Node{ 13 | constructor( io, value ) { 14 | super( io, 0, 1 ); 15 | 16 | this.inputs[ 0 ] = this.control = this.io.createParam( value ); 17 | this.E = this.io.createConstantE(); 18 | 19 | } 20 | 21 | get value() { 22 | return this.control.value; 23 | } 24 | set value( value ) { 25 | this.control.setValueAtTime( value, this.context.currentTime ); 26 | } 27 | } 28 | 29 | 30 | AudioIO.prototype.createExp = function( value1, value2 ) { 31 | return new Exp( this, value1, value2 ); 32 | }; -------------------------------------------------------------------------------- /examples/basic-synth/gruntfile.js: -------------------------------------------------------------------------------- 1 | module.exports = function( grunt ) { 2 | grunt.initConfig( { 3 | sass: { 4 | dist: { 5 | options: { 6 | style: 'expanded' 7 | }, 8 | files: { 9 | 'res/css/main.css': 'res/sass/main.scss', 10 | 'res/css/knob.css': 'res/sass/knob.scss', 11 | 'res/css/keyboard.css': 'res/sass/keyboard.scss' 12 | } 13 | } 14 | }, 15 | 16 | watch: { 17 | scripts: { 18 | files: [ '**/*.scss' ], 19 | tasks: [ 'sass' ] 20 | } 21 | } 22 | } ); 23 | 24 | grunt.loadNpmTasks( 'grunt-contrib-sass' ); 25 | grunt.loadNpmTasks( 'grunt-contrib-watch' ); 26 | 27 | grunt.registerTask( 'default', [ "sass", "watch" ] ); 28 | }; -------------------------------------------------------------------------------- /src/math/logical-operators/NOT.es6: -------------------------------------------------------------------------------- 1 | import "../../core/AudioIO.es6"; 2 | import LogicalOperator from "./LogicalOperator.es6"; 3 | 4 | 5 | class NOT extends LogicalOperator { 6 | 7 | /** 8 | * @param {Object} io Instance of AudioIO. 9 | */ 10 | constructor( io ) { 11 | super( io ); 12 | 13 | var graph = this.getGraph(); 14 | 15 | graph.abs = this.io.createAbs( 100 ); 16 | graph.subtract = this.io.createSubtract( 1 ); 17 | graph.round = this.io.createRound(); 18 | 19 | this.inputs[ 0 ].connect( graph.subtract ); 20 | graph.subtract.connect( graph.abs ); 21 | graph.abs.connect( graph.round ) 22 | 23 | graph.round.connect( this.outputs[ 0 ] ); 24 | 25 | this.setGraph( graph ); 26 | } 27 | } 28 | 29 | export default NOT; 30 | 31 | AudioIO.prototype.createNOT = function() { 32 | return new NOT( this ); 33 | }; -------------------------------------------------------------------------------- /src/math/logical-operators/OR.es6: -------------------------------------------------------------------------------- 1 | import "../../core/AudioIO.es6"; 2 | import LogicalOperator from "./LogicalOperator.es6"; 3 | 4 | 5 | class OR extends LogicalOperator { 6 | 7 | /** 8 | * @param {Object} io Instance of AudioIO. 9 | */ 10 | constructor( io ) { 11 | super( io ); 12 | 13 | var graph = this.getGraph(); 14 | 15 | graph.max = this.io.createMax(); 16 | graph.equalTo = this.io.createEqualTo( 1 ); 17 | this.inputs[ 1 ] = this.io.createClamp( 0, 1 ); 18 | 19 | this.inputs[ 0 ].connect( graph.max ); 20 | this.inputs[ 1 ].connect( graph.max.controls.value ); 21 | graph.max.connect( graph.equalTo ); 22 | graph.equalTo.connect( this.outputs[ 0 ] ); 23 | 24 | this.setGraph( graph ); 25 | } 26 | } 27 | 28 | export default OR; 29 | 30 | AudioIO.prototype.createOR = function() { 31 | return new OR( this ); 32 | }; -------------------------------------------------------------------------------- /src/math/Multiply.es6: -------------------------------------------------------------------------------- 1 | import "../core/AudioIO.es6"; 2 | import Node from "../core/Node.es6"; 3 | 4 | /** 5 | * Multiplies two audio signals together. 6 | * @param {Object} io Instance of AudioIO. 7 | */ 8 | class Multiply extends Node { 9 | constructor( io, value ) { 10 | super( io, 1, 1 ); 11 | 12 | this.inputs[ 1 ] = this.io.createParam( value ); 13 | this.outputs[ 0 ].gain.value = 0.0; 14 | 15 | this.inputs[ 0 ].connect( this.outputs[ 0 ] ); 16 | this.inputs[ 1 ].connect( this.outputs[ 0 ].gain ); 17 | 18 | this.controls.value = this.inputs[ 1 ]; 19 | } 20 | 21 | get value() { 22 | return this.controls.value.value; 23 | } 24 | set value( value ) { 25 | this.controls.value.setValueAtTime( value, this.context.currentTime ); 26 | } 27 | 28 | } 29 | 30 | AudioIO.prototype.createMultiply = function( value1, value2 ) { 31 | return new Multiply( this, value1, value2 ); 32 | }; -------------------------------------------------------------------------------- /src/fx/SineShaper.es6: -------------------------------------------------------------------------------- 1 | import AudioIO from "../core/AudioIO.es6"; 2 | import DryWetNode from "../graphs/DryWetNode.es6"; 3 | 4 | // TODO: Add feedbackLevel and delayTime Param instances 5 | // to control this node. 6 | class SineShaper extends DryWetNode { 7 | constructor( io ) { 8 | super( io ); 9 | 10 | this.controls.drive = this.io.createParam(); 11 | this.shaper = this.io.createWaveShaper( this.io.curves.Sine ); 12 | this.shaperDrive = this.context.createGain(); 13 | this.shaperDrive.gain.value = 1; 14 | 15 | this.inputs[ 0 ].connect( this.shaperDrive ); 16 | this.controls.drive.connect( this.shaperDrive.gain ); 17 | this.shaperDrive.connect( this.shaper ); 18 | this.shaper.connect( this.wet ); 19 | } 20 | } 21 | 22 | AudioIO.prototype.createSineShaper = function( time, feedbackLevel ) { 23 | return new SineShaper( this, time, feedbackLevel ); 24 | }; 25 | 26 | export default SineShaper; -------------------------------------------------------------------------------- /tests/jasmine/NodeTests.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Jasmine Spec Runner v2.2.0 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /src/math/Sign.es6: -------------------------------------------------------------------------------- 1 | import "../core/AudioIO.es6"; 2 | import Node from "../core/Node.es6"; 3 | 4 | class Sign extends Node { 5 | 6 | /** 7 | * @param {Object} io Instance of AudioIO. 8 | */ 9 | constructor( io ) { 10 | super( io, 1, 1 ); 11 | 12 | var graph = this.getGraph(); 13 | 14 | graph.shaper = this.io.createWaveShaper( this.io.curves.Sign ); 15 | 16 | graph.ifElse = this.io.createIfElse(); 17 | graph.equalToZero = this.io.createEqualToZero(); 18 | 19 | this.inputs[ 0 ].connect( graph.equalToZero ); 20 | this.inputs[ 0 ].connect( graph.ifElse.then ); 21 | this.inputs[ 0 ].connect( graph.shaper ); 22 | 23 | graph.equalToZero.connect( graph.ifElse.if ); 24 | graph.shaper.connect( graph.ifElse.else ); 25 | graph.ifElse.connect( this.outputs[ 0 ] ); 26 | 27 | this.setGraph( graph ); 28 | } 29 | } 30 | 31 | AudioIO.prototype.createSign = function() { 32 | return new Sign( this ); 33 | }; 34 | -------------------------------------------------------------------------------- /src/math/Add.es6: -------------------------------------------------------------------------------- 1 | import "../core/AudioIO.es6"; 2 | import Node from "../core/Node.es6"; 3 | 4 | /** 5 | * Adds two audio signals together. 6 | * @param {Object} io Instance of AudioIO. 7 | * 8 | * var add = io.createAdd( 2 ); 9 | * in1.connect( add ); 10 | * 11 | * var add = io.createAdd(); 12 | * in1.connect( add, 0, 0 ); 13 | * in2.connect( add, 0, 1 ); 14 | */ 15 | class Add extends Node{ 16 | constructor( io, value ) { 17 | super( io, 1, 1 ); 18 | 19 | this.inputs[ 1 ] = this.io.createParam( value ); 20 | 21 | this.inputs[ 0 ].connect( this.outputs[ 0 ] ); 22 | this.inputs[ 1 ].connect( this.outputs[ 0 ] ); 23 | 24 | // Store controllable params. 25 | this.controls.value = this.inputs[ 1 ]; 26 | } 27 | 28 | get value() { 29 | return this.controls.value.value; 30 | } 31 | set value( value ) { 32 | this.controls.value.value = value; 33 | } 34 | } 35 | 36 | 37 | AudioIO.prototype.createAdd = function( value ) { 38 | return new Add( this, value ); 39 | }; -------------------------------------------------------------------------------- /src/math/Subtract.es6: -------------------------------------------------------------------------------- 1 | import "../core/AudioIO.es6"; 2 | import Node from "../core/Node.es6"; 3 | 4 | /** 5 | * Subtracts the second input from the first. 6 | * 7 | * @param {Object} io Instance of AudioIO. 8 | */ 9 | class Subtract extends Node { 10 | constructor( io, value ) { 11 | super( io, 1, 1 ); 12 | 13 | var graph = this.getGraph(); 14 | 15 | graph.negate = this.io.createNegate(); 16 | 17 | this.inputs[ 1 ] = this.io.createParam( value ); 18 | 19 | this.inputs[ 0 ].connect( this.outputs[ 0 ] ); 20 | this.inputs[ 1 ].connect( graph.negate ); 21 | graph.negate.connect( this.outputs[ 0 ] ); 22 | 23 | this.controls.value = this.inputs[ 1 ]; 24 | 25 | this.setGraph( graph ); 26 | } 27 | 28 | get value() { 29 | return this.controls.value.value; 30 | } 31 | set value( value ) { 32 | this.controls.value.setValueAtTime( value, this.context.currentTime ); 33 | } 34 | } 35 | 36 | AudioIO.prototype.createSubtract = function( value ) { 37 | return new Subtract( this, value ); 38 | }; -------------------------------------------------------------------------------- /src/math/Wrap.es6: -------------------------------------------------------------------------------- 1 | function wrap( wrapTo, input ) { 2 | var reciprocal = Math.pow( wrapTo, -1 ), 3 | mul1 = input * reciprocal, 4 | round = Math.round( mul1 ), 5 | sub = mul1 - round, 6 | mul2 = sub * wrapTo; 7 | 8 | return mul2; 9 | } 10 | 11 | import "../core/AudioIO.es6"; 12 | import Node from "../core/Node.es6"; 13 | 14 | // TODO: 15 | // - This. 16 | // - Wait... it uses Math.round. And values will probably in a range 17 | // greater than -1 to 1. Booooo :( 18 | class Wrap extends Node { 19 | 20 | /** 21 | * @param {Object} io Instance of AudioIO. 22 | */ 23 | constructor( io, wrapTo, value ) { 24 | super( io, 0, 1 ); 25 | 26 | this.wrapTo = this.io.createParam( wrapTo ); 27 | this.inputs[ 0 ] = this.io.createParam( value ); 28 | 29 | 30 | this.reciprocal = this.io.createReciprocal( this.context.sampleRate * 0.5 ); 31 | this.multiply1 = this.io.createMultiply(); 32 | } 33 | } 34 | 35 | AudioIO.prototype.createWrap = function( wrapTo, value ) { 36 | return new Wrap( this, wrapTo, value ); 37 | }; -------------------------------------------------------------------------------- /tests/jasmine/MIT.LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2008-2014 Pivotal Labs 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /src/math/Floor.es6: -------------------------------------------------------------------------------- 1 | import "../core/AudioIO.es6"; 2 | import Node from "../core/Node.es6"; 3 | 4 | 5 | // NOTE: 6 | // Only accepts values >= -1 && <= 1. Values outside 7 | // this range are clamped to this range. 8 | class Floor extends Node { 9 | constructor( io ) { 10 | super( io, 1, 1 ); 11 | 12 | var graph = this.getGraph(); 13 | 14 | graph.shaper = this.io.createWaveShaper( this.io.curves.Floor ); 15 | 16 | // This branching is because inputting `0` values 17 | // into the waveshaper above outputs -0.5 ;( 18 | graph.ifElse = this.io.createIfElse(); 19 | graph.equalToZero = this.io.createEqualToZero(); 20 | 21 | this.inputs[ 0 ].connect( graph.equalToZero ); 22 | graph.equalToZero.connect( graph.ifElse.if ); 23 | this.inputs[ 0 ].connect( graph.ifElse.then ); 24 | graph.shaper.connect( graph.ifElse.else ); 25 | 26 | this.inputs[ 0 ].connect( graph.shaper ); 27 | graph.ifElse.connect( this.outputs[ 0 ] ); 28 | 29 | this.setGraph( graph ); 30 | } 31 | } 32 | 33 | AudioIO.prototype.createFloor = function() { 34 | return new Floor( this ); 35 | }; -------------------------------------------------------------------------------- /src/math/relational-operators/GreaterThan.es6: -------------------------------------------------------------------------------- 1 | import "../../core/AudioIO.es6"; 2 | import Node from "../../core/Node.es6"; 3 | 4 | class GreaterThan extends Node { 5 | constructor( io, value ) { 6 | super( io, 1, 1 ); 7 | 8 | var graph = this.getGraph(); 9 | 10 | this.controls.value = this.io.createParam( value ); 11 | graph.inversion = this.context.createGain(); 12 | 13 | graph.inversion.gain.value = -1; 14 | 15 | this.inputs[ 0 ].gain.value = 100000; 16 | graph.shaper = this.io.createWaveShaper( this.io.curves.GreaterThanZero ); 17 | 18 | 19 | this.controls.value.connect( graph.inversion ); 20 | graph.inversion.connect( this.inputs[ 0 ] ); 21 | this.inputs[ 0 ].connect( graph.shaper ); 22 | graph.shaper.connect( this.outputs[ 0 ] ); 23 | 24 | this.setGraph( graph ); 25 | } 26 | 27 | get value() { 28 | return this.controls.value.value; 29 | } 30 | set value( value ) { 31 | this.controls.value.setValueAtTime( value, this.context.currentTime ); 32 | } 33 | } 34 | 35 | AudioIO.prototype.createGreaterThan = function( value ) { 36 | return new GreaterThan( this, value ); 37 | }; -------------------------------------------------------------------------------- /src/math/relational-operators/LessThan.es6: -------------------------------------------------------------------------------- 1 | import "../../core/AudioIO.es6"; 2 | import Node from "../../core/Node.es6"; 3 | 4 | class LessThan extends Node { 5 | constructor( io, value ) { 6 | super( io, 1, 1 ); 7 | 8 | var graph = this.getGraph(); 9 | 10 | this.controls.value = this.io.createParam( value ); 11 | 12 | graph.valueInversion = this.context.createGain(); 13 | graph.valueInversion.gain.value = -1; 14 | 15 | this.controls.value.connect( graph.valueInversion ); 16 | 17 | this.inputs[ 0 ].gain.value = -100000; 18 | graph.shaper = this.io.createWaveShaper( this.io.curves.GreaterThanZero ); 19 | 20 | graph.valueInversion.connect( this.inputs[ 0 ] ); 21 | this.inputs[ 0 ].connect( graph.shaper ); 22 | graph.shaper.connect( this.outputs[ 0 ] ); 23 | 24 | this.setGraph( graph ); 25 | } 26 | 27 | get value() { 28 | return this.controls.value.value; 29 | } 30 | set value( value ) { 31 | this.controls.value.setValueAtTime( value, this.context.currentTime ); 32 | } 33 | } 34 | 35 | AudioIO.prototype.createLessThan = function( value ) { 36 | return new LessThan( this, value ); 37 | }; -------------------------------------------------------------------------------- /tests/node-sketches/BitReduction.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 37 | 38 | -------------------------------------------------------------------------------- /src/math/Divide.es6: -------------------------------------------------------------------------------- 1 | import "../core/AudioIO.es6"; 2 | import Node from "../core/Node.es6"; 3 | 4 | /** 5 | * Divides two numbers 6 | * @param {Object} io Instance of AudioIO. 7 | */ 8 | class Divide extends Node { 9 | constructor( io, value, maxInput ) { 10 | super( io, 1, 1 ); 11 | 12 | var graph = this.getGraph(); 13 | 14 | 15 | this.inputs[ 1 ] = this.io.createParam( value ); 16 | this.outputs[ 0 ].gain.value = 0.0; 17 | 18 | graph.reciprocal = this.io.createReciprocal( maxInput ); 19 | this.inputs[ 1 ].connect( graph.reciprocal ); 20 | 21 | this.inputs[ 0 ].connect( this.outputs[ 0 ] ); 22 | graph.reciprocal.connect( this.outputs[ 0 ].gain ); 23 | 24 | this.controls.divisor = this.inputs[ 1 ]; 25 | 26 | this.setGraph( graph ); 27 | } 28 | 29 | get value() { 30 | return this.inputs[ 1 ].value; 31 | } 32 | set value( value ) { 33 | this.inputs[ 1 ].value = value; 34 | } 35 | 36 | get maxInput() { 37 | return this.reciprocal.maxInput; 38 | } 39 | set maxInput( value ) { 40 | this.reciprocal.maxInput = value; 41 | } 42 | } 43 | 44 | AudioIO.prototype.createDivide = function( value, maxInput ) { 45 | return new Divide( this, value, maxInput ); 46 | }; -------------------------------------------------------------------------------- /src/math/Clamp.es6: -------------------------------------------------------------------------------- 1 | import "../core/AudioIO.es6"; 2 | import Node from "../core/Node.es6"; 3 | 4 | 5 | class Clamp extends Node { 6 | constructor( io, minValue, maxValue ) { 7 | super( io, 1, 1 ); // io, 1, 1 8 | 9 | var graph = this.getGraph(); 10 | 11 | graph.min = this.io.createMin( maxValue ); 12 | graph.max = this.io.createMax( minValue ); 13 | 14 | // this.inputs = [ graph.min ]; 15 | // this.outputs = [ graph.max ]; 16 | this.inputs[ 0 ].connect( graph.min ); 17 | graph.min.connect( graph.max ); 18 | graph.max.connect( this.outputs[ 0 ] ); 19 | 20 | // Store controllable params. 21 | this.controls.min = graph.min.controls.value; 22 | this.controls.max = graph.max.controls.value; 23 | 24 | this.setGraph( graph ); 25 | } 26 | 27 | get min() { 28 | return this.getGraph().max.value; 29 | } 30 | set min( value ) { 31 | this.getGraph().max.value = value; 32 | } 33 | 34 | get max() { 35 | return this.getGraph().min.value; 36 | } 37 | set max( value ) { 38 | this.getGraph().min.value = value; 39 | } 40 | } 41 | 42 | 43 | 44 | AudioIO.prototype.createClamp = function( minValue, maxValue ) { 45 | return new Clamp( this, minValue, maxValue ); 46 | }; -------------------------------------------------------------------------------- /tests/node-sketches/Counter.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 38 | 39 | -------------------------------------------------------------------------------- /src/math/SampleDelay.es6: -------------------------------------------------------------------------------- 1 | import "../core/AudioIO.es6"; 2 | import Node from "../core/Node.es6"; 3 | 4 | 5 | class SampleDelay extends Node { 6 | 7 | /** 8 | * @param {Object} io Instance of AudioIO. 9 | */ 10 | constructor( io, numSamples ) { 11 | super( io, 1, 1 ); 12 | 13 | var graph = this.getGraph(); 14 | 15 | graph.sampleSize = this.io.createConstant( 1 / this.context.sampleRate ); 16 | graph.multiply = this.io.createMultiply(); 17 | this.controls.samples = this.io.createParam( numSamples ); 18 | 19 | graph.sampleSize.connect( graph.multiply, 0, 0 ); 20 | this.controls.samples.connect( graph.multiply, 0, 1 ); 21 | 22 | graph.delay = this.context.createDelay(); 23 | graph.delay.delayTime.value = 0; 24 | graph.multiply.connect( graph.delay.delayTime ); 25 | this.inputs[ 0 ].connect( graph.delay ); 26 | graph.delay.connect( this.outputs[ 0 ] ); 27 | 28 | this.setGraph( graph ); 29 | } 30 | 31 | get samples() { 32 | return this.controls.samples.value; 33 | } 34 | set samples( value ) { 35 | this.controls.samples.value = value; 36 | } 37 | } 38 | 39 | AudioIO.prototype.createSampleDelay = function( numSamples ) { 40 | return new SampleDelay( this, numSamples ); 41 | }; -------------------------------------------------------------------------------- /src/math/relational-operators/EqualToZero.es6: -------------------------------------------------------------------------------- 1 | import "../../core/AudioIO.es6"; 2 | import Node from "../../core/Node.es6"; 3 | import EqualTo from "./EqualTo.es6"; 4 | 5 | // Haven't quite figured out why yet, but this returns 0 when input is 0. 6 | // It should return 1... 7 | // 8 | // For now, I'm just using the EqualTo node with a static 0 argument. 9 | // -------- 10 | // 11 | // class EqualToZero extends Node { 12 | // constructor( io ) { 13 | // super( io, 1, 0 ); 14 | 15 | // this.inputs[ 0 ].gain.value = 100000; 16 | 17 | // // This outputs 0.5 when input is 0, 18 | // // so it has to be piped into a node that 19 | // // transforms it into 1, and leaves zeros 20 | // // alone. 21 | // this.shaper = this.io.createWaveShaper( this.io.curves.EqualToZero ); 22 | 23 | // this.outputs[ 0 ] = this.io.createGreaterThan( 0 ); 24 | 25 | // this.inputs[ 0 ].connect( this.shaper ); 26 | // this.shaper.connect( this.outputs[ 0 ] ); 27 | // } 28 | 29 | // cleanUp() { 30 | // super(); 31 | 32 | // this.shaper.cleanUp(); 33 | // this.shaper = null; 34 | // } 35 | // } 36 | 37 | AudioIO.prototype.createEqualToZero = function() { 38 | // return new EqualToZero( this ); 39 | 40 | return new EqualTo( this, 0 ); 41 | }; -------------------------------------------------------------------------------- /src/math/trigonometry/Tan.es6: -------------------------------------------------------------------------------- 1 | import "../../core/AudioIO.es6"; 2 | import Node from "../../core/Node.es6"; 3 | 4 | // Tangent approximation! 5 | // 6 | // Only works in range of -Math.PI to Math.PI. 7 | // 8 | // sin( input ) / cos( input ) 9 | class Tan extends Node { 10 | 11 | /** 12 | * @param {Object} io Instance of AudioIO. 13 | */ 14 | constructor( io, value ) { 15 | super( io, 0, 1 ); 16 | 17 | var graph = this.getGraph(); 18 | 19 | this.inputs[ 0 ] = this.controls.value = this.io.createParam( value ); 20 | 21 | graph.sine = this.io.createSin(); 22 | graph.cos = this.io.createCos(); 23 | graph.divide = this.io.createDivide( undefined, Math.PI * 2 ); 24 | 25 | this.inputs[ 0 ].connect( graph.sine ); 26 | this.inputs[ 0 ].connect( graph.cos ); 27 | graph.sine.connect( graph.divide, 0, 0 ); 28 | graph.cos.connect( graph.divide, 0, 1 ); 29 | 30 | graph.divide.connect( this.outputs[ 0 ] ); 31 | 32 | this.setGraph( graph ); 33 | } 34 | 35 | get value() { 36 | return this.controls.value.value; 37 | } 38 | 39 | set value( value ) { 40 | this.controls.value.value = value; 41 | } 42 | } 43 | 44 | AudioIO.prototype.createTan = function( value ) { 45 | return new Tan( this, value ); 46 | }; -------------------------------------------------------------------------------- /src/math/Min.es6: -------------------------------------------------------------------------------- 1 | import "../core/AudioIO.es6"; 2 | import Node from "../core/Node.es6"; 3 | 4 | /** 5 | * When input is > `value`, outputs `value`, otherwise outputs input. 6 | * @param {AudioIO} io AudioIO instance 7 | * @param {Number} value The minimum value to test against. 8 | */ 9 | class Min extends Node { 10 | constructor( io, value ) { 11 | super( io, 1, 1 ); 12 | 13 | var graph = this.getGraph(); 14 | 15 | graph.lessThan = this.io.createLessThan(); 16 | this.controls.value = this.inputs[ 1 ] = this.io.createParam( value ); 17 | this.inputs[ 1 ].connect( graph.lessThan ); 18 | this.inputs[ 0 ].connect( graph.lessThan.controls.value ); 19 | 20 | graph.switch = this.io.createSwitch( 2, 0 ); 21 | 22 | this.inputs[ 0 ].connect( graph.switch.inputs[ 0 ] ); 23 | this.inputs[ 1 ].connect( graph.switch.inputs[ 1 ] ); 24 | graph.lessThan.connect( graph.switch.control ); 25 | graph.switch.connect( this.outputs[ 0 ] ); 26 | 27 | this.setGraph( graph ); 28 | } 29 | 30 | get value() { 31 | return this.controls.value.value; 32 | } 33 | set value( value ) { 34 | this.controls.value.value = value; 35 | } 36 | 37 | } 38 | 39 | AudioIO.prototype.createMin = function( value ) { 40 | return new Min( this, value ); 41 | }; -------------------------------------------------------------------------------- /src/math/Max.es6: -------------------------------------------------------------------------------- 1 | import "../core/AudioIO.es6"; 2 | import Node from "../core/Node.es6"; 3 | 4 | /** 5 | * When input is < `value`, outputs `value`, otherwise outputs input. 6 | * @param {AudioIO} io AudioIO instance 7 | * @param {Number} value The minimum value to test against. 8 | */ 9 | 10 | class Max extends Node { 11 | constructor( io, value ) { 12 | super( io, 1, 1 ); 13 | 14 | var graph = this.getGraph(); 15 | 16 | graph.greaterThan = this.io.createGreaterThan(); 17 | graph.switch = this.io.createSwitch( 2, 0 ); 18 | 19 | this.controls.value = this.inputs[ 1 ] = this.io.createParam( value ); 20 | this.inputs[ 1 ].connect( graph.greaterThan ); 21 | this.inputs[ 0 ].connect( graph.greaterThan.controls.value ); 22 | 23 | 24 | this.inputs[ 0 ].connect( graph.switch.inputs[ 0 ] ); 25 | this.inputs[ 1 ].connect( graph.switch.inputs[ 1 ] ); 26 | graph.greaterThan.connect( graph.switch.control ); 27 | graph.switch.connect( this.outputs[ 0 ] ); 28 | 29 | this.setGraph( graph ); 30 | } 31 | 32 | get value() { 33 | return this.controls.value.value; 34 | } 35 | set value( value ) { 36 | this.controls.value.value = value; 37 | } 38 | } 39 | 40 | AudioIO.prototype.createMax = function( value ) { 41 | return new Max( this, value ); 42 | }; -------------------------------------------------------------------------------- /src/math/Switch.es6: -------------------------------------------------------------------------------- 1 | import "../core/AudioIO.es6"; 2 | import Node from "../core/Node.es6"; 3 | 4 | class Switch extends Node { 5 | constructor( io, numCases, startingCase ) { 6 | super( io, 1, 1 ); 7 | 8 | // Ensure startingCase is never < 0 9 | startingCase = typeof startingCase === 'number' ? Math.abs( startingCase ) : startingCase; 10 | 11 | var graph = this.getGraph(); 12 | 13 | graph.cases = []; 14 | 15 | this.controls.index = this.io.createParam( startingCase ); 16 | 17 | for ( var i = 0; i < numCases; ++i ) { 18 | this.inputs[ i ] = this.context.createGain(); 19 | this.inputs[ i ].gain.value = 0.0; 20 | graph.cases[ i ] = this.io.createEqualTo( i ); 21 | graph.cases[ i ].connect( this.inputs[ i ].gain ); 22 | this.controls.index.connect( graph.cases[ i ] ); 23 | this.inputs[ i ].connect( this.outputs[ 0 ] ); 24 | } 25 | 26 | this.setGraph( graph ); 27 | } 28 | 29 | get control() { 30 | return this.controls.index.control; 31 | } 32 | 33 | get value() { 34 | return this.controls.index.value; 35 | } 36 | set value( value ) { 37 | this.controls.index.value = value; 38 | } 39 | } 40 | 41 | 42 | AudioIO.prototype.createSwitch = function( numCases, startingCase ) { 43 | return new Switch( this, numCases, startingCase ); 44 | }; -------------------------------------------------------------------------------- /src/fx/Delay.es6: -------------------------------------------------------------------------------- 1 | import AudioIO from "../core/AudioIO.es6"; 2 | import DryWetNode from "../graphs/DryWetNode.es6"; 3 | 4 | // TODO: Add feedbackLevel and delayTime Param instances 5 | // to control this node. 6 | class Delay extends DryWetNode { 7 | constructor( io, time = 0, feedbackLevel = 0 ) { 8 | super( io, 1, 1 ); 9 | 10 | // Create the control nodes. 11 | this.controls.feedback = this.io.createParam( feedbackLevel ); 12 | this.controls.time = this.io.createParam( time ); 13 | 14 | // Create feedback and delay nodes 15 | this.feedback = this.context.createGain(); 16 | this.delay = this.context.createDelay(); 17 | 18 | // Setup the feedback loop 19 | this.delay.connect( this.feedback ); 20 | this.feedback.connect( this.delay ); 21 | 22 | // Also connect the delay to the wet output. 23 | this.delay.connect( this.wet ); 24 | 25 | // Connect input to delay 26 | this.inputs[ 0 ].connect( this.delay ); 27 | 28 | this.delay.delayTime.value = 0; 29 | this.feedback.gain.value = 0; 30 | 31 | this.controls.time.connect( this.delay.delayTime ); 32 | this.controls.feedback.connect( this.feedback.gain ); 33 | } 34 | 35 | 36 | } 37 | 38 | AudioIO.prototype.createDelay = function( time, feedbackLevel ) { 39 | return new Delay( this, time, feedbackLevel ); 40 | }; 41 | 42 | export default Delay; -------------------------------------------------------------------------------- /src/graphs/PhaseOffset.es6: -------------------------------------------------------------------------------- 1 | import AudioIO from "../core/AudioIO.es6"; 2 | import Node from "../core/Node.es6"; 3 | 4 | class PhaseOffset extends Node { 5 | constructor( io ) { 6 | super( io, 1, 1 ); 7 | 8 | this.reciprocal = this.io.createReciprocal( this.context.sampleRate * 0.5 ); 9 | this.delay = this.context.createDelay(); 10 | this.multiply = this.io.createMultiply(); 11 | this.frequency = this.io.createParam(); 12 | this.phase = this.io.createParam(); 13 | this.halfPhase = this.io.createMultiply( 0.5 ); 14 | 15 | this.delay.delayTime.value = 0; 16 | 17 | this.frequency.connect( this.reciprocal ); 18 | this.reciprocal.connect( this.multiply, 0, 0 ); 19 | this.phase.connect( this.halfPhase ); 20 | this.halfPhase.connect( this.multiply, 0, 1 ); 21 | this.multiply.connect( this.delay.delayTime ); 22 | 23 | this.inputs[ 0 ].connect( this.outputs[ 0 ] ); 24 | this.inputs[ 0 ].connect( this.delay ); 25 | this.delay.connect( this.outputs[ 0 ] ); 26 | 27 | this.outputs[ 0 ].gain.value = 0.5; 28 | 29 | // Store controllable params. 30 | this.controls.frequency = this.frequency; 31 | this.controls.phase = this.phase; 32 | } 33 | 34 | } 35 | 36 | AudioIO.prototype.createPhaseOffset = function() { 37 | return new PhaseOffset( this ); 38 | }; 39 | 40 | export default PhaseOffset; -------------------------------------------------------------------------------- /src/mixins/cleaners.es6: -------------------------------------------------------------------------------- 1 | export default { 2 | cleanUpInOuts: function() { 3 | var inputs, 4 | outputs; 5 | 6 | if( Array.isArray( this.inputs ) ) { 7 | inputs = this.inputs; 8 | 9 | for( var i = 0; i < inputs.length; ++i ) { 10 | if( inputs[ i ] && typeof inputs[ i ].cleanUp === 'function' ) { 11 | inputs[ i ].cleanUp(); 12 | } 13 | else if( inputs[ i ] ) { 14 | inputs[ i ].disconnect(); 15 | } 16 | 17 | inputs[ i ] = null; 18 | } 19 | 20 | this.inputs = null; 21 | } 22 | 23 | if( Array.isArray( this.outputs ) ) { 24 | outputs = this.outputs; 25 | 26 | for( var i = 0; i < outputs.length; ++i ) { 27 | if( outputs[ i ] && typeof outputs[ i ].cleanUp === 'function' ) { 28 | outputs[ i ].cleanUp(); 29 | } 30 | else if( outputs[ i ] ) { 31 | outputs[ i ].disconnect(); 32 | } 33 | 34 | outputs[ i ] = null; 35 | } 36 | 37 | this.outputs = null; 38 | } 39 | }, 40 | 41 | cleanIO: function() { 42 | if( this.io ) { 43 | this.io = null; 44 | } 45 | 46 | if( this.context ) { 47 | this.context = null; 48 | } 49 | } 50 | }; -------------------------------------------------------------------------------- /tests/node-sketches/NoiseOscillator.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 42 | 43 | -------------------------------------------------------------------------------- /src/math/relational-operators/EqualTo.es6: -------------------------------------------------------------------------------- 1 | import "../../core/AudioIO.es6"; 2 | import Node from "../../core/Node.es6"; 3 | 4 | // gain(+100000) -> shaper( <= 0: 1, 1 ) 5 | class EqualTo extends Node { 6 | constructor( io, value ) { 7 | super( io, 1, 1 ); 8 | 9 | var graph = this.getGraph(); 10 | 11 | // TODO 12 | // - Rename this. 13 | graph.value = this.io.createParam( value ), 14 | graph.inversion = this.context.createGain(); 15 | 16 | graph.inversion.gain.value = -1; 17 | 18 | // This curve outputs 0.5 when input is 0, 19 | // so it has to be piped into a node that 20 | // transforms it into 1, and leaves zeros 21 | // alone. 22 | graph.shaper = this.io.createWaveShaper( this.io.curves.EqualToZero ); 23 | 24 | graph.greaterThanZero = this.io.createGreaterThanZero(); 25 | graph.value.connect( graph.inversion ); 26 | graph.inversion.connect( this.inputs[ 0 ] ); 27 | 28 | this.inputs[ 0 ].connect( graph.shaper ); 29 | graph.shaper.connect( graph.greaterThanZero ); 30 | graph.greaterThanZero.connect( this.outputs[ 0 ] ); 31 | 32 | this.controls.value = graph.value; 33 | 34 | this.setGraph( graph ); 35 | 36 | } 37 | 38 | get value() { 39 | return this.controls.value.value; 40 | } 41 | set value( value ) { 42 | this.controls.value.setValueAtTime( value, this.context.currentTime ); 43 | } 44 | } 45 | 46 | AudioIO.prototype.createEqualTo = function( value ) { 47 | return new EqualTo( this, value ); 48 | }; 49 | 50 | export default EqualTo; -------------------------------------------------------------------------------- /src/mixins/connections.es6: -------------------------------------------------------------------------------- 1 | export default { 2 | connect: function( node, outputChannel = 0, inputChannel = 0 ) { 3 | if ( node instanceof AudioParam || node instanceof AudioNode ) { 4 | // this.outputs[ outputChannel ].connect( node ); 5 | this.outputs[ outputChannel ].connect.call( this.outputs[ outputChannel ], node, 0, inputChannel ); 6 | } 7 | 8 | else if ( node && node.outputs && node.outputs.length ) { 9 | // if( node.inputs[ inputChannel ] instanceof Param ) { 10 | // console.log( 'CONNECTING TO PARAM' ); 11 | // node.io.constantDriver.disconnect( node.control ); 12 | // } 13 | 14 | this.outputs[ outputChannel ].connect( node.inputs[ inputChannel ] ); 15 | } 16 | 17 | else { 18 | console.error( 'ASSERT NOT REACHED' ); 19 | console.log( arguments ); 20 | console.trace(); 21 | } 22 | }, 23 | 24 | disconnect: function( node, outputChannel = 0, inputChannel = 0) { 25 | if ( node instanceof AudioParam || node instanceof AudioNode ) { 26 | this.outputs[ outputChannel ].disconnect.call( this.outputs[ outputChannel ], node, 0, inputChannel ); 27 | } 28 | 29 | else if ( node && node.inputs && node.inputs.length ) { 30 | this.outputs[ outputChannel ].disconnect( node.inputs[ inputChannel ] ); 31 | } 32 | 33 | else if( node === undefined && this.outputs ) { 34 | this.outputs.forEach( function( n ) { 35 | if( n && typeof n.disconnect === 'function' ) { 36 | n.disconnect(); 37 | } 38 | } ); 39 | } 40 | } 41 | }; -------------------------------------------------------------------------------- /src/math/Abs.es6: -------------------------------------------------------------------------------- 1 | import "../core/AudioIO.es6"; 2 | import Node from "../core/Node.es6"; 3 | 4 | var SHAPERS = {}; 5 | 6 | function generateShaperCurve( size ) { 7 | var array = new Float32Array( size ), 8 | i = 0, 9 | x = 0; 10 | 11 | for ( i; i < size; ++i ) { 12 | x = ( i / size ) * 2 - 1; 13 | array[ i ] = Math.abs( x ); 14 | } 15 | 16 | return array; 17 | } 18 | 19 | class Abs extends Node { 20 | 21 | /** 22 | * @param {Object} io Instance of AudioIO. 23 | */ 24 | constructor( io, accuracy = 10 ) { 25 | super( io, 1, 1 ); 26 | 27 | // var gainAccuracy = accuracy * 100; 28 | var gainAccuracy = Math.pow( accuracy, 2 ), 29 | graph = this.getGraph(), 30 | size = 1024 * accuracy; 31 | 32 | this.inputs[ 0 ].gain.value = 1 / gainAccuracy; 33 | this.outputs[ 0 ].gain.value = gainAccuracy; 34 | 35 | // To save creating new shaper curves (that can be quite large!) 36 | // each time an instance of Abs is created, shaper curves 37 | // are stored in the SHAPERS object above. The keys to the 38 | // SHAPERS object are the base wavetable curve size (1024) 39 | // multiplied by the accuracy argument. 40 | if( !SHAPERS[ size ] ) { 41 | SHAPERS[ size ] = generateShaperCurve( size ); 42 | } 43 | 44 | graph.shaper = this.io.createWaveShaper( SHAPERS[ size ] ); 45 | 46 | 47 | this.inputs[ 0 ].connect( graph.shaper ); 48 | graph.shaper.connect( this.outputs[ 0 ] ); 49 | 50 | this.setGraph( graph ); 51 | } 52 | } 53 | 54 | AudioIO.prototype.createAbs = function( accuracy ) { 55 | return new Abs( this, accuracy ); 56 | }; -------------------------------------------------------------------------------- /tests/jasmine/spec/Wrap2PI.js: -------------------------------------------------------------------------------- 1 | describe( "Math / OVERFLOW", function() { 2 | it( 'should overflow when gain > Number.MAX_SAFE_INTEGER', function( done ) { 3 | var _io = new AudioIO( new OfflineAudioContext( 1, 44100 * 0.01, 44100 ) ); 4 | 5 | var constant = _io.createConstant( Number.MAX_SAFE_INTEGER - 268435455.5000001 ), 6 | constant5 = _io.createConstant( 5 ), 7 | additionGain = _io.context.createGain(); 8 | 9 | constant.connect( additionGain ); 10 | constant5.connect( additionGain ); 11 | additionGain.connect( _io.master ); 12 | 13 | _io.context.oncomplete = function( e ) { 14 | var buffer = e.renderedBuffer.getChannelData( 0 ); 15 | 16 | for ( var i = 0; i < buffer.length; i++ ) { 17 | console.log( buffer[ i ], Number.MAX_SAFE_INTEGER - 5 ); 18 | expect( buffer[ i ] ).toBeCloseTo( Number.MAX_SAFE_INTEGER - 5 ); 19 | } 20 | 21 | done(); 22 | }; 23 | 24 | _io.context.startRendering(); 25 | } ); 26 | } ); 27 | 28 | 29 | function wrap( wrapTo, input ) { 30 | var reciprocal = Math.pow( wrapTo, -1 ), 31 | mul1 = input * reciprocal, 32 | round = Math.round( mul1 ), 33 | sub = mul1 - round, 34 | mul2 = sub * wrapTo; 35 | 36 | console.log( mul1 ); 37 | 38 | return mul2; 39 | } 40 | 41 | function wrap( kX, kLowerBound, kUpperBound ) { 42 | var range_size = kUpperBound - kLowerBound + 1; 43 | 44 | if ( kX < kLowerBound ) 45 | return kX + range_size * ( ( kLowerBound - kX ) / range_size + 1 ); 46 | 47 | if ( kX > kUpperBound ) 48 | return kX - range_size * ( ( kX - kUpperBound ) / range_size + 1 ); 49 | 50 | return kX; 51 | } -------------------------------------------------------------------------------- /tests/node-sketches/Follower.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 53 | 54 | -------------------------------------------------------------------------------- /examples/basic-synth/res/css/keyboard.css.map: -------------------------------------------------------------------------------- 1 | { 2 | "version": 3, 3 | "mappings": "AAAA,SAAU;EACT,QAAQ,EAAE,QAAQ;EAClB,GAAG,EAAE,CAAC;EACN,IAAI,EAAE,CAAC;EACP,KAAK,EAAE,IAAI;EACX,MAAM,EAAE,KAAK;EACb,UAAU,EAAE,mDAAiF;;AAE7F,eAAM;EACL,QAAQ,EAAE,QAAQ;EAClB,GAAG,EAAE,KAAK;EACV,IAAI,EAAE,CAAC;EACP,KAAK,EAAE,CAAC;EACR,MAAM,EAAE,GAAG;EACX,KAAK,EAAE,IAAI;EACX,SAAS,EAAE,KAAK;EAChB,MAAM,EAAE,MAAM;EACd,uBAAuB,EAAE,WAAW;EAC5B,eAAe,EAAE,WAAW;;AAEpC,sBAAO;EACN,QAAQ,EAAE,QAAQ;EAClB,KAAK,EAAE,IAAI;EACX,OAAO,EAAE,CAAC;EACV,wBAAwB,EAAE,MAAM;EACxB,gBAAgB,EAAE,MAAM;EAChC,UAAU,EAAE,MAAM;EAClB,OAAO,EAAE,YAAY;EACrB,MAAM,EAAE,IAAI;EACZ,MAAM,EAAE,OAAO;;AAEf,6BAAO;EACN,QAAQ,EAAE,QAAQ;EAClB,MAAM,EAAE,MAAM;EACd,IAAI,EAAE,CAAC;EACP,KAAK,EAAE,IAAI;EACX,UAAU,EAAE,MAAM;EAClB,OAAO,EAAE,YAAY;EACrB,cAAc,EAAE,IAAI;EACpB,mBAAmB,EAAE,IAAI;EACtB,gBAAgB,EAAE,IAAI;EACrB,eAAe,EAAE,IAAI;EACjB,WAAW,EAAE,IAAI;;AAG1B,4BAAQ;EACP,KAAK,EAAE,GAAG;EACV,MAAM,EAAE,OAAO;EACf,UAAU,EAAE,iDAAuF;EACnG,yBAAyB,EAAE,EAAE;EAC7B,0BAA0B,EAAE,EAAE;EAC9B,UAAU,EACT,qGAAyC;;AAK1C,0CAAgB;EACf,iBAAiB,EAAE,eAAiB;EAC5B,SAAS,EAAE,eAAiB;;AAGrC,kCAAQ;EACP,UAAU,EAAE,mDAAuF;;AAIrG,4BAAQ;EACP,KAAK,EAAE,EAAE;EACT,WAAW,EAAE,GAAG;EAChB,MAAM,EAAE,GAAG;EACX,UAAU,EAAE,mDAAiF;EAC7F,yBAAyB,EAAE,EAAE;EAC7B,0BAA0B,EAAE,EAAE;EAC9B,MAAM,EAAE,8BAAgC;EACxC,UAAU,EAAE,IAAI;EAChB,UAAU,EAAE,4BAA8B;EAC1C,OAAO,EAAE,KAAK;EACd,KAAK,EAAE,IAAI;;AAEX,mCAAS;EACR,OAAO,EAAE,EAAE;EACX,QAAQ,EAAE,QAAQ;EAClB,MAAM,EAAE,CAAC;EACT,IAAI,EAAE,CAAC;EACP,KAAK,EAAE,CAAC;EACR,MAAM,EAAE,GAAG;EACX,UAAU,EAAE,0CAA4C;;AAGzD,0CAAgB;EACf,iBAAiB,EAAE,yBAA6B;EACxC,SAAS,EAAE,yBAA6B;;AAGjD,kCAAQ;EACP,UAAU,EAAE,mDAAiF;;AAQ/F,qCAAiB;EAChB,WAAW,EAAE,GAAG;EAKhB,OAAO,EAAE,EAAE;;AAKd,eAAM;EACL,QAAQ,EAAE,QAAQ;EAClB,KAAK,EAAE,IAAI;EACX,MAAM,EAAE,IAAI", 4 | "sources": ["../sass/keyboard.scss"], 5 | "names": [], 6 | "file": "keyboard.css" 7 | } 8 | -------------------------------------------------------------------------------- /src/fx/BitReduction.es6: -------------------------------------------------------------------------------- 1 | import "../core/AudioIO.es6"; 2 | import Node from "../core/Node.es6"; 3 | 4 | // TODO: 5 | // - this! 6 | class BitReduction extends Node { 7 | constructor( io ) { 8 | super( io, 1, 1 ); 9 | 10 | var graph = this.getGraph(); 11 | 12 | 13 | // input -> delay -> delayMultiplier -> out 14 | // |--> NOT -> delayMultiplier.gain 15 | // counter -> equalToZero -> inputMultiplier.gain 16 | // input -> inputMultiplier -> out 17 | 18 | this.controls.bitDepth = this.io.createParam(); 19 | 20 | graph.counter = this.io.createCounter( 1, 1 ); 21 | 22 | graph.equalToZero = this.io.createEqualToZero(); 23 | graph.counter.connect( graph.equalToZero ); 24 | graph.equalToZero.connect( this.outputs[ 0 ] ); 25 | 26 | 27 | graph.delay = this.context.createDelay(); 28 | graph.delay.delayTime.value = (1 / this.context.sampleRate); 29 | 30 | graph.delayMultiplier = this.context.createGain(); 31 | graph.delayMultiplier.gain.value = 0; 32 | 33 | graph.not = this.io.createNOT(); 34 | graph.not.connect( graph.delayMultiplier.gain ); 35 | this.inputs[ 0 ].connect( graph.delay ); 36 | graph.delay.connect( this.outputs[ 0 ] ); 37 | 38 | 39 | graph.inputMultiplier = this.context.createGain(); 40 | graph.inputMultiplier.gain.value = 0; 41 | graph.equalToZero.connect( graph.inputMultiplier.gain ); 42 | this.inputs[ 0 ].connect( graph.inputMultiplier ); 43 | graph.inputMultiplier.connect( this.outputs[ 0 ] ); 44 | 45 | graph.counter.start(); 46 | 47 | this.setGraph( graph ); 48 | } 49 | } 50 | 51 | AudioIO.prototype.createBitReduction = function( deg ) { 52 | return new BitReduction( this, deg ); 53 | }; -------------------------------------------------------------------------------- /src/oscillators/OscillatorBank.es6: -------------------------------------------------------------------------------- 1 | import AudioIO from "../core/AudioIO.es6"; 2 | import Node from "../core/Node.es6"; 3 | 4 | var OSCILLATOR_TYPES = [ 5 | 'sine', 6 | 'triangle', 7 | 'sawtooth', 8 | 'square' 9 | ]; 10 | 11 | class OscillatorBank extends Node { 12 | constructor( io ) { 13 | super( io, 0, 1 ); 14 | 15 | var graph = this.getGraph(); 16 | 17 | graph.crossfader = this.io.createCrossfader( OSCILLATOR_TYPES.length, 0 ); 18 | graph.oscillators = []; 19 | 20 | this.controls.frequency = this.io.createParam(); 21 | this.controls.detune = this.io.createParam(); 22 | this.controls.waveform = graph.crossfader.controls.index; 23 | 24 | for( var i = 0; i < OSCILLATOR_TYPES.length; ++i ) { 25 | var osc = this.context.createOscillator(); 26 | 27 | osc.type = OSCILLATOR_TYPES[ i ]; 28 | osc.frequency.value = 0; 29 | osc.start( 0 ); 30 | 31 | this.controls.frequency.connect( osc.frequency ); 32 | this.controls.detune.connect( osc.detune ); 33 | osc.connect( graph.crossfader, 0, i ); 34 | 35 | graph.oscillators.push( osc ); 36 | } 37 | 38 | graph.outputLevel = this.context.createGain(); 39 | graph.outputLevel.gain.value = 0; 40 | 41 | graph.crossfader.connect( graph.outputLevel ); 42 | graph.outputLevel.connect( this.outputs[ 0 ] ); 43 | 44 | this.setGraph( graph ); 45 | } 46 | 47 | start( delay = 0 ) { 48 | this.getGraph().outputLevel.gain.value = 1; 49 | } 50 | 51 | stop( delay = 0 ) { 52 | this.getGraph().outputLevel.gain.value = 0; 53 | } 54 | } 55 | 56 | AudioIO.prototype.createOscillatorBank = function() { 57 | return new OscillatorBank( this ); 58 | }; 59 | 60 | export default OscillatorBank; -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | AudioIO 2 | ======= 3 | 4 | **Notes:** 5 | * This library is a work-in-progress. Some graphs are incomplete or broken! 6 | * Test coverage isn't 100%, nor will it ever be. Some Nodes can only really be tested by ear. 7 | * Documentation is inbound in the near future. 8 | * Since this is still pre-alpha, the spec/API might change. There are also a few inconsistences here and there that are known about and will be fixed up! 9 | 10 | 11 | About 12 | ----- 13 | 14 | AudioIO is a modular component system for building complex instruments, effects, and audio signal graphs using the WebAudio API, similar to NI's Reaktor, PureData, or Max/MSP. 15 | 16 | The goal behind this library is to elimate the need to use expensive ScriptProcessorNodes and instead pass these calculations through "native" WebAudio components. This allows for a much more efficient way of building audio effects and instruments. 17 | 18 | 19 | Features 20 | -------- 21 | 22 | * Perform both basic (add, subtract, multiply, divide), complex (abs, sin, cos, tan, max, min, sqrt, reciprocal, pow, etc.), and binary (AND, OR, NOT) maths on audio signals. 23 | 24 | * Perform comparisons between audio signals (>, <, ===). 25 | 26 | * Emulate JS logic features such as `switch` and `if`/`else` 27 | 28 | * Calculate phase offsets for oscillators using only WebAudio components. 29 | 30 | * Allow for easy creation of envelopes, and oscillators with unison, glide, and polyphony properties. 31 | 32 | 33 | Latest Build 34 | ------------ 35 | 36 | #####Version 37 | * 0.0.2 38 | 39 | #####New Additions: 40 | 41 | * Added SchroederAllPass graph. 42 | * Added SineShaper. 43 | * Added StereoRotation. 44 | * Added StereoWidth. 45 | * Added SineBank (_n_ sine waves, each with harmonic multiplier control) 46 | * OscillatorBank and NoiseOscillator now use the Crossfader node to smoothly transition between waveforms if required. -------------------------------------------------------------------------------- /examples/basic-synth/res/css/main.css.map: -------------------------------------------------------------------------------- 1 | { 2 | "version": 3, 3 | "mappings": "AAAA,CAAE;EACD,sBAAsB,EAAE,oBAAoB;EAC5C,UAAU,EAAE,UAAU;;;AAGvB,IAAK;EACJ,SAAS,EAAE,KAAK;;;AAGjB,IAAK;EACJ,SAAS,EAAE,IAAI;EACf,WAAW,EAAE,MAAM;EACnB,WAAW,EAAE,gCAAgC;EAC7C,KAAK,EAAE,IAAI;EACX,gBAAgB,EAAE,IAAI;;;AAGvB,EAAG;EACF,MAAM,EAAE,YAAY;;;AAGrB,IAAK;EACJ,QAAQ,EAAE,QAAQ;EAClB,GAAG,EAAE,CAAC;EACN,IAAI,EAAE,CAAC;EACP,KAAK,EAAE,CAAC;EACR,MAAM,EAAE,CAAC;EACT,OAAO,EAAE,IAAI;EACb,aAAa,EAAE,GAAG;EAClB,MAAM,EAAE,4BAA8B;EACtC,KAAK,EAAE,KAAK;EACZ,MAAM,EAAE,OAAO;EACf,MAAM,EAAE,IAAI;EACZ,UAAU,EAAE,IAAI;;;AAGjB,OAAQ;EACP,QAAQ,EAAE,QAAQ;EAClB,OAAO,EAAE,IAAI;EACb,UAAU,EAAE,gFAAsF;EAClG,UAAU,EACT,6EAAqC;;AAGtC,UAAG;EACF,KAAK,EAAE,IAAI;EACX,WAAW,EAAE,iCAAmC;;AAGjD,wBAAiB;EAChB,KAAK,EAAE,IAAI;EACX,WAAW,EAAE,gBAA6B;;AAK3C,8DAEe;EACd,MAAM,EAAE,OAAO;;AAGhB,wCACc;EACb,KAAK,EAAE,OAAO;EACd,UAAU,EAAE,mFAAyF;;AAErG,8CAAG;EACF,KAAK,EAAE,IAAI;;AAGZ,0EAAiB;EAChB,KAAK,EAAE,IAAI;EACX,WAAW,EAAE,gBAA6B;;AAI5C,mBAAc;EACb,GAAG,EAAE,CAAC;EACN,IAAI,EAAE,CAAC;;AAER,mBAAc;EACb,GAAG,EAAE,CAAC;EACN,KAAK,EAAE,CAAC;;AAER,sBAAG;EACF,UAAU,EAAE,KAAK;;AAInB,oBAAe;EACd,GAAG,EAAE,CAAC;EACN,IAAI,EAAE,CAAC;EACP,KAAK,EAAE,CAAC;EACR,KAAK,EAAE,OAAO;EACd,MAAM,EAAE,IAAI;;AAEZ,2BAAO;EACN,QAAQ,EAAE,QAAQ;EAClB,KAAK,EAAE,IAAI;EACX,MAAM,EAAE,IAAI;EACZ,aAAa,EAAE,KAAK;EACpB,aAAa,EAAE,kCAAoC;;AAMpD,4BAAU;EACT,OAAO,EAAE,EAAE;EACX,QAAQ,EAAE,QAAQ;EAClB,GAAG,EAAE,KAAK;EACV,IAAI,EAAE,KAAK;EACX,KAAK,EAAE,KAAK;EACZ,MAAM,EAAE,KAAK;EACb,aAAa,EAAE,MAAM;EAClB,UAAU,EACN,8EAAoC;;AAK7C,+CAES;EACR,MAAM,EAAE,OAAO;;AAGhB,gBAAW;EACV,MAAM,EAAE,KAAK;EACb,IAAI,EAAE,CAAC;EACP,KAAK,EAAE,KAAK;;AAGb,aAAQ;EACP,MAAM,EAAE,KAAK;EACb,KAAK,EAAE,OAAO;EACd,KAAK,EAAE,OAAO;;AAGf,cAAS;EACR,MAAM,EAAE,KAAK;EACb,KAAK,EAAE,CAAC;EACR,KAAK,EAAE,OAAO;;;AAMhB,MAAO;EACN,QAAQ,EAAE,QAAQ;EAClB,MAAM,EAAE,CAAC;EACT,IAAI,EAAE,CAAC;EACP,KAAK,EAAE,IAAI", 4 | "sources": ["../sass/main.scss"], 5 | "names": [], 6 | "file": "main.css" 7 | } 8 | -------------------------------------------------------------------------------- /src/graphs/Crossfader.es6: -------------------------------------------------------------------------------- 1 | import "../core/AudioIO.es6"; 2 | import Node from "../core/Node.es6"; 3 | 4 | class Crossfader extends Node { 5 | constructor( io, numCases = 2, startingCase = 0 ) { 6 | 7 | // Ensure startingCase is never < 0 8 | // and number of inputs is always >= 2 (no point 9 | // x-fading between less than two inputs!) 10 | startingCase = Math.abs( startingCase ); 11 | numCases = Math.max( numCases, 2 ); 12 | 13 | super( io, numCases, 1 ); 14 | 15 | this.clamps = []; 16 | this.subtracts = []; 17 | this.xfaders = []; 18 | this.controls.index = this.io.createParam(); 19 | 20 | for( var i = 0; i < numCases - 1; ++i ) { 21 | this.xfaders[ i ] = this.io.createDryWetNode(); 22 | this.subtracts[ i ] = this.io.createSubtract( i); 23 | this.clamps[ i ] = this.io.createClamp( 0, 1 ); 24 | 25 | if( i === 0 ) { 26 | this.inputs[ i ].connect( this.xfaders[ i ].dry ); 27 | this.inputs[ i + 1 ].connect( this.xfaders[ i ].wet ); 28 | } 29 | else { 30 | this.xfaders[ i - 1 ].connect( this.xfaders[ i ].dry ); 31 | this.inputs[ i + 1 ].connect( this.xfaders[ i ].wet ); 32 | } 33 | 34 | this.controls.index.connect( this.subtracts[ i ] ); 35 | this.subtracts[ i ].connect( this.clamps[ i ] ); 36 | this.clamps[ i ].connect( this.xfaders[ i ].controls.dryWet ); 37 | } 38 | 39 | this.xfaders[ this.xfaders.length - 1 ].connect( this.outputs[ 0 ] ); 40 | } 41 | 42 | cleanUp() { 43 | super(); 44 | } 45 | } 46 | 47 | 48 | AudioIO.prototype.createCrossfader = function( numCases, startingCase ) { 49 | return new Crossfader( this, numCases, startingCase ); 50 | }; 51 | 52 | export default Crossfader; -------------------------------------------------------------------------------- /src/core/overrides.es6: -------------------------------------------------------------------------------- 1 | // Need to override existing .connect and .disconnect 2 | // functions for "native" AudioParams and AudioNodes... 3 | // I don't like doing this, but s'gotta be done :( 4 | ( function() { 5 | var originalAudioNodeConnect = AudioNode.prototype.connect, 6 | originalAudioNodeDisconnect = AudioNode.prototype.disconnect; 7 | 8 | AudioNode.prototype.connect = function( node, outputChannel = 0, inputChannel = 0 ) { 9 | if ( node.inputs ) { 10 | if ( Array.isArray( node.inputs ) ) { 11 | this.connect( node.inputs[ inputChannel ] ); 12 | } 13 | else { 14 | this.connect( node.inputs[ 0 ], outputChannel, inputChannel ); 15 | } 16 | } 17 | 18 | else if ( node instanceof AudioNode ) { 19 | originalAudioNodeConnect.apply( this, arguments ); 20 | } 21 | else if ( node instanceof AudioParam ) { 22 | originalAudioNodeConnect.call( this, node, outputChannel ); 23 | } 24 | }; 25 | 26 | AudioNode.prototype.disconnect = function( node, outputChannel = 0, inputChannel = 0 ) { 27 | if ( node && node.inputs ) { 28 | if ( Array.isArray( node.inputs ) ) { 29 | this.disconnect( node.inputs[ inputChannel ] ); 30 | } 31 | else { 32 | this.disconnect( node.inputs[ 0 ], outputChannel, inputChannel ); 33 | } 34 | } 35 | 36 | else if ( node instanceof AudioNode ) { 37 | originalAudioNodeDisconnect.apply( this, arguments ); 38 | } 39 | else if ( node instanceof AudioParam ) { 40 | originalAudioNodeDisconnect.call( this, node, outputChannel ); 41 | } 42 | else if ( node === undefined ) { 43 | originalAudioNodeDisconnect.apply( this, arguments ); 44 | } 45 | }; 46 | }() ); -------------------------------------------------------------------------------- /tests/node-sketches/lerp.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 74 | 75 | -------------------------------------------------------------------------------- /src/graphs/EQShelf.es6: -------------------------------------------------------------------------------- 1 | import AudioIO from "../core/AudioIO.es6"; 2 | import Node from "../core/Node.es6"; 3 | 4 | class EQShelf extends Node { 5 | constructor( io, highFrequency = 2500, lowFrequency = 350, highBoost = -6, lowBoost = 0 ) { 6 | super( io, 1, 1 ); 7 | 8 | this.highFrequency = this.io.createParam( highFrequency ); 9 | this.lowFrequency = this.io.createParam( lowFrequency ); 10 | this.highBoost = this.io.createParam( highBoost ); 11 | this.lowBoost = this.io.createParam( lowBoost ); 12 | 13 | this.lowShelf = this.context.createBiquadFilter(); 14 | this.lowShelf.type = 'lowshelf'; 15 | this.lowShelf.frequency.value = lowFrequency; 16 | this.lowShelf.gain.value = lowBoost; 17 | 18 | this.highShelf = this.context.createBiquadFilter(); 19 | this.highShelf.type = 'highshelf'; 20 | this.highShelf.frequency.value = highFrequency; 21 | this.highShelf.gain.value = highBoost; 22 | 23 | this.inputs[ 0 ].connect( this.lowShelf ); 24 | this.inputs[ 0 ].connect( this.highShelf ); 25 | this.lowShelf.connect( this.outputs[ 0 ] ); 26 | this.highShelf.connect( this.outputs[ 0 ] ); 27 | 28 | // Store controllable params. 29 | // 30 | // TODO: 31 | // - Should these be references to param.control? This 32 | // might allow defaults to be set whilst also allowing 33 | // audio signal control. 34 | this.controls.highFrequency = this.highFrequency; 35 | this.controls.lowFrequency = this.lowFrequency; 36 | this.controls.highBoost = this.highBoost; 37 | this.controls.lowBoost = this.lowBoost; 38 | } 39 | 40 | } 41 | 42 | AudioIO.prototype.createEQShelf = function( highFrequency, lowFrequency, highBoost, lowBoost ) { 43 | return new EQShelf( this, highFrequency, lowFrequency, highBoost, lowBoost ); 44 | }; 45 | 46 | export default EQShelf; -------------------------------------------------------------------------------- /src/math/Pow.es6: -------------------------------------------------------------------------------- 1 | import "../core/AudioIO.es6"; 2 | import Node from "../core/Node.es6"; 3 | 4 | /** 5 | * @param {Object} io Instance of AudioIO. 6 | * 7 | * Note: DOES NOT HANDLE NEGATIVE POWERS. 8 | */ 9 | class Pow extends Node { 10 | constructor( io, value ) { 11 | super( io, 1, 1 ); 12 | 13 | var graph = this.getGraph(); 14 | 15 | graph.multipliers = []; 16 | graph.value = value; 17 | 18 | for ( var i = 0, node = this.inputs[ 0 ]; i < value - 1; ++i ) { 19 | graph.multipliers[ i ] = this.io.createMultiply(); 20 | this.inputs[ 0 ].connect( graph.multipliers[ i ].controls.value ); 21 | node.connect( graph.multipliers[ i ] ); 22 | node = graph.multipliers[ i ]; 23 | } 24 | 25 | node.connect( this.outputs[ 0 ] ); 26 | 27 | this.setGraph( graph ); 28 | } 29 | 30 | cleanUp() { 31 | this.getGraph().value = null; 32 | super(); 33 | } 34 | 35 | get value() { 36 | return this.getGraph().value; 37 | } 38 | set value( value ) { 39 | var graph = this.getGraph(); 40 | 41 | this.inputs[ 0 ].disconnect( graph.multipliers[ 0 ] ); 42 | 43 | for ( var i = graph.multipliers.length - 1; i >= 0; --i ) { 44 | graph.multipliers[ i ].disconnect(); 45 | graph.multipliers.splice( i, 1 ); 46 | } 47 | 48 | for ( var i = 0, node = this.inputs[ 0 ]; i < value - 1; ++i ) { 49 | graph.multipliers[ i ] = this.io.createMultiply(); 50 | this.inputs[ 0 ].connect( graph.multipliers[ i ].controls.value ); 51 | node.connect( graph.multipliers[ i ] ); 52 | node = graph.multipliers[ i ]; 53 | } 54 | 55 | node.connect( this.outputs[ 0 ] ); 56 | 57 | graph.value = value; 58 | } 59 | } 60 | 61 | AudioIO.prototype.createPow = function( value ) { 62 | return new Pow( this, value ); 63 | }; -------------------------------------------------------------------------------- /tests/node-sketches/lerp3.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 63 | 64 | -------------------------------------------------------------------------------- /tests/node-sketches/SineBank.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 50 | 51 | -------------------------------------------------------------------------------- /tests/jasmine/nodeSpecs/Note.js: -------------------------------------------------------------------------------- 1 | describe( "Note", function() { 2 | var node = io.createNote(); 3 | 4 | it( 'should have a context', function() { 5 | expect( node.context ).toEqual( io.context ); 6 | } ); 7 | 8 | it( "should have a reference to it's AudioIO instance", function() { 9 | expect( node.io ).toEqual( io ); 10 | } ); 11 | 12 | 13 | it( 'should default to 440hz note (A3) if !value', function() { 14 | var note = io.createNote(); 15 | expect( note.valueHz ).toEqual( 440 ); 16 | } ); 17 | 18 | it( 'should set default value if given (default format === "hz")', function() { 19 | var note = io.createNote( 230 ); 20 | expect( note.valueHz ).toEqual( 230 ); 21 | } ); 22 | 23 | 24 | it( 'should set default value AND format if given', function() { 25 | var note = io.createNote( 'A3', 'note' ); 26 | expect( note.valueNote ).toEqual( 'A3' ); 27 | 28 | note = io.createNote( 69, 'midi' ); 29 | expect( note.valueMIDI ).toEqual( 69 ); 30 | 31 | note = io.createNote( 120, 'bpm' ); 32 | expect( note.valueBPM ).toEqual( 120 ); 33 | 34 | note = io.createNote( 120, 'hz' ); 35 | expect( note.valueHz ).toEqual( 120 ); 36 | 37 | note = io.createNote( 2500, 'ms' ); 38 | expect( note.valueMs ).toBeCloseTo( 2500 ); 39 | } ); 40 | 41 | it( 'should be able to set value in various formats (needs Param to reflect value correctly)', function() { 42 | var note = io.createNote( 440 ); 43 | note.format = 'note'; 44 | note.valueNote = 'C1'; 45 | expect( note.valueNote ).toEqual( 'C1' ); 46 | } ); 47 | 48 | it( 'should be able to be transposed in semitones', function() { 49 | var note = io.createNote( 'A3', 'note' ); 50 | note.transpose( -1 ); 51 | expect( note.valueNote ).toEqual( 'G#3' ); 52 | 53 | note.transpose( 12 ); 54 | expect( note.valueNote ).toEqual( 'G#4' ); 55 | } ); 56 | } ); -------------------------------------------------------------------------------- /src/oscillators/SineBank.es6: -------------------------------------------------------------------------------- 1 | import AudioIO from "../core/AudioIO.es6"; 2 | import Node from "../core/Node.es6"; 3 | 4 | 5 | class SineBank extends Node { 6 | constructor( io, numSines = 4 ) { 7 | super( io, 0, 1 ); 8 | 9 | var graph = this.getGraph(); 10 | 11 | graph.oscillators = []; 12 | graph.harmonicMultipliers = []; 13 | graph.numSines = numSines; 14 | graph.outputLevel = this.context.createGain(); 15 | graph.outputLevel.gain.value = 1 / numSines; 16 | 17 | this.controls.frequency = this.io.createParam(); 18 | this.controls.detune = this.io.createParam(); 19 | this.controls.harmonics = []; 20 | 21 | for ( var i = 0; i < numSines; ++i ) { 22 | var osc = this.context.createOscillator(), 23 | harmonicControl = this.io.createParam(), 24 | harmonicMultiplier = this.io.createMultiply(); 25 | 26 | osc.type = 'sine'; 27 | osc.frequency.value = 0; 28 | 29 | this.controls.frequency.connect( harmonicMultiplier, 0, 0 ); 30 | harmonicControl.connect( harmonicMultiplier, 0, 1 ); 31 | harmonicMultiplier.connect( osc.frequency ); 32 | this.controls.detune.connect( osc.detune ); 33 | 34 | this.controls.harmonics[ i ] = harmonicControl; 35 | 36 | osc.start( 0 ); 37 | osc.connect( graph.outputLevel ); 38 | graph.oscillators.push( osc ); 39 | } 40 | 41 | graph.outputLevel.connect( this.outputs[ 0 ] ); 42 | 43 | this.setGraph( graph ); 44 | } 45 | 46 | start( delay = 0 ) { 47 | var graph = this.getGraph(); 48 | graph.outputLevel.gain.value = 1 / graph.numSines; 49 | } 50 | 51 | stop( delay = 0 ) { 52 | this.getGraph().outputLevel.gain.value = 0; 53 | } 54 | } 55 | 56 | AudioIO.prototype.createSineBank = function( numSines ) { 57 | return new SineBank( this, numSines ); 58 | }; 59 | 60 | export 61 | default SineBank; -------------------------------------------------------------------------------- /src/math/Lerp.es6: -------------------------------------------------------------------------------- 1 | import "../core/AudioIO.es6"; 2 | import Node from "../core/Node.es6"; 3 | 4 | class Lerp extends Node { 5 | 6 | /** 7 | * @param {Object} io Instance of AudioIO. 8 | */ 9 | constructor( io, start, end, delta ) { 10 | super( io, 3, 1 ); 11 | 12 | var graph = this.getGraph(); 13 | 14 | graph.add = this.io.createAdd(); 15 | graph.subtract = this.io.createSubtract(); 16 | graph.multiply = this.io.createMultiply(); 17 | 18 | graph.start = this.io.createParam( start ); 19 | graph.end = this.io.createParam( end ); 20 | graph.delta = this.io.createParam( delta ); 21 | 22 | graph.end.connect( graph.subtract, 0, 0 ); 23 | graph.start.connect( graph.subtract, 0, 1 ); 24 | graph.subtract.connect( graph.multiply, 0, 0 ); 25 | graph.delta.connect( graph.multiply, 0, 1 ); 26 | 27 | graph.start.connect( graph.add, 0, 0 ); 28 | graph.multiply.connect( graph.add, 0, 1 ); 29 | 30 | graph.add.connect( this.outputs[ 0 ] ); 31 | 32 | this.inputs[ 0 ].connect( graph.start ); 33 | this.inputs[ 1 ].connect( graph.end ); 34 | this.inputs[ 2 ].connect( graph.delta ); 35 | 36 | this.controls.start = graph.start; 37 | this.controls.end = graph.end; 38 | this.controls.delta = graph.delta; 39 | 40 | this.setGraph( graph ); 41 | } 42 | 43 | get start() { 44 | return this.getGraph().start.value; 45 | } 46 | set start( value ) { 47 | this.getGraph().start.value = value; 48 | } 49 | 50 | get end() { 51 | return this.getGraph().end.value; 52 | } 53 | set end( value ) { 54 | this.getGraph().end.value = value; 55 | } 56 | 57 | get delta() { 58 | return this.getGraph().delta.value; 59 | } 60 | set delta( value ) { 61 | this.getGraph().delta.value = value; 62 | } 63 | } 64 | 65 | AudioIO.prototype.createLerp = function( start, end, delta ) { 66 | return new Lerp( this ); 67 | }; -------------------------------------------------------------------------------- /src/math/Reciprocal.es6: -------------------------------------------------------------------------------- 1 | import "../core/AudioIO.es6"; 2 | import Node from "../core/Node.es6"; 3 | 4 | /** 5 | * Outputs the value of 1 / inputValue (or pow(inputValue, -1)) 6 | * Will be useful for doing multiplicative division. 7 | * 8 | * TODO: 9 | * - The waveshaper isn't accurate. It pumps out values differing 10 | * by 1.7906793140301525e-9 or more. 11 | * 12 | * @param {Object} io Instance of AudioIO. 13 | */ 14 | class Reciprocal extends Node { 15 | constructor( io, maxInput ) { 16 | super( io, 1, 1 ); 17 | 18 | var factor = maxInput || 100, 19 | gain = Math.pow( factor, -1 ), 20 | graph = this.getGraph(); 21 | 22 | graph.maxInput = this.context.createGain(); 23 | graph.maxInput.gain.setValueAtTime( gain, this.context.currentTime ); 24 | 25 | // this.inputs[ 0 ] = this.context.createGain(); 26 | this.inputs[ 0 ].gain.setValueAtTime( 0.0, this.context.currentTime ); 27 | 28 | graph.shaper = this.io.createWaveShaper( this.io.curves.Reciprocal ); 29 | 30 | // this.outputs[ 0 ] = this.context.createGain(); 31 | this.outputs[ 0 ].gain.setValueAtTime( 0.0, this.context.currentTime ); 32 | 33 | this.io.constantDriver.connect( graph.maxInput ); 34 | graph.maxInput.connect( this.inputs[ 0 ].gain ); 35 | graph.maxInput.connect( this.outputs[ 0 ].gain ); 36 | 37 | this.inputs[ 0 ].connect( graph.shaper ); 38 | graph.shaper.connect( this.outputs[ 0 ] ); 39 | 40 | this.setGraph( graph ); 41 | } 42 | 43 | get maxInput() { 44 | return graph.maxInput.gain; 45 | } 46 | set maxInput( value ) { 47 | var graph = this.getGraph(); 48 | 49 | if ( typeof value === 'number' ) { 50 | graph.maxInput.gain.cancelScheduledValues( this.context.currentTime ); 51 | graph.maxInput.gain.setValueAtTime( 1 / value, this.context.currentTime ); 52 | } 53 | } 54 | } 55 | 56 | AudioIO.prototype.createReciprocal = function( maxInput ) { 57 | return new Reciprocal( this, maxInput ); 58 | }; -------------------------------------------------------------------------------- /tests/node-sketches/OscillatorBank.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 58 | 59 | -------------------------------------------------------------------------------- /src/graphs/Counter.es6: -------------------------------------------------------------------------------- 1 | import "../core/AudioIO.es6"; 2 | import Node from "../core/Node.es6"; 3 | 4 | // TODO: 5 | // - Turn arguments into controllable parameters; 6 | class Counter extends Node { 7 | 8 | constructor( io, increment, limit, stepTime ) { 9 | super( io, 0, 1 ); 10 | 11 | this.running = false; 12 | 13 | this.stepTime = stepTime || 1 / this.context.sampleRate; 14 | 15 | this.constant = this.io.createParam( increment ); 16 | this.multiply = this.io.createMultiply(); 17 | 18 | this.delay = this.context.createDelay(); 19 | this.delay.delayTime.value = this.stepTime; 20 | 21 | this.feedback = this.context.createGain(); 22 | this.feedback.gain.value = 0; 23 | this.feedback.connect( this.delay ); 24 | 25 | this.multiply.connect( this.delay ); 26 | this.delay.connect( this.feedback ); 27 | this.feedback.connect( this.delay ); 28 | 29 | this.lessThan = this.io.createLessThan( limit ); 30 | this.delay.connect( this.lessThan ); 31 | // this.lessThan.connect( this.feedback.gain ); 32 | this.constant.connect( this.multiply, 0, 0 ); 33 | this.lessThan.connect( this.multiply, 0, 1 ); 34 | 35 | this.delay.connect( this.outputs[ 0 ] ); 36 | 37 | } 38 | 39 | reset() { 40 | var self = this; 41 | 42 | this.stop(); 43 | 44 | setTimeout( function() { 45 | self.start(); 46 | }, 16 ); 47 | } 48 | 49 | start() { 50 | if( this.running === false ) { 51 | this.running = true; 52 | this.delay.delayTime.value = this.stepTime; 53 | this.lessThan.connect( this.feedback.gain ); 54 | } 55 | } 56 | 57 | stop() { 58 | if( this.running === true ) { 59 | this.running = false; 60 | this.lessThan.disconnect( this.feedback.gain ); 61 | this.delay.delayTime.value = 0; 62 | } 63 | } 64 | 65 | cleanUp() { 66 | super(); 67 | } 68 | } 69 | 70 | AudioIO.prototype.createCounter = function( increment, limit, stepTime ) { 71 | return new Counter( this, increment, limit, stepTime ); 72 | }; -------------------------------------------------------------------------------- /tests/jasmine/spec/DegToRad.js: -------------------------------------------------------------------------------- 1 | describe( "Math / DegToRad", function() { 2 | var node = io.createDegToRad(); 3 | 4 | 5 | it( 'should have a context', function() { 6 | expect( node.context ).toEqual( io.context ); 7 | } ); 8 | 9 | it( "should have a reference to it's AudioIO instance", function() { 10 | expect( node.io ).toEqual( io ); 11 | } ); 12 | 13 | it( "should have a connect method", function() { 14 | expect( node.connect ).toEqual( jasmine.any( Function ) ); 15 | } ); 16 | 17 | it( "should have a disconnect method", function() { 18 | expect( node.disconnect ).toEqual( jasmine.any( Function ) ); 19 | } ); 20 | 21 | 22 | 23 | it( 'should have one input and one output', function() { 24 | expect( node.inputs ).toBeDefined(); 25 | expect( node.outputs ).toBeDefined(); 26 | 27 | expect( node.inputs.length ).toBeDefined(); 28 | expect( node.outputs.length ).toBeDefined(); 29 | 30 | expect( node.inputs.length ).toEqual( 1 ); 31 | expect( node.outputs.length ).toEqual( 1 ); 32 | } ); 33 | 34 | it( 'should have a cleanUp method and mark items for GC.', function() { 35 | var n = io.createDegToRad(); 36 | expect( n.cleanUp ).toEqual( jasmine.any( Function ) ); 37 | 38 | n.cleanUp(); 39 | 40 | expect( n.inputs ).toEqual( null ); 41 | expect( n.outputs ).toEqual( null ); 42 | } ); 43 | 44 | it( 'should correctly calculate conversion of degrees to radians', function( done ) { 45 | var angle = -Math.PI + Math.random() * ( Math.PI * 2 ), 46 | expected = angle * ( Math.PI / 180 ); 47 | 48 | offlineAudioTest( { 49 | onSetup: function( io ) { 50 | var a = io.createConstant( angle ), 51 | node = io.createDegToRad(); 52 | 53 | a.connect( node ); 54 | node.connect( io.master ); 55 | }, 56 | onCompare: function( value ) { 57 | expect( value ).toBeCloseTo( expected ); 58 | }, 59 | onComplete: function() { 60 | done(); 61 | } 62 | } ); 63 | } ); 64 | } ); -------------------------------------------------------------------------------- /tests/node-sketches/WaveformDirection.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 62 | 63 | -------------------------------------------------------------------------------- /src/core/WaveShaper.es6: -------------------------------------------------------------------------------- 1 | import AudioIO from "./AudioIO.es6"; 2 | import _setIO from "../mixins/setIO.es6"; 3 | import connections from "../mixins/connections.es6"; 4 | import cleaners from "../mixins/cleaners.es6"; 5 | 6 | class WaveShaper { 7 | constructor( io, curveOrIterator, size ) { 8 | this._setIO( io ); 9 | 10 | this.shaper = this.context.createWaveShaper(); 11 | 12 | // If a Float32Array is provided, use it 13 | // as the curve value. 14 | if ( curveOrIterator instanceof Float32Array ) { 15 | this.shaper.curve = curveOrIterator; 16 | } 17 | 18 | // If a function is provided, create a curve 19 | // using the function as an iterator. 20 | else if ( typeof curveOrIterator === 'function' ) { 21 | size = typeof size === 'number' && size >= 2 ? size : CONFIG.curveResolution; 22 | 23 | var array = new Float32Array( size ), 24 | i = 0, 25 | x = 0; 26 | 27 | for ( i; i < size; ++i ) { 28 | x = ( i / size ) * 2 - 1; 29 | array[ i ] = curveOrIterator( x, i, size ); 30 | } 31 | 32 | this.shaper.curve = array; 33 | } 34 | 35 | // Otherwise, default to a Linear curve. 36 | else { 37 | this.shaper.curve = this.io.curves.Linear; 38 | } 39 | 40 | this.inputs = this.outputs = [ this.shaper ]; 41 | } 42 | 43 | cleanUp() { 44 | this._cleanUpInOuts(); 45 | this._cleanIO(); 46 | this.disconnect(); 47 | this.shaper = null; 48 | } 49 | 50 | get curve() { 51 | return this.shaper.curve; 52 | } 53 | set curve( curve ) { 54 | if( curve instanceof Float32Array ) { 55 | this.shaper.curve = curve; 56 | } 57 | } 58 | } 59 | 60 | AudioIO.mixinSingle( WaveShaper.prototype, _setIO, '_setIO' ); 61 | AudioIO.mixinSingle( WaveShaper.prototype, cleaners.cleanUpInOuts, '_cleanUpInOuts' ); 62 | AudioIO.mixinSingle( WaveShaper.prototype, cleaners.cleanIO, '_cleanIO' ); 63 | AudioIO.mixin( WaveShaper.prototype, connections ); 64 | 65 | AudioIO.prototype.createWaveShaper = function( curve, size ) { 66 | return new WaveShaper( this, curve, size ); 67 | }; -------------------------------------------------------------------------------- /src/mixins/scales.es6: -------------------------------------------------------------------------------- 1 | export default { 2 | acoustic: [ 2, 2, 2, 1, 2, 1, 2 ], 3 | aeolian: [ 2, 1, 2, 2, 1, 2, 2 ], // Also natural minor 4 | algerian: [ 2, 1, 3, 1, 1, 3, 1, 2, 1, 2, 2, 1, 3 ], 5 | altered: [ 1, 2, 1, 2, 2, 2, 2 ], 6 | augmented: [ 3, 1, 3, 1, 3, 1 ], 7 | bebop: [ 2, 2, 1, 2, 2, 1, 1 ], 8 | bebopMajor: [ 2, 2, 1, 2, 2, 2, 1 ], 9 | blues: [ 3, 2, 1, 1, 3, 2 ], 10 | chromatic: [ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1 ], 11 | dorian: [ 2, 1, 2, 2, 2, 1, 2 ], 12 | doubleHarmonic: [ 1, 3, 1, 2, 1, 3, 1 ], 13 | enigmatic: [ 1, 3, 2, 2, 2, 1, 1 ], 14 | eulerFokkerGenus: [ 2, 3, 2, 3, 1, 1 ], 15 | flamenco: [ 1, 3, 1, 2, 1, 3, 1 ], 16 | gamelanPentatonic: [ 2, 3, 2, 3, 2 ], 17 | gypsyMinor: [ 2, 1, 3, 1, 1, 2, 2 ], 18 | halfDiminished: [ 2, 1, 2, 1, 2, 2, 2 ], 19 | harmonicMajor: [ 2, 2, 1, 2, 1, 3, 1 ], 20 | harmonicMinor: [ 2, 1, 2, 2, 1, 3, 1 ], 21 | hirajoshi: [ 4, 2, 1, 4, 1 ], 22 | hungarianGypsy: [ 2, 1, 3, 1, 1, 3, 1 ], // Also hungarian minor 23 | insen: [ 1, 4, 2, 3, 2 ], 24 | ionian: [ 2, 2, 1, 2, 2, 2, 1 ], // Also major 25 | istrian: [ 1, 2, 1, 2, 1, 2, 2 ], 26 | iwato: [ 1, 4, 1, 4, 2 ], 27 | locrian: [ 1, 2, 2, 1, 2, 2, 2 ], 28 | locrianMajor: [ 2, 2, 1, 1, 2, 2, 2 ], 29 | lydianAugmented: [ 2, 2, 2, 2, 1, 2, 1 ], 30 | lydianDominant: [ 2, 2, 2, 1, 2, 1, 2 ], 31 | lydian: [ 2, 2, 2, 1, 2, 2, 1 ], 32 | major: [ 2, 2, 1, 2, 2, 2, 1 ], 33 | minor: [ 2, 1, 2, 2, 1, 2, 2 ], 34 | minyo: [ 3, 2, 2, 3, 2 ], 35 | mixolydian: [ 2, 2, 1, 2, 2, 1, 2 ], 36 | miyakoBushi: [ 1, 4, 2, 1, 4 ], 37 | neopolitanMajor: [ 1, 2, 2, 2, 2, 2, 1 ], 38 | neopolitanMinor: [ 1, 2, 2, 2, 1, 3, 1 ], 39 | pentatonicMajor: [ 2, 2, 3, 2, 3 ], 40 | pentatonicMinor: [ 3, 2, 2, 3, 2 ], 41 | persian: [ 1, 3, 1, 1, 2, 3, 1 ], 42 | phrygianDominant: [ 1, 3, 1, 2, 1, 2, 2 ], 43 | phrygian: [ 1, 2, 2, 2, 1, 2, 2 ], 44 | prometheus: [ 2, 2, 2, 3, 1, 2 ], 45 | quarterTone: [ 46 | 0.5, 0.5, 0.5, 0.5, 0.5, 47 | 0.5, 0.5, 0.5, 0.5, 0.5, 48 | 0.5, 0.5, 0.5, 0.5, 0.5, 49 | 0.5, 0.5, 0.5, 0.5, 0.5, 50 | 0.5, 0.5, 0.5, 0.5 51 | ], 52 | tritone: [ 1, 3, 2, 1, 3, 2 ], 53 | ukrainianDorian: [ 2, 1, 3, 1, 2, 1, 2 ] 54 | }; -------------------------------------------------------------------------------- /tests/jasmine/spec/RadToDeg.js: -------------------------------------------------------------------------------- 1 | describe( "Math / RadToDeg", function() { 2 | var node = io.createRadToDeg(); 3 | 4 | 5 | it( 'should have a context', function() { 6 | expect( node.context ).toEqual( io.context ); 7 | } ); 8 | 9 | it( "should have a reference to it's AudioIO instance", function() { 10 | expect( node.io ).toEqual( io ); 11 | } ); 12 | 13 | it( "should have a connect method", function() { 14 | expect( node.connect ).toEqual( jasmine.any( Function ) ); 15 | } ); 16 | 17 | it( "should have a disconnect method", function() { 18 | expect( node.disconnect ).toEqual( jasmine.any( Function ) ); 19 | } ); 20 | 21 | 22 | 23 | it( 'should have one input and one output', function() { 24 | expect( node.inputs ).toBeDefined(); 25 | expect( node.outputs ).toBeDefined(); 26 | 27 | expect( node.inputs.length ).toBeDefined(); 28 | expect( node.outputs.length ).toBeDefined(); 29 | 30 | expect( node.inputs.length ).toEqual( 1 ); 31 | expect( node.outputs.length ).toEqual( 1 ); 32 | } ); 33 | 34 | it( 'should have a cleanUp method and mark items for GC.', function() { 35 | var n = io.createRadToDeg( 5 ); 36 | expect( n.cleanUp ).toEqual( jasmine.any( Function ) ); 37 | 38 | n.cleanUp(); 39 | 40 | expect( n.inputs ).toEqual( null ); 41 | expect( n.outputs ).toEqual( null ); 42 | } ); 43 | 44 | 45 | it( 'should correctly calculate conversion of radians to degrees', function( done ) { 46 | var _io = new AudioIO( new OfflineAudioContext( 1, 44100 * 0.1, 44100 ) ), 47 | _node, 48 | input, 49 | angle = Math.PI * 0.12; 50 | 51 | input = _io.createConstant( angle ); 52 | _node = _io.createRadToDeg(); 53 | 54 | input.connect( _node ); 55 | _node.connect( _io.master ); 56 | 57 | _io.context.oncomplete = function( e ) { 58 | var buffer = e.renderedBuffer.getChannelData( 0 ); 59 | 60 | for ( var i = 0; i < buffer.length; i++ ) { 61 | expect( buffer[ i ] ).toBeCloseTo( angle * ( 180 / Math.PI ) ); 62 | } 63 | 64 | done(); 65 | }; 66 | 67 | _io.context.startRendering(); 68 | } ); 69 | } ); -------------------------------------------------------------------------------- /tests/node-sketches/PinkNoise.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 62 | 63 | -------------------------------------------------------------------------------- /tests/node-sketches/PhaseOffset.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 66 | 67 | -------------------------------------------------------------------------------- /src/mixins/Pool.es6: -------------------------------------------------------------------------------- 1 | /** 2 | * Creates a pool of instantiated constructors. 3 | * 4 | * Arguments: 5 | * - `Constructor`: A reference to the constructor to use in 6 | * the pool. 7 | * 8 | * - `size`: The number of instances to create. 9 | * 10 | * - `constructorArguments`: An array of arguments to pass to the 11 | * constructor when it's instantiated. 12 | * 13 | * - `onCreate`: A function called after each individual 14 | * instance is created. Called with one argument, 15 | * which is the instance just created. 16 | */ 17 | class Pool { 18 | constructor( Constructor, size, constructorArguments, onCreate ) { 19 | this.size = size; 20 | this.Constructor = Constructor; 21 | this.constructorArguments = constructorArguments; 22 | this.onCreate = onCreate || function() {}; 23 | 24 | this.pool = []; 25 | 26 | this.populate(); 27 | } 28 | 29 | populate() { 30 | for( let i = this.size - 1, instance; i >= 0; --i ) { 31 | instance = this._createInstance.apply( this, this.constructorArguments ); 32 | this.onCreate( instance ); 33 | this.pool.push( instance ); 34 | } 35 | } 36 | 37 | get() { 38 | if( this.pool.length > 0 ) { 39 | var instance = this.pool.pop(); 40 | instance.__inUse__ = true; 41 | return instance; 42 | } 43 | else { 44 | return null; 45 | } 46 | } 47 | 48 | release( instance ) { 49 | instance.__inUse__ = false; 50 | this.pool.unshift( instance ); 51 | } 52 | } 53 | 54 | Pool.prototype._createInstance = (function() { 55 | var Constructor; 56 | 57 | // The surrogate Constructor. Returns an instance of 58 | // `this.Constructor` called with an array of arguments. 59 | function Surrogate( args ) { 60 | return Constructor.apply( this, args ); 61 | } 62 | 63 | 64 | return function() { 65 | // If it's the first time running this 66 | // function, set up the Constructor variable and the 67 | // Surrogate's prototype, so the Constructor is constructed 68 | // properly. 69 | if( !Constructor ) { 70 | Constructor = this.Constructor; 71 | Surrogate.prototype = Constructor.prototype; 72 | } 73 | 74 | // Return a new instance of the Surrogate constructor, 75 | // which itself returns a new instance of the `this.Constructor` 76 | // constructor. 77 | return new Surrogate( arguments ); 78 | }; 79 | }()); 80 | 81 | export default Pool; -------------------------------------------------------------------------------- /tests/node-sketches/PeakDetector.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 70 | 71 | -------------------------------------------------------------------------------- /tests/jasmine/spec/Cos.js: -------------------------------------------------------------------------------- 1 | describe( "Math / Cos", function() { 2 | var node = io.createCos( 1 ); 3 | 4 | 5 | it( 'should have a context', function() { 6 | expect( node.context ).toEqual( io.context ); 7 | } ); 8 | 9 | it( "should have a reference to it's AudioIO instance", function() { 10 | expect( node.io ).toEqual( io ); 11 | } ); 12 | 13 | it( "should have a connect method", function() { 14 | expect( node.connect ).toEqual( jasmine.any( Function ) ); 15 | } ); 16 | 17 | it( "should have a disconnect method", function() { 18 | expect( node.disconnect ).toEqual( jasmine.any( Function ) ); 19 | } ); 20 | 21 | 22 | 23 | it( 'should have one input and one output', function() { 24 | expect( node.inputs ).toBeDefined(); 25 | expect( node.outputs ).toBeDefined(); 26 | 27 | expect( node.inputs.length ).toBeDefined(); 28 | expect( node.outputs.length ).toBeDefined(); 29 | 30 | expect( node.inputs.length ).toEqual( 1 ); 31 | expect( node.outputs.length ).toEqual( 1 ); 32 | } ); 33 | 34 | 35 | it( 'should output cosine of audio input', function( done ) { 36 | var val = -Math.PI + Math.random() * ( Math.PI * 2 ), 37 | expected = Math.cos( val ); 38 | 39 | offlineAudioTest( { 40 | onSetup: function( io ) { 41 | var a = io.createConstant( val ), 42 | node = io.createCos(); 43 | 44 | a.connect( node ); 45 | node.connect( io.master ); 46 | }, 47 | onCompare: function( value ) { 48 | expect( value ).toBeCloseTo( expected ); 49 | }, 50 | onComplete: function() { 51 | done(); 52 | } 53 | } ); 54 | } ); 55 | 56 | it( 'should output cos( value )', function( done ) { 57 | var val = -Math.PI + Math.random() * ( Math.PI * 2 ), 58 | expected = Math.cos( val ); 59 | 60 | offlineAudioTest( { 61 | onSetup: function( io ) { 62 | var node = io.createCos( val ); 63 | node.connect( io.master ); 64 | }, 65 | onCompare: function( value ) { 66 | expect( value ).toBeCloseTo( expected ); 67 | }, 68 | onComplete: function() { 69 | done(); 70 | } 71 | } ); 72 | } ); 73 | } ); -------------------------------------------------------------------------------- /tests/node-sketches/FM.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 67 | 68 | -------------------------------------------------------------------------------- /tests/node-sketches/PMOscillator.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 67 | 68 | -------------------------------------------------------------------------------- /GruntFile.js: -------------------------------------------------------------------------------- 1 | module.exports = function( grunt ) { 2 | grunt.initConfig( { 3 | browserify: { 4 | distES5: { 5 | options: { 6 | browserifyOptions: { 7 | debug: true 8 | }, 9 | transform: [ 10 | [ 'babelify', { 11 | loose: "all", 12 | // plugins: [ "closure-elimination" ], 13 | // optional: [ 14 | // "minification.constantFolding", 15 | // "minification.deadCodeElimination", 16 | // "minification.propertyLiterals" 17 | // ] 18 | } ] 19 | ] 20 | }, 21 | files: { 22 | './build/AudioIO.js': [ './src/main.es6' ] 23 | } 24 | }, 25 | 26 | distES6: { 27 | options: { 28 | transform: [ 29 | [ 'babelify', { 30 | loose: "all", 31 | blacklist: [ 32 | 'es6.classes' 33 | ] 34 | } ] 35 | ] 36 | }, 37 | files: { 38 | './build/AudioIO.es6': [ './src/main.es6' ] 39 | } 40 | } 41 | }, 42 | 43 | uglify: { 44 | options: { 45 | sourceMaps: true 46 | }, 47 | distES5: { 48 | files: { 49 | './build/AudioIO.min.js': [ './build/AudioIO.js' ] 50 | } 51 | }, 52 | // distES6: { 53 | // files: { 54 | // './build/AudioIO.min.es6': [ './build/AudioIO.es6' ] 55 | // } 56 | // } 57 | }, 58 | 59 | watch: { 60 | scripts: { 61 | files: [ './src/**/*.es6' ], 62 | tasks: [ 'browserify:distES5' ] 63 | } 64 | } 65 | } ); 66 | 67 | grunt.loadNpmTasks( 'grunt-browserify' ); 68 | grunt.loadNpmTasks( 'grunt-contrib-watch' ); 69 | grunt.loadNpmTasks( 'grunt-contrib-uglify' ); 70 | 71 | grunt.registerTask( 'default', [ "browserify:distES5", "watch" ] ); 72 | grunt.registerTask( 'build', [ "browserify", 'uglify' ] ); 73 | }; -------------------------------------------------------------------------------- /tests/node-sketches/GeneratorPlayer.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 70 | 71 | -------------------------------------------------------------------------------- /src/fx/PingPongDelay.es6.js: -------------------------------------------------------------------------------- 1 | import AudioIO from "../core/AudioIO.es6"; 2 | import _setIO from "../mixins/setIO.es6"; 3 | import connections from "../mixins/connections.es6"; 4 | import cleaners from "../mixins/cleaners.es6"; 5 | import DryWetNode from "../graphs/DryWetNode.es6"; 6 | import Delay from "./Delay.es6"; 7 | 8 | // TODO: 9 | // - Convert this node to use Param controls 10 | // for time and feedback. 11 | 12 | class PingPongDelay extends DryWetNode { 13 | constructor( io, time = 0.25, feedbackLevel = 0.5 ) { 14 | super( io, 1, 1 ); 15 | 16 | // Create channel splitter and merger 17 | this.splitter = this.context.createChannelSplitter( 2 ); 18 | this.merger = this.context.createChannelMerger( 2 ); 19 | 20 | // Create feedback and delay nodes 21 | this.feedbackL = this.context.createGain(); 22 | this.feedbackR = this.context.createGain(); 23 | this.delayL = this.context.createDelay(); 24 | this.delayR = this.context.createDelay(); 25 | 26 | // Setup the feedback loop 27 | this.delayL.connect( this.feedbackL ); 28 | this.feedbackL.connect( this.delayR ); 29 | this.delayR.connect( this.feedbackR ); 30 | this.feedbackR.connect( this.delayL ); 31 | 32 | 33 | this.inputs[ 0 ].connect( this.splitter ); 34 | this.splitter.connect( this.delayL, 0 ); 35 | this.feedbackL.connect( this.merger, 0, 0 ); 36 | this.feedbackR.connect( this.merger, 0, 1 ); 37 | this.merger.connect( this.wet ); 38 | 39 | this.time = time; 40 | this.feedbackLevel = feedbackLevel; 41 | } 42 | 43 | get time() { 44 | return this.delayL.delayTime; 45 | } 46 | 47 | set time( value ) { 48 | this.delayL.delayTime.linearRampToValueAtTime( 49 | value, 50 | this.context.currentTime + 0.5 51 | ); 52 | 53 | this.delayR.delayTime.linearRampToValueAtTime( 54 | value, 55 | this.context.currentTime + 0.5 56 | ); 57 | } 58 | 59 | get feedbackLevel() { 60 | return this.feedbackL.gain.value; 61 | } 62 | 63 | set feedbackLevel( level ) { 64 | this.feedbackL.gain.value = level; 65 | this.feedbackR.gain.value = level; 66 | } 67 | } 68 | 69 | AudioIO.mixinSingle( PingPongDelay.prototype, _setIO, '_setIO' ); 70 | AudioIO.mixin( PingPongDelay.prototype, connections ); 71 | AudioIO.mixin( PingPongDelay.prototype, cleaners ); 72 | 73 | AudioIO.prototype.createPingPongDelay = function( time, feedbackLevel ) { 74 | return new PingPongDelay( this, time, feedbackLevel ); 75 | }; -------------------------------------------------------------------------------- /src/graphs/DryWetNode.es6: -------------------------------------------------------------------------------- 1 | import AudioIO from "../core/AudioIO.es6"; 2 | import _setIO from "../mixins/setIO.es6"; 3 | import connections from "../mixins/connections.es6"; 4 | import cleaners from "../mixins/cleaners.es6"; 5 | import Node from "../core/Node.es6"; 6 | 7 | 8 | // This function creates a graph that allows morphing 9 | // between two gain nodes. 10 | // 11 | // It looks a little bit like this: 12 | // 13 | // dry -> ---------------------------------------------> | 14 | // | v 15 | // v Gain(0-1) -> Gain(-1) -> output 16 | // (fader) (invert phase) (summing) 17 | // ^ 18 | // wet -> Gain(-1) -> -| 19 | // (invert phase) 20 | // 21 | // When adjusting the fader's gain value in this graph, 22 | // input1's gain level will change from 0 to 1, 23 | // whilst input2's gain level will change from 1 to 0. 24 | class DryWetNode extends Node { 25 | constructor( io ) { 26 | super( io, 2, 1 ); 27 | 28 | this.dry = this.inputs[ 0 ]; 29 | this.wet = this.inputs[ 1 ]; 30 | 31 | // Invert wet signal's phase 32 | this.wetInputInvert = this.context.createGain(); 33 | this.wetInputInvert.gain.value = -1; 34 | this.wet.connect( this.wetInputInvert ); 35 | 36 | // Create the fader node 37 | this.fader = this.context.createGain(); 38 | this.fader.gain.value = 0; 39 | 40 | // Create the control node. It sets the fader's value. 41 | this.dryWetControl = this.io.createParam(); 42 | this.dryWetControl.connect( this.fader.gain ); 43 | 44 | // Invert the fader node's phase 45 | this.faderInvert = this.context.createGain(); 46 | this.faderInvert.gain.value = -1; 47 | 48 | // Connect fader to fader phase inversion, 49 | // and fader phase inversion to output. 50 | this.wetInputInvert.connect( this.fader ); 51 | this.fader.connect( this.faderInvert ); 52 | this.faderInvert.connect( this.outputs[ 0 ] ); 53 | 54 | // Connect dry input to both the output and the fader node 55 | this.dry.connect( this.outputs[ 0 ] ); 56 | this.dry.connect( this.fader ); 57 | 58 | // Add a 'dryWet' property to the controls object. 59 | this.controls.dryWet = this.dryWetControl; 60 | } 61 | 62 | } 63 | 64 | 65 | 66 | 67 | AudioIO.prototype.createDryWetNode = function() { 68 | return new DryWetNode( this ); 69 | }; 70 | 71 | export default DryWetNode; 72 | -------------------------------------------------------------------------------- /tests/jasmine/MathTests.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Jasmine Spec Runner v2.2.0 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | -------------------------------------------------------------------------------- /src/mixins/chords.es6: -------------------------------------------------------------------------------- 1 | export default { 2 | Root: [ 0 ], 3 | Octave: [ 0, 12 ], 4 | Fifth: [ 0, 7 ], 5 | LowerFifth: [ 0, -5 ], 6 | Major: [ 0, 4, 7 ], 7 | MajorThird: [ 0, 4 ], 8 | MajorFifth: [ 0, 7 ], 9 | Suspended: [ 0, 5, 7 ], 10 | Suspended2nd: [ 0, 2, 7 ], 11 | Sixth: [ 0, 4, 7, 9 ], 12 | SixthNoFifth: [ 0, 4, 9 ], 13 | SixNine: [ 0, 4, 9, 14 ], 14 | Minor: [ 0, 3, 7 ], 15 | MinorSixth: [ 0, 3, 9 ], 16 | MinorSixthWithFifth: [ 0, 3, 7, 9 ], 17 | Diminished: [ 0, 3, 6 ], 18 | Augmented: [ 0, 4, 8 ], 19 | MajorSeventh: [ 0, 4, 11 ], 20 | MajorSeventhWithFifth: [ 0, 4, 7, 11 ], 21 | MajorNinth: [ 0, 4, 11, 14 ], 22 | MajorNinthWithFifth: [ 0, 4, 7, 11, 14 ], 23 | AddNinth: [ 0, 4, 7, 14 ], 24 | DominantSeventh: [ 0, 4, 10 ], 25 | DominantSeventhWithFifth: [ 0, 4, 7, 10 ], 26 | SeventhFlatFifth: [ 0, 4, 6, 10 ], 27 | SeventhSharpFifth: [ 0, 4, 8, 10 ], 28 | Ninth: [ 0, 4, 10, 14 ], 29 | SeventhFlatNinth: [ 0, 4, 10, 13 ], 30 | SeventhSharpNinth: [ 0, 4, 10, 15 ], 31 | Thirteenth: [ 0, 10, 16, 21 ], 32 | MinorSeventh: [ 0, 3, 10 ], 33 | MinorSeventhWithFifth: [ 0, 3, 7, 10 ], 34 | MinorSeventhFlatFifth: [ 0, 3, 6, 10 ], 35 | MinorSeventhSharpFifth: [ 0, 3, 8, 10 ], 36 | MinorNinth: [ 0, 3, 10, 14 ], 37 | MinorSeventhFlatNinth: [ 0, 3, 10, 13 ], 38 | DiminishedSeventh: [ 0, 3, 9 ], 39 | DiminishedSeventhWithFlatFifth: [ 0, 3, 6, 9 ], 40 | 41 | 42 | // From: 43 | // https://en.wikipedia.org/wiki/Chord_(music)#External_links 44 | // 45 | // Triads 46 | MajorTriad: [ 0, 4, 7 ], 47 | MinorTriad: [ 0, 3, 7 ], 48 | DiminishedTriad: [ 0, 3, 6 ], 49 | AugmentedTriad: [ 0, 4, 8 ], 50 | 51 | // Sevenths 52 | MajorSeventh: [ 0, 4, 7, 11 ], 53 | MinorSeventh: [ 0, 3, 7, 10 ], 54 | DominantSeventh: [ 0, 4, 7, 10 ], 55 | DominantSeventhFlatFive: [ 0, 4, 6, 10 ], 56 | DominantSeventhSharpNinth: [ 0, 4, 7, 10, 15 ], 57 | DiminishedSeventh: [ 0, 3, 6, 9 ], 58 | HalfDiminishedSeventh: [ 0, 3, 6, 10 ], 59 | DiminishedMajorSeventh: [ 0, 3, 6, 11 ], 60 | MinorMajorSeventh: [ 0, 3, 7, 11 ], 61 | AugmentedMajorSeventh: [ 0, 4, 8, 11 ], 62 | AugmentedMinorSeventh: [ 0, 4, 8, 10 ], 63 | HarmonicSeventh: [ 0, 4, 7, 9.68826 ], 64 | 65 | // Ninths 66 | MajorNinth: [ 0, 4, 11, 14 ], 67 | MinorNinth: [ 0, 3, 10, 14 ], 68 | DominantNinth: [ 0, 4, 7, 11, 14 ], 69 | DominantMinorNinth: [ 0, 4, 7, 11, 13 ], 70 | SixNine: [ 0, 4, 9, 14 ], 71 | AddNinth: [ 0, 4, 7, 14 ], 72 | Suspended2nd: [ 0, 2, 7 ], 73 | 74 | // Elevenths 75 | 76 | // Thirteenths 77 | 78 | // Sixth 79 | 80 | }; -------------------------------------------------------------------------------- /src/math/Constant.es6: -------------------------------------------------------------------------------- 1 | import AudioIO from '../core/AudioIO.es6'; 2 | import Node from '../core/Node.es6'; 3 | 4 | class Constant extends Node { 5 | /** 6 | * A constant-rate audio signal 7 | * @param {Object} io Instance of AudioIO 8 | * @param {Number} value Value of the constant 9 | */ 10 | constructor( io, value = 0.0 ) { 11 | super( io, 0, 1 ); 12 | 13 | this.outputs[ 0 ].gain.value = typeof value === 'number' ? value : 0; 14 | this.io.constantDriver.connect( this.outputs[ 0 ] ); 15 | } 16 | 17 | get value() { 18 | return this.outputs[ 0 ].gain.value; 19 | } 20 | set value( value ) { 21 | this.outputs[ 0 ].gain.value = value; 22 | } 23 | } 24 | 25 | AudioIO.prototype.createConstant = function( value ) { 26 | return new Constant( this, value ); 27 | }; 28 | 29 | 30 | // A bunch of preset constants. 31 | (function() { 32 | var E, 33 | PI, 34 | PI2, 35 | LN10, 36 | LN2, 37 | LOG10E, 38 | LOG2E, 39 | SQRT1_2, 40 | SQRT2; 41 | 42 | AudioIO.prototype.createConstantE = function() { 43 | var c = E || this.createConstant( Math.E ); 44 | E = c; 45 | return c; 46 | }; 47 | 48 | AudioIO.prototype.createConstantPI = function() { 49 | var c = PI || this.createConstant( Math.PI ); 50 | PI = c; 51 | return c; 52 | }; 53 | 54 | AudioIO.prototype.createConstantPI2 = function() { 55 | var c = PI2 || this.createConstant( Math.PI * 2 ); 56 | PI2 = c; 57 | return c; 58 | }; 59 | 60 | AudioIO.prototype.createConstantLN10 = function() { 61 | var c = LN10 || this.createConstant( Math.LN10 ); 62 | LN10 = c; 63 | return c; 64 | }; 65 | 66 | AudioIO.prototype.createConstantLN2 = function() { 67 | var c = LN2 || this.createConstant( Math.LN2 ); 68 | LN2 = c; 69 | return c; 70 | }; 71 | 72 | AudioIO.prototype.createConstantLOG10E = function() { 73 | var c = LOG10E || this.createConstant( Math.LOG10E ); 74 | LOG10E = c; 75 | return c; 76 | }; 77 | 78 | AudioIO.prototype.createConstantLOG2E = function() { 79 | var c = LOG2E || this.createConstant( Math.LOG2E ); 80 | LOG2E = c; 81 | return c; 82 | }; 83 | 84 | AudioIO.prototype.createConstantSQRT1_2 = function() { 85 | var c = SQRT1_2 || this.createConstant( Math.SQRT1_2 ); 86 | SQRT1_2 = c; 87 | return c; 88 | }; 89 | 90 | AudioIO.prototype.createConstantSQRT2 = function() { 91 | var c = SQRT2 || this.createConstant( Math.SQRT2 ); 92 | SQRT2 = c; 93 | return c; 94 | }; 95 | }()); -------------------------------------------------------------------------------- /tests/node-sketches/parabolicSaturation.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 79 | 80 | -------------------------------------------------------------------------------- /tests/jasmine/spec/Sin.js: -------------------------------------------------------------------------------- 1 | describe( "Math / Sine", function() { 2 | var node = io.createSin( 1 ); 3 | 4 | 5 | it( 'should have a context', function() { 6 | expect( node.context ).toEqual( io.context ); 7 | } ); 8 | 9 | it( "should have a reference to it's AudioIO instance", function() { 10 | expect( node.io ).toEqual( io ); 11 | } ); 12 | 13 | it( "should have a connect method", function() { 14 | expect( node.connect ).toEqual( jasmine.any( Function ) ); 15 | } ); 16 | 17 | it( "should have a disconnect method", function() { 18 | expect( node.disconnect ).toEqual( jasmine.any( Function ) ); 19 | } ); 20 | 21 | 22 | 23 | it( 'should have one input and one output', function() { 24 | expect( node.inputs ).toBeDefined(); 25 | expect( node.outputs ).toBeDefined(); 26 | 27 | expect( node.inputs.length ).toBeDefined(); 28 | expect( node.outputs.length ).toBeDefined(); 29 | 30 | expect( node.inputs.length ).toEqual( 1 ); 31 | expect( node.outputs.length ).toEqual( 1 ); 32 | } ); 33 | 34 | 35 | it( 'should output sine of audio input', function( done ) { 36 | var _io = new AudioIO( new OfflineAudioContext( 1, 44100 * 0.01, 44100 ) ), 37 | _node, 38 | input, 39 | val = -Math.PI + Math.random() * ( Math.PI * 2 ), 40 | expected = Math.sin( val ); 41 | 42 | input = _io.createConstant( val ); 43 | _node = _io.createSin(); 44 | 45 | input.connect( _node ); 46 | _node.connect( _io.master ); 47 | 48 | _io.context.oncomplete = function( e ) { 49 | var buffer = e.renderedBuffer.getChannelData( 0 ); 50 | 51 | for ( var i = 0; i < buffer.length; i++ ) { 52 | expect( buffer[ i ] ).toBeCloseTo( expected ); 53 | } 54 | 55 | done(); 56 | }; 57 | 58 | _io.context.startRendering(); 59 | } ); 60 | 61 | it( 'should output sine( value )', function( done ) { 62 | var _io = new AudioIO( new OfflineAudioContext( 1, 44100 * 0.01, 44100 ) ), 63 | _node, 64 | val = -Math.PI + Math.random() * ( Math.PI * 2 ), 65 | expected = Math.sin( val ); 66 | 67 | _node = _io.createSin( val ); 68 | 69 | _node.connect( _io.master ); 70 | 71 | _io.context.oncomplete = function( e ) { 72 | var buffer = e.renderedBuffer.getChannelData( 0 ); 73 | 74 | for ( var i = 0; i < buffer.length; i++ ) { 75 | expect( buffer[ i ] ).toBeCloseTo( expected ); 76 | } 77 | 78 | done(); 79 | }; 80 | 81 | _io.context.startRendering(); 82 | } ); 83 | } ); -------------------------------------------------------------------------------- /tests/jasmine/spec/Tan.js: -------------------------------------------------------------------------------- 1 | describe( "Math / Tan", function() { 2 | var node = io.createTan( 1 ); 3 | 4 | 5 | it( 'should have a context', function() { 6 | expect( node.context ).toEqual( io.context ); 7 | } ); 8 | 9 | it( "should have a reference to it's AudioIO instance", function() { 10 | expect( node.io ).toEqual( io ); 11 | } ); 12 | 13 | it( "should have a connect method", function() { 14 | expect( node.connect ).toEqual( jasmine.any( Function ) ); 15 | } ); 16 | 17 | it( "should have a disconnect method", function() { 18 | expect( node.disconnect ).toEqual( jasmine.any( Function ) ); 19 | } ); 20 | 21 | 22 | 23 | it( 'should have one input and one output', function() { 24 | expect( node.inputs ).toBeDefined(); 25 | expect( node.outputs ).toBeDefined(); 26 | 27 | expect( node.inputs.length ).toBeDefined(); 28 | expect( node.outputs.length ).toBeDefined(); 29 | 30 | expect( node.inputs.length ).toEqual( 1 ); 31 | expect( node.outputs.length ).toEqual( 1 ); 32 | } ); 33 | 34 | 35 | it( 'should output tangent of audio input', function( done ) { 36 | var _io = new AudioIO( new OfflineAudioContext( 1, 44100 * 0.01, 44100 ) ), 37 | _node, 38 | input, 39 | val = -Math.PI + Math.random() * ( Math.PI * 2 ), 40 | expected = Math.tan( val ); 41 | 42 | input = _io.createConstant( val ); 43 | _node = _io.createTan(); 44 | 45 | input.connect( _node ); 46 | _node.connect( _io.master ); 47 | 48 | _io.context.oncomplete = function( e ) { 49 | var buffer = e.renderedBuffer.getChannelData( 0 ); 50 | 51 | for ( var i = 0; i < buffer.length; i++ ) { 52 | expect( buffer[ i ] ).toBeCloseTo( expected, 0 ); 53 | } 54 | 55 | done(); 56 | }; 57 | 58 | _io.context.startRendering(); 59 | } ); 60 | 61 | it( 'should output tan( value )', function( done ) { 62 | var _io = new AudioIO( new OfflineAudioContext( 1, 44100 * 0.01, 44100 ) ), 63 | _node, 64 | val = -Math.PI + Math.random() * ( Math.PI * 2 ), 65 | expected = Math.tan( val ); 66 | 67 | _node = _io.createTan( val ); 68 | _node.connect( _io.master ); 69 | 70 | _io.context.oncomplete = function( e ) { 71 | var buffer = e.renderedBuffer.getChannelData( 0 ); 72 | 73 | for ( var i = 0; i < buffer.length; i++ ) { 74 | expect( buffer[ i ] ).toBeCloseTo( expected, 0 ); 75 | } 76 | 77 | done(); 78 | }; 79 | 80 | _io.context.startRendering(); 81 | } ); 82 | } ); -------------------------------------------------------------------------------- /tests/jasmine/spec/NOT.js: -------------------------------------------------------------------------------- 1 | describe( "Math / NOT", function() { 2 | var node = io.createNOT(); 3 | 4 | 5 | it( 'should have a context', function() { 6 | expect( node.context ).toEqual( io.context ); 7 | } ); 8 | 9 | it( "should have a reference to it's AudioIO instance", function() { 10 | expect( node.io ).toEqual( io ); 11 | } ); 12 | 13 | it( "should have a connect method", function() { 14 | expect( node.connect ).toEqual( jasmine.any( Function ) ); 15 | } ); 16 | 17 | it( "should have a disconnect method", function() { 18 | expect( node.disconnect ).toEqual( jasmine.any( Function ) ); 19 | } ); 20 | 21 | 22 | it( 'should have a cleanUp method and mark items for GC.', function() { 23 | var n = io.createNOT(); 24 | expect( n.cleanUp ).toEqual( jasmine.any( Function ) ); 25 | 26 | n.cleanUp(); 27 | 28 | expect( n.inputs ).toEqual( null ); 29 | expect( n.outputs ).toEqual( null ); 30 | } ); 31 | 32 | 33 | it( 'should have one input and one output', function() { 34 | expect( node.inputs ).toBeDefined(); 35 | expect( node.outputs ).toBeDefined(); 36 | 37 | expect( node.inputs.length ).toBeDefined(); 38 | expect( node.outputs.length ).toBeDefined(); 39 | 40 | expect( node.inputs.length ).toEqual( 1 ); 41 | expect( node.outputs.length ).toEqual( 1 ); 42 | } ); 43 | 44 | it( 'should emulate NOT (!) operation (!0 = 1)', function( done ) { 45 | var aValue = 0; 46 | 47 | offlineAudioTest( { 48 | onSetup: function( io ) { 49 | var a = io.createConstant( aValue ), 50 | node = io.createNOT(); 51 | 52 | a.connect( node, 0, 0 ); 53 | node.connect( io.master ); 54 | }, 55 | onCompare: function( value ) { 56 | expect( value ).toEqual( Number( !aValue ) ); 57 | }, 58 | onComplete: function() { 59 | done(); 60 | } 61 | } ); 62 | } ); 63 | 64 | it( 'should emulate NOT (!) operation (!1 = 0)', function( done ) { 65 | var aValue = 1; 66 | 67 | offlineAudioTest( { 68 | onSetup: function( io ) { 69 | var a = io.createConstant( aValue ), 70 | node = io.createNOT(); 71 | 72 | a.connect( node, 0, 0 ); 73 | node.connect( io.master ); 74 | }, 75 | onCompare: function( value ) { 76 | expect( value ).toEqual( Number( !aValue ) ); 77 | }, 78 | onComplete: function() { 79 | done(); 80 | } 81 | } ); 82 | } ); 83 | 84 | 85 | } ); -------------------------------------------------------------------------------- /src/math/trigonometry/Cos.es6: -------------------------------------------------------------------------------- 1 | import "../../core/AudioIO.es6"; 2 | import Node from "../../core/Node.es6"; 3 | 4 | // Cosine approximation! 5 | // 6 | // Only works in range of -Math.PI to Math.PI. 7 | class Cos extends Node { 8 | 9 | /** 10 | * @param {Object} io Instance of AudioIO. 11 | */ 12 | constructor( io, value ) { 13 | super( io, 0, 1 ); 14 | 15 | var graph = this.getGraph(); 16 | 17 | this.inputs[ 0 ] = this.io.createParam( value ); 18 | 19 | graph.square = this.io.createSquare(); 20 | graph.multiply1 = this.io.createMultiply( -2.605e-7 ); 21 | graph.multiply2 = this.io.createMultiply(); 22 | graph.multiply3 = this.io.createMultiply(); 23 | graph.multiply4 = this.io.createMultiply(); 24 | graph.multiply5 = this.io.createMultiply(); 25 | 26 | graph.add1 = this.io.createAdd( 2.47609e-5 ); 27 | graph.add2 = this.io.createAdd( -0.00138884 ); 28 | graph.add3 = this.io.createAdd( 0.0416666 ); 29 | graph.add4 = this.io.createAdd( -0.499923 ); 30 | graph.add5 = this.io.createAdd( 1 ); 31 | 32 | this.inputs[ 0 ].connect( graph.square ); 33 | 34 | // Connect multiply1's inputs 35 | graph.square.connect( graph.multiply1, 0, 0 ); 36 | 37 | // Connect add1's inputs 38 | graph.multiply1.connect( graph.add1, 0, 0 ); 39 | 40 | // Connect up multiply2's inputs 41 | graph.square.connect( graph.multiply2, 0, 0 ); 42 | graph.add1.connect( graph.multiply2, 0, 1 ); 43 | 44 | // Connect up add2's inputs 45 | graph.multiply2.connect( graph.add2, 0, 0 ); 46 | 47 | // Connect up multiply3's inputs 48 | graph.square.connect( graph.multiply3, 0, 0 ); 49 | graph.add2.connect( graph.multiply3, 0, 1 ); 50 | 51 | // Connect add3's inputs 52 | graph.multiply3.connect( graph.add3, 0, 0 ); 53 | 54 | // Connect multiply4's inputs 55 | graph.square.connect( graph.multiply4, 0, 0 ); 56 | graph.add3.connect( graph.multiply4, 0, 1 ); 57 | 58 | // add4's inputs 59 | graph.multiply4.connect( graph.add4, 0, 0 ); 60 | 61 | // multiply5's inputs 62 | graph.square.connect( graph.multiply5, 0, 0 ); 63 | graph.add4.connect( graph.multiply5, 0, 1 ); 64 | 65 | // add5's inputs 66 | graph.multiply5.connect( graph.add5, 0, 0 ); 67 | 68 | // Output (finally!!) 69 | graph.add5.connect( this.outputs[ 0 ] ); 70 | 71 | // Store controllable params. 72 | this.controls.value = this.inputs[ 0 ]; 73 | 74 | this.setGraph( graph ); 75 | } 76 | } 77 | 78 | AudioIO.prototype.createCos = function( value ) { 79 | return new Cos( this, value ); 80 | }; -------------------------------------------------------------------------------- /src/oscillators/FMOscillator.es6: -------------------------------------------------------------------------------- 1 | import "../core/AudioIO.es6"; 2 | import OscillatorBank from "../oscillators/OscillatorBank.es6"; 3 | 4 | class FMOscillator extends OscillatorBank { 5 | 6 | constructor( io ) { 7 | super( io ); 8 | 9 | var graph = this.getGraph( this ); 10 | 11 | // FM/modulator oscillator setup 12 | graph.fmOscillator = this.io.createOscillatorBank(); 13 | graph.fmOscAmount = this.context.createGain(); 14 | graph.fmOscAmountMultiplier = this.io.createMultiply(); 15 | graph.fmOscAmount.gain.value = 0; 16 | graph.fmOscillator.connect( graph.fmOscAmount ); 17 | graph.fmOscAmount.connect( graph.fmOscAmountMultiplier, 0, 0 ); 18 | 19 | this.controls.fmFrequency = this.io.createParam(); 20 | this.controls.fmFrequency.connect( graph.fmOscillator.controls.frequency ); 21 | this.controls.fmFrequency.connect( graph.fmOscAmountMultiplier, 0, 1 ); 22 | 23 | this.controls.fmWaveform = this.io.createParam(); 24 | this.controls.fmWaveform.connect( graph.fmOscillator.controls.waveform ); 25 | 26 | this.controls.fmOscAmount = this.io.createParam(); 27 | this.controls.fmOscAmount.connect( graph.fmOscAmount.gain ); 28 | 29 | 30 | // Self-fm setup 31 | graph.fmSelfAmounts = []; 32 | graph.fmSelfAmountMultipliers = []; 33 | this.controls.fmSelfAmount = this.io.createParam(); 34 | 35 | for( var i = 0; i < graph.oscillators.length; ++i ) { 36 | // Connect FM oscillator to the existing oscillators 37 | // frequency control. 38 | graph.fmOscAmountMultiplier.connect( graph.oscillators[ i ].frequency ); 39 | 40 | 41 | // For each oscillator in the oscillator bank, 42 | // create a FM-self GainNode, and connect the osc 43 | // to it, then it to the osc's frequency. 44 | graph.fmSelfAmounts[ i ] = this.context.createGain(); 45 | graph.fmSelfAmounts[ i ].gain.value = 0; 46 | 47 | graph.fmSelfAmountMultipliers[ i ] = this.io.createMultiply(); 48 | graph.fmSelfAmounts[ i ].connect( graph.fmSelfAmountMultipliers[ i ], 0, 0 ); 49 | this.controls.frequency.connect( graph.fmSelfAmountMultipliers[ i ], 0, 1 ); 50 | 51 | graph.oscillators[ i ].connect( graph.fmSelfAmounts[ i ] ); 52 | graph.fmSelfAmountMultipliers[ i ].connect( graph.oscillators[ i ].frequency ); 53 | 54 | // Make sure the FM-self amount is controllable with one parameter. 55 | this.controls.fmSelfAmount.connect( graph.fmSelfAmounts[ i ].gain ); 56 | } 57 | 58 | this.setGraph( graph ); 59 | } 60 | } 61 | 62 | AudioIO.prototype.createFMOscillator = function() { 63 | return new FMOscillator( this ); 64 | }; -------------------------------------------------------------------------------- /src/math/Sqrt.es6: -------------------------------------------------------------------------------- 1 | import "../core/AudioIO.es6"; 2 | import Node from "../core/Node.es6"; 3 | 4 | // https://en.wikipedia.org/wiki/Methods_of_computing_square_roots#Example 5 | // 6 | // for( var i = 0, x; i < sigFigures; ++i ) { 7 | // if( i === 0 ) { 8 | // x = sigFigures * Math.pow( 10, 2 ); 9 | // } 10 | // else { 11 | // x = 0.5 * ( x + (input / x) ); 12 | // } 13 | // } 14 | 15 | // TODO: 16 | // - Make sure Sqrt uses getGraph and setGraph. 17 | class SqrtHelper { 18 | constructor( io, previousStep, input, maxInput ) { 19 | this.multiply = io.createMultiply( 0.5 ); 20 | this.divide = io.createDivide( null, maxInput ); 21 | this.add = io.createAdd(); 22 | 23 | // input / x; 24 | input.connect( this.divide, 0, 0 ); 25 | previousStep.output.connect( this.divide, 0, 1 ); 26 | 27 | // x + ( input / x ) 28 | previousStep.output.connect( this.add, 0, 0 ); 29 | this.divide.connect( this.add, 0, 1 ); 30 | 31 | // 0.5 * ( x + ( input / x ) ) 32 | this.add.connect( this.multiply ); 33 | 34 | this.output = this.multiply; 35 | } 36 | 37 | cleanUp() { 38 | this.multiply.cleanUp(); 39 | this.divide.cleanUp(); 40 | this.add.cleanUp(); 41 | 42 | this.multiply = null; 43 | this.divide = null; 44 | this.add = null; 45 | this.output = null; 46 | } 47 | } 48 | 49 | class Sqrt extends Node { 50 | constructor( io, significantFigures, maxInput ) { 51 | super( io, 1, 1 ); 52 | 53 | // Default to 6 significant figures. 54 | significantFigures = significantFigures || 6; 55 | 56 | maxInput = maxInput || 100; 57 | 58 | this.x0 = this.io.createConstant( significantFigures * Math.pow( 10, 2 ) ); 59 | 60 | this.steps = [ { 61 | output: this.x0 62 | } ]; 63 | 64 | for ( var i = 1; i < significantFigures; ++i ) { 65 | this.steps.push( 66 | new SqrtHelper( this.io, this.steps[ i - 1 ], this.inputs[ 0 ], maxInput ) 67 | ); 68 | } 69 | 70 | this.steps[ this.steps.length - 1 ].output.connect( this.outputs[ 0 ] ); 71 | } 72 | 73 | // cleanUp() { 74 | // super(); 75 | 76 | // this.x0.cleanUp(); 77 | 78 | // this.steps[ 0 ] = null; 79 | 80 | // for( var i = this.steps.length - 1; i >= 1; --i ) { 81 | // this.steps[ i ].cleanUp(); 82 | // this.steps[ i ] = null; 83 | // } 84 | 85 | // this.x0 = null; 86 | // this.steps = null; 87 | // } 88 | } 89 | 90 | 91 | AudioIO.prototype.createSqrt = function( significantFigures, maxInput ) { 92 | return new Sqrt( this, significantFigures, maxInput ); 93 | }; -------------------------------------------------------------------------------- /src/main.es6: -------------------------------------------------------------------------------- 1 | window.AudioContext = window.AudioContext || window.webkitAudioContext; 2 | 3 | import AudioIO from './core/AudioIO.es6'; 4 | 5 | import Node from './core/Node.es6'; 6 | import Param from './core/Param.es6'; 7 | import './core/WaveShaper.es6'; 8 | 9 | 10 | // import './graphs/Crossfader.es6'; 11 | 12 | import './fx/Delay.es6'; 13 | import './fx/PingPongDelay.es6'; 14 | import './fx/SineShaper.es6'; 15 | import './fx/StereoWidth.es6'; 16 | import './fx/StereoRotation.es6'; 17 | // import './fx/BitReduction.es6'; 18 | import './fx/SchroederAllPass.es6'; 19 | 20 | import './generators/OscillatorGenerator.es6'; 21 | import './instruments/GeneratorPlayer.es6'; 22 | 23 | 24 | import './math/trigonometry/DegToRad.es6'; 25 | import './math/trigonometry/Sin.es6'; 26 | import './math/trigonometry/Cos.es6'; 27 | import './math/trigonometry/Tan.es6'; 28 | import './math/trigonometry/RadToDeg.es6'; 29 | 30 | 31 | import './math/relational-operators/EqualTo.es6'; 32 | import './math/relational-operators/EqualToZero.es6'; 33 | import './math/relational-operators/GreaterThan.es6'; 34 | import './math/relational-operators/GreaterThanZero.es6'; 35 | import './math/relational-operators/IfElse.es6'; 36 | import './math/relational-operators/LessThan.es6'; 37 | import './math/relational-operators/LessThanZero.es6'; 38 | 39 | import './math/logical-operators/LogicalOperator.es6'; 40 | import './math/logical-operators/AND.es6'; 41 | import './math/logical-operators/OR.es6'; 42 | import './math/logical-operators/NOT.es6'; 43 | 44 | import './math/Abs.es6'; 45 | import './math/Add.es6'; 46 | import './math/Average.es6'; 47 | import './math/Clamp.es6'; 48 | import './math/Constant.es6'; 49 | import './math/Divide.es6'; 50 | import './math/Floor.es6'; 51 | import './math/Max.es6'; 52 | import './math/Min.es6'; 53 | import './math/Multiply.es6'; 54 | import './math/Negate.es6'; 55 | import './math/Pow.es6'; 56 | import './math/Reciprocal.es6'; 57 | import './math/Round.es6'; 58 | import './math/Scale.es6'; 59 | import './math/ScaleExp.es6'; 60 | import './math/Sign.es6'; 61 | import './math/Sqrt.es6'; 62 | import './math/Subtract.es6'; 63 | import './math/Switch.es6'; 64 | import './math/Square.es6'; 65 | 66 | import './math/Lerp.es6'; 67 | import './math/SampleDelay.es6'; 68 | 69 | import './envelopes/CustomEnvelope.es6'; 70 | import './envelopes/ASDREnvelope.es6'; 71 | 72 | import './graphs/EQShelf.es6'; 73 | // import './graphs/DiffuseDelay.es6'; 74 | import './graphs/Counter.es6'; 75 | import './graphs/DryWetNode.es6'; 76 | import './graphs/PhaseOffset.es6'; 77 | import './graphs/Crossfader.es6'; 78 | 79 | 80 | import './oscillators/OscillatorBank.es6'; 81 | import './oscillators/NoiseOscillator.es6'; 82 | import './oscillators/FMOscillator.es6'; 83 | import './oscillators/SineBank.es6'; 84 | 85 | // import './graphs/Sketch.es6'; 86 | 87 | window.Param = Param; 88 | window.Node = Node; -------------------------------------------------------------------------------- /src/math/trigonometry/Sin.es6: -------------------------------------------------------------------------------- 1 | import "../../core/AudioIO.es6"; 2 | import Node from "../../core/Node.es6"; 3 | 4 | // Sin approximation! 5 | // 6 | // Only works in range of -Math.PI to Math.PI. 7 | class Sin extends Node { 8 | 9 | /** 10 | * @param {Object} io Instance of AudioIO. 11 | */ 12 | constructor( io, value ) { 13 | super( io, 0, 1 ); 14 | 15 | var graph = this.getGraph(); 16 | 17 | this.inputs[ 0 ] = this.controls.value = this.io.createParam( value ); 18 | 19 | graph.square = this.io.createSquare(); 20 | graph.multiply1 = this.io.createMultiply( -2.39e-8 ); 21 | graph.multiply2 = this.io.createMultiply(); 22 | graph.multiply3 = this.io.createMultiply(); 23 | graph.multiply4 = this.io.createMultiply(); 24 | graph.multiply5 = this.io.createMultiply(); 25 | graph.multiply6 = this.io.createMultiply(); 26 | 27 | graph.add1 = this.io.createAdd( 2.7526e-6 ); 28 | graph.add2 = this.io.createAdd( -0.000198409 ); 29 | graph.add3 = this.io.createAdd( 0.00833333 ); 30 | graph.add4 = this.io.createAdd( -0.166667 ); 31 | graph.add5 = this.io.createAdd( 1 ); 32 | 33 | this.inputs[ 0 ].connect( graph.square ); 34 | 35 | // Connect multiply1's inputs 36 | graph.square.connect( graph.multiply1, 0, 0 ); 37 | 38 | // Connect add1's inputs 39 | graph.multiply1.connect( graph.add1, 0, 0 ); 40 | 41 | // Connect up multiply2's inputs 42 | graph.square.connect( graph.multiply2, 0, 0 ); 43 | graph.add1.connect( graph.multiply2, 0, 1 ); 44 | 45 | // Connect up add2's inputs 46 | graph.multiply2.connect( graph.add2, 0, 0 ); 47 | 48 | // Connect up multiply3's inputs 49 | graph.square.connect( graph.multiply3, 0, 0 ); 50 | graph.add2.connect( graph.multiply3, 0, 1 ); 51 | 52 | // Connect add3's inputs 53 | graph.multiply3.connect( graph.add3, 0, 0 ); 54 | 55 | // Connect multiply4's inputs 56 | graph.square.connect( graph.multiply4, 0, 0 ); 57 | graph.add3.connect( graph.multiply4, 0, 1 ); 58 | 59 | // add4's inputs 60 | graph.multiply4.connect( graph.add4, 0, 0 ); 61 | 62 | // multiply5's inputs 63 | graph.square.connect( graph.multiply5, 0, 0 ); 64 | graph.add4.connect( graph.multiply5, 0, 1 ); 65 | 66 | // add5's inputs 67 | graph.multiply5.connect( graph.add5, 0, 0 ); 68 | 69 | // multiply6's inputs 70 | this.inputs[0].connect( graph.multiply6, 0, 0 ); 71 | graph.add5.connect( graph.multiply6, 0, 1 ); 72 | 73 | // Output (finally!!) 74 | graph.multiply6.connect( this.outputs[ 0 ] ); 75 | 76 | this.setGraph( graph ); 77 | } 78 | } 79 | 80 | AudioIO.prototype.createSin = function( value ) { 81 | return new Sin( this, value ); 82 | }; -------------------------------------------------------------------------------- /tests/node-sketches/StereoWidth.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 75 | 76 | -------------------------------------------------------------------------------- /tests/node-sketches/lerp2.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 79 | 80 | -------------------------------------------------------------------------------- /examples/basic-synth/res/css/keyboard.css: -------------------------------------------------------------------------------- 1 | #keyboard { 2 | position: relative; 3 | top: 0; 4 | left: 0; 5 | width: 100%; 6 | height: 12rem; 7 | background: linear-gradient(to bottom, #1e1e1e 0%, #0f0f0f 50%); 8 | } 9 | #keyboard .keys { 10 | position: absolute; 11 | top: 0.5em; 12 | left: 0; 13 | right: 0; 14 | bottom: 1em; 15 | width: 100%; 16 | max-width: 45rem; 17 | margin: 0 auto; 18 | -webkit-transform-style: preserve-3d; 19 | transform-style: preserve-3d; 20 | } 21 | #keyboard .keys > .key { 22 | position: relative; 23 | float: left; 24 | z-index: 1; 25 | -webkit-transform-origin: 50% 0%; 26 | transform-origin: 50% 0%; 27 | text-align: center; 28 | display: inline-block; 29 | height: 100%; 30 | cursor: pointer; 31 | } 32 | #keyboard .keys > .key > span { 33 | position: absolute; 34 | bottom: 1.25em; 35 | left: 0; 36 | width: 100%; 37 | text-align: center; 38 | display: inline-block; 39 | pointer-events: none; 40 | -webkit-user-select: none; 41 | -moz-user-select: none; 42 | -ms-user-select: none; 43 | user-select: none; 44 | } 45 | #keyboard .keys > .key.white { 46 | width: 12%; 47 | margin: 0 0.25%; 48 | background: linear-gradient(to bottom, white 0%, #fffadc 60%); 49 | border-bottom-left-radius: 6%; 50 | border-bottom-right-radius: 6%; 51 | box-shadow: inset 0 0.25em 0.5em rgba(0, 0, 0, 0.4), inset 0 -0.15em 0.3em rgba(0, 0, 0, 0.15), 0 0.5em 1em black; 52 | } 53 | #keyboard .keys > .key.white[state="down"] { 54 | -webkit-transform: rotateX(-17deg); 55 | transform: rotateX(-17deg); 56 | } 57 | #keyboard .keys > .key.white:hover { 58 | background: linear-gradient(to bottom, #ebebeb 0%, #d7d7d7 60%); 59 | } 60 | #keyboard .keys > .key.black { 61 | width: 6%; 62 | margin-left: -3%; 63 | height: 70%; 64 | background: linear-gradient(to bottom, #1e1e1e 0%, #0a0a0a 50%); 65 | border-bottom-left-radius: 8%; 66 | border-bottom-right-radius: 8%; 67 | border: 0.5em solid rgba(0, 0, 0, 0.3); 68 | border-top: none; 69 | box-shadow: 0 0 0.5em rgba(0, 0, 0, 0.8); 70 | z-index: 10000; 71 | color: #888; 72 | } 73 | #keyboard .keys > .key.black::after { 74 | content: ''; 75 | position: absolute; 76 | bottom: 0; 77 | left: 0; 78 | right: 0; 79 | height: 15%; 80 | box-shadow: inset 0 5px 5px rgba(255, 255, 255, 0.075); 81 | } 82 | #keyboard .keys > .key.black[state="down"] { 83 | -webkit-transform: scaleX(0.99) scaleY(0.95); 84 | transform: scaleX(0.99) scaleY(0.95); 85 | } 86 | #keyboard .keys > .key.black:hover { 87 | background: linear-gradient(to bottom, #323232 0%, #1e1e1e 50%); 88 | } 89 | #keyboard .keys > .key.black + .white { 90 | margin-left: -3%; 91 | z-index: -1; 92 | } 93 | #keyboard .midi { 94 | position: absolute; 95 | right: 1rem; 96 | bottom: 1rem; 97 | } 98 | 99 | /*# sourceMappingURL=keyboard.css.map */ 100 | -------------------------------------------------------------------------------- /examples/basic-synth/res/css/knob.css.map: -------------------------------------------------------------------------------- 1 | { 2 | "version": 3, 3 | "mappings": "AASA;;;;+BAIgC;EAC5B,aAAa,EAAE,SAAS;;;AAG5B;;;6BAG8B;EAC1B,QAAQ,EAAE,QAAQ;EAClB,GAAG,EAAE,CAAC;EACN,IAAI,EAAE,CAAC;EACP,KAAK,EAAE,CAAC;EACR,MAAM,EAAE,CAAC;EACT,MAAM,EAAE,IAAI;EACZ,KAAK,EAAE,IAAI;EACX,MAAM,EAAE,IAAI;;;AAGhB,KAAM;EACF,QAAQ,EAAE,QAAQ;EAClB,GAAG,EAAE,CAAC;EACN,IAAI,EAAE,CAAC;EACP,KAAK,EAAE,KAAyC;EAChD,MAAM,EAAE,KAA0C;EAClD,SAAS,EAAE,IAA4B;EACvC,MAAM,EAAE,iBAAiB;EACzB,OAAO,EAAE,YAAY;EACrB,MAAM,EAAE,SAAS;EACjB,WAAW,EAAE,gCAAgC;EAC7C,UAAU,EACN,+EAAqC;;AAGzC,WAAQ;EACJ,cAAc,EAAE,eAAe;EAC/B,WAAW,EAAE,eAAe;;AAGhC,YAAS;EACL,cAAc,EAAE,eAAe;EAC/B,WAAW,EAAE,eAAe;;AAGhC,WAAQ;EACJ,KAAK,EAAE,KAAiB;EACxB,MAAM,EAAE,KAAkB;EAC1B,SAAS,EAAE,IAA4B;;AAE3C,YAAS;EACL,KAAK,EAAE,MAA2C;EAClD,MAAM,EAAE,MAA4C;EACpD,SAAS,EAAE,OAA6B;;AAE5C,WAAQ;EACJ,KAAK,EAAE,IAA0C;EACjD,MAAM,EAAE,IAA2C;EACnD,SAAS,EAAE,MAA4B;;AAE3C,UAAO;EACH,KAAK,EAAE,MAAyC;EAChD,MAAM,EAAE,MAA0C;EAClD,SAAS,EAAE,OAA2B;;;AAS1C,gBAAW;EACP,KAAK,EAAE,GAAG;EACV,MAAM,EAAE,GAAG;EACX,cAAc,EAAE,6CAAiD;;AAEjE,uBAAO;EACH,OAAO,EAAE,CAAC;EACV,UAAU,EAAE,OAAO;EACnB,MAAM,EAAE,iBAAiB;EACzB,UAAU,EACN,wHAAqC;;AAQjD,qBAAkB;EACd,cAAc,EAAE,8CAAkD;;AAKlE,8BAAkB;EACd,MAAM,EAAE,mBAAmB;;AAE3B,sCAAU;EACN,OAAO,EAAE,EAAE;EACX,UAAU,EAAE,kFAAoF;;AAGxG,iCAAqB;EACjB,OAAO,EAAE,EAAE;EACX,QAAQ,EAAE,QAAQ;EAClB,GAAG,EAAE,IAAI;EACT,IAAI,EAAE,IAAI;EACV,KAAK,EAAE,IAAI;EACX,MAAM,EAAE,IAAI;EACZ,MAAM,EAAE,mBAAmB;EAC3B,gBAAgB,EAAE,WAAW;EAC7B,OAAO,EAAE,CAAC;;AAGd,sCAA4B;EACxB,YAAY,EAAE,MAAM;EACpB,YAAY,EAAE,GAAG;;AAKzB,4BAAyB;EACrB,YAAY,EAAE,IAAI;EAClB,UAAU,EAAE,OAAO;EACnB,UAAU,EACN,qBAAkC;;AAEtC,oCAAU;EACN,UAAU,EAAE,kFAAoF;;;AAQxG,gBAAW;EACP,QAAQ,EAAE,QAAQ;EAClB,GAAG,EAAE,CAAC;EACN,MAAM,EAAE,CAAC;EACT,IAAI,EAAE,CAAC;EACP,KAAK,EAAE,CAAC;EACR,KAAK,EAAE,GAAG;EACV,MAAM,EAAE,GAAG;EACX,SAAS,EAAE,MAAM;EACjB,MAAM,EAAE,IAAI;EACZ,UAAU,EAAE,MAAM;EAClB,cAAc,EAAE,KAAK;EACrB,KAAK,EAAE,sBAAwB;EAC/B,OAAO,EAAE,CAAC;EACV,mBAAmB,EAAE,IAAI;EACtB,gBAAgB,EAAE,IAAI;EACrB,eAAe,EAAE,IAAI;EACjB,WAAW,EAAE,IAAI;;AAG7B,qBAAkB;EACd,OAAO,EAAE,IAAI;;AAGjB,qBAAkB;EACd,KAAK,EAAE,wBAA0B;;;AASrC,6BAAwB;EACpB,MAAM,EAAE,MAAM;EACd,wBAAwB,EAAE,OAAO;EACzB,gBAAgB,EAAE,OAAO;EACjC,OAAO,EAAE,CAAC;;AAEV,oCAAS;EACL,OAAO,EAAE,EAAE;EACX,QAAQ,EAAE,QAAQ;EAClB,GAAG,EAAE,MAAM;EACX,IAAI,EAAE,CAAC;EACP,KAAK,EAAE,CAAC;EACR,KAAK,EAAE,EAAE;EACT,MAAM,EAAE,IAAI;EACZ,MAAM,EAAE,GAAG;EACX,UAAU,EAAE,sBAAwB;EACpC,UAAU,EAAE,uCAAyC;;AAI7D,yCAAsC;EAClC,UAAU,EAAE,wBAA0B;;AAG1C,yCAAsC;EAClC,KAAK,EAAE,GAAG;EACV,MAAM,EAAE,GAAG;EACX,UAAU,EAAE,uCAAyC;;AAKrD,qDAA+B;EAC3B,KAAK,EAAE,GAAG;EACV,MAAM,EAAE,GAAG;EACX,MAAM,EAAE,GAAG;EACX,aAAa,EAAE,SAAS;;AAG5B,0DAAsC;EAClC,GAAG,EAAE,MAAM;EACX,KAAK,EAAE,GAAG;EACV,MAAM,EAAE,GAAG;EACX,MAAM,EAAE,GAAG;EACX,aAAa,EAAE,SAAS;;AAG5B,0DAAsC;EAClC,UAAU,EAAE,wBAA0B;EACtC,UAAU,EAAE,sCAAwC;;;AAQhE,cAAe;EACX,QAAQ,EAAE,QAAQ;EAClB,GAAG,EAAE,IAAI;EACT,KAAK,EAAE,IAAI;EACX,UAAU,EAAE,MAAM;EAClB,SAAS,EAAE,MAAM;EACjB,cAAc,EAAE,SAAS;EACzB,KAAK,EAAE,OAAO;EACd,WAAW,EAAE,sCAAwC", 4 | "sources": ["../sass/knob.scss"], 5 | "names": [], 6 | "file": "knob.css" 7 | } 8 | -------------------------------------------------------------------------------- /tests/jasmine/spec/Square.js: -------------------------------------------------------------------------------- 1 | describe( "Math / Square", function() { 2 | var node = io.createSquare(); 3 | 4 | 5 | it( 'should have a context', function() { 6 | expect( node.context ).toEqual( io.context ); 7 | } ); 8 | 9 | it( "should have a reference to it's AudioIO instance", function() { 10 | expect( node.io ).toEqual( io ); 11 | } ); 12 | 13 | it( "should have a connect method", function() { 14 | expect( node.connect ).toEqual( jasmine.any( Function ) ); 15 | } ); 16 | 17 | it( "should have a disconnect method", function() { 18 | expect( node.disconnect ).toEqual( jasmine.any( Function ) ); 19 | } ); 20 | 21 | 22 | 23 | it( 'should have one input and one output', function() { 24 | expect( node.inputs ).toBeDefined(); 25 | expect( node.outputs ).toBeDefined(); 26 | 27 | expect( node.inputs.length ).toBeDefined(); 28 | expect( node.outputs.length ).toBeDefined(); 29 | 30 | expect( node.inputs.length ).toEqual( 1 ); 31 | expect( node.outputs.length ).toEqual( 1 ); 32 | } ); 33 | 34 | it( 'should have a cleanUp method and mark items for GC.', function() { 35 | var n = io.createMax( 5 ); 36 | expect( n.cleanUp ).toEqual( jasmine.any( Function ) ); 37 | 38 | n.cleanUp(); 39 | 40 | expect( n.inputs ).toEqual( null ); 41 | expect( n.outputs ).toEqual( null ); 42 | } ); 43 | 44 | 45 | it( 'should square a positive input', function( done ) { 46 | var _io = new AudioIO( new OfflineAudioContext( 1, 44100 * 0.01, 44100 ) ), 47 | _node, 48 | input1, input2; 49 | 50 | input1 = _io.createConstant( 2 ); 51 | _node = _io.createSquare(); 52 | 53 | input1.connect( _node ); 54 | _node.connect( _io.master ); 55 | 56 | _io.context.oncomplete = function( e ) { 57 | var buffer = e.renderedBuffer.getChannelData( 0 ); 58 | 59 | for ( var i = 0; i < buffer.length; i++ ) { 60 | expect( buffer[ i ] ).toEqual( 4 ); 61 | } 62 | 63 | done(); 64 | }; 65 | 66 | _io.context.startRendering(); 67 | } ); 68 | 69 | it( 'should square a negative input', function( done ) { 70 | var _io = new AudioIO( new OfflineAudioContext( 1, 44100 * 0.01, 44100 ) ), 71 | _node, 72 | input1, input2; 73 | 74 | input1 = _io.createConstant( -10 ); 75 | _node = _io.createSquare(); 76 | 77 | input1.connect( _node ); 78 | _node.connect( _io.master ); 79 | 80 | _io.context.oncomplete = function( e ) { 81 | var buffer = e.renderedBuffer.getChannelData( 0 ); 82 | 83 | for ( var i = 0; i < buffer.length; i++ ) { 84 | expect( buffer[ i ] ).toEqual( 100 ); 85 | } 86 | 87 | done(); 88 | }; 89 | 90 | _io.context.startRendering(); 91 | } ); 92 | } ); -------------------------------------------------------------------------------- /tests/node-sketches/ParabolicSaturation2.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 79 | 80 | -------------------------------------------------------------------------------- /tests/node-sketches/StereoRotation.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 76 | 77 | -------------------------------------------------------------------------------- /examples/basic-synth/res/css/main.css: -------------------------------------------------------------------------------- 1 | * { 2 | -webkit-font-smoothing: subpixel-antialiased; 3 | box-sizing: border-box; 4 | } 5 | 6 | html { 7 | font-size: 62.5%; 8 | } 9 | 10 | body { 11 | font-size: 1rem; 12 | line-height: 1.1rem; 13 | font-family: 'Helvetica', 'Arial', sans-serif; 14 | color: #333; 15 | background-color: #ccc; 16 | } 17 | 18 | h2 { 19 | margin: 0 0 0.75em 0; 20 | } 21 | 22 | main { 23 | position: absolute; 24 | top: 0; 25 | left: 0; 26 | right: 0; 27 | bottom: 0; 28 | padding: 1rem; 29 | border-radius: 3px; 30 | border: 4px solid rgba(0, 0, 0, 0.2); 31 | width: 70rem; 32 | height: 42.5rem; 33 | margin: auto; 34 | background: #555; 35 | } 36 | 37 | section { 38 | position: absolute; 39 | padding: 1rem; 40 | background: linear-gradient(to bottom, rgba(79, 79, 79, 0.8) 0%, rgba(41, 41, 41, 0.8) 100%); 41 | box-shadow: inset 0 -2px 4px rgba(0, 0, 0, 0.4), inset 0 1px 1px rgba(255, 255, 255, 0.4); 42 | } 43 | section h2 { 44 | color: #222; 45 | text-shadow: 0 1px 0 rgba(255, 255, 255, 0.15); 46 | } 47 | section > .knob > .label { 48 | color: #999; 49 | text-shadow: 0 -1px 0px black; 50 | } 51 | section#oscillator1, section#oscillator2, section#oscilloscope { 52 | height: 18.3rem; 53 | } 54 | section#oscillator1, section#oscillator2 { 55 | width: 14.2rem; 56 | background: linear-gradient(to bottom, rgba(150, 150, 150, 0.8) 0%, rgba(79, 79, 79, 0.8) 100%); 57 | } 58 | section#oscillator1 h2, section#oscillator2 h2 { 59 | color: #333; 60 | } 61 | section#oscillator1 > .knob > .label, section#oscillator2 > .knob > .label { 62 | color: #bbb; 63 | text-shadow: 0 -1px 0px black; 64 | } 65 | section#oscillator1 { 66 | top: 0; 67 | left: 0; 68 | } 69 | section#oscillator2 { 70 | top: 0; 71 | right: 0; 72 | } 73 | section#oscillator2 h2 { 74 | text-align: right; 75 | } 76 | section#oscilloscope { 77 | top: 0; 78 | left: 0; 79 | right: 0; 80 | width: 40.8rem; 81 | margin: auto; 82 | } 83 | section#oscilloscope canvas { 84 | position: relative; 85 | width: 100%; 86 | height: 100%; 87 | border-radius: 0.4em; 88 | border-bottom: 1px solid rgba(255, 255, 255, 0.1); 89 | } 90 | section#oscilloscope::before { 91 | content: ''; 92 | position: absolute; 93 | top: 0.6em; 94 | left: 0.6em; 95 | right: 0.6em; 96 | bottom: 0.7em; 97 | border-radius: 0.65em; 98 | box-shadow: inset 0 3em 3em rgba(0, 0, 0, 0.2), inset 0 -3em 3em rgba(255, 255, 255, 0.11); 99 | } 100 | section#envelope, section#delay, section#master { 101 | height: 11.4rem; 102 | } 103 | section#envelope { 104 | bottom: 12rem; 105 | left: 0; 106 | width: 30rem; 107 | } 108 | section#delay { 109 | bottom: 12rem; 110 | right: 14.2rem; 111 | width: 24.9rem; 112 | } 113 | section#master { 114 | bottom: 12rem; 115 | right: 0; 116 | width: 14.3rem; 117 | } 118 | 119 | footer { 120 | position: absolute; 121 | bottom: 0; 122 | left: 0; 123 | width: 100%; 124 | } 125 | 126 | /*# sourceMappingURL=main.css.map */ 127 | -------------------------------------------------------------------------------- /src/envelopes/ASDREnvelope.es6: -------------------------------------------------------------------------------- 1 | import "../core/AudioIO.es6"; 2 | import CustomEnvelope from "./CustomEnvelope.es6"; 3 | 4 | class ASDREnvelope extends CustomEnvelope { 5 | constructor( io ) { 6 | super( io ); 7 | 8 | this.times = { 9 | attack: 0.01, 10 | decay: 0.5, 11 | release: 0.5 12 | }; 13 | 14 | this.levels = { 15 | initial: 0, 16 | peak: 1, 17 | sustain: 1, 18 | release: 0 19 | }; 20 | 21 | this.addStartStep( 'initial', 0, this.levels.initial ); 22 | this.addStartStep( 'attack', this.times.attack, this.levels.peak ); 23 | this.addStartStep( 'decay', this.times.decay, this.levels.sustain ); 24 | this.addEndStep( 'release', this.times.release, this.levels.release, true ); 25 | } 26 | 27 | get attackTime() { 28 | return this.times.attack; 29 | } 30 | set attackTime( time ) { 31 | if ( typeof time === 'number' ) { 32 | this.times.attack = time; 33 | this.setStepTime( 'attack', time ); 34 | } 35 | } 36 | 37 | 38 | get decayTime() { 39 | return this.times.decay; 40 | } 41 | set decayTime( time ) { 42 | if ( typeof time === 'number' ) { 43 | this.times.decay = time; 44 | this.setStepTime( 'decay', time ); 45 | } 46 | } 47 | 48 | 49 | get releaseTime() { 50 | return this.times.release; 51 | } 52 | set releaseTime( time ) { 53 | if ( typeof time === 'number' ) { 54 | this.times.release = time; 55 | this.setStepTime( 'release', time ); 56 | } 57 | } 58 | 59 | 60 | get initialLevel() { 61 | return this.levels.initial; 62 | } 63 | set initialLevel( level ) { 64 | if ( typeof level === 'number' ) { 65 | this.levels.initial = level; 66 | this.setStepLevel( 'initial', level ); 67 | } 68 | } 69 | 70 | 71 | get peakLevel() { 72 | return this.levels.peak; 73 | } 74 | 75 | set peakLevel( level ) { 76 | if ( typeof level === 'number' ) { 77 | this.levels.peak = level; 78 | this.setStepLevel( 'attack', level ); 79 | } 80 | } 81 | 82 | 83 | get sustainLevel() { 84 | return this.levels.sustain; 85 | } 86 | set sustainLevel( level ) { 87 | if ( typeof level === 'number' ) { 88 | this.levels.sustain = level; 89 | this.setStepLevel( 'decay', level ); 90 | } 91 | } 92 | 93 | 94 | get releaseLevel() { 95 | return this.levels.release; 96 | } 97 | set releaseLevel( level ) { 98 | if ( typeof level === 'number' ) { 99 | this.levels.release = level; 100 | this.setStepLevel( 'release', level ); 101 | } 102 | } 103 | } 104 | 105 | AudioIO.prototype.createASDREnvelope = function() { 106 | return new ASDREnvelope( this ); 107 | }; 108 | 109 | export default ASDREnvelope; -------------------------------------------------------------------------------- /src/fx/StereoRotation.es6: -------------------------------------------------------------------------------- 1 | import AudioIO from "../core/AudioIO.es6"; 2 | import Node from "../core/Node.es6"; 3 | 4 | 5 | // Based on the following formula from Michael Gruhn: 6 | // - http://musicdsp.org/showArchiveComment.php?ArchiveID=255 7 | // 8 | // ------------------------------------------------------------ 9 | // 10 | // Calculate transformation matrix's coefficients 11 | // cos_coef = cos(angle); 12 | // sin_coef = sin(angle); 13 | 14 | // Do this per sample 15 | // out_left = in_left * cos_coef - in_right * sin_coef; 16 | // out_right = in_left * sin_coef + in_right * cos_coef; 17 | class StereoRotation extends Node { 18 | constructor( io, rotation ) { 19 | super( io, 1, 1 ); 20 | 21 | var graph = this.getGraph(); 22 | 23 | this.controls.rotation = this.io.createParam( rotation ); 24 | 25 | graph.splitter = this.context.createChannelSplitter( 2 ); 26 | graph.cos = this.io.createCos(); 27 | graph.sin = this.io.createSin(); 28 | 29 | this.controls.rotation.connect( graph.cos ); 30 | this.controls.rotation.connect( graph.sin ); 31 | 32 | graph.leftMultiplyCos = this.io.createMultiply(); 33 | graph.leftMultiplySin = this.io.createMultiply(); 34 | graph.rightMultiplyCos = this.io.createMultiply(); 35 | graph.rightMultiplySin = this.io.createMultiply(); 36 | graph.leftCosMinusRightSin = this.io.createSubtract(); 37 | graph.leftSinAddRightCos = this.io.createAdd(); 38 | 39 | 40 | 41 | graph.inputLeft = this.context.createGain(); 42 | graph.inputRight = this.context.createGain(); 43 | graph.merger = this.context.createChannelMerger( 2 ); 44 | 45 | graph.splitter.connect( graph.inputLeft, 0 ); 46 | graph.splitter.connect( graph.inputRight, 1 ); 47 | 48 | graph.inputLeft.connect( graph.leftMultiplyCos, 0, 0 ); 49 | graph.cos.connect( graph.leftMultiplyCos, 0, 1 ); 50 | graph.inputLeft.connect( graph.leftMultiplySin, 0, 0 ); 51 | graph.sin.connect( graph.leftMultiplySin, 0, 1); 52 | 53 | graph.inputRight.connect( graph.rightMultiplySin, 0, 0 ); 54 | graph.sin.connect( graph.rightMultiplySin, 0, 1 ); 55 | graph.inputRight.connect( graph.rightMultiplyCos, 0, 0 ); 56 | graph.cos.connect( graph.rightMultiplyCos, 0, 1 ); 57 | 58 | graph.leftMultiplyCos.connect( graph.leftCosMinusRightSin, 0, 0 ); 59 | graph.rightMultiplySin.connect( graph.leftCosMinusRightSin, 0, 1 ); 60 | graph.leftMultiplySin.connect( graph.leftSinAddRightCos, 0, 0 ); 61 | graph.rightMultiplyCos.connect( graph.leftSinAddRightCos, 0, 1 ); 62 | 63 | graph.leftCosMinusRightSin.connect( graph.merger, 0, 0 ); 64 | graph.leftSinAddRightCos.connect( graph.merger, 0, 1 ); 65 | 66 | 67 | this.inputs[ 0 ].connect( graph.splitter ); 68 | graph.merger.connect( this.outputs[ 0 ] ); 69 | 70 | this.namedInputs.left = graph.inputLeft; 71 | this.namedInputs.right = graph.inputRight; 72 | 73 | this.setGraph( graph ); 74 | } 75 | } 76 | 77 | AudioIO.prototype.createStereoRotation = function( rotation ) { 78 | return new StereoRotation( this, rotation ); 79 | }; -------------------------------------------------------------------------------- /tests/node-sketches/Sketch.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 |

Cutoff: hz

11 | 12 | 13 |

Resonance:

14 | 15 | 16 | 86 | 87 | -------------------------------------------------------------------------------- /src/core/AudioIO.es6: -------------------------------------------------------------------------------- 1 | import CONFIG from './config.es6'; 2 | import './overrides.es6'; 3 | import signalCurves from './signalCurves.es6'; 4 | import conversions from '../mixins/conversions.es6'; 5 | import math from '../mixins/math.es6'; 6 | 7 | class AudioIO { 8 | 9 | static mixin( target, source ) { 10 | for ( let i in source ) { 11 | if ( source.hasOwnProperty( i ) ) { 12 | target[ i ] = source[ i ]; 13 | } 14 | } 15 | } 16 | 17 | static mixinSingle( target, source, name ) { 18 | target[ name ] = source; 19 | } 20 | 21 | 22 | constructor( context = new AudioContext() ) { 23 | this._context = context; 24 | 25 | this.master = this._context.destination; 26 | 27 | // Create an always-on 'driver' node that constantly outputs a value 28 | // of 1. 29 | // 30 | // It's used by a fair few nodes, so makes sense to use the same 31 | // driver, rather than spamming a bunch of WaveShaperNodes all about 32 | // the place. It can't be deleted, so no worries about breaking 33 | // functionality of nodes that do use it should it attempt to be 34 | // overwritten. 35 | Object.defineProperty( this, 'constantDriver', { 36 | writeable: false, 37 | get: ( function() { 38 | let constantDriver; 39 | 40 | return function() { 41 | if ( !constantDriver || constantDriver.context !== this.context ) { 42 | constantDriver = null; 43 | 44 | let context = this.context, 45 | buffer = context.createBuffer( 1, 4096, context.sampleRate ), 46 | bufferData = buffer.getChannelData( 0 ), 47 | bufferSource = context.createBufferSource(); 48 | 49 | for ( let i = 0; i < bufferData.length; ++i ) { 50 | bufferData[ i ] = 1.0; 51 | } 52 | 53 | // for( let bufferValue of bufferData ) { 54 | // bufferValue = 1.0; 55 | // } 56 | 57 | bufferSource.buffer = buffer; 58 | bufferSource.loop = true; 59 | bufferSource.start( 0 ); 60 | 61 | constantDriver = bufferSource; 62 | } 63 | 64 | return constantDriver; 65 | } 66 | }() ) 67 | } ); 68 | } 69 | 70 | 71 | 72 | get context() { 73 | return this._context; 74 | } 75 | 76 | set context( context ) { 77 | if ( !( context instanceof AudioContext ) ) { 78 | throw new Error( "Invalid audio context given:" + context ); 79 | } 80 | 81 | this._context = context; 82 | this.master = context.destination; 83 | } 84 | } 85 | 86 | AudioIO.mixinSingle( AudioIO.prototype, signalCurves, 'curves' ); 87 | AudioIO.mixinSingle( AudioIO.prototype, conversions, 'convert' ); 88 | AudioIO.mixinSingle( AudioIO.prototype, math, 'math' ); 89 | 90 | 91 | 92 | window.AudioIO = AudioIO; 93 | export default AudioIO; -------------------------------------------------------------------------------- /examples/basic-synth/res/sass/main.scss: -------------------------------------------------------------------------------- 1 | * { 2 | -webkit-font-smoothing: subpixel-antialiased; 3 | box-sizing: border-box; 4 | } 5 | 6 | html { 7 | font-size: 62.5%; 8 | } 9 | 10 | body { 11 | font-size: 1rem; 12 | line-height: 1.1rem; 13 | font-family: 'Helvetica', 'Arial', sans-serif; 14 | color: #333; 15 | background-color: #ccc; 16 | } 17 | 18 | h2 { 19 | margin: 0 0 0.75em 0; 20 | } 21 | 22 | main { 23 | position: absolute; 24 | top: 0; 25 | left: 0; 26 | right: 0; 27 | bottom: 0; 28 | padding: 1rem; 29 | border-radius: 3px; 30 | border: 4px solid rgba( 0, 0, 0, 0.2 ); 31 | width: 70rem; 32 | height: 42.5rem; 33 | margin: auto; 34 | background: #555; 35 | } 36 | 37 | section { 38 | position: absolute; 39 | padding: 1rem; 40 | background: linear-gradient( to bottom, rgba( 79, 79, 79, 0.8 ) 0%, rgba( 41, 41, 41, 0.8 ) 100% ); 41 | box-shadow: 42 | inset 0 -2px 4px rgba( 0, 0, 0, 0.4 ), 43 | inset 0 1px 1px rgba( 255, 255, 255, 0.4 ); 44 | 45 | h2 { 46 | color: #222; 47 | text-shadow: 0 1px 0 rgba( 255, 255, 255, 0.15 ); 48 | } 49 | 50 | > .knob > .label { 51 | color: #999; 52 | text-shadow: 0 -1px 0px rgba( 0, 0, 0, 1 ); 53 | } 54 | 55 | 56 | 57 | &#oscillator1, 58 | &#oscillator2, 59 | &#oscilloscope { 60 | height: 18.3rem; 61 | } 62 | 63 | &#oscillator1, 64 | &#oscillator2 { 65 | width: 14.2rem; 66 | background: linear-gradient( to bottom, rgba( 150, 150, 150, 0.8 ) 0%, rgba( 79, 79, 79, 0.8 ) 100% ); 67 | 68 | h2 { 69 | color: #333; 70 | } 71 | 72 | > .knob > .label { 73 | color: #bbb; 74 | text-shadow: 0 -1px 0px rgba( 0, 0, 0, 1 ); 75 | } 76 | } 77 | 78 | &#oscillator1 { 79 | top: 0; 80 | left: 0; 81 | } 82 | &#oscillator2 { 83 | top: 0; 84 | right: 0; 85 | 86 | h2 { 87 | text-align: right; 88 | } 89 | } 90 | 91 | &#oscilloscope { 92 | top: 0; 93 | left: 0; 94 | right: 0; 95 | width: 40.8rem; 96 | margin: auto; 97 | 98 | canvas { 99 | position: relative; 100 | width: 100%; 101 | height: 100%; 102 | border-radius: 0.4em; 103 | border-bottom: 1px solid rgba( 255, 255, 255, 0.1 ); 104 | // border: 1px solid rgba( 255, 255, 255, 0.2 ); 105 | // border-top: 1px solid rgba( 255, 255, 255, 0.1 ); 106 | // border-bottom: 1px solid rgba( 255, 255, 255, 0.3 ); 107 | } 108 | 109 | &::before { 110 | content: ''; 111 | position: absolute; 112 | top: 0.6em; 113 | left: 0.6em; 114 | right: 0.6em; 115 | bottom: 0.7em; 116 | border-radius: 0.65em; 117 | box-shadow: 118 | inset 0 3em 3em rgba( 0, 0, 0, 0.2 ), 119 | inset 0 -3em 3em rgba( 255, 255, 255, 0.11 ); 120 | } 121 | } 122 | 123 | &#envelope, 124 | &#delay, 125 | &#master { 126 | height: 11.4rem; 127 | } 128 | 129 | &#envelope { 130 | bottom: 12rem; 131 | left: 0; 132 | width: 30rem; 133 | } 134 | 135 | &#delay { 136 | bottom: 12rem; 137 | right: 14.2rem; 138 | width: 24.9rem; 139 | } 140 | 141 | &#master { 142 | bottom: 12rem; 143 | right: 0; 144 | width: 14.3rem; 145 | } 146 | } 147 | 148 | 149 | 150 | footer { 151 | position: absolute; 152 | bottom: 0; 153 | left: 0; 154 | width: 100%; 155 | } 156 | -------------------------------------------------------------------------------- /src/math/Scale.es6: -------------------------------------------------------------------------------- 1 | import "../core/AudioIO.es6"; 2 | import Node from "../core/Node.es6"; 3 | 4 | // Given an input value and its high and low bounds, scale 5 | // that value to new high and low bounds. 6 | // 7 | // Formula from MaxMSP's Scale object: 8 | // http://www.cycling74.com/forums/topic.php?id=26593 9 | // 10 | // ((input-lowIn) / (highIn-lowIn)) * (highOut-lowOut) + lowOut; 11 | 12 | 13 | // TODO: 14 | // - Add controls! 15 | class Scale extends Node { 16 | constructor( io, lowIn, highIn, lowOut, highOut ) { 17 | super( io, 1, 1 ); 18 | 19 | var graph = this.getGraph(); 20 | 21 | this.controls.lowIn = this.io.createParam( lowIn ); 22 | this.controls.highIn = this.io.createParam( highIn ); 23 | this.controls.lowOut = this.io.createParam( lowOut ); 24 | this.controls.highOut = this.io.createParam( highOut ); 25 | 26 | 27 | // (input-lowIn) 28 | graph.inputMinusLowIn = this.io.createSubtract(); 29 | this.inputs[ 0 ].connect( graph.inputMinusLowIn, 0, 0 ); 30 | this.controls.lowIn.connect( graph.inputMinusLowIn, 0, 1 ); 31 | 32 | // (highIn-lowIn) 33 | graph.highInMinusLowIn = this.io.createSubtract(); 34 | this.controls.highIn.connect( graph.highInMinusLowIn, 0, 0 ); 35 | this.controls.lowIn.connect( graph.highInMinusLowIn, 0, 1 ); 36 | 37 | // ((input-lowIn) / (highIn-lowIn)) 38 | graph.divide = this.io.createDivide(); 39 | graph.inputMinusLowIn.connect( graph.divide, 0, 0 ); 40 | graph.highInMinusLowIn.connect( graph.divide, 0, 1 ); 41 | 42 | // (highOut-lowOut) 43 | graph.highOutMinusLowOut = this.io.createSubtract(); 44 | this.controls.highOut.connect( graph.highOutMinusLowOut, 0, 0 ); 45 | this.controls.lowOut.connect( graph.highOutMinusLowOut, 0, 1 ); 46 | 47 | // ((input-lowIn) / (highIn-lowIn)) * (highOut-lowOut) 48 | graph.multiply = this.io.createMultiply(); 49 | graph.divide.connect( graph.multiply, 0, 0 ); 50 | graph.highOutMinusLowOut.connect( graph.multiply, 0, 1 ); 51 | 52 | // ((input-lowIn) / (highIn-lowIn)) * (highOut-lowOut) + lowOut 53 | graph.add = this.io.createAdd(); 54 | graph.multiply.connect( graph.add, 0, 0 ); 55 | this.controls.lowOut.connect( graph.add, 0, 1 ); 56 | 57 | graph.add.connect( this.outputs[ 0 ] ); 58 | 59 | this.setGraph( graph ); 60 | } 61 | 62 | get lowIn() { 63 | return this.controls.lowIn.value; 64 | } 65 | set lowIn( value ) { 66 | this.controls.lowIn.value = value; 67 | } 68 | 69 | get highIn() { 70 | return this.controls.highIn.value; 71 | } 72 | set highIn( value ) { 73 | this.controls.highIn.value = value; 74 | } 75 | 76 | get lowOut() { 77 | return this.controls.lowOut.value; 78 | } 79 | set lowOut( value ) { 80 | this.controls.lowOut.value = value; 81 | } 82 | 83 | get highOut() { 84 | return this.controls.highOut.value; 85 | } 86 | set highOut( value ) { 87 | this.controls.highOut.value = value; 88 | } 89 | } 90 | 91 | 92 | AudioIO.prototype.createScale = function( lowIn, highIn, lowOut, highOut ) { 93 | return new Scale( this, lowIn, highIn, lowOut, highOut ); 94 | }; -------------------------------------------------------------------------------- /src/fx/StereoWidth.es6: -------------------------------------------------------------------------------- 1 | import AudioIO from "../core/AudioIO.es6"; 2 | import Node from "../core/Node.es6"; 3 | 4 | 5 | // Based on the following formula from Michael Gruhn: 6 | // - http://musicdsp.org/showArchiveComment.php?ArchiveID=256 7 | // 8 | // ------------------------------------------------------------ 9 | // 10 | // The graph that's created is as follows: 11 | // 12 | // |-> L -> leftAddRight( ch0 ) -> | 13 | // |-> R -> leftAddRight( ch1 ) -> | -> multiply( 0.5 ) ------> monoMinusStereo( 0 ) -> merger( 0 ) // outL 14 | // input -> splitter - |-----> monoPlusStereo( 0 ) --> merger( 1 ) // outR 15 | // |-> L -> rightMinusLeft( ch1 ) -> | 16 | // |-> R -> rightMinusLeft( ch0 ) -> | -> multiply( coef ) ---> monoMinusStereo( 1 ) -> merger( 0 ) // outL 17 | // 18 | // 19 | class StereoWidth extends Node { 20 | constructor( io, width ) { 21 | super( io, 1, 1 ); 22 | 23 | var graph = this.getGraph(); 24 | 25 | graph.splitter = this.context.createChannelSplitter( 2 ); 26 | graph.coefficient = this.io.createParam( width ); 27 | graph.coefficientHalf = this.io.createMultiply( 0.5 ); 28 | graph.inputLeft = this.context.createGain(); 29 | graph.inputRight = this.context.createGain(); 30 | graph.leftAddRight = this.io.createAdd(); 31 | graph.rightMinusLeft = this.io.createSubtract(); 32 | graph.multiplyPointFive = this.io.createMultiply( 0.5 ); 33 | graph.multiplyCoefficient = this.io.createMultiply(); 34 | graph.monoMinusStereo = this.io.createSubtract(); 35 | graph.monoPlusStereo = this.io.createAdd(); 36 | graph.merger = this.context.createChannelMerger( 2 ); 37 | 38 | graph.coefficient.connect( graph.coefficientHalf ); 39 | graph.coefficientHalf.connect( graph.multiplyCoefficient, 0, 1 ); 40 | 41 | graph.splitter.connect( graph.inputLeft, 0 ); 42 | graph.splitter.connect( graph.inputRight, 1 ); 43 | graph.inputLeft.connect( graph.leftAddRight, 0, 0 ); 44 | graph.inputRight.connect( graph.leftAddRight, 0, 1 ); 45 | graph.inputLeft.connect( graph.rightMinusLeft, 0, 1 ); 46 | graph.inputRight.connect( graph.rightMinusLeft, 0, 0 ); 47 | 48 | graph.leftAddRight.connect( graph.multiplyPointFive ); 49 | graph.rightMinusLeft.connect( graph.multiplyCoefficient, 0, 0 ); 50 | 51 | graph.multiplyPointFive.connect( graph.monoMinusStereo, 0, 0 ); 52 | graph.multiplyCoefficient.connect( graph.monoMinusStereo, 0, 1 ); 53 | 54 | graph.multiplyPointFive.connect( graph.monoPlusStereo, 0, 0 ); 55 | graph.multiplyCoefficient.connect( graph.monoPlusStereo, 0, 1 ); 56 | 57 | graph.monoMinusStereo.connect( graph.merger, 0, 0 ); 58 | graph.monoPlusStereo.connect( graph.merger, 0, 1 ); 59 | 60 | this.inputs[ 0 ].connect( graph.splitter ); 61 | graph.merger.connect( this.outputs[ 0 ] ); 62 | 63 | this.namedInputs.left = graph.inputLeft; 64 | this.namedInputs.right = graph.inputRight; 65 | 66 | this.controls.width = graph.coefficient; 67 | 68 | this.setGraph( graph ); 69 | } 70 | } 71 | 72 | AudioIO.prototype.createStereoWidth = function( width ) { 73 | return new StereoWidth( this, width ); 74 | }; -------------------------------------------------------------------------------- /tests/jasmine/nodeSpecs/Param.js: -------------------------------------------------------------------------------- 1 | describe( "Param", function() { 2 | var node = io.createParam( 100 ); 3 | 4 | it( 'should have a context', function() { 5 | expect( node.context ).toEqual( io.context ); 6 | } ); 7 | 8 | it( "should have a reference to it's AudioIO instance", function() { 9 | expect( node.io ).toEqual( io ); 10 | } ); 11 | 12 | it( "should have a connect method", function() { 13 | expect( node.connect ).toEqual( jasmine.any( Function ) ); 14 | } ); 15 | 16 | it( "should have a disconnect method", function() { 17 | expect( node.disconnect ).toEqual( jasmine.any( Function ) ); 18 | } ); 19 | 20 | 21 | 22 | it( 'should have one input and one output', function() { 23 | expect( node.inputs ).toBeDefined(); 24 | expect( node.outputs ).toBeDefined(); 25 | 26 | expect( node.inputs.length ).toBeDefined(); 27 | expect( node.outputs.length ).toBeDefined(); 28 | 29 | expect( node.inputs.length ).toEqual( 1 ); 30 | expect( node.outputs.length ).toEqual( 1 ); 31 | } ); 32 | 33 | 34 | 35 | 36 | it( 'should set default value if given', function() { 37 | var param = io.createParam( 1, 10 ); 38 | expect( param.defaultValue ).toEqual( 10 ); 39 | } ); 40 | 41 | 42 | 43 | 44 | it( 'should output value when value is passed', function( done ) { 45 | var _io = new AudioIO( new OfflineAudioContext( 1, 44100 * 0.01, 44100 ) ), 46 | node, 47 | val = Math.random() * 20; 48 | 49 | _node = _io.createParam( val ); 50 | _node.connect( _io.master ); 51 | 52 | _io.context.oncomplete = function( e ) { 53 | var buffer = e.renderedBuffer.getChannelData( 0 ); 54 | 55 | for ( var i = 0; i < buffer.length; i++ ) { 56 | expect( buffer[ i ] ).toBeCloseTo( val, 5 ); 57 | } 58 | 59 | done(); 60 | }; 61 | 62 | _io.context.startRendering(); 63 | } ); 64 | 65 | it( 'should output 0 if value is not passed', function( done ) { 66 | var _io = new AudioIO( new OfflineAudioContext( 1, 44100 * 0.01, 44100 ) ), 67 | node; 68 | 69 | _node = _io.createParam(); 70 | _node.connect( _io.master ); 71 | 72 | _io.context.oncomplete = function( e ) { 73 | var buffer = e.renderedBuffer.getChannelData( 0 ); 74 | 75 | for ( var i = 0; i < buffer.length; i++ ) { 76 | expect( buffer[ i ] ).toEqual( 0.0 ); 77 | } 78 | 79 | done(); 80 | }; 81 | 82 | _io.context.startRendering(); 83 | } ); 84 | 85 | it( 'should be controllable', function( done ) { 86 | var _io = new AudioIO( new OfflineAudioContext( 1, 44100 * 0.01, 44100 ) ), 87 | _node = _io.createParam(), 88 | control = _io.createConstant( 2 ); 89 | 90 | control.connect( _node ); 91 | _node.connect( _io.master ); 92 | 93 | _io.context.oncomplete = function( e ) { 94 | var buffer = e.renderedBuffer.getChannelData( 0 ); 95 | 96 | for ( var i = 0; i < buffer.length; i++ ) { 97 | expect( buffer[ i ] ).toEqual( 2 ); 98 | } 99 | 100 | done(); 101 | }; 102 | 103 | _io.context.startRendering(); 104 | } ); 105 | } ); -------------------------------------------------------------------------------- /tests/jasmine/spec/Constant.js: -------------------------------------------------------------------------------- 1 | describe( "Math / Constant", function() { 2 | var node = io.createConstant( 2 ); 3 | 4 | 5 | it( 'should have a context', function() { 6 | expect( node.context ).toEqual( io.context ); 7 | } ); 8 | 9 | it( "should have a reference to it's AudioIO instance", function() { 10 | expect( node.io ).toEqual( io ); 11 | } ); 12 | 13 | it( "should have a connect method", function() { 14 | expect( node.connect ).toEqual( jasmine.any( Function ) ); 15 | } ); 16 | 17 | it( "should have a disconnect method", function() { 18 | expect( node.disconnect ).toEqual( jasmine.any( Function ) ); 19 | } ); 20 | 21 | 22 | 23 | it( 'should have no inputs and one output', function() { 24 | expect( node.inputs ).toBeDefined(); 25 | expect( node.outputs ).toBeDefined(); 26 | expect( node.inputs.length ).toEqual( 0 ); 27 | expect( node.outputs.length ).toEqual( 1 ); 28 | } ); 29 | 30 | 31 | it( 'should have a cleanUp method and mark items for GC.', function() { 32 | var n = io.createConstant( 2 ); 33 | expect( n.cleanUp ).toEqual( jasmine.any( Function ) ); 34 | 35 | n.cleanUp(); 36 | 37 | expect( n.inputs ).toEqual( null ); 38 | expect( n.outputs ).toEqual( null ); 39 | } ); 40 | 41 | it( 'should output a positive number if value is positive', function( done ) { 42 | offlineAudioTest( { 43 | onSetup: function( io ) { 44 | var node = io.createConstant( 543 ); 45 | node.connect( io.master ); 46 | }, 47 | onCompare: function( value ) { 48 | expect( value ).toEqual( 543 ); 49 | }, 50 | onComplete: function() { 51 | done(); 52 | } 53 | } ); 54 | } ); 55 | 56 | it( 'should output a negative number if value is negative', function( done ) { 57 | offlineAudioTest( { 58 | onSetup: function( io ) { 59 | var node = io.createConstant( -0.25 ); 60 | node.connect( io.master ); 61 | }, 62 | onCompare: function( value ) { 63 | expect( value ).toEqual( -0.25 ); 64 | }, 65 | onComplete: function() { 66 | done(); 67 | } 68 | } ); 69 | } ); 70 | 71 | it( 'should output 0 if value is equal to 0', function( done ) { 72 | offlineAudioTest( { 73 | onSetup: function( io ) { 74 | var node = io.createConstant( 0 ); 75 | node.connect( io.master ); 76 | }, 77 | onCompare: function( value ) { 78 | expect( value ).toEqual( 0 ); 79 | }, 80 | onComplete: function() { 81 | done(); 82 | } 83 | } ); 84 | } ); 85 | 86 | 87 | it( 'should output 0 if no value is given', function( done ) { 88 | offlineAudioTest( { 89 | onSetup: function( io ) { 90 | var node = io.createConstant(); 91 | node.connect( io.master ); 92 | }, 93 | onCompare: function( value ) { 94 | expect( value ).toEqual( 0 ); 95 | }, 96 | onComplete: function() { 97 | done(); 98 | } 99 | } ); 100 | } ); 101 | } ); --------------------------------------------------------------------------------