Spaces:
Running
Running
import WavEncoder from 'wav-encoder'; | |
export const SOUND_BYTE_LIMIT = 10 * 1000 * 1000; // 10mb | |
const _computeRMS = function (samples, start, end, scaling = 0.55) { | |
const length = end - start; | |
if (length === 0) return 0; | |
// Calculate RMS, adapted from https://github.com/Tonejs/Tone.js/blob/master/Tone/component/Meter.js#L88 | |
let sum = 0; | |
for (let i = start; i < end; i++) { | |
const sample = samples[i]; | |
sum += sample ** 2; | |
} | |
const rms = Math.sqrt(sum / length); | |
const val = rms / scaling; | |
return Math.sqrt(val); | |
}; | |
const computeRMS = (samples, scaling) => _computeRMS(samples, 0, samples.length, scaling); | |
const computeChunkedRMS = function (samples, chunkSize = 1024) { | |
const sampleCount = samples.length; | |
const chunkLevels = []; | |
for (let i = 0; i < sampleCount; i += chunkSize) { | |
const maxIndex = Math.min(sampleCount, i + chunkSize); | |
chunkLevels.push(_computeRMS(samples, i, maxIndex)); | |
} | |
return chunkLevels; | |
}; | |
const encodeAndAddSoundToVM = function (vm, samples, sampleRate, name, callback) { | |
WavEncoder.encode({ | |
sampleRate: sampleRate, | |
channelData: [samples] | |
}).then(wavBuffer => { | |
const vmSound = { | |
format: '', | |
dataFormat: 'wav', | |
rate: sampleRate, | |
sampleCount: samples.length | |
}; | |
// Create an asset from the encoded .wav and get resulting md5 | |
const storage = vm.runtime.storage; | |
vmSound.asset = storage.createAsset( | |
storage.AssetType.Sound, | |
storage.DataFormat.WAV, | |
new Uint8Array(wavBuffer), | |
null, | |
true // generate md5 | |
); | |
vmSound.assetId = vmSound.asset.assetId; | |
// update vmSound object with md5 property | |
vmSound.md5 = `${vmSound.assetId}.${vmSound.dataFormat}`; | |
// The VM will update the sound name to a fresh name | |
vmSound.name = name; | |
vm.addSound(vmSound).then(() => { | |
if (callback) callback(); | |
}); | |
}); | |
}; | |
/** | |
@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 encodedByteLength = samples.length * 2; /* bitDepth 16 bit */ | |
// Resolve immediately if already within byte limit | |
if (encodedByteLength < SOUND_BYTE_LIMIT) { | |
return Promise.resolve({samples, sampleRate}); | |
} | |
// TW: Don't check if the sound will still fit at this reduced sample rate. | |
// Instead the GUI will show a warning if it's too large. | |
return resampler({samples, sampleRate}, 22050); | |
}; | |
/** | |
* 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: buffer.sampleRate / 2 | |
}; | |
}; | |
export { | |
computeRMS, | |
computeChunkedRMS, | |
encodeAndAddSoundToVM, | |
downsampleIfNeeded, | |
dropEveryOtherSample | |
}; | |