From 7704bafb005b254e873cfb3f8887aa7ed3757738 Mon Sep 17 00:00:00 2001 From: Eric Rosenbaum Date: Thu, 20 Jun 2019 13:52:01 -0400 Subject: [PATCH 01/61] Add icons --- .../sound-editor/icon--copy-to-new.svg | 18 ++++++++++ src/components/sound-editor/icon--copy.svg | 32 +++++++++++++++++ src/components/sound-editor/icon--delete.svg | 18 ++++++++++ src/components/sound-editor/icon--fade-in.svg | 34 +++++++++++++++++++ .../sound-editor/icon--fade-out.svg | 34 +++++++++++++++++++ src/components/sound-editor/icon--mute.svg | 18 ++++++++++ src/components/sound-editor/icon--paste.svg | 18 ++++++++++ 7 files changed, 172 insertions(+) create mode 100644 src/components/sound-editor/icon--copy-to-new.svg create mode 100644 src/components/sound-editor/icon--copy.svg create mode 100644 src/components/sound-editor/icon--delete.svg create mode 100644 src/components/sound-editor/icon--fade-in.svg create mode 100644 src/components/sound-editor/icon--fade-out.svg create mode 100644 src/components/sound-editor/icon--mute.svg create mode 100644 src/components/sound-editor/icon--paste.svg diff --git a/src/components/sound-editor/icon--copy-to-new.svg b/src/components/sound-editor/icon--copy-to-new.svg new file mode 100644 index 000000000..8bfc76ac5 --- /dev/null +++ b/src/components/sound-editor/icon--copy-to-new.svg @@ -0,0 +1,18 @@ + + + + Sound/General/Copy to New + Created with Sketch. + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/components/sound-editor/icon--copy.svg b/src/components/sound-editor/icon--copy.svg new file mode 100644 index 000000000..4b902ad12 --- /dev/null +++ b/src/components/sound-editor/icon--copy.svg @@ -0,0 +1,32 @@ + + + + Sound/General/Copy + Created with Sketch. + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/components/sound-editor/icon--delete.svg b/src/components/sound-editor/icon--delete.svg new file mode 100644 index 000000000..8f662201e --- /dev/null +++ b/src/components/sound-editor/icon--delete.svg @@ -0,0 +1,18 @@ + + + + Sound/General/Trim + Created with Sketch. + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/components/sound-editor/icon--fade-in.svg b/src/components/sound-editor/icon--fade-in.svg new file mode 100644 index 000000000..6bd9e6974 --- /dev/null +++ b/src/components/sound-editor/icon--fade-in.svg @@ -0,0 +1,34 @@ + + + + Sound/Effects/Fade-in + Created with Sketch. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/components/sound-editor/icon--fade-out.svg b/src/components/sound-editor/icon--fade-out.svg new file mode 100644 index 000000000..d6704bc0e --- /dev/null +++ b/src/components/sound-editor/icon--fade-out.svg @@ -0,0 +1,34 @@ + + + + Sound/Effects/Fade-out + Created with Sketch. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/components/sound-editor/icon--mute.svg b/src/components/sound-editor/icon--mute.svg new file mode 100644 index 000000000..1fcbbd559 --- /dev/null +++ b/src/components/sound-editor/icon--mute.svg @@ -0,0 +1,18 @@ + + + + Sound/Effects/Mute + Created with Sketch. + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/components/sound-editor/icon--paste.svg b/src/components/sound-editor/icon--paste.svg new file mode 100644 index 000000000..5d8bd9063 --- /dev/null +++ b/src/components/sound-editor/icon--paste.svg @@ -0,0 +1,18 @@ + + + + Sound/General/Paste + Created with Sketch. + + + + + + + + + + + + + \ No newline at end of file From 0d88c7755b65aef965ce924405409b3bc675f609 Mon Sep 17 00:00:00 2001 From: Eric Rosenbaum Date: Tue, 25 Jun 2019 17:07:50 -0400 Subject: [PATCH 02/61] Update tests --- test/__mocks__/audio-effects.js | 2 +- .../__snapshots__/sound-editor.test.jsx.snap | 370 ++++++++++++------ test/unit/components/sound-editor.test.jsx | 22 +- test/unit/containers/sound-editor.test.jsx | 54 ++- test/unit/util/audio-effects.test.js | 14 +- 5 files changed, 326 insertions(+), 136 deletions(-) diff --git a/test/__mocks__/audio-effects.js b/test/__mocks__/audio-effects.js index b8a4c6994..06c36c7e8 100644 --- a/test/__mocks__/audio-effects.js +++ b/test/__mocks__/audio-effects.js @@ -14,7 +14,7 @@ export default class MockAudioEffects { this.buffer = buffer; this.name = name; this.process = jest.fn(done => { - this._finishProcessing = renderedBuffer => done({renderedBuffer}); + this._finishProcessing = renderedBuffer => done(renderedBuffer, 0, 1); }); MockAudioEffects.instance = this; } diff --git a/test/unit/components/__snapshots__/sound-editor.test.jsx.snap b/test/unit/components/__snapshots__/sound-editor.test.jsx.snap index 38ddf76c4..32a5c45d8 100644 --- a/test/unit/components/__snapshots__/sound-editor.test.jsx.snap +++ b/test/unit/components/__snapshots__/sound-editor.test.jsx.snap @@ -3,6 +3,7 @@ exports[`Sound Editor Component matches snapshot 1`] = `
+
+ +
+ Copy +
+
+
+ +
+ Paste +
+
+
+ +
+ Copy to New +
+
+
+
@@ -71,9 +124,7 @@ exports[`Sound Editor Component matches snapshot 1`] = `
- - Save - + Delete
@@ -107,11 +158,11 @@ exports[`Sound Editor Component matches snapshot 1`] = `
@@ -148,6 +200,8 @@ exports[`Sound Editor Component matches snapshot 1`] = ` />
+
+ NaN +
+
+ NaN +
+
+
+
+
+ +
+ NaN +
+
+
+ +
+ NaN +
@@ -222,109 +421,6 @@ exports[`Sound Editor Component matches snapshot 1`] = ` } /> -
-
-
-
- -
-
- -
-
-
@@ -453,6 +549,24 @@ exports[`Sound Editor Component matches snapshot 1`] = ` +
+ +
+ + Mute + +
+
+
+ +
+ + Fade out + +
+
+
+ +
+ + Fade in + +
+
`; diff --git a/test/unit/components/sound-editor.test.jsx b/test/unit/components/sound-editor.test.jsx index efb153b1f..78aee8704 100644 --- a/test/unit/components/sound-editor.test.jsx +++ b/test/unit/components/sound-editor.test.jsx @@ -6,6 +6,7 @@ describe('Sound Editor Component', () => { let props; beforeEach(() => { props = { + canPaste: false, canUndo: true, canRedo: false, chunkLevels: [1, 2, 3], @@ -13,7 +14,14 @@ describe('Sound Editor Component', () => { playhead: 0.5, trimStart: 0.2, trimEnd: 0.8, - onActivateTrim: jest.fn(), + onContainerClick: jest.fn(), + onCopy: jest.fn(), + onCopyToNew: jest.fn(), + onFadeIn: jest.fn(), + onFadeOut: jest.fn(), + onMute: jest.fn(), + onPaste: jest.fn(), + onDelete: jest.fn(), onChangeName: jest.fn(), onPlay: jest.fn(), onRedo: jest.fn(), @@ -36,7 +44,7 @@ describe('Sound Editor Component', () => { expect(component.toJSON()).toMatchSnapshot(); }); - test('trim button appears when trims are null', () => { + test('delete button is disabled when selection is null', () => { const wrapper = mountWithIntl( { trimStart={null} /> ); - wrapper.find('[children="Trim"]').simulate('click'); - expect(props.onActivateTrim).toHaveBeenCalled(); + wrapper.find('[children="Delete"]').simulate('click'); + expect(props.onDelete).not.toHaveBeenCalled(); }); - test('save button appears when trims are not null', () => { + test('delete button calls delete when selection is not null', () => { const wrapper = mountWithIntl( { trimStart={0.25} /> ); - wrapper.find('[children="Save"]').simulate('click'); - expect(props.onActivateTrim).toHaveBeenCalled(); + wrapper.find('[children="Delete"]').simulate('click'); + expect(props.onDelete).toHaveBeenCalled(); }); test('play button appears when playhead is null', () => { diff --git a/test/unit/containers/sound-editor.test.jsx b/test/unit/containers/sound-editor.test.jsx index 6070b7592..a2f87e5f4 100644 --- a/test/unit/containers/sound-editor.test.jsx +++ b/test/unit/containers/sound-editor.test.jsx @@ -85,7 +85,7 @@ describe('Sound Editor Container', () => { expect(component.props().playhead).toEqual(null); }); - test('it sets the component props for trimming and submits to the vm', () => { + test('it sets the component props for select and delete and submits to the vm', () => { const wrapper = mountWithIntl( { ); let component = wrapper.find(SoundEditorComponent); - component.props().onActivateTrim(); + component.props().onSetTrim(0.25, 0.75); + wrapper.update(); + component = wrapper.find(SoundEditorComponent); expect(component.props().trimStart).not.toEqual(null); expect(component.props().trimEnd).not.toEqual(null); - component.props().onActivateTrim(); + component.props().onDelete(); wrapper.update(); component = wrapper.find(SoundEditorComponent); expect(vm.updateSoundBuffer).toHaveBeenCalled(); @@ -238,8 +240,8 @@ describe('Sound Editor Container', () => { expect(component.prop('canRedo')).toEqual(false); // Submitting new samples should make it possible to undo - component.props().onActivateTrim(); // Activate trimming - component.props().onActivateTrim(); // Submit new samples by calling again + component.props().onSetTrim(0.25, 0.75); + component.props().onDelete(); wrapper.update(); component = wrapper.find(SoundEditorComponent); expect(component.prop('canUndo')).toEqual(true); @@ -264,9 +266,8 @@ describe('Sound Editor Container', () => { wrapper.update(); component = wrapper.find(SoundEditorComponent); expect(component.prop('canRedo')).toEqual(true); - component.props().onActivateTrim(); // Activate trimming - component.props().onActivateTrim(); // Submit new samples by calling again - + component.props().onSetTrim(0.25, 0.75); + component.props().onDelete(); wrapper.update(); component = wrapper.find(SoundEditorComponent); expect(component.prop('canRedo')).toEqual(false); @@ -282,8 +283,8 @@ describe('Sound Editor Container', () => { let component = wrapper.find(SoundEditorComponent); // Set up an undoable state - component.props().onActivateTrim(); // Activate trimming - component.props().onActivateTrim(); // Submit new samples by calling again + component.props().onSetTrim(0.25, 0.75); + component.props().onDelete(); wrapper.update(); component = wrapper.find(SoundEditorComponent); @@ -301,4 +302,37 @@ describe('Sound Editor Container', () => { expect(mockAudioBufferPlayer.instance.play).toHaveBeenCalled(); expect(vm.updateSoundBuffer).toHaveBeenCalled(); }); + + test('undo and redo updates selection state', () => { + const wrapper = mountWithIntl( + + ); + let component = wrapper.find(SoundEditorComponent); + + // Set up an undoable state + component.props().onSetTrim(0.25, 0.75); + component.props().onDelete(); + wrapper.update(); + component = wrapper.find(SoundEditorComponent); + + expect(component.props().trimStart).toEqual(null); + expect(component.props().trimEnd).toEqual(null); + + component.props().onUndo(); + wrapper.update(); + component = wrapper.find(SoundEditorComponent); + + expect(component.props().trimStart).toEqual(0.25); + expect(component.props().trimEnd).toEqual(0.75); + + component.props().onRedo(); + wrapper.update(); + component = wrapper.find(SoundEditorComponent); + + expect(component.props().trimStart).toEqual(null); + expect(component.props().trimEnd).toEqual(null); + }); }); diff --git a/test/unit/util/audio-effects.test.js b/test/unit/util/audio-effects.test.js index d1057de53..4c1877d6d 100644 --- a/test/unit/util/audio-effects.test.js +++ b/test/unit/util/audio-effects.test.js @@ -14,19 +14,17 @@ describe('Audio Effects manager', () => { const audioBuffer = audioContext.createBuffer(1, 400, 44100); test('changes buffer length and playback rate for faster effect', () => { - const audioEffects = new AudioEffects(audioBuffer, 'faster'); + const audioEffects = new AudioEffects(audioBuffer, 'faster', 0, 1); expect(audioEffects.audioContext._.length).toBeLessThan(400); - expect(audioEffects.source.playbackRate.value).toBeGreaterThan(1); }); test('changes buffer length and playback rate for slower effect', () => { - const audioEffects = new AudioEffects(audioBuffer, 'slower'); + const audioEffects = new AudioEffects(audioBuffer, 'slower', 0, 1); expect(audioEffects.audioContext._.length).toBeGreaterThan(400); - expect(audioEffects.source.playbackRate.value).toBeLessThan(1); }); test('changes buffer length for echo effect', () => { - const audioEffects = new AudioEffects(audioBuffer, 'echo'); + const audioEffects = new AudioEffects(audioBuffer, 'echo', 0, 1); expect(audioEffects.audioContext._.length).toBeGreaterThan(400); }); @@ -43,15 +41,15 @@ describe('Effects', () => { }); test('all effects provide an input and output that are connected', () => { - const robotEffect = new RobotEffect(audioContext, 0.5); + const robotEffect = new RobotEffect(audioContext, 0.5, 0, 0.01); expect(robotEffect.input).toBeInstanceOf(AudioNode); expect(robotEffect.output).toBeInstanceOf(AudioNode); - const echoEffect = new EchoEffect(audioContext, 0.5); + const echoEffect = new EchoEffect(audioContext, 0.5, 0, 0.01); expect(echoEffect.input).toBeInstanceOf(AudioNode); expect(echoEffect.output).toBeInstanceOf(AudioNode); - const volumeEffect = new VolumeEffect(audioContext, 0.5); + const volumeEffect = new VolumeEffect(audioContext, 0.5, 0, 0.01); expect(volumeEffect.input).toBeInstanceOf(AudioNode); expect(volumeEffect.output).toBeInstanceOf(AudioNode); }); From 64562a8a0500458cbbf1e8f4399253775eea179c Mon Sep 17 00:00:00 2001 From: Eric Rosenbaum Date: Mon, 8 Jul 2019 12:48:39 -0400 Subject: [PATCH 03/61] Selection and delete --- .../audio-trimmer/audio-selector.css | 119 +++++++++++++ .../audio-trimmer/audio-selector.jsx | 73 ++++++++ src/components/sound-editor/sound-editor.jsx | 23 +-- src/containers/audio-selector.jsx | 156 ++++++++++++++++++ src/containers/sound-editor.jsx | 71 +++++--- 5 files changed, 406 insertions(+), 36 deletions(-) create mode 100644 src/components/audio-trimmer/audio-selector.css create mode 100644 src/components/audio-trimmer/audio-selector.jsx create mode 100644 src/containers/audio-selector.jsx diff --git a/src/components/audio-trimmer/audio-selector.css b/src/components/audio-trimmer/audio-selector.css new file mode 100644 index 000000000..f17f50e51 --- /dev/null +++ b/src/components/audio-trimmer/audio-selector.css @@ -0,0 +1,119 @@ +@import "../../css/colors.css"; + +$border-radius: 4px; +$trim-handle-width: 12px; +$trim-handle-height: 14px; +$stripe-size: 10px; +$hover-scale: 2; + +.absolute { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + cursor: pointer; + + /* Force the browser to paint separately to avoid composite cost with waveform */ + transform: translateZ(0); +} + +.trim-background { + cursor: pointer; + touch-action: none; +} + +.trim-background-mask { + border: 1px solid $motion-tertiary; + opacity: 0.5; + background: $motion-primary; +} + +.start-trim-background .trim-background-mask { + border-top-left-radius: $border-radius; + border-bottom-left-radius: $border-radius; +} + +.end-trim-background .trim-background-mask { + border-top-right-radius: $border-radius; + border-bottom-right-radius: $border-radius; +} + +.trim-line { + position: absolute; + top: 0; + width: 0px; + height: 100%; + border: 1px solid $motion-tertiary; +} + +.playhead-container { + position: absolute; + top: 0; + left: 0; + height: 100%; + width: 100%; + overflow: hidden; +} + +.playhead { + /* + Even though playhead is just a line, it is 100% width (the width of the waveform) + so that we can use transform: translateX() using percentages. + */ + width: 100%; + border-left: 1px solid $motion-primary; + border-top: none; + border-bottom: none; + border-right: none; +} + +.start-trim-line { + left: 0; +} + +.end-trim-line { + right: 0; +} + +.trim-handle { + position: absolute; + left: calc(-$trim-handle-width / 2); + width: $trim-handle-width; + height: $trim-handle-height; + filter: hue-rotate(180deg); +} + +.trim-handle img { + position: absolute; + width: $trim-handle-width; + height: $trim-handle-height; + + /* Make sure image dragging isn't triggered */ + user-select: none; + user-drag: none; + -webkit-user-drag: none; /* Autoprefixer doesn't seem to work for this */ + + transition: 0.2s; +} + +.top-trim-handle { + top: -$trim-handle-height; +} + +.bottom-trim-handle { + bottom: -$trim-handle-height; +} + +.top-trim-handle img { + transform: rotate(180deg); +} + +/* Increase handle size when anywhere on draggable area is hovered */ +.trim-background:hover img { + transform: scale($hover-scale); +} + +.trim-background:hover .top-trim-handle img { + transform: rotate(180deg) scale($hover-scale); +} diff --git a/src/components/audio-trimmer/audio-selector.jsx b/src/components/audio-trimmer/audio-selector.jsx new file mode 100644 index 000000000..1243edf11 --- /dev/null +++ b/src/components/audio-trimmer/audio-selector.jsx @@ -0,0 +1,73 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import classNames from 'classnames'; +import Box from '../box/box.jsx'; +import styles from './audio-selector.css'; +import handleIcon from './icon--handle.svg'; + +const AudioSelector = props => ( +
+ {props.trimStart === null ? null : ( + + + + + + + + + + + + + + + + + + + + )} + {props.playhead ? ( +
+
+
+ ) : null} +
+); + +AudioSelector.propTypes = { + containerRef: PropTypes.func, + onNewSelectionMouseDown: PropTypes.func.isRequired, + onTrimEndMouseDown: PropTypes.func.isRequired, + onTrimStartMouseDown: PropTypes.func.isRequired, + playhead: PropTypes.number, + trimEnd: PropTypes.number, + trimStart: PropTypes.number +}; + +export default AudioSelector; diff --git a/src/components/sound-editor/sound-editor.jsx b/src/components/sound-editor/sound-editor.jsx index f97338e06..ce25dc50b 100644 --- a/src/components/sound-editor/sound-editor.jsx +++ b/src/components/sound-editor/sound-editor.jsx @@ -8,15 +8,13 @@ import Label from '../forms/label.jsx'; import Input from '../forms/input.jsx'; import BufferedInputHOC from '../forms/buffered-input-hoc.jsx'; -import AudioTrimmer from '../../containers/audio-trimmer.jsx'; +import AudioSelector from '../../containers/audio-selector.jsx'; import IconButton from '../icon-button/icon-button.jsx'; import styles from './sound-editor.css'; import playIcon from '../record-modal/icon--play.svg'; import stopIcon from '../record-modal/icon--stop-playback.svg'; -import trimIcon from './icon--trim.svg'; -import trimConfirmIcon from './icon--trim-confirm.svg'; import redoIcon from './icon--redo.svg'; import undoIcon from './icon--undo.svg'; import echoIcon from './icon--echo.svg'; @@ -103,7 +101,11 @@ const messages = defineMessages({ }); const SoundEditor = props => ( -
+
@@ -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} From b34e1c69f05be68922ec244761c3cdcfcb0ff33b Mon Sep 17 00:00:00 2001 From: Eric Rosenbaum Date: Mon, 8 Jul 2019 13:02:45 -0400 Subject: [PATCH 04/61] Cleanup and connect trim button to delete --- src/components/sound-editor/sound-editor.jsx | 3 +++ src/containers/sound-editor.jsx | 10 ++++------ 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/src/components/sound-editor/sound-editor.jsx b/src/components/sound-editor/sound-editor.jsx index ce25dc50b..57fc2149e 100644 --- a/src/components/sound-editor/sound-editor.jsx +++ b/src/components/sound-editor/sound-editor.jsx @@ -15,6 +15,8 @@ import styles from './sound-editor.css'; import playIcon from '../record-modal/icon--play.svg'; import stopIcon from '../record-modal/icon--stop-playback.svg'; +import trimIcon from './icon--trim.svg'; +import trimConfirmIcon from './icon--trim-confirm.svg'; import redoIcon from './icon--redo.svg'; import undoIcon from './icon--undo.svg'; import echoIcon from './icon--echo.svg'; @@ -251,6 +253,7 @@ SoundEditor.propTypes = { chunkLevels: PropTypes.arrayOf(PropTypes.number).isRequired, intl: intlShape, name: PropTypes.string.isRequired, + onActivateTrim: PropTypes.func.isRequired, onChangeName: PropTypes.func.isRequired, onContainerClick: PropTypes.func.isRequired, onEcho: PropTypes.func.isRequired, diff --git a/src/containers/sound-editor.jsx b/src/containers/sound-editor.jsx index d3c58f811..5e54f7e9c 100644 --- a/src/containers/sound-editor.jsx +++ b/src/containers/sound-editor.jsx @@ -28,7 +28,7 @@ class SoundEditor extends React.Component { 'handleEffect', 'handleUndo', 'handleRedo', - 'submitNewSamples' + 'submitNewSamples', 'handleContainerClick', 'setRef' ]); @@ -153,11 +153,6 @@ class SoundEditor extends React.Component { trimEnd: null }); } - handleUpdateTrimEnd (trimEnd) { - this.setState({trimEnd}); - } - handleUpdateTrimStart (trimStart) { - this.setState({trimStart}); handleUpdateTrim (trimStart, trimEnd) { this.setState({trimStart, trimEnd}); this.handleStopPlaying(); @@ -195,6 +190,8 @@ class SoundEditor extends React.Component { this.undoStack.push(this.copyCurrentBuffer()); this.submitNewSamples(samples, sampleRate, true); this.handlePlay(); + } + } setRef (element) { this.ref = element; } @@ -216,6 +213,7 @@ class SoundEditor extends React.Component { setRef={this.setRef} trimEnd={this.state.trimEnd} trimStart={this.state.trimStart} + onActivateTrim={this.handleDelete} onChangeName={this.handleChangeName} onContainerClick={this.handleContainerClick} onDelete={this.handleDelete} From fdd4b61d5de22a670d86e6c16af861b6e5c174ac Mon Sep 17 00:00:00 2001 From: Eric Rosenbaum Date: Mon, 8 Jul 2019 13:57:36 -0400 Subject: [PATCH 05/61] Revert and fix tests This reverts commit 42ff50300cff7558fd03cdebb757eca8caf85199. --- test/__mocks__/audio-effects.js | 2 +- .../__snapshots__/sound-editor.test.jsx.snap | 194 +----------------- test/unit/components/sound-editor.test.jsx | 22 +- test/unit/containers/sound-editor.test.jsx | 71 +------ test/unit/util/audio-effects.test.js | 14 +- 5 files changed, 28 insertions(+), 275 deletions(-) diff --git a/test/__mocks__/audio-effects.js b/test/__mocks__/audio-effects.js index 06c36c7e8..b8a4c6994 100644 --- a/test/__mocks__/audio-effects.js +++ b/test/__mocks__/audio-effects.js @@ -14,7 +14,7 @@ export default class MockAudioEffects { this.buffer = buffer; this.name = name; this.process = jest.fn(done => { - this._finishProcessing = renderedBuffer => done(renderedBuffer, 0, 1); + this._finishProcessing = renderedBuffer => done({renderedBuffer}); }); MockAudioEffects.instance = this; } diff --git a/test/unit/components/__snapshots__/sound-editor.test.jsx.snap b/test/unit/components/__snapshots__/sound-editor.test.jsx.snap index 32a5c45d8..f9c476071 100644 --- a/test/unit/components/__snapshots__/sound-editor.test.jsx.snap +++ b/test/unit/components/__snapshots__/sound-editor.test.jsx.snap @@ -3,7 +3,7 @@ exports[`Sound Editor Component matches snapshot 1`] = `
-
- -
- Copy -
-
-
- -
- Paste -
-
-
- -
- Copy to New -
-
-
-
@@ -124,7 +72,9 @@ exports[`Sound Editor Component matches snapshot 1`] = `
- Delete + + Save +
@@ -239,26 +189,6 @@ exports[`Sound Editor Component matches snapshot 1`] = ` -
- NaN -
-
- NaN -
-
- NaN -
-
- NaN -
@@ -549,24 +419,6 @@ exports[`Sound Editor Component matches snapshot 1`] = ` -
- -
- - Mute - -
-
-
- -
- - Fade out - -
-
-
- -
- - Fade in - -
-
`; diff --git a/test/unit/components/sound-editor.test.jsx b/test/unit/components/sound-editor.test.jsx index 78aee8704..efb153b1f 100644 --- a/test/unit/components/sound-editor.test.jsx +++ b/test/unit/components/sound-editor.test.jsx @@ -6,7 +6,6 @@ describe('Sound Editor Component', () => { let props; beforeEach(() => { props = { - canPaste: false, canUndo: true, canRedo: false, chunkLevels: [1, 2, 3], @@ -14,14 +13,7 @@ describe('Sound Editor Component', () => { playhead: 0.5, trimStart: 0.2, trimEnd: 0.8, - onContainerClick: jest.fn(), - onCopy: jest.fn(), - onCopyToNew: jest.fn(), - onFadeIn: jest.fn(), - onFadeOut: jest.fn(), - onMute: jest.fn(), - onPaste: jest.fn(), - onDelete: jest.fn(), + onActivateTrim: jest.fn(), onChangeName: jest.fn(), onPlay: jest.fn(), onRedo: jest.fn(), @@ -44,7 +36,7 @@ describe('Sound Editor Component', () => { expect(component.toJSON()).toMatchSnapshot(); }); - test('delete button is disabled when selection is null', () => { + test('trim button appears when trims are null', () => { const wrapper = mountWithIntl( { trimStart={null} /> ); - wrapper.find('[children="Delete"]').simulate('click'); - expect(props.onDelete).not.toHaveBeenCalled(); + wrapper.find('[children="Trim"]').simulate('click'); + expect(props.onActivateTrim).toHaveBeenCalled(); }); - test('delete button calls delete when selection is not null', () => { + test('save button appears when trims are not null', () => { const wrapper = mountWithIntl( { trimStart={0.25} /> ); - wrapper.find('[children="Delete"]').simulate('click'); - expect(props.onDelete).toHaveBeenCalled(); + wrapper.find('[children="Save"]').simulate('click'); + expect(props.onActivateTrim).toHaveBeenCalled(); }); test('play button appears when playhead is null', () => { diff --git a/test/unit/containers/sound-editor.test.jsx b/test/unit/containers/sound-editor.test.jsx index a2f87e5f4..b0a1a8399 100644 --- a/test/unit/containers/sound-editor.test.jsx +++ b/test/unit/containers/sound-editor.test.jsx @@ -85,31 +85,6 @@ describe('Sound Editor Container', () => { expect(component.props().playhead).toEqual(null); }); - test('it sets the component props for select and delete and submits to the vm', () => { - const wrapper = mountWithIntl( - - ); - let component = wrapper.find(SoundEditorComponent); - - component.props().onSetTrim(0.25, 0.75); - - wrapper.update(); - - component = wrapper.find(SoundEditorComponent); - expect(component.props().trimStart).not.toEqual(null); - expect(component.props().trimEnd).not.toEqual(null); - - component.props().onDelete(); - wrapper.update(); - component = wrapper.find(SoundEditorComponent); - expect(vm.updateSoundBuffer).toHaveBeenCalled(); - expect(component.props().trimStart).toEqual(null); - expect(component.props().trimEnd).toEqual(null); - }); - test('it submits name changes to the vm', () => { const wrapper = mountWithIntl( { expect(component.prop('canRedo')).toEqual(false); // Submitting new samples should make it possible to undo - component.props().onSetTrim(0.25, 0.75); - component.props().onDelete(); + component.props().onFaster(); + mockAudioEffects.instance._finishProcessing(soundBuffer); wrapper.update(); component = wrapper.find(SoundEditorComponent); expect(component.prop('canUndo')).toEqual(true); @@ -266,8 +241,9 @@ describe('Sound Editor Container', () => { wrapper.update(); component = wrapper.find(SoundEditorComponent); expect(component.prop('canRedo')).toEqual(true); - component.props().onSetTrim(0.25, 0.75); - component.props().onDelete(); + component.props().onFaster(); + mockAudioEffects.instance._finishProcessing(soundBuffer); + wrapper.update(); component = wrapper.find(SoundEditorComponent); expect(component.prop('canRedo')).toEqual(false); @@ -283,8 +259,8 @@ describe('Sound Editor Container', () => { let component = wrapper.find(SoundEditorComponent); // Set up an undoable state - component.props().onSetTrim(0.25, 0.75); - component.props().onDelete(); + component.props().onFaster(); + mockAudioEffects.instance._finishProcessing(soundBuffer); wrapper.update(); component = wrapper.find(SoundEditorComponent); @@ -302,37 +278,4 @@ describe('Sound Editor Container', () => { expect(mockAudioBufferPlayer.instance.play).toHaveBeenCalled(); expect(vm.updateSoundBuffer).toHaveBeenCalled(); }); - - test('undo and redo updates selection state', () => { - const wrapper = mountWithIntl( - - ); - let component = wrapper.find(SoundEditorComponent); - - // Set up an undoable state - component.props().onSetTrim(0.25, 0.75); - component.props().onDelete(); - wrapper.update(); - component = wrapper.find(SoundEditorComponent); - - expect(component.props().trimStart).toEqual(null); - expect(component.props().trimEnd).toEqual(null); - - component.props().onUndo(); - wrapper.update(); - component = wrapper.find(SoundEditorComponent); - - expect(component.props().trimStart).toEqual(0.25); - expect(component.props().trimEnd).toEqual(0.75); - - component.props().onRedo(); - wrapper.update(); - component = wrapper.find(SoundEditorComponent); - - expect(component.props().trimStart).toEqual(null); - expect(component.props().trimEnd).toEqual(null); - }); }); diff --git a/test/unit/util/audio-effects.test.js b/test/unit/util/audio-effects.test.js index 4c1877d6d..d1057de53 100644 --- a/test/unit/util/audio-effects.test.js +++ b/test/unit/util/audio-effects.test.js @@ -14,17 +14,19 @@ describe('Audio Effects manager', () => { const audioBuffer = audioContext.createBuffer(1, 400, 44100); test('changes buffer length and playback rate for faster effect', () => { - const audioEffects = new AudioEffects(audioBuffer, 'faster', 0, 1); + const audioEffects = new AudioEffects(audioBuffer, 'faster'); expect(audioEffects.audioContext._.length).toBeLessThan(400); + expect(audioEffects.source.playbackRate.value).toBeGreaterThan(1); }); test('changes buffer length and playback rate for slower effect', () => { - const audioEffects = new AudioEffects(audioBuffer, 'slower', 0, 1); + const audioEffects = new AudioEffects(audioBuffer, 'slower'); expect(audioEffects.audioContext._.length).toBeGreaterThan(400); + expect(audioEffects.source.playbackRate.value).toBeLessThan(1); }); test('changes buffer length for echo effect', () => { - const audioEffects = new AudioEffects(audioBuffer, 'echo', 0, 1); + const audioEffects = new AudioEffects(audioBuffer, 'echo'); expect(audioEffects.audioContext._.length).toBeGreaterThan(400); }); @@ -41,15 +43,15 @@ describe('Effects', () => { }); test('all effects provide an input and output that are connected', () => { - const robotEffect = new RobotEffect(audioContext, 0.5, 0, 0.01); + const robotEffect = new RobotEffect(audioContext, 0.5); expect(robotEffect.input).toBeInstanceOf(AudioNode); expect(robotEffect.output).toBeInstanceOf(AudioNode); - const echoEffect = new EchoEffect(audioContext, 0.5, 0, 0.01); + const echoEffect = new EchoEffect(audioContext, 0.5); expect(echoEffect.input).toBeInstanceOf(AudioNode); expect(echoEffect.output).toBeInstanceOf(AudioNode); - const volumeEffect = new VolumeEffect(audioContext, 0.5, 0, 0.01); + const volumeEffect = new VolumeEffect(audioContext, 0.5); expect(volumeEffect.input).toBeInstanceOf(AudioNode); expect(volumeEffect.output).toBeInstanceOf(AudioNode); }); From f71a481d0eb6648037a4e90bc080d09abc7d7e7c Mon Sep 17 00:00:00 2001 From: Eric Rosenbaum Date: Mon, 8 Jul 2019 16:17:26 -0400 Subject: [PATCH 06/61] Remove icons not yet in use --- .../sound-editor/icon--copy-to-new.svg | 18 ---------- src/components/sound-editor/icon--copy.svg | 32 ----------------- src/components/sound-editor/icon--delete.svg | 18 ---------- src/components/sound-editor/icon--fade-in.svg | 34 ------------------- .../sound-editor/icon--fade-out.svg | 34 ------------------- src/components/sound-editor/icon--mute.svg | 18 ---------- src/components/sound-editor/icon--paste.svg | 18 ---------- 7 files changed, 172 deletions(-) delete mode 100644 src/components/sound-editor/icon--copy-to-new.svg delete mode 100644 src/components/sound-editor/icon--copy.svg delete mode 100644 src/components/sound-editor/icon--delete.svg delete mode 100644 src/components/sound-editor/icon--fade-in.svg delete mode 100644 src/components/sound-editor/icon--fade-out.svg delete mode 100644 src/components/sound-editor/icon--mute.svg delete mode 100644 src/components/sound-editor/icon--paste.svg diff --git a/src/components/sound-editor/icon--copy-to-new.svg b/src/components/sound-editor/icon--copy-to-new.svg deleted file mode 100644 index 8bfc76ac5..000000000 --- a/src/components/sound-editor/icon--copy-to-new.svg +++ /dev/null @@ -1,18 +0,0 @@ - - - - Sound/General/Copy to New - Created with Sketch. - - - - - - - - - - - - - \ No newline at end of file diff --git a/src/components/sound-editor/icon--copy.svg b/src/components/sound-editor/icon--copy.svg deleted file mode 100644 index 4b902ad12..000000000 --- a/src/components/sound-editor/icon--copy.svg +++ /dev/null @@ -1,32 +0,0 @@ - - - - Sound/General/Copy - Created with Sketch. - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/src/components/sound-editor/icon--delete.svg b/src/components/sound-editor/icon--delete.svg deleted file mode 100644 index 8f662201e..000000000 --- a/src/components/sound-editor/icon--delete.svg +++ /dev/null @@ -1,18 +0,0 @@ - - - - Sound/General/Trim - Created with Sketch. - - - - - - - - - - - - - \ No newline at end of file diff --git a/src/components/sound-editor/icon--fade-in.svg b/src/components/sound-editor/icon--fade-in.svg deleted file mode 100644 index 6bd9e6974..000000000 --- a/src/components/sound-editor/icon--fade-in.svg +++ /dev/null @@ -1,34 +0,0 @@ - - - - Sound/Effects/Fade-in - Created with Sketch. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/src/components/sound-editor/icon--fade-out.svg b/src/components/sound-editor/icon--fade-out.svg deleted file mode 100644 index d6704bc0e..000000000 --- a/src/components/sound-editor/icon--fade-out.svg +++ /dev/null @@ -1,34 +0,0 @@ - - - - Sound/Effects/Fade-out - Created with Sketch. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/src/components/sound-editor/icon--mute.svg b/src/components/sound-editor/icon--mute.svg deleted file mode 100644 index 1fcbbd559..000000000 --- a/src/components/sound-editor/icon--mute.svg +++ /dev/null @@ -1,18 +0,0 @@ - - - - Sound/Effects/Mute - Created with Sketch. - - - - - - - - - - - - - \ No newline at end of file diff --git a/src/components/sound-editor/icon--paste.svg b/src/components/sound-editor/icon--paste.svg deleted file mode 100644 index 5d8bd9063..000000000 --- a/src/components/sound-editor/icon--paste.svg +++ /dev/null @@ -1,18 +0,0 @@ - - - - Sound/General/Paste - Created with Sketch. - - - - - - - - - - - - - \ No newline at end of file From 8f949f532ff7b2dcc73264474b52e756d60ee896 Mon Sep 17 00:00:00 2001 From: Eric Rosenbaum Date: Tue, 9 Jul 2019 10:34:10 -0400 Subject: [PATCH 07/61] Apply effects to selection --- src/containers/sound-editor.jsx | 15 ++++-- src/lib/audio/audio-effects.js | 65 ++++++++++++++++++++------ src/lib/audio/effects/echo-effect.js | 7 ++- src/lib/audio/effects/robot-effect.js | 24 ++++++++-- src/lib/audio/effects/volume-effect.js | 10 +++- 5 files changed, 96 insertions(+), 25 deletions(-) diff --git a/src/containers/sound-editor.jsx b/src/containers/sound-editor.jsx index 5e54f7e9c..67ba3a1ae 100644 --- a/src/containers/sound-editor.jsx +++ b/src/containers/sound-editor.jsx @@ -168,12 +168,21 @@ 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; + + 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); + } + } }); } handleUndo () { diff --git a/src/lib/audio/audio-effects.js b/src/lib/audio/audio-effects.js index ab170f9c6..5dae9cf13 100644 --- a/src/lib/audio/audio-effects.js +++ b/src/lib/audio/audio-effects.js @@ -16,25 +16,42 @@ class AudioEffects { static get effectTypes () { return effectTypes; } - constructor (buffer, name) { + constructor (buffer, name, trimStart, trimEnd) { + this.trimStartSeconds = (trimStart * buffer.length) / buffer.sampleRate; + this.trimEndSeconds = (trimEnd * buffer.length) / buffer.sampleRate; + this.adjustedTrimStartSeconds = this.trimStartSeconds; + this.adjustedTrimEndSeconds = this.trimEndSeconds; + // Some effects will modify the playback rate and/or number of samples. // Need to precompute those values to create the offline audio context. const pitchRatio = Math.pow(2, 4 / 12); // A major third let sampleCount = buffer.length; - let playbackRate = 1; + const affectedSampleCount = Math.floor((this.trimEndSeconds - this.trimStartSeconds) * + buffer.sampleRate); + const unaffectedSampleCount = sampleCount - affectedSampleCount; + + this.playbackRate = 1; switch (name) { case effectTypes.ECHO: - sampleCount = buffer.length + (0.25 * 3 * buffer.sampleRate); + sampleCount = Math.max(sampleCount, Math.floor((this.trimEndSeconds + 0.75) * buffer.sampleRate)); break; case effectTypes.FASTER: - playbackRate = pitchRatio; - sampleCount = Math.floor(buffer.length / playbackRate); + this.playbackRate = pitchRatio; + sampleCount = unaffectedSampleCount + Math.floor(affectedSampleCount / this.playbackRate); + this.adjustedTrimEndSeconds = this.trimStartSeconds + + ((affectedSampleCount / this.playbackRate) / buffer.sampleRate); break; case effectTypes.SLOWER: - playbackRate = 1 / pitchRatio; - sampleCount = Math.floor(buffer.length / playbackRate); + this.playbackRate = 1 / pitchRatio; + sampleCount = unaffectedSampleCount + Math.floor(affectedSampleCount / this.playbackRate); + this.adjustedTrimEndSeconds = this.trimStartSeconds + + ((affectedSampleCount / this.playbackRate) / buffer.sampleRate); break; } + + this.adjustedTrimStart = this.adjustedTrimStartSeconds / (sampleCount / buffer.sampleRate); + this.adjustedTrimEnd = this.adjustedTrimEndSeconds / (sampleCount / buffer.sampleRate); + if (window.OfflineAudioContext) { this.audioContext = new window.OfflineAudioContext(1, sampleCount, buffer.sampleRate); } else { @@ -52,8 +69,17 @@ class AudioEffects { const newBuffer = this.audioContext.createBuffer(1, buffer.length, buffer.sampleRate); const newBufferData = newBuffer.getChannelData(0); const bufferLength = buffer.length; + + const startSamples = Math.floor(this.trimStartSeconds * buffer.sampleRate); + const endSamples = Math.floor(this.trimEndSeconds * buffer.sampleRate); + let counter = 0; for (let i = 0; i < bufferLength; i++) { - newBufferData[i] = originalBufferData[bufferLength - i - 1]; + if (i > startSamples && i < endSamples) { + newBufferData[i] = originalBufferData[endSamples - counter - 1]; + counter++; + } else { + newBufferData[i] = originalBufferData[i]; + } } this.buffer = newBuffer; } else { @@ -63,7 +89,6 @@ class AudioEffects { this.source = this.audioContext.createBufferSource(); this.source.buffer = this.buffer; - this.source.playbackRate.value = playbackRate; this.name = name; } process (done) { @@ -71,17 +96,26 @@ class AudioEffects { let input; let output; switch (this.name) { + case effectTypes.FASTER: + case effectTypes.SLOWER: + this.source.playbackRate.setValueAtTime(this.playbackRate, this.adjustedTrimStartSeconds); + this.source.playbackRate.setValueAtTime(1.0, this.adjustedTrimEndSeconds); + break; case effectTypes.LOUDER: - ({input, output} = new VolumeEffect(this.audioContext, 1.25)); + ({input, output} = new VolumeEffect(this.audioContext, 1.25, + this.adjustedTrimStartSeconds, this.adjustedTrimEndSeconds)); break; case effectTypes.SOFTER: - ({input, output} = new VolumeEffect(this.audioContext, 0.75)); + ({input, output} = new VolumeEffect(this.audioContext, 0.75, + this.adjustedTrimStartSeconds, this.adjustedTrimEndSeconds)); break; case effectTypes.ECHO: - ({input, output} = new EchoEffect(this.audioContext, 0.25)); + ({input, output} = new EchoEffect(this.audioContext, 0.25, + this.adjustedTrimStartSeconds, this.adjustedTrimEndSeconds)); break; case effectTypes.ROBOT: - ({input, output} = new RobotEffect(this.audioContext, 0.25)); + ({input, output} = new RobotEffect(this.audioContext, + this.adjustedTrimStartSeconds, this.adjustedTrimEndSeconds)); break; } @@ -96,7 +130,10 @@ class AudioEffects { this.source.start(); this.audioContext.startRendering(); - this.audioContext.oncomplete = done; + this.audioContext.oncomplete = ({renderedBuffer}) => { + done(renderedBuffer, this.adjustedTrimStart, this.adjustedTrimEnd); + }; + } } diff --git a/src/lib/audio/effects/echo-effect.js b/src/lib/audio/effects/echo-effect.js index 3c7c06806..182722adf 100644 --- a/src/lib/audio/effects/echo-effect.js +++ b/src/lib/audio/effects/echo-effect.js @@ -1,12 +1,15 @@ class EchoEffect { - constructor (audioContext, delayTime) { + constructor (audioContext, delayTime, startTime, endTime) { this.audioContext = audioContext; this.delayTime = delayTime; this.input = this.audioContext.createGain(); this.output = this.audioContext.createGain(); this.effectInput = this.audioContext.createGain(); - this.effectInput.gain.value = 0.75; + this.effectInput.gain.value = 0; + + this.effectInput.gain.setValueAtTime(0.75, startTime); + this.effectInput.gain.setValueAtTime(0, endTime); this.delay = this.audioContext.createDelay(1); this.delay.delayTime.value = delayTime; diff --git a/src/lib/audio/effects/robot-effect.js b/src/lib/audio/effects/robot-effect.js index 472668d53..653276eab 100644 --- a/src/lib/audio/effects/robot-effect.js +++ b/src/lib/audio/effects/robot-effect.js @@ -1,9 +1,20 @@ class RobotEffect { - constructor (audioContext) { + constructor (audioContext, startTime, endTime) { this.audioContext = audioContext; this.input = this.audioContext.createGain(); this.output = this.audioContext.createGain(); + this.passthrough = this.audioContext.createGain(); + this.effectInput = this.audioContext.createGain(); + + this.passthrough.gain.value = 1; + this.effectInput.gain.value = 0; + + this.passthrough.gain.setValueAtTime(0, startTime); + this.passthrough.gain.setValueAtTime(1, endTime); + + this.effectInput.gain.setValueAtTime(1, startTime); + this.effectInput.gain.setValueAtTime(0, endTime); // Ring modulator inspired by BBC Dalek voice // http://recherche.ircam.fr/pub/dafx11/Papers/66_e.pdf @@ -70,8 +81,13 @@ class RobotEffect { biquadFilter.frequency.value = 1000; biquadFilter.gain.value = 1.25; - this.input.connect(vcInverter1); - this.input.connect(vcDiode4); + this.input.connect(this.effectInput); + this.input.connect(this.passthrough); + + this.passthrough.connect(this.output); + + this.effectInput.connect(vcInverter1); + this.effectInput.connect(vcDiode4); vcInverter1.connect(vcDiode3); @@ -91,7 +107,7 @@ class RobotEffect { vcDiode3.connect(compressor); vcDiode4.connect(compressor); - this.input.connect(biquadFilter); + this.effectInput.connect(biquadFilter); biquadFilter.connect(compressor); compressor.connect(this.output); diff --git a/src/lib/audio/effects/volume-effect.js b/src/lib/audio/effects/volume-effect.js index bffc422ac..0cfcd6415 100644 --- a/src/lib/audio/effects/volume-effect.js +++ b/src/lib/audio/effects/volume-effect.js @@ -1,12 +1,18 @@ class VolumeEffect { - constructor (audioContext, volume) { + constructor (audioContext, volume, startSeconds, endSeconds) { this.audioContext = audioContext; this.input = this.audioContext.createGain(); this.output = this.audioContext.createGain(); this.gain = this.audioContext.createGain(); - this.gain.gain.value = volume; + + // Smoothly ramp the gain up before the start time, and down after the end time. + this.rampLength = 0.01; + this.gain.gain.setValueAtTime(1.0, Math.max(0, startSeconds - this.rampLength)); + this.gain.gain.exponentialRampToValueAtTime(volume, startSeconds); + this.gain.gain.setValueAtTime(volume, endSeconds); + this.gain.gain.exponentialRampToValueAtTime(1.0, endSeconds + this.rampLength); this.input.connect(this.gain); this.gain.connect(this.output); From 7bbe5f0589473d0f333284731fc6b855b44b6427 Mon Sep 17 00:00:00 2001 From: Eric Rosenbaum Date: Tue, 9 Jul 2019 10:42:53 -0400 Subject: [PATCH 08/61] Add fade in, fade out and mute effects --- src/components/sound-editor/icon--fade-in.svg | 34 ++++++++++++++++ .../sound-editor/icon--fade-out.svg | 34 ++++++++++++++++ src/components/sound-editor/icon--mute.svg | 18 +++++++++ src/components/sound-editor/sound-editor.jsx | 39 +++++++++++++++++++ src/containers/sound-editor.jsx | 3 ++ src/lib/audio/audio-effects.js | 19 ++++++++- src/lib/audio/effects/fade-effect.js | 27 +++++++++++++ src/lib/audio/effects/mute-effect.js | 22 +++++++++++ 8 files changed, 195 insertions(+), 1 deletion(-) create mode 100644 src/components/sound-editor/icon--fade-in.svg create mode 100644 src/components/sound-editor/icon--fade-out.svg create mode 100644 src/components/sound-editor/icon--mute.svg create mode 100644 src/lib/audio/effects/fade-effect.js create mode 100644 src/lib/audio/effects/mute-effect.js diff --git a/src/components/sound-editor/icon--fade-in.svg b/src/components/sound-editor/icon--fade-in.svg new file mode 100644 index 000000000..6bd9e6974 --- /dev/null +++ b/src/components/sound-editor/icon--fade-in.svg @@ -0,0 +1,34 @@ + + + + Sound/Effects/Fade-in + Created with Sketch. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/components/sound-editor/icon--fade-out.svg b/src/components/sound-editor/icon--fade-out.svg new file mode 100644 index 000000000..d6704bc0e --- /dev/null +++ b/src/components/sound-editor/icon--fade-out.svg @@ -0,0 +1,34 @@ + + + + Sound/Effects/Fade-out + Created with Sketch. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/components/sound-editor/icon--mute.svg b/src/components/sound-editor/icon--mute.svg new file mode 100644 index 000000000..1fcbbd559 --- /dev/null +++ b/src/components/sound-editor/icon--mute.svg @@ -0,0 +1,18 @@ + + + + Sound/Effects/Mute + Created with Sketch. + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/components/sound-editor/sound-editor.jsx b/src/components/sound-editor/sound-editor.jsx index 57fc2149e..cbd731f14 100644 --- a/src/components/sound-editor/sound-editor.jsx +++ b/src/components/sound-editor/sound-editor.jsx @@ -26,6 +26,9 @@ import louderIcon from './icon--louder.svg'; import softerIcon from './icon--softer.svg'; import robotIcon from './icon--robot.svg'; import reverseIcon from './icon--reverse.svg'; +import fadeOutIcon from './icon--fade-out.svg'; +import fadeInIcon from './icon--fade-in.svg'; +import muteIcon from './icon--mute.svg'; const BufferedInput = BufferedInputHOC(Input); @@ -99,6 +102,21 @@ const messages = defineMessages({ id: 'gui.soundEditor.reverse', description: 'Title of the button to apply the reverse effect', defaultMessage: 'Reverse' + }, + fadeOut: { + id: 'gui.soundEditor.fadeOut', + description: 'Title of the button to apply the fade out effect', + defaultMessage: 'Fade out' + }, + fadeIn: { + id: 'gui.soundEditor.fadeIn', + description: 'Title of the button to apply the fade in effect', + defaultMessage: 'Fade in' + }, + mute: { + id: 'gui.soundEditor.mute', + description: 'Title of the button to apply the mute effect', + defaultMessage: 'Mute' } }); @@ -237,12 +255,30 @@ const SoundEditor = props => ( title={} onClick={props.onSofter} /> + } + onClick={props.onMute} + /> } onClick={props.onReverse} /> + } + onClick={props.onFadeOut} + /> + } + onClick={props.onFadeIn} + /> ); @@ -257,8 +293,11 @@ SoundEditor.propTypes = { onChangeName: PropTypes.func.isRequired, onContainerClick: PropTypes.func.isRequired, onEcho: PropTypes.func.isRequired, + onFadeIn: PropTypes.func.isRequired, + onFadeOut: PropTypes.func.isRequired, onFaster: PropTypes.func.isRequired, onLouder: PropTypes.func.isRequired, + onMute: PropTypes.func.isRequired, onPlay: PropTypes.func.isRequired, onRedo: PropTypes.func.isRequired, onReverse: PropTypes.func.isRequired, diff --git a/src/containers/sound-editor.jsx b/src/containers/sound-editor.jsx index 67ba3a1ae..24c0758d3 100644 --- a/src/containers/sound-editor.jsx +++ b/src/containers/sound-editor.jsx @@ -227,8 +227,11 @@ class SoundEditor extends React.Component { onContainerClick={this.handleContainerClick} 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)} onPlay={this.handlePlay} onRedo={this.handleRedo} onReverse={this.effectFactory(effectTypes.REVERSE)} diff --git a/src/lib/audio/audio-effects.js b/src/lib/audio/audio-effects.js index 5dae9cf13..b6b78f5b0 100644 --- a/src/lib/audio/audio-effects.js +++ b/src/lib/audio/audio-effects.js @@ -1,6 +1,8 @@ import EchoEffect from './effects/echo-effect.js'; import RobotEffect from './effects/robot-effect.js'; import VolumeEffect from './effects/volume-effect.js'; +import FadeEffect from './effects/fade-effect.js'; +import MuteEffect from './effects/mute-effect.js'; const effectTypes = { ROBOT: 'robot', @@ -9,7 +11,10 @@ const effectTypes = { SOFTER: 'lower', FASTER: 'faster', SLOWER: 'slower', - ECHO: 'echo' + ECHO: 'echo', + FADEIN: 'fade in', + FADEOUT: 'fade out', + MUTE: 'mute' }; class AudioEffects { @@ -117,6 +122,18 @@ class AudioEffects { ({input, output} = new RobotEffect(this.audioContext, this.adjustedTrimStartSeconds, this.adjustedTrimEndSeconds)); break; + case effectTypes.FADEIN: + ({input, output} = new FadeEffect(this.audioContext, true, + this.adjustedTrimStartSeconds, this.adjustedTrimEndSeconds)); + break; + case effectTypes.FADEOUT: + ({input, output} = new FadeEffect(this.audioContext, false, + this.adjustedTrimStartSeconds, this.adjustedTrimEndSeconds)); + break; + case effectTypes.MUTE: + ({input, output} = new MuteEffect(this.audioContext, + this.adjustedTrimStartSeconds, this.adjustedTrimEndSeconds)); + break; } if (input && output) { diff --git a/src/lib/audio/effects/fade-effect.js b/src/lib/audio/effects/fade-effect.js new file mode 100644 index 000000000..5a13c5c16 --- /dev/null +++ b/src/lib/audio/effects/fade-effect.js @@ -0,0 +1,27 @@ +class FadeEffect { + constructor (audioContext, fadeIn, startSeconds, endSeconds) { + this.audioContext = audioContext; + + this.input = this.audioContext.createGain(); + this.output = this.audioContext.createGain(); + + this.gain = this.audioContext.createGain(); + + this.gain.gain.setValueAtTime(1, 0); + + if (fadeIn) { + this.gain.gain.setValueAtTime(0, startSeconds); + this.gain.gain.linearRampToValueAtTime(1, endSeconds); + } else { + this.gain.gain.setValueAtTime(1, startSeconds); + this.gain.gain.linearRampToValueAtTime(0, endSeconds); + } + + this.gain.gain.setValueAtTime(1, endSeconds); + + this.input.connect(this.gain); + this.gain.connect(this.output); + } +} + +export default FadeEffect; diff --git a/src/lib/audio/effects/mute-effect.js b/src/lib/audio/effects/mute-effect.js new file mode 100644 index 000000000..c2a2e68d5 --- /dev/null +++ b/src/lib/audio/effects/mute-effect.js @@ -0,0 +1,22 @@ +class MuteEffect { + constructor (audioContext, startSeconds, endSeconds) { + this.audioContext = audioContext; + + this.input = this.audioContext.createGain(); + this.output = this.audioContext.createGain(); + + this.gain = this.audioContext.createGain(); + + // Smoothly ramp the gain down before the start time, and up after the end time. + this.rampLength = 0.001; + this.gain.gain.setValueAtTime(1.0, Math.max(0, startSeconds - this.rampLength)); + this.gain.gain.linearRampToValueAtTime(0, startSeconds); + this.gain.gain.setValueAtTime(0, endSeconds); + this.gain.gain.linearRampToValueAtTime(1.0, endSeconds + this.rampLength); + + this.input.connect(this.gain); + this.gain.connect(this.output); + } +} + +export default MuteEffect; From e5a517a3250c24223aec76025d8a1ca4c6923116 Mon Sep 17 00:00:00 2001 From: Eric Rosenbaum Date: Tue, 9 Jul 2019 10:51:42 -0400 Subject: [PATCH 09/61] Include selection state in undo/redo stack --- src/containers/sound-editor.jsx | 21 ++++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/src/containers/sound-editor.jsx b/src/containers/sound-editor.jsx index 24c0758d3..d6b4e9ba0 100644 --- a/src/containers/sound-editor.jsx +++ b/src/containers/sound-editor.jsx @@ -98,7 +98,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( @@ -185,20 +185,27 @@ class SoundEditor extends React.Component { } }); } + 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.handlePlay(); + this.setState({trimStart: trimStart, trimEnd: trimEnd}, this.handlePlay); } } setRef (element) { From c11b4a34ef434a3358ab830a48020c6a9396181c Mon Sep 17 00:00:00 2001 From: Eric Rosenbaum Date: Tue, 9 Jul 2019 11:24:03 -0400 Subject: [PATCH 10/61] Protect effects from error due to very short sound --- src/containers/sound-editor.jsx | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/containers/sound-editor.jsx b/src/containers/sound-editor.jsx index d6b4e9ba0..d841b0869 100644 --- a/src/containers/sound-editor.jsx +++ b/src/containers/sound-editor.jsx @@ -171,6 +171,11 @@ class SoundEditor extends React.Component { 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); From 1e147e439714f94a0c14fd031c8731ca8693e574 Mon Sep 17 00:00:00 2001 From: Eric Rosenbaum Date: Tue, 9 Jul 2019 11:53:44 -0400 Subject: [PATCH 11/61] Fix tests --- test/__mocks__/audio-effects.js | 2 +- .../__snapshots__/sound-editor.test.jsx.snap | 54 +++++++++++++++++++ test/unit/util/audio-effects.test.js | 14 +++-- 3 files changed, 61 insertions(+), 9 deletions(-) diff --git a/test/__mocks__/audio-effects.js b/test/__mocks__/audio-effects.js index b8a4c6994..06c36c7e8 100644 --- a/test/__mocks__/audio-effects.js +++ b/test/__mocks__/audio-effects.js @@ -14,7 +14,7 @@ export default class MockAudioEffects { this.buffer = buffer; this.name = name; this.process = jest.fn(done => { - this._finishProcessing = renderedBuffer => done({renderedBuffer}); + this._finishProcessing = renderedBuffer => done(renderedBuffer, 0, 1); }); MockAudioEffects.instance = this; } diff --git a/test/unit/components/__snapshots__/sound-editor.test.jsx.snap b/test/unit/components/__snapshots__/sound-editor.test.jsx.snap index f9c476071..e589ce5bc 100644 --- a/test/unit/components/__snapshots__/sound-editor.test.jsx.snap +++ b/test/unit/components/__snapshots__/sound-editor.test.jsx.snap @@ -419,6 +419,24 @@ exports[`Sound Editor Component matches snapshot 1`] = ` +
+ +
+ + Mute + +
+
+
+ +
+ + Fade out + +
+
+
+ +
+ + Fade in + +
+
`; diff --git a/test/unit/util/audio-effects.test.js b/test/unit/util/audio-effects.test.js index d1057de53..ac34f286f 100644 --- a/test/unit/util/audio-effects.test.js +++ b/test/unit/util/audio-effects.test.js @@ -14,19 +14,17 @@ describe('Audio Effects manager', () => { const audioBuffer = audioContext.createBuffer(1, 400, 44100); test('changes buffer length and playback rate for faster effect', () => { - const audioEffects = new AudioEffects(audioBuffer, 'faster'); + const audioEffects = new AudioEffects(audioBuffer, 'faster', 0, 1); expect(audioEffects.audioContext._.length).toBeLessThan(400); - expect(audioEffects.source.playbackRate.value).toBeGreaterThan(1); }); test('changes buffer length and playback rate for slower effect', () => { - const audioEffects = new AudioEffects(audioBuffer, 'slower'); + const audioEffects = new AudioEffects(audioBuffer, 'slower', 0, 1); expect(audioEffects.audioContext._.length).toBeGreaterThan(400); - expect(audioEffects.source.playbackRate.value).toBeLessThan(1); }); test('changes buffer length for echo effect', () => { - const audioEffects = new AudioEffects(audioBuffer, 'echo'); + const audioEffects = new AudioEffects(audioBuffer, 'echo', 0, 1); expect(audioEffects.audioContext._.length).toBeGreaterThan(400); }); @@ -43,15 +41,15 @@ describe('Effects', () => { }); test('all effects provide an input and output that are connected', () => { - const robotEffect = new RobotEffect(audioContext, 0.5); + const robotEffect = new RobotEffect(audioContext, 0, 1); expect(robotEffect.input).toBeInstanceOf(AudioNode); expect(robotEffect.output).toBeInstanceOf(AudioNode); - const echoEffect = new EchoEffect(audioContext, 0.5); + const echoEffect = new EchoEffect(audioContext, 0.75, 0, 1); expect(echoEffect.input).toBeInstanceOf(AudioNode); expect(echoEffect.output).toBeInstanceOf(AudioNode); - const volumeEffect = new VolumeEffect(audioContext, 0.5); + const volumeEffect = new VolumeEffect(audioContext, 0.5, 0, 1); expect(volumeEffect.input).toBeInstanceOf(AudioNode); expect(volumeEffect.output).toBeInstanceOf(AudioNode); }); From 0a489ea2ea23d841bc2a94231fa318752ba7a38f Mon Sep 17 00:00:00 2001 From: Eric Rosenbaum Date: Tue, 9 Jul 2019 14:41:52 -0400 Subject: [PATCH 12/61] Add copy, paste, copy to new and delete buttons --- .../sound-editor/icon--copy-to-new.svg | 18 +++ src/components/sound-editor/icon--copy.svg | 32 ++++ src/components/sound-editor/icon--delete.svg | 18 +++ src/components/sound-editor/icon--lounder.svg | 17 --- src/components/sound-editor/icon--paste.svg | 18 +++ src/components/sound-editor/sound-editor.css | 19 ++- src/components/sound-editor/sound-editor.jsx | 46 ++++-- src/containers/sound-editor.jsx | 137 +++++++++++++++++- 8 files changed, 273 insertions(+), 32 deletions(-) create mode 100644 src/components/sound-editor/icon--copy-to-new.svg create mode 100644 src/components/sound-editor/icon--copy.svg create mode 100644 src/components/sound-editor/icon--delete.svg delete mode 100644 src/components/sound-editor/icon--lounder.svg create mode 100644 src/components/sound-editor/icon--paste.svg diff --git a/src/components/sound-editor/icon--copy-to-new.svg b/src/components/sound-editor/icon--copy-to-new.svg new file mode 100644 index 000000000..8bfc76ac5 --- /dev/null +++ b/src/components/sound-editor/icon--copy-to-new.svg @@ -0,0 +1,18 @@ + + + + Sound/General/Copy to New + Created with Sketch. + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/components/sound-editor/icon--copy.svg b/src/components/sound-editor/icon--copy.svg new file mode 100644 index 000000000..4b902ad12 --- /dev/null +++ b/src/components/sound-editor/icon--copy.svg @@ -0,0 +1,32 @@ + + + + Sound/General/Copy + Created with Sketch. + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/components/sound-editor/icon--delete.svg b/src/components/sound-editor/icon--delete.svg new file mode 100644 index 000000000..8f662201e --- /dev/null +++ b/src/components/sound-editor/icon--delete.svg @@ -0,0 +1,18 @@ + + + + Sound/General/Trim + Created with Sketch. + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/components/sound-editor/icon--lounder.svg b/src/components/sound-editor/icon--lounder.svg deleted file mode 100644 index a1022b4b8..000000000 --- a/src/components/sound-editor/icon--lounder.svg +++ /dev/null @@ -1,17 +0,0 @@ - - - - icon--lounder - Created with Sketch. - - - - - - - - - - - - \ No newline at end of file diff --git a/src/components/sound-editor/icon--paste.svg b/src/components/sound-editor/icon--paste.svg new file mode 100644 index 000000000..5d8bd9063 --- /dev/null +++ b/src/components/sound-editor/icon--paste.svg @@ -0,0 +1,18 @@ + + + + Sound/General/Paste + Created with Sketch. + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/components/sound-editor/sound-editor.css b/src/components/sound-editor/sound-editor.css index 279875fc8..406a26430 100644 --- a/src/components/sound-editor/sound-editor.css +++ b/src/components/sound-editor/sound-editor.css @@ -118,6 +118,7 @@ $border-radius: 0.25rem; color: $text-primary; font-size: 0.625rem; user-select: none; + } [dir="ltr"] .trim-button { @@ -130,7 +131,6 @@ $border-radius: 0.25rem; .trim-button > img { width: 1.25rem; - margin-bottom: -0.375rem; } .effect-button { @@ -150,6 +150,23 @@ $border-radius: 0.25rem; margin-bottom: -0.375rem; } +.tool-button { + flex-basis: 60px; + color: $text-primary; + font-size: 0.625rem; + user-select: none; +} + +.tool-button + .tool-button { + margin: 0; +} + +.tool-button img { + width: 4rem; + height: 1.25rem; + margin-bottom: -0.375rem; +} + /* mirror the louder/softer speaker icons when rtl */ [dir="rtl"] .effect-button:nth-of-type(6) img { transform: scaleX(-1); diff --git a/src/components/sound-editor/sound-editor.jsx b/src/components/sound-editor/sound-editor.jsx index cbd731f14..8306229eb 100644 --- a/src/components/sound-editor/sound-editor.jsx +++ b/src/components/sound-editor/sound-editor.jsx @@ -30,6 +30,11 @@ import fadeOutIcon from './icon--fade-out.svg'; import fadeInIcon from './icon--fade-in.svg'; import muteIcon from './icon--mute.svg'; +import deleteIcon from './icon--delete.svg'; +import copyIcon from './icon--copy.svg'; +import pasteIcon from './icon--paste.svg'; +import copyToNewIcon from './icon--copy-to-new.svg'; + const BufferedInput = BufferedInputHOC(Input); const messages = defineMessages({ @@ -163,17 +168,33 @@ const SoundEditor = props => ( +
+ + + +
- ) : ( - - )} - onClick={props.onActivateTrim} + className={styles.toolButton} + disabled={props.trimStart === null} + img={deleteIcon} + title={'Delete'} + onClick={props.onDelete} />
@@ -284,6 +305,7 @@ const SoundEditor = props => ( ); SoundEditor.propTypes = { + canPaste: PropTypes.bool.isRequired, canRedo: PropTypes.bool.isRequired, canUndo: PropTypes.bool.isRequired, chunkLevels: PropTypes.arrayOf(PropTypes.number).isRequired, @@ -292,12 +314,16 @@ SoundEditor.propTypes = { onActivateTrim: PropTypes.func.isRequired, onChangeName: PropTypes.func.isRequired, onContainerClick: PropTypes.func.isRequired, + onCopy: PropTypes.func.isRequired, + onCopyToNew: PropTypes.func.isRequired, + onDelete: PropTypes.func, onEcho: PropTypes.func.isRequired, onFadeIn: PropTypes.func.isRequired, onFadeOut: PropTypes.func.isRequired, onFaster: PropTypes.func.isRequired, onLouder: PropTypes.func.isRequired, onMute: PropTypes.func.isRequired, + onPaste: PropTypes.func.isRequired, onPlay: PropTypes.func.isRequired, onRedo: PropTypes.func.isRequired, onReverse: PropTypes.func.isRequired, diff --git a/src/containers/sound-editor.jsx b/src/containers/sound-editor.jsx index d841b0869..253bf8cfb 100644 --- a/src/containers/sound-editor.jsx +++ b/src/containers/sound-editor.jsx @@ -2,6 +2,7 @@ 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'; @@ -17,7 +18,9 @@ class SoundEditor extends React.Component { constructor (props) { super(props); bindAll(this, [ + 'copy', 'copyCurrentBuffer', + 'handleCopyToNew', 'handleStoppedPlaying', 'handleChangeName', 'handlePlay', @@ -29,10 +32,14 @@ class SoundEditor extends React.Component { 'handleUndo', 'handleRedo', 'submitNewSamples', + 'handleCopy', + 'handlePaste', + 'paste', 'handleContainerClick', 'setRef' ]); this.state = { + copyBuffer: null, chunkLevels: computeChunkedRMS(this.props.samples), playhead: null, // null is not playing, [0 -> 1] is playing percent trimStart: null, @@ -213,6 +220,127 @@ class SoundEditor extends React.Component { 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 trimStartSamples = trimStart * this.props.samples.length; + const trimEndSamples = trimEnd * this.props.samples.length; + + const newCopyBuffer = this.copyCurrentBuffer(); + newCopyBuffer.samples = newCopyBuffer.samples.slice(trimStartSamples, trimEndSamples); + + this.setState({ + copyBuffer: newCopyBuffer + }, callback); + } + handleCopyToNew () { + this.copy(this.copyToNew); + } + copyToNew () { + WavEncoder.encode({ + sampleRate: this.state.copyBuffer.sampleRate, + channelData: [this.state.copyBuffer.samples] + }).then(wavBuffer => { + const vmSound = { + format: '', + dataFormat: 'wav', + rate: this.state.copyBuffer.sampleRate, + sampleCount: this.state.copyBuffer.samples.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 + vmSound.name = this.props.name; + + this.props.vm.addSound(vmSound); + }); + } + 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 = this.audioContext.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 + if (this.state.trimStart === null) { + const newLength = this.props.samples.length + this.state.copyBuffer.samples.length; + const newSamples = new Float32Array(newLength); + newSamples.set(this.props.samples, 0); + newSamples.set(this.state.copyBuffer.samples, this.props.samples.length); + this.submitNewSamples(newSamples, this.props.sampleRate, false); + } else { + // else replace the selection with the pasted sound + const trimStartSamples = this.state.trimStart * this.props.samples.length; + const trimEndSamples = this.state.trimEnd * this.props.samples.length; + const firstPart = this.props.samples.slice(0, trimStartSamples); + const lastPart = this.props.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.setState({ + trimStart: adjustedTrimStart, + trimEnd: adjustedTrimEnd + }); + + this.submitNewSamples(newSamples, this.props.sampleRate, false); + } + + 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; } @@ -226,6 +354,7 @@ class SoundEditor extends React.Component { const {effectTypes} = AudioEffects; return ( 0} canUndo={this.undoStack.length > 0} chunkLevels={this.state.chunkLevels} @@ -237,6 +366,8 @@ class SoundEditor extends React.Component { onActivateTrim={this.handleDelete} 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)} @@ -244,6 +375,7 @@ class SoundEditor extends React.Component { 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)} @@ -264,10 +396,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}) => { From ee44e225f541cab799bda5ae8f89450a08af12db Mon Sep 17 00:00:00 2001 From: Eric Rosenbaum Date: Tue, 9 Jul 2019 14:58:55 -0400 Subject: [PATCH 13/61] Icon button styling --- src/components/icon-button/icon-button.css | 10 ++++++++++ src/components/icon-button/icon-button.jsx | 10 ++++++++-- 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/src/components/icon-button/icon-button.css b/src/components/icon-button/icon-button.css index f58ee6f8d..aa827a3e2 100644 --- a/src/components/icon-button/icon-button.css +++ b/src/components/icon-button/icon-button.css @@ -8,6 +8,7 @@ font-size: 0.75rem; font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; color: $motion-primary; + border-radius: 0.5rem; } .container + .container { @@ -17,3 +18,12 @@ .title { margin-top: 0.5rem; } + +.disabled { + opacity: 0.5; + pointer-events: none; +} + +.container:active { + background-color: $motion-transparent; +} diff --git a/src/components/icon-button/icon-button.jsx b/src/components/icon-button/icon-button.jsx index 832e4a65b..c6549fdf9 100644 --- a/src/components/icon-button/icon-button.jsx +++ b/src/components/icon-button/icon-button.jsx @@ -5,14 +5,19 @@ import styles from './icon-button.css'; const IconButton = ({ img, + disabled, className, title, onClick }) => (
Date: Tue, 9 Jul 2019 14:59:07 -0400 Subject: [PATCH 14/61] Add keyboard shortcuts --- src/containers/sound-editor.jsx | 61 +++++++++++++++++++++++++++++++++ 1 file changed, 61 insertions(+) diff --git a/src/containers/sound-editor.jsx b/src/containers/sound-editor.jsx index 253bf8cfb..de09b9572 100644 --- a/src/containers/sound-editor.jsx +++ b/src/containers/sound-editor.jsx @@ -35,6 +35,7 @@ class SoundEditor extends React.Component { 'handleCopy', 'handlePaste', 'paste', + 'handleKeyPress', 'handleContainerClick', 'setRef' ]); @@ -53,6 +54,8 @@ class SoundEditor extends React.Component { } 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 @@ -67,6 +70,49 @@ class SoundEditor extends React.Component { } 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 === ' ') { + if (this.state.playhead) { + this.handleStopPlaying(); + } else { + this.handlePlay(); + } + } + if (event.key === 'Delete' || event.key === 'Backspace') { + 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') { + if (this.redoStack.length > 0) { + this.handleRedo(); + } + } else if (event.key === 'z') { + if (this.undoStack.length > 0) { + this.handleUndo(); + } + } else if (event.key === 'c') { + this.handleCopy(); + } else if (event.key === 'v') { + this.handlePaste(); + } else if (event.key === 'a') { + this.handleUpdateTrim(0, 1); + } + } } resetState (samples, sampleRate) { this.audioBufferPlayer.stop(); @@ -160,6 +206,21 @@ class SoundEditor extends React.Component { trimEnd: null }); } + 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 + }); + } handleUpdateTrim (trimStart, trimEnd) { this.setState({trimStart, trimEnd}); this.handleStopPlaying(); From c11624ab06e0ea1f453db8c40d8669a9a1378b96 Mon Sep 17 00:00:00 2001 From: Eric Rosenbaum Date: Tue, 9 Jul 2019 17:23:20 -0400 Subject: [PATCH 15/61] Fix tests and remove unused icons --- src/components/sound-editor/sound-editor.jsx | 2 - .../__snapshots__/sound-editor.test.jsx.snap | 58 +++++++++++++++++-- test/unit/components/sound-editor.test.jsx | 20 ++----- 3 files changed, 58 insertions(+), 22 deletions(-) diff --git a/src/components/sound-editor/sound-editor.jsx b/src/components/sound-editor/sound-editor.jsx index 8306229eb..316a0e2cb 100644 --- a/src/components/sound-editor/sound-editor.jsx +++ b/src/components/sound-editor/sound-editor.jsx @@ -15,8 +15,6 @@ import styles from './sound-editor.css'; import playIcon from '../record-modal/icon--play.svg'; import stopIcon from '../record-modal/icon--stop-playback.svg'; -import trimIcon from './icon--trim.svg'; -import trimConfirmIcon from './icon--trim-confirm.svg'; import redoIcon from './icon--redo.svg'; import undoIcon from './icon--undo.svg'; import echoIcon from './icon--echo.svg'; diff --git a/test/unit/components/__snapshots__/sound-editor.test.jsx.snap b/test/unit/components/__snapshots__/sound-editor.test.jsx.snap index e589ce5bc..950a25704 100644 --- a/test/unit/components/__snapshots__/sound-editor.test.jsx.snap +++ b/test/unit/components/__snapshots__/sound-editor.test.jsx.snap @@ -60,7 +60,59 @@ exports[`Sound Editor Component matches snapshot 1`] = `
+
+ +
+ Copy +
+
+
+ +
+ Paste +
+
+
+ +
+ Copy to New +
+
+
+
@@ -72,9 +124,7 @@ exports[`Sound Editor Component matches snapshot 1`] = `
- - Save - + Delete
diff --git a/test/unit/components/sound-editor.test.jsx b/test/unit/components/sound-editor.test.jsx index efb153b1f..7fd465241 100644 --- a/test/unit/components/sound-editor.test.jsx +++ b/test/unit/components/sound-editor.test.jsx @@ -13,8 +13,8 @@ describe('Sound Editor Component', () => { playhead: 0.5, trimStart: 0.2, trimEnd: 0.8, - onActivateTrim: jest.fn(), onChangeName: jest.fn(), + onDelete: jest.fn(), onPlay: jest.fn(), onRedo: jest.fn(), onReverse: jest.fn(), @@ -36,19 +36,7 @@ describe('Sound Editor Component', () => { expect(component.toJSON()).toMatchSnapshot(); }); - test('trim button appears when trims are null', () => { - const wrapper = mountWithIntl( - - ); - wrapper.find('[children="Trim"]').simulate('click'); - expect(props.onActivateTrim).toHaveBeenCalled(); - }); - - test('save button appears when trims are not null', () => { + test('delete button appears when selection is not null', () => { const wrapper = mountWithIntl( { trimStart={0.25} /> ); - wrapper.find('[children="Save"]').simulate('click'); - expect(props.onActivateTrim).toHaveBeenCalled(); + wrapper.find('[children="Delete"]').simulate('click'); + expect(props.onDelete).toHaveBeenCalled(); }); test('play button appears when playhead is null', () => { From 68a4284e8c3255c64c63f4e4b5edbe29232c3763 Mon Sep 17 00:00:00 2001 From: Eric Rosenbaum Date: Tue, 9 Jul 2019 17:33:33 -0400 Subject: [PATCH 16/61] Add util for encodeAndAddSoundToVM --- src/containers/record-modal.jsx | 32 +++------------------------- src/containers/sound-editor.jsx | 36 ++++---------------------------- src/lib/audio/audio-util.js | 37 +++++++++++++++++++++++++++++++++ 3 files changed, 44 insertions(+), 61 deletions(-) diff --git a/src/containers/record-modal.jsx b/src/containers/record-modal.jsx index a7a9e8f83..548567762 100644 --- a/src/containers/record-modal.jsx +++ b/src/containers/record-modal.jsx @@ -4,6 +4,7 @@ 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 +72,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 () { diff --git a/src/containers/sound-editor.jsx b/src/containers/sound-editor.jsx index de09b9572..b61c1e649 100644 --- a/src/containers/sound-editor.jsx +++ b/src/containers/sound-editor.jsx @@ -6,7 +6,7 @@ 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'; @@ -299,37 +299,9 @@ class SoundEditor extends React.Component { }, callback); } handleCopyToNew () { - this.copy(this.copyToNew); - } - copyToNew () { - WavEncoder.encode({ - sampleRate: this.state.copyBuffer.sampleRate, - channelData: [this.state.copyBuffer.samples] - }).then(wavBuffer => { - const vmSound = { - format: '', - dataFormat: 'wav', - rate: this.state.copyBuffer.sampleRate, - sampleCount: this.state.copyBuffer.samples.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 - vmSound.name = this.props.name; - - this.props.vm.addSound(vmSound); + this.copy(() => { + encodeAndAddSoundToVM(this.props.vm, this.state.copyBuffer.samples, + this.state.copyBuffer.sampleRate, this.props.name) }); } resampleBufferToRate (buffer, newRate) { diff --git a/src/lib/audio/audio-util.js b/src/lib/audio/audio-util.js index 2b68fcbe8..a01c30de8 100644 --- a/src/lib/audio/audio-util.js +++ b/src/lib/audio/audio-util.js @@ -1,3 +1,5 @@ +import WavEncoder from 'wav-encoder'; + const SOUND_BYTE_LIMIT = 10 * 1000 * 1000; // 10mb const computeRMS = function (samples, scaling = 0.55) { @@ -23,8 +25,43 @@ const computeChunkedRMS = function (samples, chunkSize = 1024) { return chunkLevels; }; +const encodeAndAddSoundToVM = function (vm, samples, sampleRate, name, callback) { + WavEncoder.encode({ + sampleRate: sampleRate, + channelData: [samples] + }).then(wavBuffer => { + const vmSound = { + format: '', + dataFormat: 'wav', + rate: sampleRate, + sampleCount: samples.length + }; + + // Create an asset from the encoded .wav and get resulting md5 + const storage = 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 + vmSound.name = name; + + vm.addSound(vmSound).then(() => { + if (callback) callback(); + }); + }); +}; + export { computeRMS, computeChunkedRMS, + encodeAndAddSoundToVM, SOUND_BYTE_LIMIT }; From c46069759a9160a70f5d1ad60b665ddc02222fe1 Mon Sep 17 00:00:00 2001 From: Eric Rosenbaum Date: Tue, 9 Jul 2019 17:45:25 -0400 Subject: [PATCH 17/61] Cleanup and lint fixes --- src/containers/record-modal.jsx | 1 - src/containers/sound-editor.jsx | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/src/containers/record-modal.jsx b/src/containers/record-modal.jsx index 548567762..ffaf968a2 100644 --- a/src/containers/record-modal.jsx +++ b/src/containers/record-modal.jsx @@ -2,7 +2,6 @@ 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'; diff --git a/src/containers/sound-editor.jsx b/src/containers/sound-editor.jsx index b61c1e649..bda9a3467 100644 --- a/src/containers/sound-editor.jsx +++ b/src/containers/sound-editor.jsx @@ -301,7 +301,7 @@ class SoundEditor extends React.Component { handleCopyToNew () { this.copy(() => { encodeAndAddSoundToVM(this.props.vm, this.state.copyBuffer.samples, - this.state.copyBuffer.sampleRate, this.props.name) + this.state.copyBuffer.sampleRate, this.props.name); }); } resampleBufferToRate (buffer, newRate) { From cff0b178815fde035c18eb327a970d664d3e4fe9 Mon Sep 17 00:00:00 2001 From: Eric Rosenbaum Date: Mon, 15 Jul 2019 11:40:10 -0400 Subject: [PATCH 18/61] Combine CSS for trimmer and selector into single file --- .../audio-trimmer/audio-selector.css | 119 ------------------ .../audio-trimmer/audio-selector.jsx | 10 +- .../audio-trimmer/audio-trimmer.css | 26 ++++ 3 files changed, 31 insertions(+), 124 deletions(-) delete mode 100644 src/components/audio-trimmer/audio-selector.css diff --git a/src/components/audio-trimmer/audio-selector.css b/src/components/audio-trimmer/audio-selector.css deleted file mode 100644 index f17f50e51..000000000 --- a/src/components/audio-trimmer/audio-selector.css +++ /dev/null @@ -1,119 +0,0 @@ -@import "../../css/colors.css"; - -$border-radius: 4px; -$trim-handle-width: 12px; -$trim-handle-height: 14px; -$stripe-size: 10px; -$hover-scale: 2; - -.absolute { - position: absolute; - top: 0; - left: 0; - width: 100%; - height: 100%; - cursor: pointer; - - /* Force the browser to paint separately to avoid composite cost with waveform */ - transform: translateZ(0); -} - -.trim-background { - cursor: pointer; - touch-action: none; -} - -.trim-background-mask { - border: 1px solid $motion-tertiary; - opacity: 0.5; - background: $motion-primary; -} - -.start-trim-background .trim-background-mask { - border-top-left-radius: $border-radius; - border-bottom-left-radius: $border-radius; -} - -.end-trim-background .trim-background-mask { - border-top-right-radius: $border-radius; - border-bottom-right-radius: $border-radius; -} - -.trim-line { - position: absolute; - top: 0; - width: 0px; - height: 100%; - border: 1px solid $motion-tertiary; -} - -.playhead-container { - position: absolute; - top: 0; - left: 0; - height: 100%; - width: 100%; - overflow: hidden; -} - -.playhead { - /* - Even though playhead is just a line, it is 100% width (the width of the waveform) - so that we can use transform: translateX() using percentages. - */ - width: 100%; - border-left: 1px solid $motion-primary; - border-top: none; - border-bottom: none; - border-right: none; -} - -.start-trim-line { - left: 0; -} - -.end-trim-line { - right: 0; -} - -.trim-handle { - position: absolute; - left: calc(-$trim-handle-width / 2); - width: $trim-handle-width; - height: $trim-handle-height; - filter: hue-rotate(180deg); -} - -.trim-handle img { - position: absolute; - width: $trim-handle-width; - height: $trim-handle-height; - - /* Make sure image dragging isn't triggered */ - user-select: none; - user-drag: none; - -webkit-user-drag: none; /* Autoprefixer doesn't seem to work for this */ - - transition: 0.2s; -} - -.top-trim-handle { - top: -$trim-handle-height; -} - -.bottom-trim-handle { - bottom: -$trim-handle-height; -} - -.top-trim-handle img { - transform: rotate(180deg); -} - -/* Increase handle size when anywhere on draggable area is hovered */ -.trim-background:hover img { - transform: scale($hover-scale); -} - -.trim-background:hover .top-trim-handle img { - transform: rotate(180deg) scale($hover-scale); -} diff --git a/src/components/audio-trimmer/audio-selector.jsx b/src/components/audio-trimmer/audio-selector.jsx index 1243edf11..65d18c31a 100644 --- a/src/components/audio-trimmer/audio-selector.jsx +++ b/src/components/audio-trimmer/audio-selector.jsx @@ -2,12 +2,12 @@ import PropTypes from 'prop-types'; import React from 'react'; import classNames from 'classnames'; import Box from '../box/box.jsx'; -import styles from './audio-selector.css'; +import styles from './audio-trimmer.css'; import handleIcon from './icon--handle.svg'; const AudioSelector = props => (
( width: `${100 * (props.trimEnd - props.trimStart)}%` }} > - + @@ -34,7 +34,7 @@ const AudioSelector = props => ( diff --git a/src/components/audio-trimmer/audio-trimmer.css b/src/components/audio-trimmer/audio-trimmer.css index 63af2a0cb..8c0e23dc6 100644 --- a/src/components/audio-trimmer/audio-trimmer.css +++ b/src/components/audio-trimmer/audio-trimmer.css @@ -17,6 +17,10 @@ $hover-scale: 2; transform: translateZ(0); } +.selector { + cursor: pointer; +} + .trim-background { cursor: pointer; touch-action: none; @@ -35,6 +39,12 @@ $hover-scale: 2; ); } +.selection-background { + border: 1px solid $motion-tertiary; + background: $motion-primary; + opacity: 0.5; +} + .start-trim-background .trim-background-mask { border-top-left-radius: $border-radius; border-bottom-left-radius: $border-radius; @@ -53,6 +63,10 @@ $hover-scale: 2; border: 1px solid $red-tertiary; } +.selector .trim-line { + border: 1px solid $motion-tertiary; +} + .playhead-container { position: absolute; top: 0; @@ -78,10 +92,18 @@ $hover-scale: 2; right: 0; } +.start-selection-line { + left: 0; +} + .end-trim-line { left: 0; } +.end-selection-line { + right: 0; +} + .trim-handle { position: absolute; left: calc(-$trim-handle-width / 2); @@ -89,6 +111,10 @@ $hover-scale: 2; height: $trim-handle-height; } +.selector .trim-handle { + filter: hue-rotate(180deg); +} + .trim-handle img { position: absolute; width: $trim-handle-width; From 2659490a70a555f5acdbdfd2d4362d5868593e82 Mon Sep 17 00:00:00 2001 From: Eric Rosenbaum Date: Mon, 15 Jul 2019 12:00:27 -0400 Subject: [PATCH 19/61] Update snapshot --- test/unit/components/__snapshots__/sound-editor.test.jsx.snap | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/unit/components/__snapshots__/sound-editor.test.jsx.snap b/test/unit/components/__snapshots__/sound-editor.test.jsx.snap index 950a25704..406192549 100644 --- a/test/unit/components/__snapshots__/sound-editor.test.jsx.snap +++ b/test/unit/components/__snapshots__/sound-editor.test.jsx.snap @@ -157,7 +157,7 @@ exports[`Sound Editor Component matches snapshot 1`] = `
From f0a18d3fd3fe2e5b00724867abc428d6780eda18 Mon Sep 17 00:00:00 2001 From: Eric Rosenbaum Date: Tue, 16 Jul 2019 16:00:43 -0400 Subject: [PATCH 20/61] Extract selection handle and playhead components from trimmer and selector --- .../audio-trimmer/audio-selector.jsx | 42 +++++-------------- .../audio-trimmer/audio-trimmer.css | 16 +++---- .../audio-trimmer/audio-trimmer.jsx | 40 ++++++------------ src/components/audio-trimmer/playhead.jsx | 21 ++++++++++ .../audio-trimmer/selection-handle.jsx | 28 +++++++++++++ 5 files changed, 80 insertions(+), 67 deletions(-) create mode 100644 src/components/audio-trimmer/playhead.jsx create mode 100644 src/components/audio-trimmer/selection-handle.jsx diff --git a/src/components/audio-trimmer/audio-selector.jsx b/src/components/audio-trimmer/audio-selector.jsx index 65d18c31a..3a7692216 100644 --- a/src/components/audio-trimmer/audio-selector.jsx +++ b/src/components/audio-trimmer/audio-selector.jsx @@ -3,7 +3,8 @@ import React from 'react'; import classNames from 'classnames'; import Box from '../box/box.jsx'; import styles from './audio-trimmer.css'; -import handleIcon from './icon--handle.svg'; +import SelectionHandle from './selection-handle.jsx'; +import Playhead from './playhead.jsx'; const AudioSelector = props => (
( }} > - - - - - - - - - + - - - - - - - + /> )} {props.playhead ? ( -
-
-
+ ) : null}
); diff --git a/src/components/audio-trimmer/audio-trimmer.css b/src/components/audio-trimmer/audio-trimmer.css index 8c0e23dc6..ce7d60213 100644 --- a/src/components/audio-trimmer/audio-trimmer.css +++ b/src/components/audio-trimmer/audio-trimmer.css @@ -88,20 +88,20 @@ $hover-scale: 2; border-right: none; } -.start-trim-line { - right: 0; +.selector .left-handle { + left: 0 } -.start-selection-line { - left: 0; +.selector .right-handle { + right: 0 } -.end-trim-line { - left: 0; +.trimmer .left-handle { + right: 0 } -.end-selection-line { - right: 0; +.trimmer .right-handle { + left: 0 } .trim-handle { diff --git a/src/components/audio-trimmer/audio-trimmer.jsx b/src/components/audio-trimmer/audio-trimmer.jsx index 87ca75a98..d9874ddd1 100644 --- a/src/components/audio-trimmer/audio-trimmer.jsx +++ b/src/components/audio-trimmer/audio-trimmer.jsx @@ -3,11 +3,12 @@ import React from 'react'; import classNames from 'classnames'; import Box from '../box/box.jsx'; import styles from './audio-trimmer.css'; -import handleIcon from './icon--handle.svg'; +import SelectionHandle from './selection-handle.jsx'; +import Playhead from './playhead.jsx'; const AudioTrimmer = props => (
{props.trimStart === null ? null : ( @@ -20,28 +21,16 @@ const AudioTrimmer = props => ( onTouchStart={props.onTrimStartMouseDown} > - - - - - - - - + )} - {props.playhead ? ( -
-
-
+ ) : null} - {props.trimEnd === null ? null : ( ( onTouchStart={props.onTrimEndMouseDown} > - - - - - - - - + )}
diff --git a/src/components/audio-trimmer/playhead.jsx b/src/components/audio-trimmer/playhead.jsx new file mode 100644 index 000000000..5b3adddd6 --- /dev/null +++ b/src/components/audio-trimmer/playhead.jsx @@ -0,0 +1,21 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import classNames from 'classnames'; +import styles from './audio-trimmer.css'; + +const Playhead = props => ( +
+
+
+); + +Playhead.propTypes = { + playbackPosition: PropTypes.number +}; + +export default Playhead; diff --git a/src/components/audio-trimmer/selection-handle.jsx b/src/components/audio-trimmer/selection-handle.jsx new file mode 100644 index 000000000..f182ce6a4 --- /dev/null +++ b/src/components/audio-trimmer/selection-handle.jsx @@ -0,0 +1,28 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import classNames from 'classnames'; +import Box from '../box/box.jsx'; +import styles from './audio-trimmer.css'; +import handleIcon from './icon--handle.svg'; + +const SelectionHandle = props => ( + + + + + + + + +); + +SelectionHandle.propTypes = { + handleStyle: PropTypes.string, + onMouseDown: PropTypes.func +}; + +export default SelectionHandle; From df7e77c8659a87a4a951e3e16d365220a76e781a Mon Sep 17 00:00:00 2001 From: Eric Rosenbaum Date: Tue, 16 Jul 2019 16:14:28 -0400 Subject: [PATCH 21/61] Localize button titles --- src/components/sound-editor/sound-editor.jsx | 31 +++++++++++++++----- 1 file changed, 23 insertions(+), 8 deletions(-) diff --git a/src/components/sound-editor/sound-editor.jsx b/src/components/sound-editor/sound-editor.jsx index 316a0e2cb..c0ca1d1e1 100644 --- a/src/components/sound-editor/sound-editor.jsx +++ b/src/components/sound-editor/sound-editor.jsx @@ -51,10 +51,25 @@ const messages = defineMessages({ description: 'Title of the button to stop the sound', defaultMessage: 'Stop' }, - trim: { - id: 'gui.soundEditor.trim', - description: 'Title of the button to start trimminging the sound', - defaultMessage: 'Trim' + copy: { + id: 'gui.soundEditor.copy', + description: 'Title of the button to copy the sound', + defaultMessage: 'Copy' + }, + paste: { + id: 'gui.soundEditor.paste', + description: 'Title of the button to paste the sound', + defaultMessage: 'Paste' + }, + copyToNew: { + id: 'gui.soundEditor.copyToNew', + description: 'Title of the button to copy the selection into a new sound', + defaultMessage: 'Copy to New' + }, + delete: { + id: 'gui.soundEditor.delete', + description: 'Title of the button to delete the sound', + defaultMessage: 'Delete' }, save: { id: 'gui.soundEditor.save', @@ -170,20 +185,20 @@ const SoundEditor = props => (
@@ -191,7 +206,7 @@ const SoundEditor = props => ( className={styles.toolButton} disabled={props.trimStart === null} img={deleteIcon} - title={'Delete'} + title={props.intl.formatMessage(messages.delete)} onClick={props.onDelete} />
From 5062518d16158a0ebd63e0f96eaabf3abfb8bedb Mon Sep 17 00:00:00 2001 From: Eric Rosenbaum Date: Tue, 16 Jul 2019 16:41:36 -0400 Subject: [PATCH 22/61] Fix playhead CSS --- src/components/audio-trimmer/audio-selector.jsx | 2 +- src/components/audio-trimmer/audio-trimmer.css | 2 +- src/components/audio-trimmer/playhead.jsx | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/components/audio-trimmer/audio-selector.jsx b/src/components/audio-trimmer/audio-selector.jsx index 3a7692216..a7c0b41c2 100644 --- a/src/components/audio-trimmer/audio-selector.jsx +++ b/src/components/audio-trimmer/audio-selector.jsx @@ -15,7 +15,7 @@ const AudioSelector = props => ( > {props.trimStart === null ? null : ( (
Date: Mon, 22 Jul 2019 13:55:49 -0400 Subject: [PATCH 23/61] Use offline context to create offline nodes --- src/containers/sound-editor.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/containers/sound-editor.jsx b/src/containers/sound-editor.jsx index bda9a3467..c50a2d48c 100644 --- a/src/containers/sound-editor.jsx +++ b/src/containers/sound-editor.jsx @@ -311,7 +311,7 @@ class SoundEditor extends React.Component { const newLength = sampleRateRatio * buffer.samples.length; const offlineContext = new window.OfflineAudioContext(1, newLength, newRate); const source = offlineContext.createBufferSource(); - const audioBuffer = this.audioContext.createBuffer(1, buffer.samples.length, buffer.sampleRate); + const audioBuffer = offlineContext.createBuffer(1, buffer.samples.length, buffer.sampleRate); audioBuffer.getChannelData(0).set(buffer.samples); source.buffer = audioBuffer; source.connect(offlineContext.destination); From 3823dd0629f18ba9d3dd890995f5750571f072e7 Mon Sep 17 00:00:00 2001 From: Eric Rosenbaum Date: Mon, 22 Jul 2019 13:56:00 -0400 Subject: [PATCH 24/61] Remove unused prop --- src/components/sound-editor/sound-editor.jsx | 1 - src/containers/sound-editor.jsx | 1 - 2 files changed, 2 deletions(-) diff --git a/src/components/sound-editor/sound-editor.jsx b/src/components/sound-editor/sound-editor.jsx index c0ca1d1e1..bea5fa84e 100644 --- a/src/components/sound-editor/sound-editor.jsx +++ b/src/components/sound-editor/sound-editor.jsx @@ -324,7 +324,6 @@ SoundEditor.propTypes = { chunkLevels: PropTypes.arrayOf(PropTypes.number).isRequired, intl: intlShape, name: PropTypes.string.isRequired, - onActivateTrim: PropTypes.func.isRequired, onChangeName: PropTypes.func.isRequired, onContainerClick: PropTypes.func.isRequired, onCopy: PropTypes.func.isRequired, diff --git a/src/containers/sound-editor.jsx b/src/containers/sound-editor.jsx index c50a2d48c..2de6077ca 100644 --- a/src/containers/sound-editor.jsx +++ b/src/containers/sound-editor.jsx @@ -396,7 +396,6 @@ class SoundEditor extends React.Component { setRef={this.setRef} trimEnd={this.state.trimEnd} trimStart={this.state.trimStart} - onActivateTrim={this.handleDelete} onChangeName={this.handleChangeName} onContainerClick={this.handleContainerClick} onCopy={this.handleCopy} From e74255cf107e5450f6be6baa1a805191ccdd7fb1 Mon Sep 17 00:00:00 2001 From: Eric Rosenbaum Date: Mon, 22 Jul 2019 15:07:34 -0400 Subject: [PATCH 25/61] Use static getters for echo params --- src/lib/audio/audio-effects.js | 4 ++-- src/lib/audio/effects/echo-effect.js | 11 ++++++++--- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/src/lib/audio/audio-effects.js b/src/lib/audio/audio-effects.js index b6b78f5b0..7460055bb 100644 --- a/src/lib/audio/audio-effects.js +++ b/src/lib/audio/audio-effects.js @@ -38,7 +38,7 @@ class AudioEffects { this.playbackRate = 1; switch (name) { case effectTypes.ECHO: - sampleCount = Math.max(sampleCount, Math.floor((this.trimEndSeconds + 0.75) * buffer.sampleRate)); + sampleCount = Math.max(sampleCount, Math.floor((this.trimEndSeconds + EchoEffect.TAIL_SECONDS) * buffer.sampleRate)); break; case effectTypes.FASTER: this.playbackRate = pitchRatio; @@ -115,7 +115,7 @@ class AudioEffects { this.adjustedTrimStartSeconds, this.adjustedTrimEndSeconds)); break; case effectTypes.ECHO: - ({input, output} = new EchoEffect(this.audioContext, 0.25, + ({input, output} = new EchoEffect(this.audioContext, this.adjustedTrimStartSeconds, this.adjustedTrimEndSeconds)); break; case effectTypes.ROBOT: diff --git a/src/lib/audio/effects/echo-effect.js b/src/lib/audio/effects/echo-effect.js index 182722adf..0ef4b93c2 100644 --- a/src/lib/audio/effects/echo-effect.js +++ b/src/lib/audio/effects/echo-effect.js @@ -1,7 +1,12 @@ class EchoEffect { - constructor (audioContext, delayTime, startTime, endTime) { + static get DELAY_TIME () { + return 0.25; + } + static get TAIL_SECONDS () { + return 0.75; + } + constructor (audioContext, startTime, endTime) { this.audioContext = audioContext; - this.delayTime = delayTime; this.input = this.audioContext.createGain(); this.output = this.audioContext.createGain(); @@ -12,7 +17,7 @@ class EchoEffect { this.effectInput.gain.setValueAtTime(0, endTime); this.delay = this.audioContext.createDelay(1); - this.delay.delayTime.value = delayTime; + this.delay.delayTime.value = EchoEffect.DELAY_TIME; this.decay = this.audioContext.createGain(); this.decay.gain.value = 0.3; From d6f2b0a48a702ba68d404b61fa3455ace9cbcc68 Mon Sep 17 00:00:00 2001 From: Eric Rosenbaum Date: Mon, 22 Jul 2019 16:05:08 -0400 Subject: [PATCH 26/61] Clean up AudioEffects constructor (needs a refactor) --- src/lib/audio/audio-effects.js | 22 +++++++++++++--------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/src/lib/audio/audio-effects.js b/src/lib/audio/audio-effects.js index 7460055bb..d449ff88e 100644 --- a/src/lib/audio/audio-effects.js +++ b/src/lib/audio/audio-effects.js @@ -33,29 +33,33 @@ class AudioEffects { let sampleCount = buffer.length; const affectedSampleCount = Math.floor((this.trimEndSeconds - this.trimStartSeconds) * buffer.sampleRate); + let adjustedAffectedSampleCount = affectedSampleCount; const unaffectedSampleCount = sampleCount - affectedSampleCount; this.playbackRate = 1; switch (name) { case effectTypes.ECHO: - sampleCount = Math.max(sampleCount, Math.floor((this.trimEndSeconds + EchoEffect.TAIL_SECONDS) * buffer.sampleRate)); + sampleCount = Math.max(sampleCount, + Math.floor((this.trimEndSeconds + EchoEffect.TAIL_SECONDS) * buffer.sampleRate)); break; case effectTypes.FASTER: this.playbackRate = pitchRatio; - sampleCount = unaffectedSampleCount + Math.floor(affectedSampleCount / this.playbackRate); - this.adjustedTrimEndSeconds = this.trimStartSeconds + - ((affectedSampleCount / this.playbackRate) / buffer.sampleRate); + adjustedAffectedSampleCount = Math.floor(affectedSampleCount / this.playbackRate); + sampleCount = unaffectedSampleCount + adjustedAffectedSampleCount; + break; case effectTypes.SLOWER: this.playbackRate = 1 / pitchRatio; - sampleCount = unaffectedSampleCount + Math.floor(affectedSampleCount / this.playbackRate); - this.adjustedTrimEndSeconds = this.trimStartSeconds + - ((affectedSampleCount / this.playbackRate) / buffer.sampleRate); + adjustedAffectedSampleCount = Math.floor(affectedSampleCount / this.playbackRate); + sampleCount = unaffectedSampleCount + adjustedAffectedSampleCount; break; } - this.adjustedTrimStart = this.adjustedTrimStartSeconds / (sampleCount / buffer.sampleRate); - this.adjustedTrimEnd = this.adjustedTrimEndSeconds / (sampleCount / buffer.sampleRate); + const durationSeconds = sampleCount / buffer.sampleRate; + this.adjustedTrimEndSeconds = this.trimStartSeconds + + (adjustedAffectedSampleCount / buffer.sampleRate); + this.adjustedTrimStart = this.adjustedTrimStartSeconds / durationSeconds; + this.adjustedTrimEnd = this.adjustedTrimEndSeconds / durationSeconds; if (window.OfflineAudioContext) { this.audioContext = new window.OfflineAudioContext(1, sampleCount, buffer.sampleRate); From bbe72e34855ad574203294baafcc691d78903347 Mon Sep 17 00:00:00 2001 From: Eric Rosenbaum Date: Mon, 22 Jul 2019 16:10:31 -0400 Subject: [PATCH 27/61] Remove extra spaces --- src/lib/audio/audio-effects.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/lib/audio/audio-effects.js b/src/lib/audio/audio-effects.js index d449ff88e..b62f55788 100644 --- a/src/lib/audio/audio-effects.js +++ b/src/lib/audio/audio-effects.js @@ -44,13 +44,13 @@ class AudioEffects { break; case effectTypes.FASTER: this.playbackRate = pitchRatio; - adjustedAffectedSampleCount = Math.floor(affectedSampleCount / this.playbackRate); + adjustedAffectedSampleCount = Math.floor(affectedSampleCount / this.playbackRate); sampleCount = unaffectedSampleCount + adjustedAffectedSampleCount; break; case effectTypes.SLOWER: this.playbackRate = 1 / pitchRatio; - adjustedAffectedSampleCount = Math.floor(affectedSampleCount / this.playbackRate); + adjustedAffectedSampleCount = Math.floor(affectedSampleCount / this.playbackRate); sampleCount = unaffectedSampleCount + adjustedAffectedSampleCount; break; } From 40dc8487f45631c67a10efeb0fc6c3f7e6e17200 Mon Sep 17 00:00:00 2001 From: Eric Rosenbaum Date: Mon, 22 Jul 2019 17:47:44 -0400 Subject: [PATCH 28/61] Fix off-by-one error in reverse effect, add a test --- src/lib/audio/audio-effects.js | 2 +- test/unit/util/audio-effects.test.js | 19 +++++++++++++++++++ 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/src/lib/audio/audio-effects.js b/src/lib/audio/audio-effects.js index b62f55788..8dd551f38 100644 --- a/src/lib/audio/audio-effects.js +++ b/src/lib/audio/audio-effects.js @@ -83,7 +83,7 @@ class AudioEffects { const endSamples = Math.floor(this.trimEndSeconds * buffer.sampleRate); let counter = 0; for (let i = 0; i < bufferLength; i++) { - if (i > startSamples && i < endSamples) { + if (i >= startSamples && i < endSamples) { newBufferData[i] = originalBufferData[endSamples - counter - 1]; counter++; } else { diff --git a/test/unit/util/audio-effects.test.js b/test/unit/util/audio-effects.test.js index ac34f286f..f3d52152a 100644 --- a/test/unit/util/audio-effects.test.js +++ b/test/unit/util/audio-effects.test.js @@ -31,6 +31,25 @@ describe('Audio Effects manager', () => { test.skip('process starts the offline rendering context and returns a promise', () => { // @todo haven't been able to get web audio test api to actually run render }); + + test('reverse effect strictly reverses the samples', () => { + const fakeSound = [1, 2, 3, 4, 5, 6, 7, 8]; + + const fakeBuffer = audioContext.createBuffer(1, 8, 44100); + const bufferData = fakeBuffer.getChannelData(0); + fakeSound.forEach((sample, index) => { + bufferData[index] = sample; + }); + + // Reverse the entire sound + const reverseAll = new AudioEffects(fakeBuffer, 'reverse', 0, 1); + expect(Array.from(reverseAll.buffer.getChannelData(0))).toEqual(fakeSound.reverse()); + + // Reverse part of the sound + const reverseSelection = new AudioEffects(fakeBuffer, 'reverse', 0.25, 0.75); + const selectionReversed = [1, 2, 6, 5, 4, 3, 7, 8]; + expect(Array.from(reverseSelection.buffer.getChannelData(0))).toEqual(selectionReversed); + }); }); describe('Effects', () => { From d9409f0f0c4daed7a39500133f623eb9c16709f9 Mon Sep 17 00:00:00 2001 From: Eric Rosenbaum Date: Mon, 22 Jul 2019 17:48:25 -0400 Subject: [PATCH 29/61] Cleanup test --- test/unit/util/audio-effects.test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/unit/util/audio-effects.test.js b/test/unit/util/audio-effects.test.js index f3d52152a..ba6ea6b8f 100644 --- a/test/unit/util/audio-effects.test.js +++ b/test/unit/util/audio-effects.test.js @@ -64,7 +64,7 @@ describe('Effects', () => { expect(robotEffect.input).toBeInstanceOf(AudioNode); expect(robotEffect.output).toBeInstanceOf(AudioNode); - const echoEffect = new EchoEffect(audioContext, 0.75, 0, 1); + const echoEffect = new EchoEffect(audioContext, 0, 1); expect(echoEffect.input).toBeInstanceOf(AudioNode); expect(echoEffect.output).toBeInstanceOf(AudioNode); From 324f3a6687a260d9c3f3de7edb64c298cd5c04de Mon Sep 17 00:00:00 2001 From: Eric Rosenbaum Date: Tue, 23 Jul 2019 11:08:05 -0400 Subject: [PATCH 30/61] styles for trim handles and play button --- .../audio-trimmer/audio-trimmer.css | 32 ++++++++----------- src/components/audio-trimmer/icon--handle.svg | 26 +++++++-------- src/components/sound-editor/sound-editor.css | 8 +++-- 3 files changed, 32 insertions(+), 34 deletions(-) diff --git a/src/components/audio-trimmer/audio-trimmer.css b/src/components/audio-trimmer/audio-trimmer.css index 701b24eff..e7b2fc24d 100644 --- a/src/components/audio-trimmer/audio-trimmer.css +++ b/src/components/audio-trimmer/audio-trimmer.css @@ -1,10 +1,11 @@ @import "../../css/colors.css"; $border-radius: 4px; -$trim-handle-width: 12px; -$trim-handle-height: 14px; +$trim-handle-width: 30px; +$trim-handle-height: 30px; +$trim-handle-border: 3px; $stripe-size: 10px; -$hover-scale: 2; +$hover-scale: 1.25; .absolute { position: absolute; @@ -88,6 +89,10 @@ $hover-scale: 2; border-right: none; } +.right-handle { + transform: scaleX(-1); +} + .selector .left-handle { left: 0 } @@ -106,19 +111,19 @@ $hover-scale: 2; .trim-handle { position: absolute; - left: calc(-$trim-handle-width / 2); width: $trim-handle-width; height: $trim-handle-height; } -.selector .trim-handle { - filter: hue-rotate(180deg); +.trimmer .trim-handle { + filter: hue-rotate(150deg); } .trim-handle img { position: absolute; width: $trim-handle-width; height: $trim-handle-height; + left: calc(-$trim-handle-width + $trim-handle-border * 1.5); /* Make sure image dragging isn't triggered */ user-select: none; @@ -129,22 +134,13 @@ $hover-scale: 2; } .top-trim-handle { - top: -$trim-handle-height; + top: calc(-$trim-handle-height + $trim-handle-border); } .bottom-trim-handle { - bottom: -$trim-handle-height; + bottom: calc(-$trim-handle-height + $trim-handle-border); } .top-trim-handle img { - transform: rotate(180deg); -} - -/* Increase handle size when anywhere on draggable area is hovered */ -.trim-background:hover img { - transform: scale($hover-scale); -} - -.trim-background:hover .top-trim-handle img { - transform: rotate(180deg) scale($hover-scale); + transform: scaleY(-1); } diff --git a/src/components/audio-trimmer/icon--handle.svg b/src/components/audio-trimmer/icon--handle.svg index a3f52078f..dad1ea751 100644 --- a/src/components/audio-trimmer/icon--handle.svg +++ b/src/components/audio-trimmer/icon--handle.svg @@ -1,18 +1,16 @@ - - - - handle + + + + Bottom Left Handle Created with Sketch. - - - - - - - - - - + + + + + + + + diff --git a/src/components/sound-editor/sound-editor.css b/src/components/sound-editor/sound-editor.css index 406a26430..2ad9bb48a 100644 --- a/src/components/sound-editor/sound-editor.css +++ b/src/components/sound-editor/sound-editor.css @@ -64,6 +64,9 @@ border: 1px solid $ui-black-transparent; border-radius: 5px; padding: 3px; + + margin-top: 20px; + margin-bottom: 20px; } $border-radius: 0.25rem; @@ -93,9 +96,9 @@ $border-radius: 0.25rem; height: 3rem; width: 3rem; outline: none; - background: white; + background: $motion-primary; border-radius: 100%; - border: 1px solid $ui-black-transparent; + border: 4px solid $ui-white-dim; cursor: pointer; padding: 0.75rem; user-select: none; @@ -105,6 +108,7 @@ $border-radius: 0.25rem; flex-grow: 1; max-width: 100%; max-height: 100%; + filter: grayscale(1) brightness(10); /*min-width: 1.5rem;*/ } From da4b492e082f3a57d7bca1b5cd4c33e7b515a648 Mon Sep 17 00:00:00 2001 From: Eric Rosenbaum Date: Tue, 23 Jul 2019 14:21:18 -0400 Subject: [PATCH 31/61] Show trim handles over the edge of the asset panel --- src/components/asset-panel/asset-panel.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/asset-panel/asset-panel.css b/src/components/asset-panel/asset-panel.css index bfd578909..b974fea6b 100644 --- a/src/components/asset-panel/asset-panel.css +++ b/src/components/asset-panel/asset-panel.css @@ -24,7 +24,7 @@ display: flex; flex-grow: 1; flex-shrink: 1; - overflow-y: auto; + overflow: visible; } [dir="ltr"] .detail-area { From e1100f975c8937e64b1f8afc09d56c9008ad6d3c Mon Sep 17 00:00:00 2001 From: Eric Rosenbaum Date: Tue, 23 Jul 2019 14:48:22 -0400 Subject: [PATCH 32/61] Fix handle div position --- src/components/audio-trimmer/audio-trimmer.css | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/components/audio-trimmer/audio-trimmer.css b/src/components/audio-trimmer/audio-trimmer.css index e7b2fc24d..dd3b5e9a2 100644 --- a/src/components/audio-trimmer/audio-trimmer.css +++ b/src/components/audio-trimmer/audio-trimmer.css @@ -113,6 +113,7 @@ $hover-scale: 1.25; position: absolute; width: $trim-handle-width; height: $trim-handle-height; + right: 0; } .trimmer .trim-handle { @@ -123,7 +124,7 @@ $hover-scale: 1.25; position: absolute; width: $trim-handle-width; height: $trim-handle-height; - left: calc(-$trim-handle-width + $trim-handle-border * 1.5); + left: calc($trim-handle-border * 1.5); /* Make sure image dragging isn't triggered */ user-select: none; From 0eacc4b7187f43ec0527abae915b13f962ea0679 Mon Sep 17 00:00:00 2001 From: Eric Rosenbaum Date: Tue, 23 Jul 2019 15:00:22 -0400 Subject: [PATCH 33/61] Prevent undo button layout from breaking --- src/components/sound-editor/sound-editor.css | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/components/sound-editor/sound-editor.css b/src/components/sound-editor/sound-editor.css index 2ad9bb48a..87d068803 100644 --- a/src/components/sound-editor/sound-editor.css +++ b/src/components/sound-editor/sound-editor.css @@ -188,6 +188,10 @@ $border-radius: 0.25rem; margin-right: 1rem; } +.button-group { + display: flex; +} + .button-group .button { border-radius: 0; } From bf5b05e63f4266dfe101aa43d8b2e4e4a7dc6f06 Mon Sep 17 00:00:00 2001 From: Eric Rosenbaum Date: Tue, 23 Jul 2019 15:00:35 -0400 Subject: [PATCH 34/61] Re-order effects buttons and remove echo --- src/components/sound-editor/sound-editor.jsx | 30 ++++++++------------ 1 file changed, 12 insertions(+), 18 deletions(-) diff --git a/src/components/sound-editor/sound-editor.jsx b/src/components/sound-editor/sound-editor.jsx index bea5fa84e..c48107176 100644 --- a/src/components/sound-editor/sound-editor.jsx +++ b/src/components/sound-editor/sound-editor.jsx @@ -265,18 +265,6 @@ const SoundEditor = props => ( title={} onClick={props.onSlower} /> - } - onClick={props.onEcho} - /> - } - onClick={props.onRobot} - /> ( /> } - onClick={props.onReverse} + img={fadeInIcon} + title={} + onClick={props.onFadeIn} /> ( /> } - onClick={props.onFadeIn} + img={reverseIcon} + title={} + onClick={props.onReverse} + /> + } + onClick={props.onRobot} />
From 3ab1b8254b4da260f2ab3b4a98271bd99ff7da71 Mon Sep 17 00:00:00 2001 From: Eric Rosenbaum Date: Tue, 23 Jul 2019 15:29:03 -0400 Subject: [PATCH 35/61] Fix tool button styles --- src/components/icon-button/icon-button.css | 3 ++- src/components/sound-editor/sound-editor.css | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/components/icon-button/icon-button.css b/src/components/icon-button/icon-button.css index aa827a3e2..002c817ae 100644 --- a/src/components/icon-button/icon-button.css +++ b/src/components/icon-button/icon-button.css @@ -17,6 +17,7 @@ .title { margin-top: 0.5rem; + text-align: center; } .disabled { @@ -25,5 +26,5 @@ } .container:active { - background-color: $motion-transparent; + background-color: $motion-light-transparent; } diff --git a/src/components/sound-editor/sound-editor.css b/src/components/sound-editor/sound-editor.css index 87d068803..34699a3cf 100644 --- a/src/components/sound-editor/sound-editor.css +++ b/src/components/sound-editor/sound-editor.css @@ -159,6 +159,7 @@ $border-radius: 0.25rem; color: $text-primary; font-size: 0.625rem; user-select: none; + padding: 0.25rem 0; } .tool-button + .tool-button { @@ -167,7 +168,7 @@ $border-radius: 0.25rem; .tool-button img { width: 4rem; - height: 1.25rem; + height: 1.5rem; margin-bottom: -0.375rem; } From c9ec59dc8a20d9ed14383368d438b6ad41a36758 Mon Sep 17 00:00:00 2001 From: Eric Rosenbaum Date: Tue, 23 Jul 2019 15:37:00 -0400 Subject: [PATCH 36/61] Remove unused icon --- src/components/sound-editor/sound-editor.jsx | 1 - 1 file changed, 1 deletion(-) diff --git a/src/components/sound-editor/sound-editor.jsx b/src/components/sound-editor/sound-editor.jsx index c48107176..761856548 100644 --- a/src/components/sound-editor/sound-editor.jsx +++ b/src/components/sound-editor/sound-editor.jsx @@ -17,7 +17,6 @@ import playIcon from '../record-modal/icon--play.svg'; import stopIcon from '../record-modal/icon--stop-playback.svg'; import redoIcon from './icon--redo.svg'; import undoIcon from './icon--undo.svg'; -import echoIcon from './icon--echo.svg'; import fasterIcon from './icon--faster.svg'; import slowerIcon from './icon--slower.svg'; import louderIcon from './icon--louder.svg'; From 5d2287b0b5384c2aa0693cc3c68425c6c35dd59e Mon Sep 17 00:00:00 2001 From: Eric Rosenbaum Date: Tue, 23 Jul 2019 16:05:29 -0400 Subject: [PATCH 37/61] Use DragRecognizer to fix draggging on touch --- src/containers/audio-selector.jsx | 57 ++++++++++++++----------------- 1 file changed, 25 insertions(+), 32 deletions(-) diff --git a/src/containers/audio-selector.jsx b/src/containers/audio-selector.jsx index 30ef003a9..7f2b1e2b2 100644 --- a/src/containers/audio-selector.jsx +++ b/src/containers/audio-selector.jsx @@ -3,6 +3,7 @@ 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; @@ -27,6 +28,19 @@ class AudioSelector extends React.Component { }; 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; @@ -39,22 +53,18 @@ class AudioSelector extends React.Component { 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.initialTrimEnd = (getEventXY(e).x - 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); + this.containerSize = width; + this.trimEndDragRecognizer.start(e); } - 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(1, this.initialTrimStart + dx)); if (newTrim > this.initialTrimEnd) { this.setState({ @@ -66,11 +76,9 @@ class AudioSelector extends React.Component { trimStart: 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(0, this.initialTrimEnd + dx)); if (newTrim < this.initialTrimStart) { this.setState({ @@ -82,20 +90,11 @@ class AudioSelector extends React.Component { 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 @@ -109,23 +108,17 @@ class AudioSelector extends React.Component { } } handleTrimStartMouseDown (e) { - this.initialX = getEventXY(e).x; + this.containerSize = this.containerElement.getBoundingClientRect().width; + this.trimStartDragRecognizer.start(e); 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.containerSize = this.containerElement.getBoundingClientRect().width; + this.trimEndDragRecognizer.start(e); 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) { From 0f17157a3dc5cd62804a0db73074ba65fc2422bc Mon Sep 17 00:00:00 2001 From: Eric Rosenbaum Date: Tue, 23 Jul 2019 16:14:56 -0400 Subject: [PATCH 38/61] Update tests without echo effect --- test/integration/sounds.test.js | 1 - .../__snapshots__/sound-editor.test.jsx.snap | 62 +++++++------------ test/unit/components/sound-editor.test.jsx | 3 - 3 files changed, 22 insertions(+), 44 deletions(-) diff --git a/test/integration/sounds.test.js b/test/integration/sounds.test.js index 29ffba49f..1a6d7e393 100644 --- a/test/integration/sounds.test.js +++ b/test/integration/sounds.test.js @@ -55,7 +55,6 @@ describe('Working with sounds', () => { await clickText('Faster'); await clickText('Slower'); await clickText('Robot'); - await clickText('Echo'); await clickText('Reverse'); const logs = await getLogs(); diff --git a/test/unit/components/__snapshots__/sound-editor.test.jsx.snap b/test/unit/components/__snapshots__/sound-editor.test.jsx.snap index 406192549..c0c5441ae 100644 --- a/test/unit/components/__snapshots__/sound-editor.test.jsx.snap +++ b/test/unit/components/__snapshots__/sound-editor.test.jsx.snap @@ -397,42 +397,6 @@ exports[`Sound Editor Component matches snapshot 1`] = `
-
- -
- - Echo - -
-
-
- -
- - Robot - -
-
- Reverse + Fade in
@@ -525,7 +489,7 @@ exports[`Sound Editor Component matches snapshot 1`] = `
- Fade in + Reverse + +
+ +
+ +
+ + Robot
diff --git a/test/unit/components/sound-editor.test.jsx b/test/unit/components/sound-editor.test.jsx index 7fd465241..4b9d3805f 100644 --- a/test/unit/components/sound-editor.test.jsx +++ b/test/unit/components/sound-editor.test.jsx @@ -88,9 +88,6 @@ describe('Sound Editor Component', () => { wrapper.find('[children="Reverse"]').simulate('click'); expect(props.onReverse).toHaveBeenCalled(); - wrapper.find('[children="Echo"]').simulate('click'); - expect(props.onEcho).toHaveBeenCalled(); - wrapper.find('[children="Robot"]').simulate('click'); expect(props.onRobot).toHaveBeenCalled(); From 75dbed61fa3aa8c3ca9bf22d43ad1c1a9f21b0a6 Mon Sep 17 00:00:00 2001 From: Eric Rosenbaum Date: Tue, 23 Jul 2019 17:51:46 -0400 Subject: [PATCH 39/61] Fix bug where play after paste used old selection state --- src/containers/sound-editor.jsx | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/src/containers/sound-editor.jsx b/src/containers/sound-editor.jsx index 2de6077ca..930ad07df 100644 --- a/src/containers/sound-editor.jsx +++ b/src/containers/sound-editor.jsx @@ -334,6 +334,7 @@ class SoundEditor extends React.Component { newSamples.set(this.props.samples, 0); newSamples.set(this.state.copyBuffer.samples, this.props.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 * this.props.samples.length; @@ -352,15 +353,12 @@ class SoundEditor extends React.Component { 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.submitNewSamples(newSamples, this.props.sampleRate, false); + }, this.handlePlay); } - - this.handlePlay(); } handlePaste () { if (!this.state.copyBuffer) return; From 6be4e5e1228ad7c0424fcd2559fd28a422401b54 Mon Sep 17 00:00:00 2001 From: Eric Rosenbaum Date: Tue, 23 Jul 2019 18:00:38 -0400 Subject: [PATCH 40/61] Fix mirroring of effect icons for RTL --- src/components/sound-editor/sound-editor.css | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/components/sound-editor/sound-editor.css b/src/components/sound-editor/sound-editor.css index 34699a3cf..03cccb22e 100644 --- a/src/components/sound-editor/sound-editor.css +++ b/src/components/sound-editor/sound-editor.css @@ -172,12 +172,16 @@ $border-radius: 0.25rem; margin-bottom: -0.375rem; } -/* mirror the louder/softer speaker icons when rtl */ -[dir="rtl"] .effect-button:nth-of-type(6) img { +/* mirror the louder/softer/mute speaker icons when rtl */ +[dir="rtl"] .effect-button:nth-of-type(4) img { transform: scaleX(-1); } -[dir="rtl"] .effect-button:nth-of-type(7) img { +[dir="rtl"] .effect-button:nth-of-type(5) img { + transform: scaleX(-1); +} + +[dir="rtl"] .effect-button:nth-of-type(6) img { transform: scaleX(-1); } From d882d180a5fdcd3a94c16b866de4e395c9276b03 Mon Sep 17 00:00:00 2001 From: Eric Rosenbaum Date: Wed, 24 Jul 2019 11:07:10 -0400 Subject: [PATCH 41/61] Use separate play and stop icons for sound editor --- src/components/sound-editor/icon--play.svg | 20 +++++++++++++++++++ src/components/sound-editor/icon--stop.svg | 21 ++++++++++++++++++++ src/components/sound-editor/sound-editor.css | 1 - src/components/sound-editor/sound-editor.jsx | 4 ++-- 4 files changed, 43 insertions(+), 3 deletions(-) create mode 100644 src/components/sound-editor/icon--play.svg create mode 100644 src/components/sound-editor/icon--stop.svg diff --git a/src/components/sound-editor/icon--play.svg b/src/components/sound-editor/icon--play.svg new file mode 100644 index 000000000..817a2cab9 --- /dev/null +++ b/src/components/sound-editor/icon--play.svg @@ -0,0 +1,20 @@ + + + + play + Created with Sketch. + + + + + + + + + + + + + + + diff --git a/src/components/sound-editor/icon--stop.svg b/src/components/sound-editor/icon--stop.svg new file mode 100644 index 000000000..c960e66c3 --- /dev/null +++ b/src/components/sound-editor/icon--stop.svg @@ -0,0 +1,21 @@ + + + + stop-playback + Created with Sketch. + + + + + + + + + + + + + + + + diff --git a/src/components/sound-editor/sound-editor.css b/src/components/sound-editor/sound-editor.css index 03cccb22e..f46bcac21 100644 --- a/src/components/sound-editor/sound-editor.css +++ b/src/components/sound-editor/sound-editor.css @@ -108,7 +108,6 @@ $border-radius: 0.25rem; flex-grow: 1; max-width: 100%; max-height: 100%; - filter: grayscale(1) brightness(10); /*min-width: 1.5rem;*/ } diff --git a/src/components/sound-editor/sound-editor.jsx b/src/components/sound-editor/sound-editor.jsx index 761856548..b0e97a174 100644 --- a/src/components/sound-editor/sound-editor.jsx +++ b/src/components/sound-editor/sound-editor.jsx @@ -13,8 +13,8 @@ import IconButton from '../icon-button/icon-button.jsx'; import styles from './sound-editor.css'; -import playIcon from '../record-modal/icon--play.svg'; -import stopIcon from '../record-modal/icon--stop-playback.svg'; +import playIcon from './icon--play.svg'; +import stopIcon from './icon--stop.svg'; import redoIcon from './icon--redo.svg'; import undoIcon from './icon--undo.svg'; import fasterIcon from './icon--faster.svg'; From 79a819989f6f09c7ea41df5ece3e4baf3f114b09 Mon Sep 17 00:00:00 2001 From: Eric Rosenbaum Date: Wed, 24 Jul 2019 11:22:25 -0400 Subject: [PATCH 42/61] Prevent img drag and select issues --- src/components/audio-trimmer/audio-trimmer.css | 1 + src/containers/audio-selector.jsx | 4 ++++ 2 files changed, 5 insertions(+) diff --git a/src/components/audio-trimmer/audio-trimmer.css b/src/components/audio-trimmer/audio-trimmer.css index dd3b5e9a2..bfb27c677 100644 --- a/src/components/audio-trimmer/audio-trimmer.css +++ b/src/components/audio-trimmer/audio-trimmer.css @@ -114,6 +114,7 @@ $hover-scale: 1.25; width: $trim-handle-width; height: $trim-handle-height; right: 0; + user-select: none; } .trimmer .trim-handle { diff --git a/src/containers/audio-selector.jsx b/src/containers/audio-selector.jsx index 7f2b1e2b2..e3eaf3ab3 100644 --- a/src/containers/audio-selector.jsx +++ b/src/containers/audio-selector.jsx @@ -62,6 +62,8 @@ class AudioSelector extends React.Component { this.containerSize = width; this.trimEndDragRecognizer.start(e); + + e.preventDefault(); } handleTrimStartMouseMove (currentOffset, initialOffset) { const dx = (currentOffset.x - initialOffset.x) / this.containerSize; @@ -113,6 +115,7 @@ class AudioSelector extends React.Component { this.initialTrimStart = this.props.trimStart; this.initialTrimEnd = this.props.trimEnd; e.stopPropagation(); + e.preventDefault(); } handleTrimEndMouseDown (e) { this.containerSize = this.containerElement.getBoundingClientRect().width; @@ -120,6 +123,7 @@ class AudioSelector extends React.Component { this.initialTrimEnd = this.props.trimEnd; this.initialTrimStart = this.props.trimStart; e.stopPropagation(); + e.preventDefault(); } storeRef (el) { this.containerElement = el; From 24d2e344e5b58a219724446a72bd8c48729d269f Mon Sep 17 00:00:00 2001 From: Eric Rosenbaum Date: Wed, 24 Jul 2019 11:31:20 -0400 Subject: [PATCH 43/61] Prevent default on keyboard shortcuts --- src/containers/sound-editor.jsx | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/containers/sound-editor.jsx b/src/containers/sound-editor.jsx index 930ad07df..bd0599e8d 100644 --- a/src/containers/sound-editor.jsx +++ b/src/containers/sound-editor.jsx @@ -79,6 +79,7 @@ class SoundEditor extends React.Component { return; } if (event.key === ' ') { + event.preventDefault(); if (this.state.playhead) { this.handleStopPlaying(); } else { @@ -86,6 +87,7 @@ class SoundEditor extends React.Component { } } if (event.key === 'Delete' || event.key === 'Backspace') { + event.preventDefault(); if (event.shiftKey) { this.handleDeleteInverse(); } else { @@ -98,18 +100,23 @@ class SoundEditor extends React.Component { } 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); } } From 6e5f3786339e0e4baadbb9b6f19034188e732f22 Mon Sep 17 00:00:00 2001 From: Eric Rosenbaum Date: Wed, 24 Jul 2019 12:05:27 -0400 Subject: [PATCH 44/61] Prevent buffer detach issues with copy/paste on firefox --- src/containers/sound-editor.jsx | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/src/containers/sound-editor.jsx b/src/containers/sound-editor.jsx index bd0599e8d..bb0bfae59 100644 --- a/src/containers/sound-editor.jsx +++ b/src/containers/sound-editor.jsx @@ -295,10 +295,9 @@ class SoundEditor extends React.Component { const trimStart = this.state.trimStart === null ? 0.0 : this.state.trimStart; const trimEnd = this.state.trimEnd === null ? 1.0 : this.state.trimEnd; - const trimStartSamples = trimStart * this.props.samples.length; - const trimEndSamples = trimEnd * this.props.samples.length; - 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({ @@ -335,19 +334,20 @@ class SoundEditor extends React.Component { } paste () { // If there's no selection, paste at the end of the sound + const {samples} = this.copyCurrentBuffer(); if (this.state.trimStart === null) { - const newLength = this.props.samples.length + this.state.copyBuffer.samples.length; + const newLength = samples.length + this.state.copyBuffer.samples.length; const newSamples = new Float32Array(newLength); - newSamples.set(this.props.samples, 0); - newSamples.set(this.state.copyBuffer.samples, this.props.samples.length); + 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 * this.props.samples.length; - const trimEndSamples = this.state.trimEnd * this.props.samples.length; - const firstPart = this.props.samples.slice(0, trimStartSamples); - const lastPart = this.props.samples.slice(trimEndSamples); + 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); From aaa9c355e9852ad0f9f7377e1f38ad4165dd68f5 Mon Sep 17 00:00:00 2001 From: Eric Rosenbaum Date: Wed, 24 Jul 2019 14:02:20 -0400 Subject: [PATCH 45/61] Use svgs w/o masks to prevent blurriness on safari --- .../sound-editor/icon--copy-to-new.svg | 13 ++------ src/components/sound-editor/icon--copy.svg | 28 ++++------------- src/components/sound-editor/icon--fade-in.svg | 30 ++++--------------- .../sound-editor/icon--fade-out.svg | 30 ++++--------------- src/components/sound-editor/icon--mute.svg | 13 ++------ src/components/sound-editor/icon--paste.svg | 13 ++------ 6 files changed, 21 insertions(+), 106 deletions(-) diff --git a/src/components/sound-editor/icon--copy-to-new.svg b/src/components/sound-editor/icon--copy-to-new.svg index 8bfc76ac5..d5f462117 100644 --- a/src/components/sound-editor/icon--copy-to-new.svg +++ b/src/components/sound-editor/icon--copy-to-new.svg @@ -1,18 +1,9 @@ - + Sound/General/Copy to New Created with Sketch. - - - - - - - - - - + \ No newline at end of file diff --git a/src/components/sound-editor/icon--copy.svg b/src/components/sound-editor/icon--copy.svg index 4b902ad12..7bac62c1b 100644 --- a/src/components/sound-editor/icon--copy.svg +++ b/src/components/sound-editor/icon--copy.svg @@ -1,32 +1,14 @@ - + Sound/General/Copy Created with Sketch. - - - - - - - - - - - - - - + + - - - - - - - - + + \ No newline at end of file diff --git a/src/components/sound-editor/icon--fade-in.svg b/src/components/sound-editor/icon--fade-in.svg index 6bd9e6974..6c7487584 100644 --- a/src/components/sound-editor/icon--fade-in.svg +++ b/src/components/sound-editor/icon--fade-in.svg @@ -1,34 +1,14 @@ - + Sound/Effects/Fade-in Created with Sketch. - - - - - - - - - - - - - - + + - - - - - - - - - - + + \ No newline at end of file diff --git a/src/components/sound-editor/icon--fade-out.svg b/src/components/sound-editor/icon--fade-out.svg index d6704bc0e..96e8d6244 100644 --- a/src/components/sound-editor/icon--fade-out.svg +++ b/src/components/sound-editor/icon--fade-out.svg @@ -1,34 +1,14 @@ - + Sound/Effects/Fade-out Created with Sketch. - - - - - - - - - - - - - - + + - - - - - - - - - - + + \ No newline at end of file diff --git a/src/components/sound-editor/icon--mute.svg b/src/components/sound-editor/icon--mute.svg index 1fcbbd559..6cdd71d4d 100644 --- a/src/components/sound-editor/icon--mute.svg +++ b/src/components/sound-editor/icon--mute.svg @@ -3,16 +3,7 @@ Sound/Effects/Mute Created with Sketch. - - - - - - - - - - + - \ No newline at end of file + diff --git a/src/components/sound-editor/icon--paste.svg b/src/components/sound-editor/icon--paste.svg index 5d8bd9063..53ca104c1 100644 --- a/src/components/sound-editor/icon--paste.svg +++ b/src/components/sound-editor/icon--paste.svg @@ -1,18 +1,9 @@ - + Sound/General/Paste Created with Sketch. - - - - - - - - - - + \ No newline at end of file From bc3e69d04746ca9bfe52d3d055cc1f05fa61f9f1 Mon Sep 17 00:00:00 2001 From: Eric Rosenbaum Date: Wed, 24 Jul 2019 14:08:36 -0400 Subject: [PATCH 46/61] Prevent dragging img in trimmer handles on firefox --- src/containers/audio-trimmer.jsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/containers/audio-trimmer.jsx b/src/containers/audio-trimmer.jsx index 9cf6e38f1..dcfcb7502 100644 --- a/src/containers/audio-trimmer.jsx +++ b/src/containers/audio-trimmer.jsx @@ -52,6 +52,7 @@ class AudioTrimmer extends React.Component { window.addEventListener('mouseup', this.handleTrimStartMouseUp); window.addEventListener('touchmove', this.handleTrimStartMouseMove); window.addEventListener('touchend', this.handleTrimStartMouseUp); + e.preventDefault(); } handleTrimEndMouseDown (e) { this.initialX = getEventXY(e).x; @@ -60,6 +61,7 @@ class AudioTrimmer extends React.Component { window.addEventListener('mouseup', this.handleTrimEndMouseUp); window.addEventListener('touchmove', this.handleTrimEndMouseMove); window.addEventListener('touchend', this.handleTrimEndMouseUp); + e.preventDefault(); } storeRef (el) { this.containerElement = el; From c23e4c46115b6cc896445a88c3826e1bf59aacec Mon Sep 17 00:00:00 2001 From: Eric Rosenbaum Date: Wed, 24 Jul 2019 15:03:40 -0400 Subject: [PATCH 47/61] Improve effect button active state padding --- src/components/sound-editor/sound-editor.css | 1 + 1 file changed, 1 insertion(+) diff --git a/src/components/sound-editor/sound-editor.css b/src/components/sound-editor/sound-editor.css index f46bcac21..12d0106ad 100644 --- a/src/components/sound-editor/sound-editor.css +++ b/src/components/sound-editor/sound-editor.css @@ -141,6 +141,7 @@ $border-radius: 0.25rem; color: $text-primary; font-size: 0.625rem; user-select: none; + padding: 0.25rem 0; } .effect-button + .effect-button { From f28b6d8e00f3f7aecd86bf6a785167892c616b69 Mon Sep 17 00:00:00 2001 From: Eric Rosenbaum Date: Wed, 24 Jul 2019 17:01:56 -0400 Subject: [PATCH 48/61] Make the waveform rendering more precisely positioned --- src/components/sound-editor/sound-editor.css | 1 - src/components/waveform/waveform.jsx | 13 +++++++------ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/components/sound-editor/sound-editor.css b/src/components/sound-editor/sound-editor.css index 12d0106ad..b9cb97f16 100644 --- a/src/components/sound-editor/sound-editor.css +++ b/src/components/sound-editor/sound-editor.css @@ -63,7 +63,6 @@ background: hsla(300, 53%, 60%, 0.15); border: 1px solid $ui-black-transparent; border-radius: 5px; - padding: 3px; margin-top: 20px; margin-bottom: 20px; diff --git a/src/components/waveform/waveform.jsx b/src/components/waveform/waveform.jsx index 48a0a4935..31b777aa7 100644 --- a/src/components/waveform/waveform.jsx +++ b/src/components/waveform/waveform.jsx @@ -19,17 +19,18 @@ class Waveform extends React.PureComponent { // composite time when animating the playhead const takeEveryN = Math.ceil(data.length / width); - const filteredData = takeEveryN === 1 ? data : + const filteredData = takeEveryN === 1 ? data.slice(0) : data.filter((_, i) => i % takeEveryN === 0); - const cappedData = [0, ...filteredData, 0]; + filteredData[0] = 0; + filteredData[filteredData.length - 1] = 0; const points = [ - ...cappedData.map((v, i) => - [width * i / cappedData.length, height * v / 2] + ...filteredData.map((v, i) => + [width * i / filteredData.length, height * v / 2] ), - ...cappedData.reverse().map((v, i) => - [width * (cappedData.length - i - 1) / cappedData.length, -height * v / 2] + ...filteredData.reverse().map((v, i) => + [width * (filteredData.length - i - 1) / filteredData.length, -height * v / 2] ) ]; const pathComponents = points.map(([x, y], i) => { From d8df13a12c3fd5ebeb43f09a7ab0fe582f371ce3 Mon Sep 17 00:00:00 2001 From: Eric Rosenbaum Date: Wed, 24 Jul 2019 17:02:54 -0400 Subject: [PATCH 49/61] Prevent selection from moving when handles cross over --- src/components/audio-trimmer/audio-trimmer.css | 8 ++++---- src/containers/audio-selector.jsx | 4 +++- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/src/components/audio-trimmer/audio-trimmer.css b/src/components/audio-trimmer/audio-trimmer.css index bfb27c677..f3ce9fa42 100644 --- a/src/components/audio-trimmer/audio-trimmer.css +++ b/src/components/audio-trimmer/audio-trimmer.css @@ -94,19 +94,19 @@ $hover-scale: 1.25; } .selector .left-handle { - left: 0 + left: -1px } .selector .right-handle { - right: 0 + right: -1px } .trimmer .left-handle { - right: 0 + right: -1px } .trimmer .right-handle { - left: 0 + left: -1px } .trim-handle { diff --git a/src/containers/audio-selector.jsx b/src/containers/audio-selector.jsx index e3eaf3ab3..2d5974d5c 100644 --- a/src/containers/audio-selector.jsx +++ b/src/containers/audio-selector.jsx @@ -75,7 +75,8 @@ class AudioSelector extends React.Component { }); } else { this.setState({ - trimStart: newTrim + trimStart: newTrim, + trimEnd: this.initialTrimEnd }); } } @@ -89,6 +90,7 @@ class AudioSelector extends React.Component { }); } else { this.setState({ + trimStart: this.initialTrimStart, trimEnd: newTrim }); } From 0b9969c412cbf0ac35e083bc17d90413cdd525e4 Mon Sep 17 00:00:00 2001 From: Eric Rosenbaum Date: Wed, 24 Jul 2019 17:11:36 -0400 Subject: [PATCH 50/61] Update snapshot --- test/unit/components/__snapshots__/sound-editor.test.jsx.snap | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/unit/components/__snapshots__/sound-editor.test.jsx.snap b/test/unit/components/__snapshots__/sound-editor.test.jsx.snap index c0c5441ae..2a2d849ef 100644 --- a/test/unit/components/__snapshots__/sound-editor.test.jsx.snap +++ b/test/unit/components/__snapshots__/sound-editor.test.jsx.snap @@ -150,7 +150,7 @@ exports[`Sound Editor Component matches snapshot 1`] = ` > From c215104522f3540d6c0250020d9787765b442b9b Mon Sep 17 00:00:00 2001 From: Eric Rosenbaum Date: Thu, 25 Jul 2019 14:33:49 -0400 Subject: [PATCH 51/61] =?UTF-8?q?Only=20deselect-outside=20if=20there?= =?UTF-8?q?=E2=80=99s=20a=20selection?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/containers/sound-editor.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/containers/sound-editor.jsx b/src/containers/sound-editor.jsx index bb0bfae59..a2b19719f 100644 --- a/src/containers/sound-editor.jsx +++ b/src/containers/sound-editor.jsx @@ -384,7 +384,7 @@ class SoundEditor extends React.Component { } handleContainerClick (e) { // If the click is on the sound editor's div (and not any other element), delesect - if (e.target === this.ref) { + if (e.target === this.ref && this.state.trimStart !== null) { this.handleUpdateTrim(null, null); } } From afccf651579be5226c91c3ebae75305e6d2dc0a8 Mon Sep 17 00:00:00 2001 From: Eric Rosenbaum Date: Thu, 25 Jul 2019 15:10:13 -0400 Subject: [PATCH 52/61] Improve accuracy of waveform rendering --- src/components/waveform/waveform.jsx | 20 ++++++++------------ 1 file changed, 8 insertions(+), 12 deletions(-) diff --git a/src/components/waveform/waveform.jsx b/src/components/waveform/waveform.jsx index 31b777aa7..3c7b4d26b 100644 --- a/src/components/waveform/waveform.jsx +++ b/src/components/waveform/waveform.jsx @@ -22,15 +22,18 @@ class Waveform extends React.PureComponent { const filteredData = takeEveryN === 1 ? data.slice(0) : data.filter((_, i) => i % takeEveryN === 0); - filteredData[0] = 0; - filteredData[filteredData.length - 1] = 0; + // Need at least two points to render waveform. + if (filteredData.length === 1) { + filteredData.push(filteredData[0]); + } + const maxIndex = filteredData.length - 1; const points = [ ...filteredData.map((v, i) => - [width * i / filteredData.length, height * v / 2] + [width * (i / maxIndex), height * v / 2] ), ...filteredData.reverse().map((v, i) => - [width * (filteredData.length - i - 1) / filteredData.length, -height * v / 2] + [width * (1 - (i / maxIndex)), -height * v / 2] ) ]; const pathComponents = points.map(([x, y], i) => { @@ -41,15 +44,8 @@ class Waveform extends React.PureComponent { return ( - Date: Thu, 25 Jul 2019 15:12:04 -0400 Subject: [PATCH 53/61] Update snapshot --- .../__snapshots__/sound-editor.test.jsx.snap | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/test/unit/components/__snapshots__/sound-editor.test.jsx.snap b/test/unit/components/__snapshots__/sound-editor.test.jsx.snap index 2a2d849ef..af7a255a4 100644 --- a/test/unit/components/__snapshots__/sound-editor.test.jsx.snap +++ b/test/unit/components/__snapshots__/sound-editor.test.jsx.snap @@ -136,21 +136,14 @@ exports[`Sound Editor Component matches snapshot 1`] = ` > - From b0dbe9e323a6704c90ca70036a332540a841b6b3 Mon Sep 17 00:00:00 2001 From: Eric Rosenbaum Date: Thu, 25 Jul 2019 15:29:10 -0400 Subject: [PATCH 54/61] Flip copy to new button in RTL, and use a class for flipping buttons --- src/components/sound-editor/sound-editor.css | 11 +---------- src/components/sound-editor/sound-editor.jsx | 8 ++++---- 2 files changed, 5 insertions(+), 14 deletions(-) diff --git a/src/components/sound-editor/sound-editor.css b/src/components/sound-editor/sound-editor.css index b9cb97f16..7a791e6ab 100644 --- a/src/components/sound-editor/sound-editor.css +++ b/src/components/sound-editor/sound-editor.css @@ -171,16 +171,7 @@ $border-radius: 0.25rem; margin-bottom: -0.375rem; } -/* mirror the louder/softer/mute speaker icons when rtl */ -[dir="rtl"] .effect-button:nth-of-type(4) img { - transform: scaleX(-1); -} - -[dir="rtl"] .effect-button:nth-of-type(5) img { - transform: scaleX(-1); -} - -[dir="rtl"] .effect-button:nth-of-type(6) img { +[dir="rtl"] .flip-in-rtl img { transform: scaleX(-1); } diff --git a/src/components/sound-editor/sound-editor.jsx b/src/components/sound-editor/sound-editor.jsx index b0e97a174..84f56d08b 100644 --- a/src/components/sound-editor/sound-editor.jsx +++ b/src/components/sound-editor/sound-editor.jsx @@ -195,7 +195,7 @@ const SoundEditor = props => ( onClick={props.onPaste} /> ( onClick={props.onSlower} /> } onClick={props.onLouder} /> } onClick={props.onSofter} /> } onClick={props.onMute} From 032cd5aa026b2e01c2a9639f6aeb7e371e84b341 Mon Sep 17 00:00:00 2001 From: Eric Rosenbaum Date: Thu, 25 Jul 2019 15:53:07 -0400 Subject: [PATCH 55/61] Use DragRecognizer in AudioTrimmer --- src/containers/audio-trimmer.jsx | 55 ++++++++++++-------------------- 1 file changed, 20 insertions(+), 35 deletions(-) diff --git a/src/containers/audio-trimmer.jsx b/src/containers/audio-trimmer.jsx index dcfcb7502..78bd4ebac 100644 --- a/src/containers/audio-trimmer.jsx +++ b/src/containers/audio-trimmer.jsx @@ -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,54 +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); - e.preventDefault(); } 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); - e.preventDefault(); } storeRef (el) { this.containerElement = el; From 5472f0a3e92c508a0a5c8a33285e7309d3144a53 Mon Sep 17 00:00:00 2001 From: Eric Rosenbaum Date: Thu, 25 Jul 2019 16:32:27 -0400 Subject: [PATCH 56/61] Enable the accidentally disabled sound integration tests --- test/integration/sounds.test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/integration/sounds.test.js b/test/integration/sounds.test.js index 1a6d7e393..f511b9105 100644 --- a/test/integration/sounds.test.js +++ b/test/integration/sounds.test.js @@ -111,7 +111,7 @@ describe('Working with sounds', () => { await expect(logs).toEqual([]); }); - test.only('Adding multiple sounds at the same time', async () => { + test('Adding multiple sounds at the same time', async () => { const files = [ path.resolve(__dirname, '../fixtures/movie.wav'), path.resolve(__dirname, '../fixtures/sneaker.wav') From 1563f7892df2b0f3676c7467c4aa304efb1b96c3 Mon Sep 17 00:00:00 2001 From: Eric Rosenbaum Date: Thu, 25 Jul 2019 16:32:53 -0400 Subject: [PATCH 57/61] Add a test for the copy to new button --- test/integration/sounds.test.js | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/test/integration/sounds.test.js b/test/integration/sounds.test.js index f511b9105..c8cd343cd 100644 --- a/test/integration/sounds.test.js +++ b/test/integration/sounds.test.js @@ -131,4 +131,14 @@ describe('Working with sounds', () => { const logs = await getLogs(); await expect(logs).toEqual([]); }); + + test('Copy to new button adds a new sound', async () => { + await loadUri(uri); + await clickText('Sounds'); + await clickText('Copy to New', scope.soundsTab); + await clickText('Meow2', scope.soundsTab); + + const logs = await getLogs(); + await expect(logs).toEqual([]); + }); }); From 6100e0f1990355088366002e2ddb87a9109cf785 Mon Sep 17 00:00:00 2001 From: Eric Rosenbaum Date: Mon, 29 Jul 2019 14:00:04 -0400 Subject: [PATCH 58/61] Add integration tests for copy/paste --- test/integration/sounds.test.js | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/test/integration/sounds.test.js b/test/integration/sounds.test.js index c8cd343cd..62bbd5085 100644 --- a/test/integration/sounds.test.js +++ b/test/integration/sounds.test.js @@ -141,4 +141,29 @@ describe('Working with sounds', () => { const logs = await getLogs(); await expect(logs).toEqual([]); }); + + test('Copy and pasting within a sound changes its duration', async () => { + await loadUri(uri); + await clickText('Sounds'); + await findByText('0.85', scope.soundsTab); // Original meow sound duration + await clickText('Copy', scope.soundsTab); + await clickText('Paste', scope.soundsTab); + await findByText('1.70', scope.soundsTab); // Sound has doubled in duration + + const logs = await getLogs(); + await expect(logs).toEqual([]); + }); + + test('Can copy a sound from a sprite and paste into a sound on the stage', async () => { + await loadUri(uri); + await clickText('Sounds'); + await clickText('Copy', scope.soundsTab); // Copy the meow sound + await clickXpath('//span[text()="Stage"]'); + await findByText('0.02', scope.soundsTab); // Original pop sound duration + await clickText('Paste', scope.soundsTab); + await findByText('0.87', scope.soundsTab); // Duration of pop + meow sound + + const logs = await getLogs(); + await expect(logs).toEqual([]); + }); }); From 2890609053d9480133a21728a369f7ffbbe2847c Mon Sep 17 00:00:00 2001 From: Eric Rosenbaum Date: Tue, 30 Jul 2019 11:38:52 -0400 Subject: [PATCH 59/61] Add unit test for audio effect adjustedTrim times --- test/unit/util/audio-effects.test.js | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/test/unit/util/audio-effects.test.js b/test/unit/util/audio-effects.test.js index ba6ea6b8f..330fe12a1 100644 --- a/test/unit/util/audio-effects.test.js +++ b/test/unit/util/audio-effects.test.js @@ -28,6 +28,22 @@ describe('Audio Effects manager', () => { expect(audioEffects.audioContext._.length).toBeGreaterThan(400); }); + test('updates the trim positions after an effect has changed the length of selection', () => { + const slowerEffect = new AudioEffects(audioBuffer, 'slower', 0.25, 0.75); + expect(slowerEffect.adjustedTrimStartSeconds).toEqual(slowerEffect.trimStartSeconds); + expect(slowerEffect.adjustedTrimEndSeconds).toBeGreaterThan(slowerEffect.trimEndSeconds); + + const fasterEffect = new AudioEffects(audioBuffer, 'faster', 0.25, 0.75); + expect(fasterEffect.adjustedTrimStartSeconds).toEqual(fasterEffect.trimStartSeconds); + expect(fasterEffect.adjustedTrimEndSeconds).toBeLessThan(fasterEffect.trimEndSeconds); + + // Some effects do not change the length of the selection + const fadeEffect = new AudioEffects(audioBuffer, 'fade in', 0.25, 0.75); + expect(fadeEffect.adjustedTrimStartSeconds).toEqual(fadeEffect.trimStartSeconds); + // Should be within one millisecond (flooring can change the duration by one sample) + expect(fadeEffect.adjustedTrimEndSeconds).toBeCloseTo(fadeEffect.trimEndSeconds, 3); + }); + test.skip('process starts the offline rendering context and returns a promise', () => { // @todo haven't been able to get web audio test api to actually run render }); From 58499cec737a2faeb725208bb339c8a4c843a668 Mon Sep 17 00:00:00 2001 From: Eric Rosenbaum Date: Tue, 30 Jul 2019 14:57:25 -0400 Subject: [PATCH 60/61] Add integration test for sound editor keyboard shortcuts --- test/integration/sounds.test.js | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/test/integration/sounds.test.js b/test/integration/sounds.test.js index 62bbd5085..e564f511a 100644 --- a/test/integration/sounds.test.js +++ b/test/integration/sounds.test.js @@ -1,5 +1,6 @@ import path from 'path'; import SeleniumHelper from '../helpers/selenium-helper'; +import {Key} from 'selenium-webdriver'; const { clickText, @@ -166,4 +167,21 @@ describe('Working with sounds', () => { const logs = await getLogs(); await expect(logs).toEqual([]); }); + + test.only('Keyboard shortcuts', async () => { + await loadUri(uri); + await clickText('Sounds'); + const el = await findByXpath('//button[@aria-label="Choose a Sound"]'); + await el.sendKeys(Key.chord(Key.COMMAND, 'a')); // Select all + await findByText('0.85', scope.soundsTab); // Meow sound duration + await el.sendKeys(Key.DELETE); + await findByText('0.00', scope.soundsTab); // Sound is now empty + await el.sendKeys(Key.chord(Key.COMMAND, 'z')); // undo + await findByText('0.85', scope.soundsTab); // Meow sound is back + await el.sendKeys(Key.chord(Key.COMMAND, Key.SHIFT, 'z')); // redo + await findByText('0.00', scope.soundsTab); // Sound is empty again + + const logs = await getLogs(); + await expect(logs).toEqual([]); + }); }); From 6bbff14aa46bbb6c71ba17742668c703906cdbf1 Mon Sep 17 00:00:00 2001 From: Eric Rosenbaum Date: Tue, 30 Jul 2019 15:09:27 -0400 Subject: [PATCH 61/61] Update chromedriver --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 3a79a8529..838a5a48f 100644 --- a/package.json +++ b/package.json @@ -44,7 +44,7 @@ "babel-loader": "^8.0.4", "base64-loader": "1.0.0", "bowser": "1.9.4", - "chromedriver": "74.0.0", + "chromedriver": "75.1.0", "classnames": "2.2.6", "computed-style-to-inline-style": "3.0.0", "copy-webpack-plugin": "^4.5.1",