From 7bbe5f0589473d0f333284731fc6b855b44b6427 Mon Sep 17 00:00:00 2001
From: Eric Rosenbaum <eric.rosenbaum@gmail.com>
Date: Tue, 9 Jul 2019 10:42:53 -0400
Subject: [PATCH] Add fade in, fade out and mute effects

---
 src/components/sound-editor/icon--fade-in.svg | Bin 0 -> 3542 bytes
 .../sound-editor/icon--fade-out.svg           | Bin 0 -> 3499 bytes
 src/components/sound-editor/icon--mute.svg    | Bin 0 -> 1763 bytes
 src/components/sound-editor/sound-editor.jsx  |  39 ++++++++++++++++++
 src/containers/sound-editor.jsx               |   3 ++
 src/lib/audio/audio-effects.js                |  19 ++++++++-
 src/lib/audio/effects/fade-effect.js          |  27 ++++++++++++
 src/lib/audio/effects/mute-effect.js          |  22 ++++++++++
 8 files changed, 109 insertions(+), 1 deletion(-)
 create mode 100644 src/components/sound-editor/icon--fade-in.svg
 create mode 100644 src/components/sound-editor/icon--fade-out.svg
 create mode 100644 src/components/sound-editor/icon--mute.svg
 create mode 100644 src/lib/audio/effects/fade-effect.js
 create mode 100644 src/lib/audio/effects/mute-effect.js

diff --git a/src/components/sound-editor/icon--fade-in.svg b/src/components/sound-editor/icon--fade-in.svg
new file mode 100644
index 0000000000000000000000000000000000000000..6bd9e6974fed57d83e6d40da8e7103450550cb8f
GIT binary patch
literal 3542
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
YP?j`cVXBJ=)IpOpz-hz=)RnRW0NP|I+W-In

literal 0
HcmV?d00001

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..d6704bc0ee4ae2c7d7c10d9b4585c1426d4e85dc
GIT binary patch
literal 3499
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

literal 0
HcmV?d00001

diff --git a/src/components/sound-editor/icon--mute.svg b/src/components/sound-editor/icon--mute.svg
new file mode 100644
index 0000000000000000000000000000000000000000..1fcbbd55962f47042df81f333bb098d0802a0257
GIT binary patch
literal 1763
zcmcDqugJ|&C`&CW&dkrVRWj5wP*O<EOU_Tp%uBab3Jr17wNSFR<FY9(OIIk*Oex8*
zRWdRts8CYKNX<;oD1q_HGE>W)@+)kW3=|9$j6g^U*-%3zg^Jvqykc9WjFOT9D}DX)
z@^Za$W4-*MbbTWO0|WiyvUI2ttBRb=yliX=4J|D#^}(`Ac3cVy3JNxgy1EMPsd=eI
zi6!|(Rtmw{sU^u73Z|xdMhY6{7KRpvnhLrKARCLV^!1CuvWW!+ddc~@3c9*bGfFZ`
za#HPr^Gowm^j*`^Qj<%H^?gfAQf>4hG7y_mQj3%AoQqNuOHxx5$}>wcpqA;`=!2zT
zs?v%f#)6$!kXVwTkYcOkYiO)zVrpV&Zepops%KzgVQ6Y(Vyxg}XsTysWMOD%W}#!G
zXJKw)WN2V&qTp<3s%L3rXke&gq-SbjXl!V1Y@}dlre|zvYGGk)jwWhmVPS3#5=7E(
zre|qkXl8C;q+_gSXl8C`U~Fy%G65lKW@2b*Zf;@;5`~&ztY>L#VqtDzZm8g6qGxJs
zU~Ft)V5wthre|PjY;0(5rr>O%XJl+{Vr*t%4iYxDFf%eVuuw48GdD9bH8VCeMin(N
zvotfav@`^X!i_N2Gc-3dH#alaF*Mb)FfcGNGBHs=5i~Y3H8nLbQ!v&;7+|VrU~FV&
zYHp(7W2R?pXk=(&X>6opXryOoX<%q>q2O$$XJ}+%Xb7^*NYB8`$k5o_Qo&5e&`{69
z#LU>(*bu@oGcdBWFgI23G1IZoGcz_f0dbr`93vBRLqj7|1(5NUCWfX)<~rtjmPY1g
z5J7VTOEU{Ib5kTiA9FneBNH<NV<RIZQD+N1b5nD3GYb<7BvC^HJtIpKLqkIyGd&AK
zP{5fg7#iqV7#mxfnOo?X>KU4uS(q7_DmWV&=vf+BnwpuL=$L?#gfU3e&`{6Fz|h>p
z)JVrf&&(X+Y(qmmQv-7|GZRB3K_5dSJqu$)6Jv8rBvEHWV?9Fy10xF)kg0G3jP;C-
zO$-gpO+i)}nH!oL8CWP_PpVN0pfqf1qGPFNU}0=#Vh&0rhQ@lPCOU>DdKRX};J^df
z0}?USGcqwXH#Ro~nPy~UWMOP#2#Rl0Gf40wInvNn4<_n^<Rc_uXC&Vv2_prRp{bs+
znHksxNP%i-s%Kzs430lgsG6Fa8=G60f}Cq)X<%VsZmwWxpl4udYGwgSxW=HIXkuon
zV5w(fZenh1YHX+j3N;f8OG6_Q1s{lCk$fMeq>!0ns|3p8x`s-2Hu@kIB=3WY1Xwv>
zlMYsfS-dDI6qgj`XQ$dK<>lw4!g#vSBGynzAuTf}2PO$(>lT&fq}nQ_mZj$9r=&nj
zKyWdWn^>F;wgtq{HG*j^&&VuEMN$V=TUwl|04_qUGKx~uY?YKj&W3oPv^W*wey~D)
zkYPxCh(CN2i?czVO~)dWo|;#ZsBakWn+RqpDS%9{RVppY(NG34b&WJ(PE&SvGc$H}
zLw8(JYH|tKBhL9b`9(?!pz1_Pq0&~#KnYRzAlEz~r+_pexkexAG#e1Z22`)u0RY;q
Bqmcjr

literal 0
HcmV?d00001

diff --git a/src/components/sound-editor/sound-editor.jsx b/src/components/sound-editor/sound-editor.jsx
index 57fc2149e..cbd731f14 100644
--- a/src/components/sound-editor/sound-editor.jsx
+++ b/src/components/sound-editor/sound-editor.jsx
@@ -26,6 +26,9 @@ import louderIcon from './icon--louder.svg';
 import softerIcon from './icon--softer.svg';
 import robotIcon from './icon--robot.svg';
 import reverseIcon from './icon--reverse.svg';
+import fadeOutIcon from './icon--fade-out.svg';
+import fadeInIcon from './icon--fade-in.svg';
+import muteIcon from './icon--mute.svg';
 
 const BufferedInput = BufferedInputHOC(Input);
 
@@ -99,6 +102,21 @@ const messages = defineMessages({
         id: 'gui.soundEditor.reverse',
         description: 'Title of the button to apply the reverse effect',
         defaultMessage: 'Reverse'
+    },
+    fadeOut: {
+        id: 'gui.soundEditor.fadeOut',
+        description: 'Title of the button to apply the fade out effect',
+        defaultMessage: 'Fade out'
+    },
+    fadeIn: {
+        id: 'gui.soundEditor.fadeIn',
+        description: 'Title of the button to apply the fade in effect',
+        defaultMessage: 'Fade in'
+    },
+    mute: {
+        id: 'gui.soundEditor.mute',
+        description: 'Title of the button to apply the mute effect',
+        defaultMessage: 'Mute'
     }
 });
 
@@ -237,12 +255,30 @@ const SoundEditor = props => (
                 title={<FormattedMessage {...messages.softer} />}
                 onClick={props.onSofter}
             />
+            <IconButton
+                className={styles.effectButton}
+                img={muteIcon}
+                title={<FormattedMessage {...messages.mute} />}
+                onClick={props.onMute}
+            />
             <IconButton
                 className={styles.effectButton}
                 img={reverseIcon}
                 title={<FormattedMessage {...messages.reverse} />}
                 onClick={props.onReverse}
             />
+            <IconButton
+                className={styles.effectButton}
+                img={fadeOutIcon}
+                title={<FormattedMessage {...messages.fadeOut} />}
+                onClick={props.onFadeOut}
+            />
+            <IconButton
+                className={styles.effectButton}
+                img={fadeInIcon}
+                title={<FormattedMessage {...messages.fadeIn} />}
+                onClick={props.onFadeIn}
+            />
         </div>
     </div>
 );
@@ -257,8 +293,11 @@ SoundEditor.propTypes = {
     onChangeName: PropTypes.func.isRequired,
     onContainerClick: PropTypes.func.isRequired,
     onEcho: PropTypes.func.isRequired,
+    onFadeIn: PropTypes.func.isRequired,
+    onFadeOut: PropTypes.func.isRequired,
     onFaster: PropTypes.func.isRequired,
     onLouder: PropTypes.func.isRequired,
+    onMute: PropTypes.func.isRequired,
     onPlay: PropTypes.func.isRequired,
     onRedo: PropTypes.func.isRequired,
     onReverse: PropTypes.func.isRequired,
diff --git a/src/containers/sound-editor.jsx b/src/containers/sound-editor.jsx
index 67ba3a1ae..24c0758d3 100644
--- a/src/containers/sound-editor.jsx
+++ b/src/containers/sound-editor.jsx
@@ -227,8 +227,11 @@ class SoundEditor extends React.Component {
                 onContainerClick={this.handleContainerClick}
                 onDelete={this.handleDelete}
                 onEcho={this.effectFactory(effectTypes.ECHO)}
+                onFadeIn={this.effectFactory(effectTypes.FADEIN)}
+                onFadeOut={this.effectFactory(effectTypes.FADEOUT)}
                 onFaster={this.effectFactory(effectTypes.FASTER)}
                 onLouder={this.effectFactory(effectTypes.LOUDER)}
+                onMute={this.effectFactory(effectTypes.MUTE)}
                 onPlay={this.handlePlay}
                 onRedo={this.handleRedo}
                 onReverse={this.effectFactory(effectTypes.REVERSE)}
diff --git a/src/lib/audio/audio-effects.js b/src/lib/audio/audio-effects.js
index 5dae9cf13..b6b78f5b0 100644
--- a/src/lib/audio/audio-effects.js
+++ b/src/lib/audio/audio-effects.js
@@ -1,6 +1,8 @@
 import EchoEffect from './effects/echo-effect.js';
 import RobotEffect from './effects/robot-effect.js';
 import VolumeEffect from './effects/volume-effect.js';
+import FadeEffect from './effects/fade-effect.js';
+import MuteEffect from './effects/mute-effect.js';
 
 const effectTypes = {
     ROBOT: 'robot',
@@ -9,7 +11,10 @@ const effectTypes = {
     SOFTER: 'lower',
     FASTER: 'faster',
     SLOWER: 'slower',
-    ECHO: 'echo'
+    ECHO: 'echo',
+    FADEIN: 'fade in',
+    FADEOUT: 'fade out',
+    MUTE: 'mute'
 };
 
 class AudioEffects {
@@ -117,6 +122,18 @@ class AudioEffects {
             ({input, output} = new RobotEffect(this.audioContext,
                 this.adjustedTrimStartSeconds, this.adjustedTrimEndSeconds));
             break;
+        case effectTypes.FADEIN:
+            ({input, output} = new FadeEffect(this.audioContext, true,
+                this.adjustedTrimStartSeconds, this.adjustedTrimEndSeconds));
+            break;
+        case effectTypes.FADEOUT:
+            ({input, output} = new FadeEffect(this.audioContext, false,
+                this.adjustedTrimStartSeconds, this.adjustedTrimEndSeconds));
+            break;
+        case effectTypes.MUTE:
+            ({input, output} = new MuteEffect(this.audioContext,
+                this.adjustedTrimStartSeconds, this.adjustedTrimEndSeconds));
+            break;
         }
 
         if (input && output) {
diff --git a/src/lib/audio/effects/fade-effect.js b/src/lib/audio/effects/fade-effect.js
new file mode 100644
index 000000000..5a13c5c16
--- /dev/null
+++ b/src/lib/audio/effects/fade-effect.js
@@ -0,0 +1,27 @@
+class FadeEffect {
+    constructor (audioContext, fadeIn, startSeconds, endSeconds) {
+        this.audioContext = audioContext;
+
+        this.input = this.audioContext.createGain();
+        this.output = this.audioContext.createGain();
+
+        this.gain = this.audioContext.createGain();
+
+        this.gain.gain.setValueAtTime(1, 0);
+
+        if (fadeIn) {
+            this.gain.gain.setValueAtTime(0, startSeconds);
+            this.gain.gain.linearRampToValueAtTime(1, endSeconds);
+        } else {
+            this.gain.gain.setValueAtTime(1, startSeconds);
+            this.gain.gain.linearRampToValueAtTime(0, endSeconds);
+        }
+
+        this.gain.gain.setValueAtTime(1, endSeconds);
+
+        this.input.connect(this.gain);
+        this.gain.connect(this.output);
+    }
+}
+
+export default FadeEffect;
diff --git a/src/lib/audio/effects/mute-effect.js b/src/lib/audio/effects/mute-effect.js
new file mode 100644
index 000000000..c2a2e68d5
--- /dev/null
+++ b/src/lib/audio/effects/mute-effect.js
@@ -0,0 +1,22 @@
+class MuteEffect {
+    constructor (audioContext, startSeconds, endSeconds) {
+        this.audioContext = audioContext;
+
+        this.input = this.audioContext.createGain();
+        this.output = this.audioContext.createGain();
+
+        this.gain = this.audioContext.createGain();
+
+        // Smoothly ramp the gain down before the start time, and up after the end time.
+        this.rampLength = 0.001;
+        this.gain.gain.setValueAtTime(1.0, Math.max(0, startSeconds - this.rampLength));
+        this.gain.gain.linearRampToValueAtTime(0, startSeconds);
+        this.gain.gain.setValueAtTime(0, endSeconds);
+        this.gain.gain.linearRampToValueAtTime(1.0, endSeconds + this.rampLength);
+
+        this.input.connect(this.gain);
+        this.gain.connect(this.output);
+    }
+}
+
+export default MuteEffect;
-- 
GitLab