Spaces:
Running
Running
import PropTypes from 'prop-types'; | |
import React from 'react'; | |
import classNames from 'classnames'; | |
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 AudioSelector from '../../containers/audio-selector.jsx'; | |
import IconButton from '../icon-button/icon-button.jsx'; | |
import {SOUND_BYTE_LIMIT} from '../../lib/audio/audio-util.js'; | |
import styles from './sound-editor.css'; | |
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 modifyIcon from './icon--modify.svg'; | |
import fasterIcon from './icon--faster.svg'; | |
import slowerIcon from './icon--slower.svg'; | |
import louderIcon from './icon--louder.svg'; | |
import softerIcon from './icon--softer.svg'; | |
import robotIcon from './icon--robot.svg'; | |
import echoIcon from './icon--echo.svg'; | |
import highpassIcon from './icon--highpass.svg'; | |
import lowpassIcon from './icon--lowpass.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'; | |
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({ | |
sound: { | |
id: 'gui.soundEditor.sound', | |
description: 'Label for the name of the sound', | |
defaultMessage: 'Sound' | |
}, | |
play: { | |
id: 'gui.soundEditor.play', | |
description: 'Title of the button to start playing the sound', | |
defaultMessage: 'Play' | |
}, | |
stop: { | |
id: 'gui.soundEditor.stop', | |
description: 'Title of the button to stop the sound', | |
defaultMessage: 'Stop' | |
}, | |
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', | |
description: 'Title of the button to save trimmed sound', | |
defaultMessage: 'Save' | |
}, | |
undo: { | |
id: 'gui.soundEditor.undo', | |
description: 'Title of the button to undo', | |
defaultMessage: 'Undo' | |
}, | |
redo: { | |
id: 'gui.soundEditor.redo', | |
description: 'Title of the button to redo', | |
defaultMessage: 'Redo' | |
}, | |
faster: { | |
id: 'gui.soundEditor.faster', | |
description: 'Title of the button to apply the faster effect', | |
defaultMessage: 'Faster' | |
}, | |
slower: { | |
id: 'gui.soundEditor.slower', | |
description: 'Title of the button to apply the slower effect', | |
defaultMessage: 'Slower' | |
}, | |
echo: { | |
id: 'gui.soundEditor.echo', | |
description: 'Title of the button to apply the echo effect', | |
defaultMessage: 'Echo' | |
}, | |
robot: { | |
id: 'gui.soundEditor.robot', | |
description: 'Title of the button to apply the robot effect', | |
defaultMessage: 'Robot' | |
}, | |
louder: { | |
id: 'gui.soundEditor.louder', | |
description: 'Title of the button to apply the louder effect', | |
defaultMessage: 'Louder' | |
}, | |
softer: { | |
id: 'gui.soundEditor.softer', | |
description: 'Title of the button to apply thr.softer effect', | |
defaultMessage: 'Softer' | |
}, | |
reverse: { | |
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' | |
} | |
}); | |
const formatTime = timeSeconds => { | |
const minutes = (Math.floor(timeSeconds / 60)) | |
.toString() | |
.padStart(2, '0'); | |
const seconds = (timeSeconds % 60) | |
.toFixed(2) | |
.padStart(5, '0'); | |
return `${minutes}:${seconds}`; | |
}; | |
const formatDuration = (playheadPercent, trimStartPercent, trimEndPercent, durationSeconds) => { | |
// If no selection, the trim is the entire sound. | |
trimStartPercent = trimStartPercent === null ? 0 : trimStartPercent; | |
trimEndPercent = trimEndPercent === null ? 1 : trimEndPercent; | |
// If the playhead doesn't exist, assume it's at the start of the selection. | |
playheadPercent = playheadPercent === null ? trimStartPercent : playheadPercent; | |
// If selection has zero length, treat it as the entire sound being selected. | |
// This happens when the user first clicks to start making a selection. | |
const trimSize = (trimEndPercent - trimStartPercent) || 1; | |
const trimDuration = trimSize * durationSeconds; | |
const progressInTrim = (playheadPercent - trimStartPercent) / trimSize; | |
const currentTime = progressInTrim * trimDuration; | |
return `${formatTime(currentTime)} / ${formatTime(trimDuration)}`; | |
}; | |
const formatSoundSize = bytes => { | |
if (bytes > 1000 * 1000) { | |
return `${(bytes / 1000 / 1000).toFixed(2)}MB`; | |
} | |
return `${(bytes / 1000).toFixed(2)}KB`; | |
}; | |
const SoundEditor = props => ( | |
<div | |
className={styles.editorContainer} | |
ref={props.setRef} | |
onMouseDown={props.onContainerClick} | |
> | |
<div className={styles.row}> | |
<div className={styles.inputGroup}> | |
<Label text={props.intl.formatMessage(messages.sound)}> | |
<BufferedInput | |
tabIndex="1" | |
type="text" | |
value={props.name} | |
onSubmit={props.onChangeName} | |
className={styles.nameInput} | |
/> | |
</Label> | |
<div className={styles.buttonGroup}> | |
<button | |
className={styles.button} | |
disabled={!props.canUndo} | |
title={props.intl.formatMessage(messages.undo)} | |
onClick={props.onUndo} | |
> | |
<img | |
className={styles.undoIcon} | |
draggable={false} | |
src={undoIcon} | |
/> | |
</button> | |
<button | |
className={styles.button} | |
disabled={!props.canRedo} | |
title={props.intl.formatMessage(messages.redo)} | |
onClick={props.onRedo} | |
> | |
<img | |
className={styles.redoIcon} | |
draggable={false} | |
src={redoIcon} | |
/> | |
</button> | |
</div> | |
</div> | |
<div className={styles.inputGroup}> | |
<IconButton | |
className={styles.toolButton} | |
img={copyIcon} | |
title={props.intl.formatMessage(messages.copy)} | |
onClick={props.onCopy} | |
/> | |
<IconButton | |
className={styles.toolButton} | |
disabled={props.canPaste === false} | |
img={pasteIcon} | |
title={props.intl.formatMessage(messages.paste)} | |
onClick={props.onPaste} | |
/> | |
<IconButton | |
className={classNames(styles.toolButton, styles.flipInRtl)} | |
img={copyToNewIcon} | |
title={props.intl.formatMessage(messages.copyToNew)} | |
onClick={props.onCopyToNew} | |
/> | |
</div> | |
<IconButton | |
className={styles.toolButton} | |
disabled={props.trimStart === null} | |
img={deleteIcon} | |
title={props.intl.formatMessage(messages.delete)} | |
onClick={props.onDelete} | |
/> | |
</div> | |
<div className={styles.row}> | |
<div className={styles.waveformContainer}> | |
<Waveform | |
data={props.chunkLevels} | |
height={160} | |
width={600} | |
/> | |
<AudioSelector | |
playhead={props.playhead} | |
trimEnd={props.trimEnd} | |
trimStart={props.trimStart} | |
onPlay={props.onPlay} | |
onSetTrim={props.onSetTrim} | |
onStop={props.onStop} | |
/> | |
</div> | |
</div> | |
<div className={classNames(styles.row, styles.rowReverse)}> | |
<div className={classNames(styles.roundButtonOuter, styles.inputGroup)}> | |
{props.playhead ? ( | |
<button | |
className={classNames(styles.roundButton, styles.stopButtonn)} | |
title={props.intl.formatMessage(messages.stop)} | |
onClick={props.onStop} | |
> | |
<img | |
draggable={false} | |
src={stopIcon} | |
/> | |
</button> | |
) : ( | |
<button | |
className={classNames(styles.roundButton, styles.playButton)} | |
title={props.intl.formatMessage(messages.play)} | |
onClick={props.onPlay} | |
> | |
<img | |
draggable={false} | |
src={playIcon} | |
/> | |
</button> | |
)} | |
</div> | |
<div className={styles.effects}> | |
<IconButton | |
className={styles.effectButton} | |
img={modifyIcon} | |
title={"Modify"} | |
onClick={props.onModifySound} | |
/> | |
<IconButton | |
className={styles.effectButton} | |
img={fasterIcon} | |
title={<FormattedMessage {...messages.faster} />} | |
onClick={props.onFaster} | |
/> | |
<IconButton | |
className={styles.effectButton} | |
img={slowerIcon} | |
title={<FormattedMessage {...messages.slower} />} | |
onClick={props.onSlower} | |
/> | |
<IconButton | |
disabled={props.tooLoud} | |
className={classNames(styles.effectButton, styles.flipInRtl)} | |
img={louderIcon} | |
title={<FormattedMessage {...messages.louder} />} | |
onClick={props.onLouder} | |
/> | |
<IconButton | |
className={classNames(styles.effectButton, styles.flipInRtl)} | |
img={softerIcon} | |
title={<FormattedMessage {...messages.softer} />} | |
onClick={props.onSofter} | |
/> | |
<IconButton | |
className={classNames(styles.effectButton, styles.flipInRtl)} | |
img={muteIcon} | |
title={<FormattedMessage {...messages.mute} />} | |
onClick={props.onMute} | |
/> | |
<IconButton | |
className={styles.effectButton} | |
img={fadeInIcon} | |
title={<FormattedMessage {...messages.fadeIn} />} | |
onClick={props.onFadeIn} | |
/> | |
<IconButton | |
className={styles.effectButton} | |
img={fadeOutIcon} | |
title={<FormattedMessage {...messages.fadeOut} />} | |
onClick={props.onFadeOut} | |
/> | |
<IconButton | |
className={styles.effectButton} | |
img={reverseIcon} | |
title={<FormattedMessage {...messages.reverse} />} | |
onClick={props.onReverse} | |
/> | |
<IconButton | |
className={styles.effectButton} | |
img={robotIcon} | |
title={<FormattedMessage {...messages.robot} />} | |
onClick={props.onRobot} | |
/> | |
<IconButton | |
className={styles.effectButton} | |
img={echoIcon} | |
title={<FormattedMessage {...messages.echo} />} | |
onClick={props.onEcho} | |
/> | |
<IconButton | |
className={styles.effectButton} | |
img={lowpassIcon} | |
title={"Low Pass"} | |
onClick={props.onLowPass} | |
/> | |
<IconButton | |
className={styles.effectButton} | |
img={highpassIcon} | |
title={"High Pass"} | |
onClick={props.onHighPass} | |
/> | |
</div> | |
</div> | |
<div className={styles.infoRow}> | |
<div className={styles.duration}> | |
{formatDuration(props.playhead, props.trimStart, props.trimEnd, props.duration)} | |
</div> | |
<div className={styles.advancedInfo}> | |
{props.sampleRate} | |
{'Hz '} | |
{`${String(props.dataFormat).toUpperCase()} `} | |
{props.isStereo ? ( | |
<FormattedMessage | |
defaultMessage="Stereo" | |
description="Refers to a 'Stereo Sound' (2 channels)" | |
id="tw.stereo" | |
/> | |
) : ( | |
<FormattedMessage | |
defaultMessage="Mono" | |
description="Refers to a 'Mono Sound' (1 channel)" | |
id="tw.mono" | |
/> | |
)} | |
{` (${formatSoundSize(props.size)})`} | |
</div> | |
</div> | |
{props.size >= SOUND_BYTE_LIMIT && ( | |
<div className={classNames(styles.alert, styles.tooLarge)}> | |
<FormattedMessage | |
defaultMessage="This sound could be too large to upload to PenguinMod." | |
description="Message that appears when a sound exceeds the PenguinMod sound size limit." | |
id="pm.tooLarge" | |
/> | |
</div> | |
)} | |
{(props.dataFormat === "mp3" || props.dataFormat === "ogg" || props.dataFormat === "flac") && ( | |
<div className={classNames(styles.alert, styles.stereo)}> | |
<FormattedMessage | |
defaultMessage="Editing this sound will irreversibly convert it to a much larger, WAV format sound." | |
description="Message that appears when editing an mp3, ogg or flac sound." | |
id="pm.formatAlert" | |
/> | |
</div> | |
)} | |
{(props.dataFormat === "ogg") && ( | |
<div className={classNames(styles.alert, styles.tooLarge)}> | |
<FormattedMessage | |
defaultMessage="Users on iOS and MacOS will need to update their browser or device to hear any OGG sounds." | |
description="Message that appears when editing an ogg sound." | |
id="pm.oggSafariAlert" | |
/> | |
</div> | |
)} | |
{props.isStereo && ( | |
<div className={classNames(styles.alert, styles.stereo)}> | |
<FormattedMessage | |
defaultMessage="Editing this stereo sound will irreversibly convert it to mono." | |
description="Message that appears when editing a stereo sound." | |
id="tw.stereoAlert" | |
/> | |
</div> | |
)} | |
</div> | |
); | |
SoundEditor.propTypes = { | |
isStereo: PropTypes.bool.isRequired, | |
duration: PropTypes.number.isRequired, | |
dataFormat: PropTypes.number.isRequired, | |
size: PropTypes.bool.isRequired, | |
sampleRate: PropTypes.number.isRequired, | |
canPaste: PropTypes.bool.isRequired, | |
canRedo: PropTypes.bool.isRequired, | |
canUndo: PropTypes.bool.isRequired, | |
chunkLevels: PropTypes.arrayOf(PropTypes.number).isRequired, | |
intl: intlShape, | |
name: PropTypes.string.isRequired, | |
onChangeName: PropTypes.func.isRequired, | |
onContainerClick: PropTypes.func.isRequired, | |
onCopy: PropTypes.func.isRequired, | |
onCopyToNew: PropTypes.func.isRequired, | |
onDelete: PropTypes.func, | |
onEcho: PropTypes.func.isRequired, | |
onLowPass: PropTypes.func.isRequired, | |
onHighPass: PropTypes.func.isRequired, | |
onFadeIn: PropTypes.func.isRequired, | |
onFadeOut: PropTypes.func.isRequired, | |
onFaster: PropTypes.func.isRequired, | |
onModifySound: 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, | |
onRobot: PropTypes.func.isRequired, | |
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, | |
tooLoud: PropTypes.bool.isRequired, | |
trimEnd: PropTypes.number, | |
trimStart: PropTypes.number | |
}; | |
export default injectIntl(SoundEditor); | |