diff --git a/apps/common-app/src/examples/Record/Record.tsx b/apps/common-app/src/examples/Record/Record.tsx index 78e220a85..fb9ecd0e5 100644 --- a/apps/common-app/src/examples/Record/Record.tsx +++ b/apps/common-app/src/examples/Record/Record.tsx @@ -1,15 +1,15 @@ -import React, { useRef, FC, useEffect } from 'react'; +import React, { FC, useEffect, useRef } from 'react'; import { + AudioBuffer, + AudioBufferSourceNode, AudioContext, AudioManager, AudioRecorder, RecorderAdapterNode, - AudioBufferSourceNode, - AudioBuffer, } from 'react-native-audio-api'; -import { Container, Button } from '../../components'; -import { View, Text } from 'react-native'; +import { Text, View } from 'react-native'; +import { Button, Container } from '../../components'; import { colors } from '../../styles'; const SAMPLE_RATE = 16000; @@ -102,6 +102,8 @@ const Record: FC = () => { audioBuffersRef.current.push(buffer); }); + recorderAdapterRef.current.onAudioReady = () => {}; + recorderRef.current.start(); setTimeout(() => { diff --git a/packages/custom-node-generator/lib/generator.js b/packages/custom-node-generator/lib/generator.js deleted file mode 100644 index 58cf33095..000000000 --- a/packages/custom-node-generator/lib/generator.js +++ /dev/null @@ -1,55 +0,0 @@ -const fs = require('fs-extra'); -const path = require('path'); -const chalk = require('chalk'); - -class FileGenerator { - constructor() { - this.templatesDir = path.join(__dirname, '../templates'); - } - - async generate(options) { - const { outputPath, template } = options; - - const templatePath = path.join(this.templatesDir, template); - - if (!await fs.pathExists(templatePath)) { - throw new Error(`Template not found`); - } - - await fs.ensureDir(outputPath); - - await this.copyTemplate(templatePath, outputPath); - - console.log(chalk.green(`Generated files in: ${outputPath}`)); - } - - async copyTemplate(templatePath, targetPath) { - const files = await fs.readdir(templatePath); - - for (const file of files) { - const srcPath = path.join(templatePath, file); - const destPath = path.join(targetPath, file); - const stat = await fs.stat(srcPath); - - if (stat.isDirectory()) { - await fs.ensureDir(destPath); - await this.copyTemplate(srcPath, destPath); - } else { - await this.processFile(srcPath, destPath); - } - } - } - - async processFile(srcPath, destPath) { - const content = await fs.readFile(srcPath, 'utf-8'); - - await fs.writeFile(destPath, content); - console.log(chalk.cyan(`Created: ${path.relative(process.cwd(), destPath)}`)); - } -} - -const generator = new FileGenerator(); - -module.exports = { - generate: (outputPath, template) => generator.generate({ outputPath, template }), -}; \ No newline at end of file diff --git a/packages/react-native-audio-api/.eslintrc.js b/packages/react-native-audio-api/.eslintrc.js index 4b0cc2ca4..01cdc221c 100644 --- a/packages/react-native-audio-api/.eslintrc.js +++ b/packages/react-native-audio-api/.eslintrc.js @@ -6,5 +6,27 @@ module.exports = { files: ['./src/**/*.{ts,tsx}'], }, ], - ignorePatterns: ['lib', 'src/web-core/custom/signalsmithStretch' ], + ignorePatterns: ['lib', 'src/external/signalsmithStretch'], + settings: { + 'import/resolver': { + node: { + extensions: [ + '.js', + '.jsx', + '.ts', + '.tsx', + '.d.ts', + '.json', + '.web.ts', + '.web.tsx', + '.native.ts', + '.native.tsx', + '.ios.ts', + '.ios.tsx', + '.android.ts', + '.android.tsx', + ], + }, + }, + }, }; diff --git a/packages/react-native-audio-api/android/src/main/java/com/swmansion/audioapi/system/AudioFocusListener.kt b/packages/react-native-audio-api/android/src/main/java/com/swmansion/audioapi/system/AudioFocusListener.kt index e2cdd9727..f3b9b2aa0 100644 --- a/packages/react-native-audio-api/android/src/main/java/com/swmansion/audioapi/system/AudioFocusListener.kt +++ b/packages/react-native-audio-api/android/src/main/java/com/swmansion/audioapi/system/AudioFocusListener.kt @@ -13,7 +13,6 @@ class AudioFocusListener( private val audioAPIModule: WeakReference, private val lockScreenManager: WeakReference, ) : AudioManager.OnAudioFocusChangeListener { - private var playOnAudioFocus: Boolean = false private var focusRequest: AudioFocusRequest? = null override fun onAudioFocusChange(focusChange: Int) { @@ -29,32 +28,20 @@ class AudioFocusListener( audioAPIModule.get()?.invokeHandlerWithEventNameAndEventBody("interruption", body) } AudioManager.AUDIOFOCUS_LOSS_TRANSIENT -> { - playOnAudioFocus = lockScreenManager.get()?.isPlaying == true val body = HashMap().apply { put("type", "began") - put("shouldResume", playOnAudioFocus) + put("shouldResume", false) } audioAPIModule.get()?.invokeHandlerWithEventNameAndEventBody("interruption", body) } AudioManager.AUDIOFOCUS_GAIN -> { - if (playOnAudioFocus) { - val body = - HashMap().apply { - put("type", "ended") - put("shouldResume", true) - } - audioAPIModule.get()?.invokeHandlerWithEventNameAndEventBody("interruption", body) - } else { - val body = - HashMap().apply { - put("type", "ended") - put("shouldResume", false) - } - audioAPIModule.get()?.invokeHandlerWithEventNameAndEventBody("interruption", body) - } - - playOnAudioFocus = false + val body = + HashMap().apply { + put("type", "ended") + put("shouldResume", true) + } + audioAPIModule.get()?.invokeHandlerWithEventNameAndEventBody("interruption", body) } } } diff --git a/packages/react-native-audio-api/common/cpp/audioapi/core/utils/AudioDecoder.h b/packages/react-native-audio-api/common/cpp/audioapi/core/utils/AudioDecoder.h index 77c150164..5750c5c93 100644 --- a/packages/react-native-audio-api/common/cpp/audioapi/core/utils/AudioDecoder.h +++ b/packages/react-native-audio-api/common/cpp/audioapi/core/utils/AudioDecoder.h @@ -29,7 +29,10 @@ class AudioDecoder { static std::shared_ptr makeAudioBufferFromFloatBuffer(const std::vector &buffer, float outputSampleRate, int outputChannels); - static AudioFormat detectAudioFormat(const void *data, size_t size) { + static AudioFormat detectAudioFormat( + const void *data, + size_t size + ) { if (size < 12) return AudioFormat::UNKNOWN; const auto *bytes = static_cast(data); diff --git a/packages/react-native-audio-api/common/cpp/audioapi/utils/AudioBus.cpp b/packages/react-native-audio-api/common/cpp/audioapi/utils/AudioBus.cpp index 5f28b9c7f..990182ae6 100644 --- a/packages/react-native-audio-api/common/cpp/audioapi/utils/AudioBus.cpp +++ b/packages/react-native-audio-api/common/cpp/audioapi/utils/AudioBus.cpp @@ -19,6 +19,10 @@ AudioBus::AudioBus(size_t size, int numberOfChannels, float sampleRate) : numberOfChannels_(numberOfChannels), sampleRate_(sampleRate), size_(size) { + assert(numberOfChannels > 0); + assert(size > 0); + assert(sampleRate > 0.0f); + assert(numberOfChannels < 128); createChannels(); } diff --git a/packages/react-native-audio-api/src/core/WorkletProcessingNode.ts b/packages/react-native-audio-api/prev_src/WorkletProcessingNode.ts similarity index 100% rename from packages/react-native-audio-api/src/core/WorkletProcessingNode.ts rename to packages/react-native-audio-api/prev_src/WorkletProcessingNode.ts diff --git a/packages/react-native-audio-api/src/core/WorkletSourceNode.ts b/packages/react-native-audio-api/prev_src/WorkletSourceNode.ts similarity index 100% rename from packages/react-native-audio-api/src/core/WorkletSourceNode.ts rename to packages/react-native-audio-api/prev_src/WorkletSourceNode.ts diff --git a/packages/react-native-audio-api/prev_src/api.ts b/packages/react-native-audio-api/prev_src/api.ts new file mode 100644 index 000000000..e328758d8 --- /dev/null +++ b/packages/react-native-audio-api/prev_src/api.ts @@ -0,0 +1,78 @@ +import type { + IAudioContext, + IAudioRecorder, + IOfflineAudioContext, +} from './interfaces'; +import { NativeAudioAPIModule } from './specs'; +import { AudioRecorderOptions } from './types'; + +/* eslint-disable no-var */ +declare global { + var createAudioContext: ( + sampleRate: number, + initSuspended: boolean + ) => IAudioContext; + var createOfflineAudioContext: ( + numberOfChannels: number, + length: number, + sampleRate: number + ) => IOfflineAudioContext; + + var createAudioRecorder: (options: AudioRecorderOptions) => IAudioRecorder; +} +/* eslint-disable no-var */ + +if ( + global.createAudioContext == null || + global.createOfflineAudioContext == null || + global.createAudioRecorder == null || + global.AudioEventEmitter == null +) { + if (!NativeAudioAPIModule) { + throw new Error( + `Failed to install react-native-audio-api: The native module could not be found.` + ); + } + + NativeAudioAPIModule.install(); +} + +// export { default as AnalyserNode } from './core/AnalyserNode'; +// export { default as AudioBuffer } from './core/AudioBuffer'; +// export { default as AudioBufferQueueSourceNode } from './core/AudioBufferQueueSourceNode'; +// export { default as AudioBufferSourceNode } from './core/AudioBufferSourceNode'; +// export { default as AudioContext } from './core/AudioContext'; +// export { default as AudioDestinationNode } from './core/AudioDestinationNode'; +// export { default as AudioNode } from './core/AudioNode'; +// export { default as AudioParam } from './core/AudioParam'; +// export { default as AudioRecorder } from './core/AudioRecorder'; +// export { default as AudioScheduledSourceNode } from './core/AudioScheduledSourceNode'; +// export { default as BaseAudioContext } from './core/BaseAudioContext'; +// export { default as BiquadFilterNode } from './core/BiquadFilterNode'; +// export { default as GainNode } from './core/GainNode'; +// export { default as OfflineAudioContext } from './core/OfflineAudioContext'; +// export { default as OscillatorNode } from './core/OscillatorNode'; +// export { default as RecorderAdapterNode } from './core/RecorderAdapterNode'; +// export { default as StereoPannerNode } from './core/StereoPannerNode'; +// export { default as StreamerNode } from './core/StreamerNode'; +// export { default as WorkletNode } from './core/WorkletNode'; +// export { default as useSystemVolume } from './hooks/useSytemVolume'; +// export { default as AudioManager } from './system'; + +// export { +// BiquadFilterType, +// ChannelCountMode, +// ChannelInterpretation, +// ContextState, +// OscillatorType, +// PeriodicWaveConstraints, +// WindowType, +// } from './types'; + +// export { +// IndexSizeError, +// InvalidAccessError, +// InvalidStateError, +// NotSupportedError, +// RangeError, +// } from './errors'; diff --git a/packages/react-native-audio-api/prev_src/api.web.ts b/packages/react-native-audio-api/prev_src/api.web.ts new file mode 100644 index 000000000..5f912fdd6 --- /dev/null +++ b/packages/react-native-audio-api/prev_src/api.web.ts @@ -0,0 +1 @@ +export * from './web-core/custom'; diff --git a/packages/react-native-audio-api/prev_src/core/AudioContext.ts b/packages/react-native-audio-api/prev_src/core/AudioContext.ts new file mode 100644 index 000000000..e197fffc0 --- /dev/null +++ b/packages/react-native-audio-api/prev_src/core/AudioContext.ts @@ -0,0 +1,49 @@ +import { IAudioContext } from '../interfaces'; +import BaseAudioContext from './BaseAudioContext'; +import AudioManager from '../system'; +import { AudioContextOptions } from '../types'; +import { NotSupportedError } from '../errors'; +import { isWorkletsAvailable, workletsModule } from '../utils'; + +export default class AudioContext extends BaseAudioContext { + // We need to keep here a reference to this runtime to better manage its lifecycle + // eslint-disable-next-line @typescript-eslint/no-unused-vars, @typescript-eslint/no-explicit-any + private _audioRuntime: any = null; + + constructor(options?: AudioContextOptions) { + if ( + options && + options.sampleRate && + (options.sampleRate < 8000 || options.sampleRate > 96000) + ) { + throw new NotSupportedError( + `The provided sampleRate is not supported: ${options.sampleRate}` + ); + } + let audioRuntime = null; + if (isWorkletsAvailable) { + audioRuntime = workletsModule.createWorkletRuntime('AudioWorkletRuntime'); + } + + super( + global.createAudioContext( + options?.sampleRate || AudioManager.getDevicePreferredSampleRate(), + options?.initSuspended || false, + audioRuntime + ) + ); + this._audioRuntime = audioRuntime; + } + + async close(): Promise { + return (this.context as IAudioContext).close(); + } + + async resume(): Promise { + return (this.context as IAudioContext).resume(); + } + + async suspend(): Promise { + return (this.context as IAudioContext).suspend(); + } +} diff --git a/packages/react-native-audio-api/src/core/AudioDecoder.ts b/packages/react-native-audio-api/prev_src/core/AudioDecoder.ts similarity index 100% rename from packages/react-native-audio-api/src/core/AudioDecoder.ts rename to packages/react-native-audio-api/prev_src/core/AudioDecoder.ts diff --git a/packages/react-native-audio-api/src/core/AudioRecorder.ts b/packages/react-native-audio-api/prev_src/core/AudioRecorder.ts similarity index 100% rename from packages/react-native-audio-api/src/core/AudioRecorder.ts rename to packages/react-native-audio-api/prev_src/core/AudioRecorder.ts index 77eedc389..69be78bee 100644 --- a/packages/react-native-audio-api/src/core/AudioRecorder.ts +++ b/packages/react-native-audio-api/prev_src/core/AudioRecorder.ts @@ -1,8 +1,8 @@ +import { AudioEventEmitter } from '../events'; +import { OnAudioReadyEventType } from '../events/types'; import { IAudioRecorder } from '../interfaces'; import { AudioRecorderOptions } from '../types'; import AudioBuffer from './AudioBuffer'; -import { OnAudioReadyEventType } from '../events/types'; -import { AudioEventEmitter } from '../events'; import RecorderAdapterNode from './RecorderAdapterNode'; export default class AudioRecorder { diff --git a/packages/react-native-audio-api/src/core/AudioStretcher.ts b/packages/react-native-audio-api/prev_src/core/AudioStretcher.ts similarity index 100% rename from packages/react-native-audio-api/src/core/AudioStretcher.ts rename to packages/react-native-audio-api/prev_src/core/AudioStretcher.ts diff --git a/packages/react-native-audio-api/prev_src/core/BaseAudioContext.ts b/packages/react-native-audio-api/prev_src/core/BaseAudioContext.ts new file mode 100644 index 000000000..8482ac61e --- /dev/null +++ b/packages/react-native-audio-api/prev_src/core/BaseAudioContext.ts @@ -0,0 +1,292 @@ +import { InvalidAccessError, NotSupportedError } from '../errors'; +import { IBaseAudioContext } from '../interfaces'; +import { + AudioBufferBaseSourceNodeOptions, + ContextState, + PeriodicWaveConstraints, + AudioWorkletRuntime, +} from '../types'; +import { isWorkletsAvailable, workletsModule } from '../utils'; +import WorkletSourceNode from './WorkletSourceNode'; +import WorkletProcessingNode from './WorkletProcessingNode'; +import AnalyserNode from './AnalyserNode'; +import AudioBuffer from './AudioBuffer'; +import AudioBufferQueueSourceNode from './AudioBufferQueueSourceNode'; +import AudioBufferSourceNode from './AudioBufferSourceNode'; +import AudioDestinationNode from './AudioDestinationNode'; +import BiquadFilterNode from './BiquadFilterNode'; +import ConstantSourceNode from './ConstantSourceNode'; +import GainNode from './GainNode'; +import OscillatorNode from './OscillatorNode'; +import PeriodicWave from './PeriodicWave'; +import RecorderAdapterNode from './RecorderAdapterNode'; +import StereoPannerNode from './StereoPannerNode'; +import StreamerNode from './StreamerNode'; +import WorkletNode from './WorkletNode'; +import { decodeAudioData, decodePCMInBase64 } from './AudioDecoder'; + +export default class BaseAudioContext { + readonly destination: AudioDestinationNode; + readonly sampleRate: number; + readonly context: IBaseAudioContext; + + constructor(context: IBaseAudioContext) { + this.context = context; + this.destination = new AudioDestinationNode(this, context.destination); + this.sampleRate = context.sampleRate; + } + + public get currentTime(): number { + return this.context.currentTime; + } + + public get state(): ContextState { + return this.context.state; + } + + public async decodeAudioData( + input: string | ArrayBuffer, + sampleRate?: number + ): Promise { + if (!(typeof input === 'string' || input instanceof ArrayBuffer)) { + throw new TypeError('Input must be a string or ArrayBuffer'); + } + return await decodeAudioData(input, sampleRate ?? this.sampleRate); + } + + public async decodePCMInBase64( + base64String: string, + inputSampleRate: number, + inputChannelCount: number, + isInterleaved: boolean = true + ): Promise { + return await decodePCMInBase64( + base64String, + inputSampleRate, + inputChannelCount, + isInterleaved + ); + } + + createWorkletNode( + callback: (audioData: Array, channelCount: number) => void, + bufferLength: number, + inputChannelCount: number, + workletRuntime: AudioWorkletRuntime = 'AudioRuntime' + ): WorkletNode { + if (inputChannelCount < 1 || inputChannelCount > 32) { + throw new NotSupportedError( + `The number of input channels provided (${inputChannelCount}) can not be less than 1 or greater than 32` + ); + } + if (bufferLength < 1) { + throw new NotSupportedError( + `The buffer length provided (${bufferLength}) can not be less than 1` + ); + } + + if (isWorkletsAvailable) { + const shareableWorklet = workletsModule.makeShareableCloneRecursive( + (audioBuffers: Array, channelCount: number) => { + 'worklet'; + const floatAudioData: Array = audioBuffers.map( + (buffer) => new Float32Array(buffer) + ); + callback(floatAudioData, channelCount); + } + ); + return new WorkletNode( + this, + this.context.createWorkletNode( + shareableWorklet, + workletRuntime === 'UIRuntime', + bufferLength, + inputChannelCount + ) + ); + } + /// User does not have worklets as a dependency so he cannot use the worklet API. + throw new Error( + '[RnAudioApi] Worklets are not available, please install react-native-worklets as a dependency. Refer to documentation for more details.' + ); + } + + createWorkletProcessingNode( + callback: ( + inputData: Array, + outputData: Array, + framesToProcess: number, + currentTime: number + ) => void, + workletRuntime: AudioWorkletRuntime = 'AudioRuntime' + ): WorkletProcessingNode { + if (isWorkletsAvailable) { + const shareableWorklet = workletsModule.makeShareableCloneRecursive( + ( + inputBuffers: Array, + outputBuffers: Array, + framesToProcess: number, + currentTime: number + ) => { + 'worklet'; + const inputData: Array = inputBuffers.map( + (buffer) => new Float32Array(buffer, 0, framesToProcess) + ); + const outputData: Array = outputBuffers.map( + (buffer) => new Float32Array(buffer, 0, framesToProcess) + ); + callback(inputData, outputData, framesToProcess, currentTime); + } + ); + return new WorkletProcessingNode( + this, + this.context.createWorkletProcessingNode( + shareableWorklet, + workletRuntime === 'UIRuntime' + ) + ); + } + /// User does not have worklets as a dependency so he cannot use the worklet API. + throw new Error( + '[RnAudioApi] Worklets are not available, please install react-native-worklets as a dependency. Refer to documentation for more details.' + ); + } + + createWorkletSourceNode( + callback: ( + audioData: Array, + framesToProcess: number, + currentTime: number, + startOffset: number + ) => void, + workletRuntime: AudioWorkletRuntime = 'AudioRuntime' + ): WorkletSourceNode { + if (!isWorkletsAvailable) { + /// User does not have worklets as a dependency so he cannot use the worklet API. + throw new Error( + '[RnAudioApi] Worklets are not available, please install react-native-worklets as a dependency. Refer to documentation for more details.' + ); + } + const shareableWorklet = workletsModule.makeShareableCloneRecursive( + ( + audioBuffers: Array, + framesToProcess: number, + currentTime: number, + startOffset: number + ) => { + 'worklet'; + const floatAudioData: Array = audioBuffers.map( + (buffer) => new Float32Array(buffer) + ); + callback(floatAudioData, framesToProcess, currentTime, startOffset); + } + ); + return new WorkletSourceNode( + this, + this.context.createWorkletSourceNode( + shareableWorklet, + workletRuntime === 'UIRuntime' + ) + ); + } + + createRecorderAdapter(): RecorderAdapterNode { + return new RecorderAdapterNode(this, this.context.createRecorderAdapter()); + } + + createOscillator(): OscillatorNode { + return new OscillatorNode(this, this.context.createOscillator()); + } + + createStreamer(): StreamerNode { + return new StreamerNode(this, this.context.createStreamer()); + } + + createConstantSource(): ConstantSourceNode { + return new ConstantSourceNode(this, this.context.createConstantSource()); + } + + createGain(): GainNode { + return new GainNode(this, this.context.createGain()); + } + + createStereoPanner(): StereoPannerNode { + return new StereoPannerNode(this, this.context.createStereoPanner()); + } + + createBiquadFilter(): BiquadFilterNode { + return new BiquadFilterNode(this, this.context.createBiquadFilter()); + } + + createBufferSource( + options?: AudioBufferBaseSourceNodeOptions + ): AudioBufferSourceNode { + const pitchCorrection = options?.pitchCorrection ?? false; + + return new AudioBufferSourceNode( + this, + this.context.createBufferSource(pitchCorrection) + ); + } + + createBufferQueueSource( + options?: AudioBufferBaseSourceNodeOptions + ): AudioBufferQueueSourceNode { + const pitchCorrection = options?.pitchCorrection ?? false; + + return new AudioBufferQueueSourceNode( + this, + this.context.createBufferQueueSource(pitchCorrection) + ); + } + + createBuffer( + numOfChannels: number, + length: number, + sampleRate: number + ): AudioBuffer { + if (numOfChannels < 1 || numOfChannels >= 32) { + throw new NotSupportedError( + `The number of channels provided (${numOfChannels}) is outside the range [1, 32]` + ); + } + + if (length <= 0) { + throw new NotSupportedError( + `The number of frames provided (${length}) is less than or equal to the minimum bound (0)` + ); + } + + if (sampleRate < 8000 || sampleRate > 96000) { + throw new NotSupportedError( + `The sample rate provided (${sampleRate}) is outside the range [8000, 96000]` + ); + } + + return new AudioBuffer( + this.context.createBuffer(numOfChannels, length, sampleRate) + ); + } + + createPeriodicWave( + real: Float32Array, + imag: Float32Array, + constraints?: PeriodicWaveConstraints + ): PeriodicWave { + if (real.length !== imag.length) { + throw new InvalidAccessError( + `The lengths of the real (${real.length}) and imaginary (${imag.length}) arrays must match.` + ); + } + + const disableNormalization = constraints?.disableNormalization ?? false; + + return new PeriodicWave( + this.context.createPeriodicWave(real, imag, disableNormalization) + ); + } + + createAnalyser(): AnalyserNode { + return new AnalyserNode(this, this.context.createAnalyser()); + } +} diff --git a/packages/react-native-audio-api/src/core/OfflineAudioContext.ts b/packages/react-native-audio-api/prev_src/core/OfflineAudioContext.ts similarity index 100% rename from packages/react-native-audio-api/src/core/OfflineAudioContext.ts rename to packages/react-native-audio-api/prev_src/core/OfflineAudioContext.ts diff --git a/packages/react-native-audio-api/src/core/RecorderAdapterNode.ts b/packages/react-native-audio-api/prev_src/core/RecorderAdapterNode.ts similarity index 100% rename from packages/react-native-audio-api/src/core/RecorderAdapterNode.ts rename to packages/react-native-audio-api/prev_src/core/RecorderAdapterNode.ts diff --git a/packages/react-native-audio-api/src/core/StreamerNode.ts b/packages/react-native-audio-api/prev_src/core/StreamerNode.ts similarity index 100% rename from packages/react-native-audio-api/src/core/StreamerNode.ts rename to packages/react-native-audio-api/prev_src/core/StreamerNode.ts diff --git a/packages/react-native-audio-api/src/core/WorkletNode.ts b/packages/react-native-audio-api/prev_src/core/WorkletNode.ts similarity index 100% rename from packages/react-native-audio-api/src/core/WorkletNode.ts rename to packages/react-native-audio-api/prev_src/core/WorkletNode.ts diff --git a/packages/react-native-audio-api/prev_src/interfaces.ts b/packages/react-native-audio-api/prev_src/interfaces.ts new file mode 100644 index 000000000..b4a31032e --- /dev/null +++ b/packages/react-native-audio-api/prev_src/interfaces.ts @@ -0,0 +1,139 @@ +import { AudioEventCallback, AudioEventName } from './events/types'; +import { ContextState } from './types'; + +export type WorkletNodeCallback = ( + audioData: Array, + channelCount: number +) => void; + +export type WorkletSourceNodeCallback = ( + audioData: Array, + framesToProcess: number, + currentTime: number, + startOffset: number +) => void; + +export type WorkletProcessingNodeCallback = ( + inputData: Array, + outputData: Array, + framesToProcess: number, + currentTime: number +) => void; + +export type ShareableWorkletCallback = + | WorkletNodeCallback + | WorkletSourceNodeCallback + | WorkletProcessingNodeCallback; + +export interface IBaseAudioContext { + readonly destination: IAudioDestinationNode; + readonly state: ContextState; + readonly sampleRate: number; + readonly currentTime: number; + readonly decoder: IAudioDecoder; + readonly stretcher: IAudioStretcher; + + createRecorderAdapter(): IRecorderAdapterNode; + createWorkletSourceNode( + shareableWorklet: ShareableWorkletCallback, + shouldUseUiRuntime: boolean + ): IWorkletSourceNode; + createWorkletNode( + shareableWorklet: ShareableWorkletCallback, + shouldUseUiRuntime: boolean, + bufferLength: number, + inputChannelCount: number + ): IWorkletNode; + createWorkletProcessingNode( + shareableWorklet: ShareableWorkletCallback, + shouldUseUiRuntime: boolean + ): IWorkletProcessingNode; + createOscillator(): IOscillatorNode; + createConstantSource(): IConstantSourceNode; + createGain(): IGainNode; + createStereoPanner(): IStereoPannerNode; + createBiquadFilter: () => IBiquadFilterNode; + createBufferSource: (pitchCorrection: boolean) => IAudioBufferSourceNode; + createBufferQueueSource: ( + pitchCorrection: boolean + ) => IAudioBufferQueueSourceNode; + createBuffer: ( + channels: number, + length: number, + sampleRate: number + ) => IAudioBuffer; + createPeriodicWave: ( + real: Float32Array, + imag: Float32Array, + disableNormalization: boolean + ) => IPeriodicWave; + createAnalyser: () => IAnalyserNode; + createStreamer: () => IStreamerNode; +} + +export interface IAudioContext extends IBaseAudioContext { + close(): Promise; + resume(): Promise; + suspend(): Promise; +} + +export interface IOfflineAudioContext extends IBaseAudioContext { + resume(): Promise; + suspend(suspendTime: number): Promise; + startRendering(): Promise; +} + +export interface IStreamerNode extends IAudioNode { + initialize(streamPath: string): boolean; +} + +export interface IWorkletNode extends IAudioNode {} + +export interface IWorkletSourceNode extends IAudioScheduledSourceNode {} + +export interface IWorkletProcessingNode extends IAudioNode {} + +export interface IAudioRecorder { + start: () => void; + stop: () => void; + connect: (node: IRecorderAdapterNode) => void; + disconnect: () => void; + + // passing subscriptionId(uint_64 in cpp, string in js) to the cpp + onAudioReady: string; +} + +export interface IAudioDecoder { + decodeWithMemoryBlock: ( + arrayBuffer: ArrayBuffer, + sampleRate?: number + ) => Promise; + decodeWithFilePath: ( + sourcePath: string, + sampleRate?: number + ) => Promise; + decodeWithPCMInBase64: ( + b64: string, + inputSampleRate: number, + inputChannelCount: number, + interleaved?: boolean + ) => Promise; +} + +export interface IAudioStretcher { + changePlaybackSpeed: ( + arrayBuffer: AudioBuffer, + playbackSpeed: number + ) => Promise; +} + +export interface IAudioEventEmitter { + addAudioEventListener( + name: Name, + callback: AudioEventCallback + ): string; + removeAudioEventListener( + name: Name, + subscriptionId: string + ): void; +} diff --git a/packages/react-native-audio-api/src/types.ts b/packages/react-native-audio-api/prev_src/types.ts similarity index 70% rename from packages/react-native-audio-api/src/types.ts rename to packages/react-native-audio-api/prev_src/types.ts index 348deb4f4..711010bbe 100644 --- a/packages/react-native-audio-api/src/types.ts +++ b/packages/react-native-audio-api/prev_src/types.ts @@ -1,17 +1,3 @@ -export type ChannelCountMode = 'max' | 'clamped-max' | 'explicit'; - -export type ChannelInterpretation = 'speakers' | 'discrete'; - -export type BiquadFilterType = - | 'lowpass' - | 'highpass' - | 'bandpass' - | 'lowshelf' - | 'highshelf' - | 'peaking' - | 'notch' - | 'allpass'; - export type ContextState = 'running' | 'closed' | `suspended`; export type AudioWorkletRuntime = 'AudioRuntime' | 'UIRuntime'; @@ -43,8 +29,6 @@ export interface AudioRecorderOptions { bufferLengthInSamples: number; } -export type WindowType = 'blackman' | 'hann'; - export interface AudioBufferBaseSourceNodeOptions { pitchCorrection: boolean; } diff --git a/packages/react-native-audio-api/prev_src/utils/index.ts b/packages/react-native-audio-api/prev_src/utils/index.ts new file mode 100644 index 000000000..68e5035b3 --- /dev/null +++ b/packages/react-native-audio-api/prev_src/utils/index.ts @@ -0,0 +1,21 @@ +import type { ShareableWorkletCallback } from '../interfaces'; + +interface SimplifiedWorkletModule { + makeShareableCloneRecursive: ( + workletCallback: ShareableWorkletCallback + ) => ShareableWorkletCallback; +} + +export function clamp(value: number, min: number, max: number): number { + return Math.min(Math.max(value, min), max); +} + +export let isWorkletsAvailable = false; +export let workletsModule: SimplifiedWorkletModule; + +try { + workletsModule = require('react-native-worklets'); + isWorkletsAvailable = true; +} catch (error) { + isWorkletsAvailable = false; +} diff --git a/packages/react-native-audio-api/src/web-core/AudioContext.tsx b/packages/react-native-audio-api/prev_src/web-core/AudioContext.tsx similarity index 100% rename from packages/react-native-audio-api/src/web-core/AudioContext.tsx rename to packages/react-native-audio-api/prev_src/web-core/AudioContext.tsx diff --git a/packages/react-native-audio-api/src/web-core/BaseAudioContext.tsx b/packages/react-native-audio-api/prev_src/web-core/BaseAudioContext.tsx similarity index 100% rename from packages/react-native-audio-api/src/web-core/BaseAudioContext.tsx rename to packages/react-native-audio-api/prev_src/web-core/BaseAudioContext.tsx diff --git a/packages/react-native-audio-api/src/web-core/ConstantSourceNode.tsx b/packages/react-native-audio-api/prev_src/web-core/ConstantSourceNode.tsx similarity index 100% rename from packages/react-native-audio-api/src/web-core/ConstantSourceNode.tsx rename to packages/react-native-audio-api/prev_src/web-core/ConstantSourceNode.tsx diff --git a/packages/react-native-audio-api/src/web-core/OfflineAudioContext.tsx b/packages/react-native-audio-api/prev_src/web-core/OfflineAudioContext.tsx similarity index 100% rename from packages/react-native-audio-api/src/web-core/OfflineAudioContext.tsx rename to packages/react-native-audio-api/prev_src/web-core/OfflineAudioContext.tsx diff --git a/packages/react-native-audio-api/src/api.ts b/packages/react-native-audio-api/src/api.ts deleted file mode 100644 index 3fb9918bd..000000000 --- a/packages/react-native-audio-api/src/api.ts +++ /dev/null @@ -1,99 +0,0 @@ -import { NativeAudioAPIModule } from './specs'; -import { AudioRecorderOptions } from './types'; -import type { - IAudioContext, - IAudioDecoder, - IAudioRecorder, - IAudioStretcher, - IOfflineAudioContext, - IAudioEventEmitter, -} from './interfaces'; - -/* eslint-disable no-var */ -declare global { - var createAudioContext: ( - sampleRate: number, - initSuspended: boolean, - // eslint-disable-next-line @typescript-eslint/no-explicit-any - audioWorkletRuntime: any - ) => IAudioContext; - var createOfflineAudioContext: ( - numberOfChannels: number, - length: number, - sampleRate: number, - // eslint-disable-next-line @typescript-eslint/no-explicit-any - audioWorkletRuntime: any - ) => IOfflineAudioContext; - - var createAudioRecorder: (options: AudioRecorderOptions) => IAudioRecorder; - - var createAudioDecoder: () => IAudioDecoder; - - var createAudioStretcher: () => IAudioStretcher; - - var AudioEventEmitter: IAudioEventEmitter; -} -/* eslint-disable no-var */ - -if ( - global.createAudioContext == null || - global.createOfflineAudioContext == null || - global.createAudioRecorder == null || - global.createAudioDecoder == null || - global.createAudioStretcher == null || - global.AudioEventEmitter == null -) { - if (!NativeAudioAPIModule) { - throw new Error( - `Failed to install react-native-audio-api: The native module could not be found.` - ); - } - - NativeAudioAPIModule.install(); -} - -export { default as WorkletNode } from './core/WorkletNode'; -export { default as WorkletSourceNode } from './core/WorkletSourceNode'; -export { default as WorkletProcessingNode } from './core/WorkletProcessingNode'; -export { default as RecorderAdapterNode } from './core/RecorderAdapterNode'; -export { default as AudioBuffer } from './core/AudioBuffer'; -export { default as AudioBufferSourceNode } from './core/AudioBufferSourceNode'; -export { default as AudioBufferQueueSourceNode } from './core/AudioBufferQueueSourceNode'; -export { default as AudioContext } from './core/AudioContext'; -export { default as OfflineAudioContext } from './core/OfflineAudioContext'; -export { default as AudioDestinationNode } from './core/AudioDestinationNode'; -export { default as AudioNode } from './core/AudioNode'; -export { default as AnalyserNode } from './core/AnalyserNode'; -export { default as AudioParam } from './core/AudioParam'; -export { default as AudioScheduledSourceNode } from './core/AudioScheduledSourceNode'; -export { default as BaseAudioContext } from './core/BaseAudioContext'; -export { default as BiquadFilterNode } from './core/BiquadFilterNode'; -export { default as GainNode } from './core/GainNode'; -export { default as OscillatorNode } from './core/OscillatorNode'; -export { default as StereoPannerNode } from './core/StereoPannerNode'; -export { default as AudioRecorder } from './core/AudioRecorder'; -export { default as StreamerNode } from './core/StreamerNode'; -export { default as ConstantSourceNode } from './core/ConstantSourceNode'; -export { default as AudioManager } from './system'; -export { default as useSystemVolume } from './hooks/useSystemVolume'; -export { decodeAudioData, decodePCMInBase64 } from './core/AudioDecoder'; -export { default as changePlaybackSpeed } from './core/AudioStretcher'; - -export { - OscillatorType, - BiquadFilterType, - ChannelCountMode, - ChannelInterpretation, - ContextState, - WindowType, - PeriodicWaveConstraints, - AudioWorkletRuntime, -} from './types'; - -export { - IndexSizeError, - InvalidAccessError, - InvalidStateError, - RangeError, - NotSupportedError, -} from './errors'; diff --git a/packages/react-native-audio-api/src/api.web.ts b/packages/react-native-audio-api/src/api.web.ts deleted file mode 100644 index 39d7b71ae..000000000 --- a/packages/react-native-audio-api/src/api.web.ts +++ /dev/null @@ -1,35 +0,0 @@ -export { default as AudioBuffer } from './web-core/AudioBuffer'; -export { default as AudioBufferSourceNode } from './web-core/AudioBufferSourceNode'; -export { default as AudioContext } from './web-core/AudioContext'; -export { default as OfflineAudioContext } from './web-core/OfflineAudioContext'; -export { default as AudioDestinationNode } from './web-core/AudioDestinationNode'; -export { default as AudioNode } from './web-core/AudioNode'; -export { default as AnalyserNode } from './web-core/AnalyserNode'; -export { default as AudioParam } from './web-core/AudioParam'; -export { default as AudioScheduledSourceNode } from './web-core/AudioScheduledSourceNode'; -export { default as BaseAudioContext } from './web-core/BaseAudioContext'; -export { default as BiquadFilterNode } from './web-core/BiquadFilterNode'; -export { default as GainNode } from './web-core/GainNode'; -export { default as OscillatorNode } from './web-core/OscillatorNode'; -export { default as StereoPannerNode } from './web-core/StereoPannerNode'; -export { default as ConstantSourceNode } from './web-core/ConstantSourceNode'; - -export * from './web-core/custom'; - -export { - OscillatorType, - BiquadFilterType, - ChannelCountMode, - ChannelInterpretation, - ContextState, - WindowType, - PeriodicWaveConstraints, -} from './types'; - -export { - IndexSizeError, - InvalidAccessError, - InvalidStateError, - RangeError, - NotSupportedError, -} from './errors'; diff --git a/packages/react-native-audio-api/src/core/AnalyserNode.ts b/packages/react-native-audio-api/src/core/AnalyserNode.ts deleted file mode 100644 index d2d8176e1..000000000 --- a/packages/react-native-audio-api/src/core/AnalyserNode.ts +++ /dev/null @@ -1,94 +0,0 @@ -import { IndexSizeError } from '../errors'; -import { IAnalyserNode } from '../interfaces'; -import { WindowType } from '../types'; -import AudioNode from './AudioNode'; - -export default class AnalyserNode extends AudioNode { - private static allowedFFTSize: number[] = [ - 32, 64, 128, 256, 512, 1024, 2048, 4096, 8192, 16384, 32768, - ]; - - public get fftSize(): number { - return (this.node as IAnalyserNode).fftSize; - } - - public set fftSize(value: number) { - if (!AnalyserNode.allowedFFTSize.includes(value)) { - throw new IndexSizeError( - `Provided value (${value}) must be a power of 2 between 32 and 32768` - ); - } - - (this.node as IAnalyserNode).fftSize = value; - } - - public get minDecibels(): number { - return (this.node as IAnalyserNode).minDecibels; - } - - public set minDecibels(value: number) { - if (value >= this.maxDecibels) { - throw new IndexSizeError( - `The minDecibels value (${value}) must be less than maxDecibels` - ); - } - - (this.node as IAnalyserNode).minDecibels = value; - } - - public get maxDecibels(): number { - return (this.node as IAnalyserNode).maxDecibels; - } - - public set maxDecibels(value: number) { - if (value <= this.minDecibels) { - throw new IndexSizeError( - `The maxDecibels value (${value}) must be greater than minDecibels` - ); - } - - (this.node as IAnalyserNode).maxDecibels = value; - } - - public get smoothingTimeConstant(): number { - return (this.node as IAnalyserNode).smoothingTimeConstant; - } - - public set smoothingTimeConstant(value: number) { - if (value < 0 || value > 1) { - throw new IndexSizeError( - `The smoothingTimeConstant value (${value}) must be between 0 and 1` - ); - } - - (this.node as IAnalyserNode).smoothingTimeConstant = value; - } - - public get window(): WindowType { - return (this.node as IAnalyserNode).window; - } - - public set window(value: WindowType) { - (this.node as IAnalyserNode).window = value; - } - - public get frequencyBinCount(): number { - return (this.node as IAnalyserNode).frequencyBinCount; - } - - public getFloatFrequencyData(array: Float32Array): void { - (this.node as IAnalyserNode).getFloatFrequencyData(array); - } - - public getByteFrequencyData(array: Uint8Array): void { - (this.node as IAnalyserNode).getByteFrequencyData(array); - } - - public getFloatTimeDomainData(array: Float32Array): void { - (this.node as IAnalyserNode).getFloatTimeDomainData(array); - } - - public getByteTimeDomainData(array: Uint8Array): void { - (this.node as IAnalyserNode).getByteTimeDomainData(array); - } -} diff --git a/packages/react-native-audio-api/src/core/AudioBuffer.ts b/packages/react-native-audio-api/src/core/AudioBuffer.ts index 671e48d9d..9c412a5c7 100644 --- a/packages/react-native-audio-api/src/core/AudioBuffer.ts +++ b/packages/react-native-audio-api/src/core/AudioBuffer.ts @@ -1,15 +1,15 @@ -import { IAudioBuffer } from '../interfaces'; import { IndexSizeError } from '../errors'; +import type { IGenericAudioBuffer } from '../types/generics'; -export default class AudioBuffer { +export default class AudioBuffer implements IGenericAudioBuffer { readonly length: number; readonly duration: number; readonly sampleRate: number; readonly numberOfChannels: number; /** @internal */ - public readonly buffer: IAudioBuffer; + public readonly buffer: IGenericAudioBuffer; - constructor(buffer: IAudioBuffer) { + constructor(buffer: IGenericAudioBuffer) { this.buffer = buffer; this.length = buffer.length; this.duration = buffer.duration; @@ -17,17 +17,18 @@ export default class AudioBuffer { this.numberOfChannels = buffer.numberOfChannels; } - public getChannelData(channel: number): Float32Array { + public getChannelData(channel: number): Float32Array { if (channel < 0 || channel >= this.numberOfChannels) { throw new IndexSizeError( `The channel number provided (${channel}) is outside the range [0, ${this.numberOfChannels - 1}]` ); } + return this.buffer.getChannelData(channel); } public copyFromChannel( - destination: Float32Array, + destination: Float32Array, channelNumber: number, startInChannel: number = 0 ): void { @@ -47,7 +48,7 @@ export default class AudioBuffer { } public copyToChannel( - source: Float32Array, + source: Float32Array, channelNumber: number, startInChannel: number = 0 ): void { diff --git a/packages/react-native-audio-api/src/core/AudioBufferBaseSourceNode.ts b/packages/react-native-audio-api/src/core/AudioBufferBaseSourceNode.ts deleted file mode 100644 index 4b0a37ae1..000000000 --- a/packages/react-native-audio-api/src/core/AudioBufferBaseSourceNode.ts +++ /dev/null @@ -1,54 +0,0 @@ -import AudioParam from './AudioParam'; -import BaseAudioContext from './BaseAudioContext'; -import { AudioEventSubscription } from '../events'; -import { EventTypeWithValue } from '../events/types'; -import { IAudioBufferBaseSourceNode } from '../interfaces'; -import AudioScheduledSourceNode from './AudioScheduledSourceNode'; - -export default class AudioBufferBaseSourceNode extends AudioScheduledSourceNode { - readonly playbackRate: AudioParam; - readonly detune: AudioParam; - private onPositionChangedSubscription?: AudioEventSubscription; - private onPositionChangedCallback?: (event: EventTypeWithValue) => void; - - constructor(context: BaseAudioContext, node: IAudioBufferBaseSourceNode) { - super(context, node); - - this.detune = new AudioParam(node.detune, context); - this.playbackRate = new AudioParam(node.playbackRate, context); - } - - public get onPositionChanged(): - | ((event: EventTypeWithValue) => void) - | undefined { - return this.onPositionChangedCallback; - } - - public set onPositionChanged( - callback: ((event: EventTypeWithValue) => void) | null - ) { - if (!callback) { - (this.node as IAudioBufferBaseSourceNode).onPositionChanged = '0'; - this.onPositionChangedSubscription?.remove(); - this.onPositionChangedSubscription = undefined; - this.onPositionChangedCallback = undefined; - - return; - } - - this.onPositionChangedCallback = callback; - this.onPositionChangedSubscription = - this.audioEventEmitter.addAudioEventListener('positionChanged', callback); - - (this.node as IAudioBufferBaseSourceNode).onPositionChanged = - this.onPositionChangedSubscription.subscriptionId; - } - - public get onPositionChangedInterval(): number { - return (this.node as IAudioBufferBaseSourceNode).onPositionChangedInterval; - } - - public set onPositionChangedInterval(value: number) { - (this.node as IAudioBufferBaseSourceNode).onPositionChangedInterval = value; - } -} diff --git a/packages/react-native-audio-api/src/core/AudioBufferQueueSourceNode.ts b/packages/react-native-audio-api/src/core/AudioBufferQueueSourceNode.ts deleted file mode 100644 index 13bab8779..000000000 --- a/packages/react-native-audio-api/src/core/AudioBufferQueueSourceNode.ts +++ /dev/null @@ -1,50 +0,0 @@ -import { IAudioBufferQueueSourceNode } from '../interfaces'; -import AudioBufferBaseSourceNode from './AudioBufferBaseSourceNode'; -import AudioBuffer from './AudioBuffer'; -import { RangeError } from '../errors'; - -export default class AudioBufferQueueSourceNode extends AudioBufferBaseSourceNode { - public enqueueBuffer(buffer: AudioBuffer): string { - return (this.node as IAudioBufferQueueSourceNode).enqueueBuffer( - buffer.buffer - ); - } - - public dequeueBuffer(bufferId: string): void { - const id = parseInt(bufferId, 10); - if (isNaN(id) || id < 0) { - throw new RangeError( - `bufferId must be a non-negative integer: ${bufferId}` - ); - } - (this.node as IAudioBufferQueueSourceNode).dequeueBuffer(id); - } - - public clearBuffers(): void { - (this.node as IAudioBufferQueueSourceNode).clearBuffers(); - } - - public override start(when: number = 0): void { - if (when < 0) { - throw new RangeError( - `when must be a finite non-negative number: ${when}` - ); - } - - (this.node as IAudioBufferQueueSourceNode).start(when); - } - - public override stop(when: number = 0): void { - if (when < 0) { - throw new RangeError( - `when must be a finite non-negative number: ${when}` - ); - } - - (this.node as IAudioBufferQueueSourceNode).stop(when); - } - - public pause(): void { - (this.node as IAudioBufferQueueSourceNode).pause(); - } -} diff --git a/packages/react-native-audio-api/src/core/AudioBufferSourceNode.ts b/packages/react-native-audio-api/src/core/AudioBufferSourceNode.ts deleted file mode 100644 index e8dba4a23..000000000 --- a/packages/react-native-audio-api/src/core/AudioBufferSourceNode.ts +++ /dev/null @@ -1,121 +0,0 @@ -import { IAudioBufferSourceNode } from '../interfaces'; -import AudioBufferBaseSourceNode from './AudioBufferBaseSourceNode'; -import AudioBuffer from './AudioBuffer'; -import { InvalidStateError, RangeError } from '../errors'; -import { EventEmptyType } from '../events/types'; -import { AudioEventSubscription } from '../events'; - -export default class AudioBufferSourceNode extends AudioBufferBaseSourceNode { - private onLoopEndedSubscription?: AudioEventSubscription; - private onLoopEndedCallback?: (event: EventEmptyType) => void; - - public get buffer(): AudioBuffer | null { - const buffer = (this.node as IAudioBufferSourceNode).buffer; - if (!buffer) { - return null; - } - return new AudioBuffer(buffer); - } - - public set buffer(buffer: AudioBuffer | null) { - if (!buffer) { - (this.node as IAudioBufferSourceNode).setBuffer(null); - return; - } - - (this.node as IAudioBufferSourceNode).setBuffer(buffer.buffer); - } - - public get loopSkip(): boolean { - return (this.node as IAudioBufferSourceNode).loopSkip; - } - - public set loopSkip(value: boolean) { - (this.node as IAudioBufferSourceNode).loopSkip = value; - } - - public get loop(): boolean { - return (this.node as IAudioBufferSourceNode).loop; - } - - public set loop(value: boolean) { - (this.node as IAudioBufferSourceNode).loop = value; - } - - public get loopStart(): number { - return (this.node as IAudioBufferSourceNode).loopStart; - } - - public set loopStart(value: number) { - (this.node as IAudioBufferSourceNode).loopStart = value; - } - - public get loopEnd(): number { - return (this.node as IAudioBufferSourceNode).loopEnd; - } - - public set loopEnd(value: number) { - (this.node as IAudioBufferSourceNode).loopEnd = value; - } - - public start(when: number = 0, offset: number = 0, duration?: number): void { - if (when < 0) { - throw new RangeError( - `when must be a finite non-negative number: ${when}` - ); - } - - if (offset < 0) { - throw new RangeError( - `offset must be a finite non-negative number: ${offset}` - ); - } - - if (duration && duration < 0) { - throw new RangeError( - `duration must be a finite non-negative number: ${duration}` - ); - } - - if (this.hasBeenStarted) { - throw new InvalidStateError('Cannot call start more than once'); - } - - this.hasBeenStarted = true; - (this.node as IAudioBufferSourceNode).start(when, offset, duration); - } - - public override get onEnded(): ((event: EventEmptyType) => void) | undefined { - return super.onEnded as ((event: EventEmptyType) => void) | undefined; - } - - public override set onEnded( - callback: ((event: EventEmptyType) => void) | null - ) { - super.onEnded = callback; - } - - public get onLoopEnded(): ((event: EventEmptyType) => void) | undefined { - return this.onLoopEndedCallback; - } - - public set onLoopEnded(callback: ((event: EventEmptyType) => void) | null) { - if (!callback) { - (this.node as IAudioBufferSourceNode).onLoopEnded = '0'; - this.onLoopEndedSubscription?.remove(); - this.onLoopEndedSubscription = undefined; - this.onLoopEndedCallback = undefined; - - return; - } - - this.onLoopEndedCallback = callback; - this.onLoopEndedSubscription = this.audioEventEmitter.addAudioEventListener( - 'loopEnded', - callback - ); - - (this.node as IAudioBufferSourceNode).onLoopEnded = - this.onLoopEndedSubscription.subscriptionId; - } -} diff --git a/packages/react-native-audio-api/src/core/AudioContext.ts b/packages/react-native-audio-api/src/core/AudioContext.ts index e197fffc0..f3bdc38d8 100644 --- a/packages/react-native-audio-api/src/core/AudioContext.ts +++ b/packages/react-native-audio-api/src/core/AudioContext.ts @@ -1,49 +1,26 @@ -import { IAudioContext } from '../interfaces'; -import BaseAudioContext from './BaseAudioContext'; -import AudioManager from '../system'; -import { AudioContextOptions } from '../types'; -import { NotSupportedError } from '../errors'; -import { isWorkletsAvailable, workletsModule } from '../utils'; +import BaseAudioContext, { IBaseAudioContext } from './BaseAudioContext'; -export default class AudioContext extends BaseAudioContext { - // We need to keep here a reference to this runtime to better manage its lifecycle - // eslint-disable-next-line @typescript-eslint/no-unused-vars, @typescript-eslint/no-explicit-any - private _audioRuntime: any = null; +import { createNativeContext } from './helpers'; - constructor(options?: AudioContextOptions) { - if ( - options && - options.sampleRate && - (options.sampleRate < 8000 || options.sampleRate > 96000) - ) { - throw new NotSupportedError( - `The provided sampleRate is not supported: ${options.sampleRate}` - ); - } - let audioRuntime = null; - if (isWorkletsAvailable) { - audioRuntime = workletsModule.createWorkletRuntime('AudioWorkletRuntime'); - } +interface Runtime {} - super( - global.createAudioContext( - options?.sampleRate || AudioManager.getDevicePreferredSampleRate(), - options?.initSuspended || false, - audioRuntime - ) - ); - this._audioRuntime = audioRuntime; - } +/** + * The AudioContext interface represents an audio-processing graph built from + * audio modules linked together, each represented by an AudioNode. + */ +export default class AudioContext< + NativeContext extends IBaseAudioContext, +> extends BaseAudioContext { + private _audioRuntime: Runtime | null = null; - async close(): Promise { - return (this.context as IAudioContext).close(); + constructor() { + const context = createNativeContext(); + super(context as NativeContext); } +} - async resume(): Promise { - return (this.context as IAudioContext).resume(); - } +function testFunction() { + const aCtx = new AudioContext(); - async suspend(): Promise { - return (this.context as IAudioContext).suspend(); - } + console.log(aCtx.sampleRate); } diff --git a/packages/react-native-audio-api/src/core/AudioDestinationNode.ts b/packages/react-native-audio-api/src/core/AudioDestinationNode.ts deleted file mode 100644 index f1d850e90..000000000 --- a/packages/react-native-audio-api/src/core/AudioDestinationNode.ts +++ /dev/null @@ -1,3 +0,0 @@ -import AudioNode from './AudioNode'; - -export default class AudioDestinationNode extends AudioNode {} diff --git a/packages/react-native-audio-api/src/core/AudioNode.ts b/packages/react-native-audio-api/src/core/AudioNode.ts index 2e19d51d1..81e063005 100644 --- a/packages/react-native-audio-api/src/core/AudioNode.ts +++ b/packages/react-native-audio-api/src/core/AudioNode.ts @@ -1,19 +1,26 @@ -import { IAudioNode } from '../interfaces'; -import AudioParam from './AudioParam'; import { ChannelCountMode, ChannelInterpretation } from '../types'; -import BaseAudioContext from './BaseAudioContext'; -import { InvalidAccessError } from '../errors'; +import type { + IGenericAudioNode, + IGenericBaseAudioContext, +} from '../types/generics'; +import AudioParam from './AudioParam'; -export default class AudioNode { - readonly context: BaseAudioContext; +export default class AudioNode< + TContext extends IGenericBaseAudioContext, + NContext extends IGenericBaseAudioContext, + TNode extends IGenericAudioNode = IGenericAudioNode, +> implements IGenericAudioNode +{ + readonly context: TContext; readonly numberOfInputs: number; readonly numberOfOutputs: number; readonly channelCount: number; readonly channelCountMode: ChannelCountMode; readonly channelInterpretation: ChannelInterpretation; - protected readonly node: IAudioNode; - constructor(context: BaseAudioContext, node: IAudioNode) { + protected readonly node: TNode; + + constructor(context: TContext, node: TNode) { this.context = context; this.node = node; this.numberOfInputs = this.node.numberOfInputs; @@ -23,27 +30,46 @@ export default class AudioNode { this.channelInterpretation = this.node.channelInterpretation; } - public connect(destination: AudioNode | AudioParam): AudioNode | AudioParam { + public connect>( + destination: ONode + ): ONode; + + public connect(destination: AudioParam): void; + + public connect>( + destination: ONode | AudioParam + ): ONode | void { if (this.context !== destination.context) { - throw new InvalidAccessError( + throw new Error( 'Source and destination are from different BaseAudioContexts' ); } if (destination instanceof AudioParam) { - this.node.connect(destination.audioParam); - } else { - this.node.connect(destination.node); + this.node.connect(destination.param); + return; } + this.node.connect(destination.node); return destination; } - public disconnect(destination?: AudioNode | AudioParam): void { + public disconnect(): void; + + public disconnect>( + destination: ONode + ): void; + + public disconnect(destination: AudioParam): void; + + public disconnect( + destination?: AudioNode | AudioParam + ): void { if (destination instanceof AudioParam) { - this.node.disconnect(destination.audioParam); - } else { - this.node.disconnect(destination?.node); + this.node.disconnect(destination.param); + return; } + + this.node.disconnect(destination?.node); } } diff --git a/packages/react-native-audio-api/src/core/AudioParam.ts b/packages/react-native-audio-api/src/core/AudioParam.ts index d8cfb5fed..829d3bc5b 100644 --- a/packages/react-native-audio-api/src/core/AudioParam.ts +++ b/packages/react-native-audio-api/src/core/AudioParam.ts @@ -1,51 +1,62 @@ -import { IAudioParam } from '../interfaces'; -import { RangeError, InvalidStateError } from '../errors'; -import BaseAudioContext from './BaseAudioContext'; - -export default class AudioParam { +import { InvalidStateError } from '../errors'; +import { + IGenericAudioParam, + IGenericBaseAudioContext, +} from '../types/generics'; + +export default class AudioParam< + TContext extends IGenericBaseAudioContext, + NContext extends IGenericBaseAudioContext, +> implements IGenericAudioParam +{ readonly defaultValue: number; readonly minValue: number; readonly maxValue: number; - readonly audioParam: IAudioParam; - readonly context: BaseAudioContext; - - constructor(audioParam: IAudioParam, context: BaseAudioContext) { - this.audioParam = audioParam; - this.value = audioParam.value; - this.defaultValue = audioParam.defaultValue; - this.minValue = audioParam.minValue; - this.maxValue = audioParam.maxValue; + readonly param: IGenericAudioParam; + readonly context: TContext; + + constructor(param: IGenericAudioParam, context: TContext) { + this.param = param; this.context = context; + this.defaultValue = param.defaultValue; + this.minValue = param.minValue; + this.maxValue = param.maxValue; } public get value(): number { - return this.audioParam.value; + return this.param.value; } public set value(value: number) { - this.audioParam.value = value; + this.param.value = value; } - public setValueAtTime(value: number, startTime: number): AudioParam { + public setValueAtTime( + value: number, + startTime: number + ): AudioParam { if (startTime < 0) { throw new RangeError( `startTime must be a finite non-negative number: ${startTime}` ); } - this.audioParam.setValueAtTime(value, startTime); + this.param.setValueAtTime(value, startTime); return this; } - public linearRampToValueAtTime(value: number, endTime: number): AudioParam { + public linearRampToValueAtTime( + value: number, + endTime: number + ): AudioParam { if (endTime < 0) { throw new RangeError( `endTime must be a finite non-negative number: ${endTime}` ); } - this.audioParam.linearRampToValueAtTime(value, endTime); + this.param.linearRampToValueAtTime(value, endTime); return this; } @@ -53,14 +64,14 @@ export default class AudioParam { public exponentialRampToValueAtTime( value: number, endTime: number - ): AudioParam { + ): AudioParam { if (endTime < 0) { throw new RangeError( `endTime must be a finite non-negative number: ${endTime}` ); } - this.audioParam.exponentialRampToValueAtTime(value, endTime); + this.param.exponentialRampToValueAtTime(value, endTime); return this; } @@ -69,7 +80,7 @@ export default class AudioParam { target: number, startTime: number, timeConstant: number - ): AudioParam { + ): AudioParam { if (startTime < 0) { throw new RangeError( `startTime must be a finite non-negative number: ${startTime}` @@ -82,7 +93,7 @@ export default class AudioParam { ); } - this.audioParam.setTargetAtTime(target, startTime, timeConstant); + this.param.setTargetAtTime(target, startTime, timeConstant); return this; } @@ -91,7 +102,7 @@ export default class AudioParam { values: Float32Array, startTime: number, duration: number - ): AudioParam { + ): AudioParam { if (startTime < 0) { throw new RangeError( `startTime must be a finite non-negative number: ${startTime}` @@ -108,31 +119,35 @@ export default class AudioParam { throw new InvalidStateError(`values must contain at least two values`); } - this.audioParam.setValueCurveAtTime(values, startTime, duration); + this.param.setValueCurveAtTime(values, startTime, duration); return this; } - public cancelScheduledValues(cancelTime: number): AudioParam { + public cancelScheduledValues( + cancelTime: number + ): AudioParam { if (cancelTime < 0) { throw new RangeError( `cancelTime must be a finite non-negative number: ${cancelTime}` ); } - this.audioParam.cancelScheduledValues(cancelTime); + this.param.cancelScheduledValues(cancelTime); return this; } - public cancelAndHoldAtTime(cancelTime: number): AudioParam { + public cancelAndHoldAtTime( + cancelTime: number + ): AudioParam { if (cancelTime < 0) { throw new RangeError( `cancelTime must be a finite non-negative number: ${cancelTime}` ); } - this.audioParam.cancelAndHoldAtTime(cancelTime); + this.param.cancelAndHoldAtTime(cancelTime); return this; } diff --git a/packages/react-native-audio-api/src/core/AudioScheduledSourceNode.ts b/packages/react-native-audio-api/src/core/AudioScheduledSourceNode.ts deleted file mode 100644 index 8f3366187..000000000 --- a/packages/react-native-audio-api/src/core/AudioScheduledSourceNode.ts +++ /dev/null @@ -1,69 +0,0 @@ -import { IAudioScheduledSourceNode } from '../interfaces'; -import AudioNode from './AudioNode'; -import { InvalidStateError, RangeError } from '../errors'; -import { OnEndedEventType } from '../events/types'; -import { AudioEventEmitter, AudioEventSubscription } from '../events'; - -export default class AudioScheduledSourceNode extends AudioNode { - protected hasBeenStarted: boolean = false; - protected readonly audioEventEmitter = new AudioEventEmitter( - global.AudioEventEmitter - ); - - private onEndedSubscription?: AudioEventSubscription; - private onEndedCallback?: (event: OnEndedEventType) => void; - - public start(when: number = 0): void { - if (when < 0) { - throw new RangeError( - `when must be a finite non-negative number: ${when}` - ); - } - - if (this.hasBeenStarted) { - throw new InvalidStateError('Cannot call start more than once'); - } - - this.hasBeenStarted = true; - (this.node as IAudioScheduledSourceNode).start(when); - } - - public stop(when: number = 0): void { - if (when < 0) { - throw new RangeError( - `when must be a finite non-negative number: ${when}` - ); - } - - if (!this.hasBeenStarted) { - throw new InvalidStateError( - 'Cannot call stop without calling start first' - ); - } - - (this.node as IAudioScheduledSourceNode).stop(when); - } - - public get onEnded(): ((event: OnEndedEventType) => void) | undefined { - return this.onEndedCallback; - } - - public set onEnded(callback: ((event: OnEndedEventType) => void) | null) { - if (!callback) { - (this.node as IAudioScheduledSourceNode).onEnded = '0'; - this.onEndedSubscription?.remove(); - this.onEndedSubscription = undefined; - this.onEndedCallback = undefined; - return; - } - - this.onEndedCallback = callback; - this.onEndedSubscription = this.audioEventEmitter.addAudioEventListener( - 'ended', - callback - ); - - (this.node as IAudioScheduledSourceNode).onEnded = - this.onEndedSubscription.subscriptionId; - } -} diff --git a/packages/react-native-audio-api/src/core/BaseAudioContext.ts b/packages/react-native-audio-api/src/core/BaseAudioContext.ts index 8482ac61e..f90cdabd0 100644 --- a/packages/react-native-audio-api/src/core/BaseAudioContext.ts +++ b/packages/react-native-audio-api/src/core/BaseAudioContext.ts @@ -1,292 +1,46 @@ -import { InvalidAccessError, NotSupportedError } from '../errors'; -import { IBaseAudioContext } from '../interfaces'; -import { - AudioBufferBaseSourceNodeOptions, - ContextState, - PeriodicWaveConstraints, - AudioWorkletRuntime, -} from '../types'; -import { isWorkletsAvailable, workletsModule } from '../utils'; -import WorkletSourceNode from './WorkletSourceNode'; -import WorkletProcessingNode from './WorkletProcessingNode'; -import AnalyserNode from './AnalyserNode'; -import AudioBuffer from './AudioBuffer'; -import AudioBufferQueueSourceNode from './AudioBufferQueueSourceNode'; -import AudioBufferSourceNode from './AudioBufferSourceNode'; -import AudioDestinationNode from './AudioDestinationNode'; -import BiquadFilterNode from './BiquadFilterNode'; -import ConstantSourceNode from './ConstantSourceNode'; -import GainNode from './GainNode'; -import OscillatorNode from './OscillatorNode'; -import PeriodicWave from './PeriodicWave'; -import RecorderAdapterNode from './RecorderAdapterNode'; -import StereoPannerNode from './StereoPannerNode'; -import StreamerNode from './StreamerNode'; -import WorkletNode from './WorkletNode'; -import { decodeAudioData, decodePCMInBase64 } from './AudioDecoder'; +interface IRecorderAdapterNode {} +interface IOscillatorNode {} -export default class BaseAudioContext { - readonly destination: AudioDestinationNode; - readonly sampleRate: number; - readonly context: IBaseAudioContext; - - constructor(context: IBaseAudioContext) { - this.context = context; - this.destination = new AudioDestinationNode(this, context.destination); - this.sampleRate = context.sampleRate; - } - - public get currentTime(): number { - return this.context.currentTime; - } - - public get state(): ContextState { - return this.context.state; - } - - public async decodeAudioData( - input: string | ArrayBuffer, - sampleRate?: number - ): Promise { - if (!(typeof input === 'string' || input instanceof ArrayBuffer)) { - throw new TypeError('Input must be a string or ArrayBuffer'); - } - return await decodeAudioData(input, sampleRate ?? this.sampleRate); - } - - public async decodePCMInBase64( - base64String: string, - inputSampleRate: number, - inputChannelCount: number, - isInterleaved: boolean = true - ): Promise { - return await decodePCMInBase64( - base64String, - inputSampleRate, - inputChannelCount, - isInterleaved - ); - } +export interface IBaseAudioContext {} - createWorkletNode( - callback: (audioData: Array, channelCount: number) => void, - bufferLength: number, - inputChannelCount: number, - workletRuntime: AudioWorkletRuntime = 'AudioRuntime' - ): WorkletNode { - if (inputChannelCount < 1 || inputChannelCount > 32) { - throw new NotSupportedError( - `The number of input channels provided (${inputChannelCount}) can not be less than 1 or greater than 32` - ); - } - if (bufferLength < 1) { - throw new NotSupportedError( - `The buffer length provided (${bufferLength}) can not be less than 1` - ); - } - - if (isWorkletsAvailable) { - const shareableWorklet = workletsModule.makeShareableCloneRecursive( - (audioBuffers: Array, channelCount: number) => { - 'worklet'; - const floatAudioData: Array = audioBuffers.map( - (buffer) => new Float32Array(buffer) - ); - callback(floatAudioData, channelCount); - } - ); - return new WorkletNode( - this, - this.context.createWorkletNode( - shareableWorklet, - workletRuntime === 'UIRuntime', - bufferLength, - inputChannelCount - ) - ); - } - /// User does not have worklets as a dependency so he cannot use the worklet API. - throw new Error( - '[RnAudioApi] Worklets are not available, please install react-native-worklets as a dependency. Refer to documentation for more details.' - ); - } - - createWorkletProcessingNode( - callback: ( - inputData: Array, - outputData: Array, - framesToProcess: number, - currentTime: number - ) => void, - workletRuntime: AudioWorkletRuntime = 'AudioRuntime' - ): WorkletProcessingNode { - if (isWorkletsAvailable) { - const shareableWorklet = workletsModule.makeShareableCloneRecursive( - ( - inputBuffers: Array, - outputBuffers: Array, - framesToProcess: number, - currentTime: number - ) => { - 'worklet'; - const inputData: Array = inputBuffers.map( - (buffer) => new Float32Array(buffer, 0, framesToProcess) - ); - const outputData: Array = outputBuffers.map( - (buffer) => new Float32Array(buffer, 0, framesToProcess) - ); - callback(inputData, outputData, framesToProcess, currentTime); - } - ); - return new WorkletProcessingNode( - this, - this.context.createWorkletProcessingNode( - shareableWorklet, - workletRuntime === 'UIRuntime' - ) - ); - } - /// User does not have worklets as a dependency so he cannot use the worklet API. - throw new Error( - '[RnAudioApi] Worklets are not available, please install react-native-worklets as a dependency. Refer to documentation for more details.' - ); - } - - createWorkletSourceNode( - callback: ( - audioData: Array, - framesToProcess: number, - currentTime: number, - startOffset: number - ) => void, - workletRuntime: AudioWorkletRuntime = 'AudioRuntime' - ): WorkletSourceNode { - if (!isWorkletsAvailable) { - /// User does not have worklets as a dependency so he cannot use the worklet API. - throw new Error( - '[RnAudioApi] Worklets are not available, please install react-native-worklets as a dependency. Refer to documentation for more details.' - ); - } - const shareableWorklet = workletsModule.makeShareableCloneRecursive( - ( - audioBuffers: Array, - framesToProcess: number, - currentTime: number, - startOffset: number - ) => { - 'worklet'; - const floatAudioData: Array = audioBuffers.map( - (buffer) => new Float32Array(buffer) - ); - callback(floatAudioData, framesToProcess, currentTime, startOffset); - } - ); - return new WorkletSourceNode( - this, - this.context.createWorkletSourceNode( - shareableWorklet, - workletRuntime === 'UIRuntime' - ) - ); - } - - createRecorderAdapter(): RecorderAdapterNode { - return new RecorderAdapterNode(this, this.context.createRecorderAdapter()); - } - - createOscillator(): OscillatorNode { - return new OscillatorNode(this, this.context.createOscillator()); - } - - createStreamer(): StreamerNode { - return new StreamerNode(this, this.context.createStreamer()); - } - - createConstantSource(): ConstantSourceNode { - return new ConstantSourceNode(this, this.context.createConstantSource()); - } - - createGain(): GainNode { - return new GainNode(this, this.context.createGain()); - } - - createStereoPanner(): StereoPannerNode { - return new StereoPannerNode(this, this.context.createStereoPanner()); - } - - createBiquadFilter(): BiquadFilterNode { - return new BiquadFilterNode(this, this.context.createBiquadFilter()); - } - - createBufferSource( - options?: AudioBufferBaseSourceNodeOptions - ): AudioBufferSourceNode { - const pitchCorrection = options?.pitchCorrection ?? false; - - return new AudioBufferSourceNode( - this, - this.context.createBufferSource(pitchCorrection) - ); - } +interface INativeAudioContext extends IBaseAudioContext { + createMyMomNode(): IRecorderAdapterNode; +} - createBufferQueueSource( - options?: AudioBufferBaseSourceNodeOptions - ): AudioBufferQueueSourceNode { - const pitchCorrection = options?.pitchCorrection ?? false; +interface WebAudioContext + extends IBaseAudioContext, + globalThis.BaseAudioContext {} - return new AudioBufferQueueSourceNode( - this, - this.context.createBufferQueueSource(pitchCorrection) - ); +class OscillatorNode { + constructor( + context: BaseAudioContext, + nativeNode: IOscillatorNode + ) { + (context.nativeContext as WebAudioContext).createDynamicsCompressor(); } +} - createBuffer( - numOfChannels: number, - length: number, - sampleRate: number - ): AudioBuffer { - if (numOfChannels < 1 || numOfChannels >= 32) { - throw new NotSupportedError( - `The number of channels provided (${numOfChannels}) is outside the range [1, 32]` - ); - } - - if (length <= 0) { - throw new NotSupportedError( - `The number of frames provided (${length}) is less than or equal to the minimum bound (0)` - ); - } +class RecorderAdapterNode { + constructor( + context: BaseAudioContext, + nativeNode: IRecorderAdapterNode + ) {} +} - if (sampleRate < 8000 || sampleRate > 96000) { - throw new NotSupportedError( - `The sample rate provided (${sampleRate}) is outside the range [8000, 96000]` - ); - } +export default class BaseAudioContext { + readonly nativeContext: NativeContext; + readonly sampleRate: number; - return new AudioBuffer( - this.context.createBuffer(numOfChannels, length, sampleRate) - ); + constructor(nativeContext: NativeContext) { + this.nativeContext = nativeContext; + this.sampleRate = nativeContext.sampleRate; } - createPeriodicWave( - real: Float32Array, - imag: Float32Array, - constraints?: PeriodicWaveConstraints - ): PeriodicWave { - if (real.length !== imag.length) { - throw new InvalidAccessError( - `The lengths of the real (${real.length}) and imaginary (${imag.length}) arrays must match.` - ); - } + createOscillator(): OscillatorNode { + const nativeNode = this.nativeContext.createOscillator(); - const disableNormalization = constraints?.disableNormalization ?? false; - - return new PeriodicWave( - this.context.createPeriodicWave(real, imag, disableNormalization) - ); + return new OscillatorNode(this, nativeNode); } - createAnalyser(): AnalyserNode { - return new AnalyserNode(this, this.context.createAnalyser()); - } + createRecorderAdapter(): RecorderAdapterNode {} } diff --git a/packages/react-native-audio-api/src/core/BiquadFilterNode.ts b/packages/react-native-audio-api/src/core/BiquadFilterNode.ts deleted file mode 100644 index 68db7c02a..000000000 --- a/packages/react-native-audio-api/src/core/BiquadFilterNode.ts +++ /dev/null @@ -1,49 +0,0 @@ -import { InvalidAccessError } from '../errors'; -import { IBiquadFilterNode } from '../interfaces'; -import AudioNode from './AudioNode'; -import AudioParam from './AudioParam'; -import BaseAudioContext from './BaseAudioContext'; -import { BiquadFilterType } from '../types'; - -export default class BiquadFilterNode extends AudioNode { - readonly frequency: AudioParam; - readonly detune: AudioParam; - readonly Q: AudioParam; - readonly gain: AudioParam; - - constructor(context: BaseAudioContext, biquadFilter: IBiquadFilterNode) { - super(context, biquadFilter); - this.frequency = new AudioParam(biquadFilter.frequency, context); - this.detune = new AudioParam(biquadFilter.detune, context); - this.Q = new AudioParam(biquadFilter.Q, context); - this.gain = new AudioParam(biquadFilter.gain, context); - } - - public get type(): BiquadFilterType { - return (this.node as IBiquadFilterNode).type; - } - - public set type(value: BiquadFilterType) { - (this.node as IBiquadFilterNode).type = value; - } - - public getFrequencyResponse( - frequencyArray: Float32Array, - magResponseOutput: Float32Array, - phaseResponseOutput: Float32Array - ) { - if ( - frequencyArray.length !== magResponseOutput.length || - frequencyArray.length !== phaseResponseOutput.length - ) { - throw new InvalidAccessError( - `The lengths of the arrays are not the same frequencyArray: ${frequencyArray.length}, magResponseOutput: ${magResponseOutput.length}, phaseResponseOutput: ${phaseResponseOutput.length}` - ); - } - (this.node as IBiquadFilterNode).getFrequencyResponse( - frequencyArray, - magResponseOutput, - phaseResponseOutput - ); - } -} diff --git a/packages/react-native-audio-api/src/core/ConstantSourceNode.ts b/packages/react-native-audio-api/src/core/ConstantSourceNode.ts deleted file mode 100644 index 1a36c4b9f..000000000 --- a/packages/react-native-audio-api/src/core/ConstantSourceNode.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { IConstantSourceNode } from '../interfaces'; -import AudioParam from './AudioParam'; -import AudioScheduledSourceNode from './AudioScheduledSourceNode'; -import BaseAudioContext from './BaseAudioContext'; - -export default class ConstantSourceNode extends AudioScheduledSourceNode { - readonly offset: AudioParam; - - constructor(context: BaseAudioContext, node: IConstantSourceNode) { - super(context, node); - this.offset = new AudioParam(node.offset, context); - } -} diff --git a/packages/react-native-audio-api/src/core/GainNode.ts b/packages/react-native-audio-api/src/core/GainNode.ts deleted file mode 100644 index a216f1365..000000000 --- a/packages/react-native-audio-api/src/core/GainNode.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { IGainNode } from '../interfaces'; -import AudioNode from './AudioNode'; -import AudioParam from './AudioParam'; -import BaseAudioContext from './BaseAudioContext'; - -export default class GainNode extends AudioNode { - readonly gain: AudioParam; - - constructor(context: BaseAudioContext, gain: IGainNode) { - super(context, gain); - this.gain = new AudioParam(gain.gain, context); - } -} diff --git a/packages/react-native-audio-api/src/core/OscillatorNode.ts b/packages/react-native-audio-api/src/core/OscillatorNode.ts deleted file mode 100644 index 48296831c..000000000 --- a/packages/react-native-audio-api/src/core/OscillatorNode.ts +++ /dev/null @@ -1,48 +0,0 @@ -import { IOscillatorNode } from '../interfaces'; -import { OscillatorType } from '../types'; -import AudioScheduledSourceNode from './AudioScheduledSourceNode'; -import AudioParam from './AudioParam'; -import BaseAudioContext from './BaseAudioContext'; -import PeriodicWave from './PeriodicWave'; -import { InvalidStateError } from '../errors'; -import { EventEmptyType } from '../events/types'; - -export default class OscillatorNode extends AudioScheduledSourceNode { - readonly frequency: AudioParam; - readonly detune: AudioParam; - - constructor(context: BaseAudioContext, node: IOscillatorNode) { - super(context, node); - this.frequency = new AudioParam(node.frequency, context); - this.detune = new AudioParam(node.detune, context); - this.type = node.type; - } - - public get type(): OscillatorType { - return (this.node as IOscillatorNode).type; - } - - public set type(value: OscillatorType) { - if (value === 'custom') { - throw new InvalidStateError( - "'type' cannot be set directly to 'custom'. Use setPeriodicWave() to create a custom Oscillator type." - ); - } - - (this.node as IOscillatorNode).type = value; - } - - public setPeriodicWave(wave: PeriodicWave): void { - (this.node as IOscillatorNode).setPeriodicWave(wave.periodicWave); - } - - public override get onEnded(): ((event: EventEmptyType) => void) | undefined { - return super.onEnded as ((event: EventEmptyType) => void) | undefined; - } - - public override set onEnded( - callback: ((event: EventEmptyType) => void) | null - ) { - super.onEnded = callback; - } -} diff --git a/packages/react-native-audio-api/src/core/PeriodicWave.ts b/packages/react-native-audio-api/src/core/PeriodicWave.ts index b00c4505c..f7c31d9d8 100644 --- a/packages/react-native-audio-api/src/core/PeriodicWave.ts +++ b/packages/react-native-audio-api/src/core/PeriodicWave.ts @@ -1,10 +1,17 @@ -import { IPeriodicWave } from '../interfaces'; +import type { + IGenericBaseAudioContext, + IGenericPeriodicWave, +} from '../types/generics'; -export default class PeriodicWave { +export default class PeriodicWave< + TContext extends IGenericBaseAudioContext, + NContext extends IGenericBaseAudioContext, +> implements IGenericPeriodicWave +{ /** @internal */ - public readonly periodicWave: IPeriodicWave; + public readonly periodicWave: IGenericPeriodicWave; - constructor(periodicWave: IPeriodicWave) { + constructor(periodicWave: IGenericPeriodicWave) { this.periodicWave = periodicWave; } } diff --git a/packages/react-native-audio-api/src/core/StereoPannerNode.ts b/packages/react-native-audio-api/src/core/StereoPannerNode.ts deleted file mode 100644 index a8fa77006..000000000 --- a/packages/react-native-audio-api/src/core/StereoPannerNode.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { IStereoPannerNode } from '../interfaces'; -import AudioNode from './AudioNode'; -import AudioParam from './AudioParam'; -import BaseAudioContext from './BaseAudioContext'; - -export default class StereoPannerNode extends AudioNode { - readonly pan: AudioParam; - - constructor(context: BaseAudioContext, pan: IStereoPannerNode) { - super(context, pan); - this.pan = new AudioParam(pan.pan, context); - } -} diff --git a/packages/react-native-audio-api/src/core/analysis/AnalyserNode.ts b/packages/react-native-audio-api/src/core/analysis/AnalyserNode.ts new file mode 100644 index 000000000..6b9add1e1 --- /dev/null +++ b/packages/react-native-audio-api/src/core/analysis/AnalyserNode.ts @@ -0,0 +1,25 @@ +import type { WindowType } from '../../types'; +import type { IGenericBaseAudioContext } from '../../types/generics'; +import BaseAnalyserNode, { + IAbstractNativeAnalyserNode, +} from './BaseAnalyserNode'; + +// TODO: fixme - temporary any to avoid work +interface NativeAudioContext {} + +interface MobileAnalyserNode + extends IAbstractNativeAnalyserNode { + window: WindowType; +} + +export default class AnalyserNodeNative< + TContext extends IGenericBaseAudioContext, +> extends BaseAnalyserNode { + public get window(): WindowType { + return this.node.window; + } + + public set window(value: WindowType) { + this.node.window = value; + } +} diff --git a/packages/react-native-audio-api/src/core/analysis/AnalyserNode.web.ts b/packages/react-native-audio-api/src/core/analysis/AnalyserNode.web.ts new file mode 100644 index 000000000..16b0f4724 --- /dev/null +++ b/packages/react-native-audio-api/src/core/analysis/AnalyserNode.web.ts @@ -0,0 +1,19 @@ +import type { WindowType } from '../../types'; +import type { IGenericBaseAudioContext } from '../../types/generics'; +import { availabilityWarn } from '../../utils'; +import BaseAnalyserNode from './BaseAnalyserNode'; + +type NativeAnalyserNode = globalThis.AnalyserNode; +type NativeAudioContext = globalThis.BaseAudioContext; + +export default class AnalyserNode< + TContext extends IGenericBaseAudioContext, +> extends BaseAnalyserNode { + public get window(): WindowType { + return 'blackman'; + } + + public set window(_value: WindowType) { + availabilityWarn('window', 'web', '/'); + } +} diff --git a/packages/react-native-audio-api/src/core/analysis/BaseAnalyserNode.ts b/packages/react-native-audio-api/src/core/analysis/BaseAnalyserNode.ts new file mode 100644 index 000000000..e9f28508f --- /dev/null +++ b/packages/react-native-audio-api/src/core/analysis/BaseAnalyserNode.ts @@ -0,0 +1,118 @@ +import { IndexSizeError } from '../../errors'; +import type { WindowType } from '../../types'; +import type { + IGenericAudioNode, + IGenericBaseAudioContext, +} from '../../types/generics'; +import type { IAnalyserNode } from '../../types/interfaces'; +import AudioNode from '../AudioNode'; + +// "Abstract" (TODO: better naming?) interface to describe the generalized native/web implementation +// it is used as generic for BaseAnalyserNode to allow for extensions +// TODO: should be exported? +export interface IAbstractNativeAnalyserNode< + NContext extends IGenericBaseAudioContext, +> extends IGenericAudioNode { + fftSize: number; + minDecibels: number; + maxDecibels: number; + smoothingTimeConstant: number; + readonly frequencyBinCount: number; + getFloatFrequencyData(array: Float32Array): void; + getByteFrequencyData(array: Uint8Array): void; + getFloatTimeDomainData(array: Float32Array): void; + getByteTimeDomainData(array: Uint8Array): void; +} + +export const allowedFFTSize = [ + 32, 64, 128, 256, 512, 1024, 2048, 4096, 8192, 16384, 32768, +]; + +export default abstract class BaseAnalyserNode< + TContext extends IGenericBaseAudioContext, + NContext extends IGenericBaseAudioContext, + NNode extends + IAbstractNativeAnalyserNode = IAbstractNativeAnalyserNode, + > + extends AudioNode + implements IAnalyserNode +{ + public get fftSize(): number { + return this.node.fftSize; + } + + public set fftSize(value: number) { + if (!allowedFFTSize.includes(value)) { + throw new IndexSizeError( + `Provided value (${value}) must be a power of 2 between 32 and 32768` + ); + } + + this.node.fftSize = value; + } + + public get minDecibels(): number { + return this.node.minDecibels; + } + + public set minDecibels(value: number) { + if (value >= this.node.maxDecibels) { + throw new IndexSizeError( + `The minDecibels value (${value}) must be less than maxDecibels` + ); + } + + this.node.minDecibels = value; + } + + public get smoothingTimeConstant(): number { + return this.node.smoothingTimeConstant; + } + + public set smoothingTimeConstant(value: number) { + if (value < 0 || value > 1) { + throw new IndexSizeError( + `The smoothingTimeConstant value (${value}) must be between 0 and 1` + ); + } + + this.node.smoothingTimeConstant = value; + } + + public get maxDecibels(): number { + return this.node.maxDecibels; + } + + public set maxDecibels(value: number) { + if (value <= this.node.minDecibels) { + throw new IndexSizeError( + `The maxDecibels value (${value}) must be greater than minDecibels` + ); + } + + this.node.maxDecibels = value; + } + + public get frequencyBinCount(): number { + return this.node.frequencyBinCount; + } + + public getFloatFrequencyData(array: Float32Array): void { + this.node.getFloatFrequencyData(array); + } + + public getByteFrequencyData(array: Uint8Array): void { + this.node.getByteFrequencyData(array); + } + + public getFloatTimeDomainData(array: Float32Array): void { + this.node.getFloatTimeDomainData(array); + } + + public getByteTimeDomainData(array: Uint8Array): void { + this.node.getByteTimeDomainData(array); + } + + public abstract get window(): WindowType; + public abstract set window(value: WindowType); +} diff --git a/packages/react-native-audio-api/src/core/destinations/AudioDestinationNode.ts b/packages/react-native-audio-api/src/core/destinations/AudioDestinationNode.ts new file mode 100644 index 000000000..331b12a50 --- /dev/null +++ b/packages/react-native-audio-api/src/core/destinations/AudioDestinationNode.ts @@ -0,0 +1,15 @@ +import type { + IGenericAudioDestinationNode, + IGenericBaseAudioContext, +} from '../../types/generics'; +import AudioNode from '../AudioNode'; + +export default class AudioDestinationNode< + TContext extends IGenericBaseAudioContext, + NContext extends IGenericBaseAudioContext, + > + extends AudioNode> + implements IGenericAudioDestinationNode { + // TODO: implement on native side + // readonly maxChannelCount: number; +} diff --git a/packages/react-native-audio-api/src/core/effects/BiquadFilterNode.ts b/packages/react-native-audio-api/src/core/effects/BiquadFilterNode.ts new file mode 100644 index 000000000..ebcb23731 --- /dev/null +++ b/packages/react-native-audio-api/src/core/effects/BiquadFilterNode.ts @@ -0,0 +1,68 @@ +import { InvalidAccessError } from '../../errors'; +import { BiquadFilterType } from '../../types'; +import type { + IGenericBaseAudioContext, + IGenericBiquadFilterNode, +} from '../../types/generics'; +import AudioNode from '../AudioNode'; +import AudioParam from '../AudioParam'; + +export default class BiquadFilterNode< + TContext extends IGenericBaseAudioContext, + NContext extends IGenericBaseAudioContext, + > + extends AudioNode> + implements IGenericBiquadFilterNode +{ + readonly frequency: AudioParam; + readonly detune: AudioParam; + readonly Q: AudioParam; + readonly gain: AudioParam; + + constructor( + context: TContext, + biquadFilter: IGenericBiquadFilterNode + ) { + super(context, biquadFilter); + + this.Q = new AudioParam(biquadFilter.Q, context); + this.gain = new AudioParam(biquadFilter.gain, context); + this.frequency = new AudioParam( + biquadFilter.frequency, + context + ); + this.detune = new AudioParam( + biquadFilter.detune, + context + ); + } + + public get type(): BiquadFilterType { + return this.node.type; + } + + public set type(value: BiquadFilterType) { + this.node.type = value; + } + + public getFrequencyResponse( + frequencyArray: Float32Array, + magResponseOutput: Float32Array, + phaseResponseOutput: Float32Array + ) { + if ( + frequencyArray.length !== magResponseOutput.length || + frequencyArray.length !== phaseResponseOutput.length + ) { + throw new InvalidAccessError( + `The lengths of the arrays are not the same frequencyArray: ${frequencyArray.length}, magResponseOutput: ${magResponseOutput.length}, phaseResponseOutput: ${phaseResponseOutput.length}` + ); + } + + this.node.getFrequencyResponse( + frequencyArray, + magResponseOutput, + phaseResponseOutput + ); + } +} diff --git a/packages/react-native-audio-api/src/core/effects/GainNode.ts b/packages/react-native-audio-api/src/core/effects/GainNode.ts new file mode 100644 index 000000000..44583ff19 --- /dev/null +++ b/packages/react-native-audio-api/src/core/effects/GainNode.ts @@ -0,0 +1,21 @@ +import type { + IGenericBaseAudioContext, + IGenericGainNode, +} from '../../types/generics'; +import AudioNode from '../AudioNode'; +import AudioParam from '../AudioParam'; + +export default class GainNode< + TContext extends IGenericBaseAudioContext, + NContext extends IGenericBaseAudioContext, + > + extends AudioNode> + implements IGenericGainNode +{ + readonly gain: AudioParam; + + constructor(context: TContext, gain: IGenericGainNode) { + super(context, gain); + this.gain = new AudioParam(gain.gain, context); + } +} diff --git a/packages/react-native-audio-api/src/core/effects/StereoPannerNode.ts b/packages/react-native-audio-api/src/core/effects/StereoPannerNode.ts new file mode 100644 index 000000000..e1123837d --- /dev/null +++ b/packages/react-native-audio-api/src/core/effects/StereoPannerNode.ts @@ -0,0 +1,18 @@ +import type { + IGenericBaseAudioContext, + IGenericStereoPannerNode, +} from '../../types/generics'; +import AudioNode from '../AudioNode'; +import AudioParam from '../AudioParam'; + +export default class StereoPannerNode< + TContext extends IGenericBaseAudioContext, + NContext extends IGenericBaseAudioContext, +> extends AudioNode> { + readonly pan: AudioParam; + + constructor(context: TContext, pan: IGenericStereoPannerNode) { + super(context, pan); + this.pan = new AudioParam(pan.pan, context); + } +} diff --git a/packages/react-native-audio-api/src/core/helpers/createNativeContext.native.ts b/packages/react-native-audio-api/src/core/helpers/createNativeContext.native.ts new file mode 100644 index 000000000..707ae176c --- /dev/null +++ b/packages/react-native-audio-api/src/core/helpers/createNativeContext.native.ts @@ -0,0 +1,14 @@ +import type { IBaseAudioContext } from '../BaseAudioContext'; + +interface INativeAudioContext extends IBaseAudioContext { + bebebe: string; + hakoonaMatata: () => void; +} + +declare global { + function createAudioContext(): INativeAudioContext; +} + +export default function createNativeContextMobile(): IBaseAudioContext { + return global.createAudioContext(); +} diff --git a/packages/react-native-audio-api/src/core/helpers/createNativeContext.web.ts b/packages/react-native-audio-api/src/core/helpers/createNativeContext.web.ts new file mode 100644 index 000000000..37e08adf5 --- /dev/null +++ b/packages/react-native-audio-api/src/core/helpers/createNativeContext.web.ts @@ -0,0 +1,5 @@ +import type { IBaseAudioContext } from '../BaseAudioContext'; + +export default function createNativeContextWeb(): IBaseAudioContext { + return new window.AudioContext() as IBaseAudioContext; +} diff --git a/packages/react-native-audio-api/src/core/helpers/index.ts b/packages/react-native-audio-api/src/core/helpers/index.ts new file mode 100644 index 000000000..76cb5e384 --- /dev/null +++ b/packages/react-native-audio-api/src/core/helpers/index.ts @@ -0,0 +1 @@ +export { default as createNativeContext } from './createNativeContext'; diff --git a/packages/react-native-audio-api/src/core/index.ts b/packages/react-native-audio-api/src/core/index.ts new file mode 100644 index 000000000..45df788b8 --- /dev/null +++ b/packages/react-native-audio-api/src/core/index.ts @@ -0,0 +1,14 @@ +export { default as AnalyserNode } from './analysis/AnalyserNode'; + +export { default as AudioBuffer } from './AudioBuffer'; +export { default as AudioNode } from './AudioNode'; +export { default as AudioParam } from './AudioParam'; +export { default as PeriodicWave } from './PeriodicWave'; + +export { default as AudioDestinationNode } from './destinations/AudioDestinationNode'; + +export { default as BiquadFilterNode } from './effects/BiquadFilterNode'; +export { default as GainNode } from './effects/GainNode'; +export { default as StereoPannerNode } from './effects/StereoPannerNode'; + +export { default as AudioScheduledSourceNode } from './sources/AudioScheduledSourceNode'; diff --git a/packages/react-native-audio-api/src/core/sources/ABSNWeb/IStretcherNode.ts b/packages/react-native-audio-api/src/core/sources/ABSNWeb/IStretcherNode.ts new file mode 100644 index 000000000..4971f3c03 --- /dev/null +++ b/packages/react-native-audio-api/src/core/sources/ABSNWeb/IStretcherNode.ts @@ -0,0 +1,69 @@ +export interface ScheduleOptions { + rate?: number; + active?: boolean; + output?: number; + input?: number; + semitones?: number; + loopStart?: number; + loopEnd?: number; +} + +export default interface IStretcherNode extends globalThis.AudioNode { + channelCount: number; + channelCountMode: globalThis.ChannelCountMode; + channelInterpretation: globalThis.ChannelInterpretation; + context: globalThis.BaseAudioContext; + numberOfInputs: number; + numberOfOutputs: number; + + onended: + | ((this: globalThis.AudioScheduledSourceNode, ev: Event) => unknown) + | null; + addEventListener: ( + type: string, + listener: EventListenerOrEventListenerObject | null, + options?: boolean | AddEventListenerOptions | undefined + ) => void; + dispatchEvent: (event: Event) => boolean; + removeEventListener: ( + type: string, + callback: EventListenerOrEventListenerObject | null, + options?: boolean | EventListenerOptions | undefined + ) => void; + + addBuffers(channels: Float32Array[]): void; + dropBuffers(): void; + + schedule(options: ScheduleOptions): void; + + start( + when?: number, + offset?: number, + duration?: number, + rate?: number, + semitones?: number + ): void; + + stop(when?: number): void; + + connect( + destination: globalThis.AudioNode, + output?: number, + input?: number + ): globalThis.AudioNode; + connect(destination: globalThis.AudioParam, output?: number): void; + + disconnect(): void; + disconnect(output: number): void; + + disconnect(destination: globalThis.AudioNode): globalThis.AudioNode; + disconnect(destination: globalThis.AudioNode, output: number): void; + disconnect( + destination: globalThis.AudioNode, + output: number, + input: number + ): void; + + disconnect(destination: globalThis.AudioParam): void; + disconnect(destination: globalThis.AudioParam, output: number): void; +} diff --git a/packages/react-native-audio-api/src/core/sources/ABSNWeb/StretcherNodeAudioParam.ts b/packages/react-native-audio-api/src/core/sources/ABSNWeb/StretcherNodeAudioParam.ts new file mode 100644 index 000000000..fb10dad3a --- /dev/null +++ b/packages/react-native-audio-api/src/core/sources/ABSNWeb/StretcherNodeAudioParam.ts @@ -0,0 +1,92 @@ +export default class StretcherNodeAudioParam implements globalThis.AudioParam { + private _value: number; + private _setter: (value: number, when?: number) => void; + + public automationRate: AutomationRate; + public defaultValue: number; + public maxValue: number; + public minValue: number; + + constructor( + value: number, + setter: (value: number, when?: number) => void, + automationRate: AutomationRate, + minValue: number, + maxValue: number, + defaultValue: number + ) { + this._value = value; + this.automationRate = automationRate; + this.minValue = minValue; + this.maxValue = maxValue; + this.defaultValue = defaultValue; + this._setter = setter; + } + + public get value(): number { + return this._value; + } + + public set value(value: number) { + this._value = value; + + this._setter(value); + } + + cancelAndHoldAtTime(cancelTime: number): globalThis.AudioParam { + this._setter(this.defaultValue, cancelTime); + return this; + } + + cancelScheduledValues(cancelTime: number): globalThis.AudioParam { + this._setter(this.defaultValue, cancelTime); + return this; + } + + exponentialRampToValueAtTime( + _value: number, + _endTime: number + ): globalThis.AudioParam { + console.warn( + 'exponentialRampToValueAtTime is not implemented for pitch correction mode' + ); + return this; + } + + linearRampToValueAtTime( + _value: number, + _endTime: number + ): globalThis.AudioParam { + console.warn( + 'linearRampToValueAtTime is not implemented for pitch correction mode' + ); + return this; + } + + setTargetAtTime( + _target: number, + _startTime: number, + _timeConstant: number + ): globalThis.AudioParam { + console.warn( + 'setTargetAtTime is not implemented for pitch correction mode' + ); + return this; + } + + setValueAtTime(value: number, startTime: number): globalThis.AudioParam { + this._setter(value, startTime); + return this; + } + + setValueCurveAtTime( + _values: Float32Array, + _startTime: number, + _duration: number + ): globalThis.AudioParam { + console.warn( + 'setValueCurveAtTime is not implemented for pitch correction mode' + ); + return this; + } +} diff --git a/packages/react-native-audio-api/src/core/sources/ABSNWeb/StretcherProvider.ts b/packages/react-native-audio-api/src/core/sources/ABSNWeb/StretcherProvider.ts new file mode 100644 index 000000000..0368fc57d --- /dev/null +++ b/packages/react-native-audio-api/src/core/sources/ABSNWeb/StretcherProvider.ts @@ -0,0 +1,59 @@ +import { customWasmLoader, globalTag } from '../../../external'; +import IStretcherNode from './IStretcherNode'; + +type TContext = globalThis.AudioContext; + +declare global { + interface Window { + [globalTag]?: (context: TContext) => Promise; + } +} + +class StretcherProvider { + private _stretcherNode: IStretcherNode | null = null; + private _isAvailable: boolean | null = null; + private _context: TContext; + + constructor(context: TContext) { + this._context = context; + + this.createStretcherNode(); + } + + private async createStretcherNode(): Promise { + try { + if (this._stretcherNode) { + return; + } + + await customWasmLoader.getPromise(); + + if (typeof window === 'undefined' || !window[globalTag]) { + return; + } + + const node = await window[globalTag](this._context); + + this._stretcherNode = node; + this._isAvailable = true; + } catch { + throw new Error('Failed to load stretcher node'); + } + } + + getStretcherNode(): IStretcherNode { + if (this._isAvailable === false || !this._stretcherNode) { + throw new Error('Stretcher node is not available'); + } + + const node = this._stretcherNode; + this._stretcherNode = null; + this._isAvailable = null; + + this.createStretcherNode(); + + return node; + } +} + +export default StretcherProvider; diff --git a/packages/react-native-audio-api/src/core/sources/ABSNWeb/index.ts b/packages/react-native-audio-api/src/core/sources/ABSNWeb/index.ts new file mode 100644 index 000000000..43abfadbf --- /dev/null +++ b/packages/react-native-audio-api/src/core/sources/ABSNWeb/index.ts @@ -0,0 +1,3 @@ +export type { default as IStretcherNode } from './IStretcherNode'; +export { default as StretcherNodeAudioParam } from './StretcherNodeAudioParam'; +export { default as StretcherProvider } from './StretcherProvider'; diff --git a/packages/react-native-audio-api/src/core/sources/AudioBufferBaseSourceNode.native.ts b/packages/react-native-audio-api/src/core/sources/AudioBufferBaseSourceNode.native.ts new file mode 100644 index 000000000..30d213097 --- /dev/null +++ b/packages/react-native-audio-api/src/core/sources/AudioBufferBaseSourceNode.native.ts @@ -0,0 +1,85 @@ +import { AudioEventSubscription } from '../../events'; +import type { + IGenericAudioParam, + IGenericBaseAudioContext, +} from '../../types/generics'; +import { + IAudioBufferBaseSourceNode, + OnPositionChangedEventCallback, +} from '../../types/interfaces'; +import AudioParam from '../AudioParam'; +import AudioScheduledSourceNode, { + MobileAudioScheduledSourceNode, +} from './AudioScheduledSourceNode.native'; + +interface NativeAudioContext {} + +export interface NativeAudioBufferBaseSourceNode + extends MobileAudioScheduledSourceNode { + readonly playbackRate: IGenericAudioParam; + readonly detune: IGenericAudioParam; + + onPositionChanged: string; // subscriptionId or '0' for none + onPositionChangedInterval: number; +} + +export default class AudioBufferBaseSourceNode< + TContext extends IGenericBaseAudioContext, + NNode extends + NativeAudioBufferBaseSourceNode = NativeAudioBufferBaseSourceNode, + > + extends AudioScheduledSourceNode + implements IAudioBufferBaseSourceNode +{ + readonly playbackRate: AudioParam; + readonly detune: AudioParam; + private positionChangedSubscription?: AudioEventSubscription; + private onPositionChangedCallback: + | OnPositionChangedEventCallback + | undefined = undefined; + + constructor(context: TContext, node: NNode) { + super(context, node); + + this.detune = new AudioParam( + node.detune, + context + ); + this.playbackRate = new AudioParam( + node.playbackRate, + context + ); + } + + public get onPositionChanged(): OnPositionChangedEventCallback | undefined { + return this.onPositionChangedCallback; + } + + public set onPositionChanged( + callback: OnPositionChangedEventCallback | null + ) { + if (!callback) { + this.node.onPositionChanged = '0'; + this.positionChangedSubscription?.remove(); + this.positionChangedSubscription = undefined; + this.onPositionChangedCallback = undefined; + + return; + } + + this.onPositionChangedCallback = callback; + this.positionChangedSubscription = + this.audioEventEmitter.addAudioEventListener('positionChanged', callback); + + this.node.onPositionChanged = + this.positionChangedSubscription.subscriptionId; + } + + public get onPositionChangedInterval(): number { + return this.node.onPositionChangedInterval; + } + + public set onPositionChangedInterval(value: number) { + this.node.onPositionChangedInterval = value; + } +} diff --git a/packages/react-native-audio-api/src/core/sources/AudioBufferBaseSourceNode.web.ts b/packages/react-native-audio-api/src/core/sources/AudioBufferBaseSourceNode.web.ts new file mode 100644 index 000000000..a1a52956e --- /dev/null +++ b/packages/react-native-audio-api/src/core/sources/AudioBufferBaseSourceNode.web.ts @@ -0,0 +1,11 @@ +import type { IGenericBaseAudioContext } from '../../types/generics'; +import type { IAudioBufferBaseSourceNode } from '../../types/interfaces'; + +/** + * No implementation for web yet, shouldn't be used directly. Consider + * implementing if/once we ship our own AudioBufferSourceNode implementation to + * web. + */ +class AudioBufferBaseSourceNode {} + +export default AudioBufferBaseSourceNode as unknown as IAudioBufferBaseSourceNode; diff --git a/packages/react-native-audio-api/src/core/sources/AudioBufferQueueSourceNode.native.ts b/packages/react-native-audio-api/src/core/sources/AudioBufferQueueSourceNode.native.ts new file mode 100644 index 000000000..95cfde5a5 --- /dev/null +++ b/packages/react-native-audio-api/src/core/sources/AudioBufferQueueSourceNode.native.ts @@ -0,0 +1,72 @@ +import { RangeError } from '../../errors'; +import type { + IGenericAudioBuffer, + IGenericBaseAudioContext, +} from '../../types/generics'; +import type { IAudioBufferQueueSourceNode } from '../../types/interfaces'; +import AudioBuffer from '../AudioBuffer'; +import AudioBufferBaseSourceNode, { + NativeAudioBufferBaseSourceNode, +} from './AudioBufferBaseSourceNode.native'; + +interface NativeAudioBufferQueueSourceNode + extends NativeAudioBufferBaseSourceNode { + enqueueBuffer(buffer: IGenericAudioBuffer): string; + dequeueBuffer(bufferId: number): void; + clearBuffers(): void; + + start(when?: number): void; + pause(): void; + stop(when?: number): void; +} + +export default class AudioBufferQueueSourceNode< + TContext extends IGenericBaseAudioContext, + > + extends AudioBufferBaseSourceNode + implements IAudioBufferQueueSourceNode +{ + public enqueueBuffer(buffer: AudioBuffer): string { + return this.node.enqueueBuffer(buffer.buffer); + } + + public dequeueBuffer(bufferId: string): void { + const id = parseInt(bufferId, 10); + + if (isNaN(id) || id < 0) { + throw new RangeError( + `bufferId must be a non-negative integer: ${bufferId}` + ); + } + + this.node.dequeueBuffer(id); + } + + public clearBuffers(): void { + this.node.clearBuffers(); + } + + public override start(when: number = 0): void { + if (when < 0) { + throw new RangeError( + `when must be a finite non-negative number: ${when}` + ); + } + + this.node.start(when); + } + + public override stop(when: number = 0): void { + if (when < 0) { + throw new RangeError( + `when must be a finite non-negative number: ${when}` + ); + } + + this.node.stop(when); + } + + public pause(): void { + this.node.pause(); + } +} diff --git a/packages/react-native-audio-api/src/core/sources/AudioBufferQueueSourcenode.web.ts b/packages/react-native-audio-api/src/core/sources/AudioBufferQueueSourcenode.web.ts new file mode 100644 index 000000000..c84ad81a4 --- /dev/null +++ b/packages/react-native-audio-api/src/core/sources/AudioBufferQueueSourcenode.web.ts @@ -0,0 +1,7 @@ +import type { IGenericBaseAudioContext } from '../../types/generics'; +import type { IAudioBufferQueueSourceNode } from '../../types/interfaces'; + +class AudioBufferQueueSourceNode {} + +// Maybe someday we will implement this for the web as well +export default AudioBufferQueueSourceNode as unknown as IAudioBufferQueueSourceNode; diff --git a/packages/react-native-audio-api/src/core/sources/AudioBufferSourceNode.native.ts b/packages/react-native-audio-api/src/core/sources/AudioBufferSourceNode.native.ts new file mode 100644 index 000000000..820b2c1d0 --- /dev/null +++ b/packages/react-native-audio-api/src/core/sources/AudioBufferSourceNode.native.ts @@ -0,0 +1,148 @@ +import { InvalidStateError, RangeError } from '../../errors'; +import { AudioEventSubscription } from '../../events'; +import type { + IGenericAudioBuffer, + IGenericBaseAudioContext, +} from '../../types/generics'; +import type { + IAudioBufferSourceNode, + LoopEndedEventCallback, +} from '../../types/interfaces'; +import AudioBuffer from '../AudioBuffer'; +import AudioBufferBaseSourceNode, { + NativeAudioBufferBaseSourceNode, +} from './AudioBufferBaseSourceNode.native'; + +interface NativeAudioBufferSourceNode extends NativeAudioBufferBaseSourceNode { + readonly buffer: IGenericAudioBuffer | null; + loopSkip: boolean; + loop: boolean; + loopStart: number; + loopEnd: number; + + setBuffer(buffer: IGenericAudioBuffer | null): void; + + start(when?: number): void; + start(when: number, offset: number, duration?: number): void; + + onLoopEnded: string; +} + +export default class AudioBufferSourceNode< + TContext extends IGenericBaseAudioContext, + > + extends AudioBufferBaseSourceNode + implements IAudioBufferSourceNode +{ + private mBuffer: AudioBuffer | null = null; + private onLoopEndedSubscription?: AudioEventSubscription; + private onLoopEndedCallback?: LoopEndedEventCallback; + + public get buffer(): AudioBuffer | null { + return this.mBuffer; + } + + public set buffer(buffer: IGenericAudioBuffer | null) { + this.node.setBuffer(buffer); + + if (!this.node.buffer) { + this.mBuffer = null; + return; + } + + this.mBuffer = new AudioBuffer(this.node.buffer); + } + + public get loopSkip(): boolean { + return this.node.loopSkip; + } + + public set loopSkip(value: boolean) { + this.node.loopSkip = value; + } + + public get loop(): boolean { + return this.node.loop; + } + + public set loop(value: boolean) { + this.node.loop = value; + } + + public get loopStart(): number { + return this.node.loopStart; + } + + public set loopStart(value: number) { + this.node.loopStart = value; + } + + public get loopEnd(): number { + return this.node.loopEnd; + } + + public set loopEnd(value: number) { + this.node.loopEnd = value; + } + + public start(when: number = 0, offset: number = 0, duration?: number): void { + if (when < 0) { + throw new RangeError( + `when must be a finite non-negative number: ${when}` + ); + } + + if (offset < 0) { + throw new RangeError( + `offset must be a finite non-negative number: ${offset}` + ); + } + + if (duration && duration < 0) { + throw new RangeError( + `duration must be a finite non-negative number: ${duration}` + ); + } + + if (this.hasBeenStarted) { + throw new InvalidStateError('Cannot call start more than once'); + } + + this.hasBeenStarted = true; + this.node.start(when, offset, duration); + } + + public get onLoopEnded(): LoopEndedEventCallback | undefined { + return this.onLoopEndedCallback; + } + + public set onLoopEnded(callback: LoopEndedEventCallback | null) { + if (!callback) { + this.node.onLoopEnded = '0'; + this.onLoopEndedSubscription?.remove(); + this.onLoopEndedSubscription = undefined; + this.onLoopEndedCallback = undefined; + + return; + } + + this.onLoopEndedCallback = callback; + this.onLoopEndedSubscription = this.audioEventEmitter.addAudioEventListener( + 'loopEnded', + callback + ); + + this.node.onLoopEnded = this.onLoopEndedSubscription.subscriptionId; + } + + // ????????????????????????? + // TODO: Generic this out? + // public override get onEnded(): ((event: EventEmptyType) => void) | undefined { + // return super.onEnded as ((event: EventEmptyType) => void) | undefined; + // } + // public override set onEnded( + // callback: ((event: EventEmptyType) => void) | null + // ) { + // super.onEnded = callback; + // } +} diff --git a/packages/react-native-audio-api/src/core/sources/AudioBufferSourceNode.web.ts b/packages/react-native-audio-api/src/core/sources/AudioBufferSourceNode.web.ts new file mode 100644 index 000000000..2731c822f --- /dev/null +++ b/packages/react-native-audio-api/src/core/sources/AudioBufferSourceNode.web.ts @@ -0,0 +1,426 @@ +import { InvalidStateError, RangeError } from '../../errors'; + +import type { ChannelCountMode, ChannelInterpretation } from '../../types'; +import type { IGenericBaseAudioContext } from '../../types/generics'; +import type { + IAudioBufferSourceNode, + LoopEndedEventCallback, + OnEndedEventCallback, + OnPositionChangedEventCallback, +} from '../../types/interfaces'; +import { availabilityWarn } from '../../utils'; +import AudioBuffer from '../AudioBuffer'; +import AudioNode from '../AudioNode'; +import AudioParam from '../AudioParam'; + +import { IStretcherNode, StretcherNodeAudioParam } from './ABSNWeb'; + +type NativeAudioContext = globalThis.BaseAudioContext; +type NativeAudioBufferSourceNode = globalThis.AudioBufferSourceNode; +type NativeAudioParam = globalThis.AudioParam; +type NativeAudioNode = globalThis.AudioNode; + +type WebAudioNode = NativeAudioNode | IStretcherNode; +type WebBufferSourceNode = NativeAudioBufferSourceNode | IStretcherNode; +type WebAudioParam = NativeAudioParam | StretcherNodeAudioParam; + +function isStretcherNode( + node: WebBufferSourceNode, + pitchCorrection: boolean +): node is IStretcherNode { + return pitchCorrection && (node as IStretcherNode).schedule !== undefined; +} + +function isNativeNode( + node: WebBufferSourceNode, + pitchCorrection: boolean +): node is NativeAudioBufferSourceNode { + return ( + !pitchCorrection && + (node as NativeAudioBufferSourceNode).buffer !== undefined + ); +} + +export default class AudioBufferSourceNode< + TContext extends IGenericBaseAudioContext, +> implements IAudioBufferSourceNode +{ + readonly playbackRate: AudioParam; + readonly detune: AudioParam; + readonly numberOfInputs: number = 0; + readonly numberOfOutputs: number = 1; + channelCount: number = 0; + readonly channelCountMode: ChannelCountMode = 'explicit'; + readonly channelInterpretation: ChannelInterpretation = 'speakers'; + readonly context: TContext; + + private mPitchCorrection: boolean; + + private mLoop: boolean = false; + private mLoopStart: number = 0; + private mLoopEnd: number = 0; + private mNode: WebBufferSourceNode; + private mLoopSkip: boolean = false; + + private mBuffer: AudioBuffer | null = null; + private mHasBeenStarted = false; + private mCallback: OnEndedEventCallback | null = null; + + constructor( + context: TContext, + node: WebBufferSourceNode, + pitchCorrection: boolean + ) { + this.mNode = node; + this.context = context; + this.mPitchCorrection = pitchCorrection; + + if (isNativeNode(node, pitchCorrection)) { + this.detune = new AudioParam( + node.detune, + context + ); + + this.playbackRate = new AudioParam( + node.playbackRate, + context + ); + return; + } + + this.detune = new AudioParam( + new StretcherNodeAudioParam( + 0, + this.setDetune.bind(this), + 'a-rate', + -1200, + 1200, + 0 + ), + context + ); + + this.playbackRate = new AudioParam( + new StretcherNodeAudioParam( + 1, + this.setPlaybackRate.bind(this), + 'a-rate', + 0, + Infinity, + 1 + ), + context + ); + } + + setDetune(value: number, _when: number = 0) { + // This method should be called only for stretcher node (internally used with StretcherNodeAudioParam) + // for not-started nodes, we don't have to do anything. Start method will pick up the initial detune value. + if ( + !isStretcherNode(this.mNode, this.mPitchCorrection) || + !this.mHasBeenStarted + ) { + return; + } + + this.mNode.schedule({ + semitones: Math.floor(Math.max(-12, Math.min(12, value / 100))), + output: 0, + }); + } + + setPlaybackRate(value: number, when: number = 0) { + if ( + !isStretcherNode(this.mNode, this.mPitchCorrection) || + !this.mHasBeenStarted + ) { + return; + } + + this.mNode.schedule({ + rate: value, + output: when, + }); + } + + get buffer(): AudioBuffer | null { + if (isStretcherNode(this.mNode, this.mPitchCorrection)) { + return this.mBuffer; + } + + const buffer = this.mNode.buffer; + + if (!buffer) { + return null; + } + + return new AudioBuffer(buffer); + } + + set buffer(value: AudioBuffer | null) { + this.channelCount = value ? value.numberOfChannels : 0; + if (isStretcherNode(this.mNode, this.mPitchCorrection)) { + this.mBuffer = value; + + const stretcher = this.mNode; + stretcher.dropBuffers(); + + if (!value) { + return; + } + + const channelArrays: Float32Array[] = []; + + for (let i = 0; i < value.numberOfChannels; i++) { + channelArrays.push(value.getChannelData(i)); + } + + stretcher.addBuffers(channelArrays); + return; + } + + if (this.mHasBeenStarted) { + throw new InvalidStateError( + 'Cannot set buffer after the source has been started.' + ); + } + + this.mNode.buffer = value ? value.buffer : null; + } + + get loop(): boolean { + if (isStretcherNode(this.mNode, this.mPitchCorrection)) { + return this.mLoop; + } + + return this.mNode.loop; + } + + set loop(value: boolean) { + if (isStretcherNode(this.mNode, this.mPitchCorrection)) { + this.mLoop = value; + return; + } + + this.mNode.loop = value; + } + + get loopStart(): number { + if (isStretcherNode(this.mNode, this.mPitchCorrection)) { + return this.mLoopStart; + } + + return this.mNode.loopStart; + } + + set loopStart(value: number) { + if (value < 0) { + throw new RangeError('loopStart must be non-negative'); + } + + if (isStretcherNode(this.mNode, this.mPitchCorrection)) { + this.mLoopStart = value; + return; + } + + this.mNode.loopStart = value; + } + + get loopEnd(): number { + if (isStretcherNode(this.mNode, this.mPitchCorrection)) { + return this.mLoopEnd; + } + + return this.mNode.loopEnd; + } + + set loopEnd(value: number) { + if (isStretcherNode(this.mNode, this.mPitchCorrection)) { + this.mLoopEnd = value; + return; + } + + this.mNode.loopEnd = value; + } + + get loopSkip(): boolean { + availabilityWarn('AudioBufferSourceNode.loopSkip', 'web'); + return this.mLoopSkip; + } + + set loopSkip(value: boolean) { + availabilityWarn('AudioBufferSourceNode.loopSkip', 'web'); + this.mLoopSkip = value; + } + + public start(when?: number, offset?: number, duration?: number): void { + if (when && when < 0) { + throw new RangeError( + `when must be a finite non-negative number: ${when}` + ); + } + + if (offset && offset < 0) { + throw new RangeError( + `offset must be a finite non-negative number: ${offset}` + ); + } + + if (duration && duration < 0) { + throw new RangeError( + `duration must be a finite non-negative number: ${duration}` + ); + } + + if (this.mHasBeenStarted) { + throw new InvalidStateError('Cannot call start more than once'); + } + + this.mHasBeenStarted = true; + + if (!isStretcherNode(this.mNode, this.mPitchCorrection)) { + this.mNode.start(when, offset, duration); + return; + } + + const startAt = + !when || when < this.mNode.context.currentTime + ? this.mNode.context.currentTime + : when; + + if (this.loop && this.mLoopStart !== -1 && this.mLoopEnd !== -1) { + // Schedule looping if needed + this.mNode.schedule({ + loopStart: this.mLoopStart, + loopEnd: this.mLoopEnd, + }); + } + + this.mNode.start( + startAt, + offset, + duration, + this.playbackRate.value, + Math.floor(Math.max(-12, Math.min(12, this.detune.value / 100))) + ); + } + + public stop(when: number = 0): void { + if (when < 0) { + throw new RangeError( + `when must be a finite non-negative number: ${when}` + ); + } + + if (!this.mHasBeenStarted) { + throw new InvalidStateError( + 'Cannot call stop without calling start first' + ); + } + + this.mNode.stop(when); + } + + public get onPositionChanged(): OnPositionChangedEventCallback | undefined { + availabilityWarn('AudioBufferSourceNode.onPositionChanged', 'web'); + return undefined; + } + + public set onPositionChanged( + _callback: OnPositionChangedEventCallback | null + ) { + availabilityWarn('AudioBufferSourceNode.onPositionChanged', 'web'); + } + + public set onPositionChangedInterval(value: number) { + availabilityWarn('AudioBufferSourceNode.onPositionChangedInterval', 'web'); + } + + public get onPositionChangedInterval(): number { + availabilityWarn('AudioBufferSourceNode.onPositionChangedInterval', 'web'); + return 0; + } + + public get onEnded(): OnEndedEventCallback | undefined { + if (isStretcherNode(this.mNode, this.mPitchCorrection)) { + availabilityWarn('AudioBufferSourceNode.onEnded', 'web'); + } + + return this.mCallback || undefined; + } + + public set onEnded(callback: OnEndedEventCallback | null) { + if (isStretcherNode(this.mNode, this.mPitchCorrection)) { + availabilityWarn('AudioBufferSourceNode.onEnded', 'web'); + return; + } + + this.mCallback = callback; + + this.mNode.onended = () => { + this.mCallback?.({ bufferId: undefined, isLast: true }); + }; + } + + public get onLoopEnded(): LoopEndedEventCallback | undefined { + availabilityWarn('AudioBufferSourceNode.onLoopEnded', 'web'); + return undefined; + } + + public set onLoopEnded(callback: LoopEndedEventCallback | null) { + availabilityWarn('AudioBufferSourceNode.onLoopEnded', 'web'); + } + + public connect>( + destination: ONode + ): ONode; + + public connect(destination: AudioParam): void; + + public connect>( + destination: ONode | AudioParam + ): ONode | void { + if (this.context !== destination.context) { + throw new Error( + 'Source and destination are from different BaseAudioContexts' + ); + } + + if (destination instanceof AudioParam) { + this.mNode.connect(destination.param as WebAudioParam); + return; + } + + // TS hack since due to how Stretcher vs NativeAudioNode types are defined + // we cannot extend/subclass the AudioNode here (where node is protected) + // eslint-disable-next-line @typescript-eslint/no-explicit-any + this.mNode.connect((destination as any).node as WebAudioNode); + return destination; + } + + public disconnect(): void; + + public disconnect>( + destination: ONode + ): void; + + public disconnect( + destination: AudioParam + ): void; + + public disconnect( + destination?: + | AudioNode + | AudioParam + ): void { + if (destination instanceof AudioParam) { + this.mNode.disconnect(destination.param as WebAudioParam); + return; + } + + // TS hack since due to how Stretcher vs NativeAudioNode types are defined + // we cannot extend/subclass the AudioNode here (where node is protected) + // eslint-disable-next-line @typescript-eslint/no-explicit-any + this.mNode.disconnect((destination as any)?.node as WebAudioNode); + } +} diff --git a/packages/react-native-audio-api/src/core/sources/AudioScheduledSourceNode.native.ts b/packages/react-native-audio-api/src/core/sources/AudioScheduledSourceNode.native.ts new file mode 100644 index 000000000..780a536cf --- /dev/null +++ b/packages/react-native-audio-api/src/core/sources/AudioScheduledSourceNode.native.ts @@ -0,0 +1,48 @@ +import { AudioEventEmitter, AudioEventSubscription } from '../../events'; +import type { IGenericBaseAudioContext } from '../../types/generics'; +import { OnEndedEventCallback } from '../../types/interfaces'; +import BaseAudioScheduledSourceNode, { + IAbstractNativeAudioScheduledSourceNode, +} from './BaseAudioScheduledSourceNode'; + +// TODO: fixme - temporary any to avoid work +interface NativeAudioContext {} + +export interface MobileAudioScheduledSourceNode + extends IAbstractNativeAudioScheduledSourceNode { + onEnded: string; // subscriptionId or '0' for none +} + +export default class AudioScheduledSourceNodeNative< + TContext extends IGenericBaseAudioContext, + NNode extends MobileAudioScheduledSourceNode = MobileAudioScheduledSourceNode, +> extends BaseAudioScheduledSourceNode { + protected readonly audioEventEmitter = new AudioEventEmitter( + global.AudioEventEmitter + ); + + private onEndedSubscription?: AudioEventSubscription; + private onEndedCallback: OnEndedEventCallback | null = null; + + public get onEnded(): OnEndedEventCallback | undefined { + return this.onEndedCallback ?? undefined; + } + + public set onEnded(callback: OnEndedEventCallback | null) { + if (!callback) { + this.node.onEnded = '0'; + this.onEndedSubscription?.remove(); + this.onEndedSubscription = undefined; + this.onEndedCallback = null; + return; + } + + this.onEndedCallback = callback; + this.onEndedSubscription = this.audioEventEmitter.addAudioEventListener( + 'ended', + callback + ); + + this.node.onEnded = this.onEndedSubscription.subscriptionId; + } +} diff --git a/packages/react-native-audio-api/src/core/sources/AudioScheduledSourceNode.web.ts b/packages/react-native-audio-api/src/core/sources/AudioScheduledSourceNode.web.ts new file mode 100644 index 000000000..e515db83c --- /dev/null +++ b/packages/react-native-audio-api/src/core/sources/AudioScheduledSourceNode.web.ts @@ -0,0 +1,30 @@ +import type { IGenericBaseAudioContext } from '../../types/generics'; +import { OnEndedEventCallback } from '../../types/interfaces'; +import BaseAudioScheduledSourceNode from './BaseAudioScheduledSourceNode'; + +type NativeAudioContext = globalThis.BaseAudioContext; +type NativeAudioScheduledSourceNode = globalThis.AudioScheduledSourceNode; + +export default class AudioScheduledSourceNode< + TContext extends IGenericBaseAudioContext, + NNode extends NativeAudioScheduledSourceNode = NativeAudioScheduledSourceNode, +> extends BaseAudioScheduledSourceNode { + private onEndedCallback: OnEndedEventCallback | undefined = undefined; + + public get onEnded(): OnEndedEventCallback | undefined { + return this.onEndedCallback; + } + + public set onEnded(callback: OnEndedEventCallback | undefined) { + this.onEndedCallback = callback; + + if (!callback) { + this.node.onended = null; + return; + } + + this.node.onended = () => { + this.onEndedCallback?.({ bufferId: undefined, isLast: false }); + }; + } +} diff --git a/packages/react-native-audio-api/src/core/sources/BaseAudioScheduledSourceNode.ts b/packages/react-native-audio-api/src/core/sources/BaseAudioScheduledSourceNode.ts new file mode 100644 index 000000000..e99d59b8b --- /dev/null +++ b/packages/react-native-audio-api/src/core/sources/BaseAudioScheduledSourceNode.ts @@ -0,0 +1,63 @@ +import { InvalidStateError, RangeError } from '../../errors'; +import type { + IGenericAudioNode, + IGenericBaseAudioContext, +} from '../../types/generics'; +import type { + IAudioScheduledSourceNode, + OnEndedEventCallback, +} from '../../types/interfaces'; +import AudioNode from '../AudioNode'; + +export interface IAbstractNativeAudioScheduledSourceNode< + NContext extends IGenericBaseAudioContext, +> extends IGenericAudioNode { + start(when?: number): void; + stop(when?: number): void; +} + +export default abstract class BaseAudioScheduledSourceNode< + TContext extends IGenericBaseAudioContext, + NContext extends IGenericBaseAudioContext, + NNode extends + IAbstractNativeAudioScheduledSourceNode = IAbstractNativeAudioScheduledSourceNode, + > + extends AudioNode + implements IAudioScheduledSourceNode +{ + protected hasBeenStarted: boolean = false; + + public start(when: number = 0): void { + if (when < 0) { + throw new RangeError( + `when must be a finite non-negative number: ${when}` + ); + } + + if (this.hasBeenStarted) { + throw new InvalidStateError('Cannot call start more than once'); + } + + this.hasBeenStarted = true; + this.node.start(when); + } + + public stop(when: number = 0): void { + if (when < 0) { + throw new RangeError( + `when must be a finite non-negative number: ${when}` + ); + } + + if (!this.hasBeenStarted) { + throw new InvalidStateError( + 'Cannot call stop without calling start first' + ); + } + + this.node.stop(when); + } + + public abstract get onEnded(): OnEndedEventCallback | undefined; + public abstract set onEnded(callback: OnEndedEventCallback | null); +} diff --git a/packages/react-native-audio-api/src/core/sources/CostantSourceNode.ts b/packages/react-native-audio-api/src/core/sources/CostantSourceNode.ts new file mode 100644 index 000000000..24cce89bc --- /dev/null +++ b/packages/react-native-audio-api/src/core/sources/CostantSourceNode.ts @@ -0,0 +1,17 @@ +import { IGenericBaseAudioContext } from '../../types/generics'; +import IConstantSourceNode from '../../types/interfaces/IConstantSourceNode'; +import AudioParam from '../AudioParam'; +import AudioScheduledSourceNode from './AudioScheduledSourceNode'; + +export default class ConstantSourceNode< + TContext extends IGenericBaseAudioContext, + NContext extends IGenericBaseAudioContext, +> extends AudioScheduledSourceNode> { + readonly offset: AudioParam; + + constructor(context: TContext, node: IConstantSourceNode) { + super(context, node); + + this.offset = new AudioParam(node.offset, context); + } +} diff --git a/packages/react-native-audio-api/src/core/sources/OscillatorNode.ts b/packages/react-native-audio-api/src/core/sources/OscillatorNode.ts new file mode 100644 index 000000000..fefd6a164 --- /dev/null +++ b/packages/react-native-audio-api/src/core/sources/OscillatorNode.ts @@ -0,0 +1,66 @@ +import { InvalidStateError } from '../../errors'; +import type { EventEmptyType } from '../../events'; +import type { OscillatorType } from '../../types'; +import type { + IGenericBaseAudioContext, + IGenericOscillatorNode, +} from '../../types/generics'; +import AudioParam from '../AudioParam'; +import PeriodicWave from '../PeriodicWave'; + +import AudioScheduledSourceNode from './AudioScheduledSourceNode'; + +export default class OscillatorNode< + TContext extends IGenericBaseAudioContext, + NContext extends IGenericBaseAudioContext, + > + // Baka! check explanation in IGenericOscillatorNode + // eslint-disable-next-line @typescript-eslint/no-explicit-any + extends AudioScheduledSourceNode + implements IGenericOscillatorNode +{ + readonly frequency: AudioParam; + readonly detune: AudioParam; + // redefinition due to typescript limitation with generics and inheritance + protected readonly node: IGenericOscillatorNode; + + constructor(context: TContext, node: IGenericOscillatorNode) { + super(context, node); + this.node = node; + + this.frequency = new AudioParam( + node.frequency, + context + ); + + this.detune = new AudioParam(node.detune, context); + } + + public get type(): OscillatorType { + return this.node.type; + } + + public set type(value: OscillatorType) { + if (value === 'custom') { + throw new InvalidStateError( + "'type' cannot be set directly to 'custom'. Use setPeriodicWave() to create a custom Oscillator type." + ); + } + + this.node.type = value; + } + + public setPeriodicWave(wave: PeriodicWave): void { + this.node.setPeriodicWave(wave.periodicWave); + } + + public override get onEnded(): ((event: EventEmptyType) => void) | undefined { + return super.onEnded as ((event: EventEmptyType) => void) | undefined; + } + + public override set onEnded( + callback: ((event: EventEmptyType) => void) | null + ) { + super.onEnded = callback; + } +} diff --git a/packages/react-native-audio-api/src/core/sources/RecorderAdapterNode.native.ts b/packages/react-native-audio-api/src/core/sources/RecorderAdapterNode.native.ts new file mode 100644 index 000000000..ec92f5812 --- /dev/null +++ b/packages/react-native-audio-api/src/core/sources/RecorderAdapterNode.native.ts @@ -0,0 +1,21 @@ +import type { + IGenericBaseAudioContext, + IGenericRecorderAdapterNode, +} from '../../types/generics'; +import AudioNode from '../AudioNode'; + +export default class RecorderAdapterNode< + TContext extends IGenericBaseAudioContext, + NContext extends IGenericBaseAudioContext, + > + extends AudioNode + implements IGenericRecorderAdapterNode +{ + /** @internal */ + public wasConnected: boolean = false; + + /** @internal */ + public getNode(): IGenericRecorderAdapterNode { + return this.node; + } +} diff --git a/packages/react-native-audio-api/src/core/sources/RecorderAdapterNode.web.ts b/packages/react-native-audio-api/src/core/sources/RecorderAdapterNode.web.ts new file mode 100644 index 000000000..0e8ce6c4a --- /dev/null +++ b/packages/react-native-audio-api/src/core/sources/RecorderAdapterNode.web.ts @@ -0,0 +1,35 @@ +import type { + IGenericBaseAudioContext, + IGenericRecorderAdapterNode, +} from '../../types/generics'; +import { availabilityWarn } from '../../utils'; + +export default class RecorderAdapterNode< + TContext extends IGenericBaseAudioContext, + NContext extends IGenericBaseAudioContext, +> implements IGenericRecorderAdapterNode +{ + public numberOfInputs: number = 1; + public numberOfOutputs: number = 1; + public channelCount: number = 2; + public channelCountMode: 'max' | 'clamped-max' | 'explicit' = 'max'; + public channelInterpretation: 'speakers' | 'discrete' = 'speakers'; + + /** @internal */ + public wasConnected: boolean = false; + + private node: IGenericRecorderAdapterNode; + public context: TContext; + + constructor(context: TContext, node: IGenericRecorderAdapterNode) { + this.node = node; + this.context = context; + } + + /** @internal */ + public getNode(): IGenericRecorderAdapterNode { + availabilityWarn('RecorderAdapterNode', 'web'); + // TODO: fishy + return {} as IGenericRecorderAdapterNode; + } +} diff --git a/packages/react-native-audio-api/src/events/AudioEventEmitter.ts b/packages/react-native-audio-api/src/events/AudioEventEmitter.ts index 83d63bc85..bc75054d4 100644 --- a/packages/react-native-audio-api/src/events/AudioEventEmitter.ts +++ b/packages/react-native-audio-api/src/events/AudioEventEmitter.ts @@ -1,6 +1,21 @@ -import { AudioEventName, AudioEventCallback } from './types'; import AudioEventSubscription from './AudioEventSubscription'; -import { IAudioEventEmitter } from '../interfaces'; +import { AudioEventCallback, AudioEventName } from './types'; + +interface IAudioEventEmitter { + addAudioEventListener( + name: Name, + callback: AudioEventCallback + ): string; + removeAudioEventListener( + name: Name, + subscriptionId: string + ): void; +} + +/* eslint-disable no-var */ +declare global { + var AudioEventEmitter: IAudioEventEmitter; +} export default class AudioEventEmitter { private readonly audioEventEmitter: IAudioEventEmitter; diff --git a/packages/react-native-audio-api/src/events/AudioEventSubscription.ts b/packages/react-native-audio-api/src/events/AudioEventSubscription.ts index 8b82850b0..a0f7b2222 100644 --- a/packages/react-native-audio-api/src/events/AudioEventSubscription.ts +++ b/packages/react-native-audio-api/src/events/AudioEventSubscription.ts @@ -1,5 +1,5 @@ -import { AudioEventName } from './types'; -import { AudioEventEmitter } from './'; +import type AudioEventEmitter from './AudioEventEmitter'; +import type { AudioEventName } from './types'; export default class AudioEventSubscription { private readonly audioEventEmitter: AudioEventEmitter; diff --git a/packages/react-native-audio-api/src/events/index.ts b/packages/react-native-audio-api/src/events/index.ts index 7f695f798..6c453cfab 100644 --- a/packages/react-native-audio-api/src/events/index.ts +++ b/packages/react-native-audio-api/src/events/index.ts @@ -1,4 +1,4 @@ -import AudioEventEmitter from './AudioEventEmitter'; -import AudioEventSubscription from './AudioEventSubscription'; +export { default as AudioEventEmitter } from './AudioEventEmitter'; +export { default as AudioEventSubscription } from './AudioEventSubscription'; -export { AudioEventEmitter, AudioEventSubscription }; +export * from './types'; diff --git a/packages/react-native-audio-api/src/external/constants.ts b/packages/react-native-audio-api/src/external/constants.ts new file mode 100644 index 000000000..a1be9aff0 --- /dev/null +++ b/packages/react-native-audio-api/src/external/constants.ts @@ -0,0 +1,2 @@ +export const globalTag: string = '__rnaaCstStretch'; +export const eventTitle: string = 'rnaaCstStretchLoaded'; diff --git a/packages/react-native-audio-api/src/external/customWasmLoader.native.ts b/packages/react-native-audio-api/src/external/customWasmLoader.native.ts new file mode 100644 index 000000000..d0b8510c3 --- /dev/null +++ b/packages/react-native-audio-api/src/external/customWasmLoader.native.ts @@ -0,0 +1,16 @@ +import type IWasmLoader from './interface'; + +class CustomWasmLoader implements IWasmLoader { + private loadPromise: Promise = Promise.resolve(); + + async load(_pathPrefix: string = ''): Promise { + // No-op on native + return this.loadPromise; + } + + getPromise(): Promise { + return this.loadPromise; + } +} + +export default new CustomWasmLoader(); diff --git a/packages/react-native-audio-api/src/external/customWasmLoader.web.ts b/packages/react-native-audio-api/src/external/customWasmLoader.web.ts new file mode 100644 index 000000000..592017def --- /dev/null +++ b/packages/react-native-audio-api/src/external/customWasmLoader.web.ts @@ -0,0 +1,54 @@ +import { eventTitle, globalTag } from './constants'; +import type IWasmLoader from './interface'; + +class CustomWasmLoader implements IWasmLoader { + private loadPromise: Promise | null = null; + + async load(pathPrefix: string = ''): Promise { + if (typeof window === 'undefined' || typeof document === 'undefined') { + return Promise.resolve(); + } + + if (this.loadPromise) { + return this.loadPromise; + } + + this.loadPromise = new Promise((resolve, reject) => { + const scriptTag = document.createElement('script'); + scriptTag.type = 'module'; + + scriptTag.textContent = ` + import SignalsmithStretch from '${pathPrefix}/signalsmithStretch.mjs'; + window.${globalTag} = SignalsmithStretch; + window.postMessage('${eventTitle}'); + `; + + function onLoaded(event: MessageEvent) { + if (event.data !== eventTitle) { + reject(new Error(`Unexpected event received: ${event.data}`)); + return; + } + + resolve(); + window.removeEventListener('message', onLoaded); + } + + window.addEventListener('message', onLoaded); + document.head.appendChild(scriptTag); + }); + + return this.loadPromise; + } + + getPromise(): Promise { + if (!this.loadPromise) { + return Promise.reject( + new Error('WASM not loaded yet. Call load() first.') + ); + } + + return this.loadPromise; + } +} + +export default new CustomWasmLoader(); diff --git a/packages/react-native-audio-api/src/external/index.ts b/packages/react-native-audio-api/src/external/index.ts new file mode 100644 index 000000000..39e249958 --- /dev/null +++ b/packages/react-native-audio-api/src/external/index.ts @@ -0,0 +1,2 @@ +export * from './constants'; +export { default as customWasmLoader } from './customWasmLoader'; diff --git a/packages/react-native-audio-api/src/external/interface.ts b/packages/react-native-audio-api/src/external/interface.ts new file mode 100644 index 000000000..100b94802 --- /dev/null +++ b/packages/react-native-audio-api/src/external/interface.ts @@ -0,0 +1,4 @@ +export default interface IWasmLoader { + load(pathPrefix: string): Promise; + getPromise(): Promise; +} diff --git a/packages/react-native-audio-api/src/web-core/custom/signalsmithStretch/LICENSE.txt b/packages/react-native-audio-api/src/external/signalsmithStretch/LICENSE.txt similarity index 100% rename from packages/react-native-audio-api/src/web-core/custom/signalsmithStretch/LICENSE.txt rename to packages/react-native-audio-api/src/external/signalsmithStretch/LICENSE.txt diff --git a/packages/react-native-audio-api/src/web-core/custom/signalsmithStretch/README.md b/packages/react-native-audio-api/src/external/signalsmithStretch/README.md similarity index 100% rename from packages/react-native-audio-api/src/web-core/custom/signalsmithStretch/README.md rename to packages/react-native-audio-api/src/external/signalsmithStretch/README.md diff --git a/packages/react-native-audio-api/src/web-core/custom/signalsmithStretch/SignalsmithStretch.mjs b/packages/react-native-audio-api/src/external/signalsmithStretch/SignalsmithStretch.mjs similarity index 100% rename from packages/react-native-audio-api/src/web-core/custom/signalsmithStretch/SignalsmithStretch.mjs rename to packages/react-native-audio-api/src/external/signalsmithStretch/SignalsmithStretch.mjs diff --git a/packages/react-native-audio-api/src/hooks/useSystemVolume.ts b/packages/react-native-audio-api/src/hooks/useSystemVolume.ts index 8016d20d1..c658f48cf 100644 --- a/packages/react-native-audio-api/src/hooks/useSystemVolume.ts +++ b/packages/react-native-audio-api/src/hooks/useSystemVolume.ts @@ -1,23 +1,23 @@ -import { useEffect, useState } from 'react'; -import AudioManager from '../system/AudioManager'; +// import { useEffect, useState } from 'react'; +// import AudioManager from '../system/AudioManager'; -export default function useSystemVolume() { - const [volume, setVolume] = useState(0); +// export default function useSystemVolume() { +// const [volume, setVolume] = useState(0); - useEffect(() => { - AudioManager.observeVolumeChanges(true); - const listener = AudioManager.addSystemEventListener( - 'volumeChange', - (e) => { - setVolume(parseFloat(e.value.toFixed(2))); - } - ); - return () => { - listener?.remove(); +// useEffect(() => { +// AudioManager.observeVolumeChanges(true); +// const listener = AudioManager.addSystemEventListener( +// 'volumeChange', +// (e) => { +// setVolume(parseFloat(e.value.toFixed(2))); +// } +// ); +// return () => { +// listener?.remove(); - AudioManager.observeVolumeChanges(false); - }; - }, []); +// AudioManager.observeVolumeChanges(false); +// }; +// }, []); - return volume; -} +// return volume; +// } diff --git a/packages/react-native-audio-api/src/index.ts b/packages/react-native-audio-api/src/index.ts index b1c13e734..e69de29bb 100644 --- a/packages/react-native-audio-api/src/index.ts +++ b/packages/react-native-audio-api/src/index.ts @@ -1 +0,0 @@ -export * from './api'; diff --git a/packages/react-native-audio-api/src/interfaces.ts b/packages/react-native-audio-api/src/interfaces.ts deleted file mode 100644 index 67c5d0299..000000000 --- a/packages/react-native-audio-api/src/interfaces.ts +++ /dev/null @@ -1,296 +0,0 @@ -import { AudioEventCallback, AudioEventName } from './events/types'; -import { - BiquadFilterType, - ChannelCountMode, - ChannelInterpretation, - ContextState, - OscillatorType, - WindowType, -} from './types'; - -export type WorkletNodeCallback = ( - audioData: Array, - channelCount: number -) => void; - -export type WorkletSourceNodeCallback = ( - audioData: Array, - framesToProcess: number, - currentTime: number, - startOffset: number -) => void; - -export type WorkletProcessingNodeCallback = ( - inputData: Array, - outputData: Array, - framesToProcess: number, - currentTime: number -) => void; - -export type ShareableWorkletCallback = - | WorkletNodeCallback - | WorkletSourceNodeCallback - | WorkletProcessingNodeCallback; - -export interface IBaseAudioContext { - readonly destination: IAudioDestinationNode; - readonly state: ContextState; - readonly sampleRate: number; - readonly currentTime: number; - readonly decoder: IAudioDecoder; - readonly stretcher: IAudioStretcher; - - createRecorderAdapter(): IRecorderAdapterNode; - createWorkletSourceNode( - shareableWorklet: ShareableWorkletCallback, - shouldUseUiRuntime: boolean - ): IWorkletSourceNode; - createWorkletNode( - shareableWorklet: ShareableWorkletCallback, - shouldUseUiRuntime: boolean, - bufferLength: number, - inputChannelCount: number - ): IWorkletNode; - createWorkletProcessingNode( - shareableWorklet: ShareableWorkletCallback, - shouldUseUiRuntime: boolean - ): IWorkletProcessingNode; - createOscillator(): IOscillatorNode; - createConstantSource(): IConstantSourceNode; - createGain(): IGainNode; - createStereoPanner(): IStereoPannerNode; - createBiquadFilter: () => IBiquadFilterNode; - createBufferSource: (pitchCorrection: boolean) => IAudioBufferSourceNode; - createBufferQueueSource: ( - pitchCorrection: boolean - ) => IAudioBufferQueueSourceNode; - createBuffer: ( - channels: number, - length: number, - sampleRate: number - ) => IAudioBuffer; - createPeriodicWave: ( - real: Float32Array, - imag: Float32Array, - disableNormalization: boolean - ) => IPeriodicWave; - createAnalyser: () => IAnalyserNode; - createStreamer: () => IStreamerNode; -} - -export interface IAudioContext extends IBaseAudioContext { - close(): Promise; - resume(): Promise; - suspend(): Promise; -} - -export interface IOfflineAudioContext extends IBaseAudioContext { - resume(): Promise; - suspend(suspendTime: number): Promise; - startRendering(): Promise; -} - -export interface IAudioNode { - readonly context: BaseAudioContext; - readonly numberOfInputs: number; - readonly numberOfOutputs: number; - readonly channelCount: number; - readonly channelCountMode: ChannelCountMode; - readonly channelInterpretation: ChannelInterpretation; - - connect: (destination: IAudioNode | IAudioParam) => void; - disconnect: (destination?: IAudioNode | IAudioParam) => void; -} - -export interface IGainNode extends IAudioNode { - readonly gain: IAudioParam; -} - -export interface IStereoPannerNode extends IAudioNode { - readonly pan: IAudioParam; -} - -export interface IBiquadFilterNode extends IAudioNode { - readonly frequency: AudioParam; - readonly detune: AudioParam; - readonly Q: AudioParam; - readonly gain: AudioParam; - type: BiquadFilterType; - - getFrequencyResponse( - frequencyArray: Float32Array, - magResponseOutput: Float32Array, - phaseResponseOutput: Float32Array - ): void; -} - -export interface IAudioDestinationNode extends IAudioNode {} - -export interface IAudioScheduledSourceNode extends IAudioNode { - start(when: number): void; - stop: (when: number) => void; - - // passing subscriptionId(uint_64 in cpp, string in js) to the cpp - onEnded: string; -} - -export interface IAudioBufferBaseSourceNode extends IAudioScheduledSourceNode { - detune: IAudioParam; - playbackRate: IAudioParam; - - // passing subscriptionId(uint_64 in cpp, string in js) to the cpp - onPositionChanged: string; - // set how often the onPositionChanged event is called - onPositionChangedInterval: number; -} - -export interface IOscillatorNode extends IAudioScheduledSourceNode { - readonly frequency: IAudioParam; - readonly detune: IAudioParam; - type: OscillatorType; - - setPeriodicWave(periodicWave: IPeriodicWave): void; -} - -export interface IStreamerNode extends IAudioNode { - initialize(streamPath: string): boolean; -} - -export interface IConstantSourceNode extends IAudioScheduledSourceNode { - readonly offset: IAudioParam; -} - -export interface IAudioBufferSourceNode extends IAudioBufferBaseSourceNode { - buffer: IAudioBuffer | null; - loop: boolean; - loopSkip: boolean; - loopStart: number; - loopEnd: number; - - start: (when?: number, offset?: number, duration?: number) => void; - setBuffer: (audioBuffer: IAudioBuffer | null) => void; - - // passing subscriptionId(uint_64 in cpp, string in js) to the cpp - onLoopEnded: string; -} - -export interface IAudioBufferQueueSourceNode - extends IAudioBufferBaseSourceNode { - dequeueBuffer: (bufferId: number) => void; - clearBuffers: () => void; - - // returns bufferId - enqueueBuffer: (audioBuffer: IAudioBuffer) => string; - pause: () => void; -} - -export interface IAudioBuffer { - readonly length: number; - readonly duration: number; - readonly sampleRate: number; - readonly numberOfChannels: number; - - getChannelData(channel: number): Float32Array; - copyFromChannel( - destination: Float32Array, - channelNumber: number, - startInChannel: number - ): void; - copyToChannel( - source: Float32Array, - channelNumber: number, - startInChannel: number - ): void; -} - -export interface IAudioParam { - value: number; - defaultValue: number; - minValue: number; - maxValue: number; - - setValueAtTime: (value: number, startTime: number) => void; - linearRampToValueAtTime: (value: number, endTime: number) => void; - exponentialRampToValueAtTime: (value: number, endTime: number) => void; - setTargetAtTime: ( - target: number, - startTime: number, - timeConstant: number - ) => void; - setValueCurveAtTime: ( - values: Float32Array, - startTime: number, - duration: number - ) => void; - cancelScheduledValues: (cancelTime: number) => void; - cancelAndHoldAtTime: (cancelTime: number) => void; -} - -export interface IPeriodicWave {} - -export interface IAnalyserNode extends IAudioNode { - fftSize: number; - readonly frequencyBinCount: number; - minDecibels: number; - maxDecibels: number; - smoothingTimeConstant: number; - window: WindowType; - - getFloatFrequencyData: (array: Float32Array) => void; - getByteFrequencyData: (array: Uint8Array) => void; - getFloatTimeDomainData: (array: Float32Array) => void; - getByteTimeDomainData: (array: Uint8Array) => void; -} - -export interface IRecorderAdapterNode extends IAudioNode {} - -export interface IWorkletNode extends IAudioNode {} - -export interface IWorkletSourceNode extends IAudioScheduledSourceNode {} - -export interface IWorkletProcessingNode extends IAudioNode {} - -export interface IAudioRecorder { - start: () => void; - stop: () => void; - connect: (node: IRecorderAdapterNode) => void; - disconnect: () => void; - - // passing subscriptionId(uint_64 in cpp, string in js) to the cpp - onAudioReady: string; -} - -export interface IAudioDecoder { - decodeWithMemoryBlock: ( - arrayBuffer: ArrayBuffer, - sampleRate?: number - ) => Promise; - decodeWithFilePath: ( - sourcePath: string, - sampleRate?: number - ) => Promise; - decodeWithPCMInBase64: ( - b64: string, - inputSampleRate: number, - inputChannelCount: number, - interleaved?: boolean - ) => Promise; -} - -export interface IAudioStretcher { - changePlaybackSpeed: ( - arrayBuffer: AudioBuffer, - playbackSpeed: number - ) => Promise; -} - -export interface IAudioEventEmitter { - addAudioEventListener( - name: Name, - callback: AudioEventCallback - ): string; - removeAudioEventListener( - name: Name, - subscriptionId: string - ): void; -} diff --git a/packages/react-native-audio-api/src/system/AudioManager.native.ts b/packages/react-native-audio-api/src/system/AudioManager.native.ts new file mode 100644 index 000000000..b8cf4cfa4 --- /dev/null +++ b/packages/react-native-audio-api/src/system/AudioManager.native.ts @@ -0,0 +1,109 @@ +import { AudioEventEmitter } from '../events'; +import { NativeAudioAPIModule } from '../specs'; + +import type { + AudioEventSubscription, + RemoteCommandEventName, + SystemEventCallback, + SystemEventName, +} from '../events'; +import IAudioManager from './interface'; +import type { + AudioDevicesInfo, + LockScreenInfo, + PermissionStatus, + SessionOptions, +} from './types'; + +if (global.AudioEventEmitter == null) { + if (!NativeAudioAPIModule) { + throw new Error( + `Failed to install react-native-audio-api: The native module could not be found.` + ); + } + + NativeAudioAPIModule.install(); +} + +class AudioManager implements IAudioManager { + private readonly audioEventEmitter: AudioEventEmitter; + + constructor() { + this.audioEventEmitter = new AudioEventEmitter(global.AudioEventEmitter); + } + + getDevicePreferredSampleRate(): number { + return NativeAudioAPIModule!.getDevicePreferredSampleRate(); + } + + setAudioSessionActivity(enabled: boolean): Promise { + return NativeAudioAPIModule!.setAudioSessionActivity(enabled); + } + + setAudioSessionOptions(options: SessionOptions) { + NativeAudioAPIModule!.setAudioSessionOptions( + options.iosCategory ?? '', + options.iosMode ?? '', + options.iosOptions ?? [], + options.iosAllowHaptics ?? false + ); + } + + setLockScreenInfo(info: LockScreenInfo) { + NativeAudioAPIModule!.setLockScreenInfo(info); + } + + resetLockScreenInfo() { + NativeAudioAPIModule!.resetLockScreenInfo(); + } + + observeAudioInterruptions(enabled: boolean) { + NativeAudioAPIModule!.observeAudioInterruptions(enabled); + } + + /** + * @param enabled - Whether to actively reclaim the session or not + * @experimental more aggressively try to reactivate the audio session during interruptions. + * It is subject to change in the future and might be removed. + * + * In some cases (depends on app session settings and other apps using audio) system may never + * send the `interruption ended` event. This method will check if any other audio is playing + * and try to reactivate the audio session, as soon as there is "silence". + * Although this might change the expected behavior. + * + * Internally method uses `AVAudioSessionSilenceSecondaryAudioHintNotification` as well as + * interval polling to check if other audio is playing. + */ + activelyReclaimSession(enabled: boolean) { + NativeAudioAPIModule!.activelyReclaimSession(enabled); + } + + observeVolumeChanges(enabled: boolean) { + NativeAudioAPIModule!.observeVolumeChanges(enabled); + } + + enableRemoteCommand(name: RemoteCommandEventName, enabled: boolean) { + NativeAudioAPIModule!.enableRemoteCommand(name, enabled); + } + + addSystemEventListener( + name: Name, + callback: SystemEventCallback + ): AudioEventSubscription { + return this.audioEventEmitter.addAudioEventListener(name, callback); + } + + async requestRecordingPermissions(): Promise { + return NativeAudioAPIModule!.requestRecordingPermissions(); + } + + async checkRecordingPermissions(): Promise { + return NativeAudioAPIModule!.checkRecordingPermissions(); + } + + async getDevicesInfo(): Promise { + return NativeAudioAPIModule!.getDevicesInfo(); + } +} + +export default new AudioManager(); diff --git a/packages/react-native-audio-api/src/system/AudioManager.ts b/packages/react-native-audio-api/src/system/AudioManager.ts index 70a116ee4..cc778ef99 100644 --- a/packages/react-native-audio-api/src/system/AudioManager.ts +++ b/packages/react-native-audio-api/src/system/AudioManager.ts @@ -1,104 +1,91 @@ -import { - SessionOptions, +import type { + AudioEventSubscription, + RemoteCommandEventName, + SystemEventCallback, + SystemEventName, +} from '../events'; +import { availabilityWarn } from '../utils'; +import IAudioManager from './interface'; +import type { + AudioDevicesInfo, LockScreenInfo, PermissionStatus, - AudioDevicesInfo, + SessionOptions, } from './types'; -import { - SystemEventName, - SystemEventCallback, - RemoteCommandEventName, -} from '../events/types'; -import { NativeAudioAPIModule } from '../specs'; -import { AudioEventEmitter, AudioEventSubscription } from '../events'; -if (global.AudioEventEmitter == null) { - if (!NativeAudioAPIModule) { - throw new Error( - `Failed to install react-native-audio-api: The native module could not be found.` - ); +class NoopSubscription { + remove(): void { + // noop } - - NativeAudioAPIModule.install(); } -class AudioManager { - private readonly audioEventEmitter: AudioEventEmitter; - constructor() { - this.audioEventEmitter = new AudioEventEmitter(global.AudioEventEmitter); - } - +class AudioManager implements IAudioManager { getDevicePreferredSampleRate(): number { - return NativeAudioAPIModule!.getDevicePreferredSampleRate(); + availabilityWarn('AudioManager.getDevicePreferredSampleRate', 'web'); + return 0; } - setAudioSessionActivity(enabled: boolean): Promise { - return NativeAudioAPIModule!.setAudioSessionActivity(enabled); + setAudioSessionActivity(_enabled: boolean): Promise { + availabilityWarn('AudioManager.setAudioSessionActivity', 'web'); + return Promise.resolve(false); } - setAudioSessionOptions(options: SessionOptions) { - NativeAudioAPIModule!.setAudioSessionOptions( - options.iosCategory ?? '', - options.iosMode ?? '', - options.iosOptions ?? [], - options.iosAllowHaptics ?? false - ); + setAudioSessionOptions(_options: SessionOptions) { + availabilityWarn('AudioManager.setAudioSessionOptions', 'web'); } - setLockScreenInfo(info: LockScreenInfo) { - NativeAudioAPIModule!.setLockScreenInfo(info); + setLockScreenInfo(_info: LockScreenInfo) { + availabilityWarn('AudioManager.setLockScreenInfo', 'web'); } resetLockScreenInfo() { - NativeAudioAPIModule!.resetLockScreenInfo(); + availabilityWarn('AudioManager.resetLockScreenInfo', 'web'); } - observeAudioInterruptions(enabled: boolean) { - NativeAudioAPIModule!.observeAudioInterruptions(enabled); + observeAudioInterruptions(_enabled: boolean) { + availabilityWarn('AudioManager.observeAudioInterruptions', 'web'); } - /** - * @param enabled - Whether to actively reclaim the session or not - * @experimental more aggressively try to reactivate the audio session during interruptions. - * It is subject to change in the future and might be removed. - * - * In some cases (depends on app session settings and other apps using audio) system may never - * send the `interruption ended` event. This method will check if any other audio is playing - * and try to reactivate the audio session, as soon as there is "silence". - * Although this might change the expected behavior. - * - * Internally method uses `AVAudioSessionSilenceSecondaryAudioHintNotification` as well as - * interval polling to check if other audio is playing. - */ - activelyReclaimSession(enabled: boolean) { - NativeAudioAPIModule!.activelyReclaimSession(enabled); + activelyReclaimSession(_enabled: boolean): void { + availabilityWarn('AudioManager.activelyReclaimSession', 'web'); } - observeVolumeChanges(enabled: boolean) { - NativeAudioAPIModule!.observeVolumeChanges(enabled); + observeVolumeChanges(_enabled: boolean): void { + availabilityWarn('AudioManager.observeVolumeChanges', 'web'); } - enableRemoteCommand(name: RemoteCommandEventName, enabled: boolean) { - NativeAudioAPIModule!.enableRemoteCommand(name, enabled); + enableRemoteCommand(_name: RemoteCommandEventName, _enabled: boolean): void { + availabilityWarn('AudioManager.enableRemoteCommand', 'web'); } addSystemEventListener( - name: Name, - callback: SystemEventCallback + _name: Name, + _callback: SystemEventCallback ): AudioEventSubscription { - return this.audioEventEmitter.addAudioEventListener(name, callback); + availabilityWarn('AudioManager.addSystemEventListener', 'web'); + return new NoopSubscription() as unknown as AudioEventSubscription; } async requestRecordingPermissions(): Promise { - return NativeAudioAPIModule!.requestRecordingPermissions(); + // TODO: could be implemented some day, some way + availabilityWarn('AudioManager.requestRecordingPermissions', 'web'); + return Promise.resolve('Denied'); } async checkRecordingPermissions(): Promise { - return NativeAudioAPIModule!.checkRecordingPermissions(); + // TODO: could be implemented some day, some way + availabilityWarn('AudioManager.checkRecordingPermissions', 'web'); + return Promise.resolve('Denied'); } async getDevicesInfo(): Promise { - return NativeAudioAPIModule!.getDevicesInfo(); + availabilityWarn('AudioManager.getDevicesInfo', 'web'); + return Promise.resolve({ + availableInputs: [], + availableOutputs: [], + currentInputs: [], + currentOutputs: [], + }); } } diff --git a/packages/react-native-audio-api/src/system/interface.ts b/packages/react-native-audio-api/src/system/interface.ts new file mode 100644 index 000000000..bef8a2b75 --- /dev/null +++ b/packages/react-native-audio-api/src/system/interface.ts @@ -0,0 +1,34 @@ +import type { + AudioEventSubscription, + RemoteCommandEventName, + SystemEventCallback, + SystemEventName, +} from '../events'; + +import type { + AudioDevicesInfo, + LockScreenInfo, + PermissionStatus, + SessionOptions, +} from './types'; + +export default interface IAudioManager { + getDevicePreferredSampleRate(): number; + setAudioSessionActivity(enabled: boolean): Promise; + setAudioSessionOptions(options: SessionOptions): void; // TODO: should return Promise ???? + setLockScreenInfo(info: LockScreenInfo): void; + resetLockScreenInfo(): void; + observeAudioInterruptions(enabled: boolean): void; + activelyReclaimSession(enabled: boolean): void; + observeVolumeChanges(enabled: boolean): void; + enableRemoteCommand(name: RemoteCommandEventName, enabled: boolean): void; + + addSystemEventListener( + name: Name, + callback: SystemEventCallback + ): AudioEventSubscription; + + requestRecordingPermissions(): Promise; + checkRecordingPermissions(): Promise; + getDevicesInfo(): Promise; +} diff --git a/packages/react-native-audio-api/src/types/generics/IGenericAudioBuffer.ts b/packages/react-native-audio-api/src/types/generics/IGenericAudioBuffer.ts new file mode 100644 index 000000000..620bc9653 --- /dev/null +++ b/packages/react-native-audio-api/src/types/generics/IGenericAudioBuffer.ts @@ -0,0 +1,18 @@ +export default interface IGenericAudioBuffer { + readonly length: number; + readonly duration: number; + readonly sampleRate: number; + readonly numberOfChannels: number; + + getChannelData(channel: number): Float32Array; + copyFromChannel( + destination: Float32Array, + channelNumber: number, + startInChannel: number + ): void; + copyToChannel( + source: Float32Array, + channelNumber: number, + startInChannel: number + ): void; +} diff --git a/packages/react-native-audio-api/src/types/generics/IGenericAudioDestinationNode.ts b/packages/react-native-audio-api/src/types/generics/IGenericAudioDestinationNode.ts new file mode 100644 index 000000000..e45b377ba --- /dev/null +++ b/packages/react-native-audio-api/src/types/generics/IGenericAudioDestinationNode.ts @@ -0,0 +1,9 @@ +import type IGenericAudioNode from './IGenericAudioNode'; +import type IGenericBaseAudioContext from './IGenericBaseAudioContext'; + +export default interface IAudioDestinationNode< + TContext extends IGenericBaseAudioContext, +> extends IGenericAudioNode { + // TODO: implement on native side + // readonly maxChannelCount: number; +} diff --git a/packages/react-native-audio-api/src/types/generics/IGenericAudioNode.ts b/packages/react-native-audio-api/src/types/generics/IGenericAudioNode.ts new file mode 100644 index 000000000..c0eb87cc0 --- /dev/null +++ b/packages/react-native-audio-api/src/types/generics/IGenericAudioNode.ts @@ -0,0 +1,27 @@ +import type { ChannelCountMode, ChannelInterpretation } from '../../types'; +import type IGenericAudioParam from './IGenericAudioParam'; +import type IGenericBaseAudioContext from './IGenericBaseAudioContext'; + +export default interface IGenericAudioNode< + TContext extends IGenericBaseAudioContext, +> { + readonly context: TContext; + readonly numberOfInputs: number; + readonly numberOfOutputs: number; + readonly channelCount: number; + readonly channelCountMode: ChannelCountMode; + readonly channelInterpretation: ChannelInterpretation; + + connect>( + destination: D + ): D; + connect(destination: IGenericAudioParam): void; + + disconnect< + C extends IGenericBaseAudioContext, + D extends IGenericAudioNode, + >( + destination?: D + ): void; + disconnect(destination?: IGenericAudioParam): void; +} diff --git a/packages/react-native-audio-api/src/types/generics/IGenericAudioParam.ts b/packages/react-native-audio-api/src/types/generics/IGenericAudioParam.ts new file mode 100644 index 000000000..0c4b5187b --- /dev/null +++ b/packages/react-native-audio-api/src/types/generics/IGenericAudioParam.ts @@ -0,0 +1,36 @@ +import IGenericBaseAudioContext from './IGenericBaseAudioContext'; + +export default interface IGenericAudioParam< + TContext extends IGenericBaseAudioContext, +> { + readonly defaultValue: number; + readonly minValue: number; + readonly maxValue: number; + + value: number; + + cancelAndHoldAtTime: (cancelTime: number) => IGenericAudioParam; + cancelScheduledValues: (cancelTime: number) => IGenericAudioParam; + exponentialRampToValueAtTime: ( + value: number, + endTime: number + ) => IGenericAudioParam; + linearRampToValueAtTime: ( + value: number, + endTime: number + ) => IGenericAudioParam; + setTargetAtTime: ( + target: number, + startTime: number, + timeConstant: number + ) => IGenericAudioParam; + setValueAtTime: ( + value: number, + startTime: number + ) => IGenericAudioParam; + setValueCurveAtTime: ( + values: Float32Array, + startTime: number, + duration: number + ) => IGenericAudioParam; +} diff --git a/packages/react-native-audio-api/src/types/generics/IGenericAudioRecorder.ts b/packages/react-native-audio-api/src/types/generics/IGenericAudioRecorder.ts new file mode 100644 index 000000000..49f4ca392 --- /dev/null +++ b/packages/react-native-audio-api/src/types/generics/IGenericAudioRecorder.ts @@ -0,0 +1,14 @@ +import type IGenericBaseAudioContext from './IGenericBaseAudioContext'; +import type IGenericRecorderAdapterNode from './IGenericRecorderAdapterNode'; + +export default interface IGenericAudioRecorder< + TContext extends IGenericBaseAudioContext, +> { + start: () => void; + stop: () => void; + // TODO: fix this TS error + connect: (node: IGenericRecorderAdapterNode) => void; + disconnect: () => void; + + // TODO: onAudioReady +} diff --git a/packages/react-native-audio-api/src/types/generics/IGenericBaseAudioContext.ts b/packages/react-native-audio-api/src/types/generics/IGenericBaseAudioContext.ts new file mode 100644 index 000000000..54e862ce1 --- /dev/null +++ b/packages/react-native-audio-api/src/types/generics/IGenericBaseAudioContext.ts @@ -0,0 +1,2 @@ +// TODO: type-on or remove +export default interface IGenericBaseAudioContext {} diff --git a/packages/react-native-audio-api/src/types/generics/IGenericBiquadFilterNode.ts b/packages/react-native-audio-api/src/types/generics/IGenericBiquadFilterNode.ts new file mode 100644 index 000000000..1d21400aa --- /dev/null +++ b/packages/react-native-audio-api/src/types/generics/IGenericBiquadFilterNode.ts @@ -0,0 +1,20 @@ +import type { BiquadFilterType } from '../properties'; +import type IGenericAudioNode from './IGenericAudioNode'; +import type IGenericAudioParam from './IGenericAudioParam'; +import type IGenericBaseAudioContext from './IGenericBaseAudioContext'; + +export default interface IGenericBiquadFilterNode< + TContext extends IGenericBaseAudioContext, +> extends IGenericAudioNode { + readonly frequency: IGenericAudioParam; + readonly detune: IGenericAudioParam; + readonly Q: IGenericAudioParam; + readonly gain: IGenericAudioParam; + type: BiquadFilterType; + + getFrequencyResponse( + frequencyArray: Float32Array, + magResponseOutput: Float32Array, + phaseResponseOutput: Float32Array + ): void; +} diff --git a/packages/react-native-audio-api/src/types/generics/IGenericGainNode.ts b/packages/react-native-audio-api/src/types/generics/IGenericGainNode.ts new file mode 100644 index 000000000..cccf138f6 --- /dev/null +++ b/packages/react-native-audio-api/src/types/generics/IGenericGainNode.ts @@ -0,0 +1,9 @@ +import type IGenericAudioNode from './IGenericAudioNode'; +import type IGenericAudioParam from './IGenericAudioParam'; +import type IGenericBaseAudioContext from './IGenericBaseAudioContext'; + +export default interface IGenericGainNode< + TContext extends IGenericBaseAudioContext, +> extends IGenericAudioNode { + readonly gain: IGenericAudioParam; +} diff --git a/packages/react-native-audio-api/src/types/generics/IGenericOscillatorNode.ts b/packages/react-native-audio-api/src/types/generics/IGenericOscillatorNode.ts new file mode 100644 index 000000000..fda91f114 --- /dev/null +++ b/packages/react-native-audio-api/src/types/generics/IGenericOscillatorNode.ts @@ -0,0 +1,24 @@ +import { OscillatorType } from '../../types/properties'; +import type IGenericAudioNode from './IGenericAudioNode'; +import type IGenericAudioParam from './IGenericAudioParam'; +import type IGenericBaseAudioContext from './IGenericBaseAudioContext'; +import type IGenericPeriodicWave from './IGenericPeriodicWave'; + +/** + * TODO: TBD/FIXME - OscillatorNode is implemented as generic interface, but in + * reality it inherits from AudioScheduledSourceNode which can't be a generic + * interface as implementation differs between platforms (onEnded vs onended). + * This means we will have to possibly remember to provide correct types for + * onEnded method? Which possibly could be written better in the future. For + * now, lets leave it as it is, its just an oscillator lol and writing this + * comment is already too much time spent here. :cheers: + */ +export default interface IGenericOscillatorNode< + TContext extends IGenericBaseAudioContext, +> extends IGenericAudioNode { + readonly frequency: IGenericAudioParam; + readonly detune: IGenericAudioParam; + type: OscillatorType; + + setPeriodicWave(periodicWave: IGenericPeriodicWave): void; +} diff --git a/packages/react-native-audio-api/src/types/generics/IGenericPeriodicWave.ts b/packages/react-native-audio-api/src/types/generics/IGenericPeriodicWave.ts new file mode 100644 index 000000000..aeb77326d --- /dev/null +++ b/packages/react-native-audio-api/src/types/generics/IGenericPeriodicWave.ts @@ -0,0 +1,10 @@ +import IGenericBaseAudioContext from './IGenericBaseAudioContext'; + +/** + * TContext is needed to ensure that we won't try to mix objects from different + * contexts (especially different layers of contexts) + */ +export default interface IGenericPeriodicWave< + // eslint-disable-next-line @typescript-eslint/no-unused-vars + TContext extends IGenericBaseAudioContext, +> {} diff --git a/packages/react-native-audio-api/src/types/generics/IGenericRecorderAdapterNode.ts b/packages/react-native-audio-api/src/types/generics/IGenericRecorderAdapterNode.ts new file mode 100644 index 000000000..404d93a3e --- /dev/null +++ b/packages/react-native-audio-api/src/types/generics/IGenericRecorderAdapterNode.ts @@ -0,0 +1,6 @@ +import IGenericAudioNode from './IGenericAudioNode'; +import IGenericBaseAudioContext from './IGenericBaseAudioContext'; + +export default interface IGenericRecorderAdapterNode< + TContext extends IGenericBaseAudioContext, +> extends IGenericAudioNode {} diff --git a/packages/react-native-audio-api/src/types/generics/IGenericStereoPannerNode.ts b/packages/react-native-audio-api/src/types/generics/IGenericStereoPannerNode.ts new file mode 100644 index 000000000..75d58caa8 --- /dev/null +++ b/packages/react-native-audio-api/src/types/generics/IGenericStereoPannerNode.ts @@ -0,0 +1,9 @@ +import type IGenericAudioNode from './IGenericAudioNode'; +import type IGenericAudioParam from './IGenericAudioParam'; +import type IGenericBaseAudioContext from './IGenericBaseAudioContext'; + +export default interface IGenericStereoPannerNode< + TContext extends IGenericBaseAudioContext, +> extends IGenericAudioNode { + readonly pan: IGenericAudioParam; +} diff --git a/packages/react-native-audio-api/src/types/generics/index.ts b/packages/react-native-audio-api/src/types/generics/index.ts new file mode 100644 index 000000000..943806576 --- /dev/null +++ b/packages/react-native-audio-api/src/types/generics/index.ts @@ -0,0 +1,26 @@ +/** + * This file/directory contains interfaces for nodes and other types that can be + * used for providing types for each platform and our classes, without further + * modifications. + * + * Same interface/type can be used as: + * + * - A type for the native node/host object + * - A type for the web node abstraction (note: Web Audio API is fully typed, but + * this way we can operate on Generic type that isn't really connected with + * given implementation) + * - A type for our user-facing classes + */ + +export type { default as IGenericAudioBuffer } from './IGenericAudioBuffer'; +export type { default as IGenericAudioDestinationNode } from './IGenericAudioDestinationNode'; +export type { default as IGenericAudioNode } from './IGenericAudioNode'; +export type { default as IGenericAudioParam } from './IGenericAudioParam'; +export type { default as IGenericAudioRecorder } from './IGenericAudioRecorder'; +export type { default as IGenericBaseAudioContext } from './IGenericBaseAudioContext'; +export type { default as IGenericBiquadFilterNode } from './IGenericBiquadFilterNode'; +export type { default as IGenericGainNode } from './IGenericGainNode'; +export type { default as IGenericOscillatorNode } from './IGenericOscillatorNode'; +export type { default as IGenericPeriodicWave } from './IGenericPeriodicWave'; +export type { default as IGenericRecorderAdapterNode } from './IGenericRecorderAdapterNode'; +export type { default as IGenericStereoPannerNode } from './IGenericStereoPannerNode'; diff --git a/packages/react-native-audio-api/src/types/index.ts b/packages/react-native-audio-api/src/types/index.ts new file mode 100644 index 000000000..a421a6c8e --- /dev/null +++ b/packages/react-native-audio-api/src/types/index.ts @@ -0,0 +1 @@ +export * from './properties'; diff --git a/packages/react-native-audio-api/src/types/interfaces/IAnalyserNode.ts b/packages/react-native-audio-api/src/types/interfaces/IAnalyserNode.ts new file mode 100644 index 000000000..2fa1375b0 --- /dev/null +++ b/packages/react-native-audio-api/src/types/interfaces/IAnalyserNode.ts @@ -0,0 +1,19 @@ +import type { IGenericAudioNode, IGenericBaseAudioContext } from '../generics'; +import type { WindowType } from '../properties'; + +export default interface IAnalyserNode< + TContext extends IGenericBaseAudioContext, +> extends IGenericAudioNode { + fftSize: number; + readonly frequencyBinCount: number; + minDecibels: number; + maxDecibels: number; + smoothingTimeConstant: number; + window: WindowType; + + getByteFrequencyData: (array: Uint8Array) => void; + getByteTimeDomainData: (array: Uint8Array) => void; + + getFloatFrequencyData: (array: Float32Array) => void; + getFloatTimeDomainData: (array: Float32Array) => void; +} diff --git a/packages/react-native-audio-api/src/types/interfaces/IAudioBufferBaseSourceNode.ts b/packages/react-native-audio-api/src/types/interfaces/IAudioBufferBaseSourceNode.ts new file mode 100644 index 000000000..f28f4532a --- /dev/null +++ b/packages/react-native-audio-api/src/types/interfaces/IAudioBufferBaseSourceNode.ts @@ -0,0 +1,17 @@ +import type { EventTypeWithValue } from '../../events'; +import type { IGenericAudioParam, IGenericBaseAudioContext } from '../generics'; +import type IAudioScheduledSourceNode from './IAudioScheduledSourceNode'; + +export type OnPositionChangedEventCallback = ( + event: EventTypeWithValue +) => void; + +export default interface IAudioBufferBaseSourceNode< + TContext extends IGenericBaseAudioContext, +> extends IAudioScheduledSourceNode { + readonly playbackRate: IGenericAudioParam; + readonly detune: IGenericAudioParam; + + onPositionChanged: OnPositionChangedEventCallback | undefined; + onPositionChangedInterval: number; +} diff --git a/packages/react-native-audio-api/src/types/interfaces/IAudioBufferQueueSourceNode.ts b/packages/react-native-audio-api/src/types/interfaces/IAudioBufferQueueSourceNode.ts new file mode 100644 index 000000000..a16aa3ad5 --- /dev/null +++ b/packages/react-native-audio-api/src/types/interfaces/IAudioBufferQueueSourceNode.ts @@ -0,0 +1,13 @@ +import { IGenericAudioBuffer, IGenericBaseAudioContext } from '../generics'; +import IAudioBufferBaseSourceNode from './IAudioBufferBaseSourceNode'; + +export default interface IAudioBufferQueueSourceNode< + TContext extends IGenericBaseAudioContext, + TAudioBuffer extends IGenericAudioBuffer = IGenericAudioBuffer, +> extends IAudioBufferBaseSourceNode { + dequeueBuffer: (bufferId: string) => void; + clearBuffers: () => void; + + enqueueBuffer: (audioBuffer: TAudioBuffer) => string; + pause: () => void; +} diff --git a/packages/react-native-audio-api/src/types/interfaces/IAudioBufferSourceNode.ts b/packages/react-native-audio-api/src/types/interfaces/IAudioBufferSourceNode.ts new file mode 100644 index 000000000..5858faa7a --- /dev/null +++ b/packages/react-native-audio-api/src/types/interfaces/IAudioBufferSourceNode.ts @@ -0,0 +1,20 @@ +import type { EventEmptyType } from '../../events'; +import type { IGenericBaseAudioContext } from '../generics'; +import type IAudioBufferBaseSourceNode from './IAudioBufferBaseSourceNode'; + +export type LoopEndedEvent = EventEmptyType; +export type LoopEndedEventCallback = (event: LoopEndedEvent) => void; + +export default interface IAudioBufferSourceNode< + TContext extends IGenericBaseAudioContext, +> extends IAudioBufferBaseSourceNode { + buffer: AudioBuffer | null; + loopSkip: boolean; + loop: boolean; + loopStart: number; + loopEnd: number; + + start(when?: number, offset?: number, duration?: number): void; + + onLoopEnded: LoopEndedEventCallback | undefined; +} diff --git a/packages/react-native-audio-api/src/types/interfaces/IAudioScheduledSourceNode.ts b/packages/react-native-audio-api/src/types/interfaces/IAudioScheduledSourceNode.ts new file mode 100644 index 000000000..b105bc4bb --- /dev/null +++ b/packages/react-native-audio-api/src/types/interfaces/IAudioScheduledSourceNode.ts @@ -0,0 +1,13 @@ +import type { OnEndedEventType } from '../../events/types'; +import type { IGenericAudioNode, IGenericBaseAudioContext } from '../generics'; + +export type OnEndedEventCallback = (event: OnEndedEventType) => void; + +export default interface IAudioScheduledSourceNode< + TContext extends IGenericBaseAudioContext, +> extends IGenericAudioNode { + start(when?: number): void; + stop(when?: number): void; + + onEnded: OnEndedEventCallback | undefined; +} diff --git a/packages/react-native-audio-api/src/types/interfaces/IConstantSourceNode.ts b/packages/react-native-audio-api/src/types/interfaces/IConstantSourceNode.ts new file mode 100644 index 000000000..88e91447b --- /dev/null +++ b/packages/react-native-audio-api/src/types/interfaces/IConstantSourceNode.ts @@ -0,0 +1,8 @@ +import { IGenericAudioParam, IGenericBaseAudioContext } from '../generics'; +import IAudioScheduledSourceNode from './IAudioScheduledSourceNode'; + +export default interface IConstantSourceNode< + TContext extends IGenericBaseAudioContext, +> extends IAudioScheduledSourceNode { + readonly offset: IGenericAudioParam; +} diff --git a/packages/react-native-audio-api/src/types/interfaces/index.ts b/packages/react-native-audio-api/src/types/interfaces/index.ts new file mode 100644 index 000000000..129bde21f --- /dev/null +++ b/packages/react-native-audio-api/src/types/interfaces/index.ts @@ -0,0 +1,22 @@ +/** + * This file/directory contains interfaces that should be used for defining + * types and user facing implementation in classes/functions/etc. Exported by + * this library. + * + * This means that underlying native and web implementations have differences. + */ +export type { default as IAnalyserNode } from './IAnalyserNode'; +export type { + default as IAudioBufferBaseSourceNode, + OnPositionChangedEventCallback, +} from './IAudioBufferBaseSourceNode'; +export type { default as IAudioBufferQueueSourceNode } from './IAudioBufferQueueSourceNode'; +export type { + default as IAudioBufferSourceNode, + LoopEndedEvent, + LoopEndedEventCallback, +} from './IAudioBufferSourceNode'; +export type { + default as IAudioScheduledSourceNode, + OnEndedEventCallback, +} from './IAudioScheduledSourceNode'; diff --git a/packages/react-native-audio-api/src/types/playground2.ts b/packages/react-native-audio-api/src/types/playground2.ts new file mode 100644 index 000000000..8e25d1047 --- /dev/null +++ b/packages/react-native-audio-api/src/types/playground2.ts @@ -0,0 +1,167 @@ +interface BaseHalulu { + name: string; + age: number; + sayHello: () => string; + play: (toy: string) => string; + eat: (food: string) => void; + playWith: (friend: T) => string; + makeFriend: (friend: T) => string; + getFriends: () => T[]; + birthday: () => void; + getAgeInDogYears: () => number; + setName: (name: string) => void; + setAge: (age: number) => void; +} + +interface MobileHaluluInterface extends BaseHalulu { + isCute: boolean; + sleep: () => void; + setIsCute: (isCute: boolean) => void; +} + +interface WebHaluluInterface extends BaseHalulu { + fetchFromServer: (url: string) => Promise; + saveToServer: (url: string) => Promise; +} + +interface IHalulu extends BaseHalulu> { + isCute: boolean; + sleep: () => void; + setIsCute: (isCute: boolean) => void; + fetchFromServer: (url: string) => Promise; + saveToServer: (url: string) => Promise; +} + +abstract class HaluluCommon> + implements IHalulu> +{ + protected internal: IH; + protected friends: HaluluCommon[] = []; + + constructor(internal: IH) { + this.internal = internal; + } + + public get name(): string { + return this.internal.name; + } + + public get age(): number { + return this.internal.age; + } + + public abstract get isCute(): boolean; + + public abstract setIsCute(isCute: boolean): void; + + public abstract sleep(): void; + + public abstract fetchFromServer(url: string): Promise; + + public abstract saveToServer(url: string): Promise; + + public sayHello(): string { + return this.internal.sayHello(); + } + + public play(toy: string): string { + return this.internal.play(toy); + } + + public eat(food: string): void { + this.internal.eat(food); + } + + public playWith(friend: IHalulu>): string { + const self = this as HaluluCommon; + const other = friend as unknown as HaluluCommon; + + if (self === other) { + return `${this.name} cannot play with itself!`; + } + + if (!this.friends.includes(other)) { + return `${this.name} is not friends with ${other.name}`; + } + + return this.internal.playWith(other.internal); + } + + public makeFriend(friend: HaluluCommon) { + this.friends.push(friend); + return this.internal.makeFriend(friend.internal); + } + + public getFriends(): HaluluCommon[] { + return this.friends; + } + + public birthday(): void { + this.internal.birthday(); + } + + public getAgeInDogYears(): number { + return this.internal.getAgeInDogYears(); + } + + public setName(name: string): void { + this.internal.setName(name); + } + + public setAge(age: number): void { + this.internal.setAge(age); + } +} + +export class HaluluWeb + extends HaluluCommon + implements IHalulu +{ + setIsCute(_isCute: boolean): void { + console.warn('isCute is not available on web'); + } + + public get isCute(): boolean { + console.warn('isCute is not available on web'); + return false; + } + + sleep(): void { + console.warn('HaluluWeb cannot sleep'); + } + + fetchFromServer(url: string): Promise { + return this.internal.fetchFromServer(url); + } + + saveToServer(url: string): Promise { + return this.internal.saveToServer(url); + } +} + +export class HaluluMobile + extends HaluluCommon + implements IHalulu +{ + public get isCute(): boolean { + return this.internal.isCute; + } + + setIsCute(isCute: boolean): void { + this.internal.setIsCute(isCute); + } + + sleep(): void { + this.internal.sleep(); + } + + fetchFromServer(_url: string): Promise { + return Promise.reject( + new Error('fetchFromServer is not available on mobile') + ); + } + + saveToServer(_url: string): Promise { + return Promise.reject(new Error('saveToServer is not available on mobile')); + } +} diff --git a/packages/react-native-audio-api/src/types/properties.ts b/packages/react-native-audio-api/src/types/properties.ts new file mode 100644 index 000000000..09cce1a77 --- /dev/null +++ b/packages/react-native-audio-api/src/types/properties.ts @@ -0,0 +1,21 @@ +export type ChannelCountMode = 'max' | 'clamped-max' | 'explicit'; +export type ChannelInterpretation = 'speakers' | 'discrete'; + +export type BiquadFilterType = + | 'lowpass' + | 'highpass' + | 'bandpass' + | 'lowshelf' + | 'highshelf' + | 'peaking' + | 'notch' + | 'allpass'; + +export type WindowType = 'blackman' | 'hann'; + +export type OscillatorType = + | 'sine' + | 'square' + | 'sawtooth' + | 'triangle' + | 'custom'; diff --git a/packages/react-native-audio-api/src/types/typeUtils.ts b/packages/react-native-audio-api/src/types/typeUtils.ts new file mode 100644 index 000000000..0bfae6caf --- /dev/null +++ b/packages/react-native-audio-api/src/types/typeUtils.ts @@ -0,0 +1,178 @@ +type Ambiguous = W | N; +type Platform = 'web' | 'native'; +type FromAmbiguous

= + A extends Ambiguous ? (P extends 'web' ? W : N) : never; + +// type utils +type CommonKeys = { + [K in keyof W & keyof N]: [W[K]] extends [N[K]] + ? [N[K]] extends [W[K]] + ? K + : never + : never; +}[keyof W & keyof N]; + +export type CommonPart = Pick> & + Pick>; + +interface MobileHaluluInterface { + name: string; + age: number; + isCute: boolean; + sayHello: () => string; + play: (toy: string) => string; + sleep: () => void; + eat: (food: string) => void; + playWith: (friend: MobileHaluluInterface) => string; + makeFriend: (friend: MobileHaluluInterface) => string; + getFriends: () => MobileHaluluInterface[]; + birthday: () => void; + getAgeInDogYears: () => number; + setName: (name: string) => void; + setAge: (age: number) => void; + setIsCute: (isCute: boolean) => void; +} + +interface WebHaluluInterface { + name: string; + age: number; + sayHello: () => string; + play: (toy: string) => string; + eat: (food: string) => void; + playWith: (friend: WebHaluluInterface) => string; + makeFriend: (friend: WebHaluluInterface) => string; + getFriends: () => WebHaluluInterface[]; + birthday: () => void; + getAgeInDogYears: () => number; + setName: (name: string) => void; + setAge: (age: number) => void; + fetchFromServer: (url: string) => Promise; + saveToServer: (url: string) => Promise; +} + +type AHalulu = Ambiguous; +type CHalulu = CommonPart; + +type Halulu = AHalulu & CHalulu; + +interface IHalulu { + name: string; + age: number; + isCute: boolean; + sayHello: () => string; + play: (toy: string) => string; + sleep: () => void; + eat: (food: string) => void; + playWith: (friend: MobileHaluluInterface) => string; + makeFriend: (friend: MobileHaluluInterface) => string; + getFriends: () => MobileHaluluInterface[]; + birthday: () => void; + getAgeInDogYears: () => number; + setName: (name: string) => void; + setAge: (age: number) => void; + setIsCute: (isCute: boolean) => void; + fetchFromServer: (url: string) => Promise; + saveToServer: (url: string) => Promise; +} + +type SelfWeb = FromAmbiguous<'web', T>; + +export class HaluluWeb implements IHalulu { + private internalHalulu: Halulu; + + constructor(internalHalulu: Halulu) { + this.internalHalulu = internalHalulu; + } + + public get name(): string { + return this.internalHalulu.name; + } + + public get age(): number { + return this.internalHalulu.age; + } + + public get isCute(): boolean { + return false; + } + + public birthday(): void {} + + public getAgeInDogYears(): number { + return this.internalHalulu.getAgeInDogYears(); + } + + public setIsCute(_isCute: boolean): void { + console.warn('isCute is not available on web'); + } + + public setName(name: string): void { + this.internalHalulu.setName(name); + } + + public setAge(age: number): void { + this.internalHalulu.setAge(age); + } + + public getFriends(): MobileHaluluInterface[] { + return []; + } + + public sayHello(): string { + return this.internalHalulu.sayHello(); + } + + public play(toy: string): string { + return this.internalHalulu.play(toy); + } + + public sleep(): void { + console.warn('HaluluWeb cannot sleep'); + } + + public eat(food: string): void { + return this.internalHalulu.eat(food); + } + + public playWith(friend: Halulu): string { + const self = this.internalHalulu as SelfWeb; + const other = (friend as unknown as HaluluWeb) + .internalHalulu as SelfWeb; + + return self.playWith(other as unknown as AHalulu); + } + + public makeFriend(friend: Halulu): string { + return this.internalHalulu.makeFriend(friend.internalHalulu); + } + + public fetchFromServer(url: string): Promise { + return (this.internalHalulu as WebHaluluInterface).fetchFromServer(url); + } + + public saveToServer(url: string): Promise { + return (this.internalHalulu as WebHaluluInterface).saveToServer(url); + } +} + +export class HaluluMobile implements IHalulu { + private internalHalulu: Halulu; + + constructor(internalHalulu: Halulu) { + this.internalHalulu = internalHalulu; + } + + public getName(): string { + return this.internalHalulu.name; + } + + public makeFriend(friend: HaluluMobile): string { + return this.internalHalulu.makeFriend( + friend.internalHalulu as MobileHaluluInterface + ); + } + + saveToServer(_url: string): Promise { + throw new Error('saveToServer is not available on mobile'); + } +} diff --git a/packages/react-native-audio-api/src/utils/availabilityWarn.ts b/packages/react-native-audio-api/src/utils/availabilityWarn.ts new file mode 100644 index 000000000..bb554569c --- /dev/null +++ b/packages/react-native-audio-api/src/utils/availabilityWarn.ts @@ -0,0 +1,16 @@ +import makeDocLink from './makeDocLink'; + +export default function availabilityWarn( + feature: string, + platform: 'web' | 'native' = 'web', + docPath?: string +) { + const baseMsg = `The ${feature} is not available on ${platform} platform.`; + + if (!docPath) { + console.warn(baseMsg); + return; + } + + console.warn(`${baseMsg} See ${makeDocLink(docPath)} for more information.`); +} diff --git a/packages/react-native-audio-api/src/utils/constants.ts b/packages/react-native-audio-api/src/utils/constants.ts new file mode 100644 index 000000000..92f0b8f8e --- /dev/null +++ b/packages/react-native-audio-api/src/utils/constants.ts @@ -0,0 +1 @@ +export const baseUrl = 'https://docs.swmansion.com/react-native-audio-api'; diff --git a/packages/react-native-audio-api/src/utils/index.ts b/packages/react-native-audio-api/src/utils/index.ts index d2dc4143f..9c8799c90 100644 --- a/packages/react-native-audio-api/src/utils/index.ts +++ b/packages/react-native-audio-api/src/utils/index.ts @@ -1,24 +1,6 @@ -import type { ShareableWorkletCallback } from '../interfaces'; +import * as constants from './constants'; -interface SimplifiedWorkletModule { - makeShareableCloneRecursive: ( - workletCallback: ShareableWorkletCallback - ) => ShareableWorkletCallback; +export { default as availabilityWarn } from './availabilityWarn'; +export { default as makeDocLink } from './makeDocLink'; - // eslint-disable-next-line @typescript-eslint/no-explicit-any - createWorkletRuntime: (options?: any) => any; -} - -export function clamp(value: number, min: number, max: number): number { - return Math.min(Math.max(value, min), max); -} - -export let isWorkletsAvailable = false; -export let workletsModule: SimplifiedWorkletModule; - -try { - workletsModule = require('react-native-worklets'); - isWorkletsAvailable = true; -} catch (error) { - isWorkletsAvailable = false; -} +export { constants }; diff --git a/packages/react-native-audio-api/src/utils/makeDocLink.ts b/packages/react-native-audio-api/src/utils/makeDocLink.ts new file mode 100644 index 000000000..454d0b165 --- /dev/null +++ b/packages/react-native-audio-api/src/utils/makeDocLink.ts @@ -0,0 +1,5 @@ +import { baseUrl } from './constants'; + +export default function makeDocLink(path: string) { + return `${baseUrl}${path}`; +} diff --git a/packages/react-native-audio-api/src/web-core/AnalyserNode.tsx b/packages/react-native-audio-api/src/web-core/AnalyserNode.tsx deleted file mode 100644 index c74b67e05..000000000 --- a/packages/react-native-audio-api/src/web-core/AnalyserNode.tsx +++ /dev/null @@ -1,47 +0,0 @@ -import AudioNode from './AudioNode'; -import { WindowType } from '../types'; -import BaseAudioContext from './BaseAudioContext'; - -export default class AnalyserNode extends AudioNode { - fftSize: number; - readonly frequencyBinCount: number; - minDecibels: number; - maxDecibels: number; - smoothingTimeConstant: number; - - constructor(context: BaseAudioContext, node: globalThis.AnalyserNode) { - super(context, node); - - this.fftSize = node.fftSize; - this.frequencyBinCount = node.frequencyBinCount; - this.minDecibels = node.minDecibels; - this.maxDecibels = node.maxDecibels; - this.smoothingTimeConstant = node.smoothingTimeConstant; - } - - public get window(): WindowType { - return 'blackman'; - } - - public set window(value: WindowType) { - console.log( - 'React Native Audio API: setting window is not supported on web' - ); - } - - public getByteFrequencyData(array: Uint8Array): void { - (this.node as globalThis.AnalyserNode).getByteFrequencyData(array); - } - - public getByteTimeDomainData(array: Uint8Array): void { - (this.node as globalThis.AnalyserNode).getByteTimeDomainData(array); - } - - public getFloatFrequencyData(array: Float32Array): void { - (this.node as globalThis.AnalyserNode).getFloatFrequencyData(array); - } - - public getFloatTimeDomainData(array: Float32Array): void { - (this.node as globalThis.AnalyserNode).getFloatTimeDomainData(array); - } -} diff --git a/packages/react-native-audio-api/src/web-core/AudioBuffer.tsx b/packages/react-native-audio-api/src/web-core/AudioBuffer.tsx deleted file mode 100644 index d007a6dd4..000000000 --- a/packages/react-native-audio-api/src/web-core/AudioBuffer.tsx +++ /dev/null @@ -1,69 +0,0 @@ -import { IndexSizeError } from '../errors'; - -export default class AudioBuffer { - readonly length: number; - readonly duration: number; - readonly sampleRate: number; - readonly numberOfChannels: number; - - /** @internal */ - public readonly buffer: globalThis.AudioBuffer; - - constructor(buffer: globalThis.AudioBuffer) { - this.buffer = buffer; - this.length = buffer.length; - this.duration = buffer.duration; - this.sampleRate = buffer.sampleRate; - this.numberOfChannels = buffer.numberOfChannels; - } - - public getChannelData(channel: number): Float32Array { - if (channel < 0 || channel >= this.numberOfChannels) { - throw new IndexSizeError( - `The channel number provided (${channel}) is outside the range [0, ${this.numberOfChannels - 1}]` - ); - } - - return this.buffer.getChannelData(channel); - } - - public copyFromChannel( - destination: Float32Array, - channelNumber: number, - startInChannel: number = 0 - ): void { - if (channelNumber < 0 || channelNumber >= this.numberOfChannels) { - throw new IndexSizeError( - `The channel number provided (${channelNumber}) is outside the range [0, ${this.numberOfChannels - 1}]` - ); - } - - if (startInChannel < 0 || startInChannel >= this.length) { - throw new IndexSizeError( - `The startInChannel number provided (${startInChannel}) is outside the range [0, ${this.length - 1}]` - ); - } - - this.buffer.copyFromChannel(destination, channelNumber, startInChannel); - } - - public copyToChannel( - source: Float32Array, - channelNumber: number, - startInChannel: number = 0 - ): void { - if (channelNumber < 0 || channelNumber >= this.numberOfChannels) { - throw new IndexSizeError( - `The channel number provided (${channelNumber}) is outside the range [0, ${this.numberOfChannels - 1}]` - ); - } - - if (startInChannel < 0 || startInChannel >= this.length) { - throw new IndexSizeError( - `The startInChannel number provided (${startInChannel}) is outside the range [0, ${this.length - 1}]` - ); - } - - this.buffer.copyToChannel(source, channelNumber, startInChannel); - } -} diff --git a/packages/react-native-audio-api/src/web-core/AudioBufferSourceNode.tsx b/packages/react-native-audio-api/src/web-core/AudioBufferSourceNode.tsx deleted file mode 100644 index 4ca212030..000000000 --- a/packages/react-native-audio-api/src/web-core/AudioBufferSourceNode.tsx +++ /dev/null @@ -1,436 +0,0 @@ -import { InvalidStateError, RangeError } from '../errors'; - -import AudioParam from './AudioParam'; -import AudioBuffer from './AudioBuffer'; -import BaseAudioContext from './BaseAudioContext'; -import AudioScheduledSourceNode from './AudioScheduledSourceNode'; - -import { clamp } from '../utils'; -import { globalTag } from './custom/LoadCustomWasm'; - -interface ScheduleOptions { - rate?: number; - active?: boolean; - output?: number; - input?: number; - semitones?: number; - loopStart?: number; - loopEnd?: number; -} - -interface IStretcherNode extends globalThis.AudioNode { - channelCount: number; - channelCountMode: globalThis.ChannelCountMode; - channelInterpretation: globalThis.ChannelInterpretation; - context: globalThis.BaseAudioContext; - numberOfInputs: number; - numberOfOutputs: number; - - onEnded: - | ((this: globalThis.AudioScheduledSourceNode, ev: Event) => unknown) - | null; - addEventListener: ( - type: string, - listener: EventListenerOrEventListenerObject | null, - options?: boolean | AddEventListenerOptions | undefined - ) => void; - dispatchEvent: (event: Event) => boolean; - removeEventListener: ( - type: string, - callback: EventListenerOrEventListenerObject | null, - options?: boolean | EventListenerOptions | undefined - ) => void; - - addBuffers(channels: Float32Array[]): void; - dropBuffers(): void; - - schedule(options: ScheduleOptions): void; - - start( - when?: number, - offset?: number, - duration?: number, - rate?: number, - semitones?: number - ): void; - - stop(when?: number): void; - - connect( - destination: globalThis.AudioNode, - output?: number, - input?: number - ): globalThis.AudioNode; - connect(destination: globalThis.AudioParam, output?: number): void; - - disconnect(): void; - disconnect(output: number): void; - - disconnect(destination: globalThis.AudioNode): globalThis.AudioNode; - disconnect(destination: globalThis.AudioNode, output: number): void; - disconnect( - destination: globalThis.AudioNode, - output: number, - input: number - ): void; - - disconnect(destination: globalThis.AudioParam): void; - disconnect(destination: globalThis.AudioParam, output: number): void; -} - -class IStretcherNodeAudioParam implements globalThis.AudioParam { - private _value: number; - private _setter: (value: number, when?: number) => void; - - public automationRate: AutomationRate; - public defaultValue: number; - public maxValue: number; - public minValue: number; - - constructor( - value: number, - setter: (value: number, when?: number) => void, - automationRate: AutomationRate, - minValue: number, - maxValue: number, - defaultValue: number - ) { - this._value = value; - this.automationRate = automationRate; - this.minValue = minValue; - this.maxValue = maxValue; - this.defaultValue = defaultValue; - this._setter = setter; - } - - public get value(): number { - return this._value; - } - - public set value(value: number) { - this._value = value; - - this._setter(value); - } - - cancelAndHoldAtTime(cancelTime: number): globalThis.AudioParam { - this._setter(this.defaultValue, cancelTime); - return this; - } - - cancelScheduledValues(cancelTime: number): globalThis.AudioParam { - this._setter(this.defaultValue, cancelTime); - return this; - } - - exponentialRampToValueAtTime( - _value: number, - _endTime: number - ): globalThis.AudioParam { - console.warn( - 'exponentialRampToValueAtTime is not implemented for pitch correction mode' - ); - return this; - } - - linearRampToValueAtTime( - _value: number, - _endTime: number - ): globalThis.AudioParam { - console.warn( - 'linearRampToValueAtTime is not implemented for pitch correction mode' - ); - return this; - } - - setTargetAtTime( - _target: number, - _startTime: number, - _timeConstant: number - ): globalThis.AudioParam { - console.warn( - 'setTargetAtTime is not implemented for pitch correction mode' - ); - return this; - } - - setValueAtTime(value: number, startTime: number): globalThis.AudioParam { - this._setter(value, startTime); - return this; - } - - setValueCurveAtTime( - _values: Float32Array, - _startTime: number, - _duration: number - ): globalThis.AudioParam { - console.warn( - 'setValueCurveAtTime is not implemented for pitch correction mode' - ); - return this; - } -} - -type DefaultSource = globalThis.AudioBufferSourceNode; - -type IAudioBufferSourceNode = DefaultSource | IStretcherNode; - -declare global { - interface Window { - [globalTag]: ( - audioContext: globalThis.BaseAudioContext - ) => Promise; - } -} - -export default class AudioBufferSourceNode< - T extends IAudioBufferSourceNode = DefaultSource, -> extends AudioScheduledSourceNode { - private _pitchCorrection: boolean; - readonly playbackRate: AudioParam; - readonly detune: AudioParam; - - private _loop: boolean = false; - private _loopStart: number = -1; - private _loopEnd: number = -1; - - private _buffer: AudioBuffer | null = null; - - constructor(context: BaseAudioContext, node: T, pitchCorrection: boolean) { - super(context, node); - - this._pitchCorrection = pitchCorrection; - - if (pitchCorrection) { - this.detune = new AudioParam( - new IStretcherNodeAudioParam( - 0, - this.setDetune.bind(this), - 'a-rate', - -1200, - 1200, - 0 - ), - context - ); - - this.playbackRate = new AudioParam( - new IStretcherNodeAudioParam( - 1, - this.setPlaybackRate.bind(this), - 'a-rate', - 0, - Infinity, - 1 - ), - context - ); - } else { - this.detune = new AudioParam((node as DefaultSource).detune, context); - this.playbackRate = new AudioParam( - (node as DefaultSource).playbackRate, - context - ); - } - } - - private isStretcherNode() { - return this._pitchCorrection; - } - - private asStretcher(): IStretcherNode { - return this.node as IStretcherNode; - } - - private asBufferSource(): DefaultSource { - return this.node as DefaultSource; - } - - public setDetune(value: number, when: number = 0): void { - if (!this.isStretcherNode() || !this.hasBeenStarted) { - return; - } - - this.asStretcher().schedule({ - semitones: Math.floor(clamp(value / 100, -12, 12)), - output: when, - }); - } - - public setPlaybackRate(value: number, when: number = 0): void { - if (!this.isStretcherNode() || !this.hasBeenStarted) { - return; - } - - this.asStretcher().schedule({ - rate: value, - output: when, - }); - } - - public get buffer(): AudioBuffer | null { - if (this.isStretcherNode()) { - return this._buffer; - } - - const buffer = this.asBufferSource().buffer; - - if (!buffer) { - return null; - } - - return new AudioBuffer(buffer); - } - - public set buffer(buffer: AudioBuffer | null) { - if (this.isStretcherNode()) { - this._buffer = buffer; - - const stretcher = this.asStretcher(); - stretcher.dropBuffers(); - - if (!buffer) { - return; - } - - const channelArrays: Float32Array[] = []; - - for (let i = 0; i < buffer.numberOfChannels; i++) { - channelArrays.push(buffer.getChannelData(i)); - } - - stretcher.addBuffers(channelArrays); - return; - } - - if (!buffer) { - this.asBufferSource().buffer = null; - return; - } - - this.asBufferSource().buffer = buffer.buffer; - } - - public get loop(): boolean { - if (this.isStretcherNode()) { - return this._loop; - } - - return this.asBufferSource().loop; - } - - public set loop(value: boolean) { - if (this.isStretcherNode()) { - this._loop = value; - return; - } - - this.asBufferSource().loop = value; - } - - public get loopStart(): number { - if (this.isStretcherNode()) { - return this._loopStart; - } - - return this.asBufferSource().loopStart; - } - - public set loopStart(value: number) { - if (this.isStretcherNode()) { - this._loopStart = value; - return; - } - - this.asBufferSource().loopStart = value; - } - - public get loopEnd(): number { - if (this.isStretcherNode()) { - return this._loopEnd; - } - - return this.asBufferSource().loopEnd; - } - - public set loopEnd(value: number) { - if (this.isStretcherNode()) { - this._loopEnd = value; - return; - } - - this.asBufferSource().loopEnd = value; - } - - public start(when?: number, offset?: number, duration?: number): void { - if (when && when < 0) { - throw new RangeError( - `when must be a finite non-negative number: ${when}` - ); - } - - if (offset && offset < 0) { - throw new RangeError( - `offset must be a finite non-negative number: ${offset}` - ); - } - - if (duration && duration < 0) { - throw new RangeError( - `duration must be a finite non-negative number: ${duration}` - ); - } - - if (this.hasBeenStarted) { - throw new InvalidStateError('Cannot call start more than once'); - } - - this.hasBeenStarted = true; - - if (!this.isStretcherNode()) { - this.asBufferSource().start(when, offset, duration); - return; - } - - const startAt = - !when || when < this.context.currentTime - ? this.context.currentTime - : when; - - if (this.loop && this._loopStart !== -1 && this._loopEnd !== -1) { - this.asStretcher().schedule({ - loopStart: this._loopStart, - loopEnd: this._loopEnd, - }); - } - - this.asStretcher().start( - startAt, - offset, - duration, - this.playbackRate.value, - Math.floor(clamp(this.detune.value / 100, -12, 12)) - ); - } - - public stop(when: number = 0): void { - if (when < 0) { - throw new RangeError( - `when must be a finite non-negative number: ${when}` - ); - } - - if (!this.hasBeenStarted) { - throw new InvalidStateError( - 'Cannot call stop without calling start first' - ); - } - - if (!this.isStretcherNode()) { - this.asBufferSource().stop(when); - return; - } - - this.asStretcher().stop(when); - } -} diff --git a/packages/react-native-audio-api/src/web-core/AudioDestinationNode.tsx b/packages/react-native-audio-api/src/web-core/AudioDestinationNode.tsx deleted file mode 100644 index f1d850e90..000000000 --- a/packages/react-native-audio-api/src/web-core/AudioDestinationNode.tsx +++ /dev/null @@ -1,3 +0,0 @@ -import AudioNode from './AudioNode'; - -export default class AudioDestinationNode extends AudioNode {} diff --git a/packages/react-native-audio-api/src/web-core/AudioNode.tsx b/packages/react-native-audio-api/src/web-core/AudioNode.tsx deleted file mode 100644 index a66113f1f..000000000 --- a/packages/react-native-audio-api/src/web-core/AudioNode.tsx +++ /dev/null @@ -1,49 +0,0 @@ -import BaseAudioContext from './BaseAudioContext'; -import { ChannelCountMode, ChannelInterpretation } from '../types'; -import AudioParam from './AudioParam'; - -export default class AudioNode { - readonly context: BaseAudioContext; - readonly numberOfInputs: number; - readonly numberOfOutputs: number; - readonly channelCount: number; - readonly channelCountMode: ChannelCountMode; - readonly channelInterpretation: ChannelInterpretation; - - protected readonly node: globalThis.AudioNode; - - constructor(context: BaseAudioContext, node: globalThis.AudioNode) { - this.context = context; - this.node = node; - this.numberOfInputs = this.node.numberOfInputs; - this.numberOfOutputs = this.node.numberOfOutputs; - this.channelCount = this.node.channelCount; - this.channelCountMode = this.node.channelCountMode; - this.channelInterpretation = this.node.channelInterpretation; - } - - public connect(destination: AudioNode | AudioParam): AudioNode | AudioParam { - if (this.context !== destination.context) { - throw new Error( - 'Source and destination are from different BaseAudioContexts' - ); - } - - if (destination instanceof AudioParam) { - this.node.connect(destination.param); - } else { - this.node.connect(destination.node); - } - - return destination; - } - - public disconnect(destination?: AudioNode): void { - if (destination === undefined) { - this.node.disconnect(); - return; - } - - this.node.disconnect(destination.node); - } -} diff --git a/packages/react-native-audio-api/src/web-core/AudioParam.tsx b/packages/react-native-audio-api/src/web-core/AudioParam.tsx deleted file mode 100644 index 341f59e55..000000000 --- a/packages/react-native-audio-api/src/web-core/AudioParam.tsx +++ /dev/null @@ -1,138 +0,0 @@ -import { RangeError, InvalidStateError } from '../errors'; -import BaseAudioContext from './BaseAudioContext'; - -export default class AudioParam { - readonly defaultValue: number; - readonly minValue: number; - readonly maxValue: number; - readonly context: BaseAudioContext; - - readonly param: globalThis.AudioParam; - - constructor(param: globalThis.AudioParam, context: BaseAudioContext) { - this.param = param; - this.defaultValue = param.defaultValue; - this.minValue = param.minValue; - this.maxValue = param.maxValue; - this.context = context; - } - - public get value(): number { - return this.param.value; - } - - public set value(value: number) { - this.param.value = value; - } - - public setValueAtTime(value: number, startTime: number): AudioParam { - if (startTime < 0) { - throw new RangeError( - `startTime must be a finite non-negative number: ${startTime}` - ); - } - - this.param.setValueAtTime(value, startTime); - - return this; - } - - public linearRampToValueAtTime(value: number, endTime: number): AudioParam { - if (endTime < 0) { - throw new RangeError( - `endTime must be a finite non-negative number: ${endTime}` - ); - } - - this.param.linearRampToValueAtTime(value, endTime); - - return this; - } - - public exponentialRampToValueAtTime( - value: number, - endTime: number - ): AudioParam { - if (endTime < 0) { - throw new RangeError( - `endTime must be a finite non-negative number: ${endTime}` - ); - } - - this.param.exponentialRampToValueAtTime(value, endTime); - - return this; - } - - public setTargetAtTime( - target: number, - startTime: number, - timeConstant: number - ): AudioParam { - if (startTime < 0) { - throw new RangeError( - `startTime must be a finite non-negative number: ${startTime}` - ); - } - - if (timeConstant < 0) { - throw new RangeError( - `timeConstant must be a finite non-negative number: ${startTime}` - ); - } - - this.param.setTargetAtTime(target, startTime, timeConstant); - - return this; - } - - public setValueCurveAtTime( - values: Float32Array, - startTime: number, - duration: number - ): AudioParam { - if (startTime < 0) { - throw new RangeError( - `startTime must be a finite non-negative number: ${startTime}` - ); - } - - if (duration < 0) { - throw new RangeError( - `duration must be a finite non-negative number: ${startTime}` - ); - } - - if (values.length < 2) { - throw new InvalidStateError(`values must contain at least two values`); - } - - this.param.setValueCurveAtTime(values, startTime, duration); - - return this; - } - - public cancelScheduledValues(cancelTime: number): AudioParam { - if (cancelTime < 0) { - throw new RangeError( - `cancelTime must be a finite non-negative number: ${cancelTime}` - ); - } - - this.param.cancelScheduledValues(cancelTime); - - return this; - } - - public cancelAndHoldAtTime(cancelTime: number): AudioParam { - if (cancelTime < 0) { - throw new RangeError( - `cancelTime must be a finite non-negative number: ${cancelTime}` - ); - } - - this.param.cancelAndHoldAtTime(cancelTime); - - return this; - } -} diff --git a/packages/react-native-audio-api/src/web-core/AudioScheduledSourceNode.tsx b/packages/react-native-audio-api/src/web-core/AudioScheduledSourceNode.tsx deleted file mode 100644 index 343d2cbf9..000000000 --- a/packages/react-native-audio-api/src/web-core/AudioScheduledSourceNode.tsx +++ /dev/null @@ -1,43 +0,0 @@ -import AudioNode from './AudioNode'; -import { EventEmptyType } from '../events/types'; -import { RangeError, InvalidStateError } from '../errors'; - -export default class AudioScheduledSourceNode extends AudioNode { - protected hasBeenStarted: boolean = false; - - public start(when: number = 0): void { - if (when < 0) { - throw new RangeError( - `when must be a finite non-negative number: ${when}` - ); - } - - if (this.hasBeenStarted) { - throw new InvalidStateError('Cannot call start more than once'); - } - - this.hasBeenStarted = true; - (this.node as globalThis.AudioScheduledSourceNode).start(when); - } - - public stop(when: number = 0): void { - if (when < 0) { - throw new RangeError( - `when must be a finite non-negative number: ${when}` - ); - } - - if (!this.hasBeenStarted) { - throw new InvalidStateError( - 'Cannot call stop without calling start first' - ); - } - - (this.node as globalThis.AudioScheduledSourceNode).stop(when); - } - - // eslint-disable-next-line accessor-pairs - public set onEnded(callback: (event: EventEmptyType) => void) { - (this.node as globalThis.AudioScheduledSourceNode).onended = callback; - } -} diff --git a/packages/react-native-audio-api/src/web-core/BiquadFilterNode.tsx b/packages/react-native-audio-api/src/web-core/BiquadFilterNode.tsx deleted file mode 100644 index 4a8a4df21..000000000 --- a/packages/react-native-audio-api/src/web-core/BiquadFilterNode.tsx +++ /dev/null @@ -1,52 +0,0 @@ -import AudioParam from './AudioParam'; -import AudioNode from './AudioNode'; -import BaseAudioContext from './BaseAudioContext'; -import { BiquadFilterType } from '../types'; -import { InvalidAccessError } from '../errors'; - -export default class BiquadFilterNode extends AudioNode { - readonly frequency: AudioParam; - readonly detune: AudioParam; - readonly Q: AudioParam; - readonly gain: AudioParam; - - constructor( - context: BaseAudioContext, - biquadFilter: globalThis.BiquadFilterNode - ) { - super(context, biquadFilter); - this.frequency = new AudioParam(biquadFilter.frequency, context); - this.detune = new AudioParam(biquadFilter.detune, context); - this.Q = new AudioParam(biquadFilter.Q, context); - this.gain = new AudioParam(biquadFilter.gain, context); - } - - public get type(): BiquadFilterType { - return (this.node as globalThis.BiquadFilterNode).type; - } - - public set type(value: BiquadFilterType) { - (this.node as globalThis.BiquadFilterNode).type = value; - } - - public getFrequencyResponse( - frequencyArray: Float32Array, - magResponseOutput: Float32Array, - phaseResponseOutput: Float32Array - ) { - if ( - frequencyArray.length !== magResponseOutput.length || - frequencyArray.length !== phaseResponseOutput.length - ) { - throw new InvalidAccessError( - `The lengths of the arrays are not the same frequencyArray: ${frequencyArray.length}, magResponseOutput: ${magResponseOutput.length}, phaseResponseOutput: ${phaseResponseOutput.length}` - ); - } - - (this.node as globalThis.BiquadFilterNode).getFrequencyResponse( - frequencyArray, - magResponseOutput, - phaseResponseOutput - ); - } -} diff --git a/packages/react-native-audio-api/src/web-core/GainNode.tsx b/packages/react-native-audio-api/src/web-core/GainNode.tsx deleted file mode 100644 index 601de5920..000000000 --- a/packages/react-native-audio-api/src/web-core/GainNode.tsx +++ /dev/null @@ -1,12 +0,0 @@ -import BaseAudioContext from './BaseAudioContext'; -import AudioNode from './AudioNode'; -import AudioParam from './AudioParam'; - -export default class GainNode extends AudioNode { - readonly gain: AudioParam; - - constructor(context: BaseAudioContext, gain: globalThis.GainNode) { - super(context, gain); - this.gain = new AudioParam(gain.gain, context); - } -} diff --git a/packages/react-native-audio-api/src/web-core/OscillatorNode.tsx b/packages/react-native-audio-api/src/web-core/OscillatorNode.tsx deleted file mode 100644 index 612cdd9f6..000000000 --- a/packages/react-native-audio-api/src/web-core/OscillatorNode.tsx +++ /dev/null @@ -1,36 +0,0 @@ -import { OscillatorType } from '../types'; -import { InvalidStateError } from '../errors'; -import AudioScheduledSourceNode from './AudioScheduledSourceNode'; -import BaseAudioContext from './BaseAudioContext'; -import AudioParam from './AudioParam'; -import PeriodicWave from './PeriodicWave'; - -export default class OscillatorNode extends AudioScheduledSourceNode { - readonly frequency: AudioParam; - readonly detune: AudioParam; - - constructor(context: BaseAudioContext, node: globalThis.OscillatorNode) { - super(context, node); - - this.detune = new AudioParam(node.detune, context); - this.frequency = new AudioParam(node.frequency, context); - } - - public get type(): OscillatorType { - return (this.node as globalThis.OscillatorNode).type; - } - - public set type(value: OscillatorType) { - if (value === 'custom') { - throw new InvalidStateError( - "'type' cannot be set directly to 'custom'. Use setPeriodicWave() to create a custom Oscillator type." - ); - } - - (this.node as globalThis.OscillatorNode).type = value; - } - - public setPeriodicWave(wave: PeriodicWave): void { - (this.node as globalThis.OscillatorNode).setPeriodicWave(wave.periodicWave); - } -} diff --git a/packages/react-native-audio-api/src/web-core/PeriodicWave.tsx b/packages/react-native-audio-api/src/web-core/PeriodicWave.tsx deleted file mode 100644 index bdf8979e1..000000000 --- a/packages/react-native-audio-api/src/web-core/PeriodicWave.tsx +++ /dev/null @@ -1,8 +0,0 @@ -export default class PeriodicWave { - /** @internal */ - readonly periodicWave: globalThis.PeriodicWave; - - constructor(periodicWave: globalThis.PeriodicWave) { - this.periodicWave = periodicWave; - } -} diff --git a/packages/react-native-audio-api/src/web-core/StereoPannerNode.tsx b/packages/react-native-audio-api/src/web-core/StereoPannerNode.tsx deleted file mode 100644 index 2d468a205..000000000 --- a/packages/react-native-audio-api/src/web-core/StereoPannerNode.tsx +++ /dev/null @@ -1,12 +0,0 @@ -import BaseAudioContext from './BaseAudioContext'; -import AudioNode from './AudioNode'; -import AudioParam from './AudioParam'; - -export default class StereoPannerNode extends AudioNode { - readonly pan: AudioParam; - - constructor(context: BaseAudioContext, pan: globalThis.StereoPannerNode) { - super(context, pan); - this.pan = new AudioParam(pan.pan, context); - } -} diff --git a/packages/react-native-audio-api/src/web-core/custom/LoadCustomWasm.ts b/packages/react-native-audio-api/src/web-core/custom/LoadCustomWasm.ts deleted file mode 100644 index d0a608c61..000000000 --- a/packages/react-native-audio-api/src/web-core/custom/LoadCustomWasm.ts +++ /dev/null @@ -1,39 +0,0 @@ -export const globalTag = '__rnaaCstStretch'; -const eventTitle = 'rnaaCstStretchLoaded'; - -export let globalWasmPromise: Promise | null = null; - -const LoadCustomWasm = async (pathPrefix: string = '') => { - if (typeof window === 'undefined') { - return null; - } - - if (globalWasmPromise) { - return globalWasmPromise; - } - - globalWasmPromise = new Promise((resolve) => { - const loadScript = document.createElement('script'); - document.head.appendChild(loadScript); - loadScript.type = 'module'; - - loadScript.textContent = ` - import SignalsmithStretch from '${pathPrefix}/signalsmithStretch.mjs'; - window.${globalTag} = SignalsmithStretch; - window.postMessage('${eventTitle}'); - `; - - function onScriptLoaded(event: MessageEvent) { - if (event.data !== eventTitle) { - return; - } - - resolve(); - window.removeEventListener('message', onScriptLoaded); - } - - window.addEventListener('message', onScriptLoaded); - }); -}; - -export default LoadCustomWasm; diff --git a/packages/react-native-audio-api/src/web-core/custom/index.ts b/packages/react-native-audio-api/src/web-core/custom/index.ts deleted file mode 100644 index d2a7623ca..000000000 --- a/packages/react-native-audio-api/src/web-core/custom/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { default as LoadCustomWasm } from './LoadCustomWasm'; diff --git a/packages/react-native-audio-api/tsconfig.json b/packages/react-native-audio-api/tsconfig.json index 77f5e973f..792252dad 100644 --- a/packages/react-native-audio-api/tsconfig.json +++ b/packages/react-native-audio-api/tsconfig.json @@ -1,9 +1,10 @@ { "extends": "../../tsconfig.json", "compilerOptions": { + "moduleSuffixes": [".web", ".native", ""], "paths": { "react-native-audio-api": ["./src"] - }, + } }, "include": ["src"] }