@@ -6,7 +6,12 @@ import VM from 'scratch-vm';
6
6
7
7
import { connect } from 'react-redux' ;
8
8
9
- import { computeChunkedRMS , encodeAndAddSoundToVM , SOUND_BYTE_LIMIT } from '../lib/audio/audio-util.js' ;
9
+ import {
10
+ computeChunkedRMS ,
11
+ encodeAndAddSoundToVM ,
12
+ downsampleIfNeeded ,
13
+ dropEveryOtherSample
14
+ } from '../lib/audio/audio-util.js' ;
10
15
import AudioEffects from '../lib/audio/audio-effects.js' ;
11
16
import SoundEditorComponent from '../components/sound-editor/sound-editor.jsx' ;
12
17
import AudioBufferPlayer from '../lib/audio/audio-buffer-player.js' ;
@@ -39,7 +44,8 @@ class SoundEditor extends React.Component {
39
44
'paste' ,
40
45
'handleKeyPress' ,
41
46
'handleContainerClick' ,
42
- 'setRef'
47
+ 'setRef' ,
48
+ 'resampleBufferToRate'
43
49
] ) ;
44
50
this . state = {
45
51
copyBuffer : null ,
@@ -132,45 +138,32 @@ class SoundEditor extends React.Component {
132
138
} ) ;
133
139
}
134
140
submitNewSamples ( samples , sampleRate , skipUndo ) {
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 ]
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
141
166
} ) ;
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
174
167
}
175
168
handlePlay ( ) {
176
169
this . audioBufferPlayer . stop ( ) ;
@@ -209,13 +202,15 @@ class SoundEditor extends React.Component {
209
202
newSamples . set ( firstPart , 0 ) ;
210
203
newSamples . set ( secondPart , firstPart . length ) ;
211
204
}
212
- this . submitNewSamples ( newSamples , sampleRate ) ;
213
- this . setState ( {
214
- trimStart : null ,
215
- trimEnd : null
205
+ this . submitNewSamples ( newSamples , sampleRate ) . then ( ( ) => {
206
+ this . setState ( {
207
+ trimStart : null ,
208
+ trimEnd : null
209
+ } ) ;
216
210
} ) ;
217
211
}
218
212
handleDeleteInverse ( ) {
213
+ // Delete everything outside of the trimmers
219
214
const { samples, sampleRate} = this . copyCurrentBuffer ( ) ;
220
215
const sampleCount = samples . length ;
221
216
const startIndex = Math . floor ( this . state . trimStart * sampleCount ) ;
@@ -224,10 +219,13 @@ class SoundEditor extends React.Component {
224
219
if ( clippedSamples . length === 0 ) {
225
220
clippedSamples = new Float32Array ( 1 ) ;
226
221
}
227
- this . submitNewSamples ( clippedSamples , sampleRate ) ;
228
- this . setState ( {
229
- trimStart : null ,
230
- trimEnd : null
222
+ this . submitNewSamples ( clippedSamples , sampleRate ) . then ( success => {
223
+ if ( success ) {
224
+ this . setState ( {
225
+ trimStart : null ,
226
+ trimEnd : null
227
+ } ) ;
228
+ }
231
229
} ) ;
232
230
}
233
231
handleUpdateTrim ( trimStart , trimEnd ) {
@@ -257,14 +255,15 @@ class SoundEditor extends React.Component {
257
255
effects . process ( ( renderedBuffer , adjustedTrimStart , adjustedTrimEnd ) => {
258
256
const samples = renderedBuffer . getChannelData ( 0 ) ;
259
257
const sampleRate = renderedBuffer . sampleRate ;
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 ) ;
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
+ }
266
265
}
267
- }
266
+ } ) ;
268
267
} ) ;
269
268
}
270
269
tooLoud ( ) {
@@ -287,16 +286,22 @@ class SoundEditor extends React.Component {
287
286
this . redoStack . push ( this . getUndoItem ( ) ) ;
288
287
const { samples, sampleRate, trimStart, trimEnd} = this . undoStack . pop ( ) ;
289
288
if ( samples ) {
290
- this . submitNewSamples ( samples , sampleRate , true ) ;
291
- this . setState ( { trimStart : trimStart , trimEnd : trimEnd } , this . handlePlay ) ;
289
+ return this . submitNewSamples ( samples , sampleRate , true ) . then ( success => {
290
+ if ( success ) {
291
+ this . setState ( { trimStart : trimStart , trimEnd : trimEnd } , this . handlePlay ) ;
292
+ }
293
+ } ) ;
292
294
}
293
295
}
294
296
handleRedo ( ) {
295
297
const { samples, sampleRate, trimStart, trimEnd} = this . redoStack . pop ( ) ;
296
298
if ( samples ) {
297
299
this . undoStack . push ( this . getUndoItem ( ) ) ;
298
- this . submitNewSamples ( samples , sampleRate , true ) ;
299
- this . setState ( { trimStart : trimStart , trimEnd : trimEnd } , this . handlePlay ) ;
300
+ return this . submitNewSamples ( samples , sampleRate , true ) . then ( success => {
301
+ if ( success ) {
302
+ this . setState ( { trimStart : trimStart , trimEnd : trimEnd } , this . handlePlay ) ;
303
+ }
304
+ } ) ;
300
305
}
301
306
}
302
307
handleCopy ( ) {
@@ -322,25 +327,39 @@ class SoundEditor extends React.Component {
322
327
} ) ;
323
328
}
324
329
resampleBufferToRate ( buffer , newRate ) {
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
- } ;
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' ) ;
343
349
}
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
+ } ;
344
363
} ) ;
345
364
}
346
365
paste ( ) {
@@ -351,8 +370,11 @@ class SoundEditor extends React.Component {
351
370
const newSamples = new Float32Array ( newLength ) ;
352
371
newSamples . set ( samples , 0 ) ;
353
372
newSamples . set ( this . state . copyBuffer . samples , samples . length ) ;
354
- this . submitNewSamples ( newSamples , this . props . sampleRate , false ) ;
355
- this . handlePlay ( ) ;
373
+ this . submitNewSamples ( newSamples , this . props . sampleRate , false ) . then ( success => {
374
+ if ( success ) {
375
+ this . handlePlay ( ) ;
376
+ }
377
+ } ) ;
356
378
} else {
357
379
// else replace the selection with the pasted sound
358
380
const trimStartSamples = this . state . trimStart * samples . length ;
@@ -371,11 +393,14 @@ class SoundEditor extends React.Component {
371
393
const newDurationSeconds = newSamples . length / this . state . copyBuffer . sampleRate ;
372
394
const adjustedTrimStart = trimStartSeconds / newDurationSeconds ;
373
395
const adjustedTrimEnd = trimEndSeconds / newDurationSeconds ;
374
- this . submitNewSamples ( newSamples , this . props . sampleRate , false ) ;
375
- this . setState ( {
376
- trimStart : adjustedTrimStart ,
377
- trimEnd : adjustedTrimEnd
378
- } , this . handlePlay ) ;
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
+ } ) ;
379
404
}
380
405
}
381
406
handlePaste ( ) {
0 commit comments