From 26ca5da72571a76e5dbb1f3c8279d20829f8ac93 Mon Sep 17 00:00:00 2001 From: Eric Rosenbaum Date: Tue, 3 Sep 2019 18:13:45 -0400 Subject: [PATCH 1/4] Limit louder effect with a max RMS --- src/lib/audio/audio-effects.js | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/src/lib/audio/audio-effects.js b/src/lib/audio/audio-effects.js index 8dd551f38..263199c46 100644 --- a/src/lib/audio/audio-effects.js +++ b/src/lib/audio/audio-effects.js @@ -3,6 +3,7 @@ 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'; +import {computeChunkedRMS} from './audio-util.js'; const effectTypes = { ROBOT: 'robot', @@ -96,6 +97,21 @@ class AudioEffects { this.buffer = buffer; } + // If the sound is louder than an RMS limit, do not apply gain. + // This helps reduce distortion due to clipping. + this.louderGain = 1.25; + this.rmsLimit = 1.2; + if (name === effectTypes.LOUDER) { + const trimStartSamples = Math.floor(trimStart * buffer.length); + const trimEndSamples = Math.floor(trimEnd * buffer.length); + const slicedBuffer = buffer.getChannelData(0).slice(trimStartSamples, trimEndSamples); + const rmsChunks = computeChunkedRMS(slicedBuffer); + const maxRMS = Math.max(...rmsChunks); + if (maxRMS > this.rmsLimit) { + this.louderGain = 1; + } + } + this.source = this.audioContext.createBufferSource(); this.source.buffer = this.buffer; this.name = name; @@ -111,7 +127,7 @@ class AudioEffects { 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, this.louderGain, this.adjustedTrimStartSeconds, this.adjustedTrimEndSeconds)); break; case effectTypes.SOFTER: From 059d3f709b468428acff71be33990fdfe8237d1a Mon Sep 17 00:00:00 2001 From: Eric Rosenbaum Date: Wed, 4 Sep 2019 11:01:10 -0400 Subject: [PATCH 2/4] Use a waveshaper to hard limit samples --- src/lib/audio/effects/volume-effect.js | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/lib/audio/effects/volume-effect.js b/src/lib/audio/effects/volume-effect.js index 0cfcd6415..a5d12a987 100644 --- a/src/lib/audio/effects/volume-effect.js +++ b/src/lib/audio/effects/volume-effect.js @@ -14,8 +14,16 @@ class VolumeEffect { this.gain.gain.setValueAtTime(volume, endSeconds); this.gain.gain.exponentialRampToValueAtTime(1.0, endSeconds + this.rampLength); + // Use a waveshaper node to prevent sample values from exceeding -1 or 1. + // Without this, gain can cause samples to exceed this range, then they + // are clipped on save, and the sound is distorted on load. + this.waveShaper = this.audioContext.createWaveShaper(); + this.waveShaper.curve = new Float32Array([-1, 1]); + this.waveShaper.oversample = 'none'; + this.input.connect(this.gain); - this.gain.connect(this.output); + this.gain.connect(this.waveShaper); + this.waveShaper.connect(this.output); } } From 641498546113dd9057051e1666b88d3462aec0c3 Mon Sep 17 00:00:00 2001 From: Eric Rosenbaum Date: Thu, 5 Sep 2019 13:49:22 -0400 Subject: [PATCH 3/4] Calc tooLoud with chunkLevels, disable louder btn --- src/components/sound-editor/sound-editor.jsx | 2 ++ src/containers/sound-editor.jsx | 12 ++++++++++++ src/lib/audio/audio-effects.js | 18 +----------------- 3 files changed, 15 insertions(+), 17 deletions(-) diff --git a/src/components/sound-editor/sound-editor.jsx b/src/components/sound-editor/sound-editor.jsx index 84f56d08b..32a018a56 100644 --- a/src/components/sound-editor/sound-editor.jsx +++ b/src/components/sound-editor/sound-editor.jsx @@ -265,6 +265,7 @@ const SoundEditor = props => ( onClick={props.onSlower} /> } @@ -340,6 +341,7 @@ SoundEditor.propTypes = { onUndo: PropTypes.func.isRequired, playhead: PropTypes.number, setRef: PropTypes.func, + tooLoud: PropTypes.bool.isRequired, trimEnd: PropTypes.number, trimStart: PropTypes.number }; diff --git a/src/containers/sound-editor.jsx b/src/containers/sound-editor.jsx index a2b19719f..2a2aa4e7e 100644 --- a/src/containers/sound-editor.jsx +++ b/src/containers/sound-editor.jsx @@ -14,6 +14,8 @@ import log from '../lib/log.js'; const UNDO_STACK_SIZE = 99; +const MAX_RMS = 1.2; + class SoundEditor extends React.Component { constructor (props) { super(props); @@ -265,6 +267,15 @@ class SoundEditor extends React.Component { } }); } + tooLoud () { + const numChunks = this.state.chunkLevels.length; + const startIndex = this.state.trimStart === null ? + 0 : Math.floor(this.state.trimStart * numChunks); + const endIndex = this.state.trimEnd === null ? + numChunks - 1 : Math.floor(this.state.trimEnd * numChunks); + const trimChunks = this.state.chunkLevels.slice(startIndex, endIndex + 1); + return Math.max(...trimChunks) > MAX_RMS; + } getUndoItem () { return { ...this.copyCurrentBuffer(), @@ -399,6 +410,7 @@ class SoundEditor extends React.Component { name={this.props.name} playhead={this.state.playhead} setRef={this.setRef} + tooLoud={this.tooLoud()} trimEnd={this.state.trimEnd} trimStart={this.state.trimStart} onChangeName={this.handleChangeName} diff --git a/src/lib/audio/audio-effects.js b/src/lib/audio/audio-effects.js index 263199c46..8dd551f38 100644 --- a/src/lib/audio/audio-effects.js +++ b/src/lib/audio/audio-effects.js @@ -3,7 +3,6 @@ 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'; -import {computeChunkedRMS} from './audio-util.js'; const effectTypes = { ROBOT: 'robot', @@ -97,21 +96,6 @@ class AudioEffects { this.buffer = buffer; } - // If the sound is louder than an RMS limit, do not apply gain. - // This helps reduce distortion due to clipping. - this.louderGain = 1.25; - this.rmsLimit = 1.2; - if (name === effectTypes.LOUDER) { - const trimStartSamples = Math.floor(trimStart * buffer.length); - const trimEndSamples = Math.floor(trimEnd * buffer.length); - const slicedBuffer = buffer.getChannelData(0).slice(trimStartSamples, trimEndSamples); - const rmsChunks = computeChunkedRMS(slicedBuffer); - const maxRMS = Math.max(...rmsChunks); - if (maxRMS > this.rmsLimit) { - this.louderGain = 1; - } - } - this.source = this.audioContext.createBufferSource(); this.source.buffer = this.buffer; this.name = name; @@ -127,7 +111,7 @@ class AudioEffects { this.source.playbackRate.setValueAtTime(1.0, this.adjustedTrimEndSeconds); break; case effectTypes.LOUDER: - ({input, output} = new VolumeEffect(this.audioContext, this.louderGain, + ({input, output} = new VolumeEffect(this.audioContext, 1.25, this.adjustedTrimStartSeconds, this.adjustedTrimEndSeconds)); break; case effectTypes.SOFTER: From 2df0c80b30fd493b6ba05af04ec610e8e6f06501 Mon Sep 17 00:00:00 2001 From: Eric Rosenbaum Date: Tue, 10 Sep 2019 10:35:19 -0400 Subject: [PATCH 4/4] Simplify chunk counting logic --- src/containers/sound-editor.jsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/containers/sound-editor.jsx b/src/containers/sound-editor.jsx index 2a2aa4e7e..94fca9524 100644 --- a/src/containers/sound-editor.jsx +++ b/src/containers/sound-editor.jsx @@ -272,8 +272,8 @@ class SoundEditor extends React.Component { const startIndex = this.state.trimStart === null ? 0 : Math.floor(this.state.trimStart * numChunks); const endIndex = this.state.trimEnd === null ? - numChunks - 1 : Math.floor(this.state.trimEnd * numChunks); - const trimChunks = this.state.chunkLevels.slice(startIndex, endIndex + 1); + numChunks - 1 : Math.ceil(this.state.trimEnd * numChunks); + const trimChunks = this.state.chunkLevels.slice(startIndex, endIndex); return Math.max(...trimChunks) > MAX_RMS; } getUndoItem () {