Apply effects to selection
This commit is contained in:
1 parent
f71a481d0e
commit
8f949f532f
5 files changed
+96
-25
No files matched your search
@@ -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 () {
|
||||
|
||||
@@ -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);
|
||||
};
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in new issue
Block a user