Spaces:
Runtime error
Runtime error
Update src/lib/audio/audio-effects.js
Browse files- src/lib/audio/audio-effects.js +80 -23
src/lib/audio/audio-effects.js
CHANGED
|
@@ -29,7 +29,7 @@ class AudioEffects {
|
|
| 29 |
static get effectTypes () {
|
| 30 |
return effectTypes;
|
| 31 |
}
|
| 32 |
-
constructor (buffer,
|
| 33 |
this.trimStartSeconds = (trimStart * buffer.length) / buffer.sampleRate;
|
| 34 |
this.trimEndSeconds = (trimEnd * buffer.length) / buffer.sampleRate;
|
| 35 |
this.adjustedTrimStartSeconds = this.trimStartSeconds;
|
|
@@ -44,8 +44,9 @@ class AudioEffects {
|
|
| 44 |
let adjustedAffectedSampleCount = affectedSampleCount;
|
| 45 |
const unaffectedSampleCount = sampleCount - affectedSampleCount;
|
| 46 |
|
|
|
|
| 47 |
this.playbackRate = 1;
|
| 48 |
-
switch (
|
| 49 |
case effectTypes.ECHO:
|
| 50 |
sampleCount = Math.max(sampleCount,
|
| 51 |
Math.floor((this.trimEndSeconds + EchoEffect.TAIL_SECONDS) * buffer.sampleRate));
|
|
@@ -54,7 +55,6 @@ class AudioEffects {
|
|
| 54 |
this.playbackRate = pitchRatio;
|
| 55 |
adjustedAffectedSampleCount = Math.floor(affectedSampleCount / this.playbackRate);
|
| 56 |
sampleCount = unaffectedSampleCount + adjustedAffectedSampleCount;
|
| 57 |
-
|
| 58 |
break;
|
| 59 |
case effectTypes.SLOWER:
|
| 60 |
this.playbackRate = 1 / pitchRatio;
|
|
@@ -62,9 +62,7 @@ class AudioEffects {
|
|
| 62 |
sampleCount = unaffectedSampleCount + adjustedAffectedSampleCount;
|
| 63 |
break;
|
| 64 |
default:
|
| 65 |
-
if (
|
| 66 |
-
const options = name;
|
| 67 |
-
if (options.pitch !== null) {
|
| 68 |
this.playbackRate = centsToFrequency(options.pitch);
|
| 69 |
adjustedAffectedSampleCount = Math.floor(affectedSampleCount / this.playbackRate);
|
| 70 |
sampleCount = unaffectedSampleCount + adjustedAffectedSampleCount;
|
|
@@ -78,19 +76,30 @@ class AudioEffects {
|
|
| 78 |
this.adjustedTrimStart = this.adjustedTrimStartSeconds / durationSeconds;
|
| 79 |
this.adjustedTrimEnd = this.adjustedTrimEndSeconds / durationSeconds;
|
| 80 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 81 |
if (window.OfflineAudioContext) {
|
| 82 |
-
this.audioContext = new window.OfflineAudioContext(1,
|
| 83 |
} else {
|
| 84 |
// Need to use webkitOfflineAudioContext, which doesn't support all sample rates.
|
| 85 |
// Resample by adjusting sample count to make room and set offline context to desired sample rate.
|
| 86 |
-
const sampleScale = 44100 /
|
| 87 |
-
this.audioContext = new window.webkitOfflineAudioContext(1, sampleScale *
|
| 88 |
}
|
| 89 |
|
|
|
|
|
|
|
|
|
|
| 90 |
// For the reverse effect we need to manually reverse the data into a new audio buffer
|
| 91 |
// to prevent overwriting the original, so that the undo stack works correctly.
|
| 92 |
// Doing buffer.reverse() would mutate the original data.
|
| 93 |
-
if (
|
|
|
|
| 94 |
const originalBufferData = buffer.getChannelData(0);
|
| 95 |
const newBuffer = this.audioContext.createBuffer(1, buffer.length, buffer.sampleRate);
|
| 96 |
const newBufferData = newBuffer.getChannelData(0);
|
|
@@ -108,20 +117,72 @@ class AudioEffects {
|
|
| 108 |
}
|
| 109 |
}
|
| 110 |
this.buffer = newBuffer;
|
| 111 |
-
}
|
| 112 |
-
|
| 113 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 114 |
}
|
| 115 |
|
| 116 |
this.source = this.audioContext.createBufferSource();
|
| 117 |
this.source.buffer = this.buffer;
|
| 118 |
-
this.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 119 |
}
|
| 120 |
process (done) {
|
| 121 |
// Some effects need to use more nodes and must expose an input and output
|
| 122 |
let input;
|
| 123 |
let output;
|
| 124 |
-
switch (this.
|
| 125 |
case effectTypes.FASTER:
|
| 126 |
case effectTypes.SLOWER:
|
| 127 |
this.source.playbackRate.setValueAtTime(this.playbackRate, this.adjustedTrimStartSeconds);
|
|
@@ -164,15 +225,12 @@ class AudioEffects {
|
|
| 164 |
this.adjustedTrimStartSeconds, this.adjustedTrimEndSeconds));
|
| 165 |
break;
|
| 166 |
default:
|
| 167 |
-
|
| 168 |
-
if (!((typeof name === "object") && (name.special === true))) break;
|
| 169 |
-
const options = name;
|
| 170 |
-
if (options.pitch !== null) {
|
| 171 |
this.source.playbackRate.setValueAtTime(this.playbackRate, this.adjustedTrimStartSeconds);
|
| 172 |
this.source.playbackRate.setValueAtTime(1.0, this.adjustedTrimEndSeconds);
|
| 173 |
}
|
| 174 |
-
if (options
|
| 175 |
-
({input, output} = new VolumeEffect(this.audioContext, options.volume,
|
| 176 |
this.adjustedTrimStartSeconds, this.adjustedTrimEndSeconds));
|
| 177 |
}
|
| 178 |
break;
|
|
@@ -192,8 +250,7 @@ class AudioEffects {
|
|
| 192 |
this.audioContext.oncomplete = ({renderedBuffer}) => {
|
| 193 |
done(renderedBuffer, this.adjustedTrimStart, this.adjustedTrimEnd);
|
| 194 |
};
|
| 195 |
-
|
| 196 |
}
|
| 197 |
}
|
| 198 |
|
| 199 |
-
export default AudioEffects;
|
|
|
|
| 29 |
static get effectTypes () {
|
| 30 |
return effectTypes;
|
| 31 |
}
|
| 32 |
+
constructor (buffer, options, trimStart, trimEnd) {
|
| 33 |
this.trimStartSeconds = (trimStart * buffer.length) / buffer.sampleRate;
|
| 34 |
this.trimEndSeconds = (trimEnd * buffer.length) / buffer.sampleRate;
|
| 35 |
this.adjustedTrimStartSeconds = this.trimStartSeconds;
|
|
|
|
| 44 |
let adjustedAffectedSampleCount = affectedSampleCount;
|
| 45 |
const unaffectedSampleCount = sampleCount - affectedSampleCount;
|
| 46 |
|
| 47 |
+
// These affect the sampleCount
|
| 48 |
this.playbackRate = 1;
|
| 49 |
+
switch (options.preset) {
|
| 50 |
case effectTypes.ECHO:
|
| 51 |
sampleCount = Math.max(sampleCount,
|
| 52 |
Math.floor((this.trimEndSeconds + EchoEffect.TAIL_SECONDS) * buffer.sampleRate));
|
|
|
|
| 55 |
this.playbackRate = pitchRatio;
|
| 56 |
adjustedAffectedSampleCount = Math.floor(affectedSampleCount / this.playbackRate);
|
| 57 |
sampleCount = unaffectedSampleCount + adjustedAffectedSampleCount;
|
|
|
|
| 58 |
break;
|
| 59 |
case effectTypes.SLOWER:
|
| 60 |
this.playbackRate = 1 / pitchRatio;
|
|
|
|
| 62 |
sampleCount = unaffectedSampleCount + adjustedAffectedSampleCount;
|
| 63 |
break;
|
| 64 |
default:
|
| 65 |
+
if (Object.prototype.hasOwnProperty.call(options, "pitch")) {
|
|
|
|
|
|
|
| 66 |
this.playbackRate = centsToFrequency(options.pitch);
|
| 67 |
adjustedAffectedSampleCount = Math.floor(affectedSampleCount / this.playbackRate);
|
| 68 |
sampleCount = unaffectedSampleCount + adjustedAffectedSampleCount;
|
|
|
|
| 76 |
this.adjustedTrimStart = this.adjustedTrimStartSeconds / durationSeconds;
|
| 77 |
this.adjustedTrimEnd = this.adjustedTrimEndSeconds / durationSeconds;
|
| 78 |
|
| 79 |
+
let audioContextSampleRate = buffer.sampleRate;
|
| 80 |
+
let audioContextSampleCount = sampleCount;
|
| 81 |
+
if (Object.prototype.hasOwnProperty.call(options, "sampleRateEnforced")) {
|
| 82 |
+
const newSampleRate = options.sampleRateEnforced;
|
| 83 |
+
audioContextSampleRate = newSampleRate;
|
| 84 |
+
audioContextSampleCount = Math.floor((sampleCount / buffer.sampleRate) * newSampleRate);
|
| 85 |
+
}
|
| 86 |
if (window.OfflineAudioContext) {
|
| 87 |
+
this.audioContext = new window.OfflineAudioContext(1, audioContextSampleCount, audioContextSampleRate);
|
| 88 |
} else {
|
| 89 |
// Need to use webkitOfflineAudioContext, which doesn't support all sample rates.
|
| 90 |
// Resample by adjusting sample count to make room and set offline context to desired sample rate.
|
| 91 |
+
const sampleScale = 44100 / audioContextSampleRate;
|
| 92 |
+
this.audioContext = new window.webkitOfflineAudioContext(1, sampleScale * audioContextSampleCount, 44100);
|
| 93 |
}
|
| 94 |
|
| 95 |
+
// All effects not seen below use the original buffer because it is not modified.
|
| 96 |
+
this.buffer = buffer;
|
| 97 |
+
|
| 98 |
// For the reverse effect we need to manually reverse the data into a new audio buffer
|
| 99 |
// to prevent overwriting the original, so that the undo stack works correctly.
|
| 100 |
// Doing buffer.reverse() would mutate the original data.
|
| 101 |
+
if (options.preset === effectTypes.REVERSE) {
|
| 102 |
+
const buffer = this.buffer;
|
| 103 |
const originalBufferData = buffer.getChannelData(0);
|
| 104 |
const newBuffer = this.audioContext.createBuffer(1, buffer.length, buffer.sampleRate);
|
| 105 |
const newBufferData = newBuffer.getChannelData(0);
|
|
|
|
| 117 |
}
|
| 118 |
}
|
| 119 |
this.buffer = newBuffer;
|
| 120 |
+
}
|
| 121 |
+
if (Object.prototype.hasOwnProperty.call(options, "sampleRate")) {
|
| 122 |
+
// We can't overwrite the original buffer so we make a clone.
|
| 123 |
+
const buffer = this.buffer;
|
| 124 |
+
const originalBufferData = buffer.getChannelData(0);
|
| 125 |
+
const newBuffer = this.audioContext.createBuffer(1, buffer.length, buffer.sampleRate);
|
| 126 |
+
const newBufferData = newBuffer.getChannelData(0);
|
| 127 |
+
const bufferLength = buffer.length;
|
| 128 |
+
|
| 129 |
+
// Our clone from earlier also needs to keep the original buffer's sample rate, so we need to make yet another buffer.
|
| 130 |
+
const sampleRateBuffer = this.makeSampleRateBuffer(buffer, durationSeconds, options.sampleRate);
|
| 131 |
+
const sampleRateBufferData = sampleRateBuffer.getChannelData(0);
|
| 132 |
+
|
| 133 |
+
const startSamples = Math.floor(this.trimStartSeconds * buffer.sampleRate);
|
| 134 |
+
const endSamples = Math.floor(this.trimEndSeconds * buffer.sampleRate);
|
| 135 |
+
for (let i = 0; i < bufferLength; i++) {
|
| 136 |
+
if (i >= startSamples && i < endSamples) {
|
| 137 |
+
// We need to convert sampleRate back to the current buffer's sampleRate
|
| 138 |
+
const sampleRateModifiedIndex = i * (sampleRateBuffer.sampleRate / buffer.sampleRate);
|
| 139 |
+
const lowerIndex = Math.floor(sampleRateModifiedIndex);
|
| 140 |
+
const upperIndex = Math.min(lowerIndex + 1, sampleRateBuffer.length - 1);
|
| 141 |
+
const interpolation = sampleRateModifiedIndex - lowerIndex;
|
| 142 |
+
|
| 143 |
+
const sample =
|
| 144 |
+
sampleRateBufferData[lowerIndex] * (1 - interpolation) +
|
| 145 |
+
sampleRateBufferData[upperIndex] * interpolation;
|
| 146 |
+
// This works without Number.isFinite but it breaks the waveform preview SVG because sample can be NaN
|
| 147 |
+
newBufferData[i] = Number.isFinite(sample) ? sample : 0;
|
| 148 |
+
} else {
|
| 149 |
+
newBufferData[i] = originalBufferData[i];
|
| 150 |
+
}
|
| 151 |
+
}
|
| 152 |
+
this.buffer = newBuffer;
|
| 153 |
}
|
| 154 |
|
| 155 |
this.source = this.audioContext.createBufferSource();
|
| 156 |
this.source.buffer = this.buffer;
|
| 157 |
+
this.options = options;
|
| 158 |
+
}
|
| 159 |
+
makeSampleRateBuffer(buffer, durationSeconds, newSampleRate) {
|
| 160 |
+
const originalBufferData = buffer.getChannelData(0);
|
| 161 |
+
const newBufferLength = Math.floor(durationSeconds * newSampleRate);
|
| 162 |
+
const newBuffer = this.audioContext.createBuffer(1, newBufferLength, newSampleRate);
|
| 163 |
+
const newBufferData = newBuffer.getChannelData(0);
|
| 164 |
+
const bufferLength = buffer.length;
|
| 165 |
+
|
| 166 |
+
// this does work with just bufferLength but causes cut-off when newSampleRate is larger than the current sample rate
|
| 167 |
+
for (let i = 0; i < newBufferLength; i++) {
|
| 168 |
+
const originalIndex = i * (buffer.sampleRate / newSampleRate);
|
| 169 |
+
const lowerIndex = Math.floor(originalIndex);
|
| 170 |
+
const upperIndex = Math.min(lowerIndex + 1, bufferLength - 1);
|
| 171 |
+
const interpolation = originalIndex - lowerIndex;
|
| 172 |
+
|
| 173 |
+
const sample =
|
| 174 |
+
originalBufferData[lowerIndex] * (1 - interpolation) +
|
| 175 |
+
originalBufferData[upperIndex] * interpolation;
|
| 176 |
+
newBufferData[i] = sample;
|
| 177 |
+
}
|
| 178 |
+
|
| 179 |
+
return newBuffer;
|
| 180 |
}
|
| 181 |
process (done) {
|
| 182 |
// Some effects need to use more nodes and must expose an input and output
|
| 183 |
let input;
|
| 184 |
let output;
|
| 185 |
+
switch (this.options.preset) {
|
| 186 |
case effectTypes.FASTER:
|
| 187 |
case effectTypes.SLOWER:
|
| 188 |
this.source.playbackRate.setValueAtTime(this.playbackRate, this.adjustedTrimStartSeconds);
|
|
|
|
| 225 |
this.adjustedTrimStartSeconds, this.adjustedTrimEndSeconds));
|
| 226 |
break;
|
| 227 |
default:
|
| 228 |
+
if (Object.prototype.hasOwnProperty.call(this.options, "pitch")) {
|
|
|
|
|
|
|
|
|
|
| 229 |
this.source.playbackRate.setValueAtTime(this.playbackRate, this.adjustedTrimStartSeconds);
|
| 230 |
this.source.playbackRate.setValueAtTime(1.0, this.adjustedTrimEndSeconds);
|
| 231 |
}
|
| 232 |
+
if (Object.prototype.hasOwnProperty.call(this.options, "volume")) {
|
| 233 |
+
({input, output} = new VolumeEffect(this.audioContext, this.options.volume,
|
| 234 |
this.adjustedTrimStartSeconds, this.adjustedTrimEndSeconds));
|
| 235 |
}
|
| 236 |
break;
|
|
|
|
| 250 |
this.audioContext.oncomplete = ({renderedBuffer}) => {
|
| 251 |
done(renderedBuffer, this.adjustedTrimStart, this.adjustedTrimEnd);
|
| 252 |
};
|
|
|
|
| 253 |
}
|
| 254 |
}
|
| 255 |
|
| 256 |
+
export default AudioEffects;
|