Skip to content
Snippets Groups Projects
Commit 8fae1098 authored by Paul Kaplan's avatar Paul Kaplan
Browse files

Add downsampler

parent 7980c23e
No related branches found
No related tags found
No related merge requests found
...@@ -6,7 +6,11 @@ import VM from 'scratch-vm'; ...@@ -6,7 +6,11 @@ import VM from 'scratch-vm';
import {connect} from 'react-redux'; 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 AudioEffects from '../lib/audio/audio-effects.js';
import SoundEditorComponent from '../components/sound-editor/sound-editor.jsx'; import SoundEditorComponent from '../components/sound-editor/sound-editor.jsx';
import AudioBufferPlayer from '../lib/audio/audio-buffer-player.js'; import AudioBufferPlayer from '../lib/audio/audio-buffer-player.js';
...@@ -132,29 +136,27 @@ class SoundEditor extends React.Component { ...@@ -132,29 +136,27 @@ class SoundEditor extends React.Component {
}); });
} }
submitNewSamples (samples, sampleRate, skipUndo) { submitNewSamples (samples, sampleRate, skipUndo) {
return WavEncoder.encode({ return downsampleIfNeeded(samples, sampleRate, this.resampleBufferToRate)
sampleRate: sampleRate, .then(({samples: newSamples, sampleRate: newSampleRate}) =>
channelData: [samples] WavEncoder.encode({
}) sampleRate: newSampleRate,
.then(wavBuffer => { channelData: [newSamples]
if (wavBuffer.byteLength > SOUND_BYTE_LIMIT) { }).then(wavBuffer => {
log.error(`Refusing to encode sound larger than ${SOUND_BYTE_LIMIT} bytes`); if (!skipUndo) {
return Promise.reject(); this.redoStack = [];
} if (this.undoStack.length >= UNDO_STACK_SIZE) {
if (!skipUndo) { this.undoStack.shift(); // Drop the first element off the array
this.redoStack = []; }
if (this.undoStack.length >= UNDO_STACK_SIZE) { this.undoStack.push(this.getUndoItem());
this.undoStack.shift(); // Drop the first element off the array
} }
this.undoStack.push(this.getUndoItem()); this.resetState(newSamples, newSampleRate);
} this.props.vm.updateSoundBuffer(
this.resetState(samples, sampleRate); this.props.soundIndex,
this.props.vm.updateSoundBuffer( this.audioBufferPlayer.buffer,
this.props.soundIndex, new Uint8Array(wavBuffer));
this.audioBufferPlayer.buffer, return true; // Edit was successful
new Uint8Array(wavBuffer)); })
return true; // Edit was successful )
})
.catch(e => { .catch(e => {
// Encoding failed, or the sound was too large to save so edit is rejected // 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}`); log.error(`Encountered error while trying to encode sound update: ${e}`);
......
...@@ -59,9 +59,28 @@ const encodeAndAddSoundToVM = function (vm, samples, sampleRate, name, callback) ...@@ -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 { export {
computeRMS, computeRMS,
computeChunkedRMS, computeChunkedRMS,
encodeAndAddSoundToVM, encodeAndAddSoundToVM,
SOUND_BYTE_LIMIT downsampleIfNeeded
}; };
import {computeRMS, computeChunkedRMS} from '../../../src/lib/audio/audio-util'; import {computeRMS, computeChunkedRMS, downsampleIfNeeded} from '../../../src/lib/audio/audio-util';
describe('computeRMS', () => { describe('computeRMS', () => {
test('returns 0 when given no samples', () => { test('returns 0 when given no samples', () => {
...@@ -49,3 +49,37 @@ describe('computeChunkedRMS', () => { ...@@ -49,3 +49,37 @@ describe('computeChunkedRMS', () => {
expect(chunkedLevels).toEqual([Math.sqrt(1 / 0.55), Math.sqrt(1 / 0.55)]); 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');
}
});
});
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment