From 8f949f532ff7b2dcc73264474b52e756d60ee896 Mon Sep 17 00:00:00 2001 From: Eric Rosenbaum Date: Tue, 9 Jul 2019 10:34:10 -0400 Subject: [PATCH] Apply effects to selection --- src/containers/sound-editor.jsx | 15 ++++-- src/lib/audio/audio-effects.js | 65 ++++++++++++++++++++------ src/lib/audio/effects/echo-effect.js | 7 ++- src/lib/audio/effects/robot-effect.js | 24 ++++++++-- src/lib/audio/effects/volume-effect.js | 10 +++- 5 files changed, 96 insertions(+), 25 deletions(-) diff --git a/src/containers/sound-editor.jsx b/src/containers/sound-editor.jsx index 5e54f7e9c..67ba3a1ae 100644 --- a/src/containers/sound-editor.jsx +++ b/src/containers/sound-editor.jsx @@ -168,12 +168,21 @@ 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; + + 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); + } + } }); } handleUndo () { diff --git a/src/lib/audio/audio-effects.js b/src/lib/audio/audio-effects.js index ab170f9c6..5dae9cf13 100644 --- a/src/lib/audio/audio-effects.js +++ b/src/lib/audio/audio-effects.js @@ -16,25 +16,42 @@ 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); + 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 + 0.75) * buffer.sampleRate)); break; case effectTypes.FASTER: - playbackRate = pitchRatio; - sampleCount = Math.floor(buffer.length / playbackRate); + this.playbackRate = pitchRatio; + sampleCount = unaffectedSampleCount + Math.floor(affectedSampleCount / this.playbackRate); + this.adjustedTrimEndSeconds = this.trimStartSeconds + + ((affectedSampleCount / this.playbackRate) / buffer.sampleRate); break; case effectTypes.SLOWER: - playbackRate = 1 / pitchRatio; - sampleCount = Math.floor(buffer.length / playbackRate); + this.playbackRate = 1 / pitchRatio; + sampleCount = unaffectedSampleCount + Math.floor(affectedSampleCount / this.playbackRate); + this.adjustedTrimEndSeconds = this.trimStartSeconds + + ((affectedSampleCount / this.playbackRate) / buffer.sampleRate); break; } + + this.adjustedTrimStart = this.adjustedTrimStartSeconds / (sampleCount / buffer.sampleRate); + this.adjustedTrimEnd = this.adjustedTrimEndSeconds / (sampleCount / buffer.sampleRate); + if (window.OfflineAudioContext) { this.audioContext = new window.OfflineAudioContext(1, sampleCount, buffer.sampleRate); } else { @@ -52,8 +69,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 +89,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 +96,26 @@ 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, 0.25, + 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; } @@ -96,7 +130,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/effects/echo-effect.js b/src/lib/audio/effects/echo-effect.js index 3c7c06806..182722adf 100644 --- a/src/lib/audio/effects/echo-effect.js +++ b/src/lib/audio/effects/echo-effect.js @@ -1,12 +1,15 @@ class EchoEffect { - constructor (audioContext, delayTime) { + constructor (audioContext, delayTime, 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; diff --git a/src/lib/audio/effects/robot-effect.js b/src/lib/audio/effects/robot-effect.js index 472668d53..653276eab 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 bffc422ac..0cfcd6415 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);