Skip to content

Commit

Permalink
Update docs and tests
Browse files Browse the repository at this point in the history
  • Loading branch information
paulkaplan committed Dec 19, 2019
1 parent bb80fa4 commit d1d9833
Show file tree
Hide file tree
Showing 3 changed files with 48 additions and 29 deletions.
30 changes: 16 additions & 14 deletions src/containers/sound-editor.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import {
computeChunkedRMS,
encodeAndAddSoundToVM,
downsampleIfNeeded,
backupDownSampler
dropEveryOtherSample
} from '../lib/audio/audio-util.js';
import AudioEffects from '../lib/audio/audio-effects.js';
import SoundEditorComponent from '../components/sound-editor/sound-editor.jsx';
Expand Down Expand Up @@ -138,7 +138,7 @@ class SoundEditor extends React.Component {
});
}
submitNewSamples (samples, sampleRate, skipUndo) {
return downsampleIfNeeded(samples, sampleRate, this.resampleBufferToRate)
return downsampleIfNeeded({samples, sampleRate}, this.resampleBufferToRate)
.then(({samples: newSamples, sampleRate: newSampleRate}) =>
WavEncoder.encode({
sampleRate: newSampleRate,
Expand Down Expand Up @@ -208,9 +208,9 @@ class SoundEditor extends React.Component {
trimEnd: null
});
});

}
handleDeleteInverse () {
// Delete everything outside of the trimmers
const {samples, sampleRate} = this.copyCurrentBuffer();
const sampleCount = samples.length;
const startIndex = Math.floor(this.state.trimStart * sampleCount);
Expand Down Expand Up @@ -331,19 +331,21 @@ class SoundEditor extends React.Component {
const sampleRateRatio = newRate / buffer.sampleRate;
const newLength = sampleRateRatio * buffer.samples.length;
let offlineContext;
if (window.OfflineAudioContext) {
offlineContext = new window.OfflineAudioContext(1, newLength, newRate);
} else if (window.webkitOfflineAudioContext) {
try {
// Try to use either OfflineAudioContext or webkitOfflineAudioContext to resample
// The constructors will throw if trying to resample at an unsupported rate
// (e.g. Safari/webkitOAC does not support lower than 44khz).
try {
if (window.OfflineAudioContext) {
offlineContext = new window.OfflineAudioContext(1, newLength, newRate);
} else if (window.webkitOfflineAudioContext) {
offlineContext = new window.webkitOfflineAudioContext(1, newLength, newRate);
} catch {
if (newRate === (buffer.sampleRate / 2)) {
return resolve(backupDownSampler(buffer, newRate));
}
return reject('Could not resample');
}
} else {
return reject('No offline audio context');
} catch {
// If no OAC available and downsampling by 2, downsample by dropping every other sample.
if (newRate === buffer.sampleRate / 2) {
return resolve(dropEveryOtherSample(buffer));
}
return reject('Could not resample');
}
const source = offlineContext.createBufferSource();
const audioBuffer = offlineContext.createBuffer(1, buffer.samples.length, buffer.sampleRate);
Expand Down
29 changes: 23 additions & 6 deletions src/lib/audio/audio-util.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import WavEncoder from 'wav-encoder';
import log from '../log.js';

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

Expand Down Expand Up @@ -60,7 +59,21 @@ const encodeAndAddSoundToVM = function (vm, samples, sampleRate, name, callback)
});
};

const downsampleIfNeeded = (samples, sampleRate, resampler) => {
/**
@typedef SoundBuffer
@type {Object}
@property {Float32Array} samples Array of audio samples
@property {number} sampleRate Audio sample rate
*/

/**
* Downsample the given buffer to try to reduce file size below SOUND_BYTE_LIMIT
* @param {SoundBuffer} buffer - Buffer to resample
* @param {function(SoundBuffer):Promise<SoundBuffer>} resampler - resampler function
* @returns {SoundBuffer} Downsampled buffer with half the sample rate
*/
const downsampleIfNeeded = (buffer, resampler) => {
const {samples, sampleRate} = buffer;
const duration = samples.length / sampleRate;
const encodedByteLength = samples.length * 2; /* bitDepth 16 bit */
// Resolve immediately if already within byte limit
Expand All @@ -76,16 +89,20 @@ const downsampleIfNeeded = (samples, sampleRate, resampler) => {
return Promise.reject('Sound too large to save, refusing to edit');
};

const backupDownSampler = (buffer, newRate) => {
log.warn(`Using backup down sampler for conversion from ${buffer.sampleRate} to ${newRate}`);
/**
* Drop every other sample of an audio buffer as a last-resort way of downsampling.
* @param {SoundBuffer} buffer - Buffer to resample
* @returns {SoundBuffer} Downsampled buffer with half the sample rate
*/
const dropEveryOtherSample = buffer => {
const newLength = Math.floor(buffer.samples.length / 2);
const newSamples = new Float32Array(newLength);
for (let i = 0; i < newLength; i++) {
newSamples[i] = buffer.samples[i * 2];
}
return {
samples: newSamples,
sampleRate: newRate
sampleRate: buffer.rate / 2
};
};

Expand All @@ -94,5 +111,5 @@ export {
computeChunkedRMS,
encodeAndAddSoundToVM,
downsampleIfNeeded,
backupDownSampler
dropEveryOtherSample
};
18 changes: 9 additions & 9 deletions test/unit/util/audio-util.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import {
computeRMS,
computeChunkedRMS,
downsampleIfNeeded,
backupDownSampler
dropEveryOtherSample
} from '../../../src/lib/audio/audio-util';

describe('computeRMS', () => {
Expand Down Expand Up @@ -60,38 +60,38 @@ describe('downsampleIfNeeded', () => {
const sampleRate = 44100;
test('returns given data when no downsampling needed', async () => {
samples.length = 1;
const res = await downsampleIfNeeded(samples, sampleRate, null);
const res = await downsampleIfNeeded({samples, sampleRate}, null);
expect(res.samples).toEqual(samples);
expect(res.sampleRate).toEqual(sampleRate);
});
test('downsamples to 22050 if that puts it under the limit', async () => {
samples.length = 44100 * 3 * 60;
const resampler = jest.fn(() => 'TEST');
const res = await downsampleIfNeeded(samples, sampleRate, resampler);
const res = await downsampleIfNeeded({samples, sampleRate}, resampler);
expect(resampler).toHaveBeenCalledWith({samples, sampleRate}, 22050);
expect(res).toEqual('TEST');
});
test('fails if resampling would not put it under the limit', async () => {
samples.length = 44100 * 4 * 60;
try {
await downsampleIfNeeded(samples, sampleRate, null);
await downsampleIfNeeded({samples, sampleRate}, null);
} catch (e) {
expect(e).toEqual('Sound too large to save, refusing to edit');
}
});
});

describe('backupDownSampler', () => {
describe('dropEveryOtherSample', () => {
const buffer = {
samples: [1, 0, 1, 0, 1, 0, 1],
samples: [1, 0, 2, 0, 3, 0],
sampleRate: 2
};
test('result is half the length', () => {
const {samples} = backupDownSampler(buffer, 1);
const {samples} = dropEveryOtherSample(buffer, 1);
expect(samples.length).toEqual(Math.floor(buffer.samples.length / 2));
});
test('result contains only even-index items', () => {
const {samples} = backupDownSampler(buffer, 1);
expect(samples.every(v => v === 1)).toBe(true);
const {samples} = dropEveryOtherSample(buffer, 1);
expect(samples).toEqual(new Float32Array([1, 2, 3]));
});
});

0 comments on commit d1d9833

Please sign in to comment.