diff --git a/package.json b/package.json
index bad718d7eecc7b33968243a26795c72d09fe0029..9f7b08cf6fd298fc3bd5900da8bcd44c92a78187 100644
--- a/package.json
+++ b/package.json
@@ -91,6 +91,7 @@
     "svg-to-image": "1.1.3",
     "svg-url-loader": "2.1.0",
     "wav-encoder": "1.2.0",
+    "web-audio-test-api": "^0.5.2",
     "webpack": "^2.4.1",
     "webpack-dev-server": "^2.4.1",
     "xhr": "2.4.0"
diff --git a/src/containers/sound-editor.jsx b/src/containers/sound-editor.jsx
index 7170b645a24e8840c5bae4f51351819a64694c7f..67c4b1b5fdab808d0920decf7a2becd240a8d43f 100644
--- a/src/containers/sound-editor.jsx
+++ b/src/containers/sound-editor.jsx
@@ -5,7 +5,7 @@ import React from 'react';
 import {connect} from 'react-redux';
 
 import {computeChunkedRMS} from '../lib/audio/audio-util.js';
-
+import AudioEffects from '../lib/audio/audio-effects.js';
 import SoundEditorComponent from '../components/sound-editor/sound-editor.jsx';
 import AudioBufferPlayer from '../lib/audio/audio-buffer-player.js';
 
@@ -110,8 +110,14 @@ class SoundEditor extends React.Component {
     effectFactory (name) {
         return () => this.handleEffect(name);
     }
-    handleEffect (/* name */) {
-        // @todo implement effects
+    handleEffect (name) {
+        const effects = new AudioEffects(this.audioBufferPlayer.buffer, name);
+        effects.process().then(newBuffer => {
+            const samples = newBuffer.getChannelData(0);
+            const sampleRate = newBuffer.sampleRate;
+            this.submitNewSamples(samples, sampleRate);
+            this.handlePlay();
+        });
     }
     handleUndo () {
         this.redoStack.push(this.props.samples.slice(0));
@@ -128,6 +134,7 @@ class SoundEditor extends React.Component {
         }
     }
     render () {
+        const {effectTypes} = AudioEffects;
         return (
             <SoundEditorComponent
                 canRedo={this.redoStack.length > 0}
@@ -139,17 +146,17 @@ class SoundEditor extends React.Component {
                 trimStart={this.state.trimStart}
                 onActivateTrim={this.handleActivateTrim}
                 onChangeName={this.handleChangeName}
-                onEcho={this.effectFactory('echo')}
-                onFaster={this.effectFactory('faster')}
-                onLouder={this.effectFactory('louder')}
+                onEcho={this.effectFactory(effectTypes.ECHO)}
+                onFaster={this.effectFactory(effectTypes.FASTER)}
+                onLouder={this.effectFactory(effectTypes.LOUDER)}
                 onPlay={this.handlePlay}
                 onRedo={this.handleRedo}
-                onReverse={this.effectFactory('reverse')}
-                onRobot={this.effectFactory('robot')}
+                onReverse={this.effectFactory(effectTypes.REVERSE)}
+                onRobot={this.effectFactory(effectTypes.ROBOT)}
                 onSetTrimEnd={this.handleUpdateTrimEnd}
                 onSetTrimStart={this.handleUpdateTrimStart}
-                onSlower={this.effectFactory('slower')}
-                onSofter={this.effectFactory('softer')}
+                onSlower={this.effectFactory(effectTypes.SLOWER)}
+                onSofter={this.effectFactory(effectTypes.SOFTER)}
                 onStop={this.handleStopPlaying}
                 onUndo={this.handleUndo}
             />
diff --git a/src/lib/audio/audio-effects.js b/src/lib/audio/audio-effects.js
new file mode 100644
index 0000000000000000000000000000000000000000..b7ba65cb2384bd3896a607bb5c7ffef185dc84d3
--- /dev/null
+++ b/src/lib/audio/audio-effects.js
@@ -0,0 +1,82 @@
+import EchoEffect from './effects/echo-effect.js';
+import RobotEffect from './effects/robot-effect.js';
+import VolumeEffect from './effects/volume-effect.js';
+
+const effectTypes = {
+    ROBOT: 'robot',
+    REVERSE: 'reverse',
+    LOUDER: 'higher',
+    SOFTER: 'lower',
+    FASTER: 'faster',
+    SLOWER: 'slower',
+    ECHO: 'echo'
+};
+
+class AudioEffects {
+    static get effectTypes () {
+        return effectTypes;
+    }
+    constructor (buffer, name) {
+        // Some effects will modify the playback rate and/or number of samples.
+        // Need to precompute those values to create the offline audio context.
+        const pitchRatio = Math.pow(2, 4 / 12); // A major third
+        let sampleCount = buffer.length;
+        let playbackRate = 1;
+        switch (name) {
+        case effectTypes.ECHO:
+            sampleCount = buffer.length + 0.25 * 3 * buffer.sampleRate;
+            break;
+        case effectTypes.FASTER:
+            playbackRate = pitchRatio;
+            sampleCount = Math.floor(buffer.length / playbackRate);
+            break;
+        case effectTypes.SLOWER:
+            playbackRate = 1 / pitchRatio;
+            sampleCount = Math.floor(buffer.length / playbackRate);
+            break;
+        case effectTypes.REVERSE:
+            buffer.getChannelData(0).reverse();
+            break;
+        }
+
+        this.audioContext = new OfflineAudioContext(1, sampleCount, buffer.sampleRate);
+        this.buffer = buffer;
+        this.source = this.audioContext.createBufferSource();
+        this.source.buffer = this.buffer;
+        this.source.playbackRate.value = playbackRate;
+        this.name = name;
+    }
+    process () {
+        // Some effects need to use more nodes and must expose an input and output
+        let input;
+        let output;
+        switch (this.name) {
+        case effectTypes.LOUDER:
+            ({input, output} = new VolumeEffect(this.audioContext, 1.25));
+            break;
+        case effectTypes.SOFTER:
+            ({input, output} = new VolumeEffect(this.audioContext, 0.75));
+            break;
+        case effectTypes.ECHO:
+            ({input, output} = new EchoEffect(this.audioContext, 0.25));
+            break;
+        case effectTypes.ROBOT:
+            ({input, output} = new RobotEffect(this.audioContext, 0.25));
+            break;
+        }
+
+        if (input && output) {
+            this.source.connect(input);
+            output.connect(this.audioContext.destination);
+        } else {
+            // No effects nodes are needed, wire directly to the output
+            this.source.connect(this.audioContext.destination);
+        }
+
+        this.source.start();
+
+        return this.audioContext.startRendering();
+    }
+}
+
+export default AudioEffects;
diff --git a/src/lib/audio/effects/echo-effect.js b/src/lib/audio/effects/echo-effect.js
new file mode 100644
index 0000000000000000000000000000000000000000..3c7c06806b3b99ff7a09d985dd52629d323bbc32
--- /dev/null
+++ b/src/lib/audio/effects/echo-effect.js
@@ -0,0 +1,33 @@
+class EchoEffect {
+    constructor (audioContext, delayTime) {
+        this.audioContext = audioContext;
+        this.delayTime = delayTime;
+        this.input = this.audioContext.createGain();
+        this.output = this.audioContext.createGain();
+
+        this.effectInput = this.audioContext.createGain();
+        this.effectInput.gain.value = 0.75;
+
+        this.delay = this.audioContext.createDelay(1);
+        this.delay.delayTime.value = delayTime;
+        this.decay = this.audioContext.createGain();
+        this.decay.gain.value = 0.3;
+
+        this.compressor = this.audioContext.createDynamicsCompressor();
+        this.compressor.threshold.value = -5;
+        this.compressor.knee.value = 15;
+        this.compressor.ratio.value = 12;
+        this.compressor.attack.value = 0;
+        this.compressor.release.value = 0.25;
+
+        this.input.connect(this.effectInput);
+        this.effectInput.connect(this.delay);
+        this.delay.connect(this.compressor);
+        this.input.connect(this.compressor);
+        this.delay.connect(this.decay);
+        this.decay.connect(this.delay);
+        this.compressor.connect(this.output);
+    }
+}
+
+export default EchoEffect;
diff --git a/src/lib/audio/effects/robot-effect.js b/src/lib/audio/effects/robot-effect.js
new file mode 100644
index 0000000000000000000000000000000000000000..d530b9e6e039891f7881f5860de9991684f32b90
--- /dev/null
+++ b/src/lib/audio/effects/robot-effect.js
@@ -0,0 +1,101 @@
+class RobotEffect {
+    constructor (audioContext) {
+        this.audioContext = audioContext;
+
+        this.input = this.audioContext.createGain();
+        this.output = this.audioContext.createGain();
+
+        // Ring modulator inspired by BBC Dalek voice
+        // http://recherche.ircam.fr/pub/dafx11/Papers/66_e.pdf
+        // https://github.com/bbc/webaudio.prototyping.bbc.co.uk
+
+        // > There are four parallel signal paths, two which process the
+        // > combination Vc + Vin / 2 and two which process Vc - Vin/2.
+        // > Each branch consists of a non-linearity [diode]...
+        const createDiodeNode = () => {
+            const node = this.audioContext.createWaveShaper();
+
+            // Piecewise function given by (2) in Parker paper
+            const transform = (v, vb = 0.2, vl = 0.4, h = 0.65) => {
+                if (v <= vb) return 0;
+                if (v <= vl) return h * (Math.pow(v - vb, 2) / (2 * vl - 2 * vb));
+                return h * v - h * vl + h * (Math.pow(v - vb, 2) / (2 * vl - 2 * vb));
+            };
+
+            // Create the waveshaper curve with the voltage transform above
+            const bufferLength = 1024;
+            const curve = new Float32Array(bufferLength);
+            for (let i = 0; i < bufferLength; i++) {
+                const voltage = 2 * (i / bufferLength) - 1;
+                curve[i] = transform(voltage);
+            }
+            node.curve = curve;
+            return node;
+        };
+
+        const oscillator = this.audioContext.createOscillator();
+        oscillator.frequency.value = 50;
+        oscillator.start(0);
+
+        const vInGain = this.audioContext.createGain();
+        vInGain.gain.value = 0.5;
+
+        const vInInverter1 = this.audioContext.createGain();
+        vInInverter1.gain.value = -1;
+
+        const vInInverter2 = this.audioContext.createGain();
+        vInInverter2.gain.value = -1;
+
+        const vInDiode1 = createDiodeNode(this.audioContext);
+        const vInDiode2 = createDiodeNode(this.audioContext);
+
+        const vInInverter3 = this.audioContext.createGain();
+        vInInverter3.gain.value = -1;
+
+        const vcInverter1 = this.audioContext.createGain();
+        vcInverter1.gain.value = -1;
+
+        const vcDiode3 = createDiodeNode(this.audioContext);
+        const vcDiode4 = createDiodeNode(this.audioContext);
+
+        const compressor = this.audioContext.createDynamicsCompressor();
+        compressor.threshold.value = -5;
+        compressor.knee.value = 15;
+        compressor.ratio.value = 12;
+        compressor.attack.value = 0;
+        compressor.release.value = 0.25;
+
+        const biquadFilter = this.audioContext.createBiquadFilter();
+        biquadFilter.type = 'highpass';
+        biquadFilter.frequency.value = 1000;
+        biquadFilter.gain.value = 1.25;
+
+        this.input.connect(vcInverter1);
+        this.input.connect(vcDiode4);
+
+        vcInverter1.connect(vcDiode3);
+
+        oscillator.connect(vInGain);
+        vInGain.connect(vInInverter1);
+        vInGain.connect(vcInverter1);
+        vInGain.connect(vcDiode4);
+
+        vInInverter1.connect(vInInverter2);
+        vInInverter1.connect(vInDiode2);
+        vInInverter2.connect(vInDiode1);
+
+        vInDiode1.connect(vInInverter3);
+        vInDiode2.connect(vInInverter3);
+
+        vInInverter3.connect(compressor);
+        vcDiode3.connect(compressor);
+        vcDiode4.connect(compressor);
+
+        this.input.connect(biquadFilter);
+        biquadFilter.connect(compressor);
+
+        compressor.connect(this.output);
+    }
+}
+
+export default RobotEffect;
diff --git a/src/lib/audio/effects/volume-effect.js b/src/lib/audio/effects/volume-effect.js
new file mode 100644
index 0000000000000000000000000000000000000000..bffc422acbc9f1ea12bec768bc56539eb1c72085
--- /dev/null
+++ b/src/lib/audio/effects/volume-effect.js
@@ -0,0 +1,16 @@
+class VolumeEffect {
+    constructor (audioContext, volume) {
+        this.audioContext = audioContext;
+
+        this.input = this.audioContext.createGain();
+        this.output = this.audioContext.createGain();
+
+        this.gain = this.audioContext.createGain();
+        this.gain.gain.value = volume;
+
+        this.input.connect(this.gain);
+        this.gain.connect(this.output);
+    }
+}
+
+export default VolumeEffect;
diff --git a/test/__mocks__/audio-effects.js b/test/__mocks__/audio-effects.js
new file mode 100644
index 0000000000000000000000000000000000000000..bebf0fdc282ede058ca0e4be175d92e5861b565c
--- /dev/null
+++ b/test/__mocks__/audio-effects.js
@@ -0,0 +1,24 @@
+/* eslint-env jest */
+export default class MockAudioEffects {
+    static get effectTypes () { // @todo can this be imported from the real file?
+        return {
+            ROBOT: 'robot',
+            REVERSE: 'reverse',
+            LOUDER: 'higher',
+            SOFTER: 'lower',
+            FASTER: 'faster',
+            SLOWER: 'slower',
+            ECHO: 'echo'
+        };
+    }
+    constructor (buffer, name) {
+        this.buffer = buffer;
+        this.name = name;
+        this._mockResult = {};
+        this._bufferPromise = new Promise(resolve => { // eslint-disable-line no-undef
+            this._finishProcessing = (newBuffer) => resolve(newBuffer);
+        });
+        this.process = jest.fn(() => this._bufferPromise);
+        MockAudioEffects.instance = this;
+    }
+}
diff --git a/test/unit/containers/sound-editor.test.jsx b/test/unit/containers/sound-editor.test.jsx
index 2d1920063c22f1c6020faac4356a99a0d3f9d461..5a5618ea96cd88491b5a7de40cc0d321632ae1b8 100644
--- a/test/unit/containers/sound-editor.test.jsx
+++ b/test/unit/containers/sound-editor.test.jsx
@@ -3,11 +3,14 @@ import React from 'react'; // eslint-disable-line no-unused-vars
 import {mountWithIntl} from '../../helpers/intl-helpers';
 import configureStore from 'redux-mock-store';
 import mockAudioBufferPlayer from '../../__mocks__/audio-buffer-player.js';
+import mockAudioEffects from '../../__mocks__/audio-effects.js';
+
 import SoundEditor from '../../../src/containers/sound-editor'; // eslint-disable-line no-unused-vars
 // eslint-disable-next-line no-unused-vars
 import SoundEditorComponent from '../../../src/components/sound-editor/sound-editor';
 
 jest.mock('../../../src/lib/audio/audio-buffer-player', () => mockAudioBufferPlayer);
+jest.mock('../../../src/lib/audio/audio-effects', () => mockAudioEffects);
 
 describe('Sound Editor Container', () => {
     const mockStore = configureStore();
@@ -90,6 +93,74 @@ describe('Sound Editor Container', () => {
         expect(vm.renameSound).toHaveBeenCalledWith(soundIndex, 'hello');
     });
 
+    test('it handles an effect by submitting the result and playing', (done) => {
+        const wrapper = mountWithIntl(<SoundEditor store={store} soundIndex={soundIndex} />);
+        const component = wrapper.find(SoundEditorComponent);
+        component.props().onReverse(); // Could be any of the effects, just testing the end result
+        mockAudioEffects.instance._finishProcessing(soundBuffer);
+        process.nextTick(() => {
+            expect(mockAudioBufferPlayer.instance.play).toHaveBeenCalled();
+            expect(vm.updateSoundBuffer).toHaveBeenCalled();
+            done();
+        });
+    });
+
+    test('it handles reverse effect correctly', () => {
+        const wrapper = mountWithIntl(<SoundEditor store={store} soundIndex={soundIndex} />);
+        const component = wrapper.find(SoundEditorComponent);
+        component.props().onReverse();
+        expect(mockAudioEffects.instance.name).toEqual(mockAudioEffects.effectTypes.REVERSE);
+        expect(mockAudioEffects.instance.process).toHaveBeenCalled();
+    });
+
+    test('it handles louder effect correctly', () => {
+        const wrapper = mountWithIntl(<SoundEditor store={store} soundIndex={soundIndex} />);
+        const component = wrapper.find(SoundEditorComponent);
+        component.props().onLouder();
+        expect(mockAudioEffects.instance.name).toEqual(mockAudioEffects.effectTypes.LOUDER);
+        expect(mockAudioEffects.instance.process).toHaveBeenCalled();
+    });
+
+    test('it handles softer effect correctly', () => {
+        const wrapper = mountWithIntl(<SoundEditor store={store} soundIndex={soundIndex} />);
+        const component = wrapper.find(SoundEditorComponent);
+        component.props().onSofter();
+        expect(mockAudioEffects.instance.name).toEqual(mockAudioEffects.effectTypes.SOFTER);
+        expect(mockAudioEffects.instance.process).toHaveBeenCalled();
+    });
+
+    test('it handles faster effect correctly', () => {
+        const wrapper = mountWithIntl(<SoundEditor store={store} soundIndex={soundIndex} />);
+        const component = wrapper.find(SoundEditorComponent);
+        component.props().onFaster();
+        expect(mockAudioEffects.instance.name).toEqual(mockAudioEffects.effectTypes.FASTER);
+        expect(mockAudioEffects.instance.process).toHaveBeenCalled();
+    });
+
+    test('it handles slower effect correctly', () => {
+        const wrapper = mountWithIntl(<SoundEditor store={store} soundIndex={soundIndex} />);
+        const component = wrapper.find(SoundEditorComponent);
+        component.props().onSlower();
+        expect(mockAudioEffects.instance.name).toEqual(mockAudioEffects.effectTypes.SLOWER);
+        expect(mockAudioEffects.instance.process).toHaveBeenCalled();
+    });
+
+    test('it handles echo effect correctly', () => {
+        const wrapper = mountWithIntl(<SoundEditor store={store} soundIndex={soundIndex} />);
+        const component = wrapper.find(SoundEditorComponent);
+        component.props().onEcho();
+        expect(mockAudioEffects.instance.name).toEqual(mockAudioEffects.effectTypes.ECHO);
+        expect(mockAudioEffects.instance.process).toHaveBeenCalled();
+    });
+
+    test('it handles robot effect correctly', () => {
+        const wrapper = mountWithIntl(<SoundEditor store={store} soundIndex={soundIndex} />);
+        const component = wrapper.find(SoundEditorComponent);
+        component.props().onRobot();
+        expect(mockAudioEffects.instance.name).toEqual(mockAudioEffects.effectTypes.ROBOT);
+        expect(mockAudioEffects.instance.process).toHaveBeenCalled();
+    });
+
     test('undo/redo functionality', () => {
         const wrapper = mountWithIntl(<SoundEditor store={store} soundIndex={soundIndex} />);
         const component = wrapper.find(SoundEditorComponent);
diff --git a/test/unit/util/audio-effects.test.js b/test/unit/util/audio-effects.test.js
new file mode 100644
index 0000000000000000000000000000000000000000..e30a2e6c14d40528fddf8b5dd2f69ebc93bec5f0
--- /dev/null
+++ b/test/unit/util/audio-effects.test.js
@@ -0,0 +1,59 @@
+/* eslint-env jest */
+/* global AudioNode AudioContext WebAudioTestAPI */
+import 'web-audio-test-api';
+WebAudioTestAPI.setState({
+    'OfflineAudioContext#startRendering': 'promise'
+});
+
+import AudioEffects from '../../../src/lib/audio/audio-effects';
+import RobotEffect from '../../../src/lib/audio/effects/robot-effect';
+import EchoEffect from '../../../src/lib/audio/effects/echo-effect';
+import VolumeEffect from '../../../src/lib/audio/effects/volume-effect';
+
+describe('Audio Effects manager', () => {
+    let audioContext = new AudioContext();
+    let audioBuffer = audioContext.createBuffer(1, 400, 44100);
+
+    test('changes buffer length and playback rate for faster effect', () => {
+        const audioEffects = new AudioEffects(audioBuffer, 'faster');
+        expect(audioEffects.audioContext._.length).toBeLessThan(400);
+        expect(audioEffects.source.playbackRate.value).toBeGreaterThan(1);
+    });
+
+    test('changes buffer length  and playback rate for slower effect', () => {
+        const audioEffects = new AudioEffects(audioBuffer, 'slower');
+        expect(audioEffects.audioContext._.length).toBeGreaterThan(400);
+        expect(audioEffects.source.playbackRate.value).toBeLessThan(1);
+    });
+
+    test('changes buffer length for echo effect', () => {
+        const audioEffects = new AudioEffects(audioBuffer, 'echo');
+        expect(audioEffects.audioContext._.length).toBeGreaterThan(400);
+    });
+
+    test.skip('process starts the offline rendering context and returns a promise', () => {
+        // @todo haven't been able to get web audio test api to actually run render
+    });
+});
+
+describe('Effects', () => {
+    let audioContext;
+
+    beforeEach(() => {
+        audioContext = new AudioContext();
+    });
+
+    test('all effects provide an input and output that are connected', () => {
+        const robotEffect = new RobotEffect(audioContext, 0.5);
+        expect(robotEffect.input).toBeInstanceOf(AudioNode);
+        expect(robotEffect.output).toBeInstanceOf(AudioNode);
+
+        const echoEffect = new EchoEffect(audioContext, 0.5);
+        expect(echoEffect.input).toBeInstanceOf(AudioNode);
+        expect(echoEffect.output).toBeInstanceOf(AudioNode);
+
+        const volumeEffect = new VolumeEffect(audioContext, 0.5);
+        expect(volumeEffect.input).toBeInstanceOf(AudioNode);
+        expect(volumeEffect.output).toBeInstanceOf(AudioNode);
+    });
+});