From 7980c23eccc055ad03929afa3d5cfda7835a3789 Mon Sep 17 00:00:00 2001 From: Paul Kaplan <pkaplan@media.mit.edu> Date: Thu, 12 Dec 2019 11:44:15 -0500 Subject: [PATCH] Make submitNewSamples async and use promises --- src/containers/sound-editor.jsx | 135 +++++++++++---------- test/__mocks__/audio-effects.js | 5 +- test/unit/containers/sound-editor.test.jsx | 24 ++-- 3 files changed, 87 insertions(+), 77 deletions(-) diff --git a/src/containers/sound-editor.jsx b/src/containers/sound-editor.jsx index 94fca9524..52726f2e0 100644 --- a/src/containers/sound-editor.jsx +++ b/src/containers/sound-editor.jsx @@ -132,45 +132,34 @@ 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] - }); - - 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 + return WavEncoder.encode({ + sampleRate: sampleRate, + channelData: [samples] + }) + .then(wavBuffer => { + if (wavBuffer.byteLength > SOUND_BYTE_LIMIT) { + log.error(`Refusing to encode sound larger than ${SOUND_BYTE_LIMIT} bytes`); + return Promise.reject(); } - 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 + 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; // 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 + }); } handlePlay () { this.audioBufferPlayer.stop(); @@ -209,11 +198,13 @@ 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 () { const {samples, sampleRate} = this.copyCurrentBuffer(); @@ -224,10 +215,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 +251,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 +282,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 () { @@ -351,8 +352,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 +375,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/test/__mocks__/audio-effects.js b/test/__mocks__/audio-effects.js index 06c36c7e8..9597eec1b 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 b0a1a8399..4e4c967ec 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(); }); -- GitLab