Merge pull request #578 from paulkaplan/sound-editor--trim
Add trimming to the sound editor
This commit is contained in:
commit
a06a839e45
11 files changed
+716
-60
No files matched your search
@@ -10,23 +10,25 @@ const AudioTrimmer = props => (
|
||||
className={styles.absolute}
|
||||
ref={props.containerRef}
|
||||
>
|
||||
<Box
|
||||
className={classNames(styles.absolute, styles.trimBackground, styles.startTrimBackground)}
|
||||
style={{
|
||||
width: `${100 * props.trimStart}%`
|
||||
}}
|
||||
onMouseDown={props.onTrimStartMouseDown}
|
||||
>
|
||||
<Box className={classNames(styles.absolute, styles.trimBackgroundMask)} />
|
||||
<Box className={classNames(styles.trimLine, styles.startTrimLine)}>
|
||||
<Box className={classNames(styles.trimHandle, styles.topTrimHandle, styles.startTrimHandle)}>
|
||||
<img src={handleIcon} />
|
||||
</Box>
|
||||
<Box className={classNames(styles.trimHandle, styles.bottomTrimHandle, styles.startTrimHandle)}>
|
||||
<img src={handleIcon} />
|
||||
{props.trimStart !== null ? (
|
||||
<Box
|
||||
className={classNames(styles.absolute, styles.trimBackground, styles.startTrimBackground)}
|
||||
style={{
|
||||
width: `${100 * props.trimStart}%`
|
||||
}}
|
||||
onMouseDown={props.onTrimStartMouseDown}
|
||||
>
|
||||
<Box className={classNames(styles.absolute, styles.trimBackgroundMask)} />
|
||||
<Box className={classNames(styles.trimLine, styles.startTrimLine)}>
|
||||
<Box className={classNames(styles.trimHandle, styles.topTrimHandle, styles.startTrimHandle)}>
|
||||
<img src={handleIcon} />
|
||||
</Box>
|
||||
<Box className={classNames(styles.trimHandle, styles.bottomTrimHandle, styles.startTrimHandle)}>
|
||||
<img src={handleIcon} />
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
) : null}
|
||||
|
||||
{props.playhead ? (
|
||||
<Box
|
||||
@@ -37,24 +39,26 @@ const AudioTrimmer = props => (
|
||||
/>
|
||||
) : null}
|
||||
|
||||
<Box
|
||||
className={classNames(styles.absolute, styles.trimBackground, styles.endTrimBackground)}
|
||||
style={{
|
||||
left: `${100 * props.trimEnd}%`,
|
||||
width: `${100 - 100 * props.trimEnd}%`
|
||||
}}
|
||||
onMouseDown={props.onTrimEndMouseDown}
|
||||
>
|
||||
<Box className={classNames(styles.absolute, styles.trimBackgroundMask)} />
|
||||
<Box className={classNames(styles.trimLine, styles.endTrimLine)}>
|
||||
<Box className={classNames(styles.trimHandle, styles.topTrimHandle, styles.endTrimHandle)}>
|
||||
<img src={handleIcon} />
|
||||
</Box>
|
||||
<Box className={classNames(styles.trimHandle, styles.bottomTrimHandle, styles.endTrimHandle)}>
|
||||
<img src={handleIcon} />
|
||||
{props.trimEnd !== null ? (
|
||||
<Box
|
||||
className={classNames(styles.absolute, styles.trimBackground, styles.endTrimBackground)}
|
||||
style={{
|
||||
left: `${100 * props.trimEnd}%`,
|
||||
width: `${100 - 100 * props.trimEnd}%`
|
||||
}}
|
||||
onMouseDown={props.onTrimEndMouseDown}
|
||||
>
|
||||
<Box className={classNames(styles.absolute, styles.trimBackgroundMask)} />
|
||||
<Box className={classNames(styles.trimLine, styles.endTrimLine)}>
|
||||
<Box className={classNames(styles.trimHandle, styles.topTrimHandle, styles.endTrimHandle)}>
|
||||
<img src={handleIcon} />
|
||||
</Box>
|
||||
<Box className={classNames(styles.trimHandle, styles.bottomTrimHandle, styles.endTrimHandle)}>
|
||||
<img src={handleIcon} />
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
) : null }
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -63,8 +67,8 @@ AudioTrimmer.propTypes = {
|
||||
onTrimEndMouseDown: PropTypes.func.isRequired,
|
||||
onTrimStartMouseDown: PropTypes.func.isRequired,
|
||||
playhead: PropTypes.number,
|
||||
trimEnd: PropTypes.number.isRequired,
|
||||
trimStart: PropTypes.number.isRequired
|
||||
trimEnd: PropTypes.number,
|
||||
trimStart: PropTypes.number
|
||||
};
|
||||
|
||||
export default AudioTrimmer;
|
||||
@@ -0,0 +1 @@
|
||||
<svg viewBox="-4 -1 16 16" xmlns="http://www.w3.org/2000/svg"><title>sissors</title><g fill="#4C97FF" fill-rule="evenodd"><path d="M5.874 10.102c-.437 0-.792.36-.792.806 0 .445.355.807.792.807.437 0 .793-.362.793-.807 0-.445-.356-.806-.793-.806m0 2.715C4.84 12.817 4 11.96 4 10.907 4 9.858 4.84 9 5.874 9c1.034 0 1.875.856 1.875 1.908 0 1.053-.842 1.91-1.876 1.91M1.874 10.102c-.437 0-.792.36-.792.806 0 .445.355.807.792.807.437 0 .793-.362.793-.807 0-.445-.356-.806-.793-.806m0 2.715C.84 12.817 0 11.96 0 10.907 0 9.858.84 9 1.874 9c1.034 0 1.875.856 1.875 1.908 0 1.053-.842 1.91-1.876 1.91M3.907 8.452c-.274 0-.495-.225-.495-.504 0-.275.22-.5.495-.5.27 0 .492.225.492.5 0 .28-.222.504-.493.504zm-.042-3.63l-.004.008L2.278.61C2.212.43 2.017.34 1.847.41l-.71.27C.578.905.293 1.545.51 2.12l2.27 6.06c.17.464.62.74 1.08.713.462.028.908-.248 1.083-.713l.088-.237-1.165-3.118zM5.564 6.396L4.4 3.276 5.44.61c.065-.18.26-.268.43-.198l.71.27c.563.222.845.862.628 1.436L5.564 6.396z"/></g></svg>
|
||||
|
After Width: | Height: | Size: 989 B |
@@ -36,17 +36,36 @@
|
||||
}
|
||||
|
||||
.button {
|
||||
width: 2rem;
|
||||
height: 2rem;
|
||||
padding: 0.25rem;
|
||||
outline: none;
|
||||
background: white;
|
||||
border-radius: 0.25rem;
|
||||
border: 1px solid #ddd;
|
||||
cursor: pointer;
|
||||
font-size: 0.85rem;
|
||||
transition: 0.2s;
|
||||
}
|
||||
|
||||
.button img {
|
||||
flex-grow: 1;
|
||||
max-width: 100%;
|
||||
max-height: 100%;
|
||||
min-width: 1.5rem;
|
||||
}
|
||||
|
||||
.trim-button {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding-right: 10px; /* To equalize with empty whitespace from image on left */
|
||||
}
|
||||
|
||||
.trim-button-active {
|
||||
filter: hue-rotate(155deg); /* @todo replace blue -> red with real submit icon */
|
||||
}
|
||||
|
||||
.input-group-right {
|
||||
flex-grow: 1;
|
||||
display: flex;
|
||||
flex-direction: row-reverse;
|
||||
}
|
||||
@@ -1,26 +1,58 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import classNames from 'classnames';
|
||||
import Box from '../box/box.jsx';
|
||||
import {defineMessages, FormattedMessage, injectIntl, intlShape} from 'react-intl';
|
||||
|
||||
import Waveform from '../waveform/waveform.jsx';
|
||||
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 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';
|
||||
|
||||
const BufferedInput = BufferedInputHOC(Input);
|
||||
|
||||
const messages = defineMessages({
|
||||
sound: {
|
||||
id: 'soundEditor.sound',
|
||||
description: 'Lable for the name of the sound',
|
||||
defaultMessage: 'Sound'
|
||||
},
|
||||
play: {
|
||||
id: 'soundEditor.play',
|
||||
description: 'Title of the button to start playing the sound',
|
||||
defaultMessage: 'Play'
|
||||
},
|
||||
stop: {
|
||||
id: 'soundEditor.stop',
|
||||
description: 'Title of the button to stop the sound',
|
||||
defaultMessage: 'Stop'
|
||||
},
|
||||
trim: {
|
||||
id: 'soundEditor.trim',
|
||||
description: 'Title of the button to start trimminging the sound',
|
||||
defaultMessage: 'Trim'
|
||||
},
|
||||
save: {
|
||||
id: 'soundEditor.save',
|
||||
description: 'Title of the button to save trimmed sound',
|
||||
defaultMessage: 'Save'
|
||||
}
|
||||
});
|
||||
|
||||
const SoundEditor = props => (
|
||||
<Box className={styles.editorContainer}>
|
||||
<Box className={styles.row}>
|
||||
<Box className={styles.inputGroup}>
|
||||
<div className={styles.editorContainer}>
|
||||
<div className={styles.row}>
|
||||
<div className={styles.inputGroup}>
|
||||
{props.playhead ? (
|
||||
<button
|
||||
className={classNames(styles.button, styles.stopButtonn)}
|
||||
title={props.intl.formatMessage(messages.stop)}
|
||||
onClick={props.onStop}
|
||||
>
|
||||
<img src={stopIcon} />
|
||||
@@ -28,14 +60,15 @@ const SoundEditor = props => (
|
||||
) : (
|
||||
<button
|
||||
className={classNames(styles.button, styles.playButton)}
|
||||
title={props.intl.formatMessage(messages.play)}
|
||||
onClick={props.onPlay}
|
||||
>
|
||||
<img src={playIcon} />
|
||||
</button>
|
||||
)}
|
||||
</Box>
|
||||
<Box className={styles.inputGroup}>
|
||||
<Label text="Sound">
|
||||
</div>
|
||||
<div className={styles.inputGroup}>
|
||||
<Label text={props.intl.formatMessage(messages.sound)}>
|
||||
<BufferedInput
|
||||
tabIndex="1"
|
||||
type="text"
|
||||
@@ -43,27 +76,60 @@ const SoundEditor = props => (
|
||||
onSubmit={props.onChangeName}
|
||||
/>
|
||||
</Label>
|
||||
</Box>
|
||||
</Box>
|
||||
<Box className={styles.row}>
|
||||
<Box className={styles.waveformContainer}>
|
||||
</div>
|
||||
<div className={styles.inputGroupRight}>
|
||||
<button
|
||||
className={classNames(styles.button, styles.trimButton, {
|
||||
[styles.trimButtonActive]: props.trimStart !== null
|
||||
})}
|
||||
title={props.trimStart === null ? (
|
||||
props.intl.formatMessage(messages.trim)
|
||||
) : (
|
||||
props.intl.formatMessage(messages.save)
|
||||
)}
|
||||
onClick={props.onActivateTrim}
|
||||
>
|
||||
<img src={trimIcon} />
|
||||
{props.trimStart === null ? (
|
||||
<FormattedMessage {...messages.trim} />
|
||||
) : (
|
||||
<FormattedMessage {...messages.save} />
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles.row}>
|
||||
<div className={styles.waveformContainer}>
|
||||
<Waveform
|
||||
data={props.chunkLevels}
|
||||
height={180}
|
||||
width={600}
|
||||
/>
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
<AudioTrimmer
|
||||
playhead={props.playhead}
|
||||
trimEnd={props.trimEnd}
|
||||
trimStart={props.trimStart}
|
||||
onSetTrimEnd={props.onSetTrimEnd}
|
||||
onSetTrimStart={props.onSetTrimStart}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
SoundEditor.propTypes = {
|
||||
chunkLevels: PropTypes.arrayOf(PropTypes.number).isRequired,
|
||||
intl: intlShape,
|
||||
name: PropTypes.string.isRequired,
|
||||
onActivateTrim: PropTypes.func,
|
||||
onChangeName: PropTypes.func.isRequired,
|
||||
onPlay: PropTypes.func.isRequired,
|
||||
onSetTrimEnd: PropTypes.func,
|
||||
onSetTrimStart: PropTypes.func,
|
||||
onStop: PropTypes.func.isRequired,
|
||||
playhead: PropTypes.number
|
||||
playhead: PropTypes.number,
|
||||
trimEnd: PropTypes.number,
|
||||
trimStart: PropTypes.number
|
||||
};
|
||||
|
||||
export default SoundEditor;
|
||||
export default injectIntl(SoundEditor);
|
||||
@@ -20,12 +20,14 @@ class AudioTrimmer extends React.Component {
|
||||
const dx = (e.clientX - this.initialX) / containerSize;
|
||||
const newTrim = Math.max(0, Math.min(this.props.trimEnd, this.initialTrim + dx));
|
||||
this.props.onSetTrimStart(newTrim);
|
||||
e.preventDefault();
|
||||
}
|
||||
handleTrimEndMouseMove (e) {
|
||||
const containerSize = this.containerElement.getBoundingClientRect().width;
|
||||
const dx = (e.clientX - this.initialX) / containerSize;
|
||||
const newTrim = Math.min(1, Math.max(this.props.trimStart, this.initialTrim + dx));
|
||||
this.props.onSetTrimEnd(newTrim);
|
||||
e.preventDefault();
|
||||
}
|
||||
handleTrimStartMouseUp () {
|
||||
window.removeEventListener('mousemove', this.handleTrimStartMouseMove);
|
||||
|
||||
@@ -17,30 +17,50 @@ class SoundEditor extends React.Component {
|
||||
'handleChangeName',
|
||||
'handlePlay',
|
||||
'handleStopPlaying',
|
||||
'handleUpdatePlayhead'
|
||||
'handleUpdatePlayhead',
|
||||
'handleActivateTrim',
|
||||
'handleUpdateTrimEnd',
|
||||
'handleUpdateTrimStart'
|
||||
]);
|
||||
this.state = {
|
||||
chunkLevels: computeChunkedRMS(this.props.samples),
|
||||
playhead: null, // null is not playing, [0 -> 1] is playing percent
|
||||
chunkLevels: computeChunkedRMS(this.props.samples)
|
||||
trimStart: null,
|
||||
trimEnd: null
|
||||
};
|
||||
}
|
||||
componentDidMount () {
|
||||
this.audioBufferPlayer = new AudioBufferPlayer(this.props.samples, this.props.sampleRate);
|
||||
}
|
||||
componentWillReceiveProps (newProps) {
|
||||
if (newProps.soundIndex !== this.props.soundIndex) {
|
||||
this.audioBufferPlayer.stop();
|
||||
this.audioBufferPlayer = new AudioBufferPlayer(newProps.samples, newProps.sampleRate);
|
||||
this.setState({chunkLevels: computeChunkedRMS(newProps.samples)});
|
||||
if (newProps.soundId !== this.props.soundId) { // A different sound has been selected
|
||||
this.resetState(newProps.samples, newProps.sampleRate);
|
||||
}
|
||||
}
|
||||
componentWillUnmount () {
|
||||
this.audioBufferPlayer.stop();
|
||||
}
|
||||
resetState (samples, sampleRate) {
|
||||
this.audioBufferPlayer.stop();
|
||||
this.audioBufferPlayer = new AudioBufferPlayer(samples, sampleRate);
|
||||
this.setState({
|
||||
chunkLevels: computeChunkedRMS(samples),
|
||||
playhead: null,
|
||||
trimStart: null,
|
||||
trimEnd: null
|
||||
});
|
||||
}
|
||||
submitNewSamples (samples, sampleRate) {
|
||||
this.resetState(samples, sampleRate);
|
||||
this.props.onUpdateSoundBuffer(
|
||||
this.props.soundIndex,
|
||||
this.audioBufferPlayer.buffer
|
||||
);
|
||||
}
|
||||
handlePlay () {
|
||||
this.audioBufferPlayer.play(
|
||||
0,
|
||||
1,
|
||||
this.state.trimStart || 0,
|
||||
this.state.trimEnd || 1,
|
||||
this.handleUpdatePlayhead,
|
||||
this.handleStoppedPlaying);
|
||||
}
|
||||
@@ -57,6 +77,23 @@ class SoundEditor extends React.Component {
|
||||
handleChangeName (name) {
|
||||
this.props.onRenameSound(this.props.soundIndex, name);
|
||||
}
|
||||
handleActivateTrim () {
|
||||
if (this.state.trimStart === null && this.state.trimEnd === null) {
|
||||
this.setState({trimEnd: 0.95, trimStart: 0.05});
|
||||
} else {
|
||||
const sampleCount = this.props.samples.length;
|
||||
const startIndex = Math.floor(this.state.trimStart * sampleCount);
|
||||
const endIndex = Math.floor(this.state.trimEnd * sampleCount);
|
||||
const clippedSamples = this.props.samples.slice(startIndex, endIndex);
|
||||
this.submitNewSamples(clippedSamples, this.props.sampleRate);
|
||||
}
|
||||
}
|
||||
handleUpdateTrimEnd (trimEnd) {
|
||||
this.setState({trimEnd});
|
||||
}
|
||||
handleUpdateTrimStart (trimStart) {
|
||||
this.setState({trimStart});
|
||||
}
|
||||
render () {
|
||||
return (
|
||||
<SoundEditorComponent
|
||||
@@ -65,12 +102,12 @@ class SoundEditor extends React.Component {
|
||||
playhead={this.state.playhead}
|
||||
trimEnd={this.state.trimEnd}
|
||||
trimStart={this.state.trimStart}
|
||||
onActivateTrim={this.handleActivateTrim}
|
||||
onChangeName={this.handleChangeName}
|
||||
onPlay={this.handlePlay}
|
||||
onSetTrimEnd={this.handleUpdateTrimEnd}
|
||||
onSetTrimStart={this.handleUpdateTrimStart}
|
||||
onStop={this.handleStopPlaying}
|
||||
onTrim={this.handleActivateTrim}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -79,8 +116,10 @@ class SoundEditor extends React.Component {
|
||||
SoundEditor.propTypes = {
|
||||
name: PropTypes.string.isRequired,
|
||||
onRenameSound: PropTypes.func.isRequired,
|
||||
onUpdateSoundBuffer: PropTypes.func.isRequired,
|
||||
sampleRate: PropTypes.number,
|
||||
samples: PropTypes.instanceOf(Float32Array),
|
||||
soundId: PropTypes.string,
|
||||
soundIndex: PropTypes.number
|
||||
};
|
||||
|
||||
@@ -88,10 +127,12 @@ const mapStateToProps = (state, {soundIndex}) => {
|
||||
const sound = state.vm.editingTarget.sprite.sounds[soundIndex];
|
||||
const audioBuffer = state.vm.getSoundBuffer(soundIndex);
|
||||
return {
|
||||
soundId: sound.soundId,
|
||||
sampleRate: audioBuffer.sampleRate,
|
||||
samples: audioBuffer.getChannelData(0),
|
||||
name: sound.name,
|
||||
onRenameSound: state.vm.renameSound.bind(state.vm)
|
||||
onRenameSound: state.vm.renameSound.bind(state.vm),
|
||||
onUpdateSoundBuffer: state.vm.updateSoundBuffer.bind(state.vm)
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
/* eslint-env jest */
|
||||
export default class MockAudioBufferPlayer {
|
||||
constructor (samples, sampleRate) {
|
||||
this.samples = samples;
|
||||
this.sampleRate = sampleRate;
|
||||
this.play = jest.fn((trimStart, trimEnd, onUpdate) => {
|
||||
this.onUpdate = onUpdate;
|
||||
});
|
||||
this.stop = jest.fn();
|
||||
MockAudioBufferPlayer.instance = this;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
/*
|
||||
* Helpers for using enzyme and react-test-renderer with react-intl
|
||||
* Directly from https://github.com/yahoo/react-intl/wiki/Testing-with-React-Intl
|
||||
*/
|
||||
import React from 'react';
|
||||
import renderer from 'react-test-renderer';
|
||||
import {IntlProvider, intlShape} from 'react-intl';
|
||||
import {mount, shallow} from 'enzyme';
|
||||
|
||||
const intlProvider = new IntlProvider({locale: 'en'}, {});
|
||||
const {intl} = intlProvider.getChildContext();
|
||||
|
||||
const nodeWithIntlProp = node => {
|
||||
return React.cloneElement(node, {intl});
|
||||
};
|
||||
|
||||
const shallowWithIntl = (node, {context} = {}) => {
|
||||
return shallow(
|
||||
nodeWithIntlProp(node),
|
||||
{
|
||||
context: Object.assign({}, context, {intl})
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
const mountWithIntl = (node, {context, childContextTypes} = {}) => {
|
||||
return mount(
|
||||
nodeWithIntlProp(node),
|
||||
{
|
||||
context: Object.assign({}, context, {intl}),
|
||||
childContextTypes: Object.assign({}, {intl: intlShape}, childContextTypes)
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
// react-test-renderer component for use with snapshot testing
|
||||
const componentWithIntl = (children, props = {locale: 'en'}) => {
|
||||
return renderer.create(<IntlProvider {...props}>{children}</IntlProvider>);
|
||||
};
|
||||
|
||||
export {
|
||||
componentWithIntl,
|
||||
shallowWithIntl,
|
||||
mountWithIntl
|
||||
};
|
||||
@@ -0,0 +1,313 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`Sound Editor Component matches snapshot 1`] = `
|
||||
<div
|
||||
className={undefined}
|
||||
>
|
||||
<div
|
||||
className={undefined}
|
||||
>
|
||||
<div
|
||||
className={undefined}
|
||||
>
|
||||
<button
|
||||
className=""
|
||||
onClick={[Function]}
|
||||
title="Stop"
|
||||
>
|
||||
<img
|
||||
src="test-file-stub"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
className={undefined}
|
||||
>
|
||||
<label
|
||||
className={undefined}
|
||||
>
|
||||
<span
|
||||
className={undefined}
|
||||
>
|
||||
Sound
|
||||
</span>
|
||||
<input
|
||||
className=""
|
||||
onBlur={[Function]}
|
||||
onChange={[Function]}
|
||||
onKeyPress={[Function]}
|
||||
onSubmit={[Function]}
|
||||
tabIndex="1"
|
||||
type="text"
|
||||
value="sound name"
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
<div
|
||||
className={undefined}
|
||||
>
|
||||
<button
|
||||
className="undefined"
|
||||
onClick={[Function]}
|
||||
title="Save"
|
||||
>
|
||||
<img
|
||||
src="test-file-stub"
|
||||
/>
|
||||
<span>
|
||||
Save
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className={undefined}
|
||||
>
|
||||
<div
|
||||
className={undefined}
|
||||
>
|
||||
<svg
|
||||
className={undefined}
|
||||
viewBox="0 0 600 180"
|
||||
>
|
||||
<g
|
||||
transform="scale(1, -1) translate(0, -90)"
|
||||
>
|
||||
<path
|
||||
className={undefined}
|
||||
d="M0 0Q0 0 60 45,Q120 90 180 135,Q240 180 300 225,Q360 270 420 135,Q480 0 480 0,Q480 0 420 -135,Q360 -270 300 -225,Q240 -180 180 -135,Q120 -90 60 -45,Q0 0 0 0Z"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
/>
|
||||
</g>
|
||||
</svg>
|
||||
<div
|
||||
className={undefined}
|
||||
>
|
||||
<div
|
||||
className=""
|
||||
onMouseDown={[Function]}
|
||||
style={
|
||||
Object {
|
||||
"alignContent": undefined,
|
||||
"alignItems": undefined,
|
||||
"alignSelf": undefined,
|
||||
"flexBasis": undefined,
|
||||
"flexDirection": undefined,
|
||||
"flexGrow": undefined,
|
||||
"flexShrink": undefined,
|
||||
"flexWrap": undefined,
|
||||
"height": undefined,
|
||||
"justifyContent": undefined,
|
||||
"width": "20%",
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
className=""
|
||||
style={
|
||||
Object {
|
||||
"alignContent": undefined,
|
||||
"alignItems": undefined,
|
||||
"alignSelf": undefined,
|
||||
"flexBasis": undefined,
|
||||
"flexDirection": undefined,
|
||||
"flexGrow": undefined,
|
||||
"flexShrink": undefined,
|
||||
"flexWrap": undefined,
|
||||
"height": undefined,
|
||||
"justifyContent": undefined,
|
||||
"width": undefined,
|
||||
}
|
||||
}
|
||||
/>
|
||||
<div
|
||||
className=""
|
||||
style={
|
||||
Object {
|
||||
"alignContent": undefined,
|
||||
"alignItems": undefined,
|
||||
"alignSelf": undefined,
|
||||
"flexBasis": undefined,
|
||||
"flexDirection": undefined,
|
||||
"flexGrow": undefined,
|
||||
"flexShrink": undefined,
|
||||
"flexWrap": undefined,
|
||||
"height": undefined,
|
||||
"justifyContent": undefined,
|
||||
"width": undefined,
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
className=""
|
||||
style={
|
||||
Object {
|
||||
"alignContent": undefined,
|
||||
"alignItems": undefined,
|
||||
"alignSelf": undefined,
|
||||
"flexBasis": undefined,
|
||||
"flexDirection": undefined,
|
||||
"flexGrow": undefined,
|
||||
"flexShrink": undefined,
|
||||
"flexWrap": undefined,
|
||||
"height": undefined,
|
||||
"justifyContent": undefined,
|
||||
"width": undefined,
|
||||
}
|
||||
}
|
||||
>
|
||||
<img
|
||||
src="test-file-stub"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className=""
|
||||
style={
|
||||
Object {
|
||||
"alignContent": undefined,
|
||||
"alignItems": undefined,
|
||||
"alignSelf": undefined,
|
||||
"flexBasis": undefined,
|
||||
"flexDirection": undefined,
|
||||
"flexGrow": undefined,
|
||||
"flexShrink": undefined,
|
||||
"flexWrap": undefined,
|
||||
"height": undefined,
|
||||
"justifyContent": undefined,
|
||||
"width": undefined,
|
||||
}
|
||||
}
|
||||
>
|
||||
<img
|
||||
src="test-file-stub"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className=""
|
||||
style={
|
||||
Object {
|
||||
"alignContent": undefined,
|
||||
"alignItems": undefined,
|
||||
"alignSelf": undefined,
|
||||
"flexBasis": undefined,
|
||||
"flexDirection": undefined,
|
||||
"flexGrow": undefined,
|
||||
"flexShrink": undefined,
|
||||
"flexWrap": undefined,
|
||||
"height": undefined,
|
||||
"justifyContent": undefined,
|
||||
"left": "50%",
|
||||
"width": undefined,
|
||||
}
|
||||
}
|
||||
/>
|
||||
<div
|
||||
className=""
|
||||
onMouseDown={[Function]}
|
||||
style={
|
||||
Object {
|
||||
"alignContent": undefined,
|
||||
"alignItems": undefined,
|
||||
"alignSelf": undefined,
|
||||
"flexBasis": undefined,
|
||||
"flexDirection": undefined,
|
||||
"flexGrow": undefined,
|
||||
"flexShrink": undefined,
|
||||
"flexWrap": undefined,
|
||||
"height": undefined,
|
||||
"justifyContent": undefined,
|
||||
"left": "80%",
|
||||
"width": "20%",
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
className=""
|
||||
style={
|
||||
Object {
|
||||
"alignContent": undefined,
|
||||
"alignItems": undefined,
|
||||
"alignSelf": undefined,
|
||||
"flexBasis": undefined,
|
||||
"flexDirection": undefined,
|
||||
"flexGrow": undefined,
|
||||
"flexShrink": undefined,
|
||||
"flexWrap": undefined,
|
||||
"height": undefined,
|
||||
"justifyContent": undefined,
|
||||
"width": undefined,
|
||||
}
|
||||
}
|
||||
/>
|
||||
<div
|
||||
className=""
|
||||
style={
|
||||
Object {
|
||||
"alignContent": undefined,
|
||||
"alignItems": undefined,
|
||||
"alignSelf": undefined,
|
||||
"flexBasis": undefined,
|
||||
"flexDirection": undefined,
|
||||
"flexGrow": undefined,
|
||||
"flexShrink": undefined,
|
||||
"flexWrap": undefined,
|
||||
"height": undefined,
|
||||
"justifyContent": undefined,
|
||||
"width": undefined,
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
className=""
|
||||
style={
|
||||
Object {
|
||||
"alignContent": undefined,
|
||||
"alignItems": undefined,
|
||||
"alignSelf": undefined,
|
||||
"flexBasis": undefined,
|
||||
"flexDirection": undefined,
|
||||
"flexGrow": undefined,
|
||||
"flexShrink": undefined,
|
||||
"flexWrap": undefined,
|
||||
"height": undefined,
|
||||
"justifyContent": undefined,
|
||||
"width": undefined,
|
||||
}
|
||||
}
|
||||
>
|
||||
<img
|
||||
src="test-file-stub"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className=""
|
||||
style={
|
||||
Object {
|
||||
"alignContent": undefined,
|
||||
"alignItems": undefined,
|
||||
"alignSelf": undefined,
|
||||
"flexBasis": undefined,
|
||||
"flexDirection": undefined,
|
||||
"flexGrow": undefined,
|
||||
"flexShrink": undefined,
|
||||
"flexWrap": undefined,
|
||||
"height": undefined,
|
||||
"justifyContent": undefined,
|
||||
"width": undefined,
|
||||
}
|
||||
}
|
||||
>
|
||||
<img
|
||||
src="test-file-stub"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
@@ -0,0 +1,60 @@
|
||||
/* eslint-env jest */
|
||||
import React from 'react'; // eslint-disable-line no-unused-vars
|
||||
import {mountWithIntl, componentWithIntl} from '../../helpers/intl-helpers';
|
||||
import SoundEditor from '../../../src/components/sound-editor/sound-editor'; // eslint-disable-line no-unused-vars
|
||||
|
||||
describe('Sound Editor Component', () => {
|
||||
let props;
|
||||
beforeEach(() => {
|
||||
props = {
|
||||
chunkLevels: [1, 2, 3],
|
||||
name: 'sound name',
|
||||
playhead: 0.5,
|
||||
trimStart: 0.2,
|
||||
trimEnd: 0.8,
|
||||
onActivateTrim: jest.fn(),
|
||||
onChangeName: jest.fn(),
|
||||
onPlay: jest.fn(),
|
||||
onSetTrimEnd: jest.fn(),
|
||||
onSetTrimStart: jest.fn(),
|
||||
onStop: jest.fn()
|
||||
};
|
||||
});
|
||||
|
||||
test('matches snapshot', () => {
|
||||
const component = componentWithIntl(<SoundEditor {...props} />);
|
||||
expect(component.toJSON()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('trim button appears when trims are null', () => {
|
||||
const wrapper = mountWithIntl(<SoundEditor {...props} trimStart={null} trimEnd={null} />);
|
||||
wrapper.find('button[title="Trim"]').simulate('click');
|
||||
expect(props.onActivateTrim).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('save button appears when trims are not null', () => {
|
||||
const wrapper = mountWithIntl(<SoundEditor {...props} trimStart={0.25} trimEnd={0.75} />);
|
||||
wrapper.find('button[title="Save"]').simulate('click');
|
||||
expect(props.onActivateTrim).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('play button appears when playhead is null', () => {
|
||||
const wrapper = mountWithIntl(<SoundEditor {...props} playhead={null} />);
|
||||
wrapper.find('button[title="Play"]').simulate('click');
|
||||
expect(props.onPlay).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('stop button appears when playhead is not null', () => {
|
||||
const wrapper = mountWithIntl(<SoundEditor {...props} playhead={0.5} />);
|
||||
wrapper.find('button[title="Stop"]').simulate('click');
|
||||
expect(props.onStop).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('submitting name calls the callback', () => {
|
||||
const wrapper = mountWithIntl(<SoundEditor {...props} />);
|
||||
wrapper.find('input')
|
||||
.simulate('change', {target: {value: 'hello'}})
|
||||
.simulate('blur');
|
||||
expect(props.onChangeName).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,93 @@
|
||||
/* eslint-env jest */
|
||||
import React from 'react'; // eslint-disable-line no-unused-vars
|
||||
import {mountWithIntl} from '../../helpers/intl-helpers';
|
||||
import configureStore from 'redux-mock-store';
|
||||
import mockAudioBufferPlayer from '../../__mocks__/audio-buffer-player.js';
|
||||
|
||||
import SoundEditor from '../../../src/containers/sound-editor'; // eslint-disable-line no-unused-vars
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
import SoundEditorComponent from '../../../src/components/sound-editor/sound-editor';
|
||||
|
||||
jest.mock('../../../src/lib/audio/audio-buffer-player', () => mockAudioBufferPlayer);
|
||||
|
||||
describe('Sound Editor Container', () => {
|
||||
const mockStore = configureStore();
|
||||
let store;
|
||||
let soundIndex;
|
||||
let soundBuffer;
|
||||
let samples = new Float32Array([0, 0, 0]); // eslint-disable-line no-undef
|
||||
let vm;
|
||||
|
||||
beforeEach(() => {
|
||||
soundIndex = 0;
|
||||
soundBuffer = {
|
||||
sampleRate: 0,
|
||||
getChannelData: jest.fn(() => samples)
|
||||
};
|
||||
vm = {
|
||||
getSoundBuffer: jest.fn(() => soundBuffer),
|
||||
renameSound: jest.fn(),
|
||||
updateSoundBuffer: jest.fn(),
|
||||
editingTarget: {
|
||||
sprite: {
|
||||
sounds: [{name: 'first name', id: 'first id'}]
|
||||
}
|
||||
}
|
||||
};
|
||||
store = mockStore({vm});
|
||||
});
|
||||
|
||||
test('should pass the correct data to the component from the store', () => {
|
||||
const wrapper = mountWithIntl(<SoundEditor store={store} soundIndex={soundIndex} />);
|
||||
const componentProps = wrapper.find(SoundEditorComponent).props();
|
||||
// Data retreived and processed by the `connect` with the store
|
||||
expect(componentProps.name).toEqual('first name');
|
||||
expect(componentProps.chunkLevels).toEqual([0]);
|
||||
expect(mockAudioBufferPlayer.instance.samples).toEqual(samples);
|
||||
// Initial data
|
||||
expect(componentProps.playhead).toEqual(null);
|
||||
expect(componentProps.trimStart).toEqual(null);
|
||||
expect(componentProps.trimEnd).toEqual(null);
|
||||
|
||||
});
|
||||
|
||||
test('it plays when clicked and stops when clicked again', () => {
|
||||
const wrapper = mountWithIntl(<SoundEditor store={store} soundIndex={soundIndex} />);
|
||||
const component = wrapper.find(SoundEditorComponent);
|
||||
// Ensure rendering doesn't start playing any sounds
|
||||
expect(mockAudioBufferPlayer.instance.play.mock.calls).toEqual([]);
|
||||
expect(mockAudioBufferPlayer.instance.stop.mock.calls).toEqual([]);
|
||||
|
||||
component.props().onPlay();
|
||||
expect(mockAudioBufferPlayer.instance.play).toHaveBeenCalled();
|
||||
|
||||
// Mock the audio buffer player calling onUpdate
|
||||
mockAudioBufferPlayer.instance.onUpdate(0.5);
|
||||
expect(component.props().playhead).toEqual(0.5);
|
||||
|
||||
component.props().onStop();
|
||||
expect(mockAudioBufferPlayer.instance.stop).toHaveBeenCalled();
|
||||
expect(component.props().playhead).toEqual(null);
|
||||
});
|
||||
|
||||
test('it sets the component props for trimming and submits to the vm', () => {
|
||||
const wrapper = mountWithIntl(<SoundEditor store={store} soundIndex={soundIndex} />);
|
||||
const component = wrapper.find(SoundEditorComponent);
|
||||
|
||||
component.props().onActivateTrim();
|
||||
expect(component.props().trimStart).not.toEqual(null);
|
||||
expect(component.props().trimEnd).not.toEqual(null);
|
||||
|
||||
component.props().onActivateTrim();
|
||||
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(<SoundEditor store={store} soundIndex={soundIndex} />);
|
||||
const component = wrapper.find(SoundEditorComponent);
|
||||
component.props().onChangeName('hello');
|
||||
expect(vm.renameSound).toHaveBeenCalledWith(soundIndex, 'hello');
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user