@@ -248,8 +251,8 @@ SoundEditor.propTypes = {
chunkLevels: PropTypes.arrayOf(PropTypes.number).isRequired,
intl: intlShape,
name: PropTypes.string.isRequired,
- onActivateTrim: PropTypes.func,
onChangeName: PropTypes.func.isRequired,
+ onContainerClick: PropTypes.func.isRequired,
onEcho: PropTypes.func.isRequired,
onFaster: PropTypes.func.isRequired,
onLouder: PropTypes.func.isRequired,
@@ -257,13 +260,13 @@ SoundEditor.propTypes = {
onRedo: PropTypes.func.isRequired,
onReverse: PropTypes.func.isRequired,
onRobot: PropTypes.func.isRequired,
- onSetTrimEnd: PropTypes.func,
- onSetTrimStart: PropTypes.func,
+ onSetTrim: PropTypes.func,
onSlower: PropTypes.func.isRequired,
onSofter: PropTypes.func.isRequired,
onStop: PropTypes.func.isRequired,
onUndo: PropTypes.func.isRequired,
playhead: PropTypes.number,
+ setRef: PropTypes.func,
trimEnd: PropTypes.number,
trimStart: PropTypes.number
};
diff --git a/src/containers/audio-selector.jsx b/src/containers/audio-selector.jsx
new file mode 100644
index 000000000..30ef003a9
--- /dev/null
+++ b/src/containers/audio-selector.jsx
@@ -0,0 +1,156 @@
+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';
+
+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;
+ }
+ 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) {
+ this.initialX = getEventXY(e).x;
+ const {width, left} = this.containerElement.getBoundingClientRect();
+ this.initialTrimEnd = (this.initialX - left) / width;
+ this.initialTrimStart = this.initialTrimEnd;
+ this.props.onSetTrim(this.initialTrimStart, this.initialTrimEnd);
+
+ this.clickStartTime = Date.now();
+
+ window.addEventListener('mousemove', this.handleTrimEndMouseMove);
+ window.addEventListener('mouseup', this.handleTrimEndMouseUp);
+ window.addEventListener('touchmove', this.handleTrimEndMouseMove);
+ window.addEventListener('touchend', this.handleTrimEndMouseUp);
+ }
+ handleTrimStartMouseMove (e) {
+ const containerSize = this.containerElement.getBoundingClientRect().width;
+ const dx = (getEventXY(e).x - this.initialX) / 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
+ });
+ }
+ e.preventDefault();
+ }
+ handleTrimEndMouseMove (e) {
+ const containerSize = this.containerElement.getBoundingClientRect().width;
+ const dx = (getEventXY(e).x - this.initialX) / 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({
+ trimEnd: newTrim
+ });
+ }
+ e.preventDefault();
+ }
+ handleTrimStartMouseUp () {
+ window.removeEventListener('mousemove', this.handleTrimStartMouseMove);
+ window.removeEventListener('mouseup', this.handleTrimStartMouseUp);
+ window.removeEventListener('touchmove', this.handleTrimStartMouseMove);
+ window.removeEventListener('touchend', this.handleTrimStartMouseUp);
+ this.props.onSetTrim(this.state.trimStart, this.state.trimEnd);
+ }
+ handleTrimEndMouseUp () {
+ window.removeEventListener('mousemove', this.handleTrimEndMouseMove);
+ window.removeEventListener('mouseup', this.handleTrimEndMouseUp);
+ window.removeEventListener('touchmove', this.handleTrimEndMouseMove);
+ window.removeEventListener('touchend', this.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.initialX = getEventXY(e).x;
+ this.initialTrimStart = this.props.trimStart;
+ this.initialTrimEnd = this.props.trimEnd;
+ window.addEventListener('mousemove', this.handleTrimStartMouseMove);
+ window.addEventListener('mouseup', this.handleTrimStartMouseUp);
+ window.addEventListener('touchmove', this.handleTrimStartMouseMove);
+ window.addEventListener('touchend', this.handleTrimStartMouseUp);
+ e.stopPropagation();
+ }
+ handleTrimEndMouseDown (e) {
+ this.initialX = getEventXY(e).x;
+ this.initialTrimEnd = this.props.trimEnd;
+ this.initialTrimStart = this.props.trimStart;
+ window.addEventListener('mousemove', this.handleTrimEndMouseMove);
+ window.addEventListener('mouseup', this.handleTrimEndMouseUp);
+ window.addEventListener('touchmove', this.handleTrimEndMouseMove);
+ window.addEventListener('touchend', this.handleTrimEndMouseUp);
+ e.stopPropagation();
+ }
+ storeRef (el) {
+ this.containerElement = el;
+ }
+ render () {
+ return (
+
+ );
+ }
+}
+
+AudioSelector.propTypes = {
+ onSetTrim: PropTypes.func,
+ playhead: PropTypes.number,
+ trimEnd: PropTypes.number,
+ trimStart: PropTypes.number
+};
+
+export default AudioSelector;
diff --git a/src/containers/sound-editor.jsx b/src/containers/sound-editor.jsx
index a3cdaaf67..d3c58f811 100644
--- a/src/containers/sound-editor.jsx
+++ b/src/containers/sound-editor.jsx
@@ -23,13 +23,14 @@ class SoundEditor extends React.Component {
'handlePlay',
'handleStopPlaying',
'handleUpdatePlayhead',
- 'handleActivateTrim',
- 'handleUpdateTrimEnd',
- 'handleUpdateTrimStart',
+ 'handleDelete',
+ 'handleUpdateTrim',
'handleEffect',
'handleUndo',
'handleRedo',
'submitNewSamples'
+ 'handleContainerClick',
+ 'setRef'
]);
this.state = {
chunkLevels: computeChunkedRMS(this.props.samples),
@@ -40,6 +41,8 @@ class SoundEditor extends React.Component {
this.redoStack = [];
this.undoStack = [];
+
+ this.ref = null;
}
componentDidMount () {
this.audioBufferPlayer = new AudioBufferPlayer(this.props.samples, this.props.sampleRate);
@@ -49,6 +52,10 @@ class SoundEditor extends React.Component {
this.redoStack = [];
this.undoStack = [];
this.resetState(newProps.samples, newProps.sampleRate);
+ this.setState({
+ trimStart: null,
+ trimEnd: null
+ });
}
}
componentWillUnmount () {
@@ -59,9 +66,7 @@ class SoundEditor extends React.Component {
this.audioBufferPlayer = new AudioBufferPlayer(samples, sampleRate);
this.setState({
chunkLevels: computeChunkedRMS(samples),
- playhead: null,
- trimStart: null,
- trimEnd: null
+ playhead: null
});
}
submitNewSamples (samples, sampleRate, skipUndo) {
@@ -106,6 +111,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 +131,36 @@ 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});
}
handleUpdateTrimStart (trimStart) {
this.setState({trimStart});
+ handleUpdateTrim (trimStart, trimEnd) {
+ this.setState({trimStart, trimEnd});
+ this.handleStopPlaying();
}
effectFactory (name) {
return () => this.handleEffect(name);
@@ -184,6 +195,13 @@ class SoundEditor extends React.Component {
this.undoStack.push(this.copyCurrentBuffer());
this.submitNewSamples(samples, sampleRate, true);
this.handlePlay();
+ 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.handleUpdateTrim(null, null);
}
}
render () {
@@ -195,10 +213,12 @@ class SoundEditor extends React.Component {
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}
+ onDelete={this.handleDelete}
onEcho={this.effectFactory(effectTypes.ECHO)}
onFaster={this.effectFactory(effectTypes.FASTER)}
onLouder={this.effectFactory(effectTypes.LOUDER)}
@@ -206,8 +226,7 @@ class SoundEditor extends React.Component {
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}