UNPKG

react-hifi

Version:

A set of react components wich provides simple abstraption to manipulate HTML5 AudioContext API (Equalizer, visualisation, stereo, basic controls)

522 lines (504 loc) 22 kB
import React from 'react'; /*! ***************************************************************************** Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 THIS CODE IS PROVIDED ON AN *AS IS* BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION ANY IMPLIED WARRANTIES OR CONDITIONS OF TITLE, FITNESS FOR A PARTICULAR PURPOSE, MERCHANTABLITY OR NON-INFRINGEMENT. See the Apache Version 2.0 License for specific language governing permissions and limitations under the License. ***************************************************************************** */ /* global Reflect, Promise */ var extendStatics = function(d, b) { extendStatics = Object.setPrototypeOf || ({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) || function (d, b) { for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; }; return extendStatics(d, b); }; function __extends(d, b) { extendStatics(d, b); function __() { this.constructor = d; } d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __()); } var __assign = function() { __assign = Object.assign || function __assign(t) { for (var s, i = 1, n = arguments.length; i < n; i++) { s = arguments[i]; for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) t[p] = s[p]; } return t; }; return __assign.apply(this, arguments); }; var memo = React.memo, useState = React.useState, useEffect = React.useEffect; function pluginFactory(_a) { var createNode = _a.createNode, updateNode = _a.updateNode, _b = _a.shouldNotUpdate, shouldNotUpdate = _b === void 0 ? function () { return true; } : _b; return memo(function (props) { var _a = useState(), node = _a[0], setNode = _a[1]; useEffect(function () { if (props.previousNode && props.audioContext && !node) { var createdNode = createNode(props.audioContext, props); if (Array.isArray(createdNode)) { var lastInChain = createdNode[0]; props.previousNode.connect(lastInChain); for (var i = 1; i < createdNode.length; i++) { lastInChain.connect(createdNode[i]); lastInChain = createdNode[i]; } } else { props.previousNode.connect(createdNode); } setNode(createdNode); } }); useEffect(function () { if (node) { props.onRegister && props.onRegister(Array.isArray(node) ? node[node.length - 1] : node); } }, [node]); node && updateNode && props.audioContext && updateNode(node, props, props.audioContext); return null; }, function (prevProps, nextProps) { return prevProps.previousNode === nextProps.previousNode && shouldNotUpdate(prevProps, nextProps); }); } var VolumePlugin = /** @class */ (function () { function VolumePlugin() { this.createNode = this.createNode.bind(this); } VolumePlugin.prototype.shouldNotUpdate = function (prevProps, nextProps) { return prevProps.value === nextProps.value; }; VolumePlugin.prototype.createNode = function (audioContext, props) { var gainNode = audioContext.createGain(); this.updateNode(gainNode, props); return gainNode; }; VolumePlugin.prototype.updateNode = function (node, _a) { var value = _a.value; node.gain.value = value / 100; }; return VolumePlugin; }()); var index = pluginFactory(new VolumePlugin()); var StereoPlugin = /** @class */ (function () { function StereoPlugin() { } StereoPlugin.prototype.shouldNotUpdate = function (prevProps, nextProps) { return prevProps.value === nextProps.value; }; StereoPlugin.prototype.createNode = function (audioContext, _a) { var value = _a.value; return new StereoPannerNode(audioContext, { pan: value }); }; StereoPlugin.prototype.updateNode = function (_a, _b) { var pan = _a.pan; var value = _b.value; pan.value = value; }; return StereoPlugin; }()); var index$1 = pluginFactory(new StereoPlugin()); var BiQuadPlugin = /** @class */ (function () { function BiQuadPlugin() { } BiQuadPlugin.prototype.shouldNotUpdate = function (prevProps, nextProps) { return prevProps.value === nextProps.value && prevProps.q === nextProps.q; }; BiQuadPlugin.prototype.createNode = function (audioContext, _a) { var value = _a.value, freq = _a.freq, type = _a.type, q = _a.q; var filter = new BiquadFilterNode(audioContext); filter.frequency.value = freq; filter.gain.value = value; filter.type = type; if (q) { filter.Q.value = q; } return filter; }; BiQuadPlugin.prototype.updateNode = function (_a, _b) { var gain = _a.gain, Q = _a.Q; var value = _b.value, q = _b.q; gain.value = value; if (q) { Q.value = q; } }; return BiQuadPlugin; }()); var index$2 = pluginFactory(new BiQuadPlugin()); function shallowObject(a, b, compare) { var ka = 0; var kb = 0; if (compare) { for (var key in a) { if (a.hasOwnProperty(key) && !compare(a[key], b[key])) return false; ka++; } } else { for (var key in a) { if (a.hasOwnProperty(key) && a[key] !== b[key]) return false; ka++; } } for (var key in b) { if (b.hasOwnProperty(key)) kb++; } return ka === kb; } var EqualizerPlugin = /** @class */ (function () { function EqualizerPlugin() { } EqualizerPlugin.prototype.shouldNotUpdate = function (prevProps, nextProps) { return shallowObject(prevProps.data, nextProps.data) && prevProps.preAmp === nextProps.preAmp; }; EqualizerPlugin.prototype.createNode = function (audioContext, _a) { var data = _a.data, _b = _a.preAmp, preAmp = _b === void 0 ? 0 : _b; var frequencies = Object.keys(data).map(Number); return frequencies.map(function (freq, idx) { var filter = new BiquadFilterNode(audioContext); filter.frequency.value = freq; filter.gain.value = data[freq] + preAmp; switch (idx) { case 0: filter.type = 'lowshelf'; break; case frequencies.length - 1: filter.type = 'highshelf'; break; default: filter.type = 'peaking'; filter.Q.value = (2 * freq) / Math.abs(frequencies[idx + 1] - frequencies[idx - 1]); break; } return filter; }); }; EqualizerPlugin.prototype.updateNode = function (nodes, _a) { var _b = _a.preAmp, preAmp = _b === void 0 ? 0 : _b, data = _a.data; nodes.forEach(function (node, idx) { node.gain.value = data[node.frequency.value] + preAmp; }); }; return EqualizerPlugin; }()); var index$3 = pluginFactory(new EqualizerPlugin()); var ContextState; (function (ContextState) { ContextState["RUNNING"] = "running"; ContextState["SUSPENDED"] = "suspended"; ContextState["CLOSED"] = "closed"; })(ContextState || (ContextState = {})); var AnalyserByFrequencyPlugin = /** @class */ (function () { function AnalyserByFrequencyPlugin() { this.createNode = this.createNode.bind(this); this.updateNode = this.updateNode.bind(this); this.handleVisualizationChange = this.handleVisualizationChange.bind(this); } AnalyserByFrequencyPlugin.prototype.formatDataVizByFrequency = function (data, frequencies) { var values = []; var HERTZ_ITER = 23.4; var currentIndex = 0; for (var i = 0; i <= frequencies[frequencies.length - 1] + HERTZ_ITER; i = i + HERTZ_ITER) { var freq = frequencies[currentIndex]; if (i > freq && i < freq + HERTZ_ITER) { currentIndex++; values.push(data[Math.round(i / HERTZ_ITER)]); } } return values; }; AnalyserByFrequencyPlugin.prototype.handleVisualizationChange = function () { this.animationFrame = requestAnimationFrame(this.handleVisualizationChange); this.node.getByteFrequencyData(this.frequencyData); this.onVisualisationData(this.formatDataVizByFrequency(this.frequencyData, this.frequencies)); }; AnalyserByFrequencyPlugin.prototype.shouldNotUpdate = function (prevProps, nextProps) { return !!nextProps.audioContext && nextProps.audioContext.state !== ContextState.RUNNING; }; AnalyserByFrequencyPlugin.prototype.createNode = function (audioContext, props) { this.node = audioContext.createAnalyser(); this.onVisualisationData = props.onVisualisationData; this.frequencies = props.frequencies; this.node.fftSize = 32768; this.frequencyData = new Uint8Array(this.node.frequencyBinCount); return this.node; }; AnalyserByFrequencyPlugin.prototype.updateNode = function (node, props, audioContext) { if (!this.onVisualisationData && props.onVisualisationData) { this.onVisualisationData = props.onVisualisationData; this.handleVisualizationChange(); return; } else if (this.onVisualisationData && !props.onVisualisationData) { this.onVisualisationData(props.frequencies.map(function () { return 0; })); this.onVisualisationData = props.onVisualisationData; this.animationFrame && cancelAnimationFrame(this.animationFrame); return; } if (this.previousContextState !== audioContext.state) { this.previousContextState = audioContext.state; switch (audioContext.state) { case ContextState.SUSPENDED: this.animationFrame && cancelAnimationFrame(this.animationFrame); break; case ContextState.CLOSED: this.animationFrame && cancelAnimationFrame(this.animationFrame); this.onVisualisationData(props.frequencies.map(function () { return 0; })); break; case ContextState.RUNNING: this.onVisualisationData && this.handleVisualizationChange(); break; } } }; return AnalyserByFrequencyPlugin; }()); var index$4 = pluginFactory(new AnalyserByFrequencyPlugin()); var OsciloscopePlugin = /** @class */ (function () { function OsciloscopePlugin() { this.createNode = this.createNode.bind(this); this.handleVisualizationChange = this.handleVisualizationChange.bind(this); this.updateNode = this.updateNode.bind(this); } OsciloscopePlugin.prototype.handleVisualizationChange = function () { this.animationFrame = requestAnimationFrame(this.handleVisualizationChange); this.node.getByteTimeDomainData(this.dataArray); var sliceWidth = (this.width * 1.0) / this.node.frequencyBinCount; var x = 0; var data = []; for (var i = 0; i < this.node.frequencyBinCount; i++) { var v = this.dataArray[i] / 128.0; var y = (v * this.height) / 2; data.push([x, y]); x += sliceWidth; } this.onData(data); }; OsciloscopePlugin.prototype.shouldNotUpdate = function (prevProps, nextProps) { return !!nextProps.audioContext && nextProps.audioContext.state !== ContextState.RUNNING; }; OsciloscopePlugin.prototype.createNode = function (audioContext, _a) { var height = _a.height, width = _a.width, onVisualisationData = _a.onVisualisationData; this.node = audioContext.createAnalyser(); this.node.fftSize = 2048; this.dataArray = new Uint8Array(this.node.frequencyBinCount); this.width = width; this.height = height; this.onData = onVisualisationData; return this.node; }; OsciloscopePlugin.prototype.updateNode = function (node, _a, audioContext) { var height = _a.height, width = _a.width; if (this.height !== height) { this.height = height; } if (this.width !== width) { this.width = width; } if (this.previousContextState !== audioContext.state) { this.previousContextState = audioContext.state; switch (audioContext.state) { case ContextState.SUSPENDED: this.animationFrame && cancelAnimationFrame(this.animationFrame); break; case ContextState.CLOSED: this.animationFrame && cancelAnimationFrame(this.animationFrame); break; case ContextState.RUNNING: this.handleVisualizationChange(); break; } } }; return OsciloscopePlugin; }()); var index$5 = pluginFactory(new OsciloscopePlugin()); var SoundStatus; (function (SoundStatus) { SoundStatus["PAUSED"] = "PAUSED"; SoundStatus["PLAYING"] = "PLAYING"; SoundStatus["STOPPED"] = "STOPPED"; })(SoundStatus || (SoundStatus = {})); var Destination = pluginFactory({ createNode: function (audioContext) { return audioContext.destination; }, }); var SoundErrors; (function (SoundErrors) { SoundErrors["MEDIA_ERR_ABORTED"] = "Video playback aborted by the user."; SoundErrors["MEDIA_ERR_NETWORK"] = "A network error caused the audio download to fail."; SoundErrors["MEDIA_ERR_DECODE"] = "The audio playback was aborted due to a corruption problem."; SoundErrors["MEDIA_ERR_SRC_NOT_SUPPORTED"] = "The audio playback can not be loaded, either because the server or network failed or because the format is not supported."; SoundErrors["UNKNOWN"] = "An unknown error occurred during audio playback loading."; })(SoundErrors || (SoundErrors = {})); /** * Sound Component */ var Sound = /** @class */ (function (_super) { __extends(Sound, _super); function Sound(props) { var _this = _super.call(this, props) || this; _this.state = { audioContext: new AudioContext(), audioNodes: [], }; _this.handleTimeUpdate = _this.handleTimeUpdate.bind(_this); _this.attachRef = _this.attachRef.bind(_this); _this.handleRegisterPlugin = _this.handleRegisterPlugin.bind(_this); _this.handleError = _this.handleError.bind(_this); return _this; } Sound.prototype.attachRef = function (element) { if (element) { this.audio = element; } }; Sound.prototype.renderPlugins = function () { var _this = this; var children = this.props.children; if (Array.isArray(children)) { var flatChildren = children.flat(); return flatChildren.map(function (plugin, idx) { return (React.createElement(plugin.type, __assign({}, plugin.props, { key: idx, audioContext: _this.state.audioContext, previousNode: _this.state.audioNodes[idx], onRegister: _this.handleRegisterPlugin }))); }).concat([ React.createElement(Destination, { key: flatChildren.length, audioContext: this.state.audioContext, previousNode: this.state.audioNodes[flatChildren.length - 1] }), ]); } else if (children) { return [ React.createElement(children.type, __assign({}, children.props, { key: 1, audioContext: this.state.audioContext, previousNode: this.state.audioNodes[0], onRegister: this.handleRegisterPlugin })), React.createElement(Destination, { key: 2, audioContext: this.state.audioContext, previousNode: this.state.audioNodes[1] }), ]; } else { return null; } }; Sound.prototype.handleRegisterPlugin = function (plugin) { this.setState({ audioNodes: this.state.audioNodes.concat([plugin]), }); }; Sound.prototype.handleTimeUpdate = function (_a) { var target = _a.target; if (this.props.onPlaying) { this.props.onPlaying({ position: target.currentTime, duration: target.duration, }); } }; Sound.prototype.setPlayerState = function (status) { switch (status) { case Sound.status.PAUSED: this.pause(); break; case Sound.status.STOPPED: this.stop(); break; case Sound.status.PLAYING: default: this.play(); break; } }; Sound.prototype.shouldUpdatePosition = function (_a) { var position = _a.position; var nextPosition = this.props.position; if ((position || position === 0) && (nextPosition || nextPosition === 0)) { var dif = nextPosition - position; return position > nextPosition || dif > 1; } else { return false; } }; Sound.prototype.setPosition = function (position) { this.audio.currentTime = position; }; Sound.prototype.play = function () { var _this = this; this.state.audioContext.resume().then(function () { return _this.audio.play(); }); }; Sound.prototype.pause = function () { var _this = this; this.state.audioContext.suspend().then(function () { return _this.audio.pause(); }); }; Sound.prototype.stop = function () { this.pause(); this.audio.currentTime = 0; }; Sound.prototype.handleError = function (evt) { var error; switch (evt.target.error.code) { case evt.target.error.MEDIA_ERR_ABORTED: error = new Error(SoundErrors.MEDIA_ERR_ABORTED); break; case evt.target.error.MEDIA_ERR_NETWORK: error = new Error(SoundErrors.MEDIA_ERR_NETWORK); break; case evt.target.error.MEDIA_ERR_DECODE: error = new Error(SoundErrors.MEDIA_ERR_DECODE); break; case evt.target.error.MEDIA_ERR_SRC_NOT_SUPPORTED: error = new Error(SoundErrors.MEDIA_ERR_SRC_NOT_SUPPORTED); break; default: error = new Error(SoundErrors.UNKNOWN); break; } this.props.onError && this.props.onError(error); }; Sound.prototype.componentDidUpdate = function (prevProps) { var _a = this.props, playStatus = _a.playStatus, url = _a.url; if ((playStatus && prevProps.playStatus !== playStatus) || url !== prevProps.url) { this.setPlayerState(playStatus); } if (this.shouldUpdatePosition(prevProps)) { this.setPosition(this.props.position); } }; Sound.prototype.componentDidMount = function () { this.source = this.state.audioContext.createMediaElementSource(this.audio); if (!this.props.children) { this.source.connect(this.state.audioContext.destination); } else { this.setState({ audioNodes: [this.source], }); } this.setPlayerState(this.props.playStatus); this.props.position && this.setPosition(this.props.position); }; Sound.prototype.componentWillUnmount = function () { this.state.audioContext.close(); }; Sound.prototype.componentDidCatch = function (err) { this.state.audioContext.close(); this.props.onError && this.props.onError(err); }; Sound.prototype.render = function () { var _a = this.props, url = _a.url, onPlaying = _a.onPlaying, onFinishedPlaying = _a.onFinishedPlaying, onLoad = _a.onLoad, onLoading = _a.onLoading; return (React.createElement(React.Fragment, null, React.createElement("audio", { crossOrigin: "anonymous", style: { visibility: 'hidden' }, src: Array.isArray(url) ? undefined : url, ref: this.attachRef, onTimeUpdate: onPlaying ? this.handleTimeUpdate : undefined, onEnded: onFinishedPlaying, onLoadStart: onLoading, onCanPlay: onLoad, onError: this.handleError }, Array.isArray(url) && url.map(function (_a, index) { var url = _a.url, type = _a.type; return React.createElement("source", { key: index, type: type, src: url }); })), this.renderPlugins())); }; Sound.status = SoundStatus; return Sound; }(React.Component)); export default Sound; export { index$4 as AnalyserByFrequency, index$2 as BiQuadFilter, index$3 as Equalizer, index$5 as Osciloscope, index$1 as Stereo, index as Volume, pluginFactory }; //# sourceMappingURL=react-hifi.es5.js.map