From 3501e2e2f3da0505419eaf017a22d219deea68d6 Mon Sep 17 00:00:00 2001 From: erew123 <35898566+erew123@users.noreply.github.com> Date: Fri, 5 Apr 2024 10:26:33 +0100 Subject: [PATCH] Fix crunker bug over networks Crunker had an update to resolve bad performance over networks. This would affect only the TTS Generator when used remotely from AllTalk. --- system/tts_generator/crunker.d.ts | 358 ++++++++++++------------ system/tts_generator/crunker.esm.js | 2 +- system/tts_generator/crunker.esm.js.map | 2 +- 3 files changed, 181 insertions(+), 181 deletions(-) diff --git a/system/tts_generator/crunker.d.ts b/system/tts_generator/crunker.d.ts index 8aaae7b..4422095 100644 --- a/system/tts_generator/crunker.d.ts +++ b/system/tts_generator/crunker.d.ts @@ -1,179 +1,179 @@ -export interface CrunkerConstructorOptions { - /** - * Sample rate for Crunker's internal audio context. - * - * @default 44100 - */ - sampleRate: number; -} -export type CrunkerInputTypes = string | File | Blob; -/** - * An exported Crunker audio object. - */ -export interface ExportedCrunkerAudio { - blob: Blob; - url: string; - element: HTMLAudioElement; -} -/** - * Crunker is the simple way to merge, concatenate, play, export and download audio files using the Web Audio API. - */ -export default class Crunker { - private readonly _sampleRate; - private readonly _context; - /** - * Creates a new instance of Crunker with the provided options. - * - * If `sampleRate` is not defined, it will auto-select an appropriate sample rate - * for the device being used. - */ - constructor({ sampleRate }?: Partial); - /** - * Creates Crunker's internal AudioContext. - * - * @internal - */ - private _createContext; - /** - * - * The internal AudioContext used by Crunker. - */ - get context(): AudioContext; - /** - * Asynchronously fetches multiple audio files and returns an array of AudioBuffers. - */ - fetchAudio(...filepaths: CrunkerInputTypes[]): Promise; - /** - * Merges (layers) multiple AudioBuffers into a single AudioBuffer. - * - * **Visual representation:** - * - * ![](https://user-images.githubusercontent.com/12958674/88806278-968f0680-d186-11ea-9cb5-8ef2606ffcc7.png) - */ - mergeAudio(buffers: AudioBuffer[]): AudioBuffer; - /** - * Concatenates multiple AudioBuffers into a single AudioBuffer. - * - * **Visual representation:** - * - * ![](https://user-images.githubusercontent.com/12958674/88806297-9d1d7e00-d186-11ea-8cd2-c64cb0324845.png) - */ - concatAudio(buffers: AudioBuffer[]): AudioBuffer; - /** - * Pads a specified AudioBuffer with silence from a specified start time, - * for a specified length of time. - * - * Accepts float values as well as whole integers. - * - * @param buffer AudioBuffer to pad - * @param padStart Time to start padding (in seconds) - * @param seconds Duration to pad for (in seconds) - */ - padAudio(buffer: AudioBuffer, padStart?: number, seconds?: number): AudioBuffer; - /** - * Slices an AudioBuffer from the specified start time to the end time, with optional fade in and out. - * - * @param buffer AudioBuffer to slice - * @param start Start time (in seconds) - * @param end End time (in seconds) - * @param fadeIn Fade in duration (in seconds, default is 0) - * @param fadeOut Fade out duration (in seconds, default is 0) - */ - sliceAudio(buffer: AudioBuffer, start: number, end: number, fadeIn?: number, fadeOut?: number): AudioBuffer; - /** - * Plays the provided AudioBuffer in an AudioBufferSourceNode. - */ - play(buffer: AudioBuffer): AudioBufferSourceNode; - /** - * Exports the specified AudioBuffer to a Blob, Object URI and HTMLAudioElement. - * - * Note that changing the MIME type does not change the actual file format. The - * file format will **always** be a WAVE file due to how audio is stored in the - * browser. - * - * @param buffer Buffer to export - * @param type MIME type (default: `audio/wav`) - */ - export(buffer: AudioBuffer, type?: string): ExportedCrunkerAudio; - /** - * Downloads the provided Blob. - * - * @param blob Blob to download - * @param filename An optional file name to use for the download (default: `crunker`) - */ - download(blob: Blob, filename?: string): HTMLAnchorElement; - /** - * Executes a callback if the browser does not support the Web Audio API. - * - * Returns the result of the callback, or `undefined` if the Web Audio API is supported. - * - * @param callback callback to run if the browser does not support the Web Audio API - */ - notSupported(callback: () => T): T | undefined; - /** - * Closes Crunker's internal AudioContext. - */ - close(): this; - /** - * Returns the largest duration of the longest AudioBuffer. - * - * @internal - */ - private _maxDuration; - /** - * Returns the largest number of channels in an array of AudioBuffers. - * - * @internal - */ - private _maxNumberOfChannels; - /** - * Returns the sum of the lengths of an array of AudioBuffers. - * - * @internal - */ - private _totalLength; - /** - * Returns whether the browser supports the Web Audio API. - * - * @internal - */ - private _isSupported; - /** - * Writes the WAV headers for the specified Float32Array. - * - * Returns a DataView containing the WAV headers and file content. - * - * @internal - */ - private _writeHeaders; - /** - * Converts a Float32Array to 16-bit PCM. - * - * @internal - */ - private _floatTo16BitPCM; - /** - * Writes a string to a DataView at the specified offset. - * - * @internal - */ - private _writeString; - /** - * Converts an AudioBuffer to a Float32Array. - * - * @internal - */ - private _interleave; - /** - * Creates an HTMLAudioElement whose source is the specified Blob. - * - * @internal - */ - private _renderAudioElement; - /** - * Creates an Object URL for the specified Blob. - * - * @internal - */ - private _renderURL; -} \ No newline at end of file +export interface CrunkerConstructorOptions { + /** + * Sample rate for Crunker's internal audio context. + * + * @default 44100 + */ + sampleRate: number; +} +export type CrunkerInputTypes = string | File | Blob; +/** + * An exported Crunker audio object. + */ +export interface ExportedCrunkerAudio { + blob: Blob; + url: string; + element: HTMLAudioElement; +} +/** + * Crunker is the simple way to merge, concatenate, play, export and download audio files using the Web Audio API. + */ +export default class Crunker { + private readonly _sampleRate; + private readonly _context; + /** + * Creates a new instance of Crunker with the provided options. + * + * If `sampleRate` is not defined, it will auto-select an appropriate sample rate + * for the device being used. + */ + constructor({ sampleRate }?: Partial); + /** + * Creates Crunker's internal AudioContext. + * + * @internal + */ + private _createContext; + /** + * + * The internal AudioContext used by Crunker. + */ + get context(): AudioContext; + /** + * Asynchronously fetches multiple audio files and returns an array of AudioBuffers. + */ + fetchAudio(...filepaths: CrunkerInputTypes[]): Promise; + /** + * Merges (layers) multiple AudioBuffers into a single AudioBuffer. + * + * **Visual representation:** + * + * ![](https://user-images.githubusercontent.com/12958674/88806278-968f0680-d186-11ea-9cb5-8ef2606ffcc7.png) + */ + mergeAudio(buffers: AudioBuffer[]): AudioBuffer; + /** + * Concatenates multiple AudioBuffers into a single AudioBuffer. + * + * **Visual representation:** + * + * ![](https://user-images.githubusercontent.com/12958674/88806297-9d1d7e00-d186-11ea-8cd2-c64cb0324845.png) + */ + concatAudio(buffers: AudioBuffer[]): AudioBuffer; + /** + * Pads a specified AudioBuffer with silence from a specified start time, + * for a specified length of time. + * + * Accepts float values as well as whole integers. + * + * @param buffer AudioBuffer to pad + * @param padStart Time to start padding (in seconds) + * @param seconds Duration to pad for (in seconds) + */ + padAudio(buffer: AudioBuffer, padStart?: number, seconds?: number): AudioBuffer; + /** + * Slices an AudioBuffer from the specified start time to the end time, with optional fade in and out. + * + * @param buffer AudioBuffer to slice + * @param start Start time (in seconds) + * @param end End time (in seconds) + * @param fadeIn Fade in duration (in seconds, default is 0) + * @param fadeOut Fade out duration (in seconds, default is 0) + */ + sliceAudio(buffer: AudioBuffer, start: number, end: number, fadeIn?: number, fadeOut?: number): AudioBuffer; + /** + * Plays the provided AudioBuffer in an AudioBufferSourceNode. + */ + play(buffer: AudioBuffer): AudioBufferSourceNode; + /** + * Exports the specified AudioBuffer to a Blob, Object URI and HTMLAudioElement. + * + * Note that changing the MIME type does not change the actual file format. The + * file format will **always** be a WAVE file due to how audio is stored in the + * browser. + * + * @param buffer Buffer to export + * @param type MIME type (default: `audio/wav`) + */ + export(buffer: AudioBuffer, type?: string): ExportedCrunkerAudio; + /** + * Downloads the provided Blob. + * + * @param blob Blob to download + * @param filename An optional file name to use for the download (default: `crunker`) + */ + download(blob: Blob, filename?: string): HTMLAnchorElement; + /** + * Executes a callback if the browser does not support the Web Audio API. + * + * Returns the result of the callback, or `undefined` if the Web Audio API is supported. + * + * @param callback callback to run if the browser does not support the Web Audio API + */ + notSupported(callback: () => T): T | undefined; + /** + * Closes Crunker's internal AudioContext. + */ + close(): this; + /** + * Returns the largest duration of the longest AudioBuffer. + * + * @internal + */ + private _maxDuration; + /** + * Returns the largest number of channels in an array of AudioBuffers. + * + * @internal + */ + private _maxNumberOfChannels; + /** + * Returns the sum of the lengths of an array of AudioBuffers. + * + * @internal + */ + private _totalLength; + /** + * Returns whether the browser supports the Web Audio API. + * + * @internal + */ + private _isSupported; + /** + * Writes the WAV headers for the specified Float32Array. + * + * Returns a DataView containing the WAV headers and file content. + * + * @internal + */ + private _writeHeaders; + /** + * Converts a Float32Array to 16-bit PCM. + * + * @internal + */ + private _floatTo16BitPCM; + /** + * Writes a string to a DataView at the specified offset. + * + * @internal + */ + private _writeString; + /** + * Converts an AudioBuffer to a Float32Array. + * + * @internal + */ + private _interleave; + /** + * Creates an HTMLAudioElement whose source is the specified Blob. + * + * @internal + */ + private _renderAudioElement; + /** + * Creates an Object URL for the specified Blob. + * + * @internal + */ + private _renderURL; +} diff --git a/system/tts_generator/crunker.esm.js b/system/tts_generator/crunker.esm.js index 6680da4..a6f5633 100644 --- a/system/tts_generator/crunker.esm.js +++ b/system/tts_generator/crunker.esm.js @@ -1,2 +1,2 @@ -var e={d:(t,n)=>{for(var r in n)e.o(n,r)&&!e.o(t,r)&&Object.defineProperty(t,r,{enumerable:!0,get:n[r]})},o:(e,t)=>Object.prototype.hasOwnProperty.call(e,t)},t={};function n(e){return n="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(e){return typeof e}:function(e){return e&&"function"==typeof Symbol&&e.constructor===Symbol&&e!==Symbol.prototype?"symbol":typeof e},n(e)}function r(e){return function(e){if(Array.isArray(e))return a(e)}(e)||function(e){if("undefined"!=typeof Symbol&&null!=e[Symbol.iterator]||null!=e["@@iterator"])return Array.from(e)}(e)||function(e,t){if(!e)return;if("string"==typeof e)return a(e,t);var n=Object.prototype.toString.call(e).slice(8,-1);"Object"===n&&e.constructor&&(n=e.constructor.name);if("Map"===n||"Set"===n)return Array.from(e);if("Arguments"===n||/^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n))return a(e,t)}(e)||function(){throw new TypeError("Invalid attempt to spread non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.")}()}function a(e,t){(null==t||t>e.length)&&(t=e.length);for(var n=0,r=new Array(t);nu});var u=function(){function e(){var t=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{},n=t.sampleRate;o(this,e),this._context=this._createContext(n),n||(n=this._context.sampleRate),this._sampleRate=n}var t,n,a;return t=e,n=[{key:"_createContext",value:function(){var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:44100;return window.AudioContext=window.AudioContext||window.webkitAudioContext||window.mozAudioContext,new AudioContext({sampleRate:e})}},{key:"context",get:function(){return this._context}},{key:"fetchAudio",value:async function(){for(var e=this,t=arguments.length,n=new Array(t),r=0;r=0;o--)r[o]+=a[o];t.getChannelData(n).set(r)}})),t}},{key:"concatAudio",value:function(e){var t=this._context.createBuffer(this._maxNumberOfChannels(e),this._totalLength(e),this._sampleRate),n=0;return e.forEach((function(e){for(var r=0;r1&&void 0!==arguments[1]?arguments[1]:0,n=arguments.length>2&&void 0!==arguments[2]?arguments[2]:0;if(0===n)return e;if(t<0)throw new Error('Crunker: Parameter "padStart" in padAudio must be positive');if(n<0)throw new Error('Crunker: Parameter "seconds" in padAudio must be positive');for(var r=this._context.createBuffer(e.numberOfChannels,Math.ceil(e.length+n*e.sampleRate),e.sampleRate),a=0;a3&&void 0!==arguments[3]?arguments[3]:0,a=arguments.length>4&&void 0!==arguments[4]?arguments[4]:0;if(t>=n)throw new Error('Crunker: "start" time should be less than "end" time in sliceAudio method');for(var o=Math.round((n-t)*this._sampleRate),i=Math.round(t*this._sampleRate),u=this._context.createBuffer(e.numberOfChannels,o,this._sampleRate),l=0;lo-a*this._sampleRate&&(c[f]*=(o-f)/(a*this._sampleRate));return u}},{key:"play",value:function(e){var t=this._context.createBufferSource();return t.buffer=e,t.connect(this._context.destination),t.start(),t}},{key:"export",value:function(e){var t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:"audio/wav",n=this._interleave(e),r=this._writeHeaders(n,e.numberOfChannels,e.sampleRate),a=new Blob([r],{type:t});return{blob:a,url:this._renderURL(a),element:this._renderAudioElement(a)}}},{key:"download",value:function(e){var t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:"crunker",n=document.createElement("a");return n.style.display="none",n.href=this._renderURL(e),n.download="".concat(t,".").concat(e.type.split("/")[1]),n.click(),n}},{key:"notSupported",value:function(e){return this._isSupported()?void 0:e()}},{key:"close",value:function(){return this._context.close(),this}},{key:"_maxDuration",value:function(e){return Math.max.apply(Math,r(e.map((function(e){return e.duration}))))}},{key:"_maxNumberOfChannels",value:function(e){return Math.max.apply(Math,r(e.map((function(e){return e.numberOfChannels}))))}},{key:"_totalLength",value:function(e){return e.map((function(e){return e.length})).reduce((function(e,t){return e+t}),0)}},{key:"_isSupported",value:function(){return"AudioContext"in window||"webkitAudioContext"in window||"mozAudioContext"in window}},{key:"_writeHeaders",value:function(e,t,n){var r=2*t,a=2*e.length,o=36+a,i=new ArrayBuffer(8+o),u=new DataView(i);return this._writeString(u,0,"RIFF"),u.setUint32(4,o,!0),this._writeString(u,8,"WAVE"),this._writeString(u,12,"fmt "),u.setUint32(16,16,!0),u.setUint16(20,1,!0),u.setUint16(22,t,!0),u.setUint32(24,n,!0),u.setUint32(28,n*r,!0),u.setUint16(32,r,!0),u.setUint16(34,16,!0),this._writeString(u,36,"data"),u.setUint32(40,a,!0),this._floatTo16BitPCM(u,e,44)}},{key:"_floatTo16BitPCM",value:function(e,t,n){for(var r=0;r{for(var r in n)e.o(n,r)&&!e.o(t,r)&&Object.defineProperty(t,r,{enumerable:!0,get:n[r]})},o:(e,t)=>Object.prototype.hasOwnProperty.call(e,t)},t={};function n(e){return n="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(e){return typeof e}:function(e){return e&&"function"==typeof Symbol&&e.constructor===Symbol&&e!==Symbol.prototype?"symbol":typeof e},n(e)}function r(e){return function(e){if(Array.isArray(e))return a(e)}(e)||function(e){if("undefined"!=typeof Symbol&&null!=e[Symbol.iterator]||null!=e["@@iterator"])return Array.from(e)}(e)||function(e,t){if(!e)return;if("string"==typeof e)return a(e,t);var n=Object.prototype.toString.call(e).slice(8,-1);"Object"===n&&e.constructor&&(n=e.constructor.name);if("Map"===n||"Set"===n)return Array.from(e);if("Arguments"===n||/^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n))return a(e,t)}(e)||function(){throw new TypeError("Invalid attempt to spread non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.")}()}function a(e,t){(null==t||t>e.length)&&(t=e.length);for(var n=0,r=new Array(t);nu});var u=function(){function e(){var t=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{},n=t.sampleRate;o(this,e),this._context=this._createContext(n),n||(n=this._context.sampleRate),this._sampleRate=n}var t,n,a;return t=e,n=[{key:"_createContext",value:function(){var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:44100;return window.AudioContext=window.AudioContext||window.webkitAudioContext||window.mozAudioContext,new AudioContext({sampleRate:e})}},{key:"context",get:function(){return this._context}},{key:"fetchAudio",value:async function(){for(var e=this,t=arguments.length,n=new Array(t),r=0;r=0;o--)r[o]+=a[o];t.getChannelData(n).set(r)}})),t}},{key:"concatAudio",value:function(e){var t=this._context.createBuffer(this._maxNumberOfChannels(e),this._totalLength(e),this._sampleRate),n=0;return e.forEach((function(e){for(var r=0;r1&&void 0!==arguments[1]?arguments[1]:0,n=arguments.length>2&&void 0!==arguments[2]?arguments[2]:0;if(0===n)return e;if(t<0)throw new Error('Crunker: Parameter "padStart" in padAudio must be positive');if(n<0)throw new Error('Crunker: Parameter "seconds" in padAudio must be positive');for(var r=this._context.createBuffer(e.numberOfChannels,Math.ceil(e.length+n*e.sampleRate),e.sampleRate),a=0;a3&&void 0!==arguments[3]?arguments[3]:0,a=arguments.length>4&&void 0!==arguments[4]?arguments[4]:0;if(t>=n)throw new Error('Crunker: "start" time should be less than "end" time in sliceAudio method');for(var o=Math.round((n-t)*this._sampleRate),i=Math.round(t*this._sampleRate),u=this._context.createBuffer(e.numberOfChannels,o,this._sampleRate),l=0;lo-a*this._sampleRate&&(c[f]*=(o-f)/(a*this._sampleRate));return u}},{key:"play",value:function(e){var t=this._context.createBufferSource();return t.buffer=e,t.connect(this._context.destination),t.start(),t}},{key:"export",value:function(e){var t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:"audio/wav",n=this._interleave(e),r=this._writeHeaders(n,e.numberOfChannels,e.sampleRate),a=new Blob([r],{type:t});return{blob:a,url:this._renderURL(a),element:this._renderAudioElement(a)}}},{key:"download",value:function(e){var t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:"crunker",n=document.createElement("a");return n.style.display="none",n.href=this._renderURL(e),n.download="".concat(t,".").concat(e.type.split("/")[1]),n.click(),n}},{key:"notSupported",value:function(e){return this._isSupported()?void 0:e()}},{key:"close",value:function(){return this._context.close(),this}},{key:"_maxDuration",value:function(e){return Math.max.apply(Math,r(e.map((function(e){return e.duration}))))}},{key:"_maxNumberOfChannels",value:function(e){return Math.max.apply(Math,r(e.map((function(e){return e.numberOfChannels}))))}},{key:"_totalLength",value:function(e){return e.map((function(e){return e.length})).reduce((function(e,t){return e+t}),0)}},{key:"_isSupported",value:function(){return"AudioContext"in window||"webkitAudioContext"in window||"mozAudioContext"in window}},{key:"_writeHeaders",value:function(e,t,n){var r=2*t,a=2*e.length,o=36+a,i=new ArrayBuffer(8+o),u=new DataView(i);return this._writeString(u,0,"RIFF"),u.setUint32(4,o,!0),this._writeString(u,8,"WAVE"),this._writeString(u,12,"fmt "),u.setUint32(16,16,!0),u.setUint16(20,1,!0),u.setUint16(22,t,!0),u.setUint32(24,n,!0),u.setUint32(28,n*r,!0),u.setUint16(32,r,!0),u.setUint16(34,16,!0),this._writeString(u,36,"data"),u.setUint32(40,a,!0),this._floatTo16BitPCM(u,e,44)}},{key:"_floatTo16BitPCM",value:function(e,t,n){for(var r=0;r {\n\tfor(var key in definition) {\n\t\tif(__webpack_require__.o(definition, key) && !__webpack_require__.o(exports, key)) {\n\t\t\tObject.defineProperty(exports, key, { enumerable: true, get: definition[key] });\n\t\t}\n\t}\n};","__webpack_require__.o = (obj, prop) => (Object.prototype.hasOwnProperty.call(obj, prop))","export interface CrunkerConstructorOptions {\n /**\n * Sample rate for Crunker's internal audio context.\n *\n * @default 44100\n */\n sampleRate: number;\n}\n\nexport type CrunkerInputTypes = string | File | Blob;\n\n/**\n * An exported Crunker audio object.\n */\nexport interface ExportedCrunkerAudio {\n blob: Blob;\n url: string;\n element: HTMLAudioElement;\n}\n\n/**\n * Crunker is the simple way to merge, concatenate, play, export and download audio files using the Web Audio API.\n */\nexport default class Crunker {\n private readonly _sampleRate: number;\n private readonly _context: AudioContext;\n\n /**\n * Creates a new instance of Crunker with the provided options.\n *\n * If `sampleRate` is not defined, it will auto-select an appropriate sample rate\n * for the device being used.\n */\n constructor({ sampleRate }: Partial = {}) {\n this._context = this._createContext(sampleRate);\n\n sampleRate ||= this._context.sampleRate;\n\n this._sampleRate = sampleRate;\n }\n\n /**\n * Creates Crunker's internal AudioContext.\n *\n * @internal\n */\n private _createContext(sampleRate: number = 44_100): AudioContext {\n window.AudioContext = window.AudioContext || (window as any).webkitAudioContext || (window as any).mozAudioContext;\n return new AudioContext({ sampleRate });\n }\n\n /**\n *\n * The internal AudioContext used by Crunker.\n */\n get context(): AudioContext {\n return this._context;\n }\n\n /**\n * Asynchronously fetches multiple audio files and returns an array of AudioBuffers.\n */\n async fetchAudio(...filepaths: CrunkerInputTypes[]): Promise {\n return await Promise.all(\n filepaths.map(async (filepath) => {\n let buffer: ArrayBuffer;\n\n if (filepath instanceof File || filepath instanceof Blob) {\n buffer = await filepath.arrayBuffer();\n } else {\n buffer = await fetch(filepath).then((response) => {\n if (response.headers.has('Content-Type') && !response.headers.get('Content-Type')!.includes('audio/')) {\n console.warn(\n `Crunker: Attempted to fetch an audio file, but its MIME type is \\`${\n response.headers.get('Content-Type')!.split(';')[0]\n }\\`. We'll try and continue anyway. (file: \"${filepath}\")`\n );\n }\n\n return response.arrayBuffer();\n });\n }\n\n return await this._context.decodeAudioData(buffer);\n })\n );\n }\n\n /**\n * Merges (layers) multiple AudioBuffers into a single AudioBuffer.\n *\n * **Visual representation:**\n *\n * ![](https://user-images.githubusercontent.com/12958674/88806278-968f0680-d186-11ea-9cb5-8ef2606ffcc7.png)\n */\n mergeAudio(buffers: AudioBuffer[]): AudioBuffer {\n const output = this._context.createBuffer(\n this._maxNumberOfChannels(buffers),\n this._sampleRate * this._maxDuration(buffers),\n this._sampleRate\n );\n\n buffers.forEach((buffer) => {\n for (let channelNumber = 0; channelNumber < buffer.numberOfChannels; channelNumber++) {\n const outputData = output.getChannelData(channelNumber);\n const bufferData = buffer.getChannelData(channelNumber);\n\n for (let i = buffer.getChannelData(channelNumber).length - 1; i >= 0; i--) {\n outputData[i] += bufferData[i];\n }\n\n output.getChannelData(channelNumber).set(outputData);\n }\n });\n\n return output;\n }\n\n /**\n * Concatenates multiple AudioBuffers into a single AudioBuffer.\n *\n * **Visual representation:**\n *\n * ![](https://user-images.githubusercontent.com/12958674/88806297-9d1d7e00-d186-11ea-8cd2-c64cb0324845.png)\n */\n concatAudio(buffers: AudioBuffer[]): AudioBuffer {\n const output = this._context.createBuffer(\n this._maxNumberOfChannels(buffers),\n this._totalLength(buffers),\n this._sampleRate\n );\n let offset = 0;\n\n buffers.forEach((buffer) => {\n for (let channelNumber = 0; channelNumber < buffer.numberOfChannels; channelNumber++) {\n output.getChannelData(channelNumber).set(buffer.getChannelData(channelNumber), offset);\n }\n\n offset += buffer.length;\n });\n\n return output;\n }\n\n /**\n * Pads a specified AudioBuffer with silence from a specified start time,\n * for a specified length of time.\n *\n * Accepts float values as well as whole integers.\n *\n * @param buffer AudioBuffer to pad\n * @param padStart Time to start padding (in seconds)\n * @param seconds Duration to pad for (in seconds)\n */\n padAudio(buffer: AudioBuffer, padStart: number = 0, seconds: number = 0): AudioBuffer {\n if (seconds === 0) return buffer;\n\n if (padStart < 0) throw new Error('Crunker: Parameter \"padStart\" in padAudio must be positive');\n if (seconds < 0) throw new Error('Crunker: Parameter \"seconds\" in padAudio must be positive');\n\n const updatedBuffer = this._context.createBuffer(\n buffer.numberOfChannels,\n Math.ceil(buffer.length + seconds * buffer.sampleRate),\n buffer.sampleRate\n );\n\n for (let channelNumber = 0; channelNumber < buffer.numberOfChannels; channelNumber++) {\n const channelData = buffer.getChannelData(channelNumber);\n updatedBuffer\n .getChannelData(channelNumber)\n .set(channelData.subarray(0, Math.ceil(padStart * buffer.sampleRate) + 1), 0);\n\n updatedBuffer\n .getChannelData(channelNumber)\n .set(\n channelData.subarray(Math.ceil(padStart * buffer.sampleRate) + 2, updatedBuffer.length + 1),\n Math.ceil((padStart + seconds) * buffer.sampleRate)\n );\n }\n\n return updatedBuffer;\n }\n\n /**\n * Slices an AudioBuffer from the specified start time to the end time, with optional fade in and out.\n *\n * @param buffer AudioBuffer to slice\n * @param start Start time (in seconds)\n * @param end End time (in seconds)\n * @param fadeIn Fade in duration (in seconds, default is 0)\n * @param fadeOut Fade out duration (in seconds, default is 0)\n */\n sliceAudio(buffer: AudioBuffer, start: number, end: number, fadeIn: number = 0, fadeOut: number = 0): AudioBuffer {\n if (start >= end) throw new Error('Crunker: \"start\" time should be less than \"end\" time in sliceAudio method');\n\n const length = Math.round((end - start) * this._sampleRate);\n const offset = Math.round(start * this._sampleRate);\n const newBuffer = this._context.createBuffer(buffer.numberOfChannels, length, this._sampleRate);\n\n for (let channel = 0; channel < buffer.numberOfChannels; channel++) {\n const inputData = buffer.getChannelData(channel);\n const outputData = newBuffer.getChannelData(channel);\n\n for (let i = 0; i < length; i++) {\n outputData[i] = inputData[offset + i];\n\n // Apply fade in\n if (i < fadeIn * this._sampleRate) {\n outputData[i] *= i / (fadeIn * this._sampleRate);\n }\n\n // Apply fade out\n if (i > length - fadeOut * this._sampleRate) {\n outputData[i] *= (length - i) / (fadeOut * this._sampleRate);\n }\n }\n }\n\n return newBuffer;\n }\n\n /**\n * Plays the provided AudioBuffer in an AudioBufferSourceNode.\n */\n play(buffer: AudioBuffer): AudioBufferSourceNode {\n const source = this._context.createBufferSource();\n\n source.buffer = buffer;\n source.connect(this._context.destination);\n source.start();\n\n return source;\n }\n\n /**\n * Exports the specified AudioBuffer to a Blob, Object URI and HTMLAudioElement.\n *\n * Note that changing the MIME type does not change the actual file format. The\n * file format will **always** be a WAVE file due to how audio is stored in the\n * browser.\n *\n * @param buffer Buffer to export\n * @param type MIME type (default: `audio/wav`)\n */\n export(buffer: AudioBuffer, type: string = 'audio/wav'): ExportedCrunkerAudio {\n const recorded = this._interleave(buffer);\n const dataview = this._writeHeaders(recorded, buffer.numberOfChannels, buffer.sampleRate);\n const audioBlob = new Blob([dataview], { type });\n\n return {\n blob: audioBlob,\n url: this._renderURL(audioBlob),\n element: this._renderAudioElement(audioBlob),\n };\n }\n\n /**\n * Downloads the provided Blob.\n *\n * @param blob Blob to download\n * @param filename An optional file name to use for the download (default: `crunker`)\n */\n download(blob: Blob, filename: string = 'crunker'): HTMLAnchorElement {\n const a = document.createElement('a');\n\n a.style.display = 'none';\n a.href = this._renderURL(blob);\n a.download = `${filename}.${blob.type.split('/')[1]}`;\n a.click();\n\n return a;\n }\n\n /**\n * Executes a callback if the browser does not support the Web Audio API.\n *\n * Returns the result of the callback, or `undefined` if the Web Audio API is supported.\n *\n * @param callback callback to run if the browser does not support the Web Audio API\n */\n notSupported(callback: () => T): T | undefined {\n return this._isSupported() ? undefined : callback();\n }\n\n /**\n * Closes Crunker's internal AudioContext.\n */\n close(): this {\n this._context.close();\n return this;\n }\n\n /**\n * Returns the largest duration of the longest AudioBuffer.\n *\n * @internal\n */\n private _maxDuration(buffers: AudioBuffer[]): number {\n return Math.max(...buffers.map((buffer) => buffer.duration));\n }\n\n /**\n * Returns the largest number of channels in an array of AudioBuffers.\n *\n * @internal\n */\n private _maxNumberOfChannels(buffers: AudioBuffer[]): number {\n return Math.max(...buffers.map((buffer) => buffer.numberOfChannels));\n }\n\n /**\n * Returns the sum of the lengths of an array of AudioBuffers.\n *\n * @internal\n */\n private _totalLength(buffers: AudioBuffer[]): number {\n return buffers.map((buffer) => buffer.length).reduce((a, b) => a + b, 0);\n }\n\n /**\n * Returns whether the browser supports the Web Audio API.\n *\n * @internal\n */\n private _isSupported(): boolean {\n return 'AudioContext' in window || 'webkitAudioContext' in window || 'mozAudioContext' in window;\n }\n\n /**\n * Writes the WAV headers for the specified Float32Array.\n *\n * Returns a DataView containing the WAV headers and file content.\n *\n * @internal\n */\n private _writeHeaders(buffer: Float32Array, numOfChannels: number, sampleRate: number): DataView {\n const bitDepth = 16;\n const bytesPerSample = bitDepth / 8;\n const sampleSize = numOfChannels * bytesPerSample;\n\n const fileHeaderSize = 8;\n const chunkHeaderSize = 36;\n const chunkDataSize = buffer.length * bytesPerSample;\n const chunkTotalSize = chunkHeaderSize + chunkDataSize;\n\n const arrayBuffer = new ArrayBuffer(fileHeaderSize + chunkTotalSize);\n const view = new DataView(arrayBuffer);\n\n this._writeString(view, 0, 'RIFF');\n view.setUint32(4, chunkTotalSize, true);\n this._writeString(view, 8, 'WAVE');\n this._writeString(view, 12, 'fmt ');\n view.setUint32(16, 16, true);\n view.setUint16(20, 1, true);\n view.setUint16(22, numOfChannels, true);\n view.setUint32(24, sampleRate, true);\n view.setUint32(28, sampleRate * sampleSize, true);\n view.setUint16(32, sampleSize, true);\n view.setUint16(34, bitDepth, true);\n this._writeString(view, 36, 'data');\n view.setUint32(40, chunkDataSize, true);\n\n return this._floatTo16BitPCM(view, buffer, fileHeaderSize + chunkHeaderSize);\n }\n\n /**\n * Converts a Float32Array to 16-bit PCM.\n *\n * @internal\n */\n private _floatTo16BitPCM(dataview: DataView, buffer: Float32Array, offset: number): DataView {\n for (let i = 0; i < buffer.length; i++, offset += 2) {\n const tmp = Math.max(-1, Math.min(1, buffer[i]));\n dataview.setInt16(offset, tmp < 0 ? tmp * 0x8000 : tmp * 0x7fff, true);\n }\n\n return dataview;\n }\n\n /**\n * Writes a string to a DataView at the specified offset.\n *\n * @internal\n */\n private _writeString(dataview: DataView, offset: number, header: string): void {\n for (let i = 0; i < header.length; i++) {\n dataview.setUint8(offset + i, header.charCodeAt(i));\n }\n }\n\n /**\n * Converts an AudioBuffer to a Float32Array.\n *\n * @internal\n */\n private _interleave(input: AudioBuffer): Float32Array {\n const channels = Array.from({ length: input.numberOfChannels }, (_, i) => i);\n const length = channels.reduce((prev, channelIdx) => prev + input.getChannelData(channelIdx).length, 0);\n const result = new Float32Array(length);\n\n let index = 0;\n let inputIndex = 0;\n\n // for 2 channels its like: [L[0], R[0], L[1], R[1], ... , L[n], R[n]]\n while (index < length) {\n channels.forEach((channelIdx) => {\n result[index++] = input.getChannelData(channelIdx)[inputIndex];\n });\n\n inputIndex++;\n }\n\n return result;\n }\n\n /**\n * Creates an HTMLAudioElement whose source is the specified Blob.\n *\n * @internal\n */\n private _renderAudioElement(blob: Blob): HTMLAudioElement {\n const audio = document.createElement('audio');\n\n audio.controls = true;\n audio.src = this._renderURL(blob);\n\n return audio;\n }\n\n /**\n * Creates an Object URL for the specified Blob.\n *\n * @internal\n */\n private _renderURL(blob: Blob): string {\n return (window.URL || window.webkitURL).createObjectURL(blob);\n }\n}\n"],"names":["__webpack_require__","exports","definition","key","o","Object","defineProperty","enumerable","get","obj","prop","prototype","hasOwnProperty","call","Crunker","sampleRate","this","_context","_createContext","_sampleRate","window","AudioContext","webkitAudioContext","mozAudioContext","async","filepaths","Promise","all","map","filepath","buffer","File","Blob","arrayBuffer","fetch","then","response","headers","has","includes","console","warn","split","decodeAudioData","buffers","output","createBuffer","_maxNumberOfChannels","_maxDuration","forEach","channelNumber","numberOfChannels","outputData","getChannelData","bufferData","i","length","set","_totalLength","offset","padStart","seconds","Error","updatedBuffer","Math","ceil","channelData","subarray","start","end","fadeIn","fadeOut","round","newBuffer","channel","inputData","source","createBufferSource","connect","destination","type","recorded","_interleave","dataview","_writeHeaders","audioBlob","blob","url","_renderURL","element","_renderAudioElement","filename","a","document","createElement","style","display","href","download","click","callback","_isSupported","undefined","close","max","duration","reduce","b","numOfChannels","sampleSize","bitDepth","chunkDataSize","chunkTotalSize","ArrayBuffer","view","DataView","_writeString","setUint32","setUint16","_floatTo16BitPCM","fileHeaderSize","tmp","min","setInt16","header","setUint8","charCodeAt","input","channels","Array","from","_","prev","channelIdx","result","Float32Array","index","inputIndex","audio","controls","src","URL","webkitURL","createObjectURL"],"sourceRoot":""} \ No newline at end of file +{"version":3,"file":"crunker.esm.js","mappings":"AACA,IAAIA,EAAsB,CCA1BA,EAAwB,CAACC,EAASC,KACjC,IAAI,IAAIC,KAAOD,EACXF,EAAoBI,EAAEF,EAAYC,KAASH,EAAoBI,EAAEH,EAASE,IAC5EE,OAAOC,eAAeL,EAASE,EAAK,CAAEI,YAAY,EAAMC,IAAKN,EAAWC,IAE1E,ECNDH,EAAwB,CAACS,EAAKC,IAAUL,OAAOM,UAAUC,eAAeC,KAAKJ,EAAKC,I,8jDCoBlF,IAGqBI,EAAO,WAU1B,aAAqE,6DAAJ,CAAC,EAApDC,EAAU,EAAVA,WAAU,UACtBC,KAAKC,SAAWD,KAAKE,eAAeH,GAEpCA,IAAAA,EAAeC,KAAKC,SAASF,YAE7BC,KAAKG,YAAcJ,CACrB,C,UAoZC,O,EAlZD,E,EAAA,6BAKA,WAAkE,IAA3CA,EAAqB,UAAH,6CAAG,MAE1C,OADAK,OAAOC,aAAeD,OAAOC,cAAiBD,OAAeE,oBAAuBF,OAAeG,gBAC5F,IAAIF,aAAa,CAAEN,WAAAA,GAC5B,GAEA,mBAIA,WACE,OAAOC,KAAKC,QACd,GAEA,wBAGAO,iBAA4E,kCAAxDC,EAAS,yBAATA,EAAS,gBAC3B,aAAaC,QAAQC,IACnBF,EAAUG,KAAIJ,eAAOK,GACnB,IAAIC,EAkBJ,OAfEA,EADED,aAAoBE,MAAQF,aAAoBG,WACnCH,EAASI,oBAETC,MAAML,GAAUM,MAAK,SAACC,GASnC,OARIA,EAASC,QAAQC,IAAI,kBAAoBF,EAASC,QAAQ7B,IAAI,gBAAiB+B,SAAS,WAC1FC,QAAQC,KAAK,oEAAD,OAERL,EAASC,QAAQ7B,IAAI,gBAAiBkC,MAAM,KAAK,GAAE,sDACPb,EAAQ,OAInDO,EAASH,aAClB,UAGW,EAAKhB,SAAS0B,gBAAgBb,EAC7C,IAEJ,GAEA,wBAOA,SAAWc,GACT,IAAMC,EAAS7B,KAAKC,SAAS6B,aAC3B9B,KAAK+B,qBAAqBH,GAC1B5B,KAAKG,YAAcH,KAAKgC,aAAaJ,GACrC5B,KAAKG,aAgBP,OAbAyB,EAAQK,SAAQ,SAACnB,GACf,IAAK,IAAIoB,EAAgB,EAAGA,EAAgBpB,EAAOqB,iBAAkBD,IAAiB,CAIpF,IAHA,IAAME,EAAaP,EAAOQ,eAAeH,GACnCI,EAAaxB,EAAOuB,eAAeH,GAEhCK,EAAIzB,EAAOuB,eAAeH,GAAeM,OAAS,EAAGD,GAAK,EAAGA,IACpEH,EAAWG,IAAMD,EAAWC,GAG9BV,EAAOQ,eAAeH,GAAeO,IAAIL,EAC3C,CACF,IAEOP,CACT,GAEA,yBAOA,SAAYD,GACV,IAAMC,EAAS7B,KAAKC,SAAS6B,aAC3B9B,KAAK+B,qBAAqBH,GAC1B5B,KAAK0C,aAAad,GAClB5B,KAAKG,aAEHwC,EAAS,EAUb,OARAf,EAAQK,SAAQ,SAACnB,GACf,IAAK,IAAIoB,EAAgB,EAAGA,EAAgBpB,EAAOqB,iBAAkBD,IACnEL,EAAOQ,eAAeH,GAAeO,IAAI3B,EAAOuB,eAAeH,GAAgBS,GAGjFA,GAAU7B,EAAO0B,MACnB,IAEOX,CACT,GAEA,sBAUA,SAASf,GAA6E,IAAxD8B,EAAmB,UAAH,6CAAG,EAAGC,EAAkB,UAAH,6CAAG,EACpE,GAAgB,IAAZA,EAAe,OAAO/B,EAE1B,GAAI8B,EAAW,EAAG,MAAM,IAAIE,MAAM,8DAClC,GAAID,EAAU,EAAG,MAAM,IAAIC,MAAM,6DAQjC,IANA,IAAMC,EAAgB/C,KAAKC,SAAS6B,aAClChB,EAAOqB,iBACPa,KAAKC,KAAKnC,EAAO0B,OAASK,EAAU/B,EAAOf,YAC3Ce,EAAOf,YAGAmC,EAAgB,EAAGA,EAAgBpB,EAAOqB,iBAAkBD,IAAiB,CACpF,IAAMgB,EAAcpC,EAAOuB,eAAeH,GAC1Ca,EACGV,eAAeH,GACfO,IAAIS,EAAYC,SAAS,EAAGH,KAAKC,KAAKL,EAAW9B,EAAOf,YAAc,GAAI,GAE7EgD,EACGV,eAAeH,GACfO,IACCS,EAAYC,SAASH,KAAKC,KAAKL,EAAW9B,EAAOf,YAAc,EAAGgD,EAAcP,OAAS,GACzFQ,KAAKC,MAAML,EAAWC,GAAW/B,EAAOf,YAE9C,CAEA,OAAOgD,CACT,GAEA,wBASA,SAAWjC,EAAqBsC,EAAeC,GAAmE,IAAtDC,EAAiB,UAAH,6CAAG,EAAGC,EAAkB,UAAH,6CAAG,EAChG,GAAIH,GAASC,EAAK,MAAM,IAAIP,MAAM,6EAMlC,IAJA,IAAMN,EAASQ,KAAKQ,OAAOH,EAAMD,GAASpD,KAAKG,aACzCwC,EAASK,KAAKQ,MAAMJ,EAAQpD,KAAKG,aACjCsD,EAAYzD,KAAKC,SAAS6B,aAAahB,EAAOqB,iBAAkBK,EAAQxC,KAAKG,aAE1EuD,EAAU,EAAGA,EAAU5C,EAAOqB,iBAAkBuB,IAIvD,IAHA,IAAMC,EAAY7C,EAAOuB,eAAeqB,GAClCtB,EAAaqB,EAAUpB,eAAeqB,GAEnCnB,EAAI,EAAGA,EAAIC,EAAQD,IAC1BH,EAAWG,GAAKoB,EAAUhB,EAASJ,GAG/BA,EAAIe,EAAStD,KAAKG,cACpBiC,EAAWG,IAAMA,GAAKe,EAAStD,KAAKG,cAIlCoC,EAAIC,EAASe,EAAUvD,KAAKG,cAC9BiC,EAAWG,KAAOC,EAASD,IAAMgB,EAAUvD,KAAKG,cAKtD,OAAOsD,CACT,GAEA,kBAGA,SAAK3C,GACH,IAAM8C,EAAS5D,KAAKC,SAAS4D,qBAM7B,OAJAD,EAAO9C,OAASA,EAChB8C,EAAOE,QAAQ9D,KAAKC,SAAS8D,aAC7BH,EAAOR,QAEAQ,CACT,GAEA,oBAUA,SAAO9C,GAAuE,IAAlDkD,EAAe,UAAH,6CAAG,YACnCC,EAAWjE,KAAKkE,YAAYpD,GAC5BqD,EAAWnE,KAAKoE,cAAcH,EAAUnD,EAAOqB,iBAAkBrB,EAAOf,YACxEsE,EAAY,IAAIrD,KAAK,CAACmD,GAAW,CAAEH,KAAAA,IAEzC,MAAO,CACLM,KAAMD,EACNE,IAAKvE,KAAKwE,WAAWH,GACrBI,QAASzE,KAAK0E,oBAAoBL,GAEtC,GAEA,sBAMA,SAASC,GAA6D,IAAjDK,EAAmB,UAAH,6CAAG,UAChCC,EAAIC,SAASC,cAAc,KAOjC,OALAF,EAAEG,MAAMC,QAAU,OAClBJ,EAAEK,KAAOjF,KAAKwE,WAAWF,GACzBM,EAAEM,SAAW,GAAH,OAAMP,EAAQ,YAAIL,EAAKN,KAAKtC,MAAM,KAAK,IACjDkD,EAAEO,QAEKP,CACT,GAEA,0BAOA,SAAgBQ,GACd,OAAOpF,KAAKqF,oBAAiBC,EAAYF,GAC3C,GAEA,mBAGA,WAEE,OADApF,KAAKC,SAASsF,QACPvF,IACT,GAEA,0BAKA,SAAqB4B,GACnB,OAAOoB,KAAKwC,IAAG,MAARxC,KAAI,EAAQpB,EAAQhB,KAAI,SAACE,GAAM,OAAKA,EAAO2E,QAAQ,KAC5D,GAEA,kCAKA,SAA6B7D,GAC3B,OAAOoB,KAAKwC,IAAG,MAARxC,KAAI,EAAQpB,EAAQhB,KAAI,SAACE,GAAM,OAAKA,EAAOqB,gBAAgB,KACpE,GAEA,0BAKA,SAAqBP,GACnB,OAAOA,EAAQhB,KAAI,SAACE,GAAM,OAAKA,EAAO0B,MAAM,IAAEkD,QAAO,SAACd,EAAGe,GAAC,OAAKf,EAAIe,CAAC,GAAE,EACxE,GAEA,0BAKA,WACE,MAAO,iBAAkBvF,QAAU,uBAAwBA,QAAU,oBAAqBA,MAC5F,GAEA,2BAOA,SAAsBU,EAAsB8E,EAAuB7F,GACjE,IAEM8F,EADiBC,EACJF,EAIbG,EALiBD,EAKDhF,EAAO0B,OACvBwD,EAFkB,GAEiBD,EAEnC9E,EAAc,IAAIgF,YALD,EAK8BD,GAC/CE,EAAO,IAAIC,SAASlF,GAgB1B,OAdAjB,KAAKoG,aAAaF,EAAM,EAAG,QAC3BA,EAAKG,UAAU,EAAGL,GAAgB,GAClChG,KAAKoG,aAAaF,EAAM,EAAG,QAC3BlG,KAAKoG,aAAaF,EAAM,GAAI,QAC5BA,EAAKG,UAAU,GAAI,IAAI,GACvBH,EAAKI,UAAU,GAAI,GAAG,GACtBJ,EAAKI,UAAU,GAAIV,GAAe,GAClCM,EAAKG,UAAU,GAAItG,GAAY,GAC/BmG,EAAKG,UAAU,GAAItG,EAAa8F,GAAY,GAC5CK,EAAKI,UAAU,GAAIT,GAAY,GAC/BK,EAAKI,UAAU,GAtBE,IAsBY,GAC7BtG,KAAKoG,aAAaF,EAAM,GAAI,QAC5BA,EAAKG,UAAU,GAAIN,GAAe,GAE3B/F,KAAKuG,iBAAiBL,EAAMpF,EAAQ0F,GAC7C,GAEA,8BAKA,SAAyBrC,EAAoBrD,EAAsB6B,GACjE,IAAK,IAAIJ,EAAI,EAAGA,EAAIzB,EAAO0B,OAAQD,IAAKI,GAAU,EAAG,CACnD,IAAM8D,EAAMzD,KAAKwC,KAAK,EAAGxC,KAAK0D,IAAI,EAAG5F,EAAOyB,KAC5C4B,EAASwC,SAAShE,EAAQ8D,EAAM,EAAU,MAANA,EAAqB,MAANA,GAAc,EACnE,CAEA,OAAOtC,CACT,GAEA,0BAKA,SAAqBA,EAAoBxB,EAAgBiE,GACvD,IAAK,IAAIrE,EAAI,EAAGA,EAAIqE,EAAOpE,OAAQD,IACjC4B,EAAS0C,SAASlE,EAASJ,EAAGqE,EAAOE,WAAWvE,GAEpD,GAEA,yBAKA,SAAoBwE,GAClB,GAA+B,IAA3BA,EAAM5E,iBAER,OAAO4E,EAAM1E,eAAe,GAG9B,IADA,IAAM2E,EAAW,GACRzE,EAAI,EAAGA,EAAIwE,EAAM5E,iBAAkBI,IAC1CyE,EAASC,KAAKF,EAAM1E,eAAeE,IASrC,IAPA,IAAMC,EAASwE,EAAStB,QAAO,SAACwB,EAAMhE,GAAW,OAAKgE,EAAOhE,EAAYV,MAAM,GAAE,GAC3E2E,EAAS,IAAIC,aAAa5E,GAE5B6E,EAAQ,EACRC,EAAa,EAGVD,EAAQ7E,GACbwE,EAAS/E,SAAQ,SAACiB,GAChBiE,EAAOE,KAAWnE,EAAYoE,EAChC,IAEAA,IAGF,OAAOH,CACT,GAEA,iCAKA,SAA4B7C,GAC1B,IAAMiD,EAAQ1C,SAASC,cAAc,SAKrC,OAHAyC,EAAMC,UAAW,EACjBD,EAAME,IAAMzH,KAAKwE,WAAWF,GAErBiD,CACT,GAEA,wBAKA,SAAmBjD,GACjB,OAAQlE,OAAOsH,KAAOtH,OAAOuH,WAAWC,gBAAgBtD,EAC1D,I,iFAAC,EApayB,G","sources":["webpack:///webpack/bootstrap","webpack:///webpack/runtime/define property getters","webpack:///webpack/runtime/hasOwnProperty shorthand","webpack:///./crunker.ts"],"sourcesContent":["// The require scope\nvar __webpack_require__ = {};\n\n","// define getter functions for harmony exports\n__webpack_require__.d = (exports, definition) => {\n\tfor(var key in definition) {\n\t\tif(__webpack_require__.o(definition, key) && !__webpack_require__.o(exports, key)) {\n\t\t\tObject.defineProperty(exports, key, { enumerable: true, get: definition[key] });\n\t\t}\n\t}\n};","__webpack_require__.o = (obj, prop) => (Object.prototype.hasOwnProperty.call(obj, prop))","export interface CrunkerConstructorOptions {\n /**\n * Sample rate for Crunker's internal audio context.\n *\n * @default 44100\n */\n sampleRate: number;\n}\n\nexport type CrunkerInputTypes = string | File | Blob;\n\n/**\n * An exported Crunker audio object.\n */\nexport interface ExportedCrunkerAudio {\n blob: Blob;\n url: string;\n element: HTMLAudioElement;\n}\n\n/**\n * Crunker is the simple way to merge, concatenate, play, export and download audio files using the Web Audio API.\n */\nexport default class Crunker {\n private readonly _sampleRate: number;\n private readonly _context: AudioContext;\n\n /**\n * Creates a new instance of Crunker with the provided options.\n *\n * If `sampleRate` is not defined, it will auto-select an appropriate sample rate\n * for the device being used.\n */\n constructor({ sampleRate }: Partial = {}) {\n this._context = this._createContext(sampleRate);\n\n sampleRate ||= this._context.sampleRate;\n\n this._sampleRate = sampleRate;\n }\n\n /**\n * Creates Crunker's internal AudioContext.\n *\n * @internal\n */\n private _createContext(sampleRate: number = 44_100): AudioContext {\n window.AudioContext = window.AudioContext || (window as any).webkitAudioContext || (window as any).mozAudioContext;\n return new AudioContext({ sampleRate });\n }\n\n /**\n *\n * The internal AudioContext used by Crunker.\n */\n get context(): AudioContext {\n return this._context;\n }\n\n /**\n * Asynchronously fetches multiple audio files and returns an array of AudioBuffers.\n */\n async fetchAudio(...filepaths: CrunkerInputTypes[]): Promise {\n return await Promise.all(\n filepaths.map(async (filepath) => {\n let buffer: ArrayBuffer;\n\n if (filepath instanceof File || filepath instanceof Blob) {\n buffer = await filepath.arrayBuffer();\n } else {\n buffer = await fetch(filepath).then((response) => {\n if (response.headers.has('Content-Type') && !response.headers.get('Content-Type')!.includes('audio/')) {\n console.warn(\n `Crunker: Attempted to fetch an audio file, but its MIME type is \\`${\n response.headers.get('Content-Type')!.split(';')[0]\n }\\`. We'll try and continue anyway. (file: \"${filepath}\")`\n );\n }\n\n return response.arrayBuffer();\n });\n }\n\n return await this._context.decodeAudioData(buffer);\n })\n );\n }\n\n /**\n * Merges (layers) multiple AudioBuffers into a single AudioBuffer.\n *\n * **Visual representation:**\n *\n * ![](https://user-images.githubusercontent.com/12958674/88806278-968f0680-d186-11ea-9cb5-8ef2606ffcc7.png)\n */\n mergeAudio(buffers: AudioBuffer[]): AudioBuffer {\n const output = this._context.createBuffer(\n this._maxNumberOfChannels(buffers),\n this._sampleRate * this._maxDuration(buffers),\n this._sampleRate\n );\n\n buffers.forEach((buffer) => {\n for (let channelNumber = 0; channelNumber < buffer.numberOfChannels; channelNumber++) {\n const outputData = output.getChannelData(channelNumber);\n const bufferData = buffer.getChannelData(channelNumber);\n\n for (let i = buffer.getChannelData(channelNumber).length - 1; i >= 0; i--) {\n outputData[i] += bufferData[i];\n }\n\n output.getChannelData(channelNumber).set(outputData);\n }\n });\n\n return output;\n }\n\n /**\n * Concatenates multiple AudioBuffers into a single AudioBuffer.\n *\n * **Visual representation:**\n *\n * ![](https://user-images.githubusercontent.com/12958674/88806297-9d1d7e00-d186-11ea-8cd2-c64cb0324845.png)\n */\n concatAudio(buffers: AudioBuffer[]): AudioBuffer {\n const output = this._context.createBuffer(\n this._maxNumberOfChannels(buffers),\n this._totalLength(buffers),\n this._sampleRate\n );\n let offset = 0;\n\n buffers.forEach((buffer) => {\n for (let channelNumber = 0; channelNumber < buffer.numberOfChannels; channelNumber++) {\n output.getChannelData(channelNumber).set(buffer.getChannelData(channelNumber), offset);\n }\n\n offset += buffer.length;\n });\n\n return output;\n }\n\n /**\n * Pads a specified AudioBuffer with silence from a specified start time,\n * for a specified length of time.\n *\n * Accepts float values as well as whole integers.\n *\n * @param buffer AudioBuffer to pad\n * @param padStart Time to start padding (in seconds)\n * @param seconds Duration to pad for (in seconds)\n */\n padAudio(buffer: AudioBuffer, padStart: number = 0, seconds: number = 0): AudioBuffer {\n if (seconds === 0) return buffer;\n\n if (padStart < 0) throw new Error('Crunker: Parameter \"padStart\" in padAudio must be positive');\n if (seconds < 0) throw new Error('Crunker: Parameter \"seconds\" in padAudio must be positive');\n\n const updatedBuffer = this._context.createBuffer(\n buffer.numberOfChannels,\n Math.ceil(buffer.length + seconds * buffer.sampleRate),\n buffer.sampleRate\n );\n\n for (let channelNumber = 0; channelNumber < buffer.numberOfChannels; channelNumber++) {\n const channelData = buffer.getChannelData(channelNumber);\n updatedBuffer\n .getChannelData(channelNumber)\n .set(channelData.subarray(0, Math.ceil(padStart * buffer.sampleRate) + 1), 0);\n\n updatedBuffer\n .getChannelData(channelNumber)\n .set(\n channelData.subarray(Math.ceil(padStart * buffer.sampleRate) + 2, updatedBuffer.length + 1),\n Math.ceil((padStart + seconds) * buffer.sampleRate)\n );\n }\n\n return updatedBuffer;\n }\n\n /**\n * Slices an AudioBuffer from the specified start time to the end time, with optional fade in and out.\n *\n * @param buffer AudioBuffer to slice\n * @param start Start time (in seconds)\n * @param end End time (in seconds)\n * @param fadeIn Fade in duration (in seconds, default is 0)\n * @param fadeOut Fade out duration (in seconds, default is 0)\n */\n sliceAudio(buffer: AudioBuffer, start: number, end: number, fadeIn: number = 0, fadeOut: number = 0): AudioBuffer {\n if (start >= end) throw new Error('Crunker: \"start\" time should be less than \"end\" time in sliceAudio method');\n\n const length = Math.round((end - start) * this._sampleRate);\n const offset = Math.round(start * this._sampleRate);\n const newBuffer = this._context.createBuffer(buffer.numberOfChannels, length, this._sampleRate);\n\n for (let channel = 0; channel < buffer.numberOfChannels; channel++) {\n const inputData = buffer.getChannelData(channel);\n const outputData = newBuffer.getChannelData(channel);\n\n for (let i = 0; i < length; i++) {\n outputData[i] = inputData[offset + i];\n\n // Apply fade in\n if (i < fadeIn * this._sampleRate) {\n outputData[i] *= i / (fadeIn * this._sampleRate);\n }\n\n // Apply fade out\n if (i > length - fadeOut * this._sampleRate) {\n outputData[i] *= (length - i) / (fadeOut * this._sampleRate);\n }\n }\n }\n\n return newBuffer;\n }\n\n /**\n * Plays the provided AudioBuffer in an AudioBufferSourceNode.\n */\n play(buffer: AudioBuffer): AudioBufferSourceNode {\n const source = this._context.createBufferSource();\n\n source.buffer = buffer;\n source.connect(this._context.destination);\n source.start();\n\n return source;\n }\n\n /**\n * Exports the specified AudioBuffer to a Blob, Object URI and HTMLAudioElement.\n *\n * Note that changing the MIME type does not change the actual file format. The\n * file format will **always** be a WAVE file due to how audio is stored in the\n * browser.\n *\n * @param buffer Buffer to export\n * @param type MIME type (default: `audio/wav`)\n */\n export(buffer: AudioBuffer, type: string = 'audio/wav'): ExportedCrunkerAudio {\n const recorded = this._interleave(buffer);\n const dataview = this._writeHeaders(recorded, buffer.numberOfChannels, buffer.sampleRate);\n const audioBlob = new Blob([dataview], { type });\n\n return {\n blob: audioBlob,\n url: this._renderURL(audioBlob),\n element: this._renderAudioElement(audioBlob),\n };\n }\n\n /**\n * Downloads the provided Blob.\n *\n * @param blob Blob to download\n * @param filename An optional file name to use for the download (default: `crunker`)\n */\n download(blob: Blob, filename: string = 'crunker'): HTMLAnchorElement {\n const a = document.createElement('a');\n\n a.style.display = 'none';\n a.href = this._renderURL(blob);\n a.download = `${filename}.${blob.type.split('/')[1]}`;\n a.click();\n\n return a;\n }\n\n /**\n * Executes a callback if the browser does not support the Web Audio API.\n *\n * Returns the result of the callback, or `undefined` if the Web Audio API is supported.\n *\n * @param callback callback to run if the browser does not support the Web Audio API\n */\n notSupported(callback: () => T): T | undefined {\n return this._isSupported() ? undefined : callback();\n }\n\n /**\n * Closes Crunker's internal AudioContext.\n */\n close(): this {\n this._context.close();\n return this;\n }\n\n /**\n * Returns the largest duration of the longest AudioBuffer.\n *\n * @internal\n */\n private _maxDuration(buffers: AudioBuffer[]): number {\n return Math.max(...buffers.map((buffer) => buffer.duration));\n }\n\n /**\n * Returns the largest number of channels in an array of AudioBuffers.\n *\n * @internal\n */\n private _maxNumberOfChannels(buffers: AudioBuffer[]): number {\n return Math.max(...buffers.map((buffer) => buffer.numberOfChannels));\n }\n\n /**\n * Returns the sum of the lengths of an array of AudioBuffers.\n *\n * @internal\n */\n private _totalLength(buffers: AudioBuffer[]): number {\n return buffers.map((buffer) => buffer.length).reduce((a, b) => a + b, 0);\n }\n\n /**\n * Returns whether the browser supports the Web Audio API.\n *\n * @internal\n */\n private _isSupported(): boolean {\n return 'AudioContext' in window || 'webkitAudioContext' in window || 'mozAudioContext' in window;\n }\n\n /**\n * Writes the WAV headers for the specified Float32Array.\n *\n * Returns a DataView containing the WAV headers and file content.\n *\n * @internal\n */\n private _writeHeaders(buffer: Float32Array, numOfChannels: number, sampleRate: number): DataView {\n const bitDepth = 16;\n const bytesPerSample = bitDepth / 8;\n const sampleSize = numOfChannels * bytesPerSample;\n\n const fileHeaderSize = 8;\n const chunkHeaderSize = 36;\n const chunkDataSize = buffer.length * bytesPerSample;\n const chunkTotalSize = chunkHeaderSize + chunkDataSize;\n\n const arrayBuffer = new ArrayBuffer(fileHeaderSize + chunkTotalSize);\n const view = new DataView(arrayBuffer);\n\n this._writeString(view, 0, 'RIFF');\n view.setUint32(4, chunkTotalSize, true);\n this._writeString(view, 8, 'WAVE');\n this._writeString(view, 12, 'fmt ');\n view.setUint32(16, 16, true);\n view.setUint16(20, 1, true);\n view.setUint16(22, numOfChannels, true);\n view.setUint32(24, sampleRate, true);\n view.setUint32(28, sampleRate * sampleSize, true);\n view.setUint16(32, sampleSize, true);\n view.setUint16(34, bitDepth, true);\n this._writeString(view, 36, 'data');\n view.setUint32(40, chunkDataSize, true);\n\n return this._floatTo16BitPCM(view, buffer, fileHeaderSize + chunkHeaderSize);\n }\n\n /**\n * Converts a Float32Array to 16-bit PCM.\n *\n * @internal\n */\n private _floatTo16BitPCM(dataview: DataView, buffer: Float32Array, offset: number): DataView {\n for (let i = 0; i < buffer.length; i++, offset += 2) {\n const tmp = Math.max(-1, Math.min(1, buffer[i]));\n dataview.setInt16(offset, tmp < 0 ? tmp * 0x8000 : tmp * 0x7fff, true);\n }\n\n return dataview;\n }\n\n /**\n * Writes a string to a DataView at the specified offset.\n *\n * @internal\n */\n private _writeString(dataview: DataView, offset: number, header: string): void {\n for (let i = 0; i < header.length; i++) {\n dataview.setUint8(offset + i, header.charCodeAt(i));\n }\n }\n\n /**\n * Converts an AudioBuffer to a Float32Array.\n *\n * @internal\n */\n private _interleave(input: AudioBuffer): Float32Array {\n if (input.numberOfChannels === 1) {\n // No need to interleave channels, just return single channel data to save performance and memory\n return input.getChannelData(0);\n }\n const channels = [];\n for (let i = 0; i < input.numberOfChannels; i++) {\n channels.push(input.getChannelData(i));\n }\n const length = channels.reduce((prev, channelData) => prev + channelData.length, 0);\n const result = new Float32Array(length);\n\n let index = 0;\n let inputIndex = 0;\n\n // for 2 channels its like: [L[0], R[0], L[1], R[1], ... , L[n], R[n]]\n while (index < length) {\n channels.forEach((channelData) => {\n result[index++] = channelData[inputIndex];\n });\n\n inputIndex++;\n }\n\n return result;\n }\n\n /**\n * Creates an HTMLAudioElement whose source is the specified Blob.\n *\n * @internal\n */\n private _renderAudioElement(blob: Blob): HTMLAudioElement {\n const audio = document.createElement('audio');\n\n audio.controls = true;\n audio.src = this._renderURL(blob);\n\n return audio;\n }\n\n /**\n * Creates an Object URL for the specified Blob.\n *\n * @internal\n */\n private _renderURL(blob: Blob): string {\n return (window.URL || window.webkitURL).createObjectURL(blob);\n }\n}\n"],"names":["__webpack_require__","exports","definition","key","o","Object","defineProperty","enumerable","get","obj","prop","prototype","hasOwnProperty","call","Crunker","sampleRate","this","_context","_createContext","_sampleRate","window","AudioContext","webkitAudioContext","mozAudioContext","async","filepaths","Promise","all","map","filepath","buffer","File","Blob","arrayBuffer","fetch","then","response","headers","has","includes","console","warn","split","decodeAudioData","buffers","output","createBuffer","_maxNumberOfChannels","_maxDuration","forEach","channelNumber","numberOfChannels","outputData","getChannelData","bufferData","i","length","set","_totalLength","offset","padStart","seconds","Error","updatedBuffer","Math","ceil","channelData","subarray","start","end","fadeIn","fadeOut","round","newBuffer","channel","inputData","source","createBufferSource","connect","destination","type","recorded","_interleave","dataview","_writeHeaders","audioBlob","blob","url","_renderURL","element","_renderAudioElement","filename","a","document","createElement","style","display","href","download","click","callback","_isSupported","undefined","close","max","duration","reduce","b","numOfChannels","sampleSize","bitDepth","chunkDataSize","chunkTotalSize","ArrayBuffer","view","DataView","_writeString","setUint32","setUint16","_floatTo16BitPCM","fileHeaderSize","tmp","min","setInt16","header","setUint8","charCodeAt","input","channels","push","prev","result","Float32Array","index","inputIndex","audio","controls","src","URL","webkitURL","createObjectURL"],"sourceRoot":""} \ No newline at end of file