UNPKG

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
"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