diff --git a/src/containers/sound-editor.jsx b/src/containers/sound-editor.jsx
index 94fca95243f500e4d1c27ee38a8c4025bf44635e..3089b3dc344cbe2f26dc0683ad123539bb802251 100644
--- a/src/containers/sound-editor.jsx
+++ b/src/containers/sound-editor.jsx
@@ -6,7 +6,12 @@ import VM from 'scratch-vm';
 
 import {connect} from 'react-redux';
 
-import {computeChunkedRMS, encodeAndAddSoundToVM, SOUND_BYTE_LIMIT} from '../lib/audio/audio-util.js';
+import {
+    computeChunkedRMS,
+    encodeAndAddSoundToVM,
+    downsampleIfNeeded,
+    dropEveryOtherSample
+} 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';
@@ -39,7 +44,8 @@ class SoundEditor extends React.Component {
             'paste',
             'handleKeyPress',
             'handleContainerClick',
-            'setRef'
+            'setRef',
+            'resampleBufferToRate'
         ]);
         this.state = {
             copyBuffer: null,
@@ -132,45 +138,32 @@ class SoundEditor extends React.Component {
         });
     }
     submitNewSamples (samples, sampleRate, skipUndo) {
-        // Encode the new sound into a wav so that it can be stored
-        let wavBuffer = null;
-        try {
-            wavBuffer = WavEncoder.encode.sync({
-                sampleRate: sampleRate,
-                channelData: [samples]
+        return downsampleIfNeeded({samples, sampleRate}, this.resampleBufferToRate)
+            .then(({samples: newSamples, sampleRate: newSampleRate}) =>
+                WavEncoder.encode({
+                    sampleRate: newSampleRate,
+                    channelData: [newSamples]
+                }).then(wavBuffer => {
+                    if (!skipUndo) {
+                        this.redoStack = [];
+                        if (this.undoStack.length >= UNDO_STACK_SIZE) {
+                            this.undoStack.shift(); // Drop the first element off the array
+                        }
+                        this.undoStack.push(this.getUndoItem());
+                    }
+                    this.resetState(newSamples, newSampleRate);
+                    this.props.vm.updateSoundBuffer(
+                        this.props.soundIndex,
+                        this.audioBufferPlayer.buffer,
+                        new Uint8Array(wavBuffer));
+                    return true; // Edit was successful
+                })
+            )
+            .catch(e => {
+                // Encoding failed, or the sound was too large to save so edit is rejected
+                log.error(`Encountered error while trying to encode sound update: ${e}`);
+                return false; // Edit was not applied
             });
-
-            if (wavBuffer.byteLength > SOUND_BYTE_LIMIT) {
-                // Cancel the sound update by setting to null
-                wavBuffer = null;
-                log.error(`Refusing to encode sound larger than ${SOUND_BYTE_LIMIT} bytes`);
-            }
-        } catch (e) {
-            // This error state is mostly for the mock sounds used during testing.
-            // Any incorrect sound buffer trying to get interpretd as a Wav file
-            // should yield this error.
-            // This can also happen if the sound is too be allocated in memory.
-            log.error(`Encountered error while trying to encode sound update: ${e}`);
-        }
-
-        // Do not submit sound if it could not be encoded (i.e. if too large)
-        if (wavBuffer) {
-            if (!skipUndo) {
-                this.redoStack = [];
-                if (this.undoStack.length >= UNDO_STACK_SIZE) {
-                    this.undoStack.shift(); // Drop the first element off the array
-                }
-                this.undoStack.push(this.getUndoItem());
-            }
-            this.resetState(samples, sampleRate);
-            this.props.vm.updateSoundBuffer(
-                this.props.soundIndex,
-                this.audioBufferPlayer.buffer,
-                new Uint8Array(wavBuffer));
-
-            return true; // Update succeeded
-        }
-        return false; // Update failed
     }
     handlePlay () {
         this.audioBufferPlayer.stop();
@@ -209,13 +202,15 @@ class SoundEditor extends React.Component {
             newSamples.set(firstPart, 0);
             newSamples.set(secondPart, firstPart.length);
         }
-        this.submitNewSamples(newSamples, sampleRate);
-        this.setState({
-            trimStart: null,
-            trimEnd: null
+        this.submitNewSamples(newSamples, sampleRate).then(() => {
+            this.setState({
+                trimStart: null,
+                trimEnd: null
+            });
         });
     }
     handleDeleteInverse () {
+        // Delete everything outside of the trimmers
         const {samples, sampleRate} = this.copyCurrentBuffer();
         const sampleCount = samples.length;
         const startIndex = Math.floor(this.state.trimStart * sampleCount);
@@ -224,10 +219,13 @@ class SoundEditor extends React.Component {
         if (clippedSamples.length === 0) {
             clippedSamples = new Float32Array(1);
         }
-        this.submitNewSamples(clippedSamples, sampleRate);
-        this.setState({
-            trimStart: null,
-            trimEnd: null
+        this.submitNewSamples(clippedSamples, sampleRate).then(success => {
+            if (success) {
+                this.setState({
+                    trimStart: null,
+                    trimEnd: null
+                });
+            }
         });
     }
     handleUpdateTrim (trimStart, trimEnd) {
@@ -257,14 +255,15 @@ class SoundEditor extends React.Component {
         effects.process((renderedBuffer, adjustedTrimStart, adjustedTrimEnd) => {
             const samples = renderedBuffer.getChannelData(0);
             const sampleRate = renderedBuffer.sampleRate;
-            const success = this.submitNewSamples(samples, sampleRate);
-            if (success) {
-                if (this.state.trimStart === null) {
-                    this.handlePlay();
-                } else {
-                    this.setState({trimStart: adjustedTrimStart, trimEnd: adjustedTrimEnd}, this.handlePlay);
+            this.submitNewSamples(samples, sampleRate).then(success => {
+                if (success) {
+                    if (this.state.trimStart === null) {
+                        this.handlePlay();
+                    } else {
+                        this.setState({trimStart: adjustedTrimStart, trimEnd: adjustedTrimEnd}, this.handlePlay);
+                    }
                 }
-            }
+            });
         });
     }
     tooLoud () {
@@ -287,16 +286,22 @@ class SoundEditor extends React.Component {
         this.redoStack.push(this.getUndoItem());
         const {samples, sampleRate, trimStart, trimEnd} = this.undoStack.pop();
         if (samples) {
-            this.submitNewSamples(samples, sampleRate, true);
-            this.setState({trimStart: trimStart, trimEnd: trimEnd}, this.handlePlay);
+            return this.submitNewSamples(samples, sampleRate, true).then(success => {
+                if (success) {
+                    this.setState({trimStart: trimStart, trimEnd: trimEnd}, this.handlePlay);
+                }
+            });
         }
     }
     handleRedo () {
         const {samples, sampleRate, trimStart, trimEnd} = this.redoStack.pop();
         if (samples) {
             this.undoStack.push(this.getUndoItem());
-            this.submitNewSamples(samples, sampleRate, true);
-            this.setState({trimStart: trimStart, trimEnd: trimEnd}, this.handlePlay);
+            return this.submitNewSamples(samples, sampleRate, true).then(success => {
+                if (success) {
+                    this.setState({trimStart: trimStart, trimEnd: trimEnd}, this.handlePlay);
+                }
+            });
         }
     }
     handleCopy () {
@@ -322,25 +327,39 @@ class SoundEditor extends React.Component {
         });
     }
     resampleBufferToRate (buffer, newRate) {
-        return new Promise(resolve => {
-            if (window.OfflineAudioContext) {
-                const sampleRateRatio = newRate / buffer.sampleRate;
-                const newLength = sampleRateRatio * buffer.samples.length;
-                const offlineContext = new window.OfflineAudioContext(1, newLength, newRate);
-                const source = offlineContext.createBufferSource();
-                const audioBuffer = offlineContext.createBuffer(1, buffer.samples.length, buffer.sampleRate);
-                audioBuffer.getChannelData(0).set(buffer.samples);
-                source.buffer = audioBuffer;
-                source.connect(offlineContext.destination);
-                source.start();
-                offlineContext.startRendering();
-                offlineContext.oncomplete = ({renderedBuffer}) => {
-                    resolve({
-                        samples: renderedBuffer.getChannelData(0),
-                        sampleRate: newRate
-                    });
-                };
+        return new Promise((resolve, reject) => {
+            const sampleRateRatio = newRate / buffer.sampleRate;
+            const newLength = sampleRateRatio * buffer.samples.length;
+            let offlineContext;
+            // Try to use either OfflineAudioContext or webkitOfflineAudioContext to resample
+            // The constructors will throw if trying to resample at an unsupported rate
+            // (e.g. Safari/webkitOAC does not support lower than 44khz).
+            try {
+                if (window.OfflineAudioContext) {
+                    offlineContext = new window.OfflineAudioContext(1, newLength, newRate);
+                } else if (window.webkitOfflineAudioContext) {
+                    offlineContext = new window.webkitOfflineAudioContext(1, newLength, newRate);
+                }
+            } catch {
+                // If no OAC available and downsampling by 2, downsample by dropping every other sample.
+                if (newRate === buffer.sampleRate / 2) {
+                    return resolve(dropEveryOtherSample(buffer));
+                }
+                return reject('Could not resample');
             }
+            const source = offlineContext.createBufferSource();
+            const audioBuffer = offlineContext.createBuffer(1, buffer.samples.length, buffer.sampleRate);
+            audioBuffer.getChannelData(0).set(buffer.samples);
+            source.buffer = audioBuffer;
+            source.connect(offlineContext.destination);
+            source.start();
+            offlineContext.startRendering();
+            offlineContext.oncomplete = ({renderedBuffer}) => {
+                resolve({
+                    samples: renderedBuffer.getChannelData(0),
+                    sampleRate: newRate
+                });
+            };
         });
     }
     paste () {
@@ -351,8 +370,11 @@ class SoundEditor extends React.Component {
             const newSamples = new Float32Array(newLength);
             newSamples.set(samples, 0);
             newSamples.set(this.state.copyBuffer.samples, samples.length);
-            this.submitNewSamples(newSamples, this.props.sampleRate, false);
-            this.handlePlay();
+            this.submitNewSamples(newSamples, this.props.sampleRate, false).then(success => {
+                if (success) {
+                    this.handlePlay();
+                }
+            });
         } else {
             // else replace the selection with the pasted sound
             const trimStartSamples = this.state.trimStart * samples.length;
@@ -371,11 +393,14 @@ class SoundEditor extends React.Component {
             const newDurationSeconds = newSamples.length / this.state.copyBuffer.sampleRate;
             const adjustedTrimStart = trimStartSeconds / newDurationSeconds;
             const adjustedTrimEnd = trimEndSeconds / newDurationSeconds;
-            this.submitNewSamples(newSamples, this.props.sampleRate, false);
-            this.setState({
-                trimStart: adjustedTrimStart,
-                trimEnd: adjustedTrimEnd
-            }, this.handlePlay);
+            this.submitNewSamples(newSamples, this.props.sampleRate, false).then(success => {
+                if (success) {
+                    this.setState({
+                        trimStart: adjustedTrimStart,
+                        trimEnd: adjustedTrimEnd
+                    }, this.handlePlay);
+                }
+            });
         }
     }
     handlePaste () {
diff --git a/src/lib/audio/audio-util.js b/src/lib/audio/audio-util.js
index a01c30de811f9c4ec9dab47175c51e9fc96653a5..fba6be96beff35d02abcf025a987473c0bbea074 100644
--- a/src/lib/audio/audio-util.js
+++ b/src/lib/audio/audio-util.js
@@ -59,9 +59,57 @@ const encodeAndAddSoundToVM = function (vm, samples, sampleRate, name, callback)
     });
 };
 
+/**
+ @typedef SoundBuffer
+ @type {Object}
+ @property {Float32Array} samples Array of audio samples
+ @property {number} sampleRate Audio sample rate
+ */
+
+/**
+ * Downsample the given buffer to try to reduce file size below SOUND_BYTE_LIMIT
+ * @param {SoundBuffer} buffer - Buffer to resample
+ * @param {function(SoundBuffer):Promise<SoundBuffer>} resampler - resampler function
+ * @returns {SoundBuffer} Downsampled buffer with half the sample rate
+ */
+const downsampleIfNeeded = (buffer, resampler) => {
+    const {samples, sampleRate} = buffer;
+    const duration = samples.length / sampleRate;
+    const encodedByteLength = samples.length * 2; /* bitDepth 16 bit */
+    // Resolve immediately if already within byte limit
+    if (encodedByteLength < SOUND_BYTE_LIMIT) {
+        return Promise.resolve({samples, sampleRate});
+    }
+    // If encodeable at 22khz, resample and call submitNewSamples again
+    if (duration * 22050 * 2 < SOUND_BYTE_LIMIT) {
+        return resampler({samples, sampleRate}, 22050);
+    }
+    // Cannot save this sound at 22khz, refuse to edit
+    // In the future we could introduce further compression here
+    return Promise.reject('Sound too large to save, refusing to edit');
+};
+
+/**
+ * Drop every other sample of an audio buffer as a last-resort way of downsampling.
+ * @param {SoundBuffer} buffer - Buffer to resample
+ * @returns {SoundBuffer} Downsampled buffer with half the sample rate
+ */
+const dropEveryOtherSample = buffer => {
+    const newLength = Math.floor(buffer.samples.length / 2);
+    const newSamples = new Float32Array(newLength);
+    for (let i = 0; i < newLength; i++) {
+        newSamples[i] = buffer.samples[i * 2];
+    }
+    return {
+        samples: newSamples,
+        sampleRate: buffer.rate / 2
+    };
+};
+
 export {
     computeRMS,
     computeChunkedRMS,
     encodeAndAddSoundToVM,
-    SOUND_BYTE_LIMIT
+    downsampleIfNeeded,
+    dropEveryOtherSample
 };
diff --git a/test/__mocks__/audio-effects.js b/test/__mocks__/audio-effects.js
index 06c36c7e84359a3bac2b4431f21900ff15a3c0f8..9597eec1be0156abfde2e22ce9ead9fc1a520970 100644
--- a/test/__mocks__/audio-effects.js
+++ b/test/__mocks__/audio-effects.js
@@ -14,7 +14,10 @@ export default class MockAudioEffects {
         this.buffer = buffer;
         this.name = name;
         this.process = jest.fn(done => {
-            this._finishProcessing = renderedBuffer => done(renderedBuffer, 0, 1);
+            this._finishProcessing = renderedBuffer => {
+                done(renderedBuffer, 0, 1);
+                return new Promise(resolve => setTimeout(resolve));
+            };
         });
         MockAudioEffects.instance = this;
     }
diff --git a/test/unit/containers/sound-editor.test.jsx b/test/unit/containers/sound-editor.test.jsx
index b0a1a8399f64133dfa0de9328593165289b59a06..4e4c967ec9cc71dc6631410ce5b250f4347d7891 100644
--- a/test/unit/containers/sound-editor.test.jsx
+++ b/test/unit/containers/sound-editor.test.jsx
@@ -97,7 +97,7 @@ describe('Sound Editor Container', () => {
         expect(vm.renameSound).toHaveBeenCalledWith(soundIndex, 'hello');
     });
 
-    test('it handles an effect by submitting the result and playing', () => {
+    test('it handles an effect by submitting the result and playing', async () => {
         const wrapper = mountWithIntl(
             <SoundEditor
                 soundIndex={soundIndex}
@@ -106,7 +106,7 @@ describe('Sound Editor Container', () => {
         );
         const component = wrapper.find(SoundEditorComponent);
         component.props().onReverse(); // Could be any of the effects, just testing the end result
-        mockAudioEffects.instance._finishProcessing(soundBuffer);
+        await mockAudioEffects.instance._finishProcessing(soundBuffer);
         expect(mockAudioBufferPlayer.instance.play).toHaveBeenCalled();
         expect(vm.updateSoundBuffer).toHaveBeenCalled();
     });
@@ -202,7 +202,7 @@ describe('Sound Editor Container', () => {
         expect(mockAudioEffects.instance.process).toHaveBeenCalled();
     });
 
-    test('undo/redo stack state', () => {
+    test('undo/redo stack state', async () => {
         const wrapper = mountWithIntl(
             <SoundEditor
                 soundIndex={soundIndex}
@@ -216,40 +216,40 @@ describe('Sound Editor Container', () => {
 
         // Submitting new samples should make it possible to undo
         component.props().onFaster();
-        mockAudioEffects.instance._finishProcessing(soundBuffer);
+        await mockAudioEffects.instance._finishProcessing(soundBuffer);
         wrapper.update();
         component = wrapper.find(SoundEditorComponent);
         expect(component.prop('canUndo')).toEqual(true);
         expect(component.prop('canRedo')).toEqual(false);
 
         // Undoing should make it possible to redo and not possible to undo again
-        component.props().onUndo();
+        await component.props().onUndo();
         wrapper.update();
         component = wrapper.find(SoundEditorComponent);
         expect(component.prop('canUndo')).toEqual(false);
         expect(component.prop('canRedo')).toEqual(true);
 
         // Redoing should make it possible to undo and not possible to redo again
-        component.props().onRedo();
+        await component.props().onRedo();
         wrapper.update();
         component = wrapper.find(SoundEditorComponent);
         expect(component.prop('canUndo')).toEqual(true);
         expect(component.prop('canRedo')).toEqual(false);
 
         // New submission should clear the redo stack
-        component.props().onUndo(); // Undo to go back to a state where redo is enabled
+        await component.props().onUndo(); // Undo to go back to a state where redo is enabled
         wrapper.update();
         component = wrapper.find(SoundEditorComponent);
         expect(component.prop('canRedo')).toEqual(true);
         component.props().onFaster();
-        mockAudioEffects.instance._finishProcessing(soundBuffer);
+        await mockAudioEffects.instance._finishProcessing(soundBuffer);
 
         wrapper.update();
         component = wrapper.find(SoundEditorComponent);
         expect(component.prop('canRedo')).toEqual(false);
     });
 
-    test('undo and redo submit new samples and play the sound', () => {
+    test('undo and redo submit new samples and play the sound', async () => {
         const wrapper = mountWithIntl(
             <SoundEditor
                 soundIndex={soundIndex}
@@ -260,12 +260,12 @@ describe('Sound Editor Container', () => {
 
         // Set up an undoable state
         component.props().onFaster();
-        mockAudioEffects.instance._finishProcessing(soundBuffer);
+        await mockAudioEffects.instance._finishProcessing(soundBuffer);
         wrapper.update();
         component = wrapper.find(SoundEditorComponent);
 
         // Undo should update the sound buffer and play the new samples
-        component.props().onUndo();
+        await component.props().onUndo();
         expect(mockAudioBufferPlayer.instance.play).toHaveBeenCalled();
         expect(vm.updateSoundBuffer).toHaveBeenCalled();
 
@@ -274,7 +274,7 @@ describe('Sound Editor Container', () => {
         mockAudioBufferPlayer.instance.play.mockClear();
 
         // Undo should update the sound buffer and play the new samples
-        component.props().onRedo();
+        await component.props().onRedo();
         expect(mockAudioBufferPlayer.instance.play).toHaveBeenCalled();
         expect(vm.updateSoundBuffer).toHaveBeenCalled();
     });
diff --git a/test/unit/util/audio-util.test.js b/test/unit/util/audio-util.test.js
index 24ddcd4559567dce0fd1677a11a809c365233699..79ff55e8b5551ce355672ae60de67c146c445900 100644
--- a/test/unit/util/audio-util.test.js
+++ b/test/unit/util/audio-util.test.js
@@ -1,4 +1,9 @@
-import {computeRMS, computeChunkedRMS} from '../../../src/lib/audio/audio-util';
+import {
+    computeRMS,
+    computeChunkedRMS,
+    downsampleIfNeeded,
+    dropEveryOtherSample
+} from '../../../src/lib/audio/audio-util';
 
 describe('computeRMS', () => {
     test('returns 0 when given no samples', () => {
@@ -49,3 +54,44 @@ describe('computeChunkedRMS', () => {
         expect(chunkedLevels).toEqual([Math.sqrt(1 / 0.55), Math.sqrt(1 / 0.55)]);
     });
 });
+
+describe('downsampleIfNeeded', () => {
+    const samples = {length: 1};
+    const sampleRate = 44100;
+    test('returns given data when no downsampling needed', async () => {
+        samples.length = 1;
+        const res = await downsampleIfNeeded({samples, sampleRate}, null);
+        expect(res.samples).toEqual(samples);
+        expect(res.sampleRate).toEqual(sampleRate);
+    });
+    test('downsamples to 22050 if that puts it under the limit', async () => {
+        samples.length = 44100 * 3 * 60;
+        const resampler = jest.fn(() => 'TEST');
+        const res = await downsampleIfNeeded({samples, sampleRate}, resampler);
+        expect(resampler).toHaveBeenCalledWith({samples, sampleRate}, 22050);
+        expect(res).toEqual('TEST');
+    });
+    test('fails if resampling would not put it under the limit', async () => {
+        samples.length = 44100 * 4 * 60;
+        try {
+            await downsampleIfNeeded({samples, sampleRate}, null);
+        } catch (e) {
+            expect(e).toEqual('Sound too large to save, refusing to edit');
+        }
+    });
+});
+
+describe('dropEveryOtherSample', () => {
+    const buffer = {
+        samples: [1, 0, 2, 0, 3, 0],
+        sampleRate: 2
+    };
+    test('result is half the length', () => {
+        const {samples} = dropEveryOtherSample(buffer, 1);
+        expect(samples.length).toEqual(Math.floor(buffer.samples.length / 2));
+    });
+    test('result contains only even-index items', () => {
+        const {samples} = dropEveryOtherSample(buffer, 1);
+        expect(samples).toEqual(new Float32Array([1, 2, 3]));
+    });
+});