react-native-audio-api
Version:
react-native-audio-api provides system for controlling audio in React Native environment compatible with Web Audio API specification
275 lines (271 loc) • 9.2 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.default = void 0;
var _errors = require("../../../errors");
var _AudioParam = _interopRequireDefault(require("../../AudioParam.web"));
var _utils = require("../../../utils");
var _LoadCustomWasm = _interopRequireWildcard(require("./LoadCustomWasm"));
var _AudioStretcherParam = _interopRequireDefault(require("./AudioStretcherParam"));
function _interopRequireWildcard(e, t) { if ("function" == typeof WeakMap) var r = new WeakMap(), n = new WeakMap(); return (_interopRequireWildcard = function (e, t) { if (!t && e && e.__esModule) return e; var o, i, f = { __proto__: null, default: e }; if (null === e || "object" != typeof e && "function" != typeof e) return f; if (o = t ? n : r) { if (o.has(e)) return o.get(e); o.set(e, f); } for (const t in e) "default" !== t && {}.hasOwnProperty.call(e, t) && ((i = (o = Object.defineProperty) && Object.getOwnPropertyDescriptor(e, t)) && (i.get || i.set) ? o(f, t, i) : f[t] = e[t]); return f; })(e, t); }
function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
function buildScheduleOptions(options, time) {
if (time === undefined) {
return options;
}
return {
...options,
output: time
};
}
function detuneCentsToSemitones(cents) {
return (0, _utils.clamp)(cents / 100, -12, 12);
}
class AudioBufferSourceNodeStretcher {
stretcherPromise = null;
node = null;
hasBeenStarted = false;
_onEnded = null;
_loop = false;
_loopStart = -1;
_loopEnd = -1;
_loopSkip = false;
_onLoopEnded = undefined;
_buffer = null;
bufferHasBeenSet = false;
_operationChain = Promise.resolve();
_pendingParamEvents = [];
constructor(context, options) {
this.context = context;
// The worklet is loaded as a static asset (served next to the app) rather
// than bundled, so it is never transformed by the consumer's bundler. The
// consumer preloads it via LoadCustomWasm(prefix); this call is idempotent.
const stretcherPromise = (async () => {
await (0, _LoadCustomWasm.default)('/react-native-audio-api');
await _LoadCustomWasm.globalWasmPromise;
const factory = window[_LoadCustomWasm.globalTag];
return factory(context.context);
})();
this.stretcherPromise = stretcherPromise;
stretcherPromise.then(node => {
this.node = node;
});
this.detune = new _AudioStretcherParam.default(context, options.detune ?? 0, 0, -1200, 1200, event => {
this.handleParamEvent(this.detune, event, value => ({
semitones: detuneCentsToSemitones(value)
}));
}, cancelTime => {
this.cancelStretcherAutomation(cancelTime);
});
this.playbackRate = new _AudioStretcherParam.default(context, options.playbackRate ?? 1, 1, 0, Infinity, event => {
this.handleParamEvent(this.playbackRate, event, value => ({
rate: value
}));
}, cancelTime => {
this.cancelStretcherAutomation(cancelTime);
});
this.buffer = options.buffer ?? null;
}
runOnStretcher(action) {
this._operationChain = this._operationChain.then(() => this.stretcherPromise.then(action));
return this._operationChain;
}
scheduleStretcher(options, time) {
const scheduleOptions = buildScheduleOptions(options, time);
this.runOnStretcher(node => {
node.schedule(scheduleOptions);
});
}
cancelStretcherAutomation(cancelTime) {
this._pendingParamEvents = this._pendingParamEvents.filter(pending => pending.event.time < cancelTime);
this.runOnStretcher(node => {
node.cancelScheduledValues(cancelTime);
});
}
handleParamEvent(param, event, mapOptions) {
if (!this.hasBeenStarted) {
this._pendingParamEvents.push({
param,
event,
mapOptions
});
return;
}
this.applyParamEvent(param, event, mapOptions);
}
applyParamEvent(param, event, mapOptions) {
const samples = param.sampleAutomation(event, value => value);
for (const sample of samples) {
this.scheduleStretcher(mapOptions(sample.value), sample.time);
}
}
flushPendingParamEvents() {
const pending = this._pendingParamEvents;
this._pendingParamEvents = [];
for (const {
param,
event,
mapOptions
} of pending) {
this.applyParamEvent(param, event, mapOptions);
}
}
connect(destination) {
const action = node => {
if (destination instanceof _AudioParam.default) {
node.connect(destination.param);
return;
}
node.connect(destination.node);
};
this.runOnStretcher(action);
return destination;
}
disconnect(destination) {
const action = node => {
if (destination === undefined) {
node.disconnect();
return;
}
if (destination instanceof _AudioParam.default) {
node.disconnect(destination.param);
return;
}
node.disconnect(destination.node);
};
this.runOnStretcher(action);
}
start(when, offset, duration) {
if (when && when < 0) {
throw new _errors.RangeError(`when must be a finite non-negative number: ${when}`);
}
if (offset && offset < 0) {
throw new _errors.RangeError(`offset must be a finite non-negative number: ${offset}`);
}
if (duration && duration < 0) {
throw new _errors.RangeError(`duration must be a finite non-negative number: ${duration}`);
}
if (this.hasBeenStarted) {
throw new _errors.InvalidStateError('Cannot call start more than once');
}
this.hasBeenStarted = true;
const startAt = !when || when < this.context.currentTime ? this.context.currentTime : when;
this.runOnStretcher(node => {
const playbackRate = this.playbackRate.getValueAtTime(startAt);
const detune = this.detune.getValueAtTime(startAt);
node.start(startAt, offset, duration, playbackRate, detuneCentsToSemitones(detune));
// Pin the loop to the same segment start() just scheduled (output=startAt).
// A separate schedule() with no output would land at the worklet's own
// (later) currentTime as an inactive trailing segment and silence playback.
if (this.loop && this._loopStart !== -1 && this._loopEnd !== -1) {
node.schedule({
output: startAt,
loopStart: this._loopStart,
loopEnd: this._loopEnd
});
}
this.flushPendingParamEvents();
});
}
stop(when) {
if (when !== undefined && when < 0) {
throw new _errors.RangeError(`when must be a finite non-negative number: ${when}`);
}
// Passing `undefined` lets the worklet stop at its own fresh currentTime.
// Passing 0 would schedule deactivation before the start segment (a no-op).
this.runOnStretcher(node => {
node.stop(when);
});
}
get buffer() {
return this._buffer;
}
set buffer(buffer) {
if (buffer !== null && this.bufferHasBeenSet) {
throw new _errors.InvalidStateError('The buffer can only be set once and cannot be changed afterwards.');
}
this._buffer = buffer;
if (buffer !== null) {
this.bufferHasBeenSet = true;
}
this.runOnStretcher(node => {
node.dropBuffers();
if (!buffer) {
return;
}
const channelArrays = [];
for (let i = 0; i < buffer.numberOfChannels; i++) {
channelArrays.push(buffer.getChannelData(i));
}
node.addBuffers(channelArrays);
});
}
// Unlike the browser AudioBufferSourceNode, the WASM worklet only picks up
// loop bounds when they are scheduled. start() schedules the initial loop;
// these setters must re-schedule so live changes take effect while playing.
applyLoopToStretcher() {
if (!this.hasBeenStarted) {
return;
}
this.runOnStretcher(node => {
if (this._loop && this._loopStart !== -1 && this._loopEnd !== -1) {
node.schedule({
loopStart: this._loopStart,
loopEnd: this._loopEnd
});
} else {
// Zero-length loop disables looping in the worklet going forward.
node.schedule({
loopStart: 0,
loopEnd: 0
});
}
});
}
get loop() {
return this._loop;
}
set loop(value) {
this._loop = value;
this.applyLoopToStretcher();
}
get loopStart() {
return this._loopStart;
}
set loopStart(value) {
this._loopStart = value;
this.applyLoopToStretcher();
}
get loopEnd() {
return this._loopEnd;
}
set loopEnd(value) {
this._loopEnd = value;
this.applyLoopToStretcher();
}
get loopSkip() {
return this._loopSkip;
}
set loopSkip(value) {
this._loopSkip = value;
}
get onLoopEnded() {
return this._onLoopEnded;
}
// The WASM stretcher has no per-loop event; callback is stored but never fired.
set onLoopEnded(callback) {
this._onLoopEnded = callback ?? undefined;
}
get onEnded() {
return this._onEnded;
}
set onEnded(callback) {
this._onEnded = callback;
this.runOnStretcher(node => {
node.onEnded = callback;
});
}
}
exports.default = AudioBufferSourceNodeStretcher;
//# sourceMappingURL=AudioBufferSourceNodeStretcher.js.map