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} 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 };