soiz1 commited on
Commit
54abf5d
·
verified ·
1 Parent(s): ccb7526

Update src/lib/audio/audio-effects.js

Browse files
Files changed (1) hide show
  1. 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, name, trimStart, trimEnd) {
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 (name) {
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 (!((typeof name === "object") && (name.special === true))) break;
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, sampleCount, buffer.sampleRate);
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 / buffer.sampleRate;
87
- this.audioContext = new window.webkitOfflineAudioContext(1, sampleScale * sampleCount, 44100);
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 (name === effectTypes.REVERSE) {
 
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
- } else {
112
- // All other effects use the original buffer because it is not modified.
113
- this.buffer = buffer;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
114
  }
115
 
116
  this.source = this.audioContext.createBufferSource();
117
  this.source.buffer = this.buffer;
118
- this.name = name;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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.name) {
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
- const name = this.name;
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.volume !== null) {
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;