diff --git a/demo/demo.js b/demo/demo.js index d2a7f49..aa1bbb4 100644 --- a/demo/demo.js +++ b/demo/demo.js @@ -1,5 +1,25 @@ import React, { Component } from 'react'; -import { Analyser, Compressor, Song, Sequencer, Sampler, Synth } from '../src'; + +import { + Analyser, + Bitcrusher, + Chorus, + Compressor, + Delay, + Filter, + MoogFilter, + Overdrive, + Phaser, + PingPong, + Reverb, + Song, + Sequencer, + Sampler, + Synth, +} from '../src'; + +import Polysynth from './polysynth'; +import Visualization from './visualization'; import './index.css'; @@ -14,27 +34,8 @@ export default class Demo extends Component { this.audioProcess = this.audioProcess.bind(this); this.playToggle = this.playToggle.bind(this); } - componentDidMount() { - this.ctx = this.canvas.getContext('2d'); - } audioProcess(analyser) { - if (this.ctx) { - const gradient = this.ctx.createLinearGradient(0, 0, 0, 512); - gradient.addColorStop(1, '#000000'); - gradient.addColorStop(0.75, '#2ecc71'); - gradient.addColorStop(0.25, '#f1c40f'); - gradient.addColorStop(0, '#e74c3c'); - - const array = new Uint8Array(analyser.frequencyBinCount); - analyser.getByteFrequencyData(array); - this.ctx.clearRect(0, 0, 800, 512); - this.ctx.fillStyle = gradient; - - for (let i = 0; i < (array.length); i++) { - const value = array[i]; - this.ctx.fillRect(i * 12, 512, 10, value * -2); - } - } + this.visualization.audioProcess(analyser); } playToggle() { this.setState({ @@ -46,13 +47,60 @@ export default class Demo extends Component {
+ + + + + + + + + - - - - - { this.canvas = c; }} - /> + { this.visualization = c; }} /> diff --git a/demo/index.js b/demo/index.js index c8aede6..78e7a10 100644 --- a/demo/index.js +++ b/demo/index.js @@ -5,4 +5,4 @@ import Demo from './demo'; ReactDOM.render( , document.getElementById('root') -); +); \ No newline at end of file diff --git a/demo/polysynth.js b/demo/polysynth.js new file mode 100644 index 0000000..6b52725 --- /dev/null +++ b/demo/polysynth.js @@ -0,0 +1,45 @@ +import React, { PropTypes } from 'react'; + +import { + Analyser, + Bitcrusher, + Chorus, + Compressor, + Delay, + Filter, + MoogFilter, + Overdrive, + Phaser, + PingPong, + Reverb, + Song, + Sequencer, + Sampler, + Synth, +} from '../src'; + +const Polysynth = (props) => ( + + + + + + + + +); + +Polysynth.propTypes = { + steps: PropTypes.array, +}; + +export default Polysynth; diff --git a/demo/visualization.js b/demo/visualization.js new file mode 100644 index 0000000..b9e6b57 --- /dev/null +++ b/demo/visualization.js @@ -0,0 +1,43 @@ +import React, { Component } from 'react'; + +export default class Visualization extends Component { + constructor(props) { + super(props); + this.audioProcess = this.audioProcess.bind(this); + } + componentDidMount() { + this.ctx = this.canvas.getContext('2d'); + } + componentDidReceiveProps() { + + } + audioProcess(analyser) { + if (this.ctx) { + const gradient = this.ctx.createLinearGradient(0, 0, 0, 512); + gradient.addColorStop(1, '#000000'); + gradient.addColorStop(0.75, '#2ecc71'); + gradient.addColorStop(0.25, '#f1c40f'); + gradient.addColorStop(0, '#e74c3c'); + + const array = new Uint8Array(analyser.frequencyBinCount); + analyser.getByteFrequencyData(array); + this.ctx.clearRect(0, 0, 800, 512); + this.ctx.fillStyle = gradient; + + for (let i = 0; i < (array.length); i++) { + const value = array[i]; + this.ctx.fillRect(i * 12, 512, 10, value * -2); + } + } + } + render() { + return ( + { this.canvas = c; }} + /> + ); + } +} diff --git a/package.json b/package.json index 74265ed..7eaad1a 100644 --- a/package.json +++ b/package.json @@ -17,7 +17,9 @@ "audio-contour": "0.0.1", "envelope-generator": "^3.0.0", "note-parser": "^2.0.0", - "react-addons-create-fragment": "^15.3.1" + "react-addons-create-fragment": "^15.3.1", + "tunajs": "^0.4.5", + "uuid": "^2.0.2" }, "peerDependencies": { "react": "^15.2.1", diff --git a/public/index.html b/public/index.html index 7989c47..981890f 100644 --- a/public/index.html +++ b/public/index.html @@ -7,5 +7,23 @@
+ \ No newline at end of file diff --git a/public/reverb/room.wav b/public/reverb/room.wav new file mode 100755 index 0000000..f0eef84 Binary files /dev/null and b/public/reverb/room.wav differ diff --git a/src/components/analyser.js b/src/components/analyser.js index 5b3a3a0..9479d9c 100644 --- a/src/components/analyser.js +++ b/src/components/analyser.js @@ -19,16 +19,7 @@ export default class Sequencer extends Component { }; static childContextTypes = { audioContext: PropTypes.object, - bars: PropTypes.number, - barInterval: PropTypes.number, - bufferLoaded: PropTypes.func, connectNode: PropTypes.object, - registerBuffer: PropTypes.func, - registerInstrument: PropTypes.func, - resolution: PropTypes.number, - scheduler: PropTypes.object, - tempo: PropTypes.number, - totalBars: PropTypes.number, }; constructor(props, context) { super(props); diff --git a/src/components/bitcrusher.js b/src/components/bitcrusher.js new file mode 100644 index 0000000..3d94757 --- /dev/null +++ b/src/components/bitcrusher.js @@ -0,0 +1,50 @@ +/* eslint-disable no-restricted-syntax */ +import React, { PropTypes, Component } from 'react'; +import Tuna from 'tunajs'; + +export default class Bitcrusher extends Component { + static propTypes = { + children: PropTypes.node, + bits: PropTypes.number, + normfreq: PropTypes.number, + bufferSize: PropTypes.number, + }; + static defaultProps = { + bits: 4, + normfreq: 0.1, + bufferSize: 4096, + }; + static contextTypes = { + audioContext: PropTypes.object, + connectNode: PropTypes.object, + }; + static childContextTypes = { + audioContext: PropTypes.object, + connectNode: PropTypes.object, + }; + constructor(props, context) { + super(props); + + const tuna = new Tuna(context.audioContext); + + this.connectNode = new tuna.Bitcrusher({ + bits: props.bits, + normfreq: props.normfreq, + bufferSize: props.bufferSize, + }); + + this.connectNode.connect(context.connectNode); + } + getChildContext() { + return { + ...this.context, + connectNode: this.connectNode, + }; + } + componentWillUnmount() { + this.connectNode.disconnect(); + } + render() { + return {this.props.children}; + } +} diff --git a/src/components/chorus.js b/src/components/chorus.js new file mode 100644 index 0000000..ab04ff2 --- /dev/null +++ b/src/components/chorus.js @@ -0,0 +1,53 @@ +/* eslint-disable no-restricted-syntax */ +import React, { PropTypes, Component } from 'react'; +import Tuna from 'tunajs'; + +export default class Chorus extends Component { + static propTypes = { + children: PropTypes.node, + rate: PropTypes.number, + feedback: PropTypes.number, + delay: PropTypes.number, + bypass: PropTypes.number, + }; + static defaultProps = { + rate: 1.5, + feedback: 0.2, + delay: 0.0045, + bypass: 0, + }; + static contextTypes = { + audioContext: PropTypes.object, + connectNode: PropTypes.object, + }; + static childContextTypes = { + audioContext: PropTypes.object, + connectNode: PropTypes.object, + }; + constructor(props, context) { + super(props); + + const tuna = new Tuna(context.audioContext); + + this.connectNode = new tuna.Chorus({ + feedback: props.feedback, + rate: props.rate, + delay: props.delay, + bypass: props.bypass, + }); + + this.connectNode.connect(context.connectNode); + } + getChildContext() { + return { + ...this.context, + connectNode: this.connectNode, + }; + } + componentWillUnmount() { + this.connectNode.disconnect(); + } + render() { + return {this.props.children}; + } +} diff --git a/src/components/compressor.js b/src/components/compressor.js index 5c2ddc1..9d6a687 100644 --- a/src/components/compressor.js +++ b/src/components/compressor.js @@ -1,7 +1,7 @@ /* eslint-disable no-restricted-syntax */ import React, { PropTypes, Component } from 'react'; -export default class Sequencer extends Component { +export default class Compressor extends Component { static propTypes = { children: PropTypes.node, threshold: PropTypes.number, @@ -23,16 +23,7 @@ export default class Sequencer extends Component { }; static childContextTypes = { audioContext: PropTypes.object, - bars: PropTypes.number, - barInterval: PropTypes.number, - bufferLoaded: PropTypes.func, connectNode: PropTypes.object, - registerBuffer: PropTypes.func, - registerInstrument: PropTypes.func, - resolution: PropTypes.number, - scheduler: PropTypes.object, - tempo: PropTypes.number, - totalBars: PropTypes.number, }; constructor(props, context) { super(props); diff --git a/src/components/delay.js b/src/components/delay.js new file mode 100644 index 0000000..3bc3a48 --- /dev/null +++ b/src/components/delay.js @@ -0,0 +1,59 @@ +/* eslint-disable no-restricted-syntax */ +import React, { PropTypes, Component } from 'react'; +import Tuna from 'tunajs'; + +export default class Delay extends Component { + static propTypes = { + children: PropTypes.node, + feedback: PropTypes.number, + delayTime: PropTypes.number, + wetLevel: PropTypes.number, + dryLevel: PropTypes.number, + cutoff: PropTypes.number, + bypass: PropTypes.number, + }; + static defaultProps = { + feedback: 0.45, + delayTime: 150, + wetLevel: 0.25, + dryLevel: 1, + cutoff: 2000, + bypass: 0, + }; + static contextTypes = { + audioContext: PropTypes.object, + connectNode: PropTypes.object, + }; + static childContextTypes = { + audioContext: PropTypes.object, + connectNode: PropTypes.object, + }; + constructor(props, context) { + super(props); + + const tuna = new Tuna(context.audioContext); + + this.connectNode = new tuna.Delay({ + feedback: props.feedback, + delayTime: props.delayTime, + wetLevel: props.wetLevel, + dryLevel: props.dryLevel, + cutoff: props.cutoff, + bypass: props.bypass, + }); + + this.connectNode.connect(context.connectNode); + } + getChildContext() { + return { + ...this.context, + connectNode: this.connectNode, + }; + } + componentWillUnmount() { + this.connectNode.disconnect(); + } + render() { + return {this.props.children}; + } +} diff --git a/src/components/filter.js b/src/components/filter.js new file mode 100644 index 0000000..a1e467b --- /dev/null +++ b/src/components/filter.js @@ -0,0 +1,60 @@ +/* eslint-disable no-restricted-syntax */ +import React, { PropTypes, Component } from 'react'; + +export default class Filter extends Component { + static propTypes = { + children: PropTypes.node, + frequency: PropTypes.number, + Q: PropTypes.number, + gain: PropTypes.number, + type: PropTypes.string, + }; + static defaultProps = { + frequency: 2000, + Q: 0, + gain: 0, + type: 'lowpass', + }; + static contextTypes = { + audioContext: PropTypes.object, + connectNode: PropTypes.object, + }; + static childContextTypes = { + audioContext: PropTypes.object, + connectNode: PropTypes.object, + }; + constructor(props, context) { + super(props); + + this.connectNode = context.audioContext.createBiquadFilter(); + this.connectNode.connect(context.connectNode); + + this.applyProps = this.applyProps.bind(this); + } + getChildContext() { + return { + ...this.context, + connectNode: this.connectNode, + }; + } + componentDidMount() { + this.applyProps(this.props); + } + componentWillUnmount() { + this.connectNode.disconnect(); + } + applyProps(props) { + for (const prop in props) { + if (this.connectNode[prop]) { + if (typeof this.connectNode[prop] === 'object') { + this.connectNode[prop].value = props[prop]; + } else { + this.connectNode[prop] = props[prop]; + } + } + } + } + render() { + return {this.props.children}; + } +} diff --git a/src/components/moog-filter.js b/src/components/moog-filter.js new file mode 100644 index 0000000..17bdad5 --- /dev/null +++ b/src/components/moog-filter.js @@ -0,0 +1,50 @@ +/* eslint-disable no-restricted-syntax */ +import React, { PropTypes, Component } from 'react'; +import Tuna from 'tunajs'; + +export default class MoogFilter extends Component { + static propTypes = { + children: PropTypes.node, + cutoff: PropTypes.number, + resonance: PropTypes.number, + bufferSize: PropTypes.number, + }; + static defaultProps = { + cutoff: 0.065, + resonance: 3.5, + bufferSize: 4096, + }; + static contextTypes = { + audioContext: PropTypes.object, + connectNode: PropTypes.object, + }; + static childContextTypes = { + audioContext: PropTypes.object, + connectNode: PropTypes.object, + }; + constructor(props, context) { + super(props); + + const tuna = new Tuna(context.audioContext); + + this.connectNode = new tuna.MoogFilter({ + cutoff: props.cutoff, + resonance: props.resonance, + bufferSize: props.bufferSize, + }); + + this.connectNode.connect(context.connectNode); + } + getChildContext() { + return { + ...this.context, + connectNode: this.connectNode, + }; + } + componentWillUnmount() { + this.connectNode.disconnect(); + } + render() { + return {this.props.children}; + } +} diff --git a/src/components/overdrive.js b/src/components/overdrive.js new file mode 100644 index 0000000..7ee2cfc --- /dev/null +++ b/src/components/overdrive.js @@ -0,0 +1,56 @@ +/* eslint-disable no-restricted-syntax */ +import React, { PropTypes, Component } from 'react'; +import Tuna from 'tunajs'; + +export default class Overdrive extends Component { + static propTypes = { + children: PropTypes.node, + outputGain: PropTypes.number, + drive: PropTypes.number, + curveAmount: PropTypes.number, + algorithmIndex: PropTypes.number, + bypass: PropTypes.number, + }; + static defaultProps = { + outputGain: 0.5, + drive: 0.7, + curveAmount: 1, + algorithmIndex: 0, + bypass: 0, + }; + static contextTypes = { + audioContext: PropTypes.object, + connectNode: PropTypes.object, + }; + static childContextTypes = { + audioContext: PropTypes.object, + connectNode: PropTypes.object, + }; + constructor(props, context) { + super(props); + + const tuna = new Tuna(context.audioContext); + + this.connectNode = new tuna.Overdrive({ + outputGain: props.outputGain, + drive: props.drive, + curveAmount: props.curveAmount, + algorithmIndex: props.algorithmIndex, + bypass: props.bypass, + }); + + this.connectNode.connect(context.connectNode); + } + getChildContext() { + return { + ...this.context, + connectNode: this.connectNode, + }; + } + componentWillUnmount() { + this.connectNode.disconnect(); + } + render() { + return {this.props.children}; + } +} diff --git a/src/components/phaser.js b/src/components/phaser.js new file mode 100644 index 0000000..5736782 --- /dev/null +++ b/src/components/phaser.js @@ -0,0 +1,58 @@ +/* eslint-disable no-restricted-syntax */ +import React, { PropTypes, Component } from 'react'; +import Tuna from 'tunajs'; + +export default class Phaser extends Component { + static propTypes = { + children: PropTypes.node, + rate: PropTypes.number, + depth: PropTypes.number, + feedback: PropTypes.number, + stereoPhase: PropTypes.number, + baseModulationFrequency: PropTypes.number, + bypass: PropTypes.number, + }; + static defaultProps = { + rate: 1.2, + depth: 0.3, + feedback: 0.2, + stereoPhase: 30, + baseModulationFrequency: 700, + bypass: 0, + }; + static contextTypes = { + audioContext: PropTypes.object, + connectNode: PropTypes.object, + }; + static childContextTypes = { + audioContext: PropTypes.object, + connectNode: PropTypes.object, }; + constructor(props, context) { + super(props); + + const tuna = new Tuna(context.audioContext); + + this.connectNode = new tuna.Phaser({ + rate: props.rate, + depth: props.depth, + feedback: props.feedback, + stereoPhase: props.stereoPhase, + baseModulationFrequency: props.baseModulationFrequency, + bypass: props.bypass, + }); + + this.connectNode.connect(context.connectNode); + } + getChildContext() { + return { + ...this.context, + connectNode: this.connectNode, + }; + } + componentWillUnmount() { + this.connectNode.disconnect(); + } + render() { + return {this.props.children}; + } +} diff --git a/src/components/ping-pong.js b/src/components/ping-pong.js new file mode 100644 index 0000000..5e1d70f --- /dev/null +++ b/src/components/ping-pong.js @@ -0,0 +1,53 @@ +/* eslint-disable no-restricted-syntax */ +import React, { PropTypes, Component } from 'react'; +import Tuna from 'tunajs'; + +export default class PingPong extends Component { + static propTypes = { + children: PropTypes.node, + wetLevel: PropTypes.number, + feedback: PropTypes.number, + delayTimeLeft: PropTypes.number, + delayTimeRight: PropTypes.number, + }; + static defaultProps = { + wetLevel: 0.5, + feedback: 0.3, + delayTimeLeft: 150, + delayTimeRight: 200, + }; + static contextTypes = { + audioContext: PropTypes.object, + connectNode: PropTypes.object, + }; + static childContextTypes = { + audioContext: PropTypes.object, + connectNode: PropTypes.object, + }; + constructor(props, context) { + super(props); + + const tuna = new Tuna(context.audioContext); + + this.connectNode = new tuna.PingPongDelay({ + wetLevel: props.wetLevel, + feedback: props.feedback, + delayTimeLeft: props.delayTimeLeft, + delayTimeRight: props.delayTimeRight, + }); + + this.connectNode.connect(context.connectNode); + } + getChildContext() { + return { + ...this.context, + connectNode: this.connectNode, + }; + } + componentWillUnmount() { + this.connectNode.disconnect(); + } + render() { + return {this.props.children}; + } +} diff --git a/src/components/reverb.js b/src/components/reverb.js new file mode 100644 index 0000000..dbc695f --- /dev/null +++ b/src/components/reverb.js @@ -0,0 +1,62 @@ +/* eslint-disable no-restricted-syntax */ +import React, { PropTypes, Component } from 'react'; +import Tuna from 'tunajs'; + +export default class Reverb extends Component { + static propTypes = { + children: PropTypes.node, + highCut: PropTypes.number, + lowCut: PropTypes.number, + dryLevel: PropTypes.number, + wetLevel: PropTypes.number, + level: PropTypes.number, + impulse: PropTypes.string, + bypass: PropTypes.number, + }; + static defaultProps = { + highCut: 22050, + lowCut: 20, + dryLevel: 1, + wetLevel: 1, + level: 1, + impulse: 'reverb/room.wav', + bypass: 0, + }; + static contextTypes = { + audioContext: PropTypes.object, + connectNode: PropTypes.object, + }; + static childContextTypes = { + audioContext: PropTypes.object, + connectNode: PropTypes.object, + }; + constructor(props, context) { + super(props); + + const tuna = new Tuna(context.audioContext); + + this.connectNode = new tuna.Convolver({ + highCut: props.highCut, + lowCut: props.lowCut, + dryLevel: props.dryLevel, + wetLevel: props.wetLevel, + level: props.level, + impulse: props.impulse, + bypass: props.bypass, + }); + + this.connectNode.connect(context.connectNode); + } + getChildContext() { + return { + ...this.context, + connectNode: this.connectNode, + }; + } + componentWillUnmount() { + this.connectNode.disconnect(); + } + render() { + return {this.props.children}; + } +} diff --git a/src/components/sampler.js b/src/components/sampler.js index 7f0615a..d1b9c59 100644 --- a/src/components/sampler.js +++ b/src/components/sampler.js @@ -1,4 +1,6 @@ import React, { PropTypes, Component } from 'react'; +import uuid from 'uuid'; + import { BufferLoader } from '../utils/buffer-loader'; export default class Sampler extends Component { @@ -18,25 +20,25 @@ export default class Sampler extends Component { barInterval: PropTypes.number, bufferLoaded: PropTypes.func, connectNode: PropTypes.object, - registerBuffer: PropTypes.func, - registerInstrument: PropTypes.func, + getMaster: PropTypes.func, resolution: PropTypes.number, scheduler: PropTypes.object, tempo: PropTypes.number, - totalBars: PropTypes.number, }; - constructor(props, context) { + constructor(props) { super(props); this.buffer = null; this.bufferLoaded = this.bufferLoaded.bind(this); this.getSteps = this.getSteps.bind(this); this.playStep = this.playStep.bind(this); - - context.registerInstrument(this.getSteps); } componentDidMount() { - this.context.registerBuffer(); + this.id = uuid.v1(); + + const master = this.context.getMaster(); + master.instruments[this.id] = this.getSteps; + master.buffers[this.id] = 1; const bufferLoader = new BufferLoader( this.context.audioContext, @@ -46,8 +48,15 @@ export default class Sampler extends Component { bufferLoader.load(); } + componentWillUnmount() { + const master = this.context.getMaster(); + + delete master.buffers[this.id]; + delete master.instruments[this.id]; + } getSteps(playbackTime) { - const loopCount = this.context.totalBars / this.context.bars; + const totalBars = this.context.getMaster().getMaxBars(); + const loopCount = totalBars / this.context.bars; for (let i = 0; i < loopCount; i++) { const barOffset = ((this.context.barInterval * this.context.bars) * i) / 1000; const stepInterval = this.context.barInterval / this.context.resolution; @@ -75,6 +84,8 @@ export default class Sampler extends Component { } bufferLoaded([buffer]) { this.buffer = buffer; + const master = this.context.getMaster(); + delete master.buffers[this.id]; this.context.bufferLoaded(); } render() { diff --git a/src/components/sequencer.js b/src/components/sequencer.js index f488a45..929c519 100644 --- a/src/components/sequencer.js +++ b/src/components/sequencer.js @@ -1,5 +1,7 @@ import React, { PropTypes, Component } from 'react'; +import uuid from 'uuid'; + export default class Sequencer extends Component { static propTypes = { resolution: PropTypes.number, @@ -11,27 +13,13 @@ export default class Sequencer extends Component { bars: 1, }; static contextTypes = { - registerBars: PropTypes.func, + getMaster: PropTypes.func, }; static childContextTypes = { - audioContext: PropTypes.object, bars: PropTypes.number, - barInterval: PropTypes.number, - bufferLoaded: PropTypes.func, - connectNode: PropTypes.object, - registerBars: PropTypes.func, - registerBuffer: PropTypes.func, - registerInstrument: PropTypes.func, + getMaster: PropTypes.func, resolution: PropTypes.number, - scheduler: PropTypes.object, - tempo: PropTypes.number, - totalBars: PropTypes.number, }; - constructor(props, context) { - super(props); - - context.registerBars(props.bars); - } getChildContext() { return { ...this.context, @@ -39,6 +27,18 @@ export default class Sequencer extends Component { resolution: this.props.resolution, }; } + componentDidMount() { + this.id = uuid.v1(); + const master = this.context.getMaster(); + master.bars[this.id] = this.props.bars; + } + componentWillReceiveProps(nextProps) { + const master = this.context.getMaster(); + master.bars[this.id] = nextProps.bars; + } + componentWillUnmount() { + delete this.context.getMaster().bars[this.id]; + } render() { return {this.props.children}; } diff --git a/src/components/song.js b/src/components/song.js index 659b48d..feef91b 100644 --- a/src/components/song.js +++ b/src/components/song.js @@ -17,12 +17,9 @@ export default class Song extends Component { barInterval: PropTypes.number, bufferLoaded: PropTypes.func, connectNode: PropTypes.object, - registerBars: PropTypes.func, - registerBuffer: PropTypes.func, - registerInstrument: PropTypes.func, + getMaster: PropTypes.func, scheduler: PropTypes.object, tempo: PropTypes.number, - totalBars: PropTypes.number, }; constructor(props) { super(props); @@ -31,16 +28,16 @@ export default class Song extends Component { buffersLoaded: false, }; - this.bufferCount = 0; - this.bars = 1; this.barInterval = (60000 / props.tempo) * 4; - this.instrumentCallbacks = []; + this.bars = {}; + this.buffers = {}; + this.instruments = {}; + this.busses = {}; this.loop = this.loop.bind(this); - this.registerBars = this.registerBars.bind(this); - this.registerBuffer = this.registerBuffer.bind(this); - this.registerInstrument = this.registerInstrument.bind(this); this.bufferLoaded = this.bufferLoaded.bind(this); + this.getMaster = this.getMaster.bind(this); + this.getMaxBars = this.getMaxBars.bind(this); window.AudioContext = window.AudioContext || window.webkitAudioContext; this.audioContext = new AudioContext(); @@ -56,11 +53,8 @@ export default class Song extends Component { barInterval: this.barInterval, bufferLoaded: this.bufferLoaded, connectNode: this.audioContext.destination, - registerBuffer: this.registerBuffer, // make dynamic - registerBars: this.registerBars, - registerInstrument: this.registerInstrument, // make dynamic + getMaster: this.getMaster, scheduler: this.scheduler, - totalBars: this.bars, // Make dynamic }; } componentDidMount() { @@ -82,30 +76,26 @@ export default class Song extends Component { } } } + getMaster() { + return this; + } + getMaxBars() { + return Math.max.apply(Math, Object.keys(this.bars).map((b) => this.bars[b])); + } bufferLoaded() { - this.bufferCount--; - if (this.bufferCount === 0) { + if (Object.keys(this.buffers).length === 0) { this.setState({ buffersLoaded: true, }); } } loop(e) { - this.instrumentCallbacks.forEach((callback) => { + const maxBars = Object.keys(this.bars).length ? this.getMaxBars() : 1; + Object.keys(this.instruments).forEach((id) => { + const callback = this.instruments[id]; callback(e.playbackTime); }); - this.scheduler.insert(e.playbackTime + ((this.barInterval * this.bars) / 1000), this.loop); - } - registerBars(bars) { - if (bars > this.bars) { - this.bars = bars; - } - } - registerBuffer() { - this.bufferCount++; - } - registerInstrument(callback) { - this.instrumentCallbacks.push(callback); + this.scheduler.insert(e.playbackTime + ((this.barInterval * maxBars) / 1000), this.loop); } render() { return {this.props.children}; diff --git a/src/components/synth.js b/src/components/synth.js index 293db6e..b8f3fd2 100644 --- a/src/components/synth.js +++ b/src/components/synth.js @@ -1,6 +1,7 @@ import React, { PropTypes, Component } from 'react'; import parser from 'note-parser'; import contour from 'audio-contour'; +import uuid from 'uuid'; export default class Synth extends Component { static displayName = 'Synth'; @@ -13,6 +14,7 @@ export default class Synth extends Component { release: PropTypes.number, }), gain: PropTypes.number, + transpose: PropTypes.number, type: PropTypes.string.isRequired, steps: PropTypes.array.isRequired, }; @@ -23,35 +25,40 @@ export default class Synth extends Component { sustain: 0.2, release: 0.2, }, + transpose: 0, gain: 0.5, }; static contextTypes = { audioContext: PropTypes.object, bars: PropTypes.number, barInterval: PropTypes.number, - bufferLoaded: PropTypes.func, connectNode: PropTypes.object, - registerBuffer: PropTypes.func, - registerInstrument: PropTypes.func, + getMaster: PropTypes.func, resolution: PropTypes.number, scheduler: PropTypes.object, tempo: PropTypes.number, - totalBars: PropTypes.number, }; constructor(props, context) { super(props); this.getSteps = this.getSteps.bind(this); this.playStep = this.playStep.bind(this); - - context.registerInstrument(this.getSteps); + } + componentDidMount() { + this.id = uuid.v1(); + const master = this.context.getMaster(); + master.instruments[this.id] = this.getSteps; + } + componentWillUnmount() { + const master = this.context.getMaster(); + delete master.instruments[this.id]; } getSteps(playbackTime) { - const loopCount = this.context.totalBars / this.context.bars; + const totalBars = this.context.getMaster().getMaxBars(); + const loopCount = totalBars / this.context.bars; for (let i = 0; i < loopCount; i++) { const barOffset = ((this.context.barInterval * this.context.bars) * i) / 1000; const stepInterval = this.context.barInterval / this.context.resolution; - this.props.steps.forEach((step) => { const time = barOffset + ((step[0] * stepInterval) / 1000); @@ -63,9 +70,13 @@ export default class Synth extends Component { } } createOscillator(time, note, duration) { + const volumeGain = this.context.audioContext.createGain(); + volumeGain.gain.value = this.props.gain; + volumeGain.connect(this.context.connectNode); - const gain = this.context.audioContext.createGain(); - gain.connect(this.context.connectNode); + const amplitudeGain = this.context.audioContext.createGain(); + amplitudeGain.gain.value = 0; + amplitudeGain.connect(volumeGain); const env = contour(this.context.audioContext, { attack: this.props.envelope.attack, @@ -73,12 +84,16 @@ export default class Synth extends Component { sustain: this.props.envelope.sustain, release: this.props.envelope.release, }); - env.connect(gain.gain); + + env.connect(amplitudeGain.gain); const osc = this.context.audioContext.createOscillator(); - osc.frequency.value = parser.freq(note); + const transposed = note.slice(0, -1) + + (parseInt(note[note.length - 1], 0) + parseInt(this.props.transpose, 0)); + + osc.frequency.value = parser.freq(transposed); osc.type = this.props.type; - osc.connect(gain); + osc.connect(amplitudeGain); osc.start(time); env.start(time); diff --git a/src/index.js b/src/index.js index b52b067..2d27402 100644 --- a/src/index.js +++ b/src/index.js @@ -1,5 +1,14 @@ import Analyser from './components/analyser.js'; +import Bitcrusher from './components/bitcrusher.js'; +import Chorus from './components/chorus.js'; import Compressor from './components/compressor.js'; +import Delay from './components/delay.js'; +import Filter from './components/filter.js'; +import MoogFilter from './components/moog-filter.js'; +import Overdrive from './components/overdrive.js'; +import Phaser from './components/phaser.js'; +import PingPong from './components/ping-pong.js'; +import Reverb from './components/reverb.js'; import Sequencer from './components/sequencer.js'; import Sampler from './components/sampler.js'; import Song from './components/song.js'; @@ -7,7 +16,16 @@ import Synth from './components/synth.js'; export { Analyser, + Bitcrusher, + Chorus, Compressor, + Delay, + Filter, + MoogFilter, + Overdrive, + Phaser, + PingPong, + Reverb, Sequencer, Sampler, Song,