diff --git a/src/components/buffered-input/buffered-input.jsx b/src/components/buffered-input/buffered-input.jsx
deleted file mode 100644
index 13645956ea5ad1db4fb5e7b10dba5baf1bcd7736..0000000000000000000000000000000000000000
--- a/src/components/buffered-input/buffered-input.jsx
+++ /dev/null
@@ -1,52 +0,0 @@
-import bindAll from 'lodash.bindall';
-import PropTypes from 'prop-types';
-import React from 'react';
-
-class BufferedInput extends React.Component {
-    constructor (props) {
-        super(props);
-        bindAll(this, [
-            'handleChange',
-            'handleKeyPress',
-            'handleFlush'
-        ]);
-        this.state = {
-            value: null
-        };
-    }
-    handleKeyPress (e) {
-        if (e.key === 'Enter') {
-            this.handleFlush();
-            e.target.blur();
-        }
-    }
-    handleFlush () {
-        const isNumeric = typeof this.props.value === 'number';
-        const validatesNumeric = isNumeric ? !isNaN(this.state.value) : true;
-        if (this.state.value !== null && validatesNumeric) {
-            this.props.onSubmit(isNumeric ? Number(this.state.value) : this.state.value);
-        }
-        this.setState({value: null});
-    }
-    handleChange (e) {
-        this.setState({value: e.target.value});
-    }
-    render () {
-        const bufferedValue = this.state.value === null ? this.props.value : this.state.value;
-        return (
-            <input
-                {...this.props}
-                value={bufferedValue}
-                onBlur={this.handleFlush}
-                onChange={this.handleChange}
-                onKeyPress={this.handleKeyPress}
-            />
-        );
-    }
-}
-
-BufferedInput.propTypes = {
-    onSubmit: PropTypes.func.isRequired,
-    value: PropTypes.oneOfType([PropTypes.string, PropTypes.number])
-};
-export default BufferedInput;
diff --git a/src/components/forms/buffered-input-hoc.jsx b/src/components/forms/buffered-input-hoc.jsx
new file mode 100644
index 0000000000000000000000000000000000000000..246dfc03e876c3a357dedc3d09233c859c4ba37a
--- /dev/null
+++ b/src/components/forms/buffered-input-hoc.jsx
@@ -0,0 +1,60 @@
+import bindAll from 'lodash.bindall';
+import PropTypes from 'prop-types';
+import React from 'react';
+
+/**
+ * Higher Order Component to manage inputs that submit on blur and <enter>
+ * @param {React.Component} Input text input that consumes onChange, onBlur, onKeyPress
+ * @returns {React.Component} Buffered input that calls onSubmit on blur and <enter>
+ */
+export default function (Input) {
+    class BufferedInput extends React.Component {
+        constructor (props) {
+            super(props);
+            bindAll(this, [
+                'handleChange',
+                'handleKeyPress',
+                'handleFlush'
+            ]);
+            this.state = {
+                value: null
+            };
+        }
+        handleKeyPress (e) {
+            if (e.key === 'Enter') {
+                this.handleFlush();
+                e.target.blur();
+            }
+        }
+        handleFlush () {
+            const isNumeric = typeof this.props.value === 'number';
+            const validatesNumeric = isNumeric ? !isNaN(this.state.value) : true;
+            if (this.state.value !== null && validatesNumeric) {
+                this.props.onSubmit(isNumeric ? Number(this.state.value) : this.state.value);
+            }
+            this.setState({value: null});
+        }
+        handleChange (e) {
+            this.setState({value: e.target.value});
+        }
+        render () {
+            const bufferedValue = this.state.value === null ? this.props.value : this.state.value;
+            return (
+                <Input
+                    {...this.props}
+                    value={bufferedValue}
+                    onBlur={this.handleFlush}
+                    onChange={this.handleChange}
+                    onKeyPress={this.handleKeyPress}
+                />
+            );
+        }
+    }
+
+    BufferedInput.propTypes = {
+        onSubmit: PropTypes.func.isRequired,
+        value: PropTypes.oneOfType([PropTypes.string, PropTypes.number])
+    };
+
+    return BufferedInput;
+}
diff --git a/src/components/forms/input.css b/src/components/forms/input.css
new file mode 100644
index 0000000000000000000000000000000000000000..6747e2c215a38356104d977b29b9e6008f8b42f8
--- /dev/null
+++ b/src/components/forms/input.css
@@ -0,0 +1,40 @@
+@import "../../css/units.css";
+@import "../../css/colors.css";
+
+.input-form {
+    padding: $space 0.75rem;
+
+    font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
+    font-size: 0.625rem;
+    font-weight: bold;
+
+    border-width: 1px;
+    border-style: solid;
+    border-color: $form-border;
+    border-radius: 2rem;
+
+    outline: none;
+    cursor: text;
+    transition: 0.25s ease-out; /* @todo: standardize with var */
+    box-shadow: none;
+
+    /*
+        For truncating overflowing text gracefully
+        Min-width is for a bug: https://css-tricks.com/flexbox-truncated-text
+        @todo: move this out into a mixin or a helper component
+    */
+    overflow: hidden;
+    text-overflow: ellipsis;
+    white-space: nowrap;
+    min-width: 0;
+}
+
+.input-form:focus {
+    border-color: #4c97ff;
+    box-shadow: inset 0 0 0 -2px rgba(0, 0, 0, 0.1);
+}
+
+.input-small {
+    width: 3.5rem; 
+    text-align: center;
+}
diff --git a/src/components/forms/input.jsx b/src/components/forms/input.jsx
new file mode 100644
index 0000000000000000000000000000000000000000..6f9cbffa89fe93575639c4c0a035ad397bf5ad39
--- /dev/null
+++ b/src/components/forms/input.jsx
@@ -0,0 +1,27 @@
+import PropTypes from 'prop-types';
+import React from 'react';
+import classNames from 'classnames';
+
+import styles from './input.css';
+
+const Input = props => {
+    const {small, ...componentProps} = props;
+    return (
+        <input
+            {...componentProps}
+            className={classNames(styles.inputForm, {
+                [styles.inputSmall]: small
+            })}
+        />
+    );
+};
+
+Input.propTypes = {
+    small: PropTypes.bool
+};
+
+Input.defaultProps = {
+    small: false
+};
+
+export default Input;
diff --git a/src/components/forms/label.css b/src/components/forms/label.css
new file mode 100644
index 0000000000000000000000000000000000000000..5eb280b87368ee92a4e722356b7d6490e6e8cddf
--- /dev/null
+++ b/src/components/forms/label.css
@@ -0,0 +1,19 @@
+@import "../../css/units.css";
+@import "../../css/colors.css";
+
+.input-group {
+    display: inline-flex;
+    flex-direction: row;
+    align-items: center;
+}
+
+.input-label, .input-label-secondary {
+    font-size: 0.625rem;
+    margin-right: calc($space / 2);
+    user-select: none;
+    cursor: default;
+}
+
+.input-label {
+    font-weight: bold;
+}
diff --git a/src/components/forms/label.jsx b/src/components/forms/label.jsx
new file mode 100644
index 0000000000000000000000000000000000000000..62f077a746fb5bf6d70c828fa7f34097f89054ac
--- /dev/null
+++ b/src/components/forms/label.jsx
@@ -0,0 +1,25 @@
+import PropTypes from 'prop-types';
+import React from 'react';
+
+import styles from './label.css';
+
+const Label = props => (
+    <label className={styles.inputGroup}>
+        <span className={props.secondary ? styles.inputLabelSecondary : styles.inputLabel}>
+            {props.text}
+        </span>
+        {props.children}
+    </label>
+);
+
+Label.propTypes = {
+    children: PropTypes.node,
+    secondary: PropTypes.bool,
+    text: PropTypes.string.isRequired
+};
+
+Label.defaultProps = {
+    secondary: false
+};
+
+export default Label;
diff --git a/src/components/record-modal/record-modal.jsx b/src/components/record-modal/record-modal.jsx
index b215ad17f9e2ef866ff4893c49de4ac4210f25a2..7cdf2efa5d64b7c3c750e57cc1c4538194397f21 100644
--- a/src/components/record-modal/record-modal.jsx
+++ b/src/components/record-modal/record-modal.jsx
@@ -20,6 +20,7 @@ const RecordModal = props => (
                     levels={props.levels}
                     playhead={props.playhead}
                     playing={props.playing}
+                    sampleRate={props.sampleRate}
                     samples={props.samples}
                     trimEnd={props.trimEnd}
                     trimStart={props.trimStart}
@@ -58,6 +59,7 @@ RecordModal.propTypes = {
     playhead: PropTypes.number,
     playing: PropTypes.bool,
     recording: PropTypes.bool,
+    sampleRate: PropTypes.number,
     samples: PropTypes.instanceOf(Float32Array),
     trimEnd: PropTypes.number.isRequired,
     trimStart: PropTypes.number.isRequired
diff --git a/src/components/sound-editor/sound-editor.css b/src/components/sound-editor/sound-editor.css
new file mode 100644
index 0000000000000000000000000000000000000000..0bb44f115c65dfabf9e3e899e40aca4b0322fb68
--- /dev/null
+++ b/src/components/sound-editor/sound-editor.css
@@ -0,0 +1,52 @@
+@import "../../css/colors.css";
+@import "../../css/units.css";
+
+.editor-container {
+    display: flex;
+    flex-direction: column;
+    padding: calc(2 * $space);
+}
+
+.row {
+    display: flex;
+    flex-direction: row;
+    align-items: center;
+}
+
+.row + .row {
+    margin-top: calc(2 * $space);
+}
+
+.input-group + .input-group {
+    margin-left: calc(2 * $space);
+}
+
+.waveform-container {
+    display: flex;
+    justify-content: space-around;
+    align-items: center;
+    width: 100%;
+
+    position: relative;
+
+    background: $ui-pane-gray;
+    border: 1px solid $ui-pane-border;
+    border-radius: 5px;
+    padding: 3px;
+}
+
+.button {
+    width: 2rem;
+    height: 2rem;
+    padding: 0.25rem;
+    outline: none;
+    background: white;
+    border-radius: 0.25rem;
+    border: 1px solid #ddd;
+}
+
+.button img {
+    flex-grow: 1;
+    max-width: 100%;
+    max-height: 100%;
+}
diff --git a/src/components/sound-editor/sound-editor.jsx b/src/components/sound-editor/sound-editor.jsx
new file mode 100644
index 0000000000000000000000000000000000000000..3e379b3f5372e57c977438246a8690768eac66d9
--- /dev/null
+++ b/src/components/sound-editor/sound-editor.jsx
@@ -0,0 +1,69 @@
+import PropTypes from 'prop-types';
+import React from 'react';
+import classNames from 'classnames';
+import Box from '../box/box.jsx';
+import Waveform from '../waveform/waveform.jsx';
+import Label from '../forms/label.jsx';
+import Input from '../forms/input.jsx';
+import BufferedInputHOC from '../forms/buffered-input-hoc.jsx';
+
+import styles from './sound-editor.css';
+
+import playIcon from '../record-modal/icon--play.svg';
+import stopIcon from '../record-modal/icon--stop-playback.svg';
+
+const BufferedInput = BufferedInputHOC(Input);
+
+const SoundEditor = props => (
+    <Box className={styles.editorContainer}>
+        <Box className={styles.row}>
+            <Box className={styles.inputGroup}>
+                {props.playhead ? (
+                    <button
+                        className={classNames(styles.button, styles.stopButtonn)}
+                        onClick={props.onStop}
+                    >
+                        <img src={stopIcon} />
+                    </button>
+                ) : (
+                    <button
+                        className={classNames(styles.button, styles.playButton)}
+                        onClick={props.onPlay}
+                    >
+                        <img src={playIcon} />
+                    </button>
+                )}
+            </Box>
+            <Box className={styles.inputGroup}>
+                <Label text="Sound">
+                    <BufferedInput
+                        tabIndex="1"
+                        type="text"
+                        value={props.name}
+                        onSubmit={props.onChangeName}
+                    />
+                </Label>
+            </Box>
+        </Box>
+        <Box className={styles.row}>
+            <Box className={styles.waveformContainer}>
+                <Waveform
+                    data={props.chunkLevels}
+                    height={180}
+                    width={600}
+                />
+            </Box>
+        </Box>
+    </Box>
+);
+
+SoundEditor.propTypes = {
+    chunkLevels: PropTypes.arrayOf(PropTypes.number).isRequired,
+    name: PropTypes.string.isRequired,
+    onChangeName: PropTypes.func.isRequired,
+    onPlay: PropTypes.func.isRequired,
+    onStop: PropTypes.func.isRequired,
+    playhead: PropTypes.number
+};
+
+export default SoundEditor;
diff --git a/src/components/sprite-info/sprite-info.css b/src/components/sprite-info/sprite-info.css
index eaf5c3fa8798c168bbc1e8b8274f923cee17fbfe..e6a758d1c44936e7f3a35a9faa7a2cda6ee6a2a8 100644
--- a/src/components/sprite-info/sprite-info.css
+++ b/src/components/sprite-info/sprite-info.css
@@ -1,7 +1,5 @@
 @import "../../css/units.css";
-
-$form-border: #E9EEF2;
-$form-radius: calc($space / 2);
+@import "../../css/colors.css";
 
 .sprite-info {
     height: $sprite-info-height;
@@ -74,60 +72,6 @@ $form-radius: calc($space / 2);
     pointer-events: none;
 }
 
-.input-label {
-    font-size: 0.625rem;
-    font-weight: bold;
-    margin-right: calc($space / 2);
-
-    /* @todo: make this a mixin for all UI text labels */
-    user-select: none;
-    cursor: default;
-}
-
-.input-label-secondary {
-    font-size: 0.625rem;
-    margin-right: calc($space / 2);
-
-    /* @todo: make this a mixin for all UI text labels */
-    user-select: none;
-    cursor: default;
-}
-
-.input-form {
-    padding: $space 0.75rem;
-
-    font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
-    font-size: 0.625rem;
-    font-weight: bold;
-
-    border-width: 1px;
-    border-style: solid;
-    border-color: $form-border;
-    border-radius: 2rem;
-
-    outline: none;
-    cursor: text;
-    transition: 0.25s ease-out; /* @todo: standardize with var */
-    box-shadow: none;
-
-    /*
-        For truncating overflowing text gracefully
-        Min-width is for a bug: https://css-tricks.com/flexbox-truncated-text
-        @todo: move this out into a mixin or a helper component
-    */
-    overflow: hidden;
-    text-overflow: ellipsis;
-    white-space: nowrap;
-    min-width: 0;
-}
-
-.input-form:focus {
-    border-color: #4c97ff;
-    box-shadow: inset 0 0 0 -2px rgba(0, 0, 0, 0.1);
-}
-
-.x, .y, .direction { width: 3.5rem; text-align: center; }
-
 .rotation-select {
     width: 100%;
     height: 1.85rem;
@@ -135,5 +79,3 @@ $form-radius: calc($space / 2);
     user-select: none;
     outline: none;
 }
-
-.sprite-name { width: 100%; }
diff --git a/src/components/sprite-info/sprite-info.jsx b/src/components/sprite-info/sprite-info.jsx
index 73c550c2c5e4169a1f7331ba1125b552cf47c888..fc68c9121a44f6d587b46a15d1654e372b4688f4 100644
--- a/src/components/sprite-info/sprite-info.jsx
+++ b/src/components/sprite-info/sprite-info.jsx
@@ -3,7 +3,10 @@ import PropTypes from 'prop-types';
 import React from 'react';
 
 import Box from '../box/box.jsx';
-import BufferedInput from '../buffered-input/buffered-input.jsx';
+import Label from '../forms/label.jsx';
+import Input from '../forms/input.jsx';
+import BufferedInputHOC from '../forms/buffered-input-hoc.jsx';
+
 import styles from './sprite-info.css';
 
 import xIcon from './icon--x.svg';
@@ -11,6 +14,7 @@ import yIcon from './icon--y.svg';
 import showIcon from './icon--show.svg';
 import hideIcon from './icon--hide.svg';
 
+const BufferedInput = BufferedInputHOC(Input);
 const ROTATION_STYLES = ['left-right', 'don\'t rotate', 'all around'];
 
 class SpriteInfo extends React.Component {
@@ -32,16 +36,16 @@ class SpriteInfo extends React.Component {
             >
                 <div className={classNames(styles.row, styles.rowPrimary)}>
                     <div className={styles.group}>
-                        <span className={styles.inputLabel}>Sprite</span>
-                        <BufferedInput
-                            className={classNames(styles.inputForm, styles.spriteName)}
-                            disabled={this.props.disabled}
-                            placeholder="Name"
-                            tabIndex="1"
-                            type="text"
-                            value={this.props.disabled ? '' : this.props.name}
-                            onSubmit={this.props.onChangeName}
-                        />
+                        <Label text="Sprite">
+                            <BufferedInput
+                                disabled={this.props.disabled}
+                                placeholder="Name"
+                                tabIndex="1"
+                                type="text"
+                                value={this.props.disabled ? '' : this.props.name}
+                                onSubmit={this.props.onChangeName}
+                            />
+                        </Label>
                     </div>
 
                     <div className={styles.group}>
@@ -51,16 +55,17 @@ class SpriteInfo extends React.Component {
                                 src={xIcon}
                             />
                         </div>
-                        <span className={styles.inputLabel}>x</span>
-                        <BufferedInput
-                            className={classNames(styles.inputForm, styles.x)}
-                            disabled={this.props.disabled}
-                            placeholder="x"
-                            tabIndex="2"
-                            type="text"
-                            value={this.props.disabled ? '' : this.props.x}
-                            onSubmit={this.props.onChangeX}
-                        />
+                        <Label text="x">
+                            <BufferedInput
+                                small
+                                disabled={this.props.disabled}
+                                placeholder="x"
+                                tabIndex="2"
+                                type="text"
+                                value={this.props.disabled ? '' : this.props.x}
+                                onSubmit={this.props.onChangeX}
+                            />
+                        </Label>
                     </div>
 
                     <div className={styles.group}>
@@ -70,93 +75,103 @@ class SpriteInfo extends React.Component {
                                 src={yIcon}
                             />
                         </div>
-                        <span className={styles.inputLabel}>y</span>
-                        <BufferedInput
-                            className={classNames(styles.inputForm, styles.y)}
-                            disabled={this.props.disabled}
-                            placeholder="y"
-                            tabIndex="3"
-                            type="text"
-                            value={this.props.disabled ? '' : this.props.y}
-                            onSubmit={this.props.onChangeY}
-                        />
+                        <Label text="y">
+                            <BufferedInput
+                                small
+                                disabled={this.props.disabled}
+                                placeholder="y"
+                                tabIndex="3"
+                                type="text"
+                                value={this.props.disabled ? '' : this.props.y}
+                                onSubmit={this.props.onChangeY}
+                            />
+                        </Label>
                     </div>
                 </div>
 
                 <div className={classNames(styles.row, styles.rowSecondary)}>
                     <div className={styles.group}>
-                        <span className={styles.inputLabelSecondary}>
-                            Show
-                        </span>
-                        <div>
-                            <div
-                                className={classNames(
-                                    styles.radio,
-                                    styles.radioLeft,
-                                    styles.iconWrapper,
-                                    {
-                                        [styles.isActive]: this.props.visible && !this.props.disabled,
-                                        [styles.isDisabled]: this.props.disabled
-                                    }
-                                )}
-                                tabIndex="4"
-                                onClick={this.props.onClickVisible}
-                            >
-                                <img
-                                    className={styles.icon}
-                                    src={showIcon}
-                                />
-                            </div>
-                            <div
-                                className={classNames(
-                                    styles.radio,
-                                    styles.radioRight,
-                                    styles.iconWrapper,
-                                    {
-                                        [styles.isActive]: !this.props.visible && !this.props.disabled,
-                                        [styles.isDisabled]: this.props.disabled
-                                    }
-                                )}
-                                tabIndex="4"
-                                onClick={this.props.onClickNotVisible}
-                            >
-                                <img
-                                    className={styles.icon}
-                                    src={hideIcon}
-                                />
+                        <Label
+                            secondary
+                            text="Show"
+                        >
+                            <div>
+                                <div
+                                    className={classNames(
+                                        styles.radio,
+                                        styles.radioLeft,
+                                        styles.iconWrapper,
+                                        {
+                                            [styles.isActive]: this.props.visible && !this.props.disabled,
+                                            [styles.isDisabled]: this.props.disabled
+                                        }
+                                    )}
+                                    tabIndex="4"
+                                    onClick={this.props.onClickVisible}
+                                >
+                                    <img
+                                        className={styles.icon}
+                                        src={showIcon}
+                                    />
+                                </div>
+                                <div
+                                    className={classNames(
+                                        styles.radio,
+                                        styles.radioRight,
+                                        styles.iconWrapper,
+                                        {
+                                            [styles.isActive]: !this.props.visible && !this.props.disabled,
+                                            [styles.isDisabled]: this.props.disabled
+                                        }
+                                    )}
+                                    tabIndex="4"
+                                    onClick={this.props.onClickNotVisible}
+                                >
+                                    <img
+                                        className={styles.icon}
+                                        src={hideIcon}
+                                    />
+                                </div>
                             </div>
-                        </div>
+                        </Label>
                     </div>
                     <div className={styles.group}>
-                        <span className={styles.inputLabelSecondary}>Direction</span>
-                        <BufferedInput
-                            className={classNames(styles.inputForm, styles.direction)}
-                            disabled={this.props.disabled}
-                            tabIndex="5"
-                            type="text"
-                            value={this.props.disabled ? '' : this.props.direction}
-                            onSubmit={this.props.onChangeDirection}
-                        />
+                        <Label
+                            secondary
+                            text="Direction"
+                        >
+                            <BufferedInput
+                                small
+                                disabled={this.props.disabled}
+                                label="Direction"
+                                tabIndex="5"
+                                type="text"
+                                value={this.props.disabled ? '' : this.props.direction}
+                                onSubmit={this.props.onChangeDirection}
+                            />
+                        </Label>
                     </div>
                     <div className={styles.group}>
-                        <span className={styles.inputLabelSecondary}>
-                            Rotation
-                        </span>
-                        <select
-                            className={classNames(styles.selectForm, styles.rotationSelect)}
-                            disabled={this.props.disabled}
-                            value={this.props.rotationStyle}
-                            onChange={this.props.onChangeRotationStyle}
+                        <Label
+                            secondary
+                            text="Rotation"
                         >
-                            {ROTATION_STYLES.map(style => (
-                                <option
-                                    key={style}
-                                    value={style}
-                                >
-                                    {style}
-                                </option>
-                            ))}
-                        </select>
+                            <select
+                                className={classNames(styles.selectForm, styles.rotationSelect)}
+                                disabled={this.props.disabled}
+                                value={this.props.rotationStyle}
+                                onChange={this.props.onChangeRotationStyle}
+                            >
+                                {ROTATION_STYLES.map(style => (
+                                    <option
+                                        key={style}
+                                        value={style}
+                                    >
+                                        {style}
+                                    </option>
+                                ))}
+                            </select>
+                        </Label>
                     </div>
                 </div>
             </Box>
diff --git a/src/containers/playback-step.jsx b/src/containers/playback-step.jsx
index ce61bcc045a0edc011c955fdd74f5ff42bc29f5d..8c2f5e7333be319b1008d1d3a52ccb3c2c6fe684 100644
--- a/src/containers/playback-step.jsx
+++ b/src/containers/playback-step.jsx
@@ -13,7 +13,7 @@ class PlaybackStep extends React.Component {
         ]);
     }
     componentDidMount () {
-        this.audioBufferPlayer = new AudioBufferPlayer(this.props.samples);
+        this.audioBufferPlayer = new AudioBufferPlayer(this.props.samples, this.props.sampleRate);
     }
     componentWillUnmount () {
         this.audioBufferPlayer.stop();
@@ -33,6 +33,7 @@ class PlaybackStep extends React.Component {
     }
     render () {
         const {
+            sampleRate, // eslint-disable-line no-unused-vars
             onPlay, // eslint-disable-line no-unused-vars
             onStopPlaying, // eslint-disable-line no-unused-vars
             onSetPlayhead, // eslint-disable-line no-unused-vars
@@ -49,6 +50,7 @@ class PlaybackStep extends React.Component {
 }
 
 PlaybackStep.propTypes = {
+    sampleRate: PropTypes.number.isRequired,
     samples: PropTypes.instanceOf(Float32Array).isRequired,
     ...PlaybackStepComponent.propTypes
 };
diff --git a/src/containers/record-modal.jsx b/src/containers/record-modal.jsx
index 2f7cff84cba3047715fcda794610be2bb4017537..fd5647c4967722b94806dcf93b5e3d16e5ff928c 100644
--- a/src/containers/record-modal.jsx
+++ b/src/containers/record-modal.jsx
@@ -105,6 +105,7 @@ class RecordModal extends React.Component {
                 playhead={this.state.playhead}
                 playing={this.state.playing}
                 recording={this.state.recording}
+                sampleRate={this.state.sampleRate}
                 samples={this.state.samples}
                 trimEnd={this.state.trimEnd}
                 trimStart={this.state.trimStart}
diff --git a/src/containers/sound-editor.jsx b/src/containers/sound-editor.jsx
new file mode 100644
index 0000000000000000000000000000000000000000..30bd62d843fdf66800f4528c68078ec7ef7e15b2
--- /dev/null
+++ b/src/containers/sound-editor.jsx
@@ -0,0 +1,100 @@
+import bindAll from 'lodash.bindall';
+import PropTypes from 'prop-types';
+import React from 'react';
+
+import {connect} from 'react-redux';
+
+import {computeChunkedRMS} from '../lib/audio/audio-util.js';
+
+import SoundEditorComponent from '../components/sound-editor/sound-editor.jsx';
+import AudioBufferPlayer from '../lib/audio/audio-buffer-player.js';
+
+class SoundEditor extends React.Component {
+    constructor (props) {
+        super(props);
+        bindAll(this, [
+            'handleStoppedPlaying',
+            'handleChangeName',
+            'handlePlay',
+            'handleStopPlaying',
+            'handleUpdatePlayhead'
+        ]);
+        this.state = {
+            playhead: null, // null is not playing, [0 -> 1] is playing percent
+            chunkLevels: computeChunkedRMS(this.props.samples)
+        };
+    }
+    componentDidMount () {
+        this.audioBufferPlayer = new AudioBufferPlayer(this.props.samples, this.props.sampleRate);
+    }
+    componentWillReceiveProps (newProps) {
+        if (newProps.soundIndex !== this.props.soundIndex) {
+            this.audioBufferPlayer.stop();
+            this.audioBufferPlayer = new AudioBufferPlayer(newProps.samples, newProps.sampleRate);
+            this.setState({chunkLevels: computeChunkedRMS(newProps.samples)});
+        }
+    }
+    componentWillUnmount () {
+        this.audioBufferPlayer.stop();
+    }
+    handlePlay () {
+        this.audioBufferPlayer.play(
+            0,
+            1,
+            this.handleUpdatePlayhead,
+            this.handleStoppedPlaying);
+    }
+    handleStopPlaying () {
+        this.audioBufferPlayer.stop();
+        this.handleStoppedPlaying();
+    }
+    handleStoppedPlaying () {
+        this.setState({playhead: null});
+    }
+    handleUpdatePlayhead (playhead) {
+        this.setState({playhead});
+    }
+    handleChangeName (name) {
+        this.props.onRenameSound(this.props.soundIndex, name);
+    }
+    render () {
+        return (
+            <SoundEditorComponent
+                chunkLevels={this.state.chunkLevels}
+                name={this.props.name}
+                playhead={this.state.playhead}
+                trimEnd={this.state.trimEnd}
+                trimStart={this.state.trimStart}
+                onChangeName={this.handleChangeName}
+                onPlay={this.handlePlay}
+                onSetTrimEnd={this.handleUpdateTrimEnd}
+                onSetTrimStart={this.handleUpdateTrimStart}
+                onStop={this.handleStopPlaying}
+                onTrim={this.handleActivateTrim}
+            />
+        );
+    }
+}
+
+SoundEditor.propTypes = {
+    name: PropTypes.string.isRequired,
+    onRenameSound: PropTypes.func.isRequired,
+    sampleRate: PropTypes.number,
+    samples: PropTypes.instanceOf(Float32Array),
+    soundIndex: PropTypes.number
+};
+
+const mapStateToProps = (state, {soundIndex}) => {
+    const sound = state.vm.editingTarget.sprite.sounds[soundIndex];
+    const audioBuffer = state.vm.runtime.audioEngine.audioBuffers[sound.md5];
+    return {
+        sampleRate: audioBuffer.sampleRate,
+        samples: audioBuffer.getChannelData(0),
+        name: sound.name,
+        onRenameSound: state.vm.renameSound.bind(state.vm)
+    };
+};
+
+export default connect(
+    mapStateToProps
+)(SoundEditor);
diff --git a/src/containers/sound-tab.jsx b/src/containers/sound-tab.jsx
index 4aa43db974043d0871e48232ae9fa4868972eccf..f95f6f7099e7e36c45edc3c405c80486008a49d2 100644
--- a/src/containers/sound-tab.jsx
+++ b/src/containers/sound-tab.jsx
@@ -8,8 +8,8 @@ import AssetPanel from '../components/asset-panel/asset-panel.jsx';
 import soundIcon from '../components/asset-panel/icon--sound.svg';
 import addSoundFromLibraryIcon from '../components/asset-panel/icon--add-sound-lib.svg';
 import addSoundFromRecordingIcon from '../components/asset-panel/icon--add-sound-record.svg';
-
 import RecordModal from './record-modal.jsx';
+import SoundEditor from './sound-editor.jsx';
 
 import {connect} from 'react-redux';
 
@@ -38,13 +38,11 @@ class SoundTab extends React.Component {
         const target = editingTarget && sprites[editingTarget] ? sprites[editingTarget] : stage;
 
         if (target && target.sounds && this.state.selectedSoundIndex > target.sounds.length - 1) {
-            this.setState({selectedSoundIndex: target.sounds.length - 1});
+            this.setState({selectedSoundIndex: Math.max(target.sounds.length - 1, 0)});
         }
     }
 
     handleSelectSound (soundIndex) {
-        const sound = this.props.vm.editingTarget.sprite.sounds[soundIndex];
-        this.props.vm.editingTarget.audioPlayer.playSound(sound.md5);
         this.setState({selectedSoundIndex: soundIndex});
     }
 
@@ -108,6 +106,9 @@ class SoundTab extends React.Component {
                 onDeleteClick={this.handleDeleteSound}
                 onItemClick={this.handleSelectSound}
             >
+                {target.sounds && target.sounds.length > 0 ? (
+                    <SoundEditor soundIndex={this.state.selectedSoundIndex} />
+                ) : null}
                 {this.props.soundRecorderVisible ? (
                     <RecordModal />
                 ) : null}
diff --git a/src/css/colors.css b/src/css/colors.css
index 036efe3f2843547931b76a2037b781ce82027bc3..45258f13a7243bea9ddadb239654b3afe42473be 100644
--- a/src/css/colors.css
+++ b/src/css/colors.css
@@ -10,3 +10,5 @@ $red-tertiary: #E64D00;
 
 $sound-primary: #CF63CF;
 $sound-tertiary: #A63FA6;
+
+$form-border: #E9EEF2;
diff --git a/src/css/units.css b/src/css/units.css
index 3f9aaba17d8319e322b48c817438ed4e8b92458e..d951e3e1a874a4b34f22483d8d6cdc977d772e32 100644
--- a/src/css/units.css
+++ b/src/css/units.css
@@ -6,4 +6,6 @@ $menu-bar-height: 3rem;
 $sprite-info-height: 5.25rem; /* @todo: SpriteInfo isn't explicitly set to this height yet */
 $stage-menu-height: 3rem;
 
-$library-header-height: 4.375rem;
\ No newline at end of file
+$library-header-height: 4.375rem;
+
+$form-radius: calc($space / 2);
diff --git a/src/lib/audio/audio-buffer-player.js b/src/lib/audio/audio-buffer-player.js
index 5d643c8463d199d6a0cab38371a71f61c780d307..39affdfd338d607aaa16ffa5917e5629dc52a8c2 100644
--- a/src/lib/audio/audio-buffer-player.js
+++ b/src/lib/audio/audio-buffer-player.js
@@ -1,9 +1,9 @@
 import SharedAudioContext from './shared-audio-context.js';
 
 class AudioBufferPlayer {
-    constructor (samples) {
+    constructor (samples, sampleRate) {
         this.audioContext = new SharedAudioContext();
-        this.buffer = this.audioContext.createBuffer(1, samples.length, this.audioContext.sampleRate);
+        this.buffer = this.audioContext.createBuffer(1, samples.length, sampleRate);
         this.buffer.getChannelData(0).set(samples);
         this.source = null;
 
diff --git a/src/lib/audio/audio-recorder.js b/src/lib/audio/audio-recorder.js
index e10ff81ce1c7d48936e4aeb465489b9238db2cf4..576021a2ef01ae606c17dfe329b0d800e978d099 100644
--- a/src/lib/audio/audio-recorder.js
+++ b/src/lib/audio/audio-recorder.js
@@ -1,4 +1,5 @@
 import SharedAudioContext from './shared-audio-context.js';
+import {computeRMS} from './audio-util.js';
 
 class AudioRecorder {
     constructor () {
@@ -42,17 +43,6 @@ class AudioRecorder {
         this.recording = true;
     }
 
-    calculateRMS (samples) {
-        // Calculate RMS, adapted from https://github.com/Tonejs/Tone.js/blob/master/Tone/component/Meter.js#L88
-        const sum = samples.reduce((acc, v) => acc + Math.pow(v, 2), 0);
-        const rms = Math.sqrt(sum / samples.length);
-        // Scale it
-        const unity = 0.35;
-        const val = rms / unity;
-        // Scale the output curve
-        return Math.sqrt(val);
-    }
-
     attachUserMediaStream (userMediaStream, onUpdate) {
         this.userMediaStream = userMediaStream;
         this.mediaStreamSource = this.audioContext.createMediaStreamSource(userMediaStream);
@@ -76,7 +66,7 @@ class AudioRecorder {
             if (this.disposed) return;
             requestAnimationFrame(update);
             this.analyserNode.getFloatTimeDomainData(dataArray);
-            onUpdate(this.calculateRMS(dataArray));
+            onUpdate(computeRMS(dataArray));
         };
 
         requestAnimationFrame(update);
@@ -89,7 +79,7 @@ class AudioRecorder {
     }
 
     stop () {
-        const chunkLevels = this.buffers.map(buffer => this.calculateRMS(buffer));
+        const chunkLevels = this.buffers.map(buffer => computeRMS(buffer));
         const maxRMS = Math.max.apply(null, chunkLevels);
         const threshold = maxRMS / 8;
 
diff --git a/src/lib/audio/audio-util.js b/src/lib/audio/audio-util.js
new file mode 100644
index 0000000000000000000000000000000000000000..a493fc7da73b1069c38c0bee92ac050bdd4eb809
--- /dev/null
+++ b/src/lib/audio/audio-util.js
@@ -0,0 +1,27 @@
+const computeRMS = function (samples, scaling = 0.55) {
+    if (samples.length === 0) return 0;
+    // Calculate RMS, adapted from https://github.com/Tonejs/Tone.js/blob/master/Tone/component/Meter.js#L88
+    let sum = 0;
+    for (let i = 0; i < samples.length; i++) {
+        const sample = samples[i];
+        sum += Math.pow(sample, 2);
+    }
+    const rms = Math.sqrt(sum / samples.length);
+    const val = rms / scaling;
+    return Math.sqrt(val);
+};
+
+const computeChunkedRMS = function (samples, chunkSize = 1024) {
+    const sampleCount = samples.length;
+    const chunkLevels = [];
+    for (let i = 0; i < sampleCount; i += chunkSize) {
+        const maxIndex = Math.min(sampleCount, i + chunkSize);
+        chunkLevels.push(computeRMS(samples.slice(i, maxIndex)));
+    }
+    return chunkLevels;
+};
+
+export {
+    computeRMS,
+    computeChunkedRMS
+};
diff --git a/test/unit/util/audio-util.test.js b/test/unit/util/audio-util.test.js
new file mode 100644
index 0000000000000000000000000000000000000000..714381db64bb61b5ccaa1b37f9265c2ed50c3549
--- /dev/null
+++ b/test/unit/util/audio-util.test.js
@@ -0,0 +1,52 @@
+/* eslint-env jest */
+import {computeRMS, computeChunkedRMS} from '../../../src/lib/audio/audio-util';
+
+describe('computeRMS', () => {
+    test('returns 0 when given no samples', () => {
+        expect(computeRMS([])).toEqual(0);
+    });
+    test('returns the RMS scaled by the given unity value and square rooted', () => {
+        const unity = 0.5;
+        const samples = [3, 2, 1];
+        expect(computeRMS(samples, unity)).toEqual(
+            Math.sqrt(Math.sqrt((3 * 3 + 2 * 2 + 1 * 1) / 3) / 0.5)
+        );
+    });
+    test('uses a default unity value of 0.55', () => {
+        const samples = [1, 1, 1];
+        // raw rms is 1, scaled to (1 / 0.55) and square rooted
+        expect(computeRMS(samples)).toEqual(Math.sqrt(1 / 0.55));
+    });
+});
+
+
+describe('computeChunkedRMS', () => {
+    test('computes the rms for each chunk based on chunk size', () => {
+        const samples = [2, 1, 3, 2, 5];
+        const chunkedLevels = computeChunkedRMS(samples, 2);
+        // chunked to [2, 0], [3, 0], [5]
+        // rms scaled with default unity of 0.55
+        expect(chunkedLevels.length).toEqual(3);
+        expect(chunkedLevels).toEqual([
+            Math.sqrt(Math.sqrt((2 * 2 + 1 * 1) / 2) / 0.55),
+            Math.sqrt(Math.sqrt((3 * 3 + 2 * 2) / 2) / 0.55),
+            Math.sqrt(Math.sqrt((5 * 5) / 1) / 0.55)
+        ]);
+    });
+    test('chunk size larger than sample size creates single chunk', () => {
+        const samples = [1, 1, 1];
+        const chunkedLevels = computeChunkedRMS(samples, 7);
+        // chunked to [1, 1, 1]
+        // rms scaled with default unity of 0.55
+        expect(chunkedLevels.length).toEqual(1);
+        expect(chunkedLevels).toEqual([Math.sqrt(1 / 0.55)]);
+    });
+    test('chunk size as multiple is handled correctly', () => {
+        const samples = [1, 1, 1, 1, 1, 1];
+        const chunkedLevels = computeChunkedRMS(samples, 3);
+        // chunked to [1, 1, 1], [1, 1, 1]
+        // rms scaled with default unity of 0.55
+        expect(chunkedLevels.length).toEqual(2);
+        expect(chunkedLevels).toEqual([Math.sqrt(1 / 0.55), Math.sqrt(1 / 0.55)]);
+    });
+});