Merge pull request #4974 from ericrosenbaum/feature/sound-editor-selection4

Sound editor update: selection-based editing
This commit is contained in:
Eric Rosenbaum authored and GitHub committed 2019-07-30 15:27:04 -04:00
commit 7d1dfb67b7
40 files changed
+1411 -494

No files matched your search

+155
View File
@@ -0,0 +1,155 @@
import React from 'react';
import PropTypes from 'prop-types';
import bindAll from 'lodash.bindall';
import AudioSelectorComponent from '../components/audio-trimmer/audio-selector.jsx';
import {getEventXY} from '../lib/touch-utils';
import DragRecognizer from '../lib/drag-recognizer';
const MIN_LENGTH = 0.01;
const MIN_DURATION = 500;
class AudioSelector extends React.Component {
constructor (props) {
super(props);
bindAll(this, [
'handleNewSelectionMouseDown',
'handleTrimStartMouseDown',
'handleTrimEndMouseDown',
'handleTrimStartMouseMove',
'handleTrimEndMouseMove',
'handleTrimStartMouseUp',
'handleTrimEndMouseUp',
'storeRef'
]);
this.state = {
trimStart: props.trimStart,
trimEnd: props.trimEnd
};
this.clickStartTime = 0;
this.trimStartDragRecognizer = new DragRecognizer({
onDrag: this.handleTrimStartMouseMove,
onDragEnd: this.handleTrimStartMouseUp,
touchDragAngle: 90,
distanceThreshold: 0
});
this.trimEndDragRecognizer = new DragRecognizer({
onDrag: this.handleTrimEndMouseMove,
onDragEnd: this.handleTrimEndMouseUp,
touchDragAngle: 90,
distanceThreshold: 0
});
}
componentWillReceiveProps (newProps) {
if (newProps.trimStart === this.props.trimStart) return;
this.setState({
trimStart: newProps.trimStart,
trimEnd: newProps.trimEnd
});
}
clearSelection () {
this.props.onSetTrim(null, null);
}
handleNewSelectionMouseDown (e) {
const {width, left} = this.containerElement.getBoundingClientRect();
this.initialTrimEnd = (getEventXY(e).x - left) / width;
this.initialTrimStart = this.initialTrimEnd;
this.props.onSetTrim(this.initialTrimStart, this.initialTrimEnd);
this.clickStartTime = Date.now();
this.containerSize = width;
this.trimEndDragRecognizer.start(e);
e.preventDefault();
}
handleTrimStartMouseMove (currentOffset, initialOffset) {
const dx = (currentOffset.x - initialOffset.x) / this.containerSize;
const newTrim = Math.max(0, Math.min(1, this.initialTrimStart + dx));
if (newTrim > this.initialTrimEnd) {
this.setState({
trimStart: this.initialTrimEnd,
trimEnd: newTrim
});
} else {
this.setState({
trimStart: newTrim,
trimEnd: this.initialTrimEnd
});
}
}
handleTrimEndMouseMove (currentOffset, initialOffset) {
const dx = (currentOffset.x - initialOffset.x) / this.containerSize;
const newTrim = Math.min(1, Math.max(0, this.initialTrimEnd + dx));
if (newTrim < this.initialTrimStart) {
this.setState({
trimStart: newTrim,
trimEnd: this.initialTrimStart
});
} else {
this.setState({
trimStart: this.initialTrimStart,
trimEnd: newTrim
});
}
}
handleTrimStartMouseUp () {
this.props.onSetTrim(this.state.trimStart, this.state.trimEnd);
}
handleTrimEndMouseUp () {
// If the selection was made quickly (tooFast) and is small (tooShort),
// deselect instead. This allows click-to-deselect even if you drag
// a little bit by accident. It also allows very quickly making a
// selection, as long as it is above a minimum length.
const tooFast = (Date.now() - this.clickStartTime) < MIN_DURATION;
const tooShort = (this.state.trimEnd - this.state.trimStart) < MIN_LENGTH;
if (tooFast && tooShort) {
this.clearSelection();
} else {
this.props.onSetTrim(this.state.trimStart, this.state.trimEnd);
}
}
handleTrimStartMouseDown (e) {
this.containerSize = this.containerElement.getBoundingClientRect().width;
this.trimStartDragRecognizer.start(e);
this.initialTrimStart = this.props.trimStart;
this.initialTrimEnd = this.props.trimEnd;
e.stopPropagation();
e.preventDefault();
}
handleTrimEndMouseDown (e) {
this.containerSize = this.containerElement.getBoundingClientRect().width;
this.trimEndDragRecognizer.start(e);
this.initialTrimEnd = this.props.trimEnd;
this.initialTrimStart = this.props.trimStart;
e.stopPropagation();
e.preventDefault();
}
storeRef (el) {
this.containerElement = el;
}
render () {
return (
<AudioSelectorComponent
containerRef={this.storeRef}
playhead={this.props.playhead}
trimEnd={this.state.trimEnd}
trimStart={this.state.trimStart}
onNewSelectionMouseDown={this.handleNewSelectionMouseDown}
onTrimEndMouseDown={this.handleTrimEndMouseDown}
onTrimStartMouseDown={this.handleTrimStartMouseDown}
/>
);
}
}
AudioSelector.propTypes = {
onSetTrim: PropTypes.func,
playhead: PropTypes.number,
trimEnd: PropTypes.number,
trimStart: PropTypes.number
};
export default AudioSelector;
+20 -33
View File
@@ -2,7 +2,7 @@ import React from 'react';
import PropTypes from 'prop-types';
import bindAll from 'lodash.bindall';
import AudioTrimmerComponent from '../components/audio-trimmer/audio-trimmer.jsx';
import {getEventXY} from '../lib/touch-utils';
import DragRecognizer from '../lib/drag-recognizer';
const MIN_LENGTH = 0.01; // Used to stop sounds being trimmed smaller than 1%
@@ -14,52 +14,39 @@ class AudioTrimmer extends React.Component {
'handleTrimEndMouseDown',
'handleTrimStartMouseMove',
'handleTrimEndMouseMove',
'handleTrimStartMouseUp',
'handleTrimEndMouseUp',
'storeRef'
]);
this.trimStartDragRecognizer = new DragRecognizer({
onDrag: this.handleTrimStartMouseMove,
touchDragAngle: 90,
distanceThreshold: 0
});
this.trimEndDragRecognizer = new DragRecognizer({
onDrag: this.handleTrimEndMouseMove,
touchDragAngle: 90,
distanceThreshold: 0
});
}
handleTrimStartMouseMove (e) {
const containerSize = this.containerElement.getBoundingClientRect().width;
const dx = (getEventXY(e).x - this.initialX) / containerSize;
handleTrimStartMouseMove (currentOffset, initialOffset) {
const dx = (currentOffset.x - initialOffset.x) / this.containerSize;
const newTrim = Math.max(0, Math.min(this.props.trimEnd - MIN_LENGTH, this.initialTrim + dx));
this.props.onSetTrimStart(newTrim);
e.preventDefault();
}
handleTrimEndMouseMove (e) {
const containerSize = this.containerElement.getBoundingClientRect().width;
const dx = (getEventXY(e).x - this.initialX) / containerSize;
handleTrimEndMouseMove (currentOffset, initialOffset) {
const dx = (currentOffset.x - initialOffset.x) / this.containerSize;
const newTrim = Math.min(1, Math.max(this.props.trimStart + MIN_LENGTH, this.initialTrim + dx));
this.props.onSetTrimEnd(newTrim);
e.preventDefault();
}
handleTrimStartMouseUp () {
window.removeEventListener('mousemove', this.handleTrimStartMouseMove);
window.removeEventListener('mouseup', this.handleTrimStartMouseUp);
window.removeEventListener('touchmove', this.handleTrimStartMouseMove);
window.removeEventListener('touchend', this.handleTrimStartMouseUp);
}
handleTrimEndMouseUp () {
window.removeEventListener('mousemove', this.handleTrimEndMouseMove);
window.removeEventListener('mouseup', this.handleTrimEndMouseUp);
window.removeEventListener('touchmove', this.handleTrimEndMouseMove);
window.removeEventListener('touchend', this.handleTrimEndMouseUp);
}
handleTrimStartMouseDown (e) {
this.initialX = getEventXY(e).x;
this.containerSize = this.containerElement.getBoundingClientRect().width;
this.trimStartDragRecognizer.start(e);
this.initialTrim = this.props.trimStart;
window.addEventListener('mousemove', this.handleTrimStartMouseMove);
window.addEventListener('mouseup', this.handleTrimStartMouseUp);
window.addEventListener('touchmove', this.handleTrimStartMouseMove);
window.addEventListener('touchend', this.handleTrimStartMouseUp);
}
handleTrimEndMouseDown (e) {
this.initialX = getEventXY(e).x;
this.containerSize = this.containerElement.getBoundingClientRect().width;
this.trimEndDragRecognizer.start(e);
this.initialTrim = this.props.trimEnd;
window.addEventListener('mousemove', this.handleTrimEndMouseMove);
window.addEventListener('mouseup', this.handleTrimEndMouseUp);
window.addEventListener('touchmove', this.handleTrimEndMouseMove);
window.addEventListener('touchend', this.handleTrimEndMouseUp);
}
storeRef (el) {
this.containerElement = el;
+3 -30
View File
@@ -2,8 +2,8 @@ import bindAll from 'lodash.bindall';
import PropTypes from 'prop-types';
import React from 'react';
import VM from 'scratch-vm';
import WavEncoder from 'wav-encoder';
import {connect} from 'react-redux';
import {encodeAndAddSoundToVM} from '../lib/audio/audio-util.js';
import RecordModalComponent from '../components/record-modal/record-modal.jsx';
@@ -71,39 +71,12 @@ class RecordModal extends React.Component {
const startIndex = Math.floor(this.state.trimStart * sampleCount);
const endIndex = Math.floor(this.state.trimEnd * sampleCount);
const clippedSamples = this.state.samples.slice(startIndex, endIndex);
WavEncoder.encode({
sampleRate: this.state.sampleRate,
channelData: [clippedSamples]
}).then(wavBuffer => {
const vmSound = {
format: '',
dataFormat: 'wav',
rate: this.state.sampleRate,
sampleCount: clippedSamples.length
};
// Create an asset from the encoded .wav and get resulting md5
const storage = this.props.vm.runtime.storage;
vmSound.asset = storage.createAsset(
storage.AssetType.Sound,
storage.DataFormat.WAV,
new Uint8Array(wavBuffer),
null,
true // generate md5
);
vmSound.assetId = vmSound.asset.assetId;
// update vmSound object with md5 property
vmSound.md5 = `${vmSound.assetId}.${vmSound.dataFormat}`;
// The VM will update the sound name to a fresh name
// if the following is already taken
vmSound.name = 'recording1';
this.props.vm.addSound(vmSound).then(() => {
encodeAndAddSoundToVM(this.props.vm, clippedSamples, this.state.sampleRate, 'recording1',
() => {
this.props.onClose();
this.props.onNewSound();
});
});
});
}
handleCancel () {
+252 -45
View File
@@ -2,10 +2,11 @@ import bindAll from 'lodash.bindall';
import PropTypes from 'prop-types';
import React from 'react';
import WavEncoder from 'wav-encoder';
import VM from 'scratch-vm';
import {connect} from 'react-redux';
import {computeChunkedRMS, SOUND_BYTE_LIMIT} from '../lib/audio/audio-util.js';
import {computeChunkedRMS, encodeAndAddSoundToVM, SOUND_BYTE_LIMIT} 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';
@@ -17,21 +18,29 @@ class SoundEditor extends React.Component {
constructor (props) {
super(props);
bindAll(this, [
'copy',
'copyCurrentBuffer',
'handleCopyToNew',
'handleStoppedPlaying',
'handleChangeName',
'handlePlay',
'handleStopPlaying',
'handleUpdatePlayhead',
'handleActivateTrim',
'handleUpdateTrimEnd',
'handleUpdateTrimStart',
'handleDelete',
'handleUpdateTrim',
'handleEffect',
'handleUndo',
'handleRedo',
'submitNewSamples'
'submitNewSamples',
'handleCopy',
'handlePaste',
'paste',
'handleKeyPress',
'handleContainerClick',
'setRef'
]);
this.state = {
copyBuffer: null,
chunkLevels: computeChunkedRMS(this.props.samples),
playhead: null, // null is not playing, [0 -> 1] is playing percent
trimStart: null,
@@ -40,28 +49,84 @@ class SoundEditor extends React.Component {
this.redoStack = [];
this.undoStack = [];
this.ref = null;
}
componentDidMount () {
this.audioBufferPlayer = new AudioBufferPlayer(this.props.samples, this.props.sampleRate);
document.addEventListener('keydown', this.handleKeyPress);
}
componentWillReceiveProps (newProps) {
if (newProps.soundId !== this.props.soundId) { // A different sound has been selected
this.redoStack = [];
this.undoStack = [];
this.resetState(newProps.samples, newProps.sampleRate);
this.setState({
trimStart: null,
trimEnd: null
});
}
}
componentWillUnmount () {
this.audioBufferPlayer.stop();
document.removeEventListener('keydown', this.handleKeyPress);
}
handleKeyPress (event) {
if (event.target instanceof HTMLInputElement) {
// Ignore keyboard shortcuts if a text input field is focused
return;
}
if (event.key === ' ') {
event.preventDefault();
if (this.state.playhead) {
this.handleStopPlaying();
} else {
this.handlePlay();
}
}
if (event.key === 'Delete' || event.key === 'Backspace') {
event.preventDefault();
if (event.shiftKey) {
this.handleDeleteInverse();
} else {
this.handleDelete();
}
}
if (event.key === 'Escape') {
event.preventDefault();
this.handleUpdateTrim(null, null);
}
if (event.metaKey || event.ctrlKey) {
if (event.shiftKey && event.key.toLowerCase() === 'z') {
event.preventDefault();
if (this.redoStack.length > 0) {
this.handleRedo();
}
} else if (event.key === 'z') {
if (this.undoStack.length > 0) {
event.preventDefault();
this.handleUndo();
}
} else if (event.key === 'c') {
event.preventDefault();
this.handleCopy();
} else if (event.key === 'v') {
event.preventDefault();
this.handlePaste();
} else if (event.key === 'a') {
event.preventDefault();
this.handleUpdateTrim(0, 1);
}
}
}
resetState (samples, sampleRate) {
this.audioBufferPlayer.stop();
this.audioBufferPlayer = new AudioBufferPlayer(samples, sampleRate);
this.setState({
chunkLevels: computeChunkedRMS(samples),
playhead: null,
trimStart: null,
trimEnd: null
playhead: null
});
}
submitNewSamples (samples, sampleRate, skipUndo) {
@@ -93,7 +158,7 @@ class SoundEditor extends React.Component {
if (this.undoStack.length >= UNDO_STACK_SIZE) {
this.undoStack.shift(); // Drop the first element off the array
}
this.undoStack.push(this.copyCurrentBuffer());
this.undoStack.push(this.getUndoItem());
}
this.resetState(samples, sampleRate);
this.props.vm.updateSoundBuffer(
@@ -106,6 +171,7 @@ class SoundEditor extends React.Component {
return false; // Update failed
}
handlePlay () {
this.audioBufferPlayer.stop();
this.audioBufferPlayer.play(
this.state.trimStart || 0,
this.state.trimEnd || 1,
@@ -125,31 +191,46 @@ class SoundEditor extends React.Component {
handleChangeName (name) {
this.props.vm.renameSound(this.props.soundIndex, name);
}
handleActivateTrim () {
if (this.state.trimStart === null && this.state.trimEnd === null) {
this.setState({trimEnd: 0.95, trimStart: 0.05});
handleDelete () {
const {samples, sampleRate} = this.copyCurrentBuffer();
const sampleCount = samples.length;
const startIndex = Math.floor(this.state.trimStart * sampleCount);
const endIndex = Math.floor(this.state.trimEnd * sampleCount);
const firstPart = samples.slice(0, startIndex);
const secondPart = samples.slice(endIndex, sampleCount);
const newLength = firstPart.length + secondPart.length;
let newSamples;
if (newLength === 0) {
newSamples = new Float32Array(1);
} else {
const {samples, sampleRate} = this.copyCurrentBuffer();
const sampleCount = samples.length;
const startIndex = Math.floor(this.state.trimStart * sampleCount);
const endIndex = Math.floor(this.state.trimEnd * sampleCount);
if (endIndex > startIndex) { // Strictly greater to prevent 0 sample sounds
const clippedSamples = samples.slice(startIndex, endIndex);
this.submitNewSamples(clippedSamples, sampleRate);
} else {
// Just clear the trim state, it cannot be completed
this.setState({
trimStart: null,
trimEnd: null
});
}
newSamples = new Float32Array(newLength);
newSamples.set(firstPart, 0);
newSamples.set(secondPart, firstPart.length);
}
this.submitNewSamples(newSamples, sampleRate);
this.setState({
trimStart: null,
trimEnd: null
});
}
handleUpdateTrimEnd (trimEnd) {
this.setState({trimEnd});
handleDeleteInverse () {
const {samples, sampleRate} = this.copyCurrentBuffer();
const sampleCount = samples.length;
const startIndex = Math.floor(this.state.trimStart * sampleCount);
const endIndex = Math.floor(this.state.trimEnd * sampleCount);
let clippedSamples = samples.slice(startIndex, endIndex);
if (clippedSamples.length === 0) {
clippedSamples = new Float32Array(1);
}
this.submitNewSamples(clippedSamples, sampleRate);
this.setState({
trimStart: null,
trimEnd: null
});
}
handleUpdateTrimStart (trimStart) {
this.setState({trimStart});
handleUpdateTrim (trimStart, trimEnd) {
this.setState({trimStart, trimEnd});
this.handleStopPlaying();
}
effectFactory (name) {
return () => this.handleEffect(name);
@@ -162,52 +243,181 @@ class SoundEditor extends React.Component {
};
}
handleEffect (name) {
const effects = new AudioEffects(this.audioBufferPlayer.buffer, name);
effects.process(({renderedBuffer}) => {
const trimStart = this.state.trimStart === null ? 0.0 : this.state.trimStart;
const trimEnd = this.state.trimEnd === null ? 1.0 : this.state.trimEnd;
// Offline audio context needs at least 2 samples
if (this.audioBufferPlayer.buffer.length < 2) {
return;
}
const effects = new AudioEffects(this.audioBufferPlayer.buffer, name, trimStart, trimEnd);
effects.process((renderedBuffer, adjustedTrimStart, adjustedTrimEnd) => {
const samples = renderedBuffer.getChannelData(0);
const sampleRate = renderedBuffer.sampleRate;
const success = this.submitNewSamples(samples, sampleRate);
if (success) this.handlePlay();
if (success) {
if (this.state.trimStart === null) {
this.handlePlay();
} else {
this.setState({trimStart: adjustedTrimStart, trimEnd: adjustedTrimEnd}, this.handlePlay);
}
}
});
}
getUndoItem () {
return {
...this.copyCurrentBuffer(),
trimStart: this.state.trimStart,
trimEnd: this.state.trimEnd
};
}
handleUndo () {
this.redoStack.push(this.copyCurrentBuffer());
const {samples, sampleRate} = this.undoStack.pop();
this.redoStack.push(this.getUndoItem());
const {samples, sampleRate, trimStart, trimEnd} = this.undoStack.pop();
if (samples) {
this.submitNewSamples(samples, sampleRate, true);
this.handlePlay();
this.setState({trimStart: trimStart, trimEnd: trimEnd}, this.handlePlay);
}
}
handleRedo () {
const {samples, sampleRate} = this.redoStack.pop();
const {samples, sampleRate, trimStart, trimEnd} = this.redoStack.pop();
if (samples) {
this.undoStack.push(this.copyCurrentBuffer());
this.undoStack.push(this.getUndoItem());
this.submitNewSamples(samples, sampleRate, true);
this.setState({trimStart: trimStart, trimEnd: trimEnd}, this.handlePlay);
}
}
handleCopy () {
this.copy();
}
copy (callback) {
const trimStart = this.state.trimStart === null ? 0.0 : this.state.trimStart;
const trimEnd = this.state.trimEnd === null ? 1.0 : this.state.trimEnd;
const newCopyBuffer = this.copyCurrentBuffer();
const trimStartSamples = trimStart * newCopyBuffer.samples.length;
const trimEndSamples = trimEnd * newCopyBuffer.samples.length;
newCopyBuffer.samples = newCopyBuffer.samples.slice(trimStartSamples, trimEndSamples);
this.setState({
copyBuffer: newCopyBuffer
}, callback);
}
handleCopyToNew () {
this.copy(() => {
encodeAndAddSoundToVM(this.props.vm, this.state.copyBuffer.samples,
this.state.copyBuffer.sampleRate, this.props.name);
});
}
resampleBufferToRate (buffer, newRate) {
return new Promise(resolve => {
if (window.OfflineAudioContext) {
const sampleRateRatio = newRate / buffer.sampleRate;
const newLength = sampleRateRatio * buffer.samples.length;
const offlineContext = new window.OfflineAudioContext(1, newLength, newRate);
const source = offlineContext.createBufferSource();
const audioBuffer = offlineContext.createBuffer(1, buffer.samples.length, buffer.sampleRate);
audioBuffer.getChannelData(0).set(buffer.samples);
source.buffer = audioBuffer;
source.connect(offlineContext.destination);
source.start();
offlineContext.startRendering();
offlineContext.oncomplete = ({renderedBuffer}) => {
resolve({
samples: renderedBuffer.getChannelData(0),
sampleRate: newRate
});
};
}
});
}
paste () {
// If there's no selection, paste at the end of the sound
const {samples} = this.copyCurrentBuffer();
if (this.state.trimStart === null) {
const newLength = samples.length + this.state.copyBuffer.samples.length;
const newSamples = new Float32Array(newLength);
newSamples.set(samples, 0);
newSamples.set(this.state.copyBuffer.samples, samples.length);
this.submitNewSamples(newSamples, this.props.sampleRate, false);
this.handlePlay();
} else {
// else replace the selection with the pasted sound
const trimStartSamples = this.state.trimStart * samples.length;
const trimEndSamples = this.state.trimEnd * samples.length;
const firstPart = samples.slice(0, trimStartSamples);
const lastPart = samples.slice(trimEndSamples);
const newLength = firstPart.length + this.state.copyBuffer.samples.length + lastPart.length;
const newSamples = new Float32Array(newLength);
newSamples.set(firstPart, 0);
newSamples.set(this.state.copyBuffer.samples, firstPart.length);
newSamples.set(lastPart, firstPart.length + this.state.copyBuffer.samples.length);
const trimStartSeconds = trimStartSamples / this.props.sampleRate;
const trimEndSeconds = trimStartSeconds +
(this.state.copyBuffer.samples.length / this.state.copyBuffer.sampleRate);
const newDurationSeconds = newSamples.length / this.state.copyBuffer.sampleRate;
const adjustedTrimStart = trimStartSeconds / newDurationSeconds;
const adjustedTrimEnd = trimEndSeconds / newDurationSeconds;
this.submitNewSamples(newSamples, this.props.sampleRate, false);
this.setState({
trimStart: adjustedTrimStart,
trimEnd: adjustedTrimEnd
}, this.handlePlay);
}
}
handlePaste () {
if (!this.state.copyBuffer) return;
if (this.state.copyBuffer.sampleRate === this.props.sampleRate) {
this.paste();
} else {
this.resampleBufferToRate(this.state.copyBuffer, this.props.sampleRate).then(buffer => {
this.setState({
copyBuffer: buffer
}, this.paste);
});
}
}
setRef (element) {
this.ref = element;
}
handleContainerClick (e) {
// If the click is on the sound editor's div (and not any other element), delesect
if (e.target === this.ref && this.state.trimStart !== null) {
this.handleUpdateTrim(null, null);
}
}
render () {
const {effectTypes} = AudioEffects;
return (
<SoundEditorComponent
canPaste={this.state.copyBuffer !== null}
canRedo={this.redoStack.length > 0}
canUndo={this.undoStack.length > 0}
chunkLevels={this.state.chunkLevels}
name={this.props.name}
playhead={this.state.playhead}
setRef={this.setRef}
trimEnd={this.state.trimEnd}
trimStart={this.state.trimStart}
onActivateTrim={this.handleActivateTrim}
onChangeName={this.handleChangeName}
onContainerClick={this.handleContainerClick}
onCopy={this.handleCopy}
onCopyToNew={this.handleCopyToNew}
onDelete={this.handleDelete}
onEcho={this.effectFactory(effectTypes.ECHO)}
onFadeIn={this.effectFactory(effectTypes.FADEIN)}
onFadeOut={this.effectFactory(effectTypes.FADEOUT)}
onFaster={this.effectFactory(effectTypes.FASTER)}
onLouder={this.effectFactory(effectTypes.LOUDER)}
onMute={this.effectFactory(effectTypes.MUTE)}
onPaste={this.handlePaste}
onPlay={this.handlePlay}
onRedo={this.handleRedo}
onReverse={this.effectFactory(effectTypes.REVERSE)}
onRobot={this.effectFactory(effectTypes.ROBOT)}
onSetTrimEnd={this.handleUpdateTrimEnd}
onSetTrimStart={this.handleUpdateTrimStart}
onSetTrim={this.handleUpdateTrim}
onSlower={this.effectFactory(effectTypes.SLOWER)}
onSofter={this.effectFactory(effectTypes.SOFTER)}
onStop={this.handleStopPlaying}
@@ -223,10 +433,7 @@ SoundEditor.propTypes = {
samples: PropTypes.instanceOf(Float32Array),
soundId: PropTypes.string,
soundIndex: PropTypes.number,
vm: PropTypes.shape({
updateSoundBuffer: PropTypes.func,
renameSound: PropTypes.func
})
vm: PropTypes.instanceOf(VM).isRequired
};
const mapStateToProps = (state, {soundIndex}) => {