Skip to content

Commit 1dcba6e

Browse files
authored
Merge pull request scratchfoundation#5376 from LLK/revert-5338-fix-long-sound
Revert "Implement sound downsampler to try to allow more sounds to be edited"
2 parents e476548 + 79277a1 commit 1dcba6e

File tree

5 files changed

+99
-221
lines changed

5 files changed

+99
-221
lines changed

src/containers/sound-editor.jsx

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

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

9-
import {
10-
computeChunkedRMS,
11-
encodeAndAddSoundToVM,
12-
downsampleIfNeeded,
13-
dropEveryOtherSample
14-
} from '../lib/audio/audio-util.js';
9+
import {computeChunkedRMS, encodeAndAddSoundToVM, SOUND_BYTE_LIMIT} from '../lib/audio/audio-util.js';
1510
import AudioEffects from '../lib/audio/audio-effects.js';
1611
import SoundEditorComponent from '../components/sound-editor/sound-editor.jsx';
1712
import AudioBufferPlayer from '../lib/audio/audio-buffer-player.js';
@@ -44,8 +39,7 @@ class SoundEditor extends React.Component {
4439
'paste',
4540
'handleKeyPress',
4641
'handleContainerClick',
47-
'setRef',
48-
'resampleBufferToRate'
42+
'setRef'
4943
]);
5044
this.state = {
5145
copyBuffer: null,
@@ -138,32 +132,45 @@ class SoundEditor extends React.Component {
138132
});
139133
}
140134
submitNewSamples (samples, sampleRate, skipUndo) {
141-
return downsampleIfNeeded({samples, sampleRate}, this.resampleBufferToRate)
142-
.then(({samples: newSamples, sampleRate: newSampleRate}) =>
143-
WavEncoder.encode({
144-
sampleRate: newSampleRate,
145-
channelData: [newSamples]
146-
}).then(wavBuffer => {
147-
if (!skipUndo) {
148-
this.redoStack = [];
149-
if (this.undoStack.length >= UNDO_STACK_SIZE) {
150-
this.undoStack.shift(); // Drop the first element off the array
151-
}
152-
this.undoStack.push(this.getUndoItem());
153-
}
154-
this.resetState(newSamples, newSampleRate);
155-
this.props.vm.updateSoundBuffer(
156-
this.props.soundIndex,
157-
this.audioBufferPlayer.buffer,
158-
new Uint8Array(wavBuffer));
159-
return true; // Edit was successful
160-
})
161-
)
162-
.catch(e => {
163-
// Encoding failed, or the sound was too large to save so edit is rejected
164-
log.error(`Encountered error while trying to encode sound update: ${e}`);
165-
return false; // Edit was not applied
135+
// Encode the new sound into a wav so that it can be stored
136+
let wavBuffer = null;
137+
try {
138+
wavBuffer = WavEncoder.encode.sync({
139+
sampleRate: sampleRate,
140+
channelData: [samples]
166141
});
142+
143+
if (wavBuffer.byteLength > SOUND_BYTE_LIMIT) {
144+
// Cancel the sound update by setting to null
145+
wavBuffer = null;
146+
log.error(`Refusing to encode sound larger than ${SOUND_BYTE_LIMIT} bytes`);
147+
}
148+
} catch (e) {
149+
// This error state is mostly for the mock sounds used during testing.
150+
// Any incorrect sound buffer trying to get interpretd as a Wav file
151+
// should yield this error.
152+
// This can also happen if the sound is too be allocated in memory.
153+
log.error(`Encountered error while trying to encode sound update: ${e}`);
154+
}
155+
156+
// Do not submit sound if it could not be encoded (i.e. if too large)
157+
if (wavBuffer) {
158+
if (!skipUndo) {
159+
this.redoStack = [];
160+
if (this.undoStack.length >= UNDO_STACK_SIZE) {
161+
this.undoStack.shift(); // Drop the first element off the array
162+
}
163+
this.undoStack.push(this.getUndoItem());
164+
}
165+
this.resetState(samples, sampleRate);
166+
this.props.vm.updateSoundBuffer(
167+
this.props.soundIndex,
168+
this.audioBufferPlayer.buffer,
169+
new Uint8Array(wavBuffer));
170+
171+
return true; // Update succeeded
172+
}
173+
return false; // Update failed
167174
}
168175
handlePlay () {
169176
this.audioBufferPlayer.stop();
@@ -202,15 +209,13 @@ class SoundEditor extends React.Component {
202209
newSamples.set(firstPart, 0);
203210
newSamples.set(secondPart, firstPart.length);
204211
}
205-
this.submitNewSamples(newSamples, sampleRate).then(() => {
206-
this.setState({
207-
trimStart: null,
208-
trimEnd: null
209-
});
212+
this.submitNewSamples(newSamples, sampleRate);
213+
this.setState({
214+
trimStart: null,
215+
trimEnd: null
210216
});
211217
}
212218
handleDeleteInverse () {
213-
// Delete everything outside of the trimmers
214219
const {samples, sampleRate} = this.copyCurrentBuffer();
215220
const sampleCount = samples.length;
216221
const startIndex = Math.floor(this.state.trimStart * sampleCount);
@@ -219,13 +224,10 @@ class SoundEditor extends React.Component {
219224
if (clippedSamples.length === 0) {
220225
clippedSamples = new Float32Array(1);
221226
}
222-
this.submitNewSamples(clippedSamples, sampleRate).then(success => {
223-
if (success) {
224-
this.setState({
225-
trimStart: null,
226-
trimEnd: null
227-
});
228-
}
227+
this.submitNewSamples(clippedSamples, sampleRate);
228+
this.setState({
229+
trimStart: null,
230+
trimEnd: null
229231
});
230232
}
231233
handleUpdateTrim (trimStart, trimEnd) {
@@ -255,15 +257,14 @@ class SoundEditor extends React.Component {
255257
effects.process((renderedBuffer, adjustedTrimStart, adjustedTrimEnd) => {
256258
const samples = renderedBuffer.getChannelData(0);
257259
const sampleRate = renderedBuffer.sampleRate;
258-
this.submitNewSamples(samples, sampleRate).then(success => {
259-
if (success) {
260-
if (this.state.trimStart === null) {
261-
this.handlePlay();
262-
} else {
263-
this.setState({trimStart: adjustedTrimStart, trimEnd: adjustedTrimEnd}, this.handlePlay);
264-
}
260+
const success = this.submitNewSamples(samples, sampleRate);
261+
if (success) {
262+
if (this.state.trimStart === null) {
263+
this.handlePlay();
264+
} else {
265+
this.setState({trimStart: adjustedTrimStart, trimEnd: adjustedTrimEnd}, this.handlePlay);
265266
}
266-
});
267+
}
267268
});
268269
}
269270
tooLoud () {
@@ -286,22 +287,16 @@ class SoundEditor extends React.Component {
286287
this.redoStack.push(this.getUndoItem());
287288
const {samples, sampleRate, trimStart, trimEnd} = this.undoStack.pop();
288289
if (samples) {
289-
return this.submitNewSamples(samples, sampleRate, true).then(success => {
290-
if (success) {
291-
this.setState({trimStart: trimStart, trimEnd: trimEnd}, this.handlePlay);
292-
}
293-
});
290+
this.submitNewSamples(samples, sampleRate, true);
291+
this.setState({trimStart: trimStart, trimEnd: trimEnd}, this.handlePlay);
294292
}
295293
}
296294
handleRedo () {
297295
const {samples, sampleRate, trimStart, trimEnd} = this.redoStack.pop();
298296
if (samples) {
299297
this.undoStack.push(this.getUndoItem());
300-
return this.submitNewSamples(samples, sampleRate, true).then(success => {
301-
if (success) {
302-
this.setState({trimStart: trimStart, trimEnd: trimEnd}, this.handlePlay);
303-
}
304-
});
298+
this.submitNewSamples(samples, sampleRate, true);
299+
this.setState({trimStart: trimStart, trimEnd: trimEnd}, this.handlePlay);
305300
}
306301
}
307302
handleCopy () {
@@ -327,39 +322,25 @@ class SoundEditor extends React.Component {
327322
});
328323
}
329324
resampleBufferToRate (buffer, newRate) {
330-
return new Promise((resolve, reject) => {
331-
const sampleRateRatio = newRate / buffer.sampleRate;
332-
const newLength = sampleRateRatio * buffer.samples.length;
333-
let offlineContext;
334-
// Try to use either OfflineAudioContext or webkitOfflineAudioContext to resample
335-
// The constructors will throw if trying to resample at an unsupported rate
336-
// (e.g. Safari/webkitOAC does not support lower than 44khz).
337-
try {
338-
if (window.OfflineAudioContext) {
339-
offlineContext = new window.OfflineAudioContext(1, newLength, newRate);
340-
} else if (window.webkitOfflineAudioContext) {
341-
offlineContext = new window.webkitOfflineAudioContext(1, newLength, newRate);
342-
}
343-
} catch {
344-
// If no OAC available and downsampling by 2, downsample by dropping every other sample.
345-
if (newRate === buffer.sampleRate / 2) {
346-
return resolve(dropEveryOtherSample(buffer));
347-
}
348-
return reject('Could not resample');
325+
return new Promise(resolve => {
326+
if (window.OfflineAudioContext) {
327+
const sampleRateRatio = newRate / buffer.sampleRate;
328+
const newLength = sampleRateRatio * buffer.samples.length;
329+
const offlineContext = new window.OfflineAudioContext(1, newLength, newRate);
330+
const source = offlineContext.createBufferSource();
331+
const audioBuffer = offlineContext.createBuffer(1, buffer.samples.length, buffer.sampleRate);
332+
audioBuffer.getChannelData(0).set(buffer.samples);
333+
source.buffer = audioBuffer;
334+
source.connect(offlineContext.destination);
335+
source.start();
336+
offlineContext.startRendering();
337+
offlineContext.oncomplete = ({renderedBuffer}) => {
338+
resolve({
339+
samples: renderedBuffer.getChannelData(0),
340+
sampleRate: newRate
341+
});
342+
};
349343
}
350-
const source = offlineContext.createBufferSource();
351-
const audioBuffer = offlineContext.createBuffer(1, buffer.samples.length, buffer.sampleRate);
352-
audioBuffer.getChannelData(0).set(buffer.samples);
353-
source.buffer = audioBuffer;
354-
source.connect(offlineContext.destination);
355-
source.start();
356-
offlineContext.startRendering();
357-
offlineContext.oncomplete = ({renderedBuffer}) => {
358-
resolve({
359-
samples: renderedBuffer.getChannelData(0),
360-
sampleRate: newRate
361-
});
362-
};
363344
});
364345
}
365346
paste () {
@@ -370,11 +351,8 @@ class SoundEditor extends React.Component {
370351
const newSamples = new Float32Array(newLength);
371352
newSamples.set(samples, 0);
372353
newSamples.set(this.state.copyBuffer.samples, samples.length);
373-
this.submitNewSamples(newSamples, this.props.sampleRate, false).then(success => {
374-
if (success) {
375-
this.handlePlay();
376-
}
377-
});
354+
this.submitNewSamples(newSamples, this.props.sampleRate, false);
355+
this.handlePlay();
378356
} else {
379357
// else replace the selection with the pasted sound
380358
const trimStartSamples = this.state.trimStart * samples.length;
@@ -393,14 +371,11 @@ class SoundEditor extends React.Component {
393371
const newDurationSeconds = newSamples.length / this.state.copyBuffer.sampleRate;
394372
const adjustedTrimStart = trimStartSeconds / newDurationSeconds;
395373
const adjustedTrimEnd = trimEndSeconds / newDurationSeconds;
396-
this.submitNewSamples(newSamples, this.props.sampleRate, false).then(success => {
397-
if (success) {
398-
this.setState({
399-
trimStart: adjustedTrimStart,
400-
trimEnd: adjustedTrimEnd
401-
}, this.handlePlay);
402-
}
403-
});
374+
this.submitNewSamples(newSamples, this.props.sampleRate, false);
375+
this.setState({
376+
trimStart: adjustedTrimStart,
377+
trimEnd: adjustedTrimEnd
378+
}, this.handlePlay);
404379
}
405380
}
406381
handlePaste () {

src/lib/audio/audio-util.js

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

62-
/**
63-
@typedef SoundBuffer
64-
@type {Object}
65-
@property {Float32Array} samples Array of audio samples
66-
@property {number} sampleRate Audio sample rate
67-
*/
68-
69-
/**
70-
* Downsample the given buffer to try to reduce file size below SOUND_BYTE_LIMIT
71-
* @param {SoundBuffer} buffer - Buffer to resample
72-
* @param {function(SoundBuffer):Promise<SoundBuffer>} resampler - resampler function
73-
* @returns {SoundBuffer} Downsampled buffer with half the sample rate
74-
*/
75-
const downsampleIfNeeded = (buffer, resampler) => {
76-
const {samples, sampleRate} = buffer;
77-
const duration = samples.length / sampleRate;
78-
const encodedByteLength = samples.length * 2; /* bitDepth 16 bit */
79-
// Resolve immediately if already within byte limit
80-
if (encodedByteLength < SOUND_BYTE_LIMIT) {
81-
return Promise.resolve({samples, sampleRate});
82-
}
83-
// If encodeable at 22khz, resample and call submitNewSamples again
84-
if (duration * 22050 * 2 < SOUND_BYTE_LIMIT) {
85-
return resampler({samples, sampleRate}, 22050);
86-
}
87-
// Cannot save this sound at 22khz, refuse to edit
88-
// In the future we could introduce further compression here
89-
return Promise.reject('Sound too large to save, refusing to edit');
90-
};
91-
92-
/**
93-
* Drop every other sample of an audio buffer as a last-resort way of downsampling.
94-
* @param {SoundBuffer} buffer - Buffer to resample
95-
* @returns {SoundBuffer} Downsampled buffer with half the sample rate
96-
*/
97-
const dropEveryOtherSample = buffer => {
98-
const newLength = Math.floor(buffer.samples.length / 2);
99-
const newSamples = new Float32Array(newLength);
100-
for (let i = 0; i < newLength; i++) {
101-
newSamples[i] = buffer.samples[i * 2];
102-
}
103-
return {
104-
samples: newSamples,
105-
sampleRate: buffer.rate / 2
106-
};
107-
};
108-
10962
export {
11063
computeRMS,
11164
computeChunkedRMS,
11265
encodeAndAddSoundToVM,
113-
downsampleIfNeeded,
114-
dropEveryOtherSample
66+
SOUND_BYTE_LIMIT
11567
};

test/__mocks__/audio-effects.js

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,7 @@ export default class MockAudioEffects {
1414
this.buffer = buffer;
1515
this.name = name;
1616
this.process = jest.fn(done => {
17-
this._finishProcessing = renderedBuffer => {
18-
done(renderedBuffer, 0, 1);
19-
return new Promise(resolve => setTimeout(resolve));
20-
};
17+
this._finishProcessing = renderedBuffer => done(renderedBuffer, 0, 1);
2118
});
2219
MockAudioEffects.instance = this;
2320
}

0 commit comments

Comments
 (0)