Skip to content

Commit d1d9833

Browse files
committed
Update docs and tests
1 parent bb80fa4 commit d1d9833

File tree

3 files changed

+48
-29
lines changed

3 files changed

+48
-29
lines changed

src/containers/sound-editor.jsx

Lines changed: 16 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import {
1010
computeChunkedRMS,
1111
encodeAndAddSoundToVM,
1212
downsampleIfNeeded,
13-
backupDownSampler
13+
dropEveryOtherSample
1414
} from '../lib/audio/audio-util.js';
1515
import AudioEffects from '../lib/audio/audio-effects.js';
1616
import SoundEditorComponent from '../components/sound-editor/sound-editor.jsx';
@@ -138,7 +138,7 @@ class SoundEditor extends React.Component {
138138
});
139139
}
140140
submitNewSamples (samples, sampleRate, skipUndo) {
141-
return downsampleIfNeeded(samples, sampleRate, this.resampleBufferToRate)
141+
return downsampleIfNeeded({samples, sampleRate}, this.resampleBufferToRate)
142142
.then(({samples: newSamples, sampleRate: newSampleRate}) =>
143143
WavEncoder.encode({
144144
sampleRate: newSampleRate,
@@ -208,9 +208,9 @@ class SoundEditor extends React.Component {
208208
trimEnd: null
209209
});
210210
});
211-
212211
}
213212
handleDeleteInverse () {
213+
// Delete everything outside of the trimmers
214214
const {samples, sampleRate} = this.copyCurrentBuffer();
215215
const sampleCount = samples.length;
216216
const startIndex = Math.floor(this.state.trimStart * sampleCount);
@@ -331,19 +331,21 @@ class SoundEditor extends React.Component {
331331
const sampleRateRatio = newRate / buffer.sampleRate;
332332
const newLength = sampleRateRatio * buffer.samples.length;
333333
let offlineContext;
334-
if (window.OfflineAudioContext) {
335-
offlineContext = new window.OfflineAudioContext(1, newLength, newRate);
336-
} else if (window.webkitOfflineAudioContext) {
337-
try {
334+
// Try to use either OfflineAudioContext or webkitOfflineAudioContext to resample
335+
// The constructors will throw if trying to resample at an unsupported rate
336+
// (e.g. Safari/webkitOAC does not support lower than 44khz).
337+
try {
338+
if (window.OfflineAudioContext) {
339+
offlineContext = new window.OfflineAudioContext(1, newLength, newRate);
340+
} else if (window.webkitOfflineAudioContext) {
338341
offlineContext = new window.webkitOfflineAudioContext(1, newLength, newRate);
339-
} catch {
340-
if (newRate === (buffer.sampleRate / 2)) {
341-
return resolve(backupDownSampler(buffer, newRate));
342-
}
343-
return reject('Could not resample');
344342
}
345-
} else {
346-
return reject('No offline audio context');
343+
} catch {
344+
// If no OAC available and downsampling by 2, downsample by dropping every other sample.
345+
if (newRate === buffer.sampleRate / 2) {
346+
return resolve(dropEveryOtherSample(buffer));
347+
}
348+
return reject('Could not resample');
347349
}
348350
const source = offlineContext.createBufferSource();
349351
const audioBuffer = offlineContext.createBuffer(1, buffer.samples.length, buffer.sampleRate);

src/lib/audio/audio-util.js

Lines changed: 23 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import WavEncoder from 'wav-encoder';
2-
import log from '../log.js';
32

43
const SOUND_BYTE_LIMIT = 10 * 1000 * 1000; // 10mb
54

@@ -60,7 +59,21 @@ const encodeAndAddSoundToVM = function (vm, samples, sampleRate, name, callback)
6059
});
6160
};
6261

63-
const downsampleIfNeeded = (samples, sampleRate, resampler) => {
62+
/**
63+
@typedef SoundBuffer
64+
@type {Object}
65+
@property {Float32Array} samples Array of audio samples
66+
@property {number} sampleRate Audio sample rate
67+
*/
68+
69+
/**
70+
* Downsample the given buffer to try to reduce file size below SOUND_BYTE_LIMIT
71+
* @param {SoundBuffer} buffer - Buffer to resample
72+
* @param {function(SoundBuffer):Promise<SoundBuffer>} resampler - resampler function
73+
* @returns {SoundBuffer} Downsampled buffer with half the sample rate
74+
*/
75+
const downsampleIfNeeded = (buffer, resampler) => {
76+
const {samples, sampleRate} = buffer;
6477
const duration = samples.length / sampleRate;
6578
const encodedByteLength = samples.length * 2; /* bitDepth 16 bit */
6679
// Resolve immediately if already within byte limit
@@ -76,16 +89,20 @@ const downsampleIfNeeded = (samples, sampleRate, resampler) => {
7689
return Promise.reject('Sound too large to save, refusing to edit');
7790
};
7891

79-
const backupDownSampler = (buffer, newRate) => {
80-
log.warn(`Using backup down sampler for conversion from ${buffer.sampleRate} to ${newRate}`);
92+
/**
93+
* Drop every other sample of an audio buffer as a last-resort way of downsampling.
94+
* @param {SoundBuffer} buffer - Buffer to resample
95+
* @returns {SoundBuffer} Downsampled buffer with half the sample rate
96+
*/
97+
const dropEveryOtherSample = buffer => {
8198
const newLength = Math.floor(buffer.samples.length / 2);
8299
const newSamples = new Float32Array(newLength);
83100
for (let i = 0; i < newLength; i++) {
84101
newSamples[i] = buffer.samples[i * 2];
85102
}
86103
return {
87104
samples: newSamples,
88-
sampleRate: newRate
105+
sampleRate: buffer.rate / 2
89106
};
90107
};
91108

@@ -94,5 +111,5 @@ export {
94111
computeChunkedRMS,
95112
encodeAndAddSoundToVM,
96113
downsampleIfNeeded,
97-
backupDownSampler
114+
dropEveryOtherSample
98115
};

test/unit/util/audio-util.test.js

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import {
22
computeRMS,
33
computeChunkedRMS,
44
downsampleIfNeeded,
5-
backupDownSampler
5+
dropEveryOtherSample
66
} from '../../../src/lib/audio/audio-util';
77

88
describe('computeRMS', () => {
@@ -60,38 +60,38 @@ describe('downsampleIfNeeded', () => {
6060
const sampleRate = 44100;
6161
test('returns given data when no downsampling needed', async () => {
6262
samples.length = 1;
63-
const res = await downsampleIfNeeded(samples, sampleRate, null);
63+
const res = await downsampleIfNeeded({samples, sampleRate}, null);
6464
expect(res.samples).toEqual(samples);
6565
expect(res.sampleRate).toEqual(sampleRate);
6666
});
6767
test('downsamples to 22050 if that puts it under the limit', async () => {
6868
samples.length = 44100 * 3 * 60;
6969
const resampler = jest.fn(() => 'TEST');
70-
const res = await downsampleIfNeeded(samples, sampleRate, resampler);
70+
const res = await downsampleIfNeeded({samples, sampleRate}, resampler);
7171
expect(resampler).toHaveBeenCalledWith({samples, sampleRate}, 22050);
7272
expect(res).toEqual('TEST');
7373
});
7474
test('fails if resampling would not put it under the limit', async () => {
7575
samples.length = 44100 * 4 * 60;
7676
try {
77-
await downsampleIfNeeded(samples, sampleRate, null);
77+
await downsampleIfNeeded({samples, sampleRate}, null);
7878
} catch (e) {
7979
expect(e).toEqual('Sound too large to save, refusing to edit');
8080
}
8181
});
8282
});
8383

84-
describe('backupDownSampler', () => {
84+
describe('dropEveryOtherSample', () => {
8585
const buffer = {
86-
samples: [1, 0, 1, 0, 1, 0, 1],
86+
samples: [1, 0, 2, 0, 3, 0],
8787
sampleRate: 2
8888
};
8989
test('result is half the length', () => {
90-
const {samples} = backupDownSampler(buffer, 1);
90+
const {samples} = dropEveryOtherSample(buffer, 1);
9191
expect(samples.length).toEqual(Math.floor(buffer.samples.length / 2));
9292
});
9393
test('result contains only even-index items', () => {
94-
const {samples} = backupDownSampler(buffer, 1);
95-
expect(samples.every(v => v === 1)).toBe(true);
94+
const {samples} = dropEveryOtherSample(buffer, 1);
95+
expect(samples).toEqual(new Float32Array([1, 2, 3]));
9696
});
9797
});

0 commit comments

Comments
 (0)