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

378 lines (335 loc) • 9.5 kB
import AudioParam from '../../AudioParam.web'; import BaseAudioContext from '../../BaseAudioContext.web'; const RAMP_SAMPLE_INTERVAL = 0.05; type SetValueEvent = { type: 'setValue'; value: number; time: number; }; type LinearRampEvent = { type: 'linearRamp'; value: number; time: number; rampStartTime: number; startValue: number; }; type ExponentialRampEvent = { type: 'exponentialRamp'; value: number; time: number; rampStartTime: number; startValue: number; }; type SetTargetEvent = { type: 'setTarget'; target: number; startValue: number; time: number; timeConstant: number; }; type ValueCurveEvent = { type: 'valueCurve'; values: Float32Array; time: number; duration: number; }; export type StretcherAutomationEvent = | SetValueEvent | LinearRampEvent | ExponentialRampEvent | SetTargetEvent | ValueCurveEvent; type AutomationSample = { value: number; time: number; }; export default class AudioStretcherParam extends AudioParam { override readonly defaultValue: number; override readonly minValue: number; override readonly maxValue: number; private readonly _initialValue: number; private _events: StretcherAutomationEvent[] = []; private readonly _onChange: | ((event: StretcherAutomationEvent) => void) | null; private readonly _onCancel: ((cancelTime: number) => void) | null; constructor( context: BaseAudioContext, initialValue: number, defaultValue: number, minValue: number, maxValue: number, onChange?: (event: StretcherAutomationEvent) => void, onCancel?: (cancelTime: number) => void ) { const source = new globalThis.ConstantSourceNode(context.context, { offset: initialValue, }); source.start(0); super(source.offset, context); this.defaultValue = defaultValue; this.minValue = minValue; this.maxValue = maxValue; this._initialValue = initialValue; this._onChange = onChange ?? null; this._onCancel = onCancel ?? null; } override get value(): number { return super.value; } override set value(v: number) { super.value = v; this._events = [ { type: 'setValue', value: v, time: this.context.currentTime, }, ]; this._onChange?.({ type: 'setValue', value: v, time: this.context.currentTime, }); } getValueAtTime(time: number): number { if (this._events.length === 0) { return this._initialValue; } let value = this._initialValue; let tPrev = -Infinity; const sorted = [...this._events].sort((a, b) => a.time - b.time); for (const event of sorted) { if (event.type === 'setValue') { if (time < event.time) { return value; } value = event.value; tPrev = event.time; continue; } if (event.type === 'linearRamp') { const rampStart = event.rampStartTime ?? tPrev; if (time < rampStart) { continue; } if (time >= event.time) { value = event.value; tPrev = event.time; continue; } const v0 = event.startValue ?? value; const fraction = (time - rampStart) / (event.time - rampStart); return v0 + fraction * (event.value - v0); } if (event.type === 'exponentialRamp') { const rampStart = event.rampStartTime ?? tPrev; if (time < rampStart) { continue; } if (time >= event.time) { value = event.value; tPrev = event.time; continue; } const v0 = event.startValue ?? value; if (v0 === 0 || event.value === 0) { return v0; } const fraction = (time - rampStart) / (event.time - rampStart); return v0 * Math.pow(event.value / v0, fraction); } if (event.type === 'setTarget') { if (time < event.time) { return value; } const dt = time - event.time; const v0 = event.startValue; return ( event.target + (v0 - event.target) * Math.exp(-dt / event.timeConstant) ); } if (event.type === 'valueCurve') { if (time < event.time) { return value; } if (time >= event.time + event.duration) { value = event.values[event.values.length - 1]; tPrev = event.time + event.duration; continue; } const progress = (time - event.time) / event.duration; const idx = progress * (event.values.length - 1); const i0 = Math.floor(idx); const i1 = Math.min(i0 + 1, event.values.length - 1); const frac = idx - i0; return event.values[i0] + frac * (event.values[i1] - event.values[i0]); } } return value; } getRampStartTime(endTime: number): number { let tPrev = -Infinity; const sorted = [...this._events].sort((a, b) => a.time - b.time); for (const event of sorted) { if (event.time >= endTime) { break; } tPrev = event.time; } if (tPrev === -Infinity) { return this.context.currentTime; } return tPrev; } sampleAutomation( event: StretcherAutomationEvent, mapValue: (value: number) => number ): AutomationSample[] { const samples: AutomationSample[] = []; if (event.type === 'setValue') { samples.push({ value: mapValue(event.value), time: event.time, }); return samples; } if (event.type === 'linearRamp' || event.type === 'exponentialRamp') { const startTime = event.rampStartTime ?? this.getRampStartTime(event.time); const endTime = event.time; const duration = endTime - startTime; if (duration <= 0) { samples.push({ value: mapValue(event.value), time: endTime, }); return samples; } const stepCount = Math.max(2, Math.ceil(duration / RAMP_SAMPLE_INTERVAL)); for (let i = 0; i <= stepCount; i++) { const t = startTime + (duration * i) / stepCount; samples.push({ value: mapValue(this.getValueAtTime(t)), time: t, }); } return samples; } if (event.type === 'setTarget') { const startTime = event.time; const duration = Math.max(event.timeConstant * 5, RAMP_SAMPLE_INTERVAL); const stepCount = Math.max(2, Math.ceil(duration / RAMP_SAMPLE_INTERVAL)); for (let i = 0; i <= stepCount; i++) { const t = startTime + (duration * i) / stepCount; samples.push({ value: mapValue(this.getValueAtTime(t)), time: t, }); } return samples; } if (event.type === 'valueCurve') { const stepCount = Math.max( 2, Math.ceil(event.duration / RAMP_SAMPLE_INTERVAL) ); for (let i = 0; i <= stepCount; i++) { const t = event.time + (event.duration * i) / stepCount; samples.push({ value: mapValue(this.getValueAtTime(t)), time: t, }); } return samples; } return samples; } private _recordEvent(event: StretcherAutomationEvent): void { this._events.push(event); } private _removeEventsFrom(cancelTime: number): void { this._events = this._events.filter((event) => event.time < cancelTime); } override setValueAtTime(value: number, startTime: number): AudioParam { const event: SetValueEvent = { type: 'setValue', value, time: startTime, }; this._recordEvent(event); this._onChange?.(event); return this; } override linearRampToValueAtTime(value: number, endTime: number): AudioParam { const rampStartTime = this.getRampStartTime(endTime); const event: LinearRampEvent = { type: 'linearRamp', value, time: endTime, rampStartTime, startValue: this.getValueAtTime(rampStartTime), }; this._recordEvent(event); this._onChange?.(event); return this; } override exponentialRampToValueAtTime( value: number, endTime: number ): AudioParam { const rampStartTime = this.getRampStartTime(endTime); const event: ExponentialRampEvent = { type: 'exponentialRamp', value, time: endTime, rampStartTime, startValue: this.getValueAtTime(rampStartTime), }; this._recordEvent(event); this._onChange?.(event); return this; } override setTargetAtTime( target: number, startTime: number, timeConstant: number ): AudioParam { const event: SetTargetEvent = { type: 'setTarget', target, startValue: this.getValueAtTime(startTime), time: startTime, timeConstant, }; this._recordEvent(event); this._onChange?.(event); return this; } override setValueCurveAtTime( values: Float32Array, startTime: number, duration: number ): AudioParam { const event: ValueCurveEvent = { type: 'valueCurve', values, time: startTime, duration, }; this._recordEvent(event); this._onChange?.(event); return this; } override cancelScheduledValues(cancelTime: number): AudioParam { this._removeEventsFrom(cancelTime); this._onCancel?.(cancelTime); return this; } override cancelAndHoldAtTime(cancelTime: number): AudioParam { this._removeEventsFrom(cancelTime); this._onCancel?.(cancelTime); return this; } }