react-hifi
Version:
A set of react components wich provides simple abstraption to manipulate HTML5 AudioContext API (Equalizer, visualisation, stereo, basic controls)
538 lines (517 loc) • 24.5 kB
JavaScript
(function (global, factory) {
typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports, require('react')) :
typeof define === 'function' && define.amd ? define(['exports', 'react'], factory) :
(global = global || self, factory(global.reactSoundHtml5 = {}, global.React));
}(this, function (exports, React) { 'use strict';
React = React && React.hasOwnProperty('default') ? React['default'] : 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));
exports.AnalyserByFrequency = index$4;
exports.BiQuadFilter = index$2;
exports.Equalizer = index$3;
exports.Osciloscope = index$5;
exports.Stereo = index$1;
exports.Volume = index;
exports.default = Sound;
exports.pluginFactory = pluginFactory;
Object.defineProperty(exports, '__esModule', { value: true });
}));
//# sourceMappingURL=react-hifi.umd.js.map