diff --git a/package.json b/package.json
index 8841cf721dc89bb0afa65f24823aaba55703c3cc..28df3b4d5bc59fa818f62bbbfe04bc84d113c299 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",
diff --git a/src/components/asset-panel/asset-panel.css b/src/components/asset-panel/asset-panel.css
index bfd578909fdf2a8efd826bba4899fd7e8240a4bd..b974fea6b08dd544650ce803f244d6fd49cdd807 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 {
diff --git a/src/components/audio-trimmer/audio-selector.jsx b/src/components/audio-trimmer/audio-selector.jsx
new file mode 100644
index 0000000000000000000000000000000000000000..a7c0b41c2631d2554dd39575b4876bf9a282bab6
--- /dev/null
+++ b/src/components/audio-trimmer/audio-selector.jsx
@@ -0,0 +1,53 @@
+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 SelectionHandle from './selection-handle.jsx';
+import Playhead from './playhead.jsx';
+
+const AudioSelector = props => (
+    <div
+        className={classNames(styles.absolute, styles.selector)}
+        ref={props.containerRef}
+        onMouseDown={props.onNewSelectionMouseDown}
+        onTouchStart={props.onNewSelectionMouseDown}
+    >
+        {props.trimStart === null ? null : (
+            <Box
+                className={classNames(styles.absolute)}
+                style={{
+                    left: `${props.trimStart * 100}%`,
+                    width: `${100 * (props.trimEnd - props.trimStart)}%`
+                }}
+            >
+                <Box className={classNames(styles.absolute, styles.selectionBackground)} />
+                <SelectionHandle
+                    handleStyle={styles.leftHandle}
+                    onMouseDown={props.onTrimStartMouseDown}
+                />
+                <SelectionHandle
+                    handleStyle={styles.rightHandle}
+                    onMouseDown={props.onTrimEndMouseDown}
+                />
+            </Box>
+        )}
+        {props.playhead ? (
+            <Playhead
+                playbackPosition={props.playhead}
+            />
+        ) : null}
+    </div>
+);
+
+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/audio-trimmer/audio-trimmer.css b/src/components/audio-trimmer/audio-trimmer.css
index 63af2a0cb2a42dcbf2126857cf8456624caf0abb..f3ce9fa42f8a697af0e7570f3eff5cda49258c3f 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;
@@ -17,6 +18,10 @@ $hover-scale: 2;
     transform: translateZ(0);
 }
 
+.selector {
+    cursor: pointer;
+}
+
 .trim-background {
     cursor: pointer;
     touch-action: none;
@@ -35,6 +40,11 @@ $hover-scale: 2;
     );
 }
 
+.selection-background {
+    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;
@@ -68,31 +82,50 @@ $hover-scale: 2;
         so that we can use transform: translateX() using percentages.
     */
     width: 100%;
+    height: 100%;
     border-left: 1px solid $motion-primary;
     border-top: none;
     border-bottom: none;
     border-right: none;
 }
 
-.start-trim-line {
-    right: 0;
+.right-handle {
+    transform: scaleX(-1);
 }
 
-.end-trim-line {
-    left: 0;
+.selector .left-handle {
+    left: -1px
+}
+
+.selector .right-handle {
+    right: -1px
+}
+
+.trimmer .left-handle {
+    right: -1px
+}
+
+.trimmer .right-handle {
+    left: -1px
 }
 
 .trim-handle {
     position: absolute;
-    left: calc(-$trim-handle-width / 2);
     width: $trim-handle-width;
     height: $trim-handle-height;
+    right: 0;
+    user-select: none;
+}
+
+.trimmer .trim-handle {
+    filter: hue-rotate(150deg);
 }
 
 .trim-handle img {
     position: absolute;
     width: $trim-handle-width;
     height: $trim-handle-height;
+    left: calc($trim-handle-border * 1.5);
 
     /* Make sure image dragging isn't triggered */
     user-select: none;
@@ -103,22 +136,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/audio-trimmer.jsx b/src/components/audio-trimmer/audio-trimmer.jsx
index 87ca75a9803fd1f515d5a98772778167fc1977f7..d9874ddd1f5de1ca61466269f4c99542efd68ad4 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 => (
     <div
-        className={styles.absolute}
+        className={classNames(styles.absolute, styles.trimmer)}
         ref={props.containerRef}
     >
         {props.trimStart === null ? null : (
@@ -20,28 +21,16 @@ const AudioTrimmer = props => (
                 onTouchStart={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>
+                <SelectionHandle
+                    handleStyle={styles.leftHandle}
+                />
             </Box>
         )}
-
         {props.playhead ? (
-            <div className={styles.playheadContainer}>
-                <div
-                    className={classNames(styles.trimLine, styles.playhead)}
-                    style={{
-                        transform: `translateX(${100 * props.playhead}%)`
-                    }}
-                />
-            </div>
+            <Playhead
+                playbackPosition={props.playhead}
+            />
         ) : null}
-
         {props.trimEnd === null ? null : (
             <Box
                 className={classNames(styles.absolute, styles.trimBackground, styles.endTrimBackground)}
@@ -53,14 +42,9 @@ const AudioTrimmer = props => (
                 onTouchStart={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>
+                <SelectionHandle
+                    handleStyle={styles.rightHandle}
+                />
             </Box>
         )}
     </div>
diff --git a/src/components/audio-trimmer/icon--handle.svg b/src/components/audio-trimmer/icon--handle.svg
index a3f52078f6dccb45ab223b10114df876e9a3a25e..dad1ea75167bd09a44d6ed9a921339800b2ceb02 100644
Binary files a/src/components/audio-trimmer/icon--handle.svg and b/src/components/audio-trimmer/icon--handle.svg differ
diff --git a/src/components/audio-trimmer/playhead.jsx b/src/components/audio-trimmer/playhead.jsx
new file mode 100644
index 0000000000000000000000000000000000000000..be5423809e53e597e09582035245685532547b6e
--- /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 => (
+    <div className={styles.playheadContainer}>
+        <div
+            className={classNames(styles.playhead)}
+            style={{
+                transform: `translateX(${100 * props.playbackPosition}%)`
+            }}
+        />
+    </div>
+);
+
+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 0000000000000000000000000000000000000000..f182ce6a4f11ef2b8ed690857224fc4af62d5674
--- /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 => (
+    <Box
+        className={classNames(styles.trimLine, props.handleStyle)}
+        onMouseDown={props.onMouseDown}
+        onTouchStart={props.onMouseDown}
+    >
+        <Box className={classNames(styles.trimHandle, styles.topTrimHandle)}>
+            <img src={handleIcon} />
+        </Box>
+        <Box className={classNames(styles.trimHandle, styles.bottomTrimHandle)}>
+            <img src={handleIcon} />
+        </Box>
+    </Box>
+);
+
+SelectionHandle.propTypes = {
+    handleStyle: PropTypes.string,
+    onMouseDown: PropTypes.func
+};
+
+export default SelectionHandle;
diff --git a/src/components/icon-button/icon-button.css b/src/components/icon-button/icon-button.css
index f58ee6f8d198e01c70a6c2175becc8420e0983d9..002c817ae4871abaee02fc7e65806e075a23451b 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 {
@@ -16,4 +17,14 @@
 
 .title {
     margin-top: 0.5rem;
+    text-align: center;
+}
+
+.disabled {
+    opacity: 0.5;
+    pointer-events: none;
+}
+
+.container:active {
+    background-color: $motion-light-transparent;
 }
diff --git a/src/components/icon-button/icon-button.jsx b/src/components/icon-button/icon-button.jsx
index 832e4a65bb5c786c6fd8b78d7dd1d2c41fbc570d..c6549fdf9af0af4984ff2b2c882603c8ec68c99e 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
 }) => (
     <div
-        className={classNames(styles.container, className)}
+        className={classNames(
+            styles.container,
+            className,
+            disabled ? styles.disabled : null
+        )}
         role="button"
-        onClick={onClick}
+        onClick={disabled ? null : onClick}
     >
         <img
             className={styles.icon}
@@ -27,6 +32,7 @@ const IconButton = ({
 
 IconButton.propTypes = {
     className: PropTypes.string,
+    disabled: PropTypes.bool,
     img: PropTypes.string,
     onClick: PropTypes.func.isRequired,
     title: PropTypes.node.isRequired
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 0000000000000000000000000000000000000000..d5f462117e482d6f6624ebe6a3727680f4bf5d68
Binary files /dev/null and b/src/components/sound-editor/icon--copy-to-new.svg differ
diff --git a/src/components/sound-editor/icon--copy.svg b/src/components/sound-editor/icon--copy.svg
new file mode 100644
index 0000000000000000000000000000000000000000..7bac62c1bd2bb1765acd924319be02383ad7a6e1
Binary files /dev/null and b/src/components/sound-editor/icon--copy.svg differ
diff --git a/src/components/sound-editor/icon--delete.svg b/src/components/sound-editor/icon--delete.svg
new file mode 100644
index 0000000000000000000000000000000000000000..8f662201e28c700bcaa97a138efd19d7679a8bf4
Binary files /dev/null and b/src/components/sound-editor/icon--delete.svg differ
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 0000000000000000000000000000000000000000..6c74875843c26da7cc06d2d179ca6805ce8c2a9b
Binary files /dev/null and b/src/components/sound-editor/icon--fade-in.svg differ
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 0000000000000000000000000000000000000000..96e8d624463241a310a4e92352afca077a59dbf4
Binary files /dev/null and b/src/components/sound-editor/icon--fade-out.svg differ
diff --git a/src/components/sound-editor/icon--lounder.svg b/src/components/sound-editor/icon--lounder.svg
deleted file mode 100644
index a1022b4b8b8149850855693d537373e383d4c85a..0000000000000000000000000000000000000000
Binary files a/src/components/sound-editor/icon--lounder.svg and /dev/null differ
diff --git a/src/components/sound-editor/icon--mute.svg b/src/components/sound-editor/icon--mute.svg
new file mode 100644
index 0000000000000000000000000000000000000000..6cdd71d4d7703e05efd0dcda3c1f366173b5fb2d
Binary files /dev/null and b/src/components/sound-editor/icon--mute.svg differ
diff --git a/src/components/sound-editor/icon--paste.svg b/src/components/sound-editor/icon--paste.svg
new file mode 100644
index 0000000000000000000000000000000000000000..53ca104c18f75b9044120b0ece86aa845df1d6b8
Binary files /dev/null and b/src/components/sound-editor/icon--paste.svg differ
diff --git a/src/components/sound-editor/icon--play.svg b/src/components/sound-editor/icon--play.svg
new file mode 100644
index 0000000000000000000000000000000000000000..817a2cab9f74df0adfa01c78be6882b9d983d8bc
Binary files /dev/null and b/src/components/sound-editor/icon--play.svg differ
diff --git a/src/components/sound-editor/icon--stop.svg b/src/components/sound-editor/icon--stop.svg
new file mode 100644
index 0000000000000000000000000000000000000000..c960e66c3c5a34f58549ee46be0193246c0a8678
Binary files /dev/null and b/src/components/sound-editor/icon--stop.svg differ
diff --git a/src/components/sound-editor/sound-editor.css b/src/components/sound-editor/sound-editor.css
index 279875fc8dc49ac95ca881969dc6f8c03095f0d4..7a791e6ab63c9aa40be54343de6d128c9f7a5485 100644
--- a/src/components/sound-editor/sound-editor.css
+++ b/src/components/sound-editor/sound-editor.css
@@ -63,7 +63,9 @@
     background: hsla(300, 53%, 60%, 0.15);
     border: 1px solid $ui-black-transparent;
     border-radius: 5px;
-    padding: 3px;
+
+    margin-top: 20px;
+    margin-bottom: 20px;
 }
 
 $border-radius: 0.25rem;
@@ -93,9 +95,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;
@@ -118,6 +120,7 @@ $border-radius: 0.25rem;
     color: $text-primary;
     font-size: 0.625rem;
     user-select: none;
+
 }
 
 [dir="ltr"] .trim-button {
@@ -130,7 +133,6 @@ $border-radius: 0.25rem;
 
 .trim-button > img {
     width: 1.25rem;
-    margin-bottom: -0.375rem;
 }
 
 .effect-button {
@@ -138,6 +140,7 @@ $border-radius: 0.25rem;
     color: $text-primary;
     font-size: 0.625rem;
     user-select: none;
+    padding: 0.25rem 0;
 }
 
 .effect-button + .effect-button {
@@ -150,12 +153,25 @@ $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 {
-    transform: scaleX(-1);
+.tool-button {
+    flex-basis: 60px;
+    color: $text-primary;
+    font-size: 0.625rem;
+    user-select: none;
+    padding: 0.25rem 0;
 }
 
-[dir="rtl"] .effect-button:nth-of-type(7) img {
+.tool-button + .tool-button {
+    margin: 0;
+}
+
+.tool-button img {
+    width: 4rem;
+    height: 1.5rem;
+    margin-bottom: -0.375rem;
+}
+
+[dir="rtl"] .flip-in-rtl img {
     transform: scaleX(-1);
 }
 
@@ -167,6 +183,10 @@ $border-radius: 0.25rem;
     margin-right: 1rem;
 }
 
+.button-group {
+    display: flex;
+}
+
 .button-group .button {
     border-radius: 0;
 }
diff --git a/src/components/sound-editor/sound-editor.jsx b/src/components/sound-editor/sound-editor.jsx
index f97338e06a55bf75e0bceda2ddc33d8a9be1070b..84f56d08bee4ae4fa3d454ccec9cffa07c1a5374 100644
--- a/src/components/sound-editor/sound-editor.jsx
+++ b/src/components/sound-editor/sound-editor.jsx
@@ -8,24 +8,29 @@ 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 playIcon from './icon--play.svg';
+import stopIcon from './icon--stop.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';
 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';
+
+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);
 
@@ -45,10 +50,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',
@@ -99,11 +119,30 @@ 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'
     }
 });
 
 const SoundEditor = props => (
-    <div className={styles.editorContainer}>
+    <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)}>
@@ -141,17 +180,33 @@ const SoundEditor = props => (
                     </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={classNames(styles.trimButton, {
-                    [styles.trimButtonActive]: props.trimStart !== null
-                })}
-                img={props.trimStart === null ? trimIcon : trimConfirmIcon}
-                title={props.trimStart === null ? (
-                    <FormattedMessage {...messages.trim} />
-                ) : (
-                    <FormattedMessage {...messages.save} />
-                )}
-                onClick={props.onActivateTrim}
+                className={styles.toolButton}
+                disabled={props.trimStart === null}
+                img={deleteIcon}
+                title={props.intl.formatMessage(messages.delete)}
+                onClick={props.onDelete}
             />
         </div>
         <div className={styles.row}>
@@ -161,12 +216,13 @@ const SoundEditor = props => (
                     height={160}
                     width={600}
                 />
-                <AudioTrimmer
+                <AudioSelector
                     playhead={props.playhead}
                     trimEnd={props.trimEnd}
                     trimStart={props.trimStart}
-                    onSetTrimEnd={props.onSetTrimEnd}
-                    onSetTrimStart={props.onSetTrimStart}
+                    onPlay={props.onPlay}
+                    onSetTrim={props.onSetTrim}
+                    onStop={props.onStop}
                 />
             </div>
         </div>
@@ -209,61 +265,81 @@ const SoundEditor = props => (
                 onClick={props.onSlower}
             />
             <IconButton
-                className={styles.effectButton}
-                img={echoIcon}
-                title={<FormattedMessage {...messages.echo} />}
-                onClick={props.onEcho}
-            />
-            <IconButton
-                className={styles.effectButton}
-                img={robotIcon}
-                title={<FormattedMessage {...messages.robot} />}
-                onClick={props.onRobot}
-            />
-            <IconButton
-                className={styles.effectButton}
+                className={classNames(styles.effectButton, styles.flipInRtl)}
                 img={louderIcon}
                 title={<FormattedMessage {...messages.louder} />}
                 onClick={props.onLouder}
             />
             <IconButton
-                className={styles.effectButton}
+                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}
+            />
         </div>
     </div>
 );
 
 SoundEditor.propTypes = {
+    canPaste: PropTypes.bool.isRequired,
     canRedo: PropTypes.bool.isRequired,
     canUndo: PropTypes.bool.isRequired,
     chunkLevels: PropTypes.arrayOf(PropTypes.number).isRequired,
     intl: intlShape,
     name: PropTypes.string.isRequired,
-    onActivateTrim: PropTypes.func,
     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,
     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/components/waveform/waveform.jsx b/src/components/waveform/waveform.jsx
index 48a0a4935c376995624e4cf4c41c194539b27ec8..3c7b4d26b89b74c223165c3597e828b437ceca63 100644
--- a/src/components/waveform/waveform.jsx
+++ b/src/components/waveform/waveform.jsx
@@ -19,17 +19,21 @@ 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];
+        // Need at least two points to render waveform.
+        if (filteredData.length === 1) {
+            filteredData.push(filteredData[0]);
+        }
 
+        const maxIndex = filteredData.length - 1;
         const points = [
-            ...cappedData.map((v, i) =>
-                [width * i / cappedData.length, height * v / 2]
+            ...filteredData.map((v, i) =>
+                [width * (i / maxIndex), height * v / 2]
             ),
-            ...cappedData.reverse().map((v, i) =>
-                [width * (cappedData.length - i - 1) / cappedData.length, -height * v / 2]
+            ...filteredData.reverse().map((v, i) =>
+                [width * (1 - (i / maxIndex)), -height * v / 2]
             )
         ];
         const pathComponents = points.map(([x, y], i) => {
@@ -40,15 +44,8 @@ class Waveform extends React.PureComponent {
         return (
             <svg
                 className={styles.container}
-                viewBox={`-1 0 ${width} ${height}`}
+                viewBox={`0 0 ${width} ${height}`}
             >
-                <line
-                    className={styles.baseline}
-                    x1={-1}
-                    x2={width}
-                    y1={height / 2}
-                    y2={height / 2}
-                />
                 <g transform={`scale(1, -1) translate(0, -${height / 2})`}>
                     <path
                         className={styles.waveformPath}
diff --git a/src/containers/audio-selector.jsx b/src/containers/audio-selector.jsx
new file mode 100644
index 0000000000000000000000000000000000000000..2d5974d5ce8cd69ee2dd7a2ed8e9c592b94955e6
--- /dev/null
+++ b/src/containers/audio-selector.jsx
@@ -0,0 +1,155 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import bindAll from 'lodash.bindall';
+import AudioSelectorComponent from '../components/audio-trimmer/audio-selector.jsx';
+import {getEventXY} from '../lib/touch-utils';
+import DragRecognizer from '../lib/drag-recognizer';
+
+const MIN_LENGTH = 0.01;
+const MIN_DURATION = 500;
+
+class AudioSelector extends React.Component {
+    constructor (props) {
+        super(props);
+        bindAll(this, [
+            'handleNewSelectionMouseDown',
+            'handleTrimStartMouseDown',
+            'handleTrimEndMouseDown',
+            'handleTrimStartMouseMove',
+            'handleTrimEndMouseMove',
+            'handleTrimStartMouseUp',
+            'handleTrimEndMouseUp',
+            'storeRef'
+        ]);
+
+        this.state = {
+            trimStart: props.trimStart,
+            trimEnd: props.trimEnd
+        };
+
+        this.clickStartTime = 0;
+
+        this.trimStartDragRecognizer = new DragRecognizer({
+            onDrag: this.handleTrimStartMouseMove,
+            onDragEnd: this.handleTrimStartMouseUp,
+            touchDragAngle: 90,
+            distanceThreshold: 0
+        });
+        this.trimEndDragRecognizer = new DragRecognizer({
+            onDrag: this.handleTrimEndMouseMove,
+            onDragEnd: this.handleTrimEndMouseUp,
+            touchDragAngle: 90,
+            distanceThreshold: 0
+        });
+    }
+    componentWillReceiveProps (newProps) {
+        if (newProps.trimStart === this.props.trimStart) return;
+        this.setState({
+            trimStart: newProps.trimStart,
+            trimEnd: newProps.trimEnd
+        });
+    }
+    clearSelection () {
+        this.props.onSetTrim(null, null);
+    }
+    handleNewSelectionMouseDown (e) {
+        const {width, left} = this.containerElement.getBoundingClientRect();
+        this.initialTrimEnd = (getEventXY(e).x - left) / width;
+        this.initialTrimStart = this.initialTrimEnd;
+        this.props.onSetTrim(this.initialTrimStart, this.initialTrimEnd);
+
+        this.clickStartTime = Date.now();
+
+        this.containerSize = width;
+        this.trimEndDragRecognizer.start(e);
+
+        e.preventDefault();
+    }
+    handleTrimStartMouseMove (currentOffset, initialOffset) {
+        const dx = (currentOffset.x - initialOffset.x) / this.containerSize;
+        const newTrim = Math.max(0, Math.min(1, this.initialTrimStart + dx));
+        if (newTrim > this.initialTrimEnd) {
+            this.setState({
+                trimStart: this.initialTrimEnd,
+                trimEnd: newTrim
+            });
+        } else {
+            this.setState({
+                trimStart: newTrim,
+                trimEnd: this.initialTrimEnd
+            });
+        }
+    }
+    handleTrimEndMouseMove (currentOffset, initialOffset) {
+        const dx = (currentOffset.x - initialOffset.x) / this.containerSize;
+        const newTrim = Math.min(1, Math.max(0, this.initialTrimEnd + dx));
+        if (newTrim < this.initialTrimStart) {
+            this.setState({
+                trimStart: newTrim,
+                trimEnd: this.initialTrimStart
+            });
+        } else {
+            this.setState({
+                trimStart: this.initialTrimStart,
+                trimEnd: newTrim
+            });
+        }
+    }
+    handleTrimStartMouseUp () {
+        this.props.onSetTrim(this.state.trimStart, this.state.trimEnd);
+    }
+    handleTrimEndMouseUp () {
+        // If the selection was made quickly (tooFast) and is small (tooShort),
+        // deselect instead. This allows click-to-deselect even if you drag
+        // a little bit by accident. It also allows very quickly making a
+        // selection, as long as it is above a minimum length.
+        const tooFast = (Date.now() - this.clickStartTime) < MIN_DURATION;
+        const tooShort = (this.state.trimEnd - this.state.trimStart) < MIN_LENGTH;
+        if (tooFast && tooShort) {
+            this.clearSelection();
+        } else {
+            this.props.onSetTrim(this.state.trimStart, this.state.trimEnd);
+        }
+    }
+    handleTrimStartMouseDown (e) {
+        this.containerSize = this.containerElement.getBoundingClientRect().width;
+        this.trimStartDragRecognizer.start(e);
+        this.initialTrimStart = this.props.trimStart;
+        this.initialTrimEnd = this.props.trimEnd;
+        e.stopPropagation();
+        e.preventDefault();
+    }
+    handleTrimEndMouseDown (e) {
+        this.containerSize = this.containerElement.getBoundingClientRect().width;
+        this.trimEndDragRecognizer.start(e);
+        this.initialTrimEnd = this.props.trimEnd;
+        this.initialTrimStart = this.props.trimStart;
+        e.stopPropagation();
+        e.preventDefault();
+    }
+    storeRef (el) {
+        this.containerElement = el;
+    }
+    render () {
+        return (
+            <AudioSelectorComponent
+                containerRef={this.storeRef}
+                playhead={this.props.playhead}
+                trimEnd={this.state.trimEnd}
+                trimStart={this.state.trimStart}
+                onNewSelectionMouseDown={this.handleNewSelectionMouseDown}
+                onTrimEndMouseDown={this.handleTrimEndMouseDown}
+                onTrimStartMouseDown={this.handleTrimStartMouseDown}
+            />
+        );
+    }
+}
+
+AudioSelector.propTypes = {
+    onSetTrim: PropTypes.func,
+    playhead: PropTypes.number,
+    trimEnd: PropTypes.number,
+    trimStart: PropTypes.number
+};
+
+export default AudioSelector;
diff --git a/src/containers/audio-trimmer.jsx b/src/containers/audio-trimmer.jsx
index 9cf6e38f16f48fb9ef89fad9ad0beb10607b636a..78bd4ebac14f68d5ab6b13408a4b589758134be4 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,52 +14,39 @@ class AudioTrimmer extends React.Component {
             'handleTrimEndMouseDown',
             'handleTrimStartMouseMove',
             'handleTrimEndMouseMove',
-            'handleTrimStartMouseUp',
-            'handleTrimEndMouseUp',
             'storeRef'
         ]);
+        this.trimStartDragRecognizer = new DragRecognizer({
+            onDrag: this.handleTrimStartMouseMove,
+            touchDragAngle: 90,
+            distanceThreshold: 0
+        });
+        this.trimEndDragRecognizer = new DragRecognizer({
+            onDrag: this.handleTrimEndMouseMove,
+            touchDragAngle: 90,
+            distanceThreshold: 0
+        });
+
     }
-    handleTrimStartMouseMove (e) {
-        const containerSize = this.containerElement.getBoundingClientRect().width;
-        const dx = (getEventXY(e).x - this.initialX) / containerSize;
+    handleTrimStartMouseMove (currentOffset, initialOffset) {
+        const dx = (currentOffset.x - initialOffset.x) / this.containerSize;
         const newTrim = Math.max(0, Math.min(this.props.trimEnd - MIN_LENGTH, this.initialTrim + dx));
         this.props.onSetTrimStart(newTrim);
-        e.preventDefault();
     }
-    handleTrimEndMouseMove (e) {
-        const containerSize = this.containerElement.getBoundingClientRect().width;
-        const dx = (getEventXY(e).x - this.initialX) / containerSize;
+    handleTrimEndMouseMove (currentOffset, initialOffset) {
+        const dx = (currentOffset.x - initialOffset.x) / this.containerSize;
         const newTrim = Math.min(1, Math.max(this.props.trimStart + MIN_LENGTH, this.initialTrim + dx));
         this.props.onSetTrimEnd(newTrim);
-        e.preventDefault();
-    }
-    handleTrimStartMouseUp () {
-        window.removeEventListener('mousemove', this.handleTrimStartMouseMove);
-        window.removeEventListener('mouseup', this.handleTrimStartMouseUp);
-        window.removeEventListener('touchmove', this.handleTrimStartMouseMove);
-        window.removeEventListener('touchend', this.handleTrimStartMouseUp);
-    }
-    handleTrimEndMouseUp () {
-        window.removeEventListener('mousemove', this.handleTrimEndMouseMove);
-        window.removeEventListener('mouseup', this.handleTrimEndMouseUp);
-        window.removeEventListener('touchmove', this.handleTrimEndMouseMove);
-        window.removeEventListener('touchend', this.handleTrimEndMouseUp);
     }
     handleTrimStartMouseDown (e) {
-        this.initialX = getEventXY(e).x;
+        this.containerSize = this.containerElement.getBoundingClientRect().width;
+        this.trimStartDragRecognizer.start(e);
         this.initialTrim = this.props.trimStart;
-        window.addEventListener('mousemove', this.handleTrimStartMouseMove);
-        window.addEventListener('mouseup', this.handleTrimStartMouseUp);
-        window.addEventListener('touchmove', this.handleTrimStartMouseMove);
-        window.addEventListener('touchend', this.handleTrimStartMouseUp);
     }
     handleTrimEndMouseDown (e) {
-        this.initialX = getEventXY(e).x;
+        this.containerSize = this.containerElement.getBoundingClientRect().width;
+        this.trimEndDragRecognizer.start(e);
         this.initialTrim = this.props.trimEnd;
-        window.addEventListener('mousemove', this.handleTrimEndMouseMove);
-        window.addEventListener('mouseup', this.handleTrimEndMouseUp);
-        window.addEventListener('touchmove', this.handleTrimEndMouseMove);
-        window.addEventListener('touchend', this.handleTrimEndMouseUp);
     }
     storeRef (el) {
         this.containerElement = el;
diff --git a/src/containers/record-modal.jsx b/src/containers/record-modal.jsx
index a7a9e8f83d8e55a47e5be1047bf05fad5ed99224..ffaf968a250c7dcc1e1d3f7249e73abba42f84a4 100644
--- a/src/containers/record-modal.jsx
+++ b/src/containers/record-modal.jsx
@@ -2,8 +2,8 @@ import bindAll from 'lodash.bindall';
 import PropTypes from 'prop-types';
 import React from 'react';
 import VM from 'scratch-vm';
-import WavEncoder from 'wav-encoder';
 import {connect} from 'react-redux';
+import {encodeAndAddSoundToVM} from '../lib/audio/audio-util.js';
 
 import RecordModalComponent from '../components/record-modal/record-modal.jsx';
 
@@ -71,39 +71,12 @@ class RecordModal extends React.Component {
             const startIndex = Math.floor(this.state.trimStart * sampleCount);
             const endIndex = Math.floor(this.state.trimEnd * sampleCount);
             const clippedSamples = this.state.samples.slice(startIndex, endIndex);
-            WavEncoder.encode({
-                sampleRate: this.state.sampleRate,
-                channelData: [clippedSamples]
-            }).then(wavBuffer => {
-                const vmSound = {
-                    format: '',
-                    dataFormat: 'wav',
-                    rate: this.state.sampleRate,
-                    sampleCount: clippedSamples.length
-                };
 
-                // Create an asset from the encoded .wav and get resulting md5
-                const storage = this.props.vm.runtime.storage;
-                vmSound.asset = storage.createAsset(
-                    storage.AssetType.Sound,
-                    storage.DataFormat.WAV,
-                    new Uint8Array(wavBuffer),
-                    null,
-                    true // generate md5
-                );
-                vmSound.assetId = vmSound.asset.assetId;
-
-                // update vmSound object with md5 property
-                vmSound.md5 = `${vmSound.assetId}.${vmSound.dataFormat}`;
-                // The VM will update the sound name to a fresh name
-                // if the following is already taken
-                vmSound.name = 'recording1';
-
-                this.props.vm.addSound(vmSound).then(() => {
+            encodeAndAddSoundToVM(this.props.vm, clippedSamples, this.state.sampleRate, 'recording1',
+                () => {
                     this.props.onClose();
                     this.props.onNewSound();
                 });
-            });
         });
     }
     handleCancel () {
diff --git a/src/containers/sound-editor.jsx b/src/containers/sound-editor.jsx
index a3cdaaf674fee46aeddcaaac66fcfbf168deb58a..a2b19719f17eaee8fa65a67333ce5e621a382699 100644
--- a/src/containers/sound-editor.jsx
+++ b/src/containers/sound-editor.jsx
@@ -2,10 +2,11 @@ import bindAll from 'lodash.bindall';
 import PropTypes from 'prop-types';
 import React from 'react';
 import WavEncoder from 'wav-encoder';
+import VM from 'scratch-vm';
 
 import {connect} from 'react-redux';
 
-import {computeChunkedRMS, SOUND_BYTE_LIMIT} from '../lib/audio/audio-util.js';
+import {computeChunkedRMS, encodeAndAddSoundToVM, SOUND_BYTE_LIMIT} from '../lib/audio/audio-util.js';
 import AudioEffects from '../lib/audio/audio-effects.js';
 import SoundEditorComponent from '../components/sound-editor/sound-editor.jsx';
 import AudioBufferPlayer from '../lib/audio/audio-buffer-player.js';
@@ -17,21 +18,29 @@ class SoundEditor extends React.Component {
     constructor (props) {
         super(props);
         bindAll(this, [
+            'copy',
             'copyCurrentBuffer',
+            'handleCopyToNew',
             'handleStoppedPlaying',
             'handleChangeName',
             'handlePlay',
             'handleStopPlaying',
             'handleUpdatePlayhead',
-            'handleActivateTrim',
-            'handleUpdateTrimEnd',
-            'handleUpdateTrimStart',
+            'handleDelete',
+            'handleUpdateTrim',
             'handleEffect',
             'handleUndo',
             'handleRedo',
-            'submitNewSamples'
+            'submitNewSamples',
+            'handleCopy',
+            'handlePaste',
+            'paste',
+            'handleKeyPress',
+            'handleContainerClick',
+            'setRef'
         ]);
         this.state = {
+            copyBuffer: null,
             chunkLevels: computeChunkedRMS(this.props.samples),
             playhead: null, // null is not playing, [0 -> 1] is playing percent
             trimStart: null,
@@ -40,28 +49,84 @@ class SoundEditor extends React.Component {
 
         this.redoStack = [];
         this.undoStack = [];
+
+        this.ref = null;
     }
     componentDidMount () {
         this.audioBufferPlayer = new AudioBufferPlayer(this.props.samples, this.props.sampleRate);
+
+        document.addEventListener('keydown', this.handleKeyPress);
     }
     componentWillReceiveProps (newProps) {
         if (newProps.soundId !== this.props.soundId) { // A different sound has been selected
             this.redoStack = [];
             this.undoStack = [];
             this.resetState(newProps.samples, newProps.sampleRate);
+            this.setState({
+                trimStart: null,
+                trimEnd: null
+            });
         }
     }
     componentWillUnmount () {
         this.audioBufferPlayer.stop();
+
+        document.removeEventListener('keydown', this.handleKeyPress);
+    }
+    handleKeyPress (event) {
+        if (event.target instanceof HTMLInputElement) {
+            // Ignore keyboard shortcuts if a text input field is focused
+            return;
+        }
+        if (event.key === ' ') {
+            event.preventDefault();
+            if (this.state.playhead) {
+                this.handleStopPlaying();
+            } else {
+                this.handlePlay();
+            }
+        }
+        if (event.key === 'Delete' || event.key === 'Backspace') {
+            event.preventDefault();
+            if (event.shiftKey) {
+                this.handleDeleteInverse();
+            } else {
+                this.handleDelete();
+            }
+        }
+        if (event.key === 'Escape') {
+            event.preventDefault();
+            this.handleUpdateTrim(null, null);
+        }
+        if (event.metaKey || event.ctrlKey) {
+            if (event.shiftKey && event.key.toLowerCase() === 'z') {
+                event.preventDefault();
+                if (this.redoStack.length > 0) {
+                    this.handleRedo();
+                }
+            } else if (event.key === 'z') {
+                if (this.undoStack.length > 0) {
+                    event.preventDefault();
+                    this.handleUndo();
+                }
+            } else if (event.key === 'c') {
+                event.preventDefault();
+                this.handleCopy();
+            } else if (event.key === 'v') {
+                event.preventDefault();
+                this.handlePaste();
+            } else if (event.key === 'a') {
+                event.preventDefault();
+                this.handleUpdateTrim(0, 1);
+            }
+        }
     }
     resetState (samples, sampleRate) {
         this.audioBufferPlayer.stop();
         this.audioBufferPlayer = new AudioBufferPlayer(samples, sampleRate);
         this.setState({
             chunkLevels: computeChunkedRMS(samples),
-            playhead: null,
-            trimStart: null,
-            trimEnd: null
+            playhead: null
         });
     }
     submitNewSamples (samples, sampleRate, skipUndo) {
@@ -93,7 +158,7 @@ class SoundEditor extends React.Component {
                 if (this.undoStack.length >= UNDO_STACK_SIZE) {
                     this.undoStack.shift(); // Drop the first element off the array
                 }
-                this.undoStack.push(this.copyCurrentBuffer());
+                this.undoStack.push(this.getUndoItem());
             }
             this.resetState(samples, sampleRate);
             this.props.vm.updateSoundBuffer(
@@ -106,6 +171,7 @@ class SoundEditor extends React.Component {
         return false; // Update failed
     }
     handlePlay () {
+        this.audioBufferPlayer.stop();
         this.audioBufferPlayer.play(
             this.state.trimStart || 0,
             this.state.trimEnd || 1,
@@ -125,31 +191,46 @@ class SoundEditor extends React.Component {
     handleChangeName (name) {
         this.props.vm.renameSound(this.props.soundIndex, name);
     }
-    handleActivateTrim () {
-        if (this.state.trimStart === null && this.state.trimEnd === null) {
-            this.setState({trimEnd: 0.95, trimStart: 0.05});
+    handleDelete () {
+        const {samples, sampleRate} = this.copyCurrentBuffer();
+        const sampleCount = samples.length;
+        const startIndex = Math.floor(this.state.trimStart * sampleCount);
+        const endIndex = Math.floor(this.state.trimEnd * sampleCount);
+        const firstPart = samples.slice(0, startIndex);
+        const secondPart = samples.slice(endIndex, sampleCount);
+        const newLength = firstPart.length + secondPart.length;
+        let newSamples;
+        if (newLength === 0) {
+            newSamples = new Float32Array(1);
         } else {
-            const {samples, sampleRate} = this.copyCurrentBuffer();
-            const sampleCount = samples.length;
-            const startIndex = Math.floor(this.state.trimStart * sampleCount);
-            const endIndex = Math.floor(this.state.trimEnd * sampleCount);
-            if (endIndex > startIndex) { // Strictly greater to prevent 0 sample sounds
-                const clippedSamples = samples.slice(startIndex, endIndex);
-                this.submitNewSamples(clippedSamples, sampleRate);
-            } else {
-                // Just clear the trim state, it cannot be completed
-                this.setState({
-                    trimStart: null,
-                    trimEnd: null
-                });
-            }
+            newSamples = new Float32Array(newLength);
+            newSamples.set(firstPart, 0);
+            newSamples.set(secondPart, firstPart.length);
         }
+        this.submitNewSamples(newSamples, sampleRate);
+        this.setState({
+            trimStart: null,
+            trimEnd: null
+        });
     }
-    handleUpdateTrimEnd (trimEnd) {
-        this.setState({trimEnd});
+    handleDeleteInverse () {
+        const {samples, sampleRate} = this.copyCurrentBuffer();
+        const sampleCount = samples.length;
+        const startIndex = Math.floor(this.state.trimStart * sampleCount);
+        const endIndex = Math.floor(this.state.trimEnd * sampleCount);
+        let clippedSamples = samples.slice(startIndex, endIndex);
+        if (clippedSamples.length === 0) {
+            clippedSamples = new Float32Array(1);
+        }
+        this.submitNewSamples(clippedSamples, sampleRate);
+        this.setState({
+            trimStart: null,
+            trimEnd: null
+        });
     }
-    handleUpdateTrimStart (trimStart) {
-        this.setState({trimStart});
+    handleUpdateTrim (trimStart, trimEnd) {
+        this.setState({trimStart, trimEnd});
+        this.handleStopPlaying();
     }
     effectFactory (name) {
         return () => this.handleEffect(name);
@@ -162,52 +243,181 @@ class SoundEditor extends React.Component {
         };
     }
     handleEffect (name) {
-        const effects = new AudioEffects(this.audioBufferPlayer.buffer, name);
-        effects.process(({renderedBuffer}) => {
+        const trimStart = this.state.trimStart === null ? 0.0 : this.state.trimStart;
+        const trimEnd = this.state.trimEnd === null ? 1.0 : this.state.trimEnd;
+
+        // Offline audio context needs at least 2 samples
+        if (this.audioBufferPlayer.buffer.length < 2) {
+            return;
+        }
+
+        const effects = new AudioEffects(this.audioBufferPlayer.buffer, name, trimStart, trimEnd);
+        effects.process((renderedBuffer, adjustedTrimStart, adjustedTrimEnd) => {
             const samples = renderedBuffer.getChannelData(0);
             const sampleRate = renderedBuffer.sampleRate;
             const success = this.submitNewSamples(samples, sampleRate);
-            if (success) this.handlePlay();
+            if (success) {
+                if (this.state.trimStart === null) {
+                    this.handlePlay();
+                } else {
+                    this.setState({trimStart: adjustedTrimStart, trimEnd: adjustedTrimEnd}, this.handlePlay);
+                }
+            }
         });
     }
+    getUndoItem () {
+        return {
+            ...this.copyCurrentBuffer(),
+            trimStart: this.state.trimStart,
+            trimEnd: this.state.trimEnd
+        };
+    }
     handleUndo () {
-        this.redoStack.push(this.copyCurrentBuffer());
-        const {samples, sampleRate} = this.undoStack.pop();
+        this.redoStack.push(this.getUndoItem());
+        const {samples, sampleRate, trimStart, trimEnd} = this.undoStack.pop();
         if (samples) {
             this.submitNewSamples(samples, sampleRate, true);
-            this.handlePlay();
+            this.setState({trimStart: trimStart, trimEnd: trimEnd}, this.handlePlay);
         }
     }
     handleRedo () {
-        const {samples, sampleRate} = this.redoStack.pop();
+        const {samples, sampleRate, trimStart, trimEnd} = this.redoStack.pop();
         if (samples) {
-            this.undoStack.push(this.copyCurrentBuffer());
+            this.undoStack.push(this.getUndoItem());
             this.submitNewSamples(samples, sampleRate, true);
+            this.setState({trimStart: trimStart, trimEnd: trimEnd}, this.handlePlay);
+        }
+    }
+    handleCopy () {
+        this.copy();
+    }
+    copy (callback) {
+        const trimStart = this.state.trimStart === null ? 0.0 : this.state.trimStart;
+        const trimEnd = this.state.trimEnd === null ? 1.0 : this.state.trimEnd;
+
+        const newCopyBuffer = this.copyCurrentBuffer();
+        const trimStartSamples = trimStart * newCopyBuffer.samples.length;
+        const trimEndSamples = trimEnd * newCopyBuffer.samples.length;
+        newCopyBuffer.samples = newCopyBuffer.samples.slice(trimStartSamples, trimEndSamples);
+
+        this.setState({
+            copyBuffer: newCopyBuffer
+        }, callback);
+    }
+    handleCopyToNew () {
+        this.copy(() => {
+            encodeAndAddSoundToVM(this.props.vm, this.state.copyBuffer.samples,
+                this.state.copyBuffer.sampleRate, this.props.name);
+        });
+    }
+    resampleBufferToRate (buffer, newRate) {
+        return new Promise(resolve => {
+            if (window.OfflineAudioContext) {
+                const sampleRateRatio = newRate / buffer.sampleRate;
+                const newLength = sampleRateRatio * buffer.samples.length;
+                const offlineContext = new window.OfflineAudioContext(1, newLength, newRate);
+                const source = offlineContext.createBufferSource();
+                const audioBuffer = offlineContext.createBuffer(1, buffer.samples.length, buffer.sampleRate);
+                audioBuffer.getChannelData(0).set(buffer.samples);
+                source.buffer = audioBuffer;
+                source.connect(offlineContext.destination);
+                source.start();
+                offlineContext.startRendering();
+                offlineContext.oncomplete = ({renderedBuffer}) => {
+                    resolve({
+                        samples: renderedBuffer.getChannelData(0),
+                        sampleRate: newRate
+                    });
+                };
+            }
+        });
+    }
+    paste () {
+        // If there's no selection, paste at the end of the sound
+        const {samples} = this.copyCurrentBuffer();
+        if (this.state.trimStart === null) {
+            const newLength = samples.length + this.state.copyBuffer.samples.length;
+            const newSamples = new Float32Array(newLength);
+            newSamples.set(samples, 0);
+            newSamples.set(this.state.copyBuffer.samples, samples.length);
+            this.submitNewSamples(newSamples, this.props.sampleRate, false);
             this.handlePlay();
+        } else {
+            // else replace the selection with the pasted sound
+            const trimStartSamples = this.state.trimStart * samples.length;
+            const trimEndSamples = this.state.trimEnd * samples.length;
+            const firstPart = samples.slice(0, trimStartSamples);
+            const lastPart = samples.slice(trimEndSamples);
+            const newLength = firstPart.length + this.state.copyBuffer.samples.length + lastPart.length;
+            const newSamples = new Float32Array(newLength);
+            newSamples.set(firstPart, 0);
+            newSamples.set(this.state.copyBuffer.samples, firstPart.length);
+            newSamples.set(lastPart, firstPart.length + this.state.copyBuffer.samples.length);
+
+            const trimStartSeconds = trimStartSamples / this.props.sampleRate;
+            const trimEndSeconds = trimStartSeconds +
+                (this.state.copyBuffer.samples.length / this.state.copyBuffer.sampleRate);
+            const newDurationSeconds = newSamples.length / this.state.copyBuffer.sampleRate;
+            const adjustedTrimStart = trimStartSeconds / newDurationSeconds;
+            const adjustedTrimEnd = trimEndSeconds / newDurationSeconds;
+            this.submitNewSamples(newSamples, this.props.sampleRate, false);
+            this.setState({
+                trimStart: adjustedTrimStart,
+                trimEnd: adjustedTrimEnd
+            }, this.handlePlay);
+        }
+    }
+    handlePaste () {
+        if (!this.state.copyBuffer) return;
+        if (this.state.copyBuffer.sampleRate === this.props.sampleRate) {
+            this.paste();
+        } else {
+            this.resampleBufferToRate(this.state.copyBuffer, this.props.sampleRate).then(buffer => {
+                this.setState({
+                    copyBuffer: buffer
+                }, this.paste);
+            });
+        }
+    }
+    setRef (element) {
+        this.ref = element;
+    }
+    handleContainerClick (e) {
+        // If the click is on the sound editor's div (and not any other element), delesect
+        if (e.target === this.ref && this.state.trimStart !== null) {
+            this.handleUpdateTrim(null, null);
         }
     }
     render () {
         const {effectTypes} = AudioEffects;
         return (
             <SoundEditorComponent
+                canPaste={this.state.copyBuffer !== null}
                 canRedo={this.redoStack.length > 0}
                 canUndo={this.undoStack.length > 0}
                 chunkLevels={this.state.chunkLevels}
                 name={this.props.name}
                 playhead={this.state.playhead}
+                setRef={this.setRef}
                 trimEnd={this.state.trimEnd}
                 trimStart={this.state.trimStart}
-                onActivateTrim={this.handleActivateTrim}
                 onChangeName={this.handleChangeName}
+                onContainerClick={this.handleContainerClick}
+                onCopy={this.handleCopy}
+                onCopyToNew={this.handleCopyToNew}
+                onDelete={this.handleDelete}
                 onEcho={this.effectFactory(effectTypes.ECHO)}
+                onFadeIn={this.effectFactory(effectTypes.FADEIN)}
+                onFadeOut={this.effectFactory(effectTypes.FADEOUT)}
                 onFaster={this.effectFactory(effectTypes.FASTER)}
                 onLouder={this.effectFactory(effectTypes.LOUDER)}
+                onMute={this.effectFactory(effectTypes.MUTE)}
+                onPaste={this.handlePaste}
                 onPlay={this.handlePlay}
                 onRedo={this.handleRedo}
                 onReverse={this.effectFactory(effectTypes.REVERSE)}
                 onRobot={this.effectFactory(effectTypes.ROBOT)}
-                onSetTrimEnd={this.handleUpdateTrimEnd}
-                onSetTrimStart={this.handleUpdateTrimStart}
+                onSetTrim={this.handleUpdateTrim}
                 onSlower={this.effectFactory(effectTypes.SLOWER)}
                 onSofter={this.effectFactory(effectTypes.SOFTER)}
                 onStop={this.handleStopPlaying}
@@ -223,10 +433,7 @@ SoundEditor.propTypes = {
     samples: PropTypes.instanceOf(Float32Array),
     soundId: PropTypes.string,
     soundIndex: PropTypes.number,
-    vm: PropTypes.shape({
-        updateSoundBuffer: PropTypes.func,
-        renameSound: PropTypes.func
-    })
+    vm: PropTypes.instanceOf(VM).isRequired
 };
 
 const mapStateToProps = (state, {soundIndex}) => {
diff --git a/src/lib/audio/audio-effects.js b/src/lib/audio/audio-effects.js
index ab170f9c677924e2be4f4995287e9699e1271ffc..8dd551f3801cc4fc04832a46220f8f30a91ccd45 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,32 +11,56 @@ const effectTypes = {
     SOFTER: 'lower',
     FASTER: 'faster',
     SLOWER: 'slower',
-    ECHO: 'echo'
+    ECHO: 'echo',
+    FADEIN: 'fade in',
+    FADEOUT: 'fade out',
+    MUTE: 'mute'
 };
 
 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);
+        let adjustedAffectedSampleCount = affectedSampleCount;
+        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 + EchoEffect.TAIL_SECONDS) * buffer.sampleRate));
             break;
         case effectTypes.FASTER:
-            playbackRate = pitchRatio;
-            sampleCount = Math.floor(buffer.length / playbackRate);
+            this.playbackRate = pitchRatio;
+            adjustedAffectedSampleCount = Math.floor(affectedSampleCount / this.playbackRate);
+            sampleCount = unaffectedSampleCount + adjustedAffectedSampleCount;
+
             break;
         case effectTypes.SLOWER:
-            playbackRate = 1 / pitchRatio;
-            sampleCount = Math.floor(buffer.length / playbackRate);
+            this.playbackRate = 1 / pitchRatio;
+            adjustedAffectedSampleCount = Math.floor(affectedSampleCount / this.playbackRate);
+            sampleCount = unaffectedSampleCount + adjustedAffectedSampleCount;
             break;
         }
+
+        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);
         } else {
@@ -52,8 +78,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 +98,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 +105,38 @@ 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,
+                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;
+        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;
         }
 
@@ -96,7 +151,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/audio-util.js b/src/lib/audio/audio-util.js
index 2b68fcbe8af5978e87537e4fa904d431e502abf6..a01c30de811f9c4ec9dab47175c51e9fc96653a5 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
 };
diff --git a/src/lib/audio/effects/echo-effect.js b/src/lib/audio/effects/echo-effect.js
index 3c7c06806b3b99ff7a09d985dd52629d323bbc32..0ef4b93c265c529a1f9f98dbb42b0219d08507ec 100644
--- a/src/lib/audio/effects/echo-effect.js
+++ b/src/lib/audio/effects/echo-effect.js
@@ -1,15 +1,23 @@
 class EchoEffect {
-    constructor (audioContext, delayTime) {
+    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();
 
         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;
+        this.delay.delayTime.value = EchoEffect.DELAY_TIME;
         this.decay = this.audioContext.createGain();
         this.decay.gain.value = 0.3;
 
diff --git a/src/lib/audio/effects/fade-effect.js b/src/lib/audio/effects/fade-effect.js
new file mode 100644
index 0000000000000000000000000000000000000000..5a13c5c16e28f88cd93422019edea27f1282ccaf
--- /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 0000000000000000000000000000000000000000..c2a2e68d5fbb9421240321b6755d4d2d041b0b5c
--- /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;
diff --git a/src/lib/audio/effects/robot-effect.js b/src/lib/audio/effects/robot-effect.js
index 472668d531e4344be34a7fcf100d3448f46f090c..653276eabfc616c60513e584a0f7def203382e84 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 bffc422acbc9f1ea12bec768bc56539eb1c72085..0cfcd64152c151ce156e9a1a594cdb11550b5ca5 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);
diff --git a/test/__mocks__/audio-effects.js b/test/__mocks__/audio-effects.js
index b8a4c6994b8e86f7859730464072b3eb09040bce..06c36c7e84359a3bac2b4431f21900ff15a3c0f8 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/integration/sounds.test.js b/test/integration/sounds.test.js
index 29ffba49fb93b43a563e05c68cf259511a9a1bef..e564f511a3133d80e1eeb99320764e5d55918835 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,
@@ -55,7 +56,6 @@ describe('Working with sounds', () => {
         await clickText('Faster');
         await clickText('Slower');
         await clickText('Robot');
-        await clickText('Echo');
         await clickText('Reverse');
 
         const logs = await getLogs();
@@ -112,7 +112,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')
@@ -132,4 +132,56 @@ 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([]);
+    });
+
+    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([]);
+    });
+
+    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([]);
+    });
 });
diff --git a/test/unit/components/__snapshots__/sound-editor.test.jsx.snap b/test/unit/components/__snapshots__/sound-editor.test.jsx.snap
index 38ddf76c4058ac8e0723f4dacbc1761111fa4599..af7a255a407d5e6f1aac3f13293d38b83b1c9519 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`] = `
 <div
   className={undefined}
+  onMouseDown={undefined}
 >
   <div
     className={undefined}
@@ -59,7 +60,59 @@ exports[`Sound Editor Component matches snapshot 1`] = `
       </div>
     </div>
     <div
-      className="undefined"
+      className={undefined}
+    >
+      <div
+        className=""
+        onClick={undefined}
+        role="button"
+      >
+        <img
+          className={undefined}
+          draggable={false}
+          src="test-file-stub"
+        />
+        <div
+          className={undefined}
+        >
+          Copy
+        </div>
+      </div>
+      <div
+        className=""
+        onClick={undefined}
+        role="button"
+      >
+        <img
+          className={undefined}
+          draggable={false}
+          src="test-file-stub"
+        />
+        <div
+          className={undefined}
+        >
+          Paste
+        </div>
+      </div>
+      <div
+        className=""
+        onClick={undefined}
+        role="button"
+      >
+        <img
+          className={undefined}
+          draggable={false}
+          src="test-file-stub"
+        />
+        <div
+          className={undefined}
+        >
+          Copy to New
+        </div>
+      </div>
+    </div>
+    <div
+      className=""
       onClick={[Function]}
       role="button"
     >
@@ -71,9 +124,7 @@ exports[`Sound Editor Component matches snapshot 1`] = `
       <div
         className={undefined}
       >
-        <span>
-          Save
-        </span>
+        Delete
       </div>
     </div>
   </div>
@@ -85,33 +136,26 @@ exports[`Sound Editor Component matches snapshot 1`] = `
     >
       <svg
         className={undefined}
-        viewBox="-1 0 600 160"
+        viewBox="0 0 600 160"
       >
-        <line
-          className={undefined}
-          x1={-1}
-          x2={600}
-          y1={80}
-          y2={80}
-        />
         <g
           transform="scale(1, -1) translate(0, -80)"
         >
           <path
             className={undefined}
-            d="M0 0Q0 0 60 40 Q120 80 180 120 Q240 160 300 200 Q360 240 420 120 Q480 0 480 0 Q480 0 420 -120 Q360 -240 300 -200 Q240 -160 180 -120 Q120 -80 60 -40 Q0 0 0 0Z"
+            d="M0 0Q0 80 150 120 Q300 160 450 200 Q600 240 600 0 Q600 -240 450 -200 Q300 -160 150 -120 Q0 -80 0 0Z"
             strokeLinejoin="round"
             strokeWidth={1}
           />
         </g>
       </svg>
       <div
-        className={undefined}
+        className=""
+        onMouseDown={[Function]}
+        onTouchStart={[Function]}
       >
         <div
           className=""
-          onMouseDown={[Function]}
-          onTouchStart={[Function]}
           style={
             Object {
               "alignContent": undefined,
@@ -124,7 +168,8 @@ exports[`Sound Editor Component matches snapshot 1`] = `
               "flexWrap": undefined,
               "height": undefined,
               "justifyContent": undefined,
-              "width": "20%",
+              "left": "20%",
+              "width": "60.00000000000001%",
             }
           }
         >
@@ -148,6 +193,8 @@ exports[`Sound Editor Component matches snapshot 1`] = `
           />
           <div
             className=""
+            onMouseDown={[Function]}
+            onTouchStart={[Function]}
             style={
               Object {
                 "alignContent": undefined,
@@ -209,60 +256,10 @@ exports[`Sound Editor Component matches snapshot 1`] = `
               />
             </div>
           </div>
-        </div>
-        <div
-          className={undefined}
-        >
-          <div
-            className=""
-            style={
-              Object {
-                "transform": "translateX(50%)",
-              }
-            }
-          />
-        </div>
-        <div
-          className=""
-          onMouseDown={[Function]}
-          onTouchStart={[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=""
+            onMouseDown={[Function]}
+            onTouchStart={[Function]}
             style={
               Object {
                 "alignContent": undefined,
@@ -325,6 +322,18 @@ exports[`Sound Editor Component matches snapshot 1`] = `
             </div>
           </div>
         </div>
+        <div
+          className={undefined}
+        >
+          <div
+            className=""
+            style={
+              Object {
+                "transform": "translateX(50%)",
+              }
+            }
+          />
+        </div>
       </div>
     </div>
   </div>
@@ -395,7 +404,7 @@ exports[`Sound Editor Component matches snapshot 1`] = `
         className={undefined}
       >
         <span>
-          Echo
+          Louder
         </span>
       </div>
     </div>
@@ -413,13 +422,13 @@ exports[`Sound Editor Component matches snapshot 1`] = `
         className={undefined}
       >
         <span>
-          Robot
+          Softer
         </span>
       </div>
     </div>
     <div
       className=""
-      onClick={[Function]}
+      onClick={undefined}
       role="button"
     >
       <img
@@ -431,13 +440,13 @@ exports[`Sound Editor Component matches snapshot 1`] = `
         className={undefined}
       >
         <span>
-          Louder
+          Mute
         </span>
       </div>
     </div>
     <div
       className=""
-      onClick={[Function]}
+      onClick={undefined}
       role="button"
     >
       <img
@@ -449,7 +458,25 @@ exports[`Sound Editor Component matches snapshot 1`] = `
         className={undefined}
       >
         <span>
-          Softer
+          Fade in
+        </span>
+      </div>
+    </div>
+    <div
+      className=""
+      onClick={undefined}
+      role="button"
+    >
+      <img
+        className={undefined}
+        draggable={false}
+        src="test-file-stub"
+      />
+      <div
+        className={undefined}
+      >
+        <span>
+          Fade out
         </span>
       </div>
     </div>
@@ -471,6 +498,24 @@ exports[`Sound Editor Component matches snapshot 1`] = `
         </span>
       </div>
     </div>
+    <div
+      className=""
+      onClick={[Function]}
+      role="button"
+    >
+      <img
+        className={undefined}
+        draggable={false}
+        src="test-file-stub"
+      />
+      <div
+        className={undefined}
+      >
+        <span>
+          Robot
+        </span>
+      </div>
+    </div>
   </div>
 </div>
 `;
diff --git a/test/unit/components/sound-editor.test.jsx b/test/unit/components/sound-editor.test.jsx
index efb153b1ffb8fa6ac93a0dd054d1f072f882b511..4b9d3805f1d966ba076882252d31086704b136c4 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(
-            <SoundEditor
-                {...props}
-                trimEnd={null}
-                trimStart={null}
-            />
-        );
-        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(
             <SoundEditor
                 {...props}
@@ -56,8 +44,8 @@ describe('Sound Editor Component', () => {
                 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', () => {
@@ -100,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();
 
diff --git a/test/unit/containers/sound-editor.test.jsx b/test/unit/containers/sound-editor.test.jsx
index 6070b7592c914052e9bbc666d83ca6cebaf737cd..b0a1a8399f64133dfa0de9328593165289b59a06 100644
--- a/test/unit/containers/sound-editor.test.jsx
+++ b/test/unit/containers/sound-editor.test.jsx
@@ -85,29 +85,6 @@ describe('Sound Editor Container', () => {
         expect(component.props().playhead).toEqual(null);
     });
 
-    test('it sets the component props for trimming and submits to the vm', () => {
-        const wrapper = mountWithIntl(
-            <SoundEditor
-                soundIndex={soundIndex}
-                store={store}
-            />
-        );
-        let component = wrapper.find(SoundEditorComponent);
-
-        component.props().onActivateTrim();
-        wrapper.update();
-        component = wrapper.find(SoundEditorComponent);
-        expect(component.props().trimStart).not.toEqual(null);
-        expect(component.props().trimEnd).not.toEqual(null);
-
-        component.props().onActivateTrim();
-        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(
             <SoundEditor
@@ -238,8 +215,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().onFaster();
+        mockAudioEffects.instance._finishProcessing(soundBuffer);
         wrapper.update();
         component = wrapper.find(SoundEditorComponent);
         expect(component.prop('canUndo')).toEqual(true);
@@ -264,8 +241,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().onFaster();
+        mockAudioEffects.instance._finishProcessing(soundBuffer);
 
         wrapper.update();
         component = wrapper.find(SoundEditorComponent);
@@ -282,8 +259,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().onFaster();
+        mockAudioEffects.instance._finishProcessing(soundBuffer);
         wrapper.update();
         component = wrapper.find(SoundEditorComponent);
 
diff --git a/test/unit/util/audio-effects.test.js b/test/unit/util/audio-effects.test.js
index d1057de53777355a9d9f9a7dfbf4babf95c3e63d..330fe12a18f5a3702f234af479d2176908cf6f89 100644
--- a/test/unit/util/audio-effects.test.js
+++ b/test/unit/util/audio-effects.test.js
@@ -14,25 +14,58 @@ 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);
     });
 
+    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
     });
+
+    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', () => {
@@ -43,15 +76,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, 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);
     });