Skip to content

Commit 8fae109

Browse files
committed
Add downsampler
1 parent 7980c23 commit 8fae109

File tree

3 files changed

+80
-25
lines changed

3 files changed

+80
-25
lines changed

src/containers/sound-editor.jsx

Lines changed: 25 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,11 @@ import VM from 'scratch-vm';
66

77
import {connect} from 'react-redux';
88

9-
import {computeChunkedRMS, encodeAndAddSoundToVM, SOUND_BYTE_LIMIT} from '../lib/audio/audio-util.js';
9+
import {
10+
computeChunkedRMS,
11+
encodeAndAddSoundToVM,
12+
downsampleIfNeeded
13+
} from '../lib/audio/audio-util.js';
1014
import AudioEffects from '../lib/audio/audio-effects.js';
1115
import SoundEditorComponent from '../components/sound-editor/sound-editor.jsx';
1216
import AudioBufferPlayer from '../lib/audio/audio-buffer-player.js';
@@ -132,29 +136,27 @@ class SoundEditor extends React.Component {
132136
});
133137
}
134138
submitNewSamples (samples, sampleRate, skipUndo) {
135-
return WavEncoder.encode({
136-
sampleRate: sampleRate,
137-
channelData: [samples]
138-
})
139-
.then(wavBuffer => {
140-
if (wavBuffer.byteLength > SOUND_BYTE_LIMIT) {
141-
log.error(`Refusing to encode sound larger than ${SOUND_BYTE_LIMIT} bytes`);
142-
return Promise.reject();
143-
}
144-
if (!skipUndo) {
145-
this.redoStack = [];
146-
if (this.undoStack.length >= UNDO_STACK_SIZE) {
147-
this.undoStack.shift(); // Drop the first element off the array
139+
return downsampleIfNeeded(samples, sampleRate, this.resampleBufferToRate)
140+
.then(({samples: newSamples, sampleRate: newSampleRate}) =>
141+
WavEncoder.encode({
142+
sampleRate: newSampleRate,
143+
channelData: [newSamples]
144+
}).then(wavBuffer => {
145+
if (!skipUndo) {
146+
this.redoStack = [];
147+
if (this.undoStack.length >= UNDO_STACK_SIZE) {
148+
this.undoStack.shift(); // Drop the first element off the array
149+
}
150+
this.undoStack.push(this.getUndoItem());
148151
}
149-
this.undoStack.push(this.getUndoItem());
150-
}
151-
this.resetState(samples, sampleRate);
152-
this.props.vm.updateSoundBuffer(
153-
this.props.soundIndex,
154-
this.audioBufferPlayer.buffer,
155-
new Uint8Array(wavBuffer));
156-
return true; // Edit was successful
157-
})
152+
this.resetState(newSamples, newSampleRate);
153+
this.props.vm.updateSoundBuffer(
154+
this.props.soundIndex,
155+
this.audioBufferPlayer.buffer,
156+
new Uint8Array(wavBuffer));
157+
return true; // Edit was successful
158+
})
159+
)
158160
.catch(e => {
159161
// Encoding failed, or the sound was too large to save so edit is rejected
160162
log.error(`Encountered error while trying to encode sound update: ${e}`);

src/lib/audio/audio-util.js

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,9 +59,28 @@ const encodeAndAddSoundToVM = function (vm, samples, sampleRate, name, callback)
5959
});
6060
};
6161

62+
const downsampleIfNeeded = (samples, sampleRate, resampler) => {
63+
const duration = samples.length / sampleRate;
64+
const encodedByteLength = samples.length * 2; /* bitDepth 16 bit */
65+
// Resolve immediately if already within byte limit
66+
if (encodedByteLength < SOUND_BYTE_LIMIT) {
67+
return Promise.resolve({samples, sampleRate});
68+
}
69+
// If encodeable at 22khz, resample and call submitNewSamples again
70+
if (duration * 22050 * 2 < SOUND_BYTE_LIMIT) {
71+
return resampler({samples, sampleRate}, 22050);
72+
}
73+
// If encodeable at 11khz, resample and call submitNewSamples again
74+
if (duration * 11025 * 2 < SOUND_BYTE_LIMIT) {
75+
return resampler({samples, sampleRate}, 11025);
76+
}
77+
// Cannot save this sound even at 11khz, refuse to edit
78+
return Promise.reject('Sound too large to save, refusing to edit');
79+
};
80+
6281
export {
6382
computeRMS,
6483
computeChunkedRMS,
6584
encodeAndAddSoundToVM,
66-
SOUND_BYTE_LIMIT
85+
downsampleIfNeeded
6786
};

test/unit/util/audio-util.test.js

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import {computeRMS, computeChunkedRMS} from '../../../src/lib/audio/audio-util';
1+
import {computeRMS, computeChunkedRMS, downsampleIfNeeded} from '../../../src/lib/audio/audio-util';
22

33
describe('computeRMS', () => {
44
test('returns 0 when given no samples', () => {
@@ -49,3 +49,37 @@ describe('computeChunkedRMS', () => {
4949
expect(chunkedLevels).toEqual([Math.sqrt(1 / 0.55), Math.sqrt(1 / 0.55)]);
5050
});
5151
});
52+
53+
describe('downsampleIfNeeded', () => {
54+
const samples = {length: 1};
55+
const sampleRate = 44100;
56+
test('returns given data when no downsampling needed', async () => {
57+
samples.length = 1;
58+
const res = await downsampleIfNeeded(samples, sampleRate, null);
59+
expect(res.samples).toEqual(samples);
60+
expect(res.sampleRate).toEqual(sampleRate);
61+
});
62+
test('downsamples to 22050 if that puts it under the limit', async () => {
63+
samples.length = 44100 * 3 * 60;
64+
const resampler = jest.fn(() => 'TEST');
65+
const res = await downsampleIfNeeded(samples, sampleRate, resampler);
66+
expect(resampler).toHaveBeenCalledWith({samples, sampleRate}, 22050);
67+
expect(res).toEqual('TEST');
68+
});
69+
test('downsamples to 11025 if that puts it under the limit', async () => {
70+
samples.length = 44100 * 7 * 60;
71+
const resampler = jest.fn(() => 'TEST');
72+
const res = await downsampleIfNeeded(samples, sampleRate, resampler);
73+
expect(resampler).toHaveBeenCalledWith({samples, sampleRate}, 11025);
74+
expect(res).toEqual('TEST');
75+
});
76+
77+
test('fails if resampling would not put it under the limit', async () => {
78+
samples.length = 44100 * 8 * 60;
79+
try {
80+
await downsampleIfNeeded(samples, sampleRate, null);
81+
} catch (e) {
82+
expect(e).toEqual('Sound too large to save, refusing to edit');
83+
}
84+
});
85+
});

0 commit comments

Comments
 (0)