From 8fae109811afed4935fe409b296df0842b8d2c09 Mon Sep 17 00:00:00 2001 From: Paul Kaplan <pkaplan@media.mit.edu> Date: Thu, 12 Dec 2019 12:30:12 -0500 Subject: [PATCH] Add downsampler --- src/containers/sound-editor.jsx | 48 ++++++++++++++++--------------- src/lib/audio/audio-util.js | 21 +++++++++++++- test/unit/util/audio-util.test.js | 36 ++++++++++++++++++++++- 3 files changed, 80 insertions(+), 25 deletions(-) diff --git a/src/containers/sound-editor.jsx b/src/containers/sound-editor.jsx index 52726f2e0..0ce73b2f6 100644 --- a/src/containers/sound-editor.jsx +++ b/src/containers/sound-editor.jsx @@ -6,7 +6,11 @@ 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 +} 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'; @@ -132,29 +136,27 @@ class SoundEditor extends React.Component { }); } submitNewSamples (samples, sampleRate, skipUndo) { - 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(); - } - if (!skipUndo) { - this.redoStack = []; - if (this.undoStack.length >= UNDO_STACK_SIZE) { - this.undoStack.shift(); // Drop the first element off the array + 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.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 - }) + 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}`); diff --git a/src/lib/audio/audio-util.js b/src/lib/audio/audio-util.js index a01c30de8..2a4c3bb19 100644 --- a/src/lib/audio/audio-util.js +++ b/src/lib/audio/audio-util.js @@ -59,9 +59,28 @@ const encodeAndAddSoundToVM = function (vm, samples, sampleRate, name, callback) }); }; +const downsampleIfNeeded = (samples, sampleRate, resampler) => { + 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); + } + // If encodeable at 11khz, resample and call submitNewSamples again + if (duration * 11025 * 2 < SOUND_BYTE_LIMIT) { + return resampler({samples, sampleRate}, 11025); + } + // Cannot save this sound even at 11khz, refuse to edit + return Promise.reject('Sound too large to save, refusing to edit'); +}; + export { computeRMS, computeChunkedRMS, encodeAndAddSoundToVM, - SOUND_BYTE_LIMIT + downsampleIfNeeded }; diff --git a/test/unit/util/audio-util.test.js b/test/unit/util/audio-util.test.js index 24ddcd455..876cf71e4 100644 --- a/test/unit/util/audio-util.test.js +++ b/test/unit/util/audio-util.test.js @@ -1,4 +1,4 @@ -import {computeRMS, computeChunkedRMS} from '../../../src/lib/audio/audio-util'; +import {computeRMS, computeChunkedRMS, downsampleIfNeeded} from '../../../src/lib/audio/audio-util'; describe('computeRMS', () => { test('returns 0 when given no samples', () => { @@ -49,3 +49,37 @@ 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('downsamples to 11025 if that puts it under the limit', async () => { + samples.length = 44100 * 7 * 60; + const resampler = jest.fn(() => 'TEST'); + const res = await downsampleIfNeeded(samples, sampleRate, resampler); + expect(resampler).toHaveBeenCalledWith({samples, sampleRate}, 11025); + expect(res).toEqual('TEST'); + }); + + test('fails if resampling would not put it under the limit', async () => { + samples.length = 44100 * 8 * 60; + try { + await downsampleIfNeeded(samples, sampleRate, null); + } catch (e) { + expect(e).toEqual('Sound too large to save, refusing to edit'); + } + }); +}); -- GitLab